Cachinde
fix: Llama local ZeroGPU correto + fallback resiliente + learning v1 + infra restaurada
7e493b0 Download learning.py from akra35567/brain: direct link, hf CLI and curl.
- Browser
- Download file 7.07 kB
-
https://huggingface.co/spaces/akra35567/brain/resolve/main/learning.py
- Command line
-
hf download hf://spaces/akra35567/brain/learning.py
-
curl -L -o learning.py https://huggingface.co/spaces/akra35567/brain/resolve/main/learning.py
7.07 kB
| """Aprendizado v1 — log local JSONL + recuperação de casos parecidos (few-shot). | |
| Sem dependências novas, só stdlib. CPU-friendly, sem GPU. | |
| - Cada troca bot<->cliente é anexada a `conversas.jsonl` (ver `DATA_DIR`). | |
| - `POST /feedback` classifica uma resposta como "boa"/"ma". | |
| - Respostas "ma" nunca voltam como exemplo; "boa" tem prioridade. | |
| - Exemplos parecidos entram no prompt como few-shot (factos e tom). | |
| - O ficheiro nunca deve ir para git (contém números de clientes): ver `.gitignore`. | |
| """ | |
| import json | |
| import os | |
| import re | |
| import threading | |
| import unicodedata | |
| import uuid | |
| from datetime import datetime, timezone | |
| _STOPWORDS = { | |
| "a", "o", "e", "de", "do", "da", "dos", "das", "em", "um", "uma", | |
| "para", "com", "que", "quanto", "quantos", "qual", "quais", "como", | |
| "quando", "onde", "os", "as", "uns", "umas", "no", "na", "nos", | |
| "nas", "ao", "aos", "por", "mais", "muito", "isto", "isso", "este", | |
| "esta", "esse", "essa", "meu", "minha", "seu", "sua", "voce", "você", | |
| "tem", "têm", "e", "se", "qto", "qt", "q", "la", "lá", "ai", "aí", | |
| "entao", "então", "pois", "mas", "ou", "tambem", "também", | |
| } | |
| def _tokens(texto: str) -> set: | |
| sem_acento = unicodedata.normalize("NFKD", texto or "").encode("ascii", "ignore").decode() | |
| palavras = re.findall(r"[a-z0-9]+", sem_acento.lower()) | |
| return {p for p in palavras if len(p) > 2 and p not in _STOPWORDS} | |
| class LearningStore: | |
| """Guarda trocas em JSONL e recupera casos parecidos.""" | |
| def __init__( | |
| self, | |
| data_dir: str | None = None, | |
| arquivo: str = "conversas.jsonl", | |
| max_linhas: int = 2000, | |
| top_k: int = 2, | |
| min_score: float = 0.15, | |
| ): | |
| self.data_dir = data_dir or os.getenv("DATA_DIR", "./data") | |
| self.arquivo = arquivo | |
| self.max_linhas = int(os.getenv("LEARNING_MAX_LINHAS", str(max_linhas))) | |
| self.top_k = int(os.getenv("LEARNING_TOP_K", str(top_k))) | |
| self.min_score = float(os.getenv("LEARNING_MIN_SCORE", str(min_score))) | |
| self._lock = threading.Lock() | |
| try: | |
| os.makedirs(self.data_dir, exist_ok=True) | |
| except Exception as exc: | |
| print(f"[learning] não consegui criar {self.data_dir}: {exc}") | |
| def _caminho(self) -> str: | |
| return os.path.join(self.data_dir, self.arquivo) | |
| def _ler_tudo(self) -> list: | |
| registos = [] | |
| try: | |
| with open(self._caminho(), "r", encoding="utf-8") as fh: | |
| for linha in fh: | |
| linha = linha.strip() | |
| if not linha: | |
| continue | |
| try: | |
| registos.append(json.loads(linha)) | |
| except json.JSONDecodeError: | |
| continue | |
| except FileNotFoundError: | |
| pass | |
| except Exception as exc: | |
| print(f"[learning] erro a ler: {exc}") | |
| return registos | |
| def log_exchange(self, cliente: str, pergunta: str, resposta: str, | |
| provedor: str = "", modelo: str = "", | |
| mensagem_id: str | None = None) -> str: | |
| mensagem_id = mensagem_id or uuid.uuid4().hex[:12] | |
| registo = { | |
| "id": mensagem_id, | |
| "ts": datetime.now(timezone.utc).isoformat(), | |
| "cliente": cliente or "unknown", | |
| "pergunta": (pergunta or "")[:1000], | |
| "resposta": (resposta or "")[:1000], | |
| "provedor": provedor, | |
| "modelo": modelo, | |
| "avaliacao": None, | |
| } | |
| try: | |
| with self._lock: | |
| with open(self._caminho(), "a", encoding="utf-8") as fh: | |
| fh.write(json.dumps(registo, ensure_ascii=False) + "\n") | |
| self._podar() | |
| except Exception as exc: | |
| print(f"[learning] erro a registar: {exc}") | |
| return mensagem_id | |
| def _podar(self) -> None: | |
| try: | |
| with open(self._caminho(), "r", encoding="utf-8") as fh: | |
| linhas = fh.readlines() | |
| if len(linhas) > self.max_linhas + 500: | |
| with open(self._caminho(), "w", encoding="utf-8") as fh: | |
| fh.writelines(linhas[-self.max_linhas:]) | |
| except FileNotFoundError: | |
| pass | |
| except Exception as exc: | |
| print(f"[learning] erro a podar: {exc}") | |
| def rate(self, mensagem_id: str, nota: str, observacao: str = "") -> bool: | |
| if nota not in ("boa", "ma"): | |
| return False | |
| try: | |
| with self._lock: | |
| registos = self._ler_tudo() | |
| achou = False | |
| for reg in registos: | |
| if reg.get("id") == mensagem_id: | |
| reg["avaliacao"] = nota | |
| if observacao: | |
| reg["observacao"] = observacao[:500] | |
| achou = True | |
| break | |
| if not achou: | |
| return False | |
| with open(self._caminho(), "w", encoding="utf-8") as fh: | |
| for reg in registos: | |
| fh.write(json.dumps(reg, ensure_ascii=False) + "\n") | |
| return True | |
| except Exception as exc: | |
| print(f"[learning] erro a avaliar: {exc}") | |
| return False | |
| def find_similar(self, pergunta: str, k: int | None = None) -> list: | |
| base = _tokens(pergunta) | |
| if not base: | |
| return [] | |
| candidatos = [] | |
| try: | |
| for reg in self._ler_tudo(): | |
| if reg.get("avaliacao") == "ma": | |
| continue | |
| pontos = _tokens(reg.get("pergunta", "")) | |
| if not pontos: | |
| continue | |
| uniao = base | pontos | |
| score = len(base & pontos) / len(uniao) if uniao else 0.0 | |
| if reg.get("avaliacao") == "boa": | |
| score = min(1.0, score + 0.25) | |
| if score >= self.min_score: | |
| candidatos.append((score, reg)) | |
| except Exception as exc: | |
| print(f"[learning] erro a procurar: {exc}") | |
| return [] | |
| candidatos.sort(key=lambda item: item[0], reverse=True) | |
| return [reg for _, reg in candidatos[: (k or self.top_k)]] | |
| def format_examples(self, pergunta: str) -> str: | |
| exemplos = self.find_similar(pergunta) | |
| if not exemplos: | |
| return "" | |
| blocos = ["EXEMPLOS DE ATENDIMENTOS ANTERIORES (segue o mesmo padrão de factos e tom):"] | |
| total = len(blocos[0]) | |
| for i, reg in enumerate(exemplos, 1): | |
| perg = (reg.get("pergunta", "") or "")[:200] | |
| resp = (reg.get("resposta", "") or "")[:300] | |
| bloco = f"{i}) Cliente: {perg}\n Plenitude: {resp}" | |
| if total + len(bloco) > 900: | |
| break | |
| blocos.append(bloco) | |
| total += len(bloco) | |
| if len(blocos) < 2: | |
| return "" | |
| return "\n".join(blocos) | |
| def count(self) -> int: | |
| return len(self._ler_tudo()) | |