Spaces:
Running on Zero
Running on Zero
fix: persist artifacts for scored ideas
Browse filesCo-authored-by: Codex <noreply@openai.com>
- hackathon_advisor/agent.py +18 -1
- hackathon_advisor/tools.py +2 -0
- static/app.js +5 -2
- tests/test_agent.py +20 -0
hackathon_advisor/agent.py
CHANGED
|
@@ -106,6 +106,7 @@ class AdvisorEngine:
|
|
| 106 |
tool_events.append(event)
|
| 107 |
response = self._whitespace_response(idea, whitespace, score)
|
| 108 |
artifact = self._artifact(idea, score)
|
|
|
|
| 109 |
return self._result(
|
| 110 |
normalized,
|
| 111 |
corrections,
|
|
@@ -173,6 +174,11 @@ class AdvisorEngine:
|
|
| 173 |
stored.append(idea.to_dict())
|
| 174 |
state["ideas"] = stored
|
| 175 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 176 |
def _current_idea(self, state: dict[str, Any]) -> Idea | None:
|
| 177 |
current_id = state.get("current_idea_id")
|
| 178 |
for item in state.get("ideas", []):
|
|
@@ -222,6 +228,7 @@ class AdvisorEngine:
|
|
| 222 |
tool_events.append(event)
|
| 223 |
response = self._whitespace_response(idea, whitespace, score)
|
| 224 |
artifact = self._artifact(idea, score)
|
|
|
|
| 225 |
return self._result(
|
| 226 |
normalized,
|
| 227 |
corrections,
|
|
@@ -260,6 +267,7 @@ class AdvisorEngine:
|
|
| 260 |
tool_events.append(event)
|
| 261 |
response = self._plan_response(idea, score, plan)
|
| 262 |
artifact = self._artifact(idea, score)
|
|
|
|
| 263 |
return self._result(normalized, corrections, response, state, tool_events, [], [], score, plan, artifact)
|
| 264 |
|
| 265 |
def _compare_turn(
|
|
@@ -278,6 +286,8 @@ class AdvisorEngine:
|
|
| 278 |
)
|
| 279 |
return self._result(normalized, corrections, response, state, tool_events, [], [], None, [], {})
|
| 280 |
|
|
|
|
|
|
|
| 281 |
ideas = [idea for idea, _score in ranked]
|
| 282 |
state["ideas"] = [idea.to_dict() for idea in ideas]
|
| 283 |
winner, score = ranked[0]
|
|
@@ -286,7 +296,8 @@ class AdvisorEngine:
|
|
| 286 |
plan, event = self.tools.make_plan(winner, self._profile_context(state))
|
| 287 |
tool_events.append(event)
|
| 288 |
response = self._compare_response(ranked, plan)
|
| 289 |
-
artifact = self._artifact(winner, score)
|
|
|
|
| 290 |
return self._result(normalized, corrections, response, state, tool_events, [], [], score, plan, artifact)
|
| 291 |
|
| 292 |
def _project_turn(
|
|
@@ -329,6 +340,7 @@ class AdvisorEngine:
|
|
| 329 |
tool_events.append(event)
|
| 330 |
response = f"The wax seal reads {score.overall}/10, {score.verdict}, for {idea.title}."
|
| 331 |
artifact = self._artifact(idea, score)
|
|
|
|
| 332 |
return self._result(normalized, corrections, response, state, tool_events, [], [], score, [], artifact)
|
| 333 |
|
| 334 |
def _profile_turn(
|
|
@@ -360,7 +372,12 @@ class AdvisorEngine:
|
|
| 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")
|
|
|
|
| 106 |
tool_events.append(event)
|
| 107 |
response = self._whitespace_response(idea, whitespace, score)
|
| 108 |
artifact = self._artifact(idea, score)
|
| 109 |
+
self._attach_artifact(state, idea, artifact)
|
| 110 |
return self._result(
|
| 111 |
normalized,
|
| 112 |
corrections,
|
|
|
|
| 174 |
stored.append(idea.to_dict())
|
| 175 |
state["ideas"] = stored
|
| 176 |
|
| 177 |
+
def _attach_artifact(self, state: dict[str, Any], idea: Idea, artifact: dict[str, Any]) -> None:
|
| 178 |
+
idea.artifact = artifact
|
| 179 |
+
self._store_idea(state, idea)
|
| 180 |
+
state["last_artifact"] = artifact
|
| 181 |
+
|
| 182 |
def _current_idea(self, state: dict[str, Any]) -> Idea | None:
|
| 183 |
current_id = state.get("current_idea_id")
|
| 184 |
for item in state.get("ideas", []):
|
|
|
|
| 228 |
tool_events.append(event)
|
| 229 |
response = self._whitespace_response(idea, whitespace, score)
|
| 230 |
artifact = self._artifact(idea, score)
|
| 231 |
+
self._attach_artifact(state, idea, artifact)
|
| 232 |
return self._result(
|
| 233 |
normalized,
|
| 234 |
corrections,
|
|
|
|
| 267 |
tool_events.append(event)
|
| 268 |
response = self._plan_response(idea, score, plan)
|
| 269 |
artifact = self._artifact(idea, score)
|
| 270 |
+
self._attach_artifact(state, idea, artifact)
|
| 271 |
return self._result(normalized, corrections, response, state, tool_events, [], [], score, plan, artifact)
|
| 272 |
|
| 273 |
def _compare_turn(
|
|
|
|
| 286 |
)
|
| 287 |
return self._result(normalized, corrections, response, state, tool_events, [], [], None, [], {})
|
| 288 |
|
| 289 |
+
for idea, idea_score in ranked:
|
| 290 |
+
idea.artifact = self._artifact(idea, idea_score)
|
| 291 |
ideas = [idea for idea, _score in ranked]
|
| 292 |
state["ideas"] = [idea.to_dict() for idea in ideas]
|
| 293 |
winner, score = ranked[0]
|
|
|
|
| 296 |
plan, event = self.tools.make_plan(winner, self._profile_context(state))
|
| 297 |
tool_events.append(event)
|
| 298 |
response = self._compare_response(ranked, plan)
|
| 299 |
+
artifact = winner.artifact or self._artifact(winner, score)
|
| 300 |
+
self._attach_artifact(state, winner, artifact)
|
| 301 |
return self._result(normalized, corrections, response, state, tool_events, [], [], score, plan, artifact)
|
| 302 |
|
| 303 |
def _project_turn(
|
|
|
|
| 340 |
tool_events.append(event)
|
| 341 |
response = f"The wax seal reads {score.overall}/10, {score.verdict}, for {idea.title}."
|
| 342 |
artifact = self._artifact(idea, score)
|
| 343 |
+
self._attach_artifact(state, idea, artifact)
|
| 344 |
return self._result(normalized, corrections, response, state, tool_events, [], [], score, [], artifact)
|
| 345 |
|
| 346 |
def _profile_turn(
|
|
|
|
| 372 |
idea = self._current_idea(state)
|
| 373 |
if idea is not None:
|
| 374 |
idea.goals = goals
|
| 375 |
+
idea.score = None
|
| 376 |
+
idea.artifact = None
|
| 377 |
self._store_idea(state, idea)
|
| 378 |
+
last_artifact = state.get("last_artifact")
|
| 379 |
+
if isinstance(last_artifact, dict) and last_artifact.get("title") == idea.title:
|
| 380 |
+
del state["last_artifact"]
|
| 381 |
tool_events.append(ToolEvent("set_goals", f"Set {len(goals)} goals."))
|
| 382 |
labels = [goal_label(goal) for goal in goals]
|
| 383 |
response = "The seal will now bias toward: " + (", ".join(labels) or "no specific goals")
|
hackathon_advisor/tools.py
CHANGED
|
@@ -89,6 +89,7 @@ class Idea:
|
|
| 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:
|
| 94 |
return {
|
|
@@ -97,6 +98,7 @@ class Idea:
|
|
| 97 |
"pitch": self.pitch,
|
| 98 |
"goals": self.goals,
|
| 99 |
"score": self.score,
|
|
|
|
| 100 |
}
|
| 101 |
|
| 102 |
|
|
|
|
| 89 |
pitch: str
|
| 90 |
goals: list[str] = field(default_factory=lambda: GOALS[:3])
|
| 91 |
score: dict | None = None
|
| 92 |
+
artifact: dict[str, Any] | None = None
|
| 93 |
|
| 94 |
def to_dict(self) -> dict:
|
| 95 |
return {
|
|
|
|
| 98 |
"pitch": self.pitch,
|
| 99 |
"goals": self.goals,
|
| 100 |
"score": self.score,
|
| 101 |
+
"artifact": self.artifact,
|
| 102 |
}
|
| 103 |
|
| 104 |
|
static/app.js
CHANGED
|
@@ -712,8 +712,10 @@ function renderSelectedIdeaSeal(idea) {
|
|
| 712 |
}
|
| 713 |
|
| 714 |
function renderSelectedIdeaArtifact(idea) {
|
| 715 |
-
|
| 716 |
-
|
|
|
|
|
|
|
| 717 |
renderWoodMap(currentArtifact.wood_map || null);
|
| 718 |
exportButton.disabled = false;
|
| 719 |
return;
|
|
@@ -934,6 +936,7 @@ function invalidateCurrentPlan(message) {
|
|
| 934 |
|
| 935 |
function clearCurrentArtifactFor(idea) {
|
| 936 |
if (!idea || currentArtifact?.title === idea.title) currentArtifact = null;
|
|
|
|
| 937 |
if (!idea || session.last_artifact?.title === idea.title) delete session.last_artifact;
|
| 938 |
}
|
| 939 |
|
|
|
|
| 712 |
}
|
| 713 |
|
| 714 |
function renderSelectedIdeaArtifact(idea) {
|
| 715 |
+
const artifact = idea.artifact || (session.last_artifact?.title === idea.title ? session.last_artifact : null);
|
| 716 |
+
if (artifact) {
|
| 717 |
+
currentArtifact = artifact;
|
| 718 |
+
session.last_artifact = artifact;
|
| 719 |
renderWoodMap(currentArtifact.wood_map || null);
|
| 720 |
exportButton.disabled = false;
|
| 721 |
return;
|
|
|
|
| 936 |
|
| 937 |
function clearCurrentArtifactFor(idea) {
|
| 938 |
if (!idea || currentArtifact?.title === idea.title) currentArtifact = null;
|
| 939 |
+
if (idea?.artifact) delete idea.artifact;
|
| 940 |
if (!idea || session.last_artifact?.title === idea.title) delete session.last_artifact;
|
| 941 |
}
|
| 942 |
|
tests/test_agent.py
CHANGED
|
@@ -33,6 +33,7 @@ def test_agent_scores_and_persists_idea() -> None:
|
|
| 33 |
assert result.state["last_tool_resolution"]["call"]["name"] == "save_idea"
|
| 34 |
assert result.state["trace"][0]["tool_resolution"]["call"]["name"] == "save_idea"
|
| 35 |
assert result.state["last_artifact"]["title"] == result.artifact["title"]
|
|
|
|
| 36 |
assert result.artifact["wood_map"]["caption"]
|
| 37 |
assert {dot["kind"] for dot in result.artifact["wood_map"]["dots"]} >= {"idea", "echo", "inked"}
|
| 38 |
assert result.score.to_dict()["echoes"][0]["page_number"] >= 1
|
|
@@ -133,6 +134,8 @@ def test_distinct_idea_turns_append_to_board() -> None:
|
|
| 133 |
assert len(second.state["ideas"]) == 2
|
| 134 |
assert second.state["ideas"][0]["title"] == first.artifact["title"]
|
| 135 |
assert second.state["ideas"][1]["title"] == second.artifact["title"]
|
|
|
|
|
|
|
| 136 |
|
| 137 |
|
| 138 |
def test_compare_ideas_reranks_board_and_selects_winner() -> None:
|
|
@@ -147,6 +150,7 @@ def test_compare_ideas_reranks_board_and_selects_winner() -> None:
|
|
| 147 |
assert ranked.artifact["title"] == ranked.state["ideas"][0]["title"]
|
| 148 |
assert ranked.state["current_idea_id"] == ranked.state["ideas"][0]["id"]
|
| 149 |
assert ranked.state["ideas"][0]["score"]["overall"] >= ranked.state["ideas"][1]["score"]["overall"]
|
|
|
|
| 150 |
assert ranked.plan
|
| 151 |
assert "Ranked pages:" in ranked.response
|
| 152 |
assert ranked.tool_events[0].name == "compare_ideas"
|
|
@@ -193,6 +197,22 @@ def test_planner_profile_and_goals_update_state() -> None:
|
|
| 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)
|
|
|
|
| 33 |
assert result.state["last_tool_resolution"]["call"]["name"] == "save_idea"
|
| 34 |
assert result.state["trace"][0]["tool_resolution"]["call"]["name"] == "save_idea"
|
| 35 |
assert result.state["last_artifact"]["title"] == result.artifact["title"]
|
| 36 |
+
assert result.state["ideas"][0]["artifact"]["title"] == result.artifact["title"]
|
| 37 |
assert result.artifact["wood_map"]["caption"]
|
| 38 |
assert {dot["kind"] for dot in result.artifact["wood_map"]["dots"]} >= {"idea", "echo", "inked"}
|
| 39 |
assert result.score.to_dict()["echoes"][0]["page_number"] >= 1
|
|
|
|
| 134 |
assert len(second.state["ideas"]) == 2
|
| 135 |
assert second.state["ideas"][0]["title"] == first.artifact["title"]
|
| 136 |
assert second.state["ideas"][1]["title"] == second.artifact["title"]
|
| 137 |
+
assert second.state["ideas"][0]["artifact"]["title"] == first.artifact["title"]
|
| 138 |
+
assert second.state["ideas"][1]["artifact"]["title"] == second.artifact["title"]
|
| 139 |
|
| 140 |
|
| 141 |
def test_compare_ideas_reranks_board_and_selects_winner() -> None:
|
|
|
|
| 150 |
assert ranked.artifact["title"] == ranked.state["ideas"][0]["title"]
|
| 151 |
assert ranked.state["current_idea_id"] == ranked.state["ideas"][0]["id"]
|
| 152 |
assert ranked.state["ideas"][0]["score"]["overall"] >= ranked.state["ideas"][1]["score"]["overall"]
|
| 153 |
+
assert all(idea["artifact"]["title"] == idea["title"] for idea in ranked.state["ideas"])
|
| 154 |
assert ranked.plan
|
| 155 |
assert "Ranked pages:" in ranked.response
|
| 156 |
assert ranked.tool_events[0].name == "compare_ideas"
|
|
|
|
| 197 |
assert "Local-first, Build notes" in targeted.response
|
| 198 |
|
| 199 |
|
| 200 |
+
def test_goal_update_invalidates_current_idea_artifact() -> None:
|
| 201 |
+
index = ProjectIndex.from_files(Path("data/projects.json"), Path("data/project_index.json"))
|
| 202 |
+
first = AdvisorEngine(index).turn("A local-first archive cartographer for family photos", {})
|
| 203 |
+
target_engine = AdvisorEngine(
|
| 204 |
+
index,
|
| 205 |
+
planner=StaticPlanner(ToolCall("set_goals", {"goals": ["Field Notes"]})),
|
| 206 |
+
)
|
| 207 |
+
|
| 208 |
+
targeted = target_engine.turn("set goals", first.state)
|
| 209 |
+
|
| 210 |
+
idea = targeted.state["ideas"][0]
|
| 211 |
+
assert idea["score"] is None
|
| 212 |
+
assert idea["artifact"] is None
|
| 213 |
+
assert "last_artifact" not in targeted.state
|
| 214 |
+
|
| 215 |
+
|
| 216 |
def test_session_goals_apply_to_new_and_current_ideas() -> None:
|
| 217 |
index = ProjectIndex.from_files(Path("data/projects.json"), Path("data/project_index.json"))
|
| 218 |
engine = AdvisorEngine(index)
|