@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 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 |