Mahynlo commited on
Commit
4b5bc79
·
1 Parent(s): 9261227

Migración completa a Google Gemini - Agente GAIA con OAuth y validaciones

Browse files
Files changed (14) hide show
  1. .env.example +7 -0
  2. .gitattributes +1 -0
  3. .gitignore +56 -0
  4. DEPLOY.md +179 -0
  5. README.md +67 -5
  6. RESUMEN_IMPLEMENTACION.md +292 -0
  7. agents.py +246 -0
  8. api.py +24 -0
  9. app.py +267 -4
  10. final_answer.py +123 -0
  11. model.py +125 -0
  12. requirements.txt +18 -0
  13. tool.py +80 -0
  14. tools.py +25 -0
.env.example ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ # Google Gemini API Key
2
+ # Obtén tu API key gratis en: https://aistudio.google.com/apikey
3
+ GEMINI_API_KEY=your_gemini_api_key_here
4
+
5
+ # Opcional: HuggingFace Space info (se autoconfiguran en el Space)
6
+ # SPACE_ID=tu-usuario/nombre-del-space
7
+ # SPACE_HOST=nombre-del-space-tu-usuario.hf.space
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ *.env filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ env/
8
+ venv/
9
+ ENV/
10
+ build/
11
+ develop-eggs/
12
+ dist/
13
+ downloads/
14
+ eggs/
15
+ .eggs/
16
+ lib/
17
+ lib64/
18
+ parts/
19
+ sdist/
20
+ var/
21
+ wheels/
22
+ *.egg-info/
23
+ .installed.cfg
24
+ *.egg
25
+
26
+ # Environment variables
27
+ .env
28
+ .env.local
29
+
30
+ # IDEs
31
+ .vscode/
32
+ .idea/
33
+ *.swp
34
+ *.swo
35
+ *~
36
+
37
+ # OS
38
+ .DS_Store
39
+ Thumbs.db
40
+
41
+ # Logs
42
+ *.log
43
+
44
+ # Testing
45
+ .pytest_cache/
46
+ .coverage
47
+ htmlcov/
48
+
49
+ # Gradio
50
+ gradio_cached_examples/
51
+ flagged/
52
+
53
+ # Virtual environment (no subir a HF Spaces)
54
+ .venv/
55
+ venv/
56
+ .gradio
DEPLOY.md ADDED
@@ -0,0 +1,179 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 🚀 Guía de Despliegue en HuggingFace Spaces
2
+
3
+ Esta guía te ayudará a desplegar tu agente GAIA con Gemini en HuggingFace Spaces.
4
+
5
+ ## 📋 Pre-requisitos
6
+
7
+ 1. **Cuenta de HuggingFace**
8
+ - Crea una gratis en: https://huggingface.co/join
9
+
10
+ 2. **API Key de Google Gemini**
11
+ - Obtén una gratis en: https://aistudio.google.com/apikey
12
+ - No requiere tarjeta de crédito
13
+ - Límite generoso de requests gratuitos
14
+
15
+ ## 🎯 Pasos de Despliegue
16
+
17
+ ### 1. Crear el Space
18
+
19
+ 1. Ve a: https://huggingface.co/new-space
20
+ 2. Configura:
21
+ - **Owner**: Tu usuario
22
+ - **Space name**: `gaia-agent-gemini` (o el nombre que prefieras)
23
+ - **License**: MIT
24
+ - **SDK**: Gradio
25
+ - **Hardware**: CPU (gratuito) - suficiente para este proyecto
26
+ - **Visibility**: Public (para el curso)
27
+
28
+ 3. Clic en **Create Space**
29
+
30
+ ### 2. Subir el Código
31
+
32
+ **Opción A: Git (Recomendado)**
33
+
34
+ ```bash
35
+ # Clona tu nuevo Space
36
+ git clone https://huggingface.co/spaces/TU-USUARIO/gaia-agent-gemini
37
+ cd gaia-agent-gemini
38
+
39
+ # Copia todos los archivos del proyecto
40
+ cp -r /ruta/a/tu/proyecto/* .
41
+
42
+ # Sube a HuggingFace
43
+ git add .
44
+ git commit -m "Initial commit - GAIA agent with Gemini"
45
+ git push
46
+ ```
47
+
48
+ **Opción B: Web UI**
49
+
50
+ 1. Abre tu Space en HuggingFace
51
+ 2. Clic en **Files** → **Add file** → **Upload files**
52
+ 3. Arrastra y suelta todos los archivos:
53
+ - `app.py`
54
+ - `agents.py`
55
+ - `model.py`
56
+ - `tools.py`
57
+ - `tool.py`
58
+ - `final_answer.py`
59
+ - `api.py`
60
+ - `requirements.txt`
61
+ - `README.md`
62
+
63
+ ### 3. Configurar la API Key
64
+
65
+ 1. Ve a **Settings** (arriba a la derecha)
66
+ 2. Scroll hasta **Repository secrets**
67
+ 3. Clic en **New secret**
68
+ 4. Agrega:
69
+ - **Name**: `GEMINI_API_KEY`
70
+ - **Value**: Tu API key de Google (ej: `AIzaSy...`)
71
+ 5. Clic en **Add**
72
+
73
+ ### 4. Verificar el Despliegue
74
+
75
+ El Space se construirá automáticamente. Espera 2-3 minutos.
76
+
77
+ Verás:
78
+ - ⚙️ **Building** → Instalando dependencias
79
+ - 🟢 **Running** → ¡Listo para usar!
80
+
81
+ Si hay errores, revisa los **Logs** en la pestaña **Logs**.
82
+
83
+ ### 5. Probar el Agente
84
+
85
+ 1. Abre tu Space (URL: `https://huggingface.co/spaces/TU-USUARIO/gaia-agent-gemini`)
86
+ 2. Clic en **Sign in with Hugging Face**
87
+ 3. Autoriza el acceso
88
+ 4. Clic en **▶️ Ejecutar Evaluación**
89
+ 5. Observa los resultados en la tabla
90
+
91
+ ## 🔧 Configuración Avanzada
92
+
93
+ ### Cambiar el Modelo Gemini
94
+
95
+ Edita `app.py` línea 15:
96
+
97
+ ```python
98
+ # Opciones disponibles:
99
+ MODEL_ID = "gemini/gemini-2.0-flash-exp" # Rápido (recomendado)
100
+ MODEL_ID = "gemini/gemini-1.5-flash" # Estable
101
+ MODEL_ID = "gemini/gemini-1.5-pro" # Más potente (más lento)
102
+ ```
103
+
104
+ ### Limitar Número de Preguntas (Testing)
105
+
106
+ Edita `app.py`, descomenta la línea ~71:
107
+
108
+ ```python
109
+ # questions_data = questions_data[:5] # Solo 5 preguntas para testing
110
+ ```
111
+
112
+ ### Habilitar Debug Verbose
113
+
114
+ Edita `agents.py`, cambia línea ~83:
115
+
116
+ ```python
117
+ agent = create_agent(model_id=MODEL_ID, verbose=True) # Ya está activado
118
+ ```
119
+
120
+ ## 🐛 Solución de Problemas
121
+
122
+ ### Error: "GEMINI_API_KEY no configurada"
123
+
124
+ **Solución**: Verifica que agregaste el secret correctamente en Settings → Repository secrets.
125
+
126
+ ### Error: "Import gradio could not be resolved"
127
+
128
+ **Solución**: El Space se está construyendo. Espera hasta que termine (estado "Running").
129
+
130
+ ### Error: "Rate limit exceeded"
131
+
132
+ **Solución**: Gemini tiene límites gratuitos (60 requests/min). El código ya incluye retry logic. Si falla, espera 1 minuto y vuelve a intentar.
133
+
134
+ ### Error: "SPACE_ID no configurado"
135
+
136
+ **Solución**: Este error solo ocurre en ejecución local. En HuggingFace Spaces, esta variable se configura automáticamente.
137
+
138
+ ## 📊 Evaluar Resultados
139
+
140
+ Después de ejecutar, verás:
141
+
142
+ - **Puntuación**: % de respuestas correctas
143
+ - **Correctas**: N de M preguntas
144
+ - **Tabla**: Todas las preguntas y respuestas
145
+
146
+ Compara tu puntuación en el [leaderboard del curso](https://huggingface.co/spaces/agents-course-unit4-scoring).
147
+
148
+ ## 🔄 Actualizar el Código
149
+
150
+ Si haces cambios localmente:
151
+
152
+ ```bash
153
+ cd gaia-agent-gemini
154
+ # Edita archivos...
155
+ git add .
156
+ git commit -m "Descripción del cambio"
157
+ git push
158
+ ```
159
+
160
+ El Space se reconstruirá automáticamente.
161
+
162
+ ## 🎓 Recursos
163
+
164
+ - [Documentación de Spaces](https://huggingface.co/docs/hub/spaces)
165
+ - [Gradio OAuth Docs](https://www.gradio.app/guides/sharing-your-app#o-auth-login-via-hugging-face)
166
+ - [Google Gemini Docs](https://ai.google.dev/gemini-api/docs)
167
+ - [LiteLLM Gemini Guide](https://docs.litellm.ai/docs/providers/gemini)
168
+
169
+ ## 💬 Soporte
170
+
171
+ Si tienes problemas:
172
+
173
+ 1. Revisa los **Logs** en tu Space
174
+ 2. Consulta este README.md y DEPLOY.md
175
+ 3. Pregunta en el [foro del curso](https://huggingface.co/learn/agents-course)
176
+
177
+ ---
178
+
179
+ ¡Buena suerte con tu agente! 🚀
README.md CHANGED
@@ -1,14 +1,76 @@
1
  ---
2
- title: Hf Gaia Agents Course MN
3
- emoji: 📊
4
- colorFrom: green
5
- colorTo: purple
6
  sdk: gradio
7
  sdk_version: 5.49.1
8
  app_file: app.py
9
  pinned: false
10
  license: mit
11
- short_description: 'Agente AI para responder preguntas del benchmark GAIA Level '
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  ---
13
 
14
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
+ title: GAIA Agent - Gemini
3
+ emoji: 🤖
4
+ colorFrom: blue
5
+ colorTo: indigo
6
  sdk: gradio
7
  sdk_version: 5.49.1
8
  app_file: app.py
9
  pinned: false
10
  license: mit
11
+ short_description: Agente AI usando Google Gemini para GAIA Level 1 (HF Course Unit 4)
12
+ hf_oauth: true
13
+ hf_oauth_expiration_minutes: 480
14
+ ---
15
+
16
+ # 🤖 Agente GAIA con Google Gemini
17
+
18
+ Agente AI que usa **Google Gemini** para resolver tareas del benchmark GAIA Level 1.
19
+
20
+ Proyecto para el [Curso de Agentes de HuggingFace - Unit 4 Hands-on](https://huggingface.co/learn/agents-course/unit4/hands-on).
21
+
22
+ ## 🚀 Características
23
+
24
+ - ✅ **Google Gemini 2.0 Flash** - Rápido y gratuito
25
+ - ✅ **OAuth de HuggingFace** - Autenticación segura
26
+ - ✅ **OCR con Tesseract** - Procesamiento de imágenes
27
+ - ✅ **Validación GAIA** - Formato estricto de respuestas
28
+ - ✅ **Retry Logic** - Manejo robusto de rate limits
29
+
30
+ ## 📦 Configuración
31
+
32
+ 1. **Clona este Space**
33
+ 2. **Configura la API key** en Settings → Repository secrets:
34
+ - Nombre: `GEMINI_API_KEY`
35
+ - Obtén una gratis en: https://aistudio.google.com/apikey
36
+ 3. **Inicia sesión** con tu cuenta de HuggingFace
37
+ 4. **Ejecuta** la evaluación
38
+
39
+ ## 🏗️ Arquitectura
40
+
41
+ ```
42
+ app.py → Interfaz Gradio con OAuth
43
+ agents.py → Clase Agent principal
44
+ model.py → Wrapper de Gemini con LiteLLM
45
+ tools.py → OCR y procesamiento de archivos
46
+ tool.py → Registro de herramientas
47
+ final_answer.py → Validaciones de formato GAIA
48
+ api.py → Cliente HTTP para API del curso
49
+ ```
50
+
51
+ ## 🎯 Flujo de Trabajo
52
+
53
+ 1. Usuario inicia sesión con OAuth
54
+ 2. Sistema obtiene preguntas desde la API del curso
55
+ 3. Agente procesa cada pregunta con Gemini
56
+ 4. Sistema valida formato de respuestas (GAIA strict)
57
+ 5. Respuestas se envían a la API para evaluación
58
+ 6. Resultados se muestran en tabla Gradio
59
+
60
+ ## 💡 ¿Por qué Gemini?
61
+
62
+ - **Gratis** con límites generosos (gratuity API tier)
63
+ - **Rápido** - Gemini Flash optimizado para baja latencia
64
+ - **Multimodal** - Procesa texto e imágenes
65
+ - **Sin tarjeta** - No requiere método de pago
66
+
67
+ ## 📚 Recursos
68
+
69
+ - [Curso de Agentes HF](https://huggingface.co/learn/agents-course)
70
+ - [GAIA Benchmark](https://huggingface.co/gaia-benchmark)
71
+ - [Google AI Studio](https://aistudio.google.com/apikey)
72
+ - [LiteLLM Docs](https://docs.litellm.ai/)
73
+
74
  ---
75
 
76
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
RESUMEN_IMPLEMENTACION.md ADDED
@@ -0,0 +1,292 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 📝 Resumen de Implementación - Agente GAIA con Gemini
2
+
3
+ ## 🎯 Objetivo
4
+
5
+ Crear un agente AI que resuelva tareas del benchmark GAIA Level 1 usando **Google Gemini** en lugar de OpenAI, para el curso de Agentes de HuggingFace (Unit 4 Hands-on).
6
+
7
+ ## 🏗️ Arquitectura del Sistema
8
+
9
+ ```
10
+ ┌─────────────────────────────────────────────────────────────┐
11
+ │ app.py (Gradio UI) │
12
+ │ - OAuth Login │
13
+ │ - Botón ejecutar evaluación │
14
+ │ - Tabla de resultados │
15
+ └─────────────────────┬───────────────────────────────────────┘
16
+
17
+
18
+ ┌─────────────────────────────────────────────────────────────┐
19
+ │ agents.py (Agent Class) │
20
+ │ - System prompt optimizado para GAIA │
21
+ │ - Construcción de contexto (pregunta + archivos) │
22
+ │ - Limpieza de respuestas (extracción de FINAL ANSWER) │
23
+ └─────────────────────┬───────────────────────────────────────┘
24
+
25
+ ┌─────────────┼─────────────┐
26
+ ▼ ▼ ▼
27
+ ┌──────────────┐ ┌──────────┐ ┌──────────────────┐
28
+ │ model.py │ │ tool.py │ │ final_answer.py │
29
+ │ │ │ │ │ │
30
+ │ GeminiModel │ │ Tools │ │ Validaciones │
31
+ │ - LiteLLM │ │ Registry │ │ GAIA │
32
+ │ - Retry │ │ │ │ - Números │
33
+ │ - Rate Limit │ │ get_ │ │ - Strings │
34
+ │ Handling │ │ tools() │ │ - Listas │
35
+ └──────┬───────┘ └─────┬────┘ └──────────────────┘
36
+ │ │
37
+ └───────┬───────┘
38
+
39
+ ┌───────────────────────┐
40
+ │ tools.py │
41
+ │ │
42
+ │ - read_image_text() │
43
+ │ (OCR con Tesseract) │
44
+ │ - BytesIO handling │
45
+ └───────────────────────┘
46
+
47
+
48
+ ┌───────────────────────┐
49
+ │ api.py │
50
+ │ │
51
+ │ - get_questions() │
52
+ │ - submit_answers() │
53
+ └───────────────────────┘
54
+ ```
55
+
56
+ ## 📦 Componentes Creados
57
+
58
+ ### 1. `model.py` - Wrapper de Gemini
59
+
60
+ **Propósito**: Abstracción para llamar a Google Gemini con manejo robusto de errores.
61
+
62
+ **Características**:
63
+ - Usa **LiteLLM** para interfaz unificada
64
+ - Retry logic con backoff exponencial
65
+ - Extrae tiempo de espera de mensajes de error de rate limit
66
+ - Cache con `@lru_cache` para la instancia del modelo
67
+
68
+ **Modelo usado**: `gemini/gemini-2.0-flash-exp`
69
+ - Rápido (baja latencia)
70
+ - Gratuito con límites generosos
71
+ - Última versión experimental
72
+
73
+ ### 2. `agents.py` - Clase Agent Principal
74
+
75
+ **Propósito**: Orquesta la resolución de preguntas GAIA.
76
+
77
+ **System Prompt**:
78
+ - Optimizado para benchmark GAIA
79
+ - Énfasis en precisión y formato estricto
80
+ - Instrucciones de "FINAL ANSWER:" obligatorio
81
+
82
+ **Métodos clave**:
83
+ - `_build_context()`: Construye el prompt con pregunta y archivos
84
+ - `_clean_answer()`: Extrae respuesta final con regex
85
+ - `_is_reversed_text()`: Detecta texto al revés (common GAIA trick)
86
+
87
+ ### 3. `tools.py` - Procesamiento de Archivos
88
+
89
+ **Propósito**: OCR de imágenes y manejo de archivos.
90
+
91
+ **Fix aplicado**:
92
+ ```python
93
+ from io import BytesIO # Faltaba esta importación
94
+ ```
95
+
96
+ **Función principal**:
97
+ - `read_image_text(url)`: Descarga imagen y extrae texto con Tesseract
98
+
99
+ ### 4. `tool.py` - Registro de Herramientas
100
+
101
+ **Propósito**: Proporcionar herramientas disponibles al agente.
102
+
103
+ **Herramientas**:
104
+ 1. `file_from_url`: Descarga archivos
105
+ 2. `read_text_file`: Lee archivos de texto
106
+ 3. `read_image_text`: OCR de imágenes (via tools.py)
107
+
108
+ ### 5. `final_answer.py` - Validaciones GAIA
109
+
110
+ **Propósito**: Asegurar formato correcto según reglas GAIA.
111
+
112
+ **Validaciones**:
113
+ - `validate_number_format()`: Sin comas, sin unidades
114
+ - `validate_string_format()`: Sin artículos ("the", "a")
115
+ - `validate_list_format()`: Sin corchetes, comas como separadores
116
+ - `clean_answer()`: Remueve prefijos comunes
117
+
118
+ ### 6. `app.py` - Interfaz Gradio
119
+
120
+ **Propósito**: UI web con OAuth y evaluación automática.
121
+
122
+ **Características**:
123
+ - **OAuth de HuggingFace**: Login obligatorio
124
+ - **Flujo completo**:
125
+ 1. Login usuario
126
+ 2. Obtener preguntas desde API
127
+ 3. Procesar con agente Gemini
128
+ 4. Enviar respuestas
129
+ 5. Mostrar resultados
130
+ - **Manejo de errores**: Try-except en cada paso
131
+ - **Logging**: Prints informativos para debugging
132
+
133
+ ### 7. `requirements.txt` - Dependencias
134
+
135
+ ```
136
+ gradio>=5.0.0 # Web UI
137
+ litellm>=1.0.0 # Wrapper LLM unificado
138
+ google-generativeai>=0.8.0 # Gemini SDK
139
+ pandas>=2.0.0 # DataFrames
140
+ requests>=2.31.0 # HTTP
141
+ pillow>=10.0.0 # Imágenes
142
+ pytesseract>=0.3.10 # OCR
143
+ python-dotenv>=1.0.0 # .env files
144
+ ```
145
+
146
+ ### 8. `README.md` - Documentación
147
+
148
+ **Metadatos importantes**:
149
+ ```yaml
150
+ hf_oauth: true # Habilita OAuth
151
+ hf_oauth_expiration_minutes: 480 # 8 horas
152
+ sdk: gradio
153
+ sdk_version: 5.49.1
154
+ ```
155
+
156
+ ## 🔑 Cambios Clave vs OpenAI
157
+
158
+ | Aspecto | OpenAI (antes) | Gemini (ahora) |
159
+ |---------|---------------|----------------|
160
+ | **Modelo** | `gpt-4o-mini` | `gemini-2.0-flash-exp` |
161
+ | **SDK** | `openai` | `litellm` + `google-generativeai` |
162
+ | **API Key** | `OPENAI_API_KEY` | `GEMINI_API_KEY` |
163
+ | **Costo** | Pago ($) | Gratis (con límites) |
164
+ | **Inicialización** | `OpenAI(api_key=...)` | `litellm.completion(model="gemini/...")` |
165
+ | **Rate Limits** | Más altos (pago) | 60 req/min (gratis) |
166
+
167
+ ## 🚀 Flujo de Ejecución
168
+
169
+ 1. **Usuario accede al Space**
170
+ - Ve interfaz Gradio
171
+ - Login con HuggingFace OAuth
172
+
173
+ 2. **Usuario ejecuta evaluación**
174
+ - Clic en botón "Ejecutar"
175
+ - `run_and_submit_all()` se ejecuta
176
+
177
+ 3. **Sistema obtiene preguntas**
178
+ - GET request a API del curso
179
+ - Recibe lista de preguntas GAIA Level 1
180
+
181
+ 4. **Sistema procesa cada pregunta**
182
+ - `process_question()` para cada una
183
+ - Agent construye prompt
184
+ - Gemini genera respuesta
185
+ - Validaciones GAIA aplicadas
186
+ - Extracción de "FINAL ANSWER:"
187
+
188
+ 5. **Sistema envía respuestas**
189
+ - POST request a API del curso
190
+ - Payload: username, agent_code, answers
191
+
192
+ 6. **Sistema muestra resultados**
193
+ - Score, correctas/total
194
+ - Tabla con todas las preguntas y respuestas
195
+
196
+ ## 🛡️ Manejo de Errores
197
+
198
+ ### Rate Limits
199
+ ```python
200
+ if "quota exceeded" in error_msg or "rate limit" in error_msg:
201
+ # Extrae tiempo de espera del mensaje
202
+ match = re.search(r'try again in (\d+)s', error_msg)
203
+ wait_seconds = int(match.group(1)) if match else 60
204
+ time.sleep(wait_seconds + 1)
205
+ # Reintenta...
206
+ ```
207
+
208
+ ### Errores de Red
209
+ - Timeout de 30s en requests
210
+ - Try-except en cada llamada HTTP
211
+ - Mensajes de error informativos al usuario
212
+
213
+ ### Errores de Gemini
214
+ - Retry automático hasta 3 veces
215
+ - Backoff exponencial (2^attempt segundos)
216
+ - Logging de errores para debugging
217
+
218
+ ## 📊 Formato GAIA
219
+
220
+ El agente cumple con las **reglas estrictas de GAIA**:
221
+
222
+ - **Números**: Sin comas, sin unidades
223
+ - ❌ `1,234 meters`
224
+ - ✅ `1234`
225
+
226
+ - **Strings**: Sin artículos
227
+ - ❌ `The Eiffel Tower`
228
+ - ✅ `Eiffel Tower`
229
+
230
+ - **Listas**: Comas sin espacios, sin corchetes
231
+ - ❌ `[apple, banana]`
232
+ - ✅ `apple,banana`
233
+
234
+ ## 🔧 Configuración Requerida
235
+
236
+ ### En HuggingFace Spaces
237
+
238
+ 1. **Settings → Repository secrets**:
239
+ - `GEMINI_API_KEY`: Tu API key de Google
240
+
241
+ 2. **README.md** (metadata):
242
+ - `hf_oauth: true`
243
+ - `sdk: gradio`
244
+
245
+ ### Variables de Entorno Automáticas
246
+
247
+ HF Spaces configura automáticamente:
248
+ - `SPACE_ID`: `usuario/nombre-space`
249
+ - `SPACE_HOST`: `nombre-space-usuario.hf.space`
250
+
251
+ Usadas para construir el link del código del agente.
252
+
253
+ ## ✅ Testing Local
254
+
255
+ ```bash
256
+ # 1. Instalar dependencias
257
+ pip install -r requirements.txt
258
+
259
+ # 2. Configurar API key
260
+ export GEMINI_API_KEY="tu_api_key_aqui"
261
+
262
+ # 3. Ejecutar app
263
+ python app.py
264
+
265
+ # 4. Abrir navegador
266
+ # http://localhost:7860
267
+ ```
268
+
269
+ **Nota**: OAuth solo funciona en HF Spaces. En local, el perfil será `None`.
270
+
271
+ ## 📈 Próximas Mejoras
272
+
273
+ - [ ] Soporte para GAIA Level 2 y 3
274
+ - [ ] Cache de respuestas procesadas
275
+ - [ ] Métricas de tiempo de respuesta
276
+ - [ ] Soporte para más LLMs (Claude, Mistral, etc.)
277
+ - [ ] Logs persistentes en archivo
278
+ - [ ] Tests unitarios
279
+
280
+ ## 🎓 Referencias
281
+
282
+ - [GAIA Benchmark](https://huggingface.co/gaia-benchmark)
283
+ - [Curso de Agentes HF](https://huggingface.co/learn/agents-course/unit4/hands-on)
284
+ - [Google Gemini Docs](https://ai.google.dev/gemini-api/docs)
285
+ - [LiteLLM Docs](https://docs.litellm.ai/)
286
+ - [Gradio OAuth](https://www.gradio.app/guides/sharing-your-app#o-auth-login-via-hugging-face)
287
+
288
+ ---
289
+
290
+ **Implementado**: Diciembre 2024
291
+ **Stack**: Python 3.10+, Gradio 5.x, Google Gemini, LiteLLM
292
+ **Licencia**: MIT
agents.py ADDED
@@ -0,0 +1,246 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Agent class para resolver tareas GAIA usando Gemini.
3
+ """
4
+
5
+ import re
6
+ from typing import Optional, List, Any
7
+ from model import GeminiModel
8
+
9
+
10
+ class Agent:
11
+ """
12
+ Agente para resolver tareas del benchmark GAIA usando Google Gemini.
13
+ """
14
+
15
+ def __init__(
16
+ self,
17
+ model: GeminiModel,
18
+ tools: Optional[List[Any]] = None,
19
+ verbose: bool = False
20
+ ):
21
+ """
22
+ Inicializa el agente.
23
+
24
+ Args:
25
+ model: Modelo Gemini a usar
26
+ tools: Lista de herramientas disponibles (opcional)
27
+ verbose: Si True, imprime información de debug
28
+ """
29
+ self.model = model
30
+ self.tools = tools or []
31
+ self.verbose = verbose
32
+
33
+ # Prompt optimizado para GAIA benchmark
34
+ self.system_prompt = """You are an expert AI assistant specialized in solving GAIA benchmark tasks with precision.
35
+
36
+ CRITICAL FORMATTING RULES (EXACT MATCHING REQUIRED):
37
+
38
+ 1. NUMBERS:
39
+ - Write as plain digits: 42 (not 42.0 or 42,000)
40
+ - NO commas in numbers: 1000000 (not 1,000,000)
41
+ - NO units unless explicitly requested: 42 (not $42 or 42%)
42
+ - Use Arabic numerals: 9 (not nine)
43
+
44
+ 2. STRINGS:
45
+ - Lowercase preferred: paris (not Paris)
46
+ - NO articles: paris (not "the paris" or "a paris")
47
+ - NO abbreviations: san francisco (not SF or S.F.)
48
+ - Write digits in plain text unless specified
49
+
50
+ 3. LISTS:
51
+ - Comma-separated: apple,orange,banana
52
+ - NO brackets: apple,orange (not [apple,orange])
53
+ - NO quotes: apple,orange (not "apple","orange")
54
+
55
+ 4. CURRENCY (only if explicitly requested):
56
+ - Use symbol: $40.00
57
+ - Follow requested format exactly
58
+
59
+ 5. DATES:
60
+ - Follow exact format requested in question
61
+
62
+ YOUR RESPONSE STRUCTURE:
63
+ 1. Analyze the question carefully
64
+ 2. If files/images are mentioned, describe what you observe
65
+ 3. Think step by step
66
+ 4. Provide reasoning
67
+ 5. End with: FINAL ANSWER: [exact answer only]
68
+
69
+ IMPORTANT: Only text after "FINAL ANSWER:" is submitted. Be EXACT - GAIA uses strict string matching!"""
70
+
71
+ def __call__(self, question: str, files: Optional[List[str]] = None) -> str:
72
+ """
73
+ Interfaz principal para resolver una pregunta.
74
+
75
+ Args:
76
+ question: La pregunta a responder
77
+ files: Lista opcional de rutas de archivos asociados
78
+
79
+ Returns:
80
+ str: La respuesta limpia y formateada
81
+ """
82
+ if self.verbose:
83
+ print(f"\n{'='*60}")
84
+ print(f"📋 Pregunta: {question[:100]}...")
85
+ if files:
86
+ print(f"📎 Archivos: {files}")
87
+
88
+ answer = self.answer_question(question, files)
89
+
90
+ if self.verbose:
91
+ print(f"✅ Respuesta: {answer}")
92
+ print(f"{'='*60}\n")
93
+
94
+ return answer
95
+
96
+ def answer_question(self, question: str, files: Optional[List[str]] = None) -> str:
97
+ """
98
+ Procesa la pregunta y genera una respuesta.
99
+
100
+ Args:
101
+ question: La pregunta a responder
102
+ files: Lista opcional de archivos
103
+
104
+ Returns:
105
+ str: Respuesta limpia
106
+ """
107
+ try:
108
+ # Construir contexto
109
+ context = self._build_context(question, files)
110
+
111
+ # Construir prompt completo
112
+ full_prompt = f"{self.system_prompt}\n\n{context}"
113
+
114
+ if self.verbose:
115
+ print(f"🤖 Llamando a Gemini...")
116
+
117
+ # Llamar al modelo con temperatura baja para precisión
118
+ response = self.model.generate_simple(
119
+ full_prompt,
120
+ temperature=0.1,
121
+ max_tokens=1000
122
+ )
123
+
124
+ # Limpiar y formatear respuesta
125
+ clean = self._clean_answer(response)
126
+
127
+ return clean
128
+
129
+ except Exception as e:
130
+ error_msg = f"ERROR: {str(e)}"
131
+ print(f"❌ {error_msg}")
132
+ return error_msg
133
+
134
+ def _build_context(self, question: str, files: Optional[List[str]] = None) -> str:
135
+ """
136
+ Construye el contexto para el prompt.
137
+
138
+ Args:
139
+ question: La pregunta
140
+ files: Lista opcional de archivos
141
+
142
+ Returns:
143
+ str: Contexto formateado
144
+ """
145
+ context_parts = [f"TASK: {question}"]
146
+
147
+ # Añadir información sobre archivos si existen
148
+ if files:
149
+ context_parts.append("\nFILES PROVIDED:")
150
+ for file_path in files:
151
+ context_parts.append(f"- {file_path}")
152
+ context_parts.append("\nAnalyze these files if relevant to answering the question.")
153
+
154
+ # Detectar texto invertido (reversed text) - común en GAIA
155
+ if self._is_reversed_text(question):
156
+ reversed_q = question[::-1]
157
+ context_parts.append(f"\n⚠️ REVERSED TEXT DETECTED!")
158
+ context_parts.append(f"Original text: {question}")
159
+ context_parts.append(f"Actual question: {reversed_q}")
160
+ context_parts.append("Answer the reversed version in NORMAL text.")
161
+
162
+ return "\n".join(context_parts)
163
+
164
+ def _is_reversed_text(self, text: str) -> bool:
165
+ """
166
+ Detecta si el texto está invertido.
167
+
168
+ Args:
169
+ text: Texto a analizar
170
+
171
+ Returns:
172
+ bool: True si parece estar invertido
173
+ """
174
+ # Heurística: texto invertido suele empezar con "." o contener patrones invertidos
175
+ indicators = [
176
+ text.strip().startswith("."),
177
+ "?rewsna" in text.lower(),
178
+ "?noitseuq" in text.lower(),
179
+ ".rewsna eht sa" in text.lower()
180
+ ]
181
+ return any(indicators)
182
+
183
+ def _clean_answer(self, response: str) -> str:
184
+ """
185
+ Limpia y formatea la respuesta según reglas GAIA.
186
+
187
+ Args:
188
+ response: Respuesta cruda del modelo
189
+
190
+ Returns:
191
+ str: Respuesta limpia
192
+ """
193
+ # Extraer respuesta final si hay marcador "FINAL ANSWER:"
194
+ if "FINAL ANSWER:" in response.upper():
195
+ # Buscar case-insensitive
196
+ parts = re.split(r'FINAL ANSWER:', response, flags=re.IGNORECASE)
197
+ if len(parts) > 1:
198
+ response = parts[-1].strip()
199
+
200
+ # Remover prefijos comunes
201
+ prefixes = [
202
+ "The answer is:", "Answer:", "Final Answer:",
203
+ "The final answer is:", "=>", "Result:",
204
+ "Output:", "Solution:"
205
+ ]
206
+ for prefix in prefixes:
207
+ if response.lower().startswith(prefix.lower()):
208
+ response = response[len(prefix):].strip()
209
+
210
+ # Limpiar comillas y espacios
211
+ response = response.strip(" '\"")
212
+
213
+ # Remover punto final si no es parte de la respuesta
214
+ # (solo si la respuesta es larga o contiene espacios)
215
+ if response.endswith("."):
216
+ # No remover si parece un decimal o número con punto
217
+ if not response.replace(".", "").replace(",", "").replace(" ", "").isdigit():
218
+ # Solo remover si hay espacios o es muy larga
219
+ if " " in response or len(response) > 20:
220
+ response = response.rstrip(".")
221
+
222
+ # Manejar respuestas invertidas - invertir de vuelta
223
+ if self._is_reversed_text(response):
224
+ response = response[::-1]
225
+
226
+ # Remover corchetes de listas si existen
227
+ response = response.strip("[]")
228
+
229
+ return response.strip()
230
+
231
+
232
+ def create_agent(model_id: str = "gemini/gemini-2.0-flash-exp", verbose: bool = False, **kwargs) -> Agent:
233
+ """
234
+ Factory function para crear un agente con Gemini.
235
+
236
+ Args:
237
+ model_id: ID del modelo Gemini a usar
238
+ verbose: Si True, imprime información de debug
239
+ **kwargs: Argumentos adicionales para el modelo
240
+
241
+ Returns:
242
+ Agent: Instancia del agente configurado
243
+ """
244
+ from model import get_model
245
+ model = get_model(model_id, **kwargs)
246
+ return Agent(model=model, verbose=verbose)
api.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import requests
2
+
3
+ BASE_URL = "https://huggingface.co/learn/agents-course/unit4/api"
4
+
5
+ def get_random_question():
6
+ res = requests.get(f"{BASE_URL}/random-question")
7
+ res.raise_for_status()
8
+ return res.json()
9
+
10
+ def get_question_files(task_id):
11
+ res = requests.get(f"{BASE_URL}/files/{task_id}")
12
+ res.raise_for_status()
13
+ return res.json()
14
+
15
+ def submit_answers(username, space_url, answers):
16
+ res = requests.post(
17
+ f"{BASE_URL}/submit",
18
+ json={
19
+ "username": username,
20
+ "space_link": space_url,
21
+ "submissions": answers
22
+ }
23
+ )
24
+ return res.json()
app.py CHANGED
@@ -1,7 +1,270 @@
 
 
 
 
 
 
1
  import gradio as gr
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
- def greet(name):
4
- return "Hello " + name + "!!"
5
 
6
- demo = gr.Interface(fn=greet, inputs="text", outputs="text")
7
- demo.launch()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Gradio Space para agente GAIA usando Google Gemini.
3
+ Integración con API del curso y OAuth de HuggingFace.
4
+ """
5
+
6
+ import os
7
  import gradio as gr
8
+ import requests
9
+ import pandas as pd
10
+ from typing import List, Dict
11
+
12
+ # Imports del proyecto
13
+ from agents import create_agent
14
+ from api import get_random_question, submit_answers
15
+
16
+ # Constantes
17
+ DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
18
+ MODEL_ID = "gemini/gemini-2.0-flash-exp" # Gemini Flash - rápido y gratis
19
+
20
+
21
+ def process_question(agent, question: str, task_id: str, files: List[str] = None) -> Dict:
22
+ """Procesa una pregunta individual."""
23
+ try:
24
+ answer = agent(question, files)
25
+ return {
26
+ "submission": {"task_id": task_id, "submitted_answer": answer},
27
+ "log": {
28
+ "Task ID": task_id,
29
+ "Question": question[:100] + "...",
30
+ "Submitted Answer": answer
31
+ }
32
+ }
33
+ except Exception as e:
34
+ error_msg = f"ERROR: {str(e)}"
35
+ return {
36
+ "submission": {"task_id": task_id, "submitted_answer": error_msg},
37
+ "log": {
38
+ "Task ID": task_id,
39
+ "Question": question[:100] + "...",
40
+ "Submitted Answer": error_msg
41
+ }
42
+ }
43
+
44
+
45
+ def run_and_submit_all(profile: gr.OAuthProfile | None):
46
+ """
47
+ Obtiene preguntas, ejecuta el agente y envía respuestas.
48
+ """
49
+ # Verificar login
50
+ if not profile:
51
+ return "⚠️ Por favor inicia sesión con Hugging Face usando el botón de arriba.", None
52
+
53
+ username = profile.username
54
+ print(f"✅ Usuario logueado: {username}")
55
+
56
+ # Obtener SPACE_ID para el link al código
57
+ space_id = os.getenv("SPACE_ID")
58
+ if not space_id:
59
+ return "⚠️ SPACE_ID no configurado. Debe desplegarse en HuggingFace Space.", None
60
+
61
+ agent_code = f"https://huggingface.co/spaces/{space_id}/tree/main"
62
+ print(f"📦 Código: {agent_code}")
63
+
64
+ # URLs de la API
65
+ questions_url = f"{DEFAULT_API_URL}/questions"
66
+ submit_url = f"{DEFAULT_API_URL}/submit"
67
+
68
+ # 1. Crear agente con Gemini
69
+ try:
70
+ print("🤖 Inicializando agente Gemini...")
71
+ agent = create_agent(model_id=MODEL_ID, verbose=True)
72
+ except Exception as e:
73
+ error_msg = f"❌ Error al inicializar agente: {str(e)}\n"
74
+ error_msg += "Asegúrate de configurar GEMINI_API_KEY en Settings → Repository secrets"
75
+ print(error_msg)
76
+ return error_msg, None
77
+
78
+ # 2. Obtener preguntas
79
+ try:
80
+ print(f"📥 Obteniendo preguntas desde: {questions_url}")
81
+ response = requests.get(questions_url, timeout=15)
82
+ response.raise_for_status()
83
+ questions_data = response.json()
84
+
85
+ if not questions_data:
86
+ return "⚠️ No se recibieron preguntas del servidor.", None
87
+
88
+ print(f"✅ Recibidas {len(questions_data)} preguntas")
89
+
90
+ # Para testing, descomentar para limitar a 5 preguntas:
91
+ # questions_data = questions_data[:5]
92
+
93
+ except Exception as e:
94
+ error_msg = f"❌ Error al obtener preguntas: {str(e)}"
95
+ print(error_msg)
96
+ return error_msg, None
97
+
98
+ # 3. Procesar preguntas
99
+ print(f"🔄 Procesando {len(questions_data)} preguntas...")
100
+ submissions = []
101
+ logs = []
102
+
103
+ for i, q_data in enumerate(questions_data, 1):
104
+ print(f"\n{'='*60}")
105
+ print(f"Pregunta {i}/{len(questions_data)}")
106
+ print(f"{'='*60}")
107
+
108
+ task_id = q_data.get("task_id", f"unknown_{i}")
109
+ question = q_data.get("question", "")
110
+ files = q_data.get("files", [])
111
+
112
+ result = process_question(agent, question, task_id, files)
113
+ submissions.append(result["submission"])
114
+ logs.append(result["log"])
115
+
116
+ if not submissions:
117
+ return "⚠️ No se generaron respuestas.", pd.DataFrame(logs)
118
+
119
+ # 4. Enviar respuestas
120
+ print(f"\n{'='*60}")
121
+ print(f"📤 Enviando {len(submissions)} respuestas...")
122
+ print(f"{'='*60}")
123
+
124
+ submission_data = {
125
+ "username": username,
126
+ "agent_code": agent_code,
127
+ "answers": submissions
128
+ }
129
+
130
+ try:
131
+ response = requests.post(submit_url, json=submission_data, timeout=60)
132
+ response.raise_for_status()
133
+ result_data = response.json()
134
+
135
+ # Formatear resultado
136
+ status_message = (
137
+ f"✅ ¡Respuestas enviadas exitosamente!\n\n"
138
+ f"👤 Usuario: {result_data.get('username')}\n"
139
+ f"📊 Puntuación: {result_data.get('score', 'N/A')}% "
140
+ f"({result_data.get('correct_count', '?')}/{result_data.get('total_attempted', '?')} correctas)\n"
141
+ f"💬 Mensaje: {result_data.get('message', 'Sin mensaje')}"
142
+ )
143
+
144
+ print("✅ Envío exitoso")
145
+ print(status_message)
146
+
147
+ results_df = pd.DataFrame(logs)
148
+ return status_message, results_df
149
+
150
+ except requests.exceptions.HTTPError as e:
151
+ error_detail = f"El servidor respondió con código {e.response.status_code}"
152
+ try:
153
+ error_json = e.response.json()
154
+ error_detail += f"\nDetalle: {error_json.get('detail', e.response.text)}"
155
+ except:
156
+ error_detail += f"\nRespuesta: {e.response.text[:500]}"
157
+
158
+ error_msg = f"❌ Error al enviar respuestas: {error_detail}"
159
+ print(error_msg)
160
+ results_df = pd.DataFrame(logs)
161
+ return error_msg, results_df
162
+
163
+ except Exception as e:
164
+ error_msg = f"❌ Error inesperado: {str(e)}"
165
+ print(error_msg)
166
+ results_df = pd.DataFrame(logs)
167
+ return error_msg, results_df
168
+
169
+
170
+ # Interfaz Gradio
171
+ with gr.Blocks(title="Agente GAIA - Gemini", theme=gr.themes.Soft()) as demo:
172
+ gr.Markdown("""
173
+ # 🤖 Agente GAIA - Google Gemini
174
+
175
+ Este Space usa **Google Gemini** para resolver tareas GAIA Level 1.
176
+
177
+ ## 📋 Instrucciones
178
+
179
+ 1. **Clona este Space** y personalízalo
180
+ 2. **Inicia sesión** con tu cuenta de HuggingFace
181
+ 3. **Configura** `GEMINI_API_KEY` en Settings → Repository secrets
182
+ - Obtén tu API key gratis en: https://aistudio.google.com/apikey
183
+ 4. **Ejecuta** la evaluación
184
+
185
+ ---
186
+ """)
187
+
188
+ # Botón de login OAuth
189
+ gr.LoginButton()
190
+
191
+ gr.Markdown("### 🚀 Ejecutar Evaluación")
192
+
193
+ run_button = gr.Button(
194
+ "▶️ Ejecutar Evaluación y Enviar Respuestas",
195
+ variant="primary",
196
+ size="lg"
197
+ )
198
+
199
+ status_output = gr.Textbox(
200
+ label="📊 Estado / Resultado",
201
+ lines=8,
202
+ interactive=False
203
+ )
204
+
205
+ results_table = gr.DataFrame(
206
+ label="📝 Preguntas y Respuestas del Agente",
207
+ wrap=True
208
+ )
209
+
210
+ # Conectar botón
211
+ run_button.click(
212
+ fn=run_and_submit_all,
213
+ outputs=[status_output, results_table]
214
+ )
215
+
216
+ gr.Markdown("""
217
+ ---
218
+
219
+ ### 📚 Recursos
220
+
221
+ - [Curso de Agentes HuggingFace](https://huggingface.co/learn/agents-course)
222
+ - [Unit 4 Hands-on](https://huggingface.co/learn/agents-course/unit4/hands-on)
223
+ - [Google AI Studio](https://aistudio.google.com/apikey) - Obtén tu API key
224
+
225
+ ### 💡 Ventajas de Gemini
226
+
227
+ - ✅ **Gratis** con límites generosos
228
+ - ✅ **Rápido** (Gemini Flash)
229
+ - ✅ **Multimodal** (texto e imágenes)
230
+ - ✅ Sin necesidad de tarjeta de crédito
231
+
232
+ ### 🔧 Modelo Usado
233
+
234
+ - **Gemini 2.0 Flash Experimental** - Última versión, muy rápida
235
+ - Configurable en `app.py` línea 15
236
+ """)
237
 
 
 
238
 
239
+ if __name__ == "__main__":
240
+ print("\n" + "="*60)
241
+ print(" 🚀 Iniciando Agente GAIA con Gemini")
242
+ print("="*60)
243
+
244
+ # Verificar variables de entorno
245
+ space_host = os.getenv("SPACE_HOST")
246
+ space_id = os.getenv("SPACE_ID")
247
+
248
+ if space_host:
249
+ print(f"✅ SPACE_HOST: {space_host}")
250
+ print(f" URL: https://{space_host}.hf.space")
251
+ else:
252
+ print("ℹ️ SPACE_HOST no encontrado (¿local?)")
253
+
254
+ if space_id:
255
+ print(f"✅ SPACE_ID: {space_id}")
256
+ print(f" Repo: https://huggingface.co/spaces/{space_id}")
257
+ else:
258
+ print("ℹ️ SPACE_ID no encontrado (¿local?)")
259
+
260
+ if not os.getenv("GEMINI_API_KEY"):
261
+ print("⚠️ GEMINI_API_KEY no configurada")
262
+ print(" Obtén una gratis en: https://aistudio.google.com/apikey")
263
+ else:
264
+ print("✅ GEMINI_API_KEY configurada")
265
+
266
+ print("="*60 + "\n")
267
+ print("🌐 Lanzando interfaz Gradio...")
268
+
269
+ # En HF Spaces, Gradio maneja el hosting automáticamente
270
+ demo.launch()
final_answer.py ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Validaciones de formato para respuestas GAIA.
3
+ """
4
+
5
+ import re
6
+
7
+
8
+ def validate_number_format(answer: str) -> tuple[bool, str]:
9
+ """Valida formato de respuesta numérica según reglas GAIA."""
10
+ if not any(char.isdigit() for char in answer):
11
+ return True, "No es un número"
12
+
13
+ issues = []
14
+
15
+ # Verificar comas en números
16
+ if "," in answer and any(c.isdigit() for c in answer):
17
+ issues.append("Números no deben contener comas")
18
+
19
+ # Verificar símbolos de moneda no solicitados
20
+ if any(sym in answer for sym in ["$", "€", "£", "¥"]):
21
+ issues.append("Símbolo de moneda - verificar si fue solicitado")
22
+
23
+ # Verificar porcentaje
24
+ if "%" in answer:
25
+ issues.append("Símbolo % - verificar si fue solicitado")
26
+
27
+ return (len(issues) == 0, "; ".join(issues) if issues else "OK")
28
+
29
+
30
+ def validate_string_format(answer: str) -> tuple[bool, str]:
31
+ """Valida formato de respuesta de texto según reglas GAIA."""
32
+ issues = []
33
+
34
+ # Verificar artículos al inicio
35
+ if answer.lower().startswith(("the ", "a ", "an ")):
36
+ issues.append("No debe comenzar con artículos")
37
+
38
+ # Verificar mayúsculas innecesarias
39
+ if answer.isupper() and len(answer) > 3:
40
+ issues.append("Evitar mayúsculas excesivas")
41
+
42
+ return (len(issues) == 0, "; ".join(issues) if issues else "OK")
43
+
44
+
45
+ def validate_list_format(answer: str) -> tuple[bool, str]:
46
+ """Valida formato de lista según reglas GAIA."""
47
+ issues = []
48
+
49
+ if "[" in answer or "]" in answer:
50
+ issues.append("Listas no deben incluir corchetes []")
51
+
52
+ if "(" in answer or ")" in answer:
53
+ issues.append("Listas no deben incluir paréntesis ()")
54
+
55
+ return (len(issues) == 0, "; ".join(issues) if issues else "OK")
56
+
57
+
58
+ def clean_answer(answer: str) -> str:
59
+ """
60
+ Limpia la respuesta aplicando reglas GAIA.
61
+
62
+ Args:
63
+ answer: Respuesta a limpiar
64
+
65
+ Returns:
66
+ str: Respuesta limpia
67
+ """
68
+ # Remover prefijos comunes
69
+ prefixes = [
70
+ "Final Answer:", "Answer:", "The answer is:",
71
+ "Result:", "=>", "Output:"
72
+ ]
73
+ for prefix in prefixes:
74
+ if answer.startswith(prefix):
75
+ answer = answer[len(prefix):].strip()
76
+
77
+ # Remover comillas
78
+ answer = answer.strip("\"'")
79
+
80
+ # Remover corchetes de listas
81
+ answer = answer.strip("[]")
82
+
83
+ # Remover espacios extras
84
+ answer = " ".join(answer.split())
85
+
86
+ return answer
87
+
88
+
89
+ def validate_answer_format(answer: str, question: str = "") -> dict:
90
+ """
91
+ Validación completa de formato de respuesta.
92
+
93
+ Returns:
94
+ dict: {valid: bool, issues: list, warnings: list}
95
+ """
96
+ issues = []
97
+ warnings = []
98
+
99
+ if not answer or answer.strip() == "":
100
+ issues.append("Respuesta vacía")
101
+ return {"valid": False, "issues": issues, "warnings": warnings}
102
+
103
+ if len(answer) > 500:
104
+ warnings.append("Respuesta muy larga")
105
+
106
+ # Validaciones específicas
107
+ num_valid, num_msg = validate_number_format(answer)
108
+ if not num_valid:
109
+ issues.append(num_msg)
110
+
111
+ str_valid, str_msg = validate_string_format(answer)
112
+ if not str_valid:
113
+ warnings.append(str_msg)
114
+
115
+ list_valid, list_msg = validate_list_format(answer)
116
+ if not list_valid:
117
+ issues.append(list_msg)
118
+
119
+ return {
120
+ "valid": len(issues) == 0,
121
+ "issues": issues,
122
+ "warnings": warnings
123
+ }
model.py ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Model wrapper para usar Google Gemini via LiteLLM.
3
+ Similar a chiarapaglioni/GAIA-agents pero simplificado.
4
+ """
5
+
6
+ import os
7
+ import time
8
+ import re
9
+ from typing import Any, Optional
10
+ from functools import lru_cache
11
+
12
+ try:
13
+ from litellm import completion, RateLimitError
14
+ LITELLM_AVAILABLE = True
15
+ except ImportError:
16
+ LITELLM_AVAILABLE = False
17
+ print("⚠️ LiteLLM no instalado. Instala con: pip install litellm")
18
+
19
+
20
+ class GeminiModel:
21
+ """Wrapper para Gemini usando LiteLLM con manejo de rate limits."""
22
+
23
+ def __init__(self, model_id: str = "gemini/gemini-2.0-flash-exp", api_key: Optional[str] = None, max_retries: int = 5):
24
+ """
25
+ Inicializa el modelo Gemini.
26
+
27
+ Args:
28
+ model_id: ID del modelo Gemini (con prefijo gemini/)
29
+ api_key: API key de Google (GEMINI_API_KEY del env si no se proporciona)
30
+ max_retries: Número máximo de reintentos en caso de rate limit
31
+ """
32
+ if not LITELLM_AVAILABLE:
33
+ raise ImportError("LiteLLM es requerido. Instala con: pip install litellm")
34
+
35
+ self.model_id = model_id
36
+ self.api_key = api_key or os.getenv("GEMINI_API_KEY")
37
+ self.max_retries = max_retries
38
+
39
+ if not self.api_key:
40
+ raise ValueError("GEMINI_API_KEY no encontrada en variables de entorno")
41
+
42
+ def __call__(self, messages, **kwargs):
43
+ """
44
+ Llama al modelo con manejo de rate limits.
45
+
46
+ Args:
47
+ messages: Lista de mensajes en formato OpenAI/LiteLLM
48
+ **kwargs: Argumentos adicionales (temperature, max_tokens, etc.)
49
+
50
+ Returns:
51
+ str: Respuesta del modelo
52
+ """
53
+ for attempt in range(self.max_retries):
54
+ try:
55
+ response = completion(
56
+ model=self.model_id,
57
+ messages=messages,
58
+ api_key=self.api_key,
59
+ **kwargs
60
+ )
61
+ return response.choices[0].message.content
62
+
63
+ except RateLimitError as e:
64
+ error_str = str(e)
65
+ print(f"⚠️ RateLimitError (intento {attempt + 1}/{self.max_retries})")
66
+
67
+ # Intentar extraer tiempo de espera del error
68
+ match = re.search(r'"retryDelay": ?"(\d+)s"', error_str)
69
+ retry_seconds = int(match.group(1)) if match else 50
70
+
71
+ print(f"💤 Esperando {retry_seconds} segundos antes de reintentar...")
72
+ time.sleep(retry_seconds)
73
+
74
+ except Exception as e:
75
+ if attempt == self.max_retries - 1:
76
+ print(f"❌ Error después de {self.max_retries} intentos: {e}")
77
+ raise
78
+
79
+ print(f"⚠️ Error en intento {attempt + 1}/{self.max_retries}: {e}")
80
+ time.sleep(5)
81
+
82
+ raise RateLimitError(f"Rate limit excedido después de {self.max_retries} reintentos.")
83
+
84
+ def generate_simple(self, prompt: str, **kwargs) -> str:
85
+ """
86
+ Helper para generar respuesta desde un prompt simple.
87
+
88
+ Args:
89
+ prompt: Texto del prompt
90
+ **kwargs: Argumentos adicionales
91
+
92
+ Returns:
93
+ str: Respuesta generada
94
+ """
95
+ messages = [{"role": "user", "content": prompt}]
96
+ return self(messages, **kwargs)
97
+
98
+
99
+ @lru_cache(maxsize=1)
100
+ def get_gemini_model(model_id: str = "gemini/gemini-2.0-flash-exp", **kwargs) -> GeminiModel:
101
+ """
102
+ Factory function con cache para obtener instancia del modelo Gemini.
103
+
104
+ Args:
105
+ model_id: ID del modelo Gemini
106
+ **kwargs: Argumentos adicionales
107
+
108
+ Returns:
109
+ GeminiModel: Instancia del modelo con cache
110
+ """
111
+ return GeminiModel(model_id=model_id, **kwargs)
112
+
113
+
114
+ def get_model(model_id: str = "gemini/gemini-2.0-flash-exp", **kwargs) -> GeminiModel:
115
+ """
116
+ Función principal para obtener modelo.
117
+
118
+ Args:
119
+ model_id: ID del modelo (por defecto Gemini Flash)
120
+ **kwargs: Argumentos adicionales
121
+
122
+ Returns:
123
+ GeminiModel: Instancia del modelo
124
+ """
125
+ return get_gemini_model(model_id, **kwargs)
requirements.txt ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Framework web
2
+ gradio>=5.0.0
3
+
4
+ # LLM - Google Gemini via LiteLLM
5
+ litellm>=1.0.0
6
+ google-generativeai>=0.8.0
7
+
8
+ # Procesamiento de datos
9
+ pandas>=2.0.0
10
+ requests>=2.31.0
11
+
12
+ # Procesamiento de imágenes (OCR)
13
+ pillow>=10.0.0
14
+ pytesseract>=0.3.10
15
+
16
+ # Variables de entorno
17
+ python-dotenv>=1.0.0
18
+
tool.py ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Registro de herramientas disponibles para el agente GAIA.
3
+ """
4
+
5
+ import requests
6
+ from tools import read_image_text
7
+ from io import BytesIO
8
+ from PIL import Image
9
+
10
+
11
+ def file_from_url(url: str) -> bytes:
12
+ """
13
+ Descarga un archivo desde una URL y retorna su contenido como bytes.
14
+
15
+ Args:
16
+ url: URL del archivo a descargar
17
+
18
+ Returns:
19
+ Contenido del archivo en bytes
20
+ """
21
+ try:
22
+ response = requests.get(url, timeout=30)
23
+ response.raise_for_status()
24
+ return response.content
25
+ except Exception as e:
26
+ print(f"Error descargando archivo desde {url}: {e}")
27
+ return b""
28
+
29
+
30
+ def read_text_file(url: str) -> str:
31
+ """
32
+ Lee el contenido de un archivo de texto desde una URL.
33
+
34
+ Args:
35
+ url: URL del archivo de texto
36
+
37
+ Returns:
38
+ Contenido del archivo como string
39
+ """
40
+ try:
41
+ content = file_from_url(url)
42
+ if content:
43
+ # Intentar decodificar con diferentes encodings
44
+ for encoding in ['utf-8', 'latin-1', 'cp1252']:
45
+ try:
46
+ return content.decode(encoding)
47
+ except UnicodeDecodeError:
48
+ continue
49
+ # Si ninguno funciona, retornar con errores ignorados
50
+ return content.decode('utf-8', errors='ignore')
51
+ return ""
52
+ except Exception as e:
53
+ print(f"Error leyendo archivo de texto desde {url}: {e}")
54
+ return ""
55
+
56
+
57
+ def get_tools():
58
+ """
59
+ Retorna la lista de herramientas disponibles para el agente.
60
+
61
+ Returns:
62
+ Lista de diccionarios con información de cada herramienta
63
+ """
64
+ return [
65
+ {
66
+ "name": "file_from_url",
67
+ "description": "Descarga un archivo desde una URL",
68
+ "function": file_from_url
69
+ },
70
+ {
71
+ "name": "read_text_file",
72
+ "description": "Lee el contenido de un archivo de texto desde una URL",
73
+ "function": read_text_file
74
+ },
75
+ {
76
+ "name": "read_image_text",
77
+ "description": "Extrae texto de una imagen usando OCR (Tesseract)",
78
+ "function": read_image_text
79
+ }
80
+ ]
tools.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from PIL import Image
2
+ import pytesseract
3
+ import requests
4
+ from io import BytesIO
5
+ import os
6
+
7
+ # Configurar Tesseract en Windows si es necesario
8
+ # Descomenta y ajusta la ruta si Tesseract no está en PATH:
9
+ # pytesseract.pytesseract.tesseract_cmd = r'C:\Program Files\Tesseract-OCR\tesseract.exe'
10
+
11
+ def read_image_text(url: str):
12
+ """Extrae texto de una imagen con OCR."""
13
+ try:
14
+ response = requests.get(url, timeout=30)
15
+ response.raise_for_status()
16
+ img = Image.open(BytesIO(response.content))
17
+ text = pytesseract.image_to_string(img)
18
+ return text.strip()
19
+ except Exception as e:
20
+ print(f"❌ Error al leer imagen {url}: {e}")
21
+ return ""
22
+
23
+ def web_search(query: str):
24
+ """Simula búsqueda web (puedes mejorarla con Wikipedia API)."""
25
+ return f"(Simulación de búsqueda web para '{query}')"