Skip to content

Commit eec704c

Browse files
author
Jicheng Lu
committed
Merge branch 'master' of https://github.com/SciSharp/BotSharp
2 parents f4cc8f4 + 2a54764 commit eec704c

File tree

10 files changed

+106
-34
lines changed

10 files changed

+106
-34
lines changed

src/Infrastructure/BotSharp.Abstraction/Browsing/Settings/WebBrowsingSettings.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,6 @@ public class WebBrowsingSettings
77
// Default timeout in milliseconds
88
public float DefaultTimeout { get; set; } = 30000;
99
public bool IsEnableScreenshot { get; set; }
10+
// Default wait time in seconds after page is opened
11+
public int DefaultWaitTime { get; set; } = 5;
1012
}

src/Infrastructure/BotSharp.Core.Realtime/Services/RealtimeHub.cs

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ public async Task Listen(WebSocket userWebSocket,
2626
{
2727
var buffer = new byte[1024 * 16];
2828
WebSocketReceiveResult result;
29-
3029

3130

3231
do
@@ -98,24 +97,33 @@ private async Task ConnectToModel(WebSocket userWebSocket)
9897

9998
routing.Context.SetDialogs(dialogs);
10099

100+
var states = _services.GetRequiredService<IConversationStateService>();
101+
101102
await _completer.Connect(_conn,
102103
onModelReady: async () =>
103104
{
104-
// Control initial session, prevent initial response interruption
105-
await _completer.UpdateSession(_conn, turnDetection: false);
106-
107-
if (dialogs.LastOrDefault()?.Role == AgentRole.Assistant)
105+
if (states.ContainsState("init_audio_file"))
108106
{
109-
await _completer.TriggerModelInference($"Rephase your last response:\r\n{dialogs.LastOrDefault()?.Content}");
107+
await _completer.UpdateSession(_conn, turnDetection: true);
110108
}
111109
else
112110
{
113-
await _completer.TriggerModelInference("Reply based on the conversation context.");
114-
}
111+
// Control initial session, prevent initial response interruption
112+
await _completer.UpdateSession(_conn, turnDetection: false);
115113

116-
// Start turn detection
117-
await Task.Delay(1000 * 8);
118-
await _completer.UpdateSession(_conn, turnDetection: true);
114+
if (dialogs.LastOrDefault()?.Role == AgentRole.Assistant)
115+
{
116+
await _completer.TriggerModelInference($"Rephase your last response:\r\n{dialogs.LastOrDefault()?.Content}");
117+
}
118+
else
119+
{
120+
await _completer.TriggerModelInference("Reply based on the conversation context.");
121+
}
122+
123+
// Start turn detection
124+
await Task.Delay(1000 * 8);
125+
await _completer.UpdateSession(_conn, turnDetection: true);
126+
}
119127
},
120128
onModelAudioDeltaReceived: async (audioDeltaData, itemId) =>
121129
{

src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioStreamController.cs

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,21 +35,26 @@ public async Task<TwiMLResult> InitiateStreamConversation(ConversationalVoiceReq
3535
throw new ArgumentNullException(nameof(VoiceRequest.CallSid));
3636
}
3737

38-
VoiceResponse response = null;
38+
VoiceResponse response = default!;
39+
40+
if (request.AnsweredBy == "machine_start" &&
41+
request.Direction == "outbound-api" &&
42+
request.InitAudioFile != null)
43+
{
44+
response = new VoiceResponse();
45+
response.Play(new Uri($"{_settings.CallbackHost}/twilio/voice/speeches/{request.ConversationId}/{request.InitAudioFile}"));
46+
return TwiML(response);
47+
}
48+
3949
var instruction = new ConversationalVoiceResponse
4050
{
4151
SpeechPaths = [],
4252
ActionOnEmptyResult = true
4353
};
4454

45-
if (_context.HttpContext.Request.Query.ContainsKey("init_audio_file"))
55+
if (request.InitAudioFile != null)
4656
{
47-
instruction.SpeechPaths.Add(_context.HttpContext.Request.Query["init_audio_file"]);
48-
}
49-
50-
if (_context.HttpContext.Request.Query.ContainsKey("conversation_id"))
51-
{
52-
request.ConversationId = _context.HttpContext.Request.Query["conversation_id"];
57+
instruction.SpeechPaths.Add(request.InitAudioFile);
5358
}
5459

5560
await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
@@ -77,6 +82,24 @@ await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
7782
return TwiML(response);
7883
}
7984

85+
[ValidateRequest]
86+
[HttpPost("twilio/stream/status")]
87+
public async Task<ActionResult> StreamConversationStatus(ConversationalVoiceRequest request)
88+
{
89+
if (request.AnsweredBy == "machine_start" &&
90+
request.Direction == "outbound-api" &&
91+
request.InitAudioFile != null &&
92+
request.CallStatus == "completed")
93+
{
94+
// voicemail
95+
await HookEmitter.Emit<ITwilioCallStatusHook>(_services, async hook =>
96+
{
97+
await hook.OnVoicemailLeft(request.ConversationId);
98+
});
99+
}
100+
return Ok();
101+
}
102+
80103
private async Task<string> InitConversation(ConversationalVoiceRequest request)
81104
{
82105
var convService = _services.GetRequiredService<IConversationService>();
@@ -104,6 +127,11 @@ private async Task<string> InitConversation(ConversationalVoiceRequest request)
104127
new(StateConst.ROUTING_MODE, "lazy"),
105128
};
106129

130+
if (request.InitAudioFile != null)
131+
{
132+
states.Add(new("init_audio_file", request.InitAudioFile));
133+
}
134+
107135
convService.SetConversationId(conversation.Id, states);
108136
convService.SaveStates();
109137

src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioVoiceController.cs

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
6666
});
6767

