mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-02-04 03:45:12 -05:00
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.
This commit is contained in:
@@ -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<string, unknown>;
|
||||
|
||||
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<unknown, UIDataTypes, UITools>[] {
|
||||
const messages = coerceSessionChatMessages(rawMessages);
|
||||
const toolOutputsByCallId = new Map<string, unknown>();
|
||||
|
||||
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<unknown, UIDataTypes, UITools>[] = [];
|
||||
|
||||
messages.forEach((msg, index) => {
|
||||
if (msg.role === "tool") return;
|
||||
if (msg.role !== "user" && msg.role !== "assistant") return;
|
||||
|
||||
const parts: UIMessage<unknown, UIDataTypes, UITools>["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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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<string, string>) {
|
||||
const contextMessage = Object.entries(answers)
|
||||
@@ -95,10 +106,10 @@ export function CreateAgentTool({ part }: Props) {
|
||||
{hasExpandableContent && output && (
|
||||
<ToolAccordion
|
||||
{...getAccordionMeta(output)}
|
||||
defaultExpanded={output.type === "clarification_needed"}
|
||||
defaultExpanded={isClarificationNeededOutput(output)}
|
||||
>
|
||||
{(output.type === "operation_started" ||
|
||||
output.type === "operation_pending") && (
|
||||
{(isOperationStartedOutput(output) ||
|
||||
isOperationPendingOutput(output)) && (
|
||||
<div className="grid gap-2">
|
||||
<p className="text-sm text-foreground">{output.message}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
@@ -110,7 +121,7 @@ export function CreateAgentTool({ part }: Props) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{output.type === "operation_in_progress" && (
|
||||
{isOperationInProgressOutput(output) && (
|
||||
<div className="grid gap-2">
|
||||
<p className="text-sm text-foreground">{output.message}</p>
|
||||
<p className="text-xs italic text-muted-foreground">
|
||||
@@ -119,7 +130,7 @@ export function CreateAgentTool({ part }: Props) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{output.type === "agent_saved" && (
|
||||
{isAgentSavedOutput(output) && (
|
||||
<div className="grid gap-2">
|
||||
<p className="text-sm text-foreground">{output.message}</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
@@ -145,7 +156,7 @@ export function CreateAgentTool({ part }: Props) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{output.type === "agent_preview" && (
|
||||
{isAgentPreviewOutput(output) && (
|
||||
<div className="grid gap-2">
|
||||
<p className="text-sm text-foreground">{output.message}</p>
|
||||
{output.description?.trim() && (
|
||||
@@ -159,15 +170,26 @@ export function CreateAgentTool({ part }: Props) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{output.type === "clarification_needed" && (
|
||||
{isClarificationNeededOutput(output) && (
|
||||
<ClarificationQuestionsWidget
|
||||
questions={output.questions}
|
||||
questions={(output.questions ?? []).map((q) => {
|
||||
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) && (
|
||||
<div className="grid gap-2">
|
||||
<p className="text-sm text-foreground">{output.message}</p>
|
||||
{output.error && (
|
||||
|
||||
@@ -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<string, unknown>;
|
||||
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<string, unknown> | 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":
|
||||
|
||||
@@ -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<string, string>) {
|
||||
const contextMessage = Object.entries(answers)
|
||||
@@ -95,10 +106,10 @@ export function EditAgentTool({ part }: Props) {
|
||||
{hasExpandableContent && output && (
|
||||
<ToolAccordion
|
||||
{...getAccordionMeta(output)}
|
||||
defaultExpanded={output.type === "clarification_needed"}
|
||||
defaultExpanded={isClarificationNeededOutput(output)}
|
||||
>
|
||||
{(output.type === "operation_started" ||
|
||||
output.type === "operation_pending") && (
|
||||
{(isOperationStartedOutput(output) ||
|
||||
isOperationPendingOutput(output)) && (
|
||||
<div className="grid gap-2">
|
||||
<p className="text-sm text-foreground">{output.message}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
@@ -110,7 +121,7 @@ export function EditAgentTool({ part }: Props) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{output.type === "operation_in_progress" && (
|
||||
{isOperationInProgressOutput(output) && (
|
||||
<div className="grid gap-2">
|
||||
<p className="text-sm text-foreground">{output.message}</p>
|
||||
<p className="text-xs italic text-muted-foreground">
|
||||
@@ -119,7 +130,7 @@ export function EditAgentTool({ part }: Props) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{output.type === "agent_saved" && (
|
||||
{isAgentSavedOutput(output) && (
|
||||
<div className="grid gap-2">
|
||||
<p className="text-sm text-foreground">{output.message}</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
@@ -145,7 +156,7 @@ export function EditAgentTool({ part }: Props) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{output.type === "agent_preview" && (
|
||||
{isAgentPreviewOutput(output) && (
|
||||
<div className="grid gap-2">
|
||||
<p className="text-sm text-foreground">{output.message}</p>
|
||||
{output.description?.trim() && (
|
||||
@@ -159,15 +170,26 @@ export function EditAgentTool({ part }: Props) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{output.type === "clarification_needed" && (
|
||||
{isClarificationNeededOutput(output) && (
|
||||
<ClarificationQuestionsWidget
|
||||
questions={output.questions}
|
||||
questions={(output.questions ?? []).map((q) => {
|
||||
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) && (
|
||||
<div className="grid gap-2">
|
||||
<p className="text-sm text-foreground">{output.message}</p>
|
||||
{output.error && (
|
||||
|
||||
@@ -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<string, unknown>;
|
||||
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<string, unknown> | 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":
|
||||
|
||||
@@ -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) {
|
||||
<MorphingTextAnimation text={text} />
|
||||
</div>
|
||||
|
||||
{hasAgents && (
|
||||
{hasAgents && agentsFoundOutput && (
|
||||
<ToolAccordion
|
||||
badgeText={sourceLabel}
|
||||
title="Agent results"
|
||||
description={accordionDescription}
|
||||
>
|
||||
<div className="grid gap-2 sm:grid-cols-2">
|
||||
{output.agents.map((agent) => {
|
||||
{agentsFoundOutput.agents.map((agent) => {
|
||||
const href = getAgentHref(agent);
|
||||
const agentSource =
|
||||
agent.source === "library"
|
||||
|
||||
@@ -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}`;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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<string, unknown>)
|
||||
: {};
|
||||
|
||||
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<string, unknown>;
|
||||
|
||||
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<string, unknown>;
|
||||
|
||||
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) {
|
||||
<ToolAccordion
|
||||
{...getAccordionMeta(output)}
|
||||
defaultExpanded={
|
||||
output.type === "setup_requirements" ||
|
||||
output.type === "agent_details"
|
||||
isRunAgentSetupRequirementsOutput(output) ||
|
||||
isRunAgentAgentDetailsOutput(output)
|
||||
}
|
||||
>
|
||||
{output.type === "execution_started" && (
|
||||
{isRunAgentExecutionStartedOutput(output) && (
|
||||
<div className="grid gap-2">
|
||||
<div className="rounded-2xl border bg-background p-3">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
@@ -131,7 +265,7 @@ export function RunAgentTool({ part }: Props) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{output.type === "agent_details" && (
|
||||
{isRunAgentAgentDetailsOutput(output) && (
|
||||
<div className="grid gap-2">
|
||||
<p className="text-sm text-foreground">{output.message}</p>
|
||||
|
||||
@@ -153,26 +287,17 @@ export function RunAgentTool({ part }: Props) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{output.type === "setup_requirements" && (
|
||||
{isRunAgentSetupRequirementsOutput(output) && (
|
||||
<div className="grid gap-2">
|
||||
<p className="text-sm text-foreground">{output.message}</p>
|
||||
|
||||
{Object.keys(
|
||||
output.setup_info.user_readiness.missing_credentials ?? {},
|
||||
{coerceMissingCredentials(
|
||||
output.setup_info.user_readiness?.missing_credentials,
|
||||
).length > 0 && (
|
||||
<ChatCredentialsSetup
|
||||
credentials={Object.values(
|
||||
output.setup_info.user_readiness.missing_credentials ?? {},
|
||||
).map((cred) => ({
|
||||
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<string, unknown>)
|
||||
?.inputs,
|
||||
).length > 0 && (
|
||||
<div className="rounded-2xl border bg-background p-3">
|
||||
<p className="text-xs font-medium text-foreground">
|
||||
Expected inputs
|
||||
</p>
|
||||
<div className="mt-2 grid gap-2">
|
||||
{output.setup_info.requirements.inputs.map((input) => (
|
||||
{coerceExpectedInputs(
|
||||
(
|
||||
output.setup_info.requirements as Record<
|
||||
string,
|
||||
unknown
|
||||
>
|
||||
)?.inputs,
|
||||
).map((input) => (
|
||||
<div key={input.name} className="rounded-xl border p-2">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<p className="truncate text-xs font-medium text-foreground">
|
||||
@@ -208,11 +343,11 @@ export function RunAgentTool({ part }: Props) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{output.type === "need_login" && (
|
||||
{isRunAgentNeedLoginOutput(output) && (
|
||||
<p className="text-sm text-foreground">{output.message}</p>
|
||||
)}
|
||||
|
||||
{output.type === "error" && (
|
||||
{isRunAgentErrorOutput(output) && (
|
||||
<div className="grid gap-2">
|
||||
<p className="text-sm text-foreground">{output.message}</p>
|
||||
{output.error && (
|
||||
|
||||
@@ -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<string, CredentialsMeta>;
|
||||
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<string, unknown> | 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<string, unknown>;
|
||||
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<string>([
|
||||
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":
|
||||
|
||||
@@ -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<string, unknown>)
|
||||
: {};
|
||||
|
||||
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<string, unknown>;
|
||||
|
||||
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<string, unknown>;
|
||||
|
||||
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 && (
|
||||
<ToolAccordion
|
||||
{...getAccordionMeta(output)}
|
||||
defaultExpanded={output.type === "setup_requirements"}
|
||||
defaultExpanded={isRunBlockSetupRequirementsOutput(output)}
|
||||
>
|
||||
{output.type === "block_output" && (
|
||||
{isRunBlockBlockOutput(output) && (
|
||||
<div className="grid gap-2">
|
||||
<p className="text-sm text-foreground">{output.message}</p>
|
||||
|
||||
@@ -111,26 +239,17 @@ export function RunBlockTool({ part }: Props) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{output.type === "setup_requirements" && (
|
||||
{isRunBlockSetupRequirementsOutput(output) && (
|
||||
<div className="grid gap-2">
|
||||
<p className="text-sm text-foreground">{output.message}</p>
|
||||
|
||||
{Object.keys(
|
||||
output.setup_info.user_readiness.missing_credentials ?? {},
|
||||
{coerceMissingCredentials(
|
||||
output.setup_info.user_readiness?.missing_credentials,
|
||||
).length > 0 && (
|
||||
<ChatCredentialsSetup
|
||||
credentials={Object.values(
|
||||
output.setup_info.user_readiness.missing_credentials ?? {},
|
||||
).map((cred) => ({
|
||||
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<string, unknown>)
|
||||
?.inputs,
|
||||
).length > 0 && (
|
||||
<div className="rounded-2xl border bg-background p-3">
|
||||
<p className="text-xs font-medium text-foreground">
|
||||
Expected inputs
|
||||
</p>
|
||||
<div className="mt-2 grid gap-2">
|
||||
{output.setup_info.requirements.inputs.map((input) => (
|
||||
{coerceExpectedInputs(
|
||||
(
|
||||
output.setup_info.requirements as Record<
|
||||
string,
|
||||
unknown
|
||||
>
|
||||
)?.inputs,
|
||||
).map((input) => (
|
||||
<div key={input.name} className="rounded-xl border p-2">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<p className="truncate text-xs font-medium text-foreground">
|
||||
@@ -166,7 +295,7 @@ export function RunBlockTool({ part }: Props) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{output.type === "error" && (
|
||||
{isRunBlockErrorOutput(output) && (
|
||||
<div className="grid gap-2">
|
||||
<p className="text-sm text-foreground">{output.message}</p>
|
||||
{output.error && (
|
||||
|
||||
@@ -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<string, unknown>;
|
||||
}
|
||||
|
||||
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<string, CredentialsMeta>;
|
||||
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<string, unknown[]>;
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
export interface ErrorOutput {
|
||||
type: "error";
|
||||
message: string;
|
||||
session_id?: string;
|
||||
error?: string | null;
|
||||
details?: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
export type RunBlockToolOutput =
|
||||
| SetupRequirementsOutput
|
||||
| BlockOutput
|
||||
| ErrorOutput;
|
||||
| SetupRequirementsResponse
|
||||
| BlockOutputResponse
|
||||
| ErrorResponse;
|
||||
|
||||
const RUN_BLOCK_OUTPUT_TYPES = new Set<string>([
|
||||
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";
|
||||
|
||||
@@ -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 (
|
||||
<div className="py-2">
|
||||
@@ -75,9 +90,9 @@ export function SearchDocsTool({ part }: Props) {
|
||||
title={normalized.title}
|
||||
description={accordionDescription}
|
||||
>
|
||||
{output.type === "doc_search_results" && (
|
||||
{docSearchOutput && (
|
||||
<div className="grid gap-2">
|
||||
{output.results.map((r) => {
|
||||
{docSearchOutput.results.map((r) => {
|
||||
const href = r.doc_url ?? toDocsUrl(r.path);
|
||||
return (
|
||||
<div
|
||||
@@ -112,19 +127,19 @@ export function SearchDocsTool({ part }: Props) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{output.type === "doc_page" && (
|
||||
{docPageOutput && (
|
||||
<div>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-sm font-medium text-foreground">
|
||||
{output.title}
|
||||
{docPageOutput.title}
|
||||
</p>
|
||||
<p className="mt-0.5 truncate text-xs text-muted-foreground">
|
||||
{output.path}
|
||||
{docPageOutput.path}
|
||||
</p>
|
||||
</div>
|
||||
<Link
|
||||
href={output.doc_url ?? toDocsUrl(output.path)}
|
||||
href={docPageOutput.doc_url ?? toDocsUrl(docPageOutput.path)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="shrink-0 text-xs font-medium text-purple-600 hover:text-purple-700"
|
||||
@@ -133,30 +148,33 @@ export function SearchDocsTool({ part }: Props) {
|
||||
</Link>
|
||||
</div>
|
||||
<p className="mt-2 whitespace-pre-wrap text-xs text-muted-foreground">
|
||||
{truncate(output.content, 800)}
|
||||
{truncate(docPageOutput.content, 800)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{output.type === "no_results" && (
|
||||
{noResultsOutput && (
|
||||
<div>
|
||||
<p className="text-sm text-foreground">{output.message}</p>
|
||||
{output.suggestions && output.suggestions.length > 0 && (
|
||||
<ul className="mt-2 list-disc space-y-1 pl-5 text-xs text-muted-foreground">
|
||||
{output.suggestions.slice(0, 5).map((s) => (
|
||||
<li key={s}>{s}</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
<p className="text-sm text-foreground">
|
||||
{noResultsOutput.message}
|
||||
</p>
|
||||
{noResultsOutput.suggestions &&
|
||||
noResultsOutput.suggestions.length > 0 && (
|
||||
<ul className="mt-2 list-disc space-y-1 pl-5 text-xs text-muted-foreground">
|
||||
{noResultsOutput.suggestions.slice(0, 5).map((s) => (
|
||||
<li key={s}>{s}</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{output.type === "error" && (
|
||||
{errorOutput && (
|
||||
<div>
|
||||
<p className="text-sm text-foreground">{output.message}</p>
|
||||
{output.error && (
|
||||
<p className="text-sm text-foreground">{errorOutput.message}</p>
|
||||
{errorOutput.error && (
|
||||
<p className="mt-2 text-xs text-muted-foreground">
|
||||
{output.error}
|
||||
{errorOutput.error}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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 (
|
||||
<div className="py-2">
|
||||
@@ -63,7 +66,7 @@ export function ViewAgentOutputTool({ part }: Props) {
|
||||
|
||||
{hasExpandableContent && output && (
|
||||
<ToolAccordion {...getAccordionMeta(output)}>
|
||||
{output.type === "agent_output" && (
|
||||
{isAgentOutputResponse(output) && (
|
||||
<div className="grid gap-2">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<p className="text-sm text-foreground">{output.message}</p>
|
||||
@@ -136,7 +139,7 @@ export function ViewAgentOutputTool({ part }: Props) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{output.type === "no_results" && (
|
||||
{isNoResultsResponse(output) && (
|
||||
<div className="grid gap-2">
|
||||
<p className="text-sm text-foreground">{output.message}</p>
|
||||
{output.suggestions && output.suggestions.length > 0 && (
|
||||
@@ -149,7 +152,7 @@ export function ViewAgentOutputTool({ part }: Props) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{output.type === "error" && (
|
||||
{isErrorResponse(output) && (
|
||||
<div className="grid gap-2">
|
||||
<p className="text-sm text-foreground">{output.message}</p>
|
||||
{output.error && (
|
||||
|
||||
@@ -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<string, unknown[]>;
|
||||
inputs_summary?: Record<string, unknown> | 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<Record<string, unknown>> | 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<string, unknown> | 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":
|
||||
|
||||
Reference in New Issue
Block a user