Spaces:
Running on Zero
Running on Zero
fix: label exported goals for users
Browse filesCo-authored-by: Codex <noreply@openai.com>
- README.md +7 -7
- hackathon_advisor/chapter.py +15 -7
- hackathon_advisor/field_notes.py +13 -5
- tests/test_app.py +5 -2
- tests/test_chapter.py +4 -3
- tests/test_field_notes.py +2 -1
README.md
CHANGED
|
@@ -66,18 +66,18 @@ is intentionally kept out of the main user workflow.
|
|
| 66 |
## Field Notes Artifact
|
| 67 |
|
| 68 |
The `field_notes` Gradio API endpoint and `Notes` button export a Markdown build note from the exact session state:
|
| 69 |
-
builder profile,
|
| 70 |
-
keeps the
|
| 71 |
|
| 72 |
## Chapter Artifact
|
| 73 |
|
| 74 |
The `chapter` Gradio API endpoint and `Chapter` button export the public-facing idea board as an Almanac chapter:
|
| 75 |
-
one
|
| 76 |
-
|
| 77 |
|
| 78 |
## Idea Board Ranking
|
| 79 |
|
| 80 |
-
The `Rank` command rescans the saved idea board, recalculates each seal against the
|
| 81 |
strongest page as the active idea, and drafts the next build step. The app then moves that page to the top of the Idea
|
| 82 |
Board and refreshes the seal, wood map, plan, and PNG artifact around the chosen direction.
|
| 83 |
Users can also click any Idea Board page to make it current before pressing `Plan`.
|
|
@@ -121,7 +121,7 @@ as the app instead of a separate hand-curated checklist.
|
|
| 121 |
## Demo Rehearsal
|
| 122 |
|
| 123 |
`/api/demo-session` and the `Example` button load a deterministic two-turn sample: a complete project idea, profile,
|
| 124 |
-
|
| 125 |
user session, so the visible app stays focused on the builder's idea while API exports remain available for submission
|
| 126 |
evidence.
|
| 127 |
|
|
@@ -151,7 +151,7 @@ watchdog updates the page text so the demo never sits in a silent blank state du
|
|
| 151 |
|
| 152 |
## Session Persistence
|
| 153 |
|
| 154 |
-
The frontend stores the current advisor session in browser `localStorage`: profile notes, selected
|
| 155 |
trace, latest build plan, and last share artifact. Refreshing the Space restores the same cockpit state; the `Reset`
|
| 156 |
button clears the saved session and returns to the current snapshot defaults.
|
| 157 |
|
|
|
|
| 66 |
## Field Notes Artifact
|
| 67 |
|
| 68 |
The `field_notes` Gradio API endpoint and `Notes` button export a Markdown build note from the exact session state:
|
| 69 |
+
builder profile, selected goals, idea board, cited Spaces, latest build plan, planner calls, and the share caption. This
|
| 70 |
+
keeps the note tied to auditable app evidence instead of a separate hand-written summary.
|
| 71 |
|
| 72 |
## Chapter Artifact
|
| 73 |
|
| 74 |
The `chapter` Gradio API endpoint and `Chapter` button export the public-facing idea board as an Almanac chapter:
|
| 75 |
+
one idea page per saved direction, each with verdict, score, selected goals, and closest cited pages. It is the
|
| 76 |
+
shareable companion to the working notes artifact.
|
| 77 |
|
| 78 |
## Idea Board Ranking
|
| 79 |
|
| 80 |
+
The `Rank` command rescans the saved idea board, recalculates each seal against the selected goals, selects the
|
| 81 |
strongest page as the active idea, and drafts the next build step. The app then moves that page to the top of the Idea
|
| 82 |
Board and refreshes the seal, wood map, plan, and PNG artifact around the chosen direction.
|
| 83 |
Users can also click any Idea Board page to make it current before pressing `Plan`.
|
|
|
|
| 121 |
## Demo Rehearsal
|
| 122 |
|
| 123 |
`/api/demo-session` and the `Example` button load a deterministic two-turn sample: a complete project idea, profile,
|
| 124 |
+
selected goals, score seal, build plan, trace, and wood map. It is built by running the same advisor engine as a normal
|
| 125 |
user session, so the visible app stays focused on the builder's idea while API exports remain available for submission
|
| 126 |
evidence.
|
| 127 |
|
|
|
|
| 151 |
|
| 152 |
## Session Persistence
|
| 153 |
|
| 154 |
+
The frontend stores the current advisor session in browser `localStorage`: profile notes, selected goals, idea board,
|
| 155 |
trace, latest build plan, and last share artifact. Refreshing the Space restores the same cockpit state; the `Reset`
|
| 156 |
button clears the saved session and returns to the current snapshot defaults.
|
| 157 |
|
hackathon_advisor/chapter.py
CHANGED
|
@@ -3,22 +3,24 @@ from __future__ import annotations
|
|
| 3 |
from datetime import datetime, timezone
|
| 4 |
from typing import Any
|
| 5 |
|
|
|
|
|
|
|
| 6 |
|
| 7 |
def build_chapter_markdown(session: dict[str, Any], metadata: dict[str, Any]) -> str:
|
| 8 |
ideas = _list_of_dicts(session.get("ideas"))
|
| 9 |
-
|
| 10 |
artifact = session.get("last_artifact") if isinstance(session.get("last_artifact"), dict) else {}
|
| 11 |
lines = [
|
| 12 |
"# The Unwritten Almanac Chapter",
|
| 13 |
"",
|
| 14 |
f"Generated: {datetime.now(timezone.utc).isoformat(timespec='seconds')}",
|
| 15 |
f"Snapshot: {_clean(metadata.get('snapshot_generated_at'))} · {_clean(metadata.get('project_count'))} pages",
|
| 16 |
-
f"
|
| 17 |
"",
|
| 18 |
]
|
| 19 |
|
| 20 |
if not ideas:
|
| 21 |
-
lines.extend(["No
|
| 22 |
return "\n".join(lines)
|
| 23 |
|
| 24 |
for index, idea in enumerate(ideas, start=1):
|
|
@@ -33,7 +35,7 @@ def build_chapter_markdown(session: dict[str, Any], metadata: dict[str, Any]) ->
|
|
| 33 |
def _idea_page(index: int, idea: dict[str, Any]) -> list[str]:
|
| 34 |
title = _clean(idea.get("title") or f"Page {index}")
|
| 35 |
pitch = _clean(idea.get("pitch"))
|
| 36 |
-
|
| 37 |
score = idea.get("score") if isinstance(idea.get("score"), dict) else {}
|
| 38 |
verdict = _clean(score.get("verdict")) if score else "DRAFT"
|
| 39 |
overall = _clean(score.get("overall")) if score else "0.0"
|
|
@@ -41,14 +43,14 @@ def _idea_page(index: int, idea: dict[str, Any]) -> list[str]:
|
|
| 41 |
f"## Page {index}: {title}",
|
| 42 |
"",
|
| 43 |
f"Verdict: {verdict} · {overall}/10",
|
| 44 |
-
f"
|
| 45 |
"",
|
| 46 |
-
pitch or "No
|
| 47 |
"",
|
| 48 |
]
|
| 49 |
echoes = _list_of_dicts(score.get("echoes")) if score else []
|
| 50 |
if echoes:
|
| 51 |
-
lines.extend(["Closest
|
| 52 |
for echo in echoes[:3]:
|
| 53 |
project = echo.get("project") if isinstance(echo.get("project"), dict) else {}
|
| 54 |
page = _clean(echo.get("page_number")) or "?"
|
|
@@ -69,6 +71,12 @@ def _list_of_dicts(value: Any) -> list[dict[str, Any]]:
|
|
| 69 |
return [item for item in value if isinstance(item, dict)]
|
| 70 |
|
| 71 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
def _clean(value: Any) -> str:
|
| 73 |
if value is None:
|
| 74 |
return ""
|
|
|
|
| 3 |
from datetime import datetime, timezone
|
| 4 |
from typing import Any
|
| 5 |
|
| 6 |
+
from hackathon_advisor.tools import target_label
|
| 7 |
+
|
| 8 |
|
| 9 |
def build_chapter_markdown(session: dict[str, Any], metadata: dict[str, Any]) -> str:
|
| 10 |
ideas = _list_of_dicts(session.get("ideas"))
|
| 11 |
+
goals = _goal_labels(session.get("targets"))
|
| 12 |
artifact = session.get("last_artifact") if isinstance(session.get("last_artifact"), dict) else {}
|
| 13 |
lines = [
|
| 14 |
"# The Unwritten Almanac Chapter",
|
| 15 |
"",
|
| 16 |
f"Generated: {datetime.now(timezone.utc).isoformat(timespec='seconds')}",
|
| 17 |
f"Snapshot: {_clean(metadata.get('snapshot_generated_at'))} · {_clean(metadata.get('project_count'))} pages",
|
| 18 |
+
f"Goals: {', '.join(goals) if goals else 'No specific goals'}",
|
| 19 |
"",
|
| 20 |
]
|
| 21 |
|
| 22 |
if not ideas:
|
| 23 |
+
lines.extend(["No idea pages have been written yet.", ""])
|
| 24 |
return "\n".join(lines)
|
| 25 |
|
| 26 |
for index, idea in enumerate(ideas, start=1):
|
|
|
|
| 35 |
def _idea_page(index: int, idea: dict[str, Any]) -> list[str]:
|
| 36 |
title = _clean(idea.get("title") or f"Page {index}")
|
| 37 |
pitch = _clean(idea.get("pitch"))
|
| 38 |
+
goals = _goal_labels(idea.get("targets"))
|
| 39 |
score = idea.get("score") if isinstance(idea.get("score"), dict) else {}
|
| 40 |
verdict = _clean(score.get("verdict")) if score else "DRAFT"
|
| 41 |
overall = _clean(score.get("overall")) if score else "0.0"
|
|
|
|
| 43 |
f"## Page {index}: {title}",
|
| 44 |
"",
|
| 45 |
f"Verdict: {verdict} · {overall}/10",
|
| 46 |
+
f"Goals: {', '.join(goals) if goals else 'No specific goals'}",
|
| 47 |
"",
|
| 48 |
+
pitch or "No pitch recorded.",
|
| 49 |
"",
|
| 50 |
]
|
| 51 |
echoes = _list_of_dicts(score.get("echoes")) if score else []
|
| 52 |
if echoes:
|
| 53 |
+
lines.extend(["Closest cited pages:", ""])
|
| 54 |
for echo in echoes[:3]:
|
| 55 |
project = echo.get("project") if isinstance(echo.get("project"), dict) else {}
|
| 56 |
page = _clean(echo.get("page_number")) or "?"
|
|
|
|
| 71 |
return [item for item in value if isinstance(item, dict)]
|
| 72 |
|
| 73 |
|
| 74 |
+
def _goal_labels(value: Any) -> list[str]:
|
| 75 |
+
if not isinstance(value, list):
|
| 76 |
+
return []
|
| 77 |
+
return [target_label(str(target)) for target in value]
|
| 78 |
+
|
| 79 |
+
|
| 80 |
def _clean(value: Any) -> str:
|
| 81 |
if value is None:
|
| 82 |
return ""
|
hackathon_advisor/field_notes.py
CHANGED
|
@@ -3,12 +3,14 @@ from __future__ import annotations
|
|
| 3 |
from datetime import datetime, timezone
|
| 4 |
from typing import Any
|
| 5 |
|
|
|
|
|
|
|
| 6 |
|
| 7 |
def build_field_notes_markdown(session: dict[str, Any], metadata: dict[str, Any]) -> str:
|
| 8 |
ideas = _list_of_dicts(session.get("ideas"))
|
| 9 |
trace = _list_of_dicts(session.get("trace"))
|
| 10 |
profile = session.get("profile") if isinstance(session.get("profile"), dict) else {}
|
| 11 |
-
|
| 12 |
last_plan = [str(step) for step in session.get("last_plan") or []]
|
| 13 |
last_artifact = session.get("last_artifact") if isinstance(session.get("last_artifact"), dict) else {}
|
| 14 |
|
|
@@ -35,7 +37,7 @@ def build_field_notes_markdown(session: dict[str, Any], metadata: dict[str, Any]
|
|
| 35 |
lines.append(f"- {field.title()}: {value}")
|
| 36 |
else:
|
| 37 |
lines.append("- No profile notes recorded.")
|
| 38 |
-
lines.append(f"-
|
| 39 |
|
| 40 |
lines.extend(["", "## Idea Board", ""])
|
| 41 |
if ideas:
|
|
@@ -49,7 +51,7 @@ def build_field_notes_markdown(session: dict[str, Any], metadata: dict[str, Any]
|
|
| 49 |
for index, step in enumerate(last_plan, start=1):
|
| 50 |
lines.append(f"{index}. {_clean(step)}")
|
| 51 |
else:
|
| 52 |
-
lines.append("No build plan
|
| 53 |
|
| 54 |
lines.extend(["", "## Turn Trace", ""])
|
| 55 |
if trace:
|
|
@@ -89,13 +91,13 @@ def build_field_notes_markdown(session: dict[str, Any], metadata: dict[str, Any]
|
|
| 89 |
def _idea_section(index: int, idea: dict[str, Any]) -> list[str]:
|
| 90 |
title = _clean(idea.get("title") or f"Idea {index}")
|
| 91 |
pitch = _clean(idea.get("pitch"))
|
| 92 |
-
|
| 93 |
score = idea.get("score") if isinstance(idea.get("score"), dict) else {}
|
| 94 |
lines = [
|
| 95 |
f"### {index}. {title}",
|
| 96 |
"",
|
| 97 |
f"- Pitch: {pitch or 'No pitch recorded.'}",
|
| 98 |
-
f"-
|
| 99 |
]
|
| 100 |
if score:
|
| 101 |
lines.append(f"- Seal: {_clean(score.get('overall'))}/10 - {_clean(score.get('verdict'))}")
|
|
@@ -150,6 +152,12 @@ def _list_of_dicts(value: Any) -> list[dict[str, Any]]:
|
|
| 150 |
return [item for item in value if isinstance(item, dict)]
|
| 151 |
|
| 152 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
def _clean(value: Any) -> str:
|
| 154 |
if value is None:
|
| 155 |
return ""
|
|
|
|
| 3 |
from datetime import datetime, timezone
|
| 4 |
from typing import Any
|
| 5 |
|
| 6 |
+
from hackathon_advisor.tools import target_label
|
| 7 |
+
|
| 8 |
|
| 9 |
def build_field_notes_markdown(session: dict[str, Any], metadata: dict[str, Any]) -> str:
|
| 10 |
ideas = _list_of_dicts(session.get("ideas"))
|
| 11 |
trace = _list_of_dicts(session.get("trace"))
|
| 12 |
profile = session.get("profile") if isinstance(session.get("profile"), dict) else {}
|
| 13 |
+
goals = _goal_labels(session.get("targets"))
|
| 14 |
last_plan = [str(step) for step in session.get("last_plan") or []]
|
| 15 |
last_artifact = session.get("last_artifact") if isinstance(session.get("last_artifact"), dict) else {}
|
| 16 |
|
|
|
|
| 37 |
lines.append(f"- {field.title()}: {value}")
|
| 38 |
else:
|
| 39 |
lines.append("- No profile notes recorded.")
|
| 40 |
+
lines.append(f"- Goals: {', '.join(goals) if goals else 'No specific goals'}")
|
| 41 |
|
| 42 |
lines.extend(["", "## Idea Board", ""])
|
| 43 |
if ideas:
|
|
|
|
| 51 |
for index, step in enumerate(last_plan, start=1):
|
| 52 |
lines.append(f"{index}. {_clean(step)}")
|
| 53 |
else:
|
| 54 |
+
lines.append("No build plan has been generated yet.")
|
| 55 |
|
| 56 |
lines.extend(["", "## Turn Trace", ""])
|
| 57 |
if trace:
|
|
|
|
| 91 |
def _idea_section(index: int, idea: dict[str, Any]) -> list[str]:
|
| 92 |
title = _clean(idea.get("title") or f"Idea {index}")
|
| 93 |
pitch = _clean(idea.get("pitch"))
|
| 94 |
+
goals = _goal_labels(idea.get("targets"))
|
| 95 |
score = idea.get("score") if isinstance(idea.get("score"), dict) else {}
|
| 96 |
lines = [
|
| 97 |
f"### {index}. {title}",
|
| 98 |
"",
|
| 99 |
f"- Pitch: {pitch or 'No pitch recorded.'}",
|
| 100 |
+
f"- Goals: {', '.join(goals) if goals else 'No specific goals'}",
|
| 101 |
]
|
| 102 |
if score:
|
| 103 |
lines.append(f"- Seal: {_clean(score.get('overall'))}/10 - {_clean(score.get('verdict'))}")
|
|
|
|
| 152 |
return [item for item in value if isinstance(item, dict)]
|
| 153 |
|
| 154 |
|
| 155 |
+
def _goal_labels(value: Any) -> list[str]:
|
| 156 |
+
if not isinstance(value, list):
|
| 157 |
+
return []
|
| 158 |
+
return [target_label(str(target)) for target in value]
|
| 159 |
+
|
| 160 |
+
|
| 161 |
def _clean(value: Any) -> str:
|
| 162 |
if value is None:
|
| 163 |
return ""
|
tests/test_app.py
CHANGED
|
@@ -69,7 +69,8 @@ def test_field_notes_endpoint_exports_markdown() -> None:
|
|
| 69 |
|
| 70 |
assert payload.startswith("# Hackathon Advisor Field Notes")
|
| 71 |
assert "Skills: frontend" in payload
|
| 72 |
-
assert "
|
|
|
|
| 73 |
assert "## Turn Trace" in payload
|
| 74 |
assert "Record the trace and write build notes" in payload
|
| 75 |
|
|
@@ -83,7 +84,9 @@ def test_chapter_endpoint_exports_markdown() -> None:
|
|
| 83 |
assert payload.startswith("# The Unwritten Almanac Chapter")
|
| 84 |
assert "## Page 1:" in payload
|
| 85 |
assert "## Page 2:" in payload
|
| 86 |
-
assert "
|
|
|
|
|
|
|
| 87 |
|
| 88 |
|
| 89 |
def test_lora_dataset_endpoint_exports_sft_jsonl() -> None:
|
|
|
|
| 69 |
|
| 70 |
assert payload.startswith("# Hackathon Advisor Field Notes")
|
| 71 |
assert "Skills: frontend" in payload
|
| 72 |
+
assert "Goals: Build notes" in payload
|
| 73 |
+
assert "Targets: Field Notes" not in payload
|
| 74 |
assert "## Turn Trace" in payload
|
| 75 |
assert "Record the trace and write build notes" in payload
|
| 76 |
|
|
|
|
| 84 |
assert payload.startswith("# The Unwritten Almanac Chapter")
|
| 85 |
assert "## Page 1:" in payload
|
| 86 |
assert "## Page 2:" in payload
|
| 87 |
+
assert "Goals:" in payload
|
| 88 |
+
assert "Targets:" not in payload
|
| 89 |
+
assert "Closest cited pages:" in payload
|
| 90 |
|
| 91 |
|
| 92 |
def test_lora_dataset_endpoint_exports_sft_jsonl() -> None:
|
tests/test_chapter.py
CHANGED
|
@@ -23,8 +23,9 @@ def test_chapter_markdown_contains_idea_pages_and_citations() -> None:
|
|
| 23 |
assert markdown.startswith("# The Unwritten Almanac Chapter")
|
| 24 |
assert "## Page 1:" in markdown
|
| 25 |
assert "## Page 2:" in markdown
|
| 26 |
-
assert "
|
| 27 |
-
assert "
|
|
|
|
| 28 |
assert "Page 30:" in markdown
|
| 29 |
|
| 30 |
|
|
@@ -37,4 +38,4 @@ def test_empty_chapter_markdown_is_explicit() -> None:
|
|
| 37 |
},
|
| 38 |
)
|
| 39 |
|
| 40 |
-
assert "No
|
|
|
|
| 23 |
assert markdown.startswith("# The Unwritten Almanac Chapter")
|
| 24 |
assert "## Page 1:" in markdown
|
| 25 |
assert "## Page 2:" in markdown
|
| 26 |
+
assert "Goals:" in markdown
|
| 27 |
+
assert "Targets:" not in markdown
|
| 28 |
+
assert "Closest cited pages:" in markdown
|
| 29 |
assert "Page 30:" in markdown
|
| 30 |
|
| 31 |
|
|
|
|
| 38 |
},
|
| 39 |
)
|
| 40 |
|
| 41 |
+
assert "No idea pages have been written yet." in markdown
|
tests/test_field_notes.py
CHANGED
|
@@ -26,7 +26,8 @@ def test_field_notes_markdown_contains_session_decisions() -> None:
|
|
| 26 |
|
| 27 |
assert "# Hackathon Advisor Field Notes" in markdown
|
| 28 |
assert "frontend prototyping" in markdown
|
| 29 |
-
assert "
|
|
|
|
| 30 |
assert "A local-first archive cartographer for family photos" in markdown
|
| 31 |
assert "## Build Plan" in markdown
|
| 32 |
assert "Record the trace and write build notes" in markdown
|
|
|
|
| 26 |
|
| 27 |
assert "# Hackathon Advisor Field Notes" in markdown
|
| 28 |
assert "frontend prototyping" in markdown
|
| 29 |
+
assert "Goals: Build notes" in markdown
|
| 30 |
+
assert "Targets: Field Notes" not in markdown
|
| 31 |
assert "A local-first archive cartographer for family photos" in markdown
|
| 32 |
assert "## Build Plan" in markdown
|
| 33 |
assert "Record the trace and write build notes" in markdown
|