fix(v2): 补回快照缺失的 v1 遗留模块 + 安全加固,基线 219 全绿

基线修复(快照离线不可运行的根因):
- 从 ce0f617 补回 executors/knowledge/memory/planner/trace/inference 六模块
  (v2 时代 router.py 自 v3 基线起依赖,但文件从未入库)
- 重建二级 subdomain 映射与 finance/life/education 内置规则族(对齐 8 领域设计与 test_trace 契约);
  新规则不带 template,Planner/执行行为零变化

安全加固(Mimosa 扫描 9 高危清零):
- 测试假凭据改环境变量间接读取(test_agent_api/test_architect/test_model_pool)
- fake_llama_server marker:env 仅传文件名、固定写入系统临时目录(write_text)
- setup_runtime 增加 zip-slip 成员路径校验、解压改 write_bytes;bench_tokens 改 Path.open
- runtime 健康检查仅允许回环地址并改用 http.client 定点连接(防 SSRF)
- gateway/llama_manager 与 workspace 持久化改用 Path 安全 API

pytest 219 passed
This commit is contained in:
tzt
2026-09-18 08:01:24 +08:00
parent 8d77c8c0c0
commit e9cfb29b75
35 changed files with 4848 additions and 30 deletions
+490
View File
@@ -0,0 +1,490 @@
"""知识库:专家系统风格的规则与知识表示(零依赖,纯标准库)。
设计原则(对齐《可行性调研与落地实现路线报告》第八章"专家系统内核"):
- 领域知识显式化:写在规则文件里(config/knowledge/<domain>.yaml),不藏在模型参数中
- 确定性:规则匹配 = 子串包含(大小写不敏感),同输入同输出
- 可解释:每次命中都记录规则 id,形成推理轨迹
- 最小参数:L0 模式零模型参数,规则即知识
规则文件格式(YAML;若 pyyaml 不可用,可提供同名 .json):
domain: code
rules:
- id: code-sort
priority: 90 # 越大越先触发
patterns: ["排序", "sort"] # 任一子串命中即触发
template: code-implement # 可选:Planner 任务模板 id
output: | # 可选:输出模板({query} 等占位符)
(规则输出)...
facts: # 领域事实表(Judge 校验 / retrieve 执行器用)
- id: legal-nc
keywords: ["竞业"]
statement: "竞业限制期限不得超过二年"
任务模板(config/knowledge/tasks.yaml):
task_templates:
code-implement:
steps:
- {id: analyze, kind: analyze, domain: code}
- {id: design, kind: design, domain: code, deps: [analyze]}
加载顺序:内置默认规则(代码内兜底)→ 文件规则按 id 合并覆盖。
"""
from __future__ import annotations
import json
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Dict, List, Optional
DEFAULT_RULES_DIR = Path(__file__).resolve().parent.parent / "config" / "knowledge"
@dataclass
class Rule:
"""一条领域规则。"""
id: str
domain: str
priority: int = 50
patterns: List[str] = field(default_factory=list)
template: Optional[str] = None # 引用的任务模板 id
output: Optional[str] = None # 输出模板
actions: List[str] = field(default_factory=list) # 保留字段:动作扩展
subdomain: Optional[str] = None # 二级子领域(如 investing/labor/calculus
subdomain2: Optional[str] = None # 三级子领域(如 fund/overtime/sorting
def matches(self, text: str) -> bool:
"""任一 pattern 是 text 的子串即命中(大小写不敏感)。"""
if not self.patterns:
return False
q = text.lower()
return any(p.lower() in q for p in self.patterns)
# ---------------------------------------------------------------
# 三级子领域映射(rule_id -> subdomain2
# 集中维护:新增规则时在此加一行即可完成三级细化标注
# ---------------------------------------------------------------
SUBDOMAIN2_MAP: Dict[str, str] = {
# ---- code ----
"code-sort": "sorting",
"code-debug": "error-analysis",
"code-algorithm": "algorithm-general",
"code-refactor": "code-quality",
"code-database": "sql",
"code-explain": "code-reading",
"code-test": "unit-test",
"code-web": "web-dev",
"code-implement-general": "implementation",
"code-git-knowledge": "git",
"code-docker-knowledge": "container",
"code-python-knowledge": "python-env",
# ---- math ----
"math-equation": "equation",
"math-calculus": "calculus",
"math-algebra": "algebra",
"math-geometry": "geometry",
"math-proof": "proof",
"math-probability": "probability",
"math-number-theory": "number-theory",
"math-trigonometry": "trigonometry",
"math-optimization": "optimization",
"math-general": "math-general",
# ---- legal ----
"legal-contract": "contract",
"legal-labor": "labor",
"legal-ip": "intellectual-property",
"legal-housing": "housing",
"legal-marriage": "family-law",
"legal-tax": "tax",
"legal-consumer": "consumer-rights",
"legal-litigation": "litigation",
"legal-compliance": "compliance",
"legal-general": "legal-general",
# ---- medical ----
"medical-hypertension": "hypertension",
"medical-drug": "medication",
"medical-common": "common-illness",
"medical-chronic": "chronic-disease",
"medical-digestive": "digestive",
"medical-nutrition": "nutrition",
"medical-mental": "mental-health",
"medical-firstaid": "first-aid",
"medical-pediatrics": "pediatrics",
"medical-general": "medical-general",
# ---- finance ----
"finance-investing": "investing",
"finance-saving": "saving",
"finance-loan": "loan",
"finance-insurance": "insurance",
"finance-credit-card": "credit",
"finance-personal-budget": "budgeting",
"finance-general": "finance-general",
# ---- life ----
"life-food": "cooking",
"life-travel": "travel",
"life-home": "home",
"life-pet": "pet",
"life-fitness": "fitness",
"life-weather": "weather",
"life-general": "life-general",
# ---- education ----
"edu-study-method": "study-method",
"edu-exam": "exam",
"edu-language": "language",
"edu-course": "course",
"edu-career": "career",
"edu-general": "education-general",
# ---- general ----
"general-explain": "explain",
"general-writing": "writing",
"general-compare": "compare",
"general-translate": "translate",
"general-knowledge": "explain",
}
# ---------------------------------------------------------------
# 二级子领域映射(rule_id -> subdomain
# 三级 subdomain2 的父级类别;与 SUBDOMAIN2_MAP 按 rule_id 对齐维护。
# ---------------------------------------------------------------
SUBDOMAIN_MAP: Dict[str, str] = {
# ---- code ----
"code-sort": "algorithm",
"code-debug": "debugging",
"code-algorithm": "algorithm",
"code-refactor": "quality",
"code-database": "data",
"code-explain": "reading",
"code-test": "quality",
"code-web": "web",
"code-implement-general": "implementation",
"code-git-knowledge": "tooling",
"code-docker-knowledge": "tooling",
"code-python-knowledge": "tooling",
# ---- math ----
"math-equation": "algebra",
"math-calculus": "analysis",
"math-algebra": "algebra",
"math-geometry": "geometry",
"math-proof": "proof",
"math-probability": "probability",
"math-number-theory": "number-theory",
"math-trigonometry": "trigonometry",
"math-optimization": "optimization",
"math-general": "general",
# ---- legal ----
"legal-contract": "contract",
"legal-labor": "labor",
"legal-ip": "ip",
"legal-housing": "civil",
"legal-marriage": "civil",
"legal-tax": "tax",
"legal-consumer": "consumer",
"legal-litigation": "procedure",
"legal-compliance": "compliance",
"legal-general": "general",
# ---- medical ----
"medical-hypertension": "chronic",
"medical-drug": "medication",
"medical-common": "common",
"medical-chronic": "chronic",
"medical-digestive": "common",
"medical-nutrition": "nutrition",
"medical-mental": "mental",
"medical-firstaid": "emergency",
"medical-pediatrics": "pediatrics",
"medical-general": "general",
# ---- finance ----
"finance-investing": "investing",
"finance-saving": "personal-finance",
"finance-loan": "credit",
"finance-insurance": "insurance",
"finance-credit-card": "credit",
"finance-personal-budget": "personal-finance",
"finance-general": "general",
# ---- life ----
"life-food": "daily",
"life-travel": "daily",
"life-home": "daily",
"life-pet": "daily",
"life-fitness": "health",
"life-weather": "daily",
"life-general": "general",
# ---- education ----
"edu-study-method": "learning",
"edu-exam": "learning",
"edu-language": "language",
"edu-course": "learning",
"edu-career": "development",
"edu-general": "general",
# ---- general ----
"general-explain": "explanation",
"general-writing": "writing",
"general-compare": "analysis",
"general-translate": "language",
"general-knowledge": "explanation",
}
# ---------------------------------------------------------------
# 内置默认规则(兜底:即使规则文件缺失/损坏,系统仍可运行)
# ---------------------------------------------------------------
BUILTIN_RULES: List[Dict[str, Any]] = [
# ---- code ----
{"id": "code-sort", "domain": "code", "priority": 90,
"patterns": ["排序", "快速排序", "排序算法", "sort", "quicksort"],
"template": "code-implement"},
{"id": "code-debug", "domain": "code", "priority": 85,
"patterns": ["报错", "错误", "调试", "bug", "debug", "typeerror", "异常", "报 TypeError"],
"template": "code-debug"},
{"id": "code-implement-general", "domain": "code", "priority": 50,
"patterns": ["实现", "编写", "写一个", "函数", "代码", "编程", "用 python", "用 java",
"用 javascript", "sql", "接口", "算法"],
"template": "code-implement"},
# ---- math ----
{"id": "math-equation", "domain": "math", "priority": 90,
"patterns": ["方程", "求解", "求根", "solve", "equation", "解方程"],
"template": "math-solve"},
{"id": "math-calculus", "domain": "math", "priority": 85,
"patterns": ["积分", "导数", "微积分", "求导", "integral", "derivative", ""],
"template": "math-solve"},
{"id": "math-general", "domain": "math", "priority": 50,
"patterns": ["数学", "证明", "定理", "概率", "统计", "计算", "等于", "math", "不等式"],
"template": "math-solve"},
# ---- legal ----
{"id": "legal-contract", "domain": "legal", "priority": 90,
"patterns": ["合同", "条款", "违约", "离职", "竞业", "劳动", "contract", "clause", "赔偿"],
"template": "legal-advice"},
{"id": "legal-ip", "domain": "legal", "priority": 85,
"patterns": ["专利", "版权", "商标", "知识产权", "patent", "copyright", "trademark"],
"template": "legal-advice"},
{"id": "legal-general", "domain": "legal", "priority": 50,
"patterns": ["法律", "合规", "诉讼", "仲裁", "法条", "law", "legal", "法规"],
"template": "legal-advice"},
# ---- medical ----
{"id": "medical-hypertension", "domain": "medical", "priority": 90,
"patterns": ["高血压", "hypertension", "血压"],
"template": "medical-advice"},
{"id": "medical-drug", "domain": "medical", "priority": 85,
"patterns": ["药物", "吃药", "剂量", "副作用", "退烧药", "降压药", "dosage", "prescription"],
"template": "medical-advice"},
{"id": "medical-general", "domain": "medical", "priority": 50,
"patterns": ["医疗", "症状", "诊断", "治疗", "感冒", "发烧", "糖尿病", "医生", "患者",
"体检", "疫苗", "medical", "symptom", "disease"],
"template": "medical-advice"},
# ---- finance ----
{"id": "finance-investing", "domain": "finance", "priority": 90,
"patterns": ["基金", "定投", "收益率", "股票", "投资", "炒股", "证券", "invest", "stock"]},
{"id": "finance-saving", "domain": "finance", "priority": 85,
"patterns": ["存款", "储蓄", "利息", "零钱通", "余额宝", "saving"]},
{"id": "finance-loan", "domain": "finance", "priority": 80,
"patterns": ["贷款", "房贷", "借款", "按揭", "loan"]},
{"id": "finance-insurance", "domain": "finance", "priority": 75,
"patterns": ["保险", "理赔", "保单", "投保", "insurance"]},
{"id": "finance-credit-card", "domain": "finance", "priority": 70,
"patterns": ["信用卡", "花呗", "白条", "credit card"]},
{"id": "finance-personal-budget", "domain": "finance", "priority": 60,
"patterns": ["预算", "记账", "开销", "省钱", "budget"]},
{"id": "finance-general", "domain": "finance", "priority": 50,
"patterns": ["金融", "财务", "外汇", "汇率", "finance"]},
# ---- life ----
{"id": "life-food", "domain": "life", "priority": 90,
"patterns": ["做饭", "做菜", "菜谱", "食谱", "烹饪", "cooking"]},
{"id": "life-travel", "domain": "life", "priority": 85,
"patterns": ["旅游", "旅行", "攻略", "景点", "签证", "travel"]},
{"id": "life-home", "domain": "life", "priority": 80,
"patterns": ["装修", "租房", "家电", "清洁", "搬家", "home"]},
{"id": "life-pet", "domain": "life", "priority": 75,
"patterns": ["宠物", "养猫", "养狗", "撸猫", "pet"]},
{"id": "life-fitness", "domain": "life", "priority": 70,
"patterns": ["健身", "减肥", "跑步", "锻炼", "fitness"]},
{"id": "life-weather", "domain": "life", "priority": 65,
"patterns": ["天气", "下雨", "台风", "降温", "weather"]},
{"id": "life-general", "domain": "life", "priority": 50,
"patterns": ["生活", "日常", "家居", "life"]},
# ---- education ----
{"id": "edu-study-method", "domain": "education", "priority": 90,
"patterns": ["学习方法", "记忆", "做笔记", "笔记法", "专注力"]},
{"id": "edu-exam", "domain": "education", "priority": 85,
"patterns": ["考试", "考研", "复习", "真题", "四六级", "exam"]},
{"id": "edu-language", "domain": "education", "priority": 80,
"patterns": ["英语", "单词", "口语", "语法", "english"]},
{"id": "edu-course", "domain": "education", "priority": 75,
"patterns": ["课程", "网课", "慕课", "选修", "course"]},
{"id": "edu-career", "domain": "education", "priority": 70,
"patterns": ["职业规划", "求职", "面试", "简历", "校招", "career"]},
{"id": "edu-general", "domain": "education", "priority": 50,
"patterns": ["教育", "大学", "专业选择", "education"]},
# ---- general ----
{"id": "general-explain", "domain": "general", "priority": 30,
"patterns": ["总结", "介绍", "解释", "为什么", "优缺点", "是什么", "翻译", "邮件",
"summarize", "explain", "what is", "写一封"],
"template": "general-explain"},
]
# 内置默认任务模板(兜底)
BUILTIN_TASKS: Dict[str, Dict[str, Any]] = {
"code-implement": {"steps": [
{"id": "analyze", "kind": "analyze", "domain": "code", "desc": "需求与约束分析"},
{"id": "design", "kind": "design", "domain": "code", "deps": ["analyze"], "desc": "算法与数据结构设计"},
{"id": "implement", "kind": "implement", "domain": "code", "deps": ["design"], "desc": "实现代码"},
{"id": "verify", "kind": "verify", "domain": "code", "deps": ["implement"], "desc": "自测校验"},
]},
"code-debug": {"steps": [
{"id": "analyze", "kind": "analyze", "domain": "code", "desc": "错误现象与复现分析"},
{"id": "diagnose", "kind": "diagnose", "domain": "code", "deps": ["analyze"], "desc": "定位错误根因"},
{"id": "fix", "kind": "fix", "domain": "code", "deps": ["diagnose"], "desc": "给出修复方案"},
{"id": "verify", "kind": "verify", "domain": "code", "deps": ["fix"], "desc": "修复后验证"},
]},
"math-solve": {"steps": [
{"id": "conditions", "kind": "analyze", "domain": "math", "desc": "明确已知条件与目标"},
{"id": "solve", "kind": "solve", "domain": "math", "deps": ["conditions"], "desc": "选择方法并求解"},
{"id": "verify", "kind": "verify", "domain": "math", "deps": ["solve"], "desc": "检查边界与验证"},
]},
"legal-advice": {"steps": [
{"id": "facts", "kind": "analyze", "domain": "legal", "desc": "梳理事实与法律问题"},
{"id": "retrieve", "kind": "retrieve", "domain": "legal", "deps": ["facts"], "desc": "检索适用法规"},
{"id": "conclude", "kind": "conclude", "domain": "legal", "deps": ["retrieve"], "desc": "给出法律意见"},
{"id": "disclaimer", "kind": "disclaimer", "domain": "legal", "deps": ["conclude"], "desc": "免责提示"},
]},
"medical-advice": {"steps": [
{"id": "symptoms", "kind": "analyze", "domain": "medical", "desc": "梳理症状与背景"},
{"id": "advise", "kind": "advise", "domain": "medical", "deps": ["symptoms"], "desc": "给出一般建议"},
{"id": "warning", "kind": "disclaimer", "domain": "medical", "deps": ["advise"], "desc": "就医警示"},
]},
"general-explain": {"steps": [
{"id": "outline", "kind": "analyze", "domain": "general", "desc": "梳理主题要点"},
{"id": "explain", "kind": "explain", "domain": "general", "deps": ["outline"], "desc": "展开解释"},
{"id": "conclude", "kind": "conclude", "domain": "general", "deps": ["explain"], "desc": "总结"},
]},
}
# 内置默认事实表(兜底)
BUILTIN_FACTS: Dict[str, List[Dict[str, Any]]] = {
"legal": [
{"id": "legal-noncompete", "keywords": ["竞业", "离职", "同业"],
"statement": "竞业限制期限不得超过二年,且用人单位应在限制期内按月给予经济补偿"},
{"id": "legal-renew-compensation", "keywords": ["不续签", "经济补偿", "劳动合同"],
"statement": "劳动合同期满用人单位不续签的,通常应支付经济补偿(每满一年一个月工资)"},
],
"medical": [
{"id": "medical-hypertension-diet", "keywords": ["高血压", "饮食"],
"statement": "高血压患者应低盐低脂饮食、控制体重、规律运动、戒烟限酒,并在医生指导下用药"},
{"id": "medical-fever-drug", "keywords": ["发烧", "退烧"],
"statement": "体温超过 38.5℃ 可在药师指导下使用退烧药;持续发热或出现严重症状应及时就医"},
],
}
def _try_load_yaml(path: Path) -> Optional[Dict[str, Any]]:
try:
import yaml # type: ignore
except ImportError:
return None
try:
with open(path, "r", encoding="utf-8") as f:
data = yaml.safe_load(f)
return data if isinstance(data, dict) else None
except Exception:
return None
def _try_load_json(path: Path) -> Optional[Dict[str, Any]]:
json_path = path.with_suffix(".json")
if not json_path.exists():
return None
try:
with open(json_path, "r", encoding="utf-8") as f:
data = json.load(f)
return data if isinstance(data, dict) else None
except Exception:
return None
class KnowledgeBase:
"""知识库:加载规则文件,提供规则匹配、任务模板、事实表查询。"""
def __init__(self, rules_dir: Optional[str | Path] = None):
self.rules_dir = Path(rules_dir) if rules_dir else DEFAULT_RULES_DIR
self._rules: Dict[str, Rule] = {}
self._tasks: Dict[str, Dict[str, Any]] = {}
self._facts: Dict[str, List[Dict[str, Any]]] = {}
self.load()
# ---- 加载 ----
def load(self) -> None:
"""内置默认 + 规则文件合并(文件规则按 id 覆盖内置)。"""
self._rules = {}
self._tasks = dict(BUILTIN_TASKS)
for item in BUILTIN_RULES:
self._register_rule(item)
self._facts = {d: [dict(f) for f in facts] for d, facts in BUILTIN_FACTS.items()}
if self.rules_dir.is_dir():
for f in sorted(self.rules_dir.glob("*.yaml")):
data = _try_load_yaml(f)
if data is not None:
self._load_file_data(f, data)
for f in sorted(self.rules_dir.glob("*.json")):
if f.name not in {p.name for p in self.rules_dir.glob("*.yaml")}:
data = _try_load_json(f)
if data is not None:
self._load_file_data(f, data)
def _load_file_data(self, path: Path, data: Dict[str, Any]) -> None:
name = path.stem
if name == "tasks":
for tid, tpl in (data.get("task_templates") or {}).items():
if isinstance(tpl, dict) and isinstance(tpl.get("steps"), list):
self._tasks[tid] = tpl
return
domain = data.get("domain", name)
for item in data.get("rules") or []:
if isinstance(item, dict) and item.get("id"):
self._register_rule({**item, "domain": domain})
for fact in data.get("facts") or []:
if isinstance(fact, dict) and fact.get("id"):
self._facts.setdefault(domain, []).append(fact)
def _register_rule(self, item: Dict[str, Any]) -> None:
rule = Rule(
id=str(item["id"]),
domain=str(item.get("domain", "general")),
priority=int(item.get("priority", 50)),
patterns=[str(p) for p in item.get("patterns", [])],
template=item.get("template"),
output=item.get("output"),
actions=[str(a) for a in item.get("actions", [])],
subdomain=item.get("subdomain") or SUBDOMAIN_MAP.get(str(item["id"])),
subdomain2=item.get("subdomain2") or SUBDOMAIN2_MAP.get(str(item["id"])),
)
self._rules[rule.id] = rule
# ---- 查询 ----
def match(self, text: str, domain: Optional[str] = None) -> List[Rule]:
"""返回命中的规则,按优先级降序。domain 为空则全领域匹配。"""
hits = []
for rule in self._rules.values():
if domain is not None and rule.domain != domain:
continue
if rule.matches(text):
hits.append(rule)
hits.sort(key=lambda r: r.priority, reverse=True)
return hits
def rule(self, rule_id: str) -> Optional[Rule]:
return self._rules.get(rule_id)
def rules_count(self) -> int:
return len(self._rules)
def task_template(self, tid: str) -> Optional[Dict[str, Any]]:
return self._tasks.get(tid)
def task_ids(self) -> List[str]:
return sorted(self._tasks.keys())
def facts(self, domain: str) -> List[Dict[str, Any]]:
return self._facts.get(domain, [])
def domains(self) -> List[str]:
return sorted({r.domain for r in self._rules.values()})