Skip to content

Commit d90113f

Browse files
authored
feat: implement DashScopeChatCompletionService (#1)
1 parent 032ac1b commit d90113f

13 files changed

+697
-0
lines changed

.editorconfig

+364
Large diffs are not rendered by default.

SemanticKernel.DashScope.sln

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
2+
Microsoft Visual Studio Solution File, Format Version 12.00
3+
# Visual Studio Version 17
4+
VisualStudioVersion = 17.0.31903.59
5+
MinimumVisualStudioVersion = 10.0.40219.1
6+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SemanticKernel.DashScope", "src\SemanticKernel.DashScope\SemanticKernel.DashScope.csproj", "{B9EF31C7-48D7-4CA8-8D15-D6340450D3F5}"
7+
EndProject
8+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{BB73FA18-BBBE-4C34-971A-D4206FC118A2}"
9+
EndProject
10+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{6D288B49-252A-4ADB-A899-E36F21AA87DD}"
11+
ProjectSection(SolutionItems) = preProject
12+
.editorconfig = .editorconfig
13+
EndProjectSection
14+
EndProject
15+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{DAB267DF-F966-4F95-AD12-56CC78D6F274}"
16+
EndProject
17+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SemanticKernel.DashScope.IntegrationTest", "test\SemanticKernel.DashScope.IntegrationTest\SemanticKernel.DashScope.IntegrationTest.csproj", "{FA28DF4A-5A25-46C5-B2BE-614C30797B23}"
18+
EndProject
19+
Global
20+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
21+
Debug|Any CPU = Debug|Any CPU
22+
Release|Any CPU = Release|Any CPU
23+
EndGlobalSection
24+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
25+
{B9EF31C7-48D7-4CA8-8D15-D6340450D3F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
26+
{B9EF31C7-48D7-4CA8-8D15-D6340450D3F5}.Debug|Any CPU.Build.0 = Debug|Any CPU
27+
{B9EF31C7-48D7-4CA8-8D15-D6340450D3F5}.Release|Any CPU.ActiveCfg = Release|Any CPU
28+
{B9EF31C7-48D7-4CA8-8D15-D6340450D3F5}.Release|Any CPU.Build.0 = Release|Any CPU
29+
{FA28DF4A-5A25-46C5-B2BE-614C30797B23}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
30+
{FA28DF4A-5A25-46C5-B2BE-614C30797B23}.Debug|Any CPU.Build.0 = Debug|Any CPU
31+
{FA28DF4A-5A25-46C5-B2BE-614C30797B23}.Release|Any CPU.ActiveCfg = Release|Any CPU
32+
{FA28DF4A-5A25-46C5-B2BE-614C30797B23}.Release|Any CPU.Build.0 = Release|Any CPU
33+
EndGlobalSection
34+
GlobalSection(SolutionProperties) = preSolution
35+
HideSolutionNode = FALSE
36+
EndGlobalSection
37+
GlobalSection(NestedProjects) = preSolution
38+
{B9EF31C7-48D7-4CA8-8D15-D6340450D3F5} = {BB73FA18-BBBE-4C34-971A-D4206FC118A2}
39+
{FA28DF4A-5A25-46C5-B2BE-614C30797B23} = {DAB267DF-F966-4F95-AD12-56CC78D6F274}
40+
EndGlobalSection
41+
GlobalSection(ExtensibilityGlobals) = postSolution
42+
SolutionGuid = {D0F2E9A4-9782-4C3F-B459-CFCAD4C9AD9F}
43+
EndGlobalSection
44+
EndGlobal
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
using System.Runtime.CompilerServices;
2+
using Microsoft.Extensions.Options;
3+
using Microsoft.SemanticKernel;
4+
using Microsoft.SemanticKernel.ChatCompletion;
5+
using Microsoft.SemanticKernel.Services;
6+
using Sdcb.DashScope;
7+
using Sdcb.DashScope.TextGeneration;
8+
9+
namespace Cnblogs.SemanticKernel.Connectors.DashScope;
10+
11+
public sealed class DashScopeChatCompletionService : IChatCompletionService
12+
{
13+
private readonly DashScopeClient _dashScopeClient;
14+
private readonly string _modelId;
15+
private readonly Dictionary<string, object?> _attribues = [];
16+
17+
public DashScopeChatCompletionService(
18+
IOptions<DashScopeClientOptions> options,
19+
HttpClient httpClient)
20+
{
21+
_dashScopeClient = new(options.Value.ApiKey, httpClient);
22+
_modelId = options.Value.ModelId;
23+
_attribues.Add(AIServiceExtensions.ModelIdKey, _modelId);
24+
}
25+
26+
public IReadOnlyDictionary<string, object?> Attributes => _attribues;
27+
28+
public async Task<IReadOnlyList<ChatMessageContent>> GetChatMessageContentsAsync(ChatHistory chatHistory, PromptExecutionSettings? executionSettings = null, Kernel? kernel = null, CancellationToken cancellationToken = default)
29+
{
30+
var chatMessages = chatHistory.ToChatMessages();
31+
var chatParameters = executionSettings?.ToChatParameters();
32+
var response = await _dashScopeClient.TextGeneration.Chat(_modelId, chatMessages, chatParameters, cancellationToken);
33+
return [new ChatMessageContent(new AuthorRole(chatMessages[0].Role), response.Output.Text)];
34+
}
35+
36+
public async IAsyncEnumerable<StreamingChatMessageContent> GetStreamingChatMessageContentsAsync(
37+
ChatHistory chatHistory,
38+
PromptExecutionSettings? executionSettings = null,
39+
Kernel? kernel = null,
40+
[EnumeratorCancellation] CancellationToken cancellationToken = default)
41+
{
42+
var chatMessages = chatHistory.ToChatMessages();
43+
var chatParameters = executionSettings?.ToChatParameters() ?? new ChatParameters();
44+
chatParameters.IncrementalOutput = true;
45+
46+
var responses = _dashScopeClient.TextGeneration.ChatStreamed(_modelId, chatMessages, chatParameters, cancellationToken);
47+
48+
await foreach (var response in responses)
49+
{
50+
yield return new StreamingChatMessageContent(new AuthorRole(chatMessages[0].Role), response.Output.Text);
51+
}
52+
}
53+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using Microsoft.Extensions.Options;
2+
3+
namespace Cnblogs.SemanticKernel.Connectors.DashScope;
4+
5+
public class DashScopeClientOptions : IOptions<DashScopeClientOptions>
6+
{
7+
public string ModelId { get; set; } = string.Empty;
8+
9+
public string ApiKey { get; set; } = string.Empty;
10+
11+
public DashScopeClientOptions Value => this;
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
using Cnblogs.SemanticKernel.Connectors.DashScope;
2+
using Microsoft.Extensions.DependencyInjection;
3+
using Microsoft.SemanticKernel.ChatCompletion;
4+
5+
namespace Microsoft.SemanticKernel;
6+
7+
public static class DashScopeServiceCollectionExtensions
8+
{
9+
public static IKernelBuilder AddDashScopeChatCompletion(
10+
this IKernelBuilder builder,
11+
string? serviceId = null,
12+
Action<HttpClient>? configureClient = null,
13+
string configSectionPath = "dashscope")
14+
{
15+
Func<IServiceProvider, object?, DashScopeChatCompletionService> factory = (serviceProvider, _) =>
16+
serviceProvider.GetRequiredService<DashScopeChatCompletionService>();
17+
18+
if (configureClient == null)
19+
{
20+
builder.Services.AddHttpClient<DashScopeChatCompletionService>();
21+
}
22+
else
23+
{
24+
builder.Services.AddHttpClient<DashScopeChatCompletionService>(configureClient);
25+
}
26+
27+
builder.Services.AddOptions<DashScopeClientOptions>().BindConfiguration(configSectionPath);
28+
builder.Services.AddKeyedSingleton<IChatCompletionService>(serviceId, factory);
29+
return builder;
30+
}
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using Microsoft.SemanticKernel.ChatCompletion;
2+
using Sdcb.DashScope.TextGeneration;
3+
4+
namespace Cnblogs.SemanticKernel.Connectors.DashScope;
5+
6+
public static class ChatHistoryExtensions
7+
{
8+
public static IReadOnlyList<ChatMessage> ToChatMessages(this ChatHistory chatHistory)
9+
{
10+
return chatHistory
11+
.Where(x => !string.IsNullOrEmpty(x.Content))
12+
.Select(x => new ChatMessage(x.Role.ToString(), x.Content!)).
13+
ToList();
14+
}
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using System.Text.Json;
2+
using System.Text.Json.Serialization;
3+
using Microsoft.SemanticKernel;
4+
using Sdcb.DashScope.TextGeneration;
5+
6+
namespace Cnblogs.SemanticKernel.Connectors.DashScope;
7+
8+
public static class PromptExecutionSettingsExtensions
9+
{
10+
private readonly static JsonSerializerOptions JsonSerializerOptions = new()
11+
{
12+
NumberHandling = JsonNumberHandling.AllowReadingFromString
13+
};
14+
15+
public static ChatParameters? ToChatParameters(this PromptExecutionSettings executionSettings)
16+
{
17+
ChatParameters? chatParameters = null;
18+
19+
if (executionSettings?.ExtensionData?.Count > 0)
20+
{
21+
var json = JsonSerializer.Serialize(executionSettings.ExtensionData);
22+
chatParameters = JsonSerializer.Deserialize<ChatParameters>(json, JsonSerializerOptions);
23+
}
24+
25+
return chatParameters;
26+
}
27+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net8.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
<RootNamespace>Cnblogs.SemanticKernel.Connectors.DashScope</RootNamespace>
8+
</PropertyGroup>
9+
10+
<ItemGroup>
11+
<FrameworkReference Include="Microsoft.AspNetCore.App"></FrameworkReference>
12+
</ItemGroup>
13+
14+
<ItemGroup>
15+
<PackageReference Include="Microsoft.SemanticKernel.Core" Version="1.3.0" />
16+
<PackageReference Include="Sdcb.DashScope" Version="1.0.1" />
17+
</ItemGroup>
18+
19+
</Project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
using System.Diagnostics;
2+
using System.Text;
3+
using Microsoft.Extensions.Configuration;
4+
using Microsoft.Extensions.DependencyInjection;
5+
using Microsoft.SemanticKernel;
6+
7+
namespace SemanticKernel.DashScope.IntegrationTest;
8+
9+
public class DashScopeChatCompletionTests
10+
{
11+
[Fact]
12+
public async Task ChatCompletion_InvokePromptAsync_WorksCorrectly()
13+
{
14+
// Arrange
15+
var builder = Kernel.CreateBuilder();
16+
builder.Services.AddSingleton(GetConfiguration());
17+
builder.AddDashScopeChatCompletion();
18+
var kernel = builder.Build();
19+
20+
var prompt = @"<message role=""user"">博客园是什么网站</message>";
21+
PromptExecutionSettings settings = new()
22+
{
23+
ExtensionData = new Dictionary<string, object>()
24+
{
25+
{ "temperature", "0.8" }
26+
}
27+
};
28+
KernelArguments kernelArguments = new(settings);
29+
30+
// Act
31+
var result = await kernel.InvokePromptAsync(prompt, kernelArguments);
32+
33+
// Assert
34+
Assert.Contains("博客园", result.ToString());
35+
Trace.WriteLine(result.ToString());
36+
}
37+
38+
[Fact]
39+
public async Task ChatCompletion_InvokePromptStreamingAsync_WorksCorrectly()
40+
{
41+
// Arrange
42+
var builder = Kernel.CreateBuilder();
43+
builder.Services.AddSingleton(GetConfiguration());
44+
builder.AddDashScopeChatCompletion();
45+
var kernel = builder.Build();
46+
47+
// Act
48+
var prompt = @"<message role=""user"">博客园是什么网站</message>";
49+
var result = kernel.InvokePromptStreamingAsync(prompt);
50+
51+
// Assert
52+
var sb = new StringBuilder();
53+
await foreach (var message in result)
54+
{
55+
Trace.WriteLine(message);
56+
sb.Append(message);
57+
}
58+
Assert.Contains("博客园", sb.ToString());
59+
}
60+
61+
private static IConfiguration GetConfiguration()
62+
{
63+
return new ConfigurationBuilder()
64+
.SetBasePath(Directory.GetCurrentDirectory())
65+
.AddJsonFile("appsettings.json")
66+
.AddUserSecrets<DashScopeChatCompletionTests>()
67+
.Build();
68+
}
69+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
global using Xunit;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
using System.Diagnostics;
2+
using System.Text;
3+
using SemanticKernel.DashScope.IntegrationTest;
4+
using Xunit.Abstractions;
5+
using Xunit.Sdk;
6+
7+
[assembly: TestFramework($"{IntegrationTestFramework.Namespace}.{nameof(IntegrationTestFramework)}", IntegrationTestFramework.Namespace)]
8+
9+
namespace SemanticKernel.DashScope.IntegrationTest;
10+
11+
public class IntegrationTestFramework : XunitTestFramework
12+
{
13+
public const string Namespace = $"{nameof(SemanticKernel)}.{nameof(DashScope)}.{nameof(IntegrationTest)}";
14+
15+
public IntegrationTestFramework(IMessageSink messageSink) : base(messageSink)
16+
{
17+
Console.OutputEncoding = Encoding.UTF8;
18+
Trace.Listeners.Add(new ConsoleTraceListener());
19+
}
20+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net8.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
8+
<IsPackable>false</IsPackable>
9+
<IsTestProject>true</IsTestProject>
10+
<UserSecretsId>d03d5dda-9cc6-4d60-94df-e72c93ff243c</UserSecretsId>
11+
</PropertyGroup>
12+
13+
<ItemGroup>
14+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0" />
15+
<PackageReference Include="xunit" Version="2.4.2" />
16+
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
17+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
18+
<PrivateAssets>all</PrivateAssets>
19+
</PackageReference>
20+
<PackageReference Include="coverlet.collector" Version="6.0.0">
21+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
22+
<PrivateAssets>all</PrivateAssets>
23+
</PackageReference>
24+
</ItemGroup>
25+
26+
<ItemGroup>
27+
<ProjectReference Include="..\..\src\SemanticKernel.DashScope\SemanticKernel.DashScope.csproj" />
28+
</ItemGroup>
29+
30+
<ItemGroup>
31+
<None Update="appsettings.json">
32+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
33+
</None>
34+
</ItemGroup>
35+
36+
</Project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"dashscope": {
3+
"modelId": "qwen-max",
4+
"apiKey": "in-user-secret"
5+
}
6+
}

0 commit comments

Comments
 (0)