fix(frontend): agent activity links (2) (#10488)

## Changes 🏗️

My previous PR,
https://github.com/Significant-Gravitas/AutoGPT/pull/10480, didn't fully
resolve the issue of broken links sometimes appearing for some runs in
the Agent Activity dropdown.

- Fixed the logic ( verified with a deployment in dev... )
- Simplified logic, making less API calls
- If we have an execution without a clear agent ID, we display it but
don't link to it
- Re-generated API types ( _had to update call in dashboard agents
because of it_ )

## Checklist 📋

### For code changes:
- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [x] I have tested my changes according to the test plan:
  - [x] Run agents
- [x] Runs appear correctly in the activity dropdown without broken
links

### For configuration changes:

None
This commit is contained in:
Ubbe
2025-07-30 17:53:47 +04:00
committed by GitHub
parent 9ca75d93f7
commit e3fa8f6ce9
6 changed files with 433 additions and 672 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -27,8 +27,6 @@ import type { CreatorDetails } from "../../models/creatorDetails";
import type { CreatorsResponse } from "../../models/creatorsResponse";
import type { GetV2GetMyAgentsParams } from "../../models/getV2GetMyAgentsParams";
import type { GetV2ListMySubmissionsParams } from "../../models/getV2ListMySubmissionsParams";
import type { GetV2ListStoreAgentsParams } from "../../models/getV2ListStoreAgentsParams";
@@ -1952,78 +1950,45 @@ export type getV2GetMyAgentsResponse200 = {
status: 200;
};
export type getV2GetMyAgentsResponse422 = {
data: HTTPValidationError;
status: 422;
};
export type getV2GetMyAgentsResponseComposite =
| getV2GetMyAgentsResponse200
| getV2GetMyAgentsResponse422;
export type getV2GetMyAgentsResponseComposite = getV2GetMyAgentsResponse200;
export type getV2GetMyAgentsResponse = getV2GetMyAgentsResponseComposite & {
headers: Headers;
};
export const getGetV2GetMyAgentsUrl = (params?: GetV2GetMyAgentsParams) => {
const normalizedParams = new URLSearchParams();
Object.entries(params || {}).forEach(([key, value]) => {
if (value !== undefined) {
normalizedParams.append(key, value === null ? "null" : value.toString());
}
});
const stringifiedParams = normalizedParams.toString();
return stringifiedParams.length > 0
? `/api/store/myagents?${stringifiedParams}`
: `/api/store/myagents`;
export const getGetV2GetMyAgentsUrl = () => {
return `/api/store/myagents`;
};
export const getV2GetMyAgents = async (
params?: GetV2GetMyAgentsParams,
options?: RequestInit,
): Promise<getV2GetMyAgentsResponse> => {
return customMutator<getV2GetMyAgentsResponse>(
getGetV2GetMyAgentsUrl(params),
{
...options,
method: "GET",
},
);
return customMutator<getV2GetMyAgentsResponse>(getGetV2GetMyAgentsUrl(), {
...options,
method: "GET",
});
};
export const getGetV2GetMyAgentsQueryKey = (
params?: GetV2GetMyAgentsParams,
) => {
return [`/api/store/myagents`, ...(params ? [params] : [])] as const;
export const getGetV2GetMyAgentsQueryKey = () => {
return [`/api/store/myagents`] as const;
};
export const getGetV2GetMyAgentsQueryOptions = <
TData = Awaited<ReturnType<typeof getV2GetMyAgents>>,
TError = HTTPValidationError,
>(
params?: GetV2GetMyAgentsParams,
options?: {
query?: Partial<
UseQueryOptions<
Awaited<ReturnType<typeof getV2GetMyAgents>>,
TError,
TData
>
>;
request?: SecondParameter<typeof customMutator>;
},
) => {
TError = unknown,
>(options?: {
query?: Partial<
UseQueryOptions<Awaited<ReturnType<typeof getV2GetMyAgents>>, TError, TData>
>;
request?: SecondParameter<typeof customMutator>;
}) => {
const { query: queryOptions, request: requestOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getGetV2GetMyAgentsQueryKey(params);
const queryKey = queryOptions?.queryKey ?? getGetV2GetMyAgentsQueryKey();
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getV2GetMyAgents>>
> = ({ signal }) => getV2GetMyAgents(params, { signal, ...requestOptions });
> = ({ signal }) => getV2GetMyAgents({ signal, ...requestOptions });
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof getV2GetMyAgents>>,
@@ -2035,13 +2000,12 @@ export const getGetV2GetMyAgentsQueryOptions = <
export type GetV2GetMyAgentsQueryResult = NonNullable<
Awaited<ReturnType<typeof getV2GetMyAgents>>
>;
export type GetV2GetMyAgentsQueryError = HTTPValidationError;
export type GetV2GetMyAgentsQueryError = unknown;
export function useGetV2GetMyAgents<
TData = Awaited<ReturnType<typeof getV2GetMyAgents>>,
TError = HTTPValidationError,
TError = unknown,
>(
params: undefined | GetV2GetMyAgentsParams,
options: {
query: Partial<
UseQueryOptions<
@@ -2066,9 +2030,8 @@ export function useGetV2GetMyAgents<
};
export function useGetV2GetMyAgents<
TData = Awaited<ReturnType<typeof getV2GetMyAgents>>,
TError = HTTPValidationError,
TError = unknown,
>(
params?: GetV2GetMyAgentsParams,
options?: {
query?: Partial<
UseQueryOptions<
@@ -2093,9 +2056,8 @@ export function useGetV2GetMyAgents<
};
export function useGetV2GetMyAgents<
TData = Awaited<ReturnType<typeof getV2GetMyAgents>>,
TError = HTTPValidationError,
TError = unknown,
>(
params?: GetV2GetMyAgentsParams,
options?: {
query?: Partial<
UseQueryOptions<
@@ -2116,9 +2078,8 @@ export function useGetV2GetMyAgents<
export function useGetV2GetMyAgents<
TData = Awaited<ReturnType<typeof getV2GetMyAgents>>,
TError = HTTPValidationError,
TError = unknown,
>(
params?: GetV2GetMyAgentsParams,
options?: {
query?: Partial<
UseQueryOptions<
@@ -2133,7 +2094,7 @@ export function useGetV2GetMyAgents<
): UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
} {
const queryOptions = getGetV2GetMyAgentsQueryOptions(params, options);
const queryOptions = getGetV2GetMyAgentsQueryOptions(options);
const query = useQuery(queryOptions, queryClient) as UseQueryResult<
TData,
@@ -2150,10 +2111,9 @@ export function useGetV2GetMyAgents<
*/
export const prefetchGetV2GetMyAgentsQuery = async <
TData = Awaited<ReturnType<typeof getV2GetMyAgents>>,
TError = HTTPValidationError,
TError = unknown,
>(
queryClient: QueryClient,
params?: GetV2GetMyAgentsParams,
options?: {
query?: Partial<
UseQueryOptions<
@@ -2165,7 +2125,7 @@ export const prefetchGetV2GetMyAgentsQuery = async <
request?: SecondParameter<typeof customMutator>;
},
): Promise<QueryClient> => {
const queryOptions = getGetV2GetMyAgentsQueryOptions(params, options);
const queryOptions = getGetV2GetMyAgentsQueryOptions(options);
await queryClient.prefetchQuery(queryOptions);

View File

@@ -2467,30 +2467,6 @@
"tags": ["v2", "store", "private"],
"summary": "Get my agents",
"operationId": "getV2Get my agents",
"parameters": [
{
"name": "page",
"in": "query",
"required": false,
"schema": {
"type": "integer",
"minimum": 1,
"default": 1,
"title": "Page"
}
},
{
"name": "page_size",
"in": "query",
"required": false,
"schema": {
"type": "integer",
"minimum": 1,
"default": 20,
"title": "Page Size"
}
}
],
"responses": {
"200": {
"description": "Successful Response",
@@ -2499,14 +2475,6 @@
"schema": { "$ref": "#/components/schemas/MyAgentsResponse" }
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/HTTPValidationError" }
}
}
}
}
}

View File

@@ -75,17 +75,17 @@ export const PublishAgentPopout: React.FC<PublishAgentPopoutProps> = ({
const api = useBackendAPI();
// Use the auto-generated API hook
const { data, error, isLoading, refetch } = useGetV2GetMyAgents(
{
page: currentPage,
page_size: 20,
},
{
query: {
enabled: open, // Only fetch when the popout is open
const { data, error, isLoading, refetch } = useGetV2GetMyAgents({
request: {
params: {
page: currentPage,
page_size: 20,
},
},
);
query: {
enabled: open, // Only fetch when the popout is open
},
});
// Update allAgents when new data arrives
React.useEffect(() => {

View File

@@ -79,18 +79,11 @@ export function ActivityItem({ execution }: Props) {
return "Unknown";
}
const agentId = execution.library_agent_id || execution.graph_id;
const linkUrl = `/library/agents/${agentId}?executionId=${execution.id}`;
const withLibraryId = !!execution.library_agent_id;
const linkUrl = `/library/agents/${execution.library_agent_id}?executionId=${execution.id}`;
const withExecutionLink = execution.library_agent_id && execution.id;
if (!withLibraryId) return null;
return (
<Link
className="block cursor-pointer border-b border-slate-50 px-2 py-3 transition-colors last:border-b-0 hover:bg-lightGrey"
href={linkUrl}
role="button"
>
const content = (
<>
{/* Icon + Agent Name */}
<div className="flex items-center space-x-2">
{getStatusIcon()}
@@ -109,6 +102,20 @@ export function ActivityItem({ execution }: Props) {
{getTimeDisplay()}
</Text>
</div>
</>
);
return withExecutionLink ? (
<Link
className="block cursor-pointer border-b border-slate-50 px-2 py-3 transition-colors last:border-b-0 hover:bg-lightGrey"
href={linkUrl}
role="button"
>
{content}
</Link>
) : (
<div className="block border-b border-slate-50 px-2 py-3 last:border-b-0">
{content}
</div>
);
}

View File

@@ -1,17 +1,14 @@
import { useGetV1GetAllExecutions } from "@/app/api/__generated__/endpoints/graphs/graphs";
import { useGetV2GetMyAgents } from "@/app/api/__generated__/endpoints/store/store";
import { useGetV2ListLibraryAgents } from "@/app/api/__generated__/endpoints/library/library";
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { LibraryAgentResponse } from "@/app/api/__generated__/models/libraryAgentResponse";
import { MyAgentsResponse } from "@/app/api/__generated__/models/myAgentsResponse";
import { MyAgent } from "@/app/api/__generated__/models/myAgent";
import BackendAPI from "@/lib/autogpt-server-api/client";
import type { GraphExecution, GraphID } from "@/lib/autogpt-server-api/types";
import { useCallback, useEffect, useState } from "react";
import * as Sentry from "@sentry/nextjs";
import {
NotificationState,
categorizeExecutions,
createAgentInfoMap,
handleExecutionUpdate,
} from "./helpers";
@@ -22,80 +19,55 @@ type AgentInfoMap = Map<
export function useAgentActivityDropdown() {
const [api] = useState(() => new BackendAPI());
const [notifications, setNotifications] = useState<NotificationState>({
activeExecutions: [],
recentCompletions: [],
recentFailures: [],
totalCount: 0,
});
const [isConnected, setIsConnected] = useState(false);
const [agentInfoMap, setAgentInfoMap] = useState<AgentInfoMap>(new Map());
// Get library agents using generated hook
const {
data: myAgentsResponse,
isLoading: isAgentsLoading,
data: agents,
isSuccess: agentsSuccess,
error: agentsError,
} = useGetV2GetMyAgents(
{},
{
// Enable query by default
query: {
enabled: true,
},
},
);
} = useGetV2ListLibraryAgents();
// Get library agents data to map graph_id to library_agent_id
const {
data: libraryAgentsResponse,
isLoading: isLibraryAgentsLoading,
error: libraryAgentsError,
} = useGetV2ListLibraryAgents(
{},
{
query: {
enabled: true,
},
},
);
// Get all executions using generated hook
const {
data: executionsResponse,
isLoading: isExecutionsLoading,
data: executions,
isSuccess: executionsSuccess,
error: executionsError,
} = useGetV1GetAllExecutions({
query: {
enabled: true,
},
});
} = useGetV1GetAllExecutions();
// Update agent info map when both agent data sources change
// Create a map of library agents
useEffect(() => {
if (myAgentsResponse?.data && libraryAgentsResponse?.data) {
const myAgents = myAgentsResponse.data as MyAgentsResponse;
const libraryAgents = libraryAgentsResponse.data as LibraryAgentResponse;
if (agents && agentsSuccess) {
// SafeCast: library agents loaded
const libraryAgents = agents.data as LibraryAgentResponse;
if (myAgents?.agents && libraryAgents?.agents) {
const agentMap = createAgentInfoMap(myAgents.agents);
if (!libraryAgents.agents || !libraryAgents.agents.length) return;
libraryAgents.agents.forEach((libraryAgent: LibraryAgent) => {
if (libraryAgent.graph_id && libraryAgent.id) {
const existingInfo = agentMap.get(libraryAgent.graph_id);
if (existingInfo) {
agentMap.set(libraryAgent.graph_id, {
...existingInfo,
library_agent_id: libraryAgent.id,
});
}
}
});
const agentMap = new Map<
string,
{ name: string; description: string; library_agent_id?: string }
>();
setAgentInfoMap(agentMap);
}
libraryAgents.agents.forEach((agent) => {
if (agent.graph_id && agent.id) {
agentMap.set(agent.graph_id, {
name: agent.name || `Agent ${agent.graph_id.slice(0, 8)}`,
description: agent.description || "",
library_agent_id: agent.id,
});
}
});
setAgentInfoMap(agentMap);
}
}, [myAgentsResponse, libraryAgentsResponse]);
}, [agents, agentsSuccess]);
// Handle real-time execution updates
const handleExecutionEvent = useCallback(
@@ -109,41 +81,27 @@ export function useAgentActivityDropdown() {
// Process initial execution state when data loads
useEffect(() => {
if (
executionsResponse?.data &&
!isExecutionsLoading &&
agentInfoMap.size > 0
) {
const newNotifications = categorizeExecutions(
executionsResponse.data,
agentInfoMap,
);
setNotifications(newNotifications);
if (executions && executionsSuccess && agentInfoMap.size > 0) {
const notifications = categorizeExecutions(executions.data, agentInfoMap);
setNotifications(notifications);
}
}, [executionsResponse, isExecutionsLoading, agentInfoMap]);
}, [executions, executionsSuccess, agentInfoMap]);
// Initialize WebSocket connection for real-time updates
useEffect(() => {
if (!agentInfoMap.size) return;
const connectHandler = api.onWebSocketConnect(() => {
setIsConnected(true);
if (myAgentsResponse?.data) {
const myAgents = myAgentsResponse.data as MyAgentsResponse;
if (myAgents?.agents) {
myAgents.agents.forEach((agent: MyAgent) => {
api
.subscribeToGraphExecutions(agent.agent_id as GraphID)
.catch((error) => {
console.error(
`[AgentNotifications] Failed to subscribe to graph ${agent.agent_id}:`,
error,
);
});
agentInfoMap.forEach((_, graphId) => {
api.subscribeToGraphExecutions(graphId as GraphID).catch((error) => {
Sentry.captureException(error, {
tags: {
graphId,
},
});
}
}
});
});
});
const disconnectHandler = api.onWebSocketDisconnect(() => {
@@ -163,12 +121,12 @@ export function useAgentActivityDropdown() {
messageHandler();
api.disconnectWebSocket();
};
}, [api, handleExecutionEvent, myAgentsResponse]);
}, [api, handleExecutionEvent, agentInfoMap]);
return {
...notifications,
isConnected,
isLoading: isAgentsLoading || isExecutionsLoading || isLibraryAgentsLoading,
error: agentsError || executionsError || libraryAgentsError,
isReady: executionsSuccess && agentsSuccess,
error: executionsError || agentsError,
};
}