Skip to content

add twilio outbound phone call utility #755

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Nov 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions src/Plugins/BotSharp.Plugin.Twilio/BotSharp.Plugin.Twilio.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,20 @@
<OutputPath>$(SolutionDir)packages</OutputPath>
</PropertyGroup>

<ItemGroup>
<None Remove="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\functions\twilio_outbound_phone_call.json" />
<None Remove="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\templates\twilio_outbound_phone_call.fn.liquid" />
</ItemGroup>

<ItemGroup>
<Content Include="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\functions\twilio_outbound_phone_call.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\templates\twilio_outbound_phone_call.fn.liquid">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>

<ItemGroup>
<PackageReference Include="StackExchange.Redis" Version="2.7.27" />
<PackageReference Include="StrongGrid" Version="0.108.0" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using BotSharp.Plugin.Twilio.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System.ComponentModel.DataAnnotations;
using Twilio.Http;

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

[ValidateRequest]
[HttpPost("twilio/voice/init-call")]
public TwiMLResult InitiateOutboundCall(VoiceRequest request, [Required][FromQuery] string conversationId)
{
var instruction = new ConversationalVoiceResponse
{
ActionOnEmptyResult = true,
CallbackPath = $"twilio/voice/{conversationId}/receive/1",
SpeechPaths = new List<string>
{
$"twilio/voice/speeches/{conversationId}/intial.mp3"
}
};
var twilio = _services.GetRequiredService<TwilioService>();
var response = twilio.ReturnNoninterruptedInstructions(instruction);
return TwiML(response);
}

