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:
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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) {
|
|
||||||
if config == nil {
|
|
||||||
return nil, fmt.Errorf("ai config is nil")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *llm) ChatWithConfig(ctx context.Context, config models.AIConfig, systemPrompt string, userPrompt string) (*ChatCompletionResult, error) {
|
||||||
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(¶ms, 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")
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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},
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,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 为匹配阶段的路由追踪。
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user