rwfx 全链是一棵树:除预期行为↔驱动因素 1:1 外其余皆 1:N,任何节点只有一个父节点、 绝无多对一/多对多/汇聚。但弱模型常把「叙事」连到一片驱动因素 → 汇聚乱线。 - extraction_spec:ChainSpec 加 single_parent 标志(rwfx=True,xdfx 有汇聚=False); helper is_single_parent_chain / chain_level_rank;强化 rwfx ②「叙事只有一个父驱动因素」 + 新增④「全链是树,每个非根节点恰一个 from」 - flow_extract:_collapse_to_single_parent 确定性兜底——纯树业务把每个节点的多父入边 收敛到「紧邻上一层」的单一父节点(只删多余入边、不新增、不动节点),在 final 前执行; 非纯树业务(xdfx)跳过 - dag-types/types:DAG 派活忠实本阶段目标——不擅自加数量上限/精简、不替席位加体例风格、 逐条全部输出、不诱导脱离输入编造(前后台同步) - Tests: tests/test_flow_extract_single_parent.py Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
79 lines
3.5 KiB
Python
79 lines
3.5 KiB
Python
"""rwfx 结构化抽取「纯树·单父节点」确定性兜底测试。
|
|
|
|
rwfx 全链是一棵树(除预期行为↔驱动因素 1:1,其余皆 1:N),任何节点只有一个父节点、绝无
|
|
多对一/汇聚。弱模型常给「叙事」连多个驱动因素 → 乱线。``_collapse_to_single_parent`` 兜底把
|
|
每个节点的多父入边收敛到紧邻上一层的单一父节点。
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from app.gateway.routers.flow_extract import _collapse_to_single_parent
|
|
from deerflow.agents.roundtable_orchestrator.extraction_spec import (
|
|
chain_level_rank,
|
|
is_single_parent_chain,
|
|
)
|
|
|
|
|
|
def test_is_single_parent_chain() -> None:
|
|
assert is_single_parent_chain("6BF") is True
|
|
assert is_single_parent_chain("rwfx") is True
|
|
assert is_single_parent_chain("rwfx-A") is True # 多智能体 -A 后缀也认
|
|
assert is_single_parent_chain("7BF") is False # xdfx 有「脆弱点→策略」汇聚,非纯树
|
|
assert is_single_parent_chain("3Q") is False # 未写实业务保守不收敛
|
|
assert is_single_parent_chain(None) is False
|
|
|
|
|
|
def test_chain_level_rank_rwfx() -> None:
|
|
rank = chain_level_rank("6BF")
|
|
assert rank == {"任务": 0, "目的": 1, "行为体": 2, "预期行为": 3, "驱动因素": 4, "叙事": 5}
|
|
|
|
|
|
def _rwfx_graph_with_bad_narrative() -> dict:
|
|
# 叙事 s1 错误地连了 2 个驱动因素(上一层) + 1 个行为体(更上层),应被收敛成单父。
|
|
return {
|
|
"title": "t",
|
|
"nodes": [
|
|
{"id": "n1", "group": "任务"},
|
|
{"id": "m1", "group": "目的"},
|
|
{"id": "a1", "group": "行为体"},
|
|
{"id": "b1", "group": "预期行为"},
|
|
{"id": "b2", "group": "预期行为"},
|
|
{"id": "d1", "group": "驱动因素"},
|
|
{"id": "d2", "group": "驱动因素"},
|
|
{"id": "s1", "group": "叙事"},
|
|
],
|
|
"edges": [
|
|
{"from": "n1", "to": "m1"},
|
|
{"from": "m1", "to": "a1"},
|
|
{"from": "a1", "to": "b1"},
|
|
{"from": "a1", "to": "b2"},
|
|
{"from": "b1", "to": "d1"},
|
|
{"from": "b2", "to": "d2"},
|
|
{"from": "d1", "to": "s1"}, # 叙事多父:驱动因素 d1
|
|
{"from": "d2", "to": "s1"}, # 叙事多父:驱动因素 d2
|
|
{"from": "a1", "to": "s1"}, # 叙事多父:行为体 a1(跨层)
|
|
],
|
|
}
|
|
|
|
|
|
def test_collapse_narrative_multi_parent_to_single() -> None:
|
|
out = _collapse_to_single_parent(_rwfx_graph_with_bad_narrative(), "6BF")
|
|
s1_parents = [e["from"] for e in out["edges"] if e["to"] == "s1"]
|
|
# 叙事只剩一个父;应保留紧邻上一层(驱动因素),不保留更上层的行为体;同层保留第一个。
|
|
assert s1_parents == ["d1"]
|
|
# 其余每个非根节点仍恰一个父(1:1 / 1:N 本就单父,不受影响)。
|
|
for nid in ("m1", "a1", "b1", "b2", "d1", "d2"):
|
|
assert len([e for e in out["edges"] if e["to"] == nid]) == 1
|
|
# 根节点无入边。
|
|
assert [e for e in out["edges"] if e["to"] == "n1"] == []
|
|
|
|
|
|
def test_collapse_noop_when_already_tree() -> None:
|
|
# 每个节点本就单父 → 边数不变、内容不变。
|
|
graph = {
|
|
"nodes": [{"id": "n1", "group": "任务"}, {"id": "m1", "group": "目的"}, {"id": "a1", "group": "行为体"}],
|
|
"edges": [{"from": "n1", "to": "m1"}, {"from": "m1", "to": "a1"}],
|
|
}
|
|
out = _collapse_to_single_parent(graph, "6BF")
|
|
assert out["edges"] == [{"from": "n1", "to": "m1"}, {"from": "m1", "to": "a1"}]
|