Changes ui updated

main
ashok 2 weeks ago
parent 88bce72dde
commit 50263c0d07

@ -0,0 +1,12 @@
.env
.env.*
venv/
seed/
.venv/
__pycache__/
*.pyc
*.pyo
*.pyd
.git
.gitignore
app/service-account.json

@ -1,2 +1,5 @@
GOOGLE_PROJECT_ID=sylvan-deck-387207 GOOGLE_PROJECT_ID=sylvan-deck-387207
GOOGLE_APPLICATION_CREDENTIALS=C:\Users\rithv\OneDrive\Desktop\decision_engine_project\service-account.json 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

29
.gitignore vendored

@ -1,5 +1,32 @@
# Virtual environments
venv/ venv/
seed/ seed/
.venv/
# Environment & secrets
.env .env
.env.*
app/service-account.json
# Python cache
__pycache__/ __pycache__/
*.pyc *.pyc
*.pyo
*.pyd
# Build artifacts
dist/
build/
*.egg-info/
# IDE
.vscode/
.idea/
# OS
.DS_Store
Thumbs.db
# Docker volumes
pg_data/
redis_data/

@ -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"]

@ -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

@ -7,12 +7,11 @@ from sentence_transformers import SentenceTransformer
import requests as http_requests import requests as http_requests
import google.auth import google.auth
import google.auth.transport.requests import google.auth.transport.requests
import os
from app.vertex_client import get_access_token from app.vertex_client import get_access_token
load_dotenv() load_dotenv()
DATABASE_URL = "postgresql://postgres:postgres@localhost:5432/decision_engine" DATABASE_URL = os.getenv("DATABASE_URL")
engine = create_engine(DATABASE_URL) engine = create_engine(DATABASE_URL)
model = SentenceTransformer("all-MiniLM-L6-v2") 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") raise ValueError("No JSON object found in response")
def validate_schema(data: dict) -> dict: def validate_schema(data: dict, query: str) -> dict:
"""
Validate and clean Gemini response.
- Only keep valid categories
- Only keep string attributes
- Remove empty categories
- Cap at 5 attributes per category
"""
cleaned = {} 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(): for category, attributes in data.items():
# Normalize category name
cat = category.strip().title() cat = category.strip().title()
# Skip invalid categories
if cat not in VALID_CATEGORIES: if cat not in VALID_CATEGORIES:
print(f"Skipping unknown category: {cat}") print(f"Skipping unknown category: {cat}")
continue continue
# Only keep string attributes attrs = []
attrs = [a.strip() for a in attributes if isinstance(a, str) and a.strip()] for a in attributes:
if not isinstance(a, str) or not a.strip():
# Cap at 5 per category continue
attrs = attrs[:6] # ✅ 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: if attrs:
cleaned[cat] = attrs cleaned[cat] = attrs
@ -77,57 +86,75 @@ def validate_schema(data: dict) -> dict:
def call_gemini(query: str) -> dict: def call_gemini(query: str) -> dict:
prompt = f"""You are a world-class decision analysis expert. 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: MANDATORY RULES:
- Only include criteria that are HIGHLY RELEVANT to "{query}" - Select 6-8 categories from the allowed list
- Prioritize criteria that people MOST COMMONLY consider for this topic - Generate EXACTLY 8-10 attributes per category this is mandatory
- Each attribute must be SPECIFIC and MEASURABLE, not generic - Each attribute must be SPECIFIC and MEASURABLE for "{query}"
- Order attributes by importance (most important first) - First 3 attributes in EVERY category must be the MOST SEARCHED specs
- Use concise names (2-5 words max per attribute) - 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. Allowed category keys:
Use ONLY these category keys: Performance, Financial, Risk, Maintenance, Benefits, Time, Requirements, Scalability, Alternatives, Usability, Security, Reliability, Support, Sustainability Performance, Financial, Risk, Maintenance, Benefits, Time, Requirements,
Scalability, Alternatives, Usability, Security, Reliability, Support, Sustainability
Example for "buying a car": Example of CORRECT format with enough attributes:
{{"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"]}} {{"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}" 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" 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): for attempt in range(3):
try: try:
res = http_requests.post(url, res = http_requests.post(
headers={ url,
"Authorization": f"Bearer {get_access_token()}", headers={
"Content-Type": "application/json"}, "Authorization": f"Bearer {get_access_token()}",
json={ "Content-Type": "application/json"
"contents": [{"role" : "user","parts": [{"text": prompt}]}], },
"generationConfig": {"temperature": 0.2, "maxOutputTokens": 1024}}) json={
"contents": [{"role": "user", "parts": [{"text": prompt}]}],
# Handle rate limit with exponential backoff "generationConfig": {
"temperature": 0.1, # lower = more accurate, less random
"maxOutputTokens": 1024
}
},
timeout=30
)
if res.status_code == 429: if res.status_code == 429:
wait = 2 ** attempt # 1s, 2s, 4s wait = 2 ** attempt
print(f" Rate limited, waiting {wait}s... (attempt {attempt + 1})") print(f"Rate limited, waiting {wait}s... (attempt {attempt + 1})")
time.sleep(wait) time.sleep(wait)
continue 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"] raw = res.json()["candidates"][0]["content"]["parts"][0]["text"]
clean = clean_json_response(raw) clean = clean_json_response(raw)
data = json.loads(clean) 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 return validated
except (json.JSONDecodeError, ValueError) as e: except (json.JSONDecodeError, ValueError) as e:
print(f" Attempt {attempt + 1} failed: {e}") print(f"Attempt {attempt + 1} failed: {e}")
if attempt == 2: if attempt == 2:
raise RuntimeError(f"Gemini failed after 3 attempts: {e}") raise RuntimeError(f"Gemini failed after 3 attempts: {e}")
@ -135,8 +162,6 @@ Return ONLY the JSON object."""
def bootstrap_domain(query: str): def bootstrap_domain(query: str):
# Retry up to 3 times if Gemini returns bad JSON
data = None data = None
for attempt in range(3): for attempt in range(3):
try: try:
@ -148,9 +173,32 @@ def bootstrap_domain(query: str):
if attempt == 2: if attempt == 2:
raise RuntimeError(f"Gemini failed after 3 attempts: {e}") raise RuntimeError(f"Gemini failed after 3 attempts: {e}")
with engine.begin() as conn: if not data:
domain_embedding = model.encode(query).tolist() 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(""" domain_id = conn.execute(text("""
INSERT INTO domains (name, embedding) INSERT INTO domains (name, embedding)
VALUES (:n, CAST(:e AS vector)) VALUES (:n, CAST(:e AS vector))
@ -167,11 +215,11 @@ def bootstrap_domain(query: str):
"""), {"d": domain_id, "g": group}).scalar() """), {"d": domain_id, "g": group}).scalar()
for attr in attrs: for attr in attrs:
emb = model.encode(attr).tolist() emb = model_local.encode(attr).tolist()
conn.execute(text(""" conn.execute(text("""
INSERT INTO attributes (group_id, name, embedding) INSERT INTO attributes (group_id, name, embedding)
VALUES (:gid, :name, CAST(:emb AS vector)) VALUES (:gid, :name, CAST(:emb AS vector))
ON CONFLICT (group_id, name) DO NOTHING ON CONFLICT (group_id, name) DO NOTHING
"""), {"gid": group_id, "name": attr, "emb": str(emb)}) """), {"gid": group_id, "name": attr, "emb": str(emb)})
print(f"Domain bootstrapped: {query}") print(f"Domain bootstrapped: {query} ({total_attrs} attributes)")

