Spaces:
Sleeping
Sleeping
File size: 5,502 Bytes
2bb4a0d | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 | """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)
|