Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions frontend/bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -50,6 +50,7 @@ const ChatItemArea: FC<ChatItemAreaProps> = ({ items }) => {
isActive={currentSection?.item_id === item.item_id}
/>
);

default:
return (
<UnknownRenderer
Expand Down
31 changes: 31 additions & 0 deletions frontend/src/components/ui/collapsible.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"

function Collapsible({
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />
}

function CollapsibleTrigger({
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
return (
<CollapsiblePrimitive.CollapsibleTrigger
data-slot="collapsible-trigger"
{...props}
/>
)
}

function CollapsibleContent({
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
return (
<CollapsiblePrimitive.CollapsibleContent
data-slot="collapsible-content"
{...props}
/>
)
}

export { Collapsible, CollapsibleTrigger, CollapsibleContent }
2 changes: 2 additions & 0 deletions frontend/src/components/valuecell/renderer/index.tsx
Original file line number Diff line number Diff line change
@@ -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";
75 changes: 75 additions & 0 deletions frontend/src/components/valuecell/renderer/tool-call-renderer.tsx
Original file line number Diff line number Diff line change
@@ -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<ToolCallRendererProps> = ({ content }) => {
const [isOpen, setIsOpen] = useState(false);
const { tool_name, tool_result } = parse(content);
const tool_result_array = parse(tool_result);

return (
<Collapsible
open={isOpen}
onOpenChange={setIsOpen}
className={cn("min-w-96 rounded-xl p-3", styles["border-gradient"])}
data-active={isOpen}
>
<CollapsibleTrigger
className={cn(
"flex w-full items-center justify-between",
tool_result && "cursor-pointer",
)}
disabled={!tool_result}
>
<div className="flex items-center gap-2 text-gray-950">
{tool_result ? (
<Search className="size-5" />
) : (
<Spinner className="size-5" />
)}
<p className="text-base leading-5">{tool_name}</p>
</div>
{tool_result_array && (
<ChevronDown
className={cn(
"h-6 w-6 text-gray-950 transition-transform",
isOpen && "rotate-180",
)}
/>
)}
</CollapsibleTrigger>

{/* Collapsible Content */}
<CollapsibleContent>
<div className="flex flex-col gap-4 pt-2">
{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 ? (
<MarkdownRenderer
content={tool_result.content}
key={tool_result.content}
/>
) : (
<p key={tool_result}>${String(tool_result)}</p>
);
})}
</div>
</CollapsibleContent>
</Collapsible>
);
};

export default memo(ToolCallRenderer);
3 changes: 2 additions & 1 deletion frontend/src/constants/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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<string, string> = {
Expand Down
38 changes: 28 additions & 10 deletions frontend/src/lib/agent-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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":
Expand Down
20 changes: 4 additions & 16 deletions frontend/src/types/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/types/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -20,7 +21,7 @@ export type RendererPropsMap = {
sec_feed: SecFeedRendererProps;
report: ReportRendererProps;
markdown: MarkdownRendererProps;
tool_call: MarkdownRendererProps;
tool_call: ToolCallRendererProps;
};

/**
Expand Down
1 change: 1 addition & 0 deletions python/valuecell/core/coordinate/response.py
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,7 @@ def tool_call(
tool_result=tool_result,
),
role=Role.AGENT,
item_id=tool_call_id,
),
)

Expand Down