diff --git a/frontend/bun.lock b/frontend/bun.lock index 5fcd8bceb..adac41881 100644 --- a/frontend/bun.lock +++ b/frontend/bun.lock @@ -5,6 +5,7 @@ "name": "frontend", "dependencies": { "@radix-ui/react-avatar": "^1.1.10", + "@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-slot": "^1.2.3", @@ -223,6 +224,8 @@ "@radix-ui/react-avatar": ["@radix-ui/react-avatar@1.1.10", "", { "dependencies": { "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog=="], + "@radix-ui/react-collapsible": ["@radix-ui/react-collapsible@1.1.12", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA=="], + "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="], "@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], diff --git a/frontend/package.json b/frontend/package.json index 32fa4cbe5..f49f91455 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -18,6 +18,7 @@ }, "dependencies": { "@radix-ui/react-avatar": "^1.1.10", + "@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-slot": "^1.2.3", diff --git a/frontend/src/app/agent/components/chat-conversation/chat-item-area.tsx b/frontend/src/app/agent/components/chat-conversation/chat-item-area.tsx index dd7dc9b55..bce624138 100644 --- a/frontend/src/app/agent/components/chat-conversation/chat-item-area.tsx +++ b/frontend/src/app/agent/components/chat-conversation/chat-item-area.tsx @@ -1,5 +1,5 @@ import { type FC, memo } from "react"; -import UnknownRenderer from "@/components/valuecell/renderer/unknown-renderer"; +import { UnknownRenderer } from "@/components/valuecell/renderer"; import { COMPONENT_RENDERER_MAP } from "@/constants/agent"; import { cn } from "@/lib/utils"; import { useMultiSection } from "@/provider/multi-section-provider"; @@ -50,6 +50,7 @@ const ChatItemArea: FC = ({ items }) => { isActive={currentSection?.item_id === item.item_id} /> ); + default: return ( ) { + return +} + +function CollapsibleTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function CollapsibleContent({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Collapsible, CollapsibleTrigger, CollapsibleContent } diff --git a/frontend/src/components/valuecell/renderer/index.tsx b/frontend/src/components/valuecell/renderer/index.tsx index eede6c47b..d99cf0f0b 100644 --- a/frontend/src/components/valuecell/renderer/index.tsx +++ b/frontend/src/components/valuecell/renderer/index.tsx @@ -1,3 +1,5 @@ export { default as MarkdownRenderer } from "./markdown-renderer"; export { default as ReportRenderer } from "./report-renderer"; export { default as SecFeedRenderer } from "./sec-feed-renderer"; +export { default as ToolCallRenderer } from "./tool-call-renderer"; +export { default as UnknownRenderer } from "./unknown-renderer"; diff --git a/frontend/src/components/valuecell/renderer/tool-call-renderer.tsx b/frontend/src/components/valuecell/renderer/tool-call-renderer.tsx new file mode 100644 index 000000000..2c980a741 --- /dev/null +++ b/frontend/src/components/valuecell/renderer/tool-call-renderer.tsx @@ -0,0 +1,75 @@ +import { parse } from "best-effort-json-parser"; +import { ChevronDown, Search } from "lucide-react"; +import { type FC, memo, useState } from "react"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@/components/ui/collapsible"; +import { Spinner } from "@/components/ui/spinner"; +import { cn } from "@/lib/utils"; +import type { ToolCallRendererProps } from "@/types/renderer"; +import styles from "./index.module.css"; +import MarkdownRenderer from "./markdown-renderer"; + +const ToolCallRenderer: FC = ({ content }) => { + const [isOpen, setIsOpen] = useState(false); + const { tool_name, tool_result } = parse(content); + const tool_result_array = parse(tool_result); + + return ( + + +
+ {tool_result ? ( + + ) : ( + + )} +

{tool_name}

+
+ {tool_result_array && ( + + )} +
+ + {/* Collapsible Content */} + +
+ {tool_result_array && + Array.isArray(tool_result_array) && + // TODO: temporarily use content as result type, need to improve later + // biome-ignore lint/suspicious/noExplicitAny: temporarily use any as result type + tool_result_array?.map((tool_result: any) => { + return tool_result.content ? ( + + ) : ( +

${String(tool_result)}

+ ); + })} +
+
+
+ ); +}; + +export default memo(ToolCallRenderer); diff --git a/frontend/src/constants/agent.ts b/frontend/src/constants/agent.ts index 53c28e263..d4ad59874 100644 --- a/frontend/src/constants/agent.ts +++ b/frontend/src/constants/agent.ts @@ -22,6 +22,7 @@ import { MarkdownRenderer, ReportRenderer, SecFeedRenderer, + ToolCallRenderer, } from "@/components/valuecell/renderer"; import type { AgentComponentType } from "@/types/agent"; import type { RendererComponent } from "@/types/renderer"; @@ -49,7 +50,7 @@ export const COMPONENT_RENDERER_MAP: { sec_feed: SecFeedRenderer, report: ReportRenderer, markdown: MarkdownRenderer, - tool_call: MarkdownRenderer, + tool_call: ToolCallRenderer, }; export const AGENT_AVATAR_MAP: Record = { diff --git a/frontend/src/lib/agent-store.ts b/frontend/src/lib/agent-store.ts index 8a5accd2d..18cb0c630 100644 --- a/frontend/src/lib/agent-store.ts +++ b/frontend/src/lib/agent-store.ts @@ -57,13 +57,17 @@ function hasContent( } // Helper function: add or update item in task -function addOrUpdateItem(task: TaskView, newItem: ChatItem): void { +function addOrUpdateItem( + task: TaskView, + newItem: ChatItem, + event: "append" | "replace", +): void { const existingIndex = findExistingItem(task, newItem.item_id); if (existingIndex >= 0) { const existingItem = task.items[existingIndex]; // Merge content for streaming events, replace for others - if (hasContent(existingItem) && hasContent(newItem)) { + if (event === "append" && hasContent(existingItem) && hasContent(newItem)) { existingItem.payload.content += newItem.payload.content; } else { task.items[existingIndex] = newItem; @@ -74,7 +78,11 @@ function addOrUpdateItem(task: TaskView, newItem: ChatItem): void { } // Generic handler for events that create chat items -function handleChatItemEvent(draft: AgentConversationsStore, data: ChatItem) { +function handleChatItemEvent( + draft: AgentConversationsStore, + data: ChatItem, + event: "append" | "replace" = "append", +) { const { conversation, task } = ensurePath(draft, data); // Auto-maintain sections - only non-markdown types create independent sections @@ -100,13 +108,13 @@ function handleChatItemEvent(draft: AgentConversationsStore, data: ChatItem) { return; } - addOrUpdateItem(task, data); + addOrUpdateItem(task, data, event); } export function updateAgentConversationsStore( store: AgentConversationsStore, sseData: SSEData, -): AgentConversationsStore { +) { const { event, data } = sseData; // Use mutative to create new state with type-safe event handling @@ -131,11 +139,21 @@ export function updateAgentConversationsStore( handleChatItemEvent(draft, { component_type: "markdown", ...data }); break; - // TODO: tool call is not supported yet - // case "tool_call_started": - // case "tool_call_completed": - // handleChatItemEvent(draft, { component_type: "tool_call", ...data }); - // break; + case "tool_call_started": + case "tool_call_completed": { + handleChatItemEvent( + draft, + { + component_type: "tool_call", + ...data, + payload: { + content: JSON.stringify(data.payload), + }, + }, + "replace", + ); + break; + } case "reasoning_started": case "reasoning_completed": diff --git a/frontend/src/types/agent.ts b/frontend/src/types/agent.ts index d2aca3213..db8bf4873 100644 --- a/frontend/src/types/agent.ts +++ b/frontend/src/types/agent.ts @@ -35,27 +35,16 @@ export type AgentComponentMessage = MessageWithPayload<{ content: string; }>; -export type AgentToolCallStartedMessage = MessageWithPayload<{ +export type AgentToolCallMessage = MessageWithPayload<{ /** * @deprecated the tool call id is similar to the item_id */ tool_call_id: string; tool_name: string; -}>; - -export type AgentToolCallCompletedMessage = MessageWithPayload<{ - /** - * @deprecated the tool call id is similar to the item_id - */ - tool_call_id: string; - tool_name: string; - tool_call_result: string; + tool_call_result?: string; }>; type ChatMessage = AgentChunkMessage | AgentComponentMessage; -// TODO: tool call is not supported yet -// | AgentToolCallStartedMessage -// | AgentToolCallCompletedMessage; export type ChatItem = ChatMessage & { component_type: AgentComponentType; @@ -78,9 +67,8 @@ export interface AgentEventMap { plan_require_user_input: AgentPlanRequireUserInputMessage; // Tool Execution Lifecycle - tool_call_started: AgentToolCallStartedMessage; - - tool_call_completed: AgentToolCallCompletedMessage; + tool_call_started: AgentToolCallMessage; + tool_call_completed: AgentToolCallMessage; // Reasoning Process reasoning: AgentReasoningMessage; diff --git a/frontend/src/types/renderer.ts b/frontend/src/types/renderer.ts index b16fdd20c..0151405eb 100644 --- a/frontend/src/types/renderer.ts +++ b/frontend/src/types/renderer.ts @@ -11,6 +11,7 @@ export type ReportRendererProps = BaseRendererProps & { }; export type SecFeedRendererProps = BaseRendererProps; export type MarkdownRendererProps = BaseRendererProps; +export type ToolCallRendererProps = BaseRendererProps; /** * Mapping from component type to its corresponding props type @@ -20,7 +21,7 @@ export type RendererPropsMap = { sec_feed: SecFeedRendererProps; report: ReportRendererProps; markdown: MarkdownRendererProps; - tool_call: MarkdownRendererProps; + tool_call: ToolCallRendererProps; }; /** diff --git a/python/valuecell/core/coordinate/response.py b/python/valuecell/core/coordinate/response.py index 4c2f7c6ad..de97cdfaa 100644 --- a/python/valuecell/core/coordinate/response.py +++ b/python/valuecell/core/coordinate/response.py @@ -380,6 +380,7 @@ def tool_call( tool_result=tool_result, ), role=Role.AGENT, + item_id=tool_call_id, ), )