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":