diff --git a/agent/qa/src/rag/qa_v1_1_73/answer_composer.py b/agent/qa/src/rag/qa_v1_1_73/answer_composer.py index d5d580f..0ab0fc4 100644 --- a/agent/qa/src/rag/qa_v1_1_73/answer_composer.py +++ b/agent/qa/src/rag/qa_v1_1_73/answer_composer.py @@ -272,7 +272,8 @@ When citing a paper for the first time, you may insert a marker [[FIG:PMCID]] on lines = [ "--- Dialogue Context ---", "", - "以下是本会话此前的问答记录,仅供消解指代(如「它」「该基因」)使用。", + "以下是本会话此前的问答记录。若当前问题包含指代词(如「它」「该基因」「这些成员」「上面提到的」),", + "必须用本段记录确定指代对象,再按指代对象结合 Data Report 回答;答案开头先点明指代对象。", "除指代消解外,不得据此补充新事实;一切事实必须来自 Data Report / GraphChains。", ] for msg in history: diff --git a/tooling/cap/cap_selfcheck.py b/tooling/cap/cap_selfcheck.py index 4b58246..9a6285a 100644 --- a/tooling/cap/cap_selfcheck.py +++ b/tooling/cap/cap_selfcheck.py @@ -1,48 +1,104 @@ #!/usr/bin/env python3 -"""tooling/cap/cap_selfcheck.py —— 机制端到端自检(假 MCP server + 热加载证明)。 +"""tooling/cap/cap_selfcheck.py —— 机制端到端自检(单进程热加载证明)。 -流程(全程同一 QA 进程,不重启): - 1. 加载现有 capabilities.yaml → registry 工具数 - 2. 临时注册假 MCP echo 工具(manifest 落盘)→ reload → 工具表出现 echo - 3. 实际调用 echo(stdio 子进程)→ 返回正确 - 4. 停用(移除 manifest)→ reload → 工具表消失、MCP 连接关闭 +流程(单一子进程内完成,与生产同语义——registry 在启动时加载一次, +之后的 yaml 变化必须靠 reload() 热加载,diff 才有意义): + 1. 初始工具表(空 yaml → 无 fake_echo) + 2. install_manager.register_manifest 落盘 → reload → diff.added 含 fake_echo + 3. 实际调用 echo(MCP stdio 子进程)→ 返回正确 + 4. unregister → reload → diff.removed 含 fake_echo、工具表为空 5. 恢复原 capabilities.yaml 用法: - QA_ENV_FILE= python3 cap_selfcheck.py + QA_PYTHON= python3 cap_selfcheck.py - = deploy 树根(含 configs/capabilities.yaml), -即 $GENE_FAMILY_ROOT 的值。测试中的 yaml 落盘用其真实路径,因此会短暂修改 -配置文件;本脚本保证恢复。 + = 编译树根(含 configs/capabilities.yaml),即 GENE_FAMILY_ROOT。 """ from __future__ import annotations import json import os -import shutil import subprocess import sys -import tempfile -import time +import textwrap from pathlib import Path TOOL_DIR = Path(__file__).parent FAKE_MCP = TOOL_DIR / "fake_mcp_server.py" -FAKE_MANIFEST = """ -capabilities: - - name: fake_echo - provider: mcp - permission: read_only - description: 自检用 echo 工具 - session_default: true - schema: - text: {type: string, required: true, description: 要回显的文本} - mcp: - command: {python} - args: ["{fake_mcp}"] - tool: echo -""" +INNER = textwrap.dedent( + """ + import json, sys, textwrap + from pathlib import Path + + root = Path(sys.argv[1]) + sys.path.insert(0, str(root)) + sys.path.insert(0, str(root / "src")) + + from src.rag.qa_v1_1_73.capabilities.registry import get_registry + from src.rag.qa_v1_1_73.capabilities.install_manager import InstallManager, get_install_manager + from src.rag.qa_v1_1_73.capabilities.providers import get_dispatcher + + caps_yaml = root / "configs" / "capabilities.yaml" + original = caps_yaml.read_text(encoding="utf-8") + + manifest = ( + "capabilities:\\n" + " - name: fake_echo\\n" + " provider: mcp\\n" + " permission: read_only\\n" + " description: 自检用 echo 工具\\n" + " session_default: true\\n" + " schema:\\n" + " text: {type: string, required: true, description: 要回显的文本}\\n" + " mcp:\\n" + " command: " + sys.argv[2] + "\\n" + " args: [\\"" + sys.argv[3] + "\\"]\\n" + " tool: echo\\n" + ) + + results = {} + try: + # 1. 初始工具表 + r = get_registry() + results["initial_tools"] = [t.name for t in r.list_all()] + assert "fake_echo" not in results["initial_tools"], "预置状态异常" + + # 2. 安装 + 热加载(同进程,diff 有意义) + manager = InstallManager(caps_yaml) + manifests, warnings = manager.register_manifest(manifest) + assert warnings == [] and [m.name for m in manifests] == ["fake_echo"] + w, diff = manager.reload(registry=r) + assert "fake_echo" in diff["added"], f"热加载未新增: {diff}" + results["hot_reload_added"] = diff["added"] + results["registry_after_add"] = [t.name for t in r.list_all()] + + # 3. MCP 实际调用 + m = r.get("fake_echo").manifest + out = get_dispatcher().execute(m, {"text": "你好机制"}) + assert "你好机制" in out, f"echo 返回异常: {out}" + results["mcp_call_ok"] = True + + # 4. 卸载 + 热卸载(同进程) + assert manager.unregister("fake_echo") + w, diff = manager.reload(registry=r) + assert "fake_echo" in diff["removed"], f"热卸载未移除: {diff}" + get_dispatcher().shutdown_connections(diff["removed"]) + results["hot_reload_removed"] = diff["removed"] + results["registry_after_remove"] = [t.name for t in r.list_all()] + + # 5. 恢复 + caps_yaml.write_text(original, encoding="utf-8") + r.reload() + results["restored"] = True + + print("SELFCHECK PASS:", json.dumps(results, ensure_ascii=False, indent=2)) + except Exception as exc: + print(f"SELFCHECK FAIL: {exc}") + caps_yaml.write_text(original, encoding="utf-8") + sys.exit(1) + """ +) def _python() -> str: @@ -51,102 +107,24 @@ def _python() -> str: ) -def _qa_import(root: Path): - """在目标 QA 树环境里跑一段 python(用 deploy 树的 venv + PYTHONPATH)。""" - env = dict(os.environ) - env["PYTHONPATH"] = f"{root}:{root}/src" + ( - f":{env['PYTHONPATH']}" if env.get("PYTHONPATH") else "" - ) - return env - - -def run_step(root: Path, code: str) -> str: - env = _qa_import(root) - proc = subprocess.run( - [_python(), "-c", code], - capture_output=True, - text=True, - timeout=120, - env=env, - cwd=str(root), - ) - if proc.returncode != 0: - raise RuntimeError(f"step failed: {proc.stderr[-2000:]}") - return proc.stdout.strip() - - def main() -> int: root = Path(sys.argv[1]).resolve() if len(sys.argv) > 1 else None if root is None or not (root / "configs").is_dir(): print(__doc__) return 2 - caps_yaml = root / "configs" / "capabilities.yaml" - original = caps_yaml.read_text(encoding="utf-8") if caps_yaml.is_file() else "capabilities: []\n" - - manifest = FAKE_MANIFEST.format(python=_python(), fake_mcp=FAKE_MCP) - results = {} - try: - # 1. 初始工具表 - out = run_step(root, ( - "from src.rag.qa_v1_1_73.capabilities.registry import get_registry;" - "r=get_registry();" - "print(json.dumps({'tools':[t.name for t in r.list_all()]," - "'warnings':r.load_warnings}, ensure_ascii=False))" - )) - initial = json.loads(out) - results["initial_tools"] = initial["tools"] - assert "fake_echo" not in initial["tools"], "预置状态异常:fake_echo 已在工具表" - - # 2. 注册 + reload(热加载) - caps_yaml.write_text(manifest, encoding="utf-8") - out = run_step(root, ( - "from src.rag.qa_v1_1_73.capabilities.registry import get_registry;" - "w,d=get_registry().reload();" - "print(json.dumps({'warnings':w,'diff':d}, ensure_ascii=False))" - )) - reloaded = json.loads(out) - assert "fake_echo" in reloaded["diff"]["added"], "热加载未新增 fake_echo" - results["hot_reload_added"] = reloaded["diff"]["added"] - - # 3. 实际调用(MCP stdio 子进程) - out = run_step(root, ( - "from src.rag.qa_v1_1_73.capabilities.registry import get_registry;" - "from src.rag.qa_v1_1_73.capabilities.providers import get_dispatcher;" - "m=get_registry().get('fake_echo').manifest;" - "r=get_dispatcher().execute(m, {'text':'你好机制'});" - "print(r)" - )) - assert "你好机制" in out, f"echo 返回异常: {out}" - results["mcp_call_ok"] = True - - # 4. 停用 + reload(热卸载) - caps_yaml.write_text("capabilities: []\n", encoding="utf-8") - out = run_step(root, ( - "from src.rag.qa_v1_1_73.capabilities.registry import get_registry;" - "from src.rag.qa_v1_1_73.capabilities.providers import get_dispatcher;" - "w,d=get_registry().reload();" - "get_dispatcher().shutdown_connections(d['removed']);" - "print(json.dumps({'warnings':w,'diff':d}, ensure_ascii=False))" - )) - unloaded = json.loads(out) - assert "fake_echo" in unloaded["diff"]["removed"], "热卸载未移除 fake_echo" - results["hot_reload_removed"] = unloaded["diff"]["removed"] - - # 5. 恢复原文件 - caps_yaml.write_text(original, encoding="utf-8") - run_step(root, ( - "from src.rag.qa_v1_1_73.capabilities.registry import get_registry;" - "get_registry().reload();print('restored')" - )) - results["restored"] = True - - print("SELFCHECK PASS:", json.dumps(results, ensure_ascii=False, indent=2)) - return 0 - except Exception as exc: - print(f"SELFCHECK FAIL: {exc}") - return 1 - finally: - caps_yaml.write_text(original, encoding="utf-8") + env = dict(os.environ) + env["GENE_FAMILY_ROOT"] = str(root) + proc = subprocess.run( + [_python(), "-c", INNER, str(root), _python(), str(FAKE_MCP)], + capture_output=True, + text=True, + timeout=180, + env=env, + cwd=str(root), + ) + sys.stdout.write(proc.stdout) + sys.stderr.write(proc.stderr[-3000:]) + return proc.returncode if __name__ == "__main__":