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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +100 -159
app.py CHANGED
@@ -1,6 +1,6 @@
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
@@ -11,62 +11,36 @@ import os
11
  import asyncio
12
  import logging
13
  from typing import Dict, Any, List, Optional, Tuple
 
14
 
15
- # --- LIBRERÍAS DE IA Y NLP ---
16
- # Usaremos pysentimiento para un análisis de sentimiento robusto en español.
17
  from pysentimiento import create_analyzer
18
-
19
- # --- INTEGRACIÓN CON FIREBASE ---
20
- # Firebase se usará como nuestro "núcleo de memoria" persistente.
21
  import firebase_admin
22
  from firebase_admin import credentials, firestore
23
 
24
  # ==============================================================================
25
  # MÓDULO 1: CONFIGURACIÓN Y CONSTANTES DEL SISTEMA
26
  # ==============================================================================
27
- # Define el comportamiento global, los límites y las constantes de la aplicación.
28
- # Es la "constitución" de MateAI.
29
- # ==============================================================================
30
-
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
43
- "extraversion": 0.0, # Sociabilidad y energía
44
- "agreeableness": 0.0, # Amabilidad y cooperación
45
- "neuroticism": 0.0, # Estabilidad emocional (inversa)
46
  }
47
-
48
- # --- Parámetros del Motor de Gamificación ---
49
- POINTS_PER_INSIGHT = 10 # Puntos por una reflexión profunda.
50
- POINTS_PER_GOAL_COMPLETED = 25# Puntos por completar una meta a largo plazo.
51
- POINTS_PER_FEEDBACK = 5 # Puntos por dar feedback a MateAI.
52
-
53
- # --- Parámetros del Motor de Interacción ---
54
- PROACTIVE_CHECKIN_HOURS = 6 # Horas antes de que MateAI considere un "check-in".
55
- MAX_MEMORY_STREAM_ITEMS = 200 # Límite de recuerdos para no sobrecargar el perfil.
56
- SENTIMENT_THRESHOLD_NEGATIVE = -0.3 # Umbral para detectar sentimiento negativo.
57
- SENTIMENT_THRESHOLD_POSITIVE = 0.3 # Umbral para detectar sentimiento positivo.
58
 
59
  # ==============================================================================
60
  # MÓDULO 2: INICIALIZACIÓN DE SERVICIOS EXTERNOS
61
  # ==============================================================================
62
- # Conexión a la base de datos y carga de modelos de IA.
63
- # Este bloque se ejecuta una sola vez al iniciar la aplicación.
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")
@@ -74,7 +48,6 @@ try:
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 ---
78
  db = None
79
  try:
80
  if not firebase_admin._apps:
@@ -82,13 +55,13 @@ try:
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)
87
  firebase_admin.initialize_app(cred)
88
  db = firestore.client()
89
- logging.info("Firebase Admin SDK inicializado correctamente. Conexión a Firestore establecida.")
90
  else:
91
- logging.warning("SECRET 'GOOGLE_APPLICATION_CREDENTIALS_JSON' no configurado. La persistencia de datos NO funcionará.")
92
  else:
93
  db = firestore.client()
94
  logging.info("Firebase Admin SDK ya estaba inicializado.")
@@ -99,180 +72,142 @@ except Exception as e:
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):
104
  self.user_id: str = user_id
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:
166
- logging.warning(f"Intento de carga de usuario inexistente: {user_id}")
167
- return None
168
  except Exception as e:
169
- logging.error(f"Error al cargar usuario {user_id} desde Firestore: {e}")
170
  return None
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})")
182
  return new_user, msg
183
  except Exception as e:
184
- logging.error(f"Error al crear usuario en Firestore: {e}")
185
- return None, f"Error inesperado al crear el perfil: {e}"
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)
192
- await asyncio.to_thread(user_doc_ref.set, user.to_dict())
193
  return True
194
  except Exception as e:
195
- logging.error(f"Error al guardar usuario {user.user_id} en Firestore: {e}")
196
  return False
197
 
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:
212
- logging.error(f"Error en análisis de sentimiento: {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:
223
- return f"{time_greeting}, {self.user.name}. Espero que estés teniendo un día tranquilo. ¿Cómo te sentís?"
224
- else:
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"
266
- "A) Improvisar y ver a dónde te lleva el día, quizás descubrir un café nuevo o un parque. \n"
267
  "B) Aprovechar para organizar esa pila de libros, planificar la semana o adelantar una tarea pendiente."
268
  )
