File size: 6,285 Bytes
13b8931 5043fdc 13b8931 5043fdc 13b8931 5043fdc 13b8931 5043fdc 13b8931 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 | """Perfis persistentes de cliente — a Plenitude conhece quem fala.
- Guarda por cliente: nome, primeiro/último contacto, nº de trocas, último assunto.
- `linha_contexto()` diz ao prompt se o cliente é recorrente (para NÃO
repetir saudações) e qual era o assunto anterior (continuidade).
- Classificação de assunto por palavras-chave (sem dependências).
- Ficheiro `perfis.json` em `DATA_DIR`; nunca vai para git (tem PII).
"""
import json
import os
import re
import threading
import unicodedata
from datetime import datetime, timezone
_ARQUIVO = "perfis.json"
_ASSUNTOS = [
("precos", {
"preco", "precos", "custa", "custam", "custo", "valor", "valores",
"tabela", "quanto", "pagar", "pagamento", "preçario",
}),
("encomenda", {
"encomenda", "encomendar", "pedido", "pedir", "peco", "reserva",
"reservar", "marcar", "marcacao", "quero", "queria", "gostaria",
}),
("horario", {
"horario", "horarios", "hora", "horas", "aberto", "aberta",
"fechado", "fechada", "abre", "fecha", "entrega", "entregam",
"quando", "onde", "localizacao", "local", "endereco",
}),
("saudacao", {
"ola", "oi", "bom", "dia", "tarde", "noite", "bemvindo", "boas",
}),
]
# Emoções por palavras-chave (heurística leve, sem dependências).
# Ordem importa: a primeira com match ganha.
_EMOCOES = [
("zangado", {
"burro", "burra", "idiota", "estupido", "estupida", "odeio",
"merda", "porra", "caralho", "lixo", "horrivel", "pessimo",
"pessima", "reclamar", "reclamacao", "incompetente", "irritado",
"irritada", "raiva", "fdp", "cabr", "otario", "imbecil",
}),
("triste", {
"triste", "lamento", "desapontado", "desapontada", "chateado",
"chateada", "chorar", "saudade", "sozinho", "sozinha",
}),
("ansioso", {
"urgente", "rapido", "depressa", "ansioso", "ansiosa",
"preocupado", "preocupada", "aflito", "demora", "atrasado",
}),
("contente", {
"obrigado", "obrigada", "adorei", "fixe", "otimo", "otima",
"excelente", "parabens", "gostei", "amei", "top", "maravilha",
}),
("confuso", {
"nao entendo", "nao entendes", "nao percebo", "confuso",
"confusa", "como assim", "nao consigo", "perdido", "perdida",
}),
]
def detetar_emocao(texto: str) -> str:
"""Devolve zangado|triste|ansioso|contente|confuso|neutro."""
base = unicodedata.normalize("NFKD", texto or "").encode(
"ascii", "ignore").decode().lower()
base = " {} ".format(re.sub(r"[^a-z ]", " ", base))
base = re.sub(r"\s+", " ", base)
for emocao, chaves in _EMOCOES:
for chave in chaves:
if f" {chave} " in base:
return emocao
return "neutro"
def _toks(texto: str) -> set:
base = unicodedata.normalize("NFKD", texto or "").encode("ascii", "ignore").decode()
return set(re.findall(r"[a-z]+", base.lower()))
def classificar_assunto(texto: str) -> str:
toks = _toks(texto)
for assunto, chaves in _ASSUNTOS:
if toks & chaves:
return assunto
return "outro"
class ProfileStore:
def __init__(self, data_dir: str | None = None, arquivo: str = _ARQUIVO):
self.data_dir = data_dir or os.getenv("DATA_DIR", "./data")
self.arquivo = arquivo
self._lock = threading.Lock()
try:
os.makedirs(self.data_dir, exist_ok=True)
except Exception as exc:
print(f"[profiles] não consegui criar {self.data_dir}: {exc}")
def _caminho(self) -> str:
return os.path.join(self.data_dir, self.arquivo)
def _carregar(self) -> dict:
try:
with open(self._caminho(), "r", encoding="utf-8") as fh:
dados = json.load(fh)
return dados if isinstance(dados, dict) else {}
except (FileNotFoundError, json.JSONDecodeError):
return {}
except Exception as exc:
print(f"[profiles] erro a ler: {exc}")
return {}
def _guardar(self, dados: dict) -> None:
tmp = self._caminho() + ".tmp"
try:
with open(tmp, "w", encoding="utf-8") as fh:
json.dump(dados, fh, ensure_ascii=False)
os.replace(tmp, self._caminho())
except Exception as exc:
print(f"[profiles] erro a guardar: {exc}")
def obter(self, cliente: str) -> dict:
with self._lock:
return self._carregar().get(cliente or "unknown", {})
def linha_contexto(self, cliente: str, nome: str = "") -> str:
"""Devolve "" para estreantes; para recorrentes, manda não saudar."""
perfil = self.obter(cliente)
n = int(perfil.get("n_trocas", 0) or 0)
if n <= 0:
return ""
quem = perfil.get("nome") or (nome or "").strip() or "cliente sem nome registado"
assunto = perfil.get("ultimo_assunto") or "outro"
linha = (
f"Conheces este cliente ({quem}): já trocaram {n} mensagem(ns) antes; "
f"último assunto: {assunto}. NÃO cumprimentes de novo — "
f"vai direto ao assunto com continuidade."
)
emocao = perfil.get("ultima_emocao") or "neutro"
if emocao != "neutro":
linha += f" Estado emocional provável agora: {emocao}."
return linha
def registar(self, cliente: str, nome: str = "", pergunta: str = "") -> dict:
chave = cliente or "unknown"
agora = datetime.now(timezone.utc).isoformat()
with self._lock:
dados = self._carregar()
perfil = dados.get(chave, {})
nome_limpo = (nome or "").strip()
if nome_limpo and nome_limpo.lower() not in ("cliente", "visitante"):
perfil["nome"] = nome_limpo
perfil["primeiro_contacto"] = perfil.get("primeiro_contacto", agora)
perfil["ultimo_contacto"] = agora
perfil["n_trocas"] = int(perfil.get("n_trocas", 0) or 0) + 1
if pergunta:
perfil["ultimo_assunto"] = classificar_assunto(pergunta)
perfil["ultima_emocao"] = detetar_emocao(pergunta)
dados[chave] = perfil
self._guardar(dados)
return dict(perfil)
|