akra35567 commited on
Commit
e6f85fa
·
verified ·
1 Parent(s): a5d8ca6

Update modules/database.py

Browse files
Files changed (1) hide show
  1. modules/database.py +472 -239
modules/database.py CHANGED
@@ -1,15 +1,18 @@
1
- # modules/database.py — AKIRA V19 FINAL (Dezembro 2025)
2
  """
3
  Banco de dados SQLite para Akira IA.
4
- ERRO DE DIRETÓRIO CORRIGIDO
5
- Isolamento PV/Grupo
6
- Todas as tabelas e métodos
 
 
7
  """
8
  import sqlite3
9
  import time
10
  import os
11
  import json
12
  import pickle
 
13
  from typing import Optional, List, Dict, Any, Tuple
14
  from loguru import logger
15
 
@@ -20,17 +23,44 @@ class Database:
20
  self.max_retries = 5
21
  self.retry_delay = 0.1
22
 
23
- # === CORRIGIDO: Verifica se há diretório antes de criar ===
24
  db_dir = os.path.dirname(db_path)
25
- if db_dir: # Apenas cria se houver diretório no caminho
26
  os.makedirs(db_dir, exist_ok=True)
27
 
28
  self._init_db()
29
  self._ensure_all_columns_and_indexes()
30
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  # ================================================================
32
  # CONEXÃO COM RETRY + WAL
33
  # ================================================================
 
34
  def _get_connection(self) -> sqlite3.Connection:
35
  for attempt in range(self.max_retries):
36
  try:
@@ -67,12 +97,13 @@ class Database:
67
  if "database is locked" in str(e) and attempt < self.max_retries - 1:
68
  time.sleep(self.retry_delay * (2 ** attempt))
69
  continue
70
- logger.error(f"Erro SQL (tentativa {attempt+1}): {e}")
71
  raise
72
 
73
  # ================================================================
74
  # INICIALIZAÇÃO + TABELAS
75
  # ================================================================
 
76
  def _init_db(self):
77
  try:
78
  with self._get_connection() as conn:
@@ -85,44 +116,87 @@ class Database:
85
  dado TEXT,
86
  valor TEXT
87
  );
88
- CREATE TABLE IF NOT EXISTS exemplos (
 
89
  id INTEGER PRIMARY KEY AUTOINCREMENT,
90
- tipo TEXT NOT NULL,
91
- entrada TEXT NOT NULL,
92
- resposta TEXT NOT NULL
 
 
 
 
 
 
 
93
  );
94
- CREATE TABLE IF NOT EXISTS info_geral (
95
- chave TEXT PRIMARY KEY,
96
- valor TEXT NOT NULL
 
 
 
 
 
 
97
  );
98
- CREATE TABLE IF NOT EXISTS estilos (
99
- numero_usuario TEXT PRIMARY KEY,
100
- estilo TEXT NOT NULL
 
 
 
 
 
 
101
  );
102
- CREATE TABLE IF NOT EXISTS preferencias_tom (
103
- numero_usuario TEXT PRIMARY KEY,
104
- tom TEXT NOT NULL
 
 
 
 
 
 
105
  );
106
- CREATE TABLE IF NOT EXISTS afinidades (
107
- numero_usuario TEXT PRIMARY KEY,
108
- afinidade REAL NOT NULL
 
 
 
 
 
 
 
 
 
109
  );
110
- CREATE TABLE IF NOT EXISTS termos (
 
111
  id INTEGER PRIMARY KEY AUTOINCREMENT,
112
- numero_usuario TEXT NOT NULL,
113
- termo TEXT NOT NULL,
114
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP
 
 
 
 
115
  );
116
- CREATE TABLE IF NOT EXISTS aprendizados (
 
117
  id INTEGER PRIMARY KEY AUTOINCREMENT,
118
- numero_usuario TEXT NOT NULL,
119
- chave TEXT NOT NULL,
120
- valor TEXT NOT NULL,
 
121
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP
122
  );
 
123
  CREATE TABLE IF NOT EXISTS aprendizado_detalhado (
124
  id INTEGER PRIMARY KEY AUTOINCREMENT,
125
- numero_usuario TEXT NOT NULL,
126
  tipo TEXT NOT NULL,
127
  chave TEXT NOT NULL,
128
  valor TEXT NOT NULL,
@@ -132,190 +206,395 @@ class Database:
132
  criado_em DATETIME DEFAULT CURRENT_TIMESTAMP,
133
  atualizado_em DATETIME DEFAULT CURRENT_TIMESTAMP
134
  );
