73 lines
2.9 KiB
Python
73 lines
2.9 KiB
Python
from __future__ import annotations
|
|
|
|
from collections import Counter
|
|
from typing import Any
|
|
|
|
from app.services.repository import repo
|
|
|
|
|
|
def _overlap(left: list[str], right: list[str]) -> list[str]:
|
|
return sorted(set(left) & set(right))
|
|
|
|
|
|
class PlannerService:
|
|
def generate(self, *, week_start: str, anchors: list[str], target_meals: int) -> dict[str, Any]:
|
|
suggestions = [
|
|
item for item in repo.list_suggestions()
|
|
if item.get("status") in {"approved", "suggested", "saved"}
|
|
]
|
|
chosen: list[dict[str, Any]] = []
|
|
used_ids: set[str] = set()
|
|
for item in sorted(suggestions, key=lambda entry: entry.get("score", 0), reverse=True):
|
|
haystack = " ".join([str(item.get("title") or ""), " ".join(item.get("ingredients") or []), " ".join(item.get("tags") or [])]).lower()
|
|
if anchors and not any(anchor.strip().lower() in haystack for anchor in anchors if anchor.strip()):
|
|
continue
|
|
chosen.append(item)
|
|
used_ids.add(str(item.get("id")))
|
|
if len(chosen) >= min(len(anchors), target_meals):
|
|
break
|
|
ingredient_counter: Counter[str] = Counter()
|
|
for item in chosen:
|
|
ingredient_counter.update(item.get("ingredients") or [])
|
|
ranked = sorted(
|
|
(item for item in suggestions if str(item.get("id")) not in used_ids),
|
|
key=lambda entry: (
|
|
len(_overlap(list(ingredient_counter), entry.get("ingredients") or [])),
|
|
entry.get("score", 0),
|
|
),
|
|
reverse=True,
|
|
)
|
|
for item in ranked:
|
|
if len(chosen) >= target_meals:
|
|
break
|
|
chosen.append(item)
|
|
used_ids.add(str(item.get("id")))
|
|
ingredient_counter.update(item.get("ingredients") or [])
|
|
meals: list[dict[str, Any]] = []
|
|
shared = [name for name, count in ingredient_counter.items() if count >= 2]
|
|
for item in chosen[:target_meals]:
|
|
overlap = _overlap(shared, item.get("ingredients") or [])
|
|
note = "Reuses: " + ", ".join(overlap[:4]) if overlap else "Adds variety without blowing up the ingredient list."
|
|
meals.append(
|
|
{
|
|
"suggestion_id": item.get("id"),
|
|
"title": item.get("title"),
|
|
"url": item.get("url"),
|
|
"score": item.get("score"),
|
|
"ingredients": item.get("ingredients") or [],
|
|
"note": note,
|
|
}
|
|
)
|
|
return {
|
|
"week_start": week_start,
|
|
"anchors": anchors,
|
|
"target_meals": target_meals,
|
|
"shared_ingredients": shared[:12],
|
|
"meals": meals,
|
|
"summary": f"Built {len(meals)} meals with {len(shared[:12])} shared ingredient lanes.",
|
|
}
|
|
|
|
|
|
planner = PlannerService()
|
|
|