package io import ( "context" "errors" "fmt" "log/slog" "strings" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/loop" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/memory" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/tool" "git.yeij.top/AskaEth/Cyrene_v2/pkg/plugins/sdk" ) // ErrKnowledgeUnavailable 标记 knowledge 工具在**本次调用里根本不可用**: // 没有接知识库,或本回合没有解析出可用角色。它与「查得到但没命中」以及 // 「检索中途报错」都不同 —— 它对模型而言**不可自行修复**,因此拒绝文案必须 // 写明原因并要求改道,而不是让它对着同一堵墙反复重试(切片 KB1:安卓侧实测 // 连续两次「工具调用失败 · knowledge」,两次都是同一条无从纠正的 // ErrMissingPersona)。 var ErrKnowledgeUnavailable = errors.New("knowledge unavailable") // KnowledgeRetriever is the subset of the runtime memory retriever the knowledge // tool depends on. *memory.Retriever implements it; nil means no knowledge base // is configured. type KnowledgeRetriever interface { Retrieve(ctx context.Context, personaID, query string, topK int) ([]memory.Result, error) } // Compile-time assertion that knowledgeTool implements sdk.Tool. var _ sdk.Tool = (*knowledgeTool)(nil) // knowledgeTool answers a question from the configured knowledge base via a // memory retriever, returning ranked results or a clear message when absent. type knowledgeTool struct { retriever KnowledgeRetriever maxResults int log *slog.Logger } func newKnowledgeTool(t *tools) *knowledgeTool { return &knowledgeTool{ retriever: t.retriever, maxResults: t.maxResults, log: t.log, } } // Name implements sdk.Tool. func (k *knowledgeTool) Name() string { return "knowledge" } // Description implements sdk.Tool. func (k *knowledgeTool) Description() string { return "Query the knowledge base and long-term memory store for context relevant to a question." } // Parameters implements sdk.Tool. func (k *knowledgeTool) Parameters() (map[string]any, error) { return map[string]any{ "type": "object", "properties": map[string]any{ "query": map[string]any{"type": "string", "description": "Query topic."}, "persona": map[string]any{"type": "string", "description": "Persona scope id (optional; defaults to the persona of the current turn)."}, "top_k": map[string]any{"type": "integer", "description": "Max results."}, }, "required": []any{"query"}, }, nil } // Execute implements sdk.Tool. 它按「角色作用域 + query」检索记忆,并在工具 // 本身不可用时(未接知识库 / 本回合没有角色)返回一条**可消化的拒绝**:写明 // 原因并要求模型改道,而不是透出一个它无从纠正的底层错误。 func (k *knowledgeTool) Execute(ctx context.Context, args any) (any, error) { if k.retriever == nil { return nil, unavailableKnowledgeError("没有配置知识库") } m, ok := args.(map[string]any) if !ok { return nil, fmt.Errorf("io: knowledge: args must be an object") } query := argString(m, "query") if strings.TrimSpace(query) == "" { return nil, fmt.Errorf("io: knowledge: query is required") } persona, err := k.resolvePersona(ctx, m) if err != nil { return nil, err } topK := k.maxResults if v, ok := argInt(m, "top_k"); ok && v > 0 { topK = v } results, err := k.retriever.Retrieve(ctx, persona, query, topK) if err != nil { return nil, fmt.Errorf("io: knowledge: retrieve: %w", err) } // 群聊/陌生人(受限环境)只读非敏感的共享域记忆:复用循环注入路径上的 // 同一隔离判据(loop.FilterSharedVisible),绝不另造第二套 —— 否则一旦 // 管理面板把本工具放行给群聊/陌生人,就会返回角色日常(ScopePersona, // 主人私有)与 L4 意图记录(主人逐句原话),触碰 design/03 §5 // 「不泄露主人敏感信息」(审计 2026-10-05,P0 红线)。 if loop.SessionEnvFrom(ctx).Restricted() { results = loop.FilterSharedVisible(results) } out := make([]map[string]any, 0, len(results)) for _, r := range results { out = append(out, map[string]any{ "id": r.Item.ID, "kind": string(r.Item.Kind), "scope": string(r.Item.Scope), "content": r.Item.Content, "score": r.Score, }) } if len(out) == 0 { return map[string]any{ "query": query, "count": 0, "note": "no relevant knowledge found", "results": []map[string]any{}, }, nil } return map[string]any{"query": query, "count": len(out), "results": out}, nil } // resolvePersona 解析本次检索的角色作用域:显式参数 `persona` 优先;缺席时取 // **本回合角色**(循环在 Run 顶部经 tool.WithTurnPersona 发布,见 // internal/runtime/tool/turncontext.go)。 // // 为什么必须解析:记忆检索按角色作用域隔离(design/03 §3/§4),拿不到角色就 // 取不到正确的候选集。把空角色透给记忆存储只会换来它自己的 ErrMissingPersona // (memory.Store.ListByPersona 的硬校验)—— 模型既不知道要填哪个角色 id,参数 // 说明还写着 optional,于是只能对同一堵墙反复重试。两者都拿不到时返回可消化的 // 拒绝,让模型改道(design/24 §122:knowledge 无库/不可用时**明确报错**, // 而不是从广告面消失)。 func (k *knowledgeTool) resolvePersona(ctx context.Context, m map[string]any) (string, error) { if persona := strings.TrimSpace(argString(m, "persona")); persona != "" { return persona, nil } if persona := tool.TurnPersonaFrom(ctx); persona != "" { return persona, nil } return "", unavailableKnowledgeError("本次会话没有解析出可用角色") } // unavailableKnowledgeError 构造「本工具不可用」这一类失败的可消化拒绝: // 原因 + 明确要求改道。错误包住 ErrKnowledgeUnavailable,调用方据此以 // errors.Is 分类,文案本身则是给模型(以及工具失败卡片)看的。 func unavailableKnowledgeError(reason string) error { return fmt.Errorf("io: knowledge: %w(%s):重试不会成功,请改用其他工具或直接作答,不要重复调用本工具。", ErrKnowledgeUnavailable, reason) }