135
- CREATE TABLE IF NOT EXISTS vocabulario_patenteado (
136
- termo TEXT PRIMARY KEY,
137
- definicao TEXT NOT NULL,
138
- uso TEXT NOT NULL,
139
- exemplo TEXT NOT NULL
140
- );
141
- CREATE TABLE IF NOT EXISTS usuarios_privilegiados (
142
- numero_usuario TEXT PRIMARY KEY,
143
- nome TEXT NOT NULL
144
- );
145
- CREATE TABLE IF NOT EXISTS whatsapp_ids (
146
- id INTEGER PRIMARY KEY AUTOINCREMENT,
147
- whatsapp_id TEXT NOT NULL,
148
- sender_number TEXT NOT NULL,
149
- UNIQUE (whatsapp_id, sender_number)
150
- );
151
  CREATE TABLE IF NOT EXISTS embeddings (
152
  id INTEGER PRIMARY KEY AUTOINCREMENT,
153
- numero_usuario TEXT,
154
  source_type TEXT,
155
  texto TEXT,
156
- embedding BLOB NOT NULL
157
- );
158
- CREATE TABLE IF NOT EXISTS mensagens (
159
- id INTEGER PRIMARY KEY AUTOINCREMENT,
160
- usuario TEXT NOT NULL,
161
- mensagem TEXT NOT NULL,
162
- resposta TEXT NOT NULL,
163
- numero TEXT,
164
- is_reply BOOLEAN DEFAULT 0,
165
- mensagem_original TEXT,
166
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP
167
  );
168
- CREATE TABLE IF NOT EXISTS emocao_exemplos (
 
169
  id INTEGER PRIMARY KEY AUTOINCREMENT,
170
- emocao TEXT NOT NULL,
171
- entrada TEXT NOT NULL,
172
- resposta TEXT NOT NULL,
173
- tom TEXT NOT NULL,
 
 
174
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP
175
  );
176
- CREATE TABLE IF NOT EXISTS girias_aprendidas (
177
- id INTEGER PRIMARY KEY AUTOINCREMENT,
178
- numero_usuario TEXT NOT NULL,
179
- giria TEXT NOT NULL,
180
- significado TEXT NOT NULL,
181
- contexto TEXT,
182
- frequencia INTEGER DEFAULT 1,
183
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
184
- updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
185
- );
186
- CREATE TABLE IF NOT EXISTS tom_usuario (
187
- id INTEGER PRIMARY KEY AUTOINCREMENT,
188
- numero_usuario TEXT NOT NULL,
189
- tom_detectado TEXT NOT NULL,
190
- intensidade REAL DEFAULT 0.5,
191
- contexto TEXT,
192
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP
193
  );
194
- CREATE TABLE IF NOT EXISTS adaptacao_dinamica (
 
 
195
  id INTEGER PRIMARY KEY AUTOINCREMENT,
196
  numero_usuario TEXT NOT NULL,
197
- tipo_adaptacao TEXT NOT NULL,
198
- valor_anterior TEXT,
199
- valor_novo TEXT,
200
- razao TEXT,
201
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP
202
  );
203
- CREATE TABLE IF NOT EXISTS pronomes_por_tom (
204
- tom TEXT PRIMARY KEY,
205
- pronomes TEXT NOT NULL
206
- );
207
- CREATE TABLE IF NOT EXISTS contexto (
208
- user_key TEXT PRIMARY KEY,
209
- historico TEXT,
210
- emocao_atual TEXT,
211
- termos TEXT,
212
- girias TEXT,
213
- tom TEXT
214
- );
215
- ''')
216
- c.executescript('''
217
- INSERT OR IGNORE INTO pronomes_por_tom (tom, pronomes) VALUES
218
- ('formal', 'Sr., ilustre, boss, maior, homem'),
219
- ('rude', 'parvo, estúpido, burro, analfabeto, desperdício de esperma'),
220
- ('casual', 'mano, puto, cota, mwangolé, kota'),
221
- ('neutro', 'amigo, parceiro, camarada');
222
  ''')
 
223
  conn.commit()
224
- logger.info(f"Banco inicializado: {self.db_path}")
225
  except Exception as e:
226
- logger.error(f"Erro ao criar tabelas: {e}")
227
  raise
228
 
229
  # ================================================================
230
- # MIGRAÇÃO AUTOMÁTICA — GARANTE COLUNAS
231
  # ================================================================
 
232
  def _ensure_all_columns_and_indexes(self):
233
  try:
234
  with self._get_connection() as conn:
235
  c = conn.cursor()
236
 
237
- c.execute("PRAGMA table_info(embeddings)")
 
238
  cols = {row[1] for row in c.fetchall()}
239
 
240
- # Migração 1: Adicionar numero_usuario
241
- if 'numero_usuario' not in cols:
242
- c.execute("ALTER TABLE embeddings ADD COLUMN numero_usuario TEXT")
243
- logger.info("Coluna 'numero_usuario' adicionada em 'embeddings'")
244
-
245
- # Migração 2: Adicionar source_type
246
- if 'source_type' not in cols:
247
- c.execute("ALTER TABLE embeddings ADD COLUMN source_type TEXT DEFAULT 'conversa'")
248
- logger.info("Coluna 'source_type' adicionada em 'embeddings'")
 
 
249
 
250
- # Migração 3: Adicionar texto
251
- if 'texto' not in cols:
252
- c.execute("ALTER TABLE embeddings ADD COLUMN texto TEXT DEFAULT ''")
253
- logger.info("Coluna 'texto' adicionada em 'embeddings'")
 
 
254
 
255
- # Índices
256
  c.executescript('''
257
- CREATE INDEX IF NOT EXISTS idx_mensagens_numero ON mensagens(numero);
258
- CREATE INDEX IF NOT EXISTS idx_girias_usuario ON girias_aprendidas(numero_usuario);
259
- CREATE INDEX IF NOT EXISTS idx_aprendizados_usuario ON aprendizados(numero_usuario);
260
- CREATE INDEX IF NOT EXISTS idx_aprendizado_detalhado_usuario ON aprendizado_detalhado(numero_usuario);
 
 
 
