feat: 1. 新增 ollamaClient chat方法 2. 增加产品数据提取能力接口

This commit is contained in:
fuzhongyun 2025-12-18 18:19:46 +08:00
parent c2906ad926
commit 284624bcba
10 changed files with 266 additions and 60 deletions

View File

@ -54,3 +54,7 @@ func ParamErr(message string, arg ...any) *BusinessErr {
func (e *BusinessErr) Wrap(err error) *BusinessErr {
return NewBusinessErr(e.code, err.Error())
}
func KeyErr() *BusinessErr {
return &BusinessErr{code: KeyNotFound.code, message: KeyNotFound.message}
}

View File

@ -3,14 +3,15 @@ package llm
import "time"
type Options struct {
Temperature float32
MaxTokens int
Stream bool
Timeout time.Duration
Modalities []string
SystemPrompt string
Model string
TopP float32
Stop []string
Endpoint string
Temperature float32
MaxTokens int
Stream bool
Timeout time.Duration
Modalities []string
SystemPrompt string
Model string
TopP float32
Stop []string
Endpoint string
Thinking bool
}

View File

@ -15,10 +15,11 @@ func New() *Adapter { return &Adapter{} }
func (a *Adapter) Generate(ctx context.Context, input []*schema.Message, opts llm.Options) (*schema.Message, error) {
cm, err := eino_ollama.NewChatModel(ctx, &eino_ollama.ChatModelConfig{
BaseURL: opts.Endpoint,
Timeout: opts.Timeout,
Model: opts.Model,
Options: &eino_ollama.Options{Temperature: opts.Temperature, NumPredict: opts.MaxTokens},
BaseURL: opts.Endpoint,
Timeout: opts.Timeout,
Model: opts.Model,
Options: &eino_ollama.Options{Temperature: opts.Temperature, NumPredict: opts.MaxTokens},
Thinking: &eino_ollama.ThinkValue{Value: opts.Thinking},
})
if err != nil {
return nil, err

41
internal/pkg/util/time.go Normal file
View File

@ -0,0 +1,41 @@
package util
import "time"
// 判断当前时间是否在时间窗口内
// ts 时间戳字符串,支持秒级或毫秒级
// window 时间窗口,例如 10 * time.Minute
func IsInTimeWindow(ts string, window time.Duration) bool {
// 期望毫秒时间戳或秒级,简单容错
// 尝试解析为整数
var n int64
for _, base := range []int64{1, 1000} { // 秒或毫秒
if v, ok := parseInt64(ts); ok {
n = v
// 归一为毫秒
if base == 1 && len(ts) <= 10 {
n = n * 1000
}
now := time.Now().UnixMilli()
diff := now - n
if diff < 0 {
diff = -diff
}
if diff <= window.Milliseconds() {
return true
}
}
}
return false
}
func parseInt64(s string) (int64, bool) {
var n int64
for _, ch := range s {
if ch < '0' || ch > '9' {
return 0, false
}
n = n*10 + int64(ch-'0')
}
return n, true
}

View File

@ -90,6 +90,25 @@ func (c *Client) ChatStream(ctx context.Context, ch chan entitys.Response, messa
return
}
func (c *Client) Chat(ctx context.Context, messages []api.Message) (res api.ChatResponse, err error) {
// 构建聊天请求
req := &api.ChatRequest{
Model: c.config.Model,
Messages: messages,
Stream: new(bool), // 设置为false不使用流式响应
Think: &api.ThinkValue{Value: true},
}
err = c.client.Chat(ctx, req, func(resp api.ChatResponse) error {
res = resp
return nil
})
if err != nil {
return
}
return
}
func (c *Client) Generation(ctx context.Context, generateRequest *api.GenerateRequest) (result api.GenerateResponse, err error) {
err = c.client.Generate(ctx, generateRequest, func(resp api.GenerateResponse) error {
result = resp

View File

@ -11,11 +11,13 @@ import (
)
type HTTPServer struct {
app *fiber.App
service *services.ChatService
session *services.SessionService
gateway *gateway.Gateway
callback *services.CallbackService
app *fiber.App
service *services.ChatService
session *services.SessionService
gateway *gateway.Gateway
callback *services.CallbackService
chatHis *services.HistoryService
capabilityService *services.CapabilityService
}
func NewHTTPServer(
@ -25,10 +27,11 @@ func NewHTTPServer(
gateway *gateway.Gateway,
callback *services.CallbackService,
chatHis *services.HistoryService,
capabilityService *services.CapabilityService,
) *fiber.App {
//构建 server
app := initRoute()
router.SetupRoutes(app, service, session, task, gateway, callback, chatHis)
router.SetupRoutes(app, service, session, task, gateway, callback, chatHis, capabilityService)
return app
}

View File

@ -15,16 +15,18 @@ import (
)
type RouterServer struct {
app *fiber.App
service *services.ChatService
session *services.SessionService
gateway *gateway.Gateway
chatHist *services.HistoryService
app *fiber.App
service *services.ChatService
session *services.SessionService
gateway *gateway.Gateway
chatHist *services.HistoryService
capabilityService *services.CapabilityService
}
// SetupRoutes 设置路由
func SetupRoutes(app *fiber.App, ChatService *services.ChatService, sessionService *services.SessionService, task *services.TaskService,
gateway *gateway.Gateway, callbackService *services.CallbackService, chatHist *services.HistoryService,
capabilityService *services.CapabilityService,
) {
app.Use(func(c *fiber.Ctx) error {
// 设置 CORS 头
@ -84,6 +86,9 @@ func SetupRoutes(app *fiber.App, ChatService *services.ChatService, sessionServi
// 会话历史
r.Post("/chat/history/list", chatHist.List)
r.Post("/chat/history/update/content", chatHist.UpdateContent)
// 能力
r.Post("/capability/product/ingest", capabilityService.ProductIngest) // 商品数据提取
}
func routerSocket(app *fiber.App, chatService *services.ChatService) {

View File

@ -77,7 +77,8 @@ func (s *CallbackService) Callback(c *fiber.Ctx) error {
ts := strings.TrimSpace(c.Get("X-Timestamp"))
// 时间窗口(如果提供了 ts 则校验,否则跳过),窗口 5 分钟
if ts != "" && !validateTimestamp(ts, 5*time.Minute) {
// if ts != "" && !validateTimestamp(ts, 5*time.Minute) {
if ts != "" && !util.IsInTimeWindow(ts, 5*time.Minute) {
return errorcode.AuthNotFound
}
@ -101,40 +102,40 @@ func (s *CallbackService) Callback(c *fiber.Ctx) error {
}
}
func validateTimestamp(ts string, window time.Duration) bool {
// 期望毫秒时间戳或秒级,简单容错
// 尝试解析为整数
var n int64
for _, base := range []int64{1, 1000} { // 秒或毫秒
if v, ok := parseInt64(ts); ok {
n = v
// 归一为毫秒
if base == 1 && len(ts) <= 10 {
n = n * 1000
}
now := time.Now().UnixMilli()
diff := now - n
if diff < 0 {
diff = -diff
}
if diff <= window.Milliseconds() {
return true
}
}
}
return false
}
// func validateTimestamp(ts string, window time.Duration) bool {
// // 期望毫秒时间戳或秒级,简单容错
// // 尝试解析为整数
// var n int64
// for _, base := range []int64{1, 1000} { // 秒或毫秒
// if v, ok := parseInt64(ts); ok {
// n = v
// // 归一为毫秒
// if base == 1 && len(ts) <= 10 {
// n = n * 1000
// }
// now := time.Now().UnixMilli()
// diff := now - n
// if diff < 0 {
// diff = -diff
// }
// if diff <= window.Milliseconds() {
// return true
// }
// }
// }
// return false
// }
func parseInt64(s string) (int64, bool) {
var n int64
for _, ch := range s {
if ch < '0' || ch > '9' {
return 0, false
}
n = n*10 + int64(ch-'0')
}
return n, true
}
// func parseInt64(s string) (int64, bool) {
// var n int64
// for _, ch := range s {
// if ch < '0' || ch > '9' {
// return 0, false
// }
// n = n*10 + int64(ch-'0')
// }
// return n, true
// }
func (s *CallbackService) handleDingTalkCallback(c *fiber.Ctx, env Envelope) error {
// 校验taskId

View File

@ -0,0 +1,129 @@
package services
import (
"ai_scheduler/internal/config"
errorcode "ai_scheduler/internal/data/error"
"ai_scheduler/internal/pkg/util"
"ai_scheduler/internal/pkg/utils_ollama"
"context"
"fmt"
"strings"
"time"
"github.com/gofiber/fiber/v2"
"github.com/ollama/ollama/api"
)
// CapabilityService 统一回调入口
type CapabilityService struct {
cfg *config.Config
}
func NewCapabilityService(cfg *config.Config) *CapabilityService {
return &CapabilityService{
cfg: cfg,
}
}
// 产品数据提取入参
type ProductIngestReq struct {
Url string `json:"url"` // 商品详情页URL
Title string `json:"title"` // 商品标题
Text string `json:"text"` // 商品描述
Images []string `json:"images"` // 商品图片URL列表
Timestamp int64 `json:"timestamp"` // 商品发布时间戳
}
const (
// 货易通商品属性模板-中文
HYTProductPropertyTemplateZH = `{
"条码": "string", // 商品编号
"分类名称": "string", // 商品分类
"货品名称": "string", // 商品名称
"货品编号": "string", // 商品编号
"商品货号": "string", // 商品编号
"品牌": "string", // 商品品牌
"单位": "string", // 商品单位,若无则使用'个'
"规格参数": "string", // 商品规格参数
"货品说明": "string", // 商品说明
"保质期": "string", // 商品保质期
"保质期单位": "string", // 商品保质期单位
"链接": "string", //
"货品图片": ["string"], // 商品多图取1-2个即可
"电商销售价格": "decimal(10,2)", // 商品电商销售价格
"销售价": "decimal(10,2)", // 商品销售价格
"供应商报价": "decimal(10,2)", // 商品供应商报价
"税率": "number%", // 商品税率 x%
"默认供应商": "", // 空即可
"默认存放仓库": "", // 空即可
"备注": "", // 备注
"长": "string", // 商品长度decimal(10,2)+单位
"宽": "string", // 商品宽度decimal(10,2)+单位
"高": "string", // 商品高度decimal(10,2)+单位
"重量": "string", // 商品重量(kg)
"SPU名称": "string", // 商品SPU名称
"SPU编码": "string" // 编码串jd_{timestamp}_rand(1000-999)
}`
SystemPrompt = `你是一个专业的商品属性提取助手你的任务是根据用户输入提取商品的属性信息
目标属性模板%s
最终输出格式为纯JSON字符串键值对对应目标属性和提取到的属性值
最终输出不要携带markdown标识不要携带回车换行`
)
// ProductIngest 产品数据提取
func (s *CapabilityService) ProductIngest(c *fiber.Ctx) error {
// 读取头
token := strings.TrimSpace(c.Get("X-Source-Key"))
ts := strings.TrimSpace(c.Get("X-Timestamp"))
// 时间窗口校验
if ts != "" && !util.IsInTimeWindow(ts, 5*time.Minute) {
// return errorcode.AuthNotFound
}
// token校验
if token == "" || token != "A7f9KQ3mP2X8LZC4R5e" {
// return errorcode.KeyErr()
}
// 解析请求参数
req := ProductIngestReq{}
if err := c.BodyParser(&req); err != nil {
return errorcode.ParamErr("invalid request body: %v", err)
}
// 必要参数校验
if req.Text == "" {
return errorcode.ParamErr("missing required fields")
}
// 模型调用
client, cleanup, err := utils_ollama.NewClient(s.cfg)
if err != nil {
return err
}
defer cleanup()
res, err := client.Chat(context.Background(), []api.Message{
{
Role: "system",
Content: fmt.Sprintf(SystemPrompt, HYTProductPropertyTemplateZH),
},
{
Role: "user",
Content: req.Text,
},
{
Role: "user",
Content: "商品图片URL列表" + strings.Join(req.Images, ","),
},
})
if err != nil {
return err
}
// 解析模型输出
c.JSON(res.Message.Content)
return nil
}

View File

@ -12,4 +12,6 @@ var ProviderSetServices = wire.NewSet(
NewTaskService,
NewCallbackService,
NewDingBotService,
NewHistoryService)
NewHistoryService,
NewCapabilityService,
)