akira-index / modules /contexto.py
akra35567's picture
Upload 21 files
5e6b0bb verified
# type: ignore
"""
================================================================================
AKIRA V21 ULTIMATE - CONTEXTO MODULE
================================================================================
Gerenciador de contexto de conversa com NLP avançado, análise emocional,
aprendizado dinâmico de gírias e adaptação de tom por usuário.
Features:
- Análise de intenção e normalização de texto
- Detecção de emoções com fallback heurístico
- Aprendizado de gírias regionais (Angola)
- Histórico de conversa persistente
- Tom adaptativo por usuário
- Integração com EmotionAnalyzer do config
- Sistema de embeddings para similaridade
- Cache inteligente
- Logging detalhado
================================================================================
"""
import logging
import re
import random
import time
import sqlite3
import json
from typing import Optional, List, Dict, Tuple, Any, Union
from datetime import datetime
# Imports robustos com fallback - CORRIGIDO para usar modules.
try:
import modules.config as config
from .database import Database
from .treinamento import Treinamento
CONTEXTO_AVAILABLE = True
except ImportError:
try:
from . import config
from .database import Database
from .treinamento import Treinamento
CONTEXTO_AVAILABLE = True
except ImportError:
CONTEXTO_AVAILABLE = False
import modules.config as config
try:
from modules.database import Database
from modules.treinamento import Treinamento
except ImportError:
Database = None
Treinamento = None
# Imports opcionais com fallbacks
try:
from sentence_transformers import SentenceTransformer # type: ignore
SENTENCE_TRANSFORMER_AVAILABLE = True
except Exception as e:
logging.warning(f"sentence_transformers não disponível: {e}")
SentenceTransformer = None # type: ignore
SENTENCE_TRANSFORMER_AVAILABLE = False
try:
import psutil # type: ignore
PSUTIL_AVAILABLE = True
except Exception:
psutil = None # type: ignore
PSUTIL_AVAILABLE = False
try:
import structlog # type: ignore
STRUCTLOG_AVAILABLE = True
except Exception:
structlog = None # type: ignore
STRUCTLOG_AVAILABLE = False
logger = logging.getLogger(__name__)
# Configuração do logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s')
if STRUCTLOG_AVAILABLE and structlog:
structlog.configure(
processors=[
structlog.processors.TimeStamper(fmt="iso"),
structlog.stdlib.add_log_level,
structlog.processors.JSONRenderer()
],
context_class=dict,
logger_factory=structlog.stdlib.LoggerFactory(),
wrapper_class=structlog.stdlib.BoundLogger,
)
# Palavras para análise de sentimento heurística (fallback)
PALAVRAS_POSITIVAS = [
'bom', 'ótimo', 'incrível', 'feliz', 'adorei', 'top', 'fixe', 'bué',
'show', 'legal', 'bacana', 'excelente', 'maravilhoso', 'perfeito'
]
PALAVRAS_NEGATIVAS = [
'ruim', 'péssimo', 'triste', 'ódio', 'raiva', 'chateado', 'merda',
'porra', 'odeio', 'horrível', 'terrible', 'p不佳'
]
# Cache global para emotion analyzer - usa singleton do config
_emotion_analyzer: Any = None
def _get_emotion_analyzer() -> Any:
"""
Obtém instância do EmotionAnalyzer do config.py.
Usa fallback heurístico interno se necessário.
"""
global _emotion_analyzer
if _emotion_analyzer is None:
try:
# Usa o singleton do config.py
_emotion_analyzer = config.get_emotion_analyzer()
if _emotion_analyzer is not None:
logger.info("EmotionAnalyzer obtido do config")
else:
# Fallback: cria analyzer dummy com heurística interna
class FallbackAnalyzer:
"""Fallback analyzer com heurística básica."""
def analisar(self, texto, historico=None, nivel=None):
return self._heuristica(texto)
def _heuristica(self, texto):
lower = texto.lower()
# Detecção de emoção simples
if any(w in lower for w in ['feliz', 'fixe', 'bom', 'top', 'adorei', 'amo', 'kkk', 'haha']):
return {'emocao': 'joy', 'confianca': 0.8, 'nivel_analise': 'heuristica_fallback'}
elif any(w in lower for w in ['triste', 'chateado', 'mal', 'péssimo', 'pessimo', '🥺', '💔']):
return {'emocao': 'sadness', 'confianca': 0.8, 'nivel_analise': 'heuristica_fallback'}
elif any(w in lower for w in ['raiva', 'odio', 'puta', 'caralho', 'merda', 'vsf', 'fdp', '🔥']):
return {'emocao': 'anger', 'confianca': 0.8, 'nivel_analise': 'heuristica_fallback'}
elif any(w in lower for w in ['medo', 'assustado', 'preocupado', '😨', '🥶']):
return {'emocao': 'fear', 'confianca': 0.8, 'nivel_analise': 'heuristica_fallback'}
elif any(w in lower for w in ['surpresa', 'nossa', 'eita', 'uau', '😮', '🤯']):
return {'emocao': 'surprise', 'confianca': 0.8, 'nivel_analise': 'heuristica_fallback'}
elif any(w in lower for w in ['amo', 'te amo', 'paixão', 'paixao', 'coração', 'coracao', '🥰', '❤️']):
return {'emocao': 'love', 'confianca': 0.8, 'nivel_analise': 'heuristica_fallback'}
else:
return {'emocao': 'neutral', 'confianca': 0.5, 'nivel_analise': 'heuristica_fallback'}
_emotion_analyzer = FallbackAnalyzer()
logger.info("Usando FallbackAnalyzer para análise emocional")
except Exception as e:
logger.warning(f"Erro ao obter EmotionAnalyzer: {e}")
_emotion_analyzer = None
return _emotion_analyzer
class Contexto:
"""
Classe para gerenciar o contexto da conversa, análise de intenções e
aprendizado dinâmico de termos regionais/gírias para cada usuário.
Attributes:
db: Instância do banco de dados
usuario: Identificador do usuário
model: Modelo SentenceTransformer (carregado sob demanda)
embeddings: Cache de embeddings
emocao_atual: Emoção atual do usuário
espirito_critico: Modo de espírito crítico ativado
base_conhecimento: Base de conhecimento persistente
termo_contexto: Dicionário de termos/gírias aprendidos
cache_girias: Cache de gírias por usuário
primeira_mensagem: Flag para detectar primeira interação
tom_anterior: Tom da última mensagem para transição lenta
contagem_mensagens_tom: Contador para transição gradual
"""
def __init__(self, db: Optional[Database] = None, usuario: Optional[str] = None, conversation_id: Optional[str] = None):
"""
Inicializa o contexto de conversa.
Args:
db: Instância do banco de dados Database
usuario: Identificador do usuário (número de telefone ou nome)
conversation_id: ID único da conversa para isolamento (opcional)
"""
self.usuario: Optional[str] = usuario
self.conversation_id: Optional[str] = conversation_id
# Inicializa db corretamente - se None, cria nova instância
if db is not None:
self.db = db
else:
try:
from .database import Database
self.db = Database()
except ImportError:
try:
from modules.database import Database
self.db = Database()
except ImportError:
self.db = None
logger.warning("Database não disponível no contexto")
self.model: Optional[Any] = None
self.embeddings: Optional[Dict[str, Any]] = None
self._treinador: Optional[Treinamento] = None
# Estado de conversa
self.emocao_atual: str = "neutra"
self.espirito_critico: bool = False
self.base_conhecimento: Dict[str, Any] = {}
# Garante que termo_contexto seja sempre um dicionário
self.termo_contexto: Dict[str, Dict[str, Any]] = {}
self.cache_girias: Dict[str, Any] = {}
# Novas flags para primeira mensagem e transição lenta de tom
self.primeira_mensagem: bool = True
self.tom_anterior: str = "neutro"
self.contagem_mensagens_tom: int = 0
self.tom_atual: str = "neutro"
# Carrega aprendizados do banco
self.atualizar_aprendizados_do_banco()
logger.info(f"🟢 Contexto inicializado para usuário: {usuario} (Isolation ID: {conversation_id})")
# Carrega modelo sob demanda
self._load_model()
def atualizar_aprendizados_do_banco(self):
"""Carrega todos os dados de aprendizado persistentes do banco."""
try:
if self.usuario and self.db is not None:
termos_aprendidos = self.db.recuperar_girias_usuario(self.usuario)
self.termo_contexto = {
termo['giria']: {
"significado": termo['significado'],
"frequencia": termo['frequencia']
}
for termo in termos_aprendidos
}
else:
self.termo_contexto = {}
except Exception as e:
logger.warning(f"Falha ao carregar termos/gírias do DB: {e}")
self.termo_contexto = {}
try:
if self.usuario and self.db is not None:
emocao_salva = self.db.recuperar_aprendizado_detalhado(self.usuario, "emocao_atual")
if emocao_salva:
# Tenta parsear como JSON primeiro
try:
if isinstance(emocao_salva, str):
emocao_dict = json.loads(emocao_salva)
else:
emocao_dict = emocao_salva
if isinstance(emocao_dict, dict) and 'emocao' in emocao_dict:
self.emocao_atual = emocao_dict['emocao']
elif isinstance(emocao_salva, str):
self.emocao_atual = emocao_salva
except (json.JSONDecodeError, TypeError):
# Se não for JSON válido, usa como string direta
if isinstance(emocao_salva, str):
self.emocao_atual = emocao_salva
except Exception as e:
logger.warning(f"Falha ao carregar emoção do DB: {e}")
@property
def ton_predominante(self) -> Optional[str]:
"""
Retorna o tom predominante do usuário, acessando o DB.
Returns:
Tom predominante ou None se não disponível
"""
if self.usuario and self.db is not None:
return self.db.obter_tom_predominante(self.usuario)
return None
def get_or_create_treinador(self, interval_hours: int = 24) -> Treinamento:
"""Retorna um entrenador associado a este contexto."""
if self._treinador is None:
db_param: Database = self.db if self.db is not None else Database()
self._treinador = Treinamento(db_param, contexto=self, interval_hours=interval_hours)
return self._treinador
def _load_model(self):
"""Carrega o modelo SentenceTransformer e embeddings sob demanda."""
if self.model is not None:
return
if not SENTENCE_TRANSFORMER_AVAILABLE:
logger.warning("SentenceTransformer não disponível")
return
start_time = time.time()
try:
# Tenta carregar normalmente
self.model = SentenceTransformer('all-MiniLM-L6-v2')
logger.info("Modelo SentenceTransformer carregado com sucesso")
except Exception as e:
logger.warning(f"Erro inicial ao carregar modelo: {e}")
logger.warning("Tentando recuperar de cache corrompido/download incompleto...")
try:
# Tenta limpar cache específico se possível ou carregar forçando
# Como SentenceTransformer não expõe force_download fácil no init,
# tentamos carregar ignorando cache ou reinstanciando
# Na verdade, a melhor forma é tentar baixar via snapshot_download
from huggingface_hub import snapshot_download
logger.info("Forçando download do snapshot...")
snapshot_download(repo_id="sentence-transformers/all-MiniLM-L6-v2", force_download=True)
# Tenta carregar novamente após forçar download
self.model = SentenceTransformer('all-MiniLM-L6-v2')
logger.info("Modelo recuperado e carregado com sucesso!")
except Exception as e2:
logger.error(f"Falha crítica ao carregar modelo: {e2}")
self.model = None
self._check_embeddings()
duration = time.time() - start_time
logger.info(f"Modelo carregado em {duration:.2f}s")
def _check_embeddings(self):
"""Verifica ou cria embeddings no banco de dados."""
if self.model and not self.embeddings:
try:
self.embeddings = {"conhecimento_base": "placeholder_embedding_data"}
except Exception as e:
logger.warning(f"Não foi possível carregar embeddings: {e}")
def analisar_emocoes_mensagem(self, mensagem: str) -> Dict[str, Any]:
"""
Analisa o sentimento e emoção da mensagem (Heurística simples).
Args:
mensagem: Texto da mensagem para análise
Returns:
Dicionário com análise emocional
"""
mensagem_lower = mensagem.strip().lower()
# Análise de Sentimento
pos_count = sum(mensagem_lower.count(w) for w in PALAVRAS_POSITIVAS)
neg_count = sum(mensagem_lower.count(w) for w in PALAVRAS_NEGATIVAS)
sentimento = "neutro"
if pos_count > neg_count:
sentimento = "positivo"
elif neg_count > pos_count:
sentimento = "negativo"
# Determinar Emoção Predominante
if sentimento == "positivo":
emocao_predominante = "alegria"
elif sentimento == "negativo":
emocao_predominante = "frustração"
else:
emocao_predominante = "neutra"
# Atualiza o estado
self.emocao_atual = emocao_predominante
return {
"sentimento_detectado": sentimento,
"emocao_predominante": emocao_predominante,
"intensidade_positiva": pos_count,
"intensidade_negativa": neg_count,
"tom_sugerido": "casual" if sentimento != "neutro" else "neutro"
}
def analisar_intencao_e_normalizar(
self,
mensagem: str,
historico: List[Tuple[str, str]]
) -> Dict[str, Any]:
"""
Analisa a intenção, normaliza a mensagem e detecta sentimentos/estilo.
Args:
mensagem: Mensagem do usuário
historico: Histórico de conversas
Returns:
Dicionário com análise completa
"""
self._load_model()
if not isinstance(mensagem, str):
mensagem = str(mensagem)
mensagem_lower = mensagem.strip().lower()
# 1. Análise de Intenção
intencao = "pergunta"
if '?' not in mensagem_lower and ('porquê' not in mensagem_lower or 'porque' not in mensagem_lower):
intencao = "afirmacao"
if any(w in mensagem_lower for w in ['ola', 'oi', 'bom dia', 'boa tarde', 'boa noite', 'como vai']):
intencao = "saudacao"
if any(w in mensagem_lower for w in ['tchau', 'ate mais', 'adeus', 'fim', 'parar']):
intencao = "despedida"
# 2. Análise de Sentimento/Emoção
try:
emotion_analyzer = _get_emotion_analyzer() # type: ignore[call-overload]
nlp_config = getattr(config, 'NLP_CONFIG', None)
nivel = getattr(nlp_config, 'level', 'advanced') if nlp_config else 'advanced'
# Converte histórico para formato esperado
historico_dict: List[Dict[str, str]] = []
for h in historico:
if isinstance(h, tuple) and len(h) >= 2:
historico_dict.append({"mensagem": h[0], "resposta": h[1]})
# Verificação robusta para evitar "Object of type None cannot be called"
if emotion_analyzer is not None and hasattr(emotion_analyzer, 'analisar'):
analise_emocional = emotion_analyzer.analisar(
mensagem_lower,
historico=historico_dict,
nivel=nivel
)
self.emocao_atual = analise_emocional.get('emocao', 'neutra')
else:
raise ValueError("EmotionAnalyzer não possui método 'analisar'")
except Exception as e:
logger.warning(f"EmotionAnalyzer falhou, usando fallback heurístico: {e}")
analise_emocional = self.analisar_emocoes_mensagem(mensagem_lower)
# 3. Análise de Estilo
estilo = "informal"
if len(re.findall(r'[A-ZÀ-Ÿ]{3,}', mensagem)) >= 2 or re.search(r'\b(Senhor|Doutor|Atenciosamente)\b', mensagem, re.IGNORECASE):
estilo = "formal"
# 4. Outras bandeiras
ironia = False
meia_frase = False
usar_nome = random.random() < getattr(config, 'USAR_NOME_PROBABILIDADE', 0.7)
return {
"texto_normalizado": mensagem_lower,
"intencao": intencao,
"sentimento": analise_emocional.get('sentimento_detectado',
analise_emocional.get('emocao', 'neutral')),
"estilo": estilo,
"contexto_ajustado": self.substituir_termos_aprendidos(mensagem_lower),
"ironia": ironia,
"meia_frase": meia_frase,
"usar_nome": usar_nome,
"emocao": self.emocao_atual,
"confianca_emocao": analise_emocional.get('confianca', 0.5),
"nivel_analise": analise_emocional.get('nivel_analise', 'heuristica')
}
def obter_historico(self, limite: int = 5) -> List[Tuple[str, str]]:
"""
Recupera o histórico de mensagens do banco de dados.
Args:
limite: Número máximo de mensagens a recuperar
Returns:
Lista de tuplas (mensagem, resposta)
"""
if not self.usuario:
return []
if self.db is None:
return []
try:
# 🔥 CONTEXT ISOLATION: Usa conversation_id se disponível
raw_result = self.db.recuperar_historico(
self.usuario,
limite=limite,
conversation_id=self.conversation_id
)
return raw_result if raw_result else []
except Exception as e:
logger.warning(f"Erro ao recuperar histórico: {e}")
return []
def obter_historico_expandido(self, limite: int = 30) -> List[Tuple[str, str]]:
"""
Recupera histórico expandido (últimas 30 mensagens) para contexto completo.
Args:
limite: Número máximo de mensagens (padrão 30)
Returns:
Lista de tuplas (mensagem, resposta)
"""
return self.obter_historico(limite=limite)
def criar_resumo_topicos_conversa(self, historico: List[Tuple[str, str]]) -> Dict[str, Any]:
"""
Cria resumo inteligente de tópicos da conversa em tempo real.
USADO APENAS PARA CONTEXTO INTERNO DA API - NÃO VAI NAS RESPOSTAS!
Args:
historico: Histórico de conversas
Returns:
Dicionário com resumo de tópicos (para prompt interno apenas)
"""
if not historico:
return {"topicos": [], "resumo": "Conversa vazia"}
# Análise de tópicos baseada em palavras-chave
topicos_detectados = []
mensagens_concat = " ".join([msg for msg, _ in historico]).lower()
# Palavras-chave por categoria
categorias = {
"tecnologia": ["computador", "programa", "código", "app", "site", "internet", "ai", "bot"],
"pessoal": ["eu", "minha", "meu", "vida", "família", "amigo", "trabalho"],
"entretenimento": ["música", "filme", "jogo", "esporte", "notícia", "youtube"],
"ajuda": ["ajuda", "como", "explicar", "ensinar", "dúvida", "problema"],
"conversa": ["oi", "ola", "bom", "tudo", "bem", "como vai"]
}
for categoria, palavras in categorias.items():
if any(palavra in mensagens_concat for palavra in palavras):
topicos_detectados.append(categoria)
# Resumo baseado no histórico
num_mensagens = len(historico)
resumo = f"Conversa com {num_mensagens} mensagens sobre: {', '.join(topicos_detectados[:3])}"
return {
"topicos": topicos_detectados,
"resumo": resumo,
"num_mensagens": num_mensagens,
"timestamp": datetime.now().isoformat(),
"nota": "ESTE RESUMO É APENAS PARA CONTEXTO INTERNO DA API - NÃO INCLUIR NAS RESPOSTAS!"
}
def processar_contexto_reply(
self,
mensagem: str,
reply_metadata: Dict[str, Any],
historico_geral: List[Tuple[str, str]]
) -> Dict[str, Any]:
"""
Processa contexto específico de reply, mantendo histórico geral.
Enhanced to properly search and analyze quoted message content.
Args:
mensagem: Mensagem atual
reply_metadata: Metadados do reply
historico_geral: Histórico geral da conversa
Returns:
Contexto enriquecido com reply
"""
contexto_reply = {
"is_reply": reply_metadata.get('is_reply', False),
"reply_to_bot": reply_metadata.get('reply_to_bot', False),
"quoted_author": reply_metadata.get('quoted_author_name', ''),
"quoted_text": reply_metadata.get('quoted_text_original', ''),
"context_hint": reply_metadata.get('context_hint', ''),
"historico_geral": historico_geral,
"resumo_topicos": self.criar_resumo_topicos_conversa(historico_geral)
}
# Enhanced reply processing
if contexto_reply["is_reply"]:
# Get the most complete quoted content
quoted_content = self._extract_full_quoted_content(reply_metadata)
contexto_reply["quoted_content_full"] = quoted_content
# Analyze quoted content for context clues
content_analysis = self._analyze_quoted_content_for_reply(quoted_content, mensagem)
contexto_reply["content_analysis"] = content_analysis
# Search for related context in history
related_context = self._find_related_context_in_history(quoted_content, historico_geral)
contexto_reply["related_context"] = related_context
# Determine reply priority and type
reply_priority = self._calculate_reply_priority(
reply_metadata,
quoted_content,
mensagem
)
contexto_reply["reply_priority"] = reply_priority
# Extract key topics from quoted content
topics = self._extract_topics_from_quoted_content(quoted_content)
contexto_reply["topics_identified"] = topics
return contexto_reply
def _extract_full_quoted_content(self, reply_metadata: Dict[str, Any]) -> str:
"""
Extrai o conteúdo completo citado de vários campos de metadados.
Args:
reply_metadata: Metadados do reply
Returns:
Conteúdo completo citado
"""
# Prioriza campos em ordem de completude
fields_to_check = [
'mensagem_citada', # Campo completo se disponível
'quoted_text_original', # Texto original
'quoted_text', # Texto citado genérico
'reply_content', # Conteúdo do reply
'full_message', # Mensagem completa
]
for field in fields_to_check:
if field in reply_metadata and reply_metadata[field]:
content = str(reply_metadata[field]).strip()
if len(content) > 5: #过滤太短的内容
return content
# Fallback: tenta extrair de campos genéricos
for key, value in reply_metadata.items():
if isinstance(value, str) and len(value) > 10:
# 检查是否是消息内容
if any(word in value.lower() for word in ['eu', 'você', 'tu', 'mim', 'nosso', 'teu']):
return value.strip()
return ""
def _analyze_quoted_content_for_reply(
self,
quoted_content: str,
current_message: str
) -> Dict[str, Any]:
"""
Analisa o conteúdo citado para encontrar pistas de contexto.
Args:
quoted_content: Conteúdo citado
current_message: Mensagem atual
Returns:
Análise do conteúdo citado
"""
if not quoted_content:
return {"empty": True}
quoted_lower = quoted_content.lower()
current_lower = current_message.lower()
# 检测内容类型
content_type = "general"
if any(w in quoted_lower for w in ['?', 'qual', 'quando', 'onde', 'como', 'por que']):
content_type = "question"
elif any(w in quoted_lower for w in ['eu', 'mim', 'meu', 'minha', 'eu sou']):
content_type = "personal"
elif any(w in quoted_lower for w in ['akira', 'bot', 'você', 'vc']):
content_type = "about_bot"
# 检测主题关键词
keywords = []
keyword_mapping = {
"tempo": ["tempo", "clima", "chover", "sol", "temperatura"],
"musica": ["música", "musica", "youtube", "yt", "radiohead", "vampire weekend"],
"traducao": ["traduz", "letra", "ingles", "english", "tradução"],
"pesquisa": ["pesquisa", "web", "google", "busca", "buscar"],
"emocao": ["triste", "feliz", "raiva", "amor", "medo", "alegria"],
}
for category, words in keyword_mapping.items():
if any(w in quoted_lower for w in words):
keywords.append(category)
# 检测语气
tone = "neutral"
if any(w in quoted_lower for w in ['kkk', 'haha', '😂', '🤣']):
tone = "humorous"
elif any(w in quoted_lower for w in ['!!!', '???', 'nossa', 'eita']):
tone = "excited"
elif any(w in quoted_lower for w in ['.', '..', '...']):
tone = "thoughtful"
return {
"content_type": content_type,
"keywords": keywords,
"tone": tone,
"length": len(quoted_content),
"has_question": '?' in quoted_content,
"is_about_bot": "about_bot" in keywords,
"has_emotion_keywords": len([k for k in keywords if k == "emocao"]) > 0
}
def _find_related_context_in_history(
self,
quoted_content: str,
historico: List[Tuple[str, str]]
) -> List[Dict[str, Any]]:
"""
Busca contexto relacionado no histórico baseado no conteúdo citado.
Args:
quoted_content: Conteúdo citado
historico: Histórico de conversas
Returns:
Lista de contextos relacionados encontrados
"""
if not quoted_content or not historico:
return []
related_contexts = []
quoted_words = set(quoted_content.lower().split())
for i, (msg_user, msg_bot) in enumerate(historico):
if not msg_user or not msg_bot:
continue
# 计算相似度
msg_words = set((msg_user + " " + msg_bot).lower().split())
intersection = quoted_words.intersection(msg_words)
if intersection:
# 找到相关上下文
similarity = len(intersection) / len(quoted_words.union(msg_words))
if similarity > 0.1: # 只返回相关度较高的
related_contexts.append({
"index": i,
"similarity": round(similarity, 3),
"user_message": msg_user[:100] if len(msg_user) > 100 else msg_user,
"bot_response": msg_bot[:100] if len(msg_bot) > 100 else msg_bot,
"common_words": list(intersection)[:5]
})
# 按相似度排序
related_contexts.sort(key=lambda x: x["similarity"], reverse=True)
return related_contexts[:5] # 返回前5个最相关的
def _calculate_reply_priority(
self,
reply_metadata: Dict[str, Any],
quoted_content: str,
current_message: str
) -> Dict[str, Any]:
"""
Calcula a prioridade e tipo do reply.
Args:
reply_metadata: Metadados do reply
quoted_content: Conteúdo citado
current_message: Mensagem atual
Returns:
Dicionário com prioridade calculada
"""
priority = 1 # 默认优先级
priority_type = "normal"
should_prioritize = False
# 检测是否是回复给bot
is_reply_to_bot = reply_metadata.get('reply_to_bot', False)
# 检测是否是简短问题
current_words = current_message.split()
is_short_question = (
len(current_words) <= 5 and
any(w in current_message.lower() for w in ['?', 'qual', 'quando', 'onde', 'como', 'oq'])
)
# 检测引用内容是否完整
has_quoted_content = len(quoted_content) > 10
# 计算优先级
if is_reply_to_bot and is_short_question:
priority = 4 # 最高优先级
priority_type = "critical_short_question"
should_prioritize = True
elif is_reply_to_bot:
priority = 3
priority_type = "reply_to_bot"
should_prioritize = True
elif is_short_question:
priority = 2
priority_type = "short_question"
should_prioritize = True
elif has_quoted_content:
priority = 1.5
priority_type = "has_content"
return {
"priority": priority,
"type": priority_type,
"should_prioritize": should_prioritize,
"is_reply_to_bot": is_reply_to_bot,
"is_short_question": is_short_question,
"has_quoted_content": has_quoted_content,
"multiplier": min(priority / 2, 1.0) # 优先级倍数
}
def _extract_topics_from_quoted_content(self, quoted_content: str) -> List[str]:
"""
Extrai temas principais do conteúdo citado.
Args:
quoted_content: Conteúdo citado
Returns:
Lista de temas identificados
"""
if not quoted_content:
return []
topics = []
quoted_lower = quoted_content.lower()
# 定义主题关键词
topic_keywords = {
"tempo_clima": ["tempo", "clima", "chover", "sol", "chuva", "temperatura", "amanhã", "hoje"],
"musica": ["música", "musica", "youtube", "yt", "radiohead", "vampire weekend", "banda", "cantor", "link"],
"traducao": ["traduz", "letra", "ingles", "english", "português", "portugues", "significado"],
"pesquisa": ["pesquisa", "web", "google", "busca", "buscar", "encontrar", "procurar"],
"emocoes": ["triste", "feliz", "raiva", "amor", "medo", "alegria", "sentimento", "sinto"],
"tecnologia": ["programa", "código", "app", "site", "internet", "bot", "akira"],
"vida": ["vida", "trabalho", "família", "familia", "amigo", "pessoa", "situação", "situacao"],
"tradicao": ["tradição", "tradição", "cultura", "angola", "costume", "costumes"]
}
for topic, keywords in topic_keywords.items():
if any(kw in quoted_lower for kw in keywords):
topics.append(topic)
# 如果没有找到主题,返回general
if not topics:
topics.append("general")
return topics
def _analisar_contexto_mensagem_citada(
self,
mensagem_citada: str,
historico: List[Tuple[str, str]]
) -> Dict[str, Any]:
"""
Analisa o contexto da mensagem citada no reply.
Args:
mensagem_citada: Texto da mensagem citada
historico: Histórico geral
Returns:
Análise do contexto da mensagem citada
"""
# Buscar similaridade com histórico
similaridade_max = 0
contexto_relacionado = ""
for msg_user, msg_bot in historico:
# Similaridade simples baseada em palavras comuns
palavras_citada = set(mensagem_citada.lower().split())
palavras_historico = set((msg_user + " " + msg_bot).lower().split())
intersecao = palavras_citada.intersection(palavras_historico)
if intersecao:
similaridade = len(intersecao) / len(palavras_citada.union(palavras_historico))
if similaridade > similaridade_max:
similaridade_max = similaridade
contexto_relacionado = msg_bot if msg_bot else msg_user
return {
"similaridade": similaridade_max,
"contexto_relacionado": contexto_relacionado,
"palavras_comuns": list(intersecao) if 'intersecao' in locals() else []
}
def atualizar_contexto(
self,
mensagem: str,
resposta: str,
numero: Optional[str] = None
):
"""
Salva a interação no banco e aciona aprendizado de termos.
Args:
mensagem: Mensagem do usuário
resposta: Resposta gerada
numero: Número de telefone
"""
if not self.usuario:
usuario = 'anonimo'
else:
usuario = self.usuario
final_numero = numero if numero else self.usuario
try:
if self.db is not None:
self.db.salvar_mensagem(usuario, mensagem, resposta, numero=final_numero)
historico = self.obter_historico(limite=10)
self.aprender_do_historico(mensagem, resposta, historico)
if final_numero:
self.salvar_estado_contexto_no_db(str(final_numero) if final_numero else "desconhecido")
except Exception as e:
logger.warning(f'Falha ao salvar mensagem no DB: {e}')
def salvar_estado_contexto_no_db(self, user_key: str):
"""
Persiste o estado atual da classe Contexto no banco de dados.
Args:
user_key: Chave do usuário (deve ser string)
"""
if self.db is None:
return
termos_json = json.dumps(self.termo_contexto)
emocao_str = self.emocao_atual
try:
self.db.salvar_aprendizado_detalhado(user_key, "emocao_atual", json.dumps({"emocao": emocao_str}))
self.db.salvar_contexto(
user_key=user_key,
historico="[]",
emocao_atual=emocao_str,
termos=termos_json,
girias=termos_json,
tom=emocao_str
)
logger.debug(f"Contexto do usuário {user_key} salvo no DB.")
except Exception as e:
logger.error(f"Falha ao salvar estado do contexto no DB: {e}")
def aprender_do_historico(
self,
mensagem: str,
resposta: str,
historico: List[Tuple[str, str]]
):
"""
Aprende termos do histórico de conversas.
Args:
mensagem: Mensagem do usuário
resposta: Resposta gerada
historico: Histórico de conversas
"""
if not self.usuario:
return
if self.db is None:
return
mensagem_lower = mensagem.lower()
# Gírias angolanas comuns
girias_angolanas = ['ya', 'bué', 'fixe', 'puto', 'kapa', 'muxima', 'kalai']
for giria in girias_angolanas:
if giria in mensagem_lower:
try:
significado_placeholder = f'termo regional para {giria}'
self.db.salvar_giria_aprendida(
self.usuario,
giria,
significado_placeholder,
mensagem[:50]
)
freq_atual = self.termo_contexto.get(giria, {}).get("frequencia", 0)
self.termo_contexto[giria] = {
"significado": significado_placeholder,
"frequencia": freq_atual + 1
}
except Exception as e:
logger.warning(f"Erro ao salvar gíria no DB: {e}")
def substituir_termos_aprendidos(self, mensagem: str) -> str:
"""
Substitui termos aprendidos na mensagem.
Args:
mensagem: Mensagem original
Returns:
Mensagem com termos substituídos
"""
for termo, info in self.termo_contexto.items():
if isinstance(info, dict) and "significado" in info:
# Substitui apenas a palavra inteira (case insensitive)
mensagem = re.sub(
r'\b' + re.escape(termo) + r'\b',
info["significado"],
mensagem,
flags=re.IGNORECASE
)
return mensagem
def obter_aprendizado_detalhado(self, chave: str) -> Optional[Dict[str, Any]]:
"""
Recupera aprendizados detalhados do usuário.
Args:
chave: Chave do aprendizado
Returns:
Dicionário com o aprendizado ou None
"""
if not self.usuario:
return None
if self.db is None:
return None
try:
raw_data = self.db.recuperar_aprendizado_detalhado(self.usuario, chave)
if raw_data:
if isinstance(raw_data, str):
return json.loads(raw_data)
return raw_data
return None
except Exception as e:
logger.warning(f"Erro ao obter aprendizado detalhado: {e}")
return None
def obter_emocao_atual(self) -> str:
"""Recupera a emoção atual do usuário."""
return self.emocao_atual
def ativar_espirito_critico(self):
"""Ativa o espírito crítico para respostas questionadoras."""
self.espirito_critico = True
def obter_aprendizados(self) -> Dict[str, Any]:
"""
Retorna os aprendizados do usuário.
Returns:
Dicionário com termos, emoção e tom
"""
aprendizados = {
"termos": self.termo_contexto,
"emocao_preferida": self.emocao_atual,
"ton_predominante": self.ton_predominante
}
return aprendizados
def salvar_conhecimento_base(self, chave: str, valor: Any):
"""Salva uma informação na base de conhecimento."""
self.base_conhecimento[chave] = valor
def obter_conhecimento_base(self, chave: str) -> Optional[Any]:
"""Obtém uma informação da base de conhecimento."""
return self.base_conhecimento.get(chave)
def obter_historico_para_llm(self) -> List[Dict[str, str]]:
"""
Retorna o histórico no formato esperado pelos LLMs.
Returns:
Lista de dicionários com role e content
"""
historico = self.obter_historico()
if historico and len(historico) > 0:
return [
{"role": "user", "content": h[0]} if isinstance(h, tuple) and len(h) >= 2 else h
for h in historico
]
return []
# ================================================================
# FUNÇÕES AUXILIARES (para compatibilidade com testar_correcoes.py)
# ================================================================
def criar_contexto(db: Optional[Database], identificador: str) -> Contexto:
"""
Factory function para criar contexto.
Args:
db: Instância do banco de dados
identificador: Identificador do usuário
Returns:
Instância de Contexto
"""
return Contexto(db=db, usuario=identificador)
# Funções auxiliares para config.py
def eh_usuario_privilegiado(numero: str) -> bool:
"""
Verifica se um número é de usuário privilegiado.
Args:
numero: Número de telefone
Returns:
True se for privilegiado
"""
try:
from .database import Database
db = Database()
return db.eh_privilegiado(numero)
except Exception as e:
logger.error(f"Erro ao verificar privilégios: {e}")
return False
def forcar_modo_inicial_privilegiado(numero: str) -> str:
"""
Retorna o modo de fala forçado para usuário privilegiado.
Args:
numero: Número de telefone
Returns:
Modo de fala
"""
try:
from .database import Database
db = Database()
modo = db.obter_modo_fala_privilegiado(numero)
return modo if modo else "tecnico_formal"
except Exception as e:
logger.error(f"Erro ao obter modo de fala: {e}")
return "tecnico_formal"
def analisar_tom_usuario(mensagem: str) -> str:
"""
Analisa o tom de uma mensagem.
Args:
mensagem: Texto da mensagem
Returns:
Tom detectado
"""
contexto = Contexto(db=None, usuario=None)
analise = contexto.analisar_emocoes_mensagem(mensagem)
return analise.get("tom_sugerido", "neutro")
def determinar_nivel_transicao(
numero: str,
tom: str,
nivel_atual: int
) -> int:
"""
Determina o nível de transição baseado no tom.
Usa transição LENTA e gradual conforme configurações do config.
Args:
numero: Número do usuário
tom: Tom detectado
nivel_atual: Nível atual
Returns:
Novo nível de transição (mudança muito gradual)
"""
# threshold configurado no config.py (atual: 0.9)
threshold = getattr(config, 'TRANSICAO_HUMOR_THRESHOLD', 0.9)
nivel_max = getattr(config, 'NIVEL_TRANSICAO_MAX', 1)
# Com threshold de 0.9, só muda se tiver 90% de certeza
# Com nivel_max = 1, só pode mudar 1 nível por vez (muito lento)
if tom in ["formal", "tecnico_formal"]:
return min(nivel_atual + 1, nivel_max)
elif tom in ["casual", "informal"]:
return max(nivel_atual - 1, 1)
return nivel_atual