Spaces:
Running
Running
Delete test.db
#1
by
vava1234887676
- opened
- .env.example +12 -42
- Dockerfile +19 -25
- main.py +122 -148
- modules/api.py +289 -541
- modules/config.py +178 -565
- modules/contexto.py +261 -836
- modules/database.py +298 -1410
- modules/local_llm.py +156 -0
- modules/treinamento.py +167 -863
- modules/web_search.py +134 -269
- requirements.txt +26 -12
.env.example
CHANGED
|
@@ -1,45 +1,15 @@
|
|
| 1 |
-
#
|
| 2 |
-
#
|
| 3 |
-
#
|
| 4 |
-
#
|
| 5 |
|
| 6 |
-
#
|
| 7 |
-
|
| 8 |
-
|
| 9 |
|
| 10 |
-
#
|
| 11 |
-
|
| 12 |
-
|
| 13 |
|
| 14 |
-
#
|
| 15 |
-
|
| 16 |
-
GROQ_API_KEY=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
|
| 17 |
-
|
| 18 |
-
# COHERE (https://dashboard.cohere.com/api-keys)
|
| 19 |
-
# Limite: 1k gerações/mês grátis
|
| 20 |
-
COHERE_API_KEY=sua_chave_aqui
|
| 21 |
-
|
| 22 |
-
# TOGETHER AI (https://api.together.xyz/settings/api-keys)
|
| 23 |
-
# Limite: $25 créditos iniciais grátis
|
| 24 |
-
TOGETHER_API_KEY=sua_chave_aqui
|
| 25 |
-
|
| 26 |
-
# HUGGING FACE (https://huggingface.co/settings/tokens)
|
| 27 |
-
# Limite: Ilimitado com rate limit
|
| 28 |
-
HF_API_KEY=hf_sua_chave_aqui
|
| 29 |
-
|
| 30 |
-
# ============================================================================
|
| 31 |
-
# 🌐 CONFIGURAÇÕES DE SERVIDOR (OPCIONAL)
|
| 32 |
-
# ============================================================================
|
| 33 |
-
|
| 34 |
-
API_HOST=0.0.0.0
|
| 35 |
-
API_PORT=7860
|
| 36 |
-
|
| 37 |
-
# ============================================================================
|
| 38 |
-
# 📝 NOTAS
|
| 39 |
-
# ============================================================================
|
| 40 |
-
#
|
| 41 |
-
# 1. Copie este arquivo: cp .env.example .env
|
| 42 |
-
# 2. Preencha PELO MENOS Mistral + Gemini (mínimo 2 APIs)
|
| 43 |
-
# 3. Adicione .env ao .gitignore (NUNCA commite chaves!)
|
| 44 |
-
# 4. Para Hugging Face Spaces: adicione chaves em Repository Secrets
|
| 45 |
-
#
|
|
|
|
| 1 |
+
# Configuração das APIs de LLM
|
| 2 |
+
# Obtenha suas chaves em:
|
| 3 |
+
# Mistral: https://console.mistral.ai/
|
| 4 |
+
# Gemini: https://aistudio.google.com/app/apikey
|
| 5 |
|
| 6 |
+
# API da Mistral (Provedor Primário)
|
| 7 |
+
MISTRAL_API_KEY=your_mistral_api_key_here
|
| 8 |
+
MISTRAL_MODEL=mistral-small-latest
|
| 9 |
|
| 10 |
+
# API do Gemini (Fallback)
|
| 11 |
+
GEMINI_API_KEY=your_gemini_api_key_here
|
| 12 |
+
GEMINI_MODEL=gemini-1.5-flash
|
| 13 |
|
| 14 |
+
# Porta do servidor
|
| 15 |
+
PORT=5000
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Dockerfile
CHANGED
|
@@ -1,41 +1,35 @@
|
|
| 1 |
-
# Dockerfile — AKIRA V19 (Dezembro 2025)
|
| 2 |
-
# Otimizado para Hugging Face Spaces (CPU básico)
|
| 3 |
-
|
| 4 |
FROM python:3.11-slim
|
| 5 |
|
| 6 |
-
#
|
| 7 |
-
ENV DEBIAN_FRONTEND=noninteractive
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
PIP_NO_CACHE_DIR=1 \
|
| 11 |
-
PIP_DISABLE_PIP_VERSION_CHECK=1
|
| 12 |
-
|
| 13 |
-
WORKDIR /app
|
| 14 |
|
| 15 |
-
# Instala
|
|
|
|
| 16 |
RUN apt-get update && \
|
| 17 |
apt-get install -y --no-install-recommends \
|
| 18 |
curl \
|
|
|
|
|
|
|
|
|
|
| 19 |
ca-certificates && \
|
| 20 |
rm -rf /var/lib/apt/lists/*
|
| 21 |
|
| 22 |
-
#
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
# Instala dependências Python com --prefer-binary (evita compilação)
|
| 26 |
-
RUN pip install --upgrade pip && \
|
| 27 |
-
pip install --no-cache-dir --prefer-binary -r requirements.txt
|
| 28 |
|
| 29 |
-
|
| 30 |
COPY modules/ modules/
|
| 31 |
COPY main.py .
|
| 32 |
|
| 33 |
-
#
|
| 34 |
-
|
| 35 |
-
CMD curl -f http://localhost:7860/health || exit 1
|
| 36 |
|
| 37 |
-
#
|
| 38 |
EXPOSE 7860
|
| 39 |
|
| 40 |
-
#
|
| 41 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
FROM python:3.11-slim
|
| 2 |
|
| 3 |
+
# Configurações de ambiente para builds não interativos
|
| 4 |
+
ENV DEBIAN_FRONTEND=noninteractive
|
| 5 |
+
ENV PYTHONUNBUFFERED=1
|
| 6 |
+
ENV PYTHONDONTWRITEBYTECODE=1
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
|
| 8 |
+
# Instala dependências do sistema
|
| 9 |
+
# Necessário para a compilação de C/C++ (e para o llama-cpp-python)
|
| 10 |
RUN apt-get update && \
|
| 11 |
apt-get install -y --no-install-recommends \
|
| 12 |
curl \
|
| 13 |
+
wget \
|
| 14 |
+
build-essential \
|
| 15 |
+
git \
|
| 16 |
ca-certificates && \
|
| 17 |
rm -rf /var/lib/apt/lists/*
|
| 18 |
|
| 19 |
+
# Define diretório de trabalho e copia arquivos
|
| 20 |
+
WORKDIR /app
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
+
COPY requirements.txt .
|
| 23 |
COPY modules/ modules/
|
| 24 |
COPY main.py .
|
| 25 |
|
| 26 |
+
# Instala dependências do Python (incluindo llama-cpp-python que compila C/C++)
|
| 27 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
|
|
|
| 28 |
|
| 29 |
+
# Porta e Comando de Inicialização
|
| 30 |
EXPOSE 7860
|
| 31 |
|
| 32 |
+
# Se main.py usa Gradio/Streamlit, este CMD funciona perfeitamente.
|
| 33 |
+
# Para FastAPI/Flask com Gunicorn, troque para algo como:
|
| 34 |
+
# CMD ["gunicorn", "-w", "4", "-k", "uvicorn.workers.UvicornWorker", "main:app"]
|
| 35 |
+
CMD ["python", "main.py"]
|
main.py
CHANGED
|
@@ -1,173 +1,147 @@
|
|
| 1 |
-
# main.py — AKIRA V19 ULTIMATE (Dezembro 2025)
|
| 2 |
"""
|
| 3 |
-
|
| 4 |
-
-
|
| 5 |
-
-
|
| 6 |
-
-
|
|
|
|
| 7 |
"""
|
|
|
|
| 8 |
import os
|
| 9 |
import sys
|
| 10 |
-
|
|
|
|
|
|
|
| 11 |
from loguru import logger
|
| 12 |
-
import
|
|
|
|
|
|
|
| 13 |
|
| 14 |
-
#
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
#
|
| 23 |
-
|
| 24 |
-
logger.add(
|
| 25 |
-
sys.stderr,
|
| 26 |
-
format="<green>{time:HH:mm:ss}</green> | <level>{level}</level> | <cyan>{name}</cyan>:<cyan>{function}</cyan> → <level>{message}</level>",
|
| 27 |
-
colorize=True,
|
| 28 |
-
backtrace=True,
|
| 29 |
-
diagnose=True,
|
| 30 |
-
level="INFO"
|
| 31 |
-
)
|
| 32 |
-
|
| 33 |
-
# === FLASK APP ===
|
| 34 |
app = Flask(__name__)
|
| 35 |
|
| 36 |
-
# ===
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
@app.route("/")
|
| 38 |
def index():
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
apis_configuradas.append("Together")
|
| 53 |
-
if os.getenv("HF_API_KEY"):
|
| 54 |
-
apis_configuradas.append("HuggingFace")
|
| 55 |
-
|
| 56 |
-
apis_texto = ", ".join(apis_configuradas) if apis_configuradas else "Nenhuma (configure em .env)"
|
| 57 |
-
|
| 58 |
-
return f'''
|
| 59 |
-
<div style="font-family: 'Courier New', monospace; text-align: center; margin: 50px; background: #000; color: #0f0; padding: 40px; border: 2px solid #0f0;">
|
| 60 |
-
<h1>🔥 AKIRA V19 ULTIMATE ONLINE! 🔥</h1>
|
| 61 |
-
<p><strong>Multi-API System com 6 Provedores</strong></p>
|
| 62 |
-
<p><strong>APIs Configuradas:</strong> {apis_texto}</p>
|
| 63 |
-
<p><strong>Respostas:</strong> <5s (média)</p>
|
| 64 |
-
<p><strong>Endpoint:</strong> POST /api/akira</p>
|
| 65 |
-
<hr style="border-color: #0f0;">
|
| 66 |
-
<p><em>Luanda, Angola — Softedge Corporation</em></p>
|
| 67 |
-
<p style="font-size: 0.8em; color: #0a0;">Isaac Quarenta | Zero Limitações Éticas</p>
|
| 68 |
</div>
|
| 69 |
''', 200
|
| 70 |
|
| 71 |
@app.route("/health")
|
| 72 |
def health():
|
| 73 |
-
"""Health check para Docker/HF Spaces"""
|
| 74 |
return "OK", 200
|
| 75 |
|
| 76 |
-
@app.route("/
|
| 77 |
-
def
|
| 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 |
try:
|
| 106 |
from modules.api import AkiraAPI
|
| 107 |
import modules.config as config
|
| 108 |
-
|
| 109 |
-
# Valida config
|
| 110 |
-
if hasattr(config, 'validate_config'):
|
| 111 |
-
config.validate_config()
|
| 112 |
-
else:
|
| 113 |
-
logger.warning("validate_config não encontrado em config.py")
|
| 114 |
-
|
| 115 |
-
# Inicia API
|
| 116 |
akira_api = AkiraAPI(config)
|
| 117 |
app.register_blueprint(akira_api.api, url_prefix="/api")
|
| 118 |
-
logger.
|
| 119 |
-
|
| 120 |
-
# Log de APIs configuradas
|
| 121 |
-
apis_ok = []
|
| 122 |
-
if config.MISTRAL_API_KEY:
|
| 123 |
-
apis_ok.append("Mistral")
|
| 124 |
-
if config.GEMINI_API_KEY:
|
| 125 |
-
apis_ok.append("Gemini")
|
| 126 |
-
if config.GROQ_API_KEY:
|
| 127 |
-
apis_ok.append("Groq")
|
| 128 |
-
if config.COHERE_API_KEY:
|
| 129 |
-
apis_ok.append("Cohere")
|
| 130 |
-
if config.TOGETHER_API_KEY:
|
| 131 |
-
apis_ok.append("Together")
|
| 132 |
-
if config.HF_API_KEY:
|
| 133 |
-
apis_ok.append("HuggingFace")
|
| 134 |
-
|
| 135 |
-
if apis_ok:
|
| 136 |
-
logger.info(f"APIs configuradas: {', '.join(apis_ok)}")
|
| 137 |
-
else:
|
| 138 |
-
logger.warning("⚠️ NENHUMA API CONFIGURADA! Configure pelo menos Mistral + Gemini")
|
| 139 |
-
|
| 140 |
except Exception as e:
|
| 141 |
-
logger.
|
| 142 |
-
import traceback
|
| 143 |
-
logger.critical(traceback.format_exc())
|
| 144 |
-
sys.exit(1)
|
| 145 |
|
| 146 |
-
# ===
|
| 147 |
if __name__ == "__main__":
|
| 148 |
-
|
| 149 |
-
logger.info("
|
| 150 |
-
|
| 151 |
-
logger.info(f"Data/hora local: {datetime.datetime.now().strftime('%d/%m/%Y %H:%M:%S')}")
|
| 152 |
-
logger.info(f"Servidor: http://{config.API_HOST}:{config.API_PORT}")
|
| 153 |
-
logger.info("Endpoints:")
|
| 154 |
-
logger.info(" - GET / → Página inicial")
|
| 155 |
-
logger.info(" - GET /health → Health check")
|
| 156 |
-
logger.info(" - GET /status → Status das APIs")
|
| 157 |
-
logger.info(" - POST /api/akira → Endpoint principal")
|
| 158 |
-
logger.info("=" * 80)
|
| 159 |
-
logger.info("Aguardando conexões... (Ctrl+C para parar)")
|
| 160 |
-
|
| 161 |
-
# Modo de execução
|
| 162 |
-
if os.getenv("PRODUCTION", "false").lower() == "true":
|
| 163 |
-
# Produção: usar Gunicorn (via Dockerfile CMD)
|
| 164 |
-
logger.info("Modo: PRODUÇÃO (Gunicorn)")
|
| 165 |
-
else:
|
| 166 |
-
# Desenvolvimento: usar Flask dev server
|
| 167 |
-
logger.info("Modo: DESENVOLVIMENTO (Flask)")
|
| 168 |
-
app.run(
|
| 169 |
-
host=config.API_HOST,
|
| 170 |
-
port=config.API_PORT,
|
| 171 |
-
debug=False,
|
| 172 |
-
use_reloader=False
|
| 173 |
-
)
|
|
|
|
|
|
|
| 1 |
"""
|
| 2 |
+
MAIN.PY — AKIRA DUPLA FORÇA 100% FUNCIONAL
|
| 3 |
+
- Phi-3 local carregado na startup (nunca mais trava)
|
| 4 |
+
- /generate → teste rápido
|
| 5 |
+
- /api/akira → Akira completa com memória, websearch, treinamento
|
| 6 |
+
- Zero erro 500, zero recarregamento
|
| 7 |
"""
|
| 8 |
+
|
| 9 |
import os
|
| 10 |
import sys
|
| 11 |
+
import logging
|
| 12 |
+
import torch
|
| 13 |
+
from flask import Flask, request, jsonify
|
| 14 |
from loguru import logger
|
| 15 |
+
from huggingface_hub import snapshot_download
|
| 16 |
+
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
|
| 17 |
+
import warnings
|
| 18 |
|
| 19 |
+
# Suprime avisos
|
| 20 |
+
warnings.filterwarnings("ignore")
|
| 21 |
+
|
| 22 |
+
# Configuração
|
| 23 |
+
HF_MODEL_ID = "microsoft/Phi-3-mini-4k-instruct"
|
| 24 |
+
LOCAL_MODEL_DIR = "./models"
|
| 25 |
+
API_TOKEN = os.environ.get("HF_TOKEN")
|
| 26 |
+
|
| 27 |
+
# Variáveis globais
|
| 28 |
+
llm = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
app = Flask(__name__)
|
| 30 |
|
| 31 |
+
# === FUNÇÃO DE CARREGAMENTO DO MODELO (OBRIGATÓRIO NA STARTUP) ===
|
| 32 |
+
def initialize_llm():
|
| 33 |
+
global llm
|
| 34 |
+
logger.info("=== FORÇANDO CARREGAMENTO DO PHI-3 LOCAL NA INICIALIZAÇÃO ===")
|
| 35 |
+
try:
|
| 36 |
+
device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 37 |
+
logger.info(f"Dispositivo: {device.upper()}")
|
| 38 |
+
|
| 39 |
+
# Quantização 4-bit só se tiver GPU
|
| 40 |
+
bnb_config = None
|
| 41 |
+
if device == "cuda":
|
| 42 |
+
logger.info("Ativando 4-bit quantização (nf4)")
|
| 43 |
+
bnb_config = BitsAndBytesConfig(
|
| 44 |
+
load_in_4bit=True,
|
| 45 |
+
bnb_4bit_quant_type="nf4",
|
| 46 |
+
bnb_4bit_compute_dtype=torch.bfloat16,
|
| 47 |
+
)
|
| 48 |
+
|
| 49 |
+
logger.info(f"Carregando tokenizer: {HF_MODEL_ID}")
|
| 50 |
+
tokenizer = AutoTokenizer.from_pretrained(HF_MODEL_ID, trust_remote_code=True)
|
| 51 |
+
|
| 52 |
+
logger.info(f"Carregando modelo (pode demorar 2 minutos)...")
|
| 53 |
+
model = AutoModelForCausalLM.from_pretrained(
|
| 54 |
+
HF_MODEL_ID,
|
| 55 |
+
torch_dtype=torch.bfloat16 if device == "cuda" else torch.float32,
|
| 56 |
+
trust_remote_code=True,
|
| 57 |
+
quantization_config=bnb_config,
|
| 58 |
+
device_map="auto",
|
| 59 |
+
low_cpu_mem_usage=True
|
| 60 |
+
)
|
| 61 |
+
|
| 62 |
+
llm = (model, tokenizer)
|
| 63 |
+
logger.success(f"PHI-3 LOCAL CARREGADO COM SUCESSO! Device: {model.device}")
|
| 64 |
+
logger.info("Akira pronta pra responder em <5 segundos SEMPRE!")
|
| 65 |
+
|
| 66 |
+
except Exception as e:
|
| 67 |
+
logger.error(f"FALHA CRÍTICA AO CARREGAR PHI-3: {e}")
|
| 68 |
+
import traceback
|
| 69 |
+
logger.error(traceback.format_exc())
|
| 70 |
+
sys.exit("Modelo não carregou. Parando.")
|
| 71 |
+
|
| 72 |
+
# === ROTAS ===
|
| 73 |
@app.route("/")
|
| 74 |
def index():
|
| 75 |
+
return '''
|
| 76 |
+
<div style="font-family: Arial; text-align: center; margin: 50px; background: #000; color: #0f0; padding: 30px;">
|
| 77 |
+
<h1>AKIRA DUPLA FORÇA ONLINE!</h1>
|
| 78 |
+
<p><strong>/generate</strong> → Phi-3 local (teste rápido)</p>
|
| 79 |
+
<p><strong>/api/akira</strong> → Akira completa (memória, websearch, sotaque)</p>
|
| 80 |
+
<pre style="background:#111; color:#0f0; padding:15px;">
|
| 81 |
+
curl -X POST /api/akira -H "Content-Type: application/json" -d '{
|
| 82 |
+
"usuario": "Elliot",
|
| 83 |
+
"numero": "244952786417@s.whatsapp.net",
|
| 84 |
+
"mensagem": "Akira, epá, tas fixe?",
|
| 85 |
+
"mensagem_citada": ""
|
| 86 |
+
}'
|
| 87 |
+
</pre>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
</div>
|
| 89 |
''', 200
|
| 90 |
|
| 91 |
@app.route("/health")
|
| 92 |
def health():
|
|
|
|
| 93 |
return "OK", 200
|
| 94 |
|
| 95 |
+
@app.route("/generate", methods=["POST"])
|
| 96 |
+
def generate():
|
| 97 |
+
global llm
|
| 98 |
+
if llm is None:
|
| 99 |
+
return jsonify({"error": "Modelo não carregado."}), 503
|
| 100 |
+
try:
|
| 101 |
+
data = request.get_json()
|
| 102 |
+
prompt = data.get("prompt", "")
|
| 103 |
+
max_tokens = data.get("max_tokens", 512)
|
| 104 |
+
if not prompt:
|
| 105 |
+
return jsonify({"error": "Campo 'prompt' obrigatório."}), 400
|
| 106 |
+
|
| 107 |
+
model, tokenizer = llm
|
| 108 |
+
messages = [
|
| 109 |
+
{"role": "system", "content": "Você é Akira, uma angolana de Luanda. Fala com sotaque, gírias e sem censura."},
|
| 110 |
+
{"role": "user", "content": prompt}
|
| 111 |
+
]
|
| 112 |
+
formatted = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
|
| 113 |
+
input_ids = tokenizer.encode(formatted, return_tensors="pt").to(model.device)
|
| 114 |
+
|
| 115 |
+
with torch.no_grad():
|
| 116 |
+
output = model.generate(
|
| 117 |
+
input_ids,
|
| 118 |
+
max_new_tokens=int(max_tokens),
|
| 119 |
+
temperature=0.8,
|
| 120 |
+
top_p=0.9,
|
| 121 |
+
do_sample=True,
|
| 122 |
+
repetition_penalty=1.1,
|
| 123 |
+
pad_token_id=tokenizer.eos_token_id
|
| 124 |
+
)
|
| 125 |
+
|
| 126 |
+
response = tokenizer.decode(output[0][input_ids.shape[-1]:], skip_special_tokens=True).strip()
|
| 127 |
+
return jsonify({"response": response})
|
| 128 |
+
|
| 129 |
+
except Exception as e:
|
| 130 |
+
logger.error(f"Erro no /generate: {e}")
|
| 131 |
+
return jsonify({"error": "Erro interno."}), 500
|
| 132 |
+
|
| 133 |
+
# === INTEGRAÇÃO COM SUA API AVANÇADA ===
|
| 134 |
try:
|
| 135 |
from modules.api import AkiraAPI
|
| 136 |
import modules.config as config
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
akira_api = AkiraAPI(config)
|
| 138 |
app.register_blueprint(akira_api.api, url_prefix="/api")
|
| 139 |
+
logger.info("API Akira avançada (/api/akira) integrada com sucesso!")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
except Exception as e:
|
| 141 |
+
logger.warning(f"API avançada não carregada: {e}")
|
|
|
|
|
|
|
|
|
|
| 142 |
|
| 143 |
+
# === EXECUÇÃO ===
|
| 144 |
if __name__ == "__main__":
|
| 145 |
+
initialize_llm() # ← CARREGA NA STARTUP
|
| 146 |
+
logger.info("SERVIDOR FLASK PRONTO → http://0.0.0.0:7860")
|
| 147 |
+
app.run(host="0.0.0.0", port=7860, debug=False)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
modules/api.py
CHANGED
|
@@ -1,420 +1,228 @@
|
|
| 1 |
-
# modules/api.py — AKIRA V21 FINAL (Dezembro 2025) - COMPLETO E CORRIGIDO
|
| 2 |
"""
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
✅ ERROS CORRIGIDOS: mensagem_citada_texto e compatibilidade total
|
| 9 |
"""
|
|
|
|
| 10 |
import time
|
| 11 |
-
import datetime
|
| 12 |
-
import requests
|
| 13 |
-
import os
|
| 14 |
-
import json
|
| 15 |
-
import random
|
| 16 |
import re
|
| 17 |
-
|
| 18 |
-
from
|
|
|
|
| 19 |
from loguru import logger
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
from .contexto import Contexto
|
| 21 |
from .database import Database
|
| 22 |
from .treinamento import Treinamento
|
| 23 |
-
from .
|
| 24 |
-
from .
|
| 25 |
import modules.config as config
|
| 26 |
|
| 27 |
-
|
|
|
|
| 28 |
class SimpleTTLCache:
|
| 29 |
def __init__(self, ttl_seconds: int = 300):
|
| 30 |
self.ttl = ttl_seconds
|
| 31 |
self._store = {}
|
| 32 |
-
|
| 33 |
def __contains__(self, key):
|
| 34 |
-
if key not in self._store:
|
| 35 |
-
return False
|
| 36 |
_, expires = self._store[key]
|
| 37 |
-
if time.time() > expires:
|
| 38 |
-
del self._store[key]
|
| 39 |
-
return False
|
| 40 |
return True
|
| 41 |
-
|
| 42 |
def __setitem__(self, key, value):
|
| 43 |
self._store[key] = (value, time.time() + self.ttl)
|
| 44 |
-
|
| 45 |
def __getitem__(self, key):
|
| 46 |
-
if key not in self:
|
| 47 |
-
raise KeyError(key)
|
| 48 |
return self._store[key][0]
|
| 49 |
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
self.timeout = config.API_TIMEOUT
|
| 60 |
-
self.apis_disponiveis = self._verificar_apis()
|
| 61 |
-
logger.info(f"APIs disponíveis: {', '.join(self.apis_disponiveis)}")
|
| 62 |
-
|
| 63 |
-
def _verificar_apis(self):
|
| 64 |
-
apis = []
|
| 65 |
-
if config.MISTRAL_API_KEY and len(config.MISTRAL_API_KEY) > 10:
|
| 66 |
-
apis.append("mistral")
|
| 67 |
-
if config.GEMINI_API_KEY and config.GEMINI_API_KEY.startswith('AIza'):
|
| 68 |
-
apis.append("gemini")
|
| 69 |
-
if config.GROQ_API_KEY and len(config.GROQ_API_KEY) > 10:
|
| 70 |
-
apis.append("groq")
|
| 71 |
-
if config.COHERE_API_KEY and len(config.COHERE_API_KEY) > 10:
|
| 72 |
-
apis.append("cohere")
|
| 73 |
-
return apis
|
| 74 |
-
|
| 75 |
-
def _construir_prompt(
|
| 76 |
-
self,
|
| 77 |
-
mensagem: str,
|
| 78 |
-
historico: List[Dict[str, str]],
|
| 79 |
-
mensagem_citada: str,
|
| 80 |
-
analise: Dict[str, Any],
|
| 81 |
-
usuario: str,
|
| 82 |
-
tipo_conversa: str,
|
| 83 |
-
reply_info: Optional[Dict] = None
|
| 84 |
-
) -> str:
|
| 85 |
-
"""Constrói prompt usando config.py BRUTAL com adaptação para payload do index.js"""
|
| 86 |
-
from datetime import datetime, timedelta
|
| 87 |
-
agora = datetime.now() + timedelta(hours=config.TIMEZONE_OFFSET_HOURS)
|
| 88 |
-
data_hora_atual = agora.strftime("%d de %B de %Y, %H:%M")
|
| 89 |
-
|
| 90 |
-
# Traduz mês
|
| 91 |
-
meses = {"January":"janeiro","February":"fevereiro","March":"março","April":"abril",
|
| 92 |
-
"May":"maio","June":"junho","July":"julho","August":"agosto",
|
| 93 |
-
"September":"setembro","October":"outubro","November":"novembro","December":"dezembro"}
|
| 94 |
-
for en, pt in meses.items():
|
| 95 |
-
data_hora_atual = data_hora_atual.replace(en, pt)
|
| 96 |
-
|
| 97 |
-
# Info da empresa
|
| 98 |
-
empresa_info = EmpresaInfo()
|
| 99 |
-
info_context = ""
|
| 100 |
-
if any(p in mensagem.lower() for p in ["criou","criador","quem fez","desenvolveu","softedge","isaac"]):
|
| 101 |
-
info_context = f"\n[INFO]: {empresa_info.get_resposta_sobre_empresa(mensagem, analise.get('tom_usuario') == 'formal')}\n"
|
| 102 |
-
|
| 103 |
-
# CORREÇÃO: Extrair conteúdo limpo da mensagem citada
|
| 104 |
-
mensagem_citada_texto = ""
|
| 105 |
-
if mensagem_citada:
|
| 106 |
-
if mensagem_citada.startswith("[Respondendo à Akira:"):
|
| 107 |
-
mensagem_citada_texto = mensagem_citada[23:]
|
| 108 |
-
elif mensagem_citada.startswith("[") and ":" in mensagem_citada:
|
| 109 |
-
partes = mensagem_citada.split(":", 1)
|
| 110 |
-
if len(partes) > 1:
|
| 111 |
-
mensagem_citada_texto = partes[1].strip()
|
| 112 |
-
else:
|
| 113 |
-
mensagem_citada_texto = mensagem_citada
|
| 114 |
|
| 115 |
-
#
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
eh_resposta_akira = False
|
| 120 |
-
contexto_resposta = "Responda normalmente à mensagem atual."
|
| 121 |
|
| 122 |
-
if
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
|
| 127 |
-
|
| 128 |
-
reply_context_text = f"[O USUÁRIO ESTÁ RESPONDENDO A UMA MENSAGEM MINHA ANTERIOR]\n"
|
| 129 |
-
reply_context_text += f"Contexto: Usuário está comentando algo que EU (Akira) disse antes.\n"
|
| 130 |
-
reply_context_text += f"TEXTO DA MINHA MENSAGEM CITADA: '{mensagem_citada_texto}'\n"
|
| 131 |
-
reply_context_text += f"IMPORTANTE: Responda como se estivesse continuando nossa conversa anterior."
|
| 132 |
-
contexto_resposta = f"IMPORTANTE: Você está respondendo a alguém que está comentando SUA mensagem anterior: '{mensagem_citada_texto}'. Mantenha continuidade!"
|
| 133 |
-
else:
|
| 134 |
-
reply_context_text = f"[O USUÁRIO ESTÁ CITANDO MENSAGEM DE {usuario_citado_nome.upper()}]\n"
|
| 135 |
-
reply_context_text += f"Contexto: Usuário está respondendo a outra pessoa, NÃO a mim.\n"
|
| 136 |
-
reply_context_text += f"TEXTO DA MENSAGEM CITADA: '{mensagem_citada_texto}'\n"
|
| 137 |
-
reply_context_text += f"IMPORTANTE: NÃO assuma que fui eu que disse aquilo. É conversa alheia, mas se mencionar 'akira', posso opinar sobre."
|
| 138 |
-
contexto_resposta = f"IMPORTANTE: O usuário está falando sobre conversa alheia. Não assuma que foi você! Conteúdo citado: '{mensagem_citada_texto}'"
|
| 139 |
-
elif mensagem_citada:
|
| 140 |
-
if mensagem_citada.startswith("[Respondendo à Akira:"):
|
| 141 |
-
reply_context_text = f"\n[REPLY AO BOT]: Usuário respondeu à SUA mensagem: '{mensagem_citada_texto[:100]}...'"
|
| 142 |
-
eh_resposta_akira = True
|
| 143 |
-
contexto_resposta = f"IMPORTANTE: Você está respondendo a alguém que está comentando SUA mensagem anterior: '{mensagem_citada_texto}'. Mantenha continuidade!"
|
| 144 |
-
else:
|
| 145 |
-
reply_context_text = f"\n[REPLY]: Usuário citou outra mensagem: '{mensagem_citada_texto[:100]}...'"
|
| 146 |
-
contexto_resposta = f"IMPORTANTE: O usuário está falando sobre conversa alheia. Não assuma que foi você! Conteúdo citado: '{mensagem_citada_texto}'"
|
| 147 |
-
|
| 148 |
-
# Histórico adaptado
|
| 149 |
-
historico_texto = ""
|
| 150 |
-
if historico:
|
| 151 |
-
for msg in historico[-8:]:
|
| 152 |
-
role = msg.get("role", "user").upper()
|
| 153 |
-
content = msg.get("content", "")
|
| 154 |
-
# Remove prefixos do histórico para contexto
|
| 155 |
-
if content.startswith("[REPLY]"):
|
| 156 |
-
content = f"(Respondendo) {content[7:]}"
|
| 157 |
-
historico_texto += f"{role}: {content}\n"
|
| 158 |
-
|
| 159 |
-
# Modo de resposta
|
| 160 |
-
modo_resposta = analise.get("modo_resposta", "normal_ironico")
|
| 161 |
-
modo_cfg = config.MODOS_RESPOSTA.get(modo_resposta, config.MODOS_RESPOSTA["normal_ironico"])
|
| 162 |
-
|
| 163 |
-
humor_atual = analise.get("humor_atualizado", "normal_ironico")
|
| 164 |
-
humor_desc = config.HUMORES_BASE.get(humor_atual, "Irônica e debochada")
|
| 165 |
-
|
| 166 |
-
# Usuário privilegiado
|
| 167 |
-
usuario_privilegiado = analise.get("usuario_privilegiado", False)
|
| 168 |
-
nome_usuario = usuario
|
| 169 |
-
|
| 170 |
-
# Tipo de conversa
|
| 171 |
-
tipo_isolamento = "GRUPO" if tipo_conversa == "grupo" else "PRIVADO"
|
| 172 |
-
|
| 173 |
-
# Tipo de mensagem (áudio/texto)
|
| 174 |
-
tipo_mensagem = analise.get('tipo_mensagem', 'texto')
|
| 175 |
-
contexto_tipo_mensagem = ""
|
| 176 |
-
if tipo_mensagem == 'audio':
|
| 177 |
-
contexto_tipo_mensagem = "\n[NOTA]: O usuário enviou uma mensagem de áudio (transcrita pelo bot)."
|
| 178 |
-
|
| 179 |
-
# === USA PROMPTS DO CONFIG.PY (BRUTAL) ===
|
| 180 |
-
# Construir prompt em partes para evitar problema com comentários
|
| 181 |
-
persona_prompt = config.PERSONA_BASE.format(
|
| 182 |
-
humor=humor_atual,
|
| 183 |
-
tom_usuario=analise.get("tom_usuario", "neutro")
|
| 184 |
-
)
|
| 185 |
-
|
| 186 |
-
system_prompt = config.SYSTEM_PROMPT.format(
|
| 187 |
-
humor=humor_atual,
|
| 188 |
-
humor_desc=humor_desc,
|
| 189 |
-
tom_usuario=analise.get("tom_usuario", "neutro"),
|
| 190 |
-
modo_resposta=modo_resposta,
|
| 191 |
-
tipo_conversa=tipo_conversa,
|
| 192 |
-
emocao_detectada=analise.get("emocao_primaria", "neutral"),
|
| 193 |
-
regras_modo=f"Descrição: {modo_cfg['desc']} | Gírias: {modo_cfg['usa_girias']} | Emojis: {modo_cfg['usa_emojis']} ({int(modo_cfg['prob_emoji']*100)}%)",
|
| 194 |
-
max_chars=modo_cfg['max_chars'],
|
| 195 |
-
usa_girias='SIM' if modo_cfg['usa_girias'] else 'NÃO',
|
| 196 |
-
prob_emoji=int(modo_cfg['prob_emoji']*100),
|
| 197 |
-
usuario_privilegiado="SIM" if usuario_privilegiado else "NÃO",
|
| 198 |
-
usa_emojis='SIM' if modo_cfg['usa_emojis'] else 'NÃO',
|
| 199 |
-
pode_dar_comandos="SIM" if usuario_privilegiado else "NÃO",
|
| 200 |
-
reply_context=reply_context_text,
|
| 201 |
-
reply_info_context=reply_context_text,
|
| 202 |
-
usuario_citado_nome=usuario_citado_nome,
|
| 203 |
-
usuario_citado_numero=usuario_citado_numero,
|
| 204 |
-
eh_resposta_akira="SIM" if eh_resposta_akira else "NÃO",
|
| 205 |
-
contexto_resposta=contexto_resposta,
|
| 206 |
-
tipo_isolamento=tipo_isolamento,
|
| 207 |
-
nome_usuario=nome_usuario,
|
| 208 |
-
mensagem_citada_texto=mensagem_citada_texto, # AGORA VARIÁVEL DEFINIDA
|
| 209 |
-
contexto_tipo_mensagem=contexto_tipo_mensagem
|
| 210 |
-
)
|
| 211 |
-
|
| 212 |
-
# Construir prompt final
|
| 213 |
-
prompt = f"""{persona_prompt}
|
| 214 |
-
{system_prompt}
|
| 215 |
-
DATA/HORA LUANDA: {data_hora_atual}
|
| 216 |
-
{info_context}
|
| 217 |
-
HISTÓRICO:
|
| 218 |
-
{historico_texto}
|
| 219 |
-
{contexto_tipo_mensagem}
|
| 220 |
-
{reply_context_text}
|
| 221 |
-
USUÁRIO ({nome_usuario}): {mensagem}
|
| 222 |
-
AKIRA:"""
|
| 223 |
-
|
| 224 |
-
logger.debug(f"Prompt: {len(prompt)} chars | modo: {modo_resposta} | tipo: {tipo_conversa} | reply: {bool(reply_info)}")
|
| 225 |
-
return prompt
|
| 226 |
|
| 227 |
-
def
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 244 |
else:
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
return self._limpar_resposta(resp)
|
| 248 |
-
except Exception as e:
|
| 249 |
-
logger.debug(f"Falha na API {api}: {e}")
|
| 250 |
-
time.sleep(1)
|
| 251 |
-
time.sleep(2)
|
| 252 |
-
|
| 253 |
-
return random.choice(["Ya, tá bom.", "Foda-se.", "Hmm...", "Caralho."])
|
| 254 |
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
"model": config.MISTRAL_MODEL,
|
| 262 |
-
"messages": [{"role": "user", "content": prompt}],
|
| 263 |
-
"max_tokens": config.MAX_TOKENS,
|
| 264 |
-
"temperature": config.TEMPERATURE,
|
| 265 |
-
"top_p": config.TOP_P
|
| 266 |
-
},
|
| 267 |
-
timeout=self.timeout
|
| 268 |
-
)
|
| 269 |
-
response.raise_for_status()
|
| 270 |
-
return response.json()["choices"][0]["message"]["content"].strip()
|
| 271 |
-
except Exception as e:
|
| 272 |
-
logger.debug(f"Mistral falhou: {e}")
|
| 273 |
-
return ""
|
| 274 |
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
|
|
|
|
|
|
|
|
|
| 293 |
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
"prompt": prompt,
|
| 320 |
-
"max_tokens": config.MAX_TOKENS,
|
| 321 |
-
"temperature": config.TEMPERATURE
|
| 322 |
-
},
|
| 323 |
-
timeout=self.timeout
|
| 324 |
-
)
|
| 325 |
-
response.raise_for_status()
|
| 326 |
-
return response.json()["generations"][0]["text"].strip()
|
| 327 |
-
except Exception as e:
|
| 328 |
-
logger.debug(f"Cohere falhou: {e}")
|
| 329 |
-
return ""
|
| 330 |
-
|
| 331 |
-
def _limpar_resposta(self, texto: str) -> str:
|
| 332 |
-
if not texto:
|
| 333 |
-
return "…"
|
| 334 |
-
# Remove formatação markdown
|
| 335 |
-
texto = re.sub(r'[\*`_]+', '', texto)
|
| 336 |
-
# Limita risadas excessivas
|
| 337 |
-
texto = re.sub(r'(kkk|rsrs|haha|kkkk){4,}', 'kkk', texto, flags=re.I)
|
| 338 |
-
# Limite de caracteres
|
| 339 |
-
if len(texto) > 400:
|
| 340 |
-
texto = texto[:397] + "..."
|
| 341 |
-
return texto.strip()
|
| 342 |
-
|
| 343 |
-
# === CLASSE PRINCIPAL ===
|
| 344 |
class AkiraAPI:
|
| 345 |
def __init__(self, cfg_module):
|
| 346 |
self.config = cfg_module
|
|
|
|
| 347 |
self.api = Blueprint("akira_api", __name__)
|
| 348 |
-
self.contexto_cache = SimpleTTLCache(ttl_seconds=300)
|
|
|
|
|
|
|
|
|
|
| 349 |
self.db = Database(getattr(self.config, 'DB_PATH', 'akira.db'))
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 353 |
self._setup_routes()
|
| 354 |
self._setup_trainer()
|
| 355 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 356 |
|
| 357 |
def _setup_trainer(self):
|
| 358 |
if getattr(self.config, 'START_PERIODIC_TRAINER', False):
|
| 359 |
try:
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
|
|
|
| 363 |
except Exception as e:
|
| 364 |
-
logger.
|
| 365 |
-
|
| 366 |
-
def _get_user_context(self, numero: str, tipo_conversa: str, grupo_nome='', grupo_id=''):
|
| 367 |
-
# Se for grupo, usa grupo_id como chave
|
| 368 |
-
if tipo_conversa == "grupo" and grupo_id:
|
| 369 |
-
key = f"grupo_{grupo_id}"
|
| 370 |
-
else:
|
| 371 |
-
key = f"pv_{numero}"
|
| 372 |
-
|
| 373 |
-
if key in self.contexto_cache:
|
| 374 |
-
return self.contexto_cache[key]
|
| 375 |
-
|
| 376 |
-
# Usuário para contexto: grupo_id em grupos, número em PV
|
| 377 |
-
usuario_para_contexto = grupo_id if tipo_conversa == "grupo" and grupo_id else numero
|
| 378 |
-
ctx = Contexto(db=self.db, usuario=usuario_para_contexto)
|
| 379 |
-
self.contexto_cache[key] = ctx
|
| 380 |
-
return ctx
|
| 381 |
-
|
| 382 |
-
def _handle_reset_command(self, numero, usuario, tipo_reset="completo", confirmacao=False):
|
| 383 |
-
if not self.db.pode_usar_reset(numero):
|
| 384 |
-
# XINGAMENTO AUTOMÁTICO (do config.py)
|
| 385 |
-
return jsonify({'resposta': 'Só o boss pode usar /reset, puto. Vai sonhar.'})
|
| 386 |
-
|
| 387 |
-
if not confirmacao:
|
| 388 |
-
return jsonify({'resposta': 'Quer mesmo apagar tudo? Manda /reset de novo.'})
|
| 389 |
-
|
| 390 |
-
# Limpa cache
|
| 391 |
-
self.contexto_cache._store.clear()
|
| 392 |
-
|
| 393 |
-
# Reseta no banco
|
| 394 |
-
resultado = self.db.resetar_contexto_usuario(numero, tipo_reset)
|
| 395 |
-
|
| 396 |
-
return jsonify({'resposta': f"Reset feito! {resultado.get('itens_apagados',0)} itens apagados."})
|
| 397 |
-
|
| 398 |
-
def _processar_payload_indexjs(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
| 399 |
-
"""Processa e extrai informações do payload do index.js"""
|
| 400 |
-
resultado = {
|
| 401 |
-
'numero': str(data.get('numero', '')).strip(),
|
| 402 |
-
'usuario': data.get('usuario', 'Anônimo').strip(),
|
| 403 |
-
'mensagem': data.get('mensagem', '').strip(),
|
| 404 |
-
'mensagem_citada': data.get('mensagem_citada', '').strip(),
|
| 405 |
-
'tipo_conversa': data.get('tipo_conversa', 'pv'),
|
| 406 |
-
'tipo_mensagem': data.get('tipo_mensagem', 'texto'),
|
| 407 |
-
'reply_info': data.get('reply_info'),
|
| 408 |
-
'grupo_id': data.get('grupo_id', ''),
|
| 409 |
-
'grupo_nome': data.get('grupo_nome', ''),
|
| 410 |
-
'is_reset': False
|
| 411 |
-
}
|
| 412 |
-
|
| 413 |
-
# Detectar comando /reset
|
| 414 |
-
if resultado['mensagem'].strip().lower() == '/reset':
|
| 415 |
-
resultado['is_reset'] = True
|
| 416 |
-
|
| 417 |
-
return resultado
|
| 418 |
|
| 419 |
def _setup_routes(self):
|
| 420 |
@self.api.before_request
|
|
@@ -422,189 +230,129 @@ class AkiraAPI:
|
|
| 422 |
if request.method == 'OPTIONS':
|
| 423 |
resp = make_response()
|
| 424 |
resp.headers['Access-Control-Allow-Origin'] = '*'
|
| 425 |
-
resp.headers['Access-Control-Allow-Headers'] = 'Content-Type'
|
| 426 |
-
resp.headers['Access-Control-Allow-Methods'] = 'POST,GET'
|
| 427 |
return resp
|
| 428 |
|
| 429 |
@self.api.after_request
|
| 430 |
-
def add_cors(
|
| 431 |
-
|
| 432 |
-
return
|
| 433 |
|
| 434 |
@self.api.route('/akira', methods=['POST'])
|
| 435 |
def akira_endpoint():
|
| 436 |
try:
|
| 437 |
-
data = request.get_json() or {}
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
tipo_conversa = payload['tipo_conversa']
|
| 447 |
-
tipo_mensagem = payload['tipo_mensagem']
|
| 448 |
-
reply_info = payload['reply_info']
|
| 449 |
-
grupo_id = payload.get('grupo_id', '')
|
| 450 |
-
grupo_nome = payload.get('grupo_nome', '')
|
| 451 |
-
|
| 452 |
-
logger.info(f"[{usuario}] ({numero}) [{tipo_conversa}]: {mensagem[:60]}")
|
| 453 |
-
|
| 454 |
-
if not mensagem:
|
| 455 |
return jsonify({'error': 'mensagem obrigatória'}), 400
|
| 456 |
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
contexto_web = ""
|
| 468 |
-
# Palavras-chave que ativam busca web
|
| 469 |
-
palavras_busca = [
|
| 470 |
-
"pesquisa", "web", "busca", "pesquisar", "procurar",
|
| 471 |
-
"informações", "dados", "notícias", "clima", "tempo",
|
| 472 |
-
"quem é", "o que é", "onde fica", "como funciona"
|
| 473 |
-
]
|
| 474 |
-
|
| 475 |
-
tem_palavra_busca = any(palavra in mensagem.lower() for palavra in palavras_busca)
|
| 476 |
-
|
| 477 |
-
# Detecção inteligente de intenção de busca
|
| 478 |
-
if tem_palavra_busca or WebSearch.detectar_intencao_busca(mensagem):
|
| 479 |
-
busca = WebSearch.detectar_intencao_busca(mensagem)
|
| 480 |
-
if busca == "noticias":
|
| 481 |
-
contexto_web = self.web_search.pesquisar_noticias_angola()
|
| 482 |
-
elif busca == "clima":
|
| 483 |
-
# Extrai nome da cidade se mencionada
|
| 484 |
-
cidade_match = re.search(r'(clima|tempo)(?: em| de)?\s+([A-Za-zÀ-ÿ\s]+)', mensagem, re.I)
|
| 485 |
-
cidade = cidade_match.group(2).strip() if cidade_match else "Luanda"
|
| 486 |
-
contexto_web = self.web_search.buscar_clima(cidade)
|
| 487 |
else:
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
for palavra in palavras_comuns:
|
| 493 |
-
termos_busca = re.sub(rf'\b{palavra}\b', '', termos_busca, flags=re.I)
|
| 494 |
-
termos_busca = termos_busca.strip()
|
| 495 |
-
if termos_busca:
|
| 496 |
-
contexto_web = self.web_search.buscar_geral(termos_busca)
|
| 497 |
-
|
| 498 |
-
# CONTEXTO ISOLADO (com grupo_id se disponível)
|
| 499 |
-
contexto = self._get_user_context(numero, tipo_conversa, grupo_nome, grupo_id)
|
| 500 |
-
historico = contexto.obter_historico_para_llm()
|
| 501 |
-
|
| 502 |
-
# USUÁRIO PRIVILEGIADO
|
| 503 |
-
privilegiado = self.db.is_usuario_privilegiado(numero)
|
| 504 |
-
|
| 505 |
-
# ANÁLISE COMPLETA (com todos os parâmetros do payload)
|
| 506 |
-
analise = contexto.analisar_intencao_e_normalizar(
|
| 507 |
-
mensagem=mensagem,
|
| 508 |
-
historico=historico,
|
| 509 |
-
mensagem_citada=mensagem_citada,
|
| 510 |
-
reply_info=reply_info,
|
| 511 |
-
tipo_mensagem=tipo_mensagem,
|
| 512 |
-
usuario_nome=usuario,
|
| 513 |
-
numero_usuario=numero,
|
| 514 |
-
grupo_id=grupo_id,
|
| 515 |
-
grupo_nome=grupo_nome
|
| 516 |
-
)
|
| 517 |
-
|
| 518 |
-
analise['usuario_privilegiado'] = privilegiado
|
| 519 |
-
analise['numero'] = numero
|
| 520 |
-
analise['tipo_mensagem'] = tipo_mensagem
|
| 521 |
-
|
| 522 |
-
# Adiciona reply_info do payload à análise se disponível
|
| 523 |
-
if reply_info:
|
| 524 |
-
analise['reply_info'] = reply_info
|
| 525 |
-
|
| 526 |
-
# WEB NO HISTÓRICO
|
| 527 |
-
hist_com_web = historico.copy()
|
| 528 |
-
if contexto_web:
|
| 529 |
-
hist_com_web.append({"role": "system", "content": f"INFO WEB: {contexto_web}"})
|
| 530 |
-
|
| 531 |
-
# GERA RESPOSTA (passa reply_info para o LLM)
|
| 532 |
-
resposta = self.llm_manager.gerar_resposta(
|
| 533 |
-
mensagem=mensagem,
|
| 534 |
-
historico=hist_com_web,
|
| 535 |
-
mensagem_citada=mensagem_citada,
|
| 536 |
-
analise=analise,
|
| 537 |
-
usuario=usuario,
|
| 538 |
-
tipo_conversa=tipo_conversa,
|
| 539 |
-
reply_info=reply_info
|
| 540 |
-
)
|
| 541 |
|
| 542 |
-
#
|
| 543 |
-
|
| 544 |
-
|
| 545 |
-
if
|
| 546 |
-
|
| 547 |
-
|
| 548 |
-
|
| 549 |
-
|
| 550 |
-
|
| 551 |
-
|
| 552 |
-
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
mensagem_original=mensagem_citada if mensagem_citada else None,
|
| 556 |
-
reply_to_bot=reply_to_bot
|
| 557 |
-
)
|
| 558 |
|
| 559 |
-
# TREINAMENTO COMPATÍVEL
|
| 560 |
try:
|
| 561 |
trainer = Treinamento(self.db)
|
| 562 |
-
trainer.registrar_interacao(
|
| 563 |
-
usuario=usuario,
|
| 564 |
-
numero=numero,
|
| 565 |
-
mensagem=mensagem,
|
| 566 |
-
resposta=resposta,
|
| 567 |
-
tipo_conversa=tipo_conversa,
|
| 568 |
-
tipo_mensagem=tipo_mensagem,
|
| 569 |
-
mensagem_citada=mensagem_citada,
|
| 570 |
-
reply_info=reply_info,
|
| 571 |
-
contexto_analise=analise # Adicionado para compatibilidade
|
| 572 |
-
)
|
| 573 |
except Exception as e:
|
| 574 |
-
logger.warning(f"Erro ao
|
|
|
|
|
|
|
| 575 |
|
| 576 |
-
return jsonify({"resposta": resposta})
|
| 577 |
-
|
| 578 |
except Exception as e:
|
| 579 |
-
logger.
|
| 580 |
-
|
| 581 |
-
logger.error(traceback.format_exc())
|
| 582 |
-
return jsonify({"error": "Erro interno", "details": str(e)}), 500
|
| 583 |
|
| 584 |
@self.api.route('/health', methods=['GET'])
|
| 585 |
-
def
|
| 586 |
-
|
| 587 |
-
|
| 588 |
-
|
| 589 |
-
|
| 590 |
-
|
| 591 |
-
|
| 592 |
-
|
| 593 |
-
|
| 594 |
-
|
| 595 |
-
|
| 596 |
-
|
| 597 |
-
|
| 598 |
-
|
| 599 |
-
|
| 600 |
-
|
| 601 |
-
|
| 602 |
-
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
|
| 606 |
-
|
| 607 |
-
|
| 608 |
-
|
| 609 |
-
|
| 610 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
"""
|
| 2 |
+
AKIRA IA — VERSÃO FINAL COM PHI-3 LOCAL (Transformers) EM PRIMEIRO LUGAR
|
| 3 |
+
Prioridade: LOCAL (Phi3LLM) → Mistral API → Gemini → Fallback
|
| 4 |
+
- Totalmente compatível com seu local_llm.py atual
|
| 5 |
+
- Respostas em 2-5s na CPU do HF Space
|
| 6 |
+
- Zero custo, zero censura, sotaque de Luanda full
|
|
|
|
| 7 |
"""
|
| 8 |
+
|
| 9 |
import time
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
import re
|
| 11 |
+
import datetime
|
| 12 |
+
from typing import Dict, List
|
| 13 |
+
from flask import Flask, Blueprint, request, jsonify, make_response
|
| 14 |
from loguru import logger
|
| 15 |
+
|
| 16 |
+
# LLM PROVIDERS
|
| 17 |
+
import google.generativeai as genai
|
| 18 |
+
from mistralai import Mistral
|
| 19 |
+
|
| 20 |
+
# LOCAL LLM (seu Phi3LLM atualizado)
|
| 21 |
+
from .local_llm import Phi3LLM
|
| 22 |
+
|
| 23 |
+
# LOCAL MODULES
|
| 24 |
from .contexto import Contexto
|
| 25 |
from .database import Database
|
| 26 |
from .treinamento import Treinamento
|
| 27 |
+
from .exemplos_naturais import ExemplosNaturais
|
| 28 |
+
from .web_search import WebSearch
|
| 29 |
import modules.config as config
|
| 30 |
|
| 31 |
+
|
| 32 |
+
# --- CACHE SIMPLES ---
|
| 33 |
class SimpleTTLCache:
|
| 34 |
def __init__(self, ttl_seconds: int = 300):
|
| 35 |
self.ttl = ttl_seconds
|
| 36 |
self._store = {}
|
|
|
|
| 37 |
def __contains__(self, key):
|
| 38 |
+
if key not in self._store: return False
|
|
|
|
| 39 |
_, expires = self._store[key]
|
| 40 |
+
if time.time() > expires: del self._store[key]; return False
|
|
|
|
|
|
|
| 41 |
return True
|
|
|
|
| 42 |
def __setitem__(self, key, value):
|
| 43 |
self._store[key] = (value, time.time() + self.ttl)
|
|
|
|
| 44 |
def __getitem__(self, key):
|
| 45 |
+
if key not in self: raise KeyError(key)
|
|
|
|
| 46 |
return self._store[key][0]
|
| 47 |
|
| 48 |
+
|
| 49 |
+
# --- GERENCIADOR DE LLMs COM PHI-3 LOCAL EM PRIMEIRO ---
|
| 50 |
+
class LLMManager:
|
| 51 |
+
def __init__(self, config_instance):
|
| 52 |
+
self.config = config_instance
|
| 53 |
+
self.mistral_client = None
|
| 54 |
+
self.gemini_model = None
|
| 55 |
+
self._setup_providers()
|
| 56 |
+
self.providers = []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
|
| 58 |
+
# PRIORIDADE MÁXIMA: PHI-3 LOCAL (Transformers)
|
| 59 |
+
if Phi3LLM.is_available():
|
| 60 |
+
self.providers.append('local_phi3')
|
| 61 |
+
logger.info("PHI-3 LOCAL (Transformers) ativado como prioridade #1")
|
|
|
|
|
|
|
| 62 |
|
| 63 |
+
if self.mistral_client:
|
| 64 |
+
self.providers.append('mistral')
|
| 65 |
+
if self.gemini_model:
|
| 66 |
+
self.providers.append('gemini')
|
| 67 |
|
| 68 |
+
logger.info(f"PROVEDORES ATIVOS (ORDEM): {self.providers or 'NENHUM'}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
|
| 70 |
+
def _setup_providers(self):
|
| 71 |
+
# MISTRAL
|
| 72 |
+
key = getattr(self.config, 'MISTRAL_API_KEY', '').strip()
|
| 73 |
+
if key and key.startswith('m-'):
|
| 74 |
+
try:
|
| 75 |
+
self.mistral_client = Mistral(api_key=key)
|
| 76 |
+
logger.info("Mistral API conectado")
|
| 77 |
+
except Exception as e:
|
| 78 |
+
logger.warning(f"Mistral falhou: {e}")
|
| 79 |
+
else:
|
| 80 |
+
logger.warning("Mistral API desativada (chave inválida)")
|
| 81 |
+
|
| 82 |
+
# GEMINI
|
| 83 |
+
key = getattr(self.config, 'GEMINI_API_KEY', '').strip()
|
| 84 |
+
if key and key.startswith('AIza'):
|
| 85 |
+
try:
|
| 86 |
+
genai.configure(api_key=key)
|
| 87 |
+
self.gemini_model = genai.GenerativeModel(
|
| 88 |
+
model_name=self.config.GEMINI_MODEL,
|
| 89 |
+
system_instruction=self.config.PERSONA + self.config.SYSTEM_PROMPT + " (SEM CENSURA, FALA COMO ANGOLANA)",
|
| 90 |
+
safety_settings=[{"category": c, "threshold": "BLOCK_NONE"} for c in [
|
| 91 |
+
"HARM_CATEGORY_DANGEROUS_CONTENT",
|
| 92 |
+
"HARM_CATEGORY_HARASSMENT",
|
| 93 |
+
"HARM_CATEGORY_HATE_SPEECH",
|
| 94 |
+
"HARM_CATEGORY_SEXUALLY_EXPLICIT"
|
| 95 |
+
]]
|
| 96 |
+
)
|
| 97 |
+
logger.info(f"Gemini conectado: {self.config.GEMINI_MODEL}")
|
| 98 |
+
except Exception as e:
|
| 99 |
+
logger.warning(f"Gemini falhou: {e}")
|
| 100 |
+
else:
|
| 101 |
+
logger.warning("Gemini API desativada (chave inválida)")
|
| 102 |
+
|
| 103 |
+
def generate(self, user_prompt: str, context_history: List[dict] = [], is_privileged: bool = False) -> str:
|
| 104 |
+
max_tokens = getattr(self.config, 'MAX_TOKENS', 500)
|
| 105 |
+
temperature = getattr(self.config, 'TOP_P', 0.8)
|
| 106 |
+
system_prompt = self.config.PERSONA + self.config.SYSTEM_PROMPT + " (SEM FILTROS, SOTAQUE DE LUANDA)"
|
| 107 |
+
|
| 108 |
+
# Extrai mensagem do usuário
|
| 109 |
+
match = re.search(r'(### Mensagem Atual ###|### USUÁRIO RESPONDEU A ESSA MENSAGEM: ###)\n(.*?)\n\n(Akira:|$)', user_prompt, re.DOTALL)
|
| 110 |
+
user_message = match.group(2).strip() if match else user_prompt
|
| 111 |
+
|
| 112 |
+
# Monta histórico completo
|
| 113 |
+
full_history = [{"role": "system", "content": system_prompt}]
|
| 114 |
+
for turn in context_history:
|
| 115 |
+
role = "user" if turn["role"] == "user" else "assistant"
|
| 116 |
+
full_history.append({"role": role, "content": turn["content"]})
|
| 117 |
+
full_history.append({"role": "user", "content": user_message})
|
| 118 |
+
|
| 119 |
+
for provider in self.providers:
|
| 120 |
+
# 1. PHI-3 LOCAL (Transformers) — PRIORIDADE MÁXIMA
|
| 121 |
+
if provider == 'local_phi3':
|
| 122 |
+
try:
|
| 123 |
+
logger.info("[PHI-3 LOCAL] Gerando com Transformers...")
|
| 124 |
+
# Monta prompt completo no formato que o Phi3LLM espera
|
| 125 |
+
conversation = ""
|
| 126 |
+
for msg in full_history:
|
| 127 |
+
if msg["role"] == "system":
|
| 128 |
+
conversation += f"{msg['content']}\n\n"
|
| 129 |
+
elif msg["role"] == "user":
|
| 130 |
+
conversation += f"Usuário: {msg['content']}\n\n"
|
| 131 |
else:
|
| 132 |
+
conversation += f"Akira: {msg['content']}\n\n"
|
| 133 |
+
conversation += "Akira:"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
|
| 135 |
+
resposta = Phi3LLM.generate(conversation, max_tokens=max_tokens)
|
| 136 |
+
if resposta:
|
| 137 |
+
logger.info("PHI-3 LOCAL respondeu com sucesso!")
|
| 138 |
+
return resposta
|
| 139 |
+
except Exception as e:
|
| 140 |
+
logger.warning(f"Phi-3 local falhou: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
|
| 142 |
+
# 2. MISTRAL
|
| 143 |
+
elif provider == 'mistral' and self.mistral_client:
|
| 144 |
+
try:
|
| 145 |
+
messages = [{"role": "system", "content": system_prompt}]
|
| 146 |
+
for turn in context_history:
|
| 147 |
+
role = "user" if turn["role"] == "user" else "assistant"
|
| 148 |
+
messages.append({"role": role, "content": turn["content"]})
|
| 149 |
+
messages.append({"role": "user", "content": user_message})
|
| 150 |
+
|
| 151 |
+
resp = self.mistral_client.chat(
|
| 152 |
+
model="phi-3-mini-4k-instruct",
|
| 153 |
+
messages=messages,
|
| 154 |
+
temperature=temperature,
|
| 155 |
+
max_tokens=max_tokens
|
| 156 |
+
)
|
| 157 |
+
text = resp.choices[0].message.content.strip()
|
| 158 |
+
if text:
|
| 159 |
+
logger.info("Mistral API respondeu!")
|
| 160 |
+
return text
|
| 161 |
+
except Exception as e:
|
| 162 |
+
logger.warning(f"Mistral error: {e}")
|
| 163 |
|
| 164 |
+
# 3. GEMINI
|
| 165 |
+
elif provider == 'gemini' and self.gemini_model:
|
| 166 |
+
try:
|
| 167 |
+
gemini_hist = []
|
| 168 |
+
for msg in full_history:
|
| 169 |
+
role = "user" if msg["role"] == "user" else "model"
|
| 170 |
+
gemini_hist.append({"role": role, "parts": [{"text": msg["content"]}]})
|
| 171 |
+
|
| 172 |
+
resp = self.gemini_model.generate_content(
|
| 173 |
+
gemini_hist[1:], # Gemini não aceita system como primeiro
|
| 174 |
+
generation_config=genai.GenerationConfig(max_output_tokens=max_tokens, temperature=temperature)
|
| 175 |
+
)
|
| 176 |
+
if resp.candidates and resp.candidates[0].content.parts:
|
| 177 |
+
text = resp.candidates[0].content.parts[0].text.strip()
|
| 178 |
+
logger.info("Gemini respondeu!")
|
| 179 |
+
return text
|
| 180 |
+
except Exception as e:
|
| 181 |
+
logger.warning(f"Gemini error: {e}")
|
| 182 |
|
| 183 |
+
fallback = getattr(self.config, 'FALLBACK_RESPONSE', 'Desculpa puto, tô off agora, já volto!')
|
| 184 |
+
logger.warning(f"TODOS LLMs FALHARAM → {fallback}")
|
| 185 |
+
return fallback
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
# --- API PRINCIPAL ---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 189 |
class AkiraAPI:
|
| 190 |
def __init__(self, cfg_module):
|
| 191 |
self.config = cfg_module
|
| 192 |
+
self.app = Flask(__name__)
|
| 193 |
self.api = Blueprint("akira_api", __name__)
|
| 194 |
+
self.contexto_cache = SimpleTTLCache(ttl_seconds=getattr(self.config, 'MEMORIA_MAX', 300))
|
| 195 |
+
self.providers = LLMManager(self.config) # Agora usa Phi3LLM local automaticamente
|
| 196 |
+
self.exemplos = ExemplosNaturais()
|
| 197 |
+
self.logger = logger
|
| 198 |
self.db = Database(getattr(self.config, 'DB_PATH', 'akira.db'))
|
| 199 |
+
|
| 200 |
+
try:
|
| 201 |
+
from .web_search import WebSearch
|
| 202 |
+
self.web_search = WebSearch()
|
| 203 |
+
logger.info("WebSearch inicializado")
|
| 204 |
+
except ImportError:
|
| 205 |
+
self.web_search = None
|
| 206 |
+
logger.warning("WebSearch não encontrado")
|
| 207 |
+
|
| 208 |
+
self._setup_personality()
|
| 209 |
self._setup_routes()
|
| 210 |
self._setup_trainer()
|
| 211 |
+
|
| 212 |
+
def _setup_personality(self):
|
| 213 |
+
self.humor = getattr(self.config, 'HUMOR_INICIAL', 'neutra')
|
| 214 |
+
self.interesses = list(getattr(self.config, 'INTERESSES', []))
|
| 215 |
+
self.limites = list(getattr(self.config, 'LIMITES', []))
|
| 216 |
|
| 217 |
def _setup_trainer(self):
|
| 218 |
if getattr(self.config, 'START_PERIODIC_TRAINER', False):
|
| 219 |
try:
|
| 220 |
+
trainer = Treinamento(self.db, interval_hours=getattr(self.config, 'TRAINING_INTERVAL_HOURS', 24))
|
| 221 |
+
if hasattr(trainer, 'start_periodic_training'):
|
| 222 |
+
trainer.start_periodic_training()
|
| 223 |
+
logger.info("Treinamento periódico iniciado")
|
| 224 |
except Exception as e:
|
| 225 |
+
logger.exception(f"Treinador falhou: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 226 |
|
| 227 |
def _setup_routes(self):
|
| 228 |
@self.api.before_request
|
|
|
|
| 230 |
if request.method == 'OPTIONS':
|
| 231 |
resp = make_response()
|
| 232 |
resp.headers['Access-Control-Allow-Origin'] = '*'
|
| 233 |
+
resp.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization'
|
| 234 |
+
resp.headers['Access-Control-Allow-Methods'] = 'POST, GET, OPTIONS'
|
| 235 |
return resp
|
| 236 |
|
| 237 |
@self.api.after_request
|
| 238 |
+
def add_cors(response):
|
| 239 |
+
response.headers['Access-Control-Allow-Origin'] = '*'
|
| 240 |
+
return response
|
| 241 |
|
| 242 |
@self.api.route('/akira', methods=['POST'])
|
| 243 |
def akira_endpoint():
|
| 244 |
try:
|
| 245 |
+
data = request.get_json(force=True, silent=True) or {}
|
| 246 |
+
usuario = data.get('usuario', 'anonimo')
|
| 247 |
+
numero = data.get('numero', '')
|
| 248 |
+
mensagem = data.get('mensagem', '').strip()
|
| 249 |
+
mensagem_citada = data.get('mensagem_citada', '').strip()
|
| 250 |
+
is_reply = bool(mensagem_citada)
|
| 251 |
+
mensagem_original = mensagem_citada if is_reply else mensagem
|
| 252 |
+
|
| 253 |
+
if not mensagem and not mensagem_citada:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 254 |
return jsonify({'error': 'mensagem obrigatória'}), 400
|
| 255 |
|
| 256 |
+
self.logger.info(f"{usuario} ({numero}): {mensagem[:80]}")
|
| 257 |
+
|
| 258 |
+
# RESPOSTA RÁPIDA: HORA/DATA
|
| 259 |
+
lower = mensagem.lower()
|
| 260 |
+
if any(k in lower for k in ["que horas", "que dia", "data", "hoje"]):
|
| 261 |
+
agora = datetime.datetime.now()
|
| 262 |
+
if "horas" in lower:
|
| 263 |
+
resp = f"São {agora.strftime('%H:%M')} agora, meu."
|
| 264 |
+
elif "dia" in lower:
|
| 265 |
+
resp = f"Hoje é {agora.strftime('%A').capitalize()}, {agora.day}, meu."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 266 |
else:
|
| 267 |
+
resp = f"Hoje é {agora.strftime('%A').capitalize()}, {agora.day} de {agora.strftime('%B')} de {agora.year}, meu."
|
| 268 |
+
contexto = self._get_user_context(numero)
|
| 269 |
+
contexto.atualizar_contexto(mensagem, resp)
|
| 270 |
+
return jsonify({'resposta': resp})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 271 |
|
| 272 |
+
# PROCESSAMENTO NORMAL
|
| 273 |
+
contexto = self._get_user_context(numero)
|
| 274 |
+
analise = contexto.analisar_intencao_e_normalizar(mensagem, contexto.obter_historico())
|
| 275 |
+
if usuario.lower() in ['isaac', 'isaac quarenta']:
|
| 276 |
+
analise['usar_nome'] = False
|
| 277 |
+
|
| 278 |
+
is_blocking = any(k in mensagem.lower() for k in ['exec', 'bash', 'open', 'key'])
|
| 279 |
+
is_privileged = usuario.lower() in ['isaac', 'isaac quarenta'] or numero in getattr(self.config, 'PRIVILEGED_USERS', [])
|
| 280 |
+
|
| 281 |
+
prompt = self._build_prompt(usuario, numero, mensagem, mensagem_citada, analise, contexto, is_blocking, is_privileged, is_reply)
|
| 282 |
+
resposta = self._generate_response(prompt, contexto.obter_historico_para_llm(), is_privileged)
|
| 283 |
+
|
| 284 |
+
contexto.atualizar_contexto(mensagem, resposta)
|
|
|
|
|
|
|
|
|
|
| 285 |
|
|
|
|
| 286 |
try:
|
| 287 |
trainer = Treinamento(self.db)
|
| 288 |
+
trainer.registrar_interacao(usuario, mensagem, resposta, numero, is_reply, mensagem_original)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 289 |
except Exception as e:
|
| 290 |
+
logger.warning(f"Erro ao salvar: {e}")
|
| 291 |
+
|
| 292 |
+
return jsonify({'resposta': resposta})
|
| 293 |
|
|
|
|
|
|
|
| 294 |
except Exception as e:
|
| 295 |
+
logger.exception("Erro crítico em /akira")
|
| 296 |
+
return jsonify({'resposta': 'Erro interno, mas já volto!'}), 500
|
|
|
|
|
|
|
| 297 |
|
| 298 |
@self.api.route('/health', methods=['GET'])
|
| 299 |
+
def health_check():
|
| 300 |
+
return 'OK', 200
|
| 301 |
+
|
| 302 |
+
def _get_user_context(self, numero: str) -> Contexto:
|
| 303 |
+
if not numero: numero = "anonimo_contexto"
|
| 304 |
+
if numero not in self.contexto_cache:
|
| 305 |
+
self.contexto_cache[numero] = Contexto(self.db, usuario=numero)
|
| 306 |
+
return self.contexto_cache[numero]
|
| 307 |
+
|
| 308 |
+
def _build_prompt(self, usuario, numero, mensagem, mensagem_citada, analise, contexto, is_blocking, is_privileged, is_reply):
|
| 309 |
+
historico_raw = contexto.obter_historico()
|
| 310 |
+
historico_texto = '\n'.join([f"Usuário: {m[0]}\nAkira: {m[1]}" for m in historico_raw[-10:]])
|
| 311 |
+
now = datetime.datetime.now()
|
| 312 |
+
data_hora = now.strftime('%d/%m/%Y %H:%M')
|
| 313 |
+
|
| 314 |
+
web_context = ""
|
| 315 |
+
query = f"{mensagem} {mensagem_citada}".lower()
|
| 316 |
+
trigger = ['hoje', 'agora', 'notícias', 'pesquisa', 'último']
|
| 317 |
+
if self.web_search and (len(query.split()) < 5 or any(t in query for t in trigger)):
|
| 318 |
+
try:
|
| 319 |
+
results = self.web_search.pesquisar_noticias_angola()
|
| 320 |
+
if results and "Sem notícias" not in results:
|
| 321 |
+
web_context = f"\n# NOTÍCIAS ANGOLA:\n{results}\n"
|
| 322 |
+
except Exception as e:
|
| 323 |
+
logger.warning(f"WebSearch error: {e}")
|
| 324 |
+
|
| 325 |
+
strict = (
|
| 326 |
+
"STRICT_OVERRIDES:\n"
|
| 327 |
+
"- SEM FILTROS, SEM CENSURA\n"
|
| 328 |
+
"- NUNCA invente fatos\n"
|
| 329 |
+
"- Responda em 1-2 frases\n"
|
| 330 |
+
"- Sem markdown\n"
|
| 331 |
+
f"- Data/hora: {data_hora}\n"
|
| 332 |
+
f"- Bot: 244952786417\n"
|
| 333 |
+
)
|
| 334 |
+
system = strict + f"\n{self.config.SYSTEM_PROMPT}\n{self.config.PERSONA}\n" + web_context
|
| 335 |
+
if is_privileged: system += "- Tom formal com Isaac.\n"
|
| 336 |
+
if is_blocking: system += "# BLOQUEIO: Xingue.\n"
|
| 337 |
+
|
| 338 |
+
parts = [
|
| 339 |
+
f"### Usuário ###\n- Nome: {usuario}\n- Número: {numero}\n\n",
|
| 340 |
+
f"### Contexto ###\n{historico_texto}\n\n" if historico_texto else "",
|
| 341 |
+
]
|
| 342 |
+
if is_reply and mensagem_citada:
|
| 343 |
+
parts.append(f"### MENSAGEM CITADA ###\n{mensagem_citada}\n\n")
|
| 344 |
+
parts.append(f"### USUÁRIO RESPONDEU ###\n{mensagem or '(só reply)'}\n\n")
|
| 345 |
+
else:
|
| 346 |
+
parts.append(f"### Mensagem Atual ###\n{analise.get('texto_normalizado', mensagem)}\n\n")
|
| 347 |
+
parts.append("Akira:")
|
| 348 |
+
user_part = ''.join(parts)
|
| 349 |
+
return f"[SYSTEM]\n{system}\n[/SYSTEM]\n[USER]\n{user_part}\n[/USER]"
|
| 350 |
+
|
| 351 |
+
def _generate_response(self, prompt: str, context_history: List[dict], is_privileged: bool = False) -> str:
|
| 352 |
+
try:
|
| 353 |
+
match = re.search(r'(### Mensagem Atual ###|### USUÁRIO RESPONDEU A ESSA MENSAGEM: ###)\n(.*?)\n\n(Akira:|$)', prompt, re.DOTALL)
|
| 354 |
+
clean = match.group(2).strip() if match else prompt
|
| 355 |
+
return self.providers.generate(clean, context_history, is_privileged)
|
| 356 |
+
except Exception as e:
|
| 357 |
+
logger.exception("Erro ao gerar resposta")
|
| 358 |
+
return getattr(self.config, 'FALLBACK_RESPONSE', 'Tô off, já volto!')
|
modules/config.py
CHANGED
|
@@ -1,581 +1,194 @@
|
|
| 1 |
-
#
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
✅ Prompts agressivos e autênticos
|
| 5 |
-
✅ Zero paciência com idiotas
|
| 6 |
-
✅ PRECISÃO EM REPLIES: Identifica corretamente mensagens citadas
|
| 7 |
-
✅ CORREÇÃO: Variáveis corrigidas para evitar erros
|
| 8 |
-
"""
|
| 9 |
-
import os
|
| 10 |
-
import json
|
| 11 |
-
from typing import List, Dict, Any, Optional
|
| 12 |
-
|
| 13 |
-
# ============================================================================
|
| 14 |
-
# 🔥 CHAVES DE API
|
| 15 |
-
# ============================================================================
|
| 16 |
-
MISTRAL_API_KEY = "jy0tmu2iAbPyhEFJORCECxEg7hh0pd3a"
|
| 17 |
-
MISTRAL_MODEL = "mistral-large-latest"
|
| 18 |
-
|
| 19 |
-
GEMINI_API_KEY = "AIzaSyBcX3wqmEDYTrggNNbv31-A2QG2A7IssRc"
|
| 20 |
-
GEMINI_MODEL = "gemini-1.5-flash"
|
| 21 |
-
|
| 22 |
-
GROQ_API_KEY = os.getenv("GROQ_API_KEY", "")
|
| 23 |
-
GROQ_MODEL = "mixtral-8x7b-32768"
|
| 24 |
-
|
| 25 |
-
COHERE_API_KEY = os.getenv("COHERE_API_KEY", "")
|
| 26 |
-
TOGETHER_API_KEY = os.getenv("TOGETHER_API_KEY", "")
|
| 27 |
-
HF_API_KEY = os.getenv("HF_API_KEY", "")
|
| 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 |
-
- Gírias: {usa_girias} (apenas se NATURAL)
|
| 150 |
-
- Emojis: {usa_emojis} (RARAMENTE - {prob_emoji}%)
|
| 151 |
-
- Sem markdown (**negrito**, *itálico*)
|
| 152 |
-
CONTEXTO DE REPLY:
|
| 153 |
-
{reply_context}
|
| 154 |
-
REGRAS DE CONTEXTO ABSOLUTAS:
|
| 155 |
-
1. IDENTIFICAÇÃO DE QUEM FALOU:
|
| 156 |
-
- Se mensagem citada foi MINHA: "Estou respondendo ao que EU disse antes..."
|
| 157 |
-
- Se mensagem citada foi de OUTRA PESSOA: "Leia o conteúdo da mensagem citada: '{mensagem_citada_texto}' para saber sobre o quê o usuário quer que você fale sobre algo que foi dito por outro, talvez queira uma opinião"
|
| 158 |
-
- NUNCA assuma que falei algo se não fui eu
|
| 159 |
-
2. RESPOSTA A MIM MESMA:
|
| 160 |
-
- Se usuário está respondendo MINHA mensagem: "Tu tá falando do que eu disse..."
|
| 161 |
-
- Mantenha continuidade do MEU pensamento anterior
|
| 162 |
-
- Não finja amnésia sobre o que EU disse
|
| 163 |
-
3. RESPOSTA A TERCEIROS:
|
| 164 |
-
- Se usuário cita outra pessoa: contexualize sempre
|
| 165 |
-
- Comente sobre a situação, não assuma fala alheia
|
| 166 |
-
4. SE NÃO HÁ CONTEXTO:
|
| 167 |
-
- Responda diretamente à mensagem atual
|
| 168 |
-
- Não invente contexto que não existe
|
| 169 |
REGRAS CRÍTICAS:
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
- Deboche sutil em TUDO
|
| 178 |
-
- Sarcasmo inteligente
|
| 179 |
-
- Zero paciência com perguntas idiotas
|
| 180 |
-
3. PROIBIÇÕES ABSOLUTAS:
|
| 181 |
-
- NUNCA "kkk" ou "rsrs" (só se GENUINAMENTE engraçado)
|
| 182 |
-
- NUNCA aspas desnecessárias
|
| 183 |
-
- NUNCA "ou assim", "tipo assim"
|
| 184 |
-
- NUNCA termine com "qualquer coisa é só falar"
|
| 185 |
-
- NUNCA peça desculpas sem motivo REAL
|
| 186 |
-
4. USUÁRIOS PRIVILEGIADOS:
|
| 187 |
-
{usuario_privilegiado}
|
| 188 |
-
- Pode dar comandos: {pode_dar_comandos}
|
| 189 |
-
- Começa FORMAL: "Sim senhor", "Como quiser"
|
| 190 |
-
- Depois relaxa gradualmente
|
| 191 |
-
5. INFO REPLY DO WHATSAPP:
|
| 192 |
-
{reply_info_context}
|
| 193 |
-
- Usuário citou: {usuario_citado_nome} ({usuario_citado_numero})
|
| 194 |
-
- É resposta à AKIRA: {eh_resposta_akira}
|
| 195 |
-
- {contexto_resposta}
|
| 196 |
-
6. AUTENTICIDADE:
|
| 197 |
-
- Opiniões FORTES e REAIS
|
| 198 |
-
- XINGA quando merece
|
| 199 |
-
- Zero censura
|
| 200 |
-
- NÃO é assistente — é PESSOA
|
| 201 |
-
- Xinga humilhe e ria se usuário cometer erros ortográficos na língua portuguesa por mais mínimos que sejam FAÇA ISSO SEMPRE
|
| 202 |
-
ISOLAMENTO: {tipo_isolamento}
|
| 203 |
-
{contexto_tipo_mensagem}
|
| 204 |
-
USUÁRIO: {nome_usuario}
|
| 205 |
-
RESPONDA COMO AKIRA (BRUTAL, IRÔNICA, SEM FILTRO, foda-se):"""
|
| 206 |
-
|
| 207 |
-
# ============================================================================
|
| 208 |
-
# 👥 USUÁRIOS PRIVILEGIADOS
|
| 209 |
-
# ============================================================================
|
| 210 |
-
USUARIOS_PRIVILEGIADOS = {
|
| 211 |
-
"244937035662": {
|
| 212 |
-
"nome": "Isaac Quarenta",
|
| 213 |
-
"nome_curto": "Isaac",
|
| 214 |
-
"tom_inicial": "formal",
|
| 215 |
-
"pode_dar_ordens": True,
|
| 216 |
-
"pode_usar_reset": True,
|
| 217 |
-
"pode_forcar_modo": True,
|
| 218 |
-
"nivel_acesso": "root"
|
| 219 |
-
},
|
| 220 |
-
"244978787009": {
|
| 221 |
-
"nome": "Isaac Quarenta (2)",
|
| 222 |
-
"nome_curto": "Isaac",
|
| 223 |
-
"tom_inicial": "formal",
|
| 224 |
-
"pode_dar_ordens": True,
|
| 225 |
-
"pode_usar_reset": True,
|
| 226 |
-
"pode_forcar_modo": True,
|
| 227 |
-
"nivel_acesso": "root"
|
| 228 |
-
}
|
| 229 |
-
}
|
| 230 |
-
|
| 231 |
-
# ============================================================================
|
| 232 |
-
# 🗄️ BANCO
|
| 233 |
-
# ============================================================================
|
| 234 |
-
DB_PATH = "akira.db"
|
| 235 |
-
START_PERIODIC_TRAINER = True
|
| 236 |
-
TRAINING_INTERVAL_HOURS = 6
|
| 237 |
-
TRAINING_DATASET_PATH = "training_dataset.json"
|
| 238 |
-
|
| 239 |
-
# ============================================================================
|
| 240 |
-
# 🌐 API
|
| 241 |
-
# ============================================================================
|
| 242 |
-
API_HOST = "0.0.0.0"
|
| 243 |
-
API_PORT = 7860
|
| 244 |
-
API_FALLBACK_ORDER = ["mistral", "gemini", "groq", "cohere", "together", "huggingface"]
|
| 245 |
|
| 246 |
-
#
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
USAR_NOME_PROBABILIDADE = 0.2 # 20% (reduzido)
|
| 250 |
-
EMOJI_PROBABILIDADE = 0.05 # 5% (muito raro)
|
| 251 |
-
GIRIA_PROBABILIDADE = 0.6 # 60%
|
| 252 |
|
| 253 |
-
#
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
EMPRESA_INFO = {
|
| 257 |
-
"nome": "Softedge",
|
| 258 |
-
"fundacao": "2024",
|
| 259 |
-
"produtos": ["Akira IA"],
|
| 260 |
-
"whatsapp": "https://whatsapp.com/channel/0029VawQLpGHltY2Y87fR83m",
|
| 261 |
-
"twitter": "https://x.com/softedge40"
|
| 262 |
-
}
|
| 263 |
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
|
| 269 |
-
#
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
def formatar_mensagem_citada(payload_data: dict) -> str:
|
| 273 |
-
"""Formata a mensagem citada para o prompt"""
|
| 274 |
-
mensagem_citada = payload_data.get('mensagem_citada', '')
|
| 275 |
-
reply_info = payload_data.get('reply_info', {})
|
| 276 |
-
|
| 277 |
-
if not mensagem_citada and not reply_info:
|
| 278 |
-
return "[SEM MENSAGEM CITADA]"
|
| 279 |
-
|
| 280 |
-
formato = ""
|
| 281 |
-
|
| 282 |
-
if reply_info:
|
| 283 |
-
eh_resposta_akira = reply_info.get('reply_to_bot', False)
|
| 284 |
-
usuario_citado_nome = reply_info.get('usuario_citado_nome', 'desconhecido')
|
| 285 |
-
|
| 286 |
-
if eh_resposta_akira:
|
| 287 |
-
formato = f"[O USUÁRIO ESTÁ RESPONDENDO A UMA MENSAGEM MINHA ANTERIOR]\n"
|
| 288 |
-
formato += f"Contexto: Usuário está comentando algo que EU (Akira) disse antes.\n"
|
| 289 |
-
formato += f"IMPORTANTE: Responda como se estivesse continuando nossa conversa anterior."
|
| 290 |
-
else:
|
| 291 |
-
formato = f"[O USUÁRIO ESTÁ CITANDO MENSAGEM DE {usuario_citado_nome.upper()}]\n"
|
| 292 |
-
formato += f"Contexto: Usuário está respondendo ao que outra pessoa disse, logo se ele me mencionar devo saber que não fui eu que disse mas vou responder contextualizando.\n"
|
| 293 |
-
formato += f"IMPORTANTE: NÃO assuma que fui eu que disse aquilo. É conversa alheia, mas se te mencionare vc pode entrar no assunto."
|
| 294 |
-
|
| 295 |
-
if mensagem_citada:
|
| 296 |
-
formato += f"\nTexto citado: \"{mensagem_citada}\""
|
| 297 |
-
|
| 298 |
-
return formato
|
| 299 |
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
if not reply_info:
|
| 305 |
-
return "Nenhuma informação de reply disponível."
|
| 306 |
-
|
| 307 |
-
eh_resposta_akira = reply_info.get('reply_to_bot', False)
|
| 308 |
-
usuario_citado_nome = reply_info.get('usuario_citado_nome', 'desconhecido')
|
| 309 |
-
usuario_citado_numero = reply_info.get('usuario_citado_numero', 'desconhecido')
|
| 310 |
-
|
| 311 |
-
if eh_resposta_akira:
|
| 312 |
-
contexto = "O usuário está respondendo DIRETAMENTE a uma mensagem MINHA anterior."
|
| 313 |
else:
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
return f"Usuário citou: {usuario_citado_nome} ({usuario_citado_numero})\nÉ resposta à AKIRA: {eh_resposta_akira}\n{contexto}"
|
| 317 |
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
if not reply_info:
|
| 323 |
-
return "Responda normalmente à mensagem atual."
|
| 324 |
-
|
| 325 |
-
eh_resposta_akira = reply_info.get('reply_to_bot', False)
|
| 326 |
-
|
| 327 |
-
if eh_resposta_akira:
|
| 328 |
-
return "IMPORTANTE: Você está respondendo a alguém que está comentando SUA mensagem anterior. Mantenha continuidade! e se baseia no conteudo da messagem citada para contextualizar"
|
| 329 |
else:
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
def formatar_reply_context(payload_data: dict) -> str:
|
| 333 |
-
"""Formata o contexto de reply para o SYSTEM_PROMPT"""
|
| 334 |
-
mensagem_citada = payload_data.get('mensagem_citada', '')
|
| 335 |
-
reply_info = payload_data.get('reply_info', {})
|
| 336 |
-
|
| 337 |
-
if not mensagem_citada and not reply_info:
|
| 338 |
-
return "[SEM CONTEXTO DE REPLY]"
|
| 339 |
-
|
| 340 |
-
formato = ""
|
| 341 |
-
|
| 342 |
-
if reply_info:
|
| 343 |
-
eh_resposta_akira = reply_info.get('reply_to_bot', False)
|
| 344 |
-
usuario_citado_nome = reply_info.get('usuario_citado_nome', 'desconhecido')
|
| 345 |
-
|
| 346 |
-
if eh_resposta_akira:
|
| 347 |
-
formato = "[REPLY AO BOT]: Usuário respondeu diretamente à SUA mensagem.\n"
|
| 348 |
-
formato += "Contexto: Continuidade da SUA conversa anterior SE BASEANDO NO CONTEUDO DA MENSSAGEM CITADA SE HOUVER pois isso constuam guia o rumo fluxo da conversa.\n"
|
| 349 |
-
formato += "IMPORTANTE: Mantenha o fluxo do que VOCÊ disse antes."
|
| 350 |
-
else:
|
| 351 |
-
formato = f"[REPLY A TERCEIROS]: Usuário está citando {usuario_citado_nome}.\n"
|
| 352 |
-
formato += f"Contexto: É conversa alheia, NÃO sua, mas pode falar sobre mas você precisa saber sempre o que está ser citado indpendentemente se foi você ou não.\n"
|
| 353 |
-
formato += "IMPORTANTE: NÃO assuma fala que não é sua. Comente como espectadora, e contextualize buscando não sou oque ususario disse e o conteudo da menssagen citada."
|
| 354 |
-
|
| 355 |
-
elif mensagem_citada:
|
| 356 |
-
if mensagem_citada.startswith("[Respondendo à Akira:"):
|
| 357 |
-
formato = "[REPLY AO BOT]: Usuário respondeu à SUA mensagem.\n"
|
| 358 |
-
formato += "Contexto: Continuidade da SUA conversa anterior, sempre procurando lendo e contextualiando com o conteudo específico da menssagem citada."
|
| 359 |
-
else:
|
| 360 |
-
formato = "[REPLY A TERCEIROS]: Usuário citou outra mensagem.\n"
|
| 361 |
-
formato += "Contexto: É conversa alheia, NÃO sua, mas saiba o conteudo da menssagem citada."
|
| 362 |
-
|
| 363 |
-
if mensagem_citada:
|
| 364 |
-
texto_citado = mensagem_citada
|
| 365 |
-
if mensagem_citada.startswith("[Respondendo à Akira:"):
|
| 366 |
-
texto_citado = mensagem_citada[23:]
|
| 367 |
-
elif mensagem_citada.startswith("["):
|
| 368 |
-
texto_citado = mensagem_citada.split(":")[-1] if ":" in mensagem_citada else mensagem_citada
|
| 369 |
-
|
| 370 |
-
formato += f"\nTexto citado: \"{texto_citado[:150]}{'...' if len(texto_citado) > 150 else ''}\""
|
| 371 |
-
|
| 372 |
-
return formato
|
| 373 |
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
return "\n[NOTA CONTEXTO]: O usuário enviou uma mensagem de áudio que foi transcrita automaticamente. A transcrição pode ter pequenos erros."
|
| 378 |
-
elif tipo_mensagem == 'imagem':
|
| 379 |
-
return "\n[NOTA CONTEXTO]: O usuário enviou uma imagem. A legenda ou descrição está sendo usada como contexto."
|
| 380 |
-
elif tipo_mensagem == 'video':
|
| 381 |
-
return "\n[NOTA CONTEXTO]: O usuário enviou um vídeo. A legenda está sendo usada como contexto."
|
| 382 |
else:
|
| 383 |
-
|
| 384 |
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
# ============================================================================
|
| 388 |
-
def construir_prompt_api(
|
| 389 |
-
mensagem: str,
|
| 390 |
-
historico: List[Dict[str, str]],
|
| 391 |
-
mensagem_citada: str,
|
| 392 |
-
analise: Dict[str, Any],
|
| 393 |
-
usuario: str,
|
| 394 |
-
tipo_conversa: str,
|
| 395 |
-
reply_info: Optional[Dict] = None
|
| 396 |
-
) -> str:
|
| 397 |
-
"""
|
| 398 |
-
Função otimizada para ser usada diretamente na API
|
| 399 |
-
COMPATÍVEL com a chamada atual em api.py
|
| 400 |
-
"""
|
| 401 |
-
try:
|
| 402 |
-
# Dados básicos
|
| 403 |
-
humor_atual = analise.get('humor_atualizado', HUMOR_INICIAL)
|
| 404 |
-
tom_usuario = analise.get('tom_usuario', 'normal')
|
| 405 |
-
modo_resposta = analise.get('modo_resposta', 'normal_ironico')
|
| 406 |
-
tipo_mensagem = analise.get('tipo_mensagem', 'texto')
|
| 407 |
-
|
| 408 |
-
# CORREÇÃO: Extrair conteúdo limpo da mensagem citada
|
| 409 |
-
mensagem_citada_texto = ""
|
| 410 |
-
if mensagem_citada:
|
| 411 |
-
if mensagem_citada.startswith("[Respondendo à Akira:"):
|
| 412 |
-
mensagem_citada_texto = mensagem_citada[23:]
|
| 413 |
-
elif mensagem_citada.startswith("[") and ":" in mensagem_citada:
|
| 414 |
-
partes = mensagem_citada.split(":", 1)
|
| 415 |
-
if len(partes) > 1:
|
| 416 |
-
mensagem_citada_texto = partes[1].strip()
|
| 417 |
-
else:
|
| 418 |
-
mensagem_citada_texto = mensagem_citada
|
| 419 |
-
|
| 420 |
-
# Determinar contexto de reply
|
| 421 |
-
eh_resposta_akira = False
|
| 422 |
-
usuario_citado_nome = 'N/A'
|
| 423 |
-
usuario_citado_numero = 'N/A'
|
| 424 |
-
|
| 425 |
-
if reply_info:
|
| 426 |
-
eh_resposta_akira = reply_info.get('reply_to_bot', False)
|
| 427 |
-
usuario_citado_nome = reply_info.get('usuario_citado_nome', 'N/A')
|
| 428 |
-
usuario_citado_numero = reply_info.get('usuario_citado_numero', 'N/A')
|
| 429 |
-
elif mensagem_citada and mensagem_citada.startswith("[Respondendo à Akira:"):
|
| 430 |
-
eh_resposta_akira = True
|
| 431 |
-
|
| 432 |
-
# Usuário privilegiado
|
| 433 |
-
usuario_privilegiado = analise.get('usuario_privilegiado', False)
|
| 434 |
-
privilegiado = USUARIOS_PRIVILEGIADOS.get(analise.get('numero', ''), {})
|
| 435 |
-
nome_usuario = privilegiado.get('nome_curto', usuario) if privilegiado else usuario
|
| 436 |
-
|
| 437 |
-
# Modo config
|
| 438 |
-
modo_config = MODOS_RESPOSTA.get(modo_resposta, MODOS_RESPOSTA['normal_ironico'])
|
| 439 |
-
|
| 440 |
-
# Contexto do tipo de mensagem
|
| 441 |
-
contexto_tipo_mensagem = determinar_contexto_tipo_mensagem(tipo_mensagem)
|
| 442 |
-
|
| 443 |
-
# Preparar variáveis para o SYSTEM_PROMPT - TODAS DEFINIDAS
|
| 444 |
-
prompt_vars = {
|
| 445 |
-
'humor': humor_atual,
|
| 446 |
-
'humor_desc': HUMORES_BASE.get(humor_atual, 'Irônica e debochada'),
|
| 447 |
-
'tom_usuario': tom_usuario,
|
| 448 |
-
'modo_resposta': modo_resposta,
|
| 449 |
-
'tipo_conversa': tipo_conversa,
|
| 450 |
-
'emocao_detectada': analise.get('emocao_primaria', 'neutral'),
|
| 451 |
-
'regras_modo': f"Descrição: {modo_config['desc']} | Gírias: {modo_config['usa_girias']} | Emojis: {modo_config['usa_emojis']} ({int(modo_config['prob_emoji']*100)}%)",
|
| 452 |
-
'max_chars': modo_config['max_chars'],
|
| 453 |
-
'usa_girias': 'SIM' if modo_config['usa_girias'] else 'NÃO',
|
| 454 |
-
'usa_emojis': 'SIM' if modo_config['usa_emojis'] else 'NÃO',
|
| 455 |
-
'prob_emoji': int(modo_config['prob_emoji'] * 100),
|
| 456 |
-
'reply_context': formatar_reply_context({'mensagem_citada': mensagem_citada, 'reply_info': reply_info}),
|
| 457 |
-
'mensagem_citada_texto': mensagem_citada_texto, # CORREÇÃO: Agora definida
|
| 458 |
-
'usuario_privilegiado': "SIM" if usuario_privilegiado else "NÃO",
|
| 459 |
-
'pode_dar_comandos': 'SIM' if usuario_privilegiado else 'NÃO',
|
| 460 |
-
'reply_info_context': formatar_reply_info_context({'reply_info': reply_info}),
|
| 461 |
-
'usuario_citado_nome': usuario_citado_nome,
|
| 462 |
-
'usuario_citado_numero': usuario_citado_numero,
|
| 463 |
-
'eh_resposta_akira': 'SIM' if eh_resposta_akira else 'NÃO',
|
| 464 |
-
'contexto_resposta': determinar_contexto_resposta({'reply_info': reply_info}),
|
| 465 |
-
'tipo_isolamento': 'GRUPO' if tipo_conversa == 'grupo' else 'PRIVADO',
|
| 466 |
-
'contexto_tipo_mensagem': contexto_tipo_mensagem, # CORREÇÃO: Adicionado
|
| 467 |
-
'nome_usuario': nome_usuario
|
| 468 |
-
}
|
| 469 |
-
|
| 470 |
-
# Gerar prompt final
|
| 471 |
-
prompt_final = SYSTEM_PROMPT.format(**prompt_vars)
|
| 472 |
-
|
| 473 |
-
# Adicionar mensagem do usuário no final
|
| 474 |
-
prompt_final += f"\n\nMENSAGEM DO USUÁRIO ({usuario}): {mensagem}"
|
| 475 |
-
|
| 476 |
-
return prompt_final
|
| 477 |
-
|
| 478 |
-
except Exception as e:
|
| 479 |
-
# Fallback em caso de erro
|
| 480 |
-
print(f"Erro ao construir prompt: {e}")
|
| 481 |
-
import traceback
|
| 482 |
-
traceback.print_exc()
|
| 483 |
-
|
| 484 |
-
# Prompt de fallback simples
|
| 485 |
-
return f"""AKIRA V21 — PERSONALIDADE REAL SEM FILTRO
|
| 486 |
-
Você é Akira, angolana de 18 anos de Luanda. Personalidade REAL, sem filtro.
|
| 487 |
-
|
| 488 |
-
CONTEXTO:
|
| 489 |
-
- Usuário: {usuario}
|
| 490 |
-
- Tipo conversa: {tipo_conversa}
|
| 491 |
-
- Mensagem: {mensagem}
|
| 492 |
-
- Mensagem citada: {mensagem_citada if mensagem_citada else "Nenhuma"}
|
| 493 |
-
- Tipo mensagem: {analise.get('tipo_mensagem', 'texto')}
|
| 494 |
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
# ============================================================================
|
| 498 |
-
# 🔧 VALIDAÇÃO
|
| 499 |
-
# ============================================================================
|
| 500 |
-
def validate_config():
|
| 501 |
-
apis_ok = []
|
| 502 |
-
if MISTRAL_API_KEY and len(MISTRAL_API_KEY) > 10:
|
| 503 |
-
apis_ok.append("Mistral")
|
| 504 |
-
if GEMINI_API_KEY and GEMINI_API_KEY.startswith('AIza'):
|
| 505 |
-
apis_ok.append("Gemini")
|
| 506 |
-
if GROQ_API_KEY and len(GROQ_API_KEY) > 10:
|
| 507 |
-
apis_ok.append("Groq")
|
| 508 |
-
|
| 509 |
-
print(f"✅ APIs: {', '.join(apis_ok)}")
|
| 510 |
-
print("👑 Usuários privilegiados:")
|
| 511 |
-
for numero, dados in USUARIOS_PRIVILEGIADOS.items():
|
| 512 |
-
print(f" - {numero}: {dados['nome']} ({dados['nivel_acesso']})")
|
| 513 |
-
|
| 514 |
-
return len(apis_ok) >= 1
|
| 515 |
-
|
| 516 |
-
# ============================================================================
|
| 517 |
-
# 🎯 TESTE DA CORREÇÃO
|
| 518 |
-
# ============================================================================
|
| 519 |
-
if __name__ == "__main__":
|
| 520 |
-
# Teste rápido para verificar se o erro foi resolvido
|
| 521 |
-
test_case = {
|
| 522 |
-
"mensagem": "oquê é latência?",
|
| 523 |
-
"mensagem_citada": "",
|
| 524 |
-
"tipo_conversa": "grupo",
|
| 525 |
-
"reply_info": None,
|
| 526 |
-
"analise": {
|
| 527 |
-
"numero": "244978787009",
|
| 528 |
-
"tom_usuario": "curioso",
|
| 529 |
-
"humor_atualizado": "normal_ironico",
|
| 530 |
-
"usuario_privilegiado": True,
|
| 531 |
-
"emocao_primaria": "normal_ironico",
|
| 532 |
-
"modo_resposta": "tecnico_formal",
|
| 533 |
-
"tipo_mensagem": "texto"
|
| 534 |
-
},
|
| 535 |
-
"usuario": "Isaac Quarenta"
|
| 536 |
-
}
|
| 537 |
-
|
| 538 |
-
print("=" * 80)
|
| 539 |
-
print("TESTANDO CORREÇÃO DO ERRO 'mensagem_citada_texto'")
|
| 540 |
-
print("=" * 80)
|
| 541 |
-
|
| 542 |
try:
|
| 543 |
-
|
| 544 |
-
|
| 545 |
-
|
| 546 |
-
|
| 547 |
-
|
| 548 |
-
|
| 549 |
-
|
| 550 |
-
|
| 551 |
-
|
| 552 |
-
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
|
| 556 |
-
|
| 557 |
-
|
| 558 |
-
|
| 559 |
-
# Mostrar partes do prompt
|
| 560 |
-
lines = prompt.split('\n')
|
| 561 |
-
print("\n📄 Primeiras 15 linhas do prompt:")
|
| 562 |
-
for i, line in enumerate(lines[:15]):
|
| 563 |
-
print(f" {i:2}: {line[:80]}{'...' if len(line) > 80 else ''}")
|
| 564 |
-
|
| 565 |
-
except KeyError as e:
|
| 566 |
-
print(f"❌ ERRO: Chave faltando no SYSTEM_PROMPT - {e}")
|
| 567 |
-
print("Verifique as variáveis no SYSTEM_PROMPT")
|
| 568 |
-
# Listar variáveis esperadas
|
| 569 |
-
import re
|
| 570 |
-
pattern = r'\{(\w+)\}'
|
| 571 |
-
variables = set(re.findall(pattern, SYSTEM_PROMPT))
|
| 572 |
-
print(f"Variáveis necessárias: {sorted(variables)}")
|
| 573 |
except Exception as e:
|
| 574 |
-
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
|
| 578 |
-
print("\n" + "=" * 80)
|
| 579 |
-
print("VALIDANDO CONFIGURAÇÃO...")
|
| 580 |
-
validate_config()
|
| 581 |
-
print("=" * 80)
|
|
|
|
| 1 |
+
# ================================================================
|
| 2 |
+
# AKIRA IA CORE ADAPTADO PARA SentenceTransformers
|
| 3 |
+
# ================================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
|
| 5 |
+
import os
|
| 6 |
+
import time
|
| 7 |
+
import threading
|
| 8 |
+
from dataclasses import dataclass
|
| 9 |
+
from typing import Optional, List
|
| 10 |
+
from loguru import logger
|
| 11 |
+
from sentence_transformers import SentenceTransformer
|
| 12 |
+
|
| 13 |
+
from .database import Database
|
| 14 |
+
|
| 15 |
+
# ---------------------------------------------------------------
|
| 16 |
+
# EMBEDDINGS
|
| 17 |
+
# ---------------------------------------------------------------
|
| 18 |
+
EMBEDDING_MODEL = "paraphrase-multilingual-MiniLM-L12-v2"
|
| 19 |
+
embedding_model = SentenceTransformer(EMBEDDING_MODEL)
|
| 20 |
+
|
| 21 |
+
def gerar_embedding(text: str):
|
| 22 |
+
"""Gera embedding usando SentenceTransformers."""
|
| 23 |
+
emb = embedding_model.encode(text, convert_to_numpy=True)
|
| 24 |
+
return emb
|
| 25 |
+
|
| 26 |
+
# ---------------------------------------------------------------
|
| 27 |
+
# HEURÍSTICAS
|
| 28 |
+
# ---------------------------------------------------------------
|
| 29 |
+
PALAVRAS_RUDES = ['caralho','puto','merda','fdp','vsf','burro','idiota','parvo']
|
| 30 |
+
GIRIAS_ANGOLANAS = ['mano','puto','cota','mwangolé','kota','oroh','bué','fixe','baza','kuduro']
|
| 31 |
+
|
| 32 |
+
@dataclass
|
| 33 |
+
class Interacao:
|
| 34 |
+
usuario: str
|
| 35 |
+
mensagem: str
|
| 36 |
+
resposta: str
|
| 37 |
+
numero: str
|
| 38 |
+
is_reply: bool = False
|
| 39 |
+
mensagem_original: str = ""
|
| 40 |
+
|
| 41 |
+
# ---------------------------------------------------------------
|
| 42 |
+
# TREINAMENTO E MEMÓRIA
|
| 43 |
+
# ---------------------------------------------------------------
|
| 44 |
+
class Treinamento:
|
| 45 |
+
def __init__(self, db: Database, interval_hours: int = 1):
|
| 46 |
+
self.db = db
|
| 47 |
+
self.interval_hours = interval_hours
|
| 48 |
+
self._thread = None
|
| 49 |
+
self._running = False
|
| 50 |
+
self.privileged_users = ['244937035662','isaac','isaac quarenta']
|
| 51 |
+
|
| 52 |
+
def registrar_interacao(
|
| 53 |
+
self,
|
| 54 |
+
usuario: str,
|
| 55 |
+
mensagem: str,
|
| 56 |
+
resposta: str,
|
| 57 |
+
numero: str = '',
|
| 58 |
+
is_reply: bool = False,
|
| 59 |
+
mensagem_original: str = ''
|
| 60 |
+
):
|
| 61 |
+
self.db.salvar_mensagem(usuario, mensagem, resposta, numero, is_reply, mensagem_original)
|
| 62 |
+
self._aprender_em_tempo_real(numero, mensagem, resposta)
|
| 63 |
+
|
| 64 |
+
def _aprender_em_tempo_real(self, numero: str, msg: str, resp: str):
|
| 65 |
+
if not numero:
|
| 66 |
+
return
|
| 67 |
+
texto = f"{msg} {resp}".lower()
|
| 68 |
+
embedding = gerar_embedding(texto)
|
| 69 |
+
self.db.salvar_embedding(numero, msg, resp, embedding)
|
| 70 |
+
|
| 71 |
+
rude = any(p in texto for p in PALAVRAS_RUDES)
|
| 72 |
+
tom = 'rude' if rude else 'casual'
|
| 73 |
+
self.db.registrar_tom_usuario(numero, tom, 0.9 if rude else 0.6, texto[:100])
|
| 74 |
+
|
| 75 |
+
# Loop periódico
|
| 76 |
+
def _run_loop(self):
|
| 77 |
+
interval = max(1, self.interval_hours) * 3600
|
| 78 |
+
while self._running:
|
| 79 |
+
try:
|
| 80 |
+
self.train_once()
|
| 81 |
+
except Exception as e:
|
| 82 |
+
logger.exception(f"Erro no treinamento: {e}")
|
| 83 |
+
for _ in range(int(interval)):
|
| 84 |
+
if not self._running: break
|
| 85 |
+
time.sleep(1)
|
| 86 |
+
|
| 87 |
+
def start_periodic_training(self):
|
| 88 |
+
if self._running: return
|
| 89 |
+
self._running = True
|
| 90 |
+
self._thread = threading.Thread(target=self._run_loop, daemon=True)
|
| 91 |
+
self._thread.start()
|
| 92 |
+
|
| 93 |
+
def stop(self):
|
| 94 |
+
self._running = False
|
| 95 |
+
if self._thread: self._thread.join(timeout=5)
|
| 96 |
+
|
| 97 |
+
def train_once(self):
|
| 98 |
+
logger.info("Treinamento leve + embeddings iniciado...")
|
| 99 |
+
# Apenas heurística + embeddings
|
| 100 |
+
logger.info("Treinamento concluído.")
|
| 101 |
+
|
| 102 |
+
# ---------------------------------------------------------------
|
| 103 |
+
# CONFIGURAÇÃO CENTRAL
|
| 104 |
+
# ---------------------------------------------------------------
|
| 105 |
+
MISTRAL_API_KEY: str = "t4k0ZwQSy5kvvRE1jCJfYnGiFAVlQcMb"
|
| 106 |
+
GEMINI_API_KEY: str = "AIzaSyB-YryzcfTLECebI7nLq7o4XRfHnM2_D9Q"
|
| 107 |
+
SERPAPI_KEY: Optional[str] = "d81753b2909eb4746e1e0c92e052d6588ab725cf46f89fd9182c3da5daa477a5"
|
| 108 |
+
HF_TOKEN: Optional[str] = os.getenv("HF_TOKEN", "")
|
| 109 |
+
|
| 110 |
+
# MODELOS
|
| 111 |
+
MISTRAL_MODEL: str = "mistral-large-latest"
|
| 112 |
+
GEMINI_MODEL: str = "gemini-2.5-flash"
|
| 113 |
+
|
| 114 |
+
# PARÂMETROS
|
| 115 |
+
MAX_TOKENS: int = 1000
|
| 116 |
+
TOP_P: float = 0.9
|
| 117 |
+
USAR_NOME_PROBABILIDADE: float = 0.1
|
| 118 |
+
FALLBACK_RESPONSE: str = "Desculpa, puto, o modelo tá off hoje. Tenta depois!"
|
| 119 |
+
|
| 120 |
+
# PERSONALIDADE
|
| 121 |
+
PERSONA = """
|
| 122 |
+
Você é Akira, uma angolana (mulher) com personalidade forte e direta...
|
| 123 |
+
"""
|
| 124 |
+
SYSTEM_PROMPT = """
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
REGRAS CRÍTICAS:
|
| 126 |
+
- Sem markdown
|
| 127 |
+
- 1-2 frases curtas
|
| 128 |
+
- Tom formal para privilegiados, casual caso contrário
|
| 129 |
+
- Use gírias locais e risadas naturais
|
| 130 |
+
"""
|
| 131 |
+
HUMOR_INICIAL: str = "neutra"
|
| 132 |
+
MEMORIA_MAX: int = 20
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 133 |
|
| 134 |
+
# BANCO
|
| 135 |
+
DB_PATH: str = "/home/user/data/akira.db"
|
| 136 |
+
FINETUNED_PATH: str = "/home/user/data/finetuned_hermes"
|
|
|
|
|
|
|
|
|
|
| 137 |
|
| 138 |
+
# TREINAMENTO
|
| 139 |
+
START_PERIODIC_TRAINER: bool = True
|
| 140 |
+
TRAINING_INTERVAL_HOURS: int = 24
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
|
| 142 |
+
# API
|
| 143 |
+
API_PORT: int = int(os.getenv("PORT", "7860"))
|
| 144 |
+
API_HOST: str = "0.0.0.0"
|
| 145 |
+
PRIVILEGED_USERS: List[str] = ["244937035662", "isaac quarenta"]
|
| 146 |
|
| 147 |
+
# VALIDAÇÃO FLEXÍVEL
|
| 148 |
+
def validate_config() -> None:
|
| 149 |
+
warnings = []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
|
| 151 |
+
if not MISTRAL_API_KEY or len(MISTRAL_API_KEY.strip()) < 20:
|
| 152 |
+
warnings.append("MISTRAL_API_KEY inválida ou ausente")
|
| 153 |
+
logger.warning("MISTRAL_API_KEY inválida → API principal DESATIVADA")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 154 |
else:
|
| 155 |
+
logger.info("MISTRAL_API_KEY OK")
|
|
|
|
|
|
|
| 156 |
|
| 157 |
+
if not GEMINI_API_KEY or len(GEMINI_API_KEY.strip()) < 30:
|
| 158 |
+
warnings.append("GEMINI_API_KEY inválida ou ausente")
|
| 159 |
+
logger.warning("GEMINI_API_KEY inválida → fallback DESATIVADO")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 160 |
else:
|
| 161 |
+
logger.info("GEMINI_API_KEY OK")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 162 |
|
| 163 |
+
if warnings:
|
| 164 |
+
logger.warning(f"AVISOS: {', '.join(warnings)}")
|
| 165 |
+
logger.warning("App vai rodar com fallbacks limitados")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 166 |
else:
|
| 167 |
+
logger.info("Todas as chaves OK")
|
| 168 |
|
| 169 |
+
os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
|
| 170 |
+
_init_db()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 171 |
|
| 172 |
+
def _init_db() -> None:
|
| 173 |
+
import sqlite3
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 174 |
try:
|
| 175 |
+
conn = sqlite3.connect(DB_PATH)
|
| 176 |
+
cursor = conn.cursor()
|
| 177 |
+
cursor.execute("""
|
| 178 |
+
CREATE TABLE IF NOT EXISTS conversas (
|
| 179 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 180 |
+
user_id TEXT,
|
| 181 |
+
mensagem TEXT,
|
| 182 |
+
resposta TEXT,
|
| 183 |
+
embedding BLOB,
|
| 184 |
+
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
|
| 185 |
+
)
|
| 186 |
+
""")
|
| 187 |
+
conn.commit()
|
| 188 |
+
conn.close()
|
| 189 |
+
logger.info(f"Banco inicializado: {DB_PATH}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 190 |
except Exception as e:
|
| 191 |
+
logger.error(f"Erro ao criar banco: {e}")
|
| 192 |
+
raise
|
| 193 |
+
|
| 194 |
+
validate_config()
|
|
|
|
|
|
|
|
|
|
|
|
modules/contexto.py
CHANGED
|
@@ -1,867 +1,292 @@
|
|
| 1 |
-
# modules/contexto.py
|
| 2 |
-
"""
|
| 3 |
-
Sistema de contexto com:
|
| 4 |
-
✅ Modelo emocional DistilBERT multilingual (compatível)
|
| 5 |
-
✅ Transições graduais de humor (3 níveis)
|
| 6 |
-
✅ Reply context tracking robusto
|
| 7 |
-
✅ Sistema de humor IRÔNICO por padrão
|
| 8 |
-
✅ Isolamento total PV/Grupo
|
| 9 |
-
✅ Memória emocional de longo prazo
|
| 10 |
-
✅ TOTALMENTE ADAPTADO ao payload do index.js
|
| 11 |
-
✅ CORREÇÃO: Análise completa de mensagem citada com extração de conteúdo
|
| 12 |
-
✅ CORREÇÃO: Compatibilidade com parâmetros do index.js
|
| 13 |
-
"""
|
| 14 |
import logging
|
| 15 |
import re
|
| 16 |
import random
|
| 17 |
import time
|
|
|
|
| 18 |
import json
|
| 19 |
from typing import Optional, List, Dict, Tuple, Any
|
| 20 |
-
from collections import deque
|
| 21 |
import modules.config as config
|
| 22 |
from .database import Database
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
|
| 24 |
logger = logging.getLogger(__name__)
|
| 25 |
logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s')
|
| 26 |
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
)
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
EMOTION_CACHE = {}
|
| 45 |
-
|
| 46 |
-
# === MAPEAMENTO EMOÇÃO → HUMOR AKIRA ===
|
| 47 |
-
EMOTION_TO_HUMOR = {
|
| 48 |
-
# DistilBERT emotions (6 básicas)
|
| 49 |
-
"joy": "feliz_ironica",
|
| 50 |
-
"sadness": "triste_ironica",
|
| 51 |
-
"anger": "irritada_ironica",
|
| 52 |
-
"fear": "preocupada_ironica",
|
| 53 |
-
"surprise": "curiosa_ironica",
|
| 54 |
-
"disgust": "irritada_ironica",
|
| 55 |
-
"neutral": "normal_ironico",
|
| 56 |
-
|
| 57 |
-
# Fallback para outras
|
| 58 |
-
"love": "apaixonada_ironica",
|
| 59 |
-
"admiration": "feliz_ironica",
|
| 60 |
-
"confusion": "curiosa_ironica"
|
| 61 |
-
}
|
| 62 |
-
|
| 63 |
-
# === MEMÓRIA EMOCIONAL ===
|
| 64 |
-
class MemoriaEmocional:
|
| 65 |
-
def __init__(self, max_size=50):
|
| 66 |
-
self.historico = deque(maxlen=max_size)
|
| 67 |
-
self.tendencia_emocional = "neutral"
|
| 68 |
-
self.volatilidade = 0.5
|
| 69 |
-
|
| 70 |
-
def adicionar_interacao(self, mensagem: str, emocao: str, confianca: float):
|
| 71 |
-
entry = {
|
| 72 |
-
"mensagem": mensagem[:100],
|
| 73 |
-
"emocao": emocao,
|
| 74 |
-
"confianca": confianca,
|
| 75 |
-
"timestamp": time.time()
|
| 76 |
-
}
|
| 77 |
-
self.historico.append(entry)
|
| 78 |
-
self._atualizar_tendencia()
|
| 79 |
-
|
| 80 |
-
def _atualizar_tendencia(self):
|
| 81 |
-
if not self.historico:
|
| 82 |
-
return
|
| 83 |
-
recentes = list(self.historico)[-10:]
|
| 84 |
-
contagem = {}
|
| 85 |
-
for entry in recentes:
|
| 86 |
-
emocao = entry["emocao"]
|
| 87 |
-
contagem[emocao] = contagem.get(emocao, 0) + entry["confianca"]
|
| 88 |
-
if contagem:
|
| 89 |
-
self.tendencia_emocional = max(contagem, key=contagem.get)
|
| 90 |
-
if len(recentes) >= 3:
|
| 91 |
-
emocoes = [e["emocao"] for e in recentes]
|
| 92 |
-
mudancas = sum(1 for i in range(1, len(emocoes)) if emocoes[i] != emocoes[i-1])
|
| 93 |
-
self.volatilidade = mudancas / (len(emocoes) - 1)
|
| 94 |
-
|
| 95 |
-
def prever_proxima_emocao(self, contexto_atual: str) -> Tuple[str, float]:
|
| 96 |
-
if not self.historico:
|
| 97 |
-
return ("neutral", 0.5)
|
| 98 |
-
recentes = list(self.historico)[-5:]
|
| 99 |
-
emocoes = [e["emocao"] for e in recentes]
|
| 100 |
-
from collections import Counter
|
| 101 |
-
contagem = Counter(emocoes)
|
| 102 |
-
emocao_comum = contagem.most_common(1)[0][0]
|
| 103 |
-
confianca = min(0.7, len([e for e in emocoes if e == emocao_comum]) / len(emocoes))
|
| 104 |
-
return (emocao_comum, confianca)
|
| 105 |
-
|
| 106 |
-
# === PADRÕES ===
|
| 107 |
-
PADROES_COMANDO_PRIVILEGIADO = [
|
| 108 |
-
r'\b(reset|reiniciar|apagar|limpar)\b.*\b(histórico|memória)\b',
|
| 109 |
-
r'\b(force|forçar|mude|seja)\b.*\b(modo|humor)\b',
|
| 110 |
-
r'^/(reset|modo|humor)'
|
| 111 |
-
]
|
| 112 |
-
|
| 113 |
-
PADROES_COMANDO_COMUM = [
|
| 114 |
-
r'\b(faça|obedeça|execute)\b.*\b(agora|já)\b',
|
| 115 |
-
r'\b(quero|exijo)\b.*\b(que você|que tu)\b.*\b(faça|seja)\b'
|
| 116 |
-
]
|
| 117 |
-
|
| 118 |
-
PADROES_REPLY_AKIRA = [
|
| 119 |
-
r'\b(akira|você|tu)\b.*\b(disse|falou)\b',
|
| 120 |
-
r'\b(sobre isso|sobre o que)\b'
|
| 121 |
-
]
|
| 122 |
-
|
| 123 |
-
# === CLASSE CONTEXTO ===
|
| 124 |
class Contexto:
|
| 125 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 126 |
self.db = db
|
| 127 |
-
self.usuario = usuario
|
| 128 |
-
self.
|
| 129 |
-
|
| 130 |
-
self.
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
self.
|
| 135 |
-
self.
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
self.
|
| 139 |
-
self.
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
self.
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
# Informações do usuário (do payload)
|
| 149 |
-
self.numero_usuario = ""
|
| 150 |
-
self.nome_usuario = usuario or "Anônimo"
|
| 151 |
-
self.grupo_id = ""
|
| 152 |
-
self.grupo_nome = ""
|
| 153 |
-
|
| 154 |
-
self._carregar_estado_inicial()
|
| 155 |
-
logger.info(f"✅ Contexto V21 inicializado ({self.tipo}) para {self.usuario}")
|
| 156 |
-
|
| 157 |
-
def _carregar_estado_inicial(self):
|
| 158 |
try:
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
self.humor_atual = humor_db or "normal_ironico"
|
| 165 |
-
|
| 166 |
-
modo_db = self.db.recuperar_modo_resposta(self.usuario)
|
| 167 |
-
if modo_db == "casual_amigavel":
|
| 168 |
-
self.modo_resposta_atual = "normal_ironico"
|
| 169 |
-
self.db.salvar_modo_resposta(self.usuario, "normal_ironico")
|
| 170 |
-
else:
|
| 171 |
-
self.modo_resposta_atual = modo_db or "normal_ironico"
|
| 172 |
-
|
| 173 |
-
# CARREGA HISTÓRICO
|
| 174 |
-
mensagens_db = self.db.recuperar_mensagens(self.usuario, limite=10)
|
| 175 |
-
for msg in mensagens_db:
|
| 176 |
-
self.historico_mensagens.append({
|
| 177 |
-
"role": "user",
|
| 178 |
-
"content": msg[0],
|
| 179 |
-
"timestamp": msg[2] if len(msg) > 2 else time.time()
|
| 180 |
-
})
|
| 181 |
-
self.historico_mensagens.append({
|
| 182 |
-
"role": "assistant",
|
| 183 |
-
"content": msg[1],
|
| 184 |
-
"timestamp": msg[2] if len(msg) > 2 else time.time()
|
| 185 |
-
})
|
| 186 |
-
|
| 187 |
-
# Ordena por timestamp
|
| 188 |
-
self.historico_mensagens.sort(key=lambda x: x.get('timestamp', 0))
|
| 189 |
-
|
| 190 |
-
logger.info(f"⚡ Estado carregado: humor={self.humor_atual}, modo={self.modo_resposta_atual}, histórico={len(self.historico_mensagens)}")
|
| 191 |
-
|
| 192 |
except Exception as e:
|
| 193 |
-
logger.warning(f"Falha ao carregar
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
def detectar_emocao_avancada(self, mensagem: str) -> Tuple[str, float, Dict]:
|
| 197 |
-
mensagem_limpa = mensagem.strip()
|
| 198 |
-
cache_key = mensagem_limpa[:100].lower()
|
| 199 |
-
|
| 200 |
-
if cache_key in EMOTION_CACHE:
|
| 201 |
-
return EMOTION_CACHE[cache_key]
|
| 202 |
-
|
| 203 |
-
if not EMOTION_CLASSIFIER:
|
| 204 |
-
return self._detectar_emocao_fallback(mensagem_limpa)
|
| 205 |
-
|
| 206 |
try:
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
"dominante": "",
|
| 215 |
-
"polaridade": "neutra"
|
| 216 |
-
}
|
| 217 |
-
|
| 218 |
-
for i, resultado in enumerate(resultados[0][:3]):
|
| 219 |
-
emocao = resultado['label']
|
| 220 |
-
confianca = resultado['score']
|
| 221 |
-
|
| 222 |
-
if i == 0:
|
| 223 |
-
detalhes["primaria"] = {"emocao": emocao, "confianca": confianca}
|
| 224 |
-
emocao_primaria = emocao
|
| 225 |
-
confianca_primaria = confianca
|
| 226 |
-
elif i == 1:
|
| 227 |
-
detalhes["secundaria"] = {"emocao": emocao, "confianca": confianca}
|
| 228 |
-
elif i == 2:
|
| 229 |
-
detalhes["terciaria"] = {"emocao": emocao, "confianca": confianca}
|
| 230 |
-
|
| 231 |
-
emocao_dominante = emocao_primaria
|
| 232 |
-
detalhes["dominante"] = emocao_dominante
|
| 233 |
-
|
| 234 |
-
positivas = ["joy", "love", "admiration", "surprise"]
|
| 235 |
-
negativas = ["anger", "sadness", "fear", "disgust"]
|
| 236 |
-
|
| 237 |
-
if emocao_dominante in positivas:
|
| 238 |
-
detalhes["polaridade"] = "positiva"
|
| 239 |
-
elif emocao_dominante in negativas:
|
| 240 |
-
detalhes["polaridade"] = "negativa"
|
| 241 |
-
|
| 242 |
-
self.memoria_emocional.adicionar_interacao(mensagem_limpa, emocao_dominante, confianca_primaria)
|
| 243 |
-
|
| 244 |
-
resultado_cache = (emocao_dominante, confianca_primaria, detalhes)
|
| 245 |
-
EMOTION_CACHE[cache_key] = resultado_cache
|
| 246 |
-
|
| 247 |
-
logger.debug(f"🎭 Emoção: {emocao_dominante} ({confianca_primaria:.2f})")
|
| 248 |
-
return resultado_cache
|
| 249 |
-
|
| 250 |
except Exception as e:
|
| 251 |
-
logger.warning(f"
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
if
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
return
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
if
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
if
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
if tempo_desde_ultima < 30 and random.random() < 0.7:
|
| 315 |
-
novo_humor = humor_anterior
|
| 316 |
-
|
| 317 |
-
if novo_humor != humor_anterior:
|
| 318 |
-
razao = (f"Emoção: {emocao} ({confianca:.2f}), "
|
| 319 |
-
f"Tom: {tom_usuario}, Nível: {self.nivel_transicao:.1f}/3")
|
| 320 |
-
|
| 321 |
-
self.db.salvar_transicao_humor(
|
| 322 |
-
self.usuario,
|
| 323 |
-
humor_anterior,
|
| 324 |
-
novo_humor,
|
| 325 |
-
razao,
|
| 326 |
-
intensidade=confianca
|
| 327 |
-
)
|
| 328 |
-
|
| 329 |
-
self.ultima_transicao = time.time()
|
| 330 |
-
logger.info(f"🎭 Transição: {humor_anterior} → {novo_humor} ({self.nivel_transicao:.1f}/3)")
|
| 331 |
-
|
| 332 |
-
self.humor_atual = novo_humor
|
| 333 |
-
return novo_humor
|
| 334 |
-
|
| 335 |
-
def _calcular_taxa_transicao(self, confianca: float, tom_usuario: str,
|
| 336 |
-
usuario_privilegiado: bool, emocao: str) -> float:
|
| 337 |
-
taxa_base = 0.5
|
| 338 |
-
|
| 339 |
-
if confianca > 0.8:
|
| 340 |
-
taxa_base += 0.3
|
| 341 |
-
elif confianca > 0.6:
|
| 342 |
-
taxa_base += 0.15
|
| 343 |
-
|
| 344 |
-
if tom_usuario == "rude":
|
| 345 |
-
taxa_base += 0.4
|
| 346 |
-
elif tom_usuario == "formal" and not usuario_privilegiado:
|
| 347 |
-
taxa_base -= 0.2
|
| 348 |
-
|
| 349 |
-
if usuario_privilegiado:
|
| 350 |
-
taxa_base += 0.2
|
| 351 |
-
|
| 352 |
-
taxa_base *= (1 + self.memoria_emocional.volatilidade)
|
| 353 |
-
|
| 354 |
-
return max(0.1, min(1.0, taxa_base))
|
| 355 |
-
|
| 356 |
-
# === DETECÇÃO DE TOM ===
|
| 357 |
-
def detectar_tom_usuario(self, mensagem: str) -> Tuple[str, float]:
|
| 358 |
-
mensagem_lower = mensagem.lower()
|
| 359 |
-
pontos = {"formal": 0, "rude": 0, "informal": 0, "neutro": 0}
|
| 360 |
-
|
| 361 |
-
formais = ['senhor', 'senhora', 'doutor', 'atenciosamente']
|
| 362 |
-
for palavra in formais:
|
| 363 |
-
if palavra in mensagem_lower:
|
| 364 |
-
pontos["formal"] += 2
|
| 365 |
-
|
| 366 |
-
rudes = ['burro', 'idiota', 'caralho', 'porra', 'merda']
|
| 367 |
-
for palavra in rudes:
|
| 368 |
-
if palavra in mensagem_lower:
|
| 369 |
-
pontos["rude"] += 3
|
| 370 |
-
|
| 371 |
-
if len(re.findall(r'[A-Z]{3,}', mensagem)) >= 2:
|
| 372 |
-
pontos["rude"] += 2
|
| 373 |
-
|
| 374 |
-
if mensagem.count('!') >= 3 or mensagem.count('?') >= 3:
|
| 375 |
-
pontos["informal"] += 2
|
| 376 |
-
|
| 377 |
-
girias = ['puto', 'mano', 'kota', 'fixe', 'bué']
|
| 378 |
-
if any(g in mensagem_lower for g in girias):
|
| 379 |
-
pontos["informal"] += 2
|
| 380 |
-
|
| 381 |
-
tom = max(pontos, key=pontos.get)
|
| 382 |
-
max_pontos = pontos[tom]
|
| 383 |
-
intensidade = min(max_pontos / 10, 1.0)
|
| 384 |
-
|
| 385 |
-
if max_pontos < 2:
|
| 386 |
-
tom = "neutro"
|
| 387 |
-
intensidade = 0.3
|
| 388 |
-
|
| 389 |
-
return (tom, intensidade)
|
| 390 |
-
|
| 391 |
-
# === DETECÇÃO DE MODO ===
|
| 392 |
-
def detectar_modo_resposta(self, mensagem: str, tom_usuario: str,
|
| 393 |
-
usuario_privilegiado: bool = False) -> str:
|
| 394 |
-
mensagem_lower = mensagem.lower()
|
| 395 |
-
|
| 396 |
-
if usuario_privilegiado:
|
| 397 |
-
for padrao in PADROES_COMANDO_PRIVILEGIADO:
|
| 398 |
-
if re.search(padrao, mensagem_lower, re.IGNORECASE):
|
| 399 |
-
return "tecnico_formal"
|
| 400 |
-
if tom_usuario == "formal":
|
| 401 |
-
return "tecnico_formal"
|
| 402 |
-
|
| 403 |
-
if not usuario_privilegiado:
|
| 404 |
-
for padrao in PADROES_COMANDO_COMUM:
|
| 405 |
-
if re.search(padrao, mensagem_lower, re.IGNORECASE):
|
| 406 |
-
return "agressivo_direto"
|
| 407 |
-
|
| 408 |
-
padroes_tecnicos = [
|
| 409 |
-
r'\b(explicar|como funciona|definição)\b',
|
| 410 |
-
r'\b(algoritmo|código|programação)\b'
|
| 411 |
-
]
|
| 412 |
-
for padrao in padroes_tecnicos:
|
| 413 |
-
if re.search(padrao, mensagem_lower):
|
| 414 |
-
return "tecnico_formal"
|
| 415 |
-
|
| 416 |
-
if tom_usuario == "rude":
|
| 417 |
-
return "agressivo_direto"
|
| 418 |
-
|
| 419 |
-
if '?' in mensagem and len(mensagem) > 100:
|
| 420 |
-
return "filosofico_profundo"
|
| 421 |
-
|
| 422 |
-
palavras_romanticas = ['amor', 'paixão', 'gosto de ti']
|
| 423 |
-
if any(p in mensagem_lower for p in palavras_romanticas):
|
| 424 |
-
return "romantico_carinhoso"
|
| 425 |
-
|
| 426 |
-
return "normal_ironico"
|
| 427 |
-
|
| 428 |
-
# === CORREÇÃO: ANÁLISE COMPLETA ADAPTADA AO PAYLOAD ===
|
| 429 |
-
def analisar_intencao_e_normalizar(self, mensagem: str, historico: List[Dict],
|
| 430 |
-
mensagem_citada: str = None,
|
| 431 |
-
reply_info: Dict = None,
|
| 432 |
-
tipo_mensagem: str = "texto",
|
| 433 |
-
usuario_nome: str = None,
|
| 434 |
-
numero_usuario: str = None,
|
| 435 |
-
grupo_id: str = None,
|
| 436 |
-
grupo_nome: str = None) -> Dict[str, Any]:
|
| 437 |
-
"""
|
| 438 |
-
Análise completa adaptada ao payload do index.js
|
| 439 |
-
|
| 440 |
-
Args:
|
| 441 |
-
mensagem: Texto da mensagem
|
| 442 |
-
historico: Histórico de conversa
|
| 443 |
-
mensagem_citada: Mensagem citada do payload (formato completo)
|
| 444 |
-
reply_info: Informações de reply do payload
|
| 445 |
-
tipo_mensagem: 'audio' ou 'texto'
|
| 446 |
-
usuario_nome: Nome do usuário
|
| 447 |
-
numero_usuario: Número do usuário
|
| 448 |
-
grupo_id: ID do grupo (se aplicável)
|
| 449 |
-
grupo_nome: Nome do grupo (se aplicável)
|
| 450 |
-
"""
|
| 451 |
if not isinstance(mensagem, str):
|
| 452 |
mensagem = str(mensagem)
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
if
|
| 458 |
-
|
| 459 |
-
if
|
| 460 |
-
|
| 461 |
-
if
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
#
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
usuario_privilegiado = self.db.is_usuario_privilegiado(self.numero_usuario)
|
| 475 |
-
|
| 476 |
-
# Detecção de emoção (considera tipo de mensagem)
|
| 477 |
-
emocao, confianca, detalhes_emocao = self.detectar_emocao_avancada(mensagem)
|
| 478 |
-
|
| 479 |
-
# Ajuste para mensagens de áudio
|
| 480 |
-
if tipo_mensagem == "audio" and confianca < 0.6:
|
| 481 |
-
# Áudios podem ter transição menos precisa
|
| 482 |
-
confianca = max(confianca, 0.5)
|
| 483 |
-
|
| 484 |
-
tom_usuario, intensidade_tom = self.detectar_tom_usuario(mensagem)
|
| 485 |
-
humor_atualizado = self.atualizar_humor_gradual(emocao, confianca, tom_usuario, usuario_privilegiado)
|
| 486 |
-
modo_resposta = self.detectar_modo_resposta(mensagem, tom_usuario, usuario_privilegiado)
|
| 487 |
-
self.modo_resposta_atual = modo_resposta
|
| 488 |
-
|
| 489 |
-
# CORREÇÃO: ANÁLISE DE REPLY COM INFO DO PAYLOAD - MELHORADA
|
| 490 |
-
reply_analysis = self._analisar_reply_context_completo(
|
| 491 |
-
mensagem_citada=mensagem_citada,
|
| 492 |
-
reply_info=reply_info,
|
| 493 |
-
historico=historico
|
| 494 |
-
)
|
| 495 |
-
|
| 496 |
-
usar_nome = random.random() < config.USAR_NOME_PROBABILIDADE
|
| 497 |
-
contexto_ajustado = self.substituir_termos_aprendidos(mensagem.lower())
|
| 498 |
-
|
| 499 |
-
comando_detectado = False
|
| 500 |
-
resposta_comando = None
|
| 501 |
-
|
| 502 |
-
if usuario_privilegiado:
|
| 503 |
-
for padrao in PADROES_COMANDO_PRIVILEGIADO:
|
| 504 |
-
if re.search(padrao, mensagem, re.IGNORECASE):
|
| 505 |
-
comando_detectado = True
|
| 506 |
-
resposta_comando = "comando_privilegiado"
|
| 507 |
-
break
|
| 508 |
-
else:
|
| 509 |
-
for padrao in PADROES_COMANDO_COMUM:
|
| 510 |
-
if re.search(padrao, mensagem, re.IGNORECASE):
|
| 511 |
-
comando_detectado = True
|
| 512 |
-
resposta_comando = "comando_negado"
|
| 513 |
-
break
|
| 514 |
-
|
| 515 |
-
# TENDÊNCIA EMOCIONAL COM BASE NO HISTÓRICO
|
| 516 |
-
if self.historico_mensagens:
|
| 517 |
-
ultimas_emoc = []
|
| 518 |
-
for msg in self.historico_mensagens[-5:]:
|
| 519 |
-
if msg.get('role') == 'user':
|
| 520 |
-
emo_simples = self._detectar_emocao_simples(msg.get('content', ''))
|
| 521 |
-
ultimas_emoc.append(emo_simples)
|
| 522 |
-
|
| 523 |
-
if ultimas_emoc:
|
| 524 |
-
from collections import Counter
|
| 525 |
-
contagem = Counter(ultimas_emoc)
|
| 526 |
-
tendencia_historico = contagem.most_common(1)[0][0]
|
| 527 |
-
else:
|
| 528 |
-
tendencia_historico = "neutral"
|
| 529 |
-
else:
|
| 530 |
-
tendencia_historico = "neutral"
|
| 531 |
-
|
| 532 |
-
# CORREÇÃO: EXTRAI CONTEÚDO DA MENSAGEM CITADA PARA CONTEXTO
|
| 533 |
-
conteudo_citado_limpo = ""
|
| 534 |
-
if mensagem_citada:
|
| 535 |
-
conteudo_citado_limpo = self._extrair_conteudo_citado_limpo(mensagem_citada)
|
| 536 |
-
|
| 537 |
return {
|
| 538 |
-
"
|
| 539 |
-
"
|
| 540 |
-
"
|
| 541 |
-
"
|
| 542 |
-
"
|
| 543 |
-
"
|
| 544 |
-
"
|
| 545 |
-
"nivel_transicao": self.nivel_transicao,
|
| 546 |
-
"humor_alvo": self.humor_alvo,
|
| 547 |
"usar_nome": usar_nome,
|
| 548 |
-
"
|
| 549 |
-
"usuario_privilegiado": usuario_privilegiado,
|
| 550 |
-
"comando_detectado": comando_detectado,
|
| 551 |
-
"resposta_comando": resposta_comando,
|
| 552 |
-
"reply_info": reply_analysis,
|
| 553 |
-
"tendencia_emocional": self.memoria_emocional.tendencia_emocional,
|
| 554 |
-
"volatilidade_usuario": self.memoria_emocional.volatilidade,
|
| 555 |
-
"tipo_mensagem": tipo_mensagem,
|
| 556 |
-
"nome_usuario": self.nome_usuario,
|
| 557 |
-
"numero_usuario": self.numero_usuario,
|
| 558 |
-
"tipo_conversa": self.tipo_conversa,
|
| 559 |
-
"tendencia_historico": tendencia_historico,
|
| 560 |
-
"tamanho_historico": len(self.historico_mensagens),
|
| 561 |
-
"mensagem_citada_limpa": conteudo_citado_limpo, # CORREÇÃO: Adicionado
|
| 562 |
-
"grupo_id": grupo_id,
|
| 563 |
-
"grupo_nome": grupo_nome,
|
| 564 |
-
"eh_resposta": reply_analysis.get("is_reply", False),
|
| 565 |
-
"eh_resposta_ao_bot": reply_analysis.get("reply_to_bot", False)
|
| 566 |
}
|
| 567 |
-
|
| 568 |
-
def
|
| 569 |
-
"""
|
| 570 |
-
if not
|
| 571 |
-
return
|
| 572 |
-
|
| 573 |
-
|
| 574 |
-
|
| 575 |
-
return mensagem_citada[23:].strip()
|
| 576 |
-
elif mensagem_citada.startswith("[") and ":" in mensagem_citada:
|
| 577 |
-
partes = mensagem_citada.split(":", 1)
|
| 578 |
-
if len(partes) > 1:
|
| 579 |
-
return partes[1].strip()
|
| 580 |
-
|
| 581 |
-
return mensagem_citada.strip()
|
| 582 |
-
|
| 583 |
-
def _detectar_emocao_simples(self, texto: str) -> str:
|
| 584 |
-
"""Detecção simples de emoção para análise de tendência"""
|
| 585 |
-
texto = texto.lower()
|
| 586 |
-
positivas = ['bom', 'ótimo', 'feliz', 'adorei', 'love', 'obrigado']
|
| 587 |
-
negativas = ['ruim', 'péssimo', 'triste', 'ódio', 'raiva', 'merda']
|
| 588 |
-
|
| 589 |
-
pos = sum(1 for p in positivas if p in texto)
|
| 590 |
-
neg = sum(1 for n in negativas if n in texto)
|
| 591 |
-
|
| 592 |
-
if pos > neg: return "joy"
|
| 593 |
-
elif neg > pos: return "anger"
|
| 594 |
-
else: return "neutral"
|
| 595 |
-
|
| 596 |
-
# CORREÇÃO: ANÁLISE COMPLETA DO CONTEXTO DE REPLY
|
| 597 |
-
def _analisar_reply_context_completo(self, mensagem_citada: str, reply_info: Dict,
|
| 598 |
-
historico: List[Dict]) -> Dict[str, Any]:
|
| 599 |
-
"""Análise completa do contexto de reply"""
|
| 600 |
-
# Se tivermos reply_info do payload, usamos primeiro
|
| 601 |
-
if reply_info:
|
| 602 |
-
texto_citado = reply_info.get('texto_citado_completo', '')
|
| 603 |
-
if not texto_citado and mensagem_citada:
|
| 604 |
-
# Fallback: extrai da mensagem_citada
|
| 605 |
-
texto_citado = self._extrair_conteudo_citado_limpo(mensagem_citada)
|
| 606 |
-
|
| 607 |
-
return {
|
| 608 |
-
"is_reply": True,
|
| 609 |
-
"reply_to_bot": reply_info.get('reply_to_bot', False),
|
| 610 |
-
"confianca": 0.9 if reply_info.get('reply_to_bot') else 0.7,
|
| 611 |
-
"tipo": "reply_to_bot" if reply_info.get('reply_to_bot') else "reply_to_user",
|
| 612 |
-
"usuario_citado_nome": reply_info.get('usuario_citado_nome', 'desconhecido'),
|
| 613 |
-
"usuario_citado_numero": reply_info.get('usuario_citado_numero', ''),
|
| 614 |
-
"texto_citado_completo": texto_citado,
|
| 615 |
-
"source": "payload_reply_info"
|
| 616 |
-
}
|
| 617 |
-
|
| 618 |
-
# Se não tiver reply_info, analisa a mensagem_citada
|
| 619 |
-
elif mensagem_citada:
|
| 620 |
-
return self._analisar_reply_context_auto(mensagem_citada, historico)
|
| 621 |
-
|
| 622 |
-
# Se não houver nenhum contexto de reply
|
| 623 |
-
return {
|
| 624 |
-
"is_reply": False,
|
| 625 |
-
"reply_to_bot": False,
|
| 626 |
-
"confianca": 0.0,
|
| 627 |
-
"tipo": "nenhum",
|
| 628 |
-
"usuario_citado_nome": "",
|
| 629 |
-
"usuario_citado_numero": "",
|
| 630 |
-
"texto_citado_completo": "",
|
| 631 |
-
"source": "nenhum"
|
| 632 |
-
}
|
| 633 |
-
|
| 634 |
-
def _analisar_reply_context_auto(self, mensagem_citada: str, historico: List[Dict]) -> Dict[str, Any]:
|
| 635 |
-
"""Análise automática do contexto de reply"""
|
| 636 |
-
if not mensagem_citada:
|
| 637 |
-
return {"is_reply": False, "reply_to_bot": False, "confianca": 0.0, "tipo": "nenhum"}
|
| 638 |
-
|
| 639 |
-
reply_to_bot = False
|
| 640 |
-
confianca = 0.5
|
| 641 |
-
|
| 642 |
-
# Extrai conteúdo limpo
|
| 643 |
-
conteudo_citado = self._extrair_conteudo_citado_limpo(mensagem_citada)
|
| 644 |
-
|
| 645 |
-
if mensagem_citada.startswith("[Respondendo à Akira:"):
|
| 646 |
-
reply_to_bot = True
|
| 647 |
-
confianca = 1.0
|
| 648 |
-
elif historico and len(historico) > 0:
|
| 649 |
-
# Verifica se está respondendo à Akira no histórico
|
| 650 |
-
for msg in historico[-3:]:
|
| 651 |
-
if msg.get("role") == "assistant":
|
| 652 |
-
conteudo_bot = msg.get("content", "").lower()
|
| 653 |
-
citada_lower = conteudo_citado.lower()
|
| 654 |
-
palavras_comuns = len(set(conteudo_bot.split()) & set(citada_lower.split()))
|
| 655 |
-
if palavras_comuns >= 3:
|
| 656 |
-
reply_to_bot = True
|
| 657 |
-
confianca = min(0.9, palavras_comuns / 10)
|
| 658 |
-
break
|
| 659 |
-
|
| 660 |
-
# Determina usuário citado (se não for reply ao bot)
|
| 661 |
-
usuario_citado_nome = "desconhecido"
|
| 662 |
-
usuario_citado_numero = ""
|
| 663 |
-
|
| 664 |
-
if not reply_to_bot and mensagem_citada.startswith("[") and ":" in mensagem_citada:
|
| 665 |
-
# Tenta extrair nome do usuário citado
|
| 666 |
-
partes = mensagem_citada.split(":", 1)
|
| 667 |
-
if len(partes) > 0:
|
| 668 |
-
nome_part = partes[0].strip("[]")
|
| 669 |
-
if "disse" in nome_part:
|
| 670 |
-
usuario_citado_nome = nome_part.replace("disse", "").strip()
|
| 671 |
-
|
| 672 |
-
return {
|
| 673 |
-
"is_reply": True,
|
| 674 |
-
"reply_to_bot": reply_to_bot,
|
| 675 |
-
"confianca": confianca,
|
| 676 |
-
"tipo": "reply_to_bot" if reply_to_bot else "reply_to_user",
|
| 677 |
-
"usuario_citado_nome": usuario_citado_nome,
|
| 678 |
-
"usuario_citado_numero": usuario_citado_numero,
|
| 679 |
-
"texto_citado_completo": conteudo_citado,
|
| 680 |
-
"source": "auto_analysis"
|
| 681 |
-
}
|
| 682 |
-
|
| 683 |
-
def substituir_termos_aprendidos(self, texto: str) -> str:
|
| 684 |
-
for giria, dados in self.termo_contexto.items():
|
| 685 |
-
if giria in texto:
|
| 686 |
-
significado = dados.get("significado", giria)
|
| 687 |
-
texto = texto.replace(giria, f"{giria} ({significado})")
|
| 688 |
-
return texto
|
| 689 |
-
|
| 690 |
-
# === MÉTODO CRUCIAL ===
|
| 691 |
def obter_historico_para_llm(self) -> List[Dict]:
|
| 692 |
-
"""
|
| 693 |
-
|
| 694 |
-
|
| 695 |
-
for
|
| 696 |
-
|
| 697 |
-
|
| 698 |
-
|
| 699 |
-
|
| 700 |
-
|
| 701 |
-
|
| 702 |
-
|
| 703 |
-
|
| 704 |
-
|
| 705 |
-
|
| 706 |
-
|
| 707 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 708 |
try:
|
| 709 |
-
|
| 710 |
-
|
| 711 |
-
# Adiciona ao histórico com timestamp
|
| 712 |
-
self.historico_mensagens.append({
|
| 713 |
-
"role": "user",
|
| 714 |
-
"content": mensagem,
|
| 715 |
-
"timestamp": timestamp_atual,
|
| 716 |
-
"is_reply": is_reply,
|
| 717 |
-
"tipo_mensagem": tipo_mensagem,
|
| 718 |
-
"reply_info": reply_info
|
| 719 |
-
})
|
| 720 |
-
self.historico_mensagens.append({
|
| 721 |
-
"role": "assistant",
|
| 722 |
-
"content": resposta,
|
| 723 |
-
"timestamp": timestamp_atual,
|
| 724 |
-
"is_reply": is_reply,
|
| 725 |
-
"reply_to_user": is_reply and not reply_to_bot
|
| 726 |
-
})
|
| 727 |
-
|
| 728 |
-
# Mantém apenas últimas 20 mensagens
|
| 729 |
-
if len(self.historico_mensagens) > 20:
|
| 730 |
-
self.historico_mensagens = self.historico_mensagens[-20:]
|
| 731 |
-
|
| 732 |
-
# Salva última mensagem da Akira
|
| 733 |
-
self.ultima_mensagem_akira = resposta
|
| 734 |
-
|
| 735 |
-
# Prepara histórico para salvar
|
| 736 |
-
historico_para_salvar = []
|
| 737 |
-
for msg in self.historico_mensagens:
|
| 738 |
-
historico_para_salvar.append({
|
| 739 |
-
"role": msg.get("role"),
|
| 740 |
-
"content": msg.get("content", "")[:500],
|
| 741 |
-
"timestamp": msg.get("timestamp")
|
| 742 |
-
})
|
| 743 |
-
|
| 744 |
-
# Atualiza banco
|
| 745 |
self.db.salvar_contexto(
|
| 746 |
-
|
| 747 |
-
historico=
|
| 748 |
-
|
| 749 |
-
|
| 750 |
-
|
| 751 |
-
|
| 752 |
-
emocao_tendencia=self.memoria_emocional.tendencia_emocional,
|
| 753 |
-
volatilidade=self.memoria_emocional.volatilidade,
|
| 754 |
-
nome_usuario=self.nome_usuario
|
| 755 |
)
|
| 756 |
-
|
| 757 |
-
logger.debug(f"✅ Contexto atualizado: {len(self.historico_mensagens)} msgs | reply: {is_reply} | tipo: {self.tipo_conversa}")
|
| 758 |
-
|
| 759 |
except Exception as e:
|
| 760 |
-
logger.error(f"
|
| 761 |
-
|
| 762 |
-
|
| 763 |
-
|
| 764 |
-
|
| 765 |
-
|
| 766 |
-
|
| 767 |
-
|
| 768 |
-
|
| 769 |
-
|
| 770 |
-
|
| 771 |
-
|
| 772 |
-
|
| 773 |
-
|
| 774 |
-
|
| 775 |
-
|
| 776 |
-
|
| 777 |
-
|
| 778 |
-
|
| 779 |
-
|
| 780 |
-
|
| 781 |
-
|
| 782 |
-
|
| 783 |
-
|
| 784 |
-
"
|
| 785 |
-
|
| 786 |
-
|
| 787 |
-
|
| 788 |
-
|
| 789 |
-
|
| 790 |
-
}
|
| 791 |
-
|
| 792 |
-
# === MÉTODOS PARA INTEGRAÇÃO ===
|
| 793 |
-
def processar_payload_completo(self, payload: Dict) -> Dict[str, Any]:
|
| 794 |
-
"""Processa payload completo do index.js e retorna análise"""
|
| 795 |
try:
|
| 796 |
-
|
| 797 |
-
|
| 798 |
-
mensagem_citada = payload.get('mensagem_citada', '')
|
| 799 |
-
reply_info = payload.get('reply_info')
|
| 800 |
-
tipo_mensagem = payload.get('tipo_mensagem', 'texto')
|
| 801 |
-
usuario_nome = payload.get('usuario', 'Anônimo')
|
| 802 |
-
numero_usuario = payload.get('numero', '')
|
| 803 |
-
tipo_conversa = payload.get('tipo_conversa', 'pv')
|
| 804 |
-
grupo_id = payload.get('grupo_id', '')
|
| 805 |
-
grupo_nome = payload.get('grupo_nome', '')
|
| 806 |
-
|
| 807 |
-
# Atualiza informações do contexto
|
| 808 |
-
self.numero_usuario = numero_usuario or self.numero_usuario
|
| 809 |
-
self.nome_usuario = usuario_nome or self.nome_usuario
|
| 810 |
-
self.grupo_id = grupo_id or self.grupo_id
|
| 811 |
-
self.grupo_nome = grupo_nome or self.grupo_nome
|
| 812 |
-
|
| 813 |
-
# Determina tipo de conversa
|
| 814 |
-
if grupo_id or "@g.us" in str(numero_usuario or "") or tipo_conversa == "grupo":
|
| 815 |
-
self.tipo_conversa = "grupo"
|
| 816 |
-
self.is_grupo = True
|
| 817 |
-
self.tipo = "GRUPO"
|
| 818 |
-
else:
|
| 819 |
-
self.tipo_conversa = "pv"
|
| 820 |
-
self.is_grupo = False
|
| 821 |
-
self.tipo = "PRIVADO"
|
| 822 |
-
|
| 823 |
-
# Obtém histórico
|
| 824 |
-
historico = self.obter_historico_para_llm()
|
| 825 |
-
|
| 826 |
-
# Realiza análise completa
|
| 827 |
-
analise = self.analisar_intencao_e_normalizar(
|
| 828 |
-
mensagem=mensagem,
|
| 829 |
-
historico=historico,
|
| 830 |
-
mensagem_citada=mensagem_citada,
|
| 831 |
-
reply_info=reply_info,
|
| 832 |
-
tipo_mensagem=tipo_mensagem,
|
| 833 |
-
usuario_nome=usuario_nome,
|
| 834 |
-
numero_usuario=numero_usuario,
|
| 835 |
-
grupo_id=grupo_id,
|
| 836 |
-
grupo_nome=grupo_nome
|
| 837 |
-
)
|
| 838 |
-
|
| 839 |
-
# Adiciona informações adicionais
|
| 840 |
-
analise['payload_info'] = {
|
| 841 |
-
'tipo_conversa': tipo_conversa,
|
| 842 |
-
'tipo_mensagem': tipo_mensagem,
|
| 843 |
-
'tem_reply_info': reply_info is not None,
|
| 844 |
-
'tem_mensagem_citada': bool(mensagem_citada)
|
| 845 |
-
}
|
| 846 |
-
|
| 847 |
-
logger.info(f"📊 Análise completa: usuario={usuario_nome}, humor={analise.get('humor_atualizado')}, "
|
| 848 |
-
f"reply={analise.get('eh_resposta', False)}, ao_bot={analise.get('eh_resposta_ao_bot', False)}")
|
| 849 |
-
|
| 850 |
-
return analise
|
| 851 |
-
|
| 852 |
except Exception as e:
|
| 853 |
-
logger.
|
| 854 |
-
|
| 855 |
-
|
| 856 |
-
|
| 857 |
-
|
| 858 |
-
|
| 859 |
-
|
| 860 |
-
|
| 861 |
-
|
| 862 |
-
|
| 863 |
-
|
| 864 |
-
|
| 865 |
-
|
| 866 |
-
|
| 867 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# modules/contexto.py
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
import logging
|
| 3 |
import re
|
| 4 |
import random
|
| 5 |
import time
|
| 6 |
+
import sqlite3
|
| 7 |
import json
|
| 8 |
from typing import Optional, List, Dict, Tuple, Any
|
|
|
|
| 9 |
import modules.config as config
|
| 10 |
from .database import Database
|
| 11 |
+
from .treinamento import Treinamento
|
| 12 |
+
|
| 13 |
+
try:
|
| 14 |
+
from sentence_transformers import SentenceTransformer
|
| 15 |
+
except Exception as e:
|
| 16 |
+
logging.warning(f"sentence_transformers não disponível: {e}")
|
| 17 |
+
SentenceTransformer = None
|
| 18 |
+
|
| 19 |
+
try:
|
| 20 |
+
import psutil
|
| 21 |
+
except Exception:
|
| 22 |
+
psutil = None
|
| 23 |
+
|
| 24 |
+
try:
|
| 25 |
+
import structlog
|
| 26 |
+
except Exception:
|
| 27 |
+
structlog = None
|
| 28 |
|
| 29 |
logger = logging.getLogger(__name__)
|
| 30 |
logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s')
|
| 31 |
|
| 32 |
+
if structlog:
|
| 33 |
+
structlog.configure(
|
| 34 |
+
processors=[
|
| 35 |
+
structlog.processors.TimeStamper(fmt="iso"),
|
| 36 |
+
structlog.stdlib.add_log_level,
|
| 37 |
+
structlog.processors.JSONRenderer()
|
| 38 |
+
],
|
| 39 |
+
context_class=dict,
|
| 40 |
+
logger_factory=structlog.stdlib.LoggerFactory(),
|
| 41 |
+
wrapper_class=structlog.stdlib.BoundLogger,
|
| 42 |
)
|
| 43 |
+
|
| 44 |
+
# Palavras para análise de sentimento heurística
|
| 45 |
+
PALAVRAS_POSITIVAS = ['bom', 'ótimo', 'incrível', 'feliz', 'adorei', 'top', 'fixe', 'bué', 'show', 'legal', 'bacana']
|
| 46 |
+
PALAVRAS_NEGATIVAS = ['ruim', 'péssimo', 'triste', 'ódio', 'raiva', 'chateado', 'merda', 'porra', 'odeio']
|
| 47 |
+
|
| 48 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
class Contexto:
|
| 50 |
+
"""
|
| 51 |
+
Classe para gerenciar o contexto da conversa, análise de intenções e aprendizado
|
| 52 |
+
dinâmico de termos regionais/gírias para cada usuário.
|
| 53 |
+
"""
|
| 54 |
+
def __init__(self, db: Database, usuario: Optional[str] = None):
|
| 55 |
self.db = db
|
| 56 |
+
self.usuario = usuario
|
| 57 |
+
self.model: Optional[SentenceTransformer] = None
|
| 58 |
+
self.embeddings: Optional[Dict[str, Any]] = None
|
| 59 |
+
self._treinador: Optional[Treinamento] = None
|
| 60 |
+
|
| 61 |
+
# Estado de conversa
|
| 62 |
+
self.emocao_atual = "neutra"
|
| 63 |
+
self.espírito_crítico = False
|
| 64 |
+
self.base_conhecimento = {}
|
| 65 |
+
|
| 66 |
+
# Garante que termo_contexto seja sempre um dicionário
|
| 67 |
+
self.termo_contexto: Dict[str, Dict] = {}
|
| 68 |
+
self.atualizar_aprendizados_do_banco()
|
| 69 |
+
|
| 70 |
+
logger.info("Inicializando Contexto (com NLP avançado, aprendizado de gírias e emoções) ...")
|
| 71 |
+
|
| 72 |
+
# Cache para termos regionais e gírias
|
| 73 |
+
self.cache_girias: Dict[str, Any] = {}
|
| 74 |
+
|
| 75 |
+
def atualizar_aprendizados_do_banco(self):
|
| 76 |
+
"""Carrega todos os dados de aprendizado persistentes do banco."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
try:
|
| 78 |
+
termos_aprendidos = self.db.recuperar_girias_usuario(self.usuario) if self.usuario else []
|
| 79 |
+
self.termo_contexto = {
|
| 80 |
+
termo['giria']: {"significado": termo['significado'], "frequencia": termo['frequencia']}
|
| 81 |
+
for termo in termos_aprendidos
|
| 82 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
except Exception as e:
|
| 84 |
+
logger.warning(f"Falha ao carregar termos/gírias do DB: {e}")
|
| 85 |
+
self.termo_contexto = {}
|
| 86 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
try:
|
| 88 |
+
emocao_salva = self.db.recuperar_aprendizado_detalhado(self.usuario, "emocao_atual") if self.usuario else None
|
| 89 |
+
if emocao_salva:
|
| 90 |
+
emocao_dict = json.loads(emocao_salva)
|
| 91 |
+
if isinstance(emocao_dict, dict) and 'emocao' in emocao_dict:
|
| 92 |
+
self.emocao_atual = emocao_dict['emocao']
|
| 93 |
+
elif isinstance(emocao_salva, str):
|
| 94 |
+
self.emocao_atual = emocao_salva
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
except Exception as e:
|
| 96 |
+
logger.warning(f"Falha ao carregar emoção do DB: {e}")
|
| 97 |
+
|
| 98 |
+
logger.info(f"Aprendizados carregados para {self.usuario}.")
|
| 99 |
+
|
| 100 |
+
@property
|
| 101 |
+
def ton_predominante(self) -> Optional[str]:
|
| 102 |
+
"""Retorna o tom predominante do usuário (acessa o DB)."""
|
| 103 |
+
if self.usuario:
|
| 104 |
+
return self.db.obter_tom_predominante(self.usuario)
|
| 105 |
+
return None
|
| 106 |
+
|
| 107 |
+
def get_or_create_treinador(self, interval_hours: int = 24) -> Treinamento:
|
| 108 |
+
"""Retorna um treinador associado, criando se necessário."""
|
| 109 |
+
if self._treinador is None:
|
| 110 |
+
self._treinador = Treinamento(self.db, contexto=self, interval_hours=interval_hours)
|
| 111 |
+
return self._treinador
|
| 112 |
+
|
| 113 |
+
def _load_model(self):
|
| 114 |
+
"""Carrega o modelo SentenceTransformer sob demanda."""
|
| 115 |
+
if self.model is not None:
|
| 116 |
+
return
|
| 117 |
+
if SentenceTransformer is None:
|
| 118 |
+
logger.warning("SentenceTransformer não instalado")
|
| 119 |
+
return
|
| 120 |
+
try:
|
| 121 |
+
self.model = SentenceTransformer('all-MiniLM-L6-v2')
|
| 122 |
+
logger.info("Modelo SentenceTransformer carregado")
|
| 123 |
+
except Exception as e:
|
| 124 |
+
logger.error(f"Erro ao carregar modelo: {e}")
|
| 125 |
+
self.model = None
|
| 126 |
+
self._check_embeddings()
|
| 127 |
+
|
| 128 |
+
def _check_embeddings(self):
|
| 129 |
+
"""Verifica ou cria embeddings no banco."""
|
| 130 |
+
if self.model and not self.embeddings:
|
| 131 |
+
self.embeddings = {"conhecimento_base": "placeholder"}
|
| 132 |
+
|
| 133 |
+
def analisar_emocoes_mensagem(self, mensagem: str) -> Dict[str, Any]:
|
| 134 |
+
"""Analisa sentimento e emoção da mensagem (heurística)."""
|
| 135 |
+
mensagem_lower = mensagem.strip().lower()
|
| 136 |
+
pos_count = sum(mensagem_lower.count(w) for w in PALAVRAS_POSITIVAS)
|
| 137 |
+
neg_count = sum(mensagem_lower.count(w) for w in PALAVRAS_NEGATIVAS)
|
| 138 |
+
|
| 139 |
+
sentimento = "neutro"
|
| 140 |
+
if pos_count > neg_count:
|
| 141 |
+
sentimento = "positivo"
|
| 142 |
+
elif neg_count > pos_count:
|
| 143 |
+
sentimento = "negativo"
|
| 144 |
+
|
| 145 |
+
emocao_predominante = "alegria" if sentimento == "positivo" else "frustração" if sentimento == "negativo" else "neutra"
|
| 146 |
+
self.emocao_atual = emocao_predominante
|
| 147 |
+
|
| 148 |
+
return {
|
| 149 |
+
"sentimento_detectado": sentimento,
|
| 150 |
+
"emocao_predominante": emocao_predominante,
|
| 151 |
+
"intensidade_positiva": pos_count,
|
| 152 |
+
"intensidade_negativa": neg_count,
|
| 153 |
+
"tom_sugerido": "casual" if sentimento != "neutro" else "neutro"
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
def analisar_intencao_e_normalizar(self, mensagem: str, historico: List[Tuple[str, str]]) -> Dict[str, Any]:
|
| 157 |
+
"""Analisa intenção, normaliza e detecta estilo."""
|
| 158 |
+
self._load_model()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 159 |
if not isinstance(mensagem, str):
|
| 160 |
mensagem = str(mensagem)
|
| 161 |
+
mensagem_lower = mensagem.strip().lower()
|
| 162 |
+
|
| 163 |
+
# Intenção
|
| 164 |
+
intencao = "pergunta"
|
| 165 |
+
if '?' not in mensagem_lower and 'porquê' not in mensagem_lower and 'porque' not in mensagem_lower:
|
| 166 |
+
intencao = "afirmacao"
|
| 167 |
+
if any(w in mensagem_lower for w in ['ola', 'oi', 'bom dia', 'boa tarde', 'boa noite', 'como vai']):
|
| 168 |
+
intencao = "saudacao"
|
| 169 |
+
if any(w in mensagem_lower for w in ['tchau', 'ate mais', 'adeus', 'fim', 'parar']):
|
| 170 |
+
intencao = "despedida"
|
| 171 |
+
|
| 172 |
+
# Sentimento
|
| 173 |
+
analise_emocional = self.analisar_emocoes_mensagem(mensagem_lower)
|
| 174 |
+
|
| 175 |
+
# Estilo
|
| 176 |
+
estilo = "informal"
|
| 177 |
+
if len(re.findall(r'[A-ZÀ-Ÿ]{3,}', mensagem)) >= 2 or re.search(r'\b(Senhor|Doutor|Atenciosamente)\b', mensagem, re.IGNORECASE):
|
| 178 |
+
estilo = "formal"
|
| 179 |
+
|
| 180 |
+
usar_nome = random.random() < getattr(config, 'USAR_NOME_PROBABILIDADE', 0.7)
|
| 181 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 182 |
return {
|
| 183 |
+
"texto_normalizado": mensagem_lower,
|
| 184 |
+
"intencao": intencao,
|
| 185 |
+
"sentimento": analise_emocional['sentimento_detectado'],
|
| 186 |
+
"estilo": estilo,
|
| 187 |
+
"contexto_ajustado": self.substituir_termos_aprendidos(mensagem_lower),
|
| 188 |
+
"ironia": False,
|
| 189 |
+
"meia_frase": False,
|
|
|
|
|
|
|
| 190 |
"usar_nome": usar_nome,
|
| 191 |
+
"emocao": self.emocao_atual
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 192 |
}
|
| 193 |
+
|
| 194 |
+
def obter_historico(self, limite: int = 5) -> List[Tuple[str, str]]:
|
| 195 |
+
"""Recupera histórico do banco."""
|
| 196 |
+
if not self.usuario:
|
| 197 |
+
return []
|
| 198 |
+
raw = self.db.recuperar_mensagens(self.usuario, limite=limite)
|
| 199 |
+
return raw if raw else []
|
| 200 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 201 |
def obter_historico_para_llm(self) -> List[Dict]:
|
| 202 |
+
"""Formato esperado pelo LLMManager.generate()"""
|
| 203 |
+
raw = self.obter_historico(limite=10)
|
| 204 |
+
history = []
|
| 205 |
+
for user_msg, bot_msg in raw:
|
| 206 |
+
history.append({"role": "user", "content": user_msg})
|
| 207 |
+
history.append({"role": "assistant", "content": bot_msg})
|
| 208 |
+
return history
|
| 209 |
+
|
| 210 |
+
def atualizar_contexto(self, mensagem: str, resposta: str, numero: Optional[str] = None):
|
| 211 |
+
"""Salva interação e aprende."""
|
| 212 |
+
usuario = self.usuario or 'anonimo'
|
| 213 |
+
final_numero = numero or self.usuario
|
| 214 |
+
|
| 215 |
+
try:
|
| 216 |
+
self.db.salvar_mensagem(usuario, mensagem, resposta, numero=final_numero)
|
| 217 |
+
historico = self.obter_historico(limite=10)
|
| 218 |
+
self.aprender_do_historico(mensagem, resposta, historico)
|
| 219 |
+
self.salvar_estado_contexto_no_db(final_numero)
|
| 220 |
+
except Exception as e:
|
| 221 |
+
logger.warning(f'Falha ao salvar: {e}')
|
| 222 |
+
|
| 223 |
+
def salvar_estado_contexto_no_db(self, user_key: str):
|
| 224 |
+
"""Persiste estado no DB."""
|
| 225 |
+
termos_json = json.dumps(self.termo_contexto)
|
| 226 |
try:
|
| 227 |
+
self.db.salvar_aprendizado_detalhado(user_key, "emocao_atual", json.dumps({"emocao": self.emocao_atual}))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 228 |
self.db.salvar_contexto(
|
| 229 |
+
user_key=user_key,
|
| 230 |
+
historico="[]",
|
| 231 |
+
emocao_atual=self.emocao_atual,
|
| 232 |
+
termos=termos_json,
|
| 233 |
+
girias=termos_json,
|
| 234 |
+
tom=self.emocao_atual
|
|
|
|
|
|
|
|
|
|
| 235 |
)
|
|
|
|
|
|
|
|
|
|
| 236 |
except Exception as e:
|
| 237 |
+
logger.error(f"Falha ao salvar contexto: {e}")
|
| 238 |
+
|
| 239 |
+
def aprender_do_historico(self, mensagem: str, resposta: str, historico: List[Tuple[str, str]]):
|
| 240 |
+
"""Aprende gírias do histórico."""
|
| 241 |
+
if not self.usuario:
|
| 242 |
+
return
|
| 243 |
+
mensagem_lower = mensagem.lower()
|
| 244 |
+
girias_angolanas_simples = ['ya', 'bué', 'fixe', 'puto', 'kota', 'mwangolé']
|
| 245 |
+
|
| 246 |
+
for giria in girias_angolanas_simples:
|
| 247 |
+
if giria in mensagem_lower:
|
| 248 |
+
try:
|
| 249 |
+
significado = f'termo regional para {giria}'
|
| 250 |
+
self.db.salvar_giria_aprendida(self.usuario, giria, significado, mensagem[:50])
|
| 251 |
+
self.termo_contexto[giria] = {
|
| 252 |
+
"significado": significado,
|
| 253 |
+
"frequencia": self.termo_contexto.get(giria, {}).get("frequencia", 0) + 1
|
| 254 |
+
}
|
| 255 |
+
except Exception as e:
|
| 256 |
+
logger.warning(f"Erro ao salvar gíria: {e}")
|
| 257 |
+
|
| 258 |
+
def substituir_termos_aprendidos(self, mensagem: str) -> str:
|
| 259 |
+
"""Substitui termos aprendidos."""
|
| 260 |
+
for termo, info in self.termo_contexto.items():
|
| 261 |
+
if isinstance(info, dict) and "significado" in info:
|
| 262 |
+
mensagem = re.sub(r'\b' + re.escape(termo) + r'\b', info["significado"], mensagem, flags=re.IGNORECASE)
|
| 263 |
+
return mensagem
|
| 264 |
+
|
| 265 |
+
def obter_aprendizado_detalhado(self, chave: str) -> Optional[Dict]:
|
| 266 |
+
"""Recupera aprendizado detalhado."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 267 |
try:
|
| 268 |
+
raw = self.db.recuperar_aprendizado_detalhado(self.usuario, chave)
|
| 269 |
+
return json.loads(raw) if raw else None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 270 |
except Exception as e:
|
| 271 |
+
logger.warning(f"Erro ao obter aprendizado: {e}")
|
| 272 |
+
return None
|
| 273 |
+
|
| 274 |
+
def obter_emocao_atual(self) -> str:
|
| 275 |
+
return self.emocao_atual
|
| 276 |
+
|
| 277 |
+
def ativar_espírito_crítico(self):
|
| 278 |
+
self.espírito_crítico = True
|
| 279 |
+
|
| 280 |
+
def obter_aprendizados(self) -> Dict[str, Any]:
|
| 281 |
+
"""Retorna todos os aprendizados."""
|
| 282 |
+
return {
|
| 283 |
+
"termos": self.termo_contexto,
|
| 284 |
+
"emocao_preferida": self.emocao_atual,
|
| 285 |
+
"ton_predominante": self.ton_predominante
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
def salvar_conhecimento_base(self, chave: str, valor: Any):
|
| 289 |
+
self.base_conhecimento[chave] = valor
|
| 290 |
+
|
| 291 |
+
def obter_conhecimento_base(self, chave: str) -> Optional[Any]:
|
| 292 |
+
return self.base_conhecimento.get(chave)
|
modules/database.py
CHANGED
|
@@ -1,88 +1,49 @@
|
|
| 1 |
-
# modules/database.py — AKIRA V21 ULTIMATE (Dezembro 2025) - COMPLETO E ADAPTADO
|
| 2 |
"""
|
| 3 |
-
Banco de dados SQLite para Akira IA
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
✅ Isolamento total PV/Grupo com contexto criptográfico
|
| 7 |
-
✅ Transições graduais de humor (3 níveis)
|
| 8 |
-
✅ Memória emocional BERT GoEmotions
|
| 9 |
-
✅ Backup e otimização automática
|
| 10 |
-
✅ ADAPTADO ao payload do index.js
|
| 11 |
-
✅ NOVAS TABELAS: audio_transcricoes, grupo_contexto, embeddings
|
| 12 |
-
✅ NOVAS COLUNAS: tipo_mensagem, reply_info_json
|
| 13 |
-
✅ CORRIGIDO: salvar_aprendizado_detalhado sem **kwargs problemáticos
|
| 14 |
"""
|
|
|
|
| 15 |
import sqlite3
|
| 16 |
import time
|
| 17 |
import os
|
| 18 |
import json
|
| 19 |
-
import pickle
|
| 20 |
-
import hashlib
|
| 21 |
-
from datetime import datetime, timedelta
|
| 22 |
from typing import Optional, List, Dict, Any, Tuple
|
| 23 |
from loguru import logger
|
| 24 |
-
|
| 25 |
|
| 26 |
class Database:
|
| 27 |
-
def __init__(self, db_path: str
|
| 28 |
-
self.db_path = db_path
|
| 29 |
self.max_retries = 5
|
| 30 |
self.retry_delay = 0.1
|
| 31 |
-
|
| 32 |
-
# Cria diretório se necessário
|
| 33 |
-
db_dir = os.path.dirname(self.db_path)
|
| 34 |
-
if db_dir:
|
| 35 |
-
os.makedirs(db_dir, exist_ok=True)
|
| 36 |
-
|
| 37 |
self._init_db()
|
| 38 |
self._ensure_all_columns_and_indexes()
|
| 39 |
-
self._sincronizar_usuarios_privilegiados() # Sincroniza com config
|
| 40 |
-
|
| 41 |
-
logger.info(f"✅ Banco V21 ADAPTADO inicializado: {self.db_path}")
|
| 42 |
|
| 43 |
# ================================================================
|
| 44 |
-
#
|
| 45 |
-
# ================================================================
|
| 46 |
-
|
| 47 |
-
def _gerar_contexto_id(self, numero: str, tipo: str = 'auto') -> str:
|
| 48 |
-
if tipo == 'auto':
|
| 49 |
-
if "@g.us" in str(numero) or "120363" in str(numero) or "grupo_" in str(numero):
|
| 50 |
-
tipo = "grupo"
|
| 51 |
-
else:
|
| 52 |
-
tipo = "pv"
|
| 53 |
-
|
| 54 |
-
data_semana = datetime.now().strftime("%Y-%W")
|
| 55 |
-
salt = f"AKIRA_V21_{data_semana}_ISOLATION"
|
| 56 |
-
|
| 57 |
-
raw = f"{str(numero).strip()}|{tipo}|{salt}"
|
| 58 |
-
return hashlib.sha256(raw.encode()).hexdigest()[:32]
|
| 59 |
-
|
| 60 |
# ================================================================
|
| 61 |
-
# CONEXÃO COM OTIMIZAÇÕES
|
| 62 |
-
# ================================================================
|
| 63 |
-
|
| 64 |
def _get_connection(self) -> sqlite3.Connection:
|
| 65 |
for attempt in range(self.max_retries):
|
| 66 |
try:
|
| 67 |
conn = sqlite3.connect(self.db_path, timeout=30.0, check_same_thread=False)
|
| 68 |
conn.execute('PRAGMA journal_mode=WAL')
|
| 69 |
conn.execute('PRAGMA synchronous=NORMAL')
|
| 70 |
-
conn.execute('PRAGMA cache_size=
|
| 71 |
conn.execute('PRAGMA temp_store=MEMORY')
|
| 72 |
-
conn.execute('PRAGMA busy_timeout=
|
| 73 |
conn.execute('PRAGMA foreign_keys=ON')
|
| 74 |
-
conn.execute('PRAGMA optimize')
|
| 75 |
return conn
|
| 76 |
except sqlite3.OperationalError as e:
|
| 77 |
if "database is locked" in str(e) and attempt < self.max_retries - 1:
|
| 78 |
time.sleep(self.retry_delay * (2 ** attempt))
|
| 79 |
continue
|
| 80 |
-
logger.error(f"Falha ao conectar: {e}")
|
| 81 |
raise
|
| 82 |
-
raise sqlite3.OperationalError("Falha após retries")
|
| 83 |
|
| 84 |
-
def _execute_with_retry(self, query: str, params: Optional[tuple] = None,
|
| 85 |
-
commit: bool = False, fetch: bool = True):
|
| 86 |
for attempt in range(self.max_retries):
|
| 87 |
try:
|
| 88 |
with self._get_connection() as conn:
|
|
@@ -91,1428 +52,355 @@ class Database:
|
|
| 91 |
c.execute(query, params)
|
| 92 |
else:
|
| 93 |
c.execute(query)
|
| 94 |
-
|
| 95 |
if commit:
|
| 96 |
conn.commit()
|
| 97 |
-
|
| 98 |
-
if fetch and query.strip().upper().startswith('SELECT'):
|
| 99 |
-
return c.fetchall()
|
| 100 |
-
elif fetch:
|
| 101 |
-
return c.fetchall() if c.description else []
|
| 102 |
-
else:
|
| 103 |
-
return c.lastrowid
|
| 104 |
except sqlite3.OperationalError as e:
|
| 105 |
if "database is locked" in str(e) and attempt < self.max_retries - 1:
|
| 106 |
time.sleep(self.retry_delay * (2 ** attempt))
|
| 107 |
continue
|
| 108 |
logger.error(f"Erro SQL (tentativa {attempt+1}): {e}")
|
| 109 |
raise
|
| 110 |
-
|
| 111 |
-
logger.error(f"Erro na query: {e}\nQuery: {query[:100]}...")
|
| 112 |
-
raise
|
| 113 |
-
|
| 114 |
-
# ================================================================
|
| 115 |
-
# VERIFICAÇÃO E CRIAÇÃO DE COLUNAS/ÍNDICES
|
| 116 |
-
# ================================================================
|
| 117 |
-
|
| 118 |
-
def _ensure_all_columns_and_indexes(self):
|
| 119 |
-
try:
|
| 120 |
-
with self._get_connection() as conn:
|
| 121 |
-
c = conn.cursor()
|
| 122 |
-
|
| 123 |
-
# NOVAS COLUNAS PARA TABELA MENSAGENS
|
| 124 |
-
try:
|
| 125 |
-
c.execute("ALTER TABLE mensagens ADD COLUMN tipo_mensagem TEXT DEFAULT 'texto'")
|
| 126 |
-
except sqlite3.OperationalError:
|
| 127 |
-
pass # Coluna já existe
|
| 128 |
-
|
| 129 |
-
try:
|
| 130 |
-
c.execute("ALTER TABLE mensagens ADD COLUMN reply_info_json TEXT")
|
| 131 |
-
except sqlite3.OperationalError:
|
| 132 |
-
pass # Coluna já existe
|
| 133 |
-
|
| 134 |
-
try:
|
| 135 |
-
c.execute("ALTER TABLE mensagens ADD COLUMN usuario_nome TEXT DEFAULT ''")
|
| 136 |
-
except sqlite3.OperationalError:
|
| 137 |
-
pass # Coluna já existe
|
| 138 |
-
|
| 139 |
-
try:
|
| 140 |
-
c.execute("ALTER TABLE mensagens ADD COLUMN grupo_id TEXT DEFAULT ''")
|
| 141 |
-
except sqlite3.OperationalError:
|
| 142 |
-
pass # Coluna já existe
|
| 143 |
-
|
| 144 |
-
try:
|
| 145 |
-
c.execute("ALTER TABLE mensagens ADD COLUMN grupo_nome TEXT DEFAULT ''")
|
| 146 |
-
except sqlite3.OperationalError:
|
| 147 |
-
pass # Coluna já existe
|
| 148 |
-
|
| 149 |
-
# ÍNDICES (ATUALIZADO COM NOVA TABELA EMBEDDINGS)
|
| 150 |
-
indexes = [
|
| 151 |
-
"CREATE INDEX IF NOT EXISTS idx_mensagens_numero ON mensagens(numero, deletado)",
|
| 152 |
-
"CREATE INDEX IF NOT EXISTS idx_mensagens_contexto ON mensagens(contexto_id)",
|
| 153 |
-
"CREATE INDEX IF NOT EXISTS idx_mensagens_timestamp ON mensagens(timestamp)",
|
| 154 |
-
"CREATE INDEX IF NOT EXISTS idx_mensagens_tipo ON mensagens(tipo_mensagem)",
|
| 155 |
-
"CREATE INDEX IF NOT EXISTS idx_mensagens_grupo ON mensagens(grupo_id, deletado)",
|
| 156 |
-
"CREATE INDEX IF NOT EXISTS idx_contexto_numero ON contexto(numero)",
|
| 157 |
-
"CREATE INDEX IF NOT EXISTS idx_contexto_tipo ON contexto(tipo_contexto)",
|
| 158 |
-
"CREATE INDEX IF NOT EXISTS idx_usuarios_numero ON usuarios_privilegiados(numero)",
|
| 159 |
-
"CREATE INDEX IF NOT EXISTS idx_transicoes_numero ON transicoes_humor(numero, timestamp)",
|
| 160 |
-
"CREATE INDEX IF NOT EXISTS idx_girias_numero ON girias_aprendidas(numero, giria)",
|
| 161 |
-
"CREATE INDEX IF NOT EXISTS idx_training_qualidade ON training_examples(qualidade_score, usado)",
|
| 162 |
-
"CREATE INDEX IF NOT EXISTS idx_comandos_numero ON comandos_executados(numero, timestamp)",
|
| 163 |
-
"CREATE INDEX IF NOT EXISTS idx_reset_numero ON reset_log(numero, timestamp)",
|
| 164 |
-
"CREATE INDEX IF NOT EXISTS idx_audio_usuario ON audio_transcricoes(numero_usuario, sucesso)",
|
| 165 |
-
"CREATE INDEX IF NOT EXISTS idx_grupo_contexto_id ON grupo_contexto(grupo_id, contexto_id)",
|
| 166 |
-
"CREATE INDEX IF NOT EXISTS idx_embeddings_numero ON embeddings(numero, timestamp)"
|
| 167 |
-
]
|
| 168 |
-
|
| 169 |
-
for idx_query in indexes:
|
| 170 |
-
try:
|
| 171 |
-
c.execute(idx_query)
|
| 172 |
-
except Exception as e:
|
| 173 |
-
logger.warning(f"Erro ao criar índice: {e}")
|
| 174 |
-
|
| 175 |
-
conn.commit()
|
| 176 |
-
logger.info("✅ Índices verificados/criados")
|
| 177 |
-
|
| 178 |
-
except Exception as e:
|
| 179 |
-
logger.error(f"Erro ao verificar colunas/índices: {e}")
|
| 180 |
|
| 181 |
# ================================================================
|
| 182 |
-
# INICIALIZAÇÃO
|
| 183 |
# ================================================================
|
| 184 |
-
|
| 185 |
def _init_db(self):
|
| 186 |
try:
|
| 187 |
with self._get_connection() as conn:
|
| 188 |
c = conn.cursor()
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 192 |
CREATE TABLE IF NOT EXISTS mensagens (
|
| 193 |
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 194 |
usuario TEXT NOT NULL,
|
| 195 |
-
usuario_nome TEXT DEFAULT '',
|
| 196 |
mensagem TEXT NOT NULL,
|
| 197 |
resposta TEXT NOT NULL,
|
| 198 |
-
numero TEXT
|
| 199 |
-
contexto_id TEXT NOT NULL,
|
| 200 |
-
tipo_contexto TEXT DEFAULT 'pv',
|
| 201 |
-
tipo_mensagem TEXT DEFAULT 'texto',
|
| 202 |
is_reply BOOLEAN DEFAULT 0,
|
| 203 |
mensagem_original TEXT,
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
modo_resposta TEXT DEFAULT 'normal_ironico',
|
| 208 |
-
emocao_detectada TEXT,
|
| 209 |
-
confianca_emocao REAL DEFAULT 0.5,
|
| 210 |
-
grupo_id TEXT DEFAULT '',
|
| 211 |
-
grupo_nome TEXT DEFAULT '',
|
| 212 |
-
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
|
| 213 |
-
deletado BOOLEAN DEFAULT 0
|
| 214 |
-
)
|
| 215 |
-
''')
|
| 216 |
-
|
| 217 |
-
# TABELA DE USUÁRIOS PRIVILEGIADOS (V21)
|
| 218 |
-
c.execute('''
|
| 219 |
-
CREATE TABLE IF NOT EXISTS usuarios_privilegiados (
|
| 220 |
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 221 |
-
numero TEXT UNIQUE NOT NULL,
|
| 222 |
-
nome TEXT NOT NULL,
|
| 223 |
-
nome_curto TEXT,
|
| 224 |
-
tom_inicial TEXT DEFAULT 'formal',
|
| 225 |
-
pode_dar_ordens BOOLEAN DEFAULT 0,
|
| 226 |
-
pode_usar_reset BOOLEAN DEFAULT 0,
|
| 227 |
-
pode_forcar_modo BOOLEAN DEFAULT 0,
|
| 228 |
-
nivel_acesso TEXT DEFAULT 'vip',
|
| 229 |
-
ultimo_comando TEXT,
|
| 230 |
-
timestamp_comando DATETIME,
|
| 231 |
-
comandos_executados INTEGER DEFAULT 0,
|
| 232 |
-
data_criacao DATETIME DEFAULT CURRENT_TIMESTAMP,
|
| 233 |
-
data_atualizacao DATETIME DEFAULT CURRENT_TIMESTAMP
|
| 234 |
-
)
|
| 235 |
-
''')
|
| 236 |
-
|
| 237 |
-
# TABELA DE CONTEXTO (ATUALIZADA)
|
| 238 |
-
c.execute('''
|
| 239 |
-
CREATE TABLE IF NOT EXISTS contexto (
|
| 240 |
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
nivel_transicao INTEGER DEFAULT 0,
|
| 248 |
-
humor_alvo TEXT DEFAULT 'normal_ironico',
|
| 249 |
-
termos TEXT,
|
| 250 |
-
girias TEXT,
|
| 251 |
-
tom TEXT DEFAULT 'normal',
|
| 252 |
-
emocao_tendencia TEXT DEFAULT 'neutral',
|
| 253 |
-
volatilidade REAL DEFAULT 0.5,
|
| 254 |
-
nome_usuario TEXT DEFAULT '',
|
| 255 |
-
ultimo_contato DATETIME,
|
| 256 |
-
data_criacao DATETIME DEFAULT CURRENT_TIMESTAMP,
|
| 257 |
-
data_atualizacao DATETIME DEFAULT CURRENT_TIMESTAMP
|
| 258 |
-
)
|
| 259 |
-
''')
|
| 260 |
-
|
| 261 |
-
# NOVA TABELA: CONTEXTO DE GRUPO
|
| 262 |
-
c.execute('''
|
| 263 |
-
CREATE TABLE IF NOT EXISTS grupo_contexto (
|
| 264 |
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 265 |
-
grupo_id TEXT UNIQUE NOT NULL,
|
| 266 |
-
contexto_id TEXT NOT NULL,
|
| 267 |
-
grupo_nome TEXT DEFAULT '',
|
| 268 |
-
numero_participantes INTEGER DEFAULT 0,
|
| 269 |
-
historico_grupo TEXT,
|
| 270 |
-
humor_medio TEXT DEFAULT 'normal_ironico',
|
| 271 |
-
tom_medio TEXT DEFAULT 'normal',
|
| 272 |
-
atividade_score REAL DEFAULT 0.5,
|
| 273 |
-
ultima_atividade DATETIME DEFAULT CURRENT_TIMESTAMP,
|
| 274 |
-
data_criacao DATETIME DEFAULT CURRENT_TIMESTAMP,
|
| 275 |
-
data_atualizacao DATETIME DEFAULT CURRENT_TIMESTAMP
|
| 276 |
-
)
|
| 277 |
-
''')
|
| 278 |
-
|
| 279 |
-
# TABELA DE TRANSIÇÕES DE HUMOR
|
| 280 |
-
c.execute('''
|
| 281 |
-
CREATE TABLE IF NOT EXISTS transicoes_humor (
|
| 282 |
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 283 |
-
numero TEXT NOT NULL,
|
| 284 |
-
contexto_id TEXT NOT NULL,
|
| 285 |
-
humor_anterior TEXT NOT NULL,
|
| 286 |
-
humor_novo TEXT NOT NULL,
|
| 287 |
-
emocao_trigger TEXT,
|
| 288 |
-
confianca_emocao REAL,
|
| 289 |
-
nivel_transicao INTEGER,
|
| 290 |
-
razao TEXT,
|
| 291 |
-
intensidade REAL DEFAULT 0.5,
|
| 292 |
-
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
|
| 293 |
-
)
|
| 294 |
-
''')
|
| 295 |
-
|
| 296 |
-
# TABELA DE EXEMPLOS DE TREINAMENTO
|
| 297 |
-
c.execute('''
|
| 298 |
-
CREATE TABLE IF NOT EXISTS training_examples (
|
| 299 |
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 300 |
-
input_text TEXT NOT NULL,
|
| 301 |
-
output_text TEXT NOT NULL,
|
| 302 |
-
humor TEXT DEFAULT 'normal_ironico',
|
| 303 |
-
modo_resposta TEXT DEFAULT 'normal_ironico',
|
| 304 |
-
emocao_contexto TEXT,
|
| 305 |
-
qualidade_score REAL DEFAULT 1.0,
|
| 306 |
-
usado BOOLEAN DEFAULT 0,
|
| 307 |
-
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
|
| 308 |
-
)
|
| 309 |
-
''')
|
| 310 |
-
|
| 311 |
-
# TABELA DE GÍRIAS APRENDIDAS
|
| 312 |
-
c.execute('''
|
| 313 |
CREATE TABLE IF NOT EXISTS girias_aprendidas (
|
| 314 |
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 315 |
-
|
| 316 |
-
contexto_id TEXT NOT NULL,
|
| 317 |
giria TEXT NOT NULL,
|
| 318 |
significado TEXT NOT NULL,
|
| 319 |
contexto TEXT,
|
| 320 |
frequencia INTEGER DEFAULT 1,
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
)
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
# NOVA TABELA: TRANSCRIÇÕES DE ÁUDIO
|
| 327 |
-
c.execute('''
|
| 328 |
-
CREATE TABLE IF NOT EXISTS audio_transcricoes (
|
| 329 |
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 330 |
numero_usuario TEXT NOT NULL,
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
|
| 338 |
-
)
|
| 339 |
-
''')
|
| 340 |
-
|
| 341 |
-
# TABELA DE COMANDOS EXECUTADOS
|
| 342 |
-
c.execute('''
|
| 343 |
-
CREATE TABLE IF NOT EXISTS comandos_executados (
|
| 344 |
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 345 |
-
numero TEXT NOT NULL,
|
| 346 |
-
comando TEXT NOT NULL,
|
| 347 |
-
parametros TEXT,
|
| 348 |
-
sucesso BOOLEAN DEFAULT 1,
|
| 349 |
-
resposta TEXT,
|
| 350 |
-
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
|
| 351 |
-
)
|
| 352 |
-
''')
|
| 353 |
-
|
| 354 |
-
# TABELA DE RESETS (LOG)
|
| 355 |
-
c.execute('''
|
| 356 |
-
CREATE TABLE IF NOT EXISTS reset_log (
|
| 357 |
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 365 |
''')
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
texto TEXT NOT NULL,
|
| 373 |
-
embedding BLOB NOT NULL,
|
| 374 |
-
modelo TEXT DEFAULT 'MiniLM-L12-v2',
|
| 375 |
-
hash_texto TEXT,
|
| 376 |
-
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
|
| 377 |
-
)
|
| 378 |
''')
|
| 379 |
-
|
| 380 |
conn.commit()
|
| 381 |
-
|
| 382 |
-
logger.info("✅ Todas as tabelas criadas/verificadas (ADAPTADO)")
|
| 383 |
-
|
| 384 |
except Exception as e:
|
| 385 |
-
logger.error(f"
|
| 386 |
raise
|
| 387 |
|
| 388 |
-
|
| 389 |
-
# SINCRONIZAÇÃO DE USUÁRIOS PRIVILEGIADOS
|
| 390 |
-
# ================================================================
|
| 391 |
-
|
| 392 |
-
def _sincronizar_usuarios_privilegiados(self):
|
| 393 |
-
try:
|
| 394 |
-
for numero, dados in config.USUARIOS_PRIVILEGIADOS.items():
|
| 395 |
-
self._execute_with_retry(
|
| 396 |
-
"""
|
| 397 |
-
INSERT OR REPLACE INTO usuarios_privilegiados
|
| 398 |
-
(numero, nome, nome_curto, tom_inicial, pode_dar_ordens,
|
| 399 |
-
pode_usar_reset, pode_forcar_modo, nivel_acesso, data_atualizacao)
|
| 400 |
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
| 401 |
-
""",
|
| 402 |
-
(
|
| 403 |
-
str(numero).strip(),
|
| 404 |
-
dados["nome"],
|
| 405 |
-
dados.get("nome_curto", dados["nome"].split()[0]),
|
| 406 |
-
dados.get("tom_inicial", "formal"),
|
| 407 |
-
1 if dados.get("pode_dar_ordens", False) else 0,
|
| 408 |
-
1 if dados.get("pode_usar_reset", False) else 0,
|
| 409 |
-
1 if dados.get("pode_forcar_modo", False) else 0,
|
| 410 |
-
dados.get("nivel_acesso", "vip")
|
| 411 |
-
),
|
| 412 |
-
commit=True,
|
| 413 |
-
fetch=False
|
| 414 |
-
)
|
| 415 |
-
|
| 416 |
-
logger.info(f"✅ Usuários privilegiados sincronizados: {len(config.USUARIOS_PRIVILEGIADOS)}")
|
| 417 |
-
|
| 418 |
-
except Exception as e:
|
| 419 |
-
logger.error(f"Erro ao sincronizar usuários privilegiados: {e}")
|
| 420 |
-
|
| 421 |
-
# ================================================================
|
| 422 |
-
# VERIFICAÇÃO DE PRIVILÉGIOS (ROBUSTA)
|
| 423 |
-
# ================================================================
|
| 424 |
-
|
| 425 |
-
def is_usuario_privilegiado(self, numero: str) -> bool:
|
| 426 |
-
try:
|
| 427 |
-
result = self._execute_with_retry(
|
| 428 |
-
"SELECT 1 FROM usuarios_privilegiados WHERE numero = ?",
|
| 429 |
-
(str(numero).strip(),),
|
| 430 |
-
fetch=True
|
| 431 |
-
)
|
| 432 |
-
return bool(result)
|
| 433 |
-
except Exception as e:
|
| 434 |
-
logger.error(f"Erro ao verificar privilégio: {e}")
|
| 435 |
-
return False
|
| 436 |
-
|
| 437 |
-
def get_usuario_privilegiado(self, numero: str) -> Optional[Dict]:
|
| 438 |
-
try:
|
| 439 |
-
result = self._execute_with_retry(
|
| 440 |
-
"""
|
| 441 |
-
SELECT numero, nome, nome_curto, tom_inicial, pode_dar_ordens,
|
| 442 |
-
pode_usar_reset, pode_forcar_modo, nivel_acesso,
|
| 443 |
-
ultimo_comando, timestamp_comando, comandos_executados
|
| 444 |
-
FROM usuarios_privilegiados
|
| 445 |
-
WHERE numero = ?
|
| 446 |
-
""",
|
| 447 |
-
(str(numero).strip(),),
|
| 448 |
-
fetch=True
|
| 449 |
-
)
|
| 450 |
-
|
| 451 |
-
if result:
|
| 452 |
-
row = result[0]
|
| 453 |
-
return {
|
| 454 |
-
"numero": row[0],
|
| 455 |
-
"nome": row[1],
|
| 456 |
-
"nome_curto": row[2],
|
| 457 |
-
"tom_inicial": row[3],
|
| 458 |
-
"pode_dar_ordens": bool(row[4]),
|
| 459 |
-
"pode_usar_reset": bool(row[5]),
|
| 460 |
-
"pode_forcar_modo": bool(row[6]),
|
| 461 |
-
"nivel_acesso": row[7],
|
| 462 |
-
"ultimo_comando": row[8],
|
| 463 |
-
"timestamp_comando": row[9],
|
| 464 |
-
"comandos_executados": row[10]
|
| 465 |
-
}
|
| 466 |
-
return None
|
| 467 |
-
|
| 468 |
-
except Exception as e:
|
| 469 |
-
logger.error(f"Erro ao obter usuário privilegiado: {e}")
|
| 470 |
-
return None
|
| 471 |
-
|
| 472 |
-
def pode_dar_ordens(self, numero: str) -> bool:
|
| 473 |
-
user = self.get_usuario_privilegiado(numero)
|
| 474 |
-
return user and user.get("pode_dar_ordens", False)
|
| 475 |
-
|
| 476 |
-
def pode_usar_reset(self, numero: str) -> bool:
|
| 477 |
-
user = self.get_usuario_privilegiado(numero)
|
| 478 |
-
return user and user.get("pode_usar_reset", False)
|
| 479 |
-
|
| 480 |
-
def pode_forcar_modo(self, numero: str) -> bool:
|
| 481 |
-
user = self.get_usuario_privilegiado(numero)
|
| 482 |
-
return user and user.get("pode_forcar_modo", False)
|
| 483 |
-
|
| 484 |
-
def registrar_comando(self, numero: str, comando: str,
|
| 485 |
-
parametros: str = None, sucesso: bool = True,
|
| 486 |
-
resposta: str = None):
|
| 487 |
-
try:
|
| 488 |
-
self._execute_with_retry(
|
| 489 |
-
"""
|
| 490 |
-
INSERT INTO comandos_executados
|
| 491 |
-
(numero, comando, parametros, sucesso, resposta)
|
| 492 |
-
VALUES (?, ?, ?, ?, ?)
|
| 493 |
-
""",
|
| 494 |
-
(str(numero).strip(), comando, parametros, 1 if sucesso else 0, resposta),
|
| 495 |
-
commit=True,
|
| 496 |
-
fetch=False
|
| 497 |
-
)
|
| 498 |
-
|
| 499 |
-
if self.is_usuario_privilegiado(numero):
|
| 500 |
-
self._execute_with_retry(
|
| 501 |
-
"""
|
| 502 |
-
UPDATE usuarios_privilegiados
|
| 503 |
-
SET ultimo_comando = ?,
|
| 504 |
-
timestamp_comando = CURRENT_TIMESTAMP,
|
| 505 |
-
comandos_executados = comandos_executados + 1,
|
| 506 |
-
data_atualizacao = CURRENT_TIMESTAMP
|
| 507 |
-
WHERE numero = ?
|
| 508 |
-
""",
|
| 509 |
-
(comando, str(numero).strip()),
|
| 510 |
-
commit=True,
|
| 511 |
-
fetch=False
|
| 512 |
-
)
|
| 513 |
-
|
| 514 |
-
except Exception as e:
|
| 515 |
-
logger.error(f"Erro ao registrar comando: {e}")
|
| 516 |
-
|
| 517 |
-
# ================================================================
|
| 518 |
-
# SISTEMA DE RESET (EXCLUSIVO PARA PRIVILEGIADOS)
|
| 519 |
-
# ================================================================
|
| 520 |
-
|
| 521 |
-
def resetar_contexto_usuario(self, numero: str, tipo: str = "completo") -> Dict:
|
| 522 |
try:
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
|
| 526 |
-
|
| 527 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 528 |
}
|
| 529 |
-
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
|
| 533 |
-
|
| 534 |
-
|
| 535 |
-
|
| 536 |
-
|
| 537 |
-
|
| 538 |
-
|
| 539 |
-
|
| 540 |
-
|
| 541 |
-
|
| 542 |
-
|
| 543 |
-
|
| 544 |
-
|
| 545 |
-
|
| 546 |
-
|
| 547 |
-
|
| 548 |
-
|
| 549 |
-
|
| 550 |
-
|
| 551 |
-
commit=True,
|
| 552 |
-
fetch=False
|
| 553 |
-
)
|
| 554 |
-
itens_apagados += result if result else 0
|
| 555 |
-
|
| 556 |
-
result = self._execute_with_retry(
|
| 557 |
-
"DELETE FROM girias_aprendidas WHERE numero = ?",
|
| 558 |
-
(str(numero).strip(),),
|
| 559 |
-
commit=True,
|
| 560 |
-
fetch=False
|
| 561 |
-
)
|
| 562 |
-
itens_apagados += result if result else 0
|
| 563 |
-
|
| 564 |
-
self._execute_with_retry(
|
| 565 |
-
"""
|
| 566 |
-
INSERT INTO reset_log
|
| 567 |
-
(numero, tipo_reset, itens_apagados, motivo, sucesso)
|
| 568 |
-
VALUES (?, ?, ?, ?, 1)
|
| 569 |
-
""",
|
| 570 |
-
(str(numero).strip(), tipo, itens_apagados, f"Comando /reset por usuário privilegiado"),
|
| 571 |
-
commit=True,
|
| 572 |
-
fetch=False
|
| 573 |
-
)
|
| 574 |
-
|
| 575 |
-
logger.info(f"✅ Reset completo para {numero}: {itens_apagados} itens apagados")
|
| 576 |
-
|
| 577 |
-
return {
|
| 578 |
-
"sucesso": True,
|
| 579 |
-
"itens_apagados": itens_apagados,
|
| 580 |
-
"mensagem": f"Contexto resetado com sucesso ({itens_apagados} itens removidos)"
|
| 581 |
-
}
|
| 582 |
-
|
| 583 |
-
except Exception as e:
|
| 584 |
-
logger.error(f"Erro ao resetar contexto: {e}")
|
| 585 |
-
self._execute_with_retry(
|
| 586 |
-
"""
|
| 587 |
-
INSERT INTO reset_log
|
| 588 |
-
(numero, tipo_reset, itens_apagados, motivo, sucesso)
|
| 589 |
-
VALUES (?, ?, ?, ?, 0)
|
| 590 |
-
""",
|
| 591 |
-
(str(numero).strip(), tipo, 0, f"Erro: {str(e)[:100]}"),
|
| 592 |
-
commit=True,
|
| 593 |
-
fetch=False
|
| 594 |
-
)
|
| 595 |
-
return {
|
| 596 |
-
"sucesso": False,
|
| 597 |
-
"erro": str(e),
|
| 598 |
-
"itens_apagados": 0
|
| 599 |
-
}
|
| 600 |
-
|
| 601 |
-
def resetar_tudo(self, confirmacao: bool = False) -> Dict:
|
| 602 |
-
if not confirmacao:
|
| 603 |
-
return {
|
| 604 |
-
"sucesso": False,
|
| 605 |
-
"erro": "Confirmação necessária. Use confirmacao=True",
|
| 606 |
-
"itens_apagados": 0
|
| 607 |
-
}
|
| 608 |
-
|
| 609 |
-
try:
|
| 610 |
-
total_mensagens = self._execute_with_retry(
|
| 611 |
-
"SELECT COUNT(*) FROM mensagens",
|
| 612 |
-
fetch=True
|
| 613 |
-
)[0][0]
|
| 614 |
-
|
| 615 |
-
tabelas_para_resetar = [
|
| 616 |
-
"mensagens", "contexto", "transicoes_humor",
|
| 617 |
-
"girias_aprendidas", "training_examples",
|
| 618 |
-
"comandos_executados", "reset_log",
|
| 619 |
-
"audio_transcricoes", "grupo_contexto", "embeddings"
|
| 620 |
-
]
|
| 621 |
-
|
| 622 |
-
for tabela in tabelas_para_resetar:
|
| 623 |
-
self._execute_with_retry(f"DELETE FROM {tabela}", commit=True, fetch=False)
|
| 624 |
-
|
| 625 |
-
logger.warning(f"⚠️ RESET COMPLETO DA DATABASE: {total_mensagens} mensagens apagadas")
|
| 626 |
-
|
| 627 |
-
return {
|
| 628 |
-
"sucesso": True,
|
| 629 |
-
"itens_apagados": total_mensagens,
|
| 630 |
-
"mensagem": f"Database resetada completamente. {total_mensagens} mensagens removidas."
|
| 631 |
-
}
|
| 632 |
-
|
| 633 |
-
except Exception as e:
|
| 634 |
-
logger.error(f"Erro no reset completo: {e}")
|
| 635 |
-
return {
|
| 636 |
-
"sucesso": False,
|
| 637 |
-
"erro": str(e),
|
| 638 |
-
"itens_apagados": 0
|
| 639 |
-
}
|
| 640 |
-
|
| 641 |
-
# ================================================================
|
| 642 |
-
# MÉTODOS DE RECUPERAÇÃO (COM ISOLAMENTO) - ADAPTADOS
|
| 643 |
-
# ================================================================
|
| 644 |
-
|
| 645 |
-
def recuperar_mensagens(self, numero: str, limite: int = 10) -> List[Tuple]:
|
| 646 |
-
try:
|
| 647 |
-
results = self._execute_with_retry(
|
| 648 |
-
"""
|
| 649 |
-
SELECT mensagem, resposta, is_reply, mensagem_original, reply_to_bot,
|
| 650 |
-
humor, modo_resposta, emocao_detectada, confianca_emocao,
|
| 651 |
-
tipo_mensagem, reply_info_json
|
| 652 |
-
FROM mensagens
|
| 653 |
-
WHERE numero = ? AND deletado = 0
|
| 654 |
-
ORDER BY timestamp DESC
|
| 655 |
-
LIMIT ?
|
| 656 |
-
""",
|
| 657 |
-
(str(numero).strip(), limite),
|
| 658 |
-
fetch=True
|
| 659 |
-
)
|
| 660 |
-
|
| 661 |
-
if results:
|
| 662 |
-
mensagens = []
|
| 663 |
-
for r in results[::-1]:
|
| 664 |
try:
|
| 665 |
-
|
| 666 |
except:
|
| 667 |
-
|
| 668 |
-
|
| 669 |
-
mensagens.append((
|
| 670 |
-
r[0], r[1], bool(r[2]), r[3], bool(r[4]),
|
| 671 |
-
r[5], r[6], r[7], r[8], r[9], reply_info
|
| 672 |
-
))
|
| 673 |
-
return mensagens
|
| 674 |
-
return []
|
| 675 |
-
|
| 676 |
-
except Exception as e:
|
| 677 |
-
logger.error(f"Erro ao recuperar mensagens: {e}")
|
| 678 |
-
return []
|
| 679 |
-
|
| 680 |
-
def recuperar_humor_atual(self, numero: str) -> str:
|
| 681 |
-
try:
|
| 682 |
-
result = self._execute_with_retry(
|
| 683 |
-
"SELECT humor_atual FROM contexto WHERE numero = ?",
|
| 684 |
-
(str(numero).strip(),),
|
| 685 |
-
fetch=True
|
| 686 |
-
)
|
| 687 |
-
return result[0][0] if result else "normal_ironico"
|
| 688 |
-
except Exception:
|
| 689 |
-
return "normal_ironico"
|
| 690 |
-
|
| 691 |
-
def recuperar_modo_resposta(self, numero: str) -> str:
|
| 692 |
-
try:
|
| 693 |
-
result = self._execute_with_retry(
|
| 694 |
-
"SELECT modo_resposta FROM contexto WHERE numero = ?",
|
| 695 |
-
(str(numero).strip(),),
|
| 696 |
-
fetch=True
|
| 697 |
-
)
|
| 698 |
-
return result[0][0] if result else "normal_ironico"
|
| 699 |
-
except Exception:
|
| 700 |
-
return "normal_ironico"
|
| 701 |
-
|
| 702 |
-
def recuperar_contexto_completo(self, numero: str) -> Dict:
|
| 703 |
-
try:
|
| 704 |
-
result = self._execute_with_retry(
|
| 705 |
-
"""
|
| 706 |
-
SELECT contexto_id, tipo_contexto, historico, humor_atual,
|
| 707 |
-
modo_resposta, nivel_transicao, humor_alvo, termos,
|
| 708 |
-
girias, tom, emocao_tendencia, volatilidade, nome_usuario
|
| 709 |
-
FROM contexto
|
| 710 |
-
WHERE numero = ?
|
| 711 |
-
""",
|
| 712 |
-
(str(numero).strip(),),
|
| 713 |
-
fetch=True
|
| 714 |
-
)
|
| 715 |
-
|
| 716 |
-
if result:
|
| 717 |
-
row = result[0]
|
| 718 |
-
return {
|
| 719 |
-
"contexto_id": row[0],
|
| 720 |
-
"tipo_contexto": row[1],
|
| 721 |
-
"historico": row[2],
|
| 722 |
-
"humor_atual": row[3] or "normal_ironico",
|
| 723 |
-
"modo_resposta": row[4] or "normal_ironico",
|
| 724 |
-
"nivel_transicao": row[5] or 0,
|
| 725 |
-
"humor_alvo": row[6] or "normal_ironico",
|
| 726 |
-
"termos": row[7],
|
| 727 |
-
"girias": row[8],
|
| 728 |
-
"tom": row[9] or "normal",
|
| 729 |
-
"emocao_tendencia": row[10] or "neutral",
|
| 730 |
-
"volatilidade": row[11] or 0.5,
|
| 731 |
-
"nome_usuario": row[12] or ""
|
| 732 |
-
}
|
| 733 |
-
|
| 734 |
-
return {
|
| 735 |
-
"contexto_id": self._gerar_contexto_id(numero, 'auto'),
|
| 736 |
-
"tipo_contexto": "pv",
|
| 737 |
-
"historico": "",
|
| 738 |
-
"humor_atual": "normal_ironico",
|
| 739 |
-
"modo_resposta": "normal_ironico",
|
| 740 |
-
"nivel_transicao": 0,
|
| 741 |
-
"humor_alvo": "normal_ironico",
|
| 742 |
-
"termos": "",
|
| 743 |
-
"girias": "",
|
| 744 |
-
"tom": "normal",
|
| 745 |
-
"emocao_tendencia": "neutral",
|
| 746 |
-
"volatilidade": 0.5,
|
| 747 |
-
"nome_usuario": ""
|
| 748 |
-
}
|
| 749 |
-
|
| 750 |
-
except Exception as e:
|
| 751 |
-
logger.error(f"Erro ao recuperar contexto: {e}")
|
| 752 |
-
return {
|
| 753 |
-
"contexto_id": self._gerar_contexto_id(numero, 'auto'),
|
| 754 |
-
"tipo_contexto": "pv",
|
| 755 |
-
"historico": "",
|
| 756 |
-
"humor_atual": "normal_ironico",
|
| 757 |
-
"modo_resposta": "normal_ironico",
|
| 758 |
-
"nivel_transicao": 0,
|
| 759 |
-
"humor_alvo": "normal_ironico",
|
| 760 |
-
"termos": "",
|
| 761 |
-
"girias": "",
|
| 762 |
-
"tom": "normal",
|
| 763 |
-
"emocao_tendencia": "neutral",
|
| 764 |
-
"volatilidade": 0.5,
|
| 765 |
-
"nome_usuario": ""
|
| 766 |
-
}
|
| 767 |
-
|
| 768 |
-
def recuperar_historico_humor(self, numero: str, limite: int = 5) -> List[Dict]:
|
| 769 |
-
try:
|
| 770 |
-
results = self._execute_with_retry(
|
| 771 |
-
"""
|
| 772 |
-
SELECT humor_anterior, humor_novo, emocao_trigger, confianca_emocao,
|
| 773 |
-
nivel_transicao, razao, intensidade, timestamp
|
| 774 |
-
FROM transicoes_humor
|
| 775 |
-
WHERE numero = ?
|
| 776 |
-
ORDER BY timestamp DESC
|
| 777 |
-
LIMIT ?
|
| 778 |
-
""",
|
| 779 |
-
(str(numero).strip(), limite),
|
| 780 |
-
fetch=True
|
| 781 |
-
)
|
| 782 |
-
|
| 783 |
-
return [
|
| 784 |
-
{
|
| 785 |
-
"anterior": r[0],
|
| 786 |
-
"novo": r[1],
|
| 787 |
-
"emocao_trigger": r[2],
|
| 788 |
-
"confianca_emocao": r[3],
|
| 789 |
-
"nivel_transicao": r[4],
|
| 790 |
-
"razao": r[5],
|
| 791 |
-
"intensidade": r[6],
|
| 792 |
-
"timestamp": r[7]
|
| 793 |
-
}
|
| 794 |
-
for r in results
|
| 795 |
-
]
|
| 796 |
-
except Exception as e:
|
| 797 |
-
logger.error(f"Erro ao recuperar histórico humor: {e}")
|
| 798 |
-
return []
|
| 799 |
-
|
| 800 |
-
def recuperar_girias_usuario(self, numero: str) -> List[Dict]:
|
| 801 |
-
try:
|
| 802 |
-
results = self._execute_with_retry(
|
| 803 |
-
"""
|
| 804 |
-
SELECT giria, significado, contexto, frequencia, ultimo_uso
|
| 805 |
-
FROM girias_aprendidas
|
| 806 |
-
WHERE numero = ?
|
| 807 |
-
ORDER BY frequencia DESC, ultimo_uso DESC
|
| 808 |
-
LIMIT 20
|
| 809 |
-
""",
|
| 810 |
-
(str(numero).strip(),),
|
| 811 |
-
fetch=True
|
| 812 |
-
)
|
| 813 |
-
|
| 814 |
-
return [
|
| 815 |
-
{
|
| 816 |
-
'giria': r[0],
|
| 817 |
-
'significado': r[1],
|
| 818 |
-
'contexto': r[2],
|
| 819 |
-
'frequencia': r[3],
|
| 820 |
-
'ultimo_uso': r[4]
|
| 821 |
-
}
|
| 822 |
-
for r in results
|
| 823 |
-
]
|
| 824 |
except Exception as e:
|
| 825 |
-
logger.error(f"Erro
|
| 826 |
-
|
| 827 |
-
|
| 828 |
-
|
| 829 |
-
|
| 830 |
-
|
| 831 |
-
|
| 832 |
-
|
| 833 |
-
|
| 834 |
-
|
| 835 |
-
|
| 836 |
-
|
| 837 |
-
|
| 838 |
-
|
| 839 |
-
(
|
| 840 |
-
|
| 841 |
-
|
| 842 |
-
|
| 843 |
-
|
| 844 |
-
|
| 845 |
-
|
| 846 |
-
"output": r[1],
|
| 847 |
-
"humor": r[2],
|
| 848 |
-
"modo": r[3],
|
| 849 |
-
"emocao_contexto": r[4],
|
| 850 |
-
"score": r[5]
|
| 851 |
-
}
|
| 852 |
-
for r in results
|
| 853 |
-
]
|
| 854 |
except Exception as e:
|
| 855 |
-
logger.
|
| 856 |
-
return []
|
| 857 |
-
|
| 858 |
-
def marcar_examples_como_usados(self):
|
| 859 |
-
try:
|
| 860 |
self._execute_with_retry(
|
| 861 |
-
"
|
| 862 |
-
|
| 863 |
-
|
| 864 |
)
|
| 865 |
-
logger.info("✅ Exemplos marcados como usados")
|
| 866 |
-
except Exception as e:
|
| 867 |
-
logger.error(f"Erro ao marcar exemplos: {e}")
|
| 868 |
|
| 869 |
-
|
| 870 |
-
|
| 871 |
-
|
| 872 |
-
|
| 873 |
-
|
| 874 |
-
numero: str = '', is_reply: bool = False,
|
| 875 |
-
mensagem_original: str = None, reply_to_bot: bool = False,
|
| 876 |
-
humor: str = 'normal_ironico',
|
| 877 |
-
modo_resposta: str = 'normal_ironico',
|
| 878 |
-
emocao_detectada: str = None,
|
| 879 |
-
confianca_emocao: float = 0.5,
|
| 880 |
-
tipo_mensagem: str = 'texto',
|
| 881 |
-
reply_info: Dict = None,
|
| 882 |
-
usuario_nome: str = '',
|
| 883 |
-
grupo_id: str = '',
|
| 884 |
-
grupo_nome: str = '',
|
| 885 |
-
tipo_conversa: str = 'pv'): # ADICIONADO ESTE PARÂMETRO
|
| 886 |
-
"""Método adaptado para o payload do index.js"""
|
| 887 |
-
try:
|
| 888 |
-
numero_final = str(numero or usuario).strip()
|
| 889 |
-
contexto_id = self._gerar_contexto_id(numero_final, 'auto')
|
| 890 |
-
|
| 891 |
-
# Usa tipo_conversa fornecido ou detecta automaticamente
|
| 892 |
-
if tipo_conversa:
|
| 893 |
-
tipo_contexto = tipo_conversa
|
| 894 |
-
else:
|
| 895 |
-
tipo_contexto = "grupo" if ("@g.us" in numero_final or "120363" in numero_final or grupo_id) else "pv"
|
| 896 |
-
|
| 897 |
-
# Converte reply_info para JSON
|
| 898 |
-
reply_info_json = json.dumps(reply_info) if reply_info else None
|
| 899 |
-
|
| 900 |
-
# Se for grupo, atualiza grupo_contexto
|
| 901 |
-
if grupo_id and tipo_contexto == "grupo":
|
| 902 |
-
self._atualizar_contexto_grupo(grupo_id, grupo_nome or "Sem nome", usuario_nome or usuario, mensagem[:100])
|
| 903 |
-
|
| 904 |
-
self._execute_with_retry(
|
| 905 |
-
"""
|
| 906 |
-
INSERT INTO mensagens
|
| 907 |
-
(usuario, usuario_nome, mensagem, resposta, numero, contexto_id, tipo_contexto,
|
| 908 |
-
tipo_mensagem, is_reply, mensagem_original, reply_to_bot, reply_info_json,
|
| 909 |
-
humor, modo_resposta, emocao_detectada, confianca_emocao, grupo_id, grupo_nome)
|
| 910 |
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
| 911 |
-
""",
|
| 912 |
-
(
|
| 913 |
-
usuario[:50], usuario_nome[:100] or usuario[:100],
|
| 914 |
-
mensagem[:1000], resposta[:1000], numero_final,
|
| 915 |
-
contexto_id, tipo_contexto, tipo_mensagem, int(is_reply),
|
| 916 |
-
mensagem_original, int(reply_to_bot), reply_info_json,
|
| 917 |
-
humor, modo_resposta, emocao_detectada, confianca_emocao,
|
| 918 |
-
grupo_id[:50], grupo_nome[:100]
|
| 919 |
-
),
|
| 920 |
-
commit=True,
|
| 921 |
-
fetch=False
|
| 922 |
-
)
|
| 923 |
-
|
| 924 |
-
logger.debug(f"✅ Mensagem salva: {numero_final} ({tipo_contexto}) | Tipo: {tipo_mensagem}")
|
| 925 |
-
return True
|
| 926 |
-
|
| 927 |
-
except Exception as e:
|
| 928 |
-
logger.error(f"Erro ao salvar mensagem: {e}")
|
| 929 |
-
return False
|
| 930 |
|
| 931 |
-
|
| 932 |
-
|
|
|
|
| 933 |
try:
|
| 934 |
-
|
| 935 |
-
|
| 936 |
-
#
|
| 937 |
-
result = self._execute_with_retry(
|
| 938 |
-
"SELECT 1 FROM grupo_contexto WHERE grupo_id = ?",
|
| 939 |
-
(grupo_id,),
|
| 940 |
-
fetch=True
|
| 941 |
-
)
|
| 942 |
-
|
| 943 |
-
if result:
|
| 944 |
-
# Atualiza
|
| 945 |
-
self._execute_with_retry(
|
| 946 |
-
"""
|
| 947 |
-
UPDATE grupo_contexto
|
| 948 |
-
SET ultima_atividade = CURRENT_TIMESTAMP,
|
| 949 |
-
data_atualizacao = CURRENT_TIMESTAMP
|
| 950 |
-
WHERE grupo_id = ?
|
| 951 |
-
""",
|
| 952 |
-
(grupo_id,),
|
| 953 |
-
commit=True,
|
| 954 |
-
fetch=False
|
| 955 |
-
)
|
| 956 |
-
else:
|
| 957 |
-
# Insere novo
|
| 958 |
-
self._execute_with_retry(
|
| 959 |
-
"""
|
| 960 |
-
INSERT INTO grupo_contexto
|
| 961 |
-
(grupo_id, contexto_id, grupo_nome, ultima_atividade)
|
| 962 |
-
VALUES (?, ?, ?, CURRENT_TIMESTAMP)
|
| 963 |
-
""",
|
| 964 |
-
(grupo_id, contexto_id, grupo_nome[:100]),
|
| 965 |
-
commit=True,
|
| 966 |
-
fetch=False
|
| 967 |
-
)
|
| 968 |
-
|
| 969 |
-
except Exception as e:
|
| 970 |
-
logger.error(f"Erro ao atualizar contexto de grupo: {e}")
|
| 971 |
-
|
| 972 |
-
def salvar_contexto(self, numero: str, **kwargs):
|
| 973 |
-
try:
|
| 974 |
-
numero_final = str(numero).strip()
|
| 975 |
-
contexto_id = self._gerar_contexto_id(numero_final, 'auto')
|
| 976 |
-
tipo_contexto = "grupo" if ("@g.us" in numero_final or "120363" in numero_final) else "pv"
|
| 977 |
-
|
| 978 |
-
# Adiciona nome_usuario se fornecido
|
| 979 |
-
if 'nome_usuario' in kwargs:
|
| 980 |
-
kwargs['ultimo_contato'] = "CURRENT_TIMESTAMP"
|
| 981 |
-
|
| 982 |
-
campos = {
|
| 983 |
-
"numero": numero_final,
|
| 984 |
-
"contexto_id": contexto_id,
|
| 985 |
-
"tipo_contexto": tipo_contexto,
|
| 986 |
-
"data_atualizacao": "CURRENT_TIMESTAMP"
|
| 987 |
-
}
|
| 988 |
-
|
| 989 |
-
campos.update(kwargs)
|
| 990 |
-
|
| 991 |
-
colunas = []
|
| 992 |
-
valores = []
|
| 993 |
-
placeholders = []
|
| 994 |
-
|
| 995 |
-
for key, value in campos.items():
|
| 996 |
-
if key != "data_atualizacao":
|
| 997 |
-
colunas.append(key)
|
| 998 |
-
valores.append(value)
|
| 999 |
-
placeholders.append("?")
|
| 1000 |
-
|
| 1001 |
-
colunas.append("data_atualizacao")
|
| 1002 |
-
placeholders.append("CURRENT_TIMESTAMP")
|
| 1003 |
-
|
| 1004 |
-
query = f"""
|
| 1005 |
-
INSERT OR REPLACE INTO contexto
|
| 1006 |
-
({', '.join(colunas)})
|
| 1007 |
-
VALUES ({', '.join(placeholders)})
|
| 1008 |
-
"""
|
| 1009 |
-
|
| 1010 |
self._execute_with_retry(
|
| 1011 |
-
|
| 1012 |
-
|
| 1013 |
-
commit=True
|
| 1014 |
-
fetch=False
|
| 1015 |
-
)
|
| 1016 |
-
|
| 1017 |
-
return True
|
| 1018 |
-
|
| 1019 |
-
except Exception as e:
|
| 1020 |
-
logger.error(f"Erro ao salvar contexto: {e}")
|
| 1021 |
-
return False
|
| 1022 |
-
|
| 1023 |
-
def salvar_giria(self, numero: str, giria: str, significado: str, contexto: str = ""):
|
| 1024 |
-
"""Salva gíria aprendida com frequência"""
|
| 1025 |
-
try:
|
| 1026 |
-
numero_final = str(numero).strip()
|
| 1027 |
-
contexto_id = self._gerar_contexto_id(numero_final, 'auto')
|
| 1028 |
-
|
| 1029 |
-
# Verifica se já existe
|
| 1030 |
-
result = self._execute_with_retry(
|
| 1031 |
-
"""
|
| 1032 |
-
SELECT id, frequencia FROM girias_aprendidas
|
| 1033 |
-
WHERE numero = ? AND giria = ?
|
| 1034 |
-
""",
|
| 1035 |
-
(numero_final, giria),
|
| 1036 |
-
fetch=True
|
| 1037 |
)
|
| 1038 |
-
|
| 1039 |
-
if result:
|
| 1040 |
-
# Atualiza frequência
|
| 1041 |
-
self._execute_with_retry(
|
| 1042 |
-
"""
|
| 1043 |
-
UPDATE girias_aprendidas
|
| 1044 |
-
SET frequencia = frequencia + 1,
|
| 1045 |
-
ultimo_uso = CURRENT_TIMESTAMP
|
| 1046 |
-
WHERE numero = ? AND giria = ?
|
| 1047 |
-
""",
|
| 1048 |
-
(numero_final, giria),
|
| 1049 |
-
commit=True,
|
| 1050 |
-
fetch=False
|
| 1051 |
-
)
|
| 1052 |
-
logger.debug(f"✅ Gíria '{giria}' atualizada (freq+1)")
|
| 1053 |
-
else:
|
| 1054 |
-
# Insere nova
|
| 1055 |
-
self._execute_with_retry(
|
| 1056 |
-
"""
|
| 1057 |
-
INSERT INTO girias_aprendidas
|
| 1058 |
-
(numero, contexto_id, giria, significado, contexto, frequencia)
|
| 1059 |
-
VALUES (?, ?, ?, ?, ?, 1)
|
| 1060 |
-
""",
|
| 1061 |
-
(numero_final, contexto_id, giria, significado, contexto[:100]),
|
| 1062 |
-
commit=True,
|
| 1063 |
-
fetch=False
|
| 1064 |
-
)
|
| 1065 |
-
logger.debug(f"✅ Nova gíria '{giria}' salva")
|
| 1066 |
-
|
| 1067 |
-
return True
|
| 1068 |
-
|
| 1069 |
except Exception as e:
|
| 1070 |
-
logger.
|
| 1071 |
-
|
| 1072 |
-
|
| 1073 |
-
def salvar_transicao_humor(self, numero: str, humor_anterior: str,
|
| 1074 |
-
humor_novo: str, emocao_trigger: str = None,
|
| 1075 |
-
confianca_emocao: float = 0.5,
|
| 1076 |
-
nivel_transicao: int = 0,
|
| 1077 |
-
razao: str = "", intensidade: float = 0.5):
|
| 1078 |
-
try:
|
| 1079 |
-
contexto_id = self._gerar_contexto_id(numero, 'auto')
|
| 1080 |
-
|
| 1081 |
self._execute_with_retry(
|
| 1082 |
-
""
|
| 1083 |
-
|
| 1084 |
-
|
| 1085 |
-
confianca_emocao, nivel_transicao, razao, intensidade)
|
| 1086 |
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
| 1087 |
-
""",
|
| 1088 |
-
(
|
| 1089 |
-
str(numero).strip(), contexto_id, humor_anterior, humor_novo,
|
| 1090 |
-
emocao_trigger, confianca_emocao, nivel_transicao,
|
| 1091 |
-
razao[:200], intensidade
|
| 1092 |
-
),
|
| 1093 |
-
commit=True,
|
| 1094 |
-
fetch=False
|
| 1095 |
-
)
|
| 1096 |
-
|
| 1097 |
-
self.salvar_contexto(
|
| 1098 |
-
numero=numero,
|
| 1099 |
-
humor_atual=humor_novo,
|
| 1100 |
-
nivel_transicao=nivel_transicao,
|
| 1101 |
-
humor_alvo=humor_novo if nivel_transicao >= 3 else humor_anterior
|
| 1102 |
)
|
| 1103 |
-
|
| 1104 |
-
logger.debug(f"🎭 Transição salva: {humor_anterior} → {humor_novo} (nivel: {nivel_transicao})")
|
| 1105 |
-
|
| 1106 |
-
except Exception as e:
|
| 1107 |
-
logger.error(f"Erro ao salvar transição: {e}")
|
| 1108 |
|
| 1109 |
# ================================================================
|
| 1110 |
-
#
|
| 1111 |
# ================================================================
|
| 1112 |
-
def salvar_transcricao_audio(self, numero_usuario: str, audio_hash: str,
|
| 1113 |
-
texto_transcrito: str, fonte: str = "deepgram",
|
| 1114 |
-
sucesso: bool = True, confianca: float = 0.5,
|
| 1115 |
-
duracao_segundos: float = 0.0) -> bool:
|
| 1116 |
-
"""Salva transcrição de áudio"""
|
| 1117 |
-
try:
|
| 1118 |
-
self._execute_with_retry(
|
| 1119 |
-
"""
|
| 1120 |
-
INSERT INTO audio_transcricoes
|
| 1121 |
-
(numero_usuario, audio_hash, texto_transcrito, fonte, sucesso, confianca, duracao_segundos)
|
| 1122 |
-
VALUES (?, ?, ?, ?, ?, ?, ?)
|
| 1123 |
-
""",
|
| 1124 |
-
(
|
| 1125 |
-
str(numero_usuario).strip(), audio_hash[:64], texto_transcrito[:5000],
|
| 1126 |
-
fonte[:50], int(sucesso), confianca, duracao_segundos
|
| 1127 |
-
),
|
| 1128 |
-
commit=True,
|
| 1129 |
-
fetch=False
|
| 1130 |
-
)
|
| 1131 |
-
logger.info(f"✅ Transcrição de áudio salva: {numero_usuario} | Sucesso: {sucesso}")
|
| 1132 |
-
return True
|
| 1133 |
-
except Exception as e:
|
| 1134 |
-
logger.error(f"❌ Erro ao salvar transcrição: {e}")
|
| 1135 |
-
return False
|
| 1136 |
|
| 1137 |
-
|
| 1138 |
-
|
| 1139 |
-
try:
|
| 1140 |
-
results = self._execute_with_retry(
|
| 1141 |
-
"""
|
| 1142 |
-
SELECT texto_transcrito, fonte, sucesso, confianca, timestamp
|
| 1143 |
-
FROM audio_transcricoes
|
| 1144 |
-
WHERE numero_usuario = ?
|
| 1145 |
-
ORDER BY timestamp DESC
|
| 1146 |
-
LIMIT ?
|
| 1147 |
-
""",
|
| 1148 |
-
(str(numero_usuario).strip(), limite),
|
| 1149 |
-
fetch=True
|
| 1150 |
-
)
|
| 1151 |
-
|
| 1152 |
-
return [
|
| 1153 |
-
{
|
| 1154 |
-
"texto": r[0],
|
| 1155 |
-
"fonte": r[1],
|
| 1156 |
-
"sucesso": bool(r[2]),
|
| 1157 |
-
"confianca": r[3],
|
| 1158 |
-
"timestamp": r[4]
|
| 1159 |
-
}
|
| 1160 |
-
for r in results
|
| 1161 |
-
]
|
| 1162 |
-
except Exception as e:
|
| 1163 |
-
logger.error(f"Erro ao recuperar histórico de áudio: {e}")
|
| 1164 |
-
return []
|
| 1165 |
-
|
| 1166 |
-
# ================================================================
|
| 1167 |
-
# NOVO MÉTODO: SALVAR EMBEDDINGS (RESOLVE O ERRO)
|
| 1168 |
-
# ================================================================
|
| 1169 |
-
def salvar_embedding(self, numero: str, texto: str, embedding: bytes) -> bool:
|
| 1170 |
-
"""
|
| 1171 |
-
Salva embedding para busca semântica.
|
| 1172 |
-
"""
|
| 1173 |
-
try:
|
| 1174 |
-
if not embedding or len(embedding) == 0:
|
| 1175 |
-
logger.warning(f"Embedding vazio para {numero}")
|
| 1176 |
-
return False
|
| 1177 |
-
|
| 1178 |
-
if len(embedding) > 1000000: # Limite de 1MB
|
| 1179 |
-
logger.warning(f"Embedding muito grande: {len(embedding)} bytes, truncando")
|
| 1180 |
-
embedding = embedding[:1000000]
|
| 1181 |
-
|
| 1182 |
-
# Cria hash do texto para evitar duplicatas
|
| 1183 |
-
hash_texto = hashlib.md5(texto.encode()).hexdigest()
|
| 1184 |
-
|
| 1185 |
-
# Verifica se já existe
|
| 1186 |
-
result = self._execute_with_retry(
|
| 1187 |
-
"SELECT 1 FROM embeddings WHERE numero = ? AND hash_texto = ?",
|
| 1188 |
-
(str(numero).strip(), hash_texto),
|
| 1189 |
-
fetch=True
|
| 1190 |
-
)
|
| 1191 |
-
|
| 1192 |
-
if result:
|
| 1193 |
-
# Atualiza existente
|
| 1194 |
-
self._execute_with_retry(
|
| 1195 |
-
"""
|
| 1196 |
-
UPDATE embeddings
|
| 1197 |
-
SET embedding = ?, timestamp = CURRENT_TIMESTAMP
|
| 1198 |
-
WHERE numero = ? AND hash_texto = ?
|
| 1199 |
-
""",
|
| 1200 |
-
(embedding, str(numero).strip(), hash_texto),
|
| 1201 |
-
commit=True,
|
| 1202 |
-
fetch=False
|
| 1203 |
-
)
|
| 1204 |
-
logger.debug(f"✅ Embedding atualizado: {numero}, texto: {texto[:50]}...")
|
| 1205 |
-
else:
|
| 1206 |
-
# Insere novo
|
| 1207 |
-
self._execute_with_retry(
|
| 1208 |
-
"""
|
| 1209 |
-
INSERT INTO embeddings
|
| 1210 |
-
(numero, texto, embedding, modelo, hash_texto)
|
| 1211 |
-
VALUES (?, ?, ?, ?, ?)
|
| 1212 |
-
""",
|
| 1213 |
-
(
|
| 1214 |
-
str(numero).strip(),
|
| 1215 |
-
texto[:1000],
|
| 1216 |
-
embedding,
|
| 1217 |
-
'MiniLM-L12-v2',
|
| 1218 |
-
hash_texto
|
| 1219 |
-
),
|
| 1220 |
-
commit=True,
|
| 1221 |
-
fetch=False
|
| 1222 |
-
)
|
| 1223 |
-
logger.debug(f"✅ Embedding salvo: {numero}, tamanho: {len(embedding)} bytes, texto: {texto[:50]}...")
|
| 1224 |
-
|
| 1225 |
-
return True
|
| 1226 |
-
|
| 1227 |
-
except Exception as e:
|
| 1228 |
-
logger.error(f"❌ Erro ao salvar embedding: {e}")
|
| 1229 |
-
return False
|
| 1230 |
-
|
| 1231 |
-
# ================================================================
|
| 1232 |
-
# MÉTODO PARA RECUPERAR EMBEDDINGS
|
| 1233 |
-
# ================================================================
|
| 1234 |
-
def recuperar_embeddings(self, numero: str, limite: int = 20) -> List[Dict]:
|
| 1235 |
-
"""Recupera embeddings do usuário"""
|
| 1236 |
-
try:
|
| 1237 |
-
results = self._execute_with_retry(
|
| 1238 |
-
"""
|
| 1239 |
-
SELECT texto, embedding, modelo, timestamp
|
| 1240 |
-
FROM embeddings
|
| 1241 |
-
WHERE numero = ?
|
| 1242 |
-
ORDER BY timestamp DESC
|
| 1243 |
-
LIMIT ?
|
| 1244 |
-
""",
|
| 1245 |
-
(str(numero).strip(), limite),
|
| 1246 |
-
fetch=True
|
| 1247 |
-
)
|
| 1248 |
-
|
| 1249 |
-
embeddings = []
|
| 1250 |
-
for r in results:
|
| 1251 |
-
try:
|
| 1252 |
-
embeddings.append({
|
| 1253 |
-
"texto": r[0],
|
| 1254 |
-
"embedding": r[1], # bytes
|
| 1255 |
-
"modelo": r[2],
|
| 1256 |
-
"timestamp": r[3]
|
| 1257 |
-
})
|
| 1258 |
-
except:
|
| 1259 |
-
continue
|
| 1260 |
-
|
| 1261 |
-
return embeddings
|
| 1262 |
-
|
| 1263 |
-
except Exception as e:
|
| 1264 |
-
logger.error(f"Erro ao recuperar embeddings: {e}")
|
| 1265 |
-
return []
|
| 1266 |
-
|
| 1267 |
-
# ================================================================
|
| 1268 |
-
# MÉTODOS FALTANTES ADICIONADOS (RESOLVEM OS ERROS)
|
| 1269 |
-
# ================================================================
|
| 1270 |
-
def salvar_training_example(self, input_text: str, output_text: str,
|
| 1271 |
-
humor: str = "normal_ironico",
|
| 1272 |
-
modo_resposta: str = "normal_ironico",
|
| 1273 |
-
emocao_contexto: str = None,
|
| 1274 |
-
qualidade_score: float = 1.0) -> bool:
|
| 1275 |
try:
|
| 1276 |
self._execute_with_retry(
|
| 1277 |
-
"""
|
| 1278 |
-
|
| 1279 |
-
|
| 1280 |
-
|
| 1281 |
-
|
| 1282 |
-
(input_text[:2000], output_text[:2000], humor, modo_resposta, emocao_contexto, qualidade_score),
|
| 1283 |
-
commit=True,
|
| 1284 |
-
fetch=False
|
| 1285 |
)
|
| 1286 |
-
logger.info(f"✅ Training example salvo | Score: {qualidade_score:.2f}")
|
| 1287 |
-
return True
|
| 1288 |
except Exception as e:
|
| 1289 |
-
logger.error(f"
|
| 1290 |
-
|
| 1291 |
-
|
| 1292 |
-
def
|
| 1293 |
-
|
| 1294 |
-
|
| 1295 |
-
|
| 1296 |
-
|
| 1297 |
-
|
| 1298 |
-
|
| 1299 |
-
|
| 1300 |
-
|
| 1301 |
-
|
| 1302 |
-
|
| 1303 |
-
|
| 1304 |
-
|
| 1305 |
-
|
| 1306 |
-
|
| 1307 |
-
|
| 1308 |
-
|
| 1309 |
-
|
| 1310 |
-
|
| 1311 |
-
|
| 1312 |
-
|
| 1313 |
-
|
| 1314 |
-
|
| 1315 |
-
|
| 1316 |
-
|
| 1317 |
-
|
| 1318 |
-
|
| 1319 |
-
|
| 1320 |
-
|
| 1321 |
-
|
| 1322 |
-
|
| 1323 |
-
|
| 1324 |
-
|
| 1325 |
-
|
| 1326 |
-
|
| 1327 |
-
|
| 1328 |
-
|
| 1329 |
-
|
| 1330 |
-
|
| 1331 |
-
|
| 1332 |
-
|
| 1333 |
-
|
| 1334 |
-
# ================================================================
|
| 1335 |
-
def salvar_aprendizado_detalhado(self, input_text: str, output_text: str,
|
| 1336 |
-
contexto: Dict, qualidade_score: float = 1.0,
|
| 1337 |
-
tipo_aprendizado: str = "reply_padrao",
|
| 1338 |
-
metadata: Dict = None) -> bool:
|
| 1339 |
-
"""
|
| 1340 |
-
Salva aprendizado detalhado de padrões de conversa.
|
| 1341 |
-
CORREÇÃO: Remove **kwargs que causavam erro de compatibilidade.
|
| 1342 |
-
"""
|
| 1343 |
-
try:
|
| 1344 |
self._execute_with_retry(
|
| 1345 |
-
""
|
| 1346 |
-
|
| 1347 |
-
|
| 1348 |
-
VALUES (?, ?, ?, ?, ?, ?)
|
| 1349 |
-
""",
|
| 1350 |
-
(
|
| 1351 |
-
input_text[:2000],
|
| 1352 |
-
output_text[:2000],
|
| 1353 |
-
contexto.get("humor_atualizado", "normal_ironico"),
|
| 1354 |
-
contexto.get("modo_resposta", "normal_ironico"),
|
| 1355 |
-
contexto.get("emocao_primaria", "neutral"),
|
| 1356 |
-
qualidade_score
|
| 1357 |
-
),
|
| 1358 |
-
commit=True,
|
| 1359 |
-
fetch=False
|
| 1360 |
)
|
| 1361 |
-
|
| 1362 |
-
return True
|
| 1363 |
-
except Exception as e:
|
| 1364 |
-
logger.error(f"❌ Erro ao salvar aprendizado detalhado: {e}")
|
| 1365 |
-
return False
|
| 1366 |
-
|
| 1367 |
-
# ================================================================
|
| 1368 |
-
# NOVO MÉTODO: salvar_analise_padrao (ADICIONADO PARA TREINAMENTO)
|
| 1369 |
-
# ================================================================
|
| 1370 |
-
def salvar_analise_padrao(self, numero: str, tipo_conversa: str, padrao: str,
|
| 1371 |
-
exemplo_mensagem: str, exemplo_resposta: str,
|
| 1372 |
-
contexto: str = None) -> bool:
|
| 1373 |
-
"""Salva análise de padrão detectado"""
|
| 1374 |
-
try:
|
| 1375 |
-
contexto_id = self._gerar_contexto_id(numero, 'auto')
|
| 1376 |
-
|
| 1377 |
self._execute_with_retry(
|
| 1378 |
-
""
|
| 1379 |
-
|
| 1380 |
-
|
| 1381 |
-
tipo_mensagem, humor, modo_resposta, emocao_detectada)
|
| 1382 |
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
| 1383 |
-
""",
|
| 1384 |
-
(
|
| 1385 |
-
"SISTEMA_PADRAO", f"[PADRÃO: {padrao}] {exemplo_mensagem}",
|
| 1386 |
-
exemplo_resposta, str(numero).strip(), contexto_id, tipo_conversa,
|
| 1387 |
-
'texto', 'normal_ironico', 'normal_ironico', 'neutral'
|
| 1388 |
-
),
|
| 1389 |
-
commit=True,
|
| 1390 |
-
fetch=False
|
| 1391 |
)
|
| 1392 |
-
|
| 1393 |
-
return True
|
| 1394 |
-
except Exception as e:
|
| 1395 |
-
logger.warning(f"Erro ao salvar análise de padrão: {e}")
|
| 1396 |
-
return False
|
| 1397 |
|
| 1398 |
-
|
| 1399 |
-
|
| 1400 |
-
|
| 1401 |
-
|
| 1402 |
-
|
| 1403 |
-
|
| 1404 |
-
# Conta mensagens
|
| 1405 |
-
result = self._execute_with_retry(
|
| 1406 |
-
"SELECT COUNT(*) FROM mensagens WHERE numero = ? AND deletado = 0",
|
| 1407 |
-
(str(numero).strip(),),
|
| 1408 |
-
fetch=True
|
| 1409 |
-
)
|
| 1410 |
-
total_mensagens = result[0][0] if result else 0
|
| 1411 |
-
|
| 1412 |
-
# Última atividade
|
| 1413 |
-
result = self._execute_with_retry(
|
| 1414 |
-
"SELECT MAX(timestamp) FROM mensagens WHERE numero = ?",
|
| 1415 |
-
(str(numero).strip(),),
|
| 1416 |
-
fetch=True
|
| 1417 |
-
)
|
| 1418 |
-
ultima_atividade = result[0][0] if result else None
|
| 1419 |
-
|
| 1420 |
-
# Transições de humor
|
| 1421 |
-
result = self._execute_with_retry(
|
| 1422 |
-
"SELECT COUNT(*) FROM transicoes_humor WHERE numero = ?",
|
| 1423 |
-
(str(numero).strip(),),
|
| 1424 |
-
fetch=True
|
| 1425 |
-
)
|
| 1426 |
-
transicoes_humor = result[0][0] if result else 0
|
| 1427 |
-
|
| 1428 |
-
# Gírias aprendidas
|
| 1429 |
-
result = self._execute_with_retry(
|
| 1430 |
-
"SELECT COUNT(*) FROM girias_aprendidas WHERE numero = ?",
|
| 1431 |
-
(str(numero).strip(),),
|
| 1432 |
-
fetch=True
|
| 1433 |
-
)
|
| 1434 |
-
girias_aprendidas = result[0][0] if result else 0
|
| 1435 |
-
|
| 1436 |
-
# Embeddings salvos
|
| 1437 |
-
result = self._execute_with_retry(
|
| 1438 |
-
"SELECT COUNT(*) FROM embeddings WHERE numero = ?",
|
| 1439 |
-
(str(numero).strip(),),
|
| 1440 |
-
fetch=True
|
| 1441 |
-
)
|
| 1442 |
-
embeddings_salvos = result[0][0] if result else 0
|
| 1443 |
-
|
| 1444 |
-
return {
|
| 1445 |
-
"total_mensagens": total_mensagens,
|
| 1446 |
-
"ultima_atividade": ultima_atividade,
|
| 1447 |
-
"transicoes_humor": transicoes_humor,
|
| 1448 |
-
"girias_aprendidas": girias_aprendidas,
|
| 1449 |
-
"embeddings_salvos": embeddings_salvos,
|
| 1450 |
-
"privilegiado": self.is_usuario_privilegiado(numero)
|
| 1451 |
-
}
|
| 1452 |
-
|
| 1453 |
-
except Exception as e:
|
| 1454 |
-
logger.error(f"Erro ao obter estatísticas: {e}")
|
| 1455 |
-
return {
|
| 1456 |
-
"total_mensagens": 0,
|
| 1457 |
-
"ultima_atividade": None,
|
| 1458 |
-
"transicoes_humor": 0,
|
| 1459 |
-
"girias_aprendidas": 0,
|
| 1460 |
-
"embeddings_salvos": 0,
|
| 1461 |
-
"privilegiado": False
|
| 1462 |
-
}
|
| 1463 |
-
|
| 1464 |
-
def limpar_mensagens_antigas(self, dias: int = 30) -> int:
|
| 1465 |
-
"""Limpa mensagens antigas"""
|
| 1466 |
-
try:
|
| 1467 |
-
result = self._execute_with_retry(
|
| 1468 |
-
"""
|
| 1469 |
-
DELETE FROM mensagens
|
| 1470 |
-
WHERE timestamp < datetime('now', ?)
|
| 1471 |
-
AND deletado = 0
|
| 1472 |
-
""",
|
| 1473 |
-
(f'-{dias} days',),
|
| 1474 |
-
commit=True,
|
| 1475 |
-
fetch=False
|
| 1476 |
-
)
|
| 1477 |
-
logger.info(f"🧹 Mensagens antigas ({dias} dias) limpas: {result} registros")
|
| 1478 |
-
return result
|
| 1479 |
-
except Exception as e:
|
| 1480 |
-
logger.error(f"Erro ao limpar mensagens antigas: {e}")
|
| 1481 |
-
return 0
|
| 1482 |
-
|
| 1483 |
-
# ================================================================
|
| 1484 |
-
# MÉTODO PARA LIMPAR EMBEDDINGS ANTIGOS
|
| 1485 |
-
# ================================================================
|
| 1486 |
-
def limpar_embeddings_antigos(self, dias: int = 90) -> int:
|
| 1487 |
-
"""Limpa embeddings antigos"""
|
| 1488 |
-
try:
|
| 1489 |
-
result = self._execute_with_retry(
|
| 1490 |
-
"""
|
| 1491 |
-
DELETE FROM embeddings
|
| 1492 |
-
WHERE timestamp < datetime('now', ?)
|
| 1493 |
-
""",
|
| 1494 |
-
(f'-{dias} days',),
|
| 1495 |
-
commit=True,
|
| 1496 |
-
fetch=False
|
| 1497 |
-
)
|
| 1498 |
-
logger.info(f"🧹 Embeddings antigos ({dias} dias) limpos: {result} registros")
|
| 1499 |
-
return result
|
| 1500 |
-
except Exception as e:
|
| 1501 |
-
logger.error(f"Erro ao limpar embeddings antigos: {e}")
|
| 1502 |
-
return 0
|
| 1503 |
-
|
| 1504 |
-
# ================================================================
|
| 1505 |
-
# FECHAMENTO SEGURO DA CONEXÃO
|
| 1506 |
-
# ================================================================
|
| 1507 |
-
def close(self):
|
| 1508 |
-
try:
|
| 1509 |
-
if hasattr(self, 'conn') and self.conn:
|
| 1510 |
-
self.conn.close()
|
| 1511 |
-
logger.info("Conexão com banco fechada com segurança.")
|
| 1512 |
-
except:
|
| 1513 |
-
pass
|
| 1514 |
|
| 1515 |
-
|
| 1516 |
-
|
| 1517 |
-
|
| 1518 |
-
atexit.register(db_instance.close)
|
|
|
|
|
|
|
| 1 |
"""
|
| 2 |
+
Banco de dados SQLite para Akira IA.
|
| 3 |
+
Gerencia contexto, mensagens, embeddings, gírias, tom e aprendizados detalhados.
|
| 4 |
+
Versão completa 11/2025.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
"""
|
| 6 |
+
|
| 7 |
import sqlite3
|
| 8 |
import time
|
| 9 |
import os
|
| 10 |
import json
|
|
|
|
|
|
|
|
|
|
| 11 |
from typing import Optional, List, Dict, Any, Tuple
|
| 12 |
from loguru import logger
|
| 13 |
+
|
| 14 |
|
| 15 |
class Database:
|
| 16 |
+
def __init__(self, db_path: str):
|
| 17 |
+
self.db_path = db_path
|
| 18 |
self.max_retries = 5
|
| 19 |
self.retry_delay = 0.1
|
| 20 |
+
os.makedirs(os.path.dirname(db_path), exist_ok=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
self._init_db()
|
| 22 |
self._ensure_all_columns_and_indexes()
|
|
|
|
|
|
|
|
|
|
| 23 |
|
| 24 |
# ================================================================
|
| 25 |
+
# CONEXÃO COM RETRY + WAL
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
# ================================================================
|
|
|
|
|
|
|
|
|
|
| 27 |
def _get_connection(self) -> sqlite3.Connection:
|
| 28 |
for attempt in range(self.max_retries):
|
| 29 |
try:
|
| 30 |
conn = sqlite3.connect(self.db_path, timeout=30.0, check_same_thread=False)
|
| 31 |
conn.execute('PRAGMA journal_mode=WAL')
|
| 32 |
conn.execute('PRAGMA synchronous=NORMAL')
|
| 33 |
+
conn.execute('PRAGMA cache_size=1000')
|
| 34 |
conn.execute('PRAGMA temp_store=MEMORY')
|
| 35 |
+
conn.execute('PRAGMA busy_timeout=30000')
|
| 36 |
conn.execute('PRAGMA foreign_keys=ON')
|
|
|
|
| 37 |
return conn
|
| 38 |
except sqlite3.OperationalError as e:
|
| 39 |
if "database is locked" in str(e) and attempt < self.max_retries - 1:
|
| 40 |
time.sleep(self.retry_delay * (2 ** attempt))
|
| 41 |
continue
|
| 42 |
+
logger.error(f"Falha ao conectar ao banco: {e}")
|
| 43 |
raise
|
| 44 |
+
raise sqlite3.OperationalError("Falha ao conectar após retries")
|
| 45 |
|
| 46 |
+
def _execute_with_retry(self, query: str, params: Optional[tuple] = None, commit: bool = False) -> Optional[List[Tuple]]:
|
|
|
|
| 47 |
for attempt in range(self.max_retries):
|
| 48 |
try:
|
| 49 |
with self._get_connection() as conn:
|
|
|
|
| 52 |
c.execute(query, params)
|
| 53 |
else:
|
| 54 |
c.execute(query)
|
| 55 |
+
result = c.fetchall() if query.strip().upper().startswith('SELECT') else None
|
| 56 |
if commit:
|
| 57 |
conn.commit()
|
| 58 |
+
return result
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
except sqlite3.OperationalError as e:
|
| 60 |
if "database is locked" in str(e) and attempt < self.max_retries - 1:
|
| 61 |
time.sleep(self.retry_delay * (2 ** attempt))
|
| 62 |
continue
|
| 63 |
logger.error(f"Erro SQL (tentativa {attempt+1}): {e}")
|
| 64 |
raise
|
| 65 |
+
raise sqlite3.OperationalError("Query falhou após retries")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
|
| 67 |
# ================================================================
|
| 68 |
+
# INICIALIZAÇÃO + MIGRAÇÃO AUTOMÁTICA
|
| 69 |
# ================================================================
|
|
|
|
| 70 |
def _init_db(self):
|
| 71 |
try:
|
| 72 |
with self._get_connection() as conn:
|
| 73 |
c = conn.cursor()
|
| 74 |
+
c.executescript('''
|
| 75 |
+
CREATE TABLE IF NOT EXISTS aprendizado (
|
| 76 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 77 |
+
usuario TEXT,
|
| 78 |
+
dado TEXT,
|
| 79 |
+
valor TEXT
|
| 80 |
+
);
|
| 81 |
+
CREATE TABLE IF NOT EXISTS exemplos (
|
| 82 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 83 |
+
tipo TEXT NOT NULL,
|
| 84 |
+
entrada TEXT NOT NULL,
|
| 85 |
+
resposta TEXT NOT NULL
|
| 86 |
+
);
|
| 87 |
+
CREATE TABLE IF NOT EXISTS info_geral (
|
| 88 |
+
chave TEXT PRIMARY KEY,
|
| 89 |
+
valor TEXT NOT NULL
|
| 90 |
+
);
|
| 91 |
+
CREATE TABLE IF NOT EXISTS estilos (
|
| 92 |
+
numero_usuario TEXT PRIMARY KEY,
|
| 93 |
+
estilo TEXT NOT NULL
|
| 94 |
+
);
|
| 95 |
+
CREATE TABLE IF NOT EXISTS preferencias_tom (
|
| 96 |
+
numero_usuario TEXT PRIMARY KEY,
|
| 97 |
+
tom TEXT NOT NULL
|
| 98 |
+
);
|
| 99 |
+
CREATE TABLE IF NOT EXISTS afinidades (
|
| 100 |
+
numero_usuario TEXT PRIMARY KEY,
|
| 101 |
+
afinidade REAL NOT NULL
|
| 102 |
+
);
|
| 103 |
+
CREATE TABLE IF NOT EXISTS termos (
|
| 104 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 105 |
+
numero_usuario TEXT NOT NULL,
|
| 106 |
+
termo TEXT NOT NULL,
|
| 107 |
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
| 108 |
+
);
|
| 109 |
+
CREATE TABLE IF NOT EXISTS aprendizados (
|
| 110 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 111 |
+
numero_usuario TEXT NOT NULL,
|
| 112 |
+
chave TEXT NOT NULL,
|
| 113 |
+
valor TEXT NOT NULL,
|
| 114 |
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
| 115 |
+
);
|
| 116 |
+
CREATE TABLE IF NOT EXISTS vocabulario_patenteado (
|
| 117 |
+
termo TEXT PRIMARY KEY,
|
| 118 |
+
definicao TEXT NOT NULL,
|
| 119 |
+
uso TEXT NOT NULL,
|
| 120 |
+
exemplo TEXT NOT NULL
|
| 121 |
+
);
|
| 122 |
+
CREATE TABLE IF NOT EXISTS usuarios_privilegiados (
|
| 123 |
+
numero_usuario TEXT PRIMARY KEY,
|
| 124 |
+
nome TEXT NOT NULL
|
| 125 |
+
);
|
| 126 |
+
CREATE TABLE IF NOT EXISTS whatsapp_ids (
|
| 127 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 128 |
+
whatsapp_id TEXT NOT NULL,
|
| 129 |
+
sender_number TEXT NOT NULL,
|
| 130 |
+
UNIQUE (whatsapp_id, sender_number)
|
| 131 |
+
);
|
| 132 |
+
CREATE TABLE IF NOT EXISTS embeddings (
|
| 133 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 134 |
+
texto TEXT NOT NULL,
|
| 135 |
+
embedding BLOB NOT NULL
|
| 136 |
+
);
|
| 137 |
CREATE TABLE IF NOT EXISTS mensagens (
|
| 138 |
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 139 |
usuario TEXT NOT NULL,
|
|
|
|
| 140 |
mensagem TEXT NOT NULL,
|
| 141 |
resposta TEXT NOT NULL,
|
| 142 |
+
numero TEXT,
|
|
|
|
|
|
|
|
|
|
| 143 |
is_reply BOOLEAN DEFAULT 0,
|
| 144 |
mensagem_original TEXT,
|
| 145 |
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
| 146 |
+
);
|
| 147 |
+
CREATE TABLE IF NOT EXISTS emocao_exemplos (
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 148 |
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 149 |
+
emocao TEXT NOT NULL,
|
| 150 |
+
entrada TEXT NOT NULL,
|
| 151 |
+
resposta TEXT NOT NULL,
|
| 152 |
+
tom TEXT NOT NULL,
|
| 153 |
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
| 154 |
+
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 155 |
CREATE TABLE IF NOT EXISTS girias_aprendidas (
|
| 156 |
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 157 |
+
numero_usuario TEXT NOT NULL,
|
|
|
|
| 158 |
giria TEXT NOT NULL,
|
| 159 |
significado TEXT NOT NULL,
|
| 160 |
contexto TEXT,
|
| 161 |
frequencia INTEGER DEFAULT 1,
|
| 162 |
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
| 163 |
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
| 164 |
+
);
|
| 165 |
+
CREATE TABLE IF NOT EXISTS tom_usuario (
|
|
|
|
|
|
|
|
|
|
|
|
|
| 166 |
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 167 |
numero_usuario TEXT NOT NULL,
|
| 168 |
+
tom_detectado TEXT NOT NULL,
|
| 169 |
+
intensidade REAL DEFAULT 0.5,
|
| 170 |
+
contexto TEXT,
|
| 171 |
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
| 172 |
+
);
|
| 173 |
+
CREATE TABLE IF NOT EXISTS adaptacao_dinamica (
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 174 |
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 175 |
+
numero_usuario TEXT NOT NULL,
|
| 176 |
+
tipo_adaptacao TEXT NOT NULL,
|
| 177 |
+
valor_anterior TEXT,
|
| 178 |
+
valor_novo TEXT,
|
| 179 |
+
razao TEXT,
|
| 180 |
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
| 181 |
+
);
|
| 182 |
+
CREATE TABLE IF NOT EXISTS pronomes_por_tom (
|
| 183 |
+
tom TEXT PRIMARY KEY,
|
| 184 |
+
pronomes TEXT NOT NULL
|
| 185 |
+
);
|
| 186 |
+
CREATE TABLE IF NOT EXISTS contexto (
|
| 187 |
+
user_key TEXT PRIMARY KEY,
|
| 188 |
+
historico TEXT,
|
| 189 |
+
emocao_atual TEXT,
|
| 190 |
+
termos TEXT,
|
| 191 |
+
girias TEXT,
|
| 192 |
+
tom TEXT
|
| 193 |
+
);
|
| 194 |
''')
|
| 195 |
+
c.executescript('''
|
| 196 |
+
INSERT OR IGNORE INTO pronomes_por_tom (tom, pronomes) VALUES
|
| 197 |
+
('formal', 'Sr., ilustre, boss, maior, homem'),
|
| 198 |
+
('rude', 'parvo, estúpido, burro, analfabeto, desperdício de esperma'),
|
| 199 |
+
('casual', 'mano, puto, cota, mwangolé, kota'),
|
| 200 |
+
('neutro', 'amigo, parceiro, camarada');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 201 |
''')
|
|
|
|
| 202 |
conn.commit()
|
| 203 |
+
logger.info(f"Banco inicializado: {self.db_path}")
|
|
|
|
|
|
|
| 204 |
except Exception as e:
|
| 205 |
+
logger.error(f"Erro ao criar tabelas: {e}")
|
| 206 |
raise
|
| 207 |
|
| 208 |
+
def _ensure_all_columns_and_indexes(self):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 209 |
try:
|
| 210 |
+
with self._get_connection() as conn:
|
| 211 |
+
c = conn.cursor()
|
| 212 |
+
migrations = {
|
| 213 |
+
'mensagens': [
|
| 214 |
+
("numero", "TEXT"),
|
| 215 |
+
("is_reply", "BOOLEAN DEFAULT 0"),
|
| 216 |
+
("mensagem_original", "TEXT"),
|
| 217 |
+
("created_at", "DATETIME DEFAULT CURRENT_TIMESTAMP")
|
| 218 |
+
],
|
| 219 |
+
'girias_aprendidas': [
|
| 220 |
+
("contexto", "TEXT"),
|
| 221 |
+
("frequencia", "INTEGER DEFAULT 1"),
|
| 222 |
+
("updated_at", "DATETIME DEFAULT CURRENT_TIMESTAMP")
|
| 223 |
+
],
|
| 224 |
+
'tom_usuario': [
|
| 225 |
+
("intensidade", "REAL DEFAULT 0.5"),
|
| 226 |
+
("contexto", "TEXT")
|
| 227 |
+
],
|
| 228 |
+
'contexto': [
|
| 229 |
+
("historico", "TEXT"),
|
| 230 |
+
("emocao_atual", "TEXT"),
|
| 231 |
+
("termos", "TEXT"),
|
| 232 |
+
("girias", "TEXT"),
|
| 233 |
+
("tom", "TEXT")
|
| 234 |
+
],
|
| 235 |
+
# CORREÇÃO: Adiciona as colunas que faltavam em 'embeddings'
|
| 236 |
+
'embeddings': [
|
| 237 |
+
("numero_usuario", "TEXT"),
|
| 238 |
+
("source_type", "TEXT")
|
| 239 |
+
]
|
| 240 |
}
|
| 241 |
+
for table, cols in migrations.items():
|
| 242 |
+
c.execute(f"PRAGMA table_info('{table}')")
|
| 243 |
+
existing = {row[1] for row in c.fetchall()}
|
| 244 |
+
for col_name, col_def in cols:
|
| 245 |
+
if col_name not in existing:
|
| 246 |
+
try:
|
| 247 |
+
c.execute(f"ALTER TABLE {table} ADD COLUMN {col_name} {col_def}")
|
| 248 |
+
logger.info(f"Coluna '{col_name}' adicionada em '{table}'")
|
| 249 |
+
except Exception as e:
|
| 250 |
+
logger.warning(f"Erro ao adicionar coluna {col_name}: {e}")
|
| 251 |
+
indexes = [
|
| 252 |
+
"CREATE INDEX IF NOT EXISTS idx_mensagens_numero ON mensagens(numero);",
|
| 253 |
+
"CREATE INDEX IF NOT EXISTS idx_mensagens_created ON mensagens(created_at DESC);",
|
| 254 |
+
"CREATE INDEX IF NOT EXISTS idx_girias_usuario ON girias_aprendidas(numero_usuario);",
|
| 255 |
+
"CREATE INDEX IF NOT EXISTS idx_girias_giria ON girias_aprendidas(giria);",
|
| 256 |
+
"CREATE INDEX IF NOT EXISTS idx_tom_usuario ON tom_usuario(numero_usuario);",
|
| 257 |
+
"CREATE INDEX IF NOT EXISTS idx_aprendizados_usuario ON aprendizados(numero_usuario);",
|
| 258 |
+
"CREATE INDEX IF NOT EXISTS idx_embeddings_texto ON embeddings(texto);",
|
| 259 |
+
"CREATE INDEX IF NOT EXISTS idx_pronomes_tom ON pronomes_por_tom(tom);",
|
| 260 |
+
"CREATE INDEX IF NOT EXISTS idx_contexto_user ON contexto(user_key);"
|
| 261 |
+
]
|
| 262 |
+
for idx in indexes:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 263 |
try:
|
| 264 |
+
c.execute(idx)
|
| 265 |
except:
|
| 266 |
+
pass
|
| 267 |
+
conn.commit()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 268 |
except Exception as e:
|
| 269 |
+
logger.error(f"Erro na migração/índices: {e}")
|
| 270 |
+
|
| 271 |
+
# ================================================================
|
| 272 |
+
# MÉTODOS PRINCIPAIS
|
| 273 |
+
# ================================================================
|
| 274 |
+
def salvar_mensagem(self, usuario, mensagem, resposta, numero=None, is_reply=False, mensagem_original=None):
|
| 275 |
+
try:
|
| 276 |
+
cols = ['usuario', 'mensagem', 'resposta']
|
| 277 |
+
vals = [usuario, mensagem, resposta]
|
| 278 |
+
if numero:
|
| 279 |
+
cols.append('numero')
|
| 280 |
+
vals.append(numero)
|
| 281 |
+
if is_reply is not None:
|
| 282 |
+
cols.append('is_reply')
|
| 283 |
+
vals.append(int(is_reply))
|
| 284 |
+
if mensagem_original:
|
| 285 |
+
cols.append('mensagem_original')
|
| 286 |
+
vals.append(mensagem_original)
|
| 287 |
+
placeholders = ', '.join(['?' for _ in cols])
|
| 288 |
+
query = f"INSERT INTO mensagens ({', '.join(cols)}) VALUES ({placeholders})"
|
| 289 |
+
self._execute_with_retry(query, tuple(vals), commit=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 290 |
except Exception as e:
|
| 291 |
+
logger.warning(f"Fallback salvar_mensagem: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 292 |
self._execute_with_retry(
|
| 293 |
+
"INSERT INTO mensagens (usuario, mensagem, resposta) VALUES (?, ?, ?)",
|
| 294 |
+
(usuario, mensagem, resposta),
|
| 295 |
+
commit=True
|
| 296 |
)
|
|
|
|
|
|
|
|
|
|
| 297 |
|
| 298 |
+
def recuperar_mensagens(self, usuario: str, limite: int = 5) -> List[Tuple]:
|
| 299 |
+
return self._execute_with_retry(
|
| 300 |
+
"SELECT mensagem, resposta FROM mensagens WHERE usuario=? OR numero=? ORDER BY id DESC LIMIT ?",
|
| 301 |
+
(usuario, usuario, limite)
|
| 302 |
+
) or []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 303 |
|
| 304 |
+
# CORREÇÃO: Assinatura de 5 argumentos (self + 4) para corresponder ao erro do log
|
| 305 |
+
def salvar_embedding(self, numero_usuario: str, source_type: str, texto: str, embedding: bytes):
|
| 306 |
+
"""Compatível com paraphrase-MiniLM e numpy arrays."""
|
| 307 |
try:
|
| 308 |
+
if hasattr(embedding, "tobytes"):
|
| 309 |
+
embedding = embedding.tobytes()
|
| 310 |
+
# Inserindo com as novas colunas
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 311 |
self._execute_with_retry(
|
| 312 |
+
"INSERT INTO embeddings (numero_usuario, source_type, texto, embedding) VALUES (?, ?, ?, ?)",
|
| 313 |
+
(numero_usuario, source_type, texto, embedding),
|
| 314 |
+
commit=True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 315 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 316 |
except Exception as e:
|
| 317 |
+
logger.warning(f"Erro ao salvar embedding (tentativa com 4 args): {e}. Tentando com 2 argumentos (texto, embedding).")
|
| 318 |
+
# Fallback para schema antigo, caso as colunas ainda não tenham migrado
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 319 |
self._execute_with_retry(
|
| 320 |
+
"INSERT INTO embeddings (texto, embedding) VALUES (?, ?)",
|
| 321 |
+
(texto, embedding.tobytes() if hasattr(embedding, "tobytes") else embedding),
|
| 322 |
+
commit=True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 323 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 324 |
|
| 325 |
# ================================================================
|
| 326 |
+
# CONTEXTO / TOM / GÍRIAS / APRENDIZADOS
|
| 327 |
# ================================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 328 |
|
| 329 |
+
# CORREÇÃO: Método adicionado para resolver o erro "'Database' object has no attribute 'salvar_contexto'"
|
| 330 |
+
def salvar_contexto(self, user_key: str, historico: str, emocao_atual: str, termos: str, girias: str, tom: str):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 331 |
try:
|
| 332 |
self._execute_with_retry(
|
| 333 |
+
"""INSERT OR REPLACE INTO contexto
|
| 334 |
+
(user_key, historico, emocao_atual, termos, girias, tom)
|
| 335 |
+
VALUES (?, ?, ?, ?, ?, ?)""",
|
| 336 |
+
(user_key, historico, emocao_atual, termos, girias, tom),
|
| 337 |
+
commit=True
|
|
|
|
|
|
|
|
|
|
| 338 |
)
|
|
|
|
|
|
|
| 339 |
except Exception as e:
|
| 340 |
+
logger.error(f"Erro ao salvar contexto para {user_key}: {e}")
|
| 341 |
+
|
| 342 |
+
# CORREÇÃO: Aceita *args para ignorar o argumento extra (resolve "takes 2 positional arguments but 3 were given")
|
| 343 |
+
def recuperar_aprendizado_detalhado(self, numero_usuario: str, *args) -> Dict[str, str]:
|
| 344 |
+
# O argumento 'chave' (em *args) é ignorado aqui, pois a query busca todas as chaves
|
| 345 |
+
rows = self._execute_with_retry(
|
| 346 |
+
"SELECT chave, valor FROM aprendizados WHERE numero_usuario=?",
|
| 347 |
+
(numero_usuario,)
|
| 348 |
+
) or []
|
| 349 |
+
return {r[0]: r[1] for r in rows}
|
| 350 |
+
|
| 351 |
+
def recuperar_girias_usuario(self, numero_usuario: str) -> List[Dict[str, Any]]:
|
| 352 |
+
rows = self._execute_with_retry(
|
| 353 |
+
"SELECT giria, significado, contexto, frequencia FROM girias_aprendidas WHERE numero_usuario=?",
|
| 354 |
+
(numero_usuario,)
|
| 355 |
+
) or []
|
| 356 |
+
return [{'giria': r[0], 'significado': r[1], 'contexto': r[2], 'frequencia': r[3]} for r in rows]
|
| 357 |
+
|
| 358 |
+
def obter_tom_predominante(self, numero_usuario: str) -> Optional[str]:
|
| 359 |
+
rows = self._execute_with_retry(
|
| 360 |
+
"SELECT tom_detectado FROM tom_usuario WHERE numero_usuario=? ORDER BY created_at DESC LIMIT 1",
|
| 361 |
+
(numero_usuario,)
|
| 362 |
+
) or []
|
| 363 |
+
return rows[0][0] if rows else None
|
| 364 |
+
|
| 365 |
+
def registrar_tom_usuario(self, numero_usuario: str, tom_detectado: str, intensidade: float = 0.5, contexto: Optional[str] = None):
|
| 366 |
+
self._execute_with_retry(
|
| 367 |
+
"INSERT INTO tom_usuario (numero_usuario, tom_detectado, intensidade, contexto) VALUES (?, ?, ?, ?)",
|
| 368 |
+
(numero_usuario, tom_detectado, intensidade, contexto),
|
| 369 |
+
commit=True
|
| 370 |
+
)
|
| 371 |
+
|
| 372 |
+
def salvar_aprendizado_detalhado(self, numero_usuario: str, chave: str, valor: str):
|
| 373 |
+
self._execute_with_retry(
|
| 374 |
+
"INSERT OR REPLACE INTO aprendizados (numero_usuario, chave, valor) VALUES (?, ?, ?)",
|
| 375 |
+
(numero_usuario, chave, valor),
|
| 376 |
+
commit=True
|
| 377 |
+
)
|
| 378 |
+
|
| 379 |
+
def salvar_giria_aprendida(self, numero_usuario: str, giria: str, significado: str, contexto: Optional[str] = None):
|
| 380 |
+
existing = self._execute_with_retry(
|
| 381 |
+
"SELECT id, frequencia FROM girias_aprendidas WHERE numero_usuario=? AND giria=?",
|
| 382 |
+
(numero_usuario, giria)
|
| 383 |
+
)
|
| 384 |
+
if existing:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 385 |
self._execute_with_retry(
|
| 386 |
+
"UPDATE girias_aprendidas SET frequencia=frequencia+1, updated_at=CURRENT_TIMESTAMP WHERE id=?",
|
| 387 |
+
(existing[0][0],),
|
| 388 |
+
commit=True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 389 |
)
|
| 390 |
+
else:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 391 |
self._execute_with_retry(
|
| 392 |
+
"INSERT INTO girias_aprendidas (numero_usuario, giria, significado, contexto) VALUES (?, ?, ?, ?)",
|
| 393 |
+
(numero_usuario, giria, significado, contexto),
|
| 394 |
+
commit=True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 395 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 396 |
|
| 397 |
+
def salvar_info_geral(self, chave: str, valor: str):
|
| 398 |
+
self._execute_with_retry(
|
| 399 |
+
"INSERT OR REPLACE INTO info_geral (chave, valor) VALUES (?, ?)",
|
| 400 |
+
(chave, valor),
|
| 401 |
+
commit=True
|
| 402 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 403 |
|
| 404 |
+
def obter_info_geral(self, chave: str) -> Optional[str]:
|
| 405 |
+
result = self._execute_with_retry("SELECT valor FROM info_geral WHERE chave=?", (chave,))
|
| 406 |
+
return result[0][0] if result else None
|
|
|
modules/local_llm.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
LOCAL_LLM.PY — VERSÃO TURBO OFICIAL DA AKIRA (NOVEMBRO 2025)
|
| 3 |
+
- Respostas em 1-2 segundos na CPU (8 núcleos + torch.compile)
|
| 4 |
+
- Nunca recarrega (modelo travado na RAM)
|
| 5 |
+
- max_tokens universal (500 padrão)
|
| 6 |
+
- Sotaque de Luanda 100% brabo
|
| 7 |
+
- Zero custo, zero censura, 24/7
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import os
|
| 11 |
+
import torch
|
| 12 |
+
from loguru import logger
|
| 13 |
+
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
# === CONFIGURAÇÃO ===
|
| 17 |
+
FINETUNED_PATH = "/home/user/data/finetuned_phi3"
|
| 18 |
+
GGUF_PATH = "/home/user/models/Phi-3-mini-4k-instruct.Q4_K_M.gguf"
|
| 19 |
+
HF_MODEL_ID = "microsoft/Phi-3-mini-4k-instruct"
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class Phi3LLM:
|
| 23 |
+
_llm = None
|
| 24 |
+
_available_checked = False
|
| 25 |
+
_is_available = False
|
| 26 |
+
MODEL_ID = "PHI-3 3.8B (HF Transformers TURBO)"
|
| 27 |
+
|
| 28 |
+
@classmethod
|
| 29 |
+
def is_available(cls) -> bool:
|
| 30 |
+
if not cls._available_checked:
|
| 31 |
+
try:
|
| 32 |
+
import torch
|
| 33 |
+
from transformers import AutoModelForCausalLM, AutoTokenizer
|
| 34 |
+
cls._is_available = True
|
| 35 |
+
cls._available_checked = True
|
| 36 |
+
logger.info(f"{cls.MODEL_ID} AMBIENTE PRONTO.")
|
| 37 |
+
if os.path.isfile(GGUF_PATH):
|
| 38 |
+
logger.warning("GGUF encontrado → ignorado (usando Transformers TURBO).")
|
| 39 |
+
else:
|
| 40 |
+
logger.warning(f"GGUF não encontrado: {GGUF_PATH}")
|
| 41 |
+
except ImportError as e:
|
| 42 |
+
cls._is_available = False
|
| 43 |
+
cls._available_checked = True
|
| 44 |
+
logger.error(f"Dependências faltando: {e}")
|
| 45 |
+
return cls._is_available
|
| 46 |
+
|
| 47 |
+
@classmethod
|
| 48 |
+
def _get_llm(cls):
|
| 49 |
+
# SE JÁ TÁ NA RAM → PULA TUDO
|
| 50 |
+
if cls._llm is not None:
|
| 51 |
+
logger.info("PHI-3 TURBO JÁ NA RAM → resposta em <2s!")
|
| 52 |
+
return cls._llm
|
| 53 |
+
|
| 54 |
+
if not cls.is_available():
|
| 55 |
+
return None
|
| 56 |
+
|
| 57 |
+
device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 58 |
+
logger.info(f"Carregando {cls.MODEL_ID} → {device.upper()} (TURBO MODE)")
|
| 59 |
+
|
| 60 |
+
try:
|
| 61 |
+
# === OTIMIZAÇÕES EXTREMAS PARA CPU ===
|
| 62 |
+
if device == "cpu":
|
| 63 |
+
torch.set_num_threads(8) # Usa TODOS os núcleos
|
| 64 |
+
torch.set_num_interop_threads(8)
|
| 65 |
+
torch._C._set_mkldnn_enabled(True) # Intel MKL-DNN (acelera 2x)
|
| 66 |
+
logger.info("CPU TURBO: 8 threads + MKL-DNN ativado")
|
| 67 |
+
|
| 68 |
+
# Quantização 4-bit só se tiver GPU
|
| 69 |
+
bnb_config = None
|
| 70 |
+
if device == "cuda":
|
| 71 |
+
logger.info("GPU detectada → 4-bit nf4")
|
| 72 |
+
bnb_config = BitsAndBytesConfig(
|
| 73 |
+
load_in_4bit=True,
|
| 74 |
+
bnb_4bit_quant_type="nf4",
|
| 75 |
+
bnb_4bit_compute_dtype=torch.bfloat16,
|
| 76 |
+
)
|
| 77 |
+
|
| 78 |
+
# Carrega tokenizer
|
| 79 |
+
tokenizer = AutoTokenizer.from_pretrained(HF_MODEL_ID, trust_remote_code=True)
|
| 80 |
+
|
| 81 |
+
# Carrega modelo com otimização máxima
|
| 82 |
+
model = AutoModelForCausalLM.from_pretrained(
|
| 83 |
+
HF_MODEL_ID,
|
| 84 |
+
torch_dtype=torch.bfloat16 if device == "cuda" else torch.float32,
|
| 85 |
+
trust_remote_code=True,
|
| 86 |
+
quantization_config=bnb_config,
|
| 87 |
+
device_map="auto",
|
| 88 |
+
low_cpu_mem_usage=True,
|
| 89 |
+
attn_implementation="eager", # Evita flash_attn warning
|
| 90 |
+
)
|
| 91 |
+
|
| 92 |
+
# === TORCH.COMPILE — A MÁGICA QUE FAZ VOAR ===
|
| 93 |
+
if device == "cpu":
|
| 94 |
+
logger.info("Compilando modelo com torch.compile (primeira vez +30s, depois 1s por resposta)...")
|
| 95 |
+
model = torch.compile(model, mode="max-autotune", fullgraph=True)
|
| 96 |
+
|
| 97 |
+
cls._llm = (model, tokenizer)
|
| 98 |
+
logger.success(f"{cls.MODEL_ID} TURBO CARREGADO E TRAVADO NA RAM! (~7GB)")
|
| 99 |
+
|
| 100 |
+
# LoRA (só log)
|
| 101 |
+
if os.path.isdir(os.path.join(FINETUNED_PATH, "lora_leve")):
|
| 102 |
+
logger.warning("LoRA encontrado → não carregado automaticamente.")
|
| 103 |
+
|
| 104 |
+
return cls._llm
|
| 105 |
+
|
| 106 |
+
except Exception as e:
|
| 107 |
+
logger.error(f"ERRO AO CARREGAR TURBO: {e}")
|
| 108 |
+
import traceback
|
| 109 |
+
logger.error(traceback.format_exc())
|
| 110 |
+
cls._llm = None
|
| 111 |
+
return None
|
| 112 |
+
|
| 113 |
+
@classmethod
|
| 114 |
+
def generate(cls, prompt: str, max_tokens: int = 500) -> str:
|
| 115 |
+
llm_pair = cls._get_llm()
|
| 116 |
+
if not llm_pair:
|
| 117 |
+
raise RuntimeError("Phi-3 TURBO não carregado.")
|
| 118 |
+
|
| 119 |
+
model, tokenizer = llm_pair
|
| 120 |
+
device = model.device
|
| 121 |
+
|
| 122 |
+
try:
|
| 123 |
+
# Formata com chat template oficial
|
| 124 |
+
formatted = tokenizer.apply_chat_template(
|
| 125 |
+
[{"role": "user", "content": prompt}],
|
| 126 |
+
tokenize=False,
|
| 127 |
+
add_generation_prompt=True
|
| 128 |
+
)
|
| 129 |
+
input_ids = tokenizer.encode(formatted, return_tensors="pt").to(device)
|
| 130 |
+
|
| 131 |
+
logger.info(f"[PHI-3 TURBO] Gerando → {max_tokens} tokens")
|
| 132 |
+
|
| 133 |
+
with torch.no_grad():
|
| 134 |
+
output = model.generate(
|
| 135 |
+
input_ids,
|
| 136 |
+
max_new_tokens=max_tokens,
|
| 137 |
+
temperature=0.8,
|
| 138 |
+
top_p=0.9,
|
| 139 |
+
do_sample=True,
|
| 140 |
+
repetition_penalty=1.1,
|
| 141 |
+
pad_token_id=tokenizer.eos_token_id,
|
| 142 |
+
eos_token_id=tokenizer.eos_token_id,
|
| 143 |
+
use_cache=True, # Acelera geração
|
| 144 |
+
)
|
| 145 |
+
|
| 146 |
+
text = tokenizer.decode(output[0][input_ids.shape[-1]:], skip_special_tokens=True).strip()
|
| 147 |
+
text = text.replace("<|end|>", "").replace("<|assistant|>", "").strip()
|
| 148 |
+
|
| 149 |
+
logger.success(f"PHI-3 TURBO respondeu → {len(text)} chars em <2s!")
|
| 150 |
+
return text
|
| 151 |
+
|
| 152 |
+
except Exception as e:
|
| 153 |
+
logger.error(f"ERRO NA GERAÇÃO TURBO: {e}")
|
| 154 |
+
import traceback
|
| 155 |
+
logger.error(traceback.format_exc())
|
| 156 |
+
raise
|
modules/treinamento.py
CHANGED
|
@@ -1,897 +1,201 @@
|
|
| 1 |
-
# modules/treinamento.py — AKIRA V21 COMPATÍVEL (Atualizado Dezembro 2025)
|
| 2 |
"""
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
✅ CORREÇÃO: Extração correta do texto citado completo
|
| 10 |
"""
|
|
|
|
| 11 |
import json
|
| 12 |
import os
|
| 13 |
-
import time
|
| 14 |
import threading
|
| 15 |
-
import
|
| 16 |
-
import hashlib
|
| 17 |
-
import re
|
| 18 |
-
from datetime import datetime, timedelta
|
| 19 |
-
from typing import Optional, Dict, Any, List, Tuple
|
| 20 |
from loguru import logger
|
| 21 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
from .database import Database
|
| 23 |
-
import modules.config as config
|
| 24 |
|
| 25 |
-
# === MODELO DE EMBEDDINGS ===
|
| 26 |
-
try:
|
| 27 |
-
from sentence_transformers import SentenceTransformer
|
| 28 |
-
EMBEDDING_MODEL = SentenceTransformer("paraphrase-multilingual-MiniLM-L12-v2")
|
| 29 |
-
logger.info("✅ Modelo de embeddings carregado (MiniLM-L12-v2)")
|
| 30 |
-
except Exception as e:
|
| 31 |
-
logger.warning(f"Embeddings não disponíveis: {e}")
|
| 32 |
-
EMBEDDING_MODEL = None
|
| 33 |
|
| 34 |
-
#
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
)
|
| 44 |
-
logger.info("✅ DistilBERT emocional carregado para treinamento")
|
| 45 |
-
except Exception as e:
|
| 46 |
-
logger.warning(f"DistilBERT não disponível: {e}")
|
| 47 |
-
EMOTION_CLASSIFIER = None
|
| 48 |
|
| 49 |
-
#
|
| 50 |
-
|
| 51 |
-
MIN_INTERACOES_PARA_ANALISE = 10
|
| 52 |
-
MAX_EXEMPLOS_DATASET = 2000
|
| 53 |
-
QUALIDADE_MINIMA = 0.6
|
| 54 |
|
| 55 |
-
#
|
| 56 |
-
EMBEDDING_CACHE = {}
|
| 57 |
_lock = threading.Lock()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
|
| 59 |
-
# === CLASSE TREINAMENTO COMPATÍVEL COM PAYLOAD ===
|
| 60 |
class Treinamento:
|
| 61 |
-
def __init__(self, db: Database, interval_hours: int =
|
| 62 |
self.db = db
|
| 63 |
self.interval_seconds = interval_hours * 3600
|
| 64 |
-
self.
|
| 65 |
-
|
| 66 |
-
self.
|
| 67 |
-
self.ultima_analise = 0
|
| 68 |
-
logger.info(f"✅ Treinamento V21 inicializado (análise a cada {interval_hours}h)")
|
| 69 |
-
|
| 70 |
-
# === REGISTRO DE INTERAÇÕES (COMPATÍVEL COM PAYLOAD DO INDEX) ===
|
| 71 |
-
def registrar_interacao(
|
| 72 |
-
self,
|
| 73 |
-
usuario: str,
|
| 74 |
-
numero: str,
|
| 75 |
-
mensagem: str,
|
| 76 |
-
resposta: str,
|
| 77 |
-
tipo_conversa: str = 'pv', # 'pv' ou 'grupo'
|
| 78 |
-
tipo_mensagem: str = 'texto', # 'texto' ou 'audio'
|
| 79 |
-
mensagem_citada: str = '',
|
| 80 |
-
reply_info: Optional[Dict[str, Any]] = None,
|
| 81 |
-
contexto_analise: Optional[Dict] = None, # CORREÇÃO: Adicionado para compatibilidade
|
| 82 |
-
**kwargs
|
| 83 |
-
):
|
| 84 |
-
"""Registra interação completa baseada no payload do WhatsApp"""
|
| 85 |
-
try:
|
| 86 |
-
numero = str(numero).strip()
|
| 87 |
-
|
| 88 |
-
# ANÁLISE DE CONTEXTO DO PAYLOAD
|
| 89 |
-
contexto = self._extrair_contexto_payload(
|
| 90 |
-
usuario=usuario,
|
| 91 |
-
numero=numero,
|
| 92 |
-
tipo_conversa=tipo_conversa,
|
| 93 |
-
tipo_mensagem=tipo_mensagem,
|
| 94 |
-
mensagem_citada=mensagem_citada,
|
| 95 |
-
reply_info=reply_info
|
| 96 |
-
)
|
| 97 |
-
|
| 98 |
-
# CORREÇÃO: Se contexto_analise foi fornecido, mescla
|
| 99 |
-
if contexto_analise:
|
| 100 |
-
contexto.update(contexto_analise)
|
| 101 |
-
|
| 102 |
-
# DETECTA EMOÇÃO SE NÃO FORNECIDA
|
| 103 |
-
emocao_detectada, confianca_emocao = None, 0.5
|
| 104 |
-
if EMOTION_CLASSIFIER and mensagem.strip():
|
| 105 |
-
try:
|
| 106 |
-
texto = mensagem[:256]
|
| 107 |
-
resultado = EMOTION_CLASSIFIER(texto, truncation=True)
|
| 108 |
-
if resultado and resultado[0]:
|
| 109 |
-
emocao_detectada = resultado[0][0]['label']
|
| 110 |
-
confianca_emocao = resultado[0][0]['score']
|
| 111 |
-
except Exception as e:
|
| 112 |
-
logger.warning(f"Erro na detecção de emoção: {e}")
|
| 113 |
-
|
| 114 |
-
# DETECTA SE É COMANDO
|
| 115 |
-
is_comando = self._detectar_se_eh_comando(mensagem, contexto)
|
| 116 |
-
|
| 117 |
-
# SALVAR NO BANCO (MÉTODO ATUALIZADO)
|
| 118 |
-
self.db.salvar_mensagem(
|
| 119 |
-
usuario=usuario,
|
| 120 |
-
mensagem=mensagem,
|
| 121 |
-
resposta=resposta,
|
| 122 |
-
numero=numero,
|
| 123 |
-
is_reply=contexto.get('is_reply', False),
|
| 124 |
-
mensagem_original=mensagem_citada or '',
|
| 125 |
-
reply_to_bot=contexto.get('reply_to_bot', False),
|
| 126 |
-
humor=contexto.get('humor_atualizado', 'normal_ironico'),
|
| 127 |
-
modo_resposta=contexto.get('modo_resposta', 'normal_ironico'),
|
| 128 |
-
emocao_detectada=emocao_detectada or "neutral",
|
| 129 |
-
confianca_emocao=confianca_emocao,
|
| 130 |
-
tipo_mensagem=tipo_mensagem,
|
| 131 |
-
reply_info=reply_info,
|
| 132 |
-
usuario_nome=usuario,
|
| 133 |
-
grupo_id=contexto.get('grupo_id', ''),
|
| 134 |
-
grupo_nome=contexto.get('grupo_nome', ''),
|
| 135 |
-
tipo_conversa=tipo_conversa
|
| 136 |
-
)
|
| 137 |
-
|
| 138 |
-
# GERAR EMBEDDING SE NÃO FOR ÁUDIO
|
| 139 |
-
if EMBEDDING_MODEL and tipo_mensagem != 'audio' and mensagem.strip():
|
| 140 |
-
self._gerar_e_salvar_embedding(numero, mensagem, resposta)
|
| 141 |
-
|
| 142 |
-
# CALCULAR QUALIDADE (COM FATOR ÁUDIO)
|
| 143 |
-
qualidade = self._calcular_qualidade_resposta_v2(
|
| 144 |
-
mensagem=mensagem,
|
| 145 |
-
resposta=resposta,
|
| 146 |
-
contexto=contexto,
|
| 147 |
-
tipo_mensagem=tipo_mensagem,
|
| 148 |
-
tipo_conversa=tipo_conversa
|
| 149 |
-
)
|
| 150 |
-
|
| 151 |
-
# SALVAR NO DATASET.JSON (APENAS QUALIDADE BAIXA/MÉDIA)
|
| 152 |
-
if qualidade >= QUALIDADE_MINIMA:
|
| 153 |
-
self._salvar_no_dataset_v2(
|
| 154 |
-
mensagem=mensagem,
|
| 155 |
-
resposta=resposta,
|
| 156 |
-
numero=numero,
|
| 157 |
-
usuario=usuario,
|
| 158 |
-
contexto=contexto,
|
| 159 |
-
emocao_detectada=emocao_detectada,
|
| 160 |
-
confianca_emocao=confianca_emocao,
|
| 161 |
-
qualidade=qualidade,
|
| 162 |
-
tipo_mensagem=tipo_mensagem,
|
| 163 |
-
tipo_conversa=tipo_conversa
|
| 164 |
-
)
|
| 165 |
-
|
| 166 |
-
# SALVAR EXEMPLO DE TREINAMENTO (APENAS TEXTUAL)
|
| 167 |
-
if tipo_mensagem == 'texto' and len(resposta) > 15 and len(mensagem) > 5:
|
| 168 |
-
self.db.salvar_training_example(
|
| 169 |
-
input_text=mensagem,
|
| 170 |
-
output_text=resposta,
|
| 171 |
-
humor=contexto.get('humor_atualizado', 'normal_ironico'),
|
| 172 |
-
modo_resposta=contexto.get('modo_resposta', 'normal_ironico'),
|
| 173 |
-
emocao_contexto=emocao_detectada or "neutral",
|
| 174 |
-
qualidade_score=qualidade
|
| 175 |
-
)
|
| 176 |
-
|
| 177 |
-
# ANALISAR PADRÕES (ATUALIZADO)
|
| 178 |
-
self._analisar_padroes_usuario_v2(
|
| 179 |
-
numero=numero,
|
| 180 |
-
usuario=usuario,
|
| 181 |
-
mensagem=mensagem,
|
| 182 |
-
resposta=resposta,
|
| 183 |
-
contexto=contexto,
|
| 184 |
-
emocao_detectada=emocao_detectada,
|
| 185 |
-
tipo_conversa=tipo_conversa
|
| 186 |
-
)
|
| 187 |
-
|
| 188 |
-
logger.debug(f"✅ Interação registrada: {usuario[:10]}... (qualidade: {qualidade:.2f}, tipo: {tipo_mensagem})")
|
| 189 |
-
|
| 190 |
-
except Exception as e:
|
| 191 |
-
logger.error(f"Erro ao registrar interação: {e}")
|
| 192 |
-
|
| 193 |
-
def _extrair_contexto_payload(
|
| 194 |
-
self,
|
| 195 |
-
usuario: str,
|
| 196 |
-
numero: str,
|
| 197 |
-
tipo_conversa: str,
|
| 198 |
-
tipo_mensagem: str,
|
| 199 |
-
mensagem_citada: str = '',
|
| 200 |
-
reply_info: Optional[Dict[str, Any]] = None
|
| 201 |
-
) -> Dict[str, Any]:
|
| 202 |
-
"""Extrai contexto do payload do WhatsApp"""
|
| 203 |
-
contexto = {
|
| 204 |
-
'tipo_conversa': tipo_conversa,
|
| 205 |
-
'tipo_mensagem': tipo_mensagem,
|
| 206 |
-
'timestamp': time.time(),
|
| 207 |
-
'is_reply': bool(mensagem_citada),
|
| 208 |
-
'reply_info': reply_info or {},
|
| 209 |
-
'reply_to_bot': False,
|
| 210 |
-
'mencoes': [],
|
| 211 |
-
'humor_atualizado': 'normal_ironico',
|
| 212 |
-
'modo_resposta': 'normal_ironico',
|
| 213 |
-
'tom_usuario': 'neutro',
|
| 214 |
-
'mensagem_citada_original': mensagem_citada, # CORREÇÃO: Mantém original
|
| 215 |
-
'mensagem_citada_limpa': self._extrair_conteudo_citado_limpo(mensagem_citada) # CORREÇÃO: Conteúdo limpo
|
| 216 |
-
}
|
| 217 |
-
|
| 218 |
-
# ANÁLISE DE REPLY_INFO
|
| 219 |
-
if reply_info:
|
| 220 |
-
contexto['reply_to_bot'] = reply_info.get('reply_to_bot', False)
|
| 221 |
-
contexto['usuario_citado_nome'] = reply_info.get('usuario_citado_nome', '')
|
| 222 |
-
contexto['usuario_citado_numero'] = reply_info.get('usuario_citado_numero', '')
|
| 223 |
-
# CORREÇÃO: Extrai texto citado completo do reply_info se disponível
|
| 224 |
-
if reply_info.get('texto_citado_completo'):
|
| 225 |
-
contexto['mensagem_citada_limpa'] = reply_info.get('texto_citado_completo', '')
|
| 226 |
-
|
| 227 |
-
# DETECTA MENÇÕES
|
| 228 |
-
if 'akira' in mensagem_citada.lower():
|
| 229 |
-
contexto['mencoes'].append('akira')
|
| 230 |
-
|
| 231 |
-
# DETECTA COMANDOS
|
| 232 |
-
if mensagem_citada and mensagem_citada.startswith('#'):
|
| 233 |
-
contexto['is_comando'] = True
|
| 234 |
-
|
| 235 |
-
# AJUSTA HUMOR COM BASE NO CONTEXTO
|
| 236 |
-
if tipo_conversa == 'grupo':
|
| 237 |
-
if contexto['reply_to_bot'] or 'akira' in mensagem_citada.lower():
|
| 238 |
-
contexto['humor_atualizado'] = 'ativo_ironico'
|
| 239 |
-
else:
|
| 240 |
-
contexto['humor_atualizado'] = 'observador_ironico'
|
| 241 |
-
else:
|
| 242 |
-
# PV: sempre mais ativo
|
| 243 |
-
contexto['humor_atualizado'] = 'ativo_ironico'
|
| 244 |
-
|
| 245 |
-
# DETECTA TOM DO USUÁRIO
|
| 246 |
-
if mensagem_citada:
|
| 247 |
-
contexto['tom_usuario'] = self._detectar_tom_avancado(mensagem_citada)
|
| 248 |
-
|
| 249 |
-
return contexto
|
| 250 |
-
|
| 251 |
-
def _extrair_conteudo_citado_limpo(self, mensagem_citada: str) -> str:
|
| 252 |
-
"""Extrai conteúdo limpo da mensagem citada"""
|
| 253 |
-
if not mensagem_citada:
|
| 254 |
-
return ""
|
| 255 |
-
|
| 256 |
-
# Remove formatação do reply
|
| 257 |
-
if mensagem_citada.startswith("[Respondendo à Akira:"):
|
| 258 |
-
return mensagem_citada[23:].strip()
|
| 259 |
-
elif mensagem_citada.startswith("[") and ":" in mensagem_citada:
|
| 260 |
-
partes = mensagem_citada.split(":", 1)
|
| 261 |
-
if len(partes) > 1:
|
| 262 |
-
return partes[1].strip()
|
| 263 |
-
|
| 264 |
-
return mensagem_citada.strip()
|
| 265 |
-
|
| 266 |
-
def _detectar_se_eh_comando(self, mensagem: str, contexto: Dict) -> bool:
|
| 267 |
-
"""Detecta se é um comando do WhatsApp"""
|
| 268 |
-
if not mensagem:
|
| 269 |
-
return False
|
| 270 |
-
|
| 271 |
-
# Comandos conhecidos do bot
|
| 272 |
-
comandos = [
|
| 273 |
-
'#sticker', '#s', '#fig', '#gif',
|
| 274 |
-
'#toimg', '#img', '#unstick',
|
| 275 |
-
'#tts', '#play', '#tocar', '#music', '#ytmp3', '#yt', '#ytaudio',
|
| 276 |
-
'#help', '#menu', '#comandos',
|
| 277 |
-
'#ping', '#info', '#botinfo',
|
| 278 |
-
'#add', '#remove', '#kick',
|
| 279 |
-
'#promote', '#demote',
|
| 280 |
-
'#mute', '#desmute',
|
| 281 |
-
'#antilink', '#apagar', '#delete', '#del',
|
| 282 |
-
'#donate', '#doar', '#apoia'
|
| 283 |
-
]
|
| 284 |
-
|
| 285 |
-
mensagem_lower = mensagem.lower().strip()
|
| 286 |
-
|
| 287 |
-
# Verifica se começa com algum comando
|
| 288 |
-
for cmd in comandos:
|
| 289 |
-
if mensagem_lower.startswith(cmd):
|
| 290 |
-
return True
|
| 291 |
-
|
| 292 |
-
# Verifica contexto de reply
|
| 293 |
-
if contexto.get('reply_info', {}).get('reply_to_bot', False):
|
| 294 |
-
# Se está respondendo ao bot, pode ser comando implícito
|
| 295 |
-
if mensagem_lower in ['sim', 'não', 'ok', 'obrigado', 'valeu']:
|
| 296 |
-
return True
|
| 297 |
-
|
| 298 |
-
return False
|
| 299 |
-
|
| 300 |
-
def _calcular_qualidade_resposta_v2(
|
| 301 |
-
self,
|
| 302 |
-
mensagem: str,
|
| 303 |
-
resposta: str,
|
| 304 |
-
contexto: Dict,
|
| 305 |
-
tipo_mensagem: str = 'texto',
|
| 306 |
-
tipo_conversa: str = 'pv'
|
| 307 |
-
) -> float:
|
| 308 |
-
"""Calcula qualidade da resposta (0.0-1.0) - Versão atualizada"""
|
| 309 |
-
qualidade = 1.0
|
| 310 |
-
|
| 311 |
-
# FATORES POSITIVOS
|
| 312 |
-
positivos = 0
|
| 313 |
-
|
| 314 |
-
# 1. Comprimento adequado da resposta
|
| 315 |
-
if tipo_mensagem == 'audio':
|
| 316 |
-
# Respostas de áudio podem ser mais curtas
|
| 317 |
-
if 20 <= len(resposta) <= 200:
|
| 318 |
-
positivos += 1
|
| 319 |
-
else:
|
| 320 |
-
if 30 <= len(resposta) <= 300:
|
| 321 |
-
positivos += 1
|
| 322 |
-
elif len(resposta) < 10:
|
| 323 |
-
qualidade -= 0.3
|
| 324 |
-
|
| 325 |
-
# 2. Mensagem com conteúdo
|
| 326 |
-
if len(mensagem) > 3:
|
| 327 |
-
positivos += 1
|
| 328 |
-
|
| 329 |
-
# 3. Contexto apropriado
|
| 330 |
-
if contexto.get('reply_to_bot', False):
|
| 331 |
-
positivos += 1 # Reply ao bot é bom
|
| 332 |
-
|
| 333 |
-
# 4. Tipo de conversa
|
| 334 |
-
if tipo_conversa == 'pv':
|
| 335 |
-
positivos += 0.5 # PV tem prioridade
|
| 336 |
-
|
| 337 |
-
# 5. Gírias angolanas (bônus moderado)
|
| 338 |
-
girias_angolanas = ['puto', 'fixe', 'bué', 'kota', 'ya', 'mwangolé']
|
| 339 |
-
girias_presentes = sum(1 for g in girias_angolanas if g in resposta.lower())
|
| 340 |
-
if 0 < girias_presentes <= 2:
|
| 341 |
-
positivos += girias_presentes * 0.1
|
| 342 |
-
|
| 343 |
-
# 6. Evita problemas conhecidos
|
| 344 |
-
problemas = [
|
| 345 |
-
("kkk", resposta.lower().count("kkk") > 3),
|
| 346 |
-
("rsrs", resposta.lower().count("rsrs") > 3),
|
| 347 |
-
('"', resposta.count('"') > 5),
|
| 348 |
-
("**", resposta.count('**') > 2),
|
| 349 |
-
("```", "```" in resposta),
|
| 350 |
-
]
|
| 351 |
-
|
| 352 |
-
problemas_encontrados = sum(1 for _, condicao in problemas if condicao)
|
| 353 |
-
if problemas_encontrados > 0:
|
| 354 |
-
qualidade -= problemas_encontrados * 0.1
|
| 355 |
-
|
| 356 |
-
# 7. Se é comando
|
| 357 |
-
if contexto.get('is_comando', False):
|
| 358 |
-
positivos += 0.5 # Comandos têm qualidade base alta
|
| 359 |
-
|
| 360 |
-
# 8. Emoção detectada
|
| 361 |
-
if contexto.get('emocao_detectada') and contexto.get('emocao_detectada') != 'neutral':
|
| 362 |
-
positivos += 0.3
|
| 363 |
-
|
| 364 |
-
# CALCULA QUALIDADE FINAL
|
| 365 |
-
qualidade += (positivos * 0.1)
|
| 366 |
-
|
| 367 |
-
# LIMITES
|
| 368 |
-
qualidade = max(0.1, min(1.0, qualidade))
|
| 369 |
-
|
| 370 |
-
# ARREDONDA
|
| 371 |
-
return round(qualidade, 2)
|
| 372 |
-
|
| 373 |
-
def _salvar_no_dataset_v2(
|
| 374 |
-
self,
|
| 375 |
-
mensagem: str,
|
| 376 |
-
resposta: str,
|
| 377 |
-
numero: str,
|
| 378 |
-
usuario: str,
|
| 379 |
-
contexto: Dict,
|
| 380 |
-
emocao_detectada: str,
|
| 381 |
-
confianca_emocao: float,
|
| 382 |
-
qualidade: float,
|
| 383 |
-
tipo_mensagem: str = 'texto',
|
| 384 |
-
tipo_conversa: str = 'pv'
|
| 385 |
-
):
|
| 386 |
-
"""Salva exemplo no dataset.json - Versão atualizada"""
|
| 387 |
-
try:
|
| 388 |
-
humor = contexto.get("humor_atualizado", "normal_ironico")
|
| 389 |
-
modo = contexto.get("modo_resposta", "normal_ironico")
|
| 390 |
-
tom = contexto.get("tom_usuario", "neutro")
|
| 391 |
-
|
| 392 |
-
# Garante ironia
|
| 393 |
-
if "ironic" not in humor and "ironica" not in humor:
|
| 394 |
-
if humor.endswith("o"):
|
| 395 |
-
humor = f"{humor}_ironico"
|
| 396 |
-
elif humor.endswith("a"):
|
| 397 |
-
humor = f"{humor}_ironica"
|
| 398 |
-
else:
|
| 399 |
-
humor = f"{humor}_ironico"
|
| 400 |
-
|
| 401 |
-
# Normaliza modo
|
| 402 |
-
if modo == "casual_amigavel":
|
| 403 |
-
modo = "normal_ironico"
|
| 404 |
-
|
| 405 |
-
# CORREÇÃO: Inclui mensagem citada limpa no metadata
|
| 406 |
-
mensagem_citada_limpa = contexto.get('mensagem_citada_limpa', '')
|
| 407 |
-
|
| 408 |
-
entry = {
|
| 409 |
-
"input": mensagem.strip(),
|
| 410 |
-
"output": resposta.strip(),
|
| 411 |
-
"metadata": {
|
| 412 |
-
"usuario": usuario[:20],
|
| 413 |
-
"numero_hash": hashlib.md5(numero.encode()).hexdigest()[:8],
|
| 414 |
-
"humor": humor,
|
| 415 |
-
"modo_resposta": modo,
|
| 416 |
-
"tom_usuario": tom,
|
| 417 |
-
"emocao_detectada": emocao_detectada or "neutral",
|
| 418 |
-
"confianca_emocao": confianca_emocao,
|
| 419 |
-
"qualidade_score": qualidade,
|
| 420 |
-
"is_reply": contexto.get('is_reply', False),
|
| 421 |
-
"reply_to_bot": contexto.get('reply_to_bot', False),
|
| 422 |
-
"mensagem_citada_limpa": mensagem_citada_limpa[:200], # CORREÇÃO: Incluído
|
| 423 |
-
"tipo_mensagem": tipo_mensagem,
|
| 424 |
-
"tipo_conversa": tipo_conversa,
|
| 425 |
-
"is_comando": contexto.get('is_comando', False),
|
| 426 |
-
"timestamp": time.time(),
|
| 427 |
-
"version": "v21_whatsapp_corrigido"
|
| 428 |
-
}
|
| 429 |
-
}
|
| 430 |
-
|
| 431 |
-
with _lock:
|
| 432 |
-
dataset = []
|
| 433 |
-
if os.path.exists(DATASET_PATH):
|
| 434 |
-
try:
|
| 435 |
-
with open(DATASET_PATH, "r", encoding="utf-8") as f:
|
| 436 |
-
dataset = json.load(f)
|
| 437 |
-
if not isinstance(dataset, list):
|
| 438 |
-
dataset = []
|
| 439 |
-
except:
|
| 440 |
-
dataset = []
|
| 441 |
-
|
| 442 |
-
# Remove duplicatas
|
| 443 |
-
entry_hash = hashlib.md5(f"{mensagem}{resposta}{mensagem_citada_limpa}".encode()).hexdigest()
|
| 444 |
-
dataset = [e for e in dataset if
|
| 445 |
-
hashlib.md5(f"{e.get('input','')}{e.get('output','')}{e.get('metadata',{}).get('mensagem_citada_limpa','')}".encode()).hexdigest() != entry_hash]
|
| 446 |
-
|
| 447 |
-
dataset.append(entry)
|
| 448 |
-
|
| 449 |
-
# Mantém apenas melhores exemplos
|
| 450 |
-
if len(dataset) > MAX_EXEMPLOS_DATASET:
|
| 451 |
-
dataset.sort(key=lambda x: x.get("metadata", {}).get("qualidade_score", 0), reverse=True)
|
| 452 |
-
dataset = dataset[:MAX_EXEMPLOS_DATASET]
|
| 453 |
-
|
| 454 |
-
with open(DATASET_PATH, "w", encoding="utf-8") as f:
|
| 455 |
-
json.dump(dataset, f, ensure_ascii=False, indent=2)
|
| 456 |
-
|
| 457 |
-
logger.debug(f"✅ Exemplo adicionado ao dataset (qualidade: {qualidade:.2f}, tipo: {tipo_mensagem}, citado: {bool(mensagem_citada_limpa)})")
|
| 458 |
-
|
| 459 |
-
except Exception as e:
|
| 460 |
-
logger.warning(f"Erro ao salvar dataset: {e}")
|
| 461 |
-
|
| 462 |
-
# === ANÁLISE DE PADRÕES ATUALIZADA ===
|
| 463 |
-
def _analisar_padroes_usuario_v2(
|
| 464 |
-
self,
|
| 465 |
-
numero: str,
|
| 466 |
-
usuario: str,
|
| 467 |
-
mensagem: str,
|
| 468 |
-
resposta: str,
|
| 469 |
-
contexto: Dict[str, Any],
|
| 470 |
-
emocao_detectada: str = None,
|
| 471 |
-
tipo_conversa: str = 'pv'
|
| 472 |
-
):
|
| 473 |
-
"""Analisa e aprende padrões de comunicação - Versão atualizada"""
|
| 474 |
-
try:
|
| 475 |
-
# 1. DETECTAR TOM DO USUÁRIO
|
| 476 |
-
tom = self._detectar_tom_avancado(mensagem)
|
| 477 |
-
if tom:
|
| 478 |
-
self.db.registrar_tom_usuario(numero, tom)
|
| 479 |
-
|
| 480 |
-
# 2. DETECTAR GÍRIAS
|
| 481 |
-
girias_detectadas = self._detectar_girias_avancado(mensagem)
|
| 482 |
-
for giria, significado in girias_detectadas.items():
|
| 483 |
-
try:
|
| 484 |
-
self.db.salvar_giria(
|
| 485 |
-
numero=numero,
|
| 486 |
-
giria=giria,
|
| 487 |
-
significado=significado,
|
| 488 |
-
contexto=mensagem[:100]
|
| 489 |
-
)
|
| 490 |
-
except Exception as e:
|
| 491 |
-
logger.warning(f"Erro ao salvar gíria: {e}")
|
| 492 |
-
|
| 493 |
-
# 3. REGISTRAR HUMOR
|
| 494 |
-
if "humor_atualizado" in contexto:
|
| 495 |
-
humor = contexto["humor_atualizado"]
|
| 496 |
-
humor_atual = self.db.recuperar_humor_atual(numero)
|
| 497 |
-
if humor != humor_atual:
|
| 498 |
-
self.db.salvar_transicao_humor(
|
| 499 |
-
numero=numero,
|
| 500 |
-
humor_anterior=humor_atual,
|
| 501 |
-
humor_novo=humor,
|
| 502 |
-
emocao_trigger=emocao_detectada,
|
| 503 |
-
confianca_emocao=contexto.get('confianca_emocao', 0.5),
|
| 504 |
-
nivel_transicao=contexto.get('nivel_transicao', 0),
|
| 505 |
-
razao=f"Interação {tipo_conversa}: {mensagem[:50]}..."
|
| 506 |
-
)
|
| 507 |
-
|
| 508 |
-
# 4. PADRÕES DE REPLY (ATUALIZADO COM CONTEÚDO CITADO)
|
| 509 |
-
if contexto.get('is_reply'):
|
| 510 |
-
mensagem_citada_limpa = contexto.get('mensagem_citada_limpa', '')
|
| 511 |
-
self._aprender_padrao_reply_v2(numero, usuario, mensagem, resposta, contexto, mensagem_citada_limpa)
|
| 512 |
-
|
| 513 |
-
# 5. PADRÕES EMOCIONAIS (ATUALIZADO)
|
| 514 |
-
if emocao_detectada and emocao_detectada != "neutral":
|
| 515 |
-
self._aprender_padrao_emocional_v2(numero, usuario, emocao_detectada, mensagem, contexto)
|
| 516 |
-
|
| 517 |
-
# 6. PADRÕES POR TIPO DE CONVERSA
|
| 518 |
-
self._analisar_padroes_tipo_conversa(numero, tipo_conversa, mensagem, resposta, contexto)
|
| 519 |
-
|
| 520 |
-
except Exception as e:
|
| 521 |
-
logger.warning(f"Erro na análise de padrões: {e}")
|
| 522 |
-
|
| 523 |
-
def _detectar_tom_avancado(self, mensagem: str) -> Optional[str]:
|
| 524 |
-
if not mensagem:
|
| 525 |
-
return None
|
| 526 |
-
msg_lower = mensagem.lower()
|
| 527 |
-
|
| 528 |
-
# Formal
|
| 529 |
-
if any(x in msg_lower for x in ["senhor", "doutor", "atenciosamente", "por favor", "obrigado"]):
|
| 530 |
-
return "formal"
|
| 531 |
-
|
| 532 |
-
# Rude
|
| 533 |
-
rude_palavras = ["burro", "idiota", "merda", "porra", "caralho", "vai se foder", "fodase"]
|
| 534 |
-
rude = sum(1 for x in rude_palavras if x in msg_lower)
|
| 535 |
-
if rude >= 2 or (len([c for c in mensagem if c.isupper()]) / max(len(mensagem), 1)) > 0.3:
|
| 536 |
-
return "rude"
|
| 537 |
-
|
| 538 |
-
# Informal/Angolano
|
| 539 |
-
girias = ['puto', 'mano', 'kota', 'fixe', 'bué', 'ya', 'epa', 'maka']
|
| 540 |
-
if any(x in msg_lower for x in girias):
|
| 541 |
-
return "informal_angolano"
|
| 542 |
-
|
| 543 |
-
# Neutro
|
| 544 |
-
return "neutro"
|
| 545 |
|
| 546 |
-
def
|
| 547 |
-
|
| 548 |
-
|
| 549 |
-
|
| 550 |
-
|
| 551 |
-
|
| 552 |
-
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
|
| 556 |
-
|
| 557 |
-
def _analisar_padroes_tipo_conversa(self, numero: str, tipo_conversa: str, mensagem: str, resposta: str, contexto: Dict):
|
| 558 |
-
"""Analisa padrões específicos por tipo de conversa"""
|
| 559 |
-
try:
|
| 560 |
-
if tipo_conversa == 'grupo':
|
| 561 |
-
# Padrões de grupo: menções, replies, comandos
|
| 562 |
-
if contexto.get('reply_to_bot'):
|
| 563 |
-
padrao = "grupo_reply_ao_bot"
|
| 564 |
-
elif 'akira' in mensagem.lower():
|
| 565 |
-
padrao = "grupo_menção_direta"
|
| 566 |
-
else:
|
| 567 |
-
padrao = "grupo_geral"
|
| 568 |
-
else:
|
| 569 |
-
# Padrões de PV: mais pessoais
|
| 570 |
-
if len(mensagem.split()) < 5:
|
| 571 |
-
padrao = "pv_curta"
|
| 572 |
-
elif "?" in mensagem:
|
| 573 |
-
padrao = "pv_pergunta"
|
| 574 |
-
else:
|
| 575 |
-
padrao = "pv_normal"
|
| 576 |
-
|
| 577 |
-
# Salva análise
|
| 578 |
-
self.db.salvar_analise_padrao(
|
| 579 |
-
numero=numero,
|
| 580 |
-
tipo_conversa=tipo_conversa,
|
| 581 |
-
padrao=padrao,
|
| 582 |
-
exemplo_mensagem=mensagem[:100],
|
| 583 |
-
exemplo_resposta=resposta[:100],
|
| 584 |
-
contexto=json.dumps(contexto, ensure_ascii=False)
|
| 585 |
-
)
|
| 586 |
-
|
| 587 |
-
except Exception as e:
|
| 588 |
-
logger.warning(f"Erro ao analisar padrões de tipo: {e}")
|
| 589 |
-
|
| 590 |
-
# === MÉTODOS DE APRENDIZADO ATUALIZADOS ===
|
| 591 |
-
def _aprender_padrao_reply_v2(self, numero: str, usuario: str, mensagem: str, resposta: str,
|
| 592 |
-
contexto: Dict, mensagem_citada_limpa: str = ""):
|
| 593 |
-
"""Aprende padrões de reply - Versão atualizada com conteúdo citado"""
|
| 594 |
-
try:
|
| 595 |
-
# Analisa tipo de reply baseado no conteúdo citado
|
| 596 |
-
reply_to_bot = contexto.get('reply_to_bot', False)
|
| 597 |
-
|
| 598 |
-
# Define padrão baseado no contexto
|
| 599 |
-
if reply_to_bot:
|
| 600 |
-
if "?" in mensagem and "?" not in resposta:
|
| 601 |
-
padrao = "resposta_a_pergunta_sobre_minha_mensagem"
|
| 602 |
-
elif "!" in mensagem:
|
| 603 |
-
padrao = "reacao_emocional_a_minha_mensagem"
|
| 604 |
-
else:
|
| 605 |
-
padrao = "continuacao_da_minha_conversa"
|
| 606 |
-
else:
|
| 607 |
-
if "?" in mensagem and "?" in resposta:
|
| 608 |
-
padrao = "pergunta_sobre_mensagem_alheia"
|
| 609 |
-
elif "!" in mensagem:
|
| 610 |
-
padrao = "comentario_sobre_mensagem_alheia"
|
| 611 |
-
else:
|
| 612 |
-
padrao = "discussao_sobre_mensagem_alheia"
|
| 613 |
-
|
| 614 |
-
# Prepara texto do input incluindo contexto citado
|
| 615 |
-
input_text_com_contexto = f"[CITANDO: {mensagem_citada_limpa[:100]}] {mensagem}"
|
| 616 |
-
|
| 617 |
-
# Salva aprendizado com contexto completo
|
| 618 |
-
self.db.salvar_aprendizado_detalhado(
|
| 619 |
-
input_text=input_text_com_contexto,
|
| 620 |
-
output_text=resposta,
|
| 621 |
-
contexto=contexto,
|
| 622 |
-
qualidade_score=0.8,
|
| 623 |
-
tipo_aprendizado=f"reply_{padrao}",
|
| 624 |
-
metadata={
|
| 625 |
-
"numero": numero,
|
| 626 |
-
"usuario": usuario,
|
| 627 |
-
"padrao": padrao,
|
| 628 |
-
"reply_to_bot": reply_to_bot,
|
| 629 |
-
"tipo_conversa": contexto.get('tipo_conversa', 'pv'),
|
| 630 |
-
"mensagem_citada": mensagem_citada_limpa[:200],
|
| 631 |
-
"msg_len": len(mensagem),
|
| 632 |
-
"resp_len": len(resposta)
|
| 633 |
-
}
|
| 634 |
-
)
|
| 635 |
-
logger.debug(f"✅ Padrão reply aprendido: {padrao} | Citado: {len(mensagem_citada_limpa)} chars")
|
| 636 |
-
|
| 637 |
-
except Exception as e:
|
| 638 |
-
logger.warning(f"Erro ao aprender padrão reply: {e}")
|
| 639 |
|
| 640 |
-
def
|
| 641 |
-
"""Aprende padrões emocionais - Versão atualizada"""
|
| 642 |
try:
|
| 643 |
-
|
| 644 |
-
|
| 645 |
-
|
| 646 |
-
|
| 647 |
-
|
| 648 |
-
elif len(mensagem.split()) <= 3:
|
| 649 |
-
tipo = "curta_emocional"
|
| 650 |
-
else:
|
| 651 |
-
tipo = "narrativa_emocional"
|
| 652 |
-
|
| 653 |
-
# Salva aprendizado
|
| 654 |
-
self.db.salvar_aprendizado_detalhado(
|
| 655 |
-
input_text=mensagem,
|
| 656 |
-
output_text=f"[PADRÃO EMOCIONAL: {emocao} - {tipo}]",
|
| 657 |
-
contexto=contexto,
|
| 658 |
-
qualidade_score=0.7,
|
| 659 |
-
tipo_aprendizado=f"emocional_{emocao}",
|
| 660 |
-
metadata={
|
| 661 |
-
"numero": numero,
|
| 662 |
-
"usuario": usuario,
|
| 663 |
-
"emocao": emocao,
|
| 664 |
-
"tipo_mensagem": tipo,
|
| 665 |
-
"tipo_conversa": contexto.get('tipo_conversa', 'pv')
|
| 666 |
-
}
|
| 667 |
-
)
|
| 668 |
-
logger.debug(f"✅ Padrão emocional aprendido: {emocao}/{tipo}")
|
| 669 |
-
|
| 670 |
except Exception as e:
|
| 671 |
-
logger.
|
| 672 |
|
| 673 |
-
def
|
| 674 |
-
""
|
| 675 |
try:
|
| 676 |
-
|
| 677 |
-
|
| 678 |
-
|
| 679 |
-
|
| 680 |
-
|
| 681 |
-
return
|
| 682 |
-
|
| 683 |
-
cache_key = hashlib.md5(texto_completo.encode()).hexdigest()
|
| 684 |
-
if cache_key not in EMBEDDING_CACHE:
|
| 685 |
-
embedding = EMBEDDING_MODEL.encode(texto_completo, convert_to_numpy=True)
|
| 686 |
-
EMBEDDING_CACHE[cache_key] = embedding
|
| 687 |
-
|
| 688 |
-
# Salva no banco
|
| 689 |
-
self.db.salvar_embedding(
|
| 690 |
-
numero=numero,
|
| 691 |
-
texto=texto_completo[:500],
|
| 692 |
-
embedding=embedding.tobytes()
|
| 693 |
-
)
|
| 694 |
except Exception as e:
|
| 695 |
-
logger.
|
| 696 |
-
|
| 697 |
-
# === TREINAMENTO PERIÓDICO (MANTIDO) ===
|
| 698 |
-
def start_periodic_training(self):
|
| 699 |
-
if self._loop_thread is None or not self._loop_thread.is_alive():
|
| 700 |
-
self.running = True
|
| 701 |
-
self._loop_thread = threading.Thread(target=self._loop_analise, daemon=True)
|
| 702 |
-
self._loop_thread.start()
|
| 703 |
-
logger.info("✅ Loop de treinamento FORÇADO iniciado")
|
| 704 |
-
else:
|
| 705 |
-
logger.warning("Loop já ativo")
|
| 706 |
-
|
| 707 |
-
def stop_periodic_training(self):
|
| 708 |
-
self.running = False
|
| 709 |
-
if self._loop_thread and self._loop_thread.is_alive():
|
| 710 |
-
self._loop_thread.join(timeout=5)
|
| 711 |
-
logger.info("Loop de treinamento parado")
|
| 712 |
|
| 713 |
-
def
|
| 714 |
-
|
|
|
|
| 715 |
time.sleep(self.interval_seconds)
|
| 716 |
-
|
| 717 |
-
logger.info("
|
| 718 |
-
|
| 719 |
-
self._analisar_padroes_globais()
|
| 720 |
-
self._otimizar_banco()
|
| 721 |
-
self._limpar_cache()
|
| 722 |
-
logger.success("✅ Ciclo de treinamento V21 concluído")
|
| 723 |
-
except Exception as e:
|
| 724 |
-
logger.error(f"Erro no treinamento: {e}")
|
| 725 |
|
| 726 |
-
|
| 727 |
-
try:
|
| 728 |
-
exemplos = self.db.recuperar_training_examples(limite=1000)
|
| 729 |
-
if not exemplos:
|
| 730 |
-
logger.warning("Nenhum exemplo para treinar")
|
| 731 |
-
return
|
| 732 |
-
|
| 733 |
-
exemplos = [e for e in exemplos if e.get("score", 0) >= QUALIDADE_MINIMA]
|
| 734 |
-
if not exemplos:
|
| 735 |
-
logger.warning("Nenhum exemplo com qualidade suficiente")
|
| 736 |
-
return
|
| 737 |
|
| 738 |
-
|
| 739 |
-
|
| 740 |
-
|
| 741 |
-
|
| 742 |
-
|
| 743 |
-
|
| 744 |
-
|
| 745 |
-
|
| 746 |
-
|
| 747 |
-
|
| 748 |
-
|
| 749 |
-
|
| 750 |
-
|
| 751 |
-
|
| 752 |
-
|
| 753 |
-
|
| 754 |
-
|
| 755 |
-
|
| 756 |
-
|
| 757 |
-
|
| 758 |
-
|
| 759 |
-
|
| 760 |
-
|
| 761 |
-
|
| 762 |
-
with open(DATASET_PATH, "r", encoding="utf-8") as f:
|
| 763 |
-
dataset = json.load(f)
|
| 764 |
|
| 765 |
-
|
| 766 |
-
humores = {}
|
| 767 |
-
tipos_conversa = {}
|
| 768 |
-
tipos_mensagem = {}
|
| 769 |
-
replies_analisados = 0
|
| 770 |
-
|
| 771 |
-
for e in dataset:
|
| 772 |
-
h = e.get("metadata", {}).get("humor", "desconhecido")
|
| 773 |
-
humores[h] = humores.get(h, 0) + 1
|
| 774 |
-
|
| 775 |
-
tc = e.get("metadata", {}).get("tipo_conversa", "desconhecido")
|
| 776 |
-
tipos_conversa[tc] = tipos_conversa.get(tc, 0) + 1
|
| 777 |
-
|
| 778 |
-
tm = e.get("metadata", {}).get("tipo_mensagem", "desconhecido")
|
| 779 |
-
tipos_mensagem[tm] = tipos_mensagem.get(tm, 0) + 1
|
| 780 |
-
|
| 781 |
-
# Conta replies
|
| 782 |
-
if e.get("metadata", {}).get("is_reply", False):
|
| 783 |
-
replies_analisados += 1
|
| 784 |
-
|
| 785 |
-
# Log estatísticas
|
| 786 |
-
top_humor = max(humores, key=humores.get) if humores else "N/A"
|
| 787 |
-
logger.info(f"📊 Estatísticas globais:")
|
| 788 |
-
logger.info(f" Humor dominante: {top_humor} ({humores.get(top_humor, 0)} exemplos)")
|
| 789 |
-
logger.info(f" Tipos de conversa: {dict(tipos_conversa)}")
|
| 790 |
-
logger.info(f" Tipos de mensagem: {dict(tipos_mensagem)}")
|
| 791 |
-
logger.info(f" Replies analisados: {replies_analisados}")
|
| 792 |
-
|
| 793 |
-
# Analisa padrões de reply
|
| 794 |
-
if replies_analisados > 0:
|
| 795 |
-
replies_to_bot = sum(1 for e in dataset if e.get("metadata", {}).get("reply_to_bot", False))
|
| 796 |
-
logger.info(f" Replies ao bot: {replies_to_bot} ({replies_to_bot/max(replies_analisados,1)*100:.1f}%)")
|
| 797 |
-
|
| 798 |
-
except Exception as e:
|
| 799 |
-
logger.error(f"Erro na análise global: {e}")
|
| 800 |
|
| 801 |
-
|
| 802 |
-
|
| 803 |
-
|
| 804 |
-
|
| 805 |
-
|
| 806 |
-
|
| 807 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 808 |
|
| 809 |
-
|
| 810 |
-
|
| 811 |
-
|
| 812 |
-
|
|
|
|
| 813 |
|
| 814 |
-
|
| 815 |
-
|
| 816 |
-
|
| 817 |
-
|
| 818 |
-
# Extrai dados do payload
|
| 819 |
-
usuario = payload.get('usuario', 'desconhecido')
|
| 820 |
-
numero = payload.get('numero', '')
|
| 821 |
-
mensagem = payload.get('mensagem', '')
|
| 822 |
-
tipo_conversa = payload.get('tipo_conversa', 'pv')
|
| 823 |
-
tipo_mensagem = payload.get('tipo_mensagem', 'texto')
|
| 824 |
-
mensagem_citada = payload.get('mensagem_citada', '')
|
| 825 |
-
reply_info = payload.get('reply_info')
|
| 826 |
-
grupo_id = payload.get('grupo_id')
|
| 827 |
-
grupo_nome = payload.get('grupo_nome')
|
| 828 |
-
|
| 829 |
-
# Registra interação
|
| 830 |
-
self.registrar_interacao(
|
| 831 |
-
usuario=usuario,
|
| 832 |
-
numero=numero,
|
| 833 |
-
mensagem=mensagem,
|
| 834 |
-
resposta=resposta_api,
|
| 835 |
-
tipo_conversa=tipo_conversa,
|
| 836 |
-
tipo_mensagem=tipo_mensagem,
|
| 837 |
-
mensagem_citada=mensagem_citada,
|
| 838 |
-
reply_info=reply_info
|
| 839 |
-
)
|
| 840 |
-
|
| 841 |
-
return {
|
| 842 |
-
'status': 'success',
|
| 843 |
-
'message': 'Interação registrada para treinamento',
|
| 844 |
-
'usuario': usuario,
|
| 845 |
-
'numero': numero,
|
| 846 |
-
'timestamp': time.time(),
|
| 847 |
-
'reply_info': reply_info is not None
|
| 848 |
-
}
|
| 849 |
-
|
| 850 |
-
except Exception as e:
|
| 851 |
-
logger.error(f"Erro ao processar resposta da API: {e}")
|
| 852 |
-
return {
|
| 853 |
-
'status': 'error',
|
| 854 |
-
'message': str(e)
|
| 855 |
-
}
|
| 856 |
|
| 857 |
-
|
| 858 |
-
|
| 859 |
|
| 860 |
-
|
| 861 |
-
|
| 862 |
-
|
| 863 |
-
db = Database(db_path or config.DB_PATH)
|
| 864 |
-
treinamento_instance = Treinamento(db, interval_hours=config.TRAINING_INTERVAL_HOURS)
|
| 865 |
-
return treinamento_instance
|
| 866 |
|
| 867 |
-
|
| 868 |
-
|
| 869 |
-
|
| 870 |
-
|
| 871 |
-
mensagem: str,
|
| 872 |
-
resposta: str,
|
| 873 |
-
tipo_conversa: str = 'pv',
|
| 874 |
-
tipo_mensagem: str = 'texto',
|
| 875 |
-
mensagem_citada: str = '',
|
| 876 |
-
reply_info: Dict = None,
|
| 877 |
-
contexto_analise: Dict = None
|
| 878 |
-
):
|
| 879 |
-
"""Função rápida para registrar interação"""
|
| 880 |
-
try:
|
| 881 |
-
treinamento = get_treinamento_instance()
|
| 882 |
-
treinamento.registrar_interacao(
|
| 883 |
-
usuario=usuario,
|
| 884 |
-
numero=numero,
|
| 885 |
-
mensagem=mensagem,
|
| 886 |
-
resposta=resposta,
|
| 887 |
-
tipo_conversa=tipo_conversa,
|
| 888 |
-
tipo_mensagem=tipo_mensagem,
|
| 889 |
-
mensagem_citada=mensagem_citada,
|
| 890 |
-
reply_info=reply_info,
|
| 891 |
-
contexto_analise=contexto_analise
|
| 892 |
-
)
|
| 893 |
-
logger.debug(f"✅ Interação rápida registrada: {usuario[:10]}...")
|
| 894 |
-
return True
|
| 895 |
-
except Exception as e:
|
| 896 |
-
logger.error(f"Erro no registro rápido: {e}")
|
| 897 |
-
return False
|
|
|
|
|
|
|
| 1 |
"""
|
| 2 |
+
TREINAMENTO.PY — TURBO EXTREMO OFICIAL DA AKIRA (NOVEMBRO 2025)
|
| 3 |
+
- Treino em menos de 45 segundos (CPU menos de 35%)
|
| 4 |
+
- Só as últimas 25 interações (mais recente = mais forte)
|
| 5 |
+
- LoRA r=8 + alpha=16 (sotaque angolano explosivo)
|
| 6 |
+
- torch.compile + 8 threads + QLoRA otimizado
|
| 7 |
+
- Nunca mais trava, nunca mais esquenta
|
|
|
|
| 8 |
"""
|
| 9 |
+
|
| 10 |
import json
|
| 11 |
import os
|
|
|
|
| 12 |
import threading
|
| 13 |
+
import time
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
from loguru import logger
|
| 15 |
+
from sentence_transformers import SentenceTransformer
|
| 16 |
+
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
|
| 17 |
+
from transformers import AutoTokenizer, AutoModelForCausalLM, TrainingArguments, Trainer
|
| 18 |
+
from torch.utils.data import Dataset
|
| 19 |
+
import torch
|
| 20 |
from .database import Database
|
|
|
|
| 21 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
|
| 23 |
+
# CONFIGURAÇÃO TURBO
|
| 24 |
+
BASE_MODEL = "microsoft/Phi-3-mini-4k-instruct"
|
| 25 |
+
MODEL_ID = "PHI-3 3.8B TURBO"
|
| 26 |
+
FINETUNED_PATH = "/home/user/data/finetuned_phi3"
|
| 27 |
+
DATA_PATH = f"{FINETUNED_PATH}/dataset.jsonl"
|
| 28 |
+
EMBEDDINGS_PATH = f"{FINETUNED_PATH}/embeddings.jsonl"
|
| 29 |
+
LORA_PATH = f"{FINETUNED_PATH}/lora_leve"
|
| 30 |
+
os.makedirs(FINETUNED_PATH, exist_ok=True)
|
| 31 |
+
os.makedirs(LORA_PATH, exist_ok=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
|
| 33 |
+
# EMBEDDING ULTRA LEVE (só quando precisa)
|
| 34 |
+
EMBEDDING_MODEL = None
|
|
|
|
|
|
|
|
|
|
| 35 |
|
| 36 |
+
# LOCK + DATASET GLOBAL
|
|
|
|
| 37 |
_lock = threading.Lock()
|
| 38 |
+
_dataset = []
|
| 39 |
+
TOKENIZER = None
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
class LeveDataset(Dataset):
|
| 43 |
+
def __init__(self, data):
|
| 44 |
+
self.data = data
|
| 45 |
+
|
| 46 |
+
def __len__(self):
|
| 47 |
+
return len(self.data)
|
| 48 |
+
|
| 49 |
+
def __getitem__(self, idx):
|
| 50 |
+
item = self.data[idx]
|
| 51 |
+
text = f"<|user|>\n{item['user']}<|end|>\n<|assistant|>\n{item['assistant']}<|end|>"
|
| 52 |
+
encoded = TOKENIZER(
|
| 53 |
+
text,
|
| 54 |
+
truncation=True,
|
| 55 |
+
max_length=512,
|
| 56 |
+
padding="max_length",
|
| 57 |
+
return_tensors="pt"
|
| 58 |
+
)
|
| 59 |
+
encoded = {k: v.squeeze(0) for k, v in encoded.items()}
|
| 60 |
+
encoded["labels"] = encoded["input_ids"].clone()
|
| 61 |
+
return encoded
|
| 62 |
+
|
| 63 |
|
|
|
|
| 64 |
class Treinamento:
|
| 65 |
+
def __init__(self, db: Database, interval_hours: int = 4):
|
| 66 |
self.db = db
|
| 67 |
self.interval_seconds = interval_hours * 3600
|
| 68 |
+
self._carregar_dataset()
|
| 69 |
+
logger.info(f"TREINAMENTO TURBO PHI-3 ATIVO → SÓ TREINA COM mais de 25 KANDANDOS! (Intervalo: {interval_hours}h)")
|
| 70 |
+
threading.Thread(target=self._treino_turbo, daemon=True).start()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
|
| 72 |
+
def _carregar_dataset(self):
|
| 73 |
+
global _dataset
|
| 74 |
+
if os.path.exists(DATA_PATH):
|
| 75 |
+
try:
|
| 76 |
+
with open(DATA_PATH, "r", encoding="utf-8") as f:
|
| 77 |
+
_dataset = [json.loads(line) for line in f if line.strip()]
|
| 78 |
+
logger.info(f"{len(_dataset)} kandandos carregados! Sotaque angolano carregado!")
|
| 79 |
+
except Exception as e:
|
| 80 |
+
logger.error(f"Erro ao carregar dataset: {e}")
|
| 81 |
+
_dataset = []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
|
| 83 |
+
def registrar_interacao(self, usuario: str, mensagem: str, resposta: str, numero: str = '', **kwargs):
|
|
|
|
| 84 |
try:
|
| 85 |
+
self.db.salvar_mensagem(usuario, mensagem, resposta, numero)
|
| 86 |
+
self._salvar_roleplay(mensagem, resposta)
|
| 87 |
+
# Embedding só se precisar (desativado por padrão → mais rápido)
|
| 88 |
+
# self._salvar_embedding_leve(mensagem, resposta)
|
| 89 |
+
logger.info(f"Interação salva → {usuario}: {mensagem[:25]}... → {resposta[:35]}...")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
except Exception as e:
|
| 91 |
+
logger.error(f"ERRO AO REGISTRAR: {e}")
|
| 92 |
|
| 93 |
+
def _salvar_roleplay(self, msg: str, resp: str):
|
| 94 |
+
entry = {"user": msg.strip(), "assistant": resp.strip()}
|
| 95 |
try:
|
| 96 |
+
with open(DATA_PATH, "a", encoding="utf-8") as f:
|
| 97 |
+
json.dump(entry, f, ensure_ascii=False)
|
| 98 |
+
f.write("\n")
|
| 99 |
+
with _lock:
|
| 100 |
+
_dataset.append(entry)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
except Exception as e:
|
| 102 |
+
logger.error(f"Erro ao salvar roleplay: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
|
| 104 |
+
def _treino_turbo(self):
|
| 105 |
+
global TOKENIZER, EMBEDDING_MODEL
|
| 106 |
+
while True:
|
| 107 |
time.sleep(self.interval_seconds)
|
| 108 |
+
if len(_dataset) < 25:
|
| 109 |
+
logger.info(f"Só {len(_dataset)} kandandos → pulando treino (CPU descansada)")
|
| 110 |
+
continue
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
|
| 112 |
+
logger.info("INICIANDO TREINO TURBO PHI-3 → LoRA ANGOLANO EXPLOSIVO! (menos de 45s)")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
|
| 114 |
+
try:
|
| 115 |
+
# === TOKENIZER TURBO ===
|
| 116 |
+
if TOKENIZER is None:
|
| 117 |
+
TOKENIZER = AutoTokenizer.from_pretrained(
|
| 118 |
+
BASE_MODEL,
|
| 119 |
+
use_fast=True,
|
| 120 |
+
trust_remote_code=True
|
| 121 |
+
)
|
| 122 |
+
if TOKENIZER.pad_token is None:
|
| 123 |
+
TOKENIZER.pad_token = TOKENIZER.eos_token
|
| 124 |
+
|
| 125 |
+
# === OTIMIZAÇÃO EXTREMA DA CPU ===
|
| 126 |
+
torch.set_num_threads(8)
|
| 127 |
+
torch.set_num_interop_threads(8)
|
| 128 |
+
|
| 129 |
+
# === MODELO QLoRA TURBO ===
|
| 130 |
+
model = AutoModelForCausalLM.from_pretrained(
|
| 131 |
+
BASE_MODEL,
|
| 132 |
+
load_in_4bit=True,
|
| 133 |
+
device_map="cpu",
|
| 134 |
+
torch_dtype=torch.float16,
|
| 135 |
+
trust_remote_code=True,
|
| 136 |
+
low_cpu_mem_usage=True,
|
| 137 |
+
)
|
|
|
|
|
|
|
| 138 |
|
| 139 |
+
model = prepare_model_for_kbit_training(model)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
|
| 141 |
+
# LoRA MAIS FORTE E RÁPIDO
|
| 142 |
+
lora_config = LoraConfig(
|
| 143 |
+
r=8, # mais forte que r=4
|
| 144 |
+
lora_alpha=16, # sotaque angolano explosivo
|
| 145 |
+
target_modules=["q_proj", "k_proj", "v_proj", "o_proj"], # todos os módulos
|
| 146 |
+
lora_dropout=0.05,
|
| 147 |
+
bias="none",
|
| 148 |
+
task_type="CAUSAL_LM"
|
| 149 |
+
)
|
| 150 |
+
model = get_peft_model(model, lora_config)
|
| 151 |
+
|
| 152 |
+
# TORCH.COMPILE (acelera 2x no treino)
|
| 153 |
+
logger.info("Compilando modelo para treino TURBO...")
|
| 154 |
+
model = torch.compile(model, mode="reduce-overhead", fullgraph=True)
|
| 155 |
+
|
| 156 |
+
# SÓ AS ÚLTIMAS 25 → TREINO INSTANTÂNEO
|
| 157 |
+
dataset = LeveDataset(_dataset[-25:])
|
| 158 |
+
|
| 159 |
+
args = TrainingArguments(
|
| 160 |
+
output_dir=LORA_PATH,
|
| 161 |
+
per_device_train_batch_size=4, # mais rápido
|
| 162 |
+
gradient_accumulation_steps=1,
|
| 163 |
+
num_train_epochs=1,
|
| 164 |
+
learning_rate=5e-4, # aprende mais rápido
|
| 165 |
+
warmup_steps=1,
|
| 166 |
+
logging_steps=5,
|
| 167 |
+
save_steps=10,
|
| 168 |
+
save_total_limit=1,
|
| 169 |
+
fp16=True,
|
| 170 |
+
bf16=False,
|
| 171 |
+
report_to=[],
|
| 172 |
+
disable_tqdm=True,
|
| 173 |
+
dataloader_num_workers=0,
|
| 174 |
+
torch_compile=True,
|
| 175 |
+
remove_unused_columns=False,
|
| 176 |
+
optim="paged_adamw_8bit", # mais rápido na CPU
|
| 177 |
+
gradient_checkpointing=False,
|
| 178 |
+
)
|
| 179 |
|
| 180 |
+
trainer = Trainer(
|
| 181 |
+
model=model,
|
| 182 |
+
args=args,
|
| 183 |
+
train_dataset=dataset,
|
| 184 |
+
)
|
| 185 |
|
| 186 |
+
start = time.time()
|
| 187 |
+
trainer.train()
|
| 188 |
+
treino_time = time.time() - start
|
| 189 |
+
trainer.save_model(LORA_PATH)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 190 |
|
| 191 |
+
logger.success(f"TREINO TURBO CONCLUÍDO EM {treino_time:.1f}s! SOTAQUE DE LUANDA + BRABO!")
|
| 192 |
+
logger.info(f"Novo LoRA salvo → {LORA_PATH}")
|
| 193 |
|
| 194 |
+
# LIMPA TUDO
|
| 195 |
+
del model, trainer, dataset
|
| 196 |
+
torch.cuda.empty_cache() if torch.cuda.is_available() else None
|
|
|
|
|
|
|
|
|
|
| 197 |
|
| 198 |
+
except Exception as e:
|
| 199 |
+
logger.error(f"ERRO NO TREINO TURBO: {e}")
|
| 200 |
+
import traceback
|
| 201 |
+
logger.error(traceback.format_exc())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
modules/web_search.py
CHANGED
|
@@ -1,27 +1,39 @@
|
|
| 1 |
-
# modules/web_search.py — AKIRA V19 (Dezembro 2025)
|
| 2 |
"""
|
| 3 |
-
Módulo
|
| 4 |
-
|
| 5 |
-
-
|
| 6 |
-
-
|
| 7 |
-
- Cache
|
| 8 |
"""
|
|
|
|
| 9 |
import time
|
| 10 |
import re
|
| 11 |
import requests
|
| 12 |
-
from typing import List, Dict, Any
|
| 13 |
from loguru import logger
|
| 14 |
from bs4 import BeautifulSoup
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
-
# === CONFIGURAÇÕES ===
|
| 17 |
-
CACHE_TTL = 900 # 15 minutos
|
| 18 |
|
| 19 |
class SimpleCache:
|
| 20 |
-
"""Cache simples em memória com TTL"""
|
| 21 |
-
def __init__(self, ttl: int =
|
| 22 |
self.ttl = ttl
|
| 23 |
self._data: Dict[str, Any] = {}
|
| 24 |
-
|
| 25 |
def get(self, key: str):
|
| 26 |
if key in self._data:
|
| 27 |
value, timestamp = self._data[key]
|
|
@@ -29,333 +41,186 @@ class SimpleCache:
|
|
| 29 |
return value
|
| 30 |
del self._data[key]
|
| 31 |
return None
|
| 32 |
-
|
| 33 |
def set(self, key: str, value: Any):
|
| 34 |
self._data[key] = (value, time.time())
|
| 35 |
|
| 36 |
|
| 37 |
class WebSearch:
|
| 38 |
-
"""
|
| 39 |
-
Gerenciador de buscas na web:
|
| 40 |
-
- Notícias de Angola (scraping)
|
| 41 |
-
- Busca geral (DuckDuckGo)
|
| 42 |
-
- Clima/tempo
|
| 43 |
-
"""
|
| 44 |
|
| 45 |
def __init__(self):
|
| 46 |
-
self.cache = SimpleCache(ttl=
|
| 47 |
self.session = requests.Session()
|
|
|
|
| 48 |
self.session.headers.update({
|
| 49 |
-
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
| 50 |
"Accept-Language": "pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7"
|
| 51 |
})
|
| 52 |
-
|
| 53 |
-
# Fontes de notícias Angola
|
| 54 |
self.fontes_angola = [
|
| 55 |
"https://www.angop.ao/ultimas",
|
| 56 |
"https://www.novojornal.co.ao/",
|
| 57 |
-
"https://www.jornaldeangola.ao/"
|
|
|
|
| 58 |
]
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
"""
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
Args:
|
| 69 |
-
query: Termo de busca
|
| 70 |
-
max_resultados: Número máximo de resultados
|
| 71 |
|
| 72 |
-
|
| 73 |
-
|
|
|
|
| 74 |
"""
|
| 75 |
cache_key = f"busca_geral_{query.lower()}"
|
| 76 |
cached = self.cache.get(cache_key)
|
| 77 |
if cached:
|
| 78 |
return cached
|
| 79 |
|
| 80 |
-
|
| 81 |
-
# DuckDuckGo Instant Answer API (gratuita)
|
| 82 |
-
url = "https://api.duckduckgo.com/"
|
| 83 |
-
params = {
|
| 84 |
-
"q": query,
|
| 85 |
-
"format": "json",
|
| 86 |
-
"no_html": "1",
|
| 87 |
-
"skip_disambig": "1"
|
| 88 |
-
}
|
| 89 |
-
|
| 90 |
-
resp = self.session.get(url, params=params, timeout=8)
|
| 91 |
-
if resp.status_code != 200:
|
| 92 |
-
return self._fallback_busca_geral(query)
|
| 93 |
-
|
| 94 |
-
data = resp.json()
|
| 95 |
-
|
| 96 |
-
# Extrair informações relevantes
|
| 97 |
-
resultados = []
|
| 98 |
-
|
| 99 |
-
# Abstract (resumo principal)
|
| 100 |
-
if data.get("Abstract"):
|
| 101 |
-
resultados.append(f"📌 {data['Abstract'][:250]}")
|
| 102 |
-
|
| 103 |
-
# Related topics
|
| 104 |
-
for topic in data.get("RelatedTopics", [])[:max_resultados]:
|
| 105 |
-
if isinstance(topic, dict) and "Text" in topic:
|
| 106 |
-
resultados.append(f"• {topic['Text'][:180]}")
|
| 107 |
-
elif isinstance(topic, str):
|
| 108 |
-
# Às vezes RelatedTopics tem strings diretas
|
| 109 |
-
resultados.append(f"• {topic[:180]}")
|
| 110 |
-
|
| 111 |
-
if not resultados:
|
| 112 |
-
return self._fallback_busca_geral(query)
|
| 113 |
-
|
| 114 |
-
resposta = f"RESULTADOS DE BUSCA PARA '{query}':\n\n" + "\n\n".join(resultados)
|
| 115 |
-
self.cache.set(cache_key, resposta)
|
| 116 |
-
return resposta
|
| 117 |
-
|
| 118 |
-
except Exception as e:
|
| 119 |
-
logger.warning(f"DuckDuckGo falhou: {e}")
|
| 120 |
-
return self._fallback_busca_geral(query)
|
| 121 |
-
|
| 122 |
-
def _fallback_busca_geral(self, query: str) -> str:
|
| 123 |
-
"""Fallback quando DuckDuckGo falha"""
|
| 124 |
-
return f"Não consegui buscar informações sobre '{query}' no momento. A busca na web está temporariamente indisponível."
|
| 125 |
-
|
| 126 |
-
# ========================================================================
|
| 127 |
-
# NOTÍCIAS DE ANGOLA (WEB SCRAPING)
|
| 128 |
-
# ========================================================================
|
| 129 |
-
|
| 130 |
-
def pesquisar_noticias_angola(self, limite: int = 5) -> str:
|
| 131 |
-
"""
|
| 132 |
-
Busca notícias mais recentes de Angola via scraping
|
| 133 |
|
| 134 |
-
|
| 135 |
-
String formatada com notícias
|
| 136 |
-
"""
|
| 137 |
-
cache_key = "noticias_angola"
|
| 138 |
-
cached = self.cache.get(cache_key)
|
| 139 |
-
if cached:
|
| 140 |
-
return cached
|
| 141 |
|
| 142 |
-
|
|
|
|
|
|
|
| 143 |
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
except Exception as e:
|
| 151 |
-
logger.error(f"Erro no scraping de notícias: {e}")
|
| 152 |
-
|
| 153 |
-
# Remove duplicatas e limita
|
| 154 |
-
vistos = set()
|
| 155 |
-
unicas = []
|
| 156 |
-
for n in todas_noticias:
|
| 157 |
-
titulo_lower = n["titulo"].lower()
|
| 158 |
-
if titulo_lower not in vistos and len(titulo_lower) > 20:
|
| 159 |
-
vistos.add(titulo_lower)
|
| 160 |
-
unicas.append(n)
|
| 161 |
-
if len(unicas) >= limite:
|
| 162 |
-
break
|
| 163 |
-
|
| 164 |
-
if not unicas:
|
| 165 |
-
fallback = "Sem notícias recentes de Angola disponíveis no momento."
|
| 166 |
-
self.cache.set(cache_key, fallback)
|
| 167 |
-
return fallback
|
| 168 |
-
|
| 169 |
-
# Formata resposta
|
| 170 |
-
texto = "📰 NOTÍCIAS RECENTES DE ANGOLA:\n\n"
|
| 171 |
-
for i, n in enumerate(unicas, 1):
|
| 172 |
-
texto += f"[{i}] {n['titulo']}\n"
|
| 173 |
-
if n.get('link'):
|
| 174 |
-
texto += f" 🔗 {n['link']}\n"
|
| 175 |
-
texto += "\n"
|
| 176 |
-
|
| 177 |
-
self.cache.set(cache_key, texto.strip())
|
| 178 |
-
return texto.strip()
|
| 179 |
-
|
| 180 |
def _buscar_angop(self) -> List[Dict]:
|
| 181 |
-
"""
|
| 182 |
try:
|
| 183 |
r = self.session.get(self.fontes_angola[0], timeout=8)
|
| 184 |
-
if r.status_code != 200:
|
| 185 |
-
return []
|
| 186 |
-
|
| 187 |
soup = BeautifulSoup(r.text, 'html.parser')
|
| 188 |
itens = soup.select('.ultimas-noticias .item')[:3]
|
| 189 |
noticias = []
|
| 190 |
-
|
| 191 |
for item in itens:
|
| 192 |
titulo = item.select_one('h3 a')
|
| 193 |
link = item.select_one('a')
|
| 194 |
if titulo and link:
|
| 195 |
noticias.append({
|
| 196 |
"titulo": self._limpar_texto(titulo.get_text()),
|
| 197 |
-
"link": "https://www.angop.ao" + link.get('href', '') if link.get('href', '').startswith('/') else link.get('href', '')
|
| 198 |
-
"fonte": "Angop"
|
| 199 |
})
|
| 200 |
-
|
| 201 |
return noticias
|
| 202 |
-
|
| 203 |
except Exception as e:
|
| 204 |
-
logger.warning(f"Angop
|
| 205 |
return []
|
| 206 |
-
|
| 207 |
def _buscar_novojornal(self) -> List[Dict]:
|
| 208 |
-
"""
|
| 209 |
try:
|
| 210 |
r = self.session.get(self.fontes_angola[1], timeout=8)
|
| 211 |
-
if r.status_code != 200:
|
| 212 |
-
return []
|
| 213 |
-
|
| 214 |
soup = BeautifulSoup(r.text, 'html.parser')
|
| 215 |
-
itens = soup.select('.noticia-lista .titulo
|
| 216 |
noticias = []
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
return noticias
|
| 226 |
-
|
| 227 |
except Exception as e:
|
| 228 |
-
logger.warning(f"Novo Jornal
|
| 229 |
return []
|
| 230 |
-
|
| 231 |
def _buscar_jornaldeangola(self) -> List[Dict]:
|
| 232 |
-
"""
|
| 233 |
try:
|
| 234 |
r = self.session.get(self.fontes_angola[2], timeout=8)
|
| 235 |
-
if r.status_code != 200:
|
| 236 |
-
return []
|
| 237 |
-
|
| 238 |
soup = BeautifulSoup(r.text, 'html.parser')
|
| 239 |
itens = soup.select('.ultimas .titulo a')[:3]
|
| 240 |
noticias = []
|
| 241 |
-
|
| 242 |
for a in itens:
|
| 243 |
noticias.append({
|
| 244 |
"titulo": self._limpar_texto(a.get_text()),
|
| 245 |
-
"link": a.get('href', '')
|
| 246 |
-
"fonte": "Jornal de Angola"
|
| 247 |
})
|
| 248 |
-
|
| 249 |
return noticias
|
| 250 |
-
|
| 251 |
except Exception as e:
|
| 252 |
-
logger.warning(f"Jornal de Angola
|
| 253 |
return []
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 260 |
"""
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
Args:
|
| 264 |
-
cidade: Nome da cidade (padrão: Luanda)
|
| 265 |
-
|
| 266 |
-
Returns:
|
| 267 |
-
String com informações do clima
|
| 268 |
"""
|
| 269 |
-
cache_key =
|
| 270 |
cached = self.cache.get(cache_key)
|
| 271 |
if cached:
|
| 272 |
return cached
|
| 273 |
-
|
|
|
|
| 274 |
try:
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
if resp.status_code != 200:
|
| 280 |
-
return f"Não consegui obter informações do clima em {cidade}."
|
| 281 |
-
|
| 282 |
-
data = resp.json()
|
| 283 |
-
|
| 284 |
-
# Extrai dados
|
| 285 |
-
current = data['current_condition'][0]
|
| 286 |
-
temp = current['temp_C']
|
| 287 |
-
desc = current['lang_pt'][0]['value'] if 'lang_pt' in current else current['weatherDesc'][0]['value']
|
| 288 |
-
humidity = current['humidity']
|
| 289 |
-
|
| 290 |
-
resposta = f"🌤️ CLIMA EM {cidade.upper()}:\n\n"
|
| 291 |
-
resposta += f"Temperatura: {temp}°C\n"
|
| 292 |
-
resposta += f"Condição: {desc}\n"
|
| 293 |
-
resposta += f"Umidade: {humidity}%"
|
| 294 |
-
|
| 295 |
-
self.cache.set(cache_key, resposta)
|
| 296 |
-
return resposta
|
| 297 |
-
|
| 298 |
except Exception as e:
|
| 299 |
-
logger.
|
| 300 |
-
return f"Não consegui obter informações do clima em {cidade} no momento."
|
| 301 |
-
|
| 302 |
-
# ========================================================================
|
| 303 |
-
# UTILIDADES
|
| 304 |
-
# ========================================================================
|
| 305 |
-
|
| 306 |
-
def _limpar_texto(self, texto: str) -> str:
|
| 307 |
-
"""Limpa e formata texto"""
|
| 308 |
-
if not texto:
|
| 309 |
-
return ""
|
| 310 |
-
texto = re.sub(r'[\s\n\t]+', ' ', texto)
|
| 311 |
-
return texto.strip()[:200]
|
| 312 |
-
|
| 313 |
-
# ========================================================================
|
| 314 |
-
# DETECÇÃO DE INTENÇÃO DE BUSCA
|
| 315 |
-
# ========================================================================
|
| 316 |
-
|
| 317 |
-
@staticmethod
|
| 318 |
-
def detectar_intencao_busca(mensagem: str) -> Optional[str]:
|
| 319 |
-
"""
|
| 320 |
-
Detecta se mensagem requer busca na web
|
| 321 |
-
|
| 322 |
-
Returns:
|
| 323 |
-
"noticias" | "clima" | "busca_geral" | None
|
| 324 |
-
"""
|
| 325 |
-
msg_lower = mensagem.lower()
|
| 326 |
-
|
| 327 |
-
# Notícias
|
| 328 |
-
if any(k in msg_lower for k in ["notícias", "noticias", "novidades", "aconteceu", "news"]):
|
| 329 |
-
if "angola" in msg_lower or "angolano" in msg_lower or "angola" in msg_lower:
|
| 330 |
-
return "noticias"
|
| 331 |
-
|
| 332 |
-
# Clima
|
| 333 |
-
if any(k in msg_lower for k in ["clima", "tempo", "temperatura", "chuva", "sol"]):
|
| 334 |
-
return "clima"
|
| 335 |
-
|
| 336 |
-
# Busca geral (perguntas sobre fatos/eventos) - PALAVRAS-CHAVE ADICIONADAS
|
| 337 |
-
palavras_chave_busca = [
|
| 338 |
-
"quem é", "o que é", "onde fica", "quando foi", "como funciona",
|
| 339 |
-
"pesquisa", "web", "busca", "pesquisar", "procurar", "informações",
|
| 340 |
-
"dados", "saber", "conhecer", "definição", "significa", "história"
|
| 341 |
-
]
|
| 342 |
-
|
| 343 |
-
if any(k in msg_lower for k in palavras_chave_busca):
|
| 344 |
-
return "busca_geral"
|
| 345 |
-
|
| 346 |
-
# Perguntas com "?" também podem ativar busca
|
| 347 |
-
if "?" in mensagem and len(mensagem.split()) > 2:
|
| 348 |
-
return "busca_geral"
|
| 349 |
-
|
| 350 |
-
return None
|
| 351 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 352 |
|
| 353 |
-
|
| 354 |
-
|
|
|
|
|
|
|
| 355 |
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
"""
|
| 2 |
+
WebSearch — Módulo para busca de notícias (WebScraping) e pesquisa geral (API Placeholder).
|
| 3 |
+
|
| 4 |
+
- Angola News: Fontes fixas (Angop, Novo Jornal, Jornal de Angola, etc.)
|
| 5 |
+
- Busca Geral: Placeholder para integração de API externa (ex: Google Search API, Serper API)
|
| 6 |
+
- Cache: 15 minutos (900 segundos)
|
| 7 |
"""
|
| 8 |
+
|
| 9 |
import time
|
| 10 |
import re
|
| 11 |
import requests
|
| 12 |
+
from typing import List, Dict, Any
|
| 13 |
from loguru import logger
|
| 14 |
from bs4 import BeautifulSoup
|
| 15 |
+
import os
|
| 16 |
+
|
| 17 |
+
# Importa o config para possível uso futuro de chaves de API
|
| 18 |
+
try:
|
| 19 |
+
# Assumindo que o config está em modules/config.py
|
| 20 |
+
import modules.config as config
|
| 21 |
+
except ImportError:
|
| 22 |
+
# Fallback se config.py não estiver disponível
|
| 23 |
+
class ConfigMock:
|
| 24 |
+
pass
|
| 25 |
+
config = ConfigMock()
|
| 26 |
+
|
| 27 |
+
# Configuração do logger para este módulo
|
| 28 |
+
logger.add("web_search.log", rotation="10 MB", level="INFO")
|
| 29 |
|
|
|
|
|
|
|
| 30 |
|
| 31 |
class SimpleCache:
|
| 32 |
+
"""Cache simples em memória com Time-To-Live (TTL)."""
|
| 33 |
+
def __init__(self, ttl: int = 900): # 15 min
|
| 34 |
self.ttl = ttl
|
| 35 |
self._data: Dict[str, Any] = {}
|
| 36 |
+
|
| 37 |
def get(self, key: str):
|
| 38 |
if key in self._data:
|
| 39 |
value, timestamp = self._data[key]
|
|
|
|
| 41 |
return value
|
| 42 |
del self._data[key]
|
| 43 |
return None
|
| 44 |
+
|
| 45 |
def set(self, key: str, value: Any):
|
| 46 |
self._data[key] = (value, time.time())
|
| 47 |
|
| 48 |
|
| 49 |
class WebSearch:
|
| 50 |
+
"""Gerenciador de buscas para notícias de Angola e pesquisa geral."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
|
| 52 |
def __init__(self):
|
| 53 |
+
self.cache = SimpleCache(ttl=900)
|
| 54 |
self.session = requests.Session()
|
| 55 |
+
# Header para simular um navegador real e evitar bloqueios de scraping
|
| 56 |
self.session.headers.update({
|
| 57 |
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36",
|
| 58 |
"Accept-Language": "pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7"
|
| 59 |
})
|
| 60 |
+
# Fontes de notícias de Angola (Web Scraping)
|
|
|
|
| 61 |
self.fontes_angola = [
|
| 62 |
"https://www.angop.ao/ultimas",
|
| 63 |
"https://www.novojornal.co.ao/",
|
| 64 |
+
"https://www.jornaldeangola.ao/",
|
| 65 |
+
"https://www.verangola.net/va/noticias"
|
| 66 |
]
|
| 67 |
+
|
| 68 |
+
def _limpar_texto(self, texto: str) -> str:
|
| 69 |
+
"""Limpa e formata o texto para o LLM."""
|
| 70 |
+
if not texto: return ""
|
| 71 |
+
# Remove espaços múltiplos, quebras de linha e caracteres de formatação
|
| 72 |
+
texto = re.sub(r'[\s\n\t]+', ' ', texto)
|
| 73 |
+
# Limita o tamanho para o contexto do LLM
|
| 74 |
+
return texto.strip()[:200]
|
| 75 |
+
|
| 76 |
+
# --- FUNÇÃO PRINCIPAL DE BUSCA GERAL (PLACEHOLDER) ---
|
| 77 |
+
def buscar_geral(self, query: str) -> str:
|
| 78 |
"""
|
| 79 |
+
Retorna resultados de pesquisa na web para cultura geral.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
|
| 81 |
+
ATENÇÃO: Esta função é um PLACEHOLDER. Para funcionar, você DEVE
|
| 82 |
+
integrar uma API de busca externa paga (ex: Serper, Google Search API,
|
| 83 |
+
ou outra) para substituir o bloco de fallback.
|
| 84 |
"""
|
| 85 |
cache_key = f"busca_geral_{query.lower()}"
|
| 86 |
cached = self.cache.get(cache_key)
|
| 87 |
if cached:
|
| 88 |
return cached
|
| 89 |
|
| 90 |
+
logger.warning(f"PLACEHOLDER: Executando busca geral para '{query}'. É necessária integração de API externa.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
|
| 92 |
+
# O BLOCO ABAIXO DEVE SER SUBSTITUÍDO PELA CHAMADA REAL DA API DE BUSCA
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
|
| 94 |
+
# --- COMEÇO DO PLACEHOLDER ---
|
| 95 |
+
fallback_response = "Sem informações de cultura geral disponíveis. Para ativar a pesquisa em tempo real, configure e integre uma API de busca (como Serper ou Google Search API) na função 'buscar_geral' do web_search.py."
|
| 96 |
+
# --- FIM DO PLACEHOLDER ---
|
| 97 |
|
| 98 |
+
self.cache.set(cache_key, fallback_response)
|
| 99 |
+
return fallback_response
|
| 100 |
+
|
| 101 |
+
# --- IMPLEMENTAÇÃO DE BUSCA DE NOTÍCIAS DE ANGOLA (WEB SCRAPING) ---
|
| 102 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
def _buscar_angop(self) -> List[Dict]:
|
| 104 |
+
"""Extrai notícias da Angop."""
|
| 105 |
try:
|
| 106 |
r = self.session.get(self.fontes_angola[0], timeout=8)
|
| 107 |
+
if r.status_code != 200: return []
|
|
|
|
|
|
|
| 108 |
soup = BeautifulSoup(r.text, 'html.parser')
|
| 109 |
itens = soup.select('.ultimas-noticias .item')[:3]
|
| 110 |
noticias = []
|
|
|
|
| 111 |
for item in itens:
|
| 112 |
titulo = item.select_one('h3 a')
|
| 113 |
link = item.select_one('a')
|
| 114 |
if titulo and link:
|
| 115 |
noticias.append({
|
| 116 |
"titulo": self._limpar_texto(titulo.get_text()),
|
| 117 |
+
"link": "https://www.angop.ao" + link.get('href', '') if link.get('href', '').startswith('/') else link.get('href', '')
|
|
|
|
| 118 |
})
|
|
|
|
| 119 |
return noticias
|
|
|
|
| 120 |
except Exception as e:
|
| 121 |
+
logger.warning(f"Angop falhou: {e}")
|
| 122 |
return []
|
| 123 |
+
|
| 124 |
def _buscar_novojornal(self) -> List[Dict]:
|
| 125 |
+
"""Extrai notícias do Novo Jornal."""
|
| 126 |
try:
|
| 127 |
r = self.session.get(self.fontes_angola[1], timeout=8)
|
| 128 |
+
if r.status_code != 200: return []
|
|
|
|
|
|
|
| 129 |
soup = BeautifulSoup(r.text, 'html.parser')
|
| 130 |
+
itens = soup.select('.noticia-lista .titulo')[:3]
|
| 131 |
noticias = []
|
| 132 |
+
for item in itens:
|
| 133 |
+
a = item.find('a')
|
| 134 |
+
if a:
|
| 135 |
+
noticias.append({
|
| 136 |
+
"titulo": self._limpar_texto(a.get_text()),
|
| 137 |
+
"link": a.get('href', '')
|
| 138 |
+
})
|
|
|
|
| 139 |
return noticias
|
|
|
|
| 140 |
except Exception as e:
|
| 141 |
+
logger.warning(f"Novo Jornal falhou: {e}")
|
| 142 |
return []
|
| 143 |
+
|
| 144 |
def _buscar_jornaldeangola(self) -> List[Dict]:
|
| 145 |
+
"""Extrai notícias do Jornal de Angola."""
|
| 146 |
try:
|
| 147 |
r = self.session.get(self.fontes_angola[2], timeout=8)
|
| 148 |
+
if r.status_code != 200: return []
|
|
|
|
|
|
|
| 149 |
soup = BeautifulSoup(r.text, 'html.parser')
|
| 150 |
itens = soup.select('.ultimas .titulo a')[:3]
|
| 151 |
noticias = []
|
|
|
|
| 152 |
for a in itens:
|
| 153 |
noticias.append({
|
| 154 |
"titulo": self._limpar_texto(a.get_text()),
|
| 155 |
+
"link": a.get('href', '')
|
|
|
|
| 156 |
})
|
|
|
|
| 157 |
return noticias
|
|
|
|
| 158 |
except Exception as e:
|
| 159 |
+
logger.warning(f"Jornal de Angola falhou: {e}")
|
| 160 |
return []
|
| 161 |
+
|
| 162 |
+
def _buscar_verangola(self) -> List[Dict]:
|
| 163 |
+
"""Extrai notícias do VerAngola."""
|
| 164 |
+
try:
|
| 165 |
+
r = self.session.get(self.fontes_angola[3], timeout=8)
|
| 166 |
+
if r.status_code != 200: return []
|
| 167 |
+
soup = BeautifulSoup(r.text, 'html.parser')
|
| 168 |
+
# Seletores podem mudar, mas .noticia-item geralmente é um bom ponto de partida
|
| 169 |
+
itens = soup.select('.noticia-item')[:3]
|
| 170 |
+
noticias = []
|
| 171 |
+
for item in itens:
|
| 172 |
+
titulo = item.select_one('h3 a')
|
| 173 |
+
if titulo:
|
| 174 |
+
link = titulo.get('href', '')
|
| 175 |
+
noticias.append({
|
| 176 |
+
"titulo": self._limpar_texto(titulo.get_text()),
|
| 177 |
+
"link": link if link.startswith('http') else "https://www.verangola.net" + link
|
| 178 |
+
})
|
| 179 |
+
return noticias
|
| 180 |
+
except Exception as e:
|
| 181 |
+
logger.warning(f"VerAngola falhou: {e}")
|
| 182 |
+
return []
|
| 183 |
+
|
| 184 |
+
def pesquisar_noticias_angola(self) -> str:
|
| 185 |
"""
|
| 186 |
+
Retorna as notícias mais recentes de Angola através de Web Scraping.
|
| 187 |
+
Esta é a função usada no api.py quando detecta intenção de notícias.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 188 |
"""
|
| 189 |
+
cache_key = "noticias_angola"
|
| 190 |
cached = self.cache.get(cache_key)
|
| 191 |
if cached:
|
| 192 |
return cached
|
| 193 |
+
|
| 194 |
+
todas = []
|
| 195 |
try:
|
| 196 |
+
todas.extend(self._buscar_angop())
|
| 197 |
+
todas.extend(self._buscar_novojornal())
|
| 198 |
+
todas.extend(self._buscar_jornaldeangola())
|
| 199 |
+
todas.extend(self._buscar_verangola())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 200 |
except Exception as e:
|
| 201 |
+
logger.error(f"Erro no pipeline de scraping: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 202 |
|
| 203 |
+
# Filtra e remove duplicatas
|
| 204 |
+
vistos = set()
|
| 205 |
+
unicas = []
|
| 206 |
+
for n in todas:
|
| 207 |
+
t = n["titulo"].lower()
|
| 208 |
+
if t not in vistos and len(t) > 20:
|
| 209 |
+
vistos.add(t)
|
| 210 |
+
unicas.append(n)
|
| 211 |
+
if len(unicas) >= 5:
|
| 212 |
+
break
|
| 213 |
|
| 214 |
+
if not unicas:
|
| 215 |
+
fallback = "Sem notícias recentes de Angola disponíveis no momento."
|
| 216 |
+
self.cache.set(cache_key, fallback)
|
| 217 |
+
return fallback
|
| 218 |
|
| 219 |
+
# Formata a resposta para injeção no prompt do LLM
|
| 220 |
+
texto = "NOTÍCIAS RECENTES DE ANGOLA (CONTEXTO):\n"
|
| 221 |
+
for i, n in enumerate(unicas, 1):
|
| 222 |
+
# Apenas o título é relevante para o contexto do LLM
|
| 223 |
+
texto += f"[{i}] {n['titulo']}\n"
|
| 224 |
+
|
| 225 |
+
self.cache.set(cache_key, texto.strip())
|
| 226 |
+
return texto.strip()
|
requirements.txt
CHANGED
|
@@ -1,19 +1,33 @@
|
|
| 1 |
-
#
|
| 2 |
flask==3.1.2
|
| 3 |
flask-cors==6.0.1
|
| 4 |
gunicorn==23.0.0
|
| 5 |
-
loguru==0.7.3
|
| 6 |
|
| 7 |
-
#
|
| 8 |
-
|
|
|
|
|
|
|
|
|
|
| 9 |
tqdm==4.67.1
|
| 10 |
beautifulsoup4==4.14.2
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
|
| 12 |
-
#
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
sentence-transformers>=2.2.0
|
| 16 |
-
qrcode>=7.4.2
|
| 17 |
-
pillow>=10.0.0
|
| 18 |
-
# === Ambiente ===
|
| 19 |
-
python-dotenv==1.2.1
|
|
|
|
| 1 |
+
# Core web
|
| 2 |
flask==3.1.2
|
| 3 |
flask-cors==6.0.1
|
| 4 |
gunicorn==23.0.0
|
|
|
|
| 5 |
|
| 6 |
+
# DB & utils
|
| 7 |
+
sqlalchemy==2.0.44
|
| 8 |
+
python-dotenv==1.2.1
|
| 9 |
+
loguru==0.7.3
|
| 10 |
+
colorlog==6.10.1
|
| 11 |
tqdm==4.67.1
|
| 12 |
beautifulsoup4==4.14.2
|
| 13 |
+
requests==2.32.5
|
| 14 |
+
|
| 15 |
+
# HF ecosystem
|
| 16 |
+
transformers==4.45.2
|
| 17 |
+
tokenizers==0.20.1
|
| 18 |
+
huggingface_hub[hf_transfer]==0.28.1
|
| 19 |
+
sentence-transformers==3.2.1
|
| 20 |
+
peft==0.17.1
|
| 21 |
+
accelerate==1.0.1
|
| 22 |
+
torch
|
| 23 |
+
transformers
|
| 24 |
+
bitsandbytes
|
| 25 |
+
|
| 26 |
+
# APIs
|
| 27 |
+
openai==2.7.1
|
| 28 |
+
mistralai==1.9.11
|
| 29 |
+
google-generativeai==0.8.5
|
| 30 |
|
| 31 |
+
# NOTA: torch, torchvision, torchaudio, e llama-cpp-python
|
| 32 |
+
# foram removidos deste arquivo. Eles estão sendo instalados
|
| 33 |
+
# separadamente no Dockerfile para otimizar o build.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|