Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Up @@ -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>
Expand Down
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>
Expand Down
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
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
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
Expand Up @@ -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 =>
Expand Down Expand Up @@ -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>();
Expand Down
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;
Expand Down Expand Up @@ -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
};
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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);
}

Expand All @@ -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>();
Expand Down
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
Expand Up @@ -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
Expand Up @@ -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; }
Expand Down
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;

Expand Down
Loading
Loading