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

27
.gitignore vendored

@ -1,5 +1,32 @@
# Virtual environments
venv/
seed/
.venv/
# Environment & secrets
.env
.env.*
app/service-account.json
# Python cache
__pycache__/
*.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 google.auth
import google.auth.transport.requests
import os
from app.vertex_client import get_access_token
load_dotenv()
DATABASE_URL = "postgresql://postgres:postgres@localhost:5432/decision_engine"
DATABASE_URL = os.getenv("DATABASE_URL")
engine = create_engine(DATABASE_URL)
model = SentenceTransformer("all-MiniLM-L6-v2")
@ -42,31 +41,41 @@ def clean_json_response(raw: str) -> str:
raise ValueError("No JSON object found in response")
def validate_schema(data: dict) -> dict:
"""
Validate and clean Gemini response.
- Only keep valid categories
- Only keep string attributes
- Remove empty categories
- Cap at 5 attributes per category
"""
def validate_schema(data: dict, query: str) -> dict:
cleaned = {}
query_words = set(query.lower().split())
# Define topic-aware category relevance
IRRELEVANT_COMBOS = {
"shoes": ["processor", "ram", "gpu", "cpu", "battery", "charging"],
"food": ["processor", "gpu", "engine", "torque", "horsepower"],
"college": ["torque", "engine", "gpu", "charging speed"],
}
# Get blocked terms for this query
blocked = []
for topic, terms in IRRELEVANT_COMBOS.items():
if topic in query.lower():
blocked.extend(terms)
for category, attributes in data.items():
# Normalize category name
cat = category.strip().title()
# Skip invalid categories
if cat not in VALID_CATEGORIES:
print(f"Skipping unknown category: {cat}")
continue
# Only keep string attributes
attrs = [a.strip() for a in attributes if isinstance(a, str) and a.strip()]
# Cap at 5 per category
attrs = attrs[:6]
attrs = []
for a in attributes:
if not isinstance(a, str) or not a.strip():
continue
# ✅ Reject attributes containing blocked terms
a_lower = a.lower()
if any(b in a_lower for b in blocked):
print(f"❌ Rejected irrelevant attribute: {a}")
continue
attrs.append(a.strip())
# Skip empty categories
attrs = attrs[:12]
if attrs:
cleaned[cat] = attrs
@ -77,57 +86,75 @@ def validate_schema(data: dict) -> dict:
def call_gemini(query: str) -> dict:
prompt = f"""You are a world-class decision analysis expert.
Task: Generate the MOST IMPORTANT and FREQUENTLY USED evaluation criteria for someone making a decision about: "{query}"
Task: Generate a COMPREHENSIVE list of evaluation criteria for: "{query}"
Rules:
- Only include criteria that are HIGHLY RELEVANT to "{query}"
- Prioritize criteria that people MOST COMMONLY consider for this topic
- Each attribute must be SPECIFIC and MEASURABLE, not generic
- Order attributes by importance (most important first)
- Use concise names (2-5 words max per attribute)
MANDATORY RULES:
- Select 6-8 categories from the allowed list
- Generate EXACTLY 8-10 attributes per category this is mandatory
- Each attribute must be SPECIFIC and MEASURABLE for "{query}"
- First 3 attributes in EVERY category must be the MOST SEARCHED specs
- For tech products: always start with Processor, RAM, Battery, Display, Camera
- For vehicles: always start with Engine, Mileage, Price, Safety
- Order strictly by: most searched most compared most reviewed
- Use concise names (2-5 words max)
- DO NOT generate less than 8 attributes per category
Output format: Pure JSON only. No markdown. No explanation.
Use ONLY these category keys: Performance, Financial, Risk, Maintenance, Benefits, Time, Requirements, Scalability, Alternatives, Usability, Security, Reliability, Support, Sustainability
Allowed category keys:
Performance, Financial, Risk, Maintenance, Benefits, Time, Requirements,
Scalability, Alternatives, Usability, Security, Reliability, Support, Sustainability
Example for "buying a car":
{{"Performance":["Engine Power","Top Speed","0-100 Acceleration","Fuel Efficiency"],"Financial":["Purchase Price","Insurance Cost","Resale Value","Running Cost"],"Maintenance":["Service Interval","Spare Parts Availability","Warranty Period"]}}
Example of CORRECT format with enough attributes:
{{"Performance":["Engine Power","Torque Output","Top Speed","0-100 kmph Time","Fuel Efficiency","Gear Smoothness","Braking Distance","Tyre Grip","Suspension Quality","NVH Levels"],"Financial":["Ex-showroom Price","On-road Price","EMI Options","Insurance Cost","Fuel Cost Monthly","Resale Value","Maintenance Cost","Road Tax","Accessories Cost","Total Ownership Cost"],"Reliability":["Engine Reliability","Electrical Issues","Common Problems","Long Term Durability","Brand Track Record","Owner Satisfaction","Recall History","Service Quality","Spare Parts Life","Warranty Claims"]}}
Now generate for: "{query}"
Return ONLY the JSON object."""
Return ONLY valid JSON. No markdown. No explanation. Minimum 8 attributes per category."""
url = f"https://{LOCATION}-aiplatform.googleapis.com/v1/projects/{PROJECT_ID}/locations/{LOCATION}/publishers/google/models/gemini-2.5-flash-lite:generateContent"
for attempt in range(3):
try:
res = http_requests.post(url,
headers={
"Authorization": f"Bearer {get_access_token()}",
"Content-Type": "application/json"},
json={
"contents": [{"role" : "user","parts": [{"text": prompt}]}],
"generationConfig": {"temperature": 0.2, "maxOutputTokens": 1024}})
# Handle rate limit with exponential backoff
res = http_requests.post(
url,
headers={
"Authorization": f"Bearer {get_access_token()}",
"Content-Type": "application/json"
},
json={
"contents": [{"role": "user", "parts": [{"text": prompt}]}],
"generationConfig": {
"temperature": 0.1, # lower = more accurate, less random
"maxOutputTokens": 1024
}
},
timeout=30
)
if res.status_code == 429:
wait = 2 ** attempt # 1s, 2s, 4s
print(f" Rate limited, waiting {wait}s... (attempt {attempt + 1})")
wait = 2 ** attempt
print(f"Rate limited, waiting {wait}s... (attempt {attempt + 1})")
time.sleep(wait)
continue
res.raise_for_status()
raw = res.json()["candidates"][0]["content"]["parts"][0]["text"]
print("Status Code:", res.status_code)
if res.status_code != 200:
print("❌ ERROR RESPONSE:")
print(res.text)
raise RuntimeError("Vertex API failed")
data_json = res.json()
print("✅ RAW RESPONSE:", str(data_json)[:500])
raw = res.json()["candidates"][0]["content"]["parts"][0]["text"]
clean = clean_json_response(raw)
data = json.loads(clean)
validated = validate_schema(data)
validated = validate_schema(data,query)
print(f"Gemini generated {sum(len(v) for v in validated.values())} attributes across {len(validated)} categories")
return validated
except (json.JSONDecodeError, ValueError) as e:
print(f" Attempt {attempt + 1} failed: {e}")
print(f"Attempt {attempt + 1} failed: {e}")
if attempt == 2:
raise RuntimeError(f"Gemini failed after 3 attempts: {e}")
@ -135,8 +162,6 @@ Return ONLY the JSON object."""
def bootstrap_domain(query: str):
# Retry up to 3 times if Gemini returns bad JSON
data = None
for attempt in range(3):
try:
@ -148,9 +173,32 @@ def bootstrap_domain(query: str):
if attempt == 2:
raise RuntimeError(f"Gemini failed after 3 attempts: {e}")
with engine.begin() as conn:
domain_embedding = model.encode(query).tolist()
if not data:
raise RuntimeError("No data generated")
# ✅ Quality gate — reject low quality bootstraps
total_attrs = sum(len(v) for v in data.values())
if total_attrs < 10:
raise ValueError(f"Quality gate failed: only {total_attrs} attributes generated")
# ✅ Duplicate domain detection — check similarity before storing
model_local = SentenceTransformer("all-MiniLM-L6-v2")
domain_embedding = model_local.encode(query).tolist()
with engine.begin() as conn:
# Check if very similar domain already exists
existing = conn.execute(text("""
SELECT name, embedding <-> CAST(:emb AS vector) AS distance
FROM domains
ORDER BY distance
LIMIT 1
"""), {"emb": str(domain_embedding)}).fetchone()
if existing and existing.distance < 0.15:
print(f"⚠️ Similar domain already exists: '{existing.name}' (distance: {existing.distance:.3f}) — skipping bootstrap")
return # ✅ Don't store duplicate
# Store domain
domain_id = conn.execute(text("""
INSERT INTO domains (name, embedding)
VALUES (:n, CAST(:e AS vector))
@ -167,11 +215,11 @@ def bootstrap_domain(query: str):
"""), {"d": domain_id, "g": group}).scalar()
for attr in attrs:
emb = model.encode(attr).tolist()
emb = model_local.encode(attr).tolist()
conn.execute(text("""
INSERT INTO attributes (group_id, name, embedding)
VALUES (:gid, :name, CAST(:emb AS vector))
ON CONFLICT (group_id, name) DO NOTHING
"""), {"gid": group_id, "name": attr, "emb": str(emb)})
print(f"Domain bootstrapped: {query}")
print(f"Domain bootstrapped: {query} ({total_attrs} attributes)")

