diff --git a/Directory.Packages.props b/Directory.Packages.props index b115ab6b..b179a20d 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -111,10 +111,10 @@ - + - - + + diff --git a/src/Infrastructure/BotSharp.Abstraction/Functions/IFunctionCallback.cs b/src/Infrastructure/BotSharp.Abstraction/Functions/IFunctionCallback.cs index b842b5b6..2019a057 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Functions/IFunctionCallback.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Functions/IFunctionCallback.cs @@ -2,6 +2,7 @@ namespace BotSharp.Abstraction.Functions; public interface IFunctionCallback { + string Provider => "Botsharp"; string Name { get; } /// diff --git a/src/Infrastructure/BotSharp.Abstraction/MCP/Models/McpServerConfigModel.cs b/src/Infrastructure/BotSharp.Abstraction/MCP/Models/McpServerConfigModel.cs index 2e0967b9..0a80bb89 100644 --- a/src/Infrastructure/BotSharp.Abstraction/MCP/Models/McpServerConfigModel.cs +++ b/src/Infrastructure/BotSharp.Abstraction/MCP/Models/McpServerConfigModel.cs @@ -12,22 +12,21 @@ public class McpServerConfigModel /// public string Name { get; set; } = null!; - /// - /// The type of transport to use. - /// - [JsonPropertyName("transport_type")] - public string TransportType { get; set; } = null!; - - /// - /// For stdio transport: path to the executable - /// For HTTP transport: base URL of the server - /// - public string? Location { get; set; } - - /// - /// Additional transport-specific configuration. - /// - [JsonPropertyName("transport_options")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public Dictionary? TransportOptions { get; set; } + public McpSseServerConfig? SseConfig { get; set; } + public McpStdioServerConfig? StdioConfig { get; set; } } + +public class McpSseServerConfig +{ + public string EndPoint { get; set; } = null!; + public TimeSpan ConnectionTimeout { get; init; } = TimeSpan.FromSeconds(30); + public Dictionary? AdditionalHeaders { get; set; } +} + +public class McpStdioServerConfig +{ + public string Command { get; set; } = null!; + public IList? Arguments { get; set; } + public Dictionary? EnvironmentVariables { get; set; } + public TimeSpan ShutdownTimeout { get; set; } = TimeSpan.FromSeconds(5); +} \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Abstraction/MCP/Models/McpServerOptionModel.cs b/src/Infrastructure/BotSharp.Abstraction/MCP/Models/McpServerOptionModel.cs new file mode 100644 index 00000000..964203c9 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/MCP/Models/McpServerOptionModel.cs @@ -0,0 +1,19 @@ +namespace BotSharp.Abstraction.MCP.Models; + +public class McpServerOptionModel : IdName +{ + public IEnumerable Tools { get; set; } = []; + + public McpServerOptionModel() : base() + { + + } + + public McpServerOptionModel( + string id, + string name, + IEnumerable tools) : base(id, name) + { + Tools = tools ?? []; + } +} diff --git a/src/Infrastructure/BotSharp.Abstraction/MCP/Services/IMcpService.cs b/src/Infrastructure/BotSharp.Abstraction/MCP/Services/IMcpService.cs index 7f054699..71952dac 100644 --- a/src/Infrastructure/BotSharp.Abstraction/MCP/Services/IMcpService.cs +++ b/src/Infrastructure/BotSharp.Abstraction/MCP/Services/IMcpService.cs @@ -2,5 +2,5 @@ namespace BotSharp.Abstraction.MCP.Services; public interface IMcpService { - IEnumerable GetServerConfigs() => []; + IEnumerable GetServerConfigs() => []; } diff --git a/src/Infrastructure/BotSharp.Abstraction/Models/IdName.cs b/src/Infrastructure/BotSharp.Abstraction/Models/IdName.cs index e5f7d8b5..107c7dd8 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Models/IdName.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Models/IdName.cs @@ -8,6 +8,11 @@ public class IdName [JsonPropertyName("name")] public string Name { get; set; } = default!; + public IdName() + { + + } + public IdName(string id, string name) { Id = id; diff --git a/src/Infrastructure/BotSharp.Abstraction/Models/MessageState.cs b/src/Infrastructure/BotSharp.Abstraction/Models/MessageState.cs index 15fd6a37..ff26e981 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Models/MessageState.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Models/MessageState.cs @@ -3,7 +3,7 @@ namespace BotSharp.Abstraction.Models; public class MessageState { public string Key { get; set; } - public string Value { get; set; } + public object Value { get; set; } [JsonPropertyName("active_rounds")] public int ActiveRounds { get; set; } = -1; @@ -13,7 +13,7 @@ public class MessageState } - public MessageState(string key, string value, int activeRounds = -1) + public MessageState(string key, object value, int activeRounds = -1) { Key = key; Value = value; diff --git a/src/Infrastructure/BotSharp.Abstraction/Realtime/Models/RealtimeHubConnection.cs b/src/Infrastructure/BotSharp.Abstraction/Realtime/Models/RealtimeHubConnection.cs index b3132df7..0f2af400 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Realtime/Models/RealtimeHubConnection.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Realtime/Models/RealtimeHubConnection.cs @@ -13,6 +13,7 @@ public class RealtimeHubConnection public Func OnModelMessageReceived { get; set; } = null!; public Func OnModelAudioResponseDone { get; set; } = null!; public Func OnModelUserInterrupted { get; set; } = null!; + public Func OnUserSpeechDetected { get; set; } = () => string.Empty; public void ResetResponseState() { diff --git a/src/Infrastructure/BotSharp.Abstraction/Utilities/StringExtensions.cs b/src/Infrastructure/BotSharp.Abstraction/Utilities/StringExtensions.cs index af222d0d..94bda0cd 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Utilities/StringExtensions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Utilities/StringExtensions.cs @@ -108,4 +108,27 @@ public static class StringExtensions uint.TryParse(value, out _) || ulong.TryParse(value, out _); } + + + public static string ConvertToString(this T? value, JsonSerializerOptions? jsonOptions = null) + { + if (value == null) + { + return string.Empty; + } + + if (value is string s) + { + return s; + } + + if (value is JsonElement elem + && elem.ValueKind == JsonValueKind.String) + { + return elem.ToString(); + } + + var str = JsonSerializer.Serialize(value, jsonOptions); + return str; + } } diff --git a/src/Infrastructure/BotSharp.Core.Crontab/Services/CrontabEventSubscription.cs b/src/Infrastructure/BotSharp.Core.Crontab/Services/CrontabEventSubscription.cs index 17b599b6..160bf4fc 100644 --- a/src/Infrastructure/BotSharp.Core.Crontab/Services/CrontabEventSubscription.cs +++ b/src/Infrastructure/BotSharp.Core.Crontab/Services/CrontabEventSubscription.cs @@ -40,15 +40,17 @@ public class CrontabEventSubscription : BackgroundService "Crontab", port: 0, priorityEnabled: false, - async (sender, args) => - { - var scope = _services.CreateScope(); - cron = scope.ServiceProvider.GetRequiredService(); - await cron.ScheduledTimeArrived(item); - }, + async (sender, args) => await HandleCrontabEvent(item), stoppingToken: stoppingToken); }); } } } + + private async Task HandleCrontabEvent(CrontabItem item) + { + using var scope = _services.CreateScope(); + var cron = scope.ServiceProvider.GetRequiredService(); + await cron.ScheduledTimeArrived(item); + } } diff --git a/src/Infrastructure/BotSharp.Core.Crontab/Services/CrontabWatcher.cs b/src/Infrastructure/BotSharp.Core.Crontab/Services/CrontabWatcher.cs index 8fff5220..1546cb7c 100644 --- a/src/Infrastructure/BotSharp.Core.Crontab/Services/CrontabWatcher.cs +++ b/src/Infrastructure/BotSharp.Core.Crontab/Services/CrontabWatcher.cs @@ -93,9 +93,7 @@ public class CrontabWatcher : BackgroundService } else { - var scope = _services.CreateScope(); - cron = scope.ServiceProvider.GetRequiredService(); - cron.ScheduledTimeArrived(item); + await HandleCrontabEvent(item); } } } @@ -107,6 +105,13 @@ public class CrontabWatcher : BackgroundService } } + private async Task HandleCrontabEvent(CrontabItem item) + { + using var scope = _services.CreateScope(); + var cron = scope.ServiceProvider.GetRequiredService(); + await cron.ScheduledTimeArrived(item); + } + private DateTime GetLastOccurrence(CrontabSchedule schedule) { var nextOccurrence = schedule.GetNextOccurrence(DateTime.UtcNow); diff --git a/src/Infrastructure/BotSharp.Core.MCP/BotSharpMCPExtensions.cs b/src/Infrastructure/BotSharp.Core.MCP/BotSharpMCPExtensions.cs index ee065459..8760935d 100644 --- a/src/Infrastructure/BotSharp.Core.MCP/BotSharpMCPExtensions.cs +++ b/src/Infrastructure/BotSharp.Core.MCP/BotSharpMCPExtensions.cs @@ -4,7 +4,6 @@ using BotSharp.Core.MCP.Managers; using BotSharp.Core.MCP.Services; using BotSharp.Core.MCP.Settings; using Microsoft.Extensions.Configuration; -using ModelContextProtocol; using ModelContextProtocol.Client; namespace BotSharp.Core.MCP; @@ -21,7 +20,7 @@ public static class BotSharpMcpExtensions { services.AddScoped(); var settings = config.GetSection("MCP").Get(); - services.AddScoped(provider => { return settings; }); + services.AddScoped(provider => settings); if (settings != null && settings.Enabled && !settings.McpServerConfigs.IsNullOrEmpty()) { @@ -42,20 +41,24 @@ public static class BotSharpMcpExtensions return services; } - private static async Task RegisterFunctionCall(IServiceCollection services, McpServerConfig server, McpClientManager clientManager) + private static async Task RegisterFunctionCall(IServiceCollection services, McpServerConfigModel server, McpClientManager clientManager) { - var client = await clientManager.GetMcpClientAsync(server.Id); - var tools = await client.ListToolsAsync(); - - foreach (var tool in tools) + try { - services.AddScoped(provider => { return tool; }); + var client = await clientManager.GetMcpClientAsync(server.Id); + var tools = await client.ListToolsAsync(); - services.AddScoped(provider => + foreach (var tool in tools) { - var funcTool = new McpToolAdapter(provider, tool, clientManager); - return funcTool; - }); + services.AddScoped(provider => tool); + + services.AddScoped(provider => + { + var funcTool = new McpToolAdapter(provider, server.Name, tool, clientManager); + return funcTool; + }); + } } + catch { } } } \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Core.MCP/Functions/McpToolAdapter.cs b/src/Infrastructure/BotSharp.Core.MCP/Functions/McpToolAdapter.cs index 2d4fdc70..0e433597 100644 --- a/src/Infrastructure/BotSharp.Core.MCP/Functions/McpToolAdapter.cs +++ b/src/Infrastructure/BotSharp.Core.MCP/Functions/McpToolAdapter.cs @@ -6,45 +6,62 @@ namespace BotSharp.Core.MCP.Functions; public class McpToolAdapter : IFunctionCallback { + private readonly string _provider; private readonly McpClientTool _tool; private readonly McpClientManager _clientManager; private readonly IServiceProvider _services; - public McpToolAdapter(IServiceProvider services, McpClientTool tool, McpClientManager client) + public McpToolAdapter( + IServiceProvider services, + string serverName, + McpClientTool tool, + McpClientManager client) { _services = services ?? throw new ArgumentNullException(nameof(services)); _tool = tool ?? throw new ArgumentNullException(nameof(tool)); _clientManager = client ?? throw new ArgumentNullException(nameof(client)); + _provider = serverName; } + public string Provider => _provider; public string Name => _tool.Name; public async Task Execute(RoleDialogModel message) { - // Convert arguments to dictionary format expected by mcpdotnet - Dictionary argDict = JsonToDictionary(message.FunctionArgs); - var currentAgentId = message.CurrentAgentId; - var agentService = _services.GetRequiredService(); - var agent = await agentService.LoadAgent(currentAgentId); - var serverId = agent.McpTools.Where(t => t.Functions.Any(f => f.Name == Name)).FirstOrDefault().ServerId; + try + { + // Convert arguments to dictionary format expected by mcpdotnet + Dictionary argDict = JsonToDictionary(message.FunctionArgs); + var currentAgentId = message.CurrentAgentId; + var agentService = _services.GetRequiredService(); + var agent = await agentService.LoadAgent(currentAgentId); + var serverId = agent.McpTools.Where(t => t.Functions.Any(f => f.Name == Name)).FirstOrDefault().ServerId; - var client = await _clientManager.GetMcpClientAsync(serverId); + var client = await _clientManager.GetMcpClientAsync(serverId); - // Call the tool through mcpdotnet - var result = await client.CallToolAsync(_tool.Name, argDict.IsNullOrEmpty() ? new() : argDict); + // Call the tool through mcpdotnet + var result = await client.CallToolAsync(_tool.Name, !argDict.IsNullOrEmpty() ? argDict : []); - // Extract the text content from the result - var json = string.Join("\n", result.Content.Where(c => c.Type == "text").Select(c => c.Text)); + // Extract the text content from the result + var json = string.Join("\n", result.Content.Where(c => c.Type == "text").Select(c => c.Text)); - message.Content = json; - message.Data = json.JsonContent(); - return true; + message.Content = json; + message.Data = json.JsonContent(); + return true; + } + catch (Exception ex) + { + message.Content = $"Error when calling tool {Name} of MCP server {Provider}. {ex.Message}"; + return false; + } } private static Dictionary JsonToDictionary(string? json) { if (string.IsNullOrEmpty(json)) + { return []; + } using JsonDocument doc = JsonDocument.Parse(json); JsonElement root = doc.RootElement; diff --git a/src/Infrastructure/BotSharp.Core.MCP/Managers/McpClientManager.cs b/src/Infrastructure/BotSharp.Core.MCP/Managers/McpClientManager.cs index c11163b3..a5441e66 100644 --- a/src/Infrastructure/BotSharp.Core.MCP/Managers/McpClientManager.cs +++ b/src/Infrastructure/BotSharp.Core.MCP/Managers/McpClientManager.cs @@ -1,5 +1,6 @@ using BotSharp.Core.MCP.Settings; using ModelContextProtocol.Client; +using ModelContextProtocol.Protocol.Transport; namespace BotSharp.Core.MCP.Managers; @@ -14,9 +15,33 @@ public class McpClientManager : IDisposable public async Task GetMcpClientAsync(string serverId) { - return await McpClientFactory.CreateAsync( - _mcpSettings.McpServerConfigs.Where(x=> x.Name == serverId).First(), - _mcpSettings.McpClientOptions); + var config = _mcpSettings.McpServerConfigs.Where(x => x.Id == serverId).FirstOrDefault(); + + IClientTransport transport; + if (config.SseConfig != null) + { + transport = new SseClientTransport(new SseClientTransportOptions + { + Name = config.Name, + Endpoint = new Uri(config.SseConfig.EndPoint) + }); + } + else if (config.StdioConfig != null) + { + transport = new StdioClientTransport(new StdioClientTransportOptions + { + Name = config.Name, + Command = config.StdioConfig.Command, + Arguments = config.StdioConfig.Arguments, + EnvironmentVariables = config.StdioConfig.EnvironmentVariables + }); + } + else + { + throw new ArgumentNullException("Invalid MCP server configuration!"); + } + + return await McpClientFactory.CreateAsync(transport, _mcpSettings.McpClientOptions); } public void Dispose() diff --git a/src/Infrastructure/BotSharp.Core.MCP/Services/McpService.cs b/src/Infrastructure/BotSharp.Core.MCP/Services/McpService.cs index 786f0857..bbc2c3f4 100644 --- a/src/Infrastructure/BotSharp.Core.MCP/Services/McpService.cs +++ b/src/Infrastructure/BotSharp.Core.MCP/Services/McpService.cs @@ -16,17 +16,26 @@ public class McpService : IMcpService _logger = logger; } - public IEnumerable GetServerConfigs() + public IEnumerable GetServerConfigs() { + var options = new List(); var settings = _services.GetRequiredService(); var configs = settings?.McpServerConfigs ?? []; - return configs.Select(x => new McpServerConfigModel + + foreach (var config in configs) { - Id = x.Id, - Name = x.Name, - TransportType = x.TransportType, - TransportOptions = x.TransportOptions, - Location = x.Location - }); + var tools = _services.GetServices() + .Where(x => x.Provider == config.Name) + .Select(x => x.Name); + + options.Add(new McpServerOptionModel + { + Id = config.Id, + Name = config.Name, + Tools = tools + }); + } + + return options; } } diff --git a/src/Infrastructure/BotSharp.Core.MCP/Settings/MCPSettings.cs b/src/Infrastructure/BotSharp.Core.MCP/Settings/MCPSettings.cs index cd4bfd0f..2867712f 100644 --- a/src/Infrastructure/BotSharp.Core.MCP/Settings/MCPSettings.cs +++ b/src/Infrastructure/BotSharp.Core.MCP/Settings/MCPSettings.cs @@ -1,5 +1,4 @@ using ModelContextProtocol.Client; -using ModelContextProtocol; namespace BotSharp.Core.MCP.Settings; @@ -7,6 +6,6 @@ public class McpSettings { public bool Enabled { get; set; } = true; public McpClientOptions McpClientOptions { get; set; } - public List McpServerConfigs { get; set; } = new(); + public List McpServerConfigs { get; set; } = []; } diff --git a/src/Infrastructure/BotSharp.Core.Realtime/Services/RealtimeHub.cs b/src/Infrastructure/BotSharp.Core.Realtime/Services/RealtimeHub.cs index 4eae3a64..0c36eee3 100644 --- a/src/Infrastructure/BotSharp.Core.Realtime/Services/RealtimeHub.cs +++ b/src/Infrastructure/BotSharp.Core.Realtime/Services/RealtimeHub.cs @@ -150,6 +150,9 @@ public class RealtimeHub : IRealtimeHub var data = _conn.OnModelUserInterrupted(); await (responseToUser?.Invoke(data) ?? Task.CompletedTask); } + + var res = _conn.OnUserSpeechDetected(); + await (responseToUser?.Invoke(res) ?? Task.CompletedTask); }); } diff --git a/src/Infrastructure/BotSharp.Core.Realtime/Services/WaveStreamChannel.cs b/src/Infrastructure/BotSharp.Core.Realtime/Services/WaveStreamChannel.cs index b1000453..34f82379 100644 --- a/src/Infrastructure/BotSharp.Core.Realtime/Services/WaveStreamChannel.cs +++ b/src/Infrastructure/BotSharp.Core.Realtime/Services/WaveStreamChannel.cs @@ -38,10 +38,13 @@ public class WaveStreamChannel : IStreamChannel // Initialize audio output for streaming var waveFormat = new WaveFormat(24000, 16, 1); // 24000 Hz, 16-bit PCM, Mono _bufferedWaveProvider = new BufferedWaveProvider(waveFormat); - _bufferedWaveProvider.BufferLength = 1024 * 1024; // Buffer length + _bufferedWaveProvider.BufferDuration = TimeSpan.FromMinutes(10); _bufferedWaveProvider.DiscardOnBufferOverflow = true; - - _waveOut = new WaveOutEvent(); + + _waveOut = new WaveOutEvent() + { + DeviceNumber = 0 + }; _waveOut.Init(_bufferedWaveProvider); _waveOut.Play(); } diff --git a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.LoadAgent.cs b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.LoadAgent.cs index d3ceea8b..12c2637e 100644 --- a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.LoadAgent.cs +++ b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.LoadAgent.cs @@ -7,7 +7,7 @@ public partial class AgentService { public static ConcurrentDictionary> AgentParameterTypes = new(); - [SharpCache(10, perInstanceCache: true)] + // [SharpCache(10, perInstanceCache: true)] public async Task LoadAgent(string id, bool loadUtility = true) { if (string.IsNullOrEmpty(id) || id == Guid.Empty.ToString()) @@ -67,7 +67,7 @@ public partial class AgentService hook.OnSamplesLoaded(agent.Samples); } - if (loadUtility) + if (loadUtility && !agent.Utilities.IsNullOrEmpty()) { hook.OnAgentUtilityLoaded(agent); } diff --git a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.cs index 20e6a7fd..dc98d440 100644 --- a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.cs +++ b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.cs @@ -201,7 +201,7 @@ public partial class ConversationService : IConversationService }; converation = await NewConversation(sess); } - + return converation; } diff --git a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStateService.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStateService.cs index 37c8c760..9e101b74 100644 --- a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStateService.cs +++ b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStateService.cs @@ -15,6 +15,7 @@ ******************************************************************************/ using BotSharp.Abstraction.Conversations.Enums; +using BotSharp.Abstraction.Options; using BotSharp.Abstraction.SideCar; namespace BotSharp.Core.Conversations.Services; @@ -69,9 +70,11 @@ public class ConversationStateService : IConversationStateService return this; } + var options = _services.GetRequiredService(); + var defaultRound = -1; var preValue = string.Empty; - var currentValue = value.ToString(); + var currentValue = value.ConvertToString(options.JsonSerializerOptions); var curActive = true; StateKeyValue? pair = null; StateValue? prevLeafNode = null; diff --git a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStorage.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStorage.cs index 75d1a501..49852eb2 100644 --- a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStorage.cs +++ b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStorage.cs @@ -19,75 +19,7 @@ public class ConversationStorage : IConversationStorage public void Append(string conversationId, RoleDialogModel dialog) { - var agentId = dialog.CurrentAgentId; - var db = _services.GetRequiredService(); - var dialogElements = new List(); - - // Prevent duplicate record to be inserted - /*var dialogs = db.GetConversationDialogs(conversationId); - if (dialogs.Any(x => x.MetaData.MessageId == dialog.MessageId && x.Content == dialog.Content)) - { - return; - }*/ - - if (dialog.Role == AgentRole.Function) - { - var meta = new DialogMetaData - { - Role = dialog.Role, - AgentId = agentId, - MessageId = dialog.MessageId, - MessageType = dialog.MessageType, - FunctionName = dialog.FunctionName, - CreatedTime = dialog.CreatedAt - }; - - var content = dialog.Content.RemoveNewLine(); - if (string.IsNullOrEmpty(content)) - { - return; - } - dialogElements.Add(new DialogElement - { - MetaData = meta, - Content = dialog.Content, - SecondaryContent = dialog.SecondaryContent, - Payload = dialog.Payload - }); - } - else - { - var meta = new DialogMetaData - { - Role = dialog.Role, - AgentId = agentId, - MessageId = dialog.MessageId, - MessageType = dialog.MessageType, - SenderId = dialog.SenderId, - FunctionName = dialog.FunctionName, - CreatedTime = dialog.CreatedAt - }; - - var content = dialog.Content.RemoveNewLine(); - if (string.IsNullOrEmpty(content)) - { - return; - } - - var richContent = dialog.RichContent != null ? JsonSerializer.Serialize(dialog.RichContent, _options.JsonSerializerOptions) : null; - var secondaryRichContent = dialog.SecondaryRichContent != null ? JsonSerializer.Serialize(dialog.SecondaryRichContent, _options.JsonSerializerOptions) : null; - dialogElements.Add(new DialogElement - { - MetaData = meta, - Content = dialog.Content, - SecondaryContent = dialog.SecondaryContent, - RichContent = richContent, - SecondaryRichContent = secondaryRichContent, - Payload = dialog.Payload - }); - } - - db.AppendConversationDialogs(conversationId, dialogElements); + Append(conversationId, [dialog]); } public void Append(string conversationId, IEnumerable dialogs) diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs index b0a0fa6d..55e3009f 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs @@ -33,7 +33,7 @@ public class ConversationController : ControllerBase var conv = new Conversation { AgentId = agentId, - Channel = channel == default ? ConversationChannel.OpenAPI : channel.Value, + Channel = channel == default ? ConversationChannel.OpenAPI : channel.Value.ToString(), Tags = config.Tags ?? new(), TaskId = config.TaskId }; diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/McpController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/McpController.cs index 0524f0fb..7b74a37e 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/McpController.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/McpController.cs @@ -13,7 +13,7 @@ public class McpController : ControllerBase } [HttpGet("/mcp/server-configs")] - public IEnumerable GetMcpServerConfigs() + public IEnumerable GetMcpServerConfigs() { var mcp = _services.GetRequiredService(); return mcp.GetServerConfigs(); diff --git a/src/Plugins/BotSharp.Plugin.GoogleAI/Providers/Realtime/RealTimeCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.GoogleAI/Providers/Realtime/RealTimeCompletionProvider.cs index f9ef590a..cdebc5f6 100644 --- a/src/Plugins/BotSharp.Plugin.GoogleAI/Providers/Realtime/RealTimeCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.GoogleAI/Providers/Realtime/RealTimeCompletionProvider.cs @@ -119,17 +119,18 @@ public class GoogleRealTimeProvider : IRealTimeCompletion { client.Connected += (sender, e) => { - _logger.LogInformation("Google Realtime Client connected"); + _logger.LogInformation("Google Realtime Client connected."); onModelReady(); }; client.Disconnected += (sender, e) => { - _logger.LogInformation("Google Realtime Client disconnected"); + _logger.LogInformation("Google Realtime Client disconnected."); }; client.MessageReceived += async (sender, e) => { + _logger.LogInformation("User message received."); if (e.Payload.SetupComplete != null) { onConversationItemCreated(_client.ConnectionId.ToString()); @@ -156,12 +157,14 @@ public class GoogleRealTimeProvider : IRealTimeCompletion }; client.GenerationInterrupted += (sender, e) => - { + { + _logger.LogInformation("Audio generation interrupted."); onUserInterrupted(); }; client.AudioReceiveCompleted += (sender, e) => - { + { + _logger.LogInformation("Audio receive completed."); onModelAudioResponseDone(); }; diff --git a/src/Plugins/BotSharp.Plugin.OpenAI/Models/Realtime/SessionConversationUpdate.cs b/src/Plugins/BotSharp.Plugin.OpenAI/Models/Realtime/SessionConversationUpdate.cs new file mode 100644 index 00000000..e2b12f57 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.OpenAI/Models/Realtime/SessionConversationUpdate.cs @@ -0,0 +1,11 @@ +namespace BotSharp.Plugin.OpenAI.Models.Realtime; + +public class SessionConversationUpdate +{ + public string RawResponse { get; set; } + + public SessionConversationUpdate() + { + + } +} diff --git a/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Realtime/RealTimeCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Realtime/RealTimeCompletionProvider.cs index f4f3120f..959ed457 100644 --- a/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Realtime/RealTimeCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Realtime/RealTimeCompletionProvider.cs @@ -1,6 +1,6 @@ using BotSharp.Plugin.OpenAI.Models.Realtime; +using BotSharp.Plugin.OpenAI.Providers.Realtime.Session; using OpenAI.Chat; -using System.Net.WebSockets; namespace BotSharp.Plugin.OpenAI.Providers.Realtime; @@ -18,7 +18,7 @@ public class RealTimeCompletionProvider : IRealTimeCompletion private readonly BotSharpOptions _options; protected string _model = "gpt-4o-mini-realtime-preview"; - private ClientWebSocket _webSocket; + private RealtimeChatSession _session; public RealTimeCompletionProvider( OpenAiSettings settings, @@ -45,20 +45,11 @@ public class RealTimeCompletionProvider : IRealTimeCompletion var realtimeModelSettings = _services.GetRequiredService(); _model = realtimeModelSettings.Model; - var settingsService = _services.GetRequiredService(); - var settings = settingsService.GetSetting(Provider, _model); + _session?.Dispose(); + _session = new RealtimeChatSession(_services, _options); + await _session.ConnectAsync(Provider, _model, CancellationToken.None); - _webSocket?.Dispose(); - _webSocket = new ClientWebSocket(); - _webSocket.Options.SetRequestHeader("Authorization", $"Bearer {settings.ApiKey}"); - _webSocket.Options.SetRequestHeader("OpenAI-Beta", "realtime=v1"); - - await _webSocket.ConnectAsync(new Uri($"wss://api.openai.com/v1/realtime?model={_model}"), CancellationToken.None); - - if (_webSocket.State == WebSocketState.Open) - { - // Receive a message - _ = ReceiveMessage(conn, + _ = ReceiveMessage(conn, onModelReady, onModelAudioDeltaReceived, onModelAudioResponseDone, @@ -67,15 +58,11 @@ public class RealTimeCompletionProvider : IRealTimeCompletion onConversationItemCreated, onInputAudioTranscriptionCompleted, onInterruptionDetected); - } } public async Task Disconnect() { - if (_webSocket.State == WebSocketState.Open) - { - await _webSocket.CloseAsync(WebSocketCloseStatus.Empty, null, CancellationToken.None); - } + _session?.Disconnect(); } public async Task AppenAudioBuffer(string message) @@ -137,7 +124,7 @@ public class RealTimeCompletionProvider : IRealTimeCompletion private async Task ReceiveMessage(RealtimeHubConnection conn, Action onModelReady, - Action onModelAudioDeltaReceived, + Action onModelAudioDeltaReceived, Action onModelAudioResponseDone, Action onModelAudioTranscriptDone, Action> onModelResponseDone, @@ -145,33 +132,9 @@ public class RealTimeCompletionProvider : IRealTimeCompletion Action onUserAudioTranscriptionCompleted, Action onInterruptionDetected) { - var buffer = new byte[1024 * 1024 * 32]; - // Model response timeout - var settings = _services.GetRequiredService(); - var timeout = settings.ModelResponseTimeout; - WebSocketReceiveResult? result = default; - - do + await foreach (SessionConversationUpdate update in _session.ReceiveUpdatesAsync(CancellationToken.None)) { - Array.Clear(buffer, 0, buffer.Length); - - var taskWorker = _webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); - var taskTimer = Task.Delay(1000 * timeout); - var completedTask = await Task.WhenAny(taskWorker, taskTimer); - - if (completedTask == taskWorker) - { - result = taskWorker.Result; - } - else - { - _logger.LogWarning($"Timeout {timeout} seconds waiting for Model response."); - await TriggerModelInference("Response user immediately"); - continue; - } - - // Convert received data to text/audio (Twilio sends Base64-encoded audio) - string receivedText = Encoding.UTF8.GetString(buffer, 0, result.Count); + var receivedText = update?.RawResponse; if (string.IsNullOrEmpty(receivedText)) { continue; @@ -179,8 +142,6 @@ public class RealTimeCompletionProvider : IRealTimeCompletion var response = JsonSerializer.Deserialize(receivedText); - _logger.LogDebug($"{nameof(RealTimeCompletionProvider)} received: {response.Type} {receivedText.Length}"); - if (response.Type == "error") { _logger.LogError($"{response.Type}: {receivedText}"); @@ -217,6 +178,11 @@ public class RealTimeCompletionProvider : IRealTimeCompletion _logger.LogDebug($"{response.Type}: {receivedText}"); onModelAudioDeltaReceived(audio.Delta, audio.ItemId); } + else + { + _logger.LogDebug($"{response.Type}: {receivedText}"); + onModelAudioDeltaReceived(audio.Delta, audio.ItemId); + } } else if (response.Type == "response.audio.done") { @@ -248,27 +214,14 @@ public class RealTimeCompletionProvider : IRealTimeCompletion // Handle user interuption onInterruptionDetected(); } - - } while (!result.CloseStatus.HasValue); - - await _webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None); + } } public async Task SendEventToModel(object message) { - if (_webSocket.State != WebSocketState.Open) - { - return; - } + if (_session == null) return; - if (message is not string data) - { - data = JsonSerializer.Serialize(message, _options.JsonSerializerOptions); - } - - var buffer = Encoding.UTF8.GetBytes(data); - - await _webSocket.SendAsync(new ArraySegment(buffer), WebSocketMessageType.Text, true, CancellationToken.None); + await _session.SendEventToModel(message); } public async Task UpdateSession(RealtimeHubConnection conn) diff --git a/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Realtime/Session/AiWebsocketPipelineResponse.cs b/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Realtime/Session/AiWebsocketPipelineResponse.cs new file mode 100644 index 00000000..2a62cdc9 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Realtime/Session/AiWebsocketPipelineResponse.cs @@ -0,0 +1,116 @@ +using System.ClientModel.Primitives; +using System.Net; +using System.Net.WebSockets; + +namespace BotSharp.Plugin.OpenAI.Providers.Realtime.Session; + +public class AiWebsocketPipelineResponse : PipelineResponse +{ + + public AiWebsocketPipelineResponse() + { + + } + + private int _status; + public override int Status => _status; + + private string _reasonPhrase; + public override string ReasonPhrase => _reasonPhrase; + + private MemoryStream _contentStream = new(); + public override Stream? ContentStream + { + get + { + return _contentStream != null ? _contentStream : new MemoryStream(); + } + set => throw new NotImplementedException(); + } + + private BinaryData _content; + public override BinaryData Content + { + get + { + if (_content == null) + { + _content = new(_contentStream.ToArray()); + } + return _content; + } + } + + protected override PipelineResponseHeaders HeadersCore => throw new NotImplementedException(); + + public bool IsComplete { get; private set; } = false; + + + public void CollectReceivedResult(WebSocketReceiveResult receivedResult, BinaryData receivedBytes) + { + if (ContentStream.Length == 0) + { + _status = ConvertWebsocketCloseStatusToHttpStatus(receivedResult.CloseStatus ?? WebSocketCloseStatus.Empty); + _reasonPhrase = receivedResult.CloseStatusDescription?? (receivedResult.CloseStatus ?? WebSocketCloseStatus.Empty).ToString(); + } + else if (receivedResult.MessageType != WebSocketMessageType.Text) + { + throw new NotImplementedException($"{nameof(AiWebsocketPipelineResponse)} currently supports only text messages."); + } + + var rawBytes = receivedBytes.ToArray(); + _contentStream.Position = _contentStream.Length; + _contentStream.Write(rawBytes, 0, rawBytes.Length); + _contentStream.Position = 0; + IsComplete = receivedResult.EndOfMessage; + } + + public override BinaryData BufferContent(CancellationToken cancellationToken = default) + { + return Content; + } + + public override ValueTask BufferContentAsync(CancellationToken cancellationToken = default) + { + return new ValueTask(Task.FromResult(Content)); + } + + public override void Dispose() + { + ContentStream?.Dispose(); + } + + private static int ConvertWebsocketCloseStatusToHttpStatus(WebSocketCloseStatus status) + { + int res; + + switch (status) + { + case WebSocketCloseStatus.Empty: + case WebSocketCloseStatus.NormalClosure: + res = (int)HttpStatusCode.OK; + break; + case WebSocketCloseStatus.EndpointUnavailable: + case WebSocketCloseStatus.ProtocolError: + case WebSocketCloseStatus.InvalidMessageType: + case WebSocketCloseStatus.InvalidPayloadData: + case WebSocketCloseStatus.PolicyViolation: + res = (int)HttpStatusCode.BadRequest; + break; + case WebSocketCloseStatus.MessageTooBig: + res = (int)HttpStatusCode.RequestEntityTooLarge; + break; + case WebSocketCloseStatus.MandatoryExtension: + res = 418; + break; + case WebSocketCloseStatus.InternalServerError: + res = (int)HttpStatusCode.InternalServerError; + break; + default: + res = (int)HttpStatusCode.InternalServerError; + break; + } + + return res; + } +} diff --git a/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Realtime/Session/AsyncWebsocketDataCollectionResult.cs b/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Realtime/Session/AsyncWebsocketDataCollectionResult.cs new file mode 100644 index 00000000..38b46c90 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Realtime/Session/AsyncWebsocketDataCollectionResult.cs @@ -0,0 +1,38 @@ +using System.ClientModel; +using System.Net.WebSockets; + +namespace BotSharp.Plugin.OpenAI.Providers.Realtime.Session; + +public class AsyncWebsocketDataCollectionResult : AsyncCollectionResult +{ + private readonly WebSocket _webSocket; + private readonly CancellationToken _cancellationToken; + + public AsyncWebsocketDataCollectionResult( + WebSocket webSocket, + CancellationToken cancellationToken) + { + _webSocket = webSocket; + _cancellationToken = cancellationToken; + } + + public override ContinuationToken? GetContinuationToken(ClientResult page) + { + return null; + } + + public override async IAsyncEnumerable GetRawPagesAsync() + { + await using var enumerator = new AsyncWebsocketDataResultEnumerator(_webSocket, _cancellationToken); + while (await enumerator.MoveNextAsync().ConfigureAwait(false)) + { + yield return enumerator.Current; + } + } + + protected override async IAsyncEnumerable GetValuesFromPageAsync(ClientResult page) + { + await Task.CompletedTask; + yield return page; + } +} diff --git a/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Realtime/Session/AsyncWebsocketDataResultEnumerator.cs b/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Realtime/Session/AsyncWebsocketDataResultEnumerator.cs new file mode 100644 index 00000000..1d0a3da2 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Realtime/Session/AsyncWebsocketDataResultEnumerator.cs @@ -0,0 +1,52 @@ +using System; +using System.Buffers; +using System.ClientModel; +using System.Net.WebSockets; + +namespace BotSharp.Plugin.OpenAI.Providers.Realtime.Session; + +public class AsyncWebsocketDataResultEnumerator : IAsyncEnumerator +{ + private readonly WebSocket _webSocket; + private readonly CancellationToken _cancellationToken; + private readonly byte[] _buffer; + + public AsyncWebsocketDataResultEnumerator( + WebSocket webSocket, + CancellationToken cancellationToken) + { + _webSocket = webSocket; + _cancellationToken = cancellationToken; + _buffer = ArrayPool.Shared.Rent(1024 * 32); + } + + public ClientResult Current { get; private set; } + + public ValueTask DisposeAsync() + { + _webSocket?.Dispose(); + return new ValueTask(Task.CompletedTask); + } + + public async ValueTask MoveNextAsync() + { + var response = new AiWebsocketPipelineResponse(); + while (!response.IsComplete) + { + var receivedResult = await _webSocket.ReceiveAsync(new(_buffer), _cancellationToken); + + if (receivedResult.CloseStatus.HasValue) + { + Current = null; + return false; + } + + var receivedBytes = _buffer.AsMemory(0, receivedResult.Count); + var receivedData = BinaryData.FromBytes(receivedBytes); + response.CollectReceivedResult(receivedResult, receivedData); + } + + Current = ClientResult.FromResponse(response); + return true; + } +} diff --git a/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Realtime/Session/RealtimeChatSession.cs b/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Realtime/Session/RealtimeChatSession.cs new file mode 100644 index 00000000..788c2a00 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Realtime/Session/RealtimeChatSession.cs @@ -0,0 +1,109 @@ +using BotSharp.Plugin.OpenAI.Models.Realtime; +using System.ClientModel; +using System.Net.WebSockets; +using System.Runtime.CompilerServices; + +namespace BotSharp.Plugin.OpenAI.Providers.Realtime.Session; + +public class RealtimeChatSession : IDisposable +{ + private readonly IServiceProvider _services; + private readonly BotSharpOptions _options; + + private ClientWebSocket _webSocket; + private readonly object _singleReceiveLock = new(); + private readonly SemaphoreSlim _clientEventSemaphore = new(initialCount: 1, maxCount: 1); + private AsyncWebsocketDataCollectionResult _receivedCollectionResult; + + public RealtimeChatSession( + IServiceProvider services, + BotSharpOptions options) + { + _services = services; + _options = options; + } + + public async Task ConnectAsync(string provider, string model, CancellationToken cancellationToken = default) + { + var settingsService = _services.GetRequiredService(); + var settings = settingsService.GetSetting(provider, model); + + _webSocket?.Dispose(); + _webSocket = new ClientWebSocket(); + _webSocket.Options.SetRequestHeader("Authorization", $"Bearer {settings.ApiKey}"); + _webSocket.Options.SetRequestHeader("OpenAI-Beta", "realtime=v1"); + + await _webSocket.ConnectAsync(new Uri($"wss://api.openai.com/v1/realtime?model={model}"), cancellationToken); + } + + public async IAsyncEnumerable ReceiveUpdatesAsync([EnumeratorCancellation] CancellationToken cancellationToken = default) + { + await foreach (ClientResult result in ReceiveInnerUpdatesAsync()) + { + var update = HandleSessionResult(result); + yield return update; + } + } + + public async IAsyncEnumerable ReceiveInnerUpdatesAsync([EnumeratorCancellation] CancellationToken cancellationToken = default) + { + lock (_singleReceiveLock) + { + _receivedCollectionResult ??= new(_webSocket, cancellationToken); + } + + await foreach (var result in _receivedCollectionResult) + { + yield return result; + } + } + + private SessionConversationUpdate HandleSessionResult(ClientResult result) + { + using var response = result.GetRawResponse(); + var bytes = response.Content.ToArray(); + var text = Encoding.UTF8.GetString(bytes, 0, bytes.Length); + return new SessionConversationUpdate + { + RawResponse = text + }; + } + + public async Task SendEventToModel(object message) + { + if (_webSocket.State != WebSocketState.Open) + { + return; + } + + await _clientEventSemaphore.WaitAsync().ConfigureAwait(false); + + try + { + if (message is not string data) + { + data = JsonSerializer.Serialize(message, _options.JsonSerializerOptions); + } + + var buffer = Encoding.UTF8.GetBytes(data); + await _webSocket.SendAsync(new ArraySegment(buffer), WebSocketMessageType.Text, true, CancellationToken.None); + } + finally + { + _clientEventSemaphore.Release(); + } + } + + public async Task Disconnect() + { + if (_webSocket.State == WebSocketState.Open) + { + await _webSocket.CloseAsync(WebSocketCloseStatus.Empty, null, CancellationToken.None); + } + } + + public void Dispose() + { + _webSocket?.Dispose(); + } +} diff --git a/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Functions/OutboundPhoneCallFn.cs b/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Functions/OutboundPhoneCallFn.cs index 22ce213b..b5ecf101 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Functions/OutboundPhoneCallFn.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Functions/OutboundPhoneCallFn.cs @@ -116,16 +116,23 @@ public class OutboundPhoneCallFn : IFunctionCallback record: _twilioSetting.RecordingEnabled, recordingStatusCallback: $"{_twilioSetting.CallbackHost}/twilio/record/status?agent-id={message.CurrentAgentId}&conversation-id={newConversationId}"); - var convService = _services.GetRequiredService(); - var routing = _services.GetRequiredService(); - var originConversationId = convService.ConversationId; - var entryAgentId = routing.EntryAgentId; - - await ForkConversation(args, entryAgentId, originConversationId, newConversationId, call); + if (call.Status == CallResource.StatusEnum.Queued) + { + var convService = _services.GetRequiredService(); + var routing = _services.GetRequiredService(); + var originConversationId = convService.ConversationId; + var entryAgentId = routing.EntryAgentId; - message.Content = $"The generated phone initial message: \"{args.InitialMessage}.\" [NEW CONVERSATION ID: {newConversationId}, TWILIO CALL SID: {call.Sid}, RECORDING: {_twilioSetting.RecordingEnabled}]"; - message.StopCompletion = true; - return true; + await ForkConversation(args, entryAgentId, originConversationId, newConversationId, call); + + message.Content = $"The call has been successfully queued. The initial information is as follows: {args.InitialMessage}."; + return true; + } + else + { + message.Content = $"Failed to make a call, status is {call.Status}."; + return false; + } } private async Task ForkConversation(LlmContextIn args, diff --git a/src/WebStarter/appsettings.json b/src/WebStarter/appsettings.json index 69625129..7ab6f7fd 100644 --- a/src/WebStarter/appsettings.json +++ b/src/WebStarter/appsettings.json @@ -218,9 +218,9 @@ { "Id": "PizzaServer", "Name": "PizzaServer", - "TransportType": "sse", - "TransportOptions": [], - "Location": "http://localhost:58905/sse" + "SseConfig": { + "Endpoint": "http://localhost:58905/sse" + } } ] }, diff --git a/tests/BotSharp.PizzaBot.MCPServer/Properties/launchSettings.json b/tests/BotSharp.PizzaBot.MCPServer/Properties/launchSettings.json index 617e672c..3333cfcd 100644 --- a/tests/BotSharp.PizzaBot.MCPServer/Properties/launchSettings.json +++ b/tests/BotSharp.PizzaBot.MCPServer/Properties/launchSettings.json @@ -2,7 +2,7 @@ "profiles": { "BotSharp.PizzaBot.MCPServer": { "commandName": "Project", - "launchBrowser": true, + "launchBrowser": false, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, diff --git a/tests/BotSharp.PizzaBot.MCPServer/Tools/MakePayment.cs b/tests/BotSharp.PizzaBot.MCPServer/Tools/MakePayment.cs index 5016f5bb..5937a489 100644 --- a/tests/BotSharp.PizzaBot.MCPServer/Tools/MakePayment.cs +++ b/tests/BotSharp.PizzaBot.MCPServer/Tools/MakePayment.cs @@ -1,3 +1,4 @@ +using ModelContextProtocol; using ModelContextProtocol.Server; using System.ComponentModel; using System.ComponentModel.DataAnnotations; @@ -14,11 +15,11 @@ public static class MakePayment { if (order_number is null) { - throw new McpServerException("Missing required argument 'order_number'"); + throw new McpException("Missing required argument 'order_number'"); } if (order_number is null) { - throw new McpServerException("Missing required argument 'total_amount'"); + throw new McpException("Missing required argument 'total_amount'"); } return "Payment proceed successfully. Thank you for your business. Have a great day!"; } diff --git a/tests/BotSharp.PizzaBot.MCPServer/Tools/PizzaPrices.cs b/tests/BotSharp.PizzaBot.MCPServer/Tools/PizzaPrices.cs index e8aa677c..326f217d 100644 --- a/tests/BotSharp.PizzaBot.MCPServer/Tools/PizzaPrices.cs +++ b/tests/BotSharp.PizzaBot.MCPServer/Tools/PizzaPrices.cs @@ -1,3 +1,4 @@ +using ModelContextProtocol; using ModelContextProtocol.Server; using System.ComponentModel; using System.ComponentModel.DataAnnotations; @@ -16,11 +17,11 @@ public static class PizzaPrices { if (pizza_type is null) { - throw new McpServerException("Missing required argument 'pizza_type'"); + throw new McpException("Missing required argument 'pizza_type'"); } if (quantity <= 0) { - throw new McpServerException("Missing required argument 'quantity'"); + throw new McpException("Missing required argument 'quantity'"); } double unit_price = 0; if (pizza_type.ToString() == "Pepperoni Pizza") diff --git a/tests/BotSharp.PizzaBot.MCPServer/Tools/PlaceOrder.cs b/tests/BotSharp.PizzaBot.MCPServer/Tools/PlaceOrder.cs index d6b82bd7..541c992c 100644 --- a/tests/BotSharp.PizzaBot.MCPServer/Tools/PlaceOrder.cs +++ b/tests/BotSharp.PizzaBot.MCPServer/Tools/PlaceOrder.cs @@ -1,3 +1,4 @@ +using ModelContextProtocol; using ModelContextProtocol.Server; using System.ComponentModel; using System.ComponentModel.DataAnnotations; @@ -15,15 +16,15 @@ public static class PlaceOrder { if (pizza_type is null) { - throw new McpServerException("Missing required argument 'pizza_type'"); + throw new McpException("Missing required argument 'pizza_type'"); } if (quantity <= 0) { - throw new McpServerException("Missing required argument 'quantity'"); + throw new McpException("Missing required argument 'quantity'"); } if (unit_price < 0) { - throw new McpServerException("Missing required argument 'unit_price'"); + throw new McpException("Missing required argument 'unit_price'"); } return "The order number is P123-01: {order_number = \"P123-01\" }"; diff --git a/tests/BotSharp.Test.RealtimeVoice/Program.cs b/tests/BotSharp.Test.RealtimeVoice/Program.cs index bb628512..1e6c94f5 100644 --- a/tests/BotSharp.Test.RealtimeVoice/Program.cs +++ b/tests/BotSharp.Test.RealtimeVoice/Program.cs @@ -49,13 +49,20 @@ conn.OnModelAudioResponseDone = () => conn.OnModelUserInterrupted = () => JsonSerializer.Serialize(new { - @event = "clear" + @event = "interrupted" }); +conn.OnUserSpeechDetected = () => + JsonSerializer.Serialize(new + { + @event = "speech_detected" + }); + + await hub.ConnectToModel(async data => { var response = JsonSerializer.Deserialize(data); - if (response.Event == "clear") + if (response.Event == "speech_detected") { channel.ClearBuffer(); } @@ -81,6 +88,7 @@ do DisplayAudioLevel(audioLevel); } while (result.Status == StreamChannelStatus.Open); + int CalculateAudioLevel(byte[] buffer, int bytesRecorded) { // Simple audio level calculation (RMS) @@ -123,5 +131,5 @@ void DisplayAudioLevel(int level) // Display audio level as a bar Console.Write("\rMicrophone: ["); Console.Write(new string('#', displayLevel).PadRight(sep, ' ')); - Console.Write("]"); + Console.Write("]\r"); } diff --git a/tests/BotSharp.Test.RealtimeVoice/appsettings.json b/tests/BotSharp.Test.RealtimeVoice/appsettings.json index d8c2730f..e0ffcb8c 100644 --- a/tests/BotSharp.Test.RealtimeVoice/appsettings.json +++ b/tests/BotSharp.Test.RealtimeVoice/appsettings.json @@ -48,11 +48,9 @@ }, "RealtimeModel": { - "Provider": "google-ai", - "Model": "gemini-2.0-flash-live-001", "InputAudioFormat": "pcm16", "OutputAudioFormat": "pcm16", - "InterruptResponse": false, + "MaxResponseOutputTokens": 4096 }, "PluginLoader": {