R14-TQ2-PQ1-R4-R2 UI Mode Evidence Completion Latency Fix

#67
app.py CHANGED
@@ -11,6 +11,7 @@ import shutil
11
  import subprocess
12
  import sys
13
  import time
 
14
  from datetime import datetime, timezone
15
  from pathlib import Path
16
 
@@ -681,6 +682,11 @@ authenticated_user = require_alipay_login(_current_language_code())
681
  if not bool(st.session_state.get("sp_task_recovery_initialized")):
682
  recover_generation_task()
683
  st.session_state["sp_task_recovery_initialized"] = True
 
 
 
 
 
684
  _user_workspace = os.path.join(WORKSPACE_DIR, "users", _safe_key(authenticated_user.get("id")))
685
  os.makedirs(_user_workspace, exist_ok=True)
686
 
@@ -749,16 +755,16 @@ def _render_monitor_box(task: dict, lang_code: str) -> None:
749
 
750
  progress_markup = ""
751
  if status in ACTIVE_TASK_STATUSES:
752
- progress_markup = f"""
753
  <div id='sp-live-progress-wrap' style='margin:0 0 12px 0;'>
754
  <div id='sp-live-progress-text' style='margin-bottom:7px;'>{progress}% — {html.escape(node_label)}</div>
755
  <div style='width:100%;height:10px;background:rgba(128,128,128,.24);border-radius:999px;overflow:hidden;'>
756
  <div id='sp-live-progress-fill' style='width:{progress}%;height:100%;background:currentColor;border-radius:999px;transition:width .35s ease;'></div>
757
  </div>
758
  </div>
759
- """
760
 
761
- st.markdown(f"""
762
  <div class='log-box' id='sp-live-monitor'>
763
  {progress_markup}
764
  <div><b>{tr(lang_code, 'monitor_pipeline')}:</b> <span id='sp-live-status'>{html.escape(status_label)}</span></div>
@@ -775,7 +781,7 @@ def _render_monitor_box(task: dict, lang_code: str) -> None:
775
  <span style='background: #d1fae5;padding:4px 8px;border-radius:4px;color:black;'>🌐 OpenAlex: <b id='sp-live-oa'>{oa_calls}</b></span>
776
  </div>
777
  </div>
778
- """, unsafe_allow_html=True)
779
 
780
 
781
  def _watch_task_status_live(task: dict) -> dict | None:
@@ -990,24 +996,24 @@ def _render_checkpoint_preview(task: dict, lang_code: str) -> None:
990
  with left:
991
  st.markdown(tr(lang_code, "english_draft"))
992
  st.markdown(
993
- f"""
994
  <div id='sp-live-manuscript-shell' style='border:1px solid #e5e7eb;border-radius:8px;padding:14px 16px;min-height:220px;max-height:68vh;overflow:auto;background:#ffffff;color:#111827;'>
995
  <div id='sp-live-manuscript-status' style='white-space:pre-wrap;font-size:0.88em;color:#374151;opacity:1;margin:0 0 8px 0;display:{english_status_display};'>{html.escape(english_status_initial)}</div>
996
  <div id='sp-live-manuscript-preview' data-sp-placeholder='{english_placeholder}' style='white-space:pre-wrap;overflow-wrap:anywhere;margin:0;font-family:inherit;line-height:1.72;color:#111827;'>{html.escape(english_initial)}</div>
997
  <span id='sp-live-manuscript-cursor' aria-hidden='true' style='display:inline-block;width:7px;height:1.05em;background:#111827;margin-left:2px;vertical-align:-0.12em;animation:spBlink 1s steps(1,end) infinite;'></span>
998
  </div>
999
- """,
1000
  unsafe_allow_html=True,
1001
  )
1002
  with right:
1003
  st.markdown(tr(lang_code, "target_draft", flag=get_target_flag(lang_code)))
1004
  st.markdown(
1005
- f"""
1006
  <div id='sp-live-translation-shell' style='border:1px solid #e5e7eb;border-radius:8px;padding:14px 16px;min-height:220px;max-height:68vh;overflow:auto;background:#ffffff;color:#111827;'>
1007
  <div id='sp-live-translation-preview' data-sp-placeholder='{target_placeholder}' style='white-space:pre-wrap;overflow-wrap:anywhere;margin:0;font-family:inherit;line-height:1.72;color:#111827;'>{html.escape(target_initial)}</div>
1008
  <span id='sp-live-translation-cursor' aria-hidden='true' style='display:inline-block;width:7px;height:1.05em;background:#111827;margin-left:2px;vertical-align:-0.12em;animation:spBlink 1s steps(1,end) infinite;'></span>
1009
  </div>
1010
- """,
1011
  unsafe_allow_html=True,
1012
  )
1013
  st.markdown("<style>@keyframes spBlink { 0%,49% {opacity:1} 50%,100% {opacity:0} }</style>", unsafe_allow_html=True)
@@ -1178,6 +1184,35 @@ def _scroll_to_generation_action_once() -> None:
1178
  )
1179
 
1180
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1181
  def render_workspace_controls(user):
1182
  top_toolbar = st.container()
1183
  with top_toolbar:
@@ -1197,6 +1232,9 @@ def render_workspace_controls(user):
1197
 
1198
  task = st.session_state.get("sp_generation_task") or {}
1199
  task_status = str(task.get("status") or "")
 
 
 
1200
  active = task_status in ACTIVE_TASK_STATUSES
1201
  st.session_state["is_running"] = active
1202
 
@@ -1279,6 +1317,7 @@ def render_workspace_controls(user):
1279
  st.info(tr(lang_code, "pipeline_start"))
1280
  _inject_generation_action_scroll("asq")
1281
 
 
1282
  inject_file_uploader_i18n(lang_code)
1283
 
1284
 
 
11
  import subprocess
12
  import sys
