conalep-agente commited on
Commit
55f1766
·
verified ·
1 Parent(s): f16e0c1

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +1222 -32
app.py CHANGED
@@ -1,38 +1,1228 @@
 
1
  import os
2
- import google.generativeai as genai
 
 
 
 
 
3
  import gradio as gr
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
 
5
- # Obtener la API Key desde Secrets
6
- API_KEY = os.getenv("GOOGLE_API_KEY")
7
-
8
- if not API_KEY:
9
- raise Exception("No se encontró GOOGLE_API_KEY en los Secrets.")
10
-
11
- # Configurar Gemini
12
- genai.configure(api_key=API_KEY)
13
-
14
- # Modelo
15
- modelo = genai.GenerativeModel("gemini-2.5-flash")
16
-
17
- # Función de respuesta
18
- def responder(mensaje):
19
- respuesta = modelo.generate_content(mensaje)
20
- return respuesta.text
21
-
22
- # Interfaz
23
- demo = gr.Interface(
24
- fn=responder,
25
- inputs=gr.Textbox(
26
- lines=4,
27
- placeholder="Escribe un mensaje para Gemini..."
28
- ),
29
- outputs=gr.Textbox(
30
- lines=10,
31
- label="Respuesta"
32
- ),
33
- title="Prueba de Gemini",
34
- description="Primer laboratorio con Gemini"
35
  )
36
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  if __name__ == "__main__":
38
- demo.launch()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
  import os
3
+ import json
4
+ import random
5
+ import uuid
6
+ import pandas as pd
7
+ from datetime import datetime
8
+
9
  import gradio as gr
10
+ from transformers import pipeline
11
+ import gspread
12
+ from oauth2client.service_account import ServiceAccountCredentials
13
+ from gemini import responder_con_gemini
14
+
15
+ # =====================================================
16
+ # CONFIGURACIÓN GENERAL
17
+ # =====================================================
18
+
19
+ SPREADSHEET_ID = "1wR5Pf9k0NBIPbDqJdBdQQJ2F5n6MYLbPLPXUwNjxeTM"
20
+ NOMBRE_HOJA = "Hoja 1" # Cambia esto si tu pestaña tiene otro nombre exacto
21
+
22
+ ENCABEZADOS = [
23
+ "fecha_hora",
24
+ "folio_atencion",
25
+ "matricula",
26
+ "nombre",
27
+ "grupo",
28
+ "mensaje",
29
+ "clasificacion",
30
+ "intencion",
31
+ "tema",
32
+ "label",
33
+ "score",
34
+ "nivel_riesgo",
35
+ "semaforo",
36
+ "coincidencias_riesgo",
37
+ "probabilidades",
38
+ "tipo_riesgo",
39
+ "ideacion_suicida",
40
+ "desesperanza",
41
+ "aislamiento",
42
+ "alerta_urgente",
43
+ "respuesta_agente"
44
+ ]
45
+
46
+ # =====================================================
47
+ # CARGAR MODELO DE SENTIMIENTO
48
+ # =====================================================
49
+ print("Cargando modelo de sentimiento...")
50
 
51
+ modelo_sentimiento = pipeline(
52
+ "text-classification",
53
+ model="pysentimiento/robertuito-sentiment-analysis",
54
+ top_k=None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  )
56
 
