Rachit-Mangawa commited on
Commit
7f971bd
·
1 Parent(s): 323c8ef

feat: Integrate local Phi-3.5 fallback for API resilience

Browse files
Files changed (2) hide show
  1. Dockerfile +20 -9
  2. app.py +46 -19
Dockerfile CHANGED
@@ -1,15 +1,26 @@
1
- # Build stage
2
- FROM python:3.10-slim as builder
 
 
 
 
 
 
 
3
  WORKDIR /app
4
  COPY requirements.txt .
5
- RUN pip install --user --no-cache-dir -r requirements.txt
6
 
7
- # Run stage
8
- FROM python:3.10-slim
9
- WORKDIR /app
10
- COPY --from=builder /root/.local /root/.local
11
  COPY . .
12
- ENV PATH=/root/.local/bin:$PATH
 
13
  ENV PORT=8086
 
 
 
 
 
 
 
14
  EXPOSE 8086
15
- CMD ["python", "app.py"]
 
1
+ FROM python:3.10-slim
2
+
3
+ # System setup
4
+ RUN apt-get update && apt-get install -y curl procps && rm -rf /var/lib/apt/lists/*
5
+
6
+ # Install Ollama Binary
7
+ RUN curl -L https://ollama.com/download/ollama-linux-amd64 -o /usr/bin/ollama && \
8
+ chmod +x /usr/bin/ollama
9
+
10
  WORKDIR /app
11
  COPY requirements.txt .
12
+ RUN pip install --no-cache-dir -r requirements.txt
13
 
 
 
 
 
14
  COPY . .
15
+
16
+ # Environment Variables
17
  ENV PORT=8086
18
+ ENV OLLAMA_HOST=127.0.0.1:11434
19
+ ENV PYTHONUNBUFFERED=1
20
+
21
+ # Execution Script: Starts Ollama, pulls Phi, starts App
22
+ RUN echo '#!/bin/bash\nollama serve & sleep 5 && ollama pull phi3.5 && python app.py' > /app/run.sh && \
23
+ chmod +x /app/run.sh
24
+
25
  EXPOSE 8086
26
+ CMD ["/app/run.sh"]
app.py CHANGED
@@ -12,7 +12,7 @@ from dotenv import load_dotenv
12
 
13
  load_dotenv()
14
 
15
- # Vera Engine - Platinum Cloud Deployment Build
16
  logging.basicConfig(level=logging.INFO)
17
  logger = logging.getLogger("VeraEngine")
18
 
@@ -53,25 +53,46 @@ PRIORITY_MAP = {
53
 
54
  async def call_llm(prompt: str, system: str = "") -> Optional[Dict]:
55
  async with llm_semaphore:
 
56
  key = await or_rotator.get_key()
57
- if not key: return None
58
- headers = {"Authorization": f"Bearer {key}", "Content-Type": "application/json", "HTTP-Referer": "https://vera.ai", "X-Title": "Vera Engine"}
59
- payload = {
60
- "model": "google/gemini-2.0-flash-exp:free",
61
- "messages": [
62
- {"role": "system", "content": system + "\n\nRETURN JSON ONLY. NO MARKDOWN. NO FABRICATION."},
63
- {"role": "user", "content": prompt}
64
- ],
65
- "temperature": 0.0,
66
- "response_format": {"type": "json_object"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
  }
68
  async with httpx.AsyncClient() as client:
69
  try:
70
- response = await client.post("https://openrouter.ai/api/v1/chat/completions", headers=headers, json=payload, timeout=60.0)
71
  if response.status_code == 200:
72
- return json.loads(response.json()["choices"][0]["message"]["content"])
 
 
 
73
  return None
74
- except Exception: return None
75
 
76
  def prune_message(text: str) -> str:
77
  text = text.replace("\n", " ").strip()
@@ -107,16 +128,23 @@ JSON: {{"body": "...", "cta": "...", "rationale": "..."}}"""
107
  if not res:
108
  v, c = m.get('views', '2410'), m.get('calls', '18')
109
  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?"
110
- res = {"body": body, "cta": "Reply YES", "rationale": "Fallback"}
111
 
112
  res["body"] = prune_message(res.get("body", ""))
113
  return res
114
 
115
  @app.get("/v1/healthz")
116
- def healthz(): return {"status": "ok"}
 
 
 
 
 
 
 
117
 
118
  @app.get("/v1/metadata")
119
- def metadata(): return {"team_name": "Vera Lead Solver", "version": "Platinum-Docker-v1"}
120
 
121
  @app.post("/v1/context")
122
  def push_context(data: ContextPayload):
@@ -133,7 +161,7 @@ async def process_trigger(trigger_id: str):
133
  return {
134
  "conversation_id": f"conv_{trigger_id}", "merchant_id": trigger.get("merchant_id"),
135
  "customer_id": trigger.get("customer_id"), "send_as": "merchant_on_behalf" if trigger.get("scope") == "customer" else "vera",
136
- "trigger_id": trigger_id, "template_name": "vera_plat_v1", "body": composed["body"],
137
  "cta": composed["cta"], "suppression_key": trigger_id, "rationale": composed.get("rationale")
138
  }
139
 
@@ -144,7 +172,6 @@ async def tick(req: TickRequest):
144
  for tid in tids:
145
  res = await process_trigger(tid)
146
  if res: results.append(res)
147
- await asyncio.sleep(2.0)
148
  return {"actions": results}
149
 
150
  @app.post("/v1/reply")
 
12
 
13
  load_dotenv()
14
 
15
+ # Vera Engine - Unstoppable Platinum Build (Local Fallback)
16
  logging.basicConfig(level=logging.INFO)
17
  logger = logging.getLogger("VeraEngine")
18
 
 
53
 
54
  async def call_llm(prompt: str, system: str = "") -> Optional[Dict]:
55
  async with llm_semaphore:
56
+ # Tier 1: OpenRouter (Gemini 2.0 Flash)
57
  key = await or_rotator.get_key()
58
+ if key:
59
+ headers = {"Authorization": f"Bearer {key}", "Content-Type": "application/json", "HTTP-Referer": "https://vera.ai", "X-Title": "Vera Engine"}
60
+ payload = {
61
+ "model": "google/gemini-2.0-flash-exp:free",
62
+ "messages": [
63
+ {"role": "system", "content": system + "\n\nRETURN JSON ONLY. NO MARKDOWN. NO FABRICATION."},
64
+ {"role": "user", "content": prompt}
65
+ ],
66
+ "temperature": 0.0,
67
+ "response_format": {"type": "json_object"}
68
+ }
69
+ async with httpx.AsyncClient() as client:
70
+ try:
71
+ response = await client.post("https://openrouter.ai/api/v1/chat/completions", headers=headers, json=payload, timeout=15.0)
72
+ if response.status_code == 200:
73
+ return json.loads(response.json()["choices"][0]["message"]["content"])
74
+ logger.warning(f"OpenRouter failed with status {response.status_code}. Checking local fallback...")
75
+ except Exception as e:
76
+ logger.warning(f"OpenRouter connection error: {e}. Checking local fallback...")
77
+
78
+ # Tier 2: Local Fallback (Ollama + Phi-3.5-mini)
79
+ logger.info("Triggering Local Fallback: Phi-3.5")
80
+ local_payload = {
81
+ "model": "phi3.5",
82
+ "prompt": f"System: {system}. Under 320 chars, 1 CTA, strict grounding. Context: {prompt}\nUser: Generate the message JSON.",
83
+ "stream": False,
84
+ "format": "json",
85
+ "options": {"num_predict": 150, "temperature": 0.1}
86
  }
87
  async with httpx.AsyncClient() as client:
88
  try:
89
+ response = await client.post("http://127.0.0.1:11434/api/generate", json=local_payload, timeout=50.0)
90
  if response.status_code == 200:
91
+ res_text = response.json().get('response')
92
+ return json.loads(res_text)
93
+ except Exception as e:
94
+ logger.error(f"Local Fallback Failed: {e}")
95
  return None
 
96
 
97
  def prune_message(text: str) -> str:
98
  text = text.replace("\n", " ").strip()
 
128
  if not res:
129
  v, c = m.get('views', '2410'), m.get('calls', '18')
130
  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?"
131
+ res = {"body": body, "cta": "Reply YES", "rationale": "Hard Fallback"}
132
 
133
  res["body"] = prune_message(res.get("body", ""))
134
  return res
135
 
136
  @app.get("/v1/healthz")
137
+ async def healthz():
138
+ # Only return ok if local model is ready
139
+ async with httpx.AsyncClient() as client:
140
+ try:
141
+ res = await client.get("http://127.0.0.1:11434/api/tags")
142
+ if "phi3.5" in res.text: return {"status": "ok", "local_model": "ready"}
143
+ except: pass
144
+ return {"status": "starting", "message": "Ollama/Phi-3.5 warming up"}
145
 
146
  @app.get("/v1/metadata")
147
+ def metadata(): return {"team_name": "Vera Lead Solver", "version": "Platinum-Unstoppable-v1"}
148
 
149
  @app.post("/v1/context")
150
  def push_context(data: ContextPayload):
 
161
  return {
162
  "conversation_id": f"conv_{trigger_id}", "merchant_id": trigger.get("merchant_id"),
163
  "customer_id": trigger.get("customer_id"), "send_as": "merchant_on_behalf" if trigger.get("scope") == "customer" else "vera",
164
+ "trigger_id": trigger_id, "template_name": "vera_unstoppable_v1", "body": composed["body"],
165
  "cta": composed["cta"], "suppression_key": trigger_id, "rationale": composed.get("rationale")
166
  }
167
 
 
172
  for tid in tids:
173
  res = await process_trigger(tid)
174
  if res: results.append(res)
 
175
  return {"actions": results}
176
 
177
  @app.post("/v1/reply")