@ -1,10 +1,12 @@
from sqlalchemy import create_engine, text
import os
try:
from app.db_schema import ensure_schema
except ModuleNotFoundError:
from db_schema import ensure_schema
engine = create_engine("postgresql://postgres:postgres@localhost:5432/decision_engine")
DATABASE_URL = os.getenv("DATABASE_URL")
engine = create_engine(DATABASE_URL)
with engine.connect() as conn:
ensure_schema(conn)

@ -1,9 +1,6 @@
from sqlalchemy import text
from sqlalchemy.exc import SQLAlchemyError
def ensure_schema(engine) -> bool:
with engine.connect().execution_options(isolation_level="AUTOCOMMIT") as conn:
conn.execute(text("CREATE EXTENSION IF NOT EXISTS vector"))
@ -42,4 +39,23 @@ def ensure_schema(engine) -> bool:
"""))
conn.execute(text("ALTER TABLE attributes DROP CONSTRAINT IF EXISTS attributes_name_key"))
conn.execute(text("""
CREATE TABLE IF NOT EXISTS attribute_feedback (
id SERIAL PRIMARY KEY,
query TEXT NOT NULL,
attribute_name TEXT NOT NULL,
domain TEXT,
click_count INTEGER DEFAULT 1,
last_clicked TIMESTAMP DEFAULT NOW(),
UNIQUE(query, attribute_name)
)
"""))
conn.execute(text("""
ALTER TABLE domains
ADD COLUMN IF NOT EXISTS quality_score FLOAT DEFAULT 1.0,
ADD COLUMN IF NOT EXISTS created_at TIMESTAMP DEFAULT NOW(),
ADD COLUMN IF NOT EXISTS query_count INTEGER DEFAULT 0
"""))
return True

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

@ -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 re
import time
import json as json_module
import hashlib
import redis as redis_lib
from app.vertex_client import get_access_token
from app.logger import log_event
from fastapi import Request
load_dotenv()
@ -29,17 +34,37 @@ app.add_middleware(
allow_headers=["*"],
)
DATABASE_URL = "postgresql://postgres:postgres@localhost:5432/decision_engine"
DATABASE_URL = os.getenv("DATABASE_URL")
engine = create_engine(DATABASE_URL)
model = SentenceTransformer("all-MiniLM-L6-v2")
GOOGLE_PROJECT_ID = "sylvan-deck-387207"
LOCATION = "us-central1"
GOOGLE_SEARCH_API_KEY = os.getenv("GOOGLE_SEARCH_API_KEY")
GOOGLE_SEARCH_CX = os.getenv("GOOGLE_SEARCH_CX")
# Gemini URL
GEMINI_URL = f"https://{LOCATION}-aiplatform.googleapis.com/v1/projects/{GOOGLE_PROJECT_ID}/locations/{LOCATION}/publishers/google/models/gemini-2.5-flash-lite:generateContent"
# ── Startup ──────────────────────────────────────────
ensure_schema(engine)
create_index_if_not_exists()
print("✅ System ready")
print("System ready")
# ── Redis helper ──────────────────────────────────────
def get_redis():
try:
r = redis_lib.Redis(host='localhost', port=6379, decode_responses=True)
r.ping()
return r
except Exception:
return None
def get_live_cache_key(query: str) -> str:
word_count = len(query.strip().split())
prefix = "live:full" if word_count >= 3 else "live:short"
return f"{prefix}:{hashlib.md5(query.lower().strip().encode()).hexdigest()}"
# ── Async cache writer ────────────────────────────────
@ -57,10 +82,9 @@ def is_gibberish(text: str) -> bool:
words = text.lower().split()
if not words:
return True
gibberish_count = 0
for word in words:
if len(word) <= 3: # allow short words: ev, ai, bmw, suv
if len(word) <= 3:
continue
if re.search(r'[^aeiou]{6,}', word):
gibberish_count += 1
@ -72,97 +96,654 @@ def is_gibberish(text: str) -> bool:
if re.search(r'[a-z]\d{3,}[a-z]|[0-9]{4,}[a-z]', word):
gibberish_count += 1
continue
return gibberish_count > len(words) / 2
# ── Middleware ────────────────────────────────────────
@app.middleware("http")
async def log_requests(request, call_next):
start_time = time.time()
response = await call_next(request)
latency = int((time.time() - start_time) * 1000)
cache_status = getattr(request.state, "cache_status", "missing")
try:
query = request.query_params.get("query", "")
category = request.query_params.get("category", "general")
log_event({
"path": request.url.path,
"query": query,
"latency_ms": latency,
"status_code": response.status_code,
"category": category,
"cache_status": cache_status
})
print("CACHE:", cache_status)
except Exception as e:
print(f"Logging middleware error: {e}")
print("MIDDLEWARE sees:", getattr(request.state, "cache_status", "NOT_SET"))
return response
# ── Fast Gemini call ──────────────────────────────────
def call_gemini_fast(query: str, limit: int = 15) -> list:
prompt = f"""List {limit} specific evaluation criteria for: "{query}"
Rules:
- Specific to "{query}" only no competitors
- 2-5 words each
- Most important first
Return ONLY a JSON array. Example: ["Battery Life", "On Road Price", "Fuel Efficiency"]
Generate for: "{query}" """
for attempt in range(2):
try:
res = http_requests.post(
GEMINI_URL,
headers={
"Authorization": f"Bearer {get_access_token()}",
"Content-Type": "application/json"
},
json={
"contents": [{"role": "user", "parts": [{"text": prompt}]}],
"generationConfig": {
"temperature": 0.1,
"maxOutputTokens": 250 # small = fast
}
},
timeout=6
)
if res.status_code == 429:
time.sleep(2 ** attempt)
continue
res.raise_for_status()
raw = res.json()["candidates"][0]["content"]["parts"][0]["text"]
match = re.search(r'\[.*\]', raw, re.DOTALL)
if match:
suggestions = json_module.loads(match.group(0))
print(f"✅ Fast Gemini: {len(suggestions)} suggestions for '{query}'")
return suggestions
except http_requests.exceptions.Timeout:
print(f"⏰ Gemini timeout attempt {attempt+1}")
except Exception as e:
print(f"⚠️ Gemini attempt {attempt+1}: {type(e).__name__}")
time.sleep(1)
return []
def call_gemini_more(query: str, existing: list) -> list:
"""Second background call — 25 more suggestions, different ones"""
existing_str = ", ".join(existing[:15]) # send first 15 so Gemini avoids them
prompt = f"""List 25 more specific evaluation criteria for: "{query}"
Rules:
- Specific to "{query}" only
- 2-5 words each
- Do NOT repeat any of these: {existing_str}
- Different aspects not yet covered
- Most important first
Return ONLY a JSON array.
Generate for: "{query}" """
try:
res = http_requests.post(
GEMINI_URL,
headers={
"Authorization": f"Bearer {get_access_token()}",
"Content-Type": "application/json"
},
json={
"contents": [{"role": "user", "parts": [{"text": prompt}]}],
"generationConfig": {
"temperature": 0.3,
"maxOutputTokens": 600
}
},
timeout=15
)
res.raise_for_status()
raw = res.json()["candidates"][0]["content"]["parts"][0]["text"]
match = re.search(r'\[.*\]', raw, re.DOTALL)
if match:
more = json_module.loads(match.group(0))
# filter out any duplicates
existing_lower = [e.lower() for e in existing]
return [s for s in more if s.lower() not in existing_lower]
except Exception as e:
print(f"⚠️ call_gemini_more failed: {type(e).__name__}")
return []
# ── Background store in Redis + DB ───────────────────
def store_in_background(query: str, suggestions: list, r=None, cache_key: str = None):
def _store():
if r and cache_key:
try:
ttl = 86400 if len(query.split()) >= 3 else 1800
r.setex(cache_key, ttl, json_module.dumps(suggestions))
print(f"💾 Redis cached: '{query}' ({len(suggestions)} items)")
except Exception:
pass
try:
bootstrap_domain(query)
print(f"✅ DB bootstrapped: '{query}'")
if r and cache_key:
try:
r.delete(cache_key)
print(f"🗑️ Redis cleared → DB now serves '{query}'")
except Exception:
pass
except Exception as e:
print(f"⚠️ Bootstrap failed: {type(e).__name__}")
threading.Thread(target=_store, daemon=True).start()
# ── Main get suggestions function ────────────────────
def get_suggestions_fast(query: str, limit: int = 15, existing: list = None) -> dict:
existing = existing or []
# ── L1: Redis cache ──────────────────────────────
r = get_redis()
cache_key = get_live_cache_key(query)
if r:
try:
cached = r.get(cache_key)
if cached:
all_suggestions = json_module.loads(cached)
new_only = [s for s in all_suggestions if s not in existing]
print(f"⚡ Redis HIT: '{query}'{len(new_only)} available")
return {"suggestions": new_only[:limit], "cache": "redis_hit"}
except Exception:
pass
# ── L2: DB (pgvector) ────────────────────────────
try:
normalized = normalize_query(query.strip())
embedding = get_or_encode(normalized, model)
emb_param = str(embedding)
with engine.begin() as conn:
domain_row = conn.execute(text("""
SELECT id, name,
embedding <-> CAST(:emb AS vector) AS distance
FROM domains ORDER BY distance LIMIT 1
"""), {"emb": emb_param}).fetchone()
if domain_row and domain_row.distance <= 0.8:
results = conn.execute(text("""
SELECT a.name,
(1 - (a.embedding <-> CAST(:emb AS vector))) * 0.7
+ LEAST(COALESCE(af.click_count, 0), 100) * 0.003 AS score
FROM attributes a
JOIN dimension_groups g ON a.group_id = g.id
LEFT JOIN attribute_feedback af
ON af.attribute_name = a.name
AND af.domain = :domain_name
WHERE g.domain_id = :domain_id
ORDER BY score DESC
LIMIT :limit
"""), {"emb": emb_param, "domain_id": domain_row.id,
"domain_name": domain_row.name, "limit": limit * 2})
db_suggestions = [row[0] for row in results]
new_only = [s for s in db_suggestions if s not in existing]
if new_only:
print(f"⚡ DB HIT: '{query}'{len(new_only)} suggestions")
if r:
try:
r.setex(cache_key, 3600, json_module.dumps(db_suggestions))
except Exception:
pass
return {"suggestions": new_only[:limit], "cache": "db_hit",
"domain": domain_row.name}
except Exception as e:
print(f"⚠️ DB check failed: {type(e).__name__}")
# ── L3: Gemini fast (new query) ──────────────────
print(f"🔄 New query — calling Gemini fast: '{query}'")
suggestions = call_gemini_fast(query, limit=15)
if suggestions:
# Background: fetch 25 more + store everything in Redis
def fetch_more_and_store():
more = call_gemini_more(query, suggestions)
all_suggestions = suggestions + more
print(f"✅ Total suggestions after background fetch: {len(all_suggestions)}")
store_in_background(query, all_suggestions, r, cache_key)
threading.Thread(target=fetch_more_and_store, daemon=True).start()
new_only = [s for s in suggestions if s not in existing]
return {"suggestions": new_only[:limit], "cache": "gemini_fast"}
return {"suggestions": [], "cache": "gemini_failed"}
# ── Category suggestions via Gemini ──────────────────
def generate_category_suggestions(query: str, category: str, existing: list, limit: int = 15):
category_context = {
"shopping": "buying options, best price, where to buy, deals, discounts, EMI, cashback, online vs offline, delivery, sellers",
"images": "exterior design, interior photos, color options, visual features, styling, aesthetics, dimensions look, photo gallery",
"videos": "video reviews, test drive videos, owner reviews, comparison videos, YouTube channels, expert opinions, unboxing",
"news": "latest news, recent updates, new model announcement, price change, upcoming launch, recalls, awards won",
"places": "best dealers, showrooms near me, service centers, test drive locations, authorized dealers, city availability"
}
context = category_context.get(category, "general evaluation")
prompt = f"""The user is searching for "{query}" in {category.upper()} category.
Generate {limit} specific keyword suggestions related to "{query}" for the {category} context.
Focus on: {context}
Rules:
- Each suggestion must be 2-5 words
- Must be directly related to "{query}" in {category} context
- Order by most searched/relevant first
Return ONLY a JSON array of strings. No explanation."""
for attempt in range(3):
try:
res = http_requests.post(
GEMINI_URL,
headers={
"Authorization": f"Bearer {get_access_token()}",
"Content-Type": "application/json"
},
json={
"contents": [{"role": "user", "parts": [{"text": prompt}]}],
"generationConfig": {"temperature": 0.4, "maxOutputTokens": 512}
},
timeout=30
)
if res.status_code == 429:
time.sleep(2 ** attempt)
continue
res.raise_for_status()
raw = res.json()["candidates"][0]["content"]["parts"][0]["text"]
match = re.search(r'\[.*\]', raw, re.DOTALL)
if match:
suggestions = json_module.loads(match.group(0))
return {"suggestions": suggestions[:limit], "cache": "category_ai"}
return {"suggestions": [], "cache": "parse_error"}
except http_requests.exceptions.ConnectionError as e:
print(f"⚠️ Connection error attempt {attempt + 1}: {type(e).__name__}")
if attempt < 2:
time.sleep(2)
continue
return {"suggestions": [], "cache": "connection_error"}
except Exception as e:
print(f"❌ Category suggestions failed: {type(e).__name__}")
return {"suggestions": [], "cache": "error"}
return {"suggestions": [], "cache": "failed"}
# ── serve_from_db ─────────────────────────────────────
def serve_from_db(conn, domain_row, emb_param, limit, offset):
results = conn.execute(text("""
SELECT a.name,
(1 - (a.embedding <-> CAST(:emb AS vector))) * 0.7
+ LEAST(COALESCE(af.click_count, 0), 100) * 0.003 AS score
FROM attributes a
JOIN dimension_groups g ON a.group_id = g.id
LEFT JOIN attribute_feedback af ON af.attribute_name = a.name
WHERE g.domain_id = :domain_id
ORDER BY score DESC
LIMIT :limit OFFSET :offset
"""), {"emb": emb_param, "domain_id": domain_row.id,
"limit": limit, "offset": offset})
return [r[0] for r in results]
# ── /suggest endpoint ─────────────────────────────────
@app.get("/suggest")
def suggest(query: str, offset: int = 0, limit: int = 15):
def suggest(request: Request, query: str, offset: int = 0, limit: int = 15, category: str = "general"):
def build_response(data, status):
request.state.cache_status = status
data["cache"] = status
return data
start = time.time()
if len(query.strip()) < 2:
return {"suggestions": [], "cache": "skip"}
return build_response({"suggestions": []}, "skip")
# Block gibberish server-side
if is_gibberish(query.strip()):
return {"suggestions": [], "cache": "gibberish"}
return build_response({"suggestions": []}, "gibberish")
normalized = normalize_query(query.strip())
print(f"📝 Normalized: '{query}''{normalized}'")
embedding = get_or_encode(normalized, model)
word_count = len(query.strip().split())
# Category → AI directly
if category != "general":
result = generate_category_suggestions(query.strip(), category, [], limit)
return build_response(result, "category_ai")
# Use semantic cache only for full queries (3+ words) on first page
if word_count >= 3 and offset == 0:
cached = get_semantic_cache(embedding, domain=normalized)
if cached:
print(f"✅ Semantic cache HIT")
return {"suggestions": cached[offset:offset + limit], "cache": "semantic_hit"}
emb_param = str(embedding)
with engine.begin() as conn:
domain_row = conn.execute(text("""
SELECT id, name,
embedding <-> CAST(:emb AS vector) AS distance
FROM domains
ORDER BY distance
LIMIT 1
"""), {"emb": emb_param}).fetchone()
if domain_row is None or domain_row.distance > 0.8:
if not is_gibberish(query):
try:
bootstrap_domain(query)
print(f"✅ Bootstrapped: {query}")
except Exception as e:
print(f"⚠️ Bootstrap failed: {e}")
domain_row = conn.execute(text("""
SELECT id, name
FROM domains
ORDER BY embedding <-> CAST(:emb AS vector)
LIMIT 1
"""), {"emb": emb_param}).fetchone()
if domain_row is None:
return {"suggestions": [], "cache": "no_domain"}
# ✅ continues below to fetch attributes
else:
return {"suggestions": [], "cache": "no_domain"}
results = conn.execute(text("""
SELECT a.name,
1 - (a.embedding <-> CAST(:emb AS vector)) AS score
FROM attributes a
JOIN dimension_groups g ON a.group_id = g.id
WHERE g.domain_id = :domain_id
ORDER BY score DESC
LIMIT :limit OFFSET :offset
"""), {"emb": emb_param, "domain_id": domain_row.id,
"limit": limit, "offset": offset})
suggestions = [r[0] for r in results]
domain_name = domain_row.name
# Deduplicate
seen = set()
ranked = []
for name in suggestions:
if name.lower() not in seen:
seen.add(name.lower())
ranked.append(name)
# Cache only full queries on first page
if word_count >= 3 and offset == 0:
write_cache_async(normalized, embedding, ranked, domain=domain_name)
return {"suggestions": ranked, "cache": "miss", "domain": domain_name}
normalized = normalize_query(query.strip())
print(f"Normalized: '{query}''{normalized}'")
# ── Semantic cache check ──────────────────────────
try:
embedding = get_or_encode(normalized, model)
emb_param = str(embedding)
with engine.begin() as conn:
domain_row = conn.execute(text("""
SELECT id, name,
embedding <-> CAST(:emb AS vector) AS distance
FROM domains ORDER BY distance LIMIT 1
"""), {"emb": emb_param}).fetchone()
if domain_row and domain_row.distance <= 0.8:
domain_name = domain_row.name
if offset == 0:
cached = get_semantic_cache(embedding, domain=domain_name)
if cached:
distance = cached["distance"]
cached_query = cached["query"]
suggestions = cached["suggestions"]
print(f"Cache found → distance: {distance:.4f}, query: {cached_query}")
def is_relevant(q, cq):
q_words = set(q.lower().split())
c_words = set(cq.lower().split())
overlap = len(q_words & c_words) / max(len(q_words), 1)
return overlap > 0.5
if distance < 0.05 and is_relevant(normalized, cached_query):
print("✅ Strong semantic cache HIT")
return build_response(
{"suggestions": suggestions[:limit], "domain": domain_name},
"strong_hit"
)
elif distance < 0.1 and is_relevant(normalized, cached_query):
print("⚡ Medium match → refining")
fresh_results = conn.execute(text("""
SELECT a.name,
1 - (a.embedding <-> CAST(:emb AS vector)) AS score
FROM attributes a
JOIN dimension_groups g ON a.group_id = g.id
WHERE g.domain_id = :domain_id
ORDER BY score DESC
LIMIT :limit
"""), {"emb": emb_param, "domain_id": domain_row.id, "limit": limit})
fresh = [r[0] for r in fresh_results]
merged = []
seen = set()
for item in suggestions + fresh:
if item.lower() not in seen:
seen.add(item.lower())
merged.append(item)
return build_response(
{"suggestions": merged[:limit], "domain": domain_name},
"refined_hit"
)
else:
print("❌ Cache ignored (low relevance)")
results = conn.execute(text("""
SELECT a.name,
(1 - (a.embedding <-> CAST(:emb AS vector))) * 0.7
+ LEAST(COALESCE(af.click_count, 0), 100) * 0.003 AS score
FROM attributes a
JOIN dimension_groups g ON a.group_id = g.id
LEFT JOIN attribute_feedback af
ON af.attribute_name = a.name
AND af.domain = :domain_name
WHERE g.domain_id = :domain_id
ORDER BY score DESC
LIMIT :limit OFFSET :offset
"""), {"emb": emb_param, "domain_id": domain_row.id,
"domain_name": domain_name, "limit": limit, "offset": offset})
suggestions = [r[0] for r in results]
seen = set()
ranked = []
for name in suggestions:
if name.lower() not in seen:
seen.add(name.lower())
ranked.append(name)
if ranked:
if offset == 0 and len(normalized.split()) >= 2:
write_cache_async(normalized, embedding, ranked, domain=domain_name)
print(f"⚡ DB HIT: '{query}'{len(ranked)} suggestions in {int((time.time()-start)*1000)}ms")
return build_response(
{"suggestions": ranked, "domain": domain_name},
"db_hit"
)
except Exception as e:
print(f"⚠️ DB/cache check failed: {type(e).__name__}")
# ── Fast Gemini fallback ──────────────────────────
print(f"🔄 Fast Gemini for: '{query}'")
result = get_suggestions_fast(query, limit=40)
return build_response(result, result.get("cache", "gemini_fast"))
# ── /suggest/more endpoint ────────────────────────────
@app.get("/suggest/more")
def suggest_more(query: str, category: str = "general", existing: str = ""):
existing_list = [e.strip() for e in existing.split(",") if e.strip()]
if category == "general":
r = get_redis()
if r:
try:
cache_key = get_live_cache_key(query)
cached = r.get(cache_key)
if cached:
all_suggestions = json_module.loads(cached)
new_only = [s for s in all_suggestions if s not in existing_list]
print(f"⚡ Instant more: {len(new_only)} from cache")
return {"suggestions": new_only[:12], "cache": "live_hit"}
except Exception:
pass
result = get_suggestions_fast(query, limit=12, existing=existing_list)
return result
category_context = {
"shopping": "focus on buying, pricing, deals, offers, sellers, delivery, payment options",
"images": "focus on visual aspects, design, appearance, colors, aesthetics, photos",
"videos": "focus on reviews, test drives, comparisons, tutorials, unboxing, demos",
"news": "focus on latest updates, recent changes, announcements, trends, events",
"places": "focus on locations, dealers, service centers, showrooms, nearby options",
}
context = category_context.get(category, "general evaluation")
prompt = f"""Generate 12 more keyword suggestions for "{query}" in {category.upper()} context.
Focus on: {context}
Do NOT repeat: {', '.join(existing_list) if existing_list else 'none'}
Each suggestion 2-5 words max.
Return ONLY a JSON array."""
try:
res = http_requests.post(
GEMINI_URL,
headers={"Authorization": f"Bearer {get_access_token()}", "Content-Type": "application/json"},
json={"contents": [{"role": "user", "parts": [{"text": prompt}]}],
"generationConfig": {"temperature": 0.5, "maxOutputTokens": 300}},
timeout=15)
res.raise_for_status()
raw = res.json()["candidates"][0]["content"]["parts"][0]["text"]
match = re.search(r'\[.*\]', raw, re.DOTALL)
if match:
suggestions = json_module.loads(match.group(0))
return {"suggestions": [s for s in suggestions if s not in existing_list][:12],
"cache": "ai_generated"}
return {"suggestions": [], "cache": "parse_error"}
except Exception as e:
print(f"❌ suggest/more failed: {type(e).__name__}")
return {"suggestions": [], "cache": "error"}
# ── /search/videos endpoint ───────────────────────────
@app.get("/search/videos")
def search_videos(query: str):
if not GOOGLE_SEARCH_API_KEY:
return {"results": [], "error": "API key not configured"}
try:
url = "https://www.googleapis.com/youtube/v3/search"
params = {
"part": "snippet",
"q": query,
"type": "video",
"maxResults": 6,
"order": "relevance",
"key": GOOGLE_SEARCH_API_KEY
}
res = http_requests.get(url, params=params, timeout=10)
res.raise_for_status()
data = res.json()
results = []
for item in data.get("items", []):
snippet = item["snippet"]
video_id = item["id"]["videoId"]
results.append({
"title": snippet["title"],
"channel": snippet["channelTitle"],
"thumbnail": snippet["thumbnails"]["medium"]["url"],
"url": f"https://www.youtube.com/watch?v={video_id}",
"published": snippet["publishedAt"][:10],
"description": snippet.get("description", "")[:120]
})
return {"results": results, "query": query}
except Exception as e:
print(f"❌ YouTube search failed: {type(e).__name__}")
return {"results": [], "error": "Search unavailable. Please try again."}
# ── /search/shopping endpoint ─────────────────────────
@app.get("/search/shopping")
def search_shopping(query: str):
if not GOOGLE_SEARCH_API_KEY or not GOOGLE_SEARCH_CX:
return {"results": [], "error": "Google Search API not configured"}
try:
url = "https://www.googleapis.com/customsearch/v1"
params = {
"q": f"{query} buy price",
"cx": GOOGLE_SEARCH_CX,
"key": GOOGLE_SEARCH_API_KEY,
"num": 6
}
res = http_requests.get(url, params=params, timeout=10)
res.raise_for_status()
data = res.json()
results = []
for item in data.get("items", []):
results.append({
"title": item.get("title", ""),
"link": item.get("link", ""),
"snippet": item.get("snippet", "")[:150],
"source": item.get("displayLink", ""),
"image": item.get("pagemap", {}).get("cse_image", [{}])[0].get("src", "")
})
return {"results": results, "query": query}
except Exception as e:
print(f"❌ Shopping search failed: {type(e).__name__}")
return {"results": [], "error": "Search unavailable. Please try again."}
# ── /search/images endpoint ───────────────────────────
@app.get("/search/images")
def search_images(query: str):
if not GOOGLE_SEARCH_API_KEY or not GOOGLE_SEARCH_CX:
return {"results": [], "error": "Google Search API not configured"}
try:
url = "https://www.googleapis.com/customsearch/v1"
params = {
"q": query,
"cx": GOOGLE_SEARCH_CX,
"key": GOOGLE_SEARCH_API_KEY,
"searchType": "image",
"num": 6,
"safe": "active"
}
res = http_requests.get(url, params=params, timeout=10)
res.raise_for_status()
data = res.json()
results = []
for item in data.get("items", []):
results.append({
"title": item.get("title", ""),
"link": item.get("link", ""),
"source": item.get("displayLink", ""),
"thumbnail": item.get("image", {}).get("thumbnailLink", ""),
"context_link": item.get("image", {}).get("contextLink", "")
})
return {"results": results, "query": query}
except Exception as e:
print(f"❌ Image search failed: {type(e).__name__}")
return {"results": [], "error": "Search unavailable. Please try again."}
# ── /search/news endpoint ─────────────────────────────
@app.get("/search/news")
def search_news(query: str):
if not GOOGLE_SEARCH_API_KEY or not GOOGLE_SEARCH_CX:
return {"results": [], "error": "Google Search API not configured"}
try:
url = "https://www.googleapis.com/customsearch/v1"
params = {
"q": f"{query} news 2025",
"cx": GOOGLE_SEARCH_CX,
"key": GOOGLE_SEARCH_API_KEY,
"num": 6,
"sort": "date"
}
res = http_requests.get(url, params=params, timeout=10)
res.raise_for_status()
data = res.json()
results = []
for item in data.get("items", []):
results.append({
"title": item.get("title", ""),
"link": item.get("link", ""),
"snippet": item.get("snippet", "")[:200],
"source": item.get("displayLink", ""),
"image": item.get("pagemap", {}).get("cse_image", [{}])[0].get("src", "")
})
return {"results": results, "query": query}
except Exception as e:
print(f"❌ News search failed: {type(e).__name__}")
return {"results": [], "error": "Search unavailable. Please try again."}
# ── /search/places endpoint ───────────────────────────
@app.get("/search/places")
def search_places(query: str):
if not GOOGLE_SEARCH_API_KEY or not GOOGLE_SEARCH_CX:
return {"results": [], "error": "Google Search API not configured"}
try:
url = "https://www.googleapis.com/customsearch/v1"
params = {
"q": f"{query} near me dealers showroom location",
"cx": GOOGLE_SEARCH_CX,
"key": GOOGLE_SEARCH_API_KEY,
"num": 6
}
res = http_requests.get(url, params=params, timeout=10)
res.raise_for_status()
data = res.json()
results = []
for item in data.get("items", []):
results.append({
"title": item.get("title", ""),
"link": item.get("link", ""),
"snippet": item.get("snippet", "")[:200],
"source": item.get("displayLink", ""),
"image": item.get("pagemap", {}).get("cse_image", [{}])[0].get("src", "")
})
return {"results": results, "query": query}
except Exception as e:
print(f"❌ Places search failed: {type(e).__name__}")
return {"results": [], "error": "Search unavailable. Please try again."}
# ── /generate endpoint ────────────────────────────────
class GenerateRequest(BaseModel):
query: str
selected_attributes: list[str]
category: str = "general"
chat_history: list[dict] = []
@ -181,46 +762,48 @@ def generate(request: GenerateRequest):
attributes = ", ".join(request.selected_attributes) if request.selected_attributes else "general evaluation"
prompt = f"""{history_text}USER QUESTION: "{request.query}"
EVALUATION CRITERIA SELECTED: {attributes}
You are an expert advisor. The user has specifically asked about "{request.query}".
Your job is to answer the user's question "{request.query}" and analyze it through each of the selected criteria.
category_focus = {
"shopping": "Focus on buying options, best prices, deals, where to buy, payment plans, EMI options.",
"images": "Focus on visual design, appearance, color options, exterior/interior aesthetics.",
"videos": "Focus on video reviews, test drives, comparisons, what reviewers say.",
"news": "Focus on latest news, recent updates, upcoming changes, current trends.",
"places": "Focus on best places to buy, dealers, showrooms, service centers.",
"general": ""
}.get(request.category, "")
Answer the question "{request.query}" directly first in 2-3 sentences.
Then for each criterion in [{attributes}], explain how it applies specifically to "{request.query}".
prompt = f"""{history_text}USER QUESTION: "{request.query}"
CATEGORY: {request.category.upper()}
{category_focus}
EVALUATION CRITERIA: {attributes}
Format your response as:
You are an expert advisor. Answer "{request.query}" directly and analyze through each selected criterion.
Format:
## About: {request.query}
[Direct answer to the user's question in 2-3 sentences]
[Direct answer in 2-3 sentences]
---
[For each selected criterion:]
**[Criterion Name]**
- How this applies to "{request.query}"
- Specific facts, numbers, or data
- How it applies to "{request.query}"
- Specific facts or numbers
- Recommendation
---
## Bottom Line
[2-3 sentence summary answering: should the user go with "{request.query}"? What should they prioritize?]
[2-3 sentence summary]
STRICT RULES:
RULES:
- Every sentence must be about "{request.query}" specifically
- Never give generic advice not related to "{request.query}"
- If unsure about a fact, say "verify on official website"
- Use real numbers where confident
- Total response under 400 words"""
url = f"https://{LOCATION}-aiplatform.googleapis.com/v1/projects/{GOOGLE_PROJECT_ID}/locations/{LOCATION}/publishers/google/models/gemini-2.5-flash-lite:generateContent"
- If unsure say "verify on official website"
- Total under 400 words"""
print(f"🔄 Calling Vertex AI for: {request.query}")
for attempt in range(3):
try:
res = http_requests.post(
url,
GEMINI_URL,
headers={
"Authorization": f"Bearer {get_access_token()}",
"Content-Type": "application/json"
@ -231,25 +814,47 @@ STRICT RULES:
},
timeout=30
)
if res.status_code == 429:
wait = 2 ** attempt
print(f"⏳ Rate limited, waiting {wait}s...")
time.sleep(wait)
continue
print(f"✅ Vertex response: {res.status_code}")
res.raise_for_status()
answer = res.json()["candidates"][0]["content"]["parts"][0]["text"]
return {"answer": answer}
except http_requests.exceptions.Timeout:
print(f"⏰ Timeout on attempt {attempt + 1}")
if attempt == 2:
return {"answer": "Request timed out. Please try again."}
time.sleep(2)
except Exception as e:
print(f"❌ Error: {e}")
print(f"❌ Error: {type(e).__name__}")
return {"answer": "Error getting response. Please try again."}
return {"answer": "Could not get response after 3 attempts. Please try again."}
# ── /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:
print(f"Semantic cache HIT (distance: {distance:.4f}, domain: {domain})")
return json.loads(top.suggestions)
return {
"query": top.query.decode() if isinstance(top.query, bytes) else top.query,
"distance": distance,
"suggestions": json.loads(top.suggestions)}
print(f"Semantic cache MISS (distance: {distance:.4f})")
return None

@ -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,12 +1,21 @@
# app/vertex_client.py
from google.oauth2 import service_account
import google.auth.transport.requests
SERVICE_ACCOUNT_FILE = r"C:\Users\rithv\OneDrive\Desktop\decision_engine_project\app\service-account.json"
import os
def get_access_token():
# Try environment variable first
creds_path = os.getenv("GOOGLE_APPLICATION_CREDENTIALS")
# Fallback to absolute path
if not creds_path or not os.path.exists(creds_path):
creds_path = r"C:\Users\rithv\OneDrive\Desktop\decision_engine_project\app\service-account.json"
if not os.path.exists(creds_path):
raise FileNotFoundError(f"Service account file not found at: {creds_path}")
credentials = service_account.Credentials.from_service_account_file(
SERVICE_ACCOUNT_FILE,
creds_path,
scopes=["https://www.googleapis.com/auth/cloud-platform"]
)
credentials.refresh(google.auth.transport.requests.Request())

@ -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>
<head>
<meta charset="UTF-8">
<title>AI Prompt Compressor Engine</title>
<title>AI Prompt Composer Engine</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:Arial;background:#f5f5f5;padding:40px}
.container{max-width:900px;margin:auto}
.container{max-width:960px;margin:auto}
h1{margin-bottom:20px}
.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}
.extra-row{margin-top:10px}
.extra-row input{width:100%;padding:10px;border:1px solid #ccc;border-radius:8px;font-size:13px}
.extra-row{margin-top:10px;display:flex;gap:8px}
.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.secondary{background:#555}
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%}
.keyword-box b{display:block;margin-bottom:10px;font-size:14px}
.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}
.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 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:hover{border-color:#000;background:#f0f0f0}
.chip.selected{background:#16a34a;color:white;border-color:#16a34a}
.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}
.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}
@keyframes spin{to{transform:rotate(360deg)}}
.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:hover{border-color:#333;color:#333;border-style:solid}
.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}
/* ── Chat ── */
.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}
.chat-block .prompt-line{margin:4px 0;font-size:14px;color:#333}
.chat-block .kw-line{margin:4px 0;font-size:13px;color:#555}
.go-label{font-weight:bold;font-size:15px;margin-bottom:10px}
.prompt-line{margin:4px 0;font-size:14px;color:#333}
.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 strong{color:#111}
.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>
</head>
<body>
<div class="container">
<h1>AI Prompt Compressor Engine</h1>
<h1>AI Prompt Composer Engine</h1>
<!-- Prompt -->
<div class="prompt-row">
<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>
</div>
<!-- Extra -->
<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>
<!-- Selected keywords -->
<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>
<!-- 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">
<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>
<!-- 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">
<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 id="chat"></div>
@ -92,114 +208,239 @@ button:disabled{background:#999;cursor:not-allowed}
let debounceTimer;
let currentQuery = "";
let currentOffset = 0;
let lastFetchedQuery = ""; // tracks last fetched query to avoid duplicate calls
let lastFetchedQuery = "";
let currentCategory = "general";
const promptInput = document.getElementById("prompt");
const keywordsDiv = document.getElementById("keywords");
const selectedDiv = document.getElementById("selectedKeywords");
const selectedArea = document.getElementById("selectedArea");
// ── Gibberish detector ──
function isGibberish(text) {
const words = text.toLowerCase().split(/\s+/);
let gibberishCount = 0;
for (const word of words) {
if (word.length <= 3) continue; // allow short words like ev, ai, bmw
const CATEGORIES = {
general: { label: "General", badge: "general", icon: "🔍" },
shopping: { label: "Shopping", badge: "shopping", icon: "🛍️" },
images: { label: "Images", badge: "images", icon: "🖼️" },
videos: { label: "Videos", badge: "videos", icon: "🎬" },
news: { label: "News", badge: "news", icon: "📰" },
places: { label: "Places", badge: "places", icon: "📍" }
};
if (word.match(/[^aeiou]{6,}/)) { gibberishCount++; continue; } // raised to 6
// ── Gibberish check ──
function isGibberish(text) {
const words = text.toLowerCase().split(/\s+/);
let gc = 0;
for (const w of words) {
if (w.length <= 3) continue;
if (w.match(/[^aeiou]{6,}/)) { gc++; continue; }
const v = (w.match(/[aeiou]/g) || []).length;
if (w.length > 5 && v / w.length < 0.1) { gc++; continue; }
if (/[a-z]\d{3,}[a-z]|[0-9]{4,}[a-z]/.test(w)) { gc++; continue; }
}
return gc > words.length / 2;
}
const vowels = (word.match(/[aeiou]/g) || []).length;
if (word.length > 5 && vowels / word.length < 0.1) { gibberishCount++; continue; }
// ── Select category ──
function selectCategory(cat) {
currentCategory = cat;
document.querySelectorAll(".cat-tab").forEach(t => t.classList.remove("active"));
document.querySelector(`[data-cat="${cat}"]`).classList.add("active");
const badge = document.getElementById("categoryBadge");
badge.textContent = CATEGORIES[cat].label;
badge.className = `category-badge ${cat}`;
lastFetchedQuery = "";
if (/[a-z]\d{3,}[a-z]|[0-9]{4,}[a-z]/.test(word)) { gibberishCount++; continue; }
const q = promptInput.value.trim();
if (q.length >= 3 && !isGibberish(q)) {
keywordsDiv.innerHTML = `<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;
}
// ── Listen to typing ──
// ── Typing listener ──
promptInput.addEventListener("input", () => {
clearTimeout(debounceTimer);
const q = promptInput.value.trim();
currentQuery = q;
if (q.length < 3) {
if (q.length < 4) {
keywordsDiv.innerHTML = '<span class="kw-msg">Keep typing...</span>';
document.getElementById("resultsBox").classList.remove("visible");
return;
}
if (isGibberish(q)) {
keywordsDiv.innerHTML = '<span class="kw-msg">⚠️ Please enter a meaningful topic.</span>';
return;
}
keywordsDiv.innerHTML = `
<div class="kw-loading-spinner">
<div class="spinner"></div>
<span>Loading suggestions for "<b>${q}</b>"...</span>
</div>
`;
if (!keywordsDiv.querySelector('.chip')) {
showSkeletonChips();
}
debounceTimer = setTimeout(() => fetchKeywords(q), 500);
debounceTimer = setTimeout(() => {
fetchKeywords(q);
if (currentCategory !== "general") fetchCategoryResults(q, currentCategory);
}, 1000);
});
// ── Skeleton chips ──
function showSkeletonChips() {
keywordsDiv.innerHTML = "";
for (let i = 0; i < 8; i++) {
const sk = document.createElement("span");
sk.className = "chip skeleton";
sk.style.width = `${60 + Math.random() * 60}px`;
sk.innerHTML = "&nbsp;";
keywordsDiv.appendChild(sk);
}
}
// ── Fetch suggestions ──
async function fetchKeywords(query) {
// Skip if same query already fetched — avoids duplicate API calls
if (query === lastFetchedQuery) return;
lastFetchedQuery = query;
const cacheKey = `${query}__${currentCategory}`;
if (cacheKey === lastFetchedQuery) return;
lastFetchedQuery = cacheKey;
currentOffset = 0;
showSkeletonChips();
try {
const res = await fetch(`http://127.0.0.1:8000/suggest?query=${encodeURIComponent(query)}&offset=0&limit=15`);
const url = `http://127.0.0.1:8000/suggest?query=${encodeURIComponent(query)}&offset=0&limit=15&category=${currentCategory}`;
const res = await fetch(url);
const data = await res.json();
// Backend detected gibberish
if (data.cache === "gibberish") {
keywordsDiv.innerHTML = '<span class="kw-msg">⚠️ Please enter a meaningful topic.</span>';
return;
}
// No domain found or query skipped
if (data.cache === "skip" || data.cache === "no_domain" || !data.suggestions || !data.suggestions.length) {
if (!data.suggestions || !data.suggestions.length) {
keywordsDiv.innerHTML = '<span class="kw-msg">No suggestions found. Try a more specific topic.</span>';
return;
}
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) {
keywordsDiv.innerHTML = '<span class="kw-msg">Backend not reachable — is the server running?</span>';
resultsBox.classList.remove("visible");
}
}
// ── Render chips ──
function renderChips(items, append) {
if (!append) {
keywordsDiv.innerHTML = "";
currentOffset = 15;
} else {
const old = document.getElementById("loadMoreBtn");
if (old) old.remove();
}
if (!append) { keywordsDiv.innerHTML = ""; currentOffset = 15; }
else { const old = document.getElementById("loadMoreBtn"); if (old) old.remove(); }
if (!items.length && !append) {
keywordsDiv.innerHTML = '<span class="kw-msg">No suggestions found.</span>';
return;
}
items.forEach((k, i) => {
const chip = document.createElement("span");
chip.className = "chip" + (selected.includes(k) ? " selected" : "");
if (!append && i < 5) chip.classList.add("top"); // top 5 highlighted in gold
if (!append && i < 5) chip.classList.add("top");
chip.innerText = k;
chip.dataset.kw = k;
chip.onclick = () => toggleChip(k, chip);
keywordsDiv.appendChild(chip);
});
// Load more button
const btn = document.createElement("button");
btn.id = "loadMoreBtn";
btn.className = "load-more-btn";
@ -208,7 +449,7 @@ button:disabled{background:#999;cursor:not-allowed}
keywordsDiv.appendChild(btn);
}
// ── Toggle chip selection ──
// ── Toggle chip ──
function toggleChip(k, chipEl) {
if (selected.includes(k)) {
selected = selected.filter(x => x !== k);
@ -223,10 +464,7 @@ button:disabled{background:#999;cursor:not-allowed}
// ── Render selected keywords ──
function renderSelected() {
selectedDiv.innerHTML = "";
if (!selected.length) {
selectedArea.style.display = "none";
return;
}
if (!selected.length) { selectedArea.style.display = "none"; return; }
selectedArea.style.display = "block";
selected.forEach(k => {
const chip = document.createElement("span");
@ -234,22 +472,27 @@ button:disabled{background:#999;cursor:not-allowed}
chip.innerText = k + " ✕";
chip.onclick = () => {
selected = selected.filter(x => x !== k);
const kwChip = keywordsDiv.querySelector(`[data-kw="${k}"]`);
if (kwChip) kwChip.classList.remove("selected");
const kw = keywordsDiv.querySelector(`[data-kw="${k}"]`);
if (kw) kw.classList.remove("selected");
renderSelected();
};
selectedDiv.appendChild(chip);
});
}
// ── Load more suggestions ──
// ── Load more ──
async function loadMore() {
const btn = document.getElementById("loadMoreBtn");
btn.innerText = "Loading...";
btn.disabled = true;
try {
const res = await fetch(`http://127.0.0.1:8000/suggest?query=${encodeURIComponent(currentQuery)}&offset=${currentOffset}&limit=10`);
const existing = Array.from(keywordsDiv.querySelectorAll(".chip"))
.map(c => c.dataset.kw).filter(Boolean);
const res = await fetch(
`http://127.0.0.1:8000/suggest/more?query=${encodeURIComponent(currentQuery)}&category=${currentCategory}&existing=${encodeURIComponent(existing.join(","))}`
);
const data = await res.json();
if (!data.suggestions || !data.suggestions.length) {
@ -257,18 +500,13 @@ button:disabled{background:#999;cursor:not-allowed}
return;
}
const existing = Array.from(keywordsDiv.querySelectorAll(".chip"))
.map(c => c.dataset.kw?.toLowerCase()).filter(Boolean);
const newItems = data.suggestions.filter(s => !existing.includes(s.toLowerCase()));
if (!newItems.length) {
btn.innerText = "No more suggestions";
return;
}
const newItems = data.suggestions.filter(
s => !existing.map(e => e.toLowerCase()).includes(s.toLowerCase())
);
if (!newItems.length) { btn.innerText = "No more suggestions"; return; }
currentOffset += 10;
renderChips(newItems, true);
} catch (e) {
btn.innerText = "+ More";
btn.disabled = false;
@ -286,15 +524,16 @@ button:disabled{background:#999;cursor:not-allowed}
document.getElementById("askBtn").disabled = true;
step++;
const chat = document.getElementById("chat");
const chat = document.getElementById("chat");
const block = document.createElement("div");
block.className = "chat-block";
const catInfo = CATEGORIES[currentCategory];
block.innerHTML = `
<div class="go-label">${step}. Go</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="answer"><i>⏳ AI is generating answer...</i></div>
`;
<div class="answer"><i>⏳ AI is generating answer...</i></div>`;
chat.appendChild(block);
block.scrollIntoView({ behavior: "smooth" });
@ -306,6 +545,7 @@ button:disabled{background:#999;cursor:not-allowed}
body: JSON.stringify({
query: combined,
selected_attributes: selected,
category: currentCategory,
chat_history: chatHistory
})
});
@ -315,46 +555,74 @@ button:disabled{background:#999;cursor:not-allowed}
answer = "Backend not running. Start the server on port 8000.";
}
if (selected.length > 0) {
fetch("http://127.0.0.1:8000/feedback", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
query: combined,
selected_chips: selected,
domain: currentQuery
})
}).catch(() => {});
}
block.querySelector(".answer").innerHTML = `<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 = "";
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("askBtn").disabled = false;
promptInput.focus();
}
// ── Clear all ──
// ── Clear functions ──
function clearAll() {
promptInput.value = "";
document.getElementById("extra").value = "";
selected = [];
chatHistory = [];
step = 0;
lastFetchedQuery = "";
selected = []; chatHistory = []; step = 0; lastFetchedQuery = ""; currentCategory = "general";
document.querySelectorAll(".cat-tab").forEach(t => t.classList.remove("active"));
document.querySelector("[data-cat='general']").classList.add("active");
const badge = document.getElementById("categoryBadge");
badge.textContent = "General"; badge.className = "category-badge general";
renderSelected();
keywordsDiv.innerHTML = '<span class="kw-msg">Start typing to see suggestions...</span>';
document.getElementById("resultsBox").classList.remove("visible");
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) {
return text
.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')
.replace(/## (.*)/g, '<h2>$1</h2>')
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.*?)\*/g, '<em>$1</em>')
.replace(/---/g, '<hr style="border:none;border-top:1px solid #ddd;margin:10px 0"/>')
.replace(/\n\n+/g, '</p><p>')
.replace(/\n/g, '<br/>')
.replace(/^/, '<p>').replace(/$/, '</p>');
.replace(/## (.*)/g,'<h2>$1</h2>')
.replace(/\*\*(.*?)\*\*/g,'<strong>$1</strong>')
.replace(/\*(.*?)\*/g,'<em>$1</em>')
.replace(/---/g,'<hr style="border:none;border-top:1px solid #ddd;margin:10px 0"/>')
.replace(/\n\n+/g,'</p><p>')
.replace(/\n/g,'<br/>')
.replace(/^/,'<p>').replace(/$/,'</p>');
}
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