Files
socaichat/langgraph/src/agent/writer/nodes/tool-executor.ts
T
gongzhiyongandClaude Opus 4.6 86cfa317b6 feat: ChatGPT Enterprise 对标优化 — Prompt 重构 + 前端 P0 交互 + 错误处理
- 重构所有 Agent System Prompt,参考 ChatGPT 风格:自然对话、匹配用户沟通风格、避免翻译腔
- Router 路由规则结构化,新增 Decision Rules 和分组示例,提升路由准确率
- 前端 P0:textarea 多行输入 + Shift+Enter 换行、流式打字光标动画、编辑已发送消息
- 新增 ToolCallStatus 组件、process-attachment 文件处理节点、retry 重试工具
- 工具调用错误响应友好化(formatToolError 中文提示)
- Enterprise tool descriptions 增加使用场景说明
- 上下文截断提示改为中文
- 新增 soc-product-agent 产品经理 Agent 定义

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 14:35:45 +08:00

248 lines
7.5 KiB
TypeScript

/**
* Writer tool executor node: executes doc_create / doc_edit / doc_translate,
* pushes Gen-UI canvas-doc cards, and returns ToolMessages.
*/
import { typedUi } from "@langchain/langgraph-sdk/react-ui/server";
import type ComponentMap from "../../../agent-uis/index.js";
import { LangGraphRunnableConfig } from "@langchain/langgraph";
import { AIMessage } from "@langchain/core/messages";
import { createLlm, type ModelMode } from "@/agent/utils/create-llm";
import { WriterState, WriterUpdate } from "../types.js";
import {
docCreateSchema,
docEditSchema,
docTranslateSchema,
} from "./tool-defs.js";
import { formatToolError } from "@/agent/utils/retry";
/**
* Search backwards through messages to find the most recent canvas-doc content
* for a given doc_id. Returns the content string or undefined.
*/
function findDocContent(
state: WriterState,
docId: string,
): { title: string; content: string; type: "markdown" | "code"; language?: string } | undefined {
// Look through tool messages for a doc_create result containing this doc_id
for (let i = state.messages.length - 1; i >= 0; i--) {
const msg = state.messages[i];
if (msg._getType() === "tool") {
try {
const parsed = JSON.parse(
typeof msg.content === "string" ? msg.content : "",
);
if (parsed.doc_id === docId && parsed._doc_content) {
return {
title: parsed.title ?? "文档",
content: parsed._doc_content,
type: parsed.type ?? "markdown",
language: parsed.language,
};
}
} catch {
// not JSON, skip
}
}
}
return undefined;
}
export async function writerToolExecutorNode(
state: WriterState,
config: LangGraphRunnableConfig,
): Promise<WriterUpdate> {
const ui = typedUi<typeof ComponentMap>(config);
const modelMode = ((config.configurable?.modelMode as string) ?? "auto") as ModelMode;
const lastAiMessage = [...state.messages]
.reverse()
.find(
(m): m is AIMessage =>
(m as AIMessage).tool_calls !== undefined &&
((m as AIMessage).tool_calls?.length ?? 0) > 0,
) as AIMessage | undefined;
if (!lastAiMessage?.tool_calls?.length) {
return { ui: ui.items, timestamp: Date.now() };
}
const toolCalls = lastAiMessage.tool_calls!;
const toolMessages: Array<{
role: "tool";
tool_call_id: string;
content: string;
}> = [];
for (const tc of toolCalls) {
const name = tc.name;
const args = tc.args;
const id = tc.id ?? "";
try {
switch (name) {
case "doc_create": {
const parsed = docCreateSchema.parse(args);
const docId = `doc-${Date.now()}`;
ui.push(
{
name: "canvas-doc",
props: {
doc_id: docId,
title: parsed.title,
content: parsed.content,
type: parsed.type,
language: parsed.language ?? "",
},
},
{ message: lastAiMessage },
);
toolMessages.push({
role: "tool",
tool_call_id: id,
content: JSON.stringify({
doc_id: docId,
title: parsed.title,
type: parsed.type,
language: parsed.language,
char_count: parsed.content.length,
// Store content for later edits (not shown to user in text)
_doc_content: parsed.content,
}),
});
break;
}
case "doc_edit": {
const parsed = docEditSchema.parse(args);
const existing = findDocContent(state, parsed.doc_id);
const editLlm = createLlm({ modelMode, maxTokens: 8192 });
const originalContent = existing?.content ?? "(原文档内容不可用)";
const originalTitle = existing?.title ?? "文档";
const docType = existing?.type ?? "markdown";
const editResult = await editLlm.invoke([
{
role: "system",
content:
"你是文档编辑助手。根据用户指令修改以下文档,直接输出修改后的完整文档内容,不要添加任何解释或说明。",
},
{
role: "user",
content: `原文档标题: ${originalTitle}\n原文档内容:\n${originalContent}\n\n编辑指令: ${parsed.instructions}\n\n请直接输出修改后的完整文档内容:`,
},
]);
const editedContent =
typeof editResult.content === "string"
? editResult.content
: "";
ui.push(
{
name: "canvas-doc",
props: {
doc_id: parsed.doc_id,
title: originalTitle,
content: editedContent,
type: docType,
language: existing?.language ?? "",
},
},
{ message: lastAiMessage },
);
toolMessages.push({
role: "tool",
tool_call_id: id,
content: JSON.stringify({
doc_id: parsed.doc_id,
title: originalTitle,
type: docType,
char_count: editedContent.length,
_doc_content: editedContent,
}),
});
break;
}
case "doc_translate": {
const parsed = docTranslateSchema.parse(args);
const existing = findDocContent(state, parsed.doc_id);
const translateLlm = createLlm({ modelMode, maxTokens: 8192 });
const originalContent = existing?.content ?? "(原文档内容不可用)";
const originalTitle = existing?.title ?? "文档";
const docType = existing?.type ?? "markdown";
const translateResult = await translateLlm.invoke([
{
role: "system",
content: `你是专业翻译。将以下文档翻译为${parsed.target_language},保持原始格式和结构,直接输出翻译后的完整文档内容。`,
},
{
role: "user",
content: originalContent,
},
]);
const translatedContent =
typeof translateResult.content === "string"
? translateResult.content
: "";
const translatedTitle = `${originalTitle} (${parsed.target_language})`;
ui.push(
{
name: "canvas-doc",
props: {
doc_id: parsed.doc_id,
title: translatedTitle,
content: translatedContent,
type: docType,
language: existing?.language ?? "",
},
},
{ message: lastAiMessage },
);
toolMessages.push({
role: "tool",
tool_call_id: id,
content: JSON.stringify({
doc_id: parsed.doc_id,
title: translatedTitle,
type: docType,
char_count: translatedContent.length,
_doc_content: translatedContent,
}),
});
break;
}
default:
toolMessages.push({
role: "tool",
tool_call_id: id,
content: `Unknown tool: ${name}`,
});
}
} catch (e) {
toolMessages.push({
role: "tool",
tool_call_id: id,
content: formatToolError(name, e),
});
}
}
return {
messages: toolMessages,
ui: ui.items,
timestamp: Date.now(),
};
}