From 0ed355d453937b4cd760034bb857e9c86e23566d Mon Sep 17 00:00:00 2001 From: Mathieu Mack Date: Mon, 2 Sep 2024 12:42:27 +0200 Subject: [PATCH 1/9] Init package with a support of API Keys --- .github/ISSUE_TEMPLATE/bug_report.md | 50 +++++ .github/ISSUE_TEMPLATE/feature_request.md | 10 + .github/stale.yml | 19 ++ .github/workflows/ci.yml | 26 +++ ...udioMaaSPhi3ServiceCollectionExtensions.cs | 66 ++++++ ...reAIStudioMaaSPhi3ChatCompletionService.cs | 204 ++++++++++++++++++ .../ChatWithDataChoice.cs | 10 + .../ChatWithDataMessage.cs | 12 ++ .../ChatWithDataRequest.cs | 36 ++++ .../ChatWithDataResponse.cs | 30 +++ .../ChatWithDataUsage.cs | 15 ++ .../Phi3CompletionUsage.cs | 38 ++++ .../Phi3PromptExecutionSettings.cs | 148 +++++++++++++ .../Diagnostics/Verify.cs | 174 +++++++++++++++ .../RequestFailedExceptionExtensions.cs | 39 ++++ ...ernel.Connectors.AzureAIStudio.Phi3.csproj | 32 +++ src/MDev.Dotnet.SemanticKernel.sln | 30 +++ 17 files changed, 939 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/stale.yml create mode 100644 .github/workflows/ci.yml create mode 100644 src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/AIStudioMaaSPhi3ServiceCollectionExtensions.cs create mode 100644 src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/ChatCompletion/AzureAIStudioMaaSPhi3ChatCompletionService.cs create mode 100644 src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/ChatCompletionWithData/ChatWithDataChoice.cs create mode 100644 src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/ChatCompletionWithData/ChatWithDataMessage.cs create mode 100644 src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/ChatCompletionWithData/ChatWithDataRequest.cs create mode 100644 src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/ChatCompletionWithData/ChatWithDataResponse.cs create mode 100644 src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/ChatCompletionWithData/ChatWithDataUsage.cs create mode 100644 src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/ChatCompletionWithData/Phi3CompletionUsage.cs create mode 100644 src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/ChatCompletionWithData/Phi3PromptExecutionSettings.cs create mode 100644 src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/Diagnostics/Verify.cs create mode 100644 src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/Exceptions/RequestFailedExceptionExtensions.cs create mode 100644 src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3.csproj create mode 100644 src/MDev.Dotnet.SemanticKernel.sln diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..a54dc29 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,50 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: bug +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** + +Steps to reproduce the behavior. +Ideally, describes : +* Json input: +```json +{ + "resultField1": 132.46, + "resultField2": true +} +``` +* Json transformation: +```json +{ + "resultField1": "$.field1->ToInteger()", + "resultField2": "$.field2" +} +``` +* Json result: +```json +{ + "resultField1": "hello", + "resultField2": "" +} +``` +* Json expected result: +```json +{ + "resultField1": 132, + "resultField2": true +} +``` + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..ded7b41 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,10 @@ +--- +name: Feature request +about: Create a feature request +title: '' +labels: +assignees: '' + +--- + +Describe the feature here. diff --git a/.github/stale.yml b/.github/stale.yml new file mode 100644 index 0000000..e1378fd --- /dev/null +++ b/.github/stale.yml @@ -0,0 +1,19 @@ +# Number of days of inactivity before an issue becomes stale +daysUntilStale: 60 +# Number of days of inactivity before a stale issue is closed +daysUntilClose: 7 +# Issues with these labels will never be considered stale +exemptLabels: + - pinned + - security + - enhancement + - bug +# Label to use when marking an issue as stale +staleLabel: wontfix +# Comment to post when marking an issue as stale. Set to `false` to disable +markComment: > + This issue has been automatically marked as stale because it has not had + recent activity. It will be closed if no further activity occurs. Thank you + for your contributions. +# Comment to post when closing a stale issue. Set to `false` to disable +closeComment: false \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..4fdfc7a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,26 @@ +name: .NET + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + workflow_dispatch: + inputs: + publishToNuget: + description: 'Publish to nuget' + required: true + default: true + type: boolean + +jobs: + dotnet: + uses: mathieumack/MyGithubActions/.github/workflows/dotnetlib.yml@main + with: + publishToNuget: false + secrets: + NUGETPACKAGEIDENTIFIER: ${{ secrets.NUGETPACKAGEIDENTIFIER }} + NUGETAPIKEY: ${{ secrets.NUGETAPIKEY }} + SONAR_ORGANIZATION_CODE: ${{ secrets.SONAR_ORGANIZATION_CODE }} + SONAR_PROJECT_CODE: ${{ secrets.SONAR_PROJECT_CODE }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} \ No newline at end of file diff --git a/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/AIStudioMaaSPhi3ServiceCollectionExtensions.cs b/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/AIStudioMaaSPhi3ServiceCollectionExtensions.cs new file mode 100644 index 0000000..9921ca9 --- /dev/null +++ b/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/AIStudioMaaSPhi3ServiceCollectionExtensions.cs @@ -0,0 +1,66 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel; +using MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3.Diagnostics; +using System.Net.Http.Headers; +using Microsoft.Extensions.Logging; + +namespace MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3; + +public static class AIStudioMaaSPhi3ServiceCollectionExtensions +{ + /// + /// Adds the Azure OpenAI chat completion service to the list. + /// + /// The instance to augment. + /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// Azure OpenAI API key, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// A local identifier for the given AI service + /// Model identifier, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// The HttpClient to use with this service. + /// The same instance as . + public static IKernelBuilder AddAzureAIStudioPhi3ChatCompletion( + this IKernelBuilder builder, + string endpoint, + string apiKey, + string? serviceId = null, + string? modelId = null) + { + Verify.NotNull(builder); + Verify.NotNullOrWhiteSpace(endpoint); + Verify.NotNullOrWhiteSpace(apiKey); + + var deploymentIdenfifier = Guid.NewGuid().ToString(); + + var handler = new HttpClientHandler() + { + ClientCertificateOptions = ClientCertificateOption.Manual, + ServerCertificateCustomValidationCallback = + (httpRequestMessage, cert, cetChain, policyErrors) => { return true; } + }; + builder.Services.AddHttpClient($"http-{modelId}-{deploymentIdenfifier}") + .ConfigureHttpClient((services, client) => + { + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey); + client.BaseAddress = new Uri(endpoint); + }) + .ConfigurePrimaryHttpMessageHandler(() => handler); + + Func factory = (serviceProvider, _) => + { + var httpClientFactory = serviceProvider.GetRequiredService(); + var httpClient = httpClientFactory.CreateClient($"http-{modelId}-{deploymentIdenfifier}"); + + var loggerFactory = serviceProvider.GetService(); + var logger = loggerFactory?.CreateLogger(typeof(AzureAIStudioMaaSPhi3ChatCompletionService)); + + var client = new AzureAIStudioMaaSPhi3ChatCompletionService(httpClient, logger); + return client; + }; + + builder.Services.AddKeyedSingleton(serviceId, factory); + + return builder; + } +} diff --git a/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/ChatCompletion/AzureAIStudioMaaSPhi3ChatCompletionService.cs b/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/ChatCompletion/AzureAIStudioMaaSPhi3ChatCompletionService.cs new file mode 100644 index 0000000..84a8f4f --- /dev/null +++ b/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/ChatCompletion/AzureAIStudioMaaSPhi3ChatCompletionService.cs @@ -0,0 +1,204 @@ +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.ChatCompletion; +using System.Text.Json; +using MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3.ChatCompletionWithData; +using Azure; +using MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3.Exceptions; +using Azure.AI.OpenAI; +using System.Diagnostics.Metrics; +using System.Net.Http.Headers; +using System.Net.Http.Json; +using Microsoft.SemanticKernel.Connectors.OpenAI; +using Microsoft.Extensions.Logging.Abstractions; + +namespace MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3; + +internal class AzureAIStudioMaaSPhi3ChatCompletionService : IChatCompletionService +{ + /// + /// Logger instance + /// + internal ILogger Logger { get; set; } + + private readonly HttpClient httpClient; + + internal AzureAIStudioMaaSPhi3ChatCompletionService(HttpClient httpClient, ILogger? logger = null) + { + this.httpClient = httpClient; + this.Logger = logger ?? NullLogger.Instance; + } + + /// + /// Instance of for metrics. + /// + private static readonly Meter s_meter = new("Microsoft.SemanticKernel.Connectors.OpenAI"); + + /// + /// Instance of to keep track of the number of prompt tokens used. + /// + private static readonly Counter s_promptTokensCounter = + s_meter.CreateCounter( + name: "semantic_kernel.connectors.aistudio.phi3.tokens.prompt", + unit: "{token}", + description: "Number of prompt tokens used"); + + /// + /// Instance of to keep track of the number of completion tokens used. + /// + private static readonly Counter s_completionTokensCounter = + s_meter.CreateCounter( + name: "semantic_kernel.connectors.aistudio.phi3.tokens.completion", + unit: "{token}", + description: "Number of completion tokens used"); + + /// + /// Instance of to keep track of the total number of tokens used. + /// + private static readonly Counter s_totalTokensCounter = + s_meter.CreateCounter( + name: "semantic_kernel.connectors.aistudio.phi3.tokens.total", + unit: "{token}", + description: "Number of tokens used"); + + public IReadOnlyDictionary Attributes => throw new NotImplementedException(); + + public async Task> GetChatMessageContentsAsync(ChatHistory chatHistory, PromptExecutionSettings? executionSettings = null, Kernel? kernel = null, CancellationToken cancellationToken = default) + { + if (chatHistory is null) + throw new ArgumentNullException(nameof(chatHistory)); + + // Convert the incoming execution settings to OpenAI settings. + var chatExecutionSettings = Phi3PromptExecutionSettings.FromExecutionSettings(executionSettings); + bool autoInvoke = false; + ValidateMaxTokens(chatExecutionSettings.MaxTokens); + + // Create the Azure SDK ChatCompletionOptions instance from all available information. + var body = new ChatWithDataRequest() + { + MaxTokens = chatExecutionSettings.MaxTokens, + Temperature = chatExecutionSettings.Temperature, + TopP = chatExecutionSettings.TopP, + Messages = chatHistory.Select(e => new ChatWithDataMessage() + { + Role = e.Role.ToString(), + Content = e.Content.ToString() + }).ToList() + }; + + var uri = "v1/chat/completions"; + + var requestBody = JsonSerializer.Serialize(body); + + var content = new StringContent(requestBody); + content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); + + for (int iteration = 1; ; iteration++) + { + // Make the request. + HttpResponseMessage response = await httpClient.PostAsync(uri, content); + + ChatWithDataResponse responseContent = null; + + if (response.IsSuccessStatusCode) + { + responseContent = await response.Content.ReadFromJsonAsync(); + } + //else + //{ + // //Console.WriteLine(string.Format("The request failed with status code: {0}", response.StatusCode)); + + // // Print the headers - they include the requert ID and the timestamp, + // // which are useful for debugging the failure + // //Console.WriteLine(response.Headers.ToString()); + + // responseContent = await response.Content.ReadAsStringAsync(); + // //Console.WriteLine(responseContent); + //} + this.CaptureUsageDetails(responseContent.Usage); + + if (responseContent is null) + { + throw new KernelException("Chat completions not found"); + } + + // If we don't want to attempt to invoke any functions, just return the result. + // Or if we are auto-invoking but we somehow end up with other than 1 choice even though only 1 was requested, similarly bail. + return responseContent.Choices.Select(e => + ToChatMessageContent(responseContent, e)).ToList(); + } + } + + private ChatMessageContent ToChatMessageContent(ChatWithDataResponse chatWithDataResponse, ChatWithDataChoice chatWithDataChoice) + { + var metadatas = new Dictionary(); + + // Add Usage : + metadatas.Add("Usage", new Phi3CompletionUsage(chatWithDataResponse.Usage.CompletionTokens, + chatWithDataResponse.Usage.PromptTokens, + chatWithDataResponse.Usage.TotalTokens)); + + var result = new ChatMessageContent() + { + Content = chatWithDataChoice.Message.Content, + Role = AuthorRole.Assistant, + ModelId = chatWithDataResponse.Model, + Metadata = metadatas + }; + + return result; + } + + private static void ValidateMaxTokens(int? maxTokens) + { + if (maxTokens.HasValue && maxTokens < 1) + { + throw new ArgumentException($"MaxTokens {maxTokens} is not valid, the value must be greater than zero"); + } + } + + /// + /// Captures usage details, including token information. + /// + /// Instance of with usage details. + private void CaptureUsageDetails(ChatWithDataUsage usage) + { + if (usage is null) + { + if (this.Logger.IsEnabled(LogLevel.Debug)) + { + this.Logger.LogDebug("Usage information is not available."); + } + + return; + } + + if (this.Logger.IsEnabled(LogLevel.Information)) + { + this.Logger.LogInformation( + "Prompt tokens: {PromptTokens}. Completion tokens: {CompletionTokens}. Total tokens: {TotalTokens}.", + usage.PromptTokens, usage.CompletionTokens, usage.TotalTokens); + } + + s_promptTokensCounter.Add(usage.PromptTokens); + s_completionTokensCounter.Add(usage.CompletionTokens); + s_totalTokensCounter.Add(usage.TotalTokens); + } + + //private static async Task RunRequestAsync(Func> request) + //{ + // try + // { + // return await request.Invoke().ConfigureAwait(false); + // } + // catch (RequestFailedException e) + // { + // throw e.ToHttpOperationException(); + // } + //} + + public IAsyncEnumerable GetStreamingChatMessageContentsAsync(ChatHistory chatHistory, PromptExecutionSettings? executionSettings = null, Kernel? kernel = null, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } +} diff --git a/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/ChatCompletionWithData/ChatWithDataChoice.cs b/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/ChatCompletionWithData/ChatWithDataChoice.cs new file mode 100644 index 0000000..5df4048 --- /dev/null +++ b/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/ChatCompletionWithData/ChatWithDataChoice.cs @@ -0,0 +1,10 @@ +using System.Text.Json.Serialization; + +namespace MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3.ChatCompletionWithData +{ + internal sealed class ChatWithDataChoice + { + [JsonPropertyName("message")] + public ChatWithDataMessage Message { get; set; } + } +} diff --git a/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/ChatCompletionWithData/ChatWithDataMessage.cs b/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/ChatCompletionWithData/ChatWithDataMessage.cs new file mode 100644 index 0000000..d595a1a --- /dev/null +++ b/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/ChatCompletionWithData/ChatWithDataMessage.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3.ChatCompletionWithData; + +internal sealed class ChatWithDataMessage +{ + [JsonPropertyName("role")] + public string Role { get; set; } = string.Empty; + + [JsonPropertyName("content")] + public string Content { get; set; } = string.Empty; +} diff --git a/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/ChatCompletionWithData/ChatWithDataRequest.cs b/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/ChatCompletionWithData/ChatWithDataRequest.cs new file mode 100644 index 0000000..eddfd48 --- /dev/null +++ b/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/ChatCompletionWithData/ChatWithDataRequest.cs @@ -0,0 +1,36 @@ +using System.Text.Json.Serialization; + +namespace MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3.ChatCompletionWithData; + +internal sealed class ChatWithDataRequest +{ + [JsonPropertyName("temperature")] + public double Temperature { get; set; } = 0; + + [JsonPropertyName("top_p")] + public double TopP { get; set; } = 0; + + //[JsonPropertyName("stream")] + //public bool IsStreamEnabled { get; set; } + + //[JsonPropertyName("stop")] + //public IList? StopSequences { get; set; } = Array.Empty(); + + [JsonPropertyName("max_tokens")] + public int? MaxTokens { get; set; } + + //[JsonPropertyName("presence_penalty")] + //public double PresencePenalty { get; set; } = 0; + + //[JsonPropertyName("frequency_penalty")] + //public double FrequencyPenalty { get; set; } = 0; + + //[JsonPropertyName("logit_bias")] + //public IDictionary TokenSelectionBiases { get; set; } = new Dictionary(); + + //[JsonPropertyName("dataSources")] + //public IList DataSources { get; set; } = Array.Empty(); + + [JsonPropertyName("messages")] + public IList Messages { get; set; } = Array.Empty(); +} diff --git a/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/ChatCompletionWithData/ChatWithDataResponse.cs b/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/ChatCompletionWithData/ChatWithDataResponse.cs new file mode 100644 index 0000000..932266d --- /dev/null +++ b/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/ChatCompletionWithData/ChatWithDataResponse.cs @@ -0,0 +1,30 @@ +using System.Text.Json.Serialization; + +namespace MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3.ChatCompletionWithData; + +internal sealed class ChatWithDataResponse +{ + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; + + [JsonPropertyName("created")] + public int Created { get; set; } = default; + + [JsonPropertyName("choices")] + public IList Choices { get; set; } = Array.Empty(); + + [JsonPropertyName("usage")] + public ChatWithDataUsage Usage { get; set; } + + [JsonPropertyName("model")] + public string Model { get; set; } = string.Empty; + + [JsonPropertyName("object")] + public string Object { get; set; } = string.Empty; + + [JsonConstructor] + public ChatWithDataResponse(ChatWithDataUsage usage) + { + this.Usage = usage; + } +} diff --git a/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/ChatCompletionWithData/ChatWithDataUsage.cs b/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/ChatCompletionWithData/ChatWithDataUsage.cs new file mode 100644 index 0000000..9e6c00f --- /dev/null +++ b/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/ChatCompletionWithData/ChatWithDataUsage.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3.ChatCompletionWithData; + +internal sealed class ChatWithDataUsage +{ + [JsonPropertyName("prompt_tokens")] + public int PromptTokens { get; set; } + + [JsonPropertyName("completion_tokens")] + public int CompletionTokens { get; set; } + + [JsonPropertyName("total_tokens")] + public int TotalTokens { get; set; } +} diff --git a/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/ChatCompletionWithData/Phi3CompletionUsage.cs b/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/ChatCompletionWithData/Phi3CompletionUsage.cs new file mode 100644 index 0000000..5ce1657 --- /dev/null +++ b/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/ChatCompletionWithData/Phi3CompletionUsage.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3.ChatCompletionWithData; + +public class Phi3CompletionUsage +{ + /// + /// The number of tokens generated across all completions emissions. + /// + public int CompletionTokens { get; } + + /// + /// The number of tokens in the provided prompts for the completions request. + /// + public int PromptTokens { get; } + + /// + /// The total number of tokens processed for the completions request and response. + /// + public int TotalTokens { get; } + + /// + /// Initializes a new instance of Azure.AI.OpenAI.CompletionsUsage. + /// + /// The number of tokens generated across all completions emissions. + /// The number of tokens in the provided prompts for the completions request. + /// The total number of tokens processed for the completions request and response. + internal Phi3CompletionUsage(int completionTokens, int promptTokens, int totalTokens) + { + CompletionTokens = completionTokens; + PromptTokens = promptTokens; + TotalTokens = totalTokens; + } +} diff --git a/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/ChatCompletionWithData/Phi3PromptExecutionSettings.cs b/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/ChatCompletionWithData/Phi3PromptExecutionSettings.cs new file mode 100644 index 0000000..ba8eae1 --- /dev/null +++ b/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/ChatCompletionWithData/Phi3PromptExecutionSettings.cs @@ -0,0 +1,148 @@ +using Microsoft.SemanticKernel; +using System.Text.Json.Serialization; +using System.Text.Json; +using Microsoft.SemanticKernel.Connectors.OpenAI; + +namespace MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3.ChatCompletionWithData; + +[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)] +public sealed class Phi3PromptExecutionSettings : PromptExecutionSettings +{ + /// + /// Temperature controls the randomness of the completion. + /// The higher the temperature, the more random the completion. + /// Default is 1.0. + /// + [JsonPropertyName("temperature")] + public double Temperature + { + get => this._temperature; + + set + { + this.ThrowIfFrozen(); + this._temperature = value; + } + } + + /// + /// TopP controls the diversity of the completion. + /// The higher the TopP, the more diverse the completion. + /// Default is 1.0. + /// + [JsonPropertyName("top_p")] + public double TopP + { + get => this._topP; + + set + { + this.ThrowIfFrozen(); + this._topP = value; + } + } + + /// + /// The maximum number of tokens to generate in the completion. + /// + [JsonPropertyName("max_tokens")] + public int? MaxTokens + { + get => this._maxTokens; + + set + { + this.ThrowIfFrozen(); + this._maxTokens = value; + } + } + + ///// + ///// A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse + ///// + //public string? User + //{ + // get => this._user; + + // set + // { + // this.ThrowIfFrozen(); + // this._user = value; + // } + //} + + /// + public override void Freeze() + { + if (this.IsFrozen) + { + return; + } + + base.Freeze(); + } + + /// + public override PromptExecutionSettings Clone() + { + return new OpenAIPromptExecutionSettings() + { + ModelId = this.ModelId, + ExtensionData = this.ExtensionData is not null ? new Dictionary(this.ExtensionData) : null, + Temperature = this.Temperature, + TopP = this.TopP, + MaxTokens = this.MaxTokens, + //User = this.User + }; + } + + /// + /// Default max tokens for a text generation + /// + internal static int DefaultTextMaxTokens { get; } = 256; + + /// + /// Create a new settings object with the values from another settings object. + /// + /// Template configuration + /// Default max tokens + /// An instance of OpenAIPromptExecutionSettings + public static OpenAIPromptExecutionSettings FromExecutionSettings(PromptExecutionSettings? executionSettings, int? defaultMaxTokens = null) + { + if (executionSettings is null) + { + return new OpenAIPromptExecutionSettings() + { + MaxTokens = defaultMaxTokens + }; + } + + if (executionSettings is OpenAIPromptExecutionSettings settings) + { + return settings; + } + + var json = JsonSerializer.Serialize(executionSettings); + + //var jsonOptions = new JsonSerializerOptions() + //{ + + //}; + var openAIExecutionSettings = JsonSerializer.Deserialize(json); + if (openAIExecutionSettings is not null) + { + return openAIExecutionSettings; + } + + throw new ArgumentException($"Invalid execution settings, cannot convert to {nameof(OpenAIPromptExecutionSettings)}", nameof(executionSettings)); + } + + #region private ================================================================================ + + private double _temperature = 1; + private double _topP = 1; + private int? _maxTokens; + //private string? _user; + + #endregion +} diff --git a/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/Diagnostics/Verify.cs b/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/Diagnostics/Verify.cs new file mode 100644 index 0000000..304f6a8 --- /dev/null +++ b/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/Diagnostics/Verify.cs @@ -0,0 +1,174 @@ +using Microsoft.SemanticKernel; +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Text.RegularExpressions; + +namespace MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3.Diagnostics; + +[ExcludeFromCodeCoverage] +internal static class Verify +{ + private static readonly Regex s_asciiLettersDigitsUnderscoresRegex = new("^[0-9A-Za-z_]*$"); + private static readonly Regex s_filenameRegex = new("^[^.]+\\.[^.]+$"); + + /// + /// Equivalent of ArgumentNullException.ThrowIfNull + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void NotNull([NotNull] object? obj, [CallerArgumentExpression("obj")] string? paramName = null) + { + if (obj is null) + { + ThrowArgumentNullException(paramName); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void NotNullOrWhiteSpace([NotNull] string? str, [CallerArgumentExpression("str")] string? paramName = null) + { + NotNull(str, paramName); + if (string.IsNullOrWhiteSpace(str)) + { + ThrowArgumentWhiteSpaceException(paramName); + } + } + + internal static void NotNullOrEmpty(IList list, [CallerArgumentExpression("list")] string? paramName = null) + { + NotNull(list, paramName); + if (list.Count == 0) + { + throw new ArgumentException("The value cannot be empty.", paramName); + } + } + + public static void True(bool condition, string message, [CallerArgumentExpression("condition")] string? paramName = null) + { + if (!condition) + { + throw new ArgumentException(message, paramName); + } + } + + internal static void ValidPluginName([NotNull] string? pluginName, IReadOnlyKernelPluginCollection? plugins = null, [CallerArgumentExpression("pluginName")] string? paramName = null) + { + NotNullOrWhiteSpace(pluginName); + if (!s_asciiLettersDigitsUnderscoresRegex.IsMatch(pluginName)) + { + ThrowArgumentInvalidName("plugin name", pluginName, paramName); + } + + if (plugins is not null && plugins.Contains(pluginName)) + { + throw new ArgumentException($"A plugin with the name '{pluginName}' already exists."); + } + } + + internal static void ValidFunctionName([NotNull] string? functionName, [CallerArgumentExpression("functionName")] string? paramName = null) + { + NotNullOrWhiteSpace(functionName); + if (!s_asciiLettersDigitsUnderscoresRegex.IsMatch(functionName)) + { + ThrowArgumentInvalidName("function name", functionName, paramName); + } + } + + internal static void ValidFilename([NotNull] string? filename, [CallerArgumentExpression("filename")] string? paramName = null) + { + NotNullOrWhiteSpace(filename); + if (!s_filenameRegex.IsMatch(filename)) + { + throw new ArgumentException($"Invalid filename format: '{filename}'. Filename should consist of an actual name and a file extension.", paramName); + } + } + + public static void ValidateUrl(string url, bool allowQuery = false, [CallerArgumentExpression("url")] string? paramName = null) + { + NotNullOrWhiteSpace(url, paramName); + + if (!Uri.TryCreate(url, UriKind.Absolute, out var uri) || string.IsNullOrEmpty(uri.Host)) + { + throw new ArgumentException($"The `{url}` is not valid.", paramName); + } + + if (!allowQuery && !string.IsNullOrEmpty(uri.Query)) + { + throw new ArgumentException($"The `{url}` is not valid: it cannot contain query parameters.", paramName); + } + + if (!string.IsNullOrEmpty(uri.Fragment)) + { + throw new ArgumentException($"The `{url}` is not valid: it cannot contain URL fragments.", paramName); + } + } + + internal static void StartsWith(string text, string prefix, string message, [CallerArgumentExpression("text")] string? textParamName = null) + { + Debug.Assert(prefix is not null); + + NotNullOrWhiteSpace(text, textParamName); + if (!text.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) + { + throw new ArgumentException(textParamName, message); + } + } + + internal static void DirectoryExists(string path) + { + if (!Directory.Exists(path)) + { + throw new DirectoryNotFoundException($"Directory '{path}' could not be found."); + } + } + + /// + /// Make sure every function parameter name is unique + /// + /// List of parameters + internal static void ParametersUniqueness(IReadOnlyList parameters) + { + int count = parameters.Count; + if (count > 0) + { + var seen = new HashSet(StringComparer.OrdinalIgnoreCase); + for (int i = 0; i < count; i++) + { + KernelParameterMetadata p = parameters[i]; + if (string.IsNullOrWhiteSpace(p.Name)) + { + string paramName = $"{nameof(parameters)}[{i}].{p.Name}"; + if (p.Name is null) + { + ThrowArgumentNullException(paramName); + } + else + { + ThrowArgumentWhiteSpaceException(paramName); + } + } + + if (!seen.Add(p.Name)) + { + throw new ArgumentException($"The function has two or more parameters with the same name '{p.Name}'"); + } + } + } + } + + [DoesNotReturn] + private static void ThrowArgumentInvalidName(string kind, string name, string? paramName) => + throw new ArgumentException($"A {kind} can contain only ASCII letters, digits, and underscores: '{name}' is not a valid name.", paramName); + + [DoesNotReturn] + internal static void ThrowArgumentNullException(string? paramName) => + throw new ArgumentNullException(paramName); + + [DoesNotReturn] + internal static void ThrowArgumentWhiteSpaceException(string? paramName) => + throw new ArgumentException("The value cannot be an empty string or composed entirely of whitespace.", paramName); + + [DoesNotReturn] + internal static void ThrowArgumentOutOfRangeException(string? paramName, T actualValue, string message) => + throw new ArgumentOutOfRangeException(paramName, actualValue, message); +} diff --git a/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/Exceptions/RequestFailedExceptionExtensions.cs b/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/Exceptions/RequestFailedExceptionExtensions.cs new file mode 100644 index 0000000..58d7ef8 --- /dev/null +++ b/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/Exceptions/RequestFailedExceptionExtensions.cs @@ -0,0 +1,39 @@ +using Azure; +using Microsoft.SemanticKernel; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading.Tasks; + +namespace MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3.Exceptions; + +internal static class RequestFailedExceptionExtensions +{ + /// + /// Converts a to an . + /// + /// The original . + /// An instance. + public static HttpOperationException ToHttpOperationException(this RequestFailedException exception) + { + const int NoResponseReceived = 0; + + string? responseContent = null; + + try + { + responseContent = exception.GetRawResponse()?.Content?.ToString(); + } +#pragma warning disable CA1031 // Do not catch general exception types + catch { } // We want to suppress any exceptions that occur while reading the content, ensuring that an HttpOperationException is thrown instead. +#pragma warning restore CA1031 + + return new HttpOperationException( + exception.Status == NoResponseReceived ? null : (HttpStatusCode?)exception.Status, + responseContent, + exception.Message, + exception); + } +} diff --git a/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3.csproj b/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3.csproj new file mode 100644 index 0000000..20cac63 --- /dev/null +++ b/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3.csproj @@ -0,0 +1,32 @@ + + + + net8.0 + enable + enable + MACK Mathieu + MACK Mathieu + Connector for Azure AI Studio MaaS model : hi3 + ioc + true + {3ff80538-77f7-55cc-dd14-72785201b220} + README.md + preview + True + Copyright (c) MACK Mathieu + https://github.com/mathieumack/MDev.Dotnet.SemanticKernel + https://github.com/mathieumack/MDev.Dotnet.SemanticKernel + git + MIT + + + + + + + + + + + + diff --git a/src/MDev.Dotnet.SemanticKernel.sln b/src/MDev.Dotnet.SemanticKernel.sln new file mode 100644 index 0000000..b0db03d --- /dev/null +++ b/src/MDev.Dotnet.SemanticKernel.sln @@ -0,0 +1,30 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.6.33801.468 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Library", "Library", "{1C71F25C-D1B2-479A-8A64-51FAEBA6F439}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3", "MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3\MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3.csproj", "{3FF80538-77F7-55CC-DD14-72785201B220}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {3FF80538-77F7-55CC-DD14-72785201B220}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3FF80538-77F7-55CC-DD14-72785201B220}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3FF80538-77F7-55CC-DD14-72785201B220}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3FF80538-77F7-55CC-DD14-72785201B220}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {3FF80538-77F7-55CC-DD14-72785201B220} = {1C71F25C-D1B2-479A-8A64-51FAEBA6F439} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {58FEAF09-0F8E-4ED5-99DD-360DC2203259} + EndGlobalSection +EndGlobal From b3e91ddc612f4debf6e13d2165a73e859fc5ff69 Mon Sep 17 00:00:00 2001 From: Mathieu Mack Date: Mon, 2 Sep 2024 18:58:08 +0200 Subject: [PATCH 2/9] Update interface to allow apiKey or AzureCredentials --- ...udioMaaSPhi3ServiceCollectionExtensions.cs | 94 +++++++++++++++---- ...reAIStudioMaaSPhi3ChatCompletionService.cs | 31 +++--- ...ernel.Connectors.AzureAIStudio.Phi3.csproj | 1 + 3 files changed, 97 insertions(+), 29 deletions(-) diff --git a/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/AIStudioMaaSPhi3ServiceCollectionExtensions.cs b/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/AIStudioMaaSPhi3ServiceCollectionExtensions.cs index 9921ca9..f40176e 100644 --- a/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/AIStudioMaaSPhi3ServiceCollectionExtensions.cs +++ b/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/AIStudioMaaSPhi3ServiceCollectionExtensions.cs @@ -4,6 +4,7 @@ using MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3.Diagnostics; using System.Net.Http.Headers; using Microsoft.Extensions.Logging; +using Azure.Identity; namespace MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3; @@ -13,19 +14,15 @@ public static class AIStudioMaaSPhi3ServiceCollectionExtensions /// Adds the Azure OpenAI chat completion service to the list. /// /// The instance to augment. - /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart /// Azure OpenAI API key, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart /// A local identifier for the given AI service - /// Model identifier, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// The HttpClient to use with this service. /// The same instance as . public static IKernelBuilder AddAzureAIStudioPhi3ChatCompletion( this IKernelBuilder builder, string endpoint, string apiKey, - string? serviceId = null, - string? modelId = null) + string? serviceId = null) { Verify.NotNull(builder); Verify.NotNullOrWhiteSpace(endpoint); @@ -33,29 +30,94 @@ public static IKernelBuilder AddAzureAIStudioPhi3ChatCompletion( var deploymentIdenfifier = Guid.NewGuid().ToString(); - var handler = new HttpClientHandler() - { - ClientCertificateOptions = ClientCertificateOption.Manual, - ServerCertificateCustomValidationCallback = - (httpRequestMessage, cert, cetChain, policyErrors) => { return true; } - }; - builder.Services.AddHttpClient($"http-{modelId}-{deploymentIdenfifier}") + builder.Services.AddHttpClient($"http--{deploymentIdenfifier}") .ConfigureHttpClient((services, client) => { client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey); client.BaseAddress = new Uri(endpoint); - }) - .ConfigurePrimaryHttpMessageHandler(() => handler); + }); Func factory = (serviceProvider, _) => { var httpClientFactory = serviceProvider.GetRequiredService(); - var httpClient = httpClientFactory.CreateClient($"http-{modelId}-{deploymentIdenfifier}"); + var httpClient = httpClientFactory.CreateClient($"http-{deploymentIdenfifier}"); + + var loggerFactory = serviceProvider.GetService(); + var logger = loggerFactory?.CreateLogger(typeof(AzureAIStudioMaaSPhi3ChatCompletionService)); + + var client = new AzureAIStudioMaaSPhi3ChatCompletionService(httpClient, null, logger); + return client; + }; + + builder.Services.AddKeyedSingleton(serviceId, factory); + + return builder; + } + + /// + /// Adds the Azure OpenAI chat completion service to the list. + /// + /// The instance to augment. + /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// Azure OpenAI API key, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// A local identifier for the given AI service + /// The HttpClient to use with this service. + /// The same instance as . + public static IKernelBuilder AddAzureAIStudioPhi3ChatCompletion( + this IKernelBuilder builder, + string endpoint, + string apiKey, + HttpClient httpClient, + string? serviceId = null) + { + Verify.NotNull(builder); + Verify.NotNullOrWhiteSpace(endpoint); + Verify.NotNullOrWhiteSpace(apiKey); + httpClient.BaseAddress = new Uri(endpoint); + + Func factory = (serviceProvider, _) => + { + var loggerFactory = serviceProvider.GetService(); + var logger = loggerFactory?.CreateLogger(typeof(AzureAIStudioMaaSPhi3ChatCompletionService)); + + var client = new AzureAIStudioMaaSPhi3ChatCompletionService(httpClient, null, logger); + return client; + }; + + builder.Services.AddKeyedSingleton(serviceId, factory); + + return builder; + } + + /// + /// Adds the Azure OpenAI chat completion service to the list. + /// + /// The instance to augment. + /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// Azure credentials that can be used for a managed identity or for current user + /// A local identifier for the given AI service + /// The HttpClient to use with this service. + /// The same instance as . + public static IKernelBuilder AddAzureAIStudioPhi3ChatCompletion( + this IKernelBuilder builder, + string endpoint, + DefaultAzureCredential credentials, + HttpClient httpClient, + string? serviceId = null) + { + Verify.NotNull(builder); + Verify.NotNullOrWhiteSpace(endpoint); + Verify.NotNull(credentials); + + httpClient.BaseAddress = new Uri(endpoint); + + Func factory = (serviceProvider, _) => + { var loggerFactory = serviceProvider.GetService(); var logger = loggerFactory?.CreateLogger(typeof(AzureAIStudioMaaSPhi3ChatCompletionService)); - var client = new AzureAIStudioMaaSPhi3ChatCompletionService(httpClient, logger); + var client = new AzureAIStudioMaaSPhi3ChatCompletionService(httpClient, credentials, logger); return client; }; diff --git a/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/ChatCompletion/AzureAIStudioMaaSPhi3ChatCompletionService.cs b/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/ChatCompletion/AzureAIStudioMaaSPhi3ChatCompletionService.cs index 84a8f4f..478fca8 100644 --- a/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/ChatCompletion/AzureAIStudioMaaSPhi3ChatCompletionService.cs +++ b/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/ChatCompletion/AzureAIStudioMaaSPhi3ChatCompletionService.cs @@ -11,21 +11,29 @@ using System.Net.Http.Json; using Microsoft.SemanticKernel.Connectors.OpenAI; using Microsoft.Extensions.Logging.Abstractions; +using Azure.Identity; namespace MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3; internal class AzureAIStudioMaaSPhi3ChatCompletionService : IChatCompletionService { + private static readonly string[] AuthorizationScopes = new string[1] { "https://cognitiveservices.azure.com/.default" }; + /// /// Logger instance /// internal ILogger Logger { get; set; } + private readonly DefaultAzureCredential credentials; + private readonly HttpClient httpClient; - internal AzureAIStudioMaaSPhi3ChatCompletionService(HttpClient httpClient, ILogger? logger = null) + internal AzureAIStudioMaaSPhi3ChatCompletionService(HttpClient httpClient, + DefaultAzureCredential credentials = null, + ILogger? logger = null) { this.httpClient = httpClient; + this.credentials = credentials; this.Logger = logger ?? NullLogger.Instance; } @@ -93,6 +101,13 @@ public async Task> GetChatMessageContentsAsync var content = new StringContent(requestBody); content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); + // authorization: + if (credentials != null) + { + var token = await credentials.GetTokenAsync(new Azure.Core.TokenRequestContext(AuthorizationScopes)); + httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token.Token); + } + for (int iteration = 1; ; iteration++) { // Make the request. @@ -104,24 +119,14 @@ public async Task> GetChatMessageContentsAsync { responseContent = await response.Content.ReadFromJsonAsync(); } - //else - //{ - // //Console.WriteLine(string.Format("The request failed with status code: {0}", response.StatusCode)); - - // // Print the headers - they include the requert ID and the timestamp, - // // which are useful for debugging the failure - // //Console.WriteLine(response.Headers.ToString()); - - // responseContent = await response.Content.ReadAsStringAsync(); - // //Console.WriteLine(responseContent); - //} - this.CaptureUsageDetails(responseContent.Usage); if (responseContent is null) { throw new KernelException("Chat completions not found"); } + this.CaptureUsageDetails(responseContent.Usage); + // If we don't want to attempt to invoke any functions, just return the result. // Or if we are auto-invoking but we somehow end up with other than 1 choice even though only 1 was requested, similarly bail. return responseContent.Choices.Select(e => diff --git a/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3.csproj b/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3.csproj index 20cac63..d289b98 100644 --- a/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3.csproj +++ b/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3.csproj @@ -25,6 +25,7 @@ + From 8ee0060cd2522a547725e9e94a569b6d189748e7 Mon Sep 17 00:00:00 2001 From: Mathieu Mack Date: Mon, 2 Sep 2024 22:05:08 +0200 Subject: [PATCH 3/9] Update interface and semantic kernel dependency. Issues with last version (1.18-alpha) --- ...udioMaaSPhi3ServiceCollectionExtensions.cs | 1 + ...reAIStudioMaaSPhi3ChatCompletionService.cs | 125 +++++++++--------- 2 files changed, 63 insertions(+), 63 deletions(-) diff --git a/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/AIStudioMaaSPhi3ServiceCollectionExtensions.cs b/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/AIStudioMaaSPhi3ServiceCollectionExtensions.cs index f40176e..24ca489 100644 --- a/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/AIStudioMaaSPhi3ServiceCollectionExtensions.cs +++ b/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/AIStudioMaaSPhi3ServiceCollectionExtensions.cs @@ -75,6 +75,7 @@ public static IKernelBuilder AddAzureAIStudioPhi3ChatCompletion( Verify.NotNullOrWhiteSpace(apiKey); httpClient.BaseAddress = new Uri(endpoint); + httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey); Func factory = (serviceProvider, _) => { diff --git a/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/ChatCompletion/AzureAIStudioMaaSPhi3ChatCompletionService.cs b/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/ChatCompletion/AzureAIStudioMaaSPhi3ChatCompletionService.cs index 478fca8..d74a8d3 100644 --- a/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/ChatCompletion/AzureAIStudioMaaSPhi3ChatCompletionService.cs +++ b/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/ChatCompletion/AzureAIStudioMaaSPhi3ChatCompletionService.cs @@ -71,69 +71,6 @@ internal AzureAIStudioMaaSPhi3ChatCompletionService(HttpClient httpClient, public IReadOnlyDictionary Attributes => throw new NotImplementedException(); - public async Task> GetChatMessageContentsAsync(ChatHistory chatHistory, PromptExecutionSettings? executionSettings = null, Kernel? kernel = null, CancellationToken cancellationToken = default) - { - if (chatHistory is null) - throw new ArgumentNullException(nameof(chatHistory)); - - // Convert the incoming execution settings to OpenAI settings. - var chatExecutionSettings = Phi3PromptExecutionSettings.FromExecutionSettings(executionSettings); - bool autoInvoke = false; - ValidateMaxTokens(chatExecutionSettings.MaxTokens); - - // Create the Azure SDK ChatCompletionOptions instance from all available information. - var body = new ChatWithDataRequest() - { - MaxTokens = chatExecutionSettings.MaxTokens, - Temperature = chatExecutionSettings.Temperature, - TopP = chatExecutionSettings.TopP, - Messages = chatHistory.Select(e => new ChatWithDataMessage() - { - Role = e.Role.ToString(), - Content = e.Content.ToString() - }).ToList() - }; - - var uri = "v1/chat/completions"; - - var requestBody = JsonSerializer.Serialize(body); - - var content = new StringContent(requestBody); - content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); - - // authorization: - if (credentials != null) - { - var token = await credentials.GetTokenAsync(new Azure.Core.TokenRequestContext(AuthorizationScopes)); - httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token.Token); - } - - for (int iteration = 1; ; iteration++) - { - // Make the request. - HttpResponseMessage response = await httpClient.PostAsync(uri, content); - - ChatWithDataResponse responseContent = null; - - if (response.IsSuccessStatusCode) - { - responseContent = await response.Content.ReadFromJsonAsync(); - } - - if (responseContent is null) - { - throw new KernelException("Chat completions not found"); - } - - this.CaptureUsageDetails(responseContent.Usage); - - // If we don't want to attempt to invoke any functions, just return the result. - // Or if we are auto-invoking but we somehow end up with other than 1 choice even though only 1 was requested, similarly bail. - return responseContent.Choices.Select(e => - ToChatMessageContent(responseContent, e)).ToList(); - } - } - private ChatMessageContent ToChatMessageContent(ChatWithDataResponse chatWithDataResponse, ChatWithDataChoice chatWithDataChoice) { var metadatas = new Dictionary(); @@ -206,4 +143,66 @@ public IAsyncEnumerable GetStreamingChatMessageCont { throw new NotImplementedException(); } + + public async Task> GetChatMessageContentsAsync(ChatHistory chatHistory, PromptExecutionSettings? executionSettings = null, Kernel? kernel = null, CancellationToken cancellationToken = default) + { + if (chatHistory is null) + throw new ArgumentNullException(nameof(chatHistory)); + + // Convert the incoming execution settings to OpenAI settings. + var chatExecutionSettings = Phi3PromptExecutionSettings.FromExecutionSettings(executionSettings); + ValidateMaxTokens(chatExecutionSettings.MaxTokens); + + // Create the Azure SDK ChatCompletionOptions instance from all available information. + var body = new ChatWithDataRequest() + { + MaxTokens = chatExecutionSettings.MaxTokens, + Temperature = chatExecutionSettings.Temperature, + TopP = chatExecutionSettings.TopP, + Messages = chatHistory.Select(e => new ChatWithDataMessage() + { + Role = e.Role.ToString(), + Content = e.Content.ToString() + }).ToList() + }; + + var uri = "v1/chat/completions"; + + var requestBody = JsonSerializer.Serialize(body); + + var content = new StringContent(requestBody); + content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); + + // authorization: + if (credentials != null) + { + var token = await credentials.GetTokenAsync(new Azure.Core.TokenRequestContext(AuthorizationScopes)); + httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token.Token); + } + + for (int iteration = 1; ; iteration++) + { + // Make the request. + HttpResponseMessage response = await httpClient.PostAsync(uri, content); + + ChatWithDataResponse responseContent = null; + + if (response.IsSuccessStatusCode) + { + responseContent = await response.Content.ReadFromJsonAsync(); + } + + if (responseContent is null) + { + throw new KernelException("Chat completions not found"); + } + + this.CaptureUsageDetails(responseContent.Usage); + + // If we don't want to attempt to invoke any functions, just return the result. + // Or if we are auto-invoking but we somehow end up with other than 1 choice even though only 1 was requested, similarly bail. + return responseContent.Choices.Select(e => + ToChatMessageContent(responseContent, e)).ToList(); + } + } } From b057b25c0fec1e10ee5706edd8c32bf7292d80a2 Mon Sep 17 00:00:00 2001 From: Mathieu Mack Date: Mon, 2 Sep 2024 23:39:15 +0200 Subject: [PATCH 4/9] Update methods to supports HttpPipelines --- ...udioMaaSPhi3ServiceCollectionExtensions.cs | 26 ++- .../AzureCore/MDEVAzureKeyCredentialPolicy.cs | 46 +++++ ...reAIStudioMaaSPhi3ChatCompletionService.cs | 186 ++++++++++++++---- 3 files changed, 212 insertions(+), 46 deletions(-) create mode 100644 src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/AzureCore/MDEVAzureKeyCredentialPolicy.cs diff --git a/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/AIStudioMaaSPhi3ServiceCollectionExtensions.cs b/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/AIStudioMaaSPhi3ServiceCollectionExtensions.cs index 24ca489..d16b78f 100644 --- a/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/AIStudioMaaSPhi3ServiceCollectionExtensions.cs +++ b/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/AIStudioMaaSPhi3ServiceCollectionExtensions.cs @@ -5,6 +5,7 @@ using System.Net.Http.Headers; using Microsoft.Extensions.Logging; using Azure.Identity; +using Microsoft.Extensions.Http.Logging; namespace MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3; @@ -30,12 +31,7 @@ public static IKernelBuilder AddAzureAIStudioPhi3ChatCompletion( var deploymentIdenfifier = Guid.NewGuid().ToString(); - builder.Services.AddHttpClient($"http--{deploymentIdenfifier}") - .ConfigureHttpClient((services, client) => - { - client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey); - client.BaseAddress = new Uri(endpoint); - }); + builder.Services.AddHttpClient($"http--{deploymentIdenfifier}"); Func factory = (serviceProvider, _) => { @@ -45,7 +41,11 @@ public static IKernelBuilder AddAzureAIStudioPhi3ChatCompletion( var loggerFactory = serviceProvider.GetService(); var logger = loggerFactory?.CreateLogger(typeof(AzureAIStudioMaaSPhi3ChatCompletionService)); - var client = new AzureAIStudioMaaSPhi3ChatCompletionService(httpClient, null, logger); + var client = new AzureAIStudioMaaSPhi3ChatCompletionService(new Uri(endpoint), + new Azure.AzureKeyCredential(apiKey), + new(), + null, + logger); return client; }; @@ -82,7 +82,11 @@ public static IKernelBuilder AddAzureAIStudioPhi3ChatCompletion( var loggerFactory = serviceProvider.GetService(); var logger = loggerFactory?.CreateLogger(typeof(AzureAIStudioMaaSPhi3ChatCompletionService)); - var client = new AzureAIStudioMaaSPhi3ChatCompletionService(httpClient, null, logger); + var client = new AzureAIStudioMaaSPhi3ChatCompletionService(new Uri(endpoint), + new Azure.AzureKeyCredential(apiKey), + new(), + httpClient, + logger); return client; }; @@ -118,7 +122,11 @@ public static IKernelBuilder AddAzureAIStudioPhi3ChatCompletion( var loggerFactory = serviceProvider.GetService(); var logger = loggerFactory?.CreateLogger(typeof(AzureAIStudioMaaSPhi3ChatCompletionService)); - var client = new AzureAIStudioMaaSPhi3ChatCompletionService(httpClient, credentials, logger); + var client = new AzureAIStudioMaaSPhi3ChatCompletionService(new Uri(endpoint), + credentials, + new(), + httpClient, + logger); return client; }; diff --git a/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/AzureCore/MDEVAzureKeyCredentialPolicy.cs b/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/AzureCore/MDEVAzureKeyCredentialPolicy.cs new file mode 100644 index 0000000..12b40f6 --- /dev/null +++ b/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/AzureCore/MDEVAzureKeyCredentialPolicy.cs @@ -0,0 +1,46 @@ +using Azure.Core.Pipeline; +using Azure.Core; +using Azure; + +namespace MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3.AzureCore +{ + internal class MDEVAzureKeyCredentialPolicy : HttpPipelineSynchronousPolicy + { + private readonly string _name; + private readonly AzureKeyCredential _credential; + private readonly string? _prefix; + + /// + /// Initializes a new instance of the class. + /// + /// The used to authenticate requests. + /// The name of the key header used for the credential. + /// The prefix to apply before the credential key. For example, a prefix of "SharedAccessKey" would result in + /// a value of "SharedAccessKey {credential.Key}" being stamped on the request header with header key of . + public MDEVAzureKeyCredentialPolicy(AzureKeyCredential credential, string name, string? prefix = null) + { + if (credential is null) + { + throw new ArgumentNullException(nameof(credential)); + } + if (name is null) + { + throw new ArgumentNullException(nameof(name)); + } + if (name.Length == 0) + { + throw new ArgumentException("Value cannot be an empty string.", nameof(name)); + } + _credential = credential; + _name = name; + _prefix = prefix; + } + + /// + public override void OnSendingRequest(HttpMessage message) + { + base.OnSendingRequest(message); + message.Request.Headers.SetValue(_name, _prefix != null ? $"{_prefix} {_credential.Key}" : _credential.Key); + } + } +} diff --git a/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/ChatCompletion/AzureAIStudioMaaSPhi3ChatCompletionService.cs b/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/ChatCompletion/AzureAIStudioMaaSPhi3ChatCompletionService.cs index d74a8d3..cb0d5dc 100644 --- a/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/ChatCompletion/AzureAIStudioMaaSPhi3ChatCompletionService.cs +++ b/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/ChatCompletion/AzureAIStudioMaaSPhi3ChatCompletionService.cs @@ -4,14 +4,13 @@ using System.Text.Json; using MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3.ChatCompletionWithData; using Azure; -using MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3.Exceptions; using Azure.AI.OpenAI; using System.Diagnostics.Metrics; -using System.Net.Http.Headers; -using System.Net.Http.Json; -using Microsoft.SemanticKernel.Connectors.OpenAI; using Microsoft.Extensions.Logging.Abstractions; -using Azure.Identity; +using Azure.Core; +using Azure.Core.Pipeline; +using MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3.AzureCore; +using System.Net.Http; namespace MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3; @@ -24,16 +23,85 @@ internal class AzureAIStudioMaaSPhi3ChatCompletionService : IChatCompletionServi /// internal ILogger Logger { get; set; } - private readonly DefaultAzureCredential credentials; - + private readonly AzureKeyCredential _keyCredential; + private readonly TokenCredential _tokenCredential; + private readonly HttpPipeline _pipeline; + private readonly Uri endpoint; private readonly HttpClient httpClient; + private readonly OpenAIClientOptions options; + + private static RequestContext DefaultRequestContext = new RequestContext(); + private static ResponseClassifier _responseClassifier200; + //private readonly HttpClient httpClient; + private static ResponseClassifier ResponseClassifier200 + { + get + { + ResponseClassifier responseClassifier = _responseClassifier200; + if (responseClassifier == null) + { + responseClassifier = (_responseClassifier200 = new StatusCodeClassifier(stackalloc ushort[1] { 200 })); + } + + return responseClassifier; + } + } - internal AzureAIStudioMaaSPhi3ChatCompletionService(HttpClient httpClient, - DefaultAzureCredential credentials = null, + internal AzureAIStudioMaaSPhi3ChatCompletionService(Uri endpoint, + AzureKeyCredential keyCredential, + OpenAIClientOptions options, + HttpClient httpClient = null, ILogger? logger = null) { + this.endpoint = endpoint; this.httpClient = httpClient; - this.credentials = credentials; + this.options = options; + if (options == null) + { + this.options = new OpenAIClientOptions(); + } + + if (httpClient is not null) + { + this.options.Transport = new HttpClientTransport(httpClient); + this.options.RetryPolicy = new RetryPolicy(maxRetries: 0); // Disable Azure SDK retry policy if and only if a custom HttpClient is provided. + this.options.Retry.NetworkTimeout = Timeout.InfiniteTimeSpan; // Disable Azure SDK default timeout + } + + _keyCredential = keyCredential; + _pipeline = HttpPipelineBuilder.Build(this.options, Array.Empty(), new HttpPipelinePolicy[1] + { + new MDEVAzureKeyCredentialPolicy(_keyCredential, "api-key") + }, new ResponseClassifier()); + this.Logger = logger ?? NullLogger.Instance; + } + + internal AzureAIStudioMaaSPhi3ChatCompletionService(Uri endpoint, + TokenCredential tokenCredential, + OpenAIClientOptions options, + HttpClient httpClient = null, + ILogger? logger = null) + { + this.endpoint = endpoint; + this.httpClient = httpClient; + this.options = options; + if (options == null) + { + this.options = new OpenAIClientOptions(); + } + + if (httpClient is not null) + { + this.options.Transport = new HttpClientTransport(httpClient); + this.options.RetryPolicy = new RetryPolicy(maxRetries: 0); // Disable Azure SDK retry policy if and only if a custom HttpClient is provided. + this.options.Retry.NetworkTimeout = Timeout.InfiniteTimeSpan; // Disable Azure SDK default timeout + } + + _tokenCredential = tokenCredential; + _pipeline = HttpPipelineBuilder.Build(this.options, Array.Empty(), new HttpPipelinePolicy[1] + { + new BearerTokenAuthenticationPolicy(_tokenCredential, AuthorizationScopes) + }, new ResponseClassifier()); this.Logger = logger ?? NullLogger.Instance; } @@ -166,43 +234,87 @@ public async Task> GetChatMessageContentsAsync }).ToList() }; - var uri = "v1/chat/completions"; + var requestContent = RequestContent.Create(body); - var requestBody = JsonSerializer.Serialize(body); - - var content = new StringContent(requestBody); - content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); + RequestContext requestContext = FromCancellationToken(cancellationToken); + try + { + using HttpMessage message = CreatePostRequestMessage("chat/completions", requestContent, requestContext); + var response = ProcessMessage(_pipeline, message, requestContext); - // authorization: - if (credentials != null) + using JsonDocument jsonDocument = JsonDocument.Parse(response.Content); + var responseContent = System.Text.Json.JsonSerializer.Deserialize(jsonDocument); + this.CaptureUsageDetails(responseContent.Usage); + return responseContent.Choices.Select(e => + ToChatMessageContent(responseContent, e)).ToList(); + } + catch (Exception exception) { - var token = await credentials.GetTokenAsync(new Azure.Core.TokenRequestContext(AuthorizationScopes)); - httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token.Token); + throw; } + } - for (int iteration = 1; ; iteration++) + public Response ProcessMessage(HttpPipeline pipeline, HttpMessage message, RequestContext? requestContext, CancellationToken cancellationToken = default(CancellationToken)) + { + var (cancellationToken2, errorOptions) = ApplyRequestContext(requestContext); + if (!cancellationToken2.CanBeCanceled || !cancellationToken.CanBeCanceled) { - // Make the request. - HttpResponseMessage response = await httpClient.PostAsync(uri, content); + pipeline.Send(message, cancellationToken.CanBeCanceled ? cancellationToken : cancellationToken2); + } + else + { + using CancellationTokenSource cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken2, cancellationToken); + pipeline.Send(message, cancellationTokenSource.Token); + } - ChatWithDataResponse responseContent = null; + if (!message.Response.IsError || errorOptions == ErrorOptions.NoThrow) + { + return message.Response; + } - if (response.IsSuccessStatusCode) - { - responseContent = await response.Content.ReadFromJsonAsync(); - } + throw new RequestFailedException(message.Response); + } - if (responseContent is null) - { - throw new KernelException("Chat completions not found"); - } + private (CancellationToken CancellationToken, ErrorOptions ErrorOptions) ApplyRequestContext(RequestContext? requestContext) + { + if (requestContext == null) + { + return (CancellationToken.None, ErrorOptions.Default); + } - this.CaptureUsageDetails(responseContent.Usage); + return (requestContext.CancellationToken, requestContext.ErrorOptions); + } - // If we don't want to attempt to invoke any functions, just return the result. - // Or if we are auto-invoking but we somehow end up with other than 1 choice even though only 1 was requested, similarly bail. - return responseContent.Choices.Select(e => - ToChatMessageContent(responseContent, e)).ToList(); + internal RequestContext FromCancellationToken(CancellationToken cancellationToken = default(CancellationToken)) + { + if (!cancellationToken.CanBeCanceled) + { + return DefaultRequestContext; } + + return new RequestContext + { + CancellationToken = cancellationToken + }; + } + + internal RequestUriBuilder GetUri(string operationPath) + { + var builder = new RequestUriBuilder(); + builder.Reset(endpoint); + builder.AppendPath("/" + operationPath, escape: false); + return builder; + } + + internal HttpMessage CreatePostRequestMessage(string operationPath, RequestContent content, RequestContext context) + { + HttpMessage httpMessage = _pipeline.CreateMessage(context, ResponseClassifier200); + Request request = httpMessage.Request; + request.Method = RequestMethod.Post; + request.Uri = GetUri(operationPath); + request.Headers.Add("Accept", "application/json"); + request.Headers.Add("Content-Type", "application/json"); + request.Content = content; + return httpMessage; } -} +} \ No newline at end of file From 59a5c223168fec34a65ea521952343785f51e6bc Mon Sep 17 00:00:00 2001 From: Mathieu Mack Date: Mon, 2 Sep 2024 23:54:09 +0200 Subject: [PATCH 5/9] Fix sonar issues --- ...udioMaaSPhi3ServiceCollectionExtensions.cs | 2 +- ...reAIStudioMaaSPhi3ChatCompletionService.cs | 48 +++++-------------- .../Diagnostics/Verify.cs | 4 +- 3 files changed, 16 insertions(+), 38 deletions(-) diff --git a/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/AIStudioMaaSPhi3ServiceCollectionExtensions.cs b/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/AIStudioMaaSPhi3ServiceCollectionExtensions.cs index d16b78f..bb44c3f 100644 --- a/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/AIStudioMaaSPhi3ServiceCollectionExtensions.cs +++ b/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/AIStudioMaaSPhi3ServiceCollectionExtensions.cs @@ -44,7 +44,7 @@ public static IKernelBuilder AddAzureAIStudioPhi3ChatCompletion( var client = new AzureAIStudioMaaSPhi3ChatCompletionService(new Uri(endpoint), new Azure.AzureKeyCredential(apiKey), new(), - null, + httpClient, logger); return client; }; diff --git a/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/ChatCompletion/AzureAIStudioMaaSPhi3ChatCompletionService.cs b/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/ChatCompletion/AzureAIStudioMaaSPhi3ChatCompletionService.cs index cb0d5dc..7194f72 100644 --- a/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/ChatCompletion/AzureAIStudioMaaSPhi3ChatCompletionService.cs +++ b/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/ChatCompletion/AzureAIStudioMaaSPhi3ChatCompletionService.cs @@ -23,16 +23,12 @@ internal class AzureAIStudioMaaSPhi3ChatCompletionService : IChatCompletionServi /// internal ILogger Logger { get; set; } - private readonly AzureKeyCredential _keyCredential; - private readonly TokenCredential _tokenCredential; private readonly HttpPipeline _pipeline; private readonly Uri endpoint; - private readonly HttpClient httpClient; - private readonly OpenAIClientOptions options; private static RequestContext DefaultRequestContext = new RequestContext(); private static ResponseClassifier _responseClassifier200; - //private readonly HttpClient httpClient; + private static ResponseClassifier ResponseClassifier200 { get @@ -54,24 +50,21 @@ internal AzureAIStudioMaaSPhi3ChatCompletionService(Uri endpoint, ILogger? logger = null) { this.endpoint = endpoint; - this.httpClient = httpClient; - this.options = options; if (options == null) { - this.options = new OpenAIClientOptions(); + options = new OpenAIClientOptions(); } if (httpClient is not null) { - this.options.Transport = new HttpClientTransport(httpClient); - this.options.RetryPolicy = new RetryPolicy(maxRetries: 0); // Disable Azure SDK retry policy if and only if a custom HttpClient is provided. - this.options.Retry.NetworkTimeout = Timeout.InfiniteTimeSpan; // Disable Azure SDK default timeout + options.Transport = new HttpClientTransport(httpClient); + options.RetryPolicy = new RetryPolicy(maxRetries: 0); // Disable Azure SDK retry policy if and only if a custom HttpClient is provided. + options.Retry.NetworkTimeout = Timeout.InfiniteTimeSpan; // Disable Azure SDK default timeout } - _keyCredential = keyCredential; - _pipeline = HttpPipelineBuilder.Build(this.options, Array.Empty(), new HttpPipelinePolicy[1] + _pipeline = HttpPipelineBuilder.Build(options, Array.Empty(), new HttpPipelinePolicy[1] { - new MDEVAzureKeyCredentialPolicy(_keyCredential, "api-key") + new MDEVAzureKeyCredentialPolicy(keyCredential, "api-key") }, new ResponseClassifier()); this.Logger = logger ?? NullLogger.Instance; } @@ -83,24 +76,21 @@ internal AzureAIStudioMaaSPhi3ChatCompletionService(Uri endpoint, ILogger? logger = null) { this.endpoint = endpoint; - this.httpClient = httpClient; - this.options = options; if (options == null) { - this.options = new OpenAIClientOptions(); + options = new OpenAIClientOptions(); } if (httpClient is not null) { - this.options.Transport = new HttpClientTransport(httpClient); - this.options.RetryPolicy = new RetryPolicy(maxRetries: 0); // Disable Azure SDK retry policy if and only if a custom HttpClient is provided. - this.options.Retry.NetworkTimeout = Timeout.InfiniteTimeSpan; // Disable Azure SDK default timeout + options.Transport = new HttpClientTransport(httpClient); + options.RetryPolicy = new RetryPolicy(maxRetries: 0); // Disable Azure SDK retry policy if and only if a custom HttpClient is provided. + options.Retry.NetworkTimeout = Timeout.InfiniteTimeSpan; // Disable Azure SDK default timeout } - _tokenCredential = tokenCredential; - _pipeline = HttpPipelineBuilder.Build(this.options, Array.Empty(), new HttpPipelinePolicy[1] + _pipeline = HttpPipelineBuilder.Build(options, Array.Empty(), new HttpPipelinePolicy[1] { - new BearerTokenAuthenticationPolicy(_tokenCredential, AuthorizationScopes) + new BearerTokenAuthenticationPolicy(tokenCredential, AuthorizationScopes) }, new ResponseClassifier()); this.Logger = logger ?? NullLogger.Instance; } @@ -195,18 +185,6 @@ private void CaptureUsageDetails(ChatWithDataUsage usage) s_totalTokensCounter.Add(usage.TotalTokens); } - //private static async Task RunRequestAsync(Func> request) - //{ - // try - // { - // return await request.Invoke().ConfigureAwait(false); - // } - // catch (RequestFailedException e) - // { - // throw e.ToHttpOperationException(); - // } - //} - public IAsyncEnumerable GetStreamingChatMessageContentsAsync(ChatHistory chatHistory, PromptExecutionSettings? executionSettings = null, Kernel? kernel = null, CancellationToken cancellationToken = default) { throw new NotImplementedException(); diff --git a/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/Diagnostics/Verify.cs b/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/Diagnostics/Verify.cs index 304f6a8..7f03cbf 100644 --- a/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/Diagnostics/Verify.cs +++ b/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/Diagnostics/Verify.cs @@ -9,8 +9,8 @@ namespace MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3.Diagnostics; [ExcludeFromCodeCoverage] internal static class Verify { - private static readonly Regex s_asciiLettersDigitsUnderscoresRegex = new("^[0-9A-Za-z_]*$"); - private static readonly Regex s_filenameRegex = new("^[^.]+\\.[^.]+$"); + private static readonly Regex s_asciiLettersDigitsUnderscoresRegex = new("^[0-9A-Za-z_]*$", RegexOptions.None, TimeSpan.FromMilliseconds(500)); + private static readonly Regex s_filenameRegex = new("^[^.]+\\.[^.]+$", RegexOptions.None, TimeSpan.FromMilliseconds(500)); /// /// Equivalent of ArgumentNullException.ThrowIfNull From c3d9e19bfc815da5321a94e14ab256d25aafaf97 Mon Sep 17 00:00:00 2001 From: Mathieu Mack Date: Tue, 3 Sep 2024 00:31:36 +0200 Subject: [PATCH 6/9] Fix issue with OpenAIClientOptions class --- .../AzureAIStudioMaaSPhi3ChatCompletionService.cs | 13 +++++++------ .../ChatCompletion/Phi3ClientOptions.cs | 7 +++++++ ...anticKernel.Connectors.AzureAIStudio.Phi3.csproj | 2 +- 3 files changed, 15 insertions(+), 7 deletions(-) create mode 100644 src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/ChatCompletion/Phi3ClientOptions.cs diff --git a/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/ChatCompletion/AzureAIStudioMaaSPhi3ChatCompletionService.cs b/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/ChatCompletion/AzureAIStudioMaaSPhi3ChatCompletionService.cs index 7194f72..64f0b95 100644 --- a/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/ChatCompletion/AzureAIStudioMaaSPhi3ChatCompletionService.cs +++ b/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/ChatCompletion/AzureAIStudioMaaSPhi3ChatCompletionService.cs @@ -11,6 +11,7 @@ using Azure.Core.Pipeline; using MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3.AzureCore; using System.Net.Http; +using MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3.ChatCompletion; namespace MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3; @@ -44,15 +45,15 @@ private static ResponseClassifier ResponseClassifier200 } internal AzureAIStudioMaaSPhi3ChatCompletionService(Uri endpoint, - AzureKeyCredential keyCredential, - OpenAIClientOptions options, + AzureKeyCredential keyCredential, + Phi3ClientOptions options, HttpClient httpClient = null, ILogger? logger = null) { this.endpoint = endpoint; if (options == null) { - options = new OpenAIClientOptions(); + options = new Phi3ClientOptions(); } if (httpClient is not null) @@ -70,15 +71,15 @@ internal AzureAIStudioMaaSPhi3ChatCompletionService(Uri endpoint, } internal AzureAIStudioMaaSPhi3ChatCompletionService(Uri endpoint, - TokenCredential tokenCredential, - OpenAIClientOptions options, + TokenCredential tokenCredential, + Phi3ClientOptions options, HttpClient httpClient = null, ILogger? logger = null) { this.endpoint = endpoint; if (options == null) { - options = new OpenAIClientOptions(); + options = new Phi3ClientOptions(); } if (httpClient is not null) diff --git a/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/ChatCompletion/Phi3ClientOptions.cs b/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/ChatCompletion/Phi3ClientOptions.cs new file mode 100644 index 0000000..b395fc5 --- /dev/null +++ b/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/ChatCompletion/Phi3ClientOptions.cs @@ -0,0 +1,7 @@ +using Azure.Core; + +namespace MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3.ChatCompletion; + +internal class Phi3ClientOptions : ClientOptions +{ +} diff --git a/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3.csproj b/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3.csproj index d289b98..0d5a93b 100644 --- a/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3.csproj +++ b/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3.csproj @@ -6,7 +6,7 @@ enable MACK Mathieu MACK Mathieu - Connector for Azure AI Studio MaaS model : hi3 + Connector for Azure AI Studio MaaS model : phi3 ioc true {3ff80538-77f7-55cc-dd14-72785201b220} From 379f9d5ed0e196c6de6c884b5efe32c76e698d03 Mon Sep 17 00:00:00 2001 From: Mathieu Mack Date: Tue, 3 Sep 2024 09:43:51 +0200 Subject: [PATCH 7/9] Fix authorization with API Key --- .../AzureCore/MDEVAzureKeyCredentialPolicy.cs | 3 +++ .../AzureAIStudioMaaSPhi3ChatCompletionService.cs | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/AzureCore/MDEVAzureKeyCredentialPolicy.cs b/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/AzureCore/MDEVAzureKeyCredentialPolicy.cs index 12b40f6..6f9198a 100644 --- a/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/AzureCore/MDEVAzureKeyCredentialPolicy.cs +++ b/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/AzureCore/MDEVAzureKeyCredentialPolicy.cs @@ -1,6 +1,9 @@ using Azure.Core.Pipeline; using Azure.Core; using Azure; +using System.Net.Http.Headers; +using System.Net.Http; +using System.Net; namespace MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3.AzureCore { diff --git a/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/ChatCompletion/AzureAIStudioMaaSPhi3ChatCompletionService.cs b/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/ChatCompletion/AzureAIStudioMaaSPhi3ChatCompletionService.cs index 64f0b95..047971b 100644 --- a/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/ChatCompletion/AzureAIStudioMaaSPhi3ChatCompletionService.cs +++ b/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/ChatCompletion/AzureAIStudioMaaSPhi3ChatCompletionService.cs @@ -65,7 +65,7 @@ internal AzureAIStudioMaaSPhi3ChatCompletionService(Uri endpoint, _pipeline = HttpPipelineBuilder.Build(options, Array.Empty(), new HttpPipelinePolicy[1] { - new MDEVAzureKeyCredentialPolicy(keyCredential, "api-key") + new MDEVAzureKeyCredentialPolicy(keyCredential, "Authorization", "Bearer") }, new ResponseClassifier()); this.Logger = logger ?? NullLogger.Instance; } From 4060703155d3dcb8b3f470cee8c8424322bc1195 Mon Sep 17 00:00:00 2001 From: Mathieu Mack Date: Tue, 3 Sep 2024 09:45:02 +0200 Subject: [PATCH 8/9] Remove useles assignment to HttpClient --- .../AIStudioMaaSPhi3ServiceCollectionExtensions.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/AIStudioMaaSPhi3ServiceCollectionExtensions.cs b/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/AIStudioMaaSPhi3ServiceCollectionExtensions.cs index bb44c3f..5ee6336 100644 --- a/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/AIStudioMaaSPhi3ServiceCollectionExtensions.cs +++ b/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/AIStudioMaaSPhi3ServiceCollectionExtensions.cs @@ -74,9 +74,6 @@ public static IKernelBuilder AddAzureAIStudioPhi3ChatCompletion( Verify.NotNullOrWhiteSpace(endpoint); Verify.NotNullOrWhiteSpace(apiKey); - httpClient.BaseAddress = new Uri(endpoint); - httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey); - Func factory = (serviceProvider, _) => { var loggerFactory = serviceProvider.GetService(); From cb54f75de34b0235e4bdb7f1de630ad1f37699cd Mon Sep 17 00:00:00 2001 From: Mathieu Mack Date: Thu, 21 Nov 2024 21:38:04 +0100 Subject: [PATCH 9/9] Add support for phi3 dployed on Azure AI Studio --- .github/workflows/ci.yml | 2 +- ...Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3.csproj | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4fdfc7a..9bf5a14 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,7 @@ jobs: dotnet: uses: mathieumack/MyGithubActions/.github/workflows/dotnetlib.yml@main with: - publishToNuget: false + publishToNuget: ${{ github.event.inputs.publishToNuget == true }} secrets: NUGETPACKAGEIDENTIFIER: ${{ secrets.NUGETPACKAGEIDENTIFIER }} NUGETAPIKEY: ${{ secrets.NUGETAPIKEY }} diff --git a/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3.csproj b/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3.csproj index 0d5a93b..4920c3e 100644 --- a/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3.csproj +++ b/src/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3/MDev.Dotnet.SemanticKernel.Connectors.AzureAIStudio.Phi3.csproj @@ -27,7 +27,7 @@ - - + +