Files
sundynix-agentix/sundynix-dispatcher/internal/eino/model.go
T
Blizzard 4928ffc0f7 feat: 短期多轮历史接入 Eino 图 MessagesPlaceholder (⑨)
会话历史(Redis,易失,与长期画像分开)经 MCP 工具进出 Eino 图:
recall 召回历史填 MessagesPlaceholder,写回把本轮 user/assistant 落历史。

- mcp-go: internal/history(go-redis, sundynix:history:<session>, LPUSH+LTRIM 保留近20条,
  24h TTL) + 工具 history_get(返回JSON turns)/history_append; main 开 Redis(降级)
- dispatcher Eino: 模板加 MessagesPlaceholder('history'); recall 调 history_get→转 schema.Message;
  Handle 累积 answer; memorize 异步 history_append(user+assistant)
- shared: contract.MetaSessionID; gateway: SubmitTask 注入 Meta[session_id](X-Session-ID 头,缺省 default)
- demo.sh: 同会话两轮提交,验证第2轮召回第1轮历史
- 验证: 4 模块 build✓ + 3 e2e PASS; live 跑通——轮1=0轮历史→落库, 轮2 history_get 命中→注入

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 14:18:45 +08:00

89 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 eino
import (
"context"
"strings"
"github.com/cloudwego/eino/components/model"
"github.com/cloudwego/eino/schema"
"github.com/sundynix/sundynix-dispatcher/internal/llm"
)
// poolModel 把 LLM Pool 适配成 Eino 的 model.BaseChatModel
// 让 LLM Pool 能作为图中的 ChatModel 节点参与编排与流式。
// 真实接入 vLLM/Ollama 后,这里替换为后端的 Generate/Stream 即可。
type poolModel struct{ pool *llm.Pool }
func newPoolModel(p *llm.Pool) *poolModel { return &poolModel{pool: p} }
var _ model.BaseChatModel = (*poolModel)(nil)
// Generate 阻塞式生成(图被 Invoke 时用)。
func (pm *poolModel) Generate(ctx context.Context, input []*schema.Message, _ ...model.Option) (*schema.Message, error) {
var sb strings.Builder
if err := pm.pool.StreamText(ctx, replyFor(input), func(tok []byte) { sb.Write(tok) }); err != nil {
return nil, err
}
return schema.AssistantMessage(sb.String(), nil), nil
}
// Stream 流式生成(图被 Stream 时用):把回复按 token 推进 pipe。
func (pm *poolModel) Stream(ctx context.Context, input []*schema.Message, _ ...model.Option) (*schema.StreamReader[*schema.Message], error) {
sr, sw := schema.Pipe[*schema.Message](32)
text := replyFor(input)
go func() {
defer sw.Close()
if err := pm.pool.StreamText(ctx, text, func(tok []byte) {
sw.Send(schema.AssistantMessage(string(tok), nil), nil)
}); err != nil {
sw.Send(nil, err)
}
}()
return sr, nil
}
// replyFor 是占位"模型":从消息中取出注入的画像与用户输入,
// 生成一段能体现"记忆已注入"的确定性回复(证明 recall→prompt 链路真的把画像喂进来了)。
// 真实模型不需要本函数。
func replyFor(msgs []*schema.Message) string {
var profile, user string
priorTurns := 0
for i, m := range msgs {
switch m.Role {
case schema.System:
profile = m.Content
case schema.Assistant:
priorTurns++ // 历史里的助手消息 = 过往轮次
case schema.User:
if i == len(msgs)-1 {
user = m.Content // 最后一条 user 才是本轮输入
}
}
}
return "【已注入用户画像】" + condense(profile, 80) +
"(本会话已有 " + itoa(priorTurns) + " 轮历史)" +
" | 据此为你个性化作答:已编排执行该 Agent 图(输入「" + condense(user, 30) + "」)。"
}
func itoa(n int) string {
if n == 0 {
return "0"
}
var b []byte
for n > 0 {
b = append([]byte{byte('0' + n%10)}, b...)
n /= 10
}
return string(b)
}
func condense(s string, max int) string {
s = strings.TrimSpace(strings.ReplaceAll(s, "\n", " "))
r := []rune(s)
if len(r) > max {
return string(r[:max]) + "…"
}
return s
}