| |
| 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 |
|
|
| |
| |
| |
| 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}") |
|
|
| |
| |
| |
| def _ensure_all_columns_and_indexes(self): |
| with self._get_connection() as conn: |
| c = conn.cursor() |
| |
| 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 ''") |
| |
| 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() |
|
|
| |
| |
| |
| 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" |
|
|
| |
| |
| |
| 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}") |
|
|
|
|
| |
| |
| |
| if __name__ == "__main__": |
| db = Database() |
| db.salvar_mensagem("Zé", "Oi", "Epá!", "244999123456") |
| print("Banco V24 pronto!") |