GuXiaopeng commited on
Commit
c8e9e97
·
verified ·
1 Parent(s): b3b957a

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +138 -97
  2. config.py +59 -26
app.py CHANGED
@@ -3,6 +3,7 @@
3
  from core.meta_extractor import extract_golden_metadata
4
  from core.rag_engine import initialize_rag
5
  from core.qualitative_engine import get_qualitative_engine
 
6
  import streamlit as st
7
 
8
  st.set_page_config(
@@ -24,14 +25,12 @@ import zipfile
24
  import math
25
  import json
26
  from datetime import datetime, timezone, timedelta
27
- import pandas as pd
28
- import numpy as np
29
  import threading
30
  from concurrent.futures import ThreadPoolExecutor
31
  from streamlit.runtime.scriptrunner import add_script_run_ctx, get_script_run_ctx
32
 
33
  from config import APP_CONFIG, AMJ_PROMPTS, ASQ_PROMPTS, GENERIC_PROMPTS
34
- from utils.file_parser import extract_text_from_files, extract_data_from_files
35
  from utils.docx_builder import text_to_docx
36
  from core.pipeline_agent import SSciPipelineAgent
37
  from managed_auth import require_alipay_login, render_logged_in_account
@@ -124,90 +123,62 @@ def get_global_lock():
124
 
125
  global_lock = get_global_lock()
126
 
127
- def generate_psd_corr_matrix(n_constructs, max_corr=0.5):
128
- if n_constructs == 1: return np.array([[1.0]])
129
- base_corr = np.random.uniform(0.2, max_corr - 0.1)
130
- A = np.random.uniform(base_corr - 0.1, base_corr + 0.1, (n_constructs, n_constructs))
131
- A = (A + A.T) / 2
132
- np.fill_diagonal(A, 1.0)
133
- min_eig = np.min(np.linalg.eigvals(A))
134
- if min_eig < 0: A -= (min_eig - 0.01) * np.eye(n_constructs)
135
- d = np.sqrt(np.diag(A))
136
- return A / np.outer(d, d)
137
-
138
- def calculate_validity_metrics(df, constructs_info, true_loadings):
139
- metrics = []
140
- for const in constructs_info:
141
- c_name = const['name']
142
- items = const['items']
143
- sub_df = df[items]
144
- var_total = sub_df.sum(axis=1).var(ddof=1)
145
- sum_var = sub_df.var(ddof=1).sum()
146
- n_items = len(items)
147
- alpha = 0.0 if var_total <= 0 else (n_items / (n_items - 1)) * (1 - sum_var / var_total)
148
- c_loadings = np.array(true_loadings[c_name])
149
- sum_loadings_sq = np.sum(c_loadings)**2
150
- sum_sq_loadings = np.sum(c_loadings**2)
151
- sum_error = np.sum(1 - c_loadings**2)
152
- cr = sum_loadings_sq / (sum_loadings_sq + sum_error)
153
- ave = sum_sq_loadings / len(c_loadings)
154
- metrics.append({"Construct": c_name, "Items": n_items, "Cronbach's α": round(alpha, 3), "CR": round(cr, 3), "AVE": round(ave, 3)})
155
- return pd.DataFrame(metrics)
156
-
157
- def generate_empirical_data_robust(construct_names=None, n=300, scale_points=7):
158
- if not construct_names or len(construct_names) < 2:
159
- construct_names = ["Independent_Var", "Mediator_Var", "Dependent_Var"]
160
-
161
- n_constructs = len(construct_names)
162
- corr = generate_psd_corr_matrix(n_constructs, max_corr=0.55)
163
- latent_scores = np.random.multivariate_normal(mean=[0]*n_constructs, cov=corr, size=n)
164
- data, true_loadings, constructs_info = {}, {}, []
165
- target_mean = scale_points / 2.0 + 0.5
166
- target_std = scale_points / 5.0
167
-
168
- for idx, c_name in enumerate(construct_names):
169
- true_score = latent_scores[:, idx]
170
- n_items = np.random.randint(3, 6)
171
- loadings = np.random.uniform(0.72, 0.88, n_items)
172
- true_loadings[c_name] = loadings
173
- item_names = []
174
- for i, load in enumerate(loadings):
175
- error = np.random.normal(0, np.sqrt(1 - load**2), n)
176
- item_score = load * true_score + error
177
- raw_scaled = item_score * target_std + target_mean
178
- item_int = np.clip(np.round(raw_scaled), 1, scale_points).astype(int)
179
- col_name = f"{c_name}_{i+1}"
180
- data[col_name] = item_int
181
- item_names.append(col_name)
182
- constructs_info.append({"name": c_name, "items": item_names})
183
-
184
- df = pd.DataFrame(data)
185
- validity_df = calculate_validity_metrics(df, constructs_info, true_loadings)
186
- mean_scores = pd.DataFrame({c['name']: df[c['items']].mean(axis=1) for c in constructs_info})
187
- fl_matrix = mean_scores.corr().astype(object)
188
-
189
- for i, row in validity_df.iterrows():
190
- c_name = row['Construct']
191
- sqrt_ave = np.sqrt(row['AVE'])
192
- fl_matrix.loc[c_name, c_name] = f"({sqrt_ave:.3f})"
193
-
194
- for i in range(len(fl_matrix.columns)):
195
- for j in range(i + 1, len(fl_matrix.columns)):
196
- fl_matrix.iloc[i, j] = ""
197
-
198
- desc_stats = mean_scores.mean().to_frame('Mean').join(mean_scores.std().to_frame('SD'))
199
- return df, validity_df, desc_stats.join(fl_matrix)
200
-
201
- def resolve_empirical_data(uploaded_files=None, language="zh"):
202
- if uploaded_files:
203
- user_md = extract_data_from_files(uploaded_files)
204
- if user_md:
205
- return None, user_md, tr(language, "actual_stats_parsed")
206
-
207
- df, validity_df, fl_matrix_df = generate_empirical_data_robust()
208
- simulated_md_prompt = f"""### Table 1: Reliability and Convergent Validity\n{validity_df.to_markdown(index=False)}\n### Table 2: Descriptive Statistics and Discriminant Validity (Fornell-Larcker Criterion)\n*Note: Diagonal elements in parentheses are the square roots of AVE.*\n{fl_matrix_df.to_markdown()}\n[STRICT EMPIRICAL INSTRUCTION]\n1. ZERO HALLUCINATION: You MUST report the exact metrics (Cronbach's α, CR, AVE, correlations) provided in the tables above.\n2. VALIDITY DEFENSE: Explicitly state that Convergent Validity is established because all AVEs > 0.5 and CRs > 0.7.\n3. DISCRIMINANT DEFENSE: Explicitly state that Discriminant Validity is satisfied per the Fornell-Larcker criterion, as the square root of AVE (diagonal values) exceed inter-construct correlations."""
209
- status_msg = tr(language, "synthetic_stats_generated")
210
- return df, simulated_md_prompt, status_msg
211
 
212
  def _translation_system_prompt(target_lang):
213
  common = """Translate the supplied academic manuscript faithfully. Preserve every citation tag, number, percentage, coefficient, p-value, confidence interval, DOI, table structure, heading level, hypothesis label, variable abbreviation, and conclusion direction. Do not add, remove, strengthen, or weaken empirical claims. Use concise, accurate, and rigorous academic language. Do not use metaphors, analogies, rhetorical questions, promotional language, formulaic transitions, or mechanical repetition. Output only the translated manuscript."""
@@ -221,7 +192,14 @@ def _translation_system_prompt(target_lang):
221
  def async_translate_task(agent, en_text, placeholder, target_lang, quarantine_flag, interface_lang):
222
  try:
223
  sys_p = _translation_system_prompt(target_lang)
224
- trans_text = agent.call_deepseek(sys_p, f"TEXT:\n{en_text}", max_tokens=6000, model_name="deepseek-chat")
 
 
 
 
 
 
 
225
  trans_resolved = agent.resolve_citations(trans_text, quarantine=quarantine_flag)
226
  placeholder.markdown(trans_resolved)
227
  return trans_resolved
@@ -604,6 +582,7 @@ if st.session_state.start_pipeline or st.session_state.resume_pipeline:
604
  SAFE_CHUNK_WORDS = agent.chunk_words_cyrillic if target_lang in ["Russian (Русский)", "Kazakh (Қазақша)"] else agent.chunk_words_default
605
 
606
  qual_engine = get_qualitative_engine(agent)
 
607
 
608
  try:
609
  update_heartbeat()
@@ -624,6 +603,7 @@ if st.session_state.start_pipeline or st.session_state.resume_pipeline:
624
  words = state["words"]
625
  user_data_md = state["user_data_md"]
626
  user_lit_context = state["user_lit_context"]
 
627
 
628
  agent.load_state(state["agent_state"])
629
  adaptive_structure = state["adaptive_structure"]
@@ -647,6 +627,7 @@ if st.session_state.start_pipeline or st.session_state.resume_pipeline:
647
  pb = progress_bar.progress(1, text=tr(lang_code, "pipeline_start"))
648
  user_lit_context = ""
649
  user_data_md = ""
 
650
 
651
  if st.session_state.run_mode == "amj":
652
  if uploaded_papers_amj:
@@ -669,7 +650,12 @@ if st.session_state.start_pipeline or st.session_state.resume_pipeline:
669
 
670
  with st.spinner(tr(lang_code, "extract_quant")):
671
  update_heartbeat()
672
- _, user_data_md, msg = resolve_empirical_data(uploaded_data_amj, lang_code)
 
 
 
 
 
673
  st.info(msg)
674
 
675
  elif st.session_state.run_mode == "asq":
@@ -693,14 +679,21 @@ if st.session_state.start_pipeline or st.session_state.resume_pipeline:
693
 
694
  update_heartbeat()
695
  raw_interviews = extract_text_from_files([uploaded_interviews]) if uploaded_interviews else ""
696
- user_data_md, msg = qual_engine.resolve_qualitative_data(topic, keywords, raw_interviews)
 
 
 
 
 
 
697
  st.info(tr(lang_code, "qualitative_ready"))
698
 
699
  state = {
700
  "topic": topic, "keywords": keywords, "target_lang": target_lang, "words": words,
701
  "user_data_md": user_data_md, "user_lit_context": user_lit_context,
702
  "completed_sections": [], "full_draft_en": "", "full_draft_translated": "",
703
- "context_tail": "", "run_mode": st.session_state.run_mode, "interface_language": lang_code
 
704
  }
705
 
706
  pb.progress(5, text=tr(lang_code, "config_route"))
@@ -755,7 +748,18 @@ if st.session_state.start_pipeline or st.session_state.resume_pipeline:
755
  pb.progress(14, text=tr(lang_code, "anchor_progress"))
756
  with st.status(tr(lang_code, "anchor_status"), expanded=True) as status:
757
  ph = st.empty()
758
- anchor_response = ph.write_stream(stream_with_heartbeat(agent.stream_generate_anchors(topic, keywords, synthesis_result, refs_text)))
 
 
 
 
 
 
 
 
 
 
 
759
  status.update(label=tr(lang_code, "anchor_done"), state="complete", expanded=False)
760
 
761
  # ==========================================================
@@ -767,6 +771,8 @@ if st.session_state.start_pipeline or st.session_state.resume_pipeline:
767
  conclusion_match = re.search(r'-+\s*CONCLUSION\s*-+\s*(.*?)$', anchor_response_clean, re.DOTALL | re.IGNORECASE)
768
  abstract_anchor = abstract_match.group(1).strip() if abstract_match else anchor_response_clean.strip()
769
  conclusion_anchor = conclusion_match.group(1).strip() if conclusion_match else ""
 
 
770
  # ==========================================================
771
 
772
  pb.progress(15, text=tr(lang_code, "outline_progress"))
@@ -776,7 +782,14 @@ if st.session_state.start_pipeline or st.session_state.resume_pipeline:
776
  structure = plan.get("structure", [])
777
 
778
  en_abstract_clean = clean_markdown_headings(re.sub(r'(?i)^abstract[:\s]*', '', str(abstract_anchor)).strip())
779
- trans_abstract_clean = clean_markdown_headings(re.sub(r'(?i)^(摘要|abstract|аннотация|түйіндеме)[:\s]*', '', agent.call_deepseek(_translation_system_prompt(target_lang), abstract_anchor, max_tokens=3000)).strip())
 
 
 
 
 
 
 
780
 
781
  loc_abs = {"Simplified Chinese (简体中文)": "摘要", "Russian (Русский)": "Аннотация", "Kazakh (Қазақша)": "Түйіндеме"}.get(target_lang, "Abstract")
782
  loc_kw = {"Simplified Chinese (简体中文)": "关键词", "Russian (Русский)": "Ключевые слова", "Kazakh (Қазақша)": "Кілт сөздер"}.get(target_lang, "Keywords")
@@ -992,11 +1005,18 @@ if st.session_state.start_pipeline or st.session_state.resume_pipeline:
992
  update_heartbeat()
993
  st.write(tr(lang_code, "api_requested"))
994
 
995
- target_model = "deepseek-v4-flash"
996
  st.write(tr(lang_code, "model_writing", model=target_model))
997
 
998
  en_draft_clean = re.sub(r'<think>.*?</think>', '', en_ph.write_stream(
999
- stream_with_heartbeat(agent.stream_deepseek(sys_prompt_m3, base_user, max_tokens=6000, model_name=target_model, thinking=False))
 
 
 
 
 
 
 
1000
  ), flags=re.DOTALL).strip()
1001
 
1002
  status.update(label=tr(lang_code, "draft_done", name=display_name), state="complete", expanded=False)
@@ -1101,7 +1121,14 @@ if st.session_state.start_pipeline or st.session_state.resume_pipeline:
1101
  if total_words_en > upper_limit:
1102
  conclusion_match = re.search(r'(#+\s*Conclusion.*?)(?=\n#+ |\Z)', full_draft_en, re.DOTALL | re.IGNORECASE)
1103
  if conclusion_match:
1104
- compressed_en = agent.call_deepseek("Condense the following conclusion by 15-20% preserving citations.", conclusion_match.group(1), max_tokens=1500, model_name="deepseek-chat")
 
 
 
 
 
 
 
1105
  if compressed_en:
1106
  compressed_en = agent.resolve_citations(compressed_en, quarantine=True)
1107
  full_draft_en = full_draft_en.replace(conclusion_match.group(1), compressed_en.strip())
@@ -1114,7 +1141,14 @@ if st.session_state.start_pipeline or st.session_state.resume_pipeline:
1114
  else:
1115
  full_draft_translated += "\n\n" + compressed_trans.strip()
1116
  elif total_words_en < lower_limit:
1117
- expanded_en = agent.call_deepseek("Continue conclusion by adding 150 words (future research).", full_draft_en[-2000:], max_tokens=800, model_name="deepseek-chat")
 
 
 
 
 
 
 
1118
  if expanded_en:
1119
  expanded_en = agent.resolve_citations(expanded_en, quarantine=True)
1120
  full_draft_en += "\n\n" + expanded_en.strip()
@@ -1136,6 +1170,13 @@ if st.session_state.start_pipeline or st.session_state.resume_pipeline:
1136
  with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zf:
1137
  zf.writestr(f"Research_EN_{bj_time}.docx", text_to_docx(full_draft_en).getvalue())
1138
  zf.writestr(f"Research_{output_suffix}_{bj_time}.docx", text_to_docx(full_draft_translated).getvalue())
 
 
 
 
 
 
 
1139
  zip_buffer.seek(0)
1140
  st.session_state.zip_buffer = zip_buffer.getvalue()
1141
  st.session_state.zip_filename = f"Draft_{bj_time}.zip"
 
3
  from core.meta_extractor import extract_golden_metadata
4
  from core.rag_engine import initialize_rag
5
  from core.qualitative_engine import get_qualitative_engine
6
+ from core.quantitative_engine import get_quantitative_engine
7
  import streamlit as st
8
 
9
  st.set_page_config(
 
25
  import math
26
  import json
27
  from datetime import datetime, timezone, timedelta
 
 
28
  import threading
29
  from concurrent.futures import ThreadPoolExecutor
30
  from streamlit.runtime.scriptrunner import add_script_run_ctx, get_script_run_ctx
31
 
32
  from config import APP_CONFIG, AMJ_PROMPTS, ASQ_PROMPTS, GENERIC_PROMPTS
33
+ from utils.file_parser import extract_text_from_files
34
  from utils.docx_builder import text_to_docx
35
  from core.pipeline_agent import SSciPipelineAgent
36
  from managed_auth import require_alipay_login, render_logged_in_account
 
123
 
124
  global_lock = get_global_lock()
125
 
126
+ def build_generation_disclosure(run_mode, data_metadata):
127
+ metadata = data_metadata or {}
128
+ source_type = metadata.get("source_type", "unknown")
129
+ generated = source_type == "system_generated"
130
+ mode_label = "quantitative" if run_mode == "amj" else "qualitative"
131
+
132
+ lines = [
133
+ "SmartPaper Generation Disclosure / SmartPaper 生成说明",
134
+ "",
135
+ f"Research mode / 研究模式: {mode_label}",
136
+ f"Evidence source / 证据来源: {source_type}",
137
+ "",
138
+ ]
139
+ if generated:
140
+ lines.extend(
141
+ [
142
+ "The statistical results or qualitative materials used by "
143
+ "this draft were generated by the system because no "
144
+ "complete user-supplied evidence was available.",
145
+ "本初稿使用的统计结果或定性材料由系统生成,原因是用户未提供完整的相应证据材料。",
146
+ "",
147
+ "These materials support drafting and methodological "
148
+ "demonstration only. They must not be represented as "
149
+ "observed, collected, or independently verified real data.",
150
+ "这些材料仅用于论文起草和方法展示,不得被表述为已经观察、收集或独立核验的真实数据。",
151
+ "",
152
+ "The generated evidence is bounded by the configured "
153
+ "parameters and prompts. Users must replace or validate it "
154
+ "before research submission, publication, policy use, or "
155
+ "other consequential use.",
156
+ "生成证据受系统参数和提示词约束。用于投稿、发表、政策或其他重要用途前,用户必须替换或核验。",
157
+ ]
158
+ )
159
+ else:
160
+ lines.extend(
161
+ [
162
+ "The draft used evidence material supplied by the user. "
163
+ "SmartPaper did not independently verify its accuracy, "
164
+ "completeness, consent status, or provenance.",
165
+ "本初稿使用了用户提供的证据材料。SmartPaper未独立核验其准确性、完整性、授权状态或来源。",
166
+ ]
167
+ )
168
+
169
+ method = metadata.get("method")
170
+ if method:
171
+ lines.extend(["", f"Generation method / 生成方法: {method}"])
172
+ facts = metadata.get("study_facts") or {}
173
+ if facts:
174
+ lines.extend(
175
+ [
176
+ "",
177
+ "Frozen qualitative study facts / 已冻结的定性研究事实:",
178
+ json.dumps(facts, ensure_ascii=False, indent=2),
179
+ ]
180
+ )
181
+ return "\n".join(lines).strip() + "\n"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
182
 
183
  def _translation_system_prompt(target_lang):
184
  common = """Translate the supplied academic manuscript faithfully. Preserve every citation tag, number, percentage, coefficient, p-value, confidence interval, DOI, table structure, heading level, hypothesis label, variable abbreviation, and conclusion direction. Do not add, remove, strengthen, or weaken empirical claims. Use concise, accurate, and rigorous academic language. Do not use metaphors, analogies, rhetorical questions, promotional language, formulaic transitions, or mechanical repetition. Output only the translated manuscript."""
 
192
  def async_translate_task(agent, en_text, placeholder, target_lang, quarantine_flag, interface_lang):
193
  try:
194
  sys_p = _translation_system_prompt(target_lang)
195
+ trans_text = agent.call_deepseek(
196
+ sys_p,
197
+ f"TEXT:\n{en_text}",
198
+ max_tokens=6000,
199
+ model_role="translation",
200
+ thinking=False,
201
+ allow_fallback=False,
202
+ )
203
  trans_resolved = agent.resolve_citations(trans_text, quarantine=quarantine_flag)
204
  placeholder.markdown(trans_resolved)
205
  return trans_resolved
 
582
  SAFE_CHUNK_WORDS = agent.chunk_words_cyrillic if target_lang in ["Russian (Русский)", "Kazakh (Қазақша)"] else agent.chunk_words_default
583
 
584
  qual_engine = get_qualitative_engine(agent)
585
+ quant_engine = get_quantitative_engine()
586
 
587
  try:
588
  update_heartbeat()
 
603
  words = state["words"]
604
  user_data_md = state["user_data_md"]
605
  user_lit_context = state["user_lit_context"]
606
+ data_metadata = state.get("data_metadata", {})
607
 
608
  agent.load_state(state["agent_state"])
609
  adaptive_structure = state["adaptive_structure"]
 
627
  pb = progress_bar.progress(1, text=tr(lang_code, "pipeline_start"))
628
  user_lit_context = ""
629
  user_data_md = ""
630
+ data_metadata = {}
631
 
632
  if st.session_state.run_mode == "amj":
633
  if uploaded_papers_amj:
 
650
 
651
  with st.spinner(tr(lang_code, "extract_quant")):
652
  update_heartbeat()
653
+ _, user_data_md, msg, data_metadata = (
654
+ quant_engine.resolve_empirical_data(
655
+ uploaded_data_amj,
656
+ lang_code,
657
+ )
658
+ )
659
  st.info(msg)
660
 
661
  elif st.session_state.run_mode == "asq":
 
679
 
680
  update_heartbeat()
681
  raw_interviews = extract_text_from_files([uploaded_interviews]) if uploaded_interviews else ""
682
+ user_data_md, msg, data_metadata = (
683
+ qual_engine.resolve_qualitative_data(
684
+ topic,
685
+ keywords,
686
+ raw_interviews,
687
+ )
688
+ )
689
  st.info(tr(lang_code, "qualitative_ready"))
690
 
691
  state = {
692
  "topic": topic, "keywords": keywords, "target_lang": target_lang, "words": words,
693
  "user_data_md": user_data_md, "user_lit_context": user_lit_context,
694
  "completed_sections": [], "full_draft_en": "", "full_draft_translated": "",
695
+ "context_tail": "", "run_mode": st.session_state.run_mode, "interface_language": lang_code,
696
+ "data_metadata": data_metadata
697
  }
698
 
699
  pb.progress(5, text=tr(lang_code, "config_route"))
 
748
  pb.progress(14, text=tr(lang_code, "anchor_progress"))
749
  with st.status(tr(lang_code, "anchor_status"), expanded=True) as status:
750
  ph = st.empty()
751
+ anchor_response = ph.write_stream(
752
+ stream_with_heartbeat(
753
+ agent.stream_generate_anchors(
754
+ topic,
755
+ keywords,
756
+ synthesis_result,
757
+ refs_text,
758
+ data_evidence=user_data_md,
759
+ mode=st.session_state.run_mode,
760
+ )
761
+ )
762
+ )
763
  status.update(label=tr(lang_code, "anchor_done"), state="complete", expanded=False)
764
 
765
  # ==========================================================
 
771
  conclusion_match = re.search(r'-+\s*CONCLUSION\s*-+\s*(.*?)$', anchor_response_clean, re.DOTALL | re.IGNORECASE)
772
  abstract_anchor = abstract_match.group(1).strip() if abstract_match else anchor_response_clean.strip()
773
  conclusion_anchor = conclusion_match.group(1).strip() if conclusion_match else ""
774
+ agent.global_memory["abstract_anchor"] = abstract_anchor
775
+ agent.global_memory["conclusion_anchor"] = conclusion_anchor
776
  # ==========================================================
777
 
778
  pb.progress(15, text=tr(lang_code, "outline_progress"))
 
782
  structure = plan.get("structure", [])
783
 
784
  en_abstract_clean = clean_markdown_headings(re.sub(r'(?i)^abstract[:\s]*', '', str(abstract_anchor)).strip())
785
+ trans_abstract_clean = clean_markdown_headings(re.sub(r'(?i)^(摘要|abstract|аннотация|түйіндеме)[:\s]*', '', agent.call_deepseek(
786
+ _translation_system_prompt(target_lang),
787
+ abstract_anchor,
788
+ max_tokens=3000,
789
+ model_role="translation",
790
+ thinking=False,
791
+ allow_fallback=False,
792
+ )).strip())
793
 
794
  loc_abs = {"Simplified Chinese (简体中文)": "摘要", "Russian (Русский)": "Аннотация", "Kazakh (Қазақша)": "Түйіндеме"}.get(target_lang, "Abstract")
795
  loc_kw = {"Simplified Chinese (简体中文)": "关键词", "Russian (Русский)": "Ключевые слова", "Kazakh (Қазақша)": "Кілт сөздер"}.get(target_lang, "Keywords")
 
1005
  update_heartbeat()
1006
  st.write(tr(lang_code, "api_requested"))
1007
 
1008
+ target_model = agent.model_for("writing")
1009
  st.write(tr(lang_code, "model_writing", model=target_model))
1010
 
1011
  en_draft_clean = re.sub(r'<think>.*?</think>', '', en_ph.write_stream(
1012
+ stream_with_heartbeat(agent.stream_deepseek(
1013
+ sys_prompt_m3,
1014
+ base_user,
1015
+ max_tokens=6000,
1016
+ model_role="writing",
1017
+ thinking=False,
1018
+ allow_fallback=False,
1019
+ ))
1020
  ), flags=re.DOTALL).strip()
1021
 
1022
  status.update(label=tr(lang_code, "draft_done", name=display_name), state="complete", expanded=False)
 
1121
  if total_words_en > upper_limit:
1122
  conclusion_match = re.search(r'(#+\s*Conclusion.*?)(?=\n#+ |\Z)', full_draft_en, re.DOTALL | re.IGNORECASE)
1123
  if conclusion_match:
1124
+ compressed_en = agent.call_deepseek(
1125
+ "Condense the following conclusion by 15-20% preserving citations.",
1126
+ conclusion_match.group(1),
1127
+ max_tokens=1500,
1128
+ model_role="writing",
1129
+ thinking=False,
1130
+ allow_fallback=False,
1131
+ )
1132
  if compressed_en:
1133
  compressed_en = agent.resolve_citations(compressed_en, quarantine=True)
1134
  full_draft_en = full_draft_en.replace(conclusion_match.group(1), compressed_en.strip())
 
1141
  else:
1142
  full_draft_translated += "\n\n" + compressed_trans.strip()
1143
  elif total_words_en < lower_limit:
1144
+ expanded_en = agent.call_deepseek(
1145
+ "Continue conclusion by adding 150 words (future research).",
1146
+ full_draft_en[-2000:],
1147
+ max_tokens=800,
1148
+ model_role="writing",
1149
+ thinking=False,
1150
+ allow_fallback=False,
1151
+ )
1152
  if expanded_en:
1153
  expanded_en = agent.resolve_citations(expanded_en, quarantine=True)
1154
  full_draft_en += "\n\n" + expanded_en.strip()
 
1170
  with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zf:
1171
  zf.writestr(f"Research_EN_{bj_time}.docx", text_to_docx(full_draft_en).getvalue())
1172
  zf.writestr(f"Research_{output_suffix}_{bj_time}.docx", text_to_docx(full_draft_translated).getvalue())
1173
+ zf.writestr(
1174
+ "GENERATION_DISCLOSURE.txt",
1175
+ build_generation_disclosure(
1176
+ st.session_state.run_mode,
1177
+ data_metadata,
1178
+ ),
1179
+ )
1180
  zip_buffer.seek(0)
1181
  st.session_state.zip_buffer = zip_buffer.getvalue()
1182
  st.session_state.zip_filename = f"Draft_{bj_time}.zip"
config.py CHANGED
@@ -23,32 +23,65 @@ def load_config():
23
 
24
  # --- API 节点配置 ---
25
  "deepseek_api_url": os.environ.get(
26
- "DEEPSEEK_API_URL",
27
- "https://api.deepseek.com/chat/completions",
28
- ),
29
- "deepseek_default_model": os.environ.get(
30
- "DEEPSEEK_DEFAULT_MODEL",
31
- "deepseek-v4-flash",
32
- ),
33
- "deepseek_fallback_model": os.environ.get(
34
- "DEEPSEEK_FALLBACK_MODEL",
35
- "deepseek-v4-flash",
36
- ),
37
- "deepseek_default_thinking": os.environ.get(
38
- "DEEPSEEK_DEFAULT_THINKING",
39
- "true",
40
- ).strip().lower() in {"1", "true", "yes", "on"},
41
- "deepseek_reasoning_effort": os.environ.get(
42
- "DEEPSEEK_REASONING_EFFORT",
43
- "high",
44
- ),
45
- "deepseek_max_retries": int(
46
- os.environ.get("DEEPSEEK_MAX_RETRIES", "3")
47
- ),
48
- "deepseek_show_reasoning": os.environ.get(
49
- "DEEPSEEK_SHOW_REASONING",
50
- "false",
51
- ).strip().lower() in {"1", "true", "yes", "on"},
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  "openalex_api_url": os.environ.get("OPENALEX_API_URL", "https://api.openalex.org/works"),
53
  "s2_api_url": os.environ.get("S2_API_URL", "https://api.semanticscholar.org/graph/v1"),
54
 
 
23
 
24
  # --- API 节点配置 ---
25
  "deepseek_api_url": os.environ.get(
26
+ "DEEPSEEK_API_URL",
27
+ "https://api.deepseek.com/chat/completions",
28
+ ),
29
+ # Model names are centralized here and can be upgraded in HF Variables.
30
+ # Application and engine code select a task role, never a concrete model.
31
+ "deepseek_models": {
32
+ "reasoning": os.environ.get(
33
+ "DEEPSEEK_MODEL_REASONING",
34
+ "deepseek-v4-pro",
35
+ ),
36
+ "writing": os.environ.get(
37
+ "DEEPSEEK_MODEL_WRITING",
38
+ "deepseek-v4-pro",
39
+ ),
40
+ "translation": os.environ.get(
41
+ "DEEPSEEK_MODEL_TRANSLATION",
42
+ "deepseek-v4-pro",
43
+ ),
44
+ "fast": os.environ.get(
45
+ "DEEPSEEK_MODEL_FAST",
46
+ "deepseek-v4-flash",
47
+ ),
48
+ "fallback": os.environ.get(
49
+ "DEEPSEEK_MODEL_FALLBACK",
50
+ "deepseek-v4-flash",
51
+ ),
52
+ },
53
+ "deepseek_task_settings": {
54
+ "reasoning": {
55
+ "thinking": True,
56
+ "reasoning_effort": os.environ.get(
57
+ "DEEPSEEK_REASONING_EFFORT",
58
+ "high",
59
+ ),
60
+ "allow_fallback": False,
61
+ },
62
+ "writing": {
63
+ "thinking": False,
64
+ "reasoning_effort": "high",
65
+ "allow_fallback": False,
66
+ },
67
+ "translation": {
68
+ "thinking": False,
69
+ "reasoning_effort": "high",
70
+ "allow_fallback": False,
71
+ },
72
+ "fast": {
73
+ "thinking": False,
74
+ "reasoning_effort": "low",
75
+ "allow_fallback": True,
76
+ },
77
+ },
78
+ "deepseek_max_retries": int(
79
+ os.environ.get("DEEPSEEK_MAX_RETRIES", "3")
80
+ ),
81
+ "deepseek_show_reasoning": os.environ.get(
82
+ "DEEPSEEK_SHOW_REASONING",
83
+ "false",
84
+ ).strip().lower() in {"1", "true", "yes", "on"},
85
  "openalex_api_url": os.environ.get("OPENALEX_API_URL", "https://api.openalex.org/works"),
86
  "s2_api_url": os.environ.get("S2_API_URL", "https://api.semanticscholar.org/graph/v1"),
87