Skip to content

Commit 315af12

Browse files
authored
Update the docs (#385)
1 parent af21ebc commit 315af12

File tree

6 files changed

+58
-29
lines changed

6 files changed

+58
-29
lines changed

src/CrestApps.OrchardCore.Documentations/docs/ai/response-handlers.md

Lines changed: 39 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ The hub classes (`AIChatHub`, `ChatInteractionHub`) and their client interfaces
128128
```csharp
129129
using CrestApps.OrchardCore.AI.Chat.Hubs;
130130
using CrestApps.OrchardCore.AI.Chat.Interactions.Hubs;
131+
using CrestApps.OrchardCore.AI.Core.Models;
131132
using CrestApps.OrchardCore.AI.Models;
132133
using CrestApps.Support;
133134
using Microsoft.AspNetCore.Http;
@@ -152,10 +153,10 @@ internal static class GenesysWebhookEndpoint
152153
GenesysWebhookPayload payload,
153154
IAIChatSessionManager sessionManager,
154155
IAIChatSessionPromptStore promptStore,
155-
IHubContext<AIChatHub> chatHubContext,
156+
IHubContext<AIChatHub, IAIChatHubClient> chatHubContext,
156157
ISourceCatalogManager<ChatInteraction> interactionManager,
157158
IChatInteractionPromptStore interactionPromptStore,
158-
IHubContext<ChatInteractionHub> interactionHubContext)
159+
IHubContext<ChatInteractionHub, IChatInteractionHubClient> interactionHubContext)
159160
{
160161
// TODO: Validate the webhook payload signature to ensure it's authentic.
161162
@@ -179,16 +180,15 @@ internal static class GenesysWebhookEndpoint
179180
};
180181
await promptStore.CreateAsync(prompt);
181182

182-
// Notify connected client(s) via the SignalR group.
183+
// Push the new assistant message directly to connected client(s).
184+
// There is no built-in "ReceiveMessage" client method for deferred webhook replies.
185+
// The current UI appends assistant messages through the conversation events.
183186
var groupName = AIChatHub.GetSessionGroupName(session.SessionId);
184187

185-
await chatHubContext.Clients.Group(groupName).SendAsync("ReceiveMessage", new
186-
{
187-
sessionId = session.SessionId,
188-
messageId = prompt.ItemId,
189-
content = payload.AgentMessage,
190-
role = "assistant",
191-
});
188+
await chatHubContext.Clients.Group(groupName)
189+
.ReceiveConversationAssistantToken(session.SessionId, prompt.ItemId, payload.AgentMessage, prompt.ItemId);
190+
await chatHubContext.Clients.Group(groupName)
191+
.ReceiveConversationAssistantComplete(session.SessionId, prompt.ItemId);
192192
}
193193
else if (payload.ChatType == ChatContextType.ChatInteraction)
194194
{
@@ -210,16 +210,16 @@ internal static class GenesysWebhookEndpoint
210210
};
211211
await interactionPromptStore.CreateAsync(prompt);
212212

213-
// Notify connected client(s) via the SignalR group.
213+
// Push the new assistant message directly to connected client(s).
214+
// Deferred webhook replies are surfaced through the conversation events, not a
215+
// nonexistent "ReceiveMessage" event.
214216
var groupName = ChatInteractionHub.GetInteractionGroupName(interaction.ItemId);
215217

216-
await interactionHubContext.Clients.Group(groupName).SendAsync("ReceiveMessage", new
217-
{
218-
sessionId = interaction.ItemId,
219-
messageId = prompt.ItemId,
220-
content = payload.AgentMessage,
221-
role = "assistant",
222-
});
218+
await interactionHubContext.Clients.Group(groupName)
219+
.ReceiveConversationAssistantToken(interaction.ItemId, prompt.ItemId, payload.AgentMessage, prompt.ItemId);
220+
221+
await interactionHubContext.Clients.Group(groupName)
222+
.ReceiveConversationAssistantComplete(interaction.ItemId, prompt.ItemId);
223223
}
224224
else
225225
{
@@ -240,6 +240,20 @@ public override void Configure(IApplicationBuilder app, IEndpointRouteBuilder ro
240240
}
241241
```
242242

243+
:::important
244+
`ReceiveMessage` is **not** a built-in SignalR client method in the current chat UI, so calling `SendAsync("ReceiveMessage", ...)` will not update the browser. The built-in client methods are:
245+
246+
- `ReceiveConversationAssistantToken` + `ReceiveConversationAssistantComplete` to append a new assistant message directly to the current UI.
247+
- `LoadSession` / `LoadInteraction` to reload the full transcript after you persist a deferred assistant message.
248+
- `ReceiveNotification`, `UpdateNotification`, and `RemoveNotification` for transient system messages sent through `IChatNotificationSender`.
249+
250+
If you only want to notify the user about transfer state, typing, agent connection, or similar status, use `IChatNotificationSender`. If you want the external agent's reply to appear as a real assistant message in the transcript, save it to the prompt store and then either append it with `ReceiveConversationAssistantToken` / `ReceiveConversationAssistantComplete` or refresh the transcript with `LoadSession` / `LoadInteraction`.
251+
:::
252+
253+
:::note
254+
The active SignalR connection must join the session or interaction group before deferred webhook messages can be delivered in real time. Built-in CrestApps clients now do this automatically on startup by calling `LoadSession(existingSessionId)` or `LoadInteraction(existingItemId)` when the page already has an existing identifier. If you build a custom client, make sure it explicitly calls `StartSession`, `LoadSession`, or `LoadInteraction` after connecting so the current connection joins the correct group.
255+
:::
256+
243257
### Step 4: Real-Time Communication via Persistent Relay (Alternative to Webhook)
244258

245259
While webhooks work well for many integration scenarios, some third-party platforms support persistent connections for real-time bidirectional communication. The external chat relay infrastructure keeps a connection open, enabling instant delivery of events like typing indicators, agent-connected notifications, wait-time updates, and messages — without polling or callback endpoints.
@@ -263,7 +277,7 @@ Understanding which interface handles each direction of communication is key:
263277
| **User → External App** | `IExternalChatRelay.SendPromptAsync()` | Sends the user's chat message text to the external system via the relay connection. Called by the response handler when it receives a prompt. |
264278
| **User → External App** (signals) | `IExternalChatRelay.SendSignalAsync()` | Sends user signals (e.g., thumbs up/down, user typing, feedback) to the external system. Called by `IChatNotificationActionHandler` implementations. |
265279
| **External App → User** (notifications) | `IExternalChatRelayEventHandler.HandleEventAsync()` | Receives events from the relay's background listener and routes them to `IChatNotificationSender` for UI notifications (typing indicators, agent connected, wait times, connection status, session ended). |
266-
| **External App → User** (messages) | Your `IExternalChatRelay` implementation | Receives message events from the external system and writes them to the prompt store via `IAIChatSessionPromptStore`, then notifies the SignalR group via `IHubContext<AIChatHub>`. This is handled directly in your relay implementation's `DispatchEventAsync` method. |
280+
| **External App → User** (messages) | Your `IExternalChatRelay` implementation | Receives message events from the external system, writes them to the appropriate prompt store, then either appends the message with `ReceiveConversationAssistantToken` / `ReceiveConversationAssistantComplete` or reloads the transcript via `LoadSession` / `LoadInteraction`. |
267281

268282
:::note
269283
`ExternalChatRelayEvent.EventType` is a **string**, not an enum. Well-known event types are defined as constants in `ExternalChatRelayEventTypes` (e.g., `ExternalChatRelayEventTypes.AgentTyping`). You can use **any custom string** for platform-specific events — just register a keyed `IExternalChatRelayNotificationBuilder` for your event type.
@@ -546,7 +560,7 @@ public sealed class GenesysWebSocketRelay : IExternalChatRelay
546560
{
547561
var sessionManager = services.GetRequiredService<IAIChatSessionManager>();
548562
var promptStore = services.GetRequiredService<IAIChatSessionPromptStore>();
549-
var hubContext = services.GetRequiredService<IHubContext<AIChatHub>>();
563+
var hubContext = services.GetRequiredService<IHubContext<AIChatHub, IAIChatHubClient>>();
550564

551565
var session = await sessionManager.FindByIdAsync(_sessionId);
552566
if (session is null)
@@ -564,13 +578,11 @@ public sealed class GenesysWebSocketRelay : IExternalChatRelay
564578
await promptStore.CreateAsync(prompt);
565579

566580
var groupName = AIChatHub.GetSessionGroupName(session.SessionId);
567-
await hubContext.Clients.Group(groupName).SendAsync("ReceiveMessage", new
568-
{
569-
sessionId = session.SessionId,
570-
messageId = prompt.ItemId,
571-
content = relayEvent.Content,
572-
role = "assistant",
573-
});
581+
await hubContext.Clients.Group(groupName)
582+
.ReceiveConversationAssistantToken(session.SessionId, prompt.ItemId, relayEvent.Content, prompt.ItemId);
583+
584+
await hubContext.Clients.Group(groupName)
585+
.ReceiveConversationAssistantComplete(session.SessionId, prompt.ItemId);
574586
}
575587
// For ChatInteraction, follow the same pattern with the interaction pipeline.
576588
}

