Skip to content

Commit 7eaec50

Browse files
authored
feat: support kernel memory (#11)
* feat: support kernel memory * feat: support read config from IConfiguration
1 parent 84d7c8d commit 7eaec50

24 files changed

+152798
-8
lines changed

README.md

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,13 @@ var response = await kernel.InvokePromptAsync(prompt);
1919
Console.WriteLine(response);
2020
```
2121

22-
## ASP.NET Core
22+
## ASP.NET Core with KernelMemory support
23+
24+
Install Nuget package `Cnblogs.KernelMemory.AI.DashScope`
25+
26+
Install Nuget package `Microsoft.KernelMemory.Core`
27+
28+
Install Nuget package `Microsoft.KernelMemory.SemanticKernelPlugin`
2329

2430
`appsettings.json`
2531

@@ -35,19 +41,54 @@ Console.WriteLine(response);
3541

3642
`Program.cs`
3743
```csharp
44+
// Kernel Memory stuff
45+
var memory = new KernelMemoryBuilder(builder.Services).WithDashScope(builder.Configuration).Build();
46+
builder.Services.AddSingleton(memory);
47+
48+
// SK stuff
3849
builder.Services.AddDashScopeChatCompletion(builder.Configuration);
39-
builder.Services.AddScoped<Kernel>(sp => new Kernel(sp));
50+
builder.Services.AddSingleton(
51+
sp =>
52+
{
53+
var plugins = new KernelPluginCollection();
54+
plugins.AddFromObject(
55+
new MemoryPlugin(sp.GetRequiredService<IKernelMemory>(), waitForIngestionToComplete: true),
56+
"memory");
57+
return new Kernel(sp, plugins);
58+
});
4059
```
4160

4261
Services
4362

4463
```csharp
45-
public class YourService(Kernel kernel)
64+
public class YourService(Kernel kernel, IKernelMemory memory)
4665
{
4766
public async Task<string> GetCompletionAsync(string prompt)
4867
{
4968
var chatResult = await kernel.InvokePromptAsync(prompt);
5069
return chatResult.ToString();
5170
}
71+
72+
public async Task ImportDocumentAsync(string filePath, string documentId)
73+
{
74+
await memory.ImportDocumentAsync(filePath, documentId);
75+
}
76+
77+
public async Task<string> AskMemoryAsync(string question)
78+
{
79+
// use memory.ask to query kernel memory
80+
var skPrompt = """
81+
Question to Kernel Memory: {{$input}}
82+
83+
Kernel Memory Answer: {{memory.ask $input}}
84+
85+
If the answer is empty say 'I don't know' otherwise reply with a preview of the answer, truncated to 15 words.
86+
""";
87+
88+
// you can bundle created functions into a singleton service to reuse them
89+
var myFunction = kernel.CreateFunctionFromPrompt(skPrompt);
90+
var result = await myFunction.InvokeAsync(question);
91+
return result.ToString();
92+
}
5293
}
5394
```

SemanticKernel.DashScope.sln

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{DAB267DF-F
1616
EndProject
1717
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SemanticKernel.DashScope.UnitTest", "test\SemanticKernel.DashScope.UnitTest\SemanticKernel.DashScope.UnitTest.csproj", "{648EBC1D-9409-4205-905E-DD06E4443AAA}"
1818
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
1923
Global
2024
GlobalSection(SolutionConfigurationPlatforms) = preSolution
2125
Debug|Any CPU = Debug|Any CPU
@@ -30,13 +34,23 @@ Global
3034
{648EBC1D-9409-4205-905E-DD06E4443AAA}.Debug|Any CPU.Build.0 = Debug|Any CPU
3135
{648EBC1D-9409-4205-905E-DD06E4443AAA}.Release|Any CPU.ActiveCfg = Release|Any CPU
3236
{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
3345
EndGlobalSection
3446
GlobalSection(SolutionProperties) = preSolution
3547
HideSolutionNode = FALSE
3648
EndGlobalSection
3749
GlobalSection(NestedProjects) = preSolution
3850
{B9EF31C7-48D7-4CA8-8D15-D6340450D3F5} = {BB73FA18-BBBE-4C34-971A-D4206FC118A2}
3951
{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}
4054
EndGlobalSection
4155
GlobalSection(ExtensibilityGlobals) = postSolution
4256
SolutionGuid = {D0F2E9A4-9782-4C3F-B459-CFCAD4C9AD9F}

pack.sh

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
#!/bin/bash
22
set -e
33

4-
[ -z $1 ] && echo "Missing version" && exit 1
4+
[ -z "$1" ] && echo "Missing tag and version" && exit 1
55

6-
version=$1
7-
project=src/SemanticKernel.DashScope
6+
commit_tag=$1
7+
IFS=/ read -r tagname version <<< "$commit_tag"
8+
9+
version=${version:1}
10+
project=src/${tagname}
811
dotnet clean -c Release
912
dotnet build -p:Version=${version-*} -c Release $project
1013
dotnet pack $project -c Release -p:IncludeSymbols=true -p:SymbolPackageFormat=snupkg --include-source -p:PackageVersion=$version -p:Version=${version-*} -o ./artifacts
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
namespace Cnblogs.KernelMemory.AI.DashScope;
2+
3+
/// <summary>
4+
/// DashScope Settings.
5+
/// </summary>
6+
public record DashScopeConfig
7+
{
8+
/// <summary>
9+
/// Model used for text generation. Chat models can be used too.
10+
/// </summary>
11+
public string ChatCompletionModelId { get; set; } = string.Empty;
12+
13+
/// <summary>
14+
/// The max number of tokens supported by the text model.
15+
/// </summary>
16+
public int TextModelMaxTokenTotal { get; set; } = 6000;
17+
18+
/// <summary>
19+
/// Model used to embedding generation.
20+
/// </summary>
21+
public string TextEmbeddingModelId { get; set; } = string.Empty;
22+
23+
/// <summary>
24+
/// The max number of tokens supported by the embedding model.
25+
/// Defaults to 2048.
26+
/// </summary>
27+
public int EmbeddingModelMaxTokenTotal { get; set; } = 2048;
28+
29+
/// <summary>
30+
/// DashScope API key.
31+
/// </summary>
32+
public string ApiKey { get; set; } = string.Empty;
33+
34+
/// <summary>
35+
/// Validates the config.
36+
/// </summary>
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+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
using System.Reflection;
2+
using Microsoft.KernelMemory.Configuration;
3+
4+
namespace Cnblogs.KernelMemory.AI.DashScope;
5+
6+
internal static class DashScopeEmbeddedResource
7+
{
8+
private static readonly string? Namespace = typeof(DashScopeEmbeddedResource).Namespace;
9+
10+
internal static Stream ReadBpeFile()
11+
{
12+
return Read("qwen.tiktoken");
13+
}
14+
15+
private static Stream Read(string fileName)
16+
{
17+
// Get the current assembly. Note: this class is in the same assembly where the embedded resources are stored.
18+
var assembly = typeof(DashScopeEmbeddedResource).GetTypeInfo().Assembly;
19+
if (assembly == null) { throw new ConfigurationException($"[{Namespace}] {fileName} assembly not found"); }
20+
21+
// Resources are mapped like types, using the namespace and appending "." (dot) and the file name
22+
var resourceName = $"{Namespace}." + fileName;
23+
var resource = assembly.GetManifestResourceStream(resourceName);
24+
if (resource == null) { throw new ConfigurationException($"{resourceName} resource not found"); }
25+
26+
// Return the resource content, in text format.
27+
return resource;
28+
}
29+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
using Cnblogs.DashScope.Sdk;
2+
using Cnblogs.DashScope.Sdk.TextEmbedding;
3+
using Microsoft.KernelMemory;
4+
using Microsoft.KernelMemory.AI;
5+
6+
namespace Cnblogs.KernelMemory.AI.DashScope;
7+
8+
/// <summary>
9+
/// DashScope text embedding generator implementation.
10+
/// </summary>
11+
/// <param name="dashScopeClient">The <see cref="IDashScopeClient"/>.</param>
12+
/// <param name="modelId">The model id to use.</param>
13+
/// <param name="tokenizer">The tokenizer to use.</param>
14+
/// <param name="maxTokens">Maximum token limit.</param>
15+
public class DashScopeTextEmbeddingGenerator(
16+
IDashScopeClient dashScopeClient,
17+
string modelId,
18+
ITextTokenizer? tokenizer = null,
19+
int maxTokens = 2048)
20+
: ITextEmbeddingGenerator
21+
{
22+
/// <inheritdoc />
23+
public int CountTokens(string text)
24+
{
25+
return tokenizer?.CountTokens(text) ?? text.Length;
26+
}
27+
28+
/// <inheritdoc />
29+
public async Task<Embedding> GenerateEmbeddingAsync(
30+
string text,
31+
CancellationToken cancellationToken = new())
32+
{
33+
var result = await dashScopeClient.GetTextEmbeddingsAsync(modelId, [text], null, cancellationToken);
34+
return result.Output.Embeddings[0].Embedding;
35+
}
36+
37+
/// <inheritdoc />
38+
public int MaxTokens => maxTokens;
39+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
using System.Runtime.CompilerServices;
2+
using Cnblogs.DashScope.Sdk;
3+
using Microsoft.Extensions.Logging;
4+
using Microsoft.KernelMemory.AI;
5+
using Microsoft.KernelMemory.Diagnostics;
6+
7+
namespace Cnblogs.KernelMemory.AI.DashScope;
8+
9+
/// <summary>
10+
/// Text generator using DashScope.
11+
/// </summary>
12+
/// <param name="dashScopeClient">The <see cref="IDashScopeClient"/>.</param>
13+
/// <param name="modelId">Model name.</param>
14+
/// <param name="loggerFactory">Logger factory to use.</param>
15+
/// <param name="tokenizer">Tokenizer to use.</param>
16+
/// <param name="maxToken">Maximum token count.</param>
17+
public class DashScopeTextGenerator(
18+
IDashScopeClient dashScopeClient,
19+
string modelId,
20+
ILoggerFactory? loggerFactory = null,
21+
ITextTokenizer? tokenizer = null,
22+
int maxToken = 6000) : ITextGenerator
23+
{
24+
private readonly ILogger<DashScopeTextGenerator> _logger = loggerFactory?.CreateLogger<DashScopeTextGenerator>()
25+
?? DefaultLogger<DashScopeTextGenerator>.Instance;
26+
27+
/// <inheritdoc />
28+
public int CountTokens(string text)
29+
{
30+
return tokenizer?.CountTokens(text) ?? QWenTokenizer.CountTokensStatic(text);
31+
}
32+
33+
/// <inheritdoc />
34+
public async IAsyncEnumerable<string> GenerateTextAsync(
35+
string prompt,
36+
TextGenerationOptions options,
37+
[EnumeratorCancellation] CancellationToken cancellationToken = new())
38+
{
39+
var parameters = new TextGenerationParameters
40+
{
41+
TopP = options.TopP == 0 ? null : (float)options.TopP,
42+
Temperature = options.Temperature == 0 ? null : (float)options.Temperature,
43+
RepetitionPenalty =
44+
options.FrequencyPenalty == 0 ? null : ((float)options.FrequencyPenalty + 1), // dashScope's default value is 1.0, kernel memory is 0.0
45+
MaxTokens = options.MaxTokens == 0 ? null : options.MaxTokens,
46+
Stop = options.StopSequences.ToArray(),
47+
IncrementalOutput = true,
48+
ResultFormat = ResultFormats.Text
49+
};
50+
51+
if (options.TokenSelectionBiases.Count != 0)
52+
{
53+
_logger.LogWarning("TokenSelectionBiases is not supported by DashScope and will be ignored");
54+
}
55+
56+
var request = new ModelRequest<TextGenerationInput, ITextGenerationParameters>
57+
{
58+
Model = modelId,
59+
Input = new TextGenerationInput { Prompt = prompt },
60+
Parameters = parameters
61+
};
62+
var tokens = dashScopeClient.GetTextCompletionStreamAsync(request, cancellationToken);
63+
await foreach (var token in tokens)
64+
{
65+
yield return token.Output.Text!;
66+
}
67+
}
68+
69+
/// <inheritdoc />
70+
public int MaxTokenTotal => maxToken;
71+
}

0 commit comments

Comments
 (0)