13
  import time
14
+ import textwrap
15
  from datetime import datetime, timezone
16
  from pathlib import Path
17
 
 
682
  if not bool(st.session_state.get("sp_task_recovery_initialized")):
683
  recover_generation_task()
684
  st.session_state["sp_task_recovery_initialized"] = True
685
+ _recovered_task = st.session_state.get("sp_generation_task") or {}
686
+ _recovered_mode = str((_recovered_task or {}).get("mode") or "").strip().lower()
687
+ if _recovered_mode in {"amj", "asq"}:
688
+ # The task record, not the first visual tab, owns the research mode after refresh.
689
+ st.session_state["run_mode"] = _recovered_mode
690
  _user_workspace = os.path.join(WORKSPACE_DIR, "users", _safe_key(authenticated_user.get("id")))
691
  os.makedirs(_user_workspace, exist_ok=True)
692
 
 
755
 
756
  progress_markup = ""
757
  if status in ACTIVE_TASK_STATUSES:
758
+ progress_markup = textwrap.dedent(f"""
759
  <div id='sp-live-progress-wrap' style='margin:0 0 12px 0;'>
760
  <div id='sp-live-progress-text' style='margin-bottom:7px;'>{progress}% — {html.escape(node_label)}</div>
761
  <div style='width:100%;height:10px;background:rgba(128,128,128,.24);border-radius:999px;overflow:hidden;'>
762
  <div id='sp-live-progress-fill' style='width:{progress}%;height:100%;background:currentColor;border-radius:999px;transition:width .35s ease;'></div>
763
  </div>
764
  </div>
765
+ """).strip()
766
 
767
+ st.markdown(textwrap.dedent(f"""
768
  <div class='log-box' id='sp-live-monitor'>
769
  {progress_markup}
770
  <div><b>{tr(lang_code, 'monitor_pipeline')}:</b> <span id='sp-live-status'>{html.escape(status_label)}</span></div>
 
781
  <span style='background: #d1fae5;padding:4px 8px;border-radius:4px;color:black;'>🌐 OpenAlex: <b id='sp-live-oa'>{oa_calls}</b></span>
782
  </div>
783
  </div>
784
+ """).strip(), unsafe_allow_html=True)
785
 
786
 
787
  def _watch_task_status_live(task: dict) -> dict | None:
 
996
  with left:
997
  st.markdown(tr(lang_code, "english_draft"))
998
  st.markdown(
999
+ textwrap.dedent(f"""
1000
  <div id='sp-live-manuscript-shell' style='border:1px solid #e5e7eb;border-radius:8px;padding:14px 16px;min-height:220px;max-height:68vh;overflow:auto;background:#ffffff;color:#111827;'>
1001
  <div id='sp-live-manuscript-status' style='white-space:pre-wrap;font-size:0.88em;color:#374151;opacity:1;margin:0 0 8px 0;display:{english_status_display};'>{html.escape(english_status_initial)}</div>
1002
  <div id='sp-live-manuscript-preview' data-sp-placeholder='{english_placeholder}' style='white-space:pre-wrap;overflow-wrap:anywhere;margin:0;font-family:inherit;line-height:1.72;color:#111827;'>{html.escape(english_initial)}</div>
1003
  <span id='sp-live-manuscript-cursor' aria-hidden='true' style='display:inline-block;width:7px;height:1.05em;background:#111827;margin-left:2px;vertical-align:-0.12em;animation:spBlink 1s steps(1,end) infinite;'></span>
1004
  </div>
1005
+ """).strip(),
1006
  unsafe_allow_html=True,
1007
  )
1008
  with right:
1009
  st.markdown(tr(lang_code, "target_draft", flag=get_target_flag(lang_code)))
1010
  st.markdown(
1011
+ textwrap.dedent(f"""
1012
  <div id='sp-live-translation-shell' style='border:1px solid #e5e7eb;border-radius:8px;padding:14px 16px;min-height:220px;max-height:68vh;overflow:auto;background:#ffffff;color:#111827;'>
1013
  <div id='sp-live-translation-preview' data-sp-placeholder='{target_placeholder}' style='white-space:pre-wrap;overflow-wrap:anywhere;margin:0;font-family:inherit;line-height:1.72;color:#111827;'>{html.escape(target_initial)}</div>
1014
  <span id='sp-live-translation-cursor' aria-hidden='true' style='display:inline-block;width:7px;height:1.05em;background:#111827;margin-left:2px;vertical-align:-0.12em;animation:spBlink 1s steps(1,end) infinite;'></span>
1015
  </div>
1016
+ """).strip(),
1017
  unsafe_allow_html=True,
1018
  )
1019
  st.markdown("<style>@keyframes spBlink { 0%,49% {opacity:1} 50%,100% {opacity:0} }</style>", unsafe_allow_html=True)
 
1184
  )
1185
 
1186
 
1187
+ def _select_active_task_tab(task: dict, lang_code: str) -> None:
1188
+ """Keep the existing visual tab aligned with the immutable task mode after rerun/refresh."""
1189
+ status = str((task or {}).get("status") or "")
1190
+ mode = str((task or {}).get("mode") or "").strip().lower()
1191
+ if status not in ACTIVE_TASK_STATUSES and status != "resumable":
1192
+ return
1193
+ if mode not in {"amj", "asq"}:
1194
+ return
1195
+ label = tr(lang_code, "tab_asq" if mode == "asq" else "tab_amj")
1196
+ components.html(
1197
+ f"""
1198
+ <script>
1199
+ (() => {{
1200
+ const parentDocument = window.parent.document;
1201
+ const expected = {json.dumps(label)};
1202
+ const select = () => {{
1203
+ const tabs = Array.from(parentDocument.querySelectorAll('[role="tab"]'));
1204
+ const target = tabs.find((node) => (node.textContent || '').trim() === expected.trim());
1205
+ if (target && target.getAttribute('aria-selected') !== 'true') target.click();
1206
+ }};
1207
+ [0, 80, 220, 500, 900].forEach((delay) => window.parent.setTimeout(select, delay));
1208
+ }})();
1209
+ </script>
1210
+ """,
1211
+ height=0,
1212
+ width=0,
1213
+ )
1214
+
1215
+
1216
  def render_workspace_controls(user):
