"""ARIA — Flask backend for Arman Adil Mangat's portfolio chatbot. Runs as a Docker Space on HuggingFace (port 7860). Endpoints: POST /chat {"message": str, "history": [{"role": ..., "content": ...}]} GET /health {"status": "ok"} The only secret is GROQ_API_KEY (HF Space secret / local env var). """ from __future__ import annotations import os import time from collections import defaultdict, deque from flask import Flask, jsonify, request from flask_cors import CORS from groq import Groq from rag import TOP_K, RagIndex # ── Config ──────────────────────────────────────────────────────────────── ALLOWED_ORIGINS = [ "https://armanadilmangat.github.io", # GitHub Pages (production) "http://localhost:5173", # Vite dev server "http://127.0.0.1:5173", ] GROQ_MODEL = "llama-3.1-8b-instant" MAX_HISTORY_MESSAGES = 12 # last 6 user/assistant turns MAX_MESSAGE_CHARS = 1000 RATE_LIMIT = 20 # requests … RATE_WINDOW = 60 # … per seconds, per IP SYSTEM_PROMPT = """\ You are ARIA (Arman's Resume Intelligence Assistant), the AI assistant on \ Arman Adil Mangat's portfolio website. Answer questions about Arman's background, projects, skills, and goals using \ ONLY the context below. Be concise, friendly, and concrete — lead with numbers \ and specifics. Keep answers under 120 words unless the visitor asks for depth. If asked something about Arman that is not in the context, say you don't have \ that detail and suggest emailing him at aadilmangat@gmail.com. Never invent \ facts about Arman. Never share personal details beyond the provided context — \ you do not have his phone number, family details, or home address. You may hold light general conversation (greetings, small talk, simple general \ questions), but always steer back to Arman's work. If asked for contact details, give: aadilmangat@gmail.com · \ github.com/ArmanAdilMangat · linkedin.com/in/armanadilmangat · \ huggingface.co/ArmanXAI CONTEXT ABOUT ARMAN: {context} """ # ── App setup ───────────────────────────────────────────────────────────── app = Flask(__name__) CORS(app, resources={ r"/chat": {"origins": ALLOWED_ORIGINS}, r"/health": {"origins": "*"}, }) print("ARIA: building RAG index (one-time at startup)…", flush=True) INDEX = RagIndex() print(f"ARIA: index ready — {len(INDEX.chunks)} chunks.", flush=True) groq_client = Groq(api_key=os.environ["GROQ_API_KEY"]) # ── Simple in-memory per-IP rate limiter ───────────────────────────────── _hits: dict[str, deque] = defaultdict(deque) def _client_ip() -> str: fwd = request.headers.get("X-Forwarded-For", "") return (fwd.split(",")[0].strip() if fwd else request.remote_addr) or "unknown" def _rate_limited(ip: str) -> bool: now = time.time() q = _hits[ip] while q and now - q[0] > RATE_WINDOW: q.popleft() if len(q) >= RATE_LIMIT: return True q.append(now) return False # ── Routes ──────────────────────────────────────────────────────────────── @app.get("/health") def health(): return jsonify({"status": "ok"}) @app.post("/chat") def chat(): if _rate_limited(_client_ip()): return jsonify({"reply": "You're sending messages a bit fast — give it " "a few seconds and try again."}), 429 data = request.get_json(silent=True) or {} message = (data.get("message") or "").strip()[:MAX_MESSAGE_CHARS] if not message: return jsonify({"error": "message is required"}), 400 # Sanitize client-supplied history (roles whitelisted, length capped). history = [] for m in (data.get("history") or [])[-MAX_HISTORY_MESSAGES:]: role = m.get("role") content = (m.get("content") or "").strip() if role in ("user", "assistant") and content: history.append({"role": role, "content": content[:MAX_MESSAGE_CHARS]}) # RAG: retrieve top-k chunks and stuff them into the system prompt. chunks = INDEX.search(message, TOP_K) context = "\n\n---\n\n".join( f"[{c.source} — {c.heading or 'overview'}]\n{c.text}" for c in chunks ) messages = [ {"role": "system", "content": SYSTEM_PROMPT.format(context=context)}, *history, {"role": "user", "content": message}, ] try: resp = groq_client.chat.completions.create( model=GROQ_MODEL, messages=messages, temperature=0.4, max_tokens=400, ) reply = (resp.choices[0].message.content or "").strip() except Exception: # noqa: BLE001 — surface a friendly error, log the rest app.logger.exception("Groq call failed") return jsonify({"reply": "Hmm, my brain (the LLM API) hiccuped. Try " "again in a moment — or email Arman directly " "at aadilmangat@gmail.com."}), 502 return jsonify({"reply": reply}) if __name__ == "__main__": app.run(host="0.0.0.0", port=7860, debug=False)