Refactor AI Agent and AI Config handling across multiple files

- Updated function signatures to accept AI Agent and AI Config as non-pointer types for better clarity and safety.
- Modified instances where AI Agent and AI Config were dereferenced to improve code readability.
- Removed unnecessary nil checks for AI Agent and AI Config, simplifying the logic.
- Adjusted related tests and services to align with the new function signatures.
- Cleaned up code in runtime, skills, and executor packages to ensure consistency in handling AI configurations.
This commit is contained in:
mlogclub
2026-04-17 17:57:01 +08:00
parent 3b062c327c
commit 976b9defde
36 changed files with 102 additions and 282 deletions
@@ -18,7 +18,7 @@ type prepareService struct {
} }
func (s *prepareService) selectSkill(ctx context.Context, req Request) (*models.SkillDefinition, string, string, error) { func (s *prepareService) selectSkill(ctx context.Context, req Request) (*models.SkillDefinition, string, string, error) {
if req.AIAgent == nil || req.AIConfig == nil || req.UserMessage == nil || req.Conversation == nil { if req.UserMessage == nil || req.Conversation == nil {
return nil, "", "", nil return nil, "", "", nil
} }
result, err := skills.Select(ctx, skills.RuntimeContext{ result, err := skills.Select(ctx, skills.RuntimeContext{
@@ -72,10 +72,7 @@ func (c *toolCatalog) parseSkillAllowedToolCodes(skill *models.SkillDefinition)
return toolx.NormalizeToolCodes(items) return toolx.NormalizeToolCodes(items)
} }
func (c *toolCatalog) parseAgentAllowedToolCodes(aiAgent *models.AIAgent) []string { func (c *toolCatalog) parseAgentAllowedToolCodes(aiAgent models.AIAgent) []string {
if aiAgent == nil {
return nil
}
ret := make([]string, 0) ret := make([]string, 0)
if raw := strings.TrimSpace(aiAgent.AllowedMCPTools); raw != "" { if raw := strings.TrimSpace(aiAgent.AllowedMCPTools); raw != "" {
items, err := toolx.ParseAgentMCPToolsJSON(raw) items, err := toolx.ParseAgentMCPToolsJSON(raw)
@@ -94,6 +91,6 @@ func (c *toolCatalog) parseAgentAllowedToolCodes(aiAgent *models.AIAgent) []stri
return toolx.NormalizeToolCodes(ret) return toolx.NormalizeToolCodes(ret)
} }
func (c *toolCatalog) resolveAllowedToolCodes(aiAgent *models.AIAgent, skill *models.SkillDefinition) []string { func (c *toolCatalog) resolveAllowedToolCodes(aiAgent models.AIAgent, skill *models.SkillDefinition) []string {
return toolx.IntersectToolCodes(c.parseAgentAllowedToolCodes(aiAgent), c.parseSkillAllowedToolCodes(skill)) return toolx.IntersectToolCodes(c.parseAgentAllowedToolCodes(aiAgent), c.parseSkillAllowedToolCodes(skill))
} }
@@ -28,7 +28,7 @@ func TestNormalizeAllowedToolCodes(t *testing.T) {
func TestToolCatalogResolveAllowedToolCodes(t *testing.T) { func TestToolCatalogResolveAllowedToolCodes(t *testing.T) {
catalog := newToolCatalog() catalog := newToolCatalog()
agent := &models.AIAgent{ agent := models.AIAgent{
AllowedMCPTools: `[{"toolCode":"graph/create_ticket_with_confirmation"},{"toolCode":"graph/handoff_to_human"}]`, AllowedMCPTools: `[{"toolCode":"graph/create_ticket_with_confirmation"},{"toolCode":"graph/handoff_to_human"}]`,
} }
skill := &models.SkillDefinition{ skill := &models.SkillDefinition{
@@ -45,7 +45,7 @@ func TestToolCatalogResolveAllowedToolCodes(t *testing.T) {
func TestToolCatalogResolveAllowedToolCodesFallsBackWhenSkillEmpty(t *testing.T) { func TestToolCatalogResolveAllowedToolCodesFallsBackWhenSkillEmpty(t *testing.T) {
catalog := newToolCatalog() catalog := newToolCatalog()
agent := &models.AIAgent{ agent := models.AIAgent{
AllowedMCPTools: `[{"toolCode":"graph/create_ticket_with_confirmation"},{"toolCode":"graph/handoff_to_human"}]`, AllowedMCPTools: `[{"toolCode":"graph/create_ticket_with_confirmation"},{"toolCode":"graph/handoff_to_human"}]`,
} }
ret := catalog.resolveAllowedToolCodes(agent, nil) ret := catalog.resolveAllowedToolCodes(agent, nil)
+4 -4
View File
@@ -8,8 +8,8 @@ import (
type Request struct { type Request struct {
Conversation *models.Conversation Conversation *models.Conversation
UserMessage *models.Message UserMessage *models.Message
AIAgent *models.AIAgent AIAgent models.AIAgent
AIConfig *models.AIConfig AIConfig models.AIConfig
ManualSkillCode string ManualSkillCode string
SelectedSkill *models.SkillDefinition SelectedSkill *models.SkillDefinition
SkillRouteReason string SkillRouteReason string
@@ -20,8 +20,8 @@ type Request struct {
type ResumeRequest struct { type ResumeRequest struct {
Conversation *models.Conversation Conversation *models.Conversation
AIAgent *models.AIAgent AIAgent models.AIAgent
AIConfig *models.AIConfig AIConfig models.AIConfig
CheckPointID string CheckPointID string
ResumeData map[string]string ResumeData map[string]string
ToolSet *registry.ToolSet ToolSet *registry.ToolSet
+1 -1
View File
@@ -65,7 +65,7 @@ func (s *embedding) callEmbeddingAPI(ctx context.Context, text string) (*Embeddi
if err != nil { if err != nil {
return nil, err return nil, err
} }
client := newOpenAIClient(config) client := newOpenAIClient(*config)
embeddingResp, err := client.Embeddings.New(ctx, openai.EmbeddingNewParams{ embeddingResp, err := client.Embeddings.New(ctx, openai.EmbeddingNewParams{
Input: openai.EmbeddingNewParamsInputUnion{ Input: openai.EmbeddingNewParamsInputUnion{
OfString: openai.String(text), OfString: openai.String(text),
+5 -15
View File
@@ -30,14 +30,10 @@ func (s *llm) Chat(ctx context.Context, systemPrompt string, userPrompt string)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return s.ChatWithConfig(ctx, config, systemPrompt, userPrompt) return s.ChatWithConfig(ctx, *config, systemPrompt, userPrompt)
} }
func (s *llm) ChatWithConfig(ctx context.Context, config *models.AIConfig, systemPrompt string, userPrompt string) (*ChatCompletionResult, error) { func (s *llm) ChatWithConfig(ctx context.Context, config models.AIConfig, systemPrompt string, userPrompt string) (*ChatCompletionResult, error) {
if config == nil {
return nil, fmt.Errorf("ai config is nil")
}
messages := make([]openai.ChatCompletionMessageParamUnion, 0, 2) messages := make([]openai.ChatCompletionMessageParamUnion, 0, 2)
if strs.IsNotBlank(systemPrompt) { if strs.IsNotBlank(systemPrompt) {
messages = append(messages, openai.ChatCompletionMessageParamUnion{ messages = append(messages, openai.ChatCompletionMessageParamUnion{
@@ -63,7 +59,7 @@ func (s *llm) ChatWithConfig(ctx context.Context, config *models.AIConfig, syste
if config.MaxOutputTokens > 0 { if config.MaxOutputTokens > 0 {
params.MaxCompletionTokens = openai.Int(int64(config.MaxOutputTokens)) params.MaxCompletionTokens = openai.Int(int64(config.MaxOutputTokens))
} }
applyProviderSpecificChatParams(&params, config) applyProviderSpecificChatParams(params, config)
client := newOpenAIClient(config) client := newOpenAIClient(config)
chatResp, err := client.Chat.Completions.New(ctx, params) chatResp, err := client.Chat.Completions.New(ctx, params)
@@ -84,10 +80,7 @@ func (s *llm) ChatWithConfig(ctx context.Context, config *models.AIConfig, syste
}, nil }, nil
} }
func applyProviderSpecificChatParams(params *openai.ChatCompletionNewParams, config *models.AIConfig) { func applyProviderSpecificChatParams(params openai.ChatCompletionNewParams, config models.AIConfig) {
if params == nil || config == nil {
return
}
if isDashScopeQwenThinkingModel(config) { if isDashScopeQwenThinkingModel(config) {
params.SetExtraFields(map[string]any{ params.SetExtraFields(map[string]any{
"enable_thinking": false, "enable_thinking": false,
@@ -95,10 +88,7 @@ func applyProviderSpecificChatParams(params *openai.ChatCompletionNewParams, con
} }
} }
func isDashScopeQwenThinkingModel(config *models.AIConfig) bool { func isDashScopeQwenThinkingModel(config models.AIConfig) bool {
if config == nil {
return false
}
baseURL := strings.ToLower(strings.TrimSpace(config.BaseURL)) baseURL := strings.ToLower(strings.TrimSpace(config.BaseURL))
modelName := strings.ToLower(strings.TrimSpace(config.ModelName)) modelName := strings.ToLower(strings.TrimSpace(config.ModelName))
return strings.Contains(baseURL, "dashscope.aliyuncs.com") && strings.HasPrefix(modelName, "qwen3") return strings.Contains(baseURL, "dashscope.aliyuncs.com") && strings.HasPrefix(modelName, "qwen3")
+1 -1
View File
@@ -13,7 +13,7 @@ import (
"cs-agent/internal/repositories" "cs-agent/internal/repositories"
) )
func newOpenAIClient(config *models.AIConfig) openai.Client { func newOpenAIClient(config models.AIConfig) openai.Client {
opts := []option.RequestOption{ opts := []option.RequestOption{
option.WithAPIKey(config.APIKey), option.WithAPIKey(config.APIKey),
option.WithBaseURL(config.BaseURL), option.WithBaseURL(config.BaseURL),
+4 -4
View File
@@ -44,8 +44,8 @@ func DebugRunSkill(ctx context.Context, req request.SkillDebugRunRequest) (*resp
summary, err := Service.Run(ctx, applicationruntime.Request{ summary, err := Service.Run(ctx, applicationruntime.Request{
Conversation: conversation, Conversation: conversation,
UserMessage: message, UserMessage: message,
AIAgent: aiAgent, AIAgent: *aiAgent,
AIConfig: aiConfig, AIConfig: *aiConfig,
ManualSkillCode: strings.TrimSpace(req.SkillCode), ManualSkillCode: strings.TrimSpace(req.SkillCode),
}) })
if err != nil { if err != nil {
@@ -91,8 +91,8 @@ func DebugResumeSkill(ctx context.Context, req request.SkillDebugResumeRequest)
resumeText := strings.TrimSpace(req.UserMessage) resumeText := strings.TrimSpace(req.UserMessage)
summary, err := Service.Resume(ctx, applicationruntime.ResumeRequest{ summary, err := Service.Resume(ctx, applicationruntime.ResumeRequest{
Conversation: conversation, Conversation: conversation,
AIAgent: aiAgent, AIAgent: *aiAgent,
AIConfig: aiConfig, AIConfig: *aiConfig,
CheckPointID: strings.TrimSpace(req.CheckPointID), CheckPointID: strings.TrimSpace(req.CheckPointID),
ResumeData: map[string]string{ ResumeData: map[string]string{
strings.TrimSpace(pendingInterrupt.InterruptID): resumeText, strings.TrimSpace(pendingInterrupt.InterruptID): resumeText,
@@ -22,9 +22,7 @@ func buildRunMessages(ctx context.Context, req RunInput, summary *RunResult, col
} }
if collector != nil { if collector != nil {
collector.Data.Input.HistoryMessageCount = len(history.Messages) collector.Data.Input.HistoryMessageCount = len(history.Messages)
if req.AIAgent != nil {
collector.Data.Input.KnowledgeBaseIDs = utils.SplitInt64s(req.AIAgent.KnowledgeIDs) collector.Data.Input.KnowledgeBaseIDs = utils.SplitInt64s(req.AIAgent.KnowledgeIDs)
}
collector.Data.Input.CurrentUserMessagePreview = preview(req.UserMessage.Content, 120) collector.Data.Input.CurrentUserMessagePreview = preview(req.UserMessage.Content, 120)
} }
messages := make([]*schema.Message, 0, len(history.Messages)+3) messages := make([]*schema.Message, 0, len(history.Messages)+3)
@@ -41,7 +39,7 @@ func buildRunMessages(ctx context.Context, req RunInput, summary *RunResult, col
} }
func appendRetrievedContext(ctx context.Context, req RunInput, summary *RunResult, collector *callbacks.RuntimeTraceCollector, messages *[]*schema.Message) knowledgeGuardDecision { func appendRetrievedContext(ctx context.Context, req RunInput, summary *RunResult, collector *callbacks.RuntimeTraceCollector, messages *[]*schema.Message) knowledgeGuardDecision {
if req.AIAgent == nil || req.UserMessage == nil || messages == nil { if req.UserMessage == nil || messages == nil {
return knowledgeGuardDecision{} return knowledgeGuardDecision{}
} }
retriever := retrievers.NewKnowledgeRetriever(req.AIAgent) retriever := retrievers.NewKnowledgeRetriever(req.AIAgent)
@@ -15,8 +15,8 @@ type knowledgeGuardDecision struct {
Instructions []*schema.Message Instructions []*schema.Message
} }
func buildKnowledgeGuardDecision(aiAgent *models.AIAgent, retrieveResult *retrievers.KnowledgeRetrieveResult) knowledgeGuardDecision { func buildKnowledgeGuardDecision(aiAgent models.AIAgent, retrieveResult *retrievers.KnowledgeRetrieveResult) knowledgeGuardDecision {
if aiAgent == nil || retrieveResult == nil || len(retrieveResult.KnowledgeBaseIDs) == 0 { if retrieveResult == nil || len(retrieveResult.KnowledgeBaseIDs) == 0 {
return knowledgeGuardDecision{} return knowledgeGuardDecision{}
} }
fallbackReply := resolveKnowledgeFallbackReply(aiAgent, retrieveResult.FallbackMode) fallbackReply := resolveKnowledgeFallbackReply(aiAgent, retrieveResult.FallbackMode)
@@ -32,12 +32,10 @@ func buildKnowledgeGuardDecision(aiAgent *models.AIAgent, retrieveResult *retrie
} }
} }
func resolveKnowledgeFallbackReply(aiAgent *models.AIAgent, fallbackMode enums.KnowledgeFallbackMode) string { func resolveKnowledgeFallbackReply(aiAgent models.AIAgent, fallbackMode enums.KnowledgeFallbackMode) string {
if aiAgent != nil {
if reply := strings.TrimSpace(aiAgent.FallbackMessage); reply != "" { if reply := strings.TrimSpace(aiAgent.FallbackMessage); reply != "" {
return reply return reply
} }
}
switch fallbackMode { switch fallbackMode {
case enums.KnowledgeFallbackModeSuggestRetry: case enums.KnowledgeFallbackModeSuggestRetry:
return "当前知识库里没有找到足够明确的信息,你可以换个更具体的问法再试一次。" return "当前知识库里没有找到足够明确的信息,你可以换个更具体的问法再试一次。"
@@ -12,7 +12,7 @@ import (
func TestBuildKnowledgeGuardDecisionFallsBackWhenKnowledgeMisses(t *testing.T) { func TestBuildKnowledgeGuardDecisionFallsBackWhenKnowledgeMisses(t *testing.T) {
agent := newKnowledgeGuardAgentFixture() agent := newKnowledgeGuardAgentFixture()
decision := buildKnowledgeGuardDecision(&agent, &retrievers.KnowledgeRetrieveResult{ decision := buildKnowledgeGuardDecision(agent, &retrievers.KnowledgeRetrieveResult{
KnowledgeBaseIDs: []int64{1}, KnowledgeBaseIDs: []int64{1},
FallbackMode: enums.KnowledgeFallbackModeSuggestRetry, FallbackMode: enums.KnowledgeFallbackModeSuggestRetry,
}) })
@@ -28,7 +28,7 @@ func TestBuildKnowledgeGuardDecisionFallsBackWhenKnowledgeMisses(t *testing.T) {
func TestBuildKnowledgeGuardDecisionUsesAgentFallbackMessage(t *testing.T) { func TestBuildKnowledgeGuardDecisionUsesAgentFallbackMessage(t *testing.T) {
agent := newKnowledgeGuardAgentFixture() agent := newKnowledgeGuardAgentFixture()
agent.FallbackMessage = "请联系人工客服" agent.FallbackMessage = "请联系人工客服"
decision := buildKnowledgeGuardDecision(&agent, &retrievers.KnowledgeRetrieveResult{ decision := buildKnowledgeGuardDecision(agent, &retrievers.KnowledgeRetrieveResult{
KnowledgeBaseIDs: []int64{1}, KnowledgeBaseIDs: []int64{1},
FallbackMode: enums.KnowledgeFallbackModeNoAnswer, FallbackMode: enums.KnowledgeFallbackModeNoAnswer,
}) })
@@ -40,7 +40,7 @@ func TestBuildKnowledgeGuardDecisionUsesAgentFallbackMessage(t *testing.T) {
func TestBuildKnowledgeGuardDecisionInjectsStrictInstructionOnHit(t *testing.T) { func TestBuildKnowledgeGuardDecisionInjectsStrictInstructionOnHit(t *testing.T) {
agent := newKnowledgeGuardAgentFixture() agent := newKnowledgeGuardAgentFixture()
decision := buildKnowledgeGuardDecision(&agent, &retrievers.KnowledgeRetrieveResult{ decision := buildKnowledgeGuardDecision(agent, &retrievers.KnowledgeRetrieveResult{
KnowledgeBaseIDs: []int64{1}, KnowledgeBaseIDs: []int64{1},
Hits: []rag.RetrieveResult{ Hits: []rag.RetrieveResult{
{KnowledgeBaseID: 1, Score: 0.88}, {KnowledgeBaseID: 1, Score: 0.88},
+1 -28
View File
@@ -33,7 +33,7 @@ func (s *Service) ExecuteRun(ctx context.Context, req RunInput) (*RunResult, err
} }
collector := callbacks.NewRuntimeTraceCollector() collector := callbacks.NewRuntimeTraceCollector()
collector.Data.RunID = summary.RunID collector.Data.RunID = summary.RunID
if req.AIAgent == nil || req.Conversation == nil || req.UserMessage == nil { if req.Conversation == nil || req.UserMessage == nil {
summary.Status = "error" summary.Status = "error"
summary.ErrorMessage = "invalid runtime request" summary.ErrorMessage = "invalid runtime request"
collector.Data.Status = summary.Status collector.Data.Status = summary.Status
@@ -42,15 +42,6 @@ func (s *Service) ExecuteRun(ctx context.Context, req RunInput) (*RunResult, err
summary.TraceData = collector.Marshal() summary.TraceData = collector.Marshal()
return summary, fmt.Errorf("%s", summary.ErrorMessage) return summary, fmt.Errorf("%s", summary.ErrorMessage)
} }
if req.AIConfig == nil {
summary.Status = "error"
summary.ErrorMessage = "ai config is nil"
collector.Data.Status = summary.Status
collector.Data.Error.Message = summary.ErrorMessage
collector.Data.Error.Stage = "prepare"
summary.TraceData = collector.Marshal()
return summary, fmt.Errorf("%s", summary.ErrorMessage)
}
toolDefs, err := factory.NewToolFactory().BuildMCPTools(req.AIAgent) toolDefs, err := factory.NewToolFactory().BuildMCPTools(req.AIAgent)
if err != nil { if err != nil {
@@ -149,24 +140,6 @@ func (s *Service) ExecuteResume(ctx context.Context, req ResumeInput) (*RunResul
collector := callbacks.NewRuntimeTraceCollector() collector := callbacks.NewRuntimeTraceCollector()
collector.Data.RunID = summary.RunID collector.Data.RunID = summary.RunID
collector.Data.Interrupt.CheckPointID = summary.CheckPointID collector.Data.Interrupt.CheckPointID = summary.CheckPointID
if req.AIAgent == nil {
summary.Status = "error"
summary.ErrorMessage = "ai agent is nil"
collector.Data.Status = summary.Status
collector.Data.Error.Message = summary.ErrorMessage
collector.Data.Error.Stage = "resume_prepare"
summary.TraceData = collector.Marshal()
return summary, fmt.Errorf("%s", summary.ErrorMessage)
}
if req.AIConfig == nil {
summary.Status = "error"
summary.ErrorMessage = "ai config is nil"
collector.Data.Status = summary.Status
collector.Data.Error.Message = summary.ErrorMessage
collector.Data.Error.Stage = "resume_prepare"
summary.TraceData = collector.Marshal()
return summary, fmt.Errorf("%s", summary.ErrorMessage)
}
if summary.CheckPointID == "" { if summary.CheckPointID == "" {
summary.Status = "error" summary.Status = "error"
summary.ErrorMessage = "checkpoint id is required" summary.ErrorMessage = "checkpoint id is required"
+4 -4
View File
@@ -8,8 +8,8 @@ import (
type RunInput struct { type RunInput struct {
Conversation *models.Conversation Conversation *models.Conversation
UserMessage *models.Message UserMessage *models.Message
AIAgent *models.AIAgent AIAgent models.AIAgent
AIConfig *models.AIConfig AIConfig models.AIConfig
SelectedSkill *models.SkillDefinition SelectedSkill *models.SkillDefinition
SkillRouteReason string SkillRouteReason string
SkillRouteTrace string SkillRouteTrace string
@@ -19,8 +19,8 @@ type RunInput struct {
type ResumeInput struct { type ResumeInput struct {
Conversation *models.Conversation Conversation *models.Conversation
AIAgent *models.AIAgent AIAgent models.AIAgent
AIConfig *models.AIConfig AIConfig models.AIConfig
CheckPointID string CheckPointID string
ResumeData map[string]string ResumeData map[string]string
ToolSet *registry.ToolSet ToolSet *registry.ToolSet
@@ -38,10 +38,10 @@ func init() {
type CreateTicketGraph struct { type CreateTicketGraph struct {
conversation *models.Conversation conversation *models.Conversation
aiAgent *models.AIAgent aiAgent models.AIAgent
} }
func NewCreateTicketGraph(conversation *models.Conversation, aiAgent *models.AIAgent) *CreateTicketGraph { func NewCreateTicketGraph(conversation *models.Conversation, aiAgent models.AIAgent) *CreateTicketGraph {
return &CreateTicketGraph{ return &CreateTicketGraph{
conversation: conversation, conversation: conversation,
aiAgent: aiAgent, aiAgent: aiAgent,
@@ -49,7 +49,7 @@ func NewCreateTicketGraph(conversation *models.Conversation, aiAgent *models.AIA
} }
func (g *CreateTicketGraph) Run(ctx context.Context, argumentsInJSON string) (string, error) { func (g *CreateTicketGraph) Run(ctx context.Context, argumentsInJSON string) (string, error) {
if g == nil || g.conversation == nil || g.aiAgent == nil { if g == nil || g.conversation == nil {
return "", fmt.Errorf("create ticket graph not initialized") return "", fmt.Errorf("create ticket graph not initialized")
} }
wasInterrupted, hasState, state := componenttool.GetInterruptState[CreateTicketGraphState](ctx) wasInterrupted, hasState, state := componenttool.GetInterruptState[CreateTicketGraphState](ctx)
@@ -1,45 +0,0 @@
package graphs
import (
"testing"
"cs-agent/internal/models"
)
func TestCreateTicketGraphBuildCreateRequest(t *testing.T) {
graph := NewCreateTicketGraph(&models.Conversation{
ID: 12,
Subject: "fallback-title",
LastMessageSummary: "fallback-description",
}, &models.AIAgent{Name: "AI"})
req, err := graph.buildCreateRequest(`{"title":" test title ","description":" desc ","priority":2,"severity":3}`)
if err != nil {
t.Fatalf("buildCreateRequest returned error: %v", err)
}
if req.Title != "test title" || req.Description != "desc" {
t.Fatalf("unexpected request text fields: %#v", req)
}
if req.Priority != 2 || req.Severity != 3 {
t.Fatalf("unexpected request numeric fields: %#v", req)
}
}
func TestCreateTicketGraphBuildCreateRequestFallbacks(t *testing.T) {
graph := NewCreateTicketGraph(&models.Conversation{
ID: 12,
Subject: "fallback-title",
LastMessageSummary: "fallback-description",
}, &models.AIAgent{Name: "AI"})
req, err := graph.buildCreateRequest(`{}`)
if err != nil {
t.Fatalf("buildCreateRequest returned error: %v", err)
}
if req.Title != "fallback-title" {
t.Fatalf("unexpected fallback title: %#v", req)
}
if req.Description != "fallback-description" {
t.Fatalf("unexpected fallback description: %#v", req)
}
}
+3 -3
View File
@@ -33,10 +33,10 @@ func init() {
type HandoffGraph struct { type HandoffGraph struct {
conversation *models.Conversation conversation *models.Conversation
aiAgent *models.AIAgent aiAgent models.AIAgent
} }
func NewHandoffGraph(conversation *models.Conversation, aiAgent *models.AIAgent) *HandoffGraph { func NewHandoffGraph(conversation *models.Conversation, aiAgent models.AIAgent) *HandoffGraph {
return &HandoffGraph{ return &HandoffGraph{
conversation: conversation, conversation: conversation,
aiAgent: aiAgent, aiAgent: aiAgent,
@@ -44,7 +44,7 @@ func NewHandoffGraph(conversation *models.Conversation, aiAgent *models.AIAgent)
} }
func (g *HandoffGraph) Run(ctx context.Context, argumentsInJSON string) (string, error) { func (g *HandoffGraph) Run(ctx context.Context, argumentsInJSON string) (string, error) {
if g == nil || g.conversation == nil || g.aiAgent == nil { if g == nil || g.conversation == nil {
return "", fmt.Errorf("handoff graph not initialized") return "", fmt.Errorf("handoff graph not initialized")
} }
wasInterrupted, hasState, state := componenttool.GetInterruptState[HandoffGraphState](ctx) wasInterrupted, hasState, state := componenttool.GetInterruptState[HandoffGraphState](ctx)
@@ -1,41 +0,0 @@
package graphs
import (
"testing"
"cs-agent/internal/models"
)
func TestHandoffGraphBuildReason(t *testing.T) {
graph := NewHandoffGraph(&models.Conversation{ID: 1}, &models.AIAgent{Name: "AI"})
reason, err := graph.buildReason(`{"reason":" 用户需要人工确认 "}`)
if err != nil {
t.Fatalf("buildReason returned error: %v", err)
}
if reason != "用户需要人工确认" {
t.Fatalf("unexpected reason: %q", reason)
}
}
func TestHandoffGraphBuildReasonFallback(t *testing.T) {
graph := NewHandoffGraph(&models.Conversation{ID: 1}, &models.AIAgent{Name: "AI"})
reason, err := graph.buildReason(`{}`)
if err != nil {
t.Fatalf("buildReason returned error: %v", err)
}
if reason != "用户需要转人工支持" {
t.Fatalf("unexpected fallback reason: %q", reason)
}
}
func TestHandoffGraphBuildSuccessReply(t *testing.T) {
graph := NewHandoffGraph(&models.Conversation{ID: 1}, &models.AIAgent{Name: "AI"})
got := graph.buildSuccessReply()
want := "已为你转接人工客服,请稍候。,请稍候。"
if got != want {
t.Fatalf("unexpected success reply: %q", got)
}
}
+2 -6
View File
@@ -47,15 +47,11 @@ func NewService(
} }
func (s *Service) Build( func (s *Service) Build(
aiAgent *models.AIAgent, aiAgent models.AIAgent,
selectedSkill *models.SkillDefinition, selectedSkill *models.SkillDefinition,
toolDefinitions []runtimetooling.MCPToolDefinition, toolDefinitions []runtimetooling.MCPToolDefinition,
extraToolCodes map[string]string, extraToolCodes map[string]string,
) AssemblyResult { ) AssemblyResult {
baseInstruction := ""
if aiAgent != nil {
baseInstruction = strings.TrimSpace(aiAgent.SystemPrompt)
}
projectInstruction := "" projectInstruction := ""
governanceInstruction := "" governanceInstruction := ""
skillInstruction := "" skillInstruction := ""
@@ -77,7 +73,7 @@ func (s *Service) Build(
assembler = s.assembler assembler = s.assembler
} }
return assembler.Assemble(AssemblerInput{ return assembler.Assemble(AssemblerInput{
AgentInstruction: baseInstruction, AgentInstruction: strings.TrimSpace(aiAgent.SystemPrompt),
GovernanceInstruction: governanceInstruction, GovernanceInstruction: governanceInstruction,
SkillInstruction: skillInstruction, SkillInstruction: skillInstruction,
ToolAppendices: toolAppendices, ToolAppendices: toolAppendices,
@@ -31,9 +31,9 @@ type AgentFactory struct {
// 3. 后续扩展装配项时继续拉长函数签名。 // 3. 后续扩展装配项时继续拉长函数签名。
type BuildCustomerServiceAgentInput struct { type BuildCustomerServiceAgentInput struct {
// AIAgent 为当前运行的业务 Agent 配置,提供名称、描述、系统提示词等基础信息。 // AIAgent 为当前运行的业务 Agent 配置,提供名称、描述、系统提示词等基础信息。
AIAgent *models.AIAgent AIAgent models.AIAgent
// AIConfig 为模型配置,决定底层使用哪个 ChatModel。 // AIConfig 为模型配置,决定底层使用哪个 ChatModel。
AIConfig *models.AIConfig AIConfig models.AIConfig
// SelectedSkill 为当前命中的技能;为空表示本次运行未命中专项技能。 // SelectedSkill 为当前命中的技能;为空表示本次运行未命中专项技能。
SelectedSkill *models.SkillDefinition SelectedSkill *models.SkillDefinition
// InstructionToolDefinitions 用于生成 instruction 中的工具说明。 // InstructionToolDefinitions 用于生成 instruction 中的工具说明。
@@ -63,9 +63,6 @@ func NewAgentFactory() *AgentFactory {
// BuildCustomerServiceAgent 根据装配输入构建客服 ChatModelAgent。 // BuildCustomerServiceAgent 根据装配输入构建客服 ChatModelAgent。
func (f *AgentFactory) BuildCustomerServiceAgent(ctx context.Context, input BuildCustomerServiceAgentInput) (*einoagents.CustomerServiceAgent, error) { func (f *AgentFactory) BuildCustomerServiceAgent(ctx context.Context, input BuildCustomerServiceAgentInput) (*einoagents.CustomerServiceAgent, error) {
if input.AIAgent == nil || input.AIConfig == nil {
return nil, nil
}
chatModel, err := f.chatModelFactory.Build(ctx, input.AIConfig) chatModel, err := f.chatModelFactory.Build(ctx, input.AIConfig)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -18,27 +18,24 @@ func NewChatModelFactory() *ChatModelFactory {
return &ChatModelFactory{} return &ChatModelFactory{}
} }
func (f *ChatModelFactory) Build(ctx context.Context, item *models.AIConfig) (model.ToolCallingChatModel, error) { func (f *ChatModelFactory) Build(ctx context.Context, aiConfig models.AIConfig) (model.ToolCallingChatModel, error) {
if item == nil {
return nil, nil
}
conf := &openai.ChatModelConfig{ conf := &openai.ChatModelConfig{
APIKey: strings.TrimSpace(item.APIKey), APIKey: strings.TrimSpace(aiConfig.APIKey),
BaseURL: strings.TrimSpace(item.BaseURL), BaseURL: strings.TrimSpace(aiConfig.BaseURL),
Model: strings.TrimSpace(item.ModelName), Model: strings.TrimSpace(aiConfig.ModelName),
} }
if item.TimeoutMS > 0 { if aiConfig.TimeoutMS > 0 {
conf.Timeout = time.Duration(item.TimeoutMS) * time.Millisecond conf.Timeout = time.Duration(aiConfig.TimeoutMS) * time.Millisecond
} }
if item.MaxOutputTokens > 0 { if aiConfig.MaxOutputTokens > 0 {
maxCompletionTokens := item.MaxOutputTokens maxCompletionTokens := aiConfig.MaxOutputTokens
conf.MaxCompletionTokens = &maxCompletionTokens conf.MaxCompletionTokens = &maxCompletionTokens
} }
if item.Provider == enums.AIProviderOpenAI && isAzureOpenAIBaseURL(item.BaseURL) { if aiConfig.Provider == enums.AIProviderOpenAI && isAzureOpenAIBaseURL(aiConfig.BaseURL) {
conf.ByAzure = true conf.ByAzure = true
conf.APIVersion = "2024-06-01" conf.APIVersion = "2024-06-01"
} }
if extraFields := providerExtraFields(item); len(extraFields) > 0 { if extraFields := providerExtraFields(aiConfig); len(extraFields) > 0 {
conf.ExtraFields = extraFields conf.ExtraFields = extraFields
} }
return openai.NewChatModel(ctx, conf) return openai.NewChatModel(ctx, conf)
@@ -49,12 +46,9 @@ func isAzureOpenAIBaseURL(baseURL string) bool {
return strings.Contains(baseURL, ".openai.azure.com") return strings.Contains(baseURL, ".openai.azure.com")
} }
func providerExtraFields(item *models.AIConfig) map[string]any { func providerExtraFields(aiConfig models.AIConfig) map[string]any {
if item == nil { baseURL := strings.ToLower(strings.TrimSpace(aiConfig.BaseURL))
return nil modelName := strings.ToLower(strings.TrimSpace(aiConfig.ModelName))
}
baseURL := strings.ToLower(strings.TrimSpace(item.BaseURL))
modelName := strings.ToLower(strings.TrimSpace(item.ModelName))
if strings.Contains(baseURL, "dashscope.aliyuncs.com") && strings.HasPrefix(modelName, "qwen3") { if strings.Contains(baseURL, "dashscope.aliyuncs.com") && strings.HasPrefix(modelName, "qwen3") {
return map[string]any{ return map[string]any{
"enable_thinking": false, "enable_thinking": false,
@@ -20,10 +20,7 @@ func NewToolFactory() *ToolFactory {
return &ToolFactory{} return &ToolFactory{}
} }
func (f *ToolFactory) BuildMCPTools(aiAgent *models.AIAgent) ([]runtimetooling.MCPToolDefinition, error) { func (f *ToolFactory) BuildMCPTools(aiAgent models.AIAgent) ([]runtimetooling.MCPToolDefinition, error) {
if aiAgent == nil || strings.TrimSpace(aiAgent.AllowedMCPTools) == "" {
return nil, nil
}
raw, err := toolx.ParseAgentMCPToolsJSON(aiAgent.AllowedMCPTools) raw, err := toolx.ParseAgentMCPToolsJSON(aiAgent.AllowedMCPTools)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -56,7 +53,7 @@ func (f *ToolFactory) BuildMCPTools(aiAgent *models.AIAgent) ([]runtimetooling.M
return ret, nil return ret, nil
} }
func (f *ToolFactory) BuildBaseTools(ctx context.Context, aiAgent *models.AIAgent) ([]einotool.BaseTool, error) { func (f *ToolFactory) BuildBaseTools(ctx context.Context, aiAgent models.AIAgent) ([]einotool.BaseTool, error) {
definitions, err := f.BuildMCPTools(aiAgent) definitions, err := f.BuildMCPTools(aiAgent)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -7,7 +7,7 @@ import (
) )
func TestBuildMCPToolsSkipsGraphAndBuiltinTools(t *testing.T) { func TestBuildMCPToolsSkipsGraphAndBuiltinTools(t *testing.T) {
aiAgent := &models.AIAgent{ aiAgent := models.AIAgent{
AllowedMCPTools: `[ AllowedMCPTools: `[
{"toolCode":"graph/create_ticket_with_confirmation","serverCode":"graph","toolName":"create_ticket_with_confirmation"}, {"toolCode":"graph/create_ticket_with_confirmation","serverCode":"graph","toolName":"create_ticket_with_confirmation"},
{"toolCode":"builtin/tool_search","serverCode":"builtin","toolName":"tool_search"}, {"toolCode":"builtin/tool_search","serverCode":"builtin","toolName":"tool_search"},
@@ -20,7 +20,7 @@ const defaultRuntimeKnowledgeScoreThreshold = 0.3
const defaultRuntimeKnowledgeMaxContextItems = 5 const defaultRuntimeKnowledgeMaxContextItems = 5
type KnowledgeRetriever struct { type KnowledgeRetriever struct {
AIAgent *models.AIAgent AIAgent models.AIAgent
} }
type KnowledgeRetrieveOptions struct { type KnowledgeRetrieveOptions struct {
@@ -53,7 +53,7 @@ type KnowledgeRetrieveResult struct {
Policies []KnowledgeBaseRetrievePolicy Policies []KnowledgeBaseRetrievePolicy
} }
func NewKnowledgeRetriever(aiAgent *models.AIAgent) *KnowledgeRetriever { func NewKnowledgeRetriever(aiAgent models.AIAgent) *KnowledgeRetriever {
return &KnowledgeRetriever{AIAgent: aiAgent} return &KnowledgeRetriever{AIAgent: aiAgent}
} }
@@ -65,9 +65,6 @@ func DefaultKnowledgeRetrieveOptions() KnowledgeRetrieveOptions {
} }
func (r *KnowledgeRetriever) KnowledgeBaseIDs() []int64 { func (r *KnowledgeRetriever) KnowledgeBaseIDs() []int64 {
if r == nil || r.AIAgent == nil {
return nil
}
return utils.SplitInt64s(r.AIAgent.KnowledgeIDs) return utils.SplitInt64s(r.AIAgent.KnowledgeIDs)
} }
@@ -50,7 +50,7 @@ func TestResolveBuildsStaticToolMetadata(t *testing.T) {
}) })
toolSet, err := r.Resolve(registry.Context{ toolSet, err := r.Resolve(registry.Context{
Conversation: &models.Conversation{ID: 1}, Conversation: &models.Conversation{ID: 1},
AIAgent: &models.AIAgent{ID: 1}, AIAgent: models.AIAgent{ID: 1},
}) })
if err != nil { if err != nil {
t.Fatalf("resolve returned error: %v", err) t.Fatalf("resolve returned error: %v", err)
+2 -2
View File
@@ -10,8 +10,8 @@ import (
type Context struct { type Context struct {
Conversation *models.Conversation Conversation *models.Conversation
AIAgent *models.AIAgent AIAgent models.AIAgent
AIConfig *models.AIConfig AIConfig models.AIConfig
UserMessage *models.Message UserMessage *models.Message
AllowedToolCodes []string AllowedToolCodes []string
} }
@@ -28,8 +28,8 @@ func (e *runtimeReplyExecutor) Run(ctx context.Context, conversation models.Conv
summary, err := Service.Run(ctx, applicationruntime.Request{ summary, err := Service.Run(ctx, applicationruntime.Request{
Conversation: &conversation, Conversation: &conversation,
UserMessage: &message, UserMessage: &message,
AIAgent: &aiAgent, AIAgent: aiAgent,
AIConfig: aiConfig, AIConfig: *aiConfig,
}) })
if trace != nil { if trace != nil {
trace.RuntimeLatencyMs = time.Since(runtimeStartedAt).Milliseconds() trace.RuntimeLatencyMs = time.Since(runtimeStartedAt).Milliseconds()
@@ -52,8 +52,8 @@ func (e *runtimeReplyExecutor) ResumePendingInterrupt(ctx context.Context, conve
} }
summary, err := Service.Resume(ctx, applicationruntime.ResumeRequest{ summary, err := Service.Resume(ctx, applicationruntime.ResumeRequest{
Conversation: &conversation, Conversation: &conversation,
AIAgent: &aiAgent, AIAgent: aiAgent,
AIConfig: aiConfig, AIConfig: *aiConfig,
CheckPointID: strings.TrimSpace(pendingInterrupt.CheckPointID), CheckPointID: strings.TrimSpace(pendingInterrupt.CheckPointID),
ResumeData: map[string]string{ ResumeData: map[string]string{
strings.TrimSpace(pendingInterrupt.InterruptID): strings.TrimSpace(message.Content), strings.TrimSpace(pendingInterrupt.InterruptID): strings.TrimSpace(message.Content),
@@ -17,7 +17,7 @@ import (
type CreateTicketGraphTool struct { type CreateTicketGraphTool struct {
conversation *models.Conversation conversation *models.Conversation
aiAgent *models.AIAgent aiAgent models.AIAgent
} }
func NewCreateTicketGraphTool() *CreateTicketGraphTool { func NewCreateTicketGraphTool() *CreateTicketGraphTool {
@@ -37,7 +37,7 @@ func (t *CreateTicketGraphTool) Code() string {
} }
func (t *CreateTicketGraphTool) Enabled(ctx registry.Context) bool { func (t *CreateTicketGraphTool) Enabled(ctx registry.Context) bool {
return ctx.Conversation != nil && ctx.AIAgent != nil return true
} }
func (t *CreateTicketGraphTool) Build(ctx registry.Context) (einotool.BaseTool, error) { func (t *CreateTicketGraphTool) Build(ctx registry.Context) (einotool.BaseTool, error) {
@@ -100,7 +100,7 @@ func (t *CreateTicketGraphTool) Info(ctx context.Context) (*schema.ToolInfo, err
} }
func (t *CreateTicketGraphTool) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...einotool.Option) (string, error) { func (t *CreateTicketGraphTool) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...einotool.Option) (string, error) {
if t == nil || t.conversation == nil || t.aiAgent == nil { if t == nil || t.conversation == nil {
return "", fmt.Errorf("create ticket graph tool not initialized") return "", fmt.Errorf("create ticket graph tool not initialized")
} }
return graphs.NewCreateTicketGraph(t.conversation, t.aiAgent).Run(ctx, argumentsInJSON) return graphs.NewCreateTicketGraph(t.conversation, t.aiAgent).Run(ctx, argumentsInJSON)
@@ -17,7 +17,7 @@ import (
type HandoffGraphTool struct { type HandoffGraphTool struct {
conversation *models.Conversation conversation *models.Conversation
aiAgent *models.AIAgent aiAgent models.AIAgent
} }
func NewHandoffGraphTool() *HandoffGraphTool { func NewHandoffGraphTool() *HandoffGraphTool {
@@ -37,7 +37,7 @@ func (t *HandoffGraphTool) Code() string {
} }
func (t *HandoffGraphTool) Enabled(ctx registry.Context) bool { func (t *HandoffGraphTool) Enabled(ctx registry.Context) bool {
return ctx.Conversation != nil && ctx.AIAgent != nil return true
} }
func (t *HandoffGraphTool) Build(ctx registry.Context) (einotool.BaseTool, error) { func (t *HandoffGraphTool) Build(ctx registry.Context) (einotool.BaseTool, error) {
@@ -75,7 +75,7 @@ func (t *HandoffGraphTool) Info(ctx context.Context) (*schema.ToolInfo, error) {
} }
func (t *HandoffGraphTool) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...einotool.Option) (string, error) { func (t *HandoffGraphTool) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...einotool.Option) (string, error) {
if t == nil || t.conversation == nil || t.aiAgent == nil { if t == nil || t.conversation == nil {
return "", fmt.Errorf("handoff graph tool not initialized") return "", fmt.Errorf("handoff graph tool not initialized")
} }
return graphs.NewHandoffGraph(t.conversation, t.aiAgent).Run(ctx, argumentsInJSON) return graphs.NewHandoffGraph(t.conversation, t.aiAgent).Run(ctx, argumentsInJSON)
+1 -4
View File
@@ -20,10 +20,7 @@ func (l *candidateLoader) findManualSkillDefinition(skillCode string) *models.Sk
return repositories.SkillDefinitionRepository.GetByCode(sqls.DB(), skillCode) return repositories.SkillDefinitionRepository.GetByCode(sqls.DB(), skillCode)
} }
func (l *candidateLoader) loadCandidateSkills(aiAgent *models.AIAgent) []models.SkillDefinition { func (l *candidateLoader) loadCandidateSkills(aiAgent models.AIAgent) []models.SkillDefinition {
if aiAgent == nil {
return nil
}
skillIDs := utils.SplitInt64s(aiAgent.SkillIDs) skillIDs := utils.SplitInt64s(aiAgent.SkillIDs)
skills := repositories.SkillDefinitionRepository.GetByIDs(sqls.DB(), skillIDs) skills := repositories.SkillDefinitionRepository.GetByIDs(sqls.DB(), skillIDs)
ret := make([]models.SkillDefinition, 0, len(skillIDs)) ret := make([]models.SkillDefinition, 0, len(skillIDs))
+6 -6
View File
@@ -11,16 +11,16 @@ import (
func TestBuildRunLogMatchedPlan(t *testing.T) { func TestBuildRunLogMatchedPlan(t *testing.T) {
log := BuildRunLog( log := BuildRunLog(
RuntimeContext{ RuntimeContext{
AIAgent: &models.AIAgent{ID: 22}, AIAgent: models.AIAgent{ID: 22},
AIConfig: &models.AIConfig{ID: 33}, AIConfig: models.AIConfig{ID: 33},
ConversationID: 11, ConversationID: 11,
ManualSkillCode: "manual_refund", ManualSkillCode: "manual_refund",
IntentCode: "refund", IntentCode: "refund",
UserMessage: "我要退款", UserMessage: "我要退款",
}, },
&ExecutionPlan{ &ExecutionPlan{
AIAgent: &models.AIAgent{ID: 22}, AIAgent: models.AIAgent{ID: 22},
AIConfig: &models.AIConfig{ AIConfig: models.AIConfig{
ID: 33, ID: 33,
ModelName: "gpt-test", ModelName: "gpt-test",
Provider: enums.AIProviderOpenAI, Provider: enums.AIProviderOpenAI,
@@ -55,7 +55,7 @@ func TestBuildRunLogMatchedPlan(t *testing.T) {
func TestBuildRunLogNotMatchedAndError(t *testing.T) { func TestBuildRunLogNotMatchedAndError(t *testing.T) {
log := BuildRunLog( log := BuildRunLog(
RuntimeContext{ RuntimeContext{
AIAgent: &models.AIAgent{ID: 22}, AIAgent: models.AIAgent{ID: 22},
UserMessage: "随便问问", UserMessage: "随便问问",
}, },
nil, nil,
@@ -74,7 +74,7 @@ func TestBuildRunLogNotMatchedAndError(t *testing.T) {
} }
noMatchLog := BuildRunLog( noMatchLog := BuildRunLog(
RuntimeContext{AIAgent: &models.AIAgent{ID: 22}, UserMessage: "随便问问"}, RuntimeContext{AIAgent: models.AIAgent{ID: 22}, UserMessage: "随便问问"},
&ExecutionPlan{MatchReason: ""}, &ExecutionPlan{MatchReason: ""},
&ExecutionTrace{Status: "not_matched"}, &ExecutionTrace{Status: "not_matched"},
nil, nil,
+3 -3
View File
@@ -16,7 +16,7 @@ type intentTriggerConfig struct {
} }
// MatchSkill 对单个 SkillDefinition 执行命中判断。 // MatchSkill 对单个 SkillDefinition 执行命中判断。
func MatchSkill(execCtx context.Context, ctx RuntimeContext, aiAgent *models.AIAgent, aiConfig *models.AIConfig) (*models.SkillDefinition, string, *RouteTrace, error) { func MatchSkill(execCtx context.Context, ctx RuntimeContext) (*models.SkillDefinition, string, *RouteTrace, error) {
loader := newCandidateLoader() loader := newCandidateLoader()
if strs.IsNotBlank(ctx.ManualSkillCode) { if strs.IsNotBlank(ctx.ManualSkillCode) {
skill := loader.findManualSkillDefinition(ctx.ManualSkillCode) skill := loader.findManualSkillDefinition(ctx.ManualSkillCode)
@@ -29,7 +29,7 @@ func MatchSkill(execCtx context.Context, ctx RuntimeContext, aiAgent *models.AIA
}, nil }, nil
} }
candidates := loader.loadCandidateSkills(aiAgent) candidates := loader.loadCandidateSkills(ctx.AIAgent)
trace := &RouteTrace{ trace := &RouteTrace{
Status: "started", Status: "started",
CandidateSkillCodes: make([]string, 0, len(candidates)), CandidateSkillCodes: make([]string, 0, len(candidates)),
@@ -53,7 +53,7 @@ func MatchSkill(execCtx context.Context, ctx RuntimeContext, aiAgent *models.AIA
} }
} }
selected, routeTrace, err := routeSkillWithLLM(execCtx, aiConfig, ctx.UserMessage, candidates) selected, routeTrace, err := routeSkillWithLLM(execCtx, ctx.AIConfig, ctx.UserMessage, candidates)
if routeTrace != nil { if routeTrace != nil {
trace.Status = routeTrace.Status trace.Status = routeTrace.Status
trace.SelectedSkillCode = routeTrace.SelectedSkillCode trace.SelectedSkillCode = routeTrace.SelectedSkillCode
+1 -9
View File
@@ -3,8 +3,6 @@ package skills
import ( import (
"context" "context"
"strings" "strings"
"cs-agent/internal/pkg/errorsx"
) )
func newPlanService() *planService { func newPlanService() *planService {
@@ -15,13 +13,7 @@ type planService struct{}
// BuildExecutionPlan 构建当前请求的 Skill 执行计划。 // BuildExecutionPlan 构建当前请求的 Skill 执行计划。
func (s *planService) BuildExecutionPlan(execCtx context.Context, ctx RuntimeContext) (*ExecutionPlan, error) { func (s *planService) BuildExecutionPlan(execCtx context.Context, ctx RuntimeContext) (*ExecutionPlan, error) {
if ctx.AIAgent == nil { skill, matchReason, routeTrace, err := MatchSkill(execCtx, ctx)
return nil, errorsx.InvalidParam("AIAgent不能为空")
}
if ctx.AIConfig == nil {
return nil, errorsx.InvalidParam("AIConfig不能为空")
}
skill, matchReason, routeTrace, err := MatchSkill(execCtx, ctx, ctx.AIAgent, ctx.AIConfig)
if err != nil { if err != nil {
return nil, err return nil, err
} }
+1 -7
View File
@@ -9,16 +9,10 @@ import (
"cs-agent/internal/ai" "cs-agent/internal/ai"
"cs-agent/internal/models" "cs-agent/internal/models"
"cs-agent/internal/pkg/errorsx"
) )
func routeSkillWithLLM(ctx context.Context, aiConfig *models.AIConfig, userMessage string, candidates []models.SkillDefinition) (*models.SkillDefinition, *RouteTrace, error) { func routeSkillWithLLM(ctx context.Context, aiConfig models.AIConfig, userMessage string, candidates []models.SkillDefinition) (*models.SkillDefinition, *RouteTrace, error) {
trace := &RouteTrace{Status: "started"} trace := &RouteTrace{Status: "started"}
if aiConfig == nil {
trace.Status = "config_error"
trace.Error = "ai config is nil"
return nil, trace, errorsx.InvalidParam("Skill 路由依赖的 AI 配置不可用")
}
if len(candidates) == 0 { if len(candidates) == 0 {
trace.Status = "no_candidate" trace.Status = "no_candidate"
return nil, trace, nil return nil, trace, nil
+2 -13
View File
@@ -20,7 +20,7 @@ type RunLogService struct{}
func (s *RunLogService) Build(ctx RuntimeContext, plan *ExecutionPlan, trace *ExecutionTrace, err error) *models.SkillRunLog { func (s *RunLogService) Build(ctx RuntimeContext, plan *ExecutionPlan, trace *ExecutionTrace, err error) *models.SkillRunLog {
log := &models.SkillRunLog{ log := &models.SkillRunLog{
ConversationID: ctx.ConversationID, ConversationID: ctx.ConversationID,
AIAgentID: resolveRuntimeAIAgentID(ctx, plan), AIAgentID: ctx.AIAgent.ID,
ManualSkillCode: ctx.ManualSkillCode, ManualSkillCode: ctx.ManualSkillCode,
IntentCode: ctx.IntentCode, IntentCode: ctx.IntentCode,
UserMessage: ctx.UserMessage, UserMessage: ctx.UserMessage,
@@ -28,11 +28,10 @@ func (s *RunLogService) Build(ctx RuntimeContext, plan *ExecutionPlan, trace *Ex
CreatedAt: time.Now(), CreatedAt: time.Now(),
} }
if plan != nil { if plan != nil {
if plan.AIConfig != nil {
log.AIConfigID = plan.AIConfig.ID log.AIConfigID = plan.AIConfig.ID
log.UsedModel = plan.AIConfig.ModelName log.UsedModel = plan.AIConfig.ModelName
log.UsedProvider = plan.AIConfig.Provider log.UsedProvider = plan.AIConfig.Provider
}
if plan.Skill != nil { if plan.Skill != nil {
log.SkillDefinitionID = plan.Skill.ID log.SkillDefinitionID = plan.Skill.ID
log.SkillCode = plan.Skill.Code log.SkillCode = plan.Skill.Code
@@ -53,16 +52,6 @@ func (s *RunLogService) Build(ctx RuntimeContext, plan *ExecutionPlan, trace *Ex
return log return log
} }
func resolveRuntimeAIAgentID(ctx RuntimeContext, plan *ExecutionPlan) int64 {
if ctx.AIAgent != nil {
return ctx.AIAgent.ID
}
if plan != nil && plan.AIAgent != nil {
return plan.AIAgent.ID
}
return 0
}
// Write 写入 Skill 路由日志。 // Write 写入 Skill 路由日志。
func (s *RunLogService) Write(log *models.SkillRunLog) error { func (s *RunLogService) Write(log *models.SkillRunLog) error {
if log == nil { if log == nil {
+4 -4
View File
@@ -4,8 +4,8 @@ import "cs-agent/internal/models"
// RuntimeContext 表示一次 Skill 运行的输入上下文。 // RuntimeContext 表示一次 Skill 运行的输入上下文。
type RuntimeContext struct { type RuntimeContext struct {
AIAgent *models.AIAgent // AIAgent 为当前请求所属的 AI Agent,必填。 AIAgent models.AIAgent // AIAgent 为当前请求所属的 AI Agent,必填。
AIConfig *models.AIConfig // AIConfig 为当前请求实际使用的模型配置,必填。 AIConfig models.AIConfig // AIConfig 为当前请求实际使用的模型配置,必填。
UserMessage string // UserMessage 为当前用户输入。 UserMessage string // UserMessage 为当前用户输入。
ConversationID int64 // ConversationID 为当前会话 ID,无会话上下文时为 0。 ConversationID int64 // ConversationID 为当前会话 ID,无会话上下文时为 0。
ManualSkillCode string // ManualSkillCode 为显式指定的 Skill 编码。 ManualSkillCode string // ManualSkillCode 为显式指定的 Skill 编码。
@@ -14,8 +14,8 @@ type RuntimeContext struct {
// ExecutionPlan 表示 Skill Runtime 计算出的最终路由结果。 // ExecutionPlan 表示 Skill Runtime 计算出的最终路由结果。
type ExecutionPlan struct { type ExecutionPlan struct {
AIAgent *models.AIAgent // AIAgent 为本次请求所属的 AI Agent。 AIAgent models.AIAgent // AIAgent 为本次请求所属的 AI Agent。
AIConfig *models.AIConfig // AIConfig 为本次请求实际使用的模型配置。 AIConfig models.AIConfig // AIConfig 为本次请求实际使用的模型配置。
Skill *models.SkillDefinition // Skill 为最终命中的 Skill,未命中时为空。 Skill *models.SkillDefinition // Skill 为最终命中的 Skill,未命中时为空。
MatchReason string // MatchReason 为命中原因。 MatchReason string // MatchReason 为命中原因。
RouteTrace *RouteTrace // RouteTrace 为匹配阶段的路由追踪。 RouteTrace *RouteTrace // RouteTrace 为匹配阶段的路由追踪。
+1 -4
View File
@@ -288,13 +288,10 @@ func (s *conversationService) TransferConversation(conversationID, toUserID int6
return nil return nil
} }
func (s *conversationService) HandoffByAI(conversationID int64, aiAgent *models.AIAgent, reason string) error { func (s *conversationService) HandoffByAI(conversationID int64, aiAgent models.AIAgent, reason string) error {
if conversationID <= 0 { if conversationID <= 0 {
return errorsx.InvalidParam("会话不存在") return errorsx.InvalidParam("会话不存在")
} }
if aiAgent == nil {
return errorsx.InvalidParam("AI Agent 不存在")
}
now := time.Now() now := time.Now()
if err := sqls.WithTransaction(func(ctx *sqls.TxContext) error { if err := sqls.WithTransaction(func(ctx *sqls.TxContext) error {
conversation := repositories.ConversationRepository.Get(ctx.Tx, conversationID) conversation := repositories.ConversationRepository.Get(ctx.Tx, conversationID)