|
@@ -1,9 +1,11 @@
|
|
|
from __future__ import annotations
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
import json
|
|
import json
|
|
|
|
|
+import re
|
|
|
import time
|
|
import time
|
|
|
|
|
+from contextvars import ContextVar
|
|
|
from datetime import datetime, timezone
|
|
from datetime import datetime, timezone
|
|
|
-from typing import Any, Literal
|
|
|
|
|
|
|
+from typing import Any, Callable, Literal
|
|
|
|
|
|
|
|
from langgraph.graph import END, START, StateGraph
|
|
from langgraph.graph import END, START, StateGraph
|
|
|
|
|
|
|
@@ -22,6 +24,9 @@ from .models import (
|
|
|
|
|
|
|
|
|
|
|
|
|
Route = Literal["execute_tools", "summarize", "chat"]
|
|
Route = Literal["execute_tools", "summarize", "chat"]
|
|
|
|
|
+TraceSink = Callable[[AgentTraceEvent], None]
|
|
|
|
|
+
|
|
|
|
|
+_TRACE_SINK: ContextVar[TraceSink | None] = ContextVar("smqjh_agent_trace_sink", default=None)
|
|
|
|
|
|
|
|
|
|
|
|
|
class SmqjhAgentGraph:
|
|
class SmqjhAgentGraph:
|
|
@@ -36,29 +41,39 @@ class SmqjhAgentGraph:
|
|
|
def tools(self) -> list[McpToolDescriptor]:
|
|
def tools(self) -> list[McpToolDescriptor]:
|
|
|
return self._load_runnable_tools()
|
|
return self._load_runnable_tools()
|
|
|
|
|
|
|
|
- def run(self, message: str, history: list[ChatMessage] | None = None) -> AgentRunResponse:
|
|
|
|
|
- state: AgentState = {
|
|
|
|
|
- "message": message,
|
|
|
|
|
- "history": history or [],
|
|
|
|
|
- "visitedSignatures": [],
|
|
|
|
|
- "steps": 0,
|
|
|
|
|
- "trace": [_trace("system", "收到用户请求", message)],
|
|
|
|
|
- }
|
|
|
|
|
- result = self.graph.invoke(state)
|
|
|
|
|
- final = result.get("final")
|
|
|
|
|
- if final:
|
|
|
|
|
- return final
|
|
|
|
|
- observations = result.get("observations", [])
|
|
|
|
|
- trace_events = result.get("trace", [])
|
|
|
|
|
- return {
|
|
|
|
|
- "content": "这次没有生成有效回答,请稍后重试。",
|
|
|
|
|
- "model": "none",
|
|
|
|
|
- "usedMcp": False,
|
|
|
|
|
- "steps": int(result.get("steps", 0)),
|
|
|
|
|
- "toolCalls": observations,
|
|
|
|
|
- "tables": _extract_tables(observations),
|
|
|
|
|
- "trace": trace_events,
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ def run(
|
|
|
|
|
+ self,
|
|
|
|
|
+ message: str,
|
|
|
|
|
+ history: list[ChatMessage] | None = None,
|
|
|
|
|
+ emit: TraceSink | None = None,
|
|
|
|
|
+ ) -> AgentRunResponse:
|
|
|
|
|
+ token = _TRACE_SINK.set(emit)
|
|
|
|
|
+ try:
|
|
|
|
|
+ state: AgentState = {
|
|
|
|
|
+ "message": message,
|
|
|
|
|
+ "history": history or [],
|
|
|
|
|
+ "visitedSignatures": [],
|
|
|
|
|
+ "steps": 0,
|
|
|
|
|
+ "trace": [_trace("system", "已收到请求", message)],
|
|
|
|
|
+ }
|
|
|
|
|
+ result = self.graph.invoke(state)
|
|
|
|
|
+ final = result.get("final")
|
|
|
|
|
+ if final:
|
|
|
|
|
+ return final
|
|
|
|
|
+
|
|
|
|
|
+ observations = result.get("observations", [])
|
|
|
|
|
+ trace_events = result.get("trace", [])
|
|
|
|
|
+ return {
|
|
|
|
|
+ "content": "这次没有生成有效回答,请稍后重试。",
|
|
|
|
|
+ "model": "none",
|
|
|
|
|
+ "usedMcp": False,
|
|
|
|
|
+ "steps": int(result.get("steps", 0)),
|
|
|
|
|
+ "toolCalls": observations,
|
|
|
|
|
+ "tables": _extract_tables(observations),
|
|
|
|
|
+ "trace": trace_events,
|
|
|
|
|
+ }
|
|
|
|
|
+ finally:
|
|
|
|
|
+ _TRACE_SINK.reset(token)
|
|
|
|
|
|
|
|
def _build_graph(self):
|
|
def _build_graph(self):
|
|
|
workflow = StateGraph(AgentState)
|
|
workflow = StateGraph(AgentState)
|
|
@@ -84,25 +99,32 @@ class SmqjhAgentGraph:
|
|
|
return workflow.compile()
|
|
return workflow.compile()
|
|
|
|
|
|
|
|
def _load_tools_node(self, state: AgentState) -> AgentState:
|
|
def _load_tools_node(self, state: AgentState) -> AgentState:
|
|
|
|
|
+ start = _trace("system", "正在连接 MCP", "加载可用业务工具和数据库只读查询能力")
|
|
|
try:
|
|
try:
|
|
|
tools = self._load_runnable_tools()
|
|
tools = self._load_runnable_tools()
|
|
|
|
|
+ done = _trace("system", "MCP 工具加载完成", f"可用工具 {len(tools)} 个")
|
|
|
return {
|
|
return {
|
|
|
"tools": tools,
|
|
"tools": tools,
|
|
|
- "trace": _append_trace(state, _trace("system", "MCP 工具加载完成", f"可用工具 {len(tools)} 个")),
|
|
|
|
|
|
|
+ "trace": _append_trace(state, start, done),
|
|
|
}
|
|
}
|
|
|
except Exception as error:
|
|
except Exception as error:
|
|
|
|
|
+ failed = _trace("error", "MCP 工具加载失败", _error_message(error))
|
|
|
return {
|
|
return {
|
|
|
"tools": [],
|
|
"tools": [],
|
|
|
- "trace": _append_trace(state, _trace("error", "MCP 工具加载失败", _error_message(error))),
|
|
|
|
|
|
|
+ "trace": _append_trace(state, start, failed),
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
def _plan_node(self, state: AgentState) -> AgentState:
|
|
def _plan_node(self, state: AgentState) -> AgentState:
|
|
|
if int(state.get("steps", 0)) >= self.config.max_steps:
|
|
if int(state.get("steps", 0)) >= self.config.max_steps:
|
|
|
return {
|
|
return {
|
|
|
"plannedToolCalls": [],
|
|
"plannedToolCalls": [],
|
|
|
- "trace": _append_trace(state, _trace("plan", "达到最大工具步数", f"maxSteps={self.config.max_steps}")),
|
|
|
|
|
|
|
+ "trace": _append_trace(
|
|
|
|
|
+ state,
|
|
|
|
|
+ _trace("plan", "达到最大工具步数", f"maxSteps={self.config.max_steps}"),
|
|
|
|
|
+ ),
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ start = _trace("plan", "正在分析需求", "DeepSeek 正在判断是否需要查库或调用业务工具")
|
|
|
try:
|
|
try:
|
|
|
result = self.mcp.call_tool(
|
|
result = self.mcp.call_tool(
|
|
|
"smqjh.ai.tool.plan",
|
|
"smqjh.ai.tool.plan",
|
|
@@ -121,28 +143,28 @@ class SmqjhAgentGraph:
|
|
|
for call in (_normalize_tool_call(item, state.get("message", "")) for item in raw_call_items)
|
|
for call in (_normalize_tool_call(item, state.get("message", "")) for item in raw_call_items)
|
|
|
if call and _tool_signature(call["name"], call["arguments"]) not in visited
|
|
if call and _tool_signature(call["name"], call["arguments"]) not in visited
|
|
|
][:3]
|
|
][:3]
|
|
|
|
|
+ done = _trace(
|
|
|
|
|
+ "plan",
|
|
|
|
|
+ "DeepSeek 已规划工具调用" if planned else "DeepSeek 判断无需继续调用工具",
|
|
|
|
|
+ _reason_text(record),
|
|
|
|
|
+ {"toolCalls": planned},
|
|
|
|
|
+ )
|
|
|
return {
|
|
return {
|
|
|
"plannedToolCalls": planned,
|
|
"plannedToolCalls": planned,
|
|
|
- "trace": _append_trace(
|
|
|
|
|
- state,
|
|
|
|
|
- _trace(
|
|
|
|
|
- "plan",
|
|
|
|
|
- "DeepSeek 已规划工具调用" if planned else "DeepSeek 判断无需继续调用工具",
|
|
|
|
|
- _reason_text(record),
|
|
|
|
|
- {"toolCalls": planned},
|
|
|
|
|
- ),
|
|
|
|
|
- ),
|
|
|
|
|
|
|
+ "trace": _append_trace(state, start, done),
|
|
|
}
|
|
}
|
|
|
except Exception as error:
|
|
except Exception as error:
|
|
|
|
|
+ failed = _trace("error", "DeepSeek 工具规划失败", _error_message(error))
|
|
|
return {
|
|
return {
|
|
|
"plannedToolCalls": [],
|
|
"plannedToolCalls": [],
|
|
|
- "trace": _append_trace(state, _trace("error", "DeepSeek 工具规划失败", _error_message(error))),
|
|
|
|
|
|
|
+ "trace": _append_trace(state, start, failed),
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
def _execute_tools_node(self, state: AgentState) -> AgentState:
|
|
def _execute_tools_node(self, state: AgentState) -> AgentState:
|
|
|
by_name = {tool.get("name", ""): tool for tool in state.get("tools", [])}
|
|
by_name = {tool.get("name", ""): tool for tool in state.get("tools", [])}
|
|
|
observations: list[ToolObservation] = []
|
|
observations: list[ToolObservation] = []
|
|
|
visited = set(state.get("visitedSignatures", []))
|
|
visited = set(state.get("visitedSignatures", []))
|
|
|
|
|
+ trace_events = list(state.get("trace", []))
|
|
|
|
|
|
|
|
for call in state.get("plannedToolCalls", []):
|
|
for call in state.get("plannedToolCalls", []):
|
|
|
requested_name = call["name"]
|
|
requested_name = call["name"]
|
|
@@ -152,54 +174,57 @@ class SmqjhAgentGraph:
|
|
|
visited.add(signature)
|
|
visited.add(signature)
|
|
|
|
|
|
|
|
if not resolved_name:
|
|
if not resolved_name:
|
|
|
- observations.append(
|
|
|
|
|
- {
|
|
|
|
|
- "name": requested_name,
|
|
|
|
|
- "source": "mcp",
|
|
|
|
|
- "arguments": arguments,
|
|
|
|
|
- "ok": False,
|
|
|
|
|
- "error": "DeepSeek 选择了未注册 MCP 工具",
|
|
|
|
|
- }
|
|
|
|
|
- )
|
|
|
|
|
|
|
+ observation: ToolObservation = {
|
|
|
|
|
+ "name": requested_name,
|
|
|
|
|
+ "source": "mcp",
|
|
|
|
|
+ "arguments": arguments,
|
|
|
|
|
+ "ok": False,
|
|
|
|
|
+ "error": "DeepSeek 选择了未注册 MCP 工具",
|
|
|
|
|
+ }
|
|
|
|
|
+ observations.append(observation)
|
|
|
|
|
+ trace_events.append(_trace("error", f"工具未注册:{requested_name}", observation["error"]))
|
|
|
continue
|
|
continue
|
|
|
|
|
|
|
|
|
|
+ trace_events.append(_trace("tool", f"正在执行:{resolved_name}", _tool_title(resolved_name)))
|
|
|
started = time.perf_counter()
|
|
started = time.perf_counter()
|
|
|
try:
|
|
try:
|
|
|
result = self.mcp.call_tool(resolved_name, arguments)
|
|
result = self.mcp.call_tool(resolved_name, arguments)
|
|
|
- observations.append(
|
|
|
|
|
- {
|
|
|
|
|
- "name": resolved_name,
|
|
|
|
|
- "source": "mcp",
|
|
|
|
|
- "arguments": arguments,
|
|
|
|
|
- "ok": True,
|
|
|
|
|
- "result": result.get("structuredContent", result),
|
|
|
|
|
- "durationMs": int((time.perf_counter() - started) * 1000),
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ observation = {
|
|
|
|
|
+ "name": resolved_name,
|
|
|
|
|
+ "source": "mcp",
|
|
|
|
|
+ "arguments": arguments,
|
|
|
|
|
+ "ok": True,
|
|
|
|
|
+ "result": result.get("structuredContent", result),
|
|
|
|
|
+ "durationMs": int((time.perf_counter() - started) * 1000),
|
|
|
|
|
+ }
|
|
|
|
|
+ observations.append(observation)
|
|
|
|
|
+ trace_events.append(
|
|
|
|
|
+ _trace(
|
|
|
|
|
+ "tool",
|
|
|
|
|
+ f"工具执行完成:{resolved_name}",
|
|
|
|
|
+ f"{observation.get('durationMs', 0)}ms",
|
|
|
|
|
+ {"arguments": arguments, "result": _summarize_result(observation.get("result"))},
|
|
|
|
|
+ )
|
|
|
)
|
|
)
|
|
|
except Exception as error:
|
|
except Exception as error:
|
|
|
- observations.append(
|
|
|
|
|
- {
|
|
|
|
|
- "name": resolved_name,
|
|
|
|
|
- "source": "mcp",
|
|
|
|
|
- "arguments": arguments,
|
|
|
|
|
- "ok": False,
|
|
|
|
|
- "error": _error_message(error),
|
|
|
|
|
- "durationMs": int((time.perf_counter() - started) * 1000),
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ observation = {
|
|
|
|
|
+ "name": resolved_name,
|
|
|
|
|
+ "source": "mcp",
|
|
|
|
|
+ "arguments": arguments,
|
|
|
|
|
+ "ok": False,
|
|
|
|
|
+ "error": _error_message(error),
|
|
|
|
|
+ "durationMs": int((time.perf_counter() - started) * 1000),
|
|
|
|
|
+ }
|
|
|
|
|
+ observations.append(observation)
|
|
|
|
|
+ trace_events.append(
|
|
|
|
|
+ _trace(
|
|
|
|
|
+ "error",
|
|
|
|
|
+ f"工具执行失败:{resolved_name}",
|
|
|
|
|
+ observation.get("error", ""),
|
|
|
|
|
+ {"arguments": arguments},
|
|
|
|
|
+ )
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
- trace_events = state.get("trace", [])
|
|
|
|
|
- for item in observations:
|
|
|
|
|
- trace_events = [
|
|
|
|
|
- *trace_events,
|
|
|
|
|
- _trace(
|
|
|
|
|
- "tool" if item.get("ok") else "error",
|
|
|
|
|
- f"工具执行完成:{item['name']}" if item.get("ok") else f"工具执行失败:{item['name']}",
|
|
|
|
|
- f"{item.get('durationMs', 0)}ms" if item.get("ok") else item.get("error", ""),
|
|
|
|
|
- {"arguments": item.get("arguments"), "result": _summarize_result(item.get("result"))},
|
|
|
|
|
- ),
|
|
|
|
|
- ]
|
|
|
|
|
-
|
|
|
|
|
return {
|
|
return {
|
|
|
"observations": [*state.get("observations", []), *observations],
|
|
"observations": [*state.get("observations", []), *observations],
|
|
|
"plannedToolCalls": [],
|
|
"plannedToolCalls": [],
|
|
@@ -211,6 +236,7 @@ class SmqjhAgentGraph:
|
|
|
def _summarize_node(self, state: AgentState) -> AgentState:
|
|
def _summarize_node(self, state: AgentState) -> AgentState:
|
|
|
observations = state.get("observations", [])
|
|
observations = state.get("observations", [])
|
|
|
tables = _extract_tables(observations)
|
|
tables = _extract_tables(observations)
|
|
|
|
|
+ start = _trace("summary", "正在整理结果", "将工具结果整理成结论、依据和表格")
|
|
|
try:
|
|
try:
|
|
|
result = self.mcp.call_tool(
|
|
result = self.mcp.call_tool(
|
|
|
"smqjh.ai.tool.summarize",
|
|
"smqjh.ai.tool.summarize",
|
|
@@ -222,7 +248,8 @@ class SmqjhAgentGraph:
|
|
|
record = _as_dict(result.get("structuredContent"))
|
|
record = _as_dict(result.get("structuredContent"))
|
|
|
content = str(record.get("content") or "").strip() or _fallback_summary(observations, tables)
|
|
content = str(record.get("content") or "").strip() or _fallback_summary(observations, tables)
|
|
|
model = str(record.get("model") or "mcp-deepseek")
|
|
model = str(record.get("model") or "mcp-deepseek")
|
|
|
- trace_events = _append_trace(state, _trace("summary", "结果总结完成", f"{len(tables)} 个表格"))
|
|
|
|
|
|
|
+ done = _trace("summary", "结果总结完成", f"{len(tables)} 个表格")
|
|
|
|
|
+ trace_events = _append_trace(state, start, done)
|
|
|
return {
|
|
return {
|
|
|
"final": {
|
|
"final": {
|
|
|
"content": content,
|
|
"content": content,
|
|
@@ -236,7 +263,8 @@ class SmqjhAgentGraph:
|
|
|
"trace": trace_events,
|
|
"trace": trace_events,
|
|
|
}
|
|
}
|
|
|
except Exception as error:
|
|
except Exception as error:
|
|
|
- trace_events = _append_trace(state, _trace("error", "结果总结失败,已使用工具结果兜底", _error_message(error)))
|
|
|
|
|
|
|
+ failed = _trace("error", "结果总结失败,已使用工具结果兜底", _error_message(error))
|
|
|
|
|
+ trace_events = _append_trace(state, start, failed)
|
|
|
return {
|
|
return {
|
|
|
"final": {
|
|
"final": {
|
|
|
"content": _fallback_summary(observations, tables),
|
|
"content": _fallback_summary(observations, tables),
|
|
@@ -251,12 +279,14 @@ class SmqjhAgentGraph:
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
def _chat_node(self, state: AgentState) -> AgentState:
|
|
def _chat_node(self, state: AgentState) -> AgentState:
|
|
|
|
|
+ start = _trace("chat", "正在生成回复", "没有继续调用业务工具,进入普通对话回答")
|
|
|
try:
|
|
try:
|
|
|
result = self.mcp.call_tool("smqjh.ai.chat", {"request": self._assistant_request(state)})
|
|
result = self.mcp.call_tool("smqjh.ai.chat", {"request": self._assistant_request(state)})
|
|
|
record = _as_dict(result.get("structuredContent"))
|
|
record = _as_dict(result.get("structuredContent"))
|
|
|
content = str(record.get("content") or "").strip() or "DeepSeek 没有返回有效内容。"
|
|
content = str(record.get("content") or "").strip() or "DeepSeek 没有返回有效内容。"
|
|
|
model = str(record.get("model") or "mcp-deepseek")
|
|
model = str(record.get("model") or "mcp-deepseek")
|
|
|
- trace_events = _append_trace(state, _trace("chat", "普通对话完成", model))
|
|
|
|
|
|
|
+ done = _trace("chat", "普通对话完成", model)
|
|
|
|
|
+ trace_events = _append_trace(state, start, done)
|
|
|
return {
|
|
return {
|
|
|
"final": {
|
|
"final": {
|
|
|
"content": content,
|
|
"content": content,
|
|
@@ -270,7 +300,8 @@ class SmqjhAgentGraph:
|
|
|
"trace": trace_events,
|
|
"trace": trace_events,
|
|
|
}
|
|
}
|
|
|
except Exception as error:
|
|
except Exception as error:
|
|
|
- trace_events = _append_trace(state, _trace("error", "普通对话失败", _error_message(error)))
|
|
|
|
|
|
|
+ failed = _trace("error", "普通对话失败", _error_message(error))
|
|
|
|
|
+ trace_events = _append_trace(state, start, failed)
|
|
|
return {
|
|
return {
|
|
|
"final": {
|
|
"final": {
|
|
|
"content": f"暂时无法完成回答:{_error_message(error)}",
|
|
"content": f"暂时无法完成回答:{_error_message(error)}",
|
|
@@ -322,16 +353,24 @@ def _repair_tool_arguments(name: str, arguments: dict[str, Any], message: str) -
|
|
|
if name == "smqjh.product.lookup.summary":
|
|
if name == "smqjh.product.lookup.summary":
|
|
|
current = str(arguments.get("productKeyword") or "").strip()
|
|
current = str(arguments.get("productKeyword") or "").strip()
|
|
|
return {**arguments, "productKeyword": current or _extract_product_keyword(message)}
|
|
return {**arguments, "productKeyword": current or _extract_product_keyword(message)}
|
|
|
|
|
+ if name in {
|
|
|
|
|
+ "smqjh.settlement.monthly.plan",
|
|
|
|
|
+ "smqjh.settlement.monthly.preview",
|
|
|
|
|
+ "settlement.monthly.plan",
|
|
|
|
|
+ "settlement.monthly.preview",
|
|
|
|
|
+ }:
|
|
|
|
|
+ enterprise = str(arguments.get("enterprise") or "").strip() or _extract_settlement_enterprise(message)
|
|
|
|
|
+ month = str(arguments.get("month") or "").strip() or _extract_month(message)
|
|
|
|
|
+ return {**arguments, "enterprise": enterprise, "month": month}
|
|
|
return arguments
|
|
return arguments
|
|
|
|
|
|
|
|
|
|
|
|
|
def _extract_product_keyword(message: str) -> str:
|
|
def _extract_product_keyword(message: str) -> str:
|
|
|
text = message
|
|
text = message
|
|
|
- for token in [
|
|
|
|
|
|
|
+ stop_words = [
|
|
|
"帮我",
|
|
"帮我",
|
|
|
"麻烦",
|
|
"麻烦",
|
|
|
"查询一下",
|
|
"查询一下",
|
|
|
- "查一下",
|
|
|
|
|
"查询",
|
|
"查询",
|
|
|
"查看",
|
|
"查看",
|
|
|
"当前",
|
|
"当前",
|
|
@@ -353,9 +392,28 @@ def _extract_product_keyword(message: str) -> str:
|
|
|
"是多少",
|
|
"是多少",
|
|
|
"是什么",
|
|
"是什么",
|
|
|
"呢",
|
|
"呢",
|
|
|
- ]:
|
|
|
|
|
|
|
+ ]
|
|
|
|
|
+ for token in stop_words:
|
|
|
text = text.replace(token, " ")
|
|
text = text.replace(token, " ")
|
|
|
- return " ".join(text.replace(",", " ").replace("。", " ").replace("?", " ").replace("?", " ").split())
|
|
|
|
|
|
|
+ text = re.sub(r"[,。!?、;:,.!?;:]", " ", text)
|
|
|
|
|
+ return " ".join(text.split())
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _extract_settlement_enterprise(message: str) -> str:
|
|
|
|
|
+ for enterprise in ("铜仁移动", "招商银行贵阳分行", "中数未来"):
|
|
|
|
|
+ if enterprise in message:
|
|
|
|
|
+ return enterprise
|
|
|
|
|
+ return ""
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _extract_month(message: str) -> str:
|
|
|
|
|
+ explicit = re.search(r"(20\d{2})[-/年](0?[1-9]|1[0-2])", message)
|
|
|
|
|
+ if explicit:
|
|
|
|
|
+ return f"{explicit.group(1)}-{int(explicit.group(2)):02d}"
|
|
|
|
|
+ month_only = re.search(r"(?<!\d)(0?[1-9]|1[0-2])\s*月份?", message)
|
|
|
|
|
+ if month_only:
|
|
|
|
|
+ return f"{datetime.now().year}-{int(month_only.group(1)):02d}"
|
|
|
|
|
+ return ""
|
|
|
|
|
|
|
|
|
|
|
|
|
def _resolve_tool_name(name: str, by_name: dict[str, McpToolDescriptor]) -> str | None:
|
|
def _resolve_tool_name(name: str, by_name: dict[str, McpToolDescriptor]) -> str | None:
|
|
@@ -366,6 +424,8 @@ def _resolve_tool_name(name: str, by_name: dict[str, McpToolDescriptor]) -> str
|
|
|
"smqjh.order.count.query" if name == "order.count.query" else "",
|
|
"smqjh.order.count.query" if name == "order.count.query" else "",
|
|
|
"smqjh.database.smart.query" if name == "database.smart.query" else "",
|
|
"smqjh.database.smart.query" if name == "database.smart.query" else "",
|
|
|
"smqjh.database.readonly.query" if name == "database.readonly.query" else "",
|
|
"smqjh.database.readonly.query" if name == "database.readonly.query" else "",
|
|
|
|
|
+ "smqjh.settlement.monthly.preview" if name == "settlement.monthly.preview" else "",
|
|
|
|
|
+ "smqjh.settlement.monthly.plan" if name == "settlement.monthly.plan" else "",
|
|
|
"smqjh.cloud.health" if name == "cloud.health" else "",
|
|
"smqjh.cloud.health" if name == "cloud.health" else "",
|
|
|
]
|
|
]
|
|
|
return next((candidate for candidate in candidates if candidate and candidate in by_name), None)
|
|
return next((candidate for candidate in candidates if candidate and candidate in by_name), None)
|
|
@@ -402,7 +462,8 @@ def _extract_tables_from_value(title: str, value: Any) -> list[AgentResultTable]
|
|
|
if not isinstance(rows, list) or not all(isinstance(row, dict) for row in rows):
|
|
if not isinstance(rows, list) or not all(isinstance(row, dict) for row in rows):
|
|
|
return
|
|
return
|
|
|
row_records = [_normalize_row(row) for row in rows[:200]]
|
|
row_records = [_normalize_row(row) for row in rows[:200]]
|
|
|
- provided_columns = record.get("columns")
|
|
|
|
|
|
|
+ column_key = "comparisonColumns" if rows_key == "comparisonRows" else "columns"
|
|
|
|
|
+ provided_columns = record.get(column_key)
|
|
|
if isinstance(provided_columns, list) and provided_columns:
|
|
if isinstance(provided_columns, list) and provided_columns:
|
|
|
columns = [str(column) for column in provided_columns]
|
|
columns = [str(column) for column in provided_columns]
|
|
|
else:
|
|
else:
|
|
@@ -472,11 +533,17 @@ def _trace(phase: str, title: str, detail: str = "", data: Any = None) -> AgentT
|
|
|
event["detail"] = detail
|
|
event["detail"] = detail
|
|
|
if data is not None:
|
|
if data is not None:
|
|
|
event["data"] = data
|
|
event["data"] = data
|
|
|
|
|
+ sink = _TRACE_SINK.get()
|
|
|
|
|
+ if sink:
|
|
|
|
|
+ try:
|
|
|
|
|
+ sink(event)
|
|
|
|
|
+ except Exception:
|
|
|
|
|
+ pass
|
|
|
return event
|
|
return event
|
|
|
|
|
|
|
|
|
|
|
|
|
-def _append_trace(state: AgentState, event: AgentTraceEvent) -> list[AgentTraceEvent]:
|
|
|
|
|
- return [*state.get("trace", []), event]
|
|
|
|
|
|
|
+def _append_trace(state: AgentState, *events: AgentTraceEvent) -> list[AgentTraceEvent]:
|
|
|
|
|
+ return [*state.get("trace", []), *events]
|
|
|
|
|
|
|
|
|
|
|
|
|
def _reason_text(record: dict[str, Any]) -> str:
|
|
def _reason_text(record: dict[str, Any]) -> str:
|
|
@@ -496,7 +563,10 @@ def _summarize_result(value: Any) -> Any:
|
|
|
def _build_table_title(base_title: str, record: dict[str, Any], rows_key: str) -> str:
|
|
def _build_table_title(base_title: str, record: dict[str, Any], rows_key: str) -> str:
|
|
|
title = str(record.get("title") or base_title).strip()
|
|
title = str(record.get("title") or base_title).strip()
|
|
|
count = f"({record['rowCount']} 行)" if "rowCount" in record else ""
|
|
count = f"({record['rowCount']} 行)" if "rowCount" in record else ""
|
|
|
- return f"{title}:价格对比" if rows_key == "comparisonRows" else f"{title}{count}"
|
|
|
|
|
|
|
+ if rows_key == "comparisonRows":
|
|
|
|
|
+ suffix = str(record.get("comparisonTitle") or "明细表").strip()
|
|
|
|
|
+ return f"{title}:{suffix}"
|
|
|
|
|
+ return f"{title}{count}"
|
|
|
|
|
|
|
|
|
|
|
|
|
def _tool_title(name: str) -> str:
|
|
def _tool_title(name: str) -> str:
|
|
@@ -512,6 +582,7 @@ def _tool_title(name: str) -> str:
|
|
|
"smqjh.product.lookup.summary": "商品资料查询结果",
|
|
"smqjh.product.lookup.summary": "商品资料查询结果",
|
|
|
"smqjh.settlement.enterprise.list": "月结企业清单",
|
|
"smqjh.settlement.enterprise.list": "月结企业清单",
|
|
|
"smqjh.settlement.monthly.plan": "企业月结计划",
|
|
"smqjh.settlement.monthly.plan": "企业月结计划",
|
|
|
|
|
+ "smqjh.settlement.monthly.preview": "企业月结预览",
|
|
|
}
|
|
}
|
|
|
return titles.get(name, name)
|
|
return titles.get(name, name)
|
|
|
|
|
|