feat: 1.增加 eino tool 相关配置,货易通商品上传参数配置化 2. eino tool 注册方法调整

This commit is contained in:
fuzhongyun 2025-12-22 11:14:15 +08:00
parent d0ba329024
commit d8df571cce
12 changed files with 155 additions and 248 deletions

View File

@ -74,11 +74,19 @@ tools:
zltxOrderAfterSaleResellerBatch:
enabled: true
base_url: "https://gateway.dev.cdlsxd.cn/zltx_api/admin/afterSales/reseller_pre_ai"
# eino tool 配置
eino_tools:
# 货易通商品上传
hytProductUpload:
enabled: true
base_url: "https://gateway.dev.cdlsxd.cn/zltx_api/admin/oursProduct"
base_url: "https://gateway.dev.cdlsxd.cn/goods-admin/api/v1/goods/supplier/batch/add/complete"
add_url: "https://gateway.dev.cdlsxd.cn/sw//#/goods/goodsManage"
# 货易通供应商查询
hytSupplierSearch:
base_url: "https://gateway.dev.cdlsxd.cn/goods-admin/api/v1/supplier/list"
# 货易通仓库查询
hytWarehouseSearch:
base_url: "https://gateway.dev.cdlsxd.cn/goods-admin/api/v1/warehouse/list"
default_prompt:
img_recognize:

View File

