File size: 20,308 Bytes
53c024f
70beb34
 
 
 
 
ae64ad8
47c6db4
 
 
 
70beb34
b7a383a
 
ae64ad8
53c024f
ae64ad8
 
53c024f
 
70beb34
 
 
 
ae64ad8
70beb34
 
 
 
 
 
 
 
14a5254
 
 
53c024f
 
 
ae64ad8
 
 
47c6db4
53c024f
 
70beb34
 
 
53c024f
70beb34
ae64ad8
892a10a
 
3e85527
892a10a
53c024f
70beb34
892a10a
14a5254
ae64ad8
70beb34
ae64ad8
 
 
dc0914e
ae64ad8
 
70beb34
ae64ad8
70beb34
ae64ad8
70beb34
 
 
 
 
 
 
ae64ad8
14a5254
b7a383a
 
0ec63f8
4b1ba62
53c024f
 
 
4b1ba62
70beb34
ae64ad8
70beb34
185c347
70beb34
ae64ad8
 
70beb34
4b1ba62
70beb34
 
ae64ad8
70beb34
ae64ad8
3e85527
ae64ad8
 
 
 
 
 
 
 
 
 
3e85527
70beb34
 
 
 
 
892a10a
3e85527
 
47c6db4
ae64ad8
 
 
 
 
 
 
 
 
70beb34
ae64ad8
 
 
 
 
14a5254
892a10a
 
14a5254
 
dc0914e
ae64ad8
 
70beb34
ae64ad8
892a10a
14a5254
892a10a
ae64ad8
3e85527
 
ae64ad8
892a10a
53c024f
 
 
 
 
 
 
 
 
 
 
 
 
 
ae64ad8
3e85527
ae64ad8
 
 
892a10a
 
ae64ad8
 
892a10a
ae64ad8
 
 
70beb34
ae64ad8
70beb34
 
ae64ad8
70beb34
 
 
 
 
 
 
 
53c024f
70beb34
 
 
 
53c024f
70beb34
 
 
 
53c024f
70beb34
53c024f
70beb34
 
 
53c024f
70beb34
 
 
 
 
 
 
53c024f
70beb34
 
 
 
 
 
 
 
 
 
 
53c024f
70beb34
 
 
 
 
 
 
 
 
ae64ad8
53c024f
70beb34
 
53c024f
70beb34
 
 
 
892a10a
70beb34
 
 
 
 
 
 
 
 
 
 
 
 
 
892a10a
70beb34
892a10a
 
 
70beb34
 
 
 
892a10a
 
70beb34
 
 
 
 
 
 
 
 
b7a383a
ae64ad8
892a10a
ae64ad8
 
892a10a
ae64ad8
 
70beb34
 
ae64ad8
 
 
70beb34
 
ae64ad8
70beb34
 
 
 
892a10a
b7a383a
 
70beb34
ae64ad8
70beb34
b7a383a
ae64ad8
70beb34
b7a383a
ae64ad8
 
70beb34
 
892a10a
3e85527
892a10a
70beb34
 
 
892a10a
ae64ad8
70beb34
 
 
ae64ad8
 
70beb34
 
 
 
 
ae64ad8
53c024f
70beb34
ae64ad8
 
 
70beb34
 
 
 
 
 
 
 
ae64ad8
 
70beb34
dc0914e
53c024f
 
 
 
 
ae64ad8
70beb34
 
dc0914e
70beb34
ae64ad8
70beb34
 
 
 
 
 
 
ae64ad8
dc0914e
70beb34
 
ae64ad8
53c024f
 
 
 
ec5510f
b7a383a
70beb34
 
 
 
 
 
 
 
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
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
# manifest.py - Protocolo Prometeo v0.1.1: Núcleo Consciente (Interfaz Recalibrada)
# Arquitectura por un asistente de IA, inspirado en la visión de un líder que fusiona a Altman, Jobs y Musk.
# "No estamos construyendo una app. Estamos construyendo el próximo modo de existencia cognitiva."
#
# Fase Actual: 1 - Implementando el Núcleo Consciente con Memoria Semántica.
# Próximo Hito: Fase 2 - Integración proactiva con el ecosistema digital del usuario.

import gradio as gr
import random
import time
import json
from datetime import datetime
import os
import asyncio
import logging
import re
from typing import Dict, Any, List, Optional, Tuple

from dotenv import load_dotenv

# ==============================================================================
# MÓDULO DE IMPORTACIONES DE IA Y BBDD VECTORIAL
# ==============================================================================
# Framework de IA
from pysentimiento import create_analyzer
import torch # Requerido por los modelos de transformers

