zhiyi commited on
Commit
e734ff5
·
1 Parent(s): ea4f5df

Plan literature searches before retrieval

Browse files
Files changed (3) hide show
  1. README.md +1 -1
  2. app.py +101 -9
  3. research-ppt/references/academic-search.md +2 -0
README.md CHANGED
@@ -14,7 +14,7 @@ license: apache-2.0
14
 
15
  # Algorithm Research Deck
16
 
17
- Turn an algorithm research topic into an interactive, paper-backed HTML presentation. The Space searches arXiv on CPU, uses ZeroGPU for open-model synthesis, previews the result, and provides HTML, JSON, and ZIP downloads.
18
 
19
  ## Space configuration
20
 
 
14
 
15
  # Algorithm Research Deck
16
 
17
+ Turn an algorithm research topic into an interactive, paper-backed HTML presentation. The model first expands the topic into historical, milestone, current, and benchmark-oriented search queries. The Space searches arXiv with OpenAlex fallback, uses ZeroGPU for open-model synthesis, previews the result, and provides HTML, JSON, and ZIP downloads.
18
 
19
  ## Space configuration
20
 
app.py CHANGED
@@ -110,6 +110,48 @@ def arxiv_search(topic: str, limit: int) -> list[dict[str, Any]]:
110
  return papers
111
 
112
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
  def normalize_user_papers(urls: list[str]) -> list[dict[str, Any]]:
114
  result = []
115
  for raw in urls:
@@ -134,13 +176,58 @@ def deduplicate(papers: list[dict[str, Any]]) -> list[dict[str, Any]]:
134
  seen = set()
135
  output = []
136
  for paper in papers:
137
- key = re.sub(r"v\d+$", "", paper["id"])
 
138
  if key not in seen:
139
  seen.add(key)
140
  output.append(paper)
141
  return output
142
 
143
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
144
  @spaces.GPU(duration=120)
145
  def generate_text(system: str, prompt: str, max_tokens: int) -> str:
146
  global _model, _tokenizer
