diff --git a/Directory.Packages.props b/Directory.Packages.props index 4683fc9d..aa76cf61 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -54,7 +54,7 @@ - + diff --git a/src/Infrastructure/BotSharp.Abstraction/Conversations/ConversationHookBase.cs b/src/Infrastructure/BotSharp.Abstraction/Conversations/ConversationHookBase.cs index da87646e..a84c8693 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Conversations/ConversationHookBase.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Conversations/ConversationHookBase.cs @@ -78,4 +78,7 @@ public abstract class ConversationHookBase : IConversationHook public virtual Task OnNotificationGenerated(RoleDialogModel message) => Task.CompletedTask; + + public virtual Task OnUserDisconnected(Conversation conversation) + => Task.CompletedTask; } diff --git a/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationHook.cs b/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationHook.cs index c764f391..f99078ea 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationHook.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationHook.cs @@ -25,6 +25,13 @@ public interface IConversationHook /// Task OnUserAgentConnectedInitially(Conversation conversation); + /// + /// Triggered when user disconnects with agent. + /// + /// + /// + Task OnUserDisconnected(Conversation conversation); + /// /// Triggered once for every new conversation. /// diff --git a/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationService.cs b/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationService.cs index 44ceebf1..15f7e89e 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationService.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationService.cs @@ -13,7 +13,7 @@ public interface IConversationService Task> GetConversations(ConversationFilter filter); Task UpdateConversationTitle(string id, string title); Task UpdateConversationTitleAlias(string id, string titleAlias); - Task UpdateConversationTags(string conversationId, List tags); + Task UpdateConversationTags(string conversationId, List toAddTags, List toDeleteTags); Task UpdateConversationMessage(string conversationId, UpdateMessageRequest request); Task> GetLastConversations(); Task> GetIdleConversations(int batchSize, int messageLimit, int bufferHours, IEnumerable excludeAgentIds); diff --git a/src/Infrastructure/BotSharp.Abstraction/Files/Utilities/FileUtility.cs b/src/Infrastructure/BotSharp.Abstraction/Files/Utilities/FileUtility.cs index bf2644dd..db9ccb65 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Files/Utilities/FileUtility.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Files/Utilities/FileUtility.cs @@ -47,6 +47,17 @@ public static class FileUtility return $"data:{contentType};base64,{base64}"; } + public static BinaryData BuildBinaryDataFromFile(IFormFile file) + { + using var stream = new MemoryStream(); + file.CopyTo(stream); + stream.Position = 0; + var binary = BinaryData.FromStream(stream); + stream.Close(); + + return binary; + } + public static string GetFileContentType(string fileName) { string contentType; diff --git a/src/Infrastructure/BotSharp.Abstraction/Knowledges/IKnowledgeService.cs b/src/Infrastructure/BotSharp.Abstraction/Knowledges/IKnowledgeService.cs index 3fc90816..fb368b0b 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Knowledges/IKnowledgeService.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Knowledges/IKnowledgeService.cs @@ -60,6 +60,14 @@ public interface IKnowledgeService Task GetKnowledgeDocumentBinaryData(string collectionName, Guid fileId); #endregion + #region Snapshot + Task> GetVectorCollectionSnapshots(string collectionName); + Task CreateVectorCollectionSnapshot(string collectionName); + Task DownloadVectorCollectionSnapshot(string collectionName, string snapshotFileName); + Task RecoverVectorCollectionFromSnapshot(string collectionName, string snapshotFileName, BinaryData snapshotData); + Task DeleteVectorCollectionSnapshot(string collectionName, string snapshotName); + #endregion + #region Common Task RefreshVectorKnowledgeConfigs(VectorCollectionConfigsModel configs); #endregion diff --git a/src/Infrastructure/BotSharp.Abstraction/Loggers/Services/ILoggerService.cs b/src/Infrastructure/BotSharp.Abstraction/Loggers/Services/ILoggerService.cs index d704b154..564fcf37 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Loggers/Services/ILoggerService.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Loggers/Services/ILoggerService.cs @@ -6,8 +6,8 @@ namespace BotSharp.Abstraction.Loggers.Services; public interface ILoggerService { #region Conversation - Task> GetConversationContentLogs(string conversationId); - Task> GetConversationStateLogs(string conversationId); + Task> GetConversationContentLogs(string conversationId, ConversationLogFilter filter); + Task> GetConversationStateLogs(string conversationId, ConversationLogFilter filter); #endregion #region Instruction diff --git a/src/Infrastructure/BotSharp.Abstraction/Realtime/IRealtimeHook.cs b/src/Infrastructure/BotSharp.Abstraction/Realtime/IRealtimeHook.cs new file mode 100644 index 00000000..2ad8f1df --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Realtime/IRealtimeHook.cs @@ -0,0 +1,6 @@ +namespace BotSharp.Abstraction.Realtime; + +public interface IRealtimeHook +{ + string[] OnModelTranscriptPrompt(Agent agent); +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Repositories/Filters/ConversationLogFilter.cs b/src/Infrastructure/BotSharp.Abstraction/Repositories/Filters/ConversationLogFilter.cs new file mode 100644 index 00000000..59cf9d27 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Repositories/Filters/ConversationLogFilter.cs @@ -0,0 +1,17 @@ +namespace BotSharp.Abstraction.Repositories.Filters; + +public class ConversationLogFilter +{ + public int Size { get; set; } = 20; + public DateTime StartTime { get; set; } = DateTime.UtcNow; + + public ConversationLogFilter() + { + + } + + public static ConversationLogFilter Empty() + { + return new(); + } +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs b/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs index 5b92d322..332ca340 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs @@ -132,7 +132,7 @@ public interface IBotSharpRepository : IHaveServiceProvider => throw new NotImplementedException(); void UpdateConversationTitleAlias(string conversationId, string titleAlias) => throw new NotImplementedException(); - bool UpdateConversationTags(string conversationId, List tags) + bool UpdateConversationTags(string conversationId, List toAddTags, List toDeleteTags) => throw new NotImplementedException(); bool AppendConversationTags(string conversationId, List tags) => throw new NotImplementedException(); @@ -164,14 +164,14 @@ public interface IBotSharpRepository : IHaveServiceProvider #region Conversation Content Log void SaveConversationContentLog(ContentLogOutputModel log) => throw new NotImplementedException(); - List GetConversationContentLogs(string conversationId) + DateTimePagination GetConversationContentLogs(string conversationId, ConversationLogFilter filter) => throw new NotImplementedException(); #endregion #region Conversation State Log void SaveConversationStateLog(ConversationStateLogModel log) => throw new NotImplementedException(); - List GetConversationStateLogs(string conversationId) + DateTimePagination GetConversationStateLogs(string conversationId, ConversationLogFilter filter) => throw new NotImplementedException(); #endregion diff --git a/src/Infrastructure/BotSharp.Abstraction/Utilities/DateTimePagination.cs b/src/Infrastructure/BotSharp.Abstraction/Utilities/DateTimePagination.cs new file mode 100644 index 00000000..fc56e16e --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Utilities/DateTimePagination.cs @@ -0,0 +1,6 @@ +namespace BotSharp.Abstraction.Utilities; + +public class DateTimePagination : PagedItems +{ + public DateTime? NextTime { get; set; } +} diff --git a/src/Infrastructure/BotSharp.Abstraction/VectorStorage/IVectorDb.cs b/src/Infrastructure/BotSharp.Abstraction/VectorStorage/IVectorDb.cs index 66f7727a..ff090da6 100644 --- a/src/Infrastructure/BotSharp.Abstraction/VectorStorage/IVectorDb.cs +++ b/src/Infrastructure/BotSharp.Abstraction/VectorStorage/IVectorDb.cs @@ -6,14 +6,36 @@ public interface IVectorDb { string Provider { get; } - Task DoesCollectionExist(string collectionName); - Task> GetCollections(); - Task> GetPagedCollectionData(string collectionName, VectorFilter filter); - Task> GetCollectionData(string collectionName, IEnumerable ids, bool withPayload = false, bool withVector = false); - Task CreateCollection(string collectionName, int dimension); - Task DeleteCollection(string collectionName); - Task Upsert(string collectionName, Guid id, float[] vector, string text, Dictionary? payload = null); - Task> Search(string collectionName, float[] vector, IEnumerable? fields, int limit = 5, float confidence = 0.5f, bool withVector = false); - Task DeleteCollectionData(string collectionName, List ids); - Task DeleteCollectionAllData(string collectionName); + Task DoesCollectionExist(string collectionName) + => throw new NotImplementedException(); + Task> GetCollections() + => throw new NotImplementedException(); + Task> GetPagedCollectionData(string collectionName, VectorFilter filter) + => throw new NotImplementedException(); + Task> GetCollectionData(string collectionName, IEnumerable ids, + bool withPayload = false, bool withVector = false) + => throw new NotImplementedException(); + Task CreateCollection(string collectionName, int dimension) + => throw new NotImplementedException(); + Task DeleteCollection(string collectionName) + => throw new NotImplementedException(); + Task Upsert(string collectionName, Guid id, float[] vector, string text, Dictionary? payload = null) + => throw new NotImplementedException(); + Task> Search(string collectionName, float[] vector, IEnumerable? fields, + int limit = 5, float confidence = 0.5f, bool withVector = false) + => throw new NotImplementedException(); + Task DeleteCollectionData(string collectionName, List ids) + => throw new NotImplementedException(); + Task DeleteCollectionAllData(string collectionName) + => throw new NotImplementedException(); + Task> GetCollectionSnapshots(string collectionName) + => throw new NotImplementedException(); + Task CreateCollectionShapshot(string collectionName) + => throw new NotImplementedException(); + Task DownloadCollectionSnapshot(string collectionName, string snapshotFileName) + => throw new NotImplementedException(); + Task RecoverCollectionFromShapshot(string collectionName, string snapshotFileName, BinaryData snapshotData) + => throw new NotImplementedException(); + Task DeleteCollectionShapshot(string collectionName, string snapshotName) + => throw new NotImplementedException(); } diff --git a/src/Infrastructure/BotSharp.Abstraction/VectorStorage/Models/Snapshot/VectorCollectionSnapshot.cs b/src/Infrastructure/BotSharp.Abstraction/VectorStorage/Models/Snapshot/VectorCollectionSnapshot.cs new file mode 100644 index 00000000..a8405a80 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/VectorStorage/Models/Snapshot/VectorCollectionSnapshot.cs @@ -0,0 +1,9 @@ +namespace BotSharp.Abstraction.VectorStorage.Models; + +public class VectorCollectionSnapshot +{ + public string Name { get; set; } = default!; + public long Size { get; set; } + public DateTime CreatedTime { get; set; } + public string? CheckSum { get; set; } +} \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Core.Realtime/BotSharp.Core.Realtime.csproj b/src/Infrastructure/BotSharp.Core.Realtime/BotSharp.Core.Realtime.csproj index b29ff39e..c004dc50 100644 --- a/src/Infrastructure/BotSharp.Core.Realtime/BotSharp.Core.Realtime.csproj +++ b/src/Infrastructure/BotSharp.Core.Realtime/BotSharp.Core.Realtime.csproj @@ -8,6 +8,7 @@ + diff --git a/src/Infrastructure/BotSharp.Core.Realtime/Services/RealtimeHub.cs b/src/Infrastructure/BotSharp.Core.Realtime/Services/RealtimeHub.cs index 68636bd1..76302262 100644 --- a/src/Infrastructure/BotSharp.Core.Realtime/Services/RealtimeHub.cs +++ b/src/Infrastructure/BotSharp.Core.Realtime/Services/RealtimeHub.cs @@ -1,4 +1,6 @@ using BotSharp.Abstraction.Utilities; +using BotSharp.Core.Infrastructures; +using Microsoft.AspNetCore.Cors.Infrastructure; namespace BotSharp.Core.Realtime.Services; @@ -24,7 +26,6 @@ public class RealtimeHub : IRealtimeHub { var buffer = new byte[1024 * 16]; WebSocketReceiveResult result; - do @@ -86,31 +87,43 @@ public class RealtimeHub : IRealtimeHub var routing = _services.GetRequiredService(); routing.Context.Push(agent.Id); + var storage = _services.GetRequiredService(); var dialogs = convService.GetDialogHistory(); if (dialogs.Count == 0) { dialogs.Add(new RoleDialogModel(AgentRole.User, "Hi")); + storage.Append(_conn.ConversationId, dialogs.First()); } + routing.Context.SetDialogs(dialogs); + var states = _services.GetRequiredService(); + await _completer.Connect(_conn, onModelReady: async () => { - // Control initial session, prevent initial response interruption - await _completer.UpdateSession(_conn, turnDetection: false); - - if (dialogs.LastOrDefault()?.Role == AgentRole.Assistant) + if (states.ContainsState("init_audio_file")) { - await _completer.TriggerModelInference($"Rephase your last response:\r\n{dialogs.LastOrDefault()?.Content}"); + await _completer.UpdateSession(_conn, turnDetection: true); } else { - await _completer.TriggerModelInference("Reply based on the conversation context."); - } + // Control initial session, prevent initial response interruption + await _completer.UpdateSession(_conn, turnDetection: false); - // Start turn detection - await Task.Delay(1000 * 8); - await _completer.UpdateSession(_conn, turnDetection: true); + if (dialogs.LastOrDefault()?.Role == AgentRole.Assistant) + { + await _completer.TriggerModelInference($"Rephase your last response:\r\n{dialogs.LastOrDefault()?.Content}"); + } + else + { + await _completer.TriggerModelInference("Reply based on the conversation context."); + } + + // Start turn detection + await Task.Delay(1000 * 8); + await _completer.UpdateSession(_conn, turnDetection: true); + } }, onModelAudioDeltaReceived: async (audioDeltaData, itemId) => { @@ -155,6 +168,7 @@ public class RealtimeHub : IRealtimeHub { // append output audio transcript to conversation dialogs.Add(message); + storage.Append(_conn.ConversationId, message); foreach (var hook in hookProvider.HooksOrderByPriority) { @@ -174,6 +188,7 @@ public class RealtimeHub : IRealtimeHub { // append input audio transcript to conversation dialogs.Add(message); + storage.Append(_conn.ConversationId, message); foreach (var hook in hookProvider.HooksOrderByPriority) { @@ -224,6 +239,9 @@ public class RealtimeHub : IRealtimeHub }; dialogs.Add(message); + var storage = _services.GetRequiredService(); + storage.Append(_conn.ConversationId, message); + foreach (var hook in hookProvider.HooksOrderByPriority) { hook.SetAgent(agent) @@ -233,19 +251,15 @@ public class RealtimeHub : IRealtimeHub } await _completer.InsertConversationItem(message); - await _completer.TriggerModelInference("Reply based on the user input"); + var instruction = await _completer.UpdateSession(_conn); + await _completer.TriggerModelInference($"{instruction}\r\n\r\nReply based on the user input: {message.Content}"); } private async Task HandleUserDisconnected() { - // Save dialog history - var routing = _services.GetRequiredService(); - var storage = _services.GetRequiredService(); - var dialogs = routing.Context.GetDialogs(); - foreach (var item in dialogs) - { - storage.Append(_conn.ConversationId, item); - } + var convService = _services.GetRequiredService(); + var conversation = await convService.GetConversation(_conn.ConversationId); + await HookEmitter.Emit(_services, x => x.OnUserDisconnected(conversation)); } private async Task SendEventToUser(WebSocket webSocket, object message) diff --git a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.cs index a1a0868a..9fc97a72 100644 --- a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.cs +++ b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.cs @@ -59,10 +59,10 @@ public partial class ConversationService : IConversationService return conversation; } - public async Task UpdateConversationTags(string conversationId, List tags) + public async Task UpdateConversationTags(string conversationId, List toAddTags, List toDeleteTags) { var db = _services.GetRequiredService(); - return db.UpdateConversationTags(conversationId, tags); + return db.UpdateConversationTags(conversationId, toAddTags, toDeleteTags); } public async Task UpdateConversationMessage(string conversationId, UpdateMessageRequest request) diff --git a/src/Infrastructure/BotSharp.Core/Loggers/Services/LoggerService.Conversation.cs b/src/Infrastructure/BotSharp.Core/Loggers/Services/LoggerService.Conversation.cs index 5ce057c3..c424d237 100644 --- a/src/Infrastructure/BotSharp.Core/Loggers/Services/LoggerService.Conversation.cs +++ b/src/Infrastructure/BotSharp.Core/Loggers/Services/LoggerService.Conversation.cs @@ -4,18 +4,28 @@ namespace BotSharp.Core.Loggers.Services; public partial class LoggerService { - public async Task> GetConversationContentLogs(string conversationId) + public async Task> GetConversationContentLogs(string conversationId, ConversationLogFilter filter) { + if (filter == null) + { + filter = ConversationLogFilter.Empty(); + } + var db = _services.GetRequiredService(); - var logs = db.GetConversationContentLogs(conversationId); + var logs = db.GetConversationContentLogs(conversationId, filter); return await Task.FromResult(logs); } - public async Task> GetConversationStateLogs(string conversationId) + public async Task> GetConversationStateLogs(string conversationId, ConversationLogFilter filter) { + if (filter == null) + { + filter = ConversationLogFilter.Empty(); + } + var db = _services.GetRequiredService(); - var logs = db.GetConversationStateLogs(conversationId); + var logs = db.GetConversationStateLogs(conversationId, filter); return await Task.FromResult(logs); } } diff --git a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Conversation.cs b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Conversation.cs index 3d70264c..647282a4 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Conversation.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Conversation.cs @@ -158,7 +158,7 @@ public partial class FileRepository } } - public bool UpdateConversationTags(string conversationId, List tags) + public bool UpdateConversationTags(string conversationId, List toAddTags, List toDeleteTags) { if (string.IsNullOrEmpty(conversationId)) return false; @@ -170,7 +170,11 @@ public partial class FileRepository var json = File.ReadAllText(convFile); var conv = JsonSerializer.Deserialize(json, _options); - conv.Tags = tags ?? new(); + + var tags = conv.Tags ?? []; + tags = tags.Concat(toAddTags).Distinct().ToList(); + conv.Tags = tags.Where(x => !toDeleteTags.Contains(x, StringComparer.OrdinalIgnoreCase)).ToList(); + conv.UpdatedTime = DateTime.UtcNow; File.WriteAllText(convFile, JsonSerializer.Serialize(conv, _options)); return true; diff --git a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Log.cs b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Log.cs index 78027987..3913a404 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Log.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Log.cs @@ -1,4 +1,5 @@ using BotSharp.Abstraction.Loggers.Models; +using Microsoft.IdentityModel.Logging; using System.IO; namespace BotSharp.Core.Repository @@ -54,26 +55,34 @@ namespace BotSharp.Core.Repository File.WriteAllText(file, JsonSerializer.Serialize(log, _options)); } - public List GetConversationContentLogs(string conversationId) + public DateTimePagination GetConversationContentLogs(string conversationId, ConversationLogFilter filter) { - var logs = new List(); - if (string.IsNullOrEmpty(conversationId)) return logs; + if (string.IsNullOrEmpty(conversationId)) return new(); var convDir = FindConversationDirectory(conversationId); - if (string.IsNullOrEmpty(convDir)) return logs; + if (string.IsNullOrEmpty(convDir)) return new(); var logDir = Path.Combine(convDir, "content_log"); - if (!Directory.Exists(logDir)) return logs; + if (!Directory.Exists(logDir)) return new(); + var logs = new List(); foreach (var file in Directory.GetFiles(logDir)) { var text = File.ReadAllText(file); var log = JsonSerializer.Deserialize(text); - if (log == null) continue; + if (log == null || log.CreatedTime >= filter.StartTime) continue; logs.Add(log); } - return logs.OrderBy(x => x.CreatedTime).ToList(); + + logs = logs.OrderByDescending(x => x.CreatedTime).Take(filter.Size).ToList(); + logs.Reverse(); + return new DateTimePagination + { + Items = logs, + Count = logs.Count, + NextTime = logs.FirstOrDefault()?.CreatedTime + }; } #endregion @@ -99,26 +108,34 @@ namespace BotSharp.Core.Repository File.WriteAllText(file, JsonSerializer.Serialize(log, _options)); } - public List GetConversationStateLogs(string conversationId) + public DateTimePagination GetConversationStateLogs(string conversationId, ConversationLogFilter filter) { - var logs = new List(); - if (string.IsNullOrEmpty(conversationId)) return logs; + if (string.IsNullOrEmpty(conversationId)) return new(); var convDir = FindConversationDirectory(conversationId); - if (string.IsNullOrEmpty(convDir)) return logs; + if (string.IsNullOrEmpty(convDir)) return new(); var logDir = Path.Combine(convDir, "state_log"); - if (!Directory.Exists(logDir)) return logs; + if (!Directory.Exists(logDir)) return new(); + var logs = new List(); foreach (var file in Directory.GetFiles(logDir)) { var text = File.ReadAllText(file); var log = JsonSerializer.Deserialize(text); - if (log == null) continue; + if (log == null || log.CreatedTime >= filter.StartTime) continue; logs.Add(log); } - return logs.OrderBy(x => x.CreatedTime).ToList(); + + logs = logs.OrderByDescending(x => x.CreatedTime).Take(filter.Size).ToList(); + logs.Reverse(); + return new DateTimePagination + { + Items = logs, + Count = logs.Count, + NextTime = logs.FirstOrDefault()?.CreatedTime + }; } #endregion diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs index d8b71339..f032f504 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs @@ -81,11 +81,11 @@ public class ConversationController : ControllerBase } [HttpGet("/conversation/{conversationId}/dialogs")] - public async Task> GetDialogs([FromRoute] string conversationId) + public async Task> GetDialogs([FromRoute] string conversationId, [FromQuery] int count = 100) { var conv = _services.GetRequiredService(); conv.SetConversationId(conversationId, [], isReadOnly: true); - var history = conv.GetDialogHistory(fromBreakpoint: false); + var history = conv.GetDialogHistory(lastCount: count, fromBreakpoint: false); var userService = _services.GetRequiredService(); var agentService = _services.GetRequiredService(); @@ -255,7 +255,7 @@ public class ConversationController : ControllerBase public async Task UpdateConversationTags([FromRoute] string conversationId, [FromBody] UpdateConversationRequest request) { var conv = _services.GetRequiredService(); - return await conv.UpdateConversationTags(conversationId, request.Tags); + return await conv.UpdateConversationTags(conversationId, request.ToAddTags, request.ToDeleteTags); } [HttpPut("/conversation/{conversationId}/update-message")] diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/DashboardController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/DashboardController.cs index c95d819d..93cfefde 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/DashboardController.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/DashboardController.cs @@ -19,16 +19,16 @@ public class DashboardController : ControllerBase } #region User Components [HttpGet("/dashboard/components")] - public async Task GetComponents() + public async Task GetComponents() { var userService = _services.GetRequiredService(); var dashboardProfile = await userService.GetDashboard(); - if (dashboardProfile == null) return new UserDashboardModel(); + if (dashboardProfile == null) return new(); - var result = new UserDashboardModel + var result = new UserDashboardViewModel { ConversationList = dashboardProfile.ConversationList.Select( - x => new UserDashboardConversationModel + x => new UserDashboardConversationViewModel { Name = x.Name, ConversationId = x.ConversationId, @@ -40,7 +40,7 @@ public class DashboardController : ControllerBase } [HttpPost("/dashboard/component/conversation")] - public async Task UpdateDashboardConversationInstruction(UserDashboardConversationModel dashConv) + public async Task UpdateDashboardConversationInstruction(UserDashboardConversationViewModel dashConv) { if (string.IsNullOrEmpty(dashConv.Name) && string.IsNullOrEmpty(dashConv.Instruction)) { diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/KnowledgeBaseController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/KnowledgeBaseController.cs index 50b7c528..f5c67070 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/KnowledgeBaseController.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/KnowledgeBaseController.cs @@ -12,7 +12,9 @@ public class KnowledgeBaseController : ControllerBase private readonly IKnowledgeService _knowledgeService; private readonly IServiceProvider _services; - public KnowledgeBaseController(IKnowledgeService knowledgeService, IServiceProvider services) + public KnowledgeBaseController( + IKnowledgeService knowledgeService, + IServiceProvider services) { _knowledgeService = knowledgeService; _services = services; @@ -117,6 +119,46 @@ public class KnowledgeBaseController : ControllerBase #endregion + #region Snapshot + [HttpGet("/knowledge/vector/{collection}/snapshots")] + public async Task> GetVectorCollectionSnapshots([FromRoute] string collection) + { + var snapshots = await _knowledgeService.GetVectorCollectionSnapshots(collection); + return snapshots.Select(x => VectorCollectionSnapshotViewModel.From(x)); + } + + [HttpPost("/knowledge/vector/{collection}/snapshot")] + public async Task CreateVectorCollectionSnapshot([FromRoute] string collection) + { + var snapshot = await _knowledgeService.CreateVectorCollectionSnapshot(collection); + return VectorCollectionSnapshotViewModel.From(snapshot); + } + + [HttpGet("/knowledge/vector/{collection}/snapshot")] + public async Task GetVectorCollectionSnapshot([FromRoute] string collection, [FromQuery] string snapshotFileName) + { + var snapshot = await _knowledgeService.DownloadVectorCollectionSnapshot(collection, snapshotFileName); + return BuildFileResult(snapshotFileName, snapshot); + } + + [HttpPost("/knowledge/vector/{collection}/snapshot/recover")] + public async Task RecoverVectorCollectionFromSnapshot([FromRoute] string collection, IFormFile snapshotFile) + { + var fileName = snapshotFile.FileName; + var binary = FileUtility.BuildBinaryDataFromFile(snapshotFile); + var done = await _knowledgeService.RecoverVectorCollectionFromSnapshot(collection, fileName, binary); + return done; + } + + [HttpDelete("/knowledge/vector/{collection}/snapshot")] + public async Task DeleteVectorCollectionSnapshots([FromRoute] string collection, [FromBody] DeleteVectorCollectionSnapshotRequest request) + { + var done = await _knowledgeService.DeleteVectorCollectionSnapshot(collection, request.SnapshotName); + return done; + } + #endregion + + #region Document [HttpPost("/knowledge/document/{collection}/upload")] public async Task UploadKnowledgeDocuments([FromRoute] string collection, [FromBody] VectorKnowledgeUploadRequest request) @@ -187,7 +229,6 @@ public class KnowledgeBaseController : ControllerBase #endregion - #region Graph [HttpPost("/knowledge/graph/search")] public async Task SearchGraphKnowledge([FromBody] SearchGraphKnowledgeRequest request) @@ -214,4 +255,13 @@ public class KnowledgeBaseController : ControllerBase return saved ? "Success" : "Fail"; } #endregion + + #region Private methods + private FileStreamResult BuildFileResult(string fileName, BinaryData fileData) + { + var stream = fileData.ToStream(); + stream.Position = 0; + return File(stream, "application/octet-stream", Path.GetFileName(fileName)); + } + #endregion } diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/LoggerController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/LoggerController.cs index 892c3195..7032bcd3 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/LoggerController.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/LoggerController.cs @@ -10,14 +10,11 @@ namespace BotSharp.OpenAPI.Controllers; public class LoggerController : ControllerBase { private readonly IServiceProvider _services; - private readonly IUserIdentity _user; public LoggerController( - IServiceProvider services, - IUserIdentity user) + IServiceProvider services) { _services = services; - _user = user; } [HttpGet("/logger/full-log")] @@ -40,17 +37,21 @@ public class LoggerController : ControllerBase #region Conversation log [HttpGet("/logger/conversation/{conversationId}/content-log")] - public async Task> GetConversationContentLogs([FromRoute] string conversationId) + public async Task> GetConversationContentLogs( + [FromRoute] string conversationId, + [FromQuery] ConversationLogFilter request) { var logging = _services.GetRequiredService(); - return await logging.GetConversationContentLogs(conversationId); + return await logging.GetConversationContentLogs(conversationId, request); } [HttpGet("/logger/conversation/{conversationId}/state-log")] - public async Task> GetConversationStateLogs([FromRoute] string conversationId) + public async Task> GetConversationStateLogs( + [FromRoute] string conversationId, + [FromQuery] ConversationLogFilter request) { var logging = _services.GetRequiredService(); - return await logging.GetConversationStateLogs(conversationId); + return await logging.GetConversationStateLogs(conversationId, request); } #endregion diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/AgentCreationModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/Request/AgentCreationModel.cs similarity index 100% rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/AgentCreationModel.cs rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/Request/AgentCreationModel.cs diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/AgentTaskCreateModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/Request/AgentTaskCreateModel.cs similarity index 100% rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/AgentTaskCreateModel.cs rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/Request/AgentTaskCreateModel.cs diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/AgentTaskUpdateModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/Request/AgentTaskUpdateModel.cs similarity index 100% rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/AgentTaskUpdateModel.cs rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/Request/AgentTaskUpdateModel.cs diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/AgentTemplatePatchModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/Request/AgentTemplatePatchModel.cs similarity index 100% rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/AgentTemplatePatchModel.cs rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/Request/AgentTemplatePatchModel.cs diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/AgentUpdateModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/Request/AgentUpdateModel.cs similarity index 100% rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/AgentUpdateModel.cs rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/Request/AgentUpdateModel.cs diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/RoutingRuleUpdateModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/Request/RoutingRuleUpdateModel.cs similarity index 100% rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/RoutingRuleUpdateModel.cs rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/Request/RoutingRuleUpdateModel.cs diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/AgentTaskViewModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/View/AgentTaskViewModel.cs similarity index 100% rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/AgentTaskViewModel.cs rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/View/AgentTaskViewModel.cs diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/AgentViewModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/View/AgentViewModel.cs similarity index 100% rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/AgentViewModel.cs rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/View/AgentViewModel.cs diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/ConversationCreationModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/Request/ConversationCreationModel.cs similarity index 100% rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/ConversationCreationModel.cs rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/Request/ConversationCreationModel.cs diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/ConversationSummaryModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/Request/ConversationSummaryModel.cs similarity index 100% rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/ConversationSummaryModel.cs rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/Request/ConversationSummaryModel.cs diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/InputMessageFiles.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/Request/InputMessageFiles.cs similarity index 100% rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/InputMessageFiles.cs rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/Request/InputMessageFiles.cs diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/MigrateLatestStateRequest.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/Request/MigrateLatestStateRequest.cs similarity index 100% rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/MigrateLatestStateRequest.cs rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/Request/MigrateLatestStateRequest.cs diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/NewMessageModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/Request/NewMessageModel.cs similarity index 100% rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/NewMessageModel.cs rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/Request/NewMessageModel.cs diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/Request/UpdateConversationRequest.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/Request/UpdateConversationRequest.cs new file mode 100644 index 00000000..c177221c --- /dev/null +++ b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/Request/UpdateConversationRequest.cs @@ -0,0 +1,7 @@ +namespace BotSharp.OpenAPI.ViewModels.Conversations; + +public class UpdateConversationRequest +{ + public List ToAddTags { get; set; } = []; + public List ToDeleteTags { get; set; } = []; +} diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/UpdateConversationTitleAliasModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/Request/UpdateConversationTitleAliasModel.cs similarity index 100% rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/UpdateConversationTitleAliasModel.cs rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/Request/UpdateConversationTitleAliasModel.cs diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/UpdateConversationTitleModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/Request/UpdateConversationTitleModel.cs similarity index 100% rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/UpdateConversationTitleModel.cs rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/Request/UpdateConversationTitleModel.cs diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/UpdateMessageModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/Request/UpdateMessageModel.cs similarity index 100% rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/UpdateMessageModel.cs rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/Request/UpdateMessageModel.cs diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/ChatResponseModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/Response/ChatResponseModel.cs similarity index 100% rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/ChatResponseModel.cs rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/Response/ChatResponseModel.cs diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/UpdateConversationRequest.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/UpdateConversationRequest.cs deleted file mode 100644 index c9b89747..00000000 --- a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/UpdateConversationRequest.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace BotSharp.OpenAPI.ViewModels.Conversations; - -public class UpdateConversationRequest -{ - public List Tags { get; set; } = []; -} diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/ConversationViewModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/View/ConversationViewModel.cs similarity index 100% rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/ConversationViewModel.cs rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/View/ConversationViewModel.cs diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Embeddings/EmbeddingInputModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Embeddings/Request/EmbeddingInputModel.cs similarity index 100% rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Embeddings/EmbeddingInputModel.cs rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Embeddings/Request/EmbeddingInputModel.cs diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Files/MessageFileViewModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Files/View/MessageFileViewModel.cs similarity index 100% rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Files/MessageFileViewModel.cs rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Files/View/MessageFileViewModel.cs diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/InstructBaseRequest.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/Request/InstructBaseRequest.cs similarity index 100% rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/InstructBaseRequest.cs rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/Request/InstructBaseRequest.cs diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/InstructMessageModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/Request/InstructMessageModel.cs similarity index 100% rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/InstructMessageModel.cs rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/Request/InstructMessageModel.cs diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/ImageGenerationViewModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/View/ImageGenerationViewModel.cs similarity index 100% rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/ImageGenerationViewModel.cs rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/View/ImageGenerationViewModel.cs diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/InstructBaseViewModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/View/InstructBaseViewModel.cs similarity index 100% rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/InstructBaseViewModel.cs rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/View/InstructBaseViewModel.cs diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/InstructionLogViewModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/View/InstructionLogViewModel.cs similarity index 100% rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/InstructionLogViewModel.cs rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/View/InstructionLogViewModel.cs diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/MultiModalViewModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/View/MultiModalViewModel.cs similarity index 100% rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/MultiModalViewModel.cs rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/View/MultiModalViewModel.cs diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/PdfCompletionViewModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/View/PdfCompletionViewModel.cs similarity index 100% rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/PdfCompletionViewModel.cs rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/View/PdfCompletionViewModel.cs diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/SpeechToTextViewModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/View/SpeechToTextViewModel.cs similarity index 100% rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/SpeechToTextViewModel.cs rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/View/SpeechToTextViewModel.cs diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/CreateVectorCollectionRequest.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/Request/CreateVectorCollectionRequest.cs similarity index 100% rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/CreateVectorCollectionRequest.cs rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/Request/CreateVectorCollectionRequest.cs diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/Request/DeleteVectorCollectionSnapshotRequest.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/Request/DeleteVectorCollectionSnapshotRequest.cs new file mode 100644 index 00000000..30398b2c --- /dev/null +++ b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/Request/DeleteVectorCollectionSnapshotRequest.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace BotSharp.OpenAPI.ViewModels.Knowledges; + +public class DeleteVectorCollectionSnapshotRequest +{ + [JsonPropertyName("snapshot_name")] + public string SnapshotName { get; set; } = default!; +} diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/GetKnowledgeDocsRequest.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/Request/GetKnowledgeDocsRequest.cs similarity index 100% rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/GetKnowledgeDocsRequest.cs rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/Request/GetKnowledgeDocsRequest.cs diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/SearchGraphKnowledgeRequest.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/Request/SearchGraphKnowledgeRequest.cs similarity index 100% rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/SearchGraphKnowledgeRequest.cs rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/Request/SearchGraphKnowledgeRequest.cs diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/SearchVectorKnowledgeRequest.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/Request/SearchVectorKnowledgeRequest.cs similarity index 100% rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/SearchVectorKnowledgeRequest.cs rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/Request/SearchVectorKnowledgeRequest.cs diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/VectorKnowledgeCreateRequest.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/Request/VectorKnowledgeCreateRequest.cs similarity index 100% rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/VectorKnowledgeCreateRequest.cs rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/Request/VectorKnowledgeCreateRequest.cs diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/VectorKnowledgeUpdateRequest.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/Request/VectorKnowledgeUpdateRequest.cs similarity index 100% rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/VectorKnowledgeUpdateRequest.cs rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/Request/VectorKnowledgeUpdateRequest.cs diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/VectorKnowledgeUploadRequest.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/Request/VectorKnowledgeUploadRequest.cs similarity index 100% rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/VectorKnowledgeUploadRequest.cs rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/Request/VectorKnowledgeUploadRequest.cs diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/GraphKnowledgeViewModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/View/GraphKnowledgeViewModel.cs similarity index 100% rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/GraphKnowledgeViewModel.cs rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/View/GraphKnowledgeViewModel.cs diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/KnowledgeFileViewModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/View/KnowledgeFileViewModel.cs similarity index 100% rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/KnowledgeFileViewModel.cs rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/View/KnowledgeFileViewModel.cs diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/VectorCollectionConfigViewModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/View/VectorCollectionConfigViewModel.cs similarity index 100% rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/VectorCollectionConfigViewModel.cs rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/View/VectorCollectionConfigViewModel.cs diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/View/VectorCollectionSnapshotViewModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/View/VectorCollectionSnapshotViewModel.cs new file mode 100644 index 00000000..95b8a325 --- /dev/null +++ b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/View/VectorCollectionSnapshotViewModel.cs @@ -0,0 +1,35 @@ +using BotSharp.Abstraction.VectorStorage.Models; +using System.Text.Json.Serialization; + +namespace BotSharp.OpenAPI.ViewModels.Knowledges; + +public class VectorCollectionSnapshotViewModel +{ + [JsonPropertyName("name")] + public string Name { get; set; } = default!; + + [JsonPropertyName("size")] + public long Size { get; set; } + + [JsonPropertyName("created_time")] + public DateTime CreatedTime { get; set; } + + [JsonPropertyName("check_sum")] + public string? CheckSum { get; set; } + + public static VectorCollectionSnapshotViewModel? From(VectorCollectionSnapshot? model) + { + if (model == null) + { + return null; + } + + return new VectorCollectionSnapshotViewModel + { + Name = model.Name, + Size = model.Size, + CreatedTime = model.CreatedTime, + CheckSum = model.CheckSum + }; + } +} diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/VectorKnowledgeViewModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/View/VectorKnowledgeViewModel.cs similarity index 100% rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/VectorKnowledgeViewModel.cs rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/View/VectorKnowledgeViewModel.cs diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Roles/RoleAgentActionViewModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Roles/Request/RoleAgentActionViewModel.cs similarity index 100% rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Roles/RoleAgentActionViewModel.cs rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Roles/Request/RoleAgentActionViewModel.cs diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Roles/RoleViewModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Roles/Request/RoleViewModel.cs similarity index 100% rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Roles/RoleViewModel.cs rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Roles/Request/RoleViewModel.cs diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Roles/RoleUpdateModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Roles/View/RoleUpdateModel.cs similarity index 100% rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Roles/RoleUpdateModel.cs rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Roles/View/RoleUpdateModel.cs diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserAvatarModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/Request/UserAvatarModel.cs similarity index 100% rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserAvatarModel.cs rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/Request/UserAvatarModel.cs diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserCreationModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/Request/UserCreationModel.cs similarity index 100% rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserCreationModel.cs rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/Request/UserCreationModel.cs diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserResetPasswordModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/Request/UserResetPasswordModel.cs similarity index 100% rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserResetPasswordModel.cs rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/Request/UserResetPasswordModel.cs diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserUpdateModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/Request/UserUpdateModel.cs similarity index 100% rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserUpdateModel.cs rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/Request/UserUpdateModel.cs diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserAgentActionViewModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/View/UserAgentActionViewModel.cs similarity index 100% rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserAgentActionViewModel.cs rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/View/UserAgentActionViewModel.cs diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserDashboardConversationModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/View/UserDashboardConversationViewModel.cs similarity index 57% rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserDashboardConversationModel.cs rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/View/UserDashboardConversationViewModel.cs index 3a5f3491..7165fa87 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserDashboardConversationModel.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/View/UserDashboardConversationViewModel.cs @@ -1,19 +1,14 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Text.Json.Serialization; -using System.Threading.Tasks; namespace BotSharp.OpenAPI.ViewModels.Users; -public class UserDashboardModel -{ +public class UserDashboardViewModel +{ [JsonPropertyName("conversation_list")] - public IList ConversationList { get; set; } = []; + public IList ConversationList { get; set; } = []; } -public class UserDashboardConversationModel +public class UserDashboardConversationViewModel { [JsonPropertyName("name")] public string? Name { get; set; } diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserViewModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/View/UserViewModel.cs similarity index 100% rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserViewModel.cs rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/View/UserViewModel.cs diff --git a/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/ChatHubCrontabHook.cs b/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/ChatHubCrontabHook.cs index f4b4266f..cad0189d 100644 --- a/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/ChatHubCrontabHook.cs +++ b/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/ChatHubCrontabHook.cs @@ -10,7 +10,6 @@ public class ChatHubCrontabHook : ICrontabHook private readonly IHubContext _chatHub; private readonly ILogger _logger; private readonly IUserIdentity _user; - private readonly IConversationStorage _storage; private readonly BotSharpOptions _options; private readonly ChatHubSettings _settings; @@ -22,7 +21,6 @@ public class ChatHubCrontabHook : ICrontabHook IHubContext chatHub, ILogger logger, IUserIdentity user, - IConversationStorage storage, BotSharpOptions options, ChatHubSettings settings) { @@ -30,7 +28,6 @@ public class ChatHubCrontabHook : ICrontabHook _chatHub = chatHub; _logger = logger; _user = user; - _storage = storage; _options = options; _settings = settings; } @@ -58,19 +55,8 @@ public class ChatHubCrontabHook : ICrontabHook { try { - if (_settings.EventDispatchBy == EventDispatchType.Group) - { - await _chatHub.Clients.Group(item.ConversationId).SendAsync(GENERATE_NOTIFICATION, json); - } - else - { - await _chatHub.Clients.User(item.UserId).SendAsync(GENERATE_NOTIFICATION, json); - } - } - catch (Exception ex) - { - _logger.LogWarning($"Failed to send event in {nameof(ChatHubCrontabHook)} (conversation id: {item.ConversationId})." + - $"\r\n{ex.Message}\r\n{ex.InnerException}"); + await _chatHub.Clients.User(item.UserId).SendAsync(GENERATE_NOTIFICATION, json); } + catch { } } } diff --git a/src/Plugins/BotSharp.Plugin.Graph/GraphDb.cs b/src/Plugins/BotSharp.Plugin.Graph/GraphDb.cs index 0465dc4e..99378ddc 100644 --- a/src/Plugins/BotSharp.Plugin.Graph/GraphDb.cs +++ b/src/Plugins/BotSharp.Plugin.Graph/GraphDb.cs @@ -60,9 +60,9 @@ public class GraphDb : IGraphDb using (var client = http.CreateClient()) { - var uri = new Uri(url); try { + var uri = new Uri(url); var data = JsonSerializer.Serialize(request, _jsonOptions); var message = new HttpRequestMessage { diff --git a/src/Plugins/BotSharp.Plugin.KnowledgeBase/Services/KnowledgeService.Snapshot.cs b/src/Plugins/BotSharp.Plugin.KnowledgeBase/Services/KnowledgeService.Snapshot.cs new file mode 100644 index 00000000..4b839332 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.KnowledgeBase/Services/KnowledgeService.Snapshot.cs @@ -0,0 +1,64 @@ +namespace BotSharp.Plugin.KnowledgeBase.Services; + +public partial class KnowledgeService +{ + public async Task> GetVectorCollectionSnapshots(string collectionName) + { + if (string.IsNullOrWhiteSpace(collectionName)) + { + return Enumerable.Empty(); + } + + var db = GetVectorDb(); + var snapshots = await db.GetCollectionSnapshots(collectionName); + return snapshots; + } + + public async Task CreateVectorCollectionSnapshot(string collectionName) + { + if (string.IsNullOrWhiteSpace(collectionName)) + { + return null; + } + + var db = GetVectorDb(); + var snapshot = await db.CreateCollectionShapshot(collectionName); + return snapshot; + } + + public async Task DownloadVectorCollectionSnapshot(string collectionName, string snapshotFileName) + { + if (string.IsNullOrWhiteSpace(collectionName) || string.IsNullOrWhiteSpace(snapshotFileName)) + { + return BinaryData.Empty; + } + + var db = GetVectorDb(); + var snapshot = await db.DownloadCollectionSnapshot(collectionName, snapshotFileName); + return snapshot; + } + + public async Task RecoverVectorCollectionFromSnapshot(string collectionName, string snapshotFileName, BinaryData snapshotData) + { + if (string.IsNullOrWhiteSpace(collectionName)) + { + return false; + } + + var db = GetVectorDb(); + var done = await db.RecoverCollectionFromShapshot(collectionName, snapshotFileName, snapshotData); + return done; + } + + public async Task DeleteVectorCollectionSnapshot(string collectionName, string snapshotName) + { + if (string.IsNullOrWhiteSpace(collectionName) || string.IsNullOrWhiteSpace(snapshotName)) + { + return false; + } + + var db = GetVectorDb(); + var done = await db.DeleteCollectionShapshot(collectionName, snapshotName); + return done; + } +} diff --git a/src/Plugins/BotSharp.Plugin.MicrosoftExtensionsAI/MicrosoftExtensionsAIChatCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.MicrosoftExtensionsAI/MicrosoftExtensionsAIChatCompletionProvider.cs index 37d3877c..6ee77772 100644 --- a/src/Plugins/BotSharp.Plugin.MicrosoftExtensionsAI/MicrosoftExtensionsAIChatCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.MicrosoftExtensionsAI/MicrosoftExtensionsAIChatCompletionProvider.cs @@ -27,7 +27,6 @@ public sealed class MicrosoftExtensionsAIChatCompletionProvider : IChatCompletio private readonly IChatClient _client; private readonly ILogger _logger; private readonly IServiceProvider _services; - private List renderedInstructions = []; private string? _model; /// @@ -46,7 +45,7 @@ public sealed class MicrosoftExtensionsAIChatCompletionProvider : IChatCompletio /// public string Provider => "microsoft.extensions.ai"; - public string Model => _model; + public string Model => _model ?? ""; /// public void SetModelName(string model) => _model = model; @@ -56,7 +55,7 @@ public sealed class MicrosoftExtensionsAIChatCompletionProvider : IChatCompletio { // Before chat completion hook var hooks = _services.GetServices().ToArray(); - renderedInstructions = []; + List renderedInstructions = []; await Task.WhenAll(hooks.Select(hook => hook.BeforeGenerating(agent, conversations))); // Configure options @@ -145,13 +144,13 @@ public sealed class MicrosoftExtensionsAIChatCompletionProvider : IChatCompletio var completion = await _client.GetResponseAsync(messages); - RoleDialogModel result = new(AgentRole.Assistant, string.Concat(completion.Message.Contents.OfType())) + RoleDialogModel result = new(AgentRole.Assistant, completion.Text) { CurrentAgentId = agent.Id, - RenderedInstruction = string.Join("\r\n", renderedInstructions) + //RenderedInstruction = renderedInstructions, }; - if (completion.Message.Contents.OfType().FirstOrDefault() is { } fcc) + if (completion.Messages.SelectMany(m => m.Contents).OfType().FirstOrDefault() is { } fcc) { result.Role = AgentRole.Function; result.MessageId = conversations.LastOrDefault()?.MessageId ?? string.Empty; diff --git a/src/Plugins/BotSharp.Plugin.MicrosoftExtensionsAI/MicrosoftExtensionsAITextCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.MicrosoftExtensionsAI/MicrosoftExtensionsAITextCompletionProvider.cs index ed0e94d9..ef3b35c7 100644 --- a/src/Plugins/BotSharp.Plugin.MicrosoftExtensionsAI/MicrosoftExtensionsAITextCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.MicrosoftExtensionsAI/MicrosoftExtensionsAITextCompletionProvider.cs @@ -51,7 +51,7 @@ public sealed class MicrosoftExtensionsAITextCompletionProvider : ITextCompletio _tokenStatistics.StartTimer(); var completion = await _chatClient.GetResponseAsync(text); - var result = string.Concat(completion.Message.Contents.OfType()); + var result = completion.Text; _tokenStatistics.StopTimer(); // After chat completion hook diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Conversation.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Conversation.cs index 908e66b5..ebae075b 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Conversation.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Conversation.cs @@ -134,13 +134,20 @@ public partial class MongoRepository _dc.Conversations.UpdateOne(filterConv, updateConv); } - public bool UpdateConversationTags(string conversationId, List tags) + public bool UpdateConversationTags(string conversationId, List toAddTags, List toDeleteTags) { if (string.IsNullOrEmpty(conversationId)) return false; var filter = Builders.Filter.Eq(x => x.Id, conversationId); + var conv = _dc.Conversations.Find(filter).FirstOrDefault(); + if (conv == null) return false; + + var tags = conv.Tags ?? []; + tags = tags.Concat(toAddTags).Distinct().ToList(); + tags = tags.Where(x => !toDeleteTags.Contains(x, StringComparer.OrdinalIgnoreCase)).ToList(); + var update = Builders.Update - .Set(x => x.Tags, tags ?? new()) + .Set(x => x.Tags, tags) .Set(x => x.UpdatedTime, DateTime.UtcNow); var res = _dc.Conversations.UpdateOne(filter, update); diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Log.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Log.cs index 457faba2..8cd6e158 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Log.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Log.cs @@ -1,5 +1,6 @@ using BotSharp.Abstraction.Loggers.Models; using BotSharp.Abstraction.Repositories.Filters; +using MongoDB.Driver; using System.Text.Json; namespace BotSharp.Plugin.MongoStorage.Repository; @@ -34,7 +35,8 @@ public partial class MongoRepository { if (log == null) return; - var found = _dc.Conversations.AsQueryable().FirstOrDefault(x => x.Id == log.ConversationId); + var filter = Builders.Filter.Eq(x => x.Id, log.ConversationId); + var found = _dc.Conversations.Find(filter).FirstOrDefault(); if (found == null) return; var logDoc = new ConversationContentLogDocument @@ -52,25 +54,36 @@ public partial class MongoRepository _dc.ContentLogs.InsertOne(logDoc); } - public List GetConversationContentLogs(string conversationId) + public DateTimePagination GetConversationContentLogs(string conversationId, ConversationLogFilter filter) { - var logs = _dc.ContentLogs - .AsQueryable() - .Where(x => x.ConversationId == conversationId) - .Select(x => new ContentLogOutputModel - { - ConversationId = x.ConversationId, - MessageId = x.MessageId, - Name = x.Name, - AgentId = x.AgentId, - Role = x.Role, - Source = x.Source, - Content = x.Content, - CreatedTime = x.CreatedTime - }) - .OrderBy(x => x.CreatedTime) - .ToList(); - return logs; + var builder = Builders.Filter; + var logFilters = new List> + { + builder.Eq(x => x.ConversationId, conversationId), + builder.Lt(x => x.CreatedTime, filter.StartTime) + }; + var logSortDef = Builders.Sort.Descending(x => x.CreatedTime); + + var docs = _dc.ContentLogs.Find(builder.And(logFilters)).Sort(logSortDef).Limit(filter.Size).ToList(); + var logs = docs.Select(x => new ContentLogOutputModel + { + ConversationId = x.ConversationId, + MessageId = x.MessageId, + Name = x.Name, + AgentId = x.AgentId, + Role = x.Role, + Source = x.Source, + Content = x.Content, + CreatedTime = x.CreatedTime + }).ToList(); + + logs.Reverse(); + return new DateTimePagination + { + Items = logs, + Count = logs.Count, + NextTime = logs.FirstOrDefault()?.CreatedTime + }; } #endregion @@ -79,7 +92,8 @@ public partial class MongoRepository { if (log == null) return; - var found = _dc.Conversations.AsQueryable().FirstOrDefault(x => x.Id == log.ConversationId); + var filter = Builders.Filter.Eq(x => x.Id, log.ConversationId); + var found = _dc.Conversations.Find(filter).FirstOrDefault(); if (found == null) return; var logDoc = new ConversationStateLogDocument @@ -94,22 +108,33 @@ public partial class MongoRepository _dc.StateLogs.InsertOne(logDoc); } - public List GetConversationStateLogs(string conversationId) + public DateTimePagination GetConversationStateLogs(string conversationId, ConversationLogFilter filter) { - var logs = _dc.StateLogs - .AsQueryable() - .Where(x => x.ConversationId == conversationId) - .Select(x => new ConversationStateLogModel - { - ConversationId = x.ConversationId, - AgentId = x.AgentId, - MessageId = x.MessageId, - States = x.States, - CreatedTime = x.CreatedTime - }) - .OrderBy(x => x.CreatedTime) - .ToList(); - return logs; + var builder = Builders.Filter; + var logFilters = new List> + { + builder.Eq(x => x.ConversationId, conversationId), + builder.Lt(x => x.CreatedTime, filter.StartTime) + }; + var logSortDef = Builders.Sort.Descending(x => x.CreatedTime); + + var docs = _dc.StateLogs.Find(builder.And(logFilters)).Sort(logSortDef).Limit(filter.Size).ToList(); + var logs = docs.Select(x => new ConversationStateLogModel + { + ConversationId = x.ConversationId, + AgentId = x.AgentId, + MessageId = x.MessageId, + States = x.States, + CreatedTime = x.CreatedTime + }).ToList(); + + logs.Reverse(); + return new DateTimePagination + { + Items = logs, + Count = logs.Count, + NextTime = logs.FirstOrDefault()?.CreatedTime + }; } #endregion diff --git a/src/Plugins/BotSharp.Plugin.OpenAI/Models/Realtime/RealtimeSessionBody.cs b/src/Plugins/BotSharp.Plugin.OpenAI/Models/Realtime/RealtimeSessionBody.cs index 0d0947e5..f6a6322c 100644 --- a/src/Plugins/BotSharp.Plugin.OpenAI/Models/Realtime/RealtimeSessionBody.cs +++ b/src/Plugins/BotSharp.Plugin.OpenAI/Models/Realtime/RealtimeSessionBody.cs @@ -72,4 +72,11 @@ public class InputAudioTranscription { [JsonPropertyName("model")] public string Model { get; set; } = null!; + + [JsonPropertyName("language")] + public string Language { get; set; } = "en"; + + [JsonPropertyName("prompt")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Prompt { get; set; } } \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Realtime/RealTimeCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Realtime/RealTimeCompletionProvider.cs index 0d6f76b4..47a9030d 100644 --- a/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Realtime/RealTimeCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Realtime/RealTimeCompletionProvider.cs @@ -2,7 +2,9 @@ using BotSharp.Abstraction.Conversations.Enums; using BotSharp.Abstraction.Files.Utilities; using BotSharp.Abstraction.Functions.Models; using BotSharp.Abstraction.Options; +using BotSharp.Abstraction.Realtime; using BotSharp.Abstraction.Realtime.Models; +using BotSharp.Abstraction.Routing; using BotSharp.Core.Infrastructures; using BotSharp.Plugin.OpenAI.Models.Realtime; using OpenAI.Chat; @@ -42,7 +44,7 @@ public class RealTimeCompletionProvider : IRealTimeCompletion Action onModelReady, Action onModelAudioDeltaReceived, Action onModelAudioResponseDone, - Action onAudioTranscriptDone, + Action onModelAudioTranscriptDone, Action> onModelResponseDone, Action onConversationItemCreated, Action onInputAudioTranscriptionCompleted, @@ -64,7 +66,7 @@ public class RealTimeCompletionProvider : IRealTimeCompletion onModelReady, onModelAudioDeltaReceived, onModelAudioResponseDone, - onAudioTranscriptDone, + onModelAudioTranscriptDone, onModelResponseDone, onConversationItemCreated, onInputAudioTranscriptionCompleted, @@ -125,10 +127,10 @@ public class RealTimeCompletionProvider : IRealTimeCompletion Action onModelReady, Action onModelAudioDeltaReceived, Action onModelAudioResponseDone, - Action onAudioTranscriptDone, + Action onModelAudioTranscriptDone, Action> onModelResponseDone, Action onConversationItemCreated, - Action onInputAudioTranscriptionCompleted, + Action onUserAudioTranscriptionCompleted, Action onUserInterrupted) { var buffer = new byte[1024 * 32]; @@ -138,7 +140,7 @@ public class RealTimeCompletionProvider : IRealTimeCompletion { result = await _webSocket.ReceiveAsync( new ArraySegment(buffer), CancellationToken.None); - + // Convert received data to text/audio (Twilio sends Base64-encoded audio) string receivedText = Encoding.UTF8.GetString(buffer, 0, result.Count); if (string.IsNullOrEmpty(receivedText)) @@ -171,7 +173,7 @@ public class RealTimeCompletionProvider : IRealTimeCompletion _logger.LogInformation($"{response.Type}: {receivedText}"); var data = JsonSerializer.Deserialize(receivedText); await Task.Delay(1000); - onAudioTranscriptDone(data.Transcript); + onModelAudioTranscriptDone(data.Transcript); } else if (response.Type == "response.audio.delta") { @@ -201,8 +203,11 @@ public class RealTimeCompletionProvider : IRealTimeCompletion else if (response.Type == "conversation.item.input_audio_transcription.completed") { _logger.LogInformation($"{response.Type}: {receivedText}"); - var message = await OnInputAudioTranscriptionCompleted(conn, receivedText); - onInputAudioTranscriptionCompleted(message); + var message = await OnUserAudioTranscriptionCompleted(conn, receivedText); + if (!string.IsNullOrEmpty(message.Content)) + { + onUserAudioTranscriptionCompleted(message); + } } else if (response.Type == "input_audio_buffer.speech_started") { @@ -309,6 +314,9 @@ public class RealTimeCompletionProvider : IRealTimeCompletion return fn; }).ToArray(); + var words = new List(); + HookEmitter.Emit(_services, hook => words.AddRange(hook.OnModelTranscriptPrompt(agent))); + var sessionUpdate = new { type = "session.update", @@ -319,6 +327,8 @@ public class RealTimeCompletionProvider : IRealTimeCompletion InputAudioTranscription = new InputAudioTranscription { Model = "whisper-1", + Language = "en", + Prompt = string.Join(", ", words.Select(x => x.ToLower().Trim()).Distinct()).SubstringMax(1024) }, Voice = "alloy", Instructions = instruction, @@ -329,7 +339,7 @@ public class RealTimeCompletionProvider : IRealTimeCompletion MaxResponseOutputTokens = 512, TurnDetection = new RealtimeSessionTurnDetection { - Threshold = 0.8f, + Threshold = 0.9f, PrefixPadding = 300, SilenceDuration = 800 } @@ -662,7 +672,7 @@ public class RealTimeCompletionProvider : IRealTimeCompletion return outputs; } - public async Task OnInputAudioTranscriptionCompleted(RealtimeHubConnection conn, string response) + public async Task OnUserAudioTranscriptionCompleted(RealtimeHubConnection conn, string response) { var data = JsonSerializer.Deserialize(response); return new RoleDialogModel(AgentRole.User, data.Transcript) diff --git a/src/Plugins/BotSharp.Plugin.Qdrant/Models/RecoverFromSnapshotResponse.cs b/src/Plugins/BotSharp.Plugin.Qdrant/Models/RecoverFromSnapshotResponse.cs new file mode 100644 index 00000000..f346aa1b --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.Qdrant/Models/RecoverFromSnapshotResponse.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace BotSharp.Plugin.Qdrant.Models; + +public class RecoverFromSnapshotResponse +{ + [JsonPropertyName("time")] + public decimal Time { get; set; } + + [JsonPropertyName("status")] + public string Status { get; set; } + + [JsonPropertyName("result")] + public bool Result { get; set; } +} diff --git a/src/Plugins/BotSharp.Plugin.Qdrant/QdrantDb.cs b/src/Plugins/BotSharp.Plugin.Qdrant/QdrantDb.cs index ef9faa93..3c5d61e5 100644 --- a/src/Plugins/BotSharp.Plugin.Qdrant/QdrantDb.cs +++ b/src/Plugins/BotSharp.Plugin.Qdrant/QdrantDb.cs @@ -1,8 +1,14 @@ +using BotSharp.Abstraction.Options; using BotSharp.Abstraction.Utilities; using BotSharp.Abstraction.VectorStorage.Models; +using BotSharp.Plugin.Qdrant.Models; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Qdrant.Client; using Qdrant.Client.Grpc; +using System.Net.Http; +using System.Net.Mime; +using System.Text.Json; namespace BotSharp.Plugin.Qdrant; @@ -10,15 +16,18 @@ public class QdrantDb : IVectorDb { private QdrantClient _client; private readonly QdrantSetting _setting; + private readonly BotSharpOptions _options; private readonly IServiceProvider _services; private readonly ILogger _logger; public QdrantDb( QdrantSetting setting, + BotSharpOptions options, ILogger logger, IServiceProvider services) { _setting = setting; + _options = options; _logger = logger; _services = services; } @@ -39,6 +48,7 @@ public class QdrantDb : IVectorDb return _client; } + #region Collection public async Task DoesCollectionExist(string collectionName) { var client = GetClient(); @@ -86,7 +96,9 @@ public class QdrantDb : IVectorDb var collections = await GetClient().ListCollectionsAsync(); return collections.ToList(); } + #endregion + #region Collection data public async Task> GetPagedCollectionData(string collectionName, VectorFilter filter) { var exist = await DoesCollectionExist(collectionName); @@ -332,4 +344,150 @@ public class QdrantDb : IVectorDb var result = await client.DeleteAsync(collectionName, new Filter()); return result.Status == UpdateStatus.Completed; } + #endregion + + #region Snapshots + public async Task> GetCollectionSnapshots(string collectionName) + { + var exist = await DoesCollectionExist(collectionName); + if (!exist) + { + return Enumerable.Empty(); + } + + var client = GetClient(); + var data = await client.ListSnapshotsAsync(collectionName); + var snapshots = data.Select(x => new VectorCollectionSnapshot + { + Name = x.Name, + Size = x.Size, + CreatedTime = x.CreationTime.ToDateTime(), + CheckSum = x.Checksum + }); + return snapshots; + } + + public async Task CreateCollectionShapshot(string collectionName) + { + var exist = await DoesCollectionExist(collectionName); + if (!exist) + { + return null; + } + + var client = GetClient(); + var desc = await client.CreateSnapshotAsync(collectionName); + if (desc == null) + { + return null; + } + + return new VectorCollectionSnapshot + { + Name = desc.Name, + Size = desc.Size, + CreatedTime = desc.CreationTime.ToDateTime(), + CheckSum = desc.Checksum + }; + } + + public async Task DownloadCollectionSnapshot(string collectionName, string snapshotFileName) + { + var exist = await DoesCollectionExist(collectionName); + if (!exist) + { + return BinaryData.Empty; + } + + var domain = $"https://{_setting.Url}:6333"; + var url = $"{domain}/collections/{collectionName}/snapshots/{snapshotFileName}"; + + var http = _services.GetRequiredService(); + using (var client = http.CreateClient()) + { + try + { + var uri = new Uri(url); + var message = new HttpRequestMessage + { + Method = HttpMethod.Get, + RequestUri = uri + }; + + client.DefaultRequestHeaders.Add("api-key", _setting.ApiKey); + var rawResponse = await client.SendAsync(message); + rawResponse.EnsureSuccessStatusCode(); + + using var contentStream = await rawResponse.Content.ReadAsStreamAsync(); + return BinaryData.FromStream(contentStream); + } + catch (Exception ex) + { + _logger.LogError($"Error when downloading Qdrant snapshot (Endpoint: {url}, Snapshot: {snapshotFileName}). {ex.Message}\r\n{ex.InnerException}"); + return BinaryData.Empty; + } + } + } + + public async Task RecoverCollectionFromShapshot(string collectionName, string snapshotFileName, BinaryData snapshotData) + { + var domain = $"https://{_setting.Url}:6333"; + var url = $"{domain}/collections/{collectionName}/snapshots/upload"; + + var http = _services.GetRequiredService(); + using (var client = http.CreateClient()) + { + try + { + var uri = new Uri(url); + var data = new MultipartFormDataContent + { + { new StringContent(snapshotFileName), "name" }, + { new StringContent(MediaTypeNames.Application.Octet), "type" }, + { new StreamContent(snapshotData.ToStream()), "snapshot", snapshotFileName } + }; + + var message = new HttpRequestMessage + { + Method = HttpMethod.Post, + RequestUri = uri, + Content = data + }; + + client.DefaultRequestHeaders.Add("api-key", _setting.ApiKey); + var rawResponse = await client.SendAsync(message); + rawResponse.EnsureSuccessStatusCode(); + + var responseStr = await rawResponse.Content.ReadAsStringAsync(); + var response = JsonSerializer.Deserialize(responseStr, _options.JsonSerializerOptions); + return response?.Result == true; + } + catch (Exception ex) + { + _logger.LogError($"Error when uploading Qdrant snapshot (Endpoint: {url}, Snapshot: {snapshotFileName}). {ex.Message}\r\n{ex.InnerException}"); + return false; + } + } + } + + public async Task DeleteCollectionShapshot(string collectionName, string snapshotName) + { + var exist = await DoesCollectionExist(collectionName); + if (!exist) + { + return false; + } + + try + { + var client = GetClient(); + await client.DeleteSnapshotAsync(collectionName, snapshotName); + return true; + } + catch + { + return false; + } + } + #endregion } diff --git a/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioStreamController.cs b/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioStreamController.cs index dcb33959..e69f79f6 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioStreamController.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioStreamController.cs @@ -35,21 +35,26 @@ public class TwilioStreamController : TwilioController throw new ArgumentNullException(nameof(VoiceRequest.CallSid)); } - VoiceResponse response = null; + VoiceResponse response = default!; + + if (request.AnsweredBy == "machine_start" && + request.Direction == "outbound-api" && + request.InitAudioFile != null) + { + response = new VoiceResponse(); + response.Play(new Uri($"{_settings.CallbackHost}/twilio/voice/speeches/{request.ConversationId}/{request.InitAudioFile}")); + return TwiML(response); + } + var instruction = new ConversationalVoiceResponse { SpeechPaths = [], ActionOnEmptyResult = true }; - if (_context.HttpContext.Request.Query.ContainsKey("init_audio_file")) + if (request.InitAudioFile != null) { - instruction.SpeechPaths.Add(_context.HttpContext.Request.Query["init_audio_file"]); - } - - if (_context.HttpContext.Request.Query.ContainsKey("conversation_id")) - { - request.ConversationId = _context.HttpContext.Request.Query["conversation_id"]; + instruction.SpeechPaths.Add(request.InitAudioFile); } await HookEmitter.Emit(_services, async hook => @@ -77,6 +82,24 @@ public class TwilioStreamController : TwilioController return TwiML(response); } + [ValidateRequest] + [HttpPost("twilio/stream/status")] + public async Task StreamConversationStatus(ConversationalVoiceRequest request) + { + if (request.AnsweredBy == "machine_start" && + request.Direction == "outbound-api" && + request.InitAudioFile != null && + request.CallStatus == "completed") + { + // voicemail + await HookEmitter.Emit(_services, async hook => + { + await hook.OnVoicemailLeft(request.ConversationId); + }); + } + return Ok(); + } + private async Task InitConversation(ConversationalVoiceRequest request) { var convService = _services.GetRequiredService(); @@ -104,6 +127,11 @@ public class TwilioStreamController : TwilioController new(StateConst.ROUTING_MODE, "lazy"), }; + if (request.InitAudioFile != null) + { + states.Add(new("init_audio_file", request.InitAudioFile)); + } + convService.SetConversationId(conversation.Id, states); convService.SaveStates(); diff --git a/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioVoiceController.cs b/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioVoiceController.cs index 807d7af8..6507a105 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioVoiceController.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioVoiceController.cs @@ -66,7 +66,7 @@ public class TwilioVoiceController : TwilioController }); request.ConversationId = $"TwilioVoice_{request.CallSid}"; - instruction.CallbackPath = $"twilio/voice/{request.ConversationId}/receive/0?{GenerateStatesParameter(request.States)}"; + instruction.CallbackPath = $"twilio/voice/receive/0?conversation-id={request.ConversationId}&{GenerateStatesParameter(request.States)}"; var twilio = _services.GetRequiredService(); if (string.IsNullOrWhiteSpace(request.Intent)) @@ -89,7 +89,7 @@ public class TwilioVoiceController : TwilioController }; await messageQueue.EnqueueAsync(callerMessage); response = new VoiceResponse(); - response.Redirect(new Uri($"{_settings.CallbackHost}/twilio/voice/{request.ConversationId}/reply/{seqNum}?{GenerateStatesParameter(request.States)}"), HttpMethod.Post); + response.Redirect(new Uri($"{_settings.CallbackHost}/twilio/voice/reply/{seqNum}?conversation-id={request.ConversationId}&{GenerateStatesParameter(request.States)}"), HttpMethod.Post); } await HookEmitter.Emit(_services, async hook => @@ -109,7 +109,7 @@ public class TwilioVoiceController : TwilioController /// /// [ValidateRequest] - [HttpPost("twilio/voice/{conversationId}/receive/{seqNum}")] + [HttpPost("twilio/voice/receive/{seqNum}")] public async Task ReceiveCallerMessage(ConversationalVoiceRequest request) { var twilio = _services.GetRequiredService(); @@ -142,7 +142,7 @@ public class TwilioVoiceController : TwilioController await messageQueue.EnqueueAsync(callerMessage); response = new VoiceResponse(); - response.Redirect(new Uri($"{_settings.CallbackHost}/twilio/voice/{request.ConversationId}/reply/{request.SeqNum}?{GenerateStatesParameter(request.States)}&AIResponseWaitTime=0"), HttpMethod.Post); + response.Redirect(new Uri($"{_settings.CallbackHost}/twilio/voice/reply/{request.SeqNum}?conversation-id={request.ConversationId}&{GenerateStatesParameter(request.States)}&AIResponseWaitTime=0"), HttpMethod.Post); await HookEmitter.Emit(_services, async hook => { @@ -173,7 +173,7 @@ public class TwilioVoiceController : TwilioController var instruction = new ConversationalVoiceResponse { SpeechPaths = new List(), - CallbackPath = $"twilio/voice/{request.ConversationId}/receive/{request.SeqNum}?{GenerateStatesParameter(request.States)}&attempts={++request.Attempts}", + CallbackPath = $"twilio/voice/receive/{request.SeqNum}?conversation-id={request.ConversationId}&{GenerateStatesParameter(request.States)}&attempts={++request.Attempts}", ActionOnEmptyResult = true }; @@ -203,7 +203,7 @@ public class TwilioVoiceController : TwilioController /// /// [ValidateRequest] - [HttpPost("twilio/voice/{conversationId}/reply/{seqNum}")] + [HttpPost("twilio/voice/reply/{seqNum}")] public async Task ReplyCallerMessage(ConversationalVoiceRequest request) { var nextSeqNum = request.SeqNum + 1; @@ -276,7 +276,7 @@ public class TwilioVoiceController : TwilioController var instruction = new ConversationalVoiceResponse { SpeechPaths = speechPaths, - CallbackPath = $"twilio/voice/{request.ConversationId}/reply/{request.SeqNum}?{GenerateStatesParameter(request.States)}&AIResponseWaitTime={++request.AIResponseWaitTime}", + CallbackPath = $"twilio/voice/reply/{request.SeqNum}?conversation-id={request.ConversationId}&{GenerateStatesParameter(request.States)}&AIResponseWaitTime={++request.AIResponseWaitTime}", ActionOnEmptyResult = true }; @@ -315,7 +315,7 @@ public class TwilioVoiceController : TwilioController var instruction = new ConversationalVoiceResponse { SpeechPaths = instructions, - CallbackPath = $"twilio/voice/{request.ConversationId}/reply/{request.SeqNum}?{GenerateStatesParameter(request.States)}&AIResponseWaitTime={++request.AIResponseWaitTime}", + CallbackPath = $"twilio/voice/reply/{request.SeqNum}?conversation-id={request.ConversationId}&{GenerateStatesParameter(request.States)}&AIResponseWaitTime={++request.AIResponseWaitTime}", ActionOnEmptyResult = true }; @@ -361,7 +361,7 @@ public class TwilioVoiceController : TwilioController var instruction = new ConversationalVoiceResponse { SpeechPaths = [$"twilio/voice/speeches/{request.ConversationId}/{reply.SpeechFileName}"], - CallbackPath = $"twilio/voice/{request.ConversationId}/receive/{nextSeqNum}?{GenerateStatesParameter(request.States)}", + CallbackPath = $"twilio/voice/receive/{nextSeqNum}?conversation-id={request.ConversationId}&{GenerateStatesParameter(request.States)}", ActionOnEmptyResult = true, Hints = reply.Hints }; @@ -388,7 +388,7 @@ public class TwilioVoiceController : TwilioController var instruction = new ConversationalVoiceResponse { ActionOnEmptyResult = true, - CallbackPath = $"twilio/voice/{conversationId}/receive/1", + CallbackPath = $"twilio/voice/receive/1?conversation-id={conversationId}", SpeechPaths = new List { $"twilio/voice/speeches/{conversationId}/intial.mp3" diff --git a/src/Plugins/BotSharp.Plugin.Twilio/Interfaces/ITwilioCallStatusHook.cs b/src/Plugins/BotSharp.Plugin.Twilio/Interfaces/ITwilioCallStatusHook.cs new file mode 100644 index 00000000..d35a2a35 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.Twilio/Interfaces/ITwilioCallStatusHook.cs @@ -0,0 +1,8 @@ +using Task = System.Threading.Tasks.Task; + +namespace BotSharp.Plugin.Twilio.Interfaces; + +public interface ITwilioCallStatusHook +{ + Task OnVoicemailLeft(string conversationId); +} diff --git a/src/Plugins/BotSharp.Plugin.Twilio/Models/ConversationalVoiceRequest.cs b/src/Plugins/BotSharp.Plugin.Twilio/Models/ConversationalVoiceRequest.cs index 33ee7633..25e3ba0d 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/Models/ConversationalVoiceRequest.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/Models/ConversationalVoiceRequest.cs @@ -7,7 +7,7 @@ public class ConversationalVoiceRequest : VoiceRequest [FromQuery(Name = "agent-id")] public string AgentId { get; set; } = string.Empty; - [FromRoute] + [FromQuery(Name = "conversation-id")] public string ConversationId { get; set; } = string.Empty; [FromRoute] @@ -19,5 +19,26 @@ public class ConversationalVoiceRequest : VoiceRequest public string Intent { get; set; } = string.Empty; + [FromQuery(Name = "init-audio-file")] + public string? InitAudioFile { get; set; } + public List States { get; set; } = []; + + [FromForm] + public string? CallbackSource { get; set; } + + /// + /// machine_start + /// + [FromForm] + public string? AnsweredBy { get; set; } + + [FromForm] + public int MachineDetectionDuration { get; set; } + + [FromForm] + public int Duration { get; set; } + + [FromForm] + public int CallDuration { get; set; } } diff --git a/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Functions/HangupPhoneCallFn.cs b/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Functions/HangupPhoneCallFn.cs index f54c1b42..cd755306 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Functions/HangupPhoneCallFn.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Functions/HangupPhoneCallFn.cs @@ -1,4 +1,7 @@ +using BotSharp.Plugin.Twilio.OutboundPhoneCallHandler.LlmContexts; +using Microsoft.VisualBasic; using Twilio.Rest.Api.V2010.Account; +using Task = System.Threading.Tasks.Task; namespace BotSharp.Plugin.Twilio.OutboundPhoneCallHandler.Functions; @@ -20,6 +23,7 @@ public class HangupPhoneCallFn : IFunctionCallback public async Task Execute(RoleDialogModel message) { + var args = JsonSerializer.Deserialize(message.FunctionArgs); var states = _services.GetRequiredService(); var callSid = states.GetState("twilio_call_sid"); @@ -30,14 +34,20 @@ public class HangupPhoneCallFn : IFunctionCallback return false; } - // Have to find the SID by the phone number - var call = CallResource.Update( - status: CallResource.UpdateStatusEnum.Completed, - pathSid: callSid - ); + message.Content = args.GoodbyeMessage; - message.Content = "The call has ended."; - message.StopCompletion = true; + _ = Task.Run(async () => + { + await Task.Delay(args.GoodbyeMessage.Split(' ').Length * 400); + // Have to find the SID by the phone number + var call = CallResource.Update( + status: CallResource.UpdateStatusEnum.Completed, + pathSid: callSid + ); + + message.Content = "The call has been ended."; + message.StopCompletion = true; + }); return true; } diff --git a/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Functions/OutboundPhoneCallFn.cs b/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Functions/OutboundPhoneCallFn.cs index df97cd44..cb707474 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Functions/OutboundPhoneCallFn.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Functions/OutboundPhoneCallFn.cs @@ -66,9 +66,12 @@ public class OutboundPhoneCallFn : IFunctionCallback // Make outbound call var call = await CallResource.CreateAsync( - url: new Uri($"{_twilioSetting.CallbackHost}/twilio/stream?conversation_id={newConversationId}&init_audio_file={fileName}"), + url: new Uri($"{_twilioSetting.CallbackHost}/twilio/stream?conversation-id={newConversationId}&init-audio-file={fileName}"), to: new PhoneNumber(args.PhoneNumber), - from: new PhoneNumber(_twilioSetting.PhoneNumber)); + from: new PhoneNumber(_twilioSetting.PhoneNumber), + statusCallback: new Uri($"{_twilioSetting.CallbackHost}/twilio/stream/status?conversation-id={newConversationId}&init-audio-file={fileName}"), + // https://www.twilio.com/docs/voice/answering-machine-detection + machineDetection: "Enable"); var convService = _services.GetRequiredService(); var routing = _services.GetRequiredService(); diff --git a/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/LlmContexts/HangupPhoneCallArgs.cs b/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/LlmContexts/HangupPhoneCallArgs.cs new file mode 100644 index 00000000..ea2075d9 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/LlmContexts/HangupPhoneCallArgs.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace BotSharp.Plugin.Twilio.OutboundPhoneCallHandler.LlmContexts; + +public class HangupPhoneCallArgs +{ + [JsonPropertyName("goodbye_message")] + public string? GoodbyeMessage { get; set; } +} diff --git a/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/LlmContexts/LlmContextIn.cs b/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/LlmContexts/LlmContextIn.cs index cde85aa9..3f7ea8b6 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/LlmContexts/LlmContextIn.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/LlmContexts/LlmContextIn.cs @@ -5,8 +5,8 @@ namespace BotSharp.Plugin.Twilio.OutboundPhoneCallHandler.LlmContexts; public class LlmContextIn { [JsonPropertyName("phone_number")] - public string PhoneNumber { get; set; } + public string PhoneNumber { get; set; } = null!; [JsonPropertyName("initial_message")] - public string InitialMessage { get; set; } + public string? InitialMessage { get; set; } } diff --git a/src/Plugins/BotSharp.Plugin.Twilio/Services/TwilioService.cs b/src/Plugins/BotSharp.Plugin.Twilio/Services/TwilioService.cs index 6f0364bb..b17f287a 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/Services/TwilioService.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/Services/TwilioService.cs @@ -199,6 +199,7 @@ public class TwilioService } } } + var connect = new Connect(); var host = _settings.CallbackHost.Split("://").Last(); connect.Stream(url: $"wss://{host}/twilio/stream/{conversationId}"); diff --git a/src/Plugins/BotSharp.Plugin.Twilio/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-twilio-hangup_phone_call.json b/src/Plugins/BotSharp.Plugin.Twilio/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-twilio-hangup_phone_call.json index 76b0e841..773fac23 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-twilio-hangup_phone_call.json +++ b/src/Plugins/BotSharp.Plugin.Twilio/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-twilio-hangup_phone_call.json @@ -5,7 +5,11 @@ "parameters": { "type": "object", "properties": { + "goodbye_message": { + "type": "string", + "description": "A polite closing statement for ending a conversation." + } }, - "required": [] + "required": [ "goodbye_message" ] } } \ No newline at end of file