from __future__ import annotations from dataclasses import dataclass, field from typing import Any import uuid from hackathon_advisor.data import Project, ProjectIndex, WhitespaceItem from hackathon_advisor.scoring import ScoreCard, score_idea GOALS = [ "Off the Grid", "Well-Tuned", "Off-Brand", "Llama Champion", "Sharing is Caring", "Field Notes", ] GOAL_PROFILE_BY_ID = { "Off the Grid": { "label": "Local-first", "description": "Favor ideas that work without proprietary inference APIs.", }, "Well-Tuned": { "label": "Trainable", "description": "Shape good examples into a tiny fine-tune dataset.", }, "Off-Brand": { "label": "Distinct voice", "description": "Leave room for an interface and tone people remember.", }, "Llama Champion": { "label": "llama.cpp path", "description": "Prefer small-model choices that can run locally.", }, "Sharing is Caring": { "label": "Shareable artifact", "description": "Make an output people can save, post, or compare.", }, "Field Notes": { "label": "Build notes", "description": "Keep decisions easy to write up from the saved session.", }, } def goal_profiles() -> list[dict[str, str]]: return [ { "id": goal, "label": GOAL_PROFILE_BY_ID[goal]["label"], "description": GOAL_PROFILE_BY_ID[goal]["description"], } for goal in GOALS ] def goal_label(goal: str) -> str: return GOAL_PROFILE_BY_ID.get(goal, {}).get("label", goal) def normalize_goals(raw_goals: Any, default: list[str] | None = None) -> list[str]: if raw_goals is None: return list(default or []) if not isinstance(raw_goals, list): return list(default or []) goals: list[str] = [] seen: set[str] = set() for raw_goal in raw_goals: goal = str(raw_goal) if goal in GOALS and goal not in seen: goals.append(goal) seen.add(goal) return goals def goals_from_state(state: dict[str, Any]) -> list[str]: if "goals" not in state: return GOALS[:3] return normalize_goals(state.get("goals"), default=[]) @dataclass class Idea: id: str title: str pitch: str goals: list[str] = field(default_factory=lambda: GOALS[:3]) score: dict | None = None artifact: dict[str, Any] | None = None def to_dict(self) -> dict: return { "id": self.id, "title": self.title, "pitch": self.pitch, "goals": self.goals, "score": self.score, "artifact": self.artifact, } @dataclass(frozen=True) class ToolEvent: name: str summary: str def to_dict(self) -> dict: return {"name": self.name, "summary": self.summary} class AdvisorTools: def __init__(self, index: ProjectIndex) -> None: self.index = index def list_projects(self, limit: int = 8) -> tuple[list[Project], ToolEvent]: projects = self.index.top_projects(limit=limit) return projects, ToolEvent("list_projects", f"Read {len(projects)} prominent Space cards.") def search_projects(self, query: str, limit: int = 5) -> tuple[list[Project], ToolEvent]: hits = self.index.search(query, limit=limit) projects = [hit.project for hit in hits] return projects, ToolEvent("search_projects", f"Found {len(projects)} nearby Space echoes.") def find_whitespace(self, limit: int = 5) -> tuple[list[WhitespaceItem], ToolEvent]: items = self.index.find_whitespace(limit=limit) return items, ToolEvent("find_whitespace", f"Ranked {len(items)} under-explored regions.") def save_idea(self, state: dict[str, Any], title: str, pitch: str) -> tuple[Idea, ToolEvent]: ideas = [Idea(**item) for item in state.get("ideas", [])] current_id = state.get("current_idea_id") goals = goals_from_state(state) idea = next((item for item in ideas if item.id == current_id), None) if idea is None or _is_new_idea(idea, title, pitch): idea = Idea(id=uuid.uuid4().hex[:8], title=title, pitch=pitch, goals=goals) ideas.append(idea) else: idea.title = title idea.pitch = pitch idea.goals = goals state["ideas"] = [item.to_dict() for item in ideas] state["current_idea_id"] = idea.id return idea, ToolEvent("save_idea", f"Wrote idea page '{idea.title}'.") def score_idea(self, idea: Idea) -> tuple[ScoreCard, ToolEvent]: score = score_idea(self.index, idea.title, idea.pitch, idea.goals) idea.score = score.to_dict() return score, ToolEvent("score_idea", f"Pressed a five-quadrant seal: {score.overall}/10.") def make_plan(self, idea: Idea, profile: dict[str, Any] | None = None) -> tuple[list[str], ToolEvent]: plan = [ "Lock a one-sentence promise and one test input that proves what is different.", "Compare against the nearest echoes, then sharpen the part only this idea can own.", "Build the smallest happy path: input, nearby project citations, score, and one shareable output.", "Add one selected-goal feature only after the core loop is smooth enough to explain without narration.", "Write build notes from the exact decisions, screenshots, and outputs.", ] profile_steps = profile_plan_steps(profile) if profile_steps: plan[1:1] = profile_steps if any("Well" in goal for goal in idea.goals): plan.insert( max(0, len(plan) - 1), "Collect successful advisor examples before training a tiny LoRA.", ) return plan, ToolEvent("make_plan", f"Drafted {len(plan)} build steps.") def idea_from_text(text: str) -> tuple[str, str]: cleaned = " ".join(text.strip().split()) if not cleaned: return "Blank Page", "A project direction waiting for one concrete user and one concrete tension." title = cleaned for prefix in ("i want to build", "build", "make", "my idea is", "idea:"): if cleaned.lower().startswith(prefix): title = cleaned[len(prefix) :].strip(" :-") break pitch = cleaned explicit_pitch = False if " -- " in title: title, pitch = (part.strip() for part in title.split(" -- ", 1)) explicit_pitch = True raw_title = title title = raw_title[:64].strip(" .") or "Unwritten Page" if len(raw_title) > 64 or (not explicit_pitch and len(title) < len(cleaned)): title = f"{title[:58].strip()}..." return _display_title(title), pitch def _is_new_idea(current: Idea, title: str, pitch: str) -> bool: return current.title.strip().casefold() != title.strip().casefold() or current.pitch.strip() != pitch.strip() def profile_plan_steps(profile: dict[str, Any] | None) -> list[str]: if not isinstance(profile, dict): return [] steps: list[str] = [] time = _short_profile_value(profile.get("time")) skills = _short_profile_value(profile.get("skills")) constraints = _short_profile_value(profile.get("constraints")) preferences = _short_profile_value(profile.get("preferences")) if time: steps.append(f"Scope the first prototype to {time}; cut anything that cannot fit that window.") if skills: steps.append(f"Use your {skills} strength for the first working surface before adding new tooling.") if constraints: steps.append(f"Test the constraint early: {constraints}. Do this before polishing the artifact.") if preferences: steps.append(f"Shape the demo around {preferences} so the result feels intentional, not generic.") return steps def _short_profile_value(value: Any, limit: int = 84) -> str: text = " ".join(str(value or "").split()) if len(text) <= limit: return text return text[: limit - 3].rstrip(" ,.;:") + "..." def _display_title(title: str) -> str: if not title: return "Unwritten Page" if any(char.isupper() or char.isdigit() for char in title): return title[0].upper() + title[1:] return title.capitalize()