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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/Files/Base64Document.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Http\UploadedFile;
use InvalidArgumentException;
use JsonSerializable;
use Laravel\Ai\Contracts\Files\StorableFile;
use Laravel\Ai\Files\Concerns\CanBeUploadedToProvider;
Expand All @@ -14,6 +15,10 @@ class Base64Document extends Document implements Arrayable, JsonSerializable, St

public function __construct(public string $base64, ?string $mimeType = null)
{
if (blank($base64)) {
throw new InvalidArgumentException('Base64 document content cannot be empty.');
}

$this->mime = $mimeType;
}

Expand Down
5 changes: 5 additions & 0 deletions src/Files/Base64Image.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Laravel\Ai\Files;

use Illuminate\Contracts\Support\Arrayable;
use InvalidArgumentException;
use JsonSerializable;
use Laravel\Ai\Contracts\Files\StorableFile;
use Laravel\Ai\Files\Concerns\CanBeUploadedToProvider;
Expand All @@ -13,6 +14,10 @@ class Base64Image extends Image implements Arrayable, JsonSerializable, Storable

public function __construct(public string $base64, ?string $mimeType = null)
{
if (blank($base64)) {
throw new InvalidArgumentException('Base64 image content cannot be empty.');
}

$this->mime = $mimeType;
}

Expand Down
22 changes: 22 additions & 0 deletions tests/Feature/EmbeddingsFakeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,28 @@
expect($response)->toHaveCount(3);
});

test('can iterate over response', function () {
Embeddings::fake([
[
array_fill(0, 3, 0.1),
array_fill(0, 3, 0.2),
],
]);

$response = Embeddings::for(['Hello', 'World'])->dimensions(3)->generate();

$embeddings = [];

foreach ($response as $embedding) {
$embeddings[] = $embedding;
}

expect($embeddings)->toEqual([
array_fill(0, 3, 0.1),
array_fill(0, 3, 0.2),
]);
});

test('can fake embeddings with custom response', function () {
$customEmbedding = array_fill(0, 100, 0.5);

Expand Down
45 changes: 45 additions & 0 deletions tests/Feature/Providers/Anthropic/ToolMappingTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

use Illuminate\Support\Facades\Http;
use Laravel\Ai\Providers\Tools\FileSearch;
use Laravel\Ai\Providers\Tools\WebFetch;
use Laravel\Ai\Providers\Tools\WebSearch;
use Tests\Fixtures\Agents\NamedToolAgent;
use Tests\Fixtures\Agents\ToolUsingAgent;
Expand Down Expand Up @@ -93,6 +94,50 @@
});
});

test('web fetch tool sends allowed_domains', function () {
Http::fake([
'api.anthropic.com/*' => $this->fakeTextResponse('ok'),
]);

agent(tools: [(new WebFetch)->allow(['laravel.com', 'php.net'])])
->prompt('Fetch', provider: 'anthropic');

Http::assertSent(function ($request) {
$tool = collect($request->data()['tools'] ?? [])->firstWhere('name', 'web_fetch');

return data_get($tool, 'type') === 'web_fetch_20250910'
&& data_get($tool, 'allowed_domains') === ['laravel.com', 'php.net'];
});
});

test('web fetch tool defaults max_uses to ten', function () {
Http::fake([
'api.anthropic.com/*' => $this->fakeTextResponse('ok'),
]);

agent(tools: [new WebFetch])->prompt('Fetch', provider: 'anthropic');

Http::assertSent(function ($request) {
$tool = collect($request->data()['tools'] ?? [])->firstWhere('name', 'web_fetch');

return data_get($tool, 'max_uses') === 10;
});
});

test('web fetch tool forwards a custom max_uses', function () {
Http::fake([
'api.anthropic.com/*' => $this->fakeTextResponse('ok'),
]);

agent(tools: [(new WebFetch)->max(3)])->prompt('Fetch', provider: 'anthropic');

Http::assertSent(function ($request) {
$tool = collect($request->data()['tools'] ?? [])->firstWhere('name', 'web_fetch');

return data_get($tool, 'max_uses') === 3;
});
});

