Skip to content

.Net: Add AssemblyAI file service Remake from #5964 #6406

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 0 additions & 14 deletions dotnet/SK-dotnet.sln
Original file line number Diff line number Diff line change
Expand Up @@ -599,18 +599,6 @@ Global
{1F96837A-61EC-4C8F-904A-07BEBD05FDEE}.Publish|Any CPU.Build.0 = Debug|Any CPU
{1F96837A-61EC-4C8F-904A-07BEBD05FDEE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1F96837A-61EC-4C8F-904A-07BEBD05FDEE}.Release|Any CPU.Build.0 = Release|Any CPU
{13429BD6-4C4E-45EC-81AD-30BAC380AA60}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{13429BD6-4C4E-45EC-81AD-30BAC380AA60}.Debug|Any CPU.Build.0 = Debug|Any CPU
{13429BD6-4C4E-45EC-81AD-30BAC380AA60}.Publish|Any CPU.ActiveCfg = Debug|Any CPU
{13429BD6-4C4E-45EC-81AD-30BAC380AA60}.Publish|Any CPU.Build.0 = Debug|Any CPU
{13429BD6-4C4E-45EC-81AD-30BAC380AA60}.Release|Any CPU.ActiveCfg = Release|Any CPU
{13429BD6-4C4E-45EC-81AD-30BAC380AA60}.Release|Any CPU.Build.0 = Release|Any CPU
{8EE10EB0-A947-49CC-BCC1-18D93415B9E4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8EE10EB0-A947-49CC-BCC1-18D93415B9E4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8EE10EB0-A947-49CC-BCC1-18D93415B9E4}.Publish|Any CPU.ActiveCfg = Debug|Any CPU
{8EE10EB0-A947-49CC-BCC1-18D93415B9E4}.Publish|Any CPU.Build.0 = Debug|Any CPU
{8EE10EB0-A947-49CC-BCC1-18D93415B9E4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8EE10EB0-A947-49CC-BCC1-18D93415B9E4}.Release|Any CPU.Build.0 = Release|Any CPU
{3560310D-8E51-42EA-BC8F-D73F1EF52318}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3560310D-8E51-42EA-BC8F-D73F1EF52318}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3560310D-8E51-42EA-BC8F-D73F1EF52318}.Publish|Any CPU.ActiveCfg = Publish|Any CPU
Expand Down Expand Up @@ -870,8 +858,6 @@ Global
{607DD6FA-FA0D-45E6-80BA-22A373609E89} = {5C246969-D794-4EC3-8E8F-F90D4D166420}
{BCDD5B96-CCC3-46B9-8217-89CD5885F6A2} = {0247C2C9-86C3-45BA-8873-28B0948EDC0C}
{1F96837A-61EC-4C8F-904A-07BEBD05FDEE} = {1B4CBDE0-10C2-4E7D-9CD0-FE7586C96ED1}
{13429BD6-4C4E-45EC-81AD-30BAC380AA60} = {FA3720F1-C99A-49B2-9577-A940257098BF}
{8EE10EB0-A947-49CC-BCC1-18D93415B9E4} = {FA3720F1-C99A-49B2-9577-A940257098BF}
{3560310D-8E51-42EA-BC8F-D73F1EF52318} = {1B4CBDE0-10C2-4E7D-9CD0-FE7586C96ED1}
{CF31162C-DAA8-497A-9088-0FCECE46439B} = {1B4CBDE0-10C2-4E7D-9CD0-FE7586C96ED1}
{14461919-E88D-49A9-BE8C-DF704CB79122} = {1B4CBDE0-10C2-4E7D-9CD0-FE7586C96ED1}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Copyright (c) Microsoft. All rights reserved.

using System;
using System.Net.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Connectors.AssemblyAI;
using Xunit;

namespace SemanticKernel.Connectors.AssemblyAI.UnitTests;

