Spaces:
Running
Running
Binayak Panigrahi commited on
Commit Β·
f0f26c7
1
Parent(s): 3984aaf
Add application file
Browse files- api.py +408 -0
- index.html +554 -0
- rag_api.py +313 -0
- rag_integration.php +223 -0
- requirements.txt +18 -0
- 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
|