Merge branch 'master' into routing-function-refactor-1

This commit is contained in:
Haiping Chen 2025-05-05 09:12:14 -05:00
commit 3960381b7f
15 changed files with 172 additions and 31 deletions

View file

@ -126,10 +126,9 @@ BotSharp uses component design, the kernel is kept to a minimum, and business fu
- BotSharp.Plugin.ChatbotUI
### Roadmap
- [ ] A2A
- [x] MCP
- [ ] Realtime
- [x] Realtime
- [ ] Computer Use
- [x] Browser Use
- [x] Database Assistant

View file

@ -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; }
}

View file

@ -12,6 +12,7 @@ public class ConversationFilter
public string? AgentId { get; set; }
public string? Status { get; set; }
public string? Channel { get; set; }
public string? ChannelId { get; set; }
public string? UserId { get; set; }
public DateTime? StartTime { get; set; }

View file

@ -16,6 +16,9 @@
</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">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>

View file

@ -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;
}
}

View file

@ -7,7 +7,8 @@ public class CrontabUtilityHook : IAgentUtilityHook
{
public const string PREFIX = "util-crontab-";
private const string SCHEDULE_TASK_FN = $"{PREFIX}schedule_task";
private const string TASK_WAIT_FN = $"{PREFIX}task_wait";
public void AddUtilities(List<AgentUtility> utilities)
{
var items = new List<AgentUtility>
@ -15,7 +16,7 @@ public class CrontabUtilityHook : IAgentUtilityHook
new AgentUtility
{
Name = UtilityName.ScheduleTask,
Functions = [new(SCHEDULE_TASK_FN)],
Functions = [new(SCHEDULE_TASK_FN), new(TASK_WAIT_FN)],
Templates = [new($"{SCHEDULE_TASK_FN}.fn")]
}
};

View file

@ -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"
]
}
}

View file

@ -44,13 +44,17 @@ public partial class AgentService
[SharpCache(10)]
public async Task<Agent> GetAgent(string id)
{
var profile = _db.GetAgent(id);
if (string.IsNullOrWhiteSpace(id))
{
return null;
}
if (profile == null)
{
_logger.LogError($"Can't find agent {id}");
return null;
}
var profile = _db.GetAgent(id);
if (profile == null)
{
_logger.LogError($"Can't find agent {id}");
return null;
}
// Load llm config
var agentSetting = _services.GetRequiredService<AgentSettings>();

View file

@ -430,6 +430,10 @@ public partial class FileRepository
{
matched = matched && record.Channel == filter.Channel;
}
if(filter?.ChannelId != null)
{
matched = matched && record.ChannelId == filter.ChannelId;
}
if (filter?.UserId != null)
{
matched = matched && record.UserId == filter.UserId;

View file

@ -371,6 +371,10 @@ public partial class MongoRepository
{
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))
{
convFilters.Add(convBuilder.Eq(x => x.UserId, filter.UserId));

View file

@ -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);
}
}
_ = Task.Run(async () =>
{
await Task.Delay(1500);
await twilio.StartRecording(request.CallSid, request.AgentId, request.ConversationId);
});
}
await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>

View file

@ -38,6 +38,10 @@ public class TwilioRecordController : TwilioController
// recording completed
await HookEmitter.Emit<ITwilioCallStatusHook>(_services, x => x.OnRecordingCompleted(request));
}
else
{
_logger.LogError($"Unknown record status: {request.CallStatus}, {request.CallSid}");
}
return Ok();
}

View file

