From d953625a2bfdc969346823af5df4e297cb9213ee Mon Sep 17 00:00:00 2001 From: mlogclub Date: Fri, 10 Apr 2026 15:47:58 +0800 Subject: [PATCH] feat: add Graph Tool tracing and logging support in agent run logs --- .../impl/callbacks/runlog_callback.go | 10 +++++ .../internal/impl/callbacks/trace_callback.go | 14 +++++++ .../internal/impl/factory/agent_factory.go | 16 +++++++ internal/ai/runtime/reply_service.go | 42 +++++++++++++++++++ internal/builders/agent_run_log_builder.go | 1 + internal/models/models.go | 1 + internal/pkg/dto/response/skill_response.go | 1 + web/app/(console)/agent-run-logs/page.tsx | 15 +++++++ web/lib/api/admin.ts | 1 + 9 files changed, 101 insertions(+) diff --git a/internal/ai/runtime/internal/impl/callbacks/runlog_callback.go b/internal/ai/runtime/internal/impl/callbacks/runlog_callback.go index e67e71e..d0ac9f4 100644 --- a/internal/ai/runtime/internal/impl/callbacks/runlog_callback.go +++ b/internal/ai/runtime/internal/impl/callbacks/runlog_callback.go @@ -49,3 +49,13 @@ func (c *RuntimeTraceCollector) AddToolSearchItem(item ToolSearchTraceItem) { c.Data.ToolSearch.Count++ c.Data.ToolSearch.Items = append(c.Data.ToolSearch.Items, item) } + +func (c *RuntimeTraceCollector) AddGraphToolItem(item GraphToolTraceItem) { + if c == nil { + return + } + c.mu.Lock() + defer c.mu.Unlock() + c.Data.GraphTools.Count++ + c.Data.GraphTools.Items = append(c.Data.GraphTools.Items, item) +} diff --git a/internal/ai/runtime/internal/impl/callbacks/trace_callback.go b/internal/ai/runtime/internal/impl/callbacks/trace_callback.go index 71eea7a..1727e0a 100644 --- a/internal/ai/runtime/internal/impl/callbacks/trace_callback.go +++ b/internal/ai/runtime/internal/impl/callbacks/trace_callback.go @@ -22,6 +22,16 @@ type ToolSearchTraceItem struct { ErrorMessage string `json:"errorMessage,omitempty"` } +type GraphToolTraceItem struct { + ToolCode string `json:"toolCode"` + ToolName string `json:"toolName"` + Arguments map[string]any `json:"arguments,omitempty"` + ResultPreview string `json:"resultPreview,omitempty"` + LatencyMs int64 `json:"latencyMs,omitempty"` + Status string `json:"status,omitempty"` + ErrorMessage string `json:"errorMessage,omitempty"` +} + type RetrieverTraceItem struct { Query string `json:"query,omitempty"` KnowledgeBaseID int64 `json:"knowledgeBaseId,omitempty"` @@ -62,6 +72,10 @@ type RuntimeTraceData struct { Count int `json:"count,omitempty"` Items []ToolSearchTraceItem `json:"items,omitempty"` } `json:"toolSearch"` + GraphTools struct { + Count int `json:"count,omitempty"` + Items []GraphToolTraceItem `json:"items,omitempty"` + } `json:"graphTools"` Output struct { ReplyText string `json:"replyText,omitempty"` FinishReason string `json:"finishReason,omitempty"` diff --git a/internal/ai/runtime/internal/impl/factory/agent_factory.go b/internal/ai/runtime/internal/impl/factory/agent_factory.go index 5308095..721821f 100644 --- a/internal/ai/runtime/internal/impl/factory/agent_factory.go +++ b/internal/ai/runtime/internal/impl/factory/agent_factory.go @@ -67,6 +67,7 @@ func (f *AgentFactory) BuildCustomerServiceAgent(ctx context.Context, aiAgent *m ToolCode: item.ToolCode, ServerCode: item.ServerCode, ToolName: item.ToolName, + SourceType: "mcp", } } for modelName, toolCode := range extraToolCodes { @@ -87,6 +88,7 @@ func (f *AgentFactory) BuildCustomerServiceAgent(ctx context.Context, aiAgent *m ToolCode: toolCode, ServerCode: serverCode, ToolName: toolName, + SourceType: resolveToolSourceType(toolCode), } } handlers = append(handlers, einocallbacks.NewRuntimeTraceHandler(collector, toolMetadataBy)) @@ -109,6 +111,20 @@ func (f *AgentFactory) BuildCustomerServiceAgent(ctx context.Context, aiAgent *m return &einoagents.CustomerServiceAgent{Inner: inner}, nil } +func resolveToolSourceType(toolCode string) string { + toolCode = strings.TrimSpace(toolCode) + switch { + case toolCode == toolx.BuiltinToolSearchToolCode: + return toolx.BuiltinToolCatalogServerCode + case strings.HasPrefix(toolCode, toolx.GraphToolCatalogServerCode+"/"): + return toolx.GraphToolCatalogServerCode + case strings.HasPrefix(toolCode, toolx.BuiltinToolCatalogServerCode+"/"): + return toolx.BuiltinToolCatalogServerCode + default: + return "mcp" + } +} + func buildAgentInstruction(aiAgent *models.AIAgent, selectedSkill *models.SkillDefinition, toolDefinitions []einoadapter.MCPToolDefinition, extraToolCodes map[string]string) string { baseInstruction := "" if aiAgent != nil { diff --git a/internal/ai/runtime/reply_service.go b/internal/ai/runtime/reply_service.go index 9362f4e..67d6931 100644 --- a/internal/ai/runtime/reply_service.go +++ b/internal/ai/runtime/reply_service.go @@ -333,6 +333,7 @@ func (s *aiReplyService) writeRunLog(startedAt time.Time, message models.Message PlannedSkillName: strings.TrimSpace(summaryPlannedSkillName(summary)), SkillRouteTrace: strings.TrimSpace(summarySkillRouteTrace(summary)), ToolSearchTrace: extractToolSearchTrace(summary), + GraphToolTrace: extractGraphToolTrace(summary), PlannedToolCode: plannedToolCode, PlanReason: planReason, InterruptType: firstInterruptType(summary), @@ -380,9 +381,15 @@ func buildRunLogPlan(summary *Summary) (plannedAction, plannedToolCode, planReas return "interrupt", "", "pending interrupt checkpoint expired" } if summary.Interrupted { + if graphToolCode := firstGraphToolCode(summary); graphToolCode != "" { + return "graph", graphToolCode, "graph tool interrupted and is waiting for user confirmation" + } return "tool", summaryPrimaryToolCode(summary), "agent interrupted and is waiting for user confirmation" } if len(summary.InvokedToolCodes) > 0 { + if graphToolCode := firstGraphToolCode(summary); graphToolCode != "" { + return "graph", graphToolCode, "agent invoked graph tool" + } toolCode := summaryPrimaryToolCode(summary) reason := "agent invoked MCP tool" if toolCode != "" && toolCode != firstInvokedToolCode(summary) { @@ -406,6 +413,9 @@ func toRunLogFinalAction(summary *Summary) string { if skillCode := strings.TrimSpace(summaryPlannedSkillCode(summary)); skillCode != "" && strings.TrimSpace(summary.ReplyText) != "" { return "skill" } + if graphToolCode := firstGraphToolCode(summary); graphToolCode != "" && strings.TrimSpace(summary.ReplyText) != "" { + return "graph" + } switch strings.TrimSpace(summary.Status) { case "completed": return "reply" @@ -573,6 +583,17 @@ func extractToolSearchTrace(summary *Summary) string { return string(trace.ToolSearch.Raw) } +func extractGraphToolTrace(summary *Summary) string { + if summary == nil { + return "" + } + trace := parseRuntimeTraceData(summary.TraceData) + if len(trace.GraphTools.Items) == 0 || len(trace.GraphTools.Raw) == 0 { + return "" + } + return string(trace.GraphTools.Raw) +} + func firstToolSearchTargetToolCode(summary *Summary) string { trace := parseRuntimeTraceData(summary.TraceData) for _, item := range trace.ToolSearch.Items { @@ -590,6 +611,17 @@ func firstToolSearchTargetToolCode(summary *Summary) string { return "" } +func firstGraphToolCode(summary *Summary) string { + trace := parseRuntimeTraceData(summary.TraceData) + for _, item := range trace.GraphTools.Items { + toolCode := strings.TrimSpace(item.ToolCode) + if toolCode != "" { + return toolCode + } + } + return "" +} + type runtimeTraceProjection struct { ToolSearch struct { Raw json.RawMessage `json:"-"` @@ -598,6 +630,12 @@ type runtimeTraceProjection struct { CandidateToolCodes []string `json:"candidateToolCodes"` } `json:"items"` } `json:"toolSearch"` + GraphTools struct { + Raw json.RawMessage `json:"-"` + Items []struct { + ToolCode string `json:"toolCode"` + } `json:"items"` + } `json:"graphTools"` } func parseRuntimeTraceData(raw string) runtimeTraceProjection { @@ -614,6 +652,10 @@ func parseRuntimeTraceData(raw string) runtimeTraceProjection { trace.ToolSearch.Raw = append(json.RawMessage(nil), toolSearchRaw...) _ = json.Unmarshal(toolSearchRaw, &trace.ToolSearch) } + if graphToolsRaw, ok := payload["graphTools"]; ok && len(graphToolsRaw) > 0 { + trace.GraphTools.Raw = append(json.RawMessage(nil), graphToolsRaw...) + _ = json.Unmarshal(graphToolsRaw, &trace.GraphTools) + } return trace } diff --git a/internal/builders/agent_run_log_builder.go b/internal/builders/agent_run_log_builder.go index 6daa240..ed5ec4e 100644 --- a/internal/builders/agent_run_log_builder.go +++ b/internal/builders/agent_run_log_builder.go @@ -21,6 +21,7 @@ func BuildAgentRunLog(item *models.AgentRunLog) response.AgentRunLogResponse { PlannedSkillName: item.PlannedSkillName, SkillRouteTrace: item.SkillRouteTrace, ToolSearchTrace: item.ToolSearchTrace, + GraphToolTrace: item.GraphToolTrace, PlannedToolCode: item.PlannedToolCode, PlanReason: item.PlanReason, InterruptType: item.InterruptType, diff --git a/internal/models/models.go b/internal/models/models.go index d59d053..78211ef 100644 --- a/internal/models/models.go +++ b/internal/models/models.go @@ -953,6 +953,7 @@ type AgentRunLog struct { PlannedSkillName string `gorm:"type:varchar(100);not null;default:''"` SkillRouteTrace string `gorm:"type:text"` ToolSearchTrace string `gorm:"type:text"` + GraphToolTrace string `gorm:"type:text"` PlannedToolCode string `gorm:"type:varchar(200);not null;default:'';index"` PlanReason string `gorm:"type:varchar(500);not null;default:''"` InterruptType string `gorm:"type:varchar(50);not null;default:'';index"` diff --git a/internal/pkg/dto/response/skill_response.go b/internal/pkg/dto/response/skill_response.go index 8c93f42..456fa0f 100644 --- a/internal/pkg/dto/response/skill_response.go +++ b/internal/pkg/dto/response/skill_response.go @@ -49,6 +49,7 @@ type AgentRunLogResponse struct { PlannedSkillName string `json:"plannedSkillName"` SkillRouteTrace string `json:"skillRouteTrace"` ToolSearchTrace string `json:"toolSearchTrace"` + GraphToolTrace string `json:"graphToolTrace"` PlannedToolCode string `json:"plannedToolCode"` PlanReason string `json:"planReason"` InterruptType string `json:"interruptType"` diff --git a/web/app/(console)/agent-run-logs/page.tsx b/web/app/(console)/agent-run-logs/page.tsx index c981428..935ab39 100644 --- a/web/app/(console)/agent-run-logs/page.tsx +++ b/web/app/(console)/agent-run-logs/page.tsx @@ -45,6 +45,7 @@ const actionOptions = [ { value: "rag", label: "RAG" }, { value: "skill", label: "Skill" }, { value: "tool", label: "Tool" }, + { value: "graph", label: "Graph" }, { value: "handoff", label: "转人工" }, { value: "reply", label: "回复" }, { value: "fallback", label: "兜底" }, @@ -58,6 +59,8 @@ function actionBadgeVariant(action: string) { return "default" as const case "tool": return "default" as const + case "graph": + return "default" as const case "rag": return "secondary" as const case "fallback": @@ -95,6 +98,10 @@ export default function DashboardAgentRunLogsPage() { () => safeParseJSON(activeLog?.toolSearchTrace ?? ""), [activeLog?.toolSearchTrace] ) + const activeGraphToolTrace = useMemo( + () => safeParseJSON(activeLog?.graphToolTrace ?? ""), + [activeLog?.graphToolTrace] + ) const aiAgentOptions = useMemo( () => [ @@ -384,6 +391,14 @@ export default function DashboardAgentRunLogsPage() { : activeLog.toolSearchTrace } /> + } title="用户问题" diff --git a/web/lib/api/admin.ts b/web/lib/api/admin.ts index d79233c..06c2dfe 100644 --- a/web/lib/api/admin.ts +++ b/web/lib/api/admin.ts @@ -367,6 +367,7 @@ export type AgentRunLog = { plannedSkillName: string skillRouteTrace: string toolSearchTrace: string + graphToolTrace: string plannedToolCode: string planReason: string interruptType: string