JacobLinCool Codex commited on
Commit
3f78502
·
verified ·
1 Parent(s): e8d7906

fix: clear stale seals for project references

Browse files

Co-authored-by: Codex <noreply@openai.com>

hackathon_advisor/model_runtime.py CHANGED
@@ -2,6 +2,7 @@ from __future__ import annotations
2
 
3
  from dataclasses import dataclass
4
  import os
 
5
  from typing import Any, Protocol
6
 
7
  from hackathon_advisor.tools import idea_from_text
@@ -43,8 +44,13 @@ class RuleBasedPlanner:
43
  def plan(self, message: str, state: dict[str, Any]) -> ToolResolution:
44
  text = " ".join(message.strip().split())
45
  lower = text.lower()
 
46
  if not text:
47
  output = '<function name="list_projects">{"sort":"likes"}</function>'
 
 
 
 
48
  elif any(term in lower for term in ("compare", "choose", "rank")):
49
  output = '<function name="compare_ideas">{}</function>'
50
  elif any(term in lower for term in ("plan", "roadmap", "next step", "milestone")):
@@ -179,6 +185,48 @@ def _json_string(value: str) -> str:
179
  return json.dumps(value, ensure_ascii=False)
180
 
181
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
182
  def _title(text: str) -> str:
183
  title = text[:64].strip(" .") or "Unwritten Page"
184
  if any(char.isupper() or char.isdigit() for char in title):
 
2
 
3
  from dataclasses import dataclass
4
  import os
5
+ import re
6
  from typing import Any, Protocol
7
 
8
  from hackathon_advisor.tools import idea_from_text
 
44
  def plan(self, message: str, state: dict[str, Any]) -> ToolResolution:
45
  text = " ".join(message.strip().split())
46
  lower = text.lower()
47
+ project_id = _project_reference_id(text)
48
  if not text:
49
  output = '<function name="list_projects">{"sort":"likes"}</function>'
50
+ elif _wants_project_list(lower):
51
+ output = '<function name="list_projects">{"sort":"likes"}</function>'
52
+ elif project_id:
53
+ output = f'<function name="get_project">{{"id":{_json_string(project_id)}}}</function>'
54
  elif any(term in lower for term in ("compare", "choose", "rank")):
55
  output = '<function name="compare_ideas">{}</function>'
56
  elif any(term in lower for term in ("plan", "roadmap", "next step", "milestone")):
 
185
  return json.dumps(value, ensure_ascii=False)
186
 
187
 
188
+ def _wants_project_list(lower_text: str) -> bool:
189
+ exact_phrases = (
190
+ "projects",
191
+ "spaces",
192
+ "current map",
193
+ "project map",
194
+ )
195
+ command_prefixes = (
196
+ "list projects",
197
+ "list spaces",
198
+ "show projects",
199
+ "show spaces",
200
+ "show current map",
201
+ "show project map",
202
+ "open current map",
203
+ "browse projects",
204
+ "browse spaces",
205
+ )
206
+ return lower_text in exact_phrases or any(lower_text.startswith(prefix) for prefix in command_prefixes)
207
+
208
+
209
+ def _project_reference_id(text: str) -> str:
210
+ prefixes = (
211
+ "read project ",
212
+ "open project ",
213
+ "show project ",
214
+ "read space ",
215
+ "open space ",
216
+ "show space ",
217
+ )
218
+ lower = text.lower()
219
+ raw = ""
220
+ for prefix in prefixes:
221
+ if lower.startswith(prefix):
222
+ raw = text[len(prefix) :].strip()
223
+ break
224
+ if not raw:
225
+ return ""
226
+ raw = re.sub(r"^https?://huggingface\.co/spaces/", "", raw, flags=re.IGNORECASE)
227
+ return raw.split()[0].strip(".,;:!?\"'")
228
+
229
+
230
  def _title(text: str) -> str:
231
  title = text[:64].strip(" .") or "Unwritten Page"
232
  if any(char.isupper() or char.isdigit() for char in title):
