Some checks failed
kb-lint / lint (push) Has been cancelled
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
57 lines
2.2 KiB
Python
57 lines
2.2 KiB
Python
"""FastMCP 薄殼(AGENTS.md §8):search_wiki 與 read_page 兩個 tool。
|
||
查詢時只讀已編譯的 wiki 頁——不觸碰 raw 層、不做全庫掃描。
|
||
|
||
啟動(stdio):.venv/Scripts/python mcp/server.py
|
||
測試時可用環境變數 PP_QA_ROOT 指定專案根目錄。
|
||
"""
|
||
import os
|
||
import pathlib
|
||
import sys
|
||
|
||
ROOT = pathlib.Path(os.environ.get("PP_QA_ROOT",
|
||
pathlib.Path(__file__).resolve().parents[1]))
|
||
sys.path.insert(0, str(ROOT / "tools"))
|
||
import kb
|
||
import search as searchmod
|
||
from fastmcp import FastMCP
|
||
|
||
mcp = FastMCP("pp-qa-knowledge")
|
||
|
||
|
||
def _truncate(body, max_tokens):
|
||
# ponytail: token 估算用「1 token ≈ 1.5 字元」的 naive heuristic
|
||
# (繁中約 1 字/token、英文約 4 字元/token 的折衷);
|
||
# 升級路徑:以實際模型 tokenizer 校正。
|
||
limit = int(max_tokens * 1.5)
|
||
return (body, False) if len(body) <= limit else (body[:limit], True)
|
||
|
||
|
||
def search_wiki(query: str, top_k: int = 10) -> list:
|
||
"""BM25 檢索 wiki 頁,回傳 index 條目(path/title/description/tags)+ 相關度 score。
|
||
query 用繁體中文或英文關鍵詞。"""
|
||
return searchmod.search(ROOT, query, max(1, min(int(top_k), 10))) # 上限 10(AGENTS.md §8)
|
||
|
||
|
||
def read_page(path: str) -> dict:
|
||
"""讀取單一 wiki 頁全文:frontmatter + 內文(依 models.yaml token 上限截斷)
|
||
+ source_refs。path 例:wiki/entities/payment-gateway.md"""
|
||
cfg = kb.load_config(ROOT)
|
||
p = (ROOT / path).resolve()
|
||
wiki = (ROOT / "wiki").resolve()
|
||
# 信任邊界:擋路徑跳脫,只允許 wiki/ 下的 .md
|
||
if not p.is_relative_to(wiki) or p.suffix != ".md":
|
||
raise ValueError("path 須指向 wiki/ 下的 .md 頁面")
|
||
if not p.is_file():
|
||
raise FileNotFoundError(f"頁面不存在:{path}")
|
||
meta, body = kb.parse_page(p.read_text(encoding="utf-8"))
|
||
body, truncated = _truncate(body, cfg["limits"]["mcp_response_max_tokens"])
|
||
return {"frontmatter": meta, "body": body, "truncated": truncated,
|
||
"source_refs": [str(s) for s in meta.get("sources") or []]}
|
||
|
||
|
||
mcp.tool(search_wiki)
|
||
mcp.tool(read_page)
|
||
|
||
if __name__ == "__main__":
|
||
mcp.run()
|