Add Twilio Hook
This commit is contained in:
parent
421fbb5956
commit
8b645a084a
|
|
@ -10,6 +10,13 @@ public interface IConversationHook
|
|||
Conversation Conversation { get; }
|
||||
IConversationHook SetConversation(Conversation conversation);
|
||||
|
||||
/// <summary>
|
||||
/// Get the predifined intent for the conversation.
|
||||
/// It will send to the conversation context to help LLM to understand the user's intent.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
Task<string> GetConversationIntent() => Task.FromResult(string.Empty);
|
||||
|
||||
/// <summary>
|
||||
/// Triggered when user connects with agent first time.
|
||||
/// This hook is the good timing to show welcome infomation.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
namespace BotSharp.Abstraction.Infrastructures;
|
||||
|
||||
public class HookEmitOption
|
||||
{
|
||||
public bool OnlyOnce { get; set; }
|
||||
}
|
||||
|
|
@ -4,11 +4,12 @@ namespace BotSharp.Core.Infrastructures;
|
|||
|
||||
public static class HookEmitter
|
||||
{
|
||||
public static HookEmittedResult Emit<T>(IServiceProvider services, Action<T> action)
|
||||
public static HookEmittedResult Emit<T>(IServiceProvider services, Action<T> action, HookEmitOption? option = null)
|
||||
{
|
||||
var logger = services.GetRequiredService<ILogger<T>>();
|
||||
var result = new HookEmittedResult();
|
||||
var hooks = services.GetServices<T>();
|
||||
option = option ?? new();
|
||||
|
||||
foreach (var hook in hooks)
|
||||
{
|
||||
|
|
@ -16,6 +17,11 @@ public static class HookEmitter
|
|||
{
|
||||
logger.LogInformation($"Emit hook action on {action.Method.Name}({hook.GetType().Name})");
|
||||
action(hook);
|
||||
|
||||
if (option.OnlyOnce)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
@ -26,11 +32,12 @@ public static class HookEmitter
|
|||
return result;
|
||||
}
|
||||
|
||||
public static async Task<HookEmittedResult> Emit<T>(IServiceProvider services, Func<T, Task> action)
|
||||
public static async Task<HookEmittedResult> Emit<T>(IServiceProvider services, Func<T, Task> action, HookEmitOption? option = null)
|
||||
{
|
||||
var logger = services.GetRequiredService<ILogger<T>>();
|
||||
var result = new HookEmittedResult();
|
||||
var hooks = services.GetServices<T>();
|
||||
option = option ?? new();
|
||||
|
||||
foreach (var hook in hooks)
|
||||
{
|
||||
|
|
@ -38,6 +45,11 @@ public static class HookEmitter
|
|||
{
|
||||
logger.LogInformation($"Emit hook action on {action.Method.Name}({hook.GetType().Name})");
|
||||
await action(hook);
|
||||
|
||||
if (option.OnlyOnce)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
using BotSharp.Abstraction.Files;
|
||||
using BotSharp.Abstraction.Infrastructures;
|
||||
using BotSharp.Core.Infrastructures;
|
||||
using BotSharp.Plugin.Twilio.Interfaces;
|
||||
using BotSharp.Plugin.Twilio.Models;
|
||||
using BotSharp.Plugin.Twilio.Services;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
|
@ -32,122 +34,202 @@ public class TwilioVoiceController : TwilioController
|
|||
/// <exception cref="ArgumentNullException"></exception>
|
||||
[ValidateRequest]
|
||||
[HttpPost("twilio/voice/welcome")]
|
||||
public async Task<TwiMLResult> InitiateConversation(VoiceRequest request, [FromQuery] string[] states, [FromQuery] string intent)
|
||||
public async Task<TwiMLResult> InitiateConversation(ConversationalVoiceRequest request)
|
||||
{
|
||||
var text = JsonSerializer.Serialize(request);
|
||||
if (request?.CallSid == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(VoiceRequest.CallSid));
|
||||
}
|
||||
|
||||
string conversationId = $"TwilioVoice_{request.CallSid}";
|
||||
var twilio = _services.GetRequiredService<TwilioService>();
|
||||
VoiceResponse response;
|
||||
if (string.IsNullOrWhiteSpace(intent))
|
||||
VoiceResponse response = null;
|
||||
request.ConversationId = $"TwilioVoice_{request.CallSid}";
|
||||
|
||||
var instruction = new ConversationalVoiceResponse
|
||||
{
|
||||
var url = $"twilio/voice/{conversationId}/receive/0?{GenerateStatesParameter(states)}";
|
||||
response = twilio.ReturnNoninterruptedInstructions(new List<string> { "twilio/welcome.mp3" }, url, true, timeout: 2);
|
||||
SpeechPaths = ["twilio/welcome.mp3"],
|
||||
CallbackPath = $"twilio/voice/{request.ConversationId}/receive/0?{GenerateStatesParameter(request.States)}",
|
||||
ActionOnEmptyResult = true,
|
||||
Timeout = 2
|
||||
};
|
||||
await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
|
||||
{
|
||||
await hook.OnSessionCreating(request, instruction);
|
||||
}, new HookEmitOption
|
||||
{
|
||||
OnlyOnce = true
|
||||
});
|
||||
|
||||
var twilio = _services.GetRequiredService<TwilioService>();
|
||||
if (string.IsNullOrWhiteSpace(request.Intent))
|
||||
{
|
||||
response = twilio.ReturnNoninterruptedInstructions(instruction);
|
||||
}
|
||||
else
|
||||
{
|
||||
int seqNum = 0;
|
||||
var messageQueue = _services.GetRequiredService<TwilioMessageQueue>();
|
||||
var sessionManager = _services.GetRequiredService<ITwilioSessionManager>();
|
||||
await sessionManager.StageCallerMessageAsync(conversationId, seqNum, intent);
|
||||
await sessionManager.StageCallerMessageAsync(request.ConversationId, seqNum, request.Intent);
|
||||
var callerMessage = new CallerMessage()
|
||||
{
|
||||
ConversationId = conversationId,
|
||||
ConversationId = request.ConversationId,
|
||||
SeqNumber = seqNum,
|
||||
Content = intent,
|
||||
Content = request.Intent,
|
||||
From = request.From,
|
||||
States = ParseStates(states)
|
||||
States = ParseStates(request.States)
|
||||
};
|
||||
await messageQueue.EnqueueAsync(callerMessage);
|
||||
response = new VoiceResponse().Redirect(new Uri($"{_settings.CallbackHost}/twilio/voice/{conversationId}/reply/{seqNum}?{GenerateStatesParameter(states)}"), HttpMethod.Post);
|
||||
response = new VoiceResponse();
|
||||
response.Redirect(new Uri($"{_settings.CallbackHost}/twilio/voice/{request.ConversationId}/reply/{seqNum}?{GenerateStatesParameter(request.States)}"), HttpMethod.Post);
|
||||
}
|
||||
|
||||
await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
|
||||
{
|
||||
await hook.OnSessionCreated(request);
|
||||
}, new HookEmitOption
|
||||
{
|
||||
OnlyOnce = true
|
||||
});
|
||||
|
||||
return TwiML(response);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wait for caller's response
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <returns></returns>
|
||||
[ValidateRequest]
|
||||
[HttpPost("twilio/voice/{conversationId}/receive/{seqNum}")]
|
||||
public async Task<TwiMLResult> ReceiveCallerMessage([FromRoute] string conversationId, [FromRoute] int seqNum, [FromQuery] string[] states, VoiceRequest request, [FromQuery] int attempts = 1)
|
||||
public async Task<TwiMLResult> ReceiveCallerMessage(ConversationalVoiceRequest request)
|
||||
{
|
||||
var twilio = _services.GetRequiredService<TwilioService>();
|
||||
var messageQueue = _services.GetRequiredService<TwilioMessageQueue>();
|
||||
var sessionManager = _services.GetRequiredService<ITwilioSessionManager>();
|
||||
|
||||
var messages = await sessionManager.RetrieveStagedCallerMessagesAsync(conversationId, seqNum);
|
||||
var messages = await sessionManager.RetrieveStagedCallerMessagesAsync(request.ConversationId, request.SeqNum);
|
||||
string text = (request.SpeechResult + "\r\n" + request.Digits).Trim();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
messages.Add(text);
|
||||
await sessionManager.StageCallerMessageAsync(conversationId, seqNum, text);
|
||||
await sessionManager.StageCallerMessageAsync(request.ConversationId, request.SeqNum, text);
|
||||
}
|
||||
|
||||
VoiceResponse response;
|
||||
VoiceResponse response = null;
|
||||
if (messages.Any())
|
||||
{
|
||||
var messageContent = string.Join("\r\n", messages);
|
||||
var callerMessage = new CallerMessage()
|
||||
{
|
||||
ConversationId = conversationId,
|
||||
SeqNumber = seqNum,
|
||||
ConversationId = request.ConversationId,
|
||||
SeqNumber = request.SeqNum,
|
||||
Content = messageContent,
|
||||
Digits = request.Digits,
|
||||
From = request.From,
|
||||
States = ParseStates(states)
|
||||
States = ParseStates(request.States)
|
||||
};
|
||||
await messageQueue.EnqueueAsync(callerMessage);
|
||||
|
||||
response = new VoiceResponse().Redirect(new Uri($"{_settings.CallbackHost}/twilio/voice/{conversationId}/reply/{seqNum}?{GenerateStatesParameter(states)}"), HttpMethod.Post);
|
||||
response = new VoiceResponse();
|
||||
response.Redirect(new Uri($"{_settings.CallbackHost}/twilio/voice/{request.ConversationId}/reply/{request.SeqNum}?{GenerateStatesParameter(request.States)}"), HttpMethod.Post);
|
||||
|
||||
await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
|
||||
{
|
||||
await hook.OnReceivedUserMessage(request);
|
||||
}, new HookEmitOption
|
||||
{
|
||||
OnlyOnce = true
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
if (attempts >= 2)
|
||||
// keep waiting for user response
|
||||
if (request.Attempts > 2)
|
||||
{
|
||||
var speechPaths = new List<string>();
|
||||
|
||||
if (seqNum == 0)
|
||||
var instruction = new ConversationalVoiceResponse
|
||||
{
|
||||
speechPaths.Add("twilio/welcome.mp3");
|
||||
SpeechPaths = new List<string>(),
|
||||
CallbackPath = $"twilio/voice/{request.ConversationId}/receive/{request.SeqNum}?{GenerateStatesParameter(request.States)}",
|
||||
ActionOnEmptyResult = true
|
||||
};
|
||||
|
||||
// prompt user to speak clearly
|
||||
if (request.SeqNum == 0)
|
||||
{
|
||||
instruction.SpeechPaths.Add("twilio/welcome.mp3");
|
||||
}
|
||||
else
|
||||
{
|
||||
var lastRepy = await sessionManager.GetAssistantReplyAsync(conversationId, seqNum - 1);
|
||||
speechPaths.Add($"twilio/say-it-again-{Random.Shared.Next(1, 5)}.mp3");
|
||||
speechPaths.Add($"twilio/voice/speeches/{conversationId}/{lastRepy.SpeechFileName}");
|
||||
var lastRepy = await sessionManager.GetAssistantReplyAsync(request.ConversationId, request.SeqNum - 1);
|
||||
instruction.SpeechPaths.Add($"twilio/voice/speeches/{request.ConversationId}/{lastRepy.SpeechFileName}");
|
||||
}
|
||||
response = twilio.ReturnInstructions(speechPaths, $"twilio/voice/{conversationId}/receive/{seqNum}?{GenerateStatesParameter(states)}", true);
|
||||
|
||||
await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
|
||||
{
|
||||
await hook.OnWaitingUserResponse(request, instruction);
|
||||
}, new HookEmitOption
|
||||
{
|
||||
OnlyOnce = true
|
||||
});
|
||||
|
||||
response = twilio.ReturnInstructions(instruction);
|
||||
}
|
||||
else
|
||||
{
|
||||
response = twilio.ReturnInstructions(null, $"twilio/voice/{conversationId}/receive/{seqNum}?{GenerateStatesParameter(states)}&attempts={++attempts}", true);
|
||||
var instruction = new ConversationalVoiceResponse
|
||||
{
|
||||
SpeechPaths = new List<string>(),
|
||||
CallbackPath = $"twilio/voice/{request.ConversationId}/receive/{request.SeqNum}?{GenerateStatesParameter(request.States)}&attempts={++request.Attempts}",
|
||||
ActionOnEmptyResult = true
|
||||
};
|
||||
|
||||
if (request.Attempts == 2)
|
||||
{
|
||||
instruction.SpeechPaths.Add($"twilio/say-it-again-{Random.Shared.Next(1, 5)}.mp3");
|
||||
}
|
||||
|
||||
await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
|
||||
{
|
||||
await hook.OnWaitingUserResponse(request, instruction);
|
||||
}, new HookEmitOption
|
||||
{
|
||||
OnlyOnce = true
|
||||
});
|
||||
|
||||
response = twilio.ReturnInstructions(instruction);
|
||||
}
|
||||
}
|
||||
|
||||
return TwiML(response);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Polling for assistant reply after user responsed
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <returns></returns>
|
||||
[ValidateRequest]
|
||||
[HttpPost("twilio/voice/{conversationId}/reply/{seqNum}")]
|
||||
public async Task<TwiMLResult> ReplyCallerMessage([FromRoute] string conversationId, [FromRoute] int seqNum,
|
||||
[FromQuery] string[] states, VoiceRequest request)
|
||||
public async Task<TwiMLResult> ReplyCallerMessage(ConversationalVoiceRequest request)
|
||||
{
|
||||
var nextSeqNum = seqNum + 1;
|
||||
var nextSeqNum = request.SeqNum + 1;
|
||||
var sessionManager = _services.GetRequiredService<ITwilioSessionManager>();
|
||||
var twilio = _services.GetRequiredService<TwilioService>();
|
||||
var fileStorage = _services.GetRequiredService<IFileStorageService>();
|
||||
|
||||
if (request.SpeechResult != null)
|
||||
{
|
||||
await sessionManager.StageCallerMessageAsync(conversationId, nextSeqNum, request.SpeechResult);
|
||||
await sessionManager.StageCallerMessageAsync(request.ConversationId, nextSeqNum, request.SpeechResult);
|
||||
}
|
||||
|
||||
var reply = await sessionManager.GetAssistantReplyAsync(conversationId, seqNum);
|
||||
var reply = await sessionManager.GetAssistantReplyAsync(request.ConversationId, request.SeqNum);
|
||||
VoiceResponse response;
|
||||
|
||||
if (reply == null)
|
||||
{
|
||||
var indication = await sessionManager.GetReplyIndicationAsync(conversationId, seqNum);
|
||||
var indication = await sessionManager.GetReplyIndicationAsync(request.ConversationId, request.SeqNum);
|
||||
if (indication != null)
|
||||
{
|
||||
_logger.LogWarning($"Indication: {indication}");
|
||||
|
|
@ -172,9 +254,9 @@ public class TwilioVoiceController : TwilioController
|
|||
speechPaths.Add($"twilio/hold-on-short-{holdOnIndex}.mp3");
|
||||
}
|
||||
|
||||
var fileName = $"indication_{seqNum}_{segIndex}.mp3";
|
||||
fileStorage.SaveSpeechFile(conversationId, fileName, data);
|
||||
speechPaths.Add($"twilio/voice/speeches/{conversationId}/{fileName}");
|
||||
var fileName = $"indication_{request.SeqNum}_{segIndex}.mp3";
|
||||
fileStorage.SaveSpeechFile(request.ConversationId, fileName, data);
|
||||
speechPaths.Add($"twilio/voice/speeches/{request.ConversationId}/{fileName}");
|
||||
|
||||
// add typing
|
||||
var typingIndex = Random.Shared.Next(1, 7);
|
||||
|
|
@ -185,8 +267,25 @@ public class TwilioVoiceController : TwilioController
|
|||
segIndex++;
|
||||
}
|
||||
}
|
||||
response = twilio.ReturnInstructions(speechPaths, $"twilio/voice/{conversationId}/reply/{seqNum}?{GenerateStatesParameter(states)}", true);
|
||||
await sessionManager.RemoveReplyIndicationAsync(conversationId, seqNum);
|
||||
|
||||
var instruction = new ConversationalVoiceResponse
|
||||
{
|
||||
SpeechPaths = speechPaths,
|
||||
CallbackPath = $"twilio/voice/{request.ConversationId}/reply/{request.SeqNum}?{GenerateStatesParameter(request.States)}",
|
||||
ActionOnEmptyResult = true
|
||||
};
|
||||
|
||||
await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
|
||||
{
|
||||
await hook.OnIndicationGenerated(request, instruction);
|
||||
}, new HookEmitOption
|
||||
{
|
||||
OnlyOnce = true
|
||||
});
|
||||
|
||||
response = twilio.ReturnInstructions(instruction);
|
||||
|
||||
await sessionManager.RemoveReplyIndicationAsync(request.ConversationId, request.SeqNum);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -208,25 +307,69 @@ public class TwilioVoiceController : TwilioController
|
|||
instructions.Add($"twilio/typing-{typingIndex}.mp3");
|
||||
}
|
||||
|
||||
response = twilio.ReturnInstructions(instructions, $"twilio/voice/{conversationId}/reply/{seqNum}?{GenerateStatesParameter(states)}", true);
|
||||
var instruction = new ConversationalVoiceResponse
|
||||
{
|
||||
SpeechPaths = instructions,
|
||||
CallbackPath = $"twilio/voice/{request.ConversationId}/reply/{request.SeqNum}?{GenerateStatesParameter(request.States)}",
|
||||
ActionOnEmptyResult = true
|
||||
};
|
||||
|
||||
await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
|
||||
{
|
||||
await hook.OnWaitingAgentResponse(request, instruction);
|
||||
}, new HookEmitOption
|
||||
{
|
||||
OnlyOnce = true
|
||||
});
|
||||
|
||||
response = twilio.ReturnInstructions(instruction);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (reply.HumanIntervationNeeded)
|
||||
{
|
||||
response = twilio.DialCsrAgent($"twilio/voice/speeches/{conversationId}/{reply.SpeechFileName}");
|
||||
await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
|
||||
{
|
||||
await hook.OnAgentTransferring(request, _settings);
|
||||
}, new HookEmitOption
|
||||
{
|
||||
OnlyOnce = true
|
||||
});
|
||||
|
||||
response = twilio.DialCsrAgent($"twilio/voice/speeches/{request.ConversationId}/{reply.SpeechFileName}");
|
||||
}
|
||||
else if (reply.ConversationEnd)
|
||||
{
|
||||
response = twilio.HangUp($"twilio/voice/speeches/{conversationId}/{reply.SpeechFileName}");
|
||||
response = twilio.HangUp($"twilio/voice/speeches/{request.ConversationId}/{reply.SpeechFileName}");
|
||||
|
||||
await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
|
||||
{
|
||||
await hook.OnAgentHangUp(request);
|
||||
}, new HookEmitOption
|
||||
{
|
||||
OnlyOnce = true
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
response = twilio.ReturnInstructions(new List<string>
|
||||
var instruction = new ConversationalVoiceResponse
|
||||
{
|
||||
$"twilio/voice/speeches/{conversationId}/{reply.SpeechFileName}"
|
||||
}, $"twilio/voice/{conversationId}/receive/{nextSeqNum}?{GenerateStatesParameter(states)}", true, hints: reply.Hints);
|
||||
SpeechPaths = [$"twilio/voice/speeches/{request.ConversationId}/{reply.SpeechFileName}"],
|
||||
CallbackPath = $"twilio/voice/{request.ConversationId}/receive/{nextSeqNum}?{GenerateStatesParameter(request.States)}",
|
||||
ActionOnEmptyResult = true,
|
||||
Hints = reply.Hints
|
||||
};
|
||||
|
||||
await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
|
||||
{
|
||||
await hook.OnAgentResponsing(request, instruction);
|
||||
}, new HookEmitOption
|
||||
{
|
||||
OnlyOnce = true
|
||||
});
|
||||
|
||||
response = twilio.ReturnInstructions(instruction);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -246,7 +389,7 @@ public class TwilioVoiceController : TwilioController
|
|||
return result;
|
||||
}
|
||||
|
||||
private Dictionary<string, string> ParseStates(string[] states)
|
||||
private Dictionary<string, string> ParseStates(List<string> states)
|
||||
{
|
||||
var result = new Dictionary<string, string>();
|
||||
if (states is null || !states.Any())
|
||||
|
|
@ -264,9 +407,9 @@ public class TwilioVoiceController : TwilioController
|
|||
return result;
|
||||
}
|
||||
|
||||
private string GenerateStatesParameter(string[] states)
|
||||
private string GenerateStatesParameter(List<string> states)
|
||||
{
|
||||
if (states is null || states.Length == 0)
|
||||
if (states is null || states.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,88 @@
|
|||
using BotSharp.Plugin.Twilio.Models;
|
||||
using Task = System.Threading.Tasks.Task;
|
||||
|
||||
namespace BotSharp.Plugin.Twilio.Interfaces;
|
||||
|
||||
public interface ITwilioSessionHook
|
||||
{
|
||||
/// <summary>
|
||||
/// Before session creating
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="response"></param>
|
||||
/// <returns></returns>
|
||||
Task OnSessionCreating(ConversationalVoiceRequest request, ConversationalVoiceResponse response)
|
||||
=> Task.CompletedTask;
|
||||
|
||||
/// <summary>
|
||||
/// On session created
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="response"></param>
|
||||
/// <returns></returns>
|
||||
Task OnSessionCreated(ConversationalVoiceRequest request)
|
||||
=> Task.CompletedTask;
|
||||
|
||||
/// <summary>
|
||||
/// On received user message
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="response"></param>
|
||||
/// <returns></returns>
|
||||
Task OnReceivedUserMessage(ConversationalVoiceRequest request)
|
||||
=> Task.CompletedTask;
|
||||
|
||||
/// <summary>
|
||||
/// Waiting user response
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="response"></param>
|
||||
/// <returns></returns>
|
||||
Task OnWaitingUserResponse(ConversationalVoiceRequest request, ConversationalVoiceResponse response)
|
||||
=> Task.CompletedTask;
|
||||
|
||||
/// <summary>
|
||||
/// On agent generated indication
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="response"></param>
|
||||
/// <returns></returns>
|
||||
Task OnIndicationGenerated(ConversationalVoiceRequest request, ConversationalVoiceResponse response)
|
||||
=> Task.CompletedTask;
|
||||
|
||||
/// <summary>
|
||||
/// Waiting agent response
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="response"></param>
|
||||
/// <returns></returns>
|
||||
Task OnWaitingAgentResponse(ConversationalVoiceRequest request, ConversationalVoiceResponse response)
|
||||
=> Task.CompletedTask;
|
||||
|
||||
/// <summary>
|
||||
/// Before agent responsing
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="response"></param>
|
||||
/// <returns></returns>
|
||||
Task OnAgentResponsing(ConversationalVoiceRequest request, ConversationalVoiceResponse response)
|
||||
=> Task.CompletedTask;
|
||||
|
||||
/// <summary>
|
||||
/// On agent hang up
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="response"></param>
|
||||
/// <returns></returns>
|
||||
Task OnAgentHangUp(ConversationalVoiceRequest request)
|
||||
=> Task.CompletedTask;
|
||||
|
||||
/// <summary>
|
||||
/// Before agent transferred
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="response"></param>
|
||||
/// <returns></returns>
|
||||
Task OnAgentTransferring(ConversationalVoiceRequest request, TwilioSetting settings)
|
||||
=> Task.CompletedTask;
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
using BotSharp.Plugin.Twilio.Models;
|
||||
using Task = System.Threading.Tasks.Task;
|
||||
|
||||
namespace BotSharp.Plugin.Twilio.Interfaces;
|
||||
|
||||
public interface ITwilioSessionManager
|
||||
{
|
||||
Task SetAssistantReplyAsync(string conversationId, int seqNum, AssistantMessage message);
|
||||
Task<AssistantMessage> GetAssistantReplyAsync(string conversationId, int seqNum);
|
||||
Task StageCallerMessageAsync(string conversationId, int seqNum, string message);
|
||||
Task<List<string>> RetrieveStagedCallerMessagesAsync(string conversationId, int seqNum);
|
||||
Task SetReplyIndicationAsync(string conversationId, int seqNum, string indication);
|
||||
Task<string> GetReplyIndicationAsync(string conversationId, int seqNum);
|
||||
Task RemoveReplyIndicationAsync(string conversationId, int seqNum);
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BotSharp.Plugin.Twilio.Models;
|
||||
|
||||
public class ConversationalVoiceRequest : VoiceRequest
|
||||
{
|
||||
[FromRoute]
|
||||
public string ConversationId { get; set; }
|
||||
|
||||
[FromRoute]
|
||||
public int SeqNum { get; set; }
|
||||
|
||||
public int Attempts { get; set; } = 1;
|
||||
|
||||
public string Intent { get; set; }
|
||||
|
||||
public List<string> States { get; set; } = [];
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
namespace BotSharp.Plugin.Twilio.Models;
|
||||
|
||||
public class ConversationalVoiceResponse
|
||||
{
|
||||
public List<string> SpeechPaths { get; set; } = [];
|
||||
public string CallbackPath { get; set; }
|
||||
public bool ActionOnEmptyResult { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Timeout in seconds
|
||||
/// </summary>
|
||||
public int Timeout { get; set; } = 3;
|
||||
|
||||
public string Hints { get; set; }
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
using BotSharp.Plugin.Twilio.Models;
|
||||
using Task = System.Threading.Tasks.Task;
|
||||
|
||||
namespace BotSharp.Plugin.Twilio.Services
|
||||
{
|
||||
public interface ITwilioSessionManager
|
||||
{
|
||||
Task SetAssistantReplyAsync(string conversationId, int seqNum, AssistantMessage message);
|
||||
Task<AssistantMessage> GetAssistantReplyAsync(string conversationId, int seqNum);
|
||||
Task StageCallerMessageAsync(string conversationId, int seqNum, string message);
|
||||
Task<List<string>> RetrieveStagedCallerMessagesAsync(string conversationId, int seqNum);
|
||||
Task SetReplyIndicationAsync(string conversationId, int seqNum, string indication);
|
||||
Task<string> GetReplyIndicationAsync(string conversationId, int seqNum);
|
||||
Task RemoveReplyIndicationAsync(string conversationId, int seqNum);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
using BotSharp.Abstraction.Files;
|
||||
using BotSharp.Abstraction.Routing;
|
||||
using BotSharp.Core.Infrastructures;
|
||||
using BotSharp.Plugin.Twilio.Interfaces;
|
||||
using BotSharp.Plugin.Twilio.Models;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using BotSharp.Abstraction.Utilities;
|
||||
using BotSharp.Plugin.Twilio.Models;
|
||||
using Twilio.Jwt.AccessToken;
|
||||
using Token = Twilio.Jwt.AccessToken.Token;
|
||||
|
||||
|
|
@ -66,7 +67,7 @@ public class TwilioService
|
|||
return response;
|
||||
}
|
||||
|
||||
public VoiceResponse ReturnInstructions(List<string> speechPaths, string callbackPath, bool actionOnEmptyResult, int timeout = 3, string hints = null)
|
||||
public VoiceResponse ReturnInstructions(ConversationalVoiceResponse conversationalVoiceResponse)
|
||||
{
|
||||
var response = new VoiceResponse();
|
||||
var gather = new Gather()
|
||||
|
|
@ -76,17 +77,17 @@ public class TwilioService
|
|||
Gather.InputEnum.Speech,
|
||||
Gather.InputEnum.Dtmf
|
||||
},
|
||||
Action = new Uri($"{_settings.CallbackHost}/{callbackPath}"),
|
||||
Action = new Uri($"{_settings.CallbackHost}/{conversationalVoiceResponse.CallbackPath}"),
|
||||
SpeechModel = Gather.SpeechModelEnum.PhoneCall,
|
||||
SpeechTimeout = "auto", // timeout > 0 ? timeout.ToString() : "3",
|
||||
Timeout = timeout > 0 ? timeout : 3,
|
||||
ActionOnEmptyResult = actionOnEmptyResult,
|
||||
Hints = hints
|
||||
Timeout = conversationalVoiceResponse.Timeout > 0 ? conversationalVoiceResponse.Timeout : 3,
|
||||
ActionOnEmptyResult = conversationalVoiceResponse.ActionOnEmptyResult,
|
||||
Hints = conversationalVoiceResponse.Hints
|
||||
};
|
||||
|
||||
if (!speechPaths.IsNullOrEmpty())
|
||||
if (!conversationalVoiceResponse.SpeechPaths.IsNullOrEmpty())
|
||||
{
|
||||
foreach (var speechPath in speechPaths)
|
||||
foreach (var speechPath in conversationalVoiceResponse.SpeechPaths)
|
||||
{
|
||||
gather.Play(new Uri($"{_settings.CallbackHost}/{speechPath}"));
|
||||
}
|
||||
|
|
@ -95,12 +96,12 @@ public class TwilioService
|
|||
return response;
|
||||
}
|
||||
|
||||
public VoiceResponse ReturnNoninterruptedInstructions(List<string> speechPaths, string callbackPath, bool actionOnEmptyResult, int timeout = 3)
|
||||
public VoiceResponse ReturnNoninterruptedInstructions(ConversationalVoiceResponse conversationalVoiceResponse)
|
||||
{
|
||||
var response = new VoiceResponse();
|
||||
if (speechPaths != null && speechPaths.Any())
|
||||
if (conversationalVoiceResponse.SpeechPaths != null && conversationalVoiceResponse.SpeechPaths.Any())
|
||||
{
|
||||
foreach (var speechPath in speechPaths)
|
||||
foreach (var speechPath in conversationalVoiceResponse.SpeechPaths)
|
||||
{
|
||||
response.Play(new Uri($"{_settings.CallbackHost}/{speechPath}"));
|
||||
}
|
||||
|
|
@ -112,11 +113,11 @@ public class TwilioService
|
|||
Gather.InputEnum.Speech,
|
||||
Gather.InputEnum.Dtmf
|
||||
},
|
||||
Action = new Uri($"{_settings.CallbackHost}/{callbackPath}"),
|
||||
Action = new Uri($"{_settings.CallbackHost}/{conversationalVoiceResponse.CallbackPath}"),
|
||||
SpeechModel = Gather.SpeechModelEnum.PhoneCall,
|
||||
SpeechTimeout = timeout > 0 ? timeout.ToString() : "3",
|
||||
Timeout = timeout > 0 ? timeout : 3,
|
||||
ActionOnEmptyResult = actionOnEmptyResult
|
||||
SpeechTimeout = conversationalVoiceResponse.Timeout > 0 ? conversationalVoiceResponse.Timeout.ToString() : "3",
|
||||
Timeout = conversationalVoiceResponse.Timeout > 0 ? conversationalVoiceResponse.Timeout : 3,
|
||||
ActionOnEmptyResult = conversationalVoiceResponse.ActionOnEmptyResult
|
||||
};
|
||||
response.Append(gather);
|
||||
return response;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using BotSharp.Plugin.Twilio.Interfaces;
|
||||
using BotSharp.Plugin.Twilio.Models;
|
||||
using StackExchange.Redis;
|
||||
using Task = System.Threading.Tasks.Task;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using BotSharp.Abstraction.Settings;
|
||||
using BotSharp.Plugin.Twilio.Interfaces;
|
||||
using BotSharp.Plugin.Twilio.Services;
|
||||
using StackExchange.Redis;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue