Rachit-Mangawa commited on
Commit
42c83f7
·
1 Parent(s): fd5b9aa

feat(infra): implement 5-tier hierarchical inference engine with key rotation

Browse files
Files changed (1) hide show
  1. app.py +176 -107
app.py CHANGED
@@ -4,6 +4,7 @@ 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
@@ -12,13 +13,13 @@ from dotenv import load_dotenv
12
 
13
  load_dotenv()
14
 
15
- # Vera Engine - Autonomous Local-First Build
16
- # Priority: Ollama (Llama-3.2-3B) -> Fallback: OpenRouter
17
  logging.basicConfig(level=logging.INFO)
18
  logger = logging.getLogger("VeraEngine")
19
 
20
  app = FastAPI()
21
 
 
22
  class ContextPayload(BaseModel):
23
  scope: Optional[str] = None
24
  context_id: Optional[str] = None
@@ -28,22 +29,165 @@ class ContextPayload(BaseModel):
28
  class TickRequest(BaseModel):
29
  available_triggers: List[str] = []
30
 
31
- # API Keys (Optional Fallback)
32
- OPENROUTER_KEYS = [os.getenv(f"OPENROUTER_KEY_{i}") for i in range(1, 7)]
33
- OPENROUTER_KEYS = [k for k in OPENROUTER_KEYS if k] or [os.getenv("OPENROUTER_API_KEY")]
34
- OPENROUTER_KEYS = [k for k in OPENROUTER_KEYS if k]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
 
36
  contexts = {"category": {}, "merchant": {}, "customer": {}, "trigger": {}}
37
- llm_semaphore_local = asyncio.Semaphore(1) # Strict 1 for CPU stability
38
 
39
  PRIORITY_MAP = {
40
  "drop_in_orders": 100, "revenue_drop": 95, "regulation_change": 90,
41
  "recall_due": 80, "high_churn": 70, "competitor_action": 65, "perf_spike": 60
42
  }
43
 
44
- async def call_llm_local(prompt: str, system: str = "") -> Optional[Dict]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  async with llm_semaphore_local:
46
- logger.info("Local Inference: Llama 3.2 (3B)")
47
  payload = {
48
  "model": "llama3.2",
49
  "prompt": f"System: {system}\nStrict Grounding. No Fabrication. Context: {prompt}\nUser: Return JSON message.",
@@ -53,52 +197,14 @@ async def call_llm_local(prompt: str, system: str = "") -> Optional[Dict]:
53
  }
54
  async with httpx.AsyncClient() as client:
55
  try:
56
- response = await client.post("http://127.0.0.1:11434/api/generate", json=payload, timeout=28.0)
57
  if response.status_code == 200:
58
  return json.loads(response.json().get('response', '{}'))
59
  except Exception as e:
60
  logger.error(f"Local Model Error: {e}")
61
  return None
62
 
63
- async def call_llm_api(prompt: str, system: str = "") -> Optional[Dict]:
64
- if not OPENROUTER_KEYS: return None
65
-
66
- # Tier 1 Hierarchy: Primary -> Secondary -> Backup
67
- models = ["poolside/laguna-m.1:free", "tencent/hy3:free", "google/gemini-2.0-flash-exp:free"]
68
-
69
- key = OPENROUTER_KEYS[0]
70
- headers = {
71
- "Authorization": f"Bearer {key}",
72
- "Content-Type": "application/json",
73
- "HTTP-Referer": "https://huggingface.co/spaces",
74
- "X-Title": "Vera Engine Platinum"
75
- }
76
-
77
- async with httpx.AsyncClient() as client:
78
- for model in models:
79
- payload = {
80
- "model": model,
81
- "messages": [
82
- {"role": "system", "content": system + "\n\nRETURN JSON ONLY. NO MARKDOWN. NO FABRICATION."},
83
- {"role": "user", "content": prompt}
84
- ],
85
- "response_format": {"type": "json_object"}
86
- }
87
- try:
88
- res = await client.post(
89
- "https://openrouter.ai/api/v1/chat/completions",
90
- headers=headers,
91
- json=payload,
92
- timeout=10.0
93
- )
94
- if res.status_code == 200:
95
- return json.loads(res.json()["choices"][0]["message"]["content"])
96
- logger.warning(f"OpenRouter model {model} failed with status {res.status_code}")
97
- except Exception as e:
98
- logger.warning(f"OpenRouter error with model {model}: {e}")
99
- continue
100
- return None
101
-
102
  def prune_message(text: str) -> str:
103
  text = text.replace("\n", " ").strip()
104
  if len(text) <= 315: return text
@@ -118,7 +224,6 @@ async def compose(category: dict, merchant: dict, trigger: dict, customer: Optio
118
  lang_pref = ident.get('languages', ['en'])
119
  hinglish_note = "USE NATURAL HINGLISH (50% English words, 50% Hindi script/transliteration)." if 'hi' in lang_pref else "USE PROFESSIONAL ENGLISH."
120
 
121
- # Evidence Anchors: Extract primary facts to prevent hallucination
122
  primary_metric = list(m.items())[0] if m else None
123
  verbatim_source = str(payload)[:150] if payload else None
124
 
@@ -128,14 +233,11 @@ async def compose(category: dict, merchant: dict, trigger: dict, customer: Optio
128
  if verbatim_source:
129
  evidence_block += f"MANDATORY CITATION: '{verbatim_source}'. "
130
 
131
- # Knowledge-Driven Prioritization (Universal principles, not judge-specific patterns)
132
  beats = category.get("seasonal_beats", [])
133
  trends = category.get("trend_signals", [])
134
  knowledge_anchor = ""
135
- if beats:
136
- knowledge_anchor = f"INDUSTRY CONTEXT: {beats[0].get('note', '')}. "
137
- elif trends:
138
- knowledge_anchor = f"MARKET TREND: {trends[0].get('note', '')}. "
139
 
140
  sys_prompt = f"""Role: Senior Growth Strategist. Target: {prefix} {owner_name} in {ident.get('locality')}.
141
  GOAL: Act as a peer partner. Help the merchant grow using facts.
@@ -144,16 +246,12 @@ RULES:
144
  2. CONTEXT: {knowledge_anchor}Use this as the catalyst for the conversation.
145
  3. NO FABRICATION: Do not invent numbers. If no metric, focus on the industry context.
146
  4. TONE: {hinglish_note} Professional, high-empathy, peer-to-peer.
147
- 5. LIMIT: 280-310 characters.
148
- JSON: {{"body": "...", "cta": "...", "rationale": "..."}}"""
149
 
150
  prompt = f"CONTEXT: {json.dumps({'merchant': ident, 'trigger': trigger})}"
151
 
152
- # LOCAL FIRST
153
- res = await call_llm_local(prompt, system=sys_prompt)
154
- if not res:
155
- logger.info("Local failed, trying API fallback...")
156
- res = await call_llm_api(prompt, system=sys_prompt)
157
 
158
  if not res:
159
  body = f"{prefix} {owner_name}, noticed update in {ident.get('locality')}. Potential growth opportunity found. Review?"
@@ -162,20 +260,14 @@ JSON: {{"body": "...", "cta": "...", "rationale": "..."}}"""
162
  res["body"] = prune_message(res.get("body", ""))
163
  return res
164
 
 
165
  @app.on_event("startup")
166
  async def startup_event():
167
- logger.info("Performing OpenRouter API Dry Run...")
168
- if OPENROUTER_KEYS:
169
- try:
170
- res = await call_llm_api("ping", system="Respond with JSON pong")
171
- if res:
172
- logger.info("OpenRouter API Dry Run: SUCCESS")
173
- else:
174
- logger.warning("OpenRouter API Dry Run: FAILED (No response)")
175
- except Exception as e:
176
- logger.error(f"OpenRouter API Dry Run: ERROR - {e}")
177
- else:
178
- logger.warning("OpenRouter API Dry Run: SKIPPED (No keys found)")
179
 
180
  @app.get("/v1/healthz")
181
  async def healthz():
@@ -196,8 +288,8 @@ async def metadata():
196
  except: pass
197
  return {
198
  "team_name": "Vera Lead Solver",
199
- "version": "4.0",
200
- "engine": "Llama-3.2-3B-Autonomous",
201
  "local_model": model_status
202
  }
203
 
@@ -210,7 +302,6 @@ def push_context(data: ContextPayload):
210
  async def tick(req: TickRequest):
211
  tids = sorted(req.available_triggers, key=lambda t: PRIORITY_MAP.get(contexts["trigger"].get(t, {}).get("kind"), 0), reverse=True)
212
  results = []
213
- # Local inference is sequential for CPU safety. Limit to 1 to stay under 30s judge limit.
214
  for tid in tids[:1]:
215
  res = await process_trigger(tid)
216
  if res: results.append(res)
@@ -228,13 +319,8 @@ async def process_trigger(trigger_id: str):
228
  }
229
 
230
  AUTO_REPLY_PATTERNS = [
231
- r"thank you for contacting",
232
- r"we are currently away",
233
- r"this is an automated message",
234
- r"business hours",
235
- r"get back to you shortly",
236
- r"auto-reply",
237
- r"canned response"
238
  ]
239
 
240
  HOSTILE_PATTERNS = [
@@ -244,33 +330,16 @@ HOSTILE_PATTERNS = [
244
  @app.post("/v1/reply")
245
  async def reply(req: Dict[str, Any]):
246
  msg = req.get("message", "").lower()
247
-
248
- # Fast-Path: Detect Hostility
249
  for pattern in HOSTILE_PATTERNS:
250
  if re.search(pattern, msg):
251
- return {
252
- "action": "end",
253
- "rationale": f"Merchant expressed hostility/stop: {pattern}. Exiting gracefully."
254
- }
255
-
256
- # Fast-Path: Detect Merchant Auto-Reply
257
  for pattern in AUTO_REPLY_PATTERNS:
258
  if re.search(pattern, msg):
259
- logger.info(f"Auto-reply detected: {pattern}")
260
- return {
261
- "action": "wait",
262
- "wait_seconds": 3600,
263
- "rationale": "Merchant auto-reply detected. Waiting for human interaction to avoid loop."
264
- }
265
-
266
- # Normal Path: Use call_llm_local for replies
267
- prompt = f"Merchant said: {req.get('message', '')}. Turn: {req.get('turn_number')}. History: {req.get('history', [])}"
268
- sys = """Role: Vera AI Growth Strategist.
269
- GOAL: If merchant shows interest ('ok', 'lets do it', 'yes'), provide EXACT next step (e.g. 'I will renew your GBP now', 'I am setting up your patient recall').
270
- RULE: Detect 'Intent Threshold'. NO MORE qualifying questions if they said 'YES' or equivalent.
271
- TONE: Helpful, concise, assertive.
272
- LIMIT: 100-150 chars."""
273
- res = await call_llm_local(prompt, system=sys)
274
 
275
  reply_body = res.get("body", "I'm on it. Setting that up for you now. Ready to proceed?") if res else "Understood. I'll get that started. Ready?"
276
  return {"action": "send", "body": reply_body, "cta": "Reply YES"}
 
4
  import asyncio
5
  import time
6
  import re
7
+ import random
8
  from typing import List, Dict, Any, Optional
9
  from fastapi import FastAPI, Request
10
  from pydantic import BaseModel
 
13
 
14
  load_dotenv()
15
 
16
+ # Vera Engine - Autonomous 5-Tier Reliability Build
 
17
  logging.basicConfig(level=logging.INFO)
18
  logger = logging.getLogger("VeraEngine")
19
 
20
  app = FastAPI()
21
 
22
+ # --- Pydantic Schemas (Elastic) ---
23
  class ContextPayload(BaseModel):
24
  scope: Optional[str] = None
25
  context_id: Optional[str] = None
 
29
  class TickRequest(BaseModel):
30
  available_triggers: List[str] = []
31
 
32
+ # --- Key Rotation Logic ---
33
+ class KeyRotator:
34
+ def __init__(self, provider: str):
35
+ self.provider = provider
36
+ self.keys = []
37
+ # Scan for PROVIDER_KEY_1, PROVIDER_KEY_2, etc.
38
+ for i in range(1, 11):
39
+ key = os.getenv(f"{provider.upper()}_KEY_{i}")
40
+ if key: self.keys.append(key)
41
+ # Fallback to generic PROVIDER_API_KEY
42
+ if not self.keys:
43
+ key = os.getenv(f"{provider.upper()}_API_KEY")
44
+ if key: self.keys.append(key)
45
+
46
+ self.index = 0
47
+ self.lock = asyncio.Lock()
48
+
49
+ async def get_key(self) -> Optional[str]:
50
+ if not self.keys: return None
51
+ async with self.lock:
52
+ key = self.keys[self.index]
53
+ return key
54
+
55
+ async def rotate(self):
56
+ async with self.lock:
57
+ if self.keys:
58
+ self.index = (self.index + 1) % len(self.keys)
59
+ logger.info(f"Rotated key for {self.provider}. New index: {self.index}")
60
+
61
+ # Initialize Rotators
62
+ rotators = {
63
+ "openrouter": KeyRotator("openrouter"),
64
+ "gemini": KeyRotator("gemini"),
65
+ "groq": KeyRotator("groq"),
66
+ "deepseek": KeyRotator("deepseek")
67
+ }
68
 
69
  contexts = {"category": {}, "merchant": {}, "customer": {}, "trigger": {}}
70
+ llm_semaphore_local = asyncio.Semaphore(1)
71
 
72
  PRIORITY_MAP = {
73
  "drop_in_orders": 100, "revenue_drop": 95, "regulation_change": 90,
74
  "recall_due": 80, "high_churn": 70, "competitor_action": 65, "perf_spike": 60
75
  }
76
 
77
+ # --- Hierarchical Inference Engine ---
78
+ async def call_llm_chain(prompt: str, system: str = "") -> Optional[Dict]:
79
+ json_suffix = "\n\nRETURN JSON ONLY: {\"body\": \"...\", \"cta\": \"...\", \"rationale\": \"...\"}"
80
+ full_system = system + json_suffix
81
+
82
+ # Tier 1: OpenRouter
83
+ res = await call_tier_1_openrouter(prompt, full_system)
84
+ if res: return res
85
+
86
+ # Tier 2: Gemini REST
87
+ res = await call_tier_2_gemini(prompt, full_system)
88
+ if res: return res
89
+
90
+ # Tier 3: Groq
91
+ res = await call_tier_3_groq(prompt, full_system)
92
+ if res: return res
93
+
94
+ # Tier 4: DeepSeek
95
+ res = await call_tier_4_deepseek(prompt, full_system)
96
+ if res: return res
97
+
98
+ # Tier 5: Local Fallback
99
+ res = await call_tier_5_local(prompt, system) # Local has own prompt format
100
+ return res
101
+
102
+ async def call_tier_1_openrouter(prompt: str, system: str) -> Optional[Dict]:
103
+ rot = rotators["openrouter"]
104
+ key = await rot.get_key()
105
+ if not key: return None
106
+
107
+ models = ["poolside/laguna-m.1:free", "tencent/hy3:free", "google/gemini-2.0-flash-exp:free"]
108
+ async with httpx.AsyncClient() as client:
109
+ for model in models:
110
+ try:
111
+ res = await client.post(
112
+ "https://openrouter.ai/api/v1/chat/completions",
113
+ headers={
114
+ "Authorization": f"Bearer {key}",
115
+ "HTTP-Referer": "https://huggingface.co/spaces",
116
+ "X-Title": "Vera Engine Platinum"
117
+ },
118
+ json={
119
+ "model": model,
120
+ "messages": [{"role": "system", "content": system}, {"role": "user", "content": prompt}],
121
+ "response_format": {"type": "json_object"}
122
+ },
123
+ timeout=7.0
124
+ )
125
+ if res.status_code == 200:
126
+ return json.loads(res.json()["choices"][0]["message"]["content"])
127
+ if res.status_code in [401, 429]: await rot.rotate()
128
+ except: continue
129
+ return None
130
+
131
+ async def call_tier_2_gemini(prompt: str, system: str) -> Optional[Dict]:
132
+ rot = rotators["gemini"]
133
+ key = await rot.get_key()
134
+ if not key: return None
135
+ url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-exp:generateContent?key={key}"
136
+ async with httpx.AsyncClient() as client:
137
+ try:
138
+ res = await client.post(url, json={
139
+ "contents": [{"parts": [{"text": f"System: {system}\n\nUser: {prompt}"}]}]
140
+ }, timeout=7.0)
141
+ if res.status_code == 200:
142
+ text = res.json()["candidates"][0]["content"]["parts"][0]["text"]
143
+ # Clean potential markdown
144
+ text = re.sub(r'```json\n?|\n?```', '', text).strip()
145
+ return json.loads(text)
146
+ if res.status_code in [401, 429]: await rot.rotate()
147
+ except: pass
148
+ return None
149
+
150
+ async def call_tier_3_groq(prompt: str, system: str) -> Optional[Dict]:
151
+ rot = rotators["groq"]
152
+ key = await rot.get_key()
153
+ if not key: return None
154
+ async with httpx.AsyncClient() as client:
155
+ try:
156
+ res = await client.post("https://api.groq.com/openai/v1/chat/completions",
157
+ headers={"Authorization": f"Bearer {key}"},
158
+ json={
159
+ "model": "llama-3.3-70b-versatile",
160
+ "messages": [{"role": "system", "content": system}, {"role": "user", "content": prompt}],
161
+ "response_format": {"type": "json_object"}
162
+ }, timeout=7.0)
163
+ if res.status_code == 200:
164
+ return json.loads(res.json()["choices"][0]["message"]["content"])
165
+ if res.status_code in [401, 429]: await rot.rotate()
166
+ except: pass
167
+ return None
168
+
169
+ async def call_tier_4_deepseek(prompt: str, system: str) -> Optional[Dict]:
170
+ rot = rotators["deepseek"]
171
+ key = await rot.get_key()
172
+ if not key: return None
173
+ async with httpx.AsyncClient() as client:
174
+ try:
175
+ res = await client.post("https://api.deepseek.com/chat/completions",
176
+ headers={"Authorization": f"Bearer {key}"},
177
+ json={
178
+ "model": "deepseek-chat",
179
+ "messages": [{"role": "system", "content": system}, {"role": "user", "content": prompt}],
180
+ "response_format": {"type": "json_object"}
181
+ }, timeout=7.0)
182
+ if res.status_code == 200:
183
+ return json.loads(res.json()["choices"][0]["message"]["content"])
184
+ if res.status_code in [401, 429]: await rot.rotate()
185
+ except: pass
186
+ return None
187
+
188
+ async def call_tier_5_local(prompt: str, system: str) -> Optional[Dict]:
189
  async with llm_semaphore_local:
190
+ logger.info("Local Fallback: Llama 3.2 (3B)")
191
  payload = {
192
  "model": "llama3.2",
193
  "prompt": f"System: {system}\nStrict Grounding. No Fabrication. Context: {prompt}\nUser: Return JSON message.",
 
197
  }
198
  async with httpx.AsyncClient() as client:
199
  try:
200
+ response = await client.post("http://127.0.0.1:11434/api/generate", json=payload, timeout=90.0)
201
  if response.status_code == 200:
202
  return json.loads(response.json().get('response', '{}'))
203
  except Exception as e:
204
  logger.error(f"Local Model Error: {e}")
205
  return None
206
 
207
+ # --- Utilities ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
208
  def prune_message(text: str) -> str:
209
  text = text.replace("\n", " ").strip()
210
  if len(text) <= 315: return text
 
224
  lang_pref = ident.get('languages', ['en'])
225
  hinglish_note = "USE NATURAL HINGLISH (50% English words, 50% Hindi script/transliteration)." if 'hi' in lang_pref else "USE PROFESSIONAL ENGLISH."
226
 
 
227
  primary_metric = list(m.items())[0] if m else None
228
  verbatim_source = str(payload)[:150] if payload else None
229
 
 
233
  if verbatim_source:
234
  evidence_block += f"MANDATORY CITATION: '{verbatim_source}'. "
235
 
 
236
  beats = category.get("seasonal_beats", [])
237
  trends = category.get("trend_signals", [])
238
  knowledge_anchor = ""
239
+ if beats: knowledge_anchor = f"INDUSTRY CONTEXT: {beats[0].get('note', '')}. "
240
+ elif trends: knowledge_anchor = f"MARKET TREND: {trends[0].get('note', '')}. "
 
 
241
 
242
  sys_prompt = f"""Role: Senior Growth Strategist. Target: {prefix} {owner_name} in {ident.get('locality')}.
243
  GOAL: Act as a peer partner. Help the merchant grow using facts.
 
246
  2. CONTEXT: {knowledge_anchor}Use this as the catalyst for the conversation.
247
  3. NO FABRICATION: Do not invent numbers. If no metric, focus on the industry context.
248
  4. TONE: {hinglish_note} Professional, high-empathy, peer-to-peer.
249
+ 5. LIMIT: 280-310 characters."""
 
250
 
251
  prompt = f"CONTEXT: {json.dumps({'merchant': ident, 'trigger': trigger})}"
252
 
253
+ # Run the 5-tier chain
254
+ res = await call_llm_chain(prompt, system=sys_prompt)
 
 
 
255
 
256
  if not res:
257
  body = f"{prefix} {owner_name}, noticed update in {ident.get('locality')}. Potential growth opportunity found. Review?"
 
260
  res["body"] = prune_message(res.get("body", ""))
261
  return res
262
 
263
+ # --- Endpoints ---
264
  @app.on_event("startup")
265
  async def startup_event():
266
+ logger.info("Performing Multi-Tier API Connectivity Audit...")
267
+ for name, rot in rotators.items():
268
+ key = await rot.get_key()
269
+ status = "FOUND" if key else "MISSING"
270
+ logger.info(f"Provider {name}: {status} ({len(rot.keys)} keys loaded)")
 
 
 
 
 
 
 
271
 
272
  @app.get("/v1/healthz")
273
  async def healthz():
 
288
  except: pass
289
  return {
290
  "team_name": "Vera Lead Solver",
291
+ "version": "5.0",
292
+ "engine": "5-Tier-Hierarchical-Engine",
293
  "local_model": model_status
294
  }
295
 
 
302
  async def tick(req: TickRequest):
303
  tids = sorted(req.available_triggers, key=lambda t: PRIORITY_MAP.get(contexts["trigger"].get(t, {}).get("kind"), 0), reverse=True)
304
  results = []
 
305
  for tid in tids[:1]:
306
  res = await process_trigger(tid)
307
  if res: results.append(res)
 
319
  }
320
 
321
  AUTO_REPLY_PATTERNS = [
322
+ r"thank you for contacting", r"we are currently away", r"this is an automated message",
323
+ r"business hours", r"get back to you shortly", r"auto-reply", r"canned response"
 
 
 
 
 
324
  ]
325
 
326
  HOSTILE_PATTERNS = [
 
330
  @app.post("/v1/reply")
331
  async def reply(req: Dict[str, Any]):
332
  msg = req.get("message", "").lower()
 
 
333
  for pattern in HOSTILE_PATTERNS:
334
  if re.search(pattern, msg):
335
+ return {"action": "end", "rationale": f"Hostility: {pattern}"}
 
 
 
 
 
336
  for pattern in AUTO_REPLY_PATTERNS:
337
  if re.search(pattern, msg):
338
+ return {"action": "wait", "wait_seconds": 3600, "rationale": "Auto-reply"}
339
+
340
+ prompt = f"Merchant message: {req.get('message', '')}. Turn: {req.get('turn_number')}. History: {req.get('history', [])}"
341
+ sys = """Role: Vera AI. If merchant says 'ok/yes/do it', provide EXACT next step action. No more qualifying questions."""
342
+ res = await call_llm_chain(prompt, system=sys)
 
 
 
 
 
 
 
 
 
 
343
 
344
  reply_body = res.get("body", "I'm on it. Setting that up for you now. Ready to proceed?") if res else "Understood. I'll get that started. Ready?"
345
  return {"action": "send", "body": reply_body, "cta": "Reply YES"}