import json import threading from loguru import logger from typing import List, Dict, Any, Optional try: from modules.database import Database except ImportError: from database import Database class PersonaTracker: """ Rastreador de Persona em Background (Character.AI style LTM). Analisa as conversas recentes do usuário silenciosamente e extrai seus traços de personalidade, gostos e emoções no banco de dados. """ def __init__(self, db: Database, llm_client: Any): """ Args: db (Database): Instância do banco de dados (database.py) llm_client (Any): Instância do cliente LLM (ex: MultiLLMClient) """ self.db = db self.llm_client = llm_client self.processing_users = set() def track_background(self, numero_usuario: str, historico_recente: List[Dict[str, str]]) -> None: """ Dispara a análise de persona em background para não bloquear a resposta do bot. Args: numero_usuario: ID ou número do usuário. historico_recente: Lista de dicionários {'role': '...', 'content': '...'} com as últimas mensagens do usuário. """ if numero_usuario in self.processing_users: return # Já está a ser analisado neste momento if not historico_recente or len(historico_recente) < 3: return # Muito pouco contexto para extrair algo útil self.processing_users.add(numero_usuario) thread = threading.Thread( target=self._analyze_and_save, args=(numero_usuario, historico_recente), daemon=True ) thread.start() def _analyze_and_save(self, numero_usuario: str, historico: List[Dict[str, str]]) -> None: """Método interno que roda na Thread.""" try: # Recupera a persona atual para o LLM saber o que já sabemos persona_atual = self.db.recuperar_persona(numero_usuario) or {} # Formata histórico apenas com as falas do usuário user_messages = [msg['content'] for msg in historico if msg.get('role') == 'user'] if not user_messages: return historico_texto = "\n".join([f"User: {msg}" for msg in user_messages[-10:]]) # Últimas 10 msg perfil_atual_str = json.dumps(persona_atual, ensure_ascii=False) if persona_atual else "Ainda não definido." prompt = f"""Você é um analista comportamental focado em rastreamento de persona (Long-Term Memory). Analise as mensagens recentes deste usuário e atualize/extraia o seu perfil. [PERFIL ATUAL NO BANCO DE DADOS] {perfil_atual_str} [MENSAGENS RECENTES] {historico_texto} EXTRAIA/ATUALIZE os seguintes traços com base APENAS nas mensagens recentes e no perfil atual. Mantenha os traços do perfil atual que não foram contraditórios. Seja CONCISO. Use bullet points curtos na sua mente e preencha os campos em formato JSON estrito. Retorne APENAS um JSON válido estruturado assim (e NADA de texto fora das chaves): {{ "personalidade": "Resumo calmo, agressivo, divertido, direto, etc.", "vicios_linguagem": "Expressões ou gírias que ele usa muito.", "gostos": "O que ele demonstrou gostar ou tópicos de interesse.", "desgostos": "O que o irrita, o que ele odeia.", "emocional": "Traços emocionais, forças ou gatilhos/fraquezas." }} """ # Chama o LLM (garante formato json) # Agora retorna (resposta, modelo_usado) ou apenas resposta response_raw = self.llm_client.generate(prompt, []) if isinstance(response_raw, tuple): response_json_str = response_raw[0] else: response_json_str = response_raw if not response_json_str: return # Extrai o JSON (Robusto contra texto extra, markdown e quebras parciais) response_clean = response_json_str.strip() # Remove blocos de código if "```json" in response_clean: response_clean = response_clean.split("```json")[1].split("```")[0].strip() elif "```" in response_clean: response_clean = response_clean.split("```")[1].split("```")[0].strip() # Regex robusta para capturar apenas o objeto JSON ignorando texto antes/depois import re json_match = re.search(r'(\{.*\})', response_clean, re.DOTALL) if json_match: response_clean = json_match.group(1) # Limpa escapes invisiveis comuns que quebram o json.loads response_clean = response_clean.replace('\r', '').replace('\n', ' ').replace('\\"', '"').replace("\\'", "'") # Substitui aspas simples mal formadas por aspas duplas se não estiver dentro de um texto response_clean = re.sub(r"(?