kingking111009 commited on
Commit
19cdfc2
Β·
1 Parent(s): 934e108

Deploy FastAPI Recipe AI Assistant

Browse files

- Added FastAPI app with clean REST endpoints
- Integrated GPT-2 LoRA model from nutrientartcd/recipe-gpt2-lora
- Added CORS support for mobile app integration
- Added health checks and proper error handling
- Added automatic API documentation at /docs

Files changed (4) hide show
  1. Dockerfile +36 -0
  2. README.md +28 -5
  3. app.py +248 -0
  4. requirements.txt +10 -0
Dockerfile ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ # Set working directory
4
+ WORKDIR /app
5
+
6
+ # Install system dependencies
7
+ RUN apt-get update && apt-get install -y \
8
+ build-essential \
9
+ curl \
10
+ && rm -rf /var/lib/apt/lists/*
11
+
12
+ # Copy requirements and install Python dependencies
13
+ COPY requirements.txt .
14
+ RUN pip install --no-cache-dir -r requirements.txt
15
+
16
+ # Copy application code
17
+ COPY . .
18
+
19
+ # Create non-root user for security
20
+ RUN useradd -m -u 1000 user
21
+ RUN chown -R user:user /app
22
+ USER user
23
+
24
+ # Expose port
25
+ EXPOSE 7860
26
+
27
+ # Set environment variables
28
+ ENV PYTHONPATH=/app
29
+ ENV PYTHONUNBUFFERED=1
30
+
31
+ # Health check
32
+ HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \
33
+ CMD curl -f http://localhost:7860/health || exit 1
34
+
35
+ # Run the application
36
+ CMD ["python", "app.py"]
README.md CHANGED
@@ -1,10 +1,33 @@
1
  ---
2
- title: Recipe Ai Fastapi
3
- emoji: 🐠
4
- colorFrom: purple
5
- colorTo: gray
6
  sdk: docker
7
  pinned: false
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Recipe AI FastAPI
3
+ emoji: 🍳
4
+ colorFrom: green
5
+ colorTo: orange
6
  sdk: docker
7
  pinned: false
8
+ license: mit
9
  ---
10
 
11
+ # 🍳 Recipe AI Assistant FastAPI
12
+
13
+ A production-ready FastAPI service for AI-powered recipe recommendations using fine-tuned GPT-2.
14
+
15
+ ## Features
16
+
17
+ - **Clean REST API** designed for mobile apps
18
+ - **FastAPI with automatic docs** at `/docs`
19
+ - **CORS enabled** for web and mobile access
20
+ - **Health checks** and error handling
21
+ - **Multiple recommendations** with confidence scores
22
+
23
+ ## API Endpoints
24
+
25
+ ### `POST /api/recipe-suggestions`
26
+ Get personalized recipe recommendations for mobile apps.
27
+
28
+ ### `GET /health`
29
+ Health check endpoint for monitoring.
30
+
31
+ ## Model Integration
32
+
33
+ Uses fine-tuned GPT-2 LoRA model from `nutrientartcd/recipe-gpt2-lora`.
app.py ADDED
@@ -0,0 +1,248 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, HTTPException
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ from pydantic import BaseModel
4
+ from typing import List, Optional
5
+ import torch
6
+ from transformers import AutoTokenizer, AutoModelForCausalLM
7
+ from peft import PeftModel
8
+ import uvicorn
9
+ import os
10
+
11
+ # Initialize FastAPI app
12
+ app = FastAPI(
13
+ title="🍳 Recipe AI Assistant API",
14
+ description="AI-powered recipe recommendations using fine-tuned GPT-2",
15
+ version="1.0.0"
16
+ )
17
+
18
+ # Add CORS middleware for web and mobile access
19
+ app.add_middleware(
20
+ CORSMiddleware,
21
+ allow_origins=["*"], # In production, specify your domains
22
+ allow_credentials=True,
23
+ allow_methods=["*"],
24
+ allow_headers=["*"],
25
+ )
26
+
27
+ # Global variables for model
28
+ tokenizer = None
29
+ model = None
30
+ device = "cuda" if torch.cuda.is_available() else "cpu"
31
+
32
+ # Request/Response Models
33
+ class RecipeRequest(BaseModel):
34
+ ingredients: str
35
+ preferences: Optional[str] = ""
36
+ max_minutes: int = 30
37
+
38
+ class RecipeRecommendation(BaseModel):
39
+ suggestion: str
40
+ confidence: float
41
+
42
+ class RecipeResponse(BaseModel):
43
+ status: str
44
+ recommendations: List[RecipeRecommendation]
45
+ query: RecipeRequest
46
+ error: Optional[str] = None
47
+
48
+ # Load model on startup
49
+ @app.on_event("startup")
50
+ async def load_model():
51
+ global tokenizer, model
52
+
53
+ try:
54
+ print("πŸš€ Loading Recipe AI Model...")
55
+
56
+ # Load tokenizer
57
+ tokenizer = AutoTokenizer.from_pretrained("gpt2")
58
+ if tokenizer.pad_token is None:
59
+ tokenizer.pad_token = tokenizer.eos_token
60
+
61
+ # Load base model
62
+ print("πŸ“¦ Loading base GPT-2...")
63
+ base_model = AutoModelForCausalLM.from_pretrained("gpt2")
64
+
65
+ # Load your fine-tuned LoRA adapter
66
+ print("πŸ”§ Loading LoRA adapter...")
67
+ model = PeftModel.from_pretrained(
68
+ base_model,
69
+ "nutrientartcd/recipe-gpt2-lora"
70
+ ).to(device)
71
+ model.eval()
72
+
73
+ print(f"βœ… Model loaded successfully on {device}!")
74
+
75
+ except Exception as e:
76
+ print(f"❌ Error loading model: {e}")
77
+ print("πŸ”„ Falling back to base GPT-2...")
78
+
79
+ # Fallback to base model
80
+ tokenizer = AutoTokenizer.from_pretrained("gpt2")
81
+ if tokenizer.pad_token is None:
82
+ tokenizer.pad_token = tokenizer.eos_token
83
+ model = AutoModelForCausalLM.from_pretrained("gpt2").to(device)
84
+ model.eval()
85
+
86
+ # Health check endpoint
87
+ @app.get("/")
88
+ async def root():
89
+ return {
90
+ "message": "🍳 Recipe AI Assistant API",
91
+ "status": "healthy",
92
+ "model_loaded": model is not None,
93
+ "device": device
94
+ }
95
+
96
+ # Health check endpoint
97
+ @app.get("/health")
98
+ async def health_check():
99
+ return {
100
+ "status": "healthy",
101
+ "model_status": "loaded" if model is not None else "not_loaded",
102
+ "device": device
103
+ }
104
+
105
+ # Main recipe recommendation endpoint
106
+ @app.post("/api/recipe-suggestions", response_model=RecipeResponse)
107
+ async def get_recipe_suggestions(request: RecipeRequest):
108
+ try:
109
+ if model is None or tokenizer is None:
110
+ raise HTTPException(status_code=503, detail="Model not loaded")
111
+
112
+ print(f"πŸ“₯ Recipe request: {request.ingredients}, prefs: {request.preferences}, time: {request.max_minutes}")
113
+
114
+ # Generate recommendations
115
+ recommendations = await generate_recommendations(
116
+ request.ingredients,
117
+ request.preferences,
118
+ request.max_minutes
119
+ )
120
+
121
+ return RecipeResponse(
122
+ status="success",
123
+ recommendations=recommendations,
124
+ query=request
125
+ )
126
+
127
+ except HTTPException:
128
+ raise
129
+ except Exception as e:
130
+ print(f"❌ Error generating recommendations: {e}")
131
+ raise HTTPException(status_code=500, detail=str(e))
132
+
133
+ async def generate_recommendations(
134
+ ingredients: str,
135
+ preferences: str,
136
+ max_minutes: int
137
+ ) -> List[RecipeRecommendation]:
138
+ """Generate recipe recommendations using the fine-tuned model"""
139
+
140
+ try:
141
+ recommendations = []
142
+
143
+ # Generate 3 diverse recommendations
144
+ for i in range(3):
145
+ # Build prompt in training format
146
+ user_input = []
147
+ if ingredients:
148
+ user_input.append(f"I have {ingredients}.")
149
+ user_input.append(f"I'm looking for something ready in about {max_minutes} minutes.")
150
+ if preferences:
151
+ user_input.append(f"Preferences: {preferences}.")
152
+
153
+ user_prompt = " ".join(user_input)
154
+ prompt = f"User: {user_prompt}\nAssistant: "
155
+
156
+ # Vary temperature for diversity
157
+ temperature = 0.7 + (i * 0.1)
158
+
159
+ # Generate response
160
+ with torch.no_grad():
161
+ inputs = tokenizer(prompt, return_tensors="pt").to(device)
162
+
163
+ outputs = model.generate(
164
+ **inputs,
165
+ max_new_tokens=150,
166
+ temperature=temperature,
167
+ top_p=0.95,
168
+ do_sample=True,
169
+ pad_token_id=tokenizer.eos_token_id,
170
+ repetition_penalty=1.1
171
+ )
172
+
173
+ # Decode response
174
+ full_response = tokenizer.decode(outputs[0], skip_special_tokens=True)
175
+
176
+ # Extract assistant response
177
+ assistant_start = full_response.find("Assistant:")
178
+ if assistant_start != -1:
179
+ suggestion = full_response[assistant_start + len("Assistant:"):].strip()
180
+ else:
181
+ suggestion = full_response.strip()
182
+
183
+ # Calculate confidence (higher for first recommendations)
184
+ confidence = max(0.6, 1.0 - (i * 0.15))
185
+
186
+ recommendations.append(
187
+ RecipeRecommendation(
188
+ suggestion=suggestion,
189
+ confidence=confidence
190
+ )
191
+ )
192
+
193
+ return recommendations
194
+
195
+ except Exception as e:
196
+ print(f"❌ Error in generate_recommendations: {e}")
197
+ # Return fallback recommendations
198
+ return [
199
+ RecipeRecommendation(
200
+ suggestion="I'm having trouble generating custom recipes right now. Here's a quick suggestion: try a simple stir-fry with your ingredients!",
201
+ confidence=0.5
202
+ )
203
+ ]
204
+
205
+ # Ingredient parsing endpoint (bonus feature)
206
+ @app.post("/api/parse-ingredients")
207
+ async def parse_ingredients(text: dict):
208
+ """Parse ingredients from natural language text"""
209
+ try:
210
+ query = text.get("text", "")
211
+
212
+ # Simple ingredient extraction (you can enhance this)
213
+ common_ingredients = [
214
+ "chicken", "beef", "pork", "fish", "salmon", "shrimp", "tofu",
215
+ "rice", "pasta", "quinoa", "bread", "potatoes",
216
+ "tomatoes", "onion", "garlic", "ginger", "peppers", "broccoli",
217
+ "spinach", "carrots", "cheese", "milk", "eggs", "butter"
218
+ ]
219
+
220
+ found_ingredients = [ing for ing in common_ingredients if ing in query.lower()]
221
+
222
+ return {
223
+ "status": "success",
224
+ "ingredients": found_ingredients,
225
+ "original_text": query
226
+ }
227
+
228
+ except Exception as e:
229
+ raise HTTPException(status_code=500, detail=str(e))
230
+
231
+ # Recipe details endpoint (for future expansion)
232
+ @app.get("/api/recipe/{recipe_id}")
233
+ async def get_recipe_details(recipe_id: str):
234
+ """Get detailed recipe information (placeholder for future feature)"""
235
+ return {
236
+ "status": "success",
237
+ "message": "Recipe details endpoint - coming soon!",
238
+ "recipe_id": recipe_id
239
+ }
240
+
241
+ if __name__ == "__main__":
242
+ port = int(os.environ.get("PORT", 7860))
243
+ uvicorn.run(
244
+ "app:app",
245
+ host="0.0.0.0",
246
+ port=port,
247
+ reload=False
248
+ )
requirements.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi==0.104.1
2
+ uvicorn[standard]==0.24.0
3
+ torch>=2.0.0
4
+ transformers>=4.35.0
5
+ peft>=0.7.0
6
+ pydantic>=2.0.0
7
+ python-multipart==0.0.6
8
+ huggingface_hub>=0.19.0
9
+ accelerate>=0.24.0
10
+ safetensors>=0.4.0