Spaces:
Running
Running
Update core/pipeline_agent.py
Browse files- core/pipeline_agent.py +63 -41
core/pipeline_agent.py
CHANGED
|
@@ -68,6 +68,28 @@ class SSciPipelineAgent:
|
|
| 68 |
except:
|
| 69 |
self.tokenizer = tiktoken.get_encoding("gpt2")
|
| 70 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
def export_state(self):
|
| 72 |
with self.lock:
|
| 73 |
return {
|
|
@@ -151,29 +173,31 @@ class SSciPipelineAgent:
|
|
| 151 |
if self.global_memory["method_summary"]: memory_parts.append(f"【METHODOLOGICAL SUMMARY】\n{self.global_memory['method_summary']}")
|
| 152 |
|
| 153 |
golden_meta = self.global_memory.get("core_golden_metadata", [])
|
| 154 |
-
|
|
|
|
| 155 |
lower_sec = section_name.lower()
|
| 156 |
injected_data = []
|
| 157 |
|
| 158 |
if any(kw in lower_sec for kw in ["introduction", "background"]):
|
| 159 |
for p in golden_meta:
|
| 160 |
-
if p.get('future_gaps'): injected_data.append(f"[{p
|
| 161 |
if injected_data: memory_parts.append("【TARGETED INJECTION: CORE LITERATURE GAPS (FOR RESEARCH GAP FORMULATION)】\n" + "\n".join(injected_data))
|
| 162 |
|
| 163 |
elif any(kw in lower_sec for kw in ["literature", "theor", "hypothesis", "review"]):
|
| 164 |
for p in golden_meta:
|
| 165 |
-
if
|
| 166 |
-
|
|
|
|
| 167 |
if injected_data: memory_parts.append("【TARGETED INJECTION: CORE LITERATURE THEORIES & PATHWAYS (USE FOR HYPOTHESIS DEDUCTION)】\n" + "\n".join(injected_data))
|
| 168 |
|
| 169 |
elif any(kw in lower_sec for kw in ["method", "measure", "data", "sample"]):
|
| 170 |
for p in golden_meta:
|
| 171 |
-
if p.get('variables_and_measurements'): injected_data.append(f"[{p
|
| 172 |
if injected_data: memory_parts.append("【TARGETED INJECTION: CORE LITERATURE MEASUREMENTS (USE FOR SCALE ADAPTATION DEFENSE)】\n" + "\n".join(injected_data))
|
| 173 |
|
| 174 |
elif any(kw in lower_sec for kw in ["discussion", "conclusion", "implication", "result", "analysis"]):
|
| 175 |
for p in golden_meta:
|
| 176 |
-
if p.get('causal_pathways'): injected_data.append(f"[{p
|
| 177 |
if injected_data: memory_parts.append("【TARGETED INJECTION: CORE LITERATURE FINDINGS (COMPARE OUR RESULTS AGAINST THESE)】\n" + "\n".join(injected_data))
|
| 178 |
|
| 179 |
full = "\n\n".join(memory_parts)
|
|
@@ -291,7 +315,6 @@ class SSciPipelineAgent:
|
|
| 291 |
try:
|
| 292 |
response = self.call_deepseek(sys_prompt, user_prompt, max_tokens=300)
|
| 293 |
clean_resp = re.sub(r'<think>.*?</think>', '', response, flags=re.DOTALL).strip()
|
| 294 |
-
clean_resp = re.sub(r'^```.*|```$', '', clean_resp, flags=re.MULTILINE).strip()
|
| 295 |
if '||' in clean_resp:
|
| 296 |
headings = [h.strip() for h in clean_resp.split('||') if h.strip()]
|
| 297 |
if len(headings) >= 3: return headings
|
|
@@ -305,11 +328,10 @@ class SSciPipelineAgent:
|
|
| 305 |
user_prompt = f"Target Section: {section_name}\nAnalyze the reference text:\n<REFERENCE_TEXT>\n{reference_text[:10000]}\n</REFERENCE_TEXT>\nOutput ONLY a valid JSON object: {{\"tone_and_style\": \"...\", \"reporting_structure\": \"...\", \"few_shot_snippet\": \"...\"}}"
|
| 306 |
for attempt in range(3):
|
| 307 |
response = self.call_deepseek(sys_prompt, user_prompt, temp=0.2)
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
except: time.sleep(1)
|
| 313 |
return {"tone_and_style": "objective academic tone", "reporting_structure": "standard empirical reporting", "few_shot_snippet": ""}
|
| 314 |
|
| 315 |
def parse_user_core_literature(self, user_lit_text):
|
|
@@ -339,13 +361,23 @@ class SSciPipelineAgent:
|
|
| 339 |
for attempt in range(2):
|
| 340 |
try:
|
| 341 |
response = self.call_deepseek(sys_prompt, user_prompt, temp=0.1, model_name="deepseek-chat")
|
| 342 |
-
|
| 343 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 344 |
|
| 345 |
self.global_memory["core_golden_metadata"] = papers
|
| 346 |
|
| 347 |
rag_context = []
|
| 348 |
for paper in papers:
|
|
|
|
| 349 |
title = paper.get("title", "User Provided Document")
|
| 350 |
year = paper.get("year", "n.d.")
|
| 351 |
authors = paper.get("authors", ["User, Uploaded"])
|
|
@@ -396,35 +428,26 @@ class SSciPipelineAgent:
|
|
| 396 |
|
| 397 |
def generate_outline(self, topic, keywords, synthesis_result):
|
| 398 |
sys_prompt = "You are a senior academic journal reviewer. Design a strict empirical research paper outline."
|
| 399 |
-
user_prompt = f"""Topic: {topic}\nKeywords: {keywords}\nLiterature Synthesis & Gaps:\n{synthesis_result}\n\nConstruct a detailed empirical outline. Your response must be a single valid JSON object: {{"en_topic": "...", "en_keywords": "...", "structure": [{{"section": "...", "weight": 0.15, "is_method_or_result": false}}]}}.
|
| 400 |
-
CRITICAL: The 'structure' array MUST ONLY contain main body sections. DO NOT include Abstract, Keywords, Acknowledgments, or References in the structure array.
|
| 401 |
-
Do not include any other text."""
|
| 402 |
for attempt in range(3):
|
| 403 |
response = self.call_deepseek(sys_prompt, user_prompt, temp=0.2)
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
raise ValueError("Outline Generation Failed. Invalid JSON structure.")
|
| 410 |
|
| 411 |
def generate_search_queries(self, topic, keywords):
|
| 412 |
sys_prompt = "You are an expert academic librarian. Generate English search queries."
|
| 413 |
user_prompt = f"""Topic: {topic}\nKeywords: {keywords}\nGenerate exactly 3 concise English search queries. Return ONLY a valid JSON array of strings."""
|
| 414 |
for attempt in range(3):
|
| 415 |
response = self.call_deepseek(sys_prompt, user_prompt, temp=0.1 + attempt * 0.1)
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
if
|
| 420 |
-
final_queries = [str(q).strip() for q in queries if self._is_english_dominant(str(q).strip())]
|
| 421 |
-
if final_queries: return final_queries[:3]
|
| 422 |
-
except:
|
| 423 |
-
fallback_queries = re.findall(r'"([a-zA-Z\s\-]+)"', response)
|
| 424 |
-
if fallback_queries:
|
| 425 |
-
final_queries = [q.strip() for q in fallback_queries if self._is_english_dominant(q.strip())]
|
| 426 |
-
if final_queries: return final_queries[:3]
|
| 427 |
time.sleep(1)
|
|
|
|
| 428 |
combined_text = f"{topic} {keywords}"
|
| 429 |
english_chunks = re.findall(r'[a-zA-Z0-9\s\-]{4,}', combined_text)
|
| 430 |
clean_chunks = [chunk.strip() for chunk in english_chunks if len(chunk.strip()) >= 4 and self._is_english_dominant(chunk.strip())]
|
|
@@ -463,13 +486,12 @@ class SSciPipelineAgent:
|
|
| 463 |
else:
|
| 464 |
sys_prompt = "You are an expert academic librarian. The previous literature search yielded insufficient results. We must broaden the search scope."
|
| 465 |
user_prompt = f"Original Topic: {topic}\nOriginal Keywords: {keywords}\nAlready Used Queries: {list(all_used_terms)}\nGenerate exactly 3 NEW, BROADER, or RELATED English search queries (max 3-4 words each) to expand the literature search. Do NOT repeat previous queries. Return ONLY a valid JSON array of strings."
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
except:
|
| 473 |
fallback_queries = re.findall(r'"([a-zA-Z\s\-]+)"', response)
|
| 474 |
search_terms = [q.strip() for q in fallback_queries if self._is_english_dominant(q.strip())][:3]
|
| 475 |
|
|
|
|
| 68 |
except:
|
| 69 |
self.tokenizer = tiktoken.get_encoding("gpt2")
|
| 70 |
|
| 71 |
+
# 🚨 新增:无敌 JSON 提取器。杜绝大模型废话导致的解析崩溃
|
| 72 |
+
def _extract_json_from_text(self, text):
|
| 73 |
+
if not text: return None
|
| 74 |
+
text = text.strip()
|
| 75 |
+
try:
|
| 76 |
+
start_dict = text.find('{')
|
| 77 |
+
end_dict = text.rfind('}')
|
| 78 |
+
start_list = text.find('[')
|
| 79 |
+
end_list = text.rfind(']')
|
| 80 |
+
|
| 81 |
+
dict_len = end_dict - start_dict if start_dict != -1 and end_dict != -1 else -1
|
| 82 |
+
list_len = end_list - start_list if start_list != -1 and end_list != -1 else -1
|
| 83 |
+
|
| 84 |
+
if dict_len == -1 and list_len == -1: return None
|
| 85 |
+
|
| 86 |
+
if dict_len > list_len:
|
| 87 |
+
return json.loads(text[start_dict:end_dict+1])
|
| 88 |
+
else:
|
| 89 |
+
return json.loads(text[start_list:end_list+1])
|
| 90 |
+
except Exception:
|
| 91 |
+
return None
|
| 92 |
+
|
| 93 |
def export_state(self):
|
| 94 |
with self.lock:
|
| 95 |
return {
|
|
|
|
| 173 |
if self.global_memory["method_summary"]: memory_parts.append(f"【METHODOLOGICAL SUMMARY】\n{self.global_memory['method_summary']}")
|
| 174 |
|
| 175 |
golden_meta = self.global_memory.get("core_golden_metadata", [])
|
| 176 |
+
# 🚨 类型安全防护:确保 golden_meta 一定是 List,防止大模型意外包裹字典导致遍历崩溃
|
| 177 |
+
if isinstance(golden_meta, list) and section_name:
|
| 178 |
lower_sec = section_name.lower()
|
| 179 |
injected_data = []
|
| 180 |
|
| 181 |
if any(kw in lower_sec for kw in ["introduction", "background"]):
|
| 182 |
for p in golden_meta:
|
| 183 |
+
if isinstance(p, dict) and p.get('future_gaps'): injected_data.append(f"[{p.get('title', 'Ref')}] Future Gaps: {p['future_gaps']}")
|
| 184 |
if injected_data: memory_parts.append("【TARGETED INJECTION: CORE LITERATURE GAPS (FOR RESEARCH GAP FORMULATION)】\n" + "\n".join(injected_data))
|
| 185 |
|
| 186 |
elif any(kw in lower_sec for kw in ["literature", "theor", "hypothesis", "review"]):
|
| 187 |
for p in golden_meta:
|
| 188 |
+
if isinstance(p, dict):
|
| 189 |
+
if p.get('theoretical_lens'): injected_data.append(f"[{p.get('title', 'Ref')}] Theoretical Lens: {p['theoretical_lens']}")
|
| 190 |
+
if p.get('causal_pathways'): injected_data.append(f"[{p.get('title', 'Ref')}] Pathways: {p['causal_pathways']}")
|
| 191 |
if injected_data: memory_parts.append("【TARGETED INJECTION: CORE LITERATURE THEORIES & PATHWAYS (USE FOR HYPOTHESIS DEDUCTION)】\n" + "\n".join(injected_data))
|
| 192 |
|
| 193 |
elif any(kw in lower_sec for kw in ["method", "measure", "data", "sample"]):
|
| 194 |
for p in golden_meta:
|
| 195 |
+
if isinstance(p, dict) and p.get('variables_and_measurements'): injected_data.append(f"[{p.get('title', 'Ref')}] Measurements: {p['variables_and_measurements']}")
|
| 196 |
if injected_data: memory_parts.append("【TARGETED INJECTION: CORE LITERATURE MEASUREMENTS (USE FOR SCALE ADAPTATION DEFENSE)】\n" + "\n".join(injected_data))
|
| 197 |
|
| 198 |
elif any(kw in lower_sec for kw in ["discussion", "conclusion", "implication", "result", "analysis"]):
|
| 199 |
for p in golden_meta:
|
| 200 |
+
if isinstance(p, dict) and p.get('causal_pathways'): injected_data.append(f"[{p.get('title', 'Ref')}] Confirmed Pathways: {p['causal_pathways']}")
|
| 201 |
if injected_data: memory_parts.append("【TARGETED INJECTION: CORE LITERATURE FINDINGS (COMPARE OUR RESULTS AGAINST THESE)】\n" + "\n".join(injected_data))
|
| 202 |
|
| 203 |
full = "\n\n".join(memory_parts)
|
|
|
|
| 315 |
try:
|
| 316 |
response = self.call_deepseek(sys_prompt, user_prompt, max_tokens=300)
|
| 317 |
clean_resp = re.sub(r'<think>.*?</think>', '', response, flags=re.DOTALL).strip()
|
|
|
|
| 318 |
if '||' in clean_resp:
|
| 319 |
headings = [h.strip() for h in clean_resp.split('||') if h.strip()]
|
| 320 |
if len(headings) >= 3: return headings
|
|
|
|
| 328 |
user_prompt = f"Target Section: {section_name}\nAnalyze the reference text:\n<REFERENCE_TEXT>\n{reference_text[:10000]}\n</REFERENCE_TEXT>\nOutput ONLY a valid JSON object: {{\"tone_and_style\": \"...\", \"reporting_structure\": \"...\", \"few_shot_snippet\": \"...\"}}"
|
| 329 |
for attempt in range(3):
|
| 330 |
response = self.call_deepseek(sys_prompt, user_prompt, temp=0.2)
|
| 331 |
+
result = self._extract_json_from_text(response)
|
| 332 |
+
if result and isinstance(result, dict) and "few_shot_snippet" in result:
|
| 333 |
+
return result
|
| 334 |
+
time.sleep(1)
|
|
|
|
| 335 |
return {"tone_and_style": "objective academic tone", "reporting_structure": "standard empirical reporting", "few_shot_snippet": ""}
|
| 336 |
|
| 337 |
def parse_user_core_literature(self, user_lit_text):
|
|
|
|
| 361 |
for attempt in range(2):
|
| 362 |
try:
|
| 363 |
response = self.call_deepseek(sys_prompt, user_prompt, temp=0.1, model_name="deepseek-chat")
|
| 364 |
+
papers = self._extract_json_from_text(response)
|
| 365 |
+
|
| 366 |
+
if not papers: raise ValueError("Extracted JSON is null")
|
| 367 |
+
|
| 368 |
+
# 修复可能包裹在 dict 中的情况 (e.g., {"papers": [...]})
|
| 369 |
+
if isinstance(papers, dict):
|
| 370 |
+
for v in papers.values():
|
| 371 |
+
if isinstance(v, list):
|
| 372 |
+
papers = v
|
| 373 |
+
break
|
| 374 |
+
if not isinstance(papers, list): papers = [papers]
|
| 375 |
|
| 376 |
self.global_memory["core_golden_metadata"] = papers
|
| 377 |
|
| 378 |
rag_context = []
|
| 379 |
for paper in papers:
|
| 380 |
+
if not isinstance(paper, dict): continue
|
| 381 |
title = paper.get("title", "User Provided Document")
|
| 382 |
year = paper.get("year", "n.d.")
|
| 383 |
authors = paper.get("authors", ["User, Uploaded"])
|
|
|
|
| 428 |
|
| 429 |
def generate_outline(self, topic, keywords, synthesis_result):
|
| 430 |
sys_prompt = "You are a senior academic journal reviewer. Design a strict empirical research paper outline."
|
| 431 |
+
user_prompt = f"""Topic: {topic}\nKeywords: {keywords}\nLiterature Synthesis & Gaps:\n{synthesis_result}\n\nConstruct a detailed empirical outline. Your response must be a single valid JSON object: {{"en_topic": "...", "en_keywords": "...", "structure": [{{"section": "...", "weight": 0.15, "is_method_or_result": false}}]}}. Do not include any other text."""
|
|
|
|
|
|
|
| 432 |
for attempt in range(3):
|
| 433 |
response = self.call_deepseek(sys_prompt, user_prompt, temp=0.2)
|
| 434 |
+
plan = self._extract_json_from_text(response)
|
| 435 |
+
if plan and isinstance(plan, dict) and "structure" in plan:
|
| 436 |
+
return plan
|
| 437 |
+
time.sleep(2)
|
| 438 |
+
raise ValueError("Outline Generation Failed. Invalid JSON structure returned by model.")
|
|
|
|
| 439 |
|
| 440 |
def generate_search_queries(self, topic, keywords):
|
| 441 |
sys_prompt = "You are an expert academic librarian. Generate English search queries."
|
| 442 |
user_prompt = f"""Topic: {topic}\nKeywords: {keywords}\nGenerate exactly 3 concise English search queries. Return ONLY a valid JSON array of strings."""
|
| 443 |
for attempt in range(3):
|
| 444 |
response = self.call_deepseek(sys_prompt, user_prompt, temp=0.1 + attempt * 0.1)
|
| 445 |
+
queries = self._extract_json_from_text(response)
|
| 446 |
+
if queries and isinstance(queries, list) and len(queries) > 0:
|
| 447 |
+
final_queries = [str(q).strip() for q in queries if self._is_english_dominant(str(q).strip())]
|
| 448 |
+
if final_queries: return final_queries[:3]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 449 |
time.sleep(1)
|
| 450 |
+
|
| 451 |
combined_text = f"{topic} {keywords}"
|
| 452 |
english_chunks = re.findall(r'[a-zA-Z0-9\s\-]{4,}', combined_text)
|
| 453 |
clean_chunks = [chunk.strip() for chunk in english_chunks if len(chunk.strip()) >= 4 and self._is_english_dominant(chunk.strip())]
|
|
|
|
| 486 |
else:
|
| 487 |
sys_prompt = "You are an expert academic librarian. The previous literature search yielded insufficient results. We must broaden the search scope."
|
| 488 |
user_prompt = f"Original Topic: {topic}\nOriginal Keywords: {keywords}\nAlready Used Queries: {list(all_used_terms)}\nGenerate exactly 3 NEW, BROADER, or RELATED English search queries (max 3-4 words each) to expand the literature search. Do NOT repeat previous queries. Return ONLY a valid JSON array of strings."
|
| 489 |
+
response = self.call_deepseek(sys_prompt, user_prompt, temp=0.5)
|
| 490 |
+
queries = self._extract_json_from_text(response)
|
| 491 |
+
|
| 492 |
+
if queries and isinstance(queries, list):
|
| 493 |
+
search_terms = [str(q).strip() for q in queries if self._is_english_dominant(str(q).strip())][:3]
|
| 494 |
+
else:
|
|
|
|
| 495 |
fallback_queries = re.findall(r'"([a-zA-Z\s\-]+)"', response)
|
| 496 |
search_terms = [q.strip() for q in fallback_queries if self._is_english_dominant(q.strip())][:3]
|
| 497 |
|