82 lines
3.2 KiB
Python
82 lines
3.2 KiB
Python
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
|