Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement file uploading and inclusion within chat messages #60

Open
wants to merge 1 commit into
base: beta
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 47 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
- [Chat Resource](#chat-resource)
- [Text-only Input](#text-only-input)
- [Text-and-image Input](#text-and-image-input)
- [File Upload](#file-upload)
- [Text-and-video Input](#text-and-video-input)
- [Multi-turn Conversations (Chat)](#multi-turn-conversations-chat)
- [Stream Generate Content](#stream-generate-content)
- [Structured Output](#structured-output)
Expand Down Expand Up @@ -150,6 +152,50 @@ $result = $client
)
]);

$result->text(); // The picture shows a table with a white tablecloth. On the table are two cups of coffee, a bowl of blueberries, a silver spoon, and some flowers. There are also some blueberry scones on the table.
```

#### File Upload
To reference larger files and videos with various prompts, upload them to Gemini storage.

```php
$files = $client->files();
echo "Uploading\n";
$meta = $files->upload(
filename: 'video.mp4',
mimeType: MimeType::VIDEO_MP4,
displayName: 'Video'
);
echo "Processing";
do {
echo ".";
sleep(2);
$meta = $files->metadataGet($meta->uri);
} while (!$meta->state->complete());
echo "\n";

if ($meta->state == FileState::Failed) {
die("Upload failed:\n" . json_encode($meta->toArray(), JSON_PRETTY_PRINT));
}

echo "Processing complete\n" . json_encode($meta->toArray(), JSON_PRETTY_PRINT);
echo "\n{$meta->uri}";
```

#### Text-and-video Input
If the input contains both text and video, use the `gemini-pro-vision` model and upload the file beforehand.

```php
$result = $client
->geminiFlash()
->generateContent([
'What is this video?',
new UploadedFile(
fileUri: '123-456', // accepts just the name or the full URI
mimeType: MimeType::VIDEO_MP4
)
]);

$result->text(); // The picture shows a table with a white tablecloth. On the table are two cups of coffee, a bowl of blueberries, a silver spoon, and some flowers. There are also some blueberry scones on the table.
```
#### Multi-turn Conversations (Chat)
Expand Down Expand Up @@ -536,4 +582,4 @@ $client = new ClientFake([

// the `ErrorException` will be thrown
$client->geminiPro()->generateContent('test');
```
```
3 changes: 3 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@
"mockery/mockery": "^1.6.7",
"phpstan/phpstan": "^1.10"
},
"suggest": {
"ext-fileinfo": "Reads upload file size and mime type if not provided"
},
"autoload": {
"psr-4": {
"Gemini\\": "src/"
Expand Down
11 changes: 11 additions & 0 deletions src/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Gemini\Enums\ModelType;
use Gemini\Resources\ChatSession;
use Gemini\Resources\EmbeddingModel;
use Gemini\Resources\Files;
use Gemini\Resources\GenerativeModel;
use Gemini\Resources\Models;

Expand Down Expand Up @@ -65,4 +66,14 @@ public function chat(ModelType|string $model = ModelType::GEMINI_PRO): ChatSessi
{
return new ChatSession(model: $this->generativeModel(model: $model));
}

/**
* Resource to manage media file uploads to be reused across multiple requests and prompts.
*
* @link https://ai.google.dev/api/files
*/
public function files(): Files
{
return new Files($this->transporter);
}
}
5 changes: 3 additions & 2 deletions src/Concerns/HasContents.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,18 @@

use Gemini\Data\Blob;
use Gemini\Data\Content;
use Gemini\Data\UploadedFile;
use InvalidArgumentException;

trait HasContents
{
/**
* @param string|Blob|array<string|Blob>|Content ...$parts
* @param string|Blob|array<string|Blob|UploadedFile>|Content|UploadedFile ...$parts
* @return array<Content>
*
* @throws InvalidArgumentException
*/
public function partsToContents(string|Blob|array|Content ...$parts): array
public function partsToContents(string|Blob|array|Content|UploadedFile ...$parts): array
{
return array_map(
callback: static fn ($part) => Content::parse($part),
Expand Down
24 changes: 24 additions & 0 deletions src/Contracts/Resources/FilesContract.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

namespace Gemini\Contracts\Resources;

use Gemini\Enums\MimeType;
use Gemini\Responses\Files\MetadataResponse;

interface FilesContract
{
/**
* Uploads a media file to be reused across multiple requests and prompts.
* Any omitted parameters will be derived from the file.
*/
public function upload(string $filename, ?MimeType $mimeType = null, ?string $displayName = null): MetadataResponse;

/**
* Gets file upload metadata.
*
* @param string $nameOrUri Either the just file name or the complete metadata URI from an upload.
*/
public function metadataGet(string $nameOrUri): MetadataResponse;
}
2 changes: 1 addition & 1 deletion src/Data/Candidate.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public function __construct(
) {}

/**
* @param array{ content: ?array{ parts: array{ array{ text: ?string, inlineData: array{ mimeType: string, data: string } } }, role: string }, finishReason: ?string, safetyRatings: ?array{ array{ category: string, probability: string, blocked: ?bool } }, citationMetadata: ?array{ citationSources: array{ array{ startIndex: int, endIndex: int, uri: ?string, license: ?string} } }, index: ?int, tokenCount: ?int, avgLogprobs: ?float } $attributes
* @param array{ content: ?array{ parts: array{ array{ text: ?string, inlineData: array{ mimeType: string, data: string }, fileData: array{ fileUri: string, mimeType: string } } }, role: string }, finishReason: ?string, safetyRatings: ?array{ array{ category: string, probability: string, blocked: ?bool } }, citationMetadata: ?array{ citationSources: array{ array{ startIndex: int, endIndex: int, uri: ?string, license: ?string} } }, index: ?int, tokenCount: ?int, avgLogprobs: ?float } $attributes
*/
public static function from(array $attributes): self
{
Expand Down
8 changes: 5 additions & 3 deletions src/Data/Content.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,20 @@ public function __construct(
) {}

/**
* @param string|Blob|array<string|Blob>|Content $part
* @param string|Blob|array<string|Blob|UploadedFile>|Content|UploadedFile $part
*/
public static function parse(string|Blob|array|Content $part, Role $role = Role::USER): self
public static function parse(string|Blob|array|Content|UploadedFile $part, Role $role = Role::USER): self
{
return match (true) {
$part instanceof self => $part,
$part instanceof Blob => new Content(parts: [new Part(inlineData: $part)], role: $role),
$part instanceof UploadedFile => new Content(parts: [new Part(fileData: $part)], role: $role),
is_array($part) => new Content(
parts: array_map(
callback: static fn ($subPart) => match (true) {
is_string($subPart) => new Part(text: $subPart),
$subPart instanceof Blob => new Part(inlineData: $subPart),
$subPart instanceof UploadedFile => new Part(fileData: $subPart),
},
array: $part,
),
Expand All @@ -48,7 +50,7 @@ public static function parse(string|Blob|array|Content $part, Role $role = Role:
}

/**
* @param array{ parts: array{ array{ text: ?string, inlineData: array{ mimeType: string, data: string } } }, role: string } $attributes
* @param array{ parts: array{ array{ text: ?string, inlineData: array{ mimeType: string, data: string }, fileData: array{ fileUri: string, mimeType: string } } }, role: string } $attributes
*/
public static function from(array $attributes): self
{
Expand Down
11 changes: 9 additions & 2 deletions src/Data/Part.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,23 @@ final class Part implements Arrayable
/**
* @param string|null $text Inline text.
* @param Blob|null $inlineData Inline media bytes.
* @param UploadedFile|null $fileData Uploaded file information.
*/
public function __construct(
public readonly ?string $text = null,
public readonly ?Blob $inlineData = null,
public readonly ?UploadedFile $fileData = null,
) {}

/**
* @param array{ text: ?string, inlineData: ?array{ mimeType: string, data: string } } $attributes
* @param array{ text: ?string, inlineData: ?array{ mimeType: string, data: string }, fileData: ?array{ fileUri: string, mimeType: string } } $attributes
*/
public static function from(array $attributes): self
{
return new self(
text: $attributes['text'] ?? null,
inlineData: isset($attributes['inlineData']) ? Blob::from($attributes['inlineData']) : null
inlineData: isset($attributes['inlineData']) ? Blob::from($attributes['inlineData']) : null,
fileData: isset($attributes['fileData']) ? UploadedFile::from($attributes['fileData']) : null,
);
}

Expand All @@ -43,6 +46,10 @@ public function toArray(): array
$data['inlineData'] = $this->inlineData;
}

if ($this->fileData !== null) {
$data['fileData'] = $this->fileData;
}

return $data;
}
}
39 changes: 39 additions & 0 deletions src/Data/UploadedFile.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

declare(strict_types=1);

namespace Gemini\Data;

use Gemini\Contracts\Arrayable;
use Gemini\Enums\MimeType;

final class UploadedFile implements Arrayable
{
/**
* @param string $fileUri Full URI to uploaded file.
* @param MimeType $mimeType The IANA standard MIME type of the source data.
*/
public function __construct(
public readonly string $fileUri,
public readonly MimeType $mimeType,
) {}

/**
* @param array{ fileUri: string, mimeType: string } $attributes
*/
public static function from(array $attributes): self
{
return new self(
fileUri: $attributes['fileUri'],
mimeType: MimeType::from($attributes['mimeType']),
);
}

public function toArray(): array
{
return [
'fileUri' => $this->fileUri,
'mimeType' => $this->mimeType->value,
];
}
}
34 changes: 34 additions & 0 deletions src/Data/VideoMetadata.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

declare(strict_types=1);

namespace Gemini\Data;

use Gemini\Contracts\Arrayable;

/**
* Video-specific metadata from a video file upload.
*/
final class VideoMetadata implements Arrayable
{
public function __construct(
public readonly string $videoDuration,
) {}

/**
* @param array{ videoDuration: string } $attributes
*/
public static function from(array $attributes): self
{
return new self(
videoDuration: $attributes['videoDuration'],
);
}

public function toArray(): array
{
return [
'videoDuration' => $this->videoDuration,
];
}
}
28 changes: 28 additions & 0 deletions src/Enums/FileState.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

declare(strict_types=1);

namespace Gemini\Enums;

/**
* @link https://ai.google.dev/api/files#State
*/
enum FileState: string
{
/** The default value. This value is used if the state is omitted. */
case StateUnspecified = 'STATE_UNSPECIFIED';
/** File is being processed and cannot be used for inference yet. */
case Processing = 'PROCESSING';
/** File is processed and available for inference. */
case Active = 'ACTIVE';
/** File failed processing. */
case Failed = 'FAILED';

public function complete(): bool
{
return match ($this) {
self::StateUnspecified, self::Processing => false,
self::Active, self::Failed => true,
};
}
}
45 changes: 45 additions & 0 deletions src/Requests/Files/MetadataGetRequest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

declare(strict_types=1);

namespace Gemini\Requests\Files;

use Gemini\Enums\Method;
use Gemini\Foundation\Request;
use Gemini\Requests\Concerns\HasJsonBody;
use Psr\Http\Message\RequestInterface;

/**
* @link https://ai.google.dev/api/files#method:-files.get
*/
class MetadataGetRequest extends Request
{
use HasJsonBody;

protected Method $method = Method::GET;

/**
* @param string $nameOrUri Either the just file name or the complete metadata URI from an upload.
*/
public function __construct(
protected readonly string $nameOrUri
) {}

public function resolveEndpoint(): string
{
if (str_starts_with($this->nameOrUri, 'http')) {
return $this->nameOrUri;
}

return "files/{$this->nameOrUri}";
}

public function toRequest(string $baseUrl, array $headers = [], array $queryParams = []): RequestInterface
{
if (str_starts_with($this->resolveEndpoint(), 'http')) {
$baseUrl = '';
}

return parent::toRequest($baseUrl, $headers, $queryParams);
}
}
Loading