From 50263c0d07973ee26f8a2b701cd028d157fa0edd Mon Sep 17 00:00:00 2001 From: ashok Date: Fri, 10 Apr 2026 13:09:53 +0530 Subject: [PATCH] Changes ui updated --- .dockerignore | 12 + .env | 5 +- .gitignore | 29 +- Dockerfile | 30 + app/__pycache__/bootstrap.cpython-312.pyc | Bin 8341 -> 11753 bytes app/__pycache__/db_schema.cpython-312.pyc | Bin 2382 -> 3113 bytes .../embedding_cache.cpython-312.pyc | Bin 1304 -> 1308 bytes app/__pycache__/main.cpython-312.pyc | Bin 11621 -> 42893 bytes app/__pycache__/normalizer.cpython-312.pyc | Bin 1456 -> 1456 bytes .../semantic_cache.cpython-312.pyc | Bin 4536 -> 4764 bytes app/__pycache__/vertex_client.cpython-312.pyc | Bin 993 -> 1368 bytes app/analyze_logs.py | 64 ++ app/bootstrap.py | 158 ++-- app/check_db.py | 4 +- app/db_schema.py | 22 +- app/embedding_cache.py | 2 +- app/insert_test.py | 20 + app/log_cleanup.py | 15 + app/logger.py | 20 + app/main.py | 813 +++++++++++++++--- app/semantic_cache.py | 5 +- app/service-account.json | 13 - app/vertex_client.py | 17 +- docker-compose.yml | 46 + index.html | 482 ++++++++--- logs/app.log | 317 +++++++ start.bat | 10 + 27 files changed, 1793 insertions(+), 291 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 app/analyze_logs.py create mode 100644 app/insert_test.py create mode 100644 app/log_cleanup.py create mode 100644 app/logger.py delete mode 100644 app/service-account.json create mode 100644 docker-compose.yml create mode 100644 logs/app.log create mode 100644 start.bat 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 05ddbc9781045fc7607514f320191532f730d956..74f87a3548078c85c8f7728019d137465873721d 100644 GIT binary patch literal 11753 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 delta 188 zcmZ1}aZZTuG%qg~0}yy}&DH|9k^DzpDRB|da7%~(ymNPLjL^3b} zVb9XZ7nsC3IV+eW8Oj+oxi)`bn!+;q1m{Y2cAz1}?-?dnaCx#QuqHE3KEhSw^4*aU z#QxyI$iO4;-J6k-)rIi`1Bm?foed%Lt5^c4d!e+m0`ox;CuV0I=EDlC&YJ9pby%HE YCvV|S72r~16qph6g#kzvi2~IE0PU79CjbBd diff --git a/app/__pycache__/embedding_cache.cpython-312.pyc b/app/__pycache__/embedding_cache.cpython-312.pyc index 92570fad7bd652d0c21bd5ab9ec73472a042a47a..3a7d3ec3141de98618d266d11c58f82d81058868 100644 GIT binary patch delta 59 zcmbQiHHVA$G%qg~0}yZ(9LhA?$a{c^ojo--$*Rn7^F=0cMxi-CsRo7*JPf=d6I>^l NU*MA7{F7Op5dh4i5DWkS delta 55 zcmbQkHG_-yG%qg~0}zx?U6pCPk@o--3rlKl(&j5n;*5f`f#MAeA9xse1t*A3VDHe| J{Fhmt5dfV;4*viE diff --git a/app/__pycache__/main.cpython-312.pyc b/app/__pycache__/main.cpython-312.pyc index 3f635a6016f50d4c4c1f7c77bee15298d8f18f56..4be0d943f9fc17dcff0d48c270084514155c5f9c 100644 GIT binary patch literal 42893 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 Aa{vGU delta 5777 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 delta 273 zcmbQExqgnU}+WDNAGW zehyJat;zSh$C zQ9eFbMdqW5tgdRCpYf_OF}hCX6)o9rlH#u^SZGHP;-z#5>arr=&RkTtiMGxJJ{ zKr$;CiXwnikrt5n#bJ}1pHiBWYFCsznMWuaHaTBNlLe#$ E02s1Hwg3PC diff --git a/app/__pycache__/vertex_client.cpython-312.pyc b/app/__pycache__/vertex_client.cpython-312.pyc index 82d9a23d43fecc1d4c496477716a2863915ca81c..cc9cf1006dba1fbb156814df4104bd378d25a6d2 100644 GIT binary patch delta 756 zcmaFJeuIneG%qg~0}!}R-jx}^JdsaAM-9lE&XB^8!kEL5%NWJT2x2qkFhwzgX_hFK z6y_F&DAr0QO_qs%l3~dp?J&RsWH1Bq=T(d#Q%Zm&1Y|M6SO_|WaWz!FmZ65BNWKIv zmBljgp|CzXoDXNzFu=4FGo>)qGN5U(#;=91gcD{`3Udv^Y=*f^FyopSCz~_MyYs{3 z5p)eh3QIcU8rIcNAJj6|FlGrN%*-Tt2qFSXKoLW?t znVhPSn4FwnnpdKbmYI{Pke3f+0J$j&i6vGFllL)N>+#=W$}hgfmY!OYnpbvNtOVpuXGmd4Va#F3WsG8E1hJWNn4*|dn4?%y zSXvmOSSy({Szm&*X)@m8FHS8g%S=v3Qs|uLBpV?;e1SS!=WRMydU;#3i zf%x+aATgbxgkfT*j7t_POd3I#u))|VjOmPPm{v1E__Yi*3|X8oIRrg9kWrqIZ{k5= zMgb5ZJoz)DsuV+(2toyn70jT?>^J!&lQkpvWFuxhQITNRpfFEo*LX)~Xa7*Yka#yw zAJ@t8%(lXex44swQd3g%N-`63ii;V6CQRPLoTMlOWUvEqaSV`XV7McscwI>MqLA)& zA@_?y?sqt3CU>#4_=1A8hyz4`)URYH5(cpafy6Hko80`A(wtPgB5@!WWNxt-kodsN a$jJDVLGw0)>I3nt3k)KkS=bn5!TJDkn^Tnl 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 @@ -AI Prompt Compressor Engine +AI Prompt Composer Engine -
-