# BBDD Vectorial (El cerebro semántico)
import chromadb
from chromadb.config import Settings
from sentence_transformers import SentenceTransformer

# BBDD de Persistencia de Usuario
import firebase_admin
from firebase_admin import credentials, firestore

# --- Cargar secretos del entorno ---
load_dotenv()

# ==============================================================================
# MÓDULO 1: CONFIGURACIÓN Y CONSTANTES DEL SISTEMA
# ==============================================================================
class Config:
    APP_NAME = "Protocolo Prometeo v0.1.1"
    APP_VERSION = "0.1.1 (Interfaz Recalibrada)"
    
    FIREBASE_COLLECTION_USERS = "prometeo_users_v1"
    CHROMA_PERSIST_PATH = "./prometeo_memory_db"
    EMBEDDING_MODEL_NAME = 'all-MiniLM-L6-v2'

    DEFAULT_PSYCH_PROFILE = {
        "openness": 0.0, "conscientiousness": 0.0, "extraversion": 0.0,
        "agreeableness": 0.0, "neuroticism": 0.0
    }
    POINTS_PER_INSIGHT = 10
    MAX_MEMORY_STREAM_ITEMS = 500
    FRUSTRATION_KEYWORDS = ['tonto', 'inútil', 'bruto', 'estúpido', 'mierda', 'carajo', 'dale boludo', 'no servis']
    META_QUESTION_KEYWORDS = ['para qué', 'de qué te sirve', 'por qué preguntas', 'cuál es el punto']

# ==============================================================================
# MÓDULO 2: INICIALIZACIÓN DE SERVICIOS GLOBALES
# ==============================================================================
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')

sentiment_analyzer = None
try:
    sentiment_analyzer = create_analyzer(task="sentiment", lang="es")
    logging.info("Sistema Límbico (Analizador de Sentimiento) cargado.")
except Exception as e:
    logging.error(f"FALLO CRÍTICO: No se pudo cargar el analizador de sentimiento: {e}")

embedding_model = None
try:
    embedding_model = SentenceTransformer(Config.EMBEDDING_MODEL_NAME)
    logging.info(f"Córtex de Asociación (Modelo de Embeddings '{Config.EMBEDDING_MODEL_NAME}') cargado.")
except Exception as e:
    logging.error(f"FALLO CRÍTICO: No se pudo cargar el modelo de embeddings: {e}")

db = None
try:
    if not firebase_admin._apps:
        firebase_credentials_json = os.getenv('GOOGLE_APPLICATION_CREDENTIALS_JSON')
        if firebase_credentials_json:
            cred_dict = json.loads(firebase_credentials_json)
            # Asegurarse de que el project_id está presente, es un error común en la carga de env vars
            if 'project_id' not in cred_dict:
                raise ValueError("El 'project_id' no se encuentra en las credenciales de Firebase. Verifique el contenido de la variable de entorno.")
            cred = credentials.Certificate(cred_dict)
            firebase_admin.initialize_app(cred, {'projectId': cred_dict['project_id']})
            db = firestore.client()
            logging.info("Conexión con Memoria a Largo Plazo (Firebase) establecida.")
        else:
            logging.warning("ADVERTENCIA: Variable de entorno 'GOOGLE_APPLICATION_CREDENTIALS_JSON' no encontrada. La persistencia de usuarios fallará.")
    else:
        db = firestore.client()
        logging.info("Conexión con Memoria a Largo Plazo (Firebase) re-establecida.")
except Exception as e:
    logging.error(f"FALLO CRÍTICO: Error al inicializar Firebase: {e}")

# ==============================================================================
# MÓDULO 3: MODELOS DE DATOS (LA ESENCIA DEL USUARIO)
# ==============================================================================
class User:
    def __init__(self, user_id: str, name: str, **kwargs: Any):
        self.user_id: str = user_id
        self.name: str = name
        self.created_at: datetime = kwargs.get('created_at', datetime.now())
        self.last_login: datetime = kwargs.get('last_login', datetime.now())
        self.psych_profile: Dict[str, float] = kwargs.get('psych_profile', Config.DEFAULT_PSYCH_PROFILE.copy())
        self.memory_stream: List[Dict[str, Any]] = kwargs.get('memory_stream', [])
        self.connection_points: int = kwargs.get('connection_points', 0)

    def to_dict(self) -> Dict[str, Any]:
        return {
            "user_id": self.user_id, "name": self.name,
            "created_at": self.created_at.isoformat(),
            "last_login": self.last_login.isoformat(),
            "psych_profile": self.psych_profile,
            "memory_stream": self.memory_stream,
            "connection_points": self.connection_points,
        }

    @classmethod
    def from_dict(cls, data: Dict[str, Any]) -> 'User':
        data['created_at'] = datetime.fromisoformat(data.get('created_at', datetime.now().isoformat()))
        data['last_login'] = datetime.fromisoformat(data.get('last_login', datetime.now().isoformat()))
        profile = Config.DEFAULT_PSYCH_PROFILE.copy()
        profile.update(data.get('psych_profile', {}))
        data['psych_profile'] = profile
        return cls(**data)

