Spaces:
Running on Zero
Running on Zero
feat: export shareable agent traces
Browse filesCo-authored-by: Codex <noreply@openai.com>
- README.md +6 -0
- app.py +12 -8
- data/sample_trace.jsonl +4 -0
- hackathon_advisor/agent.py +29 -19
- hackathon_advisor/trace_export.py +66 -0
- scripts/generate_sample_trace.py +43 -0
- static/app.js +27 -1
- static/index.html +1 -0
- static/styles.css +2 -2
- tests/test_agent.py +13 -0
- tests/test_app.py +13 -1
- tests/test_trace_export.py +33 -0
README.md
CHANGED
|
@@ -52,11 +52,17 @@ Then open <http://127.0.0.1:7860>.
|
|
| 52 |
```bash
|
| 53 |
python scripts/crawl_hf_spaces.py --org build-small-hackathon --out data/projects.json
|
| 54 |
python scripts/build_project_index.py --projects data/projects.json --out data/project_index.json
|
|
|
|
| 55 |
```
|
| 56 |
|
| 57 |
The app uses `data/projects.json` and `data/project_index.json` at runtime. The index validates the snapshot timestamp,
|
| 58 |
source, project order, and digest before the app starts.
|
| 59 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
## Test
|
| 61 |
|
| 62 |
```bash
|
|
|
|
| 52 |
```bash
|
| 53 |
python scripts/crawl_hf_spaces.py --org build-small-hackathon --out data/projects.json
|
| 54 |
python scripts/build_project_index.py --projects data/projects.json --out data/project_index.json
|
| 55 |
+
python scripts/generate_sample_trace.py --projects data/projects.json --index data/project_index.json --out data/sample_trace.jsonl
|
| 56 |
```
|
| 57 |
|
| 58 |
The app uses `data/projects.json` and `data/project_index.json` at runtime. The index validates the snapshot timestamp,
|
| 59 |
source, project order, and digest before the app starts.
|
| 60 |
|
| 61 |
+
## Trace Artifact
|
| 62 |
+
|
| 63 |
+
The app exposes a `trace_artifact` Gradio API endpoint and a `JSONL` button in the UI. Both emit the same JSONL schema:
|
| 64 |
+
a manifest row followed by one row per agent turn. `data/sample_trace.jsonl` is a checked-in, Hub-published sample trace.
|
| 65 |
+
|
| 66 |
## Test
|
| 67 |
|
| 68 |
```bash
|
app.py
CHANGED
|
@@ -10,6 +10,7 @@ from gradio import Server
|
|
| 10 |
|
| 11 |
from hackathon_advisor.agent import AdvisorEngine
|
| 12 |
from hackathon_advisor.data import ProjectIndex
|
|
|
|
| 13 |
|
| 14 |
|
| 15 |
ROOT = Path(__file__).parent
|
|
@@ -44,10 +45,7 @@ def health() -> dict:
|
|
| 44 |
return {
|
| 45 |
"ok": True,
|
| 46 |
"projects": len(index.projects),
|
| 47 |
-
|
| 48 |
-
"index_generated_at": index.index_generated_at,
|
| 49 |
-
"index_algorithm": index.index_algorithm,
|
| 50 |
-
"snapshot_digest": index.snapshot_digest,
|
| 51 |
}
|
| 52 |
|
| 53 |
|
|
@@ -55,15 +53,21 @@ def health() -> dict:
|
|
| 55 |
def bootstrap() -> dict:
|
| 56 |
return {
|
| 57 |
"project_count": len(index.projects),
|
| 58 |
-
|
| 59 |
-
"index_generated_at": index.index_generated_at,
|
| 60 |
-
"index_algorithm": index.index_algorithm,
|
| 61 |
-
"snapshot_digest": index.snapshot_digest,
|
| 62 |
"top_projects": [project.to_public_dict() for project in index.top_projects(limit=8)],
|
| 63 |
"whitespace": [item.to_dict() for item in index.find_whitespace(limit=5)],
|
| 64 |
}
|
| 65 |
|
| 66 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
@app.api(name="agent_turn", concurrency_limit=4, stream_every=0.04)
|
| 68 |
def agent_turn(message: str, session_json: str = "{}") -> Iterator[str]:
|
| 69 |
try:
|
|
|
|
| 10 |
|
| 11 |
from hackathon_advisor.agent import AdvisorEngine
|
| 12 |
from hackathon_advisor.data import ProjectIndex
|
| 13 |
+
from hackathon_advisor.trace_export import build_trace_jsonl, trace_metadata
|
| 14 |
|
| 15 |
|
| 16 |
ROOT = Path(__file__).parent
|
|
|
|
| 45 |
return {
|
| 46 |
"ok": True,
|
| 47 |
"projects": len(index.projects),
|
| 48 |
+
**trace_metadata(index),
|
|
|
|
|
|
|
|
|
|
| 49 |
}
|
| 50 |
|
| 51 |
|
|
|
|
| 53 |
def bootstrap() -> dict:
|
| 54 |
return {
|
| 55 |
"project_count": len(index.projects),
|
| 56 |
+
**trace_metadata(index),
|
|
|
|
|
|
|
|
|
|
| 57 |
"top_projects": [project.to_public_dict() for project in index.top_projects(limit=8)],
|
| 58 |
"whitespace": [item.to_dict() for item in index.find_whitespace(limit=5)],
|
| 59 |
}
|
| 60 |
|
| 61 |
|
| 62 |
+
@app.api(name="trace_artifact", concurrency_limit=8)
|
| 63 |
+
def trace_artifact(session_json: str = "{}") -> str:
|
| 64 |
+
try:
|
| 65 |
+
session = json.loads(session_json or "{}")
|
| 66 |
+
except json.JSONDecodeError:
|
| 67 |
+
session = {}
|
| 68 |
+
return build_trace_jsonl(session, trace_metadata(index))
|
| 69 |
+
|
| 70 |
+
|
| 71 |
@app.api(name="agent_turn", concurrency_limit=4, stream_every=0.04)
|
| 72 |
def agent_turn(message: str, session_json: str = "{}") -> Iterator[str]:
|
| 73 |
try:
|
data/sample_trace.jsonl
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{"app": "hackathon-advisor", "generated_at": "2026-06-06T19:52:06+00:00", "idea_count": 1, "index": {"algorithm": "tfidf-sparse-v1", "index_generated_at": "2026-06-06T19:44:42+00:00", "snapshot_digest": "1d59c168b446bc1268ef648897fac523e85b258fd6048a37409b9cfbc5c28329", "snapshot_generated_at": "2026-06-06T19:20:47+00:00"}, "schema_version": 1, "turn_count": 3, "type": "trace_manifest"}
|
| 2 |
+
{"artifact_title": "A local-first archive cartographer for family photos", "input": "A local-first archive cartographer for family photos", "overall": 4.9, "plan_steps": 0, "response": "The ink bleeds around A local-first archive cartographer for family photos. Closest echoes: page 1: Family Bill Assistant; page 2: Kasualdad LFED; page 3: Mycelium. The seal reads ECHO x4 at 4.9/10. Keep the audience, but change the mechanism or artifact so the demo proves a gap instead of joining a cluster.", "schema_version": 1, "tools": [{"name": "save_idea", "summary": "Wrote idea page 'A local-first archive cartographer for family photos'."}, {"name": "search_projects", "summary": "Checked 5 closest project echoes."}, {"name": "score_idea", "summary": "Pressed a five-quadrant seal: 4.9/10."}], "turn_index": 1, "type": "agent_turn", "verdict": "ECHO x4"}
|
| 3 |
+
{"artifact_title": "Hands-on science coach", "input": "write bolder and find whitespace", "overall": 6.0, "plan_steps": 0, "response": "Gold gathers on Hands-on science coach. A lab-notebook companion that designs safe experiments from household materials. No close project echoes in the current snapshot. The seal reads UNWRITTEN at 6.0/10. The next move is to make one concrete before/after scene and cite the two weakest nearby echoes in the margin.", "schema_version": 1, "tools": [{"name": "find_whitespace", "summary": "Ranked 4 under-explored regions."}, {"name": "save_idea", "summary": "Wrote idea page 'Hands-on science coach'."}, {"name": "score_idea", "summary": "Pressed a five-quadrant seal: 4.8/10."}], "turn_index": 2, "type": "agent_turn", "verdict": "UNWRITTEN"}
|
| 4 |
+
{"artifact_title": "Hands-on science coach", "input": "make a build plan", "overall": 6.0, "plan_steps": 6, "response": "Mothback presses the wax for Hands-on science coach: 6.0/10, UNWRITTEN. The build path is: 1. Lock a one-sentence promise and one demo input that proves originality. 2. Refresh the Space snapshot, then tune the bleed threshold against the closest echoes. 3. Build the smallest happy path: input, citations, score seal, and shareable artifact. 4. Add one prize ", "schema_version": 1, "tools": [{"name": "score_idea", "summary": "Pressed a five-quadrant seal: 4.8/10."}, {"name": "make_plan", "summary": "Drafted 6 build steps."}], "turn_index": 3, "type": "agent_turn", "verdict": "UNWRITTEN"}
|
hackathon_advisor/agent.py
CHANGED
|
@@ -74,6 +74,8 @@ class AdvisorEngine:
|
|
| 74 |
idea = self._current_idea(state)
|
| 75 |
if idea is not None:
|
| 76 |
score, event = self.tools.score_idea(idea)
|
|
|
|
|
|
|
| 77 |
self._store_idea(state, idea)
|
| 78 |
tool_events.append(event)
|
| 79 |
plan, event = self.tools.make_plan(idea)
|
|
@@ -93,29 +95,17 @@ class AdvisorEngine:
|
|
| 93 |
artifact,
|
| 94 |
)
|
| 95 |
|
| 96 |
-
title, pitch = idea_from_text(normalized)
|
| 97 |
-
idea, event = self.tools.save_idea(state, title, pitch)
|
| 98 |
-
tool_events.append(event)
|
| 99 |
-
|
| 100 |
-
if PLAN_RE.search(normalized):
|
| 101 |
-
score, event = self.tools.score_idea(idea)
|
| 102 |
-
self._store_idea(state, idea)
|
| 103 |
-
tool_events.append(event)
|
| 104 |
-
plan, event = self.tools.make_plan(idea)
|
| 105 |
-
tool_events.append(event)
|
| 106 |
-
response = self._plan_response(idea, score, plan)
|
| 107 |
-
artifact = self._artifact(idea, score)
|
| 108 |
-
return self._result(normalized, corrections, response, state, tool_events, [], [], score, plan, artifact)
|
| 109 |
-
|
| 110 |
if WHITESPACE_RE.search(normalized):
|
| 111 |
whitespace, event = self.tools.find_whitespace(limit=4)
|
| 112 |
tool_events.append(event)
|
| 113 |
if whitespace:
|
| 114 |
-
idea
|
| 115 |
-
|
| 116 |
-
state["
|
| 117 |
-
|
| 118 |
-
|
|
|
|
|
|
|
| 119 |
score, event = self.tools.score_idea(idea)
|
| 120 |
if whitespace:
|
| 121 |
score = self._align_score_with_whitespace(score, whitespace[0])
|
|
@@ -137,6 +127,20 @@ class AdvisorEngine:
|
|
| 137 |
artifact,
|
| 138 |
)
|
| 139 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
hits = self.index.search(normalized, limit=5)
|
| 141 |
projects = [hit.project for hit in hits]
|
| 142 |
tool_events.append(ToolEvent("search_projects", f"Checked {len(projects)} closest project echoes."))
|
|
@@ -241,6 +245,12 @@ class AdvisorEngine:
|
|
| 241 |
verdict="UNWRITTEN",
|
| 242 |
)
|
| 243 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 244 |
def _opening_response(self, projects: list[Project]) -> str:
|
| 245 |
names = ", ".join(project.title for project in projects[:4])
|
| 246 |
return (
|
|
|
|
| 74 |
idea = self._current_idea(state)
|
| 75 |
if idea is not None:
|
| 76 |
score, event = self.tools.score_idea(idea)
|
| 77 |
+
score = self._align_score_from_state(score, idea, state)
|
| 78 |
+
idea.score = score.to_dict()
|
| 79 |
self._store_idea(state, idea)
|
| 80 |
tool_events.append(event)
|
| 81 |
plan, event = self.tools.make_plan(idea)
|
|
|
|
| 95 |
artifact,
|
| 96 |
)
|
| 97 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
if WHITESPACE_RE.search(normalized):
|
| 99 |
whitespace, event = self.tools.find_whitespace(limit=4)
|
| 100 |
tool_events.append(event)
|
| 101 |
if whitespace:
|
| 102 |
+
idea, event = self.tools.save_idea(state, whitespace[0].label, whitespace[0].pitch)
|
| 103 |
+
tool_events.append(event)
|
| 104 |
+
state["current_whitespace"] = whitespace[0].to_dict()
|
| 105 |
+
else:
|
| 106 |
+
title, pitch = idea_from_text(normalized)
|
| 107 |
+
idea, event = self.tools.save_idea(state, title, pitch)
|
| 108 |
+
tool_events.append(event)
|
| 109 |
score, event = self.tools.score_idea(idea)
|
| 110 |
if whitespace:
|
| 111 |
score = self._align_score_with_whitespace(score, whitespace[0])
|
|
|
|
| 127 |
artifact,
|
| 128 |
)
|
| 129 |
|
| 130 |
+
title, pitch = idea_from_text(normalized)
|
| 131 |
+
idea, event = self.tools.save_idea(state, title, pitch)
|
| 132 |
+
tool_events.append(event)
|
| 133 |
+
|
| 134 |
+
if PLAN_RE.search(normalized):
|
| 135 |
+
score, event = self.tools.score_idea(idea)
|
| 136 |
+
self._store_idea(state, idea)
|
| 137 |
+
tool_events.append(event)
|
| 138 |
+
plan, event = self.tools.make_plan(idea)
|
| 139 |
+
tool_events.append(event)
|
| 140 |
+
response = self._plan_response(idea, score, plan)
|
| 141 |
+
artifact = self._artifact(idea, score)
|
| 142 |
+
return self._result(normalized, corrections, response, state, tool_events, [], [], score, plan, artifact)
|
| 143 |
+
|
| 144 |
hits = self.index.search(normalized, limit=5)
|
| 145 |
projects = [hit.project for hit in hits]
|
| 146 |
tool_events.append(ToolEvent("search_projects", f"Checked {len(projects)} closest project echoes."))
|
|
|
|
| 245 |
verdict="UNWRITTEN",
|
| 246 |
)
|
| 247 |
|
| 248 |
+
def _align_score_from_state(self, score: ScoreCard, idea: Idea, state: dict[str, Any]) -> ScoreCard:
|
| 249 |
+
artifact = state.get("last_artifact") or {}
|
| 250 |
+
if artifact.get("title") == idea.title and artifact.get("verdict") == "UNWRITTEN":
|
| 251 |
+
return replace(score, originality=max(score.originality, 8), verdict="UNWRITTEN")
|
| 252 |
+
return score
|
| 253 |
+
|
| 254 |
def _opening_response(self, projects: list[Project]) -> str:
|
| 255 |
names = ", ".join(project.title for project in projects[:4])
|
| 256 |
return (
|
hackathon_advisor/trace_export.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from datetime import datetime, timezone
|
| 4 |
+
import json
|
| 5 |
+
from typing import Any
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
TRACE_SCHEMA_VERSION = 1
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def build_trace_jsonl(session: dict[str, Any], metadata: dict[str, Any]) -> str:
|
| 12 |
+
trace = session.get("trace") or []
|
| 13 |
+
ideas = session.get("ideas") or []
|
| 14 |
+
records = [
|
| 15 |
+
{
|
| 16 |
+
"type": "trace_manifest",
|
| 17 |
+
"schema_version": TRACE_SCHEMA_VERSION,
|
| 18 |
+
"generated_at": datetime.now(timezone.utc).isoformat(timespec="seconds"),
|
| 19 |
+
"app": "hackathon-advisor",
|
| 20 |
+
"index": {
|
| 21 |
+
"algorithm": metadata["index_algorithm"],
|
| 22 |
+
"snapshot_generated_at": metadata["snapshot_generated_at"],
|
| 23 |
+
"index_generated_at": metadata["index_generated_at"],
|
| 24 |
+
"snapshot_digest": metadata["snapshot_digest"],
|
| 25 |
+
},
|
| 26 |
+
"idea_count": len(ideas),
|
| 27 |
+
"turn_count": len(trace),
|
| 28 |
+
}
|
| 29 |
+
]
|
| 30 |
+
for index, event in enumerate(trace, start=1):
|
| 31 |
+
records.append(
|
| 32 |
+
{
|
| 33 |
+
"type": "agent_turn",
|
| 34 |
+
"schema_version": TRACE_SCHEMA_VERSION,
|
| 35 |
+
"turn_index": index,
|
| 36 |
+
"input": str(event.get("input") or ""),
|
| 37 |
+
"tools": _tools(event),
|
| 38 |
+
"verdict": str(event.get("verdict") or ""),
|
| 39 |
+
"overall": event.get("overall"),
|
| 40 |
+
"plan_steps": int(event.get("plan_steps") or 0),
|
| 41 |
+
"artifact_title": str(event.get("artifact_title") or ""),
|
| 42 |
+
"response": str(event.get("response") or ""),
|
| 43 |
+
}
|
| 44 |
+
)
|
| 45 |
+
return "\n".join(json.dumps(record, ensure_ascii=False, sort_keys=True) for record in records) + "\n"
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def trace_metadata(index: Any) -> dict[str, str]:
|
| 49 |
+
return {
|
| 50 |
+
"snapshot_generated_at": index.generated_at,
|
| 51 |
+
"index_generated_at": index.index_generated_at,
|
| 52 |
+
"index_algorithm": index.index_algorithm,
|
| 53 |
+
"snapshot_digest": index.snapshot_digest,
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def _tools(event: dict[str, Any]) -> list[dict[str, str]]:
|
| 58 |
+
tools = event.get("tools") or []
|
| 59 |
+
return [
|
| 60 |
+
{
|
| 61 |
+
"name": str(tool.get("name") or ""),
|
| 62 |
+
"summary": str(tool.get("summary") or ""),
|
| 63 |
+
}
|
| 64 |
+
for tool in tools
|
| 65 |
+
if isinstance(tool, dict)
|
| 66 |
+
]
|
scripts/generate_sample_trace.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import argparse
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
import sys
|
| 7 |
+
|
| 8 |
+
ROOT = Path(__file__).resolve().parents[1]
|
| 9 |
+
sys.path.insert(0, str(ROOT))
|
| 10 |
+
|
| 11 |
+
from hackathon_advisor.agent import AdvisorEngine
|
| 12 |
+
from hackathon_advisor.data import ProjectIndex
|
| 13 |
+
from hackathon_advisor.trace_export import build_trace_jsonl, trace_metadata
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
SAMPLE_TURNS = (
|
| 17 |
+
"A local-first archive cartographer for family photos",
|
| 18 |
+
"write bolder and find whitespace",
|
| 19 |
+
"make a build plan",
|
| 20 |
+
)
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def main() -> None:
|
| 24 |
+
parser = argparse.ArgumentParser(description="Generate a publishable sample agent trace JSONL.")
|
| 25 |
+
parser.add_argument("--projects", default="data/projects.json")
|
| 26 |
+
parser.add_argument("--index", default="data/project_index.json")
|
| 27 |
+
parser.add_argument("--out", default="data/sample_trace.jsonl")
|
| 28 |
+
args = parser.parse_args()
|
| 29 |
+
|
| 30 |
+
index = ProjectIndex.from_files(Path(args.projects), Path(args.index))
|
| 31 |
+
engine = AdvisorEngine(index)
|
| 32 |
+
state = {}
|
| 33 |
+
for turn in SAMPLE_TURNS:
|
| 34 |
+
state = engine.turn(turn, state).state
|
| 35 |
+
|
| 36 |
+
output = Path(args.out)
|
| 37 |
+
output.parent.mkdir(parents=True, exist_ok=True)
|
| 38 |
+
output.write_text(build_trace_jsonl(state, trace_metadata(index)), encoding="utf-8")
|
| 39 |
+
print(f"wrote {len(state.get('trace', []))} turns to {output}")
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
if __name__ == "__main__":
|
| 43 |
+
main()
|
static/app.js
CHANGED
|
@@ -15,6 +15,7 @@ const provenanceEl = document.querySelector("#provenance");
|
|
| 15 |
const verdictEl = document.querySelector("#verdict");
|
| 16 |
const overallEl = document.querySelector("#overall");
|
| 17 |
const exportButton = document.querySelector("#export-artifact");
|
|
|
|
| 18 |
|
| 19 |
let session = {};
|
| 20 |
let clientPromise = Client.connect(window.location.origin);
|
|
@@ -40,6 +41,10 @@ exportButton.addEventListener("click", () => {
|
|
| 40 |
exportArtifact(currentArtifact);
|
| 41 |
});
|
| 42 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
async function runTurn(message) {
|
| 44 |
input.value = "";
|
| 45 |
submit.disabled = true;
|
|
@@ -125,6 +130,7 @@ function handleEvent(event) {
|
|
| 125 |
currentArtifact = event.artifact;
|
| 126 |
exportButton.disabled = false;
|
| 127 |
}
|
|
|
|
| 128 |
}
|
| 129 |
}
|
| 130 |
|
|
@@ -238,8 +244,19 @@ function renderTrace(trace) {
|
|
| 238 |
|
| 239 |
function setCommandDisabled(disabled) {
|
| 240 |
document.querySelectorAll(".command-row button").forEach((button) => {
|
| 241 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 242 |
});
|
|
|
|
|
|
|
| 243 |
}
|
| 244 |
|
| 245 |
function exportArtifact(artifact) {
|
|
@@ -297,6 +314,15 @@ function exportArtifact(artifact) {
|
|
| 297 |
link.click();
|
| 298 |
}
|
| 299 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 300 |
function drawParchment(ctx, width, height) {
|
| 301 |
const gradient = ctx.createLinearGradient(0, 0, width, height);
|
| 302 |
gradient.addColorStop(0, "#ead7a7");
|
|
|
|
| 15 |
const verdictEl = document.querySelector("#verdict");
|
| 16 |
const overallEl = document.querySelector("#overall");
|
| 17 |
const exportButton = document.querySelector("#export-artifact");
|
| 18 |
+
const exportTraceButton = document.querySelector("#export-trace");
|
| 19 |
|
| 20 |
let session = {};
|
| 21 |
let clientPromise = Client.connect(window.location.origin);
|
|
|
|
| 41 |
exportArtifact(currentArtifact);
|
| 42 |
});
|
| 43 |
|
| 44 |
+
exportTraceButton.addEventListener("click", async () => {
|
| 45 |
+
await exportTrace();
|
| 46 |
+
});
|
| 47 |
+
|
| 48 |
async function runTurn(message) {
|
| 49 |
input.value = "";
|
| 50 |
submit.disabled = true;
|
|
|
|
| 130 |
currentArtifact = event.artifact;
|
| 131 |
exportButton.disabled = false;
|
| 132 |
}
|
| 133 |
+
exportTraceButton.disabled = !(session.trace?.length);
|
| 134 |
}
|
| 135 |
}
|
| 136 |
|
|
|
|
| 244 |
|
| 245 |
function setCommandDisabled(disabled) {
|
| 246 |
document.querySelectorAll(".command-row button").forEach((button) => {
|
| 247 |
+
const isArtifact = button.id === "export-artifact";
|
| 248 |
+
const isTrace = button.id === "export-trace";
|
| 249 |
+
button.disabled = disabled || (isArtifact && !currentArtifact) || (isTrace && !session.trace?.length);
|
| 250 |
+
});
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
async function exportTrace() {
|
| 254 |
+
const client = await clientPromise;
|
| 255 |
+
const result = await client.predict("/trace_artifact", {
|
| 256 |
+
session_json: JSON.stringify(session),
|
| 257 |
});
|
| 258 |
+
const data = Array.isArray(result.data) ? result.data[0] : result.data;
|
| 259 |
+
downloadText("hackathon-advisor-trace.jsonl", String(data || ""));
|
| 260 |
}
|
| 261 |
|
| 262 |
function exportArtifact(artifact) {
|
|
|
|
| 314 |
link.click();
|
| 315 |
}
|
| 316 |
|
| 317 |
+
function downloadText(filename, text) {
|
| 318 |
+
const blob = new Blob([text], { type: "application/jsonl;charset=utf-8" });
|
| 319 |
+
const link = document.createElement("a");
|
| 320 |
+
link.download = filename;
|
| 321 |
+
link.href = URL.createObjectURL(blob);
|
| 322 |
+
link.click();
|
| 323 |
+
setTimeout(() => URL.revokeObjectURL(link.href), 0);
|
| 324 |
+
}
|
| 325 |
+
|
| 326 |
function drawParchment(ctx, width, height) {
|
| 327 |
const gradient = ctx.createLinearGradient(0, 0, width, height);
|
| 328 |
gradient.addColorStop(0, "#ead7a7");
|
static/index.html
CHANGED
|
@@ -32,6 +32,7 @@
|
|
| 32 |
</button>
|
| 33 |
<button type="button" data-command="make a build plan" title="Draft a build plan">Plan</button>
|
| 34 |
<button type="button" data-command="compare ideas" title="Compare the idea board">Rank</button>
|
|
|
|
| 35 |
<button type="button" id="export-artifact" title="Export the current fate page" disabled>PNG</button>
|
| 36 |
</div>
|
| 37 |
<div id="corrections" class="corrections" aria-live="polite"></div>
|
|
|
|
| 32 |
</button>
|
| 33 |
<button type="button" data-command="make a build plan" title="Draft a build plan">Plan</button>
|
| 34 |
<button type="button" data-command="compare ideas" title="Compare the idea board">Rank</button>
|
| 35 |
+
<button type="button" id="export-trace" title="Export the tool trace" disabled>JSONL</button>
|
| 36 |
<button type="button" id="export-artifact" title="Export the current fate page" disabled>PNG</button>
|
| 37 |
</div>
|
| 38 |
<div id="corrections" class="corrections" aria-live="polite"></div>
|
static/styles.css
CHANGED
|
@@ -167,7 +167,7 @@ button:disabled {
|
|
| 167 |
|
| 168 |
.command-row {
|
| 169 |
display: grid;
|
| 170 |
-
grid-template-columns: repeat(
|
| 171 |
gap: 8px;
|
| 172 |
margin-top: 10px;
|
| 173 |
}
|
|
@@ -371,7 +371,7 @@ button:disabled {
|
|
| 371 |
}
|
| 372 |
|
| 373 |
.command-row {
|
| 374 |
-
grid-template-columns: repeat(
|
| 375 |
}
|
| 376 |
|
| 377 |
.score-row {
|
|
|
|
| 167 |
|
| 168 |
.command-row {
|
| 169 |
display: grid;
|
| 170 |
+
grid-template-columns: repeat(5, minmax(0, 1fr));
|
| 171 |
gap: 8px;
|
| 172 |
margin-top: 10px;
|
| 173 |
}
|
|
|
|
| 371 |
}
|
| 372 |
|
| 373 |
.command-row {
|
| 374 |
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
| 375 |
}
|
| 376 |
|
| 377 |
.score-row {
|
tests/test_agent.py
CHANGED
|
@@ -27,6 +27,7 @@ def test_agent_finds_whitespace() -> None:
|
|
| 27 |
assert result.whitespace
|
| 28 |
assert result.score is not None
|
| 29 |
assert result.artifact["verdict"] == "UNWRITTEN"
|
|
|
|
| 30 |
|
| 31 |
|
| 32 |
def test_agent_preserves_canonical_jargon_case() -> None:
|
|
@@ -49,3 +50,15 @@ def test_plan_command_uses_current_idea() -> None:
|
|
| 49 |
assert planned.plan
|
| 50 |
assert planned.artifact["title"] == first.artifact["title"]
|
| 51 |
assert planned.state["ideas"][0]["title"] == first.artifact["title"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
assert result.whitespace
|
| 28 |
assert result.score is not None
|
| 29 |
assert result.artifact["verdict"] == "UNWRITTEN"
|
| 30 |
+
assert result.state["ideas"][0]["title"] == result.whitespace[0].label
|
| 31 |
|
| 32 |
|
| 33 |
def test_agent_preserves_canonical_jargon_case() -> None:
|
|
|
|
| 50 |
assert planned.plan
|
| 51 |
assert planned.artifact["title"] == first.artifact["title"]
|
| 52 |
assert planned.state["ideas"][0]["title"] == first.artifact["title"]
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def test_plan_preserves_unwritten_whitespace_verdict() -> None:
|
| 56 |
+
index = ProjectIndex.from_files(Path("data/projects.json"), Path("data/project_index.json"))
|
| 57 |
+
engine = AdvisorEngine(index)
|
| 58 |
+
|
| 59 |
+
whitespace = engine.turn("write bolder and find whitespace", {})
|
| 60 |
+
planned = engine.turn("make a build plan", whitespace.state)
|
| 61 |
+
|
| 62 |
+
assert whitespace.artifact["verdict"] == "UNWRITTEN"
|
| 63 |
+
assert planned.artifact["title"] == whitespace.artifact["title"]
|
| 64 |
+
assert planned.artifact["verdict"] == "UNWRITTEN"
|
tests/test_app.py
CHANGED
|
@@ -1,4 +1,6 @@
|
|
| 1 |
-
|
|
|
|
|
|
|
| 2 |
|
| 3 |
|
| 4 |
def test_health_exposes_index_metadata() -> None:
|
|
@@ -17,3 +19,13 @@ def test_bootstrap_exposes_index_metadata() -> None:
|
|
| 17 |
assert payload["index_generated_at"]
|
| 18 |
assert payload["snapshot_digest"]
|
| 19 |
assert payload["top_projects"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
|
| 3 |
+
from app import bootstrap, engine, health, index, trace_artifact
|
| 4 |
|
| 5 |
|
| 6 |
def test_health_exposes_index_metadata() -> None:
|
|
|
|
| 19 |
assert payload["index_generated_at"]
|
| 20 |
assert payload["snapshot_digest"]
|
| 21 |
assert payload["top_projects"]
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def test_trace_artifact_endpoint_exports_jsonl() -> None:
|
| 25 |
+
state = engine.turn("A local-first archive cartographer for family photos", {}).state
|
| 26 |
+
payload = trace_artifact(json.dumps(state))
|
| 27 |
+
lines = [json.loads(line) for line in payload.splitlines()]
|
| 28 |
+
|
| 29 |
+
assert lines[0]["type"] == "trace_manifest"
|
| 30 |
+
assert lines[0]["turn_count"] == 1
|
| 31 |
+
assert lines[1]["type"] == "agent_turn"
|
tests/test_trace_export.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
|
| 4 |
+
from hackathon_advisor.agent import AdvisorEngine
|
| 5 |
+
from hackathon_advisor.data import ProjectIndex
|
| 6 |
+
from hackathon_advisor.trace_export import build_trace_jsonl, trace_metadata
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def test_trace_jsonl_contains_manifest_and_turns() -> None:
|
| 10 |
+
index = ProjectIndex.from_files(Path("data/projects.json"), Path("data/project_index.json"))
|
| 11 |
+
engine = AdvisorEngine(index)
|
| 12 |
+
state = engine.turn("A local-first archive cartographer for family photos", {}).state
|
| 13 |
+
state = engine.turn("make a build plan", state).state
|
| 14 |
+
|
| 15 |
+
lines = [json.loads(line) for line in build_trace_jsonl(state, trace_metadata(index)).splitlines()]
|
| 16 |
+
|
| 17 |
+
assert lines[0]["type"] == "trace_manifest"
|
| 18 |
+
assert lines[0]["turn_count"] == 2
|
| 19 |
+
assert lines[0]["index"]["algorithm"] == "tfidf-sparse-v1"
|
| 20 |
+
assert lines[1]["type"] == "agent_turn"
|
| 21 |
+
assert lines[1]["tools"]
|
| 22 |
+
assert lines[2]["plan_steps"] > 0
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def test_checked_in_sample_trace_matches_schema() -> None:
|
| 26 |
+
lines = [
|
| 27 |
+
json.loads(line)
|
| 28 |
+
for line in Path("data/sample_trace.jsonl").read_text(encoding="utf-8").splitlines()
|
| 29 |
+
]
|
| 30 |
+
|
| 31 |
+
assert lines[0]["type"] == "trace_manifest"
|
| 32 |
+
assert lines[0]["turn_count"] >= 3
|
| 33 |
+
assert all(line["schema_version"] == 1 for line in lines)
|