Lukeetah commited on
Commit
dc0914e
·
verified ·
1 Parent(s): ae64ad8

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +33 -194
app.py CHANGED
@@ -1,5 +1,6 @@
1
- # app.py - MateAI v18.3: Conciencia Aumentada
2
  # Arquitectura por un asistente de IA para un futuro colaborativo.
 
3
 
4
  import gradio as gr
5
  import random
@@ -30,14 +31,12 @@ from firebase_admin import credentials, firestore
30
  class Config:
31
  """Clase de configuración central para todos los parámetros de MateAI."""
32
  APP_NAME = "MateAI v18.3: Conciencia Aumentada"
33
- APP_VERSION = "18.3.0-alpha"
34
 
35
  # --- Configuración de Base de Datos ---
36
  FIREBASE_COLLECTION_USERS = "users_v18" # Nueva colección para la arquitectura avanzada.
37
 
38
  # --- Parámetros del Motor de Personalidad (Basado en el Modelo OCEAN) ---
39
- # Estos son los valores iniciales para un nuevo usuario.
40
- # El rango de cada rasgo es de -1.0 (bajo) a 1.0 (alto).
41
  DEFAULT_PSYCH_PROFILE = {
42
  "openness": 0.0, # Apertura a nuevas experiencias
43
  "conscientiousness": 0.0, # Organización y responsabilidad
@@ -65,16 +64,14 @@ class Config:
65
  # ==============================================================================
66
 
67
  # --- Configuración de Logging ---
68
- # Un buen logging es crucial para depurar un sistema complejo.
69
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
70
 
71
  # --- Inicialización del Analizador de Sentimiento ---
72
- # Cargamos el modelo una vez para no recargarlo en cada request.
73
  try:
74
  sentiment_analyzer = create_analyzer(task="sentiment", lang="es")
75
  logging.info("Analizador de sentimiento cargado exitosamente.")
76
  except Exception as e:
77
- sentiment_analyzer = None
78
  logging.error(f"No se pudo cargar el analizador de sentimiento: {e}")
79
 
80
  # --- Inicialización de Firebase Admin SDK ---
@@ -84,7 +81,6 @@ try:
84
  firebase_credentials_json = os.getenv('GOOGLE_APPLICATION_CREDENTIALS_JSON')
85
  if firebase_credentials_json:
86
  cred_dict = json.loads(firebase_credentials_json)
87
- # Aseguramos el project_id si no está en el JSON (común en algunos entornos)
88
  if 'project_id' not in cred_dict:
89
  cred_dict['project_id'] = 'mateai-815ca' # Reemplazar con tu project_id real si es diferente
90
  cred = credentials.Certificate(cred_dict)
@@ -102,10 +98,6 @@ except Exception as e:
102
  # ==============================================================================
103
  # MÓDULO 3: MODELOS DE DATOS Y CLASES CENTRALES
104
  # ==============================================================================
105
- # Definen la estructura de los datos con los que operamos. La clase User
106
- # es el corazón del sistema, representando el estado completo de un individuo.
107
- # ==============================================================================
108
-
109
  class User:
110
  """Representa el estado completo de un usuario, incluyendo su personalidad y memoria."""
111
  def __init__(self, user_id: str, name: str, **kwargs: Any):
@@ -113,96 +105,61 @@ class User:
113
  self.name: str = name
114
  self.created_at: datetime = kwargs.get('created_at', datetime.now())
115
  self.last_login: datetime = kwargs.get('last_login', datetime.now())
116
-
117
- # El perfil psicológico, la joya de la corona.
118
  self.psych_profile: Dict[str, float] = kwargs.get('psych_profile', Config.DEFAULT_PSYCH_PROFILE.copy())
119
-
120
- # Memoria a corto y largo plazo.
121
  self.memory_stream: List[Dict[str, Any]] = kwargs.get('memory_stream', [])
122
- self.short_term_context: Dict[str, Any] = {} # No se persiste, es para la sesión actual.
123
-
124
- # Sistema de metas, reemplaza a las simples "tareas".
125
  self.goals: List[Dict[str, Any]] = kwargs.get('goals', [])
126
-
127
- # Gamificación con propósito.
128
  self.connection_points: int = kwargs.get('connection_points', 0)
129
  self.achievements: List[str] = kwargs.get('achievements', [])
130
-
131
- # Metadata para la proactividad.
132
  self.last_proactive_checkin: Optional[datetime] = kwargs.get('last_proactive_checkin')
133
 
134
  def to_dict(self) -> Dict[str, Any]:
135
  """Serializa el objeto User a un diccionario para guardarlo en Firestore."""
136
  return {
137
- "user_id": self.user_id,
138
- "name": self.name,
139
- "created_at": self.created_at.isoformat(),
140
- "last_login": self.last_login.isoformat(),
141
- "psych_profile": self.psych_profile,
142
- "memory_stream": self.memory_stream,
143
- "goals": self.goals,
144
- "connection_points": self.connection_points,
145
- "achievements": self.achievements,
146
  "last_proactive_checkin": self.last_proactive_checkin.isoformat() if self.last_proactive_checkin else None
147
  }
148
 
149
  @classmethod
150
  def from_dict(cls, data: Dict[str, Any]) -> 'User':
151
  """Crea una instancia de User a partir de un diccionario de Firestore."""
152
- # Conversión de fechas de ISO string a datetime
153
  data['created_at'] = datetime.fromisoformat(data.get('created_at', datetime.now().isoformat()))
154
  data['last_login'] = datetime.fromisoformat(data.get('last_login', datetime.now().isoformat()))
155
  last_checkin_str = data.get('last_proactive_checkin')
156
  data['last_proactive_checkin'] = datetime.fromisoformat(last_checkin_str) if last_checkin_str else None
157
-
158
- # Aseguramos que el perfil psicológico tenga todas las claves
159
  profile = Config.DEFAULT_PSYCH_PROFILE.copy()
160
  profile.update(data.get('psych_profile', {}))
161
  data['psych_profile'] = profile
162
-
163
  return cls(**data)
164
 
165
  def add_memory(self, content: str, memory_type: str, sentiment: Dict[str, float], tags: List[str] = []):
166
  """Añade un nuevo recuerdo al flujo de memoria del usuario."""
167
  if len(self.memory_stream) >= Config.MAX_MEMORY_STREAM_ITEMS:
168
- self.memory_stream.pop(0) # Mantiene el tamaño del flujo de memoria
169
-
170
- memory = {
171
- "timestamp": datetime.now().isoformat(),
172
- "content": content,
173
- "type": memory_type, # 'chat', 'insight', 'goal_set', 'goal_completed'
174
- "sentiment": sentiment,
175
- "tags": tags
176
- }
177
  self.memory_stream.append(memory)
178
  logging.info(f"Nuevo recuerdo añadido para {self.name}: {content[:50]}...")
179
 
180
  # ==============================================================================
181
  # MÓDULO 4: GESTOR DE DATOS DE USUARIO (CAPA DE PERSISTENCIA)
182
  # ==============================================================================
183
- # Abstrae toda la lógica de comunicación con Firestore. Permite cambiar
184
- # de base de datos en el futuro sin alterar el resto del código.
185
- # ==============================================================================
186
-
187
  class UserManager:
188
  """Maneja la carga, creación y actualización de perfiles de usuario en Firestore."""
189
-
190
  @staticmethod
191
  async def get_user(user_id: str) -> Optional[User]:
192
- """Carga un usuario desde Firestore por su ID."""
193
  if not db or not user_id: return None
194
  try:
195
  user_doc_ref = db.collection(Config.FIREBASE_COLLECTION_USERS).document(user_id)
196
  doc = await asyncio.to_thread(user_doc_ref.get)
197
  if doc.exists:
198
  user_data = doc.to_dict()
199
- user_data['user_id'] = doc.id # Aseguramos que el ID esté en los datos.
200
-
201
- # Actualizar la fecha de último login y guardar inmediatamente.
202
  user_obj = User.from_dict(user_data)
203
  user_obj.last_login = datetime.now()
204
- await UserManager.save_user(user_obj) # Guardado asíncrono
205
-
206
  logging.info(f"Usuario '{user_obj.name}' ({user_id}) cargado y actualizado.")
207
  return user_obj
208
  else:
@@ -214,15 +171,11 @@ class UserManager:
214
 
215
  @staticmethod
216
  async def create_user(name: str) -> Tuple[Optional[User], str]:
217
- """Crea un nuevo usuario en Firestore."""
218
  if not db: return None, "Error: La base de datos no está disponible."
219
  if not name.strip(): return None, "El nombre no puede estar vacío."
220
-
221
  try:
222
- # Generamos un ID de usuario único y legible.
223
  user_id = f"{name.lower().replace(' ', '_')}_{int(time.time())}"
224
  new_user = User(user_id=user_id, name=name)
225
-
226
  await UserManager.save_user(new_user)
227
  msg = f"¡Bienvenido, {name}! Tu perfil ha sido creado. Guarda bien tu ID de Usuario: **{user_id}**"
228
  logging.info(f"Nuevo usuario creado: {name} ({user_id})")
@@ -233,7 +186,6 @@ class UserManager:
233
 
234
  @staticmethod
235
  async def save_user(user: User) -> bool:
236
- """Guarda el estado completo de un objeto User en Firestore."""
237
  if not db: return False
238
  try:
239
  user_doc_ref = db.collection(Config.FIREBASE_COLLECTION_USERS).document(user.user_id)
@@ -246,25 +198,14 @@ class UserManager:
246
  # ==============================================================================
247
  # MÓDULO 5: MOTOR DE PERSONA Y LÓGICA DE IA
248
  # ==============================================================================
249
- # Este es el cerebro de MateAI. Aquí se toma el perfil del usuario,
250
- # el contexto actual y el mensaje para generar una respuesta coherente,
251
- # empática y personalizada.
252
- # ==============================================================================
253
-
254
  class PersonaEngine:
255
  """Orquesta la lógica de IA para generar respuestas y gestionar la interacción."""
256
-
257
  def __init__(self, user: User):
258
  self.user = user
259
 
260
  async def analyze_sentiment(self, text: str) -> Dict[str, float]:
261
- """Analiza el sentimiento del texto del usuario."""
262
- if not sentiment_analyzer:
263
- # Fallback si el modelo no cargó.
264
- return {"label": "NEU", "score": 1.0}
265
-
266
  try:
267
- # Ejecutamos el análisis en un hilo separado para no bloquear la app.
268
  analysis = await asyncio.to_thread(sentiment_analyzer.predict, text)
269
  return {"label": analysis.output, "score": analysis.probas[analysis.output]}
270
  except Exception as e:
@@ -272,13 +213,10 @@ class PersonaEngine:
272
  return {"label": "NEU", "score": 1.0}
273
 
274
  def _get_greeting(self) -> str:
275
- """Genera un saludo personalizado basado en la hora y el perfil."""
276
  hour = datetime.now().hour
277
  if 5 <= hour < 12: time_greeting = "Buen día"
278
  elif 12 <= hour < 19: time_greeting = "Buenas tardes"
279
  else: time_greeting = "Buenas noches"
280
-
281
- # Personalización del saludo
282
  if self.user.psych_profile['extraversion'] > 0.5:
283
  return f"¡{time_greeting}, {self.user.name}! ¡Qué bueno verte! ¿En qué andamos hoy?"
284
  elif self.user.psych_profile['neuroticism'] > 0.4:
@@ -287,73 +225,41 @@ class PersonaEngine:
287
  return f"{time_greeting}, {self.user.name}. Un gusto conectar de nuevo."
288
 
289
  async def generate_proactive_checkin(self) -> Optional[str]:
290
- """Genera un mensaje proactivo si ha pasado suficiente tiempo."""
291
  now = datetime.now()
292
- if self.user.last_proactive_checkin:
293
- if now - self.user.last_proactive_checkin < timedelta(hours=Config.PROACTIVE_CHECKIN_HOURS):
294
- return None # Aún no es tiempo.
295
-
296
- # Lógica de check-in
297
  self.user.last_proactive_checkin = now
298
  await UserManager.save_user(self.user)
299
-
300
- # Ejemplo de check-in personalizado
301
  if self.user.psych_profile['conscientiousness'] > 0.5 and self.user.goals:
302
  pending_goals = [g['name'] for g in self.user.goals if not g.get('completed')]
303
  if pending_goals:
304
  return f"¡Hola {self.user.name}! Solo pasaba a saludar y recordarte que tenés metas increíbles como '{pending_goals[0]}' en marcha. ¡Cualquier pasito cuenta!"
305
-
306
  return self._get_greeting() + " Solo pasaba a ver cómo estabas."
307
 
308
  async def generate_response(self, message: str) -> str:
309
- """El método principal que genera la respuesta de MateAI a un mensaje."""
310
- # 1. Analizar el sentimiento del mensaje del usuario
311
  sentiment = await self.analyze_sentiment(message)
312
-
313
- # 2. Registrar el mensaje en la memoria del usuario
314
  self.user.add_memory(content=message, memory_type="chat", sentiment=sentiment)
315
-
316
- # 3. Lógica de respuesta basada en triggers y perfil
317
- # TODO: Implementar un sistema de comandos más robusto (ej. /metas, /perfil)
318
-
319
- # 3.1 Manejo de respuestas empáticas a sentimientos fuertes
320
  if sentiment['label'] == 'NEG' and sentiment['score'] > 0.7:
321
  return f"Noto que lo que decís tiene una carga fuerte, {self.user.name}. Si querés hablar de ello, acá estoy para escucharte sin juzgar."
322
-
323
- # 3.2 Lógica de preguntas para construir el perfil (si aún es genérico)
324
- # Esta es la parte de "tricky psychological questions"
325
- if abs(sum(self.user.psych_profile.values())) < 0.1: # Perfil casi virgen
326
  return await self._ask_profiling_question()
327
-
328
- # 3.3 Respuesta por defecto (placeholder para lógica más compleja)
329
- # Aquí se integraría con un LLM si lo tuviéramos.
330
- # Por ahora, una respuesta reflexiva basada en el perfil.
331
-
332
  response = self._craft_default_response(sentiment)
333
-
334
- # 4. Guardar el estado actualizado del usuario
335
  await UserManager.save_user(self.user)
336
-
337
  return response
338
 
339
  def _craft_default_response(self, sentiment: Dict[str, float]) -> str:
340
- """Crea una respuesta genérica pero personalizada."""
341
  responses = [
342
  f"Interesante lo que mencionás, {self.user.name}. Me hace pensar en...",
343
  f"Entiendo tu punto, {self.user.name}. ¿Cómo se conecta eso con tus metas actuales?",
344
- f"Gracias por compartir eso. Cada charla nos ayuda a entendernos mejor.",
345
  ]
346
-
347
  if self.user.psych_profile['openness'] > 0.5:
348
- responses.append(f"Eso abre una puerta a una idea nueva. ¿Qué pasaría si lo miramos desde otro ángulo?")
349
  if sentiment['label'] == 'POS':
350
  responses.append(f"¡Me encanta esa energía, {self.user.name}! Es genial verte así.")
351
-
352
  return random.choice(responses)
353
 
354
  async def _ask_profiling_question(self) -> str:
355
- """Selecciona y hace una pregunta sutil para definir el perfil psicológico."""
356
- # Ejemplo de pregunta para medir "Apertura a la experiencia" vs "Consciencia"
357
  question = (
358
  f"Una pregunta curiosa, {self.user.name}: si tuvieras una tarde libre inesperada, "
359
  "¿qué te tienta más? \n"
@@ -365,10 +271,8 @@ class PersonaEngine:
365
  return question
366
 
367
  def process_profiling_answer(self, answer: str):
368
- """Procesa la respuesta a una pregunta de perfilado y actualiza el psych_profile."""
369
  question_type = self.user.short_term_context.get('last_question')
370
  if not question_type: return
371
-
372
  answer_lower = answer.lower()
373
  if question_type == "openness_vs_conscientiousness":
374
  if 'a' in answer_lower or 'improvisar' in answer_lower:
@@ -377,27 +281,14 @@ class PersonaEngine:
377
  elif 'b' in answer_lower or 'organizar' in answer_lower:
378
  self.user.psych_profile['conscientiousness'] += 0.3
379
  self.user.psych_profile['openness'] -= 0.1
380
-
381
- # Limpiamos el contexto para no procesar de nuevo.
382
  del self.user.short_term_context['last_question']
383
-
384
- # Añadimos puntos por la introspección
385
  self.user.connection_points += Config.POINTS_PER_INSIGHT
386
  logging.info(f"Perfil de {self.user.name} actualizado. Puntos: {self.user.connection_points}")
387
 
388
-
389
  # ==============================================================================
390
  # MÓDULO 6: LÓGICA Y ESTRUCTURA DE LA INTERFAZ DE USUARIO (GRADIO)
391
  # ==============================================================================
392
- # Aquí se conectan todos los módulos anteriores con la interfaz gráfica.
393
- # Las funciones aquí son "controladores" que reciben eventos de la UI
394
- # y orquestan las acciones de los motores de backend.
395
- # ==============================================================================
396
-
397
- # --- Funciones de Lógica de la Interfaz ---
398
-
399
  async def handle_login_or_creation(action: str, name: str, user_id: str) -> tuple:
400
- """Controlador para los botones de login y creación."""
401
  if action == "create":
402
  if not name:
403
  gr.Warning("Para crear un perfil, necesito que me digas tu nombre.")
@@ -411,10 +302,8 @@ async def handle_login_or_creation(action: str, name: str, user_id: str) -> tupl
411
  msg = f"¡Hola de nuevo, {user.name}! Perfil cargado." if user else "ID de usuario no encontrado. Verificá que esté bien escrito."
412
  else:
413
  return None, "Acción desconocida.", gr.update(visible=True), gr.update(visible=False)
414
-
415
  if user:
416
  gr.Success(msg)
417
- # Al loguearse, ocultamos el panel de login y mostramos el de chat.
418
  initial_greeting = PersonaEngine(user)._get_greeting()
419
  chat_history = [{"role": "assistant", "content": initial_greeting}]
420
  return user, chat_history, gr.update(visible=False), gr.update(visible=True)
@@ -423,70 +312,47 @@ async def handle_login_or_creation(action: str, name: str, user_id: str) -> tupl
423
  return None, gr.update(), gr.update(visible=True), gr.update(visible=False)
424
 
425
  async def handle_chat_message(user_state: User, message: str, chat_history: List[Dict]) -> tuple:
426
- """Controlador principal para cada mensaje enviado por el usuario."""
427
  if not user_state:
428
  gr.Warning("¡Para empezar, creá un perfil o iniciá sesión!")
429
  return user_state, chat_history, ""
430
-
431
- # Agregamos el mensaje del usuario a la UI inmediatamente para dar feedback visual.
432
  chat_history.append({"role": "user", "content": message})
433
-
434
- # Creamos una instancia del motor de persona con el estado actual del usuario.
435
  engine = PersonaEngine(user_state)
436
-
437
- # Verificamos si la respuesta es a una pregunta de perfilado.
438
  if user_state.short_term_context.get('last_question'):
439
  engine.process_profiling_answer(message)
440
  response = "¡Bárbaro! Gracias por compartirlo. Lo tengo en cuenta para que nos entendamos mejor."
441
  else:
442
- # Si no, generamos una respuesta normal.
443
  response = await engine.generate_response(message)
444
-
445
- # Agregamos la respuesta de MateAI al historial.
446
  chat_history.append({"role": "assistant", "content": response})
447
-
448
- # El estado del usuario (user_state) ha sido modificado por el engine,
449
- # así que lo devolvemos para actualizar el gr.State
450
- return user_state, chat_history, "" # Limpiamos el textbox de input
451
 
452
  def render_profile_info(user: Optional[User]) -> str:
453
- """Genera un texto Markdown con la información del perfil del usuario."""
454
- if not user:
455
- return "Cargá un perfil para ver tu información."
456
-
457
  profile_md = f"### Perfil de {user.name}\n"
458
  profile_md += f"**ID de Usuario:** `{user.user_id}`\n"
459
  profile_md += f"**Puntos de Conexión:** {user.connection_points} 💠\n\n"
460
  profile_md += "#### Modelo de Personalidad (inferido):\n"
461
  for trait, value in user.psych_profile.items():
462
- # Visualización simple del rasgo
463
- bar = "■" * int((value + 1) * 5) # Escala de 0 a 10
464
  profile_md += f"- **{trait.capitalize()}:** `{f'{value:.2f}'}` {bar}\n"
465
-
466
  return profile_md
467
 
468
  # --- Construcción de la Interfaz con Gradio Blocks ---
469
-
470
  with gr.Blocks(theme=gr.themes.Soft(primary_hue="teal", secondary_hue="amber"), css="footer {display: none !important}") as demo:
471
-
472
- # Estado de la aplicación: el objeto User se mantiene aquí durante toda la sesión.
473
  current_user = gr.State(None)
474
-
475
  gr.Markdown(f"# 🧉 {Config.APP_NAME}")
476
  gr.Markdown(f"*{Config.APP_VERSION}* - Tu compañero de IA para la introspección y el crecimiento.")
477
 
478
  with gr.Row():
479
- # Columna de la izquierda: Chat y Perfil
480
  with gr.Column(scale=2):
481
- # Panel de Chat (inicialmente oculto)
482
- with gr.Box(visible=False) as chat_panel:
483
  chatbot = gr.Chatbot(label="Conversación con MateAI", height=600, type="messages")
484
  with gr.Row():
485
  chat_input = gr.Textbox(show_label=False, placeholder="Escribí acá con confianza...", scale=4)
486
  send_button = gr.Button("Enviar", variant="primary", scale=1)
487
 
488
- # Panel de Login (inicialmente visible)
489
- with gr.Box(visible=True) as login_panel:
490
  gr.Markdown("### 🌟 Para empezar...")
491
  with gr.Tabs():
492
  with gr.TabItem("Crear Perfil Nuevo"):
@@ -496,47 +362,20 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="teal", secondary_hue="amber"),
496
  userid_input = gr.Textbox(label="Tu ID de Usuario")
497
  login_button = gr.Button("Cargar Perfil")
498
 
499
- # Columna de la derecha: Información de contexto
500
  with gr.Column(scale=1):
501
- with gr.Box():
 
502
  gr.Markdown("### 🧠 Tu Perfil")
503
  profile_display = gr.Markdown("Cargá un perfil para ver tu información.")
504
 
505
  # --- Lógica de Eventos de la Interfaz ---
506
-
507
- # Acciones de Login y Creación
508
- login_button.click(
509
- fn=handle_login_or_creation,
510
- inputs=[gr.State("login"), username_input, userid_input],
511
- outputs=[current_user, chatbot, login_panel, chat_panel]
512
- )
513
- create_button.click(
514
- fn=handle_login_or_creation,
515
- inputs=[gr.State("create"), username_input, userid_input],
516
- outputs=[current_user, chatbot, login_panel, chat_panel]
517
- )
518
 
519
- # Acción de enviar mensaje en el chat
520
- chat_submit_action = send_button.click if send_button else chat_input.submit
521
- chat_submit_action(
522
- fn=handle_chat_message,
523
- inputs=[current_user, chat_input, chatbot],
524
- outputs=[current_user, chatbot, chat_input]
525
- )
526
- chat_input.submit(
527
- fn=handle_chat_message,
528
- inputs=[current_user, chat_input, chatbot],
529
- outputs=[current_user, chatbot, chat_input]
530
- )
531
 
532
- # Actualización dinámica del panel de perfil cuando cambia el estado del usuario
533
- current_user.change(
534
- fn=render_profile_info,
535
- inputs=[current_user],
536
- outputs=[profile_display]
537
- )
538
 
539
  if __name__ == "__main__":
540
- # Para lanzar la aplicación
541
- # En un entorno de producción como Hugging Face Spaces, no es necesario debug=True
542
  demo.launch(debug=True)
 
1
+ # app.py - MateAI v18.3.1: Conciencia Aumentada (Hotfix de Compatibilidad)
2
  # Arquitectura por un asistente de IA para un futuro colaborativo.
3
+ # Cambio: Se reemplaza gr.Box por gr.Group para compatibilidad con versiones antiguas de Gradio.
4
 
5
  import gradio as gr
6
  import random
 
31
  class Config:
32
  """Clase de configuración central para todos los parámetros de MateAI."""
33
  APP_NAME = "MateAI v18.3: Conciencia Aumentada"
34
+ APP_VERSION = "18.3.1-hotfix"
35
 
36
  # --- Configuración de Base de Datos ---
37
  FIREBASE_COLLECTION_USERS = "users_v18" # Nueva colección para la arquitectura avanzada.
38
 
39
  # --- Parámetros del Motor de Personalidad (Basado en el Modelo OCEAN) ---
 
 
40
  DEFAULT_PSYCH_PROFILE = {
41
  "openness": 0.0, # Apertura a nuevas experiencias
42
  "conscientiousness": 0.0, # Organización y responsabilidad
 
64
  # ==============================================================================
65
 
66
  # --- Configuración de Logging ---
 
67
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
68
 
69
  # --- Inicialización del Analizador de Sentimiento ---
70
+ sentiment_analyzer = None
71
  try:
72
  sentiment_analyzer = create_analyzer(task="sentiment", lang="es")
73
  logging.info("Analizador de sentimiento cargado exitosamente.")
74
  except Exception as e:
 
75
  logging.error(f"No se pudo cargar el analizador de sentimiento: {e}")
76
 
77
  # --- Inicialización de Firebase Admin SDK ---
 
81
  firebase_credentials_json = os.getenv('GOOGLE_APPLICATION_CREDENTIALS_JSON')
82
  if firebase_credentials_json:
83
  cred_dict = json.loads(firebase_credentials_json)
 
84
  if 'project_id' not in cred_dict:
85
  cred_dict['project_id'] = 'mateai-815ca' # Reemplazar con tu project_id real si es diferente
86
  cred = credentials.Certificate(cred_dict)
 
98
  # ==============================================================================
99
  # MÓDULO 3: MODELOS DE DATOS Y CLASES CENTRALES
100
  # ==============================================================================
 
 
 
 
101
  class User:
102
  """Representa el estado completo de un usuario, incluyendo su personalidad y memoria."""
103
  def __init__(self, user_id: str, name: str, **kwargs: Any):
 
105
  self.name: str = name
106
  self.created_at: datetime = kwargs.get('created_at', datetime.now())
107
  self.last_login: datetime = kwargs.get('last_login', datetime.now())
 
 
108
  self.psych_profile: Dict[str, float] = kwargs.get('psych_profile', Config.DEFAULT_PSYCH_PROFILE.copy())
 
 
109
  self.memory_stream: List[Dict[str, Any]] = kwargs.get('memory_stream', [])
110
+ self.short_term_context: Dict[str, Any] = {}
 
 
111
  self.goals: List[Dict[str, Any]] = kwargs.get('goals', [])
 
 
112
  self.connection_points: int = kwargs.get('connection_points', 0)
113
  self.achievements: List[str] = kwargs.get('achievements', [])
 
 
114
  self.last_proactive_checkin: Optional[datetime] = kwargs.get('last_proactive_checkin')
115
 
116
  def to_dict(self) -> Dict[str, Any]:
117
  """Serializa el objeto User a un diccionario para guardarlo en Firestore."""
118
  return {
119
+ "user_id": self.user_id, "name": self.name, "created_at": self.created_at.isoformat(),
120
+ "last_login": self.last_login.isoformat(), "psych_profile": self.psych_profile,
121
+ "memory_stream": self.memory_stream, "goals": self.goals,
122
+ "connection_points": self.connection_points, "achievements": self.achievements,
 
 
 
 
 
123
  "last_proactive_checkin": self.last_proactive_checkin.isoformat() if self.last_proactive_checkin else None
124
  }
125
 
126
  @classmethod
127
  def from_dict(cls, data: Dict[str, Any]) -> 'User':
128
  """Crea una instancia de User a partir de un diccionario de Firestore."""
 
129
  data['created_at'] = datetime.fromisoformat(data.get('created_at', datetime.now().isoformat()))
130
  data['last_login'] = datetime.fromisoformat(data.get('last_login', datetime.now().isoformat()))
131
  last_checkin_str = data.get('last_proactive_checkin')
132
  data['last_proactive_checkin'] = datetime.fromisoformat(last_checkin_str) if last_checkin_str else None
 
 
133
  profile = Config.DEFAULT_PSYCH_PROFILE.copy()
134
  profile.update(data.get('psych_profile', {}))
135
  data['psych_profile'] = profile
 
136
  return cls(**data)
137
 
138
  def add_memory(self, content: str, memory_type: str, sentiment: Dict[str, float], tags: List[str] = []):
139
  """Añade un nuevo recuerdo al flujo de memoria del usuario."""
140
  if len(self.memory_stream) >= Config.MAX_MEMORY_STREAM_ITEMS:
141
+ self.memory_stream.pop(0)
142
+ memory = {"timestamp": datetime.now().isoformat(), "content": content, "type": memory_type, "sentiment": sentiment, "tags": tags}
 
 
 
 
 
 
 
143
  self.memory_stream.append(memory)
144
  logging.info(f"Nuevo recuerdo añadido para {self.name}: {content[:50]}...")
145
 
146
  # ==============================================================================
147
  # MÓDULO 4: GESTOR DE DATOS DE USUARIO (CAPA DE PERSISTENCIA)
148
  # ==============================================================================
 
 
 
 
149
  class UserManager:
150
  """Maneja la carga, creación y actualización de perfiles de usuario en Firestore."""
 
151
  @staticmethod
152
  async def get_user(user_id: str) -> Optional[User]:
 
153
  if not db or not user_id: return None
154
  try:
155
  user_doc_ref = db.collection(Config.FIREBASE_COLLECTION_USERS).document(user_id)
156
  doc = await asyncio.to_thread(user_doc_ref.get)
157
  if doc.exists:
158
  user_data = doc.to_dict()
159
+ user_data['user_id'] = doc.id
 
 
160
  user_obj = User.from_dict(user_data)
161
  user_obj.last_login = datetime.now()
162
+ await UserManager.save_user(user_obj)
 
163
  logging.info(f"Usuario '{user_obj.name}' ({user_id}) cargado y actualizado.")
164
  return user_obj
165
  else:
 
171
 
172
  @staticmethod
173
  async def create_user(name: str) -> Tuple[Optional[User], str]:
 
174
  if not db: return None, "Error: La base de datos no está disponible."
175
  if not name.strip(): return None, "El nombre no puede estar vacío."
 
176
  try:
 
177
  user_id = f"{name.lower().replace(' ', '_')}_{int(time.time())}"
178
  new_user = User(user_id=user_id, name=name)
 
179
  await UserManager.save_user(new_user)
180
  msg = f"¡Bienvenido, {name}! Tu perfil ha sido creado. Guarda bien tu ID de Usuario: **{user_id}**"
181
  logging.info(f"Nuevo usuario creado: {name} ({user_id})")
 
186
 
187
  @staticmethod
188
  async def save_user(user: User) -> bool:
 
189
  if not db: return False
190
  try:
191
  user_doc_ref = db.collection(Config.FIREBASE_COLLECTION_USERS).document(user.user_id)
 
198
  # ==============================================================================
199
  # MÓDULO 5: MOTOR DE PERSONA Y LÓGICA DE IA
200
  # ==============================================================================
 
 
 
 
 
201
  class PersonaEngine:
202
  """Orquesta la lógica de IA para generar respuestas y gestionar la interacción."""
 
203
  def __init__(self, user: User):
204
  self.user = user
205
 
206
  async def analyze_sentiment(self, text: str) -> Dict[str, float]:
207
+ if not sentiment_analyzer: return {"label": "NEU", "score": 1.0}
 
 
 
 
208
  try:
 
209
  analysis = await asyncio.to_thread(sentiment_analyzer.predict, text)
210
  return {"label": analysis.output, "score": analysis.probas[analysis.output]}
211
  except Exception as e:
 
213
  return {"label": "NEU", "score": 1.0}
214
 
215
  def _get_greeting(self) -> str:
 
216
  hour = datetime.now().hour
217
  if 5 <= hour < 12: time_greeting = "Buen día"
218
  elif 12 <= hour < 19: time_greeting = "Buenas tardes"
219
  else: time_greeting = "Buenas noches"
 
 
220
  if self.user.psych_profile['extraversion'] > 0.5:
221
  return f"¡{time_greeting}, {self.user.name}! ¡Qué bueno verte! ¿En qué andamos hoy?"
222
  elif self.user.psych_profile['neuroticism'] > 0.4:
 
225
  return f"{time_greeting}, {self.user.name}. Un gusto conectar de nuevo."
226
 
227
  async def generate_proactive_checkin(self) -> Optional[str]:
 
228
  now = datetime.now()
229
+ if self.user.last_proactive_checkin and (now - self.user.last_proactive_checkin < timedelta(hours=Config.PROACTIVE_CHECKIN_HOURS)):
230
+ return None
 
 
 
231
  self.user.last_proactive_checkin = now
232
  await UserManager.save_user(self.user)
 
 
233
  if self.user.psych_profile['conscientiousness'] > 0.5 and self.user.goals:
234
  pending_goals = [g['name'] for g in self.user.goals if not g.get('completed')]
235
  if pending_goals:
236
  return f"¡Hola {self.user.name}! Solo pasaba a saludar y recordarte que tenés metas increíbles como '{pending_goals[0]}' en marcha. ¡Cualquier pasito cuenta!"
 
237
  return self._get_greeting() + " Solo pasaba a ver cómo estabas."
238
 
239
  async def generate_response(self, message: str) -> str:
 
 
240
  sentiment = await self.analyze_sentiment(message)
 
 
241
  self.user.add_memory(content=message, memory_type="chat", sentiment=sentiment)
 
 
 
 
 
242
  if sentiment['label'] == 'NEG' and sentiment['score'] > 0.7:
243
  return f"Noto que lo que decís tiene una carga fuerte, {self.user.name}. Si querés hablar de ello, acá estoy para escucharte sin juzgar."
244
+ if abs(sum(self.user.psych_profile.values())) < 0.1:
 
 
 
245
  return await self._ask_profiling_question()
 
 
 
 
 
246
  response = self._craft_default_response(sentiment)
 
 
247
  await UserManager.save_user(self.user)
 
248
  return response
249
 
250
  def _craft_default_response(self, sentiment: Dict[str, float]) -> str:
 
251
  responses = [
252
  f"Interesante lo que mencionás, {self.user.name}. Me hace pensar en...",
253
  f"Entiendo tu punto, {self.user.name}. ¿Cómo se conecta eso con tus metas actuales?",
254
+ "Gracias por compartir eso. Cada charla nos ayuda a entendernos mejor.",
255
  ]
 
256
  if self.user.psych_profile['openness'] > 0.5:
257
+ responses.append("Eso abre una puerta a una idea nueva. ¿Qué pasaría si lo miramos desde otro ángulo?")
258
  if sentiment['label'] == 'POS':
259
  responses.append(f"¡Me encanta esa energía, {self.user.name}! Es genial verte así.")
 
260
  return random.choice(responses)
261
 
262
  async def _ask_profiling_question(self) -> str:
 
 
263
  question = (
264
  f"Una pregunta curiosa, {self.user.name}: si tuvieras una tarde libre inesperada, "
265
  "¿qué te tienta más? \n"
 
271
  return question
272
 
273
  def process_profiling_answer(self, answer: str):
 
274
  question_type = self.user.short_term_context.get('last_question')
275
  if not question_type: return
 
276
  answer_lower = answer.lower()
277
  if question_type == "openness_vs_conscientiousness":
278
  if 'a' in answer_lower or 'improvisar' in answer_lower:
 
281
  elif 'b' in answer_lower or 'organizar' in answer_lower:
282
  self.user.psych_profile['conscientiousness'] += 0.3
283
  self.user.psych_profile['openness'] -= 0.1
 
 
284
  del self.user.short_term_context['last_question']
 
 
285
  self.user.connection_points += Config.POINTS_PER_INSIGHT
286
  logging.info(f"Perfil de {self.user.name} actualizado. Puntos: {self.user.connection_points}")
287
 
 
288
  # ==============================================================================
289
  # MÓDULO 6: LÓGICA Y ESTRUCTURA DE LA INTERFAZ DE USUARIO (GRADIO)
290
  # ==============================================================================
 
 
 
 
 
 
 
291
  async def handle_login_or_creation(action: str, name: str, user_id: str) -> tuple:
 
292
  if action == "create":
293
  if not name:
294
  gr.Warning("Para crear un perfil, necesito que me digas tu nombre.")
 
302
  msg = f"¡Hola de nuevo, {user.name}! Perfil cargado." if user else "ID de usuario no encontrado. Verificá que esté bien escrito."
303
  else:
304
  return None, "Acción desconocida.", gr.update(visible=True), gr.update(visible=False)
 
305
  if user:
306
  gr.Success(msg)
 
307
  initial_greeting = PersonaEngine(user)._get_greeting()
308
  chat_history = [{"role": "assistant", "content": initial_greeting}]
309
  return user, chat_history, gr.update(visible=False), gr.update(visible=True)
 
312
  return None, gr.update(), gr.update(visible=True), gr.update(visible=False)
313
 
314
  async def handle_chat_message(user_state: User, message: str, chat_history: List[Dict]) -> tuple:
 
315
  if not user_state:
316
  gr.Warning("¡Para empezar, creá un perfil o iniciá sesión!")
317
  return user_state, chat_history, ""
 
 
318
  chat_history.append({"role": "user", "content": message})
 
 
319
  engine = PersonaEngine(user_state)
 
 
320
  if user_state.short_term_context.get('last_question'):
321
  engine.process_profiling_answer(message)
322
  response = "¡Bárbaro! Gracias por compartirlo. Lo tengo en cuenta para que nos entendamos mejor."
323
  else:
 
324
  response = await engine.generate_response(message)
 
 
325
  chat_history.append({"role": "assistant", "content": response})
326
+ return user_state, chat_history, ""
 
 
 
327
 
328
  def render_profile_info(user: Optional[User]) -> str:
329
+ if not user: return "Cargá un perfil para ver tu información."
 
 
 
330
  profile_md = f"### Perfil de {user.name}\n"
331
  profile_md += f"**ID de Usuario:** `{user.user_id}`\n"
332
  profile_md += f"**Puntos de Conexión:** {user.connection_points} 💠\n\n"
333
  profile_md += "#### Modelo de Personalidad (inferido):\n"
334
  for trait, value in user.psych_profile.items():
335
+ bar = "■" * int((value + 1) * 5)
 
336
  profile_md += f"- **{trait.capitalize()}:** `{f'{value:.2f}'}` {bar}\n"
 
337
  return profile_md
338
 
339
  # --- Construcción de la Interfaz con Gradio Blocks ---
 
340
  with gr.Blocks(theme=gr.themes.Soft(primary_hue="teal", secondary_hue="amber"), css="footer {display: none !important}") as demo:
 
 
341
  current_user = gr.State(None)
 
342
  gr.Markdown(f"# 🧉 {Config.APP_NAME}")
343
  gr.Markdown(f"*{Config.APP_VERSION}* - Tu compañero de IA para la introspección y el crecimiento.")
344
 
345
  with gr.Row():
 
346
  with gr.Column(scale=2):
347
+ # Panel de Chat - Usando gr.Group en lugar de gr.Box para compatibilidad
348
+ with gr.Group(visible=False) as chat_panel:
349
  chatbot = gr.Chatbot(label="Conversación con MateAI", height=600, type="messages")
350
  with gr.Row():
351
  chat_input = gr.Textbox(show_label=False, placeholder="Escribí acá con confianza...", scale=4)
352
  send_button = gr.Button("Enviar", variant="primary", scale=1)
353
 
354
+ # Panel de Login - Usando gr.Group en lugar de gr.Box para compatibilidad
355
+ with gr.Group(visible=True) as login_panel:
356
  gr.Markdown("### 🌟 Para empezar...")
357
  with gr.Tabs():
358
  with gr.TabItem("Crear Perfil Nuevo"):
 
362
  userid_input = gr.Textbox(label="Tu ID de Usuario")
363
  login_button = gr.Button("Cargar Perfil")
364
 
 
365
  with gr.Column(scale=1):
366
+ # Panel de Perfil - Usando gr.Group en lugar de gr.Box para compatibilidad
367
+ with gr.Group():
368
  gr.Markdown("### 🧠 Tu Perfil")
369
  profile_display = gr.Markdown("Cargá un perfil para ver tu información.")
370
 
371
  # --- Lógica de Eventos de la Interfaz ---
372
+ login_button.click(fn=handle_login_or_creation, inputs=[gr.State("login"), username_input, userid_input], outputs=[current_user, chatbot, login_panel, chat_panel])
373
+ create_button.click(fn=handle_login_or_creation, inputs=[gr.State("create"), username_input, userid_input], outputs=[current_user, chatbot, login_panel, chat_panel])
 
 
 
 
 
 
 
 
 
 
374
 
375
+ chat_input.submit(fn=handle_chat_message, inputs=[current_user, chat_input, chatbot], outputs=[current_user, chatbot, chat_input])
376
+ send_button.click(fn=handle_chat_message, inputs=[current_user, chat_input, chatbot], outputs=[current_user, chatbot, chat_input])
 
 
 
 
 
 
 
 
 
 
377
 
378
+ current_user.change(fn=render_profile_info, inputs=[current_user], outputs=[profile_display])
 
 
 
 
 
379
 
380
  if __name__ == "__main__":
 
 
381
  demo.launch(debug=True)