|
|
|
|
|
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!") |