269
  self.user.short_term_context['last_question'] = "openness_vs_conscientiousness"
270
- await UserManager.save_user(self.user)
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,14 +216,25 @@ class PersonaEngine:
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.")
@@ -299,10 +245,10 @@ async def handle_login_or_creation(action: str, name: str, user_id: str) -> tupl
299
  gr.Warning("¡Che, poné tu ID para cargar el perfil!")
300
  return None, gr.update(), gr.update(visible=True), gr.update(visible=False)
301
  user = await UserManager.get_user(user_id)
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}]
@@ -315,15 +261,18 @@ async def handle_chat_message(user_state: User, message: str, chat_history: List
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."
@@ -340,18 +289,15 @@ def render_profile_info(user: Optional[User]) -> str:
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():
@@ -361,20 +307,15 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="teal", secondary_hue="amber"),
361
  with gr.TabItem("Cargar Perfil Existente"):
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__":
 
1
+ # app.py - MateAI v18.4: Conciencia Aumentada (Corrección de Lógica e Inteligencia)
2
  # Arquitectura por un asistente de IA para un futuro colaborativo.
3
+ # Cambios: Se añade inteligencia conversacional real, manejo de frustración y feedback inmediato.
4
 
5
  import gradio as gr
6
  import random
 
11
  import asyncio
12
  import logging
13
  from typing import Dict, Any, List, Optional, Tuple
14
+ import re
15
 
 
 
16
  from pysentimiento import create_analyzer
 
 
 
17
  import firebase_admin
18
  from firebase_admin import credentials, firestore
19
 
20
  # ==============================================================================
21
  # MÓDULO 1: CONFIGURACIÓN Y CONSTANTES DEL SISTEMA
22
  # ==============================================================================
 
 
 
 
23
  class Config:
24
+ APP_NAME = "MateAI v18.4: Conciencia Aumentada"
25
+ APP_VERSION = "18.4.0"
26
+ FIREBASE_COLLECTION_USERS = "users_v18"
 
 
 
 
 
27
  DEFAULT_PSYCH_PROFILE = {
28
+ "openness": 0.0, "conscientiousness": 0.0, "extraversion": 0.0,
29
+ "agreeableness": 0.0, "neuroticism": 0.0
 
 
 
30
  }
31
+ POINTS_PER_INSIGHT = 10
32
+ PROACTIVE_CHECKIN_HOURS = 6
33
+ MAX_MEMORY_STREAM_ITEMS = 200
34
+ SENTIMENT_THRESHOLD_NEGATIVE = -0.3
35
+ # Palabras clave para detectar frustración o insultos
36
+ FRUSTRATION_KEYWORDS = ['tonto', 'inútil', 'bruto', 'estúpido', 'mierda', 'carajo', 'dale boludo']
37
+ META_QUESTION_KEYWORDS = ['para qué', 'de qué te sirve', 'por qué preguntas', 'cuál es el punto']
 
 
 
 
38
 
39
  # ==============================================================================
40
  # MÓDULO 2: INICIALIZACIÓN DE SERVICIOS EXTERNOS
41
  # ==============================================================================
 
 
 
 
 
42
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
43
 
 
44
  sentiment_analyzer = None
45
  try:
46
  sentiment_analyzer = create_analyzer(task="sentiment", lang="es")
 
48
  except Exception as e:
49
  logging.error(f"No se pudo cargar el analizador de sentimiento: {e}")
50
 
 
51
  db = None
52
  try:
53
  if not firebase_admin._apps:
 
55
  if firebase_credentials_json:
56
  cred_dict = json.loads(firebase_credentials_json)
57
  if 'project_id' not in cred_dict:
58
+ cred_dict['project_id'] = 'mateai-815ca'
59
  cred = credentials.Certificate(cred_dict)
60
  firebase_admin.initialize_app(cred)
61
  db = firestore.client()
62
+ logging.info("Firebase Admin SDK inicializado correctamente.")
63
  else:
64
+ logging.warning("SECRET 'GOOGLE_APPLICATION_CREDENTIALS_JSON' no configurado.")
65
  else:
66
  db = firestore.client()
67
  logging.info("Firebase Admin SDK ya estaba inicializado.")
 
72
  # MÓDULO 3: MODELOS DE DATOS Y CLASES CENTRALES
73
  # ==============================================================================
74
  class User:
 
75
  def __init__(self, user_id: str, name: str, **kwargs: Any):
76
  self.user_id: str = user_id
77
  self.name: str = name
78
  self.created_at: datetime = kwargs.get('created_at', datetime.now())
