# OnAPB Genius Agent — Spec de Diseño **Fecha:** 2026-04-09 **Rama:** `feature/genius-agent` **Stack:** Laravel 12, Prism PHP (prism-php/prism), Google Gemini 1.5 Flash, MySQL, Alpine.js --- ## 1. Objetivo Implementar un agente de IA conversacional ("OnAPB Genius") integrado en onapb.com con dos modos de operación según el rol del usuario: - **Público** (no logueado, aficionado, jugador): asistente de navegación y consultas usando el `MANUAL_USUARIO.md` como contexto RAG simplificado. Sin tools. Sin persistencia entre sesiones. - **Admin** (SuperAdmin rol=1 / GeneralAdmin rol=2): automatización de tareas mediante function calling (Tools). Historial persistente en MySQL, auto-purgado a los 30 días. --- ## 2. Decisiones de Arquitectura | Decisión | Elección | Razón | |---|---|---| | SDK de AI | `prism-php/prism` | Soporte probado de Gemini 1.5 Flash + tools. Más estable que `laravel/ai` (nuevo). | | Modelo | `gemini-1.5-flash` | Velocidad (2–5s), costo bajo, function calling. | | Streaming | No — JSON completo | Hosting compartido Hostinger con límites PHP desconocidos. Evita problemas de output buffer. | | Memoria admin | MySQL (`agent_threads`) | Sin Redis. JSON column para mensajes. Purge automático 30 días. | | Memoria público | Session PHP | Stateless entre sesiones. Simple, sin overhead de BD. | | Tools | Solo si admin_logged_in | Validado en `GeniusAgentService`, no en Gemini. | --- ## 3. Estructura de Archivos ``` app/ ├── AI/ │ ├── Tools/ │ │ ├── CrearPartidoTool.php │ │ ├── CargarPuntajeTool.php │ │ ├── RedactarNoticiaTool.php │ │ ├── ListarEquiposTool.php │ │ └── ListarEventosTool.php │ └── Prompts/ │ ├── SystemPromptAdmin.php │ └── SystemPromptPublic.php ├── Services/ │ └── GeniusAgentService.php ├── Http/Controllers/ │ └── GeniusAgentController.php ├── Models/ │ └── AgentThread.php └── Console/Commands/ └── PurgeAgentThreads.php database/migrations/ └── xxxx_create_agent_threads_table.php resources/views/components/ └── genius-chat.blade.php routes/web.php └── POST /agent/chat (throttle: 20/min por IP) ``` --- ## 4. Schema de Base de Datos ```sql CREATE TABLE agent_threads ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, thread_id VARCHAR(36) NOT NULL UNIQUE, -- UUID generado en frontend admin_id INT NOT NULL, -- session('admin_id') messages JSON NOT NULL, -- array [{role, content}] created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, expires_at TIMESTAMP NOT NULL -- created_at + 30 días ); ``` --- ## 5. Tools Disponibles (Admin) Cada tool implementa la interfaz Prism `Tool`. El agent loop permite que Gemini llame la misma tool N veces para operaciones batch (ej. cargar puntajes de todos los partidos de una jornada). | Tool | Parámetros | Acción en BD | |---|---|---| | `CrearPartidoTool` | `id_equipo_local`, `id_equipo_visitante`, `fecha_evento`, `hora_inicio`, `hora_fin`, `sede`, `id_torneo` | `Evento::create(...)` | | `CargarPuntajeTool` | `id_evento`, `marcador_local`, `marcador_visitante` | `Evento::find()->update(...)` | | `RedactarNoticiaTool` | `titulo`, `contenido`, `id_torneo?`, `categoria?` | `Noticia::create(...)` | | `ListarEquiposTool` | `id_torneo?`, `id_club?`, `grupo?` | `Equipo::query()` con join a pivot `torneo_equipo` filtrando por `grupo` (solo lectura) | | `ListarEventosTool` | `fecha_desde?`, `fecha_hasta?`, `id_torneo?` | `Evento::query()->get()` (solo lectura) | **Nota sobre `grupo`:** No es un modelo separado. Es una columna en la tabla pivot `torneo_equipo` (relación `Torneo::equipos()->withPivot('grupo')`). `ListarEquiposTool` filtra con `wherePivot('grupo', $grupo)`. **Agregar una nueva tool en el futuro:** 1. Crear `app/AI/Tools/NuevaTool.php` 2. Registrarla en `GeniusAgentService::getAdminTools()` --- ## 6. Flujo de Datos ### Usuario público ``` POST /agent/chat { message } → session()->get('agent_messages', []) → GeniusAgentService::chatPublic(message, history) → System prompt: navegación + MANUAL_USUARIO.md completo como contexto (el archivo es pequeño; si crece >50KB usar solo las primeras 200 líneas) → Prism → Gemini (sin tools) → Respuesta de texto → session()->put('agent_messages', [...]) → return JSON { reply } ``` ### Admin ``` POST /agent/chat { message, thread_id? } → AgentThread::findOrCreate(thread_id, admin_id) → GeniusAgentService::chatAdmin(message, thread) → System prompt: automatización de tareas OnAPB → Prism → Gemini con tools → [si Gemini llama tool] → Tool::handle() → resultado → [Gemini puede llamar N tools] → agent loop → Respuesta final de texto → thread->appendMessages([...]) → thread->save() → return JSON { reply, thread_id } ``` --- ## 7. Seguridad - **Tools bloqueadas a no-admins:** `GeniusAgentService` valida `session('admin_logged_in')` antes de incluir tools. - **Rate limiting:** `throttle:20,1` en la ruta `/agent/chat`. - **Validación de input:** `message` requerido, string, máx. 1000 caracteres. - **Aislamiento de threads:** cada thread valida `admin_id = session('admin_id')`. No hay acceso cruzado entre admins. - **API key:** solo en `.env` (`GEMINI_API_KEY`). Nunca en código ni en BD. --- ## 8. Manejo de Errores | Caso | Comportamiento | |---|---| | Gemini timeout / 5xx | `catch RequestException` → JSON `{ error: "El agente no responde, reintentá en un momento." }` | | Tool falla (ej. evento no existe) | Tool retorna `{ error: "..." }` → Gemini lo incorpora en su respuesta | | Thread expirado / no encontrado | Se crea un nuevo thread automáticamente | | API key inválida / quota excedida | Log en Laravel + respuesta genérica (sin exponer detalles de la API) | | Timeout de Hostinger | `set_time_limit(120)` al inicio del controller action | --- ## 9. Purge de Threads Comando `php artisan agent:purge-threads` que elimina `agent_threads` donde `expires_at < NOW()`. **Schedule:** Se registra en `routes/console.php` para ejecutarse diariamente. En Hostinger sin queue worker, se puede configurar como cron en hPanel: ``` # Reemplazar /home/TU_USUARIO/public_html con la ruta real de tu cuenta Hostinger 0 3 * * * cd /home/TU_USUARIO/public_html && php artisan agent:purge-threads >> /dev/null 2>&1 ``` --- ## 10. Frontend — Chat Bubble Componente Blade `genius-chat.blade.php` incluido al final de `resources/views/layouts/app.blade.php`: - Botón flotante (bottom-right) con ícono de chat - Panel slide-up con historial de mensajes de la sesión actual - Alpine.js para estado local (open/closed, messages, loading spinner) - Tailwind para estilos (sin dependencias adicionales) - Rol-aware: el frontend no diferencia, la diferencia la hace el backend --- ## 11. Comandos de Deploy ### Instalación inicial (local y producción) ```bash # 1. Instalar Prism PHP composer require prism-php/prism # 2. Publicar config de Prism (opcional) php artisan vendor:publish --provider="EchoLabs\Prism\PrismServiceProvider" # 3. Agregar en .env GEMINI_API_KEY=tu_api_key_aqui # 4. Ejecutar migration de agent_threads php artisan migrate # 5. Registrar el comando de purge (se hace automáticamente con el código) # Verificar que está en routes/console.php # 6. Limpiar cachés después del deploy php artisan config:clear php artisan cache:clear php artisan view:clear php artisan route:clear # 7. Re-cachear para producción php artisan config:cache php artisan route:cache php artisan view:cache ``` ### Comandos de mantenimiento ```bash # Purge manual de threads expirados php artisan agent:purge-threads # Ver logs del agente php artisan pail --filter="GeniusAgent" # Limpiar todos los threads (emergencia) php artisan tinker --execute="App\Models\AgentThread::truncate();" ``` ### Deploy en Hostinger (via SSH o File Manager) ```bash # Subir archivos nuevos y correr: php artisan migrate --force php artisan config:cache php artisan route:cache php artisan view:cache ``` --- ## 12. Configuración `.env` requerida ```env GEMINI_API_KEY=tu_api_key_de_google_ai_studio # Opcional: ajustar timeout HTTP de Prism PRISM_HTTP_TIMEOUT=30 ```