diff --git a/README.md b/README.md index 2601a22..25b6209 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,125 @@ 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', +); + +echo "File search store created: {$fileSearchStore->name}\n"; +``` + +#### Get File Search Store +Get a specific file search store by name. + +```php +$fileSearchStore = $client->fileSearchStores()->get('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 "--- \n"; +} +``` + +#### Delete File Search Store +Delete a file search store by name. + +```php +$client->fileSearchStores()->delete('fileSearchStores/my-search-store'); +``` + +### File Search Documents + +#### Upload File Search Document +Upload a local file directly to a file search store. + +```php +use Gemini\Enums\MimeType; + +$response = $client->fileSearchStores()->upload( + storeName: 'fileSearchStores/my-search-store', + filename: 'document2.pdf', + mimeType: MimeType::APPLICATION_PDF, + displayName: 'Another Search Document' +); + +echo "File search document upload operation: {$response->name}\n"; +``` + +#### Get File Search Document +Get a specific file search document by name. + +```php +$fileSearchDocument = $client->fileSearchStores()->getDocument('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->fileSearchStores()->listDocuments(storeName: 'fileSearchStores/my-search-store', pageSize: 10); + +foreach ($response->documents 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->fileSearchStores()->deleteDocument('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. 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..dc9ba8f --- /dev/null +++ b/src/Contracts/Resources/FileSearchStoresContract.php @@ -0,0 +1,70 @@ + + */ + 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..01eab4c --- /dev/null +++ b/src/Requests/FileSearchStores/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/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..961c907 --- /dev/null +++ b/src/Requests/FileSearchStores/UploadRequest.php @@ -0,0 +1,78 @@ +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); + if ($contents === false) { + throw new \RuntimeException("Failed to read file: {$this->filename}"); + } + + $request = $factory + ->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); + } + + $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(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)); + + 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..6307e61 --- /dev/null +++ b/src/Responses/FileSearchStores/UploadResponse.php @@ -0,0 +1,54 @@ +|null $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(); +});