1217
  top_toolbar = st.container()
1218
  with top_toolbar:
 
1232
 
1233
  task = st.session_state.get("sp_generation_task") or {}
1234
  task_status = str(task.get("status") or "")
1235
+ task_mode = str(task.get("mode") or "").strip().lower()
1236
+ if task_mode in {"amj", "asq"}:
1237
+ st.session_state["run_mode"] = task_mode
1238
  active = task_status in ACTIVE_TASK_STATUSES
1239
  st.session_state["is_running"] = active
1240
 
 
1317
  st.info(tr(lang_code, "pipeline_start"))
1318
  _inject_generation_action_scroll("asq")
1319
 
1320
+ _select_active_task_tab(task, lang_code)
1321
  inject_file_uploader_i18n(lang_code)
1322
 
1323
 
core/generation_runner.py CHANGED
@@ -1923,7 +1923,13 @@ class GenerationRunner:
1923
  progress_callback=lambda: self._progress(9, "S2 + OpenAlex"),
1924
  )
1925
  if not agent.citation_map:
1926
- raise self._literature_insufficient_error([("retrieved literature pool", 0, 1)])
 
 
 
 
 
 
1927
  state["literature_candidate_target"] = literature_candidate_target
1928
  if user_lit_context:
1929
  core_refs = agent.parse_user_core_literature(user_lit_context)
@@ -1942,9 +1948,16 @@ class GenerationRunner:
1942
  self._progress(12, "literature_synthesis")
1943
  prewrite_started = time.monotonic()
1944
  prewrite_calls = agent.ds_calls
1945
- synthesis_result = self._stream_text(
1946
- agent.stream_synthesize_literature(topic, refs_text, user_core_literature=user_lit_context)
1947
- )
 
 
 
 
 
 
 
1948
  agent.record_prewrite_stage("literature_synthesis", prewrite_started, prewrite_calls)
1949
  synthesis_result = re.sub(r"<think>.*?</think>", "", synthesis_result, flags=re.DOTALL).strip()
1950
  state.update({"synthesis_result": synthesis_result, "agent_state": agent.export_state()})
@@ -2385,6 +2398,13 @@ class GenerationRunner:
2385
  "Never stack sources, substitute tags, or use literature as evidence for this study's own facts.\n"
2386
  f"<CLAIM_EVIDENCE_PLAN>\n{claim_plan_text}\n</CLAIM_EVIDENCE_PLAN>"
2387
  )
 
 
 
 
 
 
 
2388
  if mode == "asq" and section_id not in {"findings", "appendix"}:
