Spaces:
Sleeping
Sleeping
| """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 ──────────────────────────────────────────────────────────────── | |
| def health(): | |
| return jsonify({"status": "ok"}) | |
| 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) | |