Skip to content

Commit

Permalink
Enhance MemoryContext and UI components
Browse files Browse the repository at this point in the history
DETAILS

- Updated `MemoryContext.cs` with a new `SystemPrompt` constant, improved initialization logic, and enhanced logging for better clarity.
- Refined search functionality for more detailed responses and better error handling.
- Added logging in `Program.cs` to track the initialization of the vector database.
- Modified `SearchResponse.cs` to include a constructor that initializes the `Products` list.
- Improved UI in `Home.razor` with conditional rendering for the microphone component, a new button for live chat, and a "Realtime Audio Log" section.
- Enhanced chat message handling by adding timestamps and improving microphone state management.
- Updated `chat.css` to include styles for a new `.btn-purple` class, improving the visual design.
  • Loading branch information
elbruno committed Feb 21, 2025
1 parent 034b98f commit fb86b42
Show file tree
Hide file tree
Showing 5 changed files with 121 additions and 70 deletions.
118 changes: 57 additions & 61 deletions src/Products/Memory/MemoryContext.cs
Original file line number Diff line number Diff line change
@@ -1,24 +1,26 @@
using Microsoft.EntityFrameworkCore;
using SearchEntities;
using DataEntities;
using OpenAI.Chat;
using OpenAI.Embeddings;
using VectorEntities;
using Microsoft.SemanticKernel.Connectors.InMemory;
using DataEntities;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.VectorData;
using Microsoft.SemanticKernel.Connectors.InMemory;
using Newtonsoft.Json;
using OpenAI.Chat;
using OpenAI.Embeddings;
using Products.Models;
using SearchEntities;
using System.Text;
using VectorEntities;

namespace Products.Memory;

public class MemoryContext
{
private ILogger _logger;
public ChatClient? _chatClient;
public EmbeddingClient? _embeddingClient;
public IVectorStoreRecordCollection<int, ProductVector> _productsCollection;
private string _systemPrompt = "";
private bool _isMemoryCollectionInitialized = false;
private const string SystemPrompt = "You are a useful assistant. You always reply with a short and funny message. If you do not know an answer, you say 'I don't know that.' You only answer questions related to outdoor camping products. For any other type of questions, explain to the user that you only answer outdoor camping products questions. Do not store memory of the chat conversation.";

private readonly ILogger _logger;
private readonly ChatClient? _chatClient;
private readonly EmbeddingClient? _embeddingClient;
private IVectorStoreRecordCollection<int, ProductVector>? _productsCollection;
private bool _isMemoryCollectionInitialized;

public MemoryContext(ILogger logger, ChatClient? chatClient, EmbeddingClient? embeddingClient)
{
Expand All @@ -33,16 +35,18 @@ public MemoryContext(ILogger logger, ChatClient? chatClient, EmbeddingClient? em

public async Task<bool> InitMemoryContextAsync(Context db)
{
if (_isMemoryCollectionInitialized)
{
_logger.LogInformation("Memory context already initialized");
return true;
}

_logger.LogInformation("Initializing memory context");
var vectorProductStore = new InMemoryVectorStore();
_productsCollection = vectorProductStore.GetCollection<int, ProductVector>("products");
await _productsCollection.CreateCollectionIfNotExistsAsync();

// define system prompt
_systemPrompt = "You are a useful assistant. You always reply with a short and funny message. If you do not know an answer, you say 'I don't know that.' You only answer questions related to outdoor camping products. For any other type of questions, explain to the user that you only answer outdoor camping products questions. Do not store memory of the chat conversation.";

_logger.LogInformation("Get a copy of the list of products");
// get a copy of the list of products
var products = await db.Product.ToListAsync();

_logger.LogInformation("Filling products in memory");
Expand All @@ -64,18 +68,20 @@ public async Task<bool> InitMemoryContextAsync(Context db)
Price = product.Price,
ImageUrl = product.ImageUrl
};
var result = await _embeddingClient.GenerateEmbeddingAsync(productInfo);
var result = await _embeddingClient!.GenerateEmbeddingAsync(productInfo);

productVector.Vector = result.Value.ToFloats();
var recordId = await _productsCollection.UpsertAsync(productVector);
_logger.LogInformation("Product added to memory: {Product} with recordId: {RecordId}", product.Name, recordId);
}
catch (Exception exc)
{
_logger.LogError(exc, "Error adding product to memory");
_logger.LogError(exc, $"Error adding product {product.Name} to memory");
return false;
}
}

_isMemoryCollectionInitialized = true;
_logger.LogInformation("DONE! Filling products in memory");
return true;
}
Expand All @@ -85,73 +91,63 @@ public async Task<SearchResponse> Search(string search, Context db)
if (!_isMemoryCollectionInitialized)
{
await InitMemoryContextAsync(db);
_isMemoryCollectionInitialized = true;
}

