build-small-hackathon/hackathon-advisor
16
1from __future__ import annotations2 3from dataclasses import dataclass4 5from hackathon_advisor.data import ProjectIndex, SearchHit, tokenize6 7 8@dataclass(frozen=True)9class ScoreCard:10 originality: int11 delight: int12 ai_necessity: int13 feasibility: int14 goal_fit: int15 verdict: str16 echoes: tuple[SearchHit, ...]17 18 @property19 def overall(self) -> float:20 return round(21 (22 self.originality * 0.3023 + self.delight * 0.2024 + self.ai_necessity * 0.2025 + self.feasibility * 0.1526 + self.goal_fit * 0.1527 ),28 1,29 )30 31 def to_dict(self) -> dict:32 return {33 "originality": self.originality,34 "delight": self.delight,35 "ai_necessity": self.ai_necessity,36 "feasibility": self.feasibility,37 "goal_fit": self.goal_fit,38 "overall": self.overall,39 "verdict": self.verdict,40 "echoes": [41 {42 "score": round(hit.score, 3),43 "page_number": hit.page_number,44 "matched_terms": list(hit.matched_terms),45 "project": hit.project.to_public_dict(),46 }47 for hit in self.echoes48 ],49 }50 51 52def 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.056 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)61 ai_necessity = clamp_score(62 363 + _keyword_count(tokens, {"agent", "model", "embed", "search", "personal", "speech", "local"}) * 264 )65 complexity_penalty = _keyword_count(tokens, {"realtime", "video", "multiplayer", "payments", "social"})66 feasibility = clamp_score(8 - complexity_penalty)67 goal_fit = clamp_score(68 469 + _keyword_count(tokens, {"local", "offline", "small", "llama", "fine", "trace", "gradio"}) * 270 + 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(74 originality=originality,75 delight=delight,76 ai_necessity=ai_necessity,77 feasibility=feasibility,78 goal_fit=goal_fit,79 verdict=verdict,80 echoes=tuple(hits),81 )82 83 84def clamp_score(value: int) -> int:85 return max(1, min(10, value))86 87 88def _keyword_count(tokens: set[str], keywords: set[str]) -> int:89 return sum(1 for keyword in keywords if any(token.startswith(keyword) for token in tokens))90 