Spaces:
Running on Zero
Running on Zero
feat: add demo evidence bundle
Browse filesCo-authored-by: Codex <noreply@openai.com>
- README.md +7 -0
- app.py +18 -1
- hackathon_advisor/artifact_bundle.py +99 -0
- static/app.js +6 -0
- static/index.html +1 -0
- tests/test_app.py +17 -0
- tests/test_artifact_bundle.py +44 -0
README.md
CHANGED
|
@@ -96,6 +96,13 @@ target badges, score seal, build plan, trace, wood map, and export-ready artifac
|
|
| 96 |
advisor engine as a normal user session, so the demo state can be used immediately with JSONL, Notes, Chapter, LoRA,
|
| 97 |
Packet, and PNG exports.
|
| 98 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
## Prize Ledger
|
| 100 |
|
| 101 |
`/api/prize-ledger` and the in-app Prize Ledger panel expose submission evidence: the documented model stack, total
|
|
|
|
| 96 |
advisor engine as a normal user session, so the demo state can be used immediately with JSONL, Notes, Chapter, LoRA,
|
| 97 |
Packet, and PNG exports.
|
| 98 |
|
| 99 |
+
## Demo Evidence Bundle
|
| 100 |
+
|
| 101 |
+
`/api/demo-bundle.zip` and the `Bundle` button download a server-built ZIP for the deterministic demo session. The
|
| 102 |
+
bundle includes a manifest, demo session JSON, Prize Ledger JSON, trace JSONL, Field Notes, Almanac chapter, LoRA SFT
|
| 103 |
+
JSONL, Submission Packet, and a PNG export note. This gives judges or collaborators one auditable package without
|
| 104 |
+
depending on browser `localStorage`.
|
| 105 |
+
|
| 106 |
## Prize Ledger
|
| 107 |
|
| 108 |
`/api/prize-ledger` and the in-app Prize Ledger panel expose submission evidence: the documented model stack, total
|
app.py
CHANGED
|
@@ -5,10 +5,11 @@ import os
|
|
| 5 |
from pathlib import Path
|
| 6 |
from typing import Iterator
|
| 7 |
|
| 8 |
-
from fastapi.responses import FileResponse, HTMLResponse, JSONResponse
|
| 9 |
from gradio import Server
|
| 10 |
|
| 11 |
from hackathon_advisor.agent import AdvisorEngine
|
|
|
|
| 12 |
from hackathon_advisor.chapter import build_chapter_markdown
|
| 13 |
from hackathon_advisor.data import ProjectIndex
|
| 14 |
from hackathon_advisor.demo_rehearsal import build_demo_rehearsal
|
|
@@ -98,6 +99,22 @@ def demo_session() -> dict:
|
|
| 98 |
return build_demo_rehearsal(engine)
|
| 99 |
|
| 100 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
@app.api(name="tool_contract_check", concurrency_limit=8)
|
| 102 |
def tool_contract_check(model_output: str, fallback_query: str = "") -> dict:
|
| 103 |
return resolve_tool_call(model_output, fallback_query=fallback_query).to_dict()
|
|
|
|
| 5 |
from pathlib import Path
|
| 6 |
from typing import Iterator
|
| 7 |
|
| 8 |
+
from fastapi.responses import FileResponse, HTMLResponse, JSONResponse, Response
|
| 9 |
from gradio import Server
|
| 10 |
|
| 11 |
from hackathon_advisor.agent import AdvisorEngine
|
| 12 |
+
from hackathon_advisor.artifact_bundle import BUNDLE_FILENAME, build_demo_bundle_zip
|
| 13 |
from hackathon_advisor.chapter import build_chapter_markdown
|
| 14 |
from hackathon_advisor.data import ProjectIndex
|
| 15 |
from hackathon_advisor.demo_rehearsal import build_demo_rehearsal
|
|
|
|
| 99 |
return build_demo_rehearsal(engine)
|
| 100 |
|
| 101 |
|
| 102 |
+
@app.get("/api/demo-bundle.zip")
|
| 103 |
+
def demo_bundle() -> Response:
|
| 104 |
+
runtime_status = engine.runtime_status()
|
| 105 |
+
ledger = prize_ledger(runtime_status)
|
| 106 |
+
metadata = {
|
| 107 |
+
**trace_metadata(index),
|
| 108 |
+
"project_count": len(index.projects),
|
| 109 |
+
}
|
| 110 |
+
content = build_demo_bundle_zip(build_demo_rehearsal(engine), metadata, ledger)
|
| 111 |
+
return Response(
|
| 112 |
+
content=content,
|
| 113 |
+
media_type="application/zip",
|
| 114 |
+
headers={"Content-Disposition": f'attachment; filename="{BUNDLE_FILENAME}"'},
|
| 115 |
+
)
|
| 116 |
+
|
| 117 |
+
|
| 118 |
@app.api(name="tool_contract_check", concurrency_limit=8)
|
| 119 |
def tool_contract_check(model_output: str, fallback_query: str = "") -> dict:
|
| 120 |
return resolve_tool_call(model_output, fallback_query=fallback_query).to_dict()
|
hackathon_advisor/artifact_bundle.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from datetime import datetime, timezone
|
| 4 |
+
from io import BytesIO
|
| 5 |
+
import json
|
| 6 |
+
from typing import Any
|
| 7 |
+
from zipfile import ZIP_DEFLATED, ZipFile
|
| 8 |
+
|
| 9 |
+
from hackathon_advisor.chapter import build_chapter_markdown
|
| 10 |
+
from hackathon_advisor.field_notes import build_field_notes_markdown
|
| 11 |
+
from hackathon_advisor.lora_dataset import build_lora_dataset_jsonl
|
| 12 |
+
from hackathon_advisor.submission_packet import build_submission_packet_markdown
|
| 13 |
+
from hackathon_advisor.trace_export import build_trace_jsonl
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
BUNDLE_SCHEMA_VERSION = 1
|
| 17 |
+
BUNDLE_FILENAME = "hackathon-advisor-demo-bundle.zip"
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def build_demo_bundle_zip(
|
| 21 |
+
demo: dict[str, Any],
|
| 22 |
+
metadata: dict[str, Any],
|
| 23 |
+
ledger: dict[str, Any],
|
| 24 |
+
) -> bytes:
|
| 25 |
+
session = demo.get("session") if isinstance(demo.get("session"), dict) else {}
|
| 26 |
+
files = _bundle_files(session, metadata, ledger, demo)
|
| 27 |
+
manifest = _manifest(files, metadata, ledger, demo)
|
| 28 |
+
|
| 29 |
+
buffer = BytesIO()
|
| 30 |
+
with ZipFile(buffer, "w", compression=ZIP_DEFLATED) as archive:
|
| 31 |
+
archive.writestr("manifest.json", json.dumps(manifest, ensure_ascii=False, indent=2, sort_keys=True) + "\n")
|
| 32 |
+
for filename, content in files.items():
|
| 33 |
+
archive.writestr(filename, content)
|
| 34 |
+
return buffer.getvalue()
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def _bundle_files(
|
| 38 |
+
session: dict[str, Any],
|
| 39 |
+
metadata: dict[str, Any],
|
| 40 |
+
ledger: dict[str, Any],
|
| 41 |
+
demo: dict[str, Any],
|
| 42 |
+
) -> dict[str, str]:
|
| 43 |
+
return {
|
| 44 |
+
"demo-session.json": json.dumps(demo, ensure_ascii=False, indent=2, sort_keys=True) + "\n",
|
| 45 |
+
"prize-ledger.json": json.dumps(ledger, ensure_ascii=False, indent=2, sort_keys=True) + "\n",
|
| 46 |
+
"trace.jsonl": build_trace_jsonl(session, metadata),
|
| 47 |
+
"field-notes.md": build_field_notes_markdown(session, metadata),
|
| 48 |
+
"almanac-chapter.md": build_chapter_markdown(session, metadata),
|
| 49 |
+
"lora-sft.jsonl": build_lora_dataset_jsonl(session, metadata),
|
| 50 |
+
"submission-packet.md": build_submission_packet_markdown(session, metadata, ledger),
|
| 51 |
+
"png-export-note.md": _png_note(demo),
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def _manifest(
|
| 56 |
+
files: dict[str, str],
|
| 57 |
+
metadata: dict[str, Any],
|
| 58 |
+
ledger: dict[str, Any],
|
| 59 |
+
demo: dict[str, Any],
|
| 60 |
+
) -> dict[str, Any]:
|
| 61 |
+
badges = ledger.get("badges") if isinstance(ledger.get("badges"), list) else []
|
| 62 |
+
return {
|
| 63 |
+
"type": "demo_bundle_manifest",
|
| 64 |
+
"schema_version": BUNDLE_SCHEMA_VERSION,
|
| 65 |
+
"generated_at": datetime.now(timezone.utc).isoformat(timespec="seconds"),
|
| 66 |
+
"app": "hackathon-advisor",
|
| 67 |
+
"turn_count": int(demo.get("turn_count") or 0),
|
| 68 |
+
"file_count": len(files),
|
| 69 |
+
"files": list(files),
|
| 70 |
+
"runtime": ledger.get("runtime") if isinstance(ledger.get("runtime"), dict) else {},
|
| 71 |
+
"badge_status": {
|
| 72 |
+
str(badge.get("name")): str(badge.get("status"))
|
| 73 |
+
for badge in badges
|
| 74 |
+
if isinstance(badge, dict)
|
| 75 |
+
},
|
| 76 |
+
"index": {
|
| 77 |
+
"algorithm": _clean(metadata.get("index_algorithm")),
|
| 78 |
+
"snapshot_generated_at": _clean(metadata.get("snapshot_generated_at")),
|
| 79 |
+
"index_generated_at": _clean(metadata.get("index_generated_at")),
|
| 80 |
+
"snapshot_digest": _clean(metadata.get("snapshot_digest")),
|
| 81 |
+
},
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def _png_note(demo: dict[str, Any]) -> str:
|
| 86 |
+
artifact = demo.get("artifact") if isinstance(demo.get("artifact"), dict) else {}
|
| 87 |
+
title = _clean(artifact.get("title")) or "current fate page"
|
| 88 |
+
return (
|
| 89 |
+
"# PNG Export Note\n\n"
|
| 90 |
+
"The visual fate-page PNG is rendered client-side from the same session artifact so the browser can draw the "
|
| 91 |
+
"canvas with the deployed CSS and fonts. Load the Demo session in the Space, then press `PNG` to export it.\n\n"
|
| 92 |
+
f"Demo artifact title: {title}\n"
|
| 93 |
+
)
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def _clean(value: Any) -> str:
|
| 97 |
+
if value is None:
|
| 98 |
+
return ""
|
| 99 |
+
return " ".join(str(value).split())
|
static/app.js
CHANGED
|
@@ -25,6 +25,7 @@ const exportNotesButton = document.querySelector("#export-notes");
|
|
| 25 |
const exportChapterButton = document.querySelector("#export-chapter");
|
| 26 |
const exportLoraButton = document.querySelector("#export-lora");
|
| 27 |
const exportPacketButton = document.querySelector("#export-packet");
|
|
|
|
| 28 |
const resetButton = document.querySelector("#reset-session");
|
| 29 |
|
| 30 |
const SESSION_STORAGE_KEY = "hackathon-advisor-session-v1";
|
|
@@ -81,6 +82,10 @@ exportPacketButton.addEventListener("click", async () => {
|
|
| 81 |
await exportSubmissionPacket();
|
| 82 |
});
|
| 83 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
resetButton.addEventListener("click", () => {
|
| 85 |
clearSavedSession();
|
| 86 |
window.location.reload();
|
|
@@ -613,6 +618,7 @@ function renderTrace(trace) {
|
|
| 613 |
|
| 614 |
function setCommandDisabled(disabled) {
|
| 615 |
document.querySelectorAll(".command-row button").forEach((button) => {
|
|
|
|
| 616 |
const isArtifact = button.id === "export-artifact";
|
| 617 |
const isTrace = button.id === "export-trace";
|
| 618 |
const isNotes = button.id === "export-notes";
|
|
|
|
| 25 |
const exportChapterButton = document.querySelector("#export-chapter");
|
| 26 |
const exportLoraButton = document.querySelector("#export-lora");
|
| 27 |
const exportPacketButton = document.querySelector("#export-packet");
|
| 28 |
+
const exportBundleButton = document.querySelector("#export-bundle");
|
| 29 |
const resetButton = document.querySelector("#reset-session");
|
| 30 |
|
| 31 |
const SESSION_STORAGE_KEY = "hackathon-advisor-session-v1";
|
|
|
|
| 82 |
await exportSubmissionPacket();
|
| 83 |
});
|
| 84 |
|
| 85 |
+
exportBundleButton.addEventListener("click", () => {
|
| 86 |
+
window.location.assign("/api/demo-bundle.zip");
|
| 87 |
+
});
|
| 88 |
+
|
| 89 |
resetButton.addEventListener("click", () => {
|
| 90 |
clearSavedSession();
|
| 91 |
window.location.reload();
|
|
|
|
| 618 |
|
| 619 |
function setCommandDisabled(disabled) {
|
| 620 |
document.querySelectorAll(".command-row button").forEach((button) => {
|
| 621 |
+
if (button.id === "export-bundle") return;
|
| 622 |
const isArtifact = button.id === "export-artifact";
|
| 623 |
const isTrace = button.id === "export-trace";
|
| 624 |
const isNotes = button.id === "export-notes";
|
static/index.html
CHANGED
|
@@ -38,6 +38,7 @@
|
|
| 38 |
<button type="button" id="export-chapter" title="Export the Almanac chapter" disabled>Chapter</button>
|
| 39 |
<button type="button" id="export-lora" title="Export the LoRA SFT dataset" disabled>LoRA</button>
|
| 40 |
<button type="button" id="export-packet" title="Export the submission packet" disabled>Packet</button>
|
|
|
|
| 41 |
<button type="button" id="export-artifact" title="Export the current fate page" disabled>PNG</button>
|
| 42 |
<button type="button" id="reset-session" title="Clear the saved session">Reset</button>
|
| 43 |
</div>
|
|
|
|
| 38 |
<button type="button" id="export-chapter" title="Export the Almanac chapter" disabled>Chapter</button>
|
| 39 |
<button type="button" id="export-lora" title="Export the LoRA SFT dataset" disabled>LoRA</button>
|
| 40 |
<button type="button" id="export-packet" title="Export the submission packet" disabled>Packet</button>
|
| 41 |
+
<button type="button" id="export-bundle" title="Download the demo evidence bundle">Bundle</button>
|
| 42 |
<button type="button" id="export-artifact" title="Export the current fate page" disabled>PNG</button>
|
| 43 |
<button type="button" id="reset-session" title="Clear the saved session">Reset</button>
|
| 44 |
</div>
|
tests/test_app.py
CHANGED
|
@@ -1,8 +1,11 @@
|
|
| 1 |
import json
|
|
|
|
|
|
|
| 2 |
|
| 3 |
from app import (
|
| 4 |
bootstrap,
|
| 5 |
chapter_artifact,
|
|
|
|
| 6 |
demo_session,
|
| 7 |
engine,
|
| 8 |
field_notes_artifact,
|
|
@@ -129,6 +132,20 @@ def test_demo_session_endpoint_returns_export_ready_state() -> None:
|
|
| 129 |
assert payload["export_ready"]["submission_packet"] is True
|
| 130 |
|
| 131 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 132 |
def test_tool_contract_check_endpoint_defaults_safely() -> None:
|
| 133 |
payload = tool_contract_check("broken", "family archive")
|
| 134 |
|
|
|
|
| 1 |
import json
|
| 2 |
+
from io import BytesIO
|
| 3 |
+
from zipfile import ZipFile
|
| 4 |
|
| 5 |
from app import (
|
| 6 |
bootstrap,
|
| 7 |
chapter_artifact,
|
| 8 |
+
demo_bundle,
|
| 9 |
demo_session,
|
| 10 |
engine,
|
| 11 |
field_notes_artifact,
|
|
|
|
| 132 |
assert payload["export_ready"]["submission_packet"] is True
|
| 133 |
|
| 134 |
|
| 135 |
+
def test_demo_bundle_endpoint_returns_zip_attachment() -> None:
|
| 136 |
+
response = demo_bundle()
|
| 137 |
+
|
| 138 |
+
assert response.media_type == "application/zip"
|
| 139 |
+
assert "hackathon-advisor-demo-bundle.zip" in response.headers["content-disposition"]
|
| 140 |
+
with ZipFile(BytesIO(response.body)) as archive:
|
| 141 |
+
names = set(archive.namelist())
|
| 142 |
+
manifest = json.loads(archive.read("manifest.json"))
|
| 143 |
+
|
| 144 |
+
assert "submission-packet.md" in names
|
| 145 |
+
assert "lora-sft.jsonl" in names
|
| 146 |
+
assert manifest["turn_count"] == 2
|
| 147 |
+
|
| 148 |
+
|
| 149 |
def test_tool_contract_check_endpoint_defaults_safely() -> None:
|
| 150 |
payload = tool_contract_check("broken", "family archive")
|
| 151 |
|
tests/test_artifact_bundle.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
from io import BytesIO
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
from zipfile import ZipFile
|
| 5 |
+
|
| 6 |
+
from hackathon_advisor.agent import AdvisorEngine
|
| 7 |
+
from hackathon_advisor.artifact_bundle import build_demo_bundle_zip
|
| 8 |
+
from hackathon_advisor.data import ProjectIndex
|
| 9 |
+
from hackathon_advisor.demo_rehearsal import build_demo_rehearsal
|
| 10 |
+
from hackathon_advisor.prize_ledger import prize_ledger
|
| 11 |
+
from hackathon_advisor.trace_export import trace_metadata
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def test_demo_bundle_contains_submission_evidence_files() -> None:
|
| 15 |
+
index = ProjectIndex.from_files(Path("data/projects.json"), Path("data/project_index.json"))
|
| 16 |
+
engine = AdvisorEngine(index)
|
| 17 |
+
metadata = {
|
| 18 |
+
**trace_metadata(index),
|
| 19 |
+
"project_count": len(index.projects),
|
| 20 |
+
}
|
| 21 |
+
content = build_demo_bundle_zip(build_demo_rehearsal(engine), metadata, prize_ledger(engine.runtime_status()))
|
| 22 |
+
|
| 23 |
+
with ZipFile(BytesIO(content)) as archive:
|
| 24 |
+
names = set(archive.namelist())
|
| 25 |
+
manifest = json.loads(archive.read("manifest.json"))
|
| 26 |
+
trace = archive.read("trace.jsonl").decode("utf-8")
|
| 27 |
+
packet = archive.read("submission-packet.md").decode("utf-8")
|
| 28 |
+
|
| 29 |
+
assert names == {
|
| 30 |
+
"manifest.json",
|
| 31 |
+
"demo-session.json",
|
| 32 |
+
"prize-ledger.json",
|
| 33 |
+
"trace.jsonl",
|
| 34 |
+
"field-notes.md",
|
| 35 |
+
"almanac-chapter.md",
|
| 36 |
+
"lora-sft.jsonl",
|
| 37 |
+
"submission-packet.md",
|
| 38 |
+
"png-export-note.md",
|
| 39 |
+
}
|
| 40 |
+
assert manifest["type"] == "demo_bundle_manifest"
|
| 41 |
+
assert manifest["turn_count"] == 2
|
| 42 |
+
assert manifest["badge_status"]["Well-Tuned"] == "dataset-ready"
|
| 43 |
+
assert "agent_turn" in trace
|
| 44 |
+
assert "## Prize Evidence" in packet
|