From 82b831d4068e2b66d7a281841bfdb5be396be0c8 Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Mon, 19 May 2025 14:54:40 -0500 Subject: [PATCH 01/11] add session reconnect --- .../Attributes/BotSharpAuthAttribute.cs | 32 +++++ .../MLTasks/IRealTimeCompletion.cs | 4 +- .../Realtime/IRealtimeHook.cs | 2 + .../Utilities/StringExtensions.cs | 2 +- .../Services/RealtimeHub.cs | 16 ++- .../Services/AgentService.RefreshAgents.cs | 8 -- .../Session/BotSharpRealtimeSession.cs | 18 ++- .../Session/LlmRealtimeSession.cs | 33 +++-- .../Controllers/AgentController.cs | 2 + .../Controllers/PluginController.cs | 17 +-- .../Controllers/RoleController.cs | 29 +---- .../Controllers/UserController.cs | 22 +--- .../Providers/Chat/ChatCompletionProvider.cs | 2 +- .../Realtime/RealtimeTranscriptionResponse.cs | 19 ++- .../Realtime/RealTimeCompletionProvider.cs | 69 ++++++++--- .../Providers/Chat/ChatCompletionProvider.cs | 2 +- .../Realtime/RealTimeCompletionProvider.cs | 114 ++++++++++++------ .../BotSharp.LLM.Tests/GoogleRealTimeTests.cs | 2 +- 18 files changed, 251 insertions(+), 142 deletions(-) create mode 100644 src/Infrastructure/BotSharp.Abstraction/Infrastructures/Attributes/BotSharpAuthAttribute.cs diff --git a/src/Infrastructure/BotSharp.Abstraction/Infrastructures/Attributes/BotSharpAuthAttribute.cs b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/Attributes/BotSharpAuthAttribute.cs new file mode 100644 index 00000000..25193cb3 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/Attributes/BotSharpAuthAttribute.cs @@ -0,0 +1,32 @@ +using BotSharp.Abstraction.Users; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.DependencyInjection; + +namespace BotSharp.Abstraction.Infrastructures.Attributes; + +/// +/// BotSharp authorization: check whether the request user is admin or root role. +/// +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] +public class BotSharpAuthAttribute : Attribute, IAsyncAuthorizationFilter +{ + public BotSharpAuthAttribute() + { + + } + + public async Task OnAuthorizationAsync(AuthorizationFilterContext context) + { + var services = context.HttpContext.RequestServices; + + var userIdentity = services.GetRequiredService(); + var userService = services.GetRequiredService(); + + var (isAdmin, user) = await userService.IsAdminUser(userIdentity.Id); + if (!isAdmin || user == null) + { + context.Result = new BadRequestResult(); + } + } +} diff --git a/src/Infrastructure/BotSharp.Abstraction/MLTasks/IRealTimeCompletion.cs b/src/Infrastructure/BotSharp.Abstraction/MLTasks/IRealTimeCompletion.cs index d6057859..733eef92 100644 --- a/src/Infrastructure/BotSharp.Abstraction/MLTasks/IRealTimeCompletion.cs +++ b/src/Infrastructure/BotSharp.Abstraction/MLTasks/IRealTimeCompletion.cs @@ -14,11 +14,13 @@ public interface IRealTimeCompletion Func onModelAudioDeltaReceived, Func onModelAudioResponseDone, Func onModelAudioTranscriptDone, - Func, Task> onModelResponseDone, + Func, Task> onModelResponseDone, Func onConversationItemCreated, Func onInputAudioTranscriptionDone, Func onInterruptionDetected); + Task Reconnect(RealtimeHubConnection conn); + Task AppenAudioBuffer(string message); Task AppenAudioBuffer(ArraySegment data, int length); diff --git a/src/Infrastructure/BotSharp.Abstraction/Realtime/IRealtimeHook.cs b/src/Infrastructure/BotSharp.Abstraction/Realtime/IRealtimeHook.cs index bd131b93..51d7383a 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Realtime/IRealtimeHook.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Realtime/IRealtimeHook.cs @@ -1,5 +1,6 @@ using BotSharp.Abstraction.Hooks; using BotSharp.Abstraction.MLTasks; +using BotSharp.Abstraction.Realtime.Models; namespace BotSharp.Abstraction.Realtime; @@ -8,4 +9,5 @@ public interface IRealtimeHook : IHookBase Task OnModelReady(Agent agent, IRealTimeCompletion completer); string[] OnModelTranscriptPrompt(Agent agent); Task OnTranscribeCompleted(RoleDialogModel message, TranscriptionData data); + Task ShouldReconnect(RealtimeHubConnection conn) => Task.FromResult(false); } diff --git a/src/Infrastructure/BotSharp.Abstraction/Utilities/StringExtensions.cs b/src/Infrastructure/BotSharp.Abstraction/Utilities/StringExtensions.cs index 94bda0cd..ea6bb8ec 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Utilities/StringExtensions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Utilities/StringExtensions.cs @@ -5,7 +5,7 @@ namespace BotSharp.Abstraction.Utilities; public static class StringExtensions { - public static string IfNullOrEmptyAs(this string str, string defaultValue) + public static string IfNullOrEmptyAs(this string? str, string defaultValue) => string.IsNullOrEmpty(str) ? defaultValue : str; public static string SubstringMax(this string str, int maxLength) diff --git a/src/Infrastructure/BotSharp.Core.Realtime/Services/RealtimeHub.cs b/src/Infrastructure/BotSharp.Core.Realtime/Services/RealtimeHub.cs index 070bcf26..53318a5f 100644 --- a/src/Infrastructure/BotSharp.Core.Realtime/Services/RealtimeHub.cs +++ b/src/Infrastructure/BotSharp.Core.Realtime/Services/RealtimeHub.cs @@ -98,8 +98,6 @@ public class RealtimeHub : IRealtimeHub } await routing.InvokeFunction(message.FunctionName, message); - dialogs.Add(message); - storage.Append(_conn.ConversationId, message); } else { @@ -107,8 +105,8 @@ public class RealtimeHub : IRealtimeHub dialogs.Add(message); storage.Append(_conn.ConversationId, message); - var hooks = _services.GetHooksOrderByPriority(_conn.CurrentAgentId); - foreach (var hook in hooks) + var convHooks = _services.GetHooksOrderByPriority(_conn.CurrentAgentId); + foreach (var hook in convHooks) { hook.SetAgent(agent) .SetConversation(conversation); @@ -117,6 +115,16 @@ public class RealtimeHub : IRealtimeHub } } } + + var isReconnect = false; + var realtimeHooks = _services.GetHooks(_conn.CurrentAgentId); + foreach (var hook in realtimeHooks) + { + isReconnect = await hook.ShouldReconnect(_conn); + if (isReconnect) break; + } + + return isReconnect; }, onConversationItemCreated: async response => { diff --git a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.RefreshAgents.cs b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.RefreshAgents.cs index c9b3c90c..452f54c9 100644 --- a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.RefreshAgents.cs +++ b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.RefreshAgents.cs @@ -16,14 +16,6 @@ public partial class AgentService return refreshResult; } - var userIdentity = _services.GetRequiredService(); - var userService = _services.GetRequiredService(); - var (isValid, _) = await userService.IsAdminUser(userIdentity.Id); - if (!isValid) - { - return "Unauthorized user."; - } - var agentDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, dbSettings.FileRepository, _agentSettings.DataDir); diff --git a/src/Infrastructure/BotSharp.Core/Session/BotSharpRealtimeSession.cs b/src/Infrastructure/BotSharp.Core/Session/BotSharpRealtimeSession.cs index 7f5f6c15..8dc5b08d 100644 --- a/src/Infrastructure/BotSharp.Core/Session/BotSharpRealtimeSession.cs +++ b/src/Infrastructure/BotSharp.Core/Session/BotSharpRealtimeSession.cs @@ -11,6 +11,7 @@ public class BotSharpRealtimeSession : IDisposable private readonly ChatSessionOptions? _sessionOptions; private readonly object _singleReceiveLock = new(); private AsyncWebsocketDataCollectionResult _receivedCollectionResult; + private bool _disposed = false; public BotSharpRealtimeSession( IServiceProvider services, @@ -57,23 +58,30 @@ public class BotSharpRealtimeSession : IDisposable public async Task SendEventAsync(string message) { - if (_websocket.State == WebSocketState.Open) + if (_disposed || _websocket.State != WebSocketState.Open) { - var buffer = Encoding.UTF8.GetBytes(message); - await _websocket.SendAsync(new ArraySegment(buffer), WebSocketMessageType.Text, true, CancellationToken.None); + return; } + + var buffer = Encoding.UTF8.GetBytes(message); + await _websocket.SendAsync(new ArraySegment(buffer), WebSocketMessageType.Text, true, CancellationToken.None); } public async Task DisconnectAsync() { - if (_websocket.State == WebSocketState.Open) + if (_disposed || _websocket.State != WebSocketState.Open) { - await _websocket.CloseAsync(WebSocketCloseStatus.Empty, null, CancellationToken.None); + return; } + + await _websocket.CloseAsync(WebSocketCloseStatus.Empty, null, CancellationToken.None); } public void Dispose() { + if (_disposed) return; + + _disposed = true; _websocket.Dispose(); } } diff --git a/src/Infrastructure/BotSharp.Core/Session/LlmRealtimeSession.cs b/src/Infrastructure/BotSharp.Core/Session/LlmRealtimeSession.cs index 60ecee04..911338b5 100644 --- a/src/Infrastructure/BotSharp.Core/Session/LlmRealtimeSession.cs +++ b/src/Infrastructure/BotSharp.Core/Session/LlmRealtimeSession.cs @@ -13,6 +13,7 @@ public class LlmRealtimeSession : IDisposable private readonly object _singleReceiveLock = new(); private readonly SemaphoreSlim _clientEventSemaphore = new(initialCount: 1, maxCount: 1); private AsyncWebsocketDataCollectionResult _receivedCollectionResult; + private bool _disposed = false; public LlmRealtimeSession( IServiceProvider services, @@ -24,6 +25,7 @@ public class LlmRealtimeSession : IDisposable public async Task ConnectAsync(Uri uri, Dictionary? headers = null, CancellationToken cancellationToken = default) { + _disposed = false; _webSocket?.Dispose(); _webSocket = new ClientWebSocket(); @@ -73,31 +75,43 @@ public class LlmRealtimeSession : IDisposable public async Task SendEventToModelAsync(object message) { - if (_webSocket.State != WebSocketState.Open) - { - return; - } - - await _clientEventSemaphore.WaitAsync(); - try { + if (_disposed) + { + return; + } + + await _clientEventSemaphore.WaitAsync(); + + if (_webSocket.State != WebSocketState.Open) + { + return; + } + if (message is not string data) { data = JsonSerializer.Serialize(message, _sessionOptions?.JsonOptions); } + //Console.WriteLine($"Sending event to model {data.Substring(0, 20)}"); + var buffer = Encoding.UTF8.GetBytes(data); await _webSocket.SendAsync(new ArraySegment(buffer), WebSocketMessageType.Text, true, CancellationToken.None); } finally { - _clientEventSemaphore.Release(); + if (!_disposed) + { + _clientEventSemaphore.Release(); + } } } public async Task DisconnectAsync() { + if (_disposed) return; + if (_webSocket.State == WebSocketState.Open) { await _webSocket.CloseAsync(WebSocketCloseStatus.Empty, null, CancellationToken.None); @@ -106,6 +120,9 @@ public class LlmRealtimeSession : IDisposable public void Dispose() { + if (_disposed) return; + + _disposed = true; _clientEventSemaphore?.Dispose(); _webSocket?.Dispose(); } diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/AgentController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/AgentController.cs index 401a693f..52983506 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/AgentController.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/AgentController.cs @@ -1,4 +1,5 @@ using BotSharp.Abstraction.Agents.Models; +using BotSharp.Abstraction.Infrastructures.Attributes; namespace BotSharp.OpenAPI.Controllers; @@ -108,6 +109,7 @@ public class AgentController : ControllerBase return AgentViewModel.FromAgent(createdAgent); } + [BotSharpAuth] [HttpPost("/refresh-agents")] public async Task RefreshAgents() { diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/PluginController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/PluginController.cs index 4f6d3f2b..b74cc68a 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/PluginController.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/PluginController.cs @@ -1,3 +1,4 @@ +using BotSharp.Abstraction.Infrastructures.Attributes; using BotSharp.Abstraction.Plugins.Models; using BotSharp.Abstraction.Users.Enums; using BotSharp.Core.Plugins; @@ -10,15 +11,10 @@ public class PluginController(IServiceProvider services, IUserIdentity user, Plu { private readonly IUserIdentity _user = user; + [BotSharpAuth] [HttpGet("/plugins")] public async Task> GetPlugins([FromQuery] PluginFilter filter) { - var isValid = await IsValidUser(); - if (!isValid) - { - return new PagedItems(); - } - var loader = services.GetRequiredService(); return loader.GetPagedPlugins(services, filter); } @@ -72,6 +68,7 @@ public class PluginController(IServiceProvider services, IUserIdentity user, Plu return menu; } + [BotSharpAuth] [HttpPost("/plugin/{id}/install")] public PluginDef InstallPlugin([FromRoute] string id) { @@ -79,17 +76,11 @@ public class PluginController(IServiceProvider services, IUserIdentity user, Plu return loader.UpdatePluginStatus(services, id, true); } + [BotSharpAuth] [HttpPost("/plugin/{id}/remove")] public PluginDef RemovePluginStats([FromRoute] string id) { var loader = services.GetRequiredService(); return loader.UpdatePluginStatus(services, id, false); } - - private async Task IsValidUser() - { - var userService = services.GetRequiredService(); - var (isAdmin, _) = await userService.IsAdminUser(_user.Id); - return isAdmin; - } } diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/RoleController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/RoleController.cs index ee3ac863..7b356f93 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/RoleController.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/RoleController.cs @@ -1,3 +1,4 @@ +using BotSharp.Abstraction.Infrastructures.Attributes; using BotSharp.Abstraction.Roles; namespace BotSharp.OpenAPI.Controllers; @@ -20,15 +21,10 @@ public class RoleController : ControllerBase _user = user; } + [BotSharpAuth] [HttpPost("/role/refresh")] public async Task RefreshRoles() { - var isValid = await IsValidUser(); - if (!isValid) - { - return false; - } - return await _roleService.RefreshRoles(); } @@ -39,6 +35,7 @@ public class RoleController : ControllerBase return await _roleService.GetRoleOptions(); } + [BotSharpAuth] [HttpPost("/roles")] public async Task> GetRoles([FromBody] RoleFilter? filter = null) { @@ -47,12 +44,6 @@ public class RoleController : ControllerBase filter = RoleFilter.Empty(); } - var isValid = await IsValidUser(); - if (!isValid) - { - return Enumerable.Empty(); - } - var roles = await _roleService.GetRoles(filter); return roles.Select(x => RoleViewModel.FromRole(x)).ToList(); } @@ -64,25 +55,13 @@ public class RoleController : ControllerBase return RoleViewModel.FromRole(role); } + [BotSharpAuth] [HttpPut("/role")] public async Task UpdateRole([FromBody] RoleUpdateModel model) { if (model == null) return false; - var isValid = await IsValidUser(); - if (!isValid) - { - return false; - } - var role = RoleUpdateModel.ToRole(model); return await _roleService.UpdateRole(role, isUpdateRoleAgents: true); } - - private async Task IsValidUser() - { - var userService = _services.GetRequiredService(); - var (isAdmin, _) = await userService.IsAdminUser(_user.Id); - return isAdmin; - } } diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/UserController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/UserController.cs index 7cb1aa2b..88a10838 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/UserController.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/UserController.cs @@ -1,3 +1,4 @@ +using BotSharp.Abstraction.Infrastructures.Attributes; using BotSharp.Abstraction.Users.Settings; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; @@ -181,12 +182,6 @@ public class UserController : ControllerBase public async Task> GetUsers([FromBody] UserFilter filter) { var userService = _services.GetRequiredService(); - var isValid = await IsValidUser(); - if (!isValid) - { - return new PagedItems(); - } - var users = await userService.GetUsers(filter); var views = users.Items.Select(x => UserViewModel.FromUser(x)).ToList(); @@ -197,6 +192,7 @@ public class UserController : ControllerBase }; } + [BotSharpAuth] [HttpGet("/user/{id}/details")] public async Task GetUserDetails(string id) { @@ -205,17 +201,12 @@ public class UserController : ControllerBase return UserViewModel.FromUser(user); } + [BotSharpAuth] [HttpPut("/user")] public async Task UpdateUser([FromBody] UserUpdateModel model) { if (model == null) return false; - var isValid = await IsValidUser(); - if (!isValid) - { - return false; - } - var userService = _services.GetRequiredService(); var updated = await userService.UpdateUser(UserUpdateModel.ToUser(model), isUpdateUserAgents: true); return updated; @@ -251,13 +242,6 @@ public class UserController : ControllerBase #region Private methods - private async Task IsValidUser() - { - var userService = _services.GetRequiredService(); - var (isAdmin, _) = await userService.IsAdminUser(_user.Id); - return isAdmin; - } - private FileContentResult BuildFileResult(string file) { var fileStorage = _services.GetRequiredService(); diff --git a/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/Chat/ChatCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/Chat/ChatCompletionProvider.cs index e2d9a161..114ea69d 100644 --- a/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/Chat/ChatCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/Chat/ChatCompletionProvider.cs @@ -305,7 +305,7 @@ public class ChatCompletionProvider : IChatCompletion { messages.Add(new AssistantChatMessage(new List { - ChatToolCall.CreateFunctionToolCall(message.ToolCallId ?? message.FunctionName, message.FunctionName, BinaryData.FromString(message.FunctionArgs ?? "{}")) + ChatToolCall.CreateFunctionToolCall(message.ToolCallId.IfNullOrEmptyAs(message.FunctionName), message.FunctionName, BinaryData.FromString(message.FunctionArgs ?? "{}")) })); messages.Add(new ToolChatMessage(message.ToolCallId ?? message.FunctionName, message.Content)); diff --git a/src/Plugins/BotSharp.Plugin.GoogleAI/Models/Realtime/RealtimeTranscriptionResponse.cs b/src/Plugins/BotSharp.Plugin.GoogleAI/Models/Realtime/RealtimeTranscriptionResponse.cs index 189252fa..0a383c80 100644 --- a/src/Plugins/BotSharp.Plugin.GoogleAI/Models/Realtime/RealtimeTranscriptionResponse.cs +++ b/src/Plugins/BotSharp.Plugin.GoogleAI/Models/Realtime/RealtimeTranscriptionResponse.cs @@ -9,6 +9,8 @@ internal class RealtimeTranscriptionResponse : IDisposable } + private bool _disposed = false; + private MemoryStream _contentStream = new(); public Stream? ContentStream { @@ -20,6 +22,8 @@ internal class RealtimeTranscriptionResponse : IDisposable public void Collect(string text) { + if (_disposed) return; + var binary = BinaryData.FromString(text); var bytes = binary.ToArray(); @@ -30,7 +34,7 @@ internal class RealtimeTranscriptionResponse : IDisposable public string GetText() { - if (_contentStream.Length == 0) + if (_disposed || _contentStream.Length == 0) { return string.Empty; } @@ -42,12 +46,21 @@ internal class RealtimeTranscriptionResponse : IDisposable public void Clear() { - _contentStream.Position = 0; - _contentStream.SetLength(0); + try + { + if (_disposed) return; + + _contentStream.Position = 0; + _contentStream.SetLength(0); + } + catch { } } public void Dispose() { + if (_disposed) return; + + _disposed = true; _contentStream?.Dispose(); } } diff --git a/src/Plugins/BotSharp.Plugin.GoogleAI/Providers/Realtime/RealTimeCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.GoogleAI/Providers/Realtime/RealTimeCompletionProvider.cs index 5df8ee13..6f3d9d26 100644 --- a/src/Plugins/BotSharp.Plugin.GoogleAI/Providers/Realtime/RealTimeCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.GoogleAI/Providers/Realtime/RealTimeCompletionProvider.cs @@ -14,7 +14,7 @@ public class GoogleRealTimeProvider : IRealTimeCompletion public string Provider => "google-ai"; public string Model => _model; - private string _model = GoogleAIModels.Gemini2FlashExp; + private string _model = GoogleAIModels.Gemini2FlashLive001; private readonly IServiceProvider _services; private readonly ILogger _logger; @@ -35,14 +35,14 @@ public class GoogleRealTimeProvider : IRealTimeCompletion private RealtimeTranscriptionResponse _inputStream = new(); private RealtimeTranscriptionResponse _outputStream = new(); - + private bool _isBlocking = false; private RealtimeHubConnection _conn; private Func _onModelReady; private Func _onModelAudioDeltaReceived; private Func _onModelAudioResponseDone; private Func _onModelAudioTranscriptDone; - private Func, Task> _onModelResponseDone; + private Func, Task> _onModelResponseDone; private Func _onConversationItemCreated; private Func _onInputAudioTranscriptionDone; private Func _onInterruptionDetected; @@ -68,7 +68,7 @@ public class GoogleRealTimeProvider : IRealTimeCompletion Func onModelAudioDeltaReceived, Func onModelAudioResponseDone, Func onModelAudioTranscriptDone, - Func, Task> onModelResponseDone, + Func, Task> onModelResponseDone, Func onConversationItemCreated, Func onInputAudioTranscriptionDone, Func onInterruptionDetected) @@ -90,6 +90,7 @@ public class GoogleRealTimeProvider : IRealTimeCompletion var modelSettings = settingsService.GetSetting(Provider, _model); Reset(); + _isBlocking = true; _inputStream = new(); _outputStream = new(); _session = new LlmRealtimeSession(_services, new ChatSessionOptions @@ -105,6 +106,8 @@ public class GoogleRealTimeProvider : IRealTimeCompletion private async Task ReceiveMessage() { + var isReconnect = false; + await foreach (ChatSessionUpdate update in _session.ReceiveUpdatesAsync(CancellationToken.None)) { var receivedText = update?.RawResponse; @@ -124,6 +127,7 @@ public class GoogleRealTimeProvider : IRealTimeCompletion if (response.SetupComplete != null) { _logger.LogInformation($"Session setup completed."); + _isBlocking = false; } else if (response.SessionResumptionUpdate != null) { @@ -138,7 +142,7 @@ public class GoogleRealTimeProvider : IRealTimeCompletion if (functionCall != null) { var messages = OnFunctionCall(_conn, functionCall); - await _onModelResponseDone(messages); + isReconnect = await _onModelResponseDone(messages); } } else if (response.ServerContent != null) @@ -155,8 +159,6 @@ public class GoogleRealTimeProvider : IRealTimeCompletion if (response.ServerContent.ModelTurn != null) { - _logger.LogInformation($"Model audio delta received."); - // Handle input transcription var inputTranscription = _inputStream.GetText(); if (!string.IsNullOrEmpty(inputTranscription)) @@ -188,31 +190,62 @@ public class GoogleRealTimeProvider : IRealTimeCompletion // Handle output transcription var outputTranscription = _outputStream.GetText(); - if (!string.IsNullOrEmpty(outputTranscription)) - { - var messages = await OnResponseDone(_conn, outputTranscription, response.UsageMetaData); - await _onModelResponseDone(messages); - } + var messages = await OnResponseDone(_conn, outputTranscription ?? string.Empty, response.UsageMetaData); + isReconnect = await _onModelResponseDone(messages); _inputStream.Clear(); _outputStream.Clear(); } } + + if (isReconnect) + { + break; + } } catch (Exception ex) { - _logger.LogError(ex, $"Error when deserializing server response. {ex.Message}"); + _logger.LogError(ex, $"Error when handling server response. {ex.Message}"); break; } } - _inputStream.Dispose(); - _outputStream.Dispose(); - _session.Dispose(); + if (isReconnect) + { + await Reconnect(_conn); + } + else + { + _inputStream.Dispose(); + _outputStream.Dispose(); + _session.Dispose(); + } } + public async Task Reconnect(RealtimeHubConnection conn) + { + _logger.LogInformation($"Reconnecting {Provider} realtime server..."); + + _isBlocking = true; + _conn = conn; + await Disconnect(); + await Task.Delay(500); + await Connect( + _conn, + _onModelReady, + _onModelAudioDeltaReceived, + _onModelAudioResponseDone, + _onModelAudioTranscriptDone, + _onModelResponseDone, + _onConversationItemCreated, + _onInputAudioTranscriptionDone, + _onInterruptionDetected); + } + public async Task Disconnect() { + _logger.LogInformation($"Disconnecting {Provider} realtime server..."); + if (_session != null) { _inputStream?.Dispose(); @@ -224,6 +257,8 @@ public class GoogleRealTimeProvider : IRealTimeCompletion public async Task AppenAudioBuffer(string message) { + if (_isBlocking) return; + await SendEventToModel(new RealtimeClientPayload { RealtimeInput = new() @@ -235,6 +270,8 @@ public class GoogleRealTimeProvider : IRealTimeCompletion public async Task AppenAudioBuffer(ArraySegment data, int length) { + if (_isBlocking) return; + var buffer = data.AsSpan(0, length).ToArray(); await SendEventToModel(new RealtimeClientPayload { diff --git a/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Chat/ChatCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Chat/ChatCompletionProvider.cs index c0a9c0d4..074c9b48 100644 --- a/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Chat/ChatCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Chat/ChatCompletionProvider.cs @@ -272,7 +272,7 @@ public class ChatCompletionProvider : IChatCompletion { messages.Add(new AssistantChatMessage(new List { - ChatToolCall.CreateFunctionToolCall(message.ToolCallId ?? message.FunctionName, message.FunctionName, BinaryData.FromString(message.FunctionArgs ?? "{}")) + ChatToolCall.CreateFunctionToolCall(message.ToolCallId.IfNullOrEmptyAs(message.FunctionName), message.FunctionName, BinaryData.FromString(message.FunctionArgs ?? "{}")) })); messages.Add(new ToolChatMessage(message.ToolCallId ?? message.FunctionName, message.Content)); diff --git a/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Realtime/RealTimeCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Realtime/RealTimeCompletionProvider.cs index 52f63447..a2a4dba3 100644 --- a/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Realtime/RealTimeCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Realtime/RealTimeCompletionProvider.cs @@ -16,8 +16,19 @@ public class RealTimeCompletionProvider : IRealTimeCompletion private readonly ILogger _logger; private readonly BotSharpOptions _botsharpOptions; - protected string _model = "gpt-4o-mini-realtime-preview"; + private string _model = "gpt-4o-mini-realtime-preview"; private LlmRealtimeSession _session; + private bool _isBlocking = false; + + private RealtimeHubConnection _conn; + private Func _onModelReady; + private Func _onModelAudioDeltaReceived; + private Func _onModelAudioResponseDone; + private Func _onModelAudioTranscriptDone; + private Func, Task> _onModelResponseDone; + private Func _onConversationItemCreated; + private Func _onInputAudioTranscriptionDone; + private Func _onInterruptionDetected; public RealTimeCompletionProvider( IServiceProvider services, @@ -35,11 +46,21 @@ public class RealTimeCompletionProvider : IRealTimeCompletion Func onModelAudioDeltaReceived, Func onModelAudioResponseDone, Func onModelAudioTranscriptDone, - Func, Task> onModelResponseDone, + Func, Task> onModelResponseDone, Func onConversationItemCreated, Func onInputAudioTranscriptionDone, Func onInterruptionDetected) { + _conn = conn; + _onModelReady = onModelReady; + _onModelAudioDeltaReceived = onModelAudioDeltaReceived; + _onModelAudioResponseDone = onModelAudioResponseDone; + _onModelAudioTranscriptDone = onModelAudioTranscriptDone; + _onModelResponseDone = onModelResponseDone; + _onConversationItemCreated = onConversationItemCreated; + _onInputAudioTranscriptionDone = onInputAudioTranscriptionDone; + _onInterruptionDetected = onInterruptionDetected; + var settingsService = _services.GetRequiredService(); var realtimeSettings = _services.GetRequiredService(); @@ -61,31 +82,12 @@ public class RealTimeCompletionProvider : IRealTimeCompletion }, cancellationToken: CancellationToken.None); - _ = ReceiveMessage( - realtimeSettings, - conn, - onModelReady, - onModelAudioDeltaReceived, - onModelAudioResponseDone, - onModelAudioTranscriptDone, - onModelResponseDone, - onConversationItemCreated, - onInputAudioTranscriptionDone, - onInterruptionDetected); + _ = ReceiveMessage(realtimeSettings); } - private async Task ReceiveMessage( - RealtimeModelSettings realtimeSettings, - RealtimeHubConnection conn, - Func onModelReady, - Func onModelAudioDeltaReceived, - Func onModelAudioResponseDone, - Func onModelAudioTranscriptDone, - Func, Task> onModelResponseDone, - Func onConversationItemCreated, - Func onInputAudioTranscriptionDone, - Func onInterruptionDetected) + private async Task ReceiveMessage(RealtimeModelSettings realtimeSettings) { + var isReconnect = false; DateTime? startTime = null; await foreach (ChatSessionUpdate update in _session.ReceiveUpdatesAsync(CancellationToken.None)) @@ -121,7 +123,8 @@ public class RealTimeCompletionProvider : IRealTimeCompletion else if (response.Type == "session.created") { _logger.LogInformation($"{response.Type}: {receivedText}"); - await onModelReady(); + _isBlocking = false; + await _onModelReady(); } else if (response.Type == "session.updated") { @@ -135,7 +138,7 @@ public class RealTimeCompletionProvider : IRealTimeCompletion { _logger.LogInformation($"{response.Type}: {receivedText}"); var data = JsonSerializer.Deserialize(receivedText); - await onModelAudioTranscriptDone(data.Transcript); + await _onModelAudioTranscriptDone(data.Transcript); } else if (response.Type == "response.audio.delta") { @@ -143,13 +146,13 @@ public class RealTimeCompletionProvider : IRealTimeCompletion if (audio?.Delta != null) { _logger.LogDebug($"{response.Type}: {receivedText}"); - await onModelAudioDeltaReceived(audio.Delta, audio.ItemId); + await _onModelAudioDeltaReceived(audio.Delta, audio.ItemId); } } else if (response.Type == "response.audio.done") { _logger.LogInformation($"{response.Type}: {receivedText}"); - await onModelAudioResponseDone(); + await _onModelAudioResponseDone(); } else if (response.Type == "response.done") { @@ -159,14 +162,14 @@ public class RealTimeCompletionProvider : IRealTimeCompletion { if (data.StatusDetails.Type == "incomplete" && data.StatusDetails.Reason == "max_output_tokens") { - await onInterruptionDetected(); + await _onInterruptionDetected(); await TriggerModelInference("Response user concisely"); } } else { - var messages = await OnResponsedDone(conn, receivedText); - await onModelResponseDone(messages); + var messages = await OnResponsedDone(_conn, receivedText); + isReconnect = await _onModelResponseDone(messages); } } else if (response.Type == "conversation.item.created") @@ -179,23 +182,23 @@ public class RealTimeCompletionProvider : IRealTimeCompletion startTime = DateTime.UtcNow; } - await onConversationItemCreated(receivedText); + await _onConversationItemCreated(receivedText); } else if (response.Type == "conversation.item.input_audio_transcription.completed") { _logger.LogInformation($"{response.Type}: {receivedText}"); - var message = await OnUserAudioTranscriptionCompleted(conn, receivedText); + var message = await OnUserAudioTranscriptionCompleted(_conn, receivedText); if (!string.IsNullOrEmpty(message.Content)) { - await onInputAudioTranscriptionDone(message); + await _onInputAudioTranscriptionDone(message); } } else if (response.Type == "input_audio_buffer.speech_started") { _logger.LogInformation($"{response.Type}: {receivedText}"); // Handle user interuption - await onInterruptionDetected(); + await _onInterruptionDetected(); } else if (response.Type == "input_audio_buffer.speech_stopped") { @@ -205,13 +208,48 @@ public class RealTimeCompletionProvider : IRealTimeCompletion { _logger.LogInformation($"{response.Type}: {receivedText}"); } + + if (isReconnect) + { + break; + } } - _session.Dispose(); + if (isReconnect) + { + await Reconnect(_conn); + } + else + { + _session.Dispose(); + } + } + + + public async Task Reconnect(RealtimeHubConnection conn) + { + _logger.LogInformation($"Reconnecting {Provider} realtime server..."); + + _isBlocking = true; + _conn = conn; + await Disconnect(); + await Task.Delay(500); + await Connect( + _conn, + _onModelReady, + _onModelAudioDeltaReceived, + _onModelAudioResponseDone, + _onModelAudioTranscriptDone, + _onModelResponseDone, + _onConversationItemCreated, + _onInputAudioTranscriptionDone, + _onInterruptionDetected); } public async Task Disconnect() { + _logger.LogInformation($"Disconnecting {Provider} realtime server..."); + if (_session != null) { await _session.DisconnectAsync(); @@ -221,6 +259,8 @@ public class RealTimeCompletionProvider : IRealTimeCompletion public async Task AppenAudioBuffer(string message) { + if (_isBlocking) return; + var audioAppend = new { type = "input_audio_buffer.append", @@ -232,6 +272,8 @@ public class RealTimeCompletionProvider : IRealTimeCompletion public async Task AppenAudioBuffer(ArraySegment data, int length) { + if (_isBlocking) return; + var message = Convert.ToBase64String(data.AsSpan(0, length).ToArray()); await AppenAudioBuffer(message); } diff --git a/tests/BotSharp.LLM.Tests/GoogleRealTimeTests.cs b/tests/BotSharp.LLM.Tests/GoogleRealTimeTests.cs index 061f0545..8e4202a7 100644 --- a/tests/BotSharp.LLM.Tests/GoogleRealTimeTests.cs +++ b/tests/BotSharp.LLM.Tests/GoogleRealTimeTests.cs @@ -46,7 +46,7 @@ namespace BotSharp.Plugin.Google.Core async (s, s1) => { Console.WriteLine(s); }, async () => { }, async (s) => { Console.WriteLine(s); }, - async list => { Console.WriteLine(list); }, + async list => { Console.WriteLine(list); return false; }, async s => { Console.WriteLine(s); }, async model => { Console.WriteLine(model); }, async () => { Console.WriteLine("UserInterrupted"); }); From b0b6c4b651578616f9757f395429ae088e2c5b7e Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Mon, 19 May 2025 15:02:28 -0500 Subject: [PATCH 02/11] remove comments --- src/Infrastructure/BotSharp.Core/Session/LlmRealtimeSession.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Infrastructure/BotSharp.Core/Session/LlmRealtimeSession.cs b/src/Infrastructure/BotSharp.Core/Session/LlmRealtimeSession.cs index 911338b5..fe2831bc 100644 --- a/src/Infrastructure/BotSharp.Core/Session/LlmRealtimeSession.cs +++ b/src/Infrastructure/BotSharp.Core/Session/LlmRealtimeSession.cs @@ -94,8 +94,6 @@ public class LlmRealtimeSession : IDisposable data = JsonSerializer.Serialize(message, _sessionOptions?.JsonOptions); } - //Console.WriteLine($"Sending event to model {data.Substring(0, 20)}"); - var buffer = Encoding.UTF8.GetBytes(data); await _webSocket.SendAsync(new ArraySegment(buffer), WebSocketMessageType.Text, true, CancellationToken.None); } From 73c2e9c72ea7d4c912ae0e192105a99db5df9f36 Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Mon, 19 May 2025 15:44:20 -0500 Subject: [PATCH 03/11] minor refine --- .../MLTasks/IRealTimeCompletion.cs | 2 +- .../Services/RealtimeHub.cs | 5 ++- .../Realtime/RealTimeCompletionProvider.cs | 43 ++++++++----------- ...rosoftExtensionsAITextEmbeddingProvider.cs | 2 +- .../BotSharp.Plugin.OpenAI.csproj | 4 ++ .../Realtime/RealTimeCompletionProvider.cs | 23 +++------- .../BotSharp.LLM.Tests/GoogleRealTimeTests.cs | 2 +- 7 files changed, 34 insertions(+), 47 deletions(-) diff --git a/src/Infrastructure/BotSharp.Abstraction/MLTasks/IRealTimeCompletion.cs b/src/Infrastructure/BotSharp.Abstraction/MLTasks/IRealTimeCompletion.cs index 733eef92..bb3104d3 100644 --- a/src/Infrastructure/BotSharp.Abstraction/MLTasks/IRealTimeCompletion.cs +++ b/src/Infrastructure/BotSharp.Abstraction/MLTasks/IRealTimeCompletion.cs @@ -14,7 +14,7 @@ public interface IRealTimeCompletion Func onModelAudioDeltaReceived, Func onModelAudioResponseDone, Func onModelAudioTranscriptDone, - Func, Task> onModelResponseDone, + Func, Task> onModelResponseDone, Func onConversationItemCreated, Func onInputAudioTranscriptionDone, Func onInterruptionDetected); diff --git a/src/Infrastructure/BotSharp.Core.Realtime/Services/RealtimeHub.cs b/src/Infrastructure/BotSharp.Core.Realtime/Services/RealtimeHub.cs index 53318a5f..baac131c 100644 --- a/src/Infrastructure/BotSharp.Core.Realtime/Services/RealtimeHub.cs +++ b/src/Infrastructure/BotSharp.Core.Realtime/Services/RealtimeHub.cs @@ -124,7 +124,10 @@ public class RealtimeHub : IRealtimeHub if (isReconnect) break; } - return isReconnect; + if (isReconnect) + { + await _completer.Reconnect(_conn); + } }, onConversationItemCreated: async response => { diff --git a/src/Plugins/BotSharp.Plugin.GoogleAI/Providers/Realtime/RealTimeCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.GoogleAI/Providers/Realtime/RealTimeCompletionProvider.cs index 6f3d9d26..f708092d 100644 --- a/src/Plugins/BotSharp.Plugin.GoogleAI/Providers/Realtime/RealTimeCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.GoogleAI/Providers/Realtime/RealTimeCompletionProvider.cs @@ -42,7 +42,7 @@ public class GoogleRealTimeProvider : IRealTimeCompletion private Func _onModelAudioDeltaReceived; private Func _onModelAudioResponseDone; private Func _onModelAudioTranscriptDone; - private Func, Task> _onModelResponseDone; + private Func, Task> _onModelResponseDone; private Func _onConversationItemCreated; private Func _onInputAudioTranscriptionDone; private Func _onInterruptionDetected; @@ -68,11 +68,13 @@ public class GoogleRealTimeProvider : IRealTimeCompletion Func onModelAudioDeltaReceived, Func onModelAudioResponseDone, Func onModelAudioTranscriptDone, - Func, Task> onModelResponseDone, + Func, Task> onModelResponseDone, Func onConversationItemCreated, Func onInputAudioTranscriptionDone, Func onInterruptionDetected) { + _logger.LogInformation($"Connecting {Provider} realtime server..."); + _conn = conn; _onModelReady = onModelReady; _onModelAudioDeltaReceived = onModelAudioDeltaReceived; @@ -106,8 +108,6 @@ public class GoogleRealTimeProvider : IRealTimeCompletion private async Task ReceiveMessage() { - var isReconnect = false; - await foreach (ChatSessionUpdate update in _session.ReceiveUpdatesAsync(CancellationToken.None)) { var receivedText = update?.RawResponse; @@ -142,7 +142,7 @@ public class GoogleRealTimeProvider : IRealTimeCompletion if (functionCall != null) { var messages = OnFunctionCall(_conn, functionCall); - isReconnect = await _onModelResponseDone(messages); + await _onModelResponseDone(messages); } } else if (response.ServerContent != null) @@ -161,9 +161,9 @@ public class GoogleRealTimeProvider : IRealTimeCompletion { // Handle input transcription var inputTranscription = _inputStream.GetText(); - if (!string.IsNullOrEmpty(inputTranscription)) + if (!string.IsNullOrWhiteSpace(inputTranscription)) { - var message = OnUserAudioTranscriptionCompleted(_conn, inputTranscription); + var message = OnUserAudioTranscriptionCompleted(_conn, inputTranscription ?? string.Empty); await _onInputAudioTranscriptionDone(message); } _inputStream.Clear(); @@ -190,17 +190,15 @@ public class GoogleRealTimeProvider : IRealTimeCompletion // Handle output transcription var outputTranscription = _outputStream.GetText(); - var messages = await OnResponseDone(_conn, outputTranscription ?? string.Empty, response.UsageMetaData); - isReconnect = await _onModelResponseDone(messages); + if (!string.IsNullOrWhiteSpace(outputTranscription)) + { + var messages = await OnResponseDone(_conn, outputTranscription ?? string.Empty, response.UsageMetaData); + await _onModelResponseDone(messages); + } _inputStream.Clear(); _outputStream.Clear(); } } - - if (isReconnect) - { - break; - } } catch (Exception ex) { @@ -209,16 +207,9 @@ public class GoogleRealTimeProvider : IRealTimeCompletion } } - if (isReconnect) - { - await Reconnect(_conn); - } - else - { - _inputStream.Dispose(); - _outputStream.Dispose(); - _session.Dispose(); - } + _inputStream.Dispose(); + _outputStream.Dispose(); + _session.Dispose(); } @@ -369,8 +360,8 @@ public class GoogleRealTimeProvider : IRealTimeCompletion Model = Model.ToModelId(), SystemInstruction = request.SystemInstruction, Tools = request.Tools?.ToArray(), - InputAudioTranscription = realtimeSetting.InputAudioTranscribe ? new() : null, - OutputAudioTranscription = realtimeSetting.InputAudioTranscribe ? new() : null + InputAudioTranscription = new(), + OutputAudioTranscription = new() } }; diff --git a/src/Plugins/BotSharp.Plugin.MicrosoftExtensionsAI/MicrosoftExtensionsAITextEmbeddingProvider.cs b/src/Plugins/BotSharp.Plugin.MicrosoftExtensionsAI/MicrosoftExtensionsAITextEmbeddingProvider.cs index 1758468a..d7933a1f 100644 --- a/src/Plugins/BotSharp.Plugin.MicrosoftExtensionsAI/MicrosoftExtensionsAITextEmbeddingProvider.cs +++ b/src/Plugins/BotSharp.Plugin.MicrosoftExtensionsAI/MicrosoftExtensionsAITextEmbeddingProvider.cs @@ -27,7 +27,7 @@ public sealed class MicrosoftExtensionsAITextEmbeddingProvider : ITextEmbedding /// public async Task GetVectorAsync(string text) => - (await _generator.GenerateEmbeddingVectorAsync(text, CreateOptions())).ToArray(); + (await _generator.GenerateVectorAsync(text, CreateOptions())).ToArray(); /// public async Task> GetVectorsAsync(List texts) diff --git a/src/Plugins/BotSharp.Plugin.OpenAI/BotSharp.Plugin.OpenAI.csproj b/src/Plugins/BotSharp.Plugin.OpenAI/BotSharp.Plugin.OpenAI.csproj index 2455e7b5..e54033b6 100644 --- a/src/Plugins/BotSharp.Plugin.OpenAI/BotSharp.Plugin.OpenAI.csproj +++ b/src/Plugins/BotSharp.Plugin.OpenAI/BotSharp.Plugin.OpenAI.csproj @@ -10,6 +10,10 @@ $(SolutionDir)packages + + + + diff --git a/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Realtime/RealTimeCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Realtime/RealTimeCompletionProvider.cs index a2a4dba3..d008cc60 100644 --- a/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Realtime/RealTimeCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Realtime/RealTimeCompletionProvider.cs @@ -25,7 +25,7 @@ public class RealTimeCompletionProvider : IRealTimeCompletion private Func _onModelAudioDeltaReceived; private Func _onModelAudioResponseDone; private Func _onModelAudioTranscriptDone; - private Func, Task> _onModelResponseDone; + private Func, Task> _onModelResponseDone; private Func _onConversationItemCreated; private Func _onInputAudioTranscriptionDone; private Func _onInterruptionDetected; @@ -46,11 +46,13 @@ public class RealTimeCompletionProvider : IRealTimeCompletion Func onModelAudioDeltaReceived, Func onModelAudioResponseDone, Func onModelAudioTranscriptDone, - Func, Task> onModelResponseDone, + Func, Task> onModelResponseDone, Func onConversationItemCreated, Func onInputAudioTranscriptionDone, Func onInterruptionDetected) { + _logger.LogInformation($"Connecting {Provider} realtime server..."); + _conn = conn; _onModelReady = onModelReady; _onModelAudioDeltaReceived = onModelAudioDeltaReceived; @@ -87,7 +89,6 @@ public class RealTimeCompletionProvider : IRealTimeCompletion private async Task ReceiveMessage(RealtimeModelSettings realtimeSettings) { - var isReconnect = false; DateTime? startTime = null; await foreach (ChatSessionUpdate update in _session.ReceiveUpdatesAsync(CancellationToken.None)) @@ -169,7 +170,7 @@ public class RealTimeCompletionProvider : IRealTimeCompletion else { var messages = await OnResponsedDone(_conn, receivedText); - isReconnect = await _onModelResponseDone(messages); + await _onModelResponseDone(messages); } } else if (response.Type == "conversation.item.created") @@ -208,21 +209,9 @@ public class RealTimeCompletionProvider : IRealTimeCompletion { _logger.LogInformation($"{response.Type}: {receivedText}"); } - - if (isReconnect) - { - break; - } } - if (isReconnect) - { - await Reconnect(_conn); - } - else - { - _session.Dispose(); - } + _session.Dispose(); } diff --git a/tests/BotSharp.LLM.Tests/GoogleRealTimeTests.cs b/tests/BotSharp.LLM.Tests/GoogleRealTimeTests.cs index 8e4202a7..061f0545 100644 --- a/tests/BotSharp.LLM.Tests/GoogleRealTimeTests.cs +++ b/tests/BotSharp.LLM.Tests/GoogleRealTimeTests.cs @@ -46,7 +46,7 @@ namespace BotSharp.Plugin.Google.Core async (s, s1) => { Console.WriteLine(s); }, async () => { }, async (s) => { Console.WriteLine(s); }, - async list => { Console.WriteLine(list); return false; }, + async list => { Console.WriteLine(list); }, async s => { Console.WriteLine(s); }, async model => { Console.WriteLine(model); }, async () => { Console.WriteLine("UserInterrupted"); }); From aab468f41005df6054155d158998037541825180 Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Tue, 20 May 2025 10:59:42 -0500 Subject: [PATCH 04/11] add session logging and crontab settings --- .../Crontab/Settings/CrontabSettings.cs | 12 ++++++++++++ .../Realtime/Models/Session/ChatSessionOptions.cs | 3 +++ .../{ => Settings}/BotSharpDatabaseSettings.cs | 2 +- .../BotSharp.Core.Crontab/CrontabPlugin.cs | 15 +++++++++++++-- src/Infrastructure/BotSharp.Core.Crontab/Using.cs | 1 + .../Agents/Services/AgentService.CreateAgent.cs | 1 + .../Agents/Services/AgentService.RefreshAgents.cs | 1 + .../Agents/Services/AgentService.UpdateAgent.cs | 1 + .../BotSharp.Core/Agents/Services/AgentService.cs | 1 + .../BotSharp.Core/BotSharpCoreExtensions.cs | 12 +----------- .../Services/Storage/LocalFileStorageService.cs | 1 + .../Infrastructures/DistributedLocker.cs | 4 ++++ .../AsyncWebsocketDataCollectionResult.cs | 9 ++++----- .../AsyncWebsocketDataResultEnumerator.cs | 14 +++++++++----- .../BotSharp.Core/Repository/DataContextHelper.cs | 1 + .../Repository/FileRepository/FileRepository.cs | 1 + .../BotSharp.Core/Repository/RepositoryPlugin.cs | 1 + .../Provider/NativeWhisperProvider.cs | 1 + .../ChatStreamMiddleware.cs | 1 + .../Realtime/RealTimeCompletionProvider.cs | 4 +++- .../MongoDbContext.cs | 2 ++ .../MongoStoragePlugin.cs | 1 + .../Realtime/RealTimeCompletionProvider.cs | 4 +++- src/WebStarter/appsettings.json | 11 +++++++++++ 24 files changed, 78 insertions(+), 26 deletions(-) create mode 100644 src/Infrastructure/BotSharp.Abstraction/Crontab/Settings/CrontabSettings.cs rename src/Infrastructure/BotSharp.Abstraction/Repositories/{ => Settings}/BotSharpDatabaseSettings.cs (95%) diff --git a/src/Infrastructure/BotSharp.Abstraction/Crontab/Settings/CrontabSettings.cs b/src/Infrastructure/BotSharp.Abstraction/Crontab/Settings/CrontabSettings.cs new file mode 100644 index 00000000..4bca1240 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Crontab/Settings/CrontabSettings.cs @@ -0,0 +1,12 @@ +namespace BotSharp.Abstraction.Crontab.Settings; + +public class CrontabSettings +{ + public CrontabBaseSetting EventSubscriber { get; set; } = new(); + public CrontabBaseSetting Watcher { get; set; } = new(); +} + +public class CrontabBaseSetting +{ + public bool Enabled { get; set; } = true; +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Realtime/Models/Session/ChatSessionOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Realtime/Models/Session/ChatSessionOptions.cs index 3bcba8ba..f4318fbb 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Realtime/Models/Session/ChatSessionOptions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Realtime/Models/Session/ChatSessionOptions.cs @@ -1,9 +1,12 @@ +using Microsoft.Extensions.Logging; using System.Text.Json; namespace BotSharp.Abstraction.Realtime.Models.Session; public class ChatSessionOptions { + public string Provider { get; set; } public int? BufferSize { get; set; } public JsonSerializerOptions? JsonOptions { get; set; } + public ILogger? Logger { get; set; } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Repositories/BotSharpDatabaseSettings.cs b/src/Infrastructure/BotSharp.Abstraction/Repositories/Settings/BotSharpDatabaseSettings.cs similarity index 95% rename from src/Infrastructure/BotSharp.Abstraction/Repositories/BotSharpDatabaseSettings.cs rename to src/Infrastructure/BotSharp.Abstraction/Repositories/Settings/BotSharpDatabaseSettings.cs index 19f5b141..76fdf455 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Repositories/BotSharpDatabaseSettings.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Repositories/Settings/BotSharpDatabaseSettings.cs @@ -1,4 +1,4 @@ -namespace BotSharp.Abstraction.Repositories; +namespace BotSharp.Abstraction.Repositories.Settings; public class BotSharpDatabaseSettings : DatabaseBasicSettings { diff --git a/src/Infrastructure/BotSharp.Core.Crontab/CrontabPlugin.cs b/src/Infrastructure/BotSharp.Core.Crontab/CrontabPlugin.cs index feef2546..51701968 100644 --- a/src/Infrastructure/BotSharp.Core.Crontab/CrontabPlugin.cs +++ b/src/Infrastructure/BotSharp.Core.Crontab/CrontabPlugin.cs @@ -32,11 +32,22 @@ public class CrontabPlugin : IBotSharpPlugin public void RegisterDI(IServiceCollection services, IConfiguration config) { + var settings = new CrontabSettings(); + config.Bind("Crontab", settings); + services.AddSingleton(settings); + services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddHostedService(); - services.AddHostedService(); + if (settings.Watcher?.Enabled == true) + { + services.AddHostedService(); + } + + if (settings.EventSubscriber?.Enabled == true) + { + services.AddHostedService(); + } } } diff --git a/src/Infrastructure/BotSharp.Core.Crontab/Using.cs b/src/Infrastructure/BotSharp.Core.Crontab/Using.cs index f1b7a838..e1c1bca5 100644 --- a/src/Infrastructure/BotSharp.Core.Crontab/Using.cs +++ b/src/Infrastructure/BotSharp.Core.Crontab/Using.cs @@ -6,6 +6,7 @@ global using Microsoft.Extensions.DependencyInjection; global using BotSharp.Abstraction.Agents.Enums; global using BotSharp.Abstraction.Crontab; global using BotSharp.Abstraction.Crontab.Models; +global using BotSharp.Abstraction.Crontab.Settings; global using BotSharp.Abstraction.Agents; global using BotSharp.Abstraction.Plugins; global using BotSharp.Abstraction.Conversations.Models; diff --git a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.CreateAgent.cs b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.CreateAgent.cs index 5becbec6..fb4d49c4 100644 --- a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.CreateAgent.cs +++ b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.CreateAgent.cs @@ -1,3 +1,4 @@ +using BotSharp.Abstraction.Repositories.Settings; using BotSharp.Abstraction.Tasks.Models; using BotSharp.Abstraction.Users.Enums; using System.IO; diff --git a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.RefreshAgents.cs b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.RefreshAgents.cs index 452f54c9..25f7d2d5 100644 --- a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.RefreshAgents.cs +++ b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.RefreshAgents.cs @@ -1,4 +1,5 @@ using BotSharp.Abstraction.Repositories.Enums; +using BotSharp.Abstraction.Repositories.Settings; using System.IO; namespace BotSharp.Core.Agents.Services; diff --git a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.UpdateAgent.cs b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.UpdateAgent.cs index 8365639b..f2fb4ff7 100644 --- a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.UpdateAgent.cs +++ b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.UpdateAgent.cs @@ -1,4 +1,5 @@ using BotSharp.Abstraction.Repositories.Enums; +using BotSharp.Abstraction.Repositories.Settings; using BotSharp.Abstraction.Users.Enums; using BotSharp.Abstraction.Users.Models; using System.IO; diff --git a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.cs b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.cs index c54c4fd8..ca025447 100644 --- a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.cs +++ b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.cs @@ -1,3 +1,4 @@ +using BotSharp.Abstraction.Repositories.Settings; using System.IO; using System.Reflection; diff --git a/src/Infrastructure/BotSharp.Core/BotSharpCoreExtensions.cs b/src/Infrastructure/BotSharp.Core/BotSharpCoreExtensions.cs index 19b6df53..5c00de0d 100644 --- a/src/Infrastructure/BotSharp.Core/BotSharpCoreExtensions.cs +++ b/src/Infrastructure/BotSharp.Core/BotSharpCoreExtensions.cs @@ -16,6 +16,7 @@ using BotSharp.Abstraction.Templating; using BotSharp.Core.Templating; using BotSharp.Abstraction.Infrastructures.Enums; using BotSharp.Abstraction.Realtime; +using BotSharp.Abstraction.Repositories.Settings; namespace BotSharp.Core; @@ -71,17 +72,6 @@ public static class BotSharpCoreExtensions return services; } - //public static IServiceCollection UsingFileRepository(this IServiceCollection services, IConfiguration config) - //{ - // services.AddScoped(sp => - // { - // var myDatabaseSettings = sp.GetRequiredService(); - // return new FileRepository(myDatabaseSettings, sp); - // }); - - // return services; - //} - public static IApplicationBuilder UseBotSharp(this IApplicationBuilder app) { if (app == null) diff --git a/src/Infrastructure/BotSharp.Core/Files/Services/Storage/LocalFileStorageService.cs b/src/Infrastructure/BotSharp.Core/Files/Services/Storage/LocalFileStorageService.cs index ea2a68ac..6833ffa2 100644 --- a/src/Infrastructure/BotSharp.Core/Files/Services/Storage/LocalFileStorageService.cs +++ b/src/Infrastructure/BotSharp.Core/Files/Services/Storage/LocalFileStorageService.cs @@ -1,3 +1,4 @@ +using BotSharp.Abstraction.Repositories.Settings; using System.IO; namespace BotSharp.Core.Files.Services; diff --git a/src/Infrastructure/BotSharp.Core/Infrastructures/DistributedLocker.cs b/src/Infrastructure/BotSharp.Core/Infrastructures/DistributedLocker.cs index a040525f..30a967e8 100644 --- a/src/Infrastructure/BotSharp.Core/Infrastructures/DistributedLocker.cs +++ b/src/Infrastructure/BotSharp.Core/Infrastructures/DistributedLocker.cs @@ -24,7 +24,9 @@ public class DistributedLocker : IDistributedLocker var redis = _services.GetService(); if (redis == null) { +#if !DEBUG _logger.LogInformation($"The Redis server is experiencing issues and is not functioning as expected."); +#endif await action(); return true; } @@ -50,7 +52,9 @@ public class DistributedLocker : IDistributedLocker var redis = _services.GetRequiredService(); if (redis == null) { +#if !DEBUG _logger.LogWarning($"The Redis server is experiencing issues and is not functioning as expected."); +#endif action(); return false; } diff --git a/src/Infrastructure/BotSharp.Core/Infrastructures/Websocket/AsyncWebsocketDataCollectionResult.cs b/src/Infrastructure/BotSharp.Core/Infrastructures/Websocket/AsyncWebsocketDataCollectionResult.cs index 4f16397e..7364401a 100644 --- a/src/Infrastructure/BotSharp.Core/Infrastructures/Websocket/AsyncWebsocketDataCollectionResult.cs +++ b/src/Infrastructure/BotSharp.Core/Infrastructures/Websocket/AsyncWebsocketDataCollectionResult.cs @@ -1,4 +1,3 @@ -using BotSharp.Abstraction.Realtime.Models.Session; using System.ClientModel; using System.Net.WebSockets; @@ -7,16 +6,16 @@ namespace BotSharp.Core.Infrastructures.Websocket; internal class AsyncWebsocketDataCollectionResult : AsyncCollectionResult { private readonly WebSocket _webSocket; - private readonly ChatSessionOptions? _sessionOptions; + private readonly ChatSessionOptions? _options; private readonly CancellationToken _cancellationToken; public AsyncWebsocketDataCollectionResult( WebSocket webSocket, - ChatSessionOptions? sessionOptions, + ChatSessionOptions? options, CancellationToken cancellationToken) { _webSocket = webSocket; - _sessionOptions = sessionOptions; + _options = options; _cancellationToken = cancellationToken; } @@ -27,7 +26,7 @@ internal class AsyncWebsocketDataCollectionResult : AsyncCollectionResult GetRawPagesAsync() { - await using var enumerator = new AsyncWebsocketDataResultEnumerator(_webSocket, _sessionOptions, _cancellationToken); + await using var enumerator = new AsyncWebsocketDataResultEnumerator(_webSocket, _options, _cancellationToken); while (await enumerator.MoveNextAsync().ConfigureAwait(false)) { yield return enumerator.Current; diff --git a/src/Infrastructure/BotSharp.Core/Infrastructures/Websocket/AsyncWebsocketDataResultEnumerator.cs b/src/Infrastructure/BotSharp.Core/Infrastructures/Websocket/AsyncWebsocketDataResultEnumerator.cs index f89127e2..59eac990 100644 --- a/src/Infrastructure/BotSharp.Core/Infrastructures/Websocket/AsyncWebsocketDataResultEnumerator.cs +++ b/src/Infrastructure/BotSharp.Core/Infrastructures/Websocket/AsyncWebsocketDataResultEnumerator.cs @@ -1,3 +1,4 @@ +using Microsoft.AspNetCore.Builder; using System.Buffers; using System.ClientModel; using System.Net.WebSockets; @@ -7,7 +8,7 @@ namespace BotSharp.Core.Infrastructures.Websocket; internal class AsyncWebsocketDataResultEnumerator : IAsyncEnumerator { private readonly WebSocket _webSocket; - private readonly ChatSessionOptions? _sessionOptions; + private readonly ChatSessionOptions? _options; private readonly CancellationToken _cancellationToken; private readonly byte[] _buffer; @@ -15,13 +16,13 @@ internal class AsyncWebsocketDataResultEnumerator : IAsyncEnumerator 0 ? sessionOptions.BufferSize.Value : DEFAULT_BUFFER_SIZE; + var bufferSize = options?.BufferSize > 0 ? options.BufferSize.Value : DEFAULT_BUFFER_SIZE; _buffer = ArrayPool.Shared.Rent(bufferSize); } @@ -44,7 +45,10 @@ internal class AsyncWebsocketDataResultEnumerator : IAsyncEnumerator Date: Tue, 20 May 2025 13:20:13 -0500 Subject: [PATCH 05/11] fix console log --- src/WebStarter/Program.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/WebStarter/Program.cs b/src/WebStarter/Program.cs index 814aa7e1..2c9c073c 100644 --- a/src/WebStarter/Program.cs +++ b/src/WebStarter/Program.cs @@ -9,7 +9,12 @@ using StackExchange.Redis; var builder = WebApplication.CreateBuilder(args); -builder.Host.UseSerilog(); +Log.Logger = new LoggerConfiguration() + .MinimumLevel.Information() + .WriteTo.Console() + .CreateLogger(); + +builder.Host.UseSerilog(Log.Logger); string[] allowedOrigins = builder.Configuration.GetSection("AllowedOrigins").Get() ?? new[] { @@ -18,8 +23,6 @@ string[] allowedOrigins = builder.Configuration.GetSection("AllowedOrigins").Get "https://chat.scisharpstack.org" }; - - // Add BotSharp builder.Services.AddBotSharpCore(builder.Configuration, options => { From 069170a01cc8798d77db2d40d9b17d9c7a821834 Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Wed, 21 May 2025 17:34:38 -0500 Subject: [PATCH 06/11] add agent mode --- .../BotSharp.Abstraction/Agents/Enums/AgentMode.cs | 7 +++++++ .../BotSharp.Abstraction/Agents/Models/Agent.cs | 6 +++--- .../Agents/Services/AgentService.RefreshAgents.cs | 2 +- .../Agents/Services/AgentService.UpdateAgent.cs | 6 +++--- .../Services/ConversationService.SendMessage.cs | 4 ++-- .../Repository/FileRepository/FileRepository.Agent.cs | 5 +++-- .../BotSharp.Core/Routing/RoutingContext.cs | 4 ++-- .../ViewModels/Agents/Request/AgentCreationModel.cs | 8 +++++--- .../ViewModels/Agents/Request/AgentUpdateModel.cs | 7 +++++-- 9 files changed, 31 insertions(+), 18 deletions(-) create mode 100644 src/Infrastructure/BotSharp.Abstraction/Agents/Enums/AgentMode.cs diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/Enums/AgentMode.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/Enums/AgentMode.cs new file mode 100644 index 00000000..4e2cd60c --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/Enums/AgentMode.cs @@ -0,0 +1,7 @@ +namespace BotSharp.Abstraction.Agents.Enums; + +public class AgentMode +{ + public const string Eager = "eager"; + public const string Lazy = "lazy"; +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/Agent.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/Agent.cs index 1ba644e8..4562f16a 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/Agent.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/Agent.cs @@ -17,7 +17,7 @@ public class Agent /// /// Routing Mode: lazy or eager /// - public string Mode { get; set; } = "eager"; + public string Mode { get; set; } = AgentMode.Eager; public DateTime CreatedDateTime { get; set; } public DateTime UpdatedDateTime { get; set; } @@ -277,7 +277,7 @@ public class Agent return this; } - public Agent SetAgentType(string type) + public Agent SetType(string type) { Type = type; return this; @@ -288,7 +288,7 @@ public class Agent /// /// /// - public Agent SetAgentMode(string mode) + public Agent SetMode(string mode) { Mode = mode; return this; diff --git a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.RefreshAgents.cs b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.RefreshAgents.cs index 25f7d2d5..9603d1e5 100644 --- a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.RefreshAgents.cs +++ b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.RefreshAgents.cs @@ -67,7 +67,7 @@ public partial class AgentService } catch (Exception ex) { - _logger.LogError($"Failed to migrate agent in file directory: {dir}\r\nError: {ex.Message}"); + _logger.LogError(ex, $"Failed to migrate agent in file directory: {dir}\r\nError: {ex.Message}"); } } diff --git a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.UpdateAgent.cs b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.UpdateAgent.cs index f2fb4ff7..50dbcad4 100644 --- a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.UpdateAgent.cs +++ b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.UpdateAgent.cs @@ -94,12 +94,12 @@ public partial class AgentService { clonedAgent.SetId(foundAgent.Id) .SetName(foundAgent.Name) - .SetDescription(foundAgent.Description) + .SetType(foundAgent.Type) + .SetMode(foundAgent.Mode) .SetIsPublic(foundAgent.IsPublic) .SetDisabled(foundAgent.Disabled) + .SetDescription(foundAgent.Description) .SetMergeUtility(foundAgent.MergeUtility) - .SetAgentType(foundAgent.Type) - .SetAgentMode(foundAgent.Mode) .SetProfiles(foundAgent.Profiles) .SetLabels(foundAgent.Labels) .SetRoutingRules(foundAgent.RoutingRules) diff --git a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.SendMessage.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.SendMessage.cs index 1227d3dc..42e0de2c 100644 --- a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.SendMessage.cs +++ b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.SendMessage.cs @@ -83,10 +83,10 @@ public partial class ConversationService { // Check the routing mode var states = _services.GetRequiredService(); - var routingMode = states.GetState(StateConst.ROUTING_MODE, "eager"); + var routingMode = states.GetState(StateConst.ROUTING_MODE, AgentMode.Eager); routing.Context.Push(agent.Id, reason: "request started", updateLazyRouting: false); - if (routingMode == "lazy") + if (routingMode == AgentMode.Lazy) { message.CurrentAgentId = states.GetState(StateConst.LAZY_ROUTING_AGENT_ID, message.CurrentAgentId); routing.Context.Push(message.CurrentAgentId, reason: "lazy routing", updateLazyRouting: false); diff --git a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Agent.cs b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Agent.cs index 930eb13d..5494f972 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Agent.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Agent.cs @@ -383,11 +383,12 @@ namespace BotSharp.Core.Repository if (agent == null) return; agent.Name = inputAgent.Name; - agent.Description = inputAgent.Description; + agent.Type = inputAgent.Type; + agent.Mode = inputAgent.Mode; agent.IsPublic = inputAgent.IsPublic; agent.Disabled = inputAgent.Disabled; + agent.Description = inputAgent.Description; agent.MergeUtility = inputAgent.MergeUtility; - agent.Type = inputAgent.Type; agent.Profiles = inputAgent.Profiles; agent.Labels = inputAgent.Labels; agent.Utilities = inputAgent.Utilities; diff --git a/src/Infrastructure/BotSharp.Core/Routing/RoutingContext.cs b/src/Infrastructure/BotSharp.Core/Routing/RoutingContext.cs index b422a423..5a5a02da 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/RoutingContext.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/RoutingContext.cs @@ -290,8 +290,8 @@ public class RoutingContext : IRoutingContext // Set next handling agent for lazy routing mode var states = _services.GetRequiredService(); - var routingMode = states.GetState(StateConst.ROUTING_MODE, "eager"); - if (routingMode == "lazy") + var routingMode = states.GetState(StateConst.ROUTING_MODE, AgentMode.Eager); + if (routingMode == AgentMode.Lazy) { var agentId = GetCurrentAgentId(); if (agentId != BuiltInAgentId.Fallback) diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/Request/AgentCreationModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/Request/AgentCreationModel.cs index 8265ebe4..2fd2c429 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/Request/AgentCreationModel.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/Request/AgentCreationModel.cs @@ -8,6 +8,7 @@ public class AgentCreationModel public string Name { get; set; } public string Description { get; set; } public string Type { get; set; } = AgentType.Task; + public string Mode { get; set; } = AgentMode.Eager; /// /// LLM default system instructions @@ -66,6 +67,10 @@ public class AgentCreationModel return new Agent { Name = Name, + Type = Type, + Mode = Mode, + Disabled = Disabled, + IsPublic = IsPublic, Description = Description, Instruction = Instruction, ChannelInstructions = ChannelInstructions, @@ -75,9 +80,6 @@ public class AgentCreationModel Samples = Samples, Utilities = Utilities, McpTools = McpTools, - IsPublic = IsPublic, - Type = Type, - Disabled = Disabled, MergeUtility = MergeUtility, MaxMessageCount = MaxMessageCount, Profiles = Profiles, diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/Request/AgentUpdateModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/Request/AgentUpdateModel.cs index b11a9db0..da0c8b3a 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/Request/AgentUpdateModel.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/Request/AgentUpdateModel.cs @@ -9,6 +9,8 @@ public class AgentUpdateModel public string Name { get; set; } = string.Empty; public string Description { get; set; } = string.Empty; public string Type { get; set; } = AgentType.Task; + public string Mode { get; set; } = AgentMode.Eager; + /// /// Instruction /// @@ -93,12 +95,13 @@ public class AgentUpdateModel var agent = new Agent() { Name = Name ?? string.Empty, - Description = Description ?? string.Empty, + Type = Type, + Mode = Mode, IsPublic = IsPublic, Disabled = Disabled, + Description = Description ?? string.Empty, MergeUtility = MergeUtility, MaxMessageCount = MaxMessageCount, - Type = Type, Profiles = Profiles ?? [], Labels = Labels ?? [], RoutingRules = RoutingRules?.Select(x => RoutingRuleUpdateModel.ToDomainElement(x))?.ToList() ?? [], From 2bf15f50784f13ea8d362123ea90f9841051115c Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Thu, 22 May 2025 13:58:42 -0500 Subject: [PATCH 07/11] refine routing mode --- .../BotSharp.Abstraction/Agents/Enums/AgentField.cs | 2 +- .../BotSharp.Abstraction/Agents/Models/Agent.cs | 4 +++- .../Enums/AgentMode.cs => Routing/Enums/RoutingMode.cs} | 4 ++-- .../Services/ConversationService.SendMessage.cs | 5 +++-- .../Repository/FileRepository/FileRepository.Agent.cs | 6 +++--- src/Infrastructure/BotSharp.Core/Routing/RoutingContext.cs | 5 +++-- .../ViewModels/Agents/Request/AgentCreationModel.cs | 7 ++++++- .../ViewModels/Agents/Request/AgentUpdateModel.cs | 6 +++++- .../ViewModels/Agents/View/AgentViewModel.cs | 4 +++- .../Collections/AgentDocument.cs | 2 +- .../Repository/MongoRepository.Agent.cs | 6 +++--- 11 files changed, 33 insertions(+), 18 deletions(-) rename src/Infrastructure/BotSharp.Abstraction/{Agents/Enums/AgentMode.cs => Routing/Enums/RoutingMode.cs} (54%) diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/Enums/AgentField.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/Enums/AgentField.cs index 0cab0dfb..0617a9de 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/Enums/AgentField.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/Enums/AgentField.cs @@ -8,7 +8,7 @@ public enum AgentField IsPublic, Disabled, Type, - Mode, + RoutingMode, InheritAgentId, Profile, Label, diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/Agent.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/Agent.cs index 4562f16a..aa872566 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/Agent.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/Agent.cs @@ -1,5 +1,6 @@ using BotSharp.Abstraction.Functions.Models; using BotSharp.Abstraction.Plugins.Models; +using BotSharp.Abstraction.Routing.Enums; using BotSharp.Abstraction.Tasks.Models; namespace BotSharp.Abstraction.Agents.Models; @@ -17,7 +18,8 @@ public class Agent /// /// Routing Mode: lazy or eager /// - public string Mode { get; set; } = AgentMode.Eager; + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Mode { get; set; } public DateTime CreatedDateTime { get; set; } public DateTime UpdatedDateTime { get; set; } diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/Enums/AgentMode.cs b/src/Infrastructure/BotSharp.Abstraction/Routing/Enums/RoutingMode.cs similarity index 54% rename from src/Infrastructure/BotSharp.Abstraction/Agents/Enums/AgentMode.cs rename to src/Infrastructure/BotSharp.Abstraction/Routing/Enums/RoutingMode.cs index 4e2cd60c..8f946d01 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/Enums/AgentMode.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Routing/Enums/RoutingMode.cs @@ -1,6 +1,6 @@ -namespace BotSharp.Abstraction.Agents.Enums; +namespace BotSharp.Abstraction.Routing.Enums; -public class AgentMode +public class RoutingMode { public const string Eager = "eager"; public const string Lazy = "lazy"; diff --git a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.SendMessage.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.SendMessage.cs index 42e0de2c..a28ccbc7 100644 --- a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.SendMessage.cs +++ b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.SendMessage.cs @@ -2,6 +2,7 @@ using BotSharp.Abstraction.Hooks; using BotSharp.Abstraction.Infrastructures.Enums; using BotSharp.Abstraction.Messaging; using BotSharp.Abstraction.Messaging.Models.RichContent; +using BotSharp.Abstraction.Routing.Enums; using BotSharp.Abstraction.Routing.Settings; namespace BotSharp.Core.Conversations.Services; @@ -83,10 +84,10 @@ public partial class ConversationService { // Check the routing mode var states = _services.GetRequiredService(); - var routingMode = states.GetState(StateConst.ROUTING_MODE, AgentMode.Eager); + var routingMode = states.GetState(StateConst.ROUTING_MODE, RoutingMode.Eager); routing.Context.Push(agent.Id, reason: "request started", updateLazyRouting: false); - if (routingMode == AgentMode.Lazy) + if (routingMode == RoutingMode.Lazy) { message.CurrentAgentId = states.GetState(StateConst.LAZY_ROUTING_AGENT_ID, message.CurrentAgentId); routing.Context.Push(message.CurrentAgentId, reason: "lazy routing", updateLazyRouting: false); diff --git a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Agent.cs b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Agent.cs index 5494f972..f7e70ca7 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Agent.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Agent.cs @@ -27,8 +27,8 @@ namespace BotSharp.Core.Repository case AgentField.Type: UpdateAgentType(agent.Id, agent.Type); break; - case AgentField.Mode: - UpdateAgentMode(agent.Id, agent.Mode); + case AgentField.RoutingMode: + UpdateAgentRoutingMode(agent.Id, agent.Mode); break; case AgentField.InheritAgentId: UpdateAgentInheritAgentId(agent.Id, agent.InheritAgentId); @@ -145,7 +145,7 @@ namespace BotSharp.Core.Repository File.WriteAllText(agentFile, json); } - private void UpdateAgentMode(string agentId, string mode) + private void UpdateAgentRoutingMode(string agentId, string? mode) { var (agent, agentFile) = GetAgentFromFile(agentId); if (agent == null) return; diff --git a/src/Infrastructure/BotSharp.Core/Routing/RoutingContext.cs b/src/Infrastructure/BotSharp.Core/Routing/RoutingContext.cs index 5a5a02da..929289aa 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/RoutingContext.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/RoutingContext.cs @@ -1,4 +1,5 @@ using BotSharp.Abstraction.Infrastructures.Enums; +using BotSharp.Abstraction.Routing.Enums; using BotSharp.Abstraction.Routing.Settings; namespace BotSharp.Core.Routing; @@ -290,8 +291,8 @@ public class RoutingContext : IRoutingContext // Set next handling agent for lazy routing mode var states = _services.GetRequiredService(); - var routingMode = states.GetState(StateConst.ROUTING_MODE, AgentMode.Eager); - if (routingMode == AgentMode.Lazy) + var routingMode = states.GetState(StateConst.ROUTING_MODE, RoutingMode.Eager); + if (routingMode == RoutingMode.Lazy) { var agentId = GetCurrentAgentId(); if (agentId != BuiltInAgentId.Fallback) diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/Request/AgentCreationModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/Request/AgentCreationModel.cs index 2fd2c429..33553e48 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/Request/AgentCreationModel.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/Request/AgentCreationModel.cs @@ -1,5 +1,6 @@ using BotSharp.Abstraction.Agents.Models; using BotSharp.Abstraction.Functions.Models; +using BotSharp.Abstraction.Routing.Enums; namespace BotSharp.OpenAPI.ViewModels.Agents; @@ -8,7 +9,11 @@ public class AgentCreationModel public string Name { get; set; } public string Description { get; set; } public string Type { get; set; } = AgentType.Task; - public string Mode { get; set; } = AgentMode.Eager; + + /// + /// Agent routing mode + /// + public string? Mode { get; set; } /// /// LLM default system instructions diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/Request/AgentUpdateModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/Request/AgentUpdateModel.cs index da0c8b3a..3b69055f 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/Request/AgentUpdateModel.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/Request/AgentUpdateModel.cs @@ -9,7 +9,11 @@ public class AgentUpdateModel public string Name { get; set; } = string.Empty; public string Description { get; set; } = string.Empty; public string Type { get; set; } = AgentType.Task; - public string Mode { get; set; } = AgentMode.Eager; + + /// + /// Agent routing mode + /// + public string? Mode { get; set; } /// /// Instruction diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/View/AgentViewModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/View/AgentViewModel.cs index 4fede545..728a73e6 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/View/AgentViewModel.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/View/AgentViewModel.cs @@ -12,7 +12,9 @@ public class AgentViewModel public string Name { get; set; } public string Description { get; set; } public string Type { get; set; } = AgentType.Task; - public string Mode { get; set; } = null!; + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Mode { get; set; } public string Instruction { get; set; } [JsonPropertyName("channel_instructions")] diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Collections/AgentDocument.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Collections/AgentDocument.cs index 7ebded25..767a054a 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Collections/AgentDocument.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Collections/AgentDocument.cs @@ -5,7 +5,7 @@ public class AgentDocument : MongoBase public string Name { get; set; } = default!; public string Description { get; set; } = default!; public string Type { get; set; } = default!; - public string Mode { get; set; } = default!; + public string? Mode { get; set; } public string? InheritAgentId { get; set; } public string? IconUrl { get; set; } public string Instruction { get; set; } = default!; diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Agent.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Agent.cs index 6eda94ed..5e0e741a 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Agent.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Agent.cs @@ -28,8 +28,8 @@ public partial class MongoRepository case AgentField.Type: UpdateAgentType(agent.Id, agent.Type); break; - case AgentField.Mode: - UpdateAgentMode(agent.Id, agent.Mode); + case AgentField.RoutingMode: + UpdateAgentRoutingMode(agent.Id, agent.Mode); break; case AgentField.InheritAgentId: UpdateAgentInheritAgentId(agent.Id, agent.InheritAgentId); @@ -139,7 +139,7 @@ public partial class MongoRepository _dc.Agents.UpdateOne(filter, update); } - private void UpdateAgentMode(string agentId, string mode) + private void UpdateAgentRoutingMode(string agentId, string? mode) { var filter = Builders.Filter.Eq(x => x.Id, agentId); var update = Builders.Update From 1cd8ab9b03f0be28ffc20bdb439fe9719cb1666c Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Thu, 22 May 2025 14:02:39 -0500 Subject: [PATCH 08/11] minor change --- .../BotSharp.Abstraction/Agents/Models/Agent.cs | 6 +++--- .../Agents/Services/AgentService.UpdateAgent.cs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/Agent.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/Agent.cs index aa872566..431a78c5 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/Agent.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/Agent.cs @@ -1,6 +1,5 @@ using BotSharp.Abstraction.Functions.Models; using BotSharp.Abstraction.Plugins.Models; -using BotSharp.Abstraction.Routing.Enums; using BotSharp.Abstraction.Tasks.Models; namespace BotSharp.Abstraction.Agents.Models; @@ -10,6 +9,7 @@ public class Agent public string Id { get; set; } = string.Empty; public string Name { get; set; } = string.Empty; public string Description { get; set; } = string.Empty; + /// /// Agent Type /// @@ -286,11 +286,11 @@ public class Agent } /// - /// Set agent mode: lazy or eager + /// Set agent routing mode: lazy or eager /// /// /// - public Agent SetMode(string mode) + public Agent SetRoutingMode(string? mode) { Mode = mode; return this; diff --git a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.UpdateAgent.cs b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.UpdateAgent.cs index 50dbcad4..41174886 100644 --- a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.UpdateAgent.cs +++ b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.UpdateAgent.cs @@ -95,7 +95,7 @@ public partial class AgentService clonedAgent.SetId(foundAgent.Id) .SetName(foundAgent.Name) .SetType(foundAgent.Type) - .SetMode(foundAgent.Mode) + .SetRoutingMode(foundAgent.Mode) .SetIsPublic(foundAgent.IsPublic) .SetDisabled(foundAgent.Disabled) .SetDescription(foundAgent.Description) From 6def0031415f53b6517f5ad7991b2842f0646af4 Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Thu, 22 May 2025 14:59:35 -0500 Subject: [PATCH 09/11] trigger agent instruction logging in realtime --- .../Providers/Realtime/RealTimeCompletionProvider.cs | 2 +- .../Providers/Realtime/RealTimeCompletionProvider.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Plugins/BotSharp.Plugin.GoogleAI/Providers/Realtime/RealTimeCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.GoogleAI/Providers/Realtime/RealTimeCompletionProvider.cs index d2084801..2e95cfa2 100644 --- a/src/Plugins/BotSharp.Plugin.GoogleAI/Providers/Realtime/RealTimeCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.GoogleAI/Providers/Realtime/RealTimeCompletionProvider.cs @@ -343,7 +343,7 @@ public class GoogleRealTimeProvider : IRealTimeCompletion }).ToArray(); await HookEmitter.Emit(_services, - async hook => { await hook.OnSessionUpdated(agent, prompt, functions, isInit); }, agent.Id); + async hook => { await hook.OnSessionUpdated(agent, prompt, functions, isInit: false); }, agent.Id); if (_settings.Gemini.UseGoogleSearch) { diff --git a/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Realtime/RealTimeCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Realtime/RealTimeCompletionProvider.cs index 4e85be76..fd458744 100644 --- a/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Realtime/RealTimeCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Realtime/RealTimeCompletionProvider.cs @@ -381,7 +381,7 @@ public class RealTimeCompletionProvider : IRealTimeCompletion await HookEmitter.Emit(_services, async hook => { - await hook.OnSessionUpdated(agent, instruction, functions, isInit); + await hook.OnSessionUpdated(agent, instruction, functions, isInit: false); }, agent.Id); await SendEventToModel(sessionUpdate); From b52c2405937a695e67f0e8c6446c4e69b7e50653 Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Thu, 22 May 2025 16:10:09 -0500 Subject: [PATCH 10/11] fix user update --- .../Repository/FileRepository/FileRepository.User.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.User.cs b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.User.cs index c8f1fbd6..7bd5a1a9 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.User.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.User.cs @@ -263,8 +263,15 @@ public partial class FileRepository } var userFile = Path.Combine(dir, USER_FILE); - user.UpdatedTime = DateTime.UtcNow; - File.WriteAllText(userFile, JsonSerializer.Serialize(user, _options)); + var userJson = File.ReadAllText(userFile); + var curUser = JsonSerializer.Deserialize(userJson, _options); + if (curUser == null) return false; + + curUser.Type = user.Type; + curUser.Role = user.Role; + curUser.Permissions = user.Permissions; + curUser.UpdatedTime = DateTime.UtcNow; + File.WriteAllText(userFile, JsonSerializer.Serialize(curUser, _options)); if (updateUserAgents) { From ba0b486d5a4918baeac8db595a9171a086133947 Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Thu, 22 May 2025 16:57:27 -0500 Subject: [PATCH 11/11] minor change --- .../Infrastructures/Attributes/BotSharpAuthAttribute.cs | 2 +- .../BotSharp.Plugin.OpenAI/BotSharp.Plugin.OpenAI.csproj | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Infrastructure/BotSharp.Abstraction/Infrastructures/Attributes/BotSharpAuthAttribute.cs b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/Attributes/BotSharpAuthAttribute.cs index 25193cb3..46f28d6c 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Infrastructures/Attributes/BotSharpAuthAttribute.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/Attributes/BotSharpAuthAttribute.cs @@ -26,7 +26,7 @@ public class BotSharpAuthAttribute : Attribute, IAsyncAuthorizationFilter var (isAdmin, user) = await userService.IsAdminUser(userIdentity.Id); if (!isAdmin || user == null) { - context.Result = new BadRequestResult(); + context.Result = new UnauthorizedResult(); } } } diff --git a/src/Plugins/BotSharp.Plugin.OpenAI/BotSharp.Plugin.OpenAI.csproj b/src/Plugins/BotSharp.Plugin.OpenAI/BotSharp.Plugin.OpenAI.csproj index e54033b6..2455e7b5 100644 --- a/src/Plugins/BotSharp.Plugin.OpenAI/BotSharp.Plugin.OpenAI.csproj +++ b/src/Plugins/BotSharp.Plugin.OpenAI/BotSharp.Plugin.OpenAI.csproj @@ -10,10 +10,6 @@ $(SolutionDir)packages - - - -