@ -214,7 +214,7 @@ public class TwilioVoiceController : TwilioController
var reply = await sessionManager.GetAssistantReplyAsync(request.ConversationId, request.SeqNum);
VoiceResponse response;
if (request.AIResponseWaitTime > 10)
{
// Wait AI Response Timeout
@ -346,35 +346,57 @@ public class TwilioVoiceController : TwilioController
if (twilio.MachineDetected(request))
{
// voicemail
await HookEmitter.Emit<ITwilioCallStatusHook>(_services,
async hook => await hook.OnVoicemailLeft(request));
await HookEmitter.Emit<ITwilioCallStatusHook>(_services,
async hook =>
{
if (hook.IsMatch(request)) await hook.OnVoicemailLeft(request);
});
}
else
{
// phone call completed
await HookEmitter.Emit<ITwilioCallStatusHook>(_services,
async x => await x.OnUserDisconnected(request));
await HookEmitter.Emit<ITwilioCallStatusHook>(_services,
async hook =>
{
if (hook.IsMatch(request)) await hook.OnUserDisconnected(request);
});
}
}
else if (request.CallStatus == "busy")
{
await HookEmitter.Emit<ITwilioCallStatusHook>(_services,
async x => await x.OnCallBusyStatus(request));
await HookEmitter.Emit<ITwilioCallStatusHook>(_services,
async hook =>
{
if (hook.IsMatch(request)) await hook.OnCallBusyStatus(request);
});
}
else if (request.CallStatus == "no-answer")
{
await HookEmitter.Emit<ITwilioCallStatusHook>(_services,
async x => await x.OnCallNoAnswerStatus(request));
await HookEmitter.Emit<ITwilioCallStatusHook>(_services,
async hook =>
{
if (hook.IsMatch(request)) await hook.OnCallNoAnswerStatus(request);
});
}
else if (request.CallStatus == "canceled")
{
await HookEmitter.Emit<ITwilioCallStatusHook>(_services,
async x => await x.OnCallCanceledStatus(request));
await HookEmitter.Emit<ITwilioCallStatusHook>(_services,
async hook =>
{
if (hook.IsMatch(request)) await hook.OnCallCanceledStatus(request);
});
}
else if (request.CallStatus == "failed")
{
await HookEmitter.Emit<ITwilioCallStatusHook>(_services,
async x => await x.OnCallFailedStatus(request));
await HookEmitter.Emit<ITwilioCallStatusHook>(_services,
async hook =>
{
if (hook.IsMatch(request)) await hook.OnCallFailedStatus(request);
});
}
else
{
_logger.LogError($"Unknown call status: {request.CallStatus}, {request.CallSid}");
}
return Ok();

View file

@ -5,10 +5,11 @@ namespace BotSharp.Plugin.Twilio.Interfaces;
public interface ITwilioCallStatusHook
{
Task OnVoicemailLeft(ConversationalVoiceRequest request);
Task OnUserDisconnected(ConversationalVoiceRequest request);
Task OnRecordingCompleted(ConversationalVoiceRequest request);
Task OnVoicemailStarting(ConversationalVoiceRequest request);
bool IsMatch(ConversationalVoiceRequest request) => true;
Task OnVoicemailLeft(ConversationalVoiceRequest request) => Task.CompletedTask;
Task OnUserDisconnected(ConversationalVoiceRequest request) => Task.CompletedTask;
Task OnRecordingCompleted(ConversationalVoiceRequest request) => Task.CompletedTask;
Task OnVoicemailStarting(ConversationalVoiceRequest request)=> Task.CompletedTask;
/// <summary>
/// 1. The recipient's phone line is already engaged.
@ -17,11 +18,11 @@ public interface ITwilioCallStatusHook
/// </summary>
/// <param name="request"></param>
/// <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;
}

View file

@ -6,6 +6,8 @@ using BotSharp.Core.Infrastructures;
using BotSharp.Plugin.Twilio.Interfaces;
using BotSharp.Plugin.Twilio.Models;
using Twilio.Jwt.AccessToken;
using Twilio.Rest.Api.V2010.Account.Call;
using Task = System.Threading.Tasks.Task;
using Token = Twilio.Jwt.AccessToken.Token;
namespace BotSharp.Plugin.Twilio.Services;
@ -122,6 +124,22 @@ public class TwilioService
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)
{
var response = new VoiceResponse();