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
+93
View File
@@ -0,0 +1,93 @@
"""前向链推理机:知识库规则驱动的工作记忆演化(专家系统推理核心,零依赖)。
流程(经典前向链 forward chaining):
1. 初始化黑板:写入领域/难度/置信度等事实
2. 循环:在领域内匹配规则(未触发过的)→ 按优先级执行
- 命中即记录轨迹 rule:<id>@<priority>
- 规则带 output 模板 → 渲染后写入黑板章节(部分解)
- 规则带 actions → 执行动作(写事实/写章节)
3. 终止:无新规则可触发 / 达到步数上限(防死循环)
确定性保证:规则匹配基于子串包含,无随机性;同输入 → 同轨迹。
"""
from __future__ import annotations
from typing import Any, Dict, List, Optional
from .knowledge import KnowledgeBase, Rule
from .memory import WorkingMemory
def render_template(template: str, query: str, facts: Dict[str, Any]) -> str:
"""渲染输出模板:替换 {query}{facts.<key>} 占位符;缺失以 [未提供] 占位,不抛异常。"""
out = template.replace("{query}", query)
for key, value in facts.items():
out = out.replace(f"{{facts.{key}}}", str(value))
# 剩余占位符兜底
while "{" in out and "}" in out:
start = out.find("{")
end = out.find("}", start)
if end == -1:
break
out = out[:start] + "[未提供]" + out[end + 1:]
return out
class InferenceEngine:
"""前向链推理机。"""
def __init__(self, kb: KnowledgeBase, max_steps: int = 20):
self.kb = kb
self.max_steps = max_steps
# ---------------------------------------------------------------
def initialize(self, query: str, domain: str, difficulty: str,
confidence: float, memory: WorkingMemory) -> None:
"""把分类结果写入黑板(事实初始化)。"""
memory.write_fact("query", query)
memory.write_fact("domain", domain)
memory.write_fact("difficulty", difficulty)
memory.write_fact("confidence", round(confidence, 4))
memory.add_trace(f"init:domain={domain},difficulty={difficulty},conf={confidence:.2f}")
# ---------------------------------------------------------------
def run(self, query: str, domain: str, memory: WorkingMemory,
max_steps: Optional[int] = None) -> List[str]:
"""前向链主循环。返回触发规则 id 列表(按触发顺序)。"""
steps = max_steps or self.max_steps
fired: List[str] = []
for _ in range(steps):
rules = self.kb.match(query, domain=domain)
# 选第一个"未触发过"的规则
target: Optional[Rule] = None
for r in rules:
if r.id not in fired:
target = r
break
if target is None:
break # 无新规则可触发 → 终止
fired.append(target.id)
self._fire(target, query, memory)
return fired
# ---------------------------------------------------------------
def _fire(self, rule: Rule, query: str, memory: WorkingMemory) -> None:
"""执行一条规则:记录轨迹 + 写事实 + 产出章节。"""
memory.add_trace(f"rule:{rule.id}@{rule.priority}")
# 规则动作
for action in rule.actions:
self._apply_action(action, rule, query, memory)
# 规则输出模板 → 章节
if rule.output:
text = render_template(rule.output, query, memory.facts)
memory.write_section(rule.id, text)
def _apply_action(self, action: str, rule: Rule, query: str,
memory: WorkingMemory) -> None:
"""动作格式:write_fact:key=valuevalue 支持 {query} 占位)。"""
if action.startswith("write_fact:"):
kv = action[len("write_fact:"):]
key, _, value = kv.partition("=")
value = value.replace("{query}", query)
memory.write_fact(key.strip(), value.strip(), rule_id=rule.id)
# 其他动作类型暂不实现(保留扩展位)