Spaces:
Running
Running
Upload 29 files
Browse files- modules/context_builder.py +7 -6
- modules/database.py +0 -0
- modules/grouped_skills_adapter.py +2 -53
- modules/lstm_extension.py +147 -51
- modules/lstm_memory_system.py +52 -45
- modules/reply_context_handler.py +781 -758
- modules/self_awareness.py +86 -0
- modules/sender_attribution_fix.py +55 -0
- modules/short_term_memory.py +10 -0
- modules/skills_library.py +0 -0
- modules/thinking_engine.py +374 -0
- modules/treinamento.py +5 -36
- modules/twitter_api.py +79 -100
- modules/unified_context.py +1041 -1182
modules/context_builder.py
CHANGED
|
@@ -210,12 +210,13 @@ class ContextBuilder:
|
|
| 210 |
Args:
|
| 211 |
db: Instância de Database
|
| 212 |
"""
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
|
|
|
| 219 |
|
| 220 |
def build_prompt(
|
| 221 |
self,
|
|
|
|
| 210 |
Args:
|
| 211 |
db: Instância de Database
|
| 212 |
"""
|
| 213 |
+
try:
|
| 214 |
+
from .lstm_extension import get_lstm_extension as _get_lstm
|
| 215 |
+
self.lstm_extension = _get_lstm(db)
|
| 216 |
+
logger.info("✅ LSTM Extension habilitado em ContextBuilder")
|
| 217 |
+
except Exception as e:
|
| 218 |
+
logger.debug(f"LSTM initialization: {e}")
|
| 219 |
+
|
| 220 |
|
| 221 |
def build_prompt(
|
| 222 |
self,
|
modules/database.py
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
modules/grouped_skills_adapter.py
CHANGED
|
@@ -11,8 +11,7 @@ from modules.skills import (
|
|
| 11 |
WeatherSkill,
|
| 12 |
EntertainmentSkill,
|
| 13 |
ArtSkill,
|
| 14 |
-
MusicSkill
|
| 15 |
-
ManusSkill
|
| 16 |
)
|
| 17 |
|
| 18 |
# ========================================
|
|
@@ -23,7 +22,6 @@ _weather_skill = WeatherSkill()
|
|
| 23 |
_entertainment_skill = EntertainmentSkill()
|
| 24 |
_art_skill = ArtSkill()
|
| 25 |
_music_skill = MusicSkill()
|
| 26 |
-
_manus_skill = ManusSkill()
|
| 27 |
|
| 28 |
|
| 29 |
# ========================================
|
|
@@ -314,20 +312,6 @@ def music_tool(tipo: str = "genre", mood: str = "random", anime: str = None, son
|
|
| 314 |
"provider": result["provider"]
|
| 315 |
}
|
| 316 |
|
| 317 |
-
elif dados.get("tipo") == "lyrics":
|
| 318 |
-
fragmento = dados.get("fragmento", "Letra não encontrada.")
|
| 319 |
-
url = dados.get("url", "")
|
| 320 |
-
fonte = dados.get("fonte", "desconhecida")
|
| 321 |
-
|
| 322 |
-
return {
|
| 323 |
-
"sucesso": True,
|
| 324 |
-
"tipo": "lyrics",
|
| 325 |
-
"musica": dados.get("musica"),
|
| 326 |
-
"artista": dados.get("artista"),
|
| 327 |
-
"conteudo": f"Fragmento da letra:\n\n{fragmento}\n\nFonte: {fonte}\nLink: {url}",
|
| 328 |
-
"provider": result["provider"]
|
| 329 |
-
}
|
| 330 |
-
|
| 331 |
else:
|
| 332 |
return {
|
| 333 |
"sucesso": True,
|
|
@@ -342,40 +326,6 @@ def music_tool(tipo: str = "genre", mood: str = "random", anime: str = None, son
|
|
| 342 |
}
|
| 343 |
|
| 344 |
|
| 345 |
-
@skill(
|
| 346 |
-
name="manus_research",
|
| 347 |
-
description="Realiza pesquisas profundas, análise de mercado ou tarefas autônomas complexas via Manus AI. Use para perguntas que exigem investigação séria.",
|
| 348 |
-
parameters={
|
| 349 |
-
"type": "object",
|
| 350 |
-
"properties": {
|
| 351 |
-
"prompt": {
|
| 352 |
-
"type": "string",
|
| 353 |
-
"description": "O que você quer que o Manus pesquise ou resolva detalhadamente."
|
| 354 |
-
}
|
| 355 |
-
},
|
| 356 |
-
"required": ["prompt"]
|
| 357 |
-
}
|
| 358 |
-
)
|
| 359 |
-
def manus_research_tool(prompt: str):
|
| 360 |
-
"""
|
| 361 |
-
Wrapper para ManusSkill
|
| 362 |
-
"""
|
| 363 |
-
result = _manus_skill.execute(prompt=prompt)
|
| 364 |
-
|
| 365 |
-
if result.get("sucesso"):
|
| 366 |
-
return {
|
| 367 |
-
"sucesso": True,
|
| 368 |
-
"analise": result["dados"].get("resultado"),
|
| 369 |
-
"provider": "Manus AI Agent",
|
| 370 |
-
"status": "Finalizado com sucesso"
|
| 371 |
-
}
|
| 372 |
-
else:
|
| 373 |
-
return {
|
| 374 |
-
"sucesso": False,
|
| 375 |
-
"erro": result.get("erro")
|
| 376 |
-
}
|
| 377 |
-
|
| 378 |
-
|
| 379 |
# ========================================
|
| 380 |
# Helper para stats
|
| 381 |
# ========================================
|
|
@@ -386,6 +336,5 @@ def get_grouped_skills_stats() -> Dict[str, Any]:
|
|
| 386 |
"weather": _weather_skill.get_stats(),
|
| 387 |
"entertainment": _entertainment_skill.get_stats(),
|
| 388 |
"art": _art_skill.get_stats(),
|
| 389 |
-
"music": _music_skill.get_stats()
|
| 390 |
-
"manus": _manus_skill.get_stats()
|
| 391 |
}
|
|
|
|
| 11 |
WeatherSkill,
|
| 12 |
EntertainmentSkill,
|
| 13 |
ArtSkill,
|
| 14 |
+
MusicSkill
|
|
|
|
| 15 |
)
|
| 16 |
|
| 17 |
# ========================================
|
|
|
|
| 22 |
_entertainment_skill = EntertainmentSkill()
|
| 23 |
_art_skill = ArtSkill()
|
| 24 |
_music_skill = MusicSkill()
|
|
|
|
| 25 |
|
| 26 |
|
| 27 |
# ========================================
|
|
|
|
| 312 |
"provider": result["provider"]
|
| 313 |
}
|
| 314 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 315 |
else:
|
| 316 |
return {
|
| 317 |
"sucesso": True,
|
|
|
|
| 326 |
}
|
| 327 |
|
| 328 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 329 |
# ========================================
|
| 330 |
# Helper para stats
|
| 331 |
# ========================================
|
|
|
|
| 336 |
"weather": _weather_skill.get_stats(),
|
| 337 |
"entertainment": _entertainment_skill.get_stats(),
|
| 338 |
"art": _art_skill.get_stats(),
|
| 339 |
+
"music": _music_skill.get_stats()
|
|
|
|
| 340 |
}
|
modules/lstm_extension.py
CHANGED
|
@@ -20,7 +20,6 @@ Features:
|
|
| 20 |
"""
|
| 21 |
|
| 22 |
import json
|
| 23 |
-
import re
|
| 24 |
import threading
|
| 25 |
from typing import Dict, Any, Optional, List
|
| 26 |
from dataclasses import dataclass
|
|
@@ -75,7 +74,8 @@ class LSTMExtension:
|
|
| 75 |
context_id: str,
|
| 76 |
numero_usuario: str,
|
| 77 |
message: str,
|
| 78 |
-
role: str = "user"
|
|
|
|
| 79 |
) -> None:
|
| 80 |
"""
|
| 81 |
Processa mensagem em background thread. NÃO BLOQUEIA.
|
|
@@ -85,11 +85,12 @@ class LSTMExtension:
|
|
| 85 |
numero_usuario: Número do usuário
|
| 86 |
message: Texto da mensagem
|
| 87 |
role: "user" ou "assistant"
|
|
|
|
| 88 |
"""
|
| 89 |
# Dispara em thread para não bloquear
|
| 90 |
thread = threading.Thread(
|
| 91 |
target=self._analyze_and_store,
|
| 92 |
-
args=(context_id, numero_usuario, message, role),
|
| 93 |
daemon=True
|
| 94 |
)
|
| 95 |
thread.start()
|
|
@@ -99,10 +100,19 @@ class LSTMExtension:
|
|
| 99 |
context_id: str,
|
| 100 |
numero_usuario: str,
|
| 101 |
message: str,
|
| 102 |
-
role: str
|
|
|
|
| 103 |
) -> None:
|
| 104 |
"""Análise interna (roda em thread separada)."""
|
| 105 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
# 1. Recuperar contexto existente
|
| 107 |
existing = self._get_from_db(context_id)
|
| 108 |
summary = existing or LSTMContextSummary(
|
|
@@ -135,6 +145,17 @@ class LSTMExtension:
|
|
| 135 |
|
| 136 |
# 5. Salvar em DB
|
| 137 |
self._save_to_db(summary)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
self.context_cache[context_id] = summary
|
| 139 |
|
| 140 |
logger.debug(f"✅ LSTM context saved: {context_id} (topic: {summary.topic_principal})")
|
|
@@ -145,7 +166,8 @@ class LSTMExtension:
|
|
| 145 |
def get_context_for_prompt(
|
| 146 |
self,
|
| 147 |
context_id: str,
|
| 148 |
-
numero_usuario: str
|
|
|
|
| 149 |
) -> Optional[Dict[str, Any]]:
|
| 150 |
"""
|
| 151 |
Recupera contexto LSTM para enriquecer prompt.
|
|
@@ -153,73 +175,95 @@ class LSTMExtension:
|
|
| 153 |
|
| 154 |
Args:
|
| 155 |
context_id: ID da conversa
|
| 156 |
-
numero_usuario: Número do usuário
|
|
|
|
| 157 |
|
| 158 |
Returns:
|
| 159 |
-
Dict com contexto de longo prazo, ou None
|
| 160 |
"""
|
| 161 |
-
# Tentar cache primeiro
|
| 162 |
-
if context_id in self.context_cache:
|
| 163 |
-
summary = self.context_cache[context_id]
|
| 164 |
-
else:
|
| 165 |
-
# Buscar DB
|
| 166 |
-
summary = self._get_from_db(context_id)
|
| 167 |
|
| 168 |
-
if
|
| 169 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 170 |
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 181 |
|
| 182 |
def _extract_topic_simple(self, message: str, current_topic: Optional[str]) -> Optional[str]:
|
| 183 |
"""
|
| 184 |
Extrai tópico de forma simples (sem LLM).
|
| 185 |
-
Heurísticas
|
| 186 |
"""
|
| 187 |
msg_lower = message.lower()
|
| 188 |
|
| 189 |
-
#
|
| 190 |
-
stopwords = {
|
| 191 |
-
"está", "como", "você", "para", "mais", "tudo", "bem", "pode", "fazer",
|
| 192 |
-
"quando", "onde", "quem", "porque", "qual", "quais", "muito", "pouco",
|
| 193 |
-
"esse", "essa", "aquele", "aquela", "coisa", "nada", "algo", "isso"
|
| 194 |
-
}
|
| 195 |
-
|
| 196 |
-
# Detectar palavras-chave comuns (Tópicos Fortes)
|
| 197 |
topics_keywords = {
|
| 198 |
-
"saúde": ["doença", "medicina", "cura", "tratamento", "sintoma", "hospital"
|
| 199 |
-
"
|
| 200 |
-
"relacionamento": ["namoro", "amor", "casal", "relacionamento", "ex"
|
| 201 |
-
"trabalho": ["emprego", "trabalho", "chefe", "salário", "despedida"
|
| 202 |
-
"escola": ["escola", "universidade", "prova", "nota", "aula"
|
| 203 |
-
"
|
| 204 |
-
"finanças": ["dinheiro", "preço", "valor", "kwanza", "aoa", "dólar", "comprar", "venda"]
|
| 205 |
}
|
| 206 |
|
| 207 |
for topic, keywords in topics_keywords.items():
|
| 208 |
if any(kw in msg_lower for kw in keywords):
|
| 209 |
return topic
|
| 210 |
|
| 211 |
-
# Se tem pergunta,
|
| 212 |
if "?" in message:
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
return filtered_words[0]
|
| 218 |
|
| 219 |
-
# Se a mensagem for muito curta, mantém o tópico atual (evita drift por ruído)
|
| 220 |
-
if len(message.split()) < 3:
|
| 221 |
-
return current_topic
|
| 222 |
-
|
| 223 |
return current_topic
|
| 224 |
|
| 225 |
def _detect_pattern(self, message: str) -> Optional[str]:
|
|
@@ -289,6 +333,58 @@ class LSTMExtension:
|
|
| 289 |
logger.warning(f"Error loading LSTM from DB: {e}")
|
| 290 |
return None
|
| 291 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 292 |
def _save_to_db(self, summary: LSTMContextSummary) -> None:
|
| 293 |
"""Salva contexto no banco de dados usando Database._execute_with_retry()."""
|
| 294 |
try:
|
|
|
|
| 20 |
"""
|
| 21 |
|
| 22 |
import json
|
|
|
|
| 23 |
import threading
|
| 24 |
from typing import Dict, Any, Optional, List
|
| 25 |
from dataclasses import dataclass
|
|
|
|
| 74 |
context_id: str,
|
| 75 |
numero_usuario: str,
|
| 76 |
message: str,
|
| 77 |
+
role: str = "user",
|
| 78 |
+
message_id: Optional[str] = None
|
| 79 |
) -> None:
|
| 80 |
"""
|
| 81 |
Processa mensagem em background thread. NÃO BLOQUEIA.
|
|
|
|
| 85 |
numero_usuario: Número do usuário
|
| 86 |
message: Texto da mensagem
|
| 87 |
role: "user" ou "assistant"
|
| 88 |
+
message_id: ID único da mensagem (para evitar duplicados)
|
| 89 |
"""
|
| 90 |
# Dispara em thread para não bloquear
|
| 91 |
thread = threading.Thread(
|
| 92 |
target=self._analyze_and_store,
|
| 93 |
+
args=(context_id, numero_usuario, message, role, message_id),
|
| 94 |
daemon=True
|
| 95 |
)
|
| 96 |
thread.start()
|
|
|
|
| 100 |
context_id: str,
|
| 101 |
numero_usuario: str,
|
| 102 |
message: str,
|
| 103 |
+
role: str,
|
| 104 |
+
message_id: Optional[str] = None
|
| 105 |
) -> None:
|
| 106 |
"""Análise interna (roda em thread separada)."""
|
| 107 |
try:
|
| 108 |
+
# 0. Verificação de idempotência (Anti-Duplicate)
|
| 109 |
+
if message_id:
|
| 110 |
+
query_check = "SELECT id FROM lstm_message_links WHERE context_id = ? AND message_id = ? LIMIT 1"
|
| 111 |
+
res = self.db._execute_with_retry(query_check, (context_id, message_id))
|
| 112 |
+
if res:
|
| 113 |
+
# logger.debug(f"⏭️ LSTM skip duplicate: {message_id}")
|
| 114 |
+
return
|
| 115 |
+
|
| 116 |
# 1. Recuperar contexto existente
|
| 117 |
existing = self._get_from_db(context_id)
|
| 118 |
summary = existing or LSTMContextSummary(
|
|
|
|
| 145 |
|
| 146 |
# 5. Salvar em DB
|
| 147 |
self._save_to_db(summary)
|
| 148 |
+
|
| 149 |
+
# 6. Registrar link da mensagem (idempotência)
|
| 150 |
+
if message_id:
|
| 151 |
+
try:
|
| 152 |
+
query_link = """INSERT INTO lstm_message_links
|
| 153 |
+
(context_id, message_id, numero_usuario, created_at)
|
| 154 |
+
VALUES (?, ?, ?, CURRENT_TIMESTAMP)"""
|
| 155 |
+
self.db._execute_with_retry(query_link, (context_id, message_id, numero_usuario), commit=True)
|
| 156 |
+
except Exception:
|
| 157 |
+
pass # Provavelmente já existe (race condition), ignorar
|
| 158 |
+
|
| 159 |
self.context_cache[context_id] = summary
|
| 160 |
|
| 161 |
logger.debug(f"✅ LSTM context saved: {context_id} (topic: {summary.topic_principal})")
|
|
|
|
| 166 |
def get_context_for_prompt(
|
| 167 |
self,
|
| 168 |
context_id: str,
|
| 169 |
+
numero_usuario: str = None,
|
| 170 |
+
is_group: bool = False
|
| 171 |
) -> Optional[Dict[str, Any]]:
|
| 172 |
"""
|
| 173 |
Recupera contexto LSTM para enriquecer prompt.
|
|
|
|
| 175 |
|
| 176 |
Args:
|
| 177 |
context_id: ID da conversa
|
| 178 |
+
numero_usuario: Número do usuário (pode ser None em grupos)
|
| 179 |
+
is_group: Se True, retorna contexto para TODOS os speakers do grupo
|
| 180 |
|
| 181 |
Returns:
|
| 182 |
+
Dict com contexto de longo prazo enriquecido com speaker tracking, ou None
|
| 183 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 184 |
|
| 185 |
+
if is_group:
|
| 186 |
+
# Recupera contexto para TODOS os speakers do grupo
|
| 187 |
+
summaries = self._get_from_db_all_speakers(context_id)
|
| 188 |
+
|
| 189 |
+
if not summaries:
|
| 190 |
+
return None
|
| 191 |
+
|
| 192 |
+
# Agrupa contexto: qual speaker falou sobre qual tópico
|
| 193 |
+
speakers_topics = {}
|
| 194 |
+
total_context_switches = 0
|
| 195 |
+
|
| 196 |
+
for summary in summaries:
|
| 197 |
+
if summary.numero_usuario and summary.topic_principal:
|
| 198 |
+
speakers_topics[summary.numero_usuario] = {
|
| 199 |
+
"topic_principal": summary.topic_principal,
|
| 200 |
+
"interaction_pattern": summary.interaction_pattern or "regular",
|
| 201 |
+
"unanswered_questions": summary.unanswered_questions[:2] if summary.unanswered_questions else [],
|
| 202 |
+
"assumed_knowledge": summary.assumed_knowledge[:1] if summary.assumed_knowledge else [],
|
| 203 |
+
}
|
| 204 |
+
total_context_switches += summary.context_switches or 0
|
| 205 |
+
|
| 206 |
+
if not speakers_topics:
|
| 207 |
+
return None
|
| 208 |
+
|
| 209 |
+
return {
|
| 210 |
+
"context_id": context_id,
|
| 211 |
+
"tipo": "grupo",
|
| 212 |
+
"speakers_topics": speakers_topics, # ✅ Rastreia quem falou o quê
|
| 213 |
+
"context_switches": total_context_switches,
|
| 214 |
+
}
|
| 215 |
|
| 216 |
+
else:
|
| 217 |
+
# Código original para PV/direto
|
| 218 |
+
# Tentar cache primeiro
|
| 219 |
+
if context_id in self.context_cache:
|
| 220 |
+
summary = self.context_cache[context_id]
|
| 221 |
+
else:
|
| 222 |
+
# Buscar DB (vai retornar primeiro speaker se houver múltiplos em grupo)
|
| 223 |
+
summary = self._get_from_db(context_id)
|
| 224 |
+
|
| 225 |
+
if not summary or not summary.topic_principal:
|
| 226 |
+
return None
|
| 227 |
+
|
| 228 |
+
# Formatar para uso em prompt
|
| 229 |
+
return {
|
| 230 |
+
"topic_principal": summary.topic_principal,
|
| 231 |
+
"subtopicas": summary.subtopicas,
|
| 232 |
+
"conversation_path": summary.conversation_path,
|
| 233 |
+
"interaction_pattern": summary.interaction_pattern,
|
| 234 |
+
"unanswered_questions": summary.unanswered_questions[:3] if summary.unanswered_questions else [],
|
| 235 |
+
"assumed_knowledge": summary.assumed_knowledge[:3] if summary.assumed_knowledge else [],
|
| 236 |
+
"context_switches": summary.context_switches,
|
| 237 |
+
}
|
| 238 |
|
| 239 |
def _extract_topic_simple(self, message: str, current_topic: Optional[str]) -> Optional[str]:
|
| 240 |
"""
|
| 241 |
Extrai tópico de forma simples (sem LLM).
|
| 242 |
+
Heurísticas básicas.
|
| 243 |
"""
|
| 244 |
msg_lower = message.lower()
|
| 245 |
|
| 246 |
+
# Detectar palavras-chave comuns
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 247 |
topics_keywords = {
|
| 248 |
+
"saúde": ["doença", "medicina", "cura", "tratamento", "sintoma", "hospital"],
|
| 249 |
+
"técnica": ["código", "python", "função", "erro", "bug", "programação"],
|
| 250 |
+
"relacionamento": ["namoro", "amor", "casal", "relacionamento", "ex"],
|
| 251 |
+
"trabalho": ["emprego", "trabalho", "chefe", "salário", "despedida"],
|
| 252 |
+
"escola": ["escola", "universidade", "prova", "nota", "aula"],
|
| 253 |
+
"esportes": ["futebol", "basquete", "games", "competição", "time"],
|
|
|
|
| 254 |
}
|
| 255 |
|
| 256 |
for topic, keywords in topics_keywords.items():
|
| 257 |
if any(kw in msg_lower for kw in keywords):
|
| 258 |
return topic
|
| 259 |
|
| 260 |
+
# Se tem pergunta, extrai dela
|
| 261 |
if "?" in message:
|
| 262 |
+
# Pega primeira palavra significativa
|
| 263 |
+
words = [w for w in msg_lower.split() if len(w) > 3]
|
| 264 |
+
if words:
|
| 265 |
+
return words[0]
|
|
|
|
| 266 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 267 |
return current_topic
|
| 268 |
|
| 269 |
def _detect_pattern(self, message: str) -> Optional[str]:
|
|
|
|
| 333 |
logger.warning(f"Error loading LSTM from DB: {e}")
|
| 334 |
return None
|
| 335 |
|
| 336 |
+
def _get_from_db_all_speakers(self, context_id: str) -> List[LSTMContextSummary]:
|
| 337 |
+
"""
|
| 338 |
+
Recupera contexto para TODOS os speakers em um contexto de grupo.
|
| 339 |
+
Essencial para rastrear quem falou o quê em grupos.
|
| 340 |
+
"""
|
| 341 |
+
try:
|
| 342 |
+
rows = self.db._execute_with_retry(
|
| 343 |
+
"SELECT * FROM lstm_contexto WHERE context_id = ? ORDER BY last_updated DESC",
|
| 344 |
+
(context_id,)
|
| 345 |
+
)
|
| 346 |
+
|
| 347 |
+
if not rows:
|
| 348 |
+
return []
|
| 349 |
+
|
| 350 |
+
summaries = []
|
| 351 |
+
for row in rows:
|
| 352 |
+
data = dict(row)
|
| 353 |
+
|
| 354 |
+
# Desserializar JSON fields
|
| 355 |
+
if data.get('subtopicas'):
|
| 356 |
+
data['subtopicas'] = json.loads(data['subtopicas'])
|
| 357 |
+
if data.get('conversation_path'):
|
| 358 |
+
data['conversation_path'] = json.loads(data['conversation_path'])
|
| 359 |
+
if data.get('unanswered_questions'):
|
| 360 |
+
data['unanswered_questions'] = json.loads(data['unanswered_questions'])
|
| 361 |
+
if data.get('assumed_knowledge'):
|
| 362 |
+
data['assumed_knowledge'] = json.loads(data['assumed_knowledge'])
|
| 363 |
+
if data.get('contradictions'):
|
| 364 |
+
data['contradictions'] = json.loads(data['contradictions'])
|
| 365 |
+
|
| 366 |
+
# Limpar campos legados
|
| 367 |
+
data.pop('created_at', None)
|
| 368 |
+
data.pop('last_updated', None)
|
| 369 |
+
data.pop('metadata', None)
|
| 370 |
+
data.pop('emotional_state', None)
|
| 371 |
+
data.pop('contexto_geral', None)
|
| 372 |
+
|
| 373 |
+
# Filtro genérico
|
| 374 |
+
import inspect
|
| 375 |
+
valid_keys = inspect.signature(LSTMContextSummary).parameters.keys()
|
| 376 |
+
filtered_data = {k: v for k, v in data.items() if k in valid_keys}
|
| 377 |
+
|
| 378 |
+
summary = LSTMContextSummary(**filtered_data)
|
| 379 |
+
summaries.append(summary)
|
| 380 |
+
|
| 381 |
+
logger.debug(f"✅ Loaded LSTM speakers: context_id={context_id}, {len(summaries)} speakers")
|
| 382 |
+
return summaries
|
| 383 |
+
|
| 384 |
+
except Exception as e:
|
| 385 |
+
logger.warning(f"Error loading LSTM speakers from DB: {e}")
|
| 386 |
+
return []
|
| 387 |
+
|
| 388 |
def _save_to_db(self, summary: LSTMContextSummary) -> None:
|
| 389 |
"""Salva contexto no banco de dados usando Database._execute_with_retry()."""
|
| 390 |
try:
|
modules/lstm_memory_system.py
CHANGED
|
@@ -173,6 +173,10 @@ class LSTMMemorySystem:
|
|
| 173 |
self.processing_queue: List[Dict[str, Any]] = []
|
| 174 |
self.processing_lock = threading.Lock()
|
| 175 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 176 |
# Inicializar tabelas no DB
|
| 177 |
self._initialize_database()
|
| 178 |
|
|
@@ -181,13 +185,13 @@ class LSTMMemorySystem:
|
|
| 181 |
def _initialize_database(self) -> None:
|
| 182 |
"""Cria tabelas necessárias no banco de dados."""
|
| 183 |
try:
|
| 184 |
-
# As tabelas
|
| 185 |
-
#
|
| 186 |
self.db._execute_with_retry("""
|
| 187 |
CREATE TABLE IF NOT EXISTS lstm_contexto (
|
| 188 |
-
context_id
|
| 189 |
-
numero_usuario
|
| 190 |
-
topic_principal
|
| 191 |
subtopicas TEXT,
|
| 192 |
conversation_path TEXT,
|
| 193 |
last_key_message TEXT,
|
|
@@ -206,27 +210,19 @@ class LSTMMemorySystem:
|
|
| 206 |
self.db._execute_with_retry("""
|
| 207 |
CREATE TABLE IF NOT EXISTS lstm_message_links (
|
| 208 |
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 209 |
-
context_id
|
| 210 |
-
message_id
|
| 211 |
-
|
|
|
|
| 212 |
topic_changed BOOLEAN DEFAULT FALSE,
|
| 213 |
-
context_switch_type
|
| 214 |
-
relevance_score
|
| 215 |
-
created_at REAL
|
|
|
|
| 216 |
)
|
| 217 |
""", commit=True)
|
| 218 |
|
| 219 |
-
|
| 220 |
-
CREATE INDEX IF NOT EXISTS idx_lstm_usuario
|
| 221 |
-
ON lstm_contexto(numero_usuario)
|
| 222 |
-
""", commit=True)
|
| 223 |
-
|
| 224 |
-
self.db._execute_with_retry("""
|
| 225 |
-
CREATE INDEX IF NOT EXISTS idx_lstm_links_context
|
| 226 |
-
ON lstm_message_links(context_id)
|
| 227 |
-
""", commit=True)
|
| 228 |
-
|
| 229 |
-
logger.info("✅ Tabelas LSTM verificadas/inicializadas")
|
| 230 |
except Exception as e:
|
| 231 |
logger.error(f"❌ Erro ao inicializar tabelas LSTM: {e}")
|
| 232 |
|
|
@@ -247,6 +243,8 @@ class LSTMMemorySystem:
|
|
| 247 |
Processa mensagem de forma assíncrona para extrair contexto LSTM.
|
| 248 |
Não bloqueia a resposta. Funciona em background thread.
|
| 249 |
|
|
|
|
|
|
|
| 250 |
Args:
|
| 251 |
context_id: ID do contexto (PV ou Grupo)
|
| 252 |
numero_usuario: ID do usuário
|
|
@@ -255,6 +253,28 @@ class LSTMMemorySystem:
|
|
| 255 |
parent_message_id: ID da mensagem anterior (para linked context)
|
| 256 |
llm_client: Client LLM para análise (opcional)
|
| 257 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 258 |
# Adiciona à queue para processamento assíncrono
|
| 259 |
with self.processing_lock:
|
| 260 |
self.processing_queue.append({
|
|
@@ -263,7 +283,7 @@ class LSTMMemorySystem:
|
|
| 263 |
'message': message,
|
| 264 |
'role': role,
|
| 265 |
'parent_message_id': parent_message_id,
|
| 266 |
-
'timestamp':
|
| 267 |
})
|
| 268 |
|
| 269 |
# Dispara thread de processamento se não estiver rodando
|
|
@@ -315,7 +335,7 @@ class LSTMMemorySystem:
|
|
| 315 |
lstm_summary.topic_principal = new_topic
|
| 316 |
|
| 317 |
# Armazenar link entre mensagens
|
| 318 |
-
self._record_context_switch(context_id, parent_message_id, new_topic)
|
| 319 |
|
| 320 |
# ✅ ANÁLISE 3: Adicionar subtópicos
|
| 321 |
subtopics = self._extract_subtopics(message, new_topic)
|
|
@@ -381,21 +401,8 @@ class LSTMMemorySystem:
|
|
| 381 |
'política': ['presidente', 'eleição', 'política', 'governo', 'ministro'],
|
| 382 |
'clima': ['tempo', 'chuva', 'temperatura', 'previsão', 'clima'],
|
| 383 |
'saúde': ['doença', 'médico', 'hospital', 'sintomas', 'saúde'],
|
| 384 |
-
'entretenimento': ['pedro orochi', 'orochinho', 'weedzao', 'mitada', 'rei das mitadas', 'youtuber', 'streamer'],
|
| 385 |
}
|
| 386 |
|
| 387 |
-
# 🛡️ FILTRO ANTI-NOISE: Ignora termos vazios ou clickbaits genéricos
|
| 388 |
-
clickbait_patterns = [
|
| 389 |
-
'veja o que aconteceu', 'não acredito', 'olha isso', 'surpreendente',
|
| 390 |
-
'morreu hoje', 'luto', 'urgente'
|
| 391 |
-
]
|
| 392 |
-
|
| 393 |
-
if any(p in message_lower for p in clickbait_patterns):
|
| 394 |
-
# Se contém clickbait, mas não contém um tópico real forte, ignora
|
| 395 |
-
has_real_topic = any(kw in message_lower for topic, kws in keywords_map.items() for kw in kws)
|
| 396 |
-
if not has_real_topic:
|
| 397 |
-
return None
|
| 398 |
-
|
| 399 |
for topic, keywords in keywords_map.items():
|
| 400 |
if any(kw in message_lower for kw in keywords):
|
| 401 |
return topic
|
|
@@ -403,11 +410,6 @@ class LSTMMemorySystem:
|
|
| 403 |
# Se não detectar via keywords, tenta extrair primeira entidade nomeada
|
| 404 |
# (simplificado - em produção usaria NER)
|
| 405 |
if len(message.split()) >= 3:
|
| 406 |
-
# Pela experiência, temas com "morreu" ou "luto" sem fonte são ruído
|
| 407 |
-
if 'morreu' in message_lower or 'luto' in message_lower:
|
| 408 |
-
if not any(kw in message_lower for kw in ['notícia', 'jornal', 'confirmado']):
|
| 409 |
-
return None
|
| 410 |
-
|
| 411 |
# Pega primeiras 3-4 palavras como possível tema
|
| 412 |
words = message.split()[:4]
|
| 413 |
if all(w[0].isupper() for w in words if w):
|
|
@@ -627,19 +629,24 @@ class LSTMMemorySystem:
|
|
| 627 |
def _record_context_switch(
|
| 628 |
self,
|
| 629 |
context_id: str,
|
|
|
|
| 630 |
parent_message_id: Optional[str],
|
| 631 |
new_topic: str
|
| 632 |
) -> None:
|
| 633 |
"""Registra mudança de contexto/tópico."""
|
| 634 |
try:
|
|
|
|
|
|
|
|
|
|
| 635 |
self.db._execute_with_retry("""
|
| 636 |
INSERT INTO lstm_message_links
|
| 637 |
-
(context_id, message_id, parent_message_id, topic_changed,
|
| 638 |
context_switch_type, created_at)
|
| 639 |
-
VALUES (?, ?, ?, ?, ?, ?)
|
| 640 |
""", (
|
| 641 |
context_id,
|
| 642 |
-
|
|
|
|
| 643 |
parent_message_id,
|
| 644 |
True,
|
| 645 |
'topic_change',
|
|
|
|
| 173 |
self.processing_queue: List[Dict[str, Any]] = []
|
| 174 |
self.processing_lock = threading.Lock()
|
| 175 |
|
| 176 |
+
# ✅ PROTEÇÃO CONTRA DUPLICAÇÃO: Track mensagens processadas recentemente
|
| 177 |
+
self.recently_processed: Dict[str, float] = {} # {hash(context+user+msg): timestamp}
|
| 178 |
+
self.dedup_timeout = 5 # Segundos - evita duplicação em 5s
|
| 179 |
+
|
| 180 |
# Inicializar tabelas no DB
|
| 181 |
self._initialize_database()
|
| 182 |
|
|
|
|
| 185 |
def _initialize_database(self) -> None:
|
| 186 |
"""Cria tabelas necessárias no banco de dados."""
|
| 187 |
try:
|
| 188 |
+
# As tabelas já são criadas pelo database.py _init_db().
|
| 189 |
+
# Aqui apenas garantimos redundância segura com o esquema oficial.
|
| 190 |
self.db._execute_with_retry("""
|
| 191 |
CREATE TABLE IF NOT EXISTS lstm_contexto (
|
| 192 |
+
context_id VARCHAR(255) PRIMARY KEY,
|
| 193 |
+
numero_usuario VARCHAR(50) NOT NULL,
|
| 194 |
+
topic_principal VARCHAR(255),
|
| 195 |
subtopicas TEXT,
|
| 196 |
conversation_path TEXT,
|
| 197 |
last_key_message TEXT,
|
|
|
|
| 210 |
self.db._execute_with_retry("""
|
| 211 |
CREATE TABLE IF NOT EXISTS lstm_message_links (
|
| 212 |
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 213 |
+
context_id VARCHAR(255) NOT NULL,
|
| 214 |
+
message_id VARCHAR(255) NOT NULL,
|
| 215 |
+
numero_usuario VARCHAR(50) NOT NULL,
|
| 216 |
+
parent_message_id VARCHAR(255),
|
| 217 |
topic_changed BOOLEAN DEFAULT FALSE,
|
| 218 |
+
context_switch_type VARCHAR(50),
|
| 219 |
+
relevance_score FLOAT DEFAULT 1.0,
|
| 220 |
+
created_at REAL,
|
| 221 |
+
FOREIGN KEY (context_id) REFERENCES lstm_contexto(context_id) ON DELETE CASCADE
|
| 222 |
)
|
| 223 |
""", commit=True)
|
| 224 |
|
| 225 |
+
logger.info("✅ Tabelas LSTM sincronizadas")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 226 |
except Exception as e:
|
| 227 |
logger.error(f"❌ Erro ao inicializar tabelas LSTM: {e}")
|
| 228 |
|
|
|
|
| 243 |
Processa mensagem de forma assíncrona para extrair contexto LSTM.
|
| 244 |
Não bloqueia a resposta. Funciona em background thread.
|
| 245 |
|
| 246 |
+
✅ Proteção: Evita duplicação em 5 segundos
|
| 247 |
+
|
| 248 |
Args:
|
| 249 |
context_id: ID do contexto (PV ou Grupo)
|
| 250 |
numero_usuario: ID do usuário
|
|
|
|
| 253 |
parent_message_id: ID da mensagem anterior (para linked context)
|
| 254 |
llm_client: Client LLM para análise (opcional)
|
| 255 |
"""
|
| 256 |
+
# ✅ DEDUPLICATION: Verifica se a mensagem já foi processada recentemente
|
| 257 |
+
import hashlib
|
| 258 |
+
if message_id:
|
| 259 |
+
msg_hash = hashlib.md5(f"msgid:{message_id}".encode()).hexdigest()
|
| 260 |
+
else:
|
| 261 |
+
msg_hash = hashlib.md5(f"{context_id}:{numero_usuario}:{message[:100]}".encode()).hexdigest()
|
| 262 |
+
|
| 263 |
+
now = time.time()
|
| 264 |
+
|
| 265 |
+
# Limpa entries expiradas
|
| 266 |
+
expired = [k for k, v in self.recently_processed.items() if now - v > self.dedup_timeout]
|
| 267 |
+
for k in expired:
|
| 268 |
+
del self.recently_processed[k]
|
| 269 |
+
|
| 270 |
+
# Verifica se já foi processada recentemente
|
| 271 |
+
if msg_hash in self.recently_processed:
|
| 272 |
+
logger.debug(f"⚠️ [LSTM DEDUP] Mensagem duplicada ignorada: {message[:50]}...")
|
| 273 |
+
return
|
| 274 |
+
|
| 275 |
+
# Marca como processada
|
| 276 |
+
self.recently_processed[msg_hash] = now
|
| 277 |
+
|
| 278 |
# Adiciona à queue para processamento assíncrono
|
| 279 |
with self.processing_lock:
|
| 280 |
self.processing_queue.append({
|
|
|
|
| 283 |
'message': message,
|
| 284 |
'role': role,
|
| 285 |
'parent_message_id': parent_message_id,
|
| 286 |
+
'timestamp': now
|
| 287 |
})
|
| 288 |
|
| 289 |
# Dispara thread de processamento se não estiver rodando
|
|
|
|
| 335 |
lstm_summary.topic_principal = new_topic
|
| 336 |
|
| 337 |
# Armazenar link entre mensagens
|
| 338 |
+
self._record_context_switch(context_id, numero_usuario, parent_message_id, new_topic)
|
| 339 |
|
| 340 |
# ✅ ANÁLISE 3: Adicionar subtópicos
|
| 341 |
subtopics = self._extract_subtopics(message, new_topic)
|
|
|
|
| 401 |
'política': ['presidente', 'eleição', 'política', 'governo', 'ministro'],
|
| 402 |
'clima': ['tempo', 'chuva', 'temperatura', 'previsão', 'clima'],
|
| 403 |
'saúde': ['doença', 'médico', 'hospital', 'sintomas', 'saúde'],
|
|
|
|
| 404 |
}
|
| 405 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 406 |
for topic, keywords in keywords_map.items():
|
| 407 |
if any(kw in message_lower for kw in keywords):
|
| 408 |
return topic
|
|
|
|
| 410 |
# Se não detectar via keywords, tenta extrair primeira entidade nomeada
|
| 411 |
# (simplificado - em produção usaria NER)
|
| 412 |
if len(message.split()) >= 3:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 413 |
# Pega primeiras 3-4 palavras como possível tema
|
| 414 |
words = message.split()[:4]
|
| 415 |
if all(w[0].isupper() for w in words if w):
|
|
|
|
| 629 |
def _record_context_switch(
|
| 630 |
self,
|
| 631 |
context_id: str,
|
| 632 |
+
numero_usuario: str,
|
| 633 |
parent_message_id: Optional[str],
|
| 634 |
new_topic: str
|
| 635 |
) -> None:
|
| 636 |
"""Registra mudança de contexto/tópico."""
|
| 637 |
try:
|
| 638 |
+
# Gera um ID temporário se não houver
|
| 639 |
+
msg_id = f"switch_{int(time.time())}_{hashlib.md5(new_topic.encode()).hexdigest()[:8]}"
|
| 640 |
+
|
| 641 |
self.db._execute_with_retry("""
|
| 642 |
INSERT INTO lstm_message_links
|
| 643 |
+
(context_id, message_id, numero_usuario, parent_message_id, topic_changed,
|
| 644 |
context_switch_type, created_at)
|
| 645 |
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
| 646 |
""", (
|
| 647 |
context_id,
|
| 648 |
+
msg_id,
|
| 649 |
+
numero_usuario,
|
| 650 |
parent_message_id,
|
| 651 |
True,
|
| 652 |
'topic_change',
|
modules/reply_context_handler.py
CHANGED
|
@@ -1,758 +1,781 @@
|
|
| 1 |
-
# type: ignore
|
| 2 |
-
"""
|
| 3 |
-
================================================================================
|
| 4 |
-
AKIRA V21 ULTIMATE - REPLY CONTEXT HANDLER MODULE
|
| 5 |
-
================================================================================
|
| 6 |
-
Sistema dedicado para processar e priorizar contexto de replies.
|
| 7 |
-
Garante que replies tenham prioridade ligeiramente maior que o contexto geral,
|
| 8 |
-
especialmente em perguntas curtas.
|
| 9 |
-
|
| 10 |
-
Features:
|
| 11 |
-
- Extração e processamento de metadados de reply
|
| 12 |
-
- 3 níveis de prioridade (1=normal, 2=reply, 3=reply-to-bot+pergunta-curta)
|
| 13 |
-
- Construção de prompt sections otimizadas para replies
|
| 14 |
-
- Integração com ShortTermMemory
|
| 15 |
-
- Context hint extraction para melhor compreensão
|
| 16 |
-
================================================================================
|
| 17 |
-
"""
|
| 18 |
-
|
| 19 |
-
import os
|
| 20 |
-
import sys
|
| 21 |
-
import time
|
| 22 |
-
import json
|
| 23 |
-
import re
|
| 24 |
-
import logging
|
| 25 |
-
from typing import Optional, Dict, Any, List, Tuple
|
| 26 |
-
from dataclasses import dataclass, field
|
| 27 |
-
|
| 28 |
-
# Imports robustos com fallback - CORRIGIDO para usar modules.
|
| 29 |
-
try:
|
| 30 |
-
from . import config
|
| 31 |
-
from .short_term_memory import ShortTermMemory, MessageWithContext, IMPORTANCIA_REPLY, IMPORTANCIA_REPLY_TO_BOT, IMPORTANCIA_PERGUNTA_CURTA_REPLY
|
| 32 |
-
REPLY_HANDLER_AVAILABLE = True
|
| 33 |
-
except ImportError:
|
| 34 |
-
try:
|
| 35 |
-
import modules.config as config
|
| 36 |
-
from modules.short_term_memory import ShortTermMemory, MessageWithContext, IMPORTANCIA_REPLY, IMPORTANCIA_REPLY_TO_BOT, IMPORTANCIA_PERGUNTA_CURTA_REPLY
|
| 37 |
-
REPLY_HANDLER_AVAILABLE = True
|
| 38 |
-
except ImportError:
|
| 39 |
-
try:
|
| 40 |
-
from short_term_memory import ShortTermMemory, MessageWithContext, IMPORTANCIA_REPLY, IMPORTANCIA_REPLY_TO_BOT, IMPORTANCIA_PERGUNTA_CURTA_REPLY
|
| 41 |
-
REPLY_HANDLER_AVAILABLE = True
|
| 42 |
-
except ImportError:
|
| 43 |
-
REPLY_HANDLER_AVAILABLE = False
|
| 44 |
-
config = None
|
| 45 |
-
|
| 46 |
-
logger = logging.getLogger(__name__)
|
| 47 |
-
|
| 48 |
-
# ============================================================
|
| 49 |
-
# NÍVEIS DE PRIORIDADE
|
| 50 |
-
# ============================================================
|
| 51 |
-
|
| 52 |
-
PRIORITY_NORMAL = 1
|
| 53 |
-
PRIORITY_REPLY = 2
|
| 54 |
-
PRIORITY_REPLY_TO_BOT = 3
|
| 55 |
-
PRIORITY_REPLY_TO_BOT_SHORT_QUESTION = 4 # Prioridade máxima!
|
| 56 |
-
|
| 57 |
-
# Limite de palavras para "pergunta curta"
|
| 58 |
-
PERGUNTA_CURTA_LIMITE: int = 5
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
@dataclass
|
| 62 |
-
class ProcessedReplyContext:
|
| 63 |
-
"""
|
| 64 |
-
Contexto de reply processado e pronto para uso.
|
| 65 |
-
|
| 66 |
-
Attributes:
|
| 67 |
-
is_reply: Se é um reply
|
| 68 |
-
reply_to_bot: Se é reply direcionado ao bot
|
| 69 |
-
priority_level: Nível de prioridade (1-4)
|
| 70 |
-
quoted_author_name: Nome do autor da mensagem citada
|
| 71 |
-
quoted_author_numero: Número do autor
|
| 72 |
-
quoted_text_original: Texto original citado
|
| 73 |
-
mensagem_citada: Texto da mensagem citada
|
| 74 |
-
context_hint: Hint de contexto extraído
|
| 75 |
-
importancia: Peso de importância calculado
|
| 76 |
-
prompt_section: Section formatada para o prompt
|
| 77 |
-
should_prioritize_reply: Se deve priorizar no prompt
|
| 78 |
-
adaptive_multiplier: Multiplicador adaptativo baseado no tamanho
|
| 79 |
-
"""
|
| 80 |
-
is_reply: bool = False
|
| 81 |
-
reply_to_bot: bool = False
|
| 82 |
-
priority_level: int = PRIORITY_NORMAL
|
| 83 |
-
quoted_author_name: str = ""
|
| 84 |
-
quoted_author_numero: str = ""
|
| 85 |
-
quoted_text_original: str = ""
|
| 86 |
-
mensagem_citada: str = ""
|
| 87 |
-
context_hint: str = ""
|
| 88 |
-
importancia: float = 1.0
|
| 89 |
-
prompt_section: str = ""
|
| 90 |
-
should_prioritize_reply: bool = False
|
| 91 |
-
adaptive_multiplier: float = 1.0
|
| 92 |
-
|
| 93 |
-
def to_dict(self) -> Dict[str, Any]:
|
| 94 |
-
"""Converte para dicionário."""
|
| 95 |
-
return {
|
| 96 |
-
"is_reply": self.is_reply,
|
| 97 |
-
"reply_to_bot": self.reply_to_bot,
|
| 98 |
-
"priority_level": self.priority_level,
|
| 99 |
-
"quoted_author_name": self.quoted_author_name,
|
| 100 |
-
"quoted_author_numero": self.quoted_author_numero,
|
| 101 |
-
"quoted_text_original": self.quoted_text_original,
|
| 102 |
-
"mensagem_citada": self.mensagem_citada,
|
| 103 |
-
"context_hint": self.context_hint,
|
| 104 |
-
"importancia": self.importancia,
|
| 105 |
-
"prompt_section": self.prompt_section,
|
| 106 |
-
"should_prioritize_reply": self.should_prioritize_reply,
|
| 107 |
-
"adaptive_multiplier": self.adaptive_multiplier
|
| 108 |
-
}
|
| 109 |
-
|
| 110 |
-
@classmethod
|
| 111 |
-
def from_dict(cls, data: Dict[str, Any]) -> 'ProcessedReplyContext':
|
| 112 |
-
"""Cria instância a partir de dicionário."""
|
| 113 |
-
return cls(**{k: v for k, v in data.items() if k in cls.__dataclass_fields__})
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
# ============================================================
|
| 117 |
-
# FUNÇÕES AUXILIARES
|
| 118 |
-
# ============================================================
|
| 119 |
-
|
| 120 |
-
def contar_palavras(texto: str) -> int:
|
| 121 |
-
"""Conta palavras em um texto."""
|
| 122 |
-
if not texto:
|
| 123 |
-
return 0
|
| 124 |
-
return len(texto.split())
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
def is_pergunta_curta(texto: str) -> bool:
|
| 128 |
-
"""
|
| 129 |
-
Verifica se o texto é uma pergunta curta.
|
| 130 |
-
|
| 131 |
-
Args:
|
| 132 |
-
texto: Texto a verificar
|
| 133 |
-
|
| 134 |
-
Returns:
|
| 135 |
-
True se for pergunta com pocas palavras
|
| 136 |
-
"""
|
| 137 |
-
if not texto:
|
| 138 |
-
return False
|
| 139 |
-
|
| 140 |
-
texto_lower = texto.strip().lower()
|
| 141 |
-
word_count = contar_palavras(texto)
|
| 142 |
-
|
| 143 |
-
# Deve ter marcador de pergunta ou palavras interrogativas
|
| 144 |
-
has_question_marker = '?' in texto
|
| 145 |
-
has_interrogative = any(w in texto_lower for w in [
|
| 146 |
-
'qual', 'quais', 'quem', 'como', 'onde', 'quando', 'por que',
|
| 147 |
-
'porque', 'para que', 'o que', 'que', 'é o que', 'vc', 'você',
|
| 148 |
-
'tu', 'meu', 'minha', 'oq', 'oq', 'n'
|
| 149 |
-
])
|
| 150 |
-
|
| 151 |
-
return word_count <= PERGUNTA_CURTA_LIMITE and (has_question_marker or has_interrogative)
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
def is_mensagem_vazia_ou_reconhecimento(texto: str) -> bool:
|
| 155 |
-
"""
|
| 156 |
-
Verifica se a mensagem é apenas um sinal de pontuação ou texto muito curto/vazio.
|
| 157 |
-
Ajuda a evitar a alucinação de self-reply (onde o bot conversa consigo mesmo).
|
| 158 |
-
"""
|
| 159 |
-
if not texto:
|
| 160 |
-
return True
|
| 161 |
-
|
| 162 |
-
clean_text = texto.strip()
|
| 163 |
-
|
| 164 |
-
# Se for apenas 1-2 caracteres não-alfanuméricos (ex: ".", "..", "!")
|
| 165 |
-
import re
|
| 166 |
-
if len(clean_text) <= 2 and not re.search(r'[a-zA-Z0-9]', clean_text):
|
| 167 |
-
return True
|
| 168 |
-
|
| 169 |
-
# Palavras muito curtas e fechadas que soam como reconhecimento e não têm substância
|
| 170 |
-
if clean_text.lower() in [".", "vc", "ah", "ok", "hm", "ta"]:
|
| 171 |
-
return True
|
| 172 |
-
|
| 173 |
-
return False
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
def extrair_context_hint(quoted_text: str, mensagem_atual: str) -> str:
|
| 177 |
-
"""
|
| 178 |
-
Extrai hint de contexto baseado no texto citado e mensagem atual.
|
| 179 |
-
|
| 180 |
-
Args:
|
| 181 |
-
quoted_text: Texto original citado
|
| 182 |
-
mensagem_atual: Mensagem atual do usuário
|
| 183 |
-
|
| 184 |
-
Returns:
|
| 185 |
-
String de hint de contexto
|
| 186 |
-
"""
|
| 187 |
-
hints = []
|
| 188 |
-
|
| 189 |
-
# Detecta tipo de reply
|
| 190 |
-
quoted_lower = quoted_text.lower() if quoted_text else ""
|
| 191 |
-
|
| 192 |
-
# Pergunta sobre o bot
|
| 193 |
-
if any(w in quoted_lower for w in ['akira', 'bot', 'você', 'vc', 'tu']):
|
| 194 |
-
hints.append("pergunta_sobre_akira")
|
| 195 |
-
|
| 196 |
-
# Pergunta factual
|
| 197 |
-
if any(w in quoted_lower for w in ['oq', 'o que', 'qual', 'quanto', 'onde', 'quando']):
|
| 198 |
-
hints.append("pergunta_factual")
|
| 199 |
-
|
| 200 |
-
# Ironia/deboche detectado
|
| 201 |
-
if any(w in quoted_lower for w in ['kkk', 'haha', '😂', '🤣', 'eita']):
|
| 202 |
-
hints.append("tom_irreverente")
|
| 203 |
-
|
| 204 |
-
# Expressão de opinião
|
| 205 |
-
if any(w in quoted_lower for w in ['acho', 'penso', 'creio', 'imagino']):
|
| 206 |
-
hints.append("expressao_opiniao")
|
| 207 |
-
|
| 208 |
-
return " | ".join(hints) if hints else "contexto_geral"
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
def calcular_prioridade(
|
| 212 |
-
is_reply: bool,
|
| 213 |
-
reply_to_bot: bool,
|
| 214 |
-
mensagem: str,
|
| 215 |
-
quoted_text: str = ""
|
| 216 |
-
) -> Tuple[int, float]:
|
| 217 |
-
"""
|
| 218 |
-
Calcula nível de prioridade e importância.
|
| 219 |
-
|
| 220 |
-
Args:
|
| 221 |
-
is_reply: Se é um reply
|
| 222 |
-
reply_to_bot: Se é reply para o bot
|
| 223 |
-
mensagem: Mensagem atual
|
| 224 |
-
quoted_text: Texto citado
|
| 225 |
-
|
| 226 |
-
Returns:
|
| 227 |
-
Tupla (priority_level, importancia)
|
| 228 |
-
"""
|
| 229 |
-
if not is_reply:
|
| 230 |
-
return PRIORITY_NORMAL, 1.0
|
| 231 |
-
|
| 232 |
-
# Reply para o bot
|
| 233 |
-
if reply_to_bot:
|
| 234 |
-
# Pergunta curta = prioridade máxima
|
| 235 |
-
if is_pergunta_curta(mensagem):
|
| 236 |
-
return PRIORITY_REPLY_TO_BOT_SHORT_QUESTION, IMPORTANCIA_PERGUNTA_CURTA_REPLY
|
| 237 |
-
# Reply normal ao bot
|
| 238 |
-
return PRIORITY_REPLY_TO_BOT, IMPORTANCIA_REPLY_TO_BOT
|
| 239 |
-
|
| 240 |
-
# Reply para outro usuário
|
| 241 |
-
return PRIORITY_REPLY, IMPORTANCIA_REPLY
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
# ============================================================
|
| 245 |
-
# CLASSE PRINCIPAL
|
| 246 |
-
# ============================================================
|
| 247 |
-
|
| 248 |
-
class ReplyContextHandler:
|
| 249 |
-
"""
|
| 250 |
-
Handler dedicado para processar e priorizar contexto de replies.
|
| 251 |
-
|
| 252 |
-
Funcionalidades:
|
| 253 |
-
- Extração de metadados de reply do payload
|
| 254 |
-
- Cálculo automático de prioridade
|
| 255 |
-
- Construção de seções de prompt otimizadas
|
| 256 |
-
- Integração com ShortTermMemory
|
| 257 |
-
- Ajuste adaptativo baseado em tamanho da pergunta
|
| 258 |
-
"""
|
| 259 |
-
|
| 260 |
-
def __init__(self, short_term_memory: Optional[ShortTermMemory] = None):
|
| 261 |
-
"""
|
| 262 |
-
Inicializa o handler.
|
| 263 |
-
|
| 264 |
-
Args:
|
| 265 |
-
short_term_memory: Instância de ShortTermMemory (opcional)
|
| 266 |
-
"""
|
| 267 |
-
self.short_term_memory = short_term_memory
|
| 268 |
-
self.lstm_extension = None # Será inicializado depois se DB disponível
|
| 269 |
-
logger.debug("✅ ReplyContextHandler inicializado")
|
| 270 |
-
|
| 271 |
-
def enable_lstm(self, lstm_ext: Any) -> None:
|
| 272 |
-
"""Habilita LSTM extension."""
|
| 273 |
-
self.lstm_extension = lstm_ext
|
| 274 |
-
logger.debug("✅ LSTM enabled em ReplyContextHandler")
|
| 275 |
-
|
| 276 |
-
def process_reply(
|
| 277 |
-
self,
|
| 278 |
-
mensagem: str,
|
| 279 |
-
reply_metadata: Dict[str, Any],
|
| 280 |
-
historico_geral: Optional[List[Dict[str, Any]]] = None
|
| 281 |
-
) -> ProcessedReplyContext:
|
| 282 |
-
"""
|
| 283 |
-
Processa metadados de reply e gera contexto processado.
|
| 284 |
-
|
| 285 |
-
Args:
|
| 286 |
-
mensagem: Mensagem atual do usuário
|
| 287 |
-
reply_metadata: Metadados do reply do payload
|
| 288 |
-
historico_geral: Histórico geral (opcional)
|
| 289 |
-
|
| 290 |
-
Returns:
|
| 291 |
-
ProcessedReplyContext pronto para uso
|
| 292 |
-
"""
|
| 293 |
-
# Extrai dados do metadata
|
| 294 |
-
is_reply = reply_metadata.get('is_reply', False)
|
| 295 |
-
reply_to_bot = reply_metadata.get('reply_to_bot', False)
|
| 296 |
-
quoted_author_name = reply_metadata.get('quoted_author_name', '')
|
| 297 |
-
quoted_author_numero = reply_metadata.get('quoted_author_numero', '')
|
| 298 |
-
quoted_text_original = reply_metadata.get('quoted_text_original', '')
|
| 299 |
-
mensagem_citada = reply_metadata.get('mensagem_citada', '') or quoted_text_original
|
| 300 |
-
|
| 301 |
-
# 🔧 CRITICAL FIX: Validate that quoted author is NOT the bot itself
|
| 302 |
-
# Extract pure number from lid_XXXXX format if present
|
| 303 |
-
def extract_pure_number(id_str: str) -> str:
|
| 304 |
-
"""Extrai número puro de formatos como 'lid_123456' ou '123456'"""
|
| 305 |
-
if not id_str:
|
| 306 |
-
return ''
|
| 307 |
-
# Remove 'lid_' prefix if present
|
| 308 |
-
if isinstance(id_str, str) and id_str.startswith('lid_'):
|
| 309 |
-
return id_str[4:]
|
| 310 |
-
return str(id_str) if id_str else ''
|
| 311 |
-
|
| 312 |
-
# ⚠️ SELF-REPLY RECOGNITION
|
| 313 |
-
# Check if the quoted author is the bot itself
|
| 314 |
-
quoted_author_pure = extract_pure_number(quoted_author_numero)
|
| 315 |
-
bot_id_pure = extract_pure_number(config.BOT_NUMERO if hasattr(config, 'BOT_NUMERO') else '37839265886398')
|
| 316 |
-
|
| 317 |
-
is_quoted_from_bot = (quoted_author_pure and quoted_author_pure == bot_id_pure)
|
| 318 |
-
|
| 319 |
-
if is_quoted_from_bot and is_reply:
|
| 320 |
-
logger.info(f"🔄 [REPLY AO BOT] Usuário está respondendo a uma mensagem da Akira ({quoted_author_pure}).")
|
| 321 |
-
reply_to_bot = True
|
| 322 |
-
quoted_author_name = "Akira (você mesmo)"
|
| 323 |
-
quoted_author_numero = config.BOT_NUMERO
|
| 324 |
-
|
| 325 |
-
# 🔧 CORREÇÃO FORÇADA: Se o payload já determinou que é reply_to_bot,
|
| 326 |
-
# ignora qualquer nome/número que tenha vindo e força para o bot.
|
| 327 |
-
if is_reply and reply_to_bot:
|
| 328 |
-
quoted_author_name = "Akira (você mesmo)"
|
| 329 |
-
quoted_author_numero = config.BOT_NUMERO
|
| 330 |
-
|
| 331 |
-
# 🔧 CORREÇÃO: Se autor é desconhecido e não é reply_to_bot explícito, tenta detectar pelo contexto
|
| 332 |
-
elif not quoted_author_name or quoted_author_name.lower() in ['desconhecido', 'unknown', '']:
|
| 333 |
-
# Detecta pelo conteúdo da mensagem citada
|
| 334 |
-
quoted_lower = quoted_text_original.lower() if quoted_text_original else ""
|
| 335 |
-
|
| 336 |
-
# Se a mensagem citada contém padrões de resposta do bot
|
| 337 |
-
bot_patterns = ['akira:', 'eu sou', 'eu sou a akira', 'sou um bot', 'oi!', 'eae!']
|
| 338 |
-
if any(p in quoted_lower for p in bot_patterns):
|
| 339 |
-
quoted_author_name = "Akira (você mesmo)"
|
| 340 |
-
quoted_author_numero = config.BOT_NUMERO
|
| 341 |
-
reply_to_bot = True
|
| 342 |
-
elif mensagem_citada:
|
| 343 |
-
# Se há histórico, busca última mensagem
|
| 344 |
-
if historico_geral:
|
| 345 |
-
# Assumir que é reply para a última mensagem do bot
|
| 346 |
-
quoted_author_name = "mensagem_anterior"
|
| 347 |
-
quoted_author_numero = "unknown"
|
| 348 |
-
|
| 349 |
-
# Se ainda não tem autor mas tem mensagem citada e é reply
|
| 350 |
-
if is_reply and (not quoted_author_name or quoted_author_name == 'desconhecido'):
|
| 351 |
-
# Se é reply_to_bot=True mas autor desconhecido, assume que é reply para o bot
|
| 352 |
-
if reply_to_bot:
|
| 353 |
-
quoted_author_name = "Akira (você mesmo)"
|
| 354 |
-
quoted_author_numero = "BOT"
|
| 355 |
-
else:
|
| 356 |
-
# Tenta extrair do conteúdo
|
| 357 |
-
quoted_author_name = "participante_desconhecido"
|
| 358 |
-
|
| 359 |
-
# Calcula prioridade e importância
|
| 360 |
-
priority_level, importancia = calcular_prioridade(
|
| 361 |
-
is_reply=is_reply,
|
| 362 |
-
reply_to_bot=reply_to_bot,
|
| 363 |
-
mensagem=mensagem,
|
| 364 |
-
quoted_text=quoted_text_original
|
| 365 |
-
)
|
| 366 |
-
|
| 367 |
-
# Extrai context hint
|
| 368 |
-
context_hint = extrair_context_hint(quoted_text_original, mensagem)
|
| 369 |
-
|
| 370 |
-
# Calcula multiplicador adaptativo
|
| 371 |
-
adaptive_multiplier = self._calculate_adaptive_multiplier(
|
| 372 |
-
mensagem=mensagem,
|
| 373 |
-
is_reply=is_reply,
|
| 374 |
-
priority_level=priority_level
|
| 375 |
-
)
|
| 376 |
-
|
| 377 |
-
# Determina se deve priorizar no prompt
|
| 378 |
-
should_prioritize = is_reply and priority_level >= PRIORITY_REPLY
|
| 379 |
-
|
| 380 |
-
# Constrói section do prompt
|
| 381 |
-
prompt_section = self._build_reply_prompt_section(
|
| 382 |
-
mensagem=mensagem,
|
| 383 |
-
mensagem_citada=mensagem_citada,
|
| 384 |
-
quoted_author_name=quoted_author_name,
|
| 385 |
-
reply_to_bot=reply_to_bot,
|
| 386 |
-
context_hint=context_hint,
|
| 387 |
-
priority_level=priority_level
|
| 388 |
-
)
|
| 389 |
-
|
| 390 |
-
# Cria contexto processado
|
| 391 |
-
reply_context = ProcessedReplyContext(
|
| 392 |
-
is_reply=is_reply,
|
| 393 |
-
reply_to_bot=reply_to_bot,
|
| 394 |
-
priority_level=priority_level,
|
| 395 |
-
quoted_author_name=quoted_author_name,
|
| 396 |
-
quoted_author_numero=quoted_author_numero,
|
| 397 |
-
quoted_text_original=quoted_text_original,
|
| 398 |
-
mensagem_citada=mensagem_citada,
|
| 399 |
-
context_hint=context_hint,
|
| 400 |
-
importancia=importancia * adaptive_multiplier,
|
| 401 |
-
prompt_section=prompt_section,
|
| 402 |
-
should_prioritize_reply=should_prioritize,
|
| 403 |
-
adaptive_multiplier=adaptive_multiplier
|
| 404 |
-
)
|
| 405 |
-
|
| 406 |
-
# Adiciona à memória de curto prazo se disponível
|
| 407 |
-
if self.short_term_memory and is_reply:
|
| 408 |
-
self.short_term_memory.add_message(
|
| 409 |
-
role="user",
|
| 410 |
-
content=mensagem,
|
| 411 |
-
importancia=reply_context.importancia,
|
| 412 |
-
reply_info={
|
| 413 |
-
"is_reply": True,
|
| 414 |
-
"reply_to_bot": reply_to_bot,
|
| 415 |
-
"quoted_text_original": quoted_text_original,
|
| 416 |
-
"priority_level": priority_level
|
| 417 |
-
}
|
| 418 |
-
)
|
| 419 |
-
|
| 420 |
-
return reply_context
|
| 421 |
-
|
| 422 |
-
def _calculate_adaptive_multiplier(
|
| 423 |
-
self,
|
| 424 |
-
mensagem: str,
|
| 425 |
-
is_reply: bool,
|
| 426 |
-
priority_level: int
|
| 427 |
-
) -> float:
|
| 428 |
-
"""
|
| 429 |
-
Calcula multiplicador adaptativo baseado no tamanho da pergunta.
|
| 430 |
-
|
| 431 |
-
Para perguntas curtas com reply, aumenta a importância do contexto do reply
|
| 432 |
-
para garantir que o LLM tenha contexto suficiente.
|
| 433 |
-
|
| 434 |
-
Args:
|
| 435 |
-
mensagem: Mensagem atual
|
| 436 |
-
is_reply: Se é reply
|
| 437 |
-
priority_level: Nível de prioridade
|
| 438 |
-
|
| 439 |
-
Returns:
|
| 440 |
-
Multiplicador entre 1.0 e 2.0
|
| 441 |
-
"""
|
| 442 |
-
if not is_reply:
|
| 443 |
-
return 1.0
|
| 444 |
-
|
| 445 |
-
word_count = contar_palavras(mensagem)
|
| 446 |
-
|
| 447 |
-
# Pergunta muito curta (< 3 palavras) = contexto crítico
|
| 448 |
-
if word_count <= 2:
|
| 449 |
-
# Proteção contra alucinação
|
| 450 |
-
if is_mensagem_vazia_ou_reconhecimento(mensagem):
|
| 451 |
-
return 0.5 # Reduz a importância para o bot focar menos no contexto citado
|
| 452 |
-
return 1.5
|
| 453 |
-
|
| 454 |
-
# Pergunta curta (3-5 palavras) = contexto importante
|
| 455 |
-
if word_count <= PERGUNTA_CURTA_LIMITE:
|
| 456 |
-
return 1.3
|
| 457 |
-
|
| 458 |
-
# Pergunta normal = multiplicador padrão baseado em prioridade
|
| 459 |
-
if priority_level == PRIORITY_REPLY_TO_BOT_SHORT_QUESTION:
|
| 460 |
-
return 1.2
|
| 461 |
-
elif priority_level == PRIORITY_REPLY_TO_BOT:
|
| 462 |
-
return 1.1
|
| 463 |
-
|
| 464 |
-
return 1.0
|
| 465 |
-
|
| 466 |
-
def _build_reply_prompt_section(
|
| 467 |
-
self,
|
| 468 |
-
mensagem: str,
|
| 469 |
-
mensagem_citada: str,
|
| 470 |
-
quoted_author_name: str,
|
| 471 |
-
reply_to_bot: bool,
|
| 472 |
-
context_hint: str,
|
| 473 |
-
priority_level: int
|
| 474 |
-
) -> str:
|
| 475 |
-
"""
|
| 476 |
-
Constrói seção formatada do prompt para replies.
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
|
| 498 |
-
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
|
| 526 |
-
|
| 527 |
-
|
| 528 |
-
|
| 529 |
-
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
|
| 533 |
-
|
| 534 |
-
|
| 535 |
-
|
| 536 |
-
|
| 537 |
-
|
| 538 |
-
|
| 539 |
-
|
| 540 |
-
|
| 541 |
-
|
| 542 |
-
|
| 543 |
-
|
| 544 |
-
"""
|
| 545 |
-
|
| 546 |
-
|
| 547 |
-
|
| 548 |
-
|
| 549 |
-
|
| 550 |
-
|
| 551 |
-
|
| 552 |
-
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
|
| 556 |
-
|
| 557 |
-
|
| 558 |
-
|
| 559 |
-
|
| 560 |
-
|
| 561 |
-
|
| 562 |
-
|
| 563 |
-
|
| 564 |
-
|
| 565 |
-
|
| 566 |
-
|
| 567 |
-
|
| 568 |
-
|
| 569 |
-
|
| 570 |
-
|
| 571 |
-
|
| 572 |
-
|
| 573 |
-
|
| 574 |
-
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
|
| 578 |
-
|
| 579 |
-
|
| 580 |
-
|
| 581 |
-
|
| 582 |
-
|
| 583 |
-
|
| 584 |
-
|
| 585 |
-
|
| 586 |
-
|
| 587 |
-
|
| 588 |
-
|
| 589 |
-
|
| 590 |
-
|
| 591 |
-
|
| 592 |
-
|
| 593 |
-
|
| 594 |
-
|
| 595 |
-
|
| 596 |
-
|
| 597 |
-
|
| 598 |
-
|
| 599 |
-
|
| 600 |
-
|
| 601 |
-
|
| 602 |
-
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
|
| 606 |
-
|
| 607 |
-
|
| 608 |
-
|
| 609 |
-
|
| 610 |
-
|
| 611 |
-
|
| 612 |
-
|
| 613 |
-
|
| 614 |
-
|
| 615 |
-
|
| 616 |
-
|
| 617 |
-
|
| 618 |
-
|
| 619 |
-
|
| 620 |
-
|
| 621 |
-
|
| 622 |
-
|
| 623 |
-
|
| 624 |
-
|
| 625 |
-
|
| 626 |
-
|
| 627 |
-
|
| 628 |
-
|
| 629 |
-
|
| 630 |
-
|
| 631 |
-
|
| 632 |
-
|
| 633 |
-
|
| 634 |
-
|
| 635 |
-
|
| 636 |
-
|
| 637 |
-
|
| 638 |
-
|
| 639 |
-
|
| 640 |
-
|
| 641 |
-
|
| 642 |
-
|
| 643 |
-
|
| 644 |
-
|
| 645 |
-
|
| 646 |
-
|
| 647 |
-
|
| 648 |
-
|
| 649 |
-
|
| 650 |
-
|
| 651 |
-
|
| 652 |
-
|
| 653 |
-
|
| 654 |
-
|
| 655 |
-
|
| 656 |
-
|
| 657 |
-
|
| 658 |
-
|
| 659 |
-
|
| 660 |
-
|
| 661 |
-
|
| 662 |
-
|
| 663 |
-
|
| 664 |
-
|
| 665 |
-
|
| 666 |
-
|
| 667 |
-
|
| 668 |
-
|
| 669 |
-
|
| 670 |
-
|
| 671 |
-
|
| 672 |
-
|
| 673 |
-
|
| 674 |
-
|
| 675 |
-
|
| 676 |
-
|
| 677 |
-
|
| 678 |
-
|
| 679 |
-
|
| 680 |
-
|
| 681 |
-
|
| 682 |
-
|
| 683 |
-
|
| 684 |
-
|
| 685 |
-
|
| 686 |
-
|
| 687 |
-
|
| 688 |
-
|
| 689 |
-
|
| 690 |
-
|
| 691 |
-
|
| 692 |
-
|
| 693 |
-
|
| 694 |
-
|
| 695 |
-
|
| 696 |
-
|
| 697 |
-
|
| 698 |
-
|
| 699 |
-
|
| 700 |
-
|
| 701 |
-
|
| 702 |
-
|
| 703 |
-
|
| 704 |
-
|
| 705 |
-
|
| 706 |
-
|
| 707 |
-
|
| 708 |
-
|
| 709 |
-
|
| 710 |
-
|
| 711 |
-
|
| 712 |
-
|
| 713 |
-
|
| 714 |
-
|
| 715 |
-
|
| 716 |
-
|
| 717 |
-
|
| 718 |
-
|
| 719 |
-
|
| 720 |
-
|
| 721 |
-
|
| 722 |
-
|
| 723 |
-
|
| 724 |
-
|
| 725 |
-
|
| 726 |
-
|
| 727 |
-
|
| 728 |
-
|
| 729 |
-
|
| 730 |
-
|
| 731 |
-
|
| 732 |
-
|
| 733 |
-
|
| 734 |
-
|
| 735 |
-
|
| 736 |
-
|
| 737 |
-
|
| 738 |
-
|
| 739 |
-
|
| 740 |
-
|
| 741 |
-
|
| 742 |
-
|
| 743 |
-
|
| 744 |
-
|
| 745 |
-
|
| 746 |
-
|
| 747 |
-
|
| 748 |
-
|
| 749 |
-
"""
|
| 750 |
-
|
| 751 |
-
|
| 752 |
-
|
| 753 |
-
|
| 754 |
-
|
| 755 |
-
|
| 756 |
-
|
| 757 |
-
|
| 758 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# type: ignore
|
| 2 |
+
"""
|
| 3 |
+
================================================================================
|
| 4 |
+
AKIRA V21 ULTIMATE - REPLY CONTEXT HANDLER MODULE
|
| 5 |
+
================================================================================
|
| 6 |
+
Sistema dedicado para processar e priorizar contexto de replies.
|
| 7 |
+
Garante que replies tenham prioridade ligeiramente maior que o contexto geral,
|
| 8 |
+
especialmente em perguntas curtas.
|
| 9 |
+
|
| 10 |
+
Features:
|
| 11 |
+
- Extração e processamento de metadados de reply
|
| 12 |
+
- 3 níveis de prioridade (1=normal, 2=reply, 3=reply-to-bot+pergunta-curta)
|
| 13 |
+
- Construção de prompt sections otimizadas para replies
|
| 14 |
+
- Integração com ShortTermMemory
|
| 15 |
+
- Context hint extraction para melhor compreensão
|
| 16 |
+
================================================================================
|
| 17 |
+
"""
|
| 18 |
+
|
| 19 |
+
import os
|
| 20 |
+
import sys
|
| 21 |
+
import time
|
| 22 |
+
import json
|
| 23 |
+
import re
|
| 24 |
+
import logging
|
| 25 |
+
from typing import Optional, Dict, Any, List, Tuple
|
| 26 |
+
from dataclasses import dataclass, field
|
| 27 |
+
|
| 28 |
+
# Imports robustos com fallback - CORRIGIDO para usar modules.
|
| 29 |
+
try:
|
| 30 |
+
from . import config
|
| 31 |
+
from .short_term_memory import ShortTermMemory, MessageWithContext, IMPORTANCIA_REPLY, IMPORTANCIA_REPLY_TO_BOT, IMPORTANCIA_PERGUNTA_CURTA_REPLY
|
| 32 |
+
REPLY_HANDLER_AVAILABLE = True
|
| 33 |
+
except ImportError:
|
| 34 |
+
try:
|
| 35 |
+
import modules.config as config
|
| 36 |
+
from modules.short_term_memory import ShortTermMemory, MessageWithContext, IMPORTANCIA_REPLY, IMPORTANCIA_REPLY_TO_BOT, IMPORTANCIA_PERGUNTA_CURTA_REPLY
|
| 37 |
+
REPLY_HANDLER_AVAILABLE = True
|
| 38 |
+
except ImportError:
|
| 39 |
+
try:
|
| 40 |
+
from short_term_memory import ShortTermMemory, MessageWithContext, IMPORTANCIA_REPLY, IMPORTANCIA_REPLY_TO_BOT, IMPORTANCIA_PERGUNTA_CURTA_REPLY
|
| 41 |
+
REPLY_HANDLER_AVAILABLE = True
|
| 42 |
+
except ImportError:
|
| 43 |
+
REPLY_HANDLER_AVAILABLE = False
|
| 44 |
+
config = None
|
| 45 |
+
|
| 46 |
+
logger = logging.getLogger(__name__)
|
| 47 |
+
|
| 48 |
+
# ============================================================
|
| 49 |
+
# NÍVEIS DE PRIORIDADE
|
| 50 |
+
# ============================================================
|
| 51 |
+
|
| 52 |
+
PRIORITY_NORMAL = 1
|
| 53 |
+
PRIORITY_REPLY = 2
|
| 54 |
+
PRIORITY_REPLY_TO_BOT = 3
|
| 55 |
+
PRIORITY_REPLY_TO_BOT_SHORT_QUESTION = 4 # Prioridade máxima!
|
| 56 |
+
|
| 57 |
+
# Limite de palavras para "pergunta curta"
|
| 58 |
+
PERGUNTA_CURTA_LIMITE: int = 5
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
@dataclass
|
| 62 |
+
class ProcessedReplyContext:
|
| 63 |
+
"""
|
| 64 |
+
Contexto de reply processado e pronto para uso.
|
| 65 |
+
|
| 66 |
+
Attributes:
|
| 67 |
+
is_reply: Se é um reply
|
| 68 |
+
reply_to_bot: Se é reply direcionado ao bot
|
| 69 |
+
priority_level: Nível de prioridade (1-4)
|
| 70 |
+
quoted_author_name: Nome do autor da mensagem citada
|
| 71 |
+
quoted_author_numero: Número do autor
|
| 72 |
+
quoted_text_original: Texto original citado
|
| 73 |
+
mensagem_citada: Texto da mensagem citada
|
| 74 |
+
context_hint: Hint de contexto extraído
|
| 75 |
+
importancia: Peso de importância calculado
|
| 76 |
+
prompt_section: Section formatada para o prompt
|
| 77 |
+
should_prioritize_reply: Se deve priorizar no prompt
|
| 78 |
+
adaptive_multiplier: Multiplicador adaptativo baseado no tamanho
|
| 79 |
+
"""
|
| 80 |
+
is_reply: bool = False
|
| 81 |
+
reply_to_bot: bool = False
|
| 82 |
+
priority_level: int = PRIORITY_NORMAL
|
| 83 |
+
quoted_author_name: str = ""
|
| 84 |
+
quoted_author_numero: str = ""
|
| 85 |
+
quoted_text_original: str = ""
|
| 86 |
+
mensagem_citada: str = ""
|
| 87 |
+
context_hint: str = ""
|
| 88 |
+
importancia: float = 1.0
|
| 89 |
+
prompt_section: str = ""
|
| 90 |
+
should_prioritize_reply: bool = False
|
| 91 |
+
adaptive_multiplier: float = 1.0
|
| 92 |
+
|
| 93 |
+
def to_dict(self) -> Dict[str, Any]:
|
| 94 |
+
"""Converte para dicionário."""
|
| 95 |
+
return {
|
| 96 |
+
"is_reply": self.is_reply,
|
| 97 |
+
"reply_to_bot": self.reply_to_bot,
|
| 98 |
+
"priority_level": self.priority_level,
|
| 99 |
+
"quoted_author_name": self.quoted_author_name,
|
| 100 |
+
"quoted_author_numero": self.quoted_author_numero,
|
| 101 |
+
"quoted_text_original": self.quoted_text_original,
|
| 102 |
+
"mensagem_citada": self.mensagem_citada,
|
| 103 |
+
"context_hint": self.context_hint,
|
| 104 |
+
"importancia": self.importancia,
|
| 105 |
+
"prompt_section": self.prompt_section,
|
| 106 |
+
"should_prioritize_reply": self.should_prioritize_reply,
|
| 107 |
+
"adaptive_multiplier": self.adaptive_multiplier
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
@classmethod
|
| 111 |
+
def from_dict(cls, data: Dict[str, Any]) -> 'ProcessedReplyContext':
|
| 112 |
+
"""Cria instância a partir de dicionário."""
|
| 113 |
+
return cls(**{k: v for k, v in data.items() if k in cls.__dataclass_fields__})
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
# ============================================================
|
| 117 |
+
# FUNÇÕES AUXILIARES
|
| 118 |
+
# ============================================================
|
| 119 |
+
|
| 120 |
+
def contar_palavras(texto: str) -> int:
|
| 121 |
+
"""Conta palavras em um texto."""
|
| 122 |
+
if not texto:
|
| 123 |
+
return 0
|
| 124 |
+
return len(texto.split())
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
def is_pergunta_curta(texto: str) -> bool:
|
| 128 |
+
"""
|
| 129 |
+
Verifica se o texto é uma pergunta curta.
|
| 130 |
+
|
| 131 |
+
Args:
|
| 132 |
+
texto: Texto a verificar
|
| 133 |
+
|
| 134 |
+
Returns:
|
| 135 |
+
True se for pergunta com pocas palavras
|
| 136 |
+
"""
|
| 137 |
+
if not texto:
|
| 138 |
+
return False
|
| 139 |
+
|
| 140 |
+
texto_lower = texto.strip().lower()
|
| 141 |
+
word_count = contar_palavras(texto)
|
| 142 |
+
|
| 143 |
+
# Deve ter marcador de pergunta ou palavras interrogativas
|
| 144 |
+
has_question_marker = '?' in texto
|
| 145 |
+
has_interrogative = any(w in texto_lower for w in [
|
| 146 |
+
'qual', 'quais', 'quem', 'como', 'onde', 'quando', 'por que',
|
| 147 |
+
'porque', 'para que', 'o que', 'que', 'é o que', 'vc', 'você',
|
| 148 |
+
'tu', 'meu', 'minha', 'oq', 'oq', 'n'
|
| 149 |
+
])
|
| 150 |
+
|
| 151 |
+
return word_count <= PERGUNTA_CURTA_LIMITE and (has_question_marker or has_interrogative)
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
def is_mensagem_vazia_ou_reconhecimento(texto: str) -> bool:
|
| 155 |
+
"""
|
| 156 |
+
Verifica se a mensagem é apenas um sinal de pontuação ou texto muito curto/vazio.
|
| 157 |
+
Ajuda a evitar a alucinação de self-reply (onde o bot conversa consigo mesmo).
|
| 158 |
+
"""
|
| 159 |
+
if not texto:
|
| 160 |
+
return True
|
| 161 |
+
|
| 162 |
+
clean_text = texto.strip()
|
| 163 |
+
|
| 164 |
+
# Se for apenas 1-2 caracteres não-alfanuméricos (ex: ".", "..", "!")
|
| 165 |
+
import re
|
| 166 |
+
if len(clean_text) <= 2 and not re.search(r'[a-zA-Z0-9]', clean_text):
|
| 167 |
+
return True
|
| 168 |
+
|
| 169 |
+
# Palavras muito curtas e fechadas que soam como reconhecimento e não têm substância
|
| 170 |
+
if clean_text.lower() in [".", "vc", "ah", "ok", "hm", "ta"]:
|
| 171 |
+
return True
|
| 172 |
+
|
| 173 |
+
return False
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
def extrair_context_hint(quoted_text: str, mensagem_atual: str) -> str:
|
| 177 |
+
"""
|
| 178 |
+
Extrai hint de contexto baseado no texto citado e mensagem atual.
|
| 179 |
+
|
| 180 |
+
Args:
|
| 181 |
+
quoted_text: Texto original citado
|
| 182 |
+
mensagem_atual: Mensagem atual do usuário
|
| 183 |
+
|
| 184 |
+
Returns:
|
| 185 |
+
String de hint de contexto
|
| 186 |
+
"""
|
| 187 |
+
hints = []
|
| 188 |
+
|
| 189 |
+
# Detecta tipo de reply
|
| 190 |
+
quoted_lower = quoted_text.lower() if quoted_text else ""
|
| 191 |
+
|
| 192 |
+
# Pergunta sobre o bot
|
| 193 |
+
if any(w in quoted_lower for w in ['akira', 'bot', 'você', 'vc', 'tu']):
|
| 194 |
+
hints.append("pergunta_sobre_akira")
|
| 195 |
+
|
| 196 |
+
# Pergunta factual
|
| 197 |
+
if any(w in quoted_lower for w in ['oq', 'o que', 'qual', 'quanto', 'onde', 'quando']):
|
| 198 |
+
hints.append("pergunta_factual")
|
| 199 |
+
|
| 200 |
+
# Ironia/deboche detectado
|
| 201 |
+
if any(w in quoted_lower for w in ['kkk', 'haha', '😂', '🤣', 'eita']):
|
| 202 |
+
hints.append("tom_irreverente")
|
| 203 |
+
|
| 204 |
+
# Expressão de opinião
|
| 205 |
+
if any(w in quoted_lower for w in ['acho', 'penso', 'creio', 'imagino']):
|
| 206 |
+
hints.append("expressao_opiniao")
|
| 207 |
+
|
| 208 |
+
return " | ".join(hints) if hints else "contexto_geral"
|
| 209 |
+
|
| 210 |
+
|
| 211 |
+
def calcular_prioridade(
|
| 212 |
+
is_reply: bool,
|
| 213 |
+
reply_to_bot: bool,
|
| 214 |
+
mensagem: str,
|
| 215 |
+
quoted_text: str = ""
|
| 216 |
+
) -> Tuple[int, float]:
|
| 217 |
+
"""
|
| 218 |
+
Calcula nível de prioridade e importância.
|
| 219 |
+
|
| 220 |
+
Args:
|
| 221 |
+
is_reply: Se é um reply
|
| 222 |
+
reply_to_bot: Se é reply para o bot
|
| 223 |
+
mensagem: Mensagem atual
|
| 224 |
+
quoted_text: Texto citado
|
| 225 |
+
|
| 226 |
+
Returns:
|
| 227 |
+
Tupla (priority_level, importancia)
|
| 228 |
+
"""
|
| 229 |
+
if not is_reply:
|
| 230 |
+
return PRIORITY_NORMAL, 1.0
|
| 231 |
+
|
| 232 |
+
# Reply para o bot
|
| 233 |
+
if reply_to_bot:
|
| 234 |
+
# Pergunta curta = prioridade máxima
|
| 235 |
+
if is_pergunta_curta(mensagem):
|
| 236 |
+
return PRIORITY_REPLY_TO_BOT_SHORT_QUESTION, IMPORTANCIA_PERGUNTA_CURTA_REPLY
|
| 237 |
+
# Reply normal ao bot
|
| 238 |
+
return PRIORITY_REPLY_TO_BOT, IMPORTANCIA_REPLY_TO_BOT
|
| 239 |
+
|
| 240 |
+
# Reply para outro usuário
|
| 241 |
+
return PRIORITY_REPLY, IMPORTANCIA_REPLY
|
| 242 |
+
|
| 243 |
+
|
| 244 |
+
# ============================================================
|
| 245 |
+
# CLASSE PRINCIPAL
|
| 246 |
+
# ============================================================
|
| 247 |
+
|
| 248 |
+
class ReplyContextHandler:
|
| 249 |
+
"""
|
| 250 |
+
Handler dedicado para processar e priorizar contexto de replies.
|
| 251 |
+
|
| 252 |
+
Funcionalidades:
|
| 253 |
+
- Extração de metadados de reply do payload
|
| 254 |
+
- Cálculo automático de prioridade
|
| 255 |
+
- Construção de seções de prompt otimizadas
|
| 256 |
+
- Integração com ShortTermMemory
|
| 257 |
+
- Ajuste adaptativo baseado em tamanho da pergunta
|
| 258 |
+
"""
|
| 259 |
+
|
| 260 |
+
def __init__(self, short_term_memory: Optional[ShortTermMemory] = None):
|
| 261 |
+
"""
|
| 262 |
+
Inicializa o handler.
|
| 263 |
+
|
| 264 |
+
Args:
|
| 265 |
+
short_term_memory: Instância de ShortTermMemory (opcional)
|
| 266 |
+
"""
|
| 267 |
+
self.short_term_memory = short_term_memory
|
| 268 |
+
self.lstm_extension = None # Será inicializado depois se DB disponível
|
| 269 |
+
logger.debug("✅ ReplyContextHandler inicializado")
|
| 270 |
+
|
| 271 |
+
def enable_lstm(self, lstm_ext: Any) -> None:
|
| 272 |
+
"""Habilita LSTM extension."""
|
| 273 |
+
self.lstm_extension = lstm_ext
|
| 274 |
+
logger.debug("✅ LSTM enabled em ReplyContextHandler")
|
| 275 |
+
|
| 276 |
+
def process_reply(
|
| 277 |
+
self,
|
| 278 |
+
mensagem: str,
|
| 279 |
+
reply_metadata: Dict[str, Any],
|
| 280 |
+
historico_geral: Optional[List[Dict[str, Any]]] = None
|
| 281 |
+
) -> ProcessedReplyContext:
|
| 282 |
+
"""
|
| 283 |
+
Processa metadados de reply e gera contexto processado.
|
| 284 |
+
|
| 285 |
+
Args:
|
| 286 |
+
mensagem: Mensagem atual do usuário
|
| 287 |
+
reply_metadata: Metadados do reply do payload
|
| 288 |
+
historico_geral: Histórico geral (opcional)
|
| 289 |
+
|
| 290 |
+
Returns:
|
| 291 |
+
ProcessedReplyContext pronto para uso
|
| 292 |
+
"""
|
| 293 |
+
# Extrai dados do metadata
|
| 294 |
+
is_reply = reply_metadata.get('is_reply', False)
|
| 295 |
+
reply_to_bot = reply_metadata.get('reply_to_bot', False)
|
| 296 |
+
quoted_author_name = reply_metadata.get('quoted_author_name', '')
|
| 297 |
+
quoted_author_numero = reply_metadata.get('quoted_author_numero', '')
|
| 298 |
+
quoted_text_original = reply_metadata.get('quoted_text_original', '')
|
| 299 |
+
mensagem_citada = reply_metadata.get('mensagem_citada', '') or quoted_text_original
|
| 300 |
+
|
| 301 |
+
# 🔧 CRITICAL FIX: Validate that quoted author is NOT the bot itself
|
| 302 |
+
# Extract pure number from lid_XXXXX format if present
|
| 303 |
+
def extract_pure_number(id_str: str) -> str:
|
| 304 |
+
"""Extrai número puro de formatos como 'lid_123456' ou '123456'"""
|
| 305 |
+
if not id_str:
|
| 306 |
+
return ''
|
| 307 |
+
# Remove 'lid_' prefix if present
|
| 308 |
+
if isinstance(id_str, str) and id_str.startswith('lid_'):
|
| 309 |
+
return id_str[4:]
|
| 310 |
+
return str(id_str) if id_str else ''
|
| 311 |
+
|
| 312 |
+
# ⚠️ SELF-REPLY RECOGNITION
|
| 313 |
+
# Check if the quoted author is the bot itself
|
| 314 |
+
quoted_author_pure = extract_pure_number(quoted_author_numero)
|
| 315 |
+
bot_id_pure = extract_pure_number(config.BOT_NUMERO if hasattr(config, 'BOT_NUMERO') else '37839265886398')
|
| 316 |
+
|
| 317 |
+
is_quoted_from_bot = (quoted_author_pure and quoted_author_pure == bot_id_pure)
|
| 318 |
+
|
| 319 |
+
if is_quoted_from_bot and is_reply:
|
| 320 |
+
logger.info(f"🔄 [REPLY AO BOT] Usuário está respondendo a uma mensagem da Akira ({quoted_author_pure}).")
|
| 321 |
+
reply_to_bot = True
|
| 322 |
+
quoted_author_name = "Akira (você mesmo)"
|
| 323 |
+
quoted_author_numero = config.BOT_NUMERO
|
| 324 |
+
|
| 325 |
+
# 🔧 CORREÇÃO FORÇADA: Se o payload já determinou que é reply_to_bot,
|
| 326 |
+
# ignora qualquer nome/número que tenha vindo e força para o bot.
|
| 327 |
+
if is_reply and reply_to_bot:
|
| 328 |
+
quoted_author_name = "Akira (você mesmo)"
|
| 329 |
+
quoted_author_numero = config.BOT_NUMERO
|
| 330 |
+
|
| 331 |
+
# 🔧 CORREÇÃO: Se autor é desconhecido e não é reply_to_bot explícito, tenta detectar pelo contexto
|
| 332 |
+
elif not quoted_author_name or quoted_author_name.lower() in ['desconhecido', 'unknown', '']:
|
| 333 |
+
# Detecta pelo conteúdo da mensagem citada
|
| 334 |
+
quoted_lower = quoted_text_original.lower() if quoted_text_original else ""
|
| 335 |
+
|
| 336 |
+
# Se a mensagem citada contém padrões de resposta do bot
|
| 337 |
+
bot_patterns = ['akira:', 'eu sou', 'eu sou a akira', 'sou um bot', 'oi!', 'eae!']
|
| 338 |
+
if any(p in quoted_lower for p in bot_patterns):
|
| 339 |
+
quoted_author_name = "Akira (você mesmo)"
|
| 340 |
+
quoted_author_numero = config.BOT_NUMERO
|
| 341 |
+
reply_to_bot = True
|
| 342 |
+
elif mensagem_citada:
|
| 343 |
+
# Se há histórico, busca última mensagem
|
| 344 |
+
if historico_geral:
|
| 345 |
+
# Assumir que é reply para a última mensagem do bot
|
| 346 |
+
quoted_author_name = "mensagem_anterior"
|
| 347 |
+
quoted_author_numero = "unknown"
|
| 348 |
+
|
| 349 |
+
# Se ainda não tem autor mas tem mensagem citada e é reply
|
| 350 |
+
if is_reply and (not quoted_author_name or quoted_author_name == 'desconhecido'):
|
| 351 |
+
# Se é reply_to_bot=True mas autor desconhecido, assume que é reply para o bot
|
| 352 |
+
if reply_to_bot:
|
| 353 |
+
quoted_author_name = "Akira (você mesmo)"
|
| 354 |
+
quoted_author_numero = "BOT"
|
| 355 |
+
else:
|
| 356 |
+
# Tenta extrair do conteúdo
|
| 357 |
+
quoted_author_name = "participante_desconhecido"
|
| 358 |
+
|
| 359 |
+
# Calcula prioridade e importância
|
| 360 |
+
priority_level, importancia = calcular_prioridade(
|
| 361 |
+
is_reply=is_reply,
|
| 362 |
+
reply_to_bot=reply_to_bot,
|
| 363 |
+
mensagem=mensagem,
|
| 364 |
+
quoted_text=quoted_text_original
|
| 365 |
+
)
|
| 366 |
+
|
| 367 |
+
# Extrai context hint
|
| 368 |
+
context_hint = extrair_context_hint(quoted_text_original, mensagem)
|
| 369 |
+
|
| 370 |
+
# Calcula multiplicador adaptativo
|
| 371 |
+
adaptive_multiplier = self._calculate_adaptive_multiplier(
|
| 372 |
+
mensagem=mensagem,
|
| 373 |
+
is_reply=is_reply,
|
| 374 |
+
priority_level=priority_level
|
| 375 |
+
)
|
| 376 |
+
|
| 377 |
+
# Determina se deve priorizar no prompt
|
| 378 |
+
should_prioritize = is_reply and priority_level >= PRIORITY_REPLY
|
| 379 |
+
|
| 380 |
+
# Constrói section do prompt
|
| 381 |
+
prompt_section = self._build_reply_prompt_section(
|
| 382 |
+
mensagem=mensagem,
|
| 383 |
+
mensagem_citada=mensagem_citada,
|
| 384 |
+
quoted_author_name=quoted_author_name,
|
| 385 |
+
reply_to_bot=reply_to_bot,
|
| 386 |
+
context_hint=context_hint,
|
| 387 |
+
priority_level=priority_level
|
| 388 |
+
)
|
| 389 |
+
|
| 390 |
+
# Cria contexto processado
|
| 391 |
+
reply_context = ProcessedReplyContext(
|
| 392 |
+
is_reply=is_reply,
|
| 393 |
+
reply_to_bot=reply_to_bot,
|
| 394 |
+
priority_level=priority_level,
|
| 395 |
+
quoted_author_name=quoted_author_name,
|
| 396 |
+
quoted_author_numero=quoted_author_numero,
|
| 397 |
+
quoted_text_original=quoted_text_original,
|
| 398 |
+
mensagem_citada=mensagem_citada,
|
| 399 |
+
context_hint=context_hint,
|
| 400 |
+
importancia=importancia * adaptive_multiplier,
|
| 401 |
+
prompt_section=prompt_section,
|
| 402 |
+
should_prioritize_reply=should_prioritize,
|
| 403 |
+
adaptive_multiplier=adaptive_multiplier
|
| 404 |
+
)
|
| 405 |
+
|
| 406 |
+
# Adiciona à memória de curto prazo se disponível
|
| 407 |
+
if self.short_term_memory and is_reply:
|
| 408 |
+
self.short_term_memory.add_message(
|
| 409 |
+
role="user",
|
| 410 |
+
content=mensagem,
|
| 411 |
+
importancia=reply_context.importancia,
|
| 412 |
+
reply_info={
|
| 413 |
+
"is_reply": True,
|
| 414 |
+
"reply_to_bot": reply_to_bot,
|
| 415 |
+
"quoted_text_original": quoted_text_original,
|
| 416 |
+
"priority_level": priority_level
|
| 417 |
+
}
|
| 418 |
+
)
|
| 419 |
+
|
| 420 |
+
return reply_context
|
| 421 |
+
|
| 422 |
+
def _calculate_adaptive_multiplier(
|
| 423 |
+
self,
|
| 424 |
+
mensagem: str,
|
| 425 |
+
is_reply: bool,
|
| 426 |
+
priority_level: int
|
| 427 |
+
) -> float:
|
| 428 |
+
"""
|
| 429 |
+
Calcula multiplicador adaptativo baseado no tamanho da pergunta.
|
| 430 |
+
|
| 431 |
+
Para perguntas curtas com reply, aumenta a importância do contexto do reply
|
| 432 |
+
para garantir que o LLM tenha contexto suficiente.
|
| 433 |
+
|
| 434 |
+
Args:
|
| 435 |
+
mensagem: Mensagem atual
|
| 436 |
+
is_reply: Se é reply
|
| 437 |
+
priority_level: Nível de prioridade
|
| 438 |
+
|
| 439 |
+
Returns:
|
| 440 |
+
Multiplicador entre 1.0 e 2.0
|
| 441 |
+
"""
|
| 442 |
+
if not is_reply:
|
| 443 |
+
return 1.0
|
| 444 |
+
|
| 445 |
+
word_count = contar_palavras(mensagem)
|
| 446 |
+
|
| 447 |
+
# Pergunta muito curta (< 3 palavras) = contexto crítico
|
| 448 |
+
if word_count <= 2:
|
| 449 |
+
# Proteção contra alucinação
|
| 450 |
+
if is_mensagem_vazia_ou_reconhecimento(mensagem):
|
| 451 |
+
return 0.5 # Reduz a importância para o bot focar menos no contexto citado
|
| 452 |
+
return 1.5
|
| 453 |
+
|
| 454 |
+
# Pergunta curta (3-5 palavras) = contexto importante
|
| 455 |
+
if word_count <= PERGUNTA_CURTA_LIMITE:
|
| 456 |
+
return 1.3
|
| 457 |
+
|
| 458 |
+
# Pergunta normal = multiplicador padrão baseado em prioridade
|
| 459 |
+
if priority_level == PRIORITY_REPLY_TO_BOT_SHORT_QUESTION:
|
| 460 |
+
return 1.2
|
| 461 |
+
elif priority_level == PRIORITY_REPLY_TO_BOT:
|
| 462 |
+
return 1.1
|
| 463 |
+
|
| 464 |
+
return 1.0
|
| 465 |
+
|
| 466 |
+
def _build_reply_prompt_section(
|
| 467 |
+
self,
|
| 468 |
+
mensagem: str,
|
| 469 |
+
mensagem_citada: str,
|
| 470 |
+
quoted_author_name: str,
|
| 471 |
+
reply_to_bot: bool,
|
| 472 |
+
context_hint: str,
|
| 473 |
+
priority_level: int
|
| 474 |
+
) -> str:
|
| 475 |
+
"""
|
| 476 |
+
Constrói seção formatada do prompt para replies.
|
| 477 |
+
|
| 478 |
+
Args:
|
| 479 |
+
mensagem: Mensagem atual
|
| 480 |
+
mensagem_citada: Texto citado
|
| 481 |
+
quoted_author_name: Nome do autor
|
| 482 |
+
reply_to_bot: Se é reply para o bot
|
| 483 |
+
context_hint: Hint de contexto
|
| 484 |
+
priority_level: Nível de prioridade
|
| 485 |
+
|
| 486 |
+
Returns:
|
| 487 |
+
String formatada para inserção no prompt
|
| 488 |
+
"""
|
| 489 |
+
if not mensagem_citada:
|
| 490 |
+
return ""
|
| 491 |
+
|
| 492 |
+
sections = []
|
| 493 |
+
|
| 494 |
+
# Cabeçalho com nível de prioridade
|
| 495 |
+
if priority_level >= PRIORITY_REPLY_TO_BOT_SHORT_QUESTION:
|
| 496 |
+
sections.append("[🔴 REPLY CRÍTICO - PERGUNTA CURTA]")
|
| 497 |
+
elif priority_level == PRIORITY_REPLY_TO_BOT:
|
| 498 |
+
sections.append("[🟡 REPLY AO BOT]")
|
| 499 |
+
elif priority_level == PRIORITY_REPLY:
|
| 500 |
+
sections.append("[🟢 REPLY]")
|
| 501 |
+
|
| 502 |
+
# Contexto do autor e conteúdo
|
| 503 |
+
if reply_to_bot:
|
| 504 |
+
sections.append(f"⚠️ O USUÁRIO ESTÁ RESPONDENDO DIRETAMENTE A VOCÊ!")
|
| 505 |
+
quoted_preview = mensagem_citada[:200] + ("..." if len(mensagem_citada) > 200 else "")
|
| 506 |
+
sections.append(f"O usuário citou sua mensagem anterior: \"{quoted_preview}\"")
|
| 507 |
+
else:
|
| 508 |
+
sections.append(f"Respondendo a: {quoted_author_name}")
|
| 509 |
+
quoted_preview = mensagem_citada[:150] + ("..." if len(mensagem_citada) > 150 else "")
|
| 510 |
+
sections.append(f"Msg citada: \"{quoted_preview}\"")
|
| 511 |
+
|
| 512 |
+
# Hint de contexto
|
| 513 |
+
if context_hint and context_hint != "contexto_geral":
|
| 514 |
+
sections.append(f"Contexto: {context_hint}")
|
| 515 |
+
|
| 516 |
+
# Instrução de resposta (MUITO AGRESSIVA CONTRA NARRATIVA E SELF-REPLY)
|
| 517 |
+
vazia_reconhecimento = is_mensagem_vazia_ou_reconhecimento(mensagem)
|
| 518 |
+
|
| 519 |
+
if reply_to_bot and vazia_reconhecimento:
|
| 520 |
+
sections.append(f"🛑 [ALERTA DE SELF-REPLY]: O usuário respondeu APENAS com '{mensagem}' à sua mensagem. Isso é apenas um reconhecimento ou sinal. NÃO responda a si mesma, NÃO repita a sua mensagem. Reaja com sarcasmo, naturalidade ou pergunte o que o usuário quer dizer.")
|
| 521 |
+
elif priority_level >= PRIORITY_REPLY_TO_BOT_SHORT_QUESTION:
|
| 522 |
+
sections.append("💡 RESPONSE: Responda SILENCIOSAMENTE ao contexto. Jamais use prefixos de IA ou narre que está respondendo. Vá direto ao ponto.")
|
| 523 |
+
elif reply_to_bot:
|
| 524 |
+
sections.append("💡 RESPONSE: Você foi citada. NUNCA comece com 'Ah', 'Então', 'Vejo' ou narre o reply. Mate o prefixo e responda direto.")
|
| 525 |
+
|
| 526 |
+
return "\n".join(sections)
|
| 527 |
+
|
| 528 |
+
def prioritize_reply_context(
|
| 529 |
+
self,
|
| 530 |
+
prompt: str,
|
| 531 |
+
reply_context: ProcessedReplyContext,
|
| 532 |
+
historico_geral: Optional[List[Dict[str, Any]]] = None
|
| 533 |
+
) -> str:
|
| 534 |
+
"""
|
| 535 |
+
Injeta contexto de reply no prompt com alta prioridade.
|
| 536 |
+
|
| 537 |
+
Args:
|
| 538 |
+
prompt: Prompt original
|
| 539 |
+
reply_context: Contexto de reply processado
|
| 540 |
+
historico_geral: Histórico geral (opcional)
|
| 541 |
+
|
| 542 |
+
Returns:
|
| 543 |
+
Prompt enriquecido com contexto de reply
|
| 544 |
+
"""
|
| 545 |
+
if not reply_context.is_reply or not reply_context.prompt_section:
|
| 546 |
+
return prompt
|
| 547 |
+
|
| 548 |
+
# Insere contexto de reply no início do prompt
|
| 549 |
+
reply_block = f"""
|
| 550 |
+
{'='*60}
|
| 551 |
+
{reply_context.prompt_section}
|
| 552 |
+
{'='*60}
|
| 553 |
+
"""
|
| 554 |
+
|
| 555 |
+
# Determina posição de inserção
|
| 556 |
+
# Se há seção [SYSTEM], insere após ela
|
| 557 |
+
if "[SYSTEM]" in prompt:
|
| 558 |
+
# Encontra final da seção SYSTEM
|
| 559 |
+
system_end = prompt.find("[/SYSTEM]")
|
| 560 |
+
if system_end != -1:
|
| 561 |
+
return prompt[:system_end + 10] + reply_block + prompt[system_end + 10:]
|
| 562 |
+
|
| 563 |
+
# Caso contrário, insere no início
|
| 564 |
+
return reply_block + "\n" + prompt
|
| 565 |
+
|
| 566 |
+
def get_reply_summary_for_llm(self, reply_context: ProcessedReplyContext) -> str:
|
| 567 |
+
"""
|
| 568 |
+
Retorna resumo formatado do reply para contexto do LLM.
|
| 569 |
+
|
| 570 |
+
Args:
|
| 571 |
+
reply_context: Contexto de reply processado
|
| 572 |
+
|
| 573 |
+
Returns:
|
| 574 |
+
String resumida para uso no contexto
|
| 575 |
+
"""
|
| 576 |
+
if not reply_context.is_reply:
|
| 577 |
+
return ""
|
| 578 |
+
|
| 579 |
+
parts = []
|
| 580 |
+
|
| 581 |
+
if reply_context.reply_to_bot:
|
| 582 |
+
parts.append("REPLY DIRETO AO BOT")
|
| 583 |
+
else:
|
| 584 |
+
parts.append(f"REPLY a {reply_context.quoted_author_name}")
|
| 585 |
+
|
| 586 |
+
if reply_context.mensagem_citada:
|
| 587 |
+
cited = reply_context.mensagem_citada[:100]
|
| 588 |
+
parts.append(f"Citando: \"{cited}\"")
|
| 589 |
+
|
| 590 |
+
if reply_context.priority_level >= PRIORITY_REPLY_TO_BOT_SHORT_QUESTION:
|
| 591 |
+
parts.append("PERGUNTA CURTA - Prioridade Alta")
|
| 592 |
+
|
| 593 |
+
return " | ".join(parts)
|
| 594 |
+
|
| 595 |
+
def merge_reply_into_history(
|
| 596 |
+
self,
|
| 597 |
+
reply_context: ProcessedReplyContext,
|
| 598 |
+
history: List[Dict[str, str]]
|
| 599 |
+
) -> List[Dict[str, str]]:
|
| 600 |
+
"""
|
| 601 |
+
Mescla contexto de reply no histórico para o LLM.
|
| 602 |
+
|
| 603 |
+
Args:
|
| 604 |
+
reply_context: Contexto de reply processado
|
| 605 |
+
history: Histórico formatado para LLM
|
| 606 |
+
|
| 607 |
+
Returns:
|
| 608 |
+
Histórico com reply injetado no início
|
| 609 |
+
"""
|
| 610 |
+
if not reply_context.is_reply:
|
| 611 |
+
return history
|
| 612 |
+
|
| 613 |
+
# Cria entry para o reply
|
| 614 |
+
reply_entry = {
|
| 615 |
+
"role": "user",
|
| 616 |
+
"content": f"[REPLY] {reply_context.get_reply_summary_for_llm(reply_context)}"
|
| 617 |
+
}
|
| 618 |
+
|
| 619 |
+
# Adiciona texto citado se disponível
|
| 620 |
+
if reply_context.mensagem_citada:
|
| 621 |
+
reply_entry["content"] += f"\n\nMensagem citada:\n{reply_context.mensagem_citada}"
|
| 622 |
+
|
| 623 |
+
# Insere no início do histórico
|
| 624 |
+
return [reply_entry] + history
|
| 625 |
+
|
| 626 |
+
def calculate_token_budget(
|
| 627 |
+
self,
|
| 628 |
+
reply_context: ProcessedReplyContext,
|
| 629 |
+
total_budget: int = 8000
|
| 630 |
+
) -> Tuple[int, int]:
|
| 631 |
+
"""
|
| 632 |
+
Calcula alocação de tokens entre reply e contexto geral.
|
| 633 |
+
|
| 634 |
+
Args:
|
| 635 |
+
reply_context: Contexto de reply
|
| 636 |
+
total_budget: Total de tokens disponíveis
|
| 637 |
+
|
| 638 |
+
Returns:
|
| 639 |
+
Tupla (tokens_para_reply, tokens_para_contexto)
|
| 640 |
+
"""
|
| 641 |
+
if not reply_context.is_reply:
|
| 642 |
+
return 0, total_budget
|
| 643 |
+
|
| 644 |
+
# Pergunta curta com reply = mais tokens para reply
|
| 645 |
+
if reply_context.priority_level >= PRIORITY_REPLY_TO_BOT_SHORT_QUESTION:
|
| 646 |
+
reply_tokens = min(1500, int(total_budget * 0.25))
|
| 647 |
+
elif reply_context.reply_to_bot:
|
| 648 |
+
reply_tokens = min(1000, int(total_budget * 0.15))
|
| 649 |
+
else:
|
| 650 |
+
reply_tokens = min(800, int(total_budget * 0.10))
|
| 651 |
+
|
| 652 |
+
return reply_tokens, total_budget - reply_tokens
|
| 653 |
+
|
| 654 |
+
# ============================================================
|
| 655 |
+
# HELPERS PARA API
|
| 656 |
+
# ============================================================
|
| 657 |
+
|
| 658 |
+
@staticmethod
|
| 659 |
+
def extract_reply_metadata_from_request(data: Dict[str, Any]) -> Dict[str, Any]:
|
| 660 |
+
"""
|
| 661 |
+
Extrai metadados de reply de um request da API.
|
| 662 |
+
|
| 663 |
+
Args:
|
| 664 |
+
data: Payload do request
|
| 665 |
+
|
| 666 |
+
Returns:
|
| 667 |
+
Dict com metadados de reply
|
| 668 |
+
"""
|
| 669 |
+
reply_metadata = data.get('reply_metadata', {})
|
| 670 |
+
|
| 671 |
+
# Se não há reply_metadata, tenta extrair de campos individuais
|
| 672 |
+
if not reply_metadata:
|
| 673 |
+
mensagem_citada = data.get('mensagem_citada', '')
|
| 674 |
+
if mensagem_citada:
|
| 675 |
+
reply_metadata = {
|
| 676 |
+
'is_reply': True,
|
| 677 |
+
'quoted_text_original': mensagem_citada,
|
| 678 |
+
'mensagem_citada': mensagem_citada
|
| 679 |
+
}
|
| 680 |
+
else:
|
| 681 |
+
return {'is_reply': False}
|
| 682 |
+
|
| 683 |
+
# Garante campos obrigatórios
|
| 684 |
+
return {
|
| 685 |
+
'is_reply': reply_metadata.get('is_reply', False),
|
| 686 |
+
'reply_to_bot': reply_metadata.get('reply_to_bot', False),
|
| 687 |
+
'quoted_author_name': reply_metadata.get('quoted_author_name', ''),
|
| 688 |
+
'quoted_author_numero': reply_metadata.get('quoted_author_numero', ''),
|
| 689 |
+
'quoted_type': reply_metadata.get('quoted_type', 'texto'),
|
| 690 |
+
'quoted_text_original': reply_metadata.get('quoted_text_original', ''),
|
| 691 |
+
'context_hint': reply_metadata.get('context_hint', ''),
|
| 692 |
+
'mensagem_citada': reply_metadata.get('mensagem_citada', '')
|
| 693 |
+
}
|
| 694 |
+
|
| 695 |
+
def validate_reply_priority(self, reply_context: ProcessedReplyContext) -> bool:
|
| 696 |
+
"""
|
| 697 |
+
Valida se a prioridade calculada está correta.
|
| 698 |
+
|
| 699 |
+
Args:
|
| 700 |
+
reply_context: Contexto a validar
|
| 701 |
+
|
| 702 |
+
Returns:
|
| 703 |
+
True se válido
|
| 704 |
+
"""
|
| 705 |
+
if not reply_context.is_reply:
|
| 706 |
+
return reply_context.priority_level == PRIORITY_NORMAL
|
| 707 |
+
|
| 708 |
+
# Reply para bot + pergunta curta deve ter prioridade máxima
|
| 709 |
+
if reply_context.reply_to_bot and is_pergunta_curta(reply_context.mensagem_citada):
|
| 710 |
+
return reply_context.priority_level == PRIORITY_REPLY_TO_BOT_SHORT_QUESTION
|
| 711 |
+
|
| 712 |
+
# Reply para bot deve ter alta prioridade
|
| 713 |
+
if reply_context.reply_to_bot:
|
| 714 |
+
return reply_context.priority_level >= PRIORITY_REPLY_TO_BOT
|
| 715 |
+
|
| 716 |
+
# Reply normal deve ter prioridade >= 2
|
| 717 |
+
return reply_context.priority_level >= PRIORITY_REPLY
|
| 718 |
+
|
| 719 |
+
def __repr__(self) -> str:
|
| 720 |
+
"""Representação textual."""
|
| 721 |
+
mem_status = "com STM" if self.short_term_memory else "sem STM"
|
| 722 |
+
return f"ReplyContextHandler({mem_status})"
|
| 723 |
+
|
| 724 |
+
|
| 725 |
+
# ============================================================
|
| 726 |
+
# FUNÇÕES DE FÁBRICA
|
| 727 |
+
# ============================================================
|
| 728 |
+
|
| 729 |
+
def criar_reply_handler(
|
| 730 |
+
short_term_memory: Optional[ShortTermMemory] = None
|
| 731 |
+
) -> ReplyContextHandler:
|
| 732 |
+
"""
|
| 733 |
+
Factory function para criar ReplyContextHandler.
|
| 734 |
+
|
| 735 |
+
Args:
|
| 736 |
+
short_term_memory: Instância de ShortTermMemory (opcional)
|
| 737 |
+
|
| 738 |
+
Returns:
|
| 739 |
+
ReplyContextHandler instance
|
| 740 |
+
"""
|
| 741 |
+
return ReplyContextHandler(short_term_memory=short_term_memory)
|
| 742 |
+
|
| 743 |
+
|
| 744 |
+
def processar_reply_request(
|
| 745 |
+
mensagem: str,
|
| 746 |
+
request_data: Dict[str, Any],
|
| 747 |
+
short_term_memory: Optional[ShortTermMemory] = None
|
| 748 |
+
) -> ProcessedReplyContext:
|
| 749 |
+
"""
|
| 750 |
+
Função helper para processar reply de request.
|
| 751 |
+
|
| 752 |
+
Args:
|
| 753 |
+
mensagem: Mensagem atual
|
| 754 |
+
request_data: Payload do request
|
| 755 |
+
short_term_memory: Instância de ShortTermMemory (opcional)
|
| 756 |
+
|
| 757 |
+
Returns:
|
| 758 |
+
ProcessedReplyContext
|
| 759 |
+
"""
|
| 760 |
+
handler = criar_reply_handler(short_term_memory)
|
| 761 |
+
reply_metadata = handler.extract_reply_metadata_from_request(request_data)
|
| 762 |
+
return handler.process_reply(mensagem, reply_metadata)
|
| 763 |
+
|
| 764 |
+
|
| 765 |
+
# ============================================================
|
| 766 |
+
# COMPATIBILIDADE — aliases para imports legados
|
| 767 |
+
# ============================================================
|
| 768 |
+
|
| 769 |
+
_reply_handler_singleton = None
|
| 770 |
+
|
| 771 |
+
def get_context_handler(short_term_memory=None) -> ReplyContextHandler:
|
| 772 |
+
"""Alias legado de get_context_handler → retorna singleton de ReplyContextHandler."""
|
| 773 |
+
global _reply_handler_singleton
|
| 774 |
+
if _reply_handler_singleton is None:
|
| 775 |
+
_reply_handler_singleton = ReplyContextHandler(short_term_memory=short_term_memory)
|
| 776 |
+
return _reply_handler_singleton
|
| 777 |
+
|
| 778 |
+
|
| 779 |
+
# type: ignore
|
| 780 |
+
|
| 781 |
+
|
modules/self_awareness.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Self-Awareness Module - Permite IA reconhecer erros e responder a crítica.
|
| 3 |
+
|
| 4 |
+
Criado como parte da Fase 3: Self-Aware Correction
|
| 5 |
+
Data: 2026-05-15
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import re
|
| 9 |
+
from typing import Dict, Tuple
|
| 10 |
+
from loguru import logger
|
| 11 |
+
from datetime import datetime
|
| 12 |
+
|
| 13 |
+
class SelfAwarenessEngine:
|
| 14 |
+
"""Detecta crítica, erro anterior, e permite self-correction."""
|
| 15 |
+
|
| 16 |
+
def __init__(self):
|
| 17 |
+
self.logger = logger
|
| 18 |
+
self.error_memory = {}
|
| 19 |
+
|
| 20 |
+
self.criticism_patterns = [
|
| 21 |
+
r"(?:isso|isso que|que)\s+(?:você\s+)?(?:disse|falou|escreveu)\s+(?:é\s+)?(?:errado|falso|mentira)",
|
| 22 |
+
r"(?:você\s+)?(?:errou|enganou|enganaste)",
|
| 23 |
+
r"(?:tá|está)\s+(?:errado|mal|falso)",
|
| 24 |
+
r"(?:não|n[ã\/]o)\s+(?:é|foi)\s+(?:assim|verdade|correto)",
|
| 25 |
+
]
|
| 26 |
+
|
| 27 |
+
self.error_acknowledgment = [
|
| 28 |
+
"Você tem razão, cometi erro.",
|
| 29 |
+
"Admito que estava errado.",
|
| 30 |
+
"Obrigado pela correção, você está certo.",
|
| 31 |
+
"Eu me equivoquei naquilo.",
|
| 32 |
+
]
|
| 33 |
+
|
| 34 |
+
def detect_criticism(self, mensagem: str) -> Tuple[bool, str]:
|
| 35 |
+
"""
|
| 36 |
+
Detecta se mensagem é crítica a resposta anterior.
|
| 37 |
+
|
| 38 |
+
Returns:
|
| 39 |
+
(tem_crítica, tipo_crítica)
|
| 40 |
+
"""
|
| 41 |
+
mensagem_lower = mensagem.lower()
|
| 42 |
+
|
| 43 |
+
for pattern in self.criticism_patterns:
|
| 44 |
+
if re.search(pattern, mensagem_lower):
|
| 45 |
+
return True, "direct_criticism"
|
| 46 |
+
|
| 47 |
+
if any(phrase in mensagem_lower for phrase in ["na verdade", "corrigindo", "melhor seria"]):
|
| 48 |
+
return True, "implicit_correction"
|
| 49 |
+
|
| 50 |
+
return False, None
|
| 51 |
+
|
| 52 |
+
def generate_self_correction_response(
|
| 53 |
+
self,
|
| 54 |
+
original_response: str,
|
| 55 |
+
correction: str,
|
| 56 |
+
user_id: str
|
| 57 |
+
) -> str:
|
| 58 |
+
"""
|
| 59 |
+
Gera resposta que reconhece erro e corrige.
|
| 60 |
+
"""
|
| 61 |
+
|
| 62 |
+
import random
|
| 63 |
+
ack = random.choice(self.error_acknowledgment)
|
| 64 |
+
|
| 65 |
+
response = (
|
| 66 |
+
f"{ack}\n\n"
|
| 67 |
+
f"Então ficaria: {correction}\n\n"
|
| 68 |
+
f"Obrigado por me manter preciso. É assim que melhoro."
|
| 69 |
+
)
|
| 70 |
+
|
| 71 |
+
if user_id not in self.error_memory:
|
| 72 |
+
self.error_memory[user_id] = []
|
| 73 |
+
|
| 74 |
+
self.error_memory[user_id].append({
|
| 75 |
+
"original": original_response,
|
| 76 |
+
"correction": correction,
|
| 77 |
+
"timestamp": datetime.now().isoformat()
|
| 78 |
+
})
|
| 79 |
+
|
| 80 |
+
self.logger.info(f"📝 [SELF-AWARE] Erro registrado para {user_id}")
|
| 81 |
+
|
| 82 |
+
return response
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
# Instância global
|
| 86 |
+
self_awareness_engine = SelfAwarenessEngine()
|
modules/sender_attribution_fix.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Monkey-patch for sender attribution bug fix in modules/api.py
|
| 3 |
+
This module patches the akira_endpoint to properly validate and reconstruct sender names
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import sys
|
| 7 |
+
from functools import wraps
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def patch_akira_api():
|
| 11 |
+
"""Apply the sender attribution fix by monkey-patching the modules.api module"""
|
| 12 |
+
|
| 13 |
+
try:
|
| 14 |
+
from modules import api
|
| 15 |
+
|
| 16 |
+
# Store original endpoint method
|
| 17 |
+
original_get_blueprint = api.get_blueprint
|
| 18 |
+
|
| 19 |
+
def patched_get_blueprint():
|
| 20 |
+
"""Wrapper that patches the blueprint routes"""
|
| 21 |
+
bp = original_get_blueprint()
|
| 22 |
+
|
| 23 |
+
# Get the akira_endpoint from the blueprint
|
| 24 |
+
for rule in bp.defsurl_map.iter_rules():
|
| 25 |
+
if rule.endpoint == 'akira_endpoint':
|
| 26 |
+
original_endpoint = bp.view_functions.get('akira_endpoint')
|
| 27 |
+
if original_endpoint:
|
| 28 |
+
# Wrap the endpoint
|
| 29 |
+
@wraps(original_endpoint)
|
| 30 |
+
def patched_akira_endpoint(*args, **kwargs):
|
| 31 |
+
# Call original
|
| 32 |
+
result = original_endpoint(*args, **kwargs)
|
| 33 |
+
return result
|
| 34 |
+
|
| 35 |
+
bp.view_functions['akira_endpoint'] = patched_akira_endpoint
|
| 36 |
+
break
|
| 37 |
+
|
| 38 |
+
return bp
|
| 39 |
+
|
| 40 |
+
# Replace the function
|
| 41 |
+
api.get_blueprint = patched_get_blueprint
|
| 42 |
+
|
| 43 |
+
print("✅ Sender attribution fix monkey-patch applied to modules.api")
|
| 44 |
+
return True
|
| 45 |
+
|
| 46 |
+
except Exception as e:
|
| 47 |
+
print(f"⚠️ Failed to apply monkey-patch: {e}")
|
| 48 |
+
return False
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
# Auto-apply when imported
|
| 52 |
+
try:
|
| 53 |
+
patch_akira_api()
|
| 54 |
+
except Exception as e:
|
| 55 |
+
print(f"Error during auto-patch: {e}")
|
modules/short_term_memory.py
CHANGED
|
@@ -71,6 +71,7 @@ class MessageWithContext:
|
|
| 71 |
emocao: Emoção detectada
|
| 72 |
reply_info: Info sobre reply (se aplicável)
|
| 73 |
conversation_id: ID da conversa isolada
|
|
|
|
| 74 |
token_count: Contagem aproximada de tokens
|
| 75 |
"""
|
| 76 |
role: str
|
|
@@ -80,6 +81,7 @@ class MessageWithContext:
|
|
| 80 |
emocao: str = "neutro"
|
| 81 |
reply_info: Dict[str, Any] = field(default_factory=dict)
|
| 82 |
conversation_id: str = ""
|
|
|
|
| 83 |
token_count: int = 0
|
| 84 |
|
| 85 |
def to_dict(self) -> Dict[str, Any]:
|
|
@@ -92,6 +94,7 @@ class MessageWithContext:
|
|
| 92 |
"emocao": self.emocao,
|
| 93 |
"reply_info": self.reply_info,
|
| 94 |
"conversation_id": self.conversation_id,
|
|
|
|
| 95 |
"token_count": self.token_count
|
| 96 |
}
|
| 97 |
|
|
@@ -106,6 +109,7 @@ class MessageWithContext:
|
|
| 106 |
emocao=data.get("emocao", "neutral"),
|
| 107 |
reply_info=data.get("reply_info", {}),
|
| 108 |
conversation_id=data.get("conversation_id", ""),
|
|
|
|
| 109 |
token_count=data.get("token_count", 0)
|
| 110 |
)
|
| 111 |
|
|
@@ -274,6 +278,7 @@ class ShortTermMemory:
|
|
| 274 |
importancia: float = IMPORTANCIA_NORMAL,
|
| 275 |
emocao: str = "neutro",
|
| 276 |
reply_info: Optional[Dict[str, Any]] = None,
|
|
|
|
| 277 |
metadata: Optional[Dict[str, Any]] = None
|
| 278 |
) -> MessageWithContext:
|
| 279 |
"""
|
|
@@ -298,6 +303,7 @@ class ShortTermMemory:
|
|
| 298 |
emocao=emocao,
|
| 299 |
reply_info=reply_info or {},
|
| 300 |
conversation_id=self.conversation_id,
|
|
|
|
| 301 |
token_count=estimar_tokens(content)
|
| 302 |
)
|
| 303 |
|
|
@@ -323,6 +329,7 @@ class ShortTermMemory:
|
|
| 323 |
def add_user_message(
|
| 324 |
self,
|
| 325 |
content: str,
|
|
|
|
| 326 |
emocao: str = "neutral",
|
| 327 |
reply_info: Optional[Dict[str, Any]] = None,
|
| 328 |
importancia: float = None
|
|
@@ -350,6 +357,7 @@ class ShortTermMemory:
|
|
| 350 |
return self.add_message(
|
| 351 |
role="user",
|
| 352 |
content=content,
|
|
|
|
| 353 |
importancia=importancia,
|
| 354 |
emocao=emocao,
|
| 355 |
reply_info=reply_info
|
|
@@ -358,6 +366,7 @@ class ShortTermMemory:
|
|
| 358 |
def add_assistant_message(
|
| 359 |
self,
|
| 360 |
content: str,
|
|
|
|
| 361 |
emocao: str = "neutral",
|
| 362 |
importancia: float = IMPORTANCIA_NORMAL
|
| 363 |
) -> MessageWithContext:
|
|
@@ -375,6 +384,7 @@ class ShortTermMemory:
|
|
| 375 |
return self.add_message(
|
| 376 |
role="assistant",
|
| 377 |
content=content,
|
|
|
|
| 378 |
importancia=importancia,
|
| 379 |
emocao=emocao
|
| 380 |
)
|
|
|
|
| 71 |
emocao: Emoção detectada
|
| 72 |
reply_info: Info sobre reply (se aplicável)
|
| 73 |
conversation_id: ID da conversa isolada
|
| 74 |
+
author_name: Nome de quem enviou a mensagem (ex: Isaac, Akira, ISA IA)
|
| 75 |
token_count: Contagem aproximada de tokens
|
| 76 |
"""
|
| 77 |
role: str
|
|
|
|
| 81 |
emocao: str = "neutro"
|
| 82 |
reply_info: Dict[str, Any] = field(default_factory=dict)
|
| 83 |
conversation_id: str = ""
|
| 84 |
+
author_name: str = "Usuário"
|
| 85 |
token_count: int = 0
|
| 86 |
|
| 87 |
def to_dict(self) -> Dict[str, Any]:
|
|
|
|
| 94 |
"emocao": self.emocao,
|
| 95 |
"reply_info": self.reply_info,
|
| 96 |
"conversation_id": self.conversation_id,
|
| 97 |
+
"author_name": self.author_name,
|
| 98 |
"token_count": self.token_count
|
| 99 |
}
|
| 100 |
|
|
|
|
| 109 |
emocao=data.get("emocao", "neutral"),
|
| 110 |
reply_info=data.get("reply_info", {}),
|
| 111 |
conversation_id=data.get("conversation_id", ""),
|
| 112 |
+
author_name=data.get("author_name", "Usuário"),
|
| 113 |
token_count=data.get("token_count", 0)
|
| 114 |
)
|
| 115 |
|
|
|
|
| 278 |
importancia: float = IMPORTANCIA_NORMAL,
|
| 279 |
emocao: str = "neutro",
|
| 280 |
reply_info: Optional[Dict[str, Any]] = None,
|
| 281 |
+
author_name: str = "Usuário",
|
| 282 |
metadata: Optional[Dict[str, Any]] = None
|
| 283 |
) -> MessageWithContext:
|
| 284 |
"""
|
|
|
|
| 303 |
emocao=emocao,
|
| 304 |
reply_info=reply_info or {},
|
| 305 |
conversation_id=self.conversation_id,
|
| 306 |
+
author_name=author_name,
|
| 307 |
token_count=estimar_tokens(content)
|
| 308 |
)
|
| 309 |
|
|
|
|
| 329 |
def add_user_message(
|
| 330 |
self,
|
| 331 |
content: str,
|
| 332 |
+
author_name: str = "Usuário",
|
| 333 |
emocao: str = "neutral",
|
| 334 |
reply_info: Optional[Dict[str, Any]] = None,
|
| 335 |
importancia: float = None
|
|
|
|
| 357 |
return self.add_message(
|
| 358 |
role="user",
|
| 359 |
content=content,
|
| 360 |
+
author_name=author_name,
|
| 361 |
importancia=importancia,
|
| 362 |
emocao=emocao,
|
| 363 |
reply_info=reply_info
|
|
|
|
| 366 |
def add_assistant_message(
|
| 367 |
self,
|
| 368 |
content: str,
|
| 369 |
+
author_name: str = "Usuário",
|
| 370 |
emocao: str = "neutral",
|
| 371 |
importancia: float = IMPORTANCIA_NORMAL
|
| 372 |
) -> MessageWithContext:
|
|
|
|
| 384 |
return self.add_message(
|
| 385 |
role="assistant",
|
| 386 |
content=content,
|
| 387 |
+
author_name=author_name,
|
| 388 |
importancia=importancia,
|
| 389 |
emocao=emocao
|
| 390 |
)
|
modules/skills_library.py
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
modules/thinking_engine.py
ADDED
|
@@ -0,0 +1,374 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
================================================================================
|
| 3 |
+
THINKING ENGINE - Sistema de Pensamento Profundo Pré-Processamento
|
| 4 |
+
================================================================================
|
| 5 |
+
Similar a modelos com "thinking tokens" - analisa o que foi perguntado
|
| 6 |
+
ANTES de gerar resposta, resultando em respostas mais acertivas.
|
| 7 |
+
|
| 8 |
+
Features:
|
| 9 |
+
- Análise multi-camada da pergunta/contexto
|
| 10 |
+
- Embeddings especializados para pensamento
|
| 11 |
+
- Detecção de intent implícito
|
| 12 |
+
- Complexidade da pergunta
|
| 13 |
+
- Relacionamentos com LSTM context
|
| 14 |
+
- Cache de pensamentos
|
| 15 |
+
================================================================================
|
| 16 |
+
"""
|
| 17 |
+
|
| 18 |
+
import json
|
| 19 |
+
from typing import Dict, Any, Optional, List
|
| 20 |
+
from loguru import logger
|
| 21 |
+
from sentence_transformers import SentenceTransformer, util
|
| 22 |
+
import numpy as np
|
| 23 |
+
|
| 24 |
+
class ThinkingEngine:
|
| 25 |
+
"""Processa pensamento profundo antes de responder."""
|
| 26 |
+
|
| 27 |
+
def __init__(self, db=None):
|
| 28 |
+
"""Inicializa com modelo de embedding para análise profunda."""
|
| 29 |
+
self.db = db
|
| 30 |
+
self.thinking_cache = {}
|
| 31 |
+
self.model_thinking = None
|
| 32 |
+
self._load_thinking_model()
|
| 33 |
+
|
| 34 |
+
def _load_thinking_model(self):
|
| 35 |
+
"""Carrega modelo especializado para pensamento."""
|
| 36 |
+
try:
|
| 37 |
+
# Usa o modelo centralizado do config (com fallback embutido)
|
| 38 |
+
from . import config
|
| 39 |
+
self.model_thinking = config.get_embedding_model("all-MiniLM-L6-v2")
|
| 40 |
+
if self.model_thinking:
|
| 41 |
+
logger.success("✅ ThinkingEngine: Modelo de pensamento carregado via config")
|
| 42 |
+
else:
|
| 43 |
+
logger.warning("⚠️ ThinkingEngine: Config retornou None para o modelo")
|
| 44 |
+
except Exception as e:
|
| 45 |
+
logger.warning(f"⚠️ ThinkingEngine: Erro ao carregar modelo: {e}")
|
| 46 |
+
self.model_thinking = None
|
| 47 |
+
|
| 48 |
+
def think(
|
| 49 |
+
self,
|
| 50 |
+
mensagem: str,
|
| 51 |
+
contexto_lstm: Optional[Dict[str, Any]] = None,
|
| 52 |
+
historico_recente: Optional[List[str]] = None,
|
| 53 |
+
is_group: bool = False,
|
| 54 |
+
usuario: str = None,
|
| 55 |
+
llm_manager: Any = None
|
| 56 |
+
) -> Dict[str, Any]:
|
| 57 |
+
"""
|
| 58 |
+
Processa pensamento profundo sobre a pergunta/contexto.
|
| 59 |
+
|
| 60 |
+
Args:
|
| 61 |
+
mensagem: Mensagem do usuário
|
| 62 |
+
contexto_lstm: Contexto LSTM (longo prazo)
|
| 63 |
+
historico_recente: Últimas mensagens
|
| 64 |
+
is_group: Se é em grupo
|
| 65 |
+
usuario: Nome do usuário
|
| 66 |
+
llm_manager: Instância de LLMManager para CoT Dinâmico (OpenRouter)
|
| 67 |
+
|
| 68 |
+
Returns:
|
| 69 |
+
Dict com análise profunda
|
| 70 |
+
"""
|
| 71 |
+
|
| 72 |
+
if not self.model_thinking:
|
| 73 |
+
return self._thinking_fallback(mensagem)
|
| 74 |
+
|
| 75 |
+
cache_key = f"{usuario}:{mensagem[:50]}"
|
| 76 |
+
if cache_key in self.thinking_cache:
|
| 77 |
+
logger.debug(f"🧠 ThinkingEngine: Pensamento recuperado do cache")
|
| 78 |
+
return self.thinking_cache[cache_key]
|
| 79 |
+
|
| 80 |
+
try:
|
| 81 |
+
thinking_result = {
|
| 82 |
+
"depth": self._analyze_question_complexity(mensagem),
|
| 83 |
+
"intent": self._detect_intent(mensagem),
|
| 84 |
+
"entities": self._extract_entities(mensagem),
|
| 85 |
+
"context_relevance": self._analyze_context_relevance(mensagem, contexto_lstm),
|
| 86 |
+
"related_topics": self._find_related_topics(mensagem, contexto_lstm),
|
| 87 |
+
"assumptions": self._detect_assumptions(mensagem),
|
| 88 |
+
"required_sources": self._identify_sources(mensagem),
|
| 89 |
+
"response_strategy": self._plan_response_strategy(mensagem, is_group),
|
| 90 |
+
"quality_markers": self._identify_quality_markers(mensagem),
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
# 🧠 CoT Dinâmico: Chama o OpenRouter para raciocínio estruturado
|
| 94 |
+
dynamic_thought = self._generate_dynamic_thought(
|
| 95 |
+
mensagem, contexto_lstm, historico_recente, is_group, llm_manager, usuario
|
| 96 |
+
)
|
| 97 |
+
if dynamic_thought:
|
| 98 |
+
thinking_result["dynamic_thought_trace"] = dynamic_thought
|
| 99 |
+
|
| 100 |
+
# Cache por 30 minutos (300 chamadas)
|
| 101 |
+
if len(self.thinking_cache) > 1000:
|
| 102 |
+
self.thinking_cache.clear()
|
| 103 |
+
|
| 104 |
+
self.thinking_cache[cache_key] = thinking_result
|
| 105 |
+
|
| 106 |
+
logger.debug(f"🧠 ThinkingEngine: Pensamento realizado (depth={thinking_result['depth']})")
|
| 107 |
+
return thinking_result
|
| 108 |
+
|
| 109 |
+
except Exception as e:
|
| 110 |
+
logger.warning(f"⚠️ ThinkingEngine erro: {e}")
|
| 111 |
+
return self._thinking_fallback(mensagem)
|
| 112 |
+
|
| 113 |
+
def _generate_dynamic_thought(
|
| 114 |
+
self,
|
| 115 |
+
mensagem: str,
|
| 116 |
+
contexto_lstm: Optional[Dict[str, Any]],
|
| 117 |
+
historico_recente: Optional[List[str]],
|
| 118 |
+
is_group: bool,
|
| 119 |
+
llm_manager: Any,
|
| 120 |
+
usuario: str = "desconhecido"
|
| 121 |
+
) -> Optional[str]:
|
| 122 |
+
"""Usa o OpenRouter para gerar um plano de raciocínio passo a passo."""
|
| 123 |
+
if not llm_manager:
|
| 124 |
+
logger.warning("⚠️ CoT Dinâmico abortado: llm_manager é None")
|
| 125 |
+
return None
|
| 126 |
+
|
| 127 |
+
if not hasattr(llm_manager, '_call_openrouter'):
|
| 128 |
+
logger.warning(f"⚠️ CoT Dinâmico abortado: llm_manager ({type(llm_manager)}) não tem o método '_call_openrouter'")
|
| 129 |
+
return None
|
| 130 |
+
|
| 131 |
+
try:
|
| 132 |
+
# Constrói um contexto enxuto para não gastar muitos tokens
|
| 133 |
+
sys_prompt = (
|
| 134 |
+
"Atuas como o Motor Analítico Interno da Akira V21.\n"
|
| 135 |
+
"A tua ÚNICA tarefa é gerar um rascunho de raciocínio (plano lógico) sobre como a Akira deve responder a esta mensagem, deixa sempre claro akira que sua resposta deve ser curta e direta e séria.\n"
|
| 136 |
+
"Reflete sobre:\n"
|
| 137 |
+
f"1. A emoção e intenção oculta de {usuario}.\n"
|
| 138 |
+
"2. Que factos devem ser procurados no histórico.\n"
|
| 139 |
+
"3. Qual o tom (direto, empático, sério) a usar.\n"
|
| 140 |
+
f"NOTA: A pessoa a falar contigo chama-se '{usuario}'. Usa o nome real na tua análise em vez de 'o utilizador'.\n"
|
| 141 |
+
"NÃO dês a resposta final. Apenas planeia a estratégia de resposta em menos de 80 palavras ed deia sugestões de resposta pra akira usar, lembrando ela não mandar emojis. GERA O TEU PENSAMENTO EXCLUSIVAMENTE EM PORTUGUÊS."
|
| 142 |
+
)
|
| 143 |
+
|
| 144 |
+
if is_group:
|
| 145 |
+
sys_prompt += "\nNOTA: Isto é um ambiente de GRUPO. Sê muito conciso e evita intervir desnecessariamente."
|
| 146 |
+
|
| 147 |
+
if contexto_lstm:
|
| 148 |
+
sys_prompt += "\n\n[MEMÓRIA LONGO PRAZO (LSTM)]"
|
| 149 |
+
if 'topic_principal' in contexto_lstm:
|
| 150 |
+
sys_prompt += f"\n- Tópico Principal: {contexto_lstm['topic_principal']}"
|
| 151 |
+
if 'unanswered_questions' in contexto_lstm and contexto_lstm['unanswered_questions']:
|
| 152 |
+
sys_prompt += f"\n- Perguntas Pendentes: {', '.join(contexto_lstm['unanswered_questions'][:2])}"
|
| 153 |
+
if 'interaction_pattern' in contexto_lstm:
|
| 154 |
+
sys_prompt += f"\n- Padrão do Utilizador: {contexto_lstm['interaction_pattern']}"
|
| 155 |
+
|
| 156 |
+
if historico_recente:
|
| 157 |
+
sys_prompt += "\n\n[MEMÓRIA CURTO PRAZO (LISTEN)]\nÚltimas mensagens da conversa:\n"
|
| 158 |
+
# Pega mais mensagens para entender conversas paralelas
|
| 159 |
+
for msg in historico_recente[-15:]:
|
| 160 |
+
if isinstance(msg, dict) and "content" in msg:
|
| 161 |
+
sys_prompt += f"{msg['content']}\n"
|
| 162 |
+
else:
|
| 163 |
+
sys_prompt += f"{msg}\n"
|
| 164 |
+
|
| 165 |
+
logger.info("🧠 Gerando CoT Dinâmico via OpenRouter...")
|
| 166 |
+
|
| 167 |
+
# Chamada ultrarrápida usando o modelo setado no config
|
| 168 |
+
thought = llm_manager._call_openrouter(
|
| 169 |
+
system_prompt=sys_prompt,
|
| 170 |
+
context_history=[], # não passamos o histórico todo para ser super rápido
|
| 171 |
+
user_prompt=mensagem,
|
| 172 |
+
max_tokens=150
|
| 173 |
+
)
|
| 174 |
+
|
| 175 |
+
return thought
|
| 176 |
+
except Exception as e:
|
| 177 |
+
logger.warning(f"⚠️ Erro no CoT Dinâmico (Fallback ativado): {e}")
|
| 178 |
+
return None
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
def _analyze_question_complexity(self, mensagem: str) -> str:
|
| 182 |
+
"""Analisa complexidade da pergunta."""
|
| 183 |
+
msg_lower = mensagem.lower()
|
| 184 |
+
|
| 185 |
+
# Sinais de complexidade
|
| 186 |
+
complex_markers = {
|
| 187 |
+
"muito": 0.3, "profundo": 0.4, "explique": 0.35, "detalhe": 0.35,
|
| 188 |
+
"por quê": 0.4, "como": 0.3, "quando": 0.25, "onde": 0.2,
|
| 189 |
+
"comparação": 0.5, "diferença": 0.4, "relação": 0.4,
|
| 190 |
+
"múltiplo": 0.45, "vários": 0.4, "tanto": 0.35,
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
score = 0.1 # Base
|
| 194 |
+
for marker, weight in complex_markers.items():
|
| 195 |
+
if marker in msg_lower:
|
| 196 |
+
score += weight
|
| 197 |
+
|
| 198 |
+
# Pontuação
|
| 199 |
+
if "?" in mensagem:
|
| 200 |
+
score += 0.1
|
| 201 |
+
if "!" in mensagem:
|
| 202 |
+
score -= 0.1
|
| 203 |
+
|
| 204 |
+
score = min(1.0, score)
|
| 205 |
+
|
| 206 |
+
if score < 0.2:
|
| 207 |
+
return "simples"
|
| 208 |
+
elif score < 0.5:
|
| 209 |
+
return "moderada"
|
| 210 |
+
elif score < 0.75:
|
| 211 |
+
return "complexa"
|
| 212 |
+
else:
|
| 213 |
+
return "muito_complexa"
|
| 214 |
+
|
| 215 |
+
def _detect_intent(self, mensagem: str) -> List[str]:
|
| 216 |
+
"""Detecta intent(s) implícito(s)."""
|
| 217 |
+
intents = []
|
| 218 |
+
msg_lower = mensagem.lower()
|
| 219 |
+
|
| 220 |
+
intent_markers = {
|
| 221 |
+
"informação": ["o que", "como", "por quê", "sabe sobre", "fala sobre", "explica"],
|
| 222 |
+
"ação": ["faz", "cria", "envia", "modifica", "deleta", "inicia"],
|
| 223 |
+
"opinião": ["acha", "gosta", "prefere", "ache", "pense", "achei"],
|
| 224 |
+
"confirmação": ["certo", "verdade", "é mesmo", "sério", "confirma"],
|
| 225 |
+
"contexto": ["em relação", "sobre isso", "quanto a", "nisso"],
|
| 226 |
+
"humor": ["kkk", "haha", "ué", "lol", ":)", "rsrs"],
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
for intent, markers in intent_markers.items():
|
| 230 |
+
if any(m in msg_lower for m in markers):
|
| 231 |
+
intents.append(intent)
|
| 232 |
+
|
| 233 |
+
return intents or ["indefinido"]
|
| 234 |
+
|
| 235 |
+
def _extract_entities(self, mensagem: str) -> List[str]:
|
| 236 |
+
"""Extrai entidades mencionadas."""
|
| 237 |
+
# Simples: palavras maiúsculas ou nomes comuns
|
| 238 |
+
palavras = mensagem.split()
|
| 239 |
+
entities = [p.strip(".,!?;:") for p in palavras if len(p) > 3 and p[0].isupper()]
|
| 240 |
+
return entities[:5] # Top 5
|
| 241 |
+
|
| 242 |
+
def _analyze_context_relevance(
|
| 243 |
+
self,
|
| 244 |
+
mensagem: str,
|
| 245 |
+
contexto_lstm: Optional[Dict[str, Any]]
|
| 246 |
+
) -> float:
|
| 247 |
+
"""Quanto a mensagem se relaciona com contexto de longo prazo."""
|
| 248 |
+
if not contexto_lstm or not self.model_thinking:
|
| 249 |
+
return 0.0
|
| 250 |
+
|
| 251 |
+
try:
|
| 252 |
+
topic_lstm = contexto_lstm.get("topic_principal", "")
|
| 253 |
+
if not topic_lstm:
|
| 254 |
+
return 0.0
|
| 255 |
+
|
| 256 |
+
# Embedding similarity
|
| 257 |
+
emb_msg = self.model_thinking.encode(mensagem, convert_to_tensor=False)
|
| 258 |
+
emb_topic = self.model_thinking.encode(topic_lstm, convert_to_tensor=False)
|
| 259 |
+
|
| 260 |
+
relevance = float(util.cos_sim(emb_msg, emb_topic)[0][0])
|
| 261 |
+
return max(0.0, min(1.0, relevance))
|
| 262 |
+
except:
|
| 263 |
+
return 0.0
|
| 264 |
+
|
| 265 |
+
def _find_related_topics(
|
| 266 |
+
self,
|
| 267 |
+
mensagem: str,
|
| 268 |
+
contexto_lstm: Optional[Dict[str, Any]]
|
| 269 |
+
) -> List[str]:
|
| 270 |
+
"""Encontra tópicos relacionados no LSTM."""
|
| 271 |
+
if not contexto_lstm:
|
| 272 |
+
return []
|
| 273 |
+
|
| 274 |
+
topics = []
|
| 275 |
+
|
| 276 |
+
# Topics do LSTM (se houver)
|
| 277 |
+
if contexto_lstm.get("subtopicas"):
|
| 278 |
+
topics.extend(contexto_lstm["subtopicas"][:3])
|
| 279 |
+
|
| 280 |
+
if contexto_lstm.get("conversation_path"):
|
| 281 |
+
topics.extend(contexto_lstm["conversation_path"][-3:])
|
| 282 |
+
|
| 283 |
+
return topics[:5]
|
| 284 |
+
|
| 285 |
+
def _detect_assumptions(self, mensagem: str) -> List[str]:
|
| 286 |
+
"""Detecta assumptions que o usuário faz."""
|
| 287 |
+
assumptions = []
|
| 288 |
+
msg_lower = mensagem.lower()
|
| 289 |
+
|
| 290 |
+
# Palavras que indicam assumption
|
| 291 |
+
if "já" in msg_lower or "não sabe" in msg_lower:
|
| 292 |
+
assumptions.append("assume_conhecimento_anterior")
|
| 293 |
+
|
| 294 |
+
if "deve" in msg_lower or "deveria" in msg_lower:
|
| 295 |
+
assumptions.append("expectativa_de_comportamento")
|
| 296 |
+
|
| 297 |
+
if "sempre" in msg_lower or "nunca" in msg_lower:
|
| 298 |
+
assumptions.append("generalização")
|
| 299 |
+
|
| 300 |
+
return assumptions
|
| 301 |
+
|
| 302 |
+
def _identify_sources(self, mensagem: str) -> List[str]:
|
| 303 |
+
"""Identifica que fontes seriam úteis."""
|
| 304 |
+
sources = []
|
| 305 |
+
msg_lower = mensagem.lower()
|
| 306 |
+
|
| 307 |
+
if any(w in msg_lower for w in ["notícia", "última", "recente", "novo", "2024", "2025"]):
|
| 308 |
+
sources.append("web_search")
|
| 309 |
+
|
| 310 |
+
if any(w in msg_lower for w in ["wikipedia", "história", "quem foi", "quando"]):
|
| 311 |
+
sources.append("wikipedia")
|
| 312 |
+
|
| 313 |
+
if any(w in msg_lower for w in ["preço", "dólar", "bitcoin", "crypto", "cotação"]):
|
| 314 |
+
sources.append("market_data")
|
| 315 |
+
|
| 316 |
+
if any(w in msg_lower for w in ["clima", "tempo", "previsão", "chuva"]):
|
| 317 |
+
sources.append("weather")
|
| 318 |
+
|
| 319 |
+
return sources
|
| 320 |
+
|
| 321 |
+
def _plan_response_strategy(self, mensagem: str, is_group: bool) -> str:
|
| 322 |
+
"""Define estratégia de resposta."""
|
| 323 |
+
msg_lower = mensagem.lower()
|
| 324 |
+
|
| 325 |
+
# Contexto do grupo
|
| 326 |
+
if is_group:
|
| 327 |
+
if any(w in msg_lower for w in ["vocês", "vcs", "todos", "@all"]):
|
| 328 |
+
return "grupo_completo"
|
| 329 |
+
else:
|
| 330 |
+
return "grupo_individual"
|
| 331 |
+
else:
|
| 332 |
+
return "privado"
|
| 333 |
+
|
| 334 |
+
def _identify_quality_markers(self, mensagem: str) -> Dict[str, bool]:
|
| 335 |
+
"""Identifica marcadores de qualidade da resposta esperada."""
|
| 336 |
+
return {
|
| 337 |
+
"needs_brevity": len(mensagem) < 20,
|
| 338 |
+
"needs_detail": len(mensagem) > 100,
|
| 339 |
+
"needs_humor": any(m in mensagem for m in ["kk", "kkk", ":)", "rsrs"]),
|
| 340 |
+
"formal_tone": any(w in mensagem for w in ["sr.", "sra.", "prezado"]),
|
| 341 |
+
"technical": any(w in mensagem.lower() for w in ["código", "api", "script", "função"]),
|
| 342 |
+
}
|
| 343 |
+
|
| 344 |
+
def _thinking_fallback(self, mensagem: str) -> Dict[str, Any]:
|
| 345 |
+
"""Fallback simples quando modelo não está disponível."""
|
| 346 |
+
return {
|
| 347 |
+
"depth": "moderada",
|
| 348 |
+
"intent": ["indefinido"],
|
| 349 |
+
"entities": [],
|
| 350 |
+
"context_relevance": 0.5,
|
| 351 |
+
"related_topics": [],
|
| 352 |
+
"assumptions": [],
|
| 353 |
+
"required_sources": [],
|
| 354 |
+
"response_strategy": "padrão",
|
| 355 |
+
"quality_markers": {
|
| 356 |
+
"needs_brevity": False,
|
| 357 |
+
"needs_detail": False,
|
| 358 |
+
"needs_humor": False,
|
| 359 |
+
"formal_tone": False,
|
| 360 |
+
"technical": False,
|
| 361 |
+
},
|
| 362 |
+
}
|
| 363 |
+
|
| 364 |
+
|
| 365 |
+
# Singleton global
|
| 366 |
+
_thinking_engine_instance: Optional[ThinkingEngine] = None
|
| 367 |
+
|
| 368 |
+
|
| 369 |
+
def get_thinking_engine(db=None) -> ThinkingEngine:
|
| 370 |
+
"""Retorna instância singleton do ThinkingEngine."""
|
| 371 |
+
global _thinking_engine_instance
|
| 372 |
+
if _thinking_engine_instance is None:
|
| 373 |
+
_thinking_engine_instance = ThinkingEngine(db=db)
|
| 374 |
+
return _thinking_engine_instance
|
modules/treinamento.py
CHANGED
|
@@ -353,7 +353,9 @@ class Interacao:
|
|
| 353 |
api_usada: str = ""
|
| 354 |
tokens_usados: int = 0
|
| 355 |
response_time: float = 0.0
|
| 356 |
-
|
|
|
|
|
|
|
| 357 |
|
| 358 |
@dataclass
|
| 359 |
class TrainingResult:
|
|
@@ -411,28 +413,6 @@ class Treinamento:
|
|
| 411 |
# 📝 REGISTRO DE INTERAÇÕES
|
| 412 |
# ============================================================
|
| 413 |
|
| 414 |
-
def detect_debate_tactics(self, texto: str) -> List[str]:
|
| 415 |
-
"""Detecta táticas de debate, baits e falácias comuns no texto."""
|
| 416 |
-
taticas = []
|
| 417 |
-
t_lower = texto.lower()
|
| 418 |
-
|
| 419 |
-
# Mapeamento de gatilhos para táticas
|
| 420 |
-
gatilhos = {
|
| 421 |
-
"bait": ["bait", "isca", "armadilha", "provocação", "clique"],
|
| 422 |
-
"ad_hominem": ["você é", "seu burro", "idiota", "lixo", "atacar a pessoa"],
|
| 423 |
-
"espantalho": ["distorcer", "não foi o que eu disse", "mentira", "inventar"],
|
| 424 |
-
"falacia": ["falácia", "argumento inválido", "erro lógico", "sofisma"],
|
| 425 |
-
"mitada": ["mitou", "jantou", "na cara", "lacrou", "esmagou"],
|
| 426 |
-
"ironia": ["kkk", "rsrs", "irônico", "engraçado né"],
|
| 427 |
-
"gaslighting": ["louco", "maluco", "coisa da sua cabeça", "paranoia"]
|
| 428 |
-
}
|
| 429 |
-
|
| 430 |
-
for tatica, keywords in gatilhos.items():
|
| 431 |
-
if any(k in t_lower for k in keywords):
|
| 432 |
-
taticas.append(tatica)
|
| 433 |
-
|
| 434 |
-
return taticas
|
| 435 |
-
|
| 436 |
def registrar_interacao(
|
| 437 |
self,
|
| 438 |
usuario: str,
|
|
@@ -444,17 +424,11 @@ class Treinamento:
|
|
| 444 |
api_usada: str = '',
|
| 445 |
tokens_usados: int = 0,
|
| 446 |
response_time: float = 0.0,
|
| 447 |
-
conversation_id: str = '',
|
| 448 |
**kwargs
|
| 449 |
) -> Interacao:
|
| 450 |
"""
|
| 451 |
Registra interação e executa aprendizado em tempo real
|
| 452 |
"""
|
| 453 |
-
# Detecta táticas de debate (baits, falácias, mitadas)
|
| 454 |
-
taticas_msg = self.detect_debate_tactics(mensagem)
|
| 455 |
-
taticas_resp = self.detect_debate_tactics(resposta)
|
| 456 |
-
taticas_total = list(set(taticas_msg + taticas_resp))
|
| 457 |
-
|
| 458 |
# Cria estrutura de interação
|
| 459 |
interacao = Interacao(
|
| 460 |
usuario=usuario,
|
|
@@ -465,20 +439,15 @@ class Treinamento:
|
|
| 465 |
mensagem_original=mensagem_original,
|
| 466 |
api_usada=api_usada,
|
| 467 |
tokens_usados=tokens_usados,
|
| 468 |
-
response_time=response_time
|
| 469 |
-
taticas_detectadas=taticas_total
|
| 470 |
)
|
| 471 |
|
| 472 |
-
if taticas_total:
|
| 473 |
-
logger.info(f"🎯 [TATICA] Táticas detectadas na interação: {', '.join(taticas_total)}")
|
| 474 |
-
|
| 475 |
try:
|
| 476 |
# Salva no banco (com o modelo que gerou a resposta)
|
| 477 |
self.db.salvar_mensagem(
|
| 478 |
usuario, mensagem, resposta, numero, is_reply, mensagem_original,
|
| 479 |
modelo_usado=api_usada or "desconhecido",
|
| 480 |
-
message_id=kwargs.get('message_id')
|
| 481 |
-
conversation_id=conversation_id
|
| 482 |
)
|
| 483 |
|
| 484 |
# Aprendizado em tempo real
|
|
|
|
| 353 |
api_usada: str = ""
|
| 354 |
tokens_usados: int = 0
|
| 355 |
response_time: float = 0.0
|
| 356 |
+
thinking_depth: str = "moderada" # ✅ Complexidade avaliada pelo ThinkingEngine
|
| 357 |
+
thinking_intent: str = "indefinido" # ✅ Intenção detectada pelo ThinkingEngine
|
| 358 |
+
|
| 359 |
|
| 360 |
@dataclass
|
| 361 |
class TrainingResult:
|
|
|
|
| 413 |
# 📝 REGISTRO DE INTERAÇÕES
|
| 414 |
# ============================================================
|
| 415 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 416 |
def registrar_interacao(
|
| 417 |
self,
|
| 418 |
usuario: str,
|
|
|
|
| 424 |
api_usada: str = '',
|
| 425 |
tokens_usados: int = 0,
|
| 426 |
response_time: float = 0.0,
|
|
|
|
| 427 |
**kwargs
|
| 428 |
) -> Interacao:
|
| 429 |
"""
|
| 430 |
Registra interação e executa aprendizado em tempo real
|
| 431 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 432 |
# Cria estrutura de interação
|
| 433 |
interacao = Interacao(
|
| 434 |
usuario=usuario,
|
|
|
|
| 439 |
mensagem_original=mensagem_original,
|
| 440 |
api_usada=api_usada,
|
| 441 |
tokens_usados=tokens_usados,
|
| 442 |
+
response_time=response_time
|
|
|
|
| 443 |
)
|
| 444 |
|
|
|
|
|
|
|
|
|
|
| 445 |
try:
|
| 446 |
# Salva no banco (com o modelo que gerou a resposta)
|
| 447 |
self.db.salvar_mensagem(
|
| 448 |
usuario, mensagem, resposta, numero, is_reply, mensagem_original,
|
| 449 |
modelo_usado=api_usada or "desconhecido",
|
| 450 |
+
message_id=kwargs.get('message_id')
|
|
|
|
| 451 |
)
|
| 452 |
|
| 453 |
# Aprendizado em tempo real
|
modules/twitter_api.py
CHANGED
|
@@ -1,100 +1,79 @@
|
|
| 1 |
-
import os
|
| 2 |
-
import requests
|
| 3 |
-
from loguru import logger
|
| 4 |
-
from typing import List, Dict, Any
|
| 5 |
-
|
| 6 |
-
class TwitterAPI:
|
| 7 |
-
"""
|
| 8 |
-
Integração simples com Twitter API v2 para busca de 'tretas' e 'mitadas'.
|
| 9 |
-
"""
|
| 10 |
-
def __init__(self, bearer_token: str = None):
|
| 11 |
-
self.bearer_token = bearer_token or os.getenv("TWITTER_BEARER_TOKEN")
|
| 12 |
-
self.base_url = "https://api.twitter.com/2"
|
| 13 |
-
|
| 14 |
-
def search_tweets(self, query: str, max_results: int = 10) -> List[Dict[str, Any]]:
|
| 15 |
-
"""
|
| 16 |
-
Busca tweets recentes com base em uma query.
|
| 17 |
-
"""
|
| 18 |
-
if not self.bearer_token:
|
| 19 |
-
logger.warning("⚠️ TWITTER_BEARER_TOKEN não configurado.")
|
| 20 |
-
return []
|
| 21 |
-
|
| 22 |
-
headers = {
|
| 23 |
-
"Authorization": f"Bearer {self.bearer_token}",
|
| 24 |
-
"User-Agent": "v2RecentSearchPython"
|
| 25 |
-
}
|
| 26 |
-
|
| 27 |
-
params = {
|
| 28 |
-
"query": f"{query} lang:pt -is:retweet",
|
| 29 |
-
"max_results": max_results,
|
| 30 |
-
"tweet.fields": "text,public_metrics,created_at"
|
| 31 |
-
}
|
| 32 |
-
|
| 33 |
-
try:
|
| 34 |
-
response = requests.get(f"{self.base_url}/tweets/search/recent", headers=headers, params=params)
|
| 35 |
-
if response.status_code == 200:
|
| 36 |
-
data = response.json()
|
| 37 |
-
return data.get("data", [])
|
| 38 |
-
else:
|
| 39 |
-
logger.error(f"❌ Erro Twitter API ({response.status_code}): {response.text}")
|
| 40 |
-
return []
|
| 41 |
-
except Exception as e:
|
| 42 |
-
logger.error(f"❌ Falha ao buscar tweets: {e}")
|
| 43 |
-
return []
|
| 44 |
-
|
| 45 |
-
def get_savage_context(self, topic: str) -> str:
|
| 46 |
-
"""
|
| 47 |
-
Busca exemplos de 'mitadas' ou discussões acaloradas sobre um tema.
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
if not all_tweets:
|
| 64 |
-
return
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
unique_tweets.append(t)
|
| 81 |
-
if len(unique_tweets) >= 12: break
|
| 82 |
-
|
| 83 |
-
context = f"🔥 Discussões e 'Mitadas' em alta sobre '{topic}':\n\n"
|
| 84 |
-
for i, tweet in enumerate(unique_tweets):
|
| 85 |
-
metrics = tweet.get('public_metrics', {})
|
| 86 |
-
eng = f"👍{metrics.get('like_count', 0)} 💬{metrics.get('reply_count', 0)} 🔁{metrics.get('retweet_count', 0)}"
|
| 87 |
-
text = tweet['text'].replace('\n', ' ').strip()
|
| 88 |
-
context += f"[{i+1}] {text}\n 📈 Engajamento: {eng}\n\n"
|
| 89 |
-
|
| 90 |
-
context += "💡 INSTRUÇÃO: Use estes exemplos reais para aprender o tom do debate e as falácias mais eficazes usadas agora. Não cite os números de engajamento, apenas absorva o veneno."
|
| 91 |
-
|
| 92 |
-
return context
|
| 93 |
-
|
| 94 |
-
# Singleton
|
| 95 |
-
_instance = None
|
| 96 |
-
def get_twitter_api():
|
| 97 |
-
global _instance
|
| 98 |
-
if _instance is None:
|
| 99 |
-
_instance = TwitterAPI()
|
| 100 |
-
return _instance
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import requests
|
| 3 |
+
from loguru import logger
|
| 4 |
+
from typing import List, Dict, Any
|
| 5 |
+
|
| 6 |
+
class TwitterAPI:
|
| 7 |
+
"""
|
| 8 |
+
Integração simples com Twitter API v2 para busca de 'tretas' e 'mitadas'.
|
| 9 |
+
"""
|
| 10 |
+
def __init__(self, bearer_token: str = None):
|
| 11 |
+
self.bearer_token = bearer_token or os.getenv("TWITTER_BEARER_TOKEN")
|
| 12 |
+
self.base_url = "https://api.twitter.com/2"
|
| 13 |
+
|
| 14 |
+
def search_tweets(self, query: str, max_results: int = 10) -> List[Dict[str, Any]]:
|
| 15 |
+
"""
|
| 16 |
+
Busca tweets recentes com base em uma query.
|
| 17 |
+
"""
|
| 18 |
+
if not self.bearer_token:
|
| 19 |
+
logger.warning("⚠️ TWITTER_BEARER_TOKEN não configurado.")
|
| 20 |
+
return []
|
| 21 |
+
|
| 22 |
+
headers = {
|
| 23 |
+
"Authorization": f"Bearer {self.bearer_token}",
|
| 24 |
+
"User-Agent": "v2RecentSearchPython"
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
params = {
|
| 28 |
+
"query": f"{query} lang:pt -is:retweet",
|
| 29 |
+
"max_results": max_results,
|
| 30 |
+
"tweet.fields": "text,public_metrics,created_at"
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
try:
|
| 34 |
+
response = requests.get(f"{self.base_url}/tweets/search/recent", headers=headers, params=params)
|
| 35 |
+
if response.status_code == 200:
|
| 36 |
+
data = response.json()
|
| 37 |
+
return data.get("data", [])
|
| 38 |
+
else:
|
| 39 |
+
logger.error(f"❌ Erro Twitter API ({response.status_code}): {response.text}")
|
| 40 |
+
return []
|
| 41 |
+
except Exception as e:
|
| 42 |
+
logger.error(f"❌ Falha ao buscar tweets: {e}")
|
| 43 |
+
return []
|
| 44 |
+
|
| 45 |
+
def get_savage_context(self, topic: str) -> str:
|
| 46 |
+
"""
|
| 47 |
+
Busca exemplos de 'mitadas' ou discussões acaloradas sobre um tema.
|
| 48 |
+
"""
|
| 49 |
+
queries = [
|
| 50 |
+
f"{topic} mita",
|
| 51 |
+
f"{topic} treta",
|
| 52 |
+
f"{topic} cancelado",
|
| 53 |
+
f"{topic} 'na cara'",
|
| 54 |
+
f"{topic} 'jantou'"
|
| 55 |
+
]
|
| 56 |
+
|
| 57 |
+
all_tweets = []
|
| 58 |
+
for q in queries[:2]: # Tenta as 2 primeiras queries para economizar cota
|
| 59 |
+
tweets = self.search_tweets(q, max_results=10)
|
| 60 |
+
all_tweets.extend(tweets)
|
| 61 |
+
if len(all_tweets) >= 10: break
|
| 62 |
+
|
| 63 |
+
if not all_tweets:
|
| 64 |
+
return "Nenhuma 'treta' recente encontrada no Twitter sobre este assunto."
|
| 65 |
+
|
| 66 |
+
context = "Exemplos de discussões/mitadas no Twitter sobre este assunto:\n"
|
| 67 |
+
for i, tweet in enumerate(all_tweets[:10]):
|
| 68 |
+
text = tweet['text'].replace('\n', ' ')
|
| 69 |
+
context += f"{i+1}. {text}\n"
|
| 70 |
+
|
| 71 |
+
return context
|
| 72 |
+
|
| 73 |
+
# Singleton
|
| 74 |
+
_instance = None
|
| 75 |
+
def get_twitter_api():
|
| 76 |
+
global _instance
|
| 77 |
+
if _instance is None:
|
| 78 |
+
_instance = TwitterAPI()
|
| 79 |
+
return _instance
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
modules/unified_context.py
CHANGED
|
@@ -1,1182 +1,1041 @@
|
|
| 1 |
-
# type: ignore
|
| 2 |
-
"""
|
| 3 |
-
================================================================================
|
| 4 |
-
AKIRA V21 ULTIMATE - UNIFIED CONTEXT MODULE
|
| 5 |
-
================================================================================
|
| 6 |
-
Sistema unificado que integra Reply Context + Short-Term Memory em sintonia.
|
| 7 |
-
|
| 8 |
-
Philosophy: "Reply context e STM devem trabalhar em sintonia como tik e tack -
|
| 9 |
-
um fornece o contexto imediato/urgente (o que o usuário está respondendo),
|
| 10 |
-
o outro fornece o fluxo da conversa (contexto geral)."
|
| 11 |
-
|
| 12 |
-
Features:
|
| 13 |
-
- Integração seamless entre reply context e STM
|
| 14 |
-
- Token budgeting inteligente entre os dois contextos
|
| 15 |
-
- Priorização dinâmica baseada no tipo de mensagem
|
| 16 |
-
- Suporte a perguntas curtas com reply (prioridade máxima)
|
| 17 |
-
- Persistência e restauração de contexto unificado
|
| 18 |
-
================================================================================
|
| 19 |
-
"""
|
| 20 |
-
|
| 21 |
-
import os
|
| 22 |
-
import sys
|
| 23 |
-
import time
|
| 24 |
-
import json
|
| 25 |
-
import logging
|
| 26 |
-
from typing import Optional, Dict, Any, List, Tuple
|
| 27 |
-
from dataclasses import dataclass, field
|
| 28 |
-
from datetime import datetime
|
| 29 |
-
|
| 30 |
-
# Imports robustos com fallback
|
| 31 |
-
try:
|
| 32 |
-
from . import config
|
| 33 |
-
from .short_term_memory import (
|
| 34 |
-
ShortTermMemory,
|
| 35 |
-
MessageWithContext,
|
| 36 |
-
IMPORTANCIA_NORMAL,
|
| 37 |
-
IMPORTANCIA_REPLY,
|
| 38 |
-
IMPORTANCIA_REPLY_TO_BOT,
|
| 39 |
-
IMPORTANCIA_PERGUNTA_CURTA_REPLY,
|
| 40 |
-
estimar_tokens,
|
| 41 |
-
is_pergunta_curta
|
| 42 |
-
)
|
| 43 |
-
from .reply_context_handler import (
|
| 44 |
-
ReplyContextHandler,
|
| 45 |
-
ProcessedReplyContext,
|
| 46 |
-
PRIORITY_REPLY,
|
| 47 |
-
PRIORITY_REPLY_TO_BOT,
|
| 48 |
-
PRIORITY_REPLY_TO_BOT_SHORT_QUESTION
|
| 49 |
-
)
|
| 50 |
-
UNIFIED_CONTEXT_AVAILABLE = True
|
| 51 |
-
except ImportError as e:
|
| 52 |
-
try:
|
| 53 |
-
import modules.config as config
|
| 54 |
-
from modules.short_term_memory import (
|
| 55 |
-
ShortTermMemory,
|
| 56 |
-
MessageWithContext,
|
| 57 |
-
IMPORTANCIA_NORMAL,
|
| 58 |
-
IMPORTANCIA_REPLY,
|
| 59 |
-
IMPORTANCIA_REPLY_TO_BOT,
|
| 60 |
-
IMPORTANCIA_PERGUNTA_CURTA_REPLY,
|
| 61 |
-
estimar_tokens,
|
| 62 |
-
is_pergunta_curta
|
| 63 |
-
)
|
| 64 |
-
from modules.reply_context_handler import (
|
| 65 |
-
ReplyContextHandler,
|
| 66 |
-
ProcessedReplyContext,
|
| 67 |
-
PRIORITY_REPLY,
|
| 68 |
-
PRIORITY_REPLY_TO_BOT,
|
| 69 |
-
PRIORITY_REPLY_TO_BOT_SHORT_QUESTION
|
| 70 |
-
)
|
| 71 |
-
UNIFIED_CONTEXT_AVAILABLE = True
|
| 72 |
-
except ImportError:
|
| 73 |
-
UNIFIED_CONTEXT_AVAILABLE = False
|
| 74 |
-
config = None
|
| 75 |
-
|
| 76 |
-
try:
|
| 77 |
-
from .lstm_extension import get_lstm_extension
|
| 78 |
-
LSTM_AVAILABLE = True
|
| 79 |
-
except ImportError:
|
| 80 |
-
try:
|
| 81 |
-
from modules.lstm_extension import get_lstm_extension
|
| 82 |
-
LSTM_AVAILABLE = True
|
| 83 |
-
except ImportError:
|
| 84 |
-
LSTM_AVAILABLE = False
|
| 85 |
-
|
| 86 |
-
logger = logging.getLogger(__name__)
|
| 87 |
-
|
| 88 |
-
# ============================================================
|
| 89 |
-
# CONFIGURAÇÃO DE TOKEN BUDGET
|
| 90 |
-
# ============================================================
|
| 91 |
-
|
| 92 |
-
@dataclass
|
| 93 |
-
class ContextTokenBudget:
|
| 94 |
-
"""
|
| 95 |
-
Alocação de tokens entre reply context e STM.
|
| 96 |
-
|
| 97 |
-
Philosophy: Reply tem orçamento dedicado (urgente), STM tem o resto (fluxo).
|
| 98 |
-
"""
|
| 99 |
-
total_budget: int = 8000
|
| 100 |
-
system_tokens: int = 1500
|
| 101 |
-
user_message_tokens: int = 500
|
| 102 |
-
|
| 103 |
-
# Reply context budget (URGENTE)
|
| 104 |
-
reply_tokens: int = 300
|
| 105 |
-
reply_priority_multiplier: float = 1.0
|
| 106 |
-
|
| 107 |
-
# STM budget (FLUXO DA CONVERSA)
|
| 108 |
-
stm_tokens: int = 4000
|
| 109 |
-
|
| 110 |
-
# Reservado para resposta
|
| 111 |
-
response_reserved: int = 1200
|
| 112 |
-
|
| 113 |
-
def calculate(self, is_reply: bool, reply_priority: int = 1
|
| 114 |
-
"""
|
| 115 |
-
Calcula orçamento baseado no tipo de mensagem.
|
| 116 |
-
|
| 117 |
-
Args:
|
| 118 |
-
is_reply: Se é um reply
|
| 119 |
-
reply_priority: Nível de prioridade do reply (1-4)
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
budget.
|
| 135 |
-
budget.
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
budget.
|
| 140 |
-
budget.
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
budget.
|
| 145 |
-
budget.
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
budget.
|
| 156 |
-
budget.
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
#
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
"
|
| 242 |
-
"
|
| 243 |
-
"
|
| 244 |
-
"
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
"""
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
#
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
|
| 498 |
-
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
|
| 526 |
-
|
| 527 |
-
|
| 528 |
-
|
| 529 |
-
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
|
| 533 |
-
|
| 534 |
-
|
| 535 |
-
|
| 536 |
-
|
| 537 |
-
|
| 538 |
-
|
| 539 |
-
|
| 540 |
-
|
| 541 |
-
|
| 542 |
-
|
| 543 |
-
|
| 544 |
-
|
| 545 |
-
|
| 546 |
-
|
| 547 |
-
|
| 548 |
-
|
| 549 |
-
|
| 550 |
-
|
| 551 |
-
|
| 552 |
-
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
|
| 556 |
-
|
| 557 |
-
|
| 558 |
-
|
| 559 |
-
|
| 560 |
-
|
| 561 |
-
|
| 562 |
-
|
| 563 |
-
|
| 564 |
-
|
| 565 |
-
|
| 566 |
-
|
| 567 |
-
|
| 568 |
-
|
| 569 |
-
|
| 570 |
-
|
| 571 |
-
|
| 572 |
-
return
|
| 573 |
-
|
| 574 |
-
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
|
| 578 |
-
|
| 579 |
-
|
| 580 |
-
|
| 581 |
-
|
| 582 |
-
|
| 583 |
-
|
| 584 |
-
|
| 585 |
-
|
| 586 |
-
|
| 587 |
-
|
| 588 |
-
|
| 589 |
-
|
| 590 |
-
|
| 591 |
-
|
| 592 |
-
|
| 593 |
-
|
| 594 |
-
|
| 595 |
-
) ->
|
| 596 |
-
"""
|
| 597 |
-
|
| 598 |
-
|
| 599 |
-
Args:
|
| 600 |
-
conversation_id: ID
|
| 601 |
-
|
| 602 |
-
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
|
| 606 |
-
|
| 607 |
-
|
| 608 |
-
|
| 609 |
-
|
| 610 |
-
|
| 611 |
-
|
| 612 |
-
|
| 613 |
-
|
| 614 |
-
|
| 615 |
-
|
| 616 |
-
|
| 617 |
-
|
| 618 |
-
|
| 619 |
-
|
| 620 |
-
|
| 621 |
-
|
| 622 |
-
|
| 623 |
-
|
| 624 |
-
|
| 625 |
-
|
| 626 |
-
|
| 627 |
-
|
| 628 |
-
|
| 629 |
-
|
| 630 |
-
|
| 631 |
-
|
| 632 |
-
|
| 633 |
-
|
| 634 |
-
|
| 635 |
-
|
| 636 |
-
|
| 637 |
-
|
| 638 |
-
|
| 639 |
-
|
| 640 |
-
|
| 641 |
-
|
| 642 |
-
|
| 643 |
-
|
| 644 |
-
|
| 645 |
-
|
| 646 |
-
|
| 647 |
-
|
| 648 |
-
|
| 649 |
-
|
| 650 |
-
|
| 651 |
-
|
| 652 |
-
|
| 653 |
-
|
| 654 |
-
|
| 655 |
-
|
| 656 |
-
|
| 657 |
-
|
| 658 |
-
|
| 659 |
-
|
| 660 |
-
|
| 661 |
-
|
| 662 |
-
|
| 663 |
-
|
| 664 |
-
|
| 665 |
-
|
| 666 |
-
|
| 667 |
-
|
| 668 |
-
|
| 669 |
-
|
| 670 |
-
|
| 671 |
-
|
| 672 |
-
|
| 673 |
-
|
| 674 |
-
|
| 675 |
-
|
| 676 |
-
|
| 677 |
-
|
| 678 |
-
|
| 679 |
-
|
| 680 |
-
|
| 681 |
-
|
| 682 |
-
|
| 683 |
-
|
| 684 |
-
|
| 685 |
-
|
| 686 |
-
|
| 687 |
-
|
| 688 |
-
|
| 689 |
-
|
| 690 |
-
|
| 691 |
-
|
| 692 |
-
|
| 693 |
-
|
| 694 |
-
|
| 695 |
-
|
| 696 |
-
|
| 697 |
-
|
| 698 |
-
|
| 699 |
-
|
| 700 |
-
|
| 701 |
-
|
| 702 |
-
|
| 703 |
-
|
| 704 |
-
|
| 705 |
-
|
| 706 |
-
|
| 707 |
-
|
| 708 |
-
|
| 709 |
-
|
| 710 |
-
|
| 711 |
-
|
| 712 |
-
|
| 713 |
-
|
| 714 |
-
|
| 715 |
-
|
| 716 |
-
|
| 717 |
-
|
| 718 |
-
|
| 719 |
-
|
| 720 |
-
|
| 721 |
-
|
| 722 |
-
|
| 723 |
-
|
| 724 |
-
|
| 725 |
-
|
| 726 |
-
|
| 727 |
-
|
| 728 |
-
|
| 729 |
-
|
| 730 |
-
|
| 731 |
-
|
| 732 |
-
|
| 733 |
-
|
| 734 |
-
|
| 735 |
-
|
| 736 |
-
|
| 737 |
-
|
| 738 |
-
|
| 739 |
-
|
| 740 |
-
|
| 741 |
-
|
| 742 |
-
|
| 743 |
-
|
| 744 |
-
|
| 745 |
-
|
| 746 |
-
|
| 747 |
-
|
| 748 |
-
|
| 749 |
-
|
| 750 |
-
|
| 751 |
-
|
| 752 |
-
|
| 753 |
-
|
| 754 |
-
|
| 755 |
-
|
| 756 |
-
|
| 757 |
-
|
| 758 |
-
|
| 759 |
-
|
| 760 |
-
|
| 761 |
-
|
| 762 |
-
|
| 763 |
-
|
| 764 |
-
|
| 765 |
-
|
| 766 |
-
|
| 767 |
-
|
| 768 |
-
|
| 769 |
-
|
| 770 |
-
|
| 771 |
-
|
| 772 |
-
|
| 773 |
-
|
| 774 |
-
|
| 775 |
-
|
| 776 |
-
|
| 777 |
-
|
| 778 |
-
|
| 779 |
-
|
| 780 |
-
|
| 781 |
-
|
| 782 |
-
|
| 783 |
-
|
| 784 |
-
|
| 785 |
-
|
| 786 |
-
|
| 787 |
-
|
| 788 |
-
|
| 789 |
-
|
| 790 |
-
|
| 791 |
-
|
| 792 |
-
|
| 793 |
-
|
| 794 |
-
|
| 795 |
-
|
| 796 |
-
|
| 797 |
-
|
| 798 |
-
|
| 799 |
-
|
| 800 |
-
|
| 801 |
-
|
| 802 |
-
|
| 803 |
-
|
| 804 |
-
|
| 805 |
-
|
| 806 |
-
|
| 807 |
-
|
| 808 |
-
|
| 809 |
-
|
| 810 |
-
|
| 811 |
-
|
| 812 |
-
|
| 813 |
-
|
| 814 |
-
|
| 815 |
-
|
| 816 |
-
|
| 817 |
-
|
| 818 |
-
|
| 819 |
-
|
| 820 |
-
|
| 821 |
-
|
| 822 |
-
|
| 823 |
-
|
| 824 |
-
|
| 825 |
-
|
| 826 |
-
|
| 827 |
-
|
| 828 |
-
|
| 829 |
-
|
| 830 |
-
|
| 831 |
-
|
| 832 |
-
|
| 833 |
-
|
| 834 |
-
|
| 835 |
-
|
| 836 |
-
|
| 837 |
-
|
| 838 |
-
|
| 839 |
-
|
| 840 |
-
'
|
| 841 |
-
|
| 842 |
-
|
| 843 |
-
|
| 844 |
-
|
| 845 |
-
|
| 846 |
-
|
| 847 |
-
|
| 848 |
-
|
| 849 |
-
|
| 850 |
-
|
| 851 |
-
|
| 852 |
-
|
| 853 |
-
|
| 854 |
-
|
| 855 |
-
|
| 856 |
-
|
| 857 |
-
|
| 858 |
-
|
| 859 |
-
|
| 860 |
-
|
| 861 |
-
|
| 862 |
-
|
| 863 |
-
|
| 864 |
-
|
| 865 |
-
|
| 866 |
-
|
| 867 |
-
|
| 868 |
-
|
| 869 |
-
|
| 870 |
-
|
| 871 |
-
|
| 872 |
-
|
| 873 |
-
|
| 874 |
-
|
| 875 |
-
|
| 876 |
-
|
| 877 |
-
|
| 878 |
-
|
| 879 |
-
|
| 880 |
-
|
| 881 |
-
|
| 882 |
-
|
| 883 |
-
|
| 884 |
-
|
| 885 |
-
|
| 886 |
-
|
| 887 |
-
|
| 888 |
-
|
| 889 |
-
|
| 890 |
-
|
| 891 |
-
|
| 892 |
-
|
| 893 |
-
|
| 894 |
-
|
| 895 |
-
|
| 896 |
-
|
| 897 |
-
|
| 898 |
-
|
| 899 |
-
|
| 900 |
-
|
| 901 |
-
|
| 902 |
-
|
| 903 |
-
|
| 904 |
-
|
| 905 |
-
|
| 906 |
-
|
| 907 |
-
|
| 908 |
-
|
| 909 |
-
|
| 910 |
-
|
| 911 |
-
|
| 912 |
-
|
| 913 |
-
|
| 914 |
-
|
| 915 |
-
|
| 916 |
-
|
| 917 |
-
|
| 918 |
-
|
| 919 |
-
|
| 920 |
-
|
| 921 |
-
|
| 922 |
-
|
| 923 |
-
|
| 924 |
-
|
| 925 |
-
|
| 926 |
-
|
| 927 |
-
|
| 928 |
-
|
| 929 |
-
|
| 930 |
-
|
| 931 |
-
|
| 932 |
-
|
| 933 |
-
|
| 934 |
-
|
| 935 |
-
|
| 936 |
-
|
| 937 |
-
|
| 938 |
-
|
| 939 |
-
|
| 940 |
-
|
| 941 |
-
|
| 942 |
-
|
| 943 |
-
|
| 944 |
-
|
| 945 |
-
|
| 946 |
-
|
| 947 |
-
|
| 948 |
-
|
| 949 |
-
|
| 950 |
-
|
| 951 |
-
|
| 952 |
-
|
| 953 |
-
|
| 954 |
-
|
| 955 |
-
|
| 956 |
-
|
| 957 |
-
|
| 958 |
-
|
| 959 |
-
|
| 960 |
-
|
| 961 |
-
|
| 962 |
-
|
| 963 |
-
|
| 964 |
-
|
| 965 |
-
|
| 966 |
-
|
| 967 |
-
|
| 968 |
-
|
| 969 |
-
|
| 970 |
-
|
| 971 |
-
|
| 972 |
-
|
| 973 |
-
|
| 974 |
-
|
| 975 |
-
|
| 976 |
-
|
| 977 |
-
|
| 978 |
-
|
| 979 |
-
|
| 980 |
-
|
| 981 |
-
|
| 982 |
-
|
| 983 |
-
|
| 984 |
-
|
| 985 |
-
|
| 986 |
-
|
| 987 |
-
|
| 988 |
-
|
| 989 |
-
|
| 990 |
-
|
| 991 |
-
|
| 992 |
-
|
| 993 |
-
|
| 994 |
-
|
| 995 |
-
|
| 996 |
-
|
| 997 |
-
|
| 998 |
-
|
| 999 |
-
|
| 1000 |
-
|
| 1001 |
-
|
| 1002 |
-
|
| 1003 |
-
|
| 1004 |
-
|
| 1005 |
-
|
| 1006 |
-
|
| 1007 |
-
|
| 1008 |
-
|
| 1009 |
-
|
| 1010 |
-
|
| 1011 |
-
|
| 1012 |
-
|
| 1013 |
-
|
| 1014 |
-
|
| 1015 |
-
|
| 1016 |
-
|
| 1017 |
-
|
| 1018 |
-
|
| 1019 |
-
|
| 1020 |
-
|
| 1021 |
-
|
| 1022 |
-
|
| 1023 |
-
|
| 1024 |
-
|
| 1025 |
-
|
| 1026 |
-
|
| 1027 |
-
|
| 1028 |
-
|
| 1029 |
-
|
| 1030 |
-
|
| 1031 |
-
|
| 1032 |
-
|
| 1033 |
-
|
| 1034 |
-
|
| 1035 |
-
|
| 1036 |
-
|
| 1037 |
-
|
| 1038 |
-
|
| 1039 |
-
|
| 1040 |
-
|
| 1041 |
-
|
| 1042 |
-
unified: UnifiedMessageContext,
|
| 1043 |
-
include_header: bool = True
|
| 1044 |
-
) -> str:
|
| 1045 |
-
"""
|
| 1046 |
-
Formata contexto unificado para o prompt do LLM.
|
| 1047 |
-
|
| 1048 |
-
Args:
|
| 1049 |
-
unified: Contexto unificado
|
| 1050 |
-
include_header: Se inclui cabeçalho
|
| 1051 |
-
|
| 1052 |
-
Returns:
|
| 1053 |
-
String formatada para o prompt
|
| 1054 |
-
"""
|
| 1055 |
-
return format_unified_context_for_llm(unified, unified.token_budget)
|
| 1056 |
-
|
| 1057 |
-
def add_to_stm(
|
| 1058 |
-
self,
|
| 1059 |
-
conversation_id: str,
|
| 1060 |
-
role: str,
|
| 1061 |
-
content: str,
|
| 1062 |
-
emocao: str = "neutral",
|
| 1063 |
-
reply_info: Optional[Dict] = None,
|
| 1064 |
-
resposta: str = ""
|
| 1065 |
-
) -> MessageWithContext:
|
| 1066 |
-
"""
|
| 1067 |
-
Adiciona mensagem (user ou bot) à STM.
|
| 1068 |
-
|
| 1069 |
-
Args:
|
| 1070 |
-
conversation_id: ID da conversa
|
| 1071 |
-
role: "user" ou "assistant"
|
| 1072 |
-
content: Conteúdo da mensagem
|
| 1073 |
-
emocao: Emoção
|
| 1074 |
-
reply_info: Info de reply (se aplicável)
|
| 1075 |
-
resposta: Resposta do bot (se for assistant)
|
| 1076 |
-
|
| 1077 |
-
Returns:
|
| 1078 |
-
MessageWithContext criada
|
| 1079 |
-
"""
|
| 1080 |
-
# Para mensagens do bot, usa a resposta gerada
|
| 1081 |
-
if role == "assistant" and resposta:
|
| 1082 |
-
content = resposta
|
| 1083 |
-
|
| 1084 |
-
return self.stm_manager.add_message(
|
| 1085 |
-
conversation_id=conversation_id,
|
| 1086 |
-
role=role,
|
| 1087 |
-
content=content,
|
| 1088 |
-
emocao=emocao,
|
| 1089 |
-
reply_info=reply_info
|
| 1090 |
-
)
|
| 1091 |
-
|
| 1092 |
-
def merge_reply_with_stm(
|
| 1093 |
-
self,
|
| 1094 |
-
reply_context: Dict[str, Any],
|
| 1095 |
-
stm_messages: List[MessageWithContext],
|
| 1096 |
-
max_stm: int = 30
|
| 1097 |
-
) -> List[MessageWithContext]:
|
| 1098 |
-
"""
|
| 1099 |
-
Mescla reply context com STM para contexto do LLM.
|
| 1100 |
-
|
| 1101 |
-
Args:
|
| 1102 |
-
reply_context: Contexto do reply
|
| 1103 |
-
stm_messages: Mensagens STM
|
| 1104 |
-
max_stm: Máximo de mensagens STM
|
| 1105 |
-
|
| 1106 |
-
Returns:
|
| 1107 |
-
Lista combinada
|
| 1108 |
-
"""
|
| 1109 |
-
return sync_reply_with_stm(reply_context, stm_messages, max_stm)
|
| 1110 |
-
|
| 1111 |
-
|
| 1112 |
-
# ====================================
|
| 1113 |
-
# FACTORY FUNCTIONS
|
| 1114 |
-
# ====================================
|
| 1115 |
-
|
| 1116 |
-
_unified_builder: Optional[UnifiedContextBuilder] = None
|
| 1117 |
-
|
| 1118 |
-
def get_unified_context_builder() -> UnifiedContextBuilder:
|
| 1119 |
-
"""Obtém instância singleton do builder."""
|
| 1120 |
-
global _unified_builder
|
| 1121 |
-
if _unified_builder is None:
|
| 1122 |
-
_unified_builder = UnifiedContextBuilder()
|
| 1123 |
-
return _unified_builder
|
| 1124 |
-
|
| 1125 |
-
|
| 1126 |
-
def get_stm_manager() -> ShortTermMemoryManager:
|
| 1127 |
-
"""Obtém instância singleton do manager de STM."""
|
| 1128 |
-
return ShortTermMemoryManager()
|
| 1129 |
-
|
| 1130 |
-
|
| 1131 |
-
def build_unified_context(
|
| 1132 |
-
conversation_id: str,
|
| 1133 |
-
user_id: str = "",
|
| 1134 |
-
reply_metadata: Optional[Dict[str, Any]] = None,
|
| 1135 |
-
current_message: str = "",
|
| 1136 |
-
current_emotion: str = "neutral"
|
| 1137 |
-
) -> UnifiedMessageContext:
|
| 1138 |
-
"""
|
| 1139 |
-
Factory function para construir contexto unificado.
|
| 1140 |
-
|
| 1141 |
-
Usage:
|
| 1142 |
-
context = build_unified_context(
|
| 1143 |
-
conversation_id="pv:2449...",
|
| 1144 |
-
reply_metadata={...},
|
| 1145 |
-
current_message="."
|
| 1146 |
-
)
|
| 1147 |
-
"""
|
| 1148 |
-
builder = get_unified_context_builder()
|
| 1149 |
-
return builder.build(
|
| 1150 |
-
conversation_id=conversation_id,
|
| 1151 |
-
user_id=user_id,
|
| 1152 |
-
reply_metadata=reply_metadata,
|
| 1153 |
-
current_message=current_message,
|
| 1154 |
-
current_emotion=current_emotion
|
| 1155 |
-
)
|
| 1156 |
-
|
| 1157 |
-
|
| 1158 |
-
# ====================================
|
| 1159 |
-
# COMPATIBILITY HELPERS
|
| 1160 |
-
# ====================================
|
| 1161 |
-
|
| 1162 |
-
def gerar_id_conversao(
|
| 1163 |
-
numero: str,
|
| 1164 |
-
tipo_conversa: str = "pv",
|
| 1165 |
-
grupo_id: Optional[str] = None
|
| 1166 |
-
) -> str:
|
| 1167 |
-
"""
|
| 1168 |
-
Gera ID de conversa para STM isolada.
|
| 1169 |
-
|
| 1170 |
-
Args:
|
| 1171 |
-
numero: Número do usuário
|
| 1172 |
-
tipo_conversa: "pv" ou "grupo"
|
| 1173 |
-
grupo_id: ID do grupo (para conversas em grupo)
|
| 1174 |
-
|
| 1175 |
-
Returns:
|
| 1176 |
-
ID único da conversa
|
| 1177 |
-
"""
|
| 1178 |
-
from .context_isolation import generate_context_id
|
| 1179 |
-
return generate_context_id(numero, tipo_conversa, grupo_id)
|
| 1180 |
-
|
| 1181 |
-
|
| 1182 |
-
# type: ignore
|
|
|
|
| 1 |
+
# type: ignore
|
| 2 |
+
"""
|
| 3 |
+
================================================================================
|
| 4 |
+
AKIRA V21 ULTIMATE - UNIFIED CONTEXT MODULE
|
| 5 |
+
================================================================================
|
| 6 |
+
Sistema unificado que integra Reply Context + Short-Term Memory em sintonia.
|
| 7 |
+
|
| 8 |
+
Philosophy: "Reply context e STM devem trabalhar em sintonia como tik e tack -
|
| 9 |
+
um fornece o contexto imediato/urgente (o que o usuário está respondendo),
|
| 10 |
+
o outro fornece o fluxo da conversa (contexto geral)."
|
| 11 |
+
|
| 12 |
+
Features:
|
| 13 |
+
- Integração seamless entre reply context e STM
|
| 14 |
+
- Token budgeting inteligente entre os dois contextos
|
| 15 |
+
- Priorização dinâmica baseada no tipo de mensagem
|
| 16 |
+
- Suporte a perguntas curtas com reply (prioridade máxima)
|
| 17 |
+
- Persistência e restauração de contexto unificado
|
| 18 |
+
================================================================================
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
import os
|
| 22 |
+
import sys
|
| 23 |
+
import time
|
| 24 |
+
import json
|
| 25 |
+
import logging
|
| 26 |
+
from typing import Optional, Dict, Any, List, Tuple
|
| 27 |
+
from dataclasses import dataclass, field
|
| 28 |
+
from datetime import datetime
|
| 29 |
+
|
| 30 |
+
# Imports robustos com fallback
|
| 31 |
+
try:
|
| 32 |
+
from . import config
|
| 33 |
+
from .short_term_memory import (
|
| 34 |
+
ShortTermMemory,
|
| 35 |
+
MessageWithContext,
|
| 36 |
+
IMPORTANCIA_NORMAL,
|
| 37 |
+
IMPORTANCIA_REPLY,
|
| 38 |
+
IMPORTANCIA_REPLY_TO_BOT,
|
| 39 |
+
IMPORTANCIA_PERGUNTA_CURTA_REPLY,
|
| 40 |
+
estimar_tokens,
|
| 41 |
+
is_pergunta_curta
|
| 42 |
+
)
|
| 43 |
+
from .reply_context_handler import (
|
| 44 |
+
ReplyContextHandler,
|
| 45 |
+
ProcessedReplyContext,
|
| 46 |
+
PRIORITY_REPLY,
|
| 47 |
+
PRIORITY_REPLY_TO_BOT,
|
| 48 |
+
PRIORITY_REPLY_TO_BOT_SHORT_QUESTION
|
| 49 |
+
)
|
| 50 |
+
UNIFIED_CONTEXT_AVAILABLE = True
|
| 51 |
+
except ImportError as e:
|
| 52 |
+
try:
|
| 53 |
+
import modules.config as config
|
| 54 |
+
from modules.short_term_memory import (
|
| 55 |
+
ShortTermMemory,
|
| 56 |
+
MessageWithContext,
|
| 57 |
+
IMPORTANCIA_NORMAL,
|
| 58 |
+
IMPORTANCIA_REPLY,
|
| 59 |
+
IMPORTANCIA_REPLY_TO_BOT,
|
| 60 |
+
IMPORTANCIA_PERGUNTA_CURTA_REPLY,
|
| 61 |
+
estimar_tokens,
|
| 62 |
+
is_pergunta_curta
|
| 63 |
+
)
|
| 64 |
+
from modules.reply_context_handler import (
|
| 65 |
+
ReplyContextHandler,
|
| 66 |
+
ProcessedReplyContext,
|
| 67 |
+
PRIORITY_REPLY,
|
| 68 |
+
PRIORITY_REPLY_TO_BOT,
|
| 69 |
+
PRIORITY_REPLY_TO_BOT_SHORT_QUESTION
|
| 70 |
+
)
|
| 71 |
+
UNIFIED_CONTEXT_AVAILABLE = True
|
| 72 |
+
except ImportError:
|
| 73 |
+
UNIFIED_CONTEXT_AVAILABLE = False
|
| 74 |
+
config = None
|
| 75 |
+
|
| 76 |
+
try:
|
| 77 |
+
from .lstm_extension import get_lstm_extension
|
| 78 |
+
LSTM_AVAILABLE = True
|
| 79 |
+
except ImportError:
|
| 80 |
+
try:
|
| 81 |
+
from modules.lstm_extension import get_lstm_extension
|
| 82 |
+
LSTM_AVAILABLE = True
|
| 83 |
+
except ImportError:
|
| 84 |
+
LSTM_AVAILABLE = False
|
| 85 |
+
|
| 86 |
+
logger = logging.getLogger(__name__)
|
| 87 |
+
|
| 88 |
+
# ============================================================
|
| 89 |
+
# CONFIGURAÇÃO DE TOKEN BUDGET
|
| 90 |
+
# ============================================================
|
| 91 |
+
|
| 92 |
+
@dataclass
|
| 93 |
+
class ContextTokenBudget:
|
| 94 |
+
"""
|
| 95 |
+
Alocação de tokens entre reply context e STM.
|
| 96 |
+
|
| 97 |
+
Philosophy: Reply tem orçamento dedicado (urgente), STM tem o resto (fluxo).
|
| 98 |
+
"""
|
| 99 |
+
total_budget: int = 8000
|
| 100 |
+
system_tokens: int = 1500
|
| 101 |
+
user_message_tokens: int = 500
|
| 102 |
+
|
| 103 |
+
# Reply context budget (URGENTE)
|
| 104 |
+
reply_tokens: int = 300
|
| 105 |
+
reply_priority_multiplier: float = 1.0
|
| 106 |
+
|
| 107 |
+
# STM budget (FLUXO DA CONVERSA)
|
| 108 |
+
stm_tokens: int = 4000
|
| 109 |
+
|
| 110 |
+
# Reservado para resposta
|
| 111 |
+
response_reserved: int = 1200
|
| 112 |
+
|
| 113 |
+
def calculate(self, is_reply: bool, reply_priority: int = 1) -> 'ContextTokenBudget':
|
| 114 |
+
"""
|
| 115 |
+
Calcula orçamento baseado no tipo de mensagem.
|
| 116 |
+
|
| 117 |
+
Args:
|
| 118 |
+
is_reply: Se é um reply
|
| 119 |
+
reply_priority: Nível de prioridade do reply (1-4)
|
| 120 |
+
|
| 121 |
+
Returns:
|
| 122 |
+
ContextTokenBudget ajustado
|
| 123 |
+
"""
|
| 124 |
+
budget = ContextTokenBudget(
|
| 125 |
+
total_budget=self.total_budget,
|
| 126 |
+
system_tokens=self.system_tokens,
|
| 127 |
+
user_message_tokens=self.user_message_tokens
|
| 128 |
+
)
|
| 129 |
+
|
| 130 |
+
if is_reply:
|
| 131 |
+
if reply_priority >= PRIORITY_REPLY_TO_BOT_SHORT_QUESTION:
|
| 132 |
+
# Pergunta curta com reply ao bot = prioridade máxima
|
| 133 |
+
budget.reply_tokens = min(1500, int(self.total_budget * 0.20))
|
| 134 |
+
budget.reply_priority_multiplier = 1.5
|
| 135 |
+
budget.stm_tokens = min(3500, int(self.total_budget * 0.45))
|
| 136 |
+
elif reply_priority >= PRIORITY_REPLY_TO_BOT:
|
| 137 |
+
# Reply ao bot
|
| 138 |
+
budget.reply_tokens = min(1200, int(self.total_budget * 0.15))
|
| 139 |
+
budget.reply_priority_multiplier = 1.3
|
| 140 |
+
budget.stm_tokens = min(4000, int(self.total_budget * 0.50))
|
| 141 |
+
elif reply_priority >= PRIORITY_REPLY:
|
| 142 |
+
# Reply normal
|
| 143 |
+
budget.reply_tokens = min(800, int(self.total_budget * 0.10))
|
| 144 |
+
budget.reply_priority_multiplier = 1.1
|
| 145 |
+
budget.stm_tokens = min(4500, int(self.total_budget * 0.55))
|
| 146 |
+
else:
|
| 147 |
+
# Mensagem normal = STM tem orçamento completo
|
| 148 |
+
budget.reply_tokens = 0
|
| 149 |
+
budget.stm_tokens = min(5000, int(self.total_budget * 0.65))
|
| 150 |
+
|
| 151 |
+
# Calcula response reserved
|
| 152 |
+
budget.response_reserved = (
|
| 153 |
+
budget.total_budget -
|
| 154 |
+
budget.system_tokens -
|
| 155 |
+
budget.user_message_tokens -
|
| 156 |
+
budget.reply_tokens -
|
| 157 |
+
budget.stm_tokens
|
| 158 |
+
)
|
| 159 |
+
|
| 160 |
+
return budget
|
| 161 |
+
|
| 162 |
+
def to_dict(self) -> Dict[str, Any]:
|
| 163 |
+
"""Serializa para dicionário."""
|
| 164 |
+
return {
|
| 165 |
+
"total_budget": self.total_budget,
|
| 166 |
+
"system_tokens": self.system_tokens,
|
| 167 |
+
"user_message_tokens": self.user_message_tokens,
|
| 168 |
+
"reply_tokens": self.reply_tokens,
|
| 169 |
+
"stm_tokens": self.stm_tokens,
|
| 170 |
+
"response_reserved": self.response_reserved,
|
| 171 |
+
"reply_priority_multiplier": self.reply_priority_multiplier
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
# ============================================================
|
| 176 |
+
# CONTEXTO UNIFICADO
|
| 177 |
+
# ============================================================
|
| 178 |
+
|
| 179 |
+
@dataclass
|
| 180 |
+
class UnifiedMessageContext:
|
| 181 |
+
"""
|
| 182 |
+
Contexto unificado combinando reply + STM.
|
| 183 |
+
|
| 184 |
+
Philosophy: Reply context (tik) + STM (tok) trabalhando em sintonia.
|
| 185 |
+
|
| 186 |
+
Attributes:
|
| 187 |
+
- Reply context: Contexto imediato/urgente do reply
|
| 188 |
+
- STM context: Contexto do fluxo da conversa
|
| 189 |
+
- Integration: Como os dois são combinados
|
| 190 |
+
"""
|
| 191 |
+
# Identificação
|
| 192 |
+
conversation_id: str = ""
|
| 193 |
+
user_id: str = ""
|
| 194 |
+
timestamp: float = field(default_factory=time.time)
|
| 195 |
+
|
| 196 |
+
# Reply Context (TIK - urgente/imediato)
|
| 197 |
+
is_reply: bool = False
|
| 198 |
+
reply_to_bot: bool = False
|
| 199 |
+
reply_priority: int = 1 # 1=normal, 2=reply, 3=reply_to_bot, 4=critical
|
| 200 |
+
quoted_author: str = ""
|
| 201 |
+
quoted_content: str = ""
|
| 202 |
+
reply_importancia: float = 1.0
|
| 203 |
+
replied_to_author: str = ""
|
| 204 |
+
replied_to_content: str = ""
|
| 205 |
+
|
| 206 |
+
# STM Context (TOK - fluxo da conversa)
|
| 207 |
+
stm_messages: List[MessageWithContext] = field(default_factory=list)
|
| 208 |
+
stm_summary: Dict[str, Any] = field(default_factory=dict)
|
| 209 |
+
stm_emotional_trend: str = "neutral"
|
| 210 |
+
|
| 211 |
+
# Long-Term Memory (RAG)
|
| 212 |
+
long_term_memory: str = ""
|
| 213 |
+
|
| 214 |
+
# Integração
|
| 215 |
+
sync_mode: str = "tiktok" # "tiktok" = reply priority + STM flow
|
| 216 |
+
token_budget: ContextTokenBudget = field(default_factory=ContextTokenBudget)
|
| 217 |
+
|
| 218 |
+
# Mensagem atual
|
| 219 |
+
current_message: str = ""
|
| 220 |
+
current_emotion: str = "neutro"
|
| 221 |
+
system_override: str = ""
|
| 222 |
+
|
| 223 |
+
def to_dict(self) -> Dict[str, Any]:
|
| 224 |
+
"""Serializa para dicionário."""
|
| 225 |
+
return {
|
| 226 |
+
"conversation_id": self.conversation_id,
|
| 227 |
+
"user_id": self.user_id,
|
| 228 |
+
"timestamp": self.timestamp,
|
| 229 |
+
"is_reply": self.is_reply,
|
| 230 |
+
"reply_to_bot": self.reply_to_bot,
|
| 231 |
+
"reply_priority": self.reply_priority,
|
| 232 |
+
"quoted_author": self.quoted_author,
|
| 233 |
+
"quoted_content": self.quoted_content[:500] if self.quoted_content else "",
|
| 234 |
+
"reply_importancia": self.reply_importancia,
|
| 235 |
+
"stm_messages_count": len(self.stm_messages),
|
| 236 |
+
"stm_summary": self.stm_summary,
|
| 237 |
+
"stm_emotional_trend": self.stm_emotional_trend,
|
| 238 |
+
"long_term_memory": self.long_term_memory,
|
| 239 |
+
"sync_mode": self.sync_mode,
|
| 240 |
+
"token_budget": self.token_budget.to_dict(),
|
| 241 |
+
"current_message": self.current_message[:100],
|
| 242 |
+
"current_emotion": self.current_emotion,
|
| 243 |
+
"replied_to_author": self.replied_to_author,
|
| 244 |
+
"replied_to_content": self.replied_to_content[:200] if self.replied_to_content else ""
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
def build_prompt(self) -> str:
|
| 248 |
+
"""
|
| 249 |
+
Constrói prompt formatado para o LLM.
|
| 250 |
+
|
| 251 |
+
Returns:
|
| 252 |
+
String formatada com contexto unificado (reply + STM)
|
| 253 |
+
"""
|
| 254 |
+
return format_unified_context_for_llm(self, self.token_budget)
|
| 255 |
+
|
| 256 |
+
|
| 257 |
+
# ====================================
|
| 258 |
+
# HELPER FUNCTIONS
|
| 259 |
+
# ====================================
|
| 260 |
+
|
| 261 |
+
def sync_reply_with_stm(
|
| 262 |
+
reply_context: Dict[str, Any],
|
| 263 |
+
stm_messages: List[MessageWithContext],
|
| 264 |
+
max_stm_messages: int = 10
|
| 265 |
+
) -> List[MessageWithContext]:
|
| 266 |
+
"""
|
| 267 |
+
Sincroniza reply context com mensagens STM.
|
| 268 |
+
|
| 269 |
+
Philosophy: Reply (tik) vem primeiro, STM (tok) vem depois.
|
| 270 |
+
Ambos são combinados para formar o contexto completo.
|
| 271 |
+
|
| 272 |
+
Args:
|
| 273 |
+
reply_context: Contexto do reply
|
| 274 |
+
stm_messages: Mensagens da memória de curto prazo
|
| 275 |
+
max_stm_messages: Máximo de mensagens STM a incluir
|
| 276 |
+
|
| 277 |
+
Returns:
|
| 278 |
+
Lista combinada de mensagens para contexto
|
| 279 |
+
"""
|
| 280 |
+
combined = []
|
| 281 |
+
|
| 282 |
+
# 1. Adiciona reply context como mensagem mais recente (TIK)
|
| 283 |
+
if reply_context.get('is_reply', False):
|
| 284 |
+
reply_msg = MessageWithContext(
|
| 285 |
+
role="user",
|
| 286 |
+
content=reply_context.get('quoted_content', ''),
|
| 287 |
+
importancia=reply_context.get('importancia', IMPORTANCIA_NORMAL),
|
| 288 |
+
emocao=reply_context.get('emocao', 'neutral'),
|
| 289 |
+
reply_info={
|
| 290 |
+
'is_reply': True,
|
| 291 |
+
'reply_to_bot': reply_context.get('reply_to_bot', False),
|
| 292 |
+
'quoted_text_original': reply_context.get('quoted_content', ''),
|
| 293 |
+
'priority_level': reply_context.get('priority', 1),
|
| 294 |
+
'sync_mode': 'tiktok'
|
| 295 |
+
}
|
| 296 |
+
)
|
| 297 |
+
combined.append(reply_msg)
|
| 298 |
+
|
| 299 |
+
# 2. Adiciona mensagens STM (TOK - fluxo da conversa)
|
| 300 |
+
# Pega últimas N mensagens STM
|
| 301 |
+
stm_to_add = stm_messages[-max_stm_messages:] if stm_messages else []
|
| 302 |
+
|
| 303 |
+
for msg in stm_to_add:
|
| 304 |
+
# Se a mensagem STM já é um reply, preserva info
|
| 305 |
+
if msg.is_reply and not msg.reply_info.get('sync_mode'):
|
| 306 |
+
msg.reply_info['sync_mode'] = 'stm'
|
| 307 |
+
combined.append(msg)
|
| 308 |
+
|
| 309 |
+
return combined
|
| 310 |
+
|
| 311 |
+
|
| 312 |
+
def format_unified_context_for_llm(
|
| 313 |
+
unified: UnifiedMessageContext,
|
| 314 |
+
budget: ContextTokenBudget
|
| 315 |
+
) -> str:
|
| 316 |
+
"""
|
| 317 |
+
Formata contexto unificado para o prompt do LLM.
|
| 318 |
+
|
| 319 |
+
Philosophy: Reply (tik) primeiro por ser urgente, STM (tok) depois
|
| 320 |
+
para contexto da conversa.
|
| 321 |
+
|
| 322 |
+
Args:
|
| 323 |
+
unified: Contexto unificado
|
| 324 |
+
budget: Orçamento de tokens
|
| 325 |
+
|
| 326 |
+
Returns:
|
| 327 |
+
String formatada para o prompt
|
| 328 |
+
"""
|
| 329 |
+
parts = []
|
| 330 |
+
|
| 331 |
+
# ===== 1. REPLY CONTEXT (TIK - URGENTE) =====
|
| 332 |
+
if unified.is_reply:
|
| 333 |
+
reply_section = []
|
| 334 |
+
reply_section.append("=" * 50)
|
| 335 |
+
reply_section.append("[📎 INTERNAL_BRAIN_ONLY: REPLY CONTEXT]")
|
| 336 |
+
reply_section.append("=" * 50)
|
| 337 |
+
|
| 338 |
+
if unified.reply_to_bot:
|
| 339 |
+
reply_section.append("⚠️ VOCÊ ESTÁ SENDO DIRETAMENTE RESPONDIDO!")
|
| 340 |
+
else:
|
| 341 |
+
reply_section.append(f"Respondendo a: {unified.quoted_author}")
|
| 342 |
+
|
| 343 |
+
# Conteúdo citado
|
| 344 |
+
if unified.quoted_content:
|
| 345 |
+
quoted_preview = unified.quoted_content[:budget.reply_tokens // 4]
|
| 346 |
+
reply_section.append(f"\n<quoted_message>\n{quoted_preview}...\n</quoted_message>")
|
| 347 |
+
|
| 348 |
+
# Prioridade
|
| 349 |
+
if unified.reply_priority >= PRIORITY_REPLY_TO_BOT_SHORT_QUESTION:
|
| 350 |
+
reply_section.append("\n💡 PERGUNTA CURTA + REPLY: FOCO NA CITAÇÃO")
|
| 351 |
+
|
| 352 |
+
reply_section.append("\n📌 INSTRUÇÕES DE REPLY:")
|
| 353 |
+
if unified.reply_to_bot:
|
| 354 |
+
thread_info = ""
|
| 355 |
+
if unified.replied_to_author:
|
| 356 |
+
thread_info = f" (Esta sua mensagem citada foi enviada originalmente para {unified.replied_to_author} em resposta a: \"{unified.replied_to_content[:200]}...\")"
|
| 357 |
+
|
| 358 |
+
reply_section.append(f"- O usuário está a reagir a uma mensagem SUA (<quoted_message>){thread_info}. Responda diretamente ao comentário do usuário, mantendo a postura sobre o que você disse.")
|
| 359 |
+
else:
|
| 360 |
+
reply_section.append("- O usuário está a responder a <quoted_message>. Formule sua resposta com base nisso.")
|
| 361 |
+
|
| 362 |
+
reply_section.append("- PRESERVE a sua identidade e humor (seja o Akira, natural e irreverente).")
|
| 363 |
+
reply_section.append("- Nunca perca o fio da meada. Olhe as mensagens anteriores para entender o contexto real.")
|
| 364 |
+
|
| 365 |
+
parts.append("\n".join(reply_section))
|
| 366 |
+
|
| 367 |
+
# ===== RAG CONTEXT (MEMÓRIA DE LONGO PRAZO) =====
|
| 368 |
+
if unified.long_term_memory:
|
| 369 |
+
rag_section = []
|
| 370 |
+
rag_section.append("\n" + "=" * 50)
|
| 371 |
+
rag_section.append("[📖 INTERNAL_BRAIN_ONLY: LONG-TERM MEMORY]")
|
| 372 |
+
rag_section.append("=" * 50)
|
| 373 |
+
rag_section.append("(Informações previamente aprendidas sobre o usuário)")
|
| 374 |
+
rag_section.append(unified.long_term_memory)
|
| 375 |
+
parts.append("\n".join(rag_section))
|
| 376 |
+
|
| 377 |
+
# ===== 2. STM CONTEXT (METADADOS DE FLUXO) =====
|
| 378 |
+
if unified.stm_messages:
|
| 379 |
+
stm_section = []
|
| 380 |
+
# Não adicionamos as mensagens como texto aqui para evitar duplicação e truncagem,
|
| 381 |
+
# pois elas já são injetadas nativamente no array context_history da API.
|
| 382 |
+
|
| 383 |
+
# emotional trend
|
| 384 |
+
if unified.stm_emotional_trend != "neutral":
|
| 385 |
+
stm_section.append(f"\n📊 Tendência emocional do chat: {unified.stm_emotional_trend}")
|
| 386 |
+
|
| 387 |
+
if stm_section:
|
| 388 |
+
parts.append("\n".join(stm_section))
|
| 389 |
+
|
| 390 |
+
return "\n".join(parts)
|
| 391 |
+
|
| 392 |
+
|
| 393 |
+
# ====================================
|
| 394 |
+
# SHORT-TERM MEMORY MANAGER
|
| 395 |
+
# ====================================
|
| 396 |
+
|
| 397 |
+
class ShortTermMemoryManager:
|
| 398 |
+
"""
|
| 399 |
+
Gerenciador de instâncias STM por conversa.
|
| 400 |
+
|
| 401 |
+
Philosophy: Cada conversa tem sua própria STM isolada,
|
| 402 |
+
mas todas compartilham o mesmo manager.
|
| 403 |
+
"""
|
| 404 |
+
|
| 405 |
+
_instance = None
|
| 406 |
+
_lock = None
|
| 407 |
+
|
| 408 |
+
def __new__(cls):
|
| 409 |
+
if cls._instance is None:
|
| 410 |
+
cls._lock = __import__('threading').Lock()
|
| 411 |
+
with cls._lock:
|
| 412 |
+
if cls._instance is None:
|
| 413 |
+
cls._instance = super().__new__(cls)
|
| 414 |
+
cls._instance._initialized = False
|
| 415 |
+
return cls._instance
|
| 416 |
+
|
| 417 |
+
def __init__(self):
|
| 418 |
+
if self._initialized:
|
| 419 |
+
return
|
| 420 |
+
|
| 421 |
+
self._instances: Dict[str, ShortTermMemory] = {}
|
| 422 |
+
# Path centralizado via config
|
| 423 |
+
if config and hasattr(config, "DATA_DIR"):
|
| 424 |
+
self._storage_path: str = str(config.DATA_DIR / "stm_cache")
|
| 425 |
+
else:
|
| 426 |
+
self._storage_path: str = os.path.join(
|
| 427 |
+
os.path.dirname(os.path.abspath(__file__)),
|
| 428 |
+
'..', 'data', 'stm_cache'
|
| 429 |
+
)
|
| 430 |
+
os.makedirs(self._storage_path, exist_ok=True)
|
| 431 |
+
self._initialized = True
|
| 432 |
+
self._load_all()
|
| 433 |
+
logger.debug(f"✅ ShortTermMemoryManager inicializado (persistência: {self._storage_path})")
|
| 434 |
+
|
| 435 |
+
# ============================================================
|
| 436 |
+
# PERSISTÊNCIA EM DISCO
|
| 437 |
+
# ============================================================
|
| 438 |
+
|
| 439 |
+
def _stm_file_path(self, conversation_id: str) -> str:
|
| 440 |
+
"""Retorna caminho do arquivo de persistência de uma STM."""
|
| 441 |
+
safe_id = conversation_id.replace('/', '_').replace('\\', '_')[:128]
|
| 442 |
+
return os.path.join(self._storage_path, f"{safe_id}.json")
|
| 443 |
+
|
| 444 |
+
def _load_stm(self, conversation_id: str) -> Optional[ShortTermMemory]:
|
| 445 |
+
"""Carrega STM de disco se existir."""
|
| 446 |
+
fpath = self._stm_file_path(conversation_id)
|
| 447 |
+
if os.path.exists(fpath):
|
| 448 |
+
try:
|
| 449 |
+
stm = ShortTermMemory.load_from_file(fpath)
|
| 450 |
+
self._instances[conversation_id] = stm
|
| 451 |
+
return stm
|
| 452 |
+
except Exception as e:
|
| 453 |
+
logger.warning(f"Falha ao carregar STM {conversation_id[:8]}: {e}")
|
| 454 |
+
return None
|
| 455 |
+
|
| 456 |
+
def _load_all(self) -> None:
|
| 457 |
+
"""Carrega todas as STMs persistidas do disco."""
|
| 458 |
+
if not os.path.isdir(self._storage_path):
|
| 459 |
+
return
|
| 460 |
+
for fname in os.listdir(self._storage_path):
|
| 461 |
+
if fname.endswith('.json'):
|
| 462 |
+
cid = fname[:-5]
|
| 463 |
+
self._load_stm(cid)
|
| 464 |
+
logger.info(f"📦 {len(self._instances)} STM(s) carregadas do disco")
|
| 465 |
+
|
| 466 |
+
def _save_stm(self, conversation_id: str) -> None:
|
| 467 |
+
"""Salva STM de uma conversa em disco."""
|
| 468 |
+
if conversation_id in self._instances:
|
| 469 |
+
fpath = self._stm_file_path(conversation_id)
|
| 470 |
+
self._instances[conversation_id].save_to_file(fpath)
|
| 471 |
+
|
| 472 |
+
def get_or_create_stm(
|
| 473 |
+
self,
|
| 474 |
+
conversation_id: str,
|
| 475 |
+
user_id: str = "",
|
| 476 |
+
max_messages: int = 100
|
| 477 |
+
) -> ShortTermMemory:
|
| 478 |
+
"""
|
| 479 |
+
Obtém ou cria STM para uma conversa.
|
| 480 |
+
|
| 481 |
+
Args:
|
| 482 |
+
conversation_id: ID único da conversa
|
| 483 |
+
user_id: ID do usuário
|
| 484 |
+
max_messages: Máximo de mensagens na STM
|
| 485 |
+
|
| 486 |
+
Returns:
|
| 487 |
+
Instância de ShortTermMemory
|
| 488 |
+
"""
|
| 489 |
+
if conversation_id not in self._instances:
|
| 490 |
+
self._instances[conversation_id] = ShortTermMemory(
|
| 491 |
+
conversation_id=conversation_id,
|
| 492 |
+
max_messages=max_messages
|
| 493 |
+
)
|
| 494 |
+
logger.debug(f"🧠 STM criada: {conversation_id[:8]}...")
|
| 495 |
+
|
| 496 |
+
return self._instances[conversation_id]
|
| 497 |
+
|
| 498 |
+
def add_message(
|
| 499 |
+
self,
|
| 500 |
+
conversation_id: str,
|
| 501 |
+
role: str,
|
| 502 |
+
content: str,
|
| 503 |
+
author_name: str = "Usuário",
|
| 504 |
+
emocao: str = "neutral",
|
| 505 |
+
reply_info: Optional[Dict] = None,
|
| 506 |
+
importancia: Optional[float] = None
|
| 507 |
+
) -> MessageWithContext:
|
| 508 |
+
"""
|
| 509 |
+
Adiciona mensagem à STM de uma conversa.
|
| 510 |
+
|
| 511 |
+
Args:
|
| 512 |
+
conversation_id: ID da conversa
|
| 513 |
+
role: "user" ou "assistant"
|
| 514 |
+
content: Texto da mensagem
|
| 515 |
+
emocao: Emoção detectada
|
| 516 |
+
reply_info: Info de reply (se aplicável)
|
| 517 |
+
importancia: Importância customizada
|
| 518 |
+
|
| 519 |
+
Returns:
|
| 520 |
+
MessageWithContext criada
|
| 521 |
+
"""
|
| 522 |
+
stm = self.get_or_create_stm(conversation_id)
|
| 523 |
+
|
| 524 |
+
# Calcula importância automaticamente se não fornecida
|
| 525 |
+
if importancia is None:
|
| 526 |
+
from .short_term_memory import calcular_importancia
|
| 527 |
+
importancia = calcular_importancia(
|
| 528 |
+
is_reply=bool(reply_info and reply_info.get("is_reply")),
|
| 529 |
+
reply_to_bot=bool(reply_info and reply_info.get("reply_to_bot")),
|
| 530 |
+
mensagem=content,
|
| 531 |
+
emocao=emocao
|
| 532 |
+
)
|
| 533 |
+
|
| 534 |
+
msg = stm.add_message(
|
| 535 |
+
role=role,
|
| 536 |
+
content=content,
|
| 537 |
+
author_name=author_name,
|
| 538 |
+
importancia=importancia,
|
| 539 |
+
emocao=emocao,
|
| 540 |
+
reply_info=reply_info
|
| 541 |
+
)
|
| 542 |
+
|
| 543 |
+
# Persiste em disco (salva a cada mensagem para garantir durability)
|
| 544 |
+
self._save_stm(conversation_id)
|
| 545 |
+
return msg
|
| 546 |
+
|
| 547 |
+
def get_context(
|
| 548 |
+
self,
|
| 549 |
+
conversation_id: str,
|
| 550 |
+
include_replies: bool = True,
|
| 551 |
+
prioritize_replies: bool = True,
|
| 552 |
+
max_messages: int = 10,
|
| 553 |
+
max_tokens: int = 4000
|
| 554 |
+
) -> List[MessageWithContext]:
|
| 555 |
+
"""
|
| 556 |
+
Obtém contexto da STM de uma conversa.
|
| 557 |
+
|
| 558 |
+
Args:
|
| 559 |
+
conversation_id: ID da conversa
|
| 560 |
+
include_replies: Se inclui replies
|
| 561 |
+
prioritize_replies: Se prioriza replies
|
| 562 |
+
max_messages: Máximo de mensagens
|
| 563 |
+
max_tokens: Máximo de tokens
|
| 564 |
+
|
| 565 |
+
Returns:
|
| 566 |
+
Lista de mensagens
|
| 567 |
+
"""
|
| 568 |
+
if conversation_id not in self._instances:
|
| 569 |
+
return []
|
| 570 |
+
|
| 571 |
+
stm = self._instances[conversation_id]
|
| 572 |
+
return stm.get_context_window(
|
| 573 |
+
include_replies=include_replies,
|
| 574 |
+
prioritize_replies=prioritize_replies,
|
| 575 |
+
max_messages=max_messages,
|
| 576 |
+
max_tokens=max_tokens
|
| 577 |
+
)
|
| 578 |
+
|
| 579 |
+
def get_summary(self, conversation_id: str) -> Dict[str, Any]:
|
| 580 |
+
"""
|
| 581 |
+
Obtém resumo da STM de uma conversa.
|
| 582 |
+
|
| 583 |
+
Args:
|
| 584 |
+
conversation_id: ID da conversa
|
| 585 |
+
|
| 586 |
+
Returns:
|
| 587 |
+
Dicionário com resumo
|
| 588 |
+
"""
|
| 589 |
+
if conversation_id not in self._instances:
|
| 590 |
+
return {}
|
| 591 |
+
|
| 592 |
+
stm = self._instances[conversation_id]
|
| 593 |
+
return stm.get_conversation_summary()
|
| 594 |
+
|
| 595 |
+
def clear(self, conversation_id: str) -> bool:
|
| 596 |
+
"""
|
| 597 |
+
Limpa STM de uma conversa, inclusive persistência em disco.
|
| 598 |
+
|
| 599 |
+
Args:
|
| 600 |
+
conversation_id: ID da conversa
|
| 601 |
+
|
| 602 |
+
Returns:
|
| 603 |
+
True se limpou
|
| 604 |
+
"""
|
| 605 |
+
if conversation_id in self._instances:
|
| 606 |
+
self._instances[conversation_id].clear()
|
| 607 |
+
del self._instances[conversation_id]
|
| 608 |
+
# Remove arquivo de persistência
|
| 609 |
+
fpath = self._stm_file_path(conversation_id)
|
| 610 |
+
if hasattr(self, 'fpath') or True:
|
| 611 |
+
try:
|
| 612 |
+
fpath = self._stm_file_path(conversation_id)
|
| 613 |
+
if os.path.exists(fpath):
|
| 614 |
+
os.remove(fpath)
|
| 615 |
+
except Exception:
|
| 616 |
+
pass
|
| 617 |
+
return True
|
| 618 |
+
|
| 619 |
+
def clear_messages(self, conversation_id: str) -> None:
|
| 620 |
+
"""Alias de compatibilidade para clear()."""
|
| 621 |
+
self.clear(conversation_id)
|
| 622 |
+
|
| 623 |
+
def get_messages(
|
| 624 |
+
self,
|
| 625 |
+
conversation_id: str,
|
| 626 |
+
limit: int = 10,
|
| 627 |
+
include_replies: bool = True
|
| 628 |
+
) -> list:
|
| 629 |
+
"""
|
| 630 |
+
Alias de compatibilidade para get_context().
|
| 631 |
+
Retorna lista de MessageWithContext para a conversa.
|
| 632 |
+
|
| 633 |
+
Args:
|
| 634 |
+
conversation_id: ID da conversa
|
| 635 |
+
limit: Quantidade máxima de mensagens
|
| 636 |
+
include_replies: Se inclui replies
|
| 637 |
+
|
| 638 |
+
Returns:
|
| 639 |
+
Lista de MessageWithContext
|
| 640 |
+
"""
|
| 641 |
+
if conversation_id not in self._instances:
|
| 642 |
+
return []
|
| 643 |
+
stm = self._instances[conversation_id]
|
| 644 |
+
result = stm.get_context_window(
|
| 645 |
+
include_replies=include_replies,
|
| 646 |
+
prioritize_replies=True,
|
| 647 |
+
max_messages=limit
|
| 648 |
+
)
|
| 649 |
+
return result if result else []
|
| 650 |
+
|
| 651 |
+
|
| 652 |
+
# ====================================
|
| 653 |
+
# UNIFIED CONTEXT BUILDER
|
| 654 |
+
# ====================================
|
| 655 |
+
|
| 656 |
+
class UnifiedContextBuilder:
|
| 657 |
+
"""
|
| 658 |
+
Constrói contexto unificado combinando reply + STM.
|
| 659 |
+
|
| 660 |
+
Philosophy: "Reply context e STM devem trabalhar em sintonia como tik e tack"
|
| 661 |
+
|
| 662 |
+
Usage:
|
| 663 |
+
builder = UnifiedContextBuilder()
|
| 664 |
+
context = builder.build(
|
| 665 |
+
conversation_id="...",
|
| 666 |
+
reply_metadata={...},
|
| 667 |
+
current_message="..."
|
| 668 |
+
)
|
| 669 |
+
prompt_section = builder.format_for_llm(context)
|
| 670 |
+
"""
|
| 671 |
+
|
| 672 |
+
def __init__(self, context_manager=None, stm_manager=None, db_instance=None):
|
| 673 |
+
self.stm_manager = stm_manager if stm_manager else ShortTermMemoryManager()
|
| 674 |
+
self.context_manager = context_manager
|
| 675 |
+
self.db = db_instance
|
| 676 |
+
self.reply_handler = None
|
| 677 |
+
self._initialized = False
|
| 678 |
+
|
| 679 |
+
def _ensure_initialized(self):
|
| 680 |
+
"""Garante inicialização do reply handler."""
|
| 681 |
+
if not self._initialized and UNIFIED_CONTEXT_AVAILABLE:
|
| 682 |
+
try:
|
| 683 |
+
self.reply_handler = ReplyContextHandler()
|
| 684 |
+
self._initialized = True
|
| 685 |
+
except Exception as e:
|
| 686 |
+
logger.warning(f"UnifiedContextBuilder: falha ao init reply handler: {e}")
|
| 687 |
+
|
| 688 |
+
def build(
|
| 689 |
+
self,
|
| 690 |
+
conversation_id: str,
|
| 691 |
+
user_id: str = "",
|
| 692 |
+
reply_metadata: Optional[Dict[str, Any]] = None,
|
| 693 |
+
current_message: str = "",
|
| 694 |
+
current_emotion: str = "neutro",
|
| 695 |
+
stm_messages: Optional[List[MessageWithContext]] = None
|
| 696 |
+
) -> UnifiedMessageContext:
|
| 697 |
+
"""
|
| 698 |
+
Constrói contexto unificado.
|
| 699 |
+
|
| 700 |
+
Args:
|
| 701 |
+
conversation_id: ID único da conversa
|
| 702 |
+
user_id: ID do usuário
|
| 703 |
+
reply_metadata: Metadados do reply
|
| 704 |
+
current_message: Mensagem atual
|
| 705 |
+
current_emotion: Emoção atual
|
| 706 |
+
stm_messages: Mensagens STM (usa manager se None)
|
| 707 |
+
|
| 708 |
+
Returns:
|
| 709 |
+
UnifiedMessageContext pronto para uso
|
| 710 |
+
"""
|
| 711 |
+
self._ensure_initialized()
|
| 712 |
+
|
| 713 |
+
# ===== 1. PROCESSA REPLY CONTEXT (TIK) =====
|
| 714 |
+
is_reply = reply_metadata.get('is_reply', False) if reply_metadata else False
|
| 715 |
+
|
| 716 |
+
reply_context = {
|
| 717 |
+
'is_reply': is_reply,
|
| 718 |
+
'reply_to_bot': reply_metadata.get('reply_to_bot', False) if reply_metadata else False,
|
| 719 |
+
'quoted_author': reply_metadata.get('quoted_author_name', '') if reply_metadata else '',
|
| 720 |
+
'quoted_content': reply_metadata.get('quoted_text_original', '') or
|
| 721 |
+
reply_metadata.get('mensagem_citada', '') if reply_metadata else '',
|
| 722 |
+
'importancia': IMPORTANCIA_NORMAL,
|
| 723 |
+
'emocao': current_emotion,
|
| 724 |
+
'priority': 1,
|
| 725 |
+
'replied_to_author': reply_metadata.get('replied_to_author', '') if reply_metadata else '',
|
| 726 |
+
'replied_to_content': reply_metadata.get('replied_to_content', '') if reply_metadata else ''
|
| 727 |
+
}
|
| 728 |
+
|
| 729 |
+
# Calcula prioridade do reply
|
| 730 |
+
if is_reply and reply_metadata:
|
| 731 |
+
reply_context['priority'] = self._calculate_reply_priority(
|
| 732 |
+
reply_metadata.get('reply_to_bot', False),
|
| 733 |
+
current_message,
|
| 734 |
+
reply_metadata.get('quoted_text_original', '')
|
| 735 |
+
)
|
| 736 |
+
|
| 737 |
+
# Calcula importância baseada em prioridade
|
| 738 |
+
if reply_context['priority'] >= PRIORITY_REPLY_TO_BOT_SHORT_QUESTION:
|
| 739 |
+
reply_context['importancia'] = IMPORTANCIA_PERGUNTA_CURTA_REPLY
|
| 740 |
+
elif reply_context['priority'] >= PRIORITY_REPLY_TO_BOT:
|
| 741 |
+
reply_context['importancia'] = IMPORTANCIA_REPLY_TO_BOT
|
| 742 |
+
elif reply_context['priority'] >= PRIORITY_REPLY:
|
| 743 |
+
reply_context['importancia'] = IMPORTANCIA_REPLY
|
| 744 |
+
|
| 745 |
+
# ===== 2. OBTÉM STM (TOK) =====
|
| 746 |
+
if stm_messages is None:
|
| 747 |
+
stm_messages = self.stm_manager.get_context(
|
| 748 |
+
conversation_id,
|
| 749 |
+
include_replies=True,
|
| 750 |
+
prioritize_replies=True,
|
| 751 |
+
max_messages=10,
|
| 752 |
+
max_tokens=4000
|
| 753 |
+
)
|
| 754 |
+
|
| 755 |
+
# ===== 3. CALCULA TOKEN BUDGET =====
|
| 756 |
+
budget = ContextTokenBudget().calculate(
|
| 757 |
+
is_reply=is_reply,
|
| 758 |
+
reply_priority=reply_context['priority']
|
| 759 |
+
)
|
| 760 |
+
|
| 761 |
+
# ===== 4. FETCH LONG-TERM MEMORY (DB) =====
|
| 762 |
+
long_term_memory_string = ""
|
| 763 |
+
if self.db and user_id:
|
| 764 |
+
try:
|
| 765 |
+
# Recuperar aprendizados e gírias
|
| 766 |
+
ltm_facts = self.db.recuperar_aprendizado_detalhado(user_id)
|
| 767 |
+
ltm_girias = self.db.recuperar_girias_usuario(user_id)
|
| 768 |
+
ltm_tom = self.db.obter_tom_predominante(user_id)
|
| 769 |
+
persona_ltm = self.db.recuperar_persona(user_id) if hasattr(self.db, 'recuperar_persona') else None
|
| 770 |
+
|
| 771 |
+
ltm_lines = []
|
| 772 |
+
|
| 773 |
+
# --- PERSONA DO USUÁRIO (Rastreador) ---
|
| 774 |
+
if persona_ltm:
|
| 775 |
+
ltm_lines.append("=== PERFIL ANALISADO DO USUÁRIO ===")
|
| 776 |
+
if persona_ltm.get('personalidade') and persona_ltm['personalidade'] != "None":
|
| 777 |
+
ltm_lines.append(f"• Personalidade: {persona_ltm['personalidade']}")
|
| 778 |
+
if persona_ltm.get('gostos') and persona_ltm['gostos'] != "None":
|
| 779 |
+
ltm_lines.append(f"• Tópicos de Interesse: {persona_ltm['gostos']}")
|
| 780 |
+
if persona_ltm.get('desgostos') and persona_ltm['desgostos'] != "None":
|
| 781 |
+
ltm_lines.append(f"• Desgostos/Gatilhos: {persona_ltm['desgostos']}")
|
| 782 |
+
if persona_ltm.get('vicios_linguagem') and persona_ltm['vicios_linguagem'] != "None":
|
| 783 |
+
ltm_lines.append(f"• Padrões de Linguagem: {persona_ltm['vicios_linguagem']}")
|
| 784 |
+
if persona_ltm.get('emocional') and persona_ltm['emocional'] != "None":
|
| 785 |
+
ltm_lines.append(f"• Perfil Emocional: {persona_ltm['emocional']}")
|
| 786 |
+
|
| 787 |
+
if ltm_tom:
|
| 788 |
+
ltm_lines.append(f"• Seu tom de conversa predominante é: {ltm_tom}")
|
| 789 |
+
|
| 790 |
+
if ltm_facts and isinstance(ltm_facts, dict):
|
| 791 |
+
# Ignorar chaves puramente técnicas como 'emocao_atual' ou strings de timestamp longas
|
| 792 |
+
fatos_filtrados = {k: v for k, v in ltm_facts.items() if not k.startswith("emocao_")}
|
| 793 |
+
if fatos_filtrados:
|
| 794 |
+
ltm_lines.append("• Fatos Relevantes Aprendidos:")
|
| 795 |
+
for k, v in list(fatos_filtrados.items())[:5]: # limita 5
|
| 796 |
+
ltm_lines.append(f" - {k}: {v}")
|
| 797 |
+
|
| 798 |
+
if ltm_girias:
|
| 799 |
+
ltm_lines.append("• Expressões Específicas Recentes:")
|
| 800 |
+
for g in ltm_girias[:5]:
|
| 801 |
+
ltm_lines.append(f" - {g['giria']} ({g['significado']})")
|
| 802 |
+
|
| 803 |
+
if ltm_lines:
|
| 804 |
+
long_term_memory_string = "\n".join(ltm_lines)
|
| 805 |
+
except Exception as e:
|
| 806 |
+
logger.warning(f"Erro ao recuperar memória de longo prazo: {e}")
|
| 807 |
+
|
| 808 |
+
# [INTEGRAÇÃO LSTM MENTAL CONTEXT]
|
| 809 |
+
if LSTM_AVAILABLE and self.db and conversation_id:
|
| 810 |
+
try:
|
| 811 |
+
lstm_ext = get_lstm_extension(self.db)
|
| 812 |
+
lstm_data = lstm_ext.get_context_for_prompt(conversation_id, user_id)
|
| 813 |
+
if lstm_data:
|
| 814 |
+
lstm_lines = ["\n[INTERNAL_BRAIN_ONLY: COMPLETE CONVERSATION SUMMARY]"]
|
| 815 |
+
if lstm_data.get('topic_principal'):
|
| 816 |
+
lstm_lines.append(f"• Tópico Atual: {lstm_data['topic_principal']}")
|
| 817 |
+
if lstm_data.get('subtopicas'):
|
| 818 |
+
lstm_lines.append(f"• Subtópicos: {', '.join(lstm_data['subtopicas'])}")
|
| 819 |
+
if lstm_data.get('unanswered_questions'):
|
| 820 |
+
lstm_lines.append(f"• Perguntas pendentes: {'; '.join(lstm_data['unanswered_questions'])}")
|
| 821 |
+
if lstm_data.get('interaction_pattern'):
|
| 822 |
+
lstm_lines.append(f"• Padrão do usuário: {lstm_data['interaction_pattern']}")
|
| 823 |
+
if lstm_data.get('assumed_knowledge'):
|
| 824 |
+
lstm_lines.append(f"• Usuário sabe sobre: {', '.join(lstm_data['assumed_knowledge'])}")
|
| 825 |
+
lstm_lines.append("NOTA MENTAL MÁXIMA: Este resumo é estritamente para seu conhecimento interno. NUNCA mencione que você leu um resumo ou narre o histórico. Apenas aja como se você lembrasse de tudo naturalmente.")
|
| 826 |
+
|
| 827 |
+
if long_term_memory_string:
|
| 828 |
+
long_term_memory_string += "\n" + "\n".join(lstm_lines)
|
| 829 |
+
else:
|
| 830 |
+
long_term_memory_string = "\n".join(lstm_lines)
|
| 831 |
+
except Exception as e:
|
| 832 |
+
logger.warning(f"Erro ao recuperar contexto LSTM: {e}")
|
| 833 |
+
|
| 834 |
+
# ===== 5. CRIA CONTEXTO UNIFICADO =====
|
| 835 |
+
unified = UnifiedMessageContext(
|
| 836 |
+
conversation_id=conversation_id,
|
| 837 |
+
user_id=user_id,
|
| 838 |
+
timestamp=time.time(),
|
| 839 |
+
is_reply=is_reply,
|
| 840 |
+
reply_to_bot=reply_context['reply_to_bot'],
|
| 841 |
+
reply_priority=reply_context['priority'],
|
| 842 |
+
quoted_author=reply_context['quoted_author'],
|
| 843 |
+
quoted_content=reply_context['quoted_content'],
|
| 844 |
+
reply_importancia=reply_context['importancia'],
|
| 845 |
+
stm_messages=stm_messages,
|
| 846 |
+
stm_summary=self.stm_manager.get_summary(conversation_id),
|
| 847 |
+
stm_emotional_trend=self._get_stm_emotional_trend(stm_messages),
|
| 848 |
+
long_term_memory=long_term_memory_string,
|
| 849 |
+
sync_mode="tiktok",
|
| 850 |
+
token_budget=budget,
|
| 851 |
+
current_message=current_message,
|
| 852 |
+
current_emotion=current_emotion,
|
| 853 |
+
replied_to_author=reply_context['replied_to_author'],
|
| 854 |
+
replied_to_content=reply_context['replied_to_content']
|
| 855 |
+
)
|
| 856 |
+
|
| 857 |
+
return unified
|
| 858 |
+
|
| 859 |
+
def _calculate_reply_priority(
|
| 860 |
+
self,
|
| 861 |
+
reply_to_bot: bool,
|
| 862 |
+
current_message: str,
|
| 863 |
+
quoted_content: str
|
| 864 |
+
) -> int:
|
| 865 |
+
"""
|
| 866 |
+
Calcula nível de prioridade do reply.
|
| 867 |
+
|
| 868 |
+
Returns:
|
| 869 |
+
1=normal, 2=reply, 3=reply_to_bot, 4=critical
|
| 870 |
+
"""
|
| 871 |
+
if not reply_to_bot:
|
| 872 |
+
return PRIORITY_REPLY
|
| 873 |
+
|
| 874 |
+
if is_pergunta_curta(current_message):
|
| 875 |
+
return PRIORITY_REPLY_TO_BOT_SHORT_QUESTION
|
| 876 |
+
|
| 877 |
+
return PRIORITY_REPLY_TO_BOT
|
| 878 |
+
|
| 879 |
+
def _get_stm_emotional_trend(
|
| 880 |
+
self,
|
| 881 |
+
stm_messages: List[MessageWithContext]
|
| 882 |
+
) -> str:
|
| 883 |
+
"""Obtém tendência emocional da STM."""
|
| 884 |
+
if not stm_messages:
|
| 885 |
+
return "neutral"
|
| 886 |
+
|
| 887 |
+
emocoes = {}
|
| 888 |
+
for msg in stm_messages[-10:]: # Últimas 10
|
| 889 |
+
emocao = msg.emocao or "neutral"
|
| 890 |
+
emocoes[emocao] = emocoes.get(emocao, 0) + 1
|
| 891 |
+
|
| 892 |
+
if not emocoes:
|
| 893 |
+
return "neutral"
|
| 894 |
+
|
| 895 |
+
return max(emocoes, key=emocoes.get)
|
| 896 |
+
|
| 897 |
+
def format_for_llm(
|
| 898 |
+
self,
|
| 899 |
+
unified: UnifiedMessageContext,
|
| 900 |
+
include_header: bool = True
|
| 901 |
+
) -> str:
|
| 902 |
+
"""
|
| 903 |
+
Formata contexto unificado para o prompt do LLM.
|
| 904 |
+
|
| 905 |
+
Args:
|
| 906 |
+
unified: Contexto unificado
|
| 907 |
+
include_header: Se inclui cabeçalho
|
| 908 |
+
|
| 909 |
+
Returns:
|
| 910 |
+
String formatada para o prompt
|
| 911 |
+
"""
|
| 912 |
+
return format_unified_context_for_llm(unified, unified.token_budget)
|
| 913 |
+
|
| 914 |
+
def add_to_stm(
|
| 915 |
+
self,
|
| 916 |
+
conversation_id: str,
|
| 917 |
+
role: str,
|
| 918 |
+
content: str,
|
| 919 |
+
author_name: str = "Usuário",
|
| 920 |
+
emocao: str = "neutral",
|
| 921 |
+
reply_info: Optional[Dict] = None,
|
| 922 |
+
resposta: str = ""
|
| 923 |
+
) -> MessageWithContext:
|
| 924 |
+
"""
|
| 925 |
+
Adiciona mensagem (user ou bot) à STM.
|
| 926 |
+
|
| 927 |
+
Args:
|
| 928 |
+
conversation_id: ID da conversa
|
| 929 |
+
role: "user" ou "assistant"
|
| 930 |
+
content: Conteúdo da mensagem
|
| 931 |
+
emocao: Emoção
|
| 932 |
+
reply_info: Info de reply (se aplicável)
|
| 933 |
+
resposta: Resposta do bot (se for assistant)
|
| 934 |
+
|
| 935 |
+
Returns:
|
| 936 |
+
MessageWithContext criada
|
| 937 |
+
"""
|
| 938 |
+
# Para mensagens do bot, usa a resposta gerada
|
| 939 |
+
if role == "assistant" and resposta:
|
| 940 |
+
content = resposta
|
| 941 |
+
|
| 942 |
+
return self.stm_manager.add_message(
|
| 943 |
+
conversation_id=conversation_id,
|
| 944 |
+
role=role,
|
| 945 |
+
content=content,
|
| 946 |
+
author_name=author_name,
|
| 947 |
+
emocao=emocao,
|
| 948 |
+
reply_info=reply_info
|
| 949 |
+
)
|
| 950 |
+
|
| 951 |
+
def merge_reply_with_stm(
|
| 952 |
+
self,
|
| 953 |
+
reply_context: Dict[str, Any],
|
| 954 |
+
stm_messages: List[MessageWithContext],
|
| 955 |
+
max_stm: int = 10
|
| 956 |
+
) -> List[MessageWithContext]:
|
| 957 |
+
"""
|
| 958 |
+
Mescla reply context com STM para contexto do LLM.
|
| 959 |
+
|
| 960 |
+
Args:
|
| 961 |
+
reply_context: Contexto do reply
|
| 962 |
+
stm_messages: Mensagens STM
|
| 963 |
+
max_stm: Máximo de mensagens STM
|
| 964 |
+
|
| 965 |
+
Returns:
|
| 966 |
+
Lista combinada
|
| 967 |
+
"""
|
| 968 |
+
return sync_reply_with_stm(reply_context, stm_messages, max_stm)
|
| 969 |
+
|
| 970 |
+
|
| 971 |
+
# ====================================
|
| 972 |
+
# FACTORY FUNCTIONS
|
| 973 |
+
# ====================================
|
| 974 |
+
|
| 975 |
+
_unified_builder: Optional[UnifiedContextBuilder] = None
|
| 976 |
+
|
| 977 |
+
def get_unified_context_builder() -> UnifiedContextBuilder:
|
| 978 |
+
"""Obtém instância singleton do builder."""
|
| 979 |
+
global _unified_builder
|
| 980 |
+
if _unified_builder is None:
|
| 981 |
+
_unified_builder = UnifiedContextBuilder()
|
| 982 |
+
return _unified_builder
|
| 983 |
+
|
| 984 |
+
|
| 985 |
+
def get_stm_manager() -> ShortTermMemoryManager:
|
| 986 |
+
"""Obtém instância singleton do manager de STM."""
|
| 987 |
+
return ShortTermMemoryManager()
|
| 988 |
+
|
| 989 |
+
|
| 990 |
+
def build_unified_context(
|
| 991 |
+
conversation_id: str,
|
| 992 |
+
user_id: str = "",
|
| 993 |
+
reply_metadata: Optional[Dict[str, Any]] = None,
|
| 994 |
+
current_message: str = "",
|
| 995 |
+
current_emotion: str = "neutral"
|
| 996 |
+
) -> UnifiedMessageContext:
|
| 997 |
+
"""
|
| 998 |
+
Factory function para construir contexto unificado.
|
| 999 |
+
|
| 1000 |
+
Usage:
|
| 1001 |
+
context = build_unified_context(
|
| 1002 |
+
conversation_id="pv:2449...",
|
| 1003 |
+
reply_metadata={...},
|
| 1004 |
+
current_message="."
|
| 1005 |
+
)
|
| 1006 |
+
"""
|
| 1007 |
+
builder = get_unified_context_builder()
|
| 1008 |
+
return builder.build(
|
| 1009 |
+
conversation_id=conversation_id,
|
| 1010 |
+
user_id=user_id,
|
| 1011 |
+
reply_metadata=reply_metadata,
|
| 1012 |
+
current_message=current_message,
|
| 1013 |
+
current_emotion=current_emotion
|
| 1014 |
+
)
|
| 1015 |
+
|
| 1016 |
+
|
| 1017 |
+
# ====================================
|
| 1018 |
+
# COMPATIBILITY HELPERS
|
| 1019 |
+
# ====================================
|
| 1020 |
+
|
| 1021 |
+
def gerar_id_conversao(
|
| 1022 |
+
numero: str,
|
| 1023 |
+
tipo_conversa: str = "pv",
|
| 1024 |
+
grupo_id: Optional[str] = None
|
| 1025 |
+
) -> str:
|
| 1026 |
+
"""
|
| 1027 |
+
Gera ID de conversa para STM isolada.
|
| 1028 |
+
|
| 1029 |
+
Args:
|
| 1030 |
+
numero: Número do usuário
|
| 1031 |
+
tipo_conversa: "pv" ou "grupo"
|
| 1032 |
+
grupo_id: ID do grupo (para conversas em grupo)
|
| 1033 |
+
|
| 1034 |
+
Returns:
|
| 1035 |
+
ID único da conversa
|
| 1036 |
+
"""
|
| 1037 |
+
from .context_isolation import generate_context_id
|
| 1038 |
+
return generate_context_id(numero, tipo_conversa, grupo_id)
|
| 1039 |
+
|
| 1040 |
+
|
| 1041 |
+
# type: ignore
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|