Add Doris Kitchen household recipe app
This commit is contained in:
72
home/doris-kitchen/app/services/planner.py
Normal file
72
home/doris-kitchen/app/services/planner.py
Normal file
@@ -0,0 +1,72 @@
|
||||
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()
|
||||
|
||||
Reference in New Issue
Block a user