Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add persona tab #35

Merged
merged 17 commits into from
Jul 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions webapi/CopilotChat/Controllers/BotController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -115,15 +115,15 @@ public async Task<ActionResult<ChatSession>> UploadAsync(

// Upload chat history into chat repository and embeddings into memory.

// 1. Create a new chat and get the chat id.
newChat = new ChatSession(chatTitle);
// Create a new chat and get the chat id.
newChat = new ChatSession(chatTitle, bot.SystemDescription);
await this._chatRepository.CreateAsync(newChat);
await this._chatParticipantRepository.CreateAsync(new ChatParticipant(userId, newChat.Id));
chatId = newChat.Id;

string oldChatId = bot.ChatHistory.First().ChatId;

// 2. Update the app's chat storage.
// Update the app's chat storage.
foreach (var message in bot.ChatHistory)
{
var chatMessage = new ChatMessage(
Expand All @@ -139,7 +139,7 @@ public async Task<ActionResult<ChatSession>> UploadAsync(
await this._chatMessageRepository.CreateAsync(chatMessage);
}

// 3. Update the memory.
// Update the memory.
await this.BulkUpsertMemoryRecordsAsync(oldChatId, chatId, bot.Embeddings, cancellationToken);

// TODO: [Issue #47] Revert changes if any of the actions failed
Expand Down Expand Up @@ -254,6 +254,9 @@ private async Task<Bot> CreateBotAsync(IKernel kernel, Guid chatId)
ChatSession chat = await this._chatRepository.FindByIdAsync(chatIdString);
bot.ChatTitle = chat.Title;

// get the system description
bot.SystemDescription = chat.SystemDescription;
TaoChenOSU marked this conversation as resolved.
Show resolved Hide resolved

// get the chat history
bot.ChatHistory = await this.GetAllChatMessagesAsync(chatIdString);

Expand Down
4 changes: 3 additions & 1 deletion webapi/CopilotChat/Controllers/ChatHistoryController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ public async Task<IActionResult> CreateChatSessionAsync([FromBody] CreateChatPar
}

// Create a new chat session
var newChat = new ChatSession(chatParameter.Title);
var newChat = new ChatSession(chatParameter.Title, this._promptOptions.SystemDescription);
await this._sessionRepository.CreateAsync(newChat);

var initialBotMessage = this._promptOptions.InitialBotMessage;
Expand Down Expand Up @@ -201,6 +201,8 @@ public async Task<IActionResult> EditChatSessionAsync(
if (await this._sessionRepository.TryFindByIdAsync(chatId, v => chat = v))
{
chat!.Title = chatParameters.Title;
chat!.SystemDescription = chatParameters.SystemDescription;
chat!.MemoryBalance = chatParameters.MemoryBalance;
await this._sessionRepository.UpsertAsync(chat);
await messageRelayHubContext.Clients.Group(chatId).SendAsync(ChatEditedClientCall, chat);
return this.Ok(chat);
Expand Down
106 changes: 106 additions & 0 deletions webapi/CopilotChat/Controllers/ChatMemoryController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// Copyright (c) Microsoft. All rights reserved.

using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.SemanticKernel.Memory;
using SemanticKernel.Service.CopilotChat.Options;
using SemanticKernel.Service.CopilotChat.Skills.ChatSkills;
using SemanticKernel.Service.CopilotChat.Storage;

namespace SemanticKernel.Service.CopilotChat.Controllers;

/// <summary>
/// Controller for retrieving semantic memory data of chat sessions.
/// </summary>
[ApiController]
[Authorize]
public class ChatMemoryController : ControllerBase
{
private readonly ILogger<ChatMemoryController> _logger;

private readonly PromptsOptions _promptOptions;

private readonly ChatSessionRepository _chatSessionRepository;

/// <summary>
/// Initializes a new instance of the <see cref="ChatMemoryController"/> class.
/// </summary>
/// <param name="logger">The logger.</param>
/// <param name="promptsOptions">The prompts options.</param>
/// <param name="chatSessionRepository">The chat session repository.</param>
public ChatMemoryController(
ILogger<ChatMemoryController> logger,
IOptions<PromptsOptions> promptsOptions,
ChatSessionRepository chatSessionRepository)
{
this._logger = logger;
this._promptOptions = promptsOptions.Value;
this._chatSessionRepository = chatSessionRepository;
}

/// <summary>
/// Gets the semantic memory for the chat session.
/// </summary>
/// <param name="semanticTextMemory">The semantic text memory instance.</param>
/// <param name="chatId">The chat id.</param>
/// <param name="memoryName">Name of the memory type.</param>
[HttpGet]
[Route("chatMemory/{chatId:guid}/{memoryName}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> GetSemanticMemoriesAsync(
[FromServices] ISemanticTextMemory semanticTextMemory,
[FromRoute] string chatId,
[FromRoute] string memoryName)
TaoChenOSU marked this conversation as resolved.
Show resolved Hide resolved
{
// Make sure the chat session exists.
if (!await this._chatSessionRepository.TryFindByIdAsync(chatId, v => _ = v))
{
this._logger.LogWarning("Chat session: {0} does not exist.", chatId);
return this.BadRequest($"Chat session: {chatId} does not exist.");
}

// Make sure the memory name is valid.
if (!this.ValidateMemoryName(memoryName))
{
this._logger.LogWarning("Memory name: {0} is invalid.", memoryName);
return this.BadRequest($"Memory name: {memoryName} is invalid.");
}

// Gather the requested semantic memory.
// ISemanticTextMemory doesn't support retrieving all memories.
// Will use a dummy query since we don't care about relevance. An empty string will cause exception.
// minRelevanceScore is set to 0.0 to return all memories.
TaoChenOSU marked this conversation as resolved.
Show resolved Hide resolved
List<string> memories = new();
var results = semanticTextMemory.SearchAsync(
SemanticChatMemoryExtractor.MemoryCollectionName(chatId, memoryName),
"abc",
limit: 100,
minRelevanceScore: 0.0);
TaoChenOSU marked this conversation as resolved.
Show resolved Hide resolved
await foreach (var memory in results)
{
memories.Add(memory.Metadata.Text);
}

return this.Ok(memories);
}

#region Private

/// <summary>
/// Validates the memory name.
/// </summary>
/// <param name="memoryName">Name of the memory requested.</param>
/// <returns>True if the memory name is valid.</returns>
private bool ValidateMemoryName(string memoryName)
{
return this._promptOptions.MemoryMap.ContainsKey(memoryName);
TaoChenOSU marked this conversation as resolved.
Show resolved Hide resolved
}

# endregion
}
3 changes: 2 additions & 1 deletion webapi/CopilotChat/Controllers/DocumentImportController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -155,8 +155,9 @@ public async Task<IActionResult> ImportDocumentsAsync(
}

var chatId = documentImportForm.ChatId.ToString();
var userId = documentImportForm.UserId;
await messageRelayHubContext.Clients.Group(chatId)
.SendAsync(ReceiveMessageClientCall, chatMessage, chatId);
.SendAsync(ReceiveMessageClientCall, chatId, userId, chatMessage);

return this.Ok(chatMessage);
}
Expand Down
5 changes: 5 additions & 0 deletions webapi/CopilotChat/Models/Bot.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ public class Bot
/// </summary>
public string ChatTitle { get; set; } = string.Empty;

/// <summary>
/// The system description of the chat that is used to generate responses.
/// </summary>
public string SystemDescription { get; set; } = string.Empty;

/// <summary>
/// The chat history. It contains all the messages in the conversation with the bot.
/// </summary>
Expand Down
20 changes: 19 additions & 1 deletion webapi/CopilotChat/Models/ChatSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,28 @@ public class ChatSession : IStorageEntity
[JsonPropertyName("createdOn")]
public DateTimeOffset CreatedOn { get; set; }

public ChatSession(string title)
/// <summary>
/// System description of the chat that is used to generate responses.
/// </summary>
public string SystemDescription { get; set; }

/// <summary>
/// The balance between long term memory and working term memory.
/// The higher this value, the more the system will rely on long term memory by lowering
/// the relevance threshold of long term memory and increasing the threshold score of working memory.
/// </summary>
public double MemoryBalance { get; set; } = 0.5;

/// <summary>
/// Initializes a new instance of the <see cref="ChatSession"/> class.
/// </summary>
/// <param name="title">The title of the chat.</param>
/// <param name="systemDescription">The system description of the chat.</param>
public ChatSession(string title, string systemDescription)
{
this.Id = Guid.NewGuid().ToString();
this.Title = title;
this.CreatedOn = DateTimeOffset.Now;
this.SystemDescription = systemDescription;
}
}
18 changes: 15 additions & 3 deletions webapi/CopilotChat/Options/PromptsOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,16 @@ public class PromptsOptions
internal double ExternalInformationContextWeight { get; } = 0.3;

/// <summary>
/// Minimum relevance of a semantic memory to be included in the final prompt.
/// The higher the value, the answer will be more relevant to the user intent.
/// Upper bound of the relevancy score of a semantic memory to be included in the final prompt.
/// The actual relevancy score is determined by the memory balance.
/// </summary>
internal double SemanticMemoryRelevanceUpper { get; } = 0.9;

/// <summary>
/// Lower bound of the relevancy score of a semantic memory to be included in the final prompt.
/// The actual relevancy score is determined by the memory balance.
/// </summary>
internal double SemanticMemoryMinRelevance { get; } = 0.8;
internal double SemanticMemoryRelevanceLower { get; } = 0.6;

/// <summary>
/// Minimum relevance of a document memory to be included in the final prompt.
Expand Down Expand Up @@ -156,4 +162,10 @@ public class PromptsOptions
internal double IntentTopP { get; } = 1;
internal double IntentPresencePenalty { get; } = 0.5;
internal double IntentFrequencyPenalty { get; } = 0.5;

/// <summary>
/// Copy the options in case they need to be modified per chat.
/// </summary>
/// <returns>A shallow copy of the options.</returns>
internal PromptsOptions Copy() => (PromptsOptions)this.MemberwiseClone();
}
25 changes: 23 additions & 2 deletions webapi/CopilotChat/Skills/ChatSkills/ChatSkill.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,13 @@ public ChatSkill(
this._kernel = kernel;
this._chatMessageRepository = chatMessageRepository;
this._chatSessionRepository = chatSessionRepository;
this._promptOptions = promptOptions.Value;
this._messageRelayHubContext = messageRelayHubContext;
// Clone the prompt options to avoid modifying the original prompt options.
this._promptOptions = promptOptions.Value.Copy();

this._semanticChatMemorySkill = new SemanticChatMemorySkill(
promptOptions);
promptOptions,
TaoChenOSU marked this conversation as resolved.
Show resolved Hide resolved
chatSessionRepository);
this._documentMemorySkill = new DocumentMemorySkill(
promptOptions,
documentImportOptions);
Expand Down Expand Up @@ -259,6 +261,9 @@ public async Task<SKContext> ChatAsync(
[Description("ID of the response message for planner"), DefaultValue(null), SKName("responseMessageId")] string? messageId,
SKContext context)
{
// Set the system description in the prompt options
await SetSystemDescriptionAsync(chatId);

// Save this new message to memory such that subsequent chat responses can use it
await this.UpdateBotResponseStatusOnClient(chatId, "Saving user message to chat history");
await this.SaveNewMessageAsync(message, userId, userName, chatId, messageType);
Expand Down Expand Up @@ -721,5 +726,21 @@ private async Task UpdateBotResponseStatusOnClient(string chatId, string status)
await this._messageRelayHubContext.Clients.Group(chatId).SendAsync("ReceiveBotResponseStatus", chatId, status);
}

/// <summary>
/// Set the system description in the prompt options.
/// </summary>
/// <param name="chatId">Id of the chat session</param>
/// <exception cref="ArgumentException">Throw if the chat session does not exist.</exception>
private async Task SetSystemDescriptionAsync(string chatId)
{
ChatSession? chatSession = null;
if (!await this._chatSessionRepository.TryFindByIdAsync(chatId, v => chatSession = v))
{
throw new ArgumentException("Chat session does not exist.");
}

this._promptOptions.SystemDescription = chatSession!.SystemDescription;
}

# endregion
}
Original file line number Diff line number Diff line change
Expand Up @@ -123,11 +123,12 @@ internal static async Task CreateMemoryAsync(
{
var memoryCollectionName = SemanticChatMemoryExtractor.MemoryCollectionName(chatId, memoryName);

// Search if there is already a memory item that has a high similarity score with the new item.
var memories = await context.Memory.SearchAsync(
collection: memoryCollectionName,
query: item.ToFormattedString(),
limit: 1,
minRelevanceScore: options.SemanticMemoryMinRelevance,
minRelevanceScore: options.SemanticMemoryRelevanceUpper,
TaoChenOSU marked this conversation as resolved.
Show resolved Hide resolved
cancellationToken: context.CancellationToken
)
.ToListAsync()
Expand Down
Loading