From 40b9e132c5fa69e5234f3e7351092fa8a9863ed3 Mon Sep 17 00:00:00 2001 From: Kevin Guo <105208143+kevinguo-ed@users.noreply.github.com> Date: Tue, 27 Aug 2024 16:30:49 +0800 Subject: [PATCH 01/23] Added article: SignalR with Open AI --- aspnetcore/tutorials/ai-powered-group-chat.md | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 aspnetcore/tutorials/ai-powered-group-chat.md diff --git a/aspnetcore/tutorials/ai-powered-group-chat.md b/aspnetcore/tutorials/ai-powered-group-chat.md new file mode 100644 index 000000000000..21c21d6d13e7 --- /dev/null +++ b/aspnetcore/tutorials/ai-powered-group-chat.md @@ -0,0 +1,89 @@ +--- +title: Building AI-Powered Group Chat with SignalR and OpenAI +author: kevinguo-ed +description: ... +ms.author: ... +ms.date: 08/27/2024 +--- + +## Overview + +The integration of AI into applications is rapidly becoming a must-have for developers looking to help their users be more creative, productive and achieve their health goals. AI-powered features, such as intelligent chatbots, personalized recommendations, and contextual responses, add significant value to modern apps. The AI-powered apps that came out since Chat GPT captured our imagination are primarily between one user and one AI assistant. As developers get more comfortable with the capabilities of AI, they are exploring AI-powered apps in a team's context. They ask "what value can AI add to a team of collaborators"? + +This tutorial guides you through building a real-time group chat application. Among a group of human collaborators in a chat, there's an AI assistant which has access to the chat history and can be invited to help out by any collaborator when they start the message with `@gpt`. The finished app looks like this. + +{{Chat interface missing...}} + +We use Open AI for generating intelligent, context-aware responses and SignalR for delivering the response to users in a group. You can find the complete code [in this repo](https://github.com/microsoft/SignalR-Samples-AI/tree/main/AIStreaming). + +## Dependencies +You can use either Azure Open AI or Open AI for this project. Make sure to update the `endpoint` and `key` in `appsetting.json`. `OpenAIExtensions` reads the configuration when the app starts and they are required to authenticate and use either service. + +# [Open AI](#tab/open-ai) +To build this application, you will need the following: +* ASP.NET Core: To create the web application and host the SignalR hub. +* [SignalR](https://www.nuget.org/packages/Microsoft.AspNetCore.SignalR.Client): For real-time communication between clients and the server. +* [OpenAI Client](https://www.nuget.org/packages/OpenAI/2.0.0-beta.10): To interact with OpenAI's API for generating AI responses. + +# [Azure Open AI](#tab/azure-open-ai) +To build this application, you will need the following: +* ASP.NET Core: To create the web application and host the SignalR hub. +* [SignalR](https://www.nuget.org/packages/Microsoft.AspNetCore.SignalR.Client): For real-time communication between clients and the server. +* [Azure Open AI](https://www.nuget.org/packages/Azure.AI.OpenAI/2.0.0-beta.3): Azure.AI.OpenAI +--- + +## Implementation + +In this section, we'll walk through the key parts of the code that integrate SignalR with OpenAI to create an AI-enhanced group chat experience. + +### SignalR Hub integration** + +The `GroupChatHub` class manages user connections, message broadcasting, and AI interactions. When a user sends a message starting with `@gpt`, the hub forwards it to OpenAI, which generates a response. The AI's response is streamed back to the group in real-time. +```csharp +var chatClient = _openAI.GetChatClient(_options.Model); +await foreach (var completion in chatClient.CompleteChatStreamingAsync(messagesInludeHistory)) +{ + // ... + // Buffering and sending the AI's response in chunks + await Clients.Group(groupName).SendAsync("newMessageWithId", "ChatGPT", id, totalCompletion.ToString()); + // ... +} +``` + +The chat history is managed by `GroupHistoryStore`, which stores messages for context. It includes both user and assistant messages to maintain conversation continuity. +```csharp +public void UpdateGroupHistoryForAssistant(string groupName, string message) { ... } +``` + +### Streaming AI responses + +The `CompleteChatStreamingAsync()` method streams responses from OpenAI incrementally, which allows the application to send partial responses to the client as they are generated. + +The code uses a `StringBuilder` to accumulate the AI's response. It checks the length of the buffered content and sends it to the clients when it exceeds a certain threshold (e.g., 20 characters). This approach ensures that users see the AI’s response as it forms, mimicking a human-like typing effect. +```csharp +totalCompletion.Append(content); +if (totalCompletion.Length - lastSentTokenLength > 20) +{ + await Clients.Group(groupName).SendAsync("newMessageWithId", "ChatGPT", id, totalCompletion.ToString()); + lastSentTokenLength = totalCompletion.Length; +} +``` + +### Maintaining context with history + +The `GroupHistoryStore` class manages the chat history for each group. It stores both user and AI messages, ensuring that the conversation context is preserved across interactions. This context is crucial for generating coherent AI responses. +```csharp +public void UpdateGroupHistoryForAssistant(string groupName, string message) +{ + var chatMessages = _store.GetOrAdd(groupName, _ => InitiateChatMessages()); + chatMessages.Add(new AssistantChatMessage(message)); +} +``` + +### Explore further + +This project opens up exciting possibilities for further enhancement: +1. **Advanced AI features**: Leverage other OpenAI capabilities like sentiment analysis, translation, or summarization. +2. **Incorporating multiple AI agents**: You can introduce multiple AI agents with distinct roles or expertise areas within the same chat. For example, one agent might focus on text generation, the other provides image or audio generation. This can create a richer and more dynamic user experience where different AI agents interact seamlessly with users and each other. +3. **Share chat history between server instances**: Implement a database layer to persist chat history across sessions, allowing conversations to resume even after a disconnect. Beyond SQL or NO SQL based solutions, you can also explore using a caching service like Redis. It can significantly improve performance by storing frequently accessed data, such as chat history or AI responses, in memory. This reduces latency and offloads database operations, leading to faster response times, particularly in high-traffic scenarios. +4. **Leveraging Azure SignalR Service**: [Azure SignalR Service](https://learn.microsoft.com/azure/azure-signalr/signalr-overview) provides scalable and reliable real-time messaging for your application. By offloading the SignalR backplane to Azure, you can scale out the chat application easily to support thousands of concurrent users across multiple servers. Azure SignalR also simplifies management and provides built-in features like automatic reconnections. From 9a5ed0ccaccbb9550543b10407844dda27d75f48 Mon Sep 17 00:00:00 2001 From: Kevin Guo <105208143+kevinguo-ed@users.noreply.github.com> Date: Wed, 28 Aug 2024 14:53:11 +0800 Subject: [PATCH 02/23] Added missing images and fixed formatting issues --- aspnetcore/toc.yml | 2 ++ .../ai-powered-group-chat.jpg | Bin 0 -> 47882 bytes .../ai-powered-group-chat.md | 29 ++++++++++-------- ...sequence-diagram-ai-powered-group-chat.png | Bin 0 -> 43246 bytes 4 files changed, 19 insertions(+), 12 deletions(-) create mode 100644 aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.jpg rename aspnetcore/tutorials/{ => ai-powered-group-chat}/ai-powered-group-chat.md (77%) create mode 100644 aspnetcore/tutorials/ai-powered-group-chat/sequence-diagram-ai-powered-group-chat.png diff --git a/aspnetcore/toc.yml b/aspnetcore/toc.yml index f501a843b47d..5bdd4b3cd689 100644 --- a/aspnetcore/toc.yml +++ b/aspnetcore/toc.yml @@ -874,6 +874,8 @@ items: displayName: signalr - name: Tutorials items: + - name: AI-powered group chat + uid: tutorials/ai-powered-group-chat - name: SignalR with JavaScript uid: tutorials/signalr - name: SignalR with TypeScript diff --git a/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.jpg b/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8a9d78f70f19ea1685bf713f9735617abfc89c06 GIT binary patch literal 47882 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 JrpmhKy3fLsuePfvx)F)ChrY$FoHZOl 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≠ihzJs+avYo=KiO@F!a_;3e#zvU((w+6mOCu5mpT=^(3PpcTJM0s7XYE*BInT4#X#9FZkELqe2R$6LH3QLDbh!EpWBXGI4E(u1p zyY3|m!@EPhT4CT)l!}u4R?SgvOvqSf04gdq<_%|LF?2FNHe0m9plCYoRM);6eG{bKG>$#RJDJ(wyLi4OTN0dsL> zKQXtRd3FWzXLLUg)yuv~I+O+uplCX$2S?`D!Mo}AlVhMCr`fRIyvfO(NHOiAu(WZr z*J=MAzk56^VRLefLLN+ta);)OVP3ePgg-o_zO-NHD%7lacxm0f#2X;4l!eo?GG-QN zNgKS~-q174JbvFOZV@&&lSoO@FBwQtD>n zsG5nU!;&Wc)2+*xpaDah=9K0q#Mv+F_9P7nS{}6fE1o^sTofR2xBP>(Ve$vh_Wh9> zm!$qiY=K78oi6{dt62wrq9FIByf>SpCmk;L_k|NWmAxT0KcbqP|0+#si%ZORx)14c zNlxcC7l&MCHzSjh9_y!T1xPrP-oBk0n{End-=%+$6EanymP?x{PUX&SEaCETeE!UY zFQS3>x@d45t|(+IJyZ3K8NHLK(R+K@ZDju>sTpga0hx8<*|L0ZqznmmX@9m#W(F*1 zoK5&Bt4+RGXlKMGI$N;uJQD~I8@b=7*G_TWR`?m~uEzxCAwG#o`8}6)vR7}GgVZ#H zaf0#tPVo@8t7v^jvgSB72WjPghL;z(%qn}nE2%9f^ZnyL_fyxD zlCBhf>bWyqPTQGF-rM!D$p0i?mI9r-Y~PwSKhhM)7Uw@v5?QAU7W%;}VVv<#Z`S{` z8>$4_Fy(Zg*Wq#IgvG46Z0ZhWY)toK)t`=HCSFQ)c8R|*Bj+^>pI^PAapQIUE(iE+ zFt>!vgErMd*;{$Kt_4A-M6q&tyWOyGeB6;CQQZ7v#1JT-JJ#fk6gDAJ&_4FfjhX~* zH%?|}R(;Ht={B&`>IbtIe+igcv;+zp#U(A-ucIY>66NobKY6BMlUQw$sL7MQ*h

