Skip to content

Commit

Permalink
Add persona tab (microsoft#35)
Browse files Browse the repository at this point in the history
### Motivation and Context

<!-- Thank you for your contribution to the copilot-chat repo!
Please help reviewers and future users, providing the following
information:
  1. Why is this change required?
  2. What problem does it solve?
  3. What scenario does it contribute to?
  4. If it fixes an open issue, please link to the issue here.
-->
Add a persona tab to show/enable the following:
1. Meta prompt.
2. Meta prompt editing,
3. Memory (long term & short term) content.
4. Memory bias slider.

### Description
1. Webapi support for editing the meta prompt.
2. Webapi support for retrieving memory content (ChatMemoryController).
3. Webapi support for setting memory bias.
4. Webapp support for showing and enabling the features.
5. Update the initial bot greeting message.

![image](https://github.com/microsoft/chat-copilot/assets/12570346/8ac7f817-bcab-4b71-98c7-03f3afc3b8f9)

<!-- Describe your changes, the overall approach, the underlying design.
These notes will help understanding how your code works. Thanks! -->

### Contribution Checklist

<!-- Before submitting this PR, please make sure: -->

- [ ] The code builds clean without any errors or warnings
- [ ] The PR follows the [Contribution
Guidelines](https://github.com/microsoft/copilot-chat/blob/main/CONTRIBUTING.md)
and the [pre-submission formatting
script](https://github.com/microsoft/copilot-chat/blob/main/CONTRIBUTING.md#development-scripts)
raises no violations
- [ ] All unit tests pass, and I have added new tests where possible
- [ ] I didn't break anyone 😄
  • Loading branch information
TaoChenOSU authored Jul 27, 2023
1 parent 533e591 commit 09ac9ce
Show file tree
Hide file tree
Showing 26 changed files with 936 additions and 406 deletions.
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;

// 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)
{
// 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.
List<string> memories = new();
var results = semanticTextMemory.SearchAsync(
SemanticChatMemoryExtractor.MemoryCollectionName(chatId, memoryName),
"abc",
limit: 100,
minRelevanceScore: 0.0);
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);
}

# 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,
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,
cancellationToken: context.CancellationToken
)
.ToListAsync()
Expand Down
Loading

0 comments on commit 09ac9ce

Please sign in to comment.