Files
truenas-stacks/home/doris-kitchen/app/services/planner.py
2026-05-16 20:22:45 +00:00

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()