Skip to content

Commit e3e1a56

Browse files
committed
Add BotSharp.Plugin.MicrosoftExtensionsAI
1 parent e3da73b commit e3e1a56

File tree

7 files changed

+404
-0
lines changed

7 files changed

+404
-0
lines changed

BotSharp.sln

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BotSharp.Plugin.GoogleAI",
6565
EndProject
6666
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BotSharp.Plugin.SemanticKernel", "src\Plugins\BotSharp.Plugin.SemanticKernel\BotSharp.Plugin.SemanticKernel.csproj", "{73EE2CD0-3B27-4F02-A67B-762CBDD740D0}"
6767
EndProject
68+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BotSharp.Plugin.MicrosoftExtensionsAI", "src\Plugins\BotSharp.Plugin.MicrosoftExtensionsAI\BotSharp.Plugin.MicrosoftExtensionsAI.csproj", "{72CA059E-6AAA-406C-A1EB-A2243E652F5F}"
69+
EndProject
6870
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BotSharp.Plugin.SemanticKernel.UnitTests", "tests\BotSharp.Plugin.SemanticKernel.UnitTests\BotSharp.Plugin.SemanticKernel.UnitTests.csproj", "{BC57D428-A1A4-4D38-A2D0-AC6CA943F247}"
6971
EndProject
7072
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BotSharp.Plugin.Twilio", "src\Plugins\BotSharp.Plugin.Twilio\BotSharp.Plugin.Twilio.csproj", "{E627F1E3-BE03-443A-83A2-86A855A278EB}"
@@ -283,6 +285,14 @@ Global
283285
{73EE2CD0-3B27-4F02-A67B-762CBDD740D0}.Release|Any CPU.Build.0 = Release|Any CPU
284286
{73EE2CD0-3B27-4F02-A67B-762CBDD740D0}.Release|x64.ActiveCfg = Release|Any CPU
285287
{73EE2CD0-3B27-4F02-A67B-762CBDD740D0}.Release|x64.Build.0 = Release|Any CPU
288+
{72CA059E-6AAA-406C-A1EB-A2243E652F5F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
289+
{72CA059E-6AAA-406C-A1EB-A2243E652F5F}.Debug|Any CPU.Build.0 = Debug|Any CPU
290+
{72CA059E-6AAA-406C-A1EB-A2243E652F5F}.Debug|x64.ActiveCfg = Debug|Any CPU
291+
{72CA059E-6AAA-406C-A1EB-A2243E652F5F}.Debug|x64.Build.0 = Debug|Any CPU
292+
{72CA059E-6AAA-406C-A1EB-A2243E652F5F}.Release|Any CPU.ActiveCfg = Release|Any CPU
293+
{72CA059E-6AAA-406C-A1EB-A2243E652F5F}.Release|Any CPU.Build.0 = Release|Any CPU
294+
{72CA059E-6AAA-406C-A1EB-A2243E652F5F}.Release|x64.ActiveCfg = Release|Any CPU
295+
{72CA059E-6AAA-406C-A1EB-A2243E652F5F}.Release|x64.Build.0 = Release|Any CPU
286296
{BC57D428-A1A4-4D38-A2D0-AC6CA943F247}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
287297
{BC57D428-A1A4-4D38-A2D0-AC6CA943F247}.Debug|Any CPU.Build.0 = Debug|Any CPU
288298
{BC57D428-A1A4-4D38-A2D0-AC6CA943F247}.Debug|x64.ActiveCfg = Debug|Any CPU
@@ -491,6 +501,7 @@ Global
491501
{DB3DE37B-1208-4ED3-9615-A52AD0AAD69C} = {5CD330E1-9E5A-4112-8346-6E31CA98EF78}
492502
{8BC29F8A-78D6-422C-B522-10687ADC38ED} = {D5293208-2BEF-42FC-A64C-5954F61720BA}
493503
{73EE2CD0-3B27-4F02-A67B-762CBDD740D0} = {D5293208-2BEF-42FC-A64C-5954F61720BA}
504+
{72CA059E-6AAA-406C-A1EB-A2243E652F5F} = {D5293208-2BEF-42FC-A64C-5954F61720BA}
494505
{BC57D428-A1A4-4D38-A2D0-AC6CA943F247} = {32FAFFFE-A4CB-4FEE-BF7C-84518BBC6DCC}
495506
{E627F1E3-BE03-443A-83A2-86A855A278EB} = {64264688-0F5C-4AB0-8F2B-B59B717CCE00}
496507
{F06B22CB-B143-4680-8FFF-35B9E50E6C47} = {51AFE054-AE99-497D-A593-69BAEFB5106F}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>$(TargetFramework)</TargetFramework>
5+
<Nullable>enable</Nullable>
6+
<LangVersion>$(LangVersion)</LangVersion>
7+
<VersionPrefix>$(BotSharpVersion)</VersionPrefix>
8+
<GeneratePackageOnBuild>$(GeneratePackageOnBuild)</GeneratePackageOnBuild>
9+
<GenerateDocumentationFile>$(GeneratePackageOnBuild)</GenerateDocumentationFile>
10+
<OutputPath>$(SolutionDir)packages</OutputPath>
11+
<NoWarn>$(NoWarn);NU5104</NoWarn>
12+
</PropertyGroup>
13+
14+
<ItemGroup>
15+
<PackageReference Include="Microsoft.Extensions.AI.Abstractions" Version="9.0.0-preview.9.24525.1" />
16+
<PackageReference Include="System.Text.Encodings.Web" Version="8.0.0" />
17+
</ItemGroup>
18+
19+
<ItemGroup>
20+
<ProjectReference Include="..\..\Infrastructure\BotSharp.Abstraction\BotSharp.Abstraction.csproj" />
21+
</ItemGroup>
22+
23+
</Project>
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
using BotSharp.Abstraction.Agents.Enums;
2+
using BotSharp.Abstraction.Agents.Models;
3+
using BotSharp.Abstraction.Agents;
4+
using BotSharp.Abstraction.Conversations.Models;
5+
using BotSharp.Abstraction.Conversations;
6+
using BotSharp.Abstraction.Files;
7+
using BotSharp.Abstraction.Files.Utilities;
8+
using BotSharp.Abstraction.Loggers;
9+
using BotSharp.Abstraction.MLTasks;
10+
using Microsoft.Extensions.AI;
11+
using Microsoft.Extensions.DependencyInjection;
12+
using Microsoft.Extensions.Logging;
13+
using System.Collections.Generic;
14+
using System.Threading.Tasks;
15+
using System;
16+
using System.Linq;
17+
using System.Text.Json;
18+
using System.Threading;
19+
20+
namespace BotSharp.Plugin.MicrosoftExtensionsAI;
21+
22+
/// <summary>
23+
/// Provides an implementation of <see cref="IChatCompletion"/> for Microsoft.Extensions.AI.
24+
/// </summary>
25+
public sealed class MicrosoftExtensionsAIChatCompletionProvider : IChatCompletion
26+
{
27+
private readonly IChatClient _client;
28+
private readonly ILogger<MicrosoftExtensionsAIChatCompletionProvider> _logger;
29+
private readonly IServiceProvider _services;
30+
private string? _model;
31+
32+
/// <summary>
33+
/// Creates an instance of the <see cref="MicrosoftExtensionsAIChatCompletionProvider"/> class.
34+
/// </summary>
35+
public MicrosoftExtensionsAIChatCompletionProvider(
36+
IChatClient client,
37+
ILogger<MicrosoftExtensionsAIChatCompletionProvider> logger,
38+
IServiceProvider services)
39+
{
40+
_client = client;
41+
_model = _client.Metadata.ModelId;
42+
_logger = logger;
43+
_services = services;
44+
}
45+
46+
/// <inheritdoc/>
47+
public string Provider => "microsoft.extensions.ai";
48+
49+
/// <inheritdoc/>
50+
public void SetModelName(string model) => _model = model;
51+
52+
/// <inheritdoc/>
53+
public async Task<RoleDialogModel> GetChatCompletions(Agent agent, List<RoleDialogModel> conversations)
54+
{
55+
// Before chat completion hook
56+
var hooks = _services.GetServices<IContentGeneratingHook>().ToArray();
57+
await Task.WhenAll(hooks.Select(hook => hook.BeforeGenerating(agent, conversations)));
58+
59+
// Configure options
60+
var state = _services.GetRequiredService<IConversationStateService>();
61+
var options = new ChatOptions()
62+
{
63+
Temperature = float.Parse(state.GetState("temperature", "0.0")),
64+
MaxOutputTokens = int.Parse(state.GetState("max_tokens", "1024"))
65+
};
66+
67+
if (_services.GetService<IAgentService>() is { } agentService)
68+
{
69+
foreach (var function in agent.Functions)
70+
{
71+
if (agentService.RenderFunction(agent, function))
72+
{
73+
var property = agentService.RenderFunctionProperty(agent, function);
74+
(options.Tools ??= []).Add(new NopAIFunction(new(function.Name)
75+
{
76+
Description = function.Description,
77+
Parameters = property?.Properties.RootElement.Deserialize<Dictionary<string, object?>>()?.Select(p => new AIFunctionParameterMetadata(p.Key)
78+
{
79+
Schema = p.Value,
80+
}).ToList() ?? [],
81+
}));
82+
}
83+
}
84+
}
85+
86+
// Configure messages
87+
List<ChatMessage> messages = [];
88+
89+
if (_services.GetRequiredService<IAgentService>().RenderedInstruction(agent) is string instruction &&
90+
instruction.Length > 0)
91+
{
92+
messages.Add(new(ChatRole.System, instruction));
93+
}
94+
95+
if (!string.IsNullOrEmpty(agent.Knowledges))
96+
{
97+
messages.Add(new(ChatRole.System, agent.Knowledges));
98+
}
99+
100+
foreach (var sample in ProviderHelper.GetChatSamples(agent.Samples))
101+
{
102+
messages.Add(new(sample.Role == AgentRole.Assistant ? ChatRole.Assistant : ChatRole.User, sample.Content));
103+
}
104+
105+
var fileStorage = _services.GetService<IFileStorageService>();
106+
bool allowMultiModal = fileStorage is not null && _services.GetService<ILlmProviderService>()?.GetSetting(Provider, _model ?? "default")?.MultiModal is true;
107+
foreach (var x in conversations)
108+
{
109+
if (x.Role == AgentRole.Function && x.FunctionName is not null)
110+
{
111+
messages.Add(new(ChatRole.Assistant,
112+
[
113+
new FunctionCallContent(x.FunctionName, x.FunctionName, JsonSerializer.Deserialize<Dictionary<string, object?>>(x.FunctionArgs ?? "{}")),
114+
new FunctionResultContent(x.FunctionName, x.FunctionName, x.Content)
115+
]));
116+
}
117+
else if (x.Role == AgentRole.System || x.Role == AgentRole.Assistant)
118+
{
119+
messages.Add(new(x.Role == AgentRole.System ? ChatRole.System : ChatRole.Assistant, x.Content));
120+
}
121+
else if (x.Role == AgentRole.User)
122+
{
123+
List<AIContent> contents = [new TextContent(!string.IsNullOrWhiteSpace(x.Payload) ? x.Payload : x.Content)];
124+
if (allowMultiModal)
125+
{
126+
foreach (var file in x.Files)
127+
{
128+
if (!string.IsNullOrEmpty(file.FileData))
129+
{
130+
contents.Add(new ImageContent(file.FileData));
131+
}
132+
else if (!string.IsNullOrEmpty(file.FileStorageUrl))
133+
{
134+
var contentType = FileUtility.GetFileContentType(file.FileStorageUrl);
135+
var bytes = fileStorage!.GetFileBytes(file.FileStorageUrl);
136+
contents.Add(new ImageContent(bytes, contentType));
137+
}
138+
else if (!string.IsNullOrEmpty(file.FileUrl))
139+
{
140+
contents.Add(new ImageContent(file.FileUrl));
141+
}
142+
}
143+
}
144+
145+
messages.Add(new(ChatRole.User, contents) { AuthorName = x.FunctionName });
146+
}
147+
}
148+
149+
var completion = await _client.CompleteAsync(messages);
150+
151+
RoleDialogModel result = new(AgentRole.Assistant, string.Concat(completion.Message.Contents.OfType<TextContent>()))
152+
{
153+
CurrentAgentId = agent.Id
154+
};
155+
156+
if (completion.Message.Contents.OfType<FunctionCallContent>().FirstOrDefault() is { } fcc)
157+
{
158+
result.Role = AgentRole.Function;
159+
result.MessageId = conversations.LastOrDefault()?.MessageId ?? string.Empty;
160+
result.FunctionName = fcc.Name;
161+
result.FunctionArgs = fcc.Arguments is not null ? JsonSerializer.Serialize(fcc.Arguments) : null;
162+
}
163+
164+
// After chat completion hook
165+
await Task.WhenAll(hooks.Select(hook => hook.AfterGenerated(result, new() { Model = _model ?? "default" })));
166+
167+
return result;
168+
}
169+
170+
/// <inheritdoc/>
171+
public Task<bool> GetChatCompletionsAsync(Agent agent, List<RoleDialogModel> conversations, Func<RoleDialogModel, Task> onMessageReceived, Func<RoleDialogModel, Task> onFunctionExecuting) =>
172+
throw new NotImplementedException();
173+
174+
/// <inheritdoc/>
175+
public Task<bool> GetChatCompletionsStreamingAsync(Agent agent, List<RoleDialogModel> conversations, Func<RoleDialogModel, Task> onMessageReceived) =>
176+
throw new NotImplementedException();
177+
178+
private sealed class NopAIFunction(AIFunctionMetadata metadata) : AIFunction
179+
{
180+
public override AIFunctionMetadata Metadata { get; } = metadata;
181+
182+
protected override Task<object?> InvokeCoreAsync(IEnumerable<KeyValuePair<string, object?>> arguments, CancellationToken cancellationToken) =>
183+
throw new NotSupportedException();
184+
}
185+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
using BotSharp.Abstraction.MLTasks;
2+
using BotSharp.Abstraction.Plugins;
3+
using Microsoft.Extensions.Configuration;
4+
using Microsoft.Extensions.DependencyInjection;
5+
6+
namespace BotSharp.Plugin.MicrosoftExtensionsAI;
7+
8+
/// <summary>
9+
/// Use Microsoft.Extensions.AI as BotSharp plugin
10+
/// </summary>
11+
public sealed class MicrosoftExtensionsAIPlugin : IBotSharpPlugin
12+
{
13+
/// <inheritdoc/>
14+
public string Id => "B7F2AB8D-1BBA-41CE-9642-2D5E6B5F86A0";
15+
16+
/// <inheritdoc/>
17+
public string Name => "Microsoft.Extensions.AI";
18+
19+
/// <inheritdoc/>
20+
public string Description => "Microsoft.Extensions.AI Service";
21+
22+
/// <inheritdoc/>
23+
public void RegisterDI(IServiceCollection services, IConfiguration config)
24+
{
25+
services.AddScoped<ITextCompletion, MicrosoftExtensionsAITextCompletionProvider>();
26+
services.AddScoped<IChatCompletion, MicrosoftExtensionsAIChatCompletionProvider>();
27+
services.AddScoped<ITextEmbedding, MicrosoftExtensionsAITextEmbeddingProvider>();
28+
}
29+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
using BotSharp.Abstraction.Agents.Enums;
2+
using BotSharp.Abstraction.Agents.Models;
3+
using BotSharp.Abstraction.Conversations;
4+
using BotSharp.Abstraction.Conversations.Models;
5+
using BotSharp.Abstraction.Loggers;
6+
using BotSharp.Abstraction.MLTasks;
7+
using Microsoft.Extensions.AI;
8+
using Microsoft.Extensions.DependencyInjection;
9+
using System;
10+
using System.Linq;
11+
using System.Threading.Tasks;
12+
13+
namespace BotSharp.Plugin.MicrosoftExtensionsAI;
14+
15+
/// <summary>
16+
/// Provides an implementation of <see cref="ITextCompletion"/> for Microsoft.Extensions.AI.
17+
/// </summary>
18+
public sealed class MicrosoftExtensionsAITextCompletionProvider : ITextCompletion
19+
{
20+
private readonly IChatClient _chatClient;
21+
private readonly IServiceProvider _services;
22+
private readonly ITokenStatistics _tokenStatistics;
23+
private string? _model = null;
24+
25+
/// <inheritdoc/>
26+
public string Provider => "microsoft-extensions-ai";
27+
28+
/// <summary>
29+
/// Creates an instance of the <see cref="MicrosoftExtensionsAITextCompletionProvider"/> class.
30+
/// </summary>
31+
public MicrosoftExtensionsAITextCompletionProvider(
32+
IChatClient chatClient,
33+
IServiceProvider services,
34+
ITokenStatistics tokenStatistics)
35+
{
36+
_chatClient = chatClient;
37+
_services = services;
38+
_tokenStatistics = tokenStatistics;
39+
}
40+
41+
/// <inheritdoc/>
42+
public async Task<string> GetCompletion(string text, string agentId, string messageId)
43+
{
44+
var hooks = _services.GetServices<IContentGeneratingHook>().ToArray();
45+
46+
// Before chat completion hook
47+
Agent agent = new() { Id = agentId };
48+
RoleDialogModel userMessage = new(AgentRole.User, text) { MessageId = messageId };
49+
await Task.WhenAll(hooks.Select(hook => hook.BeforeGenerating(agent, [userMessage])));
50+
51+
_tokenStatistics.StartTimer();
52+
var completion = await _chatClient.CompleteAsync(text);
53+
var result = string.Concat(completion.Message.Contents.OfType<TextContent>());
54+
_tokenStatistics.StopTimer();
55+
56+
// After chat completion hook
57+
await Task.WhenAll(hooks.Select(hook =>
58+
hook.AfterGenerated(new(AgentRole.Assistant, result), new() { Model = _model ?? "default" })));
59+
60+
return result;
61+
}
62+
63+
/// <inheritdoc/>
64+
public void SetModelName(string model)
65+
{
66+
if (!string.IsNullOrWhiteSpace(model))
67+
{
68+
_model = model;
69+
}
70+
}
71+
}

0 commit comments

Comments
 (0)