-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #78 from Encamina/@mramos/V8.1.3
Bump version `8.1.3` - Introduced `IChatHistoryProvider` interface and its corresponding implementation `ChatHistoryProvider`.
- Loading branch information
Showing
13 changed files
with
333 additions
and
155 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
29 changes: 29 additions & 0 deletions
29
src/Encamina.Enmarcha.SemanticKernel.Abstractions/IChatHistoryProvider.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
using Microsoft.SemanticKernel.ChatCompletion; | ||
|
||
namespace Encamina.Enmarcha.SemanticKernel.Abstractions; | ||
|
||
/// <summary> | ||
/// Represents a collection of functions to work chat history. | ||
/// </summary> | ||
public interface IChatHistoryProvider | ||
{ | ||
/// <summary> | ||
/// Loads chat history messages. | ||
/// </summary> | ||
/// <param name="chatHistory">The current chat history.</param> | ||
/// <param name="userId">The unique identifier of the user that is owner of the chat.</param> | ||
/// <param name="remainingTokens">The total remaining tokens available for loading messages from the chat history.</param> | ||
/// <param name="cancellationToken">A cancellation token that can be used to receive notice of cancellation.</param> | ||
/// <returns>A <see cref="Task"/> that on completion indicates the asynchronous operation has executed.</returns> | ||
Task LoadChatMessagesHistoryAsync(ChatHistory chatHistory, string userId, int remainingTokens, CancellationToken cancellationToken); | ||
|
||
/// <summary> | ||
/// Saves a chat message into the conversation history. | ||
/// </summary> | ||
/// <param name="userId">The user's unique identifier.</param> | ||
/// <param name="roleName">The name of the role associated with the chat message. For example the `user`, the `assistant` or the `system`.</param> | ||
/// <param name="message">The message.</param> | ||
/// <param name="cancellationToken">A cancellation token that can be used to receive notice of cancellation.</param> | ||
/// <returns>A <see cref="Task"/> that on completion indicates the asynchronous operation has executed.</returns> | ||
Task SaveChatMessagesHistoryAsync(string userId, string roleName, string message, CancellationToken cancellationToken); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
110 changes: 110 additions & 0 deletions
110
src/Encamina.Enmarcha.SemanticKernel.Plugins.Chat/ChatHistoryProvider.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
using Encamina.Enmarcha.Data.Abstractions; | ||
|
||
using Encamina.Enmarcha.SemanticKernel.Abstractions; | ||
using Encamina.Enmarcha.SemanticKernel.Plugins.Chat.Options; | ||
using Encamina.Enmarcha.SemanticKernel.Plugins.Chat.Plugins; | ||
|
||
using Microsoft.Extensions.Options; | ||
|
||
using Microsoft.SemanticKernel.ChatCompletion; | ||
|
||
namespace Encamina.Enmarcha.SemanticKernel.Plugins.Chat; | ||
|
||
/// <inheritdoc/> | ||
public class ChatHistoryProvider : IChatHistoryProvider | ||
{ | ||
private readonly IAsyncRepository<ChatMessageHistoryRecord> chatMessagesHistoryRepository; | ||
private readonly Func<string, int> tokensLengthFunction; | ||
|
||
private ChatHistoryProviderOptions options; | ||
|
||
/// <summary> | ||
/// Initializes a new instance of the <see cref="ChatHistoryProvider"/> class. | ||
/// </summary> | ||
/// <param name="tokensLengthFunction">Function to calculate the length of a string (usually the chat messages) in tokens.</param> | ||
/// <param name="chatMessagesHistoryRepository">A valid instance of an asynchronous repository pattern implementation.</param> | ||
/// <param name="options">Configuration options for this provider.</param> | ||
public ChatHistoryProvider(Func<string, int> tokensLengthFunction, IAsyncRepository<ChatMessageHistoryRecord> chatMessagesHistoryRepository, IOptionsMonitor<ChatHistoryProviderOptions> options) | ||
{ | ||
this.tokensLengthFunction = tokensLengthFunction; | ||
this.chatMessagesHistoryRepository = chatMessagesHistoryRepository; | ||
this.options = options.CurrentValue; | ||
|
||
options.OnChange((newOptions) => this.options = newOptions); | ||
} | ||
|
||
/// <inheritdoc/> | ||
/// <remarks> | ||
/// The maximum number of messages to load is configured in <c>ChatHistoryProviderOptions.HistoryMaxMessages</c>. | ||
/// </remarks> | ||
public async Task LoadChatMessagesHistoryAsync(ChatHistory chatHistory, string userId, int remainingTokens, CancellationToken cancellationToken) | ||
{ | ||
if (options.HistoryMaxMessages <= 0 || remainingTokens <= 0) | ||
{ | ||
return; | ||
} | ||
|
||
// Obtain the chat history for the user, ordered by timestamps descending to get the most recent messages first, and then take 'N' messages. | ||
// This means that the first elements in the list are the most recent or newer messages, and the last elements in the list are the oldest messages. | ||
var result = (await chatMessagesHistoryRepository.GetAllAsync(chatMessages => chatMessages.Where(chatMessage => chatMessage.UserId == userId) | ||
.OrderByDescending(chatMessage => chatMessage.TimestampUtc) | ||
.Take(options.HistoryMaxMessages), cancellationToken)).ToList(); | ||
|
||
// Previous lines loads into `result` a list of chat history messages like this (U = stands for `User`; A = stands for `Assistant`, a message from the AI): | ||
// Newer Messages -------> Older Messages | ||
// A10 U10 A9 U9 A8 U8 A7 U7 A6 U6 A5 U5 A4 U4 A3 U3 A2 U2 A1 U1 | ||
|
||
var assistantRoleName = AuthorRole.Assistant.ToString(); // Get this here to slightly improve performance... | ||
|
||
result.TakeWhile(item => | ||
{ | ||
var itemRole = item.RoleName == assistantRoleName ? AuthorRole.Assistant : AuthorRole.User; | ||
var tokensHistoryMessage = ILengthFunctions.LengthChatMessage(item.Message, itemRole, tokensLengthFunction); | ||
|
||
if (tokensHistoryMessage <= remainingTokens) | ||
{ | ||
remainingTokens -= tokensHistoryMessage; | ||
return true; | ||
} | ||
|
||
return false; | ||
|
||
// The `TakeWhile` will reduce the number of chat history messages, taking the most recent messages until the remaining tokens are less than the tokens of the current message. | ||
// Newer Messages -------> Older Messages | ||
// A10 U10 A9 U9 A8 U8 A7 U7 A6 U6 | ||
}).Reverse().ToList().ForEach(item => | ||
{ | ||
// The (previous) `Reverse` will invert the order of the messages, so the oldest messages are the first elements in the list. | ||
// This is required because the `ChatHistory` class stacks messages in the order they were received. | ||
// The oldest came first, and the newest came last in the stack of messages. | ||
// Older Messages -------> Newer Messages | ||
// U6 A6 U7 A7 U8 A8 U9 A9 U10 A10 | ||
|
||
// In some scenarios, empty messages might be retrieved as `null` and serialized as such when creating the request message for the LLM resulting in an HTTP 400 error. | ||
// Prevent this by setting the message as an empty string if the message is `null`. | ||
var msg = item.Message ?? string.Empty; | ||
|
||
if (item.RoleName == assistantRoleName) | ||
{ | ||
chatHistory.AddAssistantMessage(msg); | ||
} | ||
else | ||
{ | ||
chatHistory.AddUserMessage(msg); | ||
} | ||
}); | ||
} | ||
|
||
/// <inheritdoc/> | ||
public async Task SaveChatMessagesHistoryAsync(string userId, string roleName, string message, CancellationToken cancellationToken) | ||
{ | ||
await chatMessagesHistoryRepository.AddAsync(new ChatMessageHistoryRecord() | ||
{ | ||
Id = Guid.NewGuid().ToString(), | ||
UserId = userId, | ||
RoleName = roleName, | ||
Message = message, | ||
TimestampUtc = DateTime.UtcNow, | ||
}, cancellationToken); | ||
} | ||
} |
70 changes: 70 additions & 0 deletions
70
src/Encamina.Enmarcha.SemanticKernel.Plugins.Chat/Extensions/IServiceCollectionExtensions.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
using CommunityToolkit.Diagnostics; | ||
|
||
using Encamina.Enmarcha.Data.Abstractions; | ||
using Encamina.Enmarcha.Data.Cosmos; | ||
|
||
using Encamina.Enmarcha.SemanticKernel.Abstractions; | ||
using Encamina.Enmarcha.SemanticKernel.Plugins.Chat.Options; | ||
using Encamina.Enmarcha.SemanticKernel.Plugins.Chat.Plugins; | ||
|
||
using Microsoft.Extensions.DependencyInjection; | ||
using Microsoft.Extensions.Options; | ||
|
||
namespace Encamina.Enmarcha.SemanticKernel.Plugins.Chat.Extensions; | ||
|
||
/// <summary> | ||
/// Extension methods for setting up services in a <see cref="IServiceCollection"/>. | ||
/// </summary> | ||
public static class IServiceCollectionExtensions | ||
{ | ||
/// <summary> | ||
/// <para> | ||
/// Add and configures the <see cref="ChatHistoryProvider"/> type as singleton service instance of the <see cref="IChatHistoryProvider"/> service to the <see cref="IServiceCollection"/>. | ||
/// </para> | ||
/// <para> | ||
/// Uses CosmosDB as the repository for chat history messages. | ||
/// </para> | ||
/// </summary> | ||
/// <param name="services">The <see cref="IServiceCollection"/> to add services to.</param> | ||
/// <param name="cosmosContainer">The name of the Cosmos DB container to store the chat history messages.</param> | ||
/// <param name="tokensLengthFunction"> | ||
/// A function to calculate the length by tokens of the chat messages. These functions are usually available in the «mixin» interface <see cref="ILengthFunctions"/>. | ||
/// </param> | ||
/// <remarks> | ||
/// This extension method uses a «Service Location» pattern provided by the <see cref="IServiceProvider"/> to resolve the following dependencies: | ||
/// <list type="bullet"> | ||
/// <item> | ||
/// <term>ChatHistoryProviderOptions</term> | ||
/// <description> | ||
/// A required dependency of type <see cref="ChatHistoryProviderOptions"/> used to retrieve the configuration options for this provider. This dependency should | ||
/// be added using any of the <see cref="OptionsServiceCollectionExtensions.AddOptions"/> extension method. | ||
/// </description> | ||
/// </item> | ||
/// <item> | ||
/// <term>ICosmosRepositoryFactory</term> | ||
/// <description> | ||
/// A required dependency of type <see cref="ICosmosRepositoryFactory"/> used to create a <see cref="ICosmosRepository{T}"/> (which | ||
/// inherits from <see cref="IAsyncRepository{T}"/>) and manage chat history messages. Use the <c>AddCosmos</c> extension method to add this dependency. | ||
/// </description> | ||
/// </item> | ||
/// </list> | ||
/// </remarks> | ||
/// <returns>The <see cref="IServiceCollection"/> so that additional calls can be chained.</returns> | ||
public static IServiceCollection AddCosmosChatHistoryProvider(this IServiceCollection services, string cosmosContainer, Func<string, int> tokensLengthFunction) | ||
{ | ||
Guard.IsNotNullOrWhiteSpace(cosmosContainer); | ||
Guard.IsNotNull(tokensLengthFunction); | ||
|
||
services.AddSingleton(sp => | ||
{ | ||
var chatMessagesHistoryRepository = sp.GetRequiredService<ICosmosRepositoryFactory>().Create<ChatMessageHistoryRecord>(cosmosContainer); | ||
var chatHistoryProviderOptions = sp.GetRequiredService<IOptionsMonitor<ChatHistoryProviderOptions>>(); | ||
|
||
return new ChatHistoryProvider(tokensLengthFunction, chatMessagesHistoryRepository, chatHistoryProviderOptions); | ||
}); | ||
|
||
services.AddSingleton<IChatHistoryProvider, ChatHistoryProvider>(sp => sp.GetRequiredService<ChatHistoryProvider>()); | ||
|
||
return services; | ||
} | ||
} |
Oops, something went wrong.