""" Recipe Processing API for Hugging Face This is the main application file for the recipe processing and product matching system. It provides a FastAPI application that can be deployed on Hugging Face Spaces. """ import os import json import logging from fastapi import FastAPI, HTTPException, File, UploadFile, Form, Body from fastapi.responses import JSONResponse from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel, Field from typing import Optional, Dict, Any, List from recipe_processor import RecipeProcessor from product_retriever_improved import ProductRetriever import uvicorn # Set up logging logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s' ) # Create FastAPI app app = FastAPI( title="Recipe Processing API", description="API for processing recipes and matching ingredients with products", version="1.0.0", ) # Add CORS middleware app.add_middleware( CORSMiddleware, allow_origins=["*"], # Allow all origins allow_credentials=True, allow_methods=["*"], # Allow all methods allow_headers=["*"], # Allow all headers ) # Define API models class ProcessRecipeRequest(BaseModel): recipe_data: Dict[str, Any] = Field(..., description="Recipe data to process") woolworths_api_key: Optional[str] = Field(None, description="Woolworths API key") coles_api_key: Optional[str] = Field(None, description="Coles API key") summarize: bool = Field(True, description="Whether to summarize recipe text") add_products: bool = Field(True, description="Whether to add product matches") class SupabaseSettings(BaseModel): supabase_url: str = Field(..., description="Supabase URL") supabase_key: str = Field(..., description="Supabase API key") class ProcessMealPlanRequest(BaseModel): supabase_url: str = Field(..., description="Supabase URL") supabase_key: str = Field(..., description="Supabase API key") session_id: Optional[str] = Field(None, description="Session ID (optional, if None gets latest)") woolworths_api_key: Optional[str] = Field(None, description="Woolworths API key") coles_api_key: Optional[str] = Field(None, description="Coles API key") summarize: bool = Field(True, description="Whether to summarize recipe text") add_products: bool = Field(True, description="Whether to add product matches") # API endpoints @app.get("/") async def root(): """API root endpoint""" return { "message": "Recipe Processing API is running", "endpoints": [ "/api/process_recipe", "/api/process_meal_plan", "/api/get_meal_plans" ] } @app.post("/api/process_recipe") async def process_recipe(request: ProcessRecipeRequest): """ Process a recipe and optionally add product matches """ try: # Initialize the recipe processor recipe_processor = RecipeProcessor() # Process the recipe processed_recipe = recipe_processor.process_recipe( request.recipe_data, summarize=request.summarize ) # Add product matches if requested if request.add_products and (request.woolworths_api_key or request.coles_api_key): # Initialize product retriever product_retriever = ProductRetriever( woolworths_api_key=request.woolworths_api_key, coles_api_key=request.coles_api_key ) # Add product matches processed_recipe = product_retriever.add_product_matches_to_recipe(processed_recipe) return processed_recipe except Exception as e: logging.error(f"Error processing recipe: {e}") raise HTTPException(status_code=500, detail=str(e)) @app.post("/api/process_meal_plan") async def process_meal_plan(request: ProcessMealPlanRequest): """ Process a meal plan from Supabase and optionally add product matches """ try: # Import Supabase client try: from supabase import create_client except ImportError: raise HTTPException(status_code=500, detail="Supabase library not installed") # Initialize Supabase client supabase = create_client(request.supabase_url, request.supabase_key) # Get meal plan if request.session_id: response = supabase.table("meal_plans").select("*").eq("id", request.session_id).execute() else: # Get the latest meal plan response = supabase.table("meal_plans").select("*").order("created_at", desc=True).limit(1).execute() if not response.data: error_msg = "No meal plan found" + (f" for session {request.session_id}" if request.session_id else "") raise HTTPException(status_code=404, detail=error_msg) # Get the meal plan data meal_plan = response.data[0] session_id = meal_plan.get("id", "") # Initialize recipe processor recipe_processor = RecipeProcessor() # Create result structure result = { "session_id": session_id, "processed": {} } # Process each meal type for meal_type in ["breakfast", "lunch", "dinner"]: if meal_type not in meal_plan: continue meal_data = meal_plan[meal_type] # Parse JSON if it's a string if isinstance(meal_data, str): try: meal_data = json.loads(meal_data) except json.JSONDecodeError: logging.error(f"Invalid JSON in {meal_type}") continue # Process this meal processed_meal = recipe_processor.process_recipe( meal_data, summarize=request.summarize ) # Add to result result["processed"][meal_type] = processed_meal # Add product matches if requested if request.add_products and (request.woolworths_api_key or request.coles_api_key): # Initialize product retriever product_retriever = ProductRetriever( woolworths_api_key=request.woolworths_api_key, coles_api_key=request.coles_api_key ) # Add product matches result = product_retriever.add_product_matches_to_meal_plan(result) # Generate combined result combined_result = {} meal_plans = {} for meal_type, meal_data in result["processed"].items(): # Extract the data we need meal_result = { 'recipe': meal_data.get('recipe', ''), 'summarized_recipe': '' } # Get summarized recipe (combine description and instructions) processed = meal_data.get('processed', {}) description = processed.get('description', {}).get('summarized', '') instructions = processed.get('instructions', {}).get('summarized', '') if description or instructions: meal_result['summarized_recipe'] = f"{description}\n\n{instructions}".strip() # Get ingredients meal_result['ingredients'] = processed.get('ingredients', []) # Add to meal plans meal_plans[meal_type] = meal_result # Add to result combined_result['meal_plans'] = meal_plans combined_result['session_id'] = session_id return combined_result except Exception as e: logging.error(f"Error processing meal plan: {e}") raise HTTPException(status_code=500, detail=str(e)) @app.post("/api/get_meal_plans") async def get_meal_plans(settings: SupabaseSettings, limit: int = 10): """Get list of available meal plans from Supabase""" try: # Import Supabase client try: from supabase import create_client except ImportError: raise HTTPException(status_code=500, detail="Supabase library not installed") # Initialize Supabase client supabase = create_client(settings.supabase_url, settings.supabase_key) # Get meal plans, most recent first response = supabase.table("meal_plans").select("id,created_at").order("created_at", desc=True).limit( limit).execute() if not response.data: return {"meal_plans": []} return {"meal_plans": response.data} except Exception as e: logging.error(f"Error getting meal plans: {e}") raise HTTPException(status_code=500, detail=str(e)) # Main function to run the server directly (for development) if __name__ == "__main__": port = int(os.environ.get("PORT", 7860)) # Hugging Face Spaces uses port 7860 uvicorn.run("app:app", host="0.0.0.0", port=port)