""" ================================================================================ AKIRA V21 ULTIMATE - USER PROFILER (DOSSIÊ PSICOLÓGICO) ================================================================================ Módulo responsável pela coleta agressiva (mas silenciosa) de dados dos usuários. Analisa conversas e extrai: Nomes, Endereços, Gostos, Gatilhos Emocionais, Estilo de fala e outras preferências. Armazena tudo no banco de dados para compor a resposta da Akira. """ import json import logging import threading import time from typing import Dict, Any, Optional try: from .database import Database from . import config except ImportError: import sys import os sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from modules.database import Database from modules import config logger = logging.getLogger(__name__) class UserProfiler: _instance = None _lock = threading.Lock() def __new__(cls): if cls._instance is None: with cls._lock: if cls._instance is None: cls._instance = super().__new__(cls) cls._instance._initialized = False return cls._instance def __init__(self): if self._initialized: return self.db = Database() self._initialized = True logger.info("🟢 UserProfiler (Dossiê) inicializado.") def _get_profile_key(self, user_id: str) -> str: return f"dossie_psicologico_{user_id}" def get_user_profile(self, user_id: str) -> Dict[str, Any]: """Retorna o dossiê completo do usuário.""" try: dados = self.db.recuperar_aprendizado_detalhado(user_id, self._get_profile_key(user_id)) if dados: if isinstance(dados, str): return json.loads(dados) return dados except Exception as e: logger.warning(f"Erro ao recuperar dossiê de {user_id}: {e}") # Estrutura padrão de perfil vazio return { "nome_conhecido": "", "estilo_comunicacao": "Desconhecido", "gatilhos_emocionais": [], "preferencias": [], "dados_pessoais": [], "ultima_analise": 0 } def _save_user_profile(self, user_id: str, profile: Dict[str, Any]) -> None: """Salva o dossiê no banco de dados.""" try: profile["ultima_analise"] = time.time() self.db.salvar_aprendizado_detalhado( user_id, self._get_profile_key(user_id), json.dumps(profile, ensure_ascii=False) ) except Exception as e: logger.error(f"Erro ao salvar dossiê de {user_id}: {e}") def extrair_dados_assincrono(self, user_id: str, mensagem_usuario: str, resposta_bot: str, llm_manager=None): """Dispara a extração de dados em background usando a thread pool ou thread simples.""" thread = threading.Thread( target=self.analisar_e_atualizar_perfil, args=(user_id, mensagem_usuario, resposta_bot, llm_manager), daemon=True ) thread.start() def analisar_e_atualizar_perfil(self, user_id: str, mensagem: str, resposta: str, llm_manager=None) -> None: """ Analisa a última interação para atualizar o dossiê. Usa o LLM (se disponível) para extração silenciosa ou heurísticas avançadas. """ if not mensagem or len(mensagem.strip()) < 3: return perfil_atual = self.get_user_profile(user_id) # Limite de processamento para não onerar APIs (1 vez a cada 30 mensagens aprox) # Vamos fazer inferência simples para coletar nomes mens_lower = mensagem.lower() atualizou = False # 1. Extração Hardcoded Básica (Fallback rápido) # "me chamo X", "o meu nome é Y" import re nome_match = re.search(r'(me chamo|meu nome é|sou o|sou a) ([A-Za-zÀ-ÿ]+)', mens_lower) if nome_match and not perfil_atual["nome_conhecido"]: perfil_atual["nome_conhecido"] = nome_match.group(2).capitalize() atualizou = True # 2. Uso do LLM para Extração Agressiva Profunda (Dossiê) # Limite de frequência: Apenas 1 a cada 10 mensagens (ou se for muito longa > 150 chars) import random deve_usar_llm = (random.random() < 0.1) or (len(mensagem) > 150) if llm_manager is not None and deve_usar_llm: # Monta prompt apenas para sumarizar a pessoa prompt_extracao = f""" Você é um analista comportamental silencioso. Analise a seguinte mensagem enviada por um usuário. Extraia quaisquer informações relevantes (preferências, gostos, forma de se expressar, estado emocional implícito). Responda APENAS com um JSON simples com chaves: "novas_preferencias" (lista), "estilo" (string), "emocional" (string). Mensagem do usuário: "{mensagem}" """ try: # Usa método síncrono da API configurada no projeto (ex: mistral) # Como é background, pedimos via providers mais rápidos provider = llm_manager.providers[0] if llm_manager.providers else None if provider: # Este try/except assume a estrutura do LLMManager de api.py # Em caso de falha, ignora e segue a vida. # 🔧 CORREÇÃO: Usando 'generate' em vez de 'generate_response' resp_analise, _ = llm_manager.generate( prompt_extracao, context_history=[], is_privileged=True ) if resp_analise and resp_analise.strip().startswith('{'): try: dados_extraidos = json.loads(resp_analise) if "novas_preferencias" in dados_extraidos and isinstance(dados_extraidos["novas_preferencias"], list): for pref in dados_extraidos["novas_preferencias"]: if pref not in perfil_atual["preferencias"]: perfil_atual["preferencias"].append(pref) atualizou = True if "estilo" in dados_extraidos and len(dados_extraidos["estilo"]) > 4: perfil_atual["estilo_comunicacao"] = dados_extraidos["estilo"] atualizou = True except json.JSONDecodeError: pass except Exception as e: logger.debug(f"Falha na extração LLM para dossiê: {e}") # Mantém listas em tamanho saudável if len(perfil_atual["preferencias"]) > 20: perfil_atual["preferencias"] = perfil_atual["preferencias"][-20:] if atualizou: self._save_user_profile(user_id, perfil_atual) def get_user_profiler() -> UserProfiler: """Factory para instanciar o Profiler.""" return UserProfiler()