Init
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
package adapter
|
||||
|
||||
import "cs-agent/internal/models"
|
||||
|
||||
type AIConfigSnapshot struct {
|
||||
ID int64
|
||||
Provider string
|
||||
ModelName string
|
||||
BaseURL string
|
||||
MaxOutputTokens int
|
||||
TimeoutMS int
|
||||
}
|
||||
|
||||
func BuildAIConfigSnapshot(item *models.AIConfig) *AIConfigSnapshot {
|
||||
if item == nil {
|
||||
return nil
|
||||
}
|
||||
return &AIConfigSnapshot{
|
||||
ID: item.ID,
|
||||
Provider: string(item.Provider),
|
||||
ModelName: item.ModelName,
|
||||
BaseURL: item.BaseURL,
|
||||
MaxOutputTokens: item.MaxOutputTokens,
|
||||
TimeoutMS: item.TimeoutMS,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package adapter
|
||||
|
||||
import "cs-agent/internal/models"
|
||||
|
||||
type ConversationSnapshot struct {
|
||||
ID int64
|
||||
AIAgentID int64
|
||||
LastMessageID int64
|
||||
CurrentAssigneeID int64
|
||||
}
|
||||
|
||||
func BuildConversationSnapshot(item *models.Conversation) *ConversationSnapshot {
|
||||
if item == nil {
|
||||
return nil
|
||||
}
|
||||
return &ConversationSnapshot{
|
||||
ID: item.ID,
|
||||
AIAgentID: item.AIAgentID,
|
||||
LastMessageID: item.LastMessageID,
|
||||
CurrentAssigneeID: item.CurrentAssigneeID,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"hash/crc32"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"cs-agent/internal/ai/mcps"
|
||||
|
||||
einojsonschema "github.com/eino-contrib/jsonschema"
|
||||
|
||||
einotool "github.com/cloudwego/eino/components/tool"
|
||||
"github.com/cloudwego/eino/schema"
|
||||
)
|
||||
|
||||
var toolNameSanitizer = regexp.MustCompile(`[^a-zA-Z0-9_]`)
|
||||
|
||||
type MCPToolDefinition struct {
|
||||
ToolCode string
|
||||
ServerCode string
|
||||
ToolName string
|
||||
ModelName string
|
||||
Title string
|
||||
Description string
|
||||
FixedArgs map[string]string
|
||||
}
|
||||
|
||||
type MCPTool struct {
|
||||
definition MCPToolDefinition
|
||||
info *schema.ToolInfo
|
||||
}
|
||||
|
||||
func NewMCPTool(definition MCPToolDefinition, metadata *mcps.ToolInfo) *MCPTool {
|
||||
return &MCPTool{
|
||||
definition: definition,
|
||||
info: buildToolInfo(definition, metadata),
|
||||
}
|
||||
}
|
||||
|
||||
var _ einotool.InvokableTool = (*MCPTool)(nil)
|
||||
|
||||
func (t *MCPTool) Info(ctx context.Context) (*schema.ToolInfo, error) {
|
||||
if t == nil || t.info == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return t.info, nil
|
||||
}
|
||||
|
||||
func (t *MCPTool) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...einotool.Option) (string, error) {
|
||||
if t == nil {
|
||||
return "", fmt.Errorf("mcp tool is nil")
|
||||
}
|
||||
arguments, err := parseArguments(argumentsInJSON)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
arguments = mergeFixedArguments(arguments, t.definition.FixedArgs)
|
||||
result, err := mcps.Runtime.CallTool(ctx, t.definition.ServerCode, t.definition.ToolName, arguments)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return buildToolResultSummary(result), nil
|
||||
}
|
||||
|
||||
func buildToolInfo(definition MCPToolDefinition, metadata *mcps.ToolInfo) *schema.ToolInfo {
|
||||
desc := strings.TrimSpace(definition.Description)
|
||||
if desc == "" && metadata != nil {
|
||||
desc = strings.TrimSpace(metadata.Description)
|
||||
}
|
||||
title := strings.TrimSpace(definition.Title)
|
||||
if title == "" && metadata != nil {
|
||||
title = strings.TrimSpace(metadata.Title)
|
||||
}
|
||||
if title != "" && desc != "" {
|
||||
desc = title + "\n\n" + desc
|
||||
} else if title != "" {
|
||||
desc = title
|
||||
}
|
||||
if desc == "" {
|
||||
desc = "Call MCP tool " + strings.TrimSpace(definition.ToolCode)
|
||||
}
|
||||
info := &schema.ToolInfo{
|
||||
Name: BuildModelToolName(definition),
|
||||
Desc: desc,
|
||||
Extra: map[string]any{
|
||||
"toolCode": definition.ToolCode,
|
||||
"serverCode": definition.ServerCode,
|
||||
"toolName": definition.ToolName,
|
||||
},
|
||||
}
|
||||
if js := buildParamsSchema(metadata); js != nil {
|
||||
info.ParamsOneOf = schema.NewParamsOneOfByJSONSchema(js)
|
||||
}
|
||||
return info
|
||||
}
|
||||
|
||||
func buildParamsSchema(metadata *mcps.ToolInfo) *einojsonschema.Schema {
|
||||
if metadata == nil || metadata.InputSchema == nil {
|
||||
return genericObjectSchema()
|
||||
}
|
||||
raw, err := json.Marshal(metadata.InputSchema)
|
||||
if err != nil || len(raw) == 0 {
|
||||
return genericObjectSchema()
|
||||
}
|
||||
js := &einojsonschema.Schema{}
|
||||
if err := json.Unmarshal(raw, js); err != nil {
|
||||
return genericObjectSchema()
|
||||
}
|
||||
return js
|
||||
}
|
||||
|
||||
func genericObjectSchema() *einojsonschema.Schema {
|
||||
return &einojsonschema.Schema{
|
||||
Version: einojsonschema.Version,
|
||||
Type: "object",
|
||||
AdditionalProperties: &einojsonschema.Schema{},
|
||||
}
|
||||
}
|
||||
|
||||
func parseArguments(argumentsInJSON string) (map[string]any, error) {
|
||||
argumentsInJSON = strings.TrimSpace(argumentsInJSON)
|
||||
if argumentsInJSON == "" {
|
||||
return map[string]any{}, nil
|
||||
}
|
||||
args := make(map[string]any)
|
||||
if err := json.Unmarshal([]byte(argumentsInJSON), &args); err != nil {
|
||||
return nil, fmt.Errorf("invalid tool arguments: %w", err)
|
||||
}
|
||||
return args, nil
|
||||
}
|
||||
|
||||
func mergeFixedArguments(arguments map[string]any, fixedArgs map[string]string) map[string]any {
|
||||
if len(arguments) == 0 && len(fixedArgs) == 0 {
|
||||
return map[string]any{}
|
||||
}
|
||||
ret := make(map[string]any, len(arguments)+len(fixedArgs))
|
||||
for key, value := range arguments {
|
||||
ret[key] = value
|
||||
}
|
||||
for key, value := range fixedArgs {
|
||||
ret[key] = strings.TrimSpace(value)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func buildToolResultSummary(result *mcps.ToolCallResult) string {
|
||||
if result == nil {
|
||||
return ""
|
||||
}
|
||||
lines := make([]string, 0, len(result.Content)+2)
|
||||
if result.IsError {
|
||||
lines = append(lines, "tool returned an error")
|
||||
}
|
||||
if result.StructuredContent != nil {
|
||||
if data, err := json.Marshal(result.StructuredContent); err == nil {
|
||||
lines = append(lines, string(data))
|
||||
}
|
||||
}
|
||||
for _, item := range result.Content {
|
||||
switch item.Type {
|
||||
case "text":
|
||||
if text := strings.TrimSpace(item.Text); text != "" {
|
||||
lines = append(lines, text)
|
||||
}
|
||||
default:
|
||||
if item.Data == nil {
|
||||
continue
|
||||
}
|
||||
if data, err := json.Marshal(item.Data); err == nil {
|
||||
lines = append(lines, string(data))
|
||||
}
|
||||
}
|
||||
}
|
||||
return strings.TrimSpace(strings.Join(lines, "\n"))
|
||||
}
|
||||
|
||||
func BuildModelToolName(definition MCPToolDefinition) string {
|
||||
if strings.TrimSpace(definition.ModelName) != "" {
|
||||
return strings.TrimSpace(definition.ModelName)
|
||||
}
|
||||
base := "mcp_" + strings.TrimSpace(definition.ServerCode) + "_" + strings.TrimSpace(definition.ToolName)
|
||||
base = toolNameSanitizer.ReplaceAllString(base, "_")
|
||||
base = strings.Trim(base, "_")
|
||||
if base == "" {
|
||||
base = "mcp_tool"
|
||||
}
|
||||
checksum := crc32.ChecksumIEEE([]byte(definition.ToolCode))
|
||||
return fmt.Sprintf("%s_%08x", base, checksum)
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"cs-agent/internal/models"
|
||||
"cs-agent/internal/pkg/enums"
|
||||
"cs-agent/internal/repositories"
|
||||
|
||||
"github.com/cloudwego/eino/schema"
|
||||
"github.com/mlogclub/simple/sqls"
|
||||
)
|
||||
|
||||
const defaultHistoryLimit = 12
|
||||
|
||||
type HistoryBuildResult struct {
|
||||
Messages []*schema.Message
|
||||
RawItems []models.Message
|
||||
}
|
||||
|
||||
func BuildHistoryMessages(conversationID int64, currentMessageID int64, limit int) HistoryBuildResult {
|
||||
if conversationID <= 0 {
|
||||
return HistoryBuildResult{}
|
||||
}
|
||||
if limit <= 0 {
|
||||
limit = defaultHistoryLimit
|
||||
}
|
||||
items := repositories.MessageRepository.Find(sqls.DB(), sqls.NewCnd().
|
||||
Eq("conversation_id", conversationID).
|
||||
Desc("id").
|
||||
Limit(limit+1))
|
||||
for i, j := 0, len(items)-1; i < j; i, j = i+1, j-1 {
|
||||
items[i], items[j] = items[j], items[i]
|
||||
}
|
||||
ret := HistoryBuildResult{
|
||||
Messages: make([]*schema.Message, 0, len(items)),
|
||||
RawItems: make([]models.Message, 0, len(items)),
|
||||
}
|
||||
for _, item := range items {
|
||||
if item.ID == currentMessageID {
|
||||
continue
|
||||
}
|
||||
msg := BuildSchemaMessage(&item)
|
||||
if msg == nil {
|
||||
continue
|
||||
}
|
||||
ret.RawItems = append(ret.RawItems, item)
|
||||
ret.Messages = append(ret.Messages, msg)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func BuildSchemaMessage(item *models.Message) *schema.Message {
|
||||
if item == nil {
|
||||
return nil
|
||||
}
|
||||
content := strings.TrimSpace(item.Content)
|
||||
if content == "" {
|
||||
return nil
|
||||
}
|
||||
switch item.SenderType {
|
||||
case enums.IMSenderTypeCustomer:
|
||||
return schema.UserMessage(content)
|
||||
case enums.IMSenderTypeAI, enums.IMSenderTypeAgent:
|
||||
return schema.AssistantMessage(content, nil)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package agents
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/cloudwego/eino/adk"
|
||||
)
|
||||
|
||||
type CustomerServiceAgent struct {
|
||||
Inner adk.Agent
|
||||
}
|
||||
|
||||
var _ adk.ResumableAgent = (*CustomerServiceAgent)(nil)
|
||||
|
||||
func (a *CustomerServiceAgent) Name(ctx context.Context) string {
|
||||
if a == nil || a.Inner == nil {
|
||||
return "customer_service_agent"
|
||||
}
|
||||
return a.Inner.Name(ctx)
|
||||
}
|
||||
|
||||
func (a *CustomerServiceAgent) Description(ctx context.Context) string {
|
||||
if a == nil || a.Inner == nil {
|
||||
return "customer service chat agent"
|
||||
}
|
||||
return a.Inner.Description(ctx)
|
||||
}
|
||||
|
||||
func (a *CustomerServiceAgent) Run(ctx context.Context, input *adk.AgentInput, options ...adk.AgentRunOption) *adk.AsyncIterator[*adk.AgentEvent] {
|
||||
if a == nil || a.Inner == nil {
|
||||
iter, gen := adk.NewAsyncIteratorPair[*adk.AgentEvent]()
|
||||
gen.Send(&adk.AgentEvent{Err: context.Canceled})
|
||||
gen.Close()
|
||||
return iter
|
||||
}
|
||||
return a.Inner.Run(ctx, input, options...)
|
||||
}
|
||||
|
||||
func (a *CustomerServiceAgent) Resume(ctx context.Context, info *adk.ResumeInfo, options ...adk.AgentRunOption) *adk.AsyncIterator[*adk.AgentEvent] {
|
||||
if a == nil || a.Inner == nil {
|
||||
iter, gen := adk.NewAsyncIteratorPair[*adk.AgentEvent]()
|
||||
gen.Send(&adk.AgentEvent{Err: fmt.Errorf("customer service agent is not initialized")})
|
||||
gen.Close()
|
||||
return iter
|
||||
}
|
||||
ra, ok := a.Inner.(adk.ResumableAgent)
|
||||
if !ok {
|
||||
iter, gen := adk.NewAsyncIteratorPair[*adk.AgentEvent]()
|
||||
gen.Send(&adk.AgentEvent{Err: fmt.Errorf("inner agent %q does not implement resumable agent", a.Inner.Name(ctx))})
|
||||
gen.Close()
|
||||
return iter
|
||||
}
|
||||
return ra.Resume(ctx, info, options...)
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package callbacks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
einotool "github.com/cloudwego/eino/components/tool"
|
||||
|
||||
"github.com/cloudwego/eino/adk"
|
||||
)
|
||||
|
||||
type ToolMetadata struct {
|
||||
ToolCode string
|
||||
ServerCode string
|
||||
ToolName string
|
||||
}
|
||||
|
||||
type RuntimeTraceHandler struct {
|
||||
*adk.BaseChatModelAgentMiddleware
|
||||
collector *RuntimeTraceCollector
|
||||
toolMetadataBy map[string]ToolMetadata
|
||||
}
|
||||
|
||||
func NewRuntimeTraceHandler(collector *RuntimeTraceCollector, toolMetadataBy map[string]ToolMetadata) *RuntimeTraceHandler {
|
||||
return &RuntimeTraceHandler{
|
||||
BaseChatModelAgentMiddleware: &adk.BaseChatModelAgentMiddleware{},
|
||||
collector: collector,
|
||||
toolMetadataBy: toolMetadataBy,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *RuntimeTraceHandler) WrapInvokableToolCall(_ context.Context, endpoint adk.InvokableToolCallEndpoint, tCtx *adk.ToolContext) (adk.InvokableToolCallEndpoint, error) {
|
||||
return func(ctx context.Context, argumentsInJSON string, opts ...einotool.Option) (string, error) {
|
||||
startedAt := time.Now()
|
||||
result, err := endpoint(ctx, argumentsInJSON, opts...)
|
||||
item := ToolTraceItem{
|
||||
ResultPreview: previewToolText(result, 300),
|
||||
LatencyMs: time.Since(startedAt).Milliseconds(),
|
||||
Status: "ok",
|
||||
}
|
||||
if tCtx != nil {
|
||||
item.ToolName = strings.TrimSpace(tCtx.Name)
|
||||
if metadata, ok := h.toolMetadataBy[item.ToolName]; ok {
|
||||
item.ToolCode = metadata.ToolCode
|
||||
item.ServerCode = metadata.ServerCode
|
||||
item.ToolName = metadata.ToolName
|
||||
}
|
||||
}
|
||||
if arguments := parseToolArguments(argumentsInJSON); len(arguments) > 0 {
|
||||
item.Arguments = arguments
|
||||
}
|
||||
if err != nil {
|
||||
item.Status = "error"
|
||||
item.ErrorMessage = err.Error()
|
||||
}
|
||||
h.collector.AddToolItem(item)
|
||||
return result, err
|
||||
}, nil
|
||||
}
|
||||
|
||||
func parseToolArguments(argumentsInJSON string) map[string]any {
|
||||
argumentsInJSON = strings.TrimSpace(argumentsInJSON)
|
||||
if argumentsInJSON == "" {
|
||||
return nil
|
||||
}
|
||||
ret := make(map[string]any)
|
||||
if err := json.Unmarshal([]byte(argumentsInJSON), &ret); err != nil {
|
||||
return nil
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func previewToolText(text string, limit int) string {
|
||||
if limit <= 0 {
|
||||
return ""
|
||||
}
|
||||
text = strings.TrimSpace(text)
|
||||
runes := []rune(text)
|
||||
if len(runes) <= limit {
|
||||
return text
|
||||
}
|
||||
return string(runes[:limit]) + "..."
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package callbacks
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type RuntimeTraceCollector struct {
|
||||
mu sync.Mutex
|
||||
Data RuntimeTraceData
|
||||
}
|
||||
|
||||
func NewRuntimeTraceCollector() *RuntimeTraceCollector {
|
||||
ret := &RuntimeTraceCollector{}
|
||||
ret.Data.Version = "v1"
|
||||
ret.Data.Status = "started"
|
||||
return ret
|
||||
}
|
||||
|
||||
func (c *RuntimeTraceCollector) Marshal() string {
|
||||
if c == nil {
|
||||
return ""
|
||||
}
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
buf, err := json.Marshal(c.Data)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return string(buf)
|
||||
}
|
||||
|
||||
func (c *RuntimeTraceCollector) AddToolItem(item ToolTraceItem) {
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.Data.Tools.Count++
|
||||
c.Data.Tools.Items = append(c.Data.Tools.Items, item)
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package callbacks
|
||||
|
||||
type ToolTraceItem struct {
|
||||
ToolCode string `json:"toolCode"`
|
||||
ServerCode string `json:"serverCode"`
|
||||
ToolName string `json:"toolName"`
|
||||
Arguments map[string]any `json:"arguments,omitempty"`
|
||||
ResultPreview string `json:"resultPreview,omitempty"`
|
||||
LatencyMs int64 `json:"latencyMs,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
ErrorMessage string `json:"errorMessage,omitempty"`
|
||||
}
|
||||
|
||||
type RetrieverTraceItem struct {
|
||||
Query string `json:"query,omitempty"`
|
||||
KnowledgeBaseID int64 `json:"knowledgeBaseId,omitempty"`
|
||||
DocumentID int64 `json:"documentId,omitempty"`
|
||||
DocumentTitle string `json:"documentTitle,omitempty"`
|
||||
Score float64 `json:"score,omitempty"`
|
||||
LatencyMs int64 `json:"latencyMs,omitempty"`
|
||||
}
|
||||
|
||||
type RuntimeTraceData struct {
|
||||
Version string `json:"version"`
|
||||
Status string `json:"status"`
|
||||
RunID string `json:"runId,omitempty"`
|
||||
Interrupt struct {
|
||||
CheckPointID string `json:"checkPointId,omitempty"`
|
||||
Items []InterruptTraceContext `json:"items,omitempty"`
|
||||
} `json:"interrupt"`
|
||||
Model struct {
|
||||
Provider string `json:"provider,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
} `json:"model"`
|
||||
Input struct {
|
||||
HistoryMessageCount int `json:"historyMessageCount,omitempty"`
|
||||
KnowledgeBaseIDs []int64 `json:"knowledgeBaseIds,omitempty"`
|
||||
ToolCodes []string `json:"toolCodes,omitempty"`
|
||||
CurrentUserMessagePreview string `json:"currentUserMessagePreview,omitempty"`
|
||||
} `json:"input"`
|
||||
Retriever struct {
|
||||
Count int `json:"count,omitempty"`
|
||||
Items []RetrieverTraceItem `json:"items,omitempty"`
|
||||
} `json:"retriever"`
|
||||
Tools struct {
|
||||
Count int `json:"count,omitempty"`
|
||||
Items []ToolTraceItem `json:"items,omitempty"`
|
||||
} `json:"tools"`
|
||||
Output struct {
|
||||
ReplyText string `json:"replyText,omitempty"`
|
||||
FinishReason string `json:"finishReason,omitempty"`
|
||||
} `json:"output"`
|
||||
Error struct {
|
||||
Message string `json:"message,omitempty"`
|
||||
Stage string `json:"stage,omitempty"`
|
||||
} `json:"error"`
|
||||
}
|
||||
|
||||
type InterruptTraceContext struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
ID string `json:"id"`
|
||||
InfoPreview string `json:"infoPreview,omitempty"`
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package retrievers
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"cs-agent/internal/ai/rag"
|
||||
"cs-agent/internal/models"
|
||||
"cs-agent/internal/pkg/utils"
|
||||
)
|
||||
|
||||
type KnowledgeRetriever struct {
|
||||
AIAgent *models.AIAgent
|
||||
}
|
||||
|
||||
func NewKnowledgeRetriever(aiAgent *models.AIAgent) *KnowledgeRetriever {
|
||||
return &KnowledgeRetriever{AIAgent: aiAgent}
|
||||
}
|
||||
|
||||
func (r *KnowledgeRetriever) KnowledgeBaseIDs() []int64 {
|
||||
if r == nil || r.AIAgent == nil {
|
||||
return nil
|
||||
}
|
||||
return utils.SplitInt64s(r.AIAgent.KnowledgeIDs)
|
||||
}
|
||||
|
||||
func (r *KnowledgeRetriever) Retrieve(ctx context.Context, query string) ([]rag.RetrieveResult, *rag.RetrieveTrace, error) {
|
||||
ids := r.KnowledgeBaseIDs()
|
||||
return rag.Retrieve.RetrieveWithTrace(ctx, rag.RetrieveRequest{
|
||||
Query: query,
|
||||
KnowledgeBaseIDs: ids,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"time"
|
||||
|
||||
"cs-agent/internal/models"
|
||||
"cs-agent/internal/repositories"
|
||||
|
||||
"github.com/cloudwego/eino/adk"
|
||||
"github.com/mlogclub/simple/sqls"
|
||||
)
|
||||
|
||||
var DefaultCheckPointStore adk.CheckPointStore = NewDBCheckPointStore()
|
||||
|
||||
type DBCheckPointStore struct{}
|
||||
|
||||
func NewDBCheckPointStore() *DBCheckPointStore {
|
||||
return &DBCheckPointStore{}
|
||||
}
|
||||
|
||||
func (s *DBCheckPointStore) Get(_ context.Context, checkPointID string) ([]byte, bool, error) {
|
||||
item := repositories.ConversationInterruptRepository.GetByCheckPointID(sqls.DB(), checkPointID)
|
||||
if item == nil || item.CheckPointData == "" {
|
||||
return nil, false, nil
|
||||
}
|
||||
return decodeCheckPointData(item.CheckPointData)
|
||||
}
|
||||
|
||||
func (s *DBCheckPointStore) Set(_ context.Context, checkPointID string, checkPoint []byte) error {
|
||||
item := repositories.ConversationInterruptRepository.GetByCheckPointID(sqls.DB(), checkPointID)
|
||||
if item == nil {
|
||||
item = buildEmptyInterrupt(checkPointID)
|
||||
}
|
||||
item.CheckPointData = encodeCheckPointData(checkPoint)
|
||||
if item.ConversationID == 0 && item.AIAgentID == 0 && item.SourceMessageID == 0 && item.Status == "" {
|
||||
return repositories.ConversationInterruptRepository.Create(sqls.DB(), item)
|
||||
}
|
||||
return repositories.ConversationInterruptRepository.UpsertByCheckPointID(sqls.DB(), item)
|
||||
}
|
||||
|
||||
func encodeCheckPointData(data []byte) string {
|
||||
if len(data) == 0 {
|
||||
return ""
|
||||
}
|
||||
return base64.StdEncoding.EncodeToString(data)
|
||||
}
|
||||
|
||||
func decodeCheckPointData(value string) ([]byte, bool, error) {
|
||||
if value == "" {
|
||||
return nil, false, nil
|
||||
}
|
||||
data, err := base64.StdEncoding.DecodeString(value)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
return data, true, nil
|
||||
}
|
||||
|
||||
func buildEmptyInterrupt(checkPointID string) *models.ConversationInterrupt {
|
||||
now := time.Now()
|
||||
return &models.ConversationInterrupt{
|
||||
CheckPointID: checkPointID,
|
||||
Status: "checkpointed",
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user