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

feat: add demo rehearsal session

Browse files

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

README.md CHANGED
@@ -89,6 +89,13 @@ session: live links, snapshot provenance, a timed demo script, artifact checklis
89
  session trace summary, social post draft, and open badge gaps. This keeps the final submission story tied to the same
90
  auditable state as the app instead of a separate hand-curated checklist.
91
 
 
 
 
 
 
 
 
92
  ## Prize Ledger
93
 
94
  `/api/prize-ledger` and the in-app Prize Ledger panel expose submission evidence: the documented model stack, total
 
89
  session trace summary, social post draft, and open badge gaps. This keeps the final submission story tied to the same
90
  auditable state as the app instead of a separate hand-curated checklist.
91
 
92
+ ## Demo Rehearsal
93
+
94
+ `/api/demo-session` and the `Demo` button load a deterministic two-turn rehearsal: a complete project idea, profile,
95
+ target badges, score seal, build plan, trace, wood map, and export-ready artifacts. It is built by running the same
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
app.py CHANGED
@@ -11,6 +11,7 @@ from gradio import Server
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.field_notes import build_field_notes_markdown
15
  from hackathon_advisor.lora_dataset import build_lora_dataset_jsonl
16
  from hackathon_advisor.prize_ledger import prize_ledger
@@ -92,6 +93,11 @@ def tool_contracts() -> dict:
92
  }
93
 
94
 
 
 
 
 
 
95
  @app.api(name="tool_contract_check", concurrency_limit=8)
96
  def tool_contract_check(model_output: str, fallback_query: str = "") -> dict:
97
  return resolve_tool_call(model_output, fallback_query=fallback_query).to_dict()
 
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
15
  from hackathon_advisor.field_notes import build_field_notes_markdown
16
  from hackathon_advisor.lora_dataset import build_lora_dataset_jsonl
17
  from hackathon_advisor.prize_ledger import prize_ledger
 
93
  }
94
 
95
 
96
+ @app.get("/api/demo-session")
97
+ 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()
hackathon_advisor/demo_rehearsal.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+
6
+ DEMO_PROMPT = (
7
+ "A local-first archive cartographer for family photos that runs offline, cites nearby Spaces, "
8
+ "and exports Field Notes."
9
+ )
10
+ DEMO_PLAN_PROMPT = "make a build plan"
11
+ DEMO_PROFILE = {
12
+ "skills": "frontend prototyping, Python, small-model evaluation",
13
+ "time": "one focused weekend",
14
+ "preferences": "auditable artifacts, local-first runtime, strong demo beat",
15
+ "constraints": "CPU Space runtime; no proprietary inference API",
16
+ }
17
+ DEMO_TARGETS = [
18
+ "Off the Grid",
19
+ "Well-Tuned",
20
+ "Off-Brand",
21
+ "Sharing is Caring",
22
+ "Field Notes",
23
+ ]
24
+
25
+
26
+ def build_demo_rehearsal(engine: Any) -> dict[str, Any]:
27
+ initial_state = {
28
+ "profile": dict(DEMO_PROFILE),
29
+ "targets": list(DEMO_TARGETS),
30
+ }
31
+ first = engine.turn(DEMO_PROMPT, initial_state)
32
+ second = engine.turn(DEMO_PLAN_PROMPT, first.state)
33
+ score = second.score or first.score
34
+ artifact = second.artifact or first.artifact
35
+ projects = first.projects or second.projects
36
+ whitespace = first.whitespace or second.whitespace
37
+ session = second.state
38
+ return {
39
+ "prompt": DEMO_PROMPT,
40
+ "plan_prompt": DEMO_PLAN_PROMPT,
41
+ "response": second.response,
42
+ "session": session,
43
+ "score": score.to_dict() if score else None,
44
+ "plan": list(second.plan),
45
+ "artifact": artifact,
46
+ "projects": [project.to_public_dict() for project in projects],
47
+ "whitespace": [item.to_dict() for item in whitespace],
48
+ "turn_count": len(session.get("trace") or []),
49
+ "export_ready": {
50
+ "trace": bool(session.get("trace")),
51
+ "notes": bool(session.get("trace")),
52
+ "chapter": bool(session.get("ideas")),
53
+ "lora_dataset": bool(session.get("trace")),
54
+ "submission_packet": bool(session.get("trace")),
55
+ "png": bool(artifact),
56
+ },
57
+ }
static/app.js CHANGED
@@ -18,6 +18,7 @@ const traceEl = document.querySelector("#trace");
18
  const provenanceEl = document.querySelector("#provenance");
