Skip to content
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

Twilio improvement #945

Merged
merged 4 commits into from
Mar 14, 2025
Merged
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
Original file line number Diff line number Diff line change
@@ -78,7 +78,4 @@ public virtual Task OnBreakpointUpdated(string conversationId, bool resetStates)

public virtual Task OnNotificationGenerated(RoleDialogModel message)
=> Task.CompletedTask;

public virtual Task OnUserDisconnected(Conversation conversation)
=> Task.CompletedTask;
}
Original file line number Diff line number Diff line change
@@ -25,13 +25,6 @@ public interface IConversationHook
/// <returns></returns>
Task OnUserAgentConnectedInitially(Conversation conversation);

/// <summary>
/// Triggered when user disconnects with agent.
/// </summary>
/// <param name="conversation"></param>
/// <returns></returns>
Task OnUserDisconnected(Conversation conversation);

/// <summary>
/// Triggered once for every new conversation.
/// </summary>
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>$(TargetFramework)</TargetFramework>
<LangVersion>$(LangVersion)</LangVersion>
<VersionPrefix>$(BotSharpVersion)</VersionPrefix>
<GeneratePackageOnBuild>$(GeneratePackageOnBuild)</GeneratePackageOnBuild>
<OutputPath>$(SolutionDir)packages</OutputPath>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
using BotSharp.Abstraction.Utilities;
using BotSharp.Core.Infrastructures;
using Microsoft.AspNetCore.Cors.Infrastructure;

namespace BotSharp.Core.Realtime.Services;

public class RealtimeHub : IRealtimeHub
@@ -257,9 +253,7 @@ private async Task HandleUserDtmfReceived()

private async Task HandleUserDisconnected()
{
var convService = _services.GetRequiredService<IConversationService>();
var conversation = await convService.GetConversation(_conn.ConversationId);
await HookEmitter.Emit<IConversationHook>(_services, x => x.OnUserDisconnected(conversation));

}

private async Task SendEventToUser(WebSocket webSocket, object message)
Original file line number Diff line number Diff line change
@@ -13,6 +13,9 @@
<Content Include="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\functions\util-twilio-hangup_phone_call.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\functions\util-twilio-text_message.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\functions\util-twilio-outbound_phone_call.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using BotSharp.Core.Infrastructures;
using BotSharp.Plugin.Twilio.Interfaces;
using BotSharp.Plugin.Twilio.Models;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

namespace BotSharp.Plugin.Twilio.Controllers;

public class TwilioRecordController : TwilioController
{
private readonly TwilioSetting _settings;
private readonly IServiceProvider _services;
private readonly ILogger _logger;

public TwilioRecordController(TwilioSetting settings, IServiceProvider services, IHttpContextAccessor context, ILogger<TwilioRecordController> logger)
{
_settings = settings;
_services = services;
_logger = logger;
}

[ValidateRequest]
[HttpPost("twilio/record/status")]
public async Task<ActionResult> PhoneRecordingStatus(ConversationalVoiceRequest request)
{
if (request.RecordingStatus == "completed")
{
_logger.LogInformation($"Recording completed for {request.CallSid}, the record URL is {request.RecordingUrl}");

// Set the recording URL to the conversation state
var convService = _services.GetRequiredService<IConversationService>();
convService.SetConversationId(request.ConversationId, new List<MessageState>
{
new("phone_recording_url", request.RecordingUrl)
});
convService.SaveStates();

// recording completed
await HookEmitter.Emit<ITwilioCallStatusHook>(_services, x => x.OnRecordingCompleted(request));
}

return Ok();
}
}
Original file line number Diff line number Diff line change
@@ -42,19 +42,20 @@ public async Task<TwiMLResult> InitiateStreamConversation(ConversationalVoiceReq
request.InitAudioFile != null)
{
response = new VoiceResponse();
response.Play(new Uri($"{_settings.CallbackHost}/twilio/voice/speeches/{request.ConversationId}/{request.InitAudioFile}"));
response.Play(new Uri(request.InitAudioFile));
return TwiML(response);
}

