完成阿里云模型服务灵积 DashScope 的 Semantic Kernel Connector
Semantic Kernel 内置的 IChatCompletionService
完结只支撑 OpenAI 与 Azure OpenAI,而我却计划结合 DashScope(阿里云模型服务灵积) 学习 Semantic Kernel。
所以决议自己着手完结一个支撑 DashScope 的 Semantic Kernel Connector —— DashScopeChatCompletionService,完结的进程也是学习 Semantic Kernel 源码的进程,
并且凭借 Sdcb.DashScope,完结变得更简单了,详见前一篇博文 凭借 .NET 开源库 Sdcb.DashScope 调用阿里云灵积通义千问 API
这儿只完结用于调用 chat completion 服务的 connector,所以只需完结 IChatCompletionService
接口,该接口承继了 IAIService
接口,总共需求完结2个办法+1个特点。
public sealed class DashScopeChatCompletionService : IChatCompletionService
{
public IReadOnlyDictionary<string, object?> Attributes { get; }
public Task<IReadOnlyList<ChatMessageContent>> GetChatMessageContentsAsync(ChatHistory chatHistory, PromptExecutionSettings? executionSettings = null, Kernel? kernel = null, CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}
public IAsyncEnumerable<StreamingChatMessageContent> GetStreamingChatMessageContentsAsync(ChatHistory chatHistory, PromptExecutionSettings? executionSettings = null, Kernel? kernel = null, CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}
}
先完结 GetChatMessageContentsAsync
办法,调用 Kernel.InvokePromptAsync
办法时会用到这个办法。
完结起来比较简单,便是易手生意:
- 把 Semantic Kernel 的
ChatHistory
转化为 Sdcb.DashScope 的IReadOnlyList<ChatMessage>
- 把 Semantic Kernel 的
PromptExecutionSettings
转化为 Sdcb.DashScope 的ChatParameters
- 把 Sdcb.DashScope 的
ResponseWrapper<ChatOutput, ChatTokenUsage>
转化为 Semantic Kernel 的IReadOnlyList<ChatMessageContent>
完结代码如下:
public async Task<IReadOnlyList<ChatMessageContent>> GetChatMessageContentsAsync(ChatHistory chatHistory, PromptExecutionSettings? executionSettings = null, Kernel? kernel = null, CancellationToken cancellationToken = default)
{
var chatMessages = chatHistory
.Where(x => !string.IsNullOrEmpty(x.Content))
.Select(x => new ChatMessage(x.Role.ToString(), x.Content!)).
ToList();
ChatParameters? chatParameters = null;
if (executionSettings?.ExtensionData?.Count > 0)
{
var json = JsonSerializer.Serialize(executionSettings.ExtensionData);
chatParameters = JsonSerializer.Deserialize<ChatParameters>(
json,
new JsonSerializerOptions { NumberHandling = JsonNumberHandling.AllowReadingFromString });
}
var response = await _dashScopeClient.TextGeneration.Chat(_modelId, chatMessages, chatParameters, cancellationToken);
return [new ChatMessageContent(new AuthorRole(chatMessages.First().Role), response.Output.Text)];
}
接下来完结 GetStreamingChatMessageContentsAsync
,调用 Kernel.InvokePromptStreamingAsync
时会用到它,相同也是易手生意。
ChatHistory
与 PromptExecutionSettings
参数的转化与 GetChatMessageContentsAsync
相同,所以引进2个扩展办法 ChatHistory.ToChatMessages
与 PromptExecutionSettings.ToChatParameters
削减重复代码,别的需求将 ChatParameters.IncrementalOutput
设置为 true
。
不同之处是返回值类型,需求将 Sdcb.DashScope 的 IAsyncEnumerable<ResponseWrapper<ChatOutput, ChatTokenUsage>>
转化为 IAsyncEnumerable<StreamingChatMessageContent>
完结代码如下:
public async IAsyncEnumerable<StreamingChatMessageContent> GetStreamingChatMessageContentsAsync(
ChatHistory chatHistory,
PromptExecutionSettings? executionSettings = null,
Kernel? kernel = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
var chatMessages = chatHistory.ToChatMessages();
var chatParameters = executionSettings?.ToChatParameters() ?? new ChatParameters();
chatParameters.IncrementalOutput = true;
var responses = _dashScopeClient.TextGeneration.ChatStreamed(_modelId, chatMessages, chatParameters, cancellationToken);
await foreach (var response in responses)
{
yield return new StreamingChatMessageContent(new AuthorRole(chatMessages[0].Role), response.Output.Text);
}
}
到这儿2个办法就完结好了,还剩余很简单完结的1个特点,轻松搞定
public sealed class DashScopeChatCompletionService : IChatCompletionService
{
private readonly DashScopeClient _dashScopeClient;
private readonly string _modelId;
private readonly Dictionary<string, object?> _attribues = [];
public DashScopeChatCompletionService(
IOptions<DashScopeClientOptions> options,
HttpClient httpClient)
{
_dashScopeClient = new(options.Value.ApiKey, httpClient);
_modelId = options.Value.ModelId;
_attribues.Add(AIServiceExtensions.ModelIdKey, _modelId);
}
public IReadOnlyDictionary<string, object?> Attributes => _attribues;
}
到此,DashScopeChatCompletionService 的完结就完结了。
接下来,完结一个扩展办法,将 DashScopeChatCompletionService 注册到依靠注入容器
public static class DashScopeServiceCollectionExtensions
{
public static IKernelBuilder AddDashScopeChatCompletion(
this IKernelBuilder builder,
string? serviceId = null,
Action<HttpClient>? configureClient = null,
string configSectionPath = "dashscope")
{
Func<IServiceProvider, object?, DashScopeChatCompletionService> factory = (serviceProvider, _) =>
serviceProvider.GetRequiredService<DashScopeChatCompletionService>();
if (configureClient == null)
{
builder.Services.AddHttpClient<DashScopeChatCompletionService>();
}
else
{
builder.Services.AddHttpClient<DashScopeChatCompletionService>(configureClient);
}
builder.Services.AddOptions<DashScopeClientOptions>().BindConfiguration(configSectionPath);
builder.Services.AddKeyedSingleton<IChatCompletionService>(serviceId, factory);
return builder;
}
}
为了便利经过装备文件装备 ModelId 与 ApiKey,引进了 DashScopeClientOptions
public class DashScopeClientOptions : IOptions<DashScopeClientOptions>
{
public string ModelId { get; set; } = string.Empty;
public string ApiKey { get; set; } = string.Empty;
public DashScopeClientOptions Value => this;
}
最终便是写测验代码验证完结是否成功,为了削减代码块的长度,下面的代码片段只列出其间一个测验用例
public class DashScopeChatCompletionTests
{
[Fact]
public async Task ChatCompletion_InvokePromptAsync_WorksCorrectly()
{
// Arrange
var builder = Kernel.CreateBuilder();
builder.Services.AddSingleton(GetConfiguration());
builder.AddDashScopeChatCompletion();
var kernel = builder.Build();
var prompt = @"<message role=""user"">博客园是什么网站</message>";
PromptExecutionSettings settings = new()
{
ExtensionData = new Dictionary<string, object>()
{
{ "temperature", "0.8" }
}
};
KernelArguments kernelArguments = new(settings);
// Act
var result = await kernel.InvokePromptAsync(prompt, kernelArguments);
// Assert
Assert.Contains("博客园", result.ToString());
Trace.WriteLine(result.ToString());
}
private static IConfiguration GetConfiguration()
{
return new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json")
.AddUserSecrets<DashScopeChatCompletionTests>()
.Build();
}
}
最终的最终便是运转测验,在 appsettings.json 中增加模型Id
{
"dashscope": {
"modelId": "qwen-max"
}
}
注:qwen-max
是通义千问千亿级大模型
经过 user-secrets 增加 api key
dotnet user-secrets set "dashscope:apiKey" "sk-xxx"
dotnet test
指令运转测验
A total of 1 test files matched the specified pattern.
博客园是一个专心于供给信息技能(IT)范畴常识共享和技能沟通的中文博客渠道,创建于2004年。博客园主要由软件开发人员、系统管理员以及对IT技能有深沉爱好的人群运用,用户能够在该网站上编撰和发布自己的博客文章,内容包括编程、软件开发、云核算、人工智能等多个范畴。一起,博客园也供给了丰厚的技能文档、教程资源和社区互动功用,旨在促进IT专业人士之间的沟通与学习。
Passed! - Failed: 0, Passed: 1, Skipped: 0, Total: 1, Duration: < 1 ms - SemanticKernel.DashScope.IntegrationTest.dll (net8.0)
测验经过!衔接 DashScope 的 Semantic Kernel Connector 开始完结完结。
完好完结代码放在 github 上,详见 https://github.com/cnblogs/semantic-kernel-dashscope/tree/v0.1.0
针对这个完结发布了 nuget 包 Cnblogs.SemanticKernel.Connectors.DashScope