feat: 1. 新增 ollamaClient chat方法 2. 增加产品数据提取能力接口
This commit is contained in:
parent
c2906ad926
commit
284624bcba
|
|
@ -54,3 +54,7 @@ func ParamErr(message string, arg ...any) *BusinessErr {
|
||||||
func (e *BusinessErr) Wrap(err error) *BusinessErr {
|
func (e *BusinessErr) Wrap(err error) *BusinessErr {
|
||||||
return NewBusinessErr(e.code, err.Error())
|
return NewBusinessErr(e.code, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func KeyErr() *BusinessErr {
|
||||||
|
return &BusinessErr{code: KeyNotFound.code, message: KeyNotFound.message}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,15 @@ package llm
|
||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
type Options struct {
|
type Options struct {
|
||||||
Temperature float32
|
Temperature float32
|
||||||
MaxTokens int
|
MaxTokens int
|
||||||
Stream bool
|
Stream bool
|
||||||
Timeout time.Duration
|
Timeout time.Duration
|
||||||
Modalities []string
|
Modalities []string
|
||||||
SystemPrompt string
|
SystemPrompt string
|
||||||
Model string
|
Model string
|
||||||
TopP float32
|
TopP float32
|
||||||
Stop []string
|
Stop []string
|
||||||
Endpoint string
|
Endpoint string
|
||||||
|
Thinking bool
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
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{
|
cm, err := eino_ollama.NewChatModel(ctx, &eino_ollama.ChatModelConfig{
|
||||||
BaseURL: opts.Endpoint,
|
BaseURL: opts.Endpoint,
|
||||||
Timeout: opts.Timeout,
|
Timeout: opts.Timeout,
|
||||||
Model: opts.Model,
|
Model: opts.Model,
|
||||||
Options: &eino_ollama.Options{Temperature: opts.Temperature, NumPredict: opts.MaxTokens},
|
Options: &eino_ollama.Options{Temperature: opts.Temperature, NumPredict: opts.MaxTokens},
|
||||||
|
Thinking: &eino_ollama.ThinkValue{Value: opts.Thinking},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -90,6 +90,25 @@ func (c *Client) ChatStream(ctx context.Context, ch chan entitys.Response, messa
|
||||||
return
|
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) {
|
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 {
|
err = c.client.Generate(ctx, generateRequest, func(resp api.GenerateResponse) error {
|
||||||
result = resp
|
result = resp
|
||||||
|
|
|
||||||
|
|
@ -11,11 +11,13 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type HTTPServer struct {
|
type HTTPServer struct {
|
||||||
app *fiber.App
|
app *fiber.App
|
||||||
service *services.ChatService
|
service *services.ChatService
|
||||||
session *services.SessionService
|
session *services.SessionService
|
||||||
gateway *gateway.Gateway
|
gateway *gateway.Gateway
|
||||||
callback *services.CallbackService
|
callback *services.CallbackService
|
||||||
|
chatHis *services.HistoryService
|
||||||
|
capabilityService *services.CapabilityService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHTTPServer(
|
func NewHTTPServer(
|
||||||
|
|
@ -25,10 +27,11 @@ func NewHTTPServer(
|
||||||
gateway *gateway.Gateway,
|
gateway *gateway.Gateway,
|
||||||
callback *services.CallbackService,
|
callback *services.CallbackService,
|
||||||
chatHis *services.HistoryService,
|
chatHis *services.HistoryService,
|
||||||
|
capabilityService *services.CapabilityService,
|
||||||
) *fiber.App {
|
) *fiber.App {
|
||||||
//构建 server
|
//构建 server
|
||||||
app := initRoute()
|
app := initRoute()
|
||||||
router.SetupRoutes(app, service, session, task, gateway, callback, chatHis)
|
router.SetupRoutes(app, service, session, task, gateway, callback, chatHis, capabilityService)
|
||||||
return app
|
return app
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,16 +15,18 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type RouterServer struct {
|
type RouterServer struct {
|
||||||
app *fiber.App
|
app *fiber.App
|
||||||
service *services.ChatService
|
service *services.ChatService
|
||||||
session *services.SessionService
|
session *services.SessionService
|
||||||
gateway *gateway.Gateway
|
gateway *gateway.Gateway
|
||||||
chatHist *services.HistoryService
|
chatHist *services.HistoryService
|
||||||
|
capabilityService *services.CapabilityService
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetupRoutes 设置路由
|
// SetupRoutes 设置路由
|
||||||
func SetupRoutes(app *fiber.App, ChatService *services.ChatService, sessionService *services.SessionService, task *services.TaskService,
|
func SetupRoutes(app *fiber.App, ChatService *services.ChatService, sessionService *services.SessionService, task *services.TaskService,
|
||||||
gateway *gateway.Gateway, callbackService *services.CallbackService, chatHist *services.HistoryService,
|
gateway *gateway.Gateway, callbackService *services.CallbackService, chatHist *services.HistoryService,
|
||||||
|
capabilityService *services.CapabilityService,
|
||||||
) {
|
) {
|
||||||
app.Use(func(c *fiber.Ctx) error {
|
app.Use(func(c *fiber.Ctx) error {
|
||||||
// 设置 CORS 头
|
// 设置 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/list", chatHist.List)
|
||||||
r.Post("/chat/history/update/content", chatHist.UpdateContent)
|
r.Post("/chat/history/update/content", chatHist.UpdateContent)
|
||||||
|
|
||||||
|
// 能力
|
||||||
|
r.Post("/capability/product/ingest", capabilityService.ProductIngest) // 商品数据提取
|
||||||
}
|
}
|
||||||
|
|
||||||
func routerSocket(app *fiber.App, chatService *services.ChatService) {
|
func routerSocket(app *fiber.App, chatService *services.ChatService) {
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,8 @@ func (s *CallbackService) Callback(c *fiber.Ctx) error {
|
||||||
ts := strings.TrimSpace(c.Get("X-Timestamp"))
|
ts := strings.TrimSpace(c.Get("X-Timestamp"))
|
||||||
|
|
||||||
// 时间窗口(如果提供了 ts 则校验,否则跳过),窗口 5 分钟
|
// 时间窗口(如果提供了 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
|
return errorcode.AuthNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -101,40 +102,40 @@ func (s *CallbackService) Callback(c *fiber.Ctx) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateTimestamp(ts string, window time.Duration) bool {
|
// func validateTimestamp(ts string, window time.Duration) bool {
|
||||||
// 期望毫秒时间戳或秒级,简单容错
|
// // 期望毫秒时间戳或秒级,简单容错
|
||||||
// 尝试解析为整数
|
// // 尝试解析为整数
|
||||||
var n int64
|
// var n int64
|
||||||
for _, base := range []int64{1, 1000} { // 秒或毫秒
|
// for _, base := range []int64{1, 1000} { // 秒或毫秒
|
||||||
if v, ok := parseInt64(ts); ok {
|
// if v, ok := parseInt64(ts); ok {
|
||||||
n = v
|
// n = v
|
||||||
// 归一为毫秒
|
// // 归一为毫秒
|
||||||
if base == 1 && len(ts) <= 10 {
|
// if base == 1 && len(ts) <= 10 {
|
||||||
n = n * 1000
|
// n = n * 1000
|
||||||
}
|
// }
|
||||||
now := time.Now().UnixMilli()
|
// now := time.Now().UnixMilli()
|
||||||
diff := now - n
|
// diff := now - n
|
||||||
if diff < 0 {
|
// if diff < 0 {
|
||||||
diff = -diff
|
// diff = -diff
|
||||||
}
|
// }
|
||||||
if diff <= window.Milliseconds() {
|
// if diff <= window.Milliseconds() {
|
||||||
return true
|
// return true
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
return false
|
// return false
|
||||||
}
|
// }
|
||||||
|
|
||||||
func parseInt64(s string) (int64, bool) {
|
// func parseInt64(s string) (int64, bool) {
|
||||||
var n int64
|
// var n int64
|
||||||
for _, ch := range s {
|
// for _, ch := range s {
|
||||||
if ch < '0' || ch > '9' {
|
// if ch < '0' || ch > '9' {
|
||||||
return 0, false
|
// return 0, false
|
||||||
}
|
// }
|
||||||
n = n*10 + int64(ch-'0')
|
// n = n*10 + int64(ch-'0')
|
||||||
}
|
// }
|
||||||
return n, true
|
// return n, true
|
||||||
}
|
// }
|
||||||
|
|
||||||
func (s *CallbackService) handleDingTalkCallback(c *fiber.Ctx, env Envelope) error {
|
func (s *CallbackService) handleDingTalkCallback(c *fiber.Ctx, env Envelope) error {
|
||||||
// 校验taskId
|
// 校验taskId
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -12,4 +12,6 @@ var ProviderSetServices = wire.NewSet(
|
||||||
NewTaskService,
|
NewTaskService,
|
||||||
NewCallbackService,
|
NewCallbackService,
|
||||||
NewDingBotService,
|
NewDingBotService,
|
||||||
NewHistoryService)
|
NewHistoryService,
|
||||||
|
NewCapabilityService,
|
||||||
|
)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue