> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# Laravel AI SDK

> Guía completa del Laravel AI SDK, que permite manejar varios proveedores de IA como OpenAI, Anthropic o Gemini con una interfaz unificada. Cubre agentes, generación de imágenes, audio, embeddings y pruebas.

## Introducción

[Laravel AI SDK](https://github.com/laravel/ai) proporciona una API unificada y expresiva para interactuar con proveedores de IA como OpenAI, Anthropic o Gemini. Con AI SDK puedes construir agentes inteligentes con herramientas y salida estructurada, generar imágenes, sintetizar voz y transcribir audio, crear embeddings vectoriales y muchas otras funciones de IA con una interfaz coherente y muy propia de Laravel.

<Info>
  Laravel AI SDK es un paquete oficial añadido en Laravel 13. Se distribuye como `laravel/ai` y permite trabajar con múltiples proveedores de IA a través de una API unificada.
</Info>

## Lista de compatibilidad por proveedor

| Función                     | Proveedores compatibles                                                                                        |
| --------------------------- | -------------------------------------------------------------------------------------------------------------- |
| Generación de texto         | OpenAI, OpenAI Compatible, Anthropic, Gemini, Azure, Bedrock, Groq, xAI, DeepSeek, Mistral, Ollama, OpenRouter |
| Generación de imágenes      | OpenAI, Gemini, xAI, Azure, Bedrock, OpenRouter                                                                |
| Síntesis de voz (TTS)       | OpenAI, ElevenLabs, Gemini                                                                                     |
| Reconocimiento de voz (STT) | OpenAI, ElevenLabs, Mistral, Gemini                                                                            |
| Embeddings                  | OpenAI, Gemini, Azure, Bedrock, Cohere, Mistral, Jina, VoyageAI, Ollama, OpenRouter                            |
| Reranking                   | Cohere, Jina, VoyageAI                                                                                         |
| Archivos                    | OpenAI, Anthropic, Gemini, Azure                                                                               |

## Instalación

<Steps>
  <Step title="Instalación del paquete">
    Instala Laravel AI SDK con Composer.

    ```shell theme={null}
    composer require laravel/ai
    ```
  </Step>

  <Step title="Publicar el archivo de configuración y las migraciones">
    Publica el archivo de configuración y las migraciones con el comando Artisan `vendor:publish`.

    ```shell theme={null}
    php artisan vendor:publish --provider="Laravel\Ai\AiServiceProvider"
    ```
  </Step>

  <Step title="Ejecutar las migraciones">
    Ejecuta las migraciones de la base de datos. Se crearán las tablas `agent_conversations` y `agent_conversation_messages`, que se usan para guardar el historial de conversaciones.

    ```shell theme={null}
    php artisan migrate
    ```
  </Step>
</Steps>

## Configuración

### Variables de entorno

Define las claves de API de los proveedores de IA que vayas a usar en el archivo `.env`.

```ini theme={null}
ANTHROPIC_API_KEY=
AZURE_OPENAI_API_KEY=
COHERE_API_KEY=
DEEPSEEK_API_KEY=
ELEVENLABS_API_KEY=
GEMINI_API_KEY=
GROQ_API_KEY=
MISTRAL_API_KEY=
OLLAMA_API_KEY=
OPENAI_API_KEY=
OPENROUTER_API_KEY=
JINA_API_KEY=
VOYAGEAI_API_KEY=
XAI_API_KEY=
```

Los modelos por defecto para texto, imágenes, audio, transcripción y embeddings también pueden configurarse en `config/ai.php`.

### URL base personalizada

Si necesitas dirigir el tráfico a través de un servicio proxy, puedes definir una URL personalizada por proveedor.

```php theme={null}
'providers' => [
    'openai' => [
        'driver' => 'openai',
        'key' => env('OPENAI_API_KEY'),
        'url' => env('OPENAI_URL'),
    ],
    'anthropic' => [
        'driver' => 'anthropic',
        'key' => env('ANTHROPIC_API_KEY'),
        'url' => env('ANTHROPIC_BASE_URL'),
    ],
],
```

Las URL base personalizadas están disponibles para OpenAI, Anthropic, Gemini, Groq, Cohere, DeepSeek, xAI y OpenRouter.

### Proveedor OpenAI-Compatible

Cuando uses APIs compatibles con OpenAI, como LM Studio, vLLM, Together, Fireworks o gateways locales, puedes configurar el proveedor con el driver `openai-compatible`. `url` es obligatorio y, si indicas `key`, se enviará como token Bearer.

```php theme={null}
'providers' => [
    'local' => [
        'driver' => 'openai-compatible',
        'url' => env('LOCAL_AI_URL'),
        'key' => env('LOCAL_AI_API_KEY'),
    ],
],
```

Una vez configurado, puedes indicarlo por su nombre de la misma forma que los demás proveedores.

```php theme={null}
agent()->prompt('What is Laravel?', provider: 'local', model: 'local-model');
```

Si defines un modelo de texto por defecto, no necesitarás indicar el modelo cada vez.

```php theme={null}
'local' => [
    'driver' => 'openai-compatible',
    'url' => env('LOCAL_AI_URL'),
    'key' => env('LOCAL_AI_API_KEY'),
    'models' => [
        'text' => [
            'default' => env('LOCAL_AI_MODEL'),
        ],
    ],
],
```

El proveedor OpenAI-Compatible soporta generación de texto, streaming, herramientas, salida estructurada y adjuntar imágenes. Si tu endpoint necesita campos adicionales en el cuerpo de la petición, utiliza [opciones de proveedor](#opciones-de-proveedor).

### Enum Lab

Para referenciar los proveedores desde el código utiliza el enum `Lab`.

```php theme={null}
use Laravel\Ai\Enums\Lab;

Lab::Anthropic;
Lab::OpenAI;
Lab::Gemini;
```

***

## Agentes

Los agentes son el bloque de construcción fundamental de Laravel AI SDK. Puedes generar una clase de agente con el comando `make:agent`.

```shell theme={null}
php artisan make:agent SalesCoach

# Generar un agente con salida estructurada
php artisan make:agent SalesCoach --structured
```

Los agentes generados se colocan en el directorio `app/Ai/Agents/`. A continuación se muestra un ejemplo de un agente que implementa todas las interfaces principales.

```php theme={null}
<?php

namespace App\Ai\Agents;

use App\Ai\Tools\RetrievePreviousTranscripts;
use App\Models\History;
use App\Models\User;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\Conversational;
use Laravel\Ai\Contracts\HasStructuredOutput;
use Laravel\Ai\Contracts\HasTools;
use Laravel\Ai\Messages\Message;
use Laravel\Ai\Promptable;
use Stringable;

class SalesCoach implements Agent, Conversational, HasTools, HasStructuredOutput
{
    use Promptable;

    public function __construct(public User $user) {}

    public function instructions(): Stringable|string
    {
        return 'You are a sales coach, analyzing transcripts and providing feedback and an overall sales strength score.';
    }

    public function messages(): iterable
    {
        return History::where('user_id', $this->user->id)
            ->latest()
            ->limit(50)
            ->get()
            ->reverse()
            ->map(function ($message) {
                return new Message($message->role, $message->content);
            })->all();
    }

    public function tools(): iterable
    {
        return [new RetrievePreviousTranscripts];
    }

    public function schema(JsonSchema $schema): array
    {
        return [
            'feedback' => $schema->string()->required(),
            'score' => $schema->integer()->min(1)->max(10)->required(),
        ];
    }
}
```

```mermaid theme={null}
flowchart TD
    A["Prompt del usuario"] --> B["Agent::prompt()"]
    B --> C["Añadir opciones"]
    C -->|"Si es Conversational"| D["Añadir historial de conversación"]
    C -->|"Si es HasTools"| E["Añadir lista de herramientas"]
    C -->|"Si es HasStructuredOutput"| F["Añadir esquema JSON"]
    D --> G["Enviar al proveedor de IA"]
    E --> G
    F --> G
    B --> G
    G --> H["Respuesta"]
    H -->|"Llamada a herramienta"| I["Ejecutar herramienta"]
    I --> G
    H -->|"Finalizado"| J["AgentResponse"]
```

### Prompt

Envía un mensaje al agente con el método `prompt()`.

```php theme={null}
$response = (new SalesCoach)->prompt('Analyze this sales transcript...');
return (string) $response;
```

Con el método estático `make()` puedes crear una instancia resolviendo sus dependencias desde el contenedor.

```php theme={null}
$agent = SalesCoach::make(user: $user);
```

El proveedor, el modelo y el timeout pueden sobreescribirse mediante los argumentos de `prompt()`.

```php theme={null}
$response = (new SalesCoach)->prompt(
    'Analyze this sales transcript...',
    provider: Lab::Anthropic,
    model: 'claude-haiku-4-5-20251001',
    timeout: 120,
);
```

### Contexto conversacional

Si implementas la interfaz `Conversational` y defines el método `messages()`, puedes pasar el historial de conversación previo a la IA.

Con el trait `RemembersConversations` puedes guardar y recuperar automáticamente el historial de conversación en la base de datos.

```php theme={null}
use Laravel\Ai\Concerns\RemembersConversations;

class SalesCoach implements Agent, Conversational
{
    use Promptable, RemembersConversations;

    public function instructions(): string
    {
        return 'You are a sales coach...';
    }
}
```

Inicia una conversación con `forUser()` y utiliza el `conversationId` devuelto para continuarla con `continue()`.

```php theme={null}
$response = (new SalesCoach)->forUser($user)->prompt('Hello!');
$conversationId = $response->conversationId;

$response = (new SalesCoach)->continue($conversationId, as: $user)->prompt('Tell me more about that.');
```

### Salida estructurada

Si implementas la interfaz `HasStructuredOutput` y defines el esquema JSON en el método `schema()`, podrás recibir la respuesta de la IA como datos estructurados.

```php theme={null}
public function schema(JsonSchema $schema): array
{
    return ['score' => $schema->integer()->required()];
}

$response = (new SalesCoach)->prompt('Analyze this...');
return $response['score'];
```

#### Objetos anidados

```php theme={null}
public function schema(JsonSchema $schema): array
{
    return [
        'score' => $schema->integer()->required(),
        'metadata' => $schema->object(fn ($schema) => [
            'confidence' => $schema->string()->enum(['low', 'medium', 'high'])->required(),
            'language' => $schema->string()->required(),
        ])->required(),
    ];
}
```

#### Arrays de objetos

```php theme={null}
public function schema(JsonSchema $schema): array
{
    return [
        'feedback' => $schema->array()->items(
            $schema->object(fn ($schema) => [
                'comment' => $schema->string()->required(),
                'score' => $schema->integer()->required(),
            ])
        )->required(),
    ];
}
```

#### anyOf (elegir entre varios esquemas)

Cuando el valor pueda coincidir con cualquiera de varios esquemas, usa el método `anyOf`.

```php theme={null}
public function schema(JsonSchema $schema): array
{
    return [
        'content' => $schema->anyOf([
            $schema->object(fn ($schema) => [
                'type' => $schema->string()->enum(['article'])->required(),
                'title' => $schema->string()->required(),
            ]),
            $schema->object(fn ($schema) => [
                'type' => $schema->string()->enum(['image'])->required(),
                'url' => $schema->string()->required(),
            ]),
        ])->required(),
    ];
}
```

### Adjuntos

Con el argumento `attachments` puedes pasar documentos e imágenes al agente.

```php theme={null}
use Laravel\Ai\Files;

$response = (new SalesCoach)->prompt(
    'Analyze the attached sales transcript...',
    attachments: [
        Files\Document::fromStorage('transcript.pdf'),
        Files\Document::fromPath('/home/laravel/transcript.md'),
        $request->file('transcript'),
    ]
);
```

Los adjuntos de imagen se manejan de la misma forma.

```php theme={null}
$response = (new ImageAnalyzer)->prompt('What is in this image?', attachments: [
    Files\Image::fromStorage('photo.jpg'),
    Files\Image::fromPath('/home/laravel/photo.jpg'),
    $request->file('photo'),
]);
```

### Streaming

Con el método `stream()` la respuesta se devuelve por fragmentos (chunks). Es útil para enviar respuestas largas al frontend en tiempo real.

```php theme={null}
Route::get('/coach', function () {
    return (new SalesCoach)->stream('Analyze this sales transcript...');
});
```

Puedes describir el procesamiento tras finalizar el streaming con el callback `then()`.

```php theme={null}
use Laravel\Ai\Responses\StreamedAgentResponse;

Route::get('/coach', function () {
    return (new SalesCoach)
        ->stream('Analyze this sales transcript...')
        ->then(function (StreamedAgentResponse $response) {
            // $response->text, $response->events, $response->usage...
        });
});
```

También puedes iterar el stream manualmente.

```php theme={null}
$stream = (new SalesCoach)->stream('Analyze this sales transcript...');

foreach ($stream as $event) {
    // ...
}
```

#### Protocolo Vercel AI SDK

Si usas Vercel AI SDK en el frontend, llama a `usingVercelDataProtocol()`.

```php theme={null}
Route::get('/coach', function () {
    return (new SalesCoach)->stream('Analyze...')->usingVercelDataProtocol();
});
```

### Broadcasting

Los eventos del stream pueden enviarse a canales de broadcast como Laravel Echo.

```php theme={null}
use Illuminate\Broadcasting\Channel;

$stream = (new SalesCoach)->stream('Analyze this sales transcript...');

foreach ($stream as $event) {
    $event->broadcast(new Channel('channel-name'));
}
```

Con `broadcastOnQueue()` puedes emitir el broadcast pasando por la cola.

```php theme={null}
(new SalesCoach)->broadcastOnQueue(
    'Analyze this sales transcript...',
    new Channel('channel-name'),
);
```

#### Omitir eventos muy grandes

Algunas plataformas de broadcast limitan los mensajes de WebSocket a aproximadamente 10 KB. Los eventos de stream con mucho contenido (como resultados de herramientas de gran tamaño) pueden superar ese límite y fallar al emitirse. Con el atributo `WithoutBroadcasting` puedes excluir del broadcast tipos de eventos concretos.

```php theme={null}
<?php

namespace App\Ai\Agents;

use Laravel\Ai\Attributes\WithoutBroadcasting;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\HasTools;
use Laravel\Ai\Promptable;
use Laravel\Ai\Streaming\Events\ToolCall;
use Laravel\Ai\Streaming\Events\ToolResult;

#[WithoutBroadcasting(ToolCall::class, ToolResult::class)]
class SearchAgent implements Agent, HasTools
{
    use Promptable;

    // ...
}
```

Los eventos excluidos no se emiten por broadcast, pero se siguen guardando en la tabla `agent_conversation_messages`. De esta forma, el frontend puede obtener todos los datos de las herramientas tras finalizar el stream. Esto funciona tanto por cola (`broadcastOnQueue`) como de forma síncrona (`broadcast` / `broadcastNow`).

### Cola

Con el método `queue()` puedes encolar el prompt para procesarlo de forma asíncrona.

```php theme={null}
use Laravel\Ai\Responses\AgentResponse;

Route::post('/coach', function (Request $request) {
    (new SalesCoach)
        ->queue($request->input('transcript'))
        ->then(function (AgentResponse $response) { /* ... */ })
        ->catch(function (Throwable $e) { /* ... */ });

    return back();
});
```

### Herramientas

Las herramientas permiten que la IA invoque funciones de tu código. Puedes generar una clase de herramienta con el comando `make:tool`.

```shell theme={null}
php artisan make:tool RandomNumberGenerator
```

```php theme={null}
<?php

namespace App\Ai\Tools;

use Illuminate\Contracts\JsonSchema\JsonSchema;
use Laravel\Ai\Contracts\Tool;
use Laravel\Ai\Tools\Request;
use Stringable;

class RandomNumberGenerator implements Tool
{
    public function description(): Stringable|string
    {
        return 'This tool may be used to generate cryptographically secure random numbers.';
    }

    public function handle(Request $request): Stringable|string
    {
        return (string) random_int($request['min'], $request['max']);
    }

    public function schema(JsonSchema $schema): array
    {
        return [
            'min' => $schema->integer()->min(0)->required(),
            'max' => $schema->integer()->required(),
        ];
    }
}
```

Registra las herramientas en el método `tools()` del agente.

```php theme={null}
public function tools(): iterable
{
    return [new RandomNumberGenerator];
}
```

#### Herramienta de búsqueda por similitud

Puedes añadir con facilidad una herramienta de búsqueda por similitud basada en embeddings vectoriales.

```php theme={null}
use App\Models\Document;
use Laravel\Ai\Tools\SimilaritySearch;

public function tools(): iterable
{
    return [
        SimilaritySearch::usingModel(Document::class, 'embedding'),
    ];
}
```

También puedes indicar opciones.

```php theme={null}
SimilaritySearch::usingModel(
    model: Document::class,
    column: 'embedding',
    minSimilarity: 0.7,
    limit: 10,
    query: fn ($query) => $query->where('published', true),
),
```

Y definir tu propia lógica de búsqueda con un closure.

```php theme={null}
new SimilaritySearch(using: function (string $query) {
    return Document::query()
        ->where('user_id', $this->user->id)
        ->whereVectorSimilarTo('embedding', $query)
        ->limit(10)
        ->get();
}),
```

Con `withDescription()` puedes personalizar la descripción de la herramienta.

```php theme={null}
SimilaritySearch::usingModel(Document::class, 'embedding')
    ->withDescription('Search the knowledge base for relevant articles.'),
```

### Herramientas de almacenamiento de archivos

Con la fábrica de herramientas `FileStorage` puedes darle al agente acceso a un [disco del sistema de archivos](/es/filesystem) de Laravel. El método `all` devuelve el conjunto de herramientas para listar, leer, generar URL, escribir, eliminar y copiar archivos en el disco indicado.

```php theme={null}
use Laravel\Ai\Tools\FileStorage;

public function tools(): iterable
{
    return FileStorage::all('local');
}
```

Si solo quieres permitir acceso de lectura, utiliza el método `readOnly`.

```php theme={null}
return FileStorage::readOnly('local');
```

Estos métodos devuelven una `Illuminate\Support\Collection`, así que puedes filtrar aún más las herramientas que se ofrecen.

```php theme={null}
use Laravel\Ai\Tools\Filesystem\DeleteFile;

return FileStorage::all('s3')
    ->reject(fn ($tool) => $tool instanceof DeleteFile);
```

### Herramientas MCP

Si tu aplicación utiliza [Laravel MCP](/es/mcp), puedes ofrecer al agente las herramientas expuestas por un servidor [Model Context Protocol](https://modelcontextprotocol.io). Puedes utilizar el [cliente MCP de Laravel](/es/mcp#cliente-mcp) para conectarte a un servidor MCP remoto o local y pasar sus herramientas directamente al agente.

<Info>
  Para usar herramientas MCP necesitas tener instalado el paquete [Laravel MCP](/es/mcp) en la aplicación.
</Info>

El método `tools` del cliente MCP devuelve una colección, así que utiliza el operador spread `...` para expandirla dentro del array `tools` del agente.

```php theme={null}
use App\Ai\Tools\RandomNumberGenerator;
use Laravel\Mcp\Client;

/**
 * Get the tools available to the agent.
 *
 * @return Tool[]
 */
public function tools(): iterable
{
    return [
        ...Client::web('https://mcp.example.com')
            ->withToken($token)
            ->tools(),

        new RandomNumberGenerator,
    ];
}
```

El AI SDK envuelve automáticamente cada herramienta MCP para que el agente pueda invocarla como cualquier otra herramienta. También puedes usar [clientes MCP con nombre](/es/mcp#clientes-con-nombre).

```php theme={null}
use Laravel\Mcp\Facades\Mcp;

public function tools(): iterable
{
    return [
        ...Mcp::client('github')->tools(),
    ];
}
```

O conectarte a un [servidor MCP local](/es/mcp#servidor-local).

```php theme={null}
use Laravel\Mcp\Client;

public function tools(): iterable
{
    return [
        ...Client::local('php', ['artisan', 'mcp:start'])->tools(),
    ];
}
```

Para más información sobre la creación de clientes MCP y la autenticación (tokens Bearer, OAuth, etc.), consulta la [documentación del cliente MCP](/es/mcp#cliente-mcp).

### Herramientas de proveedor

Son herramientas especiales que el proveedor de IA implementa de forma nativa.

#### Búsqueda web

Añade búsqueda web al agente. Compatible con Anthropic, OpenAI, Gemini y OpenRouter.

```php theme={null}
use Laravel\Ai\Providers\Tools\WebSearch;

public function tools(): iterable
{
    return [new WebSearch];
}
```

Opcionalmente puedes indicar el número de resultados, restringir dominios o especificar la ubicación.

```php theme={null}
(new WebSearch)->max(5)->allow(['laravel.com', 'php.net']),
(new WebSearch)->location(city: 'New York', region: 'NY', country: 'US');
```

#### Fetch web

Herramienta para obtener el contenido de la URL indicada. Compatible con Anthropic y Gemini.

```php theme={null}
use Laravel\Ai\Providers\Tools\WebFetch;

public function tools(): iterable
{
    return [new WebFetch];
}

(new WebFetch)->max(3)->allow(['docs.laravel.com']),
```

#### Búsqueda de archivos

Herramienta para buscar documentos en un vector store. Compatible con OpenAI y Gemini.

```php theme={null}
use Laravel\Ai\Providers\Tools\FileSearch;

public function tools(): iterable
{
    return [new FileSearch(stores: ['store_id'])];
}

// Especificar varios stores
new FileSearch(stores: ['store_1', 'store_2']);

// Filtrar por metadatos
new FileSearch(stores: ['store_id'], where: ['author' => 'Taylor Otwell', 'year' => 2026]);
```

También puedes indicar filtros complejos con `FileSearchQuery`.

```php theme={null}
use Laravel\Ai\Providers\Tools\FileSearchQuery;

new FileSearch(stores: ['store_id'], where: fn (FileSearchQuery $query) =>
    $query->where('author', 'Taylor Otwell')
          ->whereNot('status', 'draft')
          ->whereIn('category', ['news', 'updates'])
);
```

### Subagentes

Un agente también puede devolverse desde el método `tools()` de otro agente. Al registrar un agente como herramienta, el agente padre puede delegar tareas concretas en el subagente e incorporar el resultado a su respuesta original. Resulta útil cuando un agente genérico necesita acceder a agentes especializados con instrucciones, herramientas, configuración de modelo o de proveedor específicas.

Por ejemplo, un agente de atención al cliente que delega las preguntas sobre políticas de reembolso a un agente especializado en reembolsos.

```php theme={null}
<?php

namespace App\Ai\Agents;

use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\HasTools;
use Laravel\Ai\Promptable;

class CustomerSupportAgent implements Agent, HasTools
{
    use Promptable;

    public function instructions(): string
    {
        return 'You help customers with account, order, and billing questions. Delegate refund policy questions to the refunds specialist.';
    }

    public function tools(): iterable
    {
        return [
            new RefundsAgent,
        ];
    }
}
```

Para personalizar cómo se le presenta el subagente al agente padre, implementa la interfaz `CanActAsTool` en el subagente y define el nombre y la descripción de la herramienta.

```php theme={null}
<?php

namespace App\Ai\Agents;

use App\Ai\Tools\LookupOrder;
use Laravel\Ai\Attributes\Provider;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\CanActAsTool;
use Laravel\Ai\Contracts\HasTools;
use Laravel\Ai\Enums\Lab;
use Laravel\Ai\Promptable;

#[Provider(Lab::Anthropic)]
class RefundsAgent implements Agent, CanActAsTool, HasTools
{
    use Promptable;

    public function instructions(): string
    {
        return 'You are a refunds specialist. Use order details and the refund policy to give concise eligibility guidance.';
    }

    public function name(): string
    {
        return 'refunds_specialist';
    }

    public function description(): string
    {
        return 'Determine whether an order is eligible for a refund and explain the next step.';
    }

    public function tools(): iterable
    {
        return [
            new LookupOrder,
        ];
    }
}
```

Cuando el subagente no implementa `CanActAsTool`, Laravel usa el nombre de la clase como nombre de la herramienta y genera automáticamente una descripción genérica. Cada invocación de subagente es independiente y no hereda el historial de conversación del agente padre.

### Middleware

Puedes añadir middleware al agente para interceptar prompts y respuestas.

```shell theme={null}
php artisan make:agent-middleware LogPrompts
```

Implementa la interfaz `HasMiddleware` en el agente y registra el middleware en el método `middleware()`.

```php theme={null}
<?php

namespace App\Ai\Agents;

use App\Ai\Middleware\LogPrompts;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\HasMiddleware;
use Laravel\Ai\Promptable;

class SalesCoach implements Agent, HasMiddleware
{
    use Promptable;

    public function middleware(): array
    {
        return [new LogPrompts];
    }
}
```

Un ejemplo de implementación de la clase de middleware.

```php theme={null}
<?php

namespace App\Ai\Middleware;

use Closure;
use Laravel\Ai\Prompts\AgentPrompt;

class LogPrompts
{
    public function handle(AgentPrompt $prompt, Closure $next)
    {
        Log::info('Prompting agent', ['prompt' => $prompt->prompt]);
        return $next($prompt);
    }
}
```

Con `then()` también puedes añadir procesamiento posterior a la respuesta.

```php theme={null}
public function handle(AgentPrompt $prompt, Closure $next)
{
    return $next($prompt)->then(function (AgentResponse $response) {
        Log::info('Agent responded', ['text' => $response->text]);
    });
}
```

### Agentes anónimos

Con la función helper `agent()` puedes utilizar agentes anónimos sin necesidad de definir una clase.

```php theme={null}
use function Laravel\Ai\{agent};

$response = agent(
    instructions: 'You are an expert at software development.',
    messages: [],
    tools: [],
)->prompt('Tell me about Laravel');
```

También puedes crear agentes anónimos con salida estructurada.

```php theme={null}
use Illuminate\Contracts\JsonSchema\JsonSchema;

$response = agent(
    schema: fn (JsonSchema $schema) => ['number' => $schema->integer()->required()],
)->prompt('Generate a random number less than 100');
```

### Configuración del agente (atributos PHP)

Con atributos PHP puedes escribir la configuración por defecto del agente de forma declarativa.

| Atributo              | Descripción                                          |
| --------------------- | ---------------------------------------------------- |
| `#[Provider]`         | Especifica el proveedor a usar                       |
| `#[Model]`            | Especifica el modelo a usar                          |
| `#[MaxSteps]`         | Número máximo de pasos de invocación de herramientas |
| `#[MaxTokens]`        | Número máximo de tokens                              |
| `#[Temperature]`      | Parámetro de temperatura (0.0–1.0)                   |
| `#[TopP]`             | Probabilidad del núcleo de sampling (0.0–1.0)        |
| `#[Timeout]`          | Timeout (segundos)                                   |
| `#[UseCheapestModel]` | Selecciona automáticamente el modelo más económico   |
| `#[UseSmartestModel]` | Selecciona automáticamente el modelo más potente     |

```php theme={null}
<?php

namespace App\Ai\Agents;

use Laravel\Ai\Attributes\MaxSteps;
use Laravel\Ai\Attributes\MaxTokens;
use Laravel\Ai\Attributes\Model;
use Laravel\Ai\Attributes\Provider;
use Laravel\Ai\Attributes\Temperature;
use Laravel\Ai\Attributes\Timeout;
use Laravel\Ai\Attributes\TopP;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Enums\Lab;
use Laravel\Ai\Promptable;

#[Provider(Lab::Anthropic)]
#[Model('claude-haiku-4-5-20251001')]
#[MaxSteps(10)]
#[MaxTokens(4096)]
#[Temperature(0.7)]
#[Timeout(120)]
#[TopP(0.9)]
class SalesCoach implements Agent
{
    use Promptable;
}
```

También hay atributos de atajo para la selección de modelo.

```php theme={null}
// Usar el modelo más económico
#[UseCheapestModel]
class SimpleSummarizer implements Agent
{
    use Promptable;
}

// Usar el modelo más potente
#[UseSmartestModel]
class ComplexReasoner implements Agent
{
    use Promptable;
}
```

### Opciones de proveedor

Al implementar la interfaz `HasProviderOptions` puedes pasar opciones específicas del proveedor.

```php theme={null}
<?php

namespace App\Ai\Agents;

use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\HasProviderOptions;
use Laravel\Ai\Enums\Lab;
use Laravel\Ai\Promptable;

class SalesCoach implements Agent, HasProviderOptions
{
    use Promptable;

    public function providerOptions(Lab|string $provider): array
    {
        return match ($provider) {
            Lab::OpenAI => [
                'reasoning' => ['effort' => 'low'],
                'frequency_penalty' => 0.5,
                'presence_penalty' => 0.3,
            ],
            Lab::Anthropic => [
                'thinking' => ['budget_tokens' => 1024],
            ],
            default => [],
        };
    }
}
```

***

## Aprobación humana de herramientas (Human Tool Approval)

<Warning>
  Para utilizar la aprobación de herramientas necesitas un agente `Conversational` cuyo historial de conversación se persista. Para poder reanudar una invocación en pausa, el trait `RemembersConversations` aporta la persistencia necesaria.
</Warning>

En herramientas que realizan operaciones sensibles o irreversibles, como eliminar archivos o transferir dinero, puedes exigir aprobación humana antes de la ejecución. Para marcar una herramienta como aprobable, implementa el contrato `Approvable` y usa el trait `InteractsWithApprovals`. Las herramientas aprobables requieren aprobación por defecto.

```php theme={null}
<?php

namespace App\Ai\Tools;

use Illuminate\Contracts\JsonSchema\JsonSchema;
use Illuminate\Support\Facades\Storage;
use Laravel\Ai\Concerns\InteractsWithApprovals;
use Laravel\Ai\Contracts\Approvable;
use Laravel\Ai\Contracts\Tool;
use Laravel\Ai\Tools\Request;
use Stringable;

class DeleteFile implements Approvable, Tool
{
    use InteractsWithApprovals;

    /**
     * Obtiene la descripción del propósito de la herramienta
     */
    public function description(): Stringable|string
    {
        return 'Elimina un archivo del almacenamiento.';
    }

    /**
     * Ejecuta la herramienta
     */
    public function handle(Request $request): Stringable|string
    {
        Storage::delete($request['path']);

        return "Eliminado: [{$request['path']}]";
    }

    /**
     * Obtiene la definición del esquema de la herramienta
     */
    public function schema(JsonSchema $schema): array
    {
        return [
            'path' => $schema->string()->required(),
        ];
    }
}
```

Si quieres decidir si se requiere aprobación en función de los argumentos de la invocación, define el método `needsApproval` en la herramienta. Este método puede devolver un booleano o una instancia de `Approval` que incluya el motivo de la aprobación.

```php theme={null}
use Laravel\Ai\Approvals\Approval;

/**
 * Determina si la herramienta necesita aprobación para la petición dada
 */
protected function needsApproval(Request $request): Approval|bool
{
    return str_starts_with($request['path'], 'temporary/')
        ? false
        : Approval::required('Este archivo se eliminará de forma permanente.');
}
```

Al devolver las herramientas desde el método `tools` del agente también puedes sobreescribir el requisito de aprobación.

```php theme={null}
public function tools(): iterable
{
    return [
        (new SendNotification)->withoutApproval(),
        (new DeleteFile)->requireApproval('La eliminación requiere confirmación.'),
    ];
}
```

Cuando se invoca una herramienta aprobable, el agente se detiene antes de ejecutarla. Puedes examinar `pendingApprovals` en la respuesta para consultar el ID, el nombre de la herramienta, los argumentos y el motivo de aprobación de cada invocación.

```php theme={null}
$response = (new FileAssistant)
    ->forUser($user)
    ->prompt('Elimina las facturas antiguas.');

if ($response->hasPendingApprovals()) {
    foreach ($response->pendingApprovals as $approval) {
        // $approval->id
        // $approval->tool
        // $approval->arguments
        // $approval->reason
    }
}
```

Para reanudar el agente, continúa la conversación y pasa una instancia de `Decisions` con la decisión de cada invocación de herramienta pendiente. En una decisión puedes aprobar, rechazar o editar los argumentos de la invocación antes de ejecutarla.

```php theme={null}
use Laravel\Ai\Approvals\Decision;
use Laravel\Ai\Approvals\Decisions;

$response = (new FileAssistant)
    ->continue($conversationId, as: $user)
    ->prompt(Decisions::from([
        'call_abc' => Decision::approve(),
        'call_ghi' => Decision::reject('Esta factura debe conservarse.'),
    ]));
```

Los valores booleanos `true` y `false` pueden usarse como forma abreviada de aprobar y rechazar, respectivamente. Todas las invocaciones de herramienta pendientes necesitan una decisión. Si indicas un ID de invocación desconocido, ausente o ya resuelto, se lanzará `ApprovalMismatchException`. Para las invocaciones sin decisión explícita, puedes indicar una decisión por defecto con los métodos `approveRemaining` o `rejectRemaining`.

```php theme={null}
$decisions = Decisions::from([
    'call_abc' => true,
])->rejectRemaining('No fue aprobado.');

$response = (new FileAssistant)
    ->continue($conversationId, as: $user)
    ->prompt($decisions);
```

Al rechazar con un resultado, como `Decision::reject('No fue aprobado.')`, el resultado se devuelve al modelo y la respuesta continúa. Si se rechaza sin resultado, el bucle de generación se detiene en el momento en que se registra el rechazo.

La aprobación de herramientas se soporta en los métodos `prompt`, `stream`, `queue`, `broadcast`, `broadcastNow` y `broadcastOnQueue`.

Durante el streaming y el broadcasting, la pausa se representa como un evento `tool_approval_request`. Cuando utilizas el [protocolo de stream de Vercel AI SDK](#protocolo-vercel-ai-sdk), las solicitudes y los resultados de aprobación se emiten como las partes nativas de aprobación de herramientas del protocolo.

En el caso de agentes encolados, la respuesta resultante se pasa al callback `then` y Laravel también despacha el evento `ToolApprovalRequested`.

Laravel guarda el resultado de la ejecución de las herramientas aprobadas antes de pedirle al modelo que continúe. Si la generación posterior falla, las aprobaciones ya están resueltas. En lugar de reenviar la misma decisión de aprobación, continúa la conversación con un prompt de texto normal.

### Flujo completo de aprobación

Las siguientes rutas muestran un flujo completo de aprobación. La ruta `GET` devuelve la pantalla de chat y la ruta `POST` recibe un nuevo prompt de texto desde la pantalla o una decisión de aprobación. En este ejemplo se asume que el modelo `User` de la aplicación usa el trait `HasConversations`.

```php theme={null}
use App\Ai\Agents\FileAssistant;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Route;
use Illuminate\Validation\Rule;
use Laravel\Ai\Approvals\Decision;
use Laravel\Ai\Approvals\Decisions;
use Laravel\Ai\Models\Conversation;

Route::get('/chat/{conversation}', function (Request $request, Conversation $conversation) {
    Gate::authorize('view', $conversation);

    return view('chat', [
        'conversation' => $conversation,
    ]);
})->middleware('auth');

Route::post('/chat/{conversation}', function (Request $request, Conversation $conversation) {
    Gate::authorize('view', $conversation);

    $validated = $request->validate([
        'message' => ['nullable', 'string', 'required_without:decisions', 'prohibited_with:decisions'],
        'decisions' => ['nullable', 'array', 'required_without:message', 'prohibited_with:message'],
        'decisions.*.action' => ['required_with:decisions', Rule::in(['approve', 'reject'])],
        'decisions.*.result' => ['nullable', 'string'],
    ]);

    $prompt = isset($validated['decisions'])
        ? Decisions::from($validated->collect('decisions')->map(
            fn (array $decision) => match ($decision['action']) {
                'approve' => Decision::approve(),
                'reject' => Decision::reject($decision['result'] ?? null),
            }
        )->all())
        : $validated['message'];

    $response = (new FileAssistant)
        ->continue($conversation->id, as: $request->user())
        ->prompt($prompt);

    return [
        'conversation_id' => $response->conversationId,
        'status' => $response->hasPendingApprovals() ? 'awaiting_approval' : 'complete',
        'message' => $response->text,
        'approvals' => $response->pendingApprovals,
    ];
})->middleware('auth');
```

Cuando el estado de la respuesta sea `awaiting_approval`, la pantalla de chat mostrará las aprobaciones pendientes y deberá enviar al mismo endpoint la elección del usuario usando como clave el ID de la invocación de la herramienta.

```json theme={null}
{
    "decisions": {
        "call_abc": {
            "action": "approve"
        },
        "call_def": {
            "action": "reject",
            "result": "Esta factura debe conservarse."
        }
    }
}
```

Para un mensaje de chat normal, en su lugar envía el valor de `message`.

```json theme={null}
{
    "message": "Elimina las facturas antiguas."
}
```

<Tip>
  El flujo de aprobación es un mecanismo que otorga a los agentes de IA capacidades operativas potentes al tiempo que introduce una revisión humana antes de la ejecución. Aprovéchalo especialmente en herramientas cuyas operaciones no puedan deshacerse: eliminación de archivos, procesamiento de pagos, escritura en APIs externas, etc.
</Tip>

***

## Generación de imágenes

Con la clase `Image` puedes generar imágenes. Compatible con los proveedores OpenAI, Gemini y xAI.

```php theme={null}
use Laravel\Ai\Image;

$image = Image::of('A donut sitting on the kitchen counter')->generate();
$rawContent = (string) $image;
```

Puedes indicar la calidad, la relación de aspecto y el timeout.

```php theme={null}
$image = Image::of('A donut sitting on the kitchen counter')
    ->quality('high')
    ->landscape()
    ->timeout(120)
    ->generate();
```

También puedes adjuntar una imagen de referencia para editarla.

```php theme={null}
use Laravel\Ai\Files;

$image = Image::of('Update this photo to be in the style of an impressionist painting.')
    ->attachments([Files\Image::fromStorage('photo.jpg')])
    ->landscape()
    ->generate();
```

### Guardar la imagen

```php theme={null}
$path = $image->store();
$path = $image->storeAs('image.jpg');
$path = $image->storePublicly();
$path = $image->storePubliclyAs('image.jpg');
```

### Generar imágenes por cola

```php theme={null}
use Laravel\Ai\Responses\ImageResponse;

Image::of('A donut sitting on the kitchen counter')
    ->portrait()
    ->queue()
    ->then(function (ImageResponse $image) {
        $path = $image->store();
    });
```

***

## Síntesis de voz (TTS)

Con la clase `Audio` puedes convertir texto a voz. Compatible con los proveedores OpenAI y ElevenLabs.

```php theme={null}
use Laravel\Ai\Audio;

$audio = Audio::of('I love coding with Laravel.')->generate();
$rawContent = (string) $audio;
```

Puedes indicar el género de la voz, un ID de voz concreto o instrucciones de estilo.

```php theme={null}
$audio = Audio::of('I love coding with Laravel.')->female()->generate();
$audio = Audio::of('I love coding with Laravel.')->voice('voice-id-or-name')->generate();
$audio = Audio::of('I love coding with Laravel.')->female()->instructions('Said like a pirate')->generate();
```

### Guardar el audio

```php theme={null}
$path = $audio->store();
$path = $audio->storeAs('audio.mp3');
$path = $audio->storePublicly();
$path = $audio->storePubliclyAs('audio.mp3');
```

### Generar audio por cola

```php theme={null}
use Laravel\Ai\Responses\AudioResponse;

Audio::of('I love coding with Laravel.')
    ->queue()
    ->then(function (AudioResponse $audio) {
        $path = $audio->store();
    });
```

***

## Transcripción (STT)

Con la clase `Transcription` puedes convertir archivos de audio a texto. Compatible con los proveedores OpenAI, ElevenLabs y Mistral.

```php theme={null}
use Laravel\Ai\Transcription;

$transcript = Transcription::fromPath('/home/laravel/audio.mp3')->generate();
$transcript = Transcription::fromStorage('audio.mp3')->generate();
$transcript = Transcription::fromUpload($request->file('audio'))->generate();

return (string) $transcript;
```

### Diarización de hablantes

Con `diarize()` obtienes una transcripción segmentada por hablante.

```php theme={null}
$transcript = Transcription::fromStorage('audio.mp3')->diarize()->generate();
```

### Transcripción por cola

```php theme={null}
use Laravel\Ai\Responses\TranscriptionResponse;

Transcription::fromStorage('audio.mp3')
    ->queue()
    ->then(function (TranscriptionResponse $transcript) { /* ... */ });
```

***

## Resumen de texto (Text Summarization)

Con el método `summarize` que proporciona la clase `Stringable` de Laravel puedes resumir un texto. Por defecto se resume en 3 frases como máximo, utilizando el modelo de texto más económico del proveedor configurado.

```php theme={null}
use Illuminate\Support\Str;

$summary = Str::of($article)->summarize();
```

También puedes indicar el número máximo de frases del resumen, el proveedor, el modelo y el timeout. La clase `Str` ofrece la versión estática.

```php theme={null}
use Laravel\Ai\Enums\Lab;

$summary = Str::of($article)->summarize(
    sentences: 4,
    provider: Lab::Anthropic,
    model: 'claude-sonnet-5',
    timeout: 30,
);

$summary = Str::summarize($article, sentences: 4);
```

***

## Embeddings

Convierte texto en representaciones vectoriales para aprovecharlo en búsquedas por similitud, entre otras cosas.

```php theme={null}
use Illuminate\Support\Str;

// Forma con Stringable
$embeddings = Str::of('Napa Valley has great wine.')->toEmbeddings();
```

```php theme={null}
use Laravel\Ai\Embeddings;

// Procesar varios textos a la vez
$response = Embeddings::for(['Napa Valley has great wine.', 'Laravel is a PHP framework.'])->generate();
$response->embeddings; // [[0.123, 0.456, ...], [0.789, 0.012, ...]]
```

También puedes indicar el proveedor, el modelo y las dimensiones.

```php theme={null}
$response = Embeddings::for(['Napa Valley has great wine.'])
    ->dimensions(1536)
    ->generate(Lab::OpenAI, 'text-embedding-3-small');
```

### Embeddings multimodales

El método `Embeddings::for` no solo acepta cadenas: también admite imágenes, audio, documentos y vídeo, de forma que puedes generar embeddings para contenido distinto del texto. Gemini soporta embeddings de imágenes, audio, documentos y vídeo; VoyageAI soporta embeddings de imágenes y vídeo.

```php theme={null}
use Laravel\Ai\Embeddings;
use Laravel\Ai\Enums\Lab;
use Laravel\Ai\Files\Image;
use Laravel\Ai\Files\Video;

$response = Embeddings::for([
    'A vineyard at sunset.',
    Image::fromStorage('vineyard.jpg'),
    Video::fromPath('/home/laravel/tour.mp4'),
])->generate(Lab::Gemini);
```

Para las entradas multimodales se usan [las mismas clases de archivo que en los adjuntos](#adjuntos). Puedes crear estos archivos a partir de una ruta local, de un disco del sistema de archivos, de una URL remota o de contenido codificado en Base64. Las imágenes, documentos y vídeos también pueden crearse desde archivos subidos, y los documentos pueden crearse desde contenido de cadena en bruto.

```php theme={null}
use Laravel\Ai\Files\Audio;
use Laravel\Ai\Files\Document;
use Laravel\Ai\Files\Image;
use Laravel\Ai\Files\Video;

Image::fromPath('/home/laravel/photo.jpg');
Image::fromStorage('photo.jpg');
Image::fromUpload($request->file('photo'));

Audio::fromPath('/home/laravel/clip.mp3');
Audio::fromStorage('clip.mp3');
Audio::fromUpload($request->file('clip.mp3'));

Video::fromPath('/home/laravel/video.mp4');
Video::fromStorage('video.mp4');
Video::fromUpload($request->file('video'));

Document::fromUrl('https://example.com/report.pdf');
Document::fromString('Laravel is a PHP framework.', 'text/plain');
Document::fromUpload($request->file('report'));
```

<Info>
  VoyageAI no permite mezclar en la misma petición contenido multimedia de URL remotas con contenido codificado en Base64. Los archivos locales, de storage y subidos se envían como contenido codificado en Base64, y las entradas de texto se pueden combinar con cualquiera de las dos fuentes de contenido. Consulta la documentación de cada proveedor para conocer los modelos multimodales y las entradas disponibles.
</Info>

### Búsqueda vectorial (pgvector)

Ejemplo de configuración de búsqueda vectorial con PostgreSQL y la extensión pgvector.

<Steps>
  <Step title="Crear la migración">
    ```php theme={null}
    Schema::ensureVectorExtensionExists();

    Schema::create('documents', function (Blueprint $table) {
        $table->id();
        $table->string('title');
        $table->text('content');
        $table->vector('embedding', dimensions: 1536);
        $table->timestamps();
    });

    // Añadir un índice HNSW
    $table->vector('embedding', dimensions: 1536)->index();
    ```
  </Step>

  <Step title="Configurar el modelo">
    ```php theme={null}
    protected function casts(): array
    {
        return ['embedding' => 'array'];
    }
    ```
  </Step>

  <Step title="Consulta por similitud">
    ```php theme={null}
    // Buscar por vector de embedding
    $documents = Document::query()
        ->whereVectorSimilarTo('embedding', $queryEmbedding, minSimilarity: 0.4)
        ->limit(10)
        ->get();

    // Pasando una cadena, el embedding se genera automáticamente
    $documents = Document::query()
        ->whereVectorSimilarTo('embedding', 'best wineries in Napa Valley')
        ->limit(10)
        ->get();
    ```
  </Step>
</Steps>

También hay métodos de más bajo nivel.

```php theme={null}
$documents = Document::query()
    ->select('*')
    ->selectVectorDistance('embedding', $queryEmbedding, as: 'distance')
    ->whereVectorDistanceLessThan('embedding', $queryEmbedding, maxDistance: 0.3)
    ->orderByVectorDistance('embedding', $queryEmbedding)
    ->limit(10)
    ->get();
```

### Caché de embeddings

Puedes cachear los embeddings para no volver a generarlos con el mismo texto.

Configura los valores por defecto de la caché en `config/ai.php`.

```php theme={null}
'caching' => [
    'embeddings' => [
        'cache' => true,
        'store' => env('CACHE_STORE', 'database'),
    ],
],
```

También puedes controlar la caché por petición.

```php theme={null}
$response = Embeddings::for(['Napa Valley has great wine.'])->cache()->generate();
$response = Embeddings::for(['Napa Valley has great wine.'])->cache(seconds: 3600)->generate();

// Con Stringable funciona igual
$embeddings = Str::of('Napa Valley has great wine.')->toEmbeddings(cache: true);
$embeddings = Str::of('Napa Valley has great wine.')->toEmbeddings(cache: 3600);
```

***

## Reranking

Puedes reordenar (rerank) los resultados de búsqueda según su relevancia para la consulta. Compatible con los proveedores Cohere y Jina.

```php theme={null}
use Laravel\Ai\Reranking;

$response = Reranking::of([
    'Django is a Python web framework.',
    'Laravel is a PHP web application framework.',
    'React is a JavaScript library for building user interfaces.',
])->rerank('PHP frameworks');

$response->first()->document; // "Laravel is a PHP web application framework."
$response->first()->score;    // 0.95
$response->first()->index;    // 1
```

Con `limit()` puedes limitar el número de resultados devueltos.

```php theme={null}
$response = Reranking::of($documents)->limit(5)->rerank('search query');
```

### Reranking de colecciones

Puedes rerankar directamente una colección de Eloquent.

```php theme={null}
// Un solo campo
$posts = Post::all()->rerank('body', 'Laravel tutorials');

// Varios campos (se envían como JSON)
$reranked = $posts->rerank(['title', 'body'], 'Laravel tutorials');

// Generación de texto personalizada mediante closure
$reranked = $posts->rerank(fn ($post) => $post->title.': '.$post->body, 'Laravel tutorials');

// Con opciones
$reranked = $posts->rerank(
    by: 'content',
    query: 'Laravel tutorials',
    limit: 10,
    provider: Lab::Cohere,
);
```

***

## Gestión de archivos

Puedes subir archivos al proveedor de IA para referenciarlos posteriormente.

```php theme={null}
use Laravel\Ai\Files\Document;
use Laravel\Ai\Files\Image;

// Desde una ruta
$response = Document::fromPath('/home/laravel/document.pdf')->put();
$response = Image::fromPath('/home/laravel/photo.jpg')->put();

// Desde storage
$response = Document::fromStorage('document.pdf', disk: 'local')->put();
$response = Image::fromStorage('photo.jpg', disk: 'local')->put();

// Desde una URL
$response = Document::fromUrl('https://example.com/document.pdf')->put();
$response = Image::fromUrl('https://example.com/photo.jpg')->put();

return $response->id;
```

También puedes trabajar con cadenas o con subidas desde formulario.

```php theme={null}
$stored = Document::fromString('Hello, World!', 'text/plain')->put();
$stored = Document::fromUpload($request->file('document'))->put();
```

### Referenciar un archivo ya guardado

Puedes adjuntar al agente un archivo ya subido a partir de su ID.

```php theme={null}
use Laravel\Ai\Files;

$response = (new SalesCoach)->prompt(
    'Analyze the attached sales transcript...',
    attachments: [Files\Document::fromId('file-id')]
);
```

### Obtener y eliminar archivos

```php theme={null}
// Obtener
$file = Document::fromId('file-id')->get();
$file->id;
$file->mimeType();

// Eliminar
Document::fromId('file-id')->delete();
```

### Especificar el proveedor

```php theme={null}
$response = Document::fromPath('/home/laravel/document.pdf')->put(provider: Lab::Anthropic);
```

### Especificar opciones específicas del proveedor

Con el método `withProviderOptions` puedes pasar opciones de subida específicas del proveedor. Por ejemplo, puedes configurar el `purpose` del archivo en OpenAI.

```php theme={null}
use Laravel\Ai\Files\Document;

$response = Document::fromPath('/home/laravel/knowledge.txt')
    ->withProviderOptions(['purpose' => 'assistants'])
    ->put();
```

Si quieres indicar opciones distintas por proveedor, pasa un closure.

```php theme={null}
use Laravel\Ai\Enums\Lab;
use Laravel\Ai\Files\Document;

$response = Document::fromPath('/home/laravel/training.jsonl')
    ->withProviderOptions(fn (Lab|string $provider) => match ($provider) {
        Lab::OpenAI => ['purpose' => 'fine-tune'],
        default => [],
    })
    ->put();
```

***

## Vector Stores

Con los vector stores puedes gestionar los documentos en el lado del proveedor.

```php theme={null}
use Laravel\Ai\Stores;

// Crear
$store = Stores::create('Knowledge Base');
$store = Stores::create(
    name: 'Knowledge Base',
    description: 'Documentation.',
    expiresWhenIdleFor: days(30),
);
return $store->id;

// Obtener
$store = Stores::get('store_id');
$store->id;
$store->name;
$store->fileCounts;
$store->ready;

// Eliminar
Stores::delete('store_id');
$store->delete();
```

### Añadir archivos al store

```php theme={null}
$store = Stores::get('store_id');

// Añadir archivos de distintas maneras
$document = $store->add('file_id');
$document = $store->add(Document::fromId('file_id'));
$document = $store->add(Document::fromPath('/path/to/document.pdf'));
$document = $store->add(Document::fromStorage('manual.pdf'));
$document = $store->add($request->file('document'));

$document->id;
$document->fileId;
```

También puedes asociar metadatos.

```php theme={null}
$store->add(
    Document::fromPath('/path/to/document.pdf'),
    metadata: [
        'author' => 'Taylor Otwell',
        'department' => 'Engineering',
        'year' => 2026,
    ]
);
```

### Eliminar archivos del store

```php theme={null}
$store->remove('file_id');

// Para eliminar también el archivo en sí
$store->remove('file_abc123', deleteFile: true);
```

***

## Failover

Si indicas varios proveedores en un array, cuando el primero falle se hará automáticamente fallback al siguiente.

```php theme={null}
use App\Ai\Agents\SalesCoach;
use Laravel\Ai\Image;

// Failover del agente
$response = (new SalesCoach)->prompt(
    'Analyze this sales transcript...',
    provider: [Lab::OpenAI, Lab::Anthropic],
);

// Failover en generación de imágenes
$image = Image::of('A donut sitting on the kitchen counter')
    ->generate(provider: [Lab::Gemini, Lab::xAI]);
```

***

## Pruebas

Laravel AI SDK ofrece funcionalidad de fakes para pruebas, de forma que puedas testear sin llamar a la API real.

### Pruebas de agentes

```php theme={null}
use App\Ai\Agents\SalesCoach;
use Laravel\Ai\Prompts\AgentPrompt;

// Fake con respuestas fijas
SalesCoach::fake();
SalesCoach::fake(['First response', 'Second response']);

// Respuestas dinámicas con closure
SalesCoach::fake(function (AgentPrompt $prompt) {
    return 'Response for: '.$prompt->prompt;
});

// Aserciones
SalesCoach::assertPrompted('Analyze this...');
SalesCoach::assertPrompted(function (AgentPrompt $prompt) {
    return $prompt->contains('Analyze');
});
SalesCoach::assertNotPrompted('Missing prompt');
SalesCoach::assertNeverPrompted();
```

También hay aserciones para encolado.

```php theme={null}
use Laravel\Ai\QueuedAgentPrompt;

SalesCoach::assertQueued('Analyze this...');
SalesCoach::assertQueued(function (QueuedAgentPrompt $prompt) {
    return $prompt->contains('Analyze');
});
SalesCoach::assertNotQueued('Missing prompt');
SalesCoach::assertNeverQueued();
```

Con `preventStrayPrompts()` se lanza una excepción si se invoca un prompt que no ha sido definido en el fake.

```php theme={null}
SalesCoach::fake()->preventStrayPrompts();
```

Al hacer fake de un agente con salida estructurada, puedes indicar la respuesta como array. El agente devolverá una respuesta estructurada con los datos indicados.

```php theme={null}
SalesCoach::fake([
    ['score' => 87],
]);
```

<Info>
  Si se llama a `fake()` en un agente con salida estructurada sin pasar datos falsos explícitos, Laravel genera automáticamente datos falsos acordes con el esquema definido por el agente.
</Info>

Para probar agentes anónimos, usa `AnonymousAgent::fake()`.

```php theme={null}
use Laravel\Ai\AnonymousAgent;

AnonymousAgent::fake(['Test response']);
```

### Pruebas de generación de imágenes

```php theme={null}
use Laravel\Ai\Image;
use Laravel\Ai\Prompts\ImagePrompt;

Image::fake();
Image::fake([base64_encode($firstImage), base64_encode($secondImage)]);
Image::fake(function (ImagePrompt $prompt) {
    return base64_encode('...');
});

Image::assertGenerated(function (ImagePrompt $prompt) {
    return $prompt->contains('sunset') && $prompt->isLandscape();
});
Image::assertNotGenerated('Missing prompt');
Image::assertNothingGenerated();

Image::assertQueued(fn (QueuedImagePrompt $prompt) => $prompt->contains('sunset'));
Image::assertNotQueued('Missing prompt');
Image::assertNothingQueued();

Image::fake()->preventStrayImages();
```

### Pruebas de síntesis de voz

```php theme={null}
use Laravel\Ai\Audio;
use Laravel\Ai\Prompts\AudioPrompt;

Audio::fake();
Audio::fake([base64_encode($firstAudio), base64_encode($secondAudio)]);
Audio::fake(function (AudioPrompt $prompt) {
    return base64_encode('...');
});

Audio::assertGenerated(function (AudioPrompt $prompt) {
    return $prompt->contains('Hello') && $prompt->isFemale();
});
Audio::assertNotGenerated('Missing prompt');
Audio::assertNothingGenerated();

Audio::assertQueued(fn (QueuedAudioPrompt $prompt) => $prompt->contains('Hello'));
Audio::assertNotQueued('Missing prompt');
Audio::assertNothingQueued();

Audio::fake()->preventStrayAudio();
```

### Pruebas de transcripción

```php theme={null}
use Laravel\Ai\Transcription;
use Laravel\Ai\Prompts\TranscriptionPrompt;

Transcription::fake();
Transcription::fake(['First transcription text.', 'Second transcription text.']);
Transcription::fake(function (TranscriptionPrompt $prompt) {
    return 'Transcribed text...';
});

Transcription::assertGenerated(function (TranscriptionPrompt $prompt) {
    return $prompt->language === 'en' && $prompt->isDiarized();
});
Transcription::assertNotGenerated(fn (TranscriptionPrompt $prompt) => $prompt->language === 'fr');
Transcription::assertNothingGenerated();

Transcription::assertQueued(fn (QueuedTranscriptionPrompt $prompt) => $prompt->isDiarized());
Transcription::assertNotQueued(fn (QueuedTranscriptionPrompt $prompt) => $prompt->language === 'fr');
Transcription::assertNothingQueued();

Transcription::fake()->preventStrayTranscriptions();
```

### Pruebas de embeddings

```php theme={null}
use Laravel\Ai\Embeddings;
use Laravel\Ai\Prompts\EmbeddingsPrompt;

Embeddings::fake();
Embeddings::fake([[$firstEmbeddingVector], [$secondEmbeddingVector]]);
Embeddings::fake(function (EmbeddingsPrompt $prompt) {
    return array_map(
        fn () => Embeddings::fakeEmbedding($prompt->dimensions),
        $prompt->inputs
    );
});

Embeddings::assertGenerated(function (EmbeddingsPrompt $prompt) {
    return $prompt->contains('Laravel') && $prompt->dimensions === 1536;
});
Embeddings::assertNotGenerated(fn (EmbeddingsPrompt $prompt) => $prompt->contains('Other'));
Embeddings::assertNothingGenerated();

Embeddings::assertQueued(fn (QueuedEmbeddingsPrompt $prompt) => $prompt->contains('Laravel'));
Embeddings::assertNotQueued(fn (QueuedEmbeddingsPrompt $prompt) => $prompt->contains('Other'));
Embeddings::assertNothingQueued();

Embeddings::fake()->preventStrayEmbeddings();
```

### Pruebas de reranking

```php theme={null}
use Laravel\Ai\Reranking;
use Laravel\Ai\Prompts\RerankingPrompt;
use Laravel\Ai\Responses\Data\RankedDocument;

Reranking::fake();
Reranking::fake([[
    new RankedDocument(index: 0, document: 'First', score: 0.95),
    new RankedDocument(index: 1, document: 'Second', score: 0.80),
]]);

Reranking::assertReranked(function (RerankingPrompt $prompt) {
    return $prompt->contains('Laravel') && $prompt->limit === 5;
});
Reranking::assertNotReranked(fn (RerankingPrompt $prompt) => $prompt->contains('Django'));
Reranking::assertNothingReranked();
```

### Pruebas de archivos

```php theme={null}
use Laravel\Ai\Files;
use Laravel\Ai\Contracts\Files\StorableFile;
use Laravel\Ai\Files\Document;

Files::fake();

Document::fromString('Hello, Laravel!', mimeType: 'text/plain')->as('hello.txt')->put();

Files::assertStored(fn (StorableFile $file) =>
    (string) $file === 'Hello, Laravel!' && $file->mimeType() === 'text/plain'
);
Files::assertNotStored(fn (StorableFile $file) => (string) $file === 'Hello, World!');
Files::assertNothingStored();

Files::assertDeleted('file-id');
Files::assertNotDeleted('file-id');
Files::assertNothingDeleted();
```

### Pruebas de vector stores

```php theme={null}
use Laravel\Ai\Stores;

Stores::fake(); // Las operaciones sobre archivos también quedan fakeadas

$store = Stores::create('Knowledge Base');

Stores::assertCreated('Knowledge Base');
Stores::assertCreated(fn (string $name, ?string $description) => $name === 'Knowledge Base');
Stores::assertNotCreated('Other Store');
Stores::assertNothingCreated();

Stores::assertDeleted('store_id');
Stores::assertNotDeleted('other_store_id');
Stores::assertNothingDeleted();
```

También puedes hacer aserciones sobre las operaciones de archivo del store.

```php theme={null}
$store = Stores::get('store_id');
$store->add('added_id');
$store->remove('removed_id');

$store->assertAdded('added_id');
$store->assertRemoved('removed_id');
$store->assertNotAdded('other_file_id');
$store->assertNotRemoved('other_file_id');

// Verificar el contenido con un closure
$store->add(Document::fromString('Hello, World!', 'text/plain')->as('hello.txt'));
$store->assertAdded(fn (StorableFile $file) => $file->name() === 'hello.txt');
$store->assertAdded(fn (StorableFile $file) => $file->content() === 'Hello, World!');
```

***

## Eventos

Laravel AI SDK despacha los siguientes eventos. Puedes escucharlos para registrar logs, monitorizar, etc.

<AccordionGroup>
  <Accordion title="Relacionados con agentes">
    * `PromptingAgent` — antes de enviar el prompt
    * `AgentPrompted` — después de enviar el prompt
    * `StreamingAgent` — al iniciar el streaming
    * `AgentStreamed` — al finalizar el streaming
    * `InvokingTool` — antes de invocar la herramienta
    * `ToolInvoked` — después de invocar la herramienta
    * `ToolApprovalRequested` — cuando se solicita aprobación de la herramienta
    * `ToolApprovalResolved` — después de resolverse la aprobación de la herramienta
  </Accordion>

  <Accordion title="Relacionados con imágenes, audio y transcripción">
    * `GeneratingImage` — antes de generar la imagen
    * `ImageGenerated` — después de generar la imagen
    * `GeneratingAudio` — antes de generar el audio
    * `AudioGenerated` — después de generar el audio
    * `GeneratingTranscription` — antes de generar la transcripción
    * `TranscriptionGenerated` — después de generar la transcripción
  </Accordion>

  <Accordion title="Relacionados con embeddings y reranking">
    * `GeneratingEmbeddings` — antes de generar los embeddings
    * `EmbeddingsGenerated` — después de generar los embeddings
    * `Reranking` — antes del reranking
    * `Reranked` — después del reranking
  </Accordion>

  <Accordion title="Relacionados con archivos y stores">
    * `StoringFile` — antes de guardar el archivo
    * `FileStored` — después de guardar el archivo
    * `FileDeleted` — después de eliminar el archivo
    * `CreatingStore` — antes de crear el store
    * `StoreCreated` — después de crear el store
    * `AddingFileToStore` — antes de añadir el archivo al store
    * `FileAddedToStore` — después de añadir el archivo al store
    * `RemovingFileFromStore` — antes de eliminar el archivo del store
    * `FileRemovedFromStore` — después de eliminar el archivo del store
  </Accordion>
</AccordionGroup>


## Related topics

- [Resumen de las novedades de Laravel 13](/es/blog/laravel-13-new-features.md)
- [Actualización de Laravel — Marzo de 2026](/es/blog/changelog/202603.md)
- [Integración con Laravel AI SDK](/es/packages/laravel-copilot-sdk/ai-sdk.md)
- [Integración con Laravel AI SDK - VOICEVOX for Laravel](/es/packages/laravel-voicevox/ai-sdk.md)
- [Driver de Amazon Bedrock para Laravel AI SDK](/es/packages/laravel-amazon-bedrock.md)
