Files
PlantAI-rag/tests/test_qa_v1_1_73_agentic_controller.py

1020 lines
33 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 src.rag.qa_v1_1_73.agentic_controller import AgenticRetrievalController
from src.rag.qa_v1_1_73.schemas import EvidenceItem, HypothesisEntity, RetrievalHypothesis
QUESTION = "荔枝中有哪些基因家族已经被研究"
class FakeLLM:
available = True
def __init__(self, *responses):
self.responses = list(responses)
self.calls = []
def complete_json(self, system, user, *, max_tokens=2500):
self.calls.append((system, user, max_tokens))
response = self.responses.pop(0)
if isinstance(response, Exception):
raise response
return response
def test_controller_caps_replans_at_one():
controller = AgenticRetrievalController(FakeLLM(), max_replans=99)
assert controller.max_replans == 1
def hypothesis():
return RetrievalHypothesis(
question_type="survey",
direct_question_entities=[
HypothesisEntity(
mention="荔枝",
canonical_candidate="Litchi chinensis",
entity_type="species",
origin="question",
confidence=1.0,
verification_status="verified_alias",
matched_kb_entity="Litchi chinensis",
use_as_graph_seed=True,
),
HypothesisEntity(
mention="基因家族",
canonical_candidate="gene family",
entity_type="gene_family",
origin="question",
confidence=1.0,
verification_status="question_direct_unverified",
use_as_graph_seed=True,
),
],
relation_intents=["studied_gene_families", "identified"],
answer_slots=["list of gene family names"],
)
def agent_plan(**overrides):
value = {
"mode": "PLAN",
"question_analysis": {
"language": "zh",
"intent": "列出荔枝中研究过的基因家族",
"operation": "enumerate",
"expected_answer_shape": "entity_list",
"requires_graph": True,
},
"anchors": [
{
"mention": "荔枝",
"canonical_name": "Litchi chinensis",
"canonical_id": "",
"entity_type": "species",
"role": "start_anchor",
"confidence": 1.0,
"required_in_every_answer_path": True,
}
],
"answer_targets": [
{
"description": "基因家族",
"allowed_entity_types": ["gene_family"],
"role": "answer_endpoint",
}
],
"relation_plan": {
"relation_intents": ["identified", "studied_gene_families"],
"allowed_relation_categories": [],
"relation_name_hints": ["identified_in", "reported_in"],
"allow_incoming": True,
"allow_outgoing": True,
"semantic_direction": "anchor_to_answer",
"preserve_assertion_direction": True,
},
"graph_plan": {
"enabled": True,
"max_hops": 1,
"min_hops": 1,
"anchor_only_start": True,
"required_anchor_coverage": 1.0,
"required_terminal_types": ["gene_family"],
"permit_reverse_navigation": True,
"exclude_unrelated_vector_start_seeds": True,
},
"vector_plan": {
"enabled": True,
"purpose": "supporting_evidence",
"queries": ["Litchi chinensis gene family"],
"may_create_graph_start_seeds": False,
},
"acceptance_gate": {
"minimum_supported_candidates": 1,
"require_anchor_in_each_path": True,
"require_terminal_type_match": True,
"require_traceable_provenance": True,
"reject_cross_species_substitution": True,
},
"reasoning_summary": "以荔枝为锚点枚举相连基因家族",
}
value.update(overrides)
return value
def test_plan_gives_agent_direction_seed_and_endpoint_control():
controller = AgenticRetrievalController(FakeLLM(agent_plan()))
plan = controller.plan(QUESTION, hypothesis())
assert plan["anchors"] == [
{
"mention": "荔枝",
"canonical_name": "Litchi chinensis",
"canonical_id": "",
"entity_type": "species",
"role": "start_anchor",
"confidence": 1.0,
"required_in_every_answer_path": True,
}
]
assert plan["answer_targets"][0]["allowed_entity_types"] == ["gene_family"]
assert plan["relation_plan"]["allow_incoming"] is True
assert plan["graph_plan"]["anchor_only_start"] is True
assert plan["graph_plan"]["max_hops"] == 1
def test_agent_prompt_allows_structured_assertions_without_graph_for_definition_questions():
llm = FakeLLM(agent_plan())
controller = AgenticRetrievalController(llm)
controller.plan(QUESTION, hypothesis())
system_prompt = llm.calls[0][0]
assert "definition, naming, or structure" in system_prompt
assert "source, relation, target" in system_prompt
assert "graph path is optional" in system_prompt
def test_plan_removes_invented_anchor_and_never_uses_generic_target_as_start():
unsafe = agent_plan(
anchors=[
{
"mention": "TCP",
"canonical_name": "TCP gene family",
"canonical_id": "cent_invented",
"entity_type": "gene_family",
"role": "start_anchor",
"confidence": 1.0,
"required_in_every_answer_path": True,
},
{
"mention": "基因家族",
"canonical_name": "gene family",
"canonical_id": "",
"entity_type": "gene_family",
"role": "start_anchor",
"confidence": 1.0,
"required_in_every_answer_path": True,
},
],
graph_plan={"max_hops": 99},
)
controller = AgenticRetrievalController(FakeLLM(unsafe))
plan = controller.plan(QUESTION, hypothesis())
assert [item["canonical_name"] for item in plan["anchors"]] == ["Litchi chinensis"]
assert plan["graph_plan"]["max_hops"] == 3
def test_plan_falls_back_deterministically_when_agent_json_fails():
controller = AgenticRetrievalController(FakeLLM(ValueError("bad json")))
plan = controller.plan(QUESTION, hypothesis())
assert plan["plan_source"] == "deterministic_fallback"
assert plan["anchors"][0]["canonical_name"] == "Litchi chinensis"
assert plan["answer_targets"][0]["allowed_entity_types"] == ["gene_family"]
assert plan["relation_plan"]["allow_incoming"] is True
assert plan["graph_plan"]["max_hops"] == 1
def litchi_evidence():
return EvidenceItem(
evidence_id="E1",
kb="V16/V15",
source="SPL gene family",
relation="18_homologues_identified",
target="Litchi chinensis",
evidence="Eighteen SPL homologues were identified.",
paper_id="10.1080_14620316.2018.1543557",
doi="10.1080/14620316.2018.1543557",
raw={
"assertion_id": "cass_spl_litchi",
"source_type": "gene_family",
"target_type": "species",
"anchor_name": "Litchi chinensis",
"answer_entity": "SPL gene family",
"answer_entity_type": "gene_family",
},
)
def test_review_accepts_only_real_anchor_connected_ids():
llm = FakeLLM(
{
"mode": "REVIEW",
"decision": "ACCEPT",
"attempt": 0,
"accepted_evidence_ids": ["E1", "E_INVENTED"],
"accepted_chain_ids": ["AG1", "G_INVENTED"],
"problems": [],
"replan": {"required": False},
"final_handoff": {"allowed": True},
}
)
controller = AgenticRetrievalController(llm)
chains = [
{
"chain_id": "AG1",
"start": "Litchi chinensis",
"end": "SPL gene family",
"answer_entity": "SPL gene family",
"answer_entity_type": "gene_family",
"anchor_name": "Litchi chinensis",
"doi": "10.1080/14620316.2018.1543557",
"hops": [],
}
]
review = controller.review(
QUESTION, agent_plan(), [litchi_evidence()], chains, attempt=0
)
assert review["decision"] == "ACCEPT"
assert review["accepted_evidence_ids"] == ["E1"]
assert review["accepted_chain_ids"] == ["AG1"]
assert review["final_handoff"]["allowed"] is True
def test_review_overrides_false_accept_when_anchor_is_absent():
llm = FakeLLM(
{
"mode": "REVIEW",
"decision": "ACCEPT",
"attempt": 0,
"accepted_evidence_ids": ["E2"],
"accepted_chain_ids": ["G2"],
"problems": [],
"replan": {"required": False},
"final_handoff": {"allowed": True},
}
)
controller = AgenticRetrievalController(llm)
unrelated = EvidenceItem(
evidence_id="E2",
kb="V16/V15",
source="TCP gene family",
relation="identified_in",
target="strawberry",
evidence="TCP evidence",
doi="10.1/tcp",
raw={"source_type": "gene_family", "target_type": "species"},
)
review = controller.review(
QUESTION,
agent_plan(),
[unrelated],
[{"chain_id": "G2", "start": "TCP gene family", "end": "MYB family"}],
attempt=0,
)
assert review["decision"] == "REPLAN"
assert review["final_handoff"]["allowed"] is False
def test_review_rejects_cross_species_count_comparison_even_when_llm_accepts_it():
llm = FakeLLM(
{
"mode": "REVIEW",
"decision": "ACCEPT",
"attempt": 0,
"accepted_evidence_ids": ["E_COMPARE"],
"accepted_chain_ids": [],
"final_handoff": {"allowed": True},
}
)
controller = AgenticRetrievalController(llm)
comparison = EvidenceItem(
evidence_id="E_COMPARE",
kb="V16/V15",
source="HcARF genes",
relation="number_similar_to",
target="Litchi chinensis",
evidence="The number of HcARF genes was similar to that in Litchi chinensis.",
pmid="34052884",
raw={
"source_type": "gene_family",
"target_type": "species",
"anchor_name": "Litchi chinensis",
"answer_entity_type": "gene_family",
},
)
review = controller.review(QUESTION, agent_plan(), [comparison], [], attempt=0)
assert review["decision"] == "REPLAN"
assert review["accepted_evidence_ids"] == []
def test_review_abstains_after_one_failed_replan():
controller = AgenticRetrievalController(FakeLLM({"decision": "REPLAN"}))
review = controller.review(QUESTION, agent_plan(), [], [], attempt=1)
assert review["decision"] == "ABSTAIN"
assert review["final_handoff"]["allowed"] is True
assert review["accepted_evidence_ids"] == []
assert review["accepted_chain_ids"] == []
def test_review_can_accept_the_result_of_the_first_replan():
controller = AgenticRetrievalController(
FakeLLM(
{
"decision": "ACCEPT",
"accepted_evidence_ids": ["E1"],
"accepted_chain_ids": [],
"final_handoff": {"allowed": True},
}
)
)
review = controller.review(
QUESTION, agent_plan(), [litchi_evidence()], [], attempt=1
)
assert review["decision"] == "ACCEPT"
assert review["accepted_evidence_ids"] == ["E1"]
def test_review_treats_unknown_terminal_type_as_unconstrained():
controller = AgenticRetrievalController(
FakeLLM(
{
"decision": "ACCEPT",
"accepted_evidence_ids": ["E_R2R3"],
"accepted_chain_ids": [],
"final_handoff": {"allowed": True},
}
)
)
evidence = EvidenceItem(
evidence_id="E_R2R3",
kb="V16/V15",
source="R2R3-MYB proteins",
relation="contain",
target="R2 and R3 MYB repeats",
evidence="R2R3-MYB proteins contain the R2 and R3 MYB repeats.",
doi="10.1000/r2r3",
raw={
"source_type": "protein",
"target_type": "domain",
"answer_entity_type": "domain",
},
)
plan = {
"anchors": [],
"question_analysis": {
"operation": "lookup",
"expected_answer_shape": "fact",
},
"graph_plan": {"required_terminal_types": ["unknown"]},
"acceptance_gate": {
"require_anchor_in_each_path": False,
"require_traceable_provenance": True,
},
}
review = controller.review(
"R2R3-MYB蛋白为什么被称为R2R3-MYB?",
plan,
[evidence],
[],
attempt=0,
)
assert review["decision"] == "ACCEPT"
assert review["accepted_evidence_ids"] == ["E_R2R3"]
def test_deterministic_handoff_keeps_structured_direct_evidence_but_enforces_anchor():
controller = AgenticRetrievalController(FakeLLM())
direct = EvidenceItem(
evidence_id="E_R2R3",
kb="V16/V15",
source="R2R3-MYB proteins",
relation="contain",
target="R2 and R3 MYB repeats",
evidence="R2R3-MYB proteins contain the R2 and R3 MYB repeats.",
doi="10.1000/r2r3",
raw={"answer_entity_type": "domain"},
)
unrelated = EvidenceItem(
evidence_id="E_TCP",
kb="V16/V15",
source="TCP gene family",
relation="identified_in",
target="Arabidopsis thaliana",
evidence="TCP genes were identified in Arabidopsis thaliana.",
doi="10.1000/tcp",
raw={"answer_entity_type": "gene_family"},
)
direct_items, _ = controller.deterministic_handoff(
{
"anchors": [],
"graph_plan": {"required_terminal_types": ["unknown"]},
},
[direct, unrelated],
[],
)
strict_items, _ = controller.deterministic_handoff(
{
"anchors": [{"canonical_name": "Litchi chinensis"}],
"graph_plan": {"required_terminal_types": ["gene_family"]},
},
[unrelated],
[],
)
assert [item.evidence_id for item in direct_items] == ["E_R2R3", "E_TCP"]
assert strict_items == []
def hsp_hypothesis():
return RetrievalHypothesis(
question_type="mechanism",
direct_question_entities=[
HypothesisEntity(
mention="HSP基因家族",
canonical_candidate="HSP gene family",
entity_type="gene_family",
origin="question",
confidence=0.9,
verification_status="question_direct_unverified",
),
HypothesisEntity(
mention="植物",
canonical_candidate="plants",
entity_type="species",
origin="question",
confidence=0.9,
verification_status="question_direct_unverified",
),
HypothesisEntity(
mention="高温",
canonical_candidate="high temperature",
entity_type="condition",
origin="question",
confidence=0.9,
verification_status="question_direct_unverified",
),
],
relation_intents=["heat stress response", "chaperone function"],
answer_slots=["molecular mechanism", "protected target", "biological outcome"],
)
def test_mechanism_plan_keeps_question_literal_anchors_and_rejects_wrong_gene_terminal():
unsafe = agent_plan(
anchors=[
{
"mention": "HSP基因家族",
"canonical_name": "HSP gene family",
"entity_type": "gene_family",
},
{
"mention": "植物",
"canonical_name": "plants",
"entity_type": "species",
},
{
"mention": "高温",
"canonical_name": "high temperature",
"entity_type": "condition",
},
],
answer_targets=[
{
"description": "wrong endpoint",
"allowed_entity_types": ["gene_family"],
}
],
graph_plan={
"enabled": True,
"max_hops": 3,
"required_terminal_types": ["gene"],
},
)
controller = AgenticRetrievalController(FakeLLM(unsafe))
plan = controller.plan("HSP基因家族如何帮助植物应对高温?", hsp_hypothesis())
assert [item["canonical_name"] for item in plan["anchors"]] == [
"HSP gene family",
"high temperature",
]
allowed = plan["answer_targets"][0]["allowed_entity_types"]
assert "process" in allowed
assert "protein" in allowed
assert "gene" not in allowed
assert "gene_family" not in allowed
assert set(plan["graph_plan"]["required_terminal_types"]).issubset(set(allowed))
assert plan["graph_plan"]["max_hops"] == 2
assert plan["question_analysis"]["operation"] == "mechanism"
def _hsp_evidence(evidence_id, relation, target, evidence):
return EvidenceItem(
evidence_id=evidence_id,
kb="V16/V15",
source="HSPs",
relation=relation,
target=target,
evidence=evidence,
paper_id="10.1000_hsp",
doi="10.1000/hsp",
raw={"source_type": "protein", "target_type": "process"},
)
def test_group_review_does_not_apply_hsp_specific_mechanism_vocabulary_gate():
llm = FakeLLM(
{
"groups": [
{
"subquestion_id": "Q0",
"status": "SATISFIED",
"accepted_evidence_ids": [
"E_UP",
"E_TOL",
"E_PREVENT",
"E_REFOLD",
"E_INVENTED",
],
"accepted_chain_ids": ["G1", "G_INVENTED"],
"missing_answer_slots": [],
"refinement": {},
}
]
}
)
controller = AgenticRetrievalController(llm)
evidence = [
_hsp_evidence("E_UP", "upregulated_in", "heat stress", "HSPs were upregulated under heat stress."),
_hsp_evidence("E_TOL", "enhance", "heat tolerance", "HSPs enhance heat tolerance."),
_hsp_evidence(
"E_PREVENT",
"prevents",
"protein aggregation",
"HSPs act as molecular chaperones and prevent protein aggregation.",
),
_hsp_evidence(
"E_REFOLD",
"assist_refolding",
"heat-damaged proteins",
"HSPs promote refolding of heat-damaged proteins.",
),
]
chains = [
{
"chain_id": "G1",
"hops": [
{
"source": "HSPs",
"relation": "prevents",
"target": "protein aggregation",
"evidence": "HSPs prevent protein aggregation.",
"doi": "10.1000/hsp",
}
],
}
]
review = controller.review_subquestion_groups(
"HSP基因家族如何帮助植物应对高温?",
[
{
"subquestion_id": "Q0",
"question": "HSP基因家族如何帮助植物应对高温?",
"answer_slot": "molecular mechanism",
"target_entity_types": ["process", "protein", "trait"],
"acceptance_requirements": {
"requires_structured_assertion": True,
"requires_paper_provenance": True,
},
"evidence": evidence,
"chains": chains,
}
],
attempt=1,
)
group = review["groups"][0]
assert group["status"] == "SATISFIED"
assert group["accepted_evidence_ids"] == [
"E_UP",
"E_TOL",
"E_PREVENT",
"E_REFOLD",
]
assert group["accepted_chain_ids"] == ["G1"]
assert "protein-homeostasis mechanisms" not in llm.calls[0][0]
def test_group_review_recovers_valid_mechanism_ids_when_llm_describes_support_but_omits_ids():
llm = FakeLLM(
{
"groups": [
{
"subquestion_id": "Q1",
"status": "PARTIAL",
"accepted_evidence_ids": [],
"accepted_chain_ids": [],
"missing_answer_slots": [],
"review_reason": "Evidence directly states that HSPs prevent aggregation and refold damaged proteins, satisfying the answer slot.",
"refinement": {},
}
]
}
)
controller = AgenticRetrievalController(llm)
evidence = [
_hsp_evidence("E_UP", "upregulated_in", "heat stress", "HSPs were upregulated under heat stress."),
_hsp_evidence(
"E_MECH",
"act_as",
"molecular chaperones",
"HSPs prevent protein aggregation and promote refolding of heat-damaged proteins.",
),
]
review = controller.review_subquestion_groups(
"HSP基因家族如何帮助植物应对高温?",
[
{
"subquestion_id": "Q1",
"question": "HSP如何通过分子伴侣作用维持蛋白质稳态?",
"answer_slot": "molecular mechanism",
"acceptance_requirements": {
"requires_structured_assertion": True,
"requires_paper_provenance": True,
},
"evidence": evidence,
"chains": [],
}
],
attempt=1,
)
group = review["groups"][0]
assert group["accepted_evidence_ids"] == ["E_UP", "E_MECH"]
assert group["status"] == "PARTIAL"
assert group["selection_source"] == "deterministic_valid_evidence_recovery"
def test_group_review_accepts_gst_mechanism_evidence_without_hsp_terms():
llm = FakeLLM(
{
"groups": [
{
"subquestion_id": "Q1",
"status": "PARTIAL",
"accepted_evidence_ids": [],
"accepted_chain_ids": [],
"missing_answer_slots": [],
"review_reason": (
"Evidence provides direct molecular actions including "
"detoxification, peroxide scavenging, and redox regulation."
),
"refinement": {},
}
]
}
)
controller = AgenticRetrievalController(llm)
evidence = EvidenceItem(
evidence_id="Q1.A1.S1",
kb="V16/V15",
source="GST gene family",
relation="detoxifies",
target="reactive electrophiles and peroxides",
evidence=(
"Plant GSTs catalyze glutathione conjugation and contribute to "
"peroxide scavenging and cellular redox homeostasis under stress."
),
pmid="28848585",
raw={"source_type": "gene_family", "target_type": "biological_process"},
)
review = controller.review_subquestion_groups(
"GST基因家族在植物胁迫响应中有什么作用?",
[
{
"subquestion_id": "Q1",
"question": "GST通过哪些分子作用参与植物胁迫响应?",
"answer_slot": "molecular mechanism",
"anchors": [
{"mention": "GST基因家族", "canonical_name": "GST gene family"}
],
"target_entity_types": ["biological_process"],
"acceptance_requirements": {
"requires_anchor_connection": True,
"requires_structured_assertion": True,
"requires_paper_provenance": True,
},
"evidence": [evidence],
"chains": [],
}
],
attempt=1,
)
group = review["groups"][0]
assert group["accepted_evidence_ids"] == ["Q1.A1.S1"]
assert group["status"] == "PARTIAL"
assert group["selection_source"] == "deterministic_valid_evidence_recovery"
def test_group_review_recovers_valid_ids_without_matching_review_phrase():
llm = FakeLLM(
{
"groups": [
{
"subquestion_id": "Q0",
"status": "PARTIAL",
"accepted_evidence_ids": [],
"accepted_chain_ids": [],
"missing_answer_slots": [],
"review_reason": (
"Both direct and chain evidence support the overall role."
),
"refinement": {},
}
]
}
)
controller = AgenticRetrievalController(llm)
evidence = EvidenceItem(
evidence_id="Q0.A1.S1",
kb="V16/V15",
source="GST gene family",
relation="involved_in",
target="plant stress response",
evidence="GST genes participate in plant responses to abiotic stress.",
pmid="28848585",
raw={"source_type": "gene_family", "target_type": "biological_process"},
)
review = controller.review_subquestion_groups(
"GST基因家族在植物胁迫响应中有什么作用?",
[
{
"subquestion_id": "Q0",
"question": "GST基因家族在植物胁迫响应中有什么作用?",
"answer_slot": "biological role",
"anchors": [
{"mention": "GST基因家族", "canonical_name": "GST gene family"}
],
"acceptance_requirements": {
"requires_anchor_connection": True,
"requires_structured_assertion": True,
"requires_paper_provenance": True,
},
"evidence": [evidence],
"chains": [],
}
],
attempt=1,
)
group = review["groups"][0]
assert group["accepted_evidence_ids"] == ["Q0.A1.S1"]
assert group["status"] == "PARTIAL"
assert group["selection_source"] == "deterministic_valid_evidence_recovery"
def test_group_review_creates_one_deterministic_refinement_when_no_evidence_or_ids():
llm = FakeLLM(
{
"groups": [
{
"subquestion_id": "Q1",
"status": "EMPTY",
"accepted_evidence_ids": [],
"accepted_chain_ids": [],
"missing_answer_slots": [],
"review_reason": "No usable evidence was selected.",
"refinement": {},
}
]
}
)
controller = AgenticRetrievalController(llm)
group = {
"subquestion_id": "Q1",
"question": "GST通过哪些分子作用参与植物胁迫响应?",
"answer_slot": "molecular mechanism",
"anchors": [
{"mention": "GST基因家族", "canonical_name": "GST gene family"}
],
"relation_intents": ["detoxifies", "scavenges", "regulates"],
"target_entity_types": ["biological_process"],
"evidence": [],
"chains": [],
}
first = controller.review_subquestion_groups(
"GST基因家族在植物胁迫响应中有什么作用?", [group], attempt=1
)["groups"][0]
second = controller.review_subquestion_groups(
"GST基因家族在植物胁迫响应中有什么作用?", [group], attempt=2
)["groups"][0]
assert first["refinement"]["vector_queries"]
assert first["refinement"]["relation_name_hints"] == [
"detoxifies",
"scavenges",
"regulates",
]
assert second["refinement"] == {}
def test_group_review_accepts_hsp_subfamily_evidence_for_hsp_family_mechanism_anchor():
controller = AgenticRetrievalController(FakeLLM({"groups": []}))
evidence = EvidenceItem(
evidence_id="E_SHSP",
kb="V16/V15",
source="sHSP",
relation="helps",
target="damaged protein refolding",
evidence="sHSPs prevent damaged protein aggregation and help damaged proteins refold.",
pmcid="PMC9201338",
raw={"source_type": "protein_family", "target_type": "biological_process"},
)
review = controller.review_subquestion_groups(
"HSP基因家族如何帮助植物应对高温?",
[
{
"subquestion_id": "Q1",
"question": "HSP通过哪些分子伴侣机制维持蛋白质稳态?",
"answer_slot": "molecular mechanism",
"anchors": [
{
"mention": "HSP基因家族",
"canonical_name": "HSP gene family",
}
],
"target_entity_types": ["biological_process"],
"acceptance_requirements": {
"requires_anchor_connection": True,
"requires_structured_assertion": True,
"requires_paper_provenance": True,
},
"evidence": [evidence],
"chains": [],
}
],
attempt=1,
)
assert review["groups"][0]["accepted_evidence_ids"] == ["E_SHSP"]
def test_group_review_rejects_cold_stress_mechanism_for_heat_stress_question():
controller = AgenticRetrievalController(FakeLLM({"groups": []}))
cold = _hsp_evidence(
"E_COLD",
"upregulated_in",
"cold environments",
"HSPs protect plants from freezing-induced protein denaturation under cold stress.",
)
heat = _hsp_evidence(
"E_HEAT",
"prevents",
"protein denaturation",
"Under heat stress, HSPs prevent protein denaturation and aggregation.",
)
review = controller.review_subquestion_groups(
"HSP基因家族如何帮助植物应对高温?",
[
{
"subquestion_id": "Q1",
"question": "HSP应对高温的分子机制是什么?",
"answer_slot": "molecular mechanism",
"evidence": [cold, heat],
"chains": [],
}
],
attempt=1,
)
assert review["groups"][0]["accepted_evidence_ids"] == ["E_HEAT"]
def test_group_review_sends_compact_evidence_not_full_raw_payload():
llm = FakeLLM({"groups": []})
controller = AgenticRetrievalController(llm)
evidence = _hsp_evidence(
"E_COMPACT",
"prevents",
"protein aggregation",
"HSPs prevent protein aggregation.",
)
evidence.raw = {
"source_type": "protein",
"target_type": "process",
"large_internal_blob": "DO_NOT_SEND" * 1000,
}
controller.review_subquestion_groups(
"HSP基因家族如何帮助植物应对高温?",
[
{
"subquestion_id": "Q0",
"question": "HSP基因家族如何帮助植物应对高温?",
"answer_slot": "mechanism",
"evidence": [evidence],
"chains": [],
}
],
attempt=1,
)
user_payload = llm.calls[0][1]
assert "E_COMPACT" in user_payload
assert '"source_type": "protein"' in user_payload
assert "DO_NOT_SEND" not in user_payload
def test_group_review_rejects_comparison_only_evidence_for_species_enumeration():
llm = FakeLLM(
{
"groups": [
{
"subquestion_id": "Q0",
"status": "SATISFIED",
"accepted_evidence_ids": ["E_COMPARE"],
"accepted_chain_ids": [],
"refinement": {},
}
]
}
)
controller = AgenticRetrievalController(llm)
comparison = EvidenceItem(
evidence_id="E_COMPARE",
kb="V16/V15",
source="HcARF genes",
relation="number_similar_to",
target="Litchi chinensis",
evidence="The number of HcARF genes was similar to Litchi chinensis.",
pmid="34052884",
raw={"source_type": "gene_family", "target_type": "species"},
)
review = controller.review_subquestion_groups(
"荔枝中有哪些基因家族已经被研究?",
[
{
"subquestion_id": "Q0",
"question": "荔枝中有哪些基因家族已经被研究?",
"answer_slot": "gene family list",
"anchors": [
{
"mention": "荔枝",
"canonical_name": "Litchi chinensis",
"entity_type": "species",
}
],
"target_entity_types": ["gene_family"],
"acceptance_requirements": {
"requires_anchor_connection": True,
"requires_terminal_type_match": True,
"requires_structured_assertion": True,
"requires_paper_provenance": True,
},
"evidence": [comparison],
"chains": [],
}
],
attempt=1,
)
assert review["groups"][0]["status"] == "PARTIAL"
assert review["groups"][0]["accepted_evidence_ids"] == []