Spaces:
Running
Running
Update modules/database.py
Browse files- modules/database.py +301 -40
modules/database.py
CHANGED
|
@@ -1,7 +1,8 @@
|
|
| 1 |
-
# modules/database.py — AKIRA V21 FINAL CORRIGIDO (Dezembro 2025) - CORREÇÃO:
|
| 2 |
"""
|
| 3 |
✅ TOTALMENTE ADAPTADO ao index.js atualizado
|
| 4 |
✅ CORREÇÃO: Problema com message_id UNIQUE resolvido
|
|
|
|
| 5 |
✅ Métodos corretos para api.py, contexto.py, treinamento.py
|
| 6 |
✅ Estrutura completa com reply_metadata
|
| 7 |
✅ Todos os métodos necessários implementados
|
|
@@ -101,6 +102,10 @@ class Database:
|
|
| 101 |
emocao_detectada TEXT,
|
| 102 |
confianca_emocao REAL DEFAULT 0.5,
|
| 103 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
-- Grupo
|
| 105 |
grupo_id TEXT DEFAULT '',
|
| 106 |
grupo_nome TEXT DEFAULT '',
|
|
@@ -114,7 +119,7 @@ class Database:
|
|
| 114 |
comando_executado TEXT,
|
| 115 |
has_media BOOLEAN DEFAULT 0,
|
| 116 |
media_type TEXT DEFAULT '',
|
| 117 |
-
message_id TEXT,
|
| 118 |
bot_response_time_ms INTEGER DEFAULT 0,
|
| 119 |
is_mention BOOLEAN DEFAULT 0,
|
| 120 |
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
@@ -169,6 +174,7 @@ class Database:
|
|
| 169 |
humor_atual TEXT DEFAULT 'normal_ironico',
|
| 170 |
modo_resposta TEXT DEFAULT 'normal_ironico',
|
| 171 |
nivel_transicao INTEGER DEFAULT 0,
|
|
|
|
| 172 |
humor_alvo TEXT DEFAULT 'normal_ironico',
|
| 173 |
termos TEXT,
|
| 174 |
girias TEXT,
|
|
@@ -196,6 +202,7 @@ class Database:
|
|
| 196 |
output_text TEXT NOT NULL,
|
| 197 |
humor TEXT DEFAULT 'normal_ironico',
|
| 198 |
modo_resposta TEXT DEFAULT 'normal_ironico',
|
|
|
|
| 199 |
emocao_contexto TEXT,
|
| 200 |
contexto_super_claro TEXT,
|
| 201 |
tipo_interacao TEXT DEFAULT 'normal',
|
|
@@ -216,9 +223,10 @@ class Database:
|
|
| 216 |
contexto_id TEXT NOT NULL,
|
| 217 |
humor_anterior TEXT NOT NULL,
|
| 218 |
humor_novo TEXT NOT NULL,
|
|
|
|
|
|
|
| 219 |
emocao_trigger TEXT,
|
| 220 |
confianca_emocao REAL,
|
| 221 |
-
nivel_transicao INTEGER,
|
| 222 |
razao TEXT,
|
| 223 |
intensidade REAL DEFAULT 0.5,
|
| 224 |
contexto_mensagem TEXT,
|
|
@@ -270,8 +278,26 @@ class Database:
|
|
| 270 |
)
|
| 271 |
''')
|
| 272 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 273 |
conn.commit()
|
| 274 |
-
logger.info("✅ Tabelas criadas/verificadas com
|
| 275 |
|
| 276 |
except Exception as e:
|
| 277 |
logger.error(f"❌ Erro ao criar tabelas: {e}")
|
|
@@ -298,8 +324,10 @@ class Database:
|
|
| 298 |
("is_mention", "BOOLEAN DEFAULT 0"),
|
| 299 |
("has_media", "BOOLEAN DEFAULT 0"),
|
| 300 |
("media_type", "TEXT DEFAULT ''"),
|
| 301 |
-
("message_id", "TEXT"),
|
| 302 |
-
("bot_response_time_ms", "INTEGER DEFAULT 0")
|
|
|
|
|
|
|
| 303 |
]
|
| 304 |
|
| 305 |
for col_name, col_def in novas_colunas:
|
|
@@ -308,6 +336,41 @@ class Database:
|
|
| 308 |
except sqlite3.OperationalError:
|
| 309 |
pass
|
| 310 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 311 |
conn.commit()
|
| 312 |
|
| 313 |
except Exception as e:
|
|
@@ -326,6 +389,8 @@ class Database:
|
|
| 326 |
modo_resposta: str = 'normal_ironico',
|
| 327 |
emocao_detectada: str = None,
|
| 328 |
confianca_emocao: float = 0.5,
|
|
|
|
|
|
|
| 329 |
tipo_mensagem: str = 'texto',
|
| 330 |
reply_info_json: str = None,
|
| 331 |
usuario_nome: str = '',
|
|
@@ -341,7 +406,7 @@ class Database:
|
|
| 341 |
message_id: str = None,
|
| 342 |
bot_response_time_ms: int = 0,
|
| 343 |
is_mention: bool = False) -> bool:
|
| 344 |
-
"""Salva mensagem no banco -
|
| 345 |
try:
|
| 346 |
numero_final = str(numero or usuario).strip()
|
| 347 |
contexto_id = self._gerar_contexto_id(numero_final, tipo_conversa)
|
|
@@ -350,14 +415,18 @@ class Database:
|
|
| 350 |
if isinstance(reply_info_json, dict):
|
| 351 |
reply_info_json = json.dumps(reply_info_json, ensure_ascii=False)
|
| 352 |
|
| 353 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 354 |
if not message_id:
|
| 355 |
timestamp = int(time.time() * 1000)
|
| 356 |
random_suffix = random.randint(1000, 9999)
|
| 357 |
message_id = f"{numero_final}_{timestamp}_{random_suffix}"
|
| 358 |
|
| 359 |
-
#
|
| 360 |
-
# mesmo que o timestamp seja o mesmo
|
| 361 |
unique_suffix = random.randint(100, 999)
|
| 362 |
message_id = f"{message_id}_{unique_suffix}"
|
| 363 |
|
|
@@ -368,10 +437,10 @@ class Database:
|
|
| 368 |
(usuario, usuario_nome, mensagem, resposta, numero, contexto_id, tipo_contexto,
|
| 369 |
tipo_conversa, tipo_mensagem, is_reply, mensagem_original, mensagem_citada_limpa,
|
| 370 |
reply_to_bot, reply_info_json, humor, modo_resposta, emocao_detectada,
|
| 371 |
-
confianca_emocao,
|
| 372 |
-
|
| 373 |
-
bot_response_time_ms, is_mention)
|
| 374 |
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
| 375 |
""",
|
| 376 |
(
|
| 377 |
usuario[:50], usuario_nome[:100] or usuario[:100],
|
|
@@ -379,7 +448,8 @@ class Database:
|
|
| 379 |
contexto_id, tipo_conversa, tipo_conversa, tipo_mensagem,
|
| 380 |
int(is_reply), mensagem_original, mensagem_citada_limpa,
|
| 381 |
int(reply_to_bot), reply_info_json, humor, modo_resposta,
|
| 382 |
-
emocao_detectada, confianca_emocao,
|
|
|
|
| 383 |
audio_transcricao[:2000] if audio_transcricao else None,
|
| 384 |
fonte_stt[:50], confianca_stt, comando_executado[:100] if comando_executado else None,
|
| 385 |
int(has_media), media_type[:50], message_id[:200],
|
|
@@ -389,7 +459,7 @@ class Database:
|
|
| 389 |
fetch=False
|
| 390 |
)
|
| 391 |
|
| 392 |
-
logger.debug(f"✅ Mensagem salva: {numero_final} | Message ID: {message_id}")
|
| 393 |
return True
|
| 394 |
|
| 395 |
except sqlite3.IntegrityError as e:
|
|
@@ -405,10 +475,10 @@ class Database:
|
|
| 405 |
(usuario, usuario_nome, mensagem, resposta, numero, contexto_id, tipo_contexto,
|
| 406 |
tipo_conversa, tipo_mensagem, is_reply, mensagem_original, mensagem_citada_limpa,
|
| 407 |
reply_to_bot, reply_info_json, humor, modo_resposta, emocao_detectada,
|
| 408 |
-
confianca_emocao,
|
| 409 |
-
|
| 410 |
-
bot_response_time_ms, is_mention)
|
| 411 |
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
| 412 |
""",
|
| 413 |
(
|
| 414 |
usuario[:50], usuario_nome[:100] or usuario[:100],
|
|
@@ -416,7 +486,8 @@ class Database:
|
|
| 416 |
contexto_id, tipo_conversa, tipo_conversa, tipo_mensagem,
|
| 417 |
int(is_reply), mensagem_original, mensagem_citada_limpa,
|
| 418 |
int(reply_to_bot), reply_info_json, humor, modo_resposta,
|
| 419 |
-
emocao_detectada, confianca_emocao,
|
|
|
|
| 420 |
audio_transcricao[:2000] if audio_transcricao else None,
|
| 421 |
fonte_stt[:50], confianca_stt, comando_executado[:100] if comando_executado else None,
|
| 422 |
int(has_media), media_type[:50], new_message_id[:200],
|
|
@@ -437,13 +508,14 @@ class Database:
|
|
| 437 |
def salvar_training_example(self, input_text: str, output_text: str,
|
| 438 |
humor: str = "normal_ironico",
|
| 439 |
modo_resposta: str = "normal_ironico",
|
|
|
|
| 440 |
emocao_contexto: str = None,
|
| 441 |
qualidade_score: float = 1.0,
|
| 442 |
contexto_super_claro: Dict = None,
|
| 443 |
tipo_interacao: str = "normal",
|
| 444 |
score_relevancia: float = 1.0,
|
| 445 |
tags: List[str] = None) -> bool:
|
| 446 |
-
"""Salva exemplo de treinamento"""
|
| 447 |
try:
|
| 448 |
contexto_json = json.dumps(contexto_super_claro, ensure_ascii=False) if contexto_super_claro else None
|
| 449 |
tags_str = ",".join(tags) if tags else ""
|
|
@@ -451,50 +523,54 @@ class Database:
|
|
| 451 |
self._execute_with_retry(
|
| 452 |
"""
|
| 453 |
INSERT INTO training_examples
|
| 454 |
-
(input_text, output_text, humor, modo_resposta,
|
| 455 |
-
qualidade_score, contexto_super_claro,
|
| 456 |
-
|
|
|
|
| 457 |
""",
|
| 458 |
(
|
| 459 |
input_text[:2000], output_text[:2000], humor, modo_resposta,
|
| 460 |
-
emocao_contexto, qualidade_score, contexto_json,
|
| 461 |
tipo_interacao, score_relevancia, tags_str[:200]
|
| 462 |
),
|
| 463 |
commit=True,
|
| 464 |
fetch=False
|
| 465 |
)
|
| 466 |
-
logger.debug(f"✅ Training example salvo | Score: {qualidade_score:.2f}")
|
| 467 |
return True
|
| 468 |
except Exception as e:
|
| 469 |
logger.error(f"❌ Erro ao salvar training: {e}")
|
| 470 |
return False
|
| 471 |
|
| 472 |
def salvar_transicao_humor(self, numero: str, humor_anterior: str,
|
| 473 |
-
humor_novo: str,
|
|
|
|
|
|
|
| 474 |
confianca_emocao: float = 0.5,
|
| 475 |
-
nivel_transicao: int = 0,
|
| 476 |
razao: str = "", intensidade: float = 0.5,
|
| 477 |
contexto_mensagem: str = None):
|
| 478 |
-
"""Salva transição de humor"""
|
| 479 |
try:
|
| 480 |
contexto_id = self._gerar_contexto_id(numero, 'auto')
|
| 481 |
|
| 482 |
self._execute_with_retry(
|
| 483 |
"""
|
| 484 |
INSERT INTO transicoes_humor
|
| 485 |
-
(numero, contexto_id, humor_anterior, humor_novo,
|
| 486 |
-
|
| 487 |
-
|
|
|
|
| 488 |
""",
|
| 489 |
(
|
| 490 |
str(numero).strip(), contexto_id, humor_anterior, humor_novo,
|
| 491 |
-
|
| 492 |
-
razao[:200], intensidade,
|
|
|
|
| 493 |
),
|
| 494 |
commit=True,
|
| 495 |
fetch=False
|
| 496 |
)
|
| 497 |
-
logger.debug(f"🎭 Transição salva: {humor_anterior}
|
| 498 |
except Exception as e:
|
| 499 |
logger.error(f"❌ Erro ao salvar transição: {e}")
|
| 500 |
|
|
@@ -538,6 +614,136 @@ class Database:
|
|
| 538 |
logger.error(f"❌ Erro ao salvar gíria: {e}")
|
| 539 |
return False
|
| 540 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 541 |
# ========================================================================
|
| 542 |
# MÉTODOS DE RECUPERAÇÃO
|
| 543 |
# ========================================================================
|
|
@@ -548,7 +754,7 @@ class Database:
|
|
| 548 |
results = self._execute_with_retry(
|
| 549 |
"""
|
| 550 |
SELECT mensagem, resposta, is_reply, mensagem_original,
|
| 551 |
-
reply_to_bot, humor, modo_resposta, timestamp
|
| 552 |
FROM mensagens
|
| 553 |
WHERE numero = ? AND deletado = 0
|
| 554 |
ORDER BY timestamp DESC
|
|
@@ -589,13 +795,25 @@ class Database:
|
|
| 589 |
except Exception:
|
| 590 |
return "normal_ironico"
|
| 591 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 592 |
def recuperar_training_examples(self, limite: int = 100, usado: bool = False) -> List[Dict]:
|
| 593 |
"""Recupera exemplos de treinamento"""
|
| 594 |
try:
|
| 595 |
where_clause = "WHERE usado = 0" if not usado else ""
|
| 596 |
results = self._execute_with_retry(
|
| 597 |
f"""
|
| 598 |
-
SELECT input_text, output_text, humor, modo_resposta,
|
| 599 |
qualidade_score, tipo_interacao
|
| 600 |
FROM training_examples
|
| 601 |
{where_clause}
|
|
@@ -612,8 +830,9 @@ class Database:
|
|
| 612 |
"output": r[1],
|
| 613 |
"humor": r[2],
|
| 614 |
"modo": r[3],
|
| 615 |
-
"
|
| 616 |
-
"
|
|
|
|
| 617 |
}
|
| 618 |
for r in results
|
| 619 |
]
|
|
@@ -637,6 +856,44 @@ class Database:
|
|
| 637 |
except Exception as e:
|
| 638 |
logger.error(f"❌ Erro ao marcar exemplos: {e}")
|
| 639 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 640 |
# ========================================================================
|
| 641 |
# PRIVILÉGIOS
|
| 642 |
# ========================================================================
|
|
@@ -757,10 +1014,14 @@ class Database:
|
|
| 757 |
'timestamp': time.time()
|
| 758 |
}
|
| 759 |
|
|
|
|
|
|
|
| 760 |
return self.salvar_training_example(
|
| 761 |
input_text=input_text,
|
| 762 |
output_text=output_text,
|
| 763 |
humor=contexto.get("humor_atualizado", "normal_ironico"),
|
|
|
|
|
|
|
| 764 |
qualidade_score=qualidade_score,
|
| 765 |
contexto_super_claro=contexto_super_claro,
|
| 766 |
tipo_interacao=tipo_aprendizado
|
|
|
|
| 1 |
+
# modules/database.py — AKIRA V21 FINAL CORRIGIDO (Dezembro 2025) - CORREÇÃO: Com suporte a nivel_transicao
|
| 2 |
"""
|
| 3 |
✅ TOTALMENTE ADAPTADO ao index.js atualizado
|
| 4 |
✅ CORREÇÃO: Problema com message_id UNIQUE resolvido
|
| 5 |
+
✅ CORREÇÃO: Suporte para nivel_transicao adicionado
|
| 6 |
✅ Métodos corretos para api.py, contexto.py, treinamento.py
|
| 7 |
✅ Estrutura completa com reply_metadata
|
| 8 |
✅ Todos os métodos necessários implementados
|
|
|
|
| 102 |
emocao_detectada TEXT,
|
| 103 |
confianca_emocao REAL DEFAULT 0.5,
|
| 104 |
|
| 105 |
+
-- Transição
|
| 106 |
+
nivel_transicao INTEGER DEFAULT 0,
|
| 107 |
+
info_transicao_json TEXT,
|
| 108 |
+
|
| 109 |
-- Grupo
|
| 110 |
grupo_id TEXT DEFAULT '',
|
| 111 |
grupo_nome TEXT DEFAULT '',
|
|
|
|
| 119 |
comando_executado TEXT,
|
| 120 |
has_media BOOLEAN DEFAULT 0,
|
| 121 |
media_type TEXT DEFAULT '',
|
| 122 |
+
message_id TEXT,
|
| 123 |
bot_response_time_ms INTEGER DEFAULT 0,
|
| 124 |
is_mention BOOLEAN DEFAULT 0,
|
| 125 |
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
| 174 |
humor_atual TEXT DEFAULT 'normal_ironico',
|
| 175 |
modo_resposta TEXT DEFAULT 'normal_ironico',
|
| 176 |
nivel_transicao INTEGER DEFAULT 0,
|
| 177 |
+
info_transicao_json TEXT,
|
| 178 |
humor_alvo TEXT DEFAULT 'normal_ironico',
|
| 179 |
termos TEXT,
|
| 180 |
girias TEXT,
|
|
|
|
| 202 |
output_text TEXT NOT NULL,
|
| 203 |
humor TEXT DEFAULT 'normal_ironico',
|
| 204 |
modo_resposta TEXT DEFAULT 'normal_ironico',
|
| 205 |
+
nivel_transicao INTEGER DEFAULT 0,
|
| 206 |
emocao_contexto TEXT,
|
| 207 |
contexto_super_claro TEXT,
|
| 208 |
tipo_interacao TEXT DEFAULT 'normal',
|
|
|
|
| 223 |
contexto_id TEXT NOT NULL,
|
| 224 |
humor_anterior TEXT NOT NULL,
|
| 225 |
humor_novo TEXT NOT NULL,
|
| 226 |
+
nivel_transicao_anterior INTEGER DEFAULT 0,
|
| 227 |
+
nivel_transicao_novo INTEGER DEFAULT 0,
|
| 228 |
emocao_trigger TEXT,
|
| 229 |
confianca_emocao REAL,
|
|
|
|
| 230 |
razao TEXT,
|
| 231 |
intensidade REAL DEFAULT 0.5,
|
| 232 |
contexto_mensagem TEXT,
|
|
|
|
| 278 |
)
|
| 279 |
''')
|
| 280 |
|
| 281 |
+
# Interações (para treinamento)
|
| 282 |
+
c.execute('''
|
| 283 |
+
CREATE TABLE IF NOT EXISTS interacoes (
|
| 284 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 285 |
+
numero TEXT NOT NULL,
|
| 286 |
+
mensagem TEXT NOT NULL,
|
| 287 |
+
resposta TEXT NOT NULL,
|
| 288 |
+
humor TEXT DEFAULT 'normal_ironico',
|
| 289 |
+
modo_resposta TEXT DEFAULT 'normal_ironico',
|
| 290 |
+
nivel_transicao INTEGER DEFAULT 0,
|
| 291 |
+
emocao_detectada TEXT,
|
| 292 |
+
tipo_conversa TEXT DEFAULT 'pv',
|
| 293 |
+
reply_info_json TEXT,
|
| 294 |
+
qualidade_score REAL DEFAULT 1.0,
|
| 295 |
+
data_criacao DATETIME DEFAULT CURRENT_TIMESTAMP
|
| 296 |
+
)
|
| 297 |
+
''')
|
| 298 |
+
|
| 299 |
conn.commit()
|
| 300 |
+
logger.info("✅ Tabelas criadas/verificadas com suporte a nivel_transicao")
|
| 301 |
|
| 302 |
except Exception as e:
|
| 303 |
logger.error(f"❌ Erro ao criar tabelas: {e}")
|
|
|
|
| 324 |
("is_mention", "BOOLEAN DEFAULT 0"),
|
| 325 |
("has_media", "BOOLEAN DEFAULT 0"),
|
| 326 |
("media_type", "TEXT DEFAULT ''"),
|
| 327 |
+
("message_id", "TEXT"),
|
| 328 |
+
("bot_response_time_ms", "INTEGER DEFAULT 0"),
|
| 329 |
+
("nivel_transicao", "INTEGER DEFAULT 0"),
|
| 330 |
+
("info_transicao_json", "TEXT")
|
| 331 |
]
|
| 332 |
|
| 333 |
for col_name, col_def in novas_colunas:
|
|
|
|
| 336 |
except sqlite3.OperationalError:
|
| 337 |
pass
|
| 338 |
|
| 339 |
+
# Colunas para contexto
|
| 340 |
+
contexto_colunas = [
|
| 341 |
+
("info_transicao_json", "TEXT"),
|
| 342 |
+
("nivel_transicao", "INTEGER DEFAULT 0")
|
| 343 |
+
]
|
| 344 |
+
|
| 345 |
+
for col_name, col_def in contexto_colunas:
|
| 346 |
+
try:
|
| 347 |
+
c.execute(f"ALTER TABLE contexto ADD COLUMN {col_name} {col_def}")
|
| 348 |
+
except sqlite3.OperationalError:
|
| 349 |
+
pass
|
| 350 |
+
|
| 351 |
+
# Colunas para transições
|
| 352 |
+
transicoes_colunas = [
|
| 353 |
+
("nivel_transicao_anterior", "INTEGER DEFAULT 0"),
|
| 354 |
+
("nivel_transicao_novo", "INTEGER DEFAULT 0")
|
| 355 |
+
]
|
| 356 |
+
|
| 357 |
+
for col_name, col_def in transicoes_colunas:
|
| 358 |
+
try:
|
| 359 |
+
c.execute(f"ALTER TABLE transicoes_humor ADD COLUMN {col_name} {col_def}")
|
| 360 |
+
except sqlite3.OperationalError:
|
| 361 |
+
pass
|
| 362 |
+
|
| 363 |
+
# Colunas para training_examples
|
| 364 |
+
training_colunas = [
|
| 365 |
+
("nivel_transicao", "INTEGER DEFAULT 0")
|
| 366 |
+
]
|
| 367 |
+
|
| 368 |
+
for col_name, col_def in training_colunas:
|
| 369 |
+
try:
|
| 370 |
+
c.execute(f"ALTER TABLE training_examples ADD COLUMN {col_name} {col_def}")
|
| 371 |
+
except sqlite3.OperationalError:
|
| 372 |
+
pass
|
| 373 |
+
|
| 374 |
conn.commit()
|
| 375 |
|
| 376 |
except Exception as e:
|
|
|
|
| 389 |
modo_resposta: str = 'normal_ironico',
|
| 390 |
emocao_detectada: str = None,
|
| 391 |
confianca_emocao: float = 0.5,
|
| 392 |
+
nivel_transicao: int = 0,
|
| 393 |
+
info_transicao: dict = None,
|
| 394 |
tipo_mensagem: str = 'texto',
|
| 395 |
reply_info_json: str = None,
|
| 396 |
usuario_nome: str = '',
|
|
|
|
| 406 |
message_id: str = None,
|
| 407 |
bot_response_time_ms: int = 0,
|
| 408 |
is_mention: bool = False) -> bool:
|
| 409 |
+
"""Salva mensagem no banco - COM SUPORTE A nivel_transicao"""
|
| 410 |
try:
|
| 411 |
numero_final = str(numero or usuario).strip()
|
| 412 |
contexto_id = self._gerar_contexto_id(numero_final, tipo_conversa)
|
|
|
|
| 415 |
if isinstance(reply_info_json, dict):
|
| 416 |
reply_info_json = json.dumps(reply_info_json, ensure_ascii=False)
|
| 417 |
|
| 418 |
+
# Converte info_transicao para JSON se for dict
|
| 419 |
+
info_transicao_json = None
|
| 420 |
+
if info_transicao:
|
| 421 |
+
info_transicao_json = json.dumps(info_transicao, ensure_ascii=False)
|
| 422 |
+
|
| 423 |
+
# Gera message_id único se não fornecido
|
| 424 |
if not message_id:
|
| 425 |
timestamp = int(time.time() * 1000)
|
| 426 |
random_suffix = random.randint(1000, 9999)
|
| 427 |
message_id = f"{numero_final}_{timestamp}_{random_suffix}"
|
| 428 |
|
| 429 |
+
# Adiciona um sufixo aleatório extra para garantir unicidade
|
|
|
|
| 430 |
unique_suffix = random.randint(100, 999)
|
| 431 |
message_id = f"{message_id}_{unique_suffix}"
|
| 432 |
|
|
|
|
| 437 |
(usuario, usuario_nome, mensagem, resposta, numero, contexto_id, tipo_contexto,
|
| 438 |
tipo_conversa, tipo_mensagem, is_reply, mensagem_original, mensagem_citada_limpa,
|
| 439 |
reply_to_bot, reply_info_json, humor, modo_resposta, emocao_detectada,
|
| 440 |
+
confianca_emocao, nivel_transicao, info_transicao_json, grupo_id, grupo_nome,
|
| 441 |
+
audio_transcricao, fonte_stt, confianca_stt, comando_executado, has_media,
|
| 442 |
+
media_type, message_id, bot_response_time_ms, is_mention)
|
| 443 |
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
| 444 |
""",
|
| 445 |
(
|
| 446 |
usuario[:50], usuario_nome[:100] or usuario[:100],
|
|
|
|
| 448 |
contexto_id, tipo_conversa, tipo_conversa, tipo_mensagem,
|
| 449 |
int(is_reply), mensagem_original, mensagem_citada_limpa,
|
| 450 |
int(reply_to_bot), reply_info_json, humor, modo_resposta,
|
| 451 |
+
emocao_detectada, confianca_emocao, nivel_transicao,
|
| 452 |
+
info_transicao_json, grupo_id[:50], grupo_nome[:100],
|
| 453 |
audio_transcricao[:2000] if audio_transcricao else None,
|
| 454 |
fonte_stt[:50], confianca_stt, comando_executado[:100] if comando_executado else None,
|
| 455 |
int(has_media), media_type[:50], message_id[:200],
|
|
|
|
| 459 |
fetch=False
|
| 460 |
)
|
| 461 |
|
| 462 |
+
logger.debug(f"✅ Mensagem salva: {numero_final} | Nível: {nivel_transicao} | Message ID: {message_id}")
|
| 463 |
return True
|
| 464 |
|
| 465 |
except sqlite3.IntegrityError as e:
|
|
|
|
| 475 |
(usuario, usuario_nome, mensagem, resposta, numero, contexto_id, tipo_contexto,
|
| 476 |
tipo_conversa, tipo_mensagem, is_reply, mensagem_original, mensagem_citada_limpa,
|
| 477 |
reply_to_bot, reply_info_json, humor, modo_resposta, emocao_detectada,
|
| 478 |
+
confianca_emocao, nivel_transicao, info_transicao_json, grupo_id, grupo_nome,
|
| 479 |
+
audio_transcricao, fonte_stt, confianca_stt, comando_executado, has_media,
|
| 480 |
+
media_type, message_id, bot_response_time_ms, is_mention)
|
| 481 |
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
| 482 |
""",
|
| 483 |
(
|
| 484 |
usuario[:50], usuario_nome[:100] or usuario[:100],
|
|
|
|
| 486 |
contexto_id, tipo_conversa, tipo_conversa, tipo_mensagem,
|
| 487 |
int(is_reply), mensagem_original, mensagem_citada_limpa,
|
| 488 |
int(reply_to_bot), reply_info_json, humor, modo_resposta,
|
| 489 |
+
emocao_detectada, confianca_emocao, nivel_transicao,
|
| 490 |
+
info_transicao_json, grupo_id[:50], grupo_nome[:100],
|
| 491 |
audio_transcricao[:2000] if audio_transcricao else None,
|
| 492 |
fonte_stt[:50], confianca_stt, comando_executado[:100] if comando_executado else None,
|
| 493 |
int(has_media), media_type[:50], new_message_id[:200],
|
|
|
|
| 508 |
def salvar_training_example(self, input_text: str, output_text: str,
|
| 509 |
humor: str = "normal_ironico",
|
| 510 |
modo_resposta: str = "normal_ironico",
|
| 511 |
+
nivel_transicao: int = 0,
|
| 512 |
emocao_contexto: str = None,
|
| 513 |
qualidade_score: float = 1.0,
|
| 514 |
contexto_super_claro: Dict = None,
|
| 515 |
tipo_interacao: str = "normal",
|
| 516 |
score_relevancia: float = 1.0,
|
| 517 |
tags: List[str] = None) -> bool:
|
| 518 |
+
"""Salva exemplo de treinamento - COM nivel_transicao"""
|
| 519 |
try:
|
| 520 |
contexto_json = json.dumps(contexto_super_claro, ensure_ascii=False) if contexto_super_claro else None
|
| 521 |
tags_str = ",".join(tags) if tags else ""
|
|
|
|
| 523 |
self._execute_with_retry(
|
| 524 |
"""
|
| 525 |
INSERT INTO training_examples
|
| 526 |
+
(input_text, output_text, humor, modo_resposta, nivel_transicao,
|
| 527 |
+
emocao_contexto, qualidade_score, contexto_super_claro,
|
| 528 |
+
tipo_interacao, score_relevancia, tags)
|
| 529 |
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
| 530 |
""",
|
| 531 |
(
|
| 532 |
input_text[:2000], output_text[:2000], humor, modo_resposta,
|
| 533 |
+
nivel_transicao, emocao_contexto, qualidade_score, contexto_json,
|
| 534 |
tipo_interacao, score_relevancia, tags_str[:200]
|
| 535 |
),
|
| 536 |
commit=True,
|
| 537 |
fetch=False
|
| 538 |
)
|
| 539 |
+
logger.debug(f"✅ Training example salvo | Nível: {nivel_transicao} | Score: {qualidade_score:.2f}")
|
| 540 |
return True
|
| 541 |
except Exception as e:
|
| 542 |
logger.error(f"❌ Erro ao salvar training: {e}")
|
| 543 |
return False
|
| 544 |
|
| 545 |
def salvar_transicao_humor(self, numero: str, humor_anterior: str,
|
| 546 |
+
humor_novo: str, nivel_transicao_anterior: int = 0,
|
| 547 |
+
nivel_transicao_novo: int = 0,
|
| 548 |
+
emocao_trigger: str = None,
|
| 549 |
confianca_emocao: float = 0.5,
|
|
|
|
| 550 |
razao: str = "", intensidade: float = 0.5,
|
| 551 |
contexto_mensagem: str = None):
|
| 552 |
+
"""Salva transição de humor - COM nivel_transicao"""
|
| 553 |
try:
|
| 554 |
contexto_id = self._gerar_contexto_id(numero, 'auto')
|
| 555 |
|
| 556 |
self._execute_with_retry(
|
| 557 |
"""
|
| 558 |
INSERT INTO transicoes_humor
|
| 559 |
+
(numero, contexto_id, humor_anterior, humor_novo,
|
| 560 |
+
nivel_transicao_anterior, nivel_transicao_novo, emocao_trigger,
|
| 561 |
+
confianca_emocao, razao, intensidade, contexto_mensagem)
|
| 562 |
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
| 563 |
""",
|
| 564 |
(
|
| 565 |
str(numero).strip(), contexto_id, humor_anterior, humor_novo,
|
| 566 |
+
nivel_transicao_anterior, nivel_transicao_novo, emocao_trigger,
|
| 567 |
+
confianca_emocao, razao[:200], intensidade,
|
| 568 |
+
contexto_mensagem[:500] if contexto_mensagem else None
|
| 569 |
),
|
| 570 |
commit=True,
|
| 571 |
fetch=False
|
| 572 |
)
|
| 573 |
+
logger.debug(f"🎭 Transição salva: {humor_anterior}→{humor_novo} | Nível: {nivel_transicao_anterior}→{nivel_transicao_novo}")
|
| 574 |
except Exception as e:
|
| 575 |
logger.error(f"❌ Erro ao salvar transição: {e}")
|
| 576 |
|
|
|
|
| 614 |
logger.error(f"❌ Erro ao salvar gíria: {e}")
|
| 615 |
return False
|
| 616 |
|
| 617 |
+
# ========================================================================
|
| 618 |
+
# MÉTODOS DE CONTEXTO - COM nivel_transicao
|
| 619 |
+
# ========================================================================
|
| 620 |
+
|
| 621 |
+
def atualizar_contexto(self, numero: str, humor_atual: str = None,
|
| 622 |
+
modo_resposta: str = None, nivel_transicao: int = None,
|
| 623 |
+
info_transicao: dict = None, tom: str = None,
|
| 624 |
+
emocao_tendencia: str = None) -> bool:
|
| 625 |
+
"""Atualiza contexto do usuário - COM nivel_transicao"""
|
| 626 |
+
try:
|
| 627 |
+
numero_final = str(numero).strip()
|
| 628 |
+
contexto_id = self._gerar_contexto_id(numero_final, 'auto')
|
| 629 |
+
|
| 630 |
+
# Verifica se contexto existe
|
| 631 |
+
result = self._execute_with_retry(
|
| 632 |
+
"SELECT 1 FROM contexto WHERE numero = ?",
|
| 633 |
+
(numero_final,),
|
| 634 |
+
fetch=True
|
| 635 |
+
)
|
| 636 |
+
|
| 637 |
+
info_transicao_json = None
|
| 638 |
+
if info_transicao:
|
| 639 |
+
info_transicao_json = json.dumps(info_transicao, ensure_ascii=False)
|
| 640 |
+
|
| 641 |
+
if result:
|
| 642 |
+
# Atualiza existente
|
| 643 |
+
updates = []
|
| 644 |
+
params = []
|
| 645 |
+
|
| 646 |
+
if humor_atual:
|
| 647 |
+
updates.append("humor_atual = ?")
|
| 648 |
+
params.append(humor_atual)
|
| 649 |
+
if modo_resposta:
|
| 650 |
+
updates.append("modo_resposta = ?")
|
| 651 |
+
params.append(modo_resposta)
|
| 652 |
+
if nivel_transicao is not None:
|
| 653 |
+
updates.append("nivel_transicao = ?")
|
| 654 |
+
params.append(nivel_transicao)
|
| 655 |
+
if info_transicao_json:
|
| 656 |
+
updates.append("info_transicao_json = ?")
|
| 657 |
+
params.append(info_transicao_json)
|
| 658 |
+
if tom:
|
| 659 |
+
updates.append("tom = ?")
|
| 660 |
+
params.append(tom)
|
| 661 |
+
if emocao_tendencia:
|
| 662 |
+
updates.append("emocao_tendencia = ?")
|
| 663 |
+
params.append(emocao_tendencia)
|
| 664 |
+
|
| 665 |
+
updates.append("ultimo_contato = CURRENT_TIMESTAMP")
|
| 666 |
+
updates.append("data_atualizacao = CURRENT_TIMESTAMP")
|
| 667 |
+
|
| 668 |
+
if updates:
|
| 669 |
+
query = f"UPDATE contexto SET {', '.join(updates)} WHERE numero = ?"
|
| 670 |
+
params.append(numero_final)
|
| 671 |
+
self._execute_with_retry(query, tuple(params), commit=True, fetch=False)
|
| 672 |
+
else:
|
| 673 |
+
# Cria novo contexto
|
| 674 |
+
self._execute_with_retry(
|
| 675 |
+
"""
|
| 676 |
+
INSERT INTO contexto
|
| 677 |
+
(numero, contexto_id, humor_atual, modo_resposta, nivel_transicao,
|
| 678 |
+
info_transicao_json, tom, emocao_tendencia, ultimo_contato)
|
| 679 |
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
| 680 |
+
""",
|
| 681 |
+
(
|
| 682 |
+
numero_final, contexto_id,
|
| 683 |
+
humor_atual or 'normal_ironico',
|
| 684 |
+
modo_resposta or 'normal_ironico',
|
| 685 |
+
nivel_transicao or 0,
|
| 686 |
+
info_transicao_json,
|
| 687 |
+
tom or 'normal',
|
| 688 |
+
emocao_tendencia or 'neutral'
|
| 689 |
+
),
|
| 690 |
+
commit=True,
|
| 691 |
+
fetch=False
|
| 692 |
+
)
|
| 693 |
+
|
| 694 |
+
logger.debug(f"✅ Contexto atualizado: {numero_final} | Nível: {nivel_transicao}")
|
| 695 |
+
return True
|
| 696 |
+
|
| 697 |
+
except Exception as e:
|
| 698 |
+
logger.error(f"❌ Erro ao atualizar contexto: {e}")
|
| 699 |
+
return False
|
| 700 |
+
|
| 701 |
+
def recuperar_contexto(self, numero: str) -> Dict[str, Any]:
|
| 702 |
+
"""Recupera contexto completo do usuário"""
|
| 703 |
+
try:
|
| 704 |
+
result = self._execute_with_retry(
|
| 705 |
+
"""
|
| 706 |
+
SELECT humor_atual, modo_resposta, nivel_transicao, info_transicao_json,
|
| 707 |
+
tom, emocao_tendencia, ultimo_contato
|
| 708 |
+
FROM contexto WHERE numero = ?
|
| 709 |
+
""",
|
| 710 |
+
(str(numero).strip(),),
|
| 711 |
+
fetch=True
|
| 712 |
+
)
|
| 713 |
+
|
| 714 |
+
if result:
|
| 715 |
+
row = result[0]
|
| 716 |
+
info_transicao = {}
|
| 717 |
+
if row[3]:
|
| 718 |
+
try:
|
| 719 |
+
info_transicao = json.loads(row[3])
|
| 720 |
+
except:
|
| 721 |
+
pass
|
| 722 |
+
|
| 723 |
+
return {
|
| 724 |
+
"humor_atual": row[0] or "normal_ironico",
|
| 725 |
+
"modo_resposta": row[1] or "normal_ironico",
|
| 726 |
+
"nivel_transicao": row[2] or 0,
|
| 727 |
+
"info_transicao": info_transicao,
|
| 728 |
+
"tom": row[4] or "normal",
|
| 729 |
+
"emocao_tendencia": row[5] or "neutral",
|
| 730 |
+
"ultimo_contato": row[6]
|
| 731 |
+
}
|
| 732 |
+
|
| 733 |
+
return {
|
| 734 |
+
"humor_atual": "normal_ironico",
|
| 735 |
+
"modo_resposta": "normal_ironico",
|
| 736 |
+
"nivel_transicao": 0,
|
| 737 |
+
"info_transicao": {},
|
| 738 |
+
"tom": "normal",
|
| 739 |
+
"emocao_tendencia": "neutral",
|
| 740 |
+
"ultimo_contato": None
|
| 741 |
+
}
|
| 742 |
+
|
| 743 |
+
except Exception as e:
|
| 744 |
+
logger.error(f"❌ Erro ao recuperar contexto: {e}")
|
| 745 |
+
return {}
|
| 746 |
+
|
| 747 |
# ========================================================================
|
| 748 |
# MÉTODOS DE RECUPERAÇÃO
|
| 749 |
# ========================================================================
|
|
|
|
| 754 |
results = self._execute_with_retry(
|
| 755 |
"""
|
| 756 |
SELECT mensagem, resposta, is_reply, mensagem_original,
|
| 757 |
+
reply_to_bot, humor, modo_resposta, nivel_transicao, timestamp
|
| 758 |
FROM mensagens
|
| 759 |
WHERE numero = ? AND deletado = 0
|
| 760 |
ORDER BY timestamp DESC
|
|
|
|
| 795 |
except Exception:
|
| 796 |
return "normal_ironico"
|
| 797 |
|
| 798 |
+
def recuperar_nivel_transicao(self, numero: str) -> int:
|
| 799 |
+
"""Recupera nível de transição"""
|
| 800 |
+
try:
|
| 801 |
+
result = self._execute_with_retry(
|
| 802 |
+
"SELECT nivel_transicao FROM contexto WHERE numero = ?",
|
| 803 |
+
(str(numero).strip(),),
|
| 804 |
+
fetch=True
|
| 805 |
+
)
|
| 806 |
+
return result[0][0] if result else 0
|
| 807 |
+
except Exception:
|
| 808 |
+
return 0
|
| 809 |
+
|
| 810 |
def recuperar_training_examples(self, limite: int = 100, usado: bool = False) -> List[Dict]:
|
| 811 |
"""Recupera exemplos de treinamento"""
|
| 812 |
try:
|
| 813 |
where_clause = "WHERE usado = 0" if not usado else ""
|
| 814 |
results = self._execute_with_retry(
|
| 815 |
f"""
|
| 816 |
+
SELECT input_text, output_text, humor, modo_resposta, nivel_transicao,
|
| 817 |
qualidade_score, tipo_interacao
|
| 818 |
FROM training_examples
|
| 819 |
{where_clause}
|
|
|
|
| 830 |
"output": r[1],
|
| 831 |
"humor": r[2],
|
| 832 |
"modo": r[3],
|
| 833 |
+
"nivel_transicao": r[4],
|
| 834 |
+
"score": r[5],
|
| 835 |
+
"tipo": r[6]
|
| 836 |
}
|
| 837 |
for r in results
|
| 838 |
]
|
|
|
|
| 856 |
except Exception as e:
|
| 857 |
logger.error(f"❌ Erro ao marcar exemplos: {e}")
|
| 858 |
|
| 859 |
+
# ========================================================================
|
| 860 |
+
# MÉTODO PARA REGISTRAR INTERAÇÃO (PARA TREINAMENTO)
|
| 861 |
+
# ========================================================================
|
| 862 |
+
|
| 863 |
+
def registrar_interacao(self, numero: str, mensagem: str, resposta: str,
|
| 864 |
+
humor: str = 'normal_ironico',
|
| 865 |
+
modo_resposta: str = 'normal_ironico',
|
| 866 |
+
nivel_transicao: int = 0,
|
| 867 |
+
emocao_detectada: str = None,
|
| 868 |
+
tipo_conversa: str = 'pv',
|
| 869 |
+
reply_info_json: str = None,
|
| 870 |
+
qualidade_score: float = 1.0) -> bool:
|
| 871 |
+
"""Registra interação para treinamento - COM nivel_transicao"""
|
| 872 |
+
try:
|
| 873 |
+
if isinstance(reply_info_json, dict):
|
| 874 |
+
reply_info_json = json.dumps(reply_info_json, ensure_ascii=False)
|
| 875 |
+
|
| 876 |
+
self._execute_with_retry(
|
| 877 |
+
"""
|
| 878 |
+
INSERT INTO interacoes
|
| 879 |
+
(numero, mensagem, resposta, humor, modo_resposta, nivel_transicao,
|
| 880 |
+
emocao_detectada, tipo_conversa, reply_info_json, qualidade_score)
|
| 881 |
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
| 882 |
+
""",
|
| 883 |
+
(
|
| 884 |
+
str(numero).strip(), mensagem[:2000], resposta[:2000], humor, modo_resposta,
|
| 885 |
+
nivel_transicao, emocao_detectada, tipo_conversa, reply_info_json,
|
| 886 |
+
qualidade_score
|
| 887 |
+
),
|
| 888 |
+
commit=True,
|
| 889 |
+
fetch=False
|
| 890 |
+
)
|
| 891 |
+
logger.debug(f"✅ Interação registrada: {numero} | Nível: {nivel_transicao}")
|
| 892 |
+
return True
|
| 893 |
+
except Exception as e:
|
| 894 |
+
logger.error(f"❌ Erro ao registrar interação: {e}")
|
| 895 |
+
return False
|
| 896 |
+
|
| 897 |
# ========================================================================
|
| 898 |
# PRIVILÉGIOS
|
| 899 |
# ========================================================================
|
|
|
|
| 1014 |
'timestamp': time.time()
|
| 1015 |
}
|
| 1016 |
|
| 1017 |
+
nivel_transicao = contexto.get('nivel_transicao', 0)
|
| 1018 |
+
|
| 1019 |
return self.salvar_training_example(
|
| 1020 |
input_text=input_text,
|
| 1021 |
output_text=output_text,
|
| 1022 |
humor=contexto.get("humor_atualizado", "normal_ironico"),
|
| 1023 |
+
modo_resposta=contexto.get("modo_resposta", "normal_ironico"),
|
| 1024 |
+
nivel_transicao=nivel_transicao,
|
| 1025 |
qualidade_score=qualidade_score,
|
| 1026 |
contexto_super_claro=contexto_super_claro,
|
| 1027 |
tipo_interacao=tipo_aprendizado
|