feat: implement tool search tracing and logging in agent run logs
This commit is contained in:
@@ -6,6 +6,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"cs-agent/internal/pkg/toolx"
|
||||||
|
|
||||||
einotool "github.com/cloudwego/eino/components/tool"
|
einotool "github.com/cloudwego/eino/components/tool"
|
||||||
|
|
||||||
"github.com/cloudwego/eino/adk"
|
"github.com/cloudwego/eino/adk"
|
||||||
@@ -56,10 +58,25 @@ func (h *RuntimeTraceHandler) WrapInvokableToolCall(_ context.Context, endpoint
|
|||||||
item.ErrorMessage = err.Error()
|
item.ErrorMessage = err.Error()
|
||||||
}
|
}
|
||||||
h.collector.AddToolItem(item)
|
h.collector.AddToolItem(item)
|
||||||
|
if metadata, ok := h.resolveToolMetadata(item.ToolName); ok && strings.TrimSpace(metadata.ToolCode) == toolx.BuiltinToolSearchToolCode {
|
||||||
|
h.collector.AddToolSearchItem(buildToolSearchTraceItem(argumentsInJSON, result, err))
|
||||||
|
}
|
||||||
return result, err
|
return result, err
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *RuntimeTraceHandler) resolveToolMetadata(modelToolName string) (ToolMetadata, bool) {
|
||||||
|
if h == nil || h.toolMetadataBy == nil {
|
||||||
|
return ToolMetadata{}, false
|
||||||
|
}
|
||||||
|
modelToolName = strings.TrimSpace(modelToolName)
|
||||||
|
if modelToolName == "" {
|
||||||
|
return ToolMetadata{}, false
|
||||||
|
}
|
||||||
|
metadata, ok := h.toolMetadataBy[modelToolName]
|
||||||
|
return metadata, ok
|
||||||
|
}
|
||||||
|
|
||||||
func parseToolArguments(argumentsInJSON string) map[string]any {
|
func parseToolArguments(argumentsInJSON string) map[string]any {
|
||||||
argumentsInJSON = strings.TrimSpace(argumentsInJSON)
|
argumentsInJSON = strings.TrimSpace(argumentsInJSON)
|
||||||
if argumentsInJSON == "" {
|
if argumentsInJSON == "" {
|
||||||
@@ -83,3 +100,62 @@ func previewToolText(text string, limit int) string {
|
|||||||
}
|
}
|
||||||
return string(runes[:limit]) + "..."
|
return string(runes[:limit]) + "..."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func buildToolSearchTraceItem(argumentsInJSON string, result string, runErr error) ToolSearchTraceItem {
|
||||||
|
item := ToolSearchTraceItem{
|
||||||
|
Status: "ok",
|
||||||
|
}
|
||||||
|
args := parseToolArguments(argumentsInJSON)
|
||||||
|
item.Query = strings.TrimSpace(readToolSearchString(args, "query"))
|
||||||
|
item.TargetToolCode = strings.TrimSpace(readToolSearchString(args, "toolCode"))
|
||||||
|
if item.TargetToolCode != "" {
|
||||||
|
item.Action = "invoke"
|
||||||
|
item.TargetServerCode, item.TargetToolName = toolx.SplitMCPToolCode(item.TargetToolCode)
|
||||||
|
} else {
|
||||||
|
item.Action = "search"
|
||||||
|
}
|
||||||
|
if runErr != nil {
|
||||||
|
item.Status = "error"
|
||||||
|
item.ErrorMessage = runErr.Error()
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
payload := make(map[string]any)
|
||||||
|
if err := json.Unmarshal([]byte(strings.TrimSpace(result)), &payload); err != nil {
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
candidateItems, _ := payload["candidates"].([]any)
|
||||||
|
item.CandidateToolCodes = extractCandidateToolCodes(candidateItems)
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
|
func readToolSearchString(data map[string]any, key string) string {
|
||||||
|
if len(data) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
value, ok := data[key]
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
text, _ := value.(string)
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractCandidateToolCodes(items []any) []string {
|
||||||
|
if len(items) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ret := make([]string, 0, len(items))
|
||||||
|
for _, item := range items {
|
||||||
|
payload, ok := item.(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
toolCode, _ := payload["toolCode"].(string)
|
||||||
|
toolCode = strings.TrimSpace(toolCode)
|
||||||
|
if toolCode == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ret = append(ret, toolCode)
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|||||||
@@ -39,3 +39,13 @@ func (c *RuntimeTraceCollector) AddToolItem(item ToolTraceItem) {
|
|||||||
c.Data.Tools.Count++
|
c.Data.Tools.Count++
|
||||||
c.Data.Tools.Items = append(c.Data.Tools.Items, item)
|
c.Data.Tools.Items = append(c.Data.Tools.Items, item)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *RuntimeTraceCollector) AddToolSearchItem(item ToolSearchTraceItem) {
|
||||||
|
if c == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
c.Data.ToolSearch.Count++
|
||||||
|
c.Data.ToolSearch.Items = append(c.Data.ToolSearch.Items, item)
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,6 +11,17 @@ type ToolTraceItem struct {
|
|||||||
ErrorMessage string `json:"errorMessage,omitempty"`
|
ErrorMessage string `json:"errorMessage,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ToolSearchTraceItem struct {
|
||||||
|
Action string `json:"action,omitempty"`
|
||||||
|
Query string `json:"query,omitempty"`
|
||||||
|
TargetToolCode string `json:"targetToolCode,omitempty"`
|
||||||
|
TargetServerCode string `json:"targetServerCode,omitempty"`
|
||||||
|
TargetToolName string `json:"targetToolName,omitempty"`
|
||||||
|
CandidateToolCodes []string `json:"candidateToolCodes,omitempty"`
|
||||||
|
Status string `json:"status,omitempty"`
|
||||||
|
ErrorMessage string `json:"errorMessage,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
type RetrieverTraceItem struct {
|
type RetrieverTraceItem struct {
|
||||||
Query string `json:"query,omitempty"`
|
Query string `json:"query,omitempty"`
|
||||||
KnowledgeBaseID int64 `json:"knowledgeBaseId,omitempty"`
|
KnowledgeBaseID int64 `json:"knowledgeBaseId,omitempty"`
|
||||||
@@ -47,6 +58,10 @@ type RuntimeTraceData struct {
|
|||||||
Count int `json:"count,omitempty"`
|
Count int `json:"count,omitempty"`
|
||||||
Items []ToolTraceItem `json:"items,omitempty"`
|
Items []ToolTraceItem `json:"items,omitempty"`
|
||||||
} `json:"tools"`
|
} `json:"tools"`
|
||||||
|
ToolSearch struct {
|
||||||
|
Count int `json:"count,omitempty"`
|
||||||
|
Items []ToolSearchTraceItem `json:"items,omitempty"`
|
||||||
|
} `json:"toolSearch"`
|
||||||
Output struct {
|
Output struct {
|
||||||
ReplyText string `json:"replyText,omitempty"`
|
ReplyText string `json:"replyText,omitempty"`
|
||||||
FinishReason string `json:"finishReason,omitempty"`
|
FinishReason string `json:"finishReason,omitempty"`
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"cs-agent/internal/models"
|
"cs-agent/internal/models"
|
||||||
"cs-agent/internal/pkg/dto"
|
"cs-agent/internal/pkg/dto"
|
||||||
"cs-agent/internal/pkg/enums"
|
"cs-agent/internal/pkg/enums"
|
||||||
|
"cs-agent/internal/pkg/toolx"
|
||||||
"cs-agent/internal/repositories"
|
"cs-agent/internal/repositories"
|
||||||
svc "cs-agent/internal/services"
|
svc "cs-agent/internal/services"
|
||||||
|
|
||||||
@@ -331,6 +332,7 @@ func (s *aiReplyService) writeRunLog(startedAt time.Time, message models.Message
|
|||||||
PlannedSkillCode: strings.TrimSpace(summaryPlannedSkillCode(summary)),
|
PlannedSkillCode: strings.TrimSpace(summaryPlannedSkillCode(summary)),
|
||||||
PlannedSkillName: strings.TrimSpace(summaryPlannedSkillName(summary)),
|
PlannedSkillName: strings.TrimSpace(summaryPlannedSkillName(summary)),
|
||||||
SkillRouteTrace: strings.TrimSpace(summarySkillRouteTrace(summary)),
|
SkillRouteTrace: strings.TrimSpace(summarySkillRouteTrace(summary)),
|
||||||
|
ToolSearchTrace: extractToolSearchTrace(summary),
|
||||||
PlannedToolCode: plannedToolCode,
|
PlannedToolCode: plannedToolCode,
|
||||||
PlanReason: planReason,
|
PlanReason: planReason,
|
||||||
InterruptType: firstInterruptType(summary),
|
InterruptType: firstInterruptType(summary),
|
||||||
@@ -378,10 +380,15 @@ func buildRunLogPlan(summary *Summary) (plannedAction, plannedToolCode, planReas
|
|||||||
return "interrupt", "", "pending interrupt checkpoint expired"
|
return "interrupt", "", "pending interrupt checkpoint expired"
|
||||||
}
|
}
|
||||||
if summary.Interrupted {
|
if summary.Interrupted {
|
||||||
return "tool", firstInvokedToolCode(summary), "agent 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 len(summary.InvokedToolCodes) > 0 {
|
||||||
return "tool", strings.TrimSpace(summary.InvokedToolCodes[0]), "agent invoked MCP tool"
|
toolCode := summaryPrimaryToolCode(summary)
|
||||||
|
reason := "agent invoked MCP tool"
|
||||||
|
if toolCode != "" && toolCode != firstInvokedToolCode(summary) {
|
||||||
|
reason = "agent invoked dynamic tool via tool_search"
|
||||||
|
}
|
||||||
|
return "tool", toolCode, reason
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(summary.ReplyText) != "" {
|
if strings.TrimSpace(summary.ReplyText) != "" {
|
||||||
return "reply", "", "agent replied directly"
|
return "reply", "", "agent replied directly"
|
||||||
@@ -541,6 +548,68 @@ func firstInvokedToolCode(summary *Summary) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func summaryPrimaryToolCode(summary *Summary) string {
|
||||||
|
if summary == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
toolCode := firstInvokedToolCode(summary)
|
||||||
|
if toolCode != toolx.BuiltinToolSearchToolCode {
|
||||||
|
return toolCode
|
||||||
|
}
|
||||||
|
if targetToolCode := firstToolSearchTargetToolCode(summary); targetToolCode != "" {
|
||||||
|
return targetToolCode
|
||||||
|
}
|
||||||
|
return toolCode
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractToolSearchTrace(summary *Summary) string {
|
||||||
|
if summary == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
trace := parseRuntimeTraceData(summary.TraceData)
|
||||||
|
if len(trace.ToolSearch.Items) == 0 || len(trace.ToolSearch.Raw) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return string(trace.ToolSearch.Raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
func firstToolSearchTargetToolCode(summary *Summary) string {
|
||||||
|
trace := parseRuntimeTraceData(summary.TraceData)
|
||||||
|
for _, item := range trace.ToolSearch.Items {
|
||||||
|
toolCode := strings.TrimSpace(item.TargetToolCode)
|
||||||
|
if toolCode != "" {
|
||||||
|
return toolCode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type runtimeTraceProjection struct {
|
||||||
|
ToolSearch struct {
|
||||||
|
Raw json.RawMessage `json:"-"`
|
||||||
|
Items []struct {
|
||||||
|
TargetToolCode string `json:"targetToolCode"`
|
||||||
|
} `json:"items"`
|
||||||
|
} `json:"toolSearch"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseRuntimeTraceData(raw string) runtimeTraceProjection {
|
||||||
|
raw = strings.TrimSpace(raw)
|
||||||
|
if raw == "" {
|
||||||
|
return runtimeTraceProjection{}
|
||||||
|
}
|
||||||
|
var payload map[string]json.RawMessage
|
||||||
|
if err := json.Unmarshal([]byte(raw), &payload); err != nil {
|
||||||
|
return runtimeTraceProjection{}
|
||||||
|
}
|
||||||
|
var trace runtimeTraceProjection
|
||||||
|
if toolSearchRaw, ok := payload["toolSearch"]; ok && len(toolSearchRaw) > 0 {
|
||||||
|
trace.ToolSearch.Raw = append(json.RawMessage(nil), toolSearchRaw...)
|
||||||
|
_ = json.Unmarshal(toolSearchRaw, &trace.ToolSearch)
|
||||||
|
}
|
||||||
|
return trace
|
||||||
|
}
|
||||||
|
|
||||||
func isCancellationReply(replyText string) bool {
|
func isCancellationReply(replyText string) bool {
|
||||||
replyText = strings.TrimSpace(replyText)
|
replyText = strings.TrimSpace(replyText)
|
||||||
return strings.Contains(replyText, "已取消本次工单创建")
|
return strings.Contains(replyText, "已取消本次工单创建")
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ func BuildAgentRunLog(item *models.AgentRunLog) response.AgentRunLogResponse {
|
|||||||
PlannedSkillCode: item.PlannedSkillCode,
|
PlannedSkillCode: item.PlannedSkillCode,
|
||||||
PlannedSkillName: item.PlannedSkillName,
|
PlannedSkillName: item.PlannedSkillName,
|
||||||
SkillRouteTrace: item.SkillRouteTrace,
|
SkillRouteTrace: item.SkillRouteTrace,
|
||||||
|
ToolSearchTrace: item.ToolSearchTrace,
|
||||||
PlannedToolCode: item.PlannedToolCode,
|
PlannedToolCode: item.PlannedToolCode,
|
||||||
PlanReason: item.PlanReason,
|
PlanReason: item.PlanReason,
|
||||||
InterruptType: item.InterruptType,
|
InterruptType: item.InterruptType,
|
||||||
|
|||||||
@@ -952,6 +952,7 @@ type AgentRunLog struct {
|
|||||||
PlannedSkillCode string `gorm:"type:varchar(100);not null;default:'';index"`
|
PlannedSkillCode string `gorm:"type:varchar(100);not null;default:'';index"`
|
||||||
PlannedSkillName string `gorm:"type:varchar(100);not null;default:''"`
|
PlannedSkillName string `gorm:"type:varchar(100);not null;default:''"`
|
||||||
SkillRouteTrace string `gorm:"type:text"`
|
SkillRouteTrace string `gorm:"type:text"`
|
||||||
|
ToolSearchTrace string `gorm:"type:text"`
|
||||||
PlannedToolCode string `gorm:"type:varchar(200);not null;default:'';index"`
|
PlannedToolCode string `gorm:"type:varchar(200);not null;default:'';index"`
|
||||||
PlanReason string `gorm:"type:varchar(500);not null;default:''"`
|
PlanReason string `gorm:"type:varchar(500);not null;default:''"`
|
||||||
InterruptType string `gorm:"type:varchar(50);not null;default:'';index"`
|
InterruptType string `gorm:"type:varchar(50);not null;default:'';index"`
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ type AgentRunLogResponse struct {
|
|||||||
PlannedSkillCode string `json:"plannedSkillCode"`
|
PlannedSkillCode string `json:"plannedSkillCode"`
|
||||||
PlannedSkillName string `json:"plannedSkillName"`
|
PlannedSkillName string `json:"plannedSkillName"`
|
||||||
SkillRouteTrace string `json:"skillRouteTrace"`
|
SkillRouteTrace string `json:"skillRouteTrace"`
|
||||||
|
ToolSearchTrace string `json:"toolSearchTrace"`
|
||||||
PlannedToolCode string `json:"plannedToolCode"`
|
PlannedToolCode string `json:"plannedToolCode"`
|
||||||
PlanReason string `json:"planReason"`
|
PlanReason string `json:"planReason"`
|
||||||
InterruptType string `json:"interruptType"`
|
InterruptType string `json:"interruptType"`
|
||||||
|
|||||||
@@ -91,6 +91,10 @@ export default function DashboardAgentRunLogsPage() {
|
|||||||
() => safeParseJSON(activeLog?.traceData ?? ""),
|
() => safeParseJSON(activeLog?.traceData ?? ""),
|
||||||
[activeLog?.traceData]
|
[activeLog?.traceData]
|
||||||
)
|
)
|
||||||
|
const activeToolSearchTrace = useMemo(
|
||||||
|
() => safeParseJSON(activeLog?.toolSearchTrace ?? ""),
|
||||||
|
[activeLog?.toolSearchTrace]
|
||||||
|
)
|
||||||
|
|
||||||
const aiAgentOptions = useMemo(
|
const aiAgentOptions = useMemo(
|
||||||
() => [
|
() => [
|
||||||
@@ -372,6 +376,14 @@ export default function DashboardAgentRunLogsPage() {
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<TextBlock
|
||||||
|
title="动态工具选择"
|
||||||
|
value={
|
||||||
|
activeToolSearchTrace
|
||||||
|
? JSON.stringify(activeToolSearchTrace, null, 2)
|
||||||
|
: activeLog.toolSearchTrace
|
||||||
|
}
|
||||||
|
/>
|
||||||
<TextBlock
|
<TextBlock
|
||||||
icon={<BotMessageSquareIcon className="size-4" />}
|
icon={<BotMessageSquareIcon className="size-4" />}
|
||||||
title="用户问题"
|
title="用户问题"
|
||||||
|
|||||||
@@ -365,6 +365,7 @@ export type AgentRunLog = {
|
|||||||
plannedSkillCode: string
|
plannedSkillCode: string
|
||||||
plannedSkillName: string
|
plannedSkillName: string
|
||||||
skillRouteTrace: string
|
skillRouteTrace: string
|
||||||
|
toolSearchTrace: string
|
||||||
plannedToolCode: string
|
plannedToolCode: string
|
||||||
planReason: string
|
planReason: string
|
||||||
interruptType: string
|
interruptType: string
|
||||||
|
|||||||
Reference in New Issue
Block a user