Merge branch 'master' into routing-utility
This commit is contained in:
commit
20563d28f5
|
|
@ -15,4 +15,5 @@ public class StateConst
|
|||
public const string LANGUAGE = "language";
|
||||
|
||||
public const string SUB_CONVERSATION_ID = "sub_conversation_id";
|
||||
public const string ORIGIN_CONVERSATION_ID = "origin_conversation_id";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ using System;
|
|||
using BotSharp.Abstraction.Realtime.Models;
|
||||
using BotSharp.Abstraction.MLTasks;
|
||||
using BotSharp.Abstraction.Agents.Models;
|
||||
using BotSharp.Abstraction.Conversations.Enums;
|
||||
|
||||
namespace BotSharp.Core.Realtime;
|
||||
|
||||
|
|
@ -118,7 +119,7 @@ public class RealtimeHub : IRealtimeHub
|
|||
foreach (var message in messages)
|
||||
{
|
||||
// Invoke function
|
||||
if (message.MessageType == "function_call")
|
||||
if (message.MessageType == MessageTypeName.FunctionCall)
|
||||
{
|
||||
await routing.InvokeFunction(message.FunctionName, message);
|
||||
message.Role = AgentRole.Function;
|
||||
|
|
|
|||
|
|
@ -500,16 +500,6 @@ public partial class FileRepository
|
|||
batchSize = batchLimit;
|
||||
}
|
||||
|
||||
if (bufferHours <= 0)
|
||||
{
|
||||
bufferHours = 12;
|
||||
}
|
||||
|
||||
if (messageLimit <= 0)
|
||||
{
|
||||
messageLimit = 2;
|
||||
}
|
||||
|
||||
foreach (var d in Directory.GetDirectories(dir))
|
||||
{
|
||||
var convFile = Path.Combine(d, CONVERSATION_FILE);
|
||||
|
|
|
|||
|
|
@ -466,16 +466,6 @@ public partial class MongoRepository
|
|||
batchSize = batchLimit;
|
||||
}
|
||||
|
||||
if (bufferHours <= 0)
|
||||
{
|
||||
bufferHours = 12;
|
||||
}
|
||||
|
||||
if (messageLimit <= 0)
|
||||
{
|
||||
messageLimit = 2;
|
||||
}
|
||||
|
||||
while (true)
|
||||
{
|
||||
var skip = (page - 1) * batchSize;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using BotSharp.Abstraction.Conversations.Enums;
|
||||
using BotSharp.Abstraction.Files.Utilities;
|
||||
using BotSharp.Abstraction.Functions.Models;
|
||||
using BotSharp.Abstraction.Realtime.Models;
|
||||
|
|
@ -204,12 +205,18 @@ public class RealTimeCompletionProvider : IRealTimeCompletion
|
|||
|
||||
public async Task SendEventToModel(object message)
|
||||
{
|
||||
if (_webSocket.State != WebSocketState.Open)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (message is not string data)
|
||||
{
|
||||
data = JsonSerializer.Serialize(message);
|
||||
}
|
||||
|
||||
var buffer = Encoding.UTF8.GetBytes(data);
|
||||
|
||||
await _webSocket.SendAsync(new ArraySegment<byte>(buffer), WebSocketMessageType.Text, true, CancellationToken.None);
|
||||
}
|
||||
|
||||
|
|
@ -559,7 +566,8 @@ public class RealTimeCompletionProvider : IRealTimeCompletion
|
|||
CurrentAgentId = conn.EntryAgentId,
|
||||
FunctionName = output.Name,
|
||||
FunctionArgs = output.Arguments,
|
||||
ToolCallId = output.CallId
|
||||
ToolCallId = output.CallId,
|
||||
MessageType = MessageTypeName.FunctionCall
|
||||
});
|
||||
}
|
||||
else if (output.Type == "message")
|
||||
|
|
|
|||
|
|
@ -9,15 +9,16 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\functions\util-twilio-twilio_outbound_phone_call.json" />
|
||||
<None Remove="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\templates\util-twilio-twilio_outbound_phone_call.fn.liquid" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\functions\util-twilio-twilio_outbound_phone_call.json">
|
||||
<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\templates\util-twilio-twilio_outbound_phone_call.fn.liquid">
|
||||
<Content Include="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\functions\util-twilio-outbound_phone_call.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\templates\util-twilio-hangup_phone_call.fn.liquid">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\templates\util-twilio-outbound_phone_call.fn.liquid">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,6 @@ namespace BotSharp.Plugin.Twilio.OutboundPhoneCallHandler.Enums
|
|||
{
|
||||
public class UtilityName
|
||||
{
|
||||
public const string OutboundPhoneCall = "twilio.twilio-outbound-phone-call";
|
||||
public const string OutboundPhoneCall = "phone.twilio-phone-call";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,106 +0,0 @@
|
|||
using BotSharp.Abstraction.Files;
|
||||
using BotSharp.Abstraction.Infrastructures.Enums;
|
||||
using BotSharp.Abstraction.Options;
|
||||
using BotSharp.Abstraction.Routing;
|
||||
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 => "util-twilio-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))
|
||||
{
|
||||
var error = $"Invalid phone number format: {args.PhoneNumber}";
|
||||
_logger.LogError(error);
|
||||
message.Content = error;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(args.InitialMessage))
|
||||
{
|
||||
_logger.LogError("Initial message is empty.");
|
||||
message.Content = "There is an error when generating phone message.";
|
||||
return false;
|
||||
}
|
||||
|
||||
var convService = _services.GetRequiredService<IConversationService>();
|
||||
var convStorage = _services.GetRequiredService<IConversationStorage>();
|
||||
var routing = _services.GetRequiredService<IRoutingContext>();
|
||||
var fileStorage = _services.GetRequiredService<IFileStorageService>();
|
||||
var sessionManager = _services.GetRequiredService<ITwilioSessionManager>();
|
||||
var states = _services.GetRequiredService<IConversationStateService>();
|
||||
|
||||
// Fork conversation
|
||||
var entryAgentId = routing.EntryAgentId;
|
||||
var newConv = await convService.NewConversation(new Abstraction.Conversations.Models.Conversation
|
||||
{
|
||||
AgentId = entryAgentId,
|
||||
Channel = ConversationChannel.Phone
|
||||
});
|
||||
var conversationId = newConv.Id;
|
||||
convStorage.Append(conversationId, new List<RoleDialogModel>
|
||||
{
|
||||
new RoleDialogModel(AgentRole.User, "Hi")
|
||||
{
|
||||
CurrentAgentId = entryAgentId
|
||||
},
|
||||
new RoleDialogModel(AgentRole.Assistant, args.InitialMessage)
|
||||
{
|
||||
CurrentAgentId = entryAgentId
|
||||
}
|
||||
});
|
||||
states.SetState(StateConst.SUB_CONVERSATION_ID, conversationId);
|
||||
|
||||
// Generate audio
|
||||
var completion = CompletionProvider.GetAudioCompletion(_services, "openai", "tts-1");
|
||||
var data = await completion.GenerateAudioFromTextAsync(args.InitialMessage);
|
||||
var fileName = $"intial.mp3";
|
||||
fileStorage.SaveSpeechFile(conversationId, fileName, data);
|
||||
|
||||
// Call phone number
|
||||
/*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}"),
|
||||
url: new Uri($"{_twilioSetting.CallbackHost}/twilio/stream?conversation_id={conversationId}&init_audio_file={fileName}"),
|
||||
to: new PhoneNumber(args.PhoneNumber),
|
||||
from: new PhoneNumber(_twilioSetting.PhoneNumber));
|
||||
|
||||
message.Content = $"The generated phone message: {args.InitialMessage}." ?? message.Content;
|
||||
message.StopCompletion = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
using BotSharp.Plugin.Twilio.OutboundPhoneCallHandler.LlmContexts;
|
||||
using Twilio.Rest.Api.V2010.Account;
|
||||
|
||||
namespace BotSharp.Plugin.Twilio.OutboundPhoneCallHandler.Functions;
|
||||
|
||||
public class HangupPhoneCallFn : IFunctionCallback
|
||||
{
|
||||
private readonly IServiceProvider _services;
|
||||
private readonly ILogger<HangupPhoneCallFn> _logger;
|
||||
|
||||
public string Name => "util-twilio-hangup_phone_call";
|
||||
public string Indication => "Hangup";
|
||||
|
||||
public HangupPhoneCallFn(
|
||||
IServiceProvider services,
|
||||
ILogger<HangupPhoneCallFn> logger)
|
||||
{
|
||||
_services = services;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<bool> Execute(RoleDialogModel message)
|
||||
{
|
||||
var args = JsonSerializer.Deserialize<LlmContextIn>(message.FunctionArgs);
|
||||
// Have to find the SID by the phone number
|
||||
var call = CallResource.Update(
|
||||
status: CallResource.UpdateStatusEnum.Completed,
|
||||
pathSid: args.CallSid
|
||||
);
|
||||
|
||||
message.Content = "The call has ended.";
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
using BotSharp.Abstraction.Files;
|
||||
using BotSharp.Abstraction.Infrastructures.Enums;
|
||||
using BotSharp.Abstraction.Options;
|
||||
using BotSharp.Abstraction.Routing;
|
||||
using BotSharp.Core.Infrastructures;
|
||||
using BotSharp.Plugin.Twilio.OutboundPhoneCallHandler.LlmContexts;
|
||||
using Twilio.Rest.Api.V2010.Account;
|
||||
using Twilio.Types;
|
||||
using Conversation = BotSharp.Abstraction.Conversations.Models.Conversation;
|
||||
using Task = System.Threading.Tasks.Task;
|
||||
|
||||
namespace BotSharp.Plugin.Twilio.OutboundPhoneCallHandler.Functions;
|
||||
|
||||
public class OutboundPhoneCallFn : IFunctionCallback
|
||||
{
|
||||
private readonly IServiceProvider _services;
|
||||
private readonly ILogger<OutboundPhoneCallFn> _logger;
|
||||
private readonly BotSharpOptions _options;
|
||||
private readonly TwilioSetting _twilioSetting;
|
||||
|
||||
public string Name => "util-twilio-outbound_phone_call";
|
||||
public string Indication => "Dialing the phone number";
|
||||
|
||||
public OutboundPhoneCallFn(
|
||||
IServiceProvider services,
|
||||
ILogger<OutboundPhoneCallFn> 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))
|
||||
{
|
||||
var error = $"Invalid phone number format: {args.PhoneNumber}";
|
||||
_logger.LogError(error);
|
||||
message.Content = error;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(args.InitialMessage))
|
||||
{
|
||||
_logger.LogError("Initial message is empty.");
|
||||
message.Content = "There is an error when generating phone message.";
|
||||
return false;
|
||||
}
|
||||
|
||||
var fileStorage = _services.GetRequiredService<IFileStorageService>();
|
||||
var states = _services.GetRequiredService<IConversationStateService>();
|
||||
|
||||
// Fork conversation
|
||||
var newConversationId = Guid.NewGuid().ToString();
|
||||
states.SetState(StateConst.SUB_CONVERSATION_ID, newConversationId);
|
||||
|
||||
// Generate initial assistant audio
|
||||
var completion = CompletionProvider.GetAudioCompletion(_services, "openai", "tts-1");
|
||||
var data = await completion.GenerateAudioFromTextAsync(args.InitialMessage);
|
||||
var fileName = $"intial.mp3";
|
||||
fileStorage.SaveSpeechFile(newConversationId, fileName, data);
|
||||
|
||||
// Make outbound call
|
||||
var call = await CallResource.CreateAsync(
|
||||
url: new Uri($"{_twilioSetting.CallbackHost}/twilio/stream?conversation_id={newConversationId}&init_audio_file={fileName}"),
|
||||
to: new PhoneNumber(args.PhoneNumber),
|
||||
from: new PhoneNumber(_twilioSetting.PhoneNumber));
|
||||
|
||||
var convService = _services.GetRequiredService<IConversationService>();
|
||||
var routing = _services.GetRequiredService<IRoutingContext>();
|
||||
var originConversationId = convService.ConversationId;
|
||||
var entryAgentId = routing.EntryAgentId;
|
||||
|
||||
await ForkConversation(args, entryAgentId, originConversationId, newConversationId, call);
|
||||
|
||||
message.Content = $"The generated phone message: \"{args.InitialMessage}.\" [NEW CONVERSATION ID: {newConversationId}, TWILIO CALL SID: {call.Sid}]";
|
||||
message.StopCompletion = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task ForkConversation(LlmContextIn args,
|
||||
string entryAgentId,
|
||||
string originConversationId,
|
||||
string newConversationId,
|
||||
CallResource resource)
|
||||
{
|
||||
// new scope service for isolated conversation
|
||||
using var scope = _services.CreateScope();
|
||||
var services = scope.ServiceProvider;
|
||||
var convService = services.GetRequiredService<IConversationService>();
|
||||
var convStorage = services.GetRequiredService<IConversationStorage>();
|
||||
|
||||
var newConv = await convService.NewConversation(new Conversation
|
||||
{
|
||||
Id = newConversationId,
|
||||
AgentId = entryAgentId,
|
||||
Channel = ConversationChannel.Phone,
|
||||
ChannelId = resource.Sid,
|
||||
Title = args.InitialMessage
|
||||
});
|
||||
|
||||
convStorage.Append(newConversationId, new List<RoleDialogModel>
|
||||
{
|
||||
new RoleDialogModel(AgentRole.User, $"[Calling Phone To: {resource.ToFormatted}, Call SID: {resource.Sid}.]")
|
||||
{
|
||||
CurrentAgentId = entryAgentId
|
||||
},
|
||||
new RoleDialogModel(AgentRole.Assistant, args.InitialMessage)
|
||||
{
|
||||
CurrentAgentId = entryAgentId
|
||||
}
|
||||
});
|
||||
|
||||
convService.SetConversationId(newConversationId,
|
||||
[
|
||||
new MessageState(StateConst.ORIGIN_CONVERSATION_ID, originConversationId),
|
||||
new MessageState("phone_number", resource.To)
|
||||
]);
|
||||
convService.SaveStates();
|
||||
}
|
||||
}
|
||||
|
|
@ -6,15 +6,24 @@ namespace BotSharp.Plugin.Twilio.OutboundPhoneCallHandler.Hooks;
|
|||
public class OutboundPhoneCallHandlerUtilityHook : IAgentUtilityHook
|
||||
{
|
||||
private static string PREFIX = "util-twilio-";
|
||||
private static string OUTBOUND_PHONE_CALL_FN = $"{PREFIX}twilio_outbound_phone_call";
|
||||
private static string OUTBOUND_PHONE_CALL_FN = $"{PREFIX}outbound_phone_call";
|
||||
private static string HANGUP_PHONE_CALL_FN = $"{PREFIX}hangup_phone_call";
|
||||
|
||||
public void AddUtilities(List<AgentUtility> utilities)
|
||||
{
|
||||
var utility = new AgentUtility
|
||||
{
|
||||
Name = UtilityName.OutboundPhoneCall,
|
||||
Functions = [new($"{OUTBOUND_PHONE_CALL_FN}")],
|
||||
Templates = [new($"{OUTBOUND_PHONE_CALL_FN}.fn")]
|
||||
Functions =
|
||||
[
|
||||
new($"{OUTBOUND_PHONE_CALL_FN}"),
|
||||
new($"{HANGUP_PHONE_CALL_FN}")
|
||||
],
|
||||
Templates =
|
||||
[
|
||||
new($"{OUTBOUND_PHONE_CALL_FN}.fn"),
|
||||
new($"{HANGUP_PHONE_CALL_FN}.fn")
|
||||
]
|
||||
};
|
||||
|
||||
utilities.Add(utility);
|
||||
|
|
|
|||
|
|
@ -9,4 +9,7 @@ public class LlmContextIn
|
|||
|
||||
[JsonPropertyName("initial_message")]
|
||||
public string InitialMessage { get; set; }
|
||||
|
||||
[JsonPropertyName("call_sid")]
|
||||
public string CallSid { get; set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"name": "util-twilio-hangup_phone_call",
|
||||
"description": "Call this function if the user wants to end the phone call",
|
||||
"visibility_expression": "{% if states.channel == 'phone' %}visible{% endif %}",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"call_sid": {
|
||||
"type": "string",
|
||||
"description": "The unique string SID that we created to identify this Call resource."
|
||||
}
|
||||
},
|
||||
"required": [ "call_sid" ]
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +1,16 @@
|
|||
{
|
||||
"name": "util-twilio-twilio_outbound_phone_call",
|
||||
"name": "util-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.",
|
||||
"visibility_expression": "{% if states.channel != 'phone' %}visible{% endif %}",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"phone_number": {
|
||||
"to_read": "string",
|
||||
"type": "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",
|
||||
"type": "string",
|
||||
"description": "The initial message which will be sent."
|
||||
}
|
||||
},
|
||||
|
|
@ -0,0 +1 @@
|
|||
** Please call util-twilio-hangup_phone_call if user wants to end the phone call.
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
** Please call util-twilio-outbound_phone_call if user wants to make an outbound call.
|
||||
** For outbound calls, phone number format is "XXX-XXX-XXXX".
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
** Please take a look at the conversation and decide whether user wants to make an outbound call.
|
||||
** Please call util-twilio-twilio_outbound_phone_call if user wants to make an outbound call.
|
||||
Loading…
Reference in a new issue