首页 > 其他分享 >实现阿里云模型服务灵积 DashScope 的 Semantic Kernel Connector

实现阿里云模型服务灵积 DashScope 的 Semantic Kernel Connector

时间:2024-02-13 15:55:41浏览次数:32  
标签:Kernel Connector DashScope var new null public

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 时会用到它,同样也是转手买卖。

ChatHistoryPromptExecutionSettings 参数的转换与 GetChatMessageContentsAsync 一样,所以引入2个扩展方法 ChatHistory.ToChatMessagesPromptExecutionSettings.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

标签:Kernel,Connector,DashScope,var,new,null,public
From: https://www.cnblogs.com/dudu/p/18013405

相关文章

  • 借助 .NET 开源库 Sdcb.DashScope 调用阿里云灵积通义千问 API
    在昨天的博文中,我们通过SemanticKernel调用了自己部署的通义千问开源大模型,但是自己部署通义千问对服务器的配置要求很高,即使使用抢占式按量实例,每次使用时启动服务器,使用完关闭服务器,也比较麻烦,而且越高级的大模型对服务器的配置越高。所以昨天的博文中使用了很低级的Qwen-7B......
  • 初步体验通过 Semantic Kernel 与自己部署的通义千问开源大模型进行对话
    春节之前被SemanticKernel所吸引,开始了解它,学习它。在写这篇博文之前读了一些英文博文,顺便在这里分享一下:IntrotoSemanticKernel–PartOneIntrotoSemanticKernel–PartTwoBuildacustomCopilotexperiencewithyourprivatedatausingandKernelMemory......
  • 简单方法使 kernel32.dll 发生重定位
    简单方法使kernel32.dll发生重定位系统启动后kernel32.dll在每个进程的加载地址都相同。。。。吗?相信很多人都知道这个,因为这个是远程线程注入的基础,类似的还有user32.dll之类的系统dll。不过这个概念是错误的,或者说它在绝大多数时候是正确的因为通常来说这些系统dll默......
  • 使用debezium-connector-jdbc组件完成数据同步(io.debezium.connector.jdbc.JdbcSinkCo
    1.情景展示在网络上几乎找不到关于debezium-connector-jdbc插件的博客文章,基本上都在吹io.confluent.connect.jdbc.JdbcSinkConnector,由于一开始对数据同步插件并不了解,导致自己走了不少弯路。生产数据组件:debezium-connector-mysql、debezium-connector-oracle等数据库组件,通......
  • The artifact mysql:mysql-connector-java:jar:8.0.33 has been relocated to com.mys
    Theartifactmysql:mysql-connector-java:jar:8.0.33hasbeenrelocatedtocom.mysql:mysql-connector-j:jar:8.0.33:MySQLConnector/Jartifactsmovedtoreverse-DNScompliantMaven2+coordinates.1.异常信息Theartifactmysql:mysql-connector-java:jar:8.0.33hasb......
  • Semantic Kernel 通过 LocalAI 集成本地模型
    本文是基于LLama2是由Meta开源的大语言模型,通过LocalAI来集成LLama2来演示Semantickernel(简称SK)和本地大模型的集成示例。SK可以支持各种大模型,在官方示例中多是OpenAI和AzureOpenAIservice的GPT3.5+。今天我们就来看一看如何把SK和本地部署的开源大模型集成起来......
  • Understanding the linux kernel Chapter3 Processes
    ProcessDescriptorHowProcessesAreOrganizedtheprocessinstate:TASK_RUNNINGorganizedinrunqueuelistgroupTASK_STROPPED\EXIT_ZOMBIE\EXIT_DEADThereisnoneedtogroupprocessesinanyofthesethreestates,becausestopped,zombie,andd......
  • Linux修复kernel时钟异常的问题
    发现与ntp服务器同步后,间隔10秒再同步,系统时间与ntp服务器已经相差0.6秒,因此怀疑系统时钟被修改了使用adjtimex调整sudoaptinstalladjtimex对比当前系统时钟sudoadjtimex--compareWARNING:CMOStimeis53.38minbehindsystemclock---current-----suggeste......
  • Google的Jax框架的JAX-Triton目前只能成功运行在TPU设备上(使用Pallas为jax编写kernel
    使用Pallas为jax编写kernel扩展,需要使用JAX-Triton扩展包。由于Google的深度学习框架Jax主要是面向自己的TPU进行开发的,虽然也同时支持NVIDIA的GPU,但是支持力度有限,目前JAX-Triton只能在TPU设备上正常运行,无法保证在GPU上正常运行。该结果使用kaggle上的TPU和GPU进行测试获得。......
  • 通过 KernelUtil.dll 劫持 QQ / TIM 客户端 QQClientkey / QQKey 详细教程(附源码)
    前言由于QQ9.7.20版本后已经不能通过模拟网页快捷登录来截取QQClientkey/QQKey,估计是针对访问的程序做了限制,然而经过多方面测试,诸多的地区、环境、机器也针对这种获取方法做了相应的措施,导致模拟网页快捷登录来截取数据被彻底的和谐,为了解决这个问题我们只能更改思路对......