Spaces:
Running on Zero
Running on Zero
fix: clear stale seals for project references
Browse filesCo-authored-by: Codex <noreply@openai.com>
- hackathon_advisor/model_runtime.py +48 -0
- static/app.js +16 -0
- tests/test_agent.py +17 -0
- tests/test_model_runtime.py +26 -0
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 |
|