Spaces:
Running on Zero
Running on Zero
feat: export submission packet
Browse filesCo-authored-by: Codex <noreply@openai.com>
- README.md +7 -0
- app.py +18 -0
- hackathon_advisor/submission_packet.py +254 -0
- static/app.js +19 -1
- static/index.html +1 -0
- tests/test_app.py +16 -0
- tests/test_submission_packet.py +55 -0
README.md
CHANGED
|
@@ -82,6 +82,13 @@ turns. Each included turn yields a tool-call example and an advisor-response exa
|
|
| 82 |
selected targets, parsed XML tool call, tool observations, and score context preserved. This prepares the Well-Tuned
|
| 83 |
path without claiming that the adapter has already been trained or published.
|
| 84 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
## Prize Ledger
|
| 86 |
|
| 87 |
`/api/prize-ledger` and the in-app Prize Ledger panel expose submission evidence: the documented model stack, total
|
|
|
|
| 82 |
selected targets, parsed XML tool call, tool observations, and score context preserved. This prepares the Well-Tuned
|
| 83 |
path without claiming that the adapter has already been trained or published.
|
| 84 |
|
| 85 |
+
## Submission Packet
|
| 86 |
+
|
| 87 |
+
The `submission_packet` Gradio API endpoint and `Packet` button export a Markdown submission bundle for the current
|
| 88 |
+
session: live links, snapshot provenance, a timed demo script, artifact checklist, Prize Ledger evidence, model budget,
|
| 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
|
app.py
CHANGED
|
@@ -14,6 +14,7 @@ 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
|
|
|
|
| 17 |
from hackathon_advisor.tool_contracts import resolve_tool_call, tool_schemas
|
| 18 |
from hackathon_advisor.tools import TARGETS
|
| 19 |
from hackathon_advisor.trace_export import build_trace_jsonl, trace_metadata
|
|
@@ -150,6 +151,23 @@ def lora_dataset_artifact(session_json: str = "{}") -> str:
|
|
| 150 |
)
|
| 151 |
|
| 152 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
@app.api(name="agent_turn", concurrency_limit=4, stream_every=0.04)
|
| 154 |
def agent_turn(message: str, session_json: str = "{}") -> Iterator[str]:
|
| 155 |
try:
|
|
|
|
| 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
|
| 17 |
+
from hackathon_advisor.submission_packet import build_submission_packet_markdown
|
| 18 |
from hackathon_advisor.tool_contracts import resolve_tool_call, tool_schemas
|
| 19 |
from hackathon_advisor.tools import TARGETS
|
| 20 |
from hackathon_advisor.trace_export import build_trace_jsonl, trace_metadata
|
|
|
|
| 151 |
)
|
| 152 |
|
| 153 |
|
| 154 |
+
@app.api(name="submission_packet", concurrency_limit=8)
|
| 155 |
+
def submission_packet_artifact(session_json: str = "{}") -> str:
|
| 156 |
+
try:
|
| 157 |
+
session = json.loads(session_json or "{}")
|
| 158 |
+
except json.JSONDecodeError:
|
| 159 |
+
session = {}
|
| 160 |
+
runtime_status = engine.runtime_status()
|
| 161 |
+
return build_submission_packet_markdown(
|
| 162 |
+
session,
|
| 163 |
+
{
|
| 164 |
+
**trace_metadata(index),
|
| 165 |
+
"project_count": len(index.projects),
|
| 166 |
+
},
|
| 167 |
+
prize_ledger(runtime_status),
|
| 168 |
+
)
|
| 169 |
+
|
| 170 |
+
|
| 171 |
@app.api(name="agent_turn", concurrency_limit=4, stream_every=0.04)
|
| 172 |
def agent_turn(message: str, session_json: str = "{}") -> Iterator[str]:
|
| 173 |
try:
|
hackathon_advisor/submission_packet.py
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from datetime import datetime, timezone
|
| 4 |
+
from typing import Any
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
SPACE_URL = "https://huggingface.co/spaces/build-small-hackathon/hackathon-advisor"
|
| 8 |
+
LIVE_URL = "https://build-small-hackathon-hackathon-advisor.hf.space"
|
| 9 |
+
GITHUB_URL = "https://github.com/JacobLinCool/hackathon-advisor"
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def build_submission_packet_markdown(
|
| 13 |
+
session: dict[str, Any],
|
| 14 |
+
metadata: dict[str, Any],
|
| 15 |
+
ledger: dict[str, Any],
|
| 16 |
+
) -> str:
|
| 17 |
+
ideas = _list_of_dicts(session.get("ideas"))
|
| 18 |
+
trace = _list_of_dicts(session.get("trace"))
|
| 19 |
+
targets = [str(target) for target in session.get("targets") or []]
|
| 20 |
+
last_plan = [str(step) for step in session.get("last_plan") or []]
|
| 21 |
+
current = _current_idea(session, ideas)
|
| 22 |
+
score = current.get("score") if isinstance(current.get("score"), dict) else {}
|
| 23 |
+
verdict = _clean(score.get("verdict")) if score else "DRAFT"
|
| 24 |
+
overall = _clean(score.get("overall")) if score else "0.0"
|
| 25 |
+
title = _clean(current.get("title") or "Unwritten Page")
|
| 26 |
+
|
| 27 |
+
lines = [
|
| 28 |
+
"# Hackathon Advisor Submission Packet",
|
| 29 |
+
"",
|
| 30 |
+
f"Generated: {datetime.now(timezone.utc).isoformat(timespec='seconds')}",
|
| 31 |
+
"",
|
| 32 |
+
"## Links",
|
| 33 |
+
"",
|
| 34 |
+
f"- Live Space: {LIVE_URL}",
|
| 35 |
+
f"- Hugging Face repository: {SPACE_URL}",
|
| 36 |
+
f"- GitHub repository: {GITHUB_URL}",
|
| 37 |
+
"",
|
| 38 |
+
"## Snapshot",
|
| 39 |
+
"",
|
| 40 |
+
f"- Project snapshot: {_clean(metadata.get('snapshot_generated_at'))}",
|
| 41 |
+
f"- Project count: {_clean(metadata.get('project_count', 'unknown'))}",
|
| 42 |
+
f"- Index: {_clean(metadata.get('index_algorithm'))} at {_clean(metadata.get('index_generated_at'))}",
|
| 43 |
+
f"- Digest: `{_clean(metadata.get('snapshot_digest'))}`",
|
| 44 |
+
"",
|
| 45 |
+
"## Current Demo Page",
|
| 46 |
+
"",
|
| 47 |
+
f"- Title: {title}",
|
| 48 |
+
f"- Verdict: {verdict} {overall}/10",
|
| 49 |
+
f"- Targets: {', '.join(targets) if targets else 'No specific targets'}",
|
| 50 |
+
f"- Pitch: {_clean(current.get('pitch')) or 'No pitch recorded.'}",
|
| 51 |
+
"",
|
| 52 |
+
]
|
| 53 |
+
|
| 54 |
+
lines.extend(_demo_script(title, verdict, overall, current, trace, last_plan))
|
| 55 |
+
lines.extend(_artifact_checklist(trace, ideas, session))
|
| 56 |
+
lines.extend(_prize_evidence(ledger))
|
| 57 |
+
lines.extend(_model_budget(ledger))
|
| 58 |
+
lines.extend(_session_summary(ideas, trace, last_plan))
|
| 59 |
+
lines.extend(_social_post(title, verdict, ledger))
|
| 60 |
+
lines.extend(_open_gaps(ledger))
|
| 61 |
+
|
| 62 |
+
return "\n".join(lines).rstrip() + "\n"
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def _demo_script(
|
| 66 |
+
title: str,
|
| 67 |
+
verdict: str,
|
| 68 |
+
overall: str,
|
| 69 |
+
idea: dict[str, Any],
|
| 70 |
+
trace: list[dict[str, Any]],
|
| 71 |
+
last_plan: list[str],
|
| 72 |
+
) -> list[str]:
|
| 73 |
+
echoes = _echoes(idea)
|
| 74 |
+
first_echo = echoes[0] if echoes else {}
|
| 75 |
+
project = first_echo.get("project") if isinstance(first_echo.get("project"), dict) else {}
|
| 76 |
+
echo_title = _clean(project.get("title") or project.get("id") or "a cited Space")
|
| 77 |
+
page = _clean(first_echo.get("page_number")) or "?"
|
| 78 |
+
plan_step = _clean(last_plan[0]) if last_plan else "Press Plan to show the build path."
|
| 79 |
+
turn_count = len(trace)
|
| 80 |
+
return [
|
| 81 |
+
"## Demo Script",
|
| 82 |
+
"",
|
| 83 |
+
"| Time | Beat | On-screen proof |",
|
| 84 |
+
"| --- | --- | --- |",
|
| 85 |
+
f"| 0:00 | Open The Unwritten Almanac and point to the local snapshot count. | Snapshot metadata and Prize Ledger are visible. |",
|
| 86 |
+
f"| 0:10 | Type the project instinct for `{title}`. | Streaming response starts, then the seal reads {verdict} at {overall}/10. |",
|
| 87 |
+
f"| 0:30 | Show the nearest echo. | Page {page}: {echo_title}. |",
|
| 88 |
+
f"| 0:45 | Press Gap or Plan to move from diagnosis to build path. | {plan_step} |",
|
| 89 |
+
f"| 1:05 | Export evidence artifacts. | JSONL, Notes, Chapter, LoRA, Packet, and PNG buttons are available after {turn_count} recorded turns. |",
|
| 90 |
+
"| 1:20 | Close with prize honesty. | Ready badges and planned badges are separated in the Prize Ledger. |",
|
| 91 |
+
"",
|
| 92 |
+
]
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def _artifact_checklist(trace: list[dict[str, Any]], ideas: list[dict[str, Any]], session: dict[str, Any]) -> list[str]:
|
| 96 |
+
has_trace = bool(trace)
|
| 97 |
+
has_ideas = bool(ideas)
|
| 98 |
+
has_artifact = isinstance(session.get("last_artifact"), dict) and bool(session["last_artifact"].get("title"))
|
| 99 |
+
rows = [
|
| 100 |
+
("Tool trace JSONL", has_trace, "trace_artifact"),
|
| 101 |
+
("Field Notes markdown", has_trace, "field_notes"),
|
| 102 |
+
("Almanac chapter markdown", has_ideas, "chapter"),
|
| 103 |
+
("MiniCPM5 LoRA SFT JSONL", has_trace, "lora_dataset"),
|
| 104 |
+
("Submission packet markdown", True, "submission_packet"),
|
| 105 |
+
("Fate page PNG", has_artifact, "client-side canvas export"),
|
| 106 |
+
]
|
| 107 |
+
lines = ["## Artifact Checklist", "", "| Artifact | Status | Source |", "| --- | --- | --- |"]
|
| 108 |
+
for name, ready, source in rows:
|
| 109 |
+
lines.append(f"| {name} | {'ready' if ready else 'needs session'} | {source} |")
|
| 110 |
+
lines.append("")
|
| 111 |
+
return lines
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
def _prize_evidence(ledger: dict[str, Any]) -> list[str]:
|
| 115 |
+
lines = ["## Prize Evidence", "", "| Prize path | Status | Evidence |", "| --- | --- | --- |"]
|
| 116 |
+
for badge in _list_of_dicts(ledger.get("badges")):
|
| 117 |
+
lines.append(
|
| 118 |
+
"| "
|
| 119 |
+
+ " | ".join(
|
| 120 |
+
[
|
| 121 |
+
_clean(badge.get("name")),
|
| 122 |
+
_clean(badge.get("status")),
|
| 123 |
+
_clean(badge.get("evidence")),
|
| 124 |
+
]
|
| 125 |
+
)
|
| 126 |
+
+ " |"
|
| 127 |
+
)
|
| 128 |
+
lines.append("")
|
| 129 |
+
return lines
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
def _model_budget(ledger: dict[str, Any]) -> list[str]:
|
| 133 |
+
total = _clean(ledger.get("total_params_b"))
|
| 134 |
+
limit = _clean(ledger.get("tiny_titan_limit_b"))
|
| 135 |
+
eligible = "yes" if ledger.get("tiny_titan_eligible") else "no"
|
| 136 |
+
lines = [
|
| 137 |
+
"## Model Budget",
|
| 138 |
+
"",
|
| 139 |
+
f"- Total documented stack: {total}B params",
|
| 140 |
+
f"- Tiny Titan limit: {limit}B params",
|
| 141 |
+
f"- Tiny Titan eligible: {eligible}",
|
| 142 |
+
"",
|
| 143 |
+
"| Role | Model | Params | Status | Runtime |",
|
| 144 |
+
"| --- | --- | --- | --- | --- |",
|
| 145 |
+
]
|
| 146 |
+
for item in _list_of_dicts(ledger.get("model_stack")):
|
| 147 |
+
lines.append(
|
| 148 |
+
"| "
|
| 149 |
+
+ " | ".join(
|
| 150 |
+
[
|
| 151 |
+
_clean(item.get("role")),
|
| 152 |
+
_clean(item.get("model")),
|
| 153 |
+
f"{_clean(item.get('params_b'))}B",
|
| 154 |
+
_clean(item.get("status")),
|
| 155 |
+
_clean(item.get("runtime")),
|
| 156 |
+
]
|
| 157 |
+
)
|
| 158 |
+
+ " |"
|
| 159 |
+
)
|
| 160 |
+
lines.append("")
|
| 161 |
+
return lines
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
def _session_summary(ideas: list[dict[str, Any]], trace: list[dict[str, Any]], last_plan: list[str]) -> list[str]:
|
| 165 |
+
lines = ["## Session Evidence", ""]
|
| 166 |
+
if ideas:
|
| 167 |
+
lines.extend(["### Idea Board", ""])
|
| 168 |
+
for index, idea in enumerate(ideas[-4:], start=max(1, len(ideas) - 3)):
|
| 169 |
+
score = idea.get("score") if isinstance(idea.get("score"), dict) else {}
|
| 170 |
+
verdict = _clean(score.get("verdict")) if score else "DRAFT"
|
| 171 |
+
overall = _clean(score.get("overall")) if score else "0.0"
|
| 172 |
+
lines.append(f"- {index}. {_clean(idea.get('title'))}: {verdict} {overall}/10")
|
| 173 |
+
else:
|
| 174 |
+
lines.append("- No ideas recorded yet.")
|
| 175 |
+
|
| 176 |
+
lines.extend(["", "### Latest Build Plan", ""])
|
| 177 |
+
if last_plan:
|
| 178 |
+
for index, step in enumerate(last_plan, start=1):
|
| 179 |
+
lines.append(f"{index}. {_clean(step)}")
|
| 180 |
+
else:
|
| 181 |
+
lines.append("No build plan recorded yet.")
|
| 182 |
+
|
| 183 |
+
lines.extend(["", "### Tool Trace", ""])
|
| 184 |
+
if trace:
|
| 185 |
+
for index, event in enumerate(trace[-5:], start=max(1, len(trace) - 4)):
|
| 186 |
+
tools = " -> ".join(
|
| 187 |
+
_clean(tool.get("name")) for tool in _list_of_dicts(event.get("tools")) if tool.get("name")
|
| 188 |
+
)
|
| 189 |
+
lines.append(f"- Turn {index}: {_clean(event.get('input'))} [{tools or 'reply'}]")
|
| 190 |
+
else:
|
| 191 |
+
lines.append("- No tool trace recorded yet.")
|
| 192 |
+
lines.append("")
|
| 193 |
+
return lines
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
def _social_post(title: str, verdict: str, ledger: dict[str, Any]) -> list[str]:
|
| 197 |
+
ready = sum(1 for badge in _list_of_dicts(ledger.get("badges")) if badge.get("status") == "ready")
|
| 198 |
+
total = len(_list_of_dicts(ledger.get("badges")))
|
| 199 |
+
return [
|
| 200 |
+
"## Social Post Draft",
|
| 201 |
+
"",
|
| 202 |
+
(
|
| 203 |
+
"I built Hackathon Advisor, a small-model originality coach for Build Small. "
|
| 204 |
+
f"The Unwritten Almanac checks `{title}` against a local snapshot of real Spaces, cites overlap, "
|
| 205 |
+
f"and exports the evidence trail. Latest seal: {verdict}. Prize ledger: {ready}/{total} ready."
|
| 206 |
+
),
|
| 207 |
+
"",
|
| 208 |
+
f"Live: {LIVE_URL}",
|
| 209 |
+
"",
|
| 210 |
+
]
|
| 211 |
+
|
| 212 |
+
|
| 213 |
+
def _open_gaps(ledger: dict[str, Any]) -> list[str]:
|
| 214 |
+
pending = [
|
| 215 |
+
badge
|
| 216 |
+
for badge in _list_of_dicts(ledger.get("badges"))
|
| 217 |
+
if badge.get("status") not in {"ready", "eligible"}
|
| 218 |
+
]
|
| 219 |
+
lines = ["## Open Gaps", ""]
|
| 220 |
+
if pending:
|
| 221 |
+
for badge in pending:
|
| 222 |
+
lines.append(f"- {_clean(badge.get('name'))}: {_clean(badge.get('status'))} - {_clean(badge.get('evidence'))}")
|
| 223 |
+
else:
|
| 224 |
+
lines.append("- No non-ready badge states reported by the current ledger.")
|
| 225 |
+
lines.append("")
|
| 226 |
+
return lines
|
| 227 |
+
|
| 228 |
+
|
| 229 |
+
def _current_idea(session: dict[str, Any], ideas: list[dict[str, Any]]) -> dict[str, Any]:
|
| 230 |
+
current_id = _clean(session.get("current_idea_id"))
|
| 231 |
+
if current_id:
|
| 232 |
+
for idea in ideas:
|
| 233 |
+
if _clean(idea.get("id")) == current_id:
|
| 234 |
+
return idea
|
| 235 |
+
if ideas:
|
| 236 |
+
return ideas[-1]
|
| 237 |
+
return {}
|
| 238 |
+
|
| 239 |
+
|
| 240 |
+
def _echoes(idea: dict[str, Any]) -> list[dict[str, Any]]:
|
| 241 |
+
score = idea.get("score") if isinstance(idea.get("score"), dict) else {}
|
| 242 |
+
return _list_of_dicts(score.get("echoes"))
|
| 243 |
+
|
| 244 |
+
|
| 245 |
+
def _list_of_dicts(value: Any) -> list[dict[str, Any]]:
|
| 246 |
+
if not isinstance(value, list):
|
| 247 |
+
return []
|
| 248 |
+
return [item for item in value if isinstance(item, dict)]
|
| 249 |
+
|
| 250 |
+
|
| 251 |
+
def _clean(value: Any) -> str:
|
| 252 |
+
if value is None:
|
| 253 |
+
return ""
|
| 254 |
+
return " ".join(str(value).split())
|
static/app.js
CHANGED
|
@@ -23,6 +23,7 @@ const exportTraceButton = document.querySelector("#export-trace");
|
|
| 23 |
const exportNotesButton = document.querySelector("#export-notes");
|
| 24 |
const exportChapterButton = document.querySelector("#export-chapter");
|
| 25 |
const exportLoraButton = document.querySelector("#export-lora");
|
|
|
|
| 26 |
const resetButton = document.querySelector("#reset-session");
|
| 27 |
|
| 28 |
const SESSION_STORAGE_KEY = "hackathon-advisor-session-v1";
|
|
@@ -71,6 +72,10 @@ exportLoraButton.addEventListener("click", async () => {
|
|
| 71 |
await exportLoraDataset();
|
| 72 |
});
|
| 73 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
resetButton.addEventListener("click", () => {
|
| 75 |
clearSavedSession();
|
| 76 |
window.location.reload();
|
|
@@ -189,6 +194,7 @@ function renderRestoredSession(data) {
|
|
| 189 |
exportNotesButton.disabled = !(session.trace?.length);
|
| 190 |
exportChapterButton.disabled = !(session.ideas?.length);
|
| 191 |
exportLoraButton.disabled = !(session.trace?.length);
|
|
|
|
| 192 |
}
|
| 193 |
|
| 194 |
function readSavedSession() {
|
|
@@ -372,6 +378,7 @@ function handleEvent(event) {
|
|
| 372 |
exportNotesButton.disabled = !(session.trace?.length);
|
| 373 |
exportChapterButton.disabled = !(session.ideas?.length);
|
| 374 |
exportLoraButton.disabled = !(session.trace?.length);
|
|
|
|
| 375 |
saveSession();
|
| 376 |
}
|
| 377 |
}
|
|
@@ -548,13 +555,15 @@ function setCommandDisabled(disabled) {
|
|
| 548 |
const isNotes = button.id === "export-notes";
|
| 549 |
const isChapter = button.id === "export-chapter";
|
| 550 |
const isLora = button.id === "export-lora";
|
|
|
|
| 551 |
button.disabled =
|
| 552 |
disabled ||
|
| 553 |
(isArtifact && !currentArtifact) ||
|
| 554 |
(isTrace && !session.trace?.length) ||
|
| 555 |
(isNotes && !session.trace?.length) ||
|
| 556 |
(isChapter && !session.ideas?.length) ||
|
| 557 |
-
(isLora && !session.trace?.length)
|
|
|
|
| 558 |
});
|
| 559 |
}
|
| 560 |
|
|
@@ -627,6 +636,15 @@ async function exportLoraDataset() {
|
|
| 627 |
downloadText("hackathon-advisor-lora-sft.jsonl", String(data || ""));
|
| 628 |
}
|
| 629 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 630 |
function exportArtifact(artifact) {
|
| 631 |
const canvas = document.createElement("canvas");
|
| 632 |
canvas.width = 1200;
|
|
|
|
| 23 |
const exportNotesButton = document.querySelector("#export-notes");
|
| 24 |
const exportChapterButton = document.querySelector("#export-chapter");
|
| 25 |
const exportLoraButton = document.querySelector("#export-lora");
|
| 26 |
+
const exportPacketButton = document.querySelector("#export-packet");
|
| 27 |
const resetButton = document.querySelector("#reset-session");
|
| 28 |
|
| 29 |
const SESSION_STORAGE_KEY = "hackathon-advisor-session-v1";
|
|
|
|
| 72 |
await exportLoraDataset();
|
| 73 |
});
|
| 74 |
|
| 75 |
+
exportPacketButton.addEventListener("click", async () => {
|
| 76 |
+
await exportSubmissionPacket();
|
| 77 |
+
});
|
| 78 |
+
|
| 79 |
resetButton.addEventListener("click", () => {
|
| 80 |
clearSavedSession();
|
| 81 |
window.location.reload();
|
|
|
|
| 194 |
exportNotesButton.disabled = !(session.trace?.length);
|
| 195 |
exportChapterButton.disabled = !(session.ideas?.length);
|
| 196 |
exportLoraButton.disabled = !(session.trace?.length);
|
| 197 |
+
exportPacketButton.disabled = !(session.trace?.length);
|
| 198 |
}
|
| 199 |
|
| 200 |
function readSavedSession() {
|
|
|
|
| 378 |
exportNotesButton.disabled = !(session.trace?.length);
|
| 379 |
exportChapterButton.disabled = !(session.ideas?.length);
|
| 380 |
exportLoraButton.disabled = !(session.trace?.length);
|
| 381 |
+
exportPacketButton.disabled = !(session.trace?.length);
|
| 382 |
saveSession();
|
| 383 |
}
|
| 384 |
}
|
|
|
|
| 555 |
const isNotes = button.id === "export-notes";
|
| 556 |
const isChapter = button.id === "export-chapter";
|
| 557 |
const isLora = button.id === "export-lora";
|
| 558 |
+
const isPacket = button.id === "export-packet";
|
| 559 |
button.disabled =
|
| 560 |
disabled ||
|
| 561 |
(isArtifact && !currentArtifact) ||
|
| 562 |
(isTrace && !session.trace?.length) ||
|
| 563 |
(isNotes && !session.trace?.length) ||
|
| 564 |
(isChapter && !session.ideas?.length) ||
|
| 565 |
+
(isLora && !session.trace?.length) ||
|
| 566 |
+
(isPacket && !session.trace?.length);
|
| 567 |
});
|
| 568 |
}
|
| 569 |
|
|
|
|
| 636 |
downloadText("hackathon-advisor-lora-sft.jsonl", String(data || ""));
|
| 637 |
}
|
| 638 |
|
| 639 |
+
async function exportSubmissionPacket() {
|
| 640 |
+
const client = await clientPromise;
|
| 641 |
+
const result = await client.predict("/submission_packet", {
|
| 642 |
+
session_json: JSON.stringify(session),
|
| 643 |
+
});
|
| 644 |
+
const data = Array.isArray(result.data) ? result.data[0] : result.data;
|
| 645 |
+
downloadText("hackathon-advisor-submission-packet.md", String(data || ""), "text/markdown;charset=utf-8");
|
| 646 |
+
}
|
| 647 |
+
|
| 648 |
function exportArtifact(artifact) {
|
| 649 |
const canvas = document.createElement("canvas");
|
| 650 |
canvas.width = 1200;
|
static/index.html
CHANGED
|
@@ -36,6 +36,7 @@
|
|
| 36 |
<button type="button" id="export-notes" title="Export Field Notes" disabled>Notes</button>
|
| 37 |
<button type="button" id="export-chapter" title="Export the Almanac chapter" disabled>Chapter</button>
|
| 38 |
<button type="button" id="export-lora" title="Export the LoRA SFT dataset" disabled>LoRA</button>
|
|
|
|
| 39 |
<button type="button" id="export-artifact" title="Export the current fate page" disabled>PNG</button>
|
| 40 |
<button type="button" id="reset-session" title="Clear the saved session">Reset</button>
|
| 41 |
</div>
|
|
|
|
| 36 |
<button type="button" id="export-notes" title="Export Field Notes" disabled>Notes</button>
|
| 37 |
<button type="button" id="export-chapter" title="Export the Almanac chapter" disabled>Chapter</button>
|
| 38 |
<button type="button" id="export-lora" title="Export the LoRA SFT dataset" disabled>LoRA</button>
|
| 39 |
+
<button type="button" id="export-packet" title="Export the submission packet" disabled>Packet</button>
|
| 40 |
<button type="button" id="export-artifact" title="Export the current fate page" disabled>PNG</button>
|
| 41 |
<button type="button" id="reset-session" title="Clear the saved session">Reset</button>
|
| 42 |
</div>
|
tests/test_app.py
CHANGED
|
@@ -10,6 +10,7 @@ from app import (
|
|
| 10 |
lora_dataset_artifact,
|
| 11 |
prize_ledger_endpoint,
|
| 12 |
runtime,
|
|
|
|
| 13 |
tool_contract_check,
|
| 14 |
tool_contracts,
|
| 15 |
trace_artifact,
|
|
@@ -94,6 +95,21 @@ def test_lora_dataset_endpoint_exports_sft_jsonl() -> None:
|
|
| 94 |
assert lines[2]["example_kind"] == "advisor_response"
|
| 95 |
|
| 96 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
def test_tool_contracts_endpoint_exposes_schemas() -> None:
|
| 98 |
payload = tool_contracts()
|
| 99 |
|
|
|
|
| 10 |
lora_dataset_artifact,
|
| 11 |
prize_ledger_endpoint,
|
| 12 |
runtime,
|
| 13 |
+
submission_packet_artifact,
|
| 14 |
tool_contract_check,
|
| 15 |
tool_contracts,
|
| 16 |
trace_artifact,
|
|
|
|
| 95 |
assert lines[2]["example_kind"] == "advisor_response"
|
| 96 |
|
| 97 |
|
| 98 |
+
def test_submission_packet_endpoint_exports_markdown() -> None:
|
| 99 |
+
state = engine.turn(
|
| 100 |
+
"A local-first archive cartographer for family photos",
|
| 101 |
+
{"targets": ["Field Notes"]},
|
| 102 |
+
).state
|
| 103 |
+
state = engine.turn("make a build plan", state).state
|
| 104 |
+
|
| 105 |
+
payload = submission_packet_artifact(json.dumps(state))
|
| 106 |
+
|
| 107 |
+
assert payload.startswith("# Hackathon Advisor Submission Packet")
|
| 108 |
+
assert "## Demo Script" in payload
|
| 109 |
+
assert "## Prize Evidence" in payload
|
| 110 |
+
assert "Live Space:" in payload
|
| 111 |
+
|
| 112 |
+
|
| 113 |
def test_tool_contracts_endpoint_exposes_schemas() -> None:
|
| 114 |
payload = tool_contracts()
|
| 115 |
|
tests/test_submission_packet.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pathlib import Path
|
| 2 |
+
|
| 3 |
+
from hackathon_advisor.agent import AdvisorEngine
|
| 4 |
+
from hackathon_advisor.data import ProjectIndex
|
| 5 |
+
from hackathon_advisor.prize_ledger import prize_ledger
|
| 6 |
+
from hackathon_advisor.submission_packet import build_submission_packet_markdown
|
| 7 |
+
from hackathon_advisor.trace_export import trace_metadata
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def test_submission_packet_contains_demo_and_prize_evidence() -> None:
|
| 11 |
+
index = ProjectIndex.from_files(Path("data/projects.json"), Path("data/project_index.json"))
|
| 12 |
+
engine = AdvisorEngine(index)
|
| 13 |
+
state = {"targets": ["Well-Tuned", "Field Notes"]}
|
| 14 |
+
state = engine.turn("A local-first archive cartographer for family photos", state).state
|
| 15 |
+
state = engine.turn("make a build plan", state).state
|
| 16 |
+
|
| 17 |
+
markdown = build_submission_packet_markdown(
|
| 18 |
+
state,
|
| 19 |
+
{
|
| 20 |
+
**trace_metadata(index),
|
| 21 |
+
"project_count": len(index.projects),
|
| 22 |
+
},
|
| 23 |
+
prize_ledger(engine.runtime_status()),
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
assert markdown.startswith("# Hackathon Advisor Submission Packet")
|
| 27 |
+
assert "## Demo Script" in markdown
|
| 28 |
+
assert "## Artifact Checklist" in markdown
|
| 29 |
+
assert "## Prize Evidence" in markdown
|
| 30 |
+
assert "## Model Budget" in markdown
|
| 31 |
+
assert "## Social Post Draft" in markdown
|
| 32 |
+
assert "Hackathon Advisor" in markdown
|
| 33 |
+
assert "Well-Tuned | dataset-ready" in markdown
|
| 34 |
+
assert "MiniCPM5 LoRA SFT JSONL | ready | lora_dataset" in markdown
|
| 35 |
+
assert "Ready badges and planned badges are separated" in markdown
|
| 36 |
+
assert "A local-first archive cartographer for family photos" in markdown
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def test_empty_submission_packet_is_honest_about_missing_session_artifacts() -> None:
|
| 40 |
+
markdown = build_submission_packet_markdown(
|
| 41 |
+
{},
|
| 42 |
+
{
|
| 43 |
+
"snapshot_generated_at": "2026-06-06T00:00:00+00:00",
|
| 44 |
+
"project_count": 100,
|
| 45 |
+
"index_algorithm": "tfidf-sparse-v1",
|
| 46 |
+
"index_generated_at": "2026-06-06T01:00:00+00:00",
|
| 47 |
+
"snapshot_digest": "abc",
|
| 48 |
+
},
|
| 49 |
+
prize_ledger({"backend": "rules", "model_id": "deterministic-tool-router"}),
|
| 50 |
+
)
|
| 51 |
+
|
| 52 |
+
assert "Title: Unwritten Page" in markdown
|
| 53 |
+
assert "Tool trace JSONL | needs session" in markdown
|
| 54 |
+
assert "Submission packet markdown | ready" in markdown
|
| 55 |
+
assert "No ideas recorded yet." in markdown
|