feat: 第一张真实 Eino 图 + 偏好记忆(让模型知道是我)
dispatcher 不再手搓 pool.Stream,改用编译好的 Eino 图驱动;接入用户常驻画像,
推理前召回并注入 system prompt,实现个性化(架构'心脏'首次真跳)。
Eino 图(dispatcher/internal/eino): START→recall→prompt→model→END + 全局 State
- recall(Lambda): 取 Meta[user_id] → 调 MCP memory_get → ProcessState 写画像
- prompt(ChatTemplate): {profile} 注入 system,{query} 作 user
- model: poolModel 适配 LLM Pool 为 model.BaseChatModel(Generate+Stream, schema.Pipe)
- 写回: 流排空后异步 memorize(流式节点走 OnEndWithStreamOutput 非 OnEndFn)
记忆存储(mcp-go owns): GORM Profile→sundynix_user_profile(复合主键, AutoMigrate,
遵守前缀约定), 新工具 memory_get/memory_upsert, 连不上降级
Gateway: SubmitTask 注入 Meta[user_id](X-User-ID 头), PUT /api/v1/memory→memory_upsert
shared: contract.MetaUserID; llm.Pool 拆出 StreamText
验证: 4 模块 build✓ + 3 e2e PASS; live 跑通——PUT 偏好落 sundynix_user_profile,
带 X-User-ID 提交→Eino recall 召回→注入→SSE 流出含画像的个性化回答, writeback 触发
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/sundynix/sundynix-gateway/internal/dsl"
|
||||
"github.com/sundynix/sundynix-gateway/internal/nats"
|
||||
"github.com/sundynix/sundynix-gateway/internal/store"
|
||||
"github.com/sundynix/sundynix-shared/contract"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
@@ -36,6 +37,9 @@ func (h *Handler) SubmitTask(c *gin.Context) {
|
||||
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
// 附上已登录用户标识,供 Dispatcher 召回其偏好记忆。
|
||||
// 真实场景由鉴权中间件注入;此处用 X-User-ID 头,缺省匿名。
|
||||
task.Meta[contract.MetaUserID] = userID(c)
|
||||
// 持久化任务提交(best-effort:降级模式下静默跳过,不阻断发布)。
|
||||
if err := h.db.SaveTask(c.Request.Context(), task.ID, string(task.Graph)); err != nil {
|
||||
log.Printf("[gateway] save task %s failed: %v", task.ID, err)
|
||||
@@ -86,6 +90,40 @@ func (h *Handler) StreamTask(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// SetMemory: 写入/更新一条用户偏好记忆,经 NATS 调 mcp-go 的 memory_upsert 工具。
|
||||
// 桌面端"偏好记忆面板"可用它让用户显式登记/纠正模型对自己的记忆。
|
||||
func (h *Handler) SetMemory(c *gin.Context) {
|
||||
var body struct {
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil || body.Key == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "key/value required"})
|
||||
return
|
||||
}
|
||||
res, err := h.bus.CallTool(c.Request.Context(), contract.ToolSubjectGo("memory_upsert"),
|
||||
&contract.ToolCall{Tool: "memory_upsert", Args: map[string]any{
|
||||
"user_id": userID(c), "key": body.Key, "value": body.Value,
|
||||
}})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if !res.OK {
|
||||
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": res.Error})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"status": "ok", "message": res.Content})
|
||||
}
|
||||
|
||||
// userID 从请求取已登录用户标识(真实场景应由鉴权中间件注入)。
|
||||
func userID(c *gin.Context) string {
|
||||
if u := c.GetHeader("X-User-ID"); u != "" {
|
||||
return u
|
||||
}
|
||||
return "anonymous"
|
||||
}
|
||||
|
||||
func (h *Handler) Billing(c *gin.Context) {
|
||||
// TODO: 商业化与计费模块;暂以已提交任务计数演示真实读库。
|
||||
n, err := h.db.CountTasks(c.Request.Context())
|
||||
|
||||
Reference in New Issue
Block a user