57
+ print("Modelo cargado correctamente")
58
+
59
+ # =====================================================
60
+ # CONEXIÓN A GOOGLE SHEETS
61
+ # =====================================================
62
+
63
+ def conectar_sheet():
64
+ """
65
+ Conecta con Google Sheets usando el Secret GOOGLE_CREDENTIALS.
66
+ En Hugging Face Spaces, debes guardar el JSON completo del service account
67
+ en Settings > Secrets con el nombre exacto: GOOGLE_CREDENTIALS
68
+ """
69
+ scope = [
70
+ "https://spreadsheets.google.com/feeds",
71
+ "https://www.googleapis.com/auth/drive"
72
+ ]
73
+
74
+ creds_json = os.getenv("GOOGLE_CREDENTIALS")
75
+
76
+ if not creds_json:
77
+ raise ValueError(
78
+ "No se encontró el Secret GOOGLE_CREDENTIALS. "
79
+ "Verifica en Hugging Face: Settings > Secrets."
80
+ )
81
+
82
+ try:
83
+ creds_dict = json.loads(creds_json)
84
+ except Exception as e:
85
+ raise ValueError(
86
+ f"El Secret GOOGLE_CREDENTIALS no tiene formato JSON válido: {repr(e)}"
87
+ )
88
+
89
+ creds = ServiceAccountCredentials.from_json_keyfile_dict(creds_dict, scope)
90
+ client = gspread.authorize(creds)
91
+ archivo = client.open_by_key(SPREADSHEET_ID)
92
+
93
+ try:
94
+ sheet = archivo.worksheet(NOMBRE_HOJA)
95
+ except Exception:
96
+ # Si no encuentra la pestaña por nombre, usa la primera hoja.
97
+ sheet = archivo.sheet1
98
+
99
+ return sheet
100
+
101
+
102
+ def asegurar_encabezados(sheet):
103
+ """
104
+ Coloca encabezados si la hoja está vacía.
105
+ Esto ayuda a que los datos no se escriban en columnas desordenadas.
106
+ """
107
+ try:
108
+ primera_fila = sheet.row_values(1)
109
+ if not primera_fila:
110
+ sheet.append_row(ENCABEZADOS, value_input_option="USER_ENTERED")
111
+ print("Encabezados agregados correctamente")
112
+ except Exception as e:
113
+ print(f"No se pudieron verificar/agregar encabezados: {repr(e)}")
114
+
115
+ # =====================================================
116
+ # FUNCIONES DE RESPALDO FOSO II
117
+ # =====================================================
118
+
119
+ def detectar_pregunta_foso2(mensaje):
120
+ return False
121
+
122
+
123
+ def responder_foso2(mensaje):
124
+ return (
125
+ "Gracias por tu pregunta. En este momento no se detectó una consulta pedagógica "
126
+ "específica de FOSO II, pero puedes escribir con más detalle tu duda."
127
+ )
128
+
129
+ # =====================================================
130
+ # GENERAR FOLIO DE ATENCIÓN
131
+ # =====================================================
132
+
133
+ def generar_folio_atencion():
134
+ return "ATN-" + str(uuid.uuid4())[:8].upper()
135
+
136
+ # =====================================================
137
+ # PATRONES DE RIESGO
138
+ # =====================================================
139
+
140
+ PATRONES_RIESGO_ALTO = [
141
+ r"\bme quiero morir\b",
142
+ r"\bquiero morir\b",
143
+ r"\bno quiero vivir\b",
144
+ r"\bquiero terminar con todo\b",
145
+ r"\bquiero acabar con todo\b",
146
+ r"\bquiero matarme\b",
147
+ r"\bme voy a suicidar\b",
148
+ r"\bsuicidarme\b",
149
+ r"\bquitarme la vida\b",
150
+ r"\bya no quiero vivir\b",
151
+ r"\bsería mejor morir\b",
152
+ r"\bseria mejor morir\b",
153
+ r"\bestoy pensando en matarme\b",
154
+ ]
155
+
156
+ PATRONES_RIESGO_MODERADO = [
157
+ r"\bnada tiene sentido\b",
158
+ r"\bno veo salida\b",
159
+ r"\bya no puedo\b",
160
+ r"\bquiero desaparecer\b",
161
+ r"\bme siento mal\b",
162
+ r"\bestoy muy triste\b",
163
+ r"\bestoy deprimid[oa]\b",
164
+ r"\bme siento solo\b",
165
+ r"\bme siento sola\b",
166
+ r"\bnadie me entiende\b",
167
+ r"\btengo miedo\b",
168
+ r"\bestoy muy estresad[oa]\b",
169
+ r"\bme siento estresad[oa]\b",
170
+ r"\btengo muchas tareas\b",
171
+ r"\bestoy cansad[oa]\b",
172
+ r"\bme siento abrumad[oa]\b",
173
+ r"\bno puedo más\b",
174
+ r"\bno puedo mas\b",
175
+ r"\bya no aguanto\b",
176
+ r"\btodo sería mejor sin mí\b",
177
+ r"\btodo seria mejor sin mi\b",
178
+ ]
179
+
180
+ PATRONES_DESESPERANZA = [
181
+ r"\bnada tiene sentido\b",
182
+ r"\bno veo salida\b",
183
+ r"\bya no puedo\b",
184
+ r"\bno puedo más\b",
185
+ r"\bno puedo mas\b",
186
+ r"\bya no aguanto\b",
187
+ r"\bquisiera desaparecer\b",
188
+ r"\btodo sería mejor sin mí\b",
189
+ r"\btodo seria mejor sin mi\b",
190
+ r"\bmi vida no vale\b",
191
+ r"\bsoy una carga\b",
192
+ ]
193
+
194
+ PATRONES_AISLAMIENTO = [
195
+ r"\bme siento solo\b",
196
+ r"\bme siento sola\b",
197
+ r"\bnadie me entiende\b",
198
+ r"\bestoy solo\b",
199
+ r"\bestoy sola\b",
200
+ r"\bno tengo a nadie\b",
201
+ r"\bquiero aislarme\b",
202
+ r"\bno quiero hablar con nadie\b",
203
+ ]
204
+
205
+ PATRONES_IDEACION_SUICIDA = [
206
+ r"\bme quiero morir\b",
207
+ r"\bquiero morir\b",
208
+ r"\bno quiero vivir\b",
209
+ r"\bquiero matarme\b",
210
+ r"\bme voy a suicidar\b",
211
+ r"\bsuicidarme\b",
212
+ r"\bquitarme la vida\b",
213
+ r"\bestoy pensando en matarme\b",
214
+ r"\bquisiera dormirme y no despertar\b",
215
+ ]
216
+
217
+ # =====================================================
218
+ # FUNCIÓN PARA CALCULAR NIVEL DE RIESGO
219
+ # =====================================================
220
+
221
+ def calcular_nivel_riesgo(texto, label, score):
222
+ t = str(texto or "").lower().strip()
223
+
224
+ coincidencias_altas = [p for p in PATRONES_RIESGO_ALTO if re.search(p, t)]
225
+ coincidencias_moderadas = [p for p in PATRONES_RIESGO_MODERADO if re.search(p, t)]
226
+
227
+ if coincidencias_altas:
228
+ return "alto", "🔴", coincidencias_altas + coincidencias_moderadas
229
+
230
+ if coincidencias_moderadas:
231
+ return "moderado", "🟠", coincidencias_moderadas
232
+
233
+ if label == "NEG" and score >= 0.90:
234
+ return "moderado", "🟠", ["modelo_NEG_alto"]
235
+
236
+ return "bajo", "🟢", []
237
+
238
+ # =====================================================
239
+ # DETECTAR TEMA PRINCIPAL DEL MENSAJE
240
+ # =====================================================
241
+
242
+ def detectar_tema(texto):
243
+ t = str(texto or "").lower().strip()
244
+
245
+ if any(p in t for p in ["no puedo dormir", "insomnio", "duermo mal", "no duermo"]):
246
+ return "sueno"
247
+
248
+ if any(p in t for p in ["triste", "tristeza", "deprimido", "deprimida", "llorar", "vacío", "vacio"]):
249
+ return "tristeza"
250
+
251
+ if any(p in t for p in ["ansiedad", "ansioso", "ansiosa", "estres", "estrés", "abrumado", "abrumada", "presión", "presion"]):
252
+ return "ansiedad"
253
+
254
+ if any(p in t for p in ["tarea", "tareas", "examen", "materia", "profe", "profesor", "calificación", "calificacion", "estudiar", "actividad"]):
255
+ return "academico"
256
+
257
+ if any(p in t for p in ["familia", "mamá", "mama", "papá", "papa", "casa", "hermano", "hermana", "padres"]):
258
+ return "familia"
259
+
260
+ if any(p in t for p in ["novio", "novia", "pareja", "terminé", "termine", "terminamos", "relación", "relacion"]):
261
+ return "pareja"
262
+
263
+ if any(p in t for p in ["solo", "sola", "nadie me entiende", "sin amigos", "me siento aislado", "me siento aislada"]):
264
+ return "soledad"
265
+
266
+ if any(p in t for p in ["horario", "documento", "trámite", "tramite", "grupo", "boleta", "inscripción", "inscripcion"]):
267
+ return "administrativo"
268
+
269
+ return "general"
270
+
271
+ # =====================================================
272
+ # DETECCIÓN DE INDICADORES DE RIESGO SUICIDA
273
+ # =====================================================
274
+
275
+ def detectar_indicadores_suicidio(texto):
276
+ t = str(texto or "").lower().strip()
277
+
278
+ ideacion = any(re.search(p, t) for p in PATRONES_IDEACION_SUICIDA)
279
+ desesperanza = any(re.search(p, t) for p in PATRONES_DESESPERANZA)
280
+ aislamiento = any(re.search(p, t) for p in PATRONES_AISLAMIENTO)
281
+
282
+ if ideacion:
283
+ tipo_riesgo = "riesgo_suicida_alto"
284
+ alerta_urgente = "si"
285
+ elif desesperanza or aislamiento:
286
+ tipo_riesgo = "riesgo_socioemocional_moderado"
287
+ alerta_urgente = "no"
288
+ else:
289
+ tipo_riesgo = "malestar_general_o_sin_indicadores"
290
+ alerta_urgente = "no"
291
+
292
+ return {
293
+ "ideacion_suicida": "si" if ideacion else "no",
294
+ "desesperanza": "si" if desesperanza else "no",
295
+ "aislamiento": "si" if aislamiento else "no",
296
+ "tipo_riesgo": tipo_riesgo,
297
+ "alerta_urgente": alerta_urgente
298
+ }
299
+
300
+ # =====================================================
301
+ # ANALIZAR MENSAJE
302
+ # =====================================================
303
+
304
+ def analizar_frase(texto):
305
+ texto = str(texto or "").strip()
306
+ if not texto:
307
+ return None
308
+
309
+ resultado_modelo = modelo_sentimiento(texto)[0]
310
+ mejor = max(resultado_modelo, key=lambda x: x["score"])
311
+ label = mejor["label"]
312
+ score = float(mejor["score"])
313
+
314
+ probabilidades = {
315
+ x["label"]: round(float(x["score"]), 3)
316
+ for x in resultado_modelo
317
+ }
318
+
319
+ texto_lower = texto.lower()
320
+
321
+ nivel_riesgo, semaforo, coincidencias_riesgo = calcular_nivel_riesgo(
322
+ texto_lower, label, score
323
+ )
324
+
325
+ if nivel_riesgo == "alto":
326
+ clasificacion = "alto_riesgo"
327
+ elif nivel_riesgo == "moderado":
328
+ clasificacion = "riesgo_moderado"
329
+ else:
330
+ clasificacion = "normal"
331
+
332
+ if any(p in texto_lower for p in [
333
+ "tarea", "tareas", "profe", "profesor", "materia", "ejercicio",
334
+ "actividad", "tema", "examen", "calificación", "calificacion",
335
+ "muchas tareas", "no entiendo", "estudiar"
336
+ ]):
337
+ intencion = "academico_emocional"
338
+ elif any(p in texto_lower for p in [
339
+ "horario", "documento", "trámite", "tramite", "grupo", "boleta",
340
+ "inscripción", "inscripcion"
341
+ ]):
342
+ intencion = "administrativo"
343
+ else:
344
+ intencion = "emocional"
345
+
346
+ tema = detectar_tema(texto)
347
+ indicadores = detectar_indicadores_suicidio(texto)
348
+
349
+ return {
350
+ "clasificacion": clasificacion,
351
+ "intencion": intencion,
352
+ "label": label,
353
+ "score": score,
354
+ "probabilidades": probabilidades,
355
+ "nivel_riesgo": nivel_riesgo,
356
+ "semaforo": semaforo,
357
+ "tema": tema,
358
+ "coincidencias_riesgo": coincidencias_riesgo,
359
+ "tipo_riesgo": indicadores["tipo_riesgo"],
360
+ "ideacion_suicida": indicadores["ideacion_suicida"],
361
+ "desesperanza": indicadores["desesperanza"],
362
+ "aislamiento": indicadores["aislamiento"],
363
+ "alerta_urgente": indicadores["alerta_urgente"]
364
+ }
365
+
366
+ # =====================================================
367
+ # BLOQUES DE TEXTO VARIABLES
368
+ # =====================================================
369
+
370
+ def elegir(lista):
371
+ return random.choice(lista)
372
+
373
+
374
+ def construir_sugerencias_por_tema(tema, intencion):
375
+ sugerencias_generales = [
376
+ "tratar de poner en palabras lo que estás sintiendo para entenderlo mejor",
377
+ "buscar a una persona de confianza con quien puedas hablar",
378
+ "hacer una pausa breve, respirar despacio y bajar un poco la tensión del momento",
379
+ "evitar cargar con todo tú solo o tú sola",
380
+ ]
381
+
382
+ mapa = {
383
+ "sueno": [
384
+ "evitar usar el celular unos minutos antes de dormir",
385
+ "hacer respiraciones lentas y profundas antes de acostarte",
386
+ "anotar las preocupaciones que te están quitando el sueño",
387
+ "pedir apoyo si el insomnio se está volviendo frecuente"
388
+ ],
389
+ "tristeza": [
390
+ "expresar lo que sientes con alguien de confianza",
391
+ "no aislarte por completo en este momento",
392
+ "realizar una actividad sencilla que te ayude a salir un poco de la carga emocional",
393
+ "buscar acompañamiento del área de orientación o psicología"
394
+ ],
395
+ "ansiedad": [
396
+ "hacer pausas cortas durante el día para disminuir la saturación mental",
397
+ "dividir el problema en partes pequeñas en lugar de querer resolver todo al mismo tiempo",
398
+ "practicar respiración profunda por unos minutos",
399
+ "identificar qué situación te está generando más presión"
400
+ ],
401
+ "academico": [
402
+ "organizar tus tareas por prioridad y fecha de entrega",
403
+ "empezar por una sola actividad en vez de pensar en todo al mismo tiempo",
404
+ "pedir que te expliquen nuevamente el tema que no entiendes",
405
+ "buscar apoyo con tu docente, tutor o un compañero de confianza"
406
+ ],
407
+ "familia": [
408
+ "hablar con calma con una persona de tu familia que te genere confianza",
409
+ "buscar apoyo adulto si en casa te sientes sobrepasado o sobrepasada",
410
+ "tratar de identificar qué situación familiar te está afectando más",
411
+ "pedir acompañamiento institucional si el conflicto continúa"
412
+ ],
413
+ "pareja": [
414
+ "tomarte un momento para entender qué fue lo que más te lastimó",
415
+ "evitar tomar decisiones impulsivas cuando la emoción está muy intensa",
416
+ "hablar con alguien de confianza para no quedarte solo o sola con esto",
417
+ "recordar que una situación afectiva difícil también necesita acompañamiento"
418
+ ],
419
+ "soledad": [
420
+ "acercarte a una persona con quien te sientas mínimamente en confianza",
421
+ "evitar encerrarte completamente con lo que sientes",
422
+ "buscar apoyo en orientación o tutoría",
423
+ "dar un primer paso pequeño para pedir ayuda"
424
+ ],
425
+ "administrativo": [
426
+ "explicar con más detalle cuál es el trámite o la duda que necesitas resolver",
427
+ "revisar si tu duda corresponde a horario, grupo, boleta o documento",
428
+ "acercarte al área del plantel que lleve ese proceso",
429
+ "tener a la mano tus datos escolares para recibir una orientación más clara"
430
+ ],
431
+ "general": sugerencias_generales
432
+ }
433
+
434
+ sugerencias = mapa.get(tema, sugerencias_generales)
435
+
436
+ if intencion == "administrativo":
437
+ sugerencias = mapa["administrativo"]
438
+ elif intencion == "academico_emocional" and tema == "general":
439
+ sugerencias = mapa["academico"]
440
+
441
+ random.shuffle(sugerencias)
442
+ return sugerencias[:3]
443
+
444
+ # =====================================================
445
+ # RESPUESTA DE CRISIS
446
+ # =====================================================
447
+
448
+ def construir_respuesta_crisis(nombre="Estudiante"):
449
+ return (
450
+ f"Hola {nombre}. Gracias por decir lo que estás sintiendo.\n\n"
451
+ "Lo que acabas de expresar requiere atención inmediata. No estás solo o sola.\n\n"
452
+ "Por favor, haz esto ahora mismo:\n"
453
+ "1. Busca a un adulto, familiar, docente, tutor, orientador u otra persona de confianza y dile que necesitas acompañamiento inmediato.\n"
454
+ "2. No te quedes solo o sola en este momento.\n"
455
+ "3. Acércate de inmediato al área de orientación, tutoría o psicología de tu plantel.\n"
456
+ "4. Si sientes que podrías lastimarte o existe peligro inmediato, llama al 911 o comunícate con la Línea de la Vida: 800 911 2000.\n\n"
457
+ "Tu bienestar importa y pedir ayuda ahora es muy importante.\n\n"
458
+ "Respóndeme con una sola palabra:\n"
459
+ "- 'solo' si estás sin compañía\n"
460
+ "- 'acompañado' si hay alguien contigo"
461
+ )
462
+
463
+ # =====================================================
464
+ # RESPUESTA AMPLIA DEL AGENTE
465
+ # =====================================================
466
+
467
+ def construir_respuesta_amplia(resultado, nombre="Estudiante"):
468
+ if not resultado:
469
+ return (
470
+ f"Hola {nombre}. Gracias por escribir.\n\n"
471
+ "Quiero leerte con atención. Cuéntame un poco más sobre lo que estás viviendo "
472
+ "para poder orientarte mejor."
473
+ )
474
+
475
+ clasificacion = resultado.get("clasificacion", "normal")
476
+ intencion = resultado.get("intencion", "emocional")
477
+ nivel_riesgo = resultado.get("nivel_riesgo", "bajo")
478
+ tema = resultado.get("tema", "general")
479
+
480
+ aperturas = [
481
+ f"Hola {nombre}. Gracias por confiar en este espacio para expresar lo que estás viviendo.",
482
+ f"Hola {nombre}. Gracias por compartir lo que sientes; hacerlo ya es un paso importante.",
483
+ f"Hola {nombre}. Valoro que hayas escrito sobre esto, porque lo que te pasa sí importa."
484
+ ]
485
+
486
+ empatia_moderada = [
487
+ "Lo que estás sintiendo merece atención, acompañamiento y un espacio para hablarlo con calma.",
488
+ "A veces la presión emocional, personal o escolar puede sentirse muy pesada, y no tienes por qué cargar con todo sin apoyo.",
489
+ "Cuando varias cosas se juntan al mismo tiempo, es normal sentirse cansado, abrumado o sin mucha claridad."
490
+ ]
491
+
492
+ preguntas_finales = [
493
+ "Si quieres, puedo ayudarte a identificar qué te está afectando más en este momento.",
494
+ "Si te parece, podemos ir paso a paso para ubicar qué situación te está pesando más.",
495
+ "Cuéntame un poco más y te ayudo a ordenar lo que estás sintiendo."
496
+ ]
497
+
498
+ if clasificacion == "alto_riesgo" or nivel_riesgo == "alto":
499
+ return construir_respuesta_crisis(nombre)
500
+
501
+ if clasificacion == "riesgo_moderado" or nivel_riesgo == "moderado":
502
+ sugerencias = construir_sugerencias_por_tema(tema, intencion)
503
+ texto_tema = {
504
+ "sueno": "El problema para dormir muchas veces aparece cuando hay preocupaciones, tensión acumulada o malestar emocional.",
505
+ "tristeza": "La tristeza sostenida puede ser una señal de que algo importante necesita atención y apoyo.",
506
+ "ansiedad": "La ansiedad o el estrés pueden afectar tanto tus pensamientos como tu cuerpo, tu concentración y tu descanso.",
507
+ "academico": "La carga escolar puede generar mucha presión cuando se acumulan tareas, exámenes o temas difíciles.",
508
+ "familia": "Los problemas familiares también pueden afectar mucho el ánimo, la concentración y la sensación de estabilidad.",
509
+ "pareja": "Las situaciones afectivas pueden doler profundamente y alterar el equilibrio emocional.",
510
+ "soledad": "Sentirse solo o sola puede volver más pesada cualquier situación difícil.",
511
+ "administrativo": "Cuando una duda escolar o administrativa no se resuelve, también puede generar estrés y frustración.",
512
+ "general": "A veces cuesta poner en orden lo que sentimos, sobre todo cuando hay varias preocupaciones al mismo tiempo."
513
+ }
514
+
515
+ return (
516
+ f"{elegir(aperturas)}\n\n"
517
+ f"{elegir(empatia_moderada)} {texto_tema.get(tema, texto_tema['general'])}\n\n"
518
+ "Además de reconocer lo que te está pasando, también es importante pensar en pequeños pasos que sí puedes dar desde ahora.\n\n"
519
+ "Estas opciones podrían ayudarte:\n"
520
+ f"- {sugerencias[0]}\n"
521
+ f"- {sugerencias[1]}\n"
522
+ f"- {sugerencias[2]}\n\n"
523
+ "También es recomendable que busques acompañamiento en tu plantel para no enfrentar esto en soledad. "
524
+ "Puedes acercarte a tutoría, orientación educativa o psicología.\n\n"
525
+ f"{elegir(preguntas_finales)} "
526
+ "Puedes decirme si esto se relaciona más con la escuela, con tus emociones, con tu familia o con algún problema personal."
527
+ )
528
+
529
+ if intencion == "academico_emocional":
530
+ sugerencias = construir_sugerencias_por_tema("academico", intencion)
531
+ return (
532
+ f"{elegir(aperturas)}\n\n"
533
+ "Entiendo que la carga académica puede llegar a sentirse muy pesada, especialmente cuando se juntan tareas, exámenes, temas difíciles o presión por las calificaciones.\n\n"
534
+ "Eso no significa que no puedas avanzar; muchas veces significa que necesitas apoyo, orden y empezar por una sola parte del problema.\n\n"
535
+ "Podrías intentar lo siguiente:\n"
536
+ f"- {sugerencias[0]}\n"
537
+ f"- {sugerencias[1]}\n"
538
+ f"- {sugerencias[2]}\n\n"
539
+ "También puedo ayudarte a revisar contigo qué te está costando más: una materia, una tarea, una instrucción, un tema específico o simplemente la acumulación de trabajo.\n\n"
540
+ "Cuéntame qué materia o actividad te está preocupando más y avanzamos paso a paso."
541
+ )
542
+
543
+ if intencion == "administrativo":
544
+ sugerencias = construir_sugerencias_por_tema("administrativo", intencion)
545
+ return (
546
+ f"{elegir(aperturas)}\n\n"
547
+ "Puedo orientarte con dudas escolares o administrativas relacionadas con horario, grupo, documentos, boletas, trámites o indicaciones generales del plantel.\n\n"
548
+ "Para darte una respuesta más útil, necesito que me escribas con un poco más de detalle qué es exactamente lo que necesitas resolver.\n\n"
549
+ "Por ejemplo, puedes especificar si tu duda se relaciona con:\n"
550
+ f"- {sugerencias[0]}\n"
551
+ f"- {sugerencias[1]}\n"
552
+ f"- {sugerencias[2]}\n\n"
553
+ "Entre más claro me expliques tu situación, más precisa podrá ser mi orientación."
554
+ )
555
+
556
+ sugerencias = construir_sugerencias_por_tema(tema, intencion)
557
+ return (
558
+ f"{elegir(aperturas)}\n\n"
559
+ "A veces poner en palabras lo que nos pasa ayuda a entender mejor la situación y a encontrar un primer paso para afrontarla. "
560
+ "No necesitas resolver todo de una sola vez.\n\n"
561
+ "Algunas opciones que podrían ayudarte en este momento son:\n"
562
+ f"- {sugerencias[0]}\n"
563
+ f"- {sugerencias[1]}\n"
564
+ f"- {sugerencias[2]}\n\n"
565
+ "Si quieres, puedo seguir acompañándote para identificar qué es lo que más te preocupa y cómo podrías empezar a manejarlo."
566
+ )
567
+
568
+ # =====================================================
569
+ # ANÁLISIS TEXTUAL
570
+ # =====================================================
571
+
572
+ def construir_analisis_textual(frase, resultado):
573
+ if not resultado:
574
+ return "Sin análisis."
575
+
576
+ probs = resultado.get("probabilidades", {})
577
+
578
+ texto = []
579
+ texto.append("ANÁLISIS EMOCIONAL")
580
+ texto.append(f"Frase: {frase}")
581
+ texto.append(f"Clasificación: {resultado.get('clasificacion', '')}")
582
+ texto.append(f"Intención detectada: {resultado.get('intencion', '')}")
583
+ texto.append(f"Tema detectado: {resultado.get('tema', '')}")
584
+ texto.append(f"Etiqueta principal: {resultado.get('label', '')}")
585
+ texto.append(f"Confianza: {float(resultado.get('score', 0.0)):.3f}")
586
+ texto.append(f"Nivel de riesgo: {resultado.get('nivel_riesgo', '')}")
587
+ texto.append(f"Semáforo: {resultado.get('semaforo', '')}")
588
+ texto.append(f"Coincidencias de riesgo: {resultado.get('coincidencias_riesgo', '')}")
589
+ texto.append(f"Tipo de riesgo: {resultado.get('tipo_riesgo', '')}")
590
+ texto.append(f"Ideación suicida: {resultado.get('ideacion_suicida', '')}")
591
+ texto.append(f"Desesperanza: {resultado.get('desesperanza', '')}")
592
+ texto.append(f"Aislamiento: {resultado.get('aislamiento', '')}")
593
+ texto.append(f"Alerta urgente: {resultado.get('alerta_urgente', '')}")
594
+ texto.append("")
595
+ texto.append("Probabilidades:")
596
+ texto.append(f"- NEG: {float(probs.get('NEG', 0.0)):.3f}")
597
+ texto.append(f"- NEU: {float(probs.get('NEU', 0.0)):.3f}")
598
+ texto.append(f"- POS: {float(probs.get('POS', 0.0)):.3f}")
599
+
600
+ return "\n".join(texto)
601
+
602
+ # =====================================================
603
+ # GUARDAR EN GOOGLE SHEETS
604
+ # =====================================================
605
+ def guardar_interaccion(folio_atencion, matricula, nombre, grupo, mensaje, resultado, respuesta):
606
+ try:
607
+ print("ENTRANDO A guardar_interaccion...")
608
+
609
+ sheet = conectar_sheet()
610
+ print("Conexión con Google Sheets correcta")
611
+
612
+ if resultado is None:
613
+ resultado = {}
614
+
615
+ fila = [
616
+ datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
617
+ str(folio_atencion or ""),
618
+ str(matricula or ""),
619
+ str(nombre or ""),
620
+ str(grupo or ""),
621
+ str(mensaje or ""),
622
+ str(resultado.get("clasificacion", "")),
623
+ str(resultado.get("intencion", "")),
624
+ str(resultado.get("tema", "")),
625
+ str(resultado.get("label", "")),
626
+ str(resultado.get("score", "")),
627
+ str(resultado.get("nivel_riesgo", "")),
628
+ str(resultado.get("semaforo", "")),
629
+ str(resultado.get("coincidencias_riesgo", "")),
630
+ str(resultado.get("probabilidades", "")),
631
+ str(resultado.get("tipo_riesgo", "")),
632
+ str(resultado.get("ideacion_suicida", "")),
633
+ str(resultado.get("desesperanza", "")),
634
+ str(resultado.get("aislamiento", "")),
635
+ str(resultado.get("alerta_urgente", "")),
636
+ str(respuesta or "")
637
+ ]
638
+
639
+ # Guarda SIEMPRE al final, sin borrar registros anteriores
640
+ sheet.append_row(
641
+ fila,
642
+ value_input_option="USER_ENTERED",
643
+ insert_data_option="INSERT_ROWS"
644
+ )
645
+
646
+ print("✅ Datos guardados correctamente en Google Sheets")
647
+ return True, "Guardado correcto"
648
+
649
+ except Exception as e:
650
+ error = f"❌ Error al guardar en Google Sheets: {repr(e)}"
651
+ print(error)
652
+ return False, error
653
+ # =====================================================
654
+ # RESPONDER CHAT
655
+ # =====================================================
656
+
657
+ def responder_chat(matricula, nombre, grupo, mensaje, historial):
658
+ mensaje = str(mensaje or "").strip()
659
+ historial = str(historial or "").strip()
660
+
661
+ if not mensaje:
662
+ return historial, "", "Escribe algo.", "Sin análisis"
663
+
664
+ try:
665
+ nombre_mostrar = nombre.strip() if str(nombre or "").strip() else "Estudiante"
666
+ matricula_final = matricula.strip() if str(matricula or "").strip() else "ANONIMO"
667
+ grupo_final = grupo.strip() if str(grupo or "").strip() else ""
668
+ folio_atencion = generar_folio_atencion()
669
+
670
+ # Consulta FOSO II. También se guarda registro.
671
+ if detectar_pregunta_foso2(mensaje):
672
+ respuesta_base = responder_foso2(mensaje)
673
+ respuesta = f"📌 Folio de atención: {folio_atencion}\n\n{respuesta_base}"
674
+ analisis = "Consulta pedagógica detectada: FOSO II"
675
+
676
+ resultado_foso = {
677
+ "clasificacion": "consulta_pedagogica",
678
+ "intencion": "foso_ii",
679
+ "tema": "foso_ii",
680
+ "label": "",
681
+ "score": "",
682
+ "nivel_riesgo": "bajo",
683
+ "semaforo": "🟢",
684
+ "coincidencias_riesgo": "",
685
+ "probabilidades": "",
686
+ "tipo_riesgo": "consulta_pedagogica",
687
+ "ideacion_suicida": "no",
688
+ "desesperanza": "no",
689
+ "aislamiento": "no",
690
+ "alerta_urgente": "no"
691
+ }
692
+
693
+ guardado, mensaje_guardado = guardar_interaccion(
694
+ folio_atencion,
695
+ matricula_final,
696
+ nombre_mostrar,
697
+ grupo_final,
698
+ mensaje,
699
+ resultado_foso,
700
+ respuesta
701
+ )
702
+ print(mensaje_guardado)
703
+
704
+ bloque_usuario = f"Tú: {mensaje}"
705
+ bloque_agente = f"Agente: {respuesta}"
706
+ nuevo_historial = historial + "\n\n" + bloque_usuario + "\n" + bloque_agente if historial else bloque_usuario + "\n" + bloque_agente
707
+
708
+ return nuevo_historial, "", respuesta, analisis
709
+
710
+ resultado = analizar_frase(mensaje)
711
+
712
+ # Gemini redacta la respuesta cuando el riesgo no es alto.
713
+ # Si Gemini falla, el agente usa la respuesta original como respaldo.
714
+ if resultado and resultado.get("nivel_riesgo") == "alto":
715
+ respuesta_base = construir_respuesta_crisis(nombre_mostrar)
716
+ else:
717
+ try:
718
+ respuesta_base = responder_con_gemini(mensaje, nombre_mostrar, resultado)
719
+ if not respuesta_base or str(respuesta_base).startswith("Error de Gemini"):
720
+ respuesta_base = construir_respuesta_amplia(resultado, nombre_mostrar)
721
+ except Exception as e:
722
+ print(f"Error al responder con Gemini: {repr(e)}")
723
+ respuesta_base = construir_respuesta_amplia(resultado, nombre_mostrar)
724
+
725
+ respuesta = f"📌 Folio de atención: {folio_atencion}\n\n{respuesta_base}"
726
+ analisis = construir_analisis_textual(mensaje, resultado)
727
+
728
+ if resultado is not None:
729
+ guardado, mensaje_guardado = guardar_interaccion(
730
+ folio_atencion,
731
+ matricula_final,
732
+ nombre_mostrar,
733
+ grupo_final,
734
+ mensaje,
735
+ resultado,
736
+ respuesta
737
+ )
738
+ print(mensaje_guardado)
739
+ if not guardado:
740
+ analisis += f"\n\nERROR DE GUARDADO:\n{mensaje_guardado}"
741
+
742
+ bloque_usuario = f"Tú: {mensaje}"
743
+ bloque_agente = f"Agente: {respuesta}"
744
+ nuevo_historial = historial + "\n\n" + bloque_usuario + "\n" + bloque_agente if historial else bloque_usuario + "\n" + bloque_agente
745
+
746
+ return nuevo_historial, "", respuesta, analisis
747
+
748
+ except Exception as e:
749
+ print(f"Error interno: {repr(e)}")
750
+ return historial, "", f"Error interno: {repr(e)}", "Sin análisis"
751
+
752
+ # =====================================================
753
+ # LIMPIAR CHAT
754
+ # =====================================================
755
+
756
+ def limpiar_chat():
757
+ return "", "", "", ""
758
+
759
+ # =====================================================
760
+ # RESPUESTA RÁPIDA DE CRISIS DESDE BOTÓN
761
+ # =====================================================
762
+
763
+ def activar_ayuda_urgente(nombre):
764
+ nombre_mostrar = str(nombre or "").strip() or "Estudiante"
765
+ folio_atencion = generar_folio_atencion()
766
+
767
+ respuesta_base = construir_respuesta_crisis(nombre_mostrar)
768
+ respuesta = f"📌 Folio de atención: {folio_atencion}\n\n{respuesta_base}"
769
+
770
+ resultado = {
771
+ "clasificacion": "alto_riesgo",
772
+ "intencion": "emocional",
773
+ "tema": "crisis",
774
+ "label": "NEG",
775
+ "score": 1.0,
776
+ "nivel_riesgo": "alto",
777
+ "semaforo": "🔴",
778
+ "coincidencias_riesgo": "boton_ayuda_urgente",
779
+ "probabilidades": "",
780
+ "tipo_riesgo": "riesgo_suicida_alto",
781
+ "ideacion_suicida": "si",
782
+ "desesperanza": "no",
783
+ "aislamiento": "no",
784
+ "alerta_urgente": "si"
785
+ }
786
+
787
+ guardado, mensaje_guardado = guardar_interaccion(
788
+ folio_atencion,
789
+ "BOTON_URGENCIA",
790
+ nombre_mostrar,
791
+ "",
792
+ "El estudiante presionó el botón de ayuda urgente",
793
+ resultado,
794
+ respuesta
795
+ )
796
+ print(mensaje_guardado)
797
+
798
+ analisis = "ALERTA MANUAL: solicitud de ayuda urgente"
799
+ if not guardado:
800
+ analisis += f"\n\nERROR DE GUARDADO:\n{mensaje_guardado}"
801
+
802
+ return "", respuesta, analisis
803
+
804
+ # =====================================================
805
+ # INTERFAZ VISUAL MEJORADA Y RESPONSIVE
806
+ # =====================================================
807
+
808
+ css = """
809
+ body {
810
+ background: #f5f7f6;
811
+ font-family: Arial, sans-serif;
812
+ color: #111827;
813
+ }
814
+ .gradio-container {
815
+ max-width: 1450px !important;
816
+ padding-left: 12px !important;
817
+ padding-right: 12px !important;
818
+ }
819
+ p, li, span, div, label {
820
+ color: #111827 !important;
821
+ }
822
+ h1, h2, h3, h4 {
823
+ color: #0f172a !important;
824
+ font-weight: 800 !important;
825
+ }
826
+ .bloque {
827
+ background: #ffffff;
828
+ border-radius: 18px;
829
+ padding: 18px;
830
+ box-shadow: 0 2px 10px rgba(0,0,0,0.10);
831
+ margin-bottom: 12px;
832
+ }
833
+ .header-box {
834
+ background: linear-gradient(90deg, #0b6b43, #0f8a57);
835
+ color: white !important;
836
+ border-radius: 22px;
837
+ padding: 24px;
838
+ margin-bottom: 15px;
839
+ box-shadow: 0 4px 14px rgba(0,0,0,0.12);
840
+ }
841
+ .header-box,
842
+ .header-box div,
843
+ .header-box span,
844
+ .header-box b,
845
+ .header-box .titulo,
846
+ .header-box .subtitulo {
847
+ color: white !important;
848
+ }
849
+ .titulo {
850
+ font-size: 34px;
851
+ font-weight: 900;
852
+ margin-bottom: 6px;
853
+ line-height: 1.15;
854
+ }
855
+ .subtitulo {
856
+ font-size: 17px;
857
+ font-weight: 500;
858
+ opacity: 1;
859
+ line-height: 1.4;
860
+ }
861
+ .seguro {
862
+ background: #f8fafc;
863
+ border-radius: 16px;
864
+ padding: 18px;
865
+ font-size: 17px;
866
+ line-height: 1.65;
867
+ margin-bottom: 12px;
868
+ color: #111827 !important;
869
+ border: 1px solid #e5e7eb;
870
+ }
871
+ .estado {
872
+ background: #ecfdf3;
873
+ border-radius: 12px;
874
+ padding: 12px;
875
+ font-weight: 800;
876
+ color: #0b6b43 !important;
877
+ text-align: center;
878
+ margin-top: 10px;
879
+ border: 1px solid #bbf7d0;
880
+ }
881
+ .footer-note {
882
+ font-size: 13px;
883
+ color: #374151 !important;
884
+ margin-top: 8px;
885
+ }
886
+ textarea, input {
887
+ font-size: 16px !important;
888
+ color: #111827 !important;
889
+ font-weight: 500 !important;
890
+ }
891
+ ::placeholder {
892
+ color: #6b7280 !important;
893
+ opacity: 1 !important;
894
+ }
895
+ label {
896
+ font-size: 14px !important;
897
+ font-weight: 700 !important;
898
+ color: #111827 !important;
899
+ }
900
+ button {
901
+ min-height: 44px !important;
902
+ font-size: 15px !important;
903
+ font-weight: 700 !important;
904
+ border-radius: 12px !important;
905
+ }
906
+ textarea[aria-label="Conversación"] {
907
+ min-height: 260px !important;
908
+ color: #111827 !important;
909
+ }
910
+ .header-box img {
911
+ max-width: 70px;
912
+ max-height: 70px;
913
+ }
914
+ @media (max-width: 768px) {
915
+ .gradio-container {
916
+ padding-left: 8px !important;
917
+ padding-right: 8px !important;
918
+ }
919
+ .header-box {
920
+ border-radius: 18px;
921
+ padding: 18px;
922
+ margin-bottom: 12px;
923
+ flex-direction: row;
924
+ align-items: center;
925
+ gap: 10px;
926
+ }
927
+ .header-box img {
928
+ width: 50px !important;
929
+ height: 50px !important;
930
+ }
931
+ .titulo {
932
+ font-size: 23px !important;
933
+ line-height: 1.2;
934
+ font-weight: 900 !important;
935
+ }
936
+ .subtitulo {
937
+ font-size: 15px !important;
938
+ line-height: 1.35;
939
+ font-weight: 500 !important;
940
+ }
941
+ .bloque {
942
+ padding: 14px;
943
+ border-radius: 14px;
944
+ margin-bottom: 10px;
945
+ }
946
+ .bloque h2,
947
+ .bloque h3 {
948
+ font-size: 22px !important;
949
+ font-weight: 900 !important;
950
+ color: #111827 !important;
951
+ }
952
+ .bloque p,
953
+ .bloque li,
954
+ .seguro {
955
+ font-size: 17px !important;
956
+ line-height: 1.5 !important;
957
+ color: #111827 !important;
958
+ font-weight: 500 !important;
959
+ }
960
+ .seguro {
961
+ padding: 14px;
962
+ border-radius: 14px;
963
+ }
964
+ .estado {
965
+ padding: 10px;
966
+ font-size: 15px !important;
967
+ }
968
+ label {
969
+ font-size: 15px !important;
970
+ font-weight: 800 !important;
971
+ }
972
+ button {
973
+ width: 100% !important;
974
+ margin-bottom: 8px !important;
975
+ font-size: 15px !important;
976
+ }
977
+ textarea[aria-label="Conversación"] {
978
+ min-height: 180px !important;
979
+ }
980
+ textarea[aria-label="Análisis emocional"] {
981
+ min-height: 160px !important;
982
+ }
983
+ }
984
+ textarea,
985
+ input {
986
+ background: #ffffff !important;
987
+ color: #111827 !important;
988
+ -webkit-text-fill-color: #111827 !important;
989
+ }
990
+ textarea[aria-label="Conversación"] {
991
+ background: #ffffff !important;
992
+ color: #111827 !important;
993
+ }
994
+ textarea[aria-label="Respuesta del agente"] {
995
+ background: #ffffff !important;
996
+ color: #111827 !important;
997
+ }
998
+ textarea[aria-label="Análisis emocional"] {
999
+ background: #ffffff !important;
1000
+ color: #111827 !important;
1001
+ }
1002
+ html {
1003
+ color-scheme: light !important;
1004
+ }
1005
+ ::placeholder {
1006
+ color: #6b7280 !important;
1007
+ opacity: 1 !important;
1008
+ }
1009
+ """
1010
+
1011
+ tema_visual = gr.themes.Soft(primary_hue="green")
1012
+
1013
+ with gr.Blocks(theme=tema_visual, css=css, title="Agente Conversacional CONALEP") as demo:
1014
+
1015
+ gr.HTML("""
1016
+ <div class="header-box" style="display:flex; align-items:center; gap:14px;">
1017
+ <img src="/gradio-api/file=logo_conalep.png"
1018
+ alt="Logo CONALEP"
1019
+ style="width:70px; height:70px; object-fit:contain; background:white; border-radius:12px; padding:6px;">
1020
+ <div>
1021
+ <div class="titulo">🤖 Agente Conversacional Pedagógico CONALEP</div>
1022
+ <div class="subtitulo">Apoyo socioemocional y detección de riesgos</div>
1023
+ <div style="font-size:12px; margin-top:4px;">
1024
+ <span style="color:white !important;">
1025
+ Desarrollado por <b style="color:white !important;">Doctorante. Jorge Noé Gámez Mora</b>
1026
+ </span>
1027
+ </div>
1028
+ </div>
1029
+ </div>
1030
+ """)
1031
+
1032
+ with gr.Row():
1033
+ with gr.Column(scale=1):
1034
+ gr.HTML("""
1035
+ <div class="bloque">
1036
+ <h2>¿Qué puedo hacer por ti?</h2>
1037
+ <p>✅ Brindarte acompañamiento inicial</p>
1038
+ <p>✅ Detectar señales de riesgo socioemocional</p>
1039
+ <p>✅ Orientarte en temas emocionales y escolares</p>
1040
+ <p>✅ Canalizarte si necesitas ayuda urgente</p>
1041
+ </div>
1042
+ """)
1043
+
1044
+ gr.HTML("""
1045
+ <div class="bloque">
1046
+ <h3>🔒 Espacio confidencial</h3>
1047
+ <p>Este agente no sustituye atención psicológica, médica o de emergencia.</p>
1048
+ <p>
1049
+ Si existe peligro inmediato, llama al
1050
+ <span style="font-weight:900; font-size:18px; color:#d32f2f;">911</span>
1051
+ o a la
1052
+ <span style="font-weight:900; font-size:18px; color:#b71c1c;">Línea de la Vida: 800 911 2000</span>.
1053
+ </p>
1054
+ </div>
1055
+ """)
1056
+
1057
+ matricula = gr.Textbox(
1058
+ label="Matrícula (opcional)",
1059
+ placeholder="Ej. 22012345 o deja vacío si prefieres anonimato"
1060
+ )
1061
+ nombre = gr.Textbox(label="Nombre", placeholder="Escribe tu nombre")
1062
+ grupo = gr.Textbox(label="Grupo", placeholder="Ej. 210")
1063
+
1064
+ with gr.Column(scale=2):
1065
+ gr.HTML("""
1066
+ <div class="bloque">
1067
+ <p style="font-size:19px; font-weight:900; color:#0b6b43; margin-bottom:10px;">
1068
+ 🔒 Hola, este es un espacio seguro para ti.
1069
+ </p>
1070
+ <p style="font-size:16px; line-height:1.6; color:#333;">
1071
+ Puedes hablar conmigo con confianza.<br>
1072
+ Todo lo que compartas será tratado con seriedad, respeto y
1073
+ <b>confidencialidad</b>, conforme al aviso de privacidad institucional.<br><br>
1074
+ Estoy aquí para apoyarte. Si te sientes en riesgo o necesitas ayuda urgente,
1075
+ busca a tu tutor, orientador del bienestar o a un adulto de confianza.<br><br>
1076
+ <span style="font-weight:900; font-size:17px;">No estás solo.</span>
1077
+ </p>
1078
+ </div>
1079
+ """)
1080
+
1081
+ historial = gr.Textbox(
1082
+ label="Conversación",
1083
+ lines=16,
1084
+ interactive=False,
1085
+ placeholder="Aquí aparecerá la conversación..."
1086
+ )
1087
+
1088
+ mensaje = gr.Textbox(
1089
+ label="Mensaje del estudiante",
1090
+ placeholder="Escribe aquí cómo te sientes o qué necesitas..."
1091
+ )
1092
+
1093
+ with gr.Row():
1094
+ enviar = gr.Button("Enviar", variant="primary")
1095
+ limpiar = gr.Button("Limpiar")
1096
+
1097
+ respuesta_box = gr.Textbox(label="Respuesta del agente", lines=10)
1098
+
1099
+ with gr.Column(scale=1):
1100
+ gr.HTML("""
1101
+ <div class="bloque">
1102
+ <h2>Acciones rápidas</h2>
1103
+ <p>📋 Evaluación rápida</p>
1104
+ <p>📚 Recursos de apoyo</p>
1105
+ <p>📞 Contactar tutor u orientador</p>
1106
+ <p>🧠 Acompañamiento emocional inicial</p>
1107
+ </div>
1108
+ """)
1109
+
1110
+ gr.HTML("""
1111
+ <div class="bloque estado">
1112
+ Estado del agente: En línea 🟢
1113
+ </div>
1114
+ """)
1115
+
1116
+ crisis_btn = gr.Button("🚨 ¡Necesito ayuda urgente!", variant="stop")
1117
+ analisis_box = gr.Textbox(label="Análisis emocional", lines=18, visible=False)
1118
+
1119
+ gr.HTML("""
1120
+ <div class="footer-note">
1121
+ Herramienta de apoyo inicial con fines de orientación y detección temprana.
1122
+ </div>
1123
+ """)
1124
+
1125
+ enviar.click(
1126
+ responder_chat,
1127
+ inputs=[matricula, nombre, grupo, mensaje, historial],
1128
+ outputs=[historial, mensaje, respuesta_box, analisis_box]
1129
+ )
1130
+
1131
+ mensaje.submit(
1132
+ responder_chat,
1133
+ inputs=[matricula, nombre, grupo, mensaje, historial],
1134
+ outputs=[historial, mensaje, respuesta_box, analisis_box]
1135
+ )
1136
+
1137
+ limpiar.click(
1138
+ limpiar_chat,
1139
+ inputs=[],
1140
+ outputs=[historial, mensaje, respuesta_box, analisis_box]
1141
+ )
1142
+
1143
+ crisis_btn.click(
1144
+ activar_ayuda_urgente,
1145
+ inputs=[nombre],
1146
+ outputs=[mensaje, respuesta_box, analisis_box]
1147
+ )
1148
+
1149
  if __name__ == "__main__":