261
  ''')
 
262
  conn.commit()
 
 
263
  except Exception as e:
264
- logger.error(f"Erro na migração: {e}")
265
 
266
  # ================================================================
267
- # MÉTODOS DE RECUPERAÇÃO (COM ISOLAMENTO PV/GRUPO)
268
  # ================================================================
269
 
270
- def recuperar_mensagens(self, usuario: str, limite: int = 5) -> List[Tuple]:
271
  """
272
- Recupera mensagens COM ISOLAMENTO:
273
- - Grupos: apenas mensagens deste grupo
274
- - PV: apenas mensagens privadas (exclui grupos)
275
  """
276
- is_grupo = usuario.endswith("@g.us") or "120363" in usuario
277
-
278
- if is_grupo:
279
- # Grupos: busca apenas mensagens DESTE grupo específico
280
- query = """
281
- SELECT mensagem, resposta
282
- FROM mensagens
283
- WHERE numero=?
284
- ORDER BY id DESC
285
- LIMIT ?
286
- """
287
- return self._execute_with_retry(query, (usuario, limite)) or []
288
- else:
289
- # PV: busca apenas mensagens privadas (exclui grupos)
290
- query = """
291
- SELECT mensagem, resposta
292
- FROM mensagens
293
- WHERE (usuario=? OR numero=?)
294
- AND numero NOT LIKE '%@g.us%'
295
- AND numero NOT LIKE '%120363%'
296
- ORDER BY id DESC
297
- LIMIT ?
298
- """
299
- return self._execute_with_retry(query, (usuario, usuario, limite)) or []
300
-
301
- def recuperar_girias_usuario(self, numero_usuario: str) -> List[Dict[str, Any]]:
302
- rows = self._execute_with_retry(
303
- "SELECT giria, significado, contexto, frequencia FROM girias_aprendidas WHERE numero_usuario=?",
304
- (numero_usuario,)
305
- ) or []
306
- return [{'giria': r[0], 'significado': r[1], 'contexto': r[2], 'frequencia': r[3]} for r in rows]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
307
 
308
- def recuperar_aprendizado_detalhado(self, numero_usuario: str) -> Dict[str, str]:
 
 
 
309
  rows = self._execute_with_retry(
310
- "SELECT chave, valor FROM aprendizados WHERE numero_usuario=?",
311
- (numero_usuario,)
 
 
 
 
312
  ) or []
313
- return {r[0]: r[1] for r in rows}
 
 
 
 
 
 
 
 
 
314
 
315
  # ================================================================
316
- # MÉTODOS DE SALVAMENTO
317
  # ================================================================
318
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
319
  def salvar_aprendizado_detalhado(
320
  self,
321
  numero_usuario: str,
@@ -326,89 +605,43 @@ class Database:
326
  confianca: float = 1.0,
327
  contexto: Optional[str] = None
328
  ):
329
- """Salva aprendizado com valor OBRIGATÓRIO"""
 
 
330
  self._execute_with_retry(
331
  """INSERT INTO aprendizado_detalhado
332
- (numero_usuario, tipo, chave, valor, fonte, confianca, contexto)
333
  VALUES (?, ?, ?, ?, ?, ?, ?)""",
334
- (numero_usuario, tipo, chave, valor, fonte, confianca, contexto),
335
  commit=True
336
  )
337
-
338
- def salvar_mensagem(self, usuario, mensagem, resposta, numero=None, is_reply=False, mensagem_original=None):
339
- cols = ['usuario', 'mensagem', 'resposta']
340
- vals = [usuario, mensagem, resposta]
341
- if numero:
342
- cols.append('numero')
343
- vals.append(numero)
344
- if is_reply is not None:
345
- cols.append('is_reply')
346
- vals.append(int(is_reply))
347
- if mensagem_original:
348
- cols.append('mensagem_original')
349
- vals.append(mensagem_original)
350
-
351
- placeholders = ', '.join(['?' for _ in cols])
352
- query = f"INSERT INTO mensagens ({', '.join(cols)}) VALUES ({placeholders})"
353
- self._execute_with_retry(query, tuple(vals), commit=True)
354
-
355
- def salvar_embedding(self, numero_usuario: str, mensagem: str, resposta: str, embedding, texto: str = ""):
356
- try:
357
- embedding_blob = sqlite3.Binary(pickle.dumps(embedding))
358
- texto_final = texto or f"{mensagem} {resposta}"
359
- self._execute_with_retry(
360
- "INSERT INTO embeddings (numero_usuario, source_type, texto, embedding) VALUES (?, ?, ?, ?)",
361
- (numero_usuario, 'conversa', texto_final[:500], embedding_blob),
362
- commit=True
363
- )
364
- except Exception as e:
365
- logger.warning(f"Erro ao salvar embedding: {e}")
366
-
367
- def salvar_contexto(self, user_key: str, historico: str, emocao_atual: str, termos: str, girias: str, tom: str):
368
  try:
369
  self._execute_with_retry(
370
- """INSERT OR REPLACE INTO contexto
371
- (user_key, historico, emocao_atual, termos, girias, tom)
372
- VALUES (?, ?, ?, ?, ?, ?)""",
373
- (user_key, historico, emocao_atual, termos, girias, tom),
374
  commit=True
375
  )
376
  except Exception as e:
377
- logger.error(f"Falha ao salvar contexto: {e}")
378
-
379
- def carregar_contexto(self, user_key: str) -> Dict[str, Any]:
380
- rows = self._execute_with_retry(
381
- "SELECT historico, emocao_atual, termos, girias, tom FROM contexto WHERE user_key=?",
382
- (user_key,)
383
- )
384
- if not rows:
385
- return {"historico": "", "emocao_atual": "{}", "termos": "", "girias": "", "tom": "neutro"}
386
- row = rows[0]
387
- try:
388
- emocao_dict = json.loads(row[1]) if row[1] else {}
389
- except:
390
- emocao_dict = {}
391
- return {
392
- "historico": row[0] or "",
393
- "emocao_atual": emocao_dict,
394
- "termos": row[2] or "",
395
- "girias": row[3] or "",
396
- "tom": row[4] or "neutro"
397
- }
398
-
399
- # ================================================================
400
- # MÉTODO ADICIONAL: REGISTRAR TOM DO USUÁRIO
401
- # ================================================================
402
 
403
- def registrar_tom_usuario(self, numero_usuario: str, tom: str, intensidade: float = 0.5, contexto: str = ""):
404
- """Registra tom detectado do usuário"""
405
  try:
 
 
 
 
406
  self._execute_with_retry(
407
- """INSERT INTO tom_usuario
408
- (numero_usuario, tom_detectado, intensidade, contexto)
409
- VALUES (?, ?, ?, ?)""",
410
- (numero_usuario, tom, intensidade, contexto[:100]),
411
  commit=True
412
  )
413
  except Exception as e:
414
- logger.warning(f"Erro ao registrar tom: {e}")
 
1
+ # modules/database.py — AKIRA V20 FINAL (Dezembro 2025)
2
  """
3
  Banco de dados SQLite para Akira IA.
4
+ ISOLAMENTO TOTAL PV/GRUPO (criptográfico)
5
+ Controle de humor multinível
6
+ Sistema de transição de humor
7
+ ✅ Reply context tracking
8
+ ✅ Training dataset integration
9
  """
10
  import sqlite3
11
  import time
12
  import os
13
  import json
14
  import pickle
15
+ import hashlib
16
  from typing import Optional, List, Dict, Any, Tuple
17
  from loguru import logger
18
 
 
23
  self.max_retries = 5
24
  self.retry_delay = 0.1
25
 
26
+ # Cria diretório se necessário
27
  db_dir = os.path.dirname(db_path)
28
+ if db_dir:
29
  os.makedirs(db_dir, exist_ok=True)
30
 
31
  self._init_db()
32
  self._ensure_all_columns_and_indexes()
33
 
34
+ # ================================================================
35
+ # ISOLAMENTO CRIPTOGRÁFICO (NOVO)
36
+ # ================================================================
37
+
38
+ def _gerar_contexto_id(self, numero: str, tipo: str) -> str:
39
+ """
40
+ Gera ID único e isolado para contexto.
41
+
42
+ Args:
43
+ numero: Número do usuário/grupo
44
+ tipo: 'pv' ou 'grupo'
45
+
46
+ Returns:
47
+ Hash único isolando completamente PV de Grupo
48
+ """
49
+ # Detecta automaticamente se é grupo
50
+ if "@g.us" in numero or "120363" in numero:
51
+ tipo = "grupo"
52
+ else:
53
+ tipo = "pv"
54
+
55
+ # Hash único: SHA256(numero + tipo + salt)
56
+ salt = "AKIRA_CONTEXT_ISOLATION_V20"
57
+ raw = f"{numero}|{tipo}|{salt}"
58
+ return hashlib.sha256(raw.encode()).hexdigest()
59
+
60
  # ================================================================
61
  # CONEXÃO COM RETRY + WAL
62
  # ================================================================
63
+
64
  def _get_connection(self) -> sqlite3.Connection:
65
  for attempt in range(self.max_retries):
66
  try:
 
97
  if "database is locked" in str(e) and attempt < self.max_retries - 1:
98
  time.sleep(self.retry_delay * (2 ** attempt))
99
  continue
100
+ logger.error(f"Erro SQL (tentativa {attempt+1}): {e}")
101
  raise
102
 
103
  # ================================================================
104
  # INICIALIZAÇÃO + TABELAS
105
  # ================================================================
106
+
107
  def _init_db(self):
108
  try:
109
  with self._get_connection() as conn:
 
116
  dado TEXT,
117
  valor TEXT
118
  );
119
+
120
+ CREATE TABLE IF NOT EXISTS mensagens (
121
  id INTEGER PRIMARY KEY AUTOINCREMENT,
122
+ usuario TEXT NOT NULL,
123
+ mensagem TEXT NOT NULL,
124
+ resposta TEXT NOT NULL,
125
+ numero TEXT,
126
+ contexto_id TEXT NOT NULL,
127
+ tipo_contexto TEXT DEFAULT 'pv',
128
+ is_reply BOOLEAN DEFAULT 0,
129
+ mensagem_original TEXT,
130
+ reply_to_bot BOOLEAN DEFAULT 0,
131
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
132
  );
133
+
134
+ CREATE TABLE IF NOT EXISTS humor_historico (
135
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
136
+ contexto_id TEXT NOT NULL,
137
+ humor_anterior TEXT,
138
+ humor_novo TEXT NOT NULL,
139
+ razao_mudanca TEXT,
140
+ intensidade REAL DEFAULT 1.0,
141
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
142
  );
143
+
144
+ CREATE TABLE IF NOT EXISTS modo_resposta_historico (
145
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
146
+ contexto_id TEXT NOT NULL,
147
+ modo_usado TEXT NOT NULL,
148
+ tom_usuario TEXT,
149
+ contexto_mensagem TEXT,
150
+ sucesso BOOLEAN DEFAULT 1,
151
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
152
  );
153
+
154
+ CREATE TABLE IF NOT EXISTS reply_context (
155
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
156
+ contexto_id TEXT NOT NULL,
157
+ mensagem_id TEXT NOT NULL,
158
+ autor_original TEXT NOT NULL,
159
+ conteudo_original TEXT NOT NULL,
160
+ eh_resposta_a_bot BOOLEAN DEFAULT 0,
161
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
162
  );
163
+
164
+ CREATE TABLE IF NOT EXISTS contexto (
165
+ contexto_id TEXT PRIMARY KEY,
166
+ numero TEXT NOT NULL,
167
+ tipo_contexto TEXT NOT NULL,
168
+ historico TEXT,
169
+ humor_atual TEXT DEFAULT 'normal',
170
+ modo_resposta_atual TEXT DEFAULT 'casual_amigavel',
171
+ termos TEXT,
172
+ girias TEXT,
173
+ tom TEXT DEFAULT 'normal',
174
+ ultima_atualizacao DATETIME DEFAULT CURRENT_TIMESTAMP
175
  );
176
+
177
+ CREATE TABLE IF NOT EXISTS girias_aprendidas (
178
  id INTEGER PRIMARY KEY AUTOINCREMENT,
179
+ contexto_id TEXT NOT NULL,
180
+ giria TEXT NOT NULL,
181
+ significado TEXT NOT NULL,
182
+ contexto TEXT,
183
+ frequencia INTEGER DEFAULT 1,
184
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
185
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
186
  );
187
+
188
+ CREATE TABLE IF NOT EXISTS tom_usuario (
189
  id INTEGER PRIMARY KEY AUTOINCREMENT,
190
+ contexto_id TEXT NOT NULL,
191
+ tom_detectado TEXT NOT NULL,
192
+ intensidade REAL DEFAULT 0.5,
193
+ contexto TEXT,
194
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP
195
  );
196
+
197
  CREATE TABLE IF NOT EXISTS aprendizado_detalhado (
198
  id INTEGER PRIMARY KEY AUTOINCREMENT,
199
+ contexto_id TEXT NOT NULL,
200
  tipo TEXT NOT NULL,
201
  chave TEXT NOT NULL,
202
  valor TEXT NOT NULL,
 
206
  criado_em DATETIME DEFAULT CURRENT_TIMESTAMP,
207
  atualizado_em DATETIME DEFAULT CURRENT_TIMESTAMP
208
  );
209
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
210
  CREATE TABLE IF NOT EXISTS embeddings (
211
  id INTEGER PRIMARY KEY AUTOINCREMENT,
212
+ contexto_id TEXT NOT NULL,
213
  source_type TEXT,
214
  texto TEXT,
215
+ embedding BLOB NOT NULL,
 
 
 
 
 
 
 
 
 
216
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP
217
  );
218
+
219
+ CREATE TABLE IF NOT EXISTS training_examples (
220
  id INTEGER PRIMARY KEY AUTOINCREMENT,
221
+ input_text TEXT NOT NULL,
222
+ output_text TEXT NOT NULL,
223
+ humor TEXT NOT NULL,
224
+ modo_resposta TEXT NOT NULL,
225
+ qualidade_score REAL DEFAULT 1.0,
226
+ usado_treino BOOLEAN DEFAULT 0,
227
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP
228
  );
229
+
230
+ CREATE TABLE IF NOT EXISTS usuarios_privilegiados (
231
+ numero_usuario TEXT PRIMARY KEY,
232
+ nome TEXT NOT NULL,
233
+ tom_inicial TEXT DEFAULT 'formal'
 
 
 
 
 
 
 
 
 
 
 
 
234
  );
235
+
236
+ -- Tabelas legadas (mantidas para compatibilidade)
237
+ CREATE TABLE IF NOT EXISTS aprendizados (
238
  id INTEGER PRIMARY KEY AUTOINCREMENT,
239
  numero_usuario TEXT NOT NULL,
240
+ chave TEXT NOT NULL,
241
+ valor TEXT NOT NULL,
 
 
242
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP
243
  );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
244
  ''')
