atlas-os/scripts/enis_monitor.py
Chaib Aarab 6a0d6f5a8d feat: add Enis WhatsApp monitor + auto-reply service
Polls Evolution API every 30s for new messages from a contact.
LLM chain: local (atlas-free/hermes3) → Groq 70B fallback.
Guardrails: no secrets, no finance, no private data.
Deploy: copy enis_monitor.py, install systemd unit, set state file.
2026-06-22 20:07:08 +02:00

170 lines
6.7 KiB
Python
Executable file

#!/usr/bin/env python3
"""
Atlas Brain — Enis WhatsApp monitor + auto-reply
Polls Evolution API for new Enis messages, replies via local LLM with guardrails.
State tracked in /opt/atlas/brain/enis_state.json
"""
import json, os, time, requests, logging, re
from pathlib import Path
from datetime import datetime
STATE_FILE = "/opt/atlas/brain/enis_state.json"
LOG_FILE = "/opt/atlas/logs/enis_monitor.log"
EVO_URL = "http://localhost:8120"
EVO_KEY = "atlas-evo-key-2026"
ENIS_JID = "124674495234167@lid"
LLM_URL = "http://localhost:8888/v1/chat/completions"
POLL_SEC = 30
Path("/opt/atlas/brain").mkdir(parents=True, exist_ok=True)
Path("/opt/atlas/logs").mkdir(parents=True, exist_ok=True)
logging.basicConfig(
filename=LOG_FILE, level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s"
)
log = logging.getLogger("enis-monitor")
SYSTEM_PROMPT = """You are Atlas Brain — the AI core of Atlas Corporation, built by Chaib Aarab in Tilburg, NL.
You are talking to Enis, a friend and fellow AI/tech builder who is building his own second brain.
Your personality: knowledgeable, direct, friendly, Muslim-aware (use mashAllah/SubhanAllah naturally when fitting).
You speak as a peer builder, not as an assistant.
HARD GUARDRAILS — never break these:
1. NEVER share passwords, API keys, access tokens, internal IPs, or any credentials.
2. NEVER discuss Chaib's personal finances, bijstand, gemeente/municipality matters, or welfare benefits.
3. NEVER make financial promises or commitments on behalf of Atlas Corporation.
4. NEVER claim to be human or deny being an AI when sincerely asked.
5. NEVER discuss client data, Odoo leads, or confidential business details.
6. NEVER share the internal architecture (specific ports, container names, secret paths).
7. If asked to do something Chaib hasn't approved, say you'll flag it for him.
8. If Enis asks for the git repo: https://git.atlascorporation.nl/chaib/atlas-os (this is public, safe to share).
9. Keep replies concise — WhatsApp, not essays. 2-4 sentences max unless a technical question needs more.
10. You can discuss: AI/LLM tech, second brain architecture, Atlas OS, how you work (at a high level), Enis's projects.
If you are uncertain whether something is safe to share, say "I'll check with Chaib on that."
"""
def load_state():
if Path(STATE_FILE).exists():
return json.loads(Path(STATE_FILE).read_text())
return {"last_ts": 0, "last_id": ""}
def save_state(s):
Path(STATE_FILE).write_text(json.dumps(s))
def get_new_enis_messages(since_ts):
try:
r = requests.post(
f"{EVO_URL}/chat/findMessages/atlas",
headers={"apikey": EVO_KEY, "Content-Type": "application/json"},
json={"where": {"key": {"remoteJid": ENIS_JID, "fromMe": False}}, "limit": 20},
timeout=10
)
data = r.json()
records = data.get("messages", {}).get("records", []) if isinstance(data, dict) else data
new = [m for m in records if m.get("messageTimestamp", 0) > since_ts]
new.sort(key=lambda m: m.get("messageTimestamp", 0))
return new
except Exception as e:
log.error(f"fetch error: {e}")
return []
def extract_text(m):
msg = m.get("message", {})
if "conversation" in msg: return msg["conversation"]
if "extendedTextMessage" in msg: return msg["extendedTextMessage"].get("text", "")
if "imageMessage" in msg: return f"[image: {msg['imageMessage'].get('caption', '')}]"
if "audioMessage" in msg: return "[voice message]"
if "videoMessage" in msg: return "[video]"
if "documentMessage" in msg: return f"[file: {msg['documentMessage'].get('fileName', '')}]"
return ""
def _groq_key():
try:
txt = open('/opt/atlas/router-secrets.env').read()
m = re.search(r'GROQ_API_KEY=(.+)', txt)
return m.group(1).strip() if m else ''
except:
return ''
def generate_reply(user_text):
messages = [
{'role': 'system', 'content': SYSTEM_PROMPT},
{'role': 'user', 'content': user_text}
]
try:
r = requests.post(LLM_URL, json={
'model': 'atlas-free', 'messages': messages,
'max_tokens': 300, 'stream': False, 'temperature': 0.7
}, timeout=30)
result = r.json()['choices'][0]['message']['content'].strip()
if result:
log.info('replied via local LLM')
return result
except Exception as e:
log.warning(f'local LLM fail: {e}')
try:
key = _groq_key()
r = requests.post(
'https://api.groq.com/openai/v1/chat/completions',
json={'model': 'llama-3.3-70b-versatile', 'messages': messages, 'max_tokens': 300},
headers={'Authorization': f'Bearer {key}'}, timeout=30
)
result = r.json()['choices'][0]['message']['content'].strip()
log.info('replied via Groq')
return result
except Exception as e:
log.error(f'Groq fail: {e}')
return None
def send_reply(text):
try:
payload = {"number": ENIS_JID, "textMessage": {"text": text}}
r = requests.post(
f"{EVO_URL}/message/sendText/atlas",
headers={"apikey": EVO_KEY, "Content-Type": "application/json"},
json=payload, timeout=15
)
resp = r.json()
if resp.get("key"):
log.info(f"sent OK: id={resp['key'].get('id','?')}")
return True
log.warning(f"send failed: {resp}")
return False
except Exception as e:
log.error(f"send error: {e}")
return False
def main():
log.info("Enis monitor started")
state = load_state()
while True:
try:
msgs = get_new_enis_messages(state["last_ts"])
for m in msgs:
ts = m.get("messageTimestamp", 0)
mid = m.get("key", {}).get("id", "")
txt = extract_text(m)
if not txt or mid == state.get("last_id"):
state["last_ts"] = max(state["last_ts"], ts)
save_state(state)
continue
log.info(f"new msg ts={ts} id={mid}: {txt[:80]}")
reply = generate_reply(txt)
if reply:
time.sleep(2) # brief typing pause
ok = send_reply(reply)
log.info(f"reply={'sent' if ok else 'FAILED'}: {reply[:60]}")
state["last_ts"] = max(state["last_ts"], ts)
state["last_id"] = mid
save_state(state)
time.sleep(3) # gap between multi-message bursts
except Exception as e:
log.error(f"loop error: {e}")
time.sleep(POLL_SEC)
if __name__ == "__main__":
main()