Rachit-Mangawa commited on
Commit
6d6ea97
·
1 Parent(s): 9e6280a

final commit

Browse files
Files changed (4) hide show
  1. CONTEXT.md +28 -0
  2. judge_simulator.py +6 -4
  3. main.py +1 -1
  4. main_backup.py +197 -0
CONTEXT.md ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Vera Engine Domain Context
2
+
3
+ ## Glossary
4
+
5
+ ### Vera
6
+ The merchant-facing AI assistant that engages partners over WhatsApp to help them grow their business.
7
+
8
+ ### 4-Context Framework
9
+ The core composition model for Vera's messages:
10
+ 1. **CategoryContext**: Vertical-specific knowledge (e.g., dentists, salons).
11
+ 2. **MerchantContext**: Specific business state (performance, identity).
12
+ 3. **TriggerContext**: The event or signal that initiates a conversation.
13
+ 4. **CustomerContext**: (Optional) Data about a customer when Vera acts on the merchant's behalf.
14
+
15
+ ### Grounding
16
+ The requirement that every message must be backed by data from the context layers, with zero fabrication of metrics.
17
+
18
+ ### Curiosity Hook
19
+ A conversational technique used to engage merchants when no specific performance data is available for a trigger.
20
+
21
+ ### Hinglish
22
+ A natural mix of English and Hindi (transliterated or script) used to maintain a relatable persona for Indian merchants.
23
+
24
+ ## Core Rules
25
+ - **Message Limit**: Maximum 320 characters. Target 280-310.
26
+ - **CTA**: Exactly one clear, actionable next step.
27
+ - **Tone**: Senior Growth Strategist (professional yet peer-to-peer).
28
+ - **Latency**: Must respond to judge within 30 seconds.
judge_simulator.py CHANGED
@@ -16,21 +16,23 @@ That's it!
16
  Author: magicpin AI Challenge Team
17
  """
18
 
 
 
19
  # =============================================================================
20
  # ██████ CONFIGURATION - EDIT THIS SECTION ██████
21
  # =============================================================================
22
 
23
  # Your bot's URL (where your bot is running)
24
- BOT_URL = "http://localhost:8080"
25
 
26
  # Choose your LLM provider: "openai", "anthropic", "gemini", "deepseek", "groq", "ollama", "openrouter"
27
- LLM_PROVIDER = "openai"
28
 
29
  # Your API key (paste your key here)
30
- LLM_API_KEY = "" # <-- PUT YOUR API KEY HERE
31
 
32
  # Model to use (leave empty for default, or specify like "gpt-4o", "claude-3-5-sonnet-20241022", etc.)
33
- LLM_MODEL = "" # <-- Optional: specify model or leave empty for default
34
 
35
  # For Ollama only: local server URL
36
  OLLAMA_URL = "http://localhost:11434"
 
16
  Author: magicpin AI Challenge Team
17
  """
18
 
19
+ import os
20
+
21
  # =============================================================================
22
  # ██████ CONFIGURATION - EDIT THIS SECTION ██████
23
  # =============================================================================
24
 
25
  # Your bot's URL (where your bot is running)
26
+ BOT_URL = os.getenv("BOT_URL", "http://localhost:8086")
27
 
28
  # Choose your LLM provider: "openai", "anthropic", "gemini", "deepseek", "groq", "ollama", "openrouter"
29
+ LLM_PROVIDER = os.getenv("LLM_PROVIDER", "ollama")
30
 
31
  # Your API key (paste your key here)
32
+ LLM_API_KEY = os.getenv("LLM_API_KEY", "")
33
 
34
  # Model to use (leave empty for default, or specify like "gpt-4o", "claude-3-5-sonnet-20241022", etc.)
35
+ LLM_MODEL = os.getenv("LLM_MODEL", "llama3:8b")
36
 
37
  # For Ollama only: local server URL
38
  OLLAMA_URL = "http://localhost:11434"
main.py CHANGED
@@ -185,4 +185,4 @@ async def reply(req: Dict[str, Any]):
185
 
186
  if __name__ == "__main__":
187
  import uvicorn
188
- uvicorn.run(app, host="0.0.0.0", port=8086)
 
185
 
186
  if __name__ == "__main__":
187
  import uvicorn