d5 zYeNKPs*9k4%(bPf@1MHqKGSml?X>%OnUxyXmKBdDmLkA8XXoU$Cr*4HEt}0Vt@NIk`rI zBFogKm9wBZU10vlIRT20(2Hib^R4>MOLSF?S1eV&$^B4x^_iJ^c3hyd^-{WEor&XI6B-3CFF7k111Uq-mbrF*cfeT#*#|>Vn!W2Qruy8D6^K zWDvH%xx504Ne!kcfRDM;;$K^ZCHby4dX}lV%7x=IGoaF&3z_Q%(dw3&>2c^Wch8G= z#7Hj&$*wU?Dfe(cDeWRRd!nng`#{Xe`q!Y(es=!UyKDCfPc~!e0A|qf#Um?|_GI>$ zA6p!-`xVReiLYuT%$X9KH0vOsuF!j!rCQpT~`7<*-PE^WZt-e8 zNWJd66I!CWU!dHql9{rOenTxS=Hv>$!e@%`T=ABty8VIguvV(x zvb_RebH!LUn^sHdm-&m|`uo&@f=nxGmYKK~pysmG;q#AV8$|>fsg5+jE@bzs&*fTWpBVKNtxU9;Pu{uA z)M~=ef{tGz7TS2v6;f}KiXz?ABX29z`jCoM?wFh#lpnFme(0d-UQwyths_s~**Mnq zRUl1N+SeleMx=pX%j7Pp4dN_C?2qzshQ=ZCOE0U;@rw%*r-P z4i<%AHfIwLGV z@(+6qvoGevYlfIBIo%;ATH83Ev`%uTX2}P+>;-U^>C4`@-6K6;^-amVo2Okj`RjIt zS_7eb#Sp*IUkMt5)Nmc z`hqP7u7<7S66t+W^57_a>gMy3M&ZW42ax=h6H7t#u=-H_r#XHk+&zPqs#`f#Ro!2r zhZU$4oll+o{}L`#LGxR-<#X`Q5rUkzN@E2K$i@Mk9NFsWkB*=J1EN zp8o3t7$ZURcvexp`Ij_(7N(h+P(^~N5vy4S8MX&z)UWWhIfLb?Y+(Fo>a^WH{ zRIP~#xE0KWcQV~(`@GKZO|HE?>A;s6^Sa)Dxqo@JZvWM~)atcc%;#VENG^vPbYgPTq%ECKq-Jid>rJ8i=h^ArfEgr|o&R_SlL^(HnUXl6e$_tUp zi`>u80TWf>A zL~e-onPp+P@5hJLFK%u2F3HjlAE{i+6-=iNmrdfDoO`5&Tr=RojtfWgVnSttR z;amTSg{d;#JdyByO|xnKd_7*(9`2VFYeg=7s9nuj(vZFMYL3;1zI*Xj1&_lwzcQ_> zUnyM}bJlF54q_OOk#maD1&icUOS7JrBP9r-4A6-&+zB%w2lar{0Q3xO@ES5${5Ws} zBPW{S^cp0+y}rOScjImLTcnf|1w5zb4pT%LQu@ls0Y>$)y!dS7J$Il}pv4XtL6)RJ zhUg%t2>p&i-ohsaT13S#EfOj5OMz}2-gx`^E%-bJa3UMDsDpLG8OY{NuoExb04BQL zyZf&BA&otPj=j-sNDV^-OXD%n{daG^Z7oCcm@a76f>CEPQeHAK0|ry__Vrt#Q|BNj jj|San(9t^8HU8(9+S;^sMwS5&0}yz+`njxgN@xNAfwCaU literal 0 HcmV?d00001 From cb9ddee10ef443bb19067183e2670de693fb8496 Mon Sep 17 00:00:00 2001 From: Kevin Guo <105208143+kevinguo-ed@users.noreply.github.com> Date: Wed, 4 Sep 2024 14:32:18 +0800 Subject: [PATCH 03/23] Added more details about managing chat history --- .../ai-powered-group-chat.md | 30 +++++++++++-------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md b/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md index fbc5b05dc611..53e8a0846f47 100644 --- a/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md +++ b/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md @@ -55,9 +55,24 @@ await foreach (var completion in chatClient.CompleteChatStreamingAsync(messagesI } ``` -The chat history is managed by `GroupHistoryStore`, which stores messages for context. It includes both user and assistant messages to maintain conversation continuity. +### Maintaining context with history + +Every request to [Open AI's Chat Completions API](https://platform.openai.com/docs/guides/chat-completions) is stateless - Open AI doesn't store past interactions. In a chat application, what a user or an assistant has said is important for generating a response that's contextually relevant. We can achieve this by including chat history in every request to the Completions API. + +The `GroupHistoryStore` class manages chat history for each group. It stores messages posted by both the users and AI assistants, ensuring that the conversation context is preserved across interactions. This context is crucial for generating coherent AI responses. + +```csharp +// Store message generated by AI-assistant in memory +public void UpdateGroupHistoryForAssistant(string groupName, string message) +{ + var chatMessages = _store.GetOrAdd(groupName, _ => InitiateChatMessages()); + chatMessages.Add(new AssistantChatMessage(message)); +} +``` + ```csharp -public void UpdateGroupHistoryForAssistant(string groupName, string message) { ... } +// Store message generated by users in memory +_history.GetOrAddGroupHistory(groupName, userName, message); ``` ### Streaming AI responses @@ -74,17 +89,6 @@ if (totalCompletion.Length - lastSentTokenLength > 20) } ``` -### Maintaining context with history - -The `GroupHistoryStore` class manages the chat history for each group. It stores both user and AI messages, ensuring that the conversation context is preserved across interactions. This context is crucial for generating coherent AI responses. -```csharp -public void UpdateGroupHistoryForAssistant(string groupName, string message) -{ - var chatMessages = _store.GetOrAdd(groupName, _ => InitiateChatMessages()); - chatMessages.Add(new AssistantChatMessage(message)); -} -``` - ## Explore further This project opens up exciting possibilities for further enhancement: From e77a97323573929b15eef86483df601010a073a3 Mon Sep 17 00:00:00 2001 From: Wade Pickett Date: Tue, 25 Feb 2025 12:36:29 -0800 Subject: [PATCH 04/23] Update aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md Will be pulling all commits into a new PR. Commiting suggestions here before that transfer starts. Co-authored-by: Luke Latham <1622880+guardrex@users.noreply.github.com> --- .../tutorials/ai-powered-group-chat/ai-powered-group-chat.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md b/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md index 53e8a0846f47..b68c99299403 100644 --- a/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md +++ b/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md @@ -18,7 +18,7 @@ This tutorial guides you through building a real-time group chat application. Am We use OpenAI for generating intelligent, context-aware responses and SignalR for delivering the response to users in a group. You can find the complete code [in this repo](https://github.com/microsoft/SignalR-Samples-AI/tree/main/AIStreaming). ## Dependencies -You can use either Azure OpenAI or OpenAI for this project. Make sure to update the `endpoint` and `key` in `appsetting.json`. `OpenAIExtensions` reads the configuration when the app starts and they are required to authenticate and use either service. +You can use either Azure OpenAI or OpenAI for this project. Make sure to update the `endpoint` and `key` in `appsettings.json`. `OpenAIExtensions` reads the configuration when the app starts, and the configuration values for `endpoint` and `key` are required to authenticate and use either service. # [OpenAI](#tab/open-ai) To build this application, you will need the following: From 82f01f59aa8d007e3211d61466dd9f9d79d021f1 Mon Sep 17 00:00:00 2001 From: Wade Pickett Date: Tue, 25 Feb 2025 12:40:12 -0800 Subject: [PATCH 05/23] Update aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md Co-authored-by: David Pine --- .../ai-powered-group-chat/ai-powered-group-chat.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md b/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md index b68c99299403..0986c9d080e0 100644 --- a/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md +++ b/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md @@ -93,6 +93,6 @@ if (totalCompletion.Length - lastSentTokenLength > 20) This project opens up exciting possibilities for further enhancement: 1. **Advanced AI features**: Leverage other OpenAI capabilities like sentiment analysis, translation, or summarization. -2. **Incorporating multiple AI agents**: You can introduce multiple AI agents with distinct roles or expertise areas within the same chat. For example, one agent might focus on text generation, the other provides image or audio generation. This can create a richer and more dynamic user experience where different AI agents interact seamlessly with users and each other. -3. **Share chat history between server instances**: Implement a database layer to persist chat history across sessions, allowing conversations to resume even after a disconnect. Beyond SQL or NO SQL based solutions, you can also explore using a caching service like Redis. It can significantly improve performance by storing frequently accessed data, such as chat history or AI responses, in memory. This reduces latency and offloads database operations, leading to faster response times, particularly in high-traffic scenarios. -4. **Leveraging Azure SignalR Service**: [Azure SignalR Service](https://learn.microsoft.com/azure/azure-signalr/signalr-overview) provides scalable and reliable real-time messaging for your application. By offloading the SignalR backplane to Azure, you can scale out the chat application easily to support thousands of concurrent users across multiple servers. Azure SignalR also simplifies management and provides built-in features like automatic reconnections. +1. **Incorporating multiple AI agents**: You can introduce multiple AI agents with distinct roles or expertise areas within the same chat. For example, one agent might focus on text generation, the other provides image or audio generation. This can create a richer and more dynamic user experience where different AI agents interact seamlessly with users and each other. +1. **Share chat history between server instances**: Implement a database layer to persist chat history across sessions, allowing conversations to resume even after a disconnect. Beyond SQL or NO SQL based solutions, you can also explore using a caching service like Redis. It can significantly improve performance by storing frequently accessed data, such as chat history or AI responses, in memory. This reduces latency and offloads database operations, leading to faster response times, particularly in high-traffic scenarios. +1. **Leveraging Azure SignalR Service**: [Azure SignalR Service](/azure/azure-signalr/signalr-overview) provides scalable and reliable real-time messaging for your application. By offloading the SignalR backplane to Azure, you can scale out the chat application easily to support thousands of concurrent users across multiple servers. Azure SignalR also simplifies management and provides built-in features like automatic reconnections. From dabdfd6c4c8a70032d419f6582647b0139478df0 Mon Sep 17 00:00:00 2001 From: Wade Pickett Date: Tue, 25 Feb 2025 12:44:21 -0800 Subject: [PATCH 06/23] Update aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md Co-authored-by: David Pine --- .../tutorials/ai-powered-group-chat/ai-powered-group-chat.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md b/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md index 0986c9d080e0..003ce0cd1b04 100644 --- a/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md +++ b/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md @@ -57,7 +57,7 @@ await foreach (var completion in chatClient.CompleteChatStreamingAsync(messagesI ### Maintaining context with history -Every request to [Open AI's Chat Completions API](https://platform.openai.com/docs/guides/chat-completions) is stateless - Open AI doesn't store past interactions. In a chat application, what a user or an assistant has said is important for generating a response that's contextually relevant. We can achieve this by including chat history in every request to the Completions API. +Every request to [Open AI's Chat Completions API](https://platform.openai.com/docs/guides/chat-completions) is stateless—Open AI doesn't store past interactions. In a chat app, what a user or an assistant has said is important for generating a response that's contextually relevant. To achieve this, include chat history in every request to the Completions API. The `GroupHistoryStore` class manages chat history for each group. It stores messages posted by both the users and AI assistants, ensuring that the conversation context is preserved across interactions. This context is crucial for generating coherent AI responses. From c43892e8f5415350ba4b986fefde93268ded009e Mon Sep 17 00:00:00 2001 From: Wade Pickett Date: Tue, 25 Feb 2025 12:44:45 -0800 Subject: [PATCH 07/23] Update aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md Co-authored-by: David Pine --- .../ai-powered-group-chat/ai-powered-group-chat.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md b/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md index 003ce0cd1b04..5f7a37f11636 100644 --- a/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md +++ b/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md @@ -82,9 +82,15 @@ The `CompleteChatStreamingAsync()` method streams responses from OpenAI incremen The code uses a `StringBuilder` to accumulate the AI's response. It checks the length of the buffered content and sends it to the clients when it exceeds a certain threshold (e.g., 20 characters). This approach ensures that users see the AI’s response as it forms, mimicking a human-like typing effect. ```csharp totalCompletion.Append(content); + if (totalCompletion.Length - lastSentTokenLength > 20) { - await Clients.Group(groupName).SendAsync("newMessageWithId", "ChatGPT", id, totalCompletion.ToString()); + await Clients.Group(groupName).SendAsync( + "newMessageWithId", + "ChatGPT", + id, + totalCompletion.ToString()); + lastSentTokenLength = totalCompletion.Length; } ``` From d9db27513d9c0025fb86431a578ecbe0010c23d4 Mon Sep 17 00:00:00 2001 From: Wade Pickett Date: Tue, 25 Feb 2025 12:46:04 -0800 Subject: [PATCH 08/23] Update aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md Co-authored-by: David Pine --- .../tutorials/ai-powered-group-chat/ai-powered-group-chat.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md b/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md index 5f7a37f11636..1ee7f86053b2 100644 --- a/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md +++ b/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md @@ -77,7 +77,7 @@ _history.GetOrAddGroupHistory(groupName, userName, message); ### Streaming AI responses -The `CompleteChatStreamingAsync()` method streams responses from OpenAI incrementally, which allows the application to send partial responses to the client as they are generated. +The `CompleteChatStreamingAsync()` method streams responses from OpenAI incrementally, which allows the app to send partial responses to the client as they are generated. The code uses a `StringBuilder` to accumulate the AI's response. It checks the length of the buffered content and sends it to the clients when it exceeds a certain threshold (e.g., 20 characters). This approach ensures that users see the AI’s response as it forms, mimicking a human-like typing effect. ```csharp From ab6d9ad49bbb18061dd65abfcdca15248aa3a982 Mon Sep 17 00:00:00 2001 From: Wade Pickett Date: Tue, 25 Feb 2025 12:46:29 -0800 Subject: [PATCH 09/23] Update aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md Co-authored-by: David Pine --- .../tutorials/ai-powered-group-chat/ai-powered-group-chat.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md b/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md index 1ee7f86053b2..ade536f5e807 100644 --- a/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md +++ b/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md @@ -75,7 +75,7 @@ public void UpdateGroupHistoryForAssistant(string groupName, string message) _history.GetOrAddGroupHistory(groupName, userName, message); ``` -### Streaming AI responses +### Stream AI responses The `CompleteChatStreamingAsync()` method streams responses from OpenAI incrementally, which allows the app to send partial responses to the client as they are generated. From cb2b2917fcffda48bd52a2b5e64206bfcc40d756 Mon Sep 17 00:00:00 2001 From: Wade Pickett Date: Tue, 25 Feb 2025 12:47:00 -0800 Subject: [PATCH 10/23] Update aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md Co-authored-by: David Pine --- .../tutorials/ai-powered-group-chat/ai-powered-group-chat.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md b/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md index ade536f5e807..583bd1b0ba15 100644 --- a/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md +++ b/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md @@ -55,7 +55,7 @@ await foreach (var completion in chatClient.CompleteChatStreamingAsync(messagesI } ``` -### Maintaining context with history +### Maintain context with history Every request to [Open AI's Chat Completions API](https://platform.openai.com/docs/guides/chat-completions) is stateless—Open AI doesn't store past interactions. In a chat app, what a user or an assistant has said is important for generating a response that's contextually relevant. To achieve this, include chat history in every request to the Completions API. From 6d533d529e708aebe2c8d820fe1204f0073bbd97 Mon Sep 17 00:00:00 2001 From: Wade Pickett Date: Tue, 25 Feb 2025 12:47:24 -0800 Subject: [PATCH 11/23] Update aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md Co-authored-by: David Pine --- .../ai-powered-group-chat/ai-powered-group-chat.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md b/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md index 583bd1b0ba15..614ee7a0c427 100644 --- a/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md +++ b/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md @@ -46,11 +46,17 @@ In this section, we'll walk through the key parts of the code that integrate Sig The `GroupChatHub` class manages user connections, message broadcasting, and AI interactions. When a user sends a message starting with `@gpt`, the hub forwards it to OpenAI, which generates a response. The AI's response is streamed back to the group in real-time. ```csharp var chatClient = _openAI.GetChatClient(_options.Model); -await foreach (var completion in chatClient.CompleteChatStreamingAsync(messagesInludeHistory)) + +await foreach (var completion in + chatClient.CompleteChatStreamingAsync(messagesInludeHistory)) { // ... // Buffering and sending the AI's response in chunks - await Clients.Group(groupName).SendAsync("newMessageWithId", "ChatGPT", id, totalCompletion.ToString()); + await Clients.Group(groupName).SendAsync( + "newMessageWithId", + "ChatGPT", + id, + totalCompletion.ToString()); // ... } ``` From de5235152ce192fdcc2cff5696d5c03a7017f73d Mon Sep 17 00:00:00 2001 From: wadepickett Date: Thu, 27 Feb 2025 12:32:49 -0800 Subject: [PATCH 12/23] SignalR with Open AI: pulling to new PR --- .../tutorials/ai-powered-group-chat/ai-powered-group-chat.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md b/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md index 614ee7a0c427..40d9fce803c8 100644 --- a/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md +++ b/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md @@ -3,7 +3,7 @@ title: Build an AI-Powered Group Chat with SignalR and OpenAI author: kevinguo-ed description: A tutorial explaining how SignalR and OpenAI are used together to build an AI-powered group chat ms.author: kevinguo -ms.date: 08/27/2024 +ms.date: 02/26/2025 uid: tutorials/ai-powered-group-chat --- From 5ca4f85aac7f0f2f4869f622c2ccb94a43b84992 Mon Sep 17 00:00:00 2001 From: wadepickett Date: Thu, 27 Feb 2025 13:35:38 -0800 Subject: [PATCH 13/23] Change style to sample guide --- .../ai-powered-group-chat/ai-powered-group-chat.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md b/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md index 40d9fce803c8..b6cad738f46f 100644 --- a/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md +++ b/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md @@ -1,17 +1,23 @@ --- -title: Build an AI-Powered Group Chat with SignalR and OpenAI +title: Sample AI-Powered Group Chat with SignalR and OpenAI author: kevinguo-ed description: A tutorial explaining how SignalR and OpenAI are used together to build an AI-powered group chat ms.author: kevinguo -ms.date: 02/26/2025 +ms.date: 02/25/2025 uid: tutorials/ai-powered-group-chat --- +# AI-Powered Group Chat sample with SignalR and OpenAI + +The AI-Powered Group Chat sample demonstrates how to integrate OpenAI's capabilities into a real-time group chat application using ASP.NET Core SignalR. + +* View or download [the complete sample code](https://github.com/microsoft/SignalR-Samples-AI/tree/main/AIStreaming). + ## Overview -The integration of AI into applications is rapidly becoming a must-have for developers looking to help their users be more creative, productive and achieve their health goals. AI-powered features, such as intelligent chatbots, personalized recommendations, and contextual responses, add significant value to modern apps. The AI-powered apps that came out since ChatGPT captured our imagination are primarily between one user and one AI assistant. As developers get more comfortable with the capabilities of AI, they are exploring AI-powered apps in a team's context. They ask "what value can AI add to a team of collaborators"? +Integrating AI into applications is becoming essential for developers aiming to enhance user creativity, productivity, and overall experience. AI-powered features, such as intelligent chatbots, personalized recommendations, and contextual responses, add significant value to modern apps. While many AI-powered applications, like those inspired by ChatGPT, focus on interactions between a single user and an AI assistant, there is growing interest in exploring AI's potential within team environments. Developers are now asking, "What value can AI add to a team of collaborators?" -This tutorial guides you through building a real-time group chat application. Among a group of human collaborators in a chat, there's an AI assistant which has access to the chat history and can be invited to help out by any collaborator when they start the message with `@gpt`. The finished app looks like this. +This sample guide highlights the process of building a real-time group chat application. In this chat, a group of human collaborators can interact with an AI assistant that has access to the chat history. Any collaborator can invite the AI to assist by starting their message with `@gpt`. The finished app looks like this: :::image type="content" source="./ai-powered-group-chat.jpg" alt-text="user interface for the AI-powered group chat"::: From 74a5b2676ff402177158bd115b711d36e29a6e73 Mon Sep 17 00:00:00 2001 From: wadepickett Date: Thu, 27 Feb 2025 14:06:25 -0800 Subject: [PATCH 14/23] Edit pass --- .../ai-powered-group-chat.md | 35 ++++++++++++------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md b/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md index b6cad738f46f..0bd0bcaab5cf 100644 --- a/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md +++ b/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md @@ -15,33 +15,35 @@ The AI-Powered Group Chat sample demonstrates how to integrate OpenAI's capabili ## Overview -Integrating AI into applications is becoming essential for developers aiming to enhance user creativity, productivity, and overall experience. AI-powered features, such as intelligent chatbots, personalized recommendations, and contextual responses, add significant value to modern apps. While many AI-powered applications, like those inspired by ChatGPT, focus on interactions between a single user and an AI assistant, there is growing interest in exploring AI's potential within team environments. Developers are now asking, "What value can AI add to a team of collaborators?" +Integrating AI into applications is becoming essential for developers aiming to enhance user creativity, productivity, and overall experience. AI-powered features, such as intelligent chatbots, personalized recommendations, and contextual responses, add significant value to modern apps. While many AI-powered applications, like those inspired by ChatGPT, focus on interactions between a single user and an AI assistant, there's growing interest in exploring AI's potential within team environments. Developers are now asking, "What value can AI add to a team of collaborators?" This sample guide highlights the process of building a real-time group chat application. In this chat, a group of human collaborators can interact with an AI assistant that has access to the chat history. Any collaborator can invite the AI to assist by starting their message with `@gpt`. The finished app looks like this: :::image type="content" source="./ai-powered-group-chat.jpg" alt-text="user interface for the AI-powered group chat"::: -We use OpenAI for generating intelligent, context-aware responses and SignalR for delivering the response to users in a group. You can find the complete code [in this repo](https://github.com/microsoft/SignalR-Samples-AI/tree/main/AIStreaming). +This sample uses OpenAI for generating intelligent, context-aware responses and SignalR for delivering the response to users in a group. You can find the complete code [in this repo](https://github.com/microsoft/SignalR-Samples-AI/tree/main/AIStreaming). ## Dependencies -You can use either Azure OpenAI or OpenAI for this project. Make sure to update the `endpoint` and `key` in `appsettings.json`. `OpenAIExtensions` reads the configuration when the app starts, and the configuration values for `endpoint` and `key` are required to authenticate and use either service. +Either Azure OpenAI or OpenAI can be used for this project. Make sure to update the `endpoint` and `key` in `appsettings.json`. `OpenAIExtensions` reads the configuration when the app starts, and the configuration values for `endpoint` and `key` are required to authenticate and use either service. # [OpenAI](#tab/open-ai) -To build this application, you will need the following: +To build this application, the following are required: + * ASP.NET Core: To create the web application and host the SignalR hub. -* [SignalR](https://www.nuget.org/packages/Microsoft.AspNetCore.SignalR.Client): For real-time communication between clients and the server. +* [SignalR](https://www.nuget.org/packages/Microsoft.AspNetCore.SignalR.Client): For real-time communication between clients, and the server. * [OpenAI Client](https://www.nuget.org/packages/OpenAI/2.0.0-beta.10): To interact with OpenAI's API for generating AI responses. # [Azure OpenAI](#tab/azure-open-ai) -To build this application, you will need the following: +To build this application, the following are required: + * ASP.NET Core: To create the web application and host the SignalR hub. -* [SignalR](https://www.nuget.org/packages/Microsoft.AspNetCore.SignalR.Client): For real-time communication between clients and the server. +* [SignalR](https://www.nuget.org/packages/Microsoft.AspNetCore.SignalR.Client): For real-time communication between clients, and the server. * [Azure OpenAI](https://www.nuget.org/packages/Azure.AI.OpenAI/2.0.0-beta.3): Azure.AI.OpenAI --- ## Implementation -In this section, we'll walk through the key parts of the code that integrate SignalR with OpenAI to create an AI-enhanced group chat experience. +This section highlights the key parts of the code that integrate SignalR with OpenAI to create an AI-enhanced group chat experience. ### Data flow @@ -49,7 +51,13 @@ In this section, we'll walk through the key parts of the code that integrate Sig ### SignalR Hub integration -The `GroupChatHub` class manages user connections, message broadcasting, and AI interactions. When a user sends a message starting with `@gpt`, the hub forwards it to OpenAI, which generates a response. The AI's response is streamed back to the group in real-time. +The `GroupChatHub` class manages user connections, message broadcasting, and AI interactions. + +When a user sends a message starting with `@gpt`: + +* The hub forwards it to OpenAI, which generates a response. +* The AI's response is streamed back to the group in real-time. + ```csharp var chatClient = _openAI.GetChatClient(_options.Model); @@ -69,7 +77,7 @@ await foreach (var completion in ### Maintain context with history -Every request to [Open AI's Chat Completions API](https://platform.openai.com/docs/guides/chat-completions) is stateless—Open AI doesn't store past interactions. In a chat app, what a user or an assistant has said is important for generating a response that's contextually relevant. To achieve this, include chat history in every request to the Completions API. +Every request to [OpenAI's Chat Completions API](https://platform.openai.com/docs/guides/chat-completions) is stateless. OpenAI doesn't store past interactions. In a chat app, what a user or an assistant has said is important for generating a response that's contextually relevant. To achieve this, include chat history in every request to the Completions API. The `GroupHistoryStore` class manages chat history for each group. It stores messages posted by both the users and AI assistants, ensuring that the conversation context is preserved across interactions. This context is crucial for generating coherent AI responses. @@ -89,9 +97,10 @@ _history.GetOrAddGroupHistory(groupName, userName, message); ### Stream AI responses -The `CompleteChatStreamingAsync()` method streams responses from OpenAI incrementally, which allows the app to send partial responses to the client as they are generated. +The `CompleteChatStreamingAsync()` method streams responses from OpenAI incrementally, which allows the app to send partial responses to the client as they're generated. + +The code uses a `StringBuilder` to accumulate the AI's response. It checks the length of the buffered content and sends it to the clients when it exceeds a certain threshold, for example, 20 characters. This approach ensures that users see the AI’s response as it forms, mimicking a human-like typing effect. -The code uses a `StringBuilder` to accumulate the AI's response. It checks the length of the buffered content and sends it to the clients when it exceeds a certain threshold (e.g., 20 characters). This approach ensures that users see the AI’s response as it forms, mimicking a human-like typing effect. ```csharp totalCompletion.Append(content); @@ -111,6 +120,6 @@ if (totalCompletion.Length - lastSentTokenLength > 20) This project opens up exciting possibilities for further enhancement: 1. **Advanced AI features**: Leverage other OpenAI capabilities like sentiment analysis, translation, or summarization. -1. **Incorporating multiple AI agents**: You can introduce multiple AI agents with distinct roles or expertise areas within the same chat. For example, one agent might focus on text generation, the other provides image or audio generation. This can create a richer and more dynamic user experience where different AI agents interact seamlessly with users and each other. +1. **Incorporating multiple AI agents**: You can introduce multiple AI agents with distinct roles or expertise areas within the same chat. For example, one agent might focus on text generation while the other provides image or audio generation. This can create a richer and more dynamic user experience where different AI agents interact seamlessly with users and each other. 1. **Share chat history between server instances**: Implement a database layer to persist chat history across sessions, allowing conversations to resume even after a disconnect. Beyond SQL or NO SQL based solutions, you can also explore using a caching service like Redis. It can significantly improve performance by storing frequently accessed data, such as chat history or AI responses, in memory. This reduces latency and offloads database operations, leading to faster response times, particularly in high-traffic scenarios. 1. **Leveraging Azure SignalR Service**: [Azure SignalR Service](/azure/azure-signalr/signalr-overview) provides scalable and reliable real-time messaging for your application. By offloading the SignalR backplane to Azure, you can scale out the chat application easily to support thousands of concurrent users across multiple servers. Azure SignalR also simplifies management and provides built-in features like automatic reconnections. From df6d91bbd754cfa01781905da24d8cb366a0cefd Mon Sep 17 00:00:00 2001 From: wadepickett Date: Wed, 19 Mar 2025 13:23:27 -0700 Subject: [PATCH 15/23] Added instruction on code snippet --- .../ai-powered-group-chat.md | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md b/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md index 0bd0bcaab5cf..0e4bea35e795 100644 --- a/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md +++ b/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md @@ -58,6 +58,8 @@ When a user sends a message starting with `@gpt`: * The hub forwards it to OpenAI, which generates a response. * The AI's response is streamed back to the group in real-time. +The following code snippet demonstrates how the `CompleteChatStreamingAsync` method streams responses from OpenAI incrementally: + ```csharp var chatClient = _openAI.GetChatClient(_options.Model); @@ -75,14 +77,23 @@ await foreach (var completion in } ``` +In the previous code: + +* `chatClient.CompleteChatStreamingAsync(messagesIncludeHistory)` initiates the streaming of AI responses. +* The `totalCompletion.Append(content)` line accumulates the AI's response. +* If the length of the buffered content exceeds 20 characters the buffered content is sent to the clients using `Clients.Group(groupName).SendAsync`. + +This ensures that the AI's response is delivered to the users in real-time, providing a seamless and interactive chat experience. + ### Maintain context with history Every request to [OpenAI's Chat Completions API](https://platform.openai.com/docs/guides/chat-completions) is stateless. OpenAI doesn't store past interactions. In a chat app, what a user or an assistant has said is important for generating a response that's contextually relevant. To achieve this, include chat history in every request to the Completions API. The `GroupHistoryStore` class manages chat history for each group. It stores messages posted by both the users and AI assistants, ensuring that the conversation context is preserved across interactions. This context is crucial for generating coherent AI responses. +The following code demonstrates how to store messages generated by the AI assistant in memory. The `UpdateGroupHistoryForAssistant` method is called to add the AI assistant's message to the group history, ensuring that the conversation context is maintained: + ```csharp -// Store message generated by AI-assistant in memory public void UpdateGroupHistoryForAssistant(string groupName, string message) { var chatMessages = _store.GetOrAdd(groupName, _ => InitiateChatMessages()); @@ -90,8 +101,9 @@ public void UpdateGroupHistoryForAssistant(string groupName, string message) } ``` +The `_history.GetOrAddGroupHistory` method is called to add the user's message to the group history, ensuring that the conversation context is maintained: + ```csharp -// Store message generated by users in memory _history.GetOrAddGroupHistory(groupName, userName, message); ``` @@ -101,6 +113,8 @@ The `CompleteChatStreamingAsync()` method streams responses from OpenAI incremen The code uses a `StringBuilder` to accumulate the AI's response. It checks the length of the buffered content and sends it to the clients when it exceeds a certain threshold, for example, 20 characters. This approach ensures that users see the AI’s response as it forms, mimicking a human-like typing effect. +:::codeinclude source="https://github.com/microsoft/SignalR-Samples-AI/blob/main/AIStreaming/Hubs/GroupChatHub.cs" range="35-73" highlight="56-61"::: + ```csharp totalCompletion.Append(content); From dee3c1a5d925f4b51eac073fceb29ae2304e6167 Mon Sep 17 00:00:00 2001 From: wadepickett Date: Wed, 19 Mar 2025 14:38:16 -0700 Subject: [PATCH 16/23] Update toc.yml: samples repo and AI sample under Samples --- aspnetcore/toc.yml | 10 +++++++--- .../ai-powered-group-chat/ai-powered-group-chat.md | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/aspnetcore/toc.yml b/aspnetcore/toc.yml index 61e35ee02b1b..d45c26b13059 100644 --- a/aspnetcore/toc.yml +++ b/aspnetcore/toc.yml @@ -890,8 +890,6 @@ items: displayName: signalr - name: Tutorials items: - - name: AI-powered group chat - uid: tutorials/ai-powered-group-chat - name: SignalR with JavaScript uid: tutorials/signalr - name: SignalR with TypeScript @@ -899,8 +897,14 @@ items: - name: SignalR with Blazor uid: blazor/tutorials/signalr-blazor - name: Samples - href: https://github.com/aspnet/SignalR-samples displayName: signalr + items: + - name: AI-powered group chat + uid: tutorials/ai-powered-group-chat + displayName: signalr + - name: SignalR samples repository + href: https://github.com/aspnet/SignalR-samples + displayName: signalr - name: Server concepts displayName: signalr items: diff --git a/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md b/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md index 0e4bea35e795..1e265e5d26a8 100644 --- a/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md +++ b/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md @@ -3,7 +3,7 @@ title: Sample AI-Powered Group Chat with SignalR and OpenAI author: kevinguo-ed description: A tutorial explaining how SignalR and OpenAI are used together to build an AI-powered group chat ms.author: kevinguo -ms.date: 02/25/2025 +ms.date: 03/19/2025 uid: tutorials/ai-powered-group-chat --- From 9704b157988884c241fb7ce98157d19359d98450 Mon Sep 17 00:00:00 2001 From: wadepickett Date: Wed, 19 Mar 2025 16:18:18 -0700 Subject: [PATCH 17/23] Text highlight --- .../tutorials/ai-powered-group-chat/ai-powered-group-chat.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md b/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md index 1e265e5d26a8..459b24940992 100644 --- a/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md +++ b/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md @@ -113,7 +113,7 @@ The `CompleteChatStreamingAsync()` method streams responses from OpenAI incremen The code uses a `StringBuilder` to accumulate the AI's response. It checks the length of the buffered content and sends it to the clients when it exceeds a certain threshold, for example, 20 characters. This approach ensures that users see the AI’s response as it forms, mimicking a human-like typing effect. -:::codeinclude source="https://github.com/microsoft/SignalR-Samples-AI/blob/main/AIStreaming/Hubs/GroupChatHub.cs" range="35-73" highlight="56-61"::: +:::codeinclude source="https://github.com/microsoft/SignalR-Samples-AI/blob/main/AIStreaming/Hubs/GroupChatHub.cs range="35-73" highlight="56-61"::: ```csharp totalCompletion.Append(content); From 51f609c0aa80d1c2e6f22343cfe456bfda6c434e Mon Sep 17 00:00:00 2001 From: wadepickett Date: Thu, 20 Mar 2025 14:53:17 -0700 Subject: [PATCH 18/23] Removed code link --- .../tutorials/ai-powered-group-chat/ai-powered-group-chat.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md b/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md index 459b24940992..dd34d56f243e 100644 --- a/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md +++ b/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md @@ -113,8 +113,6 @@ The `CompleteChatStreamingAsync()` method streams responses from OpenAI incremen The code uses a `StringBuilder` to accumulate the AI's response. It checks the length of the buffered content and sends it to the clients when it exceeds a certain threshold, for example, 20 characters. This approach ensures that users see the AI’s response as it forms, mimicking a human-like typing effect. -:::codeinclude source="https://github.com/microsoft/SignalR-Samples-AI/blob/main/AIStreaming/Hubs/GroupChatHub.cs range="35-73" highlight="56-61"::: - ```csharp totalCompletion.Append(content); From 7b1d31abed8f31899eb71a54ad07ed8cfc98abcf Mon Sep 17 00:00:00 2001 From: wadepickett Date: Thu, 20 Mar 2025 15:28:30 -0700 Subject: [PATCH 19/23] Added description for data flow diagram --- .../ai-powered-group-chat/ai-powered-group-chat.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md b/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md index dd34d56f243e..d4fcdcfa8c86 100644 --- a/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md +++ b/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md @@ -47,8 +47,15 @@ This section highlights the key parts of the code that integrate SignalR with Op ### Data flow +The following diagram highlights the step-by-step communication and processing involved in using OpenAI services within a system, using an iterative approach to responses and handling data: + :::image type="content" source="./sequence-diagram-ai-powered-group-chat.png" alt-text="sequence diagram for the AI-powered group chat"::: +In the previous diagram: + +* The Client sends instructions to the Server, which then communicates with OpenAI to process these instructions. +* OpenAI responds with partial completion data, which the Server forwards back to the Client. This process repeats multiple times for an iterative exchange of data between these components. + ### SignalR Hub integration The `GroupChatHub` class manages user connections, message broadcasting, and AI interactions. @@ -81,7 +88,7 @@ In the previous code: * `chatClient.CompleteChatStreamingAsync(messagesIncludeHistory)` initiates the streaming of AI responses. * The `totalCompletion.Append(content)` line accumulates the AI's response. -* If the length of the buffered content exceeds 20 characters the buffered content is sent to the clients using `Clients.Group(groupName).SendAsync`. +* If the length of the buffered content exceeds 20 characters, the buffered content is sent to the clients using `Clients.Group(groupName).SendAsync`. This ensures that the AI's response is delivered to the users in real-time, providing a seamless and interactive chat experience. From ac494e903238e31e2bc0a97ff7d8a97413c3cf0f Mon Sep 17 00:00:00 2001 From: wadepickett Date: Thu, 20 Mar 2025 15:46:52 -0700 Subject: [PATCH 20/23] Small edit on diagram intro --- .../tutorials/ai-powered-group-chat/ai-powered-group-chat.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md b/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md index d4fcdcfa8c86..6d072f0875e7 100644 --- a/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md +++ b/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md @@ -47,7 +47,7 @@ This section highlights the key parts of the code that integrate SignalR with Op ### Data flow -The following diagram highlights the step-by-step communication and processing involved in using OpenAI services within a system, using an iterative approach to responses and handling data: +The following diagram highlights the step-by-step communication and processing involved in using OpenAI services, employing an iterative approach to responses and data handling: :::image type="content" source="./sequence-diagram-ai-powered-group-chat.png" alt-text="sequence diagram for the AI-powered group chat"::: From 0072d739c82d089a3efca89ffee4860440fde771 Mon Sep 17 00:00:00 2001 From: Wade Pickett Date: Fri, 21 Mar 2025 12:17:37 -0700 Subject: [PATCH 21/23] Update aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md Co-authored-by: David Pine --- .../tutorials/ai-powered-group-chat/ai-powered-group-chat.md | 1 + 1 file changed, 1 insertion(+) diff --git a/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md b/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md index 6d072f0875e7..0b19e2006bd9 100644 --- a/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md +++ b/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md @@ -24,6 +24,7 @@ This sample guide highlights the process of building a real-time group chat appl This sample uses OpenAI for generating intelligent, context-aware responses and SignalR for delivering the response to users in a group. You can find the complete code [in this repo](https://github.com/microsoft/SignalR-Samples-AI/tree/main/AIStreaming). ## Dependencies + Either Azure OpenAI or OpenAI can be used for this project. Make sure to update the `endpoint` and `key` in `appsettings.json`. `OpenAIExtensions` reads the configuration when the app starts, and the configuration values for `endpoint` and `key` are required to authenticate and use either service. # [OpenAI](#tab/open-ai) From 530c86d03b10488d7cfeb037c95b58f41a64a633 Mon Sep 17 00:00:00 2001 From: Wade Pickett Date: Fri, 21 Mar 2025 12:54:52 -0700 Subject: [PATCH 22/23] Update aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md commiting suggestion by IEvangelist Co-authored-by: David Pine --- .../ai-powered-group-chat.md | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md b/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md index 0b19e2006bd9..644da20c4784 100644 --- a/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md +++ b/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md @@ -27,19 +27,20 @@ This sample uses OpenAI for generating intelligent, context-aware responses and Either Azure OpenAI or OpenAI can be used for this project. Make sure to update the `endpoint` and `key` in `appsettings.json`. `OpenAIExtensions` reads the configuration when the app starts, and the configuration values for `endpoint` and `key` are required to authenticate and use either service. -# [OpenAI](#tab/open-ai) -To build this application, the following are required: +### [OpenAI](#tab/open-ai) +To build this application, you will need the following: * ASP.NET Core: To create the web application and host the SignalR hub. -* [SignalR](https://www.nuget.org/packages/Microsoft.AspNetCore.SignalR.Client): For real-time communication between clients, and the server. -* [OpenAI Client](https://www.nuget.org/packages/OpenAI/2.0.0-beta.10): To interact with OpenAI's API for generating AI responses. +* [SignalR](https://www.nuget.org/packages/Microsoft.AspNetCore.SignalR.Client): For real-time communication between clients and the server. +* [OpenAI Client](https://www.nuget.org/packages/OpenAI): To interact with OpenAI's API for generating AI responses. -# [Azure OpenAI](#tab/azure-open-ai) -To build this application, the following are required: +### [Azure OpenAI](#tab/azure-open-ai) +To build this application, you will need the following: * ASP.NET Core: To create the web application and host the SignalR hub. -* [SignalR](https://www.nuget.org/packages/Microsoft.AspNetCore.SignalR.Client): For real-time communication between clients, and the server. -* [Azure OpenAI](https://www.nuget.org/packages/Azure.AI.OpenAI/2.0.0-beta.3): Azure.AI.OpenAI +* [SignalR](https://www.nuget.org/packages/Microsoft.AspNetCore.SignalR.Client): For real-time communication between clients and the server. +* [Azure OpenAI](https://www.nuget.org/packages/Azure.AI.OpenAI): `Azure.AI.OpenAI` + --- ## Implementation From 6736092c20ee0d99ffda4aa4db817d12a1513876 Mon Sep 17 00:00:00 2001 From: Wade Pickett Date: Fri, 21 Mar 2025 13:14:06 -0700 Subject: [PATCH 23/23] Update aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md --- .../tutorials/ai-powered-group-chat/ai-powered-group-chat.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md b/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md index 644da20c4784..07a5824a8370 100644 --- a/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md +++ b/aspnetcore/tutorials/ai-powered-group-chat/ai-powered-group-chat.md @@ -2,7 +2,7 @@ title: Sample AI-Powered Group Chat with SignalR and OpenAI author: kevinguo-ed description: A tutorial explaining how SignalR and OpenAI are used together to build an AI-powered group chat -ms.author: kevinguo +ms.author: wpickett ms.date: 03/19/2025 uid: tutorials/ai-powered-group-chat ---