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

Copilot Chat: Show live updates from ChatSkill #2103

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from 4 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
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public class ChatController : ControllerBase, IDisposable
private const string ChatSkillName = "ChatSkill";
private const string ChatFunctionName = "Chat";
private const string ReceiveResponseClientCall = "ReceiveResponse";
private const string GeneratingResponseClientCall = "ReceiveBotTypingState";
private const string GeneratingResponseClientCall = "ReceiveBotResponseStatus";

public ChatController(ILogger<ChatController> logger, ITelemetryService telemetryService)
{
Expand Down Expand Up @@ -98,13 +98,6 @@ public async Task<IActionResult> ChatAsync(
return this.NotFound($"Failed to find {ChatSkillName}/{ChatFunctionName} on server");
}

// Broadcast bot typing state to all users
if (ask.Variables.Where(v => v.Key == "chatId").Any())
{
var chatId = ask.Variables.Where(v => v.Key == "chatId").First().Value;
await messageRelayHubContext.Clients.Group(chatId).SendAsync(GeneratingResponseClientCall, chatId, true);
}

// Run the function.
SKContext? result = null;
try
Expand Down Expand Up @@ -138,7 +131,7 @@ public async Task<IActionResult> ChatAsync(
{
var chatId = ask.Variables.Where(v => v.Key == "chatId").First().Value;
await messageRelayHubContext.Clients.Group(chatId).SendAsync(ReceiveResponseClientCall, chatSkillAskResult, chatId);
await messageRelayHubContext.Clients.Group(chatId).SendAsync(GeneratingResponseClientCall, chatId, false);
await messageRelayHubContext.Clients.Group(chatId).SendAsync(GeneratingResponseClientCall, chatId, null);
}

return this.Ok(chatSkillAskResult);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
// Copyright (c) Microsoft. All rights reserved.

using System;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.SemanticKernel;
using SemanticKernel.Service.CopilotChat.Hubs;
using SemanticKernel.Service.CopilotChat.Options;
using SemanticKernel.Service.CopilotChat.Skills.ChatSkills;
using SemanticKernel.Service.CopilotChat.Storage;
Expand Down Expand Up @@ -49,6 +51,7 @@ public static IKernel RegisterCopilotChatSkills(this IKernel kernel, IServicePro
kernel: kernel,
chatMessageRepository: sp.GetRequiredService<ChatMessageRepository>(),
chatSessionRepository: sp.GetRequiredService<ChatSessionRepository>(),
messageRelayHubContext: sp.GetRequiredService<IHubContext<MessageRelayHub>>(),
promptOptions: sp.GetRequiredService<IOptions<PromptsOptions>>(),
documentImportOptions: sp.GetRequiredService<IOptions<DocumentMemoryOptions>>(),
planner: sp.GetRequiredService<CopilotChatPlanner>(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.AI.TextCompletion;
using Microsoft.SemanticKernel.Orchestration;
using Microsoft.SemanticKernel.SkillDefinition;
using Microsoft.SemanticKernel.TemplateEngine;
using SemanticKernel.Service.CopilotChat.Hubs;
using SemanticKernel.Service.CopilotChat.Models;
using SemanticKernel.Service.CopilotChat.Options;
using SemanticKernel.Service.CopilotChat.Storage;
Expand Down Expand Up @@ -43,6 +45,11 @@ public class ChatSkill
/// </summary>
private readonly ChatSessionRepository _chatSessionRepository;

/// <summary>
/// A SignalR hub context to broadcast updates of the execution.
/// </summary>
private readonly IHubContext<MessageRelayHub> _messageRelayHubContext;

/// <summary>
/// Settings containing prompt texts.
/// </summary>
Expand Down Expand Up @@ -70,6 +77,7 @@ public ChatSkill(
IKernel kernel,
ChatMessageRepository chatMessageRepository,
ChatSessionRepository chatSessionRepository,
IHubContext<MessageRelayHub> messageRelayHubContext,
IOptions<PromptsOptions> promptOptions,
IOptions<DocumentMemoryOptions> documentImportOptions,
CopilotChatPlanner planner,
Expand All @@ -79,6 +87,7 @@ public ChatSkill(
this._chatMessageRepository = chatMessageRepository;
this._chatSessionRepository = chatSessionRepository;
this._promptOptions = promptOptions.Value;
this._messageRelayHubContext = messageRelayHubContext;

this._semanticChatMemorySkill = new SemanticChatMemorySkill(
promptOptions);
Expand Down Expand Up @@ -249,6 +258,7 @@ public async Task<SKContext> ChatAsync(
SKContext context)
{
// Save this new message to memory such that subsequent chat responses can use it
await this.UpdateResponseStatusOnClient(chatId, "Saving user message to chat history");
await this.SaveNewMessageAsync(message, userId, userName, chatId, messageType);

// Clone the context to avoid modifying the original context variables.
Expand Down Expand Up @@ -281,11 +291,13 @@ public async Task<SKContext> ChatAsync(
context.Variables.Set("prompt", prompt);

// Save this response to memory such that subsequent chat responses can use it
await this.UpdateResponseStatusOnClient(chatId, "Saving bot message to chat history");
ChatMessage botMessage = await this.SaveNewResponseAsync(response, prompt, chatId);
context.Variables.Set("messageId", botMessage.Id);
context.Variables.Set("messageType", ((int)botMessage.Type).ToString(CultureInfo.InvariantCulture));

// Extract semantic chat memory
await this.UpdateResponseStatusOnClient(chatId, "Extracting semantic chat memory");
await SemanticChatMemoryExtractor.ExtractSemanticChatMemoryAsync(
chatId,
this._kernel,
Expand All @@ -306,23 +318,27 @@ await SemanticChatMemoryExtractor.ExtractSemanticChatMemoryAsync(
private async Task<string> GetChatResponseAsync(string chatId, SKContext chatContext)
{
// 0. Get the audience
await this.UpdateResponseStatusOnClient(chatId, "Extracting audience");
var audience = await this.GetAudienceAsync(chatContext);
if (chatContext.ErrorOccurred)
{
return string.Empty;
}

// 1. Extract user intent from the conversation history.
await this.UpdateResponseStatusOnClient(chatId, "Extracting user intent");
var userIntent = await this.GetUserIntentAsync(chatContext);
if (chatContext.ErrorOccurred)
{
return string.Empty;
}

// 2. Calculate the remaining token budget.
await this.UpdateResponseStatusOnClient(chatId, "Calculating remaining token budget");
var remainingToken = this.GetChatContextTokenLimit(userIntent);

// 3. Acquire external information from planner
await this.UpdateResponseStatusOnClient(chatId, "Acquiring external information from planner");
var externalInformationTokenLimit = (int)(remainingToken * this._promptOptions.ExternalInformationContextWeight);
var planResult = await this.AcquireExternalInformationAsync(chatContext, userIntent, externalInformationTokenLimit);
if (chatContext.ErrorOccurred)
Expand All @@ -333,10 +349,12 @@ private async Task<string> GetChatResponseAsync(string chatId, SKContext chatCon
// If plan is suggested, send back to user for approval before running
if (this._externalInformationSkill.ProposedPlan != null)
{
chatContext.Variables.Set("prompt", this._externalInformationSkill.ProposedPlan.Plan.Description);
return JsonSerializer.Serialize<ProposedPlan>(this._externalInformationSkill.ProposedPlan);
}

// 4. Query relevant semantic memories
await this.UpdateResponseStatusOnClient(chatId, "Querying semantic memories");
var chatMemoriesTokenLimit = (int)(remainingToken * this._promptOptions.MemoriesResponseContextWeight);
var chatMemories = await this._semanticChatMemorySkill.QueryMemoriesAsync(userIntent, chatId, chatMemoriesTokenLimit, chatContext.Memory);
if (chatContext.ErrorOccurred)
Expand All @@ -345,6 +363,7 @@ private async Task<string> GetChatResponseAsync(string chatId, SKContext chatCon
}

// 5. Query relevant document memories
await this.UpdateResponseStatusOnClient(chatId, "Querying document memories");
var documentContextTokenLimit = (int)(remainingToken * this._promptOptions.DocumentContextWeight);
var documentMemories = await this._documentMemorySkill.QueryDocumentsAsync(userIntent, chatId, documentContextTokenLimit, chatContext.Memory);
if (chatContext.ErrorOccurred)
Expand All @@ -358,6 +377,7 @@ private async Task<string> GetChatResponseAsync(string chatId, SKContext chatCon
var chatContextTextTokenCount = remainingToken - Utilities.TokenCount(chatContextText);
if (chatContextTextTokenCount > 0)
{
await this.UpdateResponseStatusOnClient(chatId, "Extracting chat history");
var chatHistory = await this.ExtractChatHistoryAsync(chatId, chatContextTextTokenCount);
if (chatContext.ErrorOccurred)
{
Expand All @@ -381,6 +401,7 @@ private async Task<string> GetChatResponseAsync(string chatId, SKContext chatCon
skillName: nameof(ChatSkill),
description: "Complete the prompt.");

await this.UpdateResponseStatusOnClient(chatId, "Invoking the AI model");
chatContext = await completionFunction.InvokeAsync(
context: chatContext,
settings: this.CreateChatResponseCompletionSettings()
Expand Down Expand Up @@ -628,5 +649,15 @@ private int GetChatContextTokenLimit(string userIntent)
return remainingToken;
}

/// <summary>
/// Update the status of the response on the client.
/// </summary>
/// <param name="chatId">Id of the chat session</param>
/// <param name="status">Current status of the response</param>
private async Task UpdateResponseStatusOnClient(string chatId, string status)
{
await this._messageRelayHubContext.Clients.Group(chatId).SendAsync("ReceiveBotResponseStatus", chatId, status);
}

# endregion
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ import { GetResponseOptions, useChat } from '../../libs/useChat';
import { useAppDispatch, useAppSelector } from '../../redux/app/hooks';
import { RootState } from '../../redux/app/store';
import { addAlert } from '../../redux/features/app/appSlice';
import { editConversationInput } from '../../redux/features/conversations/conversationsSlice';
import {
editConversationInput,
updateBotResponseStatusFromServer,
} from '../../redux/features/conversations/conversationsSlice';
import { SpeechService } from './../../libs/services/SpeechService';
import { updateUserIsTyping } from './../../redux/features/conversations/conversationsSlice';
import { ChatStatus } from './ChatStatus';
Expand Down Expand Up @@ -150,6 +153,7 @@ export const ChatInput: React.FC<ChatInputProps> = ({ isDraggingOver, onDragLeav

setValue('');
dispatch(editConversationInput({ id: selectedId, newInput: '' }));
dispatch(updateBotResponseStatusFromServer({ chatId: selectedId, status: 'Calling the kernel' }));
onSubmit({ value, messageType, chatId: selectedId }).catch((error) => {
const message = `Error submitting chat input: ${(error as Error).message}`;
log(message);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,7 @@ export const ChatStatus: React.FC = () => {
React.useEffect(() => {
const checkAreTyping = () => {
const updatedTypingUsers: IChatUser[] = users.filter(
(chatUser: IChatUser) =>
chatUser.id !== activeUserInfo?.id &&
chatUser.isTyping,
(chatUser: IChatUser) => chatUser.id !== activeUserInfo?.id && chatUser.isTyping,
);

setTypingUserList(updatedTypingUsers);
Expand All @@ -26,6 +24,9 @@ export const ChatStatus: React.FC = () => {
}, [activeUserInfo, users]);

return (
<TypingIndicatorRenderer isBotTyping={conversations[selectedId].isBotTyping} numberOfUsersTyping={typingUserList.length} />
<TypingIndicatorRenderer
botResponseStatus={conversations[selectedId].botResponseStatus}
numberOfUsersTyping={typingUserList.length}
/>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -13,26 +13,23 @@ const useClasses = makeStyles({
});

interface TypingIndicatorRendererProps {
isBotTyping: boolean;
botResponseStatus: string | undefined;
numberOfUsersTyping: number;
}

export const TypingIndicatorRenderer: React.FC<TypingIndicatorRendererProps> = ({ isBotTyping, numberOfUsersTyping }) => {
export const TypingIndicatorRenderer: React.FC<TypingIndicatorRendererProps> = ({
botResponseStatus,
numberOfUsersTyping,
}) => {
const classes = useClasses();

let message = '';
if (isBotTyping) {
if (numberOfUsersTyping === 0) {
message = 'Bot is typing';
} else if (numberOfUsersTyping === 1) {
message = 'Bot and 1 user are typing';
} else {
message = `Bot and ${numberOfUsersTyping} users are typing`;
}
} else if (numberOfUsersTyping === 1) {
message = '1 user is typing';
let message = botResponseStatus;
if (numberOfUsersTyping === 1) {
message = message ? `${message} and 1 user is typing` : '1 user is typing';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: instead of saying "1 user", maybe write "a user"?

Suggested change
message = message ? `${message} and 1 user is typing` : '1 user is typing';
message = message ? `${message} and a user is typing` : 'A user is typing';

} else if (numberOfUsersTyping > 1) {
message = `${numberOfUsersTyping} users are typing`;
message = message
? `${message} and ${numberOfUsersTyping} users are typing`
: `${numberOfUsersTyping} users are typing`;
}

if (!message) {
Expand All @@ -46,5 +43,9 @@ export const TypingIndicatorRenderer: React.FC<TypingIndicatorRendererProps> = (
</div>
);

return <Animation name="slideInCubic" keyframeParams={{ distance: '2.4rem' }}>{typingIndicator}</Animation>;
return (
<Animation name="slideInCubic" keyframeParams={{ distance: '2.4rem' }}>
{typingIndicator}
</Animation>
);
};
8 changes: 4 additions & 4 deletions samples/apps/copilot-chat-app/webapp/src/libs/useChat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export const useChat = () => {
users: [loggedInUser],
botProfilePicture: getBotProfilePicture(Object.keys(conversations).length),
input: '',
isBotTyping: false,
botResponseStatus: undefined,
userDataLoaded: false,
};

Expand Down Expand Up @@ -153,7 +153,7 @@ export const useChat = () => {
messages: chatMessages,
botProfilePicture: getBotProfilePicture(Object.keys(loadedConversations).length),
input: '',
isBotTyping: false,
botResponseStatus: undefined,
userDataLoaded: false,
};
}
Expand Down Expand Up @@ -198,7 +198,7 @@ export const useChat = () => {
users: [loggedInUser],
messages: chatMessages,
botProfilePicture: getBotProfilePicture(Object.keys(conversations).length),
isBotTyping: false,
botResponseStatus: undefined,
};

dispatch(addConversation(newChat));
Expand Down Expand Up @@ -268,7 +268,7 @@ export const useChat = () => {
users: chatUsers,
botProfilePicture: getBotProfilePicture(Object.keys(conversations).length),
input: '',
isBotTyping: false,
botResponseStatus: undefined,
userDataLoaded: false,
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@ export interface ChatState {
botProfilePicture: string;
lastUpdatedTimestamp?: number;
input: string;
isBotTyping: boolean;
botResponseStatus: string | undefined;
userDataLoaded: boolean;
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
Conversations,
ConversationsState,
ConversationTitleChange,
initialState
initialState,
} from './ConversationsState';

export const conversationsSlice: Slice<ConversationsState> = createSlice({
Expand Down Expand Up @@ -104,13 +104,13 @@ export const conversationsSlice: Slice<ConversationsState> = createSlice({
const { userId, chatId, isTyping } = action.payload;
updateUserTypingState(state, userId, chatId, isTyping);
},
updateBotIsTypingFromServer: (
updateBotResponseStatusFromServer: (
state: ConversationsState,
action: PayloadAction<{ chatId: string; isTyping: boolean }>,
action: PayloadAction<{ chatId: string; status: string }>,
) => {
const { chatId, isTyping } = action.payload;
const { chatId, status } = action.payload;
const conversation = state.conversations[chatId];
conversation.isBotTyping = isTyping;
conversation.botResponseStatus = status;
},
},
});
Expand All @@ -126,6 +126,7 @@ export const {
updateMessageState,
updateUserIsTyping,
updateUserIsTypingFromServer,
updateBotResponseStatusFromServer,
setUsersLoaded,
} = conversationsSlice.actions;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const enum SignalRCallbackMethods {
ReceiveResponse = 'ReceiveResponse',
UserJoined = 'UserJoined',
ReceiveUserTypingState = 'ReceiveUserTypingState',
ReceiveBotTypingState = 'ReceiveBotTypingState',
ReceiveBotResponseStatus = 'ReceiveBotResponseStatus',
GlobalDocumentUploaded = 'GlobalDocumentUploaded',
ChatDocumentUploaded = 'ChatDocumentUploaded',
ChatEdited = 'ChatEdited',
Expand Down Expand Up @@ -196,8 +196,8 @@ export const registerSignalREvents = (store: Store) => {
},
);

hubConnection.on(SignalRCallbackMethods.ReceiveBotTypingState, (chatId: string, isTyping: boolean) => {
store.dispatch({ type: 'conversations/updateBotIsTypingFromServer', payload: { chatId, isTyping } });
hubConnection.on(SignalRCallbackMethods.ReceiveBotResponseStatus, (chatId: string, status: string) => {
store.dispatch({ type: 'conversations/updateBotResponseStatusFromServer', payload: { chatId, status } });
});

hubConnection.on(SignalRCallbackMethods.GlobalDocumentUploaded, (fileNames: string, userName: string) => {
Expand Down
Loading