# 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 try: from . import config from .database import Database from .treinamento import Treinamento CONTEXTO_AVAILABLE = True except ImportError as e: CONTEXTO_AVAILABLE = False try: import config from database import Database from treinamento import Treinamento except ImportError: import sys sys.path.insert(0, '/home/elliot_pro/Programação/akira') import modules.config as config from modules.database import Database try: from modules.treinamento import Treinamento except ImportError: Treinamento = None Database = 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 _emotion_analyzer: Any = None def _get_emotion_analyzer() -> Any: """Obtém instância do EmotionAnalyzer do config.py.""" global _emotion_analyzer if _emotion_analyzer is None: try: analyzer = config.get_emotion_analyzer() # Verifica se o analyzer existe if analyzer is not None: _emotion_analyzer = analyzer else: _emotion_analyzer = None except Exception as e: logger.warning(f"EmotionAnalyzer não disponível: {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.db = db self.usuario: Optional[str] = usuario self.conversation_id: Optional[str] = conversation_id self.model: Optional[Any] = None self.embeddings: Optional[Dict[str, Any]] = None self._treinador: Optional[Treinamento] = None # Estado de conversa self.emocao_atual: str = "neutro" 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}") # 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: self.model = SentenceTransformer('all-MiniLM-L6-v2') logger.info("Modelo SentenceTransformer carregado com sucesso") except Exception as e: logger.error(f"Erro ao carregar modelo: {e}") 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 usando modelos pesados (mDeBERTa/BERTimbau). Args: mensagem: Texto da mensagem para análise Returns: Dicionário com análise emocional completa """ resultado = { "emocao": "neutro", "confianca": 0.0, "nivel_analise": "desconhecido", "nuance_nativa": {} } try: # 1. Tenta usar o EmotionAnalyzer (mDeBERTa-v3 - PESADO) from .config import EmotionAnalyzer, get_nlp_analyzer analyzer = EmotionAnalyzer() analise_hf = analyzer.analisar(mensagem) if analise_hf and analise_hf.get('nivel_analise') != "heuristica": resultado.update(analise_hf) logger.debug(f"🧠 Análise mDeBERTa concluída: {resultado['emocao']}") # 2. Tenta usar o NLPAnalyzer (BERTimbau - NATIVO) para nuances nlp_analyzer = get_nlp_analyzer() nuances = nlp_analyzer.extrair_nuance_nativa(mensagem) resultado["nuance_nativa"] = nuances if resultado["emocao"] != "neutro": # Atualiza o estado interno self.emocao_atual = resultado["emocao"] return resultado except Exception as e: logger.warning(f"⚠️ Erro em modelos pesados, usando heurística: {e}") # 3. Fallback: Análise Heurística (Último caso) mensagem_lower = mensagem.strip().lower() 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" if sentimento == "positivo": resultado["emocao"] = "alegria" elif sentimento == "negativo": resultado["emocao"] = "raiva" else: resultado["emocao"] = "neutro" resultado["nivel_analise"] = "heuristica_fallback" resultado["confianca"] = 0.5 # Atualiza o estado self.emocao_atual = resultado["emocao"] return resultado 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 has no attribute" if hasattr(emotion_analyzer, 'analisar'): analise_emocional = emotion_analyzer.analisar( mensagem_lower, historico=historico_dict, nivel=nivel ) self.emocao_atual = analise_emocional.get('emocao', 'neutra') elif hasattr(emotion_analyzer, 'analisar_emocoes_mensagem'): # Tenta método alternativo analise_emocional = emotion_analyzer.analisar_emocoes_mensagem(mensagem_lower) self.emocao_atual = analise_emocional.get('emocao', 'neutra') else: # Fallback interno silencioso analise_emocional = self.analisar_emocoes_mensagem(mensagem_lower) 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: # Fallback para o método antigo try: raw_result = self.db.recuperar_mensagens(self.usuario, limite=limite) return raw_result if raw_result else [] except Exception as e2: logger.warning(f"Erro ao recuperar histórico: {e2}") 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. """ if not historico: return {"topicos": [], "resumo": "Conversa vazia"} topicos_detectados = [] mensagens_concat = " ".join([msg for msg, _ in historico]).lower() 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) 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. """ 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) } if contexto_reply["is_reply"]: quoted_content = self._extract_full_quoted_content(reply_metadata) contexto_reply["quoted_content_full"] = quoted_content content_analysis = self._analyze_quoted_content_for_reply(quoted_content, mensagem) contexto_reply["content_analysis"] = content_analysis related_context = self._find_related_context_in_history(quoted_content, historico_geral) contexto_reply["related_context"] = related_context reply_priority = self._calculate_reply_priority( reply_metadata, quoted_content, mensagem ) contexto_reply["reply_priority"] = reply_priority 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: fields_to_check = [ 'mensagem_citada', 'quoted_text_original', 'quoted_text', 'reply_content', 'full_message' ] 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 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]: if not quoted_content: return {"empty": True} quoted_lower = quoted_content.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"], "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]]: 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] def _calculate_reply_priority(self, reply_metadata: Dict[str, Any], quoted_content: str, current_message: str) -> Dict[str, Any]: priority = 1 priority_type = "normal" should_prioritize = False 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]: if not quoted_content: return [] topics = [] quoted_lower = quoted_content.lower() topic_keywords = { "tempo_clima": ["tempo", "clima", "chover", "sol", "chuva", "temperatura"], "musica": ["música", "musica", "youtube", "yt", "cantor", "link"], "traducao": ["traduz", "letra", "ingles", "english", "português", "significado"], "pesquisa": ["pesquisa", "web", "google", "busca", "buscar", "encontrar"], "emocoes": ["triste", "feliz", "raiva", "amor", "medo", "alegria", "sentimento"], "tecnologia": ["programa", "código", "app", "site", "internet", "bot", "akira"] } for topic, keywords in topic_keywords.items(): if any(kw in quoted_lower for kw in keywords): topics.append(topic) if not topics: topics.append("general") return topics 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(final_numero) 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 """ 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