vera-engine / app.py
IDKHowToCodeFr's picture
fix(api): fix HTTP 500 on reply endpoint due to kwarg mismatch and missing schema
bd72f20
Raw History Blame Contribute Delete
5.51 kB
import os
import re
import logging
from typing import List, Dict, Any, Optional
from fastapi import FastAPI
from fastapi.responses import RedirectResponse
from pydantic import BaseModel
from dotenv import load_dotenv
from scripts.composer import compose, compose_with_fallback
load_dotenv()
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("VeraArchitect")
for key_name in ["NVIDIA_API_KEY", "GROQ_API_KEY", "GEMINI_API_KEY"]:
if not os.getenv(key_name):
logger.warning(f"Missing {key_name} at startup. That tier will be skipped.")
app = FastAPI()
class ContextPayload(BaseModel):
scope: Optional[str] = None
context_id: Optional[str] = None
version: Optional[int] = None
payload: Dict[Any, Any] = {}
class TickRequest(BaseModel):
available_triggers: List[str] = []
contexts = {"category": {}, "merchant": {}, "customer": {}, "trigger": {}}
@app.get("/")
async def root():
return RedirectResponse(url="/docs")
@app.get("/v1/healthz")
async def healthz():
global contexts
contexts = {"category": {}, "merchant": {}, "customer": {}, "trigger": {}}
return {
"status": "ok",
"uptime_seconds": 100,
"contexts_loaded": {
"category": len(contexts["category"]),
"merchant": len(contexts["merchant"]),
"customer": len(contexts["customer"]),
"trigger": len(contexts["trigger"])
}
}
@app.get("/v1/metadata")
async def metadata():
return {
"team_name": "Vera Lead Solver",
"team_members": ["Agent", "User"],
"model": "Fallback Chain (NVIDIA->Groq->Gemini) -> Local Qwen2.5-3B-Instruct",
"approach": "Caveman Distiller + LLM Expander Pipeline",
"contact_email": "rachit.mangawa.ug23@nsut.ac.in",
"version": "1.0",
"submitted_at": "2026-09-26T00:00:00Z"
}
@app.post("/v1/context")
def push_context(data: ContextPayload):
current = contexts.get(data.scope, {}).get(data.context_id)
if current and current.get("version", 0) >= (data.version or 0):
return {"accepted": True, "ack_id": f"ack_{data.context_id}_v{data.version}", "stored_at": "2026-04-26T00:00:00Z", "note": "ignored_stale_version"}
contexts[data.scope][data.context_id] = {"version": data.version, "payload": data.payload}
return {"accepted": True, "ack_id": f"ack_{data.context_id}_v{data.version}", "stored_at": "2026-04-26T00:00:00Z"}
@app.post("/v1/tick")
async def tick(req: TickRequest):
import time
tids = sorted(req.available_triggers, key=lambda t: contexts["trigger"].get(t, {}).get("payload", {}).get("urgency", 0), reverse=True)
request_deadline = time.time() + 27.0
results = []
for tid in tids:
remaining = request_deadline - time.time()
if remaining <= 2.0:
logger.warning(f"Skipping trigger {tid}, {remaining:.1f}s left")
break
res = await process_trigger(tid, deadline=request_deadline)
if res: results.append(res)
return {"actions": results}
async def process_trigger(trigger_id: str, deadline: float = None):
trigger_ctx = contexts["trigger"].get(trigger_id)
if not trigger_ctx: return None
trigger = trigger_ctx["payload"]
merchant = contexts["merchant"].get(trigger.get("merchant_id", ""), {}).get("payload", {})
category = contexts["category"].get(merchant.get("category_slug", ""), {}).get("payload", {"slug": "general"})
customer = contexts["customer"].get(trigger.get("customer_id", ""), {}).get("payload")
composed = await compose(category, merchant, trigger, customer, deadline=deadline)
return {"conversation_id": f"conv_{trigger_id}", "merchant_id": trigger.get("merchant_id"), "trigger_id": trigger_id, "body": composed["body"], "cta": composed["cta"]}
AUTO_REPLY_PATTERNS = [r"thank you for contacting", r"we are currently away", r"automated message", r"business hours", r"auto-reply"]
HOSTILE_PATTERNS = [r"stop", r"spam", r"useless", r"abuse"]
INTENT_PATTERNS = [r"\bok\b", r"\byes\b", r"\bdo it\b", r"let'?s do it", r"\bsure\b"]
@app.post("/v1/reply")
async def reply(req: Dict[str, Any]):
msg, turn = req.get("message", "").lower(), req.get("turn_number", 1)
for p in HOSTILE_PATTERNS:
if re.search(p, msg): return {"action": "end", "rationale": "Hostility."}
for p in AUTO_REPLY_PATTERNS:
if re.search(p, msg):
if turn > 2: return {"action": "end", "rationale": "Repeated auto-reply."}
return {"action": "wait", "wait_seconds": 3600, "rationale": "Auto-reply wait."}
reply_schema = 'RETURN JSON FORMAT WITH keys "body", "cta", and "rationale".'
for p in INTENT_PATTERNS:
if re.search(p, msg):
res = await compose_with_fallback(
f"Merchant said: {msg}",
full_system=f"Role: Vera AI. Interest detected. Provide EXACT next step action. 15 words max. {reply_schema}"
)
return {"action": "send", "body": res.get("body", "I'm setting that up for you now.") if res else "I'm setting that up now.", "cta": "Reply YES"}
res = await compose_with_fallback(
f"Merchant: {msg}",
full_system=f"Role: Vera AI. Growth Strategist. Be concise. {reply_schema}"
)
return {"action": "send", "body": res.get("body", "Understood. Proceed?") if res else "Understood. Proceed?", "cta": "Reply YES"}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8080)