Spaces:
Running
Running
R14-TQ2-PQ1-R2 Top-Level Claim Evidence Sufficiency + Study-Owned Boundary + Four-Language Failure Localization
#63
by GuXiaopeng - opened
- app.py +18 -1
- core/generation_runner.py +159 -50
- core/pipeline_agent.py +67 -0
- i18n.py +8 -0
app.py
CHANGED
|
@@ -1004,13 +1004,30 @@ def _render_task_status_once(lang_code: str) -> None:
|
|
| 1004 |
else:
|
| 1005 |
st.warning("The task completed, but the local output file is unavailable after runtime replacement.")
|
| 1006 |
elif status == "failed":
|
| 1007 |
-
|
|
|
|
|
|
|
|
|
|
| 1008 |
elif status == "cancelled":
|
| 1009 |
detail = str(task.get("safe_error_detail") or "")
|
| 1010 |
if detail:
|
| 1011 |
st.warning(detail)
|
| 1012 |
|
| 1013 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1014 |
def _inject_generation_action_scroll(mode: str) -> None:
|
| 1015 |
"""Scroll immediately to the active generation action without a server wait."""
|
| 1016 |
target_id = "sp-generation-action-asq" if str(mode or "").lower() == "asq" else "sp-generation-action-amj"
|
|
|
|
| 1004 |
else:
|
| 1005 |
st.warning("The task completed, but the local output file is unavailable after runtime replacement.")
|
| 1006 |
elif status == "failed":
|
| 1007 |
+
error_message, translation_note = _localized_terminal_task_error(task, lang_code)
|
| 1008 |
+
st.error(error_message)
|
| 1009 |
+
if translation_note:
|
| 1010 |
+
st.warning(translation_note)
|
| 1011 |
elif status == "cancelled":
|
| 1012 |
detail = str(task.get("safe_error_detail") or "")
|
| 1013 |
if detail:
|
| 1014 |
st.warning(detail)
|
| 1015 |
|
| 1016 |
|
| 1017 |
+
def _localized_terminal_task_error(task: dict, lang_code: str) -> tuple[str, str]:
|
| 1018 |
+
"""Map stable business error codes to UI-language messages.
|
| 1019 |
+
|
| 1020 |
+
Engineering details remain in task/log state; the user-facing terminal view
|
| 1021 |
+
does not expose a long English runtime exception in non-English interfaces.
|
| 1022 |
+
"""
|
| 1023 |
+
code = str(task.get("safe_error_code") or "").strip()
|
| 1024 |
+
detail = str(task.get("safe_error_detail") or "").strip()
|
| 1025 |
+
raw = f"{code} {detail}"
|
| 1026 |
+
if "E_LITERATURE_INSUFFICIENT" in raw:
|
| 1027 |
+
return tr(lang_code, "literature_insufficient"), tr(lang_code, "upstream_translation_stopped")
|
| 1028 |
+
return detail or code or tr(lang_code, "node_error"), ""
|
| 1029 |
+
|
| 1030 |
+
|
| 1031 |
def _inject_generation_action_scroll(mode: str) -> None:
|
| 1032 |
"""Scroll immediately to the active generation action without a server wait."""
|
| 1033 |
target_id = "sp-generation-action-asq" if str(mode or "").lower() == "asq" else "sp-generation-action-amj"
|
core/generation_runner.py
CHANGED
|
@@ -1579,6 +1579,58 @@ class GenerationRunner:
|
|
| 1579 |
reserve_targets[display_name] = reserve
|
| 1580 |
return minimums, reserve_targets
|
| 1581 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1582 |
@staticmethod
|
| 1583 |
def _adaptive_literature_candidate_target(target_words: int, agent) -> int:
|
| 1584 |
"""Adaptive retrieval aspiration; never a final reference-count quota."""
|
|
@@ -1818,7 +1870,11 @@ class GenerationRunner:
|
|
| 1818 |
citation_minimums, citation_reserve_targets = self._adaptive_citation_requirements(
|
| 1819 |
adaptive_structure, mode
|
| 1820 |
)
|
| 1821 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1822 |
citation_plan = agent.build_claim_evidence_plan(
|
| 1823 |
adaptive_structure, topic, synthesis_result, mode=mode,
|
| 1824 |
target_unique=max(1, citation_plan_target),
|
|
@@ -1854,34 +1910,47 @@ class GenerationRunner:
|
|
| 1854 |
if tag and tag not in planned_tags:
|
| 1855 |
planned_items.append(item)
|
| 1856 |
planned_tags.add(tag)
|
| 1857 |
-
#
|
| 1858 |
-
#
|
| 1859 |
-
#
|
| 1860 |
-
|
| 1861 |
-
|
| 1862 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1863 |
)
|
| 1864 |
-
|
| 1865 |
-
|
| 1866 |
-
|
| 1867 |
-
|
| 1868 |
-
|
| 1869 |
-
|
| 1870 |
-
|
| 1871 |
-
|
| 1872 |
-
|
| 1873 |
-
|
|
|
|
| 1874 |
|
| 1875 |
shortfalls = [
|
| 1876 |
-
(
|
| 1877 |
-
for
|
| 1878 |
-
if
|
| 1879 |
]
|
| 1880 |
if shortfalls:
|
| 1881 |
raise self._literature_insufficient_error(shortfalls)
|
| 1882 |
logging.info(
|
| 1883 |
-
"
|
| 1884 |
-
sum(
|
| 1885 |
)
|
| 1886 |
canonical_concept_ledger = self._build_canonical_concept_ledger(
|
| 1887 |
mode, data_metadata, keywords=keywords, adaptive_structure=adaptive_structure,
|
|
@@ -1894,7 +1963,11 @@ class GenerationRunner:
|
|
| 1894 |
"citation_plan": citation_plan,
|
| 1895 |
"citation_minimums": citation_minimums,
|
| 1896 |
"citation_reserve_targets": citation_reserve_targets,
|
|
|
|
|
|
|
|
|
|
| 1897 |
"citation_validated_tags_by_unit": {},
|
|
|
|
| 1898 |
"research_logic_contract": research_logic_contract,
|
| 1899 |
"working_abstract": working_abstract,
|
| 1900 |
"working_keywords": working_keywords,
|
|
@@ -1928,6 +2001,13 @@ class GenerationRunner:
|
|
| 1928 |
citation_plan = state.get("citation_plan") if isinstance(state.get("citation_plan"), dict) else {}
|
| 1929 |
citation_minimums = dict(state.get("citation_minimums") or {})
|
| 1930 |
citation_reserve_targets = dict(state.get("citation_reserve_targets") or {})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1931 |
citation_validated_tags_by_unit = {
|
| 1932 |
str(name): list(tags or [])
|
| 1933 |
for name, tags in dict(state.get("citation_validated_tags_by_unit") or {}).items()
|
|
@@ -1936,6 +2016,12 @@ class GenerationRunner:
|
|
| 1936 |
citation_minimums, citation_reserve_targets = self._adaptive_citation_requirements(
|
| 1937 |
adaptive_structure, mode
|
| 1938 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1939 |
translation_sections = dict(state.get('translation_sections') or {})
|
| 1940 |
translation_glossary = dict(state.get('translation_glossary') or {})
|
| 1941 |
canonical_concept_ledger = state.get('canonical_concept_ledger')
|
|
@@ -2050,18 +2136,23 @@ class GenerationRunner:
|
|
| 2050 |
self._progress(15, "citation_planning")
|
| 2051 |
citation_plan = agent.build_claim_evidence_plan(
|
| 2052 |
adaptive_structure, topic, synthesis_result, mode=mode,
|
| 2053 |
-
target_unique=max(1, sum(
|
| 2054 |
)
|
|
|
|
| 2055 |
shortfalls = [
|
| 2056 |
-
(
|
| 2057 |
-
for
|
| 2058 |
-
if
|
| 2059 |
]
|
| 2060 |
if shortfalls:
|
| 2061 |
raise self._literature_insufficient_error(shortfalls)
|
| 2062 |
state["citation_plan"] = citation_plan
|
| 2063 |
state["citation_minimums"] = citation_minimums
|
| 2064 |
state["citation_reserve_targets"] = citation_reserve_targets
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2065 |
state["citation_validated_tags_by_unit"] = citation_validated_tags_by_unit
|
| 2066 |
|
| 2067 |
quote_invocation_plan = (
|
|
@@ -2300,13 +2391,9 @@ Output only final academic prose. Do not report the word count."""
|
|
| 2300 |
used_after = set(agent.used_citations)
|
| 2301 |
survived = used_after - used_before
|
| 2302 |
if assigned_tag_list and not is_quarantined:
|
| 2303 |
-
adaptive_minimum = max(1, int(citation_minimums.get(display_name, 1)))
|
| 2304 |
required_survival = min(
|
| 2305 |
len(assigned_tag_list),
|
| 2306 |
-
max(
|
| 2307 |
-
adaptive_minimum,
|
| 2308 |
-
int(math.ceil(len(assigned_tag_list) * CITATION_SEMANTIC_SURVIVAL_RATIO)),
|
| 2309 |
-
),
|
| 2310 |
)
|
| 2311 |
if len(survived) < required_survival:
|
| 2312 |
missing_items = [
|
|
@@ -2348,14 +2435,18 @@ Output only final academic prose. Do not report the word count."""
|
|
| 2348 |
en_resolved = recovered
|
| 2349 |
used_after = set(agent.used_citations)
|
| 2350 |
survived = used_after - used_before
|
| 2351 |
-
if
|
| 2352 |
-
|
| 2353 |
-
|
| 2354 |
-
|
| 2355 |
-
|
|
|
|
|
|
|
| 2356 |
)
|
| 2357 |
-
|
| 2358 |
-
|
|
|
|
|
|
|
| 2359 |
)
|
| 2360 |
if mode == "asq" and section_id == "findings":
|
| 2361 |
en_resolved = self._render_planned_qualitative_quotes(
|
|
@@ -2408,6 +2499,10 @@ Output only final academic prose. Do not report the word count."""
|
|
| 2408 |
"citation_plan": citation_plan,
|
| 2409 |
"citation_minimums": citation_minimums,
|
| 2410 |
"citation_reserve_targets": citation_reserve_targets,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2411 |
"citation_validated_tags_by_unit": citation_validated_tags_by_unit,
|
| 2412 |
"agent_state": agent.export_state(),
|
| 2413 |
"data_metadata": data_metadata,
|
|
@@ -2438,23 +2533,33 @@ Output only final academic prose. Do not report the word count."""
|
|
| 2438 |
seen_refs.add(ref)
|
| 2439 |
if not references:
|
| 2440 |
raise self._literature_insufficient_error([("final validated references", 0, 1)])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2441 |
final_shortfalls = []
|
| 2442 |
-
for
|
| 2443 |
-
validated_tags = set(
|
| 2444 |
-
|
| 2445 |
-
|
| 2446 |
-
|
| 2447 |
-
|
| 2448 |
-
|
| 2449 |
-
|
| 2450 |
-
|
| 2451 |
-
final_shortfalls.append((name, len(validated_tags), int(minimum)))
|
| 2452 |
if final_shortfalls:
|
| 2453 |
raise self._literature_insufficient_error(final_shortfalls)
|
| 2454 |
final_reference_count = len(references)
|
| 2455 |
logging.info(
|
| 2456 |
-
"
|
| 2457 |
-
final_reference_count, len(
|
| 2458 |
)
|
| 2459 |
full_draft_en = full_draft_en.rstrip() + "\n\n# References\n" + "\n\n".join(references)
|
| 2460 |
actual_words = self._english_count_for_target(full_draft_en)
|
|
@@ -2522,6 +2627,10 @@ Output only final academic prose. Do not report the word count."""
|
|
| 2522 |
"citation_plan": citation_plan,
|
| 2523 |
"citation_minimums": citation_minimums,
|
| 2524 |
"citation_reserve_targets": citation_reserve_targets,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2525 |
"citation_validated_tags_by_unit": citation_validated_tags_by_unit,
|
| 2526 |
"quality_diagnostics": quality_diagnostics,
|
| 2527 |
"english_finalized": True,
|
|
|
|
| 1579 |
reserve_targets[display_name] = reserve
|
| 1580 |
return minimums, reserve_targets
|
| 1581 |
|
| 1582 |
+
@classmethod
|
| 1583 |
+
def _top_level_citation_requirements(cls, tasks, mode: str):
|
| 1584 |
+
"""Aggregate citation authority by academic section, never by technical chunk.
|
| 1585 |
+
|
| 1586 |
+
``Introduction (Part 1/2)`` and ``Introduction (Part 2/2)`` are generation
|
| 1587 |
+
chunks, not separate scholarly evidence authorities. Planning may remain
|
| 1588 |
+
unit-local, but fatal literature sufficiency is evaluated on the canonical
|
| 1589 |
+
top-level academic section.
|
| 1590 |
+
"""
|
| 1591 |
+
allowed = cls._citation_eligible_section_ids(mode)
|
| 1592 |
+
words_by_section = {}
|
| 1593 |
+
for task in tasks or []:
|
| 1594 |
+
section_id = str(task.get('section_id') or '')
|
| 1595 |
+
if section_id not in allowed:
|
| 1596 |
+
continue
|
| 1597 |
+
words_by_section[section_id] = words_by_section.get(section_id, 0) + max(
|
| 1598 |
+
1, int(task.get('target_words') or 1)
|
| 1599 |
+
)
|
| 1600 |
+
minimums = {}
|
| 1601 |
+
reserve_targets = {}
|
| 1602 |
+
for section_id, target_words in words_by_section.items():
|
| 1603 |
+
base, words_per_source = CITATION_ROLE_RULES.get(section_id, (2, 240))
|
| 1604 |
+
minimum = max(base, int(math.ceil(target_words / max(1, words_per_source))))
|
| 1605 |
+
reserve = max(
|
| 1606 |
+
minimum,
|
| 1607 |
+
int(math.ceil(minimum / max(0.01, CITATION_SEMANTIC_SURVIVAL_RATIO))),
|
| 1608 |
+
)
|
| 1609 |
+
minimums[section_id] = minimum
|
| 1610 |
+
reserve_targets[section_id] = reserve
|
| 1611 |
+
return minimums, reserve_targets
|
| 1612 |
+
|
| 1613 |
+
@staticmethod
|
| 1614 |
+
def _citation_unit_section_map(tasks):
|
| 1615 |
+
return {
|
| 1616 |
+
str(task.get('display_name') or ''): str(task.get('section_id') or '')
|
| 1617 |
+
for task in (tasks or []) if str(task.get('display_name') or '').strip()
|
| 1618 |
+
}
|
| 1619 |
+
|
| 1620 |
+
@staticmethod
|
| 1621 |
+
def _top_level_unique_tag_counts(citation_plan, unit_section_map):
|
| 1622 |
+
tags_by_section = {}
|
| 1623 |
+
for unit_name, items in dict(citation_plan or {}).items():
|
| 1624 |
+
section_id = str(unit_section_map.get(str(unit_name)) or '')
|
| 1625 |
+
if not section_id:
|
| 1626 |
+
continue
|
| 1627 |
+
bucket = tags_by_section.setdefault(section_id, set())
|
| 1628 |
+
for item in items or []:
|
| 1629 |
+
tag = str((item or {}).get('tag') or '')
|
| 1630 |
+
if tag:
|
| 1631 |
+
bucket.add(tag)
|
| 1632 |
+
return {section_id: len(tags) for section_id, tags in tags_by_section.items()}
|
| 1633 |
+
|
| 1634 |
@staticmethod
|
| 1635 |
def _adaptive_literature_candidate_target(target_words: int, agent) -> int:
|
| 1636 |
"""Adaptive retrieval aspiration; never a final reference-count quota."""
|
|
|
|
| 1870 |
citation_minimums, citation_reserve_targets = self._adaptive_citation_requirements(
|
| 1871 |
adaptive_structure, mode
|
| 1872 |
)
|
| 1873 |
+
citation_top_level_minimums, citation_top_level_reserve_targets = self._top_level_citation_requirements(
|
| 1874 |
+
adaptive_structure, mode
|
| 1875 |
+
)
|
| 1876 |
+
citation_unit_section_map = self._citation_unit_section_map(adaptive_structure)
|
| 1877 |
+
citation_plan_target = sum(citation_top_level_reserve_targets.values())
|
| 1878 |
citation_plan = agent.build_claim_evidence_plan(
|
| 1879 |
adaptive_structure, topic, synthesis_result, mode=mode,
|
| 1880 |
target_unique=max(1, citation_plan_target),
|
|
|
|
| 1910 |
if tag and tag not in planned_tags:
|
| 1911 |
planned_items.append(item)
|
| 1912 |
planned_tags.add(tag)
|
| 1913 |
+
# Do not trigger external retrieval merely because a technical
|
| 1914 |
+
# generation chunk is below a word-derived target. Fatal evidence
|
| 1915 |
+
# authority belongs to the canonical top-level academic section.
|
| 1916 |
+
|
| 1917 |
+
top_level_counts = self._top_level_unique_tag_counts(citation_plan, citation_unit_section_map)
|
| 1918 |
+
for section_id, required in citation_top_level_minimums.items():
|
| 1919 |
+
deficit = max(0, int(required) - int(top_level_counts.get(section_id, 0)))
|
| 1920 |
+
if deficit <= 0:
|
| 1921 |
+
continue
|
| 1922 |
+
section_units = [
|
| 1923 |
+
name for name, mapped in citation_unit_section_map.items() if mapped == section_id
|
| 1924 |
+
]
|
| 1925 |
+
if not section_units:
|
| 1926 |
+
continue
|
| 1927 |
+
primary_unit = section_units[0]
|
| 1928 |
+
section_context = " ".join(
|
| 1929 |
+
self._citation_task_context(task_by_name.get(name, {}), research_logic_contract, synthesis_result)
|
| 1930 |
+
for name in section_units
|
| 1931 |
)
|
| 1932 |
+
jit_items = agent.fetch_contextual_jit_recovery_items(
|
| 1933 |
+
topic=topic, section_name=section_id, section_context=section_context,
|
| 1934 |
+
needed=deficit, excluded_tags=planned_tags,
|
| 1935 |
+
)
|
| 1936 |
+
planned_items = citation_plan.setdefault(primary_unit, [])
|
| 1937 |
+
for item in jit_items:
|
| 1938 |
+
tag = str(item.get("tag") or "")
|
| 1939 |
+
if tag and tag not in planned_tags:
|
| 1940 |
+
planned_items.append(item)
|
| 1941 |
+
planned_tags.add(tag)
|
| 1942 |
+
top_level_counts = self._top_level_unique_tag_counts(citation_plan, citation_unit_section_map)
|
| 1943 |
|
| 1944 |
shortfalls = [
|
| 1945 |
+
(section_id, int(top_level_counts.get(section_id, 0)), required)
|
| 1946 |
+
for section_id, required in citation_top_level_minimums.items()
|
| 1947 |
+
if int(top_level_counts.get(section_id, 0)) < int(required)
|
| 1948 |
]
|
| 1949 |
if shortfalls:
|
| 1950 |
raise self._literature_insufficient_error(shortfalls)
|
| 1951 |
logging.info(
|
| 1952 |
+
"Top-level citation feasibility passed: minimum_total=%s reserve_target=%s planned_unique=%s sections=%s",
|
| 1953 |
+
sum(citation_top_level_minimums.values()), citation_plan_target, len(planned_tags), len(citation_top_level_minimums),
|
| 1954 |
)
|
| 1955 |
canonical_concept_ledger = self._build_canonical_concept_ledger(
|
| 1956 |
mode, data_metadata, keywords=keywords, adaptive_structure=adaptive_structure,
|
|
|
|
| 1963 |
"citation_plan": citation_plan,
|
| 1964 |
"citation_minimums": citation_minimums,
|
| 1965 |
"citation_reserve_targets": citation_reserve_targets,
|
| 1966 |
+
"citation_top_level_minimums": citation_top_level_minimums,
|
| 1967 |
+
"citation_top_level_reserve_targets": citation_top_level_reserve_targets,
|
| 1968 |
+
"citation_unit_section_map": citation_unit_section_map,
|
| 1969 |
"citation_validated_tags_by_unit": {},
|
| 1970 |
+
"citation_required_claim_counts_by_unit": {},
|
| 1971 |
"research_logic_contract": research_logic_contract,
|
| 1972 |
"working_abstract": working_abstract,
|
| 1973 |
"working_keywords": working_keywords,
|
|
|
|
| 2001 |
citation_plan = state.get("citation_plan") if isinstance(state.get("citation_plan"), dict) else {}
|
| 2002 |
citation_minimums = dict(state.get("citation_minimums") or {})
|
| 2003 |
citation_reserve_targets = dict(state.get("citation_reserve_targets") or {})
|
| 2004 |
+
citation_top_level_minimums = dict(state.get("citation_top_level_minimums") or {})
|
| 2005 |
+
citation_top_level_reserve_targets = dict(state.get("citation_top_level_reserve_targets") or {})
|
| 2006 |
+
citation_unit_section_map = dict(state.get("citation_unit_section_map") or {})
|
| 2007 |
+
citation_required_claim_counts_by_unit = {
|
| 2008 |
+
str(name): int(count or 0)
|
| 2009 |
+
for name, count in dict(state.get("citation_required_claim_counts_by_unit") or {}).items()
|
| 2010 |
+
}
|
| 2011 |
citation_validated_tags_by_unit = {
|
| 2012 |
str(name): list(tags or [])
|
| 2013 |
for name, tags in dict(state.get("citation_validated_tags_by_unit") or {}).items()
|
|
|
|
| 2016 |
citation_minimums, citation_reserve_targets = self._adaptive_citation_requirements(
|
| 2017 |
adaptive_structure, mode
|
| 2018 |
)
|
| 2019 |
+
if not citation_top_level_minimums:
|
| 2020 |
+
citation_top_level_minimums, citation_top_level_reserve_targets = self._top_level_citation_requirements(
|
| 2021 |
+
adaptive_structure, mode
|
| 2022 |
+
)
|
| 2023 |
+
if not citation_unit_section_map:
|
| 2024 |
+
citation_unit_section_map = self._citation_unit_section_map(adaptive_structure)
|
| 2025 |
translation_sections = dict(state.get('translation_sections') or {})
|
| 2026 |
translation_glossary = dict(state.get('translation_glossary') or {})
|
| 2027 |
canonical_concept_ledger = state.get('canonical_concept_ledger')
|
|
|
|
| 2136 |
self._progress(15, "citation_planning")
|
| 2137 |
citation_plan = agent.build_claim_evidence_plan(
|
| 2138 |
adaptive_structure, topic, synthesis_result, mode=mode,
|
| 2139 |
+
target_unique=max(1, sum(citation_top_level_reserve_targets.values())),
|
| 2140 |
)
|
| 2141 |
+
top_level_counts = self._top_level_unique_tag_counts(citation_plan, citation_unit_section_map)
|
| 2142 |
shortfalls = [
|
| 2143 |
+
(section_id, int(top_level_counts.get(section_id, 0)), required)
|
| 2144 |
+
for section_id, required in citation_top_level_minimums.items()
|
| 2145 |
+
if int(top_level_counts.get(section_id, 0)) < int(required)
|
| 2146 |
]
|
| 2147 |
if shortfalls:
|
| 2148 |
raise self._literature_insufficient_error(shortfalls)
|
| 2149 |
state["citation_plan"] = citation_plan
|
| 2150 |
state["citation_minimums"] = citation_minimums
|
| 2151 |
state["citation_reserve_targets"] = citation_reserve_targets
|
| 2152 |
+
state["citation_top_level_minimums"] = citation_top_level_minimums
|
| 2153 |
+
state["citation_top_level_reserve_targets"] = citation_top_level_reserve_targets
|
| 2154 |
+
state["citation_unit_section_map"] = citation_unit_section_map
|
| 2155 |
+
state["citation_required_claim_counts_by_unit"] = citation_required_claim_counts_by_unit
|
| 2156 |
state["citation_validated_tags_by_unit"] = citation_validated_tags_by_unit
|
| 2157 |
|
| 2158 |
quote_invocation_plan = (
|
|
|
|
| 2391 |
used_after = set(agent.used_citations)
|
| 2392 |
survived = used_after - used_before
|
| 2393 |
if assigned_tag_list and not is_quarantined:
|
|
|
|
| 2394 |
required_survival = min(
|
| 2395 |
len(assigned_tag_list),
|
| 2396 |
+
max(1, int(math.ceil(len(assigned_tag_list) * CITATION_SEMANTIC_SURVIVAL_RATIO))),
|
|
|
|
|
|
|
|
|
|
| 2397 |
)
|
| 2398 |
if len(survived) < required_survival:
|
| 2399 |
missing_items = [
|
|
|
|
| 2435 |
en_resolved = recovered
|
| 2436 |
used_after = set(agent.used_citations)
|
| 2437 |
survived = used_after - used_before
|
| 2438 |
+
if not is_quarantined and section_id in self._citation_eligible_section_ids(mode):
|
| 2439 |
+
citation_required_claim_counts_by_unit[display_name] = agent.count_external_literature_claims(
|
| 2440 |
+
en_resolved, section_name=display_name
|
| 2441 |
+
)
|
| 2442 |
+
if assigned_tag_list:
|
| 2443 |
+
citation_validated_tags_by_unit[display_name] = sorted(
|
| 2444 |
+
survived, key=agent._citation_tag_number
|
| 2445 |
)
|
| 2446 |
+
logging.info(
|
| 2447 |
+
"Top-level citation accumulation: unit=%s section=%s validated=%s external_claims=%s",
|
| 2448 |
+
display_name, citation_unit_section_map.get(display_name, section_id),
|
| 2449 |
+
len(survived), citation_required_claim_counts_by_unit[display_name],
|
| 2450 |
)
|
| 2451 |
if mode == "asq" and section_id == "findings":
|
| 2452 |
en_resolved = self._render_planned_qualitative_quotes(
|
|
|
|
| 2499 |
"citation_plan": citation_plan,
|
| 2500 |
"citation_minimums": citation_minimums,
|
| 2501 |
"citation_reserve_targets": citation_reserve_targets,
|
| 2502 |
+
"citation_top_level_minimums": citation_top_level_minimums,
|
| 2503 |
+
"citation_top_level_reserve_targets": citation_top_level_reserve_targets,
|
| 2504 |
+
"citation_unit_section_map": citation_unit_section_map,
|
| 2505 |
+
"citation_required_claim_counts_by_unit": citation_required_claim_counts_by_unit,
|
| 2506 |
"citation_validated_tags_by_unit": citation_validated_tags_by_unit,
|
| 2507 |
"agent_state": agent.export_state(),
|
| 2508 |
"data_metadata": data_metadata,
|
|
|
|
| 2533 |
seen_refs.add(ref)
|
| 2534 |
if not references:
|
| 2535 |
raise self._literature_insufficient_error([("final validated references", 0, 1)])
|
| 2536 |
+
validated_by_top_level = {}
|
| 2537 |
+
claims_by_top_level = {}
|
| 2538 |
+
for unit_name, section_id in citation_unit_section_map.items():
|
| 2539 |
+
if section_id not in citation_top_level_minimums:
|
| 2540 |
+
continue
|
| 2541 |
+
validated_by_top_level.setdefault(section_id, set()).update(
|
| 2542 |
+
citation_validated_tags_by_unit.get(unit_name) or []
|
| 2543 |
+
)
|
| 2544 |
+
claims_by_top_level[section_id] = claims_by_top_level.get(section_id, 0) + int(
|
| 2545 |
+
citation_required_claim_counts_by_unit.get(unit_name, 0) or 0
|
| 2546 |
+
)
|
| 2547 |
final_shortfalls = []
|
| 2548 |
+
for section_id, planned_minimum in citation_top_level_minimums.items():
|
| 2549 |
+
validated_tags = set(validated_by_top_level.get(section_id) or [])
|
| 2550 |
+
external_claims = max(0, int(claims_by_top_level.get(section_id, 0)))
|
| 2551 |
+
# Fatal authority is evidence need, not a word-count quota. The
|
| 2552 |
+
# top-level planning minimum is therefore capped by the number of
|
| 2553 |
+
# externally-oriented claims that actually survived drafting.
|
| 2554 |
+
required = min(int(planned_minimum), external_claims) if external_claims else 0
|
| 2555 |
+
if len(validated_tags) < required:
|
| 2556 |
+
final_shortfalls.append((section_id, len(validated_tags), required))
|
|
|
|
| 2557 |
if final_shortfalls:
|
| 2558 |
raise self._literature_insufficient_error(final_shortfalls)
|
| 2559 |
final_reference_count = len(references)
|
| 2560 |
logging.info(
|
| 2561 |
+
"Top-level claim-evidence final Gate passed: validated_references=%s sections=%s",
|
| 2562 |
+
final_reference_count, len(citation_top_level_minimums),
|
| 2563 |
)
|
| 2564 |
full_draft_en = full_draft_en.rstrip() + "\n\n# References\n" + "\n\n".join(references)
|
| 2565 |
actual_words = self._english_count_for_target(full_draft_en)
|
|
|
|
| 2627 |
"citation_plan": citation_plan,
|
| 2628 |
"citation_minimums": citation_minimums,
|
| 2629 |
"citation_reserve_targets": citation_reserve_targets,
|
| 2630 |
+
"citation_top_level_minimums": citation_top_level_minimums,
|
| 2631 |
+
"citation_top_level_reserve_targets": citation_top_level_reserve_targets,
|
| 2632 |
+
"citation_unit_section_map": citation_unit_section_map,
|
| 2633 |
+
"citation_required_claim_counts_by_unit": citation_required_claim_counts_by_unit,
|
| 2634 |
"citation_validated_tags_by_unit": citation_validated_tags_by_unit,
|
| 2635 |
"quality_diagnostics": quality_diagnostics,
|
| 2636 |
"english_finalized": True,
|
core/pipeline_agent.py
CHANGED
|
@@ -2828,6 +2828,67 @@ INPUT:
|
|
| 2828 |
re.I,
|
| 2829 |
))
|
| 2830 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2831 |
@staticmethod
|
| 2832 |
def _append_raw_citation_to_sentence(sentence, tag):
|
| 2833 |
raw = str(sentence or '')
|
|
@@ -2864,6 +2925,12 @@ INPUT:
|
|
| 2864 |
stripped = str(sentence or '').strip()
|
| 2865 |
if not stripped or stripped.startswith('#') or self._citation_recovery_sentence_has_citation(stripped):
|
| 2866 |
continue
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2867 |
sentence_terms = self._citation_terms(stripped)
|
| 2868 |
if len(sentence_terms) < 4:
|
| 2869 |
continue
|
|
|
|
| 2828 |
re.I,
|
| 2829 |
))
|
| 2830 |
|
| 2831 |
+
@staticmethod
|
| 2832 |
+
def _citation_claim_provenance(sentence, section_name=''):
|
| 2833 |
+
"""Classify whether an existing sentence is eligible for external-literature recovery.
|
| 2834 |
+
|
| 2835 |
+
This is deliberately conservative and deterministic. The recovery path may
|
| 2836 |
+
only add evidence to externally-oriented claims; it must not attach a source
|
| 2837 |
+
merely to satisfy a citation target when the sentence is owned by the current
|
| 2838 |
+
study, reports the current study's results, states the authors' positioning,
|
| 2839 |
+
or serves only as structure/transition.
|
| 2840 |
+
"""
|
| 2841 |
+
text = re.sub(r'\s+', ' ', str(sentence or '')).strip()
|
| 2842 |
+
lower = text.lower()
|
| 2843 |
+
section = str(section_name or '').lower()
|
| 2844 |
+
if not text:
|
| 2845 |
+
return 'STRUCTURAL'
|
| 2846 |
+
if re.match(r'^(?:h\d+[a-z]?\s*[:.\-]|hypothesis\s+\d+\b)', lower):
|
| 2847 |
+
return 'HYPOTHESIS_STATEMENT'
|
| 2848 |
+
if re.search(r'\b(?:we hypothesi[sz]e|we propose|we predict|we expect)\b', lower):
|
| 2849 |
+
return 'HYPOTHESIS_STATEMENT'
|
| 2850 |
+
if re.search(
|
| 2851 |
+
r'\b(?:the present study|this study|our study|the current study|the present research|this research)\b',
|
| 2852 |
+
lower,
|
| 2853 |
+
):
|
| 2854 |
+
if re.search(r'\b(?:contribut|position|ask|research question|does not assume|focus|aim|seek|examine|investigat|test)\w*\b', lower):
|
| 2855 |
+
return 'STUDY_POSITIONING'
|
| 2856 |
+
return 'STUDY_OWNED_FACT'
|
| 2857 |
+
if re.search(
|
| 2858 |
+
r'\b(?:we collected|we recruited|we interviewed|we observed|we conducted|we analyzed|we analysed|'
|
| 2859 |
+
r'we used|we employed|our sample|our data|our participants|sample consisted|participants included|'
|
| 2860 |
+
r'data were collected|data was collected|to answer this question,? (?:we|the study))\b',
|
| 2861 |
+
lower,
|
| 2862 |
+
):
|
| 2863 |
+
return 'STUDY_OWNED_FACT'
|
| 2864 |
+
if any(token in section for token in ('result', 'finding')) and re.search(
|
| 2865 |
+
r'\b(?:we found|we find|our findings|our results|the results|the findings|coefficient|p\s*[<=>]|r\s*[²2]|β|beta)\b',
|
| 2866 |
+
lower,
|
| 2867 |
+
):
|
| 2868 |
+
return 'RESULT_OWNED_FACT'
|
| 2869 |
+
if len(re.findall(r'[A-Za-z]{3,}', text)) < 4 and not re.search(r'[\u4e00-\u9fff\u0400-\u04ff]', text):
|
| 2870 |
+
return 'STRUCTURAL'
|
| 2871 |
+
return 'EXTERNAL_LITERATURE_CLAIM'
|
| 2872 |
+
|
| 2873 |
+
@classmethod
|
| 2874 |
+
def _citation_recovery_eligible_claim(cls, sentence, section_name=''):
|
| 2875 |
+
return cls._citation_claim_provenance(sentence, section_name) == 'EXTERNAL_LITERATURE_CLAIM'
|
| 2876 |
+
|
| 2877 |
+
@classmethod
|
| 2878 |
+
def count_external_literature_claims(cls, text, section_name=''):
|
| 2879 |
+
return sum(
|
| 2880 |
+
1 for _start, _end, sentence in cls._sentence_spans(str(text or ''))
|
| 2881 |
+
if cls._citation_recovery_eligible_claim(sentence, section_name)
|
| 2882 |
+
)
|
| 2883 |
+
|
| 2884 |
+
@classmethod
|
| 2885 |
+
def count_citation_recovery_eligible_claims(cls, text, section_name=''):
|
| 2886 |
+
return sum(
|
| 2887 |
+
1 for _start, _end, sentence in cls._sentence_spans(str(text or ''))
|
| 2888 |
+
if cls._citation_recovery_eligible_claim(sentence, section_name)
|
| 2889 |
+
and not cls._citation_recovery_sentence_has_citation(str(sentence or ''))
|
| 2890 |
+
)
|
| 2891 |
+
|
| 2892 |
@staticmethod
|
| 2893 |
def _append_raw_citation_to_sentence(sentence, tag):
|
| 2894 |
raw = str(sentence or '')
|
|
|
|
| 2925 |
stripped = str(sentence or '').strip()
|
| 2926 |
if not stripped or stripped.startswith('#') or self._citation_recovery_sentence_has_citation(stripped):
|
| 2927 |
continue
|
| 2928 |
+
if not self._citation_recovery_eligible_claim(stripped, section_name):
|
| 2929 |
+
logging.info(
|
| 2930 |
+
'Claim-anchored citation recovery skipped non-literature claim: section=%s provenance=%s',
|
| 2931 |
+
section_name, self._citation_claim_provenance(stripped, section_name),
|
| 2932 |
+
)
|
| 2933 |
+
continue
|
| 2934 |
sentence_terms = self._citation_terms(stripped)
|
| 2935 |
if len(sentence_terms) < 4:
|
| 2936 |
continue
|
i18n.py
CHANGED
|
@@ -305,6 +305,8 @@ TEXTS: dict[str, dict[str, str]] = {
|
|
| 305 |
"word_count": "📊 本章节实际字数:{actual}(目标:{target}),累计偏差:{deviation:+d}",
|
| 306 |
"translation_running": "⏳ 英文已完成,正在后台异步翻译……",
|
| 307 |
"node_error": "🚨 节点异常已中止。",
|
|
|
|
|
|
|
| 308 |
"node_error_detail": "错误代码:{code}|{error}|系统已保存断点,可继续恢复。",
|
| 309 |
"task_cancelled": "任务已取消,断点和临时文件已删除。",
|
| 310 |
"task_start_failed": "无法启动论文任务:{error}",
|
|
@@ -481,6 +483,8 @@ TEXTS: dict[str, dict[str, str]] = {
|
|
| 481 |
"word_count": "📊 Section word count: {actual} (target: {target}); cumulative deviation: {deviation:+d}",
|
| 482 |
"translation_running": "⏳ English draft complete. Background translation is running…",
|
| 483 |
"node_error": "🚨 The node stopped because of an error.",
|
|
|
|
|
|
|
| 484 |
"node_error_detail": "Error code: {code} | {error} | A checkpoint has been saved and can be resumed.",
|
| 485 |
"task_cancelled": "The task was cancelled. Its checkpoint and temporary files were deleted.",
|
| 486 |
"task_start_failed": "The paper task could not be started: {error}",
|
|
@@ -657,6 +661,8 @@ TEXTS: dict[str, dict[str, str]] = {
|
|
| 657 |
"word_count": "📊 Объём раздела: {actual} слов (цель: {target}); накопленное отклонение: {deviation:+d}",
|
| 658 |
"translation_running": "⏳ Английский текст завершён. Выполняется фоновый перевод…",
|
| 659 |
"node_error": "🚨 Выполнение узла остановлено из-за ошибки.",
|
|
|
|
|
|
|
| 660 |
"node_error_detail": "Код ошибки: {code} | {error} | Контрольная точка сохранена; работу можно продолжить.",
|
| 661 |
"task_cancelled": "Задача отменена. Контрольная точка и временные файлы удалены.",
|
| 662 |
"task_start_failed": "Не удалось запустить подготовку статьи: {error}",
|
|
@@ -833,6 +839,8 @@ TEXTS: dict[str, dict[str, str]] = {
|
|
| 833 |
"word_count": "📊 Бөлім көлемі: {actual} сөз (мақсат: {target}); жиынтық ауытқу: {deviation:+d}",
|
| 834 |
"translation_running": "⏳ Ағылшын мәтіні аяқталды. Фондық аударма орындалуда…",
|
| 835 |
"node_error": "🚨 Қате салдарынан түйін тоқтатылды.",
|
|
|
|
|
|
|
| 836 |
"node_error_detail": "Қате коды: {code} | {error} | Бақылау нүктесі сақталды; жұмысты жалғастыруға болады.",
|
| 837 |
"task_cancelled": "Тапсырма тоқтатылды. Бақылау нүктесі мен уақытша файлдар жойылды.",
|
| 838 |
"task_start_failed": "Мақала тапсырмасын бастау мүмкін болмады: {error}",
|
|
|
|
| 305 |
"word_count": "📊 本章节实际字数:{actual}(目标:{target}),累计偏差:{deviation:+d}",
|
| 306 |
"translation_running": "⏳ 英文已完成,正在后台异步翻译……",
|
| 307 |
"node_error": "🚨 节点异常已中止。",
|
| 308 |
+
"literature_insufficient": "当前高相关文献不足以直接支撑部分核心论断。系统没有使用无关文献凑数。请调整研究题目或关键词,或上传更直接相关的核心文献后重试。",
|
| 309 |
+
"upstream_translation_stopped": "英文母稿的证据检查未通过,后续翻译已停止;当前页面中已显示的译文不是完整论文。",
|
| 310 |
"node_error_detail": "错误代码:{code}|{error}|系统已保存断点,可继续恢复。",
|
| 311 |
"task_cancelled": "任务已取消,断点和临时文件已删除。",
|
| 312 |
"task_start_failed": "无法启动论文任务:{error}",
|
|
|
|
| 483 |
"word_count": "📊 Section word count: {actual} (target: {target}); cumulative deviation: {deviation:+d}",
|
| 484 |
"translation_running": "⏳ English draft complete. Background translation is running…",
|
| 485 |
"node_error": "🚨 The node stopped because of an error.",
|
| 486 |
+
"literature_insufficient": "The available high-relevance literature is insufficient to support some core manuscript claims. Unrelated sources were not used to fill a quota. Revise the research topic or keywords, or provide more directly relevant core literature, and try again.",
|
| 487 |
+
"upstream_translation_stopped": "The English source manuscript did not pass the evidence gate, so downstream translation stopped. Any translated text currently shown is incomplete and is not a final paper.",
|
| 488 |
"node_error_detail": "Error code: {code} | {error} | A checkpoint has been saved and can be resumed.",
|
| 489 |
"task_cancelled": "The task was cancelled. Its checkpoint and temporary files were deleted.",
|
| 490 |
"task_start_failed": "The paper task could not be started: {error}",
|
|
|
|
| 661 |
"word_count": "📊 Объём раздела: {actual} слов (цель: {target}); накопленное отклонение: {deviation:+d}",
|
| 662 |
"translation_running": "⏳ Английский текст завершён. Выполняется фоновый перевод…",
|
| 663 |
"node_error": "🚨 Выполнение узла остановлено из-за ошибки.",
|
| 664 |
+
"literature_insufficient": "Доступной высокорелевантной литературы недостаточно для прямой поддержки части ключевых утверждений рукописи. Система не использовала нерелевантные источники ради количества. Измените тему или ключевые слова либо добавьте более релевантные основные источники и повторите попытку.",
|
| 665 |
+
"upstream_translation_stopped": "Английская исходная рукопись не прошла проверку доказательной базы, поэтому дальнейший перевод остановлен. Уже показанный перевод неполный и не является окончательной статьёй.",
|
| 666 |
"node_error_detail": "Код ошибки: {code} | {error} | Контрольная точка сохранена; работу можно продолжить.",
|
| 667 |
"task_cancelled": "Задача отменена. Контрольная точка и временные файлы удалены.",
|
| 668 |
"task_start_failed": "Не удалось запустить подготовку статьи: {error}",
|
|
|
|
| 839 |
"word_count": "📊 Бөлім көлемі: {actual} сөз (мақсат: {target}); жиынтық ауытқу: {deviation:+d}",
|
| 840 |
"translation_running": "⏳ Ағылшын мәтіні аяқталды. Фондық аударма орындалуда…",
|
| 841 |
"node_error": "🚨 Қате салдарынан түйін тоқтатылды.",
|
| 842 |
+
"literature_insufficient": "Қолжетімді жоғары релевантты әдебиет қолжазбаның кейбір негізгі тұжырымдарын тікелей қолдауға жеткіліксіз. Жүйе санды толтыру үшін қатысы жоқ дереккөздерді пайдаланбады. Зерттеу тақырыбын немесе кілт сөздерді өзгертіңіз не тікелей релевантты негізгі әдебиетті қосып, қайта көріңіз.",
|
| 843 |
+
"upstream_translation_stopped": "Ағылшын тіліндегі бастапқы қолжазба дәлелдер тексерісінен өтпеді, сонд��қтан кейінгі аударма тоқтатылды. Қазір көрсетілген аударма толық емес және соңғы мақала болып саналмайды.",
|
| 844 |
"node_error_detail": "Қате коды: {code} | {error} | Бақылау нүктесі сақталды; жұмысты жалғастыруға болады.",
|
| 845 |
"task_cancelled": "Тапсырма тоқтатылды. Бақылау нүктесі мен уақытша файлдар жойылды.",
|
| 846 |
"task_start_failed": "Мақала тапсырмасын бастау мүмкін болмады: {error}",
|