Skip to content

Commit 041a9c9

Browse files
authored
Merge pull request #755 from wcao-lessen/feature/twiliocallutility
add twilio outbound phone call utility
2 parents b01acea + 34fa7d8 commit 041a9c9

File tree

12 files changed

+230
-3
lines changed

12 files changed

+230
-3
lines changed

src/Plugins/BotSharp.Plugin.Twilio/BotSharp.Plugin.Twilio.csproj

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,20 @@
88
<OutputPath>$(SolutionDir)packages</OutputPath>
99
</PropertyGroup>
1010

11+
<ItemGroup>
12+
<None Remove="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\functions\twilio_outbound_phone_call.json" />
13+
<None Remove="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\templates\twilio_outbound_phone_call.fn.liquid" />
14+
</ItemGroup>
15+
16+
<ItemGroup>
17+
<Content Include="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\functions\twilio_outbound_phone_call.json">
18+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
19+
</Content>
20+
<Content Include="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\templates\twilio_outbound_phone_call.fn.liquid">
21+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
22+
</Content>
23+
</ItemGroup>
24+
1125
<ItemGroup>
1226
<PackageReference Include="StackExchange.Redis" Version="2.7.27" />
1327
<PackageReference Include="StrongGrid" Version="0.108.0" />

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using BotSharp.Plugin.Twilio.Services;
77
using Microsoft.AspNetCore.Http;
88
using Microsoft.AspNetCore.Mvc;
9+
using System.ComponentModel.DataAnnotations;
910
using Twilio.Http;
1011

1112
namespace BotSharp.Plugin.Twilio.Controllers;
@@ -373,6 +374,24 @@ await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
373374
return TwiML(response);
374375
}
375376

