Files
PlantAI-rag/tests/test_qa_v1_1_73_question_decomposition.py

305 lines
11 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
from src.rag.qa_v1_1_73.question_decomposition import (
QUESTION_DECOMPOSITION_PROMPT,
QuestionDecompositionPlanner,
)
class FakeLLM:
available = True
def __init__(self, response):
self.response = response
self.calls = []
def complete_json(self, system, user, *, max_tokens=2500):
self.calls.append((system, user, max_tokens))
if isinstance(self.response, Exception):
raise self.response
return self.response
def _subquestion(index: int) -> dict:
return {
"subquestion_id": f"Q{index}",
"question": f"子问题{index}",
"role": "required" if index == 0 else "supporting",
"answer_slot": f"槽位{index}",
"anchors": [],
"target_entity_types": ["unknown"],
"relation_intents": ["lookup"],
"constraints": [],
"retrieval": {
"vector_top_k": 99,
"retrieve_native_chains": True,
"max_hops": 9,
"allow_incoming": True,
"allow_outgoing": True,
"max_attempts": 3,
},
"acceptance_requirements": {
"requires_anchor_connection": False,
"requires_terminal_type_match": True,
"requires_structured_assertion": True,
"requires_paper_provenance": True,
},
"contribution_to_final_answer": f"回答槽位{index}",
}
def test_prompt_contains_rules_schema_and_two_domain_examples():
assert "question_decomposition_v1" in QUESTION_DECOMPOSITION_PROMPT
assert "Always create Q0" in QUESTION_DECOMPOSITION_PROMPT
assert "vector_top_k=30" in QUESTION_DECOMPOSITION_PROMPT
assert "Attempt 3 is forbidden" in QUESTION_DECOMPOSITION_PROMPT
assert "R2R3-MYB" in QUESTION_DECOMPOSITION_PROMPT
assert "Litchi chinensis" in QUESTION_DECOMPOSITION_PROMPT
assert '"subquestions"' in QUESTION_DECOMPOSITION_PROMPT
def test_planner_enforces_fixed_retrieval_contract_and_six_question_cap():
response = {
"mode": "DECOMPOSE",
"prompt_version": "invented",
"original_question": "changed",
"question_type": "naming",
"answer_goal": "解释命名",
"global_constraints": {},
"subquestions": [_subquestion(index) for index in range(7)],
"synthesis_order": [f"Q{index}" for index in range(7)],
"decomposition_summary": "test",
}
planner = QuestionDecompositionPlanner(FakeLLM(response))
plan = planner.decompose(
"R2R3-MYB蛋白为什么被称为R2R3-MYB?",
verified_entities=[],
relation_intents=["naming_origin"],
answer_slots=["名称来源"],
)
assert plan["prompt_version"] == "question_decomposition_v1"
assert plan["original_question"] == "R2R3-MYB蛋白为什么被称为R2R3-MYB?"
assert len(plan["subquestions"]) == 6
assert plan["subquestions"][0]["subquestion_id"] == "Q0"
assert plan["subquestions"][0]["question"] == "R2R3-MYB蛋白为什么被称为R2R3-MYB?"
assert all(item["retrieval"]["vector_top_k"] == 30 for item in plan["subquestions"])
assert all(item["retrieval"]["max_attempts"] == 2 for item in plan["subquestions"])
assert all(item["retrieval"]["max_hops"] <= 3 for item in plan["subquestions"])
assert plan["synthesis_order"] == [f"Q{index}" for index in range(6)]
assert plan["plan_source"] == "llm_agent"
def test_planner_falls_back_to_atomic_q0_when_agent_output_fails():
planner = QuestionDecompositionPlanner(FakeLLM(ValueError("bad json")))
plan = planner.decompose(
"WRKY转录因子的典型结构特征是什么?",
verified_entities=[{"mention": "WRKY", "canonical_name": "WRKY"}],
relation_intents=["structure"],
answer_slots=["典型结构"],
)
assert plan["plan_source"] == "deterministic_fallback"
assert len(plan["subquestions"]) == 1
assert plan["subquestions"][0]["subquestion_id"] == "Q0"
assert plan["subquestions"][0]["question"] == "WRKY转录因子的典型结构特征是什么?"
assert plan["subquestions"][0]["retrieval"]["vector_top_k"] == 30
assert plan["subquestions"][0]["retrieval"]["max_attempts"] == 2
def test_planner_rejects_unverified_anchors_and_relation_intents():
response = {
"subquestions": [
{
**_subquestion(0),
"anchors": [
{
"mention": "WRKY",
"canonical_name": "WRKY",
"canonical_id": "cent_wrky",
"entity_type": "gene_family",
},
{
"mention": "InventedFamily",
"canonical_name": "InventedFamily",
"canonical_id": "cent_fake",
"entity_type": "gene_family",
},
],
"relation_intents": ["structure", "invented_relation"],
}
]
}
planner = QuestionDecompositionPlanner(FakeLLM(response))
plan = planner.decompose(
"WRKY转录因子的典型结构特征是什么?",
verified_entities=[
{
"mention": "WRKY",
"canonical_name": "WRKY",
"canonical_id": "cent_wrky",
"entity_type": "gene_family",
}
],
relation_intents=["structure"],
answer_slots=["典型结构"],
)
q0 = plan["subquestions"][0]
assert [item["canonical_id"] for item in q0["anchors"]] == ["cent_wrky"]
assert q0["relation_intents"] == ["structure"]
def test_planner_restores_verified_hsp_anchor_and_normalizes_mechanism_target_types():
response = {
"question_type": "mechanism",
"subquestions": [
{
**_subquestion(0),
"question": "changed",
"anchors": [],
"target_entity_types": [
"molecular_mechanism",
"protein_function",
"cellular_process",
],
"acceptance_requirements": {
"requires_anchor_connection": True,
"requires_terminal_type_match": True,
"requires_structured_assertion": True,
"requires_paper_provenance": True,
},
},
{
**_subquestion(1),
"question": "What mechanisms does the HSP gene family use under heat stress?",
"anchors": [],
"target_entity_types": ["molecular_mechanism"],
},
],
}
planner = QuestionDecompositionPlanner(FakeLLM(response))
plan = planner.decompose(
"HSP基因家族如何帮助植物应对高温?",
verified_entities=[
{
"mention": "HSP基因家族",
"canonical_name": "HSP gene family",
"canonical_id": "",
"entity_type": "gene_family",
}
],
relation_intents=[],
answer_slots=["molecular mechanism"],
)
assert all(
[anchor["canonical_name"] for anchor in item["anchors"]]
== ["HSP gene family"]
for item in plan["subquestions"]
)
assert plan["subquestions"][0]["target_entity_types"] == [
"process",
"biological_process",
]
assert plan["subquestions"][1]["target_entity_types"] == ["process"]
def test_mechanism_planner_drops_supporting_questions_that_invent_new_relations_or_entities():
response = {
"question_type": "mechanism",
"subquestions": [
{
**_subquestion(0),
"relation_intents": ["protein_homeostasis"],
},
{
**_subquestion(1),
"question": "HSP如何通过分子伴侣作用防止蛋白质聚集?",
"relation_intents": ["protein_homeostasis"],
},
{
**_subquestion(2),
"question": "HSP如何与蛋白酶体协作清除错误折叠蛋白?",
"relation_intents": ["proteolysis_cooperation", "protein_homeostasis"],
},
{
**_subquestion(3),
"question": "HSP90如何调节HSF信号?",
"relation_intents": ["protein_homeostasis"],
},
{
**_subquestion(4),
"question": "不同植物中有哪些具体HSP基因参与高温响应?",
"relation_intents": ["protein_homeostasis"],
},
],
}
planner = QuestionDecompositionPlanner(FakeLLM(response))
plan = planner.decompose(
"HSP基因家族如何帮助植物应对高温?",
verified_entities=[
{
"mention": "HSP基因家族",
"canonical_name": "HSP gene family",
"canonical_id": "",
"entity_type": "gene_family",
}
],
relation_intents=["protein_homeostasis"],
answer_slots=["molecular mechanism"],
)
assert [item["question"] for item in plan["subquestions"]] == [
"HSP基因家族如何帮助植物应对高温?",
"HSP如何通过分子伴侣作用防止蛋白质聚集?",
]
assert plan["synthesis_order"] == ["Q0", "Q1"]
def test_mechanism_planner_adds_safe_broad_support_when_all_agent_support_is_rejected():
response = {
"question_type": "mechanism",
"subquestions": [
{**_subquestion(0), "relation_intents": ["protein_homeostasis"]},
{
**_subquestion(1),
"question": "HSP90如何通过HSF-HSE信号通路调控热响应?",
"relation_intents": ["invented_signaling"],
},
],
}
planner = QuestionDecompositionPlanner(FakeLLM(response))
plan = planner.decompose(
"HSP基因家族如何帮助植物应对高温?",
verified_entities=[
{
"mention": "HSP基因家族",
"canonical_name": "HSP gene family",
"entity_type": "gene_family",
},
{
"mention": "高温",
"canonical_name": "high temperature",
"entity_type": "condition",
},
],
relation_intents=["protein_homeostasis"],
answer_slots=["molecular mechanism"],
)
assert len(plan["subquestions"]) == 2
q1 = plan["subquestions"][1]
assert q1["subquestion_id"] == "Q1"
assert "直接分子作用" in q1["question"]
assert "被保护对象" in q1["question"]
assert "HSP90" not in q1["question"]
assert "HSF" not in q1["question"]
assert q1["retrieval"]["vector_top_k"] == 30