test('empty schema still includes input schema with type object', function () {
Http::fake([
'api.anthropic.com/*' => $this->fakeTextResponse('The number is 42'),
Expand Down
7 changes: 7 additions & 0 deletions tests/Unit/Files/Base64DocumentTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

use Laravel\Ai\Files\Base64Document;

test('base64 document rejects empty content', function () {
new Base64Document('');
})->throws(InvalidArgumentException::class, 'Base64 document content cannot be empty.');
7 changes: 7 additions & 0 deletions tests/Unit/Files/Base64ImageTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

use Laravel\Ai\Files\Base64Image;

test('base64 image rejects empty content', function () {
new Base64Image('');
})->throws(InvalidArgumentException::class, 'Base64 image content cannot be empty.');
36 changes: 18 additions & 18 deletions tests/Unit/Gateway/Bedrock/BedrockTextGatewayTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -311,17 +311,17 @@ public function schema(JsonSchema $schema): array
test('document format maps common mime types', function () {
$gateway = textGateway();

expect($gateway->callGetDocumentFormat(new Base64Document('', 'application/pdf')))->toBe('pdf');
expect($gateway->callGetDocumentFormat(new Base64Document('', 'text/csv')))->toBe('csv');
expect($gateway->callGetDocumentFormat(new Base64Document('', 'application/msword')))->toBe('doc');
expect($gateway->callGetDocumentFormat(new Base64Document('', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document')))->toBe('docx');
expect($gateway->callGetDocumentFormat(new Base64Document('', 'application/vnd.ms-excel')))->toBe('xls');
expect($gateway->callGetDocumentFormat(new Base64Document('', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')))->toBe('xlsx');
expect($gateway->callGetDocumentFormat(new Base64Document('', 'text/html')))->toBe('html');
expect($gateway->callGetDocumentFormat(new Base64Document('', 'text/markdown')))->toBe('md');
expect($gateway->callGetDocumentFormat(new Base64Document('', 'text/x-markdown')))->toBe('md');
expect($gateway->callGetDocumentFormat(new Base64Document('', 'text/plain; charset=utf-8')))->toBe('txt');
expect($gateway->callGetDocumentFormat(new Base64Document('', null)))->toBeNull();
expect($gateway->callGetDocumentFormat(new Base64Document(base64_encode('doc-bytes'), 'application/pdf')))->toBe('pdf');
expect($gateway->callGetDocumentFormat(new Base64Document(base64_encode('doc-bytes'), 'text/csv')))->toBe('csv');
expect($gateway->callGetDocumentFormat(new Base64Document(base64_encode('doc-bytes'), 'application/msword')))->toBe('doc');
expect($gateway->callGetDocumentFormat(new Base64Document(base64_encode('doc-bytes'), 'application/vnd.openxmlformats-officedocument.wordprocessingml.document')))->toBe('docx');
expect($gateway->callGetDocumentFormat(new Base64Document(base64_encode('doc-bytes'), 'application/vnd.ms-excel')))->toBe('xls');
expect($gateway->callGetDocumentFormat(new Base64Document(base64_encode('doc-bytes'), 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')))->toBe('xlsx');
expect($gateway->callGetDocumentFormat(new Base64Document(base64_encode('doc-bytes'), 'text/html')))->toBe('html');
expect($gateway->callGetDocumentFormat(new Base64Document(base64_encode('doc-bytes'), 'text/markdown')))->toBe('md');
expect($gateway->callGetDocumentFormat(new Base64Document(base64_encode('doc-bytes'), 'text/x-markdown')))->toBe('md');
expect($gateway->callGetDocumentFormat(new Base64Document(base64_encode('doc-bytes'), 'text/plain; charset=utf-8')))->toBe('txt');
expect($gateway->callGetDocumentFormat(new Base64Document(base64_encode('doc-bytes'), null)))->toBeNull();
});

test('user message with base64 image attachment produces image block', function () {
Expand Down Expand Up @@ -380,20 +380,20 @@ public function schema(JsonSchema $schema): array
test('image format maps common image mime types', function () {
$gateway = textGateway();

expect($gateway->callGetImageFormat(new Base64Image('', 'image/png')))->toBe('png');
expect($gateway->callGetImageFormat(new Base64Image('', 'image/jpeg')))->toBe('jpeg');
expect($gateway->callGetImageFormat(new Base64Image('', 'image/jpg')))->toBe('jpeg');
expect($gateway->callGetImageFormat(new Base64Image('', 'image/gif')))->toBe('gif');
expect($gateway->callGetImageFormat(new Base64Image('', 'image/webp')))->toBe('webp');
expect($gateway->callGetImageFormat(new Base64Image(base64_encode('image-bytes'), 'image/png')))->toBe('png');
expect($gateway->callGetImageFormat(new Base64Image(base64_encode('image-bytes'), 'image/jpeg')))->toBe('jpeg');
expect($gateway->callGetImageFormat(new Base64Image(base64_encode('image-bytes'), 'image/jpg')))->toBe('jpeg');
expect($gateway->callGetImageFormat(new Base64Image(base64_encode('image-bytes'), 'image/gif')))->toBe('gif');
expect($gateway->callGetImageFormat(new Base64Image(base64_encode('image-bytes'), 'image/webp')))->toBe('webp');
});

test('image format throws when mime type is unsupported', function () {
expect(fn () => textGateway()->callGetImageFormat(new Base64Image('', 'image/unsupported')))
expect(fn () => textGateway()->callGetImageFormat(new Base64Image(base64_encode('image-bytes'), 'image/unsupported')))
->toThrow(InvalidArgumentException::class, 'Unsupported image MIME type [image/unsupported]');
});

test('image format throws when mime type is missing', function () {
expect(fn () => textGateway()->callGetImageFormat(new Base64Image('', null)))
expect(fn () => textGateway()->callGetImageFormat(new Base64Image(base64_encode('image-bytes'), null)))
->toThrow(InvalidArgumentException::class, 'Unable to determine MIME type');
});

Expand Down