diff --git a/src/Infrastructure/BotSharp.Abstraction/Conversations/ConversationHookBase.cs b/src/Infrastructure/BotSharp.Abstraction/Conversations/ConversationHookBase.cs index 06a97174..9239b865 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Conversations/ConversationHookBase.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Conversations/ConversationHookBase.cs @@ -79,4 +79,7 @@ public abstract class ConversationHookBase : IConversationHook public virtual Task OnBreakpointUpdated(string conversationId, bool resetStates) => Task.CompletedTask; + + public virtual Task OnNotificationGenerated(RoleDialogModel message) + => Task.CompletedTask; } diff --git a/src/Infrastructure/BotSharp.Abstraction/Conversations/Enums/MessageTypeName.cs b/src/Infrastructure/BotSharp.Abstraction/Conversations/Enums/MessageTypeName.cs new file mode 100644 index 00000000..c4e73d69 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Conversations/Enums/MessageTypeName.cs @@ -0,0 +1,7 @@ +namespace BotSharp.Abstraction.Conversations.Enums; + +public static class MessageTypeName +{ + public const string Plain = "plain"; + public const string Notification = "notification"; +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationHook.cs b/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationHook.cs index 9ed47c61..34371ba5 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationHook.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationHook.cs @@ -1,5 +1,3 @@ -using BotSharp.Abstraction.Functions.Models; - namespace BotSharp.Abstraction.Conversations; public interface IConversationHook @@ -107,4 +105,11 @@ public interface IConversationHook /// /// Task OnBreakpointUpdated(string conversationId, bool resetStates); + + /// + /// Generate a notification + /// + /// + /// + Task OnNotificationGenerated(RoleDialogModel message); } diff --git a/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationService.cs b/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationService.cs index bddc5426..7e7e372e 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationService.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationService.cs @@ -8,7 +8,7 @@ public interface IConversationService IConversationStateService States { get; } string ConversationId { get; } Task NewConversation(Conversation conversation); - void SetConversationId(string conversationId, List states); + void SetConversationId(string conversationId, List states, bool isReadOnly = false); Task GetConversation(string id); Task> GetConversations(ConversationFilter filter); Task UpdateConversationTitle(string id, string title); @@ -41,7 +41,7 @@ public interface IConversationService PostbackMessageModel? replyMessage, Func onResponseReceived); - List GetDialogHistory(int lastCount = 100, bool fromBreakpoint = true); + List GetDialogHistory(int lastCount = 100, bool fromBreakpoint = true, IEnumerable? includeMessageTypes = null); Task CleanHistory(string agentId); /// diff --git a/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationStorage.cs b/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationStorage.cs index 5f7066d2..a586e217 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationStorage.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationStorage.cs @@ -2,7 +2,6 @@ namespace BotSharp.Abstraction.Conversations; public interface IConversationStorage { - void InitStorage(string conversationId); void Append(string conversationId, RoleDialogModel dialog); List GetDialogs(string conversationId); } diff --git a/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/Conversation.cs b/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/Conversation.cs index ad7ffd04..890f3211 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/Conversation.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/Conversation.cs @@ -83,6 +83,9 @@ public class DialogMetaData [JsonPropertyName("message_id")] public string MessageId { get; set; } + [JsonPropertyName("message_type")] + public string MessageType { get; set; } + [JsonPropertyName("function_name")] public string? FunctionName { get; set; } diff --git a/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/RoleDialogModel.cs b/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/RoleDialogModel.cs index 5b8f7017..679527a3 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/RoleDialogModel.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/RoleDialogModel.cs @@ -1,3 +1,4 @@ +using BotSharp.Abstraction.Conversations.Enums; using BotSharp.Abstraction.Functions.Models; using BotSharp.Abstraction.Messaging; using BotSharp.Abstraction.Messaging.Models.RichContent; @@ -11,6 +12,11 @@ public class RoleDialogModel : ITrackableMessage /// public string MessageId { get; set; } + /// + /// The message type + /// + public string MessageType { get; set; } = MessageTypeName.Plain; + /// /// user, system, assistant, function /// @@ -101,6 +107,7 @@ public class RoleDialogModel : ITrackableMessage public List GeneratedImages { get; set; } = new List(); + private RoleDialogModel() { } @@ -110,6 +117,7 @@ public class RoleDialogModel : ITrackableMessage Role = role; Content = text; MessageId = Guid.NewGuid().ToString(); + MessageType = MessageTypeName.Plain; } public override string ToString() @@ -132,6 +140,7 @@ public class RoleDialogModel : ITrackableMessage { CurrentAgentId = source.CurrentAgentId, MessageId = source.MessageId, + MessageType = source.MessageType, FunctionArgs = source.FunctionArgs, FunctionName = source.FunctionName, ToolCallId = source.ToolCallId, diff --git a/src/Infrastructure/BotSharp.Abstraction/Loggers/Enums/ContentLogSource.cs b/src/Infrastructure/BotSharp.Abstraction/Loggers/Enums/ContentLogSource.cs index 45d74a51..78ab19ad 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Loggers/Enums/ContentLogSource.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Loggers/Enums/ContentLogSource.cs @@ -7,4 +7,5 @@ public static class ContentLogSource public const string FunctionCall = "function call"; public const string AgentResponse = "agent response"; public const string HardRule = "hard rule"; + public const string Notification = "notification"; } diff --git a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.Summary.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.Summary.cs index 0983e2ed..88d3a52d 100644 --- a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.Summary.cs +++ b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.Summary.cs @@ -1,3 +1,4 @@ +using BotSharp.Abstraction.Conversations.Enums; using BotSharp.Abstraction.MLTasks; using BotSharp.Abstraction.Templating; @@ -21,6 +22,7 @@ public partial class ConversationService if (dialogs.IsNullOrEmpty()) continue; + dialogs = dialogs.Where(x => x.MessageType != MessageTypeName.Notification).ToList(); var content = GetConversationContent(dialogs); if (string.IsNullOrWhiteSpace(content)) continue; diff --git a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.cs index 6e0f97d1..8511b873 100644 --- a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.cs +++ b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.cs @@ -106,7 +106,7 @@ public partial class ConversationService : IConversationService throw new NotImplementedException(); } - public List GetDialogHistory(int lastCount = 100, bool fromBreakpoint = true) + public List GetDialogHistory(int lastCount = 100, bool fromBreakpoint = true, IEnumerable? includeMessageTypes = null) { if (string.IsNullOrEmpty(_conversationId)) { @@ -115,6 +115,15 @@ public partial class ConversationService : IConversationService var dialogs = _storage.GetDialogs(_conversationId); + if (!includeMessageTypes.IsNullOrEmpty()) + { + dialogs = dialogs.Where(x => string.IsNullOrEmpty(x.MessageType) || includeMessageTypes.Contains(x.MessageType)).ToList(); + } + else + { + dialogs = dialogs.Where(x => string.IsNullOrEmpty(x.MessageType) || x.MessageType.IsEqualTo(MessageTypeName.Plain)).ToList(); + } + if (fromBreakpoint) { var db = _services.GetRequiredService(); @@ -134,7 +143,7 @@ public partial class ConversationService : IConversationService .ToList(); } - public void SetConversationId(string conversationId, List states) + public void SetConversationId(string conversationId, List states, bool isReadOnly = false) { _conversationId = conversationId; _state.Load(_conversationId); diff --git a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStorage.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStorage.cs index 5abc4fc5..ea145ac0 100644 --- a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStorage.cs +++ b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStorage.cs @@ -41,6 +41,7 @@ public class ConversationStorage : IConversationStorage Role = dialog.Role, AgentId = agentId, MessageId = dialog.MessageId, + MessageType = dialog.MessageType, FunctionName = dialog.FunctionName, CreateTime = dialog.CreatedAt }; @@ -65,6 +66,7 @@ public class ConversationStorage : IConversationStorage Role = dialog.Role, AgentId = agentId, MessageId = dialog.MessageId, + MessageType = dialog.MessageType, SenderId = dialog.SenderId, FunctionName = dialog.FunctionName, CreateTime = dialog.CreatedAt @@ -108,6 +110,7 @@ public class ConversationStorage : IConversationStorage var role = meta.Role; var currentAgentId = meta.AgentId; var messageId = meta.MessageId; + var messageType = meta.MessageType; var function = meta.FunctionName; var senderId = role == AgentRole.Function ? currentAgentId : meta.SenderId; var createdAt = meta.CreateTime; @@ -120,6 +123,7 @@ public class ConversationStorage : IConversationStorage { CurrentAgentId = currentAgentId, MessageId = messageId, + MessageType = messageType, CreatedAt = createdAt, SenderId = senderId, FunctionName = function, @@ -143,23 +147,4 @@ public class ConversationStorage : IConversationStorage return results; } - - public void InitStorage(string conversationId) - { - var file = GetStorageFile(conversationId); - if (!File.Exists(file)) - { - File.WriteAllLines(file, new string[0]); - } - } - - private string GetStorageFile(string conversationId) - { - var dir = Path.Combine(_dbSettings.FileRepository, "conversations", conversationId); - if (!Directory.Exists(dir)) - { - Directory.CreateDirectory(dir); - } - return Path.Combine(dir, "dialogs.txt"); - } } diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs index 5521b5e6..a020a490 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs @@ -1,8 +1,10 @@ +using Azure; using BotSharp.Abstraction.Files.Constants; using BotSharp.Abstraction.Files.Enums; using BotSharp.Abstraction.Options; using BotSharp.Abstraction.Routing; using BotSharp.Abstraction.Users.Enums; +using BotSharp.Core.Infrastructures; namespace BotSharp.OpenAPI.Controllers; @@ -251,6 +253,44 @@ public class ConversationController : ControllerBase return isSuccess ? newMessageId : string.Empty; } + #region Send notification + [HttpPost("/conversation/{conversationId}/notification")] + public async Task SendNotification([FromRoute] string conversationId, [FromBody] NewMessageModel input) + { + var conv = _services.GetRequiredService(); + var routing = _services.GetRequiredService(); + var userService = _services.GetRequiredService(); + + conv.SetConversationId(conversationId, new List(), isReadOnly: true); + + var inputMsg = new RoleDialogModel(AgentRole.User, input.Text) + { + MessageId = Guid.NewGuid().ToString(), + CreatedAt = DateTime.UtcNow + }; + + var user = await userService.GetUser(_user.Id); + var response = new ChatResponseModel() + { + ConversationId = conversationId, + MessageId = inputMsg.MessageId, + Sender = new UserViewModel + { + Id = user?.Id ?? string.Empty, + FirstName = user?.FirstName ?? string.Empty, + LastName = user?.LastName ?? string.Empty + }, + CreatedAt = DateTime.UtcNow + }; + + await HookEmitter.Emit(_services, async hook => + await hook.OnNotificationGenerated(inputMsg) + ); + + return response; + } + #endregion + #region Send message [HttpPost("/conversation/{agentId}/{conversationId}")] public async Task SendMessage([FromRoute] string agentId, diff --git a/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/ChatHubConversationHook.cs b/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/ChatHubConversationHook.cs index bf2292ea..80ee2299 100644 --- a/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/ChatHubConversationHook.cs +++ b/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/ChatHubConversationHook.cs @@ -15,6 +15,7 @@ public class ChatHubConversationHook : ConversationHookBase private const string RECEIVE_ASSISTANT_MESSAGE = "OnMessageReceivedFromAssistant"; private const string GENERATE_SENDER_ACTION = "OnSenderActionGenerated"; private const string DELETE_MESSAGE = "OnMessageDeleted"; + private const string GENERATE_NOTIFICATION = "OnNotificationGenerated"; #endregion public ChatHubConversationHook( @@ -117,6 +118,31 @@ public class ChatHubConversationHook : ConversationHookBase await base.OnResponseGenerated(message); } + + public override async Task OnNotificationGenerated(RoleDialogModel message) + { + var conv = _services.GetRequiredService(); + var json = JsonSerializer.Serialize(new ChatResponseModel() + { + ConversationId = conv.ConversationId, + MessageId = message.MessageId, + Text = !string.IsNullOrEmpty(message.SecondaryContent) ? message.SecondaryContent : message.Content, + Function = message.FunctionName, + RichContent = message.SecondaryRichContent ?? message.RichContent, + Data = message.Data, + Sender = new UserViewModel() + { + FirstName = "AI", + LastName = "Assistant", + Role = AgentRole.Assistant + } + }, _options.JsonSerializerOptions); + + await GenerateNotification(json); + await base.OnNotificationGenerated(message); + } + + public override async Task OnMessageDeleted(string conversationId, string messageId) { var model = new ChatResponseModel @@ -153,5 +179,10 @@ public class ChatHubConversationHook : ConversationHookBase { await _chatHub.Clients.User(_user.Id).SendAsync(DELETE_MESSAGE, model); } + + private async Task GenerateNotification(string? json) + { + await _chatHub.Clients.User(_user.Id).SendAsync(GENERATE_NOTIFICATION, json); + } #endregion } diff --git a/src/Plugins/BotSharp.Plugin.KnowledgeBase/Services/KnowledgeService.Document.cs b/src/Plugins/BotSharp.Plugin.KnowledgeBase/Services/KnowledgeService.Document.cs index 74469177..3959f45d 100644 --- a/src/Plugins/BotSharp.Plugin.KnowledgeBase/Services/KnowledgeService.Document.cs +++ b/src/Plugins/BotSharp.Plugin.KnowledgeBase/Services/KnowledgeService.Document.cs @@ -60,7 +60,20 @@ public partial class KnowledgeService } // Save to vector db - var dataIds = await SaveToVectorDb(collectionName, fileId, file.FileName, contents, file.FileSource); + var payload = new Dictionary() + { + { KnowledgePayloadName.DataSource, VectorDataSource.File }, + { KnowledgePayloadName.FileId, fileId.ToString() }, + { KnowledgePayloadName.FileName, file.FileName }, + { KnowledgePayloadName.FileSource, file.FileSource } + }; + + if (!string.IsNullOrWhiteSpace(file.FileUrl)) + { + payload[KnowledgePayloadName.FileUrl] = file.FileUrl; + } + + var dataIds = await SaveToVectorDb(collectionName, fileId, file.FileName, contents, payload); if (!dataIds.IsNullOrEmpty()) { db.SaveKnolwedgeBaseFileMeta(new KnowledgeDocMetaData @@ -119,7 +132,20 @@ public partial class KnowledgeService var fileId = Guid.NewGuid(); var contentType = FileUtility.GetFileContentType(fileName); - var dataIds = await SaveToVectorDb(collectionName, fileId, fileName, contents, fileSource, fileUrl: refData?.Url); + var payload = new Dictionary() + { + { KnowledgePayloadName.DataSource, VectorDataSource.File }, + { KnowledgePayloadName.FileId, fileId.ToString() }, + { KnowledgePayloadName.FileName, fileName }, + { KnowledgePayloadName.FileSource, fileSource } + }; + + if (!string.IsNullOrWhiteSpace(refData?.Url)) + { + payload[KnowledgePayloadName.FileUrl] = refData.Url; + } + + var dataIds = await SaveToVectorDb(collectionName, fileId, fileName, contents, payload); db.SaveKnolwedgeBaseFileMeta(new KnowledgeDocMetaData { Collection = collectionName, @@ -386,8 +412,7 @@ public partial class KnowledgeService } private async Task> SaveToVectorDb( - string collectionName, Guid fileId, string fileName, IEnumerable contents, - string fileSource = KnowledgeDocSource.Api, string vectorDataSource = VectorDataSource.File, string? fileUrl = null) + string collectionName, Guid fileId, string fileName, IEnumerable contents, Dictionary? payload = null) { if (contents.IsNullOrEmpty()) { @@ -398,25 +423,12 @@ public partial class KnowledgeService var vectorDb = GetVectorDb(); var textEmbedding = GetTextEmbedding(collectionName); - var payload = new Dictionary - { - { KnowledgePayloadName.DataSource, vectorDataSource }, - { KnowledgePayloadName.FileId, fileId.ToString() }, - { KnowledgePayloadName.FileName, fileName }, - { KnowledgePayloadName.FileSource, fileSource } - }; - - if (!string.IsNullOrWhiteSpace(fileUrl)) - { - payload[KnowledgePayloadName.FileUrl] = fileUrl; - } - for (int i = 0; i < contents.Count(); i++) { var content = contents.ElementAt(i); var vector = await textEmbedding.GetVectorAsync(content); var dataId = Guid.NewGuid(); - var saved = await vectorDb.Upsert(collectionName, dataId, vector, content, payload); + var saved = await vectorDb.Upsert(collectionName, dataId, vector, content, payload ?? new Dictionary()); if (!saved) continue; diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Models/DialogMongoElement.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Models/DialogMongoElement.cs index 5c2a1698..1179d3cd 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Models/DialogMongoElement.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Models/DialogMongoElement.cs @@ -48,6 +48,7 @@ public class DialogMetaDataMongoElement public string Role { get; set; } public string AgentId { get; set; } public string MessageId { get; set; } + public string MessageType { get; set; } public string? FunctionName { get; set; } public string? SenderId { get; set; } public DateTime CreateTime { get; set; } @@ -64,6 +65,7 @@ public class DialogMetaDataMongoElement Role = meta.Role, AgentId = meta.AgentId, MessageId = meta.MessageId, + MessageType = meta.MessageType, FunctionName = meta.FunctionName, SenderId = meta.SenderId, CreateTime = meta.CreateTime, @@ -77,6 +79,7 @@ public class DialogMetaDataMongoElement Role = meta.Role, AgentId = meta.AgentId, MessageId = meta.MessageId, + MessageType = meta.MessageType, FunctionName = meta.FunctionName, SenderId = meta.SenderId, CreateTime = meta.CreateTime, diff --git a/src/Plugins/BotSharp.Plugin.Qdrant/QdrantDb.cs b/src/Plugins/BotSharp.Plugin.Qdrant/QdrantDb.cs index 9dde5b35..ef9faa93 100644 --- a/src/Plugins/BotSharp.Plugin.Qdrant/QdrantDb.cs +++ b/src/Plugins/BotSharp.Plugin.Qdrant/QdrantDb.cs @@ -212,42 +212,45 @@ public class QdrantDb : IVectorDb { foreach (var item in payload) { - if (item.Value is string str) - { - point.Payload[item.Key] = str; - } - else if (item.Value is bool b) + var value = item.Value?.ToString(); + if (value == null) continue; + + if (bool.TryParse(value, out var b)) { point.Payload[item.Key] = b; } - else if (item.Value is byte int8) + else if (byte.TryParse(value, out var int8)) { point.Payload[item.Key] = int8; } - else if (item.Value is short int16) + else if (short.TryParse(value, out var int16)) { point.Payload[item.Key] = int16; } - else if (item.Value is int int32) + else if (int.TryParse(value, out var int32)) { point.Payload[item.Key] = int32; } - else if (item.Value is long int64) + else if (long.TryParse(value, out var int64)) { point.Payload[item.Key] = int64; } - else if (item.Value is float f32) + else if (float.TryParse(value, out var f32)) { point.Payload[item.Key] = f32; } - else if (item.Value is double f64) + else if (double.TryParse(value, out var f64)) { point.Payload[item.Key] = f64; } - else if (item.Value is DateTime dt) + else if (DateTime.TryParse(value, out var dt)) { point.Payload[item.Key] = dt.ToUniversalTime().ToString("o"); } + else + { + point.Payload[item.Key] = value; + } } }