buildMessages($history, $message); try { $response = Prism::text() ->using(self::PROVIDER, $this->model()) ->withSystemPrompt(SystemPromptPublic::get()) ->withMessages($messages) ->asText(); return $response->text; } catch (PrismRateLimitedException $e) { report($e); return 'Demasiadas consultas en poco tiempo. Por favor, esperá un momento e intentá de nuevo.'; } catch (Throwable $e) { report($e); return 'El agente no responde en este momento. Por favor, intentá de nuevo en unos instantes.'; } } public function chatAdmin(string $message, AgentThread $thread, bool $isSuperadmin = false): string { $messages = $this->buildMessages($thread->messages ?? [], $message); try { $response = Prism::text() ->using(self::PROVIDER, $this->model()) ->withSystemPrompt(SystemPromptAdmin::get($isSuperadmin)) ->withMessages($messages) ->withTools($this->getAdminTools($isSuperadmin)) ->withMaxSteps(self::MAX_STEPS) ->asText(); if ($response->text !== '') { $reply = $response->text; } else { Log::warning('Genius admin: empty text from model', [ 'finishReason' => isset($response->finishReason) ? (is_object($response->finishReason) ? ($response->finishReason->name ?? get_class($response->finishReason)) : $response->finishReason) : null, 'steps_count' => is_countable($response->steps ?? null) ? count($response->steps) : null, 'tool_calls' => is_countable($response->toolCalls ?? null) ? count($response->toolCalls) : null, 'tool_results' => is_countable($response->toolResults ?? null) ? count($response->toolResults) : null, ]); $reply = $this->fallbackFromToolResults($response); } $updated = array_merge($thread->messages ?? [], [ ['role' => 'user', 'content' => $message], ['role' => 'assistant', 'content' => $reply], ]); $thread->update(['messages' => $updated]); return $reply; } catch (PrismRateLimitedException $e) { report($e); return 'Demasiadas consultas en poco tiempo. Por favor, esperá un momento e intentá de nuevo.'; } catch (Throwable $e) { report($e); return 'El agente no responde en este momento. Por favor, intentá de nuevo en unos instantes.'; } } private function fallbackFromToolResults($response): string { $collected = []; $steps = $response->steps ?? []; foreach ($steps as $step) { if (!empty($step->text ?? '')) { return $step->text; } foreach (($step->toolResults ?? []) as $tr) { $collected[] = [ 'tool' => $tr->toolName ?? ($tr->name ?? 'tool'), 'result' => $tr->result ?? null, ]; } } if (empty($collected)) { foreach (($response->toolResults ?? []) as $tr) { $collected[] = [ 'tool' => $tr->toolName ?? ($tr->name ?? 'tool'), 'result' => $tr->result ?? null, ]; } } if (empty($collected)) { return 'El agente no devolvió una respuesta. Revisá storage/logs/laravel.log para el detalle.'; } $lines = ["Datos obtenidos (la IA no generó un resumen en texto):"]; foreach ($collected as $item) { $decoded = is_string($item['result']) ? json_decode($item['result'], true) : $item['result']; $pretty = is_array($decoded) || is_object($decoded) ? json_encode($decoded, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) : (string) $item['result']; $lines[] = "\n**{$item['tool']}**:\n{$pretty}"; } return implode("\n", $lines); } /** @return array */ private function buildMessages(array $history, string $newMessage): array { $limit = (int) config('services.genius.history_limit', 10); $trimmed = $limit > 0 ? array_slice($history, -$limit * 2) : $history; $messages = []; foreach ($trimmed as $entry) { if (($entry['role'] ?? '') === 'user') { $messages[] = new UserMessage($entry['content']); } elseif (($entry['role'] ?? '') === 'assistant') { $messages[] = new AssistantMessage($entry['content']); } } $messages[] = new UserMessage($newMessage); return $messages; } /** @return array */ private function getAdminTools(bool $isSuperadmin): array { $readOnly = [ Tool::as('listar_torneos') ->for('Lista todos los torneos del sistema con sus IDs y fechas. Úsalo cuando el admin mencione un torneo por nombre para obtener el id_torneo.') ->using(new ListarTorneosTool()), Tool::as('listar_equipos') ->for('Lista los equipos del sistema. Acepta filtros opcionales por torneo (id_torneo) y grupo.') ->withNumberParameter('id_torneo', 'ID del torneo para filtrar equipos', false) ->withStringParameter('grupo', 'Nombre del grupo (ej. "A", "B") para filtrar dentro del torneo', false) ->using(new ListarEquiposTool()), Tool::as('listar_eventos') ->for('Lista los partidos/eventos del sistema. Acepta filtros opcionales por rango de fechas y torneo.') ->withStringParameter('fecha_desde', 'Fecha de inicio del rango (formato YYYY-MM-DD)', false) ->withStringParameter('fecha_hasta', 'Fecha de fin del rango (formato YYYY-MM-DD)', false) ->withNumberParameter('id_torneo', 'ID del torneo para filtrar eventos', false) ->using(new ListarEventosTool()), ]; if (!$isSuperadmin) { return $readOnly; } $writeTools = [ Tool::as('crear_partido') ->for('Crea un nuevo partido en el sistema. SOLO ejecutar tras confirmación explícita del superadmin.') ->withNumberParameter('id_equipo_local', 'ID del equipo local') ->withNumberParameter('id_equipo_visitante', 'ID del equipo visitante') ->withStringParameter('fecha_evento', 'Fecha del partido (formato YYYY-MM-DD)') ->withStringParameter('hora_inicio', 'Hora de inicio (formato HH:MM)') ->withStringParameter('hora_fin', 'Hora de fin (formato HH:MM)') ->withStringParameter('sede', 'Nombre de la cancha o sede del partido') ->withNumberParameter('id_torneo', 'ID del torneo al que pertenece el partido') ->using(new CrearPartidoTool()), Tool::as('cargar_puntaje') ->for('Actualiza el marcador de un partido existente. SOLO ejecutar tras confirmación explícita del superadmin.') ->withStringParameter('id_evento', 'UUID del partido a actualizar') ->withNumberParameter('marcador_local', 'Puntos del equipo local') ->withNumberParameter('marcador_visitante', 'Puntos del equipo visitante') ->using(new CargarPuntajeTool()), Tool::as('redactar_noticia') ->for('Publica una noticia en el portal OnAPB. SOLO ejecutar tras confirmación explícita del superadmin.') ->withStringParameter('titulo', 'Título de la noticia') ->withStringParameter('contenido', 'Cuerpo completo de la noticia en HTML o texto plano') ->withNumberParameter('id_torneo', 'ID del torneo relacionado (opcional)', false) ->withStringParameter('categoria', 'Categoría de la noticia (opcional)', false) ->using(new RedactarNoticiaTool()), Tool::as('eliminar_noticia') ->for('Elimina (rollback) una noticia publicada. Usar cuando el superadmin pida deshacer una creación.') ->withNumberParameter('id_noticia', 'ID numérico de la noticia a eliminar') ->using(new EliminarNoticiaTool()), Tool::as('eliminar_partido') ->for('Elimina (soft delete / rollback) un partido creado. Usar cuando el superadmin pida deshacer.') ->withStringParameter('id_evento', 'UUID del partido a eliminar') ->using(new EliminarPartidoTool()), ]; return array_merge($readOnly, $writeTools); } }