diff --git a/internal/ai/runtime/internal/executor/context_builders.go b/internal/ai/runtime/internal/executor/context_builders.go new file mode 100644 index 0000000..f6646bd --- /dev/null +++ b/internal/ai/runtime/internal/executor/context_builders.go @@ -0,0 +1,58 @@ +package executor + +import ( + "context" + "strings" + + "cs-agent/internal/ai/runtime/internal/impl/adapter" + "cs-agent/internal/ai/runtime/internal/impl/callbacks" + "cs-agent/internal/ai/runtime/internal/impl/retrievers" + "cs-agent/internal/pkg/utils" + + "github.com/cloudwego/eino/schema" +) + +func buildRunMessages(ctx context.Context, req RunInput, summary *RunResult, collector *callbacks.RuntimeTraceCollector) []*schema.Message { + if req.Conversation == nil || req.UserMessage == nil { + return nil + } + history := adapter.BuildHistoryMessages(req.Conversation.ID, req.UserMessage.ID, 12) + if summary != nil { + summary.HistoryMessageCount = len(history.Messages) + } + if collector != nil { + collector.Data.Input.HistoryMessageCount = len(history.Messages) + if req.AIAgent != nil { + collector.Data.Input.KnowledgeBaseIDs = utils.SplitInt64s(req.AIAgent.KnowledgeIDs) + } + collector.Data.Input.CurrentUserMessagePreview = preview(req.UserMessage.Content, 120) + } + messages := make([]*schema.Message, 0, len(history.Messages)+3) + messages = append(messages, history.Messages...) + appendRetrievedContext(ctx, req, summary, collector, &messages) + messages = append(messages, schema.UserMessage(strings.TrimSpace(req.UserMessage.Content))) + return messages +} + +func appendRetrievedContext(ctx context.Context, req RunInput, summary *RunResult, collector *callbacks.RuntimeTraceCollector, messages *[]*schema.Message) { + if req.AIAgent == nil || req.UserMessage == nil || messages == nil { + return + } + retriever := retrievers.NewKnowledgeRetriever(req.AIAgent) + retrieveOptions := retrievers.DefaultKnowledgeRetrieveOptions() + retrieveOptions.QueryPreview = preview(req.UserMessage.Content, 120) + retrieveResult, retrieveErr := retriever.RetrieveContextByOptions(ctx, retrieveOptions, strings.TrimSpace(req.UserMessage.Content)) + if retrieveErr != nil || retrieveResult == nil { + return + } + if summary != nil { + summary.RetrieverCount = len(retrieveResult.Hits) + } + if collector != nil { + collector.SetRetrieverSummary(retrieveResult.TraceSummary) + collector.Data.Retriever.Items = append(collector.Data.Retriever.Items, retrieveResult.TraceItems...) + } + if strings.TrimSpace(retrieveResult.ContextText) != "" { + *messages = append(*messages, schema.SystemMessage(retrieveResult.ContextText)) + } +} diff --git a/internal/ai/runtime/internal/executor/event_consumer.go b/internal/ai/runtime/internal/executor/event_consumer.go new file mode 100644 index 0000000..f130507 --- /dev/null +++ b/internal/ai/runtime/internal/executor/event_consumer.go @@ -0,0 +1,92 @@ +package executor + +import ( + "strings" + + "cs-agent/internal/ai/runtime/internal/impl/callbacks" + + "github.com/cloudwego/eino/adk" + "github.com/cloudwego/eino/schema" +) + +func consumeAgentEvents(events *adk.AsyncIterator[*adk.AgentEvent], summary *RunResult, collector *callbacks.RuntimeTraceCollector, toolDefsByModelName map[string]string) { + if summary == nil { + return + } + if collector == nil { + collector = callbacks.NewRuntimeTraceCollector() + } + for { + event, ok := events.Next() + if !ok { + break + } + if event == nil { + continue + } + if event.Action != nil && event.Action.Interrupted != nil { + summary.Status = "interrupted" + summary.Interrupted = true + summary.Interrupts = buildInterruptSummaries(event) + } + if event.Err != nil { + errMsg := strings.TrimSpace(event.Err.Error()) + if errMsg != "" { + summary.Status = "error" + summary.ErrorMessage = errMsg + } + } + if event.Output == nil || event.Output.MessageOutput == nil { + continue + } + messageOutput := event.Output.MessageOutput + switch messageOutput.Role { + case schema.Assistant: + replyText := strings.TrimSpace(messageOutput.Message.Content) + if replyText != "" { + summary.ReplyText = replyText + } + case schema.Tool: + toolName := strings.TrimSpace(messageOutput.ToolName) + if toolName == "" { + continue + } + toolCode := toolName + if mappedCode, ok := toolDefsByModelName[toolName]; ok && strings.TrimSpace(mappedCode) != "" { + toolCode = strings.TrimSpace(mappedCode) + } + summary.InvokedToolCodes = appendIfMissing(summary.InvokedToolCodes, toolCode) + } + } + if summary.Status == "started" { + switch { + case strings.TrimSpace(summary.ErrorMessage) != "": + summary.Status = "error" + case summary.Interrupted: + summary.Status = "interrupted" + case strings.TrimSpace(summary.ReplyText) != "": + summary.Status = "completed" + default: + summary.Status = "fallback" + } + } + summary.ToolCallCount = len(summary.InvokedToolCodes) +} + +func buildInterruptSummaries(event *adk.AgentEvent) []InterruptContextSummary { + if event == nil || event.Action == nil || event.Action.Interrupted == nil { + return nil + } + interrupts := event.Action.Interrupted.InterruptContexts + result := make([]InterruptContextSummary, 0, len(interrupts)) + for _, item := range interrupts { + if item == nil { + continue + } + result = append(result, InterruptContextSummary{ + ID: strings.TrimSpace(item.ID), + InfoPreview: previewInterruptInfo(item.Info), + }) + } + return result +} diff --git a/internal/ai/runtime/internal/executor/helpers.go b/internal/ai/runtime/internal/executor/helpers.go new file mode 100644 index 0000000..18b45d3 --- /dev/null +++ b/internal/ai/runtime/internal/executor/helpers.go @@ -0,0 +1,28 @@ +package executor + +import "strings" + +func appendIfMissing(items []string, item string) []string { + item = strings.TrimSpace(item) + if item == "" { + return items + } + for _, existing := range items { + if strings.TrimSpace(existing) == item { + return items + } + } + return append(items, item) +} + +func preview(text string, limit int) string { + text = strings.TrimSpace(text) + if text == "" || limit <= 0 { + return "" + } + runes := []rune(text) + if len(runes) <= limit { + return string(runes) + } + return string(runes[:limit]) + "..." +} diff --git a/internal/ai/runtime/internal/executor/run_options.go b/internal/ai/runtime/internal/executor/run_options.go new file mode 100644 index 0000000..34ba797 --- /dev/null +++ b/internal/ai/runtime/internal/executor/run_options.go @@ -0,0 +1,57 @@ +package executor + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/cloudwego/eino/adk" + "github.com/cloudwego/eino/schema" +) + +func resolveCheckPointID(input string, runID string) string { + checkPointID := strings.TrimSpace(input) + if checkPointID != "" { + return checkPointID + } + return "eino_cp_" + strings.TrimSpace(runID) +} + +func buildResumeDataMessage(resumeData map[string]any) *schema.Message { + if len(resumeData) == 0 { + return nil + } + data, err := json.Marshal(resumeData) + if err != nil { + return schema.UserMessage(fmt.Sprint(resumeData)) + } + return schema.UserMessage(string(data)) +} + +func buildRunOptions(checkPointID string) []adk.AgentRunOption { + options := make([]adk.AgentRunOption, 0, 1) + if strings.TrimSpace(checkPointID) != "" { + options = append(options, adk.WithCheckPointID(checkPointID)) + } + return options +} + +func buildResumeOptions(checkPointID string, resumeData *schema.Message) []adk.AgentRunOption { + options := make([]adk.AgentRunOption, 0, 1) + if strings.TrimSpace(checkPointID) != "" { + options = append(options, adk.WithCheckPointID(checkPointID)) + } + _ = resumeData + return options +} + +func previewInterruptInfo(info any) string { + if info == nil { + return "" + } + data, err := json.Marshal(info) + if err != nil { + return fmt.Sprint(info) + } + return string(data) +} diff --git a/internal/ai/runtime/internal/executor/service.go b/internal/ai/runtime/internal/executor/service.go index 676d161..73c689f 100644 --- a/internal/ai/runtime/internal/executor/service.go +++ b/internal/ai/runtime/internal/executor/service.go @@ -2,22 +2,12 @@ package executor import ( "context" - "encoding/json" "fmt" "strings" - "cs-agent/internal/ai/runtime/internal/impl/adapter" "cs-agent/internal/ai/runtime/internal/impl/callbacks" "cs-agent/internal/ai/runtime/internal/impl/factory" - "cs-agent/internal/ai/runtime/internal/impl/retrievers" - "cs-agent/internal/ai/runtime/registry" - "cs-agent/internal/models" - "cs-agent/internal/pkg/toolx" - "cs-agent/internal/pkg/utils" - "github.com/cloudwego/eino/adk" - einotool "github.com/cloudwego/eino/components/tool" - "github.com/cloudwego/eino/schema" "github.com/google/uuid" ) @@ -61,12 +51,6 @@ func (s *Service) ExecuteRun(ctx context.Context, req RunInput) (*RunResult, err return summary, fmt.Errorf("%s", summary.ErrorMessage) } - history := adapter.BuildHistoryMessages(req.Conversation.ID, req.UserMessage.ID, 12) - summary.HistoryMessageCount = len(history.Messages) - collector.Data.Input.HistoryMessageCount = len(history.Messages) - collector.Data.Input.KnowledgeBaseIDs = utils.SplitInt64s(req.AIAgent.KnowledgeIDs) - collector.Data.Input.CurrentUserMessagePreview = preview(req.UserMessage.Content, 120) - toolDefs, err := factory.NewToolFactory().BuildMCPTools(req.AIAgent) if err != nil { summary.Status = "error" @@ -77,31 +61,10 @@ func (s *Service) ExecuteRun(ctx context.Context, req RunInput) (*RunResult, err summary.TraceData = collector.Marshal() return summary, err } - filteredToolDefs := filterToolDefinitionsBySkill(toolDefs, req.SelectedSkill) - toolDefsByModelName := make(map[string]string, len(filteredToolDefs)) - for _, item := range filteredToolDefs { - summary.ToolCodes = append(summary.ToolCodes, item.ToolCode) - toolDefsByModelName[item.ModelName] = item.ToolCode - } - if len(filteredToolDefs) > 0 { - summary.ToolCodes = appendIfMissing(summary.ToolCodes, toolx.BuiltinToolSearch.Code) - toolDefsByModelName[toolx.BuiltinToolSearch.Name] = toolx.BuiltinToolSearch.Code - } - if req.SelectedSkill != nil { - summary.ToolCodes = appendIfMissing(summary.ToolCodes, toolx.BuiltinSkill.Code) - toolDefsByModelName[toolx.BuiltinSkill.Name] = toolx.BuiltinSkill.Code - } - for modelName, toolCode := range toolSetStaticToolCodes(req.ToolSet) { - toolCode = strings.TrimSpace(toolCode) - modelName = strings.TrimSpace(modelName) - if toolCode == "" || modelName == "" { - continue - } - summary.ToolCodes = appendIfMissing(summary.ToolCodes, toolCode) - toolDefsByModelName[modelName] = toolCode - } + tooling := prepareTooling(toolDefs, req.SelectedSkill, req.ToolSet, req.SelectedSkill != nil) + summary.ToolCodes = append(summary.ToolCodes, tooling.toolCodes...) collector.Data.Input.ToolCodes = append(collector.Data.Input.ToolCodes, summary.ToolCodes...) - collector.SetTooling(staticToolCodeList(req.ToolSet), definitionToolCodes(filteredToolDefs), len(filteredToolDefs) > 0) + collector.SetTooling(tooling.staticToolCodes, definitionToolCodes(tooling.definitions), len(tooling.definitions) > 0) collector.Data.Model.Provider = string(req.AIConfig.Provider) collector.Data.Model.Name = req.AIConfig.ModelName @@ -124,11 +87,11 @@ func (s *Service) ExecuteRun(ctx context.Context, req RunInput) (*RunResult, err AIAgent: req.AIAgent, AIConfig: req.AIConfig, SelectedSkill: req.SelectedSkill, - InstructionToolDefinitions: filteredToolDefs, - DynamicMCPToolDefinitions: filteredToolDefs, - StaticTools: toolSetStaticTools(req.ToolSet), - StaticToolCodes: toolSetStaticToolCodes(req.ToolSet), - StaticToolMetadata: toolSetStaticToolMetadata(req.ToolSet), + InstructionToolDefinitions: tooling.definitions, + DynamicMCPToolDefinitions: tooling.definitions, + StaticTools: tooling.staticTools, + StaticToolCodes: tooling.staticToolCodeMap, + StaticToolMetadata: tooling.staticToolMetadata, Collector: collector, }) if err != nil { @@ -141,10 +104,7 @@ func (s *Service) ExecuteRun(ctx context.Context, req RunInput) (*RunResult, err return summary, err } - checkPointID := strings.TrimSpace(req.CheckPointID) - if checkPointID == "" { - checkPointID = "eino_cp_" + summary.RunID - } + checkPointID := resolveCheckPointID(req.CheckPointID, summary.RunID) summary.CheckPointID = checkPointID runner := s.runnerFactory.Build(ctx, agent, false, true) if runner == nil { @@ -156,24 +116,9 @@ func (s *Service) ExecuteRun(ctx context.Context, req RunInput) (*RunResult, err summary.TraceData = collector.Marshal() return summary, fmt.Errorf("%s", summary.ErrorMessage) } - messages := make([]*schema.Message, 0, len(history.Messages)+3) - messages = append(messages, history.Messages...) - - retriever := retrievers.NewKnowledgeRetriever(req.AIAgent) - retrieveOptions := retrievers.DefaultKnowledgeRetrieveOptions() - retrieveOptions.QueryPreview = preview(req.UserMessage.Content, 120) - if retrieveResult, retrieveErr := retriever.RetrieveContextByOptions(ctx, retrieveOptions, strings.TrimSpace(req.UserMessage.Content)); retrieveErr == nil && retrieveResult != nil { - summary.RetrieverCount = len(retrieveResult.Hits) - collector.SetRetrieverSummary(retrieveResult.TraceSummary) - collector.Data.Retriever.Items = append(collector.Data.Retriever.Items, retrieveResult.TraceItems...) - if strings.TrimSpace(retrieveResult.ContextText) != "" { - messages = append(messages, schema.SystemMessage(retrieveResult.ContextText)) - } - } - - messages = append(messages, schema.UserMessage(strings.TrimSpace(req.UserMessage.Content))) + messages := buildRunMessages(ctx, req, summary, collector) collector.Data.Interrupt.CheckPointID = checkPointID - consumeAgentEvents(runner.Run(ctx, messages, buildRunOptions(checkPointID)...), summary, collector, toolDefsByModelName) + consumeAgentEvents(runner.Run(ctx, messages, buildRunOptions(checkPointID)...), summary, collector, tooling.toolDefsByModelName) summary.ModelName = req.AIConfig.ModelName collector.Data.Status = summary.Status collector.Data.Output.ReplyText = summary.ReplyText @@ -231,37 +176,21 @@ func (s *Service) ExecuteResume(ctx context.Context, req ResumeInput) (*RunResul summary.TraceData = collector.Marshal() return summary, err } - toolDefsByModelName := make(map[string]string, len(toolDefs)) - for _, item := range toolDefs { - summary.ToolCodes = append(summary.ToolCodes, item.ToolCode) - toolDefsByModelName[item.ModelName] = item.ToolCode - } - if len(toolDefs) > 0 { - summary.ToolCodes = appendIfMissing(summary.ToolCodes, toolx.BuiltinToolSearch.Code) - toolDefsByModelName[toolx.BuiltinToolSearch.Name] = toolx.BuiltinToolSearch.Code - } - for modelName, toolCode := range toolSetStaticToolCodes(req.ToolSet) { - toolCode = strings.TrimSpace(toolCode) - modelName = strings.TrimSpace(modelName) - if toolCode == "" || modelName == "" { - continue - } - summary.ToolCodes = appendIfMissing(summary.ToolCodes, toolCode) - toolDefsByModelName[modelName] = toolCode - } + tooling := prepareTooling(toolDefs, nil, req.ToolSet, false) + summary.ToolCodes = append(summary.ToolCodes, tooling.toolCodes...) collector.Data.Input.ToolCodes = append(collector.Data.Input.ToolCodes, summary.ToolCodes...) - collector.SetTooling(staticToolCodeList(req.ToolSet), definitionToolCodes(toolDefs), len(toolDefs) > 0) + collector.SetTooling(tooling.staticToolCodes, definitionToolCodes(tooling.definitions), len(tooling.definitions) > 0) collector.Data.Model.Provider = string(req.AIConfig.Provider) collector.Data.Model.Name = req.AIConfig.ModelName agent, err := s.agentFactory.BuildCustomerServiceAgent(ctx, factory.BuildCustomerServiceAgentInput{ AIAgent: req.AIAgent, AIConfig: req.AIConfig, - InstructionToolDefinitions: toolDefs, - DynamicMCPToolDefinitions: toolDefs, - StaticTools: toolSetStaticTools(req.ToolSet), - StaticToolCodes: toolSetStaticToolCodes(req.ToolSet), - StaticToolMetadata: toolSetStaticToolMetadata(req.ToolSet), + InstructionToolDefinitions: tooling.definitions, + DynamicMCPToolDefinitions: tooling.definitions, + StaticTools: tooling.staticTools, + StaticToolCodes: tooling.staticToolCodeMap, + StaticToolMetadata: tooling.staticToolMetadata, Collector: collector, }) if err != nil { @@ -294,7 +223,7 @@ func (s *Service) ExecuteResume(ctx context.Context, req ResumeInput) (*RunResul summary.TraceData = collector.Marshal() return summary, err } - consumeAgentEvents(iter, summary, collector, toolDefsByModelName) + consumeAgentEvents(iter, summary, collector, tooling.toolDefsByModelName) summary.ModelName = req.AIConfig.ModelName collector.Data.Status = summary.Status collector.Data.Output.ReplyText = summary.ReplyText @@ -302,272 +231,3 @@ func (s *Service) ExecuteResume(ctx context.Context, req ResumeInput) (*RunResul summary.TraceData = collector.Marshal() return summary, nil } - -func buildResumeDataMessage(resumeData map[string]any) *schema.Message { - if len(resumeData) == 0 { - return nil - } - data, err := json.Marshal(resumeData) - if err != nil { - return schema.UserMessage(fmt.Sprint(resumeData)) - } - return schema.UserMessage(string(data)) -} - -func buildRunOptions(checkPointID string) []adk.AgentRunOption { - options := make([]adk.AgentRunOption, 0, 1) - if strings.TrimSpace(checkPointID) != "" { - options = append(options, adk.WithCheckPointID(checkPointID)) - } - return options -} - -func buildResumeOptions(checkPointID string, resumeData *schema.Message) []adk.AgentRunOption { - options := make([]adk.AgentRunOption, 0, 1) - if strings.TrimSpace(checkPointID) != "" { - options = append(options, adk.WithCheckPointID(checkPointID)) - } - _ = resumeData - return options -} - -func consumeAgentEvents(events *adk.AsyncIterator[*adk.AgentEvent], summary *RunResult, collector *callbacks.RuntimeTraceCollector, toolDefsByModelName map[string]string) { - if summary == nil { - return - } - if collector == nil { - collector = callbacks.NewRuntimeTraceCollector() - } - for { - event, ok := events.Next() - if !ok { - break - } - if event == nil { - continue - } - if event.Action != nil && event.Action.Interrupted != nil { - summary.Status = "interrupted" - summary.Interrupted = true - summary.Interrupts = buildInterruptSummaries(event) - } - if event.Err != nil { - errMsg := strings.TrimSpace(event.Err.Error()) - if errMsg != "" { - summary.Status = "error" - summary.ErrorMessage = errMsg - } - } - if event.Output == nil || event.Output.MessageOutput == nil { - continue - } - messageOutput := event.Output.MessageOutput - switch messageOutput.Role { - case schema.Assistant: - replyText := strings.TrimSpace(messageOutput.Message.Content) - if replyText != "" { - summary.ReplyText = replyText - } - case schema.Tool: - toolName := strings.TrimSpace(messageOutput.ToolName) - if toolName == "" { - continue - } - toolCode := toolName - if mappedCode, ok := toolDefsByModelName[toolName]; ok && strings.TrimSpace(mappedCode) != "" { - toolCode = strings.TrimSpace(mappedCode) - } - summary.InvokedToolCodes = appendIfMissing(summary.InvokedToolCodes, toolCode) - } - } - if summary.Status == "started" { - switch { - case strings.TrimSpace(summary.ErrorMessage) != "": - summary.Status = "error" - case summary.Interrupted: - summary.Status = "interrupted" - case strings.TrimSpace(summary.ReplyText) != "": - summary.Status = "completed" - default: - summary.Status = "fallback" - } - } - summary.ToolCallCount = len(summary.InvokedToolCodes) -} - -func buildInterruptSummaries(event *adk.AgentEvent) []InterruptContextSummary { - if event == nil || event.Action == nil || event.Action.Interrupted == nil { - return nil - } - interrupts := event.Action.Interrupted.InterruptContexts - result := make([]InterruptContextSummary, 0, len(interrupts)) - for _, item := range interrupts { - if item == nil { - continue - } - result = append(result, InterruptContextSummary{ - ID: strings.TrimSpace(item.ID), - InfoPreview: previewInterruptInfo(item.Info), - }) - } - return result -} - -func previewInterruptInfo(info any) string { - if info == nil { - return "" - } - data, err := json.Marshal(info) - if err != nil { - return fmt.Sprint(info) - } - return string(data) -} - -func appendIfMissing(items []string, item string) []string { - item = strings.TrimSpace(item) - if item == "" { - return items - } - for _, existing := range items { - if strings.TrimSpace(existing) == item { - return items - } - } - return append(items, item) -} - -func staticToolCodeList(toolSet *registry.ToolSet) []string { - if toolSet == nil { - return nil - } - metadata := toolSetStaticToolMetadata(toolSet) - ret := make([]string, 0, len(metadata)) - for _, item := range metadata { - code := strings.TrimSpace(item.ToolCode) - if code == "" { - continue - } - ret = appendIfMissing(ret, code) - } - if len(ret) > 0 { - return ret - } - for _, code := range toolSetStaticToolCodes(toolSet) { - code = strings.TrimSpace(code) - if code == "" { - continue - } - ret = appendIfMissing(ret, code) - } - return ret -} - -func toolSetStaticTools(toolSet *registry.ToolSet) []einotool.BaseTool { - if toolSet == nil { - return nil - } - return append([]einotool.BaseTool(nil), toolSet.StaticTools...) -} - -func toolSetStaticToolCodes(toolSet *registry.ToolSet) map[string]string { - if toolSet == nil || len(toolSet.StaticToolCodes) == 0 { - return nil - } - ret := make(map[string]string, len(toolSet.StaticToolCodes)) - for name, code := range toolSet.StaticToolCodes { - ret[strings.TrimSpace(name)] = strings.TrimSpace(code) - } - return ret -} - -func toolSetStaticToolMetadata(toolSet *registry.ToolSet) map[string]registry.ToolMetadata { - if toolSet == nil || len(toolSet.StaticToolMetadata) == 0 { - return nil - } - ret := make(map[string]registry.ToolMetadata, len(toolSet.StaticToolMetadata)) - for name, item := range toolSet.StaticToolMetadata { - trimmedName := strings.TrimSpace(name) - if trimmedName == "" { - continue - } - item.ToolCode = strings.TrimSpace(item.ToolCode) - item.ServerCode = strings.TrimSpace(item.ServerCode) - item.ToolName = strings.TrimSpace(item.ToolName) - item.SourceType = strings.TrimSpace(item.SourceType) - ret[trimmedName] = item - } - return ret -} - -func definitionToolCodes(defs []adapter.MCPToolDefinition) []string { - ret := make([]string, 0, len(defs)) - for _, item := range defs { - code := strings.TrimSpace(item.ToolCode) - if code == "" { - continue - } - ret = append(ret, code) - } - return ret -} - -func filterToolDefinitionsBySkill(defs []adapter.MCPToolDefinition, skill *models.SkillDefinition) []adapter.MCPToolDefinition { - if skill == nil || strings.TrimSpace(skill.ToolWhitelist) == "" { - return defs - } - var allowed []string - if err := json.Unmarshal([]byte(skill.ToolWhitelist), &allowed); err != nil { - return defs - } - allowedSet := make(map[string]struct{}, len(allowed)) - for _, item := range allowed { - item = toolx.NormalizeToolCodeAlias(item) - if strings.TrimSpace(item) == "" { - continue - } - allowedSet[strings.TrimSpace(item)] = struct{}{} - } - if len(allowedSet) == 0 { - return defs - } - ret := make([]adapter.MCPToolDefinition, 0, len(defs)) - for _, item := range defs { - if _, ok := allowedSet[strings.TrimSpace(item.ToolCode)]; ok { - ret = append(ret, item) - } - } - return ret -} - -func parseJSONArrayList(raw string) []string { - raw = strings.TrimSpace(raw) - if raw == "" { - return nil - } - var items []string - if err := json.Unmarshal([]byte(raw), &items); err != nil { - return nil - } - ret := make([]string, 0, len(items)) - for _, item := range items { - item = strings.TrimSpace(item) - if item == "" { - continue - } - ret = append(ret, item) - } - return ret -} - -func preview(text string, limit int) string { - text = strings.TrimSpace(text) - if text == "" || limit <= 0 { - return "" - } - runes := []rune(text) - if len(runes) <= limit { - return string(runes) - } - return string(runes[:limit]) + "..." -} diff --git a/internal/ai/runtime/internal/executor/tooling.go b/internal/ai/runtime/internal/executor/tooling.go new file mode 100644 index 0000000..2766e9e --- /dev/null +++ b/internal/ai/runtime/internal/executor/tooling.go @@ -0,0 +1,186 @@ +package executor + +import ( + "encoding/json" + "strings" + + "cs-agent/internal/ai/runtime/internal/impl/adapter" + "cs-agent/internal/ai/runtime/registry" + "cs-agent/internal/models" + "cs-agent/internal/pkg/toolx" + + einotool "github.com/cloudwego/eino/components/tool" +) + +type preparedTooling struct { + definitions []adapter.MCPToolDefinition + toolCodes []string + toolDefsByModelName map[string]string + staticToolCodes []string + staticTools []einotool.BaseTool + staticToolCodeMap map[string]string + staticToolMetadata map[string]registry.ToolMetadata +} + +func prepareTooling(defs []adapter.MCPToolDefinition, selectedSkill *models.SkillDefinition, toolSet *registry.ToolSet, includeSkillTool bool) preparedTooling { + filteredDefs := filterToolDefinitionsBySkill(defs, selectedSkill) + ret := preparedTooling{ + definitions: filteredDefs, + toolCodes: make([]string, 0, len(filteredDefs)+2), + toolDefsByModelName: make(map[string]string, len(filteredDefs)), + staticToolCodes: staticToolCodeList(toolSet), + staticTools: toolSetStaticTools(toolSet), + staticToolCodeMap: toolSetStaticToolCodes(toolSet), + staticToolMetadata: toolSetStaticToolMetadata(toolSet), + } + for _, item := range filteredDefs { + toolCode := strings.TrimSpace(item.ToolCode) + modelName := strings.TrimSpace(item.ModelName) + if toolCode == "" || modelName == "" { + continue + } + ret.toolCodes = appendIfMissing(ret.toolCodes, toolCode) + ret.toolDefsByModelName[modelName] = toolCode + } + if len(filteredDefs) > 0 { + ret.toolCodes = appendIfMissing(ret.toolCodes, toolx.BuiltinToolSearch.Code) + ret.toolDefsByModelName[toolx.BuiltinToolSearch.Name] = toolx.BuiltinToolSearch.Code + } + if includeSkillTool { + ret.toolCodes = appendIfMissing(ret.toolCodes, toolx.BuiltinSkill.Code) + ret.toolDefsByModelName[toolx.BuiltinSkill.Name] = toolx.BuiltinSkill.Code + } + for modelName, toolCode := range ret.staticToolCodeMap { + modelName = strings.TrimSpace(modelName) + toolCode = strings.TrimSpace(toolCode) + if modelName == "" || toolCode == "" { + continue + } + ret.toolCodes = appendIfMissing(ret.toolCodes, toolCode) + ret.toolDefsByModelName[modelName] = toolCode + } + return ret +} + +func toolSetStaticTools(toolSet *registry.ToolSet) []einotool.BaseTool { + if toolSet == nil { + return nil + } + return append([]einotool.BaseTool(nil), toolSet.StaticTools...) +} + +func toolSetStaticToolCodes(toolSet *registry.ToolSet) map[string]string { + if toolSet == nil || len(toolSet.StaticToolCodes) == 0 { + return nil + } + ret := make(map[string]string, len(toolSet.StaticToolCodes)) + for name, code := range toolSet.StaticToolCodes { + ret[strings.TrimSpace(name)] = strings.TrimSpace(code) + } + return ret +} + +func toolSetStaticToolMetadata(toolSet *registry.ToolSet) map[string]registry.ToolMetadata { + if toolSet == nil || len(toolSet.StaticToolMetadata) == 0 { + return nil + } + ret := make(map[string]registry.ToolMetadata, len(toolSet.StaticToolMetadata)) + for name, item := range toolSet.StaticToolMetadata { + trimmedName := strings.TrimSpace(name) + if trimmedName == "" { + continue + } + item.ToolCode = strings.TrimSpace(item.ToolCode) + item.ServerCode = strings.TrimSpace(item.ServerCode) + item.ToolName = strings.TrimSpace(item.ToolName) + item.SourceType = strings.TrimSpace(item.SourceType) + ret[trimmedName] = item + } + return ret +} + +func staticToolCodeList(toolSet *registry.ToolSet) []string { + if toolSet == nil { + return nil + } + metadata := toolSetStaticToolMetadata(toolSet) + ret := make([]string, 0, len(metadata)) + for _, item := range metadata { + code := strings.TrimSpace(item.ToolCode) + if code == "" { + continue + } + ret = appendIfMissing(ret, code) + } + if len(ret) > 0 { + return ret + } + for _, code := range toolSetStaticToolCodes(toolSet) { + code = strings.TrimSpace(code) + if code == "" { + continue + } + ret = appendIfMissing(ret, code) + } + return ret +} + +func definitionToolCodes(defs []adapter.MCPToolDefinition) []string { + ret := make([]string, 0, len(defs)) + for _, item := range defs { + code := strings.TrimSpace(item.ToolCode) + if code == "" { + continue + } + ret = append(ret, code) + } + return ret +} + +func filterToolDefinitionsBySkill(defs []adapter.MCPToolDefinition, skill *models.SkillDefinition) []adapter.MCPToolDefinition { + if skill == nil || strings.TrimSpace(skill.ToolWhitelist) == "" { + return defs + } + var allowed []string + if err := json.Unmarshal([]byte(skill.ToolWhitelist), &allowed); err != nil { + return defs + } + allowedSet := make(map[string]struct{}, len(allowed)) + for _, item := range allowed { + item = toolx.NormalizeToolCodeAlias(item) + if strings.TrimSpace(item) == "" { + continue + } + allowedSet[strings.TrimSpace(item)] = struct{}{} + } + if len(allowedSet) == 0 { + return defs + } + ret := make([]adapter.MCPToolDefinition, 0, len(defs)) + for _, item := range defs { + if _, ok := allowedSet[strings.TrimSpace(item.ToolCode)]; ok { + ret = append(ret, item) + } + } + return ret +} + +func parseJSONArrayList(raw string) []string { + raw = strings.TrimSpace(raw) + if raw == "" { + return nil + } + var items []string + if err := json.Unmarshal([]byte(raw), &items); err != nil { + return nil + } + ret := make([]string, 0, len(items)) + for _, item := range items { + item = strings.TrimSpace(item) + if item == "" { + continue + } + ret = append(ret, item) + } + return ret +}