245
+
246
  conn.commit()
247
+ logger.info(f"Banco inicializado: {self.db_path}")
248
  except Exception as e:
249
+ logger.error(f"Erro ao criar tabelas: {e}")
250
  raise
251
 
252
  # ================================================================
253
+ # MIGRAÇÃO AUTOMÁTICA
254
  # ================================================================
255
+
256
  def _ensure_all_columns_and_indexes(self):
257
  try:
258
  with self._get_connection() as conn:
259
  c = conn.cursor()
260
 
261
+ # Verifica colunas de mensagens
262
+ c.execute("PRAGMA table_info(mensagens)")
263
  cols = {row[1] for row in c.fetchall()}
264
 
265
+ if 'contexto_id' not in cols:
266
+ c.execute("ALTER TABLE mensagens ADD COLUMN contexto_id TEXT DEFAULT ''")
267
+ logger.info(" Coluna 'contexto_id' adicionada em 'mensagens'")
268
+
269
+ if 'tipo_contexto' not in cols:
270
+ c.execute("ALTER TABLE mensagens ADD COLUMN tipo_contexto TEXT DEFAULT 'pv'")
271
+ logger.info("✅ Coluna 'tipo_contexto' adicionada")
272
+
273
+ if 'reply_to_bot' not in cols:
274
+ c.execute("ALTER TABLE mensagens ADD COLUMN reply_to_bot BOOLEAN DEFAULT 0")
275
+ logger.info("✅ Coluna 'reply_to_bot' adicionada")
276
 
