84d1a1dd3a
mcp-go 接通向量 RAG:embedding(OpenAI 兼容 provider 抽象) + Milvus 真实连接, kb_ingest 入库、wiki_search 真检索。retriever 节点一行不改即从桩变真。 - mcp-go internal/rag: embed.go(OpenAI 兼容 /embeddings 客户端) + milvus.go(milvus-sdk-go 真连,集合按首次 embedding 维度懒建+AUTOINDEX/COSINE索引+加载,insert/向量search) + rag.go(Engine: 切块→embed→insert / embed query→search;embedding 或 Milvus 缺则降级) - mcp-go gateway: 新工具 kb_ingest,wiki_search 换真(RAG 向量检索,kb 过滤 topK) - mcp-go main: rag.Open 读 MILVUS_ADDR/EMBED_BASE_URL/EMBED_API_KEY/EMBED_MODEL 环境变量 - gateway: POST /api/v1/kb/ingest → kb_ingest(供知识库页/脚本) - scripts/mock_embeddings.py: 确定性词法向量(字+bigram 哈希),无真 key 验证检索 - 开发期 embedding 接在线 API(无真 key 用 mock),见 llm-provider-strategy - 验证: 全模块 build✓ + e2e PASS; live——入库5条→Milvus;retriever 节点查'向量数据库' →召回 Milvus 那条→DeepSeek 答'Milvus';查'知识图谱'→Neo4j(向量检索区分正确) 注: 当前向量单路;Bleve/Neo4j 融合 + rerank + 真实语义 embedding 为后续。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
155 lines
5.8 KiB
Go
155 lines
5.8 KiB
Go
// Package mcp 实现 MCP 协议网关,把工具注册到 NATS 并响应调用。
|
|
package mcp
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"strings"
|
|
|
|
sharedbus "github.com/sundynix/sundynix-shared/bus"
|
|
"github.com/sundynix/sundynix-shared/contract"
|
|
|
|
"github.com/sundynix/sundynix-mcp-go/internal/history"
|
|
"github.com/sundynix/sundynix-mcp-go/internal/memory"
|
|
"github.com/sundynix/sundynix-mcp-go/internal/rag"
|
|
"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
|
|
history *history.Store
|
|
rag *rag.Engine
|
|
}
|
|
|
|
func NewGateway(b *sharedbus.Bus, s *search.Hybrid, m *memory.Store, h *history.Store, r *rag.Engine) *Gateway {
|
|
return &Gateway{bus: b, search: s, memory: m, history: h, rag: r}
|
|
}
|
|
|
|
// 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, kb_ingest, memory_get, memory_upsert, history_get, history_append, 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 "kb_ingest":
|
|
return g.kbIngest(ctx, call)
|
|
case "memory_get":
|
|
return g.memoryGet(ctx, call)
|
|
case "memory_upsert":
|
|
return g.memoryUpsert(ctx, call)
|
|
case "history_get":
|
|
return g.historyGet(ctx, call)
|
|
case "history_append":
|
|
return g.historyAppend(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}
|
|
}
|
|
|
|
// historyGet 召回某会话最近多轮历史,Content 为 JSON 数组 [{role,content},...](正序)。
|
|
func (g *Gateway) historyGet(ctx context.Context, call *contract.ToolCall) *contract.ToolResult {
|
|
session, _ := call.Args["session_id"].(string)
|
|
turns, err := g.history.Get(ctx, session)
|
|
if err != nil {
|
|
return &contract.ToolResult{OK: false, Error: "history_get: " + err.Error()}
|
|
}
|
|
data, _ := json.Marshal(turns)
|
|
return &contract.ToolResult{OK: true, Content: string(data)}
|
|
}
|
|
|
|
// historyAppend 追加一条会话消息(session_id + role + content)。
|
|
func (g *Gateway) historyAppend(ctx context.Context, call *contract.ToolCall) *contract.ToolResult {
|
|
session, _ := call.Args["session_id"].(string)
|
|
role, _ := call.Args["role"].(string)
|
|
content, _ := call.Args["content"].(string)
|
|
if session == "" || role == "" {
|
|
return &contract.ToolResult{OK: false, Error: "history_append: session_id 和 role 必填"}
|
|
}
|
|
if err := g.history.Append(ctx, session, role, content); err != nil {
|
|
return &contract.ToolResult{OK: false, Error: "history_append: " + err.Error()}
|
|
}
|
|
return &contract.ToolResult{OK: true}
|
|
}
|
|
|
|
// 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 经 RAG 引擎做向量检索(embedding + Milvus)。
|
|
// RAG 未就绪时降级返回空命中(不阻断图执行)。
|
|
func (g *Gateway) wikiSearch(ctx context.Context, call *contract.ToolCall) *contract.ToolResult {
|
|
q, _ := call.Args["q"].(string)
|
|
kb, _ := call.Args["kb"].(string)
|
|
topK := 5
|
|
if v, ok := call.Args["topK"].(float64); ok && v > 0 {
|
|
topK = int(v)
|
|
}
|
|
if !g.rag.Ready() {
|
|
return &contract.ToolResult{OK: true, Content: "[wiki_search] RAG 未配置(需 embedding + Milvus),无召回"}
|
|
}
|
|
hits, err := g.rag.Search(ctx, kb, q, topK)
|
|
if err != nil {
|
|
return &contract.ToolResult{OK: false, Error: "wiki_search: " + err.Error()}
|
|
}
|
|
var b strings.Builder
|
|
fmt.Fprintf(&b, "[wiki_search] 命中 %d 条(Milvus 向量检索):\n", len(hits))
|
|
for i, h := range hits {
|
|
fmt.Fprintf(&b, "%d. (%.3f) %s\n", i+1, h.Score, h.Text)
|
|
}
|
|
return &contract.ToolResult{OK: true, Content: strings.TrimRight(b.String(), "\n")}
|
|
}
|
|
|
|
// kbIngest 把文本入库(切块→embedding→Milvus)。
|
|
func (g *Gateway) kbIngest(ctx context.Context, call *contract.ToolCall) *contract.ToolResult {
|
|
kb, _ := call.Args["kb"].(string)
|
|
text, _ := call.Args["text"].(string)
|
|
if text == "" {
|
|
return &contract.ToolResult{OK: false, Error: "kb_ingest: text 必填"}
|
|
}
|
|
n, err := g.rag.Ingest(ctx, kb, text)
|
|
if err != nil {
|
|
return &contract.ToolResult{OK: false, Error: "kb_ingest: " + err.Error()}
|
|
}
|
|
return &contract.ToolResult{OK: true, Content: fmt.Sprintf("已入库 %d 块到知识库 %q", n, kb)}
|
|
}
|