Spaces:
Running on Zero
Running on Zero
zhiyi commited on
Commit ·
e734ff5
1
Parent(s): ea4f5df
Plan literature searches before retrieval
Browse files- README.md +1 -1
- app.py +101 -9
- 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
|
| 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 |
-
|
|
|
|
| 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.
|
|
|
|
|
|
|
| 331 |
user_papers = normalize_user_papers(request.paper_urls)
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|