277
+ # Migra dados antigos para usar contexto_id
278
+ c.execute("""
279
+ UPDATE mensagens
280
+ SET contexto_id = numero || '_pv'
281
+ WHERE contexto_id = '' OR contexto_id IS NULL
282
+ """)
283
 
284
+ # Índices otimizados
285
  c.executescript('''
286
+ CREATE INDEX IF NOT EXISTS idx_mensagens_contexto ON mensagens(contexto_id);
287
+ CREATE INDEX IF NOT EXISTS idx_contexto_id ON contexto(contexto_id);
288
+ CREATE INDEX IF NOT EXISTS idx_humor_contexto ON humor_historico(contexto_id);
289
+ CREATE INDEX IF NOT EXISTS idx_girias_contexto ON girias_aprendidas(contexto_id);
290
+ CREATE INDEX IF NOT EXISTS idx_tom_contexto ON tom_usuario(contexto_id);
291
+ CREATE INDEX IF NOT EXISTS idx_reply_contexto ON reply_context(contexto_id);
292
+ CREATE INDEX IF NOT EXISTS idx_training_quality ON training_examples(qualidade_score DESC);
293
  ''')
294
+
295
  conn.commit()
296
+ logger.info("✅ Migração de colunas/índices concluída")
297
+
298
  except Exception as e:
