cbd130ecae
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>
102 lines
3.5 KiB
Go
102 lines
3.5 KiB
Go
// Package mcp 实现 MCP 协议网关,把工具注册到 NATS 并响应调用。
|
|
package mcp
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
|
|
sharedbus "github.com/sundynix/sundynix-shared/bus"
|
|
"github.com/sundynix/sundynix-shared/contract"
|
|
|
|
"github.com/sundynix/sundynix-mcp-go/internal/memory"
|
|
"github.com/sundynix/sundynix-mcp-go/internal/search"
|
|
)
|
|
|
|
// Gateway 暴露 MCP 协议端点,经共享 bus 订阅 sundynix.tools.go.* 响应调用。
|
|
type Gateway struct {
|
|
bus *sharedbus.Bus
|
|
search *search.Hybrid
|
|
memory *memory.Store
|
|
}
|
|
|
|
func NewGateway(b *sharedbus.Bus, s *search.Hybrid, m *memory.Store) *Gateway {
|
|
return &Gateway{bus: b, search: s, memory: m}
|
|
}
|
|
|
|
// Serve 以队列组通配订阅 sundynix.tools.go.>,按工具名分发并阻塞。
|
|
func (g *Gateway) Serve(ctx context.Context) error {
|
|
unsub, err := g.bus.ServeTool(contract.SubjectToolsGoAll, contract.QueueToolsGo, g.dispatch)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() { _ = unsub() }()
|
|
log.Printf("[mcp_go] tools ready on %s (queue=%s): wiki_search, memory_get, memory_upsert, echo",
|
|
contract.SubjectToolsGoAll, contract.QueueToolsGo)
|
|
<-ctx.Done()
|
|
return ctx.Err()
|
|
}
|
|
|
|
// dispatch 按 ToolCall.Tool 路由到具体工具实现。
|
|
func (g *Gateway) dispatch(ctx context.Context, call *contract.ToolCall) *contract.ToolResult {
|
|
log.Printf("[mcp_go] tool=%s task=%s args=%v", call.Tool, call.TaskID, call.Args)
|
|
switch call.Tool {
|
|
case "wiki_search":
|
|
return g.wikiSearch(ctx, call)
|
|
case "memory_get":
|
|
return g.memoryGet(ctx, call)
|
|
case "memory_upsert":
|
|
return g.memoryUpsert(ctx, call)
|
|
case "echo":
|
|
return &contract.ToolResult{OK: true, Content: fmt.Sprint(call.Args["text"])}
|
|
default:
|
|
return &contract.ToolResult{OK: false, Error: "unknown tool: " + call.Tool}
|
|
}
|
|
}
|
|
|
|
// memoryGet 召回某用户的常驻画像(已渲染为可注入 prompt 的多行文本)。
|
|
func (g *Gateway) memoryGet(ctx context.Context, call *contract.ToolCall) *contract.ToolResult {
|
|
uid, _ := call.Args["user_id"].(string)
|
|
profile, err := g.memory.Get(ctx, uid)
|
|
if err != nil {
|
|
return &contract.ToolResult{OK: false, Error: "memory_get: " + err.Error()}
|
|
}
|
|
return &contract.ToolResult{OK: true, Content: profile}
|
|
}
|
|
|
|
// memoryUpsert 写入/更新一条画像偏好(user_id + key + value)。
|
|
func (g *Gateway) memoryUpsert(ctx context.Context, call *contract.ToolCall) *contract.ToolResult {
|
|
uid, _ := call.Args["user_id"].(string)
|
|
key, _ := call.Args["key"].(string)
|
|
val, _ := call.Args["value"].(string)
|
|
if uid == "" || key == "" {
|
|
return &contract.ToolResult{OK: false, Error: "memory_upsert: user_id 和 key 必填"}
|
|
}
|
|
if err := g.memory.Upsert(ctx, uid, key, val); err != nil {
|
|
return &contract.ToolResult{OK: false, Error: "memory_upsert: " + err.Error()}
|
|
}
|
|
return &contract.ToolResult{OK: true, Content: fmt.Sprintf("已记住 %s 的「%s」", uid, key)}
|
|
}
|
|
|
|
// wikiSearch 调 Hybrid 混合检索引擎。引擎目前为桩(返回空),
|
|
// 这里仍把调用链路做真:真实接入 Bleve/Milvus/Neo4j 后无需改动协议。
|
|
func (g *Gateway) wikiSearch(ctx context.Context, call *contract.ToolCall) *contract.ToolResult {
|
|
q, _ := call.Args["q"].(string)
|
|
results, err := g.search.Query(ctx, q, 5)
|
|
if err != nil {
|
|
return &contract.ToolResult{OK: false, Error: "wiki_search: " + err.Error()}
|
|
}
|
|
return &contract.ToolResult{
|
|
OK: true,
|
|
Content: fmt.Sprintf("[wiki_search] 命中 %d 条(Bleve+Milvus+Neo4j 混合检索桩)查询=%q", len(results), preview(q)),
|
|
}
|
|
}
|
|
|
|
func preview(s string) string {
|
|
r := []rune(s)
|
|
if len(r) > 40 {
|
|
return string(r[:40]) + "…"
|
|
}
|
|
return s
|
|
}
|