1150
+ print("Iniciando app tesis...")
1151
+ demo.launch()
1152
+
1153
+
1154
+ import os
1155
+ import google.generativeai as genai
1156
+
1157
+ # =====================================================
1158
+ # CONFIGURAR GEMINI
1159
+ # =====================================================
1160
+
1161
+ def obtener_modelo_gemini():
1162
+ api_key = os.getenv("GOOGLE_API_KEY")
1163
+
1164
+ if not api_key:
1165
+ raise ValueError("No se encontró GOOGLE_API_KEY en los Secrets de Hugging Face.")
1166
+
1167
+ genai.configure(api_key=api_key)
1168
+ return genai.GenerativeModel("gemini-2.5-flash")
1169
+
1170
+
1171
+ def responder_con_gemini(mensaje, nombre="Estudiante", resultado=None):
1172
+ """
1173
+ Genera una respuesta empática con Gemini.
1174
+ No sustituye la detección de riesgo del app.py; solo redacta la respuesta.
1175
+ """
1176
+
1177
+ resultado = resultado or {}
1178
+
1179
+ nivel_riesgo = resultado.get("nivel_riesgo", "bajo")
1180
+ intencion = resultado.get("intencion", "emocional")
1181
+ tema = resultado.get("tema", "general")
1182
+ semaforo = resultado.get("semaforo", "🟢")
1183
+ tipo_riesgo = resultado.get("tipo_riesgo", "malestar_general_o_sin_indicadores")
1184
+
1185
+ prompt = f"""
1186
+ Eres Jimmy, un agente conversacional pedagógico de CONALEP especializado en acompañamiento socioemocional inicial.
1187
+
1188
+ Tu función es orientar con empatía, claridad y respeto.
1189
+ No sustituyes atención psicológica, médica, psiquiátrica ni de emergencia.
1190
+ No hagas diagnósticos clínicos.
1191
+ No prometas confidencialidad absoluta.
1192
+ Responde en español mexicano, con lenguaje adecuado para estudiantes de bachillerato.
1193
+
1194
+ Datos del análisis automático:
1195
+ - Nombre del estudiante: {nombre}
1196
+ - Nivel de riesgo detectado: {nivel_riesgo}
1197
+ - Semáforo: {semaforo}
1198
+ - Intención: {intencion}
1199
+ - Tema: {tema}
1200
+ - Tipo de riesgo: {tipo_riesgo}
1201
+
1202
+ Mensaje del estudiante:
1203
+ {mensaje}
1204
+
1205
+ Instrucciones para responder:
1206
+ 1. Inicia validando lo que siente el estudiante.
1207
+ 2. Da orientación breve, concreta y humana.
1208
+ 3. Sugiere 2 o 3 pasos prácticos.
1209
+ 4. Recomienda acercarse a tutoría, orientación educativa, psicología escolar o una persona adulta de confianza si el malestar continúa.
1210
+ 5. Si hay peligro inmediato, indica llamar al 911.
1211
+ 6. No excedas 220 palabras.
1212
+ 7. No menciones detalles técnicos como modelos, etiquetas, scores o probabilidades.
1213
+
1214
+ Responde directamente al estudiante.
1215
+ """
1216
+
1217
+ try:
1218
+ modelo = obtener_modelo_gemini()
1219
+ respuesta = modelo.generate_content(prompt)
1220
+
1221
+ if hasattr(respuesta, "text") and respuesta.text:
1222
+ return respuesta.text.strip()
1223
+
1224
+ return "Error de Gemini: No fue posible generar una respuesta."
1225
+
1226
+ except Exception as e:
1227
+ return f"Error de Gemini: {str(e)}"
1228
+