static/app.js CHANGED
@@ -598,10 +598,15 @@ function handleEvent(event) {
598
  session.goals = Array.isArray(session.goals) ? session.goals : [];
599
  session.last_response = event.response || session.last_response || "";
600
  delete session.ui_status;
 
 
 
601
  if (event.score?.echoes?.length) {
602
  renderCitations(event.score.echoes);
603
  } else if (event.projects?.length) {
604
  renderProjects(event.projects);
 
 
605
  }
606
  if (event.whitespace?.length) renderWhitespace(event.whitespace);
607
  renderGoals(session.goals);
@@ -613,6 +618,8 @@ function handleEvent(event) {
613
  renderScore(event.score);
614
  ink.classList.toggle("bleed", event.score.verdict.startsWith("ECHO"));
615
  ink.classList.toggle("gold", event.score.verdict.startsWith("UNWRITTEN"));
 
 
616
  } else if (!event.projects?.length) {
617
  const idea = currentIdea();
618
  renderSelectedIdeaSeal(idea);
@@ -627,6 +634,15 @@ function handleEvent(event) {
627
  }
628
  }
629
 
 
 
 
 
 
 
 
 
 
630
  function renderIdeas(ideas) {
631
  if (ideaCountEl) ideaCountEl.textContent = ideas.length;
632
  ideasEl.innerHTML = "";
 
598
  session.goals = Array.isArray(session.goals) ? session.goals : [];
599
  session.last_response = event.response || session.last_response || "";
600
  delete session.ui_status;
601
+ const toolName = session.last_tool_resolution?.call?.name || "";
602
+ const projectReferenceOnly =
603
+ !event.score && !event.artifact?.title && ["list_projects", "get_project"].includes(toolName);
604
  if (event.score?.echoes?.length) {
605
  renderCitations(event.score.echoes);
606
  } else if (event.projects?.length) {
607
  renderProjects(event.projects);
608
+ } else if (projectReferenceOnly) {
609
+ renderProjects([], "No project page matched this request.");
610
  }
611
  if (event.whitespace?.length) renderWhitespace(event.whitespace);
612
  renderGoals(session.goals);
 
618
  renderScore(event.score);
619
  ink.classList.toggle("bleed", event.score.verdict.startsWith("ECHO"));
620
  ink.classList.toggle("gold", event.score.verdict.startsWith("UNWRITTEN"));
621
+ } else if (projectReferenceOnly) {
622
+ renderProjectReferenceState();
623
  } else if (!event.projects?.length) {
624
  const idea = currentIdea();
625
  renderSelectedIdeaSeal(idea);
 
634
  }
635
  }
636
 
637
+ function renderProjectReferenceState() {
638
+ currentArtifact = null;
639
+ renderScore(null);
640
+ setVerdictDisplay("READY", 0, null);
641
+ sealCopyEl.textContent = "Project pages are shown below. Score an idea to press a new seal.";
642
+ ink.classList.remove("bleed", "gold");
643
+ renderWoodMap(null);
644
+ }
645
+
646
  function renderIdeas(ideas) {
647
  if (ideaCountEl) ideaCountEl.textContent = ideas.length;
648
  ideasEl.innerHTML = "";
tests/test_agent.py CHANGED
@@ -179,6 +179,23 @@ def test_planner_get_project_drives_project_response() -> None:
179
  assert result.tool_events[0].name == "get_project"
180
 
181
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
182
  def test_planner_profile_and_goals_update_state() -> None:
183
  index = ProjectIndex.from_files(Path("data/projects.json"), Path("data/project_index.json"))
184
  planned = AdvisorEngine(index).turn("A local-first archive cartographer for family photos", {})
 
179
  assert result.tool_events[0].name == "get_project"
180
 
181
 
182
+ def test_rule_project_reference_does_not_create_or_score_idea() -> None:
183
+ index = ProjectIndex.from_files(Path("data/projects.json"), Path("data/project_index.json"))
184
+ engine = AdvisorEngine(index)
185
+ first = engine.turn("A local-first archive cartographer for family photos", {})
186
+
187
+ result = engine.turn("read project lolaby", first.state)
188
+
189
+ assert result.projects
190
+ assert result.projects[0].slug == "lolaby"
191
+ assert result.score is None
192
+ assert result.artifact == {}
193
+ assert len(result.state["ideas"]) == 1
194
+ assert result.state["ideas"][0]["title"] == first.artifact["title"]
195
+ assert result.state["last_artifact"]["title"] == first.artifact["title"]
196
+ assert result.state["last_tool_resolution"]["call"]["name"] == "get_project"
197
+
198
+
199
  def test_planner_profile_and_goals_update_state() -> None:
200
  index = ProjectIndex.from_files(Path("data/projects.json"), Path("data/project_index.json"))
201
  planned = AdvisorEngine(index).turn("A local-first archive cartographer for family photos", {})
tests/test_model_runtime.py CHANGED
@@ -50,6 +50,32 @@ def test_rule_planner_defaults_blank_to_list_projects() -> None:
50
  assert resolution.call.name == "list_projects"
51
 
52
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  def test_rule_planner_splits_explicit_idea_pitch() -> None:
54
  planner = RuleBasedPlanner()
55
 
 
50
  assert resolution.call.name == "list_projects"
51
 
52
 
53
+ def test_rule_planner_routes_project_reference_commands() -> None:
54
+ planner = RuleBasedPlanner()
55
+
56
+ listed = planner.plan("show current map", {})
57
+ project = planner.plan("read project lolaby", {})
58
+ project_url = planner.plan("open space https://huggingface.co/spaces/build-small-hackathon/lolaby", {})
59
+
60
+ assert listed.status == "valid"
61
+ assert listed.call.name == "list_projects"
62
+ assert project.status == "valid"
63
+ assert project.call.name == "get_project"
64
+ assert project.call.arguments["id"] == "lolaby"
65
+ assert project_url.status == "valid"
66
+ assert project_url.call.name == "get_project"
67
+ assert project_url.call.arguments["id"] == "build-small-hackathon/lolaby"
68
+
69
+
70
+ def test_rule_planner_keeps_project_words_inside_ideas() -> None:
71
+ planner = RuleBasedPlanner()
72
+
73
+ resolution = planner.plan("A dashboard that helps teams show projects to mentors", {})
74
+
75
+ assert resolution.status == "valid"
76
+ assert resolution.call.name == "save_idea"
77
+
78
+
79
  def test_rule_planner_splits_explicit_idea_pitch() -> None:
80
  planner = RuleBasedPlanner()
81