Skip to content

Commit

Permalink
.Net - Support Azure Endpoint for File-Service (#5640)
Browse files Browse the repository at this point in the history
### Motivation and Context

<!-- Thank you for your contribution to the semantic-kernel repo!
Please help reviewers and future users, providing the following
information:
  1. Why is this change required?
  2. What problem does it solve?
  3. What scenario does it contribute to?
  4. If it fixes an open issue, please link to the issue here.
-->

Add *file service* support for Azure endpoint, still only available as
REST API:


https://learn.microsoft.com/en-us/rest/api/azureopenai/files?view=rest-azureopenai-2024-02-15-preview

### Description

<!-- Describe your changes, the overall approach, the underlying design.
These notes will help understanding how your code works. Thanks! -->

- Added support for Azure Open AI deployment URl and deployment-name.
- Modified kernel-syntax-examples using the file-service to function on
Open AI and Azure Open AI

### Contribution Checklist

<!-- Before submitting this PR, please make sure: -->

- [x] The code builds clean without any errors or warnings
- [x] The PR follows the [SK Contribution
Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md)
and the [pre-submission formatting
script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts)
raises no violations
- [x] All unit tests pass, and I have added new tests where possible
- [x] I didn't break anyone 😄
  • Loading branch information
crickman authored Mar 27, 2024
1 parent 2a23617 commit 0de7d34
Show file tree
Hide file tree
Showing 7 changed files with 264 additions and 87 deletions.
60 changes: 32 additions & 28 deletions dotnet/samples/KernelSyntaxExamples/Example75_AgentTools.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,16 @@ public sealed class Example75_AgentTools : BaseTest
/// </summary>
private const string OpenAIFunctionEnabledModel = "gpt-4-1106-preview";

/// <summary>
/// Flag to force usage of OpenAI configuration if both <see cref="TestConfiguration.OpenAI"/>
/// and <see cref="TestConfiguration.AzureOpenAI"/> are defined.
/// If 'false', Azure takes precedence.
/// </summary>
/// <remarks>
/// NOTE: Retrieval tools is not currently available on Azure.
/// </remarks>
private const bool ForceOpenAI = true;

// Track agents for clean-up
private readonly List<IAgent> _agents = new();

Expand All @@ -36,26 +46,13 @@ public async Task RunCodeInterpreterToolAsync()
{
this.WriteLine("======== Using CodeInterpreter tool ========");

if (TestConfiguration.OpenAI.ApiKey == null)
{
this.WriteLine("OpenAI apiKey not found. Skipping example.");
return;
}

var builder =
new AgentBuilder()
.WithOpenAIChatCompletion(OpenAIFunctionEnabledModel, TestConfiguration.OpenAI.ApiKey)
.WithInstructions("Write only code to solve the given problem without comment.");
var builder = CreateAgentBuilder().WithInstructions("Write only code to solve the given problem without comment.");

try
{
var defaultAgent =
Track(
await builder.BuildAsync());
var defaultAgent = Track(await builder.BuildAsync());

var codeInterpreterAgent =
Track(
await builder.WithCodeInterpreter().BuildAsync());
var codeInterpreterAgent = Track(await builder.WithCodeInterpreter().BuildAsync());

await ChatAsync(
defaultAgent,
Expand Down Expand Up @@ -88,7 +85,7 @@ public async Task RunRetrievalToolAsync()
return;
}

var kernel = Kernel.CreateBuilder().AddOpenAIFiles(TestConfiguration.OpenAI.ApiKey).Build();
Kernel kernel = CreateFileEnabledKernel();
var fileService = kernel.GetRequiredService<OpenAIFileService>();
var result =
await fileService.UploadContentAsync(
Expand All @@ -98,18 +95,9 @@ await fileService.UploadContentAsync(
var fileId = result.Id;
this.WriteLine($"! {fileId}");

var defaultAgent =
Track(
await new AgentBuilder()
.WithOpenAIChatCompletion(OpenAIFunctionEnabledModel, TestConfiguration.OpenAI.ApiKey)
.BuildAsync());
var defaultAgent = Track(await CreateAgentBuilder().BuildAsync());

var retrievalAgent =
Track(
await new AgentBuilder()
.WithOpenAIChatCompletion(OpenAIFunctionEnabledModel, TestConfiguration.OpenAI.ApiKey)
.WithRetrieval()
.BuildAsync());
var retrievalAgent = Track(await CreateAgentBuilder().WithRetrieval().BuildAsync());

if (!PassFileOnRequest)
{
Expand Down Expand Up @@ -183,6 +171,22 @@ async Task InvokeAgentAsync(IAgent agent, string question)
}
}

private static Kernel CreateFileEnabledKernel()
{
return
ForceOpenAI || string.IsNullOrEmpty(TestConfiguration.AzureOpenAI.Endpoint) ?
Kernel.CreateBuilder().AddOpenAIFiles(TestConfiguration.OpenAI.ApiKey).Build() :
Kernel.CreateBuilder().AddAzureOpenAIFiles(TestConfiguration.AzureOpenAI.Endpoint, TestConfiguration.AzureOpenAI.ApiKey).Build();
}

private static AgentBuilder CreateAgentBuilder()
{
return
ForceOpenAI || string.IsNullOrEmpty(TestConfiguration.AzureOpenAI.Endpoint) ?
new AgentBuilder().WithOpenAIChatCompletion(OpenAIFunctionEnabledModel, TestConfiguration.OpenAI.ApiKey) :
new AgentBuilder().WithAzureOpenAIChatCompletion(TestConfiguration.AzureOpenAI.Endpoint, TestConfiguration.AzureOpenAI.ChatDeploymentName, TestConfiguration.AzureOpenAI.ApiKey);
}

private IAgent Track(IAgent agent)
{
this._agents.Add(agent);
Expand Down
19 changes: 10 additions & 9 deletions dotnet/samples/KernelSyntaxExamples/Example80_OpenAIFiles.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ public sealed class Example79_OpenAIFiles : BaseTest
{
private const string ResourceFileName = "30-user-context.txt";

/// <summary>
/// Flag to force usage of OpenAI configuration if both <see cref="TestConfiguration.OpenAI"/>
/// and <see cref="TestConfiguration.AzureOpenAI"/> are defined.
/// If 'false', Azure takes precedence.
/// </summary>
private const bool ForceOpenAI = false;

/// <summary>
/// Show how to utilize OpenAI file-service.
/// </summary>
Expand All @@ -26,17 +33,11 @@ public async Task RunFileLifecycleAsync()
{
this.WriteLine("======== OpenAI File-Service ========");

if (TestConfiguration.OpenAI.ApiKey == null)
{
this.WriteLine("OpenAI apiKey not found. Skipping example.");
return;
}

// Initialize file-service
var kernel =
Kernel.CreateBuilder()
.AddOpenAIFiles(TestConfiguration.OpenAI.ApiKey)
.Build();
ForceOpenAI || string.IsNullOrEmpty(TestConfiguration.AzureOpenAI.Endpoint) ?
Kernel.CreateBuilder().AddOpenAIFiles(TestConfiguration.OpenAI.ApiKey).Build() :
Kernel.CreateBuilder().AddAzureOpenAIFiles(TestConfiguration.AzureOpenAI.Endpoint, TestConfiguration.AzureOpenAI.ApiKey).Build();

var fileService = kernel.GetRequiredService<OpenAIFileService>();

Expand Down
46 changes: 29 additions & 17 deletions dotnet/samples/KernelSyntaxExamples/Example85_AgentCharts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ public sealed class Example85_AgentCharts : BaseTest
/// </summary>
private const string OpenAIFunctionEnabledModel = "gpt-4-1106-preview";

/// <summary>
/// Flag to force usage of OpenAI configuration if both <see cref="TestConfiguration.OpenAI"/>
/// and <see cref="TestConfiguration.AzureOpenAI"/> are defined.
/// If 'false', Azure takes precedence.
/// </summary>
private const bool ForceOpenAI = false;

/// <summary>
/// Create a chart and retrieve by file_id.
/// </summary>
Expand All @@ -31,21 +38,9 @@ public async Task CreateChartAsync()
{
this.WriteLine("======== Using CodeInterpreter tool ========");

if (TestConfiguration.OpenAI.ApiKey == null)
{
this.WriteLine("OpenAI apiKey not found. Skipping example.");
return;
}
var fileService = CreateFileService();

this.WriteLine(Environment.CurrentDirectory);

var fileService = new OpenAIFileService(TestConfiguration.OpenAI.ApiKey);

var agent =
await new AgentBuilder()
.WithOpenAIChatCompletion(OpenAIFunctionEnabledModel, TestConfiguration.OpenAI.ApiKey)
.WithCodeInterpreter()
.BuildAsync();
var agent = await CreateAgentBuilder().WithCodeInterpreter().BuildAsync();

try
{
Expand All @@ -54,7 +49,7 @@ public async Task CreateChartAsync()
await InvokeAgentAsync(
thread,
"1-first", @"
Display this data using a bar-chart:
Display this data using a bar-chart with no summation:
Banding Brown Pink Yellow Sum
X00000 339 433 126 898
Expand All @@ -78,12 +73,13 @@ async Task InvokeAgentAsync(IAgentThread thread, string imageName, string questi
if (message.ContentType == ChatMessageType.Image)
{
var filename = $"{imageName}.jpg";
var path = Path.Combine(Environment.CurrentDirectory, filename);
this.WriteLine($"# {message.Role}: {message.Content}");
this.WriteLine($"# {message.Role}: {path}");
var content = fileService.GetFileContent(message.Content);
await using var outputStream = File.OpenWrite(filename);
await using var inputStream = await content.GetStreamAsync();
await inputStream.CopyToAsync(outputStream);
var path = Path.Combine(Environment.CurrentDirectory, filename);
this.WriteLine($"# {message.Role}: {path}");
Process.Start(
new ProcessStartInfo
{
Expand All @@ -101,5 +97,21 @@ async Task InvokeAgentAsync(IAgentThread thread, string imageName, string questi
}
}

private static OpenAIFileService CreateFileService()
{
return
ForceOpenAI || string.IsNullOrEmpty(TestConfiguration.AzureOpenAI.Endpoint) ?
new OpenAIFileService(TestConfiguration.OpenAI.ApiKey) :
new OpenAIFileService(new Uri(TestConfiguration.AzureOpenAI.Endpoint), apiKey: TestConfiguration.AzureOpenAI.ApiKey);
}

private static AgentBuilder CreateAgentBuilder()
{
return
ForceOpenAI || string.IsNullOrEmpty(TestConfiguration.AzureOpenAI.Endpoint) ?
new AgentBuilder().WithOpenAIChatCompletion(OpenAIFunctionEnabledModel, TestConfiguration.OpenAI.ApiKey) :
new AgentBuilder().WithAzureOpenAIChatCompletion(TestConfiguration.AzureOpenAI.Endpoint, TestConfiguration.AzureOpenAI.ChatDeploymentName, TestConfiguration.AzureOpenAI.ApiKey);
}

public Example85_AgentCharts(ITestOutputHelper output) : base(output) { }
}
71 changes: 63 additions & 8 deletions dotnet/src/Connectors/Connectors.OpenAI/Files/OpenAIFileService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,50 @@ namespace Microsoft.SemanticKernel.Connectors.OpenAI;
[Experimental("SKEXP0010")]
public sealed class OpenAIFileService
{
private const string HeaderNameAuthorization = "Authorization";
private const string HeaderNameAzureApiKey = "api-key";
private const string HeaderNameOpenAIAssistant = "OpenAI-Beta";
private const string HeaderNameUserAgent = "User-Agent";
private const string HeaderOpenAIValueAssistant = "assistants=v1";
private const string OpenAIApiEndpoint = "https://api.openai.com/v1/";
private const string OpenAIApiRouteFiles = "files";
private const string AzureOpenAIApiRouteFiles = "openai/files";
private const string AzureOpenAIDefaultVersion = "2024-02-15-preview";

private readonly string _apiKey;
private readonly HttpClient _httpClient;
private readonly ILogger _logger;
private readonly Uri _serviceUri;
private readonly string? _version;
private readonly string? _organization;

/// <summary>
/// Create an instance of the Azure OpenAI chat completion connector
/// </summary>
/// <param name="endpoint">Azure Endpoint URL</param>
/// <param name="apiKey">Azure OpenAI API Key</param>
/// <param name="organization">OpenAI Organization Id (usually optional)</param>
/// <param name="version">The API version to target.</param>
/// <param name="httpClient">Custom <see cref="HttpClient"/> for HTTP requests.</param>
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/> to use for logging. If null, no logging will be performed.</param>
public OpenAIFileService(
Uri endpoint,
string apiKey,
string? organization = null,
string? version = null,
HttpClient? httpClient = null,
ILoggerFactory? loggerFactory = null)
{
Verify.NotNull(apiKey, nameof(apiKey));

this._apiKey = apiKey;
this._logger = loggerFactory?.CreateLogger(typeof(OpenAIFileService)) ?? NullLogger.Instance;
this._httpClient = HttpClientProvider.GetHttpClient(httpClient);
this._serviceUri = new Uri(this._httpClient.BaseAddress ?? endpoint, AzureOpenAIApiRouteFiles);
this._version = version ?? AzureOpenAIDefaultVersion;
this._organization = organization;
}

/// <summary>
/// Create an instance of the OpenAI chat completion connector
/// </summary>
Expand Down Expand Up @@ -86,7 +121,7 @@ public BinaryContent GetFileContent(string id, CancellationToken cancellationTok
/// </summary>
/// <param name="id">The uploaded file identifier.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>Thet metadata associated with the specified file identifier.</returns>
/// <returns>The metadata associated with the specified file identifier.</returns>
public async Task<OpenAIFileReference> GetFileAsync(string id, CancellationToken cancellationToken = default)
{
Verify.NotNull(id, nameof(id));
Expand All @@ -100,7 +135,7 @@ public async Task<OpenAIFileReference> GetFileAsync(string id, CancellationToken
/// Retrieve metadata for all previously uploaded files.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>Thet metadata of all uploaded files.</returns>
/// <returns>The metadata of all uploaded files.</returns>
public async Task<IEnumerable<OpenAIFileReference>> GetFilesAsync(CancellationToken cancellationToken = default)
{
var result = await this.ExecuteGetRequestAsync<FileInfoList>(this._serviceUri.ToString(), cancellationToken).ConfigureAwait(false);
Expand Down Expand Up @@ -133,14 +168,14 @@ public async Task<OpenAIFileReference> UploadContentAsync(BinaryContent fileCont

private async Task ExecuteDeleteRequestAsync(string url, CancellationToken cancellationToken)
{
using var request = HttpRequest.CreateDeleteRequest(url);
using var request = HttpRequest.CreateDeleteRequest(this.PrepareUrl(url));
this.AddRequestHeaders(request);
using var _ = await this._httpClient.SendWithSuccessCheckAsync(request, cancellationToken).ConfigureAwait(false);
}

private async Task<TModel> ExecuteGetRequestAsync<TModel>(string url, CancellationToken cancellationToken)
{
using var request = HttpRequest.CreateGetRequest(url);
using var request = HttpRequest.CreateGetRequest(this.PrepareUrl(url));
this.AddRequestHeaders(request);
using var response = await this._httpClient.SendWithSuccessCheckAsync(request, cancellationToken).ConfigureAwait(false);

Expand All @@ -158,7 +193,7 @@ private async Task<TModel> ExecuteGetRequestAsync<TModel>(string url, Cancellati

private async Task<Stream> StreamGetRequestAsync(string url, CancellationToken cancellationToken)
{
using var request = HttpRequest.CreateGetRequest(url);
using var request = HttpRequest.CreateGetRequest(this.PrepareUrl(url));
this.AddRequestHeaders(request);
var response = await this._httpClient.SendWithSuccessCheckAsync(request, cancellationToken).ConfigureAwait(false);
try
Expand All @@ -177,7 +212,7 @@ await response.Content.ReadAsStreamAndTranslateExceptionAsync().ConfigureAwait(f

private async Task<TModel> ExecutePostRequestAsync<TModel>(string url, HttpContent payload, CancellationToken cancellationToken)
{
using var request = new HttpRequestMessage(HttpMethod.Post, url) { Content = payload };
using var request = new HttpRequestMessage(HttpMethod.Post, this.PrepareUrl(url)) { Content = payload };
this.AddRequestHeaders(request);
using var response = await this._httpClient.SendWithSuccessCheckAsync(request, cancellationToken).ConfigureAwait(false);

Expand All @@ -193,12 +228,32 @@ private async Task<TModel> ExecutePostRequestAsync<TModel>(string url, HttpConte
};
}

private string PrepareUrl(string url)
{
if (string.IsNullOrWhiteSpace(this._version))
{
return url;
}

return $"{url}?api-version={this._version}";
}

private void AddRequestHeaders(HttpRequestMessage request)
{
request.Headers.Add("User-Agent", HttpHeaderConstant.Values.UserAgent);
request.Headers.Add("Authorization", $"Bearer {this._apiKey}");
request.Headers.Add(HeaderNameOpenAIAssistant, HeaderOpenAIValueAssistant);
request.Headers.Add(HeaderNameUserAgent, HttpHeaderConstant.Values.UserAgent);
request.Headers.Add(HttpHeaderConstant.Names.SemanticKernelVersion, HttpHeaderConstant.Values.GetAssemblyVersion(typeof(OpenAIFileService)));

if (!string.IsNullOrWhiteSpace(this._version))
{
// Azure OpenAI
request.Headers.Add(HeaderNameAzureApiKey, this._apiKey);
return;
}

// OpenAI
request.Headers.Add(HeaderNameAuthorization, $"Bearer {this._apiKey}");

if (!string.IsNullOrEmpty(this._organization))
{
this._httpClient.DefaultRequestHeaders.Add(OpenAIClientCore.OrganizationKey, this._organization);
Expand Down
Loading

0 comments on commit 0de7d34

Please sign in to comment.