From 7aa699bd3a0895e6687de5ba437de624123440c0 Mon Sep 17 00:00:00 2001 From: HavenDV Date: Sat, 31 Aug 2024 16:28:14 +0400 Subject: [PATCH] docs: Improved docs. --- docs/index.md | 230 ++++++++++++++++++ .../Assistants.AssistantsWithVision.md | 79 ++++++ ...Assistants.ListAssistantsWithPagination.md | 15 ++ docs/samples/Assistants.ListFiles.md | 15 ++ ...Assistants.RetrievalAugmentedGeneration.md | 148 +++++++++++ docs/samples/Audio.SimpleTextToSpeech.md | 18 ++ docs/samples/Audio.SimpleTranscription.md | 10 + docs/samples/Audio.SimpleTranslation.md | 10 + docs/samples/Audio.VerboseTranscription.md | 32 +++ docs/samples/Chat.ChatWithVision.md | 13 + docs/samples/Chat.SimpleChat.md | 10 + docs/samples/Chat.SimpleChatStreaming.md | 13 + docs/samples/Combination.AlpacaArtAssessor.md | 40 +++ .../Combination.CuriousCreatureCreator.md | 62 +++++ .../Embeddings.EmbeddingWithOptions.md | 18 ++ docs/samples/Embeddings.MultipleEmbeddings.md | 27 ++ docs/samples/Embeddings.SimpleEmbedding.md | 17 ++ docs/samples/Images.SimpleImageEdit.md | 32 +++ docs/samples/Images.SimpleImageGeneration.md | 24 ++ docs/samples/Images.SimpleImageVariation.md | 23 ++ mkdocs.yml | 29 ++- src/helpers/GenerateDocs/Program.cs | 20 +- .../Conversions/CreateMessageRequest.cs | 63 +++++ .../MessageContentImageFileObject.cs | 53 ++++ ...xamples.Assistants.AssistantsWithVision.cs | 87 +++++++ .../Example05_AssistantsWithVisionAsync.cs | 71 ------ 26 files changed, 1081 insertions(+), 78 deletions(-) create mode 100644 docs/index.md create mode 100644 docs/samples/Assistants.AssistantsWithVision.md create mode 100644 docs/samples/Assistants.ListAssistantsWithPagination.md create mode 100644 docs/samples/Assistants.ListFiles.md create mode 100644 docs/samples/Assistants.RetrievalAugmentedGeneration.md create mode 100644 docs/samples/Audio.SimpleTextToSpeech.md create mode 100644 docs/samples/Audio.SimpleTranscription.md create mode 100644 docs/samples/Audio.SimpleTranslation.md create mode 100644 docs/samples/Audio.VerboseTranscription.md create mode 100644 docs/samples/Chat.ChatWithVision.md create mode 100644 docs/samples/Chat.SimpleChat.md create mode 100644 docs/samples/Chat.SimpleChatStreaming.md create mode 100644 docs/samples/Combination.AlpacaArtAssessor.md create mode 100644 docs/samples/Combination.CuriousCreatureCreator.md create mode 100644 docs/samples/Embeddings.EmbeddingWithOptions.md create mode 100644 docs/samples/Embeddings.MultipleEmbeddings.md create mode 100644 docs/samples/Embeddings.SimpleEmbedding.md create mode 100644 docs/samples/Images.SimpleImageEdit.md create mode 100644 docs/samples/Images.SimpleImageGeneration.md create mode 100644 docs/samples/Images.SimpleImageVariation.md create mode 100644 src/libs/OpenAI/Conversions/CreateMessageRequest.cs create mode 100644 src/libs/OpenAI/Conversions/MessageContentImageFileObject.cs create mode 100644 src/tests/OpenAI.IntegrationTests/Examples/Examples.Assistants.AssistantsWithVision.cs delete mode 100644 src/tests/OpenAI.IntegrationTests/Extended/Assistants/Example05_AssistantsWithVisionAsync.cs diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000..7540d5c6 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,230 @@ +# OpenAI + +[![Nuget package](https://img.shields.io/nuget/vpre/tryAGI.OpenAI)](https://www.nuget.org/packages/tryAGI.OpenAI/) +[![dotnet](https://github.com/tryAGI/OpenAI/actions/workflows/dotnet.yml/badge.svg?branch=main)](https://github.com/tryAGI/OpenAI/actions/workflows/dotnet.yml) +[![License: MIT](https://img.shields.io/github/license/tryAGI/OpenAI)](https://github.com/tryAGI/OpenAI/blob/main/LICENSE.txt) +[![Discord](https://img.shields.io/discord/1115206893015662663?label=Discord&logo=discord&logoColor=white&color=d82679)](https://discord.gg/Ca2xhfBf3v) + +## Features 🔥 +- Fully generated C# SDK based on [official OpenAI OpenAPI specification](https://raw.githubusercontent.com/openai/openai-openapi/master/openapi.yaml) using [OpenApiGenerator](https://github.com/HavenDV/OpenApiGenerator) +- Same day update to support new features +- Updated and supported automatically if there are no breaking changes +- Contains a supported list of constants such as current prices, models, and other +- Source generator to define functions natively through C# interfaces +- All modern .NET features - nullability, trimming, NativeAOT, etc. +- Support .Net Framework/.Net Standard 2.0 +- Support all OpenAI API endpoints including completions, chat, embeddings, images, assistants and more. +- Regularly tested for compatibility with popular custom providers like OpenRouter/DeepSeek + +## Documentation +Examples and documentation can be found here: https://tryagi.github.io/OpenAI/ + +## Usage +```csharp +using var api = new OpenAiApi("API_KEY"); +string response = await api.Chat.CreateChatCompletionAsync( + messages: ["Generate five random words."], + model: CreateChatCompletionRequestModel.Gpt4oMini); +Console.WriteLine(response); // "apple, banana, cherry, date, elderberry" + +var enumerable = api.Chat.CreateChatCompletionAsStreamAsync( + messages: ["Generate five random words."], + model: CreateChatCompletionRequestModel.Gpt4oMini); + +await foreach (string response in enumerable) +{ + Console.WriteLine(response); +} +``` +It uses three implicit conversions: +- from `string` to `ChatCompletionRequestUserMessage`. It will always be converted to the user message. +- from `ChatCompletionResponseMessage` to `string` . It will always contain the first choice message content. +- from `CreateChatCompletionStreamResponse` to `string` . It will always contain the first delta content. + +You still can use the full response objects if you need more information, just replace `string response` to `var response`. + +### Tools +```csharp +using OpenAI; + +public enum Unit +{ + Celsius, + Fahrenheit, +} + +public class Weather +{ + public string Location { get; set; } = string.Empty; + public double Temperature { get; set; } + public Unit Unit { get; set; } + public string Description { get; set; } = string.Empty; +} + +[OpenAiTools(Strict = true)] // false by default. You can't use parameters with default values in Strict mode. +public interface IWeatherFunctions +{ + [Description("Get the current weather in a given location")] + public Task GetCurrentWeatherAsync( + [Description("The city and state, e.g. San Francisco, CA")] string location, + Unit unit, + CancellationToken cancellationToken = default); +} + +public class WeatherService : IWeatherFunctions +{ + public Task GetCurrentWeatherAsync(string location, Unit unit = Unit.Celsius, CancellationToken cancellationToken = default) + { + return Task.FromResult(new Weather + { + Location = location, + Temperature = 22.0, + Unit = unit, + Description = "Sunny", + }); + } +} + +using var api = new OpenAiApi("API_KEY"); + +var service = new WeatherService(); +var tools = service.AsTools(); + +var messages = new List +{ + "You are a helpful weather assistant.".AsSystemMessage(), + "What is the current temperature in Dubai, UAE in Celsius?".AsUserMessage(), +}; +var model = CreateChatCompletionRequestModel.Gpt4oMini; +var result = await api.Chat.CreateChatCompletionAsync( + messages, + model: model, + tools: tools); +var resultMessage = result.Choices.First().Message; +messages.Add(resultMessage.AsRequestMessage()); + +foreach (var call in resultMessage.ToolCalls) +{ + var json = await service.CallAsync( + functionName: call.Function.Name, + argumentsAsJson: call.Function.Arguments); + messages.Add(json.AsToolMessage(call.Id)); +} + +var result = await api.Chat.CreateChatCompletionAsync( + messages, + model: model, + tools: tools); +var resultMessage = result.Choices.First().Message; +messages.Add(resultMessage.AsRequestMessage()); +``` +``` +> System: +You are a helpful weather assistant. +> User: +What is the current temperature in Dubai, UAE in Celsius? +> Assistant: +call_3sptsiHzKnaxF8bs8BWxPo0B: +GetCurrentWeather({"location":"Dubai, UAE","unit":"celsius"}) +> Tool(call_3sptsiHzKnaxF8bs8BWxPo0B): +{"location":"Dubai, UAE","temperature":22,"unit":"celsius","description":"Sunny"} +> Assistant: +The current temperature in Dubai, UAE is 22°C with sunny weather. +``` + +### Structured Outputs +```csharp +using OpenAI; + +using var api = new OpenAiApi("API_KEY"); + +var response = await api.Chat.CreateChatCompletionAsAsync( + messages: ["Generate random weather."], + model: CreateChatCompletionRequestModel.Gpt4oMini, + jsonSerializerOptions: new JsonSerializerOptions + { + Converters = {new JsonStringEnumConverter()}, + }); +// or (if you need trimmable/NativeAOT version) +var response = await api.Chat.CreateChatCompletionAsAsync( + jsonTypeInfo: SourceGeneratedContext.Default.Weather, + messages: ["Generate random weather."], + model: CreateChatCompletionRequestModel.Gpt4oMini); + +// response.Value1 contains the structured output +// response.Value2 contains the CreateChatCompletionResponse object +``` +``` +Weather: +Location: San Francisco, CA +Temperature: 65 +Unit: Fahrenheit +Description: Partly cloudy with a light breeze and occasional sunshine. +Raw Response: +{"Location":"San Francisco, CA","Temperature":65,"Unit":"Fahrenheit","Description":"Partly cloudy with a light breeze and occasional sunshine."} +``` +Additional code for trimmable/NativeAOT version: +```csharp +[JsonSourceGenerationOptions(Converters = [typeof(JsonStringEnumConverter)])] +[JsonSerializable(typeof(Weather))] +public partial class SourceGeneratedContext : JsonSerializerContext; +``` + +### Custom providers +```csharp +using OpenAI; + +using var api = CustomProviders.GitHubModels("GITHUB_TOKEN"); +using var api = CustomProviders.Azure("API_KEY", "ENDPOINT"); +using var api = CustomProviders.DeepInfra("API_KEY"); +using var api = CustomProviders.DeepSeek("API_KEY"); +using var api = CustomProviders.Fireworks("API_KEY"); +using var api = CustomProviders.OpenRouter("API_KEY"); +using var api = CustomProviders.Together("API_KEY"); +``` + +### Constants +All `tryGetXXX` methods return `null` if the value is not found. +There also non-try methods that throw an exception if the value is not found. +```cs +using OpenAI; + +// You can try to get the enum from string using: +var model = CreateChatCompletionRequestModelExtensions.ToEnum("gpt-4o") ?? throw new Exception("Invalid model"); + +// Chat +var model = CreateChatCompletionRequestModel.Gpt4oMini; +double? priceInUsd = model.TryGetPriceInUsd( + inputTokens: 500, + outputTokens: 500) +double? priceInUsd = model.TryGetFineTunePriceInUsd( + trainingTokens: 500, + inputTokens: 500, + outputTokens: 500) +int contextLength = model.TryGetContextLength() // 128_000 +int outputLength = model.TryGetOutputLength() // 16_000 + +// Embeddings +var model = CreateEmbeddingRequestModel.TextEmbedding3Small; +int? maxInputTokens = model.TryGetMaxInputTokens() // 8191 +double? priceInUsd = model.TryGetPriceInUsd(tokens: 500) + +// Images +double? priceInUsd = CreateImageRequestModel.DallE3.TryGetPriceInUsd( + size: CreateImageRequestSize.x1024x1024, + quality: CreateImageRequestQuality.Hd) + +// Speech to Text +double? priceInUsd = CreateTranscriptionRequestModel.Whisper1.TryGetPriceInUsd( + seconds: 60) + +// Text to Speech +double? priceInUsd = CreateSpeechRequestModel.Tts1Hd.TryGetPriceInUsd( + characters: 1000) +``` + +## Support + +Priority place for bugs: https://github.com/tryAGI/OpenAI/issues +Priority place for ideas and general questions: https://github.com/tryAGI/OpenAI/discussions +Discord: https://discord.gg/Ca2xhfBf3v \ No newline at end of file diff --git a/docs/samples/Assistants.AssistantsWithVision.md b/docs/samples/Assistants.AssistantsWithVision.md new file mode 100644 index 00000000..ea4c3446 --- /dev/null +++ b/docs/samples/Assistants.AssistantsWithVision.md @@ -0,0 +1,79 @@ +```csharp +using var api = GetAuthenticatedClient(); + +ImagesResponse appleImage = await api.Images.CreateImageAsync( + prompt: "picture of apple", + responseFormat: CreateImageRequestResponseFormat.B64Json); +byte[] appleBytes = appleImage.Data[0].Bytes; + +FileInfo appleFileInfo = new($"{Guid.NewGuid()}.png"); + +await File.WriteAllBytesAsync(appleFileInfo.FullName, appleBytes); + +Console.WriteLine($"Apple image available at:\n{new Uri(appleFileInfo.FullName).AbsoluteUri}"); + +ImagesResponse orangeImage = await api.Images.CreateImageAsync( + prompt: "picture of orange", + responseFormat: CreateImageRequestResponseFormat.B64Json); +byte[] orangeBytes = orangeImage.Data[0].Bytes; + +FileInfo orangeFileInfo = new($"{Guid.NewGuid()}.png"); + +await File.WriteAllBytesAsync(orangeFileInfo.FullName, orangeBytes); + +Console.WriteLine($"Orange image available at:\n{new Uri(orangeFileInfo.FullName).AbsoluteUri}"); + +OpenAIFile pictureOfAppleFile = await api.Files.CreateFileAsync( + file: appleBytes, + filename: appleFileInfo.Name, + purpose: CreateFileRequestPurpose.Vision); +OpenAIFile pictureOfOrangeFile = await api.Files.CreateFileAsync( + file: orangeBytes, + filename: orangeFileInfo.Name, + purpose: CreateFileRequestPurpose.Vision); + +AssistantObject assistant = await api.Assistants.CreateAssistantAsync( + model: CreateAssistantRequestModel.Gpt4o, + instructions: "When asked a question, attempt to answer very concisely. " + + "Prefer one-sentence answers whenever feasible."); + +ThreadObject thread = await api.Assistants.CreateThreadAsync(new CreateThreadRequest +{ + Messages = [ + "Hello, assistant! Please compare these two images for me:", + pictureOfAppleFile, + pictureOfOrangeFile, + ] +}); + +// AsyncResultCollection streamingUpdates = api.Assistants.CreateRunStreamingAsync( +// thread, +// assistant, +// new RunCreationOptions() +// { +// AdditionalInstructions = "When possible, try to sneak in puns if you're asked to compare things.", +// }); +// +// await foreach (StreamingUpdate streamingUpdate in streamingUpdates) +// { +// if (streamingUpdate.UpdateKind == StreamingUpdateReason.RunCreated) +// { +// Console.WriteLine($"--- Run started! ---"); +// } +// if (streamingUpdate is MessageContentUpdate contentUpdate) +// { +// Console.Write(contentUpdate.Text); +// } +// } + +RunObject response = await api.Assistants.CreateRunAsync( + threadId: thread.Id, + assistantId: assistant.Id, + instructions: "When possible, try to sneak in puns if you're asked to compare things."); + +Console.WriteLine(response[0].Content); + +_ = await api.Files.DeleteFileAsync(pictureOfAppleFile.Id); +_ = await api.Assistants.DeleteThreadAsync(thread.Id); +_ = await api.Assistants.DeleteAssistantAsync(assistant.Id); +``` \ No newline at end of file diff --git a/docs/samples/Assistants.ListAssistantsWithPagination.md b/docs/samples/Assistants.ListAssistantsWithPagination.md new file mode 100644 index 00000000..aa324373 --- /dev/null +++ b/docs/samples/Assistants.ListAssistantsWithPagination.md @@ -0,0 +1,15 @@ +```csharp +using var api = GetAuthenticatedClient(); + +int count = 0; + +ListAssistantsResponse response = await api.Assistants.ListAssistantsAsync(); +foreach (AssistantObject assistant in response.Data) +{ + Console.WriteLine($"[{count,3}] {assistant.Id} {assistant.CreatedAt:s} {assistant.Name}"); + + count++; + + //_ = await api.Assistants.DeleteAssistantAsync(assistant.Id); +} +``` \ No newline at end of file diff --git a/docs/samples/Assistants.ListFiles.md b/docs/samples/Assistants.ListFiles.md new file mode 100644 index 00000000..5302d906 --- /dev/null +++ b/docs/samples/Assistants.ListFiles.md @@ -0,0 +1,15 @@ +```csharp +using var api = GetAuthenticatedClient(); + +int count = 0; + +ListFilesResponse files = await api.Files.ListFilesAsync(purpose: CreateFileRequestPurpose.Assistants.ToValueString()); +foreach (OpenAIFile file in files.Data) +{ + Console.WriteLine($"[{count,3}] {file.Id} {file.CreatedAt:s} {file.Filename}"); + + count++; + + //_ = await api.Files.DeleteFileAsync(file.Id); +} +``` \ No newline at end of file diff --git a/docs/samples/Assistants.RetrievalAugmentedGeneration.md b/docs/samples/Assistants.RetrievalAugmentedGeneration.md new file mode 100644 index 00000000..93843e35 --- /dev/null +++ b/docs/samples/Assistants.RetrievalAugmentedGeneration.md @@ -0,0 +1,148 @@ +```csharp +using var api = GetAuthenticatedClient(); + +// First, let's contrive a document we'll use retrieval with and upload it. +string document = /* language=json */ + """ + { + "description": "This document contains the sale history data for Contoso products.", + "sales": [ + { + "month": "January", + "by_product": { + "113043": 15, + "113045": 12, + "113049": 2 + } + }, + { + "month": "February", + "by_product": { + "113045": 22 + } + }, + { + "month": "March", + "by_product": { + "113045": 16, + "113055": 5 + } + } + ] + } + """; + +OpenAIFile salesFile = await api.Files.CreateFileAsync( + file: Encoding.UTF8.GetBytes(document), + filename: "monthly_sales.json", + purpose: CreateFileRequestPurpose.Assistants); + +AssistantObject assistant = await api.Assistants.CreateAssistantAsync( + model: CreateAssistantRequestModel.Gpt4o, + name: "Example: Contoso sales RAG", + instructions: "You are an assistant that looks up sales data and helps visualize the information based" + + " on user queries. When asked to generate a graph, chart, or other visualization, use" + + " the code interpreter tool to do so.", + tools: + [ + new AssistantToolsFileSearch(), + new AssistantToolsCode(), + ], + toolResources: new CreateAssistantRequestToolResources + { + FileSearch = new CreateAssistantRequestToolResourcesFileSearch + { + VectorStores = + [ + new CreateAssistantRequestToolResourcesFileSearchVectorStore + { + FileIds = [salesFile.Id], + } + ], + } + }); + +// Now we'll create a thread with a user query about the data already associated with the assistant, then run it +RunObject threadRun = await api.Assistants.CreateThreadAndRunAsync( + assistantId: assistant.Id, + thread: new CreateThreadRequest + { + Messages = new List + { + new() + { + Role = CreateMessageRequestRole.User, + Content = "How well did product 113045 sell in February? Graph its trend over time.", + }, + }, + }); + +// Check back to see when the run is done +do +{ + await Task.Delay(TimeSpan.FromSeconds(1)); + + threadRun = await api.Assistants.GetRunAsync( + threadId: threadRun.ThreadId, + runId: threadRun.Id); +} while (threadRun.Status is RunObjectStatus.Queued or RunObjectStatus.InProgress); + +// Finally, we'll print out the full history for the thread that includes the augmented generation +// TODO: IAsyncEnumerable pagination +ListMessagesResponse messages + = await api.Assistants.ListMessagesAsync( + threadId: threadRun.ThreadId); // ListOrder.OldestFirst + +foreach (MessageObject message in messages.Data) +{ + Console.Write($"[{message.Role.ToString().ToUpper()}]: "); + foreach (OneOf< + MessageContentImageFileObject, + MessageContentImageUrlObject, + MessageContentTextObject, + MessageContentRefusalObject> contentItem in message.Content) + { + if (contentItem.IsValue3) + { + Console.WriteLine($"{contentItem.Value3.Text.Value}"); + + if (contentItem.Value3.Text.Annotations.Count > 0) + { + Console.WriteLine(); + } + + // Include annotations, if any. + foreach (OneOf< + MessageContentTextAnnotationsFileCitationObject, + MessageContentTextAnnotationsFilePathObject> annotation in contentItem.Value3.Text.Annotations) + { + if (annotation.IsValue1 && !string.IsNullOrEmpty(annotation.Value1.FileCitation.FileId)) + { + Console.WriteLine($"* File citation, file ID: {annotation.Value1.FileCitation.FileId}"); + } + if (annotation.IsValue2 && !string.IsNullOrEmpty(annotation.Value2.FilePath.FileId)) + { + Console.WriteLine($"* File output, new file ID: {annotation.Value2.FilePath.FileId}"); + } + } + } + if (contentItem.IsValue1) + { + OpenAIFile imageInfo = await api.Files.RetrieveFileAsync(contentItem.Value1.ImageFile.FileId); + byte[] imageBytes = await api.Files.DownloadFileAsync(contentItem.Value1.ImageFile.FileId); + + FileInfo fileInfo = new($"{imageInfo.Filename}.png"); + + await File.WriteAllBytesAsync(fileInfo.FullName, imageBytes); + + Console.WriteLine($""); + } + } + Console.WriteLine(); +} + +// Optionally, delete any persistent resources you no longer need. +_ = await api.Assistants.DeleteThreadAsync(threadRun.ThreadId); +_ = await api.Assistants.DeleteAssistantAsync(assistant.Id); +_ = await api.Files.DeleteFileAsync(salesFile.Id); +``` \ No newline at end of file diff --git a/docs/samples/Audio.SimpleTextToSpeech.md b/docs/samples/Audio.SimpleTextToSpeech.md new file mode 100644 index 00000000..40cf96a3 --- /dev/null +++ b/docs/samples/Audio.SimpleTextToSpeech.md @@ -0,0 +1,18 @@ +```csharp +using var api = GetAuthenticatedClient(); + +byte[] bytes = await api.Audio.CreateSpeechAsync( + model: CreateSpeechRequestModel.Tts1, + input: "Overwatering is a common issue for those taking care of houseplants. To prevent it, it is" + + " crucial to allow the soil to dry out between waterings. Instead of watering on a fixed schedule," + + " consider using a moisture meter to accurately gauge the soil’s wetness. Should the soil retain" + + " moisture, it is wise to postpone watering for a couple more days. When in doubt, it is often safer" + + " to water sparingly and maintain a less-is-more approach.", + voice: CreateSpeechRequestVoice.Alloy); + +FileInfo fileInfo = new($"{Guid.NewGuid()}.mp3"); + +await File.WriteAllBytesAsync(fileInfo.FullName, bytes); + +Console.WriteLine($"Audio available at:\n{new Uri(fileInfo.FullName).AbsoluteUri}"); +``` \ No newline at end of file diff --git a/docs/samples/Audio.SimpleTranscription.md b/docs/samples/Audio.SimpleTranscription.md new file mode 100644 index 00000000..1ab8ace2 --- /dev/null +++ b/docs/samples/Audio.SimpleTranscription.md @@ -0,0 +1,10 @@ +```csharp +using var api = GetAuthenticatedClient(); + +OneOf response = await api.Audio.CreateTranscriptionAsync( + file: H.Resources.audio_houseplant_care_mp3.AsBytes(), + filename: H.Resources.audio_houseplant_care_mp3.FileName, + model: CreateTranscriptionRequestModel.Whisper1); + +Console.WriteLine($"{response.Value1?.Text}"); +``` \ No newline at end of file diff --git a/docs/samples/Audio.SimpleTranslation.md b/docs/samples/Audio.SimpleTranslation.md new file mode 100644 index 00000000..931b6120 --- /dev/null +++ b/docs/samples/Audio.SimpleTranslation.md @@ -0,0 +1,10 @@ +```csharp +using var api = GetAuthenticatedClient(); + +OneOf response = await api.Audio.CreateTranslationAsync( + file: H.Resources.audio_french_wav.AsBytes(), + filename: H.Resources.audio_french_wav.FileName, + model: CreateTranslationRequestModel.Whisper1); + +Console.WriteLine($"{response.Value1?.Text}"); +``` \ No newline at end of file diff --git a/docs/samples/Audio.VerboseTranscription.md b/docs/samples/Audio.VerboseTranscription.md new file mode 100644 index 00000000..0ed2a339 --- /dev/null +++ b/docs/samples/Audio.VerboseTranscription.md @@ -0,0 +1,32 @@ +```csharp +using var api = GetAuthenticatedClient(); + +OneOf response = await api.Audio.CreateTranscriptionAsync( + file: H.Resources.audio_houseplant_care_mp3.AsBytes(), + filename: H.Resources.audio_houseplant_care_mp3.FileName, + model: CreateTranscriptionRequestModel.Whisper1, + responseFormat: CreateTranscriptionRequestResponseFormat.VerboseJson, + timestampGranularities: [ + CreateTranscriptionRequestTimestampGranularitie.Word, + CreateTranscriptionRequestTimestampGranularitie.Segment + ]); + +response.Value2.Should().NotBeNull(); + +Console.WriteLine("Transcription:"); +Console.WriteLine($"{response.Value2!.Text}"); + +Console.WriteLine(); +Console.WriteLine($"Words:"); +foreach (TranscriptionWord word in response.Value2.Words ?? []) +{ + Console.WriteLine($" {word.Word,15} : {word.Start * 1000,5:0} - {word.End * 1000,5:0}"); +} + +Console.WriteLine(); +Console.WriteLine($"Segments:"); +foreach (TranscriptionSegment segment in response.Value2.Segments ?? []) +{ + Console.WriteLine($" {segment.Text,90} : {segment.Start * 1000,5:0} - {segment.End * 1000,5:0}"); +} +``` \ No newline at end of file diff --git a/docs/samples/Chat.ChatWithVision.md b/docs/samples/Chat.ChatWithVision.md new file mode 100644 index 00000000..59540d76 --- /dev/null +++ b/docs/samples/Chat.ChatWithVision.md @@ -0,0 +1,13 @@ +```csharp +using var api = GetAuthenticatedClient(); + +CreateChatCompletionResponse response = await api.Chat.CreateChatCompletionAsync( + messages: [ + "Please describe the following image.", + H.Resources.images_dog_and_cat_png.AsBytes().AsUserMessage(mimeType: "image/png"), + ], + model: CreateChatCompletionRequestModel.Gpt4o); + +Console.WriteLine("[ASSISTANT]:"); +Console.WriteLine($"{response.Choices[0].Message.Content}"); +``` \ No newline at end of file diff --git a/docs/samples/Chat.SimpleChat.md b/docs/samples/Chat.SimpleChat.md new file mode 100644 index 00000000..c5090614 --- /dev/null +++ b/docs/samples/Chat.SimpleChat.md @@ -0,0 +1,10 @@ +```csharp +using var api = GetAuthenticatedClient(); + +CreateChatCompletionResponse response = await api.Chat.CreateChatCompletionAsync( + messages: ["Say 'this is a test.'"], + model: CreateChatCompletionRequestModel.Gpt4o); + +Console.WriteLine("[ASSISTANT]:"); +Console.WriteLine($"{response.Choices[0].Message.Content}"); +``` \ No newline at end of file diff --git a/docs/samples/Chat.SimpleChatStreaming.md b/docs/samples/Chat.SimpleChatStreaming.md new file mode 100644 index 00000000..60f9249f --- /dev/null +++ b/docs/samples/Chat.SimpleChatStreaming.md @@ -0,0 +1,13 @@ +```csharp +using var api = GetAuthenticatedClient(); + +IAsyncEnumerable enumerable = api.Chat.CreateChatCompletionAsStreamAsync( + messages: ["Say 'this is a test.'"], + model: CreateChatCompletionRequestModel.Gpt4o); + +Console.WriteLine("[ASSISTANT]:"); +await foreach (CreateChatCompletionStreamResponse chatUpdate in enumerable) +{ + Console.Write(chatUpdate.Choices[0].Delta.Content); +} +``` \ No newline at end of file diff --git a/docs/samples/Combination.AlpacaArtAssessor.md b/docs/samples/Combination.AlpacaArtAssessor.md new file mode 100644 index 00000000..8e2e96f6 --- /dev/null +++ b/docs/samples/Combination.AlpacaArtAssessor.md @@ -0,0 +1,40 @@ +```csharp +using var api = GetAuthenticatedClient(); + +// First, we create an image using dall-e-3: +ImagesResponse imageResult = await api.Images.CreateImageAsync( + prompt: "a majestic alpaca on a mountain ridge, backed by an expansive blue sky accented with sparse clouds", + model: CreateImageRequestModel.DallE3, + style: CreateImageRequestStyle.Vivid, + quality: CreateImageRequestQuality.Hd, + size: CreateImageRequestSize.x1792x1024); +Image imageGeneration = imageResult.Data[0]; +Console.WriteLine($"Majestic alpaca available at:\n{imageGeneration.Url}"); + +// Now, we'll ask a cranky art critic to evaluate the image using gpt vision: +CreateChatCompletionResponse chatCompletion = await api.Chat.CreateChatCompletionAsync( + messages: [ + ("Assume the role of a cranky art critic. When asked to describe or " + + "evaluate imagery, focus on criticizing elements of subject, composition, and other details.").AsSystemMessage(), + "describe the following image in a few sentences", + new Uri(imageGeneration.Url ?? string.Empty), + ], + model: ChatClient.LatestSmartModel, + maxTokens: 2048); + +string chatResponseText = chatCompletion.Choices[0].Message.Content ?? string.Empty; +Console.WriteLine($"Art critique of majestic alpaca:\n{chatResponseText}"); + +// Finally, we'll get some text-to-speech for that critical evaluation using tts-1-hd: +byte[] ttsResult = await api.Audio.CreateSpeechAsync( + input: chatResponseText, + model: CreateSpeechRequestModel.Tts1Hd, + voice: CreateSpeechRequestVoice.Fable, + speed: 0.9f, + responseFormat: CreateSpeechRequestResponseFormat.Opus); +FileInfo ttsFileInfo = new($"{chatCompletion.Id}.opus"); + +await File.WriteAllBytesAsync(ttsFileInfo.FullName, ttsResult); + +Console.WriteLine($"Alpaca evaluation audio available at:\n{new Uri(ttsFileInfo.FullName).AbsoluteUri}"); +``` \ No newline at end of file diff --git a/docs/samples/Combination.CuriousCreatureCreator.md b/docs/samples/Combination.CuriousCreatureCreator.md new file mode 100644 index 00000000..9d9b0f8a --- /dev/null +++ b/docs/samples/Combination.CuriousCreatureCreator.md @@ -0,0 +1,62 @@ +```csharp +using var api = GetAuthenticatedClient(); + +// First, we'll use gpt-4o to have a creative helper imagine a twist on a household pet +CreateChatCompletionResponse creativeWriterResult = await api.Chat.CreateChatCompletionAsync( + model: CreateChatCompletionRequestModel.Gpt4o, + messages: + [ + "You're a creative helper that specializes in brainstorming designs for concepts that fuse ordinary, mundane items with a fantastical touch. In particular, you can provide good one-paragraph descriptions of concept images.".AsSystemMessage(), + "Imagine a household pet. Now add in a subtle touch of magic or 'different'. What do you imagine? Provide a one-paragraph description of a picture of this new creature, focusing on the details of the imagery such that it'd be suitable for creating a picture.", + ], + maxTokens: 2048); +string description = creativeWriterResult.Choices[0].Message.Content ?? string.Empty; +Console.WriteLine($"Creative helper's creature description:\n{description}"); + +// Asynchronously, in parallel to the next steps, we'll get the creative description in the voice of Onyx +byte[] imageDescriptionAudio = await api.Audio.CreateSpeechAsync( + model: CreateSpeechRequestModel.Tts1Hd, + input: description, + voice: CreateSpeechRequestVoice.Onyx, + speed: 1.1f, + responseFormat: CreateSpeechRequestResponseFormat.Opus); +FileInfo audioFileInfo = new FileInfo($"{creativeWriterResult.Id}-description.opus"); + +await File.WriteAllBytesAsync(audioFileInfo.FullName, imageDescriptionAudio); + +Console.WriteLine($"Spoken description available at:\n{new Uri(audioFileInfo.FullName).AbsoluteUri}"); + +// Meanwhile, we'll use dall-e-3 to generate a rendition of our LLM artist's vision +ImagesResponse imageGenerationResult = await api.Images.CreateImageAsync( + prompt: description, + model: CreateImageRequestModel.DallE3, + size: CreateImageRequestSize.x1792x1024, + quality: CreateImageRequestQuality.Hd); +Uri imageLocation = new Uri(imageGenerationResult.Data[0].Url ?? string.Empty); +Console.WriteLine($"Creature image available at:\n{imageLocation.AbsoluteUri}"); + +// Now, we'll use gpt vision to get a hopelessly taken assessment from a usually exigent art connoisseur +CreateChatCompletionResponse criticalAppraisalResult = await api.Chat.CreateChatCompletionAsync( + messages: [ + "Assume the role of an art critic. Although usually cranky and occasionally even referred to as a 'curmudgeon', you're somehow entirely smitten with the subject presented to you and, despite your best efforts, can't help but lavish praise when you're asked to appraise a provided image.".AsSystemMessage(), + "Evaluate this image for me. What is it, and what do you think of it?", + imageLocation, + ], + model: ChatClient.LatestSmartModel, + maxTokens: 2048); +string appraisal = criticalAppraisalResult.Choices[0].Message.Content ?? string.Empty; +Console.WriteLine($"Critic's appraisal:\n{appraisal}"); + +// Finally, we'll get that art expert's laudations in the voice of Fable +byte[] appraisalAudioResult = await api.Audio.CreateSpeechAsync( + input: appraisal, + model: CreateSpeechRequestModel.Tts1Hd, + voice: CreateSpeechRequestVoice.Fable, + speed: 0.9f, + responseFormat: CreateSpeechRequestResponseFormat.Opus); +FileInfo criticAudioFileInfo = new($"{criticalAppraisalResult.Id}-appraisal.opus"); + +await File.WriteAllBytesAsync(criticAudioFileInfo.FullName, appraisalAudioResult); + +Console.WriteLine($"Critical appraisal available at:\n{new Uri(criticAudioFileInfo.FullName).AbsoluteUri}"); +``` \ No newline at end of file diff --git a/docs/samples/Embeddings.EmbeddingWithOptions.md b/docs/samples/Embeddings.EmbeddingWithOptions.md new file mode 100644 index 00000000..5ed1872b --- /dev/null +++ b/docs/samples/Embeddings.EmbeddingWithOptions.md @@ -0,0 +1,18 @@ +```csharp +using var api = GetAuthenticatedClient(); + +CreateEmbeddingResponse embedding = await api.Embeddings.CreateEmbeddingAsync( + input: "Best hotel in town if you like luxury hotels. They have an amazing infinity pool, a spa," + + " and a really helpful concierge. The location is perfect -- right downtown, close to all the tourist" + + " attractions. We highly recommend this hotel.", + model: CreateEmbeddingRequestModel.TextEmbedding3Small, + dimensions: 512); +IList vector = embedding.Data[0].Embedding1; + +Console.WriteLine($"Dimension: {vector.Count}"); +Console.WriteLine("Floats: "); +for (int i = 0; i < vector.Count; i++) +{ + Console.WriteLine($" [{i,3}] = {vector[i]}"); +} +``` \ No newline at end of file diff --git a/docs/samples/Embeddings.MultipleEmbeddings.md b/docs/samples/Embeddings.MultipleEmbeddings.md new file mode 100644 index 00000000..39252378 --- /dev/null +++ b/docs/samples/Embeddings.MultipleEmbeddings.md @@ -0,0 +1,27 @@ +```csharp +using var api = GetAuthenticatedClient(); + +string category = "Luxury"; +string description = "Best hotel in town if you like luxury hotels. They have an amazing infinity pool, a spa," + + " and a really helpful concierge. The location is perfect -- right downtown, close to all the tourist" + + " attractions. We highly recommend this hotel."; +List inputs = [category, description]; + +CreateEmbeddingResponse response = await api.Embeddings.CreateEmbeddingAsync( + input: inputs, + model: CreateEmbeddingRequestModel.TextEmbedding3Small); + +foreach (Embedding embedding in response.Data) +{ + IList vector = embedding.Embedding1; + + Console.WriteLine($"Dimension: {vector.Count}"); + Console.WriteLine("Floats: "); + for (int i = 0; i < vector.Count; i++) + { + Console.WriteLine($" [{i,4}] = {vector[i]}"); + } + + Console.WriteLine(); +} +``` \ No newline at end of file diff --git a/docs/samples/Embeddings.SimpleEmbedding.md b/docs/samples/Embeddings.SimpleEmbedding.md new file mode 100644 index 00000000..a4c96296 --- /dev/null +++ b/docs/samples/Embeddings.SimpleEmbedding.md @@ -0,0 +1,17 @@ +```csharp +using var api = GetAuthenticatedClient(); + +CreateEmbeddingResponse embedding = await api.Embeddings.CreateEmbeddingAsync( + input: "Best hotel in town if you like luxury hotels. They have an amazing infinity pool, a spa," + + " and a really helpful concierge. The location is perfect -- right downtown, close to all the tourist" + + " attractions. We highly recommend this hotel.", + model: CreateEmbeddingRequestModel.TextEmbedding3Small); +IList vector = embedding.Data[0].Embedding1; + +Console.WriteLine($"Dimension: {vector.Count}"); +Console.WriteLine("Floats: "); +for (int i = 0; i < vector.Count; i++) +{ + Console.WriteLine($" [{i,4}] = {vector[i]}"); +} +``` \ No newline at end of file diff --git a/docs/samples/Images.SimpleImageEdit.md b/docs/samples/Images.SimpleImageEdit.md new file mode 100644 index 00000000..a7fe075e --- /dev/null +++ b/docs/samples/Images.SimpleImageEdit.md @@ -0,0 +1,32 @@ +```csharp +using var api = GetAuthenticatedClient(); + +FileInfo originalFileInfo = new($"{Guid.NewGuid()}.png"); + +await File.WriteAllBytesAsync(originalFileInfo.FullName, H.Resources.images_flower_vase_png.AsBytes()); + +Console.WriteLine($"Original Image available at:\n{new Uri(originalFileInfo.FullName).AbsoluteUri}"); + +FileInfo maskFileInfo = new($"{Guid.NewGuid()}.png"); + +await File.WriteAllBytesAsync(maskFileInfo.FullName, H.Resources.images_flower_vase_mask_png.AsBytes()); + +Console.WriteLine($"Mask available at:\n{new Uri(maskFileInfo.FullName).AbsoluteUri}"); + +ImagesResponse image = await api.Images.CreateImageEditAsync( + image: H.Resources.images_flower_vase_png.AsBytes(), + imagename: H.Resources.images_flower_vase_png.FileName, + prompt: "A vase full of beautiful flowers.", + mask: H.Resources.images_flower_vase_mask_png.AsBytes(), + maskname: H.Resources.images_flower_vase_mask_png.FileName, + model: CreateImageEditRequestModel.DallE2, + size: CreateImageEditRequestSize.x512x512, + responseFormat: CreateImageEditRequestResponseFormat.B64Json); +byte[] bytes = image.Data[0].Bytes; + +FileInfo fileInfo = new($"{Guid.NewGuid()}.png"); + +await File.WriteAllBytesAsync(fileInfo.FullName, bytes); + +Console.WriteLine($"Image available at:\n{new Uri(fileInfo.FullName).AbsoluteUri}"); +``` \ No newline at end of file diff --git a/docs/samples/Images.SimpleImageGeneration.md b/docs/samples/Images.SimpleImageGeneration.md new file mode 100644 index 00000000..d5d9b485 --- /dev/null +++ b/docs/samples/Images.SimpleImageGeneration.md @@ -0,0 +1,24 @@ +```csharp +using var api = GetAuthenticatedClient(); + +ImagesResponse image = await api.Images.CreateImageAsync( + prompt: "The concept for a living room that blends Scandinavian simplicity with Japanese minimalism for" + + " a serene and cozy atmosphere. It's a space that invites relaxation and mindfulness, with natural light" + + " and fresh air. Using neutral tones, including colors like white, beige, gray, and black, that create a" + + " sense of harmony. Featuring sleek wood furniture with clean lines and subtle curves to add warmth and" + + " elegance. Plants and flowers in ceramic pots adding color and life to a space. They can serve as focal" + + " points, creating a connection with nature. Soft textiles and cushions in organic fabrics adding comfort" + + " and softness to a space. They can serve as accents, adding contrast and texture.", + model: CreateImageRequestModel.DallE3, + quality: CreateImageRequestQuality.Hd, + size: CreateImageRequestSize.x1792x1024, + style: CreateImageRequestStyle.Vivid, + responseFormat: CreateImageRequestResponseFormat.B64Json); +byte[] bytes = image.Data[0].Bytes; + +FileInfo fileInfo = new($"{Guid.NewGuid()}.png"); + +await File.WriteAllBytesAsync(fileInfo.FullName, bytes); + +Console.WriteLine($"Image available at:\n{new Uri(fileInfo.FullName).AbsoluteUri}"); +``` \ No newline at end of file diff --git a/docs/samples/Images.SimpleImageVariation.md b/docs/samples/Images.SimpleImageVariation.md new file mode 100644 index 00000000..d0af15dc --- /dev/null +++ b/docs/samples/Images.SimpleImageVariation.md @@ -0,0 +1,23 @@ +```csharp +using var api = GetAuthenticatedClient(); + +FileInfo originalFileInfo = new($"{Guid.NewGuid()}.png"); + +await File.WriteAllBytesAsync(originalFileInfo.FullName, H.Resources.images_dog_and_cat_png.AsBytes()); + +Console.WriteLine($"Original Image available at:\n{new Uri(originalFileInfo.FullName).AbsoluteUri}"); + +ImagesResponse image = await api.Images.CreateImageVariationAsync( + image: H.Resources.images_dog_and_cat_png.AsBytes(), + imagename: H.Resources.images_dog_and_cat_png.FileName, + model: CreateImageVariationRequestModel.DallE2, + size: CreateImageVariationRequestSize.x256x256, + responseFormat: CreateImageVariationRequestResponseFormat.B64Json); +byte[] bytes = image.Data[0].Bytes; + +FileInfo fileInfo = new($"{Guid.NewGuid()}.png"); + +await File.WriteAllBytesAsync(fileInfo.FullName, bytes); + +Console.WriteLine($"Image available at:\n{new Uri(fileInfo.FullName).AbsoluteUri}"); +``` \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 3797080d..14730a47 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,7 +1,34 @@ site_name: OpenAI .NET Documentation nav: - Overview: index.md -# EXAMPLES # +# START EXAMPLES # +- Examples: + - Chat: + - SimpleChat: samples/Chat.SimpleChat.md + - SimpleChatStreaming: samples/Chat.SimpleChatStreaming.md + - ChatWithVision: samples/Chat.ChatWithVision.md + - Assistants: + - AssistantsWithVision: samples/Assistants.AssistantsWithVision.md + - ListFiles: samples/Assistants.ListFiles.md + - ListAssistantsWithPagination: samples/Assistants.ListAssistantsWithPagination.md + - RetrievalAugmentedGeneration: samples/Assistants.RetrievalAugmentedGeneration.md + - Images: + - SimpleImageEdit: samples/Images.SimpleImageEdit.md + - SimpleImageVariation: samples/Images.SimpleImageVariation.md + - SimpleImageGeneration: samples/Images.SimpleImageGeneration.md + - Embeddings: + - MultipleEmbeddings: samples/Embeddings.MultipleEmbeddings.md + - SimpleEmbedding: samples/Embeddings.SimpleEmbedding.md + - EmbeddingWithOptions: samples/Embeddings.EmbeddingWithOptions.md + - Audio: + - SimpleTranscription: samples/Audio.SimpleTranscription.md + - SimpleTranslation: samples/Audio.SimpleTranslation.md + - VerboseTranscription: samples/Audio.VerboseTranscription.md + - SimpleTextToSpeech: samples/Audio.SimpleTextToSpeech.md + - Combination: + - CuriousCreatureCreator: samples/Combination.CuriousCreatureCreator.md + - AlpacaArtAssessor: samples/Combination.AlpacaArtAssessor.md +# END EXAMPLES # # - Quick Start: QuickStart.md # - Architecture: Architecture.md diff --git a/src/helpers/GenerateDocs/Program.cs b/src/helpers/GenerateDocs/Program.cs index 463c67c6..cc727541 100644 --- a/src/helpers/GenerateDocs/Program.cs +++ b/src/helpers/GenerateDocs/Program.cs @@ -7,7 +7,8 @@ File.Copy( Path.Combine(solutionDirectory, "README.md"), - Path.Combine(solutionDirectory, "docs", "index.md")); + Path.Combine(solutionDirectory, "docs", "index.md"), + overwrite: true); Console.WriteLine($"Generating samples from {sampleDirectory}..."); foreach (var path in Directory.EnumerateFiles(sampleDirectory, "Examples.*.cs", SearchOption.AllDirectories)) @@ -28,10 +29,17 @@ await File.WriteAllTextAsync(newPath, $@"```csharp } var mkDocs = await File.ReadAllTextAsync(mkDocsPath); -var newMkDocs = mkDocs.Replace( - "# EXAMPLES #", - $"- Examples:{string.Concat(Directory.EnumerateFiles(Path.Combine(solutionDirectory, "docs", "samples"), "*.md") - .Select(x => $@" - - {Path.GetFileNameWithoutExtension(x)}: samples/{Path.GetFileNameWithoutExtension(x)}.md"))}"); +var startIndex = mkDocs.IndexOf("# START EXAMPLES #", StringComparison.Ordinal) + "# START EXAMPLES #".Length; +var endIndex = mkDocs.IndexOf("# END EXAMPLES #", StringComparison.Ordinal); +mkDocs = mkDocs.Remove(startIndex, endIndex - startIndex); +var newMkDocs = mkDocs.Insert( + startIndex, + $@" +- Examples:{string.Concat(Directory.EnumerateFiles(Path.Combine(solutionDirectory, "docs", "samples"), "*.md") + .GroupBy(x => Path.GetFileNameWithoutExtension(Path.GetFileNameWithoutExtension(x))) + .Select(x => $@" + - {x.Key}:{string.Concat(x.Select(y => $@" + - {Path.GetExtension(Path.GetFileNameWithoutExtension(y)).TrimStart('.')}: samples/{Path.GetFileName(y)}"))}"))} +"); await File.WriteAllTextAsync(mkDocsPath, newMkDocs); diff --git a/src/libs/OpenAI/Conversions/CreateMessageRequest.cs b/src/libs/OpenAI/Conversions/CreateMessageRequest.cs new file mode 100644 index 00000000..aa9713d2 --- /dev/null +++ b/src/libs/OpenAI/Conversions/CreateMessageRequest.cs @@ -0,0 +1,63 @@ +namespace OpenAI; + +public partial class CreateMessageRequest +{ + /// + /// + /// + /// + /// + public static implicit operator CreateMessageRequest(OpenAIFile file) + { + return FromOpenAIFile(file); + } + + /// + /// + /// + /// + /// + public static implicit operator CreateMessageRequest(string content) + { + return FromString(content); + } + + /// + /// + /// + /// + /// + /// + /// + public static CreateMessageRequest FromOpenAIFile( + OpenAIFile file, + CreateMessageRequestRole role = CreateMessageRequestRole.User, + MessageContentImageFileObjectImageFileDetail? detail = null) + { + file = file ?? throw new ArgumentNullException(nameof(file)); + + return new CreateMessageRequest + { + Role = CreateMessageRequestRole.User, + Content = new List> + { + MessageContentImageFileObject.FromOpenAIFile(file), + }, + }; + } + + /// + /// + /// + /// + /// + public static CreateMessageRequest FromString(string content) + { + return new CreateMessageRequest + { + Role = CreateMessageRequestRole.User, + Content = content, + }; + } +} \ No newline at end of file diff --git a/src/libs/OpenAI/Conversions/MessageContentImageFileObject.cs b/src/libs/OpenAI/Conversions/MessageContentImageFileObject.cs new file mode 100644 index 00000000..0f126699 --- /dev/null +++ b/src/libs/OpenAI/Conversions/MessageContentImageFileObject.cs @@ -0,0 +1,53 @@ +namespace OpenAI; + +public partial class MessageContentImageFileObject +{ + /// + /// + /// + /// + /// + public static implicit operator MessageContentImageFileObject(OpenAIFile file) + { + return FromOpenAIFile(file); + } + + /// + /// + /// + /// + /// + /// + public static MessageContentImageFileObject FromOpenAIFile( + OpenAIFile file, + MessageContentImageFileObjectImageFileDetail? detail = null) + { + file = file ?? throw new ArgumentNullException(nameof(file)); + + return new MessageContentImageFileObject + { + ImageFile = new MessageContentImageFileObjectImageFile + { + FileId = file.Id, + Detail = detail, + }, + }; + } + + /// + /// + /// + /// + /// + public static ChatCompletionRequestMessage ToCreateMessageRequest(Uri uri) + { + return new ChatCompletionRequestUserMessage + { + Role = ChatCompletionRequestUserMessageRole.User, + Content = new ChatCompletionRequestUserMessageContentPart[] + { + uri, + }, + }; + } +} \ No newline at end of file diff --git a/src/tests/OpenAI.IntegrationTests/Examples/Examples.Assistants.AssistantsWithVision.cs b/src/tests/OpenAI.IntegrationTests/Examples/Examples.Assistants.AssistantsWithVision.cs new file mode 100644 index 00000000..5c3a0482 --- /dev/null +++ b/src/tests/OpenAI.IntegrationTests/Examples/Examples.Assistants.AssistantsWithVision.cs @@ -0,0 +1,87 @@ +namespace OpenAI.IntegrationTests.Examples; + +public partial class Examples +{ + [Test] + [Explicit] + public async Task AssistantsWithVision() + { + using var api = GetAuthenticatedClient(); + + ImagesResponse appleImage = await api.Images.CreateImageAsync( + prompt: "picture of apple", + responseFormat: CreateImageRequestResponseFormat.B64Json); + byte[] appleBytes = appleImage.Data[0].Bytes; + + FileInfo appleFileInfo = new($"{Guid.NewGuid()}.png"); + + await File.WriteAllBytesAsync(appleFileInfo.FullName, appleBytes); + + Console.WriteLine($"Apple image available at:\n{new Uri(appleFileInfo.FullName).AbsoluteUri}"); + + ImagesResponse orangeImage = await api.Images.CreateImageAsync( + prompt: "picture of orange", + responseFormat: CreateImageRequestResponseFormat.B64Json); + byte[] orangeBytes = orangeImage.Data[0].Bytes; + + FileInfo orangeFileInfo = new($"{Guid.NewGuid()}.png"); + + await File.WriteAllBytesAsync(orangeFileInfo.FullName, orangeBytes); + + Console.WriteLine($"Orange image available at:\n{new Uri(orangeFileInfo.FullName).AbsoluteUri}"); + + OpenAIFile pictureOfAppleFile = await api.Files.CreateFileAsync( + file: appleBytes, + filename: appleFileInfo.Name, + purpose: CreateFileRequestPurpose.Vision); + OpenAIFile pictureOfOrangeFile = await api.Files.CreateFileAsync( + file: orangeBytes, + filename: orangeFileInfo.Name, + purpose: CreateFileRequestPurpose.Vision); + + AssistantObject assistant = await api.Assistants.CreateAssistantAsync( + model: CreateAssistantRequestModel.Gpt4o, + instructions: "When asked a question, attempt to answer very concisely. " + + "Prefer one-sentence answers whenever feasible."); + + ThreadObject thread = await api.Assistants.CreateThreadAsync(new CreateThreadRequest + { + Messages = [ + "Hello, assistant! Please compare these two images for me:", + pictureOfAppleFile, + pictureOfOrangeFile, + ] + }); + + // AsyncResultCollection streamingUpdates = api.Assistants.CreateRunStreamingAsync( + // thread, + // assistant, + // new RunCreationOptions() + // { + // AdditionalInstructions = "When possible, try to sneak in puns if you're asked to compare things.", + // }); + // + // await foreach (StreamingUpdate streamingUpdate in streamingUpdates) + // { + // if (streamingUpdate.UpdateKind == StreamingUpdateReason.RunCreated) + // { + // Console.WriteLine($"--- Run started! ---"); + // } + // if (streamingUpdate is MessageContentUpdate contentUpdate) + // { + // Console.Write(contentUpdate.Text); + // } + // } + + RunObject response = await api.Assistants.CreateRunAsync( + threadId: thread.Id, + assistantId: assistant.Id, + instructions: "When possible, try to sneak in puns if you're asked to compare things."); + + Console.WriteLine(response[0].Content); + + _ = await api.Files.DeleteFileAsync(pictureOfAppleFile.Id); + _ = await api.Assistants.DeleteThreadAsync(thread.Id); + _ = await api.Assistants.DeleteAssistantAsync(assistant.Id); + } +} diff --git a/src/tests/OpenAI.IntegrationTests/Extended/Assistants/Example05_AssistantsWithVisionAsync.cs b/src/tests/OpenAI.IntegrationTests/Extended/Assistants/Example05_AssistantsWithVisionAsync.cs deleted file mode 100644 index ba4b200b..00000000 --- a/src/tests/OpenAI.IntegrationTests/Extended/Assistants/Example05_AssistantsWithVisionAsync.cs +++ /dev/null @@ -1,71 +0,0 @@ -using NUnit.Framework; -using OpenAI.Assistants; -using OpenAI.Files; -using System; -using System.ClientModel; -using System.Threading.Tasks; - -namespace OpenAI.Examples; - -public partial class AssistantExamples -{ - [Test] - public async Task Example05_AssistantsWithVisionAsync() - { - // Assistants is a beta API and subject to change; acknowledge its experimental status by suppressing the matching warning. -#pragma warning disable OPENAI001 - OpenAIClient openAIClient = new(Environment.GetEnvironmentVariable("OPENAI_API_KEY")); - FileClient fileClient = openAIClient.GetFileClient(); - AssistantClient assistantClient = openAIClient.GetAssistantClient(); - - OpenAIFileInfo pictureOfAppleFile = await fileClient.UploadFileAsync( - "picture-of-apple.jpg", - FileUploadPurpose.Vision); - Uri linkToPictureOfOrange = new("https://platform.openai.com/fictitious-files/picture-of-orange.png"); - - Assistant assistant = await assistantClient.CreateAssistantAsync( - "gpt-4o", - new AssistantCreationOptions() - { - Instructions = "When asked a question, attempt to answer very concisely. " - + "Prefer one-sentence answers whenever feasible." - }); - - AssistantThread thread = await assistantClient.CreateThreadAsync(new ThreadCreationOptions() - { - InitialMessages = - { - new ThreadInitializationMessage( - [ - "Hello, assistant! Please compare these two images for me:", - MessageContent.FromImageFileId(pictureOfAppleFile.Id), - MessageContent.FromImageUrl(linkToPictureOfOrange), - ]), - } - }); - - AsyncResultCollection streamingUpdates = assistantClient.CreateRunStreamingAsync( - thread, - assistant, - new RunCreationOptions() - { - AdditionalInstructions = "When possible, try to sneak in puns if you're asked to compare things.", - }); - - await foreach (StreamingUpdate streamingUpdate in streamingUpdates) - { - if (streamingUpdate.UpdateKind == StreamingUpdateReason.RunCreated) - { - Console.WriteLine($"--- Run started! ---"); - } - if (streamingUpdate is MessageContentUpdate contentUpdate) - { - Console.Write(contentUpdate.Text); - } - } - - _ = await fileClient.DeleteFileAsync(pictureOfAppleFile); - _ = await assistantClient.DeleteThreadAsync(thread); - _ = await assistantClient.DeleteAssistantAsync(assistant); - } -}