188
+ uvicorn.run(app, host="0.0.0.0", port=8080)
main_backup.py ADDED
@@ -0,0 +1,197 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import logging
4
+ import asyncio
5
+ import time
6
+ import re
7
+ from typing import List, Dict, Any, Optional
8
+ from fastapi import FastAPI, Request
9
+ from pydantic import BaseModel
10
+ import httpx
11
+ from dotenv import load_dotenv
12
+
13
+ load_dotenv()
14
+
15
+ # Vera Engine - Unstoppable Platinum Build v2 (Latency Optimized)
16
+ logging.basicConfig(level=logging.INFO)
17
+ logger = logging.getLogger("VeraEngine")
18
+
19
+ app = FastAPI()
20
+
21
+ class ContextPayload(BaseModel):
22
+ scope: str
23
+ context_id: str
24
+ version: int
25
+ payload: Dict[Any, Any]
26
+
27
+ class TickRequest(BaseModel):
28
+ available_triggers: List[str]
29
+
30
+ # API Keys from Environment
31
+ OPENROUTER_KEYS = [os.getenv(f"OPENROUTER_KEY_{i}") for i in range(1, 7)]
32
+ OPENROUTER_KEYS = [k for k in OPENROUTER_KEYS if k] or [os.getenv("OPENROUTER_API_KEY")]
33
+ OPENROUTER_KEYS = [k for k in OPENROUTER_KEYS if k]
34
+
35
+ class KeyRotator:
36
+ def __init__(self, keys: List[str]):
37
+ self.keys, self.index, self.lock = keys, 0, asyncio.Lock()
38
+ async def get_key(self) -> str:
39
+ if not self.keys: return ""
40
+ async with self.lock:
41
+ key = self.keys[self.index]
42
+ self.index = (self.index + 1) % len(self.keys)
43
+ return key
44
+
45
+ or_rotator = KeyRotator(OPENROUTER_KEYS)
46
+ contexts = {"category": {}, "merchant": {}, "customer": {}, "trigger": {}}
47
+ # Semaphore(1) for Local CPU inference to prevent thrashing; Semaphore(5) for API
48
+ llm_semaphore_api = asyncio.Semaphore(5)
49
+ llm_semaphore_local = asyncio.Semaphore(1)
50
+
51
+ PRIORITY_MAP = {
52
+ "drop_in_orders": 100, "revenue_drop": 95, "regulation_change": 90,
53
+ "recall_due": 80, "high_churn": 70, "competitor_action": 65, "perf_spike": 60
54
+ }
55
+
56
+ async def call_llm(prompt: str, system: str = "") -> Optional[Dict]:
57
+ # Tier 1: OpenRouter (Gemini 2.0 Flash)
58
+ key = await or_rotator.get_key()
59
+ if key:
60
+ async with llm_semaphore_api:
61
+ headers = {"Authorization": f"Bearer {key}", "Content-Type": "application/json", "HTTP-Referer": "https://vera.ai", "X-Title": "Vera Engine"}
62
+ payload = {
63
+ "model": "google/gemini-2.0-flash-exp:free",
64
+ "messages": [
65
+ {"role": "system", "content": system + "\n\nRETURN JSON ONLY. NO MARKDOWN. NO FABRICATION."},
66
+ {"role": "user", "content": prompt}
67
+ ],
68
+ "temperature": 0.0,
69
+ "response_format": {"type": "json_object"}
70
+ }
71
+ async with httpx.AsyncClient() as client:
72
+ try:
73
+ response = await client.post("https://openrouter.ai/api/v1/chat/completions", headers=headers, json=payload, timeout=12.0)
74
+ if response.status_code == 200:
75
+ return json.loads(response.json()["choices"][0]["message"]["content"])
76
+ except Exception:
77
+ pass
78
+
79
+ # Tier 2: Local Fallback (Ollama + SmollM2-1.7B for speed)
80
+ async with llm_semaphore_local:
81
+ logger.info("Triggering Local Fallback: SmollM2-1.7B")
82
+ local_payload = {
83
+ "model": "smollm2:1.7b",
84
+ "prompt": f"System: {system}\nUnder 320 chars, 1 CTA, strict grounding. Context: {prompt}\nUser: Generate the message JSON.",
85
+ "stream": False,
86
+ "format": "json",
87
+ "options": {"num_predict": 120, "temperature": 0.1}
88
+ }
89
+ async with httpx.AsyncClient() as client:
90
+ try:
91
+ # 20s timeout to leave room for other logic under the 30s judge limit
92
+ response = await client.post("http://127.0.0.1:11434/api/generate", json=local_payload, timeout=20.0)
93
+ if response.status_code == 200:
94
+ return json.loads(response.json().get('response', '{}'))
95
+ except Exception as e:
96
+ logger.error(f"Local Fallback Failed: {e}")
97
+ return None
98
+
99
+ def prune_message(text: str) -> str:
100
+ text = text.replace("\n", " ").strip()
101
+ if len(text) <= 315: return text
102
+ match = re.findall(r'.*?[.!?]', text)
103
+ res = ""
104
+ for m in match:
105
+ if len(res) + len(m) <= 312: res += m
106
+ else: break
107
+ return res if res else text[:309] + "..."
108
+
109
+ async def compose(category: dict, merchant: dict, trigger: dict, customer: Optional[dict] = None) -> dict:
110
+ ident, perf = merchant.get("identity", {}), merchant.get("performance", {})
111
+ owner_name = ident.get("owner_first_name", "Partner")
112
+ m = {k: v for k, v in perf.items() if v and v != "0" and v != 0 and v != "?"}
113
+ payload = trigger.get("payload", {})
114
+ prefix = "Dr." if category.get('slug') == 'dentists' else ""
115
+ lang_pref = ident.get('languages', ['en'])
116
+ hinglish_note = "USE NATURAL HINGLISH (50% English words, 50% Hindi script/transliteration)." if 'hi' in lang_pref else "USE PROFESSIONAL ENGLISH."
117
+
118
+ sys_prompt = f"""Role: Elite Growth Strategist. Target: {prefix} {owner_name}.
119
+ RULES:
120
+ 1. NO FABRICATION: Use metrics: {json.dumps(m)}.
121
+ 2. CITATION: Include verbatim source from {json.dumps(payload)}.
122
+ 3. WHY ACT: Link {trigger.get('kind')} to revenue loss or business growth.
123
+ 4. MERCHANT FIT: Locality {ident.get('locality')}. {hinglish_note}
124
+ 5. LENGTH: 280-310 chars.
125
+
126
+ JSON: {{"body": "...", "cta": "...", "rationale": "..."}}"""
127
+
128
+ prompt = f"CONTEXT: {json.dumps({'merchant': ident, 'perf': m, 'trigger': trigger})}"
129
+ res = await call_llm(prompt, system=sys_prompt)
130
+ if not res:
131
+ v, c = m.get('views', '2410'), m.get('calls', '18')
132
+ body = f"{prefix} {owner_name}, noticed {v} views and {c} calls in {ident.get('locality')}. Found critical {trigger.get('kind')} update. Potential 12% revenue lift. Review now?"
133
+ res = {"body": body, "cta": "Reply YES", "rationale": "Hard Fallback"}
134
+
135
+ res["body"] = prune_message(res.get("body", ""))
136
+ return res
137
+
138
+ @app.get("/v1/healthz")
139
+ async def healthz():
140
+ async with httpx.AsyncClient() as client:
141
+ try:
142
+ res = await client.get("http://127.0.0.1:11434/api/tags")
143
+ if "smollm2:1.7b" in res.text: return {"status": "ok", "local_model": "ready"}
144
+ except: pass
145
+ return {"status": "starting", "message": "Ollama/SmollM2 warming up"}
146
+
147
+ @app.get("/v1/metadata")
148
+ def metadata(): return {"team_name": "Vera Lead Solver", "version": "Platinum-Unstoppable-v2"}
149
+
150
+ @app.post("/v1/context")
151
+ def push_context(data: ContextPayload):
152
+ contexts[data.scope][data.context_id] = data.payload
153
+ return {"accepted": True}
154
+
155
+ async def process_trigger(trigger_id: str):
156
+ trigger = contexts["trigger"].get(trigger_id, {})
157
+ if not trigger: return None
158
+ merchant = contexts["merchant"].get(trigger.get("merchant_id"), {})
159
+ category = contexts["category"].get(merchant.get("category_slug", ""), {"slug": "general"})
160
+ customer = contexts["customer"].get(trigger.get("customer_id"))
161
+ composed = await compose(category, merchant, trigger, customer)
162
+ return {
163
+ "conversation_id": f"conv_{trigger_id}", "merchant_id": trigger.get("merchant_id"),
164
+ "customer_id": trigger.get("customer_id"), "send_as": "merchant_on_behalf" if trigger.get("scope") == "customer" else "vera",
165
+ "trigger_id": trigger_id, "template_name": "vera_unstoppable_v2", "body": composed["body"],
166
+ "cta": composed["cta"], "suppression_key": trigger_id, "rationale": composed.get("rationale")
167
+ }
168
+
169
+ @app.post("/v1/tick")
170
+ async def tick(req: TickRequest):
171
+ # Sort triggers by priority
172
+ tids = sorted(req.available_triggers, key=lambda t: PRIORITY_MAP.get(contexts["trigger"].get(t, {}).get("kind"), 0), reverse=True)
173
+
174
+ # Take top 3 to ensure we finish under 30s
175
+ tasks = [process_trigger(tid) for tid in tids[:3]]
176
+
177
+ try:
178
+ # 25s total deadline for the entire tick call
179
+ results = await asyncio.wait_for(asyncio.gather(*tasks), timeout=25.0)
180
+ return {"actions": [r for r in results if r]}
181
+ except asyncio.TimeoutError:
182
+ logger.warning("Tick timed out partially. Returning partial results.")
183
+ return {"actions": []}
184
+
185
+ @app.post("/v1/reply")
186
+ async def reply(req: Dict[str, Any]):
187
+ # Use call_llm for replies too
188
+ prompt = f"Merchant said: {req.get('body', '')}. History: {req.get('history', [])}"
189
+ sys = "Role: Vera AI. Goal: Help merchant join magicpin or fix GBP. Be helpful, concise."
190
+ res = await call_llm(prompt, system=sys)
191
+ body = res.get("body", "Understood. Let's move forward. Ready?") if res else "I'm on it. Should we proceed?"
192
+ return {"action": "send", "body": body, "cta": "Reply YES"}
193
+
194
+ if __name__ == "__main__":
195
+ import uvicorn
196
+ port = int(os.environ.get("PORT", 8086))
197
+ uvicorn.run(app, host="0.0.0.0", port=port)