[ValidateRequest]
[HttpGet("twilio/voice/speeches/{conversationId}/{fileName}")]
public async Task<FileContentResult> GetSpeechFile([FromRoute] string conversationId, [FromRoute] string fileName)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace BotSharp.Plugin.Twilio.OutboundPhoneCallHandler.Enums
{
public class UtilityName
{
public const string OutboundPhoneCall = "twilio-outbound-phone-call";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using BotSharp.Abstraction.Files;
using BotSharp.Abstraction.Options;
using BotSharp.Core.Infrastructures;
using BotSharp.Plugin.Twilio.Interfaces;
using BotSharp.Plugin.Twilio.Models;
using BotSharp.Plugin.Twilio.OutboundPhoneCallHandler.LlmContexts;
using Twilio.Rest.Api.V2010.Account;
using Twilio.Types;

namespace BotSharp.Plugin.Twilio.OutboundPhoneCallHandler.Functions
{
public class HandleOutboundPhoneCallFn : IFunctionCallback
{
private readonly IServiceProvider _services;
private readonly ILogger<HandleOutboundPhoneCallFn> _logger;
private readonly BotSharpOptions _options;
private readonly TwilioSetting _twilioSetting;

public string Name => "twilio_outbound_phone_call";
public string Indication => "Dialing the number";

public HandleOutboundPhoneCallFn(
IServiceProvider services,
ILogger<HandleOutboundPhoneCallFn> logger,
BotSharpOptions options,
TwilioSetting twilioSetting)
{
_services = services;
_logger = logger;
_options = options;
_twilioSetting = twilioSetting;
}

public async Task<bool> Execute(RoleDialogModel message)
{
var args = JsonSerializer.Deserialize<LlmContextIn>(message.FunctionArgs, _options.JsonSerializerOptions);
if (args.PhoneNumber.Length != 12 || !args.PhoneNumber.StartsWith("+1", StringComparison.OrdinalIgnoreCase))
{
_logger.LogError("Invalid phone number format: {phone}", args.PhoneNumber);
return false;
}
if (string.IsNullOrWhiteSpace(args.InitialMessage))
{
_logger.LogError("Initial message is empty.");
return false;
}
var completion = CompletionProvider.GetAudioCompletion(_services, "openai", "tts-1");
var fileStorage = _services.GetRequiredService<IFileStorageService>();
var data = await completion.GenerateAudioFromTextAsync(args.InitialMessage);
var conversationId = Guid.NewGuid().ToString();
var fileName = $"intial.mp3";
fileStorage.SaveSpeechFile(conversationId, fileName, data);
// TODO: Add initial message in the new conversation
var sessionManager = _services.GetRequiredService<ITwilioSessionManager>();
await sessionManager.SetAssistantReplyAsync(conversationId, 0, new AssistantMessage
{
Content = args.InitialMessage,
SpeechFileName = fileName
});
var call = await CallResource.CreateAsync(
url: new Uri($"{_twilioSetting.CallbackHost}/twilio/voice/init-call?conversationId={conversationId}"),
to: new PhoneNumber(args.PhoneNumber),
from: new PhoneNumber(_twilioSetting.PhoneNumber));
message.StopCompletion = true;
return true;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using BotSharp.Abstraction.Agents.Models;
using BotSharp.Abstraction.Agents.Settings;
using BotSharp.Abstraction.Repositories;
using BotSharp.Abstraction.Utilities;
using BotSharp.Plugin.Twilio.OutboundPhoneCallHandler.Enums;

namespace BotSharp.Plugin.Twilio.OutboundPhoneCallHandler.Hooks
{
internal class OutboundPhoneCallHandlerHook : AgentHookBase
{
private static string FUNCTION_NAME = "twilio_outbound_phone_call";

public override string SelfId => string.Empty;

public OutboundPhoneCallHandlerHook(IServiceProvider services, AgentSettings settings) : base(services, settings)
{
}

public override void OnAgentLoaded(Agent agent)
{
var conv = _services.GetRequiredService<IConversationService>();
var isConvMode = conv.IsConversationMode();
var isEnabled = !agent.Utilities.IsNullOrEmpty() && agent.Utilities.Contains(UtilityName.OutboundPhoneCall);

if (isConvMode && isEnabled)
{
var (prompt, fn) = GetPromptAndFunction();
if (fn != null)
{
if (!string.IsNullOrWhiteSpace(prompt))
{
agent.Instruction += $"\r\n\r\n{prompt}\r\n\r\n";
}

if (agent.Functions == null)
{
agent.Functions = new List<FunctionDef> { fn };
}
else
{
agent.Functions.Add(fn);
}
}
}

base.OnAgentLoaded(agent);
}

private (string, FunctionDef) GetPromptAndFunction()
{
var db = _services.GetRequiredService<IBotSharpRepository>();
var agent = db.GetAgent(BuiltInAgentId.UtilityAssistant);
var prompt = agent?.Templates?.FirstOrDefault(x => x.Name.IsEqualTo($"{FUNCTION_NAME}.fn"))?.Content ?? string.Empty;
var loadAttachmentFn = agent?.Functions?.FirstOrDefault(x => x.Name.IsEqualTo(FUNCTION_NAME));
return (prompt, loadAttachmentFn);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using BotSharp.Plugin.Twilio.OutboundPhoneCallHandler.Enums;

namespace BotSharp.Plugin.Twilio.OutboundPhoneCallHandler.Hooks
{
public class OutboundPhoneCallHandlerUtilityHook : IAgentUtilityHook
{
public void AddUtilities(List<string> utilities)
{
utilities.Add(UtilityName.OutboundPhoneCall);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System.Text.Json.Serialization;

namespace BotSharp.Plugin.Twilio.OutboundPhoneCallHandler.LlmContexts
{
public class LlmContextIn
{
[JsonPropertyName("phone_number")]
public string PhoneNumber { get; set; }

[JsonPropertyName("initial_message")]
public string InitialMessage { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System.Text.Json.Serialization;

namespace BotSharp.Plugin.Twilio.OutboundPhoneCallHandler.LlmContexts
{
public class LlmContextOut
{
[JsonPropertyName("conversation_id")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string ConversationId { get; set; }
}
}
5 changes: 3 additions & 2 deletions src/Plugins/BotSharp.Plugin.Twilio/Services/TwilioService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ public VoiceResponse ReturnInstructions(ConversationalVoiceResponse conversation
public VoiceResponse ReturnNoninterruptedInstructions(ConversationalVoiceResponse conversationalVoiceResponse)
{
var response = new VoiceResponse();
response.Pause(2);
if (conversationalVoiceResponse.SpeechPaths != null && conversationalVoiceResponse.SpeechPaths.Any())
{
foreach (var speechPath in conversationalVoiceResponse.SpeechPaths)
Expand Down Expand Up @@ -152,8 +153,8 @@ public VoiceResponse HoldOn(int interval, string message = null)
var response = new VoiceResponse();
var gather = new Gather()
{
Input = new List<Gather.InputEnum>()
{
Input = new List<Gather.InputEnum>()
{
Gather.InputEnum.Speech,
Gather.InputEnum.Dtmf
},
Expand Down
6 changes: 5 additions & 1 deletion src/Plugins/BotSharp.Plugin.Twilio/TwilioPlugin.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using BotSharp.Abstraction.Settings;
using BotSharp.Plugin.Twilio.Interfaces;
using BotSharp.Plugin.Twilio.OutboundPhoneCallHandler.Hooks;
using BotSharp.Plugin.Twilio.Services;
using StackExchange.Redis;
using Twilio;

namespace BotSharp.Plugin.Twilio;

Expand All @@ -18,7 +20,7 @@ public void RegisterDI(IServiceCollection services, IConfiguration config)
var settingService = provider.GetRequiredService<ISettingService>();
return settingService.Bind<TwilioSetting>("Twilio");
});

TwilioClient.Init(config["Twilio:AccountSid"], config["Twilio:AuthToken"]);
services.AddScoped<TwilioService>();

var conn = ConnectionMultiplexer.Connect(config["Twilio:RedisConnectionString"]);
Expand All @@ -28,5 +30,7 @@ public void RegisterDI(IServiceCollection services, IConfiguration config)
services.AddSingleton<TwilioMessageQueue>();
services.AddHostedService<TwilioMessageQueueService>();
services.AddTwilioRequestValidation();
services.AddScoped<IAgentHook, OutboundPhoneCallHandlerHook>();
services.AddScoped<IAgentUtilityHook, OutboundPhoneCallHandlerUtilityHook>();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"name": "twilio_outbound_phone_call",
"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.",
"parameters": {
"type": "object",
"properties": {
"phone_number": {
"to_read": "string",
"description": "The phone number which will be dialed. It needs to be a valid phone number starting with +1."
},
"initial_message": {
"to_read": "string",
"description": "The initial message which will be sent."
}
},
"required": [ "phone_number", "initial_message" ]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
** Please take a look at the conversation and decide whether user wants to make an outbound call.
** Please call handle_outbound_call if user wants to make an outbound call.