/// <summary>
/// Unit tests for <see cref="AssemblyAIServiceCollectionExtensions"/> class.
/// </summary>
public sealed class AssemblyAIFilesExtensionsTests
{
private const string ApiKey = "Test123";
private const string Endpoint = "http://localhost:1234/";
private const string ServiceId = "AssemblyAI";

[Fact]
public void AddServiceToKernelBuilder()
{
// Arrange & Act
using var httpClient = new HttpClient();
var kernel = Kernel.CreateBuilder()
.AddAssemblyAIFiles(
apiKey: ApiKey,
endpoint: new Uri(Endpoint),
serviceId: ServiceId,
httpClient: httpClient
)
.Build();

// Assert
var service = kernel.GetRequiredService<AssemblyAIFileService>();
Assert.NotNull(service);
Assert.IsType<AssemblyAIFileService>(service);

service = kernel.GetRequiredService<AssemblyAIFileService>(ServiceId);
Assert.NotNull(service);
Assert.IsType<AssemblyAIFileService>(service);
}

[Fact]
public void AddServiceToServiceCollection()
{
// Arrange & Act
var services = new ServiceCollection();
services.AddAssemblyAIFiles(
apiKey: ApiKey,
endpoint: new Uri(Endpoint),
serviceId: ServiceId
);
using var provider = services.BuildServiceProvider();

// Assert
var service = provider.GetRequiredKeyedService<AssemblyAIFileService>(ServiceId);
Assert.NotNull(service);
Assert.IsType<AssemblyAIFileService>(service);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@
using Microsoft.SemanticKernel.Connectors.AssemblyAI;
using Xunit;

namespace SemanticKernel.Connectors.UnitTests.AssemblyAI;
namespace SemanticKernel.Connectors.AssemblyAI.UnitTests;

/// <summary>
/// Unit tests for <see cref="AssemblyAIServiceCollectionExtensions"/> class.
/// </summary>
public sealed class AssemblyAIAudioToTextServiceExtensionsTests
public sealed class AssemblyAIServiceCollectionExtensionsTests
{
private const string ApiKey = "Test123";
private const string Endpoint = "http://localhost:1234/";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<Nullable>enable</Nullable>
<ImplicitUsings>disable</ImplicitUsings>
<IsPackable>false</IsPackable>
<NoWarn>SKEXP0001;SKEXP0005;SKEXP0070;CS1591</NoWarn>
<NoWarn>SKEXP0001;SKEXP0070;CS1591</NoWarn>
</PropertyGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
// Copyright (c) Microsoft. All rights reserved.

using System;
using System.IO;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Connectors.AssemblyAI;
using SemanticKernel.Connectors.AssemblyAI.UnitTests;
using Xunit;

namespace SemanticKernel.Connectors.UnitTests.AssemblyAI;
namespace SemanticKernel.Connectors.AssemblyAI.UnitTests;

/// <summary>
/// Unit tests for <see cref="AssemblyAIAudioToTextService"/> class.
Expand Down Expand Up @@ -116,38 +114,6 @@ public async Task GetTextContentByUrlWorksCorrectlyAsync()
Assert.Equal(ExpectedTranscriptText, result[0].Text);
}

[Fact]
public async Task GetTextContentByStreamWorksCorrectlyAsync()
{
// Arrange
var service = new AssemblyAIAudioToTextService("api-key", httpClient: this._httpClient);
using var uploadFileResponse = new HttpResponseMessage(System.Net.HttpStatusCode.OK);
uploadFileResponse.Content = new StringContent(UploadFileResponseContent);
using var transcribeResponse = new HttpResponseMessage(System.Net.HttpStatusCode.OK);
transcribeResponse.Content = new StringContent(CreateTranscriptResponseContent);
using var transcribedResponse = new HttpResponseMessage(System.Net.HttpStatusCode.OK);
transcribedResponse.Content = new StringContent(TranscriptCompletedResponseContent);
this._messageHandlerStub.ResponsesToReturn =
[
uploadFileResponse,
transcribeResponse,
transcribedResponse
];

using var ms = new MemoryStream();

// Act
var result = await service.GetTextContentsAsync(
new AudioStreamContent(ms)
).ConfigureAwait(true);

// Assert
Assert.NotNull(result);
Assert.NotNull(result);
Assert.Single(result);
Assert.Equal(ExpectedTranscriptText, result[0].Text);
}

[Fact]
public async Task HttpErrorShouldThrowWithErrorMessageAsync()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// Copyright (c) Microsoft. All rights reserved.

using System;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Connectors.AssemblyAI;
using Xunit;

namespace SemanticKernel.Connectors.AssemblyAI.UnitTests;

/// <summary>
/// Unit tests for <see cref="AssemblyAIAudioToTextService"/> class.
/// </summary>
public sealed class AssemblyAIFileServiceTests : IDisposable
{
private const string UploadedFileUrl = "http://localhost/path/to/file.mp3";

private const string UploadFileResponseContent =
$$"""
{
"upload_url": "{{UploadedFileUrl}}"
}
""";

private readonly MultipleHttpMessageHandlerStub _messageHandlerStub;
private readonly HttpClient _httpClient;

public AssemblyAIFileServiceTests()
{
this._messageHandlerStub = new MultipleHttpMessageHandlerStub();
this._httpClient = new HttpClient(this._messageHandlerStub, false);
}

[Fact]
public void ConstructorWithHttpClientWorksCorrectly()
{
// Arrange & Act
var service = new AssemblyAIAudioToTextService("api-key", httpClient: this._httpClient);

// Assert
Assert.NotNull(service);
}

[Fact]
public async Task UploadFileAsync()
{
// Arrange
var service = new AssemblyAIFileService("api-key", httpClient: this._httpClient);
using var uploadFileResponse = new HttpResponseMessage(System.Net.HttpStatusCode.OK);
uploadFileResponse.Content = new StringContent(UploadFileResponseContent);
using var transcribeResponse = new HttpResponseMessage(System.Net.HttpStatusCode.OK);
this._messageHandlerStub.ResponsesToReturn =
[
uploadFileResponse,
];
using var stream = new BinaryData("data").ToStream();

// Act
var result = await service.UploadAsync(stream).ConfigureAwait(true);

// Assert
Assert.NotNull(result);
Assert.Null(result.Data);
Assert.Equal(new Uri(UploadedFileUrl), result.Uri);
}

[Fact]
public async Task HttpErrorShouldThrowWithErrorMessageAsync()
{
// Arrange
var service = new AssemblyAIFileService("api-key", httpClient: this._httpClient);
using var uploadFileResponse = new HttpResponseMessage(System.Net.HttpStatusCode.InternalServerError);
this._messageHandlerStub.ResponsesToReturn =
[
uploadFileResponse
];
using var stream = new BinaryData("data").ToStream();
// Act & Assert
await Assert.ThrowsAsync<HttpOperationException>(
async () => await service.UploadAsync(stream).ConfigureAwait(true)
).ConfigureAwait(true);
}

[Fact]
public async Task JsonErrorShouldThrowWithErrorMessageAsync()
{
// Arrange
var service = new AssemblyAIFileService("api-key", httpClient: this._httpClient);
using var uploadFileResponse = new HttpResponseMessage(System.Net.HttpStatusCode.Unauthorized);
const string ErrorMessage = "Bad API key";
uploadFileResponse.Content = new StringContent(
$$"""
{
"error": "{{ErrorMessage}}"
}
""",
Encoding.UTF8,
"application/json"
);
this._messageHandlerStub.ResponsesToReturn =
[
uploadFileResponse
];
using var stream = new BinaryData("data").ToStream();

// Act & Assert
await Assert.ThrowsAsync<HttpOperationException>(
async () => await service.UploadAsync(stream).ConfigureAwait(true)
).ConfigureAwait(true);
}

public void Dispose()
{
this._httpClient.Dispose();
this._messageHandlerStub.Dispose();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ namespace Microsoft.SemanticKernel.Connectors.AssemblyAI;
/// <summary>
/// Execution settings for AssemblyAI speech-to-text execution.
/// </summary>
public class AssemblyAIAudioToTextExecutionSettings : PromptExecutionSettings
public sealed class AssemblyAIAudioToTextExecutionSettings : PromptExecutionSettings
{
/// <summary>
/// The time between each poll for the transcript status, until the status is completed.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,42 @@ public static IKernelBuilder AddAssemblyAIAudioToText(
{
Verify.NotNull(builder);

builder.Services.AddKeyedSingleton<IAudioToTextService>(serviceId, (serviceProvider, _)
builder.Services.AddKeyedSingleton<IAudioToTextService>(serviceId, (_, _)
=> new AssemblyAIAudioToTextService(
apiKey,
endpoint,
httpClient));

return builder;
}

/// <summary>
/// Adds the AssemblyAI file service to the kernel.
/// </summary>
/// <param name="builder">The <see cref="IKernelBuilder"/> instance to augment.</param>
/// <param name="apiKey">AssemblyAI API key, <a href="https://www.assemblyai.com/dashboard">get your API key from the dashboard.</a></param>
/// <param name="endpoint">The endpoint URL to the AssemblyAI API.</param>
/// <param name="serviceId">A local identifier for the given AI service.</param>
/// <param name="httpClient">The HttpClient to use with this service.</param>
/// <returns>The same instance as <paramref name="builder"/>.</returns>
public static IKernelBuilder AddAssemblyAIFiles(
this IKernelBuilder builder,
string apiKey,
Uri? endpoint = null,
string? serviceId = null,
HttpClient? httpClient = null
)
{
Verify.NotNull(builder);

builder.Services.AddKeyedSingleton(serviceId, (_, _) =>
new AssemblyAIFileService(
apiKey,
endpoint,
httpClient
)
);

return builder;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,35 @@ public static IServiceCollection AddAssemblyAIAudioToText(
apiKey,
endpoint,
HttpClientProvider.GetHttpClient(serviceProvider)
));
)
);

return services;
}

/// <summary>
/// Adds the AssemblyAI file service to the list.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/> instance to augment.</param>
/// <param name="apiKey">AssemblyAI API key, <a href="https://www.assemblyai.com/dashboard">get your API key from the dashboard.</a></param>
/// <param name="endpoint">The endpoint URL to the AssemblyAI API.</param>
/// <param name="serviceId">A local identifier for the given AI service.</param>
/// <returns>The same instance as <paramref name="services"/>.</returns>
public static IServiceCollection AddAssemblyAIFiles(
this IServiceCollection services,
string apiKey,
Uri? endpoint = null,
string? serviceId = null
)
{
Verify.NotNull(services);
services.AddKeyedSingleton(serviceId, (serviceProvider, _) =>
new AssemblyAIFileService(
apiKey,
endpoint,
HttpClientProvider.GetHttpClient(serviceProvider)
)
);

return services;
}
Expand Down
Loading
Loading