Spaces:
Running on Zero
Running on Zero
zhiyi commited on
Commit ·
5b12fdd
1
Parent(s): 470e251
Generate and validate reports in structured stages
Browse files- README.md +2 -0
- app.py +262 -24
- tests/test_app.py +33 -1
README.md
CHANGED
|
@@ -18,6 +18,8 @@ Turn an algorithm research topic into an interactive, paper-backed HTML presenta
|
|
| 18 |
|
| 19 |
Users can disable online paper search. In that mode the deck relies on the model's existing knowledge and visibly discloses that papers and recency were not externally verified. Download controls sit in the presentation toolbar: HTML for the standalone interactive deck, JSON for structured research data, and ZIP for both.
|
| 20 |
|
|
|
|
|
|
|
| 21 |
## Space configuration
|
| 22 |
|
| 23 |
Secret:
|
|
|
|
| 18 |
|
| 19 |
Users can disable online paper search. In that mode the deck relies on the model's existing knowledge and visibly discloses that papers and recency were not externally verified. Download controls sit in the presentation toolbar: HTML for the standalone interactive deck, JSON for structured research data, and ZIP for both.
|
| 20 |
|
| 21 |
+
Report generation is staged: raw papers are compressed into evidence cards, then the model produces an outline, a causal technical lineage and method comparison, and section content. Every stage is validated with Pydantic and receives at most two targeted repair attempts before rendering, preventing valid-but-empty JSON from producing blank slides.
|
| 22 |
+
|
| 23 |
## Space configuration
|
| 24 |
|
| 25 |
Secret:
|
app.py
CHANGED
|
@@ -18,7 +18,7 @@ import gradio as gr
|
|
| 18 |
import httpx
|
| 19 |
import spaces
|
| 20 |
from huggingface_hub import InferenceClient
|
| 21 |
-
from pydantic import BaseModel, Field
|
| 22 |
from pypdf import PdfReader
|
| 23 |
|
| 24 |
|
|
@@ -47,6 +47,82 @@ class ResearchRequest(BaseModel):
|
|
| 47 |
paper_urls: list[str] = Field(default_factory=list, max_length=12)
|
| 48 |
|
| 49 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
DEPTHS = {
|
| 51 |
"quick": {"discover": 30, "screen": 10, "deep": 2},
|
| 52 |
"standard": {"discover": 60, "screen": 20, "deep": 4},
|
|
@@ -329,42 +405,201 @@ def extract_pdf(paper: dict[str, Any], max_chars: int = 14000) -> str:
|
|
| 329 |
return "\n".join(pages)[:max_chars]
|
| 330 |
|
| 331 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 332 |
def build_report(
|
| 333 |
request: ResearchRequest,
|
| 334 |
papers: list[dict[str, Any]],
|
| 335 |
deep_texts: dict[str, str],
|
| 336 |
english_understanding: dict[str, Any],
|
| 337 |
) -> dict[str, Any]:
|
| 338 |
-
|
| 339 |
for paper in papers:
|
| 340 |
item = {k: paper.get(k) for k in ("id", "title", "summary", "authors", "published", "updated", "url")}
|
| 341 |
if paper["id"] in deep_texts:
|
| 342 |
-
item["
|
| 343 |
-
|
| 344 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 345 |
"request": request.model_dump(),
|
| 346 |
"english_understanding": english_understanding,
|
| 347 |
"research_cutoff": datetime.now(timezone.utc).date().isoformat(),
|
| 348 |
-
"
|
| 349 |
-
"
|
| 350 |
-
"
|
| 351 |
-
|
| 352 |
-
"
|
| 353 |
-
|
| 354 |
-
"sections": [{"title": "string", "takeaway": "string", "body": ["string"], "paper_ids": ["string"], "details": "string"}],
|
| 355 |
-
"comparisons": [{"method": "string", "mechanism": "string", "strengths": "string", "limitations": "string", "engineering": "string"}],
|
| 356 |
-
"open_questions": ["string"],
|
| 357 |
-
},
|
| 358 |
}
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
"
|
| 362 |
-
"
|
| 363 |
-
json.dumps(
|
| 364 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 365 |
)
|
| 366 |
-
|
| 367 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 368 |
|
| 369 |
|
| 370 |
def esc(value: Any) -> str:
|
|
@@ -494,7 +729,10 @@ def run_research_ui(
|
|
| 494 |
english_understanding["evidence_mode"] = "Model knowledge only; online paper search was disabled by the user."
|
| 495 |
progress(0.45, desc="已关闭联网检索,使用模型已有知识生成")
|
| 496 |
progress(0.68, desc="DeepSeek-V4-Flash 正在生成技术路线")
|
| 497 |
-
|
|
|
|
|
|
|
|
|
|
| 498 |
cutoff = datetime.now(timezone.utc).date().isoformat()
|
| 499 |
content = render_presentation(report, screen, cutoff)
|
| 500 |
job_dir = DATA_DIR / uuid.uuid4().hex
|
|
|
|
| 18 |
import httpx
|
| 19 |
import spaces
|
| 20 |
from huggingface_hub import InferenceClient
|
| 21 |
+
from pydantic import BaseModel, Field, ValidationError
|
| 22 |
from pypdf import PdfReader
|
| 23 |
|
| 24 |
|
|
|
|
| 47 |
paper_urls: list[str] = Field(default_factory=list, max_length=12)
|
| 48 |
|
| 49 |
|
| 50 |
+
class EvidenceCard(BaseModel):
|
| 51 |
+
paper_id: str = Field(min_length=1)
|
| 52 |
+
title: str = Field(min_length=2)
|
| 53 |
+
year: str = Field(min_length=1)
|
| 54 |
+
lineage_role: str = Field(min_length=3)
|
| 55 |
+
problem: str = Field(min_length=10)
|
| 56 |
+
core_idea: str = Field(min_length=10)
|
| 57 |
+
delta: str = Field(min_length=10)
|
| 58 |
+
evidence: list[str] = Field(min_length=1, max_length=4)
|
| 59 |
+
limitations: list[str] = Field(min_length=1, max_length=4)
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
class EvidenceCardsEnvelope(BaseModel):
|
| 63 |
+
cards: list[EvidenceCard] = Field(min_length=1)
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
class SectionPlan(BaseModel):
|
| 67 |
+
id: str = Field(min_length=2)
|
| 68 |
+
title: str = Field(min_length=2)
|
| 69 |
+
purpose: str = Field(min_length=10)
|
| 70 |
+
paper_ids: list[str] = Field(default_factory=list)
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
class ReportOutline(BaseModel):
|
| 74 |
+
title: str = Field(min_length=2)
|
| 75 |
+
subtitle: str = Field(min_length=2)
|
| 76 |
+
executive_summary: list[str] = Field(min_length=3, max_length=5)
|
| 77 |
+
section_plan: list[SectionPlan] = Field(min_length=5, max_length=10)
|
| 78 |
+
open_questions: list[str] = Field(min_length=2, max_length=5)
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
class LineageNode(BaseModel):
|
| 82 |
+
year: str = Field(min_length=1)
|
| 83 |
+
method: str = Field(min_length=2)
|
| 84 |
+
paper_id: str = ""
|
| 85 |
+
bottleneck: str = Field(min_length=10)
|
| 86 |
+
innovation: str = Field(min_length=10)
|
| 87 |
+
limitation: str = Field(min_length=10)
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
class MethodComparison(BaseModel):
|
| 91 |
+
method: str = Field(min_length=2)
|
| 92 |
+
mechanism: str = Field(min_length=10)
|
| 93 |
+
strengths: str = Field(min_length=10)
|
| 94 |
+
limitations: str = Field(min_length=10)
|
| 95 |
+
engineering: str = Field(min_length=10)
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
class TechnicalSynthesis(BaseModel):
|
| 99 |
+
lineage: list[LineageNode] = Field(min_length=4, max_length=9)
|
| 100 |
+
comparisons: list[MethodComparison] = Field(min_length=3, max_length=8)
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
class ReportSection(BaseModel):
|
| 104 |
+
title: str = Field(min_length=2)
|
| 105 |
+
takeaway: str = Field(min_length=10)
|
| 106 |
+
body: list[str] = Field(min_length=2, max_length=5)
|
| 107 |
+
paper_ids: list[str] = Field(default_factory=list)
|
| 108 |
+
details: str = Field(min_length=20)
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
class SectionsEnvelope(BaseModel):
|
| 112 |
+
sections: list[ReportSection] = Field(min_length=5, max_length=10)
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
class ResearchReport(BaseModel):
|
| 116 |
+
title: str = Field(min_length=2)
|
| 117 |
+
subtitle: str = Field(min_length=2)
|
| 118 |
+
executive_summary: list[str] = Field(min_length=3, max_length=5)
|
| 119 |
+
lineage: list[LineageNode] = Field(min_length=4, max_length=9)
|
| 120 |
+
sections: list[ReportSection] = Field(min_length=5, max_length=10)
|
| 121 |
+
comparisons: list[MethodComparison] = Field(min_length=3, max_length=8)
|
| 122 |
+
open_questions: list[str] = Field(min_length=2, max_length=5)
|
| 123 |
+
english_understanding: dict[str, Any] = Field(default_factory=dict)
|
| 124 |
+
|
| 125 |
+
|
| 126 |
DEPTHS = {
|
| 127 |
"quick": {"discover": 30, "screen": 10, "deep": 2},
|
| 128 |
"standard": {"discover": 60, "screen": 20, "deep": 4},
|
|
|
|
| 405 |
return "\n".join(pages)[:max_chars]
|
| 406 |
|
| 407 |
|
| 408 |
+
def validate_stage_with_repairs(
|
| 409 |
+
result: dict[str, Any],
|
| 410 |
+
model_type: type[BaseModel],
|
| 411 |
+
stage_name: str,
|
| 412 |
+
source_context: dict[str, Any],
|
| 413 |
+
max_tokens: int,
|
| 414 |
+
) -> BaseModel:
|
| 415 |
+
candidate = result
|
| 416 |
+
for attempt in range(3):
|
| 417 |
+
try:
|
| 418 |
+
return model_type.model_validate(candidate)
|
| 419 |
+
except ValidationError as exc:
|
| 420 |
+
if attempt == 2:
|
| 421 |
+
raise RuntimeError(f"{stage_name} remained invalid after two repairs: {exc}") from exc
|
| 422 |
+
candidate = chat_json(
|
| 423 |
+
f"Repair the {stage_name} JSON. Preserve valid content, fill missing fields, and return only the complete corrected object.",
|
| 424 |
+
json.dumps({
|
| 425 |
+
"validation_errors": exc.errors(include_url=False),
|
| 426 |
+
"object_to_repair": candidate,
|
| 427 |
+
"source_context": source_context,
|
| 428 |
+
"schema": model_type.model_json_schema(),
|
| 429 |
+
}, ensure_ascii=False),
|
| 430 |
+
max_tokens,
|
| 431 |
+
)
|
| 432 |
+
raise RuntimeError(f"{stage_name} validation failed")
|
| 433 |
+
|
| 434 |
+
|
| 435 |
def build_report(
|
| 436 |
request: ResearchRequest,
|
| 437 |
papers: list[dict[str, Any]],
|
| 438 |
deep_texts: dict[str, str],
|
| 439 |
english_understanding: dict[str, Any],
|
| 440 |
) -> dict[str, Any]:
|
| 441 |
+
raw_evidence = []
|
| 442 |
for paper in papers:
|
| 443 |
item = {k: paper.get(k) for k in ("id", "title", "summary", "authors", "published", "updated", "url")}
|
| 444 |
if paper["id"] in deep_texts:
|
| 445 |
+
item["selected_text_extract"] = deep_texts[paper["id"]]
|
| 446 |
+
raw_evidence.append(item)
|
| 447 |
+
|
| 448 |
+
if raw_evidence:
|
| 449 |
+
card_payload = {
|
| 450 |
+
"request": request.model_dump(),
|
| 451 |
+
"papers": raw_evidence,
|
| 452 |
+
"requirements": {
|
| 453 |
+
"one_card_per_useful_paper": True,
|
| 454 |
+
"paraphrase_instead_of_copy": True,
|
| 455 |
+
"cite_only_supplied_paper_ids": True,
|
| 456 |
+
"schema": EvidenceCardsEnvelope.model_json_schema(),
|
| 457 |
+
},
|
| 458 |
+
}
|
| 459 |
+
cards_result = chat_json(
|
| 460 |
+
"Create compact evidence cards for an algorithm literature review. Keep experimental conditions and limitations. "
|
| 461 |
+
"Do not invent results, citations, or paper IDs. Return only the required JSON object and no empty fields.",
|
| 462 |
+
json.dumps(card_payload, ensure_ascii=False),
|
| 463 |
+
3600,
|
| 464 |
+
)
|
| 465 |
+
cards = validate_stage_with_repairs(
|
| 466 |
+
cards_result,
|
| 467 |
+
EvidenceCardsEnvelope,
|
| 468 |
+
"evidence cards",
|
| 469 |
+
{"request": request.model_dump(), "papers": raw_evidence},
|
| 470 |
+
3600,
|
| 471 |
+
).model_dump()["cards"]
|
| 472 |
+
else:
|
| 473 |
+
cards = []
|
| 474 |
+
|
| 475 |
+
shared_context = {
|
| 476 |
"request": request.model_dump(),
|
| 477 |
"english_understanding": english_understanding,
|
| 478 |
"research_cutoff": datetime.now(timezone.utc).date().isoformat(),
|
| 479 |
+
"evidence_cards": cards,
|
| 480 |
+
"evidence_rule": (
|
| 481 |
+
"Use only supplied paper IDs for citations. State evidence gaps instead of inventing support."
|
| 482 |
+
if cards else
|
| 483 |
+
"Online evidence is disabled. Do not invent paper IDs or claim that recent literature was verified."
|
| 484 |
+
),
|
|
|
|
|
|
|
|
|
|
|
|
|
| 485 |
}
|
| 486 |
+
|
| 487 |
+
outline_result = chat_json(
|
| 488 |
+
"Design a concise technical research presentation for algorithm engineers. Return only JSON matching the schema. "
|
| 489 |
+
"The executive summary must contain substantive conclusions. Every section needs a distinct purpose.",
|
| 490 |
+
json.dumps({
|
| 491 |
+
**shared_context,
|
| 492 |
+
"requirements": {
|
| 493 |
+
"executive_summary_items": "3 to 5",
|
| 494 |
+
"section_plan_items": "5 to 10",
|
| 495 |
+
"open_question_items": "2 to 5",
|
| 496 |
+
"schema": ReportOutline.model_json_schema(),
|
| 497 |
+
},
|
| 498 |
+
}, ensure_ascii=False),
|
| 499 |
+
3000,
|
| 500 |
+
)
|
| 501 |
+
outline = validate_stage_with_repairs(
|
| 502 |
+
outline_result,
|
| 503 |
+
ReportOutline,
|
| 504 |
+
"report outline",
|
| 505 |
+
shared_context,
|
| 506 |
+
3200,
|
| 507 |
+
)
|
| 508 |
+
|
| 509 |
+
synthesis_result = chat_json(
|
| 510 |
+
"Build a causal technical lineage and a consistent method comparison. Return only JSON matching the schema. "
|
| 511 |
+
"Each lineage node must connect a predecessor bottleneck to an innovation and remaining limitation. "
|
| 512 |
+
"Compare every method on the same engineering-relevant dimensions and avoid unsupported numeric rankings.",
|
| 513 |
+
json.dumps({
|
| 514 |
+
**shared_context,
|
| 515 |
+
"outline": outline.model_dump(),
|
| 516 |
+
"requirements": {
|
| 517 |
+
"lineage_nodes": "4 to 9",
|
| 518 |
+
"comparison_methods": "3 to 8",
|
| 519 |
+
"schema": TechnicalSynthesis.model_json_schema(),
|
| 520 |
+
},
|
| 521 |
+
}, ensure_ascii=False),
|
| 522 |
+
3200,
|
| 523 |
+
)
|
| 524 |
+
synthesis = validate_stage_with_repairs(
|
| 525 |
+
synthesis_result,
|
| 526 |
+
TechnicalSynthesis,
|
| 527 |
+
"technical synthesis",
|
| 528 |
+
{**shared_context, "outline": outline.model_dump()},
|
| 529 |
+
3600,
|
| 530 |
+
)
|
| 531 |
+
|
| 532 |
+
sections_result = chat_json(
|
| 533 |
+
"Write presentation-ready technical sections from the approved outline. Return only JSON matching the schema. "
|
| 534 |
+
"Explain mechanisms before benchmark claims. Each takeaway must be supported by its body. "
|
| 535 |
+
"Use only supplied paper IDs; use an empty paper_ids list when external evidence is disabled.",
|
| 536 |
+
json.dumps({
|
| 537 |
+
**shared_context,
|
| 538 |
+
"outline": outline.model_dump(),
|
| 539 |
+
"lineage": [item.model_dump() for item in synthesis.lineage],
|
| 540 |
+
"requirements": {
|
| 541 |
+
"one_section_per_outline_item": True,
|
| 542 |
+
"body_items_per_section": "2 to 5",
|
| 543 |
+
"schema": SectionsEnvelope.model_json_schema(),
|
| 544 |
+
},
|
| 545 |
+
}, ensure_ascii=False),
|
| 546 |
+
5200,
|
| 547 |
+
)
|
| 548 |
+
sections = validate_stage_with_repairs(
|
| 549 |
+
sections_result,
|
| 550 |
+
SectionsEnvelope,
|
| 551 |
+
"report sections",
|
| 552 |
+
{**shared_context, "outline": outline.model_dump()},
|
| 553 |
+
5200,
|
| 554 |
)
|
| 555 |
+
|
| 556 |
+
assembled = {
|
| 557 |
+
"title": outline.title,
|
| 558 |
+
"subtitle": outline.subtitle,
|
| 559 |
+
"executive_summary": outline.executive_summary,
|
| 560 |
+
"lineage": [item.model_dump() for item in synthesis.lineage],
|
| 561 |
+
"sections": [item.model_dump() for item in sections.sections],
|
| 562 |
+
"comparisons": [item.model_dump() for item in synthesis.comparisons],
|
| 563 |
+
"open_questions": outline.open_questions,
|
| 564 |
+
"english_understanding": english_understanding,
|
| 565 |
+
}
|
| 566 |
+
allowed_ids = {paper["id"] for paper in papers}
|
| 567 |
+
for node in assembled["lineage"]:
|
| 568 |
+
if node.get("paper_id") not in allowed_ids:
|
| 569 |
+
node["paper_id"] = ""
|
| 570 |
+
for section in assembled["sections"]:
|
| 571 |
+
section["paper_ids"] = [paper_id for paper_id in section["paper_ids"] if paper_id in allowed_ids]
|
| 572 |
+
return validate_report_with_repairs(assembled, shared_context)
|
| 573 |
+
|
| 574 |
+
|
| 575 |
+
def validate_report_with_repairs(report: dict[str, Any], context: dict[str, Any]) -> dict[str, Any]:
|
| 576 |
+
candidate = report
|
| 577 |
+
for attempt in range(3):
|
| 578 |
+
try:
|
| 579 |
+
validated = ResearchReport.model_validate(candidate).model_dump()
|
| 580 |
+
allowed_ids = {card.get("paper_id") for card in context.get("evidence_cards", [])}
|
| 581 |
+
for node in validated["lineage"]:
|
| 582 |
+
if node.get("paper_id") not in allowed_ids:
|
| 583 |
+
node["paper_id"] = ""
|
| 584 |
+
for section in validated["sections"]:
|
| 585 |
+
section["paper_ids"] = [paper_id for paper_id in section["paper_ids"] if paper_id in allowed_ids]
|
| 586 |
+
return validated
|
| 587 |
+
except ValidationError as exc:
|
| 588 |
+
if attempt == 2:
|
| 589 |
+
raise RuntimeError(f"Report remained incomplete after two repairs: {exc}") from exc
|
| 590 |
+
candidate = chat_json(
|
| 591 |
+
"Repair an incomplete algorithm research report. Preserve valid content and fill only missing or invalid fields. "
|
| 592 |
+
"Return the complete report as one JSON object matching the schema. Do not invent paper IDs.",
|
| 593 |
+
json.dumps({
|
| 594 |
+
"validation_errors": exc.errors(include_url=False),
|
| 595 |
+
"report_to_repair": candidate,
|
| 596 |
+
"evidence_cards": context.get("evidence_cards", []),
|
| 597 |
+
"schema": ResearchReport.model_json_schema(),
|
| 598 |
+
}, ensure_ascii=False),
|
| 599 |
+
5200,
|
| 600 |
+
)
|
| 601 |
+
candidate["english_understanding"] = context.get("english_understanding", {})
|
| 602 |
+
raise RuntimeError("Report validation failed")
|
| 603 |
|
| 604 |
|
| 605 |
def esc(value: Any) -> str:
|
|
|
|
| 729 |
english_understanding["evidence_mode"] = "Model knowledge only; online paper search was disabled by the user."
|
| 730 |
progress(0.45, desc="已关闭联网检索,使用模型已有知识生成")
|
| 731 |
progress(0.68, desc="DeepSeek-V4-Flash 正在生成技术路线")
|
| 732 |
+
try:
|
| 733 |
+
report = build_report(request, screen, deep_texts, english_understanding)
|
| 734 |
+
except (ValidationError, RuntimeError) as exc:
|
| 735 |
+
raise gr.Error(f"报告结构生成失败,模型在两次定向修复后仍未满足完整性要求:{exc}") from exc
|
| 736 |
cutoff = datetime.now(timezone.utc).date().isoformat()
|
| 737 |
content = render_presentation(report, screen, cutoff)
|
| 738 |
job_dir = DATA_DIR / uuid.uuid4().hex
|
tests/test_app.py
CHANGED
|
@@ -1,6 +1,8 @@
|
|
| 1 |
import unittest
|
| 2 |
|
| 3 |
-
from
|
|
|
|
|
|
|
| 4 |
|
| 5 |
|
| 6 |
class AppTests(unittest.TestCase):
|
|
@@ -17,6 +19,36 @@ class AppTests(unittest.TestCase):
|
|
| 17 |
self.assertIn("<!doctype html>", output)
|
| 18 |
self.assertIn("保存 PDF", output)
|
| 19 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
if __name__ == "__main__":
|
| 22 |
unittest.main()
|
|
|
|
| 1 |
import unittest
|
| 2 |
|
| 3 |
+
from pydantic import ValidationError
|
| 4 |
+
|
| 5 |
+
from app import ResearchReport, deduplicate, render_presentation, safe_slug
|
| 6 |
|
| 7 |
|
| 8 |
class AppTests(unittest.TestCase):
|
|
|
|
| 19 |
self.assertIn("<!doctype html>", output)
|
| 20 |
self.assertIn("保存 PDF", output)
|
| 21 |
|
| 22 |
+
def test_empty_report_is_rejected(self):
|
| 23 |
+
with self.assertRaises(ValidationError):
|
| 24 |
+
ResearchReport.model_validate({})
|
| 25 |
+
|
| 26 |
+
def test_complete_report_is_accepted(self):
|
| 27 |
+
section = {
|
| 28 |
+
"title": "Method evolution",
|
| 29 |
+
"takeaway": "Each transition addresses a concrete bottleneck.",
|
| 30 |
+
"body": ["First supported point.", "Second supported point."],
|
| 31 |
+
"paper_ids": [],
|
| 32 |
+
"details": "Sufficient implementation and evidence details for validation.",
|
| 33 |
+
}
|
| 34 |
+
report = {
|
| 35 |
+
"title": "Test report",
|
| 36 |
+
"subtitle": "Validated structured output",
|
| 37 |
+
"executive_summary": ["Conclusion one is substantive.", "Conclusion two is substantive.", "Conclusion three is substantive."],
|
| 38 |
+
"lineage": [
|
| 39 |
+
{"year": str(2020 + i), "method": f"Method {i}", "paper_id": "", "bottleneck": "A concrete predecessor bottleneck.", "innovation": "A concrete technical innovation.", "limitation": "A remaining technical limitation."}
|
| 40 |
+
for i in range(4)
|
| 41 |
+
],
|
| 42 |
+
"sections": [section for _ in range(5)],
|
| 43 |
+
"comparisons": [
|
| 44 |
+
{"method": f"Method {i}", "mechanism": "A sufficiently detailed mechanism.", "strengths": "A sufficiently detailed advantage.", "limitations": "A sufficiently detailed limitation.", "engineering": "A sufficiently detailed engineering note."}
|
| 45 |
+
for i in range(3)
|
| 46 |
+
],
|
| 47 |
+
"open_questions": ["What evidence would change the choice?", "Which constraint dominates deployment?"],
|
| 48 |
+
"english_understanding": {},
|
| 49 |
+
}
|
| 50 |
+
self.assertEqual(ResearchReport.model_validate(report).title, "Test report")
|
| 51 |
+
|
| 52 |
|
| 53 |
if __name__ == "__main__":
|
| 54 |
unittest.main()
|