This commit is contained in:
mlogclub
2026-04-09 10:01:23 +08:00
commit efe801b8bf
707 changed files with 110595 additions and 0 deletions
@@ -0,0 +1,112 @@
package factory
import (
"context"
"strings"
einoadapter "cs-agent/internal/ai/runtime/internal/impl/adapter"
einoagents "cs-agent/internal/ai/runtime/internal/impl/agents"
einocallbacks "cs-agent/internal/ai/runtime/internal/impl/callbacks"
"cs-agent/internal/models"
"github.com/cloudwego/eino/adk"
einobasetool "github.com/cloudwego/eino/components/tool"
"github.com/cloudwego/eino/compose"
)
type AgentFactory struct {
chatModelFactory *ChatModelFactory
toolFactory *ToolFactory
}
func NewAgentFactory() *AgentFactory {
return &AgentFactory{
chatModelFactory: NewChatModelFactory(),
toolFactory: NewToolFactory(),
}
}
func (f *AgentFactory) BuildCustomerServiceAgent(ctx context.Context, aiAgent *models.AIAgent, aiConfig *models.AIConfig,
toolDefinitions []einoadapter.MCPToolDefinition, extraTools []einobasetool.BaseTool, extraToolCodes map[string]string,
collector *einocallbacks.RuntimeTraceCollector) (*einoagents.CustomerServiceAgent, error) {
if aiAgent == nil || aiConfig == nil {
return nil, nil
}
chatModel, err := f.chatModelFactory.Build(ctx, aiConfig)
if err != nil {
return nil, err
}
baseTools, err := f.toolFactory.BuildBaseToolsByDefinitions(ctx, toolDefinitions)
if err != nil {
return nil, err
}
allTools := make([]einobasetool.BaseTool, 0, len(baseTools)+len(extraTools))
allTools = append(allTools, extraTools...)
allTools = append(allTools, baseTools...)
handlers := make([]adk.ChatModelAgentMiddleware, 0, 1)
if collector != nil {
toolMetadataBy := make(map[string]einocallbacks.ToolMetadata, len(toolDefinitions))
for _, item := range toolDefinitions {
toolMetadataBy[item.ModelName] = einocallbacks.ToolMetadata{
ToolCode: item.ToolCode,
ServerCode: item.ServerCode,
ToolName: item.ToolName,
}
}
handlers = append(handlers, einocallbacks.NewRuntimeTraceHandler(collector, toolMetadataBy))
}
inner, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{
Name: strings.TrimSpace(aiAgent.Name),
Description: strings.TrimSpace(aiAgent.Description),
Instruction: buildAgentInstruction(aiAgent, extraToolCodes),
Model: chatModel,
ToolsConfig: adk.ToolsConfig{
ToolsNodeConfig: compose.ToolsNodeConfig{
Tools: allTools,
},
},
Handlers: handlers,
})
if err != nil {
return nil, err
}
return &einoagents.CustomerServiceAgent{Inner: inner}, nil
}
func buildAgentInstruction(aiAgent *models.AIAgent, extraToolCodes map[string]string) string {
baseInstruction := ""
if aiAgent != nil {
baseInstruction = strings.TrimSpace(aiAgent.SystemPrompt)
}
appendixParts := make([]string, 0, 1)
if hasToolCode(extraToolCodes, "builtin/create_ticket_with_confirmation") {
appendixParts = append(appendixParts, strings.TrimSpace(`
你可以在确认信息充分后调用 create_ticket_with_confirmation 工具来创建工单,但必须遵守以下规则:
1. 只有在用户明确表达希望提交工单、投诉、报障、售后处理等诉求时,才考虑调用该工具。
2. 调用前你必须已经整理出清晰的工单标题和问题描述;如果信息不足,先继续追问,不要过早调用。
3. 一旦准备创建工单,必须调用 create_ticket_with_confirmation 工具,禁止直接口头宣称“已经创建工单”。
4. 该工具会先向用户发起确认。用户确认后才会真正创建工单;用户取消则结束本次建单流程。
5. 如果用户只是咨询、抱怨或泛泛表达不满,但没有明确要求建单,优先继续澄清,不要主动创建工单。
`))
}
if len(appendixParts) == 0 {
return baseInstruction
}
if baseInstruction == "" {
return strings.Join(appendixParts, "\n\n")
}
return baseInstruction + "\n\n" + strings.Join(appendixParts, "\n\n")
}
func hasToolCode(toolCodes map[string]string, target string) bool {
target = strings.TrimSpace(target)
if target == "" {
return false
}
for _, toolCode := range toolCodes {
if strings.TrimSpace(toolCode) == target {
return true
}
}
return false
}
@@ -0,0 +1,64 @@
package factory
import (
"context"
"strings"
"time"
"cs-agent/internal/models"
"cs-agent/internal/pkg/enums"
openai "github.com/cloudwego/eino-ext/components/model/openai"
"github.com/cloudwego/eino/components/model"
)
type ChatModelFactory struct{}
func NewChatModelFactory() *ChatModelFactory {
return &ChatModelFactory{}
}
func (f *ChatModelFactory) Build(ctx context.Context, item *models.AIConfig) (model.ToolCallingChatModel, error) {
if item == nil {
return nil, nil
}
conf := &openai.ChatModelConfig{
APIKey: strings.TrimSpace(item.APIKey),
BaseURL: strings.TrimSpace(item.BaseURL),
Model: strings.TrimSpace(item.ModelName),
}
if item.TimeoutMS > 0 {
conf.Timeout = time.Duration(item.TimeoutMS) * time.Millisecond
}
if item.MaxOutputTokens > 0 {
maxCompletionTokens := item.MaxOutputTokens
conf.MaxCompletionTokens = &maxCompletionTokens
}
if item.Provider == enums.AIProviderOpenAI && isAzureOpenAIBaseURL(item.BaseURL) {
conf.ByAzure = true
conf.APIVersion = "2024-06-01"
}
if extraFields := providerExtraFields(item); len(extraFields) > 0 {
conf.ExtraFields = extraFields
}
return openai.NewChatModel(ctx, conf)
}
func isAzureOpenAIBaseURL(baseURL string) bool {
baseURL = strings.ToLower(strings.TrimSpace(baseURL))
return strings.Contains(baseURL, ".openai.azure.com")
}
func providerExtraFields(item *models.AIConfig) map[string]any {
if item == nil {
return nil
}
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") {
return map[string]any{
"enable_thinking": false,
}
}
return nil
}
@@ -0,0 +1,27 @@
package factory
import (
"context"
einostore "cs-agent/internal/ai/runtime/internal/impl/store"
"github.com/cloudwego/eino/adk"
)
type RunnerFactory struct{}
func NewRunnerFactory() *RunnerFactory {
return &RunnerFactory{}
}
func (f *RunnerFactory) Build(ctx context.Context, agent adk.Agent, enableStreaming bool, enableCheckpoint bool) *adk.Runner {
var checkpointStore adk.CheckPointStore
if enableCheckpoint {
checkpointStore = einostore.DefaultCheckPointStore
}
return adk.NewRunner(ctx, adk.RunnerConfig{
Agent: agent,
EnableStreaming: enableStreaming,
CheckPointStore: checkpointStore,
})
}
@@ -0,0 +1,100 @@
package factory
import (
"context"
"encoding/json"
"strings"
"cs-agent/internal/ai/mcps"
impladapter "cs-agent/internal/ai/runtime/internal/impl/adapter"
"cs-agent/internal/models"
"cs-agent/internal/pkg/dto/request"
einotool "github.com/cloudwego/eino/components/tool"
)
type ToolFactory struct{}
func NewToolFactory() *ToolFactory {
return &ToolFactory{}
}
func (f *ToolFactory) BuildMCPTools(aiAgent *models.AIAgent) ([]impladapter.MCPToolDefinition, error) {
if aiAgent == nil || strings.TrimSpace(aiAgent.AllowedMCPTools) == "" {
return nil, nil
}
var raw []request.AIAgentMCPToolRequest
if err := json.Unmarshal([]byte(aiAgent.AllowedMCPTools), &raw); err != nil {
return nil, err
}
ret := make([]impladapter.MCPToolDefinition, 0, len(raw))
for _, item := range raw {
toolCode := strings.TrimSpace(item.ServerCode) + "/" + strings.TrimSpace(item.ToolName)
definition := impladapter.MCPToolDefinition{
ToolCode: toolCode,
ServerCode: strings.TrimSpace(item.ServerCode),
ToolName: strings.TrimSpace(item.ToolName),
Title: strings.TrimSpace(item.Title),
Description: strings.TrimSpace(item.Description),
FixedArgs: cloneStringMap(item.Arguments),
}
definition.ModelName = impladapter.BuildModelToolName(definition)
ret = append(ret, definition)
}
return ret, nil
}
func (f *ToolFactory) BuildBaseTools(ctx context.Context, aiAgent *models.AIAgent) ([]einotool.BaseTool, error) {
definitions, err := f.BuildMCPTools(aiAgent)
if err != nil {
return nil, err
}
return f.BuildBaseToolsByDefinitions(ctx, definitions)
}
func (f *ToolFactory) BuildBaseToolsByDefinitions(ctx context.Context, definitions []impladapter.MCPToolDefinition) ([]einotool.BaseTool, error) {
if len(definitions) == 0 {
return nil, nil
}
metadataByCode, err := f.loadToolMetadata(ctx, definitions)
if err != nil {
return nil, err
}
ret := make([]einotool.BaseTool, 0, len(definitions))
for _, item := range definitions {
ret = append(ret, impladapter.NewMCPTool(item, metadataByCode[item.ToolCode]))
}
return ret, nil
}
func (f *ToolFactory) loadToolMetadata(ctx context.Context, definitions []impladapter.MCPToolDefinition) (map[string]*mcps.ToolInfo, error) {
toolsByCode := make(map[string]*mcps.ToolInfo, len(definitions))
serverCodes := make(map[string]struct{})
for _, item := range definitions {
serverCodes[item.ServerCode] = struct{}{}
}
for serverCode := range serverCodes {
toolInfos, err := mcps.Runtime.ListTools(ctx, serverCode)
if err != nil {
return nil, err
}
for i := range toolInfos {
toolInfo := toolInfos[i]
toolCode := strings.TrimSpace(serverCode) + "/" + strings.TrimSpace(toolInfo.Name)
toolInfoCopy := toolInfo
toolsByCode[toolCode] = &toolInfoCopy
}
}
return toolsByCode, nil
}
func cloneStringMap(input map[string]string) map[string]string {
if len(input) == 0 {
return nil
}
ret := make(map[string]string, len(input))
for key, value := range input {
ret[key] = value
}
return ret
}