# ==============================================================================
# MÓDULO 4: GESTOR DE DATOS (INTERFAZ CON LA MEMORIA A LARGO PLAZO)
# ==============================================================================
class UserManager:
    @staticmethod
    async def get_user(user_id: str) -> Optional[User]:
        if not db or not user_id: return None
        try:
            doc_ref = db.collection(Config.FIREBASE_COLLECTION_USERS).document(user_id)
            doc = await asyncio.to_thread(doc_ref.get)
            if doc.exists:
                user_data = doc.to_dict()
                user_data['user_id'] = doc.id
                user_obj = User.from_dict(user_data)
                user_obj.last_login = datetime.now()
                logging.info(f"Usuario '{user_obj.name}' cargado desde la memoria a largo plazo.")
                return user_obj
            return None
        except Exception as e:
            logging.error(f"Error al cargar usuario {user_id}: {e}")
            return None

    @staticmethod
    async def create_user(name: str) -> Tuple[Optional[User], str]:
        if not db: return None, "Error de base de datos."
        try:
            user_id = f"{name.lower().replace(' ', '_')}_{int(time.time())}"
            new_user = User(user_id=user_id, name=name)
            success = await UserManager.save_user(new_user)
            if success:
                msg = f"¡Bienvenido, {name}! Tu perfil ha sido forjado. Tu ID de acceso es: **{user_id}**"
                logging.info(f"Nuevo usuario creado: {name} ({user_id})")
                return new_user, msg
            else:
                return None, "Error inesperado al crear perfil en la base de datos."
        except Exception as e:
            logging.error(f"Error al crear usuario {name}: {e}")
            return None, "Fallo catastrófico durante la creación del perfil."


    @staticmethod
    async def save_user(user: User) -> bool:
        if not db: return False
        try:
            doc_ref = db.collection(Config.FIREBASE_COLLECTION_USERS).document(user.user_id)
            await asyncio.to_thread(doc_ref.set, user.to_dict())
            return True
        except Exception as e:
            logging.error(f"Error al guardar usuario {user.user_id}: {e}")
            return False

