Spaces:
Running
Running
R14-CIT-A1: adaptive citation sufficiency gate
#57
by GuXiaopeng - opened
- core/generation_runner.py +217 -29
- core/pipeline_agent.py +3 -3
core/generation_runner.py
CHANGED
|
@@ -34,9 +34,15 @@ from utils.file_parser import extract_text_from_files
|
|
| 34 |
|
| 35 |
ProgressCallback = Callable[..., None]
|
| 36 |
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
TRANSLATION_CONTRACT_VERSION = 'hierarchical-multilingual-v5'
|
| 41 |
|
| 42 |
|
|
@@ -1493,6 +1499,66 @@ class GenerationRunner:
|
|
| 1493 |
"ratio": round(ratio, 4), "status": status, "recommendation": recommendation, "nonfatal": True,
|
| 1494 |
}
|
| 1495 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1496 |
def _resolve_paths(self, items) -> list[str]:
|
| 1497 |
output = []
|
| 1498 |
for item in items or []:
|
|
@@ -1615,16 +1681,16 @@ class GenerationRunner:
|
|
| 1615 |
refs_text = str(state.get("refs_text") or "")
|
| 1616 |
if not refs_text:
|
| 1617 |
self._progress(8, "S2 + OpenAlex")
|
|
|
|
| 1618 |
refs_text = agent.fetch_references_parallel(
|
| 1619 |
topic,
|
| 1620 |
keywords,
|
| 1621 |
-
min_required=
|
| 1622 |
progress_callback=lambda: self._progress(9, "S2 + OpenAlex"),
|
| 1623 |
)
|
| 1624 |
-
if
|
| 1625 |
-
raise
|
| 1626 |
-
|
| 1627 |
-
)
|
| 1628 |
if user_lit_context:
|
| 1629 |
core_refs = agent.parse_user_core_literature(user_lit_context)
|
| 1630 |
if core_refs:
|
|
@@ -1699,29 +1765,86 @@ class GenerationRunner:
|
|
| 1699 |
if jit_texts:
|
| 1700 |
rag_engine._initialize_chunks([jit_texts])
|
| 1701 |
self._progress(15, "citation_planning")
|
| 1702 |
-
|
| 1703 |
-
adaptive_structure,
|
| 1704 |
)
|
| 1705 |
-
|
| 1706 |
-
|
| 1707 |
-
|
|
|
|
| 1708 |
)
|
| 1709 |
-
|
| 1710 |
-
|
|
|
|
|
|
|
|
|
|
| 1711 |
for items in citation_plan.values()
|
| 1712 |
for item in items
|
| 1713 |
if item.get("tag")
|
| 1714 |
}
|
| 1715 |
-
|
| 1716 |
-
|
| 1717 |
-
|
| 1718 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1719 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1720 |
state.update(
|
| 1721 |
{
|
| 1722 |
"adaptive_structure": adaptive_structure,
|
| 1723 |
"quote_invocation_plan": quote_invocation_plan,
|
| 1724 |
"citation_plan": citation_plan,
|
|
|
|
|
|
|
|
|
|
| 1725 |
"research_logic_contract": research_logic_contract,
|
| 1726 |
"working_abstract": working_abstract,
|
| 1727 |
"working_keywords": working_keywords,
|
|
@@ -1753,6 +1876,16 @@ class GenerationRunner:
|
|
| 1753 |
state.get("research_logic_contract") or agent.global_memory.get("research_logic_contract") or ""
|
| 1754 |
)
|
| 1755 |
citation_plan = state.get("citation_plan") if isinstance(state.get("citation_plan"), dict) else {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1756 |
translation_sections = dict(state.get('translation_sections') or {})
|
| 1757 |
translation_glossary = dict(state.get('translation_glossary') or {})
|
| 1758 |
canonical_concept_ledger = state.get('canonical_concept_ledger')
|
|
@@ -1866,9 +1999,20 @@ class GenerationRunner:
|
|
| 1866 |
if not citation_plan:
|
| 1867 |
self._progress(15, "citation_planning")
|
| 1868 |
citation_plan = agent.build_claim_evidence_plan(
|
| 1869 |
-
adaptive_structure, topic, synthesis_result, mode=mode,
|
|
|
|
| 1870 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1871 |
state["citation_plan"] = citation_plan
|
|
|
|
|
|
|
|
|
|
| 1872 |
|
| 1873 |
quote_invocation_plan = (
|
| 1874 |
dict(state.get("quote_invocation_plan") or {}) if mode == "asq" else {}
|
|
@@ -2105,9 +2249,13 @@ Output only final academic prose. Do not report the word count."""
|
|
| 2105 |
used_after = set(agent.used_citations)
|
| 2106 |
survived = used_after - used_before
|
| 2107 |
if assigned_tag_list and not is_quarantined:
|
|
|
|
| 2108 |
required_survival = min(
|
| 2109 |
len(assigned_tag_list),
|
| 2110 |
-
max(
|
|
|
|
|
|
|
|
|
|
| 2111 |
)
|
| 2112 |
if len(survived) < required_survival:
|
| 2113 |
missing_items = [
|
|
@@ -2127,6 +2275,13 @@ Output only final academic prose. Do not report the word count."""
|
|
| 2127 |
excluded_tags=used_after | {str(item.get("tag") or "") for item in recovery_items},
|
| 2128 |
)
|
| 2129 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2130 |
supplemental_raw = agent.build_citation_recovery_paragraph(
|
| 2131 |
recovery_items[:needed], section_name=display_name, section_context=en_resolved
|
| 2132 |
)
|
|
@@ -2140,6 +2295,15 @@ Output only final academic prose. Do not report the word count."""
|
|
| 2140 |
en_resolved = en_resolved.rstrip() + "\n\n" + supplemental_resolved.strip()
|
| 2141 |
used_after = set(agent.used_citations)
|
| 2142 |
survived = used_after - used_before
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2143 |
if mode == "asq" and section_id == "findings":
|
| 2144 |
en_resolved = self._render_planned_qualitative_quotes(
|
| 2145 |
en_resolved,
|
|
@@ -2188,6 +2352,10 @@ Output only final academic prose. Do not report the word count."""
|
|
| 2188 |
"context_tail": context_tail,
|
| 2189 |
"accumulated_deviation": accumulated_deviation,
|
| 2190 |
"quality_diagnostics": quality_diagnostics,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2191 |
"agent_state": agent.export_state(),
|
| 2192 |
"data_metadata": data_metadata,
|
| 2193 |
}
|
|
@@ -2215,18 +2383,33 @@ Output only final academic prose. Do not report the word count."""
|
|
| 2215 |
if ref and ref not in seen_refs:
|
| 2216 |
references.append(f"- {ref}")
|
| 2217 |
seen_refs.add(ref)
|
| 2218 |
-
if
|
| 2219 |
-
raise
|
| 2220 |
-
|
| 2221 |
-
|
| 2222 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2223 |
full_draft_en = full_draft_en.rstrip() + "\n\n# References\n" + "\n\n".join(references)
|
| 2224 |
actual_words = self._english_count_for_target(full_draft_en)
|
| 2225 |
self._upsert_quality_diagnostic(
|
| 2226 |
quality_diagnostics, self._validate_target_length(full_draft_en, target_words)
|
| 2227 |
)
|
| 2228 |
body_only, _reference_tail = agent.validate_reference_tail(
|
| 2229 |
-
full_draft_en, minimum_references=
|
| 2230 |
)
|
| 2231 |
agent.validate_manuscript_structure(body_only, adaptive_structure, translated=False)
|
| 2232 |
agent.enforce_resolved_claim_specific_citations(body_only)
|
|
@@ -2245,7 +2428,7 @@ Output only final academic prose. Do not report the word count."""
|
|
| 2245 |
full_draft_en = replace_abstract(full_draft_en, final_abstract)
|
| 2246 |
full_draft_en = self._replace_keywords(full_draft_en, final_keywords)
|
| 2247 |
body_only, _reference_tail = agent.validate_reference_tail(
|
| 2248 |
-
full_draft_en, minimum_references=
|
| 2249 |
)
|
| 2250 |
agent.validate_manuscript_structure(body_only, adaptive_structure, translated=False)
|
| 2251 |
agent.enforce_resolved_claim_specific_citations(body_only)
|
|
@@ -2282,6 +2465,11 @@ Output only final academic prose. Do not report the word count."""
|
|
| 2282 |
"final_abstract": final_abstract,
|
| 2283 |
"final_keywords": final_keywords,
|
| 2284 |
"front_matter_phase": "final",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2285 |
"quality_diagnostics": quality_diagnostics,
|
| 2286 |
"english_finalized": True,
|
| 2287 |
"translation_complete": False,
|
|
@@ -2305,7 +2493,7 @@ Output only final academic prose. Do not report the word count."""
|
|
| 2305 |
)
|
| 2306 |
self._shutdown_translation_worker(wait=True)
|
| 2307 |
body_only, reference_tail = agent.validate_reference_tail(
|
| 2308 |
-
full_draft_en, minimum_references=
|
| 2309 |
)
|
| 2310 |
section_order = self._unique_top_level_sections(adaptive_structure)
|
| 2311 |
if not section_order:
|
|
|
|
| 34 |
|
| 35 |
ProgressCallback = Callable[..., None]
|
| 36 |
|
| 37 |
+
CITATION_SEMANTIC_SURVIVAL_RATIO = 0.78
|
| 38 |
+
CITATION_ROLE_RULES = {
|
| 39 |
+
"introduction": (3, 180),
|
| 40 |
+
"theory": (3, 180),
|
| 41 |
+
"theoretical_framing": (3, 180),
|
| 42 |
+
"methodology": (2, 280),
|
| 43 |
+
"methods": (2, 280),
|
| 44 |
+
"discussion": (2, 220),
|
| 45 |
+
}
|
| 46 |
TRANSLATION_CONTRACT_VERSION = 'hierarchical-multilingual-v5'
|
| 47 |
|
| 48 |
|
|
|
|
| 1499 |
"ratio": round(ratio, 4), "status": status, "recommendation": recommendation, "nonfatal": True,
|
| 1500 |
}
|
| 1501 |
|
| 1502 |
+
@staticmethod
|
| 1503 |
+
def _citation_eligible_section_ids(mode: str) -> set[str]:
|
| 1504 |
+
return (
|
| 1505 |
+
{"introduction", "theoretical_framing", "methods", "discussion"}
|
| 1506 |
+
if str(mode).lower() == "asq"
|
| 1507 |
+
else {"introduction", "theory", "methodology", "discussion"}
|
| 1508 |
+
)
|
| 1509 |
+
|
| 1510 |
+
@classmethod
|
| 1511 |
+
def _adaptive_citation_requirements(cls, tasks, mode: str):
|
| 1512 |
+
"""Derive evidence demand and semantic-survival reserve from manuscript units."""
|
| 1513 |
+
allowed = cls._citation_eligible_section_ids(mode)
|
| 1514 |
+
minimums = {}
|
| 1515 |
+
reserve_targets = {}
|
| 1516 |
+
for task in tasks or []:
|
| 1517 |
+
section_id = str(task.get("section_id") or "")
|
| 1518 |
+
display_name = str(task.get("display_name") or "").strip()
|
| 1519 |
+
if section_id not in allowed or not display_name:
|
| 1520 |
+
continue
|
| 1521 |
+
base, words_per_source = CITATION_ROLE_RULES.get(section_id, (2, 240))
|
| 1522 |
+
target_words = max(1, int(task.get("target_words") or 1))
|
| 1523 |
+
minimum = max(base, int(math.ceil(target_words / max(1, words_per_source))))
|
| 1524 |
+
reserve = max(
|
| 1525 |
+
minimum,
|
| 1526 |
+
int(math.ceil(minimum / max(0.01, CITATION_SEMANTIC_SURVIVAL_RATIO))),
|
| 1527 |
+
)
|
| 1528 |
+
minimums[display_name] = minimum
|
| 1529 |
+
reserve_targets[display_name] = reserve
|
| 1530 |
+
return minimums, reserve_targets
|
| 1531 |
+
|
| 1532 |
+
@staticmethod
|
| 1533 |
+
def _adaptive_literature_candidate_target(target_words: int, agent) -> int:
|
| 1534 |
+
"""Adaptive retrieval aspiration; never a final reference-count quota."""
|
| 1535 |
+
dynamic = max(24, int(math.ceil(max(1, int(target_words or 1)) / 90.0)))
|
| 1536 |
+
configured_cap = max(1, int(getattr(agent, "lit_target_papers", dynamic) or dynamic))
|
| 1537 |
+
return min(configured_cap, dynamic)
|
| 1538 |
+
|
| 1539 |
+
@staticmethod
|
| 1540 |
+
def _citation_task_context(task, research_logic_contract="", synthesis_result="") -> str:
|
| 1541 |
+
parts = [
|
| 1542 |
+
str(task.get("display_name") or ""),
|
| 1543 |
+
str(task.get("required_content") or ""),
|
| 1544 |
+
" ".join(str(value) for value in (task.get("concepts") or []) if str(value).strip()),
|
| 1545 |
+
str(research_logic_contract or "")[:1600],
|
| 1546 |
+
str(synthesis_result or "")[:1600],
|
| 1547 |
+
]
|
| 1548 |
+
return re.sub(r"\s+", " ", " ".join(part for part in parts if part)).strip()
|
| 1549 |
+
|
| 1550 |
+
@staticmethod
|
| 1551 |
+
def _literature_insufficient_error(shortfalls) -> RuntimeError:
|
| 1552 |
+
details = "; ".join(
|
| 1553 |
+
f"{name}={actual}/{required}"
|
| 1554 |
+
for name, actual, required in (shortfalls or [])[:8]
|
| 1555 |
+
)
|
| 1556 |
+
return RuntimeError(
|
| 1557 |
+
"E_LITERATURE_INSUFFICIENT: high-relevance claim-specific literature is insufficient "
|
| 1558 |
+
f"for the planned manuscript units ({details}). Please revise the research topic or "
|
| 1559 |
+
"keywords and try again; unrelated references will not be used to satisfy a quota."
|
| 1560 |
+
)
|
| 1561 |
+
|
| 1562 |
def _resolve_paths(self, items) -> list[str]:
|
| 1563 |
output = []
|
| 1564 |
for item in items or []:
|
|
|
|
| 1681 |
refs_text = str(state.get("refs_text") or "")
|
| 1682 |
if not refs_text:
|
| 1683 |
self._progress(8, "S2 + OpenAlex")
|
| 1684 |
+
literature_candidate_target = self._adaptive_literature_candidate_target(target_words, agent)
|
| 1685 |
refs_text = agent.fetch_references_parallel(
|
| 1686 |
topic,
|
| 1687 |
keywords,
|
| 1688 |
+
min_required=literature_candidate_target,
|
| 1689 |
progress_callback=lambda: self._progress(9, "S2 + OpenAlex"),
|
| 1690 |
)
|
| 1691 |
+
if not agent.citation_map:
|
| 1692 |
+
raise self._literature_insufficient_error([("retrieved literature pool", 0, 1)])
|
| 1693 |
+
state["literature_candidate_target"] = literature_candidate_target
|
|
|
|
| 1694 |
if user_lit_context:
|
| 1695 |
core_refs = agent.parse_user_core_literature(user_lit_context)
|
| 1696 |
if core_refs:
|
|
|
|
| 1765 |
if jit_texts:
|
| 1766 |
rag_engine._initialize_chunks([jit_texts])
|
| 1767 |
self._progress(15, "citation_planning")
|
| 1768 |
+
citation_minimums, citation_reserve_targets = self._adaptive_citation_requirements(
|
| 1769 |
+
adaptive_structure, mode
|
| 1770 |
)
|
| 1771 |
+
citation_plan_target = sum(citation_reserve_targets.values())
|
| 1772 |
+
citation_plan = agent.build_claim_evidence_plan(
|
| 1773 |
+
adaptive_structure, topic, synthesis_result, mode=mode,
|
| 1774 |
+
target_unique=max(1, citation_plan_target),
|
| 1775 |
)
|
| 1776 |
+
|
| 1777 |
+
# Fill unit-specific reserve gaps before expensive drafting. Existing
|
| 1778 |
+
# retrieved evidence is preferred; bounded contextual JIT is last resort.
|
| 1779 |
+
planned_tags = {
|
| 1780 |
+
str(item.get("tag") or "")
|
| 1781 |
for items in citation_plan.values()
|
| 1782 |
for item in items
|
| 1783 |
if item.get("tag")
|
| 1784 |
}
|
| 1785 |
+
task_by_name = {
|
| 1786 |
+
str(task.get("display_name") or ""): task
|
| 1787 |
+
for task in adaptive_structure
|
| 1788 |
+
if str(task.get("display_name") or "")
|
| 1789 |
+
}
|
| 1790 |
+
for display_name, reserve_target in citation_reserve_targets.items():
|
| 1791 |
+
planned_items = citation_plan.setdefault(display_name, [])
|
| 1792 |
+
shortfall = max(0, reserve_target - len(planned_items))
|
| 1793 |
+
if shortfall <= 0:
|
| 1794 |
+
continue
|
| 1795 |
+
section_context = self._citation_task_context(
|
| 1796 |
+
task_by_name.get(display_name, {}), research_logic_contract, synthesis_result
|
| 1797 |
+
)
|
| 1798 |
+
recovery_items = agent.select_contextual_citation_recovery_items(
|
| 1799 |
+
[], section_name=display_name, section_context=section_context,
|
| 1800 |
+
needed=shortfall, excluded_tags=planned_tags,
|
| 1801 |
)
|
| 1802 |
+
for item in recovery_items:
|
| 1803 |
+
tag = str(item.get("tag") or "")
|
| 1804 |
+
if tag and tag not in planned_tags:
|
| 1805 |
+
planned_items.append(item)
|
| 1806 |
+
planned_tags.add(tag)
|
| 1807 |
+
# External JIT is not used merely to hit the reserve target.
|
| 1808 |
+
# It is triggered only when the unit remains below its actual
|
| 1809 |
+
# evidence-demand minimum after exhausting already retrieved
|
| 1810 |
+
# relevant literature.
|
| 1811 |
+
minimum_shortfall = max(
|
| 1812 |
+
0, int(citation_minimums.get(display_name, 0)) - len(planned_items)
|
| 1813 |
+
)
|
| 1814 |
+
if minimum_shortfall > 0:
|
| 1815 |
+
jit_items = agent.fetch_contextual_jit_recovery_items(
|
| 1816 |
+
topic=topic, section_name=display_name, section_context=section_context,
|
| 1817 |
+
needed=minimum_shortfall, excluded_tags=planned_tags,
|
| 1818 |
+
)
|
| 1819 |
+
for item in jit_items:
|
| 1820 |
+
tag = str(item.get("tag") or "")
|
| 1821 |
+
if tag and tag not in planned_tags:
|
| 1822 |
+
planned_items.append(item)
|
| 1823 |
+
planned_tags.add(tag)
|
| 1824 |
+
|
| 1825 |
+
shortfalls = [
|
| 1826 |
+
(name, len(citation_plan.get(name) or []), required)
|
| 1827 |
+
for name, required in citation_minimums.items()
|
| 1828 |
+
if len(citation_plan.get(name) or []) < required
|
| 1829 |
+
]
|
| 1830 |
+
if shortfalls:
|
| 1831 |
+
raise self._literature_insufficient_error(shortfalls)
|
| 1832 |
+
logging.info(
|
| 1833 |
+
"Adaptive citation feasibility passed: minimum_total=%s reserve_target=%s planned_unique=%s units=%s",
|
| 1834 |
+
sum(citation_minimums.values()), citation_plan_target, len(planned_tags), len(citation_minimums),
|
| 1835 |
+
)
|
| 1836 |
+
canonical_concept_ledger = self._build_canonical_concept_ledger(
|
| 1837 |
+
mode, data_metadata, keywords=keywords, adaptive_structure=adaptive_structure,
|
| 1838 |
+
research_logic_contract=research_logic_contract,
|
| 1839 |
+
)
|
| 1840 |
state.update(
|
| 1841 |
{
|
| 1842 |
"adaptive_structure": adaptive_structure,
|
| 1843 |
"quote_invocation_plan": quote_invocation_plan,
|
| 1844 |
"citation_plan": citation_plan,
|
| 1845 |
+
"citation_minimums": citation_minimums,
|
| 1846 |
+
"citation_reserve_targets": citation_reserve_targets,
|
| 1847 |
+
"citation_validated_tags_by_unit": {},
|
| 1848 |
"research_logic_contract": research_logic_contract,
|
| 1849 |
"working_abstract": working_abstract,
|
| 1850 |
"working_keywords": working_keywords,
|
|
|
|
| 1876 |
state.get("research_logic_contract") or agent.global_memory.get("research_logic_contract") or ""
|
| 1877 |
)
|
| 1878 |
citation_plan = state.get("citation_plan") if isinstance(state.get("citation_plan"), dict) else {}
|
| 1879 |
+
citation_minimums = dict(state.get("citation_minimums") or {})
|
| 1880 |
+
citation_reserve_targets = dict(state.get("citation_reserve_targets") or {})
|
| 1881 |
+
citation_validated_tags_by_unit = {
|
| 1882 |
+
str(name): list(tags or [])
|
| 1883 |
+
for name, tags in dict(state.get("citation_validated_tags_by_unit") or {}).items()
|
| 1884 |
+
}
|
| 1885 |
+
if not citation_minimums:
|
| 1886 |
+
citation_minimums, citation_reserve_targets = self._adaptive_citation_requirements(
|
| 1887 |
+
adaptive_structure, mode
|
| 1888 |
+
)
|
| 1889 |
translation_sections = dict(state.get('translation_sections') or {})
|
| 1890 |
translation_glossary = dict(state.get('translation_glossary') or {})
|
| 1891 |
canonical_concept_ledger = state.get('canonical_concept_ledger')
|
|
|
|
| 1999 |
if not citation_plan:
|
| 2000 |
self._progress(15, "citation_planning")
|
| 2001 |
citation_plan = agent.build_claim_evidence_plan(
|
| 2002 |
+
adaptive_structure, topic, synthesis_result, mode=mode,
|
| 2003 |
+
target_unique=max(1, sum(citation_reserve_targets.values())),
|
| 2004 |
)
|
| 2005 |
+
shortfalls = [
|
| 2006 |
+
(name, len(citation_plan.get(name) or []), required)
|
| 2007 |
+
for name, required in citation_minimums.items()
|
| 2008 |
+
if len(citation_plan.get(name) or []) < required
|
| 2009 |
+
]
|
| 2010 |
+
if shortfalls:
|
| 2011 |
+
raise self._literature_insufficient_error(shortfalls)
|
| 2012 |
state["citation_plan"] = citation_plan
|
| 2013 |
+
state["citation_minimums"] = citation_minimums
|
| 2014 |
+
state["citation_reserve_targets"] = citation_reserve_targets
|
| 2015 |
+
state["citation_validated_tags_by_unit"] = citation_validated_tags_by_unit
|
| 2016 |
|
| 2017 |
quote_invocation_plan = (
|
| 2018 |
dict(state.get("quote_invocation_plan") or {}) if mode == "asq" else {}
|
|
|
|
| 2249 |
used_after = set(agent.used_citations)
|
| 2250 |
survived = used_after - used_before
|
| 2251 |
if assigned_tag_list and not is_quarantined:
|
| 2252 |
+
adaptive_minimum = max(1, int(citation_minimums.get(display_name, 1)))
|
| 2253 |
required_survival = min(
|
| 2254 |
len(assigned_tag_list),
|
| 2255 |
+
max(
|
| 2256 |
+
adaptive_minimum,
|
| 2257 |
+
int(math.ceil(len(assigned_tag_list) * CITATION_SEMANTIC_SURVIVAL_RATIO)),
|
| 2258 |
+
),
|
| 2259 |
)
|
| 2260 |
if len(survived) < required_survival:
|
| 2261 |
missing_items = [
|
|
|
|
| 2275 |
excluded_tags=used_after | {str(item.get("tag") or "") for item in recovery_items},
|
| 2276 |
)
|
| 2277 |
)
|
| 2278 |
+
planned_tags_for_unit = {str(item.get("tag") or "") for item in planned_items}
|
| 2279 |
+
for recovery_item in recovery_items[:needed]:
|
| 2280 |
+
tag = str(recovery_item.get("tag") or "")
|
| 2281 |
+
if tag and tag not in planned_tags_for_unit:
|
| 2282 |
+
planned_items.append(recovery_item)
|
| 2283 |
+
planned_tags_for_unit.add(tag)
|
| 2284 |
+
citation_plan[display_name] = planned_items
|
| 2285 |
supplemental_raw = agent.build_citation_recovery_paragraph(
|
| 2286 |
recovery_items[:needed], section_name=display_name, section_context=en_resolved
|
| 2287 |
)
|
|
|
|
| 2295 |
en_resolved = en_resolved.rstrip() + "\n\n" + supplemental_resolved.strip()
|
| 2296 |
used_after = set(agent.used_citations)
|
| 2297 |
survived = used_after - used_before
|
| 2298 |
+
if assigned_tag_list and not is_quarantined:
|
| 2299 |
+
adaptive_minimum = max(1, int(citation_minimums.get(display_name, 1)))
|
| 2300 |
+
if len(survived) < adaptive_minimum:
|
| 2301 |
+
raise self._literature_insufficient_error(
|
| 2302 |
+
[(display_name, len(survived), adaptive_minimum)]
|
| 2303 |
+
)
|
| 2304 |
+
citation_validated_tags_by_unit[display_name] = sorted(
|
| 2305 |
+
survived, key=agent._citation_tag_number
|
| 2306 |
+
)
|
| 2307 |
if mode == "asq" and section_id == "findings":
|
| 2308 |
en_resolved = self._render_planned_qualitative_quotes(
|
| 2309 |
en_resolved,
|
|
|
|
| 2352 |
"context_tail": context_tail,
|
| 2353 |
"accumulated_deviation": accumulated_deviation,
|
| 2354 |
"quality_diagnostics": quality_diagnostics,
|
| 2355 |
+
"citation_plan": citation_plan,
|
| 2356 |
+
"citation_minimums": citation_minimums,
|
| 2357 |
+
"citation_reserve_targets": citation_reserve_targets,
|
| 2358 |
+
"citation_validated_tags_by_unit": citation_validated_tags_by_unit,
|
| 2359 |
"agent_state": agent.export_state(),
|
| 2360 |
"data_metadata": data_metadata,
|
| 2361 |
}
|
|
|
|
| 2383 |
if ref and ref not in seen_refs:
|
| 2384 |
references.append(f"- {ref}")
|
| 2385 |
seen_refs.add(ref)
|
| 2386 |
+
if not references:
|
| 2387 |
+
raise self._literature_insufficient_error([("final validated references", 0, 1)])
|
| 2388 |
+
final_shortfalls = []
|
| 2389 |
+
for name, minimum in citation_minimums.items():
|
| 2390 |
+
validated_tags = set(citation_validated_tags_by_unit.get(name) or [])
|
| 2391 |
+
if not validated_tags:
|
| 2392 |
+
validated_tags = {
|
| 2393 |
+
str(item.get("tag") or "")
|
| 2394 |
+
for item in (citation_plan.get(name) or [])
|
| 2395 |
+
if str(item.get("tag") or "") in agent.used_citations
|
| 2396 |
+
}
|
| 2397 |
+
if len(validated_tags) < int(minimum):
|
| 2398 |
+
final_shortfalls.append((name, len(validated_tags), int(minimum)))
|
| 2399 |
+
if final_shortfalls:
|
| 2400 |
+
raise self._literature_insufficient_error(final_shortfalls)
|
| 2401 |
+
final_reference_count = len(references)
|
| 2402 |
+
logging.info(
|
| 2403 |
+
"Adaptive citation final Gate passed: validated_references=%s units=%s",
|
| 2404 |
+
final_reference_count, len(citation_minimums),
|
| 2405 |
+
)
|
| 2406 |
full_draft_en = full_draft_en.rstrip() + "\n\n# References\n" + "\n\n".join(references)
|
| 2407 |
actual_words = self._english_count_for_target(full_draft_en)
|
| 2408 |
self._upsert_quality_diagnostic(
|
| 2409 |
quality_diagnostics, self._validate_target_length(full_draft_en, target_words)
|
| 2410 |
)
|
| 2411 |
body_only, _reference_tail = agent.validate_reference_tail(
|
| 2412 |
+
full_draft_en, minimum_references=max(1, final_reference_count)
|
| 2413 |
)
|
| 2414 |
agent.validate_manuscript_structure(body_only, adaptive_structure, translated=False)
|
| 2415 |
agent.enforce_resolved_claim_specific_citations(body_only)
|
|
|
|
| 2428 |
full_draft_en = replace_abstract(full_draft_en, final_abstract)
|
| 2429 |
full_draft_en = self._replace_keywords(full_draft_en, final_keywords)
|
| 2430 |
body_only, _reference_tail = agent.validate_reference_tail(
|
| 2431 |
+
full_draft_en, minimum_references=max(1, final_reference_count)
|
| 2432 |
)
|
| 2433 |
agent.validate_manuscript_structure(body_only, adaptive_structure, translated=False)
|
| 2434 |
agent.enforce_resolved_claim_specific_citations(body_only)
|
|
|
|
| 2465 |
"final_abstract": final_abstract,
|
| 2466 |
"final_keywords": final_keywords,
|
| 2467 |
"front_matter_phase": "final",
|
| 2468 |
+
"final_reference_count": final_reference_count,
|
| 2469 |
+
"citation_plan": citation_plan,
|
| 2470 |
+
"citation_minimums": citation_minimums,
|
| 2471 |
+
"citation_reserve_targets": citation_reserve_targets,
|
| 2472 |
+
"citation_validated_tags_by_unit": citation_validated_tags_by_unit,
|
| 2473 |
"quality_diagnostics": quality_diagnostics,
|
| 2474 |
"english_finalized": True,
|
| 2475 |
"translation_complete": False,
|
|
|
|
| 2493 |
)
|
| 2494 |
self._shutdown_translation_worker(wait=True)
|
| 2495 |
body_only, reference_tail = agent.validate_reference_tail(
|
| 2496 |
+
full_draft_en, minimum_references=max(1, int(state.get("final_reference_count") or 1))
|
| 2497 |
)
|
| 2498 |
section_order = self._unique_top_level_sections(adaptive_structure)
|
| 2499 |
if not section_order:
|
core/pipeline_agent.py
CHANGED
|
@@ -2698,9 +2698,9 @@ INPUT:
|
|
| 2698 |
|
| 2699 |
At most one query is issued to each literature backend. New references
|
| 2700 |
are admitted only when title/abstract evidence has distinctive lexical
|
| 2701 |
-
overlap with the current manuscript unit; otherwise the
|
| 2702 |
-
is allowed to fail rather than padding the
|
| 2703 |
-
literature.
|
| 2704 |
"""
|
| 2705 |
needed = max(0, int(needed or 0))
|
| 2706 |
if needed <= 0:
|
|
|
|
| 2698 |
|
| 2699 |
At most one query is issued to each literature backend. New references
|
| 2700 |
are admitted only when title/abstract evidence has distinctive lexical
|
| 2701 |
+
overlap with the current manuscript unit; otherwise the adaptive
|
| 2702 |
+
evidence-sufficiency Gate is allowed to fail rather than padding the
|
| 2703 |
+
manuscript with irrelevant literature.
|
| 2704 |
"""
|
| 2705 |
needed = max(0, int(needed or 0))
|
| 2706 |
if needed <= 0:
|