diff --git a/argi-studio/agent-chat-ui/package.json b/argi-studio/agent-chat-ui/package.json index 7abad4c8b..93a1eb210 100644 --- a/argi-studio/agent-chat-ui/package.json +++ b/argi-studio/agent-chat-ui/package.json @@ -17,7 +17,7 @@ "start": "next start", "lint": "next lint", "lint:fix": "next lint --fix", - "test": "node --test src/lib/argi-api.test.mjs", + "test": "node --test src/lib/argi-api.test.mjs src/components/graph/graph-output.test.mjs", "format": "prettier --write .", "format:check": "prettier --check ." }, diff --git a/argi-studio/agent-chat-ui/src/components/graph/GraphChatArea.tsx b/argi-studio/agent-chat-ui/src/components/graph/GraphChatArea.tsx index 5a3029e4b..ac9c6af7f 100644 --- a/argi-studio/agent-chat-ui/src/components/graph/GraphChatArea.tsx +++ b/argi-studio/agent-chat-ui/src/components/graph/GraphChatArea.tsx @@ -4,13 +4,7 @@ import React, { useState, FormEvent, useMemo } from "react"; import { useGraphStream } from "@/providers/GraphStream"; import { useGraphThreads } from "@/providers/GraphThread"; import { Button } from "@/components/ui/button"; - -function extractTextFromState(state: Record | null): string { - if (!state?.messages || !Array.isArray(state.messages)) return ""; - const messages = state.messages as Array<{ content?: string; text?: string }>; - const last = messages[messages.length - 1]; - return last?.content ?? last?.text ?? ""; -} +import { extractTextContent, extractTextFromState } from "./graph-output"; export function GraphChatArea() { const { @@ -40,10 +34,11 @@ export function GraphChatArea() { if (accumulatedChunk) return accumulatedChunk; const fromState = extractTextFromState(currentState); if (fromState) return fromState; - const lastWithMessage = [...nodeOutputs] + const lastMessageText = [...nodeOutputs] .reverse() - .find((o) => o.message?.content); - return lastWithMessage?.message?.content ?? ""; + .map((output) => extractTextContent(output.message?.content)) + .find((text) => text.length > 0); + return lastMessageText ?? ""; }, [accumulatedChunk, currentState, nodeOutputs]); return ( diff --git a/argi-studio/agent-chat-ui/src/components/graph/graph-output.test.mjs b/argi-studio/agent-chat-ui/src/components/graph/graph-output.test.mjs new file mode 100644 index 000000000..26a8d07c4 --- /dev/null +++ b/argi-studio/agent-chat-ui/src/components/graph/graph-output.test.mjs @@ -0,0 +1,68 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; +import test from "node:test"; +import vm from "node:vm"; +import { createRequire } from "node:module"; + +const require = createRequire(import.meta.url); +const ts = require("typescript"); + +function loadGraphOutputModule() { + const sourcePath = path.resolve("src/components/graph/graph-output.ts"); + const source = fs.readFileSync(sourcePath, "utf8"); + + const { outputText } = ts.transpileModule(source, { + compilerOptions: { + module: ts.ModuleKind.CommonJS, + target: ts.ScriptTarget.ES2022, + }, + }); + + const module = { exports: {} }; + + vm.runInNewContext(outputText, { + exports: module.exports, + module, + }); + + return module.exports; +} + +test("preserves legacy string message content", () => { + const { extractTextContent } = loadGraphOutputModule(); + + assert.equal(extractTextContent("Hello ARGI"), "Hello ARGI"); +}); + +test("extracts text from structured content parts", () => { + const { extractTextContent } = loadGraphOutputModule(); + const content = [ + { type: "TEXT", text: "Hello " }, + { type: "TEXT", text: "ARGI" }, + ]; + + assert.equal(extractTextContent(content), "Hello ARGI"); +}); + +test("uses textual state result when no message text exists", () => { + const { extractTextFromState } = loadGraphOutputModule(); + const state = { result: "echo: hello" }; + + assert.equal(extractTextFromState(state), "echo: hello"); +}); + +test("uses state result when messages contain only user content", () => { + const { extractTextFromState } = loadGraphOutputModule(); + const state = { + result: "echo: hello", + messages: [ + { + role: "USER", + content: [{ type: "TEXT", text: "hello" }], + }, + ], + }; + + assert.equal(extractTextFromState(state), "echo: hello"); +}); diff --git a/argi-studio/agent-chat-ui/src/components/graph/graph-output.ts b/argi-studio/agent-chat-ui/src/components/graph/graph-output.ts new file mode 100644 index 000000000..27fc741d0 --- /dev/null +++ b/argi-studio/agent-chat-ui/src/components/graph/graph-output.ts @@ -0,0 +1,77 @@ +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +export function extractTextContent(content: unknown): string { + if (typeof content === "string") { + return content; + } + + if (!Array.isArray(content)) { + return ""; + } + + return content + .map((part) => { + if ( + !isRecord(part) || + part.type !== "TEXT" || + typeof part.text !== "string" + ) { + return ""; + } + + return part.text; + }) + .join(""); +} + +export function extractTextFromState( + state: Record | null, +): string { + if (!state) { + return ""; + } + + let legacyMessageText = ""; + if (Array.isArray(state.messages)) { + for (let index = state.messages.length - 1; index >= 0; index -= 1) { + const message = state.messages[index]; + if (!isRecord(message)) { + continue; + } + + let messageText = extractTextContent(message.content); + if (!messageText && typeof message.text === "string") { + messageText = message.text; + } + if (!messageText) { + continue; + } + + const role = + typeof message.role === "string" + ? message.role + : typeof message.messageType === "string" + ? message.messageType + : ""; + if (role.toUpperCase() === "ASSISTANT") { + return messageText; + } + + if ( + !("role" in message) && + !("messageType" in message) && + !legacyMessageText + ) { + legacyMessageText = messageText; + } + } + } + + if (typeof state.result === "string") { + return state.result; + } + + return legacyMessageText; +} diff --git a/argi-studio/agent-chat-ui/src/providers/GraphStream.tsx b/argi-studio/agent-chat-ui/src/providers/GraphStream.tsx index b00704016..8653d4c52 100644 --- a/argi-studio/agent-chat-ui/src/providers/GraphStream.tsx +++ b/argi-studio/agent-chat-ui/src/providers/GraphStream.tsx @@ -13,6 +13,7 @@ import React, { import { createApiClient, GraphRunResponse } from "@/lib/argi-api"; import { toast } from "sonner"; import { useGraphThreads } from "./GraphThread"; +import { extractTextContent } from "@/components/graph/graph-output"; export interface GraphNodeOutput extends GraphRunResponse { /** Index in execution order */ @@ -105,7 +106,8 @@ export function GraphStreamProvider({ children }: GraphStreamProviderProps) { if (event.state && Object.keys(event.state).length > 0) { setCurrentState(event.state as Record); } - const text = event.chunk ?? event.message?.content ?? ""; + const text = + event.chunk ?? extractTextContent(event.message?.content); if (text) { setAccumulatedChunk((prev) => prev + text); }