var response = new SearchResponse();
response.Response = $"I don't know the answer for your question. Your question is: [{search}]";
Product? firstProduct = new Product();
var responseText = "";
var response = new SearchResponse
{
Response = $"I don't know the answer for your question. Your question is: [{search}]"
};
try
{
var result = await _embeddingClient.GenerateEmbeddingAsync(search);
var result = await _embeddingClient!.GenerateEmbeddingAsync(search);
var vectorSearchQuery = result.Value.ToFloats();

var searchOptions = new VectorSearchOptions()
var searchOptions = new VectorSearchOptions
{
Top = 1,
Top = 2,
VectorPropertyName = "Vector"
};

// search the vector database for the most similar product
var searchResults = await _productsCollection.VectorizedSearchAsync(vectorSearchQuery, searchOptions);
double searchScore = 0.0;
var searchResults = await _productsCollection!.VectorizedSearchAsync(vectorSearchQuery, searchOptions);
var sbFoundProducts = new StringBuilder();
int productPosition = 1;
await foreach (var searchItem in searchResults.Results)
{
if (searchItem.Score > 0.5)
{
// product found, search the db for the product details
firstProduct = new Product
var foundProduct = await db.FindAsync<Product>(searchItem.Record.Id);
if (foundProduct != null)
{
Id = searchItem.Record.Id,
Name = searchItem.Record.Name,
Description = searchItem.Record.Description,
Price = searchItem.Record.Price,
ImageUrl = searchItem.Record.ImageUrl
};

searchScore = searchItem.Score.Value;
responseText = $"The product [{firstProduct.Name}] fits with the search criteria [{search}][{searchItem.Score.Value.ToString("0.00")}]";
_logger.LogInformation($"Search Response: {responseText}");
response.Products.Add(foundProduct);
sbFoundProducts.AppendLine($"- Product {productPosition}:");
sbFoundProducts.AppendLine($" - Name: {foundProduct.Name}");
sbFoundProducts.AppendLine($" - Description: {foundProduct.Description}");
sbFoundProducts.AppendLine($" - Price: {foundProduct.Price}");
productPosition++;
}
}
}

// let's improve the response message
var prompt = @$"You are an intelligent assistant helping clients with their search about outdoor products. Generate a catchy and friendly message using the following information:
var prompt = @$"You are an intelligent assistant helping clients with their search about outdoor products.
Generate a catchy and friendly message using the information below.
Add a comparison between the products found and the search criteria.
Include products details.
- User Question: {search}
- Found Product Name: {firstProduct.Name}
- Found Product Description: {firstProduct.Description}
- Found Product Price: {firstProduct.Price}
Include the found product information in the response to the user question.";
- Found Products:
{sbFoundProducts}";

var messages = new List<ChatMessage>
{
new SystemChatMessage(_systemPrompt),
new UserChatMessage(prompt)
};

_logger.LogInformation("{ChatHistory}", JsonConvert.SerializeObject(messages));

var resultPrompt = await _chatClient.CompleteChatAsync(messages);
responseText = resultPrompt.Value.Content[0].Text!;

// create a response object
response = new SearchResponse
{
Products = firstProduct == null ? [new Product()] : [firstProduct],
Response = responseText
new SystemChatMessage(SystemPrompt),
new UserChatMessage(prompt)
};

_logger.LogInformation("{ChatHistory}", JsonConvert.SerializeObject(messages));

