From 70c34033ce7ef6c70c345e993295911ce47febef Mon Sep 17 00:00:00 2001
From: Chris <66376200+crickman@users.noreply.github.com>
Date: Tue, 13 Feb 2024 10:27:25 -0800
Subject: [PATCH 001/375] .Net - OpenAI File Service ADR (#4803)
### Motivation and Context
ADR in alignment with
https://github.com/microsoft/semantic-kernel/issues/4558
Adding support for **OpenAI File Service** to be used in conjunction
with Agent/Assistant API:
https://platform.openai.com/docs/api-reference/files
### Description
OpenAI provides a file service for uploading files to be used for
*assistant retrieval* or *model fine-tuning*:
`https://api.openai.com/v1/files`
> Note: *Azure Open AI* does not currently support the OpenAI file
service API.
### Contribution Checklist
- [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 :smile:
---
docs/decisions/0026-file-service.md | 144 ++++++++++++++++++++++++++++
1 file changed, 144 insertions(+)
create mode 100644 docs/decisions/0026-file-service.md
diff --git a/docs/decisions/0026-file-service.md b/docs/decisions/0026-file-service.md
new file mode 100644
index 0000000000..7f08866429
--- /dev/null
+++ b/docs/decisions/0026-file-service.md
@@ -0,0 +1,144 @@
+---
+# These are optional elements. Feel free to remove any of them.
+status: proposed
+contact: crickman, mabolan, semenshi
+date: 2024-01-16
+---
+
+# File Services
+
+## Context and Problem Statement
+OpenAI provides a file service for uploading files to be used for *assistant retrieval* or *model fine-tuning*: `https://api.openai.com/v1/files`
+
+Other providers may also offer some type of file-service, such as Gemini.
+
+> Note: *Azure Open AI* does not currently support the OpenAI file service API.
+
+## Considered Options
+
+1. Add OpenAI file service support to `Microsoft.SemanticKernel.Experimental.Agents`
+2. Add a file service abstraction and implement support for OpenAI
+3. Add OpenAI file service support without abstraction
+
+## Decision Outcome
+
+> Option 3. **Add OpenAI file service support without abstraction**
+> Mark code as experimental using label: `SKEXP0015`
+
+Defining a generalized file service interface provides an extensibility point for other vendors, in addition to *OpenAI*.
+
+## Pros and Cons of the Options
+
+### Option 1. Add OpenAI file service support to `Microsoft.SemanticKernel.Experimental.Agents`
+**Pro:**
+1. No impact to existing AI connectors.
+
+**Con:**
+1. No reuse via AI connectors.
+1. No common abstraction.
+1. Unnatural dependency binding for uses other than with OpenAI assistants.
+
+### Option 2. Add a file service abstraction and implement support for OpenAI
+**Pro:**
+1. Defines a common interface for file service interactions.
+1. Allows for specialization for vendor specific services.
+
+**Con:**
+1. Other systems may diverge from existing assumptions.
+
+
+### Option 3. Add OpenAI file service support without abstraction
+**Pro:**
+1. Provides support for OpenAI file-service.
+
+**Con:**
+1. File service offerings from other vendors supported case-by-case without commonality.
+
+
+## More Information
+
+### Signature of BinaryContent
+
+> Note: `BinaryContent` object able to provide either `BinaryData` or `Stream` regardless of which constructor is invoked.
+
+#### `Microsoft.SemanticKernel.Abstractions`
+
+```csharp
+namespace Microsoft.SemanticKernel;
+
+///
+/// Represents binary content.
+///
+public sealed class BinaryContent : KernelContent
+{
+ public BinaryContent(
+ BinaryData content,
+ string? modelId = null,
+ object? innerContent = null,
+ IReadOnlyDictionary? metadata = null);
+
+ public BinaryContent(
+ Func streamProvider,
+ string? modelId = null,
+ object? innerContent = null,
+ IReadOnlyDictionary? metadata = null);
+
+ public Task GetContentAsync();
+
+ public Task GetStreamAsync();
+}
+```
+### Signatures for Option 3:
+
+#### `Microsoft.SemanticKernel.Connectors.OpenAI`
+```csharp
+namespace Microsoft.SemanticKernel.Connectors.OpenAI;
+
+public sealed class OpenAIFileService
+{
+ public async Task GetFileAsync(
+ string id,
+ CancellationToken cancellationToken = default);
+
+ public async Task> GetFilesAsync(CancellationToken cancellationToken = default);
+
+ public async Task GetFileContentAsync(
+ string id,
+ CancellationToken cancellationToken = default);
+
+ public async Task DeleteFileAsync(
+ string id,
+ CancellationToken cancellationToken = default);
+
+ public async Task UploadContentAsync(
+ BinaryContent content,
+ OpenAIFileUploadExecutionSettings settings,
+ CancellationToken cancellationToken = default);
+}
+
+public sealed class OpenAIFileUploadExecutionSettings
+{
+ public string FileName { get; }
+
+ public OpenAIFilePurpose Purpose { get; }
+}
+
+public sealed class OpenAIFileReference
+{
+ public string Id { get; set; }
+
+ public DateTime CreatedTimestamp { get; set; }
+
+ public string FileName { get; set; }
+
+ public OpenAIFilePurpose Purpose { get; set; }
+
+ public int SizeInBytes { get; set; }
+}
+
+public enum OpenAIFilePurpose
+{
+ Assistants,
+ Finetuning,
+}
+```
From 182917f752209d9ec37016f82e10ef40cb590790 Mon Sep 17 00:00:00 2001
From: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com>
Date: Tue, 13 Feb 2024 20:28:07 +0000
Subject: [PATCH 002/375] .Net: Fixed Azure AI Search asynchronous operation
execution (#4992)
### Motivation and Context
Resolves: https://github.com/microsoft/semantic-kernel/issues/4952
Currently, in Azure AI Search memory store, asynchronous operation (e.g.
document search, index creation) is not executed asynchronously. This
causes the problems on exception handling. For example, when trying to
get record with `GetAsync` method in non-existent index, instead of
returning `null`, as expected by memory store logic, it throws
`RequestFailedException` that comes from Azure AI Search .NET SDK. In
other methods, even in case of `RequestFailedException`, this exception
should be translated to `HttpOperationException` as per decision for
exception handling in all connectors.
### Description
1. Added new constructor in `AzureAISearchMemoryStore` that accepts
`SearchIndexClient` instance for easier mocking in unit tests.
2. Fixed `RunMemoryStoreOperation` method to execute operations
asynchronously.
3. Added overload for `RunMemoryStoreOperation` method to handle
`IAsyncEnumerable` response.
4. Added new project for Azure AI Search unit tests -
`Connectors.AzureAISearch.UnitTests`.
5. Added unit tests for basic scenarios in `AzureAISearchMemoryStore`
including exception handling.
### Contribution Checklist
- [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 :smile:
---
dotnet/Directory.Packages.props | 2 +-
dotnet/SK-dotnet.sln | 9 +
.../.editorconfig | 6 +
.../AzureAISearchMemoryStoreTests.cs | 419 ++++++++++++++++++
.../Connectors.AzureAISearch.UnitTests.csproj | 44 ++
.../AzureAISearchMemoryStore.cs | 89 ++--
.../Connectors.Memory.AzureAISearch.csproj | 4 +
7 files changed, 539 insertions(+), 34 deletions(-)
create mode 100644 dotnet/src/Connectors/Connectors.AzureAISearch.UnitTests/.editorconfig
create mode 100644 dotnet/src/Connectors/Connectors.AzureAISearch.UnitTests/AzureAISearchMemoryStoreTests.cs
create mode 100644 dotnet/src/Connectors/Connectors.AzureAISearch.UnitTests/Connectors.AzureAISearch.UnitTests.csproj
diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props
index d6a12595f9..98e06b7449 100644
--- a/dotnet/Directory.Packages.props
+++ b/dotnet/Directory.Packages.props
@@ -78,7 +78,7 @@
-
+
all
diff --git a/dotnet/SK-dotnet.sln b/dotnet/SK-dotnet.sln
index 701b487d0e..fd5ceef50c 100644
--- a/dotnet/SK-dotnet.sln
+++ b/dotnet/SK-dotnet.sln
@@ -219,6 +219,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Linq", "Linq", "{607DD6FA-F
src\InternalUtilities\test\Linq\AsyncEnumerable.cs = src\InternalUtilities\test\Linq\AsyncEnumerable.cs
EndProjectSection
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Connectors.AzureAISearch.UnitTests", "src\Connectors\Connectors.AzureAISearch.UnitTests\Connectors.AzureAISearch.UnitTests.csproj", "{BCDD5B96-CCC3-46B9-8217-89CD5885F6A2}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -500,6 +502,12 @@ Global
{95CAA25F-A0DE-4A5B-92BA-7D56C0E822A8}.Publish|Any CPU.Build.0 = Publish|Any CPU
{95CAA25F-A0DE-4A5B-92BA-7D56C0E822A8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{95CAA25F-A0DE-4A5B-92BA-7D56C0E822A8}.Release|Any CPU.Build.0 = Release|Any CPU
+ {BCDD5B96-CCC3-46B9-8217-89CD5885F6A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {BCDD5B96-CCC3-46B9-8217-89CD5885F6A2}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {BCDD5B96-CCC3-46B9-8217-89CD5885F6A2}.Publish|Any CPU.ActiveCfg = Debug|Any CPU
+ {BCDD5B96-CCC3-46B9-8217-89CD5885F6A2}.Publish|Any CPU.Build.0 = Debug|Any CPU
+ {BCDD5B96-CCC3-46B9-8217-89CD5885F6A2}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {BCDD5B96-CCC3-46B9-8217-89CD5885F6A2}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -573,6 +581,7 @@ Global
{95CAA25F-A0DE-4A5B-92BA-7D56C0E822A8} = {9ECD1AA0-75B3-4E25-B0B5-9F0945B64974}
{EB2C141A-AE5F-4080-8790-13EB16323CEF} = {958AD708-F048-4FAF-94ED-D2F2B92748B9}
{607DD6FA-FA0D-45E6-80BA-22A373609E89} = {5C246969-D794-4EC3-8E8F-F90D4D166420}
+ {BCDD5B96-CCC3-46B9-8217-89CD5885F6A2} = {0247C2C9-86C3-45BA-8873-28B0948EDC0C}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {FBDC56A3-86AD-4323-AA0F-201E59123B83}
diff --git a/dotnet/src/Connectors/Connectors.AzureAISearch.UnitTests/.editorconfig b/dotnet/src/Connectors/Connectors.AzureAISearch.UnitTests/.editorconfig
new file mode 100644
index 0000000000..394eef685f
--- /dev/null
+++ b/dotnet/src/Connectors/Connectors.AzureAISearch.UnitTests/.editorconfig
@@ -0,0 +1,6 @@
+# Suppressing errors for Test projects under dotnet folder
+[*.cs]
+dotnet_diagnostic.CA2007.severity = none # Do not directly await a Task
+dotnet_diagnostic.VSTHRD111.severity = none # Use .ConfigureAwait(bool) is hidden by default, set to none to prevent IDE from changing on autosave
+dotnet_diagnostic.CS1591.severity = none # Missing XML comment for publicly visible type or member
+dotnet_diagnostic.IDE1006.severity = warning # Naming rule violations
diff --git a/dotnet/src/Connectors/Connectors.AzureAISearch.UnitTests/AzureAISearchMemoryStoreTests.cs b/dotnet/src/Connectors/Connectors.AzureAISearch.UnitTests/AzureAISearchMemoryStoreTests.cs
new file mode 100644
index 0000000000..5ebab857b3
--- /dev/null
+++ b/dotnet/src/Connectors/Connectors.AzureAISearch.UnitTests/AzureAISearchMemoryStoreTests.cs
@@ -0,0 +1,419 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.Net;
+using System.Threading;
+using System.Threading.Tasks;
+using Azure;
+using Azure.Search.Documents;
+using Azure.Search.Documents.Indexes;
+using Azure.Search.Documents.Indexes.Models;
+using Azure.Search.Documents.Models;
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.Connectors.AzureAISearch;
+using Microsoft.SemanticKernel.Memory;
+using Moq;
+using Xunit;
+
+namespace SemanticKernel.Connectors.UnitTests.Memory.AzureAISearch;
+
+///
+/// Unit tests for class.
+///
+public sealed class AzureAISearchMemoryStoreTests
+{
+ private readonly Mock _mockSearchIndexClient = new();
+ private readonly Mock _mockSearchClient = new();
+
+ private readonly AzureAISearchMemoryStore _service;
+
+ public AzureAISearchMemoryStoreTests()
+ {
+ this._mockSearchIndexClient
+ .Setup(x => x.GetSearchClient(It.IsAny()))
+ .Returns(this._mockSearchClient.Object);
+
+ this._service = new AzureAISearchMemoryStore(this._mockSearchIndexClient.Object);
+ }
+
+ [Fact]
+ public async Task GetCollectionsReturnsIndexNamesAsync()
+ {
+ // Arrange
+ Page page = Page.FromValues(new[]
+ {
+ new SearchIndex("index-1"),
+ new SearchIndex("index-2"),
+ }, null, Mock.Of());
+
+ var pageable = AsyncPageable.FromPages(new[] { page });
+
+ this._mockSearchIndexClient
+ .Setup(x => x.GetIndexesAsync(It.IsAny()))
+ .Returns(pageable);
+
+ // Act
+ var indexes = new List();
+
+ await foreach (var index in this._service.GetCollectionsAsync())
+ {
+ indexes.Add(index);
+ }
+
+ // Assert
+ Assert.Equal("index-1", indexes[0]);
+ Assert.Equal("index-2", indexes[1]);
+ }
+
+ [Fact]
+ public async Task GetCollectionsOnErrorThrowsHttpOperationExceptionAsync()
+ {
+ // Arrange
+ this._mockSearchIndexClient
+ .Setup(x => x.GetIndexesAsync(It.IsAny()))
+ .Throws(new RequestFailedException((int)HttpStatusCode.InternalServerError, "test error response"));
+
+ // Act & Assert
+ var indexes = new List();
+
+ var exception = await Assert.ThrowsAsync(async () =>
+ {
+ await foreach (var index in this._service.GetCollectionsAsync())
+ {
+ }
+ });
+
+ Assert.Equal("test error response", exception.Message);
+ Assert.Equal(HttpStatusCode.InternalServerError, exception.StatusCode);
+ }
+
+ [Theory]
+ [InlineData("index-1", true)]
+ [InlineData("index-2", true)]
+ [InlineData("index-3", false)]
+ public async Task DoesCollectionExistReturnsValidResultAsync(string collectionName, bool expectedResult)
+ {
+ // Arrange
+ Page page = Page.FromValues(new[]
+ {
+ new SearchIndex("index-1"),
+ new SearchIndex("index-2"),
+ }, null, Mock.Of());
+
+ var pageable = AsyncPageable.FromPages(new[] { page });
+
+ this._mockSearchIndexClient
+ .Setup(x => x.GetIndexesAsync(It.IsAny()))
+ .Returns(pageable);
+
+ // Act
+ var result = await this._service.DoesCollectionExistAsync(collectionName);
+
+ // Assert
+ Assert.Equal(expectedResult, result);
+ }
+
+ [Fact]
+ public async Task DoesCollectionExistOnErrorThrowsHttpOperationExceptionAsync()
+ {
+ // Arrange
+ this._mockSearchIndexClient
+ .Setup(x => x.GetIndexesAsync(It.IsAny()))
+ .Throws(new RequestFailedException((int)HttpStatusCode.InternalServerError, "test error response"));
+
+ // Act
+ var exception = await Assert.ThrowsAsync(() => this._service.DoesCollectionExistAsync("test-index"));
+
+ // Assert
+ Assert.Equal("test error response", exception.Message);
+ Assert.Equal(HttpStatusCode.InternalServerError, exception.StatusCode);
+ }
+
+ [Fact]
+ public async Task DeleteCollectionCallsDeleteIndexMethodAsync()
+ {
+ // Arrange
+ this._mockSearchIndexClient
+ .Setup(x => x.DeleteIndexAsync(It.IsAny(), It.IsAny()))
+ .ReturnsAsync(It.IsAny());
+
+ // Act
+ await this._service.DeleteCollectionAsync("test-index");
+
+ // Assert
+ this._mockSearchIndexClient.Verify(x => x.DeleteIndexAsync("test-index", It.IsAny()), Times.Once());
+ }
+
+ [Fact]
+ public async Task DeleteCollectionOnErrorThrowsHttpOperationExceptionAsync()
+ {
+ // Arrange
+ this._mockSearchIndexClient
+ .Setup(x => x.DeleteIndexAsync(It.IsAny(), It.IsAny()))
+ .Throws(new RequestFailedException((int)HttpStatusCode.InternalServerError, "test error response"));
+
+ // Act
+ var exception = await Assert.ThrowsAsync(() => this._service.DeleteCollectionAsync("test-index"));
+
+ // Assert
+ Assert.Equal("test error response", exception.Message);
+ Assert.Equal(HttpStatusCode.InternalServerError, exception.StatusCode);
+ }
+
+ [Fact]
+ public async Task UpsertReturnsValidRecordKeyAsync()
+ {
+ // Arrange
+ var indexingResult = SearchModelFactory.IndexingResult("record-id", null, true, 200);
+ var results = SearchModelFactory.IndexDocumentsResult(new[] { indexingResult });
+
+ this._mockSearchClient
+ .Setup(x => x.IndexDocumentsAsync(
+ It.IsAny>(),
+ It.IsAny(),
+ It.IsAny()))
+ .ReturnsAsync(Response.FromValue(results, Mock.Of()));
+
+ // Act
+ var result = await this._service.UpsertAsync("test-index", this.GetTestMemoryRecord("record-id"));
+
+ // Assert
+ Assert.Equal("record-id", result);
+ }
+
+ [Fact]
+ public async Task UpsertOnInternalServerErrorThrowsHttpOperationExceptionAsync()
+ {
+ // Arrange
+ this._mockSearchClient
+ .Setup(x => x.IndexDocumentsAsync(
+ It.IsAny>(),
+ It.IsAny(),
+ It.IsAny()))
+ .Throws(new RequestFailedException((int)HttpStatusCode.InternalServerError, "test error response"));
+
+ // Act
+ var exception = await Assert.ThrowsAsync(() => this._service.UpsertAsync("test-index", this.GetTestMemoryRecord("record-id")));
+
+ // Assert
+ Assert.Equal("test error response", exception.Message);
+ Assert.Equal(HttpStatusCode.InternalServerError, exception.StatusCode);
+ }
+
+ [Fact]
+ public async Task UpsertOnNotFoundErrorCreatesIndexAsync()
+ {
+ // Arrange
+ var indexingResult = SearchModelFactory.IndexingResult("record-id", null, true, 200);
+ var results = SearchModelFactory.IndexDocumentsResult(new[] { indexingResult });
+
+ this._mockSearchClient
+ .SetupSequence(x => x.IndexDocumentsAsync(
+ It.IsAny>(),
+ It.IsAny(),
+ It.IsAny()))
+ .Throws(new RequestFailedException((int)HttpStatusCode.NotFound, "not found"))
+ .ReturnsAsync(Response.FromValue(results, Mock.Of()));
+
+ this._mockSearchIndexClient
+ .Setup(x => x.CreateIndexAsync(It.IsAny(), It.IsAny()))
+ .ReturnsAsync(Response.FromValue(new SearchIndex("test-index"), Mock.Of()));
+
+ // Act
+ var result = await this._service.UpsertAsync("test-index", this.GetTestMemoryRecord("record-id"));
+
+ // Assert
+ Assert.Equal("record-id", result);
+ this._mockSearchIndexClient
+ .Verify(x => x.CreateIndexAsync(It.IsAny(), It.IsAny()), Times.Once());
+ }
+
+ [Fact]
+ public async Task GetReturnsValidRecordAsync()
+ {
+ // Arrange
+ this._mockSearchClient
+ .Setup(x => x.GetDocumentAsync(It.IsAny(), It.IsAny(), It.IsAny()))
+ .ReturnsAsync(Response.FromValue(new AzureAISearchMemoryRecord("record-id"), Mock.Of()));
+
+ // Act
+ var result = await this._service.GetAsync("test-index", "record-id");
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal(AzureAISearchMemoryRecord.EncodeId("record-id"), result.Key);
+ }
+
+ [Fact]
+ public async Task GetReturnsNullWhenRecordDoesNotExistAsync()
+ {
+ // Arrange
+ this._mockSearchClient
+ .Setup(x => x.GetDocumentAsync(It.IsAny(), It.IsAny(), It.IsAny()))
+ .ThrowsAsync(new RequestFailedException((int)HttpStatusCode.NotFound, "test error response"));
+
+ // Act
+ var result = await this._service.GetAsync("test-collection", "test-record");
+
+ // Assert
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public async Task GetNearestMatchesReturnsValidRecordAsync()
+ {
+ // Arrange
+ var searchMemoryRecord = new AzureAISearchMemoryRecord("record-id");
+ var searchResult = SearchModelFactory.SearchResult(searchMemoryRecord, 0.8, null);
+ var results = SearchModelFactory.SearchResults([searchResult], 1, null, 0.9, Mock.Of());
+
+ this._mockSearchClient
+ .Setup(x => x.SearchAsync(It.IsAny(), It.IsAny(), It.IsAny()))
+ .ReturnsAsync(Response.FromValue(results, Mock.Of()));
+
+ ReadOnlyMemory embedding = new[] { 1f, 3f, 5f };
+
+ // Act
+ var recordKeys = new List();
+
+ await foreach (var (record, score) in this._service.GetNearestMatchesAsync("test-index", embedding, 1))
+ {
+ recordKeys.Add(record.Key);
+ }
+
+ // Assert
+ Assert.Equal(AzureAISearchMemoryRecord.EncodeId("record-id"), recordKeys[0]);
+ }
+
+ [Fact]
+ public async Task GetNearestMatchesOnInternalServerErrorThrowsHttpOperationExceptionAsync()
+ {
+ // Arrange
+ this._mockSearchClient
+ .Setup(x => x.SearchAsync(It.IsAny(), It.IsAny(), It.IsAny()))
+ .Throws(new RequestFailedException((int)HttpStatusCode.InternalServerError, "test error response"));
+
+ ReadOnlyMemory embedding = new[] { 1f, 3f, 5f };
+
+ // Act
+ var exception = await Assert.ThrowsAsync(async () =>
+ {
+ await foreach (var (record, score) in this._service.GetNearestMatchesAsync("test-index", embedding, 1))
+ {
+ }
+ });
+
+ // Assert
+ Assert.Equal("test error response", exception.Message);
+ Assert.Equal(HttpStatusCode.InternalServerError, exception.StatusCode);
+ }
+
+ [Fact]
+ public async Task GetNearestMatchesOnNotFoundErrorReturnsEmptyAsync()
+ {
+ // Arrange
+ this._mockSearchClient
+ .Setup(x => x.SearchAsync(It.IsAny(), It.IsAny(), It.IsAny()))
+ .Throws(new RequestFailedException((int)HttpStatusCode.NotFound, "not found"));
+
+ ReadOnlyMemory embedding = new[] { 1f, 3f, 5f };
+
+ // Act
+ var recordKeys = new List();
+
+ await foreach (var (record, score) in this._service.GetNearestMatchesAsync("test-index", embedding, 1))
+ {
+ recordKeys.Add(record.Key);
+ }
+
+ // Assert
+ Assert.Empty(recordKeys);
+ }
+
+ [Fact]
+ public async Task RemoveBatchCallsDeleteDocumentsMethodAsync()
+ {
+ // Arrange
+ var indexingResult = SearchModelFactory.IndexingResult("record-id", null, true, 200);
+ var results = SearchModelFactory.IndexDocumentsResult(new[] { indexingResult });
+
+ this._mockSearchClient
+ .Setup(x => x.DeleteDocumentsAsync(
+ It.IsAny>(),
+ It.IsAny(),
+ It.IsAny()))
+ .ReturnsAsync(Response.FromValue(results, Mock.Of()));
+
+ // Act
+ var exception = await Record.ExceptionAsync(() => this._service.RemoveBatchAsync("text-index", ["record-id"]));
+
+ // Assert
+ Assert.Null(exception);
+
+ this._mockSearchClient
+ .Verify(x => x.DeleteDocumentsAsync(
+ It.IsAny>(),
+ It.IsAny(),
+ It.IsAny()), Times.Once());
+ }
+
+ [Fact]
+ public async Task RemoveBatchOnNotFoundErrorDoesNotThrowExceptionAsync()
+ {
+ // Arrange
+ this._mockSearchClient
+ .Setup(x => x.DeleteDocumentsAsync(
+ It.IsAny>(),
+ It.IsAny(),
+ It.IsAny()))
+ .Throws(new RequestFailedException((int)HttpStatusCode.NotFound, "not found"));
+
+ // Act
+ var exception = await Record.ExceptionAsync(() => this._service.RemoveBatchAsync("text-index", ["record-id"]));
+
+ // Assert
+ Assert.Null(exception);
+
+ this._mockSearchClient
+ .Verify(x => x.DeleteDocumentsAsync(
+ It.IsAny>(),
+ It.IsAny(),
+ It.IsAny()), Times.Once());
+ }
+
+ [Fact]
+ public async Task RemoveBatchOnInternalServerErrorThrowsHttpOperationExceptionAsync()
+ {
+ // Arrange
+ this._mockSearchClient
+ .Setup(x => x.DeleteDocumentsAsync(
+ It.IsAny>(),
+ It.IsAny(),
+ It.IsAny()))
+ .Throws(new RequestFailedException((int)HttpStatusCode.InternalServerError, "test error response"));
+
+ // Act
+ var exception = await Assert.ThrowsAsync(() => this._service.RemoveBatchAsync("text-index", ["record-id"]));
+
+ // Assert
+ Assert.Equal("test error response", exception.Message);
+ Assert.Equal(HttpStatusCode.InternalServerError, exception.StatusCode);
+
+ this._mockSearchClient
+ .Verify(x => x.DeleteDocumentsAsync(
+ It.IsAny>(),
+ It.IsAny(),
+ It.IsAny()), Times.Once());
+ }
+
+ #region private
+
+ private MemoryRecord GetTestMemoryRecord(string id)
+ {
+ ReadOnlyMemory embedding = new[] { 1f, 3f, 5f };
+ return MemoryRecord.LocalRecord(id, "text", "description", embedding);
+ }
+
+ #endregion
+}
diff --git a/dotnet/src/Connectors/Connectors.AzureAISearch.UnitTests/Connectors.AzureAISearch.UnitTests.csproj b/dotnet/src/Connectors/Connectors.AzureAISearch.UnitTests/Connectors.AzureAISearch.UnitTests.csproj
new file mode 100644
index 0000000000..cfe061ff79
--- /dev/null
+++ b/dotnet/src/Connectors/Connectors.AzureAISearch.UnitTests/Connectors.AzureAISearch.UnitTests.csproj
@@ -0,0 +1,44 @@
+
+
+
+ SemanticKernel.Connectors.AzureAISearch.UnitTests
+ SemanticKernel.Connectors.AzureAISearch.UnitTests
+ net6.0
+ 12
+ LatestMajor
+ true
+ enable
+ disable
+ false
+ SKEXP0003,SKEXP0021
+
+
+
+
+
+
+
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/src/Connectors/Connectors.Memory.AzureAISearch/AzureAISearchMemoryStore.cs b/dotnet/src/Connectors/Connectors.Memory.AzureAISearch/AzureAISearchMemoryStore.cs
index b02a4ae438..1764db93fb 100644
--- a/dotnet/src/Connectors/Connectors.Memory.AzureAISearch/AzureAISearchMemoryStore.cs
+++ b/dotnet/src/Connectors/Connectors.Memory.AzureAISearch/AzureAISearchMemoryStore.cs
@@ -31,9 +31,8 @@ public class AzureAISearchMemoryStore : IMemoryStore
/// Azure AI Search URI, e.g. "https://contoso.search.windows.net"
/// API Key
public AzureAISearchMemoryStore(string endpoint, string apiKey)
+ : this(new SearchIndexClient(new Uri(endpoint), new AzureKeyCredential(apiKey), GetClientOptions()))
{
- AzureKeyCredential credentials = new(apiKey);
- this._adminClient = new SearchIndexClient(new Uri(endpoint), credentials, GetClientOptions());
}
///
@@ -42,8 +41,17 @@ public class AzureAISearchMemoryStore : IMemoryStore
/// Azure AI Search URI, e.g. "https://contoso.search.windows.net"
/// Azure service
public AzureAISearchMemoryStore(string endpoint, TokenCredential credentials)
+ : this(new SearchIndexClient(new Uri(endpoint), credentials, GetClientOptions()))
{
- this._adminClient = new SearchIndexClient(new Uri(endpoint), credentials, GetClientOptions());
+ }
+
+ ///
+ /// Create a new instance of memory storage using Azure AI Search.
+ ///
+ /// Azure AI Search client that can be used to manage indexes on a Search service.
+ public AzureAISearchMemoryStore(SearchIndexClient searchIndexClient)
+ {
+ this._adminClient = searchIndexClient;
}
///
@@ -56,7 +64,7 @@ public class AzureAISearchMemoryStore : IMemoryStore
///
public IAsyncEnumerable GetCollectionsAsync(CancellationToken cancellationToken = default)
{
- return RunMemoryStoreOperation(() => this.GetIndexesAsync(cancellationToken));
+ return RunMemoryStoreOperationAsync(() => this.GetIndexesAsync(cancellationToken));
}
///
@@ -64,7 +72,7 @@ public class AzureAISearchMemoryStore : IMemoryStore
{
var normalizedIndexName = this.NormalizeIndexName(collectionName);
- var indexes = RunMemoryStoreOperation(() => this.GetIndexesAsync(cancellationToken));
+ var indexes = RunMemoryStoreOperationAsync(() => this.GetIndexesAsync(cancellationToken));
return await indexes
.AnyAsync(index =>
@@ -76,19 +84,21 @@ public class AzureAISearchMemoryStore : IMemoryStore
}
///
- public Task DeleteCollectionAsync(string collectionName, CancellationToken cancellationToken = default)
+ public async Task DeleteCollectionAsync(string collectionName, CancellationToken cancellationToken = default)
{
var normalizedIndexName = this.NormalizeIndexName(collectionName);
- return RunMemoryStoreOperation(() => this._adminClient.DeleteIndexAsync(normalizedIndexName, cancellationToken));
+ await RunMemoryStoreOperationAsync(() => this._adminClient.DeleteIndexAsync(normalizedIndexName, cancellationToken))
+ .ConfigureAwait(false);
}
///
- public Task UpsertAsync(string collectionName, MemoryRecord record, CancellationToken cancellationToken = default)
+ public async Task UpsertAsync(string collectionName, MemoryRecord record, CancellationToken cancellationToken = default)
{
var normalizedIndexName = this.NormalizeIndexName(collectionName);
- return RunMemoryStoreOperation(() => this.UpsertRecordAsync(normalizedIndexName, AzureAISearchMemoryRecord.FromMemoryRecord(record), cancellationToken));
+ return await RunMemoryStoreOperationAsync(() => this.UpsertRecordAsync(normalizedIndexName, AzureAISearchMemoryRecord.FromMemoryRecord(record), cancellationToken))
+ .ConfigureAwait(false);
}
///
@@ -98,7 +108,8 @@ public class AzureAISearchMemoryStore : IMemoryStore
var searchRecords = records.Select(AzureAISearchMemoryRecord.FromMemoryRecord).ToList();
- var result = await RunMemoryStoreOperation(() => this.UpsertBatchAsync(normalizedIndexName, searchRecords, cancellationToken)).ConfigureAwait(false);
+ var result = await RunMemoryStoreOperationAsync(() => this.UpsertBatchAsync(normalizedIndexName, searchRecords, cancellationToken))
+ .ConfigureAwait(false);
foreach (var x in result) { yield return x; }
}
@@ -115,12 +126,8 @@ public class AzureAISearchMemoryStore : IMemoryStore
try
{
- result = await RunMemoryStoreOperation(async () =>
- {
- return await client
- .GetDocumentAsync(encodedId, cancellationToken: cancellationToken)
- .ConfigureAwait(false);
- }).ConfigureAwait(false);
+ result = await RunMemoryStoreOperationAsync(() => client.GetDocumentAsync(encodedId, cancellationToken: cancellationToken))
+ .ConfigureAwait(false);
}
catch (HttpOperationException e) when (e.StatusCode == System.Net.HttpStatusCode.NotFound)
{
@@ -196,12 +203,8 @@ public class AzureAISearchMemoryStore : IMemoryStore
Response>? searchResult = null;
try
{
- searchResult = await RunMemoryStoreOperation(async () =>
- {
- return await client
- .SearchAsync(null, options, cancellationToken: cancellationToken)
- .ConfigureAwait(false);
- }).ConfigureAwait(false);
+ searchResult = await RunMemoryStoreOperationAsync(() => client.SearchAsync(null, options, cancellationToken: cancellationToken))
+ .ConfigureAwait(false);
}
catch (HttpOperationException e) when (e.StatusCode == System.Net.HttpStatusCode.NotFound)
{
@@ -222,9 +225,9 @@ public class AzureAISearchMemoryStore : IMemoryStore
}
///
- public Task RemoveAsync(string collectionName, string key, CancellationToken cancellationToken = default)
+ public async Task RemoveAsync(string collectionName, string key, CancellationToken cancellationToken = default)
{
- return this.RemoveBatchAsync(collectionName, new[] { key }, cancellationToken);
+ await this.RemoveBatchAsync(collectionName, new[] { key }, cancellationToken).ConfigureAwait(false);
}
///
@@ -235,9 +238,10 @@ public class AzureAISearchMemoryStore : IMemoryStore
var records = keys.Select(x => new AzureAISearchMemoryRecord(x));
var client = this.GetSearchClient(normalizedIndexName);
+
try
{
- await RunMemoryStoreOperation(() => client.DeleteDocumentsAsync(records, cancellationToken: cancellationToken)).ConfigureAwait(false);
+ await RunMemoryStoreOperationAsync(() => client.DeleteDocumentsAsync(records, cancellationToken: cancellationToken)).ConfigureAwait(false);
}
catch (HttpOperationException e) when (e.StatusCode == System.Net.HttpStatusCode.NotFound)
{
@@ -421,17 +425,11 @@ public class AzureAISearchMemoryStore : IMemoryStore
};
}
- ///
- /// Executes a memory store operation by invoking the provided operation delegate.
- ///
- /// The return type of the operation.
- /// The operation delegate to be executed.
- /// The result of the memory store operation.
- private static T RunMemoryStoreOperation(Func operation)
+ private static async Task RunMemoryStoreOperationAsync(Func> operation)
{
try
{
- return operation.Invoke();
+ return await operation.Invoke().ConfigureAwait(false);
}
catch (RequestFailedException e)
{
@@ -439,6 +437,31 @@ public class AzureAISearchMemoryStore : IMemoryStore
}
}
+ private static async IAsyncEnumerable RunMemoryStoreOperationAsync(Func> operation)
+ {
+ IAsyncEnumerator enumerator = operation.Invoke().GetAsyncEnumerator();
+
+ await using (enumerator.ConfigureAwait(false))
+ {
+ while (true)
+ {
+ try
+ {
+ if (!await enumerator.MoveNextAsync().ConfigureAwait(false))
+ {
+ break;
+ }
+ }
+ catch (RequestFailedException e)
+ {
+ throw e.ToHttpOperationException();
+ }
+
+ yield return enumerator.Current;
+ }
+ }
+ }
+
private static double ScoreToCosineSimilarity(double score)
{
// Azure AI Search score formula. The min value is 0.333 for cosine similarity -1.
diff --git a/dotnet/src/Connectors/Connectors.Memory.AzureAISearch/Connectors.Memory.AzureAISearch.csproj b/dotnet/src/Connectors/Connectors.Memory.AzureAISearch/Connectors.Memory.AzureAISearch.csproj
index 51a5ebac72..f2434708c6 100644
--- a/dotnet/src/Connectors/Connectors.Memory.AzureAISearch/Connectors.Memory.AzureAISearch.csproj
+++ b/dotnet/src/Connectors/Connectors.Memory.AzureAISearch/Connectors.Memory.AzureAISearch.csproj
@@ -24,6 +24,10 @@
+
+
+
+
From 86756033a7404b6bc3e7a05d7aee197ebc0095d7 Mon Sep 17 00:00:00 2001
From: Chris <66376200+crickman@users.noreply.github.com>
Date: Tue, 13 Feb 2024 12:44:27 -0800
Subject: [PATCH 003/375] .Net - Open AI File Service Support (#4868)
### Motivation and Context
Add support for Open AI File Service:
- https://platform.openai.com/docs/api-reference/files
- https://api.openai.com/v1/files
### Description
ADR: https://github.com/microsoft/semantic-kernel/pull/4803
### Contribution Checklist
- [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 :smile:
---
dotnet/Directory.Packages.props | 1 +
dotnet/docs/EXPERIMENTS.md | 1 +
.../Example74_FlowOrchestrator.cs | 15 +-
.../Example75_AgentTools.cs | 39 +--
.../Example79_OpenAIFiles.cs | 72 +++++
.../KernelSyntaxExamples.csproj | 2 +-
.../Resources/EmbeddedResource.cs | 15 +-
.../Files/OpenAIFilePurpose.cs | 22 ++
.../Files/OpenAIFileReference.cs | 38 +++
.../Files/OpenAIFileService.cs | 265 +++++++++++++++++
.../OpenAIFileUploadExecutionSettings.cs | 35 +++
.../OpenAIServiceCollectionExtensions.cs | 62 ++++
.../Connectors.UnitTests.csproj | 2 +-
.../OpenAI/Files/OpenAIFileServiceTests.cs | 266 ++++++++++++++++++
.../Experimental.Assistants.UnitTests.csproj | 49 ----
.../Extensions/KernelExtensionTests.cs | 44 ---
.../KernelFunctionExtensionTests.cs | 52 ----
.../OpenAIRestExtensions.AssistantTests.cs | 67 -----
.../OpenAIRestExtensions.MessagesTests.cs | 69 -----
.../OpenAIRestExtensions.RunTests.cs | 71 -----
.../OpenAIRestExtensions.ThreadTests.cs | 57 ----
.../Integration/AssistantHarness.cs | 148 ----------
.../Integration/RunHarness.cs | 157 -----------
.../Integration/ThreadHarness.cs | 71 -----
.../Assistants.UnitTests/MockExtensions.cs | 21 --
.../Templates/GameAssistant.yaml | 4 -
.../Templates/PoetAssistant.yaml | 4 -
.../Assistants.UnitTests/TestConfig.cs | 29 --
.../Assistants.UnitTests/testsettings.json | 3 -
.../Experimental/Assistants/AssemblyInfo.cs | 6 -
.../Assistants/AssistantBuilder.Static.cs | 59 ----
.../Assistants/AssistantBuilder.cs | 196 -------------
.../Assistants/AssistantPlugin.cs | 38 ---
.../Assistants/AssistantResponse.cs | 29 --
.../Exceptions/AssistantException.cs | 35 ---
.../Assistants/Experimental.Assistants.csproj | 31 --
.../Extensions/AssistantsKernelExtensions.cs | 21 --
.../AssistantsKernelFunctionExtensions.cs | 89 ------
.../OpenAIRestExtensions.Assistant.cs | 136 ---------
.../OpenAIRestExtensions.Messages.cs | 119 --------
.../Extensions/OpenAIRestExtensions.Run.cs | 137 ---------
.../Extensions/OpenAIRestExtensions.Thread.cs | 69 -----
.../Extensions/OpenAIRestExtensions.cs | 91 ------
.../src/Experimental/Assistants/IAssistant.cs | 93 ------
.../Assistants/IAssistantExtensions.cs | 39 ---
.../Experimental/Assistants/IChatMessage.cs | 36 ---
.../Experimental/Assistants/IChatThread.cs | 50 ----
.../Assistants/Internal/Assistant.cs | 227 ---------------
.../Assistants/Internal/ChatMessage.cs | 44 ---
.../Assistants/Internal/ChatRun.cs | 184 ------------
.../Assistants/Internal/ChatThread.cs | 130 ---------
.../Assistants/Internal/OpenAIRestContext.cs | 31 --
.../Models/AssistantConfigurationModel.cs | 54 ----
.../Assistants/Models/AssistantModel.cs | 91 ------
.../Assistants/Models/OpenAIListModel.cs | 45 ---
.../Assistants/Models/OpenAIParameters.cs | 56 ----
.../Assistants/Models/ThreadMessageModel.cs | 123 --------
.../Assistants/Models/ThreadModel.cs | 42 ---
.../Assistants/Models/ThreadRunModel.cs | 139 ---------
.../Assistants/Models/ThreadRunStepModel.cs | 204 --------------
.../Assistants/Models/ToolModel.cs | 49 ----
.../Assistants/Models/ToolResultModel.cs | 24 --
.../src/Http/HttpResponseStream.cs | 69 +++++
.../Contents/BinaryContent.cs | 100 +++++++
.../SemanticKernel.Abstractions.csproj | 1 +
65 files changed, 968 insertions(+), 3600 deletions(-)
create mode 100644 dotnet/samples/KernelSyntaxExamples/Example79_OpenAIFiles.cs
create mode 100644 dotnet/src/Connectors/Connectors.OpenAI/Files/OpenAIFilePurpose.cs
create mode 100644 dotnet/src/Connectors/Connectors.OpenAI/Files/OpenAIFileReference.cs
create mode 100644 dotnet/src/Connectors/Connectors.OpenAI/Files/OpenAIFileService.cs
create mode 100644 dotnet/src/Connectors/Connectors.OpenAI/Files/OpenAIFileUploadExecutionSettings.cs
create mode 100644 dotnet/src/Connectors/Connectors.UnitTests/OpenAI/Files/OpenAIFileServiceTests.cs
delete mode 100644 dotnet/src/Experimental/Assistants.UnitTests/Experimental.Assistants.UnitTests.csproj
delete mode 100644 dotnet/src/Experimental/Assistants.UnitTests/Extensions/KernelExtensionTests.cs
delete mode 100644 dotnet/src/Experimental/Assistants.UnitTests/Extensions/KernelFunctionExtensionTests.cs
delete mode 100644 dotnet/src/Experimental/Assistants.UnitTests/Extensions/OpenAIRestExtensions.AssistantTests.cs
delete mode 100644 dotnet/src/Experimental/Assistants.UnitTests/Extensions/OpenAIRestExtensions.MessagesTests.cs
delete mode 100644 dotnet/src/Experimental/Assistants.UnitTests/Extensions/OpenAIRestExtensions.RunTests.cs
delete mode 100644 dotnet/src/Experimental/Assistants.UnitTests/Extensions/OpenAIRestExtensions.ThreadTests.cs
delete mode 100644 dotnet/src/Experimental/Assistants.UnitTests/Integration/AssistantHarness.cs
delete mode 100644 dotnet/src/Experimental/Assistants.UnitTests/Integration/RunHarness.cs
delete mode 100644 dotnet/src/Experimental/Assistants.UnitTests/Integration/ThreadHarness.cs
delete mode 100644 dotnet/src/Experimental/Assistants.UnitTests/MockExtensions.cs
delete mode 100644 dotnet/src/Experimental/Assistants.UnitTests/Templates/GameAssistant.yaml
delete mode 100644 dotnet/src/Experimental/Assistants.UnitTests/Templates/PoetAssistant.yaml
delete mode 100644 dotnet/src/Experimental/Assistants.UnitTests/TestConfig.cs
delete mode 100644 dotnet/src/Experimental/Assistants.UnitTests/testsettings.json
delete mode 100644 dotnet/src/Experimental/Assistants/AssemblyInfo.cs
delete mode 100644 dotnet/src/Experimental/Assistants/AssistantBuilder.Static.cs
delete mode 100644 dotnet/src/Experimental/Assistants/AssistantBuilder.cs
delete mode 100644 dotnet/src/Experimental/Assistants/AssistantPlugin.cs
delete mode 100644 dotnet/src/Experimental/Assistants/AssistantResponse.cs
delete mode 100644 dotnet/src/Experimental/Assistants/Exceptions/AssistantException.cs
delete mode 100644 dotnet/src/Experimental/Assistants/Experimental.Assistants.csproj
delete mode 100644 dotnet/src/Experimental/Assistants/Extensions/AssistantsKernelExtensions.cs
delete mode 100644 dotnet/src/Experimental/Assistants/Extensions/AssistantsKernelFunctionExtensions.cs
delete mode 100644 dotnet/src/Experimental/Assistants/Extensions/OpenAIRestExtensions.Assistant.cs
delete mode 100644 dotnet/src/Experimental/Assistants/Extensions/OpenAIRestExtensions.Messages.cs
delete mode 100644 dotnet/src/Experimental/Assistants/Extensions/OpenAIRestExtensions.Run.cs
delete mode 100644 dotnet/src/Experimental/Assistants/Extensions/OpenAIRestExtensions.Thread.cs
delete mode 100644 dotnet/src/Experimental/Assistants/Extensions/OpenAIRestExtensions.cs
delete mode 100644 dotnet/src/Experimental/Assistants/IAssistant.cs
delete mode 100644 dotnet/src/Experimental/Assistants/IAssistantExtensions.cs
delete mode 100644 dotnet/src/Experimental/Assistants/IChatMessage.cs
delete mode 100644 dotnet/src/Experimental/Assistants/IChatThread.cs
delete mode 100644 dotnet/src/Experimental/Assistants/Internal/Assistant.cs
delete mode 100644 dotnet/src/Experimental/Assistants/Internal/ChatMessage.cs
delete mode 100644 dotnet/src/Experimental/Assistants/Internal/ChatRun.cs
delete mode 100644 dotnet/src/Experimental/Assistants/Internal/ChatThread.cs
delete mode 100644 dotnet/src/Experimental/Assistants/Internal/OpenAIRestContext.cs
delete mode 100644 dotnet/src/Experimental/Assistants/Models/AssistantConfigurationModel.cs
delete mode 100644 dotnet/src/Experimental/Assistants/Models/AssistantModel.cs
delete mode 100644 dotnet/src/Experimental/Assistants/Models/OpenAIListModel.cs
delete mode 100644 dotnet/src/Experimental/Assistants/Models/OpenAIParameters.cs
delete mode 100644 dotnet/src/Experimental/Assistants/Models/ThreadMessageModel.cs
delete mode 100644 dotnet/src/Experimental/Assistants/Models/ThreadModel.cs
delete mode 100644 dotnet/src/Experimental/Assistants/Models/ThreadRunModel.cs
delete mode 100644 dotnet/src/Experimental/Assistants/Models/ThreadRunStepModel.cs
delete mode 100644 dotnet/src/Experimental/Assistants/Models/ToolModel.cs
delete mode 100644 dotnet/src/Experimental/Assistants/Models/ToolResultModel.cs
create mode 100644 dotnet/src/InternalUtilities/src/Http/HttpResponseStream.cs
create mode 100644 dotnet/src/SemanticKernel.Abstractions/Contents/BinaryContent.cs
diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props
index 98e06b7449..948f961ff5 100644
--- a/dotnet/Directory.Packages.props
+++ b/dotnet/Directory.Packages.props
@@ -24,6 +24,7 @@
+
diff --git a/dotnet/docs/EXPERIMENTS.md b/dotnet/docs/EXPERIMENTS.md
index 50f99e702d..f98d36a7a4 100644
--- a/dotnet/docs/EXPERIMENTS.md
+++ b/dotnet/docs/EXPERIMENTS.md
@@ -24,6 +24,7 @@ You can use the following diagnostic IDs to ignore warnings or errors for a part
- SKEXP0012: OpenAI image service
- SKEXP0013: OpenAI parameters
- SKEXP0014: OpenAI chat history extension
+- SKEXP0015: OpenAI file service
## Memory connectors
diff --git a/dotnet/samples/KernelSyntaxExamples/Example74_FlowOrchestrator.cs b/dotnet/samples/KernelSyntaxExamples/Example74_FlowOrchestrator.cs
index b5a924e72b..76e051bb58 100644
--- a/dotnet/samples/KernelSyntaxExamples/Example74_FlowOrchestrator.cs
+++ b/dotnet/samples/KernelSyntaxExamples/Example74_FlowOrchestrator.cs
@@ -58,12 +58,7 @@ provides:
");
[Fact(Skip = "Can take more than 1 minute")]
- public Task RunAsync()
- {
- return RunExampleAsync();
- }
-
- private async Task RunExampleAsync()
+ public async Task RunAsync()
{
var bingConnector = new BingConnector(TestConfiguration.Bing.ApiKey);
var webSearchEnginePlugin = new WebSearchEnginePlugin(bingConnector);
@@ -76,18 +71,18 @@ provides:
FlowOrchestrator orchestrator = new(
GetKernelBuilder(LoggerFactory),
- await FlowStatusProvider.ConnectAsync(new VolatileMemoryStore()).ConfigureAwait(false),
+ await FlowStatusProvider.ConnectAsync(new VolatileMemoryStore()),
plugins,
config: GetOrchestratorConfig());
var sessionId = Guid.NewGuid().ToString();
WriteLine("*****************************************************");
- WriteLine("Executing " + nameof(RunExampleAsync));
+ WriteLine("Executing " + nameof(RunAsync));
Stopwatch sw = new();
sw.Start();
WriteLine("Flow: " + s_flow.Name);
var question = s_flow.Steps.First().Goal;
- var result = await orchestrator.ExecuteFlowAsync(s_flow, sessionId, question).ConfigureAwait(false);
+ var result = await orchestrator.ExecuteFlowAsync(s_flow, sessionId, question);
WriteLine("Question: " + question);
WriteLine("Answer: " + result.Metadata!["answer"]);
@@ -105,7 +100,7 @@ provides:
foreach (var t in userInputs)
{
WriteLine($"User: {t}");
- result = await orchestrator.ExecuteFlowAsync(s_flow, sessionId, t).ConfigureAwait(false);
+ result = await orchestrator.ExecuteFlowAsync(s_flow, sessionId, t);
var responses = result.GetValue>()!;
foreach (var response in responses)
{
diff --git a/dotnet/samples/KernelSyntaxExamples/Example75_AgentTools.cs b/dotnet/samples/KernelSyntaxExamples/Example75_AgentTools.cs
index 9d5959b695..f3814d7fc7 100644
--- a/dotnet/samples/KernelSyntaxExamples/Example75_AgentTools.cs
+++ b/dotnet/samples/KernelSyntaxExamples/Example75_AgentTools.cs
@@ -4,7 +4,10 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.Connectors.OpenAI;
using Microsoft.SemanticKernel.Experimental.Agents;
+using Resources;
using Xunit;
using Xunit.Abstractions;
@@ -80,29 +83,27 @@ public sealed class Example75_AgentTools : BaseTest
return;
}
- // REQUIRED:
- //
- // Use `curl` to upload document prior to running example and assign the
- // identifier to `fileId`.
- //
- // Powershell:
- // curl https://api.openai.com/v1/files `
- // -H "Authorization: Bearer $Env:OPENAI_APIKEY" `
- // -F purpose="assistants" `
- // -F file="@Resources/travelinfo.txt"
+ var kernel = Kernel.CreateBuilder().AddOpenAIFiles(TestConfiguration.OpenAI.ApiKey).Build();
+ var fileService = kernel.GetRequiredService();
+ var result =
+ await fileService.UploadContentAsync(
+ new BinaryContent(() => Task.FromResult(EmbeddedResource.ReadStream("travelinfo.txt")!)),
+ new OpenAIFileUploadExecutionSettings("travelinfo.txt", OpenAIFilePurpose.Assistants));
- var fileId = "";
+ var fileId = result.Id;
var defaultAgent =
- await new AgentBuilder()
- .WithOpenAIChatCompletion(OpenAIFunctionEnabledModel, TestConfiguration.OpenAI.ApiKey)
- .BuildAsync();
+ Track(
+ await new AgentBuilder()
+ .WithOpenAIChatCompletion(OpenAIFunctionEnabledModel, TestConfiguration.OpenAI.ApiKey)
+ .BuildAsync());
var retrievalAgent =
- await new AgentBuilder()
- .WithOpenAIChatCompletion(OpenAIFunctionEnabledModel, TestConfiguration.OpenAI.ApiKey)
- .WithRetrieval(fileId)
- .BuildAsync();
+ Track(
+ await new AgentBuilder()
+ .WithOpenAIChatCompletion(OpenAIFunctionEnabledModel, TestConfiguration.OpenAI.ApiKey)
+ .WithRetrieval(fileId)
+ .BuildAsync());
try
{
@@ -115,7 +116,7 @@ public sealed class Example75_AgentTools : BaseTest
}
finally
{
- await Task.WhenAll(this._agents.Select(a => a.DeleteAsync()));
+ await Task.WhenAll(this._agents.Select(a => a.DeleteAsync()).Append(fileService.DeleteFileAsync(fileId)));
}
}
diff --git a/dotnet/samples/KernelSyntaxExamples/Example79_OpenAIFiles.cs b/dotnet/samples/KernelSyntaxExamples/Example79_OpenAIFiles.cs
new file mode 100644
index 0000000000..f878a7486a
--- /dev/null
+++ b/dotnet/samples/KernelSyntaxExamples/Example79_OpenAIFiles.cs
@@ -0,0 +1,72 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Threading.Tasks;
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.Connectors.OpenAI;
+using Resources;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace Examples;
+
+// ReSharper disable once InconsistentNaming
+///
+/// Showcase usage of Open AI file-service.
+///
+public sealed class Example79_OpenAIFiles : BaseTest
+{
+ private const string ResourceFileName = "30-user-context.txt";
+
+ ///
+ /// Show how to utilize OpenAI file-service.
+ ///
+ [Fact]
+ 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();
+
+ var fileService = kernel.GetRequiredService();
+
+ // Upload file
+ var fileContent = new BinaryContent(() => Task.FromResult(EmbeddedResource.ReadStream(ResourceFileName)!));
+ var fileReference =
+ await fileService.UploadContentAsync(
+ fileContent,
+ new OpenAIFileUploadExecutionSettings(ResourceFileName, OpenAIFilePurpose.Assistants));
+
+ WriteLine("SOURCE:");
+ WriteLine($"# Name: {fileReference.FileName}");
+ WriteLine("# Content:");
+ WriteLine(await fileContent.GetContentAsync());
+
+ try
+ {
+ // Retrieve file metadata for validation.
+ var copyReference = await fileService.GetFileAsync(fileReference.Id);
+ Assert.Equal(fileReference.Id, copyReference.Id);
+ WriteLine("REFERENCE:");
+ WriteLine($"# ID: {fileReference.Id}");
+ WriteLine($"# Name: {fileReference.FileName}");
+ WriteLine($"# Purpose: {fileReference.Purpose}");
+ }
+ finally
+ {
+ // Remove file
+ await fileService.DeleteFileAsync(fileReference.Id);
+ }
+ }
+
+ public Example79_OpenAIFiles(ITestOutputHelper output) : base(output) { }
+}
diff --git a/dotnet/samples/KernelSyntaxExamples/KernelSyntaxExamples.csproj b/dotnet/samples/KernelSyntaxExamples/KernelSyntaxExamples.csproj
index 3ca6bfe9e7..4755342c69 100644
--- a/dotnet/samples/KernelSyntaxExamples/KernelSyntaxExamples.csproj
+++ b/dotnet/samples/KernelSyntaxExamples/KernelSyntaxExamples.csproj
@@ -10,7 +10,7 @@
true
false
- CS8618,IDE0009,CA1051,CA1050,CA1707,CA2007,VSTHRD111,CS1591,RCS1110,CA5394,SKEXP0001,SKEXP0002,SKEXP0003,SKEXP0004,SKEXP0010,SKEXP0011,,SKEXP0012,SKEXP0020,SKEXP0021,SKEXP0022,SKEXP0023,SKEXP0024,SKEXP0025,SKEXP0026,SKEXP0027,SKEXP0028,SKEXP0029,SKEXP0030,SKEXP0031,SKEXP0032,SKEXP0040,SKEXP0041,SKEXP0042,SKEXP0050,SKEXP0051,SKEXP0052,SKEXP0053,SKEXP0054,SKEXP0055,SKEXP0060,SKEXP0061,SKEXP0101,SKEXP0102
+ CS8618,IDE0009,CA1051,CA1050,CA1707,CA2007,VSTHRD111,CS1591,RCS1110,CA5394,SKEXP0001,SKEXP0002,SKEXP0003,SKEXP0004,SKEXP0010,SKEXP0011,,SKEXP0012,SKEXP0015,SKEXP0020,SKEXP0021,SKEXP0022,SKEXP0023,SKEXP0024,SKEXP0025,SKEXP0026,SKEXP0027,SKEXP0028,SKEXP0029,SKEXP0030,SKEXP0031,SKEXP0032,SKEXP0040,SKEXP0041,SKEXP0042,SKEXP0050,SKEXP0051,SKEXP0052,SKEXP0053,SKEXP0054,SKEXP0055,SKEXP0060,SKEXP0061,SKEXP0101,SKEXP0102
Library
diff --git a/dotnet/samples/KernelSyntaxExamples/Resources/EmbeddedResource.cs b/dotnet/samples/KernelSyntaxExamples/Resources/EmbeddedResource.cs
index 34f3867835..3b8a588479 100644
--- a/dotnet/samples/KernelSyntaxExamples/Resources/EmbeddedResource.cs
+++ b/dotnet/samples/KernelSyntaxExamples/Resources/EmbeddedResource.cs
@@ -23,13 +23,15 @@ internal static class EmbeddedResource
internal static string Read(string fileName)
{
// Get the current assembly. Note: this class is in the same assembly where the embedded resources are stored.
- Assembly? assembly = typeof(EmbeddedResource).GetTypeInfo().Assembly;
- if (assembly == null) { throw new ConfigurationException($"[{s_namespace}] {fileName} assembly not found"); }
+ Assembly assembly =
+ typeof(EmbeddedResource).GetTypeInfo().Assembly ??
+ throw new ConfigurationException($"[{s_namespace}] {fileName} assembly not found");
// Resources are mapped like types, using the namespace and appending "." (dot) and the file name
var resourceName = $"{s_namespace}." + fileName;
- using Stream? resource = assembly.GetManifestResourceStream(resourceName);
- if (resource == null) { throw new ConfigurationException($"{resourceName} resource not found"); }
+ using Stream resource =
+ assembly.GetManifestResourceStream(resourceName) ??
+ throw new ConfigurationException($"{resourceName} resource not found");
// Return the resource content, in text format.
using var reader = new StreamReader(resource);
@@ -39,8 +41,9 @@ internal static class EmbeddedResource
internal static Stream? ReadStream(string fileName)
{
// Get the current assembly. Note: this class is in the same assembly where the embedded resources are stored.
- Assembly? assembly = typeof(EmbeddedResource).GetTypeInfo().Assembly;
- if (assembly == null) { throw new ConfigurationException($"[{s_namespace}] {fileName} assembly not found"); }
+ Assembly assembly =
+ typeof(EmbeddedResource).GetTypeInfo().Assembly ??
+ throw new ConfigurationException($"[{s_namespace}] {fileName} assembly not found");
// Resources are mapped like types, using the namespace and appending "." (dot) and the file name
var resourceName = $"{s_namespace}." + fileName;
diff --git a/dotnet/src/Connectors/Connectors.OpenAI/Files/OpenAIFilePurpose.cs b/dotnet/src/Connectors/Connectors.OpenAI/Files/OpenAIFilePurpose.cs
new file mode 100644
index 0000000000..bf647296a0
--- /dev/null
+++ b/dotnet/src/Connectors/Connectors.OpenAI/Files/OpenAIFilePurpose.cs
@@ -0,0 +1,22 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Diagnostics.CodeAnalysis;
+
+namespace Microsoft.SemanticKernel.Connectors.OpenAI;
+
+///
+/// Defines the purpose associated with the uploaded file.
+///
+[Experimental("SKEXP0015")]
+public enum OpenAIFilePurpose
+{
+ ///
+ /// File to be used by assistants for model processing.
+ ///
+ Assistants,
+
+ ///
+ /// File to be used by fine-tuning jobs.
+ ///
+ FineTune,
+}
diff --git a/dotnet/src/Connectors/Connectors.OpenAI/Files/OpenAIFileReference.cs b/dotnet/src/Connectors/Connectors.OpenAI/Files/OpenAIFileReference.cs
new file mode 100644
index 0000000000..de2f37e79d
--- /dev/null
+++ b/dotnet/src/Connectors/Connectors.OpenAI/Files/OpenAIFileReference.cs
@@ -0,0 +1,38 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Diagnostics.CodeAnalysis;
+
+namespace Microsoft.SemanticKernel.Connectors.OpenAI;
+
+///
+/// References an uploaded file by id.
+///
+[Experimental("SKEXP0015")]
+public sealed class OpenAIFileReference
+{
+ ///
+ /// The file identifier.
+ ///
+ public string Id { get; set; } = string.Empty;
+
+ ///
+ /// The timestamp the file was uploaded.s
+ ///
+ public DateTime CreatedTimestamp { get; set; }
+
+ ///
+ /// The name of the file.s
+ ///
+ public string FileName { get; set; } = string.Empty;
+
+ ///
+ /// Describes the associated purpose of the file.
+ ///
+ public OpenAIFilePurpose Purpose { get; set; }
+
+ ///
+ /// The file size, in bytes.
+ ///
+ public int SizeInBytes { get; set; }
+}
diff --git a/dotnet/src/Connectors/Connectors.OpenAI/Files/OpenAIFileService.cs b/dotnet/src/Connectors/Connectors.OpenAI/Files/OpenAIFileService.cs
new file mode 100644
index 0000000000..ac76d8af29
--- /dev/null
+++ b/dotnet/src/Connectors/Connectors.OpenAI/Files/OpenAIFileService.cs
@@ -0,0 +1,265 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Linq;
+using System.Net.Http;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.SemanticKernel.Http;
+
+namespace Microsoft.SemanticKernel.Connectors.OpenAI;
+
+///
+/// File service access for OpenAI: https://api.openai.com/v1/files
+///
+[Experimental("SKEXP0015")]
+public sealed class OpenAIFileService
+{
+ private const string OpenAIApiEndpoint = "https://api.openai.com/v1/";
+ private const string OpenAIApiRouteFiles = "files";
+
+ private readonly string _apiKey;
+ private readonly HttpClient _httpClient;
+ private readonly ILogger _logger;
+ private readonly Uri _serviceUri;
+ private readonly string? _organization;
+
+ ///
+ /// Create an instance of the OpenAI chat completion connector
+ ///
+ /// OpenAI API Key
+ /// OpenAI Organization Id (usually optional)
+ /// Custom for HTTP requests.
+ /// The to use for logging. If null, no logging will be performed.
+ public OpenAIFileService(
+ string apiKey,
+ string? organization = 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 ?? new Uri(OpenAIApiEndpoint), OpenAIApiRouteFiles);
+ this._organization = organization;
+ }
+
+ ///
+ /// Remove a previously uploaded file.
+ ///
+ /// The uploaded file identifier.
+ /// The to monitor for cancellation requests. The default is .
+ public async Task DeleteFileAsync(string id, CancellationToken cancellationToken = default)
+ {
+ Verify.NotNull(id, nameof(id));
+
+ await this.ExecuteDeleteRequestAsync($"{this._serviceUri}/{id}", cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ /// Retrieve the file content from a previously uploaded file.
+ ///
+ /// The uploaded file identifier.
+ /// The to monitor for cancellation requests. The default is .
+ /// The file content as
+ ///
+ /// Files uploaded with do not support content retrieval.
+ ///
+ public BinaryContent GetFileContent(string id, CancellationToken cancellationToken = default)
+ {
+ Verify.NotNull(id, nameof(id));
+
+ return new BinaryContent(() => this.StreamGetRequestAsync($"{this._serviceUri}/{id}/content", cancellationToken));
+ }
+
+ ///
+ /// Retrieve metadata for a previously uploaded file.
+ ///
+ /// The uploaded file identifier.
+ /// The to monitor for cancellation requests. The default is .
+ /// Thet metadata associated with the specified file identifier.
+ public async Task GetFileAsync(string id, CancellationToken cancellationToken = default)
+ {
+ Verify.NotNull(id, nameof(id));
+
+ var result = await this.ExecuteGetRequestAsync($"{this._serviceUri}/{id}", cancellationToken).ConfigureAwait(false);
+
+ return this.ConvertFileReference(result);
+ }
+
+ ///
+ /// Retrieve metadata for all previously uploaded files.
+ ///
+ /// The to monitor for cancellation requests. The default is .
+ /// Thet metadata of all uploaded files.
+ public async Task> GetFilesAsync(CancellationToken cancellationToken = default)
+ {
+ var result = await this.ExecuteGetRequestAsync(this._serviceUri.ToString(), cancellationToken).ConfigureAwait(false);
+
+ return result.Data.Select(r => this.ConvertFileReference(r)).ToArray();
+ }
+
+ ///
+ /// Upload a file.
+ ///
+ /// The file content as
+ /// The upload settings
+ /// The to monitor for cancellation requests. The default is .
+ /// The file metadata.
+ public async Task UploadContentAsync(BinaryContent fileContent, OpenAIFileUploadExecutionSettings settings, CancellationToken cancellationToken = default)
+ {
+ Verify.NotNull(settings, nameof(settings));
+
+ using var formData = new MultipartFormDataContent();
+ using var contentPurpose = new StringContent(this.ConvertPurpose(settings.Purpose));
+ using var contentStream = await fileContent.GetStreamAsync().ConfigureAwait(false);
+ using var contentFile = new StreamContent(contentStream);
+ formData.Add(contentPurpose, "purpose");
+ formData.Add(contentFile, "file", settings.FileName);
+
+ var result = await this.ExecutePostRequestAsync(this._serviceUri.ToString(), formData, cancellationToken).ConfigureAwait(false);
+
+ return this.ConvertFileReference(result);
+ }
+
+ private async Task ExecuteDeleteRequestAsync(string url, CancellationToken cancellationToken)
+ {
+ using var request = HttpRequest.CreateDeleteRequest(url);
+ this.AddRequestHeaders(request);
+ using var _ = await this._httpClient.SendWithSuccessCheckAsync(request, cancellationToken).ConfigureAwait(false);
+ }
+
+ private async Task ExecuteGetRequestAsync(string url, CancellationToken cancellationToken)
+ {
+ using var request = HttpRequest.CreateGetRequest(url);
+ this.AddRequestHeaders(request);
+ using var response = await this._httpClient.SendWithSuccessCheckAsync(request, cancellationToken).ConfigureAwait(false);
+
+ var body = await response.Content.ReadAsStringWithExceptionMappingAsync().ConfigureAwait(false);
+
+ var model = JsonSerializer.Deserialize(body);
+
+ return
+ model ??
+ throw new KernelException($"Unexpected response from {url}")
+ {
+ Data = { { "ResponseData", body } },
+ };
+ }
+
+ private async Task StreamGetRequestAsync(string url, CancellationToken cancellationToken)
+ {
+ using var request = HttpRequest.CreateGetRequest(url);
+ this.AddRequestHeaders(request);
+ var response = await this._httpClient.SendWithSuccessCheckAsync(request, cancellationToken).ConfigureAwait(false);
+ try
+ {
+ return
+ new HttpResponseStream(
+ await response.Content.ReadAsStreamAndTranslateExceptionAsync().ConfigureAwait(false),
+ response);
+ }
+ catch
+ {
+ response.Dispose();
+ throw;
+ }
+ }
+
+ private async Task ExecutePostRequestAsync(string url, HttpContent payload, CancellationToken cancellationToken)
+ {
+ using var request = new HttpRequestMessage(HttpMethod.Post, url) { Content = payload };
+ this.AddRequestHeaders(request);
+ using var response = await this._httpClient.SendWithSuccessCheckAsync(request, cancellationToken).ConfigureAwait(false);
+
+ var body = await response.Content.ReadAsStringWithExceptionMappingAsync().ConfigureAwait(false);
+
+ var model = JsonSerializer.Deserialize(body);
+
+ return
+ model ??
+ throw new KernelException($"Unexpected response from {url}")
+ {
+ Data = { { "ResponseData", body } },
+ };
+ }
+
+ private void AddRequestHeaders(HttpRequestMessage request)
+ {
+ request.Headers.Add("User-Agent", HttpHeaderValues.UserAgent);
+ request.Headers.Add("Authorization", $"Bearer {this._apiKey}");
+
+ if (!string.IsNullOrEmpty(this._organization))
+ {
+ this._httpClient.DefaultRequestHeaders.Add(OpenAIClientCore.OrganizationKey, this._organization);
+ }
+ }
+
+ private OpenAIFileReference ConvertFileReference(FileInfo result)
+ {
+ return
+ new OpenAIFileReference
+ {
+ Id = result.Id,
+ FileName = result.FileName,
+ CreatedTimestamp = DateTimeOffset.FromUnixTimeSeconds(result.CreatedAt).UtcDateTime,
+ SizeInBytes = result.Bytes ?? 0,
+ Purpose = this.ConvertPurpose(result.Purpose),
+ };
+ }
+
+ private OpenAIFilePurpose ConvertPurpose(string purpose) =>
+ purpose.ToUpperInvariant() switch
+ {
+ "ASSISTANTS" => OpenAIFilePurpose.Assistants,
+ "FINE-TUNE" => OpenAIFilePurpose.FineTune,
+ _ => throw new KernelException($"Unknown {nameof(OpenAIFilePurpose)}: {purpose}."),
+ };
+
+ private string ConvertPurpose(OpenAIFilePurpose purpose) =>
+ purpose switch
+ {
+ OpenAIFilePurpose.Assistants => "assistants",
+ OpenAIFilePurpose.FineTune => "fine-tune",
+ _ => throw new KernelException($"Unknown {nameof(OpenAIFilePurpose)}: {purpose}."),
+ };
+
+ private class FileInfoList
+ {
+ [JsonPropertyName("data")]
+ public FileInfo[] Data { get; set; } = Array.Empty();
+
+ [JsonPropertyName("object")]
+ public string Object { get; set; } = "list";
+ }
+
+ private class FileInfo
+ {
+ [JsonPropertyName("id")]
+ public string Id { get; set; } = string.Empty;
+
+ [JsonPropertyName("object")]
+ public string Object { get; set; } = "file";
+
+ [JsonPropertyName("bytes")]
+ public int? Bytes { get; set; }
+
+ [JsonPropertyName("created_at")]
+ public long CreatedAt { get; set; }
+
+ [JsonPropertyName("filename")]
+ public string FileName { get; set; } = string.Empty;
+
+ [JsonPropertyName("purpose")]
+ public string Purpose { get; set; } = string.Empty;
+ }
+}
diff --git a/dotnet/src/Connectors/Connectors.OpenAI/Files/OpenAIFileUploadExecutionSettings.cs b/dotnet/src/Connectors/Connectors.OpenAI/Files/OpenAIFileUploadExecutionSettings.cs
new file mode 100644
index 0000000000..8b042835be
--- /dev/null
+++ b/dotnet/src/Connectors/Connectors.OpenAI/Files/OpenAIFileUploadExecutionSettings.cs
@@ -0,0 +1,35 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Diagnostics.CodeAnalysis;
+
+namespace Microsoft.SemanticKernel.Connectors.OpenAI;
+
+///
+/// Execution serttings associated with Open AI file upload .
+///
+[Experimental("SKEXP0015")]
+public sealed class OpenAIFileUploadExecutionSettings
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The file name
+ /// The file purpose
+ public OpenAIFileUploadExecutionSettings(string fileName, OpenAIFilePurpose purpose)
+ {
+ Verify.NotNull(fileName, nameof(fileName));
+
+ this.FileName = fileName;
+ this.Purpose = purpose;
+ }
+
+ ///
+ /// The file name.
+ ///
+ public string FileName { get; }
+
+ ///
+ /// The file purpose.
+ ///
+ public OpenAIFilePurpose Purpose { get; }
+}
diff --git a/dotnet/src/Connectors/Connectors.OpenAI/OpenAIServiceCollectionExtensions.cs b/dotnet/src/Connectors/Connectors.OpenAI/OpenAIServiceCollectionExtensions.cs
index aa1b9c383c..d6343ef0b4 100644
--- a/dotnet/src/Connectors/Connectors.OpenAI/OpenAIServiceCollectionExtensions.cs
+++ b/dotnet/src/Connectors/Connectors.OpenAI/OpenAIServiceCollectionExtensions.cs
@@ -1178,6 +1178,68 @@ public static class OpenAIServiceCollectionExtensions
#endregion
+ #region Files
+
+ ///
+ /// Add the OpenAI file service to the list
+ ///
+ /// The instance to augment.
+ /// OpenAI API key, see https://platform.openai.com/account/api-keys
+ /// OpenAI organization id. This is usually optional unless your account belongs to multiple organizations.
+ /// A local identifier for the given AI service
+ /// The HttpClient to use with this service.
+ /// The same instance as .
+ [Experimental("SKEXP0015")]
+ public static IKernelBuilder AddOpenAIFiles(
+ this IKernelBuilder builder,
+ string apiKey,
+ string? orgId = null,
+ string? serviceId = null,
+ HttpClient? httpClient = null)
+ {
+ Verify.NotNull(builder);
+ Verify.NotNullOrWhiteSpace(apiKey);
+
+ builder.Services.AddKeyedSingleton(serviceId, (serviceProvider, _) =>
+ new OpenAIFileService(
+ apiKey,
+ orgId,
+ HttpClientProvider.GetHttpClient(httpClient, serviceProvider),
+ serviceProvider.GetService()));
+
+ return builder;
+ }
+
+ ///
+ /// Add the OpenAI file service to the list
+ ///
+ /// The instance to augment.
+ /// OpenAI API key, see https://platform.openai.com/account/api-keys
+ /// OpenAI organization id. This is usually optional unless your account belongs to multiple organizations.
+ /// A local identifier for the given AI service
+ /// The same instance as .
+ [Experimental("SKEXP0015")]
+ public static IServiceCollection AddOpenAIFiles(
+ this IServiceCollection services,
+ string apiKey,
+ string? orgId = null,
+ string? serviceId = null)
+ {
+ Verify.NotNull(services);
+ Verify.NotNullOrWhiteSpace(apiKey);
+
+ services.AddKeyedSingleton(serviceId, (serviceProvider, _) =>
+ new OpenAIFileService(
+ apiKey,
+ orgId,
+ HttpClientProvider.GetHttpClient(serviceProvider),
+ serviceProvider.GetService()));
+
+ return services;
+ }
+
+ #endregion
+
private static OpenAIClient CreateAzureOpenAIClient(string endpoint, AzureKeyCredential credentials, HttpClient? httpClient) =>
new(new Uri(endpoint), credentials, ClientCore.GetOpenAIClientOptions(httpClient));
diff --git a/dotnet/src/Connectors/Connectors.UnitTests/Connectors.UnitTests.csproj b/dotnet/src/Connectors/Connectors.UnitTests/Connectors.UnitTests.csproj
index a541b834ba..3ca1ee09ce 100644
--- a/dotnet/src/Connectors/Connectors.UnitTests/Connectors.UnitTests.csproj
+++ b/dotnet/src/Connectors/Connectors.UnitTests/Connectors.UnitTests.csproj
@@ -10,7 +10,7 @@
enable
disable
false
- CA2007,CA1806,CA1869,CA1861,IDE0300,VSTHRD111,SKEXP0001,SKEXP0002,SKEXP0003,SKEXP0004,SKEXP0010,SKEXP0011,SKEXP0012,SKEXP0013,SKEXP0014,SKEXP0020,SKEXP0021,SKEXP0022,SKEXP0023,SKEXP0024,SKEXP0025,SKEXP0026,SKEXP0027,SKEXP0028,SKEXP0029,SKEXP0030,SKEXP0031,SKEXP0032,SKEXP0052
+ CA2007,CA1806,CA1869,CA1861,IDE0300,VSTHRD111,SKEXP0001,SKEXP0002,SKEXP0003,SKEXP0004,SKEXP0010,SKEXP0011,SKEXP0012,SKEXP0013,SKEXP0014,SKEXP0015,SKEXP0020,SKEXP0021,SKEXP0022,SKEXP0023,SKEXP0024,SKEXP0025,SKEXP0026,SKEXP0027,SKEXP0028,SKEXP0029,SKEXP0030,SKEXP0031,SKEXP0032,SKEXP0052
diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/Files/OpenAIFileServiceTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/Files/OpenAIFileServiceTests.cs
new file mode 100644
index 0000000000..9af2f2a334
--- /dev/null
+++ b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/Files/OpenAIFileServiceTests.cs
@@ -0,0 +1,266 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.IO;
+using System.Linq;
+using System.Net.Http;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.Connectors.OpenAI;
+using Moq;
+using Xunit;
+
+namespace SemanticKernel.Connectors.UnitTests.OpenAI.Files;
+
+///
+/// Unit tests for class.
+///
+public sealed class OpenAIFileServiceTests : IDisposable
+{
+ private readonly HttpMessageHandlerStub _messageHandlerStub;
+ private readonly HttpClient _httpClient;
+ private readonly Mock _mockLoggerFactory;
+
+ public OpenAIFileServiceTests()
+ {
+ this._messageHandlerStub = new HttpMessageHandlerStub();
+ this._httpClient = new HttpClient(this._messageHandlerStub, false);
+ this._mockLoggerFactory = new Mock();
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void ConstructorWorksCorrectly(bool includeLoggerFactory)
+ {
+ // Arrange & Act
+ var service = includeLoggerFactory ?
+ new OpenAIFileService("api-key", "organization", loggerFactory: this._mockLoggerFactory.Object) :
+ new OpenAIFileService("api-key", "organization");
+
+ // Assert
+ Assert.NotNull(service);
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public async Task DeleteFileWorksCorrectlyAsync(bool isFailedRequest)
+ {
+ // Arrange
+ var service = new OpenAIFileService("api-key", "organization", this._httpClient);
+ using var response =
+ isFailedRequest ?
+ this.CreateFailedResponse() :
+ this.CreateSuccessResponse(
+ """
+ {
+ "id": "123",
+ "filename": "test.txt",
+ "purpose": "assistants",
+ "bytes": 120000,
+ "created_at": 1677610602
+ }
+ """);
+ this._messageHandlerStub.ResponseToReturn = response;
+
+ // Act & Assert
+ if (isFailedRequest)
+ {
+ await Assert.ThrowsAsync(() => service.DeleteFileAsync("file-id"));
+ }
+ else
+ {
+ await service.DeleteFileAsync("file-id");
+ }
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public async Task GetFileWorksCorrectlyAsync(bool isFailedRequest)
+ {
+ // Arrange
+ var service = new OpenAIFileService("api-key", "organization", this._httpClient);
+ using var response =
+ isFailedRequest ?
+ this.CreateFailedResponse() :
+ this.CreateSuccessResponse(
+ """
+ {
+ "id": "123",
+ "filename": "file.txt",
+ "purpose": "assistants",
+ "bytes": 120000,
+ "created_at": 1677610602
+ }
+ """);
+ this._messageHandlerStub.ResponseToReturn = response;
+
+ // Act & Assert
+ if (isFailedRequest)
+ {
+ await Assert.ThrowsAsync(() => service.GetFileAsync("file-id"));
+ }
+ else
+ {
+ var file = await service.GetFileAsync("file-id");
+ Assert.NotNull(file);
+ Assert.NotEqual(string.Empty, file.Id);
+ Assert.NotEqual(string.Empty, file.FileName);
+ Assert.NotEqual(DateTime.MinValue, file.CreatedTimestamp);
+ Assert.NotEqual(0, file.SizeInBytes);
+ }
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public async Task GetFilesWorksCorrectlyAsync(bool isFailedRequest)
+ {
+ // Arrange
+ var service = new OpenAIFileService("api-key", "organization", this._httpClient);
+ using var response =
+ isFailedRequest ?
+ this.CreateFailedResponse() :
+ this.CreateSuccessResponse(
+ """
+ {
+ "data": [
+ {
+ "id": "123",
+ "filename": "file1.txt",
+ "purpose": "assistants",
+ "bytes": 120000,
+ "created_at": 1677610602
+ },
+ {
+ "id": "456",
+ "filename": "file2.txt",
+ "purpose": "assistants",
+ "bytes": 999,
+ "created_at": 1677610606
+ }
+ ]
+ }
+ """);
+ this._messageHandlerStub.ResponseToReturn = response;
+
+ // Act & Assert
+ if (isFailedRequest)
+ {
+ await Assert.ThrowsAsync(() => service.GetFilesAsync());
+ }
+ else
+ {
+ var files = (await service.GetFilesAsync()).ToArray();
+ Assert.NotNull(files);
+ Assert.NotEmpty(files);
+ }
+ }
+
+ [Fact]
+ public async Task GetFileContentWorksCorrectlyAsync()
+ {
+ // Arrange
+ var data = BinaryData.FromString("Hello AI!");
+ var service = new OpenAIFileService("api-key", "organization", this._httpClient);
+ this._messageHandlerStub.ResponseToReturn =
+ new HttpResponseMessage(System.Net.HttpStatusCode.OK)
+ {
+ Content = new ByteArrayContent(data.ToArray())
+ };
+
+ // Act & Assert
+ var content = service.GetFileContent("file-id");
+ var result = await content.GetContentAsync();
+ Assert.Equal(data.ToArray(), result.ToArray());
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public async Task UploadContentWorksCorrectlyAsync(bool isFailedRequest)
+ {
+ // Arrange
+ var service = new OpenAIFileService("api-key", "organization", this._httpClient);
+ using var response =
+ isFailedRequest ?
+ this.CreateFailedResponse() :
+ this.CreateSuccessResponse(
+ """
+ {
+ "id": "123",
+ "filename": "test.txt",
+ "purpose": "assistants",
+ "bytes": 120000,
+ "created_at": 1677610602
+ }
+ """);
+ this._messageHandlerStub.ResponseToReturn = response;
+
+ var settings = new OpenAIFileUploadExecutionSettings("test.txt", OpenAIFilePurpose.Assistants);
+
+ await using var stream = new MemoryStream();
+ await using (var writer = new StreamWriter(stream, leaveOpen: true))
+ {
+ await writer.WriteLineAsync("test");
+ await writer.FlushAsync();
+ }
+
+ stream.Position = 0;
+
+ var content = new BinaryContent(() => Task.FromResult(stream));
+
+ // Act & Assert
+ if (isFailedRequest)
+ {
+ await Assert.ThrowsAsync(() => service.UploadContentAsync(content, settings));
+ }
+ else
+ {
+ var file = await service.UploadContentAsync(content, settings);
+ Assert.NotNull(file);
+ Assert.NotEqual(string.Empty, file.Id);
+ Assert.NotEqual(string.Empty, file.FileName);
+ Assert.NotEqual(DateTime.MinValue, file.CreatedTimestamp);
+ Assert.NotEqual(0, file.SizeInBytes);
+ }
+ }
+
+ private HttpResponseMessage CreateSuccessResponse(string payload)
+ {
+ return
+ new HttpResponseMessage(System.Net.HttpStatusCode.OK)
+ {
+ Content =
+ new StringContent(
+ payload,
+ Encoding.UTF8,
+ "application/json")
+ };
+ }
+
+ private HttpResponseMessage CreateFailedResponse(string? payload = null)
+ {
+ return
+ new HttpResponseMessage(System.Net.HttpStatusCode.BadRequest)
+ {
+ Content =
+ string.IsNullOrEmpty(payload) ?
+ null :
+ new StringContent(
+ payload,
+ Encoding.UTF8,
+ "application/json")
+ };
+ }
+
+ public void Dispose()
+ {
+ this._httpClient.Dispose();
+ this._messageHandlerStub.Dispose();
+ }
+}
diff --git a/dotnet/src/Experimental/Assistants.UnitTests/Experimental.Assistants.UnitTests.csproj b/dotnet/src/Experimental/Assistants.UnitTests/Experimental.Assistants.UnitTests.csproj
deleted file mode 100644
index bdd40950b4..0000000000
--- a/dotnet/src/Experimental/Assistants.UnitTests/Experimental.Assistants.UnitTests.csproj
+++ /dev/null
@@ -1,49 +0,0 @@
-
-
- SemanticKernel.Experimental.Assistants.UnitTests
- SemanticKernel.Experimental.Assistants.UnitTests
- net6.0
- LatestMajor
- true
- enable
- disable
- false
- CS1591;SKEXP0101
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- all
-
-
- all
-
-
-
-
- PreserveNewest
-
-
- PreserveNewest
-
-
- PreserveNewest
-
-
- PreserveNewest
-
-
-
\ No newline at end of file
diff --git a/dotnet/src/Experimental/Assistants.UnitTests/Extensions/KernelExtensionTests.cs b/dotnet/src/Experimental/Assistants.UnitTests/Extensions/KernelExtensionTests.cs
deleted file mode 100644
index d30e849edb..0000000000
--- a/dotnet/src/Experimental/Assistants.UnitTests/Extensions/KernelExtensionTests.cs
+++ /dev/null
@@ -1,44 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using Microsoft.SemanticKernel;
-using Microsoft.SemanticKernel.Experimental.Assistants.Exceptions;
-using Microsoft.SemanticKernel.Experimental.Assistants.Extensions;
-using Xunit;
-
-namespace SemanticKernel.Experimental.Assistants.UnitTests;
-
-[Trait("Category", "Unit Tests")]
-[Trait("Feature", "Assistant")]
-public sealed class KernelExtensionTests
-{
- private const string TwoPartToolName = "Fake-Bogus";
-
- [Fact]
- public static void InvokeTwoPartTool()
- {
- //Arrange
- var function = KernelFunctionFactory.CreateFromMethod(() => { }, functionName: "Bogus");
-
- var kernel = new Kernel();
- kernel.Plugins.Add(KernelPluginFactory.CreateFromFunctions("Fake", "Fake functions", new[] { function }));
-
- //Act
- var tool = kernel.GetAssistantTool(TwoPartToolName);
-
- //Assert
- Assert.NotNull(tool);
- Assert.Equal("Bogus", tool.Name);
- }
-
- [Theory]
- [InlineData("Bogus")]
- [InlineData("i-am-not-valid")]
- public static void InvokeInvalidSinglePartTool(string toolName)
- {
- //Arrange
- var kernel = new Kernel();
-
- //Act & Assert
- Assert.Throws(() => kernel.GetAssistantTool(toolName));
- }
-}
diff --git a/dotnet/src/Experimental/Assistants.UnitTests/Extensions/KernelFunctionExtensionTests.cs b/dotnet/src/Experimental/Assistants.UnitTests/Extensions/KernelFunctionExtensionTests.cs
deleted file mode 100644
index 576ce97766..0000000000
--- a/dotnet/src/Experimental/Assistants.UnitTests/Extensions/KernelFunctionExtensionTests.cs
+++ /dev/null
@@ -1,52 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using System.Collections.Generic;
-using Microsoft.SemanticKernel;
-using Microsoft.SemanticKernel.Experimental.Assistants;
-using Xunit;
-
-namespace SemanticKernel.Experimental.Assistants.UnitTests;
-
-[Trait("Category", "Unit Tests")]
-[Trait("Feature", "Assistant")]
-public sealed class KernelFunctionExtensionTests
-{
- private const string ToolName = "Bogus";
- private const string PluginName = "Fake";
-
- [Fact]
- public static void GetTwoPartName()
- {
- var function = KernelFunctionFactory.CreateFromMethod(() => true, ToolName);
-
- string qualifiedName = function.GetQualifiedName(PluginName);
-
- Assert.Equal($"{PluginName}-{ToolName}", qualifiedName);
- }
-
- [Fact]
- public static void GetToolModelFromFunction()
- {
- const string FunctionDescription = "Bogus description";
- const string RequiredParamName = "required";
- const string OptionalParamName = "optional";
-
- var requiredParam = new KernelParameterMetadata("required") { IsRequired = true };
- var optionalParam = new KernelParameterMetadata("optional");
- var parameters = new List { requiredParam, optionalParam };
- var function = KernelFunctionFactory.CreateFromMethod(() => true, ToolName, FunctionDescription, parameters);
-
- var toolModel = function.ToToolModel(PluginName);
- var properties = toolModel.Function?.Parameters.Properties;
- var required = toolModel.Function?.Parameters.Required;
-
- Assert.Equal("function", toolModel.Type);
- Assert.Equal($"{PluginName}-{ToolName}", toolModel.Function?.Name);
- Assert.Equal(FunctionDescription, toolModel.Function?.Description);
- Assert.Equal(2, properties?.Count);
- Assert.True(properties?.ContainsKey(RequiredParamName));
- Assert.True(properties?.ContainsKey(OptionalParamName));
- Assert.Equal(1, required?.Count ?? 0);
- Assert.True(required?.Contains(RequiredParamName) ?? false);
- }
-}
diff --git a/dotnet/src/Experimental/Assistants.UnitTests/Extensions/OpenAIRestExtensions.AssistantTests.cs b/dotnet/src/Experimental/Assistants.UnitTests/Extensions/OpenAIRestExtensions.AssistantTests.cs
deleted file mode 100644
index 87a12e5386..0000000000
--- a/dotnet/src/Experimental/Assistants.UnitTests/Extensions/OpenAIRestExtensions.AssistantTests.cs
+++ /dev/null
@@ -1,67 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using System.Net;
-using System.Net.Http;
-using System.Threading;
-using System.Threading.Tasks;
-using Microsoft.SemanticKernel.Experimental.Assistants;
-using Microsoft.SemanticKernel.Experimental.Assistants.Internal;
-using Microsoft.SemanticKernel.Experimental.Assistants.Models;
-using Moq;
-using Moq.Protected;
-using Xunit;
-
-namespace SemanticKernel.Experimental.Assistants.UnitTests;
-
-[Trait("Category", "Unit Tests")]
-[Trait("Feature", "Assistant")]
-public sealed class OpenAIRestExtensionsAssistantTests
-{
- private const string BogusApiKey = "bogus";
- private const string TestAssistantId = "assistantId";
-
- private readonly AssistantModel _assistantModel = new();
- private readonly OpenAIRestContext _restContext;
- private readonly Mock _mockHttpMessageHandler = new();
-
- public OpenAIRestExtensionsAssistantTests()
- {
- this._mockHttpMessageHandler
- .Protected()
- .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny())
- .ReturnsAsync(() => new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent("{}") });
- this._restContext = new(BogusApiKey, () => new HttpClient(this._mockHttpMessageHandler.Object));
- }
-
- [Fact]
- public async Task CreateAssistantModelAsync()
- {
- await this._restContext.CreateAssistantModelAsync(this._assistantModel).ConfigureAwait(true);
-
- this._mockHttpMessageHandler.VerifyMock(HttpMethod.Post, 1, OpenAIRestExtensions.BaseAssistantUrl);
- }
-
- [Fact]
- public async Task GetAssistantModelAsync()
- {
- await this._restContext.GetAssistantModelAsync(TestAssistantId).ConfigureAwait(true);
-
- this._mockHttpMessageHandler.VerifyMock(HttpMethod.Get, 1, OpenAIRestExtensions.GetAssistantUrl(TestAssistantId));
- }
-
- [Fact]
- public async Task ListAssistantModelsAsync()
- {
- await this._restContext.ListAssistantModelsAsync(10, false, "20").ConfigureAwait(true);
-
- this._mockHttpMessageHandler.VerifyMock(HttpMethod.Get, 1, $"{OpenAIRestExtensions.BaseAssistantUrl}?limit=10&order=desc&after=20");
- }
-
- [Fact]
- public async Task DeleteAssistantModelAsync()
- {
- await this._restContext.DeleteAssistantModelAsync(TestAssistantId).ConfigureAwait(true);
-
- this._mockHttpMessageHandler.VerifyMock(HttpMethod.Delete, 1, OpenAIRestExtensions.GetAssistantUrl(TestAssistantId));
- }
-}
diff --git a/dotnet/src/Experimental/Assistants.UnitTests/Extensions/OpenAIRestExtensions.MessagesTests.cs b/dotnet/src/Experimental/Assistants.UnitTests/Extensions/OpenAIRestExtensions.MessagesTests.cs
deleted file mode 100644
index 4168daace4..0000000000
--- a/dotnet/src/Experimental/Assistants.UnitTests/Extensions/OpenAIRestExtensions.MessagesTests.cs
+++ /dev/null
@@ -1,69 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using System.Net;
-using System.Net.Http;
-using System.Threading;
-using System.Threading.Tasks;
-using Microsoft.SemanticKernel.Experimental.Assistants;
-using Microsoft.SemanticKernel.Experimental.Assistants.Internal;
-using Moq;
-using Moq.Protected;
-using Xunit;
-
-namespace SemanticKernel.Experimental.Assistants.UnitTests;
-
-[Trait("Category", "Unit Tests")]
-[Trait("Feature", "Assistant")]
-public sealed class OpenAIRestExtensionsMessagesTests
-{
- private const string BogusApiKey = "bogus";
- private const string TestThreadId = "threadId";
- private const string TestMessageId = "msgId";
- private const string TestContent = "Blah blah";
-
- private readonly OpenAIRestContext _restContext;
- private readonly Mock _mockHttpMessageHandler = new();
-
- public OpenAIRestExtensionsMessagesTests()
- {
- this._mockHttpMessageHandler
- .Protected()
- .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny())
- .ReturnsAsync(() => new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent("{}") });
- this._restContext = new(BogusApiKey, () => new HttpClient(this._mockHttpMessageHandler.Object));
- }
-
- [Fact]
- public async Task CreateMessageModelAsync()
- {
- await this._restContext.CreateUserTextMessageAsync(TestThreadId, TestContent).ConfigureAwait(true);
-
- this._mockHttpMessageHandler.VerifyMock(HttpMethod.Post, 1, OpenAIRestExtensions.GetMessagesUrl(TestThreadId));
- }
-
- [Fact]
- public async Task GetMessageModelAsync()
- {
- await this._restContext.GetMessageAsync(TestThreadId, TestMessageId).ConfigureAwait(true);
-
- this._mockHttpMessageHandler.VerifyMock(HttpMethod.Get, 1, OpenAIRestExtensions.GetMessagesUrl(TestThreadId, TestMessageId));
- }
-
- [Fact]
- public async Task GetMessageModelsAsync()
- {
- await this._restContext.GetMessagesAsync(TestThreadId).ConfigureAwait(true);
-
- this._mockHttpMessageHandler.VerifyMock(HttpMethod.Get, 1, OpenAIRestExtensions.GetMessagesUrl(TestThreadId));
- }
-
- [Fact]
- public async Task GetSpecificMessageModelsAsync()
- {
- var messageIDs = new string[] { "1", "2", "3" };
-
- await this._restContext.GetMessagesAsync(TestThreadId, messageIDs).ConfigureAwait(true);
-
- this._mockHttpMessageHandler.VerifyMock(HttpMethod.Get, messageIDs.Length);
- }
-}
diff --git a/dotnet/src/Experimental/Assistants.UnitTests/Extensions/OpenAIRestExtensions.RunTests.cs b/dotnet/src/Experimental/Assistants.UnitTests/Extensions/OpenAIRestExtensions.RunTests.cs
deleted file mode 100644
index 41ef1ed510..0000000000
--- a/dotnet/src/Experimental/Assistants.UnitTests/Extensions/OpenAIRestExtensions.RunTests.cs
+++ /dev/null
@@ -1,71 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using System;
-using System.Net;
-using System.Net.Http;
-using System.Threading;
-using System.Threading.Tasks;
-using Microsoft.SemanticKernel.Experimental.Assistants;
-using Microsoft.SemanticKernel.Experimental.Assistants.Internal;
-using Microsoft.SemanticKernel.Experimental.Assistants.Models;
-using Moq;
-using Moq.Protected;
-using Xunit;
-
-namespace SemanticKernel.Experimental.Assistants.UnitTests;
-
-[Trait("Category", "Unit Tests")]
-[Trait("Feature", "Assistant")]
-public sealed class OpenAIRestExtensionsRunTests
-{
- private const string BogusApiKey = "bogus";
- private const string TestAssistantId = "assistantId";
- private const string TestThreadId = "threadId";
- private const string TestRunId = "runId";
-
- private readonly OpenAIRestContext _restContext;
- private readonly Mock _mockHttpMessageHandler = new();
-
- public OpenAIRestExtensionsRunTests()
- {
- this._mockHttpMessageHandler
- .Protected()
- .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny())
- .ReturnsAsync(() => new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent("{}") });
- this._restContext = new(BogusApiKey, () => new HttpClient(this._mockHttpMessageHandler.Object));
- }
-
- [Fact]
- public async Task CreateRunAsync()
- {
- await this._restContext.CreateRunAsync(TestThreadId, TestAssistantId).ConfigureAwait(true);
-
- this._mockHttpMessageHandler.VerifyMock(HttpMethod.Post, 1, OpenAIRestExtensions.GetRunUrl(TestThreadId));
- }
-
- [Fact]
- public async Task GetRunAsync()
- {
- await this._restContext.GetRunAsync(TestThreadId, TestRunId).ConfigureAwait(true);
-
- this._mockHttpMessageHandler.VerifyMock(HttpMethod.Get, 1, OpenAIRestExtensions.GetRunUrl(TestThreadId, TestRunId));
- }
-
- [Fact]
- public async Task GetRunStepsAsync()
- {
- await this._restContext.GetRunStepsAsync(TestThreadId, TestRunId).ConfigureAwait(true);
-
- this._mockHttpMessageHandler.VerifyMock(HttpMethod.Get, 1, OpenAIRestExtensions.GetRunStepsUrl(TestThreadId, TestRunId));
- }
-
- [Fact]
- public async Task AddToolOutputsAsync()
- {
- var toolResults = Array.Empty();
-
- await this._restContext.AddToolOutputsAsync(TestThreadId, TestRunId, toolResults).ConfigureAwait(true);
-
- this._mockHttpMessageHandler.VerifyMock(HttpMethod.Post, 1, OpenAIRestExtensions.GetRunToolOutput(TestThreadId, TestRunId));
- }
-}
diff --git a/dotnet/src/Experimental/Assistants.UnitTests/Extensions/OpenAIRestExtensions.ThreadTests.cs b/dotnet/src/Experimental/Assistants.UnitTests/Extensions/OpenAIRestExtensions.ThreadTests.cs
deleted file mode 100644
index 042eafee48..0000000000
--- a/dotnet/src/Experimental/Assistants.UnitTests/Extensions/OpenAIRestExtensions.ThreadTests.cs
+++ /dev/null
@@ -1,57 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using System.Net;
-using System.Net.Http;
-using System.Threading;
-using System.Threading.Tasks;
-using Microsoft.SemanticKernel.Experimental.Assistants;
-using Microsoft.SemanticKernel.Experimental.Assistants.Internal;
-using Moq;
-using Moq.Protected;
-using Xunit;
-
-namespace SemanticKernel.Experimental.Assistants.UnitTests;
-
-[Trait("Category", "Unit Tests")]
-[Trait("Feature", "Assistant")]
-public sealed class OpenAIRestExtensionsThreadTests
-{
- private const string BogusApiKey = "bogus";
- private const string TestThreadId = "threadId";
-
- private readonly OpenAIRestContext _restContext;
- private readonly Mock _mockHttpMessageHandler = new();
-
- public OpenAIRestExtensionsThreadTests()
- {
- this._mockHttpMessageHandler
- .Protected()
- .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny())
- .ReturnsAsync(() => new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent("{}") });
- this._restContext = new(BogusApiKey, () => new HttpClient(this._mockHttpMessageHandler.Object));
- }
-
- [Fact]
- public async Task CreateThreadModelAsync()
- {
- await this._restContext.CreateThreadModelAsync().ConfigureAwait(true);
-
- this._mockHttpMessageHandler.VerifyMock(HttpMethod.Post, 1, OpenAIRestExtensions.BaseThreadUrl);
- }
-
- [Fact]
- public async Task GetThreadModelAsync()
- {
- await this._restContext.GetThreadModelAsync(TestThreadId).ConfigureAwait(true);
-
- this._mockHttpMessageHandler.VerifyMock(HttpMethod.Get, 1, OpenAIRestExtensions.GetThreadUrl(TestThreadId));
- }
-
- [Fact]
- public async Task DeleteThreadModelAsync()
- {
- await this._restContext.DeleteThreadModelAsync(TestThreadId).ConfigureAwait(true);
-
- this._mockHttpMessageHandler.VerifyMock(HttpMethod.Delete, 1, OpenAIRestExtensions.GetThreadUrl(TestThreadId));
- }
-}
diff --git a/dotnet/src/Experimental/Assistants.UnitTests/Integration/AssistantHarness.cs b/dotnet/src/Experimental/Assistants.UnitTests/Integration/AssistantHarness.cs
deleted file mode 100644
index 11e32b85ef..0000000000
--- a/dotnet/src/Experimental/Assistants.UnitTests/Integration/AssistantHarness.cs
+++ /dev/null
@@ -1,148 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-#define DISABLEHOST // Comment line to enable
-using System;
-using System.Collections.Generic;
-using System.Threading.Tasks;
-using Microsoft.SemanticKernel.Experimental.Assistants;
-using Microsoft.SemanticKernel.Experimental.Assistants.Internal;
-using Microsoft.SemanticKernel.Experimental.Assistants.Models;
-using Xunit;
-using Xunit.Abstractions;
-
-namespace SemanticKernel.Experimental.Assistants.UnitTests.Integration;
-
-///
-/// Dev harness for manipulating assistants.
-///
-///
-/// Comment out DISABLEHOST definition to enable tests.
-/// Not enabled by default.
-///
-[Trait("Category", "Integration Tests")]
-[Trait("Feature", "Assistant")]
-public sealed class AssistantHarness
-{
-#if DISABLEHOST
- private const string SkipReason = "Harness only for local/dev environment";
-#else
- private const string SkipReason = null;
-#endif
-
- private readonly ITestOutputHelper _output;
-
- ///
- /// Test constructor.
- ///
- public AssistantHarness(ITestOutputHelper output)
- {
- this._output = output;
- }
-
- ///
- /// Verify creation and retrieval of assistant.
- ///
- [Fact(Skip = SkipReason)]
- public async Task VerifyAssistantLifecycleAsync()
- {
- var assistant =
- await AssistantBuilder.NewAsync(
- apiKey: TestConfig.OpenAIApiKey,
- model: TestConfig.SupportedGpt35TurboModel,
- instructions: "say something funny",
- name: "Fred",
- description: "test assistant").ConfigureAwait(true);
-
- this.DumpAssistant(assistant);
-
- var copy =
- await AssistantBuilder.GetAssistantAsync(
- apiKey: TestConfig.OpenAIApiKey,
- assistantId: assistant.Id).ConfigureAwait(true);
-
- this.DumpAssistant(copy);
- }
-
- ///
- /// Verify creation and retrieval of assistant.
- ///
- [Fact(Skip = SkipReason)]
- public async Task VerifyAssistantDefinitionAsync()
- {
- var assistant =
- await new AssistantBuilder()
- .WithOpenAIChatCompletion(TestConfig.SupportedGpt35TurboModel, TestConfig.OpenAIApiKey)
- .FromTemplatePath("Templates/PoetAssistant.yaml")
- .BuildAsync()
- .ConfigureAwait(true);
-
- this.DumpAssistant(assistant);
-
- var copy =
- await AssistantBuilder.GetAssistantAsync(
- apiKey: TestConfig.OpenAIApiKey,
- assistantId: assistant.Id).ConfigureAwait(true);
-
- this.DumpAssistant(copy);
- }
-
- ///
- /// Verify creation and retrieval of assistant.
- ///
- [Fact(Skip = SkipReason)]
- public async Task VerifyAssistantListAsync()
- {
- var context = new OpenAIRestContext(TestConfig.OpenAIApiKey);
- var assistants = await context.ListAssistantModelsAsync().ConfigureAwait(true);
- foreach (var assistant in assistants)
- {
- this.DumpAssistant(assistant);
- }
- }
-
- ///
- /// Verify creation and retrieval of assistant.
- ///
- [Fact(Skip = SkipReason)]
- public async Task VerifyAssistantDeleteAsync()
- {
- var names =
- new HashSet(StringComparer.OrdinalIgnoreCase)
- {
- "Fred",
- "Barney",
- "DeleteMe",
- "Poet",
- "Math Tutor",
- };
-
- var context = new OpenAIRestContext(TestConfig.OpenAIApiKey);
- var assistants = await context.ListAssistantModelsAsync().ConfigureAwait(true);
- foreach (var assistant in assistants)
- {
- if (!string.IsNullOrWhiteSpace(assistant.Name) && names.Contains(assistant.Name))
- {
- this._output.WriteLine($"Removing: {assistant.Name} - {assistant.Id}");
- await context.DeleteAssistantModelAsync(assistant.Id).ConfigureAwait(true);
- }
- }
- }
-
- private void DumpAssistant(AssistantModel assistant)
- {
- this._output.WriteLine($"# {assistant.Id}");
- this._output.WriteLine($"# {assistant.Model}");
- this._output.WriteLine($"# {assistant.Instructions}");
- this._output.WriteLine($"# {assistant.Name}");
- this._output.WriteLine($"# {assistant.Description}{Environment.NewLine}");
- }
-
- private void DumpAssistant(IAssistant assistant)
- {
- this._output.WriteLine($"# {assistant.Id}");
- this._output.WriteLine($"# {assistant.Model}");
- this._output.WriteLine($"# {assistant.Instructions}");
- this._output.WriteLine($"# {assistant.Name}");
- this._output.WriteLine($"# {assistant.Description}{Environment.NewLine}");
- }
-}
diff --git a/dotnet/src/Experimental/Assistants.UnitTests/Integration/RunHarness.cs b/dotnet/src/Experimental/Assistants.UnitTests/Integration/RunHarness.cs
deleted file mode 100644
index b2d9c1403b..0000000000
--- a/dotnet/src/Experimental/Assistants.UnitTests/Integration/RunHarness.cs
+++ /dev/null
@@ -1,157 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-#define DISABLEHOST // Comment line to enable
-using System.Collections.Generic;
-using System.ComponentModel;
-using System.Linq;
-using System.Threading.Tasks;
-using Microsoft.SemanticKernel;
-using Microsoft.SemanticKernel.Experimental.Assistants;
-using Xunit;
-using Xunit.Abstractions;
-
-#pragma warning disable CA1812 // Uninstantiated internal types
-
-namespace SemanticKernel.Experimental.Assistants.UnitTests.Integration;
-
-///
-/// Dev harness for manipulating runs.
-///
-///
-/// Comment out DISABLEHOST definition to enable tests.
-/// Not enabled by default.
-///
-[Trait("Category", "Integration Tests")]
-[Trait("Feature", "Assistant")]
-public sealed class RunHarness
-{
-#if DISABLEHOST
- private const string SkipReason = "Harness only for local/dev environment";
-#else
- private const string SkipReason = null;
-#endif
-
- private readonly ITestOutputHelper _output;
-
- ///
- /// Test constructor.
- ///
- public RunHarness(ITestOutputHelper output)
- {
- this._output = output;
- }
-
- ///
- /// Verify creation of run.
- ///
- [Fact(Skip = SkipReason)]
- public async Task VerifyRunLifecycleAsync()
- {
- var assistant =
- await AssistantBuilder.NewAsync(
- apiKey: TestConfig.OpenAIApiKey,
- model: TestConfig.SupportedGpt35TurboModel,
- instructions: "say something funny",
- name: "Fred",
- description: "funny assistant").ConfigureAwait(true);
-
- var thread = await assistant.NewThreadAsync().ConfigureAwait(true);
-
- await this.ChatAsync(
- thread,
- assistant,
- "I was on my way to the store this morning and...",
- "That was great! Tell me another.").ConfigureAwait(true);
- }
-
- ///
- /// Verify creation of run.
- ///
- [Fact(Skip = SkipReason)]
- public async Task VerifyRunFromDefinitionAsync()
- {
- var assistant =
- await new AssistantBuilder()
- .WithOpenAIChatCompletion(TestConfig.SupportedGpt35TurboModel, TestConfig.OpenAIApiKey)
- .FromTemplatePath("Templates/PoetAssistant.yaml")
- .BuildAsync()
- .ConfigureAwait(true);
-
- var thread = await assistant.NewThreadAsync().ConfigureAwait(true);
-
- await this.ChatAsync(
- thread,
- assistant,
- "Eggs are yummy and beautiful geometric gems.",
- "It rains a lot in Seattle.").ConfigureAwait(true);
- }
-
- ///
- /// Verify creation of run.
- ///
- [Fact(Skip = SkipReason)]
- public async Task VerifyFunctionLifecycleAsync()
- {
- var gamePlugin = KernelPluginFactory.CreateFromType();
-
- var assistant =
- await new AssistantBuilder()
- .WithOpenAIChatCompletion(TestConfig.SupportedGpt35TurboModel, TestConfig.OpenAIApiKey)
- .FromTemplatePath("Templates/GameAssistant.yaml")
- .WithPlugin(gamePlugin)
- .BuildAsync()
- .ConfigureAwait(true);
-
- var thread = await assistant.NewThreadAsync().ConfigureAwait(true);
-
- await this.ChatAsync(
- thread,
- assistant,
- "What is the question for the guessing game?",
- "Is it 'RED'?",
- "What is the answer?").ConfigureAwait(true);
- }
-
- private async Task ChatAsync(IChatThread thread, IAssistant assistant, params string[] messages)
- {
- foreach (var message in messages)
- {
- var messageUser = await thread.AddUserMessageAsync(message).ConfigureAwait(true);
- this.LogMessage(messageUser);
-
- var assistantMessages = await thread.InvokeAsync(assistant).ToArrayAsync().ConfigureAwait(true);
- this.LogMessages(assistantMessages);
- }
- }
-
- private void LogMessages(IEnumerable messages)
- {
- foreach (var message in messages)
- {
- this.LogMessage(message);
- }
- }
-
- private void LogMessage(IChatMessage message)
- {
- this._output.WriteLine($"# {message.Id}");
- this._output.WriteLine($"# {message.Content}");
- this._output.WriteLine($"# {message.Role}");
- this._output.WriteLine($"# {message.AssistantId}");
- }
-
- private sealed class GuessingGame
- {
- ///
- /// Get the question
- ///
- [KernelFunction, Description("Get the guessing game question")]
- public string GetQuestion() => "What color am I thinking of?";
-
- ///
- /// Get the answer
- ///
- [KernelFunction, Description("Get the answer to the guessing game question.")]
- public string GetAnswer() => "Blue";
- }
-}
diff --git a/dotnet/src/Experimental/Assistants.UnitTests/Integration/ThreadHarness.cs b/dotnet/src/Experimental/Assistants.UnitTests/Integration/ThreadHarness.cs
deleted file mode 100644
index 0546bbd43a..0000000000
--- a/dotnet/src/Experimental/Assistants.UnitTests/Integration/ThreadHarness.cs
+++ /dev/null
@@ -1,71 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-#define DISABLEHOST // Comment line to enable
-using System.Threading.Tasks;
-using Microsoft.SemanticKernel;
-using Microsoft.SemanticKernel.Experimental.Assistants;
-using Microsoft.SemanticKernel.Experimental.Assistants.Internal;
-using Xunit;
-using Xunit.Abstractions;
-
-namespace SemanticKernel.Experimental.Assistants.UnitTests.Integration;
-
-///
-/// Dev harness for manipulating threads.
-///
-///
-/// Comment out DISABLEHOST definition to enable tests.
-/// Not enabled by default.
-///
-[Trait("Category", "Integration Tests")]
-[Trait("Feature", "Assistant")]
-public sealed class ThreadHarness
-{
-#if DISABLEHOST
- private const string SkipReason = "Harness only for local/dev environment";
-#else
- private const string SkipReason = null;
-#endif
-
- private readonly ITestOutputHelper _output;
-
- ///
- /// Test constructor.
- ///
- public ThreadHarness(ITestOutputHelper output)
- {
- this._output = output;
- }
-
- ///
- /// Verify creation and retrieval of thread.
- ///
- [Fact(Skip = SkipReason)]
- public async Task VerifyThreadLifecycleAsync()
- {
- var assistant =
- await new AssistantBuilder()
- .WithOpenAIChatCompletion(TestConfig.SupportedGpt35TurboModel, TestConfig.OpenAIApiKey)
- .WithName("DeleteMe")
- .BuildAsync()
- .ConfigureAwait(true);
-
- var thread = await assistant.NewThreadAsync().ConfigureAwait(true);
-
- Assert.NotNull(thread.Id);
-
- this._output.WriteLine($"# {thread.Id}");
-
- var message = await thread.AddUserMessageAsync("I'm so confused!").ConfigureAwait(true);
- Assert.NotNull(message);
-
- this._output.WriteLine($"# {message.Id}");
-
- var context = new OpenAIRestContext(TestConfig.OpenAIApiKey);
- var copy = await context.GetThreadModelAsync(thread.Id).ConfigureAwait(true);
-
- await context.DeleteThreadModelAsync(thread.Id).ConfigureAwait(true);
-
- await Assert.ThrowsAsync(() => context.GetThreadModelAsync(thread.Id)).ConfigureAwait(true);
- }
-}
diff --git a/dotnet/src/Experimental/Assistants.UnitTests/MockExtensions.cs b/dotnet/src/Experimental/Assistants.UnitTests/MockExtensions.cs
deleted file mode 100644
index 5f2e147e73..0000000000
--- a/dotnet/src/Experimental/Assistants.UnitTests/MockExtensions.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using System;
-using System.Net.Http;
-using System.Threading;
-using Moq;
-using Moq.Protected;
-
-namespace SemanticKernel.Experimental.Assistants.UnitTests;
-
-internal static class MockExtensions
-{
- public static void VerifyMock(this Mock mockHandler, HttpMethod method, int times, string? uri = null)
- {
- mockHandler.Protected().Verify(
- "SendAsync",
- Times.Exactly(times),
- ItExpr.Is(req => req.Method == method && (uri == null || req.RequestUri == new Uri(uri))),
- ItExpr.IsAny());
- }
-}
diff --git a/dotnet/src/Experimental/Assistants.UnitTests/Templates/GameAssistant.yaml b/dotnet/src/Experimental/Assistants.UnitTests/Templates/GameAssistant.yaml
deleted file mode 100644
index dce10488e8..0000000000
--- a/dotnet/src/Experimental/Assistants.UnitTests/Templates/GameAssistant.yaml
+++ /dev/null
@@ -1,4 +0,0 @@
-name: Fred
-instructions: |
- Run a guessing game where the user tries to guess the answer to a question but don't tell them the answer unless they give up by asking for the answer.
-
diff --git a/dotnet/src/Experimental/Assistants.UnitTests/Templates/PoetAssistant.yaml b/dotnet/src/Experimental/Assistants.UnitTests/Templates/PoetAssistant.yaml
deleted file mode 100644
index 7e356ddd61..0000000000
--- a/dotnet/src/Experimental/Assistants.UnitTests/Templates/PoetAssistant.yaml
+++ /dev/null
@@ -1,4 +0,0 @@
-name: Poet
-instructions: |
- Compose a sonnet inspired by the user input.
-description: You are a poet that composes poems based on user input.
diff --git a/dotnet/src/Experimental/Assistants.UnitTests/TestConfig.cs b/dotnet/src/Experimental/Assistants.UnitTests/TestConfig.cs
deleted file mode 100644
index 30263aa773..0000000000
--- a/dotnet/src/Experimental/Assistants.UnitTests/TestConfig.cs
+++ /dev/null
@@ -1,29 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using System.Reflection;
-using Microsoft.Extensions.Configuration;
-using Xunit.Sdk;
-
-namespace SemanticKernel.Experimental.Assistants.UnitTests;
-
-internal static class TestConfig
-{
- public const string SupportedGpt35TurboModel = "gpt-3.5-turbo-1106";
-
- public static IConfiguration Configuration { get; } = CreateConfiguration();
-
- public static string OpenAIApiKey =>
- TestConfig.Configuration.GetValue("OpenAIApiKey") ??
- throw new TestClassException("Missing OpenAI APIKey.");
-
- private static IConfiguration CreateConfiguration()
- {
- return
- new ConfigurationBuilder()
- .AddEnvironmentVariables()
- .AddJsonFile("testsettings.json")
- .AddJsonFile("testsettings.development.json", optional: true)
- .AddUserSecrets(Assembly.GetExecutingAssembly())
- .Build();
- }
-}
diff --git a/dotnet/src/Experimental/Assistants.UnitTests/testsettings.json b/dotnet/src/Experimental/Assistants.UnitTests/testsettings.json
deleted file mode 100644
index d456a389e0..0000000000
--- a/dotnet/src/Experimental/Assistants.UnitTests/testsettings.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
- "OpenAIApiKey": ""
-}
diff --git a/dotnet/src/Experimental/Assistants/AssemblyInfo.cs b/dotnet/src/Experimental/Assistants/AssemblyInfo.cs
deleted file mode 100644
index 951ee2d582..0000000000
--- a/dotnet/src/Experimental/Assistants/AssemblyInfo.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using System.Diagnostics.CodeAnalysis;
-
-// This assembly is currently experimental.
-[assembly: Experimental("SKEXP0101")]
diff --git a/dotnet/src/Experimental/Assistants/AssistantBuilder.Static.cs b/dotnet/src/Experimental/Assistants/AssistantBuilder.Static.cs
deleted file mode 100644
index 8b52b43bb5..0000000000
--- a/dotnet/src/Experimental/Assistants/AssistantBuilder.Static.cs
+++ /dev/null
@@ -1,59 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using System.Collections.Generic;
-using System.Threading;
-using System.Threading.Tasks;
-using Microsoft.SemanticKernel.Experimental.Assistants.Internal;
-
-namespace Microsoft.SemanticKernel.Experimental.Assistants;
-
-///
-/// Context for interacting with OpenAI REST API.
-///
-public partial class AssistantBuilder
-{
- ///
- /// Create a new assistant.
- ///
- /// The OpenAI API key
- /// The assistant chat model (required)
- /// The assistant instructions (required)
- /// The assistant name (optional)
- /// The assistant description(optional)
- /// The requested .
- public static async Task NewAsync(
- string apiKey,
- string model,
- string instructions,
- string? name = null,
- string? description = null)
- {
- return
- await new AssistantBuilder()
- .WithOpenAIChatCompletion(model, apiKey)
- .WithInstructions(instructions)
- .WithName(name)
- .WithDescription(description)
- .BuildAsync().ConfigureAwait(false);
- }
-
- ///
- /// Retrieve an existing assistant, by identifier.
- ///
- /// A context for accessing OpenAI REST endpoint
- /// The assistant identifier
- /// Plugins to initialize as assistant tools
- /// A cancellation token
- /// An initialized instance.
- public static async Task GetAssistantAsync(
- string apiKey,
- string assistantId,
- IEnumerable? plugins = null,
- CancellationToken cancellationToken = default)
- {
- var restContext = new OpenAIRestContext(apiKey);
- var resultModel = await restContext.GetAssistantModelAsync(assistantId, cancellationToken).ConfigureAwait(false);
-
- return new Assistant(resultModel, restContext, plugins);
- }
-}
diff --git a/dotnet/src/Experimental/Assistants/AssistantBuilder.cs b/dotnet/src/Experimental/Assistants/AssistantBuilder.cs
deleted file mode 100644
index 1dc37b4ac1..0000000000
--- a/dotnet/src/Experimental/Assistants/AssistantBuilder.cs
+++ /dev/null
@@ -1,196 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Net.Http;
-using System.Threading;
-using System.Threading.Tasks;
-using Microsoft.SemanticKernel.Experimental.Assistants.Exceptions;
-using Microsoft.SemanticKernel.Experimental.Assistants.Internal;
-using Microsoft.SemanticKernel.Experimental.Assistants.Models;
-using YamlDotNet.Serialization;
-
-namespace Microsoft.SemanticKernel.Experimental.Assistants;
-
-///
-/// Fluent builder for initializing an instance.
-///
-public partial class AssistantBuilder
-{
- private readonly AssistantModel _model;
- private readonly KernelPluginCollection _plugins;
-
- private string? _apiKey;
- private Func? _httpClientProvider;
-
- ///
- /// Initializes a new instance of the class.
- ///
- public AssistantBuilder()
- {
- this._model = new AssistantModel();
- this._plugins = new KernelPluginCollection();
- }
-
- ///
- /// Create a instance.
- ///
- /// A cancellation token
- /// A new instance.
- public async Task BuildAsync(CancellationToken cancellationToken = default)
- {
- if (string.IsNullOrWhiteSpace(this._model.Model))
- {
- throw new AssistantException("Model must be defined for assistant.");
- }
-
- if (string.IsNullOrWhiteSpace(this._apiKey))
- {
- throw new AssistantException("ApiKey must be provided for assistant.");
- }
-
- return
- await Assistant.CreateAsync(
- new OpenAIRestContext(this._apiKey!, this._httpClientProvider),
- this._model,
- this._plugins,
- cancellationToken).ConfigureAwait(false);
- }
-
- ///
- /// Define the OpenAI chat completion service (required).
- ///
- /// instance for fluid expression.
- public AssistantBuilder WithOpenAIChatCompletion(string model, string apiKey)
- {
- this._apiKey = apiKey;
- this._model.Model = model;
-
- return this;
- }
-
- ///
- /// Create a new assistant from a yaml formatted string.
- ///
- /// YAML assistant definition.
- /// instance for fluid expression.
- public AssistantBuilder FromTemplate(string template)
- {
- var deserializer = new DeserializerBuilder().Build();
-
- var assistantKernelModel = deserializer.Deserialize(template);
-
- return
- this
- .WithInstructions(assistantKernelModel.Instructions.Trim())
- .WithName(assistantKernelModel.Name.Trim())
- .WithDescription(assistantKernelModel.Description.Trim());
- }
-
- ///
- /// Create a new assistant from a yaml template.
- ///
- /// Path to a configuration file.
- /// instance for fluid expression.
- public AssistantBuilder FromTemplatePath(string templatePath)
- {
- var yamlContent = File.ReadAllText(templatePath);
-
- return this.FromTemplate(yamlContent);
- }
-
- ///
- /// Provide an httpclient (optional).
- ///
- /// instance for fluid expression.
- public AssistantBuilder WithHttpClient(HttpClient httpClient)
- {
- this._httpClientProvider ??= () => httpClient;
-
- return this;
- }
-
- ///
- /// Define the assistant description (optional).
- ///
- /// instance for fluid expression.
- public AssistantBuilder WithDescription(string? description)
- {
- this._model.Description = description;
-
- return this;
- }
-
- ///
- /// Define the assistant instructions (optional).
- ///
- /// instance for fluid expression.
- public AssistantBuilder WithInstructions(string instructions)
- {
- this._model.Instructions = instructions;
-
- return this;
- }
-
- ///
- /// Define the assistant metadata (optional).
- ///
- /// instance for fluid expression.
- public AssistantBuilder WithMetadata(string key, object value)
- {
- this._model.Metadata[key] = value;
-
- return this;
- }
-
- ///
- /// Define the assistant metadata (optional).
- ///
- /// instance for fluid expression.
- public AssistantBuilder WithMetadata(IDictionary metadata)
- {
- foreach (var kvp in metadata)
- {
- this._model.Metadata[kvp.Key] = kvp.Value;
- }
-
- return this;
- }
-
- ///
- /// Define the assistant name (optional).
- ///
- /// instance for fluid expression.
- public AssistantBuilder WithName(string? name)
- {
- this._model.Name = name;
-
- return this;
- }
-
- ///
- /// Define functions associated with assistant instance (optional).
- ///
- /// instance for fluid expression.
- public AssistantBuilder WithPlugin(KernelPlugin? plugin)
- {
- if (plugin != null)
- {
- this._plugins.Add(plugin);
- }
-
- return this;
- }
-
- ///
- /// Define functions associated with assistant instance (optional).
- ///
- /// instance for fluid expression.
- public AssistantBuilder WithPlugins(IEnumerable plugins)
- {
- this._plugins.AddRange(plugins);
-
- return this;
- }
-}
diff --git a/dotnet/src/Experimental/Assistants/AssistantPlugin.cs b/dotnet/src/Experimental/Assistants/AssistantPlugin.cs
deleted file mode 100644
index 26d9cfae88..0000000000
--- a/dotnet/src/Experimental/Assistants/AssistantPlugin.cs
+++ /dev/null
@@ -1,38 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-using Microsoft.SemanticKernel.Experimental.Assistants.Internal;
-
-namespace Microsoft.SemanticKernel.Experimental.Assistants;
-
-///
-/// Specialization of for
-///
-public abstract class AssistantPlugin : KernelPlugin
-{
- ///
- protected AssistantPlugin(string name, string? description = null)
- : base(name, description)
- {
- // No specialization...
- }
-
- internal abstract Assistant Assistant { get; }
-
- ///
- /// Invoke plugin with user input
- ///
- /// The user input
- /// A cancel token
- /// The assistant response
- public async Task InvokeAsync(string input, CancellationToken cancellationToken = default)
- {
- var args = new KernelArguments { { "input", input } };
- var result = await this.First().InvokeAsync(this.Assistant.Kernel, args, cancellationToken).ConfigureAwait(false);
- var response = result.GetValue()!;
-
- return response.Message;
- }
-}
diff --git a/dotnet/src/Experimental/Assistants/AssistantResponse.cs b/dotnet/src/Experimental/Assistants/AssistantResponse.cs
deleted file mode 100644
index c92ce3662f..0000000000
--- a/dotnet/src/Experimental/Assistants/AssistantResponse.cs
+++ /dev/null
@@ -1,29 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using System.Text.Json.Serialization;
-
-namespace Microsoft.SemanticKernel.Experimental.Assistants;
-
-///
-/// Response from assistant when called as a .
-///
-public class AssistantResponse
-{
- ///
- /// The thread-id for the assistant conversation.
- ///
- [JsonPropertyName("thread_id")]
- public string ThreadId { get; set; } = string.Empty;
-
- ///
- /// The assistant response.
- ///
- [JsonPropertyName("response")]
- public string Message { get; set; } = string.Empty;
-
- ///
- /// Instructions from assistant on next steps.
- ///
- [JsonPropertyName("system_instructions")]
- public string Instructions { get; set; } = string.Empty;
-}
diff --git a/dotnet/src/Experimental/Assistants/Exceptions/AssistantException.cs b/dotnet/src/Experimental/Assistants/Exceptions/AssistantException.cs
deleted file mode 100644
index 2f3057b115..0000000000
--- a/dotnet/src/Experimental/Assistants/Exceptions/AssistantException.cs
+++ /dev/null
@@ -1,35 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using System;
-
-namespace Microsoft.SemanticKernel.Experimental.Assistants.Exceptions;
-
-///
-/// Assistant specific .
-///
-public class AssistantException : KernelException
-{
- ///
- /// Initializes a new instance of the class.
- ///
- public AssistantException()
- {
- }
-
- ///
- /// Initializes a new instance of the class with a specified error message.
- ///
- /// The error message that explains the reason for the exception.
- public AssistantException(string? message) : base(message)
- {
- }
-
- ///
- /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of this exception.
- ///
- /// The error message that explains the reason for the exception.
- /// The exception that is the cause of the current exception, or a null reference if no inner exception is specified.
- public AssistantException(string? message, Exception? innerException) : base(message, innerException)
- {
- }
-}
diff --git a/dotnet/src/Experimental/Assistants/Experimental.Assistants.csproj b/dotnet/src/Experimental/Assistants/Experimental.Assistants.csproj
deleted file mode 100644
index 2edcf111ad..0000000000
--- a/dotnet/src/Experimental/Assistants/Experimental.Assistants.csproj
+++ /dev/null
@@ -1,31 +0,0 @@
-
-
-
- Microsoft.SemanticKernel.Experimental.Assistants
- Microsoft.SemanticKernel.Experimental.Assistants
- netstandard2.0
- alpha
- Latest
-
-
-
-
-
- Semantic Kernel Assistants
- Semantic Kernel Assistants
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/dotnet/src/Experimental/Assistants/Extensions/AssistantsKernelExtensions.cs b/dotnet/src/Experimental/Assistants/Extensions/AssistantsKernelExtensions.cs
deleted file mode 100644
index d508b01f95..0000000000
--- a/dotnet/src/Experimental/Assistants/Extensions/AssistantsKernelExtensions.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using Microsoft.SemanticKernel.Experimental.Assistants.Exceptions;
-
-namespace Microsoft.SemanticKernel.Experimental.Assistants.Extensions;
-
-internal static class AssistantsKernelExtensions
-{
- ///
- /// Retrieve a kernel function based on the tool name.
- ///
- public static KernelFunction GetAssistantTool(this Kernel kernel, string toolName)
- {
- string[] nameParts = toolName.Split('-');
- return nameParts.Length switch
- {
- 2 => kernel.Plugins.GetFunction(nameParts[0], nameParts[1]),
- _ => throw new AssistantException($"Unknown tool: {toolName}"),
- };
- }
-}
diff --git a/dotnet/src/Experimental/Assistants/Extensions/AssistantsKernelFunctionExtensions.cs b/dotnet/src/Experimental/Assistants/Extensions/AssistantsKernelFunctionExtensions.cs
deleted file mode 100644
index cc56d69289..0000000000
--- a/dotnet/src/Experimental/Assistants/Extensions/AssistantsKernelFunctionExtensions.cs
+++ /dev/null
@@ -1,89 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using Json.More;
-using Microsoft.SemanticKernel.Experimental.Assistants.Models;
-
-namespace Microsoft.SemanticKernel.Experimental.Assistants;
-
-internal static class AssistantsKernelFunctionExtensions
-{
- ///
- /// Produce a fully qualified toolname.
- ///
- public static string GetQualifiedName(this KernelFunction function, string pluginName)
- {
- return $"{pluginName}-{function.Name}";
- }
-
- ///
- /// Convert to an OpenAI tool model.
- ///
- /// The source function
- /// The plugin name
- /// An OpenAI tool model
- public static ToolModel ToToolModel(this KernelFunction function, string pluginName)
- {
- var metadata = function.Metadata;
- var required = new List(metadata.Parameters.Count);
- var properties =
- metadata.Parameters.ToDictionary(
- p => p.Name,
- p =>
- {
- if (p.IsRequired)
- {
- required.Add(p.Name);
- }
-
- return
- new OpenAIParameter
- {
- Type = ConvertType(p.ParameterType),
- Description = p.Description,
- };
- });
-
- var payload =
- new ToolModel
- {
- Type = "function",
- Function =
- new()
- {
- Name = function.GetQualifiedName(pluginName),
- Description = function.Description,
- Parameters =
- new OpenAIParameters
- {
- Properties = properties,
- Required = required,
- },
- },
- };
-
- return payload;
- }
-
- private static string ConvertType(Type? type)
- {
- if (type == null || type == typeof(string))
- {
- return "string";
- }
-
- if (type.IsNumber())
- {
- return "number";
- }
-
- if (type.IsEnum)
- {
- return "enum";
- }
-
- return type.Name;
- }
-}
diff --git a/dotnet/src/Experimental/Assistants/Extensions/OpenAIRestExtensions.Assistant.cs b/dotnet/src/Experimental/Assistants/Extensions/OpenAIRestExtensions.Assistant.cs
deleted file mode 100644
index 361f1249f0..0000000000
--- a/dotnet/src/Experimental/Assistants/Extensions/OpenAIRestExtensions.Assistant.cs
+++ /dev/null
@@ -1,136 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using System.Collections.Generic;
-using System.Globalization;
-using System.Threading;
-using System.Threading.Tasks;
-using System.Web;
-using Microsoft.SemanticKernel.Experimental.Assistants.Internal;
-using Microsoft.SemanticKernel.Experimental.Assistants.Models;
-
-namespace Microsoft.SemanticKernel.Experimental.Assistants;
-
-///
-/// Supported OpenAI REST API actions for assistants.
-///
-internal static partial class OpenAIRestExtensions
-{
- internal const string BaseAssistantUrl = $"{BaseUrl}/assistants";
-
- ///
- /// Create a new assistant.
- ///
- /// A context for accessing OpenAI REST endpoint
- /// The assistant definition
- /// A cancellation token
- /// An assistant definition
- public static Task CreateAssistantModelAsync(
- this OpenAIRestContext context,
- AssistantModel model,
- CancellationToken cancellationToken = default)
- {
- var payload =
- new
- {
- model = model.Model,
- name = model.Name,
- description = model.Description,
- instructions = model.Instructions,
- tools = model.Tools,
- file_ids = model.FileIds,
- metadata = model.Metadata,
- };
-
- return
- context.ExecutePostAsync(
- BaseAssistantUrl,
- payload,
- cancellationToken);
- }
-
- ///
- /// Retrieve an assistant by identifier.
- ///
- /// A context for accessing OpenAI REST endpoint
- /// The assistant identifier
- /// A cancellation token
- /// An assistant definition
- public static Task GetAssistantModelAsync(
- this OpenAIRestContext context,
- string assistantId,
- CancellationToken cancellationToken = default)
- {
- return
- context.ExecuteGetAsync(
- GetAssistantUrl(assistantId),
- cancellationToken);
- }
-
- ///
- /// Retrieve all assistants.
- ///
- /// A context for accessing OpenAI REST endpoint
- /// A limit on the number of objects to be returned.
- /// Limit can range between 1 and 100, and the default is 20.
- /// Set to true to sort by ascending created_at timestamp
- /// instead of descending.
- /// A cursor for use in pagination. This is an object ID that defines
- /// your place in the list. For instance, if you make a list request and receive 100 objects,
- /// ending with obj_foo, your subsequent call can include after=obj_foo in order to
- /// fetch the next page of the list.
- /// A cursor for use in pagination. This is an object ID that defines
- /// your place in the list. For instance, if you make a list request and receive 100 objects,
- /// ending with obj_foo, your subsequent call can include before=obj_foo in order to
- /// fetch the previous page of the list.
- /// List of retrieved Assistants
- /// A cancellation token
- /// An enumeration of assistant definitions
- public static async Task> ListAssistantModelsAsync(
- this OpenAIRestContext context,
- int limit = 20,
- bool ascending = false,
- string? after = null,
- string? before = null,
- CancellationToken cancellationToken = default)
- {
- var query = HttpUtility.ParseQueryString(string.Empty);
- query["limit"] = limit.ToString(CultureInfo.InvariantCulture);
- query["order"] = ascending ? "asc" : "desc";
- if (!string.IsNullOrWhiteSpace(after))
- {
- query["after"] = after;
- }
- if (!string.IsNullOrWhiteSpace(before))
- {
- query["before"] = before;
- }
-
- string requestUrl = string.Join("?", BaseAssistantUrl, query.ToString());
-
- var result =
- await context.ExecuteGetAsync(
- requestUrl,
- cancellationToken).ConfigureAwait(false);
-
- return result.Data;
- }
-
- ///
- /// Delete an existing assistant
- ///
- /// A context for accessing OpenAI REST endpoint
- /// Identifier of assistant to delete
- /// A cancellation token
- public static Task DeleteAssistantModelAsync(
- this OpenAIRestContext context,
- string id,
- CancellationToken cancellationToken = default)
- {
- return context.ExecuteDeleteAsync(GetAssistantUrl(id), cancellationToken);
- }
-
- internal static string GetAssistantUrl(string assistantId)
- {
- return $"{BaseAssistantUrl}/{assistantId}";
- }
-}
diff --git a/dotnet/src/Experimental/Assistants/Extensions/OpenAIRestExtensions.Messages.cs b/dotnet/src/Experimental/Assistants/Extensions/OpenAIRestExtensions.Messages.cs
deleted file mode 100644
index 61d426907b..0000000000
--- a/dotnet/src/Experimental/Assistants/Extensions/OpenAIRestExtensions.Messages.cs
+++ /dev/null
@@ -1,119 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using System.Collections.Generic;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-using Microsoft.SemanticKernel.ChatCompletion;
-using Microsoft.SemanticKernel.Experimental.Assistants.Internal;
-using Microsoft.SemanticKernel.Experimental.Assistants.Models;
-
-namespace Microsoft.SemanticKernel.Experimental.Assistants;
-
-///
-/// Supported OpenAI REST API actions for thread messages.
-///
-internal static partial class OpenAIRestExtensions
-{
- ///
- /// Create a new message.
- ///
- /// A context for accessing OpenAI REST endpoint
- /// The thread identifier
- /// The message text
- /// A cancellation token
- /// A message definition
- public static Task CreateUserTextMessageAsync(
- this OpenAIRestContext context,
- string threadId,
- string content,
- CancellationToken cancellationToken = default)
- {
- var payload =
- new
- {
- role = AuthorRole.User.Label,
- content,
- };
-
- return
- context.ExecutePostAsync(
- GetMessagesUrl(threadId),
- payload,
- cancellationToken);
- }
-
- ///
- /// Retrieve an message by identifier.
- ///
- /// A context for accessing OpenAI REST endpoint
- /// The thread identifier
- /// The message identifier
- /// A cancellation token
- /// A message definition
- public static Task GetMessageAsync(
- this OpenAIRestContext context,
- string threadId,
- string messageId,
- CancellationToken cancellationToken = default)
- {
- return
- context.ExecuteGetAsync(
- GetMessagesUrl(threadId, messageId),
- cancellationToken);
- }
-
- ///
- /// Retrieve all thread messages.
- ///
- /// A context for accessing OpenAI REST endpoint
- /// The thread identifier
- /// A cancellation token
- /// A message list definition
- public static Task GetMessagesAsync(
- this OpenAIRestContext context,
- string threadId,
- CancellationToken cancellationToken = default)
- {
- return
- context.ExecuteGetAsync(
- GetMessagesUrl(threadId),
- cancellationToken);
- }
-
- ///
- /// Retrieve all thread messages.
- ///
- /// A context for accessing OpenAI REST endpoint
- /// The thread identifier
- /// The set of message identifiers to retrieve
- /// A cancellation token
- /// A message list definition
- public static async Task> GetMessagesAsync(
- this OpenAIRestContext context,
- string threadId,
- IEnumerable messageIds,
- CancellationToken cancellationToken = default)
- {
- var tasks =
- messageIds.Select(
- id =>
- context.ExecuteGetAsync(
- GetMessagesUrl(threadId, id),
- cancellationToken)).ToArray();
-
- await Task.WhenAll(tasks).ConfigureAwait(false);
-
- return tasks.Select(t => t.Result).ToArray();
- }
-
- internal static string GetMessagesUrl(string threadId)
- {
- return $"{BaseThreadUrl}/{threadId}/messages";
- }
-
- internal static string GetMessagesUrl(string threadId, string messageId)
- {
- return $"{BaseThreadUrl}/{threadId}/messages/{messageId}";
- }
-}
diff --git a/dotnet/src/Experimental/Assistants/Extensions/OpenAIRestExtensions.Run.cs b/dotnet/src/Experimental/Assistants/Extensions/OpenAIRestExtensions.Run.cs
deleted file mode 100644
index 989ca93a8a..0000000000
--- a/dotnet/src/Experimental/Assistants/Extensions/OpenAIRestExtensions.Run.cs
+++ /dev/null
@@ -1,137 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using System.Collections.Generic;
-using System.Threading;
-using System.Threading.Tasks;
-using Microsoft.SemanticKernel.Experimental.Assistants.Internal;
-using Microsoft.SemanticKernel.Experimental.Assistants.Models;
-
-namespace Microsoft.SemanticKernel.Experimental.Assistants;
-
-///
-/// Supported OpenAI REST API actions for thread runs.
-///
-internal static partial class OpenAIRestExtensions
-{
- ///
- /// Create a new run.
- ///
- /// A context for accessing OpenAI REST endpoint
- /// A thread identifier
- /// The assistant identifier
- /// Optional instruction override
- /// The assistant tools
- /// A cancellation token
- /// A run definition
- public static Task CreateRunAsync(
- this OpenAIRestContext context,
- string threadId,
- string assistantId,
- string? instructions = null,
- IEnumerable? tools = null,
- CancellationToken cancellationToken = default)
- {
- var payload =
- new
- {
- assistant_id = assistantId,
- instructions,
- tools,
- };
-
- return
- context.ExecutePostAsync(
- GetRunUrl(threadId),
- payload,
- cancellationToken);
- }
-
- ///
- /// Retrieve an run by identifier.
- ///
- /// A context for accessing OpenAI REST endpoint
- /// A thread identifier
- /// A run identifier
- /// A cancellation token
- /// A run definition
- public static Task GetRunAsync(
- this OpenAIRestContext context,
- string threadId,
- string runId,
- CancellationToken cancellationToken = default)
- {
- return
- context.ExecuteGetAsync(
- GetRunUrl(threadId, runId),
- cancellationToken);
- }
-
- ///
- /// Retrieve run steps by identifier.
- ///
- /// A context for accessing OpenAI REST endpoint
- /// A thread identifier
- /// A run identifier
- /// A cancellation token
- /// A set of run steps
- public static Task GetRunStepsAsync(
- this OpenAIRestContext context,
- string threadId,
- string runId,
- CancellationToken cancellationToken = default)
- {
- return
- context.ExecuteGetAsync(
- GetRunStepsUrl(threadId, runId),
- cancellationToken);
- }
-
- ///
- /// Add a function result for a run.
- ///
- /// A context for accessing OpenAI REST endpoint
- /// A thread identifier
- /// The run identifier
- /// The function/tool results.
- /// A cancellation token
- /// A run definition
- public static Task AddToolOutputsAsync(
- this OpenAIRestContext context,
- string threadId,
- string runId,
- IEnumerable results,
- CancellationToken cancellationToken = default)
- {
- var payload =
- new
- {
- tool_outputs = results
- };
-
- return
- context.ExecutePostAsync(
- GetRunToolOutput(threadId, runId),
- payload,
- cancellationToken);
- }
-
- internal static string GetRunUrl(string threadId)
- {
- return $"{BaseThreadUrl}/{threadId}/runs";
- }
-
- internal static string GetRunUrl(string threadId, string runId)
- {
- return $"{BaseThreadUrl}/{threadId}/runs/{runId}";
- }
-
- internal static string GetRunStepsUrl(string threadId, string runId)
- {
- return $"{BaseThreadUrl}/{threadId}/runs/{runId}/steps";
- }
-
- internal static string GetRunToolOutput(string threadId, string runId)
- {
- return $"{BaseThreadUrl}/{threadId}/runs/{runId}/submit_tool_outputs";
- }
-}
diff --git a/dotnet/src/Experimental/Assistants/Extensions/OpenAIRestExtensions.Thread.cs b/dotnet/src/Experimental/Assistants/Extensions/OpenAIRestExtensions.Thread.cs
deleted file mode 100644
index 711b0e25e8..0000000000
--- a/dotnet/src/Experimental/Assistants/Extensions/OpenAIRestExtensions.Thread.cs
+++ /dev/null
@@ -1,69 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using System.Threading;
-using System.Threading.Tasks;
-using Microsoft.SemanticKernel.Experimental.Assistants.Internal;
-using Microsoft.SemanticKernel.Experimental.Assistants.Models;
-
-namespace Microsoft.SemanticKernel.Experimental.Assistants;
-
-///
-/// Supported OpenAI REST API actions for threads.
-///
-internal static partial class OpenAIRestExtensions
-{
- internal const string BaseThreadUrl = $"{BaseUrl}/threads";
-
- ///
- /// Create a new thread.
- ///
- /// A context for accessing OpenAI REST endpoint
- /// A cancellation token
- /// A thread definition
- public static Task CreateThreadModelAsync(
- this OpenAIRestContext context,
- CancellationToken cancellationToken = default)
- {
- return
- context.ExecutePostAsync(
- BaseThreadUrl,
- cancellationToken);
- }
-
- ///
- /// Retrieve an thread by identifier.
- ///
- /// A context for accessing OpenAI REST endpoint
- /// The thread identifier
- /// A cancellation token
- /// A thread definition
- public static Task GetThreadModelAsync(
- this OpenAIRestContext context,
- string threadId,
- CancellationToken cancellationToken = default)
- {
- return
- context.ExecuteGetAsync(
- GetThreadUrl(threadId),
- cancellationToken);
- }
-
- ///
- /// Delete an existing thread.
- ///
- /// A context for accessing OpenAI REST endpoint
- /// Identifier of thread to delete
- /// A cancellation token
- public static Task DeleteThreadModelAsync(
- this OpenAIRestContext context,
- string id,
- CancellationToken cancellationToken = default)
- {
- return context.ExecuteDeleteAsync(GetThreadUrl(id), cancellationToken);
- }
-
- internal static string GetThreadUrl(string threadId)
- {
- return $"{BaseThreadUrl}/{threadId}";
- }
-}
diff --git a/dotnet/src/Experimental/Assistants/Extensions/OpenAIRestExtensions.cs b/dotnet/src/Experimental/Assistants/Extensions/OpenAIRestExtensions.cs
deleted file mode 100644
index e98198311a..0000000000
--- a/dotnet/src/Experimental/Assistants/Extensions/OpenAIRestExtensions.cs
+++ /dev/null
@@ -1,91 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using System.Text.Json;
-using System.Threading;
-using System.Threading.Tasks;
-using Microsoft.SemanticKernel.Experimental.Assistants.Exceptions;
-using Microsoft.SemanticKernel.Experimental.Assistants.Internal;
-
-namespace Microsoft.SemanticKernel.Experimental.Assistants;
-
-internal static partial class OpenAIRestExtensions
-{
- private const string BaseUrl = "https://api.openai.com/v1";
- private const string HeaderNameOpenAIAssistant = "OpenAI-Beta";
- private const string HeaderNameAuthorization = "Authorization";
- private const string HeaderOpenAIValueAssistant = "assistants=v1";
-
- private static async Task ExecuteGetAsync(
- this OpenAIRestContext context,
- string url,
- CancellationToken cancellationToken = default)
- {
- using var request = HttpRequest.CreateGetRequest(url);
-
- request.Headers.Add(HeaderNameAuthorization, $"Bearer {context.ApiKey}");
- request.Headers.Add(HeaderNameOpenAIAssistant, HeaderOpenAIValueAssistant);
-
- using var response = await context.GetHttpClient().SendAsync(request, cancellationToken).ConfigureAwait(false);
- if (!response.IsSuccessStatusCode)
- {
- throw new AssistantException($"Unexpected failure: {response.StatusCode} [{url}]");
- }
-
- string responseBody = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
-
- // Common case is for failure exception to be raised by REST invocation.
- // Null result is a logical possibility, but unlikely edge case.
- // Might occur due to model alignment issues over time.
- return
- JsonSerializer.Deserialize(responseBody) ??
- throw new AssistantException($"Null result processing: {typeof(TResult).Name}");
- }
-
- private static Task ExecutePostAsync(
- this OpenAIRestContext context,
- string url,
- CancellationToken cancellationToken = default)
- {
- return context.ExecutePostAsync(url, payload: null, cancellationToken);
- }
-
- private static async Task ExecutePostAsync(
- this OpenAIRestContext context,
- string url,
- object? payload,
- CancellationToken cancellationToken = default)
- {
- using var request = HttpRequest.CreatePostRequest(url, payload);
-
- request.Headers.Add(HeaderNameAuthorization, $"Bearer {context.ApiKey}");
- request.Headers.Add(HeaderNameOpenAIAssistant, HeaderOpenAIValueAssistant);
-
- using var response = await context.GetHttpClient().SendAsync(request, cancellationToken).ConfigureAwait(false);
- if (!response.IsSuccessStatusCode)
- {
- throw new AssistantException($"Unexpected failure: {response.StatusCode} [{url}]");
- }
-
- string responseBody = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
- return
- JsonSerializer.Deserialize(responseBody) ??
- throw new AssistantException($"Null result processing: {typeof(TResult).Name}");
- }
-
- private static async Task ExecuteDeleteAsync(
- this OpenAIRestContext context,
- string url,
- CancellationToken cancellationToken = default)
- {
- using var request = HttpRequest.CreateDeleteRequest(url);
-
- request.Headers.Add(HeaderNameAuthorization, $"Bearer {context.ApiKey}");
- request.Headers.Add(HeaderNameOpenAIAssistant, HeaderOpenAIValueAssistant);
-
- using var response = await context.GetHttpClient().SendAsync(request, cancellationToken).ConfigureAwait(false);
- if (!response.IsSuccessStatusCode)
- {
- throw new AssistantException($"Unexpected failure: {response.StatusCode} [{url}]");
- }
- }
-}
diff --git a/dotnet/src/Experimental/Assistants/IAssistant.cs b/dotnet/src/Experimental/Assistants/IAssistant.cs
deleted file mode 100644
index 5530e33ec8..0000000000
--- a/dotnet/src/Experimental/Assistants/IAssistant.cs
+++ /dev/null
@@ -1,93 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace Microsoft.SemanticKernel.Experimental.Assistants;
-
-///
-/// Represents an assistant that can call the model and use tools.
-///
-public interface IAssistant
-{
- ///
- /// The assistant identifier (which can be referenced in API endpoints).
- ///
- string Id { get; }
-
- ///
- /// Always "assistant"
- ///
-#pragma warning disable CA1720 // Identifier contains type name - We don't control the schema
-#pragma warning disable CA1716 // Identifiers should not match keywords
- string Object { get; }
-#pragma warning restore CA1716 // Identifiers should not match keywords
-#pragma warning restore CA1720 // Identifier contains type name
-
- ///
- /// Unix timestamp (in seconds) for when the assistant was created
- ///
- long CreatedAt { get; }
-
- ///
- /// Name of the assistant
- ///
- string? Name { get; }
-
- ///
- /// The description of the assistant
- ///
- string? Description { get; }
-
- ///
- /// ID of the model to use
- ///
- string Model { get; }
-
- ///
- /// The system instructions that the assistant uses
- ///
- string Instructions { get; }
-
- ///
- /// A semantic-kernel instance associated with the assistant.
- ///
- internal Kernel Kernel { get; }
-
- ///
- /// Tools defined for run execution.
- ///
- public KernelPluginCollection Plugins { get; }
-
- ///
- /// Expose the assistant as a plugin.
- ///
- public AssistantPlugin AsPlugin();
-
- ///
- /// Creates a new assistant chat thread.
- ///
- /// A cancellation token
- Task NewThreadAsync(CancellationToken cancellationToken = default);
-
- ///
- /// Gets an existing assistant chat thread.
- ///
- /// The id of the existing chat thread.
- /// A cancellation token
- Task GetThreadAsync(string id, CancellationToken cancellationToken = default);
-
- ///
- /// Deletes an existing assistant chat thread.
- ///
- /// The id of the existing chat thread. Allows for null-fallthrough to simplify caller patterns.
- /// A cancellation token
- Task DeleteThreadAsync(string? id, CancellationToken cancellationToken = default);
-
- ///
- /// Delete current assistant. Terminal state - Unable to perform any
- /// subsequent actions.
- ///
- /// A cancellation token
- Task DeleteAsync(CancellationToken cancellationToken = default);
-}
diff --git a/dotnet/src/Experimental/Assistants/IAssistantExtensions.cs b/dotnet/src/Experimental/Assistants/IAssistantExtensions.cs
deleted file mode 100644
index 26ebecc32d..0000000000
--- a/dotnet/src/Experimental/Assistants/IAssistantExtensions.cs
+++ /dev/null
@@ -1,39 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using System.Collections.Generic;
-using System.Runtime.CompilerServices;
-using System.Threading;
-
-namespace Microsoft.SemanticKernel.Experimental.Assistants;
-
-///
-/// Convenience actions for .
-///
-public static class IAssistantExtensions
-{
- ///
- /// Invoke assistant with user input
- ///
- /// the assistant
- /// the user input
- /// a cancel token
- /// chat messages
- public static async IAsyncEnumerable InvokeAsync(
- this IAssistant assistant,
- string input,
- [EnumeratorCancellation] CancellationToken cancellationToken = default)
- {
- IChatThread thread = await assistant.NewThreadAsync(cancellationToken).ConfigureAwait(false);
- try
- {
- await foreach (var message in thread.InvokeAsync(assistant, input, cancellationToken))
- {
- yield return message;
- }
- }
- finally
- {
- await thread.DeleteAsync(cancellationToken).ConfigureAwait(false);
- }
- }
-}
diff --git a/dotnet/src/Experimental/Assistants/IChatMessage.cs b/dotnet/src/Experimental/Assistants/IChatMessage.cs
deleted file mode 100644
index 05e0e51c60..0000000000
--- a/dotnet/src/Experimental/Assistants/IChatMessage.cs
+++ /dev/null
@@ -1,36 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using System.Collections.ObjectModel;
-
-namespace Microsoft.SemanticKernel.Experimental.Assistants;
-
-///
-/// Represents a message that is part of an assistant thread.
-///
-public interface IChatMessage
-{
- ///
- /// The message identifier (which can be referenced in API endpoints).
- ///
- string Id { get; }
-
- ///
- /// The id of the assistant associated with the a message where role = "assistant", otherwise null.
- ///
- string? AssistantId { get; }
-
- ///
- /// The chat message content.
- ///
- string Content { get; }
-
- ///
- /// The role associated with the chat message.
- ///
- string Role { get; }
-
- ///
- /// Properties associated with the message.
- ///
- ReadOnlyDictionary Properties { get; }
-}
diff --git a/dotnet/src/Experimental/Assistants/IChatThread.cs b/dotnet/src/Experimental/Assistants/IChatThread.cs
deleted file mode 100644
index cd29324db8..0000000000
--- a/dotnet/src/Experimental/Assistants/IChatThread.cs
+++ /dev/null
@@ -1,50 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using System.Collections.Generic;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace Microsoft.SemanticKernel.Experimental.Assistants;
-
-///
-/// Represents a thread that contains messages.
-///
-public interface IChatThread
-{
- ///
- /// The thread identifier (which can be referenced in API endpoints).
- ///
- string Id { get; }
-
- ///
- /// Add a textual user message to the thread.
- ///
- /// The user message
- /// A cancellation token
- ///
- Task AddUserMessageAsync(string message, CancellationToken cancellationToken = default);
-
- ///
- /// Advance the thread with the specified assistant.
- ///
- /// An assistant instance.
- /// A cancellation token
- /// The resulting assistant message(s)
- IAsyncEnumerable InvokeAsync(IAssistant assistant, CancellationToken cancellationToken = default);
-
- ///
- /// Advance the thread with the specified assistant.
- ///
- /// An assistant instance.
- /// The user message
- /// A cancellation token
- /// The resulting assistant message(s)
- IAsyncEnumerable InvokeAsync(IAssistant assistant, string userMessage, CancellationToken cancellationToken = default);
-
- ///
- /// Delete current thread. Terminal state - Unable to perform any
- /// subsequent actions.
- ///
- /// A cancellation token
- Task DeleteAsync(CancellationToken cancellationToken = default);
-}
diff --git a/dotnet/src/Experimental/Assistants/Internal/Assistant.cs b/dotnet/src/Experimental/Assistants/Internal/Assistant.cs
deleted file mode 100644
index 5d8b1585e3..0000000000
--- a/dotnet/src/Experimental/Assistants/Internal/Assistant.cs
+++ /dev/null
@@ -1,227 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using System;
-using System.Collections.Generic;
-using System.ComponentModel;
-using System.Diagnostics.CodeAnalysis;
-using System.Linq;
-using System.Text.RegularExpressions;
-using System.Threading;
-using System.Threading.Tasks;
-using Microsoft.SemanticKernel.Experimental.Assistants.Exceptions;
-using Microsoft.SemanticKernel.Experimental.Assistants.Models;
-
-namespace Microsoft.SemanticKernel.Experimental.Assistants.Internal;
-
-///
-/// Represents an assistant that can call the model and use tools.
-///
-internal sealed class Assistant : IAssistant
-{
- ///
- public string Id => this._model.Id;
-
- ///
- public Kernel Kernel { get; }
-
- ///
- public KernelPluginCollection Plugins => this.Kernel.Plugins;
-
- ///
-#pragma warning disable CA1720 // Identifier contains type name - We don't control the schema
-#pragma warning disable CA1716 // Identifiers should not match keywords
- public string Object => this._model.Object;
-#pragma warning restore CA1720 // Identifier contains type name - We don't control the schema
-#pragma warning restore CA1716 // Identifiers should not match keywords
-
- ///
- public long CreatedAt => this._model.CreatedAt;
-
- ///
- public string? Name => this._model.Name;
-
- ///
- public string? Description => this._model.Description;
-
- ///
- public string Model => this._model.Model;
-
- ///
- public string Instructions => this._model.Instructions;
-
- private static readonly Regex s_removeInvalidCharsRegex = new("[^0-9A-Za-z-]");
-
- private readonly OpenAIRestContext _restContext;
- private readonly AssistantModel _model;
-
- private AssistantPlugin? _assistantPlugin;
- private bool _isDeleted;
-
- ///
- /// Create a new assistant.
- ///
- /// A context for accessing OpenAI REST endpoint
- /// The assistant definition
- /// Plugins to initialize as assistant tools
- /// A cancellation token
- /// An initialized instance.
- public static async Task CreateAsync(
- OpenAIRestContext restContext,
- AssistantModel assistantModel,
- IEnumerable? plugins = null,
- CancellationToken cancellationToken = default)
- {
- var resultModel = await restContext.CreateAssistantModelAsync(assistantModel, cancellationToken).ConfigureAwait(false);
-
- return new Assistant(resultModel, restContext, plugins);
- }
-
- ///
- /// Initializes a new instance of the class.
- ///
- internal Assistant(
- AssistantModel model,
- OpenAIRestContext restContext,
- IEnumerable? plugins = null)
- {
- this._model = model;
- this._restContext = restContext;
-
- IKernelBuilder builder = Kernel.CreateBuilder();
- ;
- this.Kernel =
- Kernel
- .CreateBuilder()
- .AddOpenAIChatCompletion(this._model.Model, this._restContext.ApiKey)
- .Build();
-
- if (plugins is not null)
- {
- this.Kernel.Plugins.AddRange(plugins);
- }
- }
-
- public AssistantPlugin AsPlugin() => this._assistantPlugin ??= this.DefinePlugin();
-
- ///
- public Task NewThreadAsync(CancellationToken cancellationToken = default)
- {
- this.ThrowIfDeleted();
-
- return ChatThread.CreateAsync(this._restContext, cancellationToken);
- }
-
- ///
- public Task GetThreadAsync(string id, CancellationToken cancellationToken = default)
- {
- this.ThrowIfDeleted();
-
- return ChatThread.GetAsync(this._restContext, id, cancellationToken);
- }
-
- ///
- public async Task DeleteThreadAsync(string? id, CancellationToken cancellationToken = default)
- {
- if (string.IsNullOrWhiteSpace(id))
- {
- return;
- }
-
- await this._restContext.DeleteThreadModelAsync(id!, cancellationToken).ConfigureAwait(false);
- }
-
- ///
- public async Task DeleteAsync(CancellationToken cancellationToken = default)
- {
- if (this._isDeleted)
- {
- return;
- }
-
- await this._restContext.DeleteAssistantModelAsync(this.Id, cancellationToken).ConfigureAwait(false);
- this._isDeleted = true;
- }
-
- ///
- /// Marshal thread run through interface.
- ///
- /// The user input
- /// A cancellation token.
- /// An assistant response (
- private async Task AskAsync(
- [Description("The user message provided to the assistant.")]
- string input,
- CancellationToken cancellationToken = default)
- {
- var thread = await this.NewThreadAsync(cancellationToken).ConfigureAwait(false);
- try
- {
- await thread.AddUserMessageAsync(input, cancellationToken).ConfigureAwait(false);
-
- var messages = await thread.InvokeAsync(this, cancellationToken).ToArrayAsync(cancellationToken).ConfigureAwait(false);
- var response =
- new AssistantResponse
- {
- ThreadId = thread.Id,
- Message = string.Concat(messages.Select(m => m.Content)),
- };
-
- return response;
- }
- finally
- {
- await thread.DeleteAsync(cancellationToken).ConfigureAwait(false);
- }
- }
-
- private AssistantPluginImpl DefinePlugin()
- {
- var functionAsk = KernelFunctionFactory.CreateFromMethod(this.AskAsync, description: this.Description);
-
- return new AssistantPluginImpl(this, functionAsk);
- }
-
- private void ThrowIfDeleted()
- {
- if (this._isDeleted)
- {
- throw new AssistantException($"{nameof(Assistant)}: {this.Id} has been deleted.");
- }
- }
-
- private sealed class AssistantPluginImpl : AssistantPlugin
- {
- public KernelFunction FunctionAsk { get; }
-
- internal override Assistant Assistant { get; }
-
- public override int FunctionCount => 1;
-
- private static readonly string s_functionName = nameof(Assistant.AskAsync).Substring(0, nameof(Assistant.AskAsync).Length - 5);
-
- public AssistantPluginImpl(Assistant assistant, KernelFunction functionAsk)
- : base(s_removeInvalidCharsRegex.Replace(assistant.Name ?? assistant.Id, string.Empty),
- assistant.Description ?? assistant.Instructions)
- {
- this.Assistant = assistant;
- this.FunctionAsk = functionAsk;
- }
-
- public override IEnumerator GetEnumerator()
- {
- yield return this.FunctionAsk;
- }
-
- public override bool TryGetFunction(string name, [NotNullWhen(true)] out KernelFunction? function)
- {
- function = null;
-
- if (s_functionName.Equals(name, StringComparison.OrdinalIgnoreCase))
- {
- function = this.FunctionAsk;
- }
-
- return function != null;
- }
- }
-}
diff --git a/dotnet/src/Experimental/Assistants/Internal/ChatMessage.cs b/dotnet/src/Experimental/Assistants/Internal/ChatMessage.cs
deleted file mode 100644
index 8842f4a916..0000000000
--- a/dotnet/src/Experimental/Assistants/Internal/ChatMessage.cs
+++ /dev/null
@@ -1,44 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using System.Collections.Generic;
-using System.Collections.ObjectModel;
-using System.Linq;
-using Microsoft.SemanticKernel.Experimental.Assistants.Models;
-
-namespace Microsoft.SemanticKernel.Experimental.Assistants.Internal;
-
-///
-/// Represents a message that is part of an assistant thread.
-///
-internal sealed class ChatMessage : IChatMessage
-{
- ///
- public string Id { get; }
-
- ///
- public string? AssistantId { get; }
-
- ///
- public string Content { get; }
-
- ///
- public string Role { get; }
-
- ///
- public ReadOnlyDictionary Properties { get; }
-
- ///
- /// Initializes a new instance of the