79
  self.last_login: datetime = kwargs.get('last_login', datetime.now())
80
+ self.login_count: int = kwargs.get('login_count', 0) # NUEVO: Contador de inicios de sesión
81
  self.psych_profile: Dict[str, float] = kwargs.get('psych_profile', Config.DEFAULT_PSYCH_PROFILE.copy())
82
  self.memory_stream: List[Dict[str, Any]] = kwargs.get('memory_stream', [])
83
  self.short_term_context: Dict[str, Any] = {}
 
84
  self.connection_points: int = kwargs.get('connection_points', 0)
 
 
85
 
86
  def to_dict(self) -> Dict[str, Any]:
 
87
  return {
88
  "user_id": self.user_id, "name": self.name, "created_at": self.created_at.isoformat(),
89
+ "last_login": self.last_login.isoformat(), "login_count": self.login_count,
90
+ "psych_profile": self.psych_profile, "memory_stream": self.memory_stream,
91
+ "connection_points": self.connection_points,
 
92
  }
93
 
94
  @classmethod
95
  def from_dict(cls, data: Dict[str, Any]) -> 'User':
 
96
  data['created_at'] = datetime.fromisoformat(data.get('created_at', datetime.now().isoformat()))
97
  data['last_login'] = datetime.fromisoformat(data.get('last_login', datetime.now().isoformat()))
 
 
98
  profile = Config.DEFAULT_PSYCH_PROFILE.copy()
99
  profile.update(data.get('psych_profile', {}))
100
  data['psych_profile'] = profile
101
  return cls(**data)
 
 
 
 
 
 
 
 
102
 
103
  # ==============================================================================
104
+ # MÓDULO 4: GESTOR DE DATOS DE USUARIO
105
  # ==============================================================================
106
  class UserManager:
 
107
  @staticmethod
108
  async def get_user(user_id: str) -> Optional[User]:
109
  if not db or not user_id: return None
110
  try:
111
+ doc_ref = db.collection(Config.FIREBASE_COLLECTION_USERS).document(user_id)
112
+ doc = await asyncio.to_thread(doc_ref.get)
113
  if doc.exists:
114
  user_data = doc.to_dict()
115
  user_data['user_id'] = doc.id
116
  user_obj = User.from_dict(user_data)
117
  user_obj.last_login = datetime.now()
118
+ user_obj.login_count += 1 # Aumentamos el contador en cada carga
119
  await UserManager.save_user(user_obj)
120
+ logging.info(f"Usuario '{user_obj.name}' cargado. Login #{user_obj.login_count}.")
121
  return user_obj
122
+ return None
 
 
123
  except Exception as e:
124
+ logging.error(f"Error al cargar usuario {user_id}: {e}")
125
  return None
126
 
127
  @staticmethod
128
  async def create_user(name: str) -> Tuple[Optional[User], str]:
129
+ if not db: return None, "Error de base de datos."
 
130
  try:
131
  user_id = f"{name.lower().replace(' ', '_')}_{int(time.time())}"
132
+ new_user = User(user_id=user_id, name=name, login_count=1) # El primer login es la creación
133
  await UserManager.save_user(new_user)
134
+ msg = f"¡Bienvenido, {name}! Tu perfil ha sido creado. Guarda este ID: **{user_id}**"
135
  logging.info(f"Nuevo usuario creado: {name} ({user_id})")
136
  return new_user, msg
137
  except Exception as e:
138
+ logging.error(f"Error al crear usuario: {e}")
139
+ return None, "Error inesperado al crear perfil."
140
 
141
  @staticmethod
142
  async def save_user(user: User) -> bool:
143
  if not db: return False
144
  try:
145
+ doc_ref = db.collection(Config.FIREBASE_COLLECTION_USERS).document(user.user_id)
146
+ await asyncio.to_thread(doc_ref.set, user.to_dict())
147
  return True
148
  except Exception as e:
149
+ logging.error(f"Error al guardar usuario {user.user_id}: {e}")
150
  return False
151
 
152
  # ==============================================================================
153
+ # MÓDULO 5: MOTOR DE PERSONA Y LÓGICA DE IA (REFACTORIZADO)
154
  # ==============================================================================
155
  class PersonaEngine:
 
156
  def __init__(self, user: User):
157
  self.user = user
158
 
 
 
 
 
 
 
 
 
 
159
  def _get_greeting(self) -> str:
