File size: 7,544 Bytes
ba70b93
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
"""

================================================================================

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()