Refactor skill handling and improve runtime trace capabilities
- Removed the selectSkill method from prepareService and adjusted related logic in the Run method of Service. - Updated tool catalog to parse agent allowed tool codes directly. - Simplified Request and RunInput structures by removing unnecessary fields. - Enhanced the RuntimeTraceCollector to manage skill activation and visibility. - Introduced a new databaseSkillBackend to manage skill definitions and their metadata. - Added tests for skill backend functionalities to ensure correct behavior. - Updated various factory methods to accommodate changes in skill handling. - Improved documentation and descriptions for better clarity.
This commit is contained in:
+1
-1
Submodule docs updated: 9731debd6a...b42fb50bd9
@@ -1,14 +1,5 @@
|
|||||||
package runtime
|
package runtime
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"cs-agent/internal/ai/skills"
|
|
||||||
"cs-agent/internal/models"
|
|
||||||
)
|
|
||||||
|
|
||||||
func newPrepareService(catalog *toolCatalog) *prepareService {
|
func newPrepareService(catalog *toolCatalog) *prepareService {
|
||||||
return &prepareService{catalog: catalog}
|
return &prepareService{catalog: catalog}
|
||||||
}
|
}
|
||||||
@@ -17,28 +8,6 @@ type prepareService struct {
|
|||||||
catalog *toolCatalog
|
catalog *toolCatalog
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *prepareService) selectSkill(ctx context.Context, req Request) (*models.SkillDefinition, string, string, error) {
|
|
||||||
result, err := skills.Select(ctx, skills.RuntimeContext{
|
|
||||||
AIAgent: req.AIAgent,
|
|
||||||
AIConfig: req.AIConfig,
|
|
||||||
UserMessage: strings.TrimSpace(req.UserMessage.Content),
|
|
||||||
ConversationID: req.Conversation.ID,
|
|
||||||
ManualSkillCode: strings.TrimSpace(req.ManualSkillCode),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, "", "", err
|
|
||||||
}
|
|
||||||
if result == nil || result.Plan == nil || result.Plan.Skill == nil {
|
|
||||||
traceData := marshalSkillRouteTrace(result)
|
|
||||||
reason := ""
|
|
||||||
if result != nil && result.Plan != nil {
|
|
||||||
reason = strings.TrimSpace(result.Plan.MatchReason)
|
|
||||||
}
|
|
||||||
return nil, reason, traceData, nil
|
|
||||||
}
|
|
||||||
return result.Plan.Skill, strings.TrimSpace(result.Plan.MatchReason), marshalSkillRouteTrace(result), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *prepareService) prepareToolsForRun(req *Request) error {
|
func (s *prepareService) prepareToolsForRun(req *Request) error {
|
||||||
if req == nil || req.ToolSet != nil || s.catalog == nil {
|
if req == nil || req.ToolSet != nil || s.catalog == nil {
|
||||||
return nil
|
return nil
|
||||||
@@ -66,22 +35,3 @@ func (s *prepareService) prepareToolsForResume(req *ResumeRequest) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func marshalSkillRouteTrace(result *skills.ExecutionResult) string {
|
|
||||||
if result == nil || result.Plan == nil || result.Plan.RouteTrace == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
buf, err := json.Marshal(result.Plan.RouteTrace)
|
|
||||||
if err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return string(buf)
|
|
||||||
}
|
|
||||||
|
|
||||||
func cloneSkillDefinition(item *models.SkillDefinition) *models.SkillDefinition {
|
|
||||||
if item == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
clone := *item
|
|
||||||
return &clone
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -27,13 +27,6 @@ func (s *Service) Run(ctx context.Context, req Request) (*Summary, error) {
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
req.UserMessage.Content = utils.BuildRuntimeMessageText(req.UserMessage.MessageType, req.UserMessage.Content)
|
req.UserMessage.Content = utils.BuildRuntimeMessageText(req.UserMessage.MessageType, req.UserMessage.Content)
|
||||||
selectedSkill, skillReason, skillTrace, skillErr := s.prepare.selectSkill(ctx, req)
|
|
||||||
req.SelectedSkill = selectedSkill
|
|
||||||
req.SkillRouteReason = skillReason
|
|
||||||
req.SkillRouteTrace = skillTrace
|
|
||||||
if req.SelectedSkill != nil {
|
|
||||||
req.SelectedSkill = cloneSkillDefinition(req.SelectedSkill)
|
|
||||||
}
|
|
||||||
if err := s.prepare.prepareToolsForRun(&req); err != nil {
|
if err := s.prepare.prepareToolsForRun(&req); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -42,24 +35,13 @@ func (s *Service) Run(ctx context.Context, req Request) (*Summary, error) {
|
|||||||
UserMessage: req.UserMessage,
|
UserMessage: req.UserMessage,
|
||||||
AIAgent: req.AIAgent,
|
AIAgent: req.AIAgent,
|
||||||
AIConfig: req.AIConfig,
|
AIConfig: req.AIConfig,
|
||||||
SelectedSkill: req.SelectedSkill,
|
|
||||||
SkillRouteReason: req.SkillRouteReason,
|
|
||||||
SkillRouteTrace: req.SkillRouteTrace,
|
|
||||||
CheckPointID: req.CheckPointID,
|
CheckPointID: req.CheckPointID,
|
||||||
ToolSet: req.ToolSet,
|
ToolSet: req.ToolSet,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ret := toSummary(summary)
|
return toSummary(summary), err
|
||||||
if ret != nil && skillErr != nil && ret.PlanReason == "" {
|
|
||||||
ret.PlanReason = "skill_failed_fallback_runtime"
|
|
||||||
}
|
}
|
||||||
return ret, err
|
return toSummary(summary), nil
|
||||||
}
|
|
||||||
ret := toSummary(summary)
|
|
||||||
if ret != nil && skillErr != nil && ret.PlanReason == "" {
|
|
||||||
ret.PlanReason = "skill_failed_fallback_runtime"
|
|
||||||
}
|
|
||||||
return ret, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) Resume(ctx context.Context, req ResumeRequest) (*Summary, error) {
|
func (s *Service) Resume(ctx context.Context, req ResumeRequest) (*Summary, error) {
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ func (c *toolCatalog) resolveForRun(req *Request) (*registry.ToolSet, error) {
|
|||||||
AIAgent: req.AIAgent,
|
AIAgent: req.AIAgent,
|
||||||
AIConfig: req.AIConfig,
|
AIConfig: req.AIConfig,
|
||||||
UserMessage: req.UserMessage,
|
UserMessage: req.UserMessage,
|
||||||
AllowedToolCodes: c.resolveAllowedToolCodes(req.AIAgent, req.SelectedSkill),
|
AllowedToolCodes: c.parseAgentAllowedToolCodes(req.AIAgent),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,10 +10,6 @@ type Request struct {
|
|||||||
UserMessage models.Message
|
UserMessage models.Message
|
||||||
AIAgent models.AIAgent
|
AIAgent models.AIAgent
|
||||||
AIConfig models.AIConfig
|
AIConfig models.AIConfig
|
||||||
ManualSkillCode string
|
|
||||||
SelectedSkill *models.SkillDefinition
|
|
||||||
SkillRouteReason string
|
|
||||||
SkillRouteTrace string
|
|
||||||
CheckPointID string
|
CheckPointID string
|
||||||
ToolSet *registry.ToolSet
|
ToolSet *registry.ToolSet
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,16 +47,11 @@ func DebugRunSkill(ctx context.Context, req request.SkillDebugRunRequest) (*resp
|
|||||||
UserMessage: message,
|
UserMessage: message,
|
||||||
AIAgent: *aiAgent,
|
AIAgent: *aiAgent,
|
||||||
AIConfig: *aiConfig,
|
AIConfig: *aiConfig,
|
||||||
ManualSkillCode: strings.TrimSpace(req.SkillCode),
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return buildSkillDebugRunResponse(req, summary, nil), err
|
return buildSkillDebugRunResponse(req, summary, nil), err
|
||||||
}
|
}
|
||||||
selectedSkill := svc.SkillDefinitionService.GetByCode(strings.TrimSpace(req.SkillCode))
|
return buildSkillDebugRunResponse(req, summary, nil), nil
|
||||||
if summary == nil || strings.TrimSpace(summary.PlannedSkillCode) == "" {
|
|
||||||
return nil, errorsx.InvalidParam("Skill 未命中")
|
|
||||||
}
|
|
||||||
return buildSkillDebugRunResponse(req, summary, selectedSkill), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func DebugResumeSkill(ctx context.Context, req request.SkillDebugResumeRequest) (*response.SkillDebugRunResponse, error) {
|
func DebugResumeSkill(ctx context.Context, req request.SkillDebugResumeRequest) (*response.SkillDebugRunResponse, error) {
|
||||||
|
|||||||
@@ -43,32 +43,18 @@ func (s *Service) ExecuteRun(ctx context.Context, req RunInput) (*RunResult, err
|
|||||||
summary.TraceData = collector.Marshal()
|
summary.TraceData = collector.Marshal()
|
||||||
return summary, err
|
return summary, err
|
||||||
}
|
}
|
||||||
tooling := prepareTooling(toolDefs, req.SelectedSkill, req.ToolSet, req.SelectedSkill != nil)
|
hasVisibleSkills := factory.HasVisibleSkills(req.AIAgent)
|
||||||
|
tooling := prepareTooling(toolDefs, nil, req.ToolSet, hasVisibleSkills)
|
||||||
summary.ToolCodes = append(summary.ToolCodes, tooling.toolCodes...)
|
summary.ToolCodes = append(summary.ToolCodes, tooling.toolCodes...)
|
||||||
collector.Data.Input.ToolCodes = append(collector.Data.Input.ToolCodes, summary.ToolCodes...)
|
collector.Data.Input.ToolCodes = append(collector.Data.Input.ToolCodes, summary.ToolCodes...)
|
||||||
collector.SetTooling(tooling.staticToolCodes, definitionToolCodes(tooling.definitions), len(tooling.definitions) > 0)
|
collector.SetTooling(tooling.staticToolCodes, definitionToolCodes(tooling.definitions), len(tooling.definitions) > 0)
|
||||||
|
|
||||||
collector.Data.Model.Provider = string(req.AIConfig.Provider)
|
collector.Data.Model.Provider = string(req.AIConfig.Provider)
|
||||||
collector.Data.Model.Name = req.AIConfig.ModelName
|
collector.Data.Model.Name = req.AIConfig.ModelName
|
||||||
summary.SelectedSkillCode = ""
|
|
||||||
summary.SelectedSkillName = ""
|
|
||||||
summary.SkillRouteReason = strings.TrimSpace(req.SkillRouteReason)
|
|
||||||
summary.SkillRouteTrace = strings.TrimSpace(req.SkillRouteTrace)
|
|
||||||
if req.SelectedSkill != nil {
|
|
||||||
summary.SelectedSkillCode = strings.TrimSpace(req.SelectedSkill.Code)
|
|
||||||
summary.SelectedSkillName = strings.TrimSpace(req.SelectedSkill.Name)
|
|
||||||
summary.SkillAllowedToolCodes = parseJSONArrayList(req.SelectedSkill.ToolWhitelist)
|
|
||||||
collector.Data.Skill.Code = summary.SelectedSkillCode
|
|
||||||
collector.Data.Skill.Name = summary.SelectedSkillName
|
|
||||||
collector.Data.Skill.AllowedToolCodes = append([]string(nil), summary.SkillAllowedToolCodes...)
|
|
||||||
}
|
|
||||||
collector.Data.Skill.RouteReason = summary.SkillRouteReason
|
|
||||||
collector.Data.Skill.RouteTrace = summary.SkillRouteTrace
|
|
||||||
|
|
||||||
agent, err := s.agentFactory.BuildCustomerServiceAgent(ctx, factory.BuildCustomerServiceAgentInput{
|
agent, err := s.agentFactory.BuildCustomerServiceAgent(ctx, factory.BuildCustomerServiceAgentInput{
|
||||||
AIAgent: req.AIAgent,
|
AIAgent: req.AIAgent,
|
||||||
AIConfig: req.AIConfig,
|
AIConfig: req.AIConfig,
|
||||||
SelectedSkill: req.SelectedSkill,
|
|
||||||
InstructionToolDefinitions: tooling.definitions,
|
InstructionToolDefinitions: tooling.definitions,
|
||||||
DynamicMCPToolDefinitions: tooling.definitions,
|
DynamicMCPToolDefinitions: tooling.definitions,
|
||||||
StaticTools: tooling.staticTools,
|
StaticTools: tooling.staticTools,
|
||||||
@@ -114,6 +100,7 @@ func (s *Service) ExecuteRun(ctx context.Context, req RunInput) (*RunResult, err
|
|||||||
collector.Data.Status = summary.Status
|
collector.Data.Status = summary.Status
|
||||||
collector.Data.Output.ReplyText = summary.ReplyText
|
collector.Data.Output.ReplyText = summary.ReplyText
|
||||||
collector.Data.Output.FinishReason = summary.Status
|
collector.Data.Output.FinishReason = summary.Status
|
||||||
|
syncSkillSummaryFromCollector(summary, collector)
|
||||||
summary.TraceData = collector.Marshal()
|
summary.TraceData = collector.Marshal()
|
||||||
return summary, nil
|
return summary, nil
|
||||||
}
|
}
|
||||||
@@ -149,7 +136,8 @@ func (s *Service) ExecuteResume(ctx context.Context, req ResumeInput) (*RunResul
|
|||||||
summary.TraceData = collector.Marshal()
|
summary.TraceData = collector.Marshal()
|
||||||
return summary, err
|
return summary, err
|
||||||
}
|
}
|
||||||
tooling := prepareTooling(toolDefs, nil, req.ToolSet, false)
|
hasVisibleSkills := factory.HasVisibleSkills(req.AIAgent)
|
||||||
|
tooling := prepareTooling(toolDefs, nil, req.ToolSet, hasVisibleSkills)
|
||||||
summary.ToolCodes = append(summary.ToolCodes, tooling.toolCodes...)
|
summary.ToolCodes = append(summary.ToolCodes, tooling.toolCodes...)
|
||||||
collector.Data.Input.ToolCodes = append(collector.Data.Input.ToolCodes, summary.ToolCodes...)
|
collector.Data.Input.ToolCodes = append(collector.Data.Input.ToolCodes, summary.ToolCodes...)
|
||||||
collector.SetTooling(tooling.staticToolCodes, definitionToolCodes(tooling.definitions), len(tooling.definitions) > 0)
|
collector.SetTooling(tooling.staticToolCodes, definitionToolCodes(tooling.definitions), len(tooling.definitions) > 0)
|
||||||
@@ -211,6 +199,19 @@ func (s *Service) ExecuteResume(ctx context.Context, req ResumeInput) (*RunResul
|
|||||||
collector.Data.Status = summary.Status
|
collector.Data.Status = summary.Status
|
||||||
collector.Data.Output.ReplyText = summary.ReplyText
|
collector.Data.Output.ReplyText = summary.ReplyText
|
||||||
collector.Data.Output.FinishReason = summary.Status
|
collector.Data.Output.FinishReason = summary.Status
|
||||||
|
syncSkillSummaryFromCollector(summary, collector)
|
||||||
summary.TraceData = collector.Marshal()
|
summary.TraceData = collector.Marshal()
|
||||||
return summary, nil
|
return summary, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func syncSkillSummaryFromCollector(summary *RunResult, collector *callbacks.RuntimeTraceCollector) {
|
||||||
|
if summary == nil || collector == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
trace := collector.Data.Skill
|
||||||
|
summary.SelectedSkillCode = strings.TrimSpace(trace.Code)
|
||||||
|
summary.SelectedSkillName = strings.TrimSpace(trace.Name)
|
||||||
|
summary.SkillRouteReason = strings.TrimSpace(trace.RouteReason)
|
||||||
|
summary.SkillRouteTrace = strings.TrimSpace(trace.RouteTrace)
|
||||||
|
summary.SkillAllowedToolCodes = append([]string(nil), trace.AllowedToolCodes...)
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,9 +10,6 @@ type RunInput struct {
|
|||||||
UserMessage models.Message
|
UserMessage models.Message
|
||||||
AIAgent models.AIAgent
|
AIAgent models.AIAgent
|
||||||
AIConfig models.AIConfig
|
AIConfig models.AIConfig
|
||||||
SelectedSkill *models.SkillDefinition
|
|
||||||
SkillRouteReason string
|
|
||||||
SkillRouteTrace string
|
|
||||||
CheckPointID string
|
CheckPointID string
|
||||||
ToolSet *registry.ToolSet
|
ToolSet *registry.ToolSet
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,10 @@ func BuildSelectedSkillActivationInstruction(skill *models.SkillDefinition) stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
func BuildSelectedSkillDocument(skill *models.SkillDefinition, toolDefinitions []runtimetooling.MCPToolDefinition) string {
|
func BuildSelectedSkillDocument(skill *models.SkillDefinition, toolDefinitions []runtimetooling.MCPToolDefinition) string {
|
||||||
|
return BuildSkillDocument(skill, toolDefinitions)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BuildSkillDocument(skill *models.SkillDefinition, toolDefinitions []runtimetooling.MCPToolDefinition) string {
|
||||||
if skill == nil {
|
if skill == nil {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ type RuntimeTraceHandler struct {
|
|||||||
*adk.BaseChatModelAgentMiddleware
|
*adk.BaseChatModelAgentMiddleware
|
||||||
collector *RuntimeTraceCollector
|
collector *RuntimeTraceCollector
|
||||||
toolMetadataBy map[string]ToolMetadata
|
toolMetadataBy map[string]ToolMetadata
|
||||||
|
skillMetadataBy map[string]SkillMetadata
|
||||||
}
|
}
|
||||||
|
|
||||||
type graphAnalyzeConversationResult struct {
|
type graphAnalyzeConversationResult struct {
|
||||||
@@ -65,11 +66,12 @@ type toolSearchSearchResult struct {
|
|||||||
Candidates []toolSearchCandidateResult `json:"candidates"`
|
Candidates []toolSearchCandidateResult `json:"candidates"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRuntimeTraceHandler(collector *RuntimeTraceCollector, toolMetadataBy map[string]ToolMetadata) *RuntimeTraceHandler {
|
func NewRuntimeTraceHandler(collector *RuntimeTraceCollector, toolMetadataBy map[string]ToolMetadata, skillMetadataBy map[string]SkillMetadata) *RuntimeTraceHandler {
|
||||||
return &RuntimeTraceHandler{
|
return &RuntimeTraceHandler{
|
||||||
BaseChatModelAgentMiddleware: &adk.BaseChatModelAgentMiddleware{},
|
BaseChatModelAgentMiddleware: &adk.BaseChatModelAgentMiddleware{},
|
||||||
collector: collector,
|
collector: collector,
|
||||||
toolMetadataBy: toolMetadataBy,
|
toolMetadataBy: toolMetadataBy,
|
||||||
|
skillMetadataBy: skillMetadataBy,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,10 +125,42 @@ func (h *RuntimeTraceHandler) WrapInvokableToolCall(_ context.Context, endpoint
|
|||||||
if metadata, ok := h.resolveToolMetadata(item.ToolName); ok && strings.TrimSpace(metadata.ToolCode) == toolx.BuiltinToolSearch.Code {
|
if metadata, ok := h.resolveToolMetadata(item.ToolName); ok && strings.TrimSpace(metadata.ToolCode) == toolx.BuiltinToolSearch.Code {
|
||||||
h.collector.AddToolSearchItem(h.buildToolSearchTraceItem(argumentsInJSON, result, err))
|
h.collector.AddToolSearchItem(h.buildToolSearchTraceItem(argumentsInJSON, result, err))
|
||||||
}
|
}
|
||||||
|
if metadata, ok := h.resolveToolMetadata(item.ToolName); ok && strings.TrimSpace(metadata.ToolCode) == toolx.BuiltinSkill.Code {
|
||||||
|
h.tryActivateSkill(argumentsInJSON)
|
||||||
|
}
|
||||||
return result, err
|
return result, err
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *RuntimeTraceHandler) tryActivateSkill(argumentsInJSON string) {
|
||||||
|
if h == nil || h.collector == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var args struct {
|
||||||
|
Skill string `json:"skill"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal([]byte(strings.TrimSpace(argumentsInJSON)), &args); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
code := strings.TrimSpace(args.Skill)
|
||||||
|
if code == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
meta, ok := h.skillMetadataBy[code]
|
||||||
|
if !ok {
|
||||||
|
meta = SkillMetadata{Code: code}
|
||||||
|
}
|
||||||
|
buf, err := json.Marshal(map[string]any{
|
||||||
|
"source": "eino_skill_tool",
|
||||||
|
"skill": code,
|
||||||
|
})
|
||||||
|
routeTrace := ""
|
||||||
|
if err == nil {
|
||||||
|
routeTrace = string(buf)
|
||||||
|
}
|
||||||
|
h.collector.ActivateSkill(meta, "eino_skill_tool", routeTrace)
|
||||||
|
}
|
||||||
|
|
||||||
func parseGraphToolOutcome(toolCode string, result string) (recommendedAction, riskLevel string, ticketDraftReady bool) {
|
func parseGraphToolOutcome(toolCode string, result string) (recommendedAction, riskLevel string, ticketDraftReady bool) {
|
||||||
toolCode = strings.TrimSpace(toolCode)
|
toolCode = strings.TrimSpace(toolCode)
|
||||||
if toolCode == "" || strings.TrimSpace(result) == "" {
|
if toolCode == "" || strings.TrimSpace(result) == "" {
|
||||||
|
|||||||
@@ -36,3 +36,32 @@ func TestExtractCandidateToolCodes(t *testing.T) {
|
|||||||
t.Fatalf("unexpected candidate codes: %#v", got)
|
t.Fatalf("unexpected candidate codes: %#v", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTryActivateSkill(t *testing.T) {
|
||||||
|
collector := NewRuntimeTraceCollector()
|
||||||
|
handler := &RuntimeTraceHandler{
|
||||||
|
collector: collector,
|
||||||
|
skillMetadataBy: map[string]SkillMetadata{
|
||||||
|
"after_sales_escalation_skill": {
|
||||||
|
Code: "after_sales_escalation_skill",
|
||||||
|
Name: "售后升级",
|
||||||
|
AllowedToolCodes: []string{"graph/handoff_to_human"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
handler.tryActivateSkill(`{"skill":"after_sales_escalation_skill"}`)
|
||||||
|
|
||||||
|
if collector.Data.Skill.Code != "after_sales_escalation_skill" {
|
||||||
|
t.Fatalf("unexpected skill code: %#v", collector.Data.Skill)
|
||||||
|
}
|
||||||
|
if collector.Data.Skill.Name != "售后升级" {
|
||||||
|
t.Fatalf("unexpected skill name: %#v", collector.Data.Skill)
|
||||||
|
}
|
||||||
|
if collector.Data.Skill.RouteReason != "eino_skill_tool" {
|
||||||
|
t.Fatalf("unexpected route reason: %#v", collector.Data.Skill)
|
||||||
|
}
|
||||||
|
if len(collector.Data.Skill.AllowedToolCodes) != 1 || collector.Data.Skill.AllowedToolCodes[0] != "graph/handoff_to_human" {
|
||||||
|
t.Fatalf("unexpected allowed tools: %#v", collector.Data.Skill.AllowedToolCodes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -63,6 +63,43 @@ func (c *RuntimeTraceCollector) SetSkillMiddleware(enabled bool, toolName string
|
|||||||
c.Data.Skill.MiddlewareToolName = toolName
|
c.Data.Skill.MiddlewareToolName = toolName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SkillMetadata struct {
|
||||||
|
Code string
|
||||||
|
Name string
|
||||||
|
Description string
|
||||||
|
AllowedToolCodes []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *RuntimeTraceCollector) SetVisibleSkills(skills map[string]SkillMetadata) {
|
||||||
|
if c == nil || len(skills) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
codes := make([]string, 0, len(skills))
|
||||||
|
for code := range skills {
|
||||||
|
if code == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
codes = append(codes, code)
|
||||||
|
}
|
||||||
|
c.Data.Skill.VisibleCodes = append([]string(nil), codes...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *RuntimeTraceCollector) ActivateSkill(skill SkillMetadata, routeReason string, routeTrace string) {
|
||||||
|
if c == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
c.Data.Skill.Code = skill.Code
|
||||||
|
c.Data.Skill.Name = skill.Name
|
||||||
|
c.Data.Skill.Description = skill.Description
|
||||||
|
c.Data.Skill.AllowedToolCodes = append([]string(nil), skill.AllowedToolCodes...)
|
||||||
|
c.Data.Skill.RouteReason = routeReason
|
||||||
|
c.Data.Skill.RouteTrace = routeTrace
|
||||||
|
}
|
||||||
|
|
||||||
func (c *RuntimeTraceCollector) SetRetrieverSummary(summary RetrieverTraceSummary) {
|
func (c *RuntimeTraceCollector) SetRetrieverSummary(summary RetrieverTraceSummary) {
|
||||||
if c == nil {
|
if c == nil {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -142,11 +142,13 @@ type RuntimeTraceData struct {
|
|||||||
type SkillTraceData struct {
|
type SkillTraceData struct {
|
||||||
Code string `json:"code,omitempty"`
|
Code string `json:"code,omitempty"`
|
||||||
Name string `json:"name,omitempty"`
|
Name string `json:"name,omitempty"`
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
RouteReason string `json:"routeReason,omitempty"`
|
RouteReason string `json:"routeReason,omitempty"`
|
||||||
RouteTrace string `json:"routeTrace,omitempty"`
|
RouteTrace string `json:"routeTrace,omitempty"`
|
||||||
AllowedToolCodes []string `json:"allowedToolCodes,omitempty"`
|
AllowedToolCodes []string `json:"allowedToolCodes,omitempty"`
|
||||||
MiddlewareEnabled bool `json:"middlewareEnabled,omitempty"`
|
MiddlewareEnabled bool `json:"middlewareEnabled,omitempty"`
|
||||||
MiddlewareToolName string `json:"middlewareToolName,omitempty"`
|
MiddlewareToolName string `json:"middlewareToolName,omitempty"`
|
||||||
|
VisibleCodes []string `json:"visibleCodes,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type InterruptTraceContext struct {
|
type InterruptTraceContext struct {
|
||||||
|
|||||||
@@ -34,8 +34,6 @@ type BuildCustomerServiceAgentInput struct {
|
|||||||
AIAgent models.AIAgent
|
AIAgent models.AIAgent
|
||||||
// AIConfig 为模型配置,决定底层使用哪个 ChatModel。
|
// AIConfig 为模型配置,决定底层使用哪个 ChatModel。
|
||||||
AIConfig models.AIConfig
|
AIConfig models.AIConfig
|
||||||
// SelectedSkill 为当前命中的技能;为空表示本次运行未命中专项技能。
|
|
||||||
SelectedSkill *models.SkillDefinition
|
|
||||||
// InstructionToolDefinitions 用于生成 instruction 中的工具说明。
|
// InstructionToolDefinitions 用于生成 instruction 中的工具说明。
|
||||||
// 它描述“当前允许模型理解和使用的 MCP 工具范围”。
|
// 它描述“当前允许模型理解和使用的 MCP 工具范围”。
|
||||||
InstructionToolDefinitions []tooling.MCPToolDefinition
|
InstructionToolDefinitions []tooling.MCPToolDefinition
|
||||||
@@ -73,11 +71,11 @@ func (f *AgentFactory) BuildCustomerServiceAgent(ctx context.Context, input Buil
|
|||||||
}
|
}
|
||||||
allTools := make([]tool.BaseTool, 0, len(input.StaticTools))
|
allTools := make([]tool.BaseTool, 0, len(input.StaticTools))
|
||||||
allTools = append(allTools, input.StaticTools...)
|
allTools = append(allTools, input.StaticTools...)
|
||||||
instructionResult := f.instructionService.Build(input.AIAgent, input.SelectedSkill, input.InstructionToolDefinitions, input.StaticToolCodes)
|
instructionResult := f.instructionService.Build(input.AIAgent, nil, input.InstructionToolDefinitions, input.StaticToolCodes)
|
||||||
handlers := make([]adk.ChatModelAgentMiddleware, 0, 3)
|
handlers := make([]adk.ChatModelAgentMiddleware, 0, 3)
|
||||||
if f.handlerService != nil {
|
if f.handlerService != nil {
|
||||||
builtHandlers, err := f.handlerService.Build(ctx, BuildAgentHandlersInput{
|
builtHandlers, err := f.handlerService.Build(ctx, BuildAgentHandlersInput{
|
||||||
SelectedSkill: input.SelectedSkill,
|
AIAgent: input.AIAgent,
|
||||||
InstructionToolDefinitions: input.InstructionToolDefinitions,
|
InstructionToolDefinitions: input.InstructionToolDefinitions,
|
||||||
DynamicToolDefinitions: input.DynamicMCPToolDefinitions,
|
DynamicToolDefinitions: input.DynamicMCPToolDefinitions,
|
||||||
DynamicTools: dynamicTools,
|
DynamicTools: dynamicTools,
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ type AgentHandlerService struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type BuildAgentHandlersInput struct {
|
type BuildAgentHandlersInput struct {
|
||||||
SelectedSkill *models.SkillDefinition
|
AIAgent models.AIAgent
|
||||||
InstructionToolDefinitions []runtimetooling.MCPToolDefinition
|
InstructionToolDefinitions []runtimetooling.MCPToolDefinition
|
||||||
DynamicToolDefinitions []runtimetooling.MCPToolDefinition
|
DynamicToolDefinitions []runtimetooling.MCPToolDefinition
|
||||||
DynamicTools []einobasetool.BaseTool
|
DynamicTools []einobasetool.BaseTool
|
||||||
@@ -46,20 +46,31 @@ func (s *AgentHandlerService) Build(ctx context.Context, input BuildAgentHandler
|
|||||||
}
|
}
|
||||||
handlers = append(handlers, toolSearchHandler)
|
handlers = append(handlers, toolSearchHandler)
|
||||||
}
|
}
|
||||||
if input.SelectedSkill != nil {
|
skillMetadataByCode := buildRuntimeSkillMetadataMap(input.AIAgent)
|
||||||
skillHandler, err := s.skillMiddleware.Build(ctx, input.SelectedSkill, input.InstructionToolDefinitions)
|
if len(skillMetadataByCode) > 0 {
|
||||||
|
skillHandler, err := s.skillMiddleware.Build(ctx, input.AIAgent, input.InstructionToolDefinitions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
handlers = append(handlers, skillHandler)
|
handlers = append(handlers, skillHandler)
|
||||||
}
|
}
|
||||||
if input.Collector != nil {
|
if input.Collector != nil {
|
||||||
toolMetadataBy := buildRuntimeTraceToolMetadata(input.DynamicToolDefinitions, input.StaticToolMetadata, input.SelectedSkill)
|
toolMetadataBy := buildRuntimeTraceToolMetadata(input.DynamicToolDefinitions, input.StaticToolMetadata, len(skillMetadataByCode) > 0)
|
||||||
if input.SelectedSkill != nil {
|
traceSkillMetadata := make(map[string]einocallbacks.SkillMetadata, len(skillMetadataByCode))
|
||||||
|
for code, item := range skillMetadataByCode {
|
||||||
|
traceSkillMetadata[code] = einocallbacks.SkillMetadata{
|
||||||
|
Code: item.Code,
|
||||||
|
Name: item.Name,
|
||||||
|
Description: item.Description,
|
||||||
|
AllowedToolCodes: append([]string(nil), item.AllowedToolCodes...),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(skillMetadataByCode) > 0 {
|
||||||
input.Collector.SetSkillMiddleware(true, toolx.BuiltinSkill.Name)
|
input.Collector.SetSkillMiddleware(true, toolx.BuiltinSkill.Name)
|
||||||
}
|
}
|
||||||
|
input.Collector.SetVisibleSkills(traceSkillMetadata)
|
||||||
input.Collector.SetInstructionSummary(input.InstructionSummary)
|
input.Collector.SetInstructionSummary(input.InstructionSummary)
|
||||||
handlers = append(handlers, einocallbacks.NewRuntimeTraceHandler(input.Collector, toolMetadataBy))
|
handlers = append(handlers, einocallbacks.NewRuntimeTraceHandler(input.Collector, toolMetadataBy, traceSkillMetadata))
|
||||||
}
|
}
|
||||||
return handlers, nil
|
return handlers, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,60 +2,186 @@ package factory
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
runtimeinstruction "cs-agent/internal/ai/runtime/instruction"
|
runtimeinstruction "cs-agent/internal/ai/runtime/instruction"
|
||||||
runtimetooling "cs-agent/internal/ai/runtime/tooling"
|
runtimetooling "cs-agent/internal/ai/runtime/tooling"
|
||||||
"cs-agent/internal/models"
|
"cs-agent/internal/models"
|
||||||
|
"cs-agent/internal/pkg/enums"
|
||||||
|
"cs-agent/internal/pkg/utils"
|
||||||
|
"cs-agent/internal/services"
|
||||||
|
|
||||||
einoskill "github.com/cloudwego/eino/adk/middlewares/skill"
|
einoskill "github.com/cloudwego/eino/adk/middlewares/skill"
|
||||||
)
|
)
|
||||||
|
|
||||||
type selectedSkillBackend struct {
|
type runtimeSkillMetadata struct {
|
||||||
frontMatter einoskill.FrontMatter
|
Code string
|
||||||
skill einoskill.Skill
|
Name string
|
||||||
|
Description string
|
||||||
|
AllowedToolCodes []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func newSelectedSkillBackend(selectedSkill *models.SkillDefinition, toolDefinitions []runtimetooling.MCPToolDefinition) (*selectedSkillBackend, error) {
|
type databaseSkillBackend struct {
|
||||||
if selectedSkill == nil {
|
toolDefinitions []runtimetooling.MCPToolDefinition
|
||||||
return nil, fmt.Errorf("selected skill is nil")
|
skillsByCode map[string]models.SkillDefinition
|
||||||
|
order []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDatabaseSkillBackend(aiAgent models.AIAgent, toolDefinitions []runtimetooling.MCPToolDefinition) (*databaseSkillBackend, error) {
|
||||||
|
visibleSkills := loadVisibleSkills(aiAgent)
|
||||||
|
if len(visibleSkills) == 0 {
|
||||||
|
return nil, fmt.Errorf("no visible skills available")
|
||||||
}
|
}
|
||||||
skillName := strings.TrimSpace(selectedSkill.Code)
|
ret := &databaseSkillBackend{
|
||||||
if skillName == "" {
|
toolDefinitions: append([]runtimetooling.MCPToolDefinition(nil), toolDefinitions...),
|
||||||
return nil, fmt.Errorf("selected skill code is empty")
|
skillsByCode: make(map[string]models.SkillDefinition, len(visibleSkills)),
|
||||||
|
order: make([]string, 0, len(visibleSkills)),
|
||||||
}
|
}
|
||||||
description := strings.TrimSpace(selectedSkill.Description)
|
for _, item := range visibleSkills {
|
||||||
content := runtimeinstruction.BuildSelectedSkillDocument(selectedSkill, toolDefinitions)
|
code := strings.TrimSpace(item.Code)
|
||||||
return &selectedSkillBackend{
|
if code == "" {
|
||||||
frontMatter: einoskill.FrontMatter{
|
continue
|
||||||
Name: skillName,
|
}
|
||||||
Description: description,
|
ret.skillsByCode[code] = item
|
||||||
},
|
ret.order = append(ret.order, code)
|
||||||
skill: einoskill.Skill{
|
}
|
||||||
|
if len(ret.skillsByCode) == 0 {
|
||||||
|
return nil, fmt.Errorf("no visible skills available")
|
||||||
|
}
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *databaseSkillBackend) List(_ context.Context) ([]einoskill.FrontMatter, error) {
|
||||||
|
if b == nil || len(b.order) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
ret := make([]einoskill.FrontMatter, 0, len(b.order))
|
||||||
|
for _, code := range b.order {
|
||||||
|
item, ok := b.skillsByCode[code]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ret = append(ret, einoskill.FrontMatter{
|
||||||
|
Name: strings.TrimSpace(item.Code),
|
||||||
|
Description: skillListDescription(item),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *databaseSkillBackend) Get(_ context.Context, name string) (einoskill.Skill, error) {
|
||||||
|
if b == nil {
|
||||||
|
return einoskill.Skill{}, fmt.Errorf("database skill backend is nil")
|
||||||
|
}
|
||||||
|
name = strings.TrimSpace(name)
|
||||||
|
if name == "" {
|
||||||
|
return einoskill.Skill{}, fmt.Errorf("skill name is empty")
|
||||||
|
}
|
||||||
|
item, ok := b.skillsByCode[name]
|
||||||
|
if !ok {
|
||||||
|
return einoskill.Skill{}, fmt.Errorf("skill %q not found", name)
|
||||||
|
}
|
||||||
|
return einoskill.Skill{
|
||||||
FrontMatter: einoskill.FrontMatter{
|
FrontMatter: einoskill.FrontMatter{
|
||||||
Name: skillName,
|
Name: strings.TrimSpace(item.Code),
|
||||||
Description: description,
|
Description: skillListDescription(item),
|
||||||
},
|
|
||||||
Content: content,
|
|
||||||
},
|
},
|
||||||
|
Content: runtimeinstruction.BuildSkillDocument(&item, filterSkillToolDefinitions(b.toolDefinitions, &item)),
|
||||||
|
BaseDirectory: "",
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *selectedSkillBackend) List(_ context.Context) ([]einoskill.FrontMatter, error) {
|
func loadVisibleSkills(aiAgent models.AIAgent) []models.SkillDefinition {
|
||||||
if b == nil {
|
ids := utils.SplitInt64s(strings.TrimSpace(aiAgent.SkillIDs))
|
||||||
return nil, nil
|
if len(ids) == 0 {
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
return []einoskill.FrontMatter{b.frontMatter}, nil
|
byID := services.SkillDefinitionService.GetByIDs(ids)
|
||||||
|
if len(byID) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ret := make([]models.SkillDefinition, 0, len(ids))
|
||||||
|
for _, id := range ids {
|
||||||
|
item, ok := byID[id]
|
||||||
|
if !ok || item.Status != enums.StatusOk || strings.TrimSpace(item.Code) == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ret = append(ret, item)
|
||||||
|
}
|
||||||
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *selectedSkillBackend) Get(_ context.Context, name string) (einoskill.Skill, error) {
|
func buildRuntimeSkillMetadataMap(aiAgent models.AIAgent) map[string]runtimeSkillMetadata {
|
||||||
if b == nil {
|
visibleSkills := loadVisibleSkills(aiAgent)
|
||||||
return einoskill.Skill{}, fmt.Errorf("selected skill backend is nil")
|
if len(visibleSkills) == 0 {
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
name = strings.TrimSpace(name)
|
ret := make(map[string]runtimeSkillMetadata, len(visibleSkills))
|
||||||
if name == "" || strings.EqualFold(name, b.frontMatter.Name) {
|
for _, item := range visibleSkills {
|
||||||
return b.skill, nil
|
code := strings.TrimSpace(item.Code)
|
||||||
|
if code == "" {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
return einoskill.Skill{}, fmt.Errorf("skill %q not found", name)
|
ret[code] = runtimeSkillMetadata{
|
||||||
|
Code: code,
|
||||||
|
Name: strings.TrimSpace(item.Name),
|
||||||
|
Description: skillListDescription(item),
|
||||||
|
AllowedToolCodes: parseSkillToolWhitelist(item.ToolWhitelist),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func HasVisibleSkills(aiAgent models.AIAgent) bool {
|
||||||
|
return len(buildRuntimeSkillMetadataMap(aiAgent)) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func skillListDescription(item models.SkillDefinition) string {
|
||||||
|
if desc := strings.TrimSpace(item.Description); desc != "" {
|
||||||
|
return desc
|
||||||
|
}
|
||||||
|
if name := strings.TrimSpace(item.Name); name != "" {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(item.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseSkillToolWhitelist(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 filterSkillToolDefinitions(defs []runtimetooling.MCPToolDefinition, skill *models.SkillDefinition) []runtimetooling.MCPToolDefinition {
|
||||||
|
allowed := parseSkillToolWhitelist(skill.ToolWhitelist)
|
||||||
|
if len(allowed) == 0 {
|
||||||
|
return defs
|
||||||
|
}
|
||||||
|
allowedSet := make(map[string]struct{}, len(allowed))
|
||||||
|
for _, item := range allowed {
|
||||||
|
allowedSet[item] = struct{}{}
|
||||||
|
}
|
||||||
|
ret := make([]runtimetooling.MCPToolDefinition, 0, len(defs))
|
||||||
|
for _, item := range defs {
|
||||||
|
if _, ok := allowedSet[strings.TrimSpace(item.ToolCode)]; ok {
|
||||||
|
ret = append(ret, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,128 @@
|
|||||||
|
package factory
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
runtimetooling "cs-agent/internal/ai/runtime/tooling"
|
||||||
|
"cs-agent/internal/models"
|
||||||
|
"cs-agent/internal/pkg/enums"
|
||||||
|
|
||||||
|
"github.com/glebarez/sqlite"
|
||||||
|
"github.com/mlogclub/simple/sqls"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDatabaseSkillBackendListAndGet(t *testing.T) {
|
||||||
|
setupSkillBackendTestDB(t)
|
||||||
|
createSkillDefinitionForTest(t, models.SkillDefinition{
|
||||||
|
ID: 1,
|
||||||
|
Code: "after_sales_escalation_skill",
|
||||||
|
Name: "售后升级",
|
||||||
|
Description: "处理转人工和升级诉求",
|
||||||
|
Instruction: "请优先判断是否需要转人工。",
|
||||||
|
ToolWhitelist: `["graph/handoff_to_human"]`,
|
||||||
|
Status: enums.StatusOk,
|
||||||
|
})
|
||||||
|
createSkillDefinitionForTest(t, models.SkillDefinition{
|
||||||
|
ID: 2,
|
||||||
|
Code: "disabled_skill",
|
||||||
|
Name: "禁用技能",
|
||||||
|
Description: "不会被暴露",
|
||||||
|
Instruction: "noop",
|
||||||
|
Status: enums.StatusDeleted,
|
||||||
|
})
|
||||||
|
|
||||||
|
backend, err := newDatabaseSkillBackend(models.AIAgent{SkillIDs: "1,2"}, []runtimetooling.MCPToolDefinition{
|
||||||
|
{ToolCode: "graph/handoff_to_human", Title: "转人工确认流程"},
|
||||||
|
{ToolCode: "graph/prepare_ticket_draft", Title: "整理工单草稿"},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("newDatabaseSkillBackend returned error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
matters, err := backend.List(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("List returned error: %v", err)
|
||||||
|
}
|
||||||
|
if len(matters) != 1 || matters[0].Name != "after_sales_escalation_skill" {
|
||||||
|
t.Fatalf("unexpected matters: %#v", matters)
|
||||||
|
}
|
||||||
|
|
||||||
|
skill, err := backend.Get(context.Background(), "after_sales_escalation_skill")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Get returned error: %v", err)
|
||||||
|
}
|
||||||
|
if skill.Name != "after_sales_escalation_skill" {
|
||||||
|
t.Fatalf("unexpected skill name: %#v", skill)
|
||||||
|
}
|
||||||
|
if skill.Content == "" || !containsAll(skill.Content, "处理转人工和升级诉求", "graph/handoff_to_human") {
|
||||||
|
t.Fatalf("unexpected skill content: %q", skill.Content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHasVisibleSkills(t *testing.T) {
|
||||||
|
setupSkillBackendTestDB(t)
|
||||||
|
createSkillDefinitionForTest(t, models.SkillDefinition{
|
||||||
|
ID: 3,
|
||||||
|
Code: "enabled_skill",
|
||||||
|
Name: "启用技能",
|
||||||
|
Description: "可见",
|
||||||
|
Instruction: "noop",
|
||||||
|
Status: enums.StatusOk,
|
||||||
|
})
|
||||||
|
createSkillDefinitionForTest(t, models.SkillDefinition{
|
||||||
|
ID: 4,
|
||||||
|
Code: "deleted_skill",
|
||||||
|
Name: "删除技能",
|
||||||
|
Description: "不可见",
|
||||||
|
Instruction: "noop",
|
||||||
|
Status: enums.StatusDeleted,
|
||||||
|
})
|
||||||
|
if !HasVisibleSkills(models.AIAgent{SkillIDs: "3,4"}) {
|
||||||
|
t.Fatalf("expected visible skills")
|
||||||
|
}
|
||||||
|
if HasVisibleSkills(models.AIAgent{SkillIDs: "4"}) {
|
||||||
|
t.Fatalf("expected no visible skills")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupSkillBackendTestDB(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
db, err := gorm.Open(sqlite.Open("file:skill_backend_test?mode=memory&cache=shared"), &gorm.Config{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("open sqlite failed: %v", err)
|
||||||
|
}
|
||||||
|
if err := db.AutoMigrate(&models.SkillDefinition{}); err != nil {
|
||||||
|
t.Fatalf("auto migrate skill definition failed: %v", err)
|
||||||
|
}
|
||||||
|
if err := db.Exec("DELETE FROM skill_definitions").Error; err != nil {
|
||||||
|
t.Fatalf("cleanup skill definitions failed: %v", err)
|
||||||
|
}
|
||||||
|
sqls.SetDB(db)
|
||||||
|
}
|
||||||
|
|
||||||
|
func createSkillDefinitionForTest(t *testing.T, item models.SkillDefinition) {
|
||||||
|
t.Helper()
|
||||||
|
now := time.Now()
|
||||||
|
if item.CreatedAt.IsZero() {
|
||||||
|
item.CreatedAt = now
|
||||||
|
}
|
||||||
|
if item.UpdatedAt.IsZero() {
|
||||||
|
item.UpdatedAt = now
|
||||||
|
}
|
||||||
|
if err := sqls.DB().Create(&item).Error; err != nil {
|
||||||
|
t.Fatalf("create skill definition failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func containsAll(text string, items ...string) bool {
|
||||||
|
for _, item := range items {
|
||||||
|
if item != "" && !strings.Contains(text, item) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
@@ -19,10 +19,10 @@ func NewSkillMiddlewareService() *SkillMiddlewareService {
|
|||||||
|
|
||||||
func (s *SkillMiddlewareService) Build(
|
func (s *SkillMiddlewareService) Build(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
selectedSkill *models.SkillDefinition,
|
aiAgent models.AIAgent,
|
||||||
toolDefinitions []runtimetooling.MCPToolDefinition,
|
toolDefinitions []runtimetooling.MCPToolDefinition,
|
||||||
) (adk.ChatModelAgentMiddleware, error) {
|
) (adk.ChatModelAgentMiddleware, error) {
|
||||||
backend, err := newSelectedSkillBackend(selectedSkill, toolDefinitions)
|
backend, err := newDatabaseSkillBackend(aiAgent, toolDefinitions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
einocallbacks "cs-agent/internal/ai/runtime/internal/impl/callbacks"
|
einocallbacks "cs-agent/internal/ai/runtime/internal/impl/callbacks"
|
||||||
"cs-agent/internal/ai/runtime/registry"
|
"cs-agent/internal/ai/runtime/registry"
|
||||||
runtimetooling "cs-agent/internal/ai/runtime/tooling"
|
runtimetooling "cs-agent/internal/ai/runtime/tooling"
|
||||||
"cs-agent/internal/models"
|
|
||||||
"cs-agent/internal/pkg/enums"
|
"cs-agent/internal/pkg/enums"
|
||||||
"cs-agent/internal/pkg/toolx"
|
"cs-agent/internal/pkg/toolx"
|
||||||
)
|
)
|
||||||
@@ -24,7 +23,7 @@ func buildInstructionTraceSummary(summary runtimeinstruction.AssemblySummary) ei
|
|||||||
func buildRuntimeTraceToolMetadata(
|
func buildRuntimeTraceToolMetadata(
|
||||||
dynamicToolDefinitions []runtimetooling.MCPToolDefinition,
|
dynamicToolDefinitions []runtimetooling.MCPToolDefinition,
|
||||||
staticToolMetadata map[string]registry.ToolMetadata,
|
staticToolMetadata map[string]registry.ToolMetadata,
|
||||||
selectedSkill *models.SkillDefinition,
|
includeSkillTool bool,
|
||||||
) map[string]einocallbacks.ToolMetadata {
|
) map[string]einocallbacks.ToolMetadata {
|
||||||
ret := make(map[string]einocallbacks.ToolMetadata, len(dynamicToolDefinitions)+len(staticToolMetadata)+1)
|
ret := make(map[string]einocallbacks.ToolMetadata, len(dynamicToolDefinitions)+len(staticToolMetadata)+1)
|
||||||
for _, item := range dynamicToolDefinitions {
|
for _, item := range dynamicToolDefinitions {
|
||||||
@@ -54,7 +53,7 @@ func buildRuntimeTraceToolMetadata(
|
|||||||
SourceType: metadata.SourceType,
|
SourceType: metadata.SourceType,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if selectedSkill != nil {
|
if includeSkillTool {
|
||||||
resolved := toolx.ResolveToolMetadata(toolx.BuiltinSkill.Code, toolx.BuiltinSkill.Name)
|
resolved := toolx.ResolveToolMetadata(toolx.BuiltinSkill.Code, toolx.BuiltinSkill.Name)
|
||||||
ret[toolx.BuiltinSkill.Name] = einocallbacks.ToolMetadata{
|
ret[toolx.BuiltinSkill.Name] = einocallbacks.ToolMetadata{
|
||||||
ToolCode: resolved.ToolCode,
|
ToolCode: resolved.ToolCode,
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ var (
|
|||||||
ServerCode: "builtin",
|
ServerCode: "builtin",
|
||||||
Name: "skill",
|
Name: "skill",
|
||||||
Title: "加载专项技能说明",
|
Title: "加载专项技能说明",
|
||||||
Description: "用于加载当前命中的专项技能说明文档。仅在本轮已命中 Skill 时可用,适合将专项处理规则按需注入上下文。",
|
Description: "用于按需加载当前 Agent 可用的专项技能说明文档,适合在需要专项处理规则时再注入上下文。",
|
||||||
SourceType: enums.ToolSourceTypeBuiltin,
|
SourceType: enums.ToolSourceTypeBuiltin,
|
||||||
AutoInjected: true,
|
AutoInjected: true,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,6 +79,10 @@ func (s *skillDefinitionService) GetByCode(code string) *models.SkillDefinition
|
|||||||
return repositories.SkillDefinitionRepository.GetByCode(sqls.DB(), code)
|
return repositories.SkillDefinitionRepository.GetByCode(sqls.DB(), code)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *skillDefinitionService) GetByIDs(ids []int64) map[int64]models.SkillDefinition {
|
||||||
|
return repositories.SkillDefinitionRepository.GetByIDs(sqls.DB(), ids)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *skillDefinitionService) CreateSkillDefinition(req request.CreateSkillDefinitionRequest, operator *dto.AuthPrincipal) (*models.SkillDefinition, error) {
|
func (s *skillDefinitionService) CreateSkillDefinition(req request.CreateSkillDefinitionRequest, operator *dto.AuthPrincipal) (*models.SkillDefinition, error) {
|
||||||
if operator == nil {
|
if operator == nil {
|
||||||
return nil, errorsx.Unauthorized("未登录或登录已过期")
|
return nil, errorsx.Unauthorized("未登录或登录已过期")
|
||||||
|
|||||||
Reference in New Issue
Block a user