Skip to content

Commit 6fd943b

Browse files
authored
Merge pull request #3 from AssemblyAI/niels/add-di
Refactor to add plugin using DI
2 parents 5a0afd2 + da954e5 commit 6fd943b

File tree

10 files changed

+375
-167
lines changed

10 files changed

+375
-167
lines changed

README.md

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ Add the [AssemblyAI.SemanticKernel NuGet package](https://www.nuget.org/packages
2222
dotnet add package AssemblyAI.SemanticKernel
2323
```
2424

25-
Next, register the `TranscriptPlugin` into your kernel:
25+
Next, register the `AssemblyAI` plugin into your kernel:
2626

2727
```csharp
2828
using AssemblyAI.SemanticKernel;
@@ -37,6 +37,7 @@ string apiKey = Environment.GetEnvironmentVariable("ASSEMBLYAI_API_KEY")
3737

3838
kernel.ImportPluginFromObject(
3939
new TranscriptPlugin(apiKey: apiKey)
40+
TranscriptPlugin.PluginName
4041
);
4142
```
4243

@@ -45,8 +46,8 @@ kernel.ImportPluginFromObject(
4546
Get the `Transcribe` function from the transcript plugin and invoke it with the context variables.
4647
```csharp
4748
var result = await kernel.InvokeAsync(
48-
nameof(TranscriptPlugin),
49-
TranscriptPlugin.TranscribeFunctionName,
49+
nameof(AssemblyAIPlugin),
50+
AssemblyAIPlugin.TranscribeFunctionName,
5051
new KernelArguments
5152
{
5253
["INPUT"] = "https://storage.googleapis.com/aai-docs-samples/espn.m4a"
@@ -58,7 +59,7 @@ Console.WriteLine(result.GetValue<string>());
5859
You can get the transcript using `result.GetValue<string>()`.
5960

6061
You can also upload local audio and video file. To do this:
61-
- Set the `TranscriptPlugin.AllowFileSystemAccess` property to `true`.
62+
- Set the `AssemblyAI:Plugin:AllowFileSystemAccess` configuration to `true`.
6263
- Configure the `INPUT` variable with a local file path.
6364

6465
```csharp
@@ -69,8 +70,8 @@ kernel.ImportPluginFromObject(
6970
}
7071
);
7172
var result = await kernel.InvokeAsync(
72-
nameof(TranscriptPlugin),
73-
TranscriptPlugin.TranscribeFunctionName,
73+
nameof(AssemblyAIPlugin),
74+
AssemblyAIPlugin.TranscribeFunctionName,
7475
new KernelArguments
7576
{
7677
["INPUT"] = "https://storage.googleapis.com/aai-docs-samples/espn.m4a"
@@ -84,7 +85,7 @@ You can also invoke the function from within a semantic function like this.
8485
```csharp
8586
const string prompt = """
8687
Here is a transcript:
87-
{{TranscriptPlugin.Transcribe "https://storage.googleapis.com/aai-docs-samples/espn.m4a"}}
88+
{{AssemblyAIPlugin.Transcribe "https://storage.googleapis.com/aai-docs-samples/espn.m4a"}}
8889
---
8990
Summarize the transcript.
9091
""";

src/AssemblyAI.SemanticKernel/AssemblyAI.SemanticKernel.csproj

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@
1111
<PackageTags>SemanticKernel;AI;AssemblyAI;transcript</PackageTags>
1212
<Company>AssemblyAI</Company>
1313
<Product>AssemblyAI</Product>
14-
<AssemblyVersion>1.0.3.0</AssemblyVersion>
15-
<FileVersion>1.0.3.0</FileVersion>
16-
<PackageVersion>1.0.3</PackageVersion>
14+
<AssemblyVersion>1.1.0.0</AssemblyVersion>
15+
<FileVersion>1.1.0.0</FileVersion>
16+
<PackageVersion>1.1.0</PackageVersion>
1717
<OutputType>Library</OutputType>
1818
<PackageLicenseExpression>MIT</PackageLicenseExpression>
1919
<PackageProjectUrl>https://github.com/AssemblyAI/assemblyai-semantic-kernel</PackageProjectUrl>
@@ -31,6 +31,12 @@
3131
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
3232
</PropertyGroup>
3333
<ItemGroup>
34+
<PackageReference Include="Microsoft.Extensions.Options">
35+
<Version>8.0.0</Version>
36+
</PackageReference>
37+
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions">
38+
<Version>8.0.0</Version>
39+
</PackageReference>
3440
<PackageReference Include="Microsoft.SemanticKernel">
3541
<Version>1.0.1</Version>
3642
</PackageReference>
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
using System;
2+
using System.ComponentModel;
3+
using System.IO;
4+
using System.Net.Http;
5+
using System.Net.Http.Headers;
6+
using System.Net.Http.Json;
7+
using System.Text;
8+
using System.Text.Json;
9+
using System.Threading.Tasks;
10+
using Microsoft.Extensions.DependencyInjection;
11+
using Microsoft.Extensions.Options;
12+
using Microsoft.SemanticKernel;
13+
14+
namespace AssemblyAI.SemanticKernel
15+
{
16+
public class AssemblyAIPlugin
17+
{
18+
internal AssemblyAIPluginOptions Options { get; }
19+
20+
private string ApiKey => Options.ApiKey;
21+
22+
private bool AllowFileSystemAccess => Options.AllowFileSystemAccess;
23+
24+
public AssemblyAIPlugin(string apiKey)
25+
{
26+
Options = new AssemblyAIPluginOptions
27+
{
28+
ApiKey = apiKey
29+
};
30+
}
31+
32+
public AssemblyAIPlugin(string apiKey, bool allowFileSystemAccess)
33+
{
34+
Options = new AssemblyAIPluginOptions
35+
{
36+
ApiKey = apiKey,
37+
AllowFileSystemAccess = allowFileSystemAccess
38+
};
39+
}
40+
41+
[ActivatorUtilitiesConstructor]
42+
public AssemblyAIPlugin(IOptions<AssemblyAIPluginOptions> options)
43+
{
44+
Options = options.Value;
45+
}
46+
47+
public const string TranscribeFunctionName = nameof(Transcribe);
48+
49+
[KernelFunction, Description("Transcribe an audio or video file to text.")]
50+
public async Task<string> Transcribe(
51+
[Description("The public URL or the local path of the audio or video file to transcribe.")]
52+
string input
53+
)
54+
{
55+
if (string.IsNullOrEmpty(input))
56+
{
57+
throw new Exception("The INPUT parameter is required.");
58+
}
59+
60+
using (var httpClient = new HttpClient())
61+
{
62+
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(ApiKey);
63+
string audioUrl;
64+
if (TryGetPath(input, out var filePath))
65+
{
66+
if (AllowFileSystemAccess == false)
67+
{
68+
throw new Exception(
69+
"You need to allow file system access to upload files. Set AssemblyAI:Plugin:AllowFileSystemAccess to true."
70+
);
71+
}
72+
73+
audioUrl = await UploadFileAsync(filePath, httpClient);
74+
}
75+
else
76+
{
77+
audioUrl = input;
78+
}
79+
80+
var transcript = await CreateTranscriptAsync(audioUrl, httpClient);
81+
transcript = await WaitForTranscriptToProcess(transcript, httpClient);
82+
return transcript.Text ?? throw new Exception("Transcript text is null. This should not happen.");
83+
}
84+
}
85+
86+
private static bool TryGetPath(string input, out string filePath)
87+
{
88+
if (Uri.TryCreate(input, UriKind.Absolute, out var inputUrl))
89+
{
90+
if (inputUrl.IsFile)
91+
{
92+
filePath = inputUrl.LocalPath;
93+
return true;
94+
}
95+
96+
filePath = null;
97+
return false;
98+
}
99+
100+
filePath = input;
101+
return true;
102+
}
103+
104+
private static async Task<string> UploadFileAsync(string path, HttpClient httpClient)
105+
{
106+
using (var fileStream = File.OpenRead(path))
107+
using (var fileContent = new StreamContent(fileStream))
108+
{
109+
fileContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
110+
using (var response = await httpClient.PostAsync("https://api.assemblyai.com/v2/upload", fileContent))
111+
{
112+
response.EnsureSuccessStatusCode();
113+
var jsonDoc = await response.Content.ReadFromJsonAsync<JsonDocument>();
114+
return jsonDoc?.RootElement.GetProperty("upload_url").GetString();
115+
}
116+
}
117+
}
118+
119+
private static async Task<Transcript> CreateTranscriptAsync(string audioUrl, HttpClient httpClient)
120+
{
121+
var jsonString = JsonSerializer.Serialize(new
122+
{
123+
audio_url = audioUrl
124+
});
125+
126+
var content = new StringContent(jsonString, Encoding.UTF8, "application/json");
127+
using (var response = await httpClient.PostAsync("https://api.assemblyai.com/v2/transcript", content))
128+
{
129+
response.EnsureSuccessStatusCode();
130+
var transcript = await response.Content.ReadFromJsonAsync<Transcript>();
131+
if (transcript.Status == "error") throw new Exception(transcript.Error);
132+
return transcript;
133+
}
134+
}
135+
136+
private static async Task<Transcript> WaitForTranscriptToProcess(Transcript transcript, HttpClient httpClient)
137+
{
138+
var pollingEndpoint = $"https://api.assemblyai.com/v2/transcript/{transcript.Id}";
139+
140+
while (true)
141+
{
142+
var pollingResponse = await httpClient.GetAsync(pollingEndpoint);
143+
pollingResponse.EnsureSuccessStatusCode();
144+
transcript = (await pollingResponse.Content.ReadFromJsonAsync<Transcript>());
145+
switch (transcript.Status)
146+
{
147+
case "processing":
148+
case "queued":
149+
await Task.Delay(TimeSpan.FromSeconds(3));
150+
break;
151+
case "completed":
152+
return transcript;
153+
case "error":
154+
throw new Exception(transcript.Error);
155+
default:
156+
throw new Exception("This code shouldn't be reachable.");
157+
}
158+
}
159+
}
160+
}
161+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
namespace AssemblyAI.SemanticKernel
2+
{
3+
/// <summary>
4+
/// Options to configure the AssemblyAI plugin with.
5+
/// </summary>
6+
public class AssemblyAIPluginOptions
7+
{
8+
/// <summary>
9+
/// The name of the plugin registered into Semantic Kernel.
10+
/// Defaults to "AssemblyAIPlugin".
11+
/// </summary>
12+
public string PluginName { get; set; }
13+
14+
/// <summary>
15+
/// The AssemblyAI API key. Find your API key at https://www.assemblyai.com/app/account
16+
/// </summary>
17+
public string ApiKey { get; set; }
18+
19+
/// <summary>
20+
/// If true, you can transcribe audio files from disk.
21+
/// The file be uploaded to AssemblyAI's server to transcribe and deleted when transcription is completed.
22+
/// If false, an exception will be thrown when trying to transcribe files from disk.
23+
/// </summary>
24+
public bool AllowFileSystemAccess { get; set; }
25+
}
26+
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
using System;
2+
using Microsoft.Extensions.Configuration;
3+
using Microsoft.Extensions.DependencyInjection;
4+
using Microsoft.Extensions.Options;
5+
using Microsoft.SemanticKernel;
6+
7+
// ReSharper disable UnusedMember.Global
8+
// ReSharper disable MemberCanBePrivate.Global
9+
10+
namespace AssemblyAI.SemanticKernel
11+
{
12+
public static class Extensions
13+
{
14+
/// <summary>
15+
/// Configure the AssemblyAI plugins using the specified configuration section path.
16+
/// </summary>
17+
/// <param name="builder"></param>
18+
/// <param name="configuration">The configuration to bind options to</param>
19+
/// <returns></returns>
20+
public static IKernelBuilder AddAssemblyAIPlugin(
21+
this IKernelBuilder builder,
22+
IConfiguration configuration
23+
)
24+
{
25+
var pluginConfigurationSection = configuration.GetSection("AssemblyAI:Plugin");
26+
// if configuration exists at section, use that config, otherwise using section that was passed in.
27+
if (pluginConfigurationSection.Exists())
28+
{
29+
configuration = pluginConfigurationSection;
30+
}
31+
32+
var services = builder.Services;
33+
var optionsBuilder = services.AddOptions<AssemblyAIPluginOptions>();
34+
optionsBuilder.Bind(configuration);
35+
ValidateOptions(optionsBuilder);
36+
AddPlugin(builder);
37+
return builder;
38+
}
39+
40+
/// <summary>
41+
/// Configure the AssemblyAI plugins using the specified options.
42+
/// </summary>
43+
/// <param name="builder"></param>
44+
/// <param name="options">Options to configure plugin with</param>
45+
/// <returns></returns>
46+
public static IKernelBuilder AddAssemblyAIPlugin(
47+
this IKernelBuilder builder,
48+
AssemblyAIPluginOptions options
49+
)
50+
{
51+
var services = builder.Services;
52+
var optionsBuilder = services.AddOptions<AssemblyAIPluginOptions>();
53+
optionsBuilder.Configure(optionsToConfigure =>
54+
{
55+
optionsToConfigure.ApiKey = options.ApiKey;
56+
optionsToConfigure.AllowFileSystemAccess = options.AllowFileSystemAccess;
57+
});
58+
ValidateOptions(optionsBuilder);
59+
AddPlugin(builder);
60+
return builder;
61+
}
62+
63+
/// <summary>
64+
/// Configure the AssemblyAI plugins using the specified options.
65+
/// </summary>
66+
/// <param name="builder"></param>
67+
/// <param name="configureOptions">Action to configure options</param>
68+
/// <returns></returns>
69+
public static IKernelBuilder AddAssemblyAIPlugin(
70+
this IKernelBuilder builder,
71+
Action<AssemblyAIPluginOptions> configureOptions
72+
)
73+
{
74+
var services = builder.Services;
75+
var optionsBuilder = services.AddOptions<AssemblyAIPluginOptions>();
76+
optionsBuilder.Configure(configureOptions);
77+
ValidateOptions(optionsBuilder);
78+
AddPlugin(builder);
79+
return builder;
80+
}
81+
82+
/// <summary>
83+
/// Configure the AssemblyAI plugins using the specified options.
84+
/// </summary>
85+
/// <param name="builder"></param>
86+
/// <param name="configureOptions">Action to configure options</param>
87+
/// <returns></returns>
88+
public static IKernelBuilder AddAssemblyAIPlugin(
89+
this IKernelBuilder builder,
90+
Action<IServiceProvider, AssemblyAIPluginOptions> configureOptions
91+
)
92+
{
93+
var services = builder.Services;
94+
var optionsBuilder = services.AddOptions<AssemblyAIPluginOptions>();
95+
optionsBuilder.Configure<IServiceProvider>((options, provider) => configureOptions(provider, options));
96+
ValidateOptions(optionsBuilder);
97+
AddPlugin(builder);
98+
return builder;
99+
}
100+
101+
private static void ValidateOptions(OptionsBuilder<AssemblyAIPluginOptions> optionsBuilder)
102+
{
103+
optionsBuilder.Validate(
104+
options => !string.IsNullOrEmpty(options.ApiKey),
105+
"AssemblyAI:Plugin:ApiKey must be configured."
106+
);
107+
}
108+
109+
private static void AddPlugin(IKernelBuilder builder)
110+
{
111+
using (var sp = builder.Services.BuildServiceProvider())
112+
{
113+
var config = sp.GetRequiredService<IOptions<AssemblyAIPluginOptions>>().Value;
114+
var pluginName = string.IsNullOrEmpty(config.PluginName) ? null : config.PluginName;
115+
builder.Plugins.AddFromType<AssemblyAIPlugin>(pluginName);
116+
}
117+
}
118+
}
119+
}

0 commit comments

Comments
 (0)