AI Prompt Compressor Engine

+

AI Prompt Composer Engine

+
+
+
- + +
+
- Selected Keywords +
+ Selected Keywords + +
+ +
+
šŸ” General
+
šŸ›ļø Shopping
+
šŸ–¼ļø Images
+
šŸŽ¬ Videos
+
šŸ“° News
+
šŸ“ Places
+
+ +
- Suggestions +
+ Suggestions +
+ General + +
+
Start typing to see suggestions...
+ +
+
+ +
+
+
+ +
- +
@@ -92,114 +208,239 @@ button:disabled{background:#999;cursor:not-allowed} let debounceTimer; let currentQuery = ""; let currentOffset = 0; - let lastFetchedQuery = ""; // tracks last fetched query to avoid duplicate calls + let lastFetchedQuery = ""; + let currentCategory = "general"; const promptInput = document.getElementById("prompt"); const keywordsDiv = document.getElementById("keywords"); const selectedDiv = document.getElementById("selectedKeywords"); const selectedArea = document.getElementById("selectedArea"); - // ── Gibberish detector ── - function isGibberish(text) { - const words = text.toLowerCase().split(/\s+/); - let gibberishCount = 0; - - for (const word of words) { - if (word.length <= 3) continue; // allow short words like ev, ai, bmw + const CATEGORIES = { + general: { label: "General", badge: "general", icon: "šŸ”" }, + shopping: { label: "Shopping", badge: "shopping", icon: "šŸ›ļø" }, + images: { label: "Images", badge: "images", icon: "šŸ–¼ļø" }, + videos: { label: "Videos", badge: "videos", icon: "šŸŽ¬" }, + news: { label: "News", badge: "news", icon: "šŸ“°" }, + places: { label: "Places", badge: "places", icon: "šŸ“" } + }; - if (word.match(/[^aeiou]{6,}/)) { gibberishCount++; continue; } // raised to 6 + // ── Gibberish check ── + function isGibberish(text) { + const words = text.toLowerCase().split(/\s+/); + let gc = 0; + for (const w of words) { + if (w.length <= 3) continue; + if (w.match(/[^aeiou]{6,}/)) { gc++; continue; } + const v = (w.match(/[aeiou]/g) || []).length; + if (w.length > 5 && v / w.length < 0.1) { gc++; continue; } + if (/[a-z]\d{3,}[a-z]|[0-9]{4,}[a-z]/.test(w)) { gc++; continue; } + } + return gc > words.length / 2; + } - const vowels = (word.match(/[aeiou]/g) || []).length; - if (word.length > 5 && vowels / word.length < 0.1) { gibberishCount++; continue; } + // ── Select category ── + function selectCategory(cat) { + currentCategory = cat; + document.querySelectorAll(".cat-tab").forEach(t => t.classList.remove("active")); + document.querySelector(`[data-cat="${cat}"]`).classList.add("active"); + const badge = document.getElementById("categoryBadge"); + badge.textContent = CATEGORIES[cat].label; + badge.className = `category-badge ${cat}`; + lastFetchedQuery = ""; - if (/[a-z]\d{3,}[a-z]|[0-9]{4,}[a-z]/.test(word)) { gibberishCount++; continue; } + const q = promptInput.value.trim(); + if (q.length >= 3 && !isGibberish(q)) { + keywordsDiv.innerHTML = `
Loading ${CATEGORIES[cat].label} suggestions...
`; + clearTimeout(debounceTimer); + debounceTimer = setTimeout(() => fetchKeywords(q), 300); + if (cat !== "general") fetchCategoryResults(q, cat); + else document.getElementById("resultsBox").classList.remove("visible"); + } else { + document.getElementById("resultsBox").classList.remove("visible"); + } } - return gibberishCount > words.length / 2; -} - - // ── Listen to typing ── + // ── Typing listener ── promptInput.addEventListener("input", () => { clearTimeout(debounceTimer); const q = promptInput.value.trim(); currentQuery = q; - if (q.length < 3) { + if (q.length < 4) { keywordsDiv.innerHTML = 'Keep typing...'; + document.getElementById("resultsBox").classList.remove("visible"); return; } - if (isGibberish(q)) { keywordsDiv.innerHTML = 'āš ļø Please enter a meaningful topic.'; return; } - keywordsDiv.innerHTML = ` -
-
- Loading suggestions for "${q}"... -
- `; + if (!keywordsDiv.querySelector('.chip')) { + showSkeletonChips(); + } - debounceTimer = setTimeout(() => fetchKeywords(q), 500); + debounceTimer = setTimeout(() => { + fetchKeywords(q); + if (currentCategory !== "general") fetchCategoryResults(q, currentCategory); + }, 1000); }); + // ── Skeleton chips ── + function showSkeletonChips() { + keywordsDiv.innerHTML = ""; + for (let i = 0; i < 8; i++) { + const sk = document.createElement("span"); + sk.className = "chip skeleton"; + sk.style.width = `${60 + Math.random() * 60}px`; + sk.innerHTML = " "; + keywordsDiv.appendChild(sk); + } + } + // ── Fetch suggestions ── async function fetchKeywords(query) { - // Skip if same query already fetched — avoids duplicate API calls - if (query === lastFetchedQuery) return; - lastFetchedQuery = query; + const cacheKey = `${query}__${currentCategory}`; + if (cacheKey === lastFetchedQuery) return; + lastFetchedQuery = cacheKey; currentOffset = 0; + showSkeletonChips(); + try { - const res = await fetch(`http://127.0.0.1:8000/suggest?query=${encodeURIComponent(query)}&offset=0&limit=15`); + const url = `http://127.0.0.1:8000/suggest?query=${encodeURIComponent(query)}&offset=0&limit=15&category=${currentCategory}`; + const res = await fetch(url); const data = await res.json(); - // Backend detected gibberish if (data.cache === "gibberish") { keywordsDiv.innerHTML = 'āš ļø Please enter a meaningful topic.'; return; } - - // No domain found or query skipped - if (data.cache === "skip" || data.cache === "no_domain" || !data.suggestions || !data.suggestions.length) { + if (!data.suggestions || !data.suggestions.length) { keywordsDiv.innerHTML = 'No suggestions found. Try a more specific topic.'; return; } - renderChips(data.suggestions, false); + } catch (e) { + keywordsDiv.innerHTML = 'Backend not reachable.'; + } + } + + // ── Fetch category results ── + async function fetchCategoryResults(query, category) { + const resultsBox = document.getElementById("resultsBox"); + const resultsGrid = document.getElementById("resultsGrid"); + const resultsTitle = document.getElementById("resultsTitle"); + + if (category === "general") { + resultsBox.classList.remove("visible"); + return; + } + + resultsGrid.innerHTML = ""; + + const endpointMap = { + videos: "/search/videos", + shopping: "/search/shopping", + images: "/search/images", + news: "/search/news", + places: "/search/places" + }; + + const endpoint = endpointMap[category]; + if (!endpoint) return; + + try { + const res = await fetch(`http://127.0.0.1:8000${endpoint}?query=${encodeURIComponent(query)}`); + const data = await res.json(); + + if (data.error || !data.results || !data.results.length) { + resultsBox.classList.remove("visible"); + return; + } + + const icons = { shopping:"šŸ›ļø", images:"šŸ–¼ļø", videos:"šŸŽ¬", news:"šŸ“°", places:"šŸ“" }; + resultsTitle.textContent = `${icons[category]} ${CATEGORIES[category].label} results for "${query}"`; + resultsBox.classList.add("visible"); + + if (category === "videos") { + resultsGrid.style.gridTemplateColumns = "repeat(auto-fill, minmax(260px, 1fr))"; + resultsGrid.innerHTML = data.results.map(v => ` +
+ ${escHtml(v.title)} +
+
${escHtml(v.title)}
+
šŸ“ŗ ${escHtml(v.channel)}
+
${v.published}
+
+
`).join(""); + + } else if (category === "shopping") { + resultsGrid.style.gridTemplateColumns = "repeat(auto-fill, minmax(220px, 1fr))"; + resultsGrid.innerHTML = data.results.map(s => ` + + ${s.image ? `` : ""} +
${escHtml(s.title)}
+
šŸ”— ${escHtml(s.source)}
+
${escHtml(s.snippet)}
+
`).join(""); + + } else if (category === "images") { + resultsGrid.style.gridTemplateColumns = "repeat(auto-fill, minmax(180px, 1fr))"; + resultsGrid.innerHTML = data.results.map(img => ` + + ${escHtml(img.title)} +
${escHtml(img.source)}
+
`).join(""); + + } else if (category === "news") { + resultsGrid.style.gridTemplateColumns = "1fr"; + resultsGrid.innerHTML = data.results.map(n => ` + + ${n.image ? `` : ""} +
+
${escHtml(n.title)}
+
šŸ“° ${escHtml(n.source)}
+
${escHtml(n.snippet)}
+
+
`).join(""); + + } else if (category === "places") { + resultsGrid.style.gridTemplateColumns = "1fr"; + resultsGrid.innerHTML = data.results.map(p => ` + + ${p.image ? `` : ""} +
+
${escHtml(p.title)}
+
šŸ“ ${escHtml(p.source)}
+
${escHtml(p.snippet)}
+
+
`).join(""); + } } catch (e) { - keywordsDiv.innerHTML = 'Backend not reachable — is the server running?'; + resultsBox.classList.remove("visible"); } } // ── Render chips ── function renderChips(items, append) { - if (!append) { - keywordsDiv.innerHTML = ""; - currentOffset = 15; - } else { - const old = document.getElementById("loadMoreBtn"); - if (old) old.remove(); - } - + if (!append) { keywordsDiv.innerHTML = ""; currentOffset = 15; } + else { const old = document.getElementById("loadMoreBtn"); if (old) old.remove(); } if (!items.length && !append) { keywordsDiv.innerHTML = 'No suggestions found.'; return; } - items.forEach((k, i) => { const chip = document.createElement("span"); chip.className = "chip" + (selected.includes(k) ? " selected" : ""); - if (!append && i < 5) chip.classList.add("top"); // top 5 highlighted in gold + if (!append && i < 5) chip.classList.add("top"); chip.innerText = k; chip.dataset.kw = k; chip.onclick = () => toggleChip(k, chip); keywordsDiv.appendChild(chip); }); - - // Load more button const btn = document.createElement("button"); btn.id = "loadMoreBtn"; btn.className = "load-more-btn"; @@ -208,7 +449,7 @@ button:disabled{background:#999;cursor:not-allowed} keywordsDiv.appendChild(btn); } - // ── Toggle chip selection ── + // ── Toggle chip ── function toggleChip(k, chipEl) { if (selected.includes(k)) { selected = selected.filter(x => x !== k); @@ -223,10 +464,7 @@ button:disabled{background:#999;cursor:not-allowed} // ── Render selected keywords ── function renderSelected() { selectedDiv.innerHTML = ""; - if (!selected.length) { - selectedArea.style.display = "none"; - return; - } + if (!selected.length) { selectedArea.style.display = "none"; return; } selectedArea.style.display = "block"; selected.forEach(k => { const chip = document.createElement("span"); @@ -234,22 +472,27 @@ button:disabled{background:#999;cursor:not-allowed} chip.innerText = k + " āœ•"; chip.onclick = () => { selected = selected.filter(x => x !== k); - const kwChip = keywordsDiv.querySelector(`[data-kw="${k}"]`); - if (kwChip) kwChip.classList.remove("selected"); + const kw = keywordsDiv.querySelector(`[data-kw="${k}"]`); + if (kw) kw.classList.remove("selected"); renderSelected(); }; selectedDiv.appendChild(chip); }); } - // ── Load more suggestions ── + // ── Load more ── async function loadMore() { const btn = document.getElementById("loadMoreBtn"); btn.innerText = "Loading..."; btn.disabled = true; try { - const res = await fetch(`http://127.0.0.1:8000/suggest?query=${encodeURIComponent(currentQuery)}&offset=${currentOffset}&limit=10`); + const existing = Array.from(keywordsDiv.querySelectorAll(".chip")) + .map(c => c.dataset.kw).filter(Boolean); + + const res = await fetch( + `http://127.0.0.1:8000/suggest/more?query=${encodeURIComponent(currentQuery)}&category=${currentCategory}&existing=${encodeURIComponent(existing.join(","))}` + ); const data = await res.json(); if (!data.suggestions || !data.suggestions.length) { @@ -257,18 +500,13 @@ button:disabled{background:#999;cursor:not-allowed} return; } - const existing = Array.from(keywordsDiv.querySelectorAll(".chip")) - .map(c => c.dataset.kw?.toLowerCase()).filter(Boolean); - const newItems = data.suggestions.filter(s => !existing.includes(s.toLowerCase())); - - if (!newItems.length) { - btn.innerText = "No more suggestions"; - return; - } + const newItems = data.suggestions.filter( + s => !existing.map(e => e.toLowerCase()).includes(s.toLowerCase()) + ); + if (!newItems.length) { btn.innerText = "No more suggestions"; return; } currentOffset += 10; renderChips(newItems, true); - } catch (e) { btn.innerText = "+ More"; btn.disabled = false; @@ -286,15 +524,16 @@ button:disabled{background:#999;cursor:not-allowed} document.getElementById("askBtn").disabled = true; step++; - const chat = document.getElementById("chat"); + const chat = document.getElementById("chat"); const block = document.createElement("div"); block.className = "chat-block"; + const catInfo = CATEGORIES[currentCategory]; block.innerHTML = `
${step}. Go
Prompt: ${escHtml(combined)}
+
Category: ${catInfo.icon} ${catInfo.label}
Keywords: ${selected.length ? selected.map(escHtml).join(", ") : "none selected"}
-
ā³ AI is generating answer...
- `; +
ā³ AI is generating answer...
`; chat.appendChild(block); block.scrollIntoView({ behavior: "smooth" }); @@ -306,6 +545,7 @@ button:disabled{background:#999;cursor:not-allowed} body: JSON.stringify({ query: combined, selected_attributes: selected, + category: currentCategory, chat_history: chatHistory }) }); @@ -315,46 +555,74 @@ button:disabled{background:#999;cursor:not-allowed} answer = "Backend not running. Start the server on port 8000."; } + if (selected.length > 0) { + fetch("http://127.0.0.1:8000/feedback", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + query: combined, + selected_chips: selected, + domain: currentQuery + }) + }).catch(() => {}); + } + block.querySelector(".answer").innerHTML = `AI Answer

${formatAnswer(answer)}`; - chatHistory.push({ query: combined, chips: [...selected], answer }); + chatHistory.push({ query: combined, chips: [...selected], category: currentCategory, answer }); - // Reset after submit - promptInput.value = ""; document.getElementById("extra").value = ""; - selected = []; - lastFetchedQuery = ""; // reset so next query fetches fresh - renderSelected(); - keywordsDiv.innerHTML = 'Start typing to see suggestions...'; - document.getElementById("goBtn").disabled = false; document.getElementById("askBtn").disabled = false; promptInput.focus(); } - // ── Clear all ── + // ── Clear functions ── function clearAll() { promptInput.value = ""; document.getElementById("extra").value = ""; - selected = []; - chatHistory = []; - step = 0; - lastFetchedQuery = ""; + selected = []; chatHistory = []; step = 0; lastFetchedQuery = ""; currentCategory = "general"; + document.querySelectorAll(".cat-tab").forEach(t => t.classList.remove("active")); + document.querySelector("[data-cat='general']").classList.add("active"); + const badge = document.getElementById("categoryBadge"); + badge.textContent = "General"; badge.className = "category-badge general"; renderSelected(); keywordsDiv.innerHTML = 'Start typing to see suggestions...'; + document.getElementById("resultsBox").classList.remove("visible"); document.getElementById("chat").innerHTML = ""; } - // ── Format AI answer (markdown-like) ── + function clearPrompt() { + promptInput.value = ""; + keywordsDiv.innerHTML = 'Start typing...'; + lastFetchedQuery = ""; + document.getElementById("resultsBox").classList.remove("visible"); + promptInput.focus(); + } + + function clearExtra() { document.getElementById("extra").value = ""; } + + function clearSelected() { + selected = []; + document.querySelectorAll(".chip.selected").forEach(c => c.classList.remove("selected")); + renderSelected(); + } + + function clearSuggestions() { + keywordsDiv.innerHTML = 'Start typing to see suggestions...'; + lastFetchedQuery = ""; + } + + // ── Format AI answer ── function formatAnswer(text) { return text .replace(/&/g,'&').replace(//g,'>') - .replace(/## (.*)/g, '

$1

') - .replace(/\*\*(.*?)\*\*/g, '$1') - .replace(/\*(.*?)\*/g, '$1') - .replace(/---/g, '
') - .replace(/\n\n+/g, '

') - .replace(/\n/g, '
') - .replace(/^/, '

').replace(/$/, '

'); + .replace(/## (.*)/g,'

$1

') + .replace(/\*\*(.*?)\*\*/g,'$1') + .replace(/\*(.*?)\*/g,'$1') + .replace(/---/g,'
') + .replace(/\n\n+/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