2389
  citation_rule += (
2390
  "\nASQ EVIDENCE-CHANNEL RULE: A sentence whose factual subject is this study's setting, participants, interviews, data sources, cases, actors, observations, events, quotations, analytic steps, or empirical findings must not carry an external [CITE-XX] tag. "
@@ -2715,7 +2735,13 @@ Output only final academic prose. Do not report the word count."""
2715
  references.append(f"- {ref}")
2716
  seen_refs.add(ref)
2717
  if not references:
2718
- raise self._literature_insufficient_error([("final validated references", 0, 1)])
 
 
 
 
 
 
2719
  validated_by_top_level = {}
2720
  claims_by_top_level = {}
2721
  for unit_name, section_id in citation_unit_section_map.items():
@@ -2738,15 +2764,16 @@ Output only final academic prose. Do not report the word count."""
2738
  for section_id, planned_minimum in citation_top_level_minimums.items():
2739
  validated_tags = set(validated_by_top_level.get(section_id) or [])
2740
  external_claims = max(0, int(claims_by_top_level.get(section_id, 0)))
2741
- # Fatal authority is evidence need, not a word-count quota. The
2742
  # top-level planning minimum is therefore capped by the number of
2743
  # externally-oriented claims that actually survived drafting.
2744
  required = min(int(planned_minimum), external_claims) if external_claims else 0
2745
  if len(validated_tags) < required:
2746
  final_shortfalls.append((section_id, len(validated_tags), required))
2747
  final_reference_count = len(references)
2748
- if final_shortfalls:
2749
- self._record_evidence_coverage_event(
 
2750
  state, final_shortfalls, phase="final_claim_evidence_gate",
2751
  preview_status="limited",
2752
  )
@@ -2782,7 +2809,7 @@ Output only final academic prose. Do not report the word count."""
2782
  quality_diagnostics, self._validate_target_length(full_draft_en, target_words)
2783
  )
2784
  body_only, _reference_tail = agent.validate_reference_tail(
2785
- full_draft_en, minimum_references=max(1, final_reference_count)
2786
  )
2787
  agent.validate_manuscript_structure(body_only, adaptive_structure, translated=False)
2788
  agent.enforce_resolved_claim_specific_citations(body_only)
@@ -2801,7 +2828,7 @@ Output only final academic prose. Do not report the word count."""
2801
  full_draft_en = replace_abstract(full_draft_en, final_abstract)
2802
  full_draft_en = self._replace_keywords(full_draft_en, final_keywords)
2803
  body_only, _reference_tail = agent.validate_reference_tail(
2804
- full_draft_en, minimum_references=max(1, final_reference_count)
2805
  )
2806
  agent.validate_manuscript_structure(body_only, adaptive_structure, translated=False)
2807
  agent.enforce_resolved_claim_specific_citations(body_only)
@@ -2874,7 +2901,7 @@ Output only final academic prose. Do not report the word count."""
2874
  )
2875
  self._shutdown_translation_worker(wait=True)
2876
  body_only, reference_tail = agent.validate_reference_tail(
2877
- full_draft_en, minimum_references=max(1, int(state.get("final_reference_count") or 1))
2878
  )
2879
  section_order = self._unique_top_level_sections(adaptive_structure)
2880
  if not section_order:
 
1923
  progress_callback=lambda: self._progress(9, "S2 + OpenAlex"),
1924
  )
1925
  if not agent.citation_map:
1926
+ self._record_evidence_coverage_event(
1927
+ state, [("retrieved literature pool", 0, 1)],
1928
+ phase="retrieval_pool_empty", preview_status="limited_risk",
1929
+ )
1930
+ logging.warning(
1931
+ "No directly validated literature was retrieved; continuing in evidence-limited mode without padding references."
1932
+ )
1933
  state["literature_candidate_target"] = literature_candidate_target
1934
  if user_lit_context:
1935
  core_refs = agent.parse_user_core_literature(user_lit_context)
 
1948
  self._progress(12, "literature_synthesis")
1949
  prewrite_started = time.monotonic()
1950
  prewrite_calls = agent.ds_calls
1951
+ if not str(refs_text or "").strip() and not str(user_lit_context or "").strip():
1952
+ synthesis_result = (
1953
+ "EVIDENCE-LIMITED CONTEXT: no directly validated external literature is currently available. "
1954
+ "Continue with the frozen study evidence, cautious conceptual reasoning, and general scholarly knowledge, "
1955
+ "but do not invent citations or bibliographic metadata and do not present general model knowledge as retrieved evidence."
1956
+ )
1957
+ else:
1958
+ synthesis_result = self._stream_text(
1959
+ agent.stream_synthesize_literature(topic, refs_text, user_core_literature=user_lit_context)
1960
+ )
1961
  agent.record_prewrite_stage("literature_synthesis", prewrite_started, prewrite_calls)
1962
  synthesis_result = re.sub(r"<think>.*?</think>", "", synthesis_result, flags=re.DOTALL).strip()
1963
  state.update({"synthesis_result": synthesis_result, "agent_state": agent.export_state()})
 
2398
  "Never stack sources, substitute tags, or use literature as evidence for this study's own facts.\n"
2399
  f"<CLAIM_EVIDENCE_PLAN>\n{claim_plan_text}\n</CLAIM_EVIDENCE_PLAN>"
2400
  )
2401
+ if not claim_plan_text.strip():
2402
+ citation_rule += (
2403
+ "\nEVIDENCE-LIMITED WRITING RULE: no directly validated source is assigned to this unit. "
2404
+ "Continue the scholarly argument using frozen study evidence, cautious conceptual reasoning, and general scholarly knowledge. "
2405
+ "Do not invent citations, authors, titles, DOI values, or bibliographic metadata; do not describe general model knowledge as if it were retrieved or directly validated literature. "
2406
+ "Use appropriately qualified language for propositions, interpretations, and research opportunities."
2407
+ )
2408
  if mode == "asq" and section_id not in {"findings", "appendix"}:
2409
  citation_rule += (
2410
  "\nASQ EVIDENCE-CHANNEL RULE: A sentence whose factual subject is this study's setting, participants, interviews, data sources, cases, actors, observations, events, quotations, analytic steps, or empirical findings must not carry an external [CITE-XX] tag. "
 
2735
  references.append(f"- {ref}")
2736
  seen_refs.add(ref)
2737
  if not references:
2738
+ self._record_evidence_coverage_event(
2739
+ state, [("final validated references", 0, 1)],
2740
+ phase="final_validated_references_empty", preview_status="limited",
2741
+ )
2742
+ logging.warning(
2743
+ "Final validated reference list is empty; completing in evidence-limited mode without unrelated or invented references."
2744
+ )
2745
  validated_by_top_level = {}
2746
  claims_by_top_level = {}
2747
  for unit_name, section_id in citation_unit_section_map.items():
 
2764
  for section_id, planned_minimum in citation_top_level_minimums.items():
2765
  validated_tags = set(validated_by_top_level.get(section_id) or [])
2766
  external_claims = max(0, int(claims_by_top_level.get(section_id, 0)))
2767
+ # Evidence authority is need-based, not a word-count quota. The
2768
  # top-level planning minimum is therefore capped by the number of
2769
  # externally-oriented claims that actually survived drafting.
2770
  required = min(int(planned_minimum), external_claims) if external_claims else 0
2771
  if len(validated_tags) < required:
2772
  final_shortfalls.append((section_id, len(validated_tags), required))
2773
  final_reference_count = len(references)
2774
+ if final_shortfalls or final_reference_count == 0:
2775
+ if final_shortfalls:
2776
+ self._record_evidence_coverage_event(
2777
  state, final_shortfalls, phase="final_claim_evidence_gate",
2778
  preview_status="limited",
2779
  )
 
2809
  quality_diagnostics, self._validate_target_length(full_draft_en, target_words)
2810
  )
2811
  body_only, _reference_tail = agent.validate_reference_tail(
2812
+ full_draft_en, minimum_references=final_reference_count
2813
  )
2814
  agent.validate_manuscript_structure(body_only, adaptive_structure, translated=False)
2815
  agent.enforce_resolved_claim_specific_citations(body_only)
 
2828
  full_draft_en = replace_abstract(full_draft_en, final_abstract)
2829
  full_draft_en = self._replace_keywords(full_draft_en, final_keywords)
2830
  body_only, _reference_tail = agent.validate_reference_tail(
2831
+ full_draft_en, minimum_references=final_reference_count
2832
  )
2833
  agent.validate_manuscript_structure(body_only, adaptive_structure, translated=False)
2834
  agent.enforce_resolved_claim_specific_citations(body_only)
 
2901
  )
2902
  self._shutdown_translation_worker(wait=True)
2903
  body_only, reference_tail = agent.validate_reference_tail(
2904
+ full_draft_en, minimum_references=max(0, int(state.get("final_reference_count") or 0))
2905
  )
2906
  section_order = self._unique_top_level_sections(adaptive_structure)
2907
  if not section_order:
core/pipeline_agent.py CHANGED
@@ -1247,34 +1247,14 @@ class SSciPipelineAgent:
1247
  "{topic}", topic).replace("{user_injection}", user_injection).replace(
1248
  "{refs_text}", refs_text).replace("{directive}", APP_CONFIG['deep_synthesis_directive'])
1249
 
1250
- def _stream_with_reasoning_exhaustion_recovery():
1251
- try:
1252
- reasoning_budget = int((APP_CONFIG.get("prewrite_reasoning_max_tokens") or {}).get("literature_synthesis", 2400))
1253
- yield from self.stream_deepseek(
1254
- sys_prompt,
1255
- user_prompt,
1256
- max_tokens=reasoning_budget,
1257
- model_role="reasoning",
1258
- thinking=True,
1259
- allow_fallback=False,
1260
- reasoning_only_recovery=True,
1261
- )
1262
- return
1263
- except DeepSeekReasoningOnlyExhaustionError as exc:
1264
- logging.warning(
1265
- "Literature synthesis reasoning-only exhaustion; retrying once with thinking disabled: %s",
1266
- exc,
1267
- )
1268
- self._touch_execution_progress("literature_synthesis_direct_recovery")
1269
-
1270
- recovery_system = (
1271
  sys_prompt
1272
- + "\n\nRECOVERY MODE: Produce the final literature synthesis directly. "
1273
- "Do not perform extended hidden reasoning. Preserve the original evidence, "
1274
- "citation boundaries, uncertainty, and scope; do not invent facts."
1275
  )
1276
  yield from self.stream_deepseek(
1277
- recovery_system,
1278
  user_prompt,
1279
  max_tokens=8000,
1280
  model_role="reasoning",
@@ -1282,7 +1262,7 @@ class SSciPipelineAgent:
1282
  allow_fallback=False,
1283
  )
1284
 
1285
- return _stream_with_reasoning_exhaustion_recovery()
1286
 
1287
  # Generate abstract and conclusion anchors from literature plus evidence.
1288
  def stream_generate_research_logic(
@@ -1800,12 +1780,14 @@ USER KEYWORD SEEDS: {keywords}
1800
  tail = value[match.end():].strip()
1801
  if not body:
1802
  raise DeepSeekAPIError('References tail validation failed: manuscript body is empty.')
 
1803
  if not tail:
 
 
1804
  raise DeepSeekAPIError('References tail validation failed: References is empty.')
1805
  if re.search(r'(?m)^#\s+', tail):
1806
  raise DeepSeekAPIError('References tail validation failed: another H1 appears after References.')
1807
  refs = [line.strip() for line in tail.splitlines() if line.strip().startswith('- ')]
1808
- required = max(1, int(minimum_references or 1))
1809
  if len(refs) < required:
1810
  raise DeepSeekAPIError(
1811
  f'References tail validation failed: references={len(refs)}, required>={required}.'
@@ -3378,14 +3360,13 @@ Return exactly:
3378
  for technical_attempt in range(technical_retries):
3379
  self._check_cancelled()
3380
  try:
3381
- compact_retry = technical_attempt > 0
3382
  response = self.call_deepseek(
3383
  system_prompt,
3384
  user_prompt,
3385
- max_tokens=3200 if compact_retry else 8000,
3386
  temp=0.0,
3387
  model_role="reasoning",
3388
- thinking=not compact_retry,
3389
  allow_fallback=False,
3390
  )
3391
  parsed = self._extract_json_from_text(response)
 
1247
  "{topic}", topic).replace("{user_injection}", user_injection).replace(
1248
  "{refs_text}", refs_text).replace("{directive}", APP_CONFIG['deep_synthesis_directive'])
1249
 
1250
+ def _stream_direct_synthesis():
1251
+ direct_system = (
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1252
  sys_prompt
1253
+ + "\n\nDIRECT SYNTHESIS MODE: Produce the final literature synthesis directly without extended hidden reasoning. "
1254
+ "Preserve the supplied evidence, citation boundaries, uncertainty, and scope; do not invent facts or sources."
 
1255
  )
1256
  yield from self.stream_deepseek(
1257
+ direct_system,
1258
  user_prompt,
1259
  max_tokens=8000,
1260
  model_role="reasoning",
 
1262
  allow_fallback=False,
1263
  )
1264
 
1265
+ return _stream_direct_synthesis()
1266
 
1267
  # Generate abstract and conclusion anchors from literature plus evidence.
1268
  def stream_generate_research_logic(
 
1780
  tail = value[match.end():].strip()
1781
  if not body:
1782
  raise DeepSeekAPIError('References tail validation failed: manuscript body is empty.')
1783
+ required = max(0, int(minimum_references if minimum_references is not None else 1))
1784
  if not tail:
1785
+ if required == 0:
1786
+ return body, ""
1787
  raise DeepSeekAPIError('References tail validation failed: References is empty.')
1788
  if re.search(r'(?m)^#\s+', tail):
1789
  raise DeepSeekAPIError('References tail validation failed: another H1 appears after References.')
1790
  refs = [line.strip() for line in tail.splitlines() if line.strip().startswith('- ')]
 
1791
  if len(refs) < required:
1792
  raise DeepSeekAPIError(
1793
  f'References tail validation failed: references={len(refs)}, required>={required}.'
 
3360
  for technical_attempt in range(technical_retries):
3361
  self._check_cancelled()
3362
  try:
 
3363
  response = self.call_deepseek(
3364
  system_prompt,
3365
  user_prompt,
3366
+ max_tokens=3200 if technical_attempt == 0 else 4200,
3367
  temp=0.0,
3368
  model_role="reasoning",
3369
+ thinking=False,
3370
  allow_fallback=False,
3371
  )
3372
  parsed = self._extract_json_from_text(response)
core/qualitative_engine.py CHANGED
@@ -335,30 +335,12 @@ Rules:
335
  # content retries here. Only a completed but locally invalid packet
336
  # reaches the second attempt.
337
  if local_attempt == 0:
338
- reasoning_budget = int((APP_CONFIG.get("prewrite_reasoning_max_tokens") or {}).get("qualitative_packet", 2400))
339
- try:
340
- response = "".join(
341
- str(chunk or "")
342
- for chunk in self.agent.stream_deepseek(
343
- system_prompt,
344
- base_prompt,
345
- max_tokens=reasoning_budget,
346
- model_role="reasoning",
347
- thinking=True,
348
- allow_fallback=False,
349
- reasoning_only_recovery=True,
350
- )
351
- )
352
- except Exception as exc:
353
- if exc.__class__.__name__ != "DeepSeekReasoningOnlyExhaustionError":
354
- raise
355
- logging.warning("Qualitative packet reasoning-only exhaustion; switching once to direct final-output mode.")
356
- response = self.agent.call_deepseek(
357
- system_prompt + "\n\nDIRECT OUTPUT MODE: Produce the complete evidence packet directly without extended hidden reasoning.",
358
- base_prompt,
359
- max_tokens=8000, temp=0.0, model_role="reasoning",
360
- thinking=False, allow_fallback=False, response_format={"type": "json_object"},
361
- )
362
  else:
363
  retry_prompt = base_prompt + (
364
  "\n\nLOCAL VALIDATION RETRY: The previous completed response failed local "
 
335
  # content retries here. Only a completed but locally invalid packet
336
  # reaches the second attempt.
337
  if local_attempt == 0:
338
+ response = self.agent.call_deepseek(
339
+ system_prompt + "\n\nDIRECT OUTPUT MODE: Produce the complete evidence packet directly without extended hidden reasoning.",
340
+ base_prompt,
341
+ max_tokens=8000, temp=0.0, model_role="reasoning",
342
+ thinking=False, allow_fallback=False, response_format={"type": "json_object"},
343
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
344
  else:
345
  retry_prompt = base_prompt + (
346
  "\n\nLOCAL VALIDATION RETRY: The previous completed response failed local "
i18n.py CHANGED
@@ -319,8 +319,8 @@ TEXTS: dict[str, dict[str, str]] = {
319
  "node_error": "🚨 节点异常已中止。",
320
  "literature_insufficient": "当前没有检索到可用于形成有效参考文献列表的直接相关文献。系统未使用无关文献凑数。请检查研究题目、关键词或补充核心文献后重试。",
321
  "upstream_translation_stopped": "英文母稿未形成可交付的有效参考文献列表,因此后续翻译已停止;当前页面中已显示的译文不是完整论文。",
322
- "evidence_limited_running": "文献证据覆盖提示:当前检索与语义核验显示,部分核心外部论断可直接匹配的高相关文献较少。这可能与研究主题较新、领域较专门、跨领域文献分散、关键词表达或数据库覆盖等因素有关;仅凭这一状态不能判断研究价值。系统将继续生成论文,并且不会使用无关文献补足数量。",
323
- "evidence_limited_completed": "论文已完整生成。部分核心外部论断的直接文献证据覆盖有限,系统未使用不相关文献凑数。该状态不能判断研究价值;建议正式投稿前结合 EVIDENCE_COVERAGE_REPORT.txt 进一步补充或复核相关核心文献。",
324
  "node_error_detail": "错误代码:{code}|{error}|系统已保存断点,可继续恢复。",
325
  "task_cancelled": "任务已取消,断点和临时文件已删除。",
326
  "task_start_failed": "无法启动论文任务:{error}",
@@ -519,8 +519,8 @@ TEXTS: dict[str, dict[str, str]] = {
519
  "node_error": "🚨 The node stopped because of an error.",
520
  "literature_insufficient": "No directly relevant literature was retrieved that can support a valid final reference list. Unrelated sources were not used to fill a quota. Check the topic or keywords, or provide relevant core literature, and try again.",
521
  "upstream_translation_stopped": "The English source manuscript could not form a deliverable validated reference list, so downstream translation stopped. Any translated text currently shown is incomplete and is not a final paper.",
522
- "evidence_limited_running": "Literature-evidence coverage notice: current retrieval and semantic validation indicate that some core external claims have relatively few directly matching high-relevance sources. This may reflect a newer topic, a specialized field, dispersed interdisciplinary literature, keyword phrasing, or database coverage; this status alone does not determine research value. Generation will continue, and unrelated sources will not be used to fill a quota.",
523
- "evidence_limited_completed": "The paper was generated in full. Direct literature-evidence coverage is limited for some core external claims, and unrelated sources were not used to fill a quota. This status does not determine research value. Before formal submission, review EVIDENCE_COVERAGE_REPORT.txt and consider adding or rechecking relevant core literature.",
524
  "node_error_detail": "Error code: {code} | {error} | A checkpoint has been saved and can be resumed.",
525
  "task_cancelled": "The task was cancelled. Its checkpoint and temporary files were deleted.",
526
  "task_start_failed": "The paper task could not be started: {error}",
@@ -719,8 +719,8 @@ TEXTS: dict[str, dict[str, str]] = {
719
  "node_error": "🚨 Выполнение узла остановлено из-за ошибки.",
720
  "literature_insufficient": "Не удалось найти прямо релевантную литературу, достаточную для формирования валидного итогового списка источников. Система не использовала нерелевантные публикации ради количества. Проверьте тему и ключевые слова либо добавьте релевантные основные источники и повторите попытку.",
721
  "upstream_translation_stopped": "Для английской исходной рукописи не удалось сформировать пригодный к выдаче валидный список источников, поэтому дальнейший перевод остановлен. Уже показанный перевод неполный и не является окончательной статьёй.",
722
- "evidence_limited_running": "Уведомление о покрытии литературными доказательствами: текущий поиск и семантическая проверка показывают, что для части ключевых внешних утверждений найдено относительно мало высокорелевантных источников с прямой поддержкой. Это может быть связано с новизной темы, узкой специализацией, рассредоточенностью междисциплинарной литературы, формулировкой ключевых слов или охватом баз данных; по одному этому признаку нельзя судить о ценности исследования. Генерация продолжится, а нерелевантные источники не будут использоваться ради количества.",
723
- "evidence_limited_completed": "Статья полностью сформирована. Для части ключевых внешних утверждений прямое покрытие литературными доказательствами ограничено; нерелевантные источники не использовались ради количества. Этот статус не определяет ценность исследования. Перед подачей рекомендуется изучить EVIDENCE_COVERAGE_REPORT.txt и при необходимости дополнить или перепроверить основную литературу.",
724
  "node_error_detail": "Код ошибки: {code} | {error} | Контрольная точка сохранена; работу можно продолжить.",
725
  "task_cancelled": "Задача отменена. Контрольная точка и временные файлы удалены.",
726
  "task_start_failed": "Не удалось запустить подготовку статьи: {error}",
@@ -919,8 +919,8 @@ TEXTS: dict[str, dict[str, str]] = {
919
  "node_error": "🚨 Қате салдарынан түйін тоқтатылды.",
920
  "literature_insufficient": "Жарамды соңғы әдебиеттер тізімін құруға жеткілікті тікелей релевантты әдебиет табылмады. Жүйе санды толтыру үшін қатысы жоқ дереккөздерді пайдаланбады. Тақырып пен кілт сөздерді тексеріңіз немесе релевантты негізгі әдебиетті қосып, қайта көріңіз.",
921
  "upstream_translation_stopped": "Ағылшын бастапқы қолжазбасы үшін жеткізуге жарамды тексерілген әдебиеттер тізімі қалыптаспады, сондықтан кейінгі аударма тоқтатылды. Қазір көрсетілген аударма толық емес және соңғы мақала болып саналмайды.",
922
- "evidence_limited_running": "Әдеби дәлелдерді қамту туралы ескерту: ағымдағы іздеу және семантикалық тексеру кейбір негізгі сыртқы тұжырымдарға тікелей сәйкес келетін жоғары релевантты дереккөздер салыстырмалы түрде аз екенін көрсетеді. Бұл тақырыптың жаңалығына, саланың тар мамандандырылуына, пәнаралық әдебиеттің шашыраңқылығына, кілт сөздердің тұжырымдалуына немесе дерекқор қамтуына байланысты болуы мүмкін; осы мәртебенің өзі зерттеу құндылығын анықтамайды. Генерация жалғасады және жүйе санды толтыру үшін қатысы жоқ дереккөздерді пайдаланбайды.",
923
- "evidence_limited_completed": "Мақала толық жасалды. Кейбір негізгі сыртқы тұжырымдар үшін тікелей әдеби дәлелдер қамтуы шектеулі, ал жүйе санды толтыру үшін қатысы жоқ дереккөздерді пайдаланбады. Бұл мәртебе зерттеу құндылығын анықтамайды. Ресми тапсыру алдында EVIDENCE_COVERAGE_REPORT.txt файлын қарап, қажет болса релевантты негізгі әдебиетті толықтыру немесе қайта тексеру ұсынылады.",
924
  "node_error_detail": "Қате коды: {code} | {error} | Бақылау нүктесі сақталды; жұмысты жалғастыруға болады.",
925
  "task_cancelled": "Тапсырма тоқтатылды. Бақылау нүктесі мен уақытша файлдар жойылды.",
926
  "task_start_failed": "Мақала тапсырмасын бастау мүмкін болмады: {error}",
 
319
  "node_error": "🚨 节点异常已中止。",
320
  "literature_insufficient": "当前没有检索到可用于形成有效参考文献列表的直接相关文献。系统未使用无关文献凑数。请检查研究题目、关键词或补充核心文献后重试。",
321
  "upstream_translation_stopped": "英文母稿未形成可交付的有效参考文献列表,因此后续翻译已停止;当前页面中已显示的译文不是完整论文。",
322
+ "evidence_limited_running": "文献证据覆盖提示:当前主题可能较前沿、较小众或具有较强跨学科特征,因此数据库中能够直接核验的高相关文献可能较少。这不会中断论文生成,也不代表研究价值不足。系统将继续完成论文,并坚持不使用无关或无法核验的文献凑数。建议后续结合领域核心文献进一步完善;对尚未被充分研究的问题保持探索,可能形成有价值的学术贡献。",
323
+ "evidence_limited_completed": "论文已完整生成。当前主题的直接可核验文献证据覆盖有限,可能与研究较前沿、较小众或跨学科有关;这不代表研究价值不足。系统未使用无关或无法核验的文献凑数。正式投稿前建议结合 EVIDENCE_COVERAGE_REPORT.txt 补充或复核领域核心文献,并继续审慎推进有价值的新问题探索。",
324
  "node_error_detail": "错误代码:{code}|{error}|系统已保存断点,可继续恢复。",
325
  "task_cancelled": "任务已取消,断点和临时文件已删除。",
326
  "task_start_failed": "无法启动论文任务:{error}",
 
519
  "node_error": "🚨 The node stopped because of an error.",
520
  "literature_insufficient": "No directly relevant literature was retrieved that can support a valid final reference list. Unrelated sources were not used to fill a quota. Check the topic or keywords, or provide relevant core literature, and try again.",
521
  "upstream_translation_stopped": "The English source manuscript could not form a deliverable validated reference list, so downstream translation stopped. Any translated text currently shown is incomplete and is not a final paper.",
522
+ "evidence_limited_running": "Literature-evidence coverage notice: this topic may be frontier, niche, or strongly interdisciplinary, so databases may contain relatively few high-relevance sources that can be directly validated. This will not interrupt manuscript generation and does not imply low research value. The system will continue the paper without padding it with irrelevant or unverifiable references. Consider adding core field literature later; careful exploration of under-studied questions may yield a valuable academic contribution.",
523
+ "evidence_limited_completed": "The paper was generated in full despite limited directly validated literature coverage. The topic may be frontier, niche, or interdisciplinary; this does not imply low research value. No irrelevant or unverifiable references were added merely to reach a count. Before formal submission, review EVIDENCE_COVERAGE_REPORT.txt and supplement or recheck core field literature while continuing careful exploration of the research question.",
524
  "node_error_detail": "Error code: {code} | {error} | A checkpoint has been saved and can be resumed.",
525
  "task_cancelled": "The task was cancelled. Its checkpoint and temporary files were deleted.",
526
  "task_start_failed": "The paper task could not be started: {error}",
 
719
  "node_error": "🚨 Выполнение узла остановлено из-за ошибки.",
720
  "literature_insufficient": "Не удалось найти прямо релевантную литературу, достаточную для формирования валидного итогового списка источников. Система не использовала нерелевантные публикации ради количества. Проверьте тему и ключевые слова либо добавьте релевантные основные источники и повторите попытку.",
721
  "upstream_translation_stopped": "Для английской исходной рукописи не удалось сформировать пригодный к выдаче валидный список источников, поэтому дальнейший перевод остановлен. Уже показанный перевод неполный и не является окончательной статьёй.",
722
+ "evidence_limited_running": "Уведомление о литературных доказательствах: тема может быть передовой, узкоспециализированной или междисциплинарной, поэтому в базах данных может быть немного высокорелевантных источников, которые удаётся напрямую подтвердить. Это не остановит создание статьи и не означает низкую научную ценность исследования. Система продолжит работу и не будет добавлять нерелевантные или непроверяемые источники ради количества. Позднее рекомендуется дополнить основную литературу по области; тщательное изучение недостаточно исследованных вопросов может дать ценный научный вклад.",
723
+ "evidence_limited_completed": "Статья полностью сформирована при ограниченном покрытии напрямую подтверждённой литературой. Тема может быть передовой, узкой или междисциплинарной; это не означает низкую научную ценность. Нерелевантные или непроверяемые источники не добавлялись ради количества. Перед подачей рекомендуется изучить EVIDENCE_COVERAGE_REPORT.txt, дополнить или перепроверить основную литературу и продолжить осторожное исследование нового вопроса.",
724
  "node_error_detail": "Код ошибки: {code} | {error} | Контрольная точка сохранена; работу можно продолжить.",
725
  "task_cancelled": "Задача отменена. Контрольная точка и временные файлы удалены.",
726
  "task_start_failed": "Не удалось запустить подготовку статьи: {error}",
 
919
  "node_error": "🚨 Қате салдарынан түйін тоқтатылды.",
920
  "literature_insufficient": "Жарамды соңғы әдебиеттер тізімін құруға жеткілікті тікелей релевантты әдебиет табылмады. Жүйе санды толтыру үшін қатысы жоқ дереккөздерді пайдаланбады. Тақырып пен кілт сөздерді тексеріңіз немесе релевантты негізгі әдебиетті қосып, қайта көріңіз.",
921
  "upstream_translation_stopped": "Ағылшын бастапқы қолжазбасы үшін жеткізуге жарамды тексерілген әдебиеттер тізімі қалыптаспады, сондықтан кейінгі аударма тоқтатылды. Қазір көрсетілген аударма толық емес және соңғы мақала болып саналмайды.",
922
+ "evidence_limited_running": "Әдеби дәлелдер туралы ескерту: тақырып өте жаңа, тар ауқымды немесе пәнаралық болуы мүмкін, сондықтан дерекқорларда тікелей тексеруге болатын жоғары релевантты еңбектер аз кездесуі ықтимал. Бұл мақала жасауды тоқтатпайды және зерттеудің ғылыми құндылығы төмен дегенді білдірмейді. Жүйе жұмысты жалғастырады және санды толтыру үшін қатысы жоқ немесе тексерілмейтін дереккөздерді қоспайды. Кейін саланың негізгі әдебиетін толықтырған жөн; аз зерттелген мәселелерді мұқият зерделеу құнды ғылыми үлеске әкелуі мүмкін.",
923
+ "evidence_limited_completed": "Мақала тікелей тексерілген әдебиет қамтуы шектеулі болғанына қарамастан толық жасалды. Тақырып жаңа, тар ауқымды немесе пәнаралық болуы мүмкін; бұл зерттеу құндылығы төмен дегенді білдірмейді. Сан үшін қатысы жоқ немесе тексерілмейтін дереккөздер қосылған жоқ. Ресми тапсыру алдында EVIDENCE_COVERAGE_REPORT.txt файлын қарап, негізгі әдебиетті толықтыру немесе қайта тексеру және жаңа зерттеу мәселесін мұқият дамытуды жалғастыру ұсынылады.",
924
  "node_error_detail": "Қате коды: {code} | {error} | Бақылау нүктесі сақталды; жұмысты жалғастыруға болады.",
925
  "task_cancelled": "Тапсырма тоқтатылды. Бақылау нүктесі мен уақытша файлдар жойылды.",
926
  "task_start_failed": "Мақала тапсырмасын бастау мүмкін болмады: {error}",