diff --git a/sdk/provisioning/Azure.Provisioning.CloudMachine/api/Azure.Provisioning.CloudMachine.netstandard2.0.cs b/sdk/provisioning/Azure.Provisioning.CloudMachine/api/Azure.Provisioning.CloudMachine.netstandard2.0.cs index 51c7397d135d..d96673634daa 100644 --- a/sdk/provisioning/Azure.Provisioning.CloudMachine/api/Azure.Provisioning.CloudMachine.netstandard2.0.cs +++ b/sdk/provisioning/Azure.Provisioning.CloudMachine/api/Azure.Provisioning.CloudMachine.netstandard2.0.cs @@ -1,3 +1,9 @@ +public partial class AiModel +{ + public AiModel(string model, string modelVersion) { } + public string Model { get { throw null; } } + public string ModelVersion { get { throw null; } } +} namespace Azure.CloudMachine { public partial class CloudMachineClient : Azure.CloudMachine.CloudMachineWorkspace @@ -51,10 +57,11 @@ public readonly partial struct StorageServices private readonly int _dummyPrimitive; public void DeleteBlob(string path) { } public System.BinaryData DownloadBlob(string path) { throw null; } - public string UploadBytes(System.BinaryData bytes, string? name = null) { throw null; } - public string UploadBytes(byte[] bytes, string? name = null) { throw null; } - public string UploadBytes(System.ReadOnlyMemory bytes, string? name = null) { throw null; } - public string UploadJson(object json, string? name = null) { throw null; } + public string UploadBinaryData(System.BinaryData data, string? name = null, bool overwrite = false) { throw null; } + public string UploadBytes(byte[] bytes, string? name = null, bool overwrite = false) { throw null; } + public string UploadBytes(System.ReadOnlyMemory bytes, string? name = null, bool overwrite = false) { throw null; } + public string UploadJson(object json, string? name = null, bool overwrite = false) { throw null; } + public string UploadStream(System.IO.Stream fileStream, string? name = null, bool overwrite = false) { throw null; } public void WhenBlobUploaded(System.Action function) { } } } @@ -131,12 +138,11 @@ namespace Azure.Provisioning.CloudMachine.OpenAI public static partial class AzureOpenAIExtensions { public static OpenAI.Chat.ChatClient GetOpenAIChatClient(this Azure.Core.ClientWorkspace workspace) { throw null; } + public static OpenAI.Embeddings.EmbeddingClient GetOpenAIEmbeddingsClient(this Azure.Core.ClientWorkspace workspace) { throw null; } } public partial class OpenAIFeature : Azure.Provisioning.CloudMachine.CloudMachineFeature { - public OpenAIFeature(string model, string modelVersion) { } - public string Model { get { throw null; } } - public string ModelVersion { get { throw null; } } + public OpenAIFeature(AiModel chatDeployment, AiModel? embeddingsDeployment = null) { } public override void AddTo(Azure.Provisioning.CloudMachine.CloudMachineInfrastructure cloudMachine) { } } } diff --git a/sdk/provisioning/Azure.Provisioning.CloudMachine/src/AzureSdkExtensions/AIModel.cs b/sdk/provisioning/Azure.Provisioning.CloudMachine/src/AzureSdkExtensions/AIModel.cs new file mode 100644 index 000000000000..7a8618e4f876 --- /dev/null +++ b/sdk/provisioning/Azure.Provisioning.CloudMachine/src/AzureSdkExtensions/AIModel.cs @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +public class AiModel +{ + public AiModel(string model, string modelVersion) { Model = model; ModelVersion = modelVersion; } + public string Model { get; } + public string ModelVersion { get; } +} diff --git a/sdk/provisioning/Azure.Provisioning.CloudMachine/src/AzureSdkExtensions/OpenAIFeature.cs b/sdk/provisioning/Azure.Provisioning.CloudMachine/src/AzureSdkExtensions/OpenAIFeature.cs index f80d0eede9b0..5ebf2c79a3a2 100644 --- a/sdk/provisioning/Azure.Provisioning.CloudMachine/src/AzureSdkExtensions/OpenAIFeature.cs +++ b/sdk/provisioning/Azure.Provisioning.CloudMachine/src/AzureSdkExtensions/OpenAIFeature.cs @@ -8,15 +8,24 @@ using Azure.Provisioning.Authorization; using Azure.Provisioning.CognitiveServices; using OpenAI.Chat; +using OpenAI.Embeddings; namespace Azure.Provisioning.CloudMachine.OpenAI; public class OpenAIFeature : CloudMachineFeature { - public string Model { get; } - public string ModelVersion { get; } + private AiModel _chatDeployment; + private AiModel? _embeddingsDeployment; - public OpenAIFeature(string model, string modelVersion) { Model = model; ModelVersion = modelVersion; } + public OpenAIFeature(AiModel chatDeployment, AiModel? embeddingsDeployment = default) + { + if (chatDeployment == null) + { + throw new ArgumentNullException(nameof(chatDeployment)); + } + _chatDeployment = chatDeployment; + _embeddingsDeployment = embeddingsDeployment; + } public override void AddTo(CloudMachineInfrastructure cloudMachine) { @@ -31,6 +40,7 @@ public override void AddTo(CloudMachineInfrastructure cloudMachine) CustomSubDomainName = cloudMachine.Id }, }; + cloudMachine.AddResource(cognitiveServices); cloudMachine.AddResource(cognitiveServices.CreateRoleAssignment( CognitiveServicesBuiltInRole.CognitiveServicesOpenAIContributor, @@ -38,24 +48,44 @@ public override void AddTo(CloudMachineInfrastructure cloudMachine) cloudMachine.PrincipalIdParameter) ); - // TODO: if we every support more than one deployment, they need to be chained using DependsOn. - // The reason is that deployments need to be deployed/created serially. - CognitiveServicesAccountDeployment deployment = new("openai_deployment", "2023-05-01") + CognitiveServicesAccountDeployment chat = new("openai_deployment", "2023-05-01") { Parent = cognitiveServices, Name = cloudMachine.Id, Properties = new CognitiveServicesAccountDeploymentProperties() { - Model = new CognitiveServicesAccountDeploymentModel() { - Name = this.Model, + Model = new CognitiveServicesAccountDeploymentModel() + { + Name = _chatDeployment.Model, Format = "OpenAI", - Version = this.ModelVersion, + Version = _chatDeployment.ModelVersion } }, }; + cloudMachine.AddResource(chat); - cloudMachine.AddResource(cognitiveServices); - cloudMachine.AddResource(deployment); + if (_embeddingsDeployment != null) + { + CognitiveServicesAccountDeployment embeddings = new("openai_deployment", "2023-05-01") + { + Parent = cognitiveServices, + Name = $"{cloudMachine.Id}-embedding", + Properties = new CognitiveServicesAccountDeploymentProperties() + { + Model = new CognitiveServicesAccountDeploymentModel() + { + Name = _embeddingsDeployment.Model, + Format = "OpenAI", + Version = _embeddingsDeployment.ModelVersion + } + }, + }; + + // Ensure that additional deployments, are chained using DependsOn. + // The reason is that deployments need to be deployed/created serially. + embeddings.DependsOn.Add(chat); + cloudMachine.AddResource(embeddings); + } } } @@ -72,6 +102,17 @@ public static ChatClient GetOpenAIChatClient(this ClientWorkspace workspace) return chatClient; } + public static EmbeddingClient GetOpenAIEmbeddingsClient(this ClientWorkspace workspace) + { + EmbeddingClient embeddingsClient = workspace.Subclients.Get(() => + { + AzureOpenAIClient aoiaClient = workspace.Subclients.Get(() => CreateAzureOpenAIClient(workspace)); + return workspace.CreateEmbeddingsClient(aoiaClient); + }); + + return embeddingsClient; + } + private static AzureOpenAIClient CreateAzureOpenAIClient(this ClientWorkspace workspace) { ClientConnectionOptions connection = workspace.GetConnectionOptions(typeof(AzureOpenAIClient)); @@ -81,7 +122,7 @@ private static AzureOpenAIClient CreateAzureOpenAIClient(this ClientWorkspace wo } else { - return new(connection.Endpoint, new ApiKeyCredential(connection.ApiKeyCredential!)); + return new(connection.Endpoint, new ApiKeyCredential(connection.ApiKeyCredential!)); } } @@ -91,4 +132,11 @@ private static ChatClient CreateChatClient(this ClientWorkspace workspace, Azure ChatClient chat = client.GetChatClient(connection.Id); return chat; } + + private static EmbeddingClient CreateEmbeddingsClient(this ClientWorkspace workspace, AzureOpenAIClient client) + { + ClientConnectionOptions connection = workspace.GetConnectionOptions(typeof(EmbeddingClient)); + EmbeddingClient embeddings = client.GetEmbeddingClient(connection.Id); + return embeddings; + } } diff --git a/sdk/provisioning/Azure.Provisioning.CloudMachine/src/OFX/CloudMachineWorkspace.cs b/sdk/provisioning/Azure.Provisioning.CloudMachine/src/OFX/CloudMachineWorkspace.cs index c4505a5f5ca5..52f734326472 100644 --- a/sdk/provisioning/Azure.Provisioning.CloudMachine/src/OFX/CloudMachineWorkspace.cs +++ b/sdk/provisioning/Azure.Provisioning.CloudMachine/src/OFX/CloudMachineWorkspace.cs @@ -64,6 +64,8 @@ public override ClientConnectionOptions GetConnectionOptions(Type clientType, st return new ClientConnectionOptions(new($"https://{this.Id}.openai.azure.com"), Credential); case "OpenAI.Chat.ChatClient": return new ClientConnectionOptions(Id); + case "OpenAI.Embeddings.EmbeddingClient": + return new ClientConnectionOptions($"{Id}-embedding"); default: throw new Exception($"unknown client {clientId}"); } diff --git a/sdk/provisioning/Azure.Provisioning.CloudMachine/src/OFX/StorageServices.cs b/sdk/provisioning/Azure.Provisioning.CloudMachine/src/OFX/StorageServices.cs index 4d82fc4a8581..51c1077e21df 100644 --- a/sdk/provisioning/Azure.Provisioning.CloudMachine/src/OFX/StorageServices.cs +++ b/sdk/provisioning/Azure.Provisioning.CloudMachine/src/OFX/StorageServices.cs @@ -9,6 +9,7 @@ using Azure.Core; using Azure.Storage.Blobs; using Azure.Storage.Blobs.Models; +using Azure.Storage.Blobs.Specialized; namespace Azure.CloudMachine; @@ -42,28 +43,63 @@ private BlobContainerClient GetContainer(string containerName) return container; } - public string UploadJson(object json, string? name = default) + public string UploadJson(object json, string? name = default, bool overwrite = false) { BlobContainerClient container = GetDefaultContainer(); if (name == default) name = $"b{Guid.NewGuid()}"; - container.UploadBlob(name, BinaryData.FromObjectAsJson(json)); + var client = container.GetBlockBlobClient(name); + var options = new BlobUploadOptions + { + Conditions = overwrite ? null : new BlobRequestConditions { IfNoneMatch = new ETag("*") }, + HttpHeaders = new BlobHttpHeaders { ContentType = ContentType.ApplicationJson.ToString() } + }; + + client.Upload(BinaryData.FromObjectAsJson(json).ToStream(), options); + return name; + } + + public string UploadStream(Stream fileStream, string? name = default, bool overwrite = false) + { + BlobContainerClient container = GetDefaultContainer(); + + if (name == default) + name = $"b{Guid.NewGuid()}"; + var client = container.GetBlockBlobClient(name); + var options = new BlobUploadOptions + { + Conditions = overwrite ? null : new BlobRequestConditions { IfNoneMatch = new ETag("*") }, + HttpHeaders = new BlobHttpHeaders { ContentType = ContentType.ApplicationOctetStream.ToString() } + }; + + client.Upload(fileStream, options); return name; } - public string UploadBytes(BinaryData bytes, string? name = default) + + public string UploadBinaryData(BinaryData data, string? name = default, bool overwrite = false) { BlobContainerClient container = GetDefaultContainer(); - if (name == default) name = $"b{Guid.NewGuid()}"; - container.UploadBlob(name, bytes); + if (name == default) + name = $"b{Guid.NewGuid()}"; + + var client = container.GetBlockBlobClient(name); + var options = new BlobUploadOptions + { + Conditions = overwrite ? null : new BlobRequestConditions { IfNoneMatch = new ETag("*") }, + HttpHeaders = new BlobHttpHeaders { ContentType = ContentType.ApplicationOctetStream.ToString() } + }; + + client.Upload(data.ToStream(), options); return name; } - public string UploadBytes(byte[] bytes, string? name = default) - => UploadBytes(BinaryData.FromBytes(bytes), name); - public string UploadBytes(ReadOnlyMemory bytes, string? name = default) - => UploadBytes(BinaryData.FromBytes(bytes), name); + + public string UploadBytes(byte[] bytes, string? name = default, bool overwrite = false) + => UploadBinaryData(BinaryData.FromBytes(bytes), name, overwrite); + public string UploadBytes(ReadOnlyMemory bytes, string? name = default, bool overwrite = false) + => UploadBinaryData(BinaryData.FromBytes(bytes), name, overwrite); public BinaryData DownloadBlob(string path) { diff --git a/sdk/provisioning/Azure.Provisioning.CloudMachine/tests/CloudMachineTests.cs b/sdk/provisioning/Azure.Provisioning.CloudMachine/tests/CloudMachineTests.cs index 299fff234d80..3c2077ecc449 100644 --- a/sdk/provisioning/Azure.Provisioning.CloudMachine/tests/CloudMachineTests.cs +++ b/sdk/provisioning/Azure.Provisioning.CloudMachine/tests/CloudMachineTests.cs @@ -24,12 +24,13 @@ public void Provisioning(string[] args) if (CloudMachineInfrastructure.Configure(args, (cm) => { cm.AddFeature(new KeyVaultFeature()); - cm.AddFeature(new OpenAIFeature("gpt-35-turbo", "0125")); + cm.AddFeature(new OpenAIFeature(new AiModel("gpt-35-turbo", "0125"), new AiModel("text-embedding-ada-002", "2"))); })) return; CloudMachineWorkspace cm = new(); Console.WriteLine(cm.Id); + var embeddings = cm.GetOpenAIEmbeddingsClient(); } [Ignore("no recordings yet")] @@ -71,7 +72,7 @@ public void OpenAI(string[] args) { if (CloudMachineInfrastructure.Configure(args, (cm) => { - cm.AddFeature(new OpenAIFeature("gpt-35-turbo", "0125")); + cm.AddFeature(new OpenAIFeature(new AiModel("gpt-35-turbo", "0125"))); })) return; @@ -137,7 +138,7 @@ public void Demo(string[] args) CloudMachineClient cm = new(); // setup - cm.Messaging.WhenMessageReceived((string message) => cm.Storage.UploadBytes(BinaryData.FromString(message))); + cm.Messaging.WhenMessageReceived((string message) => cm.Storage.UploadBinaryData(BinaryData.FromString(message))); cm.Storage.WhenBlobUploaded((StorageFile file) => { var content = file.Download();