Public Access
F9-lite reuses the pre-existing OLLAMA_* config (config.py:36-38: OLLAMA_BASE_URL=https://ollama.com/v1, OLLAMA_API_KEY, OLLAMA_MODEL=kimi-k2.6:cloud). Avoids the local model pull (F9-full would be 4 GB on disk + a separate uvicorn process). Cloud LLM — operator’s existing OLLAMA billing applies per call. Sprint 13 splits the Sprint 11 "Generate Meal Plan" CTA into a 2-step modal: "Use the recipe library" (default, Sprint 11’s existing flow) or "Ask the LLM" (new). The LLM path POSTs to /api/llm/plan with a free-text prompt; the backend calls kimi-k2.6:cloud on ollama.com, parses the LLM’s JSON picks, creates a fresh plan, fills the LLM’s picks, and falls through to the Sprint 6+ fillEmptySlots pattern for the slots the LLM didn’t cover. Backend: - backend/app/api/llm_plan.py (NEW, ~280 lines). 1 endpoint (POST /api/llm/plan body {prompt, week_start}) + 4 helpers: - _ensure_ollama_configured — 503 on missing OLLAMA_API_KEY. - _serialize_library — reads up to 200 recipes for the family, sorted alphabetically. Cap prevents prompt-token overflow on kimi-k2. - _ask_llm — mirrors llm_matcher._ask_ollama (same URL, same headers, max_tokens=800, temperature=0, strips think blocks, 60s timeout). - _parse_picks — tolerant JSON parser. Handles markdown code fences, trailing commentary, and bare JSON. On failure returns []; the library fill takes over. - _validate_picks — drops invalid entries: missing fields, out-of-range day_of_week, unknown meal_type, unknown recipe_id. Returns a list of LLMPickedItem. Flow: rejects duplicate week (400) and empty library (400), builds the prompt, calls the LLM, validates picks, creates the plan, inserts the LLM-picked items, fills the rest from the library (Sprint 6+ pattern, re-implemented inline to avoid a self-HTTP-call), returns {plan_id, picked_count, filled_count, failed_count, reasoning}. - backend/app/schemas/__init__.py — added LLMPlanRequest + LLMPlanResponse. - backend/app/main.py:65-66 — registered llm_plan_api.router at the /api/llm prefix. No collision with the pre-existing WIP recipes.py. Frontend: - frontend/src/api/index.ts — added llm.plan(data) method. - frontend/src/pages/Dashboard.tsx — added the prompt modal (radio for library vs. LLM + textarea for the LLM path with 500-char counter) + new state (showPromptModal, promptMode, promptText, promptBusy) + extracted Sprint 11’s body into generateFromLibrary + added generateFromLLM. The modal is inline (not a separate component) because it depends on 4 local states + 3 handlers. Click-outside-to-dismiss is disabled while promptBusy is true. The textarea autoFocuses when LLM mode is selected. Added the Button import. LLM tolerance: a 60s timeout, parse-failure (markdown code fences, trailing commentary), or empty response all return 0 picks; the library fill takes over. The user never sees a crash — at worst, picked_count: 0 and the toast reads "Planned N meals (LLM picked 0, library filled the rest)". Verified: npm run build green (tsc 0 errors, vite 0 errors). Bundle: 500.28 → 503.82 kB (+3.5 kB). Backend AST clean on all 3 changed files. No new dependencies, no migration, no pre-existing WIP files touched. Deploy: git pull + docker compose up -d --build backend frontend (no migration, no new dependencies).
67 lines
2.7 KiB
Python
67 lines
2.7 KiB
Python
from fastapi import FastAPI, Depends
|
|
from fastapi.staticfiles import StaticFiles
|
|
from sqlalchemy.orm import Session
|
|
from sqlalchemy import text
|
|
from app.database import get_db
|
|
from app.config import settings
|
|
import logging
|
|
|
|
logging.basicConfig(level=settings.LOG_LEVEL)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
app = FastAPI(
|
|
title="MealPlanner",
|
|
description="Self-hosted meal planning system",
|
|
version="0.1.0",
|
|
)
|
|
|
|
# Serve generated recipe images from local filesystem.
|
|
app.mount("/static", StaticFiles(directory="static"), name="static")
|
|
|
|
|
|
@app.get("/health")
|
|
def health_check(db: Session = Depends(get_db)):
|
|
return {"status": "ok"}
|
|
|
|
|
|
@app.get("/health/db")
|
|
def health_check_db(db: Session = Depends(get_db)):
|
|
try:
|
|
db.execute(text("SELECT 1"))
|
|
return {"status": "ok", "database": "connected"}
|
|
except Exception as e:
|
|
return {"status": "error", "database": "disconnected", "error": str(e)}
|
|
|
|
|
|
from app.api import profile, meals, shopping_list, pantry, admin, auth
|
|
from app.api import ingredients as ingredients_api
|
|
from app.api import recipes as recipes_api
|
|
from app.api import never_suggest as never_suggest_api
|
|
from app.api import meal_plans as meal_plans_api
|
|
from app.api import feedback as feedback_api
|
|
from app.api import orchestrate as orchestrate_api
|
|
from app.api import recipe_search as recipe_search_api
|
|
from app.api import llm_plan as llm_plan_api
|
|
|
|
app.include_router(auth.router, prefix="/api/auth", tags=["auth"])
|
|
app.include_router(profile.router, prefix="/api/profile", tags=["profile"])
|
|
app.include_router(meals.router, prefix="/api/meals", tags=["meals"])
|
|
app.include_router(shopping_list.router, prefix="/api/shopping-list", tags=["shopping-list"])
|
|
app.include_router(pantry.router, prefix="/api/pantry", tags=["pantry"])
|
|
app.include_router(admin.router, prefix="/api/admin", tags=["admin"])
|
|
app.include_router(orchestrate_api.router, prefix="/api/orchestrate", tags=["orchestrate"])
|
|
app.include_router(ingredients_api.public_router)
|
|
app.include_router(ingredients_api.admin_router)
|
|
app.include_router(ingredients_api._match_admin_router)
|
|
app.include_router(recipes_api.public_router)
|
|
app.include_router(recipes_api.admin_router)
|
|
app.include_router(never_suggest_api.public_router)
|
|
app.include_router(never_suggest_api.admin_router)
|
|
app.include_router(meal_plans_api.admin_router)
|
|
app.include_router(meal_plans_api.public_router)
|
|
app.include_router(feedback_api.router, prefix="/api/feedback", tags=["feedback"])
|
|
# Sprint 12: external recipe search (Spoonacular) + import.
|
|
app.include_router(recipe_search_api.router, prefix="/api/recipes", tags=["recipes"])
|
|
# Sprint 13: F9-lite — free-text meal-plan synthesis via Ollama Cloud.
|
|
app.include_router(llm_plan_api.router, prefix="/api/llm", tags=["llm"])
|