feat(frontend): Update chat service and UI to support Orchestrator architecture and native subgraph streaming

This commit is contained in:
Yunxiao Xu
2026-02-23 17:59:57 -08:00
parent b8d8651924
commit 8fcfc4ee88
3 changed files with 36 additions and 21 deletions

View File

@@ -9,9 +9,9 @@ interface ExecutionStatusProps {
const PHASE_CONFIG = [ const PHASE_CONFIG = [
{ label: "Analyzing query...", match: "Query analysis complete." }, { label: "Analyzing query...", match: "Query analysis complete." },
{ label: "Generating strategic plan...", match: "Strategic plan generated." }, { label: "Generating high-level plan...", match: "Checklist generated." },
{ label: "Writing analysis code...", match: "Analysis code generated." }, { label: "Delegating to specialists...", match: "Task assigned." },
{ label: "Performing data analysis...", match: "Data analysis and visualization complete." } { label: "Synthesizing final answer...", match: "Final synthesis complete." }
] ]
export function ExecutionStatus({ steps, isComplete, className }: ExecutionStatusProps) { export function ExecutionStatus({ steps, isComplete, className }: ExecutionStatusProps) {

View File

@@ -3,21 +3,21 @@ import { ChatService, type ChatEvent, type MessageResponse } from "./chat"
describe("ChatService SSE Parsing", () => { describe("ChatService SSE Parsing", () => {
it("should correctly parse a text stream chunk", () => { it("should correctly parse a text stream chunk", () => {
const rawChunk = `data: {"type": "on_chat_model_stream", "name": "summarizer", "data": {"chunk": "Hello"}}\n\n` const rawChunk = `data: {"type": "on_chat_model_stream", "name": "synthesizer", "data": {"chunk": "Hello"}}\n\n`
const events = ChatService.parseSSEChunk(rawChunk) const events = ChatService.parseSSEChunk(rawChunk)
expect(events).toHaveLength(1) expect(events).toHaveLength(1)
expect(events[0]).toEqual({ expect(events[0]).toEqual({
type: "on_chat_model_stream", type: "on_chat_model_stream",
name: "summarizer", name: "synthesizer",
data: { chunk: "Hello" } data: { chunk: "Hello" }
}) })
}) })
it("should handle multiple events in one chunk", () => { it("should handle multiple events in one chunk", () => {
const rawChunk = const rawChunk =
`data: {"type": "on_chat_model_stream", "name": "summarizer", "data": {"chunk": "Hello"}}\n\n` + `data: {"type": "on_chat_model_stream", "name": "synthesizer", "data": {"chunk": "Hello"}}\n\n` +
`data: {"type": "on_chat_model_stream", "name": "summarizer", "data": {"chunk": " World"}}\n\n` `data: {"type": "on_chat_model_stream", "name": "synthesizer", "data": {"chunk": " World"}}\n\n`
const events = ChatService.parseSSEChunk(rawChunk) const events = ChatService.parseSSEChunk(rawChunk)
@@ -25,8 +25,8 @@ describe("ChatService SSE Parsing", () => {
expect(events[1].data!.chunk).toBe(" World") expect(events[1].data!.chunk).toBe(" World")
}) })
it("should parse encoded plots from executor node", () => { it("should parse encoded plots from data_analyst_worker node", () => {
const rawChunk = `data: {"type": "on_chain_end", "name": "executor", "data": {"encoded_plots": ["base64data"]}}\n\n` const rawChunk = `data: {"type": "on_chain_end", "name": "data_analyst_worker", "data": {"encoded_plots": ["base64data"]}}\n\n`
const events = ChatService.parseSSEChunk(rawChunk) const events = ChatService.parseSSEChunk(rawChunk)
expect(events[0].data!.encoded_plots).toEqual(["base64data"]) expect(events[0].data!.encoded_plots).toEqual(["base64data"])
@@ -45,7 +45,7 @@ describe("ChatService Message State Management", () => {
const messages: MessageResponse[] = [{ id: "1", role: "assistant", content: "Initial", created_at: new Date().toISOString() }] const messages: MessageResponse[] = [{ id: "1", role: "assistant", content: "Initial", created_at: new Date().toISOString() }]
const event: ChatEvent = { const event: ChatEvent = {
type: "on_chat_model_stream", type: "on_chat_model_stream",
node: "summarizer", node: "synthesizer",
data: { chunk: { content: " text" } } data: { chunk: { content: " text" } }
} }
@@ -57,7 +57,7 @@ describe("ChatService Message State Management", () => {
const messages: MessageResponse[] = [{ id: "1", role: "assistant", content: "Analysis", created_at: new Date().toISOString(), plots: [] }] const messages: MessageResponse[] = [{ id: "1", role: "assistant", content: "Analysis", created_at: new Date().toISOString(), plots: [] }]
const event: ChatEvent = { const event: ChatEvent = {
type: "on_chain_end", type: "on_chain_end",
name: "executor", name: "data_analyst_worker",
data: { encoded_plots: ["plot1"] } data: { encoded_plots: ["plot1"] }
} }

View File

@@ -86,7 +86,8 @@ export const ChatService = {
const { type, name, node, data } = event const { type, name, node, data } = event
// 1. Handle incremental LLM chunks for terminal nodes // 1. Handle incremental LLM chunks for terminal nodes
if (type === "on_chat_model_stream" && (node === "summarizer" || node === "researcher" || node === "clarification")) { // Now using 'synthesizer' for the final user response
if (type === "on_chat_model_stream" && (node === "synthesizer" || node === "clarification")) {
const chunk = data?.chunk?.content || "" const chunk = data?.chunk?.content || ""
if (!chunk) return messages if (!chunk) return messages
@@ -110,7 +111,7 @@ export const ChatService = {
if (!lastMsg || lastMsg.role !== "assistant") return messages if (!lastMsg || lastMsg.role !== "assistant") return messages
// Terminal nodes final text // Terminal nodes final text
if (name === "summarizer" || name === "researcher" || name === "clarification") { if (name === "synthesizer" || name === "clarification") {
const messages_list = data?.output?.messages const messages_list = data?.output?.messages
const msg = messages_list ? messages_list[messages_list.length - 1]?.content : null const msg = messages_list ? messages_list[messages_list.length - 1]?.content : null
@@ -121,8 +122,8 @@ export const ChatService = {
} }
} }
// Plots from executor // Plots from data analyst worker
if (name === "executor" && data?.encoded_plots) { if (name === "data_analyst_worker" && data?.encoded_plots) {
lastMsg.plots = [...(lastMsg.plots || []), ...data.encoded_plots] lastMsg.plots = [...(lastMsg.plots || []), ...data.encoded_plots]
// Filter out the 'active' step and replace with 'complete' // Filter out the 'active' step and replace with 'complete'
const filteredSteps = (lastMsg.steps || []).filter(s => s !== "Performing data analysis..."); const filteredSteps = (lastMsg.steps || []).filter(s => s !== "Performing data analysis...");
@@ -134,15 +135,25 @@ export const ChatService = {
// Status for intermediate nodes (completion) // Status for intermediate nodes (completion)
const statusMap: Record<string, string> = { const statusMap: Record<string, string> = {
"query_analyzer": "Query analysis complete.", "query_analyzer": "Query analysis complete.",
"planner": "Strategic plan generated.", "planner": "Checklist generated.",
"coder": "Analysis code generated." "delegate": "Task assigned.",
"reflector": "Result verified.",
"coder": "Analysis code generated.",
"executor": "Code execution complete.",
"searcher": "Web search complete.",
"summarizer": "Task summary generated."
} }
if (name && statusMap[name]) { if (name && statusMap[name]) {
// Find and replace the active status if it exists // Find and replace the active status if it exists
const activeStatus = name === "query_analyzer" ? "Analyzing query..." : const activeStatus = name === "query_analyzer" ? "Analyzing query..." :
name === "planner" ? "Generating strategic plan..." : name === "planner" ? "Generating high-level plan..." :
name === "coder" ? "Writing analysis code..." : null; name === "delegate" ? "Routing task..." :
name === "reflector" ? "Evaluating results..." :
name === "coder" ? "Writing analysis code..." :
name === "executor" ? "Executing code..." :
name === "searcher" ? "Searching web..." :
name === "summarizer" ? "Summarizing results..." : null;
let filteredSteps = lastMsg.steps || []; let filteredSteps = lastMsg.steps || [];
if (activeStatus) { if (activeStatus) {
@@ -159,9 +170,13 @@ export const ChatService = {
if (type === "on_chain_start") { if (type === "on_chain_start") {
const startStatusMap: Record<string, string> = { const startStatusMap: Record<string, string> = {
"query_analyzer": "Analyzing query...", "query_analyzer": "Analyzing query...",
"planner": "Generating strategic plan...", "planner": "Generating high-level plan...",
"delegate": "Routing task...",
"reflector": "Evaluating results...",
"coder": "Writing analysis code...", "coder": "Writing analysis code...",
"executor": "Performing data analysis..." "executor": "Executing code...",
"searcher": "Searching web...",
"summarizer": "Summarizing results..."
} }
if (name && startStatusMap[name]) { if (name && startStatusMap[name]) {