Merge branch 'master' of https://github.com/SciSharp/BotSharp into features/call-dummy-func

This commit is contained in:
Jicheng Lu 2025-03-18 23:31:08 -05:00
commit 369ca68f5b
12 changed files with 130 additions and 57 deletions

View file

@ -20,7 +20,7 @@ public class PageActionArgs
public bool OpenNewTab { get; set; } = true;
[JsonPropertyName("open_blank_page")]
public bool OpenBlankPage { get; set; } = true;
[JsonPropertyName("enable_response_callback")]
public bool EnableResponseCallback { get; set; } = false;
/// <summary>

View file

@ -7,9 +7,20 @@ public class WebPageResponseData
public string ResponseData { get; set; } = null!;
public bool ResponseInMemory { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public string Method { get; set; }
public List<WebPageCookieData> Cookies { get; set; }
public int ResponseCode { get; set; }
public override string ToString()
{
return $"{Url} {ResponseData.Length}";
}
}
public class WebPageCookieData
{
public string Name { get; set; } = null!;
public string Value { get; set; } = null!;
public string Domain { get; set; } = null!;
public string Path { get; set; } = null!;
public float Expires { get; set; }
}

View file

@ -116,13 +116,14 @@ public class RealtimeHub : IRealtimeHub
}
else
{
// Push dialogs into model context
// Append dialogs into model context
var history = "[CONVERSATION HISTORY]\r\n";
foreach (var message in dialogs)
{
await _completer.InsertConversationItem(message);
history += $"{message.Role}: {message.Content}\r\n";
}
await _completer.TriggerModelInference($"{instruction}\r\n\r\nAssist user without repeating your previous statement.");
await _completer.TriggerModelInference($"{instruction}\r\n\r\n{history}\r\n\r\nAssist user without repeating your previous statement.");
}
},
onModelAudioDeltaReceived: async (audioDeltaData, itemId) =>

View file

@ -9,6 +9,15 @@
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<None Remove="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\functions\util-twilio-transfer_phone_call.json" />
<None Remove="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\functions\util-twilio-hangup_phone_call.json" />
<None Remove="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\functions\util-twilio-text_message.json" />
<None Remove="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\functions\util-twilio-outbound_phone_call.json" />
<None Remove="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\templates\util-twilio-hangup_phone_call.fn.liquid" />
<None Remove="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\templates\util-twilio-outbound_phone_call.fn.liquid" />
</ItemGroup>
<ItemGroup>
<Content Include="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\functions\util-twilio-transfer_phone_call.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
@ -22,6 +31,12 @@
<Content Include="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\functions\util-twilio-outbound_phone_call.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\templates\util-twilio-hangup_phone_call.fn.liquid">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\templates\util-twilio-outbound_phone_call.fn.liquid">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>

View file

@ -164,7 +164,7 @@ public class TwilioVoiceController : TwilioController
OnlyOnce = true
});
response = twilio.HangUp(null);
response = twilio.HangUp(string.Empty);
}
// keep waiting for user response
else
@ -433,8 +433,18 @@ public class TwilioVoiceController : TwilioController
[HttpPost("twilio/voice/hang-up")]
public async Task<TwiMLResult> Hangup(ConversationalVoiceRequest request)
{
var instruction = new ConversationalVoiceResponse
{
ConversationId = request.ConversationId
};
if (request.InitAudioFile != null)
{
instruction.SpeechPaths.Add(request.InitAudioFile);
}
var twilio = _services.GetRequiredService<TwilioService>();
var response = twilio.HangUp("twilio/bye.mp3");
var response = twilio.HangUp(instruction);
return TwiML(response);
}

View file

