From 2502fd6391e5e8efb949e28e722ecf28ca1792f9 Mon Sep 17 00:00:00 2001 From: abhi1992002 Date: Wed, 4 Feb 2026 09:30:30 +0530 Subject: [PATCH] Refactor tools in copilot-2 to utilize generated response types for improved type safety and clarity. Updated FindBlocks, FindAgents, CreateAgent, EditAgent, and RunAgent tools to leverage new API response models, enhancing maintainability and reducing redundancy in output handling. --- .../helpers/convertChatSessionToUiMessages.ts | 128 +++++++++++ .../src/app/(platform)/copilot-2/page.tsx | 44 +++- .../tools/CreateAgent/CreateAgent.tsx | 70 ++++-- .../copilot-2/tools/CreateAgent/helpers.tsx | 181 +++++++++------- .../copilot-2/tools/EditAgent/EditAgent.tsx | 70 ++++-- .../copilot-2/tools/EditAgent/helpers.tsx | 181 +++++++++------- .../copilot-2/tools/FindAgents/FindAgents.tsx | 21 +- .../copilot-2/tools/FindAgents/helpers.tsx | 85 ++++---- .../copilot-2/tools/FindBlocks/FindBlocks.tsx | 12 +- .../copilot-2/tools/FindBlocks/helpers.tsx | 38 +++- .../copilot-2/tools/RunAgent/RunAgent.tsx | 205 +++++++++++++++--- .../copilot-2/tools/RunAgent/helpers.tsx | 170 ++++++--------- .../copilot-2/tools/RunBlock/RunBlock.tsx | 183 +++++++++++++--- .../copilot-2/tools/RunBlock/helpers.tsx | 110 ++++------ .../copilot-2/tools/SearchDocs/SearchDocs.tsx | 80 ++++--- .../copilot-2/tools/SearchDocs/helpers.tsx | 117 +++++----- .../tools/ViewAgentOutput/ViewAgentOutput.tsx | 19 +- .../tools/ViewAgentOutput/helpers.tsx | 93 ++++---- 18 files changed, 1162 insertions(+), 645 deletions(-) create mode 100644 autogpt_platform/frontend/src/app/(platform)/copilot-2/helpers/convertChatSessionToUiMessages.ts diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot-2/helpers/convertChatSessionToUiMessages.ts b/autogpt_platform/frontend/src/app/(platform)/copilot-2/helpers/convertChatSessionToUiMessages.ts new file mode 100644 index 0000000000..a3f2bc28bf --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/copilot-2/helpers/convertChatSessionToUiMessages.ts @@ -0,0 +1,128 @@ +import type { UIMessage, UIDataTypes, UITools } from "ai"; + +interface SessionChatMessage { + role: string; + content: string | null; + tool_call_id: string | null; + tool_calls: unknown[] | null; +} + +function coerceSessionChatMessages( + rawMessages: unknown[], +): SessionChatMessage[] { + return rawMessages + .map((m) => { + if (!m || typeof m !== "object") return null; + const msg = m as Record; + + const role = typeof msg.role === "string" ? msg.role : null; + if (!role) return null; + + return { + role, + content: + typeof msg.content === "string" + ? msg.content + : msg.content == null + ? null + : String(msg.content), + tool_call_id: + typeof msg.tool_call_id === "string" + ? msg.tool_call_id + : msg.tool_call_id == null + ? null + : String(msg.tool_call_id), + tool_calls: Array.isArray(msg.tool_calls) ? msg.tool_calls : null, + }; + }) + .filter((m): m is SessionChatMessage => m !== null); +} + +function safeJsonParse(value: string): unknown { + try { + return JSON.parse(value) as unknown; + } catch { + return value; + } +} + +function toToolInput(rawArguments: unknown): unknown { + if (typeof rawArguments === "string") { + const trimmed = rawArguments.trim(); + return trimmed ? safeJsonParse(trimmed) : {}; + } + if (rawArguments && typeof rawArguments === "object") return rawArguments; + return {}; +} + +export function convertChatSessionMessagesToUiMessages( + sessionId: string, + rawMessages: unknown[], +): UIMessage[] { + const messages = coerceSessionChatMessages(rawMessages); + const toolOutputsByCallId = new Map(); + + for (const msg of messages) { + if (msg.role !== "tool") continue; + if (!msg.tool_call_id) continue; + if (msg.content == null) continue; + toolOutputsByCallId.set(msg.tool_call_id, msg.content); + } + + const uiMessages: UIMessage[] = []; + + messages.forEach((msg, index) => { + if (msg.role === "tool") return; + if (msg.role !== "user" && msg.role !== "assistant") return; + + const parts: UIMessage["parts"] = []; + + if (typeof msg.content === "string" && msg.content.trim()) { + parts.push({ type: "text", text: msg.content, state: "done" }); + } + + if (msg.role === "assistant" && Array.isArray(msg.tool_calls)) { + for (const rawToolCall of msg.tool_calls) { + if (!rawToolCall || typeof rawToolCall !== "object") continue; + const toolCall = rawToolCall as { + id?: unknown; + function?: { name?: unknown; arguments?: unknown }; + }; + + const toolCallId = String(toolCall.id ?? "").trim(); + const toolName = String(toolCall.function?.name ?? "").trim(); + if (!toolCallId || !toolName) continue; + + const input = toToolInput(toolCall.function?.arguments); + const output = toolOutputsByCallId.get(toolCallId); + + if (output !== undefined) { + parts.push({ + type: `tool-${toolName}`, + toolCallId, + state: "output-available", + input, + output: typeof output === "string" ? safeJsonParse(output) : output, + }); + } else { + parts.push({ + type: `tool-${toolName}`, + toolCallId, + state: "input-available", + input, + }); + } + } + } + + if (parts.length === 0) return; + + uiMessages.push({ + id: `${sessionId}-${index}`, + role: msg.role, + parts, + }); + }); + + return uiMessages; +} diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot-2/page.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot-2/page.tsx index 7069c5ab66..cb1f0f3d6d 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot-2/page.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/copilot-2/page.tsx @@ -2,18 +2,21 @@ import { useChat } from "@ai-sdk/react"; import { DefaultChatTransport } from "ai"; -import { useState, useMemo } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { parseAsString, useQueryState } from "nuqs"; import { ChatSidebar } from "./components/ChatSidebar/ChatSidebar"; import { ChatContainer } from "./components/ChatContainer/ChatContainer"; import { SidebarProvider, SidebarInset } from "@/components/ui/sidebar"; import { Button } from "@/components/ui/button"; import { CopyIcon, CheckIcon } from "@phosphor-icons/react"; +import { getV2GetSession } from "@/app/api/__generated__/endpoints/chat/chat"; +import { convertChatSessionMessagesToUiMessages } from "./helpers/convertChatSessionToUiMessages"; export default function Page() { const [input, setInput] = useState(""); const [copied, setCopied] = useState(false); const [sessionId] = useQueryState("sessionId", parseAsString); + const hydrationSeq = useRef(0); function handleCopySessionId() { if (!sessionId) return; @@ -41,11 +44,48 @@ export default function Page() { }); }, [sessionId]); - const { messages, sendMessage, status, error } = useChat({ + const { messages, sendMessage, status, error, setMessages } = useChat({ id: sessionId ?? undefined, transport: transport ?? undefined, }); + useEffect(() => { + hydrationSeq.current += 1; + const seq = hydrationSeq.current; + const controller = new AbortController(); + + if (!sessionId) { + setMessages([]); + return; + } + + const currentSessionId = sessionId; + + async function hydrate() { + try { + const response = await getV2GetSession(currentSessionId, { + signal: controller.signal, + }); + if (response.status !== 200) return; + + const uiMessages = convertChatSessionMessagesToUiMessages( + currentSessionId, + response.data.messages ?? [], + ); + if (controller.signal.aborted) return; + if (hydrationSeq.current !== seq) return; + setMessages(uiMessages); + } catch (error) { + if ((error as { name?: string } | null)?.name === "AbortError") return; + console.warn("Failed to hydrate chat session:", error); + } + } + + void hydrate(); + + return () => controller.abort(); + }, [sessionId, setMessages]); + function handleMessageSubmit(e: React.FormEvent) { e.preventDefault(); if (!input.trim() || !sessionId) return; diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/CreateAgent/CreateAgent.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/CreateAgent/CreateAgent.tsx index 54209be7dc..09192834ad 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/CreateAgent/CreateAgent.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/CreateAgent/CreateAgent.tsx @@ -5,11 +5,21 @@ import Link from "next/link"; import { MorphingTextAnimation } from "../../components/MorphingTextAnimation/MorphingTextAnimation"; import { ToolAccordion } from "../../components/ToolAccordion/ToolAccordion"; import { useCopilotChatActions } from "../../components/CopilotChatActionsProvider/useCopilotChatActions"; -import { ClarificationQuestionsWidget } from "@/components/contextual/Chat/components/ClarificationQuestionsWidget/ClarificationQuestionsWidget"; +import { + ClarificationQuestionsWidget, + type ClarifyingQuestion as WidgetClarifyingQuestion, +} from "@/components/contextual/Chat/components/ClarificationQuestionsWidget/ClarificationQuestionsWidget"; import { formatMaybeJson, getAnimationText, getCreateAgentToolOutput, + isAgentPreviewOutput, + isAgentSavedOutput, + isClarificationNeededOutput, + isErrorOutput, + isOperationInProgressOutput, + isOperationPendingOutput, + isOperationStartedOutput, StateIcon, truncateText, type CreateAgentToolOutput, @@ -32,27 +42,28 @@ function getAccordionMeta(output: CreateAgentToolOutput): { title: string; description?: string; } { - if (output.type === "agent_saved") { + if (isAgentSavedOutput(output)) { return { badgeText: "Create agent", title: output.agent_name }; } - if (output.type === "agent_preview") { + if (isAgentPreviewOutput(output)) { return { badgeText: "Create agent", title: output.agent_name, description: `${output.node_count} block${output.node_count === 1 ? "" : "s"}`, }; } - if (output.type === "clarification_needed") { + if (isClarificationNeededOutput(output)) { + const questions = output.questions ?? []; return { badgeText: "Create agent", title: "Needs clarification", - description: `${output.questions.length} question${output.questions.length === 1 ? "" : "s"}`, + description: `${questions.length} question${questions.length === 1 ? "" : "s"}`, }; } if ( - output.type === "operation_started" || - output.type === "operation_pending" || - output.type === "operation_in_progress" + isOperationStartedOutput(output) || + isOperationPendingOutput(output) || + isOperationInProgressOutput(output) ) { return { badgeText: "Create agent", title: "Creating agent" }; } @@ -67,13 +78,13 @@ export function CreateAgentTool({ part }: Props) { const hasExpandableContent = part.state === "output-available" && !!output && - (output.type === "operation_started" || - output.type === "operation_pending" || - output.type === "operation_in_progress" || - output.type === "agent_preview" || - output.type === "agent_saved" || - output.type === "clarification_needed" || - output.type === "error"); + (isOperationStartedOutput(output) || + isOperationPendingOutput(output) || + isOperationInProgressOutput(output) || + isAgentPreviewOutput(output) || + isAgentSavedOutput(output) || + isClarificationNeededOutput(output) || + isErrorOutput(output)); function handleClarificationAnswers(answers: Record) { const contextMessage = Object.entries(answers) @@ -95,10 +106,10 @@ export function CreateAgentTool({ part }: Props) { {hasExpandableContent && output && ( - {(output.type === "operation_started" || - output.type === "operation_pending") && ( + {(isOperationStartedOutput(output) || + isOperationPendingOutput(output)) && (

{output.message}

@@ -110,7 +121,7 @@ export function CreateAgentTool({ part }: Props) {

)} - {output.type === "operation_in_progress" && ( + {isOperationInProgressOutput(output) && (

{output.message}

@@ -119,7 +130,7 @@ export function CreateAgentTool({ part }: Props) {

)} - {output.type === "agent_saved" && ( + {isAgentSavedOutput(output) && (

{output.message}

@@ -145,7 +156,7 @@ export function CreateAgentTool({ part }: Props) {
)} - {output.type === "agent_preview" && ( + {isAgentPreviewOutput(output) && (

{output.message}

{output.description?.trim() && ( @@ -159,15 +170,26 @@ export function CreateAgentTool({ part }: Props) {
)} - {output.type === "clarification_needed" && ( + {isClarificationNeededOutput(output) && ( { + const item: WidgetClarifyingQuestion = { + question: q.question, + keyword: q.keyword, + }; + const example = + typeof q.example === "string" && q.example.trim() + ? q.example.trim() + : null; + if (example) item.example = example; + return item; + })} message={output.message} onSubmitAnswers={handleClarificationAnswers} /> )} - {output.type === "error" && ( + {isErrorOutput(output) && (

{output.message}

{output.error && ( diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/CreateAgent/helpers.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/CreateAgent/helpers.tsx index 8153ce1406..c5a465dd6f 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/CreateAgent/helpers.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/CreateAgent/helpers.tsx @@ -4,81 +4,23 @@ import { CircleNotchIcon, XCircleIcon, } from "@phosphor-icons/react"; - -export interface ClarifyingQuestion { - question: string; - keyword: string; - example?: string; -} - -export interface OperationStartedOutput { - type: "operation_started"; - message: string; - session_id?: string; - operation_id: string; - tool_name: string; -} - -export interface OperationPendingOutput { - type: "operation_pending"; - message: string; - session_id?: string; - operation_id: string; - tool_name: string; -} - -export interface OperationInProgressOutput { - type: "operation_in_progress"; - message: string; - session_id?: string; - tool_call_id: string; -} - -export interface AgentPreviewOutput { - type: "agent_preview"; - message: string; - session_id?: string; - agent_json: Record; - agent_name: string; - description: string; - node_count: number; - link_count: number; -} - -export interface AgentSavedOutput { - type: "agent_saved"; - message: string; - session_id?: string; - agent_id: string; - agent_name: string; - library_agent_id: string; - library_agent_link: string; - agent_page_link: string; -} - -export interface ClarificationNeededOutput { - type: "clarification_needed"; - message: string; - session_id?: string; - questions: ClarifyingQuestion[]; -} - -export interface ErrorOutput { - type: "error"; - message: string; - session_id?: string; - error?: string | null; - details?: Record | null; -} +import type { AgentPreviewResponse } from "@/app/api/__generated__/models/agentPreviewResponse"; +import type { AgentSavedResponse } from "@/app/api/__generated__/models/agentSavedResponse"; +import type { ClarificationNeededResponse } from "@/app/api/__generated__/models/clarificationNeededResponse"; +import type { ErrorResponse } from "@/app/api/__generated__/models/errorResponse"; +import type { OperationInProgressResponse } from "@/app/api/__generated__/models/operationInProgressResponse"; +import type { OperationPendingResponse } from "@/app/api/__generated__/models/operationPendingResponse"; +import type { OperationStartedResponse } from "@/app/api/__generated__/models/operationStartedResponse"; +import { ResponseType } from "@/app/api/__generated__/models/responseType"; export type CreateAgentToolOutput = - | OperationStartedOutput - | OperationPendingOutput - | OperationInProgressOutput - | AgentPreviewOutput - | AgentSavedOutput - | ClarificationNeededOutput - | ErrorOutput; + | OperationStartedResponse + | OperationPendingResponse + | OperationInProgressResponse + | AgentPreviewResponse + | AgentSavedResponse + | ClarificationNeededResponse + | ErrorResponse; function parseOutput(output: unknown): CreateAgentToolOutput | null { if (!output) return null; @@ -86,12 +28,35 @@ function parseOutput(output: unknown): CreateAgentToolOutput | null { const trimmed = output.trim(); if (!trimmed) return null; try { - return JSON.parse(trimmed) as CreateAgentToolOutput; + return parseOutput(JSON.parse(trimmed) as unknown); } catch { return null; } } - if (typeof output === "object") return output as CreateAgentToolOutput; + if (typeof output === "object") { + const type = (output as { type?: unknown }).type; + if ( + type === ResponseType.operation_started || + type === ResponseType.operation_pending || + type === ResponseType.operation_in_progress || + type === ResponseType.agent_preview || + type === ResponseType.agent_saved || + type === ResponseType.clarification_needed || + type === ResponseType.error + ) { + return output as CreateAgentToolOutput; + } + if ("operation_id" in output && "tool_name" in output) + return output as OperationStartedResponse | OperationPendingResponse; + if ("tool_call_id" in output) return output as OperationInProgressResponse; + if ("agent_json" in output && "agent_name" in output) + return output as AgentPreviewResponse; + if ("agent_id" in output && "library_agent_id" in output) + return output as AgentSavedResponse; + if ("questions" in output) return output as ClarificationNeededResponse; + if ("error" in output || "details" in output) + return output as ErrorResponse; + } return null; } @@ -102,6 +67,58 @@ export function getCreateAgentToolOutput( return parseOutput((part as { output?: unknown }).output); } +export function isOperationStartedOutput( + output: CreateAgentToolOutput, +): output is OperationStartedResponse { + return ( + output.type === ResponseType.operation_started || + ("operation_id" in output && "tool_name" in output) + ); +} + +export function isOperationPendingOutput( + output: CreateAgentToolOutput, +): output is OperationPendingResponse { + return output.type === ResponseType.operation_pending; +} + +export function isOperationInProgressOutput( + output: CreateAgentToolOutput, +): output is OperationInProgressResponse { + return ( + output.type === ResponseType.operation_in_progress || + "tool_call_id" in output + ); +} + +export function isAgentPreviewOutput( + output: CreateAgentToolOutput, +): output is AgentPreviewResponse { + return output.type === ResponseType.agent_preview || "agent_json" in output; +} + +export function isAgentSavedOutput( + output: CreateAgentToolOutput, +): output is AgentSavedResponse { + return ( + output.type === ResponseType.agent_saved || "agent_page_link" in output + ); +} + +export function isClarificationNeededOutput( + output: CreateAgentToolOutput, +): output is ClarificationNeededResponse { + return ( + output.type === ResponseType.clarification_needed || "questions" in output + ); +} + +export function isErrorOutput( + output: CreateAgentToolOutput, +): output is ErrorResponse { + return output.type === ResponseType.error || "error" in output; +} + export function getAnimationText(part: { state: ToolUIPart["state"]; input?: unknown; @@ -115,15 +132,13 @@ export function getAnimationText(part: { case "output-available": { const output = parseOutput(part.output); if (!output) return "Agent created"; - if (output.type === "operation_started") return "Agent creation started"; - if (output.type === "operation_pending") - return "Agent creation in progress"; - if (output.type === "operation_in_progress") + if (isOperationStartedOutput(output)) return "Agent creation started"; + if (isOperationPendingOutput(output)) return "Agent creation in progress"; + if (isOperationInProgressOutput(output)) return "Agent creation already in progress"; - if (output.type === "agent_saved") return `Saved: ${output.agent_name}`; - if (output.type === "agent_preview") - return `Preview: ${output.agent_name}`; - if (output.type === "clarification_needed") return "Needs clarification"; + if (isAgentSavedOutput(output)) return `Saved: ${output.agent_name}`; + if (isAgentPreviewOutput(output)) return `Preview: ${output.agent_name}`; + if (isClarificationNeededOutput(output)) return "Needs clarification"; return "Error creating agent"; } case "output-error": diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/EditAgent/EditAgent.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/EditAgent/EditAgent.tsx index a170342900..615d8f8a34 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/EditAgent/EditAgent.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/EditAgent/EditAgent.tsx @@ -5,11 +5,21 @@ import Link from "next/link"; import { MorphingTextAnimation } from "../../components/MorphingTextAnimation/MorphingTextAnimation"; import { ToolAccordion } from "../../components/ToolAccordion/ToolAccordion"; import { useCopilotChatActions } from "../../components/CopilotChatActionsProvider/useCopilotChatActions"; -import { ClarificationQuestionsWidget } from "@/components/contextual/Chat/components/ClarificationQuestionsWidget/ClarificationQuestionsWidget"; +import { + ClarificationQuestionsWidget, + type ClarifyingQuestion as WidgetClarifyingQuestion, +} from "@/components/contextual/Chat/components/ClarificationQuestionsWidget/ClarificationQuestionsWidget"; import { formatMaybeJson, getAnimationText, getEditAgentToolOutput, + isAgentPreviewOutput, + isAgentSavedOutput, + isClarificationNeededOutput, + isErrorOutput, + isOperationInProgressOutput, + isOperationPendingOutput, + isOperationStartedOutput, StateIcon, truncateText, type EditAgentToolOutput, @@ -32,27 +42,28 @@ function getAccordionMeta(output: EditAgentToolOutput): { title: string; description?: string; } { - if (output.type === "agent_saved") { + if (isAgentSavedOutput(output)) { return { badgeText: "Edit agent", title: output.agent_name }; } - if (output.type === "agent_preview") { + if (isAgentPreviewOutput(output)) { return { badgeText: "Edit agent", title: output.agent_name, description: `${output.node_count} block${output.node_count === 1 ? "" : "s"}`, }; } - if (output.type === "clarification_needed") { + if (isClarificationNeededOutput(output)) { + const questions = output.questions ?? []; return { badgeText: "Edit agent", title: "Needs clarification", - description: `${output.questions.length} question${output.questions.length === 1 ? "" : "s"}`, + description: `${questions.length} question${questions.length === 1 ? "" : "s"}`, }; } if ( - output.type === "operation_started" || - output.type === "operation_pending" || - output.type === "operation_in_progress" + isOperationStartedOutput(output) || + isOperationPendingOutput(output) || + isOperationInProgressOutput(output) ) { return { badgeText: "Edit agent", title: "Editing agent" }; } @@ -67,13 +78,13 @@ export function EditAgentTool({ part }: Props) { const hasExpandableContent = part.state === "output-available" && !!output && - (output.type === "operation_started" || - output.type === "operation_pending" || - output.type === "operation_in_progress" || - output.type === "agent_preview" || - output.type === "agent_saved" || - output.type === "clarification_needed" || - output.type === "error"); + (isOperationStartedOutput(output) || + isOperationPendingOutput(output) || + isOperationInProgressOutput(output) || + isAgentPreviewOutput(output) || + isAgentSavedOutput(output) || + isClarificationNeededOutput(output) || + isErrorOutput(output)); function handleClarificationAnswers(answers: Record) { const contextMessage = Object.entries(answers) @@ -95,10 +106,10 @@ export function EditAgentTool({ part }: Props) { {hasExpandableContent && output && ( - {(output.type === "operation_started" || - output.type === "operation_pending") && ( + {(isOperationStartedOutput(output) || + isOperationPendingOutput(output)) && (

{output.message}

@@ -110,7 +121,7 @@ export function EditAgentTool({ part }: Props) {

)} - {output.type === "operation_in_progress" && ( + {isOperationInProgressOutput(output) && (

{output.message}

@@ -119,7 +130,7 @@ export function EditAgentTool({ part }: Props) {

)} - {output.type === "agent_saved" && ( + {isAgentSavedOutput(output) && (

{output.message}

@@ -145,7 +156,7 @@ export function EditAgentTool({ part }: Props) {
)} - {output.type === "agent_preview" && ( + {isAgentPreviewOutput(output) && (

{output.message}

{output.description?.trim() && ( @@ -159,15 +170,26 @@ export function EditAgentTool({ part }: Props) {
)} - {output.type === "clarification_needed" && ( + {isClarificationNeededOutput(output) && ( { + const item: WidgetClarifyingQuestion = { + question: q.question, + keyword: q.keyword, + }; + const example = + typeof q.example === "string" && q.example.trim() + ? q.example.trim() + : null; + if (example) item.example = example; + return item; + })} message={output.message} onSubmitAnswers={handleClarificationAnswers} /> )} - {output.type === "error" && ( + {isErrorOutput(output) && (

{output.message}

{output.error && ( diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/EditAgent/helpers.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/EditAgent/helpers.tsx index af2b38449c..ac7fe62b42 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/EditAgent/helpers.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/EditAgent/helpers.tsx @@ -4,81 +4,23 @@ import { CircleNotchIcon, XCircleIcon, } from "@phosphor-icons/react"; - -export interface ClarifyingQuestion { - question: string; - keyword: string; - example?: string; -} - -export interface OperationStartedOutput { - type: "operation_started"; - message: string; - session_id?: string; - operation_id: string; - tool_name: string; -} - -export interface OperationPendingOutput { - type: "operation_pending"; - message: string; - session_id?: string; - operation_id: string; - tool_name: string; -} - -export interface OperationInProgressOutput { - type: "operation_in_progress"; - message: string; - session_id?: string; - tool_call_id: string; -} - -export interface AgentPreviewOutput { - type: "agent_preview"; - message: string; - session_id?: string; - agent_json: Record; - agent_name: string; - description: string; - node_count: number; - link_count: number; -} - -export interface AgentSavedOutput { - type: "agent_saved"; - message: string; - session_id?: string; - agent_id: string; - agent_name: string; - library_agent_id: string; - library_agent_link: string; - agent_page_link: string; -} - -export interface ClarificationNeededOutput { - type: "clarification_needed"; - message: string; - session_id?: string; - questions: ClarifyingQuestion[]; -} - -export interface ErrorOutput { - type: "error"; - message: string; - session_id?: string; - error?: string | null; - details?: Record | null; -} +import type { AgentPreviewResponse } from "@/app/api/__generated__/models/agentPreviewResponse"; +import type { AgentSavedResponse } from "@/app/api/__generated__/models/agentSavedResponse"; +import type { ClarificationNeededResponse } from "@/app/api/__generated__/models/clarificationNeededResponse"; +import type { ErrorResponse } from "@/app/api/__generated__/models/errorResponse"; +import type { OperationInProgressResponse } from "@/app/api/__generated__/models/operationInProgressResponse"; +import type { OperationPendingResponse } from "@/app/api/__generated__/models/operationPendingResponse"; +import type { OperationStartedResponse } from "@/app/api/__generated__/models/operationStartedResponse"; +import { ResponseType } from "@/app/api/__generated__/models/responseType"; export type EditAgentToolOutput = - | OperationStartedOutput - | OperationPendingOutput - | OperationInProgressOutput - | AgentPreviewOutput - | AgentSavedOutput - | ClarificationNeededOutput - | ErrorOutput; + | OperationStartedResponse + | OperationPendingResponse + | OperationInProgressResponse + | AgentPreviewResponse + | AgentSavedResponse + | ClarificationNeededResponse + | ErrorResponse; function parseOutput(output: unknown): EditAgentToolOutput | null { if (!output) return null; @@ -86,12 +28,35 @@ function parseOutput(output: unknown): EditAgentToolOutput | null { const trimmed = output.trim(); if (!trimmed) return null; try { - return JSON.parse(trimmed) as EditAgentToolOutput; + return parseOutput(JSON.parse(trimmed) as unknown); } catch { return null; } } - if (typeof output === "object") return output as EditAgentToolOutput; + if (typeof output === "object") { + const type = (output as { type?: unknown }).type; + if ( + type === ResponseType.operation_started || + type === ResponseType.operation_pending || + type === ResponseType.operation_in_progress || + type === ResponseType.agent_preview || + type === ResponseType.agent_saved || + type === ResponseType.clarification_needed || + type === ResponseType.error + ) { + return output as EditAgentToolOutput; + } + if ("operation_id" in output && "tool_name" in output) + return output as OperationStartedResponse | OperationPendingResponse; + if ("tool_call_id" in output) return output as OperationInProgressResponse; + if ("agent_json" in output && "agent_name" in output) + return output as AgentPreviewResponse; + if ("agent_id" in output && "library_agent_id" in output) + return output as AgentSavedResponse; + if ("questions" in output) return output as ClarificationNeededResponse; + if ("error" in output || "details" in output) + return output as ErrorResponse; + } return null; } @@ -102,6 +67,58 @@ export function getEditAgentToolOutput( return parseOutput((part as { output?: unknown }).output); } +export function isOperationStartedOutput( + output: EditAgentToolOutput, +): output is OperationStartedResponse { + return ( + output.type === ResponseType.operation_started || + ("operation_id" in output && "tool_name" in output) + ); +} + +export function isOperationPendingOutput( + output: EditAgentToolOutput, +): output is OperationPendingResponse { + return output.type === ResponseType.operation_pending; +} + +export function isOperationInProgressOutput( + output: EditAgentToolOutput, +): output is OperationInProgressResponse { + return ( + output.type === ResponseType.operation_in_progress || + "tool_call_id" in output + ); +} + +export function isAgentPreviewOutput( + output: EditAgentToolOutput, +): output is AgentPreviewResponse { + return output.type === ResponseType.agent_preview || "agent_json" in output; +} + +export function isAgentSavedOutput( + output: EditAgentToolOutput, +): output is AgentSavedResponse { + return ( + output.type === ResponseType.agent_saved || "agent_page_link" in output + ); +} + +export function isClarificationNeededOutput( + output: EditAgentToolOutput, +): output is ClarificationNeededResponse { + return ( + output.type === ResponseType.clarification_needed || "questions" in output + ); +} + +export function isErrorOutput( + output: EditAgentToolOutput, +): output is ErrorResponse { + return output.type === ResponseType.error || "error" in output; +} + export function getAnimationText(part: { state: ToolUIPart["state"]; input?: unknown; @@ -115,15 +132,13 @@ export function getAnimationText(part: { case "output-available": { const output = parseOutput(part.output); if (!output) return "Agent updated"; - if (output.type === "operation_started") return "Agent update started"; - if (output.type === "operation_pending") - return "Agent update in progress"; - if (output.type === "operation_in_progress") + if (isOperationStartedOutput(output)) return "Agent update started"; + if (isOperationPendingOutput(output)) return "Agent update in progress"; + if (isOperationInProgressOutput(output)) return "Agent update already in progress"; - if (output.type === "agent_saved") return `Saved: ${output.agent_name}`; - if (output.type === "agent_preview") - return `Preview: ${output.agent_name}`; - if (output.type === "clarification_needed") return "Needs clarification"; + if (isAgentSavedOutput(output)) return `Saved: ${output.agent_name}`; + if (isAgentPreviewOutput(output)) return `Preview: ${output.agent_name}`; + if (isClarificationNeededOutput(output)) return "Needs clarification"; return "Error editing agent"; } case "output-error": diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/FindAgents/FindAgents.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/FindAgents/FindAgents.tsx index aa3a16f4fd..4208d6160f 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/FindAgents/FindAgents.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/FindAgents/FindAgents.tsx @@ -8,6 +8,7 @@ import { getAnimationText, getFindAgentsOutput, getSourceLabelFromToolType, + isAgentsFoundOutput, StateIcon, } from "./helpers"; import { ToolAccordion } from "../../components/ToolAccordion/ToolAccordion"; @@ -33,13 +34,17 @@ export function FindAgentsTool({ part }: Props) { ? String((part.input as { query?: unknown }).query ?? "").trim() : ""; - const isAgentsFound = - part.state === "output-available" && output?.type === "agents_found"; + const agentsFoundOutput = + part.state === "output-available" && output && isAgentsFoundOutput(output) + ? output + : null; + const hasAgents = - isAgentsFound && - output.agents.length > 0 && - (typeof output.count !== "number" || output.count > 0); - const totalCount = isAgentsFound ? output.count : 0; + !!agentsFoundOutput && + agentsFoundOutput.agents.length > 0 && + (typeof agentsFoundOutput.count !== "number" || + agentsFoundOutput.count > 0); + const totalCount = agentsFoundOutput ? agentsFoundOutput.count : 0; const { label: sourceLabel, source } = getSourceLabelFromToolType(part.type); const scopeText = source === "library" @@ -58,14 +63,14 @@ export function FindAgentsTool({ part }: Props) {
- {hasAgents && ( + {hasAgents && agentsFoundOutput && (
- {output.agents.map((agent) => { + {agentsFoundOutput.agents.map((agent) => { const href = getAgentHref(agent); const agentSource = agent.source === "library" diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/FindAgents/helpers.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/FindAgents/helpers.tsx index 2f057369af..6b376d6f9c 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/FindAgents/helpers.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/FindAgents/helpers.tsx @@ -4,45 +4,20 @@ import { CircleNotchIcon, XCircleIcon, } from "@phosphor-icons/react"; +import type { AgentInfo } from "@/app/api/__generated__/models/agentInfo"; +import type { AgentsFoundResponse } from "@/app/api/__generated__/models/agentsFoundResponse"; +import type { ErrorResponse } from "@/app/api/__generated__/models/errorResponse"; +import type { NoResultsResponse } from "@/app/api/__generated__/models/noResultsResponse"; +import { ResponseType } from "@/app/api/__generated__/models/responseType"; export interface FindAgentInput { query: string; } -export interface AgentInfo { - id: string; - name: string; - description: string; - source?: "marketplace" | "library" | string; -} - -export interface AgentsFoundOutput { - type: "agents_found"; - title?: string; - message?: string; - session_id?: string; - agents: AgentInfo[]; - count: number; -} - -export interface NoResultsOutput { - type: "no_results"; - message: string; - suggestions?: string[]; - session_id?: string; -} - -export interface ErrorOutput { - type: "error"; - message: string; - error?: string; - session_id?: string; -} - export type FindAgentsOutput = - | AgentsFoundOutput - | NoResultsOutput - | ErrorOutput; + | AgentsFoundResponse + | NoResultsResponse + | ErrorResponse; export type FindAgentsToolType = | "tool-find_agent" @@ -55,13 +30,26 @@ function parseOutput(output: unknown): FindAgentsOutput | null { const trimmed = output.trim(); if (!trimmed) return null; try { - return JSON.parse(trimmed) as FindAgentsOutput; + return parseOutput(JSON.parse(trimmed) as unknown); } catch { return null; } } if (typeof output === "object") { - return output as FindAgentsOutput; + const type = (output as { type?: unknown }).type; + if ( + type === ResponseType.agents_found || + type === ResponseType.no_results || + type === ResponseType.error + ) { + return output as FindAgentsOutput; + } + if ("agents" in output && "count" in output) + return output as AgentsFoundResponse; + if ("suggestions" in output && !("error" in output)) + return output as NoResultsResponse; + if ("error" in output || "details" in output) + return output as ErrorResponse; } return null; } @@ -71,6 +59,27 @@ export function getFindAgentsOutput(part: unknown): FindAgentsOutput | null { return parseOutput((part as { output?: unknown }).output); } +export function isAgentsFoundOutput( + output: FindAgentsOutput, +): output is AgentsFoundResponse { + return output.type === ResponseType.agents_found || "agents" in output; +} + +export function isNoResultsOutput( + output: FindAgentsOutput, +): output is NoResultsResponse { + return ( + output.type === ResponseType.no_results || + ("suggestions" in output && !("error" in output)) + ); +} + +export function isErrorOutput( + output: FindAgentsOutput, +): output is ErrorResponse { + return output.type === ResponseType.error || "error" in output; +} + export function getSourceLabelFromToolType(toolType?: FindAgentsToolType): { source: "marketplace" | "library" | "unknown"; label: string; @@ -112,18 +121,18 @@ export function getAnimationText(part: { if (!output) { return query ? `Found agents ${scope} for "${query}"` : "Found agents"; } - if (output.type === "no_results") { + if (isNoResultsOutput(output)) { return query ? `No agents found ${scope} for "${query}"` : `No agents found ${scope}`; } - if (output.type === "agents_found") { + if (isAgentsFoundOutput(output)) { const count = output.count ?? output.agents?.length ?? 0; const countText = `Found ${count} agent${count === 1 ? "" : "s"}`; if (query) return `${countText} ${scope} for "${query}"`; return `${countText} ${scope}`; } - if (output.type === "error") { + if (isErrorOutput(output)) { return `Error finding agents ${scope}`; } return `Found agents ${scope}`; diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/FindBlocks/FindBlocks.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/FindBlocks/FindBlocks.tsx index f2facc9d80..923642293f 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/FindBlocks/FindBlocks.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/FindBlocks/FindBlocks.tsx @@ -1,5 +1,5 @@ import { MorphingTextAnimation } from "../../components/MorphingTextAnimation/MorphingTextAnimation"; -import { BlockInfo } from "@/app/api/__generated__/models/blockInfo"; +import type { BlockListResponse } from "@/app/api/__generated__/models/blockListResponse"; import { ToolUIPart } from "ai"; import { getAnimationText, StateIcon } from "./helpers"; @@ -7,15 +7,7 @@ export interface FindBlockInput { query: string; } -export interface FindBlockOutput { - type: "block_list"; - message: string; - session_id: string; - blocks: BlockInfo[]; - count: number; - query: string; - usage_hint: string; -} +export type FindBlockOutput = BlockListResponse; export interface FindBlockToolPart { type: string; diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/FindBlocks/helpers.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/FindBlocks/helpers.tsx index 495d803c13..c9140056b9 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/FindBlocks/helpers.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/FindBlocks/helpers.tsx @@ -1,16 +1,34 @@ import { ToolUIPart } from "ai"; -import { - FindBlockInput, - FindBlockOutput, - FindBlockToolPart, -} from "./FindBlocks"; +import type { BlockListResponse } from "@/app/api/__generated__/models/blockListResponse"; +import { ResponseType } from "@/app/api/__generated__/models/responseType"; +import { FindBlockInput, FindBlockToolPart } from "./FindBlocks"; import { CheckCircleIcon, CircleNotchIcon, XCircleIcon, } from "@phosphor-icons/react"; -export const getAnimationText = (part: FindBlockToolPart): string => { +function parseOutput(output: unknown): BlockListResponse | null { + if (!output) return null; + if (typeof output === "string") { + const trimmed = output.trim(); + if (!trimmed) return null; + try { + return parseOutput(JSON.parse(trimmed) as unknown); + } catch { + return null; + } + } + if (typeof output === "object") { + const type = (output as { type?: unknown }).type; + if (type === ResponseType.block_list || "blocks" in output) { + return output as BlockListResponse; + } + } + return null; +} + +export function getAnimationText(part: FindBlockToolPart): string { switch (part.state) { case "input-streaming": return "Searching blocks for you"; @@ -21,7 +39,7 @@ export const getAnimationText = (part: FindBlockToolPart): string => { } case "output-available": { - const parsed = JSON.parse(part.output as string) as FindBlockOutput; + const parsed = parseOutput(part.output); if (parsed) { return `Found ${parsed.count} "${(part.input as FindBlockInput).query}" blocks`; } @@ -34,9 +52,9 @@ export const getAnimationText = (part: FindBlockToolPart): string => { default: return "Processing"; } -}; +} -export const StateIcon = ({ state }: { state: ToolUIPart["state"] }) => { +export function StateIcon({ state }: { state: ToolUIPart["state"] }) { switch (state) { case "input-streaming": case "input-available": @@ -53,4 +71,4 @@ export const StateIcon = ({ state }: { state: ToolUIPart["state"] }) => { default: return null; } -}; +} diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/RunAgent/RunAgent.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/RunAgent/RunAgent.tsx index 83b601b949..d3c695c456 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/RunAgent/RunAgent.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/RunAgent/RunAgent.tsx @@ -5,11 +5,19 @@ import Link from "next/link"; import { MorphingTextAnimation } from "../../components/MorphingTextAnimation/MorphingTextAnimation"; import { ToolAccordion } from "../../components/ToolAccordion/ToolAccordion"; import { useCopilotChatActions } from "../../components/CopilotChatActionsProvider/useCopilotChatActions"; -import { ChatCredentialsSetup } from "@/components/contextual/Chat/components/ChatCredentialsSetup/ChatCredentialsSetup"; +import { + ChatCredentialsSetup, + type CredentialInfo, +} from "@/components/contextual/Chat/components/ChatCredentialsSetup/ChatCredentialsSetup"; import { formatMaybeJson, getAnimationText, getRunAgentToolOutput, + isRunAgentAgentDetailsOutput, + isRunAgentErrorOutput, + isRunAgentExecutionStartedOutput, + isRunAgentNeedLoginOutput, + isRunAgentSetupRequirementsOutput, StateIcon, type RunAgentToolOutput, } from "./helpers"; @@ -31,15 +39,19 @@ function getAccordionMeta(output: RunAgentToolOutput): { title: string; description?: string; } { - if (output.type === "execution_started") { + if (isRunAgentExecutionStartedOutput(output)) { + const statusText = + typeof output.status === "string" && output.status.trim() + ? output.status.trim() + : "started"; return { badgeText: "Run agent", title: output.graph_name, - description: `Status: ${output.status}`, + description: `Status: ${statusText}`, }; } - if (output.type === "agent_details") { + if (isRunAgentAgentDetailsOutput(output)) { return { badgeText: "Run agent", title: output.agent.name, @@ -47,9 +59,12 @@ function getAccordionMeta(output: RunAgentToolOutput): { }; } - if (output.type === "setup_requirements") { + if (isRunAgentSetupRequirementsOutput(output)) { const missingCredsCount = Object.keys( - output.setup_info.user_readiness.missing_credentials ?? {}, + (output.setup_info.user_readiness?.missing_credentials ?? {}) as Record< + string, + unknown + >, ).length; return { badgeText: "Run agent", @@ -61,13 +76,132 @@ function getAccordionMeta(output: RunAgentToolOutput): { }; } - if (output.type === "need_login") { + if (isRunAgentNeedLoginOutput(output)) { return { badgeText: "Run agent", title: "Sign in required" }; } return { badgeText: "Run agent", title: "Error" }; } +function coerceMissingCredentials( + rawMissingCredentials: unknown, +): CredentialInfo[] { + const missing = + rawMissingCredentials && typeof rawMissingCredentials === "object" + ? (rawMissingCredentials as Record) + : {}; + + const validTypes = new Set([ + "api_key", + "oauth2", + "user_password", + "host_scoped", + ]); + + const results: CredentialInfo[] = []; + + Object.values(missing).forEach((value) => { + if (!value || typeof value !== "object") return; + const cred = value as Record; + + const provider = + typeof cred.provider === "string" ? cred.provider.trim() : ""; + if (!provider) return; + + const providerName = + typeof cred.provider_name === "string" && cred.provider_name.trim() + ? cred.provider_name.trim() + : provider.replace(/_/g, " "); + + const title = + typeof cred.title === "string" && cred.title.trim() + ? cred.title.trim() + : providerName; + + const types = + Array.isArray(cred.types) && cred.types.length > 0 + ? cred.types + : typeof cred.type === "string" + ? [cred.type] + : []; + + const credentialTypes = types + .map((t) => (typeof t === "string" ? t.trim() : "")) + .filter( + (t): t is "api_key" | "oauth2" | "user_password" | "host_scoped" => + validTypes.has(t), + ); + + if (credentialTypes.length === 0) return; + + const scopes = Array.isArray(cred.scopes) + ? cred.scopes.filter((s): s is string => typeof s === "string") + : undefined; + + const item: CredentialInfo = { + provider, + providerName, + credentialTypes, + title, + }; + if (scopes && scopes.length > 0) { + item.scopes = scopes; + } + results.push(item); + }); + + return results; +} + +function coerceExpectedInputs(rawInputs: unknown): Array<{ + name: string; + title: string; + type: string; + description?: string; + required: boolean; +}> { + if (!Array.isArray(rawInputs)) return []; + const results: Array<{ + name: string; + title: string; + type: string; + description?: string; + required: boolean; + }> = []; + + rawInputs.forEach((value, index) => { + if (!value || typeof value !== "object") return; + const input = value as Record; + + const name = + typeof input.name === "string" && input.name.trim() + ? input.name.trim() + : `input-${index}`; + const title = + typeof input.title === "string" && input.title.trim() + ? input.title.trim() + : name; + const type = typeof input.type === "string" ? input.type : "unknown"; + const description = + typeof input.description === "string" && input.description.trim() + ? input.description.trim() + : undefined; + const required = Boolean(input.required); + + const item: { + name: string; + title: string; + type: string; + description?: string; + required: boolean; + } = { name, title, type, required }; + if (description) item.description = description; + results.push(item); + }); + + return results; +} + export function RunAgentTool({ part }: Props) { const text = getAnimationText(part); const { onSend } = useCopilotChatActions(); @@ -76,11 +210,11 @@ export function RunAgentTool({ part }: Props) { const hasExpandableContent = part.state === "output-available" && !!output && - (output.type === "execution_started" || - output.type === "agent_details" || - output.type === "setup_requirements" || - output.type === "need_login" || - output.type === "error"); + (isRunAgentExecutionStartedOutput(output) || + isRunAgentAgentDetailsOutput(output) || + isRunAgentSetupRequirementsOutput(output) || + isRunAgentNeedLoginOutput(output) || + isRunAgentErrorOutput(output)); function handleAllCredentialsComplete() { onSend( @@ -99,11 +233,11 @@ export function RunAgentTool({ part }: Props) { - {output.type === "execution_started" && ( + {isRunAgentExecutionStartedOutput(output) && (
@@ -131,7 +265,7 @@ export function RunAgentTool({ part }: Props) {
)} - {output.type === "agent_details" && ( + {isRunAgentAgentDetailsOutput(output) && (

{output.message}

@@ -153,26 +287,17 @@ export function RunAgentTool({ part }: Props) {
)} - {output.type === "setup_requirements" && ( + {isRunAgentSetupRequirementsOutput(output) && (

{output.message}

- {Object.keys( - output.setup_info.user_readiness.missing_credentials ?? {}, + {coerceMissingCredentials( + output.setup_info.user_readiness?.missing_credentials, ).length > 0 && ( ({ - provider: cred.provider, - providerName: - cred.provider_name ?? cred.provider.replace(/_/g, " "), - credentialTypes: (cred.types ?? [cred.type]) as Array< - "api_key" | "oauth2" | "user_password" | "host_scoped" - >, - title: cred.title, - scopes: cred.scopes, - }))} + credentials={coerceMissingCredentials( + output.setup_info.user_readiness?.missing_credentials, + )} agentName={output.setup_info.agent_name} message={output.message} onAllCredentialsComplete={handleAllCredentialsComplete} @@ -180,13 +305,23 @@ export function RunAgentTool({ part }: Props) { /> )} - {output.setup_info.requirements.inputs?.length > 0 && ( + {coerceExpectedInputs( + (output.setup_info.requirements as Record) + ?.inputs, + ).length > 0 && (

Expected inputs

- {output.setup_info.requirements.inputs.map((input) => ( + {coerceExpectedInputs( + ( + output.setup_info.requirements as Record< + string, + unknown + > + )?.inputs, + ).map((input) => (

@@ -208,11 +343,11 @@ export function RunAgentTool({ part }: Props) {

)} - {output.type === "need_login" && ( + {isRunAgentNeedLoginOutput(output) && (

{output.message}

)} - {output.type === "error" && ( + {isRunAgentErrorOutput(output) && (

{output.message}

{output.error && ( diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/RunAgent/helpers.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/RunAgent/helpers.tsx index 709b3393e8..b8e511802d 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/RunAgent/helpers.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/RunAgent/helpers.tsx @@ -4,6 +4,12 @@ import { CircleNotchIcon, XCircleIcon, } from "@phosphor-icons/react"; +import type { AgentDetailsResponse } from "@/app/api/__generated__/models/agentDetailsResponse"; +import type { ErrorResponse } from "@/app/api/__generated__/models/errorResponse"; +import type { ExecutionStartedResponse } from "@/app/api/__generated__/models/executionStartedResponse"; +import type { NeedLoginResponse } from "@/app/api/__generated__/models/needLoginResponse"; +import { ResponseType } from "@/app/api/__generated__/models/responseType"; +import type { SetupRequirementsResponse } from "@/app/api/__generated__/models/setupRequirementsResponse"; export interface RunAgentInput { username_agent_slug?: string; @@ -15,99 +21,55 @@ export interface RunAgentInput { timezone?: string; } -export interface CredentialsMeta { - id: string; - provider: string; - provider_name?: string; - type: string; - types?: string[]; - title: string; - scopes?: string[]; -} - -export interface SetupInfo { - agent_id: string; - agent_name: string; - requirements: { - credentials: CredentialsMeta[]; - inputs: Array<{ - name: string; - title: string; - type: string; - description: string; - required: boolean; - }>; - execution_modes: string[]; - }; - user_readiness: { - has_all_credentials: boolean; - missing_credentials: Record; - ready_to_run: boolean; - }; -} - -export interface SetupRequirementsOutput { - type: "setup_requirements"; - message: string; - session_id?: string; - setup_info: SetupInfo; - graph_id?: string | null; - graph_version?: number | null; -} - -export interface ExecutionStartedOutput { - type: "execution_started"; - message: string; - session_id?: string; - execution_id: string; - graph_id: string; - graph_name: string; - library_agent_id?: string | null; - library_agent_link?: string | null; - status: string; -} - -export interface ErrorOutput { - type: "error"; - message: string; - session_id?: string; - error?: string | null; - details?: Record | null; -} - -export interface NeedLoginOutput { - type: "need_login"; - message: string; - session_id?: string; -} - -export interface AgentDetailsOutput { - type: "agent_details"; - message: string; - session_id?: string; - agent: { - id: string; - name: string; - description: string; - inputs: Record; - credentials: CredentialsMeta[]; - execution_options?: { - manual?: boolean; - scheduled?: boolean; - webhook?: boolean; - }; - }; - user_authenticated?: boolean; - graph_id?: string | null; - graph_version?: number | null; -} - export type RunAgentToolOutput = - | SetupRequirementsOutput - | ExecutionStartedOutput - | AgentDetailsOutput - | NeedLoginOutput - | ErrorOutput; + | SetupRequirementsResponse + | ExecutionStartedResponse + | AgentDetailsResponse + | NeedLoginResponse + | ErrorResponse; + +const RUN_AGENT_OUTPUT_TYPES = new Set([ + ResponseType.setup_requirements, + ResponseType.execution_started, + ResponseType.agent_details, + ResponseType.need_login, + ResponseType.error, +]); + +export function isRunAgentSetupRequirementsOutput( + output: RunAgentToolOutput, +): output is SetupRequirementsResponse { + return ( + output.type === ResponseType.setup_requirements || + ("setup_info" in output && typeof output.setup_info === "object") + ); +} + +export function isRunAgentExecutionStartedOutput( + output: RunAgentToolOutput, +): output is ExecutionStartedResponse { + return ( + output.type === ResponseType.execution_started || "execution_id" in output + ); +} + +export function isRunAgentAgentDetailsOutput( + output: RunAgentToolOutput, +): output is AgentDetailsResponse { + return output.type === ResponseType.agent_details || "agent" in output; +} + +export function isRunAgentNeedLoginOutput( + output: RunAgentToolOutput, +): output is NeedLoginResponse { + return output.type === ResponseType.need_login; +} + +export function isRunAgentErrorOutput( + output: RunAgentToolOutput, +): output is ErrorResponse { + return output.type === ResponseType.error || "error" in output; +} function parseOutput(output: unknown): RunAgentToolOutput | null { if (!output) return null; @@ -115,12 +77,23 @@ function parseOutput(output: unknown): RunAgentToolOutput | null { const trimmed = output.trim(); if (!trimmed) return null; try { - return JSON.parse(trimmed) as RunAgentToolOutput; + return parseOutput(JSON.parse(trimmed) as unknown); } catch { return null; } } - if (typeof output === "object") return output as RunAgentToolOutput; + if (typeof output === "object") { + const type = (output as { type?: unknown }).type; + if (typeof type === "string" && RUN_AGENT_OUTPUT_TYPES.has(type)) { + return output as RunAgentToolOutput; + } + if ("execution_id" in output) return output as ExecutionStartedResponse; + if ("setup_info" in output) return output as SetupRequirementsResponse; + if ("agent" in output) return output as AgentDetailsResponse; + if ("error" in output || "details" in output) + return output as ErrorResponse; + if (type === ResponseType.need_login) return output as NeedLoginResponse; + } return null; } @@ -165,16 +138,17 @@ export function getAnimationText(part: { case "output-available": { const output = parseOutput(part.output); if (!output) return "Agent run updated"; - if (output.type === "execution_started") { + if (isRunAgentExecutionStartedOutput(output)) { return `Started: ${output.graph_name}`; } - if (output.type === "agent_details") { + if (isRunAgentAgentDetailsOutput(output)) { return `Agent inputs: ${output.agent.name}`; } - if (output.type === "setup_requirements") { + if (isRunAgentSetupRequirementsOutput(output)) { return `Needs setup: ${output.setup_info.agent_name}`; } - if (output.type === "need_login") return "Sign in required to run agent"; + if (isRunAgentNeedLoginOutput(output)) + return "Sign in required to run agent"; return "Error running agent"; } case "output-error": diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/RunBlock/RunBlock.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/RunBlock/RunBlock.tsx index 5315105999..46766abaf9 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/RunBlock/RunBlock.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/RunBlock/RunBlock.tsx @@ -4,11 +4,17 @@ import type { ToolUIPart } from "ai"; import { MorphingTextAnimation } from "../../components/MorphingTextAnimation/MorphingTextAnimation"; import { ToolAccordion } from "../../components/ToolAccordion/ToolAccordion"; import { useCopilotChatActions } from "../../components/CopilotChatActionsProvider/useCopilotChatActions"; -import { ChatCredentialsSetup } from "@/components/contextual/Chat/components/ChatCredentialsSetup/ChatCredentialsSetup"; +import { + ChatCredentialsSetup, + type CredentialInfo, +} from "@/components/contextual/Chat/components/ChatCredentialsSetup/ChatCredentialsSetup"; import { formatMaybeJson, getAnimationText, getRunBlockToolOutput, + isRunBlockBlockOutput, + isRunBlockErrorOutput, + isRunBlockSetupRequirementsOutput, StateIcon, type RunBlockToolOutput, } from "./helpers"; @@ -30,7 +36,7 @@ function getAccordionMeta(output: RunBlockToolOutput): { title: string; description?: string; } { - if (output.type === "block_output") { + if (isRunBlockBlockOutput(output)) { const keys = Object.keys(output.outputs ?? {}); return { badgeText: "Run block", @@ -42,9 +48,12 @@ function getAccordionMeta(output: RunBlockToolOutput): { }; } - if (output.type === "setup_requirements") { + if (isRunBlockSetupRequirementsOutput(output)) { const missingCredsCount = Object.keys( - output.setup_info.user_readiness.missing_credentials ?? {}, + (output.setup_info.user_readiness?.missing_credentials ?? {}) as Record< + string, + unknown + >, ).length; return { badgeText: "Run block", @@ -59,6 +68,125 @@ function getAccordionMeta(output: RunBlockToolOutput): { return { badgeText: "Run block", title: "Error" }; } +function coerceMissingCredentials( + rawMissingCredentials: unknown, +): CredentialInfo[] { + const missing = + rawMissingCredentials && typeof rawMissingCredentials === "object" + ? (rawMissingCredentials as Record) + : {}; + + const validTypes = new Set([ + "api_key", + "oauth2", + "user_password", + "host_scoped", + ]); + + const results: CredentialInfo[] = []; + + Object.values(missing).forEach((value) => { + if (!value || typeof value !== "object") return; + const cred = value as Record; + + const provider = + typeof cred.provider === "string" ? cred.provider.trim() : ""; + if (!provider) return; + + const providerName = + typeof cred.provider_name === "string" && cred.provider_name.trim() + ? cred.provider_name.trim() + : provider.replace(/_/g, " "); + + const title = + typeof cred.title === "string" && cred.title.trim() + ? cred.title.trim() + : providerName; + + const types = + Array.isArray(cred.types) && cred.types.length > 0 + ? cred.types + : typeof cred.type === "string" + ? [cred.type] + : []; + + const credentialTypes = types + .map((t) => (typeof t === "string" ? t.trim() : "")) + .filter( + (t): t is "api_key" | "oauth2" | "user_password" | "host_scoped" => + validTypes.has(t), + ); + + if (credentialTypes.length === 0) return; + + const scopes = Array.isArray(cred.scopes) + ? cred.scopes.filter((s): s is string => typeof s === "string") + : undefined; + + const item: CredentialInfo = { + provider, + providerName, + credentialTypes, + title, + }; + if (scopes && scopes.length > 0) { + item.scopes = scopes; + } + results.push(item); + }); + + return results; +} + +function coerceExpectedInputs(rawInputs: unknown): Array<{ + name: string; + title: string; + type: string; + description?: string; + required: boolean; +}> { + if (!Array.isArray(rawInputs)) return []; + const results: Array<{ + name: string; + title: string; + type: string; + description?: string; + required: boolean; + }> = []; + + rawInputs.forEach((value, index) => { + if (!value || typeof value !== "object") return; + const input = value as Record; + + const name = + typeof input.name === "string" && input.name.trim() + ? input.name.trim() + : `input-${index}`; + const title = + typeof input.title === "string" && input.title.trim() + ? input.title.trim() + : name; + const type = typeof input.type === "string" ? input.type : "unknown"; + const description = + typeof input.description === "string" && input.description.trim() + ? input.description.trim() + : undefined; + const required = Boolean(input.required); + + const item: { + name: string; + title: string; + type: string; + description?: string; + required: boolean; + } = { name, title, type, required }; + if (description) item.description = description; + results.push(item); + }); + + return results; +} + export function RunBlockTool({ part }: Props) { const text = getAnimationText(part); const { onSend } = useCopilotChatActions(); @@ -67,9 +195,9 @@ export function RunBlockTool({ part }: Props) { const hasExpandableContent = part.state === "output-available" && !!output && - (output.type === "block_output" || - output.type === "setup_requirements" || - output.type === "error"); + (isRunBlockBlockOutput(output) || + isRunBlockSetupRequirementsOutput(output) || + isRunBlockErrorOutput(output)); function handleAllCredentialsComplete() { onSend( @@ -87,9 +215,9 @@ export function RunBlockTool({ part }: Props) { {hasExpandableContent && output && ( - {output.type === "block_output" && ( + {isRunBlockBlockOutput(output) && (

{output.message}

@@ -111,26 +239,17 @@ export function RunBlockTool({ part }: Props) {
)} - {output.type === "setup_requirements" && ( + {isRunBlockSetupRequirementsOutput(output) && (

{output.message}

- {Object.keys( - output.setup_info.user_readiness.missing_credentials ?? {}, + {coerceMissingCredentials( + output.setup_info.user_readiness?.missing_credentials, ).length > 0 && ( ({ - provider: cred.provider, - providerName: - cred.provider_name ?? cred.provider.replace(/_/g, " "), - credentialTypes: (cred.types ?? [cred.type]) as Array< - "api_key" | "oauth2" | "user_password" | "host_scoped" - >, - title: cred.title, - scopes: cred.scopes, - }))} + credentials={coerceMissingCredentials( + output.setup_info.user_readiness?.missing_credentials, + )} agentName={output.setup_info.agent_name} message={output.message} onAllCredentialsComplete={handleAllCredentialsComplete} @@ -138,13 +257,23 @@ export function RunBlockTool({ part }: Props) { /> )} - {output.setup_info.requirements.inputs?.length > 0 && ( + {coerceExpectedInputs( + (output.setup_info.requirements as Record) + ?.inputs, + ).length > 0 && (

Expected inputs

- {output.setup_info.requirements.inputs.map((input) => ( + {coerceExpectedInputs( + ( + output.setup_info.requirements as Record< + string, + unknown + > + )?.inputs, + ).map((input) => (

@@ -166,7 +295,7 @@ export function RunBlockTool({ part }: Props) {

)} - {output.type === "error" && ( + {isRunBlockErrorOutput(output) && (

{output.message}

{output.error && ( diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/RunBlock/helpers.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/RunBlock/helpers.tsx index bf97031dbf..eb94e59108 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/RunBlock/helpers.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/RunBlock/helpers.tsx @@ -4,72 +4,47 @@ import { CircleNotchIcon, XCircleIcon, } from "@phosphor-icons/react"; +import type { BlockOutputResponse } from "@/app/api/__generated__/models/blockOutputResponse"; +import type { ErrorResponse } from "@/app/api/__generated__/models/errorResponse"; +import { ResponseType } from "@/app/api/__generated__/models/responseType"; +import type { SetupRequirementsResponse } from "@/app/api/__generated__/models/setupRequirementsResponse"; export interface RunBlockInput { block_id?: string; input_data?: Record; } -export interface CredentialsMeta { - id: string; - provider: string; - provider_name?: string; - type: string; - types?: string[]; - title: string; - scopes?: string[]; -} - -export interface SetupInfo { - agent_id: string; - agent_name: string; - requirements: { - credentials: CredentialsMeta[]; - inputs: Array<{ - name: string; - title: string; - type: string; - description: string; - required: boolean; - }>; - execution_modes: string[]; - }; - user_readiness: { - has_all_credentials: boolean; - missing_credentials: Record; - ready_to_run: boolean; - }; -} - -export interface SetupRequirementsOutput { - type: "setup_requirements"; - message: string; - session_id?: string; - setup_info: SetupInfo; -} - -export interface BlockOutput { - type: "block_output"; - message: string; - session_id?: string; - block_id: string; - block_name: string; - outputs: Record; - success: boolean; -} - -export interface ErrorOutput { - type: "error"; - message: string; - session_id?: string; - error?: string | null; - details?: Record | null; -} - export type RunBlockToolOutput = - | SetupRequirementsOutput - | BlockOutput - | ErrorOutput; + | SetupRequirementsResponse + | BlockOutputResponse + | ErrorResponse; + +const RUN_BLOCK_OUTPUT_TYPES = new Set([ + ResponseType.setup_requirements, + ResponseType.block_output, + ResponseType.error, +]); + +export function isRunBlockSetupRequirementsOutput( + output: RunBlockToolOutput, +): output is SetupRequirementsResponse { + return ( + output.type === ResponseType.setup_requirements || + ("setup_info" in output && typeof output.setup_info === "object") + ); +} + +export function isRunBlockBlockOutput( + output: RunBlockToolOutput, +): output is BlockOutputResponse { + return output.type === ResponseType.block_output || "block_id" in output; +} + +export function isRunBlockErrorOutput( + output: RunBlockToolOutput, +): output is ErrorResponse { + return output.type === ResponseType.error || "error" in output; +} function parseOutput(output: unknown): RunBlockToolOutput | null { if (!output) return null; @@ -77,12 +52,21 @@ function parseOutput(output: unknown): RunBlockToolOutput | null { const trimmed = output.trim(); if (!trimmed) return null; try { - return JSON.parse(trimmed) as RunBlockToolOutput; + return parseOutput(JSON.parse(trimmed) as unknown); } catch { return null; } } - if (typeof output === "object") return output as RunBlockToolOutput; + if (typeof output === "object") { + const type = (output as { type?: unknown }).type; + if (typeof type === "string" && RUN_BLOCK_OUTPUT_TYPES.has(type)) { + return output as RunBlockToolOutput; + } + if ("block_id" in output) return output as BlockOutputResponse; + if ("setup_info" in output) return output as SetupRequirementsResponse; + if ("error" in output || "details" in output) + return output as ErrorResponse; + } return null; } @@ -115,9 +99,9 @@ export function getAnimationText(part: { case "output-available": { const output = parseOutput(part.output); if (!output) return "Block run updated"; - if (output.type === "block_output") + if (isRunBlockBlockOutput(output)) return `Block ran: ${output.block_name}`; - if (output.type === "setup_requirements") { + if (isRunBlockSetupRequirementsOutput(output)) { return `Needs setup: ${output.setup_info.agent_name}`; } return "Error running block"; diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/SearchDocs/SearchDocs.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/SearchDocs/SearchDocs.tsx index b1a037b95f..98c592543e 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/SearchDocs/SearchDocs.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/SearchDocs/SearchDocs.tsx @@ -10,6 +10,10 @@ import { getDocsToolTitle, getToolLabel, getAnimationText, + isDocPageOutput, + isDocSearchResultsOutput, + isErrorOutput, + isNoResultsOutput, StateIcon, toDocsUrl, type DocsToolType, @@ -46,21 +50,32 @@ export function SearchDocsTool({ part }: Props) { const isOutputAvailable = part.state === "output-available" && !!output; + const docSearchOutput = + isOutputAvailable && output && isDocSearchResultsOutput(output) + ? output + : null; + const docPageOutput = + isOutputAvailable && output && isDocPageOutput(output) ? output : null; + const noResultsOutput = + isOutputAvailable && output && isNoResultsOutput(output) ? output : null; + const errorOutput = + isOutputAvailable && output && isErrorOutput(output) ? output : null; + const hasExpandableContent = isOutputAvailable && - ((output.type === "doc_search_results" && output.count > 0) || - output.type === "doc_page" || - output.type === "no_results" || - output.type === "error"); + ((!!docSearchOutput && docSearchOutput.count > 0) || + !!docPageOutput || + !!noResultsOutput || + !!errorOutput); const accordionDescription = - hasExpandableContent && output - ? output.type === "doc_search_results" - ? `Found ${output.count} result${output.count === 1 ? "" : "s"} for "${output.query}"` - : output.type === "doc_page" - ? output.path - : output.message - : null; + hasExpandableContent && docSearchOutput + ? `Found ${docSearchOutput.count} result${docSearchOutput.count === 1 ? "" : "s"} for "${docSearchOutput.query}"` + : hasExpandableContent && docPageOutput + ? docPageOutput.path + : hasExpandableContent && (noResultsOutput || errorOutput) + ? ((noResultsOutput ?? errorOutput)?.message ?? null) + : null; return (
@@ -75,9 +90,9 @@ export function SearchDocsTool({ part }: Props) { title={normalized.title} description={accordionDescription} > - {output.type === "doc_search_results" && ( + {docSearchOutput && (
- {output.results.map((r) => { + {docSearchOutput.results.map((r) => { const href = r.doc_url ?? toDocsUrl(r.path); return (
)} - {output.type === "doc_page" && ( + {docPageOutput && (

- {output.title} + {docPageOutput.title}

- {output.path} + {docPageOutput.path}

- {truncate(output.content, 800)} + {truncate(docPageOutput.content, 800)}

)} - {output.type === "no_results" && ( + {noResultsOutput && (
-

{output.message}

- {output.suggestions && output.suggestions.length > 0 && ( -
    - {output.suggestions.slice(0, 5).map((s) => ( -
  • {s}
  • - ))} -
- )} +

+ {noResultsOutput.message} +

+ {noResultsOutput.suggestions && + noResultsOutput.suggestions.length > 0 && ( +
    + {noResultsOutput.suggestions.slice(0, 5).map((s) => ( +
  • {s}
  • + ))} +
+ )}
)} - {output.type === "error" && ( + {errorOutput && (
-

{output.message}

- {output.error && ( +

{errorOutput.message}

+ {errorOutput.error && (

- {output.error} + {errorOutput.error}

)}
diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/SearchDocs/helpers.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/SearchDocs/helpers.tsx index 2c1583ce6e..16248bfc4a 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/SearchDocs/helpers.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/SearchDocs/helpers.tsx @@ -4,6 +4,11 @@ import { CircleNotchIcon, XCircleIcon, } from "@phosphor-icons/react"; +import type { DocPageResponse } from "@/app/api/__generated__/models/docPageResponse"; +import type { DocSearchResultsResponse } from "@/app/api/__generated__/models/docSearchResultsResponse"; +import type { ErrorResponse } from "@/app/api/__generated__/models/errorResponse"; +import type { NoResultsResponse } from "@/app/api/__generated__/models/noResultsResponse"; +import { ResponseType } from "@/app/api/__generated__/models/responseType"; export interface SearchDocsInput { query: string; @@ -13,53 +18,11 @@ export interface GetDocPageInput { path: string; } -export interface DocSearchResult { - title: string; - path: string; - section: string; - snippet: string; - score: number; - doc_url?: string | null; -} - -export interface DocSearchResultsOutput { - type: "doc_search_results"; - message: string; - session_id?: string; - results: DocSearchResult[]; - count: number; - query: string; -} - -export interface DocPageOutput { - type: "doc_page"; - message: string; - session_id?: string; - title: string; - path: string; - content: string; - doc_url?: string | null; -} - -export interface NoResultsOutput { - type: "no_results"; - message: string; - suggestions?: string[]; - session_id?: string; -} - -export interface ErrorOutput { - type: "error"; - message: string; - error?: string; - session_id?: string; -} - export type DocsToolOutput = - | DocSearchResultsOutput - | DocPageOutput - | NoResultsOutput - | ErrorOutput; + | DocSearchResultsResponse + | DocPageResponse + | NoResultsResponse + | ErrorResponse; export type DocsToolType = "tool-search_docs" | "tool-get_doc_page" | string; @@ -80,13 +43,29 @@ function parseOutput(output: unknown): DocsToolOutput | null { const trimmed = output.trim(); if (!trimmed) return null; try { - return JSON.parse(trimmed) as DocsToolOutput; + return parseOutput(JSON.parse(trimmed) as unknown); } catch { return null; } } if (typeof output === "object") { - return output as DocsToolOutput; + const type = (output as { type?: unknown }).type; + if ( + type === ResponseType.doc_search_results || + type === ResponseType.doc_page || + type === ResponseType.no_results || + type === ResponseType.error + ) { + return output as DocsToolOutput; + } + if ("results" in output && "query" in output) + return output as DocSearchResultsResponse; + if ("content" in output && "path" in output) + return output as DocPageResponse; + if ("suggestions" in output && !("error" in output)) + return output as NoResultsResponse; + if ("error" in output || "details" in output) + return output as ErrorResponse; } return null; } @@ -96,18 +75,43 @@ export function getDocsToolOutput(part: unknown): DocsToolOutput | null { return parseOutput((part as { output?: unknown }).output); } +export function isDocSearchResultsOutput( + output: DocsToolOutput, +): output is DocSearchResultsResponse { + return output.type === ResponseType.doc_search_results || "results" in output; +} + +export function isDocPageOutput( + output: DocsToolOutput, +): output is DocPageResponse { + return output.type === ResponseType.doc_page || "content" in output; +} + +export function isNoResultsOutput( + output: DocsToolOutput, +): output is NoResultsResponse { + return ( + output.type === ResponseType.no_results || + ("suggestions" in output && !("error" in output)) + ); +} + +export function isErrorOutput(output: DocsToolOutput): output is ErrorResponse { + return output.type === ResponseType.error || "error" in output; +} + export function getDocsToolTitle( toolType: DocsToolType, output: DocsToolOutput, ): string { if (toolType === "tool-search_docs") { - if (output.type === "doc_search_results") return "Documentation results"; - if (output.type === "no_results") return "No documentation found"; + if (isDocSearchResultsOutput(output)) return "Documentation results"; + if (isNoResultsOutput(output)) return "No documentation found"; return "Documentation search error"; } - if (output.type === "doc_page") return "Documentation page"; - if (output.type === "no_results") return "No documentation found"; + if (isDocPageOutput(output)) return "Documentation page"; + if (isNoResultsOutput(output)) return "No documentation found"; return "Documentation page error"; } @@ -134,13 +138,13 @@ export function getAnimationText(part: { part.input as SearchDocsInput | undefined )?.query?.trim(); if (!output) return "Found documentation"; - if (output.type === "doc_search_results") { + if (isDocSearchResultsOutput(output)) { const count = output.count ?? output.results.length; return query ? `Found ${count} doc result${count === 1 ? "" : "s"} for "${query}"` : `Found ${count} doc result${count === 1 ? "" : "s"}`; } - if (output.type === "no_results") { + if (isNoResultsOutput(output)) { return query ? `No docs found for "${query}"` : "No docs found"; } return "Error searching docs"; @@ -164,9 +168,8 @@ export function getAnimationText(part: { case "output-available": { const output = parseOutput(part.output); if (!output) return "Loaded documentation page"; - if (output.type === "doc_page") return `Loaded "${output.title}"`; - if (output.type === "no_results") - return "Documentation page not found"; + if (isDocPageOutput(output)) return `Loaded "${output.title}"`; + if (isNoResultsOutput(output)) return "Documentation page not found"; return "Error loading documentation page"; } case "output-error": diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/ViewAgentOutput/ViewAgentOutput.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/ViewAgentOutput/ViewAgentOutput.tsx index 11277d3eee..5e31bf5019 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/ViewAgentOutput/ViewAgentOutput.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/ViewAgentOutput/ViewAgentOutput.tsx @@ -8,6 +8,9 @@ import { formatMaybeJson, getAnimationText, getViewAgentOutputToolOutput, + isAgentOutputResponse, + isErrorResponse, + isNoResultsResponse, StateIcon, type ViewAgentOutputToolOutput, } from "./helpers"; @@ -29,7 +32,7 @@ function getAccordionMeta(output: ViewAgentOutputToolOutput): { title: string; description?: string; } { - if (output.type === "agent_output") { + if (isAgentOutputResponse(output)) { const status = output.execution?.status; return { badgeText: "Agent output", @@ -37,7 +40,7 @@ function getAccordionMeta(output: ViewAgentOutputToolOutput): { description: status ? `Status: ${status}` : output.message, }; } - if (output.type === "no_results") { + if (isNoResultsResponse(output)) { return { badgeText: "Agent output", title: "No results" }; } return { badgeText: "Agent output", title: "Error" }; @@ -50,9 +53,9 @@ export function ViewAgentOutputTool({ part }: Props) { const hasExpandableContent = part.state === "output-available" && !!output && - (output.type === "agent_output" || - output.type === "no_results" || - output.type === "error"); + (isAgentOutputResponse(output) || + isNoResultsResponse(output) || + isErrorResponse(output)); return (
@@ -63,7 +66,7 @@ export function ViewAgentOutputTool({ part }: Props) { {hasExpandableContent && output && ( - {output.type === "agent_output" && ( + {isAgentOutputResponse(output) && (

{output.message}

@@ -136,7 +139,7 @@ export function ViewAgentOutputTool({ part }: Props) {
)} - {output.type === "no_results" && ( + {isNoResultsResponse(output) && (

{output.message}

{output.suggestions && output.suggestions.length > 0 && ( @@ -149,7 +152,7 @@ export function ViewAgentOutputTool({ part }: Props) {
)} - {output.type === "error" && ( + {isErrorResponse(output) && (

{output.message}

{output.error && ( diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/ViewAgentOutput/helpers.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/ViewAgentOutput/helpers.tsx index 96327ecbd6..72573372ce 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/ViewAgentOutput/helpers.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/ViewAgentOutput/helpers.tsx @@ -4,6 +4,10 @@ import { CircleNotchIcon, XCircleIcon, } from "@phosphor-icons/react"; +import type { AgentOutputResponse } from "@/app/api/__generated__/models/agentOutputResponse"; +import type { ErrorResponse } from "@/app/api/__generated__/models/errorResponse"; +import type { NoResultsResponse } from "@/app/api/__generated__/models/noResultsResponse"; +import { ResponseType } from "@/app/api/__generated__/models/responseType"; export interface ViewAgentOutputInput { agent_name?: string; @@ -13,47 +17,10 @@ export interface ViewAgentOutputInput { run_time?: string; } -export interface ExecutionOutputInfo { - execution_id: string; - status: string; - started_at?: string | null; - ended_at?: string | null; - outputs: Record; - inputs_summary?: Record | null; -} - -export interface AgentOutputOutput { - type: "agent_output"; - message: string; - session_id?: string; - agent_name: string; - agent_id: string; - library_agent_id?: string | null; - library_agent_link?: string | null; - execution?: ExecutionOutputInfo | null; - available_executions?: Array> | null; - total_executions: number; -} - -export interface NoResultsOutput { - type: "no_results"; - message: string; - session_id?: string; - suggestions?: string[]; -} - -export interface ErrorOutput { - type: "error"; - message: string; - session_id?: string; - error?: string | null; - details?: Record | null; -} - export type ViewAgentOutputToolOutput = - | AgentOutputOutput - | NoResultsOutput - | ErrorOutput; + | AgentOutputResponse + | NoResultsResponse + | ErrorResponse; function parseOutput(output: unknown): ViewAgentOutputToolOutput | null { if (!output) return null; @@ -61,15 +28,53 @@ function parseOutput(output: unknown): ViewAgentOutputToolOutput | null { const trimmed = output.trim(); if (!trimmed) return null; try { - return JSON.parse(trimmed) as ViewAgentOutputToolOutput; + return parseOutput(JSON.parse(trimmed) as unknown); } catch { return null; } } - if (typeof output === "object") return output as ViewAgentOutputToolOutput; + if (typeof output === "object") { + const type = (output as { type?: unknown }).type; + if ( + type === ResponseType.agent_output || + type === ResponseType.no_results || + type === ResponseType.error + ) { + return output as ViewAgentOutputToolOutput; + } + if ("agent_id" in output && "agent_name" in output) { + return output as AgentOutputResponse; + } + if ("suggestions" in output && !("error" in output)) { + return output as NoResultsResponse; + } + if ("error" in output || "details" in output) + return output as ErrorResponse; + } return null; } +export function isAgentOutputResponse( + output: ViewAgentOutputToolOutput, +): output is AgentOutputResponse { + return output.type === ResponseType.agent_output || "agent_id" in output; +} + +export function isNoResultsResponse( + output: ViewAgentOutputToolOutput, +): output is NoResultsResponse { + return ( + output.type === ResponseType.no_results || + ("suggestions" in output && !("error" in output)) + ); +} + +export function isErrorResponse( + output: ViewAgentOutputToolOutput, +): output is ErrorResponse { + return output.type === ResponseType.error || "error" in output; +} + export function getViewAgentOutputToolOutput( part: unknown, ): ViewAgentOutputToolOutput | null { @@ -106,12 +111,12 @@ export function getAnimationText(part: { case "output-available": { const output = parseOutput(part.output); if (!output) return "Loaded agent outputs"; - if (output.type === "agent_output") { + if (isAgentOutputResponse(output)) { if (output.execution) return `Loaded output (${output.execution.status})`; return "Loaded agent outputs"; } - if (output.type === "no_results") return "No outputs found"; + if (isNoResultsResponse(output)) return "No outputs found"; return "Error loading agent output"; } case "output-error":