Spaces:
Sleeping
Sleeping
| # app.py β VERA backend (improved) | |
| import os | |
| import re | |
| import uvicorn | |
| import json | |
| import time | |
| import hashlib | |
| import logging | |
| import requests | |
| from collections import deque | |
| import uuid | |
| from fastapi.staticfiles import StaticFiles | |
| from fastapi import FastAPI, Request, Header | |
| from fastapi.responses import JSONResponse, FileResponse | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from sentence_transformers import SentenceTransformer | |
| from pinecone import Pinecone, ServerlessSpec | |
| # βββββββββββββββββββββββββββββββββ Logging βββββββββββββββββββββββββββββββββββββ | |
| logging.basicConfig(level=logging.INFO) | |
| log = logging.getLogger("vera-backend") | |
| # βββββββββββββββββββββββββββββββββ Env / Config βββββββββββββββββββββββββββββββ | |
| PINECONE_API_KEY = os.getenv("PINECONE_API_KEY") | |
| HF_API_KEY = os.getenv("HF_API_KEY") | |
| # realtime search (pick one you have a key for; Tavily recommended) | |
| TAVILY_API_KEY = os.getenv("TAVILY_API_KEY") # https://www.tavily.com/ | |
| BING_API_KEY = os.getenv("BING_API_KEY") | |
| # Optional: You can also add SERPAPI_API_KEY / BING_API_KEY if you prefer | |
| URL_DECISION = os.getenv("URL_DECISION", "https://thevera-decision-making-model.hf.space/generate_text") | |
| URL_SUMMARIZER = os.getenv("URL_SUMMARIZER", "https://thevera-get-best-answer.hf.space/summarize") | |
| INSTR_DECISION = os.getenv("INSTR_DECISION", "instructions_L1.txt") | |
| INSTR_HEALTH = os.getenv("INSTR_HEALTH", "instructions_health.txt") | |
| REGION = os.getenv("PINECONE_REGION", "us-east-1") | |
| INDEX_NAME = os.getenv("PINECONE_INDEX", "veradb") | |
| EMBED_MODEL_ID = os.getenv("EMBED_MODEL_ID", "distiluse-base-multilingual-cased-v1") | |
| EMBED_DIM = int(os.getenv("EMBED_DIM", "512")) | |
| # Memory / session config | |
| SESSION_TTL_SEC = int(os.getenv("SESSION_TTL_SEC", "7200")) # 2 hours | |
| SESSION_MAX_TURNS = int(os.getenv("SESSION_MAX_TURNS", "10")) # keep last 10 items (user+assistant) | |
| # Timeouts | |
| HTTP_TIMEOUT = int(os.getenv("HTTP_TIMEOUT", "60")) | |
| # βββββββββββββββββββββββββββββββββ FastAPI App ββββββββββββββββββββββββββββββββ | |
| app = FastAPI() | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], # or restrict to your static Space origin | |
| allow_credentials=True, | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| os.makedirs("generated", exist_ok=True) | |
| app.mount("/generated", StaticFiles(directory="generated"), name="generated") | |
| # βββββββββββββββββββββββββββββββββ Dependencies βββββββββββββββββββββββββββββββ | |
| # Pinecone (guard if key missing) | |
| pc = None | |
| index = None | |
| if PINECONE_API_KEY: | |
| try: | |
| pc = Pinecone(api_key=PINECONE_API_KEY) | |
| if INDEX_NAME not in pc.list_indexes().names(): | |
| pc.create_index( | |
| name=INDEX_NAME, | |
| dimension=EMBED_DIM, | |
| metric="cosine", | |
| spec=ServerlessSpec(cloud="aws", region=REGION), | |
| ) | |
| index = pc.Index(INDEX_NAME) | |
| log.info("Pinecone index ready: %s", INDEX_NAME) | |
| except Exception: | |
| log.exception("Pinecone init failed") | |
| else: | |
| log.warning("PINECONE_API_KEY not set β retrieval disabled.") | |
| # Embeddings | |
| embed_model = SentenceTransformer(EMBED_MODEL_ID) | |
| # βββββββββββββββββββββββββββββββββ Utilities ββββββββββββββββββββββββββββββββββ | |
| def read_file(path: str) -> str: | |
| try: | |
| with open(path, "r", encoding="utf-8") as f: | |
| return f.read() | |
| except Exception as e: | |
| log.error("Failed to read %s: %s", path, e) | |
| return "" | |
| def post_hf(url: str, payload: dict): | |
| """Call HF microservice with bearer if provided; return text or ''.""" | |
| try: | |
| s = requests.Session() | |
| if HF_API_KEY: | |
| s.headers.update({"Authorization": f"Bearer {HF_API_KEY}"}) | |
| r = s.post(url, json=payload, timeout=HTTP_TIMEOUT) | |
| if r.status_code != 200: | |
| log.error("HF call failed %s: %s", r.status_code, r.text[:240]) | |
| return "" | |
| data = r.json() | |
| return data.get("response") or data.get("answer") or "" | |
| except Exception: | |
| log.exception("HF call exception") | |
| return "" | |
| def detect_language(text: str) -> str: | |
| # Devanagari β 'hi' (Hindi/Sanskrit), else English | |
| return "hi" if any("\u0900" <= ch <= "\u097F" for ch in text) else "en" | |
| def clamp(n, lo, hi): | |
| try: | |
| n = float(n) | |
| except Exception: | |
| return lo | |
| return max(lo, min(hi, n)) | |
| # βββββββββββββββββββββββββββββββ In-memory sessions βββββββββββββββββββββββββββ | |
| # { session_id: {"history": deque[{"role": "user"/"assistant", "content": str}], "last_category": str, "t": float} } | |
| SESSIONS = {} | |
| def prune_sessions(): | |
| now = time.time() | |
| stale = [sid for sid, s in SESSIONS.items() if (now - s.get("t", now)) > SESSION_TTL_SEC] | |
| for sid in stale: | |
| SESSIONS.pop(sid, None) | |
| def get_session_id(request: Request, body: dict) -> str: | |
| sid = (body.get("session_id") or "").strip() | |
| if sid: | |
| return sid | |
| # derive from ip + ua | |
| ip = request.client.host if request.client else "0.0.0.0" | |
| ua = request.headers.get("user-agent", "") | |
| raw = f"{ip}|{ua}"[:256] | |
| return hashlib.sha256(raw.encode("utf-8")).hexdigest()[:32] | |
| def append_history(session_id: str, role: str, content: str): | |
| if not session_id: | |
| return | |
| prune_sessions() | |
| s = SESSIONS.setdefault(session_id, {"history": deque(maxlen=SESSION_MAX_TURNS*2), "last_category": None, "t": time.time()}) | |
| s["history"].append({"role": role, "content": content}) | |
| s["t"] = time.time() | |
| def recent_history_text(session_id: str, max_chars: int = 4000) -> str: | |
| s = SESSIONS.get(session_id) | |
| if not s: | |
| return "" | |
| parts = [] | |
| for item in list(s["history"])[-SESSION_MAX_TURNS*2:]: | |
| parts.append(f'{item["role"]}: {item["content"]}') | |
| if sum(len(p) for p in parts) > max_chars: | |
| break | |
| return "\n".join(parts) | |
| def set_last_category(session_id: str, cat: str): | |
| if not session_id: | |
| return | |
| s = SESSIONS.setdefault(session_id, {"history": deque(maxlen=SESSION_MAX_TURNS*2), "last_category": None, "t": time.time()}) | |
| s["last_category"] = cat | |
| s["t"] = time.time() | |
| def get_last_category(session_id: str): | |
| s = SESSIONS.get(session_id) | |
| return s.get("last_category") if s else None | |
| # βββββββββββββββββββββββββββββββ Decision helpers βββββββββββββββββββββββββββββ | |
| def decide_category(user_text: str, session_id: str = "") -> str: | |
| """ | |
| Returns exactly one of: | |
| health_wellness, spiritual_guidance, generate_image, realtime_query, other_query | |
| """ | |
| # Quick heuristics (avoid a remote call for obvious cases) | |
| lt = user_text.strip().lower() | |
| if lt.startswith("/image") or lt.startswith("image:"): | |
| return "generate_image" | |
| if any(k in lt for k in ["latest", "today", "this week", "breaking", "price now", "live score"]): | |
| return "realtime_query" | |
| # Follow-up heuristics: short/anaphoric β reuse last category | |
| if len(lt) < 40 and re.search(r"\b(continue|and\?|more|what about|same|that|it|those)\b", lt): | |
| prev = get_last_category(session_id) | |
| if prev: | |
| return prev | |
| # Model decision | |
| instructions = read_file(INSTR_DECISION) | |
| payload = {"prompt": user_text, "instructions": instructions} | |
| raw = (post_hf(URL_DECISION, payload) or "").strip().lower() | |
| mapping = { | |
| "health_wellness": "health_wellness", | |
| "spiritual_guidance": "spiritual_guidance", | |
| "generate_image": "generate_image", | |
| "realtime_query": "realtime_query", | |
| "other_query": "other_query", | |
| } | |
| return mapping.get(raw, "other_query") | |
| def decide_health_namespace(user_text: str) -> str: | |
| """ | |
| Returns ayurvedic, western, or both (lowercase). | |
| """ | |
| instructions = read_file(INSTR_HEALTH) | |
| payload = {"prompt": user_text, "instructions": instructions} | |
| raw = (post_hf(URL_DECISION, payload) or "").strip().lower() | |
| return raw if raw in {"ayurvedic", "western", "both"} else "both" | |
| # βββββββββββββββββββββββββββββ Pinecone retrieval βββββββββββββββββββββββββββββ | |
| def query_pinecone(namespace: str, prompt: str, top_k: int = 12): | |
| if not index: | |
| return [] | |
| try: | |
| vec = embed_model.encode(prompt).tolist() | |
| res = index.query(namespace=namespace, vector=vec, top_k=top_k, include_metadata=True) | |
| return res.get("matches", []) or [] | |
| except Exception: | |
| log.exception("Pinecone query failed for ns=%s", namespace) | |
| return [] | |
| def build_context(matches, cap_chars: int = 6000) -> str: | |
| ctx, size = [], 0 | |
| for m in matches: | |
| meta = m.get("metadata") or {} | |
| t = (meta.get("text") or "").strip() | |
| if not t: | |
| continue | |
| ctx.append(t) | |
| size += len(t) | |
| if size >= cap_chars: | |
| break | |
| return "\n\n".join(ctx) | |
| # βββββββββββββββββββββββββββββ Realtime web search ββββββββββββββββββββββββββββ | |
| def search_bing(query: str, k: int = 5): | |
| items = [] | |
| if not BING_API_KEY: | |
| return items | |
| try: | |
| r = requests.get( | |
| "https://api.bing.microsoft.com/v7.0/search", | |
| headers={"Ocp-Apim-Subscription-Key": BING_API_KEY}, | |
| params={"q": query, "mkt": "en-US", "count": k, "textDecorations": False, "answerCount": k}, | |
| timeout=HTTP_TIMEOUT | |
| ) | |
| if r.status_code == 200: | |
| j = r.json() | |
| for w in (j.get("webPages", {}).get("value", [])[:k]): | |
| items.append({ | |
| "title": w.get("name") or "", | |
| "url": w.get("url") or "", | |
| "content": w.get("snippet") or "", | |
| }) | |
| except Exception: | |
| log.exception("Bing search failed") | |
| return items | |
| def search_web(query: str, k: int = 5): | |
| """ | |
| Returns list of {title, url, content}. | |
| Priority: Tavily -> DuckDuckGo IA -> Wikipedia summaries | |
| """ | |
| results = [] | |
| # 1) Tavily | |
| if TAVILY_API_KEY: | |
| try: | |
| r = requests.post( | |
| "https://api.tavily.com/search", | |
| json={"api_key": TAVILY_API_KEY, "query": query, "max_results": k, "include_answer": False}, | |
| timeout=HTTP_TIMEOUT, | |
| ) | |
| if r.status_code == 200: | |
| data = r.json() | |
| for item in (data.get("results") or [])[:k]: | |
| results.append({ | |
| "title": item.get("title") or "", | |
| "url": item.get("url") or "", | |
| "content": item.get("content") or item.get("snippet") or "", | |
| }) | |
| except Exception: | |
| log.exception("Tavily search failed") | |
| # 2) Bing fallback | |
| if not results: | |
| results = search_bing(query, k=k) | |
| # 3) DuckDuckGo Instant Answer (no key) | |
| if not results: | |
| try: | |
| r = requests.get( | |
| "https://api.duckduckgo.com/", | |
| params={"q": query, "format": "json", "no_html": 1, "no_redirect": 1}, | |
| timeout=HTTP_TIMEOUT, | |
| ) | |
| if r.status_code == 200: | |
| data = r.json() | |
| if data.get("AbstractText"): | |
| results.append({ | |
| "title": data.get("Heading") or "", | |
| "url": data.get("AbstractURL") or "", | |
| "content": data.get("AbstractText") or "", | |
| }) | |
| for rt in (data.get("RelatedTopics") or []): | |
| if isinstance(rt, dict) and rt.get("Text"): | |
| results.append({ | |
| "title": rt.get("Text")[:80], | |
| "url": (rt.get("FirstURL") or ""), | |
| "content": rt.get("Text") or "", | |
| }) | |
| results = results[:k] | |
| except Exception: | |
| log.exception("DuckDuckGo IA failed") | |
| # 4) Wikipedia (simple) | |
| if not results: | |
| try: | |
| r = requests.get("https://en.wikipedia.org/w/api.php", | |
| params={"action":"opensearch","search":query,"limit":k,"namespace":0,"format":"json"}, | |
| timeout=HTTP_TIMEOUT) | |
| if r.status_code == 200: | |
| data = r.json() | |
| titles = data[1] or [] | |
| urls = data[3] or [] | |
| for t,u in list(zip(titles, urls))[:k]: | |
| results.append({"title": t, "url": u, "content": t}) | |
| except Exception: | |
| log.exception("Wikipedia search failed") | |
| return results[:k] | |
| def summarize_search(query: str, lang: str) -> str: | |
| items = search_web(query, k=5) | |
| if not items: | |
| return "I couldn't find current information right now. Please try rephrasing or ask a more specific query." | |
| # Compose context for the summarizer | |
| ctx_parts = [] | |
| for it in items: | |
| ctx_parts.append(f"{it['title']}\n{it['content']}\nSource: {it['url']}\n") | |
| context = "\n\n".join(ctx_parts) | |
| payload = { | |
| "context": context, | |
| "question": f"Using the sources above, answer this query concisely and include a friendly tone.\nQuery: {query}", | |
| "language": "Hindi" if lang == "hi" else "English", | |
| "reply_type": "brief", | |
| } | |
| ans = (post_hf(URL_SUMMARIZER, payload) or "").strip() | |
| # Add sources footer | |
| src_lines = [] | |
| for i, it in enumerate(items[:3], 1): | |
| if not it.get("url"): | |
| continue | |
| src_lines.append(f"{i}. {it['title'] or it['url']} β {it['url']}") | |
| if src_lines: | |
| ans += "\n\nSources:\n" + "\n".join(src_lines) | |
| return ans or "No answer available." | |
| # βββββββββββββββββββββββββββββ Domain handlers ββββββββββββββββββββββββββββββββ | |
| def handle_spiritual(user_text: str, lang: str) -> str: | |
| # Try existing namespace "spritual" then sane "spiritual" | |
| matches = query_pinecone("spritual", user_text, top_k=20) | |
| if not matches: | |
| matches = query_pinecone("spiritual", user_text, top_k=20) | |
| if not matches: | |
| # Positive fallback | |
| base = "Hindi" if lang == "hi" else "English" | |
| payload = { | |
| "context": "", | |
| "question": f"Provide compassionate spiritual guidance. Question: {user_text}", | |
| "language": base, | |
| "reply_type": "detailed", | |
| } | |
| return (post_hf(URL_SUMMARIZER, payload) or "Let's reflect on this with kindness and clarity.").strip() | |
| context = build_context(matches) | |
| reply_type = "brief" if re.search(r"\b(brief|short|tl;dr)\b", user_text.lower()) else "detailed" | |
| payload = { | |
| "context": context, | |
| "question": user_text, | |
| "language": "Hindi" if (lang == "hi" and "in english" not in user_text.lower()) else "English", | |
| "reply_type": reply_type, | |
| } | |
| ans = post_hf(URL_SUMMARIZER, payload) or "" | |
| return ans.strip() or "May this perspective be helpful." | |
| def merge_health(ay, we): | |
| merged = (ay or []) + (we or []) | |
| merged.sort(key=lambda m: m.get("score", 0), reverse=True) | |
| return merged[:12] | |
| def handle_health(user_text: str, lang: str, image_text: str = "") -> str: | |
| choice = decide_health_namespace(user_text) | |
| if choice == "ayurvedic": | |
| matches = query_pinecone("ayurvedic", user_text, 14) | |
| elif choice == "western": | |
| matches = query_pinecone("western", user_text, 14) | |
| else: | |
| ay = query_pinecone("ayurvedic", user_text, 10) | |
| we = query_pinecone("western", user_text, 10) | |
| matches = merge_health(ay, we) | |
| base_lang = "Hindi" if lang == "hi" else "English" | |
| reply_type = "detailed" if any(k in user_text.lower() for k in ["detail", "explain", "why", "how"]) else "brief" | |
| context = build_context(matches) if matches else "" | |
| if image_text: | |
| context = (context + "\n\nExtracted from health image:\n" + image_text).strip() | |
| payload = { | |
| "context": context, | |
| "question": user_text, | |
| "language": base_lang, | |
| "reply_type": reply_type, | |
| } | |
| ans = (post_hf(URL_SUMMARIZER, payload) or "").strip() | |
| disclaimer = ("\n\n**Note:** This is educational, not a medical diagnosis. " | |
| "Consult a clinician for personalized care.") | |
| return ans + disclaimer if ans else "I couldn't parse enough contextβcould you add a few details?" | |
| def handle_realtime(user_text: str, lang: str) -> str: | |
| # Real search + summarization | |
| return summarize_search(user_text, lang) | |
| def handle_other(user_text: str, lang: str, context_extra: str = "") -> str: | |
| payload = { | |
| "context": context_extra or "", | |
| "question": user_text, | |
| "language": "Hindi" if lang == "hi" else "English", | |
| "reply_type": "detailed", | |
| } | |
| ans = post_hf(URL_SUMMARIZER, payload) or "" | |
| return ans.strip() or "Hereβs a helpful overview." | |
| def generate_image_free(prompt: str) -> str: | |
| """Return a URL to an image for the prompt. Prefers HF Inference -> fallback Pollinations.""" | |
| # 1) Hugging Face Inference API (binary image) | |
| if HF_API_KEY: | |
| try: | |
| r = requests.post( | |
| "https://api-inference.huggingface.co/models/stabilityai/stable-diffusion-2-1", | |
| headers={ | |
| "Authorization": f"Bearer {HF_API_KEY}", | |
| "Accept": "image/png" | |
| }, | |
| json={"inputs": prompt}, | |
| timeout=120 | |
| ) | |
| if r.status_code == 200 and r.content: | |
| fname = f"{uuid.uuid4().hex}.png" | |
| path = os.path.join("generated", fname) | |
| with open(path, "wb") as f: | |
| f.write(r.content) | |
| return f"/generated/{fname}" | |
| else: | |
| log.warning("HF image gen failed %s: %s", r.status_code, r.text[:200]) | |
| except Exception: | |
| log.exception("HF image gen exception") | |
| # 2) Pollinations (no key; public) | |
| try: | |
| from urllib.parse import quote | |
| return f"https://image.pollinations.ai/prompt/{quote(prompt)}" | |
| except Exception: | |
| return "" | |
| def handle_image(user_text: str, lang: str) -> str: | |
| prompt = user_text.replace("/image", "").strip() or user_text | |
| url = generate_image_free(prompt) | |
| if url: | |
| return f"Hereβs your image for: **{prompt}**\n\n" | |
| return f"I couldnβt generate the image right now. Try again in a moment.\n\n**Prompt:** {prompt}" | |
| # βββββββββββββββββββββββββββββ Routes βββββββββββββββββββββββββββββββββββββββββ | |
| def root(): | |
| return {"ok": True, "service": "vera-backend"} | |
| def healthz(): | |
| return {"ok": True} | |
| async def reset_session(request: Request): | |
| body = await request.json() | |
| sid = (body.get("session_id") or "").strip() | |
| if sid and sid in SESSIONS: | |
| SESSIONS.pop(sid, None) | |
| return {"ok": True, "reset": True} | |
| return {"ok": True, "reset": False} | |
| async def generate_result(request: Request, user_agent: str = Header(default="")): | |
| body = await request.json() | |
| raw_text = (body.get("message") or "").strip() | |
| image_text = (body.get("image_text") or "").strip() | |
| if not raw_text: | |
| return JSONResponse(status_code=400, content={"response": "No message provided"}) | |
| session_id = get_session_id(request, body) | |
| # Parse a forced category from the header line | |
| allowed = {"health_wellness","spiritual_guidance","generate_image","realtime_query","other_query"} | |
| forcible = None | |
| text = raw_text | |
| if text.startswith("::category="): | |
| m = re.match(r"^::category\s*=\s*([a-zA-Z_]+)(?:\s+|[\r\n]+)?(.*)$", text, flags=re.S) | |
| if m: | |
| key = m.group(1).strip().lower() | |
| remainder = (m.group(2) or "").strip() | |
| if key in allowed: | |
| forcible = key | |
| text = remainder | |
| # Also allow JSON override | |
| force_from_body = (body.get("force_category") or "").strip().lower() or None | |
| if not forcible and force_from_body in allowed: | |
| forcible = force_from_body | |
| effective_text = text or raw_text | |
| lang = detect_language(effective_text) | |
| # Decide category (with session-aware heuristics) | |
| decision = forcible or decide_category(effective_text, session_id=session_id) | |
| set_last_category(session_id, decision) | |
| # Keep memory | |
| append_history(session_id, "user", effective_text) | |
| # Build optional conversation context for generic answers | |
| convo_ctx = recent_history_text(session_id) | |
| handlers = { | |
| "spiritual_guidance": lambda t, l: handle_spiritual(t, l), | |
| "health_wellness": lambda t, l: handle_health(t, l, image_text=image_text), | |
| "realtime_query": lambda t, l: handle_realtime(t, l), | |
| "generate_image": lambda t, l: handle_image(t, l), | |
| "other_query": lambda t, l: handle_other(t, l, context_extra=convo_ctx), | |
| } | |
| func = handlers.get(decision, handlers["other_query"]) | |
| try: | |
| answer = func(effective_text, lang) | |
| append_history(session_id, "assistant", answer) | |
| return JSONResponse(content={"response": f"{decision}\n{answer}", "language": lang, "session_id": session_id}) | |
| except Exception as e: | |
| log.exception("Handler failed") | |
| return JSONResponse( | |
| status_code=500, | |
| content={"response": f"other_query\nAn error occurred, but here's a quick tip: please try again in a moment.", "language": lang, "session_id": session_id} | |
| ) | |
| # βββββββββββββββββββββββββββββ Main βββββββββββββββββββββββββββββββββββββββββββ | |
| if __name__ == "__main__": | |
| uvicorn.run(app, host="0.0.0.0", port=int(os.getenv("PORT", 7860))) | |