From 5fb73826531340c8a88bc45c3f64d56f41bc9c09 Mon Sep 17 00:00:00 2001 From: David Cochrum Date: Sun, 17 Nov 2024 18:48:19 -0500 Subject: [PATCH] Implement file uploading and inclusion within chat messages --- README.md | 48 ++++++++++++- composer.json | 3 + src/Client.php | 11 +++ src/Concerns/HasContents.php | 5 +- src/Contracts/Resources/FilesContract.php | 24 +++++++ src/Data/Candidate.php | 2 +- src/Data/Content.php | 8 ++- src/Data/Part.php | 11 ++- src/Data/UploadedFile.php | 39 +++++++++++ src/Data/VideoMetadata.php | 34 +++++++++ src/Enums/FileState.php | 28 ++++++++ src/Requests/Files/MetadataGetRequest.php | 45 ++++++++++++ src/Requests/Files/UploadRequest.php | 59 ++++++++++++++++ .../GenerateContentRequest.php | 3 +- src/Resources/ChatSession.php | 5 +- src/Resources/Files.php | 42 +++++++++++ src/Resources/GenerativeModel.php | 5 +- src/Responses/Files/MetadataResponse.php | 69 +++++++++++++++++++ src/Responses/Files/UploadResponse.php | 37 ++++++++++ .../GenerateContentResponse.php | 2 +- .../Files/MetadataResponseFixture.php | 22 ++++++ .../Fixtures/Files/UploadResponseFixture.php | 12 ++++ tests/Client.php | 7 ++ tests/Pest.php | 6 +- tests/Resources/Files.php | 34 +++++++++ tests/Resources/GenerativeModel.php | 15 ++++ tests/Responses/Files/MetadataResponse.php | 38 ++++++++++ tests/Responses/Files/UploadResponse.php | 40 +++++++++++ .../Resources/ChatSessionTestResource.php | 16 +++++ 29 files changed, 652 insertions(+), 18 deletions(-) create mode 100644 src/Contracts/Resources/FilesContract.php create mode 100644 src/Data/UploadedFile.php create mode 100644 src/Data/VideoMetadata.php create mode 100644 src/Enums/FileState.php create mode 100644 src/Requests/Files/MetadataGetRequest.php create mode 100644 src/Requests/Files/UploadRequest.php create mode 100644 src/Resources/Files.php create mode 100644 src/Responses/Files/MetadataResponse.php create mode 100644 src/Responses/Files/UploadResponse.php create mode 100644 src/Testing/Responses/Fixtures/Files/MetadataResponseFixture.php create mode 100644 src/Testing/Responses/Fixtures/Files/UploadResponseFixture.php create mode 100644 tests/Resources/Files.php create mode 100644 tests/Responses/Files/MetadataResponse.php create mode 100644 tests/Responses/Files/UploadResponse.php diff --git a/README.md b/README.md index 3a6518c..69b3a80 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,8 @@ - [Chat Resource](#chat-resource) - [Text-only Input](#text-only-input) - [Text-and-image Input](#text-and-image-input) + - [File Upload](#file-upload) + - [Text-and-video Input](#text-and-video-input) - [Multi-turn Conversations (Chat)](#multi-turn-conversations-chat) - [Stream Generate Content](#stream-generate-content) - [Structured Output](#structured-output) @@ -150,6 +152,50 @@ $result = $client ) ]); +$result->text(); // The picture shows a table with a white tablecloth. On the table are two cups of coffee, a bowl of blueberries, a silver spoon, and some flowers. There are also some blueberry scones on the table. +``` + +#### File Upload +To reference larger files and videos with various prompts, upload them to Gemini storage. + +```php +$files = $client->files(); +echo "Uploading\n"; +$meta = $files->upload( + filename: 'video.mp4', + mimeType: MimeType::VIDEO_MP4, + displayName: 'Video' +); +echo "Processing"; +do { + echo "."; + sleep(2); + $meta = $files->metadataGet($meta->uri); +} while (!$meta->state->complete()); +echo "\n"; + +if ($meta->state == FileState::Failed) { + die("Upload failed:\n" . json_encode($meta->toArray(), JSON_PRETTY_PRINT)); +} + +echo "Processing complete\n" . json_encode($meta->toArray(), JSON_PRETTY_PRINT); +echo "\n{$meta->uri}"; +``` + +#### Text-and-video Input +If the input contains both text and video, use the `gemini-pro-vision` model and upload the file beforehand. + +```php +$result = $client + ->geminiFlash() + ->generateContent([ + 'What is this video?', + new UploadedFile( + fileUri: '123-456', // accepts just the name or the full URI + mimeType: MimeType::VIDEO_MP4 + ) + ]); + $result->text(); // The picture shows a table with a white tablecloth. On the table are two cups of coffee, a bowl of blueberries, a silver spoon, and some flowers. There are also some blueberry scones on the table. ``` #### Multi-turn Conversations (Chat) @@ -536,4 +582,4 @@ $client = new ClientFake([ // the `ErrorException` will be thrown $client->geminiPro()->generateContent('test'); -``` \ No newline at end of file +``` diff --git a/composer.json b/composer.json index 387867b..9c54157 100644 --- a/composer.json +++ b/composer.json @@ -31,6 +31,9 @@ "mockery/mockery": "^1.6.7", "phpstan/phpstan": "^1.10" }, + "suggest": { + "ext-fileinfo": "Reads upload file size and mime type if not provided" + }, "autoload": { "psr-4": { "Gemini\\": "src/" diff --git a/src/Client.php b/src/Client.php index fc5cb3f..86b7456 100644 --- a/src/Client.php +++ b/src/Client.php @@ -10,6 +10,7 @@ use Gemini\Enums\ModelType; use Gemini\Resources\ChatSession; use Gemini\Resources\EmbeddingModel; +use Gemini\Resources\Files; use Gemini\Resources\GenerativeModel; use Gemini\Resources\Models; @@ -65,4 +66,14 @@ public function chat(ModelType|string $model = ModelType::GEMINI_PRO): ChatSessi { return new ChatSession(model: $this->generativeModel(model: $model)); } + + /** + * Resource to manage media file uploads to be reused across multiple requests and prompts. + * + * @link https://ai.google.dev/api/files + */ + public function files(): Files + { + return new Files($this->transporter); + } } diff --git a/src/Concerns/HasContents.php b/src/Concerns/HasContents.php index f511bac..978ca51 100644 --- a/src/Concerns/HasContents.php +++ b/src/Concerns/HasContents.php @@ -6,17 +6,18 @@ use Gemini\Data\Blob; use Gemini\Data\Content; +use Gemini\Data\UploadedFile; use InvalidArgumentException; trait HasContents { /** - * @param string|Blob|array|Content ...$parts + * @param string|Blob|array|Content|UploadedFile ...$parts * @return array * * @throws InvalidArgumentException */ - public function partsToContents(string|Blob|array|Content ...$parts): array + public function partsToContents(string|Blob|array|Content|UploadedFile ...$parts): array { return array_map( callback: static fn ($part) => Content::parse($part), diff --git a/src/Contracts/Resources/FilesContract.php b/src/Contracts/Resources/FilesContract.php new file mode 100644 index 0000000..9b0467e --- /dev/null +++ b/src/Contracts/Resources/FilesContract.php @@ -0,0 +1,24 @@ +|Content $part + * @param string|Blob|array|Content|UploadedFile $part */ - public static function parse(string|Blob|array|Content $part, Role $role = Role::USER): self + public static function parse(string|Blob|array|Content|UploadedFile $part, Role $role = Role::USER): self { return match (true) { $part instanceof self => $part, $part instanceof Blob => new Content(parts: [new Part(inlineData: $part)], role: $role), + $part instanceof UploadedFile => new Content(parts: [new Part(fileData: $part)], role: $role), is_array($part) => new Content( parts: array_map( callback: static fn ($subPart) => match (true) { is_string($subPart) => new Part(text: $subPart), $subPart instanceof Blob => new Part(inlineData: $subPart), + $subPart instanceof UploadedFile => new Part(fileData: $subPart), }, array: $part, ), @@ -48,7 +50,7 @@ public static function parse(string|Blob|array|Content $part, Role $role = Role: } /** - * @param array{ parts: array{ array{ text: ?string, inlineData: array{ mimeType: string, data: string } } }, role: string } $attributes + * @param array{ parts: array{ array{ text: ?string, inlineData: array{ mimeType: string, data: string }, fileData: array{ fileUri: string, mimeType: string } } }, role: string } $attributes */ public static function from(array $attributes): self { diff --git a/src/Data/Part.php b/src/Data/Part.php index 7b7d93c..ee89855 100644 --- a/src/Data/Part.php +++ b/src/Data/Part.php @@ -14,20 +14,23 @@ final class Part implements Arrayable /** * @param string|null $text Inline text. * @param Blob|null $inlineData Inline media bytes. + * @param UploadedFile|null $fileData Uploaded file information. */ public function __construct( public readonly ?string $text = null, public readonly ?Blob $inlineData = null, + public readonly ?UploadedFile $fileData = null, ) {} /** - * @param array{ text: ?string, inlineData: ?array{ mimeType: string, data: string } } $attributes + * @param array{ text: ?string, inlineData: ?array{ mimeType: string, data: string }, fileData: ?array{ fileUri: string, mimeType: string } } $attributes */ public static function from(array $attributes): self { return new self( text: $attributes['text'] ?? null, - inlineData: isset($attributes['inlineData']) ? Blob::from($attributes['inlineData']) : null + inlineData: isset($attributes['inlineData']) ? Blob::from($attributes['inlineData']) : null, + fileData: isset($attributes['fileData']) ? UploadedFile::from($attributes['fileData']) : null, ); } @@ -43,6 +46,10 @@ public function toArray(): array $data['inlineData'] = $this->inlineData; } + if ($this->fileData !== null) { + $data['fileData'] = $this->fileData; + } + return $data; } } diff --git a/src/Data/UploadedFile.php b/src/Data/UploadedFile.php new file mode 100644 index 0000000..a43ef0b --- /dev/null +++ b/src/Data/UploadedFile.php @@ -0,0 +1,39 @@ + $this->fileUri, + 'mimeType' => $this->mimeType->value, + ]; + } +} diff --git a/src/Data/VideoMetadata.php b/src/Data/VideoMetadata.php new file mode 100644 index 0000000..4f749c9 --- /dev/null +++ b/src/Data/VideoMetadata.php @@ -0,0 +1,34 @@ + $this->videoDuration, + ]; + } +} diff --git a/src/Enums/FileState.php b/src/Enums/FileState.php new file mode 100644 index 0000000..211e29a --- /dev/null +++ b/src/Enums/FileState.php @@ -0,0 +1,28 @@ + false, + self::Active, self::Failed => true, + }; + } +} diff --git a/src/Requests/Files/MetadataGetRequest.php b/src/Requests/Files/MetadataGetRequest.php new file mode 100644 index 0000000..73bba0f --- /dev/null +++ b/src/Requests/Files/MetadataGetRequest.php @@ -0,0 +1,45 @@ +nameOrUri, 'http')) { + return $this->nameOrUri; + } + + return "files/{$this->nameOrUri}"; + } + + public function toRequest(string $baseUrl, array $headers = [], array $queryParams = []): RequestInterface + { + if (str_starts_with($this->resolveEndpoint(), 'http')) { + $baseUrl = ''; + } + + return parent::toRequest($baseUrl, $headers, $queryParams); + } +} diff --git a/src/Requests/Files/UploadRequest.php b/src/Requests/Files/UploadRequest.php new file mode 100644 index 0000000..e57509c --- /dev/null +++ b/src/Requests/Files/UploadRequest.php @@ -0,0 +1,59 @@ + ['display_name' => $this->displayName]]); + $contents = file_get_contents($this->filename); + + $request = $factory + ->createRequest($this->method->value, str_replace('/v1', '/upload/v1', $baseUrl).$this->resolveEndpoint()) + ->withHeader('X-Goog-Upload-Protocol', 'multipart'); + foreach ($headers as $name => $value) { + $request = $request->withHeader($name, $value); + } + $request = $request->withHeader('Content-Type', "multipart/related; boundary={$boundary}") + ->withBody($factory->createStream(<<mimeType->value} + +{$contents} +--{$boundary}-- +BOD)); + + return $request; + } +} diff --git a/src/Requests/GenerativeModel/GenerateContentRequest.php b/src/Requests/GenerativeModel/GenerateContentRequest.php index 4e35a87..9a2e98e 100644 --- a/src/Requests/GenerativeModel/GenerateContentRequest.php +++ b/src/Requests/GenerativeModel/GenerateContentRequest.php @@ -9,6 +9,7 @@ use Gemini\Data\Content; use Gemini\Data\GenerationConfig; use Gemini\Data\SafetySetting; +use Gemini\Data\UploadedFile; use Gemini\Enums\Method; use Gemini\Foundation\Request; use Gemini\Requests\Concerns\HasJsonBody; @@ -21,7 +22,7 @@ class GenerateContentRequest extends Request protected Method $method = Method::POST; /** - * @param array|Content> $parts + * @param array|Content|UploadedFile> $parts * @param array $safetySettings */ public function __construct( diff --git a/src/Resources/ChatSession.php b/src/Resources/ChatSession.php index 7563ec8..7d50d2e 100644 --- a/src/Resources/ChatSession.php +++ b/src/Resources/ChatSession.php @@ -8,6 +8,7 @@ use Gemini\Contracts\Resources\ChatSessionContract; use Gemini\Data\Blob; use Gemini\Data\Content; +use Gemini\Data\UploadedFile; use Gemini\Responses\GenerativeModel\GenerateContentResponse; /** @@ -27,9 +28,9 @@ public function __construct( ) {} /** - * @param string|Blob|array|Content ...$parts + * @param string|Blob|array|Content|UploadedFile ...$parts */ - public function sendMessage(string|Blob|array|Content ...$parts): GenerateContentResponse + public function sendMessage(string|Blob|array|Content|UploadedFile ...$parts): GenerateContentResponse { $this->history = array_merge($this->history, $this->partsToContents(...$parts)); diff --git a/src/Resources/Files.php b/src/Resources/Files.php new file mode 100644 index 0000000..fb81fd4 --- /dev/null +++ b/src/Resources/Files.php @@ -0,0 +1,42 @@ + $response */ + $response = $this->transporter->request(new UploadRequest($filename, $displayName, $mimeType)); + + return UploadResponse::from($response->data())->file; + } + + public function metadataGet(string $nameOrUri): MetadataResponse + { + /** @var ResponseDTO $response */ + $response = $this->transporter->request(new MetadataGetRequest($nameOrUri)); + + return MetadataResponse::from($response->data()); + } +} diff --git a/src/Resources/GenerativeModel.php b/src/Resources/GenerativeModel.php index 85e7a3a..f6b3c41 100644 --- a/src/Resources/GenerativeModel.php +++ b/src/Resources/GenerativeModel.php @@ -11,6 +11,7 @@ use Gemini\Data\Content; use Gemini\Data\GenerationConfig; use Gemini\Data\SafetySetting; +use Gemini\Data\UploadedFile; use Gemini\Enums\ModelType; use Gemini\Requests\GenerativeModel\CountTokensRequest; use Gemini\Requests\GenerativeModel\GenerateContentRequest; @@ -82,9 +83,9 @@ public function countTokens(string|Blob|array|Content ...$parts): CountTokensRes * * @see https://ai.google.dev/api/rest/v1beta/models/generateContent */ - public function generateContent(string|Blob|array|Content ...$parts): GenerateContentResponse + public function generateContent(string|Blob|array|Content|UploadedFile ...$parts): GenerateContentResponse { - /** @var ResponseDTO $response */ + /** @var ResponseDTO $response */ $response = $this->transporter->request( request: new GenerateContentRequest( model: $this->model, diff --git a/src/Responses/Files/MetadataResponse.php b/src/Responses/Files/MetadataResponse.php new file mode 100644 index 0000000..8a01b11 --- /dev/null +++ b/src/Responses/Files/MetadataResponse.php @@ -0,0 +1,69 @@ + $this->name, + 'displayName' => $this->displayName, + 'mimeType' => $this->mimeType, + 'sizeBytes' => $this->sizeBytes, + 'createTime' => $this->createTime, + 'updateTime' => $this->updateTime, + 'expirationTime' => $this->expirationTime, + 'sha256Hash' => $this->sha256Hash, + 'uri' => $this->uri, + 'state' => $this->state->value, + 'videoMetadata' => $this->videoMetadata?->toArray(), + ]; + } +} diff --git a/src/Responses/Files/UploadResponse.php b/src/Responses/Files/UploadResponse.php new file mode 100644 index 0000000..3243e7e --- /dev/null +++ b/src/Responses/Files/UploadResponse.php @@ -0,0 +1,37 @@ + $this->file->toArray(), + ]; + } +} diff --git a/src/Responses/GenerativeModel/GenerateContentResponse.php b/src/Responses/GenerativeModel/GenerateContentResponse.php index c1c3a64..5571d00 100644 --- a/src/Responses/GenerativeModel/GenerateContentResponse.php +++ b/src/Responses/GenerativeModel/GenerateContentResponse.php @@ -96,7 +96,7 @@ public function json(): mixed } /** - * @param array{ candidates: ?array{ array{ content: array{ parts: array{ array{ text: ?string, inlineData: array{ mimeType: string, data: string } } }, role: string }, finishReason: string, safetyRatings: array{ array{ category: string, probability: string, blocked: ?bool } }, citationMetadata: ?array{ citationSources: array{ array{ startIndex: int, endIndex: int, uri: ?string, license: ?string} } }, index: int, tokenCount: ?int, avgLogprobs: ?float } }, promptFeedback: ?array{ safetyRatings: array{ array{ category: string, probability: string, blocked: ?bool } }, blockReason: ?string }, usageMetadata: array{ promptTokenCount: int, candidatesTokenCount: int, totalTokenCount: int, cachedContentTokenCount: ?int } } $attributes + * @param array{ candidates: ?array{ array{ content: array{ parts: array{ array{ text: ?string, inlineData: array{ mimeType: string, data: string }, fileData: array{ fileUri: string, mimeType: string } } }, role: string }, finishReason: string, safetyRatings: array{ array{ category: string, probability: string, blocked: ?bool } }, citationMetadata: ?array{ citationSources: array{ array{ startIndex: int, endIndex: int, uri: ?string, license: ?string} } }, index: int, tokenCount: ?int, avgLogprobs: ?float } }, promptFeedback: ?array{ safetyRatings: array{ array{ category: string, probability: string, blocked: ?bool } }, blockReason: ?string }, usageMetadata: array{ promptTokenCount: int, candidatesTokenCount: int, totalTokenCount: int, cachedContentTokenCount: ?int } } $attributes */ public static function from(array $attributes): self { diff --git a/src/Testing/Responses/Fixtures/Files/MetadataResponseFixture.php b/src/Testing/Responses/Fixtures/Files/MetadataResponseFixture.php new file mode 100644 index 0000000..a1b1869 --- /dev/null +++ b/src/Testing/Responses/Fixtures/Files/MetadataResponseFixture.php @@ -0,0 +1,22 @@ + 'files/123-456', + 'displayName' => 'Display', + 'mimeType' => 'text/plain', + 'sizeBytes' => '321', + 'createTime' => '2014-10-02T15:01:23.045123456Z', + 'updateTime' => '2014-10-02T15:01:23.045234567Z', + 'expirationTime' => '2014-10-04T15:01:23.045123456Z', + 'sha256Hash' => 'OTVmMTI2MGFiMGQ5MTRmNGZlNWNkZWMxN2Y2YWE1MDI5YmNiOTc3ZjdiZWIzZjQ2YjkzNWI4NGRkNjk4MjViNA==', + 'uri' => 'https://generativelanguage.googleapis.com/v1beta/files/c8d2psijx17p', + 'state' => 'PROCESSING', + 'videoMetadata' => ['videoDuration' => '13s'], + ]; +} diff --git a/src/Testing/Responses/Fixtures/Files/UploadResponseFixture.php b/src/Testing/Responses/Fixtures/Files/UploadResponseFixture.php new file mode 100644 index 0000000..c9c3afc --- /dev/null +++ b/src/Testing/Responses/Fixtures/Files/UploadResponseFixture.php @@ -0,0 +1,12 @@ + MetadataResponseFixture::ATTRIBUTES, + ]; +} diff --git a/tests/Client.php b/tests/Client.php index 5765f8a..a780bc1 100644 --- a/tests/Client.php +++ b/tests/Client.php @@ -1,6 +1,7 @@ generativeModel(model: ModelType::GEMINI_PRO))->toBeInstanceOf(GenerativeModel::class); }); + +it('has files', function () { + $gemini = Gemini::client(apiKey: 'foo'); + + expect($gemini->files())->toBeInstanceOf(Files::class); +}); diff --git a/tests/Pest.php b/tests/Pest.php index 3cd394e..baa7d53 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -8,7 +8,7 @@ use Gemini\Transporters\DTOs\ResponseDTO; use Psr\Http\Message\ResponseInterface; -function mockClient(Method $method, string $endpoint, ResponseDTO|ResponseContract|ResponseInterface|string $response, array $params = [], int $times = 1, $methodName = 'request', bool $validateParams = false): Client +function mockClient(Method $method, string $endpoint, ResponseDTO|ResponseContract|ResponseInterface|string $response, array $params = [], int $times = 1, $methodName = 'request', bool $validateParams = false, string $rootPath = '/v1beta/'): Client { $transporter = Mockery::mock(TransporterContract::class); @@ -20,7 +20,7 @@ function mockClient(Method $method, string $endpoint, ResponseDTO|ResponseContra $transporter ->shouldReceive($methodName) ->times($times) - ->withArgs(function (Request $request) use ($validateParams, $method, $endpoint, $params) { + ->withArgs(function (Request $request) use ($validateParams, $method, $endpoint, $params, $rootPath) { $psrRequest = $request->toRequest(baseUrl: 'https://generativelanguage.googleapis.com/v1beta/'); if ($validateParams) { @@ -36,7 +36,7 @@ function mockClient(Method $method, string $endpoint, ResponseDTO|ResponseContra } return $psrRequest->getMethod() === $method->value - && $psrRequest->getUri()->getPath() === "/v1beta/$endpoint"; + && $psrRequest->getUri()->getPath() === $rootPath.$endpoint; })->andReturn($response); return new Client($transporter); diff --git a/tests/Resources/Files.php b/tests/Resources/Files.php new file mode 100644 index 0000000..6538b22 --- /dev/null +++ b/tests/Resources/Files.php @@ -0,0 +1,34 @@ +tmpFile = tmpfile(); + $this->tmpFilepath = stream_get_meta_data($this->tmpFile)['uri']; + }); + afterEach(function () { + fclose($this->tmpFile); + }); + + test('request', function () { + $client = mockClient(method: Method::POST, endpoint: 'files', response: UploadResponse::fake(), rootPath: '/upload/v1beta/'); + + $result = $client->files()->upload($this->tmpFilepath, MimeType::TEXT_PLAIN, 'Display'); + + expect($result) + ->toBeInstanceOf(MetadataResponse::class); + }); +}); + +test('metadata get', function () { + $client = mockClient(method: Method::GET, endpoint: 'files/123-456', response: MetadataResponse::fake()); + + $result = $client->files()->metadataGet('123-456'); + + expect($result) + ->toBeInstanceOf(MetadataResponse::class); +}); diff --git a/tests/Resources/GenerativeModel.php b/tests/Resources/GenerativeModel.php index e1eb40b..2434659 100644 --- a/tests/Resources/GenerativeModel.php +++ b/tests/Resources/GenerativeModel.php @@ -6,10 +6,12 @@ use Gemini\Data\GenerationConfig; use Gemini\Data\PromptFeedback; use Gemini\Data\SafetySetting; +use Gemini\Data\UploadedFile; use Gemini\Data\UsageMetadata; use Gemini\Enums\HarmBlockThreshold; use Gemini\Enums\HarmCategory; use Gemini\Enums\Method; +use Gemini\Enums\MimeType; use Gemini\Enums\ModelType; use Gemini\Resources\ChatSession; use Gemini\Responses\GenerativeModel\CountTokensResponse; @@ -162,6 +164,19 @@ ->usageMetadata->toBeInstanceOf(UsageMetadata::class); }); +test('generate content with uploaded file', function () { + $modelType = ModelType::GEMINI_PRO; + $client = mockClient(method: Method::POST, endpoint: "{$modelType->value}:generateContent", response: GenerateContentResponse::fake()); + + $result = $client->geminiPro()->generateContent(['Analyze file', new UploadedFile('123-456', MimeType::TEXT_PLAIN)]); + + expect($result) + ->toBeInstanceOf(GenerateContentResponse::class) + ->candidates->toBeArray()->each->toBeInstanceOf(Candidate::class) + ->promptFeedback->toBeInstanceOf(PromptFeedback::class) + ->usageMetadata->toBeInstanceOf(UsageMetadata::class); +}); + test('start chat', function () { $modelType = ModelType::GEMINI_PRO; $client = mockClient(method: Method::POST, endpoint: "{$modelType->value}:generateContent", response: GenerateContentResponse::fake(), times: 0); diff --git a/tests/Responses/Files/MetadataResponse.php b/tests/Responses/Files/MetadataResponse.php new file mode 100644 index 0000000..2f6fa12 --- /dev/null +++ b/tests/Responses/Files/MetadataResponse.php @@ -0,0 +1,38 @@ +toArray()); + + expect($response) + ->toBeInstanceOf(MetadataResponse::class) + ->videoMetadata->toBeInstanceOf(VideoMetadata::class); +}); + +test('fake', function () { + $response = MetadataResponse::fake(); + + expect($response) + ->name->toBe('files/123-456'); +}); + +test('to array', function () { + $attributes = MetadataResponse::fake()->toArray(); + + $response = MetadataResponse::from($attributes); + + expect($response->toArray()) + ->toBeArray() + ->toBe($attributes); +}); + +test('fake with override', function () { + $response = MetadataResponse::fake([ + 'name' => 'files/987-654', + ]); + + expect($response) + ->name->toBe('files/987-654'); +}); diff --git a/tests/Responses/Files/UploadResponse.php b/tests/Responses/Files/UploadResponse.php new file mode 100644 index 0000000..437d013 --- /dev/null +++ b/tests/Responses/Files/UploadResponse.php @@ -0,0 +1,40 @@ +toArray()); + + expect($response) + ->toBeInstanceOf(UploadResponse::class) + ->file->toBeInstanceOf(MetadataResponse::class); +}); + +test('fake', function () { + $response = UploadResponse::fake(); + + expect($response) + ->file->name->toBe('files/123-456'); +}); + +test('to array', function () { + $attributes = UploadResponse::fake()->toArray(); + + $response = UploadResponse::from($attributes); + + expect($response->toArray()) + ->toBeArray() + ->toBe($attributes); +}); + +test('fake with override', function () { + $response = UploadResponse::fake([ + 'file' => [ + 'name' => 'files/987-654', + ], + ]); + + expect($response) + ->file->name->toBe('files/987-654'); +}); diff --git a/tests/Testing/Resources/ChatSessionTestResource.php b/tests/Testing/Resources/ChatSessionTestResource.php index 361cfa8..1d4a58f 100644 --- a/tests/Testing/Resources/ChatSessionTestResource.php +++ b/tests/Testing/Resources/ChatSessionTestResource.php @@ -2,6 +2,8 @@ declare(strict_types=1); +use Gemini\Data\UploadedFile; +use Gemini\Enums\MimeType; use Gemini\Enums\ModelType; use Gemini\Resources\ChatSession; use Gemini\Responses\GenerativeModel\GenerateContentResponse; @@ -19,3 +21,17 @@ $parameters[0] === 'Hello'; }); }); + +it('records a chat message request with an uploaded file', function () { + $fake = new ClientFake([ + GenerateContentResponse::fake(), + ]); + + $fake->chat()->sendMessage(['Hello', $file = new UploadedFile('123-456', MimeType::TEXT_PLAIN)]); + + $fake->assertSent(resource: ChatSession::class, model: ModelType::GEMINI_PRO, callback: function (string $method, array $parameters) use ($file) { + return $method === 'sendMessage' && + $parameters[0][0] === 'Hello' && + $parameters[0][1] === $file; + }); +});