akira / modules /user_profiler.py
akra35567's picture
Upload user_profiler.py
ba70b93 verified
"""
================================================================================
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()