Un client poate avea mai multi angajati care scriu pe WhatsApp de pe numere diferite; toti gasesc acum acelasi profil (rag/client.py, ~/.maria-bridge/clients.json). Profilul intra in prompt sub o sectiune DESPRE CLIENT, la fiecare mesaj, stateless ca restul Mariei — dar nu atinge cautarea/scorul din rag/rank.py, ca sa nu strice pragurile deja calibrate. Administrare din dashboard (sectiune noua "Clienti cunoscuti"), nu din chat, ca sa ramana pe canalul autentificat. 117/117 teste maria-whatsapp-bridge + teste noi pentru rutele de dashboard. Co-Authored-By: Claude Agent <noreply@anthropic.com>
88 lines
2.8 KiB
Python
88 lines
2.8 KiB
Python
#!/usr/bin/env python3
|
|
"""Profil de client (firma), asociat cu mai multe numere de WhatsApp.
|
|
|
|
Un singur fisier `clients.json`: la ordinul de marime de aici (zeci de firme)
|
|
nu justifica o baza de date, si un singur fisier e mai usor de editat de mana
|
|
din dashboard decat mai multe fisiere sincronizate intre ele. Numerele stau
|
|
IN clientul lor (nu intr-o mapare separata numar->client), fiindca cautarea e
|
|
mereu "cine e clientul care are numarul X" — o mapare separata ar trebui
|
|
tinuta sincronizata cu clients.json, doua surse de adevar pentru acelasi lucru.
|
|
|
|
Mesaj de grup fara `participant` (adica bridge-ul n-a stiut cine a scris) nu
|
|
identifica niciun client: JID-ul de grup nu e un numar de telefon, si a-l
|
|
cauta printre numere ar asocia gresit tot grupul cu primul client care se
|
|
potriveste din intamplare.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import re
|
|
|
|
import config
|
|
|
|
|
|
def _norm(numar_brut: str) -> str:
|
|
return re.sub(r"\D", "", numar_brut or "")
|
|
|
|
|
|
def numar(msg: dict) -> str:
|
|
"""Numarul care a scris, normalizat (doar cifre). Gol daca nu se poate sti."""
|
|
jid = msg.get("participant")
|
|
if not jid:
|
|
frm = msg.get("from") or ""
|
|
if frm.endswith("@g.us"):
|
|
return "" # grup fara participant: nu stim cine a scris
|
|
jid = frm
|
|
return _norm(jid.split("@")[0])
|
|
|
|
|
|
def _load() -> dict:
|
|
try:
|
|
return json.loads(config.CLIENTS_FILE.read_text(encoding="utf-8"))
|
|
except (OSError, ValueError):
|
|
return {}
|
|
|
|
|
|
def profil(msg_sau_jid) -> str | None:
|
|
"""Textul de profil al clientului asociat, sau `None` daca numarul nu e cunoscut."""
|
|
n = numar(msg_sau_jid) if isinstance(msg_sau_jid, dict) else _norm(
|
|
str(msg_sau_jid).split("@")[0])
|
|
if not n:
|
|
return None
|
|
for client in _load().values():
|
|
if n in client.get("numere", []):
|
|
return client.get("profil")
|
|
return None
|
|
|
|
|
|
def toti() -> dict:
|
|
return _load()
|
|
|
|
|
|
def salveaza(id: str, nume: str, profil: str, numere: list[str]) -> None:
|
|
"""Upsert. Normalizeaza fiecare numar acum, ca lookup-ul sa functioneze mereu."""
|
|
date = _load()
|
|
date[id] = {
|
|
"nume": nume,
|
|
"profil": profil,
|
|
"numere": [n for n in (_norm(x) for x in numere) if n],
|
|
}
|
|
tmp = config.CLIENTS_FILE.with_suffix(".tmp")
|
|
config.STATE_DIR.mkdir(parents=True, exist_ok=True)
|
|
tmp.write_text(json.dumps(date, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
os.replace(tmp, config.CLIENTS_FILE)
|
|
|
|
|
|
def sterge(id: str) -> bool:
|
|
date = _load()
|
|
if id not in date:
|
|
return False
|
|
del date[id]
|
|
tmp = config.CLIENTS_FILE.with_suffix(".tmp")
|
|
config.STATE_DIR.mkdir(parents=True, exist_ok=True)
|
|
tmp.write_text(json.dumps(date, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
os.replace(tmp, config.CLIENTS_FILE)
|
|
return True
|