@ -1,4 +1,6 @@
using BotSharp.Abstraction.Files;
using BotSharp.Abstraction.Routing;
using BotSharp.Core.Infrastructures;
using BotSharp.Plugin.Twilio.OutboundPhoneCallHandler.LlmContexts;
using Twilio.Rest.Api.V2010.Account;
@ -27,6 +29,7 @@ public class HangupPhoneCallFn : IFunctionCallback
{
var args = JsonSerializer.Deserialize<HangupPhoneCallArgs>(message.FunctionArgs);
var fileStorage = _services.GetRequiredService<IFileStorageService>();
var routing = _services.GetRequiredService<IRoutingService>();
var conversationId = routing.Context.ConversationId;
var states = _services.GetRequiredService<IConversationStateService>();
@ -39,21 +42,28 @@ public class HangupPhoneCallFn : IFunctionCallback
return false;
}
if (args.AnythingElseToHelp)
{
message.Content = "Tell me how I can help.";
}
else
{
var call = CallResource.Update(
url: new Uri($"{_twilioSetting.CallbackHost}/twilio/voice/hang-up?conversation-id={conversationId}"),
pathSid: callSid
);
var processUrl = $"{_twilioSetting.CallbackHost}/twilio/voice/hang-up?conversation-id={conversationId}";
message.Content = "The call is ending.";
message.StopCompletion = true;
// Generate initial assistant audio
string initAudioFile = null;
if (!string.IsNullOrEmpty(args.ResponseContent))
{
var completion = CompletionProvider.GetAudioCompletion(_services, "openai", "tts-1");
var data = await completion.GenerateAudioFromTextAsync(args.ResponseContent);
initAudioFile = "ending.mp3";
fileStorage.SaveSpeechFile(conversationId, initAudioFile, data);
processUrl += $"&init-audio-file={initAudioFile}";
}
var call = CallResource.Update(
url: new Uri(processUrl),
pathSid: callSid
);
message.Content = args.Reason;
message.StopCompletion = true;
return true;
}
}

View file

@ -35,44 +35,35 @@ public class TransferPhoneCallFn : IFunctionCallback
var fileStorage = _services.GetRequiredService<IFileStorageService>();
var states = _services.GetRequiredService<IConversationStateService>();
var routing = _services.GetRequiredService<IRoutingService>();
var conversationId = routing.Context.ConversationId;
var processUrl = $"{_twilioSetting.CallbackHost}/twilio/voice/transfer-call?conversation-id={conversationId}&transfer-to={args.PhoneNumber}";
// Generate initial assistant audio
string initAudioFile = null;
if (!string.IsNullOrEmpty(args.TransitionMessage))
{
var completion = CompletionProvider.GetAudioCompletion(_services, "openai", "tts-1");
var data = await completion.GenerateAudioFromTextAsync(args.TransitionMessage);
initAudioFile = "transfer.mp3";
fileStorage.SaveSpeechFile(conversationId, initAudioFile, data);
processUrl += $"&init-audio-file={initAudioFile}";
}
if (!string.IsNullOrEmpty(initAudioFile))
{
processUrl += $"&init-audio-file={initAudioFile}";
}
// Forward call
var sid = states.GetState("twilio_call_sid");
if (string.IsNullOrEmpty(sid))
{
_logger.LogError("Twilio call sid is empty.");
message.Content = "There is an error when transferring the phone call.";
return false;
}
else
{
var call = CallResource.Update(
pathSid: sid,
url: new Uri(processUrl));
message.Content = args.TransitionMessage;
return true;
var routing = _services.GetRequiredService<IRoutingService>();
var conversationId = routing.Context.ConversationId;
var processUrl = $"{_twilioSetting.CallbackHost}/twilio/voice/transfer-call?conversation-id={conversationId}&transfer-to={args.PhoneNumber}";
// Generate initial assistant audio
if (!string.IsNullOrEmpty(args.TransitionMessage))
{
var completion = CompletionProvider.GetAudioCompletion(_services, "openai", "tts-1");
var data = await completion.GenerateAudioFromTextAsync(args.TransitionMessage);
var initAudioFile = "transfer.mp3";
fileStorage.SaveSpeechFile(conversationId, initAudioFile, data);
processUrl += $"&init-audio-file={initAudioFile}";
}
// Transfer call
var call = CallResource.Update(
pathSid: sid,
url: new Uri(processUrl));
message.Content = args.TransitionMessage;
return true;
}
}