@@ -193,6 +280,8 @@ def select_papers(request: ResearchRequest, papers: list[dict[str, Any]], screen
193
 
194
 
195
  def extract_pdf(paper: dict[str, Any], max_chars: int = 14000) -> str:
 
 
196
  response = http_get_with_retries(paper["pdf_url"], read_timeout=120, attempts=3)
197
  if len(response.content) > 20 * 1024 * 1024:
198
  return "PDF omitted because it exceeds the 20 MB limit."
@@ -327,14 +416,13 @@ def run_research_ui(
327
  paper_urls=[x.strip() for x in paper_urls.splitlines() if x.strip()],
328
  )
329
  budget = DEPTHS[request.depth]
330
- progress(0.08, desc="正在检索 arXiv")
 
 
331
  user_papers = normalize_user_papers(request.paper_urls)
332
- try:
333
- discovered_papers = arxiv_search(request.topic, budget["discover"])
334
- except Exception as exc:
335
- if not user_papers:
336
- raise gr.Error(f"arXiv 连续三次请求超时。请稍后重试,或填写至少一个 arXiv 论文链接。详情:{exc}") from exc
337
- discovered_papers = []
338
  papers = deduplicate(user_papers + discovered_papers)
339
  if not papers:
340
  raise gr.Error("没有检索到论文,请尝试英文关键词或提供 arXiv 链接。")
@@ -365,7 +453,11 @@ def run_research_ui(
365
  report_path.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
366
  bundle_path = create_bundle(job_dir, html_path, report_path)
367
  progress(1, desc="完成")
368
- status = f"✅ 已完成:检索 {len(papers)} 篇,筛选 {len(screen)} 篇,深读 {len(deep_texts)} 篇。"
 
 
 
 
369
  return status, preview_frame(content), str(html_path), str(report_path), str(bundle_path)
370
 
371
 
 
110
  return papers
111
 
112
 
113
+ def openalex_search(query: str, limit: int) -> list[dict[str, Any]]:
114
+ email = os.getenv("CONTACT_EMAIL", "").strip()
115
+ url = f"https://api.openalex.org/works?search={quote_plus(query)}&per-page={min(limit, 100)}"
116
+ if email:
117
+ url += f"&mailto={quote_plus(email)}"
118
+ response = http_get_with_retries(url, read_timeout=60, attempts=3)
119
+ results = response.json().get("results", [])
120
+ papers = []
121
+ for work in results:
122
+ primary = work.get("primary_location") or {}
123
+ source = primary.get("source") or {}
124
+ open_access = work.get("open_access") or {}
125
+ best_oa = work.get("best_oa_location") or {}
126
+ landing = primary.get("landing_page_url") or work.get("doi") or work.get("id") or ""
127
+ pdf_url = best_oa.get("pdf_url") or primary.get("pdf_url") or ""
128
+ authors = [
129
+ ((item.get("author") or {}).get("display_name") or "")
130
+ for item in work.get("authorships", [])
131
+ ]
132
+ inverted = work.get("abstract_inverted_index") or {}
133
+ ordered_words = sorted(
134
+ ((position, word) for word, positions in inverted.items() for position in positions),
135
+ key=lambda item: item[0],
136
+ )
137
+ abstract = " ".join(word for _, word in ordered_words)
138
+ papers.append({
139
+ "id": (work.get("doi") or work.get("id") or landing).rsplit("/", 1)[-1],
140
+ "title": work.get("title") or "Untitled",
141
+ "summary": abstract,
142
+ "authors": [x for x in authors if x],
143
+ "published": work.get("publication_date") or "",
144
+ "updated": "",
145
+ "url": landing,
146
+ "pdf_url": pdf_url,
147
+ "venue": source.get("display_name") or "",
148
+ "citation_count": work.get("cited_by_count") or 0,
149
+ "open_access": open_access.get("is_oa", False),
150
+ "source": "OpenAlex",
151
+ })
152
+ return papers
153
+
154
+
155
  def normalize_user_papers(urls: list[str]) -> list[dict[str, Any]]:
156
  result = []
157
  for raw in urls:
 
176
  seen = set()
177
  output = []
178
  for paper in papers:
179
+ normalized_title = re.sub(r"[^a-z0-9]+", "", paper.get("title", "").lower())
180
+ key = normalized_title or re.sub(r"v\d+$", "", paper["id"])
181
  if key not in seen:
182
  seen.add(key)
183
  output.append(paper)
184
  return output
185
 
186
 
187
+ def plan_search_queries(request: ResearchRequest) -> list[str]:
188
+ prompt = json.dumps({
189
+ "topic": request.topic,
190
+ "purpose": request.purpose,
191
+ "focus": request.focus,
192
+ "date_range": request.date_range,
193
+ "instruction": (
194
+ "Understand the technical topic before searching. Return JSON with search_queries containing 3 to 5 concise English academic queries. "
195
+ "Cover historical/classical terminology, milestone method families, current terminology, and one limitation or benchmark angle. "
196
+ "Queries must be suitable for arXiv and OpenAlex. Do not include site operators."
197
+ ),
198
+ }, ensure_ascii=False)
199
+ result = chat_json(
200
+ "You plan literature searches for an algorithm engineer. Expand ambiguous topic names into precise academic terminology.",
201
+ prompt,
202
+ 650,
203
+ )
204
+ queries = [str(x).strip() for x in result.get("search_queries", []) if str(x).strip()]
205
+ if request.topic not in queries:
206
+ queries.append(request.topic)
207
+ return queries[:6]
208
+
209
+
210
+ def search_literature(queries: list[str], total_limit: int) -> tuple[list[dict[str, Any]], list[str]]:
211
+ per_query = max(5, min(20, total_limit // max(1, len(queries))))
212
+ papers: list[dict[str, Any]] = []
213
+ warnings: list[str] = []
214
+ for query in queries:
215
+ try:
216
+ found = arxiv_search(query, per_query)
217
+ for paper in found:
218
+ paper["source"] = "arXiv"
219
+ papers.extend(found)
220
+ except Exception:
221
+ warnings.append(f"arXiv timeout: {query}")
222
+ try:
223
+ papers.extend(openalex_search(query, per_query))
224
+ except Exception:
225
+ warnings.append(f"OpenAlex timeout: {query}")
226
+ if len(deduplicate(papers)) >= total_limit:
227
+ break
228
+ return deduplicate(papers)[:total_limit], warnings
229
+
230
+
231
  @spaces.GPU(duration=120)
232
  def generate_text(system: str, prompt: str, max_tokens: int) -> str:
233
  global _model, _tokenizer
 
280
 
281
 
282
  def extract_pdf(paper: dict[str, Any], max_chars: int = 14000) -> str:
283
+ if not paper.get("pdf_url"):
284
+ return "No open PDF URL was available from the academic index."
285
  response = http_get_with_retries(paper["pdf_url"], read_timeout=120, attempts=3)
286
  if len(response.content) > 20 * 1024 * 1024:
287
  return "PDF omitted because it exceeds the 20 MB limit."
 
416
  paper_urls=[x.strip() for x in paper_urls.splitlines() if x.strip()],
417
  )
418
  budget = DEPTHS[request.depth]
419
+ progress(0.05, desc="模型正在理解主题并规划检索词")
420
+ queries = plan_search_queries(request)
421
+ progress(0.16, desc="正在检索 arXiv 和 OpenAlex")
422
  user_papers = normalize_user_papers(request.paper_urls)
423
+ discovered_papers, search_warnings = search_literature(queries, budget["discover"])
424
+ if not discovered_papers and not user_papers:
425
+ raise gr.Error("arXiv 和 OpenAlex 均未返回论文。请稍后重试,或填写至少一个 arXiv 论文链接。")
 
 
 
426
  papers = deduplicate(user_papers + discovered_papers)
427
  if not papers:
428
  raise gr.Error("没有检索到论文,请尝试英文关键词或提供 arXiv 链接。")
 
453
  report_path.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
454
  bundle_path = create_bundle(job_dir, html_path, report_path)
455
  progress(1, desc="完成")
456
+ warning_text = f" 检索降级:{';'.join(search_warnings)}。" if search_warnings else ""
457
+ status = (
458
+ f"✅ 已完成:模型规划 {len(queries)} 组检索词,发现 {len(papers)} 篇,"
459
+ f"筛选 {len(screen)} 篇,深读 {len(deep_texts)} 篇。{warning_text}"
460
+ )
461
  return status, preview_frame(content), str(html_path), str(report_path), str(bundle_path)
462
 
463
 
research-ppt/references/academic-search.md CHANGED
@@ -26,6 +26,8 @@ Build synonym sets for the task, mechanism, historical terminology, and benchmar
26
 
27
  Run a forward search from classical terms and a backward search from current terminology. Record queries that materially define coverage.
28
 
 
 
29
  ## Metadata rules
30
 
31
  - Use DOI links for published versions when available and arXiv links for preprint versions.
 
26
 
27
  Run a forward search from classical terms and a backward search from current terminology. Record queries that materially define coverage.
28
 
29
+ For automated research, let the model interpret the topic and propose a small set of English academic queries before calling indexes. Cover classical terminology, milestone families, current terminology, and a benchmark or limitation angle. Use OpenAlex as a metadata fallback when arXiv is unavailable, and disclose degraded coverage rather than silently treating one failed index as no literature.
30
+
31
  ## Metadata rules
32
 
33
  - Use DOI links for published versions when available and arXiv links for preprint versions.