JacobLinCool Codex commited on
Commit
e86200e
·
verified ·
1 Parent(s): f68e817

feat: add demo evidence bundle

Browse files

Co-authored-by: Codex <noreply@openai.com>

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