Spaces:
Running on Zero
Running on Zero
refactor: rename session targets to goals
Browse filesCo-authored-by: Codex <noreply@openai.com>
- app.py +4 -4
- hackathon_advisor/agent.py +18 -18
- hackathon_advisor/chapter.py +4 -4
- hackathon_advisor/demo_rehearsal.py +2 -2
- hackathon_advisor/field_notes.py +4 -4
- hackathon_advisor/lora_dataset.py +4 -4
- hackathon_advisor/scoring.py +3 -3
- hackathon_advisor/submission_packet.py +2 -2
- hackathon_advisor/tools.py +30 -30
- static/app.js +51 -51
- static/index.html +2 -2
- static/styles.css +13 -13
- tests/test_agent.py +8 -8
- tests/test_app.py +7 -7
- tests/test_demo_rehearsal.py +2 -2
- tests/test_field_notes.py +1 -1
- tests/test_lora_dataset.py +3 -3
- tests/test_submission_packet.py +1 -1
app.py
CHANGED
|
@@ -19,7 +19,7 @@ from hackathon_advisor.lora_training_kit import TRAINING_KIT_FILENAME, build_lor
|
|
| 19 |
from hackathon_advisor.prize_ledger import prize_ledger
|
| 20 |
from hackathon_advisor.submission_packet import build_submission_packet_markdown
|
| 21 |
from hackathon_advisor.tool_contracts import resolve_tool_call, tool_schemas
|
| 22 |
-
from hackathon_advisor.tools import
|
| 23 |
from hackathon_advisor.trace_export import build_trace_jsonl, trace_metadata
|
| 24 |
|
| 25 |
|
|
@@ -70,9 +70,9 @@ def bootstrap() -> dict:
|
|
| 70 |
**trace_metadata(index),
|
| 71 |
"top_projects": [project.to_public_dict() for project in index.top_projects(limit=8)],
|
| 72 |
"whitespace": [item.to_dict() for item in index.find_whitespace(limit=5)],
|
| 73 |
-
"
|
| 74 |
-
"
|
| 75 |
-
"
|
| 76 |
"profile_fields": PROFILE_FIELDS,
|
| 77 |
"prize_ledger": prize_ledger(runtime_status),
|
| 78 |
}
|
|
|
|
| 19 |
from hackathon_advisor.prize_ledger import prize_ledger
|
| 20 |
from hackathon_advisor.submission_packet import build_submission_packet_markdown
|
| 21 |
from hackathon_advisor.tool_contracts import resolve_tool_call, tool_schemas
|
| 22 |
+
from hackathon_advisor.tools import GOALS, goal_profiles
|
| 23 |
from hackathon_advisor.trace_export import build_trace_jsonl, trace_metadata
|
| 24 |
|
| 25 |
|
|
|
|
| 70 |
**trace_metadata(index),
|
| 71 |
"top_projects": [project.to_public_dict() for project in index.top_projects(limit=8)],
|
| 72 |
"whitespace": [item.to_dict() for item in index.find_whitespace(limit=5)],
|
| 73 |
+
"goal_options": GOALS,
|
| 74 |
+
"goal_profiles": goal_profiles(),
|
| 75 |
+
"default_goals": GOALS[:3],
|
| 76 |
"profile_fields": PROFILE_FIELDS,
|
| 77 |
"prize_ledger": prize_ledger(runtime_status),
|
| 78 |
}
|
hackathon_advisor/agent.py
CHANGED
|
@@ -10,14 +10,14 @@ from hackathon_advisor.model_runtime import ToolPlanner, create_tool_planner, ru
|
|
| 10 |
from hackathon_advisor.scoring import ScoreCard
|
| 11 |
from hackathon_advisor.tool_contracts import ToolCall
|
| 12 |
from hackathon_advisor.tools import (
|
| 13 |
-
|
| 14 |
AdvisorTools,
|
| 15 |
Idea,
|
| 16 |
ToolEvent,
|
|
|
|
|
|
|
| 17 |
idea_from_text,
|
| 18 |
-
|
| 19 |
-
target_label,
|
| 20 |
-
targets_from_state,
|
| 21 |
)
|
| 22 |
from hackathon_advisor.wood_map import build_wood_map
|
| 23 |
|
|
@@ -62,7 +62,7 @@ class AdvisorEngine:
|
|
| 62 |
state = dict(state or {})
|
| 63 |
state.setdefault("ideas", [])
|
| 64 |
state.setdefault("profile", {})
|
| 65 |
-
state.setdefault("
|
| 66 |
normalized, corrections = normalize_text(message)
|
| 67 |
resolution = self.planner.plan(normalized, state)
|
| 68 |
state["last_tool_resolution"] = resolution.to_dict()
|
|
@@ -129,7 +129,7 @@ class AdvisorEngine:
|
|
| 129 |
return self._profile_turn(call, normalized, corrections, state, tool_events)
|
| 130 |
|
| 131 |
if call.name == "set_goals":
|
| 132 |
-
return self.
|
| 133 |
|
| 134 |
return self._idea_research_turn(call, normalized, corrections, state, tool_events)
|
| 135 |
|
|
@@ -177,13 +177,13 @@ class AdvisorEngine:
|
|
| 177 |
current_id = state.get("current_idea_id")
|
| 178 |
for item in state.get("ideas", []):
|
| 179 |
if item.get("id") == current_id:
|
| 180 |
-
return self.
|
| 181 |
if state.get("ideas"):
|
| 182 |
-
return self.
|
| 183 |
return None
|
| 184 |
|
| 185 |
-
def
|
| 186 |
-
idea.
|
| 187 |
return idea
|
| 188 |
|
| 189 |
def _profile_context(self, state: dict[str, Any]) -> dict[str, Any]:
|
|
@@ -347,7 +347,7 @@ class AdvisorEngine:
|
|
| 347 |
response = f"Profile updated: {field} = {profile[field]}."
|
| 348 |
return self._result(normalized, corrections, response, state, tool_events, [], [], None, [], {})
|
| 349 |
|
| 350 |
-
def
|
| 351 |
self,
|
| 352 |
call: ToolCall,
|
| 353 |
normalized: str,
|
|
@@ -355,14 +355,14 @@ class AdvisorEngine:
|
|
| 355 |
state: dict[str, Any],
|
| 356 |
tool_events: list[ToolEvent],
|
| 357 |
) -> TurnResult:
|
| 358 |
-
|
| 359 |
-
state["
|
| 360 |
idea = self._current_idea(state)
|
| 361 |
if idea is not None:
|
| 362 |
-
idea.
|
| 363 |
self._store_idea(state, idea)
|
| 364 |
-
tool_events.append(ToolEvent("set_goals", f"Set {len(
|
| 365 |
-
labels = [
|
| 366 |
response = "The seal will now bias toward: " + (", ".join(labels) or "no specific goals")
|
| 367 |
return self._result(normalized, corrections, response, state, tool_events, [], [], None, [], {})
|
| 368 |
|
|
@@ -371,7 +371,7 @@ class AdvisorEngine:
|
|
| 371 |
if idea_id:
|
| 372 |
for item in state.get("ideas", []):
|
| 373 |
if item.get("id") == idea_id:
|
| 374 |
-
return self.
|
| 375 |
return self._current_idea(state)
|
| 376 |
|
| 377 |
def _record_trace(
|
|
@@ -442,7 +442,7 @@ class AdvisorEngine:
|
|
| 442 |
ranked: list[tuple[Idea, ScoreCard]] = []
|
| 443 |
for item in state.get("ideas", []):
|
| 444 |
try:
|
| 445 |
-
idea = self.
|
| 446 |
except TypeError:
|
| 447 |
continue
|
| 448 |
score, _event = self.tools.score_idea(idea)
|
|
|
|
| 10 |
from hackathon_advisor.scoring import ScoreCard
|
| 11 |
from hackathon_advisor.tool_contracts import ToolCall
|
| 12 |
from hackathon_advisor.tools import (
|
| 13 |
+
GOALS,
|
| 14 |
AdvisorTools,
|
| 15 |
Idea,
|
| 16 |
ToolEvent,
|
| 17 |
+
goal_label,
|
| 18 |
+
goals_from_state,
|
| 19 |
idea_from_text,
|
| 20 |
+
normalize_goals,
|
|
|
|
|
|
|
| 21 |
)
|
| 22 |
from hackathon_advisor.wood_map import build_wood_map
|
| 23 |
|
|
|
|
| 62 |
state = dict(state or {})
|
| 63 |
state.setdefault("ideas", [])
|
| 64 |
state.setdefault("profile", {})
|
| 65 |
+
state.setdefault("goals", GOALS[:3])
|
| 66 |
normalized, corrections = normalize_text(message)
|
| 67 |
resolution = self.planner.plan(normalized, state)
|
| 68 |
state["last_tool_resolution"] = resolution.to_dict()
|
|
|
|
| 129 |
return self._profile_turn(call, normalized, corrections, state, tool_events)
|
| 130 |
|
| 131 |
if call.name == "set_goals":
|
| 132 |
+
return self._goal_turn(call, normalized, corrections, state, tool_events)
|
| 133 |
|
| 134 |
return self._idea_research_turn(call, normalized, corrections, state, tool_events)
|
| 135 |
|
|
|
|
| 177 |
current_id = state.get("current_idea_id")
|
| 178 |
for item in state.get("ideas", []):
|
| 179 |
if item.get("id") == current_id:
|
| 180 |
+
return self._with_session_goals(Idea(**item), state)
|
| 181 |
if state.get("ideas"):
|
| 182 |
+
return self._with_session_goals(Idea(**state["ideas"][-1]), state)
|
| 183 |
return None
|
| 184 |
|
| 185 |
+
def _with_session_goals(self, idea: Idea, state: dict[str, Any]) -> Idea:
|
| 186 |
+
idea.goals = goals_from_state(state)
|
| 187 |
return idea
|
| 188 |
|
| 189 |
def _profile_context(self, state: dict[str, Any]) -> dict[str, Any]:
|
|
|
|
| 347 |
response = f"Profile updated: {field} = {profile[field]}."
|
| 348 |
return self._result(normalized, corrections, response, state, tool_events, [], [], None, [], {})
|
| 349 |
|
| 350 |
+
def _goal_turn(
|
| 351 |
self,
|
| 352 |
call: ToolCall,
|
| 353 |
normalized: str,
|
|
|
|
| 355 |
state: dict[str, Any],
|
| 356 |
tool_events: list[ToolEvent],
|
| 357 |
) -> TurnResult:
|
| 358 |
+
goals = normalize_goals(call.arguments.get("goals"), default=[])
|
| 359 |
+
state["goals"] = goals
|
| 360 |
idea = self._current_idea(state)
|
| 361 |
if idea is not None:
|
| 362 |
+
idea.goals = goals
|
| 363 |
self._store_idea(state, idea)
|
| 364 |
+
tool_events.append(ToolEvent("set_goals", f"Set {len(goals)} goals."))
|
| 365 |
+
labels = [goal_label(goal) for goal in goals]
|
| 366 |
response = "The seal will now bias toward: " + (", ".join(labels) or "no specific goals")
|
| 367 |
return self._result(normalized, corrections, response, state, tool_events, [], [], None, [], {})
|
| 368 |
|
|
|
|
| 371 |
if idea_id:
|
| 372 |
for item in state.get("ideas", []):
|
| 373 |
if item.get("id") == idea_id:
|
| 374 |
+
return self._with_session_goals(Idea(**item), state)
|
| 375 |
return self._current_idea(state)
|
| 376 |
|
| 377 |
def _record_trace(
|
|
|
|
| 442 |
ranked: list[tuple[Idea, ScoreCard]] = []
|
| 443 |
for item in state.get("ideas", []):
|
| 444 |
try:
|
| 445 |
+
idea = self._with_session_goals(Idea(**item), state)
|
| 446 |
except TypeError:
|
| 447 |
continue
|
| 448 |
score, _event = self.tools.score_idea(idea)
|
hackathon_advisor/chapter.py
CHANGED
|
@@ -3,12 +3,12 @@ from __future__ import annotations
|
|
| 3 |
from datetime import datetime, timezone
|
| 4 |
from typing import Any
|
| 5 |
|
| 6 |
-
from hackathon_advisor.tools import
|
| 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("
|
| 12 |
artifact = session.get("last_artifact") if isinstance(session.get("last_artifact"), dict) else {}
|
| 13 |
lines = [
|
| 14 |
"# The Unwritten Almanac Chapter",
|
|
@@ -35,7 +35,7 @@ def build_chapter_markdown(session: dict[str, Any], metadata: dict[str, Any]) ->
|
|
| 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("
|
| 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"
|
|
@@ -74,7 +74,7 @@ def _list_of_dicts(value: Any) -> list[dict[str, Any]]:
|
|
| 74 |
def _goal_labels(value: Any) -> list[str]:
|
| 75 |
if not isinstance(value, list):
|
| 76 |
return []
|
| 77 |
-
return [
|
| 78 |
|
| 79 |
|
| 80 |
def _clean(value: Any) -> str:
|
|
|
|
| 3 |
from datetime import datetime, timezone
|
| 4 |
from typing import Any
|
| 5 |
|
| 6 |
+
from hackathon_advisor.tools import goal_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("goals"))
|
| 12 |
artifact = session.get("last_artifact") if isinstance(session.get("last_artifact"), dict) else {}
|
| 13 |
lines = [
|
| 14 |
"# The Unwritten Almanac Chapter",
|
|
|
|
| 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("goals"))
|
| 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"
|
|
|
|
| 74 |
def _goal_labels(value: Any) -> list[str]:
|
| 75 |
if not isinstance(value, list):
|
| 76 |
return []
|
| 77 |
+
return [goal_label(str(goal)) for goal in value]
|
| 78 |
|
| 79 |
|
| 80 |
def _clean(value: Any) -> str:
|
hackathon_advisor/demo_rehearsal.py
CHANGED
|
@@ -14,7 +14,7 @@ DEMO_PROFILE = {
|
|
| 14 |
"preferences": "auditable artifacts, local-first runtime, strong demo beat",
|
| 15 |
"constraints": "CPU Space runtime; no proprietary inference API",
|
| 16 |
}
|
| 17 |
-
|
| 18 |
"Off the Grid",
|
| 19 |
"Well-Tuned",
|
| 20 |
"Off-Brand",
|
|
@@ -26,7 +26,7 @@ DEMO_TARGETS = [
|
|
| 26 |
def build_demo_rehearsal(engine: Any) -> dict[str, Any]:
|
| 27 |
initial_state = {
|
| 28 |
"profile": dict(DEMO_PROFILE),
|
| 29 |
-
"
|
| 30 |
}
|
| 31 |
first = engine.turn(DEMO_PROMPT, initial_state)
|
| 32 |
second = engine.turn(DEMO_PLAN_PROMPT, first.state)
|
|
|
|
| 14 |
"preferences": "auditable artifacts, local-first runtime, strong demo beat",
|
| 15 |
"constraints": "CPU Space runtime; no proprietary inference API",
|
| 16 |
}
|
| 17 |
+
DEMO_GOALS = [
|
| 18 |
"Off the Grid",
|
| 19 |
"Well-Tuned",
|
| 20 |
"Off-Brand",
|
|
|
|
| 26 |
def build_demo_rehearsal(engine: Any) -> dict[str, Any]:
|
| 27 |
initial_state = {
|
| 28 |
"profile": dict(DEMO_PROFILE),
|
| 29 |
+
"goals": list(DEMO_GOALS),
|
| 30 |
}
|
| 31 |
first = engine.turn(DEMO_PROMPT, initial_state)
|
| 32 |
second = engine.turn(DEMO_PLAN_PROMPT, first.state)
|
hackathon_advisor/field_notes.py
CHANGED
|
@@ -3,14 +3,14 @@ from __future__ import annotations
|
|
| 3 |
from datetime import datetime, timezone
|
| 4 |
from typing import Any
|
| 5 |
|
| 6 |
-
from hackathon_advisor.tools import
|
| 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("
|
| 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 |
|
|
@@ -91,7 +91,7 @@ def build_field_notes_markdown(session: dict[str, Any], metadata: dict[str, Any]
|
|
| 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("
|
| 95 |
score = idea.get("score") if isinstance(idea.get("score"), dict) else {}
|
| 96 |
lines = [
|
| 97 |
f"### {index}. {title}",
|
|
@@ -155,7 +155,7 @@ def _list_of_dicts(value: Any) -> list[dict[str, Any]]:
|
|
| 155 |
def _goal_labels(value: Any) -> list[str]:
|
| 156 |
if not isinstance(value, list):
|
| 157 |
return []
|
| 158 |
-
return [
|
| 159 |
|
| 160 |
|
| 161 |
def _clean(value: Any) -> str:
|
|
|
|
| 3 |
from datetime import datetime, timezone
|
| 4 |
from typing import Any
|
| 5 |
|
| 6 |
+
from hackathon_advisor.tools import goal_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("goals"))
|
| 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 |
|
|
|
|
| 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("goals"))
|
| 95 |
score = idea.get("score") if isinstance(idea.get("score"), dict) else {}
|
| 96 |
lines = [
|
| 97 |
f"### {index}. {title}",
|
|
|
|
| 155 |
def _goal_labels(value: Any) -> list[str]:
|
| 156 |
if not isinstance(value, list):
|
| 157 |
return []
|
| 158 |
+
return [goal_label(str(goal)) for goal in value]
|
| 159 |
|
| 160 |
|
| 161 |
def _clean(value: Any) -> str:
|
hackathon_advisor/lora_dataset.py
CHANGED
|
@@ -23,8 +23,8 @@ RESPONSE_SYSTEM_PROMPT = (
|
|
| 23 |
def build_lora_dataset_jsonl(session: dict[str, Any], metadata: dict[str, Any]) -> str:
|
| 24 |
trace = _list_of_dicts(session.get("trace"))
|
| 25 |
ideas = _list_of_dicts(session.get("ideas"))
|
| 26 |
-
|
| 27 |
-
examples = _examples(trace,
|
| 28 |
records = [
|
| 29 |
{
|
| 30 |
"type": "lora_sft_manifest",
|
|
@@ -47,7 +47,7 @@ def build_lora_dataset_jsonl(session: dict[str, Any], metadata: dict[str, Any])
|
|
| 47 |
return "\n".join(json.dumps(record, ensure_ascii=False, sort_keys=True) for record in records) + "\n"
|
| 48 |
|
| 49 |
|
| 50 |
-
def _examples(trace: list[dict[str, Any]],
|
| 51 |
examples: list[dict[str, Any]] = []
|
| 52 |
for turn_index, event in enumerate(trace, start=1):
|
| 53 |
if not _is_successful_turn(event):
|
|
@@ -65,7 +65,7 @@ def _examples(trace: list[dict[str, Any]], targets: list[str]) -> list[dict[str,
|
|
| 65 |
"base_model": BASE_MODEL,
|
| 66 |
"adapter_task": ADAPTER_TASK,
|
| 67 |
"turn_index": turn_index,
|
| 68 |
-
"
|
| 69 |
"score": _score(event),
|
| 70 |
"tool_call": tool_call,
|
| 71 |
"tool_observations": _tool_observations(event),
|
|
|
|
| 23 |
def build_lora_dataset_jsonl(session: dict[str, Any], metadata: dict[str, Any]) -> str:
|
| 24 |
trace = _list_of_dicts(session.get("trace"))
|
| 25 |
ideas = _list_of_dicts(session.get("ideas"))
|
| 26 |
+
goals = [str(goal) for goal in session.get("goals") or []]
|
| 27 |
+
examples = _examples(trace, goals)
|
| 28 |
records = [
|
| 29 |
{
|
| 30 |
"type": "lora_sft_manifest",
|
|
|
|
| 47 |
return "\n".join(json.dumps(record, ensure_ascii=False, sort_keys=True) for record in records) + "\n"
|
| 48 |
|
| 49 |
|
| 50 |
+
def _examples(trace: list[dict[str, Any]], goals: list[str]) -> list[dict[str, Any]]:
|
| 51 |
examples: list[dict[str, Any]] = []
|
| 52 |
for turn_index, event in enumerate(trace, start=1):
|
| 53 |
if not _is_successful_turn(event):
|
|
|
|
| 65 |
"base_model": BASE_MODEL,
|
| 66 |
"adapter_task": ADAPTER_TASK,
|
| 67 |
"turn_index": turn_index,
|
| 68 |
+
"goals": goals,
|
| 69 |
"score": _score(event),
|
| 70 |
"tool_call": tool_call,
|
| 71 |
"tool_observations": _tool_observations(event),
|
hackathon_advisor/scoring.py
CHANGED
|
@@ -49,12 +49,12 @@ class ScoreCard:
|
|
| 49 |
}
|
| 50 |
|
| 51 |
|
| 52 |
-
def score_idea(index: ProjectIndex, title: str, pitch: str,
|
| 53 |
text = f"{title} {pitch}".strip()
|
| 54 |
hits = index.search(text, limit=4)
|
| 55 |
top_overlap = hits[0].score if hits else 0.0
|
| 56 |
tokens = set(tokenize(text))
|
| 57 |
-
|
| 58 |
|
| 59 |
originality = clamp_score(10 - round(top_overlap * 18))
|
| 60 |
delight = clamp_score(4 + _keyword_count(tokens, {"story", "visual", "game", "ritual", "share", "voice"}) * 2)
|
|
@@ -67,7 +67,7 @@ def score_idea(index: ProjectIndex, title: str, pitch: str, targets: list[str] |
|
|
| 67 |
goal_fit = clamp_score(
|
| 68 |
4
|
| 69 |
+ _keyword_count(tokens, {"local", "offline", "small", "llama", "fine", "trace", "gradio"}) * 2
|
| 70 |
-
+ min(len(
|
| 71 |
)
|
| 72 |
verdict = "UNWRITTEN" if top_overlap < 0.16 else f"ECHO x{sum(1 for hit in hits if hit.score >= 0.12)}"
|
| 73 |
return ScoreCard(
|
|
|
|
| 49 |
}
|
| 50 |
|
| 51 |
|
| 52 |
+
def score_idea(index: ProjectIndex, title: str, pitch: str, goals: list[str] | None = None) -> ScoreCard:
|
| 53 |
text = f"{title} {pitch}".strip()
|
| 54 |
hits = index.search(text, limit=4)
|
| 55 |
top_overlap = hits[0].score if hits else 0.0
|
| 56 |
tokens = set(tokenize(text))
|
| 57 |
+
goals = goals or []
|
| 58 |
|
| 59 |
originality = clamp_score(10 - round(top_overlap * 18))
|
| 60 |
delight = clamp_score(4 + _keyword_count(tokens, {"story", "visual", "game", "ritual", "share", "voice"}) * 2)
|
|
|
|
| 67 |
goal_fit = clamp_score(
|
| 68 |
4
|
| 69 |
+ _keyword_count(tokens, {"local", "offline", "small", "llama", "fine", "trace", "gradio"}) * 2
|
| 70 |
+
+ min(len(goals), 3)
|
| 71 |
)
|
| 72 |
verdict = "UNWRITTEN" if top_overlap < 0.16 else f"ECHO x{sum(1 for hit in hits if hit.score >= 0.12)}"
|
| 73 |
return ScoreCard(
|
hackathon_advisor/submission_packet.py
CHANGED
|
@@ -16,7 +16,7 @@ def build_submission_packet_markdown(
|
|
| 16 |
) -> str:
|
| 17 |
ideas = _list_of_dicts(session.get("ideas"))
|
| 18 |
trace = _list_of_dicts(session.get("trace"))
|
| 19 |
-
|
| 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 {}
|
|
@@ -46,7 +46,7 @@ def build_submission_packet_markdown(
|
|
| 46 |
"",
|
| 47 |
f"- Title: {title}",
|
| 48 |
f"- Verdict: {verdict} {overall}/10",
|
| 49 |
-
f"-
|
| 50 |
f"- Pitch: {_clean(current.get('pitch')) or 'No pitch recorded.'}",
|
| 51 |
"",
|
| 52 |
]
|
|
|
|
| 16 |
) -> str:
|
| 17 |
ideas = _list_of_dicts(session.get("ideas"))
|
| 18 |
trace = _list_of_dicts(session.get("trace"))
|
| 19 |
+
goals = [str(goal) for goal in session.get("goals") 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 {}
|
|
|
|
| 46 |
"",
|
| 47 |
f"- Title: {title}",
|
| 48 |
f"- Verdict: {verdict} {overall}/10",
|
| 49 |
+
f"- Goals: {', '.join(goals) if goals else 'No specific goals'}",
|
| 50 |
f"- Pitch: {_clean(current.get('pitch')) or 'No pitch recorded.'}",
|
| 51 |
"",
|
| 52 |
]
|
hackathon_advisor/tools.py
CHANGED
|
@@ -8,7 +8,7 @@ from hackathon_advisor.data import Project, ProjectIndex, WhitespaceItem
|
|
| 8 |
from hackathon_advisor.scoring import ScoreCard, score_idea
|
| 9 |
|
| 10 |
|
| 11 |
-
|
| 12 |
"Off the Grid",
|
| 13 |
"Well-Tuned",
|
| 14 |
"Off-Brand",
|
|
@@ -17,7 +17,7 @@ TARGETS = [
|
|
| 17 |
"Field Notes",
|
| 18 |
]
|
| 19 |
|
| 20 |
-
|
| 21 |
"Off the Grid": {
|
| 22 |
"label": "Local-first",
|
| 23 |
"description": "Favor ideas that work without proprietary inference APIs.",
|
|
@@ -45,41 +45,41 @@ TARGET_PROFILE_BY_ID = {
|
|
| 45 |
}
|
| 46 |
|
| 47 |
|
| 48 |
-
def
|
| 49 |
return [
|
| 50 |
{
|
| 51 |
-
"id":
|
| 52 |
-
"label":
|
| 53 |
-
"description":
|
| 54 |
}
|
| 55 |
-
for
|
| 56 |
]
|
| 57 |
|
| 58 |
|
| 59 |
-
def
|
| 60 |
-
return
|
| 61 |
|
| 62 |
|
| 63 |
-
def
|
| 64 |
-
if
|
| 65 |
return list(default or [])
|
| 66 |
-
if not isinstance(
|
| 67 |
return list(default or [])
|
| 68 |
|
| 69 |
-
|
| 70 |
seen: set[str] = set()
|
| 71 |
-
for
|
| 72 |
-
|
| 73 |
-
if
|
| 74 |
-
|
| 75 |
-
seen.add(
|
| 76 |
-
return
|
| 77 |
|
| 78 |
|
| 79 |
-
def
|
| 80 |
-
if "
|
| 81 |
-
return
|
| 82 |
-
return
|
| 83 |
|
| 84 |
|
| 85 |
@dataclass
|
|
@@ -87,7 +87,7 @@ class Idea:
|
|
| 87 |
id: str
|
| 88 |
title: str
|
| 89 |
pitch: str
|
| 90 |
-
|
| 91 |
score: dict | None = None
|
| 92 |
|
| 93 |
def to_dict(self) -> dict:
|
|
@@ -95,7 +95,7 @@ class Idea:
|
|
| 95 |
"id": self.id,
|
| 96 |
"title": self.title,
|
| 97 |
"pitch": self.pitch,
|
| 98 |
-
"
|
| 99 |
"score": self.score,
|
| 100 |
}
|
| 101 |
|
|
@@ -129,21 +129,21 @@ class AdvisorTools:
|
|
| 129 |
def save_idea(self, state: dict[str, Any], title: str, pitch: str) -> tuple[Idea, ToolEvent]:
|
| 130 |
ideas = [Idea(**item) for item in state.get("ideas", [])]
|
| 131 |
current_id = state.get("current_idea_id")
|
| 132 |
-
|
| 133 |
idea = next((item for item in ideas if item.id == current_id), None)
|
| 134 |
if idea is None or _is_new_idea(idea, title, pitch):
|
| 135 |
-
idea = Idea(id=uuid.uuid4().hex[:8], title=title, pitch=pitch,
|
| 136 |
ideas.append(idea)
|
| 137 |
else:
|
| 138 |
idea.title = title
|
| 139 |
idea.pitch = pitch
|
| 140 |
-
idea.
|
| 141 |
state["ideas"] = [item.to_dict() for item in ideas]
|
| 142 |
state["current_idea_id"] = idea.id
|
| 143 |
return idea, ToolEvent("save_idea", f"Wrote idea page '{idea.title}'.")
|
| 144 |
|
| 145 |
def score_idea(self, idea: Idea) -> tuple[ScoreCard, ToolEvent]:
|
| 146 |
-
score = score_idea(self.index, idea.title, idea.pitch, idea.
|
| 147 |
idea.score = score.to_dict()
|
| 148 |
return score, ToolEvent("score_idea", f"Pressed a five-quadrant seal: {score.overall}/10.")
|
| 149 |
|
|
@@ -158,7 +158,7 @@ class AdvisorTools:
|
|
| 158 |
profile_steps = profile_plan_steps(profile)
|
| 159 |
if profile_steps:
|
| 160 |
plan[1:1] = profile_steps
|
| 161 |
-
if any("Well" in
|
| 162 |
plan.insert(
|
| 163 |
max(0, len(plan) - 1),
|
| 164 |
"Prepare a tiny LoRA dataset from successful advisor turns before training.",
|
|
|
|
| 8 |
from hackathon_advisor.scoring import ScoreCard, score_idea
|
| 9 |
|
| 10 |
|
| 11 |
+
GOALS = [
|
| 12 |
"Off the Grid",
|
| 13 |
"Well-Tuned",
|
| 14 |
"Off-Brand",
|
|
|
|
| 17 |
"Field Notes",
|
| 18 |
]
|
| 19 |
|
| 20 |
+
GOAL_PROFILE_BY_ID = {
|
| 21 |
"Off the Grid": {
|
| 22 |
"label": "Local-first",
|
| 23 |
"description": "Favor ideas that work without proprietary inference APIs.",
|
|
|
|
| 45 |
}
|
| 46 |
|
| 47 |
|
| 48 |
+
def goal_profiles() -> list[dict[str, str]]:
|
| 49 |
return [
|
| 50 |
{
|
| 51 |
+
"id": goal,
|
| 52 |
+
"label": GOAL_PROFILE_BY_ID[goal]["label"],
|
| 53 |
+
"description": GOAL_PROFILE_BY_ID[goal]["description"],
|
| 54 |
}
|
| 55 |
+
for goal in GOALS
|
| 56 |
]
|
| 57 |
|
| 58 |
|
| 59 |
+
def goal_label(goal: str) -> str:
|
| 60 |
+
return GOAL_PROFILE_BY_ID.get(goal, {}).get("label", goal)
|
| 61 |
|
| 62 |
|
| 63 |
+
def normalize_goals(raw_goals: Any, default: list[str] | None = None) -> list[str]:
|
| 64 |
+
if raw_goals is None:
|
| 65 |
return list(default or [])
|
| 66 |
+
if not isinstance(raw_goals, list):
|
| 67 |
return list(default or [])
|
| 68 |
|
| 69 |
+
goals: list[str] = []
|
| 70 |
seen: set[str] = set()
|
| 71 |
+
for raw_goal in raw_goals:
|
| 72 |
+
goal = str(raw_goal)
|
| 73 |
+
if goal in GOALS and goal not in seen:
|
| 74 |
+
goals.append(goal)
|
| 75 |
+
seen.add(goal)
|
| 76 |
+
return goals
|
| 77 |
|
| 78 |
|
| 79 |
+
def goals_from_state(state: dict[str, Any]) -> list[str]:
|
| 80 |
+
if "goals" not in state:
|
| 81 |
+
return GOALS[:3]
|
| 82 |
+
return normalize_goals(state.get("goals"), default=[])
|
| 83 |
|
| 84 |
|
| 85 |
@dataclass
|
|
|
|
| 87 |
id: str
|
| 88 |
title: str
|
| 89 |
pitch: str
|
| 90 |
+
goals: list[str] = field(default_factory=lambda: GOALS[:3])
|
| 91 |
score: dict | None = None
|
| 92 |
|
| 93 |
def to_dict(self) -> dict:
|
|
|
|
| 95 |
"id": self.id,
|
| 96 |
"title": self.title,
|
| 97 |
"pitch": self.pitch,
|
| 98 |
+
"goals": self.goals,
|
| 99 |
"score": self.score,
|
| 100 |
}
|
| 101 |
|
|
|
|
| 129 |
def save_idea(self, state: dict[str, Any], title: str, pitch: str) -> tuple[Idea, ToolEvent]:
|
| 130 |
ideas = [Idea(**item) for item in state.get("ideas", [])]
|
| 131 |
current_id = state.get("current_idea_id")
|
| 132 |
+
goals = goals_from_state(state)
|
| 133 |
idea = next((item for item in ideas if item.id == current_id), None)
|
| 134 |
if idea is None or _is_new_idea(idea, title, pitch):
|
| 135 |
+
idea = Idea(id=uuid.uuid4().hex[:8], title=title, pitch=pitch, goals=goals)
|
| 136 |
ideas.append(idea)
|
| 137 |
else:
|
| 138 |
idea.title = title
|
| 139 |
idea.pitch = pitch
|
| 140 |
+
idea.goals = goals
|
| 141 |
state["ideas"] = [item.to_dict() for item in ideas]
|
| 142 |
state["current_idea_id"] = idea.id
|
| 143 |
return idea, ToolEvent("save_idea", f"Wrote idea page '{idea.title}'.")
|
| 144 |
|
| 145 |
def score_idea(self, idea: Idea) -> tuple[ScoreCard, ToolEvent]:
|
| 146 |
+
score = score_idea(self.index, idea.title, idea.pitch, idea.goals)
|
| 147 |
idea.score = score.to_dict()
|
| 148 |
return score, ToolEvent("score_idea", f"Pressed a five-quadrant seal: {score.overall}/10.")
|
| 149 |
|
|
|
|
| 158 |
profile_steps = profile_plan_steps(profile)
|
| 159 |
if profile_steps:
|
| 160 |
plan[1:1] = profile_steps
|
| 161 |
+
if any("Well" in goal for goal in idea.goals):
|
| 162 |
plan.insert(
|
| 163 |
max(0, len(plan) - 1),
|
| 164 |
"Prepare a tiny LoRA dataset from successful advisor turns before training.",
|
static/app.js
CHANGED
|
@@ -8,7 +8,7 @@ const corrections = document.querySelector("#corrections");
|
|
| 8 |
const projectsEl = document.querySelector("#projects");
|
| 9 |
const whitespaceEl = document.querySelector("#whitespace");
|
| 10 |
const ideasEl = document.querySelector("#ideas");
|
| 11 |
-
const
|
| 12 |
const profileEl = document.querySelector("#profile");
|
| 13 |
const woodMapEl = document.querySelector("#wood-map");
|
| 14 |
const scoreEl = document.querySelector("#score");
|
|
@@ -22,14 +22,14 @@ const sealCopyEl = document.querySelector("#seal-copy");
|
|
| 22 |
const verdictStampEl = document.querySelector("#verdict-stamp");
|
| 23 |
const spreadEl = document.querySelector("#spread");
|
| 24 |
const ideaCountEl = document.querySelector("#idea-count");
|
| 25 |
-
const
|
| 26 |
const demoButton = document.querySelector("#load-demo");
|
| 27 |
const exportButton = document.querySelector("#export-artifact");
|
| 28 |
const exportNotesButton = document.querySelector("#export-notes");
|
| 29 |
const exportChapterButton = document.querySelector("#export-chapter");
|
| 30 |
const resetButton = document.querySelector("#reset-session");
|
| 31 |
|
| 32 |
-
const SESSION_STORAGE_KEY = "hackathon-advisor-session-
|
| 33 |
const FIELD_NOTES_FILENAME = "hackathon-advisor-field-notes.md";
|
| 34 |
const CHAPTER_FILENAME = "hackathon-advisor-chapter.md";
|
| 35 |
const PNG_EXPORT_LABEL = "PNG";
|
|
@@ -37,9 +37,9 @@ const PNG_EXPORT_LABEL = "PNG";
|
|
| 37 |
let session = {};
|
| 38 |
let clientPromise = Client.connect(window.location.origin);
|
| 39 |
let currentArtifact = null;
|
| 40 |
-
let
|
| 41 |
-
let
|
| 42 |
-
let
|
| 43 |
let profileFields = [];
|
| 44 |
let turnWatchdog = null;
|
| 45 |
let sawTurnToken = false;
|
|
@@ -89,16 +89,16 @@ resetButton.addEventListener("click", () => {
|
|
| 89 |
resetSession();
|
| 90 |
});
|
| 91 |
|
| 92 |
-
|
| 93 |
const target = event.target;
|
| 94 |
-
if (!(target instanceof HTMLInputElement) || !target.dataset.
|
| 95 |
const checked = new Set(
|
| 96 |
-
Array.from(
|
| 97 |
);
|
| 98 |
-
session.
|
| 99 |
-
|
| 100 |
saveSession();
|
| 101 |
-
|
| 102 |
renderIdeas(session.ideas || []);
|
| 103 |
});
|
| 104 |
|
|
@@ -175,15 +175,15 @@ async function bootstrap() {
|
|
| 175 |
if (!response.ok) throw new Error(`project index failed with ${response.status}`);
|
| 176 |
const data = await response.json();
|
| 177 |
bootstrapData = data;
|
| 178 |
-
const rawProfiles = Array.isArray(data.
|
| 179 |
-
const rawOptions = Array.isArray(data.
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
profileFields = data.profile_fields || [];
|
| 184 |
session = normalizeSession(readSavedSession(), defaultSession(data));
|
| 185 |
renderProvenance(data);
|
| 186 |
-
|
| 187 |
renderProfile(session.profile);
|
| 188 |
renderRestoredSession(data);
|
| 189 |
renderWhitespace(data.whitespace || []);
|
|
@@ -205,7 +205,7 @@ function handleBootstrapError(error) {
|
|
| 205 |
renderScore(null);
|
| 206 |
setVerdictDisplay("INDEX CLOSED", 0, null);
|
| 207 |
renderWoodMap(null);
|
| 208 |
-
|
| 209 |
renderProfile({});
|
| 210 |
renderIdeas([]);
|
| 211 |
renderProjects([]);
|
|
@@ -216,7 +216,7 @@ function handleBootstrapError(error) {
|
|
| 216 |
function defaultSession(data = bootstrapData) {
|
| 217 |
return {
|
| 218 |
profile: {},
|
| 219 |
-
|
| 220 |
};
|
| 221 |
}
|
| 222 |
|
|
@@ -292,7 +292,7 @@ function setActionButtonLabel(button, label) {
|
|
| 292 |
|
| 293 |
function setSessionControlsDisabled(disabled) {
|
| 294 |
sessionControlsLocked = disabled;
|
| 295 |
-
|
| 296 |
target.disabled = disabled;
|
| 297 |
});
|
| 298 |
profileEl.querySelectorAll("input[data-profile-field]").forEach((field) => {
|
|
@@ -320,7 +320,7 @@ function resetSession() {
|
|
| 320 |
ink.textContent = "The book is open. The next page waits for its first line.";
|
| 321 |
ink.classList.remove("thinking", "bleed", "gold");
|
| 322 |
corrections.textContent = "Session reset.";
|
| 323 |
-
|
| 324 |
renderProfile(session.profile);
|
| 325 |
renderScore(null);
|
| 326 |
setVerdictDisplay("READY", 0, null);
|
|
@@ -366,7 +366,7 @@ async function loadDemoSession() {
|
|
| 366 |
function applyDemoSession(data) {
|
| 367 |
session = data.session || {};
|
| 368 |
session.profile = session.profile || {};
|
| 369 |
-
session.
|
| 370 |
session.last_response = data.response || session.last_response || "";
|
| 371 |
session.ui_status = `example loaded: ${data.turn_count || 0} advisor turns`;
|
| 372 |
currentArtifact = data.artifact || session.last_artifact || null;
|
|
@@ -378,7 +378,7 @@ function applyDemoSession(data) {
|
|
| 378 |
ink.classList.toggle("bleed", data.score.verdict.startsWith("ECHO"));
|
| 379 |
ink.classList.toggle("gold", data.score.verdict.startsWith("UNWRITTEN"));
|
| 380 |
}
|
| 381 |
-
|
| 382 |
renderProfile(session.profile);
|
| 383 |
renderIdeas(session.ideas || []);
|
| 384 |
renderPlan(data.plan || session.last_plan || []);
|
|
@@ -450,9 +450,9 @@ function normalizeSession(savedSession, defaultSession) {
|
|
| 450 |
const normalized = { ...defaultSession };
|
| 451 |
if (!savedSession) return normalized;
|
| 452 |
normalized.profile = savedSession.profile && typeof savedSession.profile === "object" ? savedSession.profile : {};
|
| 453 |
-
const
|
| 454 |
-
normalized.
|
| 455 |
-
if (!normalized.
|
| 456 |
if (Array.isArray(savedSession.ideas)) normalized.ideas = savedSession.ideas;
|
| 457 |
if (Array.isArray(savedSession.trace)) normalized.trace = savedSession.trace;
|
| 458 |
if (Array.isArray(savedSession.last_plan)) normalized.last_plan = savedSession.last_plan;
|
|
@@ -472,7 +472,7 @@ function restoreSessionCopy() {
|
|
| 472 |
if (status) corrections.textContent = status;
|
| 473 |
}
|
| 474 |
|
| 475 |
-
function
|
| 476 |
const byId = new Map(
|
| 477 |
profiles
|
| 478 |
.filter((profile) => profile && typeof profile.id === "string")
|
|
@@ -504,22 +504,22 @@ function clearSavedSession() {
|
|
| 504 |
}
|
| 505 |
}
|
| 506 |
|
| 507 |
-
function
|
| 508 |
-
const selected = new Set(
|
| 509 |
-
if (
|
| 510 |
-
|
| 511 |
-
if (!
|
| 512 |
-
|
| 513 |
return;
|
| 514 |
}
|
| 515 |
-
for (const option of
|
| 516 |
-
const profile =
|
| 517 |
const label = document.createElement("label");
|
| 518 |
-
label.className = `
|
| 519 |
label.innerHTML = `
|
| 520 |
<input
|
| 521 |
type="checkbox"
|
| 522 |
-
data-
|
| 523 |
aria-label="${escapeAttribute(profile.label)}"
|
| 524 |
${sessionControlsLocked ? "disabled" : ""}
|
| 525 |
${selected.has(option) ? "checked" : ""}
|
|
@@ -527,12 +527,12 @@ function renderTargets(selectedTargets) {
|
|
| 527 |
<span class="check" aria-hidden="true">
|
| 528 |
<svg class="icon"><use href="#icon-check"></use></svg>
|
| 529 |
</span>
|
| 530 |
-
<span class="
|
| 531 |
<strong>${escapeHtml(profile.label)}</strong>
|
| 532 |
${profile.description ? `<small>${escapeHtml(profile.description)}</small>` : ""}
|
| 533 |
</span>
|
| 534 |
`;
|
| 535 |
-
|
| 536 |
}
|
| 537 |
}
|
| 538 |
|
|
@@ -583,7 +583,7 @@ function handleEvent(event) {
|
|
| 583 |
}
|
| 584 |
session = event.state || {};
|
| 585 |
session.profile = session.profile || {};
|
| 586 |
-
session.
|
| 587 |
session.last_response = event.response || session.last_response || "";
|
| 588 |
delete session.ui_status;
|
| 589 |
if (event.score?.echoes?.length) {
|
|
@@ -592,7 +592,7 @@ function handleEvent(event) {
|
|
| 592 |
renderProjects(event.projects);
|
| 593 |
}
|
| 594 |
if (event.whitespace?.length) renderWhitespace(event.whitespace);
|
| 595 |
-
|
| 596 |
renderProfile(session.profile);
|
| 597 |
renderIdeas(session.ideas || []);
|
| 598 |
renderPlan(event.plan || []);
|
|
@@ -622,7 +622,7 @@ function renderIdeas(ideas) {
|
|
| 622 |
}
|
| 623 |
for (const idea of visibleIdeas(ideas)) {
|
| 624 |
const score = idea.score?.overall ? Number(idea.score.overall).toFixed(1) : "0.0";
|
| 625 |
-
const
|
| 626 |
const selected = idea.id === session.current_idea_id;
|
| 627 |
const verdict = idea.score?.verdict || "DRAFT";
|
| 628 |
const isEcho = String(verdict).startsWith("ECHO");
|
|
@@ -639,7 +639,7 @@ function renderIdeas(ideas) {
|
|
| 639 |
</div>
|
| 640 |
<p>${escapeHtml((idea.pitch || "").slice(0, 120))}</p>
|
| 641 |
<span class="iverdict ${isEcho ? "echo" : "unwritten"}">${escapeHtml(verdict)}</span>
|
| 642 |
-
${
|
| 643 |
`;
|
| 644 |
ideasEl.append(item);
|
| 645 |
}
|
|
@@ -663,8 +663,8 @@ function selectIdea(ideaId) {
|
|
| 663 |
if (!idea) return;
|
| 664 |
bumpSessionRevision();
|
| 665 |
session.current_idea_id = idea.id;
|
| 666 |
-
if (Array.isArray(idea.
|
| 667 |
-
session.
|
| 668 |
}
|
| 669 |
const score = idea.score || null;
|
| 670 |
if (score) {
|
|
@@ -682,7 +682,7 @@ function selectIdea(ideaId) {
|
|
| 682 |
renderWoodMap(null);
|
| 683 |
exportButton.disabled = true;
|
| 684 |
}
|
| 685 |
-
|
| 686 |
renderIdeas(session.ideas);
|
| 687 |
renderPlan([]);
|
| 688 |
session.ui_status = `selected: ${idea.title}`;
|
|
@@ -690,8 +690,8 @@ function selectIdea(ideaId) {
|
|
| 690 |
saveSession();
|
| 691 |
}
|
| 692 |
|
| 693 |
-
function
|
| 694 |
-
return
|
| 695 |
}
|
| 696 |
|
| 697 |
function renderScore(score) {
|
|
@@ -873,11 +873,11 @@ function clearTurnWatchdog() {
|
|
| 873 |
}
|
| 874 |
}
|
| 875 |
|
| 876 |
-
function
|
| 877 |
const currentId = session.current_idea_id;
|
| 878 |
if (!currentId || !Array.isArray(session.ideas)) return;
|
| 879 |
const idea = session.ideas.find((item) => item.id === currentId);
|
| 880 |
-
if (idea) idea.
|
| 881 |
}
|
| 882 |
|
| 883 |
async function exportNotes() {
|
|
|
|
| 8 |
const projectsEl = document.querySelector("#projects");
|
| 9 |
const whitespaceEl = document.querySelector("#whitespace");
|
| 10 |
const ideasEl = document.querySelector("#ideas");
|
| 11 |
+
const goalsEl = document.querySelector("#goals");
|
| 12 |
const profileEl = document.querySelector("#profile");
|
| 13 |
const woodMapEl = document.querySelector("#wood-map");
|
| 14 |
const scoreEl = document.querySelector("#score");
|
|
|
|
| 22 |
const verdictStampEl = document.querySelector("#verdict-stamp");
|
| 23 |
const spreadEl = document.querySelector("#spread");
|
| 24 |
const ideaCountEl = document.querySelector("#idea-count");
|
| 25 |
+
const goalCountEl = document.querySelector("#goal-count");
|
| 26 |
const demoButton = document.querySelector("#load-demo");
|
| 27 |
const exportButton = document.querySelector("#export-artifact");
|
| 28 |
const exportNotesButton = document.querySelector("#export-notes");
|
| 29 |
const exportChapterButton = document.querySelector("#export-chapter");
|
| 30 |
const resetButton = document.querySelector("#reset-session");
|
| 31 |
|
| 32 |
+
const SESSION_STORAGE_KEY = "hackathon-advisor-session-v2";
|
| 33 |
const FIELD_NOTES_FILENAME = "hackathon-advisor-field-notes.md";
|
| 34 |
const CHAPTER_FILENAME = "hackathon-advisor-chapter.md";
|
| 35 |
const PNG_EXPORT_LABEL = "PNG";
|
|
|
|
| 37 |
let session = {};
|
| 38 |
let clientPromise = Client.connect(window.location.origin);
|
| 39 |
let currentArtifact = null;
|
| 40 |
+
let goalOptions = [];
|
| 41 |
+
let goalProfiles = [];
|
| 42 |
+
let goalProfileById = new Map();
|
| 43 |
let profileFields = [];
|
| 44 |
let turnWatchdog = null;
|
| 45 |
let sawTurnToken = false;
|
|
|
|
| 89 |
resetSession();
|
| 90 |
});
|
| 91 |
|
| 92 |
+
goalsEl.addEventListener("change", (event) => {
|
| 93 |
const target = event.target;
|
| 94 |
+
if (!(target instanceof HTMLInputElement) || !target.dataset.goal) return;
|
| 95 |
const checked = new Set(
|
| 96 |
+
Array.from(goalsEl.querySelectorAll("input[data-goal]:checked")).map((input) => input.dataset.goal),
|
| 97 |
);
|
| 98 |
+
session.goals = goalOptions.filter((option) => checked.has(option));
|
| 99 |
+
syncCurrentIdeaGoals();
|
| 100 |
saveSession();
|
| 101 |
+
renderGoals(session.goals);
|
| 102 |
renderIdeas(session.ideas || []);
|
| 103 |
});
|
| 104 |
|
|
|
|
| 175 |
if (!response.ok) throw new Error(`project index failed with ${response.status}`);
|
| 176 |
const data = await response.json();
|
| 177 |
bootstrapData = data;
|
| 178 |
+
const rawProfiles = Array.isArray(data.goal_profiles) ? data.goal_profiles : [];
|
| 179 |
+
const rawOptions = Array.isArray(data.goal_options) ? data.goal_options : [];
|
| 180 |
+
goalProfiles = normalizeGoalProfiles(rawProfiles, rawOptions);
|
| 181 |
+
goalOptions = goalProfiles.map((goal) => goal.id);
|
| 182 |
+
goalProfileById = new Map(goalProfiles.map((goal) => [goal.id, goal]));
|
| 183 |
profileFields = data.profile_fields || [];
|
| 184 |
session = normalizeSession(readSavedSession(), defaultSession(data));
|
| 185 |
renderProvenance(data);
|
| 186 |
+
renderGoals(session.goals);
|
| 187 |
renderProfile(session.profile);
|
| 188 |
renderRestoredSession(data);
|
| 189 |
renderWhitespace(data.whitespace || []);
|
|
|
|
| 205 |
renderScore(null);
|
| 206 |
setVerdictDisplay("INDEX CLOSED", 0, null);
|
| 207 |
renderWoodMap(null);
|
| 208 |
+
renderGoals([]);
|
| 209 |
renderProfile({});
|
| 210 |
renderIdeas([]);
|
| 211 |
renderProjects([]);
|
|
|
|
| 216 |
function defaultSession(data = bootstrapData) {
|
| 217 |
return {
|
| 218 |
profile: {},
|
| 219 |
+
goals: data?.default_goals || goalOptions.slice(0, 3),
|
| 220 |
};
|
| 221 |
}
|
| 222 |
|
|
|
|
| 292 |
|
| 293 |
function setSessionControlsDisabled(disabled) {
|
| 294 |
sessionControlsLocked = disabled;
|
| 295 |
+
goalsEl.querySelectorAll("input[data-goal]").forEach((target) => {
|
| 296 |
target.disabled = disabled;
|
| 297 |
});
|
| 298 |
profileEl.querySelectorAll("input[data-profile-field]").forEach((field) => {
|
|
|
|
| 320 |
ink.textContent = "The book is open. The next page waits for its first line.";
|
| 321 |
ink.classList.remove("thinking", "bleed", "gold");
|
| 322 |
corrections.textContent = "Session reset.";
|
| 323 |
+
renderGoals(session.goals);
|
| 324 |
renderProfile(session.profile);
|
| 325 |
renderScore(null);
|
| 326 |
setVerdictDisplay("READY", 0, null);
|
|
|
|
| 366 |
function applyDemoSession(data) {
|
| 367 |
session = data.session || {};
|
| 368 |
session.profile = session.profile || {};
|
| 369 |
+
session.goals = Array.isArray(session.goals) ? session.goals : [];
|
| 370 |
session.last_response = data.response || session.last_response || "";
|
| 371 |
session.ui_status = `example loaded: ${data.turn_count || 0} advisor turns`;
|
| 372 |
currentArtifact = data.artifact || session.last_artifact || null;
|
|
|
|
| 378 |
ink.classList.toggle("bleed", data.score.verdict.startsWith("ECHO"));
|
| 379 |
ink.classList.toggle("gold", data.score.verdict.startsWith("UNWRITTEN"));
|
| 380 |
}
|
| 381 |
+
renderGoals(session.goals);
|
| 382 |
renderProfile(session.profile);
|
| 383 |
renderIdeas(session.ideas || []);
|
| 384 |
renderPlan(data.plan || session.last_plan || []);
|
|
|
|
| 450 |
const normalized = { ...defaultSession };
|
| 451 |
if (!savedSession) return normalized;
|
| 452 |
normalized.profile = savedSession.profile && typeof savedSession.profile === "object" ? savedSession.profile : {};
|
| 453 |
+
const savedGoals = Array.isArray(savedSession.goals) ? savedSession.goals : defaultSession.goals;
|
| 454 |
+
normalized.goals = goalOptions.filter((option) => savedGoals.includes(option));
|
| 455 |
+
if (!normalized.goals.length && defaultSession.goals?.length) normalized.goals = [...defaultSession.goals];
|
| 456 |
if (Array.isArray(savedSession.ideas)) normalized.ideas = savedSession.ideas;
|
| 457 |
if (Array.isArray(savedSession.trace)) normalized.trace = savedSession.trace;
|
| 458 |
if (Array.isArray(savedSession.last_plan)) normalized.last_plan = savedSession.last_plan;
|
|
|
|
| 472 |
if (status) corrections.textContent = status;
|
| 473 |
}
|
| 474 |
|
| 475 |
+
function normalizeGoalProfiles(profiles, options) {
|
| 476 |
const byId = new Map(
|
| 477 |
profiles
|
| 478 |
.filter((profile) => profile && typeof profile.id === "string")
|
|
|
|
| 504 |
}
|
| 505 |
}
|
| 506 |
|
| 507 |
+
function renderGoals(selectedGoals) {
|
| 508 |
+
const selected = new Set(selectedGoals || []);
|
| 509 |
+
if (goalCountEl) goalCountEl.textContent = selected.size;
|
| 510 |
+
goalsEl.innerHTML = "";
|
| 511 |
+
if (!goalOptions.length) {
|
| 512 |
+
goalsEl.innerHTML = `<div class="empty">No goals loaded.</div>`;
|
| 513 |
return;
|
| 514 |
}
|
| 515 |
+
for (const option of goalOptions) {
|
| 516 |
+
const profile = goalProfileById.get(option) || { label: option, description: "" };
|
| 517 |
const label = document.createElement("label");
|
| 518 |
+
label.className = `goal-toggle goal ${selected.has(option) ? "on" : ""}`;
|
| 519 |
label.innerHTML = `
|
| 520 |
<input
|
| 521 |
type="checkbox"
|
| 522 |
+
data-goal="${escapeAttribute(option)}"
|
| 523 |
aria-label="${escapeAttribute(profile.label)}"
|
| 524 |
${sessionControlsLocked ? "disabled" : ""}
|
| 525 |
${selected.has(option) ? "checked" : ""}
|
|
|
|
| 527 |
<span class="check" aria-hidden="true">
|
| 528 |
<svg class="icon"><use href="#icon-check"></use></svg>
|
| 529 |
</span>
|
| 530 |
+
<span class="goal-copy">
|
| 531 |
<strong>${escapeHtml(profile.label)}</strong>
|
| 532 |
${profile.description ? `<small>${escapeHtml(profile.description)}</small>` : ""}
|
| 533 |
</span>
|
| 534 |
`;
|
| 535 |
+
goalsEl.append(label);
|
| 536 |
}
|
| 537 |
}
|
| 538 |
|
|
|
|
| 583 |
}
|
| 584 |
session = event.state || {};
|
| 585 |
session.profile = session.profile || {};
|
| 586 |
+
session.goals = Array.isArray(session.goals) ? session.goals : [];
|
| 587 |
session.last_response = event.response || session.last_response || "";
|
| 588 |
delete session.ui_status;
|
| 589 |
if (event.score?.echoes?.length) {
|
|
|
|
| 592 |
renderProjects(event.projects);
|
| 593 |
}
|
| 594 |
if (event.whitespace?.length) renderWhitespace(event.whitespace);
|
| 595 |
+
renderGoals(session.goals);
|
| 596 |
renderProfile(session.profile);
|
| 597 |
renderIdeas(session.ideas || []);
|
| 598 |
renderPlan(event.plan || []);
|
|
|
|
| 622 |
}
|
| 623 |
for (const idea of visibleIdeas(ideas)) {
|
| 624 |
const score = idea.score?.overall ? Number(idea.score.overall).toFixed(1) : "0.0";
|
| 625 |
+
const goals = (idea.goals || []).slice(0, 3).map(goalDisplayName).join(" · ");
|
| 626 |
const selected = idea.id === session.current_idea_id;
|
| 627 |
const verdict = idea.score?.verdict || "DRAFT";
|
| 628 |
const isEcho = String(verdict).startsWith("ECHO");
|
|
|
|
| 639 |
</div>
|
| 640 |
<p>${escapeHtml((idea.pitch || "").slice(0, 120))}</p>
|
| 641 |
<span class="iverdict ${isEcho ? "echo" : "unwritten"}">${escapeHtml(verdict)}</span>
|
| 642 |
+
${goals ? `<small>${escapeHtml(goals)}</small>` : ""}
|
| 643 |
`;
|
| 644 |
ideasEl.append(item);
|
| 645 |
}
|
|
|
|
| 663 |
if (!idea) return;
|
| 664 |
bumpSessionRevision();
|
| 665 |
session.current_idea_id = idea.id;
|
| 666 |
+
if (Array.isArray(idea.goals) && idea.goals.length) {
|
| 667 |
+
session.goals = goalOptions.filter((option) => idea.goals.includes(option));
|
| 668 |
}
|
| 669 |
const score = idea.score || null;
|
| 670 |
if (score) {
|
|
|
|
| 682 |
renderWoodMap(null);
|
| 683 |
exportButton.disabled = true;
|
| 684 |
}
|
| 685 |
+
renderGoals(session.goals || []);
|
| 686 |
renderIdeas(session.ideas);
|
| 687 |
renderPlan([]);
|
| 688 |
session.ui_status = `selected: ${idea.title}`;
|
|
|
|
| 690 |
saveSession();
|
| 691 |
}
|
| 692 |
|
| 693 |
+
function goalDisplayName(goal) {
|
| 694 |
+
return goalProfileById.get(goal)?.label || goal;
|
| 695 |
}
|
| 696 |
|
| 697 |
function renderScore(score) {
|
|
|
|
| 873 |
}
|
| 874 |
}
|
| 875 |
|
| 876 |
+
function syncCurrentIdeaGoals() {
|
| 877 |
const currentId = session.current_idea_id;
|
| 878 |
if (!currentId || !Array.isArray(session.ideas)) return;
|
| 879 |
const idea = session.ideas.find((item) => item.id === currentId);
|
| 880 |
+
if (idea) idea.goals = [...(session.goals || [])];
|
| 881 |
}
|
| 882 |
|
| 883 |
async function exportNotes() {
|
static/index.html
CHANGED
|
@@ -94,8 +94,8 @@
|
|
| 94 |
</section>
|
| 95 |
|
| 96 |
<section class="section">
|
| 97 |
-
<div class="eyebrow">Goals <span id="
|
| 98 |
-
<div id="
|
| 99 |
</section>
|
| 100 |
</aside>
|
| 101 |
|
|
|
|
| 94 |
</section>
|
| 95 |
|
| 96 |
<section class="section">
|
| 97 |
+
<div class="eyebrow">Goals <span id="goal-count" class="count">0</span></div>
|
| 98 |
+
<div id="goals" class="goal-list"></div>
|
| 99 |
</section>
|
| 100 |
</aside>
|
| 101 |
|
static/styles.css
CHANGED
|
@@ -788,7 +788,7 @@ textarea:disabled {
|
|
| 788 |
.project-list,
|
| 789 |
.whitespace-list,
|
| 790 |
.idea-list,
|
| 791 |
-
.
|
| 792 |
.profile-grid {
|
| 793 |
display: grid;
|
| 794 |
gap: 9px;
|
|
@@ -914,14 +914,14 @@ textarea:disabled {
|
|
| 914 |
|
| 915 |
.idea:disabled,
|
| 916 |
.gap-item:disabled,
|
| 917 |
-
.
|
| 918 |
.profile-field:has(input:disabled) {
|
| 919 |
opacity: 0.64;
|
| 920 |
}
|
| 921 |
|
| 922 |
.idea:focus-visible,
|
| 923 |
.gap-item:focus-visible,
|
| 924 |
-
.
|
| 925 |
.profile-field input:focus-visible,
|
| 926 |
.btn:focus-visible,
|
| 927 |
.prompt:focus-visible {
|
|
@@ -1024,7 +1024,7 @@ textarea:disabled {
|
|
| 1024 |
box-shadow: 0 0 0 3px rgba(47, 107, 65, 0.12);
|
| 1025 |
}
|
| 1026 |
|
| 1027 |
-
.
|
| 1028 |
position: relative;
|
| 1029 |
display: flex;
|
| 1030 |
align-items: flex-start;
|
|
@@ -1037,22 +1037,22 @@ textarea:disabled {
|
|
| 1037 |
transition: background 0.2s, border-color 0.2s, opacity 0.2s;
|
| 1038 |
}
|
| 1039 |
|
| 1040 |
-
.
|
| 1041 |
background: rgba(255, 247, 224, 0.52);
|
| 1042 |
}
|
| 1043 |
|
| 1044 |
-
.
|
| 1045 |
background: rgba(47, 107, 65, 0.07);
|
| 1046 |
border-color: rgba(47, 107, 65, 0.4);
|
| 1047 |
}
|
| 1048 |
|
| 1049 |
-
.
|
| 1050 |
position: absolute;
|
| 1051 |
opacity: 0;
|
| 1052 |
pointer-events: none;
|
| 1053 |
}
|
| 1054 |
|
| 1055 |
-
.
|
| 1056 |
display: grid;
|
| 1057 |
width: 18px;
|
| 1058 |
height: 18px;
|
|
@@ -1065,25 +1065,25 @@ textarea:disabled {
|
|
| 1065 |
transition: background 0.2s, border-color 0.2s, color 0.2s;
|
| 1066 |
}
|
| 1067 |
|
| 1068 |
-
.
|
| 1069 |
color: #f6ecd2;
|
| 1070 |
background: var(--leaf);
|
| 1071 |
border-color: var(--leaf);
|
| 1072 |
}
|
| 1073 |
|
| 1074 |
-
.
|
| 1075 |
width: 11px;
|
| 1076 |
height: 11px;
|
| 1077 |
stroke-width: 2.4;
|
| 1078 |
}
|
| 1079 |
|
| 1080 |
-
.
|
| 1081 |
display: grid;
|
| 1082 |
min-width: 0;
|
| 1083 |
gap: 3px;
|
| 1084 |
}
|
| 1085 |
|
| 1086 |
-
.
|
| 1087 |
color: var(--ink);
|
| 1088 |
font-family: var(--label);
|
| 1089 |
font-size: 0.8rem;
|
|
@@ -1091,7 +1091,7 @@ textarea:disabled {
|
|
| 1091 |
line-height: 1.2;
|
| 1092 |
}
|
| 1093 |
|
| 1094 |
-
.
|
| 1095 |
color: var(--ink-faint);
|
| 1096 |
font-family: var(--label);
|
| 1097 |
font-size: 0.7rem;
|
|
|
|
| 788 |
.project-list,
|
| 789 |
.whitespace-list,
|
| 790 |
.idea-list,
|
| 791 |
+
.goal-list,
|
| 792 |
.profile-grid {
|
| 793 |
display: grid;
|
| 794 |
gap: 9px;
|
|
|
|
| 914 |
|
| 915 |
.idea:disabled,
|
| 916 |
.gap-item:disabled,
|
| 917 |
+
.goal-toggle:has(input:disabled),
|
| 918 |
.profile-field:has(input:disabled) {
|
| 919 |
opacity: 0.64;
|
| 920 |
}
|
| 921 |
|
| 922 |
.idea:focus-visible,
|
| 923 |
.gap-item:focus-visible,
|
| 924 |
+
.goal-toggle:focus-within,
|
| 925 |
.profile-field input:focus-visible,
|
| 926 |
.btn:focus-visible,
|
| 927 |
.prompt:focus-visible {
|
|
|
|
| 1024 |
box-shadow: 0 0 0 3px rgba(47, 107, 65, 0.12);
|
| 1025 |
}
|
| 1026 |
|
| 1027 |
+
.goal-toggle {
|
| 1028 |
position: relative;
|
| 1029 |
display: flex;
|
| 1030 |
align-items: flex-start;
|
|
|
|
| 1037 |
transition: background 0.2s, border-color 0.2s, opacity 0.2s;
|
| 1038 |
}
|
| 1039 |
|
| 1040 |
+
.goal-toggle:hover {
|
| 1041 |
background: rgba(255, 247, 224, 0.52);
|
| 1042 |
}
|
| 1043 |
|
| 1044 |
+
.goal-toggle.on {
|
| 1045 |
background: rgba(47, 107, 65, 0.07);
|
| 1046 |
border-color: rgba(47, 107, 65, 0.4);
|
| 1047 |
}
|
| 1048 |
|
| 1049 |
+
.goal-toggle input {
|
| 1050 |
position: absolute;
|
| 1051 |
opacity: 0;
|
| 1052 |
pointer-events: none;
|
| 1053 |
}
|
| 1054 |
|
| 1055 |
+
.goal-toggle .check {
|
| 1056 |
display: grid;
|
| 1057 |
width: 18px;
|
| 1058 |
height: 18px;
|
|
|
|
| 1065 |
transition: background 0.2s, border-color 0.2s, color 0.2s;
|
| 1066 |
}
|
| 1067 |
|
| 1068 |
+
.goal-toggle.on .check {
|
| 1069 |
color: #f6ecd2;
|
| 1070 |
background: var(--leaf);
|
| 1071 |
border-color: var(--leaf);
|
| 1072 |
}
|
| 1073 |
|
| 1074 |
+
.goal-toggle .check .icon {
|
| 1075 |
width: 11px;
|
| 1076 |
height: 11px;
|
| 1077 |
stroke-width: 2.4;
|
| 1078 |
}
|
| 1079 |
|
| 1080 |
+
.goal-copy {
|
| 1081 |
display: grid;
|
| 1082 |
min-width: 0;
|
| 1083 |
gap: 3px;
|
| 1084 |
}
|
| 1085 |
|
| 1086 |
+
.goal-copy strong {
|
| 1087 |
color: var(--ink);
|
| 1088 |
font-family: var(--label);
|
| 1089 |
font-size: 0.8rem;
|
|
|
|
| 1091 |
line-height: 1.2;
|
| 1092 |
}
|
| 1093 |
|
| 1094 |
+
.goal-copy small {
|
| 1095 |
color: var(--ink-faint);
|
| 1096 |
font-family: var(--label);
|
| 1097 |
font-size: 0.7rem;
|
tests/test_agent.py
CHANGED
|
@@ -175,7 +175,7 @@ def test_planner_get_project_drives_project_response() -> None:
|
|
| 175 |
assert result.tool_events[0].name == "get_project"
|
| 176 |
|
| 177 |
|
| 178 |
-
def
|
| 179 |
index = ProjectIndex.from_files(Path("data/projects.json"), Path("data/project_index.json"))
|
| 180 |
profile_engine = AdvisorEngine(
|
| 181 |
index,
|
|
@@ -189,32 +189,32 @@ def test_planner_profile_and_targets_update_state() -> None:
|
|
| 189 |
targeted = target_engine.turn("set goals", profile.state)
|
| 190 |
|
| 191 |
assert targeted.state["profile"]["skills"] == "frontend"
|
| 192 |
-
assert targeted.state["
|
| 193 |
assert "Local-first, Build notes" in targeted.response
|
| 194 |
|
| 195 |
|
| 196 |
-
def
|
| 197 |
index = ProjectIndex.from_files(Path("data/projects.json"), Path("data/project_index.json"))
|
| 198 |
engine = AdvisorEngine(index)
|
| 199 |
-
state = {"
|
| 200 |
|
| 201 |
first = engine.turn("A local-first archive cartographer for family photos", state)
|
| 202 |
first_idea = first.state["ideas"][0]
|
| 203 |
planned = engine.turn("make a build plan", first.state)
|
| 204 |
|
| 205 |
-
assert first_idea["
|
| 206 |
assert all("LoRA" not in step for step in planned.plan)
|
| 207 |
|
| 208 |
|
| 209 |
-
def
|
| 210 |
index = ProjectIndex.from_files(Path("data/projects.json"), Path("data/project_index.json"))
|
| 211 |
engine = AdvisorEngine(index)
|
| 212 |
-
state = {"
|
| 213 |
|
| 214 |
first = engine.turn("A local-first archive cartographer for family photos", state)
|
| 215 |
planned = engine.turn("make a build plan", first.state)
|
| 216 |
|
| 217 |
-
assert first.state["ideas"][0]["
|
| 218 |
assert any("LoRA" in step for step in planned.plan)
|
| 219 |
|
| 220 |
|
|
|
|
| 175 |
assert result.tool_events[0].name == "get_project"
|
| 176 |
|
| 177 |
|
| 178 |
+
def test_planner_profile_and_goals_update_state() -> None:
|
| 179 |
index = ProjectIndex.from_files(Path("data/projects.json"), Path("data/project_index.json"))
|
| 180 |
profile_engine = AdvisorEngine(
|
| 181 |
index,
|
|
|
|
| 189 |
targeted = target_engine.turn("set goals", profile.state)
|
| 190 |
|
| 191 |
assert targeted.state["profile"]["skills"] == "frontend"
|
| 192 |
+
assert targeted.state["goals"] == ["Off the Grid", "Field Notes"]
|
| 193 |
assert "Local-first, Build notes" in targeted.response
|
| 194 |
|
| 195 |
|
| 196 |
+
def test_session_goals_apply_to_new_and_current_ideas() -> None:
|
| 197 |
index = ProjectIndex.from_files(Path("data/projects.json"), Path("data/project_index.json"))
|
| 198 |
engine = AdvisorEngine(index)
|
| 199 |
+
state = {"goals": ["Field Notes"]}
|
| 200 |
|
| 201 |
first = engine.turn("A local-first archive cartographer for family photos", state)
|
| 202 |
first_idea = first.state["ideas"][0]
|
| 203 |
planned = engine.turn("make a build plan", first.state)
|
| 204 |
|
| 205 |
+
assert first_idea["goals"] == ["Field Notes"]
|
| 206 |
assert all("LoRA" not in step for step in planned.plan)
|
| 207 |
|
| 208 |
|
| 209 |
+
def test_well_tuned_goal_adds_training_step_to_plan() -> None:
|
| 210 |
index = ProjectIndex.from_files(Path("data/projects.json"), Path("data/project_index.json"))
|
| 211 |
engine = AdvisorEngine(index)
|
| 212 |
+
state = {"goals": ["Well-Tuned"]}
|
| 213 |
|
| 214 |
first = engine.turn("A local-first archive cartographer for family photos", state)
|
| 215 |
planned = engine.turn("make a build plan", first.state)
|
| 216 |
|
| 217 |
+
assert first.state["ideas"][0]["goals"] == ["Well-Tuned"]
|
| 218 |
assert any("LoRA" in step for step in planned.plan)
|
| 219 |
|
| 220 |
|
tests/test_app.py
CHANGED
|
@@ -40,10 +40,10 @@ def test_bootstrap_exposes_index_metadata() -> None:
|
|
| 40 |
assert payload["snapshot_digest"]
|
| 41 |
assert payload["runtime"]["tool_count"] >= 8
|
| 42 |
assert payload["top_projects"]
|
| 43 |
-
assert payload["
|
| 44 |
-
assert [
|
| 45 |
-
assert payload["
|
| 46 |
-
assert "description" in payload["
|
| 47 |
assert "skills" in payload["profile_fields"]
|
| 48 |
assert payload["prize_ledger"]["tiny_titan_eligible"] is True
|
| 49 |
|
|
@@ -61,7 +61,7 @@ def test_trace_artifact_endpoint_exports_jsonl() -> None:
|
|
| 61 |
def test_field_notes_endpoint_exports_markdown() -> None:
|
| 62 |
state = engine.turn(
|
| 63 |
"A local-first archive cartographer for family photos",
|
| 64 |
-
{"profile": {"skills": "frontend"}, "
|
| 65 |
).state
|
| 66 |
state = engine.turn("make a build plan", state).state
|
| 67 |
|
|
@@ -92,7 +92,7 @@ def test_chapter_endpoint_exports_markdown() -> None:
|
|
| 92 |
def test_lora_dataset_endpoint_exports_sft_jsonl() -> None:
|
| 93 |
state = engine.turn(
|
| 94 |
"A local-first archive cartographer for family photos",
|
| 95 |
-
{"
|
| 96 |
).state
|
| 97 |
state = engine.turn("make a build plan", state).state
|
| 98 |
|
|
@@ -109,7 +109,7 @@ def test_lora_dataset_endpoint_exports_sft_jsonl() -> None:
|
|
| 109 |
def test_submission_packet_endpoint_exports_markdown() -> None:
|
| 110 |
state = engine.turn(
|
| 111 |
"A local-first archive cartographer for family photos",
|
| 112 |
-
{"
|
| 113 |
).state
|
| 114 |
state = engine.turn("make a build plan", state).state
|
| 115 |
|
|
|
|
| 40 |
assert payload["snapshot_digest"]
|
| 41 |
assert payload["runtime"]["tool_count"] >= 8
|
| 42 |
assert payload["top_projects"]
|
| 43 |
+
assert payload["default_goals"] == payload["goal_options"][:3]
|
| 44 |
+
assert [goal["id"] for goal in payload["goal_profiles"]] == payload["goal_options"]
|
| 45 |
+
assert payload["goal_profiles"][0]["label"] == "Local-first"
|
| 46 |
+
assert "description" in payload["goal_profiles"][0]
|
| 47 |
assert "skills" in payload["profile_fields"]
|
| 48 |
assert payload["prize_ledger"]["tiny_titan_eligible"] is True
|
| 49 |
|
|
|
|
| 61 |
def test_field_notes_endpoint_exports_markdown() -> None:
|
| 62 |
state = engine.turn(
|
| 63 |
"A local-first archive cartographer for family photos",
|
| 64 |
+
{"profile": {"skills": "frontend"}, "goals": ["Field Notes"]},
|
| 65 |
).state
|
| 66 |
state = engine.turn("make a build plan", state).state
|
| 67 |
|
|
|
|
| 92 |
def test_lora_dataset_endpoint_exports_sft_jsonl() -> None:
|
| 93 |
state = engine.turn(
|
| 94 |
"A local-first archive cartographer for family photos",
|
| 95 |
+
{"goals": ["Well-Tuned"]},
|
| 96 |
).state
|
| 97 |
state = engine.turn("make a build plan", state).state
|
| 98 |
|
|
|
|
| 109 |
def test_submission_packet_endpoint_exports_markdown() -> None:
|
| 110 |
state = engine.turn(
|
| 111 |
"A local-first archive cartographer for family photos",
|
| 112 |
+
{"goals": ["Field Notes"]},
|
| 113 |
).state
|
| 114 |
state = engine.turn("make a build plan", state).state
|
| 115 |
|
tests/test_demo_rehearsal.py
CHANGED
|
@@ -2,7 +2,7 @@ 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
|
| 6 |
|
| 7 |
|
| 8 |
def test_demo_rehearsal_builds_complete_session() -> None:
|
|
@@ -19,7 +19,7 @@ def test_demo_rehearsal_builds_complete_session() -> None:
|
|
| 19 |
assert payload["artifact"]["wood_map"]["dots"]
|
| 20 |
assert payload["plan"]
|
| 21 |
assert any("LoRA" in step for step in payload["plan"])
|
| 22 |
-
assert session["
|
| 23 |
assert session["profile"]["constraints"] == "CPU Space runtime; no proprietary inference API"
|
| 24 |
assert len(session["trace"]) == 2
|
| 25 |
assert payload["export_ready"] == {
|
|
|
|
| 2 |
|
| 3 |
from hackathon_advisor.agent import AdvisorEngine
|
| 4 |
from hackathon_advisor.data import ProjectIndex
|
| 5 |
+
from hackathon_advisor.demo_rehearsal import DEMO_GOALS, build_demo_rehearsal
|
| 6 |
|
| 7 |
|
| 8 |
def test_demo_rehearsal_builds_complete_session() -> None:
|
|
|
|
| 19 |
assert payload["artifact"]["wood_map"]["dots"]
|
| 20 |
assert payload["plan"]
|
| 21 |
assert any("LoRA" in step for step in payload["plan"])
|
| 22 |
+
assert session["goals"] == DEMO_GOALS
|
| 23 |
assert session["profile"]["constraints"] == "CPU Space runtime; no proprietary inference API"
|
| 24 |
assert len(session["trace"]) == 2
|
| 25 |
assert payload["export_ready"] == {
|
tests/test_field_notes.py
CHANGED
|
@@ -11,7 +11,7 @@ def test_field_notes_markdown_contains_session_decisions() -> None:
|
|
| 11 |
engine = AdvisorEngine(index)
|
| 12 |
state = {
|
| 13 |
"profile": {"skills": "frontend prototyping"},
|
| 14 |
-
"
|
| 15 |
}
|
| 16 |
first = engine.turn("A local-first archive cartographer for family photos", state)
|
| 17 |
planned = engine.turn("make a build plan", first.state)
|
|
|
|
| 11 |
engine = AdvisorEngine(index)
|
| 12 |
state = {
|
| 13 |
"profile": {"skills": "frontend prototyping"},
|
| 14 |
+
"goals": ["Field Notes"],
|
| 15 |
}
|
| 16 |
first = engine.turn("A local-first archive cartographer for family photos", state)
|
| 17 |
planned = engine.turn("make a build plan", first.state)
|
tests/test_lora_dataset.py
CHANGED
|
@@ -10,7 +10,7 @@ from hackathon_advisor.trace_export import trace_metadata
|
|
| 10 |
def test_lora_dataset_exports_tool_call_and_response_examples() -> None:
|
| 11 |
index = ProjectIndex.from_files(Path("data/projects.json"), Path("data/project_index.json"))
|
| 12 |
engine = AdvisorEngine(index)
|
| 13 |
-
state = {"
|
| 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 |
|
|
@@ -26,14 +26,14 @@ def test_lora_dataset_exports_tool_call_and_response_examples() -> None:
|
|
| 26 |
assert manifest["index"]["algorithm"] == "tfidf-sparse-v1"
|
| 27 |
assert {example["example_kind"] for example in examples} == {"tool_call", "advisor_response"}
|
| 28 |
assert examples[0]["messages"][2]["content"].startswith('<function name="save_idea">')
|
| 29 |
-
assert examples[0]["
|
| 30 |
assert examples[1]["messages"][1]["content"].startswith("A local-first archive")
|
| 31 |
assert "Tool observations:" in examples[1]["messages"][1]["content"]
|
| 32 |
assert examples[1]["messages"][2]["content"]
|
| 33 |
system_messages = "\n".join(example["messages"][0]["content"] for example in examples)
|
| 34 |
assert "Mothback" not in system_messages
|
| 35 |
assert "Build Small" not in system_messages
|
| 36 |
-
assert "prize
|
| 37 |
assert "selected goals" in system_messages
|
| 38 |
|
| 39 |
|
|
|
|
| 10 |
def test_lora_dataset_exports_tool_call_and_response_examples() -> None:
|
| 11 |
index = ProjectIndex.from_files(Path("data/projects.json"), Path("data/project_index.json"))
|
| 12 |
engine = AdvisorEngine(index)
|
| 13 |
+
state = {"goals": ["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 |
|
|
|
|
| 26 |
assert manifest["index"]["algorithm"] == "tfidf-sparse-v1"
|
| 27 |
assert {example["example_kind"] for example in examples} == {"tool_call", "advisor_response"}
|
| 28 |
assert examples[0]["messages"][2]["content"].startswith('<function name="save_idea">')
|
| 29 |
+
assert examples[0]["goals"] == ["Well-Tuned", "Field Notes"]
|
| 30 |
assert examples[1]["messages"][1]["content"].startswith("A local-first archive")
|
| 31 |
assert "Tool observations:" in examples[1]["messages"][1]["content"]
|
| 32 |
assert examples[1]["messages"][2]["content"]
|
| 33 |
system_messages = "\n".join(example["messages"][0]["content"] for example in examples)
|
| 34 |
assert "Mothback" not in system_messages
|
| 35 |
assert "Build Small" not in system_messages
|
| 36 |
+
assert "prize " + "tar" + "gets" not in system_messages
|
| 37 |
assert "selected goals" in system_messages
|
| 38 |
|
| 39 |
|
tests/test_submission_packet.py
CHANGED
|
@@ -10,7 +10,7 @@ from hackathon_advisor.trace_export import trace_metadata
|
|
| 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 = {"
|
| 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 |
|
|
|
|
| 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 = {"goals": ["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 |
|