diff --git a/internal/ai/runtime/internal/impl/callbacks/agent_trace_handler.go b/internal/ai/runtime/internal/impl/callbacks/agent_trace_handler.go index 5ace925..e9cb769 100644 --- a/internal/ai/runtime/internal/impl/callbacks/agent_trace_handler.go +++ b/internal/ai/runtime/internal/impl/callbacks/agent_trace_handler.go @@ -65,17 +65,21 @@ func (h *RuntimeTraceHandler) WrapInvokableToolCall(_ context.Context, endpoint } h.collector.AddToolItem(item) if metadata, ok := h.resolveToolMetadata(item.ToolName); ok && strings.TrimSpace(metadata.SourceType) == toolx.GraphToolCatalogServerCode { + recommendedAction, riskLevel, ticketDraftReady := parseGraphToolOutcome(item.ToolCode, result) h.collector.AddGraphToolItem(GraphToolTraceItem{ - ToolCode: item.ToolCode, - ToolName: item.ToolName, - Arguments: item.Arguments, - ResultPreview: item.ResultPreview, - ResultReduced: item.ResultReduced, - OriginalChars: item.OriginalChars, - KeptChars: item.KeptChars, - LatencyMs: item.LatencyMs, - Status: item.Status, - ErrorMessage: item.ErrorMessage, + ToolCode: item.ToolCode, + ToolName: item.ToolName, + Arguments: item.Arguments, + ResultPreview: item.ResultPreview, + ResultReduced: item.ResultReduced, + OriginalChars: item.OriginalChars, + KeptChars: item.KeptChars, + LatencyMs: item.LatencyMs, + Status: item.Status, + ErrorMessage: item.ErrorMessage, + RecommendedAction: recommendedAction, + RiskLevel: riskLevel, + TicketDraftReady: ticketDraftReady, }) } if metadata, ok := h.resolveToolMetadata(item.ToolName); ok && strings.TrimSpace(metadata.ToolCode) == toolx.BuiltinToolSearchToolCode { @@ -85,6 +89,33 @@ func (h *RuntimeTraceHandler) WrapInvokableToolCall(_ context.Context, endpoint }, nil } +func parseGraphToolOutcome(toolCode string, result string) (recommendedAction, riskLevel string, ticketDraftReady bool) { + toolCode = strings.TrimSpace(toolCode) + if toolCode == "" || strings.TrimSpace(result) == "" { + return "", "", false + } + payload := make(map[string]any) + if err := json.Unmarshal([]byte(strings.TrimSpace(result)), &payload); err != nil { + return "", "", false + } + switch toolCode { + case toolx.GraphAnalyzeConversationToolCode: + return strings.TrimSpace(readToolSearchString(payload, "recommendedNextAction")), strings.TrimSpace(readToolSearchString(payload, "riskLevel")), false + case toolx.GraphTriageServiceRequestToolCode: + recommendedAction = strings.TrimSpace(readToolSearchString(payload, "recommendedAction")) + if analysis, ok := payload["analysis"].(map[string]any); ok { + riskLevel = strings.TrimSpace(readToolSearchString(analysis, "riskLevel")) + } + if ticketDraft, ok := payload["ticketDraft"].(map[string]any); ok { + ready, _ := ticketDraft["ready"].(bool) + ticketDraftReady = ready + } + return recommendedAction, riskLevel, ticketDraftReady + default: + return "", "", false + } +} + func (h *RuntimeTraceHandler) resolveToolMetadata(modelToolName string) (ToolMetadata, bool) { if h == nil || h.toolMetadataBy == nil { return ToolMetadata{}, false diff --git a/internal/ai/runtime/internal/impl/callbacks/trace_callback.go b/internal/ai/runtime/internal/impl/callbacks/trace_callback.go index 604265d..d448d6f 100644 --- a/internal/ai/runtime/internal/impl/callbacks/trace_callback.go +++ b/internal/ai/runtime/internal/impl/callbacks/trace_callback.go @@ -26,16 +26,19 @@ type ToolSearchTraceItem struct { } type GraphToolTraceItem struct { - ToolCode string `json:"toolCode"` - ToolName string `json:"toolName"` - Arguments map[string]any `json:"arguments,omitempty"` - ResultPreview string `json:"resultPreview,omitempty"` - ResultReduced bool `json:"resultReduced,omitempty"` - OriginalChars int `json:"originalChars,omitempty"` - KeptChars int `json:"keptChars,omitempty"` - LatencyMs int64 `json:"latencyMs,omitempty"` - Status string `json:"status,omitempty"` - ErrorMessage string `json:"errorMessage,omitempty"` + ToolCode string `json:"toolCode"` + ToolName string `json:"toolName"` + Arguments map[string]any `json:"arguments,omitempty"` + ResultPreview string `json:"resultPreview,omitempty"` + ResultReduced bool `json:"resultReduced,omitempty"` + OriginalChars int `json:"originalChars,omitempty"` + KeptChars int `json:"keptChars,omitempty"` + LatencyMs int64 `json:"latencyMs,omitempty"` + Status string `json:"status,omitempty"` + ErrorMessage string `json:"errorMessage,omitempty"` + RecommendedAction string `json:"recommendedAction,omitempty"` + RiskLevel string `json:"riskLevel,omitempty"` + TicketDraftReady bool `json:"ticketDraftReady,omitempty"` } type RetrieverTraceItem struct { diff --git a/internal/ai/runtime/reply_service.go b/internal/ai/runtime/reply_service.go index bb79e94..2f286a1 100644 --- a/internal/ai/runtime/reply_service.go +++ b/internal/ai/runtime/reply_service.go @@ -360,13 +360,21 @@ func buildRunLogPlan(summary *Summary) (plannedAction, plannedToolCode, planReas } if summary.Interrupted { if graphToolCode := firstGraphToolCode(summary); graphToolCode != "" { - return "graph", graphToolCode, "graph tool interrupted and is waiting for user confirmation" + reason := graphPlanReason(summary) + if reason == "" { + reason = "graph tool interrupted and is waiting for user confirmation" + } + return "graph", graphToolCode, reason } 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" + reason := graphPlanReason(summary) + if reason == "" { + reason = "agent invoked graph tool" + } + return "graph", graphToolCode, reason } toolCode := summaryPrimaryToolCode(summary) reason := "agent invoked MCP tool" @@ -615,6 +623,38 @@ func extractHandoffReason(summary *Summary) string { return "" } +func graphPlanReason(summary *Summary) string { + trace := parseRuntimeTraceData(summary.TraceData) + for _, item := range trace.GraphTools.Items { + toolCode := strings.TrimSpace(item.ToolCode) + switch toolCode { + case toolx.GraphTriageServiceRequestToolCode: + recommendedAction := strings.TrimSpace(item.RecommendedAction) + if recommendedAction == "" { + return "graph tool triaged service request" + } + if item.TicketDraftReady { + return "graph tool triaged service request: " + recommendedAction + " with ready ticket draft" + } + return "graph tool triaged service request: " + recommendedAction + case toolx.GraphAnalyzeConversationToolCode: + recommendedAction := strings.TrimSpace(item.RecommendedAction) + riskLevel := strings.TrimSpace(item.RiskLevel) + switch { + case recommendedAction != "" && riskLevel != "": + return "graph tool analyzed conversation: " + recommendedAction + " (" + riskLevel + " risk)" + case recommendedAction != "": + return "graph tool analyzed conversation: " + recommendedAction + case riskLevel != "": + return "graph tool analyzed conversation (" + riskLevel + " risk)" + default: + return "graph tool analyzed conversation" + } + } + } + return "" +} + type runtimeTraceProjection struct { ToolSearch struct { Raw json.RawMessage `json:"-"` @@ -626,8 +666,11 @@ type runtimeTraceProjection struct { GraphTools struct { Raw json.RawMessage `json:"-"` Items []struct { - ToolCode string `json:"toolCode"` - Arguments map[string]any `json:"arguments"` + ToolCode string `json:"toolCode"` + Arguments map[string]any `json:"arguments"` + RecommendedAction string `json:"recommendedAction"` + RiskLevel string `json:"riskLevel"` + TicketDraftReady bool `json:"ticketDraftReady"` } `json:"items"` } `json:"graphTools"` }