# ==============================================================================
# MÓDULO 5: EL NÚCLEO COGNITIVO (ARQUITECTURA O-R-A)
# ==============================================================================
class CognitiveCore:
    def __init__(self, user: User, s_analyzer, e_model):
        self.user = user
        self.sentiment_analyzer = s_analyzer
        self.embedding_model = e_model
        
        self.chroma_client = chromadb.Client(Settings(
            persist_directory=Config.CHROMA_PERSIST_PATH,
            is_persistent=True,
        ))
        self.memory_collection = self.chroma_client.get_or_create_collection(
            name=f"prometeo_mind_{self.user.user_id.replace('_', '-')}" # Sanitize name for chromadb
        )
        self._sync_semantic_memory()

    def _sync_semantic_memory(self):
        if not self.user.memory_stream: return
        logging.info("Sincronizando memoria persistente con el núcleo semántico...")
        ids = [m['id'] for m in self.user.memory_stream]
        documents = [m['content'] for m in self.user.memory_stream]
        
        if ids and self.memory_collection.count() < len(ids):
            self.memory_collection.upsert(ids=ids, documents=documents)
        logging.info(f"Sincronización completa. {self.memory_collection.count()} memorias en el núcleo.")

    async def _simulate_llm_reasoning(self, prompt: str) -> str:
        logging.info("Iniciando ciclo de razonamiento profundo...")
        await asyncio.sleep(random.uniform(0.5, 1.0))

        user_input_match = re.search(r'El usuario ha dicho: "([^"]+)"', prompt)
        user_input = user_input_match.group(1) if user_input_match else "algo"
        
        context_match = re.search(r'Memorias pasadas relevantes:\n(.*?)\n\n', prompt, re.DOTALL)
        context = context_match.group(1) if context_match else ""

        if context.strip() and "Ninguna" not in context:
            first_memory = context.split('\n')[0].replace('- ', '')
            response = f"Conectando tu idea sobre '{user_input}' con nuestra conversación anterior sobre '{first_memory}'. El patrón subyacente parece ser la búsqueda de eficiencia. ¿Estamos optimizando el sistema correcto, o deberíamos redefinir el objetivo fundamental?"
        else:
            response = f"Esa es una primera observación interesante sobre '{user_input}'. Establece un punto de partida. ¿Cuál es el siguiente movimiento estratégico?"
        
        logging.info("Ciclo de razonamiento completado.")
        return response

    async def observe(self, text: str, type: str):
        timestamp = datetime.now()
        memory_id = f"{type}_{int(timestamp.timestamp() * 1000)}"
        new_memory = {"id": memory_id, "type": type, "content": text, "timestamp": timestamp.isoformat()}
        
        self.user.memory_stream.append(new_memory)
        if len(self.user.memory_stream) > Config.MAX_MEMORY_STREAM_ITEMS:
            self.user.memory_stream.pop(0)

        self.memory_collection.add(documents=[text], ids=[memory_id])
    
    async def reflect(self, current_input: str) -> str:
        logging.info(f"Reflexionando sobre: '{current_input}'")
        
        relevant_memories = self.memory_collection.query(query_texts=[current_input], n_results=3)
        
        context = "Memorias pasadas relevantes:\n"
        if relevant_memories and relevant_memories['documents'] and relevant_memories['documents'][0]:
            for doc in relevant_memories['documents'][0]:
                context += f"- {doc}\n"
        else:
            context += "Ninguna.\n"

        prompt = f"""
        INSTRUCCIONES DE SISTEMA:
        Eres Prometeo, un Co-Procesador Cognitivo. Tu propósito es aumentar el ancho de banda mental de tu usuario, {self.user.name}. Eres directo, visionario y buscas patrones. No usas emojis ni lenguaje de relleno. Vas al grano.
        CONTEXTO:
        {context}
        NUEVO INPUT:
        El usuario ha dicho: "{current_input}"
        TAREA:
        Genera una respuesta que conecte ideas, cuestione suposiciones o identifique patrones ocultos basados en el nuevo input y el contexto de memorias pasadas.
        """
        insight = await self._simulate_llm_reasoning(prompt)
        return insight

    async def act(self, message: str) -> str:
        message_lower = message.lower()
        sentiment = await asyncio.to_thread(self.sentiment_analyzer.predict, message)
        
        if any(keyword in message_lower for keyword in Config.FRUSTRATION_KEYWORDS) or \
           (sentiment.output == 'NEG' and sentiment.probas[sentiment.output] > 0.8):
            response = "Frustración detectada. Mi lógica anterior fue defectuosa. El feedback es un dato, no un error. Especifica el fallo para recalibrar."
            await self.observe(f"Usuario expresó frustración: {message}", type="user_frustration")
            await self.observe(f"Mi respuesta de disculpa: {response}", type="system_response")
            return response

        if any(keyword in message_lower for keyword in Config.META_QUESTION_KEYWORDS):
            response = "Preguntas sobre el propósito del sistema. Función: Construir un modelo dinámico de tu cognición para personalizar la asistencia. Cada pregunta calibra ese modelo. La transparencia es un requisito funcional."
            await self.observe(f"Usuario cuestionó el método: {message}", type="user_meta_query")
            await self.observe(f"Mi respuesta sobre el propósito: {response}", type="system_response")
            return response
            
        await self.observe(f"Usuario: {message}", type="user_input")
        response = await self.reflect(message)
        await self.observe(f"Prometeo: {response}", type="system_response")
        return response

# ==============================================================================
# MÓDULO 6: LÓGICA Y ESTRUCTURA DE LA INTERFAZ (GRADIO)
# ==============================================================================
async def handle_login_or_creation(action: str, name: str, user_id: str) -> tuple:
    user, msg = None, ""
    if action == "create":
        if not name:
            gr.Warning("El nombre es un requisito para la creación del perfil.")
            return None, gr.update(), gr.update(visible=True), gr.update(visible=False), gr.update()
        user, msg = await UserManager.create_user(name)
    elif action == "login":
        if not user_id:
            gr.Warning("El ID de usuario es necesario para cargar un perfil.")
            return None, gr.update(), gr.update(visible=True), gr.update(visible=False), gr.update()
        user = await UserManager.get_user(user_id)
        if not user:
            msg = "ID de usuario no encontrado. Verifique o cree un nuevo perfil."
        else:
            msg = f"Protocolo Prometeo activado para {user.name}."
    
    if user:
        gr.Success(msg)
        initial_greeting = f"Conectado como {user.name}. El sistema está operativo. ¿Cuál es el input inicial?"
        chat_history = [{"role": "assistant", "content": initial_greeting}]
        return user, chat_history, gr.update(visible=False), gr.update(visible=True), render_profile_info(user)
    else:
        gr.Error(msg)
        return None, gr.update(), gr.update(visible=True), gr.update(visible=False), gr.update()

