diff --git a/src/Mscc.GenerativeAI/GenerativeModel.cs b/src/Mscc.GenerativeAI/GenerativeModel.cs index 3f852b6..f275077 100644 --- a/src/Mscc.GenerativeAI/GenerativeModel.cs +++ b/src/Mscc.GenerativeAI/GenerativeModel.cs @@ -499,7 +499,7 @@ public async Task TransferOwnership(string model, string emailAddress) /// Uploads a file to the File API backend. /// /// URI or path to the file to upload. - /// A name displazed for the uploaded file. + /// A name displayed for the uploaded file. /// Flag indicating whether to use resumable upload. /// A cancellation token to cancel the upload. /// A URI of the uploaded file. @@ -557,6 +557,53 @@ public async Task UploadFile(string uri, } } + public async Task UploadFile(Stream stream, + string displayName, + string mimeType, + bool resumable = false, + CancellationToken cancellationToken = default) + { + if (stream == null) throw new ArgumentNullException(nameof(stream)); + if (stream.Length > Constants.MaxUploadFileSize) throw new MaxUploadFileSizeException(nameof(stream)); + if (string.IsNullOrEmpty(mimeType)) throw new ArgumentException(nameof(mimeType)); + if (string.IsNullOrEmpty(displayName)) throw new ArgumentException(nameof(displayName)); + + var totalBytes = stream.Length; + var request = new UploadMediaRequest() + { + File = new FileRequest() + { + DisplayName = displayName + } + }; + + var url = $"{EndpointGoogleAi}/upload/{Version}/files"; // v1beta3 // ?key={apiKey} + if (resumable) + { + url = $"{EndpointGoogleAi}/resumable/upload/{Version}/files"; // v1beta3 // ?key={apiKey} + } + url = ParseUrl(url).AddQueryString(new Dictionary() + { + ["alt"] = "json", + ["uploadType"] = "multipart" + }); + string json = Serialize(request); + + var multipartContent = new MultipartContent("related"); + multipartContent.Add(new StringContent(json, Encoding.UTF8, Constants.MediaType)); + multipartContent.Add(new StreamContent(stream, (int)Constants.ChunkSize) + { + Headers = { + ContentType = new MediaTypeHeaderValue(mimeType), + ContentLength = totalBytes + } + }); + + var response = await Client.PostAsync(url, multipartContent, cancellationToken); + await response.EnsureSuccessAsync(); + return await Deserialize(response); + } + /// /// Lists the metadata for Files owned by the requesting project. /// diff --git a/tests/Mscc.GenerativeAI/GoogleAi_Gemini15Pro_Should.cs b/tests/Mscc.GenerativeAI/GoogleAi_Gemini15Pro_Should.cs index 7193463..9383b35 100644 --- a/tests/Mscc.GenerativeAI/GoogleAi_Gemini15Pro_Should.cs +++ b/tests/Mscc.GenerativeAI/GoogleAi_Gemini15Pro_Should.cs @@ -349,6 +349,44 @@ public async void Upload_File_WithResume_Using_FileAPI() File.Delete(filePath); } + [Theory] + [InlineData("scones.jpg", "Set of blueberry scones", "image/jpeg")] + [InlineData("cat.jpg", "Wildcat on snow", "image/jpeg")] + [InlineData("cat.jpg", "Cat in the snow", "image/jpeg")] + [InlineData("image.jpg", "Sample drawing", "image/jpeg")] + [InlineData("animals.mp4", "Zootopia in da house", "video/mp4")] + [InlineData("sample.mp3", "State_of_the_Union_Address_30_January_1961", "audio/mp3")] + [InlineData("pixel.mp3", "Pixel Feature Drops: March 2023", "audio/mp3")] + [InlineData("gemini.pdf", + "Gemini 1.5: Unlocking multimodal understanding across millions of tokens of context", "application/pdf")] + public async void Upload_Stream_Using_FileAPI(string filename, string displayName, string mimeType) + { + // Arrange + var filePath = Path.Combine(Environment.CurrentDirectory, "payload", filename); + IGenerativeAI genAi = new GoogleAI(_fixture.ApiKey); + var model = genAi.GenerativeModel(_model); + + // Act + using (var fs = new FileStream(filePath, FileMode.Open)) + { + var response = await model.UploadFile(fs, displayName, mimeType); + + // Assert + response.Should().NotBeNull(); + response.File.Should().NotBeNull(); + response.File.Name.Should().NotBeNull(); + response.File.DisplayName.Should().Be(displayName); + // response.File.MimeType.Should().Be("image/jpeg"); + // response.File.CreateTime.Should().BeGreaterThan(DateTime.Now.Add(TimeSpan.FromHours(48))); + // response.File.ExpirationTime.Should().NotBeNull(); + // response.File.UpdateTime.Should().NotBeNull(); + response.File.SizeBytes.Should().BeGreaterThan(0); + response.File.Sha256Hash.Should().NotBeNull(); + response.File.Uri.Should().NotBeNull(); + _output.WriteLine($"Uploaded file '{response?.File.DisplayName}' as: {response?.File.Uri}"); + } + } + [Fact] public async void List_Files() {