19
  const verdictEl = document.querySelector("#verdict");
20
  const overallEl = document.querySelector("#overall");
 
21
  const exportButton = document.querySelector("#export-artifact");
22
  const exportTraceButton = document.querySelector("#export-trace");
23
  const exportNotesButton = document.querySelector("#export-notes");
@@ -51,6 +52,10 @@ document.querySelectorAll("[data-command]").forEach((button) => {
51
  });
52
  });
53
 
 
 
 
 
54
  exportButton.addEventListener("click", () => {
55
  if (!currentArtifact) return;
56
  exportArtifact(currentArtifact);
@@ -161,6 +166,64 @@ async function bootstrap() {
161
  renderWhitespace(data.whitespace || []);
162
  }
163
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
164
  function renderProvenance(data) {
165
  const snapshot = shortDate(data.snapshot_generated_at);
166
  const index = shortDate(data.index_generated_at);
 
18
  const provenanceEl = document.querySelector("#provenance");
19
  const verdictEl = document.querySelector("#verdict");
20
  const overallEl = document.querySelector("#overall");
21
+ const demoButton = document.querySelector("#load-demo");
22
  const exportButton = document.querySelector("#export-artifact");
23
  const exportTraceButton = document.querySelector("#export-trace");
24
  const exportNotesButton = document.querySelector("#export-notes");
 
52
  });
53
  });
54
 
