Binayak Panigrahi commited on
Commit
f0f26c7
Β·
1 Parent(s): 3984aaf

Add application file

Browse files
Files changed (6) hide show
  1. api.py +408 -0
  2. index.html +554 -0
  3. rag_api.py +313 -0
  4. rag_integration.php +223 -0
  5. requirements.txt +18 -0
  6. start_rag.sh +114 -0
api.py ADDED
@@ -0,0 +1,408 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, HTTPException
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ from fastapi.responses import FileResponse
4
+ from pydantic import BaseModel
5
+ from openai import OpenAI
6
+ from datetime import datetime
7
+ from pathlib import Path
8
+ import requests
9
+ import json
10
+ import re
11
+ import os
12
+
13
+ app = FastAPI(
14
+ title="Store Product Search API",
15
+ description=(
16
+ "Three-step RAG pipeline for natural-language product search:\n"
17
+ " 1. User message β†’ NVIDIA Llama 3.1 70B (SQL generator)\n"
18
+ " 2. Generated SQL β†’ PHP DB layer (execution)\n"
19
+ " 3. Query results + user message β†’ NVIDIA Llama 3.1 70B (natural language reply)"
20
+ ),
21
+ )
22
+
23
+ # ── CORS ───────────────────────────────────────────────────────────────────────
24
+ app.add_middleware(
25
+ CORSMiddleware,
26
+ allow_origins=["*"],
27
+ allow_credentials=True,
28
+ allow_methods=["*"],
29
+ allow_headers=["*"],
30
+ )
31
+
32
+ # ── Configuration ──────────────────────────────────────────────────────────────
33
+ NVIDIA_API_KEY = os.environ.get("NVIDIA_API_KEY", "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") # Set as env/secret
34
+ PHP_DB_URL = os.environ.get("PHP_DB_URL", "http://localhost/api_ct/db_api.php")
35
+ INTERNAL_SECRET = os.environ.get("INTERNAL_SECRET", "change_this_secret_in_production")
36
+
37
+ # Output directory for SQL query results
38
+ OUTPUT_DIR = Path("./sql_results")
39
+ OUTPUT_DIR.mkdir(exist_ok=True)
40
+
41
+ # NVIDIA LLM Configuration (for SQL generation and natural language replies)
42
+ LLM_URL = "https://integrate.api.nvidia.com/v1"
43
+ LLM_MODEL = "meta/llama-3.1-70b-instruct"
44
+
45
+ # Validate API key
46
+ if not NVIDIA_API_KEY:
47
+ raise RuntimeError(
48
+ "❌ NVIDIA_API_KEY is not set!\n"
49
+ "Set it with: export NVIDIA_API_KEY='your_actual_key'\n"
50
+ "Or add to .env: NVIDIA_API_KEY=your_actual_key"
51
+ )
52
+
53
+ # Initialize OpenAI client with NVIDIA base URL (OpenAI-compatible API)
54
+ client = OpenAI(
55
+ base_url=LLM_URL,
56
+ api_key=NVIDIA_API_KEY
57
+ )
58
+
59
+ print(f"βœ“ NVIDIA API configured with model: {LLM_MODEL}")
60
+
61
+ # Anthropic kept for reference (can be removed if fully migrating to NVIDIA)
62
+ # ANTHROPIC_URL = "https://api.anthropic.com/v1/messages"
63
+ # ANTHROPIC_MODEL = "claude-sonnet-4-20250514"
64
+ # ANTHROPIC_HEADERS = {...}
65
+
66
+ PHP_HEADERS = {
67
+ "Content-Type": "application/json",
68
+ "X-Internal-Secret": INTERNAL_SECRET,
69
+ }
70
+
71
+ # ── Database schema (injected into the SQL-generation prompt) ─────────────────
72
+ DB_SCHEMA = """
73
+
74
+ Table: item_master (main product table)
75
+ Columns:
76
+ id INT Primary key, auto-increment
77
+ item_name VARCHAR(1000) Product name (e.g. 'Bag-B27-BLACK', 'LADIES PURSE B-55', 'WS-253 BLACK PU-SLIPPER')
78
+ category_id INT FK β€” category (see known category IDs below)
79
+ subcategory_id INT FK β€” subcategory
80
+ BrandID INT FK β€” brands
81
+ VendorID INT FK β€” vendor
82
+ store_id INT Always 1 in this dataset
83
+ mrp DOUBLE MRP price in INR (e.g. 496, 410, 599)
84
+ hsn VARCHAR(50) HSN/tax code (e.g. '4202' for bags, '6405' for footwear, '61112000' for kids clothes, '5407' for sarees)
85
+ size_dimension VARCHAR(45) Physical dimensions or clothing size (e.g. 'W-24 X H-17.5 X B-6', '4, 5, 6, 7', '26, 28, 30')
86
+ weight DECIMAL(11,2) Weight in grams
87
+ color VARCHAR(45) Hex color code (e.g. '#000000' for black, '#ff0000' for red, '#1b1b18' for near-black)
88
+ packingtype VARCHAR(30) Packing type: 'PCS', 'Box', 'Other'
89
+ packingtime INT Days to pack
90
+ tax_p DOUBLE Tax percentage (18 for bags/footwear, 5 for clothing)
91
+ reorder_qty INT Reorder quantity threshold
92
+ Qty INT Current stock quantity (filter Qty >= 0 for in-stock)
93
+ stock_movement VARCHAR(50) Always 'FIFO'
94
+ saleprice DOUBLE Actual selling price in INR (usually mrp/2; use this as the display price)
95
+ dis_p DOUBLE Discount percentage (0–100; if > 0 item is on discount)
96
+ description TEXT Product description
97
+ status VARCHAR(20) 'Active' or 'Inactive' β€” always filter WHERE status = 'Active'
98
+ ongoing_offer VARCHAR(11) 'yes' or 'no'
99
+ discount_percentage VARCHAR(11) Additional discount label
100
+
101
+ Known category_id values (approximate β€” use LIKE on item_name for category filtering):
102
+ 1 = Girls' Frocks / Baby Frocks
103
+ 3 = Sarees (chiffon, dola silk)
104
+ 9 = Men's Footwear (slippers)
105
+ 10 = Women's Footwear (sandals, slippers, heels, flip-flops)
106
+ 11 = Kids' Sets / Combo (boy/girl outfit sets, kids' purses)
107
+ 13 = Bags (backpacks, shoulder bags, sling bags, combo bags)
108
+ 19 = Shoulder Bags / Sling Bags (women)
109
+ 20 = Ladies Purses / Clutches / Wallets
110
+ 21 = Mobile Side Bags / Cross Body Bags
111
+ 22 = Sling Bags / Cross Body Bags
112
+ 23 = Handbags / Clutches / Ladies Sling Bags
113
+
114
+ Color notes: color is stored as hex (#000000 = Black, #ffffff = White).
115
+ For color search by name, use item_name LIKE '%COLOR%' (e.g. item_name LIKE '%BLACK%').
116
+
117
+ Table: product_images (product photos)
118
+ Columns:
119
+ image_id INT Primary key
120
+ product_id INT FK β†’ item_master.id
121
+ path_url VARCHAR(100) Filename (e.g. '83_1769753521_01.avif') β€” prepend your base image URL
122
+ default_img VARCHAR(1) 'y' = primary/default image, 'n' = additional image
123
+ img_seq INT Display order (1 = first shown)
124
+
125
+ To get the images for a product:
126
+ LEFT JOIN product_images ON item_master.id = product_images.product_id
127
+
128
+ Table: brands (brand information)
129
+ Columns:
130
+ BrandID INT Primary key
131
+ Brand VARCHAR(100) Brand name (e.g. 'Nike', 'Adidas', 'Reebok')
132
+
133
+ Table: category (product categories)
134
+ Columns:
135
+ category_id INT Primary key
136
+ category_name VARCHAR(100) Category name (e.g. 'Men's Footwear', '')
137
+
138
+ Table: subcategory (product subcategories)
139
+ Columns:
140
+ subcategoryid INT Primary key
141
+ subcategory VARCHAR(100) Subcategory name (e.g. 'Slippers', 'Sarees', 'Kids Sets')
142
+
143
+ Table: vendor (vendor information)
144
+ Columns:
145
+ ID INT Primary key
146
+ Name VARCHAR(100) Vendor name (e.g. 'Vendor A', 'Vendor B')
147
+ """
148
+
149
+ # ── System prompts ─────────────────────────────────────────────────────────────
150
+
151
+ SQL_SYSTEM_PROMPT = f"""You are an expert MySQL query generator for an online fashion store.
152
+
153
+ DATABASE SCHEMA:
154
+ {DB_SCHEMA}
155
+
156
+ Your task:
157
+ - Analyse the user's natural language product search request.
158
+ - Generate a single valid MySQL SELECT query that retrieves matching products.
159
+ - ALWAYS include these columns: id, name, category, brand, color, size, gender, price, discount_pct, stock, rating, description
160
+ - Apply WHERE filters based on what the user asked (price, color, size, category, gender, etc.)
161
+ - Use LIKE for partial text matches on name, category, brand, color.
162
+ - For price constraints: "within 500", "under 500", "below 500" β†’ price <= 500; "above 500" β†’ price >= 500; "between X and Y" β†’ price BETWEEN X AND Y
163
+ - For size: match exactly. Map "XXL" β†’ size = 'XXL', "extra large" β†’ size = 'XL', etc.
164
+ - For color: use LIKE '%Blue%' (case-insensitive intent).
165
+ - For product image: use LEFT JOIN product_images ON item_master.id = product_images.product_id AND product_images.default_img = 'y' to get the main image URL.
166
+ - Always filter WHERE stock >= 0 (only show in-stock items).
167
+ - Add ORDER BY rating DESC, price ASC to surface best value first.
168
+ - LIMIT results to 10 rows maximum.
169
+ - Return ONLY the raw SQL query β€” no markdown fences, no explanation, no preamble. Just the SQL.
170
+
171
+ Rules:
172
+ - Never use DROP, DELETE, INSERT, UPDATE, ALTER, TRUNCATE, or any DML/DDL.
173
+ - Only SELECT from the item_master table and joined tables.
174
+ - If the user's request is vague (e.g. "I want a nice dress"), generate a broad query that returns popular/relevant items (e.g. SELECT ... WHERE category LIKE '%dress%' OR name LIKE '%dress%' ORDER BY rating DESC LIMIT 10).
175
+ - Don't include Qty column of item_master in WHERE clause.
176
+ - If the request is ambiguous, generate a broad but relevant query.
177
+ """
178
+
179
+ RESPONSE_SYSTEM_PROMPT = """You are a helpful, friendly online fashion store assistant named "ShopBot".
180
+
181
+ Your task:
182
+ - You receive the user's original search query and a JSON array of matching products from the database.
183
+ - Respond in a warm, conversational tone β€” like a knowledgeable sales assistant.
184
+ - Summarize what was found, highlight 2–3 standout products with specific details (name, price, color, size, rating).
185
+ - If no products are found, suggest alternatives or ask clarifying questions.
186
+ - Keep responses concise (3–5 sentences max unless listing products).
187
+ - Format product listings clearly: use "β†’" bullet style.
188
+ - Always mention price in β‚Ή (Indian Rupees).
189
+ - Do NOT mention SQL, databases, or internal systems to the user.
190
+ - End with a helpful follow-up question or offer (e.g., "Want me to filter by a specific color?").
191
+ """
192
+
193
+
194
+ # ── Request / Response models ──────────────────────────────────────────────────
195
+
196
+ class SearchRequest(BaseModel):
197
+ message: str
198
+ conversation_history: list = [] # list of {"role": "user"|"assistant", "content": "..."}
199
+
200
+
201
+ class SearchResponse(BaseModel):
202
+ reply: str
203
+ products: list
204
+ generated_sql: str
205
+ row_count: int
206
+
207
+
208
+ # ── Helpers ───────────────────────────────────────────────────────────────────
209
+
210
+ def generate_sql(user_message: str) -> str:
211
+ """Step 1: Ask NVIDIA Llama to generate SQL from the user's natural language query."""
212
+ try:
213
+ response = client.chat.completions.create(
214
+ model=LLM_MODEL,
215
+ messages=[
216
+ {"role": "system", "content": SQL_SYSTEM_PROMPT},
217
+ {"role": "user", "content": f"User search query: {user_message}\n\nGenerate the SQL SELECT query."},
218
+ ],
219
+ temperature=0.2,
220
+ top_p=0.7,
221
+ max_tokens=512,
222
+ )
223
+ raw = response.choices[0].message.content.strip()
224
+ except Exception as e:
225
+ raise HTTPException(status_code=502, detail=f"NVIDIA SQL-gen error: {str(e)}")
226
+
227
+ print("SQL-gen response:", raw)
228
+
229
+ # Strip any accidental markdown fences
230
+ raw = re.sub(r"```sql\s*", "", raw, flags=re.IGNORECASE)
231
+ raw = re.sub(r"```\s*", "", raw).strip()
232
+
233
+ if not raw.upper().startswith("SELECT"):
234
+ raise HTTPException(
235
+ status_code=502,
236
+ detail=f"SQL generator did not return a SELECT statement. Got: {raw[:200]}",
237
+ )
238
+
239
+ print("Generated SQL:", raw)
240
+ return raw
241
+
242
+
243
+ def execute_sql(sql: str) -> dict:
244
+ """Step 2: Send SQL to the PHP DB layer and get results."""
245
+ try:
246
+ resp = requests.post(
247
+ PHP_DB_URL,
248
+ headers=PHP_HEADERS,
249
+ json={"sql": sql},
250
+ timeout=15,
251
+ )
252
+ resp.raise_for_status()
253
+ except requests.exceptions.RequestException as e:
254
+ raise HTTPException(status_code=502, detail=f"PHP DB layer error: {str(e)}")
255
+
256
+ result = resp.json()
257
+ print("DB result:", result)
258
+
259
+ if not result.get("success", False):
260
+ raise HTTPException(
261
+ status_code=422,
262
+ detail=f"Query failed: {result.get('error', 'Unknown DB error')}",
263
+ )
264
+
265
+ # Save results to file with timestamp
266
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
267
+ filename = OUTPUT_DIR / f"sql_result_{timestamp}.json"
268
+
269
+ output_data = {
270
+ "timestamp": datetime.now().isoformat(),
271
+ "sql_query": sql,
272
+ "results": result.get("results", []),
273
+ "row_count": result.get("row_count", 0),
274
+ "execution_status": "success" if result.get("success") else "failed"
275
+ }
276
+
277
+ try:
278
+ with open(filename, "w", encoding="utf-8") as f:
279
+ json.dump(output_data, f, ensure_ascii=False, indent=2)
280
+ print(f"βœ“ Results saved to: {filename}")
281
+ except Exception as e:
282
+ print(f"⚠️ Failed to save results to file: {str(e)}")
283
+
284
+ return result
285
+
286
+
287
+ def generate_reply(user_message: str, products: list, conversation_history: list) -> str:
288
+ """Step 3: Ask NVIDIA Llama to turn the query results into a friendly response."""
289
+
290
+ products_json = json.dumps(products, ensure_ascii=False, indent=2)
291
+
292
+ # Build messages: system + history + current turn
293
+ messages = [{"role": "system", "content": RESPONSE_SYSTEM_PROMPT}]
294
+
295
+ # Add conversation history
296
+ messages.extend(conversation_history)
297
+
298
+ # Add current user query
299
+ messages.append({
300
+ "role": "user",
301
+ "content": (
302
+ f"User query: {user_message}\n\n"
303
+ f"Product search results ({len(products)} items found):\n{products_json}\n\n"
304
+ f"Please give a helpful, friendly response to the user."
305
+ ),
306
+ })
307
+
308
+ try:
309
+ response = client.chat.completions.create(
310
+ model=LLM_MODEL,
311
+ messages=messages,
312
+ temperature=0.2,
313
+ top_p=0.7,
314
+ max_tokens=1024,
315
+ )
316
+ reply = response.choices[0].message.content.strip()
317
+ except Exception as e:
318
+ raise HTTPException(status_code=502, detail=f"NVIDIA reply-gen error: {str(e)}")
319
+
320
+ print("Reply response:", reply)
321
+
322
+ if not reply:
323
+ raise HTTPException(status_code=502, detail="LLM returned an empty reply")
324
+
325
+ return reply
326
+
327
+
328
+ # ── Endpoint ───────────────────────────────────────────────────────────────────
329
+
330
+ @app.post("/search", response_model=SearchResponse)
331
+ async def product_search(req: SearchRequest):
332
+ """
333
+ Natural-language product search endpoint.
334
+
335
+ Pipeline:
336
+ 1. user message β†’ Claude (SQL generator) β†’ SQL SELECT statement
337
+ 2. SQL β†’ PHP DB layer β†’ JSON product rows
338
+ 3. products JSON + user message β†’ Claude (ShopBot) β†’ friendly reply
339
+
340
+ Body:
341
+ - message: the user's search text (e.g. "I need a blue XXL shirt under 500")
342
+ - conversation_history: optional list of prior turns for context
343
+ """
344
+ if not req.message.strip():
345
+ raise HTTPException(status_code=400, detail="Message cannot be empty")
346
+
347
+ # Step 1: Generate SQL
348
+ sql = generate_sql(req.message)
349
+
350
+ # Step 2: Execute against DB
351
+ db_result = execute_sql(sql)
352
+ products = db_result.get("results", [])
353
+ row_count = db_result.get("row_count", 0)
354
+
355
+ # Step 3: Generate friendly response
356
+ reply = generate_reply(req.message, products, req.conversation_history)
357
+
358
+ return SearchResponse(
359
+ reply=reply,
360
+ products=products,
361
+ generated_sql=sql,
362
+ row_count=row_count,
363
+ )
364
+
365
+
366
+ @app.get("/health")
367
+ async def health():
368
+ return {"status": "healthy", "model": LLM_MODEL, "provider": "NVIDIA"}
369
+
370
+
371
+ @app.get("/results")
372
+ async def list_results():
373
+ """List all saved SQL query results."""
374
+ try:
375
+ files = sorted(OUTPUT_DIR.glob("sql_result_*.json"), reverse=True)
376
+ results_info = []
377
+ for f in files[:20]: # Last 20 results
378
+ results_info.append({
379
+ "filename": f.name,
380
+ "path": str(f),
381
+ "created": f.stat().st_mtime
382
+ })
383
+ return {"status": "success", "results": results_info, "total": len(results_info)}
384
+ except Exception as e:
385
+ raise HTTPException(status_code=500, detail=f"Failed to list results: {str(e)}")
386
+
387
+
388
+ @app.get("/results/{filename}")
389
+ async def get_result(filename: str):
390
+ """Retrieve a specific saved SQL query result."""
391
+ filepath = OUTPUT_DIR / filename
392
+ if not filepath.exists() or not filepath.suffix == ".json":
393
+ raise HTTPException(status_code=404, detail="Result file not found")
394
+
395
+ try:
396
+ return FileResponse(filepath, media_type="application/json", filename=filename)
397
+ except Exception as e:
398
+ raise HTTPException(status_code=500, detail=f"Failed to retrieve result: {str(e)}")
399
+
400
+
401
+ @app.get("/")
402
+ async def root():
403
+ return FileResponse("index.html")
404
+
405
+
406
+ if __name__ == "__main__":
407
+ import uvicorn
408
+ uvicorn.run("api:app", host="0.0.0.0", port=8000, reload=True)
index.html ADDED
@@ -0,0 +1,554 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>ShopBot β€” Product Search</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
8
+ <link href="https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,400;0,9..40,500;1,9..40,300&display=swap" rel="stylesheet" />
9
+ <style>
10
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
11
+
12
+ :root {
13
+ --sage: #3d6b4f;
14
+ --sage-light:#e8f0eb;
15
+ --sage-mid: #a8c4b0;
16
+ --cream: #faf8f4;
17
+ --ink: #1a1a18;
18
+ --ink-muted: #6b6b66;
19
+ --ink-faint: #a8a8a4;
20
+ --surface: #ffffff;
21
+ --border: rgba(0,0,0,0.08);
22
+ --accent: #c17d3c;
23
+ --accent-bg: #fdf3e7;
24
+ --radius-sm: 8px;
25
+ --radius-md: 14px;
26
+ --radius-lg: 22px;
27
+ }
28
+
29
+ body {
30
+ font-family: 'DM Sans', sans-serif;
31
+ background: var(--cream);
32
+ color: var(--ink);
33
+ min-height: 100vh;
34
+ display: flex;
35
+ flex-direction: column;
36
+ }
37
+
38
+ /* ── HEADER ── */
39
+ header {
40
+ padding: 1.25rem 2rem;
41
+ background: var(--surface);
42
+ border-bottom: 1px solid var(--border);
43
+ display: flex;
44
+ align-items: center;
45
+ gap: 14px;
46
+ position: sticky;
47
+ top: 0;
48
+ z-index: 100;
49
+ }
50
+ .logo-mark {
51
+ width: 38px; height: 38px;
52
+ background: var(--sage);
53
+ border-radius: 10px;
54
+ display: flex; align-items: center; justify-content: center;
55
+ flex-shrink: 0;
56
+ }
57
+ .logo-mark svg { width: 20px; height: 20px; fill: none; stroke: #fff; stroke-width: 2; stroke-linecap: round; }
58
+ .header-text h1 { font-family: 'DM Serif Display', serif; font-size: 1.15rem; font-weight: 400; color: var(--ink); letter-spacing: -0.01em; }
59
+ .header-text p { font-size: 0.75rem; color: var(--ink-muted); margin-top: 1px; }
60
+ .status-dot { margin-left: auto; display: flex; align-items: center; gap: 6px; font-size: 0.72rem; color: var(--ink-muted); }
61
+ .dot { width: 7px; height: 7px; border-radius: 50%; background: #4caf7d; animation: pulse 2s infinite; }
62
+ @keyframes pulse { 0%,100%{opacity:1} 50%{opacity:.4} }
63
+
64
+ /* ── MAIN LAYOUT ── */
65
+ main {
66
+ flex: 1;
67
+ display: flex;
68
+ flex-direction: column;
69
+ max-width: 860px;
70
+ width: 100%;
71
+ margin: 0 auto;
72
+ padding: 0 1rem;
73
+ }
74
+
75
+ /* ── CHAT AREA ── */
76
+ #chat {
77
+ flex: 1;
78
+ overflow-y: auto;
79
+ padding: 2rem 0 1rem;
80
+ display: flex;
81
+ flex-direction: column;
82
+ gap: 1.5rem;
83
+ min-height: 0;
84
+ }
85
+
86
+ /* ── WELCOME ── */
87
+ .welcome {
88
+ text-align: center;
89
+ padding: 3rem 1rem 2rem;
90
+ }
91
+ .welcome-icon {
92
+ width: 64px; height: 64px;
93
+ background: var(--sage-light);
94
+ border-radius: 18px;
95
+ display: flex; align-items: center; justify-content: center;
96
+ margin: 0 auto 1.25rem;
97
+ }
98
+ .welcome-icon svg { width: 32px; height: 32px; stroke: var(--sage); fill: none; stroke-width: 1.8; stroke-linecap: round; }
99
+ .welcome h2 { font-family: 'DM Serif Display', serif; font-size: 1.7rem; font-weight: 400; color: var(--ink); margin-bottom: 0.5rem; }
100
+ .welcome p { font-size: 0.88rem; color: var(--ink-muted); max-width: 360px; margin: 0 auto 1.75rem; line-height: 1.6; }
101
+ .chips { display: flex; flex-wrap: wrap; justify-content: center; gap: 8px; }
102
+ .chip {
103
+ padding: 7px 14px;
104
+ border-radius: 999px;
105
+ border: 1px solid var(--border);
106
+ background: var(--surface);
107
+ font-size: 0.78rem;
108
+ color: var(--ink-muted);
109
+ cursor: pointer;
110
+ transition: all .15s;
111
+ }
112
+ .chip:hover { border-color: var(--sage-mid); color: var(--sage); background: var(--sage-light); }
113
+
114
+ /* ── MESSAGE BUBBLES ── */
115
+ .msg { display: flex; gap: 10px; }
116
+ .msg.user { flex-direction: row-reverse; }
117
+ .msg.bot { flex-direction: row; }
118
+
119
+ .avatar {
120
+ width: 30px; height: 30px; border-radius: 50%;
121
+ flex-shrink: 0; display: flex; align-items: center; justify-content: center;
122
+ font-size: 0.7rem; font-weight: 500; margin-top: 2px;
123
+ }
124
+ .avatar.bot { background: var(--sage); color: #fff; }
125
+ .avatar.user { background: var(--ink); color: #fff; font-size: 0.65rem; }
126
+
127
+ .bubble {
128
+ max-width: 75%;
129
+ padding: 0.85rem 1.1rem;
130
+ border-radius: var(--radius-lg);
131
+ font-size: 0.875rem;
132
+ line-height: 1.65;
133
+ }
134
+ .msg.user .bubble {
135
+ background: var(--ink);
136
+ color: #fff;
137
+ border-bottom-right-radius: var(--radius-sm);
138
+ }
139
+ .msg.bot .bubble {
140
+ background: var(--surface);
141
+ border: 1px solid var(--border);
142
+ border-bottom-left-radius: var(--radius-sm);
143
+ color: var(--ink);
144
+ }
145
+
146
+ /* ── PRODUCT GRID ── */
147
+ .products-grid {
148
+ display: grid;
149
+ grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
150
+ gap: 10px;
151
+ margin-top: 12px;
152
+ }
153
+ .product-card {
154
+ background: var(--cream);
155
+ border: 1px solid var(--border);
156
+ border-radius: var(--radius-md);
157
+ padding: 12px;
158
+ cursor: pointer;
159
+ transition: all .2s;
160
+ position: relative;
161
+ }
162
+ .product-card:hover { border-color: var(--sage-mid); transform: translateY(-1px); }
163
+ .product-card .badge {
164
+ position: absolute; top: 10px; right: 10px;
165
+ background: var(--accent-bg);
166
+ color: var(--accent);
167
+ font-size: 0.65rem;
168
+ font-weight: 500;
169
+ padding: 2px 7px;
170
+ border-radius: 999px;
171
+ }
172
+ .product-img {
173
+ width: 100%; height: 100px;
174
+ background: var(--sage-light);
175
+ border-radius: var(--radius-sm);
176
+ display: flex; align-items: center; justify-content: center;
177
+ margin-bottom: 10px;
178
+ overflow: hidden;
179
+ }
180
+ .product-img svg { width: 36px; height: 36px; stroke: var(--sage); fill: none; stroke-width: 1.4; }
181
+ .product-img img { width: 100%; height: 100%; object-fit: cover; }
182
+ .product-name { font-size: 0.8rem; font-weight: 500; color: var(--ink); margin-bottom: 4px; line-height: 1.3; }
183
+ .product-meta { font-size: 0.7rem; color: var(--ink-muted); display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 6px; }
184
+ .product-price { font-size: 0.95rem; font-weight: 500; color: var(--sage); }
185
+ .product-rating { font-size: 0.7rem; color: var(--accent); }
186
+
187
+ /* ── SQL DEBUG ── */
188
+ .sql-block {
189
+ margin-top: 10px;
190
+ background: #f4f4f0;
191
+ border-radius: var(--radius-sm);
192
+ padding: 8px 12px;
193
+ font-family: 'Courier New', monospace;
194
+ font-size: 0.7rem;
195
+ color: var(--ink-muted);
196
+ white-space: pre-wrap;
197
+ word-break: break-all;
198
+ border-left: 3px solid var(--sage-mid);
199
+ cursor: pointer;
200
+ }
201
+ .sql-label { font-size: 0.65rem; color: var(--ink-faint); margin-bottom: 4px; text-transform: uppercase; letter-spacing: 0.05em; }
202
+ .sql-toggle { font-size: 0.72rem; color: var(--ink-muted); cursor: pointer; display: inline-flex; align-items: center; gap: 4px; margin-top: 8px; }
203
+ .sql-toggle:hover { color: var(--sage); }
204
+
205
+ /* ── TYPING INDICATOR ── */
206
+ .typing { display: flex; gap: 5px; align-items: center; padding: 12px 16px; }
207
+ .typing span {
208
+ width: 6px; height: 6px; border-radius: 50%;
209
+ background: var(--sage-mid); display: inline-block;
210
+ animation: bounce 1.2s infinite;
211
+ }
212
+ .typing span:nth-child(2) { animation-delay: .2s; }
213
+ .typing span:nth-child(3) { animation-delay: .4s; }
214
+ @keyframes bounce { 0%,100%{transform:translateY(0)} 50%{transform:translateY(-5px)} }
215
+
216
+ /* ── INPUT BAR ── */
217
+ .input-bar {
218
+ padding: 1rem 0 1.5rem;
219
+ position: sticky;
220
+ bottom: 0;
221
+ background: linear-gradient(to top, var(--cream) 80%, transparent);
222
+ }
223
+ .input-wrap {
224
+ display: flex;
225
+ align-items: flex-end;
226
+ gap: 10px;
227
+ background: var(--surface);
228
+ border: 1px solid var(--border);
229
+ border-radius: var(--radius-lg);
230
+ padding: 10px 10px 10px 16px;
231
+ box-shadow: 0 2px 16px rgba(0,0,0,0.06);
232
+ }
233
+ #user-input {
234
+ flex: 1;
235
+ border: none;
236
+ outline: none;
237
+ resize: none;
238
+ font-family: 'DM Sans', sans-serif;
239
+ font-size: 0.875rem;
240
+ color: var(--ink);
241
+ background: transparent;
242
+ line-height: 1.5;
243
+ max-height: 120px;
244
+ min-height: 22px;
245
+ }
246
+ #user-input::placeholder { color: var(--ink-faint); }
247
+ #send-btn {
248
+ width: 36px; height: 36px;
249
+ border-radius: 50%;
250
+ border: none;
251
+ background: var(--sage);
252
+ color: #fff;
253
+ cursor: pointer;
254
+ display: flex; align-items: center; justify-content: center;
255
+ transition: all .15s;
256
+ flex-shrink: 0;
257
+ }
258
+ #send-btn:hover { background: #2e5039; transform: scale(1.05); }
259
+ #send-btn:disabled { background: var(--ink-faint); cursor: not-allowed; transform: none; }
260
+ #send-btn svg { width: 16px; height: 16px; stroke: #fff; fill: none; stroke-width: 2.2; stroke-linecap: round; }
261
+
262
+ .hint { text-align: center; font-size: 0.68rem; color: var(--ink-faint); margin-top: 8px; }
263
+
264
+ /* ── CATEGORY ICONS ── */
265
+ .cat-icon { width: 36px; height: 36px; stroke: var(--sage); fill: none; stroke-width: 1.4; }
266
+ </style>
267
+ </head>
268
+ <body>
269
+
270
+ <header>
271
+ <div class="logo-mark">
272
+ <svg viewBox="0 0 24 24"><path d="M6 2l.01 6L10 12l-3.99 4.01L6 22h12v-6l-4-4 4-3.99V2H6z"/></svg>
273
+ </div>
274
+ <div class="header-text">
275
+ <h1>ShopBot</h1>
276
+ <p>AI-powered product search</p>
277
+ </div>
278
+ <div class="status-dot"><span class="dot"></span> Live</div>
279
+ </header>
280
+
281
+ <main>
282
+ <div id="chat">
283
+ <div class="welcome" id="welcome">
284
+ <div class="welcome-icon">
285
+ <svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/></svg>
286
+ </div>
287
+ <h2>What are you looking for?</h2>
288
+ <p>Describe what you need in plain language β€” size, color, price, category β€” and I'll find it.</p>
289
+ <div class="chips">
290
+ <span class="chip" onclick="setInput('Blue dress under β‚Ή500')">Blue dress under β‚Ή500</span>
291
+ <span class="chip" onclick="setInput('XXL white shirt for men')">XXL white shirt for men</span>
292
+ <span class="chip" onclick="setInput('Nike running shoes size 9')">Nike shoes size 9</span>
293
+ <span class="chip" onclick="setInput('Women ethnic wear below 700')">Women ethnic below β‚Ή700</span>
294
+ <span class="chip" onclick="setInput('Best rated jeans')">Best rated jeans</span>
295
+ </div>
296
+ </div>
297
+ </div>
298
+
299
+ <div class="input-bar">
300
+ <div class="input-wrap">
301
+ <textarea id="user-input" rows="1" placeholder="Try: 'I need a red XXL shirt within β‚Ή500'…" maxlength="400"></textarea>
302
+ <button id="send-btn" onclick="sendMessage()" title="Search">
303
+ <svg viewBox="0 0 24 24"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>
304
+ </button>
305
+ </div>
306
+ <p class="hint">Press Enter to send Β· Shift+Enter for new line</p>
307
+ </div>
308
+ </main>
309
+
310
+ <script>
311
+ const API_BASE = ""; // same origin; change to "http://localhost:8000" for dev
312
+ let history = [];
313
+ let isLoading = false;
314
+
315
+ const chatEl = document.getElementById("chat");
316
+ const inputEl = document.getElementById("user-input");
317
+ const sendBtn = document.getElementById("send-btn");
318
+ const welcomeEl= document.getElementById("welcome");
319
+
320
+ function setInput(text) {
321
+ inputEl.value = text;
322
+ inputEl.focus();
323
+ autoResize();
324
+ }
325
+
326
+ inputEl.addEventListener("keydown", e => {
327
+ if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); sendMessage(); }
328
+ });
329
+ inputEl.addEventListener("input", autoResize);
330
+ function autoResize() {
331
+ inputEl.style.height = "auto";
332
+ inputEl.style.height = Math.min(inputEl.scrollHeight, 120) + "px";
333
+ }
334
+
335
+ function sendMessage() {
336
+ const text = inputEl.value.trim();
337
+ if (!text || isLoading) return;
338
+
339
+ if (welcomeEl) { welcomeEl.style.display = "none"; }
340
+
341
+ appendBubble("user", text);
342
+ inputEl.value = "";
343
+ autoResize();
344
+
345
+ const typingId = appendTyping();
346
+ setLoading(true);
347
+
348
+ fetch(`${API_BASE}/search`, {
349
+ method: "POST",
350
+ headers: { "Content-Type": "application/json" },
351
+ body: JSON.stringify({ message: text, conversation_history: history }),
352
+ })
353
+ .then(r => r.json())
354
+ .then(data => {
355
+ removeTyping(typingId);
356
+ setLoading(false);
357
+
358
+ if (data.detail) {
359
+ appendBubble("bot", `⚠️ ${data.detail}`);
360
+ return;
361
+ }
362
+
363
+ history.push({ role: "user", content: text });
364
+ history.push({ role: "assistant", content: data.reply });
365
+ if (history.length > 20) history = history.slice(-20);
366
+
367
+ appendBotResponse(data);
368
+ })
369
+ .catch(err => {
370
+ removeTyping(typingId);
371
+ setLoading(false);
372
+ appendBubble("bot", "Connection error. Is the server running? (" + err.message + ")");
373
+ });
374
+ }
375
+
376
+ function setLoading(v) {
377
+ isLoading = v;
378
+ sendBtn.disabled = v;
379
+ }
380
+
381
+ function appendBubble(role, text) {
382
+ const wrap = document.createElement("div");
383
+ wrap.className = "msg " + role;
384
+
385
+ const av = document.createElement("div");
386
+ av.className = "avatar " + role;
387
+ av.textContent = role === "bot" ? "S" : "You";
388
+
389
+ const bub = document.createElement("div");
390
+ bub.className = "bubble";
391
+ bub.textContent = text;
392
+
393
+ wrap.appendChild(av);
394
+ wrap.appendChild(bub);
395
+ chatEl.appendChild(wrap);
396
+ scrollToBottom();
397
+ return wrap;
398
+ }
399
+
400
+ function appendBotResponse(data) {
401
+ const wrap = document.createElement("div");
402
+ wrap.className = "msg bot";
403
+
404
+ const av = document.createElement("div");
405
+ av.className = "avatar bot";
406
+ av.textContent = "S";
407
+
408
+ const bub = document.createElement("div");
409
+ bub.className = "bubble";
410
+
411
+ const replyText = document.createElement("div");
412
+ replyText.style.marginBottom = data.products && data.products.length ? "12px" : "0";
413
+ replyText.textContent = data.reply;
414
+ bub.appendChild(replyText);
415
+
416
+ // Product grid
417
+ if (data.products && data.products.length > 0) {
418
+ const grid = document.createElement("div");
419
+ grid.className = "products-grid";
420
+ data.products.forEach(p => grid.appendChild(makeCard(p)));
421
+ bub.appendChild(grid);
422
+ }
423
+
424
+ // SQL debug toggle
425
+ if (data.generated_sql) {
426
+ const toggle = document.createElement("span");
427
+ toggle.className = "sql-toggle";
428
+ toggle.innerHTML = `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg> View generated SQL`;
429
+ const sqlBlock = document.createElement("div");
430
+ sqlBlock.className = "sql-block";
431
+ sqlBlock.style.display = "none";
432
+ const label = document.createElement("div");
433
+ label.className = "sql-label";
434
+ label.textContent = "Generated SQL Β· " + (data.row_count || 0) + " results";
435
+ const code = document.createElement("code");
436
+ code.textContent = data.generated_sql;
437
+ sqlBlock.appendChild(label);
438
+ sqlBlock.appendChild(code);
439
+
440
+ let open = false;
441
+ toggle.onclick = () => {
442
+ open = !open;
443
+ sqlBlock.style.display = open ? "block" : "none";
444
+ toggle.innerHTML = (open
445
+ ? `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="18 15 12 9 6 15"/></svg> Hide SQL`
446
+ : `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg> View generated SQL`);
447
+ };
448
+ bub.appendChild(toggle);
449
+ bub.appendChild(sqlBlock);
450
+ }
451
+
452
+ wrap.appendChild(av);
453
+ wrap.appendChild(bub);
454
+ chatEl.appendChild(wrap);
455
+ scrollToBottom();
456
+ }
457
+
458
+ function makeCard(p) {
459
+ const card = document.createElement("div");
460
+ card.className = "product-card";
461
+ card.title = p.description || p.name;
462
+
463
+ const discountPct = parseInt(p.discount_pct) || 0;
464
+ if (discountPct > 0) {
465
+ const badge = document.createElement("div");
466
+ badge.className = "badge";
467
+ badge.textContent = `-${discountPct}%`;
468
+ card.appendChild(badge);
469
+ }
470
+
471
+ const img = document.createElement("div");
472
+ img.className = "product-img";
473
+ if (p.image_url) {
474
+ const i = document.createElement("img");
475
+ i.src = 'http://localhost/api_ct/productimage/'+p.image_url;
476
+ i.alt = p.name;
477
+ i.onerror = () => { img.innerHTML = categoryIcon(p.category); };
478
+ img.appendChild(i);
479
+ } else {
480
+ img.innerHTML = categoryIcon(p.category);
481
+ }
482
+ card.appendChild(img);
483
+
484
+ const name = document.createElement("div");
485
+ name.className = "product-name";
486
+ name.textContent = p.name;
487
+ card.appendChild(name);
488
+
489
+ const meta = document.createElement("div");
490
+ meta.className = "product-meta";
491
+ if (p.color) meta.innerHTML += `<span>${p.color}</span>`;
492
+ if (p.size) meta.innerHTML += `<span>Β· ${p.size}</span>`;
493
+ if (p.brand) meta.innerHTML += `<span>Β· ${p.brand}</span>`;
494
+ card.appendChild(meta);
495
+
496
+ const bottom = document.createElement("div");
497
+ bottom.style.display = "flex"; bottom.style.justifyContent = "space-between"; bottom.style.alignItems = "center";
498
+ const price = document.createElement("div");
499
+ price.className = "product-price";
500
+ const finalPrice = discountPct > 0 ? (parseFloat(p.price) * (1 - discountPct/100)).toFixed(0) : parseFloat(p.price).toFixed(0);
501
+ price.textContent = `β‚Ή${finalPrice}`;
502
+ if (discountPct > 0) {
503
+ const original = document.createElement("span");
504
+ original.style.cssText = "font-size:0.65rem;color:#a8a8a4;text-decoration:line-through;margin-left:4px;";
505
+ original.textContent = `β‚Ή${parseFloat(p.price).toFixed(0)}`;
506
+ price.appendChild(original);
507
+ }
508
+ const rating = document.createElement("div");
509
+ rating.className = "product-rating";
510
+ if (p.rating) rating.textContent = `β˜… ${parseFloat(p.rating).toFixed(1)}`;
511
+ bottom.appendChild(price);
512
+ bottom.appendChild(rating);
513
+ card.appendChild(bottom);
514
+
515
+ return card;
516
+ }
517
+
518
+ function categoryIcon(cat) {
519
+ const c = (cat || "").toLowerCase();
520
+ if (c.includes("dress") || c.includes("kurti"))
521
+ return `<svg class="cat-icon" viewBox="0 0 24 24"><path d="M12 2l-4 4H5l2 4v12h10V10l2-4h-3L12 2z"/></svg>`;
522
+ if (c.includes("shirt") || c.includes("t-shirt") || c.includes("tee"))
523
+ return `<svg class="cat-icon" viewBox="0 0 24 24"><path d="M2 7l4-4 4 4v13H2V7zm20 0l-4-4-4 4v13h8V7z"/><line x1="6" y1="7" x2="18" y2="7"/></svg>`;
524
+ if (c.includes("jean") || c.includes("pant"))
525
+ return `<svg class="cat-icon" viewBox="0 0 24 24"><path d="M4 2h16v4l-2 16h-4l-2-8-2 8H6L4 6V2z"/></svg>`;
526
+ if (c.includes("shoe") || c.includes("sandal") || c.includes("sneaker"))
527
+ return `<svg class="cat-icon" viewBox="0 0 24 24"><path d="M3 16l2-8h6l2 4h7a1 1 0 010 2l-1 2H3z"/><line x1="7" y1="8" x2="7" y2="16"/></svg>`;
528
+ return `<svg class="cat-icon" viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M9 3v18M15 3v18"/></svg>`;
529
+ }
530
+
531
+ function appendTyping() {
532
+ const id = "typing-" + Date.now();
533
+ const wrap = document.createElement("div");
534
+ wrap.className = "msg bot"; wrap.id = id;
535
+ const av = document.createElement("div");
536
+ av.className = "avatar bot"; av.textContent = "S";
537
+ const bub = document.createElement("div");
538
+ bub.className = "bubble";
539
+ bub.innerHTML = `<div class="typing"><span></span><span></span><span></span></div>`;
540
+ wrap.appendChild(av); wrap.appendChild(bub);
541
+ chatEl.appendChild(wrap);
542
+ scrollToBottom();
543
+ return id;
544
+ }
545
+ function removeTyping(id) {
546
+ const el = document.getElementById(id);
547
+ if (el) el.remove();
548
+ }
549
+ function scrollToBottom() {
550
+ chatEl.scrollTop = chatEl.scrollHeight;
551
+ }
552
+ </script>
553
+ </body>
554
+ </html>
rag_api.py ADDED
@@ -0,0 +1,313 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Product RAG API - Retrieval-Augmented Generation for E-Commerce Chat
3
+ Helps users discover products through natural language queries
4
+ Uses NVIDIA Mistral models for intelligent recommendations
5
+ """
6
+
7
+ from fastapi import FastAPI, HTTPException
8
+ from fastapi.middleware.cors import CORSMiddleware
9
+ from pydantic import BaseModel
10
+ from typing import List, Optional
11
+ import os
12
+ from dotenv import load_dotenv
13
+ import numpy as np
14
+ from sentence_transformers import SentenceTransformer
15
+ import faiss
16
+ import json
17
+ import requests
18
+
19
+ # Load environment variables
20
+ load_dotenv()
21
+
22
+ # ── Configuration ─────────────────────────────────────────────────────────────
23
+ NVIDIA_API_KEY = os.getenv("NVIDIA_API_KEY", "")
24
+ EMBEDDING_MODEL = "sentence-transformers/all-MiniLM-L6-v2" # Fast, lightweight model
25
+ NVIDIA_API_URL = "https://integrate.api.nvidia.com/v1/chat/completions"
26
+ NVIDIA_MODEL = os.getenv("NVIDIA_MODEL", "mistralai/mistral-medium-3.5-128b")
27
+
28
+ app = FastAPI(
29
+ title="Product RAG API",
30
+ description="Retrieval-Augmented Generation for e-commerce product search and recommendations",
31
+ )
32
+
33
+ app.add_middleware(
34
+ CORSMiddleware,
35
+ allow_origins=["*"],
36
+ allow_credentials=True,
37
+ allow_methods=["*"],
38
+ allow_headers=["*"],
39
+ )
40
+
41
+ # ── Validate Configuration ────────────────────────────────────────────────────
42
+ if not NVIDIA_API_KEY:
43
+ print("⚠️ WARNING: NVIDIA_API_KEY not set. Set it in .env file")
44
+ else:
45
+ print(f"βœ“ NVIDIA_API_KEY configured (last 8 chars: ...{NVIDIA_API_KEY[-8:]})")
46
+
47
+ print(f"βœ“ Using NVIDIA model: {NVIDIA_MODEL}")
48
+ print(f"βœ“ NVIDIA API endpoint: {NVIDIA_API_URL}")
49
+
50
+ # ── Initialize Embedding Model ────────────────────────────────────────────────
51
+ try:
52
+ embedding_model = SentenceTransformer(EMBEDDING_MODEL)
53
+ print(f"βœ“ Embedding model loaded: {EMBEDDING_MODEL}")
54
+ except Exception as e:
55
+ print(f"βœ— Failed to load embedding model: {e}")
56
+ embedding_model = None
57
+
58
+ # ── System Prompt ─────────────────────────────────────────────────────────────
59
+ RAG_SYSTEM_PROMPT = """You are a helpful e-commerce shopping assistant.
60
+ Your goal is to help customers find the perfect products based on their needs.
61
+
62
+ Given the customer's query and a list of relevant products from the catalog, provide:
63
+ 1. A friendly, natural response addressing their query
64
+ 2. Specific product recommendations with brief explanations why each product fits their needs
65
+ 3. Price and key feature information
66
+ 4. Alternative suggestions if appropriate
67
+
68
+ Keep responses concise, helpful, and focused on the products provided.
69
+ Format product recommendations clearly with product names, prices, and key features."""
70
+
71
+ # ── Pydantic Models ───────────────────────────────────────────────────────────
72
+
73
+ class Product(BaseModel):
74
+ id: str | int
75
+ name: str
76
+ description: str
77
+ price: float
78
+ category: str
79
+ brand: Optional[str] = None
80
+ image_path: Optional[str] = None
81
+ rating: Optional[float] = None
82
+
83
+
84
+ class SearchRequest(BaseModel):
85
+ query: str
86
+ products: List[Product]
87
+ top_k: int = 5 # Number of products to retrieve
88
+
89
+
90
+ class RecommendedProduct(BaseModel):
91
+ id: str | int
92
+ name: str
93
+ price: float
94
+ reason: Optional[str] = None
95
+
96
+
97
+ class SearchResponse(BaseModel):
98
+ message: str
99
+ products: List[RecommendedProduct]
100
+ timestamp: str
101
+
102
+
103
+ # ── RAG Functions ─────────────────────────────────────────────────────────────
104
+
105
+ def create_embeddings(texts: List[str]) -> np.ndarray:
106
+ """Create embeddings for a list of texts using sentence-transformers"""
107
+ if not embedding_model:
108
+ raise HTTPException(status_code=500, detail="Embedding model not initialized")
109
+
110
+ embeddings = embedding_model.encode(texts, convert_to_numpy=True)
111
+ return embeddings
112
+
113
+
114
+ def search_products(
115
+ query: str,
116
+ products: List[Product],
117
+ top_k: int = 5
118
+ ) -> List[Product]:
119
+ """
120
+ Search for relevant products using vector similarity
121
+ Returns top_k most relevant products
122
+ """
123
+ if not products:
124
+ return []
125
+
126
+ if len(products) <= top_k:
127
+ return products
128
+
129
+ # Create product descriptions for embedding
130
+ product_texts = [
131
+ f"{p.name} {p.category} {p.brand or ''} {p.description}".strip()
132
+ for p in products
133
+ ]
134
+
135
+ # Encode query and products
136
+ query_embedding = embedding_model.encode([query], convert_to_numpy=True)[0]
137
+ product_embeddings = create_embeddings(product_texts)
138
+
139
+ # Use FAISS for efficient similarity search
140
+ embedding_dim = product_embeddings.shape[1]
141
+ index = faiss.IndexFlatL2(embedding_dim)
142
+ index.add(product_embeddings.astype(np.float32))
143
+
144
+ # Search for top_k similar products
145
+ query_vector = np.array([query_embedding], dtype=np.float32)
146
+ distances, indices = index.search(query_vector, min(top_k, len(products)))
147
+
148
+ # Return relevant products in order
149
+ relevant_products = [products[i] for i in indices[0]]
150
+ return relevant_products
151
+
152
+
153
+ def generate_rag_response(
154
+ query: str,
155
+ relevant_products: List[Product]
156
+ ) -> tuple[str, List[RecommendedProduct]]:
157
+ """
158
+ Generate LLM response using RAG with retrieved products via NVIDIA API
159
+ Returns (message, recommended_products)
160
+ """
161
+ if not NVIDIA_API_KEY:
162
+ raise HTTPException(
163
+ status_code=500,
164
+ detail="NVIDIA_API_KEY not configured. Add to .env file"
165
+ )
166
+
167
+ # Format products for the prompt
168
+ product_context = "\n".join([
169
+ f"- {p.name} (ID: {p.id}) - β‚Ή{p.price} - {p.category} - {p.description}"
170
+ for p in relevant_products
171
+ ])
172
+
173
+ # Prepare NVIDIA API request
174
+ headers = {
175
+ "Authorization": f"Bearer {NVIDIA_API_KEY}",
176
+ "Accept": "application/json"
177
+ }
178
+
179
+ payload = {
180
+ "model": NVIDIA_MODEL,
181
+ "messages": [
182
+ {"role": "system", "content": RAG_SYSTEM_PROMPT},
183
+ {
184
+ "role": "user",
185
+ "content": f"""Customer query: {query}
186
+
187
+ Available products from our catalog:
188
+ {product_context}
189
+
190
+ Please provide personalized recommendations based on these products."""
191
+ }
192
+ ],
193
+ "max_tokens": 500,
194
+ "temperature": 0.7,
195
+ "top_p": 1.0,
196
+ "stream": False
197
+ }
198
+
199
+ try:
200
+ response = requests.post(NVIDIA_API_URL, headers=headers, json=payload, timeout=30)
201
+ response.raise_for_status()
202
+
203
+ result = response.json()
204
+ ai_message = result.get("choices", [{}])[0].get("message", {}).get("content", "")
205
+
206
+ if not ai_message:
207
+ raise HTTPException(
208
+ status_code=502,
209
+ detail="NVIDIA API returned empty response"
210
+ )
211
+
212
+ # Format recommended products
213
+ recommended = [
214
+ RecommendedProduct(
215
+ id=p.id,
216
+ name=p.name,
217
+ price=p.price,
218
+ reason=f"{p.category} - {p.description[:100]}"
219
+ )
220
+ for p in relevant_products
221
+ ]
222
+
223
+ return ai_message, recommended
224
+
225
+ except requests.exceptions.RequestException as e:
226
+ error_msg = str(e)
227
+ if hasattr(e.response, 'text'):
228
+ error_msg = f"{error_msg}: {e.response.text}"
229
+ print(f"NVIDIA API error: {error_msg}")
230
+ raise HTTPException(status_code=502, detail=f"NVIDIA API error: {error_msg}")
231
+ except Exception as e:
232
+ print(f"Unexpected error calling NVIDIA API: {str(e)}")
233
+ raise HTTPException(status_code=502, detail=f"LLM error: {str(e)}")
234
+
235
+
236
+ # ── Endpoints ─────────────────────────────────────────────────────────────────
237
+
238
+ @app.post("/search-products", response_model=SearchResponse)
239
+ async def search_products_endpoint(request: SearchRequest) -> SearchResponse:
240
+ """
241
+ Main RAG endpoint for product search and recommendations
242
+
243
+ 1. Retrieves relevant products based on query
244
+ 2. Generates AI response with recommendations
245
+ 3. Returns message + product list
246
+ """
247
+ if not request.query.strip():
248
+ raise HTTPException(status_code=400, detail="Query cannot be empty")
249
+
250
+ if not request.products:
251
+ raise HTTPException(status_code=400, detail="Products list cannot be empty")
252
+
253
+ # Step 1: Retrieve relevant products
254
+ relevant_products = search_products(
255
+ request.query,
256
+ request.products,
257
+ request.top_k
258
+ )
259
+
260
+ # Step 2: Generate LLM response with retrieved products
261
+ ai_message, recommended_products = generate_rag_response(
262
+ request.query,
263
+ relevant_products
264
+ )
265
+
266
+ # Step 3: Return response
267
+ from datetime import datetime
268
+ return SearchResponse(
269
+ message=ai_message,
270
+ products=recommended_products,
271
+ timestamp=datetime.utcnow().isoformat()
272
+ )
273
+
274
+
275
+ @app.get("/health")
276
+ async def health():
277
+ """Health check endpoint"""
278
+ return {
279
+ "status": "healthy",
280
+ "embedding_model": EMBEDDING_MODEL,
281
+ "llm": NVIDIA_MODEL,
282
+ "provider": "NVIDIA",
283
+ "rag_enabled": embedding_model is not None and bool(NVIDIA_API_KEY)
284
+ }
285
+
286
+
287
+ @app.get("/")
288
+ async def root():
289
+ """Root endpoint with API information"""
290
+ return {
291
+ "name": "Product RAG API",
292
+ "version": "2.0.0",
293
+ "description": "Retrieval-Augmented Generation for e-commerce product recommendations",
294
+ "pipeline": "Sentence-Transformers β†’ FAISS β†’ NVIDIA Mistral",
295
+ "model": NVIDIA_MODEL,
296
+ "provider": "NVIDIA",
297
+ "endpoints": {
298
+ "POST /search-products": "Search products and get AI recommendations",
299
+ "GET /health": "Health check",
300
+ "GET /docs": "Swagger UI (API documentation)",
301
+ },
302
+ "how_to_use": {
303
+ "step_1": "Call POST /search-products with customer query and product list",
304
+ "step_2": "API retrieves relevant products using semantic search",
305
+ "step_3": "NVIDIA Mistral generates personalized recommendation message",
306
+ "returns": "AI-generated message + recommended products with prices"
307
+ }
308
+ }
309
+
310
+
311
+ if __name__ == "__main__":
312
+ import uvicorn
313
+ uvicorn.run("rag_api:app", host="0.0.0.0", port=8004, reload=True)
rag_integration.php ADDED
@@ -0,0 +1,223 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * RAG Integration for Chat - PHP Integration Layer
4
+ * Connects PHP backend with Python RAG API (powered by NVIDIA Mistral)
5
+ *
6
+ * This file provides functions to:
7
+ * 1. Fetch products from database
8
+ * 2. Call the RAG API
9
+ * 3. Format and return responses to the frontend
10
+ *
11
+ * The RAG API uses:
12
+ * - Sentence-Transformers for semantic search
13
+ * - FAISS for vector similarity
14
+ * - NVIDIA Mistral models for intelligent recommendations
15
+ */
16
+
17
+ // Configuration
18
+ define('RAG_API_URL', getenv('RAG_API_URL') ?: 'http://localhost:8004');
19
+ define('RAG_ENDPOINT', '/search-products');
20
+
21
+ /**
22
+ * Fetch products from database
23
+ * Customize this function based on your database schema
24
+ *
25
+ * @param mysqli $conn Database connection
26
+ * @param string $category Optional category filter
27
+ * @param int $limit Max products to fetch
28
+ * @return array Array of product objects
29
+ */
30
+ function get_products_for_rag($conn, $category = null, $limit = 50) {
31
+ $query = "SELECT id, name, description, price, category, brand, image_path, rating
32
+ FROM products
33
+ WHERE status = 'active'";
34
+
35
+ if ($category) {
36
+ $category = $conn->real_escape_string($category);
37
+ $query .= " AND category = '$category'";
38
+ }
39
+
40
+ $query .= " LIMIT $limit";
41
+
42
+ $result = $conn->query($query);
43
+
44
+ if (!$result) {
45
+ error_log("Database error in get_products_for_rag: " . $conn->error);
46
+ return [];
47
+ }
48
+
49
+ $products = [];
50
+ while ($row = $result->fetch_assoc()) {
51
+ $products[] = [
52
+ 'id' => (int)$row['id'],
53
+ 'name' => $row['name'],
54
+ 'description' => $row['description'],
55
+ 'price' => (float)$row['price'],
56
+ 'category' => $row['category'],
57
+ 'brand' => $row['brand'] ?? null,
58
+ 'image_path' => $row['image_path'] ?? null,
59
+ 'rating' => $row['rating'] ?? null,
60
+ ];
61
+ }
62
+
63
+ return $products;
64
+ }
65
+
66
+ /**
67
+ * Call the RAG API for product recommendations
68
+ *
69
+ * @param string $query User's natural language query
70
+ * @param array $products Array of product objects
71
+ * @param int $top_k Number of top recommendations to return
72
+ * @return array|false Response from RAG API or false on error
73
+ */
74
+ function call_rag_api($query, $products, $top_k = 5) {
75
+ if (!$query || empty($products)) {
76
+ error_log("RAG API error: Empty query or products");
77
+ return false;
78
+ }
79
+
80
+ // Prepare payload
81
+ $payload = [
82
+ 'query' => $query,
83
+ 'products' => $products,
84
+ 'top_k' => $top_k
85
+ ];
86
+
87
+ // Make POST request to RAG API
88
+ $ch = curl_init();
89
+ curl_setopt($ch, CURLOPT_URL, RAG_API_URL . RAG_ENDPOINT);
90
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
91
+ curl_setopt($ch, CURLOPT_POST, true);
92
+ curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
93
+ curl_setopt($ch, CURLOPT_HTTPHEADER, [
94
+ 'Content-Type: application/json',
95
+ 'Accept: application/json'
96
+ ]);
97
+ curl_setopt($ch, CURLOPT_TIMEOUT, 30);
98
+
99
+ $response = curl_exec($ch);
100
+ $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
101
+ $curl_error = curl_error($ch);
102
+ curl_close($ch);
103
+
104
+ // Handle curl errors
105
+ if ($curl_error) {
106
+ error_log("RAG API curl error: $curl_error");
107
+ return false;
108
+ }
109
+
110
+ // Handle HTTP errors
111
+ if ($http_code !== 200) {
112
+ error_log("RAG API HTTP error: $http_code - $response");
113
+ return false;
114
+ }
115
+
116
+ // Parse and return response
117
+ $data = json_decode($response, true);
118
+ if (!$data) {
119
+ error_log("RAG API response parse error: " . json_last_error_msg());
120
+ return false;
121
+ }
122
+
123
+ return $data;
124
+ }
125
+
126
+ /**
127
+ * Check if RAG API is healthy
128
+ *
129
+ * @return bool True if API is responsive
130
+ */
131
+ function check_rag_health() {
132
+ $ch = curl_init();
133
+ curl_setopt($ch, CURLOPT_URL, RAG_API_URL . '/health');
134
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
135
+ curl_setopt($ch, CURLOPT_TIMEOUT, 5);
136
+
137
+ curl_exec($ch);
138
+ $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
139
+ curl_close($ch);
140
+
141
+ return $http_code === 200;
142
+ }
143
+
144
+ /**
145
+ * Main function: Get AI recommendations for user query
146
+ * This is the primary function to call from your chat endpoint
147
+ *
148
+ * @param mysqli $conn Database connection
149
+ * @param string $user_query User's question/query
150
+ * @param string $category Optional product category to search in
151
+ * @return array Response containing message and products
152
+ */
153
+ function get_rag_recommendations($conn, $user_query, $category = null) {
154
+ // Check RAG API health
155
+ if (!check_rag_health()) {
156
+ error_log("RAG API is not available");
157
+ return [
158
+ 'success' => false,
159
+ 'message' => 'Recommendation service temporarily unavailable',
160
+ 'products' => []
161
+ ];
162
+ }
163
+
164
+ // Fetch products from database
165
+ $products = get_products_for_rag($conn, $category, 50);
166
+
167
+ if (empty($products)) {
168
+ return [
169
+ 'success' => false,
170
+ 'message' => 'No products available for recommendation',
171
+ 'products' => []
172
+ ];
173
+ }
174
+
175
+ // Call RAG API
176
+ $rag_response = call_rag_api($user_query, $products, 5);
177
+
178
+ if (!$rag_response) {
179
+ error_log("Failed to get RAG recommendations");
180
+ return [
181
+ 'success' => false,
182
+ 'message' => 'Failed to generate recommendations',
183
+ 'products' => []
184
+ ];
185
+ }
186
+
187
+ return [
188
+ 'success' => true,
189
+ 'message' => $rag_response['message'] ?? 'No message',
190
+ 'products' => $rag_response['products'] ?? [],
191
+ 'timestamp' => $rag_response['timestamp'] ?? date('c')
192
+ ];
193
+ }
194
+
195
+ // ── Example Usage (if this file is called directly) ──────────────────────────
196
+
197
+ if ($_SERVER['REQUEST_METHOD'] === 'POST' && php_sapi_name() !== 'cli') {
198
+ header('Content-Type: application/json');
199
+
200
+ $input = json_decode(file_get_contents('php://input'), true);
201
+ $query = $input['query'] ?? '';
202
+
203
+ if (!$query) {
204
+ http_response_code(400);
205
+ echo json_encode(['error' => 'Query is required']);
206
+ exit;
207
+ }
208
+
209
+ // Include your database connection
210
+ // require_once 'db.php';
211
+
212
+ // Uncomment below when integrated with your system:
213
+ // $result = get_rag_recommendations($conn, $query);
214
+ // echo json_encode($result);
215
+
216
+ // For now, return example response
217
+ echo json_encode([
218
+ 'success' => true,
219
+ 'message' => 'RAG API integration ready. Call get_rag_recommendations($conn, $query) from your chat handler.',
220
+ 'products' => []
221
+ ]);
222
+ }
223
+ ?>
requirements.txt ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ setuptools>=70.0.0
2
+ wheel
3
+ fastapi==0.104.1
4
+ uvicorn==0.24.0
5
+ python-dotenv==1.0.0
6
+ openai>=1.0.0
7
+ pydantic>=2.10.0
8
+ pydantic-settings>=2.3.0
9
+ requests==2.31.0
10
+ pymysql==1.1.0
11
+ numpy>=2.4.0
12
+ langchain>=0.1.0
13
+ langchain-community>=0.0.10
14
+ faiss-cpu>=1.13.0
15
+ sentence-transformers>=2.2.2
16
+ python-multipart==0.0.6
17
+ cors==1.0.1
18
+ python-json-logger>=2.0.0
start_rag.sh ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ # RAG API Quick Start Script
4
+ # Automatically sets up and starts the RAG API server
5
+
6
+ set -e
7
+
8
+ PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
9
+ cd "$PROJECT_DIR"
10
+
11
+ echo "================================"
12
+ echo " RAG API Quick Start"
13
+ echo "================================"
14
+ echo ""
15
+
16
+ # Check if Python is installed
17
+ if ! command -v python3 &> /dev/null; then
18
+ echo "❌ Python3 is not installed. Please install Python 3.8+"
19
+ exit 1
20
+ fi
21
+
22
+ echo "βœ“ Python3 found: $(python3 --version)"
23
+ echo ""
24
+
25
+ # Create virtual environment if it doesn't exist
26
+ if [ ! -d "venv" ]; then
27
+ echo "πŸ“¦ Creating virtual environment..."
28
+ python3 -m venv venv
29
+ echo "βœ“ Virtual environment created"
30
+ else
31
+ echo "βœ“ Virtual environment already exists"
32
+ fi
33
+
34
+ echo ""
35
+ echo "πŸ”§ Activating virtual environment..."
36
+ source venv/bin/activate
37
+
38
+ echo "βœ“ Virtual environment activated"
39
+ echo ""
40
+
41
+ # Check if requirements are installed
42
+ echo "πŸ“š Installing/updating requirements..."
43
+ pip install -q -r requirements.txt
44
+ echo "βœ“ Requirements installed"
45
+
46
+ echo ""
47
+ echo "βš™οΈ Checking environment variables..."
48
+
49
+ # Check if .env exists
50
+ if [ ! -f ".env" ]; then
51
+ echo "❌ .env file not found!"
52
+ echo ""
53
+ echo "Please create .env with:"
54
+ echo " NVIDIA_API_KEY=nvapi-your-key-here"
55
+ echo " NVIDIA_MODEL=mistralai/mistral-medium-3.5-128b"
56
+ echo " RAG_API_URL=http://localhost:8004"
57
+ echo ""
58
+ echo " Get NVIDIA API key from: https://build.nvidia.com/"
59
+ echo ""
60
+ echo " 1. Copy template: cp .env.example .env"
61
+ echo " 2. Edit .env with your NVIDIA API key"
62
+ echo " 3. Run this script again"
63
+ exit 1
64
+ else
65
+ echo "βœ“ .env file found"
66
+ fi
67
+
68
+ # Check if NVIDIA_API_KEY is set
69
+ if ! grep -q "NVIDIA_API_KEY=" .env || grep "NVIDIA_API_KEY=nvapi-" .env | grep -q "your-key"; then
70
+ echo "❌ NVIDIA_API_KEY not configured in .env"
71
+ echo ""
72
+ echo "Please edit .env and add your actual NVIDIA API key:"
73
+ echo " nano .env"
74
+ echo ""
75
+ echo "Get key from: https://build.nvidia.com/"
76
+ exit 1
77
+ fi
78
+
79
+ echo "βœ“ Environment configured"
80
+ echo ""
81
+
82
+ # Final checks
83
+ echo "πŸ§ͺ Running health checks..."
84
+ python3 -c "
85
+ from sentence_transformers import SentenceTransformer
86
+ print('βœ“ Sentence-Transformers OK')
87
+ " || {
88
+ echo "❌ Failed to import sentence-transformers"
89
+ exit 1
90
+ }
91
+
92
+ python3 -c "
93
+ import faiss
94
+ print('βœ“ FAISS OK')
95
+ " || {
96
+ echo "❌ Failed to import FAISS"
97
+ exit 1
98
+ }
99
+
100
+ echo ""
101
+ echo "================================"
102
+ echo " βœ… All checks passed!"
103
+ echo "================================"
104
+ echo ""
105
+ echo "πŸš€ Starting RAG API Server..."
106
+ echo ""
107
+ echo " Server will run on: http://0.0.0.0:8004"
108
+ echo " API Docs: http://localhost:8004/docs"
109
+ echo ""
110
+ echo " Press Ctrl+C to stop the server"
111
+ echo ""
112
+
113
+ # Start the server
114
+ python3 rag_api.py