mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
fix(frontend): agent activity graph names (#11233)
## Changes 🏗️ We weren't fetching all library agents, just the first 15... to compute the agent map on the Agent Activity dropdown. We suspect that is causing some agent executions coming as `Unknown agent`. In this changes, I'm fetching all the library agents upfront ( _without blocking page load_ ) and caching them on the browser, so we have all the details to render the agent runs. This is re-used in the library as well for fast initial load on the agents list page. ## 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] First request populates cache; subsequent identical requests hit cache - [x] Editing an agent invalidates relevant cache keys and serves fresh data - [x] Different query params generate distinct cache entries - [x] Cache layer gracefully falls back to live data on errors - [x] 404 behavior for unknown agents unchanged ### For configuration changes: None
This commit is contained in:
@@ -64,7 +64,7 @@ import { useRouter, usePathname, useSearchParams } from "next/navigation";
|
||||
import RunnerUIWrapper, { RunnerUIWrapperRef } from "../RunnerUIWrapper";
|
||||
import OttoChatWidget from "@/app/(platform)/build/components/legacy-builder/OttoChatWidget";
|
||||
import { useToast } from "@/components/molecules/Toast/use-toast";
|
||||
import { useCopyPaste } from "../../../../../../hooks/useCopyPaste";
|
||||
import { useCopyPaste } from "../useCopyPaste";
|
||||
import NewControlPanel from "@/app/(platform)/build/components/NewControlPanel/NewControlPanel";
|
||||
import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag";
|
||||
import { BuildActionBar } from "../BuildActionBar";
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { getV2ListLibraryAgentsResponse } from "@/app/api/__generated__/endpoints/library/library";
|
||||
import type { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
|
||||
import { LibraryAgentResponse } from "@/app/api/__generated__/models/libraryAgentResponse";
|
||||
|
||||
export function filterAgents(agents: LibraryAgent[], term?: string | null) {
|
||||
const t = term?.trim().toLowerCase();
|
||||
if (!t) return agents;
|
||||
return agents.filter(
|
||||
(a) =>
|
||||
a.name.toLowerCase().includes(t) ||
|
||||
a.description.toLowerCase().includes(t),
|
||||
);
|
||||
}
|
||||
|
||||
export function getInitialData(
|
||||
cachedAgents: LibraryAgent[],
|
||||
searchTerm: string | null,
|
||||
pageSize: number,
|
||||
) {
|
||||
const filtered = filterAgents(
|
||||
cachedAgents as unknown as LibraryAgent[],
|
||||
searchTerm,
|
||||
);
|
||||
|
||||
if (!filtered.length) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const firstPageAgents: LibraryAgent[] = filtered.slice(0, pageSize);
|
||||
const totalItems = filtered.length;
|
||||
const totalPages = Math.max(1, Math.ceil(totalItems / pageSize));
|
||||
|
||||
const firstPage: getV2ListLibraryAgentsResponse = {
|
||||
status: 200,
|
||||
data: {
|
||||
agents: firstPageAgents,
|
||||
pagination: {
|
||||
total_items: totalItems,
|
||||
total_pages: totalPages,
|
||||
current_page: 1,
|
||||
page_size: pageSize,
|
||||
},
|
||||
} satisfies LibraryAgentResponse,
|
||||
headers: new Headers(),
|
||||
};
|
||||
|
||||
return { pageParams: [1], pages: [firstPage] };
|
||||
}
|
||||
@@ -3,9 +3,13 @@
|
||||
import { useGetV2ListLibraryAgentsInfinite } from "@/app/api/__generated__/endpoints/library/library";
|
||||
import { LibraryAgentResponse } from "@/app/api/__generated__/models/libraryAgentResponse";
|
||||
import { useLibraryPageContext } from "../state-provider";
|
||||
import { useLibraryAgentsStore } from "@/hooks/useLibraryAgents/store";
|
||||
import { getInitialData } from "./helpers";
|
||||
|
||||
export const useLibraryAgentList = () => {
|
||||
const { searchTerm, librarySort } = useLibraryPageContext();
|
||||
const { agents: cachedAgents } = useLibraryAgentsStore();
|
||||
|
||||
const {
|
||||
data: agents,
|
||||
fetchNextPage,
|
||||
@@ -21,6 +25,7 @@ export const useLibraryAgentList = () => {
|
||||
},
|
||||
{
|
||||
query: {
|
||||
initialData: getInitialData(cachedAgents, searchTerm, 8),
|
||||
getNextPageParam: (lastPage) => {
|
||||
const pagination = (lastPage.data as LibraryAgentResponse).pagination;
|
||||
const isMore =
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { usePostV2AddMarketplaceAgent } from "@/app/api/__generated__/endpoints/library/library";
|
||||
import {
|
||||
getGetV2ListLibraryAgentsQueryKey,
|
||||
usePostV2AddMarketplaceAgent,
|
||||
} from "@/app/api/__generated__/endpoints/library/library";
|
||||
import { useToast } from "@/components/molecules/Toast/use-toast";
|
||||
import { useRouter } from "next/navigation";
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
@@ -6,6 +9,7 @@ import { useGetV2DownloadAgentFile } from "@/app/api/__generated__/endpoints/sto
|
||||
import { useOnboarding } from "@/providers/onboarding/onboarding-provider";
|
||||
import { analytics } from "@/services/analytics";
|
||||
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
interface UseAgentInfoProps {
|
||||
storeListingVersionId: string;
|
||||
@@ -15,6 +19,7 @@ export const useAgentInfo = ({ storeListingVersionId }: UseAgentInfoProps) => {
|
||||
const { toast } = useToast();
|
||||
const router = useRouter();
|
||||
const { completeStep } = useOnboarding();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const {
|
||||
mutateAsync: addMarketplaceAgentToLibrary,
|
||||
@@ -46,6 +51,10 @@ export const useAgentInfo = ({ storeListingVersionId }: UseAgentInfoProps) => {
|
||||
if (isAddingAgentFirstTime) {
|
||||
completeStep("MARKETPLACE_ADD_AGENT");
|
||||
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: getGetV2ListLibraryAgentsQueryKey(),
|
||||
});
|
||||
|
||||
analytics.sendDatafastEvent("add_to_library", {
|
||||
name: data.name,
|
||||
id: data.id,
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
} from "@/app/api/__generated__/endpoints/auth/auth";
|
||||
import { SettingsForm } from "@/app/(platform)/profile/(user)/settings/components/SettingsForm/SettingsForm";
|
||||
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
|
||||
import { useTimezoneDetection } from "@/hooks/useTimezoneDetection";
|
||||
import { useTimezoneDetection } from "@/app/(platform)/profile/(user)/settings/useTimezoneDetection";
|
||||
import * as React from "react";
|
||||
import SettingsLoading from "./loading";
|
||||
import { redirect } from "next/navigation";
|
||||
@@ -28,6 +28,7 @@ export default function SettingsPage() {
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
useTimezoneDetection(timezone);
|
||||
|
||||
const { user, isUserLoading } = useSupabase();
|
||||
|
||||
@@ -1,26 +1,20 @@
|
||||
import { useGetV1ListAllExecutions } from "@/app/api/__generated__/endpoints/graphs/graphs";
|
||||
import { useGetV2ListLibraryAgents } from "@/app/api/__generated__/endpoints/library/library";
|
||||
|
||||
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 { toast } from "sonner";
|
||||
import {
|
||||
NotificationState,
|
||||
categorizeExecutions,
|
||||
handleExecutionUpdate,
|
||||
} from "./helpers";
|
||||
|
||||
type AgentInfoMap = Map<
|
||||
string,
|
||||
{ name: string; description: string; library_agent_id?: string }
|
||||
>;
|
||||
import { useLibraryAgents } from "@/hooks/useLibraryAgents/useLibraryAgents";
|
||||
|
||||
export function useAgentActivityDropdown() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const [api] = useState(() => new BackendAPI());
|
||||
const { agentInfoMap } = useLibraryAgents();
|
||||
|
||||
const [notifications, setNotifications] = useState<NotificationState>({
|
||||
activeExecutions: [],
|
||||
@@ -30,13 +24,6 @@ export function useAgentActivityDropdown() {
|
||||
});
|
||||
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
const [agentInfoMap, setAgentInfoMap] = useState<AgentInfoMap>(new Map());
|
||||
|
||||
const {
|
||||
data: agents,
|
||||
isSuccess: agentsSuccess,
|
||||
error: agentsError,
|
||||
} = useGetV2ListLibraryAgents();
|
||||
|
||||
const {
|
||||
data: executions,
|
||||
@@ -46,59 +33,6 @@ export function useAgentActivityDropdown() {
|
||||
query: { select: (res) => (res.status === 200 ? res.data : null) },
|
||||
});
|
||||
|
||||
// Create a map of library agents
|
||||
useEffect(() => {
|
||||
if (agentsError) {
|
||||
Sentry.captureException(agentsError, {
|
||||
tags: {
|
||||
context: "library_agents_fetch",
|
||||
},
|
||||
});
|
||||
toast.error("Failed to load agent information", {
|
||||
description:
|
||||
"There was a problem connecting to our servers. Agent activity may be limited.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (agents && agentsSuccess) {
|
||||
if (agents.status !== 200) {
|
||||
Sentry.captureException(new Error("Failed to load library agents"), {
|
||||
extra: {
|
||||
status: agents.status,
|
||||
error: agents.data,
|
||||
},
|
||||
});
|
||||
toast.error("Invalid agent data received", {
|
||||
description:
|
||||
"The server returned invalid data. Agent activity may be limited.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const libraryAgents = agents.data;
|
||||
|
||||
if (!libraryAgents.agents || !libraryAgents.agents.length) return;
|
||||
|
||||
const agentMap = new Map<
|
||||
string,
|
||||
{ name: string; description: string; library_agent_id?: string }
|
||||
>();
|
||||
|
||||
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);
|
||||
}
|
||||
}, [agents, agentsSuccess, agentsError]);
|
||||
|
||||
// Handle real-time execution updates
|
||||
const handleExecutionEvent = useCallback(
|
||||
(execution: GraphExecution) => {
|
||||
@@ -156,8 +90,8 @@ export function useAgentActivityDropdown() {
|
||||
return {
|
||||
...notifications,
|
||||
isConnected,
|
||||
isReady: executionsSuccess && agentsSuccess,
|
||||
error: executionsError || agentsError,
|
||||
isReady: executionsSuccess,
|
||||
error: executionsError,
|
||||
isOpen,
|
||||
setIsOpen,
|
||||
};
|
||||
|
||||
@@ -32,6 +32,8 @@ import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag";
|
||||
import { useOnboarding } from "@/providers/onboarding/onboarding-provider";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { getGetV2ListLibraryAgentsQueryKey } from "@/app/api/__generated__/endpoints/library/library";
|
||||
|
||||
export default function useAgentGraph(
|
||||
flowID?: GraphID,
|
||||
@@ -44,6 +46,7 @@ export default function useAgentGraph(
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
const api = useBackendAPI();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const [isScheduling, setIsScheduling] = useState(false);
|
||||
const [savedAgent, setSavedAgent] = useState<Graph | null>(null);
|
||||
@@ -755,6 +758,11 @@ export default function useAgentGraph(
|
||||
setIsSaving(true);
|
||||
try {
|
||||
await _saveAgent();
|
||||
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: getGetV2ListLibraryAgentsQueryKey(),
|
||||
});
|
||||
|
||||
completeStep("BUILDER_SAVE_AGENT");
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
|
||||
128
autogpt_platform/frontend/src/hooks/useLibraryAgents/store.ts
Normal file
128
autogpt_platform/frontend/src/hooks/useLibraryAgents/store.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import { create } from "zustand";
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
import { storage, Key } from "@/services/storage/local-storage";
|
||||
import {
|
||||
getV2ListLibraryAgents,
|
||||
type getV2ListLibraryAgentsResponse,
|
||||
} from "@/app/api/__generated__/endpoints/library/library";
|
||||
import type { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
|
||||
|
||||
export type AgentInfo = LibraryAgent;
|
||||
|
||||
type AgentStore = {
|
||||
agents: AgentInfo[];
|
||||
lastUpdatedAt?: number;
|
||||
isRefreshing: boolean;
|
||||
error?: unknown;
|
||||
loadFromCache: () => void;
|
||||
refreshAll: () => Promise<void>;
|
||||
};
|
||||
|
||||
type CachedAgents = {
|
||||
agents: LibraryAgent[];
|
||||
lastUpdatedAt: number;
|
||||
};
|
||||
|
||||
async function fetchAllLibraryAgents() {
|
||||
const pageSize = 100;
|
||||
let page = 1;
|
||||
const all: LibraryAgent[] = [];
|
||||
|
||||
let res: getV2ListLibraryAgentsResponse | undefined;
|
||||
try {
|
||||
res = await getV2ListLibraryAgents({ page, page_size: pageSize });
|
||||
} catch (err) {
|
||||
Sentry.captureException(err, { tags: { context: "library_agents_fetch" } });
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (!res || res.status !== 200) return all;
|
||||
|
||||
const { agents, pagination } = res.data;
|
||||
all.push(...agents);
|
||||
|
||||
const totalPages = pagination?.total_pages ?? 1;
|
||||
|
||||
for (page = 2; page <= totalPages; page += 1) {
|
||||
try {
|
||||
const next = await getV2ListLibraryAgents({ page, page_size: pageSize });
|
||||
if (next.status === 200) {
|
||||
all.push(...next.data.agents);
|
||||
}
|
||||
} catch (err) {
|
||||
Sentry.captureException(err, {
|
||||
tags: { context: "library_agents_fetch" },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return all;
|
||||
}
|
||||
|
||||
function persistCache(cached: CachedAgents) {
|
||||
try {
|
||||
storage.set(Key.LIBRARY_AGENTS_CACHE, JSON.stringify(cached));
|
||||
} catch (error) {
|
||||
// Ignore cache failures
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Failed to persist library agents cache", error);
|
||||
Sentry.captureException(error, {
|
||||
tags: { context: "library_agents_cache_persist" },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function readCache(): CachedAgents | undefined {
|
||||
try {
|
||||
const raw = storage.get(Key.LIBRARY_AGENTS_CACHE);
|
||||
if (!raw) return;
|
||||
return JSON.parse(raw) as CachedAgents;
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
export const useLibraryAgentsStore = create<AgentStore>((set, get) => ({
|
||||
agents: [],
|
||||
lastUpdatedAt: undefined,
|
||||
isRefreshing: false,
|
||||
error: undefined,
|
||||
loadFromCache: () => {
|
||||
const cached = readCache();
|
||||
if (cached?.agents?.length) {
|
||||
set({ agents: cached.agents, lastUpdatedAt: cached.lastUpdatedAt });
|
||||
}
|
||||
},
|
||||
refreshAll: async () => {
|
||||
if (get().isRefreshing) return;
|
||||
set({ isRefreshing: true, error: undefined });
|
||||
try {
|
||||
const agents = await fetchAllLibraryAgents();
|
||||
const snapshot: CachedAgents = { agents, lastUpdatedAt: Date.now() };
|
||||
persistCache(snapshot);
|
||||
set({ agents, lastUpdatedAt: snapshot.lastUpdatedAt });
|
||||
} catch (error) {
|
||||
set({ error });
|
||||
} finally {
|
||||
set({ isRefreshing: false });
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
export function buildAgentInfoMap(agents: AgentInfo[]) {
|
||||
const map = new Map<
|
||||
string,
|
||||
{ name: string; description: string; library_agent_id?: string }
|
||||
>();
|
||||
agents.forEach((a) => {
|
||||
if (a.graph_id && a.id) {
|
||||
map.set(a.graph_id, {
|
||||
name:
|
||||
a.name || (a.graph_id ? `Agent ${a.graph_id.slice(0, 8)}` : "Agent"),
|
||||
description: a.description || "",
|
||||
library_agent_id: a.id,
|
||||
});
|
||||
}
|
||||
});
|
||||
return map;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { buildAgentInfoMap, useLibraryAgentsStore } from "./store";
|
||||
|
||||
let initialized = false;
|
||||
|
||||
export function useLibraryAgents() {
|
||||
const { agents, isRefreshing, lastUpdatedAt, loadFromCache, refreshAll } =
|
||||
useLibraryAgentsStore();
|
||||
|
||||
useEffect(() => {
|
||||
if (!initialized) {
|
||||
loadFromCache();
|
||||
void refreshAll();
|
||||
initialized = true;
|
||||
}
|
||||
}, [loadFromCache, refreshAll]);
|
||||
|
||||
const agentInfoMap = useMemo(() => buildAgentInfoMap(agents), [agents]);
|
||||
|
||||
return { agents, agentInfoMap, isRefreshing, lastUpdatedAt };
|
||||
}
|
||||
@@ -7,6 +7,7 @@ export enum Key {
|
||||
COPIED_FLOW_DATA = "copied-flow-data",
|
||||
SHEPHERD_TOUR = "shepherd-tour",
|
||||
WALLET_LAST_SEEN_CREDITS = "wallet-last-seen-credits",
|
||||
LIBRARY_AGENTS_CACHE = "library-agents-cache",
|
||||
}
|
||||
|
||||
function get(key: Key) {
|
||||
|
||||
Reference in New Issue
Block a user