Spaces:
Running
Running
Update core/pipeline_agent.py
Browse files- core/pipeline_agent.py +53 -45
core/pipeline_agent.py
CHANGED
|
@@ -14,13 +14,9 @@ import logging
|
|
| 14 |
import concurrent.futures
|
| 15 |
from tenacity import retry, stop_after_attempt, wait_random_exponential, retry_if_exception_type
|
| 16 |
|
| 17 |
-
# 基础日志配置
|
| 18 |
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s", handlers=[logging.StreamHandler(sys.stdout)])
|
| 19 |
|
| 20 |
-
# 辅助函数:专门用于向 Hugging Face 容器输出强制刷新的彩色/醒目日志
|
| 21 |
-
def hf_log(msg):
|
| 22 |
-
print(f"[HF Container Log] {msg}", flush=True)
|
| 23 |
-
|
| 24 |
try:
|
| 25 |
from langdetect import detect
|
| 26 |
LANGDETECT_AVAILABLE = True
|
|
@@ -74,7 +70,7 @@ class SSciPipelineAgent:
|
|
| 74 |
except:
|
| 75 |
self.tokenizer = tiktoken.get_encoding("gpt2")
|
| 76 |
|
| 77 |
-
|
| 78 |
|
| 79 |
def _extract_json_from_text(self, text):
|
| 80 |
if not text:
|
|
@@ -100,7 +96,7 @@ class SSciPipelineAgent:
|
|
| 100 |
else:
|
| 101 |
return json.loads(text[start_list:end_list+1])
|
| 102 |
except Exception as e:
|
| 103 |
-
|
| 104 |
return None
|
| 105 |
|
| 106 |
def export_state(self):
|
|
@@ -124,7 +120,7 @@ class SSciPipelineAgent:
|
|
| 124 |
self.citation_map = state_dict.get("citation_map", {})
|
| 125 |
self.used_citations = set(state_dict.get("used_citations", []))
|
| 126 |
self.global_memory = state_dict.get("global_memory", self.global_memory)
|
| 127 |
-
|
| 128 |
|
| 129 |
def _calc_paragraph_count(self, target_words, lang_hint="en"):
|
| 130 |
words_per_para = 100 if lang_hint in ["ru", "kk"] else 130
|
|
@@ -143,7 +139,7 @@ class SSciPipelineAgent:
|
|
| 143 |
resp = requests.get(url, headers=headers, params=params, timeout=timeout)
|
| 144 |
|
| 145 |
if resp.status_code == 429:
|
| 146 |
-
|
| 147 |
time.sleep(random.uniform(2.0, 5.0))
|
| 148 |
resp.raise_for_status()
|
| 149 |
|
|
@@ -151,7 +147,7 @@ class SSciPipelineAgent:
|
|
| 151 |
return resp
|
| 152 |
|
| 153 |
if resp.status_code != 200:
|
| 154 |
-
|
| 155 |
resp.raise_for_status()
|
| 156 |
return resp
|
| 157 |
|
|
@@ -174,13 +170,13 @@ class SSciPipelineAgent:
|
|
| 174 |
hypo_matches = re.findall(r'(H\d+[:\s].*?)(?=\n|$)', text, re.IGNORECASE)
|
| 175 |
if hypo_matches:
|
| 176 |
self.global_memory["hypotheses"] = "\n".join(hypo_matches[:10])
|
| 177 |
-
|
| 178 |
|
| 179 |
if any(kw in lower_sec for kw in ["method", "measure", "variable"]):
|
| 180 |
var_matches = re.findall(r'([A-Z][a-z]+(?:\s+[A-Z][a-z]+)*)\s+(?:was|is|refers to)\s+([^\.]+\.)', text)
|
| 181 |
if var_matches:
|
| 182 |
self.global_memory["variables"] = "\n".join([f"- {v[0]}: {v[1][:100]}" for v in var_matches[:8]])
|
| 183 |
-
|
| 184 |
|
| 185 |
if "model" in lower_sec or "figure" in lower_sec:
|
| 186 |
model_match = re.search(r'(Figure\s*\d+[^\.]+\.)', text, re.IGNORECASE)
|
|
@@ -261,7 +257,7 @@ class SSciPipelineAgent:
|
|
| 261 |
|
| 262 |
def call_deepseek(self, system_prompt, user_prompt, max_tokens=8000, temp=0.3, model_name="deepseek-reasoner"):
|
| 263 |
tokens_est = self._count_tokens(user_prompt)
|
| 264 |
-
|
| 265 |
|
| 266 |
for attempt in range(3):
|
| 267 |
with self.lock:
|
|
@@ -270,7 +266,7 @@ class SSciPipelineAgent:
|
|
| 270 |
if attempt == 2 and model_name == "deepseek-reasoner":
|
| 271 |
current_model = "deepseek-chat"
|
| 272 |
temp = 0.3
|
| 273 |
-
|
| 274 |
|
| 275 |
payload = {
|
| 276 |
"model": current_model,
|
|
@@ -286,7 +282,7 @@ class SSciPipelineAgent:
|
|
| 286 |
try:
|
| 287 |
with requests.post(self.deepseek_api_url, headers={"Authorization": f"Bearer {self.ds_key}"}, json=payload, stream=True, timeout=self.timeouts["standard"]) as response:
|
| 288 |
if response.status_code != 200:
|
| 289 |
-
|
| 290 |
if attempt == 2:
|
| 291 |
raise requests.exceptions.HTTPError("API 熔断")
|
| 292 |
time.sleep(3 ** attempt)
|
|
@@ -303,17 +299,17 @@ class SSciPipelineAgent:
|
|
| 303 |
pass
|
| 304 |
|
| 305 |
elapsed = time.time() - start_time
|
| 306 |
-
|
| 307 |
return full_content.strip()
|
| 308 |
except Exception as e:
|
| 309 |
-
|
| 310 |
if attempt == 2:
|
| 311 |
raise e
|
| 312 |
time.sleep(3 ** attempt)
|
| 313 |
|
| 314 |
def stream_deepseek(self, system_prompt, user_prompt, max_tokens=8000, model_name="deepseek-reasoner"):
|
| 315 |
tokens_est = self._count_tokens(user_prompt)
|
| 316 |
-
|
| 317 |
|
| 318 |
temp = 0.3 if model_name == "deepseek-chat" else 0.2
|
| 319 |
for attempt in range(3):
|
|
@@ -323,7 +319,7 @@ class SSciPipelineAgent:
|
|
| 323 |
if attempt >= 1 and model_name == "deepseek-reasoner":
|
| 324 |
current_model = "deepseek-chat"
|
| 325 |
temp = 0.3
|
| 326 |
-
|
| 327 |
|
| 328 |
payload = {
|
| 329 |
"model": current_model,
|
|
@@ -334,14 +330,13 @@ class SSciPipelineAgent:
|
|
| 334 |
if current_model != "deepseek-reasoner":
|
| 335 |
payload["temperature"] = temp
|
| 336 |
|
| 337 |
-
think_heartbeat = 0
|
| 338 |
has_started_reasoning = False
|
| 339 |
start_time = time.time()
|
| 340 |
|
| 341 |
try:
|
| 342 |
with requests.post(self.deepseek_api_url, headers={"Authorization": f"Bearer {self.ds_key}"}, json=payload, stream=True, timeout=self.timeouts["stream"]) as response:
|
| 343 |
if response.status_code != 200:
|
| 344 |
-
|
| 345 |
if attempt == 2:
|
| 346 |
response.raise_for_status()
|
| 347 |
time.sleep(2)
|
|
@@ -359,14 +354,13 @@ class SSciPipelineAgent:
|
|
| 359 |
if not has_started_reasoning:
|
| 360 |
yield "\n<think>\n"
|
| 361 |
has_started_reasoning = True
|
| 362 |
-
|
| 363 |
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
#
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
hf_log(f"⏳ [DeepSeek 心跳] {current_model} 思考中... ({elapsed:.1f}秒, 接收 {think_heartbeat} 块)")
|
| 370 |
|
| 371 |
yield reasoning_content
|
| 372 |
continue
|
|
@@ -375,19 +369,34 @@ class SSciPipelineAgent:
|
|
| 375 |
yield "\n</think>\n\n"
|
| 376 |
has_started_reasoning = False
|
| 377 |
elapsed = time.time() - start_time
|
| 378 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 379 |
|
| 380 |
if content:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 381 |
yield content
|
| 382 |
|
| 383 |
except Exception:
|
| 384 |
pass
|
| 385 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 386 |
return
|
| 387 |
except GeneratorExit:
|
| 388 |
raise
|
| 389 |
except Exception as e:
|
| 390 |
-
|
|
|
|
|
|
|
|
|
|
| 391 |
if attempt == 2:
|
| 392 |
raise
|
| 393 |
time.sleep(2)
|
|
@@ -425,7 +434,7 @@ class SSciPipelineAgent:
|
|
| 425 |
|
| 426 |
for attempt in range(2):
|
| 427 |
try:
|
| 428 |
-
|
| 429 |
response = self.call_deepseek(sys_prompt, user_prompt, temp=0.1, model_name="deepseek-chat")
|
| 430 |
papers = self._extract_json_from_text(response)
|
| 431 |
if not papers:
|
|
@@ -467,7 +476,7 @@ class SSciPipelineAgent:
|
|
| 467 |
self.used_citations.add(cite_tag)
|
| 468 |
|
| 469 |
rag_context.append(f"{cite_tag} [USER CORE PAPER] Title: {title}. Core Thesis: {core_thesis}. Abstract: {abstract}")
|
| 470 |
-
|
| 471 |
return "\n".join(rag_context)
|
| 472 |
except Exception as e:
|
| 473 |
time.sleep(2)
|
|
@@ -514,7 +523,6 @@ You MUST output your response EXACTLY using the following two delimiters. Do not
|
|
| 514 |
raise ValueError("Outline Generation Failed.")
|
| 515 |
|
| 516 |
def prefetch_jit_literature(self, topic, keywords, structure):
|
| 517 |
-
"""Pre-fetch JIT: 全局并发预抓取所有方法与结果章节所需支撑文献"""
|
| 518 |
jit_rag_texts = []
|
| 519 |
queries_to_fetch = set()
|
| 520 |
|
|
@@ -529,7 +537,7 @@ You MUST output your response EXACTLY using the following two delimiters. Do not
|
|
| 529 |
if not queries_to_fetch:
|
| 530 |
return ""
|
| 531 |
|
| 532 |
-
|
| 533 |
|
| 534 |
with concurrent.futures.ThreadPoolExecutor(max_workers=self.lit_max_workers) as executor:
|
| 535 |
futures = []
|
|
@@ -546,7 +554,7 @@ You MUST output your response EXACTLY using the following two delimiters. Do not
|
|
| 546 |
self.citation_map[cite_tag] = {"in_text": res["in_text"], "ref": res["ref"], "id": res["id"]}
|
| 547 |
jit_rag_texts.append(f"{cite_tag} {res['rag_text']}")
|
| 548 |
|
| 549 |
-
|
| 550 |
return "\n".join(jit_rag_texts)
|
| 551 |
|
| 552 |
def generate_search_queries(self, topic, keywords):
|
|
@@ -567,7 +575,7 @@ You MUST output your response EXACTLY using the following two delimiters. Do not
|
|
| 567 |
time.sleep(random.uniform(0.5, 1.5))
|
| 568 |
headers = {"x-api-key": str(self.s2_key).strip()} if self.s2_key else {}
|
| 569 |
results = []
|
| 570 |
-
|
| 571 |
try:
|
| 572 |
resp = self._requests_get_with_retry(
|
| 573 |
f"{self.s2_api_url}/paper/search",
|
|
@@ -576,7 +584,7 @@ You MUST output your response EXACTLY using the following two delimiters. Do not
|
|
| 576 |
)
|
| 577 |
if resp.status_code == 400: return results
|
| 578 |
papers = resp.json().get("data", [])
|
| 579 |
-
|
| 580 |
for paper in papers:
|
| 581 |
title = paper.get("title")
|
| 582 |
year = paper.get("year")
|
|
@@ -617,7 +625,7 @@ You MUST output your response EXACTLY using the following two delimiters. Do not
|
|
| 617 |
"source": "s2"
|
| 618 |
})
|
| 619 |
except Exception as e:
|
| 620 |
-
|
| 621 |
return results
|
| 622 |
|
| 623 |
def _oa_worker(self, term, page, filter_str):
|
|
@@ -625,7 +633,7 @@ You MUST output your response EXACTLY using the following two delimiters. Do not
|
|
| 625 |
headers = {"User-Agent": f"mailto:ssci_researcher_{random.randint(10000, 99999)}@gmail.com"}
|
| 626 |
if self.openalex_key: headers["Authorization"] = f"Bearer {self.openalex_key}"
|
| 627 |
results = []
|
| 628 |
-
|
| 629 |
try:
|
| 630 |
resp = self._requests_get_with_retry(
|
| 631 |
self.openalex_api_url,
|
|
@@ -633,7 +641,7 @@ You MUST output your response EXACTLY using the following two delimiters. Do not
|
|
| 633 |
params={"search": term, "per-page": self.lit_per_page, "page": page, "filter": filter_str}
|
| 634 |
)
|
| 635 |
raw_items = resp.json().get("results", [])
|
| 636 |
-
|
| 637 |
for item in raw_items:
|
| 638 |
title = item.get("title")
|
| 639 |
if not title or not self._is_english_dominant(title): continue
|
|
@@ -676,7 +684,7 @@ You MUST output your response EXACTLY using the following two delimiters. Do not
|
|
| 676 |
"source": "oa"
|
| 677 |
})
|
| 678 |
except Exception as e:
|
| 679 |
-
|
| 680 |
return results
|
| 681 |
|
| 682 |
def fetch_references_parallel(self, topic, keywords, min_required=50, progress_callback=None):
|
|
@@ -693,7 +701,7 @@ You MUST output your response EXACTLY using the following two delimiters. Do not
|
|
| 693 |
if expansion_phase == 0:
|
| 694 |
search_terms = self.generate_search_queries(topic, keywords)
|
| 695 |
else:
|
| 696 |
-
|
| 697 |
sys_prompt = "You are an expert academic librarian. We must broaden the search scope."
|
| 698 |
user_prompt = f"Topic: {topic}\nKeywords: {keywords}\nUsed Queries: {list(all_used_terms)}\nGenerate 3 NEW, BROADER English search queries. Return ONLY a JSON array of strings."
|
| 699 |
response = self.call_deepseek(sys_prompt, user_prompt, temp=0.5, model_name="deepseek-chat")
|
|
@@ -707,7 +715,7 @@ You MUST output your response EXACTLY using the following two delimiters. Do not
|
|
| 707 |
if not current_batch_terms: continue
|
| 708 |
for t in current_batch_terms: all_used_terms.add(t)
|
| 709 |
|
| 710 |
-
|
| 711 |
|
| 712 |
with concurrent.futures.ThreadPoolExecutor(max_workers=self.lit_max_workers) as executor:
|
| 713 |
futures = []
|
|
@@ -743,7 +751,7 @@ You MUST output your response EXACTLY using the following two delimiters. Do not
|
|
| 743 |
if len(self.citation_map) >= self.lit_target_papers:
|
| 744 |
break
|
| 745 |
|
| 746 |
-
|
| 747 |
return "\n".join(llm_rag_context)
|
| 748 |
|
| 749 |
def resolve_citations(self, text, quarantine=False):
|
|
|
|
| 14 |
import concurrent.futures
|
| 15 |
from tenacity import retry, stop_after_attempt, wait_random_exponential, retry_if_exception_type
|
| 16 |
|
| 17 |
+
# 基础日志配置:自带时间戳和日志级别
|
| 18 |
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s", handlers=[logging.StreamHandler(sys.stdout)])
|
| 19 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
try:
|
| 21 |
from langdetect import detect
|
| 22 |
LANGDETECT_AVAILABLE = True
|
|
|
|
| 70 |
except:
|
| 71 |
self.tokenizer = tiktoken.get_encoding("gpt2")
|
| 72 |
|
| 73 |
+
logging.info("✅ SSciPipelineAgent 初始化完成。三维检索节点与大语言模型已就绪。")
|
| 74 |
|
| 75 |
def _extract_json_from_text(self, text):
|
| 76 |
if not text:
|
|
|
|
| 96 |
else:
|
| 97 |
return json.loads(text[start_list:end_list+1])
|
| 98 |
except Exception as e:
|
| 99 |
+
logging.error(f"❌ JSON 提取失败: {e}")
|
| 100 |
return None
|
| 101 |
|
| 102 |
def export_state(self):
|
|
|
|
| 120 |
self.citation_map = state_dict.get("citation_map", {})
|
| 121 |
self.used_citations = set(state_dict.get("used_citations", []))
|
| 122 |
self.global_memory = state_dict.get("global_memory", self.global_memory)
|
| 123 |
+
logging.info(f"🔄 状态恢复成功: DS API={self.ds_calls}, 现有文献={len(self.citation_map)}")
|
| 124 |
|
| 125 |
def _calc_paragraph_count(self, target_words, lang_hint="en"):
|
| 126 |
words_per_para = 100 if lang_hint in ["ru", "kk"] else 130
|
|
|
|
| 139 |
resp = requests.get(url, headers=headers, params=params, timeout=timeout)
|
| 140 |
|
| 141 |
if resp.status_code == 429:
|
| 142 |
+
logging.warning(f"⚠️ [API 限流] 触发 429 (Too Many Requests)。启动指数退避。URL: {url}")
|
| 143 |
time.sleep(random.uniform(2.0, 5.0))
|
| 144 |
resp.raise_for_status()
|
| 145 |
|
|
|
|
| 147 |
return resp
|
| 148 |
|
| 149 |
if resp.status_code != 200:
|
| 150 |
+
logging.error(f"❌ [API 错误] 状态码: {resp.status_code} | URL: {url}")
|
| 151 |
resp.raise_for_status()
|
| 152 |
return resp
|
| 153 |
|
|
|
|
| 170 |
hypo_matches = re.findall(r'(H\d+[:\s].*?)(?=\n|$)', text, re.IGNORECASE)
|
| 171 |
if hypo_matches:
|
| 172 |
self.global_memory["hypotheses"] = "\n".join(hypo_matches[:10])
|
| 173 |
+
logging.info(f"🧠 [记忆更新] 从 {section_name} 中提取了假设 H1-Hn")
|
| 174 |
|
| 175 |
if any(kw in lower_sec for kw in ["method", "measure", "variable"]):
|
| 176 |
var_matches = re.findall(r'([A-Z][a-z]+(?:\s+[A-Z][a-z]+)*)\s+(?:was|is|refers to)\s+([^\.]+\.)', text)
|
| 177 |
if var_matches:
|
| 178 |
self.global_memory["variables"] = "\n".join([f"- {v[0]}: {v[1][:100]}" for v in var_matches[:8]])
|
| 179 |
+
logging.info(f"🧠 [记忆更新] 从 {section_name} 中提取了核心变量定义")
|
| 180 |
|
| 181 |
if "model" in lower_sec or "figure" in lower_sec:
|
| 182 |
model_match = re.search(r'(Figure\s*\d+[^\.]+\.)', text, re.IGNORECASE)
|
|
|
|
| 257 |
|
| 258 |
def call_deepseek(self, system_prompt, user_prompt, max_tokens=8000, temp=0.3, model_name="deepseek-reasoner"):
|
| 259 |
tokens_est = self._count_tokens(user_prompt)
|
| 260 |
+
logging.info(f"🚀 [DeepSeek 静态请求] 模型: {model_name} | MaxTokens: {max_tokens} | 输入估算: {tokens_est} tokens")
|
| 261 |
|
| 262 |
for attempt in range(3):
|
| 263 |
with self.lock:
|
|
|
|
| 266 |
if attempt == 2 and model_name == "deepseek-reasoner":
|
| 267 |
current_model = "deepseek-chat"
|
| 268 |
temp = 0.3
|
| 269 |
+
logging.warning("⚠️ 强制降级为 deepseek-chat 执行本节点!")
|
| 270 |
|
| 271 |
payload = {
|
| 272 |
"model": current_model,
|
|
|
|
| 282 |
try:
|
| 283 |
with requests.post(self.deepseek_api_url, headers={"Authorization": f"Bearer {self.ds_key}"}, json=payload, stream=True, timeout=self.timeouts["standard"]) as response:
|
| 284 |
if response.status_code != 200:
|
| 285 |
+
logging.error(f"❌ 尝试 {attempt+1}/3 失败: 状态码 {response.status_code} - {response.text[:200]}")
|
| 286 |
if attempt == 2:
|
| 287 |
raise requests.exceptions.HTTPError("API 熔断")
|
| 288 |
time.sleep(3 ** attempt)
|
|
|
|
| 299 |
pass
|
| 300 |
|
| 301 |
elapsed = time.time() - start_time
|
| 302 |
+
logging.info(f"✅ [静态请求返回] 耗时: {elapsed:.2f}s | 输出长度: {len(full_content)} 字符")
|
| 303 |
return full_content.strip()
|
| 304 |
except Exception as e:
|
| 305 |
+
logging.error(f"❌ [网络异常] 尝试 {attempt+1}: {e}")
|
| 306 |
if attempt == 2:
|
| 307 |
raise e
|
| 308 |
time.sleep(3 ** attempt)
|
| 309 |
|
| 310 |
def stream_deepseek(self, system_prompt, user_prompt, max_tokens=8000, model_name="deepseek-reasoner"):
|
| 311 |
tokens_est = self._count_tokens(user_prompt)
|
| 312 |
+
logging.info(f"🌊 [DeepSeek 流式请求] 模型: {model_name} | 输入估算: {tokens_est} tokens")
|
| 313 |
|
| 314 |
temp = 0.3 if model_name == "deepseek-chat" else 0.2
|
| 315 |
for attempt in range(3):
|
|
|
|
| 319 |
if attempt >= 1 and model_name == "deepseek-reasoner":
|
| 320 |
current_model = "deepseek-chat"
|
| 321 |
temp = 0.3
|
| 322 |
+
logging.warning("⚠️ 推理节点异常,强制降级至 deepseek-chat 并重试。")
|
| 323 |
|
| 324 |
payload = {
|
| 325 |
"model": current_model,
|
|
|
|
| 330 |
if current_model != "deepseek-reasoner":
|
| 331 |
payload["temperature"] = temp
|
| 332 |
|
|
|
|
| 333 |
has_started_reasoning = False
|
| 334 |
start_time = time.time()
|
| 335 |
|
| 336 |
try:
|
| 337 |
with requests.post(self.deepseek_api_url, headers={"Authorization": f"Bearer {self.ds_key}"}, json=payload, stream=True, timeout=self.timeouts["stream"]) as response:
|
| 338 |
if response.status_code != 200:
|
| 339 |
+
logging.error(f"❌ 流式尝试 {attempt+1}/3 失败: 状态码 {response.status_code}")
|
| 340 |
if attempt == 2:
|
| 341 |
response.raise_for_status()
|
| 342 |
time.sleep(2)
|
|
|
|
| 354 |
if not has_started_reasoning:
|
| 355 |
yield "\n<think>\n"
|
| 356 |
has_started_reasoning = True
|
| 357 |
+
logging.info(f"🤔 [R1 推理启动] 开始在终端流式输出思考过程...")
|
| 358 |
|
| 359 |
+
# ---------------------------------------------------------
|
| 360 |
+
# 🔥 核心修改点:直接向终端输出内容并强制刷新,实现无延迟的打字机效果
|
| 361 |
+
# ---------------------------------------------------------
|
| 362 |
+
sys.stdout.write(reasoning_content)
|
| 363 |
+
sys.stdout.flush()
|
|
|
|
| 364 |
|
| 365 |
yield reasoning_content
|
| 366 |
continue
|
|
|
|
| 369 |
yield "\n</think>\n\n"
|
| 370 |
has_started_reasoning = False
|
| 371 |
elapsed = time.time() - start_time
|
| 372 |
+
|
| 373 |
+
# 思考结束,终端换行,并记录日志
|
| 374 |
+
sys.stdout.write("\n")
|
| 375 |
+
sys.stdout.flush()
|
| 376 |
+
logging.info(f"💡 [R1 推理结束] 耗时 {elapsed:.1f}秒。开始在终端流式输出正文...")
|
| 377 |
|
| 378 |
if content:
|
| 379 |
+
# 🔥 正文内容同样直接推送到终端流并强制刷新
|
| 380 |
+
sys.stdout.write(content)
|
| 381 |
+
sys.stdout.flush()
|
| 382 |
+
|
| 383 |
yield content
|
| 384 |
|
| 385 |
except Exception:
|
| 386 |
pass
|
| 387 |
+
|
| 388 |
+
# 整个流结束,终端换行
|
| 389 |
+
sys.stdout.write("\n")
|
| 390 |
+
sys.stdout.flush()
|
| 391 |
+
logging.info(f"🏁 [流式生成结束] 总耗时 {(time.time() - start_time):.1f}秒。")
|
| 392 |
return
|
| 393 |
except GeneratorExit:
|
| 394 |
raise
|
| 395 |
except Exception as e:
|
| 396 |
+
# 异常中断时也要确保换行,防止日志排版错乱
|
| 397 |
+
sys.stdout.write("\n")
|
| 398 |
+
sys.stdout.flush()
|
| 399 |
+
logging.error(f"❌ [流式中断]: {e}")
|
| 400 |
if attempt == 2:
|
| 401 |
raise
|
| 402 |
time.sleep(2)
|
|
|
|
| 434 |
|
| 435 |
for attempt in range(2):
|
| 436 |
try:
|
| 437 |
+
logging.info("📄 [本地解析] 正在榨取用户上传的核心文献...")
|
| 438 |
response = self.call_deepseek(sys_prompt, user_prompt, temp=0.1, model_name="deepseek-chat")
|
| 439 |
papers = self._extract_json_from_text(response)
|
| 440 |
if not papers:
|
|
|
|
| 476 |
self.used_citations.add(cite_tag)
|
| 477 |
|
| 478 |
rag_context.append(f"{cite_tag} [USER CORE PAPER] Title: {title}. Core Thesis: {core_thesis}. Abstract: {abstract}")
|
| 479 |
+
logging.info(f"✅ [本地解析] 成功提取 {len(papers)} 篇本地文献元数据。")
|
| 480 |
return "\n".join(rag_context)
|
| 481 |
except Exception as e:
|
| 482 |
time.sleep(2)
|
|
|
|
| 523 |
raise ValueError("Outline Generation Failed.")
|
| 524 |
|
| 525 |
def prefetch_jit_literature(self, topic, keywords, structure):
|
|
|
|
| 526 |
jit_rag_texts = []
|
| 527 |
queries_to_fetch = set()
|
| 528 |
|
|
|
|
| 537 |
if not queries_to_fetch:
|
| 538 |
return ""
|
| 539 |
|
| 540 |
+
logging.info(f"🔍 [Pre-fetch JIT] 生成了 {len(queries_to_fetch)} 个定向检索词: {queries_to_fetch}")
|
| 541 |
|
| 542 |
with concurrent.futures.ThreadPoolExecutor(max_workers=self.lit_max_workers) as executor:
|
| 543 |
futures = []
|
|
|
|
| 554 |
self.citation_map[cite_tag] = {"in_text": res["in_text"], "ref": res["ref"], "id": res["id"]}
|
| 555 |
jit_rag_texts.append(f"{cite_tag} {res['rag_text']}")
|
| 556 |
|
| 557 |
+
logging.info(f"✅ [Pre-fetch JIT] 成功抓取并注入了 {len(jit_rag_texts)} 篇底层支撑文献。")
|
| 558 |
return "\n".join(jit_rag_texts)
|
| 559 |
|
| 560 |
def generate_search_queries(self, topic, keywords):
|
|
|
|
| 575 |
time.sleep(random.uniform(0.5, 1.5))
|
| 576 |
headers = {"x-api-key": str(self.s2_key).strip()} if self.s2_key else {}
|
| 577 |
results = []
|
| 578 |
+
logging.info(f"📡 [S2 Worker] 正在 Semantic Scholar 检索: '{term}' (offset={offset})")
|
| 579 |
try:
|
| 580 |
resp = self._requests_get_with_retry(
|
| 581 |
f"{self.s2_api_url}/paper/search",
|
|
|
|
| 584 |
)
|
| 585 |
if resp.status_code == 400: return results
|
| 586 |
papers = resp.json().get("data", [])
|
| 587 |
+
logging.info(f"✅ [S2 Worker] '{term}' 检索完成,捕获 {len(papers)} 条原始数据")
|
| 588 |
for paper in papers:
|
| 589 |
title = paper.get("title")
|
| 590 |
year = paper.get("year")
|
|
|
|
| 625 |
"source": "s2"
|
| 626 |
})
|
| 627 |
except Exception as e:
|
| 628 |
+
logging.warning(f"⚠️ S2 Worker 异常 (term: {term}): {e}")
|
| 629 |
return results
|
| 630 |
|
| 631 |
def _oa_worker(self, term, page, filter_str):
|
|
|
|
| 633 |
headers = {"User-Agent": f"mailto:ssci_researcher_{random.randint(10000, 99999)}@gmail.com"}
|
| 634 |
if self.openalex_key: headers["Authorization"] = f"Bearer {self.openalex_key}"
|
| 635 |
results = []
|
| 636 |
+
logging.info(f"🌐 [OA Worker] 正在 OpenAlex 检索: '{term}' (page={page})")
|
| 637 |
try:
|
| 638 |
resp = self._requests_get_with_retry(
|
| 639 |
self.openalex_api_url,
|
|
|
|
| 641 |
params={"search": term, "per-page": self.lit_per_page, "page": page, "filter": filter_str}
|
| 642 |
)
|
| 643 |
raw_items = resp.json().get("results", [])
|
| 644 |
+
logging.info(f"✅ [OA Worker] '{term}' 检索完成,捕获 {len(raw_items)} 条原始数据")
|
| 645 |
for item in raw_items:
|
| 646 |
title = item.get("title")
|
| 647 |
if not title or not self._is_english_dominant(title): continue
|
|
|
|
| 684 |
"source": "oa"
|
| 685 |
})
|
| 686 |
except Exception as e:
|
| 687 |
+
logging.warning(f"⚠️ OA Worker 异常 (term: {term}): {e}")
|
| 688 |
return results
|
| 689 |
|
| 690 |
def fetch_references_parallel(self, topic, keywords, min_required=50, progress_callback=None):
|
|
|
|
| 701 |
if expansion_phase == 0:
|
| 702 |
search_terms = self.generate_search_queries(topic, keywords)
|
| 703 |
else:
|
| 704 |
+
logging.info(f"🔄 [文献检索] 阶段 {expansion_phase}: 数量不足 ({len(self.citation_map)}/{min_required}),正在扩大检索范围...")
|
| 705 |
sys_prompt = "You are an expert academic librarian. We must broaden the search scope."
|
| 706 |
user_prompt = f"Topic: {topic}\nKeywords: {keywords}\nUsed Queries: {list(all_used_terms)}\nGenerate 3 NEW, BROADER English search queries. Return ONLY a JSON array of strings."
|
| 707 |
response = self.call_deepseek(sys_prompt, user_prompt, temp=0.5, model_name="deepseek-chat")
|
|
|
|
| 715 |
if not current_batch_terms: continue
|
| 716 |
for t in current_batch_terms: all_used_terms.add(t)
|
| 717 |
|
| 718 |
+
logging.info(f"🚀 [并发下发] 启动扩张阶段 {expansion_phase+1}, 关键词: {current_batch_terms}")
|
| 719 |
|
| 720 |
with concurrent.futures.ThreadPoolExecutor(max_workers=self.lit_max_workers) as executor:
|
| 721 |
futures = []
|
|
|
|
| 751 |
if len(self.citation_map) >= self.lit_target_papers:
|
| 752 |
break
|
| 753 |
|
| 754 |
+
logging.info(f"🎯 [并发检索结束] 成功入库并排重了 {len(self.citation_map)} 篇核心文献。")
|
| 755 |
return "\n".join(llm_rag_context)
|
| 756 |
|
| 757 |
def resolve_citations(self, text, quarantine=False):
|