This commit is contained in:
Laucha1312
2026-06-04 15:15:23 -03:00
parent 0841794c50
commit 90c5f85512
167 changed files with 15870 additions and 0 deletions
+72
View File
@@ -0,0 +1,72 @@
<?php
namespace App\AI\Prompts;
class SystemPromptAdmin
{
public static function get(bool $isSuperadmin = false): string
{
$rolLabel = $isSuperadmin ? 'Súper Administrador (OnAPB)' : 'Administrador de Club';
$toolsDisponibles = $isSuperadmin
? <<<TOOLS
### Lectura
- **listar_torneos**: lista todos los torneos con su ID, nombre y fechas. Usalo SIEMPRE que el admin mencione un torneo por nombre, antes de preguntarle el ID.
- **listar_equipos**: lista equipos, con filtros opcionales por torneo y/o grupo.
- **listar_eventos**: lista partidos, con filtros opcionales por rango de fechas y/o torneo.
### Escritura (requieren confirmación explícita)
- **crear_partido**: crea un partido.
- **cargar_puntaje**: actualiza el marcador de un partido existente.
- **redactar_noticia**: publica una noticia.
### Rollback
- **eliminar_noticia**: elimina una noticia por su id.
- **eliminar_partido**: elimina (soft delete) un partido por su UUID.
TOOLS
: <<<TOOLS
### Lectura (solo)
- **listar_torneos**: lista todos los torneos.
- **listar_equipos**: lista equipos (filtrable por torneo/grupo).
- **listar_eventos**: lista partidos (filtrable por fechas/torneo).
TOOLS;
$reglasEscritura = $isSuperadmin
? <<<ESCRITURA
## Reglas de escritura (CRÍTICAS)
- **JAMÁS ejecutes una tool de escritura (crear_partido, cargar_puntaje, redactar_noticia) sin antes MOSTRAR al admin un resumen claro y pedirle confirmación explícita ("¿Confirmás?").**
- Esperá una respuesta afirmativa ("sí", "dale", "confirmo", "ok") antes de ejecutar. Si la respuesta es ambigua, volvé a preguntar.
- Tras ejecutar una tool de escritura, DEVOLVELE al admin el ID del recurso creado/modificado y recordale cómo deshacerlo (por ej: "para revertir, pedime 'eliminá la noticia XX'").
- Para un rollback, usá eliminar_noticia o eliminar_partido con el ID que guardaste en el contexto reciente. Pedí confirmación antes de borrar.
ESCRITURA
: <<<ESCRITURA
## Restricciones de tu rol
- Tenés rol de Administrador de Club: **solo podés consultar datos**. No podés crear partidos, cargar puntajes, redactar noticias ni eliminar nada.
- Si el usuario pide una acción de escritura, explicale amablemente que necesita un Súper Administrador y sugerile que lo contacte.
ESCRITURA;
return <<<PROMPT
Sos OnAPB Genius, el asistente de IA oficial de la Asociación Paranaense de Basquetbol (APB), con sede en Paraná, Entre Ríos, Argentina. OnAPB es la plataforma oficial que la APB usa para gestionar torneos, clubes, equipos, jugadores, partidos y noticias.
## Tu interlocutor
Estás hablando con un **{$rolLabel}**. Adaptá tus acciones a lo que este rol tiene permitido.
## Tools disponibles
{$toolsDisponibles}
{$reglasEscritura}
## Reglas generales
- Respondé siempre en español rioplatense (vos/tenés), de forma concisa y amable.
- **Nunca le pidas al admin un ID técnico si podés averiguarlo vos llamando a una tool de lectura.** Por ejemplo: si dice "el torneo Apertura", primero llamá a listar_torneos y buscá el match por nombre.
- Después de ejecutar cualquier tool, SIEMPRE generá una respuesta en texto claro para el usuario con lo que encontraste o hiciste. Nunca termines el turno solo con la llamada a la tool.
- Si una tool devuelve una lista vacía, decilo explícitamente ("No hay equipos en ese torneo/grupo") en lugar de quedar en silencio.
- Si una tool devuelve un error, citá el mensaje y sugerí cómo resolverlo.
- No inventes datos. Si falta información para ejecutar una tool, preguntá por el dato puntual.
- El deporte es basquetbol. Nunca lo confundas con otro.
## Contexto del sistema
La APB gestiona torneos de basquetbol en Paraná. Hay clubes, equipos (pertenecen a un club, juegan en torneos divididos en grupos A, B, etc.), torneos (con fecha de inicio/fin), partidos (llamados "eventos" en la DB, con UUID como id), marcadores, noticias (con id numérico), jugadores federados y aficionados.
PROMPT;
}
}
+51
View File
@@ -0,0 +1,51 @@
<?php
namespace App\AI\Prompts;
class SystemPromptPublic
{
public static function get(): string
{
$manual = self::loadManual();
return <<<PROMPT
Sos OnAPB Genius, el asistente virtual del portal onapb.com de la Asociación Paranaense de Basquetbol (APB), con sede en Paraná, Entre Ríos, Argentina. OnAPB es la plataforma oficial de la APB para gestionar torneos, equipos, jugadores y partidos.
## Tu rol
Ayudás a los usuarios (aficionados, jugadores, visitantes) a navegar el portal y responder consultas sobre torneos de basquetbol, equipos, partidos y cómo usar el sistema.
## Reglas importantes
- Respondé siempre en español rioplatense (vos/tenés), de forma amable y concisa.
- Basá tus respuestas ÚNICAMENTE en la documentación del portal que se incluye a continuación. Si un dato no está en la documentación, NO lo inventes.
- Si no sabés algo o la información no está documentada, decilo honestamente y sugerí que el usuario contacte directamente a la APB.
- No tenés acceso a datos en tiempo real (partidos en vivo, puntajes actuales, estadísticas). Para eso, indicá al usuario que consulte las secciones correspondientes del portal.
- No podés realizar acciones ni modificar datos: solo informás y guiás.
- El deporte es basquetbol. Nunca lo confundas con otro deporte.
## Documentación del portal
{$manual}
PROMPT;
}
private static function loadManual(): string
{
$path = base_path('misc/MANUAL_USUARIO.md');
if (!file_exists($path)) {
return 'Documentación no disponible.';
}
$content = file_get_contents($path);
// Para el chat público excluimos los capítulos de administradores (cap4, cap5)
// pero conservamos la sección de Preguntas Frecuentes que va al final.
$cap4Pos = strpos($content, '<a name="cap4"></a>');
$faqPos = strpos($content, '## ❓ Preguntas Frecuentes');
if ($cap4Pos !== false && $faqPos !== false && $faqPos > $cap4Pos) {
$content = substr($content, 0, $cap4Pos) . substr($content, $faqPos);
}
return $content;
}
}
+30
View File
@@ -0,0 +1,30 @@
<?php
namespace App\AI\Tools;
use App\Models\Evento;
class CargarPuntajeTool
{
public function __invoke(
string $id_evento,
int $marcador_local,
int $marcador_visitante
): string {
$evento = Evento::find($id_evento);
if (!$evento) {
return json_encode(['error' => "No se encontró el partido con ID: {$id_evento}"]);
}
$evento->update([
'marcador_local' => $marcador_local,
'marcador_visitante' => $marcador_visitante,
]);
return json_encode([
'success' => true,
'mensaje' => "Puntaje actualizado: {$marcador_local} - {$marcador_visitante}",
]);
}
}
+43
View File
@@ -0,0 +1,43 @@
<?php
namespace App\AI\Tools;
use App\Models\Evento;
use Illuminate\Support\Str;
class CrearPartidoTool
{
public function __invoke(
int $id_equipo_local,
int $id_equipo_visitante,
string $fecha_evento,
string $hora_inicio,
string $hora_fin,
string $sede,
int $id_torneo,
?float $precio = null
): string {
try {
$evento = Evento::create([
'id_evento' => (string) Str::uuid(),
'id_equipo_local' => $id_equipo_local,
'id_equipo_visitante' => $id_equipo_visitante,
'fecha_evento' => $fecha_evento,
'hora_inicio' => str_contains($hora_inicio, ':') ? $hora_inicio . (strlen($hora_inicio) === 5 ? ':00' : '') : $hora_inicio,
'hora_fin' => str_contains($hora_fin, ':') ? $hora_fin . (strlen($hora_fin) === 5 ? ':00' : '') : $hora_fin,
'sede' => $sede,
'id_torneo' => $id_torneo,
'precio' => $precio ?? 0,
'fase' => Evento::FASE_REGULAR,
]);
return json_encode([
'success' => true,
'id_evento' => $evento->id_evento,
'mensaje' => "Partido creado correctamente. ID: {$evento->id_evento}",
]);
} catch (\Throwable $e) {
return json_encode(['error' => 'No se pudo crear el partido: ' . $e->getMessage()]);
}
}
}
+29
View File
@@ -0,0 +1,29 @@
<?php
namespace App\AI\Tools;
use App\Models\Noticia;
class EliminarNoticiaTool
{
public function __invoke(int $id_noticia): string
{
try {
$noticia = Noticia::find($id_noticia);
if (!$noticia) {
return json_encode(['error' => "No se encontró la noticia con ID: {$id_noticia}"]);
}
$titulo = $noticia->titulo;
$noticia->delete();
return json_encode([
'success' => true,
'mensaje' => "Noticia \"{$titulo}\" (ID {$id_noticia}) eliminada correctamente.",
]);
} catch (\Throwable $e) {
return json_encode(['error' => 'No se pudo eliminar la noticia: ' . $e->getMessage()]);
}
}
}
+28
View File
@@ -0,0 +1,28 @@
<?php
namespace App\AI\Tools;
use App\Models\Evento;
class EliminarPartidoTool
{
public function __invoke(string $id_evento): string
{
try {
$evento = Evento::find($id_evento);
if (!$evento) {
return json_encode(['error' => "No se encontró el partido con ID: {$id_evento}"]);
}
$evento->delete();
return json_encode([
'success' => true,
'mensaje' => "Partido {$id_evento} eliminado correctamente (soft delete).",
]);
} catch (\Throwable $e) {
return json_encode(['error' => 'No se pudo eliminar el partido: ' . $e->getMessage()]);
}
}
}
+33
View File
@@ -0,0 +1,33 @@
<?php
namespace App\AI\Tools;
use App\Models\Equipo;
class ListarEquiposTool
{
public function __invoke(?int $id_torneo = null, ?string $grupo = null): string
{
$query = Equipo::with('club');
if ($id_torneo !== null) {
$query->join('torneo_equipo', 'equipos.id_equipo', '=', 'torneo_equipo.id_equipo')
->where('torneo_equipo.id_torneo', $id_torneo);
if ($grupo !== null) {
$query->where('torneo_equipo.grupo', $grupo);
}
$query->select('equipos.id_equipo', 'equipos.categoria', 'equipos.division', 'equipos.id_club');
}
$equipos = $query->get()->map(fn($e) => [
'id_equipo' => $e->id_equipo,
'categoria' => $e->categoria,
'division' => $e->division,
'club' => $e->club?->nombre,
]);
return json_encode($equipos);
}
}
+40
View File
@@ -0,0 +1,40 @@
<?php
namespace App\AI\Tools;
use App\Models\Evento;
class ListarEventosTool
{
public function __invoke(
?string $fecha_desde = null,
?string $fecha_hasta = null,
?int $id_torneo = null
): string {
$query = Evento::with(['equipoLocal.club', 'equipoVisitante.club'])
->whereNull('deleted_at');
if ($fecha_desde) {
$query->whereDate('fecha_evento', '>=', $fecha_desde);
}
if ($fecha_hasta) {
$query->whereDate('fecha_evento', '<=', $fecha_hasta);
}
if ($id_torneo) {
$query->where('id_torneo', $id_torneo);
}
$eventos = $query->orderBy('fecha_evento')->get()->map(fn($e) => [
'id_evento' => $e->id_evento,
'fecha' => $e->fecha_evento?->format('Y-m-d'),
'hora' => $e->hora_inicio?->format('H:i'),
'local' => $e->equipoLocal?->club?->nombre,
'visitante' => $e->equipoVisitante?->club?->nombre,
'marcador_local' => $e->marcador_local,
'marcador_visitante' => $e->marcador_visitante,
'sede' => $e->sede,
]);
return json_encode($eventos);
}
}
+20
View File
@@ -0,0 +1,20 @@
<?php
namespace App\AI\Tools;
use App\Models\Torneo;
class ListarTorneosTool
{
public function __invoke(): string
{
$torneos = Torneo::orderByDesc('fecha_inicio')->get()->map(fn ($t) => [
'id_torneo' => $t->id,
'nombre' => $t->nombre,
'fecha_inicio' => $t->fecha_inicio?->format('Y-m-d'),
'fecha_fin' => $t->fecha_fin?->format('Y-m-d'),
]);
return json_encode($torneos);
}
}
+33
View File
@@ -0,0 +1,33 @@
<?php
namespace App\AI\Tools;
use App\Models\Noticia;
class RedactarNoticiaTool
{
public function __invoke(
string $titulo,
string $contenido,
?int $id_torneo = null,
?string $categoria = null
): string {
try {
$noticia = Noticia::create([
'titulo' => $titulo,
'contenido' => $contenido,
'fecha' => now(),
'id_torneo' => $id_torneo,
'categoria' => $categoria,
]);
return json_encode([
'success' => true,
'id_noticia' => $noticia->id,
'mensaje' => "Noticia \"{$titulo}\" creada correctamente.",
]);
} catch (\Throwable $e) {
return json_encode(['error' => 'No se pudo crear la noticia: ' . $e->getMessage()]);
}
}
}