Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
25 changes: 24 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,29 @@
# Release Notes

## [Unreleased](https://github.com/laravel/ai/compare/v0.9.0...0.x)
## [Unreleased](https://github.com/laravel/ai/compare/v0.9.1...0.x)

## [v0.9.1](https://github.com/laravel/ai/compare/v0.9.0...v0.9.1) - 2026-07-14

### What's Changed

* Remove dead dall-e-3 image integration dataset by [@mrdzen](https://github.com/mrdzen) in https://github.com/laravel/ai/pull/779
* Forward attachment provider options to OpenAI text generation requests by [@mrdzen](https://github.com/mrdzen) in https://github.com/laravel/ai/pull/768
* Add tool choice support for Gemini, OpenAI, and Anthropic by [@JVillator0](https://github.com/JVillator0) in https://github.com/laravel/ai/pull/780
* Add tests for the image portrait and custom size helpers by [@dfinchenko](https://github.com/dfinchenko) in https://github.com/laravel/ai/pull/783
* Add interactive agent console command for the workbench by [@pushpak1300](https://github.com/pushpak1300) in https://github.com/laravel/ai/pull/786
* Add web fetch tool coverage for the Anthropic provider by [@dfinchenko](https://github.com/dfinchenko) in https://github.com/laravel/ai/pull/792
* [0.x] Validate blank base64 input consistently across file classes by [@Button99](https://github.com/Button99) in https://github.com/laravel/ai/pull/788
* Add coverage for iterating the embeddings response by [@dfinchenko](https://github.com/dfinchenko) in https://github.com/laravel/ai/pull/790
* Add web search user location coverage for the Anthropic provider by [@dfinchenko](https://github.com/dfinchenko) in https://github.com/laravel/ai/pull/791
* Fix`TextDelta::combine` glues multi-step streamed text together mid-sentence by [@crishoj](https://github.com/crishoj) in https://github.com/laravel/ai/pull/789
* Fix empty Bedrock stream content blocks by [@iAmKevinMcKee](https://github.com/iAmKevinMcKee) in https://github.com/laravel/ai/pull/785

### New Contributors

* [@crishoj](https://github.com/crishoj) made their first contribution in https://github.com/laravel/ai/pull/789
* [@iAmKevinMcKee](https://github.com/iAmKevinMcKee) made their first contribution in https://github.com/laravel/ai/pull/785

**Full Changelog**: https://github.com/laravel/ai/compare/v0.9.0...v0.9.1

## [v0.9.0](https://github.com/laravel/ai/compare/v0.8.1...v0.9.0) - 2026-07-07

Expand Down
4 changes: 3 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,15 @@
"laravel/serializable-closure": "^2.0"
},
"require-dev": {
"driftingly/rector-laravel": "^2.5",
"laravel/mcp": "^0.8",
"laravel/pint": "^1.26",
"mockery/mockery": "^1.6.12",
"orchestra/testbench": "^10.6|^11.0",
"pestphp/pest": "^3.0|^4.0",
"pestphp/pest-plugin-laravel": "^3.0|^4.0",
"phpstan/phpstan": "^2.1"
"phpstan/phpstan": "^2.1",
"rector/rector": "^2.5"
},
"suggest": {
"laravel/mcp": "Required to use MCP client tools or MCP server tools with Laravel AI agents."
Expand Down
50 changes: 50 additions & 0 deletions rector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

declare(strict_types=1);

use Rector\CodingStyle\Rector\Encapsed\EncapsedStringsToSprintfRector;
use Rector\Config\RectorConfig;
use Rector\DeadCode\Rector\For_\RemoveDeadIfForeachForRector;
use Rector\DeadCode\Rector\For_\RemoveDeadLoopRector;
use Rector\Php80\Rector\Class_\ClassPropertyAssignToConstructorPromotionRector;
use Rector\Php81\Rector\Property\ReadOnlyPropertyRector;
use Rector\Strict\Rector\Empty_\DisallowedEmptyRuleFixerRector;
use Rector\TypeDeclaration\Rector\FuncCall\AddArrayFunctionClosureParamTypeRector;
use Rector\TypeDeclaration\Rector\FunctionLike\AddClosureParamTypeForArrayMapRector;
use Rector\TypeDeclaration\Rector\FunctionLike\AddClosureParamTypeForArrayReduceRector;
use Rector\TypeDeclaration\Rector\StmtsAwareInterface\SafeDeclareStrictTypesRector;
use RectorLaravel\Rector\Class_\TablePropertyToTableAttributeRector;
use RectorLaravel\Set\LaravelSetProvider;

return RectorConfig::configure()
->withPaths([
__DIR__.'/src',
__DIR__.'/tests',
])
->withSkip([
ReadOnlyPropertyRector::class,
EncapsedStringsToSprintfRector::class,
DisallowedEmptyRuleFixerRector::class,
// Renames public constructor parameters (e.g. mimeType -> mime), breaking named-argument callers.
ClassPropertyAssignToConstructorPromotionRector::class,
// Infers closure param types too narrowly for polymorphic tool lists (Tool|Agent), causing TypeErrors.
AddClosureParamTypeForArrayMapRector::class,
AddClosureParamTypeForArrayReduceRector::class,
AddArrayFunctionClosureParamTypeRector::class,
// Deletes empty-bodied loops, but iterating a generator (e.g. draining a stream) has side effects and must be kept.
RemoveDeadLoopRector::class,
RemoveDeadIfForeachForRector::class,
// Skipped for now to keep the diff focused (no declare(strict_types=1) churn).
SafeDeclareStrictTypesRector::class,
TablePropertyToTableAttributeRector::class,
])
->withPreparedSets(
deadCode: true,
codeQuality: true,
codingStyle: true,
typeDeclarations: true,
earlyReturn: true,
)
->withPhpSets(php81: true)
->withSetProviders(LaravelSetProvider::class)
->withComposerBased(laravel: true);
16 changes: 8 additions & 8 deletions src/AiManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ class AiManager extends MultipleInstanceManager
*/
public function audioProvider(?string $name = null): AudioProvider
{
return tap($this->instance($name), function ($instance) {
return tap($this->instance($name), function ($instance): void {
if (! $instance instanceof AudioProvider) {
throw new LogicException('Provider ['.$instance::class.'] does not support audio generation.');
}
Expand Down Expand Up @@ -90,7 +90,7 @@ public function fakeableAudioProvider(?string $name = null): AudioProvider
*/
public function embeddingProvider(?string $name = null): EmbeddingProvider
{
return tap($this->instance($name), function ($instance) {
return tap($this->instance($name), function ($instance): void {
if (! $instance instanceof EmbeddingProvider) {
throw new LogicException('Provider ['.$instance::class.'] does not support embedding generation.');
}
Expand Down Expand Up @@ -118,7 +118,7 @@ public function fakeableEmbeddingProvider(?string $name = null): EmbeddingProvid
*/
public function rerankingProvider(?string $name = null): RerankingProvider
{
return tap($this->instance($name), function ($instance) {
return tap($this->instance($name), function ($instance): void {
if (! $instance instanceof RerankingProvider) {
throw new LogicException('Provider ['.$instance::class.'] does not support reranking.');
}
Expand Down Expand Up @@ -146,7 +146,7 @@ public function fakeableRerankingProvider(?string $name = null): RerankingProvid
*/
public function imageProvider(?string $name = null): ImageProvider
{
return tap($this->instance($name), function ($instance) {
return tap($this->instance($name), function ($instance): void {
if (! $instance instanceof ImageProvider) {
throw new LogicException('Provider ['.$instance::class.'] does not support image generation.');
}
Expand Down Expand Up @@ -174,7 +174,7 @@ public function fakeableImageProvider(?string $name = null): ImageProvider
*/
public function textProvider(?string $name = null): TextProvider
{
return tap($this->instance($name), function ($instance) {
return tap($this->instance($name), function ($instance): void {
if (! $instance instanceof TextProvider) {
throw new LogicException('Provider ['.$instance::class.'] does not support text generation.');
}
Expand Down Expand Up @@ -202,7 +202,7 @@ public function textProviderFor(Agent $agent, ?string $name = null): TextProvide
*/
public function transcriptionProvider(?string $name = null): TranscriptionProvider
{
return tap($this->instance($name), function ($instance) {
return tap($this->instance($name), function ($instance): void {
if (! $instance instanceof TranscriptionProvider) {
throw new LogicException('Provider ['.$instance::class.'] does not support transcription generation.');
}
Expand Down Expand Up @@ -230,7 +230,7 @@ public function fakeableTranscriptionProvider(?string $name = null): Transcripti
*/
public function fileProvider(?string $name = null): FileProvider
{
return tap($this->instance($name), function ($instance) {
return tap($this->instance($name), function ($instance): void {
if (! $instance instanceof FileProvider) {
throw new LogicException('Provider ['.$instance::class.'] does not support file management.');
}
Expand Down Expand Up @@ -258,7 +258,7 @@ public function fakeableFileProvider(?string $name = null): FileProvider
*/
public function storeProvider(?string $name = null): StoreProvider
{
return tap($this->instance($name), function ($instance) {
return tap($this->instance($name), function ($instance): void {
if (! $instance instanceof StoreProvider) {
throw new LogicException('Provider ['.$instance::class.'] does not support store management.');
}
Expand Down
12 changes: 7 additions & 5 deletions src/AiServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,20 @@
use Laravel\Ai\Console\Commands\MakeToolCommand;
use Laravel\Ai\Contracts\ConversationStore;
use Laravel\Ai\Enums\Lab;
use Laravel\Ai\Responses\AudioResponse;
use Laravel\Ai\Storage\DatabaseConversationStore;

class AiServiceProvider extends ServiceProvider
{
/**
* Register the package's services.
*/
#[\Override]
public function register(): void
{
$this->app->singleton(AiManager::class, fn ($app): AiManager => new AiManager($app));

$this->app->singleton(ConversationStore::class, fn () => new DatabaseConversationStore(
$this->app->singleton(ConversationStore::class, fn (): DatabaseConversationStore => new DatabaseConversationStore(
config('ai.conversations.connection'),
));

Expand Down Expand Up @@ -79,7 +81,7 @@ public function boot(): void
?string $instructions = null,
?string $model = null,
?int $timeout = null,
) {
): AudioResponse {
$request = Audio::of($this->value());

if (! is_null($voice)) {
Expand Down Expand Up @@ -107,8 +109,8 @@ public function boot(): void
) {
$resolver = match (true) {
$by instanceof Closure => $by,
is_array($by) => fn ($item) => json_encode(
(new Collection($by))->mapWithKeys(fn ($field) => [$field => data_get($item, $field)])->all()
is_array($by) => fn ($item): string|false => json_encode(
(new Collection($by))->mapWithKeys(fn ($field): array => [$field => data_get($item, $field)])->all()
),
default => fn ($item) => data_get($item, $by),
};
Expand All @@ -118,7 +120,7 @@ public function boot(): void
->rerank($query, $provider, $model);

return (new Collection($response->results))->map(
fn ($result) => $this->values()[$result->index]
fn ($result): mixed => $this->values()[$result->index]
);
});
}
Expand Down
12 changes: 4 additions & 8 deletions src/Concerns/InteractsWithFakeAgents.php
Original file line number Diff line number Diff line change
Expand Up @@ -84,13 +84,11 @@ public function assertAgentWasPrompted(
?string $message = null): self
{
$callback = is_string($callback)
? fn ($prompt) => $prompt->prompt === $callback
? fn ($prompt): bool => $prompt->prompt === $callback
: $callback;

PHPUnit::assertTrue(
(new Collection($prompts ?? $this->recordedPrompts[$agent] ?? []))->contains(function ($prompt) use ($callback) {
return $callback($prompt);
}),
(new Collection($prompts ?? $this->recordedPrompts[$agent] ?? []))->contains(fn ($prompt) => $callback($prompt)),
$message ?? 'An expected prompt was not received.'
);

Expand Down Expand Up @@ -120,13 +118,11 @@ public function assertAgentNotPrompted(
?string $message = null): self
{
$callback = is_string($callback)
? fn ($prompt) => $prompt->prompt === $callback
? fn ($prompt): bool => $prompt->prompt === $callback
: $callback;

PHPUnit::assertTrue(
(new Collection($prompts ?? $this->recordedPrompts[$agent] ?? []))->doesntContain(function ($prompt) use ($callback) {
return $callback($prompt);
}),
(new Collection($prompts ?? $this->recordedPrompts[$agent] ?? []))->doesntContain(fn ($prompt) => $callback($prompt)),
$message ?? 'An unexpected prompt was received.'
);

Expand Down
16 changes: 4 additions & 12 deletions src/Concerns/InteractsWithFakeAudio.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,7 @@ public function recordAudioGeneration(AudioPrompt|QueuedAudioPrompt $prompt): se
public function assertAudioGenerated(Closure $callback): self
{
PHPUnit::assertTrue(
(new Collection($this->recordedAudioGenerations))->contains(function (AudioPrompt $prompt) use ($callback) {
return $callback($prompt);
}),
(new Collection($this->recordedAudioGenerations))->contains(fn (AudioPrompt $prompt) => $callback($prompt)),
'An expected audio generation was not recorded.'
);

Expand All @@ -69,9 +67,7 @@ public function assertAudioGenerated(Closure $callback): self
public function assertAudioNotGenerated(Closure $callback): self
{
PHPUnit::assertTrue(
(new Collection($this->recordedAudioGenerations))->doesntContain(function (AudioPrompt $prompt) use ($callback) {
return $callback($prompt);
}),
(new Collection($this->recordedAudioGenerations))->doesntContain(fn (AudioPrompt $prompt) => $callback($prompt)),
'An unexpected audio generation was recorded.'
);

Expand All @@ -97,9 +93,7 @@ public function assertNoAudioGenerated(): self
public function assertAudioQueued(Closure $callback): self
{
PHPUnit::assertTrue(
(new Collection($this->recordedQueuedAudioGenerations))->contains(function (QueuedAudioPrompt $prompt) use ($callback) {
return $callback($prompt);
}),
(new Collection($this->recordedQueuedAudioGenerations))->contains(fn (QueuedAudioPrompt $prompt) => $callback($prompt)),
'An expected queued audio generation was not recorded.'
);

Expand All @@ -112,9 +106,7 @@ public function assertAudioQueued(Closure $callback): self
public function assertAudioNotQueued(Closure $callback): self
{
PHPUnit::assertTrue(
(new Collection($this->recordedQueuedAudioGenerations))->doesntContain(function (QueuedAudioPrompt $prompt) use ($callback) {
return $callback($prompt);
}),
(new Collection($this->recordedQueuedAudioGenerations))->doesntContain(fn (QueuedAudioPrompt $prompt) => $callback($prompt)),
'An unexpected queued audio generation was recorded.'
);

Expand Down
16 changes: 4 additions & 12 deletions src/Concerns/InteractsWithFakeEmbeddings.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,7 @@ public function recordEmbeddingsGeneration(EmbeddingsPrompt|QueuedEmbeddingsProm
public function assertEmbeddingsGenerated(Closure $callback): self
{
PHPUnit::assertTrue(
(new Collection($this->recordedEmbeddingsGenerations))->contains(function (EmbeddingsPrompt $prompt) use ($callback) {
return $callback($prompt);
}),
(new Collection($this->recordedEmbeddingsGenerations))->contains(fn (EmbeddingsPrompt $prompt) => $callback($prompt)),
'An expected embeddings generation was not recorded.'
);

Expand All @@ -69,9 +67,7 @@ public function assertEmbeddingsGenerated(Closure $callback): self
public function assertEmbeddingsNotGenerated(Closure $callback): self
{
PHPUnit::assertTrue(
(new Collection($this->recordedEmbeddingsGenerations))->doesntContain(function (EmbeddingsPrompt $prompt) use ($callback) {
return $callback($prompt);
}),
(new Collection($this->recordedEmbeddingsGenerations))->doesntContain(fn (EmbeddingsPrompt $prompt) => $callback($prompt)),
'An unexpected embeddings generation was recorded.'
);

Expand All @@ -97,9 +93,7 @@ public function assertNoEmbeddingsGenerated(): self
public function assertEmbeddingsQueued(Closure $callback): self
{
PHPUnit::assertTrue(
(new Collection($this->recordedQueuedEmbeddingsGenerations))->contains(function (QueuedEmbeddingsPrompt $prompt) use ($callback) {
return $callback($prompt);
}),
(new Collection($this->recordedQueuedEmbeddingsGenerations))->contains(fn (QueuedEmbeddingsPrompt $prompt) => $callback($prompt)),
'An expected queued embeddings generation was not recorded.'
);

Expand All @@ -112,9 +106,7 @@ public function assertEmbeddingsQueued(Closure $callback): self
public function assertEmbeddingsNotQueued(Closure $callback): self
{
PHPUnit::assertTrue(
(new Collection($this->recordedQueuedEmbeddingsGenerations))->doesntContain(function (QueuedEmbeddingsPrompt $prompt) use ($callback) {
return $callback($prompt);
}),
(new Collection($this->recordedQueuedEmbeddingsGenerations))->doesntContain(fn (QueuedEmbeddingsPrompt $prompt) => $callback($prompt)),
'An unexpected queued embeddings generation was recorded.'
);

Expand Down
20 changes: 6 additions & 14 deletions src/Concerns/InteractsWithFakeFiles.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,7 @@ public function recordFileDeletion(string $fileId): self
public function assertFileUploaded(Closure $callback): self
{
PHPUnit::assertTrue(
(new Collection($this->recordedFileUploads))->contains(function (array $upload) use ($callback) {
return $callback($upload['file']);
}),
(new Collection($this->recordedFileUploads))->contains(fn (array $upload) => $callback($upload['file'])),
'An expected file upload was not recorded.'
);

Expand All @@ -76,9 +74,7 @@ public function assertFileUploaded(Closure $callback): self
public function assertFileNotUploaded(Closure $callback): self
{
PHPUnit::assertTrue(
(new Collection($this->recordedFileUploads))->doesntContain(function (array $upload) use ($callback) {
return $callback($upload['file']);
}),
(new Collection($this->recordedFileUploads))->doesntContain(fn (array $upload) => $callback($upload['file'])),
'An unexpected file upload was recorded.'
);

Expand All @@ -105,13 +101,11 @@ public function assertFileDeleted(Closure|string $callback): self
{
if (is_string($callback)) {
$fileId = $callback;
$callback = fn ($id) => $id === $fileId;
$callback = fn ($id): bool => $id === $fileId;
}

PHPUnit::assertTrue(
(new Collection($this->recordedFileDeletions))->contains(function (string $id) use ($callback) {
return $callback($id);
}),
(new Collection($this->recordedFileDeletions))->contains(fn (string $id) => $callback($id)),
'An expected file deletion was not recorded.'
);

Expand All @@ -125,13 +119,11 @@ public function assertFileNotDeleted(Closure|string $callback): self
{
if (is_string($callback)) {
$fileId = $callback;
$callback = fn ($id) => $id === $fileId;
$callback = fn ($id): bool => $id === $fileId;
}

PHPUnit::assertTrue(
(new Collection($this->recordedFileDeletions))->doesntContain(function (string $id) use ($callback) {
return $callback($id);
}),
(new Collection($this->recordedFileDeletions))->doesntContain(fn (string $id) => $callback($id)),
'An unexpected file deletion was recorded.'
);

Expand Down
Loading