summaryrefslogtreecommitdiff
path: root/routers
diff options
context:
space:
mode:
Diffstat (limited to 'routers')
-rw-r--r--routers/grocery.py135
-rw-r--r--routers/menus.py191
2 files changed, 242 insertions, 84 deletions
diff --git a/routers/grocery.py b/routers/grocery.py
index cde2068..17215d2 100644
--- a/routers/grocery.py
+++ b/routers/grocery.py
@@ -12,6 +12,11 @@ router = APIRouter(prefix="/api/grocery", tags=["grocery"])
ai_router = APIRouter(prefix="/api/ai", tags=["ai"])
+def _current_monday():
+ today = datetime.utcnow().date()
+ return today - timedelta(days=today.weekday())
+
+
@router.get("")
async def list_grocery_lists(db: Session = Depends(get_db)):
"""List all grocery lists."""
@@ -22,8 +27,7 @@ async def list_grocery_lists(db: Session = Depends(get_db)):
@router.get("/current")
async def get_current_grocery_list(db: Session = Depends(get_db)):
"""Get grocery list for current week."""
- today = datetime.utcnow().date()
- monday = today - timedelta(days=today.weekday())
+ monday = _current_monday()
grocery_list = db.query(GroceryList).filter(GroceryList.generated_for == monday).first()
if not grocery_list:
@@ -41,57 +45,49 @@ async def get_grocery_list(id: int, db: Session = Depends(get_db)):
@router.post("/generate")
-async def generate_grocery_list(db: Session = Depends(get_db)):
+async def generate_grocery_list(request: dict = Body(default={}), db: Session = Depends(get_db)):
"""Generate a grocery list for the current week."""
+ user_notes = request.get("user_notes") if request else None
+
# Get current week's Monday
- today = datetime.utcnow().date()
- monday = today - timedelta(days=today.weekday())
+ monday = _current_monday()
# Fetch current MenuPlan
menu_plan = db.query(MenuPlan).filter(MenuPlan.week_start == monday).first()
- if not menu_plan:
- raise HTTPException(status_code=404, detail="No menu plan for current week. Generate a menu first.")
- # Parse the plan JSON
- try:
- plan_dict = json.loads(menu_plan.plan)
- except json.JSONDecodeError:
- raise HTTPException(status_code=500, detail="Invalid menu plan data")
+ pantry_context = pantry_service.build_pantry_context(db)
- # Collect all recipe IDs and fetch recipes
- recipe_ids = set()
- for day_meals in plan_dict.values():
- if isinstance(day_meals, dict):
- for recipe_id in day_meals.values():
- if isinstance(recipe_id, int):
- recipe_ids.add(recipe_id)
+ try:
+ if menu_plan:
+ # Parse the plan JSON (now a flat array of recipe IDs)
+ try:
+ recipe_ids = json.loads(menu_plan.plan)
+ except json.JSONDecodeError:
+ raise HTTPException(status_code=500, detail="Invalid menu plan data")
- recipes = db.query(Recipe).filter(Recipe.id.in_(recipe_ids)).all() if recipe_ids else []
- recipe_map = {r.id: r for r in recipes}
+ recipes = db.query(Recipe).filter(Recipe.id.in_(recipe_ids)).all() if recipe_ids else []
+ recipe_map = {r.id: r for r in recipes}
- # Build menu_plan_for_ai
- menu_plan_for_ai = {}
- for day, day_meals in plan_dict.items():
- menu_plan_for_ai[day] = {}
- for meal_type, recipe_id in day_meals.items():
- recipe = recipe_map.get(recipe_id)
- if recipe:
- try:
- ingredients = json.loads(recipe.ingredients)
- except (json.JSONDecodeError, TypeError):
- ingredients = []
- menu_plan_for_ai[day][meal_type] = {
- "name": recipe.name,
- "ingredients": ingredients,
- }
+ recipes_for_ai = []
+ for recipe_id in recipe_ids:
+ recipe = recipe_map.get(recipe_id)
+ if recipe:
+ try:
+ ingredients = json.loads(recipe.ingredients)
+ except (json.JSONDecodeError, TypeError):
+ ingredients = []
+ recipes_for_ai.append({
+ "name": recipe.name,
+ "meal_type": recipe.meal_type,
+ "ingredients": ingredients,
+ })
- # Build pantry context and generate grocery list
- try:
- pantry_context = pantry_service.build_pantry_context(db)
- ai_result = await ai_service.generate_grocery_list(menu_plan_for_ai, pantry_context)
+ ai_result = await ai_service.generate_grocery_list(recipes_for_ai, pantry_context, user_notes)
+ else:
+ ai_result = await ai_service.generate_grocery_list_from_pantry(pantry_context, user_notes)
except ValueError as e:
raise HTTPException(status_code=503, detail=str(e))
- except (ConnectionError, Exception) as e:
+ except Exception as e:
raise HTTPException(status_code=503, detail=f"Ollama service error: {str(e)}")
# Upsert GroceryList
@@ -122,6 +118,18 @@ async def generate_grocery_list(db: Session = Depends(get_db)):
}
+@router.delete("/current")
+async def delete_current_grocery_list(db: Session = Depends(get_db)):
+ """Delete the grocery list for the current week."""
+ monday = _current_monday()
+ grocery_list = db.query(GroceryList).filter(GroceryList.generated_for == monday).first()
+ if not grocery_list:
+ raise HTTPException(status_code=404, detail="No grocery list for current week")
+ db.delete(grocery_list)
+ db.commit()
+ return {"status": "deleted"}
+
+
@router.put("/{id}/purchased")
async def mark_purchased(id: int, db: Session = Depends(get_db)):
"""Mark a grocery list as purchased and update pantry."""
@@ -175,7 +183,7 @@ async def suggest_recipe_endpoint(db: Session = Depends(get_db)):
return result
except ValueError as e:
raise HTTPException(status_code=503, detail=str(e))
- except (ConnectionError, Exception) as e:
+ except Exception as e:
raise HTTPException(status_code=503, detail=f"Ollama service error: {str(e)}")
@@ -184,3 +192,46 @@ async def list_models():
"""List available Ollama models."""
models = await ai_service.get_available_models()
return {"models": models}
+
+
+@ai_router.post("/chat")
+async def chat_endpoint(request: dict = Body(default={}), db: Session = Depends(get_db)):
+ """Chat with Commis using current kitchen context."""
+ message = request.get("message", "").strip()
+ if not message:
+ raise HTTPException(status_code=400, detail="Message is required")
+
+ history = request.get("history", [])
+
+ # Build pantry context
+ pantry_context = pantry_service.build_pantry_context(db)
+
+ # Get current week's menu recipes
+ monday = _current_monday()
+
+ menu_plan = db.query(MenuPlan).filter(MenuPlan.week_start == monday).first()
+ menu_context = []
+ if menu_plan:
+ try:
+ plan_ids = json.loads(menu_plan.plan)
+ recipes = db.query(Recipe).filter(Recipe.id.in_(plan_ids)).all() if plan_ids else []
+ menu_context = [{"name": r.name, "meal_type": r.meal_type} for r in recipes]
+ except Exception:
+ menu_context = []
+
+ # Get current grocery list items
+ grocery_list = db.query(GroceryList).filter(GroceryList.generated_for == monday).first()
+ grocery_context = []
+ if grocery_list:
+ try:
+ grocery_context = json.loads(grocery_list.items)
+ except Exception:
+ grocery_context = []
+
+ try:
+ response = await ai_service.chat_with_commis(message, history, pantry_context, menu_context, grocery_context)
+ return {"response": response}
+ except ValueError as e:
+ raise HTTPException(status_code=503, detail=str(e))
+ except Exception as e:
+ raise HTTPException(status_code=503, detail=f"Ollama service error: {str(e)}")
diff --git a/routers/menus.py b/routers/menus.py
index 49c4654..9ec1d1d 100644
--- a/routers/menus.py
+++ b/routers/menus.py
@@ -1,4 +1,4 @@
-from fastapi import APIRouter, Depends, HTTPException
+from fastapi import APIRouter, Depends, HTTPException, Body
from sqlalchemy.orm import Session
from datetime import datetime, timedelta, date
import json
@@ -12,11 +12,15 @@ from config import settings
router = APIRouter(prefix="/api/menus", tags=["menus"])
+def _current_monday():
+ today = datetime.utcnow().date()
+ return today - timedelta(days=today.weekday())
+
+
@router.get("/current")
async def get_current_menu(db: Session = Depends(get_db)):
"""Get menu plan for current week."""
- today = datetime.utcnow().date()
- monday = today - timedelta(days=today.weekday())
+ monday = _current_monday()
menu = db.query(MenuPlan).filter(MenuPlan.week_start == monday).first()
if not menu:
@@ -37,67 +41,64 @@ async def generate_menu(request: dict = None, db: Session = Depends(get_db)):
try:
if isinstance(week_start, str):
week_start = datetime.fromisoformat(week_start).date()
- else:
- week_start = week_start
except Exception:
raise HTTPException(status_code=400, detail="Invalid week_start date")
else:
- today = datetime.utcnow().date()
- week_start = today - timedelta(days=today.weekday())
+ week_start = _current_monday()
# Build pantry context and generate menu
try:
pantry_context = pantry_service.build_pantry_context(db)
- ai_result = await ai_service.generate_weekly_menu(pantry_context)
+ user_notes = request.get("user_notes") if request and isinstance(request, dict) else None
+ ai_result = await ai_service.generate_weekly_menu(pantry_context, user_notes)
except ValueError as e:
raise HTTPException(status_code=503, detail=str(e))
- except (ConnectionError, Exception) as e:
+ except Exception as e:
raise HTTPException(status_code=503, detail=f"Ollama service error: {str(e)}")
- # Save recipes and build plan dict
- week_plan = ai_result.get("week_plan", {})
- plan_dict = {}
-
- for day, meals in week_plan.items():
- plan_dict[day] = {}
- for meal_type, meal_data in meals.items():
- if isinstance(meal_data, dict) and "name" in meal_data:
- meal_name = meal_data["name"]
+ # Save recipes and build plan list (flat array of IDs)
+ recipes_ai = ai_result.get("recipes", [])
+ plan_ids = []
- # Check if recipe exists by name
- existing_recipe = db.query(Recipe).filter(Recipe.name == meal_name).first()
- if existing_recipe:
- recipe_id = existing_recipe.id
- else:
- # Create new recipe
- new_recipe = Recipe(
- name=meal_name,
- meal_type=meal_type,
- ingredients=json.dumps(meal_data.get("ingredients", [])),
- instructions=meal_data.get("instructions", ""),
- estimated_time_minutes=meal_data.get("time_minutes", 30),
- servings=meal_data.get("serves", 2),
- source="ai",
- )
- db.add(new_recipe)
- db.flush()
- recipe_id = new_recipe.id
+ valid_recipes = [r for r in recipes_ai if isinstance(r, dict) and "name" in r]
+ all_names = [r["name"] for r in valid_recipes]
+ existing = db.query(Recipe).filter(Recipe.name.in_(all_names)).all()
+ existing_by_name = {r.name: r for r in existing}
- plan_dict[day][meal_type] = recipe_id
+ for recipe_data in valid_recipes:
+ meal_name = recipe_data["name"]
+ if meal_name in existing_by_name:
+ recipe_id = existing_by_name[meal_name].id
+ else:
+ new_recipe = Recipe(
+ name=meal_name,
+ meal_type=recipe_data.get("meal_type", "dinner"),
+ ingredients=json.dumps(recipe_data.get("ingredients", [])),
+ instructions=recipe_data.get("instructions", ""),
+ estimated_time_minutes=recipe_data.get("time_minutes", 30),
+ servings=recipe_data.get("serves", 2),
+ source="ai",
+ )
+ db.add(new_recipe)
+ db.flush()
+ recipe_id = new_recipe.id
+ plan_ids.append(recipe_id)
- # Upsert MenuPlan
+ ai_notes = ai_result.get("notes", "")
existing_plan = db.query(MenuPlan).filter(MenuPlan.week_start == week_start).first()
if existing_plan:
- existing_plan.plan = json.dumps(plan_dict)
+ existing_plan.plan = json.dumps(plan_ids)
existing_plan.generated_at = datetime.utcnow()
existing_plan.model_used = settings.model_name
+ existing_plan.notes = ai_notes
menu = existing_plan
else:
menu = MenuPlan(
week_start=week_start,
- plan=json.dumps(plan_dict),
+ plan=json.dumps(plan_ids),
generated_at=datetime.utcnow(),
model_used=settings.model_name,
+ notes=ai_notes,
)
db.add(menu)
@@ -106,8 +107,8 @@ async def generate_menu(request: dict = None, db: Session = Depends(get_db)):
return {
"menu_plan": MenuPlanRead.from_orm(menu),
- "week_plan": week_plan,
- "notes": ai_result.get("notes", ""),
+ "recipes": recipes_ai,
+ "notes": menu.notes or "",
}
@@ -127,6 +128,112 @@ async def get_menu(week_start: date, db: Session = Depends(get_db)):
return MenuPlanRead.from_orm(menu)
+@router.delete("/current")
+async def delete_current_menu(db: Session = Depends(get_db)):
+ """Delete the menu plan for the current week."""
+ monday = _current_monday()
+ menu = db.query(MenuPlan).filter(MenuPlan.week_start == monday).first()
+ if not menu:
+ raise HTTPException(status_code=404, detail="No menu plan for current week")
+ db.delete(menu)
+ db.commit()
+ return {"status": "deleted"}
+
+
+@router.delete("/current/recipes/{recipe_id}")
+async def remove_recipe_from_menu(recipe_id: int, db: Session = Depends(get_db)):
+ """Remove a single recipe from the current week's plan."""
+ monday = _current_monday()
+ menu = db.query(MenuPlan).filter(MenuPlan.week_start == monday).first()
+ if not menu:
+ raise HTTPException(status_code=404, detail="No menu plan for current week")
+
+ try:
+ plan_ids = json.loads(menu.plan)
+ except json.JSONDecodeError:
+ raise HTTPException(status_code=500, detail="Invalid menu plan data")
+
+ if recipe_id not in plan_ids:
+ raise HTTPException(status_code=404, detail="Recipe not in current menu plan")
+
+ plan_ids.remove(recipe_id)
+ menu.plan = json.dumps(plan_ids)
+ db.commit()
+ return {"status": "removed", "plan": plan_ids}
+
+
+@router.post("/current/recipes/{recipe_id}/swap")
+async def swap_recipe_in_menu(recipe_id: int, request: dict = Body(default={}), db: Session = Depends(get_db)):
+ """Swap a single recipe in the current week's plan with a new AI suggestion."""
+ monday = _current_monday()
+ menu = db.query(MenuPlan).filter(MenuPlan.week_start == monday).first()
+ if not menu:
+ raise HTTPException(status_code=404, detail="No menu plan for current week")
+
+ try:
+ plan_ids = json.loads(menu.plan)
+ except json.JSONDecodeError:
+ raise HTTPException(status_code=500, detail="Invalid menu plan data")
+
+ # Find the recipe being swapped
+ old_recipe = db.query(Recipe).filter(Recipe.id == recipe_id).first()
+ if not old_recipe:
+ raise HTTPException(status_code=404, detail="Recipe not found")
+
+ meal_type = old_recipe.meal_type
+
+ # Get names of all other recipes in the plan to avoid repeats
+ other_ids = [rid for rid in plan_ids if rid != recipe_id]
+ other_recipes = db.query(Recipe).filter(Recipe.id.in_(other_ids)).all() if other_ids else []
+ existing_names = [r.name for r in other_recipes]
+
+ # Generate replacement
+ user_notes = request.get("user_notes") if request else None
+ try:
+ pantry_context = pantry_service.build_pantry_context(db)
+ ai_result = await ai_service.generate_replacement_recipe(meal_type, existing_names, pantry_context, user_notes)
+ except ValueError as e:
+ raise HTTPException(status_code=503, detail=str(e))
+ except Exception as e:
+ raise HTTPException(status_code=503, detail=f"Ollama service error: {str(e)}")
+
+ recipe_data = ai_result.get("recipe", {})
+ if not recipe_data or "name" not in recipe_data:
+ raise HTTPException(status_code=503, detail="AI returned invalid recipe data")
+
+ # Save new recipe
+ new_recipe = Recipe(
+ name=recipe_data["name"],
+ meal_type=recipe_data.get("meal_type", meal_type),
+ ingredients=json.dumps(recipe_data.get("ingredients", [])),
+ instructions=recipe_data.get("instructions", ""),
+ estimated_time_minutes=recipe_data.get("time_minutes", 30),
+ servings=recipe_data.get("serves", 2),
+ source="ai",
+ )
+ db.add(new_recipe)
+ db.flush()
+
+ # Swap the ID in the plan
+ idx = plan_ids.index(recipe_id)
+ plan_ids[idx] = new_recipe.id
+ menu.plan = json.dumps(plan_ids)
+ db.commit()
+ db.refresh(new_recipe)
+
+ return {
+ "recipe": {
+ "id": new_recipe.id,
+ "name": new_recipe.name,
+ "meal_type": new_recipe.meal_type,
+ "ingredients": recipe_data.get("ingredients", []),
+ "instructions": new_recipe.instructions,
+ "estimated_time_minutes": new_recipe.estimated_time_minutes,
+ "servings": new_recipe.servings,
+ }
+ }
+
+
@router.delete("/{id}")
async def delete_menu(id: int, db: Session = Depends(get_db)):
"""Delete a menu plan."""