feat(maria): profil de client (firma), asociat cu mai multe numere
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>
This commit is contained in:
@@ -70,6 +70,8 @@ MARIA_LOG_DIR = MARIA_STATE_DIR / "logs"
|
||||
MARIA_VENV_PY = MARIA_STATE_DIR / "venv" / "bin" / "python"
|
||||
MARIA_RAG_DIR = _BRIDGE.parent / "maria-whatsapp-bridge" / "rag"
|
||||
_MARIA_SAFE_NAME = re.compile(r"^[A-Za-z0-9._-]{1,200}$")
|
||||
MARIA_CLIENTS_FILE = MARIA_STATE_DIR / "clients.json"
|
||||
_MARIA_CLIENT_ID = re.compile(r"^[a-z0-9_-]{1,40}$")
|
||||
|
||||
|
||||
def maria_env() -> dict:
|
||||
@@ -239,6 +241,56 @@ def maria_delete_document(name: str) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def maria_clients() -> list[dict]:
|
||||
"""Oglinda lui `maria-whatsapp-bridge/rag/client.py:toti` — tine-o la fel.
|
||||
|
||||
Nu importam modulul acela (colizeaza pe "config", ca la documente).
|
||||
"""
|
||||
try:
|
||||
date = json.loads(MARIA_CLIENTS_FILE.read_text(encoding="utf-8"))
|
||||
except (OSError, ValueError):
|
||||
date = {}
|
||||
return [{"id": cid, **c} for cid, c in sorted(date.items())]
|
||||
|
||||
|
||||
def _maria_save_clients(date: dict) -> None:
|
||||
MARIA_STATE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
tmp = MARIA_CLIENTS_FILE.with_suffix(".tmp")
|
||||
tmp.write_text(json.dumps(date, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
os.replace(tmp, MARIA_CLIENTS_FILE)
|
||||
|
||||
|
||||
def maria_save_client(cid: str, nume: str, profil: str, numere: list[str]) -> None:
|
||||
if not cid or not _MARIA_CLIENT_ID.match(cid):
|
||||
raise ValueError(f"id de client invalid: {cid!r}")
|
||||
normalizate = [n for n in (re.sub(r"\D", "", x) for x in numere) if n]
|
||||
try:
|
||||
date = json.loads(MARIA_CLIENTS_FILE.read_text(encoding="utf-8"))
|
||||
except (OSError, ValueError):
|
||||
date = {}
|
||||
for alt_id, alt in date.items():
|
||||
if alt_id == cid:
|
||||
continue
|
||||
furate = set(normalizate) & set(alt.get("numere", []))
|
||||
if furate:
|
||||
raise ValueError(
|
||||
f"numarul {sorted(furate)[0]} e deja atribuit clientului {alt.get('nume', alt_id)!r}")
|
||||
date[cid] = {"nume": nume, "profil": profil, "numere": normalizate}
|
||||
_maria_save_clients(date)
|
||||
|
||||
|
||||
def maria_delete_client(cid: str) -> bool:
|
||||
try:
|
||||
date = json.loads(MARIA_CLIENTS_FILE.read_text(encoding="utf-8"))
|
||||
except (OSError, ValueError):
|
||||
date = {}
|
||||
if cid not in date:
|
||||
return False
|
||||
del date[cid]
|
||||
_maria_save_clients(date)
|
||||
return True
|
||||
|
||||
|
||||
def _maria_run(script: str, timeout: float = 600.0, extra_args: list[str] | None = None) -> dict:
|
||||
interpreter = str(MARIA_VENV_PY) if MARIA_VENV_PY.exists() else sys.executable
|
||||
args = [interpreter, str(MARIA_RAG_DIR / script), *(extra_args or [])]
|
||||
@@ -751,6 +803,8 @@ class Handler(SimpleHTTPRequestHandler):
|
||||
return self.send_json({"documents": maria_documents()})
|
||||
if path == "/api/maria/escalations":
|
||||
return self.send_json({"escalations": maria_escalations()})
|
||||
if path == "/api/maria/clients":
|
||||
return self.send_json({"clients": maria_clients()})
|
||||
if path == "/api/maria/logs":
|
||||
try:
|
||||
n = min(max(int(qs.get("lines", ["200"])[0]), 1), 2000)
|
||||
@@ -791,6 +845,10 @@ class Handler(SimpleHTTPRequestHandler):
|
||||
return self.handle_maria_document_write()
|
||||
if path == "/api/maria/documents/delete":
|
||||
return self.handle_maria_document_delete()
|
||||
if path == "/api/maria/clients":
|
||||
return self.handle_maria_client_write()
|
||||
if path == "/api/maria/clients/delete":
|
||||
return self.handle_maria_client_delete()
|
||||
if path == "/api/maria/reindex":
|
||||
return self.send_json(maria_reindex())
|
||||
if path == "/api/maria/sync":
|
||||
@@ -911,6 +969,25 @@ class Handler(SimpleHTTPRequestHandler):
|
||||
return self.send_json({"ok": False, "error": str(exc)}, 400)
|
||||
return self.send_json({"ok": ok, "name": name}, 200 if ok else 404)
|
||||
|
||||
def handle_maria_client_write(self):
|
||||
data = self.read_json()
|
||||
cid = str(data.get("id") or "")
|
||||
numere = data.get("numere") or []
|
||||
if isinstance(numere, str):
|
||||
numere = numere.splitlines()
|
||||
try:
|
||||
maria_save_client(cid, str(data.get("nume") or ""), str(data.get("profil") or ""),
|
||||
list(numere))
|
||||
except (ValueError, OSError) as exc:
|
||||
return self.send_json({"ok": False, "error": str(exc)}, 400)
|
||||
return self.send_json({"ok": True, "id": cid})
|
||||
|
||||
def handle_maria_client_delete(self):
|
||||
data = self.read_json()
|
||||
cid = str(data.get("id") or "")
|
||||
ok = maria_delete_client(cid)
|
||||
return self.send_json({"ok": ok, "id": cid}, 200 if ok else 404)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
bind = config.get("DASHBOARD_BIND") or "127.0.0.1"
|
||||
|
||||
@@ -120,6 +120,26 @@
|
||||
<div id="mEsc" class="empty">se încarcă…</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>Clienți cunoscuți <span class="count" id="mClientCount"></span></h2>
|
||||
<div class="table-scroll">
|
||||
<table class="stack-mobil">
|
||||
<thead><tr><th>Nume</th><th>Numere</th><th></th><th></th></tr></thead>
|
||||
<tbody id="mClients"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<h2 style="margin-top:var(--space-5)" id="mClientFormTitle">Adaugă client</h2>
|
||||
<input type="text" id="mClientId" placeholder="id (ex: acme)" style="width:100%;margin-bottom:var(--space-3)">
|
||||
<input type="text" id="mClientNume" placeholder="nume firmă" style="width:100%;margin-bottom:var(--space-3)">
|
||||
<textarea id="mClientProfil" placeholder="profil (context liber despre client)..." style="width:100%;min-height:80px;margin-bottom:var(--space-3)"></textarea>
|
||||
<textarea id="mClientNumere" placeholder="numere de telefon, unul pe linie" style="width:100%;min-height:80px;font-family:monospace"></textarea>
|
||||
<div class="actions" style="margin-top:var(--space-3)">
|
||||
<button class="primary" onclick="mSaveClient()">Salvează</button>
|
||||
<button class="small" onclick="mResetClientForm()">Renunță</button>
|
||||
</div>
|
||||
<div class="detail" id="mClientMsg"></div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>Documente indexate <span class="count" id="mDocCount"></span></h2>
|
||||
<div class="detail" id="mIndexDetail"></div>
|
||||
@@ -521,6 +541,61 @@ async function mDelDoc(name) {
|
||||
await mReindex();
|
||||
}
|
||||
|
||||
var mClientsCache = [];
|
||||
async function mLoadClients() {
|
||||
var res = await api('api/maria/clients');
|
||||
var clienti = res.ok ? (res.data.clients || []) : [];
|
||||
mClientsCache = clienti;
|
||||
document.getElementById('mClientCount').textContent = clienti.length ? clienti.length : '';
|
||||
document.getElementById('mClients').innerHTML = clienti.length ? clienti.map(function (c) {
|
||||
return '<tr><td data-label="Nume">' + esc(c.nume || c.id) + '</td>'
|
||||
+ '<td data-label="Numere" class="mono">' + esc((c.numere || []).join(', ')) + '</td>'
|
||||
+ '<td><button class="small" onclick="mEditClient(\'' + esc(c.id) + '\')">Editează</button></td>'
|
||||
+ '<td><button class="small danger" onclick="mDelClient(\'' + esc(c.id) + '\')">Șterge</button></td></tr>';
|
||||
}).join('') : '<tr><td colspan="4" class="empty" data-label="">niciun client</td></tr>';
|
||||
}
|
||||
|
||||
function mEditClient(id) {
|
||||
var c = mClientsCache.find(function (x) { return x.id === id; });
|
||||
if (!c) return;
|
||||
document.getElementById('mClientId').value = c.id;
|
||||
document.getElementById('mClientId').disabled = true;
|
||||
document.getElementById('mClientNume').value = c.nume || '';
|
||||
document.getElementById('mClientProfil').value = c.profil || '';
|
||||
document.getElementById('mClientNumere').value = (c.numere || []).join('\n');
|
||||
document.getElementById('mClientFormTitle').textContent = 'Editează client: ' + (c.nume || c.id);
|
||||
document.getElementById('mClientMsg').textContent = '';
|
||||
}
|
||||
|
||||
function mResetClientForm() {
|
||||
document.getElementById('mClientId').value = '';
|
||||
document.getElementById('mClientId').disabled = false;
|
||||
document.getElementById('mClientNume').value = '';
|
||||
document.getElementById('mClientProfil').value = '';
|
||||
document.getElementById('mClientNumere').value = '';
|
||||
document.getElementById('mClientFormTitle').textContent = 'Adaugă client';
|
||||
document.getElementById('mClientMsg').textContent = '';
|
||||
}
|
||||
|
||||
async function mSaveClient() {
|
||||
var id = document.getElementById('mClientId').value.trim();
|
||||
var nume = document.getElementById('mClientNume').value.trim();
|
||||
var profil = document.getElementById('mClientProfil').value;
|
||||
var numere = document.getElementById('mClientNumere').value;
|
||||
var msg = document.getElementById('mClientMsg');
|
||||
if (!id || !nume) { msg.textContent = 'completează id și nume'; return; }
|
||||
var res = await post('api/maria/clients', { id: id, nume: nume, profil: profil, numere: numere });
|
||||
if (!res.ok) { msg.textContent = 'eșuat: ' + (res.data.error || res.status); return; }
|
||||
mResetClientForm();
|
||||
await mLoadClients();
|
||||
}
|
||||
|
||||
async function mDelClient(id) {
|
||||
if (!confirm('Ștergi clientul ' + id + '?')) return;
|
||||
await post('api/maria/clients/delete', { id: id });
|
||||
await mLoadClients();
|
||||
}
|
||||
|
||||
var mLogFile = 'bridge';
|
||||
function mSetLog(which) { mLogFile = which; mRefreshLogs(); }
|
||||
async function mRefreshLogs() {
|
||||
@@ -535,7 +610,7 @@ async function mRefreshLogs() {
|
||||
|
||||
// ── pornire ───────────────────────────────────────────────────
|
||||
refresh(); refreshDoctor(); refreshLogs();
|
||||
mRefresh(); mRefreshLogs();
|
||||
mRefresh(); mRefreshLogs(); mLoadClients();
|
||||
setInterval(refresh, 5000);
|
||||
setInterval(refreshDoctor, 30000);
|
||||
setInterval(mRefresh, 4000);
|
||||
|
||||
Reference in New Issue
Block a user