Files
sundynix-agentix/sundynix-mcp-go/internal/memory/store.go
T
Blizzard cbd130ecae 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>
2026-06-10 14:06:18 +08:00

82 lines
2.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package memory 是偏好记忆的存储后端(第 5 层 I/O 型工具持有)。
// 常驻画像存 Postgres,按 sundynix_ 前缀约定 + AutoMigrate 自动迁移。
package memory
import (
"context"
"fmt"
"log"
"sort"
"strings"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
// Profile 是用户常驻画像的一条 key/value 偏好(always-on memory)。
// 复合主键 (user_id, key):同一用户同一键 upsert 覆盖。
type Profile struct {
UserID string `gorm:"primaryKey;column:user_id;size:64"`
Key string `gorm:"primaryKey;size:64"`
Value string `gorm:"type:text"`
}
// TableName 固定表名,遵守 sundynix_ 前缀约定。
func (Profile) TableName() string { return "sundynix_user_profile" }
// Store 封装画像读写。db 为 nil 表示降级(无 Postgres 时记忆功能空转,不阻断工具服务)。
type Store struct{ db *gorm.DB }
// Open 连接 Postgres 并自动迁移 sundynix_user_profile。连接失败不 fatal:返回降级实例。
func Open(dsn string) *Store {
db, err := gorm.Open(postgres.New(postgres.Config{DSN: dsn}), &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent),
})
if err != nil {
log.Printf("[memory] postgres 不可用,记忆降级(召回为空): %v", err)
return &Store{}
}
if err := db.AutoMigrate(&Profile{}); err != nil {
log.Printf("[memory] AutoMigrate 失败,记忆降级: %v", err)
return &Store{}
}
log.Println("[memory] postgres connected, migrated sundynix_user_profile")
return &Store{db: db}
}
// Get 返回某用户的画像,渲染为可直接注入 prompt 的多行文本(按 key 排序,稳定输出)。
func (s *Store) Get(ctx context.Context, userID string) (string, error) {
if s.db == nil || userID == "" {
return "", nil
}
var rows []Profile
if err := s.db.WithContext(ctx).Where("user_id = ?", userID).Find(&rows).Error; err != nil {
return "", err
}
sort.Slice(rows, func(i, j int) bool { return rows[i].Key < rows[j].Key })
var b strings.Builder
for _, r := range rows {
fmt.Fprintf(&b, "- %s%s\n", r.Key, r.Value)
}
return strings.TrimRight(b.String(), "\n"), nil
}
// Upsert 写入/更新一条画像偏好(key 冲突即覆盖 value)。
func (s *Store) Upsert(ctx context.Context, userID, key, value string) error {
if s.db == nil {
return fmt.Errorf("memory store disabled")
}
return s.db.WithContext(ctx).Save(&Profile{UserID: userID, Key: key, Value: value}).Error
}
// Close 释放连接。
func (s *Store) Close() {
if s.db == nil {
return
}
if sqlDB, err := s.db.DB(); err == nil {
_ = sqlDB.Close()
}
}