Merge branch 'master' of https://github.com/SciSharp/BotSharp into test/realtime-chat
This commit is contained in:
commit
c1004df986
|
|
@ -126,10 +126,9 @@ BotSharp uses component design, the kernel is kept to a minimum, and business fu
|
||||||
- BotSharp.Plugin.ChatbotUI
|
- BotSharp.Plugin.ChatbotUI
|
||||||
|
|
||||||
### Roadmap
|
### Roadmap
|
||||||
|
|
||||||
- [ ] A2A
|
- [ ] A2A
|
||||||
- [x] MCP
|
- [x] MCP
|
||||||
- [ ] Realtime
|
- [x] Realtime
|
||||||
- [ ] Computer Use
|
- [ ] Computer Use
|
||||||
- [x] Browser Use
|
- [x] Browser Use
|
||||||
- [x] Database Assistant
|
- [x] Database Assistant
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace BotSharp.Abstraction.Crontab.Models;
|
||||||
|
|
||||||
|
public class TaskWaitArgs
|
||||||
|
{
|
||||||
|
|
||||||
|
[JsonPropertyName("delay_time")]
|
||||||
|
public int DelayTime { get; set; }
|
||||||
|
}
|
||||||
|
|
@ -12,6 +12,7 @@ public class ConversationFilter
|
||||||
public string? AgentId { get; set; }
|
public string? AgentId { get; set; }
|
||||||
public string? Status { get; set; }
|
public string? Status { get; set; }
|
||||||
public string? Channel { get; set; }
|
public string? Channel { get; set; }
|
||||||
|
public string? ChannelId { get; set; }
|
||||||
public string? UserId { get; set; }
|
public string? UserId { get; set; }
|
||||||
public DateTime? StartTime { get; set; }
|
public DateTime? StartTime { get; set; }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,9 @@
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<Content Include="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\functions\util-crontab-task_wait.json">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
<Content Include="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\functions\util-crontab-schedule_task.json">
|
<Content Include="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\functions\util-crontab-schedule_task.json">
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
</Content>
|
</Content>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
using BotSharp.Core.Crontab.Hooks;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace BotSharp.Core.Crontab.Functions;
|
||||||
|
|
||||||
|
public class TaskWaitFn : IFunctionCallback
|
||||||
|
{
|
||||||
|
public string Name => $"{CrontabUtilityHook.PREFIX}task_wait";
|
||||||
|
|
||||||
|
private readonly ILogger<TaskWaitFn> _logger;
|
||||||
|
public TaskWaitFn(ILogger<TaskWaitFn> logger)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
public async Task<bool> Execute(RoleDialogModel message)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var args = JsonSerializer.Deserialize<TaskWaitArgs>(message.FunctionArgs);
|
||||||
|
if (args != null && args.DelayTime > 0)
|
||||||
|
{
|
||||||
|
await Task.Delay(args.DelayTime * 1000);
|
||||||
|
}
|
||||||
|
message.Content = "wait task completed";
|
||||||
|
}
|
||||||
|
catch (JsonException jsonEx)
|
||||||
|
{
|
||||||
|
message.Content = "Invalid function arguments format.";
|
||||||
|
_logger.LogError(jsonEx, "Json deserialization failed.");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
message.Content = "Unable to perform delay task";
|
||||||
|
_logger.LogError(ex, "crontab wait task failed.");
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,6 +7,7 @@ public class CrontabUtilityHook : IAgentUtilityHook
|
||||||
{
|
{
|
||||||
public const string PREFIX = "util-crontab-";
|
public const string PREFIX = "util-crontab-";
|
||||||
private const string SCHEDULE_TASK_FN = $"{PREFIX}schedule_task";
|
private const string SCHEDULE_TASK_FN = $"{PREFIX}schedule_task";
|
||||||
|
private const string TASK_WAIT_FN = $"{PREFIX}task_wait";
|
||||||
|
|
||||||
public void AddUtilities(List<AgentUtility> utilities)
|
public void AddUtilities(List<AgentUtility> utilities)
|
||||||
{
|
{
|
||||||
|
|
@ -15,7 +16,7 @@ public class CrontabUtilityHook : IAgentUtilityHook
|
||||||
new AgentUtility
|
new AgentUtility
|
||||||
{
|
{
|
||||||
Name = UtilityName.ScheduleTask,
|
Name = UtilityName.ScheduleTask,
|
||||||
Functions = [new(SCHEDULE_TASK_FN)],
|
Functions = [new(SCHEDULE_TASK_FN), new(TASK_WAIT_FN)],
|
||||||
Templates = [new($"{SCHEDULE_TASK_FN}.fn")]
|
Templates = [new($"{SCHEDULE_TASK_FN}.fn")]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"name": "util-crontab-task_wait",
|
||||||
|
"description": "wait for a peroid of time then process",
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"delay_time": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "delay time in seconds"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"delay_time"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -44,8 +44,12 @@ public partial class AgentService
|
||||||
[SharpCache(10)]
|
[SharpCache(10)]
|
||||||
public async Task<Agent> GetAgent(string id)
|
public async Task<Agent> GetAgent(string id)
|
||||||
{
|
{
|
||||||
var profile = _db.GetAgent(id);
|
if (string.IsNullOrWhiteSpace(id))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var profile = _db.GetAgent(id);
|
||||||
if (profile == null)
|
if (profile == null)
|
||||||
{
|
{
|
||||||
_logger.LogError($"Can't find agent {id}");
|
_logger.LogError($"Can't find agent {id}");
|
||||||
|
|
|
||||||
|
|
@ -430,6 +430,10 @@ public partial class FileRepository
|
||||||
{
|
{
|
||||||
matched = matched && record.Channel == filter.Channel;
|
matched = matched && record.Channel == filter.Channel;
|
||||||
}
|
}
|
||||||
|
if(filter?.ChannelId != null)
|
||||||
|
{
|
||||||
|
matched = matched && record.ChannelId == filter.ChannelId;
|
||||||
|
}
|
||||||
if (filter?.UserId != null)
|
if (filter?.UserId != null)
|
||||||
{
|
{
|
||||||
matched = matched && record.UserId == filter.UserId;
|
matched = matched && record.UserId == filter.UserId;
|
||||||
|
|
|
||||||
|
|
@ -371,6 +371,10 @@ public partial class MongoRepository
|
||||||
{
|
{
|
||||||
convFilters.Add(convBuilder.Eq(x => x.Channel, filter.Channel));
|
convFilters.Add(convBuilder.Eq(x => x.Channel, filter.Channel));
|
||||||
}
|
}
|
||||||
|
if (!string.IsNullOrEmpty(filter?.ChannelId))
|
||||||
|
{
|
||||||
|
convFilters.Add(convBuilder.Eq(x => x.ChannelId, filter.ChannelId));
|
||||||
|
}
|
||||||
if (!string.IsNullOrEmpty(filter?.UserId))
|
if (!string.IsNullOrEmpty(filter?.UserId))
|
||||||
{
|
{
|
||||||
convFilters.Add(convBuilder.Eq(x => x.UserId, filter.UserId));
|
convFilters.Add(convBuilder.Eq(x => x.UserId, filter.UserId));
|
||||||
|
|
|
||||||
|
|
@ -107,6 +107,12 @@ public class TwilioInboundController : TwilioController
|
||||||
response.Redirect(new Uri($"{_settings.CallbackHost}/twilio/voice/reply/{seqNum}?agent-id={request.AgentId}&conversation-id={request.ConversationId}&{twilio.GenerateStatesParameter(request.States)}"), HttpMethod.Post);
|
response.Redirect(new Uri($"{_settings.CallbackHost}/twilio/voice/reply/{seqNum}?agent-id={request.AgentId}&conversation-id={request.ConversationId}&{twilio.GenerateStatesParameter(request.States)}"), HttpMethod.Post);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
await Task.Delay(1500);
|
||||||
|
await twilio.StartRecording(request.CallSid, request.AgentId, request.ConversationId);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
|
await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,10 @@ public class TwilioRecordController : TwilioController
|
||||||
// recording completed
|
// recording completed
|
||||||
await HookEmitter.Emit<ITwilioCallStatusHook>(_services, x => x.OnRecordingCompleted(request));
|
await HookEmitter.Emit<ITwilioCallStatusHook>(_services, x => x.OnRecordingCompleted(request));
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogError($"Unknown record status: {request.CallStatus}, {request.CallSid}");
|
||||||
|
}
|
||||||
|
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -347,34 +347,56 @@ public class TwilioVoiceController : TwilioController
|
||||||
{
|
{
|
||||||
// voicemail
|
// voicemail
|
||||||
await HookEmitter.Emit<ITwilioCallStatusHook>(_services,
|
await HookEmitter.Emit<ITwilioCallStatusHook>(_services,
|
||||||
async hook => await hook.OnVoicemailLeft(request));
|
async hook =>
|
||||||
|
{
|
||||||
|
if (hook.IsMatch(request)) await hook.OnVoicemailLeft(request);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// phone call completed
|
// phone call completed
|
||||||
await HookEmitter.Emit<ITwilioCallStatusHook>(_services,
|
await HookEmitter.Emit<ITwilioCallStatusHook>(_services,
|
||||||
async x => await x.OnUserDisconnected(request));
|
async hook =>
|
||||||
|
{
|
||||||
|
if (hook.IsMatch(request)) await hook.OnUserDisconnected(request);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (request.CallStatus == "busy")
|
else if (request.CallStatus == "busy")
|
||||||
{
|
{
|
||||||
await HookEmitter.Emit<ITwilioCallStatusHook>(_services,
|
await HookEmitter.Emit<ITwilioCallStatusHook>(_services,
|
||||||
async x => await x.OnCallBusyStatus(request));
|
async hook =>
|
||||||
|
{
|
||||||
|
if (hook.IsMatch(request)) await hook.OnCallBusyStatus(request);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
else if (request.CallStatus == "no-answer")
|
else if (request.CallStatus == "no-answer")
|
||||||
{
|
{
|
||||||
await HookEmitter.Emit<ITwilioCallStatusHook>(_services,
|
await HookEmitter.Emit<ITwilioCallStatusHook>(_services,
|
||||||
async x => await x.OnCallNoAnswerStatus(request));
|
async hook =>
|
||||||
|
{
|
||||||
|
if (hook.IsMatch(request)) await hook.OnCallNoAnswerStatus(request);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
else if (request.CallStatus == "canceled")
|
else if (request.CallStatus == "canceled")
|
||||||
{
|
{
|
||||||
await HookEmitter.Emit<ITwilioCallStatusHook>(_services,
|
await HookEmitter.Emit<ITwilioCallStatusHook>(_services,
|
||||||
async x => await x.OnCallCanceledStatus(request));
|
async hook =>
|
||||||
|
{
|
||||||
|
if (hook.IsMatch(request)) await hook.OnCallCanceledStatus(request);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
else if (request.CallStatus == "failed")
|
else if (request.CallStatus == "failed")
|
||||||
{
|
{
|
||||||
await HookEmitter.Emit<ITwilioCallStatusHook>(_services,
|
await HookEmitter.Emit<ITwilioCallStatusHook>(_services,
|
||||||
async x => await x.OnCallFailedStatus(request));
|
async hook =>
|
||||||
|
{
|
||||||
|
if (hook.IsMatch(request)) await hook.OnCallFailedStatus(request);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogError($"Unknown call status: {request.CallStatus}, {request.CallSid}");
|
||||||
}
|
}
|
||||||
|
|
||||||
return Ok();
|
return Ok();
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,11 @@ namespace BotSharp.Plugin.Twilio.Interfaces;
|
||||||
|
|
||||||
public interface ITwilioCallStatusHook
|
public interface ITwilioCallStatusHook
|
||||||
{
|
{
|
||||||
Task OnVoicemailLeft(ConversationalVoiceRequest request);
|
bool IsMatch(ConversationalVoiceRequest request) => true;
|
||||||
Task OnUserDisconnected(ConversationalVoiceRequest request);
|
Task OnVoicemailLeft(ConversationalVoiceRequest request) => Task.CompletedTask;
|
||||||
Task OnRecordingCompleted(ConversationalVoiceRequest request);
|
Task OnUserDisconnected(ConversationalVoiceRequest request) => Task.CompletedTask;
|
||||||
Task OnVoicemailStarting(ConversationalVoiceRequest request);
|
Task OnRecordingCompleted(ConversationalVoiceRequest request) => Task.CompletedTask;
|
||||||
|
Task OnVoicemailStarting(ConversationalVoiceRequest request)=> Task.CompletedTask;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 1. The recipient's phone line is already engaged.
|
/// 1. The recipient's phone line is already engaged.
|
||||||
|
|
@ -17,11 +18,11 @@ public interface ITwilioCallStatusHook
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="request"></param>
|
/// <param name="request"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
Task OnCallBusyStatus(ConversationalVoiceRequest request);
|
Task OnCallBusyStatus(ConversationalVoiceRequest request)=> Task.CompletedTask;
|
||||||
|
|
||||||
Task OnCallNoAnswerStatus(ConversationalVoiceRequest request);
|
Task OnCallNoAnswerStatus(ConversationalVoiceRequest request) => Task.CompletedTask;
|
||||||
|
|
||||||
Task OnCallCanceledStatus(ConversationalVoiceRequest request);
|
Task OnCallCanceledStatus(ConversationalVoiceRequest request)=> Task.CompletedTask;
|
||||||
|
|
||||||
Task OnCallFailedStatus(ConversationalVoiceRequest request);
|
Task OnCallFailedStatus(ConversationalVoiceRequest request)=> Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ using BotSharp.Core.Infrastructures;
|
||||||
using BotSharp.Plugin.Twilio.Interfaces;
|
using BotSharp.Plugin.Twilio.Interfaces;
|
||||||
using BotSharp.Plugin.Twilio.Models;
|
using BotSharp.Plugin.Twilio.Models;
|
||||||
using Twilio.Jwt.AccessToken;
|
using Twilio.Jwt.AccessToken;
|
||||||
|
using Twilio.Rest.Api.V2010.Account.Call;
|
||||||
|
using Task = System.Threading.Tasks.Task;
|
||||||
using Token = Twilio.Jwt.AccessToken.Token;
|
using Token = Twilio.Jwt.AccessToken.Token;
|
||||||
|
|
||||||
namespace BotSharp.Plugin.Twilio.Services;
|
namespace BotSharp.Plugin.Twilio.Services;
|
||||||
|
|
@ -122,6 +124,22 @@ public class TwilioService
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task StartRecording(string callSid, string agentId, string conversationId)
|
||||||
|
{
|
||||||
|
if (_settings.RecordingEnabled)
|
||||||
|
{
|
||||||
|
// https://help.twilio.com/articles/360010317333-Recording-Incoming-Twilio-Voice-Calls
|
||||||
|
var recordStatusUrl = $"{_settings.CallbackHost}/twilio/record/status?agent-id={agentId}&conversation-id={conversationId}";
|
||||||
|
var recording = await RecordingResource.CreateAsync(pathCallSid: callSid,
|
||||||
|
recordingStatusCallback: new Uri(recordStatusUrl),
|
||||||
|
trim: "trim-silence",
|
||||||
|
recordingChannels: "dual",
|
||||||
|
recordingTrack: "both");
|
||||||
|
|
||||||
|
_logger.LogInformation($"Recording started: {recording.CallSid} {recording.Sid}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public VoiceResponse HangUp(string speechPath)
|
public VoiceResponse HangUp(string speechPath)
|
||||||
{
|
{
|
||||||
var response = new VoiceResponse();
|
var response = new VoiceResponse();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue