import { useEffect, useMemo, useState } from "react"; import { createPipeline, deleteGraphConfig, getGraphConfig, getGraphDefaultConfig, listAvailableGraphs, listGraphConfigs, listPipelines, stopPipeline, upsertGraphConfig, } from "./api/frontApis"; import type { GraphConfigListItem, GraphConfigReadResponse, PipelineRunInfo, } from "./types"; type EditableAgent = { id: string; isDraft: boolean; graphId: string; pipelineId: string; promptSetId?: string; toolKeys: string[]; prompts: Record; port: number; llmName: string; }; const DEFAULT_ENTRY_POINT = "fastapi_server/server_dashscope.py"; const DEFAULT_LLM_NAME = "qwen-plus"; const DEFAULT_PORT = 8100; const GRAPH_ARCH_IMAGE_MODULES = import.meta.glob( "../assets/images/graph_arch/*.{png,jpg,jpeg,webp,gif}", { eager: true, import: "default" } ) as Record; const FALLBACK_PROMPTS_BY_GRAPH: Record> = { routing: { route_prompt: "", chat_prompt: "", tool_prompt: "", }, react: { sys_prompt: "", }, }; function makeAgentKey(pipelineId: string, promptSetId: string): string { return `${pipelineId}::${promptSetId}`; } function parseToolCsv(value: string): string[] { const out: string[] = []; const seen = new Set(); for (const token of value.split(",")) { const trimmed = token.trim(); if (!trimmed || seen.has(trimmed)) { continue; } seen.add(trimmed); out.push(trimmed); } return out; } function getGraphArchImage(graphId: string): string | null { const normalizedGraphId = graphId.trim().toLowerCase(); for (const [path, source] of Object.entries(GRAPH_ARCH_IMAGE_MODULES)) { const fileName = path.split("/").pop() || ""; const baseName = fileName.split(".")[0]?.toLowerCase() || ""; if (baseName === normalizedGraphId) { return source; } } return null; } function toEditable( config: GraphConfigReadResponse, draft: boolean ): EditableAgent { return { id: draft ? `draft-${Date.now()}-${Math.random().toString(36).slice(2, 6)}` : makeAgentKey(config.pipeline_id, config.prompt_set_id), isDraft: draft, graphId: config.graph_id || config.pipeline_id, pipelineId: config.pipeline_id, promptSetId: config.prompt_set_id, toolKeys: config.tool_keys || [], prompts: config.prompt_dict || {}, port: DEFAULT_PORT, llmName: DEFAULT_LLM_NAME, }; } export default function App() { const [graphs, setGraphs] = useState([]); const [configItems, setConfigItems] = useState([]); const [running, setRunning] = useState([]); const [draftAgents, setDraftAgents] = useState([]); const [selectedId, setSelectedId] = useState(null); const [editor, setEditor] = useState(null); const [statusMessage, setStatusMessage] = useState(""); const [busy, setBusy] = useState(false); const configKeySet = useMemo( () => new Set(configItems.map((x) => makeAgentKey(x.pipeline_id, x.prompt_set_id))), [configItems] ); const visibleConfigItems = useMemo( () => configItems.filter((item) => { // Hide the pre-seeded template entries (pipeline_id === graph_id, name "default") if ( item.name.toLowerCase() === "default" && item.graph_id && item.pipeline_id === item.graph_id ) { return false; } return true; }), [configItems] ); const selectedRuns = useMemo(() => { if (!editor?.pipelineId) { return []; } return running.filter((run) => { if (run.pipeline_id !== editor.pipelineId) { return false; } if (!editor.promptSetId) { return true; } return run.prompt_set_id === editor.promptSetId; }); }, [editor, running]); async function refreshConfigs(): Promise { const resp = await listGraphConfigs(); setConfigItems(resp.items); } async function refreshRunning(): Promise { const resp = await listPipelines(); setRunning(resp.items); } async function bootstrap(): Promise { setBusy(true); setStatusMessage("Loading graphs and agent configs..."); try { const [graphResp, configResp, runsResp] = await Promise.all([ listAvailableGraphs(), listGraphConfigs(), listPipelines(), ]); setGraphs(graphResp.available_graphs || []); setConfigItems(configResp.items || []); setRunning(runsResp.items || []); setStatusMessage(""); } catch (error) { setStatusMessage((error as Error).message); } finally { setBusy(false); } } useEffect(() => { bootstrap(); const timer = setInterval(() => { refreshRunning().catch(() => undefined); }, 5000); return () => clearInterval(timer); }, []); useEffect(() => { if (selectedId && !selectedId.startsWith("draft-") && !configKeySet.has(selectedId)) { setSelectedId(null); setEditor(null); } }, [selectedId, configKeySet]); async function selectExisting(item: GraphConfigListItem): Promise { const id = makeAgentKey(item.pipeline_id, item.prompt_set_id); setSelectedId(id); setBusy(true); setStatusMessage("Loading agent details..."); try { const detail = await getGraphConfig(item.pipeline_id, item.prompt_set_id); const editable = toEditable(detail, false); editable.id = id; editable.port = editor?.pipelineId === editable.pipelineId ? editor.port : DEFAULT_PORT; editable.llmName = editor?.pipelineId === editable.pipelineId ? editor.llmName : DEFAULT_LLM_NAME; setEditor(editable); setStatusMessage(""); } catch (error) { setStatusMessage((error as Error).message); } finally { setBusy(false); } } async function addDraftAgent(): Promise { const graphId = graphs[0] || "routing"; setBusy(true); setStatusMessage("Preparing new agent draft..."); try { const defaults = await loadPromptDefaults(graphId); const editable = toEditable(defaults, true); editable.graphId = graphId; editable.pipelineId = ""; editable.promptSetId = undefined; editable.id = `draft-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`; setDraftAgents((prev) => [editable, ...prev]); setEditor(editable); setSelectedId(editable.id); setStatusMessage(""); } catch (error) { setStatusMessage((error as Error).message); } finally { setBusy(false); } } async function changeGraph(graphId: string): Promise { if (!editor) { return; } setBusy(true); setStatusMessage("Loading default prompts for selected graph..."); try { const defaults = await loadPromptDefaults(graphId); setEditorAndSyncDraft((prev) => ({ ...prev, graphId, prompts: { ...defaults.prompt_dict }, toolKeys: defaults.tool_keys || [], })); setStatusMessage(""); } catch (error) { setStatusMessage((error as Error).message); } finally { setBusy(false); } } function setEditorAndSyncDraft( updater: (prev: EditableAgent) => EditableAgent ): void { setEditor((prev) => { if (!prev) { return prev; } const next = updater(prev); if (next.isDraft) { setDraftAgents((drafts) => drafts.map((draft) => (draft.id === next.id ? next : draft))); } return next; }); } function updateEditor(key: K, value: EditableAgent[K]): void { setEditorAndSyncDraft((prev) => ({ ...prev, [key]: value })); } function updatePrompt(key: string, value: string): void { setEditorAndSyncDraft((prev) => ({ ...prev, prompts: { ...prev.prompts, [key]: value, }, })); } async function loadPromptDefaults(graphId: string): Promise { try { return await getGraphDefaultConfig(graphId); } catch { const fallbackPrompts = FALLBACK_PROMPTS_BY_GRAPH[graphId] || { sys_prompt: "" }; setStatusMessage( `No backend default config found for '${graphId}'. Using built-in fallback fields.` ); return { graph_id: graphId, pipeline_id: graphId, prompt_set_id: "default", tool_keys: [], prompt_dict: fallbackPrompts, }; } } async function saveConfig(): Promise { if (!editor) { return; } const promptEntries = Object.entries(editor.prompts); if (!editor.pipelineId.trim()) { setStatusMessage("pipeline_id is required."); return; } if (!editor.graphId.trim()) { setStatusMessage("graph_id is required."); return; } if (promptEntries.length === 0) { setStatusMessage("At least one prompt field is required."); return; } if (promptEntries.some(([_, content]) => !content.trim())) { setStatusMessage("All prompt fields must be filled."); return; } setBusy(true); setStatusMessage("Saving agent config..."); try { const upsertResp = await upsertGraphConfig({ graph_id: editor.graphId, pipeline_id: editor.pipelineId.trim(), prompt_set_id: editor.promptSetId, tool_keys: editor.toolKeys, prompt_dict: editor.prompts, }); await refreshConfigs(); const detail = await getGraphConfig(upsertResp.pipeline_id, upsertResp.prompt_set_id); const saved = toEditable(detail, false); saved.id = makeAgentKey(upsertResp.pipeline_id, upsertResp.prompt_set_id); saved.port = editor.port; saved.llmName = editor.llmName; setEditor(saved); setSelectedId(saved.id); setDraftAgents((prev) => prev.filter((d) => d.id !== editor.id)); setStatusMessage("Agent config saved."); } catch (error) { setStatusMessage((error as Error).message); } finally { setBusy(false); } } async function deleteSelected(): Promise { if (!editor) { return; } if (editor.isDraft || !editor.promptSetId) { setDraftAgents((prev) => prev.filter((d) => d.id !== editor.id)); setEditor(null); setSelectedId(null); setStatusMessage("Draft deleted."); return; } setBusy(true); setStatusMessage("Deleting agent config..."); try { await deleteGraphConfig(editor.pipelineId, editor.promptSetId); await refreshConfigs(); setEditor(null); setSelectedId(null); setStatusMessage("Agent deleted."); } catch (error) { setStatusMessage((error as Error).message); } finally { setBusy(false); } } async function runSelected(): Promise { if (!editor) { return; } if (!editor.promptSetId) { setStatusMessage("Save the agent first before running."); return; } if (!editor.pipelineId.trim()) { setStatusMessage("pipeline_id is required before run."); return; } if (!Number.isInteger(editor.port) || editor.port <= 0) { setStatusMessage("port must be a positive integer."); return; } setBusy(true); setStatusMessage("Starting agent..."); try { const resp = await createPipeline({ graph_id: editor.graphId, pipeline_id: editor.pipelineId.trim(), prompt_set_id: editor.promptSetId, tool_keys: editor.toolKeys, port: editor.port, entry_point: DEFAULT_ENTRY_POINT, llm_name: editor.llmName, }); await refreshRunning(); setStatusMessage(`Agent started. URL: ${resp.url}`); } catch (error) { setStatusMessage((error as Error).message); } finally { setBusy(false); } } async function stopSelected(): Promise { if (!editor) { return; } const target = selectedRuns[0]; if (!target) { setStatusMessage("No running instance found for this agent."); return; } setBusy(true); setStatusMessage("Stopping agent..."); try { await stopPipeline(target.run_id); await refreshRunning(); setStatusMessage("Agent stopped."); } catch (error) { setStatusMessage((error as Error).message); } finally { setBusy(false); } } const rows = [ ...draftAgents.map((d) => ({ id: d.id, label: d.pipelineId || "(new agent)", graphId: d.graphId, isDraft: true, })), ...visibleConfigItems.map((item) => ({ id: makeAgentKey(item.pipeline_id, item.prompt_set_id), label: item.pipeline_id, graphId: item.graph_id || item.pipeline_id, isDraft: false, })), ]; const graphArchImage = editor ? getGraphArchImage(editor.graphId) : null; return (

Agent Configuration

{statusMessage ?

{statusMessage}

: null} {!editor ? (

Select an agent from the left or create a new one.

) : (
{graphArchImage && (

Graph Architecture

{`${editor.graphId}
)}

Prompts

{Object.keys(editor.prompts).length === 0 ? (

No prompt keys returned from backend.

) : ( Object.entries(editor.prompts).map(([key, value]) => (