2
This commit is contained in:
@@ -0,0 +1,229 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user