@ -1,10 +1,12 @@
from sqlalchemy import create_engine, text from sqlalchemy import create_engine, text
import os
try: try:
from app.db_schema import ensure_schema from app.db_schema import ensure_schema
except ModuleNotFoundError: except ModuleNotFoundError:
from db_schema import ensure_schema 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: with engine.connect() as conn:
ensure_schema(conn) ensure_schema(conn)

@ -1,9 +1,6 @@
from sqlalchemy import text from sqlalchemy import text
from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.exc import SQLAlchemyError
def ensure_schema(engine) -> bool: def ensure_schema(engine) -> bool:
with engine.connect().execution_options(isolation_level="AUTOCOMMIT") as conn: with engine.connect().execution_options(isolation_level="AUTOCOMMIT") as conn:
conn.execute(text("CREATE EXTENSION IF NOT EXISTS vector")) 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("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 return True

@ -11,7 +11,7 @@ def get_or_encode(query: str, model) -> list:
otherwise encodes with model and caches it. otherwise encodes with model and caches it.
Always call this with the NORMALIZED query. Always call this with the NORMALIZED query.
""" """
key = f"emb:{query}" key = f"emb:v1:{query}"
# Check Redis first # Check Redis first
cached = redis_client.get(key) cached = redis_client.get(key)

@ -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")

@ -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()

@ -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}")

