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:
@@ -0,0 +1,103 @@
|
||||
"""规则 Planner:把查询拆解为子任务 DAG(任务分解,专家系统风格,零参数)。
|
||||
|
||||
拆解逻辑(确定性规则):
|
||||
1. 在分类领域内匹配知识规则
|
||||
2. 取最高优先级且带 template 的命中规则 → 对应任务模板
|
||||
3. 非 easy 难度且有模板 → 生成多节点 DAG(模板 steps 转 TaskNode,含依赖)
|
||||
4. easy 难度或无模板命中 → 单节点直接求解(不拆,最小开销)
|
||||
5. 拆解深度防护:节点不再递归拆解(当前为单层拆解,模板本身即最终粒度)
|
||||
|
||||
对齐架构目标:"路由模型把任务拆解后分步骤交给各个小模型",
|
||||
L0 模式下各子任务由规则执行器完成(零参数),L2 模式可交给本地小模型。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import List, Optional
|
||||
|
||||
from .knowledge import KnowledgeBase
|
||||
from .memory import TaskGraph, TaskNode
|
||||
from .models import Classification
|
||||
|
||||
# 单节点求解时按领域选择默认动作 kind
|
||||
_SINGLE_KIND = {
|
||||
"code": "implement",
|
||||
"math": "solve",
|
||||
"legal": "conclude",
|
||||
"medical": "advise",
|
||||
"general": "explain",
|
||||
"finance": "conclude",
|
||||
"life": "advise",
|
||||
"education": "design",
|
||||
}
|
||||
|
||||
# 强制拆解领域:即使 easy 也走完整任务模板
|
||||
# (legal 需要 retrieve+disclaimer,medical 需要 advise+warning,
|
||||
# finance 需要 retrieve+风险免责——均为领域硬要求)
|
||||
FORCE_SPLIT_DOMAINS = {"legal", "medical", "finance"}
|
||||
|
||||
# 强制拆解模板:命中即拆(debug 流程必须 analyze→diagnose→fix→verify)
|
||||
FORCE_SPLIT_TEMPLATES = {"code-debug"}
|
||||
|
||||
|
||||
class Planner:
|
||||
"""规则 Planner:查询 → 子任务 DAG。"""
|
||||
|
||||
def __init__(self, kb: KnowledgeBase, max_depth: int = 3):
|
||||
self.kb = kb
|
||||
self.max_depth = max_depth
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
def plan(self, query: str, classification: Classification) -> TaskGraph:
|
||||
domain = classification.domain
|
||||
difficulty = classification.difficulty
|
||||
|
||||
# 1. 领域内匹配规则,取最高优先级带模板的规则
|
||||
template_id: Optional[str] = None
|
||||
hits = self.kb.match(query, domain=domain)
|
||||
for h in hits:
|
||||
if h.template:
|
||||
template_id = h.template
|
||||
break
|
||||
|
||||
graph = TaskGraph()
|
||||
|
||||
# 2. 非 easy / 强制拆解领域 / 强制拆解模板 → 多节点 DAG
|
||||
if template_id and (difficulty != "easy"
|
||||
or domain in FORCE_SPLIT_DOMAINS
|
||||
or template_id in FORCE_SPLIT_TEMPLATES):
|
||||
tpl = self.kb.task_template(template_id)
|
||||
if tpl and tpl.get("steps"):
|
||||
for step in tpl["steps"]:
|
||||
node = TaskNode(
|
||||
id=str(step["id"]),
|
||||
kind=str(step.get("kind", "solve")),
|
||||
domain=str(step.get("domain", domain)),
|
||||
query=query,
|
||||
deps=[str(d) for d in step.get("deps", [])],
|
||||
desc=str(step.get("desc", "")),
|
||||
)
|
||||
graph.add_node(node)
|
||||
return graph
|
||||
|
||||
# 3. easy / 无模板 → 单节点
|
||||
kind = _SINGLE_KIND.get(domain, "explain")
|
||||
graph.add_node(TaskNode(
|
||||
id="solve",
|
||||
kind=kind,
|
||||
domain=domain,
|
||||
query=query,
|
||||
desc=f"单节点求解({domain}/{difficulty})",
|
||||
))
|
||||
return graph
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
def explain_plan(self, graph: TaskGraph) -> List[str]:
|
||||
"""把 DAG 渲染为可读的拆解轨迹(用于 route 与 --trace)。"""
|
||||
if len(graph) == 1:
|
||||
n = graph.nodes()[0]
|
||||
return [f"plan:single[{n.kind}]"]
|
||||
parts = []
|
||||
for n in graph.topo_order():
|
||||
dep = f"<{','.join(n.deps)}" if n.deps else ""
|
||||
parts.append(f"{n.id}:{n.kind}{dep}")
|
||||
return [f"plan:multi[{len(graph)}]({' -> '.join(parts)})"]
|
||||
Reference in New Issue
Block a user