4
4
using BotSharp . Abstraction . MLTasks ;
5
5
using BotSharp . Abstraction . Conversations . Enums ;
6
6
using BotSharp . Abstraction . Routing . Models ;
7
+ using NetTopologySuite . Index . HPRtree ;
8
+ using BotSharp . Abstraction . Agents . Models ;
9
+ using Microsoft . Identity . Client . Extensions . Msal ;
10
+ using Microsoft . AspNetCore . Cors . Infrastructure ;
7
11
8
12
namespace BotSharp . Core . Realtime ;
9
13
@@ -46,9 +50,14 @@ public async Task Listen(WebSocket userWebSocket,
46
50
{
47
51
await completer . AppenAudioBuffer ( conn . Data ) ;
48
52
}
53
+ else if ( conn . Event == "user_dtmf_received" )
54
+ {
55
+ await HandleUserDtmfReceived ( completer , conn ) ;
56
+ }
49
57
else if ( conn . Event == "user_disconnected" )
50
58
{
51
59
await completer . Disconnect ( ) ;
60
+ await HandleUserDisconnected ( conn ) ;
52
61
}
53
62
} while ( ! result . CloseStatus . HasValue ) ;
54
63
@@ -58,8 +67,6 @@ public async Task Listen(WebSocket userWebSocket,
58
67
private async Task ConnectToModel ( IRealTimeCompletion completer , WebSocket userWebSocket , RealtimeHubConnection conn )
59
68
{
60
69
var hookProvider = _services . GetRequiredService < ConversationHookProvider > ( ) ;
61
- var storage = _services . GetRequiredService < IConversationStorage > ( ) ;
62
-
63
70
var convService = _services . GetRequiredService < IConversationService > ( ) ;
64
71
convService . SetConversationId ( conn . ConversationId , [ ] ) ;
65
72
var conversation = await convService . GetConversation ( conn . ConversationId ) ;
@@ -92,8 +99,8 @@ private async Task ConnectToModel(IRealTimeCompletion completer, WebSocket userW
92
99
await completer . Connect ( conn ,
93
100
onModelReady : async ( ) =>
94
101
{
95
- // Control initial session
96
- await completer . UpdateSession ( conn ) ;
102
+ // Control initial session, prevent initial response interruption
103
+ await completer . UpdateSession ( conn , turnDetection : false ) ;
97
104
98
105
// Add dialog history
99
106
foreach ( var item in dialogs )
@@ -103,17 +110,41 @@ await completer.Connect(conn,
103
110
104
111
if ( dialogs . LastOrDefault ( ) ? . Role == AgentRole . Assistant )
105
112
{
106
- // await completer.TriggerModelInference($"Rephase your last response:\r\n{dialogs.LastOrDefault()?.Content}");
113
+ await completer . TriggerModelInference ( $ "Rephase your last response:\r \n { dialogs . LastOrDefault ( ) ? . Content } ") ;
107
114
}
108
115
else
109
116
{
110
117
await completer . TriggerModelInference ( "Reply based on the conversation context." ) ;
111
118
}
119
+
120
+ // Start turn detection
121
+ await Task . Delay ( 1000 * 8 ) ;
122
+ await completer . UpdateSession ( conn , turnDetection : true ) ;
112
123
} ,
113
124
onModelAudioDeltaReceived : async audioDeltaData =>
114
125
{
126
+ // If this is the first delta of a new response, set the start timestamp
127
+ if ( ! conn . ResponseStartTimestamp . HasValue )
128
+ {
129
+ conn . ResponseStartTimestamp = conn . LatestMediaTimestamp ;
130
+ _logger . LogDebug ( $ "Setting start timestamp for new response: { conn . ResponseStartTimestamp } ms") ;
131
+ }
132
+
115
133
var data = conn . OnModelMessageReceived ( audioDeltaData ) ;
116
134
await SendEventToUser ( userWebSocket , data ) ;
135
+
136
+ // Send mark messages to Media Streams so we know if and when AI response playback is finished
137
+ if ( ! string . IsNullOrEmpty ( conn . StreamId ) )
138
+ {
139
+ var markEvent = new
140
+ {
141
+ @event = "mark" ,
142
+ streamSid = conn . StreamId ,
143
+ mark = new { name = "responsePart" }
144
+ } ;
145
+ await SendEventToUser ( userWebSocket , markEvent ) ;
146
+ conn . MarkQueue . Enqueue ( "responsePart" ) ;
147
+ }
117
148
} ,
118
149
onModelAudioResponseDone : async ( ) =>
119
150
{
@@ -160,16 +191,18 @@ await completer.Connect(conn,
160
191
await completer . TriggerModelInference ( "Reply based on the function's output." ) ;
161
192
}
162
193
}
163
- // append output audio transcript to conversation
164
- storage . Append ( conn . ConversationId , message ) ;
165
- dialogs . Add ( message ) ;
166
-
167
- foreach ( var hook in hookProvider . HooksOrderByPriority )
194
+ else
168
195
{
169
- hook . SetAgent ( agent )
170
- . SetConversation ( conversation ) ;
196
+ // append output audio transcript to conversation
197
+ dialogs . Add ( message ) ;
198
+
199
+ foreach ( var hook in hookProvider . HooksOrderByPriority )
200
+ {
201
+ hook . SetAgent ( agent )
202
+ . SetConversation ( conversation ) ;
171
203
172
- await hook . OnResponseGenerated ( message ) ;
204
+ await hook . OnResponseGenerated ( message ) ;
205
+ }
173
206
}
174
207
}
175
208
} ,
@@ -180,7 +213,6 @@ await completer.Connect(conn,
180
213
onInputAudioTranscriptionCompleted : async message =>
181
214
{
182
215
// append input audio transcript to conversation
183
- storage . Append ( conn . ConversationId , message ) ;
184
216
dialogs . Add ( message ) ;
185
217
186
218
foreach ( var hook in hookProvider . HooksOrderByPriority )
@@ -193,11 +225,56 @@ await completer.Connect(conn,
193
225
} ,
194
226
onUserInterrupted : async ( ) =>
195
227
{
228
+ // Reset states
229
+ conn . MarkQueue . Clear ( ) ;
230
+ conn . LastAssistantItem = null ;
231
+ conn . ResponseStartTimestamp = null ;
232
+
196
233
var data = conn . OnModelUserInterrupted ( ) ;
197
234
await SendEventToUser ( userWebSocket , data ) ;
198
235
} ) ;
199
236
}
200
237
238
+ private async Task HandleUserDtmfReceived ( IRealTimeCompletion completer , RealtimeHubConnection conn )
239
+ {
240
+ var routing = _services . GetRequiredService < IRoutingService > ( ) ;
241
+ var hookProvider = _services . GetRequiredService < ConversationHookProvider > ( ) ;
242
+ var agentService = _services . GetRequiredService < IAgentService > ( ) ;
243
+ var agent = await agentService . LoadAgent ( conn . CurrentAgentId ) ;
244
+ var dialogs = routing . Context . GetDialogs ( ) ;
245
+ var convService = _services . GetRequiredService < IConversationService > ( ) ;
246
+ var conversation = await convService . GetConversation ( conn . ConversationId ) ;
247
+
248
+ var message = new RoleDialogModel ( AgentRole . User , conn . Data )
249
+ {
250
+ CurrentAgentId = routing . Context . GetCurrentAgentId ( )
251
+ } ;
252
+ dialogs . Add ( message ) ;
253
+
254
+ foreach ( var hook in hookProvider . HooksOrderByPriority )
255
+ {
256
+ hook . SetAgent ( agent )
257
+ . SetConversation ( conversation ) ;
258
+
259
+ await hook . OnMessageReceived ( message ) ;
260
+ }
261
+
262
+ await completer . InsertConversationItem ( message ) ;
263
+ await completer . TriggerModelInference ( "Reply based on the user input" ) ;
264
+ }
265
+
266
+ private async Task HandleUserDisconnected ( RealtimeHubConnection conn )
267
+ {
268
+ // Save dialog history
269
+ var routing = _services . GetRequiredService < IRoutingService > ( ) ;
270
+ var storage = _services . GetRequiredService < IConversationStorage > ( ) ;
271
+ var dialogs = routing . Context . GetDialogs ( ) ;
272
+ foreach ( var item in dialogs )
273
+ {
274
+ storage . Append ( conn . ConversationId , item ) ;
275
+ }
276
+ }
277
+
201
278
private async Task SendEventToUser ( WebSocket webSocket , object message )
202
279
{
203
280
var data = JsonSerializer . Serialize ( message ) ;
0 commit comments