feat(search): append raw entity fields after view_node summary

view_node 按题两轮摘要(summarize_node)会吞掉 entities/visible_text
字段信号,Agent 站在证据节点上仍漏读实体(benchmark 错题 M1,案例
786-2、872-3、750-1)。dispatcher 侧在摘要后确定性追加 [实体]/[画面
文字] 原文区块,LLM 无法吞掉。

- TreeEnvironment.node_entity_fields:按层级取 card 实体字段原文,
  去空白、去重、分号拼接;空字段省键;未知节点抛 KeyError
- _handle_view_node Phase 2.5:摘要后、子节点概览前追加实体区块
- 附带 ruff format 修正 test_tree_environment.py 两处既有格式

算法 #11(树环境语义搜索)数据访问层扩展,不改搜索算法本身。
This commit is contained in:
2026-07-11 08:53:56 -04:00
parent 291a8108e1
commit 4e0e05210d
4 changed files with 226 additions and 2 deletions
+94 -2
View File
@@ -250,7 +250,8 @@ class TestGetNodeText:
"""锚模式应返回带锚文本和 anchor_map 字典。"""
env = TreeEnvironment(_make_test_index())
text, anchor_map = env.get_node_text(
"vid_L1_000_L2_000_L3_000", anchor=True,
"vid_L1_000_L2_000_L3_000",
anchor=True,
)
# 锚文本包含 [cN] 标记
assert "[c1]" in text
@@ -271,7 +272,8 @@ class TestGetNodeText:
"""无字幕的 L3 节点锚模式不应产生 [sN] 锚。"""
env = TreeEnvironment(_make_test_index())
text, anchor_map = env.get_node_text(
"vid_L1_000_L2_000_L3_001", anchor=True,
"vid_L1_000_L2_000_L3_001",
anchor=True,
)
assert anchor_map is not None
assert not any(k.startswith("s") for k in anchor_map)
@@ -322,3 +324,93 @@ class TestGetChildrenInfo:
env = TreeEnvironment(index)
children = env.get_children_info("vid_L1_000")
assert len(children[0]["summary"]) == 123 # 120 + "..."
# ── node_entity_fields 测试(Spec-1 B)───────────────────────
def _make_entity_test_index() -> TreeIndex:
"""带实体字段的最小三层树(含一个空字段 L2)。"""
l3 = L3Node(
id="vid_L1_000_L2_000_L3_000",
card=L3Card(
frame_summary="一名男子戴耳机",
visible_entities=["Bluetooth headset (both ears)", "man"],
ongoing_actions=["talking"],
visible_text=["EARPHONE BOTTLE OPENER"],
spatial_layout="man center",
visual_attributes={},
),
timestamp=10.0,
)
l2 = L2Node(
id="vid_L1_000_L2_000",
card=L2Card(
event_description="产品评测",
entities=["Bluetooth headset (both ears)", "reviewer"],
actions=["reviewing"],
action_subjects=["reviewer"],
visible_text=["$9.99"],
spatial_relations="",
state_changes=None,
),
time_range=(0.0, 60.0),
children=[l3],
)
l2_empty = L2Node(
id="vid_L1_000_L2_001",
card=L2Card(
event_description="空镜",
entities=[],
actions=[],
action_subjects=[],
visible_text=[],
spatial_relations="",
state_changes=None,
),
time_range=(60.0, 120.0),
)
l1 = L1Node(
id="vid_L1_000",
card=L1Card(
scene_summary="评测场景",
main_setting="室内",
key_entities=["reviewer"],
main_actions=["评测"],
topic_keywords=["数码"],
visible_text=[],
temporal_flow="线性",
),
time_range=(0.0, 120.0),
children=[l2, l2_empty],
)
return TreeIndex(metadata=IndexMeta("/test.mp4", "video"), roots=[l1])
class TestNodeEntityFields:
"""node_entity_fields 方法测试(B 修复:dispatcher 追加原文)。"""
def test_l2_entities_and_visible_text(self) -> None:
"""L2 节点应返回 entities 和 visible_text 原文。"""
env = TreeEnvironment(_make_entity_test_index())
fields = env.node_entity_fields("vid_L1_000_L2_000")
assert "Bluetooth headset (both ears)" in fields["实体"]
assert "$9.99" in fields["画面文字"]
def test_l3_visible_entities(self) -> None:
"""L3 节点应返回 visible_entities 和 visible_text 原文。"""
env = TreeEnvironment(_make_entity_test_index())
fields = env.node_entity_fields("vid_L1_000_L2_000_L3_000")
assert "Bluetooth headset (both ears)" in fields["实体"]
assert "EARPHONE BOTTLE OPENER" in fields["画面文字"]
def test_empty_fields_omitted(self) -> None:
"""实体/画面文字均为空时应返回空字典(不含空键)。"""
env = TreeEnvironment(_make_entity_test_index())
assert env.node_entity_fields("vid_L1_000_L2_001") == {}
def test_unknown_node_raises(self) -> None:
"""查询不存在的节点应抛出 KeyError。"""
env = TreeEnvironment(_make_entity_test_index())
with pytest.raises(KeyError):
env.node_entity_fields("nonexistent")