From 42241344b16b62debaa9fcfacb1087234ea01872 Mon Sep 17 00:00:00 2001 From: Johann Fradj Date: Sun, 30 Nov 2025 21:42:44 +0100 Subject: [PATCH 01/27] feat: implement Gemini File Search APIs (Stores & Documents) --- src/Client.php | 7 + src/Contracts/ClientContract.php | 3 + .../Resources/FileSearchStoresContract.php | 69 ++++++++ .../FileSearchStores/CreateRequest.php | 42 +++++ .../FileSearchStores/DeleteRequest.php | 31 ++++ .../Documents/DeleteRequest.php | 34 ++++ .../FileSearchStores/Documents/GetRequest.php | 25 +++ .../Documents/ListRequest.php | 46 +++++ src/Requests/FileSearchStores/GetRequest.php | 25 +++ src/Requests/FileSearchStores/ListRequest.php | 45 +++++ .../FileSearchStores/UploadRequest.php | 72 ++++++++ src/Resources/FileSearchStores.php | 91 ++++++++++ .../Documents/DocumentResponse.php | 52 ++++++ .../Documents/ListResponse.php | 49 ++++++ .../FileSearchStoreResponse.php | 40 +++++ .../FileSearchStores/ListResponse.php | 49 ++++++ .../FileSearchStores/UploadResponse.php | 54 ++++++ .../Documents/DocumentResponseFixture.php | 16 ++ .../Documents/ListResponseFixture.php | 15 ++ .../FileSearchStoreResponseFixture.php | 13 ++ .../FileSearchStores/ListResponseFixture.php | 15 ++ .../UploadResponseFixture.php | 14 ++ tests/Resources/FileSearchStores.php | 162 ++++++++++++++++++ 23 files changed, 969 insertions(+) create mode 100644 src/Contracts/Resources/FileSearchStoresContract.php create mode 100644 src/Requests/FileSearchStores/CreateRequest.php create mode 100644 src/Requests/FileSearchStores/DeleteRequest.php create mode 100644 src/Requests/FileSearchStores/Documents/DeleteRequest.php create mode 100644 src/Requests/FileSearchStores/Documents/GetRequest.php create mode 100644 src/Requests/FileSearchStores/Documents/ListRequest.php create mode 100644 src/Requests/FileSearchStores/GetRequest.php create mode 100644 src/Requests/FileSearchStores/ListRequest.php create mode 100644 src/Requests/FileSearchStores/UploadRequest.php create mode 100644 src/Resources/FileSearchStores.php create mode 100644 src/Responses/FileSearchStores/Documents/DocumentResponse.php create mode 100644 src/Responses/FileSearchStores/Documents/ListResponse.php create mode 100644 src/Responses/FileSearchStores/FileSearchStoreResponse.php create mode 100644 src/Responses/FileSearchStores/ListResponse.php create mode 100644 src/Responses/FileSearchStores/UploadResponse.php create mode 100644 src/Testing/Responses/Fixtures/FileSearchStores/Documents/DocumentResponseFixture.php create mode 100644 src/Testing/Responses/Fixtures/FileSearchStores/Documents/ListResponseFixture.php create mode 100644 src/Testing/Responses/Fixtures/FileSearchStores/FileSearchStoreResponseFixture.php create mode 100644 src/Testing/Responses/Fixtures/FileSearchStores/ListResponseFixture.php create mode 100644 src/Testing/Responses/Fixtures/FileSearchStores/UploadResponseFixture.php create mode 100644 tests/Resources/FileSearchStores.php diff --git a/src/Client.php b/src/Client.php index ecad008..f3a8d58 100644 --- a/src/Client.php +++ b/src/Client.php @@ -8,6 +8,7 @@ use Gemini\Contracts\ClientContract; use Gemini\Contracts\Resources\CachedContentsContract; use Gemini\Contracts\Resources\FilesContract; +use Gemini\Contracts\Resources\FileSearchStoresContract; use Gemini\Contracts\Resources\GenerativeModelContract; use Gemini\Contracts\TransporterContract; use Gemini\Enums\ModelType; @@ -15,6 +16,7 @@ use Gemini\Resources\ChatSession; use Gemini\Resources\EmbeddingModel; use Gemini\Resources\Files; +use Gemini\Resources\FileSearchStores; use Gemini\Resources\GenerativeModel; use Gemini\Resources\Models; @@ -81,4 +83,9 @@ public function cachedContents(): CachedContentsContract { return new CachedContents($this->transporter); } + + public function fileSearchStores(): FileSearchStoresContract + { + return new FileSearchStores($this->transporter); + } } diff --git a/src/Contracts/ClientContract.php b/src/Contracts/ClientContract.php index 356f621..481ef01 100644 --- a/src/Contracts/ClientContract.php +++ b/src/Contracts/ClientContract.php @@ -9,6 +9,7 @@ use Gemini\Contracts\Resources\ChatSessionContract; use Gemini\Contracts\Resources\EmbeddingModalContract; use Gemini\Contracts\Resources\FilesContract; +use Gemini\Contracts\Resources\FileSearchStoresContract; use Gemini\Contracts\Resources\GenerativeModelContract; use Gemini\Contracts\Resources\ModelContract; @@ -35,4 +36,6 @@ public function chat(BackedEnum|string $model): ChatSessionContract; public function files(): FilesContract; public function cachedContents(): CachedContentsContract; + + public function fileSearchStores(): FileSearchStoresContract; } diff --git a/src/Contracts/Resources/FileSearchStoresContract.php b/src/Contracts/Resources/FileSearchStoresContract.php new file mode 100644 index 0000000..c75a878 --- /dev/null +++ b/src/Contracts/Resources/FileSearchStoresContract.php @@ -0,0 +1,69 @@ + + */ + public function defaultBody(): array + { + $body = []; + + if ($this->displayName !== null) { + $body['displayName'] = $this->displayName; + } + + return $body; + } +} diff --git a/src/Requests/FileSearchStores/DeleteRequest.php b/src/Requests/FileSearchStores/DeleteRequest.php new file mode 100644 index 0000000..45a696c --- /dev/null +++ b/src/Requests/FileSearchStores/DeleteRequest.php @@ -0,0 +1,31 @@ +name; + } + + public function defaultQuery(): array + { + return $this->force ? ['force' => 'true'] : []; + } +} diff --git a/src/Requests/FileSearchStores/Documents/DeleteRequest.php b/src/Requests/FileSearchStores/Documents/DeleteRequest.php new file mode 100644 index 0000000..fa8825d --- /dev/null +++ b/src/Requests/FileSearchStores/Documents/DeleteRequest.php @@ -0,0 +1,34 @@ +name; + } + + /** + * @return array + */ + public function defaultQuery(): array + { + return $this->force ? ['force' => 'true'] : []; + } +} diff --git a/src/Requests/FileSearchStores/Documents/GetRequest.php b/src/Requests/FileSearchStores/Documents/GetRequest.php new file mode 100644 index 0000000..a436a57 --- /dev/null +++ b/src/Requests/FileSearchStores/Documents/GetRequest.php @@ -0,0 +1,25 @@ +name; + } +} diff --git a/src/Requests/FileSearchStores/Documents/ListRequest.php b/src/Requests/FileSearchStores/Documents/ListRequest.php new file mode 100644 index 0000000..f275994 --- /dev/null +++ b/src/Requests/FileSearchStores/Documents/ListRequest.php @@ -0,0 +1,46 @@ +pageSize !== null) { + $query['pageSize'] = $this->pageSize; + } + + if ($this->nextPageToken !== null) { + $query['pageToken'] = $this->nextPageToken; + } + + return $query; + } + + public function resolveEndpoint(): string + { + return $this->storeName.'/documents'; + } +} diff --git a/src/Requests/FileSearchStores/GetRequest.php b/src/Requests/FileSearchStores/GetRequest.php new file mode 100644 index 0000000..753c98f --- /dev/null +++ b/src/Requests/FileSearchStores/GetRequest.php @@ -0,0 +1,25 @@ +name; + } +} diff --git a/src/Requests/FileSearchStores/ListRequest.php b/src/Requests/FileSearchStores/ListRequest.php new file mode 100644 index 0000000..ddf001c --- /dev/null +++ b/src/Requests/FileSearchStores/ListRequest.php @@ -0,0 +1,45 @@ +pageSize !== null) { + $query['pageSize'] = $this->pageSize; + } + + if ($this->nextPageToken !== null) { + $query['pageToken'] = $this->nextPageToken; + } + + return $query; + } + + public function resolveEndpoint(): string + { + return 'fileSearchStores'; + } +} diff --git a/src/Requests/FileSearchStores/UploadRequest.php b/src/Requests/FileSearchStores/UploadRequest.php new file mode 100644 index 0000000..21c21bf --- /dev/null +++ b/src/Requests/FileSearchStores/UploadRequest.php @@ -0,0 +1,72 @@ +storeName.':uploadToFileSearchStore'; + } + + public function toRequest(string $baseUrl, array $headers = [], array $queryParams = []): RequestInterface + { + $factory = new Psr17Factory; + $boundary = rand(111111, 999999); + + $metadata = []; + if ($this->displayName) { + $metadata['displayName'] = $this->displayName; + } + if ($this->mimeType) { + $metadata['mimeType'] = $this->mimeType->value; + } + + $requestJson = json_encode($metadata); + $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); + } + + $contentType = $this->mimeType instanceof MimeType ? $this->mimeType->value : 'application/octet-stream'; + + $request = $request->withHeader('Content-Type', "multipart/related; boundary={$boundary}") + ->withBody($factory->createStream(<< $response */ + $response = $this->transporter->request(new CreateRequest($displayName)); + + return FileSearchStoreResponse::from($response->data()); + } + + public function get(string $name): FileSearchStoreResponse + { + /** @var ResponseDTO $response */ + $response = $this->transporter->request(new GetRequest($name)); + + return FileSearchStoreResponse::from($response->data()); + } + + public function list(?int $pageSize = null, ?string $nextPageToken = null): ListResponse + { + /** @var ResponseDTO, nextPageToken: ?string }> $response */ + $response = $this->transporter->request(new ListRequest(pageSize: $pageSize, nextPageToken: $nextPageToken)); + + return ListResponse::from($response->data()); + } + + public function delete(string $name, bool $force = false): void + { + $this->transporter->request(new DeleteRequest($name, $force)); + } + + public function upload(string $storeName, string $filename, ?MimeType $mimeType = null, ?string $displayName = null): UploadResponse + { + $mimeType ??= MimeType::from((string) mime_content_type($filename)); + $displayName ??= $filename; + + /** @var ResponseDTO, done: bool, response?: array, error?: array }> $response */ + $response = $this->transporter->request(new UploadRequest($storeName, $filename, $displayName, $mimeType)); + + return UploadResponse::from($response->data()); + } + + public function listDocuments(string $storeName, ?int $pageSize = null, ?string $nextPageToken = null): DocumentListResponse + { + /** @var ResponseDTO, updateTime?: string, createTime?: string }>, nextPageToken: ?string }> $response */ + $response = $this->transporter->request(new ListDocumentsRequest($storeName, $pageSize, $nextPageToken)); + + return DocumentListResponse::from($response->data()); + } + + public function getDocument(string $name): DocumentResponse + { + /** @var ResponseDTO, updateTime?: string, createTime?: string }> $response */ + $response = $this->transporter->request(new GetDocumentRequest($name)); + + return DocumentResponse::from($response->data()); + } + + public function deleteDocument(string $name, bool $force = false): void + { + $this->transporter->request(new DeleteDocumentRequest($name, $force)); + } +} diff --git a/src/Responses/FileSearchStores/Documents/DocumentResponse.php b/src/Responses/FileSearchStores/Documents/DocumentResponse.php new file mode 100644 index 0000000..fbebbc5 --- /dev/null +++ b/src/Responses/FileSearchStores/Documents/DocumentResponse.php @@ -0,0 +1,52 @@ + $customMetadata + */ + public function __construct( + public readonly string $name, + public readonly ?string $displayName = null, + public readonly array $customMetadata = [], + public readonly ?string $updateTime = null, + public readonly ?string $createTime = null, + ) {} + + /** + * @param array{ name: string, displayName?: string, customMetadata?: array, updateTime?: string, createTime?: string } $attributes + */ + public static function from(array $attributes): self + { + return new self( + name: $attributes['name'], + displayName: $attributes['displayName'] ?? null, + customMetadata: $attributes['customMetadata'] ?? [], + updateTime: $attributes['updateTime'] ?? null, + createTime: $attributes['createTime'] ?? null, + ); + } + + public function toArray(): array + { + return [ + 'name' => $this->name, + 'displayName' => $this->displayName, + 'customMetadata' => $this->customMetadata, + 'updateTime' => $this->updateTime, + 'createTime' => $this->createTime, + ]; + } +} diff --git a/src/Responses/FileSearchStores/Documents/ListResponse.php b/src/Responses/FileSearchStores/Documents/ListResponse.php new file mode 100644 index 0000000..5036d3e --- /dev/null +++ b/src/Responses/FileSearchStores/Documents/ListResponse.php @@ -0,0 +1,49 @@ + $documents + */ + public function __construct( + public readonly array $documents, + public readonly ?string $nextPageToken = null, + ) {} + + /** + * @param array{ documents: ?array, updateTime?: string, createTime?: string }>, nextPageToken: ?string } $attributes + */ + public static function from(array $attributes): self + { + return new self( + documents: array_map( + fn (array $document): DocumentResponse => DocumentResponse::from($document), + $attributes['documents'] ?? [] + ), + nextPageToken: $attributes['nextPageToken'] ?? null, + ); + } + + public function toArray(): array + { + return [ + 'documents' => array_map( + fn (DocumentResponse $document): array => $document->toArray(), + $this->documents + ), + 'nextPageToken' => $this->nextPageToken, + ]; + } +} diff --git a/src/Responses/FileSearchStores/FileSearchStoreResponse.php b/src/Responses/FileSearchStores/FileSearchStoreResponse.php new file mode 100644 index 0000000..feab3a0 --- /dev/null +++ b/src/Responses/FileSearchStores/FileSearchStoreResponse.php @@ -0,0 +1,40 @@ + $this->name, + 'displayName' => $this->displayName, + ]; + } +} diff --git a/src/Responses/FileSearchStores/ListResponse.php b/src/Responses/FileSearchStores/ListResponse.php new file mode 100644 index 0000000..08035e9 --- /dev/null +++ b/src/Responses/FileSearchStores/ListResponse.php @@ -0,0 +1,49 @@ + $fileSearchStores + */ + public function __construct( + public readonly array $fileSearchStores, + public readonly ?string $nextPageToken = null, + ) {} + + /** + * @param array{ fileSearchStores: ?array, nextPageToken: ?string } $attributes + */ + public static function from(array $attributes): self + { + return new self( + fileSearchStores: array_map( + fn (array $store): FileSearchStoreResponse => FileSearchStoreResponse::from($store), + $attributes['fileSearchStores'] ?? [] + ), + nextPageToken: $attributes['nextPageToken'] ?? null, + ); + } + + public function toArray(): array + { + return [ + 'fileSearchStores' => array_map( + fn (FileSearchStoreResponse $store): array => $store->toArray(), + $this->fileSearchStores + ), + 'nextPageToken' => $this->nextPageToken, + ]; + } +} diff --git a/src/Responses/FileSearchStores/UploadResponse.php b/src/Responses/FileSearchStores/UploadResponse.php new file mode 100644 index 0000000..c94d34f --- /dev/null +++ b/src/Responses/FileSearchStores/UploadResponse.php @@ -0,0 +1,54 @@ + $metadata + * @param array|null $response + * @param array|null $error + */ + public function __construct( + public readonly string $name, + public readonly bool $done, + public readonly ?array $metadata = null, + public readonly ?array $response = null, + public readonly ?array $error = null, + ) {} + + /** + * @param array{ name: string, done: bool, metadata?: array, response?: array, error?: array } $attributes + */ + public static function from(array $attributes): self + { + return new self( + name: $attributes['name'], + done: $attributes['done'] ?? false, + metadata: $attributes['metadata'] ?? null, + response: $attributes['response'] ?? null, + error: $attributes['error'] ?? null, + ); + } + + public function toArray(): array + { + return [ + 'name' => $this->name, + 'done' => $this->done, + 'metadata' => $this->metadata, + 'response' => $this->response, + 'error' => $this->error, + ]; + } +} diff --git a/src/Testing/Responses/Fixtures/FileSearchStores/Documents/DocumentResponseFixture.php b/src/Testing/Responses/Fixtures/FileSearchStores/Documents/DocumentResponseFixture.php new file mode 100644 index 0000000..0a3f2e7 --- /dev/null +++ b/src/Testing/Responses/Fixtures/FileSearchStores/Documents/DocumentResponseFixture.php @@ -0,0 +1,16 @@ + 'fileSearchStores/123/documents/abc', + 'displayName' => 'My Document', + 'customMetadata' => [], + 'updateTime' => '2024-01-01T00:00:00Z', + 'createTime' => '2024-01-01T00:00:00Z', + ]; +} diff --git a/src/Testing/Responses/Fixtures/FileSearchStores/Documents/ListResponseFixture.php b/src/Testing/Responses/Fixtures/FileSearchStores/Documents/ListResponseFixture.php new file mode 100644 index 0000000..d7bcde0 --- /dev/null +++ b/src/Testing/Responses/Fixtures/FileSearchStores/Documents/ListResponseFixture.php @@ -0,0 +1,15 @@ + [ + DocumentResponseFixture::ATTRIBUTES, + ], + 'nextPageToken' => 'next-page-token', + ]; +} diff --git a/src/Testing/Responses/Fixtures/FileSearchStores/FileSearchStoreResponseFixture.php b/src/Testing/Responses/Fixtures/FileSearchStores/FileSearchStoreResponseFixture.php new file mode 100644 index 0000000..4ee27e2 --- /dev/null +++ b/src/Testing/Responses/Fixtures/FileSearchStores/FileSearchStoreResponseFixture.php @@ -0,0 +1,13 @@ + 'fileSearchStores/123-456', + 'displayName' => 'My Store', + ]; +} diff --git a/src/Testing/Responses/Fixtures/FileSearchStores/ListResponseFixture.php b/src/Testing/Responses/Fixtures/FileSearchStores/ListResponseFixture.php new file mode 100644 index 0000000..481bfa4 --- /dev/null +++ b/src/Testing/Responses/Fixtures/FileSearchStores/ListResponseFixture.php @@ -0,0 +1,15 @@ + [ + FileSearchStoreResponseFixture::ATTRIBUTES, + ], + 'nextPageToken' => 'next-page-token', + ]; +} diff --git a/src/Testing/Responses/Fixtures/FileSearchStores/UploadResponseFixture.php b/src/Testing/Responses/Fixtures/FileSearchStores/UploadResponseFixture.php new file mode 100644 index 0000000..0c58487 --- /dev/null +++ b/src/Testing/Responses/Fixtures/FileSearchStores/UploadResponseFixture.php @@ -0,0 +1,14 @@ + 'operations/123-456', + 'metadata' => ['some' => 'meta'], + 'done' => false, + ]; +} diff --git a/tests/Resources/FileSearchStores.php b/tests/Resources/FileSearchStores.php new file mode 100644 index 0000000..392b418 --- /dev/null +++ b/tests/Resources/FileSearchStores.php @@ -0,0 +1,162 @@ + 'My Store'], + validateParams: true + ); + + $result = $client->fileSearchStores()->create('My Store'); + + expect($result) + ->toBeInstanceOf(FileSearchStoreResponse::class) + ->name->toBe('fileSearchStores/123-456') + ->displayName->toBe('My Store'); +}); + +test('get', function () { + $client = mockClient( + method: Method::GET, + endpoint: 'fileSearchStores/123-456', + response: FileSearchStoreResponse::fake() + ); + + $result = $client->fileSearchStores()->get('fileSearchStores/123-456'); + + expect($result) + ->toBeInstanceOf(FileSearchStoreResponse::class) + ->name->toBe('fileSearchStores/123-456'); +}); + +test('list', function () { + $client = mockClient( + method: Method::GET, + endpoint: 'fileSearchStores', + response: ListResponse::fake() + ); + + $result = $client->fileSearchStores()->list(); + + expect($result) + ->toBeInstanceOf(ListResponse::class) + ->fileSearchStores->toHaveCount(1) + ->fileSearchStores->each->toBeInstanceOf(FileSearchStoreResponse::class); +}); + +test('delete', function () { + $client = mockClient( + method: Method::DELETE, + endpoint: 'fileSearchStores/123-456', + response: new \Gemini\Transporters\DTOs\ResponseDTO([]) + ); + + $client->fileSearchStores()->delete('fileSearchStores/123-456'); + + // If no exception, it passed. + expect(true)->toBeTrue(); +}); + +test('delete with force', function () { + $client = mockClient( + method: Method::DELETE, + endpoint: 'fileSearchStores/123-456', + response: new \Gemini\Transporters\DTOs\ResponseDTO([]), + params: ['force' => 'true'], + validateParams: true + ); + + $client->fileSearchStores()->delete('fileSearchStores/123-456', true); + + expect(true)->toBeTrue(); +}); + +describe('upload', function () { + beforeEach(function () { + $this->tmpFile = tmpfile(); + $this->tmpFilepath = stream_get_meta_data($this->tmpFile)['uri']; + }); + afterEach(function () { + fclose($this->tmpFile); + }); + + test('upload', function () { + $client = mockClient( + method: Method::POST, + endpoint: 'fileSearchStores/123:uploadToFileSearchStore', + response: UploadResponse::fake(), + rootPath: '/upload/v1beta/' + ); + + $result = $client->fileSearchStores()->upload('fileSearchStores/123', $this->tmpFilepath, MimeType::TEXT_PLAIN, 'Display'); + + expect($result) + ->toBeInstanceOf(UploadResponse::class) + ->name->toBe('operations/123-456'); + }); +}); + +test('list documents', function () { + $client = mockClient( + method: Method::GET, + endpoint: 'fileSearchStores/123/documents', + response: DocumentListResponse::fake() + ); + + $result = $client->fileSearchStores()->listDocuments('fileSearchStores/123'); + + expect($result) + ->toBeInstanceOf(DocumentListResponse::class) + ->documents->toHaveCount(1) + ->documents->each->toBeInstanceOf(DocumentResponse::class); +}); + +test('get document', function () { + $client = mockClient( + method: Method::GET, + endpoint: 'fileSearchStores/123/documents/abc', + response: DocumentResponse::fake() + ); + + $result = $client->fileSearchStores()->getDocument('fileSearchStores/123/documents/abc'); + + expect($result) + ->toBeInstanceOf(DocumentResponse::class) + ->name->toBe('fileSearchStores/123/documents/abc'); +}); + +test('delete document', function () { + $client = mockClient( + method: Method::DELETE, + endpoint: 'fileSearchStores/123/documents/abc', + response: new \Gemini\Transporters\DTOs\ResponseDTO([]) + ); + + $client->fileSearchStores()->deleteDocument('fileSearchStores/123/documents/abc'); + + expect(true)->toBeTrue(); +}); + +test('delete document with force', function () { + $client = mockClient( + method: Method::DELETE, + endpoint: 'fileSearchStores/123/documents/abc', + response: new \Gemini\Transporters\DTOs\ResponseDTO([]), + params: ['force' => 'true'], + validateParams: true + ); + + $client->fileSearchStores()->deleteDocument('fileSearchStores/123/documents/abc', true); + + expect(true)->toBeTrue(); +}); From 01560091a31497989f96a2aa991b7c585d207c17 Mon Sep 17 00:00:00 2001 From: Johann Fradj Date: Mon, 1 Dec 2025 11:36:01 +0100 Subject: [PATCH 02/27] feat: Add Google Maps and File Search tool support This commit introduces comprehensive support for Google Maps and File Search tools, enhancing the client's grounding capabilities. --- README.md | 56 ++++++++++++++ src/Data/FileSearch.php | 46 ++++++++++++ src/Data/GoogleMaps.php | 41 ++++++++++ src/Data/RetrievalConfig.php | 43 +++++++++++ src/Data/Tool.php | 10 +++ src/Data/ToolConfig.php | 15 +++- src/Testing/ClientFake.php | 6 ++ .../FileSearchStoresTestResource.php | 65 ++++++++++++++++ tests/Data/Tool.php | 48 ++++++++++++ tests/Data/ToolConfig.php | 75 +++++++++++++++++++ 10 files changed, 402 insertions(+), 3 deletions(-) create mode 100644 src/Data/FileSearch.php create mode 100644 src/Data/GoogleMaps.php create mode 100644 src/Data/RetrievalConfig.php create mode 100644 src/Testing/Resources/FileSearchStoresTestResource.php create mode 100644 tests/Data/Tool.php create mode 100644 tests/Data/ToolConfig.php diff --git a/README.md b/README.md index 2601a22..8ab36ae 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,8 @@ - [Function calling](#function-calling) - [Code Execution](#code-execution) - [Grounding with Google Search](#grounding-with-google-search) + - [Grounding with Google Maps](#grounding-with-google-maps) + - [Grounding with File Search](#grounding-with-file-search) - [System Instructions](#system-instructions) - [Speech generation](#speech-generation) - [Thinking Mode](#thinking-mode) @@ -482,6 +484,59 @@ if ($groundingMetadata !== null) { } ``` +#### Grounding with Google Maps +Grounding with Google Maps allows the model to utilize real-world geographical data. This enables more precise location-based responses, such as finding nearby points of interest. + +```php +use Gemini\Data\GoogleMaps; +use Gemini\Data\RetrievalConfig; +use Gemini\Data\Tool; +use Gemini\Data\ToolConfig; + +$tool = new Tool( + googleMaps: new GoogleMaps(enableWidget: true) +); + +$toolConfig = new ToolConfig( + retrievalConfig: new RetrievalConfig( + latitude: 40.758896, + longitude: -73.985130 + ) +); + +$response = $client + ->generativeModel(model: 'gemini-2.0-flash') + ->withTool($tool) + ->withToolConfig($toolConfig) + ->generateContent('Find coffee shops near me'); + +echo $response->text(); +// (Model output referencing coffee shops) +``` + +#### Grounding with File Search +Grounding with File Search enables the model to retrieve and utilize information from your indexed files. This is useful for answering questions based on private or extensive document collections. + +```php +use Gemini\Data\FileSearch; +use Gemini\Data\Tool; + +$tool = new Tool( + fileSearch: new FileSearch( + fileSearchStoreNames: ['files/my-document-store'], + metadataFilter: 'author = "Robert Graves"' + ) +); + +$response = $client + ->generativeModel(model: 'gemini-2.0-flash') + ->withTool($tool) + ->generateContent('Summarize the document about Greek myths by Robert Graves'); + +echo $response->text(); +// (Model output summarizing the document) +``` + #### System Instructions System instructions let you steer the behavior of the model based on your specific needs and use cases. You can set the role and personality of the model, define the format of responses, and provide goals and guardrails for model behavior. @@ -631,6 +686,7 @@ Every prompt you send to the model includes parameter values that control how th Also, you can use safety settings to adjust the likelihood of getting responses that may be considered harmful. By default, safety settings block content with medium and/or high probability of being unsafe content across all dimensions. Learn more about [safety settings](https://ai.google.dev/docs/concepts#safety_setting). +When using tools like `FileSearch`, you may also provide additional configuration via `ToolConfig`, such as `RetrievalConfig` for geographical context. ```php use Gemini\Data\GenerationConfig; diff --git a/src/Data/FileSearch.php b/src/Data/FileSearch.php new file mode 100644 index 0000000..34a7552 --- /dev/null +++ b/src/Data/FileSearch.php @@ -0,0 +1,46 @@ + $fileSearchStoreNames Required. The file search store names. + * @param string|null $metadataFilter Optional. A filter for metadata. + */ + public function __construct( + public readonly array $fileSearchStoreNames, + public readonly ?string $metadataFilter = null, + ) {} + + /** + * @param array{ fileSearchStoreNames: array, metadataFilter?: string } $attributes + */ + public static function from(array $attributes): self + { + return new self( + fileSearchStoreNames: $attributes['fileSearchStoreNames'], + metadataFilter: $attributes['metadataFilter'] ?? null, + ); + } + + public function toArray(): array + { + $data = [ + 'fileSearchStoreNames' => $this->fileSearchStoreNames, + ]; + + if ($this->metadataFilter !== null) { + $data['metadataFilter'] = $this->metadataFilter; + } + + return $data; + } +} diff --git a/src/Data/GoogleMaps.php b/src/Data/GoogleMaps.php new file mode 100644 index 0000000..6eff13c --- /dev/null +++ b/src/Data/GoogleMaps.php @@ -0,0 +1,41 @@ +|stdClass + */ + public function toArray(): array|stdClass + { + if ($this->enableWidget === null) { + return new stdClass; + } + + return [ + 'enableWidget' => $this->enableWidget, + ]; + } +} diff --git a/src/Data/RetrievalConfig.php b/src/Data/RetrievalConfig.php new file mode 100644 index 0000000..410dd50 --- /dev/null +++ b/src/Data/RetrievalConfig.php @@ -0,0 +1,43 @@ + [ + 'latitude' => $this->latitude, + 'longitude' => $this->longitude, + ], + ]; + } +} diff --git a/src/Data/Tool.php b/src/Data/Tool.php index 0dec85a..039da6f 100644 --- a/src/Data/Tool.php +++ b/src/Data/Tool.php @@ -27,6 +27,8 @@ public function __construct( public ?CodeExecution $codeExecution = null, public ?GoogleSearch $googleSearch = null, public ?UrlContext $urlContext = null, + public ?GoogleMaps $googleMaps = null, + public ?FileSearch $fileSearch = null, ) {} public function toArray(): array @@ -56,6 +58,14 @@ public function toArray(): array $data['url_context'] = $this->urlContext->toArray(); } + if ($this->googleMaps !== null) { + $data['googleMaps'] = $this->googleMaps->toArray(); + } + + if ($this->fileSearch !== null) { + $data['fileSearch'] = $this->fileSearch->toArray(); + } + return $data; } } diff --git a/src/Data/ToolConfig.php b/src/Data/ToolConfig.php index 3ed580a..d9469d0 100644 --- a/src/Data/ToolConfig.php +++ b/src/Data/ToolConfig.php @@ -15,12 +15,21 @@ final class ToolConfig implements Arrayable { public function __construct( public ?FunctionCallingConfig $functionCallingConfig = null, + public ?RetrievalConfig $retrievalConfig = null, ) {} public function toArray(): array { - return [ - 'functionCallingConfig' => $this->functionCallingConfig?->toArray(), - ]; + $data = []; + + if ($this->functionCallingConfig !== null) { + $data['functionCallingConfig'] = $this->functionCallingConfig->toArray(); + } + + if ($this->retrievalConfig !== null) { + $data['retrievalConfig'] = $this->retrievalConfig->toArray(); + } + + return $data; } } diff --git a/src/Testing/ClientFake.php b/src/Testing/ClientFake.php index 5a95f12..78c4ded 100644 --- a/src/Testing/ClientFake.php +++ b/src/Testing/ClientFake.php @@ -14,6 +14,7 @@ use Gemini\Testing\Resources\CachedContentsTestResource; use Gemini\Testing\Resources\ChatSessionTestResource; use Gemini\Testing\Resources\EmbeddingModelTestResource; +use Gemini\Testing\Resources\FileSearchStoresTestResource; use Gemini\Testing\Resources\FilesTestResource; use Gemini\Testing\Resources\GenerativeModelTestResource; use Gemini\Testing\Resources\ModelTestResource; @@ -251,4 +252,9 @@ public function cachedContents(): CachedContentsTestResource { return new CachedContentsTestResource(fake: $this); } + + public function fileSearchStores(): FileSearchStoresTestResource + { + return new FileSearchStoresTestResource(fake: $this); + } } diff --git a/src/Testing/Resources/FileSearchStoresTestResource.php b/src/Testing/Resources/FileSearchStoresTestResource.php new file mode 100644 index 0000000..0565df4 --- /dev/null +++ b/src/Testing/Resources/FileSearchStoresTestResource.php @@ -0,0 +1,65 @@ +record(method: __FUNCTION__, args: func_get_args()); + } + + public function get(string $name): FileSearchStoreResponse + { + return $this->record(method: __FUNCTION__, args: func_get_args()); + } + + public function list(?int $pageSize = null, ?string $nextPageToken = null): ListResponse + { + return $this->record(method: __FUNCTION__, args: func_get_args()); + } + + public function delete(string $name, bool $force = false): void + { + $this->record(method: __FUNCTION__, args: func_get_args()); + } + + public function upload(string $storeName, string $filename, ?MimeType $mimeType = null, ?string $displayName = null): UploadResponse + { + return $this->record(method: __FUNCTION__, args: func_get_args()); + } + + public function listDocuments(string $storeName, ?int $pageSize = null, ?string $nextPageToken = null): DocumentListResponse + { + return $this->record(method: __FUNCTION__, args: func_get_args()); + } + + public function getDocument(string $name): DocumentResponse + { + return $this->record(method: __FUNCTION__, args: func_get_args()); + } + + public function deleteDocument(string $name, bool $force = false): void + { + $this->record(method: __FUNCTION__, args: func_get_args()); + } +} diff --git a/tests/Data/Tool.php b/tests/Data/Tool.php new file mode 100644 index 0000000..783a5e5 --- /dev/null +++ b/tests/Data/Tool.php @@ -0,0 +1,48 @@ +toArray()) + ->toEqual([ + 'googleMaps' => new stdClass, + 'fileSearch' => [ + 'fileSearchStoreNames' => ['test-store'], + ], + ]); +}); + +test('to array with google maps arguments', function () { + $tool = new Tool( + googleMaps: new GoogleMaps(enableWidget: true), + ); + + expect($tool->toArray()) + ->toEqual([ + 'googleMaps' => ['enableWidget' => true], + ]); +}); + +test('to array with file search arguments', function () { + $tool = new Tool( + fileSearch: new FileSearch( + fileSearchStoreNames: ['test-store-2'], + metadataFilter: 'author = "Robert Graves"' + ), + ); + + expect($tool->toArray()) + ->toEqual([ + 'fileSearch' => [ + 'fileSearchStoreNames' => ['test-store-2'], + 'metadataFilter' => 'author = "Robert Graves"', + ], + ]); +}); diff --git a/tests/Data/ToolConfig.php b/tests/Data/ToolConfig.php new file mode 100644 index 0000000..44d71cd --- /dev/null +++ b/tests/Data/ToolConfig.php @@ -0,0 +1,75 @@ +toArray()) + ->toBe([ + 'functionCallingConfig' => [ + 'mode' => 'AUTO', + 'allowedFunctionNames' => null, + ], + 'retrievalConfig' => [ + 'latLng' => [ + 'latitude' => 40.758896, + 'longitude' => -73.985130, + ], + ], + ]); +}); + +test('to array with only function calling config', function () { + $toolConfig = new ToolConfig( + functionCallingConfig: new FunctionCallingConfig(mode: Mode::AUTO), + retrievalConfig: null, + ); + + expect($toolConfig->toArray()) + ->toBe([ + 'functionCallingConfig' => [ + 'mode' => 'AUTO', + 'allowedFunctionNames' => null, + ], + ]); +}); + +test('to array with only retrieval config', function () { + $toolConfig = new ToolConfig( + functionCallingConfig: null, + retrievalConfig: new RetrievalConfig( + latitude: 40.758896, + longitude: -73.985130 + ) + ); + + expect($toolConfig->toArray()) + ->toBe([ + 'retrievalConfig' => [ + 'latLng' => [ + 'latitude' => 40.758896, + 'longitude' => -73.985130, + ], + ], + ]); +}); + +test('to array with no config', function () { + $toolConfig = new ToolConfig( + functionCallingConfig: null, + retrievalConfig: null, + ); + + expect($toolConfig->toArray()) + ->toBe([]); +}); From 9c8a1fa416c960e9ebbfdc4a57c51d773f19700c Mon Sep 17 00:00:00 2001 From: Johann Fradj Date: Mon, 1 Dec 2025 14:45:38 +0100 Subject: [PATCH 03/27] docs: Update README.md with File Search APIs documentation --- README.md | 172 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 172 insertions(+) diff --git a/README.md b/README.md index 2601a22..38f9662 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,17 @@ - [Update Cached Content](#update-cached-content) - [Delete Cached Content](#delete-cached-content) - [Use Cached Content](#use-cached-content) + - [File Search Stores](#file-search-stores) + - [Create File Search Store](#create-file-search-store) + - [Get File Search Store](#get-file-search-store) + - [List File Search Stores](#list-file-search-stores) + - [Delete File Search Store](#delete-file-search-store) + - [Update File Search Store](#update-file-search-store) + - [File Search Documents](#file-search-documents) + - [Create File Search Document](#create-file-search-document) + - [Get File Search Document](#get-file-search-document) + - [List File Search Documents](#list-file-search-documents) + - [Delete File Search Document](#delete-file-search-document) - [Embedding Resource](#embedding-resource) - [Models](#models) - [List Models](#list-models) @@ -834,6 +845,167 @@ echo "Cached tokens used: {$response->usageMetadata->cachedContentTokenCount}\n" echo "New tokens used: {$response->usageMetadata->promptTokenCount}\n"; ``` +### File Search Stores + +File search allows you to search files that were uploaded through the File API. + +#### Create File Search Store +Create a file search store. + +```php +use Gemini\Enums\FileState; +use Gemini\Enums\MimeType; +use Gemini\Enums\Schema; +use Gemini\Enums\DataType; + +$files = $client->files(); +echo "Uploading\n"; +$meta = $files->upload( + filename: 'document.pdf', + mimeType: MimeType::APPLICATION_PDF, + displayName: 'Document for search' +); +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)); +} + +$fileSearchStore = $client->fileSearchStores()->create( + displayName: 'My Search Store', + defaultSchema: new Schema( + declarations: [ + 'name' => new Schema(type: DataType::STRING), + 'size' => new Schema(type: DataType::INTEGER), + ], + ), + defaultDocumentConfig: [ + 'files/'.basename($meta->uri), + ], +); + +echo "File search store created: {$fileSearchStore->name}\n"; +``` + +#### Get File Search Store +Get a specific file search store by name. + +```php +$fileSearchStore = $client->fileSearchStores()->retrieve('fileSearchStores/my-search-store'); + +echo "Name: {$fileSearchStore->name}\n"; +echo "Display Name: {$fileSearchStore->displayName}\n"; +``` + +#### List File Search Stores +List all file search stores. + +```php +$response = $client->fileSearchStores()->list(pageSize: 10); + +foreach ($response->fileSearchStores as $fileSearchStore) { + echo "Name: {$fileSearchStore->name}\n"; + echo "Display Name: {$fileSearchStore->displayName}\n"; + echo "Create Time: {$fileSearchStore->createTime}\n"; + echo "Update Time: {$fileSearchStore->updateTime}\n"; + echo "--- \n"; +} +``` + +#### Delete File Search Store +Delete a file search store by name. + +```php +$client->fileSearchStores()->delete('fileSearchStores/my-search-store'); +``` + +#### Update File Search Store +Update a file search store. + +```php +$fileSearchStore = $client->fileSearchStores()->update( + name: 'fileSearchStores/my-search-store', + displayName: 'My Updated Search Store', +); + +echo "File search store updated: {$fileSearchStore->name}\n"; +``` + +### File Search Documents + +#### Create File Search Document +Create a file search document within a store. + +```php +use Gemini\Enums\FileState; +use Gemini\Enums\MimeType; + +$files = $client->files(); +echo "Uploading\n"; +$meta = $files->upload( + filename: 'document2.pdf', + mimeType: MimeType::APPLICATION_PDF, + displayName: 'Another document for search' +); +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)); +} + +$fileSearchDocument = $client->fileSearchDocuments()->create( + parent: 'fileSearchStores/my-search-store', + file: 'files/'.basename($meta->uri), + displayName: 'Another Search Document', +); + +echo "File search document created: {$fileSearchDocument->name}\n"; +``` + +#### Get File Search Document +Get a specific file search document by name. + +```php +$fileSearchDocument = $client->fileSearchDocuments()->retrieve('fileSearchStores/my-search-store/fileSearchDocuments/my-document'); + +echo "Name: {$fileSearchDocument->name}\n"; +echo "Display Name: {$fileSearchDocument->displayName}\n"; +``` + +#### List File Search Documents +List all file search documents within a store. + +```php +$response = $client->fileSearchDocuments()->list(parent: 'fileSearchStores/my-search-store', pageSize: 10); + +foreach ($response->fileSearchDocuments as $fileSearchDocument) { + echo "Name: {$fileSearchDocument->name}\n"; + echo "Display Name: {$fileSearchDocument->displayName}\n"; + echo "Create Time: {$fileSearchDocument->createTime}\n"; + echo "Update Time: {$fileSearchDocument->updateTime}\n"; + echo "--- \n"; +} +``` + +#### Delete File Search Document +Delete a file search document by name. + +```php +$client->fileSearchDocuments()->delete('fileSearchStores/my-search-store/fileSearchDocuments/my-document'); +``` + ### Embedding Resource Embedding is a technique used to represent information as a list of floating point numbers in an array. With Gemini, you can represent text (words, sentences, and blocks of text) in a vectorized form, making it easier to compare and contrast embeddings. For example, two texts that share a similar subject matter or sentiment should have similar embeddings, which can be identified through mathematical comparison techniques such as cosine similarity. From 24e6f947bada3d72f447c2383129379f5a04dcf8 Mon Sep 17 00:00:00 2001 From: Johann Fradj Date: Mon, 15 Dec 2025 11:22:01 +0100 Subject: [PATCH 04/27] Update src/Resources/FileSearchStores.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Resources/FileSearchStores.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Resources/FileSearchStores.php b/src/Resources/FileSearchStores.php index 372bc4b..f0c416e 100644 --- a/src/Resources/FileSearchStores.php +++ b/src/Resources/FileSearchStores.php @@ -59,7 +59,7 @@ public function delete(string $name, bool $force = false): void public function upload(string $storeName, string $filename, ?MimeType $mimeType = null, ?string $displayName = null): UploadResponse { - $mimeType ??= MimeType::from((string) mime_content_type($filename)); + $mimeType ??= MimeType::from(mime_content_type($filename) ?: throw new \RuntimeException("Failed to determine MIME type for: {$filename}")); $displayName ??= $filename; /** @var ResponseDTO, done: bool, response?: array, error?: array }> $response */ From f44877c1c282deb0d2c1e885f35c9c69c1beaade Mon Sep 17 00:00:00 2001 From: Johann Fradj Date: Mon, 15 Dec 2025 11:23:56 +0100 Subject: [PATCH 05/27] Update src/Responses/FileSearchStores/UploadResponse.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Responses/FileSearchStores/UploadResponse.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Responses/FileSearchStores/UploadResponse.php b/src/Responses/FileSearchStores/UploadResponse.php index c94d34f..6307e61 100644 --- a/src/Responses/FileSearchStores/UploadResponse.php +++ b/src/Responses/FileSearchStores/UploadResponse.php @@ -15,7 +15,7 @@ class UploadResponse implements ResponseContract use Fakeable; /** - * @param array $metadata + * @param array|null $metadata * @param array|null $response * @param array|null $error */ From eb0fb541836c535e915b17310640fb14a921da61 Mon Sep 17 00:00:00 2001 From: Johann Fradj Date: Mon, 15 Dec 2025 11:24:08 +0100 Subject: [PATCH 06/27] Update README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 38f9662..8270654 100644 --- a/README.md +++ b/README.md @@ -912,8 +912,6 @@ $response = $client->fileSearchStores()->list(pageSize: 10); foreach ($response->fileSearchStores as $fileSearchStore) { echo "Name: {$fileSearchStore->name}\n"; echo "Display Name: {$fileSearchStore->displayName}\n"; - echo "Create Time: {$fileSearchStore->createTime}\n"; - echo "Update Time: {$fileSearchStore->updateTime}\n"; echo "--- \n"; } ``` From 6429503a63210ebb63bebed050927c0b4ac46b0e Mon Sep 17 00:00:00 2001 From: Johann Fradj Date: Mon, 15 Dec 2025 11:24:33 +0100 Subject: [PATCH 07/27] Update src/Requests/FileSearchStores/UploadRequest.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Requests/FileSearchStores/UploadRequest.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Requests/FileSearchStores/UploadRequest.php b/src/Requests/FileSearchStores/UploadRequest.php index 21c21bf..ad66af7 100644 --- a/src/Requests/FileSearchStores/UploadRequest.php +++ b/src/Requests/FileSearchStores/UploadRequest.php @@ -44,6 +44,9 @@ public function toRequest(string $baseUrl, array $headers = [], array $queryPara $requestJson = json_encode($metadata); $contents = file_get_contents($this->filename); + if ($contents === false) { + throw new \RuntimeException("Failed to read file: {$this->filename}"); + } $request = $factory ->createRequest($this->method->value, str_replace('/v1', '/upload/v1', $baseUrl).$this->resolveEndpoint()) From 4645154883890ddfeaffa154a7a9d3b022b2c1e1 Mon Sep 17 00:00:00 2001 From: Johann Fradj Date: Mon, 15 Dec 2025 11:25:08 +0100 Subject: [PATCH 08/27] Update src/Requests/FileSearchStores/UploadRequest.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Requests/FileSearchStores/UploadRequest.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Requests/FileSearchStores/UploadRequest.php b/src/Requests/FileSearchStores/UploadRequest.php index ad66af7..961c907 100644 --- a/src/Requests/FileSearchStores/UploadRequest.php +++ b/src/Requests/FileSearchStores/UploadRequest.php @@ -49,7 +49,10 @@ public function toRequest(string $baseUrl, array $headers = [], array $queryPara } $request = $factory - ->createRequest($this->method->value, str_replace('/v1', '/upload/v1', $baseUrl).$this->resolveEndpoint()) + ->createRequest( + $this->method->value, + preg_replace('#/v1(beta)?#', '/upload/v1$1', $baseUrl) . $this->resolveEndpoint() + ) ->withHeader('X-Goog-Upload-Protocol', 'multipart'); foreach ($headers as $name => $value) { $request = $request->withHeader($name, $value); From da3d1ea68b2d32daebe6c24bf71ec8e75a378526 Mon Sep 17 00:00:00 2001 From: Johann Fradj Date: Mon, 15 Dec 2025 11:25:27 +0100 Subject: [PATCH 09/27] Update README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8270654..ebd1246 100644 --- a/README.md +++ b/README.md @@ -897,7 +897,7 @@ echo "File search store created: {$fileSearchStore->name}\n"; Get a specific file search store by name. ```php -$fileSearchStore = $client->fileSearchStores()->retrieve('fileSearchStores/my-search-store'); +$fileSearchStore = $client->fileSearchStores()->get('fileSearchStores/my-search-store'); echo "Name: {$fileSearchStore->name}\n"; echo "Display Name: {$fileSearchStore->displayName}\n"; From ce5456ee03269b36ccf019ad4e0e3be82da7d760 Mon Sep 17 00:00:00 2001 From: Johann Fradj Date: Mon, 15 Dec 2025 11:25:42 +0100 Subject: [PATCH 10/27] Update README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ebd1246..fb7d116 100644 --- a/README.md +++ b/README.md @@ -976,7 +976,7 @@ echo "File search document created: {$fileSearchDocument->name}\n"; Get a specific file search document by name. ```php -$fileSearchDocument = $client->fileSearchDocuments()->retrieve('fileSearchStores/my-search-store/fileSearchDocuments/my-document'); +$fileSearchDocument = $client->fileSearchStores()->getDocument('fileSearchStores/my-search-store/fileSearchDocuments/my-document'); echo "Name: {$fileSearchDocument->name}\n"; echo "Display Name: {$fileSearchDocument->displayName}\n"; From a4e837cceafed36f39009b9610f547d01744a253 Mon Sep 17 00:00:00 2001 From: Johann Fradj Date: Mon, 15 Dec 2025 11:26:46 +0100 Subject: [PATCH 11/27] Update README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- README.md | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/README.md b/README.md index fb7d116..b3e1f0a 100644 --- a/README.md +++ b/README.md @@ -923,18 +923,6 @@ Delete a file search store by name. $client->fileSearchStores()->delete('fileSearchStores/my-search-store'); ``` -#### Update File Search Store -Update a file search store. - -```php -$fileSearchStore = $client->fileSearchStores()->update( - name: 'fileSearchStores/my-search-store', - displayName: 'My Updated Search Store', -); - -echo "File search store updated: {$fileSearchStore->name}\n"; -``` - ### File Search Documents #### Create File Search Document From b049f0b94f4312b258fd0426b994449163d3d82f Mon Sep 17 00:00:00 2001 From: Johann Fradj Date: Mon, 15 Dec 2025 11:27:09 +0100 Subject: [PATCH 12/27] Update src/Contracts/Resources/FileSearchStoresContract.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Contracts/Resources/FileSearchStoresContract.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Contracts/Resources/FileSearchStoresContract.php b/src/Contracts/Resources/FileSearchStoresContract.php index c75a878..dc9ba8f 100644 --- a/src/Contracts/Resources/FileSearchStoresContract.php +++ b/src/Contracts/Resources/FileSearchStoresContract.php @@ -1,4 +1,5 @@ Date: Mon, 15 Dec 2025 11:27:48 +0100 Subject: [PATCH 13/27] Update src/Requests/FileSearchStores/DeleteRequest.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Requests/FileSearchStores/DeleteRequest.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Requests/FileSearchStores/DeleteRequest.php b/src/Requests/FileSearchStores/DeleteRequest.php index 45a696c..01eab4c 100644 --- a/src/Requests/FileSearchStores/DeleteRequest.php +++ b/src/Requests/FileSearchStores/DeleteRequest.php @@ -24,6 +24,9 @@ public function resolveEndpoint(): string return $this->name; } + /** + * @return array + */ public function defaultQuery(): array { return $this->force ? ['force' => 'true'] : []; From f8d2ae98545f3346ad1c51138b6720ba9fd6faa8 Mon Sep 17 00:00:00 2001 From: Johann Fradj Date: Mon, 15 Dec 2025 11:28:01 +0100 Subject: [PATCH 14/27] Update README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- README.md | 9 --------- 1 file changed, 9 deletions(-) diff --git a/README.md b/README.md index b3e1f0a..8e4bb0c 100644 --- a/README.md +++ b/README.md @@ -879,15 +879,6 @@ if ($meta->state == FileState::Failed) { $fileSearchStore = $client->fileSearchStores()->create( displayName: 'My Search Store', - defaultSchema: new Schema( - declarations: [ - 'name' => new Schema(type: DataType::STRING), - 'size' => new Schema(type: DataType::INTEGER), - ], - ), - defaultDocumentConfig: [ - 'files/'.basename($meta->uri), - ], ); echo "File search store created: {$fileSearchStore->name}\n"; From bdb09516880a4c4c94d2b5cf22a4e55cc6f34d20 Mon Sep 17 00:00:00 2001 From: Johann Fradj Date: Mon, 15 Dec 2025 11:28:14 +0100 Subject: [PATCH 15/27] Update README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8e4bb0c..527f7bd 100644 --- a/README.md +++ b/README.md @@ -980,7 +980,7 @@ foreach ($response->fileSearchDocuments as $fileSearchDocument) { Delete a file search document by name. ```php -$client->fileSearchDocuments()->delete('fileSearchStores/my-search-store/fileSearchDocuments/my-document'); +$client->fileSearchStores()->deleteDocument('fileSearchStores/my-search-store/fileSearchDocuments/my-document'); ``` ### Embedding Resource From 29e3ff425c98b9360b0485d0313615fe17cf20ae Mon Sep 17 00:00:00 2001 From: Johann Fradj Date: Mon, 15 Dec 2025 11:50:48 +0100 Subject: [PATCH 16/27] Fix README: replace non-existent fileSearchDocuments with fileSearchStores methods --- README.md | 39 ++++++++++----------------------------- 1 file changed, 10 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index a0b3111..1c3a5e3 100644 --- a/README.md +++ b/README.md @@ -995,46 +995,27 @@ echo "File search store updated: {$fileSearchStore->name}\n"; ### File Search Documents -#### Create File Search Document -Create a file search document within a store. +#### Upload File Search Document +Upload a local file directly to a file search store. ```php -use Gemini\Enums\FileState; use Gemini\Enums\MimeType; -$files = $client->files(); -echo "Uploading\n"; -$meta = $files->upload( +$response = $client->fileSearchStores()->upload( + storeName: 'fileSearchStores/my-search-store', filename: 'document2.pdf', mimeType: MimeType::APPLICATION_PDF, - displayName: 'Another document for search' -); -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)); -} - -$fileSearchDocument = $client->fileSearchDocuments()->create( - parent: 'fileSearchStores/my-search-store', - file: 'files/'.basename($meta->uri), - displayName: 'Another Search Document', + displayName: 'Another Search Document' ); -echo "File search document created: {$fileSearchDocument->name}\n"; +echo "File search document upload operation: {$response->name}\n"; ``` #### Get File Search Document Get a specific file search document by name. ```php -$fileSearchDocument = $client->fileSearchDocuments()->retrieve('fileSearchStores/my-search-store/fileSearchDocuments/my-document'); +$fileSearchDocument = $client->fileSearchStores()->getDocument('fileSearchStores/my-search-store/fileSearchDocuments/my-document'); echo "Name: {$fileSearchDocument->name}\n"; echo "Display Name: {$fileSearchDocument->displayName}\n"; @@ -1044,9 +1025,9 @@ echo "Display Name: {$fileSearchDocument->displayName}\n"; List all file search documents within a store. ```php -$response = $client->fileSearchDocuments()->list(parent: 'fileSearchStores/my-search-store', pageSize: 10); +$response = $client->fileSearchStores()->listDocuments(storeName: 'fileSearchStores/my-search-store', pageSize: 10); -foreach ($response->fileSearchDocuments as $fileSearchDocument) { +foreach ($response->documents as $fileSearchDocument) { echo "Name: {$fileSearchDocument->name}\n"; echo "Display Name: {$fileSearchDocument->displayName}\n"; echo "Create Time: {$fileSearchDocument->createTime}\n"; @@ -1059,7 +1040,7 @@ foreach ($response->fileSearchDocuments as $fileSearchDocument) { Delete a file search document by name. ```php -$client->fileSearchDocuments()->delete('fileSearchStores/my-search-store/fileSearchDocuments/my-document'); +$client->fileSearchStores()->deleteDocument('fileSearchStores/my-search-store/fileSearchDocuments/my-document'); ``` ### Embedding Resource From 195da56c8f1c02ceb843b74a6a410bb4bd13ddab Mon Sep 17 00:00:00 2001 From: Johann Fradj Date: Mon, 15 Dec 2025 12:05:30 +0100 Subject: [PATCH 17/27] fix: README --- README.md | 35 ++++++++--------------------------- 1 file changed, 8 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 527f7bd..25b6209 100644 --- a/README.md +++ b/README.md @@ -916,39 +916,20 @@ $client->fileSearchStores()->delete('fileSearchStores/my-search-store'); ### File Search Documents -#### Create File Search Document -Create a file search document within a store. +#### Upload File Search Document +Upload a local file directly to a file search store. ```php -use Gemini\Enums\FileState; use Gemini\Enums\MimeType; -$files = $client->files(); -echo "Uploading\n"; -$meta = $files->upload( +$response = $client->fileSearchStores()->upload( + storeName: 'fileSearchStores/my-search-store', filename: 'document2.pdf', mimeType: MimeType::APPLICATION_PDF, - displayName: 'Another document for search' -); -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)); -} - -$fileSearchDocument = $client->fileSearchDocuments()->create( - parent: 'fileSearchStores/my-search-store', - file: 'files/'.basename($meta->uri), - displayName: 'Another Search Document', + displayName: 'Another Search Document' ); -echo "File search document created: {$fileSearchDocument->name}\n"; +echo "File search document upload operation: {$response->name}\n"; ``` #### Get File Search Document @@ -965,9 +946,9 @@ echo "Display Name: {$fileSearchDocument->displayName}\n"; List all file search documents within a store. ```php -$response = $client->fileSearchDocuments()->list(parent: 'fileSearchStores/my-search-store', pageSize: 10); +$response = $client->fileSearchStores()->listDocuments(storeName: 'fileSearchStores/my-search-store', pageSize: 10); -foreach ($response->fileSearchDocuments as $fileSearchDocument) { +foreach ($response->documents as $fileSearchDocument) { echo "Name: {$fileSearchDocument->name}\n"; echo "Display Name: {$fileSearchDocument->displayName}\n"; echo "Create Time: {$fileSearchDocument->createTime}\n"; From 19aecf7cc95c31e601e4a49d512e8d43a7b0c5bb Mon Sep 17 00:00:00 2001 From: Johann Fradj Date: Mon, 15 Dec 2025 12:08:45 +0100 Subject: [PATCH 18/27] Update README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3effb8e..a455471 100644 --- a/README.md +++ b/README.md @@ -697,7 +697,7 @@ Every prompt you send to the model includes parameter values that control how th Also, you can use safety settings to adjust the likelihood of getting responses that may be considered harmful. By default, safety settings block content with medium and/or high probability of being unsafe content across all dimensions. Learn more about [safety settings](https://ai.google.dev/docs/concepts#safety_setting). -When using tools like `FileSearch`, you may also provide additional configuration via `ToolConfig`, such as `RetrievalConfig` for geographical context. +When using tools like `GoogleMaps`, you may also provide additional configuration via `ToolConfig`, such as `RetrievalConfig` for geographical context. ```php use Gemini\Data\GenerationConfig; From 3f9748f353e458848d54a400f571f3b87d038cef Mon Sep 17 00:00:00 2001 From: Johann Fradj Date: Wed, 24 Dec 2025 15:36:50 +0100 Subject: [PATCH 19/27] feat: add Maps + FileSearch grounding metadata --- src/Data/Candidate.php | 2 +- src/Data/GroundingChunk.php | 20 +++- src/Data/GroundingMetadata.php | 2 +- src/Data/Map.php | 55 ++++++++++ src/Data/PlaceAnswerSources.php | 45 ++++++++ src/Data/RetrievedContext.php | 51 +++++++++ src/Data/ReviewSnippet.php | 47 ++++++++ src/Resources/GenerativeModel.php | 2 +- .../FileSearchStores/UploadResponse.php | 2 +- .../GenerateContentResponse.php | 2 +- tests/Data/GroundingChunk.php | 103 ++++++++++++++++++ 11 files changed, 325 insertions(+), 6 deletions(-) create mode 100644 src/Data/Map.php create mode 100644 src/Data/PlaceAnswerSources.php create mode 100644 src/Data/RetrievedContext.php create mode 100644 src/Data/ReviewSnippet.php create mode 100644 tests/Data/GroundingChunk.php diff --git a/src/Data/Candidate.php b/src/Data/Candidate.php index 69a99f2..62ea6fb 100644 --- a/src/Data/Candidate.php +++ b/src/Data/Candidate.php @@ -43,7 +43,7 @@ public function __construct( ) {} /** - * @param array{ content: ?array{ parts: array{ array{ text: ?string, inlineData: ?array{ mimeType: string, data: string }, fileData: ?array{ fileUri: string, mimeType: string }, functionCall: ?array{ name: string, args: array|null }, functionResponse: ?array{ name: string, response: array } } }, 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, groundingAttributions: ?array|null }, functionResponse: ?array{ name: string, response: array } } }, role: string } }>, groundingMetadata?: array{ groundingChunks: ?array, groundingSupports: ?array|null, confidenceScores: array|null, segment: ?array{ partIndex: ?int, startIndex: ?int, endIndex: ?int, text: ?string } }>, webSearchQueries: ?array, searchEntryPoint?: array{ renderedContent?: string|null, sdkBlob?: string|null }, retrievalMetadata: ?array{ googleSearchDynamicRetrievalScore?: float|null } }, logprobsResult?: array{ topCandidates: array }>, chosenCandidates: array }, urlRetrievalMetadata?: array{ urlRetrievalContexts: array } } $attributes + * @param array{ content: ?array{ parts: array{ array{ text: ?string, inlineData: ?array{ mimeType: string, data: string }, fileData: ?array{ fileUri: string, mimeType: string }, functionCall: ?array{ name: string, args: array|null }, functionResponse: ?array{ name: string, response: array } } }, 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, groundingAttributions: ?array|null }, functionResponse: ?array{ name: string, response: array } } }, role: string } }>, groundingMetadata?: array{ groundingChunks: ?array } } }>, groundingSupports: ?array|null, confidenceScores: array|null, segment: ?array{ partIndex: ?int, startIndex: ?int, endIndex: ?int, text: ?string } }>, webSearchQueries: ?array, searchEntryPoint?: array{ renderedContent?: string|null, sdkBlob?: string|null }, retrievalMetadata: ?array{ googleSearchDynamicRetrievalScore?: float|null } }, logprobsResult?: array{ topCandidates: array }>, chosenCandidates: array }, urlRetrievalMetadata?: array{ urlRetrievalContexts: array } } $attributes */ public static function from(array $attributes): self { diff --git a/src/Data/GroundingChunk.php b/src/Data/GroundingChunk.php index 636cfc1..c9232db 100644 --- a/src/Data/GroundingChunk.php +++ b/src/Data/GroundingChunk.php @@ -15,18 +15,28 @@ final class GroundingChunk implements Arrayable { /** * @param Web|null $web Grounding chunk from the web. + * @param RetrievedContext|null $retrievedContext Grounding chunk from the file search tool. + * @param Map|null $map Grounding chunk from Google Maps. */ public function __construct( public readonly ?Web $web = null, + public readonly ?RetrievedContext $retrievedContext = null, + public readonly ?Map $map = null, ) {} /** - * @param array{ web: null|array{ title: ?string, uri: ?string } } $attributes + * @param array{ + * web: null|array{ title: ?string, uri: ?string }, + * retrievedContext: null|array{ uri: ?string, title: ?string, text: ?string, fileSearchStore: ?string }, + * maps: null|array{ uri: ?string, title: ?string, text: ?string, placeId: ?string, placeAnswerSources: ?array{ reviewSnippets: array } } + * } $attributes */ public static function from(array $attributes): self { return new self( web: isset($attributes['web']) ? Web::from($attributes['web']) : null, + retrievedContext: isset($attributes['retrievedContext']) ? RetrievedContext::from($attributes['retrievedContext']) : null, + map: isset($attributes['maps']) ? Map::from($attributes['maps']) : null, ); } @@ -38,6 +48,14 @@ public function toArray(): array $data['web'] = $this->web->toArray(); } + if ($this->retrievedContext !== null) { + $data['retrievedContext'] = $this->retrievedContext->toArray(); + } + + if ($this->map !== null) { + $data['maps'] = $this->map->toArray(); + } + return $data; } } diff --git a/src/Data/GroundingMetadata.php b/src/Data/GroundingMetadata.php index 06e228b..357b522 100644 --- a/src/Data/GroundingMetadata.php +++ b/src/Data/GroundingMetadata.php @@ -29,7 +29,7 @@ public function __construct( ) {} /** - * @param array{ groundingChunks: ?array, groundingSupports: ?array|null, confidenceScores: array|null, segment: ?array{ partIndex: ?int, startIndex: ?int, endIndex: ?int, text: ?string } }>, webSearchQueries: ?array, searchEntryPoint?: array{ renderedContent?: string|null, sdkBlob?: string|null }, retrievalMetadata: ?array{ googleSearchDynamicRetrievalScore?: float|null } } $attributes + * @param array{ groundingChunks: ?array } } }>, groundingSupports: ?array|null, confidenceScores: array|null, segment: ?array{ partIndex: ?int, startIndex: ?int, endIndex: ?int, text: ?string } }>, webSearchQueries: ?array, searchEntryPoint?: array{ renderedContent?: string|null, sdkBlob?: string|null }, retrievalMetadata: ?array{ googleSearchDynamicRetrievalScore?: float|null } } $attributes */ public static function from(array $attributes): self { diff --git a/src/Data/Map.php b/src/Data/Map.php new file mode 100644 index 0000000..47c870b --- /dev/null +++ b/src/Data/Map.php @@ -0,0 +1,55 @@ + } } $attributes + */ + public static function from(array $attributes): self + { + return new self( + uri: $attributes['uri'] ?? null, + title: $attributes['title'] ?? null, + text: $attributes['text'] ?? null, + placeId: $attributes['placeId'] ?? null, + placeAnswerSources: isset($attributes['placeAnswerSources']) ? PlaceAnswerSources::from($attributes['placeAnswerSources']) : null, + ); + } + + public function toArray(): array + { + return [ + 'uri' => $this->uri, + 'title' => $this->title, + 'text' => $this->text, + 'placeId' => $this->placeId, + 'placeAnswerSources' => $this->placeAnswerSources?->toArray(), + ]; + } +} diff --git a/src/Data/PlaceAnswerSources.php b/src/Data/PlaceAnswerSources.php new file mode 100644 index 0000000..309aef5 --- /dev/null +++ b/src/Data/PlaceAnswerSources.php @@ -0,0 +1,45 @@ + $reviewSnippets These are snippets of reviews used to generate answers about the features of a given place in Google Maps. + */ + public function __construct( + public readonly array $reviewSnippets, + ) {} + + /** + * @param array{ reviewSnippets: array } $attributes + */ + public static function from(array $attributes): self + { + return new self( + reviewSnippets: array_map( + static fn (array $snippet): ReviewSnippet => ReviewSnippet::from($snippet), + $attributes['reviewSnippets'], + ), + ); + } + + public function toArray(): array + { + return [ + 'reviewSnippets' => array_map( + static fn (ReviewSnippet $snippet): array => $snippet->toArray(), + $this->reviewSnippets, + ), + ]; + } +} diff --git a/src/Data/RetrievedContext.php b/src/Data/RetrievedContext.php new file mode 100644 index 0000000..27530e3 --- /dev/null +++ b/src/Data/RetrievedContext.php @@ -0,0 +1,51 @@ + $this->uri, + 'title' => $this->title, + 'text' => $this->text, + 'fileSearchStore' => $this->fileSearchStore, + ]; + } +} diff --git a/src/Data/ReviewSnippet.php b/src/Data/ReviewSnippet.php new file mode 100644 index 0000000..7e3ddb4 --- /dev/null +++ b/src/Data/ReviewSnippet.php @@ -0,0 +1,47 @@ + $this->title, + 'googleMapsUri' => $this->googleMapsUri, + 'reviewId' => $this->reviewId, + ]; + } +} diff --git a/src/Resources/GenerativeModel.php b/src/Resources/GenerativeModel.php index 7769731..cf84ab0 100644 --- a/src/Resources/GenerativeModel.php +++ b/src/Resources/GenerativeModel.php @@ -112,7 +112,7 @@ public function countTokens(string|Blob|array|Content|UploadedFile ...$parts): C */ public function generateContent(string|Blob|array|Content|UploadedFile ...$parts): GenerateContentResponse { - /** @var ResponseDTO|null }, functionResponse: ?array{ name: string, response: array } } }, 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, groundingAttributions: ?array|null }, functionResponse: ?array{ name: string, response: array } } }, role: string } }>, groundingMetadata?: array{ groundingChunks: ?array, groundingSupports: ?array|null, confidenceScores: array|null, segment: ?array{ partIndex: ?int, startIndex: ?int, endIndex: ?int, text: ?string } }>, webSearchQueries: ?array, searchEntryPoint?: array{ renderedContent?: string|null, sdkBlob?: string|null }, retrievalMetadata: ?array{ googleSearchDynamicRetrievalScore?: float|null } }, logprobsResult?: array{ topCandidates: array }>, chosenCandidates: array }, urlRetrievalMetadata?: array{ urlRetrievalContexts: array } }>, promptFeedback: ?array{ safetyRatings: array{ array{ category: string, probability: string, blocked: ?bool } }, blockReason: ?string }, usageMetadata: array{ promptTokenCount: int, totalTokenCount: int, candidatesTokenCount: ?int, cachedContentTokenCount: ?int, toolUsePromptTokenCount: ?int, thoughtsTokenCount: ?int, promptTokensDetails: list|null, cacheTokensDetails: list|null, candidatesTokensDetails: list|null, toolUsePromptTokensDetails: list|null }, modelVersion: ?string }> $response */ + /** @var ResponseDTO|null }, functionResponse: ?array{ name: string, response: array } } }, 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, groundingAttributions: ?array|null }, functionResponse: ?array{ name: string, response: array } } }, role: string } }>, groundingMetadata?: array{ groundingChunks: ?array } } }>, groundingSupports: ?array|null, confidenceScores: array|null, segment: ?array{ partIndex: ?int, startIndex: ?int, endIndex: ?int, text: ?string } }>, webSearchQueries: ?array, searchEntryPoint?: array{ renderedContent?: string|null, sdkBlob?: string|null }, retrievalMetadata: ?array{ googleSearchDynamicRetrievalScore?: float|null } }, logprobsResult?: array{ topCandidates: array }>, chosenCandidates: array }, urlRetrievalMetadata?: array{ urlRetrievalContexts: array } }>, promptFeedback: ?array{ safetyRatings: array{ array{ category: string, probability: string, blocked: ?bool } }, blockReason: ?string }, usageMetadata: array{ promptTokenCount: int, totalTokenCount: int, candidatesTokenCount: ?int, cachedContentTokenCount: ?int, toolUsePromptTokenCount: ?int, thoughtsTokenCount: ?int, promptTokensDetails: list|null, cacheTokensDetails: list|null, candidatesTokensDetails: list|null, toolUsePromptTokensDetails: list|null }, modelVersion: ?string }> $response */ $response = $this->transporter->request( request: new GenerateContentRequest( model: $this->model, diff --git a/src/Responses/FileSearchStores/UploadResponse.php b/src/Responses/FileSearchStores/UploadResponse.php index c94d34f..4c3c05d 100644 --- a/src/Responses/FileSearchStores/UploadResponse.php +++ b/src/Responses/FileSearchStores/UploadResponse.php @@ -34,7 +34,7 @@ public static function from(array $attributes): self { return new self( name: $attributes['name'], - done: $attributes['done'] ?? false, + done: $attributes['done'], metadata: $attributes['metadata'] ?? null, response: $attributes['response'] ?? null, error: $attributes['error'] ?? null, diff --git a/src/Responses/GenerativeModel/GenerateContentResponse.php b/src/Responses/GenerativeModel/GenerateContentResponse.php index e1e50ac..367dd60 100644 --- a/src/Responses/GenerativeModel/GenerateContentResponse.php +++ b/src/Responses/GenerativeModel/GenerateContentResponse.php @@ -98,7 +98,7 @@ public function json(bool $associative = false, int $flags = 0): mixed } /** - * @param array{ candidates: ?array|null }, functionResponse: ?array{ name: string, response: array } } }, 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, groundingAttributions: ?array|null }, functionResponse: ?array{ name: string, response: array } } }, role: string } }>, groundingMetadata?: array{ groundingChunks: ?array, groundingSupports: ?array|null, confidenceScores: array|null, segment: ?array{ partIndex: ?int, startIndex: ?int, endIndex: ?int, text: ?string } }>, webSearchQueries: ?array, searchEntryPoint?: array{ renderedContent?: string|null, sdkBlob?: string|null }, retrievalMetadata: ?array{ googleSearchDynamicRetrievalScore?: float|null } }, logprobsResult?: array{ topCandidates: array }>, chosenCandidates: array }, urlRetrievalMetadata?: array{ urlRetrievalContexts: array } }>, promptFeedback: ?array{ safetyRatings: array{ array{ category: string, probability: string, blocked: ?bool } }, blockReason: ?string }, usageMetadata: array{ promptTokenCount: int, totalTokenCount: int, candidatesTokenCount: ?int, cachedContentTokenCount: ?int, toolUsePromptTokenCount: ?int, thoughtsTokenCount: ?int, promptTokensDetails: list|null, cacheTokensDetails: list|null, candidatesTokensDetails: list|null, toolUsePromptTokensDetails: list|null }, modelVersion: ?string } $attributes + * @param array{ candidates: ?array|null }, functionResponse: ?array{ name: string, response: array } } }, 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, groundingAttributions: ?array|null }, functionResponse: ?array{ name: string, response: array } } }, role: string } }>, groundingMetadata?: array{ groundingChunks: ?array } } }>, groundingSupports: ?array|null, confidenceScores: array|null, segment: ?array{ partIndex: ?int, startIndex: ?int, endIndex: ?int, text: ?string } }>, webSearchQueries: ?array, searchEntryPoint?: array{ renderedContent?: string|null, sdkBlob?: string|null }, retrievalMetadata: ?array{ googleSearchDynamicRetrievalScore?: float|null } }, logprobsResult?: array{ topCandidates: array }>, chosenCandidates: array }, urlRetrievalMetadata?: array{ urlRetrievalContexts: array } }>, promptFeedback: ?array{ safetyRatings: array{ array{ category: string, probability: string, blocked: ?bool } }, blockReason: ?string }, usageMetadata: array{ promptTokenCount: int, totalTokenCount: int, candidatesTokenCount: ?int, cachedContentTokenCount: ?int, toolUsePromptTokenCount: ?int, thoughtsTokenCount: ?int, promptTokensDetails: list|null, cacheTokensDetails: list|null, candidatesTokensDetails: list|null, toolUsePromptTokensDetails: list|null }, modelVersion: ?string } $attributes */ public static function from(array $attributes): self { diff --git a/tests/Data/GroundingChunk.php b/tests/Data/GroundingChunk.php new file mode 100644 index 0000000..d251a35 --- /dev/null +++ b/tests/Data/GroundingChunk.php @@ -0,0 +1,103 @@ +toArray()) + ->toEqual([ + 'maps' => [ + 'uri' => 'http://maps.google.com/place', + 'title' => 'Place Title', + 'text' => 'Place Description', + 'placeId' => 'place-123', + 'placeAnswerSources' => [ + 'reviewSnippets' => [ + [ + 'title' => 'Review Title', + 'googleMapsUri' => 'http://maps.google.com/review', + 'reviewId' => 'review-123', + ], + ], + ], + ], + ]); +}); + +test('from array', function () { + $data = [ + 'maps' => [ + 'uri' => 'http://maps.google.com/place', + 'title' => 'Place Title', + 'text' => 'Place Description', + 'placeId' => 'place-123', + 'placeAnswerSources' => [ + 'reviewSnippets' => [ + [ + 'title' => 'Review Title', + 'googleMapsUri' => 'http://maps.google.com/review', + 'reviewId' => 'review-123', + ], + ], + ], + ], + ]; + + $groundingChunk = GroundingChunk::from($data); + + expect($groundingChunk)->toBeInstanceOf(GroundingChunk::class) + ->and($groundingChunk->map)->toBeInstanceOf(Map::class) + ->and($groundingChunk->map->uri)->toBe('http://maps.google.com/place') + ->and($groundingChunk->map->placeAnswerSources)->toBeInstanceOf(PlaceAnswerSources::class) + ->and($groundingChunk->map->placeAnswerSources->reviewSnippets[0])->toBeInstanceOf(ReviewSnippet::class) + ->and($groundingChunk->map->placeAnswerSources->reviewSnippets[0]->title)->toBe('Review Title'); +}); + +test('from array with all fields', function () { + $data = [ + 'web' => [ + 'uri' => 'http://google.com', + 'title' => 'Google', + ], + 'retrievedContext' => [ + 'uri' => 'http://example.com/doc', + 'title' => 'Doc Title', + 'text' => 'Doc Text', + 'fileSearchStore' => 'stores/123', + ], + 'maps' => [ + 'uri' => 'http://maps.google.com/place', + 'title' => 'Place Title', + ], + ]; + + $groundingChunk = GroundingChunk::from($data); + + expect($groundingChunk->web->uri)->toBe('http://google.com') + ->and($groundingChunk->retrievedContext->text)->toBe('Doc Text') + ->and($groundingChunk->map->title)->toBe('Place Title'); +}); From 395d15e73db76a326cc02a2fdf5d233877157943 Mon Sep 17 00:00:00 2001 From: Johann Fradj Date: Wed, 24 Dec 2025 16:01:43 +0100 Subject: [PATCH 20/27] chore: lint --- src/Contracts/Resources/FileSearchStoresContract.php | 1 + src/Requests/FileSearchStores/UploadRequest.php | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Contracts/Resources/FileSearchStoresContract.php b/src/Contracts/Resources/FileSearchStoresContract.php index dc9ba8f..ec4385b 100644 --- a/src/Contracts/Resources/FileSearchStoresContract.php +++ b/src/Contracts/Resources/FileSearchStoresContract.php @@ -1,4 +1,5 @@ createRequest( $this->method->value, - preg_replace('#/v1(beta)?#', '/upload/v1$1', $baseUrl) . $this->resolveEndpoint() + preg_replace('#/v1(beta)?#', '/upload/v1$1', $baseUrl).$this->resolveEndpoint() ) ->withHeader('X-Goog-Upload-Protocol', 'multipart'); foreach ($headers as $name => $value) { From 752a20994d1f3d2a8406f10ab071d404a6125185 Mon Sep 17 00:00:00 2001 From: Johann Fradj Date: Fri, 26 Dec 2025 09:45:26 +0100 Subject: [PATCH 21/27] fix: Gemini API response doesn't follow its own documentation (https://ai.google.dev/api/file-search/file-search-stores#response-body) key "done" isn't always present. --- src/Resources/FileSearchStores.php | 2 +- src/Responses/FileSearchStores/UploadResponse.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Resources/FileSearchStores.php b/src/Resources/FileSearchStores.php index f0c416e..952eda0 100644 --- a/src/Resources/FileSearchStores.php +++ b/src/Resources/FileSearchStores.php @@ -62,7 +62,7 @@ public function upload(string $storeName, string $filename, ?MimeType $mimeType $mimeType ??= MimeType::from(mime_content_type($filename) ?: throw new \RuntimeException("Failed to determine MIME type for: {$filename}")); $displayName ??= $filename; - /** @var ResponseDTO, done: bool, response?: array, error?: array }> $response */ + /** @var ResponseDTO, done?: bool, response?: array, error?: array }> $response */ $response = $this->transporter->request(new UploadRequest($storeName, $filename, $displayName, $mimeType)); return UploadResponse::from($response->data()); diff --git a/src/Responses/FileSearchStores/UploadResponse.php b/src/Responses/FileSearchStores/UploadResponse.php index a96db0f..a9806be 100644 --- a/src/Responses/FileSearchStores/UploadResponse.php +++ b/src/Responses/FileSearchStores/UploadResponse.php @@ -28,13 +28,13 @@ public function __construct( ) {} /** - * @param array{ name: string, done: bool, metadata?: array, response?: array, error?: array } $attributes + * @param array{ name: string, done?: bool, metadata?: array, response?: array, error?: array } $attributes */ public static function from(array $attributes): self { return new self( name: $attributes['name'], - done: $attributes['done'], + done: $attributes['done'] ?? (isset($attributes['response']) || isset($attributes['error'])), metadata: $attributes['metadata'] ?? null, response: $attributes['response'] ?? null, error: $attributes['error'] ?? null, From 743b8815444beeb9235a2274c2532e498765f50d Mon Sep 17 00:00:00 2001 From: Johann Fradj Date: Fri, 26 Dec 2025 09:45:58 +0100 Subject: [PATCH 22/27] fix: Do not set a default display name --- src/Resources/FileSearchStores.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Resources/FileSearchStores.php b/src/Resources/FileSearchStores.php index 952eda0..adc2683 100644 --- a/src/Resources/FileSearchStores.php +++ b/src/Resources/FileSearchStores.php @@ -60,7 +60,6 @@ public function delete(string $name, bool $force = false): void public function upload(string $storeName, string $filename, ?MimeType $mimeType = null, ?string $displayName = null): UploadResponse { $mimeType ??= MimeType::from(mime_content_type($filename) ?: throw new \RuntimeException("Failed to determine MIME type for: {$filename}")); - $displayName ??= $filename; /** @var ResponseDTO, done?: bool, response?: array, error?: array }> $response */ $response = $this->transporter->request(new UploadRequest($storeName, $filename, $displayName, $mimeType)); From cb8b9f1352d8ebc76c20794c3c3c1c95490a3fc9 Mon Sep 17 00:00:00 2001 From: Johann Fradj Date: Fri, 26 Dec 2025 09:47:14 +0100 Subject: [PATCH 23/27] fix: Do not throw exception on unmapped mimetype + try to infer mimetype at request time --- src/Requests/FileSearchStores/UploadRequest.php | 2 +- src/Resources/FileSearchStores.php | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Requests/FileSearchStores/UploadRequest.php b/src/Requests/FileSearchStores/UploadRequest.php index b26186d..11e62ab 100644 --- a/src/Requests/FileSearchStores/UploadRequest.php +++ b/src/Requests/FileSearchStores/UploadRequest.php @@ -58,7 +58,7 @@ public function toRequest(string $baseUrl, array $headers = [], array $queryPara $request = $request->withHeader($name, $value); } - $contentType = $this->mimeType instanceof MimeType ? $this->mimeType->value : 'application/octet-stream'; + $contentType = $this->mimeType instanceof MimeType ? $this->mimeType->value : (mime_content_type($this->filename) ?: 'application/octet-stream'); $request = $request->withHeader('Content-Type', "multipart/related; boundary={$boundary}") ->withBody($factory->createStream(<<, done?: bool, response?: array, error?: array }> $response */ $response = $this->transporter->request(new UploadRequest($storeName, $filename, $displayName, $mimeType)); From 81c2fc4eac0762cb27e04a42a63bacd8ec02e297 Mon Sep 17 00:00:00 2001 From: Johann Fradj Date: Fri, 26 Dec 2025 09:48:21 +0100 Subject: [PATCH 24/27] fix: request was malformed if no metadata was provided --- src/Requests/FileSearchStores/UploadRequest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Requests/FileSearchStores/UploadRequest.php b/src/Requests/FileSearchStores/UploadRequest.php index 11e62ab..c10b373 100644 --- a/src/Requests/FileSearchStores/UploadRequest.php +++ b/src/Requests/FileSearchStores/UploadRequest.php @@ -42,7 +42,7 @@ public function toRequest(string $baseUrl, array $headers = [], array $queryPara $metadata['mimeType'] = $this->mimeType->value; } - $requestJson = json_encode($metadata); + $requestJson = empty($metadata) ? '' : json_encode($metadata); $contents = file_get_contents($this->filename); if ($contents === false) { throw new \RuntimeException("Failed to read file: {$this->filename}"); From 7e2accc921d3513571957d09903e2dd03bb5694c Mon Sep 17 00:00:00 2001 From: Johann Fradj Date: Fri, 26 Dec 2025 10:21:20 +0100 Subject: [PATCH 25/27] feat: Add custom metadata support to fileSearchStores upload --- .../Resources/FileSearchStoresContract.php | 2 +- .../FileSearchStores/UploadRequest.php | 18 +++++++++++++ src/Resources/FileSearchStores.php | 4 +-- .../FileSearchStoresTestResource.php | 2 +- tests/Resources/FileSearchStores.php | 25 +++++++++++++++++++ .../FileSearchStoresTestResource.php | 25 +++++++++++++++++++ 6 files changed, 72 insertions(+), 4 deletions(-) create mode 100644 tests/Testing/Resources/FileSearchStoresTestResource.php diff --git a/src/Contracts/Resources/FileSearchStoresContract.php b/src/Contracts/Resources/FileSearchStoresContract.php index ec4385b..37d4839 100644 --- a/src/Contracts/Resources/FileSearchStoresContract.php +++ b/src/Contracts/Resources/FileSearchStoresContract.php @@ -46,7 +46,7 @@ public function delete(string $name, bool $force = false): void; * * @see https://ai.google.dev/api/file-search/file-search-stores#method:-media.uploadtofilesearchstore */ - public function upload(string $storeName, string $filename, ?MimeType $mimeType = null, ?string $displayName = null): UploadResponse; + public function upload(string $storeName, string $filename, ?MimeType $mimeType = null, ?string $displayName = null, array $customMetadata = []): UploadResponse; /** * List documents in a file search store. diff --git a/src/Requests/FileSearchStores/UploadRequest.php b/src/Requests/FileSearchStores/UploadRequest.php index c10b373..7018239 100644 --- a/src/Requests/FileSearchStores/UploadRequest.php +++ b/src/Requests/FileSearchStores/UploadRequest.php @@ -22,6 +22,7 @@ public function __construct( protected readonly string $filename, protected readonly ?string $displayName = null, protected ?MimeType $mimeType = null, + protected array $customMetadata = [], ) {} public function resolveEndpoint(): string @@ -42,6 +43,23 @@ public function toRequest(string $baseUrl, array $headers = [], array $queryPara $metadata['mimeType'] = $this->mimeType->value; } + if (! empty($this->customMetadata)) { + $metadata['customMetadata'] = []; + foreach ($this->customMetadata as $key => $value) { + $entry = ['key' => (string) $key]; + + if (is_int($value) || is_float($value)) { + $entry['numericValue'] = $value; + } elseif (is_array($value)) { + $entry['stringListValue'] = ['values' => array_map('strval', $value)]; + } else { + $entry['stringValue'] = (string) $value; + } + + $metadata['customMetadata'][] = $entry; + } + } + $requestJson = empty($metadata) ? '' : json_encode($metadata); $contents = file_get_contents($this->filename); if ($contents === false) { diff --git a/src/Resources/FileSearchStores.php b/src/Resources/FileSearchStores.php index 279ae3a..0268325 100644 --- a/src/Resources/FileSearchStores.php +++ b/src/Resources/FileSearchStores.php @@ -57,10 +57,10 @@ public function delete(string $name, bool $force = false): void $this->transporter->request(new DeleteRequest($name, $force)); } - public function upload(string $storeName, string $filename, ?MimeType $mimeType = null, ?string $displayName = null): UploadResponse + public function upload(string $storeName, string $filename, ?MimeType $mimeType = null, ?string $displayName = null, array $customMetadata = []): UploadResponse { /** @var ResponseDTO, done?: bool, response?: array, error?: array }> $response */ - $response = $this->transporter->request(new UploadRequest($storeName, $filename, $displayName, $mimeType)); + $response = $this->transporter->request(new UploadRequest($storeName, $filename, $displayName, $mimeType, $customMetadata)); return UploadResponse::from($response->data()); } diff --git a/src/Testing/Resources/FileSearchStoresTestResource.php b/src/Testing/Resources/FileSearchStoresTestResource.php index 0565df4..740871e 100644 --- a/src/Testing/Resources/FileSearchStoresTestResource.php +++ b/src/Testing/Resources/FileSearchStoresTestResource.php @@ -43,7 +43,7 @@ public function delete(string $name, bool $force = false): void $this->record(method: __FUNCTION__, args: func_get_args()); } - public function upload(string $storeName, string $filename, ?MimeType $mimeType = null, ?string $displayName = null): UploadResponse + public function upload(string $storeName, string $filename, ?MimeType $mimeType = null, ?string $displayName = null, array $customMetadata = []): UploadResponse { return $this->record(method: __FUNCTION__, args: func_get_args()); } diff --git a/tests/Resources/FileSearchStores.php b/tests/Resources/FileSearchStores.php index 392b418..df5bcd5 100644 --- a/tests/Resources/FileSearchStores.php +++ b/tests/Resources/FileSearchStores.php @@ -104,6 +104,31 @@ ->toBeInstanceOf(UploadResponse::class) ->name->toBe('operations/123-456'); }); + + test('upload with custom metadata', function () { + $client = mockClient( + method: Method::POST, + endpoint: 'fileSearchStores/123:uploadToFileSearchStore', + response: UploadResponse::fake(), + rootPath: '/upload/v1beta/' + ); + + $result = $client->fileSearchStores()->upload( + 'fileSearchStores/123', + $this->tmpFilepath, + MimeType::TEXT_PLAIN, + 'Display', + [ + 'key_string' => 'value', + 'key_int' => 123, + 'key_list' => ['a', 'b'], + ] + ); + + expect($result) + ->toBeInstanceOf(UploadResponse::class) + ->name->toBe('operations/123-456'); + }); }); test('list documents', function () { diff --git a/tests/Testing/Resources/FileSearchStoresTestResource.php b/tests/Testing/Resources/FileSearchStoresTestResource.php new file mode 100644 index 0000000..25cf1a3 --- /dev/null +++ b/tests/Testing/Resources/FileSearchStoresTestResource.php @@ -0,0 +1,25 @@ +fileSearchStores()->upload('store-name', 'filename.pdf', MimeType::APPLICATION_PDF, 'display name', ['key' => 'value']); + + $fake->assertSent(resource: FileSearchStores::class, callback: function ($method, $parameters) { + return $method === 'upload' && + $parameters[0] === 'store-name' && + $parameters[1] === 'filename.pdf' && + $parameters[2] === MimeType::APPLICATION_PDF && + $parameters[3] === 'display name' && + $parameters[4] === ['key' => 'value']; + }); +}); From a2f2452dcb66d4e33bbef6a48c262922f294fd7e Mon Sep 17 00:00:00 2001 From: Johann Fradj Date: Fri, 26 Dec 2025 10:48:02 +0100 Subject: [PATCH 26/27] feat: Add state, sizeBytes, and mimeType to DocumentResponse --- src/Enums/DocumentState.php | 20 +++++ .../Documents/DocumentResponse.php | 12 ++- .../Documents/DocumentResponseFixture.php | 3 + .../Documents/DocumentResponseTest.php | 83 +++++++++++++++++++ 4 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 src/Enums/DocumentState.php create mode 100644 tests/Responses/FileSearchStores/Documents/DocumentResponseTest.php diff --git a/src/Enums/DocumentState.php b/src/Enums/DocumentState.php new file mode 100644 index 0000000..f138e43 --- /dev/null +++ b/src/Enums/DocumentState.php @@ -0,0 +1,20 @@ +, updateTime?: string, createTime?: string } $attributes + * @param array{ name: string, displayName?: string, customMetadata?: array, updateTime?: string, createTime?: string, mimeType?: string, sizeBytes?: string, state?: string } $attributes */ public static function from(array $attributes): self { @@ -36,6 +40,9 @@ public static function from(array $attributes): self customMetadata: $attributes['customMetadata'] ?? [], updateTime: $attributes['updateTime'] ?? null, createTime: $attributes['createTime'] ?? null, + mimeType: $attributes['mimeType'] ?? null, + sizeBytes: (int) ($attributes['sizeBytes'] ?? 0), + state: isset($attributes['state']) ? DocumentState::tryFrom($attributes['state']) : null, ); } @@ -47,6 +54,9 @@ public function toArray(): array 'customMetadata' => $this->customMetadata, 'updateTime' => $this->updateTime, 'createTime' => $this->createTime, + 'mimeType' => $this->mimeType, + 'sizeBytes' => $this->sizeBytes, + 'state' => $this->state?->value, ]; } } diff --git a/src/Testing/Responses/Fixtures/FileSearchStores/Documents/DocumentResponseFixture.php b/src/Testing/Responses/Fixtures/FileSearchStores/Documents/DocumentResponseFixture.php index 0a3f2e7..6b2b59b 100644 --- a/src/Testing/Responses/Fixtures/FileSearchStores/Documents/DocumentResponseFixture.php +++ b/src/Testing/Responses/Fixtures/FileSearchStores/Documents/DocumentResponseFixture.php @@ -12,5 +12,8 @@ final class DocumentResponseFixture 'customMetadata' => [], 'updateTime' => '2024-01-01T00:00:00Z', 'createTime' => '2024-01-01T00:00:00Z', + 'mimeType' => 'text/plain', + 'sizeBytes' => '1024', + 'state' => 'STATE_ACTIVE', ]; } diff --git a/tests/Responses/FileSearchStores/Documents/DocumentResponseTest.php b/tests/Responses/FileSearchStores/Documents/DocumentResponseTest.php new file mode 100644 index 0000000..b653b20 --- /dev/null +++ b/tests/Responses/FileSearchStores/Documents/DocumentResponseTest.php @@ -0,0 +1,83 @@ + 'fileSearchStores/123/documents/abc', + 'displayName' => 'My Document', + 'customMetadata' => [], + 'updateTime' => '2024-01-01T00:00:00Z', + 'createTime' => '2024-01-01T00:00:00Z', + 'mimeType' => 'text/plain', + 'sizeBytes' => '1024', + 'state' => 'STATE_ACTIVE', + ]; + + $response = DocumentResponse::from($attributes); + + expect($response) + ->toBeInstanceOf(DocumentResponse::class) + ->name->toBe('fileSearchStores/123/documents/abc') + ->displayName->toBe('My Document') + ->customMetadata->toBe([]) + ->updateTime->toBe('2024-01-01T00:00:00Z') + ->createTime->toBe('2024-01-01T00:00:00Z') + ->mimeType->toBe('text/plain') + ->sizeBytes->toBe(1024) + ->state->toBe(DocumentState::Active); +}); + +test('from with missing fields', function () { + $attributes = [ + 'name' => 'fileSearchStores/123/documents/abc', + ]; + + $response = DocumentResponse::from($attributes); + + expect($response) + ->toBeInstanceOf(DocumentResponse::class) + ->name->toBe('fileSearchStores/123/documents/abc') + ->displayName->toBeNull() + ->customMetadata->toBe([]) + ->updateTime->toBeNull() + ->createTime->toBeNull() + ->mimeType->toBeNull() + ->sizeBytes->toBe(0) + ->state->toBeNull(); +}); + +test('to array', function () { + $response = new DocumentResponse( + name: 'fileSearchStores/123/documents/abc', + displayName: 'My Document', + customMetadata: [], + updateTime: '2024-01-01T00:00:00Z', + createTime: '2024-01-01T00:00:00Z', + mimeType: 'text/plain', + sizeBytes: 1024, + state: DocumentState::Active, + ); + + expect($response->toArray()) + ->toBe([ + 'name' => 'fileSearchStores/123/documents/abc', + 'displayName' => 'My Document', + 'customMetadata' => [], + 'updateTime' => '2024-01-01T00:00:00Z', + 'createTime' => '2024-01-01T00:00:00Z', + 'mimeType' => 'text/plain', + 'sizeBytes' => 1024, + 'state' => 'STATE_ACTIVE', + ]); +}); + +test('fake', function () { + $response = DocumentResponse::fake(); + + expect($response) + ->toBeInstanceOf(DocumentResponse::class) + ->name->toBe('fileSearchStores/123/documents/abc') + ->state->toBe(DocumentState::Active); +}); From 1d35001d31749b6045b2a3575243ff241cda6a87 Mon Sep 17 00:00:00 2001 From: Johann Fradj Date: Mon, 29 Dec 2025 17:55:37 +0100 Subject: [PATCH 27/27] fix: phpstan errors in FileSearchStores --- src/Contracts/Resources/FileSearchStoresContract.php | 2 ++ src/Requests/FileSearchStores/UploadRequest.php | 3 +++ src/Resources/FileSearchStores.php | 3 +++ 3 files changed, 8 insertions(+) diff --git a/src/Contracts/Resources/FileSearchStoresContract.php b/src/Contracts/Resources/FileSearchStoresContract.php index 37d4839..4bbe1b9 100644 --- a/src/Contracts/Resources/FileSearchStoresContract.php +++ b/src/Contracts/Resources/FileSearchStoresContract.php @@ -44,6 +44,8 @@ public function delete(string $name, bool $force = false): void; /** * Upload a file to a file search store. * + * @param array> $customMetadata + * * @see https://ai.google.dev/api/file-search/file-search-stores#method:-media.uploadtofilesearchstore */ public function upload(string $storeName, string $filename, ?MimeType $mimeType = null, ?string $displayName = null, array $customMetadata = []): UploadResponse; diff --git a/src/Requests/FileSearchStores/UploadRequest.php b/src/Requests/FileSearchStores/UploadRequest.php index 7018239..b670f25 100644 --- a/src/Requests/FileSearchStores/UploadRequest.php +++ b/src/Requests/FileSearchStores/UploadRequest.php @@ -17,6 +17,9 @@ class UploadRequest extends Request { protected Method $method = Method::POST; + /** + * @param array> $customMetadata + */ public function __construct( protected readonly string $storeName, protected readonly string $filename, diff --git a/src/Resources/FileSearchStores.php b/src/Resources/FileSearchStores.php index 0268325..a88aee3 100644 --- a/src/Resources/FileSearchStores.php +++ b/src/Resources/FileSearchStores.php @@ -57,6 +57,9 @@ public function delete(string $name, bool $force = false): void $this->transporter->request(new DeleteRequest($name, $force)); } + /** + * @param array> $customMetadata + */ public function upload(string $storeName, string $filename, ?MimeType $mimeType = null, ?string $displayName = null, array $customMetadata = []): UploadResponse { /** @var ResponseDTO, done?: bool, response?: array, error?: array }> $response */