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,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,
}
}