diff --git a/README.md b/README.md index 2601a22..a455471 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) @@ -49,6 +51,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) @@ -482,6 +495,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 +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 `GoogleMaps`, you may also provide additional configuration via `ToolConfig`, such as `RetrievalConfig` for geographical context. ```php use Gemini\Data\GenerationConfig; @@ -834,6 +901,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..4bbe1b9 --- /dev/null +++ b/src/Contracts/Resources/FileSearchStoresContract.php @@ -0,0 +1,73 @@ +> $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; + + /** + * List documents in a file search store. + * + * @see https://ai.google.dev/api/file-search/documents#method:-fileSearchStores.documents.list + */ + public function listDocuments(string $storeName, ?int $pageSize = null, ?string $nextPageToken = null): DocumentListResponse; + + /** + * Get a document. + * + * @see https://ai.google.dev/api/file-search/documents#method:-fileSearchStores.documents.get + */ + public function getDocument(string $name): DocumentResponse; + + /** + * Delete a document. + * + * @see https://ai.google.dev/api/file-search/documents#method:-fileSearchStores.documents.delete + */ + public function deleteDocument(string $name, bool $force = false): void; +} 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/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/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/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/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/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/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 @@ + + */ + 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..b670f25 --- /dev/null +++ b/src/Requests/FileSearchStores/UploadRequest.php @@ -0,0 +1,99 @@ +> $customMetadata + */ + public function __construct( + protected readonly string $storeName, + protected readonly string $filename, + protected readonly ?string $displayName = null, + protected ?MimeType $mimeType = null, + protected array $customMetadata = [], + ) {} + + public function resolveEndpoint(): string + { + return $this->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; + } + + 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) { + 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 : (mime_content_type($this->filename) ?: '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)); + } + + /** + * @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 */ + $response = $this->transporter->request(new UploadRequest($storeName, $filename, $displayName, $mimeType, $customMetadata)); + + 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/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/Documents/DocumentResponse.php b/src/Responses/FileSearchStores/Documents/DocumentResponse.php new file mode 100644 index 0000000..a6a9be5 --- /dev/null +++ b/src/Responses/FileSearchStores/Documents/DocumentResponse.php @@ -0,0 +1,62 @@ + $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, + public readonly ?string $mimeType = null, + public readonly int $sizeBytes = 0, + public readonly ?DocumentState $state = null, + ) {} + + /** + * @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 + { + return new self( + name: $attributes['name'], + displayName: $attributes['displayName'] ?? null, + 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, + ); + } + + public function toArray(): array + { + return [ + 'name' => $this->name, + 'displayName' => $this->displayName, + 'customMetadata' => $this->customMetadata, + 'updateTime' => $this->updateTime, + 'createTime' => $this->createTime, + 'mimeType' => $this->mimeType, + 'sizeBytes' => $this->sizeBytes, + 'state' => $this->state?->value, + ]; + } +} 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..a9806be --- /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'] ?? (isset($attributes['response']) || isset($attributes['error'])), + 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/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/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..740871e --- /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, array $customMetadata = []): 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/src/Testing/Responses/Fixtures/FileSearchStores/Documents/DocumentResponseFixture.php b/src/Testing/Responses/Fixtures/FileSearchStores/Documents/DocumentResponseFixture.php new file mode 100644 index 0000000..6b2b59b --- /dev/null +++ b/src/Testing/Responses/Fixtures/FileSearchStores/Documents/DocumentResponseFixture.php @@ -0,0 +1,19 @@ + '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', + ]; +} 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/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'); +}); 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([]); +}); diff --git a/tests/Resources/FileSearchStores.php b/tests/Resources/FileSearchStores.php new file mode 100644 index 0000000..df5bcd5 --- /dev/null +++ b/tests/Resources/FileSearchStores.php @@ -0,0 +1,187 @@ + '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('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 () { + $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(); +}); 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); +}); 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']; + }); +});