from __future__ import annotations import re from typing import Any def normalize_token(value: str) -> str: value = re.sub(r"[^a-z0-9]+", " ", value.lower()) return " ".join(part for part in value.split() if part) def extract_ingredients(payload: dict[str, Any]) -> list[str]: out: list[str] = [] seen: set[str] = set() for item in payload.get("items") or []: name = normalize_token(str(item.get("name") or "")) if not name or name in seen: continue seen.add(name) out.append(name) return out def extract_tags(payload: dict[str, Any]) -> list[str]: return [normalize_token(str(tag)) for tag in (payload.get("tags") or []) if normalize_token(str(tag))] def score_recipe(query: str, payload: dict[str, Any], profile: dict[str, Any]) -> tuple[int, list[str]]: score = 50 reasons: list[str] = [] haystack = " ".join( [ normalize_token(str(payload.get("name") or "")), normalize_token(str(payload.get("description") or "")), " ".join(extract_ingredients(payload)), " ".join(extract_tags(payload)), ] ) query_terms = [term for term in normalize_token(query).split() if len(term) > 2] if query_terms: matched = sum(1 for term in query_terms if term in haystack) score += matched * 6 if matched: reasons.append(f"Matches query terms: {matched}") ingredients = extract_ingredients(payload) tags = extract_tags(payload) approved_ingredients = {normalize_token(k): int(v) for k, v in (profile.get("approved_ingredients") or {}).items()} rejected_ingredients = {normalize_token(k): int(v) for k, v in (profile.get("rejected_ingredients") or {}).items()} approved_tags = {normalize_token(k): int(v) for k, v in (profile.get("approved_tags") or {}).items()} rejected_tags = {normalize_token(k): int(v) for k, v in (profile.get("rejected_tags") or {}).items()} soft_blacklist = {normalize_token(item.get("name", "")) for item in (profile.get("soft_blacklist") or []) if item.get("enabled")} for term in sorted(soft_blacklist): if term and term in haystack: score -= 30 reasons.append(f"Soft blacklist term: {term}") liked_hits = 0 for ingredient in ingredients: liked_hits += approved_ingredients.get(ingredient, 0) score -= min(rejected_ingredients.get(ingredient, 0) * 7, 18) for tag in tags: liked_hits += approved_tags.get(tag, 0) score -= min(rejected_tags.get(tag, 0) * 5, 12) if liked_hits: score += min(liked_hits * 3, 18) reasons.append("Aligned with prior approvals") title = str(payload.get("name") or "") if len(title.split()) <= 10: score += 3 reasons.append("Clear short title") total_time = payload.get("time") if isinstance(total_time, int): if total_time <= 45: score += 6 reasons.append("Weeknight-friendly time") elif total_time >= 120: score -= 6 description = str(payload.get("description") or "") if description.count("\n") >= 3: score += 4 reasons.append("Structured instructions") return max(0, min(100, score)), reasons