View file

@ -4,6 +4,9 @@ namespace BotSharp.Plugin.Twilio.OutboundPhoneCallHandler.LlmContexts;
public class HangupPhoneCallArgs
{
[JsonPropertyName("anything_else_to_help")]
public bool AnythingElseToHelp { get; set; } = true;
[JsonPropertyName("reason")]
public string Reason { get; set; } = null!;
[JsonPropertyName("response_content")]
public string ResponseContent { get; set; } = null!;
}

View file

@ -149,6 +149,22 @@ public class TwilioService
return response;
}
public VoiceResponse HangUp(ConversationalVoiceResponse voiceResponse)
{
var response = new VoiceResponse();
var conversationId = voiceResponse.ConversationId;
if (voiceResponse.SpeechPaths != null && voiceResponse.SpeechPaths.Any())
{
foreach (var speechPath in voiceResponse.SpeechPaths)
{
var uri = GetSpeechPath(conversationId, speechPath);
response.Play(new Uri(uri));
}
}
response.Hangup();
return response;
}
public VoiceResponse DialCsrAgent(string speechPath)
{
var response = new VoiceResponse();

View file

@ -9,11 +9,11 @@
"type": "string",
"description": "The reason why user wants to end the phone call."
},
"anything_else_to_help": {
"type": "boolean",
"description": "Check if user has anything else to help."
"response_content": {
"type": "string",
"description": "A statement said to the user when politely ending a conversation."
}
},
"required": [ "reason", "anything_else_to_help" ]
"required": [ "reason", "response_content" ]
}
}

View file

@ -1,5 +1,6 @@
using Azure;
using BotSharp.Abstraction.Browsing.Settings;
using Microsoft.Playwright;
using System.IO;
namespace BotSharp.Plugin.WebDriver.Drivers.PlaywrightDriver;
@ -131,7 +132,6 @@ public class PlaywrightInstance : IDisposable
{
return page;
}
page.Request += async (sender, e) =>
{
await HandleFetchRequest(e, message, args);
@ -154,7 +154,7 @@ public class PlaywrightInstance : IDisposable
public async Task HandleFetchResponse(IResponse response, MessageInfo message, PageActionArgs args)
{
if (response.Status != 204 &&
if (response.Status != 204 && response.Status != 302 &&
response.Headers.ContainsKey("content-type") &&
(response.Request.ResourceType == "fetch" || response.Request.ResourceType == "xhr") &&
(args.ExcludeResponseUrls == null || !args.ExcludeResponseUrls.Any(url => response.Url.ToLower().Contains(url))) &&
@ -164,11 +164,23 @@ public class PlaywrightInstance : IDisposable
try
{
var context = await GetContext(message.ContextId);
var cookies = await context.CookiesAsync(new string[] { response.Url });
var result = new WebPageResponseData
{
Url = response.Url.ToLower(),
PostData = response.Request?.PostData ?? string.Empty,
ResponseInMemory = args.ResponseInMemory
ResponseInMemory = args.ResponseInMemory,
Method = response.Request.Method,
ResponseCode = response.Status,
Cookies = cookies.Select(x => new WebPageCookieData
{
Name = x.Name,
Value = x.Value,
Domain = x.Domain,
Path = x.Path,
Expires = x.Expires
}).ToList()
};
var html = await response.TextAsync();

View file

@ -15,6 +15,10 @@
"open_blank_page": {
"type": "boolean",
"description": "Open blank page"
},
"enable_response_callback": {
"type": "boolean",
"description": "Enable response callback"
}
},
"required": [ "url" ]