Devansh959/Zomato-AI-Recommender
0
1import os2import sqlite33import pandas as pd4import json5from fastapi import FastAPI, HTTPException6from pydantic import BaseModel7from typing import Optional, List8from contextlib import asynccontextmanager9from dotenv import load_dotenv10from groq import Groq11 12load_dotenv()13 14GROQ_API_KEY = os.getenv("GROQ_API_KEY")15if GROQ_API_KEY:16 client = Groq(api_key=GROQ_API_KEY)17else:18 client = None19 print("WARNING: GROQ_API_KEY not found in .env file.")20 21df_restaurants = pd.DataFrame()22 23def get_clean_dataframe():24 db_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'data', 'zomato.sqlite')25 with sqlite3.connect(db_path) as conn:26 df = pd.read_sql('SELECT * FROM restaurants', conn)27 28 df['clean_rate'] = df['rate'].astype(str).str.extract(r'(\d+\.\d+)').astype(float)29 df['clean_cost'] = df['approx_cost(for_two_people)'].astype(str).str.replace(',', '').str.extract(r'(\d+)').astype(float)30 df['clean_votes'] = pd.to_numeric(df['votes'], errors='coerce').fillna(0)31 32 return df33 34@asynccontextmanager35async def lifespan(app: FastAPI):36 global df_restaurants37 try:38 print("Loading and cleaning restaurant dataset into memory...")39 df_restaurants = get_clean_dataframe()40 print(f"Loaded {len(df_restaurants)} restaurants.")41 except Exception as e:42 print(f"Failed to load dataset: {e}")43 yield44 df_restaurants = pd.DataFrame()45 46app = FastAPI(title="Zomato Recommender API", lifespan=lifespan)47 48class UserPreferences(BaseModel):49 location: Optional[str] = None50 max_budget: Optional[float] = None51 cuisine: Optional[str] = None52 min_rating: Optional[float] = None53 additional_preferences: Optional[str] = None54 55@app.get("/api/locations")56def get_locations():57 if df_restaurants.empty:58 return {"locations": []}59 locs = df_restaurants['location'].dropna().unique().tolist()60 locs = sorted([str(loc).strip() for loc in locs if str(loc).strip()])61 return {"locations": ["Any"] + locs}62 63@app.post("/api/recommend")64def recommend_restaurants(prefs: UserPreferences):65 if df_restaurants.empty:66 raise HTTPException(status_code=500, detail="Dataset not loaded.")67 if not client:68 raise HTTPException(status_code=500, detail="Groq API key is missing. Check your .env file.")69 70 df = df_restaurants.copy()71 72 if prefs.location:73 loc_term = prefs.location.lower()74 df = df[df['location'].str.lower().str.contains(loc_term, na=False) | 75 df['listed_in(city)'].str.lower().str.contains(loc_term, na=False)]76 if prefs.cuisine:77 cuisine_term = prefs.cuisine.lower()78 df = df[df['cuisines'].str.lower().str.contains(cuisine_term, na=False)]79 if prefs.min_rating:80 df = df[df['clean_rate'] >= prefs.min_rating]81 if prefs.max_budget is not None:82 df = df[df['clean_cost'] <= prefs.max_budget]83 84 df = df.sort_values(by=['clean_rate', 'clean_votes'], ascending=[False, False])85 df = df.drop_duplicates(subset=['name'])86 87 # Get up to 15 matches to show to the user88 top_matches = df.head(15) 89 90 if len(top_matches) == 0:91 return {"recommendations": [], "message": "No restaurants found matching your exact criteria."}92 93 # Prepare MINIMAL JSON structure for Groq to save massive amounts of tokens94 context_data = []95 for _, row in top_matches.iterrows():96 context_data.append({97 "name": str(row['name']),98 "cuisines": str(row['cuisines']),99 "rating": row['clean_rate'] if pd.notnull(row['clean_rate']) else "Unknown"100 })101 102 system_prompt = """103 You are an expert Zomato restaurant recommendation AI. 104 You will receive a list of filtered restaurants and the user's exact preferences.105 Your task is to provide a personalized explanation for ALL the restaurants provided in the list.106 You MUST return ONLY a raw JSON object. Do NOT include markdown blocks like ```json.107 Format your response EXACTLY as this JSON object:108 {109 "recommendations": [110 {111 "restaurant_name": "Name",112 "ai_explanation": "A 1-2 sentence personalized explanation of why this fits the user's specific preferences."113 }114 ]115 }116 """117 118 user_prompt = f"""119 User Preferences:120 - Location: {prefs.location or 'Any'}121 - Max Budget for Two: {prefs.max_budget or 'Any'}122 - Cuisine: {prefs.cuisine or 'Any'}123 - Min Rating: {prefs.min_rating or 'Any'}124 - Additional Preferences: {prefs.additional_preferences or 'None specifically'}125 126 Here is the structured data of restaurants:127 {json.dumps(context_data, indent=2)}128 129 Please return the JSON object with explanations for all restaurants.130 """131 132 try:133 response = client.chat.completions.create(134 messages=[135 {"role": "system", "content": system_prompt},136 {"role": "user", "content": user_prompt}137 ],138 model="llama-3.1-8b-instant",139 temperature=0.2, 140 max_tokens=2000,141 response_format={"type": "json_object"}142 )143 144 ai_response_text = response.choices[0].message.content.strip()145 146 if ai_response_text.startswith("```json"):147 ai_response_text = ai_response_text[7:]148 if ai_response_text.endswith("```"):149 ai_response_text = ai_response_text[:-3]150 151 ai_json = json.loads(ai_response_text)152 ai_recs = ai_json.get("recommendations", [])153 154 # Map AI explanations back to the original full dataframe!155 exp_map = {r.get("restaurant_name"): r.get("ai_explanation") for r in ai_recs}156 157 final_results = []158 for _, row in top_matches.iterrows():159 name = str(row['name'])160 final_results.append({161 "restaurant_name": name,162 "cuisine": str(row['cuisines']),163 "rating": row['clean_rate'] if pd.notnull(row['clean_rate']) else None,164 "estimated_cost": str(row['approx_cost(for_two_people)']),165 "url": str(row['url']),166 "ai_explanation": exp_map.get(name, "A great match based on your standard filters.")167 })168 169 return {170 "total_matches_filtered": len(df),171 "recommendations": final_results172 }173 except Exception as e:174 raise HTTPException(status_code=500, detail=f"Error communicating with AI engine: {str(e)}")175 