y
import requests
import re
import json
class LocalInferenceEngine:
def init(self, base_url: str = "http://localhost:11434"):
self.base_url = base_url
self.default_model = "gemma2:2b"
self.fallback_model = "gemma2:9b"
def generate_campaign_data(self, prompt: str) -> dict:
"""
Generates structured campaign data using local Gemma models.
Implements fallback logic and JSON sanitization.
"""
try:
return self._query_model(prompt, self.default_model)
except (json.JSONDecodeError, requests.RequestException):
# Fallback to larger model if 2b fails or produces invalid JSON
print("Warning: Default model failed. Falling back to 9b.")
return self._query_model(prompt, self.fallback_model)
def _query_model(self, prompt: str, model: str) -> dict:
payload = {
"model": model,
"prompt": prompt,
"stream": False,
"options": {
"temperature": 0.2,
"num_ctx": 4096
}
}
response = requests.post(
f"{self.base_url}/api/generate",
json=payload,
timeout=30
)
response.raise_for_status()
raw_output = response.json().get("response", "")
return self._extract_json(raw_output)
def _extract_json(self, text: str) -> dict:
"""
Extracts and sanitizes JSON from model output.
Handles markdown fences and escape artifacts.
"""
# Remove markdown code blocks if present
json_match = re.search(r"```(?:json)?\s*(.*?)\s*```", text, re.DOTALL)
if json_match:
text = json_match.group(1)
# Fix common escaping issues
text = text.replace("\\n", "\n").replace("\\\"", "\"")
try:
return json.loads(text)
except json.JSONDecodeError as e:
raise ValueError(f"Failed to parse model output: {e}")
**Rationale:** The `_extract_json` method is critical. Local models often wrap JSON in markdown blocks or introduce escape characters. This sanitizer ensures the automation pipeline receives valid JSON, preventing crashes during campaign execution. The fallback mechanism ensures reliability when the smaller model struggles with complex prompts.
#### 2. Flask Backend with SQLite Integration
The backend handles webhook ingestion, lead scoring, and campaign scheduling.
```python
# server.py
import sqlite3
import os
from flask import Flask, request, jsonify
from inference import LocalInferenceEngine
app = Flask(__name__, static_folder="public")
engine = LocalInferenceEngine()
DB_PATH = os.environ.get("DB_PATH", "automation.db")
def init_db():
"""Initializes SQLite database with required tables."""
with sqlite3.connect(DB_PATH) as conn:
conn.execute("""
CREATE TABLE IF NOT EXISTS leads (
id INTEGER PRIMARY KEY AUTOINCREMENT,
contact_id TEXT UNIQUE,
score REAL DEFAULT 0.0,
preferences TEXT,
last_interaction TEXT
)
""")
conn.execute("""
CREATE TABLE IF NOT EXISTS campaigns (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT,
schedule TEXT,
status TEXT DEFAULT 'pending'
)
""")
conn.commit()
@app.route("/webhook/whatsapp", methods=["POST"])
def handle_whatsapp():
"""Processes inbound WhatsApp messages and updates lead scores."""
payload = request.json
contact_id = payload.get("from")
message = payload.get("body", "")
if not contact_id:
return jsonify({"error": "Missing contact_id"}), 400
# Update lead score based on message content
with sqlite3.connect(DB_PATH) as conn:
conn.execute(
"UPDATE leads SET score = score + 1.0, last_interaction = datetime('now') WHERE contact_id = ?",
(contact_id,)
)
if conn.rowcount == 0:
conn.execute(
"INSERT INTO leads (contact_id, score, last_interaction) VALUES (?, 1.0, datetime('now'))",
(contact_id,)
)
conn.commit()
# Generate response using local LLM
prompt = f"Generate a friendly reply to: '{message}'. Output as JSON: {{\"reply\": \"...\"}}"
try:
response_data = engine.generate_campaign_data(prompt)
reply_text = response_data.get("reply", "Thanks for your message!")
except Exception:
reply_text = "Message received. We'll follow up shortly."
return jsonify({"status": "processed", "reply": reply_text})
@app.route("/")
def dashboard():
return app.send_static_file("index.html")
if __name__ == "__main__":
init_db()
app.run(host="127.0.0.1", port=5000)
Rationale: SQLite handles state without external dependencies. The handle_whatsapp route demonstrates real-time processing: updating lead scores and generating a response via the local inference engine. This keeps the entire workflow self-contained.
3. RTL-First UI with Tailwind CDN
The frontend uses Tailwind CDN for styling and supports right-to-left layouts for Hebrew content.
<!-- public/index.html -->
<!DOCTYPE html>
<html lang="he" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Marketing Automation Hub</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
/* Glassmorphism utility */
.glass-card {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
</style>
</head>
<body class="bg-gradient-to-br from-slate-900 to-slate-800 min-h-screen text-white font-sans">
<div class="container mx-auto p-6">
<header class="mb-8">
<h1 class="text-3xl font-bold">מערכת אוטומציה שיווקית</h1>
<p class="text-slate-400 mt-2">Local-first automation with Ollama</p>
</header>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Campaign Status Card -->
<div class="glass-card rounded-xl p-6 shadow-lg">
<h2 class="text-xl font-semibold mb-4">מצב קמפיין</h2>
<div class="space-y-3">
<div class="flex justify-between items-center">
<span>Active Leads</span>
<span class="bg-emerald-500/20 text-emerald-300 px-3 py-1 rounded-full text-sm">142</span>
</div>
<div class="flex justify-between items-center">
<span>Conversion Rate</span>
<span class="bg-blue-500/20 text-blue-300 px-3 py-1 rounded-full text-sm">8.4%</span>
</div>
</div>
</div>
<!-- Quick Actions Card -->
<div class="glass-card rounded-xl p-6 shadow-lg">
<h2 class="text-xl font-semibold mb-4">פעולות מהירות</h2>
<button class="w-full bg-indigo-600 hover:bg-indigo-700 text-white py-2 rounded-lg transition-colors mb-3">
יצוא נתונים
</button>
<button class="w-full bg-slate-700 hover:bg-slate-600 text-white py-2 rounded-lg transition-colors">
נהל קמפיין חדש
</button>
</div>
</div>
</div>
</body>
</html>
Rationale: The dir="rtl" attribute on the <html> tag ensures the browser applies right-to-left layout rules. Tailwind's logical properties (e.g., ms- for margin-start) automatically adapt to RTL. The glassmorphism effect uses backdrop-filter for a modern aesthetic without heavy CSS frameworks.
4. Deployment via Tailscale Funnel
To expose the app securely:
# Start the Flask app
python server.py
# In a separate terminal, expose port 5000 via Tailscale
tailscale funnel 5000
Rationale: Tailscale Funnel creates a secure HTTPS tunnel to the local Flask server. This avoids port forwarding and provides authentication via Tailscale's identity management. The app is accessible via a Tailscale URL without exposing the host to the public internet.
Pitfall Guide
1. JSON Fragmentation in Model Output
Explanation: Local models may output JSON wrapped in markdown fences or include trailing text, causing json.loads to fail.
Fix: Implement a robust sanitizer that strips markdown blocks and handles escape sequences before parsing. Always validate the output structure.
2. RTL Layout Conflicts
Explanation: Using physical properties like ml- (margin-left) in RTL contexts breaks layout alignment.
Fix: Use Tailwind's logical properties (ms-, me-, ps-, pe-) which automatically flip based on dir="rtl". Test with dir="rtl" enabled during development.
3. Model Context Overflow
Explanation: Smaller models like gemma2:2b have limited context windows. Long prompts or conversation histories may cause truncation or degraded output.
Fix: Implement prompt truncation strategies. Use the fallback to gemma2:9b for complex tasks requiring more context. Monitor context usage and adjust num_ctx in Ollama options.
4. Tailscale Funnel Security Misconfiguration
Explanation: Exposing a local app via Tailscale Funnel without proper ACLs can allow unauthorized access.
Fix: Configure Tailscale ACLs to restrict access to specific users or tags. Use Tailscale's built-in authentication and avoid exposing sensitive endpoints without additional auth layers.
5. SQLite Write Contention
Explanation: Concurrent writes to SQLite can cause "database is locked" errors, especially in webhook-heavy scenarios.
Fix: Enable WAL mode (PRAGMA journal_mode=WAL;) for better concurrency. Serialize writes where possible or use a connection pool if scaling beyond single-file limits.
6. WhatsApp Session Persistence
Explanation: whatsapp-web.js sessions may expire or fail to restore, breaking webhook integration.
Fix: Implement session persistence by saving and restoring session data to disk. Handle re-authentication flows gracefully and monitor session health.
7. Hebrew Tokenization Quirks
Explanation: While Gemma supports Hebrew, prompt structure and tokenization can affect output quality.
Fix: Use system prompts in the target language. Test tokenization with sample Hebrew text and adjust temperature/settings for optimal generation. Ensure the UI handles bidirectional text correctly.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| High-Volume Campaigns | Cloud API | Scalability and throughput | High (Per-token) |
| Sensitive Data Processing | Local Ollama | Zero data egress, privacy | Hardware CapEx |
| Simple Lead Scoring | Gemma 2b | Low latency, minimal resources | Free |
| Complex Reasoning/Copy | Gemma 9b | Higher accuracy, better context | Higher VRAM |
| Multi-User Collaboration | Postgres + Docker | Concurrency, scalability | Infrastructure |
| Rapid Prototyping | Single-File Flask | Fast iteration, low overhead | Developer Time |
Configuration Template
# config.py
import os
class Config:
# Database
DB_PATH = os.environ.get("DB_PATH", "automation.db")
SQLITE_WAL = True
# Ollama
OLLAMA_BASE_URL = os.environ.get("OLLAMA_URL", "http://localhost:11434")
DEFAULT_MODEL = "gemma2:2b"
FALLBACK_MODEL = "gemma2:9b"
NUM_CTX = 4096
TEMPERATURE = 0.2
# Flask
FLASK_HOST = "127.0.0.1"
FLASK_PORT = 5000
SECRET_KEY = os.environ.get("SECRET_KEY", "change-me-in-production")
# Tailscale
TAILSCALE_FUNNEL_PORT = 5000
# UI
RTL_ENABLED = True
TAILWIND_CDN = "https://cdn.tailwindcss.com"
Quick Start Guide
- Install Ollama: Download and install Ollama. Run
ollama pull gemma2:2b and ollama pull gemma2:9b.
- Setup Backend: Create a virtual environment, install Flask and requests, and save the
server.py and inference.py files.
- Run Application: Execute
python server.py. The Flask app will start on http://127.0.0.1:5000.
- Expose Securely: Run
tailscale funnel 5000 to generate a secure HTTPS URL for remote access.
- Test Integration: Use
curl to send a test webhook payload to /webhook/whatsapp and verify the response and database updates.
This architecture provides a robust, privacy-preserving foundation for marketing automation. By leveraging local inference, file-based storage, and secure networking, teams can build automation tools that are resilient, portable, and fully under their control.