Mahynlo
		
	commited on
		
		
					Commit 
							
							·
						
						4b5bc79
	
1
								Parent(s):
							
							9261227
								
Migración completa a Google Gemini - Agente GAIA con OAuth y validaciones
Browse files- .env.example +7 -0
- .gitattributes +1 -0
- .gitignore +56 -0
- DEPLOY.md +179 -0
- README.md +67 -5
- RESUMEN_IMPLEMENTACION.md +292 -0
- agents.py +246 -0
- api.py +24 -0
- app.py +267 -4
- final_answer.py +123 -0
- model.py +125 -0
- requirements.txt +18 -0
- tool.py +80 -0
- 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:  | 
| 3 | 
            -
            emoji:  | 
| 4 | 
            -
            colorFrom:  | 
| 5 | 
            -
            colorTo:  | 
| 6 | 
             
            sdk: gradio
         | 
| 7 | 
             
            sdk_version: 5.49.1
         | 
| 8 | 
             
            app_file: app.py
         | 
| 9 | 
             
            pinned: false
         | 
| 10 | 
             
            license: mit
         | 
| 11 | 
            -
            short_description:  | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 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 | 
            -
             | 
| 7 | 
            -
             | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | 
|  | |
| 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}')"
         | 