6868
request.ConversationId = $"TwilioVoice_{request.CallSid}";
69-
instruction.CallbackPath = $"twilio/voice/{request.ConversationId}/receive/0?{GenerateStatesParameter(request.States)}";
69+
instruction.CallbackPath = $"twilio/voice/receive/0?conversation-id={request.ConversationId}&{GenerateStatesParameter(request.States)}";
7070

7171
var twilio = _services.GetRequiredService<TwilioService>();
7272
if (string.IsNullOrWhiteSpace(request.Intent))
@@ -89,7 +89,7 @@ await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
8989
};
9090
await messageQueue.EnqueueAsync(callerMessage);
9191
response = new VoiceResponse();
92-
response.Redirect(new Uri($"{_settings.CallbackHost}/twilio/voice/{request.ConversationId}/reply/{seqNum}?{GenerateStatesParameter(request.States)}"), HttpMethod.Post);
92+
response.Redirect(new Uri($"{_settings.CallbackHost}/twilio/voice/reply/{seqNum}?conversation-id={request.ConversationId}&{GenerateStatesParameter(request.States)}"), HttpMethod.Post);
9393
}
9494

9595
await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
@@ -109,7 +109,7 @@ await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
109109
/// <param name="request"></param>
110110
/// <returns></returns>
111111
[ValidateRequest]
112-
[HttpPost("twilio/voice/{conversationId}/receive/{seqNum}")]
112+
[HttpPost("twilio/voice/receive/{seqNum}")]
113113
public async Task<TwiMLResult> ReceiveCallerMessage(ConversationalVoiceRequest request)
114114
{
115115
var twilio = _services.GetRequiredService<TwilioService>();
@@ -142,7 +142,7 @@ public async Task<TwiMLResult> ReceiveCallerMessage(ConversationalVoiceRequest r
142142
await messageQueue.EnqueueAsync(callerMessage);
143143

144144
response = new VoiceResponse();
145-
response.Redirect(new Uri($"{_settings.CallbackHost}/twilio/voice/{request.ConversationId}/reply/{request.SeqNum}?{GenerateStatesParameter(request.States)}&AIResponseWaitTime=0"), HttpMethod.Post);
145+
response.Redirect(new Uri($"{_settings.CallbackHost}/twilio/voice/reply/{request.SeqNum}?conversation-id={request.ConversationId}&{GenerateStatesParameter(request.States)}&AIResponseWaitTime=0"), HttpMethod.Post);
146146

147147
await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
148148
{
@@ -173,7 +173,7 @@ await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
173173
var instruction = new ConversationalVoiceResponse
174174
{
175175
SpeechPaths = new List<string>(),
176-
CallbackPath = $"twilio/voice/{request.ConversationId}/receive/{request.SeqNum}?{GenerateStatesParameter(request.States)}&attempts={++request.Attempts}",
176+
CallbackPath = $"twilio/voice/receive/{request.SeqNum}?conversation-id={request.ConversationId}&{GenerateStatesParameter(request.States)}&attempts={++request.Attempts}",
177177
ActionOnEmptyResult = true
178178
};
179179

@@ -203,7 +203,7 @@ await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
203203
/// <param name="request"></param>
204204
/// <returns></returns>
205205
[ValidateRequest]
206-
[HttpPost("twilio/voice/{conversationId}/reply/{seqNum}")]
206+
[HttpPost("twilio/voice/reply/{seqNum}")]
207207
public async Task<TwiMLResult> ReplyCallerMessage(ConversationalVoiceRequest request)
208208
{
209209
var nextSeqNum = request.SeqNum + 1;
@@ -276,7 +276,7 @@ await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
276276
var instruction = new ConversationalVoiceResponse
277277
{
278278
SpeechPaths = speechPaths,
279-
CallbackPath = $"twilio/voice/{request.ConversationId}/reply/{request.SeqNum}?{GenerateStatesParameter(request.States)}&AIResponseWaitTime={++request.AIResponseWaitTime}",
279+
CallbackPath = $"twilio/voice/reply/{request.SeqNum}?conversation-id={request.ConversationId}&{GenerateStatesParameter(request.States)}&AIResponseWaitTime={++request.AIResponseWaitTime}",
280280
ActionOnEmptyResult = true
281281
};
282282

@@ -315,7 +315,7 @@ await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
315315
var instruction = new ConversationalVoiceResponse
316316
{
317317
SpeechPaths = instructions,
318-
CallbackPath = $"twilio/voice/{request.ConversationId}/reply/{request.SeqNum}?{GenerateStatesParameter(request.States)}&AIResponseWaitTime={++request.AIResponseWaitTime}",
318+
CallbackPath = $"twilio/voice/reply/{request.SeqNum}?conversation-id={request.ConversationId}&{GenerateStatesParameter(request.States)}&AIResponseWaitTime={++request.AIResponseWaitTime}",
319319
ActionOnEmptyResult = true
320320
};
321321

@@ -361,7 +361,7 @@ await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
361361
var instruction = new ConversationalVoiceResponse
362362
{
363363
SpeechPaths = [$"twilio/voice/speeches/{request.ConversationId}/{reply.SpeechFileName}"],
364-
CallbackPath = $"twilio/voice/{request.ConversationId}/receive/{nextSeqNum}?{GenerateStatesParameter(request.States)}",
364+
CallbackPath = $"twilio/voice/receive/{nextSeqNum}?conversation-id={request.ConversationId}&{GenerateStatesParameter(request.States)}",
365365
ActionOnEmptyResult = true,
366366
Hints = reply.Hints
367367
};
@@ -388,7 +388,7 @@ public TwiMLResult InitiateOutboundCall(VoiceRequest request, [Required][FromQue
388388
var instruction = new ConversationalVoiceResponse
389389
{
390390
ActionOnEmptyResult = true,
391-
CallbackPath = $"twilio/voice/{conversationId}/receive/1",
391+
CallbackPath = $"twilio/voice/receive/1?conversation-id={conversationId}",
392392
SpeechPaths = new List<string>
393393
{
394394
$"twilio/voice/speeches/{conversationId}/intial.mp3"
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
using Task = System.Threading.Tasks.Task;
2+
3+
namespace BotSharp.Plugin.Twilio.Interfaces;
4+
5+
public interface ITwilioCallStatusHook
6+
{
7+
Task OnVoicemailLeft(string conversationId);
8+
}

src/Plugins/BotSharp.Plugin.Twilio/Models/ConversationalVoiceRequest.cs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ public class ConversationalVoiceRequest : VoiceRequest
77
[FromQuery(Name = "agent-id")]
88
public string AgentId { get; set; } = string.Empty;
99

10-
[FromRoute]
10+
[FromQuery(Name = "conversation-id")]
1111
public string ConversationId { get; set; } = string.Empty;
1212

1313
[FromRoute]
@@ -19,5 +19,26 @@ public class ConversationalVoiceRequest : VoiceRequest
1919

2020
public string Intent { get; set; } = string.Empty;
2121

22+
[FromQuery(Name = "init-audio-file")]
23+
public string? InitAudioFile { get; set; }
24+
2225
public List<string> States { get; set; } = [];
26+
27+
[FromForm]
28+
public string? CallbackSource { get; set; }
29+
30+
/// <summary>
31+
/// machine_start
32+
/// </summary>
33+
[FromForm]
34+
public string? AnsweredBy { get; set; }
35+
36+
[FromForm]
37+
public int MachineDetectionDuration { get; set; }
38+
39+
[FromForm]
40+
public int Duration { get; set; }
41+
42+
[FromForm]
43+
public int CallDuration { get; set; }
2344
}

src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Functions/OutboundPhoneCallFn.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,12 @@ public async Task<bool> Execute(RoleDialogModel message)
6666

6767
// Make outbound call
6868
var call = await CallResource.CreateAsync(
69-
url: new Uri($"{_twilioSetting.CallbackHost}/twilio/stream?conversation_id={newConversationId}&init_audio_file={fileName}"),
69+
url: new Uri($"{_twilioSetting.CallbackHost}/twilio/stream?conversation-id={newConversationId}&init-audio-file={fileName}"),
7070
to: new PhoneNumber(args.PhoneNumber),
71-
from: new PhoneNumber(_twilioSetting.PhoneNumber));
71+
from: new PhoneNumber(_twilioSetting.PhoneNumber),
72+
statusCallback: new Uri($"{_twilioSetting.CallbackHost}/twilio/stream/status?conversation-id={newConversationId}&init-audio-file={fileName}"),
73+
// https://www.twilio.com/docs/voice/answering-machine-detection
74+
machineDetection: "Enable");
7275

7376
var convService = _services.GetRequiredService<IConversationService>();
7477
var routing = _services.GetRequiredService<IRoutingContext>();

src/Plugins/BotSharp.Plugin.Twilio/Services/TwilioService.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ public VoiceResponse ReturnBidirectionalMediaStreamsInstructions(string conversa
199199
}
200200
}
201201
}
202+
202203
var connect = new Connect();
203204
var host = _settings.CallbackHost.Split("://").Last();
204205
connect.Stream(url: $"wss://{host}/twilio/stream/{conversationId}");

src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightInstance.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,8 @@ public async Task<IBrowserContext> InitContext(string ctxId, BrowserActionArgs a
7777
Args =
7878
[
7979
"--disable-infobars",
80-
"--test-type"
80+
"--test-type",
81+
"--no-sandbox"
8182
// "--start-maximized"
8283
]
8384
});

src/Plugins/BotSharp.Plugin.WebDriver/UtilFunctions/UtilWebGoToPageFn.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public async Task<bool> Execute(RoleDialogModel message)
2626
var _webDriver = _services.GetRequiredService<WebBrowsingSettings>();
2727
args.Timeout = _webDriver.DefaultTimeout;
2828
args.WaitForNetworkIdle = false;
29-
args.WaitTime = 5;
29+
args.WaitTime = _webDriver.DefaultWaitTime;
3030
args.OpenNewTab = true;
3131

3232
var conv = _services.GetRequiredService<IConversationService>();

0 commit comments

Comments
 (0)