Files
socaichat/langgraph/src/agent/searcher/nodes/tool-executor.ts
T
gongzhiyongandClaude Sonnet 4.6 2430fd2174
Deploy LangGraph Server to Azure Web App / build-and-deploy (push) Failing after 21s
Deploy LangGraph UI to Azure Static Web Apps / build-and-deploy (push) Failing after 42s
fix(P1): ticket UUID mapping + sandbox/ticket/search fallback improvements
- Fix ticket_detail UUID resolution: use find() to match ticketNumber instead of tickets[0]
- Add sandbox fallback hint in coder tool-executor catch block
- Add ticket_list empty result friendly hint with suggestions
- Add google_search auto-supplement with Jina when results < 3
- Enhance enterprise tool-executor general catch with fallback_hint

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 21:57:56 +08:00

234 lines
7.9 KiB
TypeScript

/**
* Searcher tool executor: executes search tool calls, pushes Gen-UI cards.
*/
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 { SearcherState, SearcherUpdate } from "../types.js";
import type { ToolExecStatus } from "../../types.js";
import {
googleSearch,
webSearch,
webRead,
jinaRerank,
} from "../../enterprise/tools/soc-client.js";
import {
googleSearchSchema,
webSearchDeepSchema,
webReadSchema,
} from "./agent.js";
import { executeWithRetry, formatToolError } from "@/agent/utils/retry";
export async function toolExecutorNode(
state: SearcherState,
config: LangGraphRunnableConfig,
): Promise<SearcherUpdate> {
const ui = typedUi<typeof ComponentMap>(config);
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;
}> = [];
const statusList: ToolExecStatus[] = [];
const executions = toolCalls.map(async (tc) => {
const name = tc.name;
const args = tc.args;
const id = tc.id ?? "";
try {
switch (name) {
case "google_search": {
const parsed = googleSearchSchema.parse(args);
let results: Array<{ title: string; url: string; snippet: string }>;
let knowledgeGraph: { title: string; description: string } | undefined;
let fallbackUsed = false;
try {
const data = await executeWithRetry(() => googleSearch(parsed.query));
results = data.results.map((r) => ({
title: r.title,
url: r.url,
snippet: r.snippet,
}));
knowledgeGraph = data.knowledgeGraph;
// Auto-supplement with Jina if results < 3
if (results.length < 3) {
try {
const supplementData = await webSearch(parsed.query + " 详细信息");
const supplementResults = supplementData.results.slice(0, 3 - results.length).map((r) => ({
title: r.title ?? "",
url: r.url ?? "",
snippet: (r.description ?? r.content ?? "").slice(0, 200),
}));
results = [...results, ...supplementResults];
} catch { /* supplement search failed silently */ }
}
} catch {
// google_search failed, fallback to web_search_deep (Jina)
console.warn(`[fallback] google_search failed, trying web_search_deep`);
const data = await webSearch(parsed.query);
results = (data.results ?? []).slice(0, 5).map((r) => ({
title: r.title ?? "",
url: r.url ?? "",
snippet: (r.description ?? r.content ?? "").slice(0, 200),
}));
fallbackUsed = true;
}
ui.push(
{
name: "search-result",
props: {
query: parsed.query,
total: results.length,
results,
sourceType: "external_web",
confidence: fallbackUsed ? "medium" : "high",
},
},
{ message: lastAiMessage },
);
statusList.push({
tool: "google_search",
status: fallbackUsed ? "fallback" : "ok",
...(fallbackUsed ? { message: "快速搜索不可用,已使用深度搜索替代" } : {}),
});
return {
role: "tool" as const,
tool_call_id: id,
content: JSON.stringify({
total: results.length,
results,
knowledgeGraph,
...(fallbackUsed ? { note: "快速搜索不可用,已使用深度搜索替代" } : {}),
}),
};
}
case "web_search_deep": {
const parsed = webSearchDeepSchema.parse(args);
let enriched: Array<{ title: string; url: string; snippet: string }>;
let fallbackUsed = false;
try {
const data = await executeWithRetry(() => webSearch(parsed.query));
enriched = (data.results ?? []).slice(0, 5).map((r) => ({
title: r.title ?? "",
url: r.url ?? "",
snippet: (r.description ?? r.content ?? "").slice(0, 200),
}));
// Rerank results using Jina Reranker as post-processing
try {
const docs = enriched.map((r) => `${r.title} ${r.snippet}`);
if (docs.length > 0) {
const reranked = await jinaRerank(parsed.query, docs, Math.min(docs.length, 5));
enriched = reranked.results
.sort((a, b) => b.relevance_score - a.relevance_score)
.map((r) => enriched[r.index]);
}
} catch {
// Rerank failed silently, use original order
}
} catch {
// web_search_deep (Jina) failed, fallback to google_search
console.warn(`[fallback] web_search_deep failed, trying google_search`);
const data = await googleSearch(parsed.query);
enriched = data.results.slice(0, 5).map((r) => ({
title: r.title,
url: r.url,
snippet: r.snippet,
}));
fallbackUsed = true;
}
ui.push(
{
name: "search-result",
props: {
query: parsed.query,
total: enriched.length,
results: enriched,
sourceType: "external_web",
confidence: fallbackUsed ? "medium" : "high",
},
},
{ message: lastAiMessage },
);
statusList.push({
tool: "web_search_deep",
status: fallbackUsed ? "fallback" : "ok",
...(fallbackUsed ? { message: "深度搜索不可用,已使用快速搜索替代" } : {}),
});
return {
role: "tool" as const,
tool_call_id: id,
content: JSON.stringify({
total: enriched.length,
results: enriched,
...(fallbackUsed ? { note: "深度搜索不可用,已使用快速搜索替代" } : {}),
}),
};
}
case "web_read": {
const parsed = webReadSchema.parse(args);
const data = await executeWithRetry(() => webRead(parsed.url));
// Truncate content to avoid token explosion
const truncated = data.content.slice(0, 4000);
statusList.push({ tool: "web_read", status: "ok" });
return {
role: "tool" as const,
tool_call_id: id,
content: JSON.stringify({
title: data.title,
url: parsed.url,
content: truncated,
truncated: data.content.length > 4000,
}),
};
}
default:
return {
role: "tool" as const,
tool_call_id: id,
content: `Unknown tool: ${name}`,
};
}
} catch (e) {
statusList.push({ tool: name, status: "error", message: formatToolError(name, e) });
return {
role: "tool" as const,
tool_call_id: id,
content: formatToolError(name, e),
};
}
});
const results = await Promise.all(executions);
toolMessages.push(...results);
return {
messages: toolMessages,
ui: ui.items,
timestamp: Date.now(),
toolStatus: statusList,
};
}