@ -15,6 +15,7 @@ type Config struct {
Coze CozeConfig `mapstructure:"coze"`
Sys SysConfig `mapstructure:"sys"`
Tools ToolsConfig `mapstructure:"tools"`
EinoTools EinoToolsConfig `mapstructure:"eino_tools"`
Logging LoggingConfig `mapstructure:"logging"`
Redis Redis `mapstructure:"redis"`
DB DB `mapstructure:"db"`
@ -141,8 +142,6 @@ type ToolsConfig struct {
CozeExpress ToolConfig `mapstructure:"cozeExpress"`
// Coze 公司查询工具
CozeCompany ToolConfig `mapstructure:"cozeCompany"`
// 货易通商品上传
HytProductUpload ToolConfig `mapstructure:"hytProductUpload"`
}
// ToolConfig 单个工具配置
@ -155,6 +154,16 @@ type ToolConfig struct {
AddURL string `mapstructure:"add_url"`
}
// EinoToolsConfig eino tool 配置
type EinoToolsConfig struct {
// 货易通商品上传
HytProductUpload ToolConfig `mapstructure:"hytProductUpload"`
// 货易通供应商查询
HytSupplierSearch ToolConfig `mapstructure:"hytSupplierSearch"`
// 货易通仓库查询
HytWarehouseSearch ToolConfig `mapstructure:"hytWarehouseSearch"`
}
// LoggingConfig 日志配置
type LoggingConfig struct {
Level string `mapstructure:"level"`

View File

@ -32,11 +32,6 @@ const (
"货品图片": ["string"], // 商品多图取1-2个即可
"电商销售价格": "string", // 商品电商销售价格 decimal(10,2)
"销售价": "string", // 商品销售价格 decimal(10,2)
"供应商报价": "string", // 商品供应商报价 decimal(10,2)
"税率": "string", // 商品税率 x%
"默认供应商": "string", // 供应商名称
"默认存放仓库": "string", // 仓库名称
"利润": "string", // 商品利润 decimal(10,2)
"备注": "string", // 备注
"长": "string", // 商品长度decimal(10,2)+单位
"宽": "string", // 商品宽度decimal(10,2)+单位
@ -44,135 +39,14 @@ const (
"重量": "string", // 商品重量(kg)
"SPU名称": "string", // 商品SPU名称
"SPU编码": "string" // 编码串jd_{timestamp}_rand(1000-999)
"供应商报价": "string", // 商品供应商报价 decimal(10,2)
"税率": "string", // 商品税率 x%
"利润": "string", // 商品利润 decimal(10,2)
"默认供应商": "string", // 供应商名称
"默认存放仓库": "string", // 仓库名称
}`
)
// 商品属性模板
const (
HYTProductPropertyTemplate = `{
"important_data": {
"type": "object",
"properties": {
"supplier_name": {
"type": "string",
"description": "供应商名称"
},
"warehouse_name": {
"type": "string",
"description": "仓库名称"
},
"profit": {
"type": "float64",
"description": "利润 decimal(10,2)"
},
"tax_rate": {
"type": "integer",
"description": "税率 (x)%"
}
}
},
"goods_info": {
"type": "object",
"properties": {
"title": {
"type": "string",
"description": "商品名称"
},
"brand": {
"type": "string",
"description": "品牌"
},
"category": {
"type": "string",
"description": "分类"
},
"price": {
"type": "float64",
"description": "市场价 decimal(10,2)"
},
"sales_price": {
"type": "float64",
"description": "建议销售价 decimal(10,2)"
},
"discount": {
"type": "integer",
"description": "折扣百分比 公式:(市场价-建议销售价)/市场价*100"
},
"goods_attributes": {
"type": "string",
"description": "商品属性"
},
"goods_bar_code": {
"type": "string",
"description": "商品条码"
},
"goods_illustration": {
"type": "string",
"description": "商品插图"
},
"goods_num": {
"type": "string",
"description": "商品编号"
},
"introduction": {
"type": "string",
"description": "商品介绍"
},
"spu_name": {
"type": "string",
"description": "SPU名称"
},
"spu_num": {
"type": "string",
"description": "SPU编号"
},
"stock": {
"type": "integer",
"description": "库存"
},
"tax_rate": {
"type": "integer",
"description": "税率"
},
"unit": {
"type": "string",
"description": "单位"
},
"weight": {
"type": "string",
"description": "重量"
}
}
},
"goods_media_list": {
"type": "array",
"description": "商品媒体文件列表",
"items": {
"type": "object",
"properties": {
"url": {
"type": "string",
"description": "媒体文件URL"
},
"type": {
"type": "integer",
"description": "媒体类型(1:图片, 2:视频)"
},
"sort": {
"type": "integer",
"description": "排序序号"
}
}
}
}
}`
)
// 外部平台地址
const (
HYTProductListPageURL = "https://gateway.dev.cdlsxd.cn/sw//#/goods/goodsManage"
)
// 缓存key
const (
CapabilityProductIngestCacheKey = "ai_scheduler:capability:product_ingest:%s"

View File

@ -9,7 +9,17 @@ import (
"errors"
)
func Call(ctx context.Context, cfg config.ToolConfig, toolReq *ProductUploadRequest) (toolResp *ProductUploadResponse, err error) {
type Client struct {
cfg config.ToolConfig
}
func New(cfg config.ToolConfig) *Client {
return &Client{
cfg: cfg,
}
}
func (c *Client) Call(ctx context.Context, toolReq *ProductUploadRequest) (toolResp *ProductUploadResponse, err error) {
// 商品有且只能有一个
if len(toolReq.GoodsList) != 1 {
err = errors.New("商品只能有一个")
@ -20,7 +30,7 @@ func Call(ctx context.Context, cfg config.ToolConfig, toolReq *ProductUploadRequ
req := l_request.Request{
Method: "Post",
Url: "https://gateway.dev.cdlsxd.cn/goods-admin/api/v1/goods/supplier/batch/add/complete",
Url: c.cfg.BaseURL,
Json: apiReq,
}
res, err := req.Send()
@ -51,7 +61,7 @@ func Call(ctx context.Context, cfg config.ToolConfig, toolReq *ProductUploadRequ
}
toolResp = &ProductUploadResponse{
PreviewUrl: "https://gateway.dev.cdlsxd.cn/sw//#/goods/goodsManage",
PreviewUrl: c.cfg.AddURL,
SpuNum: toolReq.GoodsList[0].GoodsInfo.SpuNum,
Id: resMap.Data.Ids[0],
}

View File

@ -50,7 +50,8 @@ func Test_Call(t *testing.T) {
},
},
}
toolResp, err := Call(context.Background(), config.ToolConfig{}, req)
client := New(config.ToolConfig{})
toolResp, err := client.Call(context.Background(), req)
if err != nil {
t.Errorf("Call() error = %v", err)

View File

@ -1,16 +1,27 @@
package supplier_search
import (
"ai_scheduler/internal/config"
"ai_scheduler/internal/pkg/l_request"
"context"
"encoding/json"
"errors"
"fmt"
)
func Call(ctx context.Context, name string) (int, error) {
type Client struct {
cfg config.ToolConfig
}
func New(cfg config.ToolConfig) *Client {
return &Client{
cfg: cfg,
}
}
func (c *Client) Call(ctx context.Context, name string) (int, error) {
if name == "" {
return 0, errors.New("supplier name is empty")
// 如果没有供应商名返回0不报错由上层业务决定是否允许
return 0, nil
}
reqBody := SearchRequest{
@ -27,7 +38,7 @@ func Call(ctx context.Context, name string) (int, error) {
req := l_request.Request{
Method: "Post",
Url: "https://gateway.dev.cdlsxd.cn/goods-admin/api/v1/supplier/list",
Url: c.cfg.BaseURL,
Json: apiReq,
Headers: map[string]string{
"User-Agent": "Apifox/1.0.0 (https://apifox.com)",

View File

@ -1,13 +1,24 @@
package warehouse_search
import (
"ai_scheduler/internal/config"
"ai_scheduler/internal/pkg/l_request"
"context"
"encoding/json"
"fmt"
)
func Call(ctx context.Context, name string) (int, error) {
type Client struct {
cfg config.ToolConfig
}
func New(cfg config.ToolConfig) *Client {
return &Client{
cfg: cfg,
}
}
func (c *Client) Call(ctx context.Context, name string) (int, error) {
if name == "" {
// 如果没有仓库名返回0不报错由上层业务决定是否允许
return 0, nil
@ -22,7 +33,7 @@ func Call(ctx context.Context, name string) (int, error) {
req := l_request.Request{
Method: "Get",
Url: "https://gateway.dev.cdlsxd.cn/goods-admin/api/v1/warehouse/list",
Url: c.cfg.BaseURL,
Params: params,
Headers: map[string]string{
"User-Agent": "Apifox/1.0.0 (https://apifox.com)",

View File

@ -1,16 +1,29 @@
package tools
type Tool interface{
Name() string
import (
"ai_scheduler/internal/config"
"ai_scheduler/internal/domain/tools/hyt/product_upload"
"ai_scheduler/internal/domain/tools/hyt/supplier_search"
"ai_scheduler/internal/domain/tools/hyt/warehouse_search"
)
type Manager struct {
Hyt *HytTools
// Zltx *ZltxTools
}
var registry = map[string]Tool{}
func Register(t Tool){
registry[t.Name()] = t
type HytTools struct {
ProductUpload *product_upload.Client
SupplierSearch *supplier_search.Client
WarehouseSearch *warehouse_search.Client
}
func Get(name string) Tool{
return registry[name]
func NewManager(cfg *config.Config) *Manager {
return &Manager{
Hyt: &HytTools{
ProductUpload: product_upload.New(cfg.EinoTools.HytProductUpload),
SupplierSearch: supplier_search.New(cfg.EinoTools.HytSupplierSearch),
WarehouseSearch: warehouse_search.New(cfg.EinoTools.HytWarehouseSearch),
},
}
}

View File

@ -2,36 +2,33 @@ package hyt
import (
"ai_scheduler/internal/config"
"ai_scheduler/internal/data/constants"
toolManager "ai_scheduler/internal/domain/tools"
toolPu "ai_scheduler/internal/domain/tools/hyt/product_upload"
toolSs "ai_scheduler/internal/domain/tools/hyt/supplier_search"
toolWs "ai_scheduler/internal/domain/tools/hyt/warehouse_search"
"ai_scheduler/internal/domain/workflow/runtime"
"ai_scheduler/internal/entitys"
"context"
"encoding/json"
"errors"
"fmt"
"strconv"
"strings"
"sync"
eino_ollama "github.com/cloudwego/eino-ext/components/model/ollama"
"github.com/cloudwego/eino/components/prompt"
"github.com/cloudwego/eino/compose"
"github.com/cloudwego/eino/schema"
)
const WorkflowID = "hyt.productUpload"
func init() {
runtime.Register(WorkflowID, func(d *runtime.Deps) (runtime.Workflow, error) {
return &productUpload{cfg: d.Conf}, nil
return &productUpload{cfg: d.Conf, toolManager: d.ToolManager}, nil
})
}
type productUpload struct {
cfg *config.Config
data *ProductUploadWorkflowInput
cfg *config.Config
toolManager *toolManager.Manager
data *ProductUploadWorkflowInput
}
type ProductUploadWorkflowInput struct {
@ -41,8 +38,8 @@ type ProductUploadWorkflowInput struct {
func (o *productUpload) ID() string { return WorkflowID }
func (o *productUpload) Invoke(ctx context.Context, rec *entitys.Recognize) (map[string]any, error) {
// 构建工作流 (使用 V2 Graph 版本)
runnable, err := o.buildWorkflowV2(ctx)
// 构建工作流
runnable, err := o.buildWorkflow(ctx)
if err != nil {
return nil, err
}
@ -103,65 +100,11 @@ type ProductUploadContext struct {
UploadResp *toolPu.ProductUploadResponse
}
// buildWorkflow 构建基于 Graph 的并行工作流
func (o *productUpload) buildWorkflow(ctx context.Context) (compose.Runnable[*ProductUploadWorkflowInput, map[string]any], error) {
// 定义工作流
c := compose.NewChain[*ProductUploadWorkflowInput, map[string]any]()
// AI映射工具所需参数'
paramMappingModel, err := eino_ollama.NewChatModel(ctx, &eino_ollama.ChatModelConfig{
BaseURL: o.cfg.Ollama.BaseURL,
Timeout: o.cfg.Ollama.Timeout,
Model: o.cfg.Ollama.Model,
Thinking: &eino_ollama.ThinkValue{Value: true},
Options: &eino_ollama.Options{Temperature: 0.5},
})
if err != nil {
return nil, err
}
// 1. 构建参数LLM数映射提示词
c.AppendChatTemplate(prompt.FromMessages(
schema.FString,
schema.SystemMessage("你是一个专业的商品参数解析器,你需要根据用户输入的商品描述,解析出商品的目标参数。"),
schema.SystemMessage("目标参数:"+constants.HYTProductPropertyTemplate),
schema.UserMessage("用户输入:{{.Text}}"),
))
// 2. 调用LLM
c.AppendChatModel(paramMappingModel)
// 3.工具参数整理
c.AppendLambda(compose.InvokableLambda(func(_ context.Context, in *schema.Message) (*toolPu.ProductUploadRequest, error) {
toolReq := &toolPu.ProductUploadRequest{}
if err := json.Unmarshal([]byte(in.Content), toolReq); err != nil {
return nil, err
}
return toolReq, nil
}))
// 4.工具调用
c.AppendLambda(compose.InvokableLambda(func(_ context.Context, in *toolPu.ProductUploadRequest) (*toolPu.ProductUploadResponse, error) {
toolRes, err := toolPu.Call(ctx, o.cfg.Tools.HytProductUpload, in)
return toolRes, err
}))
// 5.结果数据映射
c.AppendLambda(compose.InvokableLambda(func(_ context.Context, in *toolPu.ProductUploadResponse) (map[string]any, error) {
return map[string]any{
"预览URL(货易通商品列表)": in.PreviewUrl,
"SPU编码": in.SpuNum,
"商品ID": in.Id,
}, nil
}))
// 6.编译工作流
return c.Compile(ctx)
}
// buildWorkflowV2 构建基于 Graph 的并行工作流
func (o *productUpload) buildWorkflowV2(ctx context.Context) (compose.Runnable[*ProductUploadWorkflowInput, map[string]any], error) {
g := compose.NewGraph[*ProductUploadWorkflowInput, map[string]any]()
// 1. DataMapping 节点: 解析 JSON -> 填充基础 Request -> 提取供应商/仓库名
// 1. DataMapping 节点: 解析 JSON -> 填充基础 Request
g.AddLambdaNode("data_mapping", compose.InvokableLambda(func(ctx context.Context, in *ProductUploadWorkflowInput) (*ProductUploadContext, error) {
state := &ProductUploadContext{
mu: &sync.Mutex{}, // 初始化锁
@ -176,6 +119,21 @@ func (o *productUpload) buildWorkflowV2(ctx context.Context) (compose.Runnable[*
if err := json.Unmarshal([]byte(in.Text), &ingestData); err != nil {
return nil, fmt.Errorf("解析商品数据失败: %w", err)
}
// 必填校验
if ingestData.SupplierName == "" {
return nil, errors.New("供应商名称不能为空")
}
if ingestData.WarehouseName == "" {
return nil, errors.New("仓库名称不能为空")
}
if ingestData.Profit == "" {
return nil, errors.New("利润不能为空")
}
if ingestData.TaxRate == "" {
return nil, errors.New("税率不能为空")
}
state.IngestData = &ingestData
state.SupplierName = ingestData.SupplierName
state.WarehouseName = ingestData.WarehouseName
@ -240,32 +198,38 @@ func (o *productUpload) buildWorkflowV2(ctx context.Context) (compose.Runnable[*
// 2. 获取供应商ID 节点
g.AddLambdaNode("get_supplier_id", compose.InvokableLambda(func(ctx context.Context, state *ProductUploadContext) (*ProductUploadContext, error) {
if state.SupplierName != "" {
supplierId, err := toolSs.Call(ctx, state.SupplierName)
if err != nil {
// 记录日志,但不阻断流程,可能允许 ID 为 0
fmt.Printf("warning: failed to get supplier id for %s: %v\n", state.SupplierName, err)
} else {
state.mu.Lock()
defer state.mu.Unlock()
state.UploadReq.SupplierId = supplierId
}
if state.SupplierName == "" {
return state, errors.New("供应商名称不能为空")
}
supplierId, err := o.toolManager.Hyt.SupplierSearch.Call(ctx, state.SupplierName)
if err != nil {
// 记录日志,但不阻断流程,可能允许 ID 为 0
fmt.Printf("warning: failed to get supplier id for %s: %v\n", state.SupplierName, err)
} else {
state.mu.Lock()
defer state.mu.Unlock()
state.UploadReq.SupplierId = supplierId
}
return state, nil
}))
// 3. 获取仓库ID 节点
g.AddLambdaNode("get_warehouse_id", compose.InvokableLambda(func(ctx context.Context, state *ProductUploadContext) (*ProductUploadContext, error) {
if state.WarehouseName != "" {
warehouseId, err := toolWs.Call(ctx, state.WarehouseName)
if err != nil {
fmt.Printf("warning: failed to get warehouse id for %s: %v\n", state.WarehouseName, err)
} else {
state.mu.Lock()
defer state.mu.Unlock()
state.UploadReq.WarehouseId = warehouseId
}
if state.WarehouseName == "" {
return state, errors.New("仓库名称不能为空")
}
warehouseId, err := o.toolManager.Hyt.WarehouseSearch.Call(ctx, state.WarehouseName)
if err != nil {
fmt.Printf("warning: failed to get warehouse id for %s: %v\n", state.WarehouseName, err)
} else {
state.mu.Lock()
defer state.mu.Unlock()
state.UploadReq.WarehouseId = warehouseId
}
return state, nil
}))
@ -280,7 +244,7 @@ func (o *productUpload) buildWorkflowV2(ctx context.Context) (compose.Runnable[*
// 5. 上传节点
g.AddLambdaNode("upload_product", compose.InvokableLambda(func(ctx context.Context, state *ProductUploadContext) (*ProductUploadContext, error) {
toolRes, err := toolPu.Call(ctx, o.cfg.Tools.HytProductUpload, state.UploadReq)
toolRes, err := o.toolManager.Hyt.ProductUpload.Call(ctx, state.UploadReq)
if err != nil {
return nil, err
}

View File

@ -5,6 +5,8 @@ import (
"ai_scheduler/internal/domain/workflow/runtime"
"ai_scheduler/internal/pkg/utils_ollama"
toolManager "ai_scheduler/internal/domain/tools"
"github.com/google/wire"
)
@ -13,7 +15,7 @@ var ProviderSetWorkflow = wire.NewSet(NewRegistry)
// NewRegistry 注入共享依赖并注册默认 Registry确保自注册工作流可被发现
func NewRegistry(conf *config.Config, llm *utils_ollama.Client) *runtime.Registry {
// 步骤1设置运行时依赖配置与LLM客户端供工作流工厂在首次实例化时使用必须在任何调用 Invoke 之前完成,否则会触发 "deps not set"
runtime.SetDeps(&runtime.Deps{Conf: conf, LLM: llm})
runtime.SetDeps(&runtime.Deps{Conf: conf, LLM: llm, ToolManager: toolManager.NewManager(conf)})
// 步骤2创建新的工作流注册表注册表负责按工作流ID惰性实例化并缓存单例实例保障并发访问下的安全
r := runtime.NewRegistry()
// 步骤3将该注册表设置为全局默认便于通过 runtime.Default() 获取;自注册的工作流可通过默认注册表被发现并调用

View File

@ -2,11 +2,13 @@ package workflow
import (
"ai_scheduler/internal/config"
toolManager "ai_scheduler/internal/domain/tools"
"ai_scheduler/internal/pkg/utils_ollama"
)
// 仅声明依赖结构,避免在 workflow 包内实现注册中心逻辑导致循环依赖
type Deps struct {
Conf *config.Config
LLM *utils_ollama.Client
Conf *config.Config
LLM *utils_ollama.Client
ToolManager *toolManager.Manager
}

View File

@ -2,6 +2,7 @@ package runtime
import (
"ai_scheduler/internal/config"
toolManager "ai_scheduler/internal/domain/tools"
"ai_scheduler/internal/entitys"
"ai_scheduler/internal/pkg/utils_ollama"
"context"
@ -16,8 +17,9 @@ type Workflow interface {
}
type Deps struct {
Conf *config.Config
LLM *utils_ollama.Client
Conf *config.Config
LLM *utils_ollama.Client
ToolManager *toolManager.Manager
}
type Factory func(deps *Deps) (Workflow, error)