230 lines
9.8 KiB
PHP
230 lines
9.8 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\AI\Prompts\SystemPromptAdmin;
|
|
use App\AI\Prompts\SystemPromptPublic;
|
|
use App\AI\Tools\CargarPuntajeTool;
|
|
use App\AI\Tools\CrearPartidoTool;
|
|
use App\AI\Tools\EliminarNoticiaTool;
|
|
use App\AI\Tools\EliminarPartidoTool;
|
|
use App\AI\Tools\ListarEquiposTool;
|
|
use App\AI\Tools\ListarEventosTool;
|
|
use App\AI\Tools\ListarTorneosTool;
|
|
use App\AI\Tools\RedactarNoticiaTool;
|
|
use App\Models\AgentThread;
|
|
use Illuminate\Support\Facades\Log;
|
|
use Prism\Prism\Exceptions\PrismRateLimitedException;
|
|
use Prism\Prism\Facades\Prism;
|
|
use Prism\Prism\Facades\Tool;
|
|
use Prism\Prism\ValueObjects\Messages\AssistantMessage;
|
|
use Prism\Prism\ValueObjects\Messages\UserMessage;
|
|
use Throwable;
|
|
|
|
class GeniusAgentService
|
|
{
|
|
private const PROVIDER = 'gemini';
|
|
private const MAX_STEPS = 6;
|
|
|
|
private function model(): string
|
|
{
|
|
return config('services.genius.model', 'gemini-2.5-flash-lite');
|
|
}
|
|
|
|
public function chatPublic(string $message, array $history): string
|
|
{
|
|
$messages = $this->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<int, \Prism\Prism\Contracts\Message> */
|
|
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<int, \Prism\Prism\Tool> */
|
|
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);
|
|
}
|
|
}
|