diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..8f45822f --- /dev/null +++ b/.dockerignore @@ -0,0 +1,12 @@ +.env +.env.* +venv/ +seed/ +.venv/ +__pycache__/ +*.pyc +*.pyo +*.pyd +.git +.gitignore +app/service-account.json \ No newline at end of file diff --git a/.env b/.env index a75eff72..eee1bcd8 100644 --- a/.env +++ b/.env @@ -1,2 +1,5 @@ GOOGLE_PROJECT_ID=sylvan-deck-387207 -GOOGLE_APPLICATION_CREDENTIALS=C:\Users\rithv\OneDrive\Desktop\decision_engine_project\service-account.json \ No newline at end of file +GOOGLE_APPLICATION_CREDENTIALS=C:\Users\rithv\OneDrive\Desktop\decision_engine_project\app\service-account.json +DATABASE_URL = "postgresql://postgres:postgres@localhost:5432/decision_engine" +GOOGLE_SEARCH_API_KEY = AIzaSyBrqZPVbX8-HukUQEccLXwJ3MF6ZbG5cwc +GOOGLE_SEARCH_CX = 02d66a7fa55f7490c \ No newline at end of file diff --git a/.gitignore b/.gitignore index 5ddee301..c245931a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,32 @@ +# Virtual environments venv/ seed/ +.venv/ + +# Environment & secrets .env +.env.* +app/service-account.json + +# Python cache __pycache__/ -*.pyc \ No newline at end of file +*.pyc +*.pyo +*.pyd + +# Build artifacts +dist/ +build/ +*.egg-info/ + +# IDE +.vscode/ +.idea/ + +# OS +.DS_Store +Thumbs.db + +# Docker volumes +pg_data/ +redis_data/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..0e4104e8 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,30 @@ +# Use Python 3.11 slim +FROM python:3.11-slim + +# Set working directory +WORKDIR /app + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + build-essential \ + libpq-dev \ + curl \ + && rm -rf /var/lib/apt/lists/* + +# Copy requirements first (for Docker layer caching) +COPY requirements.txt . + +# Install Python dependencies +RUN pip install --no-cache-dir -r requirements.txt + +# Download spaCy model +RUN python -m spacy download en_core_web_sm + +# Copy entire project +COPY . . + +# Expose FastAPI port +EXPOSE 8000 + +# Start the server +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/app/__pycache__/bootstrap.cpython-312.pyc b/app/__pycache__/bootstrap.cpython-312.pyc index 05ddbc97..74f87a35 100644 Binary files a/app/__pycache__/bootstrap.cpython-312.pyc and b/app/__pycache__/bootstrap.cpython-312.pyc differ diff --git a/app/__pycache__/db_schema.cpython-312.pyc b/app/__pycache__/db_schema.cpython-312.pyc index 41faa535..8b4e4856 100644 Binary files a/app/__pycache__/db_schema.cpython-312.pyc and b/app/__pycache__/db_schema.cpython-312.pyc differ diff --git a/app/__pycache__/embedding_cache.cpython-312.pyc b/app/__pycache__/embedding_cache.cpython-312.pyc index 92570fad..3a7d3ec3 100644 Binary files a/app/__pycache__/embedding_cache.cpython-312.pyc and b/app/__pycache__/embedding_cache.cpython-312.pyc differ diff --git a/app/__pycache__/main.cpython-312.pyc b/app/__pycache__/main.cpython-312.pyc index 3f635a60..4be0d943 100644 Binary files a/app/__pycache__/main.cpython-312.pyc and b/app/__pycache__/main.cpython-312.pyc differ diff --git a/app/__pycache__/normalizer.cpython-312.pyc b/app/__pycache__/normalizer.cpython-312.pyc index 6ea4104e..6c9653ac 100644 Binary files a/app/__pycache__/normalizer.cpython-312.pyc and b/app/__pycache__/normalizer.cpython-312.pyc differ diff --git a/app/__pycache__/semantic_cache.cpython-312.pyc b/app/__pycache__/semantic_cache.cpython-312.pyc index eb0aff12..a376fe9b 100644 Binary files a/app/__pycache__/semantic_cache.cpython-312.pyc and b/app/__pycache__/semantic_cache.cpython-312.pyc differ diff --git a/app/__pycache__/vertex_client.cpython-312.pyc b/app/__pycache__/vertex_client.cpython-312.pyc index 82d9a23d..cc9cf100 100644 Binary files a/app/__pycache__/vertex_client.cpython-312.pyc and b/app/__pycache__/vertex_client.cpython-312.pyc differ diff --git a/app/analyze_logs.py b/app/analyze_logs.py new file mode 100644 index 00000000..4423c752 --- /dev/null +++ b/app/analyze_logs.py @@ -0,0 +1,64 @@ +import json +from collections import defaultdict + +LOG_FILE = "logs/app.log" + +total_requests = 0 +total_latency = 0 +query_count = defaultdict(int) +slow_requests = [] + +with open(LOG_FILE, "r") as f: + for line in f: + try: + log = json.loads(line) + + total_requests += 1 + latency = log.get("latency_ms", 0) + query = log.get("query", "") + + total_latency += latency + query_count[query] += 1 + + if latency > 1000: # slow threshold + slow_requests.append((query, latency)) + + except: + continue + +# Results +print("\nš BASIC METRICS") +print("=" * 30) + +if total_requests > 0: + print(f"Total Requests: {total_requests}") + print(f"Avg Latency: {total_latency // total_requests} ms") + +print("\nš„ Top Queries:") +for q, count in sorted(query_count.items(), key=lambda x: x[1], reverse=True)[:5]: + print(f"{q} ā {count} times") + +print("\nā ļø Slow Requests (>1000ms):") +for q, lat in slow_requests[:5]: + print(f"{q} ā {lat} ms") + +cache_hits = 0 + +if log.get("latency_ms", 0) < 500: + cache_hits += 1 + +print(f"\nā” Fast Requests (<500ms): {cache_hits}") + +hit_count = 0 + +if log.get("cache_status") in ["strong_hit", "refined_hit"]: + hit_count += 1 + +hit_rate = (hit_count / total_requests) * 100 + +fallback_count = 0 + +if log.get("cache_status") == "miss": + fallback_count += 1 + +fallback_rate = (fallback_count / total_requests) * 100 \ No newline at end of file diff --git a/app/bootstrap.py b/app/bootstrap.py index e5f13dea..3f8fbed5 100644 --- a/app/bootstrap.py +++ b/app/bootstrap.py @@ -7,12 +7,11 @@ from sentence_transformers import SentenceTransformer import requests as http_requests import google.auth import google.auth.transport.requests -import os from app.vertex_client import get_access_token load_dotenv() -DATABASE_URL = "postgresql://postgres:postgres@localhost:5432/decision_engine" +DATABASE_URL = os.getenv("DATABASE_URL") engine = create_engine(DATABASE_URL) model = SentenceTransformer("all-MiniLM-L6-v2") @@ -42,31 +41,41 @@ def clean_json_response(raw: str) -> str: raise ValueError("No JSON object found in response") -def validate_schema(data: dict) -> dict: - """ - Validate and clean Gemini response. - - Only keep valid categories - - Only keep string attributes - - Remove empty categories - - Cap at 5 attributes per category - """ +def validate_schema(data: dict, query: str) -> dict: cleaned = {} + query_words = set(query.lower().split()) + + # Define topic-aware category relevance + IRRELEVANT_COMBOS = { + "shoes": ["processor", "ram", "gpu", "cpu", "battery", "charging"], + "food": ["processor", "gpu", "engine", "torque", "horsepower"], + "college": ["torque", "engine", "gpu", "charging speed"], + } + + # Get blocked terms for this query + blocked = [] + for topic, terms in IRRELEVANT_COMBOS.items(): + if topic in query.lower(): + blocked.extend(terms) + for category, attributes in data.items(): - # Normalize category name cat = category.strip().title() - - # Skip invalid categories if cat not in VALID_CATEGORIES: print(f"Skipping unknown category: {cat}") continue - # Only keep string attributes - attrs = [a.strip() for a in attributes if isinstance(a, str) and a.strip()] - - # Cap at 5 per category - attrs = attrs[:6] + attrs = [] + for a in attributes: + if not isinstance(a, str) or not a.strip(): + continue + # ā Reject attributes containing blocked terms + a_lower = a.lower() + if any(b in a_lower for b in blocked): + print(f"ā Rejected irrelevant attribute: {a}") + continue + attrs.append(a.strip()) - # Skip empty categories + attrs = attrs[:12] if attrs: cleaned[cat] = attrs @@ -77,57 +86,75 @@ def validate_schema(data: dict) -> dict: def call_gemini(query: str) -> dict: - - prompt = f"""You are a world-class decision analysis expert. -Task: Generate the MOST IMPORTANT and FREQUENTLY USED evaluation criteria for someone making a decision about: "{query}" +Task: Generate a COMPREHENSIVE list of evaluation criteria for: "{query}" -Rules: -- Only include criteria that are HIGHLY RELEVANT to "{query}" -- Prioritize criteria that people MOST COMMONLY consider for this topic -- Each attribute must be SPECIFIC and MEASURABLE, not generic -- Order attributes by importance (most important first) -- Use concise names (2-5 words max per attribute) +MANDATORY RULES: +- Select 6-8 categories from the allowed list +- Generate EXACTLY 8-10 attributes per category ā this is mandatory +- Each attribute must be SPECIFIC and MEASURABLE for "{query}" +- First 3 attributes in EVERY category must be the MOST SEARCHED specs +- For tech products: always start with Processor, RAM, Battery, Display, Camera +- For vehicles: always start with Engine, Mileage, Price, Safety +- Order strictly by: most searched ā most compared ā most reviewed +- Use concise names (2-5 words max) +- DO NOT generate less than 8 attributes per category -Output format: Pure JSON only. No markdown. No explanation. -Use ONLY these category keys: Performance, Financial, Risk, Maintenance, Benefits, Time, Requirements, Scalability, Alternatives, Usability, Security, Reliability, Support, Sustainability +Allowed category keys: +Performance, Financial, Risk, Maintenance, Benefits, Time, Requirements, +Scalability, Alternatives, Usability, Security, Reliability, Support, Sustainability -Example for "buying a car": -{{"Performance":["Engine Power","Top Speed","0-100 Acceleration","Fuel Efficiency"],"Financial":["Purchase Price","Insurance Cost","Resale Value","Running Cost"],"Maintenance":["Service Interval","Spare Parts Availability","Warranty Period"]}} +Example of CORRECT format with enough attributes: +{{"Performance":["Engine Power","Torque Output","Top Speed","0-100 kmph Time","Fuel Efficiency","Gear Smoothness","Braking Distance","Tyre Grip","Suspension Quality","NVH Levels"],"Financial":["Ex-showroom Price","On-road Price","EMI Options","Insurance Cost","Fuel Cost Monthly","Resale Value","Maintenance Cost","Road Tax","Accessories Cost","Total Ownership Cost"],"Reliability":["Engine Reliability","Electrical Issues","Common Problems","Long Term Durability","Brand Track Record","Owner Satisfaction","Recall History","Service Quality","Spare Parts Life","Warranty Claims"]}} Now generate for: "{query}" -Return ONLY the JSON object.""" +Return ONLY valid JSON. No markdown. No explanation. Minimum 8 attributes per category.""" url = f"https://{LOCATION}-aiplatform.googleapis.com/v1/projects/{PROJECT_ID}/locations/{LOCATION}/publishers/google/models/gemini-2.5-flash-lite:generateContent" for attempt in range(3): try: - res = http_requests.post(url, - headers={ - "Authorization": f"Bearer {get_access_token()}", - "Content-Type": "application/json"}, - json={ - "contents": [{"role" : "user","parts": [{"text": prompt}]}], - "generationConfig": {"temperature": 0.2, "maxOutputTokens": 1024}}) - - # Handle rate limit with exponential backoff + res = http_requests.post( + url, + headers={ + "Authorization": f"Bearer {get_access_token()}", + "Content-Type": "application/json" + }, + json={ + "contents": [{"role": "user", "parts": [{"text": prompt}]}], + "generationConfig": { + "temperature": 0.1, # lower = more accurate, less random + "maxOutputTokens": 1024 + } + }, + timeout=30 + ) + if res.status_code == 429: - wait = 2 ** attempt # 1s, 2s, 4s - print(f" Rate limited, waiting {wait}s... (attempt {attempt + 1})") + wait = 2 ** attempt + print(f"Rate limited, waiting {wait}s... (attempt {attempt + 1})") time.sleep(wait) continue - - res.raise_for_status() + + print("Status Code:", res.status_code) + if res.status_code != 200: + print("ā ERROR RESPONSE:") + print(res.text) + raise RuntimeError("Vertex API failed") + + data_json = res.json() + print("ā RAW RESPONSE:", str(data_json)[:500]) + raw = res.json()["candidates"][0]["content"]["parts"][0]["text"] - clean = clean_json_response(raw) data = json.loads(clean) - validated = validate_schema(data) + validated = validate_schema(data,query) + print(f"Gemini generated {sum(len(v) for v in validated.values())} attributes across {len(validated)} categories") return validated except (json.JSONDecodeError, ValueError) as e: - print(f" Attempt {attempt + 1} failed: {e}") + print(f"Attempt {attempt + 1} failed: {e}") if attempt == 2: raise RuntimeError(f"Gemini failed after 3 attempts: {e}") @@ -135,8 +162,6 @@ Return ONLY the JSON object.""" def bootstrap_domain(query: str): - - # Retry up to 3 times if Gemini returns bad JSON data = None for attempt in range(3): try: @@ -148,9 +173,32 @@ def bootstrap_domain(query: str): if attempt == 2: raise RuntimeError(f"Gemini failed after 3 attempts: {e}") - with engine.begin() as conn: - domain_embedding = model.encode(query).tolist() + if not data: + raise RuntimeError("No data generated") + + # ā Quality gate ā reject low quality bootstraps + total_attrs = sum(len(v) for v in data.values()) + if total_attrs < 10: + raise ValueError(f"Quality gate failed: only {total_attrs} attributes generated") + # ā Duplicate domain detection ā check similarity before storing + model_local = SentenceTransformer("all-MiniLM-L6-v2") + domain_embedding = model_local.encode(query).tolist() + + with engine.begin() as conn: + # Check if very similar domain already exists + existing = conn.execute(text(""" + SELECT name, embedding <-> CAST(:emb AS vector) AS distance + FROM domains + ORDER BY distance + LIMIT 1 + """), {"emb": str(domain_embedding)}).fetchone() + + if existing and existing.distance < 0.15: + print(f"ā ļø Similar domain already exists: '{existing.name}' (distance: {existing.distance:.3f}) ā skipping bootstrap") + return # ā Don't store duplicate + + # Store domain domain_id = conn.execute(text(""" INSERT INTO domains (name, embedding) VALUES (:n, CAST(:e AS vector)) @@ -167,11 +215,11 @@ def bootstrap_domain(query: str): """), {"d": domain_id, "g": group}).scalar() for attr in attrs: - emb = model.encode(attr).tolist() + emb = model_local.encode(attr).tolist() conn.execute(text(""" INSERT INTO attributes (group_id, name, embedding) VALUES (:gid, :name, CAST(:emb AS vector)) ON CONFLICT (group_id, name) DO NOTHING """), {"gid": group_id, "name": attr, "emb": str(emb)}) - print(f"Domain bootstrapped: {query}") \ No newline at end of file + print(f"ā Domain bootstrapped: {query} ({total_attrs} attributes)") \ No newline at end of file diff --git a/app/check_db.py b/app/check_db.py index 4d0eb7d1..7ed30636 100644 --- a/app/check_db.py +++ b/app/check_db.py @@ -1,10 +1,12 @@ from sqlalchemy import create_engine, text +import os try: from app.db_schema import ensure_schema except ModuleNotFoundError: from db_schema import ensure_schema -engine = create_engine("postgresql://postgres:postgres@localhost:5432/decision_engine") +DATABASE_URL = os.getenv("DATABASE_URL") +engine = create_engine(DATABASE_URL) with engine.connect() as conn: ensure_schema(conn) diff --git a/app/db_schema.py b/app/db_schema.py index 3f2ff112..05cf8157 100644 --- a/app/db_schema.py +++ b/app/db_schema.py @@ -1,9 +1,6 @@ from sqlalchemy import text from sqlalchemy.exc import SQLAlchemyError - - - def ensure_schema(engine) -> bool: with engine.connect().execution_options(isolation_level="AUTOCOMMIT") as conn: conn.execute(text("CREATE EXTENSION IF NOT EXISTS vector")) @@ -41,5 +38,24 @@ def ensure_schema(engine) -> bool: ) """)) conn.execute(text("ALTER TABLE attributes DROP CONSTRAINT IF EXISTS attributes_name_key")) + + conn.execute(text(""" + CREATE TABLE IF NOT EXISTS attribute_feedback ( + id SERIAL PRIMARY KEY, + query TEXT NOT NULL, + attribute_name TEXT NOT NULL, + domain TEXT, + click_count INTEGER DEFAULT 1, + last_clicked TIMESTAMP DEFAULT NOW(), + UNIQUE(query, attribute_name) + ) + """)) + + conn.execute(text(""" + ALTER TABLE domains + ADD COLUMN IF NOT EXISTS quality_score FLOAT DEFAULT 1.0, + ADD COLUMN IF NOT EXISTS created_at TIMESTAMP DEFAULT NOW(), + ADD COLUMN IF NOT EXISTS query_count INTEGER DEFAULT 0 + """)) return True \ No newline at end of file diff --git a/app/embedding_cache.py b/app/embedding_cache.py index 437705c7..ba288e6a 100644 --- a/app/embedding_cache.py +++ b/app/embedding_cache.py @@ -11,7 +11,7 @@ def get_or_encode(query: str, model) -> list: otherwise encodes with model and caches it. Always call this with the NORMALIZED query. """ - key = f"emb:{query}" + key = f"emb:v1:{query}" # Check Redis first cached = redis_client.get(key) diff --git a/app/insert_test.py b/app/insert_test.py new file mode 100644 index 00000000..db93c632 --- /dev/null +++ b/app/insert_test.py @@ -0,0 +1,20 @@ +from sqlalchemy import create_engine, text +from sentence_transformers import SentenceTransformer +import os + +DATABASE_URL = os.getenv("DATABASE_URL") +engine = create_engine(DATABASE_URL) + +model = SentenceTransformer("all-MiniLM-L6-v2") + +name = "Range" +embedding = model.encode(name).tolist() + +with engine.connect() as conn: + conn.execute(text(""" + INSERT INTO attributes (name, embedding) + VALUES (:name, :embedding) + """), {"name": name, "embedding": embedding}) + conn.commit() + +print("Inserted successfully") diff --git a/app/log_cleanup.py b/app/log_cleanup.py new file mode 100644 index 00000000..0f01cd7e --- /dev/null +++ b/app/log_cleanup.py @@ -0,0 +1,15 @@ +import os +import time + +LOG_FILE = "logs/app.log" +MAX_SIZE_MB = 50 # rotate if too big + +def cleanup_logs(): + if not os.path.exists(LOG_FILE): + return + + size_mb = os.path.getsize(LOG_FILE) / (1024 * 1024) + + if size_mb > MAX_SIZE_MB: + os.rename(LOG_FILE, f"logs/app_{int(time.time())}.log") + open(LOG_FILE, "w").close() \ No newline at end of file diff --git a/app/logger.py b/app/logger.py new file mode 100644 index 00000000..9c7368d2 --- /dev/null +++ b/app/logger.py @@ -0,0 +1,20 @@ +import os +import json +from datetime import datetime + +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +LOG_DIR = os.path.join(BASE_DIR, "logs") +LOG_FILE = os.path.join(LOG_DIR, "app.log") + +def log_event(data: dict): + try: + os.makedirs(LOG_DIR, exist_ok=True) + + data["timestamp"] = datetime.utcnow().isoformat() + + with open(LOG_FILE, "a") as f: + f.write(json.dumps(data) + "\n") + + except Exception as e: + print(f"ā Logging failed: {e}") + \ No newline at end of file diff --git a/app/main.py b/app/main.py index 58bacd90..781136b7 100644 --- a/app/main.py +++ b/app/main.py @@ -15,7 +15,12 @@ import requests as http_requests import os import re import time +import json as json_module +import hashlib +import redis as redis_lib from app.vertex_client import get_access_token +from app.logger import log_event +from fastapi import Request load_dotenv() @@ -29,17 +34,37 @@ app.add_middleware( allow_headers=["*"], ) -DATABASE_URL = "postgresql://postgres:postgres@localhost:5432/decision_engine" +DATABASE_URL = os.getenv("DATABASE_URL") engine = create_engine(DATABASE_URL) model = SentenceTransformer("all-MiniLM-L6-v2") GOOGLE_PROJECT_ID = "sylvan-deck-387207" LOCATION = "us-central1" +GOOGLE_SEARCH_API_KEY = os.getenv("GOOGLE_SEARCH_API_KEY") +GOOGLE_SEARCH_CX = os.getenv("GOOGLE_SEARCH_CX") + +# Gemini URL +GEMINI_URL = f"https://{LOCATION}-aiplatform.googleapis.com/v1/projects/{GOOGLE_PROJECT_ID}/locations/{LOCATION}/publishers/google/models/gemini-2.5-flash-lite:generateContent" # āā Startup āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā ensure_schema(engine) create_index_if_not_exists() -print("ā System ready") +print("System ready") + + +# āā Redis helper āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā +def get_redis(): + try: + r = redis_lib.Redis(host='localhost', port=6379, decode_responses=True) + r.ping() + return r + except Exception: + return None + +def get_live_cache_key(query: str) -> str: + word_count = len(query.strip().split()) + prefix = "live:full" if word_count >= 3 else "live:short" + return f"{prefix}:{hashlib.md5(query.lower().strip().encode()).hexdigest()}" # āā Async cache writer āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā @@ -57,10 +82,9 @@ def is_gibberish(text: str) -> bool: words = text.lower().split() if not words: return True - gibberish_count = 0 for word in words: - if len(word) <= 3: # allow short words: ev, ai, bmw, suv + if len(word) <= 3: continue if re.search(r'[^aeiou]{6,}', word): gibberish_count += 1 @@ -72,97 +96,654 @@ def is_gibberish(text: str) -> bool: if re.search(r'[a-z]\d{3,}[a-z]|[0-9]{4,}[a-z]', word): gibberish_count += 1 continue - return gibberish_count > len(words) / 2 +# āā Middleware āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā +@app.middleware("http") +async def log_requests(request, call_next): + start_time = time.time() + response = await call_next(request) + latency = int((time.time() - start_time) * 1000) + cache_status = getattr(request.state, "cache_status", "missing") + + try: + query = request.query_params.get("query", "") + category = request.query_params.get("category", "general") + log_event({ + "path": request.url.path, + "query": query, + "latency_ms": latency, + "status_code": response.status_code, + "category": category, + "cache_status": cache_status + }) + print("CACHE:", cache_status) + except Exception as e: + print(f"Logging middleware error: {e}") + + print("MIDDLEWARE sees:", getattr(request.state, "cache_status", "NOT_SET")) + return response + + +# āā Fast Gemini call āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā +def call_gemini_fast(query: str, limit: int = 15) -> list: + prompt = f"""List {limit} specific evaluation criteria for: "{query}" +Rules: +- Specific to "{query}" only ā no competitors +- 2-5 words each +- Most important first + +Return ONLY a JSON array. Example: ["Battery Life", "On Road Price", "Fuel Efficiency"] +Generate for: "{query}" """ + + for attempt in range(2): + try: + res = http_requests.post( + GEMINI_URL, + headers={ + "Authorization": f"Bearer {get_access_token()}", + "Content-Type": "application/json" + }, + json={ + "contents": [{"role": "user", "parts": [{"text": prompt}]}], + "generationConfig": { + "temperature": 0.1, + "maxOutputTokens": 250 # small = fast + } + }, + timeout=6 + ) + if res.status_code == 429: + time.sleep(2 ** attempt) + continue + res.raise_for_status() + raw = res.json()["candidates"][0]["content"]["parts"][0]["text"] + match = re.search(r'\[.*\]', raw, re.DOTALL) + if match: + suggestions = json_module.loads(match.group(0)) + print(f"ā Fast Gemini: {len(suggestions)} suggestions for '{query}'") + return suggestions + except http_requests.exceptions.Timeout: + print(f"ā° Gemini timeout attempt {attempt+1}") + except Exception as e: + print(f"ā ļø Gemini attempt {attempt+1}: {type(e).__name__}") + time.sleep(1) + return [] + + +def call_gemini_more(query: str, existing: list) -> list: + """Second background call ā 25 more suggestions, different ones""" + existing_str = ", ".join(existing[:15]) # send first 15 so Gemini avoids them + prompt = f"""List 25 more specific evaluation criteria for: "{query}" +Rules: +- Specific to "{query}" only +- 2-5 words each +- Do NOT repeat any of these: {existing_str} +- Different aspects not yet covered +- Most important first + +Return ONLY a JSON array. +Generate for: "{query}" """ + + try: + res = http_requests.post( + GEMINI_URL, + headers={ + "Authorization": f"Bearer {get_access_token()}", + "Content-Type": "application/json" + }, + json={ + "contents": [{"role": "user", "parts": [{"text": prompt}]}], + "generationConfig": { + "temperature": 0.3, + "maxOutputTokens": 600 + } + }, + timeout=15 + ) + res.raise_for_status() + raw = res.json()["candidates"][0]["content"]["parts"][0]["text"] + match = re.search(r'\[.*\]', raw, re.DOTALL) + if match: + more = json_module.loads(match.group(0)) + # filter out any duplicates + existing_lower = [e.lower() for e in existing] + return [s for s in more if s.lower() not in existing_lower] + except Exception as e: + print(f"ā ļø call_gemini_more failed: {type(e).__name__}") + return [] + +# āā Background store in Redis + DB āāāāāāāāāāāāāāāāāāā +def store_in_background(query: str, suggestions: list, r=None, cache_key: str = None): + def _store(): + if r and cache_key: + try: + ttl = 86400 if len(query.split()) >= 3 else 1800 + r.setex(cache_key, ttl, json_module.dumps(suggestions)) + print(f"š¾ Redis cached: '{query}' ({len(suggestions)} items)") + except Exception: + pass + try: + bootstrap_domain(query) + print(f"ā DB bootstrapped: '{query}'") + if r and cache_key: + try: + r.delete(cache_key) + print(f"šļø Redis cleared ā DB now serves '{query}'") + except Exception: + pass + except Exception as e: + print(f"ā ļø Bootstrap failed: {type(e).__name__}") + + threading.Thread(target=_store, daemon=True).start() + + +# āā Main get suggestions function āāāāāāāāāāāāāāāāāāāā +def get_suggestions_fast(query: str, limit: int = 15, existing: list = None) -> dict: + existing = existing or [] + + # āā L1: Redis cache āāāāāāāāāāāāāāāāāāāāāāāāāāāāāā + r = get_redis() + cache_key = get_live_cache_key(query) + + if r: + try: + cached = r.get(cache_key) + if cached: + all_suggestions = json_module.loads(cached) + new_only = [s for s in all_suggestions if s not in existing] + print(f"ā” Redis HIT: '{query}' ā {len(new_only)} available") + return {"suggestions": new_only[:limit], "cache": "redis_hit"} + except Exception: + pass + + # āā L2: DB (pgvector) āāāāāāāāāāāāāāāāāāāāāāāāāāāā + try: + normalized = normalize_query(query.strip()) + embedding = get_or_encode(normalized, model) + emb_param = str(embedding) + + with engine.begin() as conn: + domain_row = conn.execute(text(""" + SELECT id, name, + embedding <-> CAST(:emb AS vector) AS distance + FROM domains ORDER BY distance LIMIT 1 + """), {"emb": emb_param}).fetchone() + + if domain_row and domain_row.distance <= 0.8: + results = conn.execute(text(""" + SELECT a.name, + (1 - (a.embedding <-> CAST(:emb AS vector))) * 0.7 + + LEAST(COALESCE(af.click_count, 0), 100) * 0.003 AS score + FROM attributes a + JOIN dimension_groups g ON a.group_id = g.id + LEFT JOIN attribute_feedback af + ON af.attribute_name = a.name + AND af.domain = :domain_name + WHERE g.domain_id = :domain_id + ORDER BY score DESC + LIMIT :limit + """), {"emb": emb_param, "domain_id": domain_row.id, + "domain_name": domain_row.name, "limit": limit * 2}) + + db_suggestions = [row[0] for row in results] + new_only = [s for s in db_suggestions if s not in existing] + + if new_only: + print(f"ā” DB HIT: '{query}' ā {len(new_only)} suggestions") + if r: + try: + r.setex(cache_key, 3600, json_module.dumps(db_suggestions)) + except Exception: + pass + return {"suggestions": new_only[:limit], "cache": "db_hit", + "domain": domain_row.name} + except Exception as e: + print(f"ā ļø DB check failed: {type(e).__name__}") + + # āā L3: Gemini fast (new query) āāāāāāāāāāāāāāāāāā + print(f"š New query ā calling Gemini fast: '{query}'") + suggestions = call_gemini_fast(query, limit=15) + + if suggestions: + # Background: fetch 25 more + store everything in Redis + def fetch_more_and_store(): + more = call_gemini_more(query, suggestions) + all_suggestions = suggestions + more + print(f"ā Total suggestions after background fetch: {len(all_suggestions)}") + store_in_background(query, all_suggestions, r, cache_key) + + threading.Thread(target=fetch_more_and_store, daemon=True).start() + + new_only = [s for s in suggestions if s not in existing] + return {"suggestions": new_only[:limit], "cache": "gemini_fast"} + + return {"suggestions": [], "cache": "gemini_failed"} + + +# āā Category suggestions via Gemini āāāāāāāāāāāāāāāāāā +def generate_category_suggestions(query: str, category: str, existing: list, limit: int = 15): + category_context = { + "shopping": "buying options, best price, where to buy, deals, discounts, EMI, cashback, online vs offline, delivery, sellers", + "images": "exterior design, interior photos, color options, visual features, styling, aesthetics, dimensions look, photo gallery", + "videos": "video reviews, test drive videos, owner reviews, comparison videos, YouTube channels, expert opinions, unboxing", + "news": "latest news, recent updates, new model announcement, price change, upcoming launch, recalls, awards won", + "places": "best dealers, showrooms near me, service centers, test drive locations, authorized dealers, city availability" + } + context = category_context.get(category, "general evaluation") + prompt = f"""The user is searching for "{query}" in {category.upper()} category. +Generate {limit} specific keyword suggestions related to "{query}" for the {category} context. +Focus on: {context} +Rules: +- Each suggestion must be 2-5 words +- Must be directly related to "{query}" in {category} context +- Order by most searched/relevant first +Return ONLY a JSON array of strings. No explanation.""" + + for attempt in range(3): + try: + res = http_requests.post( + GEMINI_URL, + headers={ + "Authorization": f"Bearer {get_access_token()}", + "Content-Type": "application/json" + }, + json={ + "contents": [{"role": "user", "parts": [{"text": prompt}]}], + "generationConfig": {"temperature": 0.4, "maxOutputTokens": 512} + }, + timeout=30 + ) + if res.status_code == 429: + time.sleep(2 ** attempt) + continue + res.raise_for_status() + raw = res.json()["candidates"][0]["content"]["parts"][0]["text"] + match = re.search(r'\[.*\]', raw, re.DOTALL) + if match: + suggestions = json_module.loads(match.group(0)) + return {"suggestions": suggestions[:limit], "cache": "category_ai"} + return {"suggestions": [], "cache": "parse_error"} + except http_requests.exceptions.ConnectionError as e: + print(f"ā ļø Connection error attempt {attempt + 1}: {type(e).__name__}") + if attempt < 2: + time.sleep(2) + continue + return {"suggestions": [], "cache": "connection_error"} + except Exception as e: + print(f"ā Category suggestions failed: {type(e).__name__}") + return {"suggestions": [], "cache": "error"} + return {"suggestions": [], "cache": "failed"} + + +# āā serve_from_db āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā +def serve_from_db(conn, domain_row, emb_param, limit, offset): + results = conn.execute(text(""" + SELECT a.name, + (1 - (a.embedding <-> CAST(:emb AS vector))) * 0.7 + + LEAST(COALESCE(af.click_count, 0), 100) * 0.003 AS score + FROM attributes a + JOIN dimension_groups g ON a.group_id = g.id + LEFT JOIN attribute_feedback af ON af.attribute_name = a.name + WHERE g.domain_id = :domain_id + ORDER BY score DESC + LIMIT :limit OFFSET :offset + """), {"emb": emb_param, "domain_id": domain_row.id, + "limit": limit, "offset": offset}) + return [r[0] for r in results] + + # āā /suggest endpoint āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā @app.get("/suggest") -def suggest(query: str, offset: int = 0, limit: int = 15): +def suggest(request: Request, query: str, offset: int = 0, limit: int = 15, category: str = "general"): + + def build_response(data, status): + request.state.cache_status = status + data["cache"] = status + return data + + start = time.time() + if len(query.strip()) < 2: - return {"suggestions": [], "cache": "skip"} + return build_response({"suggestions": []}, "skip") - # Block gibberish server-side if is_gibberish(query.strip()): - return {"suggestions": [], "cache": "gibberish"} + return build_response({"suggestions": []}, "gibberish") - normalized = normalize_query(query.strip()) - print(f"š Normalized: '{query}' ā '{normalized}'") - - embedding = get_or_encode(normalized, model) - word_count = len(query.strip().split()) + # Category ā AI directly + if category != "general": + result = generate_category_suggestions(query.strip(), category, [], limit) + return build_response(result, "category_ai") - # Use semantic cache only for full queries (3+ words) on first page - if word_count >= 3 and offset == 0: - cached = get_semantic_cache(embedding, domain=normalized) - if cached: - print(f"ā Semantic cache HIT") - return {"suggestions": cached[offset:offset + limit], "cache": "semantic_hit"} - - emb_param = str(embedding) - with engine.begin() as conn: - domain_row = conn.execute(text(""" - SELECT id, name, - embedding <-> CAST(:emb AS vector) AS distance - FROM domains - ORDER BY distance - LIMIT 1 - """), {"emb": emb_param}).fetchone() - - if domain_row is None or domain_row.distance > 0.8: - if not is_gibberish(query): - try: - bootstrap_domain(query) - print(f"ā Bootstrapped: {query}") - except Exception as e: - print(f"ā ļø Bootstrap failed: {e}") - - domain_row = conn.execute(text(""" - SELECT id, name - FROM domains - ORDER BY embedding <-> CAST(:emb AS vector) - LIMIT 1 - """), {"emb": emb_param}).fetchone() - - if domain_row is None: - return {"suggestions": [], "cache": "no_domain"} - # ā continues below to fetch attributes - else: - return {"suggestions": [], "cache": "no_domain"} - - results = conn.execute(text(""" - SELECT a.name, - 1 - (a.embedding <-> CAST(:emb AS vector)) AS score - FROM attributes a - JOIN dimension_groups g ON a.group_id = g.id - WHERE g.domain_id = :domain_id - ORDER BY score DESC - LIMIT :limit OFFSET :offset - """), {"emb": emb_param, "domain_id": domain_row.id, - "limit": limit, "offset": offset}) - - suggestions = [r[0] for r in results] - domain_name = domain_row.name - - # Deduplicate - seen = set() - ranked = [] - for name in suggestions: - if name.lower() not in seen: - seen.add(name.lower()) - ranked.append(name) - - # Cache only full queries on first page - if word_count >= 3 and offset == 0: - write_cache_async(normalized, embedding, ranked, domain=domain_name) - - return {"suggestions": ranked, "cache": "miss", "domain": domain_name} + normalized = normalize_query(query.strip()) + print(f"Normalized: '{query}' ā '{normalized}'") + + # āā Semantic cache check āāāāāāāāāāāāāāāāāāāāāāāāāā + try: + embedding = get_or_encode(normalized, model) + emb_param = str(embedding) + + with engine.begin() as conn: + domain_row = conn.execute(text(""" + SELECT id, name, + embedding <-> CAST(:emb AS vector) AS distance + FROM domains ORDER BY distance LIMIT 1 + """), {"emb": emb_param}).fetchone() + + if domain_row and domain_row.distance <= 0.8: + domain_name = domain_row.name + + if offset == 0: + cached = get_semantic_cache(embedding, domain=domain_name) + if cached: + distance = cached["distance"] + cached_query = cached["query"] + suggestions = cached["suggestions"] + print(f"Cache found ā distance: {distance:.4f}, query: {cached_query}") + + def is_relevant(q, cq): + q_words = set(q.lower().split()) + c_words = set(cq.lower().split()) + overlap = len(q_words & c_words) / max(len(q_words), 1) + return overlap > 0.5 + + if distance < 0.05 and is_relevant(normalized, cached_query): + print("ā Strong semantic cache HIT") + return build_response( + {"suggestions": suggestions[:limit], "domain": domain_name}, + "strong_hit" + ) + elif distance < 0.1 and is_relevant(normalized, cached_query): + print("ā” Medium match ā refining") + fresh_results = conn.execute(text(""" + SELECT a.name, + 1 - (a.embedding <-> CAST(:emb AS vector)) AS score + FROM attributes a + JOIN dimension_groups g ON a.group_id = g.id + WHERE g.domain_id = :domain_id + ORDER BY score DESC + LIMIT :limit + """), {"emb": emb_param, "domain_id": domain_row.id, "limit": limit}) + fresh = [r[0] for r in fresh_results] + merged = [] + seen = set() + for item in suggestions + fresh: + if item.lower() not in seen: + seen.add(item.lower()) + merged.append(item) + return build_response( + {"suggestions": merged[:limit], "domain": domain_name}, + "refined_hit" + ) + else: + print("ā Cache ignored (low relevance)") + + results = conn.execute(text(""" + SELECT a.name, + (1 - (a.embedding <-> CAST(:emb AS vector))) * 0.7 + + LEAST(COALESCE(af.click_count, 0), 100) * 0.003 AS score + FROM attributes a + JOIN dimension_groups g ON a.group_id = g.id + LEFT JOIN attribute_feedback af + ON af.attribute_name = a.name + AND af.domain = :domain_name + WHERE g.domain_id = :domain_id + ORDER BY score DESC + LIMIT :limit OFFSET :offset + """), {"emb": emb_param, "domain_id": domain_row.id, + "domain_name": domain_name, "limit": limit, "offset": offset}) + + suggestions = [r[0] for r in results] + seen = set() + ranked = [] + for name in suggestions: + if name.lower() not in seen: + seen.add(name.lower()) + ranked.append(name) + + if ranked: + if offset == 0 and len(normalized.split()) >= 2: + write_cache_async(normalized, embedding, ranked, domain=domain_name) + print(f"ā” DB HIT: '{query}' ā {len(ranked)} suggestions in {int((time.time()-start)*1000)}ms") + return build_response( + {"suggestions": ranked, "domain": domain_name}, + "db_hit" + ) + + except Exception as e: + print(f"ā ļø DB/cache check failed: {type(e).__name__}") + + # āā Fast Gemini fallback āāāāāāāāāāāāāāāāāāāāāāāāāā + print(f"š Fast Gemini for: '{query}'") + result = get_suggestions_fast(query, limit=40) + return build_response(result, result.get("cache", "gemini_fast")) + + +# āā /suggest/more endpoint āāāāāāāāāāāāāāāāāāāāāāāāāāāā +@app.get("/suggest/more") +def suggest_more(query: str, category: str = "general", existing: str = ""): + existing_list = [e.strip() for e in existing.split(",") if e.strip()] + + if category == "general": + r = get_redis() + if r: + try: + cache_key = get_live_cache_key(query) + cached = r.get(cache_key) + if cached: + all_suggestions = json_module.loads(cached) + new_only = [s for s in all_suggestions if s not in existing_list] + print(f"ā” Instant more: {len(new_only)} from cache") + return {"suggestions": new_only[:12], "cache": "live_hit"} + except Exception: + pass + + result = get_suggestions_fast(query, limit=12, existing=existing_list) + return result + + category_context = { + "shopping": "focus on buying, pricing, deals, offers, sellers, delivery, payment options", + "images": "focus on visual aspects, design, appearance, colors, aesthetics, photos", + "videos": "focus on reviews, test drives, comparisons, tutorials, unboxing, demos", + "news": "focus on latest updates, recent changes, announcements, trends, events", + "places": "focus on locations, dealers, service centers, showrooms, nearby options", + } + context = category_context.get(category, "general evaluation") + prompt = f"""Generate 12 more keyword suggestions for "{query}" in {category.upper()} context. +Focus on: {context} +Do NOT repeat: {', '.join(existing_list) if existing_list else 'none'} +Each suggestion 2-5 words max. +Return ONLY a JSON array.""" + + try: + res = http_requests.post( + GEMINI_URL, + headers={"Authorization": f"Bearer {get_access_token()}", "Content-Type": "application/json"}, + json={"contents": [{"role": "user", "parts": [{"text": prompt}]}], + "generationConfig": {"temperature": 0.5, "maxOutputTokens": 300}}, + timeout=15) + res.raise_for_status() + raw = res.json()["candidates"][0]["content"]["parts"][0]["text"] + match = re.search(r'\[.*\]', raw, re.DOTALL) + if match: + suggestions = json_module.loads(match.group(0)) + return {"suggestions": [s for s in suggestions if s not in existing_list][:12], + "cache": "ai_generated"} + return {"suggestions": [], "cache": "parse_error"} + except Exception as e: + print(f"ā suggest/more failed: {type(e).__name__}") + return {"suggestions": [], "cache": "error"} + + +# āā /search/videos endpoint āāāāāāāāāāāāāāāāāāāāāāāāāāā +@app.get("/search/videos") +def search_videos(query: str): + if not GOOGLE_SEARCH_API_KEY: + return {"results": [], "error": "API key not configured"} + try: + url = "https://www.googleapis.com/youtube/v3/search" + params = { + "part": "snippet", + "q": query, + "type": "video", + "maxResults": 6, + "order": "relevance", + "key": GOOGLE_SEARCH_API_KEY + } + res = http_requests.get(url, params=params, timeout=10) + res.raise_for_status() + data = res.json() + results = [] + for item in data.get("items", []): + snippet = item["snippet"] + video_id = item["id"]["videoId"] + results.append({ + "title": snippet["title"], + "channel": snippet["channelTitle"], + "thumbnail": snippet["thumbnails"]["medium"]["url"], + "url": f"https://www.youtube.com/watch?v={video_id}", + "published": snippet["publishedAt"][:10], + "description": snippet.get("description", "")[:120] + }) + return {"results": results, "query": query} + except Exception as e: + print(f"ā YouTube search failed: {type(e).__name__}") + return {"results": [], "error": "Search unavailable. Please try again."} + + +# āā /search/shopping endpoint āāāāāāāāāāāāāāāāāāāāāāāāā +@app.get("/search/shopping") +def search_shopping(query: str): + if not GOOGLE_SEARCH_API_KEY or not GOOGLE_SEARCH_CX: + return {"results": [], "error": "Google Search API not configured"} + try: + url = "https://www.googleapis.com/customsearch/v1" + params = { + "q": f"{query} buy price", + "cx": GOOGLE_SEARCH_CX, + "key": GOOGLE_SEARCH_API_KEY, + "num": 6 + } + res = http_requests.get(url, params=params, timeout=10) + res.raise_for_status() + data = res.json() + results = [] + for item in data.get("items", []): + results.append({ + "title": item.get("title", ""), + "link": item.get("link", ""), + "snippet": item.get("snippet", "")[:150], + "source": item.get("displayLink", ""), + "image": item.get("pagemap", {}).get("cse_image", [{}])[0].get("src", "") + }) + return {"results": results, "query": query} + except Exception as e: + print(f"ā Shopping search failed: {type(e).__name__}") + return {"results": [], "error": "Search unavailable. Please try again."} + + +# āā /search/images endpoint āāāāāāāāāāāāāāāāāāāāāāāāāāā +@app.get("/search/images") +def search_images(query: str): + if not GOOGLE_SEARCH_API_KEY or not GOOGLE_SEARCH_CX: + return {"results": [], "error": "Google Search API not configured"} + try: + url = "https://www.googleapis.com/customsearch/v1" + params = { + "q": query, + "cx": GOOGLE_SEARCH_CX, + "key": GOOGLE_SEARCH_API_KEY, + "searchType": "image", + "num": 6, + "safe": "active" + } + res = http_requests.get(url, params=params, timeout=10) + res.raise_for_status() + data = res.json() + results = [] + for item in data.get("items", []): + results.append({ + "title": item.get("title", ""), + "link": item.get("link", ""), + "source": item.get("displayLink", ""), + "thumbnail": item.get("image", {}).get("thumbnailLink", ""), + "context_link": item.get("image", {}).get("contextLink", "") + }) + return {"results": results, "query": query} + except Exception as e: + print(f"ā Image search failed: {type(e).__name__}") + return {"results": [], "error": "Search unavailable. Please try again."} + + +# āā /search/news endpoint āāāāāāāāāāāāāāāāāāāāāāāāāāāāā +@app.get("/search/news") +def search_news(query: str): + if not GOOGLE_SEARCH_API_KEY or not GOOGLE_SEARCH_CX: + return {"results": [], "error": "Google Search API not configured"} + try: + url = "https://www.googleapis.com/customsearch/v1" + params = { + "q": f"{query} news 2025", + "cx": GOOGLE_SEARCH_CX, + "key": GOOGLE_SEARCH_API_KEY, + "num": 6, + "sort": "date" + } + res = http_requests.get(url, params=params, timeout=10) + res.raise_for_status() + data = res.json() + results = [] + for item in data.get("items", []): + results.append({ + "title": item.get("title", ""), + "link": item.get("link", ""), + "snippet": item.get("snippet", "")[:200], + "source": item.get("displayLink", ""), + "image": item.get("pagemap", {}).get("cse_image", [{}])[0].get("src", "") + }) + return {"results": results, "query": query} + except Exception as e: + print(f"ā News search failed: {type(e).__name__}") + return {"results": [], "error": "Search unavailable. Please try again."} + + +# āā /search/places endpoint āāāāāāāāāāāāāāāāāāāāāāāāāāā +@app.get("/search/places") +def search_places(query: str): + if not GOOGLE_SEARCH_API_KEY or not GOOGLE_SEARCH_CX: + return {"results": [], "error": "Google Search API not configured"} + try: + url = "https://www.googleapis.com/customsearch/v1" + params = { + "q": f"{query} near me dealers showroom location", + "cx": GOOGLE_SEARCH_CX, + "key": GOOGLE_SEARCH_API_KEY, + "num": 6 + } + res = http_requests.get(url, params=params, timeout=10) + res.raise_for_status() + data = res.json() + results = [] + for item in data.get("items", []): + results.append({ + "title": item.get("title", ""), + "link": item.get("link", ""), + "snippet": item.get("snippet", "")[:200], + "source": item.get("displayLink", ""), + "image": item.get("pagemap", {}).get("cse_image", [{}])[0].get("src", "") + }) + return {"results": results, "query": query} + except Exception as e: + print(f"ā Places search failed: {type(e).__name__}") + return {"results": [], "error": "Search unavailable. Please try again."} # āā /generate endpoint āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā class GenerateRequest(BaseModel): query: str selected_attributes: list[str] + category: str = "general" chat_history: list[dict] = [] @@ -181,46 +762,48 @@ def generate(request: GenerateRequest): attributes = ", ".join(request.selected_attributes) if request.selected_attributes else "general evaluation" - prompt = f"""{history_text}USER QUESTION: "{request.query}" -EVALUATION CRITERIA SELECTED: {attributes} - -You are an expert advisor. The user has specifically asked about "{request.query}". -Your job is to answer the user's question "{request.query}" and analyze it through each of the selected criteria. + category_focus = { + "shopping": "Focus on buying options, best prices, deals, where to buy, payment plans, EMI options.", + "images": "Focus on visual design, appearance, color options, exterior/interior aesthetics.", + "videos": "Focus on video reviews, test drives, comparisons, what reviewers say.", + "news": "Focus on latest news, recent updates, upcoming changes, current trends.", + "places": "Focus on best places to buy, dealers, showrooms, service centers.", + "general": "" + }.get(request.category, "") -Answer the question "{request.query}" directly first in 2-3 sentences. -Then for each criterion in [{attributes}], explain how it applies specifically to "{request.query}". + prompt = f"""{history_text}USER QUESTION: "{request.query}" +CATEGORY: {request.category.upper()} +{category_focus} +EVALUATION CRITERIA: {attributes} -Format your response as: +You are an expert advisor. Answer "{request.query}" directly and analyze through each selected criterion. +Format: ## About: {request.query} -[Direct answer to the user's question in 2-3 sentences] +[Direct answer in 2-3 sentences] --- -[For each selected criterion:] **[Criterion Name]** -- How this applies to "{request.query}" -- Specific facts, numbers, or data +- How it applies to "{request.query}" +- Specific facts or numbers - Recommendation --- ## Bottom Line -[2-3 sentence summary answering: should the user go with "{request.query}"? What should they prioritize?] +[2-3 sentence summary] -STRICT RULES: +RULES: - Every sentence must be about "{request.query}" specifically -- Never give generic advice not related to "{request.query}" -- If unsure about a fact, say "verify on official website" - Use real numbers where confident -- Total response under 400 words""" - - url = f"https://{LOCATION}-aiplatform.googleapis.com/v1/projects/{GOOGLE_PROJECT_ID}/locations/{LOCATION}/publishers/google/models/gemini-2.5-flash-lite:generateContent" +- If unsure say "verify on official website" +- Total under 400 words""" print(f"š Calling Vertex AI for: {request.query}") for attempt in range(3): try: res = http_requests.post( - url, + GEMINI_URL, headers={ "Authorization": f"Bearer {get_access_token()}", "Content-Type": "application/json" @@ -231,25 +814,47 @@ STRICT RULES: }, timeout=30 ) - if res.status_code == 429: wait = 2 ** attempt print(f"ā³ Rate limited, waiting {wait}s...") time.sleep(wait) continue - print(f"ā Vertex response: {res.status_code}") res.raise_for_status() answer = res.json()["candidates"][0]["content"]["parts"][0]["text"] return {"answer": answer} - except http_requests.exceptions.Timeout: print(f"ā° Timeout on attempt {attempt + 1}") if attempt == 2: return {"answer": "Request timed out. Please try again."} time.sleep(2) except Exception as e: - print(f"ā Error: {e}") + print(f"ā Error: {type(e).__name__}") return {"answer": "Error getting response. Please try again."} - return {"answer": "Could not get response after 3 attempts. Please try again."} \ No newline at end of file + return {"answer": "Could not get response after 3 attempts. Please try again."} + + +# āā /feedback endpoint āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā +class FeedbackRequest(BaseModel): + query: str + selected_chips: list[str] + domain: str = "" + + +@app.post("/feedback") +def feedback(request: FeedbackRequest): + try: + with engine.begin() as conn: + for chip in request.selected_chips: + conn.execute(text(""" + INSERT INTO attribute_feedback (query, attribute_name, domain, click_count) + VALUES (:query, :attr, :domain, 1) + ON CONFLICT (query, attribute_name) + DO UPDATE SET click_count = attribute_feedback.click_count + 1, + last_clicked = NOW() + """), {"query": request.query, "attr": chip, "domain": request.domain}) + return {"status": "ok"} + except Exception as e: + print(f"Feedback write failed: {type(e).__name__}") + return {"status": "error"} \ No newline at end of file diff --git a/app/semantic_cache.py b/app/semantic_cache.py index 5060dc7f..354acd63 100644 --- a/app/semantic_cache.py +++ b/app/semantic_cache.py @@ -81,7 +81,10 @@ def get_semantic_cache(embedding: list, domain: str = None): if distance < SIMILARITY_THRESHOLD: print(f"Semantic cache HIT (distance: {distance:.4f}, domain: {domain})") - return json.loads(top.suggestions) + return { + "query": top.query.decode() if isinstance(top.query, bytes) else top.query, + "distance": distance, + "suggestions": json.loads(top.suggestions)} print(f"Semantic cache MISS (distance: {distance:.4f})") return None diff --git a/app/service-account.json b/app/service-account.json deleted file mode 100644 index 26b79df9..00000000 --- a/app/service-account.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "type": "service_account", - "project_id": "sylvan-deck-387207", - "private_key_id": "dfcde7ef895c5677b3db9af908ff253155a03c21", - "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCdWs2WvTssenBd\nJkRBDeNK6K5TYiS8vB53LNeaMJNF6x76SR6Ns4STNNopDzgGrNinVpBRTy8k8COc\nwzEAiDvdoBkATu4KM5+fYizRXTP47c4Yn6QrLam85atdU3s89AQA+rXsXGON+8LO\n7s7qmB69jaOAYRB6UrT3LFjeQB/O6Tsw2T6vmoHtL+mtcLWyRdWsk/WEz2RZmvmG\n9/79LdQQS8Hqj81RssZ3FdBkTYVL9vEoXo9DmuTlLlY2F0q2aM0cHYPTHTVCKKdh\nNZt5ZsOwTdkxczM/fZpHtSCyErZR9JGeG1VGUSQbN5GN9OavsFvF09ERFtbQ60Y0\naaOOCQjFAgMBAAECggEAGW3WMZkNGggDZppLh3PWGoH1whXnN/Tyu3GsugdFlZQE\noo/0dxPexedRpjcGZ9XBAXH0yp8QUFjaeHf20E4z1oIL6EfZIh7rmddExOTaBE1x\n8/rAjhXIC3XWNrPKA7SvfPUHN1ZK5GQePFDNcY350co09Qc6oXoCMrug9PHJ8ibv\n/cA5J4ktrP2BIqa4YhCnMOjavgt3Fd1UjeJ4lnE8guN2Ke2Z2hNgdS9heo92htSK\nq+vkHs/HXEmtTA3Tg1wOyd6lXkprNqGNzkQtlGjei0I6V7E/snAj2abOH2c5U5ai\nq0hapOD8v3QVCBcxObYXd9WqLTFoFMSXfesAc7aF1QKBgQDENRuNNvnYmCP90bPb\nGPMLlbg20YAeAuIHrV0Jm7i0yk/JTyeG1uUiRjFZFhuTWJeEqe1NYYDvPjEe8vaQ\ndkAhSh35tLlpED2rnD+H1uhWl3730P6TfWI/v++U8aTpFD8Jfrzos1quF/RwTl/k\nP3oW4ZkEK832bXluBHmRvNmtIwKBgQDNTqTY5pNWfhXaTly/UISf8d/7vF5ziAvd\n7XMQbj+bj16W5YXRcwO5ZmNHFbUxrwnSDRzsDrlwUiV4rTzTQ3sRcFvw+EbNygKp\nNDkPPtkkjiRWDet9awuQDQ+7zKzrgh2C2uopQDuTg/SJZieBURJMMW4dunyxijlA\nt25bR/bU9wKBgETqTmoUVD9SeNnPDTg4lC2OgeynOzPPWWrO5q3YR1Eg+lM//Scs\nVcDrHKwoyri/VkDfmp0iUTI3CvPO7PGixzWqHcs2QiV38eFT+TCSOHsprQwIGVLe\nqGKx3MnY8k53sQh5voqRbJlXiqDjtmSqMwzUYnWHmUkj/JG6+qRIy8A3AoGBALxO\nW6iNo6n7L3Px1+OpqFtDcBrmpQL2T1wYRCdX14OItktU7a4z/cB5BqnWYUDWuP0u\nBc8FmlRJJBFRY66qACD4m3ujXN23YUVsnsE69dMvhGhhkBKSsiJHuJyZmCjSSNsS\nix+WyI3+w7WaOrXDdDLqS4N83o3Ap86R7+hNUzn1AoGBALHsqD4/w0shYZjXkvZb\nmh8Z4P7R/QKraYZjSZJqY6dh/EXfOXWBOlQVvABzoiVShBl3MibNhGHdFEAoFzrI\nFVq9/jHzJGGtyH3JxrZp0hOHt9Bd50ZuqCwCCFiJDShlFbdWbDvC+guEw/oJEZDB\nQTaufmfSE/AiQcDSc9PbnKPL\n-----END PRIVATE KEY-----\n", - "client_email": "sylvan-deck-387207@appspot.gserviceaccount.com", - "client_id": "113050669736679470974", - "auth_uri": "https://accounts.google.com/o/oauth2/auth", - "token_uri": "https://oauth2.googleapis.com/token", - "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", - "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/sylvan-deck-387207%40appspot.gserviceaccount.com", - "universe_domain": "googleapis.com" -} diff --git a/app/vertex_client.py b/app/vertex_client.py index 93e38fb9..2327d026 100644 --- a/app/vertex_client.py +++ b/app/vertex_client.py @@ -1,13 +1,22 @@ # app/vertex_client.py from google.oauth2 import service_account import google.auth.transport.requests - -SERVICE_ACCOUNT_FILE = r"C:\Users\rithv\OneDrive\Desktop\decision_engine_project\app\service-account.json" +import os def get_access_token(): + # Try environment variable first + creds_path = os.getenv("GOOGLE_APPLICATION_CREDENTIALS") + + # Fallback to absolute path + if not creds_path or not os.path.exists(creds_path): + creds_path = r"C:\Users\rithv\OneDrive\Desktop\decision_engine_project\app\service-account.json" + + if not os.path.exists(creds_path): + raise FileNotFoundError(f"Service account file not found at: {creds_path}") + credentials = service_account.Credentials.from_service_account_file( - SERVICE_ACCOUNT_FILE, + creds_path, scopes=["https://www.googleapis.com/auth/cloud-platform"] ) credentials.refresh(google.auth.transport.requests.Request()) - return credentials.token \ No newline at end of file + return credentials.token \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..1081a965 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,46 @@ +version: "3.9" + +services: + + postgres: + image: ankane/pgvector # ā matches your existing container + container_name: decision_pg + restart: always + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: decision_engine + ports: + - "5432:5432" + volumes: + - pg_data:/var/lib/postgresql/data + + redis: + image: redis/redis-stack-server:latest + container_name: decision_redis + restart: always + ports: + - "6379:6379" + volumes: + - redis_data:/data + + backend: + build: . + container_name: decision_backend + restart: always + ports: + - "8000:8000" + environment: + DATABASE_URL: postgresql://postgres:postgres@postgres:5432/decision_engine + GOOGLE_PROJECT_ID: sylvan-deck-387207 + GOOGLE_APPLICATION_CREDENTIALS: /app/app/service-account.json + REDIS_URL: redis://redis:6379 + volumes: + - ./app/service-account.json:/app/app/service-account.json:ro + depends_on: + - postgres + - redis + +volumes: + pg_data: + redis_data: \ No newline at end of file diff --git a/index.html b/index.html index 39b256ae..261f4684 100644 --- a/index.html +++ b/index.html @@ -2,84 +2,200 @@
-')
- .replace(/\n/g, '
')
- .replace(/^/, '
').replace(/$/, '
'); + .replace(/## (.*)/g,'')
+ .replace(/\n/g,'
')
+ .replace(/^/,'
').replace(/$/,'
'); } function escHtml(t) { diff --git a/logs/app.log b/logs/app.log new file mode 100644 index 00000000..83a7f0b3 --- /dev/null +++ b/logs/app.log @@ -0,0 +1,317 @@ +{"path": "/suggest", "query": "test", "latency_ms": 8392, "status_code": 200, "category": "general", "timestamp": "2026-03-24T07:41:32.768471"} +{"path": "/suggest", "query": "test", "latency_ms": 253, "status_code": 200, "category": "general", "timestamp": "2026-03-24T09:23:27.882782"} +{"path": "/suggest", "query": "copp", "latency_ms": 9445, "status_code": 200, "category": "general", "timestamp": "2026-03-24T09:39:12.819615"} +{"path": "/suggest", "query": "copper bottels", "latency_ms": 9909, "status_code": 200, "category": "general", "timestamp": "2026-03-24T09:39:16.239737"} +{"path": "/suggest", "query": "copper bott", "latency_ms": 10646, "status_code": 200, "category": "general", "timestamp": "2026-03-24T09:39:18.126377"} +{"path": "/suggest", "query": "copper bottles", "latency_ms": 9745, "status_code": 200, "category": "general", "timestamp": "2026-03-24T09:39:18.258745"} +{"path": "/suggest", "query": "copper bottles", "latency_ms": 30, "status_code": 200, "category": "general", "timestamp": "2026-03-24T09:39:36.052442"} +{"path": "/search/shopping", "query": "copper bottles", "latency_ms": 2, "status_code": 200, "category": "general", "timestamp": "2026-03-24T09:40:15.918487"} +{"path": "/suggest", "query": "copper bottles", "latency_ms": 3217, "status_code": 200, "category": "shopping", "timestamp": "2026-03-24T09:40:19.434479"} +{"path": "/suggest", "query": "copper bottles", "latency_ms": 4228, "status_code": 200, "category": "shopping", "timestamp": "2026-03-24T09:40:41.432054"} +{"path": "/search/images", "query": "copper bottles", "latency_ms": 2, "status_code": 200, "category": "general", "timestamp": "2026-03-24T09:40:44.661391"} +{"path": "/suggest", "query": "copper bottles", "latency_ms": 4261, "status_code": 200, "category": "images", "timestamp": "2026-03-24T09:40:49.225755"} +{"path": "/suggest", "query": "cmf buds", "latency_ms": 13295, "status_code": 200, "category": "general", "timestamp": "2026-03-24T09:54:38.648363"} +{"path": "/suggest", "query": "cmf buds 3 pro", "latency_ms": 11145, "status_code": 200, "category": "general", "timestamp": "2026-03-24T09:54:39.793944"} +{"path": "/suggest", "query": "iot sensors", "latency_ms": 9305, "status_code": 200, "category": "general", "timestamp": "2026-03-24T10:00:43.085947"} +{"path": "/suggest", "query": "dairy", "latency_ms": 13541, "status_code": 200, "category": "general", "timestamp": "2026-03-24T10:13:31.686838"} +{"path": "/suggest", "query": "dairy milk chocolates", "latency_ms": 10339, "status_code": 200, "category": "general", "timestamp": "2026-03-24T10:13:32.559251"} +{"path": "/suggest", "query": "hot chcocla", "latency_ms": 13455, "status_code": 200, "category": "general", "cache_status": "unknown", "timestamp": "2026-03-24T10:17:15.591739"} +{"path": "/suggest", "query": "hot chocolates", "latency_ms": 11486, "status_code": 200, "category": "general", "cache_status": "unknown", "timestamp": "2026-03-24T10:17:17.196962"} +{"path": "/suggest", "query": "hot c", "latency_ms": 14026, "status_code": 200, "category": "general", "cache_status": "unknown", "timestamp": "2026-03-24T10:17:17.534570"} +{"path": "/suggest", "query": "hot chocolate", "latency_ms": 10758, "status_code": 200, "category": "general", "cache_status": "unknown", "timestamp": "2026-03-24T10:17:18.576700"} +{"path": "/suggest", "query": "honda unicorn", "latency_ms": 11937, "status_code": 200, "category": "general", "cache_status": "unknown", "timestamp": "2026-03-24T10:31:17.874156"} +{"path": "/suggest", "query": "honda unicorn", "latency_ms": 122, "status_code": 200, "category": "general", "cache_status": "unknown", "timestamp": "2026-03-24T10:31:33.652317"} +{"path": "/suggest", "query": "kellogs cornflakes", "latency_ms": 10662, "status_code": 200, "category": "general", "cache_status": "unknown", "timestamp": "2026-03-24T10:45:09.976742"} +{"path": "/suggest", "query": "kellogs cornflakes", "latency_ms": 138, "status_code": 200, "category": "general", "cache_status": "unknown", "timestamp": "2026-03-24T10:45:15.048033"} +{"path": "/suggest", "query": "kellogs cornflake", "latency_ms": 145, "status_code": 200, "category": "general", "cache_status": "unknown", "timestamp": "2026-03-24T10:46:58.172268"} +{"path": "/suggest", "query": "kellogs cornflak", "latency_ms": 201, "status_code": 200, "category": "general", "cache_status": "unknown", "timestamp": "2026-03-24T10:47:01.882495"} +{"path": "/suggest", "query": "kellogs cornfla", "latency_ms": 132, "status_code": 200, "category": "general", "cache_status": "unknown", "timestamp": "2026-03-24T10:47:03.089693"} +{"path": "/suggest", "query": "kellogs cornfl", "latency_ms": 124, "status_code": 200, "category": "general", "cache_status": "unknown", "timestamp": "2026-03-24T10:47:04.498375"} +{"path": "/suggest", "query": "kellogs", "latency_ms": 217, "status_code": 200, "category": "general", "cache_status": "unknown", "timestamp": "2026-03-24T10:47:08.437136"} +{"path": "/suggest", "query": "kellogs cornflakes", "latency_ms": 43, "status_code": 200, "category": "general", "cache_status": "unknown", "timestamp": "2026-03-24T10:47:15.633809"} +{"path": "/suggest", "query": "kellogs cornflakes", "latency_ms": 35, "status_code": 200, "category": "general", "cache_status": "unknown", "timestamp": "2026-03-24T10:47:17.922680"} +{"path": "/suggest", "query": "kellogs cornflakes", "latency_ms": 34, "status_code": 200, "category": "general", "cache_status": "unknown", "timestamp": "2026-03-24T10:47:19.355561"} +{"path": "/suggest", "query": "kellogs cornflakes", "latency_ms": 32, "status_code": 200, "category": "general", "cache_status": "unknown", "timestamp": "2026-03-24T10:47:20.965773"} +{"path": "/suggest", "query": "yoga", "latency_ms": 9861, "status_code": 200, "category": "general", "cache_status": "unknown", "timestamp": "2026-03-24T10:53:26.729801"} +{"path": "/suggest", "query": "yogabar museli", "latency_ms": 8080, "status_code": 200, "category": "general", "cache_status": "unknown", "timestamp": "2026-03-24T10:53:28.181778"} +{"path": "/suggest", "query": "yogabar muesli", "latency_ms": 49, "status_code": 200, "category": "general", "cache_status": "unknown", "timestamp": "2026-03-24T10:53:29.941432"} +{"path": "/suggest", "query": "yogabar muesli", "latency_ms": 9044, "status_code": 200, "category": "general", "cache_status": "unknown", "timestamp": "2026-03-24T10:53:34.666932"} +{"path": "/suggest", "query": "yogabar muesli", "latency_ms": 39, "status_code": 200, "category": "general", "cache_status": "unknown", "timestamp": "2026-03-24T10:53:36.841584"} +{"path": "/suggest", "query": "honda unicorn", "latency_ms": 10648, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-24T11:15:51.644457"} +{"path": "/suggest", "query": "blackand", "latency_ms": 3140, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-24T11:39:28.434300"} +{"path": "/suggest", "query": "blackandwhite whiskey", "latency_ms": 3883, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-24T11:39:31.620899"} +{"path": "/suggest", "query": "blackandwhite whiskey", "latency_ms": 20, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-24T11:39:44.752100"} +{"path": "/suggest/more", "query": "blackandwhite whiskey", "latency_ms": 2086, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-24T11:39:46.846080"} +{"path": "/suggest", "query": "iqoo n", "latency_ms": 4173, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-24T11:40:54.322251"} +{"path": "/suggest", "query": "iqoo neo 10", "latency_ms": 4155, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-24T11:40:55.738036"} +{"path": "/suggest", "query": "iphone 17", "latency_ms": 4011, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-24T11:41:10.653704"} +{"path": "/suggest", "query": "iphone 17pro max", "latency_ms": 2702, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-24T11:41:10.749465"} +{"path": "/suggest", "query": "iphone 17pro max", "latency_ms": 17, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-24T11:41:29.961441"} +{"path": "/suggest/more", "query": "iphone 17pro max", "latency_ms": 3604, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-24T11:41:33.573375"} +{"path": "/suggest", "query": "iphone 17", "latency_ms": 599, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-24T11:49:00.054777"} +{"path": "/suggest", "query": "iphone 17pro max", "latency_ms": 55, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-24T11:49:01.270320"} +{"path": "/suggest", "query": "honda unicorn", "latency_ms": 82, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-24T11:49:28.656041"} +{"path": "/suggest", "query": "honda", "latency_ms": 522, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-24T11:54:58.776240"} +{"path": "/suggest", "query": "honda unicorn", "latency_ms": 37, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-24T11:55:00.028301"} +{"path": "/suggest/more", "query": "honda unicorn", "latency_ms": 1944, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-24T11:55:07.376788"} +{"path": "/suggest/more", "query": "honda unicorn", "latency_ms": 3451, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-24T11:55:18.506256"} +{"path": "/suggest/more", "query": "honda unicorn", "latency_ms": 15908, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-24T11:55:42.265218"} +{"path": "/search/shopping", "query": "honda unicorn", "latency_ms": 1, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-24T11:56:50.346526"} +{"path": "/suggest", "query": "honda unicorn", "latency_ms": 2141, "status_code": 200, "category": "shopping", "cache_status": "missing", "timestamp": "2026-03-24T11:56:52.793321"} +{"path": "/search/images", "query": "honda unicorn", "latency_ms": 1, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-24T11:56:57.296811"} +{"path": "/suggest", "query": "honda unicorn", "latency_ms": 3637, "status_code": 200, "category": "images", "cache_status": "missing", "timestamp": "2026-03-24T11:57:01.235059"} +{"path": "/search/videos", "query": "honda unicorn", "latency_ms": 0, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-24T11:57:03.095838"} +{"path": "/suggest", "query": "honda unicorn", "latency_ms": 2091, "status_code": 200, "category": "videos", "cache_status": "missing", "timestamp": "2026-03-24T11:57:05.488950"} +{"path": "/search/news", "query": "honda unicorn", "latency_ms": 1, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-24T11:57:07.863378"} +{"path": "/suggest", "query": "honda unicorn", "latency_ms": 3370, "status_code": 200, "category": "news", "cache_status": "missing", "timestamp": "2026-03-24T11:57:11.531548"} +{"path": "/search/places", "query": "honda unicorn", "latency_ms": 1, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-24T11:57:15.120335"} +{"path": "/suggest", "query": "honda unicorn", "latency_ms": 3551, "status_code": 200, "category": "places", "cache_status": "missing", "timestamp": "2026-03-24T11:57:18.976096"} +{"path": "/generate", "query": "", "latency_ms": 0, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-24T11:57:25.591184"} +{"path": "/generate", "query": "", "latency_ms": 5116, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-24T11:57:30.714999"} +{"path": "/feedback", "query": "", "latency_ms": 0, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-24T11:57:30.721025"} +{"path": "/feedback", "query": "", "latency_ms": 73, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-24T11:57:30.799674"} +{"path": "/suggest", "query": "honda unicorn", "latency_ms": 26, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-24T11:58:41.779230"} +{"path": "/suggest", "query": "cb unicorn", "latency_ms": 834, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-25T05:54:48.031586"} +{"path": "/suggest/more", "query": "cb unicorn", "latency_ms": 2888, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-25T05:54:53.348580"} +{"path": "/suggest/more", "query": "cb unicorn", "latency_ms": 4128, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-25T05:55:05.127322"} +{"path": "/suggest", "query": "best pods under 10k", "latency_ms": 5947, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-25T06:28:27.202965"} +{"path": "/suggest", "query": "best pods under", "latency_ms": 7490, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-25T06:28:27.238368"} +{"path": "/suggest/more", "query": "best pods under 10k", "latency_ms": 4409, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-25T06:28:38.204072"} +{"path": "/suggest", "query": "bmw x7", "latency_ms": 4688, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-25T06:28:55.010697"} +{"path": "/suggest/more", "query": "bmw x7", "latency_ms": 3978, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-25T06:29:10.311858"} +{"path": "/suggest", "query": "best colleges in india for mba", "latency_ms": 5246, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-25T06:29:32.184825"} +{"path": "/suggest/more", "query": "best colleges in india for mba", "latency_ms": 4348, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-25T06:30:05.141143"} +{"path": "/suggest", "query": "how much the cost of ferari hurcan cost and tell me the all about it", "latency_ms": 5722, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-25T06:49:45.260892"} +{"path": "/suggest", "query": "how much the cost of ferrari hurcan cost and tell me the all about it", "latency_ms": 5516, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-25T06:50:05.731741"} +{"path": "/suggest/more", "query": "how much the cost of ferrari hurcan cost and tell me the all about it", "latency_ms": 4838, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-25T06:50:07.278390"} +{"path": "/suggest/more", "query": "how much the cost of ferrari hurcan cost and tell me the all about it", "latency_ms": 4701, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-25T06:50:35.951450"} +{"path": "/suggest", "query": "royal enfiled", "latency_ms": 5998, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-25T06:51:07.377766"} +{"path": "/suggest", "query": "royal enfiled himalayan", "latency_ms": 5373, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-25T06:51:14.535511"} +{"path": "/suggest", "query": "royal enfiled himalayan 450", "latency_ms": 4309, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-25T06:51:15.963887"} +{"path": "/suggest/more", "query": "royal enfiled himalayan 450", "latency_ms": 5249, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-25T06:51:18.844037"} +{"path": "/suggest/more", "query": "royal enfiled himalayan 450", "latency_ms": 5294, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-25T06:51:43.712836"} +{"path": "/suggest", "query": "royal enfiled himalayan 450 and also the ktm", "latency_ms": 758, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-25T06:52:50.697397"} +{"path": "/suggest", "query": "royal enfiled himalayan 450 and also the ktm adventure", "latency_ms": 307, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-25T06:52:53.909312"} +{"path": "/suggest/more", "query": "royal enfiled himalayan 450 and also the ktm adventure", "latency_ms": 5641, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-25T06:53:11.065759"} +{"path": "/suggest", "query": "royal enfiled himalayan 450 and also the ktm adventure. apart from this i also want the off roading car", "latency_ms": 4456, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-25T06:53:31.473835"} +{"path": "/suggest", "query": "royal enfiled himalayan 450 and also the ktm adventure. apart from this i also want the off roading car. which you suggest to take", "latency_ms": 368, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-25T06:53:50.945610"} +{"path": "/suggest", "query": "royal enfiled himalayan 450 and also the ktm adventure. apart from this i also want the off roading car. which you suggest to take in off road cars", "latency_ms": 325, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-25T06:53:55.204613"} +{"path": "/suggest/more", "query": "royal enfiled himalayan 450 and also the ktm adventure. apart from this i also want the off roading car. which you suggest to take in off road cars", "latency_ms": 5741, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-25T06:54:22.646397"} +{"path": "/suggest/more", "query": "royal enfiled himalayan 450 and also the ktm adventure. apart from this i also want the off roading car. which you suggest to take in off road cars", "latency_ms": 5242, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-25T06:54:40.668447"} +{"path": "/suggest/more", "query": "royal enfiled himalayan 450 and also the ktm adventure. apart from this i also want the off roading car. which you suggest to take in off road cars", "latency_ms": 3786, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-25T06:54:52.854731"} +{"path": "/search/shopping", "query": "royal enfiled himalayan 450 and also the ktm adventure. apart from this i also want the off roading car. which you suggest to take in off road cars", "latency_ms": 370, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-25T06:55:07.097639"} +{"path": "/suggest", "query": "royal enfiled himalayan 450 and also the ktm adventure. apart from this i also want the off roading car. which you suggest to take in off road cars", "latency_ms": 5224, "status_code": 200, "category": "shopping", "cache_status": "missing", "timestamp": "2026-03-25T06:55:12.311264"} +{"path": "/suggest", "query": "which would be the best off roading car in india", "latency_ms": 12343, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-25T10:09:55.763828"} +{"path": "/search/shopping", "query": "which would be the best off roading car in india", "latency_ms": 1407, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-25T10:10:44.427882"} +{"path": "/suggest", "query": "which would be the best off roading car in india", "latency_ms": 4581, "status_code": 200, "category": "shopping", "cache_status": "missing", "timestamp": "2026-03-25T10:10:47.901140"} +{"path": "/suggest/more", "query": "which would be the best off roading car in india", "latency_ms": 4154, "status_code": 200, "category": "shopping", "cache_status": "missing", "timestamp": "2026-03-25T10:10:55.174280"} +{"path": "/search/images", "query": "which would be the best off roading car in india", "latency_ms": 1308, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-25T10:11:17.388808"} +{"path": "/search/news", "query": "which would be the best off roading car in india", "latency_ms": 1366, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-25T10:11:20.087371"} +{"path": "/suggest", "query": "which would be the best off roading car in india", "latency_ms": 3764, "status_code": 200, "category": "images", "cache_status": "missing", "timestamp": "2026-03-25T10:11:20.144976"} +{"path": "/suggest", "query": "which would be the best off roading car in india", "latency_ms": 5228, "status_code": 200, "category": "news", "cache_status": "missing", "timestamp": "2026-03-25T10:11:24.252265"} +{"path": "/suggest", "query": "which would be the best off roading car in india", "latency_ms": 277, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-25T10:11:28.714448"} +{"path": "/suggest", "query": "amd ryzen 5", "latency_ms": 11595, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-25T10:22:13.498751"} +{"path": "/suggest", "query": "dairy milk chocolate", "latency_ms": 8283, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-25T10:42:56.359288"} +{"path": "/suggest", "query": "britannia", "latency_ms": 6875, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-25T11:17:55.743128"} +{"path": "/suggest", "query": "britannia timepass biscuits", "latency_ms": 5293, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-25T11:18:01.484124"} +{"path": "/suggest/more", "query": "britannia timepass biscuits", "latency_ms": 45, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-25T11:18:12.920280"} +{"path": "/suggest/more", "query": "britannia timepass biscuits", "latency_ms": 21, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-25T11:18:14.260805"} +{"path": "/suggest/more", "query": "britannia timepass biscuits", "latency_ms": 16, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-25T11:18:15.649810"} +{"path": "/suggest/more", "query": "britannia timepass biscuits", "latency_ms": 19, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-25T11:18:16.813562"} +{"path": "/suggest/more", "query": "britannia timepass biscuits", "latency_ms": 19, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-25T11:18:18.372159"} +{"path": "/search/shopping", "query": "britannia timepass biscuits", "latency_ms": 1366, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-25T11:18:31.081874"} +{"path": "/suggest", "query": "britannia timepass biscuits", "latency_ms": 3354, "status_code": 200, "category": "shopping", "cache_status": "missing", "timestamp": "2026-03-25T11:18:33.371404"} +{"path": "/generate", "query": "", "latency_ms": 4, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-25T11:18:41.030866"} +{"path": "/generate", "query": "", "latency_ms": 8866, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-25T11:18:49.906342"} +{"path": "/feedback", "query": "", "latency_ms": 0, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-25T11:18:49.916086"} +{"path": "/feedback", "query": "", "latency_ms": 107, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-25T11:18:50.032849"} +{"path": "/suggest", "query": "best ac under 50k", "latency_ms": 9807, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-25T11:23:54.496622"} +{"path": "/suggest/more", "query": "best ac under 50k", "latency_ms": 4630, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-25T11:24:25.388183"} +{"path": "/search/shopping", "query": "best ac under 50k", "latency_ms": 1458, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-25T11:24:33.490047"} +{"path": "/suggest", "query": "best ac under 50k", "latency_ms": 4919, "status_code": 200, "category": "shopping", "cache_status": "missing", "timestamp": "2026-03-25T11:24:37.257917"} +{"path": "/search/images", "query": "best ac under 50k", "latency_ms": 1333, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-25T11:24:44.953247"} +{"path": "/suggest", "query": "best ac under 50k", "latency_ms": 4425, "status_code": 200, "category": "images", "cache_status": "missing", "timestamp": "2026-03-25T11:24:48.356621"} +{"path": "/search/videos", "query": "best ac under 50k", "latency_ms": 1176, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-25T11:24:56.303835"} +{"path": "/suggest", "query": "best ac under 50k", "latency_ms": 5068, "status_code": 200, "category": "videos", "cache_status": "missing", "timestamp": "2026-03-25T11:25:00.499375"} +{"path": "/search/news", "query": "best ac under 50k", "latency_ms": 1254, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-25T11:25:06.233618"} +{"path": "/suggest", "query": "best ac under 50k", "latency_ms": 4534, "status_code": 200, "category": "news", "cache_status": "missing", "timestamp": "2026-03-25T11:25:09.827945"} +{"path": "/search/places", "query": "best ac under 50k", "latency_ms": 1046, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-25T11:25:19.869167"} +{"path": "/suggest", "query": "best ac under 50k", "latency_ms": 2886, "status_code": 200, "category": "places", "cache_status": "missing", "timestamp": "2026-03-25T11:25:22.012546"} +{"path": "/search/places", "query": "ap cm", "latency_ms": 1328, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-25T11:25:46.999661"} +{"path": "/suggest", "query": "ap cm", "latency_ms": 4711, "status_code": 200, "category": "places", "cache_status": "missing", "timestamp": "2026-03-25T11:25:50.380689"} +{"path": "/suggest", "query": "ap cm", "latency_ms": 6922, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-25T11:25:53.560551"} +{"path": "/suggest", "query": "ap cm name", "latency_ms": 334, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-25T11:26:16.317524"} +{"path": "/suggest", "query": "andhra pradesh cm", "latency_ms": 6546, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-25T11:26:47.909881"} +{"path": "/suggest", "query": "andhra pradesh cheif minister", "latency_ms": 6045, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-25T11:26:52.657588"} +{"path": "/suggest/more", "query": "andhra pradesh cheif minister", "latency_ms": 4955, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-25T11:26:55.501989"} +{"path": "/suggest", "query": "rolls royce", "latency_ms": 4978, "status_code": 200, "category": "general", "cache_status": "gemini_fast", "timestamp": "2026-03-25T11:35:37.486417"} +{"path": "/suggest", "query": "rolls royce cullinan", "latency_ms": 220, "status_code": 200, "category": "general", "cache_status": "db_hit", "timestamp": "2026-03-25T11:35:57.513871"} +{"path": "/suggest", "query": "porshce", "latency_ms": 3472, "status_code": 200, "category": "general", "cache_status": "gemini_fast", "timestamp": "2026-03-25T11:37:10.762971"} +{"path": "/suggest", "query": "porshce cayan", "latency_ms": 3859, "status_code": 200, "category": "general", "cache_status": "gemini_fast", "timestamp": "2026-03-25T11:37:13.852103"} +{"path": "/suggest/more", "query": "porshce cayan", "latency_ms": 20, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-25T11:37:26.311942"} +{"path": "/suggest/more", "query": "porshce cayan", "latency_ms": 135, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-03-25T11:37:28.138621"} +{"path": "/suggest", "query": "ai prompt composer engine", "latency_ms": 4464, "status_code": 200, "category": "general", "cache_status": "gemini_fast", "timestamp": "2026-04-01T10:02:58.329112"} +{"path": "/suggest", "query": "who is the best cheif minister upto now in telangana?", "latency_ms": 2560, "status_code": 200, "category": "general", "cache_status": "db_hit", "timestamp": "2026-04-01T10:06:48.019856"} +{"path": "/suggest", "query": "galaxy", "latency_ms": 4171, "status_code": 200, "category": "general", "cache_status": "gemini_fast", "timestamp": "2026-04-06T08:35:39.192180"} +{"path": "/suggest", "query": "galaxy fold s7", "latency_ms": 3458, "status_code": 200, "category": "general", "cache_status": "gemini_fast", "timestamp": "2026-04-06T08:35:42.945724"} +{"path": "/suggest", "query": "galaxy fold s", "latency_ms": 4593, "status_code": 200, "category": "general", "cache_status": "gemini_fast", "timestamp": "2026-04-06T08:35:43.001746"} +{"path": "/suggest/more", "query": "galaxy fold s7", "latency_ms": 147, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-04-06T08:36:20.252792"} +{"path": "/search/shopping", "query": "galaxy fold s7", "latency_ms": 577, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-04-06T08:39:33.819319"} +{"path": "/suggest", "query": "galaxy fold s7", "latency_ms": 4436, "status_code": 200, "category": "shopping", "cache_status": "category_ai", "timestamp": "2026-04-06T08:39:37.988895"} +{"path": "/search/news", "query": "galaxy fold s7", "latency_ms": 722, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-04-06T08:51:23.233737"} +{"path": "/suggest", "query": "galaxy fold s7", "latency_ms": 2885, "status_code": 200, "category": "news", "cache_status": "category_ai", "timestamp": "2026-04-06T08:51:25.671943"} +{"path": "/generate", "query": "", "latency_ms": 3, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-04-06T08:52:12.659955"} +{"path": "/generate", "query": "", "latency_ms": 8316, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-04-06T08:52:20.984192"} +{"path": "/feedback", "query": "", "latency_ms": 0, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-04-06T08:52:20.996423"} +{"path": "/feedback", "query": "", "latency_ms": 228, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-04-06T08:52:21.230130"} +{"path": "/generate", "query": "", "latency_ms": 0, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-04-06T08:59:57.660294"} +{"path": "/generate", "query": "", "latency_ms": 9129, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-04-06T09:00:06.804105"} +{"path": "/feedback", "query": "", "latency_ms": 0, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-04-06T09:00:06.812836"} +{"path": "/feedback", "query": "", "latency_ms": 58, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-04-06T09:00:06.876453"} +{"path": "/generate", "query": "", "latency_ms": 1, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-04-06T09:00:37.903845"} +{"path": "/generate", "query": "", "latency_ms": 7089, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-04-06T09:00:44.999990"} +{"path": "/feedback", "query": "", "latency_ms": 0, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-04-06T09:00:45.010119"} +{"path": "/feedback", "query": "", "latency_ms": 29, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-04-06T09:00:45.045139"} +{"path": "/search/news", "query": "poco f4 vs", "latency_ms": 2188, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-04-06T09:15:01.194224"} +{"path": "/suggest", "query": "poco f4 vs", "latency_ms": 7074, "status_code": 200, "category": "news", "cache_status": "category_ai", "timestamp": "2026-04-06T09:15:06.023929"} +{"path": "/search/news", "query": "poco f4 vs oneplus nord", "latency_ms": 1826, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-04-06T09:15:06.613033"} +{"path": "/suggest", "query": "poco f4 vs oneplus nord", "latency_ms": 5264, "status_code": 200, "category": "news", "cache_status": "category_ai", "timestamp": "2026-04-06T09:15:10.050812"} +{"path": "/suggest", "query": "samsung", "latency_ms": 2004, "status_code": 200, "category": "general", "cache_status": "db_hit", "timestamp": "2026-04-06T10:07:52.770665"} +{"path": "/suggest", "query": "samsung s", "latency_ms": 4704, "status_code": 200, "category": "general", "cache_status": "gemini_fast", "timestamp": "2026-04-06T10:07:58.235468"} +{"path": "/suggest", "query": "samsung s2", "latency_ms": 5195, "status_code": 200, "category": "general", "cache_status": "gemini_fast", "timestamp": "2026-04-06T10:07:59.959709"} +{"path": "/suggest", "query": "samsung s24", "latency_ms": 4879, "status_code": 200, "category": "general", "cache_status": "gemini_fast", "timestamp": "2026-04-06T10:08:01.171578"} +{"path": "/search/shopping", "query": "samsung s24", "latency_ms": 1854, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-04-06T10:08:05.070144"} +{"path": "/suggest", "query": "samsung s24", "latency_ms": 4101, "status_code": 200, "category": "shopping", "cache_status": "category_ai", "timestamp": "2026-04-06T10:08:07.625539"} +{"path": "/suggest", "query": "iphone 1", "latency_ms": 712, "status_code": 200, "category": "general", "cache_status": "db_hit", "timestamp": "2026-04-08T08:19:51.539533"} +{"path": "/suggest", "query": "iphone 16pro max", "latency_ms": 200, "status_code": 200, "category": "general", "cache_status": "db_hit", "timestamp": "2026-04-08T08:19:53.968201"} +{"path": "/suggest/more", "query": "iphone 16pro max", "latency_ms": 167, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-04-08T08:20:02.395425"} +{"path": "/search/shopping", "query": "iphone 16pro max", "latency_ms": 1273, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-04-08T08:20:12.439825"} +{"path": "/suggest", "query": "iphone 16pro max", "latency_ms": 4689, "status_code": 200, "category": "shopping", "cache_status": "category_ai", "timestamp": "2026-04-08T08:20:16.165247"} +{"path": "/search/videos", "query": "iphone 16pro max", "latency_ms": 1072, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-04-08T08:20:32.558553"} +{"path": "/suggest", "query": "iphone 16pro max", "latency_ms": 4654, "status_code": 200, "category": "videos", "cache_status": "category_ai", "timestamp": "2026-04-08T08:20:36.439925"} +{"path": "/search/news", "query": "iphone 16pro max", "latency_ms": 1212, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-04-08T08:20:43.009047"} +{"path": "/suggest", "query": "iphone 16pro max", "latency_ms": 5095, "status_code": 200, "category": "news", "cache_status": "category_ai", "timestamp": "2026-04-08T08:20:47.193498"} +{"path": "/search/places", "query": "iphone 16pro max", "latency_ms": 1178, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-04-08T08:20:51.900152"} +{"path": "/suggest", "query": "iphone 16pro max", "latency_ms": 3843, "status_code": 200, "category": "places", "cache_status": "category_ai", "timestamp": "2026-04-08T08:20:54.875439"} +{"path": "/search/news", "query": "iphone 16pro max", "latency_ms": 1317, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-04-08T08:21:12.697016"} +{"path": "/suggest", "query": "iphone 16pro max", "latency_ms": 4787, "status_code": 200, "category": "news", "cache_status": "category_ai", "timestamp": "2026-04-08T08:21:16.478475"} +{"path": "/suggest", "query": "iphone 16pro max", "latency_ms": 120, "status_code": 200, "category": "general", "cache_status": "db_hit", "timestamp": "2026-04-08T08:21:17.473584"} +{"path": "/suggest", "query": "iphone", "latency_ms": 874, "status_code": 200, "category": "general", "cache_status": "db_hit", "timestamp": "2026-04-08T08:55:36.488225"} +{"path": "/suggest", "query": "iphone 17pro mac", "latency_ms": 195, "status_code": 200, "category": "general", "cache_status": "db_hit", "timestamp": "2026-04-08T08:55:38.586719"} +{"path": "/suggest", "query": "iphone 17pro max", "latency_ms": 181, "status_code": 200, "category": "general", "cache_status": "db_hit", "timestamp": "2026-04-08T08:55:40.161123"} +{"path": "/suggest/more", "query": "iphone 17pro max", "latency_ms": 150, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-04-08T08:55:43.711385"} +{"path": "/search/shopping", "query": "iphone 17pro max", "latency_ms": 1208, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-04-08T08:55:46.274554"} +{"path": "/suggest", "query": "iphone 17pro max", "latency_ms": 4828, "status_code": 200, "category": "shopping", "cache_status": "category_ai", "timestamp": "2026-04-08T08:55:50.199124"} +{"path": "/search/shopping", "query": "iphone 17pro max", "latency_ms": 1287, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-04-08T08:55:57.682868"} +{"path": "/suggest", "query": "iphone 17pro max", "latency_ms": 4364, "status_code": 200, "category": "shopping", "cache_status": "category_ai", "timestamp": "2026-04-08T08:56:01.129312"} +{"path": "/generate", "query": "", "latency_ms": 6, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-04-08T08:56:18.358293"} +{"path": "/generate", "query": "", "latency_ms": 11370, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-04-08T08:56:29.745629"} +{"path": "/feedback", "query": "", "latency_ms": 0, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-04-08T08:56:29.757366"} +{"path": "/feedback", "query": "", "latency_ms": 464, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-04-08T08:56:30.235963"} +{"path": "/search/images", "query": "iphone 17pro max", "latency_ms": 1735, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-04-08T08:56:53.430680"} +{"path": "/suggest", "query": "iphone 17pro max", "latency_ms": 293, "status_code": 200, "category": "general", "cache_status": "db_hit", "timestamp": "2026-04-08T08:56:56.138214"} +{"path": "/suggest", "query": "iphone 17pro max", "latency_ms": 9606, "status_code": 200, "category": "images", "cache_status": "category_ai", "timestamp": "2026-04-08T08:57:01.603187"} +{"path": "/suggest", "query": "i[hone", "latency_ms": 6724, "status_code": 200, "category": "general", "cache_status": "gemini_fast", "timestamp": "2026-04-08T08:57:39.749883"} +{"path": "/suggest", "query": "iphone 16 pro max", "latency_ms": 234, "status_code": 200, "category": "general", "cache_status": "db_hit", "timestamp": "2026-04-08T08:57:42.669372"} +{"path": "/search/shopping", "query": "iphone 16 pro max", "latency_ms": 1320, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-04-08T08:57:46.372636"} +{"path": "/suggest", "query": "iphone 16 pro max", "latency_ms": 3570, "status_code": 200, "category": "shopping", "cache_status": "category_ai", "timestamp": "2026-04-08T08:57:48.931874"} +{"path": "/search/shopping", "query": "buy iphone 16 pro max", "latency_ms": 1176, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-04-08T08:58:06.459594"} +{"path": "/suggest", "query": "buy iphone 16 pro max", "latency_ms": 4543, "status_code": 200, "category": "shopping", "cache_status": "category_ai", "timestamp": "2026-04-08T08:58:09.826386"} +{"path": "/suggest/more", "query": "buy iphone 16 pro max", "latency_ms": 3847, "status_code": 200, "category": "shopping", "cache_status": "missing", "timestamp": "2026-04-08T08:58:16.792153"} +{"path": "/suggest", "query": "buy iphone 16 pro max", "latency_ms": 233, "status_code": 200, "category": "general", "cache_status": "db_hit", "timestamp": "2026-04-08T08:58:18.836982"} +{"path": "/suggest", "query": "iPhone 17 Pro Max price India", "latency_ms": 230, "status_code": 200, "category": "general", "cache_status": "db_hit", "timestamp": "2026-04-08T08:58:36.068269"} +{"path": "/search/shopping", "query": "iPhone 17 Pro Max price India", "latency_ms": 1149, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-04-08T08:58:39.490280"} +{"path": "/suggest", "query": "iPhone 17 Pro Max price India", "latency_ms": 2716, "status_code": 200, "category": "shopping", "cache_status": "category_ai", "timestamp": "2026-04-08T08:58:41.360163"} +{"path": "/search/images", "query": "iPhone 17 Pro Max price India", "latency_ms": 1460, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-04-08T08:58:49.665747"} +{"path": "/search/shopping", "query": "iPhone 17 Pro Max price India", "latency_ms": 1920, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-04-08T08:58:50.727321"} +{"path": "/suggest", "query": "iPhone 17 Pro Max price India", "latency_ms": 5456, "status_code": 200, "category": "shopping", "cache_status": "category_ai", "timestamp": "2026-04-08T08:58:54.566624"} +{"path": "/suggest", "query": "iPhone 17 Pro Max price India", "latency_ms": 6366, "status_code": 200, "category": "images", "cache_status": "category_ai", "timestamp": "2026-04-08T08:58:54.875983"} +{"path": "/search/shopping", "query": "iPhone 17 Pro Max camera specs", "latency_ms": 1372, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-04-08T08:59:04.596724"} +{"path": "/suggest", "query": "iPhone 17 Pro Max camera specs", "latency_ms": 5682, "status_code": 200, "category": "shopping", "cache_status": "category_ai", "timestamp": "2026-04-08T08:59:08.592939"} +{"path": "/suggest/more", "query": "iPhone 17 Pro Max camera specs", "latency_ms": 4863, "status_code": 200, "category": "shopping", "cache_status": "missing", "timestamp": "2026-04-08T08:59:15.153199"} +{"path": "/search/shopping", "query": "thinking to buy a", "latency_ms": 4792, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-04-08T08:59:54.072893"} +{"path": "/search/shopping", "query": "thinking to buy a samsung s", "latency_ms": 1720, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-04-08T08:59:54.079463"} +{"path": "/suggest", "query": "thinking to buy a", "latency_ms": 7510, "status_code": 200, "category": "shopping", "cache_status": "category_ai", "timestamp": "2026-04-08T08:59:56.656011"} +{"path": "/search/shopping", "query": "thinking to buy a samsung s23 ultra", "latency_ms": 1739, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-04-08T08:59:56.937115"} +{"path": "/suggest", "query": "thinking to buy a samsung s", "latency_ms": 4828, "status_code": 200, "category": "shopping", "cache_status": "category_ai", "timestamp": "2026-04-08T08:59:57.188634"} +{"path": "/suggest", "query": "thinking to buy a samsung s23 ultra", "latency_ms": 4500, "status_code": 200, "category": "shopping", "cache_status": "category_ai", "timestamp": "2026-04-08T08:59:59.699542"} +{"path": "/suggest/more", "query": "thinking to buy a samsung s23 ultra", "latency_ms": 6857, "status_code": 200, "category": "shopping", "cache_status": "missing", "timestamp": "2026-04-08T09:00:12.971094"} +{"path": "/search/shopping", "query": "samsung washing machine", "latency_ms": 1856, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-04-08T09:00:30.055684"} +{"path": "/search/shopping", "query": "samsung washing machine offers", "latency_ms": 1458, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-04-08T09:00:32.059996"} +{"path": "/suggest", "query": "samsung washing machine", "latency_ms": 4264, "status_code": 200, "category": "shopping", "cache_status": "category_ai", "timestamp": "2026-04-08T09:00:32.465734"} +{"path": "/suggest", "query": "samsung washing machine offers", "latency_ms": 4982, "status_code": 200, "category": "shopping", "cache_status": "category_ai", "timestamp": "2026-04-08T09:00:35.585074"} +{"path": "/suggest/more", "query": "samsung washing machine offers", "latency_ms": 4689, "status_code": 200, "category": "shopping", "cache_status": "missing", "timestamp": "2026-04-08T09:00:39.122243"} +{"path": "/suggest", "query": "samsung washing machine offers", "latency_ms": 6217, "status_code": 200, "category": "general", "cache_status": "gemini_fast", "timestamp": "2026-04-08T09:00:47.738562"} +{"path": "/suggest/more", "query": "samsung washing machine offers", "latency_ms": 44, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-04-08T09:00:53.711146"} +{"path": "/suggest", "query": "iqoo neo 10", "latency_ms": 372, "status_code": 200, "category": "general", "cache_status": "db_hit", "timestamp": "2026-04-08T09:01:33.884985"} +{"path": "/suggest", "query": "iqoo neo 10 5g", "latency_ms": 264, "status_code": 200, "category": "general", "cache_status": "db_hit", "timestamp": "2026-04-08T09:01:36.533466"} +{"path": "/suggest/more", "query": "iqoo neo 10 5g", "latency_ms": 169, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-04-08T09:01:47.648073"} +{"path": "/search/shopping", "query": "iqoo neo 10 5g", "latency_ms": 2118, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-04-08T09:01:57.466599"} +{"path": "/suggest", "query": "iqoo neo 10 5g", "latency_ms": 5701, "status_code": 200, "category": "shopping", "cache_status": "category_ai", "timestamp": "2026-04-08T09:02:01.351644"} +{"path": "/search/shopping", "query": "iqoo neo 10 5g offers and", "latency_ms": 1530, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-04-08T09:02:17.141146"} +{"path": "/suggest", "query": "iqoo neo 10 5g offers and", "latency_ms": 3985, "status_code": 200, "category": "shopping", "cache_status": "category_ai", "timestamp": "2026-04-08T09:02:19.590336"} +{"path": "/suggest", "query": "iqoo neo 10 5g offers and", "latency_ms": 245, "status_code": 200, "category": "general", "cache_status": "db_hit", "timestamp": "2026-04-08T09:02:22.492460"} +{"path": "/suggest/more", "query": "iqoo neo 10 5g offers and", "latency_ms": 162, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-04-08T09:02:30.444001"} +{"path": "/suggest/more", "query": "iqoo neo 10 5g offers and", "latency_ms": 22, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-04-08T09:02:33.443928"} +{"path": "/suggest", "query": "iphone 17 pro max", "latency_ms": 6177, "status_code": 200, "category": "shopping", "cache_status": "category_ai", "timestamp": "2026-04-08T09:07:32.164581"} +{"path": "/favicon.ico", "query": "", "latency_ms": 18, "status_code": 404, "category": "general", "cache_status": "missing", "timestamp": "2026-04-08T09:07:32.609546"} +{"path": "/suggest", "query": "iphone 17 pro max", "latency_ms": 5960, "status_code": 200, "category": "shopping", "cache_status": "category_ai", "timestamp": "2026-04-08T09:15:38.639552"} +{"path": "/suggest", "query": "iphone 17 pro max", "latency_ms": 1942, "status_code": 200, "category": "general", "cache_status": "db_hit", "timestamp": "2026-04-08T09:16:02.216698"} +{"path": "/search/shopping", "query": "iphone 17 pro max", "latency_ms": 1608, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-04-08T09:16:05.775795"} +{"path": "/suggest", "query": "iphone 17 pro max", "latency_ms": 3863, "status_code": 200, "category": "shopping", "cache_status": "category_ai", "timestamp": "2026-04-08T09:16:08.329685"} +{"path": "/suggest/more", "query": "iphone 17 pro max", "latency_ms": 4580, "status_code": 200, "category": "shopping", "cache_status": "missing", "timestamp": "2026-04-08T09:16:14.372291"} +{"path": "/search/shopping", "query": "iPhone 17 Pro Max EMI options", "latency_ms": 1519, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-04-08T09:16:40.902397"} +{"path": "/suggest", "query": "iPhone 17 Pro Max EMI options", "latency_ms": 4893, "status_code": 200, "category": "shopping", "cache_status": "category_ai", "timestamp": "2026-04-08T09:16:44.276160"} +{"path": "/suggest", "query": "iPhone 17 Pro Max EMI options", "latency_ms": 409, "status_code": 200, "category": "general", "cache_status": "db_hit", "timestamp": "2026-04-08T09:16:44.459759"} +{"path": "/suggest/more", "query": "iPhone 17 Pro Max EMI options", "latency_ms": 429, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-04-08T09:16:47.150370"} +{"path": "/search/shopping", "query": "iPhone 17 Pro Max EMI options", "latency_ms": 1287, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-04-08T09:16:56.663109"} +{"path": "/suggest", "query": "iPhone 17 Pro Max EMI options", "latency_ms": 3662, "status_code": 200, "category": "shopping", "cache_status": "category_ai", "timestamp": "2026-04-08T09:16:59.344416"} +{"path": "/search/shopping", "query": "iqoo neo 10", "latency_ms": 1360, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-04-08T09:17:11.927616"} +{"path": "/search/shopping", "query": "iqoo neo 10 5g", "latency_ms": 1920, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-04-08T09:17:14.716982"} +{"path": "/suggest", "query": "iqoo neo 10", "latency_ms": 4157, "status_code": 200, "category": "shopping", "cache_status": "category_ai", "timestamp": "2026-04-08T09:17:14.721779"} +{"path": "/suggest/more", "query": "iqoo neo 10 5g", "latency_ms": 4000, "status_code": 200, "category": "shopping", "cache_status": "missing", "timestamp": "2026-04-08T09:17:20.733040"} +{"path": "/suggest", "query": "iqoo neo 10 5g", "latency_ms": 168, "status_code": 200, "category": "general", "cache_status": "db_hit", "timestamp": "2026-04-08T09:17:22.988137"} +{"path": "/suggest", "query": "iqoo neo 10 5g", "latency_ms": 12636, "status_code": 200, "category": "shopping", "cache_status": "category_ai", "timestamp": "2026-04-08T09:17:25.434918"} +{"path": "/suggest", "query": "mahindra thar roxx", "latency_ms": 6732, "status_code": 200, "category": "general", "cache_status": "gemini_fast", "timestamp": "2026-04-08T09:17:51.255009"} +{"path": "/search/shopping", "query": "mahindra thar roxx", "latency_ms": 966, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-04-08T09:18:00.308895"} +{"path": "/suggest", "query": "mahindra thar roxx", "latency_ms": 4412, "status_code": 200, "category": "shopping", "cache_status": "category_ai", "timestamp": "2026-04-08T09:18:04.058361"} +{"path": "/suggest", "query": "mahindra thar roxx", "latency_ms": 145, "status_code": 200, "category": "general", "cache_status": "db_hit", "timestamp": "2026-04-08T09:18:12.502341"} +{"path": "/suggest", "query": "mahindra thar roxx", "latency_ms": 139, "status_code": 200, "category": "general", "cache_status": "db_hit", "timestamp": "2026-04-08T09:18:17.279756"} +{"path": "/search/shopping", "query": "mahindra thar roxx", "latency_ms": 1380, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-04-08T09:18:17.665065"} +{"path": "/suggest/more", "query": "mahindra thar roxx", "latency_ms": 199, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-04-08T09:18:19.107295"} +{"path": "/suggest", "query": "mahindra thar roxx", "latency_ms": 4568, "status_code": 200, "category": "shopping", "cache_status": "category_ai", "timestamp": "2026-04-08T09:18:21.155384"} +{"path": "/suggest/more", "query": "mahindra thar roxx", "latency_ms": 19, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-04-08T09:18:24.889275"} +{"path": "/suggest", "query": "mahindra thar roxx", "latency_ms": 119, "status_code": 200, "category": "general", "cache_status": "db_hit", "timestamp": "2026-04-08T09:18:33.602883"} +{"path": "/suggest/more", "query": "mahindra thar roxx", "latency_ms": 32, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-04-08T09:18:36.355164"} +{"path": "/suggest/more", "query": "mahindra thar roxx", "latency_ms": 17, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-04-08T09:19:00.230747"} +{"path": "/suggest", "query": "office chairs", "latency_ms": 5459, "status_code": 200, "category": "general", "cache_status": "gemini_fast", "timestamp": "2026-04-08T09:24:44.563165"} +{"path": "/search/shopping", "query": "office chairs", "latency_ms": 798, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-04-08T09:24:50.842189"} +{"path": "/suggest", "query": "office chairs", "latency_ms": 99, "status_code": 200, "category": "general", "cache_status": "redis_hit", "timestamp": "2026-04-08T09:24:51.783600"} +{"path": "/suggest", "query": "office chairs", "latency_ms": 3712, "status_code": 200, "category": "shopping", "cache_status": "category_ai", "timestamp": "2026-04-08T09:24:54.125872"} +{"path": "/suggest", "query": "office chairs", "latency_ms": 114, "status_code": 200, "category": "general", "cache_status": "redis_hit", "timestamp": "2026-04-08T09:24:58.647585"} +{"path": "/suggest/more", "query": "office chairs", "latency_ms": 167, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-04-08T09:25:03.765175"} +{"path": "/suggest", "query": "best", "latency_ms": 5266, "status_code": 200, "category": "general", "cache_status": "gemini_fast", "timestamp": "2026-04-08T09:25:17.692097"} +{"path": "/suggest", "query": "best electric tootbrush", "latency_ms": 3685, "status_code": 200, "category": "general", "cache_status": "gemini_fast", "timestamp": "2026-04-08T09:25:24.297799"} +{"path": "/suggest", "query": "best electric tootbrush", "latency_ms": 105, "status_code": 200, "category": "general", "cache_status": "redis_hit", "timestamp": "2026-04-08T09:25:38.319761"} +{"path": "/suggest", "query": "havells inve", "latency_ms": 5918, "status_code": 200, "category": "general", "cache_status": "gemini_fast", "timestamp": "2026-04-08T09:35:47.738563"} +{"path": "/suggest", "query": "havells inveter", "latency_ms": 3037, "status_code": 200, "category": "general", "cache_status": "gemini_fast", "timestamp": "2026-04-08T09:35:48.087223"} +{"path": "/suggest", "query": "havells inverter ac", "latency_ms": 4585, "status_code": 200, "category": "general", "cache_status": "gemini_fast", "timestamp": "2026-04-08T09:35:53.235716"} +{"path": "/search/shopping", "query": "havells inverter ac", "latency_ms": 1505, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-04-08T09:36:05.858815"} +{"path": "/suggest", "query": "havells inverter ac", "latency_ms": 3116, "status_code": 200, "category": "shopping", "cache_status": "category_ai", "timestamp": "2026-04-08T09:36:07.469131"} +{"path": "/suggest", "query": "havells inverter ac", "latency_ms": 176, "status_code": 200, "category": "general", "cache_status": "db_hit", "timestamp": "2026-04-08T09:36:27.833536"} +{"path": "/suggest/more", "query": "havells inverter ac", "latency_ms": 144, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-04-08T09:36:28.754413"} +{"path": "/suggest/more", "query": "havells inverter ac", "latency_ms": 12, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-04-08T09:36:30.125653"} +{"path": "/suggest", "query": "mahind", "latency_ms": 5439, "status_code": 200, "category": "general", "cache_status": "gemini_fast", "timestamp": "2026-04-08T09:55:39.588204"} +{"path": "/suggest", "query": "mahindra be 6", "latency_ms": 4609, "status_code": 200, "category": "general", "cache_status": "gemini_fast", "timestamp": "2026-04-08T09:55:41.762995"} +{"path": "/suggest/more", "query": "mahindra be 6", "latency_ms": 29, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-04-08T09:55:49.731223"} +{"path": "/suggest/more", "query": "mahindra be 6", "latency_ms": 25, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-04-08T09:55:50.857915"} +{"path": "/suggest", "query": "budget friendly refrigerators", "latency_ms": 7236, "status_code": 200, "category": "general", "cache_status": "gemini_fast", "timestamp": "2026-04-09T09:11:15.197442"} +{"path": "/suggest/more", "query": "budget friendly refrigerators", "latency_ms": 33, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-04-09T09:11:23.075619"} +{"path": "/suggest/more", "query": "budget friendly refrigerators", "latency_ms": 21, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-04-09T09:11:24.342069"} +{"path": "/suggest/more", "query": "budget friendly refrigerators", "latency_ms": 22, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-04-09T09:11:25.544698"} +{"path": "/suggest/more", "query": "budget friendly refrigerators", "latency_ms": 18, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-04-09T09:11:26.458691"} +{"path": "/suggest", "query": "iphone 167", "latency_ms": 3678, "status_code": 200, "category": "general", "cache_status": "gemini_fast", "timestamp": "2026-04-09T09:11:41.503045"} +{"path": "/suggest", "query": "iphone 16 pro max", "latency_ms": 4858, "status_code": 200, "category": "general", "cache_status": "gemini_fast", "timestamp": "2026-04-09T09:11:46.003886"} +{"path": "/search/shopping", "query": "iphone 16 pro max", "latency_ms": 1493, "status_code": 200, "category": "general", "cache_status": "missing", "timestamp": "2026-04-09T09:11:47.371655"} +{"path": "/suggest", "query": "iphone 16 pro max", "latency_ms": 4969, "status_code": 200, "category": "shopping", "cache_status": "category_ai", "timestamp": "2026-04-09T09:11:51.150199"} +{"path": "/suggest/more", "query": "iphone 16 pro max", "latency_ms": 4372, "status_code": 200, "category": "shopping", "cache_status": "missing", "timestamp": "2026-04-09T09:12:00.455428"} diff --git a/start.bat b/start.bat new file mode 100644 index 00000000..26e0e13b --- /dev/null +++ b/start.bat @@ -0,0 +1,10 @@ +@echo off +echo Starting Docker containers... +docker start decision_pg +docker start decision_redis +echo Waiting for PostgreSQL to be ready... +timeout /t 3 +echo Starting FastAPI server... +cd C:\Users\rithv\OneDrive\Desktop\decision_engine_project +call venv\Scripts\activate +python -m uvicorn app.main:app --host 127.0.0.1 --port 8000 \ No newline at end of file