299
+ logger.error(f"Erro na migração: {e}")
300
 
301
  # ================================================================
302
+ # MÉTODOS DE RECUPERAÇÃO (COM ISOLAMENTO ABSOLUTO)
303
  # ================================================================
304
 
305
+ def recuperar_mensagens(self, numero: str, limite: int = 10) -> List[Tuple]:
306
  """
307
+ Recupera mensagens COM ISOLAMENTO TOTAL.
308
+ Usa contexto_id para garantir que PV nunca vaza para grupo.
 
309
  """
310
+ contexto_id = self._gerar_contexto_id(numero, 'auto')
311
+
312
+ query = """
313
+ SELECT mensagem, resposta, is_reply, mensagem_original, reply_to_bot
314
+ FROM mensagens
315
+ WHERE contexto_id = ?
316
+ ORDER BY id DESC
317
+ LIMIT ?
318
+ """
319
+
320
+ results = self._execute_with_retry(query, (contexto_id, limite))
321
+ return results if results else []
322
+
323
+ def recuperar_humor_atual(self, numero: str) -> str:
324
+ """Recupera humor atual do contexto isolado"""
325
+ contexto_id = self._gerar_contexto_id(numero, 'auto')
326
+
327
+ query = "SELECT humor_atual FROM contexto WHERE contexto_id = ?"
328
+ result = self._execute_with_retry(query, (contexto_id,))
329
+
330
+ if result and result[0][0]:
331
+ return result[0][0]
332
+
333
+ return "normal"
334
+
335
+ def recuperar_historico_humor(self, numero: str, limite: int = 5) -> List[Dict]:
336
+ """Recupera histórico de transições de humor"""
337
+ contexto_id = self._gerar_contexto_id(numero, 'auto')
338
+
339
+ query = """
340
+ SELECT humor_anterior, humor_novo, razao_mudanca, intensidade, created_at
341
+ FROM humor_historico
342
+ WHERE contexto_id = ?
343
+ ORDER BY id DESC
344
+ LIMIT ?
345
+ """
346
+
347
+ results = self._execute_with_retry(query, (contexto_id, limite))
348
+
349
+ if not results:
350
+ return []
351
+
352
+ return [
353
+ {
354
+ "anterior": r[0],
355
+ "novo": r[1],
356
+ "razao": r[2],
357
+ "intensidade": r[3],
358
+ "timestamp": r[4]
359
+ }
360
+ for r in results
361
+ ]
362
+
363
+ def recuperar_modo_resposta(self, numero: str) -> str:
364
+ """Recupera modo de resposta atual"""
365
+ contexto_id = self._gerar_contexto_id(numero, 'auto')
366
+
367
+ query = "SELECT modo_resposta_atual FROM contexto WHERE contexto_id = ?"
368
+ result = self._execute_with_retry(query, (contexto_id,))
369
+
370
+ if result and result[0][0]:
371
+ return result[0][0]
372
+
373
+ return "casual_amigavel"
374
 
