Spaces:
Running
Running
| # 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}") | |
| 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 | |