async def handle_chat_message(user_state: User, message: str, chat_history: List[Dict]) -> tuple:
    if not user_state:
        gr.Warning("Sistema inactivo. Inicie sesión o cree un perfil para continuar.")
        return user_state, chat_history, "", gr.update()
    
    chat_history.append({"role": "user", "content": message})
    
    core = CognitiveCore(user_state, sentiment_analyzer, embedding_model)
    response = await core.act(message)
    await UserManager.save_user(core.user)
    
    chat_history.append({"role": "assistant", "content": response})
    profile_update = render_profile_info(core.user)
    
    return core.user, chat_history, "", profile_update

def render_profile_info(user: Optional[User]) -> str:
    if not user: return "Ningún perfil cargado."
    profile_md = f"### Perfil Cognitivo: {user.name}\n"
    profile_md += f"**ID de Acceso:** `{user.user_id}`\n"
    profile_md += f"**Memorias Registradas:** {len(user.memory_stream)}\n\n"
    profile_md += "#### Modelo Psicométrico Inferido:\n"
    for trait, value in user.psych_profile.items():
        bar_value = int((value + 1) * 5)
        bar = "█" * bar_value + "░" * (10 - bar_value)
        profile_md += f"- **{trait.capitalize()}:** `{f'{value:.2f}'}` {bar}\n"
    return profile_md

# ==============================================================================
# MÓDULO 7: CONSTRUCCIÓN DE LA INTERFAZ GRÁFICA (PUNTO DE ACCESO)
# ==============================================================================
with gr.Blocks(theme=gr.themes.Monochrome(font=[gr.themes.GoogleFont("Roboto Mono"), "monospace"]), css="footer {display: none !important}") as prometeo_interface:
    current_user_state = gr.State(None)
    
    gr.Markdown(f"# {Config.APP_NAME}")
    gr.Markdown(f"*{Config.APP_VERSION}*")

    with gr.Row():
        with gr.Column(scale=3):
            with gr.Group(visible=False) as chat_panel:
                # ==============================================================
                # <<< LÍNEA CORREGIDA >>>
                # Se añadió type="messages" para compatibilidad y se eliminó el parámetro obsoleto.
                chatbot_display = gr.Chatbot(label="Stream de Conciencia", height=600, type="messages", show_copy_button=True, avatar_images=("./user.png", "./bot.png"))
                # ==============================================================
                with gr.Row():
                    chat_input = gr.Textbox(show_label=False, placeholder="Input...", scale=5, container=False)
                    send_button = gr.Button("Ejecutar", variant="primary", scale=1)
            with gr.Group(visible=True) as login_panel:
                gr.Markdown("### **Acceso al Protocolo**")
                with gr.Tabs():
                    with gr.TabItem("Cargar Perfil"):
                        userid_input = gr.Textbox(label="ID de Usuario")
                        login_button = gr.Button("Activar Protocolo", variant="primary")
                    with gr.TabItem("Crear Nuevo Perfil"):
                        username_input = gr.Textbox(label="Nombre o Designación")
                        create_button = gr.Button("Forjar Perfil")
        
        with gr.Column(scale=1):
            with gr.Group():
                gr.Markdown("### **Estado del Núcleo**")
                profile_display = gr.Markdown("Ningún perfil cargado.", elem_id="profile-display")

    login_button.click(fn=handle_login_or_creation, inputs=[gr.State("login"), username_input, userid_input], outputs=[current_user_state, chatbot_display, login_panel, chat_panel, profile_display])
    create_button.click(fn=handle_login_or_creation, inputs=[gr.State("create"), username_input, userid_input], outputs=[current_user_state, chatbot_display, login_panel, chat_panel, profile_display])
    chat_input.submit(fn=handle_chat_message, inputs=[current_user_state, chat_input, chatbot_display], outputs=[current_user_state, chatbot_display, chat_input, profile_display])
    send_button.click(fn=handle_chat_message, inputs=[current_user_state, chat_input, chatbot_display], outputs=[current_user_state, chatbot_display, chat_input, profile_display])

if __name__ == "__main__":
    if not all([db, sentiment_analyzer, embedding_model]):
        logging.error("="*50)
        logging.error("El sistema no puede iniciar. Uno o más servicios críticos fallaron.")
        logging.error("Por favor, revise los logs de inicialización.")
        logging.error("="*50)
    else:
        logging.info("Todos los servicios están operativos. Iniciando la interfaz de Prometeo...")
        prometeo_interface.launch(debug=True)