375
+ def recuperar_girias_usuario(self, numero: str) -> List[Dict[str, Any]]:
376
+ """Recupera gírias aprendidas (isolado por contexto)"""
377
+ contexto_id = self._gerar_contexto_id(numero, 'auto')
378
+
379
  rows = self._execute_with_retry(
380
+ """SELECT giria, significado, contexto, frequencia
381
+ FROM girias_aprendidas
382
+ WHERE contexto_id = ?
383
+ ORDER BY frequencia DESC
384
+ LIMIT 20""",
385
+ (contexto_id,)
386
  ) or []
387
+
388
+ return [
389
+ {
390
+ 'giria': r[0],
391
+ 'significado': r[1],
392
+ 'contexto': r[2],
393
+ 'frequencia': r[3]
394
+ }
395
+ for r in rows
396
+ ]
397
 
398
  # ================================================================
399
+ # MÉTODOS DE SALVAMENTO (COM ISOLAMENTO)
400
  # ================================================================
401
 
402
+ def salvar_mensagem(
403
+ self,
404
+ usuario: str,
405
+ mensagem: str,
406
+ resposta: str,
407
+ numero: str = None,
408
+ is_reply: bool = False,
409
+ mensagem_original: str = None,
410
+ reply_to_bot: bool = False
411
+ ):
412
+ """Salva mensagem com contexto isolado"""
413
+ numero_final = numero or usuario
414
+ contexto_id = self._gerar_contexto_id(numero_final, 'auto')
415
+
416
+ # Detecta tipo
417
+ tipo_contexto = "grupo" if ("@g.us" in numero_final or "120363" in numero_final) else "pv"
418
+
419
+ query = """
420
+ INSERT INTO mensagens
421
+ (usuario, mensagem, resposta, numero, contexto_id, tipo_contexto, is_reply, mensagem_original, reply_to_bot)
422
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
423
+ """
424
+
425
+ self._execute_with_retry(
426
+ query,
427
+ (usuario, mensagem, resposta, numero_final, contexto_id, tipo_contexto,
428
+ int(is_reply), mensagem_original, int(reply_to_bot)),
429
+ commit=True
430
+ )
431
+
432
+ logger.debug(f"✅ Mensagem salva: contexto_id={contexto_id[:8]}..., tipo={tipo_contexto}")
433
+
434
+ def salvar_transicao_humor(
435
+ self,
436
+ numero: str,
437
+ humor_anterior: str,
438
+ humor_novo: str,
439
+ razao: str = "",
440
+ intensidade: float = 1.0
441
+ ):
442
+ """Registra transição de humor"""
443
+ contexto_id = self._gerar_contexto_id(numero, 'auto')
444
+
445
+ query = """
446
+ INSERT INTO humor_historico
447
+ (contexto_id, humor_anterior, humor_novo, razao_mudanca, intensidade)
448
+ VALUES (?, ?, ?, ?, ?)
449
+ """
450
+
451
+ self._execute_with_retry(
452
+ query,
453
+ (contexto_id, humor_anterior, humor_novo, razao, intensidade),
454
+ commit=True
455
+ )
456
+
457
+ # Atualiza contexto
458
+ self._execute_with_retry(
459
+ "UPDATE contexto SET humor_atual = ? WHERE contexto_id = ?",
460
+ (humor_novo, contexto_id),
461
+ commit=True
462
+ )
463
+
464
+ def salvar_reply_context(
465
+ self,
466
+ numero: str,
467
+ mensagem_id: str,
468
+ autor_original: str,
469
+ conteudo_original: str,
470
+ eh_resposta_a_bot: bool
471
+ ):
472
+ """Salva contexto de reply"""
473
+ contexto_id = self._gerar_contexto_id(numero, 'auto')
474
+
475
+ query = """
476
+ INSERT INTO reply_context
477
+ (contexto_id, mensagem_id, autor_original, conteudo_original, eh_resposta_a_bot)
478
+ VALUES (?, ?, ?, ?, ?)
479
+ """
480
+
481
+ self._execute_with_retry(
482
+ query,
483
+ (contexto_id, mensagem_id, autor_original, conteudo_original, int(eh_resposta_a_bot)),
484
+ commit=True
485
+ )
486
+
487
+ def salvar_training_example(
488
+ self,
489
+ input_text: str,
490
+ output_text: str,
491
+ humor: str,
492
+ modo_resposta: str,
493
+ qualidade_score: float = 1.0
494
+ ):
495
+ """Salva exemplo para training dataset"""
496
+ query = """
497
+ INSERT INTO training_examples
498
+ (input_text, output_text, humor, modo_resposta, qualidade_score)
499
+ VALUES (?, ?, ?, ?, ?)
500
+ """
501
+
502
+ self._execute_with_retry(
503
+ query,
504
+ (input_text[:500], output_text[:500], humor, modo_resposta, qualidade_score),
505
+ commit=True
506
+ )
507
+
508
+ def recuperar_training_examples(self, limite: int = 100) -> List[Dict]:
509
+ """Recupera exemplos para treinamento"""
510
+ query = """
511
+ SELECT input_text, output_text, humor, modo_resposta, qualidade_score
512
+ FROM training_examples
513
+ WHERE usado_treino = 0
514
+ ORDER BY qualidade_score DESC
515
+ LIMIT ?
516
+ """
517
+
518
+ results = self._execute_with_retry(query, (limite,))
519
+
520
+ if not results:
521
+ return []
522
+
523
+ return [
524
+ {
525
+ "input": r[0],
526
+ "output": r[1],
527
+ "humor": r[2],
528
+ "modo": r[3],
529
+ "score": r[4]
530
+ }
531
+ for r in results
532
+ ]
533
+
534
+ def marcar_examples_como_usados(self):
535
+ """Marca exemplos como já usados no treinamento"""
536
+ self._execute_with_retry(
537
+ "UPDATE training_examples SET usado_treino = 1 WHERE usado_treino = 0",
538
+ commit=True
539
+ )
540
+
541
+ def salvar_contexto(
542
+ self,
543
+ numero: str,
544
+ historico: str = "",
545
+ humor_atual: str = "normal",
546
+ modo_resposta: str = "casual_amigavel",
547
+ termos: str = "",
548
+ girias: str = "",
549
+ tom: str = "normal"
550
+ ):
551
+ """Salva contexto completo"""
552
+ contexto_id = self._gerar_contexto_id(numero, 'auto')
553
+ tipo_contexto = "grupo" if ("@g.us" in numero or "120363" in numero) else "pv"
554
+
555
+ query = """
556
+ INSERT OR REPLACE INTO contexto
557
+ (contexto_id, numero, tipo_contexto, historico, humor_atual, modo_resposta_atual, termos, girias, tom, ultima_atualizacao)
558
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
559
+ """
560
+
561
+ self._execute_with_retry(
562
+ query,
563
+ (contexto_id, numero, tipo_contexto, historico, humor_atual, modo_resposta, termos, girias, tom),
564
+ commit=True
565
+ )
566
+
567
+ def carregar_contexto(self, numero: str) -> Dict[str, Any]:
568
+ """Carrega contexto completo"""
569
+ contexto_id = self._gerar_contexto_id(numero, 'auto')
570
+
571
+ rows = self._execute_with_retry(
572
+ """SELECT historico, humor_atual, modo_resposta_atual, termos, girias, tom
573
+ FROM contexto
574
+ WHERE contexto_id = ?""",
575
+ (contexto_id,)
576
+ )
577
+
578
+ if not rows:
579
+ return {
580
+ "historico": "",
581
+ "humor_atual": "normal",
582
+ "modo_resposta": "casual_amigavel",
583
+ "termos": "",
584
+ "girias": "",
585
+ "tom": "normal"
586
+ }
587
+
588
+ row = rows[0]
589
+ return {
590
+ "historico": row[0] or "",
591
+ "humor_atual": row[1] or "normal",
592
+ "modo_resposta": row[2] or "casual_amigavel",
593
+ "termos": row[3] or "",
594
+ "girias": row[4] or "",
595
+ "tom": row[5] or "normal"
596
+ }
597
+
598
  def salvar_aprendizado_detalhado(
599
  self,
600
  numero_usuario: str,
 
605
  confianca: float = 1.0,
606
  contexto: Optional[str] = None
607
  ):
