Skip to content

Commit

Permalink
[C#] feat: Add Kernel Memory Data source to Teams Chef Bot (#1222)
Browse files Browse the repository at this point in the history
## Linked issues

closes: #1200  (issue number)

## Details
* Add KernelMemory data source into Teams Chef Bot
* Fix minor bug in `ChatMessage`

## Attestation Checklist

- [x] My code follows the style guidelines of this project

- I have checked for/fixed spelling, linting, and other errors
- I have commented my code for clarity
- I have made corresponding changes to the documentation (updating the
doc strings in the code is sufficient)
- My changes generate no new warnings
- I have added tests that validates my changes, and provides sufficient
test coverage. I have tested with:
  - Local testing
  - E2E testing in Teams
- New and existing unit tests pass locally with my changes
  • Loading branch information
singhk97 authored Jan 31, 2024
1 parent 1883aa9 commit 2a6b47f
Show file tree
Hide file tree
Showing 27 changed files with 1,852 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,27 @@ namespace Microsoft.Teams.AI.Tests.AITests.Models
{
public class ChatMessageExtensionsTests
{
[Fact]
public void Test_InvalidRole_ToAzureSdkChatMessage()
{
// Arrange
var chatMessage = new ChatMessage(new AI.Models.ChatRole("InvalidRole"));

// Act
var ex = Assert.Throws<TeamsAIException>(() => chatMessage.ToChatRequestMessage());

// Assert
Assert.Equal($"Invalid chat message role: InvalidRole", ex.Message);
}

[Fact]
public void Test_UserRole_ToAzureSdkChatMessage()
{
// Arrange
var chatMessage = new ChatMessage(AI.Models.ChatRole.User)
{
Content = "test-content",
Name = "author"
};

// Act
Expand All @@ -22,6 +36,7 @@ public void Test_UserRole_ToAzureSdkChatMessage()
Assert.Equal(Azure.AI.OpenAI.ChatRole.User, result.Role);
Assert.Equal(typeof(ChatRequestUserMessage), result.GetType());
Assert.Equal("test-content", ((ChatRequestUserMessage)result).Content);
Assert.Equal("author", ((ChatRequestUserMessage)result).Name);
}

[Fact]
Expand Down Expand Up @@ -67,6 +82,7 @@ public void Test_SystemRole_ToAzureSdkChatMessage()
var chatMessage = new ChatMessage(AI.Models.ChatRole.System)
{
Content = "test-content",
Name = "author"
};

// Act
Expand All @@ -76,6 +92,7 @@ public void Test_SystemRole_ToAzureSdkChatMessage()
Assert.Equal(Azure.AI.OpenAI.ChatRole.System, result.Role);
Assert.Equal(typeof(ChatRequestSystemMessage), result.GetType());
Assert.Equal("test-content", ((ChatRequestSystemMessage)result).Content);
Assert.Equal("author", ((ChatRequestSystemMessage)result).Name);
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public class ChatMessage
/// <summary>
/// The text associated with this message payload.
/// </summary>
public string? Content { get; set; }
public string Content { get; set; } = string.Empty;

/// <summary>
/// The name of the author of this message. `name` is required if role is `function`, and it should be the name of the
Expand Down Expand Up @@ -53,15 +53,15 @@ public class FunctionCall
/// <summary>
/// The name of the function to call.
/// </summary>
public string Name { get; set; }
public string Name { get; set; } = string.Empty;

/// <summary>
/// The arguments to call the function with, as generated by the model in JSON format.
/// Note that the model does not always generate valid JSON, and may hallucinate parameters
/// not defined by your function schema. Validate the arguments in your code before calling
/// your function.
/// </summary>
public string Arguments { get; set; }
public string Arguments { get; set; } = string.Empty;

/// <summary>
/// Creates an instance of `FunctionCall`.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Azure.AI.OpenAI;
using Microsoft.Teams.AI.Exceptions;
using Microsoft.Teams.AI.Utilities;

namespace Microsoft.Teams.AI.AI.Models
{
Expand All @@ -15,23 +16,32 @@ internal static class ChatMessageExtensions
/// <returns>An <see cref="ChatRequestMessage"/>.</returns>
public static ChatRequestMessage ToChatRequestMessage(this ChatMessage chatMessage)
{
Verify.NotNull(chatMessage.Content);
Verify.NotNull(chatMessage.Role);

ChatRole role = chatMessage.Role;
ChatRequestMessage? message = null;

if (role == ChatRole.User)
{
message = new ChatRequestUserMessage(chatMessage.Content);
ChatRequestUserMessage userMessage = new(chatMessage.Content);

if (chatMessage.Name != null)
{
userMessage.Name = chatMessage.Name;
}

message = userMessage;
}

if (role == ChatRole.Assistant)
{
Azure.AI.OpenAI.FunctionCall functionCall = new(chatMessage.FunctionCall?.Name, chatMessage.FunctionCall?.Arguments);
ChatRequestAssistantMessage assistantMessage = new(chatMessage.Content);

ChatRequestAssistantMessage assistantMessage = new(chatMessage.Content)
if (chatMessage.FunctionCall != null)
{
FunctionCall = functionCall,
Name = chatMessage.Name,
};
assistantMessage.FunctionCall = new(chatMessage.FunctionCall.Name ?? "", chatMessage.FunctionCall.Arguments ?? "");
}

if (chatMessage.ToolCalls != null)
{
Expand All @@ -41,22 +51,34 @@ public static ChatRequestMessage ToChatRequestMessage(this ChatMessage chatMessa
}
}

if (chatMessage.Name != null)
{
assistantMessage.Name = chatMessage.Name;
}

message = assistantMessage;
}

if (role == ChatRole.System)
{
message = new ChatRequestSystemMessage(chatMessage.Content);
ChatRequestSystemMessage systemMessage = new(chatMessage.Content);

if (chatMessage.Name != null)
{
systemMessage.Name = chatMessage.Name;
}

message = systemMessage;
}

if (role == ChatRole.Function)
{
message = new ChatRequestFunctionMessage(chatMessage.Name, chatMessage.Content);
message = new ChatRequestFunctionMessage(chatMessage.Name ?? "", chatMessage.Content);
}

if (role == ChatRole.Tool)
{
message = new ChatRequestToolMessage(chatMessage.Content, chatMessage.ToolCallId);
message = new ChatRequestToolMessage(chatMessage.Content, chatMessage.ToolCallId ?? "");
}

if (message == null)
Expand Down
125 changes: 125 additions & 0 deletions dotnet/samples/04.ai.a.teamsChefBot/KernelMemoryDataSource.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
using Microsoft.Bot.Builder;
using Microsoft.KernelMemory;
using Microsoft.Teams.AI.AI.DataSources;
using Microsoft.Teams.AI.AI.Prompts.Sections;
using Microsoft.Teams.AI.AI.Tokenizers;
using Microsoft.Teams.AI.State;
using System.Text;

namespace TeamsChefBot
{
/// <summary>
/// The class connects the Kernel Memory library data source to the bot.
/// Kernel Memory is a library that allows you to index and query any data using LLM and natural language,
/// tracking sources and showing citations (https://github.com/microsoft/kernel-memory).
/// </summary>
public class KernelMemoryDataSource : IDataSource
{
private readonly IKernelMemory _kernelMemory;
private readonly Task? _ingestTask;

public KernelMemoryDataSource(string name, IKernelMemory memoryInstance)
{
ArgumentNullException.ThrowIfNull(memoryInstance);

this._kernelMemory = memoryInstance;
this.Name = name;

if (memoryInstance.GetDocumentStatusAsync("doc-1").Result?.Completed != true)
{
// Ingest documents on construction
this._ingestTask = this.IngestAsync();
}
}

public string Name { get; }

/// <summary>
/// Loads documents from the 'files' folder into Kernel Memory's in-memory vector database.
/// </summary>
/// <returns></returns>
private async Task IngestAsync()
{
Console.WriteLine("Loading documents from the 'files' folder into Kernel Memory's in-memory vector database");

var importTasks = new List<Task>();
string[] Documents = Directory.GetFiles("files");

int i = 0;
foreach (string doc in Documents)
{
importTasks.Add(this._kernelMemory.ImportDocumentAsync(doc, documentId: $"doc-{i}"));
i++;
}

await Task.WhenAll(importTasks);
}

public async Task<RenderedPromptSection<string>> RenderDataAsync(ITurnContext context, IMemory memory, ITokenizer tokenizer, int maxTokens, CancellationToken cancellationToken = default)
{
if (this._ingestTask?.IsCompleted == false)
{
// Wait for ingestion to complete
await _ingestTask;
}

string? ask = memory.GetValue("temp.input") as string;

if (ask == null)
{
return new RenderedPromptSection<string>(string.Empty, 0);
}

// Query index for all relevant documents
SearchResult result = await this._kernelMemory.SearchAsync(ask);

if (result.NoResult)
{
Console.WriteLine("No results when querying Kernel Memory found");
return new RenderedPromptSection<string>(string.Empty, 0);
}

List<Citation> citations = result.Results;

// Add documents until you run out of tokens
int length = 0;
StringBuilder output = new();
string connector = "";
bool maxTokensReached = false;
foreach (Citation citation in citations)
{
// Start a new doc
StringBuilder doc = new();
doc.Append($"{connector}<context>\n");
length += tokenizer.Encode($"{connector}<context>\n").Count;
// Add ending tag count to token count
length += tokenizer.Encode("</context>\n").Count;

foreach (var partition in citation.Partitions)
{
// Add the partition to the doc
int partitionLength = tokenizer.Encode(partition.Text).Count;
int remainingTokens = maxTokens - (length + partitionLength);
if (remainingTokens < 0)
{
maxTokensReached = true;
break;
}
length += partitionLength;
doc.Append($"{partition.Text}\n");
}

doc.Append("</context>\n");
output.Append(doc.ToString());
connector = "\n\n";

if (maxTokensReached)
{
break;
}
}

return new RenderedPromptSection<string>(output.ToString(), length, length > maxTokens);
}
}
}
34 changes: 34 additions & 0 deletions dotnet/samples/04.ai.a.teamsChefBot/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using Microsoft.Teams.AI.State;
using Microsoft.Teams.AI;
using TeamsChefBot;
using Microsoft.KernelMemory;

var builder = WebApplication.CreateBuilder(args);

Expand Down Expand Up @@ -35,16 +36,27 @@
// Create AI Model
if (!string.IsNullOrEmpty(config.OpenAI?.ApiKey))
{
// Create OpenAI Model
builder.Services.AddSingleton<OpenAIModel>(sp => new(
new OpenAIModelOptions(config.OpenAI.ApiKey, "gpt-3.5-turbo")
{
LogRequests = true
},
sp.GetService<ILoggerFactory>()
));

// Create Kernel Memory Serverless instance using OpenAI embeddings API
builder.Services.AddSingleton<IKernelMemory>((sp) =>
{
return new KernelMemoryBuilder()
.WithOpenAIDefaults(config.OpenAI.ApiKey)
.WithSimpleFileStorage()
.Build<MemoryServerless>();
});
}
else if (!string.IsNullOrEmpty(config.Azure?.OpenAIApiKey) && !string.IsNullOrEmpty(config.Azure.OpenAIEndpoint))
{
// Create Azure OpenAI Model
builder.Services.AddSingleton<OpenAIModel>(sp => new(
new AzureOpenAIModelOptions(
config.Azure.OpenAIApiKey,
Expand All @@ -56,6 +68,25 @@
},
sp.GetService<ILoggerFactory>()
));

// Create Kernel Memory Serverless instance using AzureOpenAI embeddings API
builder.Services.AddSingleton<IKernelMemory>((sp) =>
{
AzureOpenAIConfig azureConfig = new()
{
Auth = AzureOpenAIConfig.AuthTypes.APIKey,
APIKey = config.Azure.OpenAIApiKey,
Endpoint = config.Azure.OpenAIEndpoint,
APIType = AzureOpenAIConfig.APITypes.EmbeddingGeneration,
Deployment = "text-embedding-ada-002" // Update this to the deployment you want to use
};

return new KernelMemoryBuilder()
.WithAzureOpenAITextEmbeddingGeneration(azureConfig)
.WithAzureOpenAITextGeneration(azureConfig)
.WithSimpleFileStorage()
.Build<MemoryServerless>();
});
}
else
{
Expand All @@ -74,6 +105,9 @@
PromptFolder = "./Prompts"
});

KernelMemoryDataSource dataSource = new("teams-ai", sp.GetService<IKernelMemory>()!);
prompts.AddDataSource("teams-ai", dataSource);

// Create ActionPlanner
ActionPlanner<TurnState> planner = new(
options: new(
Expand Down
7 changes: 5 additions & 2 deletions dotnet/samples/04.ai.a.teamsChefBot/Prompts/Chat/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"completion_type": "chat",
"include_history": true,
"include_input": true,
"max_input_tokens": 2800,
"max_input_tokens": 2000,
"max_tokens": 1000,
"temperature": 0.2,
"top_p": 0.0,
Expand All @@ -16,6 +16,9 @@
"stop_sequences": []
},
"augmentation": {
"augmentation_type": "none"
"augmentation_type": "none",
"data_sources": {
"teams-ai": 900
}
}
}
2 changes: 2 additions & 0 deletions dotnet/samples/04.ai.a.teamsChefBot/TeamsChefBot.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
<PackageReference Include="Microsoft.Bot.Builder" Version="4.21.1" />
<PackageReference Include="Microsoft.Bot.Builder.Integration.AspNet.Core" Version="4.21.1" />
<PackageReference Include="Microsoft.Bot.Connector" Version="4.21.1" />
<PackageReference Include="Microsoft.KernelMemory.AI.OpenAI" Version="0.26.240121.1" />
<PackageReference Include="Microsoft.KernelMemory.Core" Version="0.26.240121.1" />
<PackageReference Include="Microsoft.Teams.AI" Version="1.1.*-*" />
</ItemGroup>

Expand Down
1 change: 1 addition & 0 deletions dotnet/samples/04.ai.a.teamsChefBot/files/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Each document in this folder is a markdown file that was scraped from the Teams AI Github repository. This knowledge base will be used by the Teams Chef bot to answer questions about the Teams AI library.
Loading

0 comments on commit 2a6b47f

Please sign in to comment.