377+
[ValidateRequest]
378+
[HttpPost("twilio/voice/init-call")]
379+
public TwiMLResult InitiateOutboundCall(VoiceRequest request, [Required][FromQuery] string conversationId)
380+
{
381+
var instruction = new ConversationalVoiceResponse
382+
{
383+
ActionOnEmptyResult = true,
384+
CallbackPath = $"twilio/voice/{conversationId}/receive/1",
385+
SpeechPaths = new List<string>
386+
{
387+
$"twilio/voice/speeches/{conversationId}/intial.mp3"
388+
}
389+
};
390+
var twilio = _services.GetRequiredService<TwilioService>();
391+
var response = twilio.ReturnNoninterruptedInstructions(instruction);
392+
return TwiML(response);
393+
}
394+
376395
[ValidateRequest]
377396
[HttpGet("twilio/voice/speeches/{conversationId}/{fileName}")]
378397
public async Task<FileContentResult> GetSpeechFile([FromRoute] string conversationId, [FromRoute] string fileName)
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
namespace BotSharp.Plugin.Twilio.OutboundPhoneCallHandler.Enums
2+
{
3+
public class UtilityName
4+
{
5+
public const string OutboundPhoneCall = "twilio-outbound-phone-call";
6+
}
7+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
using BotSharp.Abstraction.Files;
2+
using BotSharp.Abstraction.Options;
3+
using BotSharp.Core.Infrastructures;
4+
using BotSharp.Plugin.Twilio.Interfaces;
5+
using BotSharp.Plugin.Twilio.Models;
6+
using BotSharp.Plugin.Twilio.OutboundPhoneCallHandler.LlmContexts;
7+
using Twilio.Rest.Api.V2010.Account;
8+
using Twilio.Types;
9+
10+
namespace BotSharp.Plugin.Twilio.OutboundPhoneCallHandler.Functions
11+
{
12+
public class HandleOutboundPhoneCallFn : IFunctionCallback
13+
{
14+
private readonly IServiceProvider _services;
15+
private readonly ILogger<HandleOutboundPhoneCallFn> _logger;
16+
private readonly BotSharpOptions _options;
17+
private readonly TwilioSetting _twilioSetting;
18+
19+
public string Name => "twilio_outbound_phone_call";
20+
public string Indication => "Dialing the number";
21+
22+
public HandleOutboundPhoneCallFn(
23+
IServiceProvider services,
24+
ILogger<HandleOutboundPhoneCallFn> logger,
25+
BotSharpOptions options,
26+
TwilioSetting twilioSetting)
27+
{
28+
_services = services;
29+
_logger = logger;
30+
_options = options;
31+
_twilioSetting = twilioSetting;
32+
}
33+
34+
public async Task<bool> Execute(RoleDialogModel message)
35+
{
36+
var args = JsonSerializer.Deserialize<LlmContextIn>(message.FunctionArgs, _options.JsonSerializerOptions);
37+
if (args.PhoneNumber.Length != 12 || !args.PhoneNumber.StartsWith("+1", StringComparison.OrdinalIgnoreCase))
38+
{
39+
_logger.LogError("Invalid phone number format: {phone}", args.PhoneNumber);
40+
return false;
41+
}
42+
if (string.IsNullOrWhiteSpace(args.InitialMessage))
43+
{
44+
_logger.LogError("Initial message is empty.");
45+
return false;
46+
}
47+
var completion = CompletionProvider.GetAudioCompletion(_services, "openai", "tts-1");
48+
var fileStorage = _services.GetRequiredService<IFileStorageService>();
49+
var data = await completion.GenerateAudioFromTextAsync(args.InitialMessage);
50+
var conversationId = Guid.NewGuid().ToString();
51+
var fileName = $"intial.mp3";
52+
fileStorage.SaveSpeechFile(conversationId, fileName, data);
53+
// TODO: Add initial message in the new conversation
54+
var sessionManager = _services.GetRequiredService<ITwilioSessionManager>();
55+
await sessionManager.SetAssistantReplyAsync(conversationId, 0, new AssistantMessage
56+
{
57+
Content = args.InitialMessage,
58+
SpeechFileName = fileName
59+
});
60+
var call = await CallResource.CreateAsync(
61+
url: new Uri($"{_twilioSetting.CallbackHost}/twilio/voice/init-call?conversationId={conversationId}"),
62+
to: new PhoneNumber(args.PhoneNumber),
63+
from: new PhoneNumber(_twilioSetting.PhoneNumber));
64+
message.StopCompletion = true;
65+
return true;
66+
}
67+
}
68+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
using BotSharp.Abstraction.Agents.Models;
2+
using BotSharp.Abstraction.Agents.Settings;
3+
using BotSharp.Abstraction.Repositories;
4+
using BotSharp.Abstraction.Utilities;
5+
using BotSharp.Plugin.Twilio.OutboundPhoneCallHandler.Enums;
6+
7+
namespace BotSharp.Plugin.Twilio.OutboundPhoneCallHandler.Hooks
8+
{
9+
internal class OutboundPhoneCallHandlerHook : AgentHookBase
10+
{
11+
private static string FUNCTION_NAME = "twilio_outbound_phone_call";
12+
13+
public override string SelfId => string.Empty;
14+
15+
public OutboundPhoneCallHandlerHook(IServiceProvider services, AgentSettings settings) : base(services, settings)
16+
{
17+
}
18+
19+
public override void OnAgentLoaded(Agent agent)
20+
{
21+
var conv = _services.GetRequiredService<IConversationService>();
22+
var isConvMode = conv.IsConversationMode();
23+
var isEnabled = !agent.Utilities.IsNullOrEmpty() && agent.Utilities.Contains(UtilityName.OutboundPhoneCall);
24+
25+
if (isConvMode && isEnabled)
26+
{
27+
var (prompt, fn) = GetPromptAndFunction();
28+
if (fn != null)
29+
{
30+
if (!string.IsNullOrWhiteSpace(prompt))
31+
{
32+
agent.Instruction += $"\r\n\r\n{prompt}\r\n\r\n";
33+
}
34+
35+
if (agent.Functions == null)
36+
{
37+
agent.Functions = new List<FunctionDef> { fn };
38+
}
39+
else
40+
{
41+
agent.Functions.Add(fn);
42+
}
43+
}
44+
}
45+
46+
base.OnAgentLoaded(agent);
47+
}
48+
49+
private (string, FunctionDef) GetPromptAndFunction()
50+
{
51+
var db = _services.GetRequiredService<IBotSharpRepository>();
52+
var agent = db.GetAgent(BuiltInAgentId.UtilityAssistant);
53+
var prompt = agent?.Templates?.FirstOrDefault(x => x.Name.IsEqualTo($"{FUNCTION_NAME}.fn"))?.Content ?? string.Empty;
54+
var loadAttachmentFn = agent?.Functions?.FirstOrDefault(x => x.Name.IsEqualTo(FUNCTION_NAME));
55+
return (prompt, loadAttachmentFn);
56+
}
57+
}
58+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using BotSharp.Plugin.Twilio.OutboundPhoneCallHandler.Enums;
2+
3+
namespace BotSharp.Plugin.Twilio.OutboundPhoneCallHandler.Hooks
4+
{
5+
public class OutboundPhoneCallHandlerUtilityHook : IAgentUtilityHook
6+
{
7+
public void AddUtilities(List<string> utilities)
8+
{
9+
utilities.Add(UtilityName.OutboundPhoneCall);
10+
}
11+
}
12+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using System.Text.Json.Serialization;
2+
3+
namespace BotSharp.Plugin.Twilio.OutboundPhoneCallHandler.LlmContexts
4+
{
5+
public class LlmContextIn
6+
{
7+
[JsonPropertyName("phone_number")]
8+
public string PhoneNumber { get; set; }
9+
10+
[JsonPropertyName("initial_message")]
11+
public string InitialMessage { get; set; }
12+
}
13+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
using System.Text.Json.Serialization;
2+
3+
namespace BotSharp.Plugin.Twilio.OutboundPhoneCallHandler.LlmContexts
4+
{
5+
public class LlmContextOut
6+
{
7+
[JsonPropertyName("conversation_id")]
8+
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
9+
public string ConversationId { get; set; }
10+
}
11+
}

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ public VoiceResponse ReturnInstructions(ConversationalVoiceResponse conversation
9999
public VoiceResponse ReturnNoninterruptedInstructions(ConversationalVoiceResponse conversationalVoiceResponse)
100100
{
101101
var response = new VoiceResponse();
102+
response.Pause(2);
102103
if (conversationalVoiceResponse.SpeechPaths != null && conversationalVoiceResponse.SpeechPaths.Any())
103104
{
104105
foreach (var speechPath in conversationalVoiceResponse.SpeechPaths)
@@ -152,8 +153,8 @@ public VoiceResponse HoldOn(int interval, string message = null)
152153
var response = new VoiceResponse();
153154
var gather = new Gather()
154155
{
155-
Input = new List<Gather.InputEnum>()
156-
{
156+
Input = new List<Gather.InputEnum>()
157+
{
157158
Gather.InputEnum.Speech,
158159
Gather.InputEnum.Dtmf
159160
},

src/Plugins/BotSharp.Plugin.Twilio/TwilioPlugin.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
using BotSharp.Abstraction.Settings;
22
using BotSharp.Plugin.Twilio.Interfaces;
3+
using BotSharp.Plugin.Twilio.OutboundPhoneCallHandler.Hooks;
34
using BotSharp.Plugin.Twilio.Services;
45
using StackExchange.Redis;
6+
using Twilio;
57

68
namespace BotSharp.Plugin.Twilio;
79

@@ -18,7 +20,7 @@ public void RegisterDI(IServiceCollection services, IConfiguration config)
1820
var settingService = provider.GetRequiredService<ISettingService>();
1921
return settingService.Bind<TwilioSetting>("Twilio");
2022
});
21-
23+
TwilioClient.Init(config["Twilio:AccountSid"], config["Twilio:AuthToken"]);
2224
services.AddScoped<TwilioService>();
2325

2426
var conn = ConnectionMultiplexer.Connect(config["Twilio:RedisConnectionString"]);
@@ -28,5 +30,7 @@ public void RegisterDI(IServiceCollection services, IConfiguration config)
2830
services.AddSingleton<TwilioMessageQueue>();
2931
services.AddHostedService<TwilioMessageQueueService>();
3032
services.AddTwilioRequestValidation();
33+
services.AddScoped<IAgentHook, OutboundPhoneCallHandlerHook>();
34+
services.AddScoped<IAgentUtilityHook, OutboundPhoneCallHandlerUtilityHook>();
3135
}
3236
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"name": "twilio_outbound_phone_call",
3+
"description": "If the user wants to initiate a phone call, you need to capture the phone number and compose the message the users wants to send. Then call this function to make an outbound call via Twilio.",
4+
"parameters": {
5+
"type": "object",
6+
"properties": {
7+
"phone_number": {
8+
"to_read": "string",
9+
"description": "The phone number which will be dialed. It needs to be a valid phone number starting with +1."
10+
},
11+
"initial_message": {
12+
"to_read": "string",
13+
"description": "The initial message which will be sent."
14+
}
15+
},
16+
"required": [ "phone_number", "initial_message" ]
17+
}
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
** Please take a look at the conversation and decide whether user wants to make an outbound call.
2+
** Please call handle_outbound_call if user wants to make an outbound call.

0 commit comments

Comments
 (0)