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.
This commit is contained in:
parent
bd1d3af713
commit
6a0d6f5a8d
2 changed files with 182 additions and 0 deletions
170
scripts/enis_monitor.py
Executable file
170
scripts/enis_monitor.py
Executable file
|
|
@ -0,0 +1,170 @@
|
||||||
|
#!/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()
|
||||||
12
scripts/enis_monitor.service
Normal file
12
scripts/enis_monitor.service
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
[Unit]
|
||||||
|
Description=Atlas Brain — Enis WhatsApp monitor + auto-reply
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=/usr/bin/python3 /opt/atlas/brain/enis_monitor.py
|
||||||
|
Restart=always
|
||||||
|
RestartSec=10
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
Loading…
Reference in a new issue