@elyracode/stack-laravel-ai 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -0
- package/extensions/index.ts +12 -0
- package/package.json +27 -0
- package/skills/laravel-ai/SKILL.md +1354 -0
package/README.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# @elyracode/stack-laravel-ai
|
|
2
|
+
|
|
3
|
+
Elyra extension for the **Laravel AI SDK** (`laravel/ai`) -- the complete reference for building AI-powered Laravel applications.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
elyra install npm:@elyracode/stack-laravel-ai
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## What's included
|
|
12
|
+
|
|
13
|
+
Comprehensive knowledge of the entire Laravel AI SDK:
|
|
14
|
+
|
|
15
|
+
- **Agents**: Creating, prompting, conversation context, RemembersConversations
|
|
16
|
+
- **Sub-Agents**: Multi-agent delegation with CanActAsTool, isolated execution
|
|
17
|
+
- **Tools**: Custom tools, provider tools (WebSearch, WebFetch, FileSearch), SimilaritySearch
|
|
18
|
+
- **Structured Output**: JSON schemas with nested objects and arrays
|
|
19
|
+
- **Streaming**: SSE, Vercel AI SDK protocol, broadcasting
|
|
20
|
+
- **Embeddings**: Vector columns (pgvector), similarity queries, caching
|
|
21
|
+
- **Vector Stores**: File indexing, metadata, FileSearch integration
|
|
22
|
+
- **Images**: Generation, reference images, storage
|
|
23
|
+
- **Audio**: TTS, STT, diarization
|
|
24
|
+
- **Reranking**: Semantic reordering with Cohere, Jina, VoyageAI
|
|
25
|
+
- **Files**: Provider file storage, retrieval, deletion
|
|
26
|
+
- **Testing**: Faking agents, images, audio, embeddings, reranking, files, stores
|
|
27
|
+
- **Configuration**: Provider attributes, middleware, failover, provider options
|
|
28
|
+
- **Events**: Full lifecycle event coverage
|
|
29
|
+
|
|
30
|
+
## Usage
|
|
31
|
+
|
|
32
|
+
Just ask Elyra about building AI features in Laravel:
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
> Build a customer service system with a main agent that routes to refund and support sub-agents
|
|
36
|
+
> Create an agent that analyzes sales transcripts with structured output
|
|
37
|
+
> Set up RAG with embeddings and vector stores for my knowledge base
|
|
38
|
+
> Add streaming AI responses to my Laravel app with broadcasting
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The agent generates complete, working Laravel AI SDK code following official patterns.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@elyracode/coding-agent";
|
|
2
|
+
|
|
3
|
+
export default function (elyra: ExtensionAPI) {
|
|
4
|
+
elyra.registerCommand("laravel:ai", {
|
|
5
|
+
description: "Laravel AI SDK quick reference and scaffolding help",
|
|
6
|
+
handler: async (_args, ctx) => {
|
|
7
|
+
ctx.ui.notify(
|
|
8
|
+
"Laravel AI SDK profile loaded. Skills: laravel-ai. Use /skill:laravel-ai for full reference.",
|
|
9
|
+
);
|
|
10
|
+
},
|
|
11
|
+
});
|
|
12
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@elyracode/stack-laravel-ai",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "Elyra extension for Laravel AI SDK -- agents, sub-agents, tools, structured output, embeddings, and more",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"keywords": ["elyra-package", "laravel", "ai-sdk", "agents", "sub-agents", "tools", "embeddings"],
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"author": "Knut W. Horne",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/kwhorne/elyra.git",
|
|
12
|
+
"directory": "packages/stack-laravel-ai"
|
|
13
|
+
},
|
|
14
|
+
"elyra": {
|
|
15
|
+
"skills": ["./skills"],
|
|
16
|
+
"extensions": ["./extensions/index.ts"]
|
|
17
|
+
},
|
|
18
|
+
"peerDependencies": {
|
|
19
|
+
"@elyracode/coding-agent": "*",
|
|
20
|
+
"typebox": "*"
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"clean": "echo 'nothing to clean'",
|
|
24
|
+
"build": "echo 'nothing to build'",
|
|
25
|
+
"check": "echo 'nothing to check'"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,1354 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: laravel-ai
|
|
3
|
+
description: Deep knowledge of the Laravel AI SDK (laravel/ai). Use when building AI agents, tools, sub-agents, structured output, embeddings, vector stores, image generation, audio, reranking, files, or any AI feature in a Laravel application.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Laravel AI SDK Reference
|
|
7
|
+
|
|
8
|
+
## Installation
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
composer require laravel/ai
|
|
12
|
+
php artisan vendor:publish --provider="Laravel\Ai\AiServiceProvider"
|
|
13
|
+
php artisan migrate
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Creates `agent_conversations` and `agent_conversation_messages` tables for conversation storage.
|
|
17
|
+
|
|
18
|
+
## Configuration
|
|
19
|
+
|
|
20
|
+
Set provider keys in `.env`:
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
ANTHROPIC_API_KEY=
|
|
24
|
+
AZURE_OPENAI_API_KEY=
|
|
25
|
+
COHERE_API_KEY=
|
|
26
|
+
DEEPSEEK_API_KEY=
|
|
27
|
+
ELEVENLABS_API_KEY=
|
|
28
|
+
GEMINI_API_KEY=
|
|
29
|
+
GROQ_API_KEY=
|
|
30
|
+
MISTRAL_API_KEY=
|
|
31
|
+
OLLAMA_API_KEY=
|
|
32
|
+
OPENAI_API_KEY=
|
|
33
|
+
OPENROUTER_API_KEY=
|
|
34
|
+
JINA_API_KEY=
|
|
35
|
+
VOYAGEAI_API_KEY=
|
|
36
|
+
XAI_API_KEY=
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Default models for text, images, audio, transcription, and embeddings are configured in `config/ai.php`.
|
|
40
|
+
|
|
41
|
+
### Custom Base URLs
|
|
42
|
+
|
|
43
|
+
Route requests through proxies or alternative endpoints:
|
|
44
|
+
|
|
45
|
+
```php
|
|
46
|
+
// config/ai.php
|
|
47
|
+
'providers' => [
|
|
48
|
+
'openai' => [
|
|
49
|
+
'driver' => 'openai',
|
|
50
|
+
'key' => env('OPENAI_API_KEY'),
|
|
51
|
+
'url' => env('OPENAI_BASE_URL'),
|
|
52
|
+
],
|
|
53
|
+
'anthropic' => [
|
|
54
|
+
'driver' => 'anthropic',
|
|
55
|
+
'key' => env('ANTHROPIC_API_KEY'),
|
|
56
|
+
'url' => env('ANTHROPIC_BASE_URL'),
|
|
57
|
+
],
|
|
58
|
+
],
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Supported for: OpenAI, Anthropic, Gemini, Groq, Cohere, DeepSeek, xAI, OpenRouter.
|
|
62
|
+
|
|
63
|
+
### Lab Enum
|
|
64
|
+
|
|
65
|
+
Reference providers throughout code with `Laravel\Ai\Enums\Lab`:
|
|
66
|
+
|
|
67
|
+
```php
|
|
68
|
+
use Laravel\Ai\Enums\Lab;
|
|
69
|
+
|
|
70
|
+
Lab::Anthropic;
|
|
71
|
+
Lab::OpenAI;
|
|
72
|
+
Lab::Gemini;
|
|
73
|
+
// ...
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Creating Agents
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
php artisan make:agent SalesCoach
|
|
80
|
+
php artisan make:agent SalesCoach --structured
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Agent Class Structure
|
|
84
|
+
|
|
85
|
+
```php
|
|
86
|
+
<?php
|
|
87
|
+
|
|
88
|
+
namespace App\Ai\Agents;
|
|
89
|
+
|
|
90
|
+
use App\Ai\Tools\RetrievePreviousTranscripts;
|
|
91
|
+
use App\Models\History;
|
|
92
|
+
use App\Models\User;
|
|
93
|
+
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
|
94
|
+
use Laravel\Ai\Contracts\Agent;
|
|
95
|
+
use Laravel\Ai\Contracts\Conversational;
|
|
96
|
+
use Laravel\Ai\Contracts\HasStructuredOutput;
|
|
97
|
+
use Laravel\Ai\Contracts\HasTools;
|
|
98
|
+
use Laravel\Ai\Messages\Message;
|
|
99
|
+
use Laravel\Ai\Promptable;
|
|
100
|
+
use Stringable;
|
|
101
|
+
|
|
102
|
+
class SalesCoach implements Agent, Conversational, HasTools, HasStructuredOutput
|
|
103
|
+
{
|
|
104
|
+
use Promptable;
|
|
105
|
+
|
|
106
|
+
public function __construct(public User $user) {}
|
|
107
|
+
|
|
108
|
+
public function instructions(): Stringable|string
|
|
109
|
+
{
|
|
110
|
+
return 'You are a sales coach, analyzing transcripts and providing feedback and an overall sales strength score.';
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
public function messages(): iterable
|
|
114
|
+
{
|
|
115
|
+
return History::where('user_id', $this->user->id)
|
|
116
|
+
->latest()
|
|
117
|
+
->limit(50)
|
|
118
|
+
->get()
|
|
119
|
+
->reverse()
|
|
120
|
+
->map(fn ($message) => new Message($message->role, $message->content))
|
|
121
|
+
->all();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
public function tools(): iterable
|
|
125
|
+
{
|
|
126
|
+
return [
|
|
127
|
+
new RetrievePreviousTranscripts,
|
|
128
|
+
];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
public function schema(JsonSchema $schema): array
|
|
132
|
+
{
|
|
133
|
+
return [
|
|
134
|
+
'feedback' => $schema->string()->required(),
|
|
135
|
+
'score' => $schema->integer()->min(1)->max(10)->required(),
|
|
136
|
+
];
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Key Interfaces
|
|
142
|
+
|
|
143
|
+
- `Agent` -- required for all agents, defines `instructions()`
|
|
144
|
+
- `Conversational` -- enables `messages()` for conversation context
|
|
145
|
+
- `HasTools` -- enables `tools()` returning tool/sub-agent instances
|
|
146
|
+
- `HasStructuredOutput` -- enables `schema()` for JSON output
|
|
147
|
+
- `HasMiddleware` -- enables `middleware()` for prompt interception
|
|
148
|
+
- `HasProviderOptions` -- enables `providerOptions()` for provider-specific config
|
|
149
|
+
- `CanActAsTool` -- allows agent to be used as a sub-agent tool
|
|
150
|
+
|
|
151
|
+
### Key Traits
|
|
152
|
+
|
|
153
|
+
- `Promptable` -- provides `prompt()`, `stream()`, `queue()`, `broadcastOnQueue()`, `make()`, `fake()`
|
|
154
|
+
- `RemembersConversations` -- auto-stores/retrieves conversation history
|
|
155
|
+
|
|
156
|
+
## Prompting
|
|
157
|
+
|
|
158
|
+
```php
|
|
159
|
+
$response = (new SalesCoach)->prompt('Analyze this sales transcript...');
|
|
160
|
+
return (string) $response;
|
|
161
|
+
|
|
162
|
+
// Container resolution with dependency injection
|
|
163
|
+
$agent = SalesCoach::make(user: $user);
|
|
164
|
+
|
|
165
|
+
// Override provider/model/timeout
|
|
166
|
+
$response = (new SalesCoach)->prompt(
|
|
167
|
+
'Analyze this sales transcript...',
|
|
168
|
+
provider: Lab::Anthropic,
|
|
169
|
+
model: 'claude-haiku-4-5-20251001',
|
|
170
|
+
timeout: 120,
|
|
171
|
+
);
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## Conversation Context
|
|
175
|
+
|
|
176
|
+
Implement `Conversational` to return previous messages:
|
|
177
|
+
|
|
178
|
+
```php
|
|
179
|
+
use Laravel\Ai\Messages\Message;
|
|
180
|
+
|
|
181
|
+
public function messages(): iterable
|
|
182
|
+
{
|
|
183
|
+
return History::where('user_id', $this->user->id)
|
|
184
|
+
->latest()->limit(50)->get()->reverse()
|
|
185
|
+
->map(fn ($m) => new Message($m->role, $m->content))->all();
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### RemembersConversations
|
|
190
|
+
|
|
191
|
+
Auto-stores and retrieves conversation history in the database:
|
|
192
|
+
|
|
193
|
+
```php
|
|
194
|
+
use Laravel\Ai\Concerns\RemembersConversations;
|
|
195
|
+
use Laravel\Ai\Contracts\Agent;
|
|
196
|
+
use Laravel\Ai\Contracts\Conversational;
|
|
197
|
+
use Laravel\Ai\Promptable;
|
|
198
|
+
|
|
199
|
+
class SalesCoach implements Agent, Conversational
|
|
200
|
+
{
|
|
201
|
+
use Promptable, RemembersConversations;
|
|
202
|
+
|
|
203
|
+
public function instructions(): string
|
|
204
|
+
{
|
|
205
|
+
return 'You are a sales coach...';
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Start conversation for a user
|
|
210
|
+
$response = (new SalesCoach)->forUser($user)->prompt('Hello!');
|
|
211
|
+
$conversationId = $response->conversationId;
|
|
212
|
+
|
|
213
|
+
// Continue existing conversation
|
|
214
|
+
$response = (new SalesCoach)
|
|
215
|
+
->continue($conversationId, as: $user)
|
|
216
|
+
->prompt('Tell me more about that.');
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## Structured Output
|
|
220
|
+
|
|
221
|
+
Implement `HasStructuredOutput` and define a `schema` method:
|
|
222
|
+
|
|
223
|
+
```php
|
|
224
|
+
public function schema(JsonSchema $schema): array
|
|
225
|
+
{
|
|
226
|
+
return [
|
|
227
|
+
'score' => $schema->integer()->required(),
|
|
228
|
+
];
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
$response = (new SalesCoach)->prompt('Analyze...');
|
|
232
|
+
return $response['score']; // Access like array
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Nested Objects
|
|
236
|
+
|
|
237
|
+
```php
|
|
238
|
+
public function schema(JsonSchema $schema): array
|
|
239
|
+
{
|
|
240
|
+
return [
|
|
241
|
+
'score' => $schema->integer()->required(),
|
|
242
|
+
'metadata' => $schema->object(fn ($schema) => [
|
|
243
|
+
'confidence' => $schema->string()->enum(['low', 'medium', 'high'])->required(),
|
|
244
|
+
'language' => $schema->string()->required(),
|
|
245
|
+
])->required(),
|
|
246
|
+
];
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### Arrays of Objects
|
|
251
|
+
|
|
252
|
+
```php
|
|
253
|
+
public function schema(JsonSchema $schema): array
|
|
254
|
+
{
|
|
255
|
+
return [
|
|
256
|
+
'feedback' => $schema->array()
|
|
257
|
+
->items(
|
|
258
|
+
$schema->object(fn ($schema) => [
|
|
259
|
+
'comment' => $schema->string()->required(),
|
|
260
|
+
'score' => $schema->integer()->required(),
|
|
261
|
+
])
|
|
262
|
+
)
|
|
263
|
+
->required(),
|
|
264
|
+
];
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
## Attachments
|
|
269
|
+
|
|
270
|
+
Pass images and documents with prompts:
|
|
271
|
+
|
|
272
|
+
```php
|
|
273
|
+
use Laravel\Ai\Files;
|
|
274
|
+
|
|
275
|
+
$response = (new SalesCoach)->prompt(
|
|
276
|
+
'Analyze the attached sales transcript...',
|
|
277
|
+
attachments: [
|
|
278
|
+
Files\Document::fromStorage('transcript.pdf'),
|
|
279
|
+
Files\Document::fromPath('/home/laravel/transcript.md'),
|
|
280
|
+
$request->file('transcript'), // UploadedFile
|
|
281
|
+
]
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
// Images
|
|
285
|
+
$response = (new ImageAnalyzer)->prompt(
|
|
286
|
+
'What is in this image?',
|
|
287
|
+
attachments: [
|
|
288
|
+
Files\Image::fromStorage('photo.jpg'),
|
|
289
|
+
Files\Image::fromPath('/home/laravel/photo.jpg'),
|
|
290
|
+
$request->file('photo'),
|
|
291
|
+
]
|
|
292
|
+
);
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
## Streaming
|
|
296
|
+
|
|
297
|
+
```php
|
|
298
|
+
// Return from route (SSE)
|
|
299
|
+
Route::get('/coach', fn () =>
|
|
300
|
+
(new SalesCoach)->stream('Analyze this sales transcript...')
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
// With callback after completion
|
|
304
|
+
(new SalesCoach)
|
|
305
|
+
->stream('Analyze this sales transcript...')
|
|
306
|
+
->then(function (StreamedAgentResponse $response) {
|
|
307
|
+
// $response->text, $response->events, $response->usage
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
// Iterate events manually
|
|
311
|
+
foreach ((new SalesCoach)->stream('Analyze...') as $event) {
|
|
312
|
+
// ...
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Vercel AI SDK protocol
|
|
316
|
+
(new SalesCoach)->stream('Analyze...')
|
|
317
|
+
->usingVercelDataProtocol();
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
## Broadcasting
|
|
321
|
+
|
|
322
|
+
```php
|
|
323
|
+
use Illuminate\Broadcasting\Channel;
|
|
324
|
+
|
|
325
|
+
// Broadcast events manually
|
|
326
|
+
foreach ((new SalesCoach)->stream('Analyze...') as $event) {
|
|
327
|
+
$event->broadcast(new Channel('channel-name'));
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Queue + broadcast
|
|
331
|
+
(new SalesCoach)->broadcastOnQueue(
|
|
332
|
+
'Analyze this sales transcript...',
|
|
333
|
+
new Channel('channel-name'),
|
|
334
|
+
);
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
## Queueing
|
|
338
|
+
|
|
339
|
+
```php
|
|
340
|
+
use Laravel\Ai\Responses\AgentResponse;
|
|
341
|
+
|
|
342
|
+
Route::post('/coach', function (Request $request) {
|
|
343
|
+
(new SalesCoach)
|
|
344
|
+
->queue($request->input('transcript'))
|
|
345
|
+
->then(function (AgentResponse $response) {
|
|
346
|
+
// ...
|
|
347
|
+
})
|
|
348
|
+
->catch(function (Throwable $e) {
|
|
349
|
+
// ...
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
return back();
|
|
353
|
+
});
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
## Tools
|
|
357
|
+
|
|
358
|
+
```bash
|
|
359
|
+
php artisan make:tool RandomNumberGenerator
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
### Tool Class Structure
|
|
363
|
+
|
|
364
|
+
```php
|
|
365
|
+
<?php
|
|
366
|
+
|
|
367
|
+
namespace App\Ai\Tools;
|
|
368
|
+
|
|
369
|
+
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
|
370
|
+
use Laravel\Ai\Contracts\Tool;
|
|
371
|
+
use Laravel\Ai\Tools\Request;
|
|
372
|
+
use Stringable;
|
|
373
|
+
|
|
374
|
+
class RandomNumberGenerator implements Tool
|
|
375
|
+
{
|
|
376
|
+
public function description(): Stringable|string
|
|
377
|
+
{
|
|
378
|
+
return 'This tool may be used to generate cryptographically secure random numbers.';
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
public function handle(Request $request): Stringable|string
|
|
382
|
+
{
|
|
383
|
+
return (string) random_int($request['min'], $request['max']);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
public function schema(JsonSchema $schema): array
|
|
387
|
+
{
|
|
388
|
+
return [
|
|
389
|
+
'min' => $schema->integer()->min(0)->required(),
|
|
390
|
+
'max' => $schema->integer()->required(),
|
|
391
|
+
];
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
### SimilaritySearch Tool
|
|
397
|
+
|
|
398
|
+
For RAG -- agents search vector embeddings stored in the database:
|
|
399
|
+
|
|
400
|
+
```php
|
|
401
|
+
use App\Models\Document;
|
|
402
|
+
use Laravel\Ai\Tools\SimilaritySearch;
|
|
403
|
+
|
|
404
|
+
public function tools(): iterable
|
|
405
|
+
{
|
|
406
|
+
return [
|
|
407
|
+
SimilaritySearch::usingModel(Document::class, 'embedding'),
|
|
408
|
+
];
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// With filtering options
|
|
412
|
+
SimilaritySearch::usingModel(
|
|
413
|
+
model: Document::class,
|
|
414
|
+
column: 'embedding',
|
|
415
|
+
minSimilarity: 0.7,
|
|
416
|
+
limit: 10,
|
|
417
|
+
query: fn ($query) => $query->where('published', true),
|
|
418
|
+
),
|
|
419
|
+
|
|
420
|
+
// Custom closure for full control
|
|
421
|
+
new SimilaritySearch(using: function (string $query) {
|
|
422
|
+
return Document::query()
|
|
423
|
+
->where('user_id', $this->user->id)
|
|
424
|
+
->whereVectorSimilarTo('embedding', $query)
|
|
425
|
+
->limit(10)
|
|
426
|
+
->get();
|
|
427
|
+
}),
|
|
428
|
+
|
|
429
|
+
// Custom description
|
|
430
|
+
SimilaritySearch::usingModel(Document::class, 'embedding')
|
|
431
|
+
->withDescription('Search the knowledge base for relevant articles.'),
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
### Provider Tools
|
|
435
|
+
|
|
436
|
+
Native tools executed by the AI provider, not your application.
|
|
437
|
+
|
|
438
|
+
#### WebSearch
|
|
439
|
+
|
|
440
|
+
Supported: Anthropic, OpenAI, Gemini
|
|
441
|
+
|
|
442
|
+
```php
|
|
443
|
+
use Laravel\Ai\Providers\Tools\WebSearch;
|
|
444
|
+
|
|
445
|
+
public function tools(): iterable
|
|
446
|
+
{
|
|
447
|
+
return [
|
|
448
|
+
new WebSearch,
|
|
449
|
+
// With limits and domain restrictions
|
|
450
|
+
(new WebSearch)->max(5)->allow(['laravel.com', 'php.net']),
|
|
451
|
+
// With location
|
|
452
|
+
(new WebSearch)->location(city: 'New York', region: 'NY', country: 'US'),
|
|
453
|
+
];
|
|
454
|
+
}
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
#### WebFetch
|
|
458
|
+
|
|
459
|
+
Supported: Anthropic, Gemini
|
|
460
|
+
|
|
461
|
+
```php
|
|
462
|
+
use Laravel\Ai\Providers\Tools\WebFetch;
|
|
463
|
+
|
|
464
|
+
public function tools(): iterable
|
|
465
|
+
{
|
|
466
|
+
return [
|
|
467
|
+
new WebFetch,
|
|
468
|
+
(new WebFetch)->max(3)->allow(['docs.laravel.com']),
|
|
469
|
+
];
|
|
470
|
+
}
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
#### FileSearch
|
|
474
|
+
|
|
475
|
+
Supported: OpenAI, Gemini
|
|
476
|
+
|
|
477
|
+
```php
|
|
478
|
+
use Laravel\Ai\Providers\Tools\FileSearch;
|
|
479
|
+
use Laravel\Ai\Providers\Tools\FileSearchQuery;
|
|
480
|
+
|
|
481
|
+
public function tools(): iterable
|
|
482
|
+
{
|
|
483
|
+
return [
|
|
484
|
+
new FileSearch(stores: ['store_id']),
|
|
485
|
+
// Multiple stores
|
|
486
|
+
new FileSearch(stores: ['store_1', 'store_2']),
|
|
487
|
+
// Simple metadata filter
|
|
488
|
+
new FileSearch(stores: ['store_id'], where: [
|
|
489
|
+
'author' => 'Taylor Otwell',
|
|
490
|
+
'year' => 2026,
|
|
491
|
+
]),
|
|
492
|
+
// Complex metadata filter
|
|
493
|
+
new FileSearch(stores: ['store_id'], where: fn (FileSearchQuery $query) =>
|
|
494
|
+
$query->where('author', 'Taylor Otwell')
|
|
495
|
+
->whereNot('status', 'draft')
|
|
496
|
+
->whereIn('category', ['news', 'updates'])
|
|
497
|
+
),
|
|
498
|
+
];
|
|
499
|
+
}
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
## Sub-Agents
|
|
503
|
+
|
|
504
|
+
Sub-agents are agents returned as tools from another agent. The parent delegates tasks to specialized sub-agents. Each sub-agent runs in isolation -- it does NOT receive the parent's conversation history.
|
|
505
|
+
|
|
506
|
+
### Parent Agent
|
|
507
|
+
|
|
508
|
+
```php
|
|
509
|
+
class CustomerSupportAgent implements Agent, HasTools
|
|
510
|
+
{
|
|
511
|
+
use Promptable;
|
|
512
|
+
|
|
513
|
+
public function instructions(): string
|
|
514
|
+
{
|
|
515
|
+
return 'You help customers with account, order, and billing questions. Delegate refund policy questions to the refunds specialist.';
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
public function tools(): iterable
|
|
519
|
+
{
|
|
520
|
+
return [
|
|
521
|
+
new RefundsAgent,
|
|
522
|
+
new TechnicalSupportAgent,
|
|
523
|
+
];
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
### Sub-Agent with CanActAsTool
|
|
529
|
+
|
|
530
|
+
```php
|
|
531
|
+
use Laravel\Ai\Attributes\Provider;
|
|
532
|
+
use Laravel\Ai\Contracts\Agent;
|
|
533
|
+
use Laravel\Ai\Contracts\CanActAsTool;
|
|
534
|
+
use Laravel\Ai\Contracts\HasTools;
|
|
535
|
+
use Laravel\Ai\Enums\Lab;
|
|
536
|
+
use Laravel\Ai\Promptable;
|
|
537
|
+
|
|
538
|
+
#[Provider(Lab::Anthropic)]
|
|
539
|
+
class RefundsAgent implements Agent, CanActAsTool, HasTools
|
|
540
|
+
{
|
|
541
|
+
use Promptable;
|
|
542
|
+
|
|
543
|
+
public function instructions(): string
|
|
544
|
+
{
|
|
545
|
+
return 'You are a refunds specialist. Use order details and the refund policy to give concise eligibility guidance.';
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
public function name(): string
|
|
549
|
+
{
|
|
550
|
+
return 'refunds_specialist';
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
public function description(): string
|
|
554
|
+
{
|
|
555
|
+
return 'Determine whether an order is eligible for a refund and explain the next step.';
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
public function tools(): iterable
|
|
559
|
+
{
|
|
560
|
+
return [new LookupOrder];
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
If a sub-agent does not implement `CanActAsTool`, Laravel uses the class basename as the tool name and a generic description.
|
|
566
|
+
|
|
567
|
+
## Middleware
|
|
568
|
+
|
|
569
|
+
```bash
|
|
570
|
+
php artisan make:agent-middleware LogPrompts
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
```php
|
|
574
|
+
<?php
|
|
575
|
+
|
|
576
|
+
namespace App\Ai\Middleware;
|
|
577
|
+
|
|
578
|
+
use Closure;
|
|
579
|
+
use Laravel\Ai\Prompts\AgentPrompt;
|
|
580
|
+
use Laravel\Ai\Responses\AgentResponse;
|
|
581
|
+
|
|
582
|
+
class LogPrompts
|
|
583
|
+
{
|
|
584
|
+
public function handle(AgentPrompt $prompt, Closure $next)
|
|
585
|
+
{
|
|
586
|
+
Log::info('Prompting agent', ['prompt' => $prompt->prompt]);
|
|
587
|
+
|
|
588
|
+
return $next($prompt)->then(function (AgentResponse $response) {
|
|
589
|
+
Log::info('Agent responded', ['text' => $response->text]);
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
On the agent:
|
|
596
|
+
|
|
597
|
+
```php
|
|
598
|
+
use Laravel\Ai\Contracts\HasMiddleware;
|
|
599
|
+
|
|
600
|
+
class SalesCoach implements Agent, HasMiddleware
|
|
601
|
+
{
|
|
602
|
+
use Promptable;
|
|
603
|
+
|
|
604
|
+
public function middleware(): array
|
|
605
|
+
{
|
|
606
|
+
return [new LogPrompts];
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
## Anonymous Agents
|
|
612
|
+
|
|
613
|
+
Quick ad-hoc agents without a dedicated class:
|
|
614
|
+
|
|
615
|
+
```php
|
|
616
|
+
use function Laravel\Ai\{agent};
|
|
617
|
+
|
|
618
|
+
$response = agent(
|
|
619
|
+
instructions: 'You are an expert at software development.',
|
|
620
|
+
messages: [],
|
|
621
|
+
tools: [],
|
|
622
|
+
)->prompt('Tell me about Laravel');
|
|
623
|
+
|
|
624
|
+
// With structured output
|
|
625
|
+
$response = agent(
|
|
626
|
+
schema: fn (JsonSchema $schema) => [
|
|
627
|
+
'number' => $schema->integer()->required(),
|
|
628
|
+
],
|
|
629
|
+
)->prompt('Generate a random number less than 100');
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
## Agent Configuration (Attributes)
|
|
633
|
+
|
|
634
|
+
```php
|
|
635
|
+
use Laravel\Ai\Attributes\{Provider, Model, MaxSteps, MaxTokens, Temperature, Timeout, TopP, UseCheapestModel, UseSmartestModel};
|
|
636
|
+
use Laravel\Ai\Enums\Lab;
|
|
637
|
+
|
|
638
|
+
#[Provider(Lab::Anthropic)]
|
|
639
|
+
#[Model('claude-haiku-4-5-20251001')]
|
|
640
|
+
#[MaxSteps(10)]
|
|
641
|
+
#[MaxTokens(4096)]
|
|
642
|
+
#[Temperature(0.7)]
|
|
643
|
+
#[Timeout(120)]
|
|
644
|
+
#[TopP(0.9)]
|
|
645
|
+
class SalesCoach implements Agent
|
|
646
|
+
{
|
|
647
|
+
use Promptable;
|
|
648
|
+
// ...
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// Cost optimization -- cheapest model for the provider
|
|
652
|
+
#[UseCheapestModel]
|
|
653
|
+
class SimpleSummarizer implements Agent { use Promptable; }
|
|
654
|
+
|
|
655
|
+
// Maximum capability -- most capable model for the provider
|
|
656
|
+
#[UseSmartestModel]
|
|
657
|
+
class ComplexReasoner implements Agent { use Promptable; }
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
Available attributes:
|
|
661
|
+
- `MaxSteps` -- max tool-use steps
|
|
662
|
+
- `MaxTokens` -- max generated tokens
|
|
663
|
+
- `Model` -- specific model name
|
|
664
|
+
- `Provider` -- AI provider (or array for failover)
|
|
665
|
+
- `Temperature` -- sampling temperature (0.0-1.0)
|
|
666
|
+
- `Timeout` -- HTTP timeout in seconds (default: 60)
|
|
667
|
+
- `TopP` -- nucleus sampling probability (0.0-1.0)
|
|
668
|
+
- `UseCheapestModel` -- cheapest model for cost optimization
|
|
669
|
+
- `UseSmartestModel` -- most capable model for complex tasks
|
|
670
|
+
|
|
671
|
+
## Provider Options
|
|
672
|
+
|
|
673
|
+
For provider-specific options (OpenAI reasoning, Anthropic thinking, penalties):
|
|
674
|
+
|
|
675
|
+
```php
|
|
676
|
+
use Laravel\Ai\Contracts\HasProviderOptions;
|
|
677
|
+
use Laravel\Ai\Enums\Lab;
|
|
678
|
+
|
|
679
|
+
class SalesCoach implements Agent, HasProviderOptions
|
|
680
|
+
{
|
|
681
|
+
use Promptable;
|
|
682
|
+
|
|
683
|
+
public function providerOptions(Lab|string $provider): array
|
|
684
|
+
{
|
|
685
|
+
return match ($provider) {
|
|
686
|
+
Lab::OpenAI => [
|
|
687
|
+
'reasoning' => ['effort' => 'low'],
|
|
688
|
+
'frequency_penalty' => 0.5,
|
|
689
|
+
'presence_penalty' => 0.3,
|
|
690
|
+
],
|
|
691
|
+
Lab::Anthropic => [
|
|
692
|
+
'thinking' => ['budget_tokens' => 1024],
|
|
693
|
+
'cache_control' => ['type' => 'ephemeral'],
|
|
694
|
+
],
|
|
695
|
+
default => [],
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
The method receives the current provider, useful with failover to return different options per provider.
|
|
702
|
+
|
|
703
|
+
## Images
|
|
704
|
+
|
|
705
|
+
```php
|
|
706
|
+
use Laravel\Ai\Image;
|
|
707
|
+
|
|
708
|
+
$image = Image::of('A donut sitting on the kitchen counter')->generate();
|
|
709
|
+
$rawContent = (string) $image;
|
|
710
|
+
|
|
711
|
+
// With options
|
|
712
|
+
$image = Image::of('A donut sitting on the kitchen counter')
|
|
713
|
+
->quality('high') // 'high', 'medium', 'low'
|
|
714
|
+
->landscape() // or ->portrait(), ->square()
|
|
715
|
+
->timeout(120)
|
|
716
|
+
->generate();
|
|
717
|
+
|
|
718
|
+
// With reference images
|
|
719
|
+
$image = Image::of('Update this photo to impressionist style.')
|
|
720
|
+
->attachments([
|
|
721
|
+
Files\Image::fromStorage('photo.jpg'),
|
|
722
|
+
// Files\Image::fromPath('/home/laravel/photo.jpg'),
|
|
723
|
+
// Files\Image::fromUrl('https://example.com/photo.jpg'),
|
|
724
|
+
// $request->file('photo'),
|
|
725
|
+
])
|
|
726
|
+
->landscape()
|
|
727
|
+
->generate();
|
|
728
|
+
|
|
729
|
+
// Storage
|
|
730
|
+
$path = $image->store();
|
|
731
|
+
$path = $image->storeAs('image.jpg');
|
|
732
|
+
$path = $image->storePublicly();
|
|
733
|
+
$path = $image->storePubliclyAs('image.jpg');
|
|
734
|
+
|
|
735
|
+
// Queue
|
|
736
|
+
Image::of('A donut on the counter')
|
|
737
|
+
->portrait()
|
|
738
|
+
->queue()
|
|
739
|
+
->then(function (ImageResponse $image) {
|
|
740
|
+
$path = $image->store();
|
|
741
|
+
});
|
|
742
|
+
```
|
|
743
|
+
|
|
744
|
+
## Audio (TTS)
|
|
745
|
+
|
|
746
|
+
```php
|
|
747
|
+
use Laravel\Ai\Audio;
|
|
748
|
+
use Illuminate\Support\Str;
|
|
749
|
+
|
|
750
|
+
$audio = Audio::of('I love coding with Laravel.')->generate();
|
|
751
|
+
$rawContent = (string) $audio;
|
|
752
|
+
|
|
753
|
+
// Via Stringable
|
|
754
|
+
$audio = Str::of('I love coding with Laravel.')->toAudio();
|
|
755
|
+
|
|
756
|
+
// Voice options
|
|
757
|
+
$audio = Audio::of('I love coding with Laravel.')
|
|
758
|
+
->female() // or ->male()
|
|
759
|
+
->generate();
|
|
760
|
+
|
|
761
|
+
$audio = Audio::of('I love coding with Laravel.')
|
|
762
|
+
->voice('voice-id-or-name')
|
|
763
|
+
->generate();
|
|
764
|
+
|
|
765
|
+
// With instructions
|
|
766
|
+
$audio = Audio::of('I love coding with Laravel.')
|
|
767
|
+
->female()
|
|
768
|
+
->instructions('Said like a pirate')
|
|
769
|
+
->generate();
|
|
770
|
+
|
|
771
|
+
// Storage
|
|
772
|
+
$path = $audio->store();
|
|
773
|
+
$path = $audio->storeAs('audio.mp3');
|
|
774
|
+
$path = $audio->storePublicly();
|
|
775
|
+
$path = $audio->storePubliclyAs('audio.mp3');
|
|
776
|
+
|
|
777
|
+
// Queue
|
|
778
|
+
Audio::of('I love coding with Laravel.')
|
|
779
|
+
->queue()
|
|
780
|
+
->then(function (AudioResponse $audio) {
|
|
781
|
+
$path = $audio->store();
|
|
782
|
+
});
|
|
783
|
+
```
|
|
784
|
+
|
|
785
|
+
## Transcription (STT)
|
|
786
|
+
|
|
787
|
+
```php
|
|
788
|
+
use Laravel\Ai\Transcription;
|
|
789
|
+
|
|
790
|
+
$transcript = Transcription::fromPath('/home/laravel/audio.mp3')->generate();
|
|
791
|
+
$transcript = Transcription::fromStorage('audio.mp3')->generate();
|
|
792
|
+
$transcript = Transcription::fromUpload($request->file('audio'))->generate();
|
|
793
|
+
|
|
794
|
+
return (string) $transcript;
|
|
795
|
+
|
|
796
|
+
// With diarization (speaker segmentation)
|
|
797
|
+
$transcript = Transcription::fromStorage('audio.mp3')
|
|
798
|
+
->diarize()
|
|
799
|
+
->generate();
|
|
800
|
+
|
|
801
|
+
// Queue
|
|
802
|
+
Transcription::fromStorage('audio.mp3')
|
|
803
|
+
->queue()
|
|
804
|
+
->then(function (TranscriptionResponse $transcript) {
|
|
805
|
+
// ...
|
|
806
|
+
});
|
|
807
|
+
```
|
|
808
|
+
|
|
809
|
+
## Embeddings
|
|
810
|
+
|
|
811
|
+
```php
|
|
812
|
+
use Illuminate\Support\Str;
|
|
813
|
+
use Laravel\Ai\Embeddings;
|
|
814
|
+
|
|
815
|
+
// Single via Stringable
|
|
816
|
+
$embeddings = Str::of('Napa Valley has great wine.')->toEmbeddings();
|
|
817
|
+
|
|
818
|
+
// Multiple inputs
|
|
819
|
+
$response = Embeddings::for([
|
|
820
|
+
'Napa Valley has great wine.',
|
|
821
|
+
'Laravel is a PHP framework.',
|
|
822
|
+
])->generate();
|
|
823
|
+
|
|
824
|
+
$response->embeddings; // [[0.123, 0.456, ...], [0.789, 0.012, ...]]
|
|
825
|
+
|
|
826
|
+
// With dimensions and provider
|
|
827
|
+
$response = Embeddings::for(['Napa Valley has great wine.'])
|
|
828
|
+
->dimensions(1536)
|
|
829
|
+
->generate(Lab::OpenAI, 'text-embedding-3-small');
|
|
830
|
+
```
|
|
831
|
+
|
|
832
|
+
### Querying Embeddings (PostgreSQL + pgvector)
|
|
833
|
+
|
|
834
|
+
Migration:
|
|
835
|
+
|
|
836
|
+
```php
|
|
837
|
+
Schema::ensureVectorExtensionExists();
|
|
838
|
+
|
|
839
|
+
Schema::create('documents', function (Blueprint $table) {
|
|
840
|
+
$table->id();
|
|
841
|
+
$table->string('title');
|
|
842
|
+
$table->text('content');
|
|
843
|
+
$table->vector('embedding', dimensions: 1536)->index();
|
|
844
|
+
$table->timestamps();
|
|
845
|
+
});
|
|
846
|
+
```
|
|
847
|
+
|
|
848
|
+
Model cast:
|
|
849
|
+
|
|
850
|
+
```php
|
|
851
|
+
protected function casts(): array
|
|
852
|
+
{
|
|
853
|
+
return ['embedding' => 'array'];
|
|
854
|
+
}
|
|
855
|
+
```
|
|
856
|
+
|
|
857
|
+
Queries:
|
|
858
|
+
|
|
859
|
+
```php
|
|
860
|
+
use App\Models\Document;
|
|
861
|
+
|
|
862
|
+
// whereVectorSimilarTo accepts array of floats or a plain string
|
|
863
|
+
// When a string is given, Laravel auto-generates embeddings
|
|
864
|
+
$documents = Document::query()
|
|
865
|
+
->whereVectorSimilarTo('embedding', 'best wineries in Napa Valley')
|
|
866
|
+
->limit(10)
|
|
867
|
+
->get();
|
|
868
|
+
|
|
869
|
+
// With minimum similarity threshold
|
|
870
|
+
$documents = Document::query()
|
|
871
|
+
->whereVectorSimilarTo('embedding', $queryEmbedding, minSimilarity: 0.4)
|
|
872
|
+
->limit(10)
|
|
873
|
+
->get();
|
|
874
|
+
|
|
875
|
+
// Lower-level methods for full control
|
|
876
|
+
$documents = Document::query()
|
|
877
|
+
->select('*')
|
|
878
|
+
->selectVectorDistance('embedding', $queryEmbedding, as: 'distance')
|
|
879
|
+
->whereVectorDistanceLessThan('embedding', $queryEmbedding, maxDistance: 0.3)
|
|
880
|
+
->orderByVectorDistance('embedding', $queryEmbedding)
|
|
881
|
+
->limit(10)
|
|
882
|
+
->get();
|
|
883
|
+
```
|
|
884
|
+
|
|
885
|
+
### Caching Embeddings
|
|
886
|
+
|
|
887
|
+
Enable globally in `config/ai.php`:
|
|
888
|
+
|
|
889
|
+
```php
|
|
890
|
+
'caching' => [
|
|
891
|
+
'embeddings' => [
|
|
892
|
+
'cache' => true,
|
|
893
|
+
'store' => env('CACHE_STORE', 'database'),
|
|
894
|
+
],
|
|
895
|
+
],
|
|
896
|
+
```
|
|
897
|
+
|
|
898
|
+
Or per-request:
|
|
899
|
+
|
|
900
|
+
```php
|
|
901
|
+
$response = Embeddings::for(['Napa Valley has great wine.'])
|
|
902
|
+
->cache()
|
|
903
|
+
->generate();
|
|
904
|
+
|
|
905
|
+
// Custom duration
|
|
906
|
+
$response = Embeddings::for(['...'])
|
|
907
|
+
->cache(seconds: 3600)
|
|
908
|
+
->generate();
|
|
909
|
+
|
|
910
|
+
// Via Stringable
|
|
911
|
+
$embeddings = Str::of('text')->toEmbeddings(cache: true);
|
|
912
|
+
$embeddings = Str::of('text')->toEmbeddings(cache: 3600);
|
|
913
|
+
```
|
|
914
|
+
|
|
915
|
+
## Reranking
|
|
916
|
+
|
|
917
|
+
Reorder documents by semantic relevance to a query:
|
|
918
|
+
|
|
919
|
+
```php
|
|
920
|
+
use Laravel\Ai\Reranking;
|
|
921
|
+
|
|
922
|
+
$response = Reranking::of([
|
|
923
|
+
'Django is a Python web framework.',
|
|
924
|
+
'Laravel is a PHP web application framework.',
|
|
925
|
+
'React is a JavaScript library for building user interfaces.',
|
|
926
|
+
])->rerank('PHP frameworks');
|
|
927
|
+
|
|
928
|
+
$response->first()->document; // "Laravel is a PHP web application framework."
|
|
929
|
+
$response->first()->score; // 0.95
|
|
930
|
+
$response->first()->index; // 1 (original position)
|
|
931
|
+
|
|
932
|
+
// Limit results
|
|
933
|
+
$response = Reranking::of($documents)
|
|
934
|
+
->limit(5)
|
|
935
|
+
->rerank('search query');
|
|
936
|
+
```
|
|
937
|
+
|
|
938
|
+
### Reranking Collections
|
|
939
|
+
|
|
940
|
+
```php
|
|
941
|
+
// Rerank by a single field
|
|
942
|
+
$posts = Post::all()->rerank('body', 'Laravel tutorials');
|
|
943
|
+
|
|
944
|
+
// Rerank by multiple fields (sent as JSON)
|
|
945
|
+
$reranked = $posts->rerank(['title', 'body'], 'Laravel tutorials');
|
|
946
|
+
|
|
947
|
+
// Rerank using a closure
|
|
948
|
+
$reranked = $posts->rerank(
|
|
949
|
+
fn ($post) => $post->title.': '.$post->body,
|
|
950
|
+
'Laravel tutorials'
|
|
951
|
+
);
|
|
952
|
+
|
|
953
|
+
// With limit and provider
|
|
954
|
+
$reranked = $posts->rerank(
|
|
955
|
+
by: 'content',
|
|
956
|
+
query: 'Laravel tutorials',
|
|
957
|
+
limit: 10,
|
|
958
|
+
provider: Lab::Cohere
|
|
959
|
+
);
|
|
960
|
+
```
|
|
961
|
+
|
|
962
|
+
## Files
|
|
963
|
+
|
|
964
|
+
Store files with AI providers for reuse in conversations without re-uploading:
|
|
965
|
+
|
|
966
|
+
```php
|
|
967
|
+
use Laravel\Ai\Files\Document;
|
|
968
|
+
use Laravel\Ai\Files\Image;
|
|
969
|
+
|
|
970
|
+
// Store from various sources
|
|
971
|
+
$response = Document::fromPath('/home/laravel/document.pdf')->put();
|
|
972
|
+
$response = Document::fromStorage('document.pdf', disk: 'local')->put();
|
|
973
|
+
$response = Document::fromUrl('https://example.com/document.pdf')->put();
|
|
974
|
+
$response = Document::fromString('Hello, World!', 'text/plain')->put();
|
|
975
|
+
$response = Document::fromUpload($request->file('document'))->put();
|
|
976
|
+
|
|
977
|
+
$response = Image::fromPath('/home/laravel/photo.jpg')->put();
|
|
978
|
+
$response = Image::fromStorage('photo.jpg', disk: 'local')->put();
|
|
979
|
+
$response = Image::fromUrl('https://example.com/photo.jpg')->put();
|
|
980
|
+
|
|
981
|
+
$fileId = $response->id;
|
|
982
|
+
|
|
983
|
+
// Reference stored file in conversations
|
|
984
|
+
$response = (new SalesCoach)->prompt(
|
|
985
|
+
'Analyze the attached sales transcript...',
|
|
986
|
+
attachments: [
|
|
987
|
+
Document::fromId('file-id'),
|
|
988
|
+
]
|
|
989
|
+
);
|
|
990
|
+
|
|
991
|
+
// Retrieve and delete
|
|
992
|
+
$file = Document::fromId('file-id')->get();
|
|
993
|
+
$file->id;
|
|
994
|
+
$file->mimeType();
|
|
995
|
+
|
|
996
|
+
Document::fromId('file-id')->delete();
|
|
997
|
+
|
|
998
|
+
// Specify provider
|
|
999
|
+
$response = Document::fromPath('/path/to/doc.pdf')
|
|
1000
|
+
->put(provider: Lab::Anthropic);
|
|
1001
|
+
```
|
|
1002
|
+
|
|
1003
|
+
## Vector Stores
|
|
1004
|
+
|
|
1005
|
+
Searchable file collections for RAG:
|
|
1006
|
+
|
|
1007
|
+
```php
|
|
1008
|
+
use Laravel\Ai\Stores;
|
|
1009
|
+
use Laravel\Ai\Files\Document;
|
|
1010
|
+
|
|
1011
|
+
// Create
|
|
1012
|
+
$store = Stores::create('Knowledge Base');
|
|
1013
|
+
$store = Stores::create(
|
|
1014
|
+
name: 'Knowledge Base',
|
|
1015
|
+
description: 'Documentation and reference materials.',
|
|
1016
|
+
expiresWhenIdleFor: days(30),
|
|
1017
|
+
);
|
|
1018
|
+
|
|
1019
|
+
$storeId = $store->id;
|
|
1020
|
+
|
|
1021
|
+
// Retrieve
|
|
1022
|
+
$store = Stores::get('store_id');
|
|
1023
|
+
$store->id;
|
|
1024
|
+
$store->name;
|
|
1025
|
+
$store->fileCounts;
|
|
1026
|
+
$store->ready;
|
|
1027
|
+
|
|
1028
|
+
// Delete
|
|
1029
|
+
Stores::delete('store_id');
|
|
1030
|
+
$store->delete();
|
|
1031
|
+
```
|
|
1032
|
+
|
|
1033
|
+
### Adding Files to Stores
|
|
1034
|
+
|
|
1035
|
+
```php
|
|
1036
|
+
$store = Stores::get('store_id');
|
|
1037
|
+
|
|
1038
|
+
// Add already-stored files
|
|
1039
|
+
$document = $store->add('file_id');
|
|
1040
|
+
$document = $store->add(Document::fromId('file_id'));
|
|
1041
|
+
|
|
1042
|
+
// Store and add in one step
|
|
1043
|
+
$document = $store->add(Document::fromPath('/path/to/document.pdf'));
|
|
1044
|
+
$document = $store->add(Document::fromStorage('manual.pdf'));
|
|
1045
|
+
$document = $store->add($request->file('document'));
|
|
1046
|
+
|
|
1047
|
+
$document->id;
|
|
1048
|
+
$document->fileId;
|
|
1049
|
+
|
|
1050
|
+
// With metadata (for filtering with FileSearch)
|
|
1051
|
+
$store->add(Document::fromPath('/path/to/document.pdf'), metadata: [
|
|
1052
|
+
'author' => 'Taylor Otwell',
|
|
1053
|
+
'department' => 'Engineering',
|
|
1054
|
+
'year' => 2026,
|
|
1055
|
+
]);
|
|
1056
|
+
|
|
1057
|
+
// Remove from store
|
|
1058
|
+
$store->remove('file_id');
|
|
1059
|
+
$store->remove('file_abc123', deleteFile: true); // Also delete from provider storage
|
|
1060
|
+
|
|
1061
|
+
// Use with FileSearch tool
|
|
1062
|
+
new FileSearch(stores: [$store->id]);
|
|
1063
|
+
```
|
|
1064
|
+
|
|
1065
|
+
## Failover
|
|
1066
|
+
|
|
1067
|
+
Provide an array of providers/models for automatic failover:
|
|
1068
|
+
|
|
1069
|
+
```php
|
|
1070
|
+
use Laravel\Ai\Image;
|
|
1071
|
+
|
|
1072
|
+
$response = (new SalesCoach)->prompt(
|
|
1073
|
+
'Analyze this sales transcript...',
|
|
1074
|
+
provider: [Lab::OpenAI, Lab::Anthropic],
|
|
1075
|
+
);
|
|
1076
|
+
|
|
1077
|
+
$image = Image::of('A donut on the counter')
|
|
1078
|
+
->generate(provider: [Lab::Gemini, Lab::xAI]);
|
|
1079
|
+
```
|
|
1080
|
+
|
|
1081
|
+
## Testing
|
|
1082
|
+
|
|
1083
|
+
### Agents
|
|
1084
|
+
|
|
1085
|
+
```php
|
|
1086
|
+
use App\Ai\Agents\SalesCoach;
|
|
1087
|
+
use Laravel\Ai\Prompts\AgentPrompt;
|
|
1088
|
+
|
|
1089
|
+
// Fixed response for every prompt
|
|
1090
|
+
SalesCoach::fake();
|
|
1091
|
+
|
|
1092
|
+
// Ordered responses
|
|
1093
|
+
SalesCoach::fake(['First response', 'Second response']);
|
|
1094
|
+
|
|
1095
|
+
// Dynamic responses
|
|
1096
|
+
SalesCoach::fake(fn (AgentPrompt $prompt) => 'Response for: '.$prompt->prompt);
|
|
1097
|
+
|
|
1098
|
+
// Prevent stray prompts (throws if no fake defined)
|
|
1099
|
+
SalesCoach::fake()->preventStrayPrompts();
|
|
1100
|
+
|
|
1101
|
+
// Assertions
|
|
1102
|
+
SalesCoach::assertPrompted('Analyze this...');
|
|
1103
|
+
SalesCoach::assertPrompted(fn (AgentPrompt $prompt) => $prompt->contains('Analyze'));
|
|
1104
|
+
SalesCoach::assertNotPrompted('Missing prompt');
|
|
1105
|
+
SalesCoach::assertNeverPrompted();
|
|
1106
|
+
|
|
1107
|
+
// Queued assertions
|
|
1108
|
+
SalesCoach::assertQueued('Analyze this...');
|
|
1109
|
+
SalesCoach::assertQueued(fn (QueuedAgentPrompt $prompt) => $prompt->contains('Analyze'));
|
|
1110
|
+
SalesCoach::assertNotQueued('Missing prompt');
|
|
1111
|
+
SalesCoach::assertNeverQueued();
|
|
1112
|
+
```
|
|
1113
|
+
|
|
1114
|
+
When `fake()` is invoked on a structured output agent, fake data matching the schema is auto-generated.
|
|
1115
|
+
|
|
1116
|
+
### Images
|
|
1117
|
+
|
|
1118
|
+
```php
|
|
1119
|
+
use Laravel\Ai\Image;
|
|
1120
|
+
use Laravel\Ai\Prompts\ImagePrompt;
|
|
1121
|
+
|
|
1122
|
+
Image::fake();
|
|
1123
|
+
Image::fake([base64_encode($firstImage), base64_encode($secondImage)]);
|
|
1124
|
+
Image::fake(fn (ImagePrompt $prompt) => base64_encode('...'));
|
|
1125
|
+
Image::fake()->preventStrayImages();
|
|
1126
|
+
|
|
1127
|
+
Image::assertGenerated(fn (ImagePrompt $prompt) => $prompt->contains('sunset') && $prompt->isLandscape());
|
|
1128
|
+
Image::assertNotGenerated('Missing prompt');
|
|
1129
|
+
Image::assertNothingGenerated();
|
|
1130
|
+
|
|
1131
|
+
// Queued
|
|
1132
|
+
Image::assertQueued(fn (QueuedImagePrompt $prompt) => $prompt->contains('sunset'));
|
|
1133
|
+
Image::assertNotQueued('Missing prompt');
|
|
1134
|
+
Image::assertNothingQueued();
|
|
1135
|
+
```
|
|
1136
|
+
|
|
1137
|
+
### Audio
|
|
1138
|
+
|
|
1139
|
+
```php
|
|
1140
|
+
use Laravel\Ai\Audio;
|
|
1141
|
+
use Laravel\Ai\Prompts\AudioPrompt;
|
|
1142
|
+
|
|
1143
|
+
Audio::fake();
|
|
1144
|
+
Audio::fake()->preventStrayAudio();
|
|
1145
|
+
|
|
1146
|
+
Audio::assertGenerated(fn (AudioPrompt $prompt) => $prompt->contains('Hello') && $prompt->isFemale());
|
|
1147
|
+
Audio::assertNotGenerated('Missing prompt');
|
|
1148
|
+
Audio::assertNothingGenerated();
|
|
1149
|
+
|
|
1150
|
+
Audio::assertQueued(fn (QueuedAudioPrompt $prompt) => $prompt->contains('Hello'));
|
|
1151
|
+
```
|
|
1152
|
+
|
|
1153
|
+
### Transcriptions
|
|
1154
|
+
|
|
1155
|
+
```php
|
|
1156
|
+
use Laravel\Ai\Transcription;
|
|
1157
|
+
use Laravel\Ai\Prompts\TranscriptionPrompt;
|
|
1158
|
+
|
|
1159
|
+
Transcription::fake();
|
|
1160
|
+
Transcription::fake(['First transcription text.', 'Second transcription text.']);
|
|
1161
|
+
Transcription::fake()->preventStrayTranscriptions();
|
|
1162
|
+
|
|
1163
|
+
Transcription::assertGenerated(fn (TranscriptionPrompt $prompt) => $prompt->language === 'en' && $prompt->isDiarized());
|
|
1164
|
+
Transcription::assertNotGenerated(fn (TranscriptionPrompt $prompt) => $prompt->language === 'fr');
|
|
1165
|
+
Transcription::assertNothingGenerated();
|
|
1166
|
+
```
|
|
1167
|
+
|
|
1168
|
+
### Embeddings
|
|
1169
|
+
|
|
1170
|
+
```php
|
|
1171
|
+
use Laravel\Ai\Embeddings;
|
|
1172
|
+
use Laravel\Ai\Prompts\EmbeddingsPrompt;
|
|
1173
|
+
|
|
1174
|
+
Embeddings::fake();
|
|
1175
|
+
Embeddings::fake()->preventStrayEmbeddings();
|
|
1176
|
+
|
|
1177
|
+
Embeddings::assertGenerated(fn (EmbeddingsPrompt $prompt) => $prompt->contains('Laravel') && $prompt->dimensions === 1536);
|
|
1178
|
+
Embeddings::assertNotGenerated(fn (EmbeddingsPrompt $prompt) => $prompt->contains('Other'));
|
|
1179
|
+
Embeddings::assertNothingGenerated();
|
|
1180
|
+
```
|
|
1181
|
+
|
|
1182
|
+
### Reranking
|
|
1183
|
+
|
|
1184
|
+
```php
|
|
1185
|
+
use Laravel\Ai\Reranking;
|
|
1186
|
+
use Laravel\Ai\Prompts\RerankingPrompt;
|
|
1187
|
+
use Laravel\Ai\Responses\Data\RankedDocument;
|
|
1188
|
+
|
|
1189
|
+
Reranking::fake();
|
|
1190
|
+
Reranking::fake([
|
|
1191
|
+
[
|
|
1192
|
+
new RankedDocument(index: 0, document: 'First', score: 0.95),
|
|
1193
|
+
new RankedDocument(index: 1, document: 'Second', score: 0.80),
|
|
1194
|
+
],
|
|
1195
|
+
]);
|
|
1196
|
+
|
|
1197
|
+
Reranking::assertReranked(fn (RerankingPrompt $prompt) => $prompt->contains('Laravel') && $prompt->limit === 5);
|
|
1198
|
+
Reranking::assertNotReranked(fn (RerankingPrompt $prompt) => $prompt->contains('Django'));
|
|
1199
|
+
Reranking::assertNothingReranked();
|
|
1200
|
+
```
|
|
1201
|
+
|
|
1202
|
+
### Files
|
|
1203
|
+
|
|
1204
|
+
```php
|
|
1205
|
+
use Laravel\Ai\Files;
|
|
1206
|
+
use Laravel\Ai\Contracts\Files\StorableFile;
|
|
1207
|
+
use Laravel\Ai\Files\Document;
|
|
1208
|
+
|
|
1209
|
+
Files::fake();
|
|
1210
|
+
|
|
1211
|
+
Document::fromString('Hello, Laravel!', mimeType: 'text/plain')
|
|
1212
|
+
->as('hello.txt')
|
|
1213
|
+
->put();
|
|
1214
|
+
|
|
1215
|
+
Files::assertStored(fn (StorableFile $file) =>
|
|
1216
|
+
(string) $file === 'Hello, Laravel!' && $file->mimeType() === 'text/plain'
|
|
1217
|
+
);
|
|
1218
|
+
Files::assertNotStored(fn (StorableFile $file) => (string) $file === 'Hello, World!');
|
|
1219
|
+
Files::assertNothingStored();
|
|
1220
|
+
|
|
1221
|
+
Files::assertDeleted('file-id');
|
|
1222
|
+
Files::assertNotDeleted('file-id');
|
|
1223
|
+
Files::assertNothingDeleted();
|
|
1224
|
+
```
|
|
1225
|
+
|
|
1226
|
+
### Vector Stores
|
|
1227
|
+
|
|
1228
|
+
```php
|
|
1229
|
+
use Laravel\Ai\Stores;
|
|
1230
|
+
|
|
1231
|
+
Stores::fake(); // Also fakes file operations
|
|
1232
|
+
|
|
1233
|
+
$store = Stores::create('Knowledge Base');
|
|
1234
|
+
|
|
1235
|
+
Stores::assertCreated('Knowledge Base');
|
|
1236
|
+
Stores::assertCreated(fn (string $name, ?string $description) => $name === 'Knowledge Base');
|
|
1237
|
+
Stores::assertNotCreated('Other Store');
|
|
1238
|
+
Stores::assertNothingCreated();
|
|
1239
|
+
|
|
1240
|
+
Stores::assertDeleted('store_id');
|
|
1241
|
+
Stores::assertNotDeleted('other_store_id');
|
|
1242
|
+
Stores::assertNothingDeleted();
|
|
1243
|
+
|
|
1244
|
+
// File assertions on store instances
|
|
1245
|
+
$store = Stores::get('store_id');
|
|
1246
|
+
$store->add('added_id');
|
|
1247
|
+
$store->remove('removed_id');
|
|
1248
|
+
|
|
1249
|
+
$store->assertAdded('added_id');
|
|
1250
|
+
$store->assertRemoved('removed_id');
|
|
1251
|
+
$store->assertNotAdded('other_file_id');
|
|
1252
|
+
$store->assertNotRemoved('other_file_id');
|
|
1253
|
+
|
|
1254
|
+
// Assert by file content
|
|
1255
|
+
$store->add(Document::fromString('Hello, World!', 'text/plain')->as('hello.txt'));
|
|
1256
|
+
$store->assertAdded(fn (StorableFile $file) => $file->name() === 'hello.txt');
|
|
1257
|
+
```
|
|
1258
|
+
|
|
1259
|
+
## Events
|
|
1260
|
+
|
|
1261
|
+
The SDK dispatches lifecycle events you can listen to:
|
|
1262
|
+
|
|
1263
|
+
- `PromptingAgent`, `AgentPrompted`
|
|
1264
|
+
- `StreamingAgent`, `AgentStreamed`
|
|
1265
|
+
- `InvokingTool`, `ToolInvoked`
|
|
1266
|
+
- `GeneratingImage`, `ImageGenerated`
|
|
1267
|
+
- `GeneratingAudio`, `AudioGenerated`
|
|
1268
|
+
- `GeneratingTranscription`, `TranscriptionGenerated`
|
|
1269
|
+
- `GeneratingEmbeddings`, `EmbeddingsGenerated`
|
|
1270
|
+
- `Reranking`, `Reranked`
|
|
1271
|
+
- `StoringFile`, `FileStored`, `FileDeleted`
|
|
1272
|
+
- `CreatingStore`, `StoreCreated`
|
|
1273
|
+
- `AddingFileToStore`, `FileAddedToStore`, `RemovingFileFromStore`, `FileRemovedFromStore`
|
|
1274
|
+
|
|
1275
|
+
## Multi-Agent Pattern Example
|
|
1276
|
+
|
|
1277
|
+
Build a customer service system with a router agent and specialized sub-agents:
|
|
1278
|
+
|
|
1279
|
+
```php
|
|
1280
|
+
// Main agent routes messages to specialists
|
|
1281
|
+
class CustomerServiceRouter implements Agent, HasTools
|
|
1282
|
+
{
|
|
1283
|
+
use Promptable;
|
|
1284
|
+
|
|
1285
|
+
public function instructions(): string
|
|
1286
|
+
{
|
|
1287
|
+
return 'You are a customer service router. Analyze the customer message and delegate to the appropriate specialist agent.';
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
public function tools(): iterable
|
|
1291
|
+
{
|
|
1292
|
+
return [
|
|
1293
|
+
new RefundAgent,
|
|
1294
|
+
new TechnicalSupportAgent,
|
|
1295
|
+
new BillingAgent,
|
|
1296
|
+
];
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
// Each sub-agent is a specialist with its own tools and provider
|
|
1301
|
+
#[Provider(Lab::Anthropic)]
|
|
1302
|
+
#[UseCheapestModel]
|
|
1303
|
+
class RefundAgent implements Agent, CanActAsTool, HasTools
|
|
1304
|
+
{
|
|
1305
|
+
use Promptable;
|
|
1306
|
+
|
|
1307
|
+
public function instructions(): string
|
|
1308
|
+
{
|
|
1309
|
+
return 'You handle refund requests. Check order status and apply refund policy.';
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
public function name(): string { return 'refund_specialist'; }
|
|
1313
|
+
public function description(): string { return 'Handle refund eligibility and processing.'; }
|
|
1314
|
+
|
|
1315
|
+
public function tools(): iterable
|
|
1316
|
+
{
|
|
1317
|
+
return [new LookupOrder, new ProcessRefund];
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
#[Provider(Lab::OpenAI)]
|
|
1322
|
+
class TechnicalSupportAgent implements Agent, CanActAsTool, HasTools
|
|
1323
|
+
{
|
|
1324
|
+
use Promptable;
|
|
1325
|
+
|
|
1326
|
+
public function instructions(): string
|
|
1327
|
+
{
|
|
1328
|
+
return 'You handle technical support questions. Search the knowledge base and provide solutions.';
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
public function name(): string { return 'technical_support'; }
|
|
1332
|
+
public function description(): string { return 'Troubleshoot technical issues and provide solutions.'; }
|
|
1333
|
+
|
|
1334
|
+
public function tools(): iterable
|
|
1335
|
+
{
|
|
1336
|
+
return [
|
|
1337
|
+
SimilaritySearch::usingModel(KnowledgeArticle::class, 'embedding')
|
|
1338
|
+
->withDescription('Search technical knowledge base.'),
|
|
1339
|
+
];
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
```
|
|
1343
|
+
|
|
1344
|
+
## Provider Support
|
|
1345
|
+
|
|
1346
|
+
| Feature | Providers |
|
|
1347
|
+
|---------|-----------|
|
|
1348
|
+
| Text | OpenAI, Anthropic, Gemini, Azure, Bedrock, Groq, xAI, DeepSeek, Mistral, Ollama, OpenRouter |
|
|
1349
|
+
| Images | OpenAI, Gemini, xAI, Azure, Bedrock, OpenRouter |
|
|
1350
|
+
| TTS | OpenAI, ElevenLabs, Gemini |
|
|
1351
|
+
| STT | OpenAI, ElevenLabs, Mistral, Gemini |
|
|
1352
|
+
| Embeddings | OpenAI, Gemini, Azure, Bedrock, Cohere, Mistral, Jina, VoyageAI, Ollama, OpenRouter |
|
|
1353
|
+
| Reranking | Cohere, Jina, VoyageAI |
|
|
1354
|
+
| Files | OpenAI, Anthropic, Gemini |
|