@ -15,7 +15,12 @@ import requests as http_requests
import os import os
import re import re
import time import time
import json as json_module
import hashlib
import redis as redis_lib
from app.vertex_client import get_access_token from app.vertex_client import get_access_token
from app.logger import log_event
from fastapi import Request
load_dotenv() load_dotenv()
@ -29,17 +34,37 @@ app.add_middleware(
allow_headers=["*"], allow_headers=["*"],
) )
DATABASE_URL = "postgresql://postgres:postgres@localhost:5432/decision_engine" DATABASE_URL = os.getenv("DATABASE_URL")
engine = create_engine(DATABASE_URL) engine = create_engine(DATABASE_URL)
model = SentenceTransformer("all-MiniLM-L6-v2") model = SentenceTransformer("all-MiniLM-L6-v2")
GOOGLE_PROJECT_ID = "sylvan-deck-387207" GOOGLE_PROJECT_ID = "sylvan-deck-387207"
LOCATION = "us-central1" 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 ────────────────────────────────────────── # ── Startup ──────────────────────────────────────────
ensure_schema(engine) ensure_schema(engine)
create_index_if_not_exists() 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 ──────────────────────────────── # ── Async cache writer ────────────────────────────────
@ -57,10 +82,9 @@ def is_gibberish(text: str) -> bool:
words = text.lower().split() words = text.lower().split()
if not words: if not words:
return True return True
gibberish_count = 0 gibberish_count = 0
for word in words: for word in words:
if len(word) <= 3: # allow short words: ev, ai, bmw, suv if len(word) <= 3:
continue continue
if re.search(r'[^aeiou]{6,}', word): if re.search(r'[^aeiou]{6,}', word):
gibberish_count += 1 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): if re.search(r'[a-z]\d{3,}[a-z]|[0-9]{4,}[a-z]', word):
gibberish_count += 1 gibberish_count += 1
continue continue
return gibberish_count > len(words) / 2 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 ───────────────────────────────── # ── /suggest endpoint ─────────────────────────────────
@app.get("/suggest") @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: if len(query.strip()) < 2:
return {"suggestions": [], "cache": "skip"} return build_response({"suggestions": []}, "skip")
# Block gibberish server-side
if is_gibberish(query.strip()): if is_gibberish(query.strip()):
return {"suggestions": [], "cache": "gibberish"} return build_response({"suggestions": []}, "gibberish")
normalized = normalize_query(query.strip()) # Category → AI directly
print(f"📝 Normalized: '{query}''{normalized}'") if category != "general":
result = generate_category_suggestions(query.strip(), category, [], limit)
embedding = get_or_encode(normalized, model) return build_response(result, "category_ai")
word_count = len(query.strip().split())
# Use semantic cache only for full queries (3+ words) on first page normalized = normalize_query(query.strip())
if word_count >= 3 and offset == 0: print(f"Normalized: '{query}''{normalized}'")
cached = get_semantic_cache(embedding, domain=normalized)
if cached: # ── Semantic cache check ──────────────────────────
print(f"✅ Semantic cache HIT") try:
return {"suggestions": cached[offset:offset + limit], "cache": "semantic_hit"} embedding = get_or_encode(normalized, model)
emb_param = str(embedding)
emb_param = str(embedding)
with engine.begin() as conn: with engine.begin() as conn:
domain_row = conn.execute(text(""" domain_row = conn.execute(text("""
SELECT id, name, SELECT id, name,
embedding <-> CAST(:emb AS vector) AS distance embedding <-> CAST(:emb AS vector) AS distance
FROM domains FROM domains ORDER BY distance LIMIT 1
ORDER BY distance """), {"emb": emb_param}).fetchone()
LIMIT 1
"""), {"emb": emb_param}).fetchone() if domain_row and domain_row.distance <= 0.8:
domain_name = domain_row.name
if domain_row is None or domain_row.distance > 0.8:
if not is_gibberish(query): if offset == 0:
try: cached = get_semantic_cache(embedding, domain=domain_name)
bootstrap_domain(query) if cached:
print(f"✅ Bootstrapped: {query}") distance = cached["distance"]
except Exception as e: cached_query = cached["query"]
print(f"⚠️ Bootstrap failed: {e}") suggestions = cached["suggestions"]
print(f"Cache found → distance: {distance:.4f}, query: {cached_query}")
domain_row = conn.execute(text("""
SELECT id, name def is_relevant(q, cq):
FROM domains q_words = set(q.lower().split())
ORDER BY embedding <-> CAST(:emb AS vector) c_words = set(cq.lower().split())
LIMIT 1 overlap = len(q_words & c_words) / max(len(q_words), 1)
"""), {"emb": emb_param}).fetchone() return overlap > 0.5
if domain_row is None: if distance < 0.05 and is_relevant(normalized, cached_query):
return {"suggestions": [], "cache": "no_domain"} print("✅ Strong semantic cache HIT")
# ✅ continues below to fetch attributes return build_response(
else: {"suggestions": suggestions[:limit], "domain": domain_name},
return {"suggestions": [], "cache": "no_domain"} "strong_hit"
)
results = conn.execute(text(""" elif distance < 0.1 and is_relevant(normalized, cached_query):
SELECT a.name, print("⚡ Medium match → refining")
1 - (a.embedding <-> CAST(:emb AS vector)) AS score fresh_results = conn.execute(text("""
FROM attributes a SELECT a.name,
JOIN dimension_groups g ON a.group_id = g.id 1 - (a.embedding <-> CAST(:emb AS vector)) AS score
WHERE g.domain_id = :domain_id FROM attributes a
ORDER BY score DESC JOIN dimension_groups g ON a.group_id = g.id
LIMIT :limit OFFSET :offset WHERE g.domain_id = :domain_id
"""), {"emb": emb_param, "domain_id": domain_row.id, ORDER BY score DESC
"limit": limit, "offset": offset}) LIMIT :limit
"""), {"emb": emb_param, "domain_id": domain_row.id, "limit": limit})
suggestions = [r[0] for r in results] fresh = [r[0] for r in fresh_results]
domain_name = domain_row.name merged = []
seen = set()
# Deduplicate for item in suggestions + fresh:
seen = set() if item.lower() not in seen:
ranked = [] seen.add(item.lower())
for name in suggestions: merged.append(item)
if name.lower() not in seen: return build_response(
seen.add(name.lower()) {"suggestions": merged[:limit], "domain": domain_name},
ranked.append(name) "refined_hit"
)
# Cache only full queries on first page else:
if word_count >= 3 and offset == 0: print("❌ Cache ignored (low relevance)")
write_cache_async(normalized, embedding, ranked, domain=domain_name)
results = conn.execute(text("""
return {"suggestions": ranked, "cache": "miss", "domain": domain_name} 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 ──────────────────────────────── # ── /generate endpoint ────────────────────────────────
class GenerateRequest(BaseModel): class GenerateRequest(BaseModel):
query: str query: str
selected_attributes: list[str] selected_attributes: list[str]
category: str = "general"
chat_history: list[dict] = [] chat_history: list[dict] = []
@ -181,46 +762,48 @@ def generate(request: GenerateRequest):
attributes = ", ".join(request.selected_attributes) if request.selected_attributes else "general evaluation" attributes = ", ".join(request.selected_attributes) if request.selected_attributes else "general evaluation"
prompt = f"""{history_text}USER QUESTION: "{request.query}" category_focus = {
EVALUATION CRITERIA SELECTED: {attributes} "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.",
You are an expert advisor. The user has specifically asked about "{request.query}". "videos": "Focus on video reviews, test drives, comparisons, what reviewers say.",
Your job is to answer the user's question "{request.query}" and analyze it through each of the selected criteria. "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. prompt = f"""{history_text}USER QUESTION: "{request.query}"
Then for each criterion in [{attributes}], explain how it applies specifically to "{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} ## 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]** **[Criterion Name]**
- How this applies to "{request.query}" - How it applies to "{request.query}"
- Specific facts, numbers, or data - Specific facts or numbers
- Recommendation - Recommendation
--- ---
## Bottom Line ## 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 - 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 - Use real numbers where confident
- Total response under 400 words""" - If unsure say "verify on official website"
- Total 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"
print(f"🔄 Calling Vertex AI for: {request.query}") print(f"🔄 Calling Vertex AI for: {request.query}")
for attempt in range(3): for attempt in range(3):
try: try:
res = http_requests.post( res = http_requests.post(
url, GEMINI_URL,
headers={ headers={
"Authorization": f"Bearer {get_access_token()}", "Authorization": f"Bearer {get_access_token()}",
"Content-Type": "application/json" "Content-Type": "application/json"
@ -231,25 +814,47 @@ STRICT RULES:
}, },
timeout=30 timeout=30
) )
if res.status_code == 429: if res.status_code == 429:
wait = 2 ** attempt wait = 2 ** attempt
print(f"⏳ Rate limited, waiting {wait}s...") print(f"⏳ Rate limited, waiting {wait}s...")
time.sleep(wait) time.sleep(wait)
continue continue
print(f"✅ Vertex response: {res.status_code}") print(f"✅ Vertex response: {res.status_code}")
res.raise_for_status() res.raise_for_status()
answer = res.json()["candidates"][0]["content"]["parts"][0]["text"] answer = res.json()["candidates"][0]["content"]["parts"][0]["text"]
return {"answer": answer} return {"answer": answer}
except http_requests.exceptions.Timeout: except http_requests.exceptions.Timeout:
print(f"⏰ Timeout on attempt {attempt + 1}") print(f"⏰ Timeout on attempt {attempt + 1}")
if attempt == 2: if attempt == 2:
return {"answer": "Request timed out. Please try again."} return {"answer": "Request timed out. Please try again."}
time.sleep(2) time.sleep(2)
except Exception as e: except Exception as e:
print(f"❌ Error: {e}") print(f"❌ Error: {type(e).__name__}")
return {"answer": "Error getting response. Please try again."} return {"answer": "Error getting response. Please try again."}
return {"answer": "Could not get response after 3 attempts. Please try again."} 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"}

@ -81,7 +81,10 @@ def get_semantic_cache(embedding: list, domain: str = None):
if distance < SIMILARITY_THRESHOLD: if distance < SIMILARITY_THRESHOLD:
print(f"Semantic cache HIT (distance: {distance:.4f}, domain: {domain})") 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})") print(f"Semantic cache MISS (distance: {distance:.4f})")
return None return None

@ -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"
}

@ -1,13 +1,22 @@
# app/vertex_client.py # app/vertex_client.py
from google.oauth2 import service_account from google.oauth2 import service_account
import google.auth.transport.requests import google.auth.transport.requests
import os
SERVICE_ACCOUNT_FILE = r"C:\Users\rithv\OneDrive\Desktop\decision_engine_project\app\service-account.json"
def get_access_token(): 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( credentials = service_account.Credentials.from_service_account_file(
SERVICE_ACCOUNT_FILE, creds_path,
scopes=["https://www.googleapis.com/auth/cloud-platform"] scopes=["https://www.googleapis.com/auth/cloud-platform"]
) )
credentials.refresh(google.auth.transport.requests.Request()) credentials.refresh(google.auth.transport.requests.Request())
return credentials.token return credentials.token

@ -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:

@ -2,84 +2,200 @@
<html> <html>
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>AI Prompt Compressor Engine</title> <title>AI Prompt Composer Engine</title>
<style> <style>
*{box-sizing:border-box;margin:0;padding:0} *{box-sizing:border-box;margin:0;padding:0}
body{font-family:Arial;background:#f5f5f5;padding:40px} body{font-family:Arial;background:#f5f5f5;padding:40px}
.container{max-width:900px;margin:auto} .container{max-width:960px;margin:auto}
h1{margin-bottom:20px} h1{margin-bottom:20px}
.prompt-row{display:flex;gap:8px;width:100%} .prompt-row{display:flex;gap:8px;width:100%}
.prompt-row input{flex:1;padding:10px;border:1px solid #ccc;border-radius:8px;font-size:14px} .prompt-row input{flex:1;padding:10px;border:1px solid #ccc;border-radius:8px;font-size:14px}
.extra-row{margin-top:10px} .extra-row{margin-top:10px;display:flex;gap:8px}
.extra-row input{width:100%;padding:10px;border:1px solid #ccc;border-radius:8px;font-size:13px} .extra-row input{flex:1;padding:10px;border:1px solid #ccc;border-radius:8px;font-size:13px}
button{padding:10px 16px;border:none;border-radius:8px;background:black;color:white;cursor:pointer;font-size:13px} button{padding:10px 16px;border:none;border-radius:8px;background:black;color:white;cursor:pointer;font-size:13px}
button.secondary{background:#555} button.secondary{background:#555}
button:disabled{background:#999;cursor:not-allowed} button:disabled{background:#999;cursor:not-allowed}
.keyword-box{margin-top:20px;padding:16px;background:white;border-radius:10px;border:1px solid #ddd;min-height:80px;width:100%} .clear-field-btn{padding:4px 10px;border:1px solid #ddd;border-radius:6px;background:white;color:#999;cursor:pointer;font-size:12px;transition:all .15s;flex-shrink:0}
.keyword-box b{display:block;margin-bottom:10px;font-size:14px} .clear-field-btn:hover{border-color:#ef4444;color:#ef4444;background:#fff5f5}
.selected-area{margin-top:16px;padding:12px 16px;background:white;border-radius:10px;border:1px solid #ddd;display:none} .selected-area{margin-top:16px;padding:12px 16px;background:white;border-radius:10px;border:1px solid #ddd;display:none}
.selected-area b{display:block;margin-bottom:8px;font-size:14px}
/* ── Category Tabs ── */
.category-tabs{display:flex;gap:8px;margin-top:20px;flex-wrap:wrap}
.cat-tab{display:flex;align-items:center;gap:6px;padding:7px 14px;border-radius:20px;border:1px solid #ddd;background:white;cursor:pointer;font-size:13px;color:#555;transition:all .15s;user-select:none}
.cat-tab:hover{border-color:#333;color:#111}
.cat-tab.active{background:#111;color:white;border-color:#111}
/* ── Keyword box ── */
.keyword-box{margin-top:12px;padding:16px;background:white;border-radius:10px;border:1px solid #ddd;min-height:80px}
.box-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px}
.box-title{font-size:14px;font-weight:bold}
.category-badge{font-size:11px;padding:3px 10px;border-radius:20px;background:#f0f0f0;color:#555}
.category-badge.images{background:#fef3c7;color:#d97706}
.category-badge.shopping{background:#d1fae5;color:#065f46}
.category-badge.videos{background:#fee2e2;color:#991b1b}
.category-badge.news{background:#dbeafe;color:#1e40af}
.category-badge.places{background:#ede9fe;color:#5b21b6}
.category-badge.general{background:#f0f0f0;color:#555}
/* ── Chips ── */
.chip{display:inline-block;padding:7px 14px;margin:4px;border-radius:20px;border:1px solid #ccc;cursor:pointer;background:#fafafa;font-size:13px;transition:all .15s} .chip{display:inline-block;padding:7px 14px;margin:4px;border-radius:20px;border:1px solid #ccc;cursor:pointer;background:#fafafa;font-size:13px;transition:all .15s}
.chip:hover{border-color:#000;background:#f0f0f0} .chip:hover{border-color:#000;background:#f0f0f0}
.chip.selected{background:#16a34a;color:white;border-color:#16a34a} .chip.selected{background:#16a34a;color:white;border-color:#16a34a}
.chip.top{border-color:#f59e0b;background:#fffbeb;font-weight:600} .chip.top{border-color:#f59e0b;background:#fffbeb;font-weight:600}
.chip.top:hover{background:#fef3c7}
.chip.top.selected{background:#16a34a;border-color:#16a34a;color:white} .chip.top.selected{background:#16a34a;border-color:#16a34a;color:white}
.kw-loading-spinner{display:flex;align-items:center;gap:8px;color:#999;font-size:13px;padding:8px 0} .kw-loading-spinner{display:flex;align-items:center;gap:8px;color:#999;font-size:13px;padding:8px 0}
.spinner{width:16px;height:16px;border:2px solid #ddd;border-top:2px solid #333;border-radius:50%;animation:spin .8s linear infinite;flex-shrink:0} .spinner{width:16px;height:16px;border:2px solid #ddd;border-top:2px solid #333;border-radius:50%;animation:spin .8s linear infinite;flex-shrink:0}
@keyframes spin{to{transform:rotate(360deg)}} @keyframes spin{to{transform:rotate(360deg)}}
.kw-msg{color:#999;font-size:13px;padding:8px 0;display:block} .kw-msg{color:#999;font-size:13px;padding:8px 0;display:block}
.load-more-btn{display:inline-block;padding:6px 14px;margin:6px 4px;border-radius:20px;border:1px dashed #ccc;background:white;color:#666;font-size:12px;cursor:pointer;transition:all .2s} .load-more-btn{display:inline-block;padding:6px 14px;margin:6px 4px;border-radius:20px;border:1px dashed #ccc;background:white;color:#666;font-size:12px;cursor:pointer;transition:all .2s}
.load-more-btn:hover{border-color:#333;color:#333;border-style:solid} .load-more-btn:hover{border-color:#333;color:#333;border-style:solid}
.load-more-btn:disabled{color:#bbb;cursor:not-allowed} .load-more-btn:disabled{color:#bbb;cursor:not-allowed}
/* ── Results box ── */
.results-box{margin-top:16px;display:none}
.results-box.visible{display:block}
.results-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px}
.results-title{font-size:14px;font-weight:bold;color:#333}
.results-grid{display:grid;gap:12px;margin-top:8px}
/* Video card */
.video-card{background:white;border:1px solid #ddd;border-radius:10px;overflow:hidden;cursor:pointer;transition:transform .15s,box-shadow .15s;text-decoration:none;color:inherit;display:block}
.video-card:hover{transform:translateY(-2px);box-shadow:0 4px 16px rgba(0,0,0,.1)}
.video-card img{width:100%;height:150px;object-fit:cover}
.video-card .vc-info{padding:10px}
.video-card .vc-title{font-size:13px;font-weight:600;margin-bottom:4px;line-height:1.4}
.video-card .vc-channel{font-size:11px;color:#888}
.video-card .vc-date{font-size:10px;color:#aaa;margin-top:2px}
/* Shopping card */
.shop-card{background:white;border:1px solid #ddd;border-radius:10px;padding:14px;cursor:pointer;transition:transform .15s,box-shadow .15s;text-decoration:none;color:inherit;display:block}
.shop-card:hover{transform:translateY(-2px);box-shadow:0 4px 16px rgba(0,0,0,.1)}
.shop-card img{width:100%;height:120px;object-fit:contain;margin-bottom:8px;border-radius:6px;background:#f5f5f5}
.shop-card .sc-title{font-size:13px;font-weight:600;margin-bottom:4px;line-height:1.4}
.shop-card .sc-source{font-size:11px;color:#16a34a;font-weight:500}
.shop-card .sc-snippet{font-size:11px;color:#666;margin-top:4px;line-height:1.4}
/* Image card */
.image-card{background:white;border:1px solid #ddd;border-radius:10px;overflow:hidden;cursor:pointer;transition:transform .15s;text-decoration:none;display:block}
.image-card:hover{transform:scale(1.02)}
.image-card img{width:100%;height:160px;object-fit:cover}
.image-card .ic-title{font-size:11px;color:#666;padding:6px 10px}
/* News card */
.news-card{background:white;border:1px solid #ddd;border-radius:10px;padding:14px;display:flex;gap:12px;text-decoration:none;color:inherit;transition:box-shadow .15s}
.news-card:hover{box-shadow:0 4px 16px rgba(0,0,0,.1)}
.news-card img{width:80px;height:80px;object-fit:cover;border-radius:6px;flex-shrink:0}
.news-card .nc-info{flex:1}
.news-card .nc-title{font-size:13px;font-weight:600;line-height:1.4;margin-bottom:4px}
.news-card .nc-source{font-size:11px;color:#1e40af;font-weight:500}
.news-card .nc-snippet{font-size:11px;color:#666;margin-top:4px;line-height:1.4}
/* Places card */
.place-card{background:white;border:1px solid #ddd;border-radius:10px;padding:14px;display:flex;gap:12px;text-decoration:none;color:inherit;transition:box-shadow .15s}
.place-card:hover{box-shadow:0 4px 16px rgba(0,0,0,.1)}
.place-card img{width:70px;height:70px;object-fit:cover;border-radius:6px;flex-shrink:0;background:#f5f5f5}
.place-card .pc-info{flex:1}
.place-card .pc-title{font-size:13px;font-weight:600;margin-bottom:4px}
.place-card .pc-source{font-size:11px;color:#5b21b6;font-weight:500}
.place-card .pc-snippet{font-size:11px;color:#666;margin-top:4px;line-height:1.4}
/* ── Actions ── */
.actions{margin-top:16px;display:flex;gap:8px;align-items:center} .actions{margin-top:16px;display:flex;gap:8px;align-items:center}
/* ── Chat ── */
.chat-block{margin-top:24px;background:white;padding:20px;border-radius:10px;border:1px solid #ddd} .chat-block{margin-top:24px;background:white;padding:20px;border-radius:10px;border:1px solid #ddd}
.chat-block .go-label{font-weight:bold;font-size:15px;margin-bottom:10px} .go-label{font-weight:bold;font-size:15px;margin-bottom:10px}
.chat-block .prompt-line{margin:4px 0;font-size:14px;color:#333} .prompt-line{margin:4px 0;font-size:14px;color:#333}
.chat-block .kw-line{margin:4px 0;font-size:13px;color:#555} .kw-line{margin:4px 0;font-size:13px;color:#555}
.cat-line{margin:4px 0;font-size:12px;color:#888}
.answer{background:#f1f1f1;padding:14px;border-radius:8px;margin-top:12px;line-height:1.7;font-size:14px} .answer{background:#f1f1f1;padding:14px;border-radius:8px;margin-top:12px;line-height:1.7;font-size:14px}
.answer strong{color:#111} .answer strong{color:#111}
.answer h2{font-size:15px;margin:10px 0 6px} .answer h2{font-size:15px;margin:10px 0 6px}
/* ── Skeleton shimmer ── */
.chip.skeleton{
background:linear-gradient(90deg,#f0f0f0 25%,#e0e0e0 50%,#f0f0f0 75%);
background-size:200% 100%;
animation:shimmer 1.2s infinite;
border-color:transparent;
cursor:default;
min-width:60px;
height:34px;
}
@keyframes shimmer{
0%{background-position:200% 0}
100%{background-position:-200% 0}
}
</style> </style>
</head> </head>
<body> <body>
<div class="container"> <div class="container">
<h1>AI Prompt Compressor Engine</h1> <h1>AI Prompt Composer Engine</h1>
<!-- Prompt -->
<div class="prompt-row"> <div class="prompt-row">
<input type="text" id="prompt" placeholder="Type your prompt or topic..." /> <input type="text" id="prompt" placeholder="Type your prompt or topic..." />
<button type="button" class="clear-field-btn" onclick="clearPrompt()"></button>
<button type="button" id="goBtn" onclick="askAI()">GO</button> <button type="button" id="goBtn" onclick="askAI()">GO</button>
</div> </div>
<!-- Extra -->
<div class="extra-row"> <div class="extra-row">
<input type="text" id="extra" placeholder="Add extra instruction to refine the prompt (optional)" /> <input type="text" id="extra" placeholder="Add extra instruction (optional)" />
<button type="button" class="clear-field-btn" onclick="clearExtra()"></button>
</div> </div>
<!-- Selected keywords -->
<div class="selected-area" id="selectedArea"> <div class="selected-area" id="selectedArea">
<b>Selected Keywords</b> <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
<b>Selected Keywords</b>
<button type="button" class="clear-field-btn" onclick="clearSelected()">✕ Clear</button>
</div>
<div id="selectedKeywords"></div> <div id="selectedKeywords"></div>
</div> </div>
<!-- Category Tabs -->
<div class="category-tabs">
<div class="cat-tab active" data-cat="general" onclick="selectCategory('general')">🔍 General</div>
<div class="cat-tab" data-cat="shopping" onclick="selectCategory('shopping')">🛍️ Shopping</div>
<div class="cat-tab" data-cat="images" onclick="selectCategory('images')">🖼️ Images</div>
<div class="cat-tab" data-cat="videos" onclick="selectCategory('videos')">🎬 Videos</div>
<div class="cat-tab" data-cat="news" onclick="selectCategory('news')">📰 News</div>
<div class="cat-tab" data-cat="places" onclick="selectCategory('places')">📍 Places</div>
</div>
<!-- Suggestions -->
<div class="keyword-box"> <div class="keyword-box">
<b>Suggestions</b> <div class="box-header">
<span class="box-title">Suggestions</span>
<div style="display:flex;align-items:center;gap:8px">
<span class="category-badge general" id="categoryBadge">General</span>
<button type="button" class="clear-field-btn" onclick="clearSuggestions()"></button>
</div>
</div>
<div id="keywords"><span class="kw-msg">Start typing to see suggestions...</span></div> <div id="keywords"><span class="kw-msg">Start typing to see suggestions...</span></div>
</div> </div>
<!-- Results box -->
<div class="results-box" id="resultsBox">
<div class="results-header">
<span class="results-title" id="resultsTitle"></span>
</div>
<div class="results-grid" id="resultsGrid"></div>
</div>
<!-- Actions -->
<div class="actions"> <div class="actions">
<button type="button" id="askBtn" onclick="askAI()">Ask AI</button> <button type="button" id="askBtn" onclick="askAI()">Ask AI</button>
<button type="button" class="secondary" onclick="clearAll()">Clear</button> <button type="button" class="secondary" onclick="clearAll()">Clear All</button>
</div> </div>
<div id="chat"></div> <div id="chat"></div>
@ -92,114 +208,239 @@ button:disabled{background:#999;cursor:not-allowed}
let debounceTimer; let debounceTimer;
let currentQuery = ""; let currentQuery = "";
let currentOffset = 0; let currentOffset = 0;
let lastFetchedQuery = ""; // tracks last fetched query to avoid duplicate calls let lastFetchedQuery = "";
let currentCategory = "general";
const promptInput = document.getElementById("prompt"); const promptInput = document.getElementById("prompt");
const keywordsDiv = document.getElementById("keywords"); const keywordsDiv = document.getElementById("keywords");
const selectedDiv = document.getElementById("selectedKeywords"); const selectedDiv = document.getElementById("selectedKeywords");
const selectedArea = document.getElementById("selectedArea"); const selectedArea = document.getElementById("selectedArea");
// ── Gibberish detector ── const CATEGORIES = {
function isGibberish(text) { general: { label: "General", badge: "general", icon: "🔍" },
const words = text.toLowerCase().split(/\s+/); shopping: { label: "Shopping", badge: "shopping", icon: "🛍️" },
let gibberishCount = 0; images: { label: "Images", badge: "images", icon: "🖼️" },
videos: { label: "Videos", badge: "videos", icon: "🎬" },
for (const word of words) { news: { label: "News", badge: "news", icon: "📰" },
if (word.length <= 3) continue; // allow short words like ev, ai, bmw 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; // ── Select category ──
if (word.length > 5 && vowels / word.length < 0.1) { gibberishCount++; continue; } 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 = `<div class="kw-loading-spinner"><div class="spinner"></div><span>Loading ${CATEGORIES[cat].label} suggestions...</span></div>`;
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; // ── Typing listener ──
}
// ── Listen to typing ──
promptInput.addEventListener("input", () => { promptInput.addEventListener("input", () => {
clearTimeout(debounceTimer); clearTimeout(debounceTimer);
const q = promptInput.value.trim(); const q = promptInput.value.trim();
currentQuery = q; currentQuery = q;
if (q.length < 3) { if (q.length < 4) {
keywordsDiv.innerHTML = '<span class="kw-msg">Keep typing...</span>'; keywordsDiv.innerHTML = '<span class="kw-msg">Keep typing...</span>';
document.getElementById("resultsBox").classList.remove("visible");
return; return;
} }
if (isGibberish(q)) { if (isGibberish(q)) {
keywordsDiv.innerHTML = '<span class="kw-msg">⚠️ Please enter a meaningful topic.</span>'; keywordsDiv.innerHTML = '<span class="kw-msg">⚠️ Please enter a meaningful topic.</span>';
return; return;
} }
keywordsDiv.innerHTML = ` if (!keywordsDiv.querySelector('.chip')) {
<div class="kw-loading-spinner"> showSkeletonChips();
<div class="spinner"></div> }
<span>Loading suggestions for "<b>${q}</b>"...</span>
</div>
`;
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 = "&nbsp;";
keywordsDiv.appendChild(sk);
}
}
// ── Fetch suggestions ── // ── Fetch suggestions ──
async function fetchKeywords(query) { async function fetchKeywords(query) {
// Skip if same query already fetched — avoids duplicate API calls const cacheKey = `${query}__${currentCategory}`;
if (query === lastFetchedQuery) return; if (cacheKey === lastFetchedQuery) return;
lastFetchedQuery = query; lastFetchedQuery = cacheKey;
currentOffset = 0; currentOffset = 0;
showSkeletonChips();
try { 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(); const data = await res.json();
// Backend detected gibberish
if (data.cache === "gibberish") { if (data.cache === "gibberish") {
keywordsDiv.innerHTML = '<span class="kw-msg">⚠️ Please enter a meaningful topic.</span>'; keywordsDiv.innerHTML = '<span class="kw-msg">⚠️ Please enter a meaningful topic.</span>';
return; return;
} }
if (!data.suggestions || !data.suggestions.length) {
// No domain found or query skipped
if (data.cache === "skip" || data.cache === "no_domain" || !data.suggestions || !data.suggestions.length) {
keywordsDiv.innerHTML = '<span class="kw-msg">No suggestions found. Try a more specific topic.</span>'; keywordsDiv.innerHTML = '<span class="kw-msg">No suggestions found. Try a more specific topic.</span>';
return; return;
} }
renderChips(data.suggestions, false); renderChips(data.suggestions, false);
} catch (e) {
keywordsDiv.innerHTML = '<span class="kw-msg">Backend not reachable.</span>';
}
}
// ── 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 => `
<a href="${v.url}" target="_blank" class="video-card">
<img src="${v.thumbnail}" alt="${escHtml(v.title)}" onerror="this.style.display='none'"/>
<div class="vc-info">
<div class="vc-title">${escHtml(v.title)}</div>
<div class="vc-channel">📺 ${escHtml(v.channel)}</div>
<div class="vc-date">${v.published}</div>
</div>
</a>`).join("");
} else if (category === "shopping") {
resultsGrid.style.gridTemplateColumns = "repeat(auto-fill, minmax(220px, 1fr))";
resultsGrid.innerHTML = data.results.map(s => `
<a href="${s.link}" target="_blank" class="shop-card">
${s.image ? `<img src="${s.image}" alt="" onerror="this.style.display='none'"/>` : ""}
<div class="sc-title">${escHtml(s.title)}</div>
<div class="sc-source">🔗 ${escHtml(s.source)}</div>
<div class="sc-snippet">${escHtml(s.snippet)}</div>
</a>`).join("");
} else if (category === "images") {
resultsGrid.style.gridTemplateColumns = "repeat(auto-fill, minmax(180px, 1fr))";
resultsGrid.innerHTML = data.results.map(img => `
<a href="${img.context_link}" target="_blank" class="image-card">
<img src="${img.thumbnail}" alt="${escHtml(img.title)}" onerror="this.background='#eee'"/>
<div class="ic-title">${escHtml(img.source)}</div>
</a>`).join("");
} else if (category === "news") {
resultsGrid.style.gridTemplateColumns = "1fr";
resultsGrid.innerHTML = data.results.map(n => `
<a href="${n.link}" target="_blank" class="news-card">
${n.image ? `<img src="${n.image}" alt="" onerror="this.style.display='none'"/>` : ""}
<div class="nc-info">
<div class="nc-title">${escHtml(n.title)}</div>
<div class="nc-source">📰 ${escHtml(n.source)}</div>
<div class="nc-snippet">${escHtml(n.snippet)}</div>
</div>
</a>`).join("");
} else if (category === "places") {
resultsGrid.style.gridTemplateColumns = "1fr";
resultsGrid.innerHTML = data.results.map(p => `
<a href="${p.link}" target="_blank" class="place-card">
${p.image ? `<img src="${p.image}" alt="" onerror="this.style.display='none'"/>` : ""}
<div class="pc-info">
<div class="pc-title">${escHtml(p.title)}</div>
<div class="pc-source">📍 ${escHtml(p.source)}</div>
<div class="pc-snippet">${escHtml(p.snippet)}</div>
</div>
</a>`).join("");
}
} catch (e) { } catch (e) {
keywordsDiv.innerHTML = '<span class="kw-msg">Backend not reachable — is the server running?</span>'; resultsBox.classList.remove("visible");
} }
} }
// ── Render chips ── // ── Render chips ──
function renderChips(items, append) { function renderChips(items, append) {
if (!append) { if (!append) { keywordsDiv.innerHTML = ""; currentOffset = 15; }
keywordsDiv.innerHTML = ""; else { const old = document.getElementById("loadMoreBtn"); if (old) old.remove(); }
currentOffset = 15;
} else {
const old = document.getElementById("loadMoreBtn");
if (old) old.remove();
}
if (!items.length && !append) { if (!items.length && !append) {
keywordsDiv.innerHTML = '<span class="kw-msg">No suggestions found.</span>'; keywordsDiv.innerHTML = '<span class="kw-msg">No suggestions found.</span>';
return; return;
} }
items.forEach((k, i) => { items.forEach((k, i) => {
const chip = document.createElement("span"); const chip = document.createElement("span");
chip.className = "chip" + (selected.includes(k) ? " selected" : ""); 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.innerText = k;
chip.dataset.kw = k; chip.dataset.kw = k;
chip.onclick = () => toggleChip(k, chip); chip.onclick = () => toggleChip(k, chip);
keywordsDiv.appendChild(chip); keywordsDiv.appendChild(chip);
}); });
// Load more button
const btn = document.createElement("button"); const btn = document.createElement("button");
btn.id = "loadMoreBtn"; btn.id = "loadMoreBtn";
btn.className = "load-more-btn"; btn.className = "load-more-btn";
@ -208,7 +449,7 @@ button:disabled{background:#999;cursor:not-allowed}
keywordsDiv.appendChild(btn); keywordsDiv.appendChild(btn);
} }
// ── Toggle chip selection ── // ── Toggle chip ──
function toggleChip(k, chipEl) { function toggleChip(k, chipEl) {
if (selected.includes(k)) { if (selected.includes(k)) {
selected = selected.filter(x => x !== k); selected = selected.filter(x => x !== k);
@ -223,10 +464,7 @@ button:disabled{background:#999;cursor:not-allowed}
// ── Render selected keywords ── // ── Render selected keywords ──
function renderSelected() { function renderSelected() {
selectedDiv.innerHTML = ""; selectedDiv.innerHTML = "";
if (!selected.length) { if (!selected.length) { selectedArea.style.display = "none"; return; }
selectedArea.style.display = "none";
return;
}
selectedArea.style.display = "block"; selectedArea.style.display = "block";
selected.forEach(k => { selected.forEach(k => {
const chip = document.createElement("span"); const chip = document.createElement("span");
@ -234,22 +472,27 @@ button:disabled{background:#999;cursor:not-allowed}
chip.innerText = k + " ✕"; chip.innerText = k + " ✕";
chip.onclick = () => { chip.onclick = () => {
selected = selected.filter(x => x !== k); selected = selected.filter(x => x !== k);
const kwChip = keywordsDiv.querySelector(`[data-kw="${k}"]`); const kw = keywordsDiv.querySelector(`[data-kw="${k}"]`);
if (kwChip) kwChip.classList.remove("selected"); if (kw) kw.classList.remove("selected");
renderSelected(); renderSelected();
}; };
selectedDiv.appendChild(chip); selectedDiv.appendChild(chip);
}); });
} }
// ── Load more suggestions ── // ── Load more ──
async function loadMore() { async function loadMore() {
const btn = document.getElementById("loadMoreBtn"); const btn = document.getElementById("loadMoreBtn");
btn.innerText = "Loading..."; btn.innerText = "Loading...";
btn.disabled = true; btn.disabled = true;
try { 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(); const data = await res.json();
if (!data.suggestions || !data.suggestions.length) { if (!data.suggestions || !data.suggestions.length) {
@ -257,18 +500,13 @@ button:disabled{background:#999;cursor:not-allowed}
return; return;
} }
const existing = Array.from(keywordsDiv.querySelectorAll(".chip")) const newItems = data.suggestions.filter(
.map(c => c.dataset.kw?.toLowerCase()).filter(Boolean); s => !existing.map(e => e.toLowerCase()).includes(s.toLowerCase())
const newItems = data.suggestions.filter(s => !existing.includes(s.toLowerCase())); );
if (!newItems.length) {
btn.innerText = "No more suggestions";
return;
}
if (!newItems.length) { btn.innerText = "No more suggestions"; return; }
currentOffset += 10; currentOffset += 10;
renderChips(newItems, true); renderChips(newItems, true);
} catch (e) { } catch (e) {
btn.innerText = "+ More"; btn.innerText = "+ More";
btn.disabled = false; btn.disabled = false;
@ -286,15 +524,16 @@ button:disabled{background:#999;cursor:not-allowed}
document.getElementById("askBtn").disabled = true; document.getElementById("askBtn").disabled = true;
step++; step++;
const chat = document.getElementById("chat"); const chat = document.getElementById("chat");
const block = document.createElement("div"); const block = document.createElement("div");
block.className = "chat-block"; block.className = "chat-block";
const catInfo = CATEGORIES[currentCategory];
block.innerHTML = ` block.innerHTML = `
<div class="go-label">${step}. Go</div> <div class="go-label">${step}. Go</div>
<div class="prompt-line"><b>Prompt:</b> ${escHtml(combined)}</div> <div class="prompt-line"><b>Prompt:</b> ${escHtml(combined)}</div>
<div class="cat-line"><b>Category:</b> ${catInfo.icon} ${catInfo.label}</div>
<div class="kw-line"><b>Keywords:</b> ${selected.length ? selected.map(escHtml).join(", ") : "<i>none selected</i>"}</div> <div class="kw-line"><b>Keywords:</b> ${selected.length ? selected.map(escHtml).join(", ") : "<i>none selected</i>"}</div>
<div class="answer"><i>⏳ AI is generating answer...</i></div> <div class="answer"><i>⏳ AI is generating answer...</i></div>`;
`;
chat.appendChild(block); chat.appendChild(block);
block.scrollIntoView({ behavior: "smooth" }); block.scrollIntoView({ behavior: "smooth" });
@ -306,6 +545,7 @@ button:disabled{background:#999;cursor:not-allowed}
body: JSON.stringify({ body: JSON.stringify({
query: combined, query: combined,
selected_attributes: selected, selected_attributes: selected,
category: currentCategory,
chat_history: chatHistory chat_history: chatHistory
}) })
}); });
@ -315,46 +555,74 @@ button:disabled{background:#999;cursor:not-allowed}
answer = "Backend not running. Start the server on port 8000."; 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 = `<b>AI Answer</b><br/><br/>${formatAnswer(answer)}`; block.querySelector(".answer").innerHTML = `<b>AI Answer</b><br/><br/>${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 = ""; document.getElementById("extra").value = "";
selected = [];
lastFetchedQuery = ""; // reset so next query fetches fresh
renderSelected();
keywordsDiv.innerHTML = '<span class="kw-msg">Start typing to see suggestions...</span>';
document.getElementById("goBtn").disabled = false; document.getElementById("goBtn").disabled = false;
document.getElementById("askBtn").disabled = false; document.getElementById("askBtn").disabled = false;
promptInput.focus(); promptInput.focus();
} }
// ── Clear all ── // ── Clear functions ──
function clearAll() { function clearAll() {
promptInput.value = ""; promptInput.value = "";
document.getElementById("extra").value = ""; document.getElementById("extra").value = "";
selected = []; selected = []; chatHistory = []; step = 0; lastFetchedQuery = ""; currentCategory = "general";
chatHistory = []; document.querySelectorAll(".cat-tab").forEach(t => t.classList.remove("active"));
step = 0; document.querySelector("[data-cat='general']").classList.add("active");
lastFetchedQuery = ""; const badge = document.getElementById("categoryBadge");
badge.textContent = "General"; badge.className = "category-badge general";
renderSelected(); renderSelected();
keywordsDiv.innerHTML = '<span class="kw-msg">Start typing to see suggestions...</span>'; keywordsDiv.innerHTML = '<span class="kw-msg">Start typing to see suggestions...</span>';
document.getElementById("resultsBox").classList.remove("visible");
document.getElementById("chat").innerHTML = ""; document.getElementById("chat").innerHTML = "";
} }
// ── Format AI answer (markdown-like) ── function clearPrompt() {
promptInput.value = "";
keywordsDiv.innerHTML = '<span class="kw-msg">Start typing...</span>';
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 = '<span class="kw-msg">Start typing to see suggestions...</span>';
lastFetchedQuery = "";
}
// ── Format AI answer ──
function formatAnswer(text) { function formatAnswer(text) {
return text return text
.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;') .replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')
.replace(/## (.*)/g, '<h2>$1</h2>') .replace(/## (.*)/g,'<h2>$1</h2>')
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>') .replace(/\*\*(.*?)\*\*/g,'<strong>$1</strong>')
.replace(/\*(.*?)\*/g, '<em>$1</em>') .replace(/\*(.*?)\*/g,'<em>$1</em>')
.replace(/---/g, '<hr style="border:none;border-top:1px solid #ddd;margin:10px 0"/>') .replace(/---/g,'<hr style="border:none;border-top:1px solid #ddd;margin:10px 0"/>')
.replace(/\n\n+/g, '</p><p>') .replace(/\n\n+/g,'</p><p>')
.replace(/\n/g, '<br/>') .replace(/\n/g,'<br/>')
.replace(/^/, '<p>').replace(/$/, '</p>'); .replace(/^/,'<p>').replace(/$/,'</p>');
} }
function escHtml(t) { function escHtml(t) {

@ -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"}

@ -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
Loading…
Cancel
Save