Init
This commit is contained in:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user