File size: 5,505 Bytes
fdeeb34
 
c1c5ecb
fdeeb34
9e6280a
d896c38
fdeeb34
 
c1c5ecb
 
fdeeb34
 
 
 
9e6280a
fdeeb34
c1c5ecb
 
 
 
fdeeb34
 
 
3858fba
 
 
 
fdeeb34
 
3858fba
fdeeb34
e67482e
fdeeb34
d896c38
 
 
 
fdeeb34
e67482e
 
 
 
 
 
 
 
 
 
 
 
 
a219be1
 
e67482e
 
 
 
 
 
 
 
 
 
fdeeb34
 
 
90962fb
 
 
 
e67482e
 
fdeeb34
c508545
 
c1c5ecb
e67482e
c1c5ecb
c508545
c1c5ecb
 
 
 
 
 
c508545
 
 
c1c5ecb
e67482e
 
 
 
 
 
 
c1c5ecb
9e6280a
fdeeb34
08b98ed
9e6280a
 
7e9d7e2
fdeeb34
 
9e6280a
08b98ed
9e6280a
08b98ed
 
 
 
bd72f20
 
08b98ed
 
bd72f20
 
 
 
9e6280a
08b98ed
bd72f20
 
 
 
9e6280a
fdeeb34
 
 
9e6280a
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
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)