src/CrestApps.OrchardCore.Documentations/docs/changelog/v2.0.0.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,8 @@ A new suite of modules for multi-channel communication:
157157
- **Chat Mode (Voice Input & Output)** — AI Chat and Chat Interactions now support a unified **Chat Mode** dropdown with three options: **Text Only** (default), **Audio Input** (microphone button for speech-to-text dictation), and **Conversation** (two-way voice interaction with auto-send and text-to-speech). Users can click a microphone button to record speech, which is streamed to the server via SignalR and transcribed using the configured `ISpeechToTextClient` (e.g., OpenAI Whisper, Azure OpenAI Whisper). The transcribed text appears in the input field for review before sending. For AI Chat, configure via the **Chat Mode** dropdown on AI Profiles or AI Profile Templates (visible only for Chat profile types). For Chat Interactions, configure via the site-level **Chat Mode** dropdown under **Settings → Artificial Intelligence → Chat Interactions**. Audio Input requires a **Default Speech-to-Text Deployment**; Conversation requires both STT and TTS default deployments.
158158
- **Workflow Integration** — AI Completion tasks for Orchard Core Workflows.
159159
- **Extensible Chat Response Handlers** — Chat prompts are now routed through a pluggable `IChatResponseHandler` abstraction. The built-in AI handler remains the default, but custom handlers can be registered to route prompts to external systems (e.g., live agent platforms like Genesys). Handlers support two modes: **streaming** (immediate response like AI) and **deferred** (response arrives later via webhook). Sessions and interactions have a `ResponseHandlerName` property that can be changed mid-conversation by AI functions for live agent handoff scenarios. AI Profiles and Templates support an **Initial Response Handler** setting to bypass AI entirely. The chat UI shows a handler selector when multiple handlers are registered. Custom handlers are **not supported in Conversation mode** — the resolver always returns the AI handler when `ChatMode.Conversation` is active because conversation mode requires the AI pipeline for speech-to-text and text-to-speech. See the [Response Handlers documentation](../ai/response-handlers.md) for details.
160+
- **Response handler documentation corrected** — The deferred webhook examples no longer tell integrators to call `SendAsync("ReceiveMessage", ...)`, because there is no built-in `ReceiveMessage` SignalR client method in the current AI Chat or Chat Interaction UI. The docs now show the supported message-delivery APIs: persist the assistant reply, then either append it with `ReceiveConversationAssistantToken` / `ReceiveConversationAssistantComplete` or reload the transcript with `LoadSession` / `LoadInteraction`. Transient system messages such as typing, transfer, and agent-connected updates still use `IChatNotificationSender`. This is a documentation correction only; no runtime behavior changed.
161+
- **Existing chat connections now rejoin deferred-response groups on startup** — When an AI Chat page or Chat Interaction page opened with an existing session or interaction already rendered, the browser could display the existing transcript without calling `LoadSession` or `LoadInteraction`. That meant the active SignalR connection never joined the corresponding group, so deferred webhook replies were persisted but not shown in real time until the user sent another message. The built-in JavaScript clients now automatically call `LoadSession(existingSessionId)` or `LoadInteraction(existingItemId)` on startup so the current connection joins the proper group immediately and deferred external replies can arrive live.
160162
- **External Chat Relay** — New protocol-agnostic infrastructure for real-time bidirectional communication with third-party live-agent platforms. Unlike the webhook pattern (where the external system calls back into your application), an external chat relay maintains a persistent connection so events like typing indicators, agent-connected notifications, wait-time updates, connection-status signals, and messages flow instantly without polling. The relay interface (`IExternalChatRelay`) is transport-agnostic — implementations can use WebSocket, SSE, gRPC streaming, WebRTC data channels, message queues, event buses, or any other protocol. Key abstractions: `IExternalChatRelay` (persistent connection interface with `SendPromptAsync` for user→external, `SendSignalAsync` for feedback signals, and `IsConnectedAsync()` for status checks), `IExternalChatRelayManager` (singleton lifecycle manager), `IExternalChatRelayEventHandler` (event-to-notification router using keyed builder/handler pattern). Event types are strings (`ExternalChatRelayEventTypes` constants) for extensibility — custom event types are supported out of the box. Built-in event types include: `agent-typing`, `agent-stopped-typing`, `agent-connected`, `agent-disconnected`, `agent-reconnecting`, `connection-lost`, `connection-restored`, `message`, `wait-time-updated`, `session-ended`. The default handler resolves keyed `IExternalChatRelayNotificationBuilder` services per event type — each builder declares a `NotificationType` (used to create the notification) and populates properties via `Build`. The `IExternalChatRelayNotificationHandler` supports send, update, and remove operations. To add custom event types, register a keyed builder: `services.AddKeyedScoped<IExternalChatRelayNotificationBuilder, MyBuilder>("my-event")`. See the [Response Handlers documentation](../ai/response-handlers.md#step-4-real-time-communication-via-persistent-relay-alternative-to-webhook) for a complete implementation example.
161163
- **Chat UI Notifications** — New extensible notification system that allows C# code to send transient system messages to the chat interface via SignalR — no JavaScript required. Built-in notifications include typing indicators ("Mike is typing…"), transfer status with estimated wait times and cancel buttons, agent-connected indicators, and conversation/session ended indicators. The `IChatNotificationSender` interface provides `SendAsync`, `UpdateAsync`, and `RemoveAsync` methods. Well-known notification types are available via `ChatNotificationTypes` and action names via `ChatNotificationActionNames`. Notifications are created using `new ChatNotification("type")` with the `Type` serving as the sole identifier — e.g., `new ChatNotification(ChatNotificationTypes.Typing)`. All user-facing strings accept `IStringLocalizer` for full localization support. `ChatNotification` requires a `type` parameter in its constructor and the `Type` setter is private. Notification system messages support action buttons that trigger server-side `IChatNotificationActionHandler` callbacks (registered as keyed services). Built-in action handlers: `cancel-transfer` (resets handler to AI) and `end-session` (closes the session). The system uses an extensible transport architecture — `IChatNotificationTransport` implementations are registered as keyed services by `ChatContextType`, allowing third-party modules to add notification support for custom hubs. Custom notification types with custom actions and styling are fully supported. See the [Chat UI Notifications documentation](../ai/chat-notifications.md) for details.
162164
- **`SpeechTextSanitizer` Utility** — Extracted `SanitizeForSpeech` from both hub classes into a shared `SpeechTextSanitizer.Sanitize()` static method in `CrestApps.OrchardCore.AI.Core.Services`. This utility strips markdown formatting, code blocks, emoji, and other non-speech elements from text before passing it to a text-to-speech engine. Available for reuse by any module.

src/Modules/CrestApps.OrchardCore.AI.Chat.Interactions/Assets/js/chat-interaction.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1280,6 +1280,11 @@ window.chatInteractionManager = function () {
12801280
this.chatContainer = document.querySelector(config.chatContainerElementSelector);
12811281
this.placeholder = document.querySelector(config.placeholderElementSelector);
12821282

1283+
const itemId = this.getItemId();
1284+
if (itemId) {
1285+
this.loadInteraction(itemId);
1286+
}
1287+
12831288
// Pause auto-scroll when the user manually scrolls up during streaming.
12841289
this.chatContainer.addEventListener('scroll', () => {
12851290
if (!this.stream) {

src/Modules/CrestApps.OrchardCore.AI.Chat.Interactions/wwwroot/scripts/chat-interaction.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1319,6 +1319,10 @@ window.chatInteractionManager = function () {
13191319
this.buttonElement = document.querySelector(config.sendButtonElementSelector);
13201320
this.chatContainer = document.querySelector(config.chatContainerElementSelector);
13211321
this.placeholder = document.querySelector(config.placeholderElementSelector);
1322+
var itemId = this.getItemId();
1323+
if (itemId) {
1324+
this.loadInteraction(itemId);
1325+
}
13221326

13231327
// Pause auto-scroll when the user manually scrolls up during streaming.
13241328
this.chatContainer.addEventListener('scroll', function () {

src/Modules/CrestApps.OrchardCore.AI.Chat/Assets/js/ai-chat.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1759,7 +1759,10 @@ window.openAIChatManager = function () {
17591759
this.renderHandlerSelector();
17601760
}
17611761

1762-
if (config.autoCreateSession && !config.widget && !this.getSessionId()) {
1762+
const sessionId = this.getSessionId();
1763+
if (!config.widget && sessionId) {
1764+
this.loadSession(sessionId);
1765+
} else if (config.autoCreateSession && !config.widget && !sessionId) {
17631766
this.startNewSession();
17641767
}
17651768

src/Modules/CrestApps.OrchardCore.AI.Chat/wwwroot/scripts/ai-chat.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1820,7 +1820,10 @@ window.openAIChatManager = function () {
18201820
if (this.placeholder && this.responseHandlers.length > 0) {
18211821
this.renderHandlerSelector();
18221822
}
1823-
if (config.autoCreateSession && !config.widget && !this.getSessionId()) {
1823+
var sessionId = this.getSessionId();
1824+
if (!config.widget && sessionId) {
1825+
this.loadSession(sessionId);
1826+
} else if (config.autoCreateSession && !config.widget && !sessionId) {
18241827
this.startNewSession();
18251828
}
18261829

0 commit comments

Comments
 (0)