├── src ├── SemanticKernel.DashScope │ ├── JsonOptionCache.cs │ ├── DashScopeOptions.cs │ ├── DashScopeChatMessageContent.cs │ ├── FunctionDefinition.cs │ ├── DashScopeMapper.cs │ ├── SemanticKernel.DashScope.csproj │ ├── DashScopeTextEmbeddingGenerationService.cs │ ├── FunctionDefinitionConvertor.cs │ ├── DashScopePromptExecutionSettings.cs │ ├── DashScopeServiceCollectionExtensions.cs │ ├── ToolCallBehavior.cs │ └── DashScopeChatCompletionService.cs └── KernelMemory.DashScope │ ├── QWenTextTokenizer.cs │ ├── TokenUsageMapper.cs │ ├── LengthTokenizer.cs │ ├── KernelMemory.DashScope.csproj │ ├── DashScopeTextEmbeddingGenerator.cs │ ├── DashScopeConfig.cs │ ├── DashScopeTextGenerator.cs │ └── DependencyInjector.cs ├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── pack.sh ├── test ├── SemanticKernel.DashScope.UnitTest │ ├── MockLoggerFactory.cs │ ├── SemanticKernel.DashScope.UnitTest.csproj │ ├── TextEmbeddingTests.cs │ ├── ServiceCollectionExtensionsTests.cs │ ├── Cases.cs │ ├── TextCompletionTests.cs │ └── ChatCompletionTests.cs ├── KernelMemory.DashScope.UnitTests │ ├── LengthTokenizerTests.cs │ ├── DashScopeConfigTests.cs │ ├── KernelMemory.DashScope.UnitTests.csproj │ ├── KernelMemoryBuilderExtensionTests.cs │ ├── DependencyInjectorTests.cs │ ├── DashScopeTextEmbeddingGeneratorTests.cs │ ├── Cases.cs │ └── DashScopeTextGeneratorTests.cs └── Directory.Build.props ├── LICENSE ├── README.md ├── SemanticKernel.DashScope.sln ├── .gitignore └── .editorconfig /src/SemanticKernel.DashScope/JsonOptionCache.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | 3 | namespace Cnblogs.SemanticKernel.Connectors.DashScope; 4 | 5 | internal class JsonOptionCache 6 | { 7 | public static JsonSerializerOptions ReadPermissive { get; } = new() 8 | { 9 | AllowTrailingCommas = true, 10 | PropertyNameCaseInsensitive = true, 11 | ReadCommentHandling = JsonCommentHandling.Skip, 12 | }; 13 | } -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | build-test: 11 | runs-on: ubuntu-latest 12 | container: mcr.microsoft.com/dotnet/sdk:8.0 13 | 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | - name: Build 18 | run: dotnet build -c Release 19 | - name: Test 20 | run: dotnet test -c Release 21 | -------------------------------------------------------------------------------- /pack.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | [ -z "$1" ] && echo "Missing tag and version" && exit 1 5 | 6 | commit_tag=$1 7 | IFS=/ read -r tagname version <<< "$commit_tag" 8 | 9 | version=${version:1} 10 | project=src/${tagname} 11 | dotnet clean -c Release 12 | dotnet build -p:Version=${version-*} -c Release $project 13 | dotnet pack $project -c Release -p:IncludeSymbols=true -p:SymbolPackageFormat=snupkg --include-source -p:PackageVersion=$version -p:Version=${version-*} -o ./artifacts 14 | -------------------------------------------------------------------------------- /test/SemanticKernel.DashScope.UnitTest/MockLoggerFactory.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using NSubstitute; 3 | using NSubstitute.Extensions; 4 | 5 | namespace SemanticKernel.DashScope.UnitTest; 6 | 7 | public static class MockLoggerFactory 8 | { 9 | public static ILogger MockLogger() 10 | { 11 | var logger = Substitute.For>(); 12 | logger.Configure().IsEnabled(Arg.Any()).Returns(true); 13 | return logger; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/KernelMemory.DashScope.UnitTests/LengthTokenizerTests.cs: -------------------------------------------------------------------------------- 1 | using Cnblogs.KernelMemory.AI.DashScope; 2 | using FluentAssertions; 3 | 4 | namespace KernelMemory.DashScope.UnitTests; 5 | 6 | public class LengthTokenizerTests 7 | { 8 | [Fact] 9 | public void LengthTokenizer_ReturnLength_SuccessAsync() 10 | { 11 | // Arrange 12 | var tokenizer = new LengthTokenizer(); 13 | 14 | // Act 15 | var count = tokenizer.CountTokens(Cases.Text); 16 | 17 | // Assert 18 | count.Should().Be(Cases.Text.Length); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release Package 2 | 3 | on: 4 | release: 5 | types: 6 | - published 7 | 8 | jobs: 9 | release: 10 | environment: nuget 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: actions/setup-dotnet@v4 15 | with: 16 | dotnet-version: '8' 17 | - name: pack and push 18 | env: 19 | nuget_key: ${{ secrets.NUGETAPIKEY }} 20 | run: | 21 | ./pack.sh ${GITHUB_REF:10} 22 | dotnet nuget push ./artifacts/*.* -s https://api.nuget.org/v3/index.json -k $nuget_key --skip-duplicate 23 | -------------------------------------------------------------------------------- /test/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | net8.0 4 | enable 5 | enable 6 | false 7 | true 8 | SKEXP0001 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/KernelMemory.DashScope/QWenTextTokenizer.cs: -------------------------------------------------------------------------------- 1 | using Cnblogs.DashScope.Core; 2 | using Microsoft.KernelMemory.AI; 3 | 4 | namespace Cnblogs.KernelMemory.AI.DashScope; 5 | 6 | /// 7 | /// Tokenizer using QWen 8 | /// 9 | public class QWenTextTokenizer : ITextTokenizer 10 | { 11 | /// 12 | public int CountTokens(string text) 13 | { 14 | return QWenTokenizer.CountTokens(text); 15 | } 16 | 17 | /// 18 | public IReadOnlyList GetTokens(string text) 19 | { 20 | return QWenTokenizer.Tokenizer.EncodeToTokens(text, out _).Select(x => x.Value).ToList(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/KernelMemory.DashScope/TokenUsageMapper.cs: -------------------------------------------------------------------------------- 1 | using Cnblogs.DashScope.Core; 2 | using Microsoft.KernelMemory; 3 | 4 | namespace Cnblogs.KernelMemory.AI.DashScope; 5 | 6 | internal static class TokenUsageMapper 7 | { 8 | public static TokenUsage? ToKernelMemoryTokenUsage(this TextGenerationTokenUsage? usage, string? modelId) 9 | { 10 | if (usage == null) 11 | { 12 | return null; 13 | } 14 | 15 | return new TokenUsage() 16 | { 17 | ServiceTokensIn = usage.InputTokens, 18 | ServiceTokensOut = usage.OutputTokens, 19 | ModelName = modelId 20 | }; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/KernelMemory.DashScope/LengthTokenizer.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.KernelMemory.AI; 2 | 3 | namespace Cnblogs.KernelMemory.AI.DashScope; 4 | 5 | /// 6 | /// Not a real tokenizer. Return the length of given text as token count, used for apis that limiting the text length instead of token count. 7 | /// 8 | public class LengthTokenizer : ITextTokenizer 9 | { 10 | /// 11 | public int CountTokens(string text) 12 | { 13 | return text.Length; 14 | } 15 | 16 | /// 17 | public IReadOnlyList GetTokens(string text) 18 | { 19 | return text.Select(x => $"{x}").ToList(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/SemanticKernel.DashScope/DashScopeOptions.cs: -------------------------------------------------------------------------------- 1 | namespace Cnblogs.SemanticKernel.Connectors.DashScope; 2 | 3 | /// 4 | /// Options about DashScopeClient 5 | /// 6 | public class DashScopeOptions 7 | { 8 | /// 9 | /// The text embedding model id. 10 | /// 11 | public string TextEmbeddingModelId { get; set; } = string.Empty; 12 | 13 | /// 14 | /// Default model name for chat completion. 15 | /// 16 | public string ChatCompletionModelId { get; set; } = string.Empty; 17 | 18 | /// 19 | /// The DashScope api key. 20 | /// 21 | public string ApiKey { get; set; } = string.Empty; 22 | } 23 | -------------------------------------------------------------------------------- /src/SemanticKernel.DashScope/DashScopeChatMessageContent.cs: -------------------------------------------------------------------------------- 1 | using Cnblogs.DashScope.Core; 2 | using Microsoft.SemanticKernel; 3 | using Microsoft.SemanticKernel.ChatCompletion; 4 | 5 | namespace Cnblogs.SemanticKernel.Connectors.DashScope; 6 | 7 | /// 8 | /// DashScope specialized message content 9 | /// 10 | public class DashScopeChatMessageContent( 11 | AuthorRole role, 12 | string content, 13 | Dictionary? metadata = null, 14 | string? name = null, 15 | List? toolCalls = null) 16 | : ChatMessageContent(role, content, metadata: metadata) 17 | { 18 | /// 19 | /// The name of tool if role is tool. 20 | /// 21 | public string? Name { get; } = name; 22 | 23 | /// 24 | /// Optional tool calls. 25 | /// 26 | public List? ToolCalls { get; } = toolCalls; 27 | } 28 | -------------------------------------------------------------------------------- /test/SemanticKernel.DashScope.UnitTest/SemanticKernel.DashScope.UnitTest.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | all 5 | runtime; build; native; contentfiles; analyzers; buildtransitive 6 | 7 | 8 | 9 | 10 | all 11 | runtime; build; native; contentfiles; analyzers; buildtransitive 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/SemanticKernel.DashScope/FunctionDefinition.cs: -------------------------------------------------------------------------------- 1 | using Cnblogs.DashScope.Core; 2 | using Microsoft.SemanticKernel; 3 | 4 | namespace Cnblogs.SemanticKernel.Connectors.DashScope; 5 | 6 | /// 7 | /// Function definition for model to use. 8 | /// 9 | public record FunctionDefinition : IFunctionDefinition 10 | { 11 | /// 12 | /// Creates a new function definition. 13 | /// 14 | /// The name of this function. 15 | /// The description of this function. 16 | /// Parameter map of this function. 17 | public FunctionDefinition(string Name, string Description, KernelJsonSchema? Parameters) 18 | { 19 | this.Description = Description; 20 | this.Name = Name; 21 | this.Parameters = Parameters; 22 | } 23 | 24 | /// 25 | public string Name { get; init; } 26 | 27 | /// 28 | public string Description { get; init; } 29 | 30 | /// 31 | public object? Parameters { get; init; } 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 cnblogs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/SemanticKernel.DashScope/DashScopeMapper.cs: -------------------------------------------------------------------------------- 1 | using Cnblogs.DashScope.Core; 2 | using Microsoft.SemanticKernel.ChatCompletion; 3 | 4 | namespace Cnblogs.SemanticKernel.Connectors.DashScope; 5 | 6 | internal static class DashScopeMapper 7 | { 8 | public static List ToChatMessages(this ChatHistory history) 9 | { 10 | return history.Select( 11 | x => 12 | { 13 | if (x is DashScopeChatMessageContent d) 14 | { 15 | return new TextChatMessage(x.Role.Label, x.Content ?? string.Empty, d.Name, ToolCalls: d.ToolCalls); 16 | } 17 | 18 | return new TextChatMessage(x.Role.Label, x.Content ?? string.Empty); 19 | }).ToList(); 20 | } 21 | 22 | public static Dictionary? ToMetaData( 23 | this ModelResponse? response) 24 | where TUsage : class 25 | where TOutput : class 26 | { 27 | return response == null 28 | ? null 29 | : new Dictionary(StringComparer.OrdinalIgnoreCase) 30 | { 31 | { "Usage", response.Usage }, { "RequestId", response.RequestId } 32 | }; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /test/SemanticKernel.DashScope.UnitTest/TextEmbeddingTests.cs: -------------------------------------------------------------------------------- 1 | using Cnblogs.DashScope.Core; 2 | using Cnblogs.SemanticKernel.Connectors.DashScope; 3 | using FluentAssertions; 4 | using NSubstitute; 5 | using NSubstitute.Extensions; 6 | 7 | namespace SemanticKernel.DashScope.UnitTest; 8 | 9 | public class TextEmbeddingTests 10 | { 11 | [Fact] 12 | public async Task TextEmbedding_GetEmbedding_SuccessAsync() 13 | { 14 | // Arrange 15 | var dashScopeClient = Substitute.For(); 16 | dashScopeClient.Configure() 17 | .GetEmbeddingsAsync(Arg.Any>()) 18 | .Returns(Task.FromResult(Cases.TextEmbeddingResponse)); 19 | var service = new DashScopeTextEmbeddingGenerationService(Cases.ModelId, dashScopeClient); 20 | var data = new List { Cases.Prompt }; 21 | 22 | // Act 23 | var response = await service.GenerateEmbeddingsAsync(data); 24 | 25 | // Assert 26 | await dashScopeClient.Received().GetEmbeddingsAsync( 27 | Arg.Is>( 28 | x => x.Model == Cases.ModelId && x.Input.Texts == data)); 29 | response.Should().NotBeNull(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/SemanticKernel.DashScope/SemanticKernel.DashScope.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | Cnblogs.SemanticKernel.Connectors.DashScope 8 | Cnblogs.SemanticKernel.Connectors.DashScope 9 | Semantic Kernel Connector to Aliyun DashScope. 10 | README.md 11 | AI;SemanticKernel;DashScope;Qwen 12 | https://github.com/cnblogs/semantic-kernel-dashscope 13 | git 14 | MIT 15 | cnblogs.com 16 | true 17 | SKEXP0001 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /test/KernelMemory.DashScope.UnitTests/DashScopeConfigTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | 3 | namespace KernelMemory.DashScope.UnitTests; 4 | 5 | public class DashScopeConfigTests 6 | { 7 | [Theory] 8 | [InlineData(" ")] 9 | [InlineData("")] 10 | public void Validate_NoApiKey_Throw(string apiKey) 11 | { 12 | // Arrange 13 | var config = Cases.DashScopeConfig with { ApiKey = apiKey }; 14 | 15 | // Act 16 | var act = () => config.EnsureValid(); 17 | 18 | // Assert 19 | act.Should().Throw("api key should not be null or whitespace"); 20 | } 21 | 22 | [Fact] 23 | public void Validate_NegativeTextGenerateTokenCount_Throw() 24 | { 25 | // Arrange 26 | var config = Cases.DashScopeConfig with { TextModelMaxTokenTotal = -1 }; 27 | 28 | // Act 29 | var act = () => config.EnsureValid(); 30 | 31 | // Assert 32 | act.Should().Throw("token count can not be less than 0"); 33 | } 34 | 35 | [Fact] 36 | public void Validate_NegativeTextEmbeddingTokenCount_Throw() 37 | { 38 | // Arrange 39 | var config = Cases.DashScopeConfig with { EmbeddingModelMaxTokenTotal = -1 }; 40 | 41 | // Act 42 | var act = () => config.EnsureValid(); 43 | 44 | // Assert 45 | act.Should().Throw("token count can not be less than 0"); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/KernelMemory.DashScope/KernelMemory.DashScope.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | Cnblogs.KernelMemory.AI.DashScope 8 | Cnblogs.KernelMemory.AI.DashScope 9 | Provide access to DashScope LLM models in Kernel Memory to generate embeddings and text 10 | README.md 11 | AI;SemanticKernel;DashScope;Qwen 12 | https://github.com/cnblogs/semantic-kernel-dashscope 13 | git 14 | MIT 15 | cnblogs.com 16 | true 17 | KMEXP00 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /test/KernelMemory.DashScope.UnitTests/KernelMemory.DashScope.UnitTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | false 9 | true 10 | 11 | 12 | 13 | 14 | all 15 | runtime; build; native; contentfiles; analyzers; buildtransitive 16 | 17 | 18 | 19 | 20 | 21 | all 22 | runtime; build; native; contentfiles; analyzers; buildtransitive 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/KernelMemory.DashScope/DashScopeTextEmbeddingGenerator.cs: -------------------------------------------------------------------------------- 1 | using Cnblogs.DashScope.Core; 2 | using Microsoft.KernelMemory; 3 | using Microsoft.KernelMemory.AI; 4 | 5 | namespace Cnblogs.KernelMemory.AI.DashScope; 6 | 7 | /// 8 | /// DashScope text embedding generator implementation. 9 | /// 10 | /// The . 11 | /// The model id to use. 12 | /// The tokenizer to use. 13 | /// Maximum token limit. 14 | public class DashScopeTextEmbeddingGenerator( 15 | IDashScopeClient dashScopeClient, 16 | string modelId, 17 | ITextTokenizer? tokenizer = null, 18 | int maxTokens = 8192) 19 | : ITextEmbeddingGenerator 20 | { 21 | /// 22 | public int CountTokens(string text) 23 | { 24 | return tokenizer?.CountTokens(text) ?? text.Length; 25 | } 26 | 27 | /// 28 | public IReadOnlyList GetTokens(string text) 29 | { 30 | return tokenizer?.GetTokens(text) ?? [text]; 31 | } 32 | 33 | /// 34 | public async Task GenerateEmbeddingAsync( 35 | string text, 36 | CancellationToken cancellationToken = new()) 37 | { 38 | var result = await dashScopeClient.GetEmbeddingsAsync( 39 | new ModelRequest 40 | { 41 | Input = new TextEmbeddingInput { Texts = [text] }, 42 | Model = modelId 43 | }, 44 | cancellationToken); 45 | return result.Output.Embeddings[0].Embedding; 46 | } 47 | 48 | /// 49 | public int MaxTokens => maxTokens; 50 | } 51 | -------------------------------------------------------------------------------- /src/KernelMemory.DashScope/DashScopeConfig.cs: -------------------------------------------------------------------------------- 1 | namespace Cnblogs.KernelMemory.AI.DashScope; 2 | 3 | /// 4 | /// DashScope Settings. 5 | /// 6 | public record DashScopeConfig 7 | { 8 | /// 9 | /// Model used for text generation. Chat models can be used too. 10 | /// 11 | public string ChatCompletionModelId { get; set; } = string.Empty; 12 | 13 | /// 14 | /// The max number of tokens supported by the text model. 15 | /// 16 | public int TextModelMaxTokenTotal { get; set; } = 6000; 17 | 18 | /// 19 | /// Model used to embedding generation. 20 | /// 21 | public string TextEmbeddingModelId { get; set; } = string.Empty; 22 | 23 | /// 24 | /// The max number of tokens supported by the embedding model. 25 | /// Defaults to 2048. 26 | /// 27 | public int EmbeddingModelMaxTokenTotal { get; set; } = 2048; 28 | 29 | /// 30 | /// DashScope API key. 31 | /// 32 | public string ApiKey { get; set; } = string.Empty; 33 | 34 | /// 35 | /// Validates the config. 36 | /// 37 | public void EnsureValid() 38 | { 39 | if (string.IsNullOrWhiteSpace(ApiKey)) 40 | { 41 | throw new ArgumentOutOfRangeException(nameof(ApiKey), ApiKey, "Api key cannot be null or empty"); 42 | } 43 | 44 | if (TextModelMaxTokenTotal < 1) 45 | { 46 | throw new ArgumentOutOfRangeException( 47 | nameof(TextModelMaxTokenTotal), 48 | TextModelMaxTokenTotal, 49 | $"{nameof(TextModelMaxTokenTotal)} cannot be less than 1"); 50 | } 51 | 52 | if (EmbeddingModelMaxTokenTotal < 1) 53 | { 54 | throw new ArgumentOutOfRangeException( 55 | nameof(EmbeddingModelMaxTokenTotal), 56 | EmbeddingModelMaxTokenTotal, 57 | $"{nameof(EmbeddingModelMaxTokenTotal)} cannot be less than 1"); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/SemanticKernel.DashScope/DashScopeTextEmbeddingGenerationService.cs: -------------------------------------------------------------------------------- 1 | using Cnblogs.DashScope.Core; 2 | using Microsoft.SemanticKernel; 3 | using Microsoft.SemanticKernel.Embeddings; 4 | using Microsoft.SemanticKernel.Services; 5 | 6 | namespace Cnblogs.SemanticKernel.Connectors.DashScope; 7 | 8 | /// 9 | /// DashScope text embedding service. 10 | /// 11 | public class DashScopeTextEmbeddingGenerationService : ITextEmbeddingGenerationService 12 | { 13 | private readonly IDashScopeClient _client; 14 | private readonly string _modelId; 15 | private readonly Dictionary _attributes = new(); 16 | 17 | /// 18 | /// Create an instance of the DashScope text embedding connector. 19 | /// 20 | /// The model name. 21 | /// The underlying . 22 | public DashScopeTextEmbeddingGenerationService(string modelId, IDashScopeClient client) 23 | { 24 | _client = client; 25 | _modelId = modelId; 26 | _attributes.Add(AIServiceExtensions.ModelIdKey, modelId); 27 | } 28 | 29 | /// 30 | public async Task>> GenerateEmbeddingsAsync( 31 | IList data, 32 | Kernel? kernel = null, 33 | CancellationToken cancellationToken = new()) 34 | { 35 | var result = new List>(data.Count); 36 | var embeddings = await _client.GetEmbeddingsAsync( 37 | new ModelRequest 38 | { 39 | Model = _modelId, Input = new TextEmbeddingInput { Texts = data } 40 | }, 41 | cancellationToken); 42 | if (embeddings.Output.Embeddings.Count != data.Count) 43 | { 44 | throw new KernelException( 45 | $"Expected {data.Count} text embedding(s), but received {embeddings.Output.Embeddings.Count}"); 46 | } 47 | 48 | result.AddRange(embeddings.Output.Embeddings.Select(t => new ReadOnlyMemory(t.Embedding))); 49 | 50 | return result; 51 | } 52 | 53 | /// 54 | public IReadOnlyDictionary Attributes => _attributes; 55 | } 56 | -------------------------------------------------------------------------------- /test/KernelMemory.DashScope.UnitTests/KernelMemoryBuilderExtensionTests.cs: -------------------------------------------------------------------------------- 1 | using Cnblogs.KernelMemory.AI.DashScope; 2 | using FluentAssertions; 3 | using Microsoft.Extensions.Configuration; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Microsoft.KernelMemory; 6 | using Microsoft.KernelMemory.AI; 7 | 8 | namespace KernelMemory.DashScope.UnitTests; 9 | 10 | public class KernelMemoryBuilderExtensionTests 11 | { 12 | [Fact] 13 | public void WithDashScopeDefaults_UseDefaults_Success() 14 | { 15 | // Arrange 16 | var service = new ServiceCollection(); 17 | var builder = new KernelMemoryBuilder(service); 18 | 19 | // Act 20 | var memory = builder.WithDashScopeDefaults(Cases.DashScopeConfig.ApiKey).Build(); 21 | var provider = service.BuildServiceProvider(); 22 | 23 | // Assert 24 | memory.Should().BeOfType(); 25 | provider.GetService().Should().NotBeNull().And.BeOfType(); 26 | provider.GetService().Should().NotBeNull().And 27 | .BeOfType(); 28 | } 29 | 30 | [Fact] 31 | public void WithDashScope_UseConfigObject_Success() 32 | { 33 | // Arrange 34 | var service = new ServiceCollection(); 35 | var builder = new KernelMemoryBuilder(service); 36 | 37 | // Act 38 | var memory = builder.WithDashScope(Cases.DashScopeConfig).Build(); 39 | var provider = service.BuildServiceProvider(); 40 | 41 | // Assert 42 | memory.Should().BeOfType(); 43 | provider.GetService().Should().NotBeNull().And.BeOfType(); 44 | provider.GetService().Should().NotBeNull().And 45 | .BeOfType(); 46 | } 47 | 48 | [Fact] 49 | public void WithDashScope_UseConfiguration_Success() 50 | { 51 | // Arrange 52 | var service = new ServiceCollection(); 53 | var builder = new KernelMemoryBuilder(service); 54 | var configuration = new ConfigurationBuilder().AddInMemoryCollection(Cases.Configurations).Build(); 55 | 56 | // Act 57 | var memory = builder.WithDashScope(configuration).Build(); 58 | var provider = service.BuildServiceProvider(); 59 | 60 | // Assert 61 | memory.Should().BeOfType(); 62 | provider.GetService().Should().NotBeNull().And.BeOfType(); 63 | provider.GetService().Should().NotBeNull().And 64 | .BeOfType(); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /test/SemanticKernel.DashScope.UnitTest/ServiceCollectionExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | using Cnblogs.SemanticKernel.Connectors.DashScope; 2 | using FluentAssertions; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Microsoft.SemanticKernel; 5 | using Microsoft.SemanticKernel.ChatCompletion; 6 | using Microsoft.SemanticKernel.Embeddings; 7 | using Microsoft.SemanticKernel.TextGeneration; 8 | 9 | namespace SemanticKernel.DashScope.UnitTest; 10 | 11 | public class ServiceCollectionExtensionsTests 12 | { 13 | [Theory] 14 | [InlineData(InitializationType.ApiKey)] 15 | [InlineData(InitializationType.Configuration)] 16 | public void ServiceCollectionExtension_AddChatService_AddTextAndChatService(InitializationType type) 17 | { 18 | // Arrange 19 | var builder = Kernel.CreateBuilder(); 20 | builder.Services.AddLogging(); 21 | 22 | // Act 23 | _ = type switch 24 | { 25 | InitializationType.ApiKey => builder.Services.AddDashScopeChatCompletion(Cases.ApiKey, Cases.ModelId), 26 | InitializationType.Configuration => builder.Services.AddDashScopeChatCompletion(Cases.Configuration), 27 | _ => throw new ArgumentOutOfRangeException(nameof(type)) 28 | }; 29 | 30 | // Assert 31 | var provider = builder.Services.BuildServiceProvider(); 32 | var chat = provider.GetRequiredService(); 33 | var text = provider.GetRequiredService(); 34 | chat.Should().BeOfType(); 35 | text.Should().BeOfType(); 36 | } 37 | 38 | [Theory] 39 | [InlineData(InitializationType.ApiKey)] 40 | [InlineData(InitializationType.Configuration)] 41 | public void ServiceCollectionExtension_AddEmbeddingService_AddEmbeddingService(InitializationType type) 42 | { 43 | // Arrange 44 | var builder = Kernel.CreateBuilder(); 45 | 46 | // Act 47 | _ = type switch 48 | { 49 | InitializationType.ApiKey => builder.Services.AddDashScopeTextEmbeddingGeneration(Cases.ApiKey, Cases.ModelId), 50 | InitializationType.Configuration => builder.Services.AddDashScopeTextEmbeddingGeneration(Cases.Configuration), 51 | _ => throw new ArgumentOutOfRangeException(nameof(type)) 52 | }; 53 | 54 | // Assert 55 | var provider = builder.Services.BuildServiceProvider(); 56 | var text = provider.GetRequiredService(); 57 | text.Should().BeOfType(); 58 | } 59 | 60 | public enum InitializationType 61 | { 62 | ApiKey, 63 | Configuration 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /test/KernelMemory.DashScope.UnitTests/DependencyInjectorTests.cs: -------------------------------------------------------------------------------- 1 | using Cnblogs.DashScope.Core; 2 | using Cnblogs.KernelMemory.AI.DashScope; 3 | using FluentAssertions; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Microsoft.KernelMemory; 6 | using Microsoft.KernelMemory.AI; 7 | using NSubstitute; 8 | 9 | namespace KernelMemory.DashScope.UnitTests; 10 | 11 | public class DependencyInjectorTests 12 | { 13 | [Fact] 14 | public void TextEmbedding_NormalConfig_Inject() 15 | { 16 | // Arrange 17 | var services = new ServiceCollection(); 18 | var client = Substitute.For(); 19 | services.AddSingleton(client); 20 | 21 | // Act 22 | var provider = services.AddDashScopeTextEmbeddingGeneration(Cases.DashScopeConfig).BuildServiceProvider(); 23 | 24 | // Assert 25 | var generator = provider.GetService(); 26 | generator.Should() 27 | .NotBeNull("the text embedding service should be injected").And 28 | .BeOfType("DashScope implementation should be used"); 29 | generator!.MaxTokens.Should().Be(Cases.DashScopeConfig.EmbeddingModelMaxTokenTotal); 30 | } 31 | 32 | [Fact] 33 | public void TextEmbedding_InvalidConfig_Throw() 34 | { 35 | // Arrange 36 | var services = new ServiceCollection(); 37 | 38 | // Act 39 | var act = () => services.AddDashScopeTextEmbeddingGeneration(Cases.InvalidConfig).BuildServiceProvider(); 40 | 41 | // Assert 42 | act.Should().Throw("config must be valid"); 43 | } 44 | 45 | [Fact] 46 | public void TextGenerator_NormalConfig_Inject() 47 | { 48 | // Arrange 49 | var services = new ServiceCollection(); 50 | var client = Substitute.For(); 51 | services.AddSingleton(client); 52 | 53 | // Act 54 | var provider = services.AddDashScopeTextGeneration(Cases.DashScopeConfig).BuildServiceProvider(); 55 | 56 | // Assert 57 | var generator = provider.GetService(); 58 | generator.Should() 59 | .NotBeNull("the text embedding service should be injected").And 60 | .BeOfType("DashScope implementation should be used"); 61 | generator!.MaxTokenTotal.Should().Be(Cases.DashScopeConfig.TextModelMaxTokenTotal); 62 | } 63 | 64 | [Fact] 65 | public void TextGenerator_InvalidConfig_Throw() 66 | { 67 | // Arrange 68 | var services = new ServiceCollection(); 69 | 70 | // Act 71 | var act = () => services.AddDashScopeTextGeneration(Cases.InvalidConfig).BuildServiceProvider(); 72 | 73 | // Assert 74 | act.Should().Throw("config must be valid"); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /test/KernelMemory.DashScope.UnitTests/DashScopeTextEmbeddingGeneratorTests.cs: -------------------------------------------------------------------------------- 1 | using Cnblogs.DashScope.Core; 2 | using Cnblogs.KernelMemory.AI.DashScope; 3 | using FluentAssertions; 4 | using NSubstitute; 5 | using NSubstitute.Extensions; 6 | 7 | namespace KernelMemory.DashScope.UnitTests; 8 | 9 | public class DashScopeTextEmbeddingGeneratorTests 10 | { 11 | [Fact] 12 | public async Task EmbeddingGenerator_GenerateEmbeddings_GenerateAsync() 13 | { 14 | // Arrange 15 | ModelRequest? captured = null; 16 | var client = Substitute.For(); 17 | client.Configure().GetEmbeddingsAsync( 18 | Arg.Do>(x => captured = x)) 19 | .Returns(Cases.TextEmbeddingResponse); 20 | var generator = new DashScopeTextEmbeddingGenerator(client, Cases.ModelId); 21 | 22 | // Act 23 | var embeddings = await generator.GenerateEmbeddingAsync(Cases.Text); 24 | 25 | // Assert 26 | embeddings.Data.ToArray().Should().BeEquivalentTo(Cases.Embeddings); 27 | captured!.Parameters.Should().BeNull("no parameter suitable for kernel memory"); 28 | captured!.Input.Texts.Should().BeEquivalentTo([Cases.Text], "input text should be respected"); 29 | } 30 | 31 | [Fact] 32 | public void EmbeddingGenerator_NullTokenizer_UseLengthTokenizer() 33 | { 34 | // Arrange 35 | var client = Substitute.For(); 36 | var generator = new DashScopeTextEmbeddingGenerator(client, Cases.ModelId, tokenizer: null); 37 | 38 | // Act 39 | var count = generator.CountTokens(Cases.Text); 40 | 41 | // Assert 42 | count.Should().Be(Cases.Text.Length, "Length tokenizer will be used if no tokenizer is given"); 43 | } 44 | 45 | [Fact] 46 | public void EmbeddingGenerator_SpecifyTokenizer_UseGivenTokenizer() 47 | { 48 | // Arrange 49 | var client = Substitute.For(); 50 | var generator = new DashScopeTextEmbeddingGenerator(client, Cases.ModelId, tokenizer: new QWenTextTokenizer()); 51 | 52 | // Act 53 | var count = generator.CountTokens(Cases.Text); 54 | 55 | // Assert 56 | count.Should().Be(Cases.Tokens.Length, "If given, tokenizer from constructor should be used for count tokens"); 57 | } 58 | 59 | [Fact] 60 | public void EmbeddingGenerator_SpecifyMaxToken_SetMaxToken() 61 | { 62 | // Arrange 63 | const int maxToken = 1000; 64 | var client = Substitute.For(); 65 | var generator = new DashScopeTextEmbeddingGenerator(client, Cases.ModelId, null, maxToken); 66 | 67 | // Act 68 | var count = generator.MaxTokens; 69 | 70 | // Assert 71 | count.Should().Be(maxToken, "max token specified in constructor should be respected"); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SemanticKernel.DashScope 2 | 3 | Make DashScope work with Semantic Kernel and Kernel Memory. 4 | 5 | ## Get started with SemanticKernel 6 | 7 | Add the NuGet package to your project. 8 | 9 | ```shell 10 | dotnet add package Cnblogs.SemanticKernel.Connectors.DashScope 11 | ``` 12 | 13 | ```cs 14 | using Microsoft.SemanticKernel; 15 | 16 | var builder = Kernel.CreateBuilder(); 17 | builder.Services.AddDashScopeChatCompletion("your-api-key", "qwen-max"); 18 | var kernel = builder.Build(); 19 | 20 | var prompt = "Tell me about the Cnblogs"; 21 | var response = await kernel.InvokePromptAsync(prompt); 22 | Console.WriteLine(response); 23 | ``` 24 | 25 | ## ASP.NET Core with KernelMemory support 26 | 27 | Install Nuget package `Cnblogs.KernelMemory.AI.DashScope` 28 | 29 | Install Nuget package `Microsoft.KernelMemory.Core` 30 | 31 | Install Nuget package `Microsoft.KernelMemory.SemanticKernelPlugin` 32 | 33 | `appsettings.json` 34 | 35 | ```json 36 | { 37 | "dashScope": { 38 | "apiKey": "your-key", 39 | "chatCompletionModelId": "qwen-max", 40 | "textEmbeddingModelId": "text-embedding-v3" 41 | } 42 | } 43 | ``` 44 | 45 | `Program.cs` 46 | 47 | ```csharp 48 | // Kernel Memory stuff 49 | var memory = new KernelMemoryBuilder(builder.Services).WithDashScope(builder.Configuration).Build(); 50 | builder.Services.AddSingleton(memory); 51 | 52 | // SK stuff 53 | builder.Services.AddDashScopeChatCompletion(builder.Configuration); 54 | builder.Services.AddSingleton( 55 | sp => 56 | { 57 | var plugins = new KernelPluginCollection(); 58 | plugins.AddFromObject( 59 | new MemoryPlugin(sp.GetRequiredService(), waitForIngestionToComplete: true), 60 | "memory"); 61 | return new Kernel(sp, plugins); 62 | }); 63 | ``` 64 | 65 | Services 66 | 67 | ```csharp 68 | public class YourService(Kernel kernel, IKernelMemory memory) 69 | { 70 | public async Task GetCompletionAsync(string prompt) 71 | { 72 | var chatResult = await kernel.InvokePromptAsync(prompt); 73 | return chatResult.ToString(); 74 | } 75 | 76 | public async Task ImportDocumentAsync(string filePath, string documentId) 77 | { 78 | await memory.ImportDocumentAsync(filePath, documentId); 79 | } 80 | 81 | public async Task AskMemoryAsync(string question) 82 | { 83 | // use memory.ask to query kernel memory 84 | var skPrompt = """ 85 | Question to Kernel Memory: {{$input}} 86 | 87 | Kernel Memory Answer: {{memory.ask $input}} 88 | 89 | If the answer is empty say 'I don't know' otherwise reply with a preview of the answer, truncated to 15 words. 90 | """; 91 | 92 | // you can bundle created functions into a singleton service to reuse them 93 | var myFunction = kernel.CreateFunctionFromPrompt(skPrompt); 94 | var result = await myFunction.InvokeAsync(question); 95 | return result.ToString(); 96 | } 97 | } 98 | ``` 99 | -------------------------------------------------------------------------------- /src/KernelMemory.DashScope/DashScopeTextGenerator.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | using Cnblogs.DashScope.Core; 3 | using Microsoft.Extensions.Logging; 4 | using Microsoft.KernelMemory; 5 | using Microsoft.KernelMemory.AI; 6 | using Microsoft.KernelMemory.Diagnostics; 7 | 8 | namespace Cnblogs.KernelMemory.AI.DashScope; 9 | 10 | /// 11 | /// Text generator using DashScope. 12 | /// 13 | /// The . 14 | /// Model name. 15 | /// Logger factory to use. 16 | /// Tokenizer to use. 17 | /// Maximum token count. 18 | public class DashScopeTextGenerator( 19 | IDashScopeClient dashScopeClient, 20 | string modelId, 21 | ILoggerFactory? loggerFactory = null, 22 | ITextTokenizer? tokenizer = null, 23 | int maxToken = 6000) : ITextGenerator 24 | { 25 | private readonly ILogger _logger = loggerFactory?.CreateLogger() 26 | ?? DefaultLogger.Instance; 27 | 28 | /// 29 | public int CountTokens(string text) 30 | { 31 | return tokenizer?.CountTokens(text) ?? QWenTokenizer.CountTokens(text); 32 | } 33 | 34 | /// 35 | public IReadOnlyList GetTokens(string text) 36 | { 37 | return tokenizer?.GetTokens(text) 38 | ?? QWenTokenizer.Tokenizer.EncodeToTokens(text, out _).Select(x => x.Value).ToList(); 39 | } 40 | 41 | /// 42 | public async IAsyncEnumerable GenerateTextAsync( 43 | string prompt, 44 | TextGenerationOptions options, 45 | [EnumeratorCancellation] CancellationToken cancellationToken = new()) 46 | { 47 | var parameters = new TextGenerationParameters 48 | { 49 | TopP = options.NucleusSampling == 0 ? null : (float)options.NucleusSampling, 50 | Temperature = options.Temperature == 0 ? null : (float)options.Temperature, 51 | RepetitionPenalty = 52 | options.FrequencyPenalty == 0 53 | ? null 54 | : ((float)options.FrequencyPenalty + 1), // dashScope's default value is 1.0, kernel memory is 0.0 55 | MaxTokens = options.MaxTokens == 0 ? null : options.MaxTokens, 56 | Stop = options.StopSequences.ToArray(), 57 | IncrementalOutput = true, 58 | ResultFormat = ResultFormats.Text 59 | }; 60 | 61 | if (options.TokenSelectionBiases.Count != 0) 62 | { 63 | _logger.LogWarning("TokenSelectionBiases is not supported by DashScope and will be ignored"); 64 | } 65 | 66 | var request = new ModelRequest 67 | { 68 | Model = modelId, 69 | Input = new TextGenerationInput { Prompt = prompt }, 70 | Parameters = parameters 71 | }; 72 | var tokens = dashScopeClient.GetTextCompletionStreamAsync(request, cancellationToken); 73 | await foreach (var token in tokens) 74 | { 75 | yield return new GeneratedTextContent( 76 | token.Output.Text ?? string.Empty, 77 | token.Usage.ToKernelMemoryTokenUsage(modelId)); 78 | } 79 | } 80 | 81 | /// 82 | public int MaxTokenTotal => maxToken; 83 | } 84 | -------------------------------------------------------------------------------- /test/KernelMemory.DashScope.UnitTests/Cases.cs: -------------------------------------------------------------------------------- 1 | using Cnblogs.DashScope.Core; 2 | using Cnblogs.KernelMemory.AI.DashScope; 3 | using Microsoft.KernelMemory.AI; 4 | 5 | namespace KernelMemory.DashScope.UnitTests; 6 | 7 | public static class Cases 8 | { 9 | public const string Text = "代码改变世界"; 10 | public static readonly int[] Tokens = [46100, 101933, 99489]; 11 | public const string ModelId = "qwen-max"; 12 | 13 | public static readonly TextGenerationOptions TextGenerationOptions = new() 14 | { 15 | NucleusSampling = 0.8, 16 | FrequencyPenalty = 0.1, 17 | MaxTokens = 1000, 18 | PresencePenalty = 0, 19 | ResultsPerPrompt = 1, 20 | StopSequences = ["你好"], 21 | TokenSelectionBiases = new() { { 19432, 0.5f } } 22 | }; 23 | 24 | public static readonly TextGenerationParameters TextGenerationParameters = new() 25 | { 26 | TopP = 0.8f, 27 | RepetitionPenalty = 1.1f, 28 | MaxTokens = 1000, 29 | EnableSearch = null, 30 | IncrementalOutput = true, 31 | ResultFormat = ResultFormats.Text, 32 | Seed = null, 33 | Stop = (string[]) ["你好"], 34 | TopK = null 35 | }; 36 | 37 | public static readonly ModelResponse TextGenerationResponse = new() 38 | { 39 | Output = new() { FinishReason = "stop", Text = "1+1 等于 2。这是最基本的数学加法之一,在十进制计数体系中,任何情况下两个一相加的结果都是二。" }, 40 | RequestId = "4ef2ed16-4dc3-9083-a723-fb2e80c84d3b", 41 | Usage = new() 42 | { 43 | InputTokens = 8, 44 | OutputTokens = 35, 45 | TotalTokens = 43 46 | } 47 | }; 48 | 49 | public static readonly float[] Embeddings = 50 | [ 51 | -0.011274966097430674f, 52 | 0.019980622395909534f, 53 | 0.009520792656014333f, 54 | 0.01861119331524994f, 55 | 0.0346400346498275f, 56 | -0.0045615030567685115f, 57 | 0.020032791122791806f, 58 | 0.02697123179813376f, 59 | -0.003495304701112112f, 60 | 0.03883961716385025f, 61 | 0.02131092493140743f 62 | ]; 63 | 64 | public static readonly ModelResponse TextEmbeddingResponse = new() 65 | { 66 | Output = new( 67 | [ 68 | new TextEmbeddingItem( 69 | 0, 70 | Embeddings) 71 | ]), 72 | RequestId = "4ef2ed16-4dc3-9083-a723-fb2e80c84d3b", 73 | Usage = new(3) 74 | }; 75 | 76 | public static readonly DashScopeConfig DashScopeConfig = new() 77 | { 78 | ApiKey = "apiKey", 79 | TextEmbeddingModelId = "text-embedding-v2", 80 | EmbeddingModelMaxTokenTotal = 1000, 81 | ChatCompletionModelId = "qwen-max", 82 | TextModelMaxTokenTotal = 1000 83 | }; 84 | 85 | public static readonly DashScopeConfig InvalidConfig = new() 86 | { 87 | ApiKey = string.Empty, 88 | TextEmbeddingModelId = "text-embedding-v2", 89 | EmbeddingModelMaxTokenTotal = 1000, 90 | ChatCompletionModelId = "qwen-max", 91 | TextModelMaxTokenTotal = 1000 92 | }; 93 | 94 | public static readonly Dictionary Configurations = new() 95 | { 96 | { "dashScope:apiKey", "apiKey" }, 97 | { "dashScope:chatCompletionModelId", "qwen-max" }, 98 | { "dashScope:textEmbeddingModelId", "text-embedding-v2" } 99 | }; 100 | } 101 | -------------------------------------------------------------------------------- /SemanticKernel.DashScope.sln: -------------------------------------------------------------------------------- 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.UnitTest", "test\SemanticKernel.DashScope.UnitTest\SemanticKernel.DashScope.UnitTest.csproj", "{648EBC1D-9409-4205-905E-DD06E4443AAA}" 18 | EndProject 19 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KernelMemory.DashScope", "src\KernelMemory.DashScope\KernelMemory.DashScope.csproj", "{B68EBD2E-8169-4546-95CC-92D6F097C039}" 20 | EndProject 21 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KernelMemory.DashScope.UnitTests", "test\KernelMemory.DashScope.UnitTests\KernelMemory.DashScope.UnitTests.csproj", "{A69BEA7D-6B14-4185-9696-94814F76A2DA}" 22 | EndProject 23 | Global 24 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 25 | Debug|Any CPU = Debug|Any CPU 26 | Release|Any CPU = Release|Any CPU 27 | EndGlobalSection 28 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 29 | {B9EF31C7-48D7-4CA8-8D15-D6340450D3F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 30 | {B9EF31C7-48D7-4CA8-8D15-D6340450D3F5}.Debug|Any CPU.Build.0 = Debug|Any CPU 31 | {B9EF31C7-48D7-4CA8-8D15-D6340450D3F5}.Release|Any CPU.ActiveCfg = Release|Any CPU 32 | {B9EF31C7-48D7-4CA8-8D15-D6340450D3F5}.Release|Any CPU.Build.0 = Release|Any CPU 33 | {648EBC1D-9409-4205-905E-DD06E4443AAA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 34 | {648EBC1D-9409-4205-905E-DD06E4443AAA}.Debug|Any CPU.Build.0 = Debug|Any CPU 35 | {648EBC1D-9409-4205-905E-DD06E4443AAA}.Release|Any CPU.ActiveCfg = Release|Any CPU 36 | {648EBC1D-9409-4205-905E-DD06E4443AAA}.Release|Any CPU.Build.0 = Release|Any CPU 37 | {B68EBD2E-8169-4546-95CC-92D6F097C039}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 38 | {B68EBD2E-8169-4546-95CC-92D6F097C039}.Debug|Any CPU.Build.0 = Debug|Any CPU 39 | {B68EBD2E-8169-4546-95CC-92D6F097C039}.Release|Any CPU.ActiveCfg = Release|Any CPU 40 | {B68EBD2E-8169-4546-95CC-92D6F097C039}.Release|Any CPU.Build.0 = Release|Any CPU 41 | {A69BEA7D-6B14-4185-9696-94814F76A2DA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 42 | {A69BEA7D-6B14-4185-9696-94814F76A2DA}.Debug|Any CPU.Build.0 = Debug|Any CPU 43 | {A69BEA7D-6B14-4185-9696-94814F76A2DA}.Release|Any CPU.ActiveCfg = Release|Any CPU 44 | {A69BEA7D-6B14-4185-9696-94814F76A2DA}.Release|Any CPU.Build.0 = Release|Any CPU 45 | EndGlobalSection 46 | GlobalSection(SolutionProperties) = preSolution 47 | HideSolutionNode = FALSE 48 | EndGlobalSection 49 | GlobalSection(NestedProjects) = preSolution 50 | {B9EF31C7-48D7-4CA8-8D15-D6340450D3F5} = {BB73FA18-BBBE-4C34-971A-D4206FC118A2} 51 | {648EBC1D-9409-4205-905E-DD06E4443AAA} = {DAB267DF-F966-4F95-AD12-56CC78D6F274} 52 | {B68EBD2E-8169-4546-95CC-92D6F097C039} = {BB73FA18-BBBE-4C34-971A-D4206FC118A2} 53 | {A69BEA7D-6B14-4185-9696-94814F76A2DA} = {DAB267DF-F966-4F95-AD12-56CC78D6F274} 54 | EndGlobalSection 55 | GlobalSection(ExtensibilityGlobals) = postSolution 56 | SolutionGuid = {D0F2E9A4-9782-4C3F-B459-CFCAD4C9AD9F} 57 | EndGlobalSection 58 | EndGlobal 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | obj/ 3 | /node_modules 4 | /wwwroot/node_modules 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | *.userprefs 10 | [Dd]ebug/ 11 | [Dd]ebugPublic/ 12 | [Rr]elease/ 13 | [Rr]eleases/ 14 | x64/ 15 | x86/ 16 | bld/ 17 | [Bb]in/ 18 | [Oo]bj/ 19 | [Ll]og/ 20 | .vs/ 21 | [Tt]est[Rr]esult*/ 22 | [Bb]uild[Ll]og.* 23 | *.VisualState.xml 24 | TestResult.xml 25 | [Dd]ebugPS/ 26 | [Rr]eleasePS/ 27 | dlldata.c 28 | project.lock.json 29 | project.fragment.lock.json 30 | artifacts/ 31 | *_i.c 32 | *_p.c 33 | *_i.h 34 | *.ilk 35 | *.meta 36 | *.obj 37 | *.pch 38 | *.pdb 39 | *.pgc 40 | *.pgd 41 | *.rsp 42 | *.sbr 43 | *.tlb 44 | *.tli 45 | *.tlh 46 | *.tmp 47 | *.tmp_proj 48 | *.log 49 | *.vspscc 50 | *.vssscc 51 | .builds 52 | *.pidb 53 | *.svclog 54 | *.scc 55 | _Chutzpah* 56 | ipch/ 57 | *.aps 58 | *.ncb 59 | *.opendb 60 | *.opensdf 61 | *.sdf 62 | *.cachefile 63 | *.VC.db 64 | *.VC.VC.opendb 65 | *.psess 66 | *.vsp 67 | *.vspx 68 | *.sap 69 | $tf/ 70 | *.gpState 71 | _ReSharper*/ 72 | *.[Rr]e[Ss]harper 73 | *.DotSettings.user 74 | .JustCode 75 | _TeamCity* 76 | *.dotCover 77 | *.coverage 78 | *.coveragexml 79 | _NCrunch_* 80 | .*crunch*.local.xml 81 | nCrunchTemp_* 82 | *.mm.* 83 | AutoTest.Net/ 84 | .sass-cache/ 85 | [Ee]xpress/ 86 | DocProject/buildhelp/ 87 | DocProject/Help/*.HxT 88 | DocProject/Help/*.HxC 89 | DocProject/Help/*.hhc 90 | DocProject/Help/*.hhk 91 | DocProject/Help/*.hhp 92 | DocProject/Help/Html2 93 | DocProject/Help/html 94 | publish/ 95 | *.[Pp]ublish.xml 96 | *.azurePubxml 97 | *.pubxml 98 | *.publishproj 99 | PublishScripts/ 100 | *.nupkg 101 | **/packages/* 102 | !**/packages/build/ 103 | *.nuget.props 104 | *.nuget.targets 105 | csx/ 106 | *.build.csdef 107 | ecf/ 108 | rcf/ 109 | AppPackages/ 110 | BundleArtifacts/ 111 | Package.StoreAssociation.xml 112 | _pkginfo.txt 113 | *.[Cc]ache 114 | !*.[Cc]ache/ 115 | ClientBin/ 116 | ~$* 117 | *~ 118 | *.dbmdl 119 | *.dbproj.schemaview 120 | *.jfm 121 | *.pfx 122 | *.publishsettings 123 | node_modules/ 124 | orleans.codegen.cs 125 | Generated_Code/ 126 | _UpgradeReport_Files/ 127 | Backup*/ 128 | UpgradeLog*.XML 129 | UpgradeLog*.htm 130 | *.mdf 131 | *.ldf 132 | *.rdl.data 133 | *.bim.layout 134 | *.bim_*.settings 135 | FakesAssemblies/ 136 | *.GhostDoc.xml 137 | .ntvs_analysis.dat 138 | *.plg 139 | *.opt 140 | **/*.HTMLClient/GeneratedArtifacts 141 | **/*.DesktopClient/GeneratedArtifacts 142 | **/*.DesktopClient/ModelManifest.xml 143 | **/*.Server/GeneratedArtifacts 144 | **/*.Server/ModelManifest.xml 145 | _Pvt_Extensions 146 | .paket/paket.exe 147 | paket-files/ 148 | .fake/ 149 | .idea/ 150 | *.sln.iml 151 | .cr/ 152 | __pycache__/ 153 | *.pyc 154 | *.rsuser 155 | mono_crash.* 156 | [Ww][Ii][Nn]32/ 157 | [Aa][Rr][Mm]/ 158 | [Aa][Rr][Mm]64/ 159 | [Ll]ogs/ 160 | Generated\ Files/ 161 | nunit-*.xml 162 | BenchmarkDotNet.Artifacts/ 163 | ScaffoldingReadMe.txt 164 | StyleCopReport.xml 165 | *_h.h 166 | *.iobj 167 | *.ipdb 168 | *_wpftmp.csproj 169 | *.tlog 170 | *.e2e 171 | .axoCover/* 172 | !.axoCover/settings.json 173 | coverage*.json 174 | coverage*.xml 175 | coverage*.info 176 | *.snupkg 177 | **/[Pp]ackages/* 178 | !**/[Pp]ackages/build/ 179 | *.appx 180 | *.appxbundle 181 | *.appxupload 182 | !?*.[Cc]ache/ 183 | ServiceFabricBackup/ 184 | *.rptproj.bak 185 | *.ndf 186 | *.rptproj.rsuser 187 | *- [Bb]ackup.rdl 188 | *- [Bb]ackup ([0-9]).rdl 189 | *- [Bb]ackup ([0-9][0-9]).rdl 190 | *.vbw 191 | *.vbp 192 | *.dsw 193 | *.dsp 194 | .cr/personal 195 | *.tss 196 | *.jmconfig 197 | *.btp.cs 198 | *.btm.cs 199 | *.odx.cs 200 | *.xsd.cs 201 | OpenCover/ 202 | ASALocalRun/ 203 | *.binlog 204 | *.nvuser 205 | .mfractor/ 206 | .localhistory/ 207 | .vshistory/ 208 | healthchecksdb 209 | MigrationBackup/ 210 | .ionide/ 211 | FodyWeavers.xsd 212 | .vscode/* 213 | !.vscode/settings.json 214 | !.vscode/tasks.json 215 | !.vscode/launch.json 216 | !.vscode/extensions.json 217 | *.code-workspace 218 | .history/ 219 | *.cab 220 | *.msi 221 | *.msix 222 | *.msm 223 | *.msp 224 | coverage*[.json, .xml, .info] 225 | -------------------------------------------------------------------------------- /src/SemanticKernel.DashScope/FunctionDefinitionConvertor.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using System.Text.Json; 3 | using Cnblogs.DashScope.Core; 4 | using Json.Schema; 5 | using Json.Schema.Generation; 6 | using Microsoft.SemanticKernel; 7 | 8 | namespace Cnblogs.SemanticKernel.Connectors.DashScope; 9 | 10 | /// 11 | /// Convertors from to 12 | /// 13 | internal static class FunctionDefinitionConvertor 14 | { 15 | private static readonly KernelJsonSchema DefaultSchemaForTypelessParameter = 16 | KernelJsonSchema.Parse("{\"type\":\"string\"}"); 17 | 18 | private const char FunctionNameSeparator = '-'; 19 | 20 | public static FunctionDefinition ToFunctionDefinition(this KernelFunctionMetadata metadata) 21 | { 22 | var required = new List(); 23 | var properties = new Dictionary(); 24 | foreach (var parameter in metadata.Parameters) 25 | { 26 | properties.Add( 27 | parameter.Name, 28 | parameter.Schema ?? GetDefaultSchemaForTypelessParameter(parameter.Description)); 29 | if (parameter.IsRequired) 30 | { 31 | required.Add(parameter.Name); 32 | } 33 | } 34 | 35 | var schema = KernelJsonSchema.Parse( 36 | JsonSerializer.Serialize( 37 | new 38 | { 39 | type = "object", 40 | required, 41 | properties 42 | })); 43 | 44 | var qualifiedName = string.IsNullOrEmpty(metadata.PluginName) 45 | ? metadata.Name 46 | : string.Join(FunctionNameSeparator, metadata.PluginName, metadata.Name); 47 | return new FunctionDefinition(qualifiedName, metadata.Description, schema); 48 | } 49 | 50 | public static bool TryGetKernelFunctionAndArguments( 51 | this KernelPluginCollection collection, 52 | FunctionCall functionCall, 53 | [NotNullWhen(true)] out KernelFunction? function, 54 | out KernelArguments? arguments) 55 | { 56 | var qualifiedName = functionCall.Name.AsSpan(); 57 | var separatorIndex = qualifiedName.IndexOf(FunctionNameSeparator); 58 | string? pluginName = null; 59 | var functionName = functionCall.Name; 60 | if (separatorIndex > 0) 61 | { 62 | pluginName = qualifiedName[..separatorIndex].Trim().ToString(); 63 | functionName = qualifiedName[(separatorIndex + 1)..].Trim().ToString(); 64 | } 65 | 66 | arguments = null; 67 | if (collection.TryGetFunction(pluginName, functionName, out function) == false) 68 | { 69 | return false; 70 | } 71 | 72 | if (string.IsNullOrEmpty(functionCall.Arguments)) 73 | { 74 | return true; 75 | } 76 | 77 | var dic = JsonSerializer.Deserialize>(functionCall.Arguments)!; 78 | arguments = new KernelArguments(); 79 | foreach (var parameter in dic) 80 | { 81 | arguments[parameter.Key] = parameter.Value?.ToString(); 82 | } 83 | 84 | return true; 85 | } 86 | 87 | private static KernelJsonSchema GetDefaultSchemaForTypelessParameter(string? description) 88 | { 89 | // If there's a description, incorporate it. 90 | if (!string.IsNullOrWhiteSpace(description)) 91 | { 92 | return KernelJsonSchema.Parse( 93 | JsonSerializer.Serialize( 94 | new JsonSchemaBuilder() 95 | .FromType(typeof(string)) 96 | .Description(description) 97 | .Build())); 98 | } 99 | 100 | // Otherwise, we can use a cached schema for a string with no description. 101 | return DefaultSchemaForTypelessParameter; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /test/KernelMemory.DashScope.UnitTests/DashScopeTextGeneratorTests.cs: -------------------------------------------------------------------------------- 1 | using Cnblogs.DashScope.Core; 2 | using Cnblogs.KernelMemory.AI.DashScope; 3 | using FluentAssertions; 4 | using Microsoft.Extensions.Logging.Abstractions; 5 | using NSubstitute; 6 | using NSubstitute.Extensions; 7 | 8 | namespace KernelMemory.DashScope.UnitTests; 9 | 10 | public class DashScopeTextGeneratorTests 11 | { 12 | [Fact] 13 | public async Task TextGenerator_GenerateText_IncrementalGenerateAsync() 14 | { 15 | // Arrange 16 | var list = new[] { Cases.TextGenerationResponse }; 17 | var client = Substitute.For(); 18 | ModelRequest? captured = null; 19 | client.Configure() 20 | .GetTextCompletionStreamAsync( 21 | Arg.Do>(x => captured = x)) 22 | .Returns(list.ToAsyncEnumerable()); 23 | var generator = new DashScopeTextGenerator(client, Cases.ModelId, new NullLoggerFactory()); 24 | 25 | // Act 26 | var response = await generator.GenerateTextAsync(Cases.Text, Cases.TextGenerationOptions).ToListAsync(); 27 | 28 | // Assert 29 | response[0].Text.Should().BeSameAs( 30 | Cases.TextGenerationResponse.Output.Text, 31 | "generated text should mapped from output.text"); 32 | captured.Should().BeEquivalentTo( 33 | new { Parameters = Cases.TextGenerationParameters }, 34 | "text options should be mapped to text generation parameters correctly"); 35 | } 36 | 37 | [Fact] 38 | public async Task TextGenerator_DefaultsToZero_MapZeroToNullAsync() 39 | { 40 | // Arrange 41 | var list = new[] { Cases.TextGenerationResponse }; 42 | var client = Substitute.For(); 43 | ModelRequest? captured = null; 44 | client.Configure() 45 | .GetTextCompletionStreamAsync( 46 | Arg.Do>(x => captured = x)) 47 | .Returns(list.ToAsyncEnumerable()); 48 | var generator = new DashScopeTextGenerator(client, Cases.ModelId, new NullLoggerFactory()); 49 | 50 | // Act 51 | var response = await generator.GenerateTextAsync(Cases.Text, Cases.TextGenerationOptions).ToListAsync(); 52 | 53 | // Assert 54 | response[0].Text.Should().BeSameAs( 55 | Cases.TextGenerationResponse.Output.Text, 56 | "generated text should mapped from output.text"); 57 | captured.Should().BeEquivalentTo( 58 | new { Parameters = Cases.TextGenerationParameters }, 59 | "text options should be mapped to text generation parameters correctly"); 60 | } 61 | 62 | [Fact] 63 | public void TextGenerator_NullTokenizer_UseQWenTokenizer() 64 | { 65 | // Arrange 66 | var client = Substitute.For(); 67 | var generator = new DashScopeTextGenerator(client, Cases.ModelId, new NullLoggerFactory(), tokenizer: null); 68 | 69 | // Act 70 | var count = generator.CountTokens(Cases.Text); 71 | 72 | // Assert 73 | count.Should().Be(Cases.Tokens.Length, "QWen tokenizer will be used if no tokenizer is given"); 74 | } 75 | 76 | [Fact] 77 | public void TextGenerator_SpecifyTokenizer_UseGivenTokenizer() 78 | { 79 | // Arrange 80 | var client = Substitute.For(); 81 | var generator = new DashScopeTextGenerator(client, Cases.ModelId, new NullLoggerFactory(), tokenizer: new LengthTokenizer()); 82 | 83 | // Act 84 | var count = generator.CountTokens(Cases.Text); 85 | 86 | // Assert 87 | count.Should().Be(Cases.Text.Length, "If given, tokenizer from constructor should be used for count tokens"); 88 | } 89 | 90 | [Fact] 91 | public void TextGenerator_SpecifyMaxToken_SetMaxToken() 92 | { 93 | // Arrange 94 | const int maxToken = 1000; 95 | var client = Substitute.For(); 96 | var generator = new DashScopeTextGenerator(client, Cases.ModelId, new NullLoggerFactory(), null, maxToken); 97 | 98 | // Act 99 | var count = generator.MaxTokenTotal; 100 | 101 | // Assert 102 | count.Should().Be(maxToken, "max token specified in constructor should be respected"); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/SemanticKernel.DashScope/DashScopePromptExecutionSettings.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using System.Text.Json; 3 | using Cnblogs.DashScope.Core; 4 | using Microsoft.SemanticKernel; 5 | using Microsoft.SemanticKernel.ChatCompletion; 6 | 7 | namespace Cnblogs.SemanticKernel.Connectors.DashScope; 8 | 9 | /// 10 | /// Settings for DashScope prompt execution. 11 | /// 12 | public class DashScopePromptExecutionSettings : PromptExecutionSettings, ITextGenerationParameters 13 | { 14 | /// 15 | public bool? IncrementalOutput { get; set; } 16 | 17 | /// 18 | public ulong? Seed { get; set; } 19 | 20 | /// 21 | public float? TopP { get; set; } 22 | 23 | /// 24 | public int? TopK { get; set; } 25 | 26 | /// 27 | public string? ResultFormat { get; set; } 28 | 29 | /// 30 | public DashScopeResponseFormat? ResponseFormat { get; } 31 | 32 | /// 33 | public int? MaxTokens { get; set; } 34 | 35 | /// 36 | public float? RepetitionPenalty { get; set; } 37 | 38 | /// 39 | public float? PresencePenalty { get; } 40 | 41 | /// 42 | public float? Temperature { get; set; } 43 | 44 | /// 45 | public TextGenerationStop? Stop { get; set; } 46 | 47 | /// 48 | public bool? EnableSearch { get; set; } 49 | 50 | /// 51 | public ToolChoice? ToolChoice { get; } 52 | 53 | /// 54 | public bool? ParallelToolCalls { get; set; } 55 | 56 | /// 57 | public IEnumerable? Tools { get; set; } 58 | 59 | /// 60 | /// Gets or sets the behavior for how tool calls are handled. 61 | /// 62 | /// 63 | /// 64 | /// To disable all tool calling, set the property to null (the default). 65 | /// 66 | /// To allow the model to request one of any number of functions, set the property to an 67 | /// instance returned from , called with 68 | /// a list of the functions available. 69 | /// 70 | /// 71 | /// To allow the model to request one of any of the functions in the supplied , 72 | /// set the property to if the client should simply 73 | /// send the information about the functions and not handle the response in any special manner, or 74 | /// if the client should attempt to automatically 75 | /// invoke the function and send the result back to the service. 76 | /// 77 | /// 78 | /// For all options where an instance is provided, auto-invoke behavior may be selected. If the service 79 | /// sends a request for a function call, if auto-invoke has been requested, the client will attempt to 80 | /// resolve that function from the functions available in the , and if found, rather 81 | /// than returning the response back to the caller, it will handle the request automatically, invoking 82 | /// the function, and sending back the result. The intermediate messages will be retained in the 83 | /// if an instance was provided. 84 | /// 85 | public ToolCallBehavior? ToolCallBehavior { get; set; } 86 | 87 | [return: NotNullIfNotNull(nameof(settings))] 88 | internal static DashScopePromptExecutionSettings? FromPromptExecutionSettings(PromptExecutionSettings? settings) 89 | { 90 | if (settings is null) 91 | { 92 | return null; 93 | } 94 | 95 | if (settings is DashScopePromptExecutionSettings dashScopePromptExecutionSettings) 96 | { 97 | return dashScopePromptExecutionSettings; 98 | } 99 | 100 | var json = JsonSerializer.Serialize(settings); 101 | var response = 102 | JsonSerializer.Deserialize(json, JsonOptionCache.ReadPermissive); 103 | if (response is not null) 104 | { 105 | return response; 106 | } 107 | 108 | throw new ArgumentException( 109 | $"The input execution setting can not be converted to {nameof(DashScopePromptExecutionSettings)}", 110 | nameof(settings)); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/SemanticKernel.DashScope/DashScopeServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using Cnblogs.DashScope.Core; 2 | using Cnblogs.SemanticKernel.Connectors.DashScope; 3 | using Microsoft.Extensions.Configuration; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Microsoft.Extensions.Logging; 6 | using Microsoft.SemanticKernel.ChatCompletion; 7 | using Microsoft.SemanticKernel.Embeddings; 8 | using Microsoft.SemanticKernel.TextGeneration; 9 | 10 | // ReSharper disable once CheckNamespace 11 | namespace Microsoft.SemanticKernel; 12 | 13 | /// 14 | /// Extensions for DI. 15 | /// 16 | public static class DashScopeServiceCollectionExtensions 17 | { 18 | #region TextEmbedding 19 | 20 | /// 21 | /// Adds a DashScope text embedding generation service 22 | /// 23 | /// The . 24 | /// The configuration root. 25 | /// The local identifier of service. 26 | /// The section name in configuration. 27 | /// 28 | public static IServiceCollection AddDashScopeTextEmbeddingGeneration( 29 | this IServiceCollection services, 30 | IConfiguration configuration, 31 | string sectionName = "dashScope", 32 | string? serviceId = null) 33 | { 34 | var option = configuration.GetOptions(sectionName); 35 | return services.AddDashScopeTextEmbeddingGeneration(option.ApiKey, option.TextEmbeddingModelId, serviceId); 36 | } 37 | 38 | /// 39 | /// Adds a DashScope text embedding generation service. 40 | /// 41 | /// The . 42 | /// The api key of DashScope. 43 | /// The model id. 44 | /// A local identifier for the given AI service. 45 | /// 46 | public static IServiceCollection AddDashScopeTextEmbeddingGeneration( 47 | this IServiceCollection services, 48 | string apiKey, 49 | string modelId, 50 | string? serviceId = null) 51 | { 52 | return services.AddKeyedSingleton( 53 | serviceId, 54 | (_, _) => new DashScopeTextEmbeddingGenerationService(modelId, new DashScopeClient(apiKey))); 55 | } 56 | 57 | #endregion 58 | 59 | #region ChatCompletion 60 | 61 | /// 62 | /// Add DashScope as chat completion service and fetch from . 63 | /// 64 | /// The . 65 | /// The configuration root. 66 | /// The local identifier of service. 67 | /// The section name in configuration. 68 | /// 69 | public static IServiceCollection AddDashScopeChatCompletion( 70 | this IServiceCollection services, 71 | IConfiguration configuration, 72 | string? serviceId = null, 73 | string sectionName = "dashScope") 74 | { 75 | var option = configuration.GetOptions(sectionName); 76 | return services.AddDashScopeChatCompletion(option.ApiKey, option.ChatCompletionModelId, serviceId); 77 | } 78 | 79 | /// 80 | /// Add DashScope as chat completion service. 81 | /// 82 | /// The 83 | /// The api key for DashScope. 84 | /// The model name. 85 | /// The local identifier of service. 86 | /// 87 | public static IServiceCollection AddDashScopeChatCompletion( 88 | this IServiceCollection services, 89 | string apiKey, 90 | string modelId, 91 | string? serviceId = null) 92 | { 93 | services.AddKeyedSingleton( 94 | serviceId, 95 | (sp, _) => new DashScopeChatCompletionService( 96 | modelId, 97 | new DashScopeClient(apiKey), 98 | sp.GetService())); 99 | return services.AddKeyedSingleton( 100 | serviceId, 101 | (sp, _) => new DashScopeChatCompletionService( 102 | modelId, 103 | new DashScopeClient(apiKey), 104 | sp.GetService())); 105 | } 106 | 107 | #endregion 108 | 109 | private static DashScopeOptions GetOptions(this IConfiguration configuration, string sectionName) 110 | { 111 | return configuration.GetSection(sectionName).Get() 112 | ?? throw new InvalidOperationException( 113 | $"Can not resolve {nameof(DashScopeOptions)} from section: {sectionName}"); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /test/SemanticKernel.DashScope.UnitTest/Cases.cs: -------------------------------------------------------------------------------- 1 | using Cnblogs.DashScope.Core; 2 | using Microsoft.Extensions.Configuration; 3 | using Microsoft.SemanticKernel; 4 | using Microsoft.SemanticKernel.ChatCompletion; 5 | 6 | namespace SemanticKernel.DashScope.UnitTest; 7 | 8 | public static class Cases 9 | { 10 | public const string Prompt = "prompt"; 11 | public const string ModelId = "qwen-max"; 12 | public const string ModelIdAlter = "qwen-plus"; 13 | public const string ApiKey = "api-key"; 14 | 15 | public static readonly ChatHistory ChatHistory = new("You are a helpful assistant") 16 | { 17 | new ChatMessageContent(AuthorRole.User, "请问 1+1 是多少") 18 | }; 19 | 20 | public static readonly IConfiguration Configuration = 21 | new ConfigurationBuilder().AddInMemoryCollection( 22 | new Dictionary 23 | { 24 | { "dashScope:apiKey", ApiKey }, 25 | { "dashScope:chatCompletionModelId", ModelId }, 26 | { "dashScope:textEmbeddingModelId", ModelIdAlter } 27 | }).Build(); 28 | 29 | public static readonly ModelResponse TextGenerationResponse = new() 30 | { 31 | Output = new() { FinishReason = "stop", Text = "1+1 等于 2。这是最基本的数学加法之一,在十进制计数体系中,任何情况下两个一相加的结果都是二。" }, 32 | RequestId = "4ef2ed16-4dc3-9083-a723-fb2e80c84d3b", 33 | Usage = new() 34 | { 35 | InputTokens = 8, 36 | OutputTokens = 35, 37 | TotalTokens = 43 38 | } 39 | }; 40 | 41 | public static readonly ModelResponse TextEmbeddingResponse = new() 42 | { 43 | Output = new( 44 | [ 45 | new( 46 | 0, 47 | [ 48 | -0.005039233741296242f, 0.014719783208941139f, 0.005160200817840309f, 0.024575416603162974f, 49 | 0.04125613978976582f, -0.003979180149475871f 50 | ]) 51 | ]), 52 | RequestId = "1773f7b2-2148-9f74-b335-b413e398a116", 53 | Usage = new(3) 54 | }; 55 | 56 | public static KernelFunction NormalFunction(Action method) 57 | => KernelFunctionFactory.CreateFromMethod( 58 | (string location) => 59 | { 60 | method(); 61 | return "Weather"; 62 | }, 63 | "GetCurrentWeather"); 64 | 65 | public static KernelFunction AlterFunction(Action method) 66 | => KernelFunctionFactory.CreateFromMethod( 67 | (string location) => 68 | { 69 | method(); 70 | return "Weather"; 71 | }, 72 | "GetCurrentWeatherAlter"); 73 | 74 | public static KernelPlugin Plugin(params KernelFunction[] functions) 75 | => KernelPluginFactory.CreateFromFunctions("MyPlugin", functions); 76 | 77 | public static ModelResponse ErrToolCallResponse( 78 | KernelFunction[] functions, 79 | string toolType = "function", 80 | string pluginName = "MyPlugin", 81 | string paramBody = "{\"location\": \"LA\"}") 82 | => new() 83 | { 84 | Output = new() 85 | { 86 | Choices = 87 | [ 88 | new() 89 | { 90 | FinishReason = "tool_call", 91 | Message = new( 92 | "assistant", 93 | string.Empty, 94 | ToolCalls: functions.Select( 95 | (f, i) => new ToolCall( 96 | $"{i}", 97 | toolType, 98 | i, 99 | new($"{pluginName}-{f.Name}", paramBody))).ToList()) 100 | } 101 | ] 102 | }, 103 | Usage = new() 104 | { 105 | InputTokens = 10, 106 | OutputTokens = 30, 107 | TotalTokens = 40 108 | } 109 | }; 110 | 111 | public static ModelResponse 112 | ToolCallResponse(params KernelFunction[] functions) 113 | => new() 114 | { 115 | Output = new() 116 | { 117 | Choices = 118 | [ 119 | new() 120 | { 121 | FinishReason = "tool_call", 122 | Message = new( 123 | "assistant", 124 | string.Empty, 125 | ToolCalls: functions.Select( 126 | f => new ToolCall( 127 | "0", 128 | "function", 129 | 0, 130 | new($"MyPlugin-{f.Name}", "{\"location\": \"LA\"}"))).ToList()) 131 | } 132 | ] 133 | }, 134 | Usage = new() 135 | { 136 | InputTokens = 10, 137 | OutputTokens = 30, 138 | TotalTokens = 40 139 | } 140 | }; 141 | 142 | public static readonly ModelResponse ChatGenerationResponse = new() 143 | { 144 | Output = new() 145 | { 146 | Choices = 147 | [ 148 | new() 149 | { 150 | FinishReason = "stop", 151 | Message = new( 152 | "assistant", 153 | "1+1 等于 2。这是最基本的数学加法之一,在十进制计数体系中,任何两个相同的数字相加都等于该数字的二倍。") 154 | } 155 | ] 156 | }, 157 | RequestId = "e764bfe3-c0b7-97a0-ae57-cd99e1580960", 158 | Usage = new() 159 | { 160 | TotalTokens = 47, 161 | OutputTokens = 39, 162 | InputTokens = 8 163 | } 164 | }; 165 | } 166 | -------------------------------------------------------------------------------- /test/SemanticKernel.DashScope.UnitTest/TextCompletionTests.cs: -------------------------------------------------------------------------------- 1 | using Cnblogs.DashScope.Core; 2 | using Cnblogs.SemanticKernel.Connectors.DashScope; 3 | using FluentAssertions; 4 | using Microsoft.Extensions.Logging.Abstractions; 5 | using Microsoft.SemanticKernel; 6 | using NSubstitute; 7 | using NSubstitute.Extensions; 8 | 9 | namespace SemanticKernel.DashScope.UnitTest; 10 | 11 | public class TextCompletionTests 12 | { 13 | [Theory] 14 | [MemberData(nameof(Settings))] 15 | public async Task GetTextContent_Normal_SuccessAsync(PromptExecutionSettings? settings) 16 | { 17 | // Arrange 18 | var dashScopeClient = Substitute.For(); 19 | dashScopeClient.Configure() 20 | .GetTextCompletionAsync(Arg.Any>()) 21 | .Returns(Task.FromResult(Cases.TextGenerationResponse)); 22 | var service = new DashScopeChatCompletionService( 23 | Cases.ModelId, 24 | dashScopeClient, 25 | NullLoggerFactory.Instance); 26 | 27 | // Act 28 | var response = await service.GetTextContentsAsync(Cases.Prompt, settings); 29 | 30 | // Assert 31 | await dashScopeClient.Received().GetTextCompletionAsync( 32 | Arg.Is>( 33 | x => x.Parameters != null 34 | && x.Parameters.Seed == (settings == null ? null : 1000) 35 | && x.Parameters.IncrementalOutput == false 36 | && x.Parameters.ResultFormat == ResultFormats.Text)); 37 | response.Should().BeEquivalentTo([new { Cases.TextGenerationResponse.Output.Text }]); 38 | response[0].Metadata.Should() 39 | .Contain( 40 | [ 41 | new KeyValuePair("Usage", Cases.TextGenerationResponse.Usage), 42 | new KeyValuePair("RequestId", Cases.TextGenerationResponse.RequestId) 43 | ]); 44 | } 45 | 46 | [Fact] 47 | public async Task GetTextContent_OverrideModelId_SuccessAsync() 48 | { 49 | // Arrange 50 | var dashScopeClient = Substitute.For(); 51 | dashScopeClient.Configure() 52 | .GetTextCompletionAsync(Arg.Any>()) 53 | .Returns(Task.FromResult(Cases.TextGenerationResponse)); 54 | var service = new DashScopeChatCompletionService( 55 | Cases.ModelId, 56 | dashScopeClient, 57 | NullLoggerFactory.Instance); 58 | var settings = new DashScopePromptExecutionSettings { ModelId = Cases.ModelIdAlter }; 59 | 60 | // Act 61 | _ = await service.GetTextContentsAsync(Cases.Prompt, settings); 62 | 63 | // Assert 64 | await dashScopeClient.Received().GetTextCompletionAsync( 65 | Arg.Is>(x => x.Model == Cases.ModelIdAlter)); 66 | } 67 | 68 | [Theory] 69 | [MemberData(nameof(Settings))] 70 | public async Task GetTextContentStream_Normal_SuccessAsync(PromptExecutionSettings? settings) 71 | { 72 | // Arrange 73 | var dashScopeClient = Substitute.For(); 74 | var list = new[] { Cases.TextGenerationResponse }; 75 | dashScopeClient.Configure() 76 | .GetTextCompletionStreamAsync(Arg.Any>()) 77 | .Returns(list.ToAsyncEnumerable()); 78 | var service = new DashScopeChatCompletionService( 79 | Cases.ModelId, 80 | dashScopeClient, 81 | NullLoggerFactory.Instance); 82 | 83 | // Act 84 | var response = await service.GetStreamingTextContentsAsync(Cases.Prompt, settings).ToListAsync(); 85 | 86 | // Assert 87 | _ = dashScopeClient.Received().GetTextCompletionStreamAsync( 88 | Arg.Is>( 89 | x => x.Parameters != null 90 | && x.Parameters.Seed == (settings == null ? null : 1000) 91 | && x.Parameters.IncrementalOutput == true 92 | && x.Parameters.ResultFormat == ResultFormats.Text)); 93 | response.Should().BeEquivalentTo([new { Cases.TextGenerationResponse.Output.Text }]); 94 | response[0].Metadata.Should() 95 | .Contain( 96 | [ 97 | new KeyValuePair("Usage", Cases.TextGenerationResponse.Usage), 98 | new KeyValuePair("RequestId", Cases.TextGenerationResponse.RequestId) 99 | ]); 100 | } 101 | 102 | [Fact] 103 | public async Task GetTextContentStream_OverrideModelId_SuccessAsync() 104 | { 105 | // Arrange 106 | var dashScopeClient = Substitute.For(); 107 | var list = new[] { Cases.TextGenerationResponse }; 108 | dashScopeClient.Configure() 109 | .GetTextCompletionStreamAsync(Arg.Any>()) 110 | .Returns(list.ToAsyncEnumerable()); 111 | var service = new DashScopeChatCompletionService( 112 | Cases.ModelId, 113 | dashScopeClient, 114 | NullLoggerFactory.Instance); 115 | var settings = new PromptExecutionSettings { ModelId = Cases.ModelIdAlter }; 116 | 117 | // Act 118 | _ = await service.GetStreamingTextContentsAsync(Cases.Prompt, settings).ToListAsync(); 119 | 120 | // Assert 121 | _ = dashScopeClient.Received().GetTextCompletionStreamAsync( 122 | Arg.Is>(x => x.Model == Cases.ModelIdAlter)); 123 | } 124 | 125 | public static TheoryData Settings 126 | => new() 127 | { 128 | null, 129 | new DashScopePromptExecutionSettings { Seed = 1000 }, 130 | new PromptExecutionSettings { ExtensionData = new Dictionary { { "seed", 1000 } } } 131 | }; 132 | } 133 | -------------------------------------------------------------------------------- /src/SemanticKernel.DashScope/ToolCallBehavior.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using Cnblogs.DashScope.Core; 3 | using Microsoft.SemanticKernel; 4 | 5 | namespace Cnblogs.SemanticKernel.Connectors.DashScope; 6 | 7 | /// 8 | /// Represents a behavior for DashScope tool calls. 9 | /// 10 | public abstract class ToolCallBehavior 11 | { 12 | /// 13 | /// The default maximum number of tool-call auto-invokes that can be made in a single request. 14 | /// 15 | /// 16 | /// After this number of iterations as part of a single user request is reached, auto-invocation 17 | /// will be disabled (e.g. will behave like )). 18 | /// This is a safeguard against possible runaway execution if the model routinely re-requests 19 | /// the same function over and over. It is currently hardcoded, but in the future it could 20 | /// be made configurable by the developer. Other configuration is also possible in the future, 21 | /// such as a delegate on the instance that can be invoked upon function call failure (e.g. failure 22 | /// to find the requested function, failure to invoke the function, etc.), with behaviors for 23 | /// what to do in such a case, e.g. respond to the model telling it to try again. With parallel tool call 24 | /// support, where the model can request multiple tools in a single response, it is significantly 25 | /// less likely that this limit is reached, as most of the time only a single request is needed. 26 | /// 27 | private const int DefaultMaximumAutoInvokeAttempts = 5; 28 | 29 | internal ToolCallBehavior(bool autoInvoke) 30 | { 31 | MaximumAutoInvokeAttempts = autoInvoke ? DefaultMaximumAutoInvokeAttempts : 0; 32 | } 33 | 34 | /// 35 | /// Options to control tool call result serialization behavior. 36 | /// 37 | public JsonSerializerOptions? ToolCallResultSerializerOptions { get; set; } 38 | 39 | internal int MaximumAutoInvokeAttempts { get; } 40 | 41 | /// Configures the with any tools this provides. 42 | /// The used for the operation. This can be queried to determine what tools to provide into the . 43 | /// The destination to configure. 44 | internal abstract void ConfigureOptions(Kernel? kernel, DashScopePromptExecutionSettings options); 45 | 46 | /// Gets an instance that will provide the specified list of functions to the model. 47 | /// The functions that should be made available to the model. 48 | /// true to attempt to automatically handle function call requests; otherwise, false. 49 | /// 50 | /// The that may be set into 51 | /// to indicate that the specified functions should be made available to the model. 52 | /// 53 | public static ToolCallBehavior EnableFunctions(IEnumerable functions, bool autoInvoke) 54 | { 55 | return new EnabledFunctions(functions, autoInvoke); 56 | } 57 | 58 | /// 59 | /// Gets an instance that will provide all of the 's plugins' function information. 60 | /// Function call requests from the model will be propagated back to the caller. 61 | /// 62 | /// 63 | /// If no is available, no function information will be provided to the model. 64 | /// 65 | public static ToolCallBehavior EnableKernelFunctions { get; } = new KernelFunctions(autoInvoke: false); 66 | 67 | /// 68 | /// Gets an instance that will both provide all of the 's plugins' function information 69 | /// to the model and attempt to automatically handle any function call requests. 70 | /// 71 | /// 72 | /// When successful, tool call requests from the model become an implementation detail, with the service 73 | /// handling invoking any requested functions and supplying the results back to the model. 74 | /// If no is available, no function information will be provided to the model. 75 | /// 76 | public static ToolCallBehavior AutoInvokeKernelFunctions { get; } = new KernelFunctions(autoInvoke: true); 77 | 78 | private sealed class EnabledFunctions(IEnumerable functions, bool autoInvoke = false) 79 | : ToolCallBehavior(autoInvoke) 80 | { 81 | /// 82 | internal override void ConfigureOptions(Kernel? kernel, DashScopePromptExecutionSettings options) 83 | { 84 | // If no kernel is provided, we don't have any tools to provide. 85 | if (kernel is null) 86 | { 87 | return; 88 | } 89 | 90 | // Provide all functions from the kernel. 91 | if (functions.Any()) 92 | { 93 | options.Tools = functions 94 | .Select(x => new ToolDefinition(ToolTypes.Function, x.ToFunctionDefinition())) 95 | .ToList(); 96 | } 97 | } 98 | } 99 | 100 | private sealed class KernelFunctions(bool autoInvoke = false) : ToolCallBehavior(autoInvoke) 101 | { 102 | /// 103 | internal override void ConfigureOptions(Kernel? kernel, DashScopePromptExecutionSettings options) 104 | { 105 | // If no kernel is provided, we don't have any tools to provide. 106 | if (kernel is null) 107 | { 108 | return; 109 | } 110 | 111 | // Provide all functions from the kernel. 112 | var functions = kernel.Plugins.GetFunctionsMetadata(); 113 | if (functions.Count > 0) 114 | { 115 | options.Tools = functions 116 | .Select(x => new ToolDefinition(ToolTypes.Function, x.ToFunctionDefinition())) 117 | .ToList(); 118 | } 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/KernelMemory.DashScope/DependencyInjector.cs: -------------------------------------------------------------------------------- 1 | using Cnblogs.DashScope.Core; 2 | using Cnblogs.KernelMemory.AI.DashScope; 3 | using Microsoft.Extensions.Configuration; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Microsoft.Extensions.Logging; 6 | using Microsoft.KernelMemory.AI; 7 | 8 | #pragma warning disable IDE0130 // reduce number of "using" statements 9 | // ReSharper disable once CheckNamespace - reduce number of "using" statements 10 | namespace Microsoft.KernelMemory; 11 | 12 | /// 13 | /// Helper methods for DI. 14 | /// 15 | public static class DependencyInjector 16 | { 17 | private const string DefaultTextModel = "qwen-max"; 18 | private const int DefaultTextModelMaxToken = 6000; 19 | 20 | private const string DefaultEmbeddingModel = "text-embedding-v3"; 21 | private const int DefaultEmbeddingModelMaxToken = 8192; 22 | 23 | /// 24 | /// Use default DashScope models (qwen-max and text-embedding-v2) and settings for ingestion and retrieval. 25 | /// 26 | /// Kernel Memory builder 27 | /// DashScope API Key 28 | /// Tokenizer used to count tokens used by prompts 29 | /// Tokenizer used to count tokens sent to the embedding generator 30 | /// Whether to use DashScope defaults only for ingestion, and not for retrieval (search and ask API) 31 | /// KM builder instance 32 | public static IKernelMemoryBuilder WithDashScopeDefaults( 33 | this IKernelMemoryBuilder builder, 34 | string apiKey, 35 | ITextTokenizer? textGenerationTokenizer = null, 36 | ITextTokenizer? textEmbeddingTokenizer = null, 37 | bool onlyForRetrieval = false) 38 | { 39 | textGenerationTokenizer ??= new QWenTextTokenizer(); 40 | textEmbeddingTokenizer ??= new LengthTokenizer(); 41 | 42 | var config = new DashScopeConfig 43 | { 44 | ChatCompletionModelId = DefaultTextModel, 45 | TextModelMaxTokenTotal = DefaultTextModelMaxToken, 46 | TextEmbeddingModelId = DefaultEmbeddingModel, 47 | EmbeddingModelMaxTokenTotal = DefaultEmbeddingModelMaxToken, 48 | ApiKey = apiKey, 49 | }; 50 | config.EnsureValid(); 51 | 52 | var client = new DashScopeClient(config.ApiKey); 53 | builder.WithDashScope(config, textEmbeddingTokenizer, textGenerationTokenizer, onlyForRetrieval, client); 54 | return builder; 55 | } 56 | 57 | /// 58 | /// Use DashScope models for ingestion and retrieval. 59 | /// 60 | /// The . 61 | /// Configuration root. 62 | /// Section name to bind from. 63 | /// Tokenizer used to count tokens used by embedding generator. 64 | /// Tokenizer used to count tokens used by prompts 65 | /// Whether to use DashScope only for ingestion, not for retrieval (search and ask API) 66 | /// The underlying . 67 | public static IKernelMemoryBuilder WithDashScope( 68 | this IKernelMemoryBuilder builder, 69 | IConfiguration configuration, 70 | string sectionName = "dashScope", 71 | ITextTokenizer? embeddingTokenizer = null, 72 | ITextTokenizer? textTokenizer = null, 73 | bool onlyForRetrieval = false, 74 | IDashScopeClient? dashScopeClient = null) 75 | { 76 | var config = configuration.GetConfig(sectionName); 77 | return builder.WithDashScope(config, embeddingTokenizer, textTokenizer, onlyForRetrieval, dashScopeClient); 78 | } 79 | 80 | /// 81 | /// Use DashScope models for ingestion and retrieval. 82 | /// 83 | /// The . 84 | /// Settings for DashScope. 85 | /// Tokenizer used to count tokens used by embedding generator. 86 | /// Tokenizer used to count tokens used by prompts 87 | /// Whether to use DashScope only for ingestion, not for retrieval (search and ask API) 88 | /// The underlying . 89 | /// 90 | public static IKernelMemoryBuilder WithDashScope( 91 | this IKernelMemoryBuilder builder, 92 | DashScopeConfig config, 93 | ITextTokenizer? embeddingTokenizer = null, 94 | ITextTokenizer? textTokenizer = null, 95 | bool onlyForRetrieval = false, 96 | IDashScopeClient? dashScopeClient = null) 97 | { 98 | config.EnsureValid(); 99 | embeddingTokenizer ??= new LengthTokenizer(); 100 | textTokenizer ??= new QWenTextTokenizer(); 101 | dashScopeClient ??= new DashScopeClient(config.ApiKey); 102 | builder.WithDashScopeTextGeneration(config, textTokenizer, dashScopeClient); 103 | builder.WithDashScopeTextEmbeddingGeneration(config, embeddingTokenizer, onlyForRetrieval, dashScopeClient); 104 | return builder; 105 | } 106 | 107 | /// 108 | /// Use DashScope models to generate text. 109 | /// 110 | /// The / 111 | /// DashScope settings. 112 | /// The tokenizer to use. 113 | /// Underlying . 114 | /// 115 | public static IKernelMemoryBuilder WithDashScopeTextGeneration( 116 | this IKernelMemoryBuilder builder, 117 | DashScopeConfig config, 118 | ITextTokenizer? tokenizer = null, 119 | IDashScopeClient? dashScopeClient = null) 120 | { 121 | config.EnsureValid(); 122 | tokenizer ??= new QWenTextTokenizer(); 123 | dashScopeClient ??= new DashScopeClient(config.ApiKey); 124 | builder.Services.AddDashScopeTextGeneration(config, tokenizer, dashScopeClient); 125 | return builder; 126 | } 127 | 128 | /// 129 | /// Use DashScope models to generate text embedding. 130 | /// 131 | /// The . 132 | /// DashScope settings. 133 | /// Tokenizer used to count tokens sent to the embedding generator 134 | /// Whether to use DashScope only for ingestion, not for retrieval (search and ask API) 135 | /// Underlying . 136 | /// 137 | public static IKernelMemoryBuilder WithDashScopeTextEmbeddingGeneration( 138 | this IKernelMemoryBuilder builder, 139 | DashScopeConfig config, 140 | ITextTokenizer? tokenizer = null, 141 | bool onlyForRetrieval = false, 142 | IDashScopeClient? dashScopeClient = null) 143 | { 144 | config.EnsureValid(); 145 | tokenizer ??= new LengthTokenizer(); 146 | dashScopeClient ??= new DashScopeClient(config.ApiKey); 147 | builder.Services.AddDashScopeTextEmbeddingGeneration(config, tokenizer, dashScopeClient); 148 | if (!onlyForRetrieval) 149 | { 150 | builder.AddIngestionEmbeddingGenerator( 151 | new DashScopeTextEmbeddingGenerator( 152 | dashScopeClient, 153 | config.TextEmbeddingModelId, 154 | tokenizer, 155 | config.EmbeddingModelMaxTokenTotal)); 156 | } 157 | 158 | return builder; 159 | } 160 | 161 | /// 162 | /// Implement with DashScope. 163 | /// 164 | /// The . 165 | /// Settings for DashScope. 166 | /// The tokenizer to use, defaults to . 167 | /// The underlying . 168 | /// 169 | public static IServiceCollection AddDashScopeTextEmbeddingGeneration( 170 | this IServiceCollection services, 171 | DashScopeConfig config, 172 | ITextTokenizer? tokenizer = null, 173 | IDashScopeClient? dashScopeClient = null) 174 | { 175 | config.EnsureValid(); 176 | tokenizer ??= new LengthTokenizer(); 177 | 178 | return services.AddSingleton( 179 | sp => new DashScopeTextEmbeddingGenerator( 180 | dashScopeClient ?? sp.GetRequiredService(), 181 | config.TextEmbeddingModelId, 182 | tokenizer, 183 | config.EmbeddingModelMaxTokenTotal)); 184 | } 185 | 186 | /// 187 | /// Implement with DashScope. 188 | /// 189 | /// The . 190 | /// Settings for DashScope. 191 | /// The tokenizer to use, defaults to . 192 | /// The underlying . 193 | /// 194 | public static IServiceCollection AddDashScopeTextGeneration( 195 | this IServiceCollection services, 196 | DashScopeConfig config, 197 | ITextTokenizer? tokenizer = null, 198 | IDashScopeClient? dashScopeClient = null) 199 | { 200 | config.EnsureValid(); 201 | tokenizer ??= new QWenTextTokenizer(); 202 | 203 | return services.AddSingleton( 204 | sp => new DashScopeTextGenerator( 205 | dashScopeClient ?? sp.GetRequiredService(), 206 | config.ChatCompletionModelId, 207 | sp.GetService(), 208 | tokenizer, 209 | config.TextModelMaxTokenTotal)); 210 | } 211 | 212 | private static DashScopeConfig GetConfig(this IConfiguration configuration, string sectionName) 213 | { 214 | return configuration.GetSection(sectionName).Get() 215 | ?? throw new InvalidOperationException( 216 | $"Can not resolve {nameof(DashScopeConfig)} from section: {sectionName}"); 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /src/SemanticKernel.DashScope/DashScopeChatCompletionService.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | using System.Text.Json; 3 | using Cnblogs.DashScope.Core; 4 | using Microsoft.Extensions.Logging; 5 | using Microsoft.Extensions.Logging.Abstractions; 6 | using Microsoft.SemanticKernel; 7 | using Microsoft.SemanticKernel.ChatCompletion; 8 | using Microsoft.SemanticKernel.Services; 9 | using Microsoft.SemanticKernel.TextGeneration; 10 | 11 | namespace Cnblogs.SemanticKernel.Connectors.DashScope; 12 | 13 | /// 14 | /// DashScope chat completion service. 15 | /// 16 | public sealed class DashScopeChatCompletionService : IChatCompletionService, ITextGenerationService 17 | { 18 | private readonly IDashScopeClient _dashScopeClient; 19 | private readonly Dictionary _attributes = new(); 20 | private readonly string _modelId; 21 | private readonly ILogger _logger; 22 | 23 | /// 24 | /// Creates a new DashScope chat completion service. 25 | /// 26 | /// 27 | /// 28 | /// 29 | public DashScopeChatCompletionService( 30 | string modelId, 31 | IDashScopeClient dashScopeClient, 32 | ILoggerFactory? loggerFactory = null) 33 | { 34 | _dashScopeClient = dashScopeClient; 35 | _modelId = modelId; 36 | _logger = loggerFactory != null 37 | ? loggerFactory.CreateLogger() 38 | : NullLogger.Instance; 39 | _attributes.Add(AIServiceExtensions.ModelIdKey, _modelId); 40 | } 41 | 42 | /// 43 | public async Task> GetChatMessageContentsAsync( 44 | ChatHistory chat, 45 | PromptExecutionSettings? executionSettings = null, 46 | Kernel? kernel = null, 47 | CancellationToken cancellationToken = default) 48 | { 49 | var chatParameters = DashScopePromptExecutionSettings.FromPromptExecutionSettings(executionSettings); 50 | chatParameters ??= new DashScopePromptExecutionSettings(); 51 | chatParameters.IncrementalOutput = false; 52 | chatParameters.ResultFormat = ResultFormats.Message; 53 | chatParameters.ToolCallBehavior?.ConfigureOptions(kernel, chatParameters); 54 | 55 | var autoInvoke = kernel is not null && chatParameters.ToolCallBehavior?.MaximumAutoInvokeAttempts > 0; 56 | for (var it = 1; ; it++) 57 | { 58 | var chatParametersTools = chatParameters.Tools?.ToList(); 59 | var response = await _dashScopeClient.GetTextCompletionAsync( 60 | new ModelRequest 61 | { 62 | Input = new TextGenerationInput { Messages = chat.ToChatMessages() }, 63 | Model = string.IsNullOrEmpty(chatParameters.ModelId) ? _modelId : chatParameters.ModelId, 64 | Parameters = chatParameters 65 | }, 66 | cancellationToken); 67 | CaptureTokenUsage(response.Usage); 68 | EnsureChoiceExists(response.Output.Choices); 69 | var message = response.Output.Choices![0].Message; 70 | var chatMessageContent = new DashScopeChatMessageContent( 71 | new AuthorRole(message.Role), 72 | message.Content, 73 | name: null, 74 | toolCalls: message.ToolCalls, 75 | metadata: response.ToMetaData()); 76 | if (autoInvoke == false || message.ToolCalls is null) 77 | { 78 | // no needs to invoke tool 79 | return [chatMessageContent]; 80 | } 81 | 82 | LogToolCalls(message.ToolCalls); 83 | chat.Add(chatMessageContent); 84 | 85 | foreach (var call in message.ToolCalls) 86 | { 87 | if (call.Type is not ToolTypes.Function) 88 | { 89 | AddResponseMessage(chat, null, "Error: Tool call was not a function call.", call.Id); 90 | continue; 91 | } 92 | 93 | // ensure not calling function that was not included in request list. 94 | if (chatParametersTools?.Any( 95 | x => string.Equals(x.Function?.Name, call.Function.Name, StringComparison.OrdinalIgnoreCase)) 96 | != true) 97 | { 98 | AddResponseMessage( 99 | chat, 100 | null, 101 | "Error: Function call requests for a function that wasn't defined.", 102 | call.Id); 103 | continue; 104 | } 105 | 106 | object? callResult; 107 | try 108 | { 109 | if (kernel!.Plugins.TryGetKernelFunctionAndArguments( 110 | call.Function, 111 | out var kernelFunction, 112 | out var kernelArguments) 113 | == false) 114 | { 115 | AddResponseMessage(chat, null, "Error: Requested function could not be found.", call.Id); 116 | continue; 117 | } 118 | 119 | var functionResult = await kernelFunction.InvokeAsync(kernel, kernelArguments, cancellationToken); 120 | callResult = functionResult.GetValue() ?? string.Empty; 121 | } 122 | catch (JsonException) 123 | { 124 | AddResponseMessage(chat, null, "Error: Function call arguments were invalid JSON.", call.Id); 125 | continue; 126 | } 127 | catch (Exception) 128 | { 129 | AddResponseMessage(chat, null, "Error: Exception while invoking function. {e.Message}", call.Id); 130 | continue; 131 | } 132 | 133 | var stringResult = ProcessFunctionResult(callResult, chatParameters.ToolCallBehavior); 134 | AddResponseMessage(chat, stringResult, null, call.Id); 135 | } 136 | 137 | chatParameters.Tools = []; 138 | chatParameters.ToolCallBehavior?.ConfigureOptions(kernel, chatParameters); 139 | if (it >= chatParameters.ToolCallBehavior!.MaximumAutoInvokeAttempts) 140 | { 141 | autoInvoke = false; 142 | if (_logger.IsEnabled(LogLevel.Debug)) 143 | { 144 | _logger.LogDebug( 145 | "Maximum auto-invoke ({MaximumAutoInvoke}) reached", 146 | chatParameters.ToolCallBehavior!.MaximumAutoInvokeAttempts); 147 | } 148 | } 149 | } 150 | } 151 | 152 | /// 153 | public async IAsyncEnumerable GetStreamingChatMessageContentsAsync( 154 | ChatHistory chatHistory, 155 | PromptExecutionSettings? executionSettings = null, 156 | Kernel? kernel = null, 157 | [EnumeratorCancellation] CancellationToken cancellationToken = default) 158 | { 159 | var chatMessages = chatHistory.ToChatMessages(); 160 | executionSettings ??= new DashScopePromptExecutionSettings(); 161 | var parameters = DashScopePromptExecutionSettings.FromPromptExecutionSettings(executionSettings); 162 | parameters.IncrementalOutput = true; 163 | parameters.ResultFormat = ResultFormats.Message; 164 | parameters.ToolCallBehavior?.ConfigureOptions(kernel, parameters); 165 | var responses = _dashScopeClient.GetTextCompletionStreamAsync( 166 | new ModelRequest 167 | { 168 | Input = new TextGenerationInput { Messages = chatMessages }, 169 | Model = string.IsNullOrEmpty(parameters.ModelId) ? _modelId : parameters.ModelId, 170 | Parameters = parameters 171 | }, 172 | cancellationToken); 173 | 174 | await foreach (var response in responses) 175 | { 176 | var message = response.Output.Choices![0].Message; 177 | yield return new StreamingChatMessageContent( 178 | new AuthorRole(message.Role), 179 | message.Content, 180 | modelId: _modelId, 181 | metadata: response.ToMetaData()); 182 | } 183 | } 184 | 185 | /// 186 | public IReadOnlyDictionary Attributes => _attributes; 187 | 188 | /// 189 | public async Task> GetTextContentsAsync( 190 | string prompt, 191 | PromptExecutionSettings? executionSettings = null, 192 | Kernel? kernel = null, 193 | CancellationToken cancellationToken = new()) 194 | { 195 | var chatParameters = DashScopePromptExecutionSettings.FromPromptExecutionSettings(executionSettings); 196 | chatParameters ??= new DashScopePromptExecutionSettings(); 197 | chatParameters.IncrementalOutput = false; 198 | chatParameters.ResultFormat = ResultFormats.Text; 199 | var response = await _dashScopeClient.GetTextCompletionAsync( 200 | new ModelRequest 201 | { 202 | Input = new TextGenerationInput { Prompt = prompt }, 203 | Model = string.IsNullOrEmpty(chatParameters.ModelId) ? _modelId : chatParameters.ModelId, 204 | Parameters = chatParameters 205 | }, 206 | cancellationToken); 207 | return [new TextContent(response.Output.Text, _modelId, metadata: response.ToMetaData())]; 208 | } 209 | 210 | /// 211 | public async IAsyncEnumerable GetStreamingTextContentsAsync( 212 | string prompt, 213 | PromptExecutionSettings? executionSettings = null, 214 | Kernel? kernel = null, 215 | [EnumeratorCancellation] CancellationToken cancellationToken = new()) 216 | { 217 | executionSettings ??= new DashScopePromptExecutionSettings(); 218 | var parameters = DashScopePromptExecutionSettings.FromPromptExecutionSettings(executionSettings); 219 | parameters.IncrementalOutput = true; 220 | parameters.ResultFormat = ResultFormats.Text; 221 | var responses = _dashScopeClient.GetTextCompletionStreamAsync( 222 | new ModelRequest 223 | { 224 | Input = new TextGenerationInput { Prompt = prompt }, 225 | Model = string.IsNullOrEmpty(parameters.ModelId) ? _modelId : parameters.ModelId, 226 | Parameters = parameters 227 | }, 228 | cancellationToken); 229 | 230 | await foreach (var response in responses) 231 | { 232 | yield return new StreamingTextContent( 233 | response.Output.Text, 234 | modelId: string.IsNullOrEmpty(parameters.ModelId) ? _modelId : parameters.ModelId, 235 | metadata: response.ToMetaData()); 236 | } 237 | } 238 | 239 | private void CaptureTokenUsage(TextGenerationTokenUsage? usage) 240 | { 241 | if (usage is null) 242 | { 243 | if (_logger.IsEnabled(LogLevel.Debug)) 244 | { 245 | _logger.LogDebug("Usage info is not available"); 246 | } 247 | 248 | return; 249 | } 250 | 251 | if (_logger.IsEnabled(LogLevel.Information)) 252 | { 253 | _logger.LogInformation( 254 | "Input tokens: {InputTokens}. Output tokens: {CompletionTokens}. Total tokens: {TotalTokens}", 255 | usage.InputTokens, 256 | usage.OutputTokens, 257 | usage.TotalTokens); 258 | } 259 | } 260 | 261 | private void LogToolCalls(IReadOnlyCollection? calls) 262 | { 263 | if (calls is null) 264 | { 265 | return; 266 | } 267 | 268 | if (_logger.IsEnabled(LogLevel.Debug)) 269 | { 270 | _logger.LogDebug("Tool requests: {Requests}", calls.Count); 271 | } 272 | 273 | if (_logger.IsEnabled(LogLevel.Trace)) 274 | { 275 | _logger.LogTrace( 276 | "Function call requests: {Requests}", 277 | string.Join(", ", calls.Select(ftc => $"{ftc.Function.Name}({ftc.Function.Arguments})"))); 278 | } 279 | } 280 | 281 | private void AddResponseMessage(ChatHistory chat, string? result, string? errorMessage, string? toolId) 282 | { 283 | // Log any error 284 | if (errorMessage is not null && _logger.IsEnabled(LogLevel.Debug)) 285 | { 286 | _logger.LogDebug("Failed to handle tool request ({ToolId}). {Error}", toolId, errorMessage); 287 | } 288 | 289 | // Add the tool response message to both the chat options and to the chat history. 290 | result ??= errorMessage ?? string.Empty; 291 | chat.Add(new DashScopeChatMessageContent(AuthorRole.Tool, result, name: toolId)); 292 | } 293 | 294 | private static void EnsureChoiceExists(List? choices) 295 | { 296 | if (choices is null || choices.Count == 0) 297 | { 298 | throw new KernelException("No choice was returned from model"); 299 | } 300 | } 301 | 302 | private static string ProcessFunctionResult(object functionResult, ToolCallBehavior? toolCallBehavior) 303 | { 304 | if (functionResult is string stringResult) 305 | { 306 | return stringResult; 307 | } 308 | 309 | // This is an optimization to use ChatMessageContent content directly 310 | // without unnecessary serialization of the whole message content class. 311 | if (functionResult is ChatMessageContent chatMessageContent) 312 | { 313 | return chatMessageContent.ToString(); 314 | } 315 | 316 | // For polymorphic serialization of unknown in advance child classes of the KernelContent class, 317 | // a corresponding JsonTypeInfoResolver should be provided via the JsonSerializerOptions.TypeInfoResolver property. 318 | // For more details about the polymorphic serialization, see the article at: 319 | // https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/polymorphism?pivots=dotnet-8-0 320 | return JsonSerializer.Serialize(functionResult, toolCallBehavior?.ToolCallResultSerializerOptions); 321 | } 322 | } 323 | -------------------------------------------------------------------------------- /test/SemanticKernel.DashScope.UnitTest/ChatCompletionTests.cs: -------------------------------------------------------------------------------- 1 | using Cnblogs.DashScope.Core; 2 | using Cnblogs.SemanticKernel.Connectors.DashScope; 3 | using FluentAssertions; 4 | using Microsoft.Extensions.Logging.Abstractions; 5 | using Microsoft.SemanticKernel; 6 | using Microsoft.SemanticKernel.ChatCompletion; 7 | using NSubstitute; 8 | using NSubstitute.Extensions; 9 | 10 | namespace SemanticKernel.DashScope.UnitTest; 11 | 12 | public class ChatCompletionTests 13 | { 14 | [Theory] 15 | [MemberData(nameof(Settings))] 16 | public async Task ChatCompletion_Normal_SuccessAsync(PromptExecutionSettings? settings) 17 | { 18 | // Arrange 19 | var dashScopeClient = Substitute.For(); 20 | dashScopeClient.Configure() 21 | .GetTextCompletionAsync(Arg.Any>()) 22 | .Returns(Task.FromResult(Cases.ChatGenerationResponse)); 23 | var service = new DashScopeChatCompletionService( 24 | Cases.ModelId, 25 | dashScopeClient, 26 | NullLoggerFactory.Instance); 27 | 28 | // Act 29 | var response = await service.GetChatMessageContentsAsync(Cases.ChatHistory, settings); 30 | 31 | // Assert 32 | await dashScopeClient.Received().GetTextCompletionAsync( 33 | Arg.Is>( 34 | x => x.Parameters != null 35 | && x.Parameters.Seed == (settings == null ? null : 1000) 36 | && x.Parameters.IncrementalOutput == false 37 | && x.Parameters.ResultFormat == ResultFormats.Message)); 38 | response.Should().BeEquivalentTo([new { Cases.ChatGenerationResponse.Output.Choices![0].Message.Content }]); 39 | response[0].Metadata.Should() 40 | .Contain( 41 | [ 42 | new KeyValuePair("Usage", Cases.ChatGenerationResponse.Usage), 43 | new KeyValuePair("RequestId", Cases.ChatGenerationResponse.RequestId) 44 | ]); 45 | } 46 | 47 | [Fact] 48 | public async Task ChatCompletion_ToolCalling_SuccessAsync() 49 | { 50 | // Arrange 51 | var functionCallCount = 0; 52 | var kernel = Kernel.CreateBuilder().Build(); 53 | var function = Cases.NormalFunction(() => functionCallCount++); 54 | kernel.Plugins.Add(Cases.Plugin(function)); 55 | var dashScopeClient = Substitute.For(); 56 | dashScopeClient.Configure() 57 | .GetTextCompletionAsync(Arg.Any>()) 58 | .Returns(Task.FromResult(Cases.ToolCallResponse(function)), Task.FromResult(Cases.ChatGenerationResponse)); 59 | var service = new DashScopeChatCompletionService( 60 | Cases.ModelId, 61 | dashScopeClient, 62 | NullLoggerFactory.Instance); 63 | var settings = 64 | new DashScopePromptExecutionSettings { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; 65 | var history = new ChatHistory(); 66 | 67 | // Act 68 | var response = await service.GetChatMessageContentsAsync(history, settings, kernel); 69 | 70 | // Assert 71 | functionCallCount.Should().Be(1); 72 | response.Should().HaveCount(1); // model response 73 | history.Should().HaveCount(2); // tool response + model response 74 | } 75 | 76 | [Fact] 77 | public async Task ChatCompletion_MaximumToolCallingCount_SuccessAsync() 78 | { 79 | // Arrange 80 | const int maximumAutoInvokeTime = 5; 81 | const int autoInvokeResponsesCount = 6; 82 | var functionCallCount = 0; 83 | var kernel = Kernel.CreateBuilder().Build(); 84 | var function = Cases.NormalFunction(() => functionCallCount++); 85 | kernel.Plugins.Add(Cases.Plugin(function)); 86 | var dashScopeClient = Substitute.For(); 87 | dashScopeClient.Configure() 88 | .GetTextCompletionAsync(Arg.Any>()) 89 | .Returns( 90 | Task.FromResult(Cases.ToolCallResponse(function)), 91 | Enumerable.Range(0, autoInvokeResponsesCount - 1) 92 | .Select(_ => Task.FromResult(Cases.ToolCallResponse(function))).ToArray()); 93 | var service = new DashScopeChatCompletionService( 94 | Cases.ModelId, 95 | dashScopeClient, 96 | NullLoggerFactory.Instance); 97 | var settings = 98 | new DashScopePromptExecutionSettings { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; 99 | var history = new ChatHistory(); 100 | 101 | // Act 102 | _ = await service.GetChatMessageContentsAsync(history, settings, kernel); 103 | 104 | // Assert 105 | functionCallCount.Should().Be(maximumAutoInvokeTime, "tool can only be invoked below maximum auto invoke time"); 106 | } 107 | 108 | [Fact] 109 | public async Task ChatCompletion_ToolTypeIsNotFunction_SkipAsync() 110 | { 111 | // Arrange 112 | const string nonFunctionToolType = "search"; 113 | var functionCallCount = 0; 114 | var kernel = Kernel.CreateBuilder().Build(); 115 | var function = Cases.NormalFunction(() => functionCallCount++); 116 | kernel.Plugins.Add(Cases.Plugin(function)); 117 | var dashScopeClient = Substitute.For(); 118 | dashScopeClient.Configure() 119 | .GetTextCompletionAsync(Arg.Any>()) 120 | .Returns( 121 | Task.FromResult(Cases.ErrToolCallResponse([function], toolType: nonFunctionToolType)), 122 | Task.FromResult(Cases.ChatGenerationResponse)); 123 | var service = new DashScopeChatCompletionService( 124 | Cases.ModelId, 125 | dashScopeClient, 126 | NullLoggerFactory.Instance); 127 | var settings = 128 | new DashScopePromptExecutionSettings { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; 129 | var history = new ChatHistory(); 130 | 131 | // Act 132 | _ = await service.GetChatMessageContentsAsync(history, settings, kernel); 133 | 134 | // Assert 135 | functionCallCount.Should().Be(0, "Tool type can only be function"); 136 | } 137 | 138 | [Fact] 139 | public async Task ChatCompletion_FunctionCallWithMalformedJson_SkipAsync() 140 | { 141 | // Arrange 142 | const string malFormedJson = "invalid json"; 143 | var functionCallCount = 0; 144 | var kernel = Kernel.CreateBuilder().Build(); 145 | var function = Cases.NormalFunction(() => functionCallCount++); 146 | kernel.Plugins.Add(Cases.Plugin(function)); 147 | var dashScopeClient = Substitute.For(); 148 | dashScopeClient.Configure() 149 | .GetTextCompletionAsync(Arg.Any>()) 150 | .Returns( 151 | Task.FromResult(Cases.ErrToolCallResponse([function], paramBody: malFormedJson)), 152 | Task.FromResult(Cases.ChatGenerationResponse)); 153 | var service = new DashScopeChatCompletionService( 154 | Cases.ModelId, 155 | dashScopeClient, 156 | NullLoggerFactory.Instance); 157 | var settings = 158 | new DashScopePromptExecutionSettings { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; 159 | var history = new ChatHistory(); 160 | 161 | // Act 162 | _ = await service.GetChatMessageContentsAsync(history, settings, kernel); 163 | 164 | // Assert 165 | functionCallCount.Should().Be(0, "malformed json should be skipped"); 166 | history.Should().HaveCount(2, "error message should be added to chat history"); 167 | } 168 | 169 | [Fact] 170 | public async Task ChatCompletion_FunctionThrowException_SkipAsync() 171 | { 172 | // Arrange 173 | var functionCallCount = 0; 174 | var kernel = Kernel.CreateBuilder().Build(); 175 | var function1 = Cases.NormalFunction(() => throw new InvalidOperationException()); 176 | var function2 = Cases.AlterFunction(() => functionCallCount++); 177 | kernel.Plugins.Add(Cases.Plugin(function1, function2)); 178 | var dashScopeClient = Substitute.For(); 179 | dashScopeClient.Configure() 180 | .GetTextCompletionAsync(Arg.Any>()) 181 | .Returns( 182 | Task.FromResult(Cases.ToolCallResponse(function1, function2)), 183 | Task.FromResult(Cases.ChatGenerationResponse)); 184 | var service = new DashScopeChatCompletionService( 185 | Cases.ModelId, 186 | dashScopeClient, 187 | NullLoggerFactory.Instance); 188 | var settings = 189 | new DashScopePromptExecutionSettings { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; 190 | var history = new ChatHistory(); 191 | 192 | // Act 193 | _ = await service.GetChatMessageContentsAsync(history, settings, kernel); 194 | 195 | // Assert 196 | functionCallCount.Should().Be(1, "interrupted function call should be skipped"); 197 | history.Should().HaveCount(3, "interrupted function call error message should be added to chat history"); 198 | } 199 | 200 | [Fact] 201 | public async Task ChatCompletion_FunctionDoesNotExists_SkipAsync() 202 | { 203 | // Arrange 204 | var functionCallCount = 0; 205 | var kernel = Kernel.CreateBuilder().Build(); 206 | var function = Cases.NormalFunction(() => functionCallCount++); 207 | var plugin = Cases.Plugin(function); 208 | 209 | // not adds function to kernel 210 | // kernel.Plugins.Add(plugin); 211 | var dashScopeClient = Substitute.For(); 212 | dashScopeClient.Configure() 213 | .GetTextCompletionAsync(Arg.Any>()) 214 | .Returns( 215 | Task.FromResult(Cases.ToolCallResponse(function)), 216 | Task.FromResult(Cases.ChatGenerationResponse)); 217 | var service = new DashScopeChatCompletionService( 218 | Cases.ModelId, 219 | dashScopeClient, 220 | NullLoggerFactory.Instance); 221 | var settings = 222 | new DashScopePromptExecutionSettings 223 | { 224 | ToolCallBehavior = ToolCallBehavior.EnableFunctions(plugin.GetFunctionsMetadata(), autoInvoke: true) 225 | }; 226 | var history = new ChatHistory(); 227 | 228 | // Act 229 | _ = await service.GetChatMessageContentsAsync(history, settings, kernel); 230 | 231 | // Assert 232 | functionCallCount.Should().Be(0, "Should not call function that not exists in kernel"); 233 | } 234 | 235 | [Fact] 236 | public async Task ChatCompletion_CallingNotProvidedFunction_SkipAsync() 237 | { 238 | // Arrange 239 | var function1CallCount = 0; 240 | var function2CallCount = 0; 241 | var kernel = Kernel.CreateBuilder().Build(); 242 | var function1 = Cases.NormalFunction(() => function1CallCount++); 243 | var function2 = Cases.AlterFunction(() => function2CallCount++); 244 | kernel.Plugins.Add(Cases.Plugin(function1, function2)); 245 | 246 | var responseCallingFunction2 = Cases.ToolCallResponse(function2); 247 | var dashScopeClient = Substitute.For(); 248 | dashScopeClient.Configure() 249 | .GetTextCompletionAsync(Arg.Any>()) 250 | .Returns(Task.FromResult(responseCallingFunction2)); 251 | var service = new DashScopeChatCompletionService( 252 | Cases.ModelId, 253 | dashScopeClient, 254 | NullLoggerFactory.Instance); 255 | var settings = 256 | new DashScopePromptExecutionSettings 257 | { 258 | ToolCallBehavior = ToolCallBehavior.EnableFunctions([function1.Metadata], autoInvoke: true) 259 | }; 260 | var history = new ChatHistory(); 261 | 262 | // Act 263 | _ = await service.GetChatMessageContentsAsync(history, settings, kernel); 264 | 265 | // Assert 266 | function1CallCount.Should().Be(0, "can not invoke tools that was not provided in request"); 267 | function2CallCount.Should().Be(0, "tools that not presented in response should not be called"); 268 | } 269 | 270 | [Fact] 271 | public async Task ChatCompletion_CustomModel_SuccessAsync() 272 | { 273 | // Arrange 274 | var dashScopeClient = Substitute.For(); 275 | dashScopeClient.Configure() 276 | .GetTextCompletionAsync(Arg.Any>()) 277 | .Returns(Task.FromResult(Cases.ChatGenerationResponse)); 278 | var service = new DashScopeChatCompletionService( 279 | Cases.ModelId, 280 | dashScopeClient, 281 | NullLoggerFactory.Instance); 282 | var settings = new DashScopePromptExecutionSettings { ModelId = Cases.ModelIdAlter }; 283 | 284 | // Act 285 | _ = await service.GetChatMessageContentsAsync(Cases.ChatHistory, settings); 286 | 287 | // Assert 288 | await dashScopeClient.Received().GetTextCompletionAsync( 289 | Arg.Is>(x => x.Model == Cases.ModelIdAlter)); 290 | } 291 | 292 | [Theory] 293 | [MemberData(nameof(Settings))] 294 | public async Task ChatCompletionStream_Normal_SuccessAsync(PromptExecutionSettings? settings) 295 | { 296 | // Arrange 297 | var dashScopeClient = Substitute.For(); 298 | var list = new[] { Cases.ChatGenerationResponse }; 299 | dashScopeClient.Configure() 300 | .GetTextCompletionStreamAsync(Arg.Any>()) 301 | .Returns(list.ToAsyncEnumerable()); 302 | var service = new DashScopeChatCompletionService( 303 | Cases.ModelId, 304 | dashScopeClient, 305 | NullLoggerFactory.Instance); 306 | 307 | // Act 308 | var response = await service.GetStreamingChatMessageContentsAsync(Cases.ChatHistory, settings).ToListAsync(); 309 | 310 | // Assert 311 | _ = dashScopeClient.Received().GetTextCompletionStreamAsync( 312 | Arg.Is>( 313 | x => x.Parameters != null 314 | && x.Parameters.Seed == (settings == null ? null : 1000) 315 | && x.Parameters.IncrementalOutput == true 316 | && x.Parameters.ResultFormat == ResultFormats.Message)); 317 | response.Should().BeEquivalentTo([new { Cases.ChatGenerationResponse.Output.Choices![0].Message.Content }]); 318 | response[0].Metadata.Should() 319 | .Contain( 320 | [ 321 | new KeyValuePair("Usage", Cases.ChatGenerationResponse.Usage), 322 | new KeyValuePair("RequestId", Cases.ChatGenerationResponse.RequestId) 323 | ]); 324 | } 325 | 326 | [Fact] 327 | public async Task ChatCompletionStream_CustomModel_SuccessAsync() 328 | { 329 | // Arrange 330 | var dashScopeClient = Substitute.For(); 331 | var list = new[] { Cases.ChatGenerationResponse }; 332 | dashScopeClient.Configure() 333 | .GetTextCompletionStreamAsync(Arg.Any>()) 334 | .Returns(list.ToAsyncEnumerable()); 335 | var service = new DashScopeChatCompletionService( 336 | Cases.ModelId, 337 | dashScopeClient, 338 | NullLoggerFactory.Instance); 339 | var settings = new DashScopePromptExecutionSettings { ModelId = Cases.ModelIdAlter }; 340 | 341 | // Act 342 | _ = await service.GetStreamingChatMessageContentsAsync(Cases.ChatHistory, settings).ToListAsync(); 343 | 344 | // Assert 345 | _ = dashScopeClient.Received().GetTextCompletionStreamAsync( 346 | Arg.Is>(x => x.Model == Cases.ModelIdAlter)); 347 | } 348 | 349 | public static TheoryData Settings 350 | => new() 351 | { 352 | null, 353 | new DashScopePromptExecutionSettings { Seed = 1000 }, 354 | new PromptExecutionSettings { ExtensionData = new Dictionary { { "seed", 1000 } } } 355 | }; 356 | } 357 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | trim_trailing_whitespace = true 7 | insert_final_newline = true 8 | indent_style = space 9 | indent_size = 4 10 | 11 | [*.cs] 12 | 13 | # Microsoft .NET properties 14 | csharp_style_var_elsewhere = true:suggestion 15 | csharp_style_var_for_built_in_types = true:suggestion 16 | csharp_style_var_when_type_is_apparent = true:suggestion 17 | dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary:none 18 | dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:none 19 | dotnet_style_parentheses_in_relational_binary_operators = never_if_unnecessary:none 20 | dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion 21 | dotnet_style_predefined_type_for_member_access = true:suggestion 22 | dotnet_style_qualification_for_event = false:suggestion 23 | dotnet_style_qualification_for_field = false:suggestion 24 | dotnet_style_qualification_for_method = false:suggestion 25 | dotnet_style_qualification_for_property = false:suggestion 26 | dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion 27 | dotnet_sort_system_directives_first = true 28 | dotnet_style_readonly_field = true:suggestion 29 | 30 | # Expression-level preferences 31 | dotnet_style_object_initializer = true:suggestion 32 | dotnet_style_collection_initializer = true:suggestion 33 | dotnet_style_explicit_tuple_names = true:suggestion 34 | dotnet_style_coalesce_expression = true:suggestion 35 | dotnet_style_null_propagation = true:suggestion 36 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion 37 | dotnet_style_prefer_inferred_tuple_names = true:suggestion 38 | dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion 39 | dotnet_style_prefer_auto_properties = true:suggestion 40 | dotnet_style_prefer_conditional_expression_over_assignment = true:silent 41 | dotnet_style_prefer_conditional_expression_over_return = true:silent 42 | csharp_prefer_simple_default_expression = true:suggestion 43 | 44 | # Expression-bodied members 45 | csharp_style_expression_bodied_properties = true:silent 46 | csharp_style_expression_bodied_indexers = true:silent 47 | csharp_style_expression_bodied_accessors = true:silent 48 | csharp_style_expression_bodied_constructors = false:silent 49 | csharp_style_expression_bodied_lambdas = true:suggestion 50 | csharp_style_expression_bodied_local_functions = false:silent 51 | csharp_style_expression_bodied_methods = false:silent 52 | csharp_style_expression_bodied_operators = false:silent 53 | 54 | # Pattern matching 55 | csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion 56 | csharp_style_pattern_matching_over_as_with_null_check = true:suggestion 57 | csharp_style_inlined_variable_declaration = true:suggestion 58 | 59 | # Null checking preferences 60 | csharp_style_throw_expression = true:suggestion 61 | csharp_style_conditional_delegate_call = true:suggestion 62 | 63 | # Space preferences 64 | csharp_space_after_cast = false 65 | csharp_space_after_colon_in_inheritance_clause = true 66 | csharp_space_after_comma = true 67 | csharp_space_after_dot = false 68 | csharp_space_after_keywords_in_control_flow_statements = true 69 | csharp_space_after_semicolon_in_for_statement = true 70 | csharp_space_around_binary_operators = before_and_after 71 | csharp_space_around_declaration_statements = false 72 | csharp_space_before_colon_in_inheritance_clause = true 73 | csharp_space_before_comma = false 74 | csharp_space_before_dot = false 75 | csharp_space_before_open_square_brackets = false 76 | csharp_space_before_semicolon_in_for_statement = false 77 | csharp_space_between_empty_square_brackets = false 78 | csharp_space_between_method_call_empty_parameter_list_parentheses = false 79 | csharp_space_between_method_call_name_and_opening_parenthesis = false 80 | csharp_space_between_method_call_parameter_list_parentheses = false 81 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false 82 | csharp_space_between_method_declaration_name_and_open_parenthesis = false 83 | csharp_space_between_method_declaration_parameter_list_parentheses = false 84 | csharp_space_between_parentheses = false 85 | csharp_space_between_square_brackets = false 86 | 87 | # ReSharper properties 88 | resharper_braces_for_for = required 89 | resharper_braces_for_foreach = required 90 | resharper_braces_for_ifelse = required 91 | resharper_braces_for_while = required 92 | resharper_csharp_keep_blank_lines_in_code = 1 93 | resharper_csharp_keep_blank_lines_in_declarations = 1 94 | resharper_csharp_wrap_after_declaration_lpar = true 95 | resharper_csharp_wrap_after_invocation_lpar = true 96 | resharper_csharp_wrap_arguments_style = chop_if_long 97 | resharper_csharp_wrap_before_binary_opsign = true 98 | resharper_csharp_wrap_before_first_type_parameter_constraint = true 99 | resharper_csharp_wrap_parameters_style = chop_if_long 100 | resharper_keep_existing_attribute_arrangement = true 101 | resharper_keep_existing_declaration_parens_arrangement = false 102 | resharper_keep_existing_embedded_arrangement = false 103 | resharper_keep_existing_expr_member_arrangement = false 104 | resharper_keep_existing_initializer_arrangement = false 105 | resharper_keep_existing_invocation_parens_arrangement = false 106 | resharper_keep_existing_switch_expression_arrangement = false 107 | resharper_max_initializer_elements_on_line = 2 108 | resharper_place_accessorholder_attribute_on_same_line = false 109 | resharper_place_field_attribute_on_same_line = false 110 | resharper_place_linq_into_on_new_line = false 111 | resharper_place_simple_anonymousmethod_on_single_line = false 112 | resharper_place_simple_embedded_statement_on_same_line = false 113 | resharper_space_between_attribute_sections = false 114 | resharper_wrap_before_arrow_with_expressions = true 115 | resharper_wrap_before_extends_colon = true 116 | resharper_wrap_before_linq_expression = true 117 | resharper_wrap_chained_binary_expressions = chop_if_long 118 | 119 | # Pattern matching preferences 120 | csharp_style_prefer_not_pattern = true:suggestion 121 | csharp_style_prefer_pattern_matching = true:silent 122 | csharp_style_prefer_switch_expression = true:suggestion 123 | 124 | # Modifier preferences 125 | csharp_prefer_static_local_function = true:suggestion 126 | csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:silent 127 | 128 | # Code-block preferences 129 | csharp_prefer_braces = true:silent 130 | csharp_prefer_simple_using_statement = false:none 131 | 132 | # Expression-level preferences 133 | csharp_style_deconstructed_variable_declaration = true:suggestion 134 | csharp_style_pattern_local_over_anonymous_function = true:suggestion 135 | csharp_style_prefer_index_operator = true:suggestion 136 | csharp_style_prefer_range_operator = true:suggestion 137 | csharp_style_unused_value_assignment_preference = discard_variable:suggestion 138 | csharp_style_unused_value_expression_statement_preference = discard_variable:silent 139 | 140 | # 'using' directive preferences 141 | csharp_using_directive_placement = outside_namespace:silent 142 | 143 | #### C# Formatting Rules #### 144 | 145 | # New line preferences 146 | csharp_new_line_before_catch = true 147 | csharp_new_line_before_else = true 148 | csharp_new_line_before_finally = true 149 | csharp_new_line_before_members_in_anonymous_types = true 150 | csharp_new_line_before_members_in_object_initializers = true 151 | csharp_new_line_before_open_brace = all 152 | csharp_new_line_between_query_expression_clauses = true 153 | 154 | # Indentation preferences 155 | csharp_indent_block_contents = true 156 | csharp_indent_braces = false 157 | csharp_indent_case_contents = true 158 | csharp_indent_case_contents_when_block = true 159 | csharp_indent_labels = one_less_than_current 160 | csharp_indent_switch_labels = true 161 | 162 | # Wrapping preferences 163 | csharp_preserve_single_line_blocks = true 164 | csharp_preserve_single_line_statements = false 165 | 166 | #### Naming styles #### 167 | 168 | # Naming rules 169 | 170 | dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.severity = suggestion 171 | dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.symbols = types_and_namespaces 172 | dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.style = pascalcase 173 | 174 | dotnet_naming_rule.interfaces_should_be_ipascalcase.severity = suggestion 175 | dotnet_naming_rule.interfaces_should_be_ipascalcase.symbols = interfaces 176 | dotnet_naming_rule.interfaces_should_be_ipascalcase.style = ipascalcase 177 | 178 | dotnet_naming_rule.type_parameters_should_be_tpascalcase.severity = suggestion 179 | dotnet_naming_rule.type_parameters_should_be_tpascalcase.symbols = type_parameters 180 | dotnet_naming_rule.type_parameters_should_be_tpascalcase.style = tpascalcase 181 | 182 | dotnet_naming_rule.methods_should_be_pascalcase.severity = suggestion 183 | dotnet_naming_rule.methods_should_be_pascalcase.symbols = methods 184 | dotnet_naming_rule.methods_should_be_pascalcase.style = pascalcase 185 | 186 | dotnet_naming_rule.properties_should_be_pascalcase.severity = suggestion 187 | dotnet_naming_rule.properties_should_be_pascalcase.symbols = properties 188 | dotnet_naming_rule.properties_should_be_pascalcase.style = pascalcase 189 | 190 | dotnet_naming_rule.events_should_be_pascalcase.severity = suggestion 191 | dotnet_naming_rule.events_should_be_pascalcase.symbols = events 192 | dotnet_naming_rule.events_should_be_pascalcase.style = pascalcase 193 | 194 | dotnet_naming_rule.local_variables_should_be_camelcase.severity = suggestion 195 | dotnet_naming_rule.local_variables_should_be_camelcase.symbols = local_variables 196 | dotnet_naming_rule.local_variables_should_be_camelcase.style = camelcase 197 | 198 | dotnet_naming_rule.local_constants_should_be_camelcase.severity = suggestion 199 | dotnet_naming_rule.local_constants_should_be_camelcase.symbols = local_constants 200 | dotnet_naming_rule.local_constants_should_be_camelcase.style = camelcase 201 | 202 | dotnet_naming_rule.parameters_should_be_camelcase.severity = suggestion 203 | dotnet_naming_rule.parameters_should_be_camelcase.symbols = parameters 204 | dotnet_naming_rule.parameters_should_be_camelcase.style = camelcase 205 | 206 | dotnet_naming_rule.public_fields_should_be_pascalcase.severity = suggestion 207 | dotnet_naming_rule.public_fields_should_be_pascalcase.symbols = public_fields 208 | dotnet_naming_rule.public_fields_should_be_pascalcase.style = pascalcase 209 | 210 | dotnet_naming_rule.private_fields_should_be__camelcase.severity = suggestion 211 | dotnet_naming_rule.private_fields_should_be__camelcase.symbols = private_fields 212 | dotnet_naming_rule.private_fields_should_be__camelcase.style = _camelcase 213 | 214 | dotnet_naming_rule.private_static_fields_should_be_s_camelcase.severity = none 215 | dotnet_naming_rule.private_static_fields_should_be_s_camelcase.symbols = private_static_fields 216 | dotnet_naming_rule.private_static_fields_should_be_s_camelcase.style = s_camelcase 217 | 218 | dotnet_naming_rule.public_constant_fields_should_be_pascalcase.severity = suggestion 219 | dotnet_naming_rule.public_constant_fields_should_be_pascalcase.symbols = public_constant_fields 220 | dotnet_naming_rule.public_constant_fields_should_be_pascalcase.style = pascalcase 221 | 222 | dotnet_naming_rule.private_constant_fields_should_be_pascalcase.severity = suggestion 223 | dotnet_naming_rule.private_constant_fields_should_be_pascalcase.symbols = private_constant_fields 224 | dotnet_naming_rule.private_constant_fields_should_be_pascalcase.style = pascalcase 225 | 226 | dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.severity = suggestion 227 | dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.symbols = public_static_readonly_fields 228 | dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.style = pascalcase 229 | 230 | dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.severity = suggestion 231 | dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.symbols = private_static_readonly_fields 232 | dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.style = pascalcase 233 | 234 | dotnet_naming_rule.enums_should_be_pascalcase.severity = suggestion 235 | dotnet_naming_rule.enums_should_be_pascalcase.symbols = enums 236 | dotnet_naming_rule.enums_should_be_pascalcase.style = pascalcase 237 | 238 | dotnet_naming_rule.local_functions_should_be_pascalcase.severity = suggestion 239 | dotnet_naming_rule.local_functions_should_be_pascalcase.symbols = local_functions 240 | dotnet_naming_rule.local_functions_should_be_pascalcase.style = pascalcase 241 | 242 | dotnet_naming_rule.non_field_members_should_be_pascalcase.severity = suggestion 243 | dotnet_naming_rule.non_field_members_should_be_pascalcase.symbols = non_field_members 244 | dotnet_naming_rule.non_field_members_should_be_pascalcase.style = pascalcase 245 | 246 | # name all constant fields using PascalCase 247 | dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion 248 | dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields 249 | dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style 250 | dotnet_naming_symbols.constant_fields.applicable_kinds = field 251 | dotnet_naming_symbols.constant_fields.required_modifiers = const 252 | dotnet_naming_style.pascal_case_style.capitalization = pascal_case 253 | 254 | dotnet_naming_rule.static_fields_should_have_prefix.severity = suggestion 255 | dotnet_naming_rule.static_fields_should_have_prefix.symbols = static_fields 256 | dotnet_naming_rule.static_fields_should_have_prefix.style = static_prefix_style 257 | dotnet_naming_symbols.static_fields.applicable_kinds = field 258 | dotnet_naming_symbols.static_fields.required_modifiers = static 259 | dotnet_naming_symbols.static_fields.applicable_accessibilities = private, internal, private_protected 260 | dotnet_naming_style.static_prefix_style.required_prefix = s_ 261 | dotnet_naming_style.static_prefix_style.capitalization = camel_case 262 | 263 | # internal and private fields should be _camelCase 264 | dotnet_naming_rule.camel_case_for_private_internal_fields.severity = suggestion 265 | dotnet_naming_rule.camel_case_for_private_internal_fields.symbols = private_internal_fields 266 | dotnet_naming_rule.camel_case_for_private_internal_fields.style = camel_case_underscore_style 267 | dotnet_naming_symbols.private_internal_fields.applicable_kinds = field 268 | dotnet_naming_symbols.private_internal_fields.applicable_accessibilities = private, internal 269 | dotnet_naming_style.camel_case_underscore_style.required_prefix = _ 270 | dotnet_naming_style.camel_case_underscore_style.capitalization = camel_case 271 | 272 | # Symbol specifications 273 | 274 | dotnet_naming_symbols.interfaces.applicable_kinds = interface 275 | dotnet_naming_symbols.interfaces.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 276 | dotnet_naming_symbols.interfaces.required_modifiers = 277 | 278 | dotnet_naming_symbols.enums.applicable_kinds = enum 279 | dotnet_naming_symbols.enums.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 280 | dotnet_naming_symbols.enums.required_modifiers = 281 | 282 | dotnet_naming_symbols.events.applicable_kinds = event 283 | dotnet_naming_symbols.events.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 284 | dotnet_naming_symbols.events.required_modifiers = 285 | 286 | dotnet_naming_symbols.methods.applicable_kinds = method 287 | dotnet_naming_symbols.methods.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 288 | dotnet_naming_symbols.methods.required_modifiers = 289 | 290 | dotnet_naming_symbols.properties.applicable_kinds = property 291 | dotnet_naming_symbols.properties.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 292 | dotnet_naming_symbols.properties.required_modifiers = 293 | 294 | dotnet_naming_symbols.public_fields.applicable_kinds = field 295 | dotnet_naming_symbols.public_fields.applicable_accessibilities = public, internal 296 | dotnet_naming_symbols.public_fields.required_modifiers = 297 | 298 | dotnet_naming_symbols.private_fields.applicable_kinds = field 299 | dotnet_naming_symbols.private_fields.applicable_accessibilities = private, protected, protected_internal, private_protected 300 | dotnet_naming_symbols.private_fields.required_modifiers = 301 | 302 | dotnet_naming_symbols.private_static_fields.applicable_kinds = field 303 | dotnet_naming_symbols.private_static_fields.applicable_accessibilities = private, protected, protected_internal, private_protected 304 | dotnet_naming_symbols.private_static_fields.required_modifiers = static 305 | 306 | dotnet_naming_symbols.types_and_namespaces.applicable_kinds = namespace, class, struct, interface, enum 307 | dotnet_naming_symbols.types_and_namespaces.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 308 | dotnet_naming_symbols.types_and_namespaces.required_modifiers = 309 | 310 | dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method 311 | dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 312 | dotnet_naming_symbols.non_field_members.required_modifiers = 313 | 314 | dotnet_naming_symbols.type_parameters.applicable_kinds = namespace 315 | dotnet_naming_symbols.type_parameters.applicable_accessibilities = * 316 | dotnet_naming_symbols.type_parameters.required_modifiers = 317 | 318 | dotnet_naming_symbols.private_constant_fields.applicable_kinds = field 319 | dotnet_naming_symbols.private_constant_fields.applicable_accessibilities = private, protected, protected_internal, private_protected 320 | dotnet_naming_symbols.private_constant_fields.required_modifiers = const 321 | 322 | dotnet_naming_symbols.local_variables.applicable_kinds = local 323 | dotnet_naming_symbols.local_variables.applicable_accessibilities = local 324 | dotnet_naming_symbols.local_variables.required_modifiers = 325 | 326 | dotnet_naming_symbols.local_constants.applicable_kinds = local 327 | dotnet_naming_symbols.local_constants.applicable_accessibilities = local 328 | dotnet_naming_symbols.local_constants.required_modifiers = const 329 | 330 | dotnet_naming_symbols.parameters.applicable_kinds = parameter 331 | dotnet_naming_symbols.parameters.applicable_accessibilities = * 332 | dotnet_naming_symbols.parameters.required_modifiers = 333 | 334 | dotnet_naming_symbols.public_constant_fields.applicable_kinds = field 335 | dotnet_naming_symbols.public_constant_fields.applicable_accessibilities = public, internal 336 | dotnet_naming_symbols.public_constant_fields.required_modifiers = const 337 | 338 | dotnet_naming_symbols.public_static_readonly_fields.applicable_kinds = field 339 | dotnet_naming_symbols.public_static_readonly_fields.applicable_accessibilities = public, internal 340 | dotnet_naming_symbols.public_static_readonly_fields.required_modifiers = readonly, static 341 | 342 | dotnet_naming_symbols.private_static_readonly_fields.applicable_kinds = field 343 | dotnet_naming_symbols.private_static_readonly_fields.applicable_accessibilities = private, protected, protected_internal, private_protected 344 | dotnet_naming_symbols.private_static_readonly_fields.required_modifiers = readonly, static 345 | 346 | dotnet_naming_symbols.local_functions.applicable_kinds = local_function 347 | dotnet_naming_symbols.local_functions.applicable_accessibilities = * 348 | dotnet_naming_symbols.local_functions.required_modifiers = 349 | 350 | # Naming styles 351 | 352 | dotnet_naming_style.pascalcase.required_prefix = 353 | dotnet_naming_style.pascalcase.required_suffix = 354 | dotnet_naming_style.pascalcase.word_separator = 355 | dotnet_naming_style.pascalcase.capitalization = pascal_case 356 | 357 | dotnet_naming_style.ipascalcase.required_prefix = I 358 | dotnet_naming_style.ipascalcase.required_suffix = 359 | dotnet_naming_style.ipascalcase.word_separator = 360 | dotnet_naming_style.ipascalcase.capitalization = pascal_case 361 | 362 | dotnet_naming_style.tpascalcase.required_prefix = T 363 | dotnet_naming_style.tpascalcase.required_suffix = 364 | dotnet_naming_style.tpascalcase.word_separator = 365 | dotnet_naming_style.tpascalcase.capitalization = pascal_case 366 | 367 | dotnet_naming_style._camelcase.required_prefix = _ 368 | dotnet_naming_style._camelcase.required_suffix = 369 | dotnet_naming_style._camelcase.word_separator = 370 | dotnet_naming_style._camelcase.capitalization = camel_case 371 | 372 | dotnet_naming_style.camelcase.required_prefix = 373 | dotnet_naming_style.camelcase.required_suffix = 374 | dotnet_naming_style.camelcase.word_separator = 375 | dotnet_naming_style.camelcase.capitalization = camel_case 376 | 377 | dotnet_naming_style.s_camelcase.required_prefix = s_ 378 | dotnet_naming_style.s_camelcase.required_suffix = 379 | dotnet_naming_style.s_camelcase.word_separator = 380 | dotnet_naming_style.s_camelcase.capitalization = camel_case 381 | 382 | # Xml config files 383 | [*.{xml,csproj}] 384 | indent_size = 2 385 | tab_width = 2 386 | 387 | [*.{props,targets,config,nuspec,json}] 388 | indent_size = 2 389 | --------------------------------------------------------------------------------