608
+ """Salva aprendizado com contexto isolado"""
609
+ contexto_id = self._gerar_contexto_id(numero_usuario, 'auto')
610
+
611
  self._execute_with_retry(
612
  """INSERT INTO aprendizado_detalhado
613
+ (contexto_id, tipo, chave, valor, fonte, confianca, contexto)
614
  VALUES (?, ?, ?, ?, ?, ?, ?)""",
615
+ (contexto_id, tipo, chave, valor, fonte, confianca, contexto),
616
  commit=True
617
  )
618
+
619
+ def registrar_tom_usuario(self, numero_usuario: str, tom: str, intensidade: float = 0.5, contexto: str = ""):
620
+ """Registra tom detectado (isolado)"""
621
+ contexto_id = self._gerar_contexto_id(numero_usuario, 'auto')
622
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
623
  try:
624
  self._execute_with_retry(
625
+ """INSERT INTO tom_usuario
626
+ (contexto_id, tom_detectado, intensidade, contexto)
627
+ VALUES (?, ?, ?, ?)""",
628
+ (contexto_id, tom, intensidade, contexto[:100]),
629
  commit=True
630
  )
631
  except Exception as e:
632
+ logger.warning(f"Erro ao registrar tom: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
633
 
634
+ def salvar_embedding(self, numero_usuario: str, mensagem: str, resposta: str, embedding, texto: str = ""):
635
+ """Salva embedding isolado"""
636
  try:
637
+ contexto_id = self._gerar_contexto_id(numero_usuario, 'auto')
638
+ embedding_blob = sqlite3.Binary(pickle.dumps(embedding))
639
+ texto_final = texto or f"{mensagem} {resposta}"
640
+
641
  self._execute_with_retry(
642
+ "INSERT INTO embeddings (contexto_id, source_type, texto, embedding) VALUES (?, ?, ?, ?)",
643
+ (contexto_id, 'conversa', texto_final[:500], embedding_blob),
 
 
644
  commit=True
645
  )
646
  except Exception as e:
647
+ logger.warning(f"Erro ao salvar embedding: {e}")