# database.py — V24 — COMPLETO COM salvar_embedding() + NUMPY + MIGRAÇÕES import sqlite3 import time import os import json import numpy as np from typing import List, Dict, Any, Tuple from loguru import logger class Database: def __init__(self, db_path: str = "/app/akira.db"): self.db_path = db_path self.max_retries = 5 self.retry_delay = 0.1 os.makedirs(os.path.dirname(db_path), exist_ok=True) self._init_db() self._ensure_all_columns_and_indexes() def _get_connection(self) -> sqlite3.Connection: for attempt in range(self.max_retries): try: conn = sqlite3.connect(self.db_path, timeout=30.0, check_same_thread=False) conn.execute('PRAGMA journal_mode=WAL') conn.execute('PRAGMA synchronous=NORMAL') conn.execute('PRAGMA busy_timeout=30000') conn.execute('PRAGMA foreign_keys=ON') return conn except sqlite3.OperationalError as e: if "database is locked" in str(e) and attempt < self.max_retries - 1: time.sleep(self.retry_delay * (2 ** attempt)) continue logger.error(f"Falha ao conectar: {e}") raise def _execute(self, query: str, params: tuple = (), commit: bool = False): for attempt in range(self.max_retries): try: with self._get_connection() as conn: c = conn.cursor() c.execute(query, params) result = c.fetchall() if query.strip().upper().startswith('SELECT') else None if commit: conn.commit() return result except sqlite3.OperationalError as e: if "database is locked" in str(e) and attempt < self.max_retries - 1: time.sleep(self.retry_delay * (2 ** attempt)) continue logger.error(f"Erro SQL: {e}") raise # ================================================================ # INICIALIZAÇÃO — TODAS AS TABELAS DO V18 + NOVAS # ================================================================ def _init_db(self): with self._get_connection() as conn: c = conn.cursor() c.executescript(''' -- TABELAS ORIGINAIS (V18) CREATE TABLE IF NOT EXISTS aprendizado ( id INTEGER PRIMARY KEY AUTOINCREMENT, usuario TEXT, dado TEXT, valor TEXT ); CREATE TABLE IF NOT EXISTS exemplos ( id INTEGER PRIMARY KEY AUTOINCREMENT, tipo TEXT NOT NULL, entrada TEXT NOT NULL, resposta TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS info_geral ( chave TEXT PRIMARY KEY, valor TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS estilos ( numero_usuario TEXT PRIMARY KEY, estilo TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS preferencias_tom ( numero_usuario TEXT PRIMARY KEY, tom TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS afinidades ( numero_usuario TEXT PRIMARY KEY, afinidade REAL NOT NULL ); CREATE TABLE IF NOT EXISTS termos ( id INTEGER PRIMARY KEY AUTOINCREMENT, numero_usuario TEXT NOT NULL, termo TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS aprendizados ( id INTEGER PRIMARY KEY AUTOINCREMENT, numero_usuario TEXT NOT NULL, chave TEXT NOT NULL, valor TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS aprendizado_detalhado ( id INTEGER PRIMARY KEY AUTOINCREMENT, numero_usuario TEXT NOT NULL, tipo TEXT NOT NULL, chave TEXT NOT NULL, valor TEXT NOT NULL, fonte TEXT, confianca REAL DEFAULT 1.0, contexto TEXT, criado_em DATETIME DEFAULT CURRENT_TIMESTAMP, atualizado_em DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS vocabulario_patenteado ( termo TEXT PRIMARY KEY, definicao TEXT NOT NULL, uso TEXT NOT NULL, exemplo TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS usuarios_privilegiados ( numero_usuario TEXT PRIMARY KEY, nome TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS whatsapp_ids ( id INTEGER PRIMARY KEY AUTOINCREMENT, whatsapp_id TEXT NOT NULL, sender_number TEXT NOT NULL, UNIQUE (whatsapp_id, sender_number) ); CREATE TABLE IF NOT EXISTS embeddings ( id INTEGER PRIMARY KEY AUTOINCREMENT, numero_usuario TEXT, source_type TEXT, texto TEXT, embedding BLOB NOT NULL ); CREATE TABLE IF NOT EXISTS mensagens ( id INTEGER PRIMARY KEY AUTOINCREMENT, usuario TEXT NOT NULL, mensagem TEXT NOT NULL, resposta TEXT NOT NULL, numero TEXT, is_reply BOOLEAN DEFAULT 0, mensagem_original TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS emocao_exemplos ( id INTEGER PRIMARY KEY AUTOINCREMENT, emocao TEXT NOT NULL, entrada TEXT NOT NULL, resposta TEXT NOT NULL, tom TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS girias_aprendidas ( id INTEGER PRIMARY KEY AUTOINCREMENT, numero_usuario TEXT NOT NULL, giria TEXT NOT NULL, significado TEXT NOT NULL, contexto TEXT, frequencia INTEGER DEFAULT 1, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS tom_usuario ( id INTEGER PRIMARY KEY AUTOINCREMENT, numero_usuario TEXT NOT NULL, tom_detectado TEXT NOT NULL, intensidade REAL DEFAULT 0.5, contexto TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS adaptacao_dinamica ( id INTEGER PRIMARY KEY AUTOINCREMENT, numero_usuario TEXT NOT NULL, tipo_adaptacao TEXT NOT NULL, valor_anterior TEXT, valor_novo TEXT, razao TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS pronomes_por_tom ( tom TEXT PRIMARY KEY, pronomes TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS contexto ( user_key TEXT PRIMARY KEY, historico TEXT, emocao_atual TEXT, termos TEXT, girias TEXT, tom TEXT ); -- TABELAS NOVAS (V22/V23) CREATE TABLE IF NOT EXISTS abrev_usuarios ( id INTEGER PRIMARY KEY AUTOINCREMENT, numero_usuario TEXT NOT NULL, abreviacao TEXT NOT NULL, palavra_completa TEXT, frequencia INTEGER DEFAULT 1 ); -- DADOS INICIAIS INSERT OR IGNORE INTO pronomes_por_tom (tom, pronomes) VALUES ('formal', 'Sr., ilustre, boss, maior, homem'), ('rude', 'parvo, estúpido, burro, analfabeto, desperdício de esperma'), ('casual', 'mano, puto, cota, mwangolé, kota'), ('neutro', 'amigo, parceiro, camarada'); ''') conn.commit() logger.info(f"Banco V24 inicializado: {self.db_path}") # ================================================================ # MIGRAÇÃO AUTOMÁTICA — GARANTE COLUNAS + ÍNDICES # ================================================================ def _ensure_all_columns_and_indexes(self): with self._get_connection() as conn: c = conn.cursor() # Migrações para embeddings c.execute("PRAGMA table_info(embeddings)") cols = {row[1] for row in c.fetchall()} if 'numero_usuario' not in cols: c.execute("ALTER TABLE embeddings ADD COLUMN numero_usuario TEXT") if 'source_type' not in cols: c.execute("ALTER TABLE embeddings ADD COLUMN source_type TEXT DEFAULT 'conversa'") if 'texto' not in cols: c.execute("ALTER TABLE embeddings ADD COLUMN texto TEXT DEFAULT ''") # Índices c.executescript(''' CREATE INDEX IF NOT EXISTS idx_mensagens_numero ON mensagens(numero); CREATE INDEX IF NOT EXISTS idx_girias_usuario ON girias_aprendidas(numero_usuario); CREATE INDEX IF NOT EXISTS idx_abrev_usuario ON abrev_usuarios(numero_usuario); CREATE INDEX IF NOT EXISTS idx_aprendizados_usuario ON aprendizados(numero_usuario); CREATE INDEX IF NOT EXISTS idx_aprendizado_detalhado_usuario ON aprendizado_detalhado(numero_usuario); ''') conn.commit() # ================================================================ # MÉTODOS PRINCIPAIS (para app.py) # ================================================================ def salvar_mensagem(self, usuario, mensagem, resposta, numero=None, is_reply=False, mensagem_original=None): cols = ['usuario', 'mensagem', 'resposta'] vals = [usuario, mensagem, resposta] if numero: cols.append('numero'); vals.append(numero) if is_reply is not None: cols.append('is_reply'); vals.append(int(is_reply)) if mensagem_original: cols.append('mensagem_original'); vals.append(mensagem_original) placeholders = ', '.join(['?' for _ in cols]) query = f"INSERT INTO mensagens ({', '.join(cols)}) VALUES ({placeholders})" self._execute(query, tuple(vals), commit=True) def carregar_contexto(self, numero: str) -> Dict[str, Any]: row = self._execute("SELECT historico, girias, tom FROM contexto WHERE user_key=?", (numero,)) if row: return { "historico": json.loads(row[0][0] or "[]")[-8:], "girias": json.loads(row[0][1] or "[]"), "tom": row[0][2] or "neutro" } return {"historico": [], "girias": [], "tom": "neutro"} def salvar_contexto(self, numero: str, historico: List, girias: List, tom: str): self._execute( "INSERT OR REPLACE INTO contexto (user_key, historico, girias, tom) VALUES (?, ?, ?, ?)", (numero, json.dumps(historico), json.dumps(girias), tom), commit=True ) def recuperar_girias_usuario(self, numero_usuario: str) -> List[str]: rows = self._execute("SELECT giria FROM girias_aprendidas WHERE numero_usuario=?", (numero_usuario,)) return [r[0] for r in rows] if rows else [] def recuperar_abreviacoes_usuario(self, numero_usuario: str) -> Dict[str, str]: rows = self._execute("SELECT abreviacao, palavra_completa FROM abrev_usuarios WHERE numero_usuario=?", (numero_usuario,)) return {r[0]: r[1] for r in rows} if rows else {} def aprender_giria(self, numero: str, giria: str, significado: str = None): row = self._execute("SELECT frequencia FROM girias_aprendidas WHERE numero_usuario=? AND giria=?", (numero, giria)) if row: self._execute("UPDATE girias_aprendidas SET frequencia = frequencia + 1, updated_at = CURRENT_TIMESTAMP WHERE numero_usuario=? AND giria=?", (numero, giria), commit=True) else: self._execute("INSERT INTO girias_aprendidas (numero_usuario, giria, significado) VALUES (?, ?, ?)", (numero, giria, significado or ""), commit=True) def aprender_abreviacao(self, numero: str, abrev: str, completa: str): row = self._execute("SELECT frequencia FROM abrev_usuarios WHERE numero_usuario=? AND abreviacao=?", (numero, abrev)) if row: self._execute("UPDATE abrev_usuarios SET frequencia = frequencia + 1 WHERE numero_usuario=? AND abreviacao=?", (numero, abrev), commit=True) else: self._execute("INSERT INTO abrev_usuarios (numero_usuario, abreviacao, palavra_completa) VALUES (?, ?, ?)", (numero, abrev, completa), commit=True) def detectar_tom(self, numero: str, mensagem: str) -> str: rude = any(w in mensagem.lower() for w in ["parvo", "burro", "merda"]) casual = any(w in mensagem.lower() for w in ["epá", "kandando", "bué", "kota"]) if rude: return "rude" if casual: return "casual" return "neutro" # ================================================================ # NOVO MÉTODO: salvar_embedding() — COM NUMPY # ================================================================ def salvar_embedding(self, numero_usuario: str, mensagem: str, resposta: str, embedding: list, texto: str = ""): """ Salva embedding no banco (tabela 'embeddings'). """ try: embedding_blob = np.array(embedding, dtype=np.float32).tobytes() self._execute( """INSERT INTO embeddings (numero_usuario, source_type, texto, embedding) VALUES (?, ?, ?, ?)""", (numero_usuario, 'conversa', texto or f"{mensagem} {resposta}", embedding_blob), commit=True ) logger.debug(f"Embedding salvo para {numero_usuario}") except Exception as e: logger.error(f"Erro ao salvar embedding: {e}") # ================================================================ # TESTE RÁPIDO (OPCIONAL) # ================================================================ if __name__ == "__main__": db = Database() db.salvar_mensagem("Zé", "Oi", "Epá!", "244999123456") print("Banco V24 pronto!")