diff --git a/eino-project/cmd/server/__debug_bin2704070066 b/eino-project/cmd/server/__debug_bin2704070066 new file mode 100644 index 0000000..fdcc57f Binary files /dev/null and b/eino-project/cmd/server/__debug_bin2704070066 differ diff --git a/eino-project/cmd/server/__debug_bin4041733900 b/eino-project/cmd/server/__debug_bin4041733900 new file mode 100644 index 0000000..1970e67 Binary files /dev/null and b/eino-project/cmd/server/__debug_bin4041733900 differ diff --git a/eino-project/cmd/server/wire_gen.go b/eino-project/cmd/server/wire_gen.go index fb811b9..f5d39e9 100644 --- a/eino-project/cmd/server/wire_gen.go +++ b/eino-project/cmd/server/wire_gen.go @@ -12,6 +12,7 @@ import ( "eino-project/internal/data" "eino-project/internal/data/repoimpl" "eino-project/internal/domain/context" + "eino-project/internal/domain/llm" "eino-project/internal/domain/monitor" "eino-project/internal/domain/session" "eino-project/internal/domain/vector" @@ -44,8 +45,10 @@ func wireApp(confServer *conf.Server, confData *conf.Data, bootstrap *conf.Boots customerRepo := repoimpl.NewCustomerRepo(dataData, logger) customerUseCase := biz.NewCustomerUseCase(customerRepo, logger, knowledgeSearcher, documentProcessor, contextManager, monitorMonitor) sessionManager := session.NewMemorySessionManager(logger) - customerService := service.NewCustomerService(customerUseCase, sessionManager, monitorMonitor, logger) - httpServer := server.NewHTTPServer(confServer, customerService, logger) + llmLLM := llm.NewLLM(bootstrap) + customerService := service.NewCustomerService(customerUseCase, sessionManager, monitorMonitor, logger, llmLLM) + agentService := service.NewAgentService(logger, llmLLM) + httpServer := server.NewHTTPServer(confServer, customerService, agentService, logger) app := newApp(logger, httpServer) return app, func() { cleanup() diff --git a/eino-project/docker-compose.yml b/eino-project/docker-compose.yml index 1f9dded..94260a1 100644 --- a/eino-project/docker-compose.yml +++ b/eino-project/docker-compose.yml @@ -38,23 +38,23 @@ services: - eino-network # Coze-Loop 监控服务 - coze-loop: - image: cozedev/coze-loop:latest - container_name: eino-coze-loop - ports: - - "8080:8080" - environment: - - COZE_LOOP_PORT=8080 - - COZE_LOOP_HOST=0.0.0.0 - volumes: - - coze_data:/app/data - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:8080/health"] - interval: 20s - timeout: 5s - retries: 3 - networks: - - eino-network + # coze-loop: + # image: cozedev/coze-loop:latest + # container_name: eino-coze-loop + # ports: + # - "8080:8080" + # environment: + # - COZE_LOOP_PORT=8080 + # - COZE_LOOP_HOST=0.0.0.0 + # volumes: + # - coze_data:/app/data + # healthcheck: + # test: ["CMD", "curl", "-f", "http://localhost:8080/health"] + # interval: 20s + # timeout: 5s + # retries: 3 + # networks: + # - eino-network # 智能客服后端服务 eino-service: diff --git a/eino-project/go.mod b/eino-project/go.mod index 565af73..8f763da 100644 --- a/eino-project/go.mod +++ b/eino-project/go.mod @@ -10,6 +10,7 @@ require ( github.com/coze-dev/cozeloop-go v0.1.15 github.com/go-kratos/kratos/v2 v2.8.0 github.com/google/wire v0.6.0 + github.com/gorilla/websocket v1.5.3 github.com/mattn/go-sqlite3 v1.14.22 github.com/redis/go-redis/v9 v9.14.1 go.uber.org/automaxprocs v1.5.1 @@ -46,7 +47,6 @@ require ( github.com/google/uuid v1.6.0 // indirect github.com/goph/emperror v0.17.2 // indirect github.com/gorilla/mux v1.8.1 // indirect - github.com/gorilla/websocket v1.5.3 // indirect github.com/invopop/yaml v0.1.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect diff --git a/eino-project/go.sum b/eino-project/go.sum index 2599002..c6f0805 100644 --- a/eino-project/go.sum +++ b/eino-project/go.sum @@ -1,5 +1,3 @@ -cel.dev/expr v0.16.1 h1:NR0+oFYzR1CqLFhTAqg3ql59G9VfN8fKq1TCHJ6gq1g= -cel.dev/expr v0.16.1/go.mod h1:AsGA5zb3WruAEQeQng1RZdGEXmBj0jvMWh6l5SnNuC8= dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= @@ -27,8 +25,6 @@ github.com/bytedance/sonic v1.14.1 h1:FBMC0zVz5XUmE4z9wF4Jey0An5FueFvOsTKKKtwIl7 github.com/bytedance/sonic v1.14.1/go.mod h1:gi6uhQLMbTdeP0muCnrjHLeCUPyb70ujhnNlhOylAFc= github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= -github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g= -github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= github.com/certifi/gocertifi v0.0.0-20190105021004-abcd57078448/go.mod h1:GJKEexRPVJrBSOjoqN5VNOIKJ5Q3RViH6eu3puDRwx4= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -38,8 +34,6 @@ github.com/cloudwego/eino v0.5.10 h1:/U5el2Ky5nKY1mE3nKHh8fnPKka5xlC19PW0s/jvbxw github.com/cloudwego/eino v0.5.10/go.mod h1:N6E+toMzWw/3ql0IVM5n5lbYFCeblCYx7ebH16kt1JQ= github.com/cloudwego/eino-ext/components/model/ollama v0.1.5 h1:afBsBFwk8cRT3RT5LOW8jTdV4uZipCQpNQuBwM1zWL4= github.com/cloudwego/eino-ext/components/model/ollama v0.1.5/go.mod h1:agEXRRhFGNcTjaryXKNYxI58niFnVws5ipOz5QkScIc= -github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 h1:QVw89YDxXxEe+l8gU8ETbOasdwEV+avkR75ZzsVV9WI= -github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/coze-dev/cozeloop-go v0.1.15 h1:oUQ7U1h4AyPd1IUR+Ob7TDtby/cjhZvBz+vEH7obncI= github.com/coze-dev/cozeloop-go v0.1.15/go.mod h1:lM7cmUEZlnAlQYdwfk4Li0SC3RdZ++QMHX75nvKceSc= github.com/coze-dev/cozeloop-go/spec v0.1.4-0.20250829072213-3812ddbfb735 h1:qxAwjHy0SLQazDO3oGJ8D24vOeM2Oz2+n27bNPegBls= @@ -55,10 +49,6 @@ github.com/eino-contrib/jsonschema v1.0.2 h1:HaxruBMUdnXa7Lg/lX8g0Hk71ZIfdTZXmBQ github.com/eino-contrib/jsonschema v1.0.2/go.mod h1:cpnX4SyKjWjGC7iN2EbhxaTdLqGjCi0e9DxpLYxddD4= github.com/eino-contrib/ollama v0.1.0 h1:z1NaMdKW6X1ftP8g5xGGR5zDRPUtuTKFq35vBQgxsN4= github.com/eino-contrib/ollama v0.1.0/go.mod h1:mYsQ7b3DeqY8bHPuD3MZJYTqkgyL6LoemxoP/B7ZNhA= -github.com/envoyproxy/go-control-plane v0.13.0 h1:HzkeUz1Knt+3bK+8LG1bxOO/jzWZmdxpwC51i202les= -github.com/envoyproxy/go-control-plane v0.13.0/go.mod h1:GRaKG3dwvFoTg4nj7aXdZnvMg4d7nvT/wl9WgVXn3Q8= -github.com/envoyproxy/protoc-gen-validate v1.1.0 h1:tntQDh69XqOCOZsDz0lVJQez/2L6Uu2PdjCQwWCJ3bM= -github.com/envoyproxy/protoc-gen-validate v1.1.0/go.mod h1:sXRDRVmzEbkM7CVcM06s9shE/m23dg3wzjl0UWqJ2q4= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= @@ -174,8 +164,6 @@ github.com/perimeterx/marshmallow v1.1.4/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0V github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.2-0.20201214064552-5dd12d0cfe7f h1:lJqhwddJVYAkyp72a4pwzMClI20xTwL7miDdm2W/KBM= github.com/pkg/errors v0.9.2-0.20201214064552-5dd12d0cfe7f/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= -github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= @@ -239,6 +227,8 @@ go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= go.uber.org/automaxprocs v1.5.1 h1:e1YG66Lrk73dn4qhg8WFSvhF0JuFQF0ERIp4rpuV8Qk= go.uber.org/automaxprocs v1.5.1/go.mod h1:BF4eumQw0P9GtnuxxovUd06vwm1o18oMzFtK66vU6XU= +go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= +go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= golang.org/x/arch v0.11.0 h1:KXV8WWKCXm6tRpLirl2szsO5j/oOODwZf4hATmGVNs4= golang.org/x/arch v0.11.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= diff --git a/eino-project/internal/domain/agent/intent.go b/eino-project/internal/domain/agent/intent.go new file mode 100644 index 0000000..5d1be90 --- /dev/null +++ b/eino-project/internal/domain/agent/intent.go @@ -0,0 +1,34 @@ +package agent + +import ( + "context" + "log" + + "eino-project/internal/domain/llm" + + "github.com/cloudwego/eino/adk" +) + +func NewIntentAgent(ctx context.Context, models llm.LLM) adk.Agent { + intentModel, err := models.Intent() + if err != nil { + log.Fatal(err) + } + a, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{ + Name: "意图识别智能体", + Description: "根据用户输入识别意图", + Instruction: ` + # 你是一个意图识别智能体,根据用户输入识别用户的意图。 + - 当用户输入商品相关时,意图为"商品查询"(product) + - 当用户输入订单相关时,意图为"订单诊断"(order) + - 当用户输入其他问题时,意图为"其他"(other) + - 输出结构为: {"intent": "product|order|other"} + `, + Model: intentModel, + OutputKey: "intent", + }) + if err != nil { + log.Fatal(err) + } + return a +} diff --git a/eino-project/internal/domain/agent/intent/intent.go b/eino-project/internal/domain/agent/intent/intent.go deleted file mode 100644 index 4125e5a..0000000 --- a/eino-project/internal/domain/agent/intent/intent.go +++ /dev/null @@ -1,17 +0,0 @@ -package intent - -import ( - "context" -) - -type IntentAgent interface { - Classify(ctx context.Context, message string) (string, error) -} - -type passthroughAgent struct{} - -func NewPassthrough() IntentAgent { return &passthroughAgent{} } - -func (p *passthroughAgent) Classify(ctx context.Context, message string) (string, error) { - return "general_inquiry", nil -} \ No newline at end of file diff --git a/eino-project/internal/domain/agent/product.go b/eino-project/internal/domain/agent/product.go new file mode 100644 index 0000000..f1303b0 --- /dev/null +++ b/eino-project/internal/domain/agent/product.go @@ -0,0 +1,48 @@ +package agent + +import ( + "context" + + "eino-project/internal/domain/llm" + "eino-project/internal/domain/tools" + + "github.com/cloudwego/eino/adk" + "github.com/cloudwego/eino/components/tool" + "github.com/cloudwego/eino/compose" +) + +// NewProductChatAgent 使用 ADK ChatModelAgent 构造一个具备工具选择能力的商品查询 Agent +func NewProductChatAgent(ctx context.Context, models llm.LLM) adk.Agent { + chatModel, err := models.Chat() + if err != nil { + return nil + } + toolsCfg := adk.ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{ + tools.NewProductByIDTool(), + tools.NewProductSearchByNameTool(), + }, + ExecuteSequentially: false, + }, + // ReturnDirectly: map[string]bool{ + // "get_product_by_id": true, + // "search_product_by_name": true, + // }, + } + agent, _ := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{ + Name: "商品查询智能体", + Description: "根据用户输入查询商品信息,支持按ID或名称检索", + Model: chatModel, + ToolsConfig: toolsCfg, + Instruction: ` + # 你是一个商品查询智能体,根据用户输入查询商品信息。 + - 当用户输入商品ID时,优先使用"get_product_by_id"工具查询商品详情。 + - 当用户输入商品名称时,使用"search_product_by_name"工具搜索相关商品。 + - 当用户需要查询多个结果时,合并所有结果返回。 + - 输出结构为: {"items": [...], "source": "id|name", "count": int} 无需解释文本 + `, + }) + + return agent +} diff --git a/eino-project/internal/domain/agent/product/handler.go b/eino-project/internal/domain/agent/product/handler.go deleted file mode 100644 index 8681ed8..0000000 --- a/eino-project/internal/domain/agent/product/handler.go +++ /dev/null @@ -1,63 +0,0 @@ -package product - -import ( - "encoding/json" - "net/http" - "strings" - - tool "eino-project/internal/tools/product" -) - -type Handler struct { - tool tool.Tool -} - -func NewHandler(t tool.Tool) *Handler { return &Handler{tool: t} } - -type queryRequest struct { - Query struct { - ID string `json:"id"` - Name string `json:"name"` - } `json:"query"` - Page int `json:"page,omitempty"` - Size int `json:"size,omitempty"` -} - -type queryResponse struct { - Items []*tool.Product `json:"items"` - Source string `json:"source"` - Count int `json:"count"` -} - -func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) - return - } - - var req queryRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, "Invalid request body", http.StatusBadRequest) - return - } - - var items []*tool.Product - src := "" - - if id := strings.TrimSpace(req.Query.ID); id != "" { - if p, _ := h.tool.GetByID(r.Context(), id); p != nil { - items = append(items, p) - } - src = "id" - } else if name := strings.TrimSpace(req.Query.Name); name != "" { - res, _ := h.tool.SearchByName(r.Context(), name) - items = res - src = "name" - } else { - http.Error(w, "query.id or query.name required", http.StatusBadRequest) - return - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(&queryResponse{Items: items, Source: src, Count: len(items)}) -} \ No newline at end of file diff --git a/eino-project/internal/domain/llm/llm.go b/eino-project/internal/domain/llm/llm.go index 36fa9fb..4496bfc 100644 --- a/eino-project/internal/domain/llm/llm.go +++ b/eino-project/internal/domain/llm/llm.go @@ -8,25 +8,25 @@ import ( ) type LLM interface { - Chat() (model.BaseChatModel, error) - Intent() (model.BaseChatModel, error) + Chat() (model.ToolCallingChatModel, error) + Intent() (model.ToolCallingChatModel, error) } type llm struct { cfg *conf.Bootstrap onceChat sync.Once onceIntent sync.Once - chat model.BaseChatModel - intent model.BaseChatModel - cache map[string]model.BaseChatModel + chat model.ToolCallingChatModel + intent model.ToolCallingChatModel + cache map[string]model.ToolCallingChatModel } func NewLLM(cfg *conf.Bootstrap) LLM { - return &llm{cfg: cfg, cache: make(map[string]model.BaseChatModel)} + return &llm{cfg: cfg, cache: make(map[string]model.ToolCallingChatModel)} } // 获取Ollama聊天模型实例 -func (r *llm) Chat() (model.BaseChatModel, error) { +func (r *llm) Chat() (model.ToolCallingChatModel, error) { var err error r.onceChat.Do(func() { r.chat, err = newOllamaChatModel(r.cfg) @@ -35,7 +35,7 @@ func (r *llm) Chat() (model.BaseChatModel, error) { } // 获取Ollama意图识别模型实例 -func (r *llm) Intent() (model.BaseChatModel, error) { +func (r *llm) Intent() (model.ToolCallingChatModel, error) { var err error r.onceIntent.Do(func() { r.intent, err = newOllamaIntentModel(r.cfg) diff --git a/eino-project/internal/domain/llm/ollama_chat.go b/eino-project/internal/domain/llm/ollama_chat.go index a6b2e02..4e58325 100644 --- a/eino-project/internal/domain/llm/ollama_chat.go +++ b/eino-project/internal/domain/llm/ollama_chat.go @@ -12,7 +12,7 @@ import ( ) // newOllamaChatModel Ollama聊天模型实例 -func newOllamaChatModel(c *conf.Bootstrap) (model.BaseChatModel, error) { +func newOllamaChatModel(c *conf.Bootstrap) (model.ToolCallingChatModel, error) { if c == nil || c.Ai == nil || c.Ai.Ollama == nil { return nil, fmt.Errorf("AI configuration is missing") } diff --git a/eino-project/internal/domain/llm/ollama_intent.go b/eino-project/internal/domain/llm/ollama_intent.go index 382d92c..f66624d 100644 --- a/eino-project/internal/domain/llm/ollama_intent.go +++ b/eino-project/internal/domain/llm/ollama_intent.go @@ -12,7 +12,7 @@ import ( ) // newOllamaIntentModel Ollama意图识别模型实例 -func newOllamaIntentModel(c *conf.Bootstrap) (model.BaseChatModel, error) { +func newOllamaIntentModel(c *conf.Bootstrap) (model.ToolCallingChatModel, error) { if c == nil || c.Ai == nil || c.Ai.Ollama == nil { return nil, fmt.Errorf("AI configuration is missing") } diff --git a/eino-project/internal/domain/provier_set.go b/eino-project/internal/domain/provier_set.go index 8e481f6..a42b15b 100644 --- a/eino-project/internal/domain/provier_set.go +++ b/eino-project/internal/domain/provier_set.go @@ -1,15 +1,14 @@ package domain import ( - "eino-project/internal/domain/context" - "eino-project/internal/domain/llm" - "eino-project/internal/domain/monitor" - "eino-project/internal/domain/session" - "eino-project/internal/domain/tools" - "eino-project/internal/domain/vector" - "eino-project/internal/domain/workflow" + "eino-project/internal/domain/context" + "eino-project/internal/domain/llm" + "eino-project/internal/domain/monitor" + "eino-project/internal/domain/session" + "eino-project/internal/domain/vector" + "eino-project/internal/domain/workflow" - "github.com/google/wire" + "github.com/google/wire" ) // ProviderSet is domain providers. @@ -18,7 +17,6 @@ var ProviderSet = wire.NewSet( llm.NewLLM, monitor.NewMonitorFromBootstrapConfig, session.NewMemorySessionManager, - tools.NewMemoryProductTool, workflow.NewChatWorkflow, vector.NewVectorServiceFromBootstrapConfig, diff --git a/eino-project/internal/domain/tools/product.go b/eino-project/internal/domain/tools/product.go index b66729f..5af19a0 100644 --- a/eino-project/internal/domain/tools/product.go +++ b/eino-project/internal/domain/tools/product.go @@ -3,8 +3,20 @@ package tools import ( "context" "strings" + + "github.com/cloudwego/eino/components/tool" + "github.com/cloudwego/eino/components/tool/utils" ) +var productsMock = []*Product{ + {ID: "P001", Name: "手机 A1", Price: 1999, Description: "入门级智能手机"}, + {ID: "P002", Name: "手机 Pro X", Price: 5999, Description: "旗舰级拍照手机"}, + {ID: "P003", Name: "笔记本 M3", Price: 8999, Description: "轻薄高性能笔记本"}, + {ID: "P004", Name: "耳机 Air", Price: 1299, Description: "降噪真无线耳机"}, + {ID: "271", Name: "商品 271", Price: 2710, Description: "样例商品用于测试按ID查询"}, +} + +// 工具返回 type Product struct { ID string `json:"id"` Name string `json:"name"` @@ -12,40 +24,49 @@ type Product struct { Description string `json:"description"` } -type Tool interface { - GetByID(ctx context.Context, id string) (*Product, error) - SearchByName(ctx context.Context, name string) ([]*Product, error) +// --- Eino ADK Tool 实现:按ID查询 --- +type ProductByIDInput struct { + ID string `json:"id" jsonschema:"description=商品ID"` } -type memoryTool struct { - items []*Product +func NewProductByIDTool() tool.InvokableTool { + toolImpl, err := utils.InferTool("get_product_by_id", "根据商品ID查询商品详情,返回商品对象。", productByID) + if err != nil { + panic(err) + } + return toolImpl } -func NewMemoryProductTool() Tool { - return &memoryTool{items: []*Product{ - {ID: "P001", Name: "手机 A1", Price: 1999, Description: "入门级智能手机"}, - {ID: "P002", Name: "手机 Pro X", Price: 5999, Description: "旗舰级拍照手机"}, - {ID: "P003", Name: "笔记本 M3", Price: 8999, Description: "轻薄高性能笔记本"}, - {ID: "P004", Name: "耳机 Air", Price: 1299, Description: "降噪真无线耳机"}, - }} -} - -func (m *memoryTool) GetByID(ctx context.Context, id string) (*Product, error) { - for _, it := range m.items { - if it.ID == id { +func productByID(ctx context.Context, in *ProductByIDInput) (*Product, error) { + for _, it := range productsMock { + if it.ID == in.ID { return it, nil } } - return nil, nil + return &Product{}, nil } -func (m *memoryTool) SearchByName(ctx context.Context, name string) ([]*Product, error) { - if name == "" { - return nil, nil +// --- Eino ADK Tool 实现:按名称搜索 --- + +type ProductSearchInput struct { + Name string `json:"name" jsonschema:"description=商品名称关键词"` +} + +func NewProductSearchByNameTool() tool.InvokableTool { + toolImpl, err := utils.InferTool("search_product_by_name", "根据商品名称关键词搜索商品列表,返回数组。", productSearchByName) + if err != nil { + panic(err) } - q := strings.ToLower(name) + return toolImpl +} + +func productSearchByName(ctx context.Context, in *ProductSearchInput) ([]*Product, error) { + if in.Name == "" { + return []*Product{}, nil + } + q := strings.ToLower(in.Name) var out []*Product - for _, it := range m.items { + for _, it := range productsMock { if strings.Contains(strings.ToLower(it.Name), q) { out = append(out, it) } diff --git a/eino-project/internal/pkg/adkutil/adkutil.go b/eino-project/internal/pkg/adkutil/adkutil.go new file mode 100644 index 0000000..0a6f89c --- /dev/null +++ b/eino-project/internal/pkg/adkutil/adkutil.go @@ -0,0 +1,198 @@ +package adkutil + +import ( + "context" + "encoding/json" + "errors" + + "github.com/cloudwego/eino/adk" + "github.com/cloudwego/eino/schema" + krlog "github.com/go-kratos/kratos/v2/log" +) + +type Result struct { + Customized any + Message *schema.Message +} + +func QueryJSON[T any](ctx context.Context, agent adk.Agent, query string) (*T, error) { + r, err := Query(ctx, agent, query) + if err != nil { + return nil, err + } + if r.Customized != nil { + if v, ok := r.Customized.(*T); ok { + return v, nil + } + b, _ := json.Marshal(r.Customized) + var out T + if json.Unmarshal(b, &out) == nil { + return &out, nil + } + } + if r.Message != nil && r.Message.Content != "" { + var out T + if json.Unmarshal([]byte(r.Message.Content), &out) == nil { + return &out, nil + } + } + return nil, errors.New("agent output not match target type") +} + +func QueryWithLogger(ctx context.Context, agent adk.Agent, query string, logger *krlog.Helper) (Result, error) { + runner := adk.NewRunner(ctx, adk.RunnerConfig{Agent: agent}) + it := runner.Query(ctx, query) + var out Result + var lastErr error + if logger != nil { + logger.Infof("agent query start: %s", query) + } + for { + ev, ok := it.Next() + if !ok || ev == nil { + break + } + if logger != nil { + logger.Infof("agent event received: err=%v", ev.Err) + } + if ev.Err != nil { + lastErr = ev.Err + } + if ev.Output != nil { + if ev.Output.CustomizedOutput != nil { + out.Customized = ev.Output.CustomizedOutput + if logger != nil { + b, _ := json.Marshal(ev.Output.CustomizedOutput) + logger.Infof("agent customized output=%s", string(b)) + } + } + if ev.Output.MessageOutput != nil { + msg, _ := ev.Output.MessageOutput.GetMessage() + if msg != nil { + out.Message = msg + if logger != nil { + logger.Infof("agent message role=%s content=%s", msg.Role, msg.Content) + if len(msg.ToolCalls) > 0 { + for _, tc := range msg.ToolCalls { + if tc.Function.Name != "" { + logger.Infof("agent tool call name=%s args=%s", tc.Function.Name, tc.Function.Arguments) + } + } + } + } + } + } + } + } + if out.Customized != nil || out.Message != nil { + return out, nil + } + if lastErr != nil { + return out, lastErr + } + return out, errors.New("agent no output") +} + +func QueryJSONWithLogger[T any](ctx context.Context, agent adk.Agent, query string, logger *krlog.Helper) (*T, error) { + r, err := QueryWithLogger(ctx, agent, query, logger) + if err != nil { + return nil, err + } + if r.Customized != nil { + if v, ok := r.Customized.(*T); ok { + return v, nil + } + b, _ := json.Marshal(r.Customized) + var out T + if json.Unmarshal(b, &out) == nil { + return &out, nil + } + } + if r.Message != nil && r.Message.Content != "" { + var out T + if json.Unmarshal([]byte(r.Message.Content), &out) == nil { + return &out, nil + } + } + return nil, errors.New("agent output not match target type") +} + +// Query 对 Agent 发起一次非流式请求,并提取统一结果 +// - 优先返回工具的 CustomizedOutput(结构化输出) +// - 若无,则回退到最终的 MessageOutput(文本或可解析JSON) +// - 若均无,则返回错误(agent no output) +func Query(ctx context.Context, agent adk.Agent, query string) (Result, error) { + runner := adk.NewRunner(ctx, adk.RunnerConfig{Agent: agent}) + it := runner.Query(ctx, query) + var out Result + var lastErr error + for { + ev, ok := it.Next() + if !ok || ev == nil { + break + } + if ev.Err != nil { + lastErr = ev.Err + break + } + if ev.Output != nil { + if ev.Output.CustomizedOutput != nil { + out.Customized = ev.Output.CustomizedOutput + } + if ev.Output.MessageOutput != nil { + msg, _ := ev.Output.MessageOutput.GetMessage() + if msg != nil { + out.Message = msg + } + } + } + } + if out.Customized != nil || out.Message != nil { + return out, nil + } + if lastErr != nil { + return out, lastErr + } + return out, errors.New("agent no output") +} + +// ToPayload 将 Query 的结果转换为可直接返回的 payload +// - CustomizedOutput 直接返回 +// - MessageOutput 尝试当作 JSON 解析,失败则包装为 {"message": text} +func ToPayload(res Result) any { + if res.Customized != nil { + return res.Customized + } + if res.Message != nil { + var obj any + if json.Unmarshal([]byte(res.Message.Content), &obj) == nil { + return obj + } + return map[string]any{"message": res.Message.Content} + } + return nil +} + +// Stream 以流式方式消费 Agent 输出,将 MessageOutput 的内容按片段写入 channel +// 用于 SSE/WS 等场景;工具直接返回通常会合并到最终消息 +func Stream(ctx context.Context, agent adk.Agent, query string) (<-chan string, error) { + runner := adk.NewRunner(ctx, adk.RunnerConfig{Agent: agent, EnableStreaming: true}) + it := runner.Query(ctx, query) + ch := make(chan string, 8) + go func() { + defer close(ch) + for { + ev, ok := it.Next() + if !ok || ev == nil { + break + } + if ev.Output != nil && ev.Output.MessageOutput != nil { + msg, _ := ev.Output.MessageOutput.GetMessage() + if msg != nil && msg.Content != "" { + ch <- msg.Content + } + } + } + }() + return ch, nil +} diff --git a/eino-project/internal/server/http.go b/eino-project/internal/server/http.go index 171b739..c1749ad 100644 --- a/eino-project/internal/server/http.go +++ b/eino-project/internal/server/http.go @@ -14,7 +14,7 @@ import ( ) // NewHTTPServer new an HTTP server. -func NewHTTPServer(c *conf.Server, customerService *service.CustomerService, logger log.Logger) *http.Server { +func NewHTTPServer(c *conf.Server, customerService *service.CustomerService, agentService *service.AgentService, logger log.Logger) *http.Server { var opts = []http.ServerOption{ http.Middleware( recovery.Recovery(), @@ -37,9 +37,6 @@ func NewHTTPServer(c *conf.Server, customerService *service.CustomerService, log // 添加SSE流式聊天的自定义路由 srv.HandleFunc("/api/chat/stream", customerService.HandleStreamChat) - // 商品查询 Agent 路由 - srv.HandleFunc("/api/agents/product/query", customerService.HandleProductQuery) - // WebSocket 聊天路由(/api/chat/ws) srv.HandleFunc("/api/chat/ws", func(w nethttp.ResponseWriter, r *nethttp.Request) { upgrader := websocket.Upgrader{CheckOrigin: func(r *nethttp.Request) bool { return true }} @@ -50,8 +47,11 @@ func NewHTTPServer(c *conf.Server, customerService *service.CustomerService, log customerService.HandleWebSocketChat(conn) }) + route := srv.Route("/api") + // 商品查询 Agent 路由 + route.POST("/agents/product/query", agentService.HandleProductQuery) // 订单诊断工作流(直接调用流水线输出) - srv.HandleFunc("/api/workflow/order/diagnosis", customerService.HandleOrderDiagnosis) + // route.POST("/api/workflow/order/diagnosis", customerService.HandleOrderDiagnosis) return srv } diff --git a/eino-project/internal/service/agent.go b/eino-project/internal/service/agent.go new file mode 100644 index 0000000..e37cd93 --- /dev/null +++ b/eino-project/internal/service/agent.go @@ -0,0 +1,74 @@ +package service + +import ( + "context" + "eino-project/internal/domain/agent" + "eino-project/internal/domain/llm" + "eino-project/internal/pkg/adkutil" + "encoding/json" + "net/http" + + "github.com/cloudwego/eino/adk" + "github.com/go-kratos/kratos/v2/log" + kratoshttp "github.com/go-kratos/kratos/v2/transport/http" +) + +type AgentService struct { + productAgent adk.Agent + log *log.Helper +} + +func NewAgentService( + logger log.Logger, + models llm.LLM, +) *AgentService { + return &AgentService{ + log: log.NewHelper(logger), + // 构建商品查询 ChatModelAgent,绑定工具并让模型自动选择调用 + productAgent: agent.NewProductChatAgent(context.Background(), models), + } +} + +type ProductQueryRes struct { + Items []*ProductItem `json:"items"` + Source string `json:"source"` + Count int `json:"count"` +} +type ProductItem struct { + ID string `json:"id"` + Name string `json:"name"` + Price int `json:"price"` + Description string `json:"description"` +} + +type CommonResp struct { + Code int `json:"code"` + Msg string `json:"msg"` + Data interface{} `json:"data"` +} + +// HandleProductQuery 商品查询处理 +func (a *AgentService) HandleProductQuery(ctx kratoshttp.Context) error { + type reqType struct { + Message string `json:"message"` + Session string `json:"session_id"` + } + + var req reqType + if err := json.NewDecoder(ctx.Request().Body).Decode(&req); err != nil { + return ctx.JSON(http.StatusBadRequest, &CommonResp{ + Code: http.StatusBadRequest, + Msg: "Invalid request body", + }) + } + if a.productAgent == nil { + return ctx.JSON(http.StatusInternalServerError, &CommonResp{Code: http.StatusInternalServerError, Msg: "product agent not configured"}) + } + + out, err := adkutil.QueryJSONWithLogger[ProductQueryRes](ctx, a.productAgent, req.Message, a.log) + if err != nil { + return ctx.JSON(http.StatusInternalServerError, &CommonResp{Code: http.StatusInternalServerError, Msg: err.Error()}) + } + + return ctx.JSON(http.StatusOK, &CommonResp{Code: http.StatusOK, Msg: "success", Data: out}) +} diff --git a/eino-project/internal/service/customer.go b/eino-project/internal/service/customer.go index f140439..299ac5e 100644 --- a/eino-project/internal/service/customer.go +++ b/eino-project/internal/service/customer.go @@ -11,10 +11,13 @@ import ( pb "eino-project/api/customer/v1" "eino-project/internal/biz" + "eino-project/internal/domain/agent" + "eino-project/internal/domain/llm" "eino-project/internal/domain/monitor" "eino-project/internal/domain/session" wf "eino-project/internal/domain/workflow" + "github.com/cloudwego/eino/adk" "github.com/go-kratos/kratos/v2/log" ) @@ -27,16 +30,26 @@ type CustomerService struct { monitor monitor.Monitor log *log.Helper chatWorkflow wf.ChatWorkflow + // productAgent 商品查询Agent(Eino ADK ChatModelAgent实现,绑定工具并自动选择调用) + productAgent adk.Agent } // NewCustomerService 创建智能客服服务 -func NewCustomerService(customerUseCase *biz.CustomerUseCase, sessionManager session.SessionManager, monitor monitor.Monitor, logger log.Logger) *CustomerService { - return &CustomerService{ - customerUseCase: customerUseCase, - sessionManager: sessionManager, - monitor: monitor, - log: log.NewHelper(logger), - } +func NewCustomerService( + customerUseCase *biz.CustomerUseCase, + sessionManager session.SessionManager, + monitor monitor.Monitor, + logger log.Logger, + models llm.LLM, +) *CustomerService { + return &CustomerService{ + customerUseCase: customerUseCase, + sessionManager: sessionManager, + monitor: monitor, + log: log.NewHelper(logger), + // 构建商品查询 ChatModelAgent,绑定工具并让模型自动选择调用 + productAgent: agent.NewProductChatAgent(context.Background(), models), + } } func (s *CustomerService) SetChatWorkflow(w wf.ChatWorkflow) { s.chatWorkflow = w } @@ -603,9 +616,3 @@ func (s *CustomerService) HandleOrderDiagnosis(w http.ResponseWriter, r *http.Re w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{"order_id": req.OrderID, "result": resp}) } - -// HandleProductQuery 商品查询处理 -func (s *CustomerService) HandleProductQuery(w http.ResponseWriter, r *http.Request) { - - json.NewEncoder(w).Encode(map[string]interface{}{}) -} diff --git a/eino-project/internal/service/service.go b/eino-project/internal/service/service.go index 56c7d2c..97485f8 100644 --- a/eino-project/internal/service/service.go +++ b/eino-project/internal/service/service.go @@ -3,4 +3,4 @@ package service import "github.com/google/wire" // ProviderSet is service providers. -var ProviderSet = wire.NewSet(NewCustomerService) +var ProviderSet = wire.NewSet(NewCustomerService, NewAgentService)