refactor: 将客服后端重构为宿主可嵌入模块
- 注入数据库、运行时配置、统一响应、文件存储和平台 AI 能力,补充业务读写工具与客户快捷操作契约。 - 移除模块内重复的组织、客户、工单、标签、技能、旧工作流、MCP 和迁移实现,将身份权限与业务主体交由宿主管理。 - 使用 libSQL 重构向量存储,并完善图片消息、访客身份、排队调度、企业微信和支持聊天页面。 - 统一 HTTP、DTO 与 WebSocket 的 snake_case 协议,补齐模块初始化、业务动作和公共载荷等回归测试。
This commit is contained in:
@@ -62,35 +62,28 @@ func (t *AnalyzeConversationTool) Info(ctx context.Context) (*schema.ToolInfo, e
|
||||
},
|
||||
},
|
||||
orderedmap.Pair[string, *einojsonschema.Schema]{
|
||||
Key: "observedIssue",
|
||||
Key: "observed_issue",
|
||||
Value: &einojsonschema.Schema{
|
||||
Type: "string",
|
||||
Description: i18nx.Get("tool.graph.analyzeConversation.param.observedIssue"),
|
||||
},
|
||||
},
|
||||
orderedmap.Pair[string, *einojsonschema.Schema]{
|
||||
Key: "needTicket",
|
||||
Value: &einojsonschema.Schema{
|
||||
Type: "boolean",
|
||||
Description: i18nx.Get("tool.graph.triageServiceRequest.param.needTicket"),
|
||||
},
|
||||
},
|
||||
orderedmap.Pair[string, *einojsonschema.Schema]{
|
||||
Key: "needHumanHandoff",
|
||||
Key: "need_human_handoff",
|
||||
Value: &einojsonschema.Schema{
|
||||
Type: "boolean",
|
||||
Description: i18nx.Get("tool.graph.triageServiceRequest.param.needHumanHandoff"),
|
||||
},
|
||||
},
|
||||
orderedmap.Pair[string, *einojsonschema.Schema]{
|
||||
Key: "needQualityCheck",
|
||||
Key: "need_quality_check",
|
||||
Value: &einojsonschema.Schema{
|
||||
Type: "boolean",
|
||||
Description: i18nx.Get("tool.graph.analyzeConversation.param.needQualityCheck"),
|
||||
},
|
||||
},
|
||||
orderedmap.Pair[string, *einojsonschema.Schema]{
|
||||
Key: "additionalContext",
|
||||
Key: "additional_context",
|
||||
Value: &einojsonschema.Schema{
|
||||
Type: "string",
|
||||
Description: i18nx.Get("tool.graph.analyzeConversation.param.additionalContext"),
|
||||
@@ -99,8 +92,8 @@ func (t *AnalyzeConversationTool) Info(ctx context.Context) (*schema.ToolInfo, e
|
||||
)),
|
||||
}),
|
||||
Extra: map[string]any{
|
||||
"toolCode": toolx.GraphAnalyzeConversation.Code,
|
||||
"sourceType": toolx.GraphAnalyzeConversation.SourceType,
|
||||
"tool_code": toolx.GraphAnalyzeConversation.Code,
|
||||
"source_type": toolx.GraphAnalyzeConversation.SourceType,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"code.tczkiot.com/wlw/ai-agent/internal/ai/runtime/graphs"
|
||||
"code.tczkiot.com/wlw/ai-agent/internal/ai/runtime/registry"
|
||||
"code.tczkiot.com/wlw/ai-agent/internal/models"
|
||||
"code.tczkiot.com/wlw/ai-agent/internal/pkg/i18nx"
|
||||
"code.tczkiot.com/wlw/ai-agent/internal/pkg/toolx"
|
||||
|
||||
einotool "github.com/cloudwego/eino/components/tool"
|
||||
"github.com/cloudwego/eino/schema"
|
||||
einojsonschema "github.com/eino-contrib/jsonschema"
|
||||
orderedmap "github.com/wk8/go-ordered-map/v2"
|
||||
)
|
||||
|
||||
type CreateTicketGraphTool struct {
|
||||
conversation models.Conversation
|
||||
aiAgent models.AIAgent
|
||||
}
|
||||
|
||||
func NewCreateTicketGraphTool() *CreateTicketGraphTool {
|
||||
return &CreateTicketGraphTool{}
|
||||
}
|
||||
|
||||
func (t *CreateTicketGraphTool) Spec() toolx.ToolSpec {
|
||||
return toolx.GraphCreateTicketConfirm
|
||||
}
|
||||
|
||||
func (t *CreateTicketGraphTool) Name() string {
|
||||
return toolx.GraphCreateTicketConfirm.Name
|
||||
}
|
||||
|
||||
func (t *CreateTicketGraphTool) Code() string {
|
||||
return toolx.GraphCreateTicketConfirm.Code
|
||||
}
|
||||
|
||||
func (t *CreateTicketGraphTool) Enabled(ctx registry.Context) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (t *CreateTicketGraphTool) Build(ctx registry.Context) (einotool.BaseTool, error) {
|
||||
if !t.Enabled(ctx) {
|
||||
return nil, nil
|
||||
}
|
||||
return &CreateTicketGraphTool{
|
||||
conversation: ctx.Conversation,
|
||||
aiAgent: ctx.AIAgent,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t *CreateTicketGraphTool) Info(ctx context.Context) (*schema.ToolInfo, error) {
|
||||
return &schema.ToolInfo{
|
||||
Name: toolx.GraphCreateTicketConfirm.Name,
|
||||
Desc: i18nx.Get("tool.graph.createTicketConfirm.info"),
|
||||
ParamsOneOf: schema.NewParamsOneOfByJSONSchema(&einojsonschema.Schema{
|
||||
Version: einojsonschema.Version,
|
||||
Type: "object",
|
||||
Required: []string{
|
||||
"title",
|
||||
"description",
|
||||
},
|
||||
Properties: orderedmap.New[string, *einojsonschema.Schema](orderedmap.WithInitialData(
|
||||
orderedmap.Pair[string, *einojsonschema.Schema]{
|
||||
Key: "title",
|
||||
Value: &einojsonschema.Schema{
|
||||
Type: "string",
|
||||
Description: i18nx.Get("tool.graph.createTicketConfirm.param.title"),
|
||||
},
|
||||
},
|
||||
orderedmap.Pair[string, *einojsonschema.Schema]{
|
||||
Key: "description",
|
||||
Value: &einojsonschema.Schema{
|
||||
Type: "string",
|
||||
Description: i18nx.Get("tool.graph.createTicketConfirm.param.description"),
|
||||
},
|
||||
},
|
||||
)),
|
||||
}),
|
||||
Extra: map[string]any{
|
||||
"toolCode": toolx.GraphCreateTicketConfirm.Code,
|
||||
"sourceType": "graph",
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t *CreateTicketGraphTool) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...einotool.Option) (string, error) {
|
||||
return graphs.NewCreateTicketGraph(t.conversation, t.aiAgent).Run(ctx, argumentsInJSON)
|
||||
}
|
||||
@@ -68,8 +68,8 @@ func (t *HandoffGraphTool) Info(ctx context.Context) (*schema.ToolInfo, error) {
|
||||
)),
|
||||
}),
|
||||
Extra: map[string]any{
|
||||
"toolCode": toolx.GraphHandoffConversation.Code,
|
||||
"sourceType": toolx.GraphHandoffConversation.SourceType,
|
||||
"tool_code": toolx.GraphHandoffConversation.Code,
|
||||
"source_type": toolx.GraphHandoffConversation.SourceType,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -19,18 +19,24 @@ func ParseConfirmationDecision(value string) Decision {
|
||||
if value == "" {
|
||||
return ""
|
||||
}
|
||||
confirmWords := []string{"确认", "是", "好的", "可以", "ok", "yes", "继续", "同意"}
|
||||
for _, item := range confirmWords {
|
||||
if strings.Contains(value, item) {
|
||||
return DecisionConfirm
|
||||
}
|
||||
cancelWords := []string{
|
||||
"不确认", "取消", "不用", "不需要", "算了", "no", "cancel",
|
||||
"不提交", "不要提交", "暂不提交", "不办理", "不要办理", "不执行", "不要执行",
|
||||
}
|
||||
cancelWords := []string{"取消", "不用", "不需要", "算了", "no"}
|
||||
for _, item := range cancelWords {
|
||||
if strings.Contains(value, item) {
|
||||
return DecisionCancel
|
||||
}
|
||||
}
|
||||
confirmWords := []string{
|
||||
"确认", "是", "好的", "可以", "ok", "yes", "continue", "confirm", "继续", "同意",
|
||||
"提交", "确定", "办理", "执行",
|
||||
}
|
||||
for _, item := range confirmWords {
|
||||
if strings.Contains(value, item) {
|
||||
return DecisionConfirm
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -40,10 +46,6 @@ func NewRuntimeStaticTool(toolCode string) registry.Tool {
|
||||
return NewTriageServiceRequestTool()
|
||||
case toolx.GraphAnalyzeConversation.Code:
|
||||
return NewAnalyzeConversationTool()
|
||||
case toolx.GraphPrepareTicketDraft.Code:
|
||||
return NewPrepareTicketDraftTool()
|
||||
case toolx.GraphCreateTicketConfirm.Code:
|
||||
return NewCreateTicketGraphTool()
|
||||
case toolx.GraphHandoffConversation.Code:
|
||||
return NewHandoffGraphTool()
|
||||
default:
|
||||
|
||||
@@ -10,8 +10,6 @@ func TestNewRuntimeStaticTool(t *testing.T) {
|
||||
items := []string{
|
||||
toolx.GraphTriageServiceRequest.Code,
|
||||
toolx.GraphAnalyzeConversation.Code,
|
||||
toolx.GraphPrepareTicketDraft.Code,
|
||||
toolx.GraphCreateTicketConfirm.Code,
|
||||
toolx.GraphHandoffConversation.Code,
|
||||
}
|
||||
for _, item := range items {
|
||||
@@ -30,3 +28,16 @@ func TestNewRuntimeStaticToolReturnsNilForUnknownTool(t *testing.T) {
|
||||
t.Fatalf("expected nil tool for unknown tool code")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseConfirmationDecisionSupportsBusinessActionWords(t *testing.T) {
|
||||
for _, input := range []string{"确认", "提交", "确定办理", "执行"} {
|
||||
if got := ParseConfirmationDecision(input); got != DecisionConfirm {
|
||||
t.Fatalf("ParseConfirmationDecision(%q) = %q", input, got)
|
||||
}
|
||||
}
|
||||
for _, input := range []string{"不确认", "好的,取消", "不提交", "不要办理", "暂不执行"} {
|
||||
if got := ParseConfirmationDecision(input); got != DecisionCancel {
|
||||
t.Fatalf("ParseConfirmationDecision(%q) = %q", input, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"code.tczkiot.com/wlw/ai-agent/internal/ai/runtime/graphs"
|
||||
"code.tczkiot.com/wlw/ai-agent/internal/ai/runtime/registry"
|
||||
"code.tczkiot.com/wlw/ai-agent/internal/models"
|
||||
"code.tczkiot.com/wlw/ai-agent/internal/pkg/i18nx"
|
||||
"code.tczkiot.com/wlw/ai-agent/internal/pkg/toolx"
|
||||
|
||||
einotool "github.com/cloudwego/eino/components/tool"
|
||||
"github.com/cloudwego/eino/schema"
|
||||
einojsonschema "github.com/eino-contrib/jsonschema"
|
||||
orderedmap "github.com/wk8/go-ordered-map/v2"
|
||||
)
|
||||
|
||||
type PrepareTicketDraftTool struct {
|
||||
conversation models.Conversation
|
||||
}
|
||||
|
||||
func NewPrepareTicketDraftTool() *PrepareTicketDraftTool {
|
||||
return &PrepareTicketDraftTool{}
|
||||
}
|
||||
|
||||
func (t *PrepareTicketDraftTool) Spec() toolx.ToolSpec {
|
||||
return toolx.GraphPrepareTicketDraft
|
||||
}
|
||||
|
||||
func (t *PrepareTicketDraftTool) Name() string {
|
||||
return toolx.GraphPrepareTicketDraft.Name
|
||||
}
|
||||
|
||||
func (t *PrepareTicketDraftTool) Code() string {
|
||||
return toolx.GraphPrepareTicketDraft.Code
|
||||
}
|
||||
|
||||
func (t *PrepareTicketDraftTool) Enabled(ctx registry.Context) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (t *PrepareTicketDraftTool) Build(ctx registry.Context) (einotool.BaseTool, error) {
|
||||
if !t.Enabled(ctx) {
|
||||
return nil, nil
|
||||
}
|
||||
return &PrepareTicketDraftTool{conversation: ctx.Conversation}, nil
|
||||
}
|
||||
|
||||
func (t *PrepareTicketDraftTool) Info(ctx context.Context) (*schema.ToolInfo, error) {
|
||||
return &schema.ToolInfo{
|
||||
Name: toolx.GraphPrepareTicketDraft.Name,
|
||||
Desc: i18nx.Get("tool.graph.prepareTicketDraft.info"),
|
||||
ParamsOneOf: schema.NewParamsOneOfByJSONSchema(&einojsonschema.Schema{
|
||||
Version: einojsonschema.Version,
|
||||
Type: "object",
|
||||
Properties: orderedmap.New[string, *einojsonschema.Schema](orderedmap.WithInitialData(
|
||||
orderedmap.Pair[string, *einojsonschema.Schema]{
|
||||
Key: "title",
|
||||
Value: &einojsonschema.Schema{
|
||||
Type: "string",
|
||||
Description: i18nx.Get("tool.graph.prepareTicketDraft.param.title"),
|
||||
},
|
||||
},
|
||||
orderedmap.Pair[string, *einojsonschema.Schema]{
|
||||
Key: "description",
|
||||
Value: &einojsonschema.Schema{
|
||||
Type: "string",
|
||||
Description: i18nx.Get("tool.graph.prepareTicketDraft.param.description"),
|
||||
},
|
||||
},
|
||||
orderedmap.Pair[string, *einojsonschema.Schema]{
|
||||
Key: "issue",
|
||||
Value: &einojsonschema.Schema{
|
||||
Type: "string",
|
||||
Description: i18nx.Get("tool.graph.prepareTicketDraft.param.issue"),
|
||||
},
|
||||
},
|
||||
orderedmap.Pair[string, *einojsonschema.Schema]{
|
||||
Key: "impact",
|
||||
Value: &einojsonschema.Schema{
|
||||
Type: "string",
|
||||
Description: i18nx.Get("tool.graph.prepareTicketDraft.param.impact"),
|
||||
},
|
||||
},
|
||||
orderedmap.Pair[string, *einojsonschema.Schema]{
|
||||
Key: "expectedOutcome",
|
||||
Value: &einojsonschema.Schema{
|
||||
Type: "string",
|
||||
Description: i18nx.Get("tool.graph.prepareTicketDraft.param.expectedOutcome"),
|
||||
},
|
||||
},
|
||||
orderedmap.Pair[string, *einojsonschema.Schema]{
|
||||
Key: "currentAttempt",
|
||||
Value: &einojsonschema.Schema{
|
||||
Type: "string",
|
||||
Description: i18nx.Get("tool.graph.prepareTicketDraft.param.currentAttempt"),
|
||||
},
|
||||
},
|
||||
)),
|
||||
}),
|
||||
Extra: map[string]any{
|
||||
"toolCode": toolx.GraphPrepareTicketDraft.Code,
|
||||
"sourceType": "graph",
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t *PrepareTicketDraftTool) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...einotool.Option) (string, error) {
|
||||
return graphs.NewPrepareTicketDraftGraph(t.conversation).Run(ctx, argumentsInJSON)
|
||||
}
|
||||
@@ -1,287 +0,0 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"code.tczkiot.com/wlw/ai-agent/internal/ai/mcps"
|
||||
"code.tczkiot.com/wlw/ai-agent/internal/ai/runtime/registry"
|
||||
"code.tczkiot.com/wlw/ai-agent/internal/ai/runtime/tooling"
|
||||
aitooling "code.tczkiot.com/wlw/ai-agent/internal/ai/tooling"
|
||||
"code.tczkiot.com/wlw/ai-agent/internal/pkg/i18nx"
|
||||
"code.tczkiot.com/wlw/ai-agent/internal/pkg/toolx"
|
||||
|
||||
einotool "github.com/cloudwego/eino/components/tool"
|
||||
"github.com/cloudwego/eino/schema"
|
||||
einojsonschema "github.com/eino-contrib/jsonschema"
|
||||
orderedmap "github.com/wk8/go-ordered-map/v2"
|
||||
)
|
||||
|
||||
type ToolSearchTool struct {
|
||||
allowedToolCodes []string
|
||||
}
|
||||
|
||||
func NewToolSearchTool() *ToolSearchTool {
|
||||
return &ToolSearchTool{}
|
||||
}
|
||||
|
||||
func (t *ToolSearchTool) Spec() toolx.ToolSpec {
|
||||
return toolx.BuiltinToolSearch
|
||||
}
|
||||
|
||||
func (t *ToolSearchTool) Name() string {
|
||||
return toolx.BuiltinToolSearch.Name
|
||||
}
|
||||
|
||||
func (t *ToolSearchTool) Code() string {
|
||||
return toolx.BuiltinToolSearch.Code
|
||||
}
|
||||
|
||||
func (t *ToolSearchTool) Enabled(ctx registry.Context) bool {
|
||||
return len(filterAllowedMCPToolCodes(ctx.AllowedToolCodes)) > 0
|
||||
}
|
||||
|
||||
func (t *ToolSearchTool) Build(ctx registry.Context) (einotool.BaseTool, error) {
|
||||
if !t.Enabled(ctx) {
|
||||
return nil, nil
|
||||
}
|
||||
return &ToolSearchTool{
|
||||
allowedToolCodes: filterAllowedMCPToolCodes(ctx.AllowedToolCodes),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t *ToolSearchTool) Info(ctx context.Context) (*schema.ToolInfo, error) {
|
||||
return &schema.ToolInfo{
|
||||
Name: toolx.BuiltinToolSearch.Name,
|
||||
Desc: "当你需要使用当前会话允许的长尾 MCP 工具时,先调用本工具搜索合适的 toolCode;确认目标后,可再次调用本工具并传入 toolCode 与 arguments 代理执行。不要用它替代明确固定的内置流程工具。",
|
||||
ParamsOneOf: schema.NewParamsOneOfByJSONSchema(&einojsonschema.Schema{
|
||||
Version: einojsonschema.Version,
|
||||
Type: "object",
|
||||
Properties: orderedmap.New[string, *einojsonschema.Schema](orderedmap.WithInitialData(
|
||||
orderedmap.Pair[string, *einojsonschema.Schema]{
|
||||
Key: "query",
|
||||
Value: &einojsonschema.Schema{
|
||||
Type: "string",
|
||||
Description: "要搜索的工具意图、能力或关键词;当只想列出候选工具时使用。",
|
||||
},
|
||||
},
|
||||
orderedmap.Pair[string, *einojsonschema.Schema]{
|
||||
Key: "toolCode",
|
||||
Value: &einojsonschema.Schema{
|
||||
Type: "string",
|
||||
Description: "已确定目标后要调用的 MCP toolCode,例如 mcp_server/tool_name。",
|
||||
},
|
||||
},
|
||||
orderedmap.Pair[string, *einojsonschema.Schema]{
|
||||
Key: "arguments",
|
||||
Value: &einojsonschema.Schema{
|
||||
Type: "object",
|
||||
Description: "调用目标工具时传入的参数对象。",
|
||||
AdditionalProperties: &einojsonschema.Schema{},
|
||||
},
|
||||
},
|
||||
)),
|
||||
}),
|
||||
Extra: map[string]any{
|
||||
"toolCode": toolx.BuiltinToolSearch.Code,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t *ToolSearchTool) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...einotool.Option) (string, error) {
|
||||
if t == nil {
|
||||
return "", fmt.Errorf("tool search tool is nil")
|
||||
}
|
||||
req, err := parseToolSearchRequest(argumentsInJSON)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if req.ToolCode != "" {
|
||||
return t.invokeTargetTool(ctx, req.ToolCode, req.Arguments)
|
||||
}
|
||||
return t.searchCandidates(ctx, req.Query)
|
||||
}
|
||||
|
||||
type toolSearchRequest struct {
|
||||
Query string `json:"query"`
|
||||
ToolCode string `json:"toolCode"`
|
||||
Arguments map[string]any `json:"arguments"`
|
||||
}
|
||||
|
||||
type toolSearchCandidate struct {
|
||||
ToolCode string `json:"toolCode"`
|
||||
ServerCode string `json:"serverCode"`
|
||||
ToolName string `json:"toolName"`
|
||||
Title string `json:"title,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
}
|
||||
|
||||
func parseToolSearchRequest(argumentsInJSON string) (*toolSearchRequest, error) {
|
||||
argumentsInJSON = strings.TrimSpace(argumentsInJSON)
|
||||
if argumentsInJSON == "" {
|
||||
return &toolSearchRequest{}, nil
|
||||
}
|
||||
var req toolSearchRequest
|
||||
if err := json.Unmarshal([]byte(argumentsInJSON), &req); err != nil {
|
||||
return nil, fmt.Errorf("invalid tool_search arguments: %w", err)
|
||||
}
|
||||
req.Query = strings.TrimSpace(req.Query)
|
||||
req.ToolCode = strings.TrimSpace(req.ToolCode)
|
||||
if req.Arguments == nil {
|
||||
req.Arguments = map[string]any{}
|
||||
}
|
||||
return &req, nil
|
||||
}
|
||||
|
||||
func (t *ToolSearchTool) searchCandidates(ctx context.Context, query string) (string, error) {
|
||||
candidates, err := t.loadAllowedCandidates(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
matched := filterCandidatesByQuery(candidates, query)
|
||||
if len(matched) == 0 {
|
||||
return "未找到匹配的动态工具,请换个关键词,或继续向用户追问后再搜索。", nil
|
||||
}
|
||||
if len(matched) > 8 {
|
||||
matched = matched[:8]
|
||||
}
|
||||
buf, err := json.Marshal(map[string]any{
|
||||
"query": strings.TrimSpace(query),
|
||||
"total": len(matched),
|
||||
"candidates": matched,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(buf), nil
|
||||
}
|
||||
|
||||
func (t *ToolSearchTool) invokeTargetTool(ctx context.Context, toolCode string, arguments map[string]any) (string, error) {
|
||||
toolCode = strings.TrimSpace(toolCode)
|
||||
serverCode, toolName := toolx.SplitMCPToolCode(toolCode)
|
||||
if serverCode == "" || toolName == "" {
|
||||
return "", i18nx.Errorf("error.e0077")
|
||||
}
|
||||
if !containsToolCode(t.allowedToolCodes, toolCode) {
|
||||
return "", i18nx.Errorf("error.e0279")
|
||||
}
|
||||
// The published Agent allow-list is the approval boundary for MCP tools.
|
||||
// The registry still enforces call limits and safety metadata.
|
||||
_, result, err := aitooling.DefaultMCPExecutor.Execute(ctx, toolCode, arguments, aitooling.Policy{
|
||||
AllowedToolCodes: t.allowedToolCodes,
|
||||
Confirmed: true,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return aitooling.SanitizePreview(buildToolCallResultSummary(result)), nil
|
||||
}
|
||||
|
||||
func (t *ToolSearchTool) loadAllowedCandidates(ctx context.Context) ([]toolSearchCandidate, error) {
|
||||
serverToToolCodes := make(map[string]map[string]struct{})
|
||||
for _, toolCode := range t.allowedToolCodes {
|
||||
serverCode, toolName := toolx.SplitMCPToolCode(toolCode)
|
||||
if serverCode == "" || toolName == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := serverToToolCodes[serverCode]; !ok {
|
||||
serverToToolCodes[serverCode] = make(map[string]struct{})
|
||||
}
|
||||
serverToToolCodes[serverCode][toolCode] = struct{}{}
|
||||
}
|
||||
serverCodes := make([]string, 0, len(serverToToolCodes))
|
||||
for serverCode := range serverToToolCodes {
|
||||
serverCodes = append(serverCodes, serverCode)
|
||||
}
|
||||
slices.Sort(serverCodes)
|
||||
ret := make([]toolSearchCandidate, 0)
|
||||
for _, serverCode := range serverCodes {
|
||||
tools, err := mcps.Runtime.ListTools(ctx, serverCode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
allowed := serverToToolCodes[serverCode]
|
||||
for _, item := range tools {
|
||||
toolCode := toolx.BuildMCPToolCode(serverCode, item.Name)
|
||||
if _, ok := allowed[toolCode]; !ok {
|
||||
continue
|
||||
}
|
||||
ret = append(ret, toolSearchCandidate{
|
||||
ToolCode: toolCode,
|
||||
ServerCode: serverCode,
|
||||
ToolName: strings.TrimSpace(item.Name),
|
||||
Title: strings.TrimSpace(item.Title),
|
||||
Description: strings.TrimSpace(item.Description),
|
||||
})
|
||||
}
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func filterAllowedMCPToolCodes(input []string) []string {
|
||||
if len(input) == 0 {
|
||||
return nil
|
||||
}
|
||||
ret := make([]string, 0, len(input))
|
||||
for _, item := range input {
|
||||
item = strings.TrimSpace(item)
|
||||
serverCode, toolName := toolx.SplitMCPToolCode(item)
|
||||
if serverCode == "" || toolName == "" {
|
||||
continue
|
||||
}
|
||||
ret = append(ret, item)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func containsToolCode(items []string, target string) bool {
|
||||
target = strings.TrimSpace(target)
|
||||
if target == "" {
|
||||
return false
|
||||
}
|
||||
for _, item := range items {
|
||||
if strings.TrimSpace(item) == target {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func filterCandidatesByQuery(candidates []toolSearchCandidate, query string) []toolSearchCandidate {
|
||||
query = strings.TrimSpace(strings.ToLower(query))
|
||||
if query == "" {
|
||||
return candidates
|
||||
}
|
||||
ret := make([]toolSearchCandidate, 0, len(candidates))
|
||||
for _, item := range candidates {
|
||||
searchText := strings.ToLower(strings.Join([]string{
|
||||
item.ToolCode,
|
||||
item.ServerCode,
|
||||
item.ToolName,
|
||||
item.Title,
|
||||
item.Description,
|
||||
}, "\n"))
|
||||
if strings.Contains(searchText, query) {
|
||||
ret = append(ret, item)
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func cloneArguments(input map[string]any) map[string]any {
|
||||
if len(input) == 0 {
|
||||
return map[string]any{}
|
||||
}
|
||||
ret := make(map[string]any, len(input))
|
||||
for key, value := range input {
|
||||
ret[key] = value
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func buildToolCallResultSummary(result *mcps.ToolCallResult) string {
|
||||
return tooling.BuildReducedToolResultSummary(result)
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package tools
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestParseToolSearchRequest(t *testing.T) {
|
||||
req, err := parseToolSearchRequest(`{"query":" search docs ","toolCode":" mcp_server/search ","arguments":{"q":"hello"}}`)
|
||||
if err != nil {
|
||||
t.Fatalf("parseToolSearchRequest returned error: %v", err)
|
||||
}
|
||||
if req.Query != "search docs" {
|
||||
t.Fatalf("unexpected query: %q", req.Query)
|
||||
}
|
||||
if req.ToolCode != "mcp_server/search" {
|
||||
t.Fatalf("unexpected toolCode: %q", req.ToolCode)
|
||||
}
|
||||
if req.Arguments["q"] != "hello" {
|
||||
t.Fatalf("unexpected arguments: %#v", req.Arguments)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseToolSearchRequestDefaultsArguments(t *testing.T) {
|
||||
req, err := parseToolSearchRequest(`{"query":"list"}`)
|
||||
if err != nil {
|
||||
t.Fatalf("parseToolSearchRequest returned error: %v", err)
|
||||
}
|
||||
if req.Arguments == nil {
|
||||
t.Fatalf("expected non-nil arguments map")
|
||||
}
|
||||
if len(req.Arguments) != 0 {
|
||||
t.Fatalf("expected empty arguments map, got %#v", req.Arguments)
|
||||
}
|
||||
}
|
||||
@@ -62,28 +62,21 @@ func (t *TriageServiceRequestTool) Info(ctx context.Context) (*schema.ToolInfo,
|
||||
},
|
||||
},
|
||||
orderedmap.Pair[string, *einojsonschema.Schema]{
|
||||
Key: "observedIssue",
|
||||
Key: "observed_issue",
|
||||
Value: &einojsonschema.Schema{
|
||||
Type: "string",
|
||||
Description: i18nx.Get("tool.graph.triageServiceRequest.param.observedIssue"),
|
||||
},
|
||||
},
|
||||
orderedmap.Pair[string, *einojsonschema.Schema]{
|
||||
Key: "needTicket",
|
||||
Value: &einojsonschema.Schema{
|
||||
Type: "boolean",
|
||||
Description: i18nx.Get("tool.graph.triageServiceRequest.param.needTicket"),
|
||||
},
|
||||
},
|
||||
orderedmap.Pair[string, *einojsonschema.Schema]{
|
||||
Key: "needHumanHandoff",
|
||||
Key: "need_human_handoff",
|
||||
Value: &einojsonschema.Schema{
|
||||
Type: "boolean",
|
||||
Description: i18nx.Get("tool.graph.triageServiceRequest.param.needHumanHandoff"),
|
||||
},
|
||||
},
|
||||
orderedmap.Pair[string, *einojsonschema.Schema]{
|
||||
Key: "additionalContext",
|
||||
Key: "additional_context",
|
||||
Value: &einojsonschema.Schema{
|
||||
Type: "string",
|
||||
Description: i18nx.Get("tool.graph.triageServiceRequest.param.additionalContext"),
|
||||
@@ -92,8 +85,8 @@ func (t *TriageServiceRequestTool) Info(ctx context.Context) (*schema.ToolInfo,
|
||||
)),
|
||||
}),
|
||||
Extra: map[string]any{
|
||||
"toolCode": toolx.GraphTriageServiceRequest.Code,
|
||||
"sourceType": "graph",
|
||||
"tool_code": toolx.GraphTriageServiceRequest.Code,
|
||||
"source_type": "graph",
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user