from __future__ import annotations import hashlib import json import logging import os import re from backend.env import load_project_env from backend.models import SpaceItem from backend.storage import cache_get, cache_set from backend.tracks import TRACK_NAMES logger = logging.getLogger(__name__) load_project_env() QUERY_PROFILE_CACHE_VERSION = "v6" REASON_CACHE_VERSION = "v22" BLURB_CACHE_VERSION = "v10" RERANK_CACHE_VERSION = "v11" NEBIUS_BASE_URL = "https://api.tokenfactory.nebius.com/v1/" NEBIUS_MODEL = os.getenv("NEBIUS_MODEL", "nvidia/Nemotron-3-Nano-Omni") class LLMService: def __init__(self) -> None: self._client = None def available(self) -> bool: return self._load_client() is not None @staticmethod def _coerce_space(space: SpaceItem | dict) -> SpaceItem: if isinstance(space, SpaceItem): return space if isinstance(space, dict): readme = str(space.get("readme", space.get("readme_text", ""))) summary = str(space.get("summary", space.get("desc", ""))) zone = str(space.get("zone", space.get("category", "Other"))) return SpaceItem.from_dict( { "repo_id": space.get("repo_id", space.get("id", "")), "title": space.get("title", space.get("name", "")), "summary": summary, "url": space.get("url", ""), "zone": zone, "track": space.get("track", ""), "tags": space.get("tags", []), "difficulty": space.get("difficulty", "casual"), "likes": space.get("likes", 0), "sdk": space.get("sdk", "unknown"), "status": space.get("status", "unknown"), "last_modified": space.get("last_modified", ""), "emoji": space.get("emoji", "🚀"), "readme_text": readme, } ) raise TypeError(f"Unsupported space type: {type(space)!r}") @staticmethod def _coerce_spaces(spaces: list[SpaceItem | dict]) -> list[SpaceItem]: return [LLMService._coerce_space(space) for space in spaces] @staticmethod def _coerce_semantic_profile(profile) -> dict: if isinstance(profile, dict): return dict(profile) if isinstance(profile, str): try: parsed = json.loads(profile) except Exception: return {} if isinstance(parsed, dict): return parsed return {} @staticmethod def _normalize_text(text: str) -> str: return " ".join((text or "").split()) @staticmethod def _strip_clause(text: str) -> str: clause = LLMService._normalize_text(text).strip(" .:-") clause = re.sub(r"^(?:the|this|that)\s+(?:app|space|project)\s+", "", clause, flags=re.I) clause = re.sub(r"^(?:it|you|users?|people|we)\s+", "", clause, flags=re.I) clause = re.sub(r"^(?:can|could|will|would|should|lets?|helps?|allows?|enables?|generates?|creates?|shows?|returns?|tracks?|flags?|surfaces?|guides?|provides?|keeps?|turns?|makes?)\s+", "", clause, flags=re.I) return clause.strip(" .:-") @staticmethod def _query_fit_phrase(query: str, liked_text: str = "", semantic_profile: dict | None = None) -> str: normalized = (query or "").lower() combined = f"{query or ''} {liked_text or ''}".lower() if any(phrase in combined for phrase in ("playing cards", "card game", "card-based", "card matching", "deckbuilder", "poker")): return "users looking for casual card-based games" if any(phrase in combined for phrase in ("tutor", "teaching", "lesson", "study", "education")): return "users looking for tutor or learning apps" if any(phrase in combined for phrase in ("writing", "journal", "story", "creative")): return "people looking for writing or storytelling apps" if any(phrase in combined for phrase in ("news", "headline", "article", "report")): return "people looking for simple news explainers" if any(phrase in combined for phrase in ("security", "privacy", "cyber", "phishing", "scam", "fraud", "malware")): return "people looking for security and privacy tools" profile = LLMService._coerce_semantic_profile(semantic_profile) audience = str(profile.get("audience", "")).strip() if audience: return audience if normalized.strip(): return f"people exploring {normalized.strip()}" return "people browsing this category" @staticmethod def _activity_phrase(summary: str, semantic_profile: dict | None = None, evidence: str = "") -> str: profile = LLMService._coerce_semantic_profile(semantic_profile) for key in ("primary_activity", "activity", "one_liner"): value = str(profile.get(key, "")).strip() if value: return value for source in (evidence, summary): text = str(source or "").strip() if text: clause = LLMService._strip_clause(text) if clause: return clause return "a focused app" @staticmethod def _first_profile_clause(profile: dict | None, keys: tuple[str, ...]) -> str: data = LLMService._coerce_semantic_profile(profile) for key in keys: value = data.get(key) if isinstance(value, list): for item in value: clause = LLMService._strip_clause(str(item)) if clause: return clause elif value: clause = LLMService._strip_clause(str(value)) if clause: return clause return "" @staticmethod def _sentence_from_clause(prefix: str, clause: str) -> str: clause = LLMService._strip_clause(clause) if not clause: return "" first_word = clause.split(" ", 1)[0].lower() if prefix.lower() == "it" and first_word in {"helps", "lets", "allows", "enables", "generates", "creates", "shows", "returns", "tracks", "flags", "surfaces", "guides", "provides", "keeps", "turns", "makes"}: return f"It {clause}" return f"{prefix} {clause}" @staticmethod def _build_local_recommendation_reason( *, query: str, title: str, summary: str, evidence: str, semantic_profile: dict | None = None, liked_text: str = "", ) -> str: profile = LLMService._coerce_semantic_profile(semantic_profile) activity = LLMService._activity_phrase(summary, profile, evidence) action_clause = LLMService._first_profile_clause(profile, ("core_actions", "key_snippets")) value_clause = LLMService._first_profile_clause(profile, ("value_points", "outcomes", "key_snippets")) fit_phrase = LLMService._query_fit_phrase(query, liked_text=liked_text, semantic_profile=profile) first_sentence = f"{title or 'This app'} offers {activity}" if action_clause: first_sentence = f"{first_sentence} where you {action_clause}" first_sentence = f"{first_sentence}, making it a great fit for {fit_phrase}" second_sentence = "" if value_clause: second_sentence = LLMService._sentence_from_clause("Its", value_clause) if not second_sentence: second_sentence = f"That makes it a strong fit for {fit_phrase}" reason = f"{first_sentence}. {second_sentence}." reason = LLMService._normalize_output(reason) if len(reason.split(". ")) > 2: reason = " ".join(reason.split(". ")[:2]).strip() return reason @staticmethod def _candidate_reason(candidate: dict, query: str, profile: dict | None = None) -> str: title = str(candidate.get("title") or candidate.get("name") or "").strip() summary = str(candidate.get("summary") or candidate.get("description") or candidate.get("desc") or "").strip() semantic_profile = candidate.get("semantic_profile") if isinstance(candidate.get("semantic_profile"), dict) else {} if not semantic_profile and isinstance(profile, dict): semantic_profile = profile evidence = str(candidate.get("readme_excerpt") or candidate.get("readme_summary") or summary or "").strip() reason = LLMService._build_local_recommendation_reason( query=query, title=title, summary=summary, evidence=evidence, semantic_profile=semantic_profile, ) return reason @staticmethod def _tokenize_terms(text: str) -> list[str]: terms: list[str] = [] for token in re.findall(r"[a-z0-9]+", (text or "").lower()): if len(token) > 2 and token not in terms: terms.append(token) return terms @staticmethod def _candidate_text(candidate: dict) -> str: parts = [ str(candidate.get("title") or candidate.get("name") or ""), str(candidate.get("summary") or candidate.get("description") or candidate.get("desc") or ""), str(candidate.get("track") or ""), str(candidate.get("category") or candidate.get("zone") or ""), " ".join(str(tag) for tag in (candidate.get("tags", []) or [])[:8]), str(candidate.get("readme_excerpt") or candidate.get("readme_summary") or candidate.get("readme") or ""), ] profile = candidate.get("semantic_profile") if isinstance(candidate.get("semantic_profile"), dict) else {} if profile: for key in ("primary_activity", "audience", "value_proposition", "value_points", "core_actions", "key_snippets"): value = profile.get(key) if isinstance(value, list): parts.extend(str(item) for item in value if str(item).strip()) elif value: parts.append(str(value)) return LLMService._normalize_text(" ".join(parts)) @staticmethod def _score_candidate_text(query_terms: list[str], candidate_text: str) -> tuple[float, list[str]]: source = candidate_text.lower() hits = [term for term in query_terms if term and term in source] return float(len(hits)), hits def _load_client(self): if self._client is False: return None if self._client is not None: return self._client api_key = os.environ.get("NEBIUS_API_KEY", "").strip() if not api_key: logger.warning("NEBIUS_API_KEY is not configured; falling back to local review copy.") self._client = False return None try: from openai import OpenAI except Exception as exc: logger.warning("openai package is unavailable; falling back to local review copy: %s", exc) self._client = False return None try: self._client = OpenAI(base_url=NEBIUS_BASE_URL, api_key=api_key) return self._client except Exception as exc: logger.warning("Nebius client initialization failed; falling back to local review copy: %s", exc) self._client = False return None def _chat_via_http( self, *, messages: list[dict], max_tokens: int, temperature: float, response_format: dict | None = None, reasoning_effort: str | None = None, ) -> str: return "" @staticmethod def _normalize_output(text: str) -> str: text = (text or "").strip() text = text.replace("\r", " ").replace("\n", " ") text = re.sub(r".*?", " ", text, flags=re.S | re.I) if text.lower().startswith("") and "" in text.lower(): text = text.split("", 1)[1] text = text.replace("`", "") text = text.replace("*", "") text = text.replace("|", " ") text = " ".join(text.split()) text = text.removeprefix("Why it fits:") text = text.removeprefix("Reason:") return text.strip() @staticmethod def _unwrap_reason_text(text: str) -> str: reason = LLMService._normalize_output(text) if reason.startswith("{") and '"reason"' in reason: try: payload = json.loads(reason) if isinstance(payload, dict): reason = LLMService._normalize_output(str(payload.get("reason", ""))) except Exception: pass return reason @staticmethod def _finalize_reason_text(text: str) -> str: reason = LLMService._unwrap_reason_text(text) if not reason: return "" reason = re.sub(r"\s+", " ", reason).strip(" .:-") if not reason: return "" sentence_parts = re.split(r"(?<=[.!?])\s+", reason) if len(sentence_parts) > 2: reason = " ".join(sentence_parts[:2]).strip() return reason.strip() @staticmethod def _parse_json_text(text: str, expected: str = "dict"): raw = (text or "").strip() if not raw: return None candidates = [raw] if raw.startswith("```"): fenced = re.sub(r"^```(?:json)?\s*|\s*```$", "", raw, flags=re.S | re.I).strip() if fenced: candidates.append(fenced) for opener, closer in (("{", "}"), ("[", "]")): start = raw.find(opener) end = raw.rfind(closer) if start != -1 and end != -1 and end > start: sliced = raw[start : end + 1].strip() if sliced: candidates.append(sliced) for candidate in candidates: candidate = re.sub(r".*?", " ", candidate, flags=re.S | re.I).strip() try: parsed = json.loads(candidate) except Exception: continue if expected == "dict" and isinstance(parsed, dict): return parsed if expected == "list" and isinstance(parsed, list): return parsed if expected == "any": return parsed if isinstance(parsed, str): try: nested = json.loads(parsed) except Exception: continue if expected == "dict" and isinstance(nested, dict): return nested if expected == "list" and isinstance(nested, list): return nested return None @staticmethod def _looks_generic_reason(text: str) -> bool: lowered = (text or "").lower() if not lowered.strip(): return True generic_phrases = [ "this space appears related", "this one stands out because", "the readme points to", "it fits the query", "core interaction", "broad category label", "", "this feels relevant to", "which makes the connection feel concrete rather than generic", "the main interaction already lines up", "it seems to focus on", ] if any(phrase in lowered for phrase in generic_phrases): return True code_markers = ["def ", "import ", "from ", "=>", "::", "()", "filename=", "duration=", "type="] if sum(1 for marker in code_markers if marker in lowered) >= 2: return True return False @staticmethod def _extract_message_text(message_content) -> str: if isinstance(message_content, str): return message_content.strip() if isinstance(message_content, list): parts: list[str] = [] for item in message_content: if isinstance(item, dict): item_type = str(item.get("type", "")).strip().lower() if item_type == "text": value = str(item.get("text", "")).strip() if value: parts.append(value) elif item: parts.append(str(item).strip()) return "\n".join(part for part in parts if part).strip() if message_content is None: return "" return str(message_content).strip() @staticmethod def _preview_object(value, limit: int = 2000) -> str: try: if hasattr(value, "model_dump"): value = value.model_dump() elif hasattr(value, "to_dict"): value = value.to_dict() elif hasattr(value, "__dict__") and not isinstance(value, (str, bytes, dict, list, tuple)): value = dict(value.__dict__) text = json.dumps(value, ensure_ascii=False, default=str) except Exception: text = str(value) text = text.replace("\n", " ") return text[:limit] @staticmethod def _extract_choice_text(choice) -> str: if choice is None: return "" message = getattr(choice, "message", None) if message is None and isinstance(choice, dict): message = choice.get("message") if message is None: return "" content = getattr(message, "content", None) if content is None and isinstance(message, dict): content = message.get("content") text = LLMService._extract_message_text(content) if text: return text for attr in ("parsed",): value = getattr(message, attr, None) if value is None and isinstance(message, dict): value = message.get(attr) if value: text = LLMService._extract_message_text(value) if text: return text return "" @staticmethod def _prepare_messages(messages: list[dict]) -> list[dict]: prepared: list[dict] = [] for message in messages: if not isinstance(message, dict): continue role = str(message.get("role", "user")).strip() or "user" content = message.get("content", "") if role == "system" or isinstance(content, list): prepared.append({"role": role, "content": content}) continue prepared.append( { "role": role, "content": [ { "type": "text", "text": str(content), } ], } ) return prepared def chat( self, messages: list[dict], cache_key: str | None = None, max_tokens: int = 180, temperature: float = 0.4, response_format: dict | None = None, reasoning_effort: str | None = None, ) -> str: if cache_key: cached = cache_get(cache_key) if cached: return cached client = self._load_client() if client is None: return "" try: response = client.chat.completions.create( model=NEBIUS_MODEL, messages=self._prepare_messages(messages), max_tokens=max_tokens, temperature=temperature, ) choice = response.choices[0] if getattr(response, "choices", None) else None text = self._extract_choice_text(choice) if cache_key and text: cache_set(cache_key, text) return text except Exception as exc: logger.warning("Nebius review generation failed; falling back to local review copy: %s", exc) return "" def _chat( self, system_prompt: str, user_prompt: str, cache_key: str | None = None, max_tokens: int = 180, temperature: float = 0.4, response_format: dict | None = None, reasoning_effort: str | None = None, ) -> str: return self.chat( [ {"role": "system", "content": system_prompt}, {"role": "user", "content": [{"type": "text", "text": user_prompt}]}, ], cache_key=cache_key, max_tokens=max_tokens, temperature=temperature, response_format=response_format, reasoning_effort=reasoning_effort, ) def rewrite_recommendation_reason( self, *, query: str, repo_id: str = "", title: str, summary: str, track: str, zone: str, tags: list[str], evidence: str, matched_signals: list[str] | None = None, liked_text: str = "", semantic_profile: dict | None = None, readme_text: str = "", ) -> str: matched_signals = matched_signals or [] cache_payload = { "version": REASON_CACHE_VERSION, "query": query, "repo_id": repo_id, "title": title, "summary": summary, "track": track, "zone": zone, "tags": tags[:8], "evidence": evidence[:900], "matched_signals": matched_signals[:6], "liked_text": liked_text, "semantic_profile": self._coerce_semantic_profile(semantic_profile), "readme_text": readme_text[:4000], } cache_key = "reason:" + hashlib.sha1( json.dumps(cache_payload, sort_keys=True, ensure_ascii=False).encode("utf-8") ).hexdigest() cached = cache_get(cache_key) if cached: reason = self._finalize_reason_text(cached) if reason: return reason profile = self._coerce_semantic_profile(semantic_profile) readme_excerpt = self._normalize_text(readme_text)[:12000] system_prompt = ( "You write the visible Why it fits text for an app recommendation card. " "Return only the final user-facing blurb, never reasoning or analysis. " "Output exactly 2 short sentences. " "Use a warm, natural, editorial product-review tone. " "Sentence 1 must say what the app experience actually is. " "Sentence 2 must explain why it matches the user's query. " "Do not mention the README, instructions, metadata, evidence, or evaluation process." ) user_prompt = "\n".join( [ f"User query: {query or 'none'}", f"App name: {title or 'This app'}", f"Short summary: {summary or 'not provided'}", f"Track: {track or 'unknown'}", f"Category: {zone or 'Other'}", f"Tags: {', '.join(tags[:8]) or 'none'}", f"Helpful matching signals: {', '.join(matched_signals[:6]) or 'none'}", f"Semantic profile: {json.dumps(profile, ensure_ascii=False) if profile else '{}'}", "Write in this tone:", "MatchWise offers a fun card-playing experience where you flip and match cards, making it a great fit for users looking for casual card-based games. Its endless AI-generated challenges keep the gameplay fresh and engaging.", "README content:", readme_excerpt or evidence or summary or "not available", "Return only the final 2 sentences.", ] ) text = self._chat( system_prompt, user_prompt, cache_key=cache_key, max_tokens=80, temperature=0.1, ) reason = self._finalize_reason_text(text) if text else "" if not reason: retry_prompt = "\n".join( [ f"User query: {query or 'none'}", f"App name: {title or 'This app'}", f"Summary: {summary or 'not provided'}", f"Evidence: {evidence or 'not available'}", "Return only the final answer. No thinking. No analysis. No draft. Exactly 2 sentences.", ] ) text = self._chat( system_prompt, retry_prompt, max_tokens=80, temperature=0.0, ) reason = self._finalize_reason_text(text) if text else "" if not reason or self._looks_generic_reason(reason): reason = self._build_local_recommendation_reason( query=query, title=title, summary=summary, evidence=evidence, semantic_profile=semantic_profile, liked_text=liked_text, ) if reason: cache_set(cache_key, reason) return reason return "" def generate_reason(self, query: str, space: SpaceItem) -> str: return self.rewrite_recommendation_reason( query=query, title=space.title, summary=space.summary, track=space.track, zone=space.zone, tags=space.tags, evidence=space.readme_text[:900].strip() or space.summary, matched_signals=[space.track] if space.track else [], readme_text=space.readme_text, ) def generate_recommendation_blurb(self, query: str, space: SpaceItem) -> dict: cache_key = f"{BLURB_CACHE_VERSION}:blurb:{space.repo_id}:{query.lower().strip()}" cached = cache_get(cache_key) if cached: try: payload = json.loads(cached) if isinstance(payload, dict): payload.setdefault("summary", "") payload.setdefault("reason", "") payload["reason"] = self._finalize_reason_text(payload.get("reason", "")) return payload except Exception: pass readme_excerpt = space.readme_text[:900].strip() reason = self.rewrite_recommendation_reason( query=query, title=space.title, summary=space.summary, track=space.track, zone=space.zone, tags=space.tags, evidence=readme_excerpt or space.summary, matched_signals=[space.track] if space.track else [], readme_text=space.readme_text, ) payload = {"summary": space.summary or "", "reason": reason} if reason: cache_set(cache_key, json.dumps(payload, ensure_ascii=False)) return payload def generate_query_profile(self, query: str, spaces: list[SpaceItem | dict]) -> dict: cache_key = f"{QUERY_PROFILE_CACHE_VERSION}:profile:{query.lower().strip()}" cached = cache_get(cache_key) if cached: try: payload = json.loads(cached) if isinstance(payload, dict): return payload except Exception: pass coerced_spaces = self._coerce_spaces(spaces) sample_spaces = sorted(coerced_spaces, key=lambda item: (-item.likes, item.title.lower()))[:12] query_terms = self._tokenize_terms(query) fallback = self._fallback_query_profile(query, coerced_spaces) track_scores = {track: 0.0 for track in TRACK_NAMES} for space in sample_spaces: candidate_text = self._candidate_text( { "title": space.title, "summary": space.summary, "track": space.track, "zone": space.zone, "tags": space.tags, "readme_excerpt": space.readme_text[:300], "semantic_profile": {}, } ) score, _ = self._score_candidate_text(query_terms, candidate_text) if space.track in track_scores: track_scores[space.track] += score + (space.likes / 1000.0) primary_track = max(track_scores.items(), key=lambda item: item[1])[0] if any(track_scores.values()) else fallback.get("primary_track") keywords = query_terms[:8] if not keywords and query.strip(): keywords = [query.strip()] must_have = keywords[:4] avoid = [term for term in fallback.get("avoid", []) if term] confidence = 0.25 if query_terms: confidence += min(0.5, len(query_terms) * 0.08) if primary_track: confidence += 0.1 payload = { "primary_track": primary_track, "keywords": keywords, "must_have": must_have, "avoid": avoid, "summary": query.strip() or fallback.get("summary", ""), "confidence": round(min(confidence, 0.95), 2) if query else 0.0, } cache_set(cache_key, json.dumps(payload, ensure_ascii=False)) return payload def generate_example_queries(self, spaces: list[SpaceItem | dict], track_names: list[str]) -> list[str]: cache_key = "example_queries:v2" cached = cache_get(cache_key) if cached: try: payload = json.loads(cached) if isinstance(payload, list) and payload: return [str(item) for item in payload][:8] except Exception: pass coerced_spaces = self._coerce_spaces(spaces) fallback = self._fallback_example_queries(coerced_spaces) cache_set(cache_key, json.dumps(fallback, ensure_ascii=False)) return fallback @staticmethod def _candidate_prompt_payload(candidate: dict) -> dict: return { "repo_id": str(candidate.get("repo_id") or candidate.get("id") or "").strip(), "title": str(candidate.get("title") or candidate.get("name") or "").strip(), "summary": str(candidate.get("summary") or candidate.get("description") or candidate.get("desc") or "").strip(), "track": str(candidate.get("track") or "").strip(), "category": str(candidate.get("category") or candidate.get("zone") or "").strip(), "tags": list(candidate.get("tags", []) or [])[:8], "likes": int(candidate.get("likes", 0) or 0), "readme_excerpt": str( candidate.get("readme_excerpt") or candidate.get("readme_summary") or candidate.get("readme", "") or "" )[:900], "signals": list(candidate.get("matched_signals", []) or [])[:6], } def rerank_candidates(self, query: str, candidates: list[dict], profile: dict | None = None, limit: int = 12) -> list[dict]: if not candidates: return [] profile = profile or {} prompt_candidates = [self._candidate_prompt_payload(item) for item in candidates] candidate_ids = ",".join(item.get("repo_id", "") for item in prompt_candidates) cache_key = f"{RERANK_CACHE_VERSION}:rerank:" + hashlib.sha1( f"{query.lower().strip()}|{candidate_ids}|{json.dumps(profile, sort_keys=True, ensure_ascii=False)}".encode( "utf-8" ) ).hexdigest() cached = cache_get(cache_key) if cached: try: payload = json.loads(cached) if isinstance(payload, list): return [item for item in payload if isinstance(item, dict)][:limit] except Exception: pass query_terms = self._tokenize_terms(query) profile_terms = [] for key in ("keywords", "must_have", "avoid", "summary"): value = profile.get(key, []) if isinstance(value, list): profile_terms.extend(str(item) for item in value if str(item).strip()) elif value: profile_terms.append(str(value)) profile_terms = self._tokenize_terms(" ".join(profile_terms)) scored_candidates: list[dict] = [] for item in candidates: repo_id = str(item.get("repo_id", "")).strip() if not repo_id: continue candidate_text = self._candidate_text(item) query_score, query_hits = self._score_candidate_text(query_terms, candidate_text) profile_score, profile_hits = self._score_candidate_text(profile_terms, candidate_text) if profile_terms else (0.0, []) signal_text = " ".join(str(signal) for signal in (item.get("signals", []) or []) if str(signal).strip()) signal_score, signal_hits = self._score_candidate_text(query_terms, signal_text) if signal_text else (0.0, []) likes = float(item.get("likes", 0) or 0) likes_bonus = min(likes / 5000.0, 0.12) score = query_score * 5.0 + profile_score * 2.0 + signal_score * 1.5 + likes_bonus if not query_terms and not profile_terms: score += float(item.get("rank_score", 0) or 0) / 100.0 if query_terms and not query_hits and not profile_hits and not signal_hits: score *= 0.15 reason = self._candidate_reason(item, query, profile) scored_candidates.append( { "repo_id": repo_id, "reason": reason, "rank_score": round(min(max(score * 10.0, 0.0), 100.0), 2), } ) scored_candidates.sort(key=lambda item: (-float(item.get("rank_score", 0) or 0), str(item.get("repo_id", "")))) fallback = scored_candidates[:limit] cache_set(cache_key, json.dumps(fallback, ensure_ascii=False)) return fallback def _fallback_example_queries(self, spaces: list[SpaceItem]) -> list[str]: ordered = sorted(spaces, key=lambda s: (-s.likes, s.title.lower())) buckets: dict[str, list[SpaceItem]] = {} for space in ordered: buckets.setdefault(space.track or "Other", []).append(space) queries: list[str] = [] for track, track_spaces in buckets.items(): if track_spaces: space = track_spaces[0] queries.append(f"find apps like {space.title.lower()} for {track.lower()}") while len(queries) < 8: queries.extend( [ "fun learning app for beginners", "creative story or writing space", "useful AI tools for builders", "game or puzzle space to try", ] ) deduped: list[str] = [] for query in queries: if query not in deduped: deduped.append(query) return deduped[:8] def _fallback_query_profile(self, query: str, spaces: list[SpaceItem]) -> dict: tokens = [token for token in query.lower().split() if len(token) > 2] track_scores = {track: 0 for track in TRACK_NAMES} for token in tokens: if token in {"agent", "llm", "llama", "gguf", "api", "builder", "trace", "dataset", "career", "job", "security", "cybersecurity", "privacy", "audit", "phishing", "scam", "fraud", "vulnerability", "threat"}: track_scores["Backyard AI"] += 1 if token in {"game", "quiz", "puzzle", "learning", "study", "story", "creative", "language", "translate", "demo"}: track_scores["An Adventure in Thousand Token Wood"] += 1 primary_track = max(track_scores.items(), key=lambda item: item[1])[0] if any(track_scores.values()) else None return { "primary_track": primary_track, "keywords": tokens[:8], "must_have": tokens[:4], "avoid": [], "summary": query, "confidence": 0.35 if query else 0.0, } _SERVICE: LLMService | None = None def get_llm_service() -> LLMService: global _SERVICE if _SERVICE is None: _SERVICE = LLMService() return _SERVICE def generate_recommendation_reason(query: str, space: SpaceItem) -> str: return get_llm_service().generate_reason(query, space) def generate_recommendation_blurb(query: str, space: SpaceItem) -> dict: return get_llm_service().generate_recommendation_blurb(query, space) def generate_example_queries(spaces: list[SpaceItem], track_names: list[str]) -> list[str]: return get_llm_service().generate_example_queries(spaces, track_names) def generate_query_profile(query: str, spaces: list[SpaceItem]) -> dict: return get_llm_service().generate_query_profile(query, spaces) def rerank_candidates(query: str, candidates: list[dict], profile: dict | None = None, limit: int = 12) -> list[dict]: return get_llm_service().rerank_candidates(query, candidates, profile=profile, limit=limit)