var instruction = new ConversationalVoiceResponse
{
ConversationId = request.ConversationId,
SpeechPaths = [],
ActionOnEmptyResult = true
};

if (request.InitAudioFile != null)
{
instruction.SpeechPaths.Add(request.InitAudioFile);
instruction.SpeechPaths.Add($"twilio/voice/speeches/{request.ConversationId}/{request.InitAudioFile}");
}

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

[ValidateRequest]
[HttpPost("twilio/stream/status")]
public async Task<ActionResult> StreamConversationStatus(ConversationalVoiceRequest request)
{
if (request.AnsweredBy == "machine_start" &&
request.Direction == "outbound-api" &&
request.InitAudioFile != null &&
request.CallStatus == "completed")
{
// voicemail
await HookEmitter.Emit<ITwilioCallStatusHook>(_services, async hook =>
{
await hook.OnVoicemailLeft(request.ConversationId);
});
}
return Ok();
}

private async Task<string> InitConversation(ConversationalVoiceRequest request)
{
var convService = _services.GetRequiredService<IConversationService>();
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
using BotSharp.Abstraction.Files;
using BotSharp.Abstraction.Infrastructures;
using BotSharp.Abstraction.Repositories;
using BotSharp.Core.Infrastructures;
using BotSharp.Plugin.Twilio.Interfaces;
using BotSharp.Plugin.Twilio.Models;
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;
@@ -54,6 +52,7 @@ public async Task<TwiMLResult> InitiateConversation(ConversationalVoiceRequest r
VoiceResponse response = null;
var instruction = new ConversationalVoiceResponse
{
ConversationId = request.ConversationId,
SpeechPaths = ["twilio/welcome.mp3"],
ActionOnEmptyResult = true
};
@@ -172,6 +171,7 @@ await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
{
var instruction = new ConversationalVoiceResponse
{
ConversationId = request.ConversationId,
SpeechPaths = new List<string>(),
CallbackPath = $"twilio/voice/receive/{request.SeqNum}?conversation-id={request.ConversationId}&{GenerateStatesParameter(request.States)}&attempts={++request.Attempts}",
ActionOnEmptyResult = true
@@ -275,6 +275,7 @@ await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>

var instruction = new ConversationalVoiceResponse
{
ConversationId = request.ConversationId,
SpeechPaths = speechPaths,
CallbackPath = $"twilio/voice/reply/{request.SeqNum}?conversation-id={request.ConversationId}&{GenerateStatesParameter(request.States)}&AIResponseWaitTime={++request.AIResponseWaitTime}",
ActionOnEmptyResult = true
@@ -314,6 +315,7 @@ await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>

var instruction = new ConversationalVoiceResponse
{
ConversationId = request.ConversationId,
SpeechPaths = instructions,
CallbackPath = $"twilio/voice/reply/{request.SeqNum}?conversation-id={request.ConversationId}&{GenerateStatesParameter(request.States)}&AIResponseWaitTime={++request.AIResponseWaitTime}",
ActionOnEmptyResult = true
@@ -360,6 +362,7 @@ await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
{
var instruction = new ConversationalVoiceResponse
{
ConversationId = request.ConversationId,
SpeechPaths = [$"twilio/voice/speeches/{request.ConversationId}/{reply.SpeechFileName}"],
CallbackPath = $"twilio/voice/receive/{nextSeqNum}?conversation-id={request.ConversationId}&{GenerateStatesParameter(request.States)}",
ActionOnEmptyResult = true,
@@ -382,23 +385,34 @@ await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
}

[ValidateRequest]
[HttpPost("twilio/voice/init-call")]
public TwiMLResult InitiateOutboundCall(VoiceRequest request, [Required][FromQuery] string conversationId)
[HttpPost("twilio/voice/init-outbound-call")]
public TwiMLResult InitiateOutboundCall(ConversationalVoiceRequest request)
{
VoiceResponse response = default!;
if (request.AnsweredBy == "machine_start" &&
request.Direction == "outbound-api" &&
request.InitAudioFile != null)
{
response = new VoiceResponse();
response.Play(new Uri(request.InitAudioFile));
return TwiML(response);
}

var instruction = new ConversationalVoiceResponse
{
ConversationId = request.ConversationId,
ActionOnEmptyResult = true,
CallbackPath = $"twilio/voice/receive/1?conversation-id={conversationId}",
SpeechPaths = new List<string>
{
$"twilio/voice/speeches/{conversationId}/intial.mp3"
}
CallbackPath = $"twilio/voice/receive/1?conversation-id={request.ConversationId}",
};
string tag = $"twilio:{Request.Form["AnsweredBy"]}";
var db = _services.GetRequiredService<IBotSharpRepository>();
db.AppendConversationTags(conversationId, new List<string> { tag });

if (request.InitAudioFile != null)
{
instruction.CallbackPath += $"&init-audio-file={request.InitAudioFile}";
instruction.SpeechPaths.Add($"twilio/voice/speeches/{request.ConversationId}/{request.InitAudioFile}");
}

var twilio = _services.GetRequiredService<TwilioService>();
var response = twilio.ReturnNoninterruptedInstructions(instruction);
response = twilio.ReturnNoninterruptedInstructions(instruction);
return TwiML(response);
}

@@ -415,6 +429,32 @@ public async Task<FileContentResult> GetSpeechFile([FromRoute] string conversati
return result;
}

[ValidateRequest]
[HttpPost("twilio/voice/status")]
public async Task<ActionResult> PhoneCallStatus(ConversationalVoiceRequest request)
{
if (request.CallStatus == "completed")
{
if (request.AnsweredBy == "machine_start" &&
request.Direction == "outbound-api" &&
request.InitAudioFile != null)
{
// voicemail
await HookEmitter.Emit<ITwilioCallStatusHook>(_services, async hook =>
{
await hook.OnVoicemailLeft(request);
});
}
else
{
// phone call completed
await HookEmitter.Emit<ITwilioCallStatusHook>(_services, x => x.OnUserDisconnected(request));
}
}

return Ok();
}

private Dictionary<string, string> ParseStates(List<string> states)
{
var result = new Dictionary<string, string>();
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
using BotSharp.Plugin.Twilio.Models;
using Task = System.Threading.Tasks.Task;

namespace BotSharp.Plugin.Twilio.Interfaces;

public interface ITwilioCallStatusHook
{
Task OnVoicemailLeft(string conversationId);
Task OnVoicemailLeft(ConversationalVoiceRequest request);
Task OnUserDisconnected(ConversationalVoiceRequest request);
Task OnRecordingCompleted(ConversationalVoiceRequest request);
}
Original file line number Diff line number Diff line change
@@ -6,7 +6,7 @@ public class AssistantMessage
public bool HumanIntervationNeeded { get; set; }
public string Content { get; set; }
public string MessageId { get; set; }
public string SpeechFileName { get; set; }
public string? SpeechFileName { get; set; }
public string Hints { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -2,6 +2,7 @@ namespace BotSharp.Plugin.Twilio.Models;

public class ConversationalVoiceResponse
{
public string ConversationId { get; set; } = null!;
public List<string> SpeechPaths { get; set; } = [];
public string CallbackPath { get; set; }
public bool ActionOnEmptyResult { get; set; }
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using BotSharp.Plugin.Twilio.OutboundPhoneCallHandler.LlmContexts;
using Microsoft.VisualBasic;
using Twilio.Rest.Api.V2010.Account;
using Task = System.Threading.Tasks.Task;

Loading
Oops, something went wrong.
Loading
Oops, something went wrong.