55
+ demoButton.addEventListener("click", async () => {
56
+ await loadDemoSession();
57
+ });
58
+
59
  exportButton.addEventListener("click", () => {
60
  if (!currentArtifact) return;
61
  exportArtifact(currentArtifact);
 
166
  renderWhitespace(data.whitespace || []);
167
  }
168
 
169
+ async function loadDemoSession() {
170
+ submit.disabled = true;
171
+ setCommandDisabled(true);
172
+ ink.classList.remove("bleed", "gold");
173
+ ink.classList.add("thinking");
174
+ ink.textContent = "The demo pages are turning.";
175
+ corrections.textContent = "";
176
+ try {
177
+ const response = await fetch("/api/demo-session");
178
+ if (!response.ok) throw new Error(`demo rehearsal failed with ${response.status}`);
179
+ applyDemoSession(await response.json());
180
+ } catch (error) {
181
+ ink.textContent = `The demo rehearsal could not be loaded: ${error.message}`;
182
+ ink.classList.remove("thinking");
183
+ ink.classList.add("bleed");
184
+ } finally {
185
+ submit.disabled = false;
186
+ setCommandDisabled(false);
187
+ input.focus();
188
+ }
189
+ }
190
+
191
+ function applyDemoSession(data) {
192
+ session = data.session || {};
193
+ session.profile = session.profile || {};
194
+ session.targets = Array.isArray(session.targets) ? session.targets : [];
195
+ currentArtifact = data.artifact || session.last_artifact || null;
196
+ ink.textContent = data.response || "Demo rehearsal loaded.";
197
+ ink.classList.remove("thinking");
198
+ if (data.score) {
199
+ verdictEl.textContent = data.score.verdict;
200
+ overallEl.textContent = Number(data.score.overall).toFixed(1);
201
+ renderScore(data.score);
202
+ ink.classList.toggle("bleed", data.score.verdict.startsWith("ECHO"));
203
+ ink.classList.toggle("gold", data.score.verdict.startsWith("UNWRITTEN"));
204
+ }
205
+ renderTargets(session.targets);
206
+ renderProfile(session.profile);
207
+ renderIdeas(session.ideas || []);
208
+ renderTrace(session.trace || []);
209
+ renderPlan(data.plan || session.last_plan || []);
210
+ renderWhitespace(data.whitespace || []);
211
+ if (currentArtifact?.wood_map) renderWoodMap(currentArtifact.wood_map);
212
+ if (data.score?.echoes?.length) {
213
+ renderCitations(data.score.echoes);
214
+ } else {
215
+ renderProjects(data.projects || []);
216
+ }
217
+ exportButton.disabled = !currentArtifact;
218
+ exportTraceButton.disabled = !(session.trace?.length);
219
+ exportNotesButton.disabled = !(session.trace?.length);
220
+ exportChapterButton.disabled = !(session.ideas?.length);
221
+ exportLoraButton.disabled = !(session.trace?.length);
222
+ exportPacketButton.disabled = !(session.trace?.length);
223
+ corrections.textContent = `demo: ${data.turn_count || 0} recorded turns`;
224
+ saveSession();
225
+ }
226
+
227
  function renderProvenance(data) {
228
  const snapshot = shortDate(data.snapshot_generated_at);
229
  const index = shortDate(data.index_generated_at);
static/index.html CHANGED
@@ -27,6 +27,7 @@
27
  <button id="submit" type="submit" title="Ink the page">Ink</button>
28
  </form>
29
  <div class="command-row" aria-label="Advisor commands">
 
30
  <button type="button" data-command="write bolder and find whitespace" title="Find a gold margin">
31
  Gap
32
  </button>
 
27
  <button id="submit" type="submit" title="Ink the page">Ink</button>
28
  </form>
29
  <div class="command-row" aria-label="Advisor commands">
30
+ <button type="button" id="load-demo" title="Load a complete demo session">Demo</button>
31
  <button type="button" data-command="write bolder and find whitespace" title="Find a gold margin">
32
  Gap
33
  </button>
tests/test_app.py CHANGED
@@ -3,6 +3,7 @@ import json
3
  from app import (
4
  bootstrap,
5
  chapter_artifact,
 
6
  engine,
7
  field_notes_artifact,
8
  health,
@@ -117,6 +118,17 @@ def test_tool_contracts_endpoint_exposes_schemas() -> None:
117
  assert any(tool["function"]["name"] == "search_projects" for tool in payload["tools"])
118
 
119
 
 
 
 
 
 
 
 
 
 
 
 
120
  def test_tool_contract_check_endpoint_defaults_safely() -> None:
121
  payload = tool_contract_check("broken", "family archive")
122
 
 
3
  from app import (
4
  bootstrap,
5
  chapter_artifact,
6
+ demo_session,
7
  engine,
8
  field_notes_artifact,
9
  health,
 
118
  assert any(tool["function"]["name"] == "search_projects" for tool in payload["tools"])
119
 
120
 
121
+ def test_demo_session_endpoint_returns_export_ready_state() -> None:
122
+ payload = demo_session()
123
+
124
+ assert payload["turn_count"] == 2
125
+ assert payload["session"]["trace"]
126
+ assert payload["session"]["ideas"]
127
+ assert payload["plan"]
128
+ assert payload["artifact"]["wood_map"]["dots"]
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
 
tests/test_demo_rehearsal.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+
3
+ from hackathon_advisor.agent import AdvisorEngine
4
+ from hackathon_advisor.data import ProjectIndex
5
+ from hackathon_advisor.demo_rehearsal import DEMO_TARGETS, build_demo_rehearsal
6
+
7
+
8
+ def test_demo_rehearsal_builds_complete_session() -> None:
9
+ index = ProjectIndex.from_files(Path("data/projects.json"), Path("data/project_index.json"))
10
+ engine = AdvisorEngine(index)
11
+
12
+ payload = build_demo_rehearsal(engine)
13
+ session = payload["session"]
14
+
15
+ assert payload["turn_count"] == 2
16
+ assert payload["score"]["overall"] > 0
17
+ assert payload["artifact"]["title"]
18
+ assert payload["artifact"]["wood_map"]["dots"]
19
+ assert payload["plan"]
20
+ assert any("LoRA" in step for step in payload["plan"])
21
+ assert session["targets"] == DEMO_TARGETS
22
+ assert session["profile"]["constraints"] == "CPU Space runtime; no proprietary inference API"
23
+ assert len(session["trace"]) == 2
24
+ assert payload["export_ready"] == {
25
+ "trace": True,
26
+ "notes": True,
27
+ "chapter": True,
28
+ "lora_dataset": True,
29
+ "submission_packet": True,
30
+ "png": True,
31
+ }