160
+ # CORREGIDO: Saludo diferenciado para la primera vez.
161
+ if self.user.login_count <= 1:
162
+ return f"¡Hola, {self.user.name}! Soy MateAI, tu compañero para la introspección. Es un gusto conocerte. Para empezar a entendernos, a veces te haré algunas preguntas. ¿Listo para empezar?"
163
+
164
  hour = datetime.now().hour
165
  if 5 <= hour < 12: time_greeting = "Buen día"
166
  elif 12 <= hour < 19: time_greeting = "Buenas tardes"
167
  else: time_greeting = "Buenas noches"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
 
169
+ return f"{time_greeting}, {self.user.name}. Qué bueno conectar de nuevo."
170
+
171
  async def generate_response(self, message: str) -> str:
172
+ message_lower = message.lower()
173
+ sentiment = await asyncio.to_thread(sentiment_analyzer.predict, message)
174
+
175
+ # LÓGICA 1: Manejar frustración del usuario. Tiene máxima prioridad.
176
+ if any(keyword in message_lower for keyword in Config.FRUSTRATION_KEYWORDS) or \
177
+ (sentiment.output == 'NEG' and sentiment.probas[sentiment.output] > 0.8):
178
+ return f"Entiendo tu frustración. Claramente mi respuesta anterior no fue buena y pido disculpas. Soy un sistema en desarrollo y tu feedback honesto me ayuda a mejorar. Por favor, decime qué te molestó o cómo puedo ayudarte mejor."
179
+
180
+ # LÓGICA 2: Manejar meta-preguntas sobre el propósito.
181
+ if any(keyword in message_lower for keyword in Config.META_QUESTION_KEYWORDS):
182
+ return f"Buena pregunta. Te hago estas preguntas para ir construyendo un mapa de tu personalidad, sin que tengas que llenar un formulario. Saber si preferís la improvisación o la planificación, por ejemplo, me ayuda a entender qué tipo de consejos o reflexiones te pueden servir más. No es para juzgar, sino para personalizar nuestra conversación. Por cierto, gracias por preguntar, demuestra curiosidad."
183
+
184
+ # LÓGICA 3: Procesar respuesta a una pregunta de perfilado.
185
+ if self.user.short_term_context.get('last_question'):
186
+ response, points_awarded = self.process_profiling_answer(message)
187
+ await UserManager.save_user(self.user)
188
+ return f"{response} (Por tu reflexión, sumaste **{points_awarded} Puntos de Conexión** 💠)."
189
 
190
+ # LÓGICA 4: Hacer una pregunta de perfilado si el perfil es nuevo.
191
+ if abs(sum(self.user.psych_profile.values())) < 0.1: # Perfil casi virgen
192
+ return self._ask_profiling_question()
 
 
 
 
 
 
 
 
193
 
194
+ # LÓGICA 5: Respuesta por defecto (mejorada).
195
+ return self._craft_default_response(message)
196
+
197
+ def _ask_profiling_question(self) -> str:
198
  question = (
199
+ f"Una pregunta curiosa, {self.user.name}: si tuvieras una tarde libre inesperada, ¿qué te tienta más?\n"
200
+ "A) Improvisar y ver a dónde te lleva el día, quizás descubrir un café nuevo o un parque.\n"
201
+
202
  "B) Aprovechar para organizar esa pila de libros, planificar la semana o adelantar una tarea pendiente."
203
  )
204
  self.user.short_term_context['last_question'] = "openness_vs_conscientiousness"
 
205
  return question
206
 
207
+ def process_profiling_answer(self, answer: str) -> Tuple[str, int]:
208
  question_type = self.user.short_term_context.get('last_question')
209
+ if not question_type: return "Hmm, no recuerdo haberte preguntado nada. Sigamos.", 0
210
+
211
  answer_lower = answer.lower()
212
  if question_type == "openness_vs_conscientiousness":
213
  if 'a' in answer_lower or 'improvisar' in answer_lower:
 
216
  elif 'b' in answer_lower or 'organizar' in answer_lower:
217
  self.user.psych_profile['conscientiousness'] += 0.3
218
  self.user.psych_profile['openness'] -= 0.1
219
+
220
  del self.user.short_term_context['last_question']
221
  self.user.connection_points += Config.POINTS_PER_INSIGHT
222
  logging.info(f"Perfil de {self.user.name} actualizado. Puntos: {self.user.connection_points}")
223
+ # CORREGIDO: Devolvemos una respuesta y los puntos ganados para feedback inmediato.
224
+ return "¡Bárbaro! Gracias por compartirlo, lo tengo en cuenta para que nos entendamos mejor.", Config.POINTS_PER_INSIGHT
225
+
226
+ def _craft_default_response(self, message: str) -> str:
227
+ return random.choice([
228
+ f"Entendido. ¿Hay algo más en lo que estés pensando, {self.user.name}?",
229
+ "Ok, te sigo. ¿Querés explorar más esa idea?",
230
+ "Gracias por compartir. Siempre es bueno tener tu perspectiva.",
231
+ ])
232
 
233
  # ==============================================================================
234
+ # MÓDULO 6: LÓGICA Y ESTRUCTURA DE LA INTERFAZ (GRADIO)
235
  # ==============================================================================
236
  async def handle_login_or_creation(action: str, name: str, user_id: str) -> tuple:
237
+ user, msg = None, ""
238
  if action == "create":
239
  if not name:
240
  gr.Warning("Para crear un perfil, necesito que me digas tu nombre.")
 
245
  gr.Warning("¡Che, poné tu ID para cargar el perfil!")
246
  return None, gr.update(), gr.update(visible=True), gr.update(visible=False)
247
  user = await UserManager.get_user(user_id)
248
+ if not user: msg = "ID de usuario no encontrado. Verificá que esté bien escrito."
249
+
 
250
  if user:
251
+ if action == "login": msg = f"Perfil de {user.name} cargado."
252
  gr.Success(msg)
253
  initial_greeting = PersonaEngine(user)._get_greeting()
254
  chat_history = [{"role": "assistant", "content": initial_greeting}]
 
261
  if not user_state:
262
  gr.Warning("¡Para empezar, creá un perfil o iniciá sesión!")
263
  return user_state, chat_history, ""
264
+
265
  chat_history.append({"role": "user", "content": message})
266
+
267
  engine = PersonaEngine(user_state)
268
+ response = await engine.generate_response(message)
269
+
270
+ # Guardamos el estado del usuario DESPUÉS de generar la respuesta.
271
+ await UserManager.save_user(engine.user)
272
+
273
  chat_history.append({"role": "assistant", "content": response})
274
+
275
+ return engine.user, chat_history, ""
276
 
277
  def render_profile_info(user: Optional[User]) -> str:
278
  if not user: return "Cargá un perfil para ver tu información."
 
289
  with gr.Blocks(theme=gr.themes.Soft(primary_hue="teal", secondary_hue="amber"), css="footer {display: none !important}") as demo:
290
  current_user = gr.State(None)
291
  gr.Markdown(f"# 🧉 {Config.APP_NAME}")
292
+ gr.Markdown(f"*{Config.APP_VERSION}* - Un compañero de IA que aprende con vos.")
293
 
294
  with gr.Row():
295
  with gr.Column(scale=2):
 
296
  with gr.Group(visible=False) as chat_panel:
297
+ chatbot = gr.Chatbot(label="Conversación con MateAI", height=600, type="messages", show_copy_button=True)
298
  with gr.Row():
299
  chat_input = gr.Textbox(show_label=False, placeholder="Escribí acá con confianza...", scale=4)
300
  send_button = gr.Button("Enviar", variant="primary", scale=1)
 
 
301
  with gr.Group(visible=True) as login_panel:
302
  gr.Markdown("### 🌟 Para empezar...")
303
  with gr.Tabs():
 
307
  with gr.TabItem("Cargar Perfil Existente"):
308
  userid_input = gr.Textbox(label="Tu ID de Usuario")
309
  login_button = gr.Button("Cargar Perfil")
 
310
  with gr.Column(scale=1):
 
311
  with gr.Group():
312
  gr.Markdown("### 🧠 Tu Perfil")
313
  profile_display = gr.Markdown("Cargá un perfil para ver tu información.")
314
 
 
315
  login_button.click(fn=handle_login_or_creation, inputs=[gr.State("login"), username_input, userid_input], outputs=[current_user, chatbot, login_panel, chat_panel])
316
  create_button.click(fn=handle_login_or_creation, inputs=[gr.State("create"), username_input, userid_input], outputs=[current_user, chatbot, login_panel, chat_panel])
 
317
  chat_input.submit(fn=handle_chat_message, inputs=[current_user, chat_input, chatbot], outputs=[current_user, chatbot, chat_input])
318
  send_button.click(fn=handle_chat_message, inputs=[current_user, chat_input, chatbot], outputs=[current_user, chatbot, chat_input])
 
319
  current_user.change(fn=render_profile_info, inputs=[current_user], outputs=[profile_display])
320
 
321
  if __name__ == "__main__":