var resultPrompt = await _chatClient!.CompleteChatAsync(messages);
response.Response = resultPrompt.Value.Content[0].Text!;
}
catch (Exception ex)
{
Expand All @@ -160,4 +156,4 @@ public async Task<SearchResponse> Search(string search, Context db)
}
return response;
}
}
}
5 changes: 5 additions & 0 deletions src/Products/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,11 @@
app.Logger.LogError(exc, "Error creating database");
}
DbInitializer.Initialize(context);

app.Logger.LogInformation("Start fill products in vector db");
var memoryContext = app.Services.GetRequiredService<MemoryContext>();
await memoryContext.InitMemoryContextAsync(context);
app.Logger.LogInformation("Done fill products in vector db");
}

app.Run();
5 changes: 5 additions & 0 deletions src/SearchEntities/SearchResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ namespace SearchEntities;

public class SearchResponse
{
public SearchResponse()
{
Products = new List<DataEntities.Product>();
}

[JsonPropertyName("id")]
public string? Response { get; set; }

Expand Down
44 changes: 35 additions & 9 deletions src/StoreRealtime/Components/Pages/Home.razor
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,11 @@

<PageTitle>eShopLite - Realtime chat support</PageTitle>

@* AUDIO DEVICES *@
<Microphone OnMicAvailable="@OnMicAvailable" />
@if (isMicActive)
{
<Microphone OnMicAvailable="@OnMicAvailable" />
}

<Speaker @ref="@speaker" />

<div class="net-brand-page">
Expand All @@ -38,11 +41,16 @@
</div>
}
</div>
<button id="ButtonChatStartStop" class="btn btn-purple" @onclick="ToggleMic">
@(isMicActive ? "Stop Live Chat" : "Start Live Chat")
</button>
</div>

@* REALTIME AUDIO LOG SECTION *@
<div class="info-log-section">

<h4>Realtime Audio Log</h4>
<i class="icon-top-right">🔊</i>
<ul class="info-log">
@foreach (var message in messages)
{
Expand All @@ -55,17 +63,34 @@

@code {
private ConversationManager? conversationManager;
private readonly CancellationTokenSource disposalCts = new();
private CancellationTokenSource disposalCts = new();
private bool isMicActive = false;
private Speaker? speaker;
private readonly List<LogMessage> messages = new();
private readonly List<RealtimeChatMessage> chatMessages = new();
private ElementReference mainContentSection;

private void ToggleMic()
{
if (isMicActive)
{
// Stop interaction
conversationManager?.Dispose();
isMicActive = false;
}
else
{
// Start interaction
disposalCts = new CancellationTokenSource();
isMicActive = true;
}
}

private void OnMicAvailable(PipeReader micReader)
{
conversationManager = new(RealtimeConversationClient, contosoProductContext, logger);
_ = RunAsBackgroundTask(() => conversationManager.RunAsync(micReader.AsStream(), speaker!,
AddMessageAsync, AddChatMessageAsync, disposalCts.Token));
AddMessageAsync, AddChatMessageAsync, disposalCts.Token));
}

public void Dispose()
Expand All @@ -77,10 +102,11 @@
private async Task AddChatMessageAsync(string message, bool isUserMessage)
{
var cm = new RealtimeChatMessage
{
Message = message,
IsUser = isUserMessage
};
{
Message = message,
IsUser = isUserMessage,
Timestamp = DateTime.Now
};
chatMessages.Add(cm);
await InvokeAsync(StateHasChanged);
ScrollToBottom(mainContentSection);
Expand Down Expand Up @@ -113,4 +139,4 @@
}
}

<link rel="stylesheet" href="css/chat.css" />
<link rel="stylesheet" href="css/chat.css" />
19 changes: 19 additions & 0 deletions src/StoreRealtime/wwwroot/css/chat.css
Original file line number Diff line number Diff line change
Expand Up @@ -251,3 +251,22 @@
height: 100%;
}

.btn-purple {
background-color: #512bd4; /* .NET purple */
color: white;
border: none;
padding: 10px 20px;
cursor: pointer;
border-radius: 5px;
}

.btn-purple:hover {
background-color: #3e1a9e; /* Darker .NET purple */
}

.icon-top-right {
position: absolute;
top: 10px;
right: 10px;
font-size: 24px;
}

0 comments on commit fb86b42

Please sign in to comment.