diff --git a/BotSharp.sln b/BotSharp.sln index 972dc35f..4aa709de 100644 --- a/BotSharp.sln +++ b/BotSharp.sln @@ -143,6 +143,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.LLM.Tests", "tests EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Test.RealtimeVoice", "tests\BotSharp.Test.RealtimeVoice\BotSharp.Test.RealtimeVoice.csproj", "{B067B126-88CD-4282-BEEF-7369B64423EF}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Plugin.ChartHandler", "src\Plugins\BotSharp.Plugin.ChartHandler\BotSharp.Plugin.ChartHandler.csproj", "{0428DEAA-E4FE-4259-A6D8-6EDD1A9D0702}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -599,6 +601,14 @@ Global {B067B126-88CD-4282-BEEF-7369B64423EF}.Release|Any CPU.Build.0 = Release|Any CPU {B067B126-88CD-4282-BEEF-7369B64423EF}.Release|x64.ActiveCfg = Release|Any CPU {B067B126-88CD-4282-BEEF-7369B64423EF}.Release|x64.Build.0 = Release|Any CPU + {0428DEAA-E4FE-4259-A6D8-6EDD1A9D0702}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0428DEAA-E4FE-4259-A6D8-6EDD1A9D0702}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0428DEAA-E4FE-4259-A6D8-6EDD1A9D0702}.Debug|x64.ActiveCfg = Debug|Any CPU + {0428DEAA-E4FE-4259-A6D8-6EDD1A9D0702}.Debug|x64.Build.0 = Debug|Any CPU + {0428DEAA-E4FE-4259-A6D8-6EDD1A9D0702}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0428DEAA-E4FE-4259-A6D8-6EDD1A9D0702}.Release|Any CPU.Build.0 = Release|Any CPU + {0428DEAA-E4FE-4259-A6D8-6EDD1A9D0702}.Release|x64.ActiveCfg = Release|Any CPU + {0428DEAA-E4FE-4259-A6D8-6EDD1A9D0702}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -668,6 +678,7 @@ Global {7D0DB012-9798-4BB9-B15B-A5B0B7B3B094} = {32FAFFFE-A4CB-4FEE-BF7C-84518BBC6DCC} {7C0C7D13-D161-4AB0-9C29-83A0F1FF990E} = {32FAFFFE-A4CB-4FEE-BF7C-84518BBC6DCC} {B067B126-88CD-4282-BEEF-7369B64423EF} = {32FAFFFE-A4CB-4FEE-BF7C-84518BBC6DCC} + {0428DEAA-E4FE-4259-A6D8-6EDD1A9D0702} = {51AFE054-AE99-497D-A593-69BAEFB5106F} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {A9969D89-C98B-40A5-A12B-FC87E55B3A19} diff --git a/Directory.Packages.props b/Directory.Packages.props index 03798e37..b51dfc1e 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -38,7 +38,7 @@ - + @@ -46,7 +46,7 @@ - + @@ -74,7 +74,7 @@ - + diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/Constants/LlmConstant.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/Constants/LlmConstant.cs index 3919ca57..e45c8b15 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/Constants/LlmConstant.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/Constants/LlmConstant.cs @@ -3,4 +3,5 @@ namespace BotSharp.Abstraction.Agents.Constants; public static class LlmConstant { public const int DEFAULT_MAX_OUTPUT_TOKEN = 1024; + public const string DEFAULT_REASONING_EFFORT_LEVEL = "low"; } diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentLlmConfig.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentLlmConfig.cs index f1df3a01..75e3aa93 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentLlmConfig.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentLlmConfig.cs @@ -34,4 +34,11 @@ public class AgentLlmConfig [JsonPropertyName("max_output_tokens")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public int? MaxOutputTokens { get; set; } + + /// + /// Reasoning effort level + /// + [JsonPropertyName("reasoning_effort_level")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? ReasoningEffortLevel { get; set; } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Browsing/IWebBrowser.cs b/src/Infrastructure/BotSharp.Abstraction/Browsing/IWebBrowser.cs index 0e6b9ba3..eebf7bfe 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Browsing/IWebBrowser.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Browsing/IWebBrowser.cs @@ -30,4 +30,5 @@ public interface IWebBrowser Task SendHttpRequest(MessageInfo message, HttpRequestParams actionParams); Task GetAttributeValue(MessageInfo message, ElementLocatingArgs location); Task SetAttributeValue(MessageInfo message, ElementLocatingArgs location); + string GetCurrentUrl(MessageInfo message); } diff --git a/src/Infrastructure/BotSharp.Abstraction/Conversations/ConversationHookBase.cs b/src/Infrastructure/BotSharp.Abstraction/Conversations/ConversationHookBase.cs index 0a5244f8..855bd79e 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Conversations/ConversationHookBase.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Conversations/ConversationHookBase.cs @@ -49,10 +49,10 @@ public abstract class ConversationHookBase : IConversationHook public virtual Task OnHumanInterventionNeeded(RoleDialogModel message) => Task.CompletedTask; - public virtual Task OnFunctionExecuting(RoleDialogModel message, string from = InvokeSource.Manual) + public virtual Task OnFunctionExecuting(RoleDialogModel message, InvokeFunctionOptions? options = null) => Task.CompletedTask; - public virtual Task OnFunctionExecuted(RoleDialogModel message, string from = InvokeSource.Manual) + public virtual Task OnFunctionExecuted(RoleDialogModel message, InvokeFunctionOptions? options = null) => Task.CompletedTask; public virtual Task OnMessageReceived(RoleDialogModel message) diff --git a/src/Infrastructure/BotSharp.Abstraction/Conversations/Dtos/ChatResponseDto.cs b/src/Infrastructure/BotSharp.Abstraction/Conversations/Dtos/ChatResponseDto.cs index 19b5e8b2..9e4fcb41 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Conversations/Dtos/ChatResponseDto.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Conversations/Dtos/ChatResponseDto.cs @@ -32,6 +32,10 @@ public class ChatResponseDto : InstructResult [JsonPropertyName("payload")] public string? Payload { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("indication")] + public string? Indication { get; set; } + [JsonPropertyName("has_message_files")] public bool HasMessageFiles { get; set; } diff --git a/src/Infrastructure/BotSharp.Abstraction/Conversations/Enums/ChatEvent.cs b/src/Infrastructure/BotSharp.Abstraction/Conversations/Enums/ChatEvent.cs new file mode 100644 index 00000000..0d68fef7 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Conversations/Enums/ChatEvent.cs @@ -0,0 +1,22 @@ +namespace BotSharp.Abstraction.Conversations.Enums; + +public static class ChatEvent +{ + public const string OnConversationInitFromClient = nameof(OnConversationInitFromClient); + public const string OnMessageReceivedFromClient = nameof(OnMessageReceivedFromClient); + public const string OnMessageReceivedFromAssistant = nameof(OnMessageReceivedFromAssistant); + + public const string OnMessageDeleted = nameof(OnMessageDeleted); + public const string OnNotificationGenerated = nameof(OnNotificationGenerated); + public const string OnIndicationReceived = nameof(OnIndicationReceived); + + public const string OnConversationContentLogGenerated = nameof(OnConversationContentLogGenerated); + public const string OnConversateStateLogGenerated = nameof(OnConversateStateLogGenerated); + public const string OnAgentQueueChanged = nameof(OnAgentQueueChanged); + public const string OnStateChangeGenerated = nameof(OnStateChangeGenerated); + + public const string BeforeReceiveLlmStreamMessage = nameof(BeforeReceiveLlmStreamMessage); + public const string OnReceiveLlmStreamMessage = nameof(OnReceiveLlmStreamMessage); + public const string AfterReceiveLlmStreamMessage = nameof(AfterReceiveLlmStreamMessage); + public const string OnSenderActionGenerated = nameof(OnSenderActionGenerated); +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationHook.cs b/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationHook.cs index d1b6f83c..5a1d8aa2 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationHook.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationHook.cs @@ -61,7 +61,7 @@ public interface IConversationHook : IHookBase /// /// /// - Task OnFunctionExecuting(RoleDialogModel message, string from = InvokeSource.Manual); + Task OnFunctionExecuting(RoleDialogModel message, InvokeFunctionOptions? options = null); /// /// Triggered when the function calling completed. @@ -69,7 +69,7 @@ public interface IConversationHook : IHookBase /// /// /// - Task OnFunctionExecuted(RoleDialogModel message, string from = InvokeSource.Manual); + Task OnFunctionExecuted(RoleDialogModel message, InvokeFunctionOptions? options = null); Task OnResponseGenerated(RoleDialogModel message); diff --git a/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationProgressService.cs b/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationProgressService.cs deleted file mode 100644 index 8958cab5..00000000 --- a/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationProgressService.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace BotSharp.Abstraction.Conversations; - -public delegate Task FunctionExecuting(RoleDialogModel msg); -public delegate Task FunctionExecuted(RoleDialogModel msg); - -public interface IConversationProgressService -{ - FunctionExecuted OnFunctionExecuted { get; set; } - FunctionExecuting OnFunctionExecuting { get; set; } -} diff --git a/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/ConversationSenderActionModel.cs b/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/ConversationSenderActionModel.cs index b49fc55d..502cca4c 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/ConversationSenderActionModel.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/ConversationSenderActionModel.cs @@ -1,5 +1,3 @@ -using BotSharp.Abstraction.Messaging.Enums; - namespace BotSharp.Abstraction.Conversations.Models; public class ConversationSenderActionModel diff --git a/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/RoleDialogModel.cs b/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/RoleDialogModel.cs index 6a8af2d6..6bc82f81 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/RoleDialogModel.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/RoleDialogModel.cs @@ -120,7 +120,7 @@ public class RoleDialogModel : ITrackableMessage [JsonIgnore(Condition = JsonIgnoreCondition.Always)] public bool IsStreaming { get; set; } - private RoleDialogModel() + public RoleDialogModel() { } diff --git a/src/Infrastructure/BotSharp.Abstraction/Knowledges/IKnowledgeService.cs b/src/Infrastructure/BotSharp.Abstraction/Knowledges/IKnowledgeService.cs index 688e939e..d7d9d528 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Knowledges/IKnowledgeService.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Knowledges/IKnowledgeService.cs @@ -1,4 +1,5 @@ using BotSharp.Abstraction.Graph.Models; +using BotSharp.Abstraction.Models; using BotSharp.Abstraction.VectorStorage.Models; namespace BotSharp.Abstraction.Knowledges; @@ -13,6 +14,7 @@ public interface IKnowledgeService Task GetVectorCollectionDetails(string collectionName); Task> SearchVectorKnowledge(string query, string collectionName, VectorSearchOptions options); Task> GetPagedVectorCollectionData(string collectionName, VectorFilter filter); + Task> GetVectorCollectionData(string collectionName, IEnumerable ids, VectorQueryOptions? options = null); Task DeleteVectorCollectionData(string collectionName, string id); Task DeleteVectorCollectionAllData(string collectionName); Task CreateVectorCollectionData(string collectionName, VectorCreateModel create); @@ -69,6 +71,11 @@ public interface IKnowledgeService Task DeleteVectorCollectionSnapshot(string collectionName, string snapshotName); #endregion + #region Index + Task> CreateVectorCollectionPayloadIndexes(string collectionName, IEnumerable options); + Task> DeleteVectorCollectionPayloadIndexes(string collectionName, IEnumerable options); + #endregion + #region Common Task RefreshVectorKnowledgeConfigs(VectorCollectionConfigsModel configs); #endregion diff --git a/src/Infrastructure/BotSharp.Abstraction/MLTasks/Settings/LlmModelSetting.cs b/src/Infrastructure/BotSharp.Abstraction/MLTasks/Settings/LlmModelSetting.cs index e74a7c37..23e055d2 100644 --- a/src/Infrastructure/BotSharp.Abstraction/MLTasks/Settings/LlmModelSetting.cs +++ b/src/Infrastructure/BotSharp.Abstraction/MLTasks/Settings/LlmModelSetting.cs @@ -47,7 +47,15 @@ public class LlmModelSetting /// public int Dimension { get; set; } - public LlmCost Cost { get; set; } = new(); + /// + /// Settings for reasoning model + /// + public ReasoningSetting? Reasoning { get; set; } + + /// + /// Settings for llm cost + /// + public LlmCostSetting Cost { get; set; } = new(); public override string ToString() { @@ -55,10 +63,16 @@ public class LlmModelSetting } } +public class ReasoningSetting +{ + public float Temperature { get; set; } = 1.0f; + public string? EffortLevel { get; set; } +} + /// /// Cost per 1K tokens /// -public class LlmCost +public class LlmCostSetting { // Input public float TextInputCost { get; set; } = 0f; diff --git a/src/Infrastructure/BotSharp.Abstraction/MessageHub/Models/HubObserveData.cs b/src/Infrastructure/BotSharp.Abstraction/MessageHub/Models/HubObserveData.cs new file mode 100644 index 00000000..d345ab86 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/MessageHub/Models/HubObserveData.cs @@ -0,0 +1,6 @@ +namespace BotSharp.Abstraction.MessageHub.Models; + +public class HubObserveData : ObserveDataBase where TData : class, new() +{ + public TData Data { get; set; } = null!; +} diff --git a/src/Infrastructure/BotSharp.Abstraction/MessageHub/Models/ObserveDataBase.cs b/src/Infrastructure/BotSharp.Abstraction/MessageHub/Models/ObserveDataBase.cs new file mode 100644 index 00000000..c7d5c0f6 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/MessageHub/Models/ObserveDataBase.cs @@ -0,0 +1,7 @@ +namespace BotSharp.Abstraction.MessageHub.Models; + +public class ObserveDataBase +{ + public string EventName { get; set; } = null!; + public string RefId { get; set; } = null!; +} diff --git a/src/Infrastructure/BotSharp.Abstraction/MessageHub/Models/ObserverSubscription.cs b/src/Infrastructure/BotSharp.Abstraction/MessageHub/Models/ObserverSubscription.cs new file mode 100644 index 00000000..0ba75531 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/MessageHub/Models/ObserverSubscription.cs @@ -0,0 +1,28 @@ +using BotSharp.Abstraction.MessageHub.Observers; + +namespace BotSharp.Abstraction.MessageHub.Models; + +public class ObserverSubscription +{ + public IBotSharpObserver Observer { get; set; } + public IDisposable Subscription { get; set; } + + public ObserverSubscription() + { + + } + + public ObserverSubscription( + IBotSharpObserver observer, + IDisposable subscription) + { + Observer = observer; + Subscription = subscription; + } + + public void UnSubscribe() + { + Observer.Deactivate(); + Subscription.Dispose(); + } +} diff --git a/src/Infrastructure/BotSharp.Abstraction/MessageHub/Observers/BotSharpObserverBase.cs b/src/Infrastructure/BotSharp.Abstraction/MessageHub/Observers/BotSharpObserverBase.cs new file mode 100644 index 00000000..1381ea03 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/MessageHub/Observers/BotSharpObserverBase.cs @@ -0,0 +1,48 @@ + +namespace BotSharp.Abstraction.MessageHub.Observers; + +public abstract class BotSharpObserverBase : IBotSharpObserver +{ + private bool _active = false; + protected Dictionary> _listeners = []; + + protected BotSharpObserverBase() + { + + } + + public virtual string Name => string.Empty; + + public virtual bool Active => _active; + + public virtual void Activate() + { + _active = true; + } + + public virtual void Deactivate() + { + _active = false; + _listeners = []; + } + + public virtual void SetEventListeners(Dictionary> listeners) + { + _listeners = listeners; + } + + public virtual void OnCompleted() + { + + } + + public virtual void OnError(Exception error) + { + + } + + public virtual void OnNext(T value) + { + + } +} diff --git a/src/Infrastructure/BotSharp.Abstraction/MessageHub/Observers/IBotSharpObserver.cs b/src/Infrastructure/BotSharp.Abstraction/MessageHub/Observers/IBotSharpObserver.cs new file mode 100644 index 00000000..158ef4a6 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/MessageHub/Observers/IBotSharpObserver.cs @@ -0,0 +1,11 @@ +namespace BotSharp.Abstraction.MessageHub.Observers; + +public interface IBotSharpObserver : IObserver +{ + string Name { get; } + bool Active { get; } + + void SetEventListeners(Dictionary> listeners); + void Activate(); + void Deactivate(); +} diff --git a/src/Infrastructure/BotSharp.Abstraction/MessageHub/Services/IObserverService.cs b/src/Infrastructure/BotSharp.Abstraction/MessageHub/Services/IObserverService.cs new file mode 100644 index 00000000..582849aa --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/MessageHub/Services/IObserverService.cs @@ -0,0 +1,13 @@ +using BotSharp.Abstraction.MessageHub.Models; + +namespace BotSharp.Abstraction.MessageHub.Services; + +public interface IObserverService +{ + IDisposable SubscribeObservers( + string refId, + IEnumerable? names = null, + Dictionary>? listeners = null) where T : ObserveDataBase; + + void UnSubscribeObservers(IEnumerable? names = null) where T : ObserveDataBase; +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Messaging/BotSharpMessageParser.cs b/src/Infrastructure/BotSharp.Abstraction/Messaging/BotSharpMessageParser.cs index 1f495a44..25a97b2b 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Messaging/BotSharpMessageParser.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Messaging/BotSharpMessageParser.cs @@ -1,7 +1,7 @@ -using BotSharp.Abstraction.Messaging.Models.RichContent.Template; -using BotSharp.Abstraction.Messaging.Models.RichContent; using System.Text.Json; using System.Reflection; +using BotSharp.Abstraction.Messaging.Models.RichContent.Template; +using BotSharp.Abstraction.Messaging.Models.RichContent; namespace BotSharp.Abstraction.Messaging; @@ -38,6 +38,10 @@ public static class BotSharpMessageParser { targetType = typeof(TextMessage); } + else if (richType == RichTypeEnum.ProgramCode) + { + targetType = typeof(ProgramCodeTemplateMessage); + } else if (richType == RichTypeEnum.GenericTemplate) { if (root.TryGetProperty("element_type", out element)) @@ -84,9 +88,9 @@ public static class BotSharpMessageParser { targetType = typeof(CouponTemplateMessage); } - else if (templateType == TemplateTypeEnum.Product) + else if (templateType == TemplateTypeEnum.ProgramCode) { - targetType = typeof(ProductTemplateMessage); + targetType = typeof(ProgramCodeTemplateMessage); } else if (templateType == TemplateTypeEnum.Generic) { diff --git a/src/Infrastructure/BotSharp.Abstraction/Messaging/Enums/RichTypeEnum.cs b/src/Infrastructure/BotSharp.Abstraction/Messaging/Enums/RichTypeEnum.cs index 7603f790..8d66904b 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Messaging/Enums/RichTypeEnum.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Messaging/Enums/RichTypeEnum.cs @@ -9,4 +9,5 @@ public static class RichTypeEnum public const string QuickReply = "quick_reply"; public const string Text = "text"; public const string Attachment = "attachment"; + public const string ProgramCode = "program_code"; } diff --git a/src/Infrastructure/BotSharp.Abstraction/Messaging/Enums/TemplateTypeEnum.cs b/src/Infrastructure/BotSharp.Abstraction/Messaging/Enums/TemplateTypeEnum.cs index 74f88735..73a4e9d4 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Messaging/Enums/TemplateTypeEnum.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Messaging/Enums/TemplateTypeEnum.cs @@ -6,5 +6,5 @@ public static class TemplateTypeEnum public const string Coupon = "coupon"; public const string Generic = "generic"; public const string MultiSelect = "multi-select"; - public const string Product = "product"; + public const string ProgramCode = "program_code"; } diff --git a/src/Infrastructure/BotSharp.Abstraction/Messaging/Models/RichContent/Template/ProductTemplateMessage.cs b/src/Infrastructure/BotSharp.Abstraction/Messaging/Models/RichContent/Template/ProductTemplateMessage.cs deleted file mode 100644 index 05af097a..00000000 --- a/src/Infrastructure/BotSharp.Abstraction/Messaging/Models/RichContent/Template/ProductTemplateMessage.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace BotSharp.Abstraction.Messaging.Models.RichContent.Template; - -public class ProductTemplateMessage : IRichMessage, ITemplateMessage -{ - [JsonPropertyName("rich_type")] - public string RichType => RichTypeEnum.GenericTemplate; - - [JsonPropertyName("text")] - [Translate] - public string Text { get; set; } = string.Empty; - - [JsonPropertyName("template_type")] - public string TemplateType => TemplateTypeEnum.Product; -} - -public class ProductElement -{ - public string Id { get; set; } -} diff --git a/src/Infrastructure/BotSharp.Abstraction/Messaging/Models/RichContent/Template/ProgramCodeTemplateMessage.cs b/src/Infrastructure/BotSharp.Abstraction/Messaging/Models/RichContent/Template/ProgramCodeTemplateMessage.cs new file mode 100644 index 00000000..60f6464a --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Messaging/Models/RichContent/Template/ProgramCodeTemplateMessage.cs @@ -0,0 +1,16 @@ +namespace BotSharp.Abstraction.Messaging.Models.RichContent.Template; + +public class ProgramCodeTemplateMessage : IRichMessage, ITemplateMessage +{ + [JsonPropertyName("rich_type")] + public string RichType => RichTypeEnum.ProgramCode; + + [JsonPropertyName("text")] + public string Text { get; set; } = string.Empty; + + [JsonPropertyName("template_type")] + public virtual string TemplateType { get; set; } = TemplateTypeEnum.ProgramCode; + + [JsonPropertyName("language")] + public string Language { get; set; } = string.Empty; +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Models/SuccessFailResponse.cs b/src/Infrastructure/BotSharp.Abstraction/Models/SuccessFailResponse.cs new file mode 100644 index 00000000..11985259 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Models/SuccessFailResponse.cs @@ -0,0 +1,7 @@ +namespace BotSharp.Abstraction.Models; + +public class SuccessFailResponse +{ + public List Success { get; set; } = []; + public List Fail { get; set; } = []; +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Observables/Models/HubObserveData.cs b/src/Infrastructure/BotSharp.Abstraction/Observables/Models/HubObserveData.cs deleted file mode 100644 index bf771cb4..00000000 --- a/src/Infrastructure/BotSharp.Abstraction/Observables/Models/HubObserveData.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace BotSharp.Abstraction.Observables.Models; - -public class HubObserveData : ObserveDataBase -{ - public string EventName { get; set; } = null!; - public RoleDialogModel Data { get; set; } = null!; -} diff --git a/src/Infrastructure/BotSharp.Abstraction/Observables/Models/ObserveDataBase.cs b/src/Infrastructure/BotSharp.Abstraction/Observables/Models/ObserveDataBase.cs deleted file mode 100644 index 17773272..00000000 --- a/src/Infrastructure/BotSharp.Abstraction/Observables/Models/ObserveDataBase.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace BotSharp.Abstraction.Observables.Models; - -public abstract class ObserveDataBase -{ - public IServiceProvider ServiceProvider { get; set; } = null!; -} diff --git a/src/Infrastructure/BotSharp.Abstraction/Plugins/PluginLoaderSettings.cs b/src/Infrastructure/BotSharp.Abstraction/Plugins/PluginLoaderSettings.cs index 1a08fc5b..70564846 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Plugins/PluginLoaderSettings.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Plugins/PluginLoaderSettings.cs @@ -3,6 +3,4 @@ namespace BotSharp.Abstraction.Plugins; public class PluginSettings { public string[] Assemblies { get; set; } = new string[0]; - - public string[] ExcludedFunctions { get; set; } = new string[0]; } diff --git a/src/Infrastructure/BotSharp.Abstraction/Realtime/IRealtimeHook.cs b/src/Infrastructure/BotSharp.Abstraction/Realtime/IRealtimeHook.cs index 51d7383a..b4fed514 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Realtime/IRealtimeHook.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Realtime/IRealtimeHook.cs @@ -6,8 +6,15 @@ namespace BotSharp.Abstraction.Realtime; public interface IRealtimeHook : IHookBase { - Task OnModelReady(Agent agent, IRealTimeCompletion completer); - string[] OnModelTranscriptPrompt(Agent agent); - Task OnTranscribeCompleted(RoleDialogModel message, TranscriptionData data); - Task ShouldReconnect(RealtimeHubConnection conn) => Task.FromResult(false); + Task OnModelReady(Agent agent, IRealTimeCompletion completer) + => Task.CompletedTask; + + string[] OnModelTranscriptPrompt(Agent agent) + => []; + + Task OnTranscribeCompleted(RoleDialogModel message, TranscriptionData data) + => Task.CompletedTask; + + Task ShouldReconnect(RealtimeHubConnection conn, RoleDialogModel message) + => Task.FromResult(false); } diff --git a/src/Infrastructure/BotSharp.Abstraction/Repositories/Filters/ConversationFilter.cs b/src/Infrastructure/BotSharp.Abstraction/Repositories/Filters/ConversationFilter.cs index b94876c3..e274adb7 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Repositories/Filters/ConversationFilter.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Repositories/Filters/ConversationFilter.cs @@ -16,6 +16,7 @@ public class ConversationFilter public string? ChannelId { get; set; } public string? UserId { get; set; } public DateTime? StartTime { get; set; } + public DateTime? EndTime { get; set; } /// /// Agent task id diff --git a/src/Infrastructure/BotSharp.Abstraction/Repositories/Filters/ConversationStateKeysFilter.cs b/src/Infrastructure/BotSharp.Abstraction/Repositories/Filters/ConversationStateKeysFilter.cs index bff4b43c..201a031c 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Repositories/Filters/ConversationStateKeysFilter.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Repositories/Filters/ConversationStateKeysFilter.cs @@ -8,6 +8,8 @@ public class ConversationStateKeysFilter public bool PreLoad { get; set; } public List? AgentIds { get; set; } public List? UserIds { get; set; } + public DateTime? StartTime { get; set; } + public DateTime? EndTime { get; set; } public ConversationStateKeysFilter() { diff --git a/src/Infrastructure/BotSharp.Abstraction/Repositories/Filters/InstructLogFilter.cs b/src/Infrastructure/BotSharp.Abstraction/Repositories/Filters/InstructLogFilter.cs index c1040f94..aeec7b18 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Repositories/Filters/InstructLogFilter.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Repositories/Filters/InstructLogFilter.cs @@ -8,6 +8,8 @@ public class InstructLogFilter : Pagination public List? TemplateNames { get; set; } public List? UserIds { get; set; } public List? States { get; set; } + public DateTime? StartTime { get; set; } + public DateTime? EndTime { get; set; } public static InstructLogFilter Empty() { diff --git a/src/Infrastructure/BotSharp.Abstraction/Repositories/Filters/InstructLogKeysFilter.cs b/src/Infrastructure/BotSharp.Abstraction/Repositories/Filters/InstructLogKeysFilter.cs index c3eade89..ef04ede6 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Repositories/Filters/InstructLogKeysFilter.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Repositories/Filters/InstructLogKeysFilter.cs @@ -8,6 +8,8 @@ public class InstructLogKeysFilter public bool PreLoad { get; set; } public List? AgentIds { get; set; } public List? UserIds { get; set; } + public DateTime? StartTime { get; set; } + public DateTime? EndTime { get; set; } public InstructLogKeysFilter() { diff --git a/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs b/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs index d608d71f..8e0e2d55 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs @@ -56,7 +56,7 @@ public interface IBotSharpRepository : IHaveServiceProvider void UpdateUserPhone(string userId, string Iphone, string regionCode) => throw new NotImplementedException(); void UpdateUserIsDisable(string userId, bool isDisable) => throw new NotImplementedException(); void UpdateUsersIsDisable(List userIds, bool isDisable) => throw new NotImplementedException(); - PagedItems GetUsers(UserFilter filter) => throw new NotImplementedException(); + ValueTask> GetUsers(UserFilter filter) => throw new NotImplementedException(); List SearchLoginUsers(User filter, string source = UserSource.Internal) =>throw new NotImplementedException(); User? GetUserDetails(string userId, bool includeAgent = false) => throw new NotImplementedException(); bool UpdateUser(User user, bool updateUserAgents = false) => throw new NotImplementedException(); @@ -93,7 +93,7 @@ public interface IBotSharpRepository : IHaveServiceProvider #endregion #region Agent Task - PagedItems GetAgentTasks(AgentTaskFilter filter) + ValueTask> GetAgentTasks(AgentTaskFilter filter) => throw new NotImplementedException(); AgentTask? GetAgentTask(string agentId, string taskId) => throw new NotImplementedException(); @@ -126,7 +126,7 @@ public interface IBotSharpRepository : IHaveServiceProvider => throw new NotImplementedException(); Conversation GetConversation(string conversationId, bool isLoadStates = false) => throw new NotImplementedException(); - PagedItems GetConversations(ConversationFilter filter) + ValueTask> GetConversations(ConversationFilter filter) => throw new NotImplementedException(); void UpdateConversationTitle(string conversationId, string title) => throw new NotImplementedException(); @@ -179,7 +179,7 @@ public interface IBotSharpRepository : IHaveServiceProvider bool SaveInstructionLogs(IEnumerable logs) => throw new NotImplementedException(); - PagedItems GetInstructionLogs(InstructLogFilter filter) + ValueTask> GetInstructionLogs(InstructLogFilter filter) => throw new NotImplementedException(); List GetInstructionLogSearchKeys(InstructLogKeysFilter filter) @@ -227,7 +227,7 @@ public interface IBotSharpRepository : IHaveServiceProvider /// bool DeleteKnolwedgeBaseFileMeta(string collectionName, string vectorStoreProvider, Guid? fileId = null) => throw new NotImplementedException(); - PagedItems GetKnowledgeBaseFileMeta(string collectionName, string vectorStoreProvider, KnowledgeFileFilter filter) + ValueTask> GetKnowledgeBaseFileMeta(string collectionName, string vectorStoreProvider, KnowledgeFileFilter filter) => throw new NotImplementedException(); #endregion @@ -236,7 +236,7 @@ public interface IBotSharpRepository : IHaveServiceProvider => throw new NotImplementedException(); bool DeleteCrontabItem(string conversationId) => throw new NotImplementedException(); - PagedItems GetCrontabItems(CrontabItemFilter filter) + ValueTask> GetCrontabItems(CrontabItemFilter filter) => throw new NotImplementedException(); #endregion } diff --git a/src/Infrastructure/BotSharp.Abstraction/Routing/IRoutingService.cs b/src/Infrastructure/BotSharp.Abstraction/Routing/IRoutingService.cs index 56c98a82..d387fab1 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Routing/IRoutingService.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Routing/IRoutingService.cs @@ -26,8 +26,8 @@ public interface IRoutingService /// RoutingRule[] GetRulesByAgentId(string id); - Task InvokeAgent(string agentId, List dialogs, string from = InvokeSource.Manual, bool useStream = false); - Task InvokeFunction(string name, RoleDialogModel messages, string from = InvokeSource.Manual); + Task InvokeAgent(string agentId, List dialogs, InvokeAgentOptions? options = null); + Task InvokeFunction(string name, RoleDialogModel messages, InvokeFunctionOptions? options = null); Task InstructLoop(Agent agent, RoleDialogModel message, List dialogs); /// diff --git a/src/Infrastructure/BotSharp.Abstraction/Routing/Models/InvokeOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Routing/Models/InvokeOptions.cs new file mode 100644 index 00000000..5e031787 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Routing/Models/InvokeOptions.cs @@ -0,0 +1,31 @@ +namespace BotSharp.Abstraction.Routing.Models; + +public abstract class InvokeOptions +{ + public string From { get; set; } +} + +public class InvokeAgentOptions : InvokeOptions +{ + public bool UseStream { get; set; } + + public static InvokeAgentOptions Default() + { + return new() + { + From = InvokeSource.Manual, + UseStream = false + }; + } +} + +public class InvokeFunctionOptions : InvokeOptions +{ + public static InvokeFunctionOptions Default() + { + return new() + { + From = InvokeSource.Manual + }; + } +} \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Abstraction/SideCar/Attributes/SideCarAttribute.cs b/src/Infrastructure/BotSharp.Abstraction/SideCar/Attributes/SideCarAttribute.cs index 4426d71f..1d93dcba 100644 --- a/src/Infrastructure/BotSharp.Abstraction/SideCar/Attributes/SideCarAttribute.cs +++ b/src/Infrastructure/BotSharp.Abstraction/SideCar/Attributes/SideCarAttribute.cs @@ -23,7 +23,12 @@ public class SideCarAttribute : AsyncMoAttribute var instance = context.Target; var retType = context.ReturnType; - var serviceProvider = ((IHaveServiceProvider)instance).ServiceProvider; + var serviceProvider = (instance as IHaveServiceProvider)?.ServiceProvider; + if (serviceProvider == null) + { + return; + } + var (sidecar, sidecarMethod) = GetSideCarMethod(serviceProvider, methodName, retType, methodArgs); if (sidecar == null || sidecarMethod == null) { diff --git a/src/Infrastructure/BotSharp.Abstraction/SideCar/Models/SideCarOptions.cs b/src/Infrastructure/BotSharp.Abstraction/SideCar/Models/SideCarOptions.cs index f221cab2..bc6609c0 100644 --- a/src/Infrastructure/BotSharp.Abstraction/SideCar/Models/SideCarOptions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/SideCar/Models/SideCarOptions.cs @@ -3,19 +3,23 @@ namespace BotSharp.Abstraction.SideCar.Models; public class SideCarOptions { public bool IsInheritStates { get; set; } - public IEnumerable? InheritStateKeys { get; set; } + public HashSet? InheritStateKeys { get; set; } + public HashSet? ExcludedStateKeys { get; set; } public static SideCarOptions Empty() { return new(); } - public static SideCarOptions InheritStates(IEnumerable? targetStates = null) + public static SideCarOptions InheritStates( + HashSet? includedStates = null, + HashSet? excludedStates = null) { return new() { IsInheritStates = true, - InheritStateKeys = targetStates + InheritStateKeys = includedStates, + ExcludedStateKeys = excludedStates }; } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Users/IUserService.cs b/src/Infrastructure/BotSharp.Abstraction/Users/IUserService.cs index 1499ea17..d03687ff 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Users/IUserService.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Users/IUserService.cs @@ -7,6 +7,7 @@ namespace BotSharp.Abstraction.Users; public interface IUserService { Task GetUser(string id); + Task> GetUsers(List ids); Task> GetUsers(UserFilter filter); Task> SearchLoginUsers(User filter); Task GetUserDetails(string userId, bool includeAgent = false); diff --git a/src/Infrastructure/BotSharp.Abstraction/Utilities/Pagination.cs b/src/Infrastructure/BotSharp.Abstraction/Utilities/Pagination.cs index 4d21013b..c63b0056 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Utilities/Pagination.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Utilities/Pagination.cs @@ -50,6 +50,6 @@ public class Pagination : ICacheKey public class PagedItems { - public int Count { get; set; } + public long Count { get; set; } public IEnumerable Items { get; set; } = new List(); } diff --git a/src/Infrastructure/BotSharp.Abstraction/VectorStorage/IVectorDb.cs b/src/Infrastructure/BotSharp.Abstraction/VectorStorage/IVectorDb.cs index cb830bcc..95d6257c 100644 --- a/src/Infrastructure/BotSharp.Abstraction/VectorStorage/IVectorDb.cs +++ b/src/Infrastructure/BotSharp.Abstraction/VectorStorage/IVectorDb.cs @@ -14,8 +14,7 @@ public interface IVectorDb => throw new NotImplementedException(); Task> GetPagedCollectionData(string collectionName, VectorFilter filter) => throw new NotImplementedException(); - Task> GetCollectionData(string collectionName, IEnumerable ids, - bool withPayload = false, bool withVector = false) + Task> GetCollectionData(string collectionName, IEnumerable ids, VectorQueryOptions? options = null) => throw new NotImplementedException(); Task CreateCollection(string collectionName, int dimension) => throw new NotImplementedException(); @@ -23,8 +22,7 @@ public interface IVectorDb => 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) + Task> Search(string collectionName, float[] vector, VectorSearchOptions? options = null) => throw new NotImplementedException(); Task DeleteCollectionData(string collectionName, List ids) => throw new NotImplementedException(); @@ -40,4 +38,9 @@ public interface IVectorDb => throw new NotImplementedException(); Task DeleteCollectionShapshot(string collectionName, string snapshotName) => throw new NotImplementedException(); + + Task CreateCollectionPayloadIndex(string collectionName, CreateVectorCollectionIndexOptions options) + => throw new NotImplementedException(); + Task DeleteCollectionPayloadIndex(string collectionName, DeleteVectorCollectionIndexOptions options) + => throw new NotImplementedException(); } diff --git a/src/Infrastructure/BotSharp.Abstraction/VectorStorage/Models/VectorCollectionIndexOptions.cs b/src/Infrastructure/BotSharp.Abstraction/VectorStorage/Models/VectorCollectionIndexOptions.cs new file mode 100644 index 00000000..809bbf6b --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/VectorStorage/Models/VectorCollectionIndexOptions.cs @@ -0,0 +1,18 @@ +namespace BotSharp.Abstraction.VectorStorage.Models; + +public class VectorCollectionIndexOptions +{ + [JsonPropertyName("field_name")] + public string FieldName { get; set; } = null!; +} + +public class CreateVectorCollectionIndexOptions : VectorCollectionIndexOptions +{ + [JsonPropertyName("field_schema_type")] + public string FieldSchemaType { get; set; } = null!; +} + +public class DeleteVectorCollectionIndexOptions : VectorCollectionIndexOptions +{ + +} \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Abstraction/VectorStorage/Models/VectorFilter.cs b/src/Infrastructure/BotSharp.Abstraction/VectorStorage/Models/VectorFilter.cs index 584e8653..43ce6e04 100644 --- a/src/Infrastructure/BotSharp.Abstraction/VectorStorage/Models/VectorFilter.cs +++ b/src/Infrastructure/BotSharp.Abstraction/VectorStorage/Models/VectorFilter.cs @@ -6,15 +6,23 @@ public class VectorFilter : StringIdPagination public bool WithVector { get; set; } /// - /// For keyword search + /// Filter group: each item contains a logical operator and a list of key-value pairs /// - [JsonPropertyName("search_pairs")] - public IEnumerable? SearchPairs { get; set; } - + [JsonPropertyName("filter_groups")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public IEnumerable? FilterGroups { get; set; } /// - /// Included payload keys + /// Order by a specific field /// - [JsonPropertyName("included_payloads")] - public IEnumerable? IncludedPayloads { get; set; } + [JsonPropertyName("order_by")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public VectorSort? OrderBy { get; set; } + + /// + /// Included payload fields + /// + [JsonPropertyName("fields")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public IEnumerable? Fields { get; set; } } \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Abstraction/VectorStorage/Models/VectorFilterGroup.cs b/src/Infrastructure/BotSharp.Abstraction/VectorStorage/Models/VectorFilterGroup.cs new file mode 100644 index 00000000..0b2eb6ac --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/VectorStorage/Models/VectorFilterGroup.cs @@ -0,0 +1,10 @@ +namespace BotSharp.Abstraction.VectorStorage.Models; + +public class VectorFilterGroup +{ + [JsonPropertyName("filters")] + public IEnumerable? Filters { get; set; } + + [JsonPropertyName("filter_operator")] + public string FilterOperator { get; set; } = "or"; +} diff --git a/src/Infrastructure/BotSharp.Abstraction/VectorStorage/Models/VectorQueryOptions.cs b/src/Infrastructure/BotSharp.Abstraction/VectorStorage/Models/VectorQueryOptions.cs new file mode 100644 index 00000000..37bad608 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/VectorStorage/Models/VectorQueryOptions.cs @@ -0,0 +1,12 @@ +namespace BotSharp.Abstraction.VectorStorage.Models; + +public class VectorQueryOptions +{ + public bool WithPayload { get; set; } + public bool WithVector { get; set; } + + public static VectorQueryOptions Default() + { + return new(); + } +} diff --git a/src/Infrastructure/BotSharp.Abstraction/VectorStorage/Models/VectorSearchOptions.cs b/src/Infrastructure/BotSharp.Abstraction/VectorStorage/Models/VectorSearchOptions.cs index 64943dd1..ef888bb5 100644 --- a/src/Infrastructure/BotSharp.Abstraction/VectorStorage/Models/VectorSearchOptions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/VectorStorage/Models/VectorSearchOptions.cs @@ -4,8 +4,21 @@ namespace BotSharp.Abstraction.VectorStorage.Models; public class VectorSearchOptions { - public IEnumerable? Fields { get; set; } = new List { KnowledgePayloadName.Text, KnowledgePayloadName.Answer }; + public IEnumerable? Fields { get; set; } = [KnowledgePayloadName.Text, KnowledgePayloadName.Answer]; + public IEnumerable? FilterGroups { get; set; } public int? Limit { get; set; } = 5; public float? Confidence { get; set; } = 0.5f; public bool WithVector { get; set; } + + public static VectorSearchOptions Default() + { + return new() + { + Fields = [KnowledgePayloadName.Text, KnowledgePayloadName.Answer], + FilterGroups = null, + Limit = 5, + Confidence = 0.5f, + WithVector = false + }; + } } diff --git a/src/Infrastructure/BotSharp.Abstraction/VectorStorage/Models/VectorSort.cs b/src/Infrastructure/BotSharp.Abstraction/VectorStorage/Models/VectorSort.cs new file mode 100644 index 00000000..469018b0 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/VectorStorage/Models/VectorSort.cs @@ -0,0 +1,10 @@ +namespace BotSharp.Abstraction.VectorStorage.Models; + +public class VectorSort +{ + [JsonPropertyName("field")] + public string? Field { get; set; } + + [JsonPropertyName("order")] + public string? Order { get; set; } = "desc"; +} diff --git a/src/Infrastructure/BotSharp.Core.Crontab/Services/CrontabService.cs b/src/Infrastructure/BotSharp.Core.Crontab/Services/CrontabService.cs index 255faa71..b2101ee5 100644 --- a/src/Infrastructure/BotSharp.Core.Crontab/Services/CrontabService.cs +++ b/src/Infrastructure/BotSharp.Core.Crontab/Services/CrontabService.cs @@ -44,7 +44,7 @@ public class CrontabService : ICrontabService, ITaskFeeder public async Task> GetCrontable() { var repo = _services.GetRequiredService(); - var crontable = repo.GetCrontabItems(CrontabItemFilter.Empty()); + var crontable = await repo.GetCrontabItems(CrontabItemFilter.Empty()); // Add fixed crontab items from cronsources var fixedCrantabItems = crontable.Items.ToList(); diff --git a/src/Infrastructure/BotSharp.Core.Crontab/Services/CrontabWatcher.cs b/src/Infrastructure/BotSharp.Core.Crontab/Services/CrontabWatcher.cs index 1546cb7c..f6ce7a26 100644 --- a/src/Infrastructure/BotSharp.Core.Crontab/Services/CrontabWatcher.cs +++ b/src/Infrastructure/BotSharp.Core.Crontab/Services/CrontabWatcher.cs @@ -45,6 +45,7 @@ public class CrontabWatcher : BackgroundService { var cron = services.GetRequiredService(); var crons = await cron.GetCrontable(); + var settings = services.GetRequiredService(); var publisher = services.GetService(); foreach (var item in crons) @@ -87,7 +88,7 @@ public class CrontabWatcher : BackgroundService { _logger.LogInformation($"The current time matches the cron expression {item}"); - if (publisher != null) + if (publisher != null && settings.EventSubscriber.Enabled) { await publisher.PublishAsync($"Crontab:{item.Title}", item.Cron); } diff --git a/src/Infrastructure/BotSharp.Core.Realtime/Hooks/RealtimeConversationHook.cs b/src/Infrastructure/BotSharp.Core.Realtime/Hooks/RealtimeConversationHook.cs index 095ff2b8..07b8646c 100644 --- a/src/Infrastructure/BotSharp.Core.Realtime/Hooks/RealtimeConversationHook.cs +++ b/src/Infrastructure/BotSharp.Core.Realtime/Hooks/RealtimeConversationHook.cs @@ -11,7 +11,7 @@ public class RealtimeConversationHook : ConversationHookBase, IConversationHook _services = services; } - public async Task OnFunctionExecuting(RoleDialogModel message, string from = InvokeSource.Manual) + public async Task OnFunctionExecuting(RoleDialogModel message, InvokeFunctionOptions? options = null) { var hub = _services.GetRequiredService(); if (hub.HubConn == null) @@ -32,10 +32,10 @@ public class RealtimeConversationHook : ConversationHookBase, IConversationHook } } - public async Task OnFunctionExecuted(RoleDialogModel message, string from = InvokeSource.Manual) + public async Task OnFunctionExecuted(RoleDialogModel message, InvokeFunctionOptions? options = null) { var hub = _services.GetRequiredService(); - if (from != InvokeSource.Llm || hub.HubConn == null) + if (options?.From != InvokeSource.Llm || hub.HubConn == null) { return; } diff --git a/src/Infrastructure/BotSharp.Core.Realtime/Services/RealtimeHub.cs b/src/Infrastructure/BotSharp.Core.Realtime/Services/RealtimeHub.cs index fe544b4d..8c057b95 100644 --- a/src/Infrastructure/BotSharp.Core.Realtime/Services/RealtimeHub.cs +++ b/src/Infrastructure/BotSharp.Core.Realtime/Services/RealtimeHub.cs @@ -99,7 +99,18 @@ public class RealtimeHub : IRealtimeHub agent.Id); } - await routing.InvokeFunction(message.FunctionName, message, from: InvokeSource.Llm); + await routing.InvokeFunction(message.FunctionName, message, options: new() { From = InvokeSource.Llm }); + + var hooks = _services.GetHooks(_conn.CurrentAgentId); + foreach (var hook in hooks) + { + if (await hook.ShouldReconnect(_conn, message)) + { + await _completer.Reconnect(_conn); + _logger.LogWarning("Reconnecting to model due to function call: {FunctionName}", message.FunctionName); + break; + } + } } else { @@ -117,19 +128,6 @@ public class RealtimeHub : IRealtimeHub } } } - - var isReconnect = false; - var realtimeHooks = _services.GetHooks(_conn.CurrentAgentId); - foreach (var hook in realtimeHooks) - { - isReconnect = await hook.ShouldReconnect(_conn); - if (isReconnect) break; - } - - if (isReconnect) - { - await _completer.Reconnect(_conn); - } }, onConversationItemCreated: async response => { diff --git a/src/Infrastructure/BotSharp.Core.SideCar/Services/BotSharpConversationSideCar.cs b/src/Infrastructure/BotSharp.Core.SideCar/Services/BotSharpConversationSideCar.cs index d636fa71..79711cb6 100644 --- a/src/Infrastructure/BotSharp.Core.SideCar/Services/BotSharpConversationSideCar.cs +++ b/src/Infrastructure/BotSharp.Core.SideCar/Services/BotSharpConversationSideCar.cs @@ -15,6 +15,9 @@ ******************************************************************************/ using BotSharp.Core.Infrastructures; +using NetTopologySuite.Index.KdTree; +using Newtonsoft.Json.Linq; +using static System.Runtime.InteropServices.JavaScript.JSType; namespace BotSharp.Core.SideCar.Services; @@ -109,7 +112,7 @@ public class BotSharpConversationSideCar : IConversationSideCar var response = await InnerExecute(agentId, text, postback, states); AfterExecute(); - _logger.LogInformation($"Existing side car conversation..."); + _logger.LogInformation($"Exiting side car conversation..."); return response; } @@ -189,24 +192,29 @@ public class BotSharpConversationSideCar : IConversationSideCar private void RestoreStates(ConversationState prevStates) { - var innerStates = prevStates; + var preValues = prevStates.Values.ToList(); + var copy = JsonSerializer.Deserialize>(JsonSerializer.Serialize(preValues)); + var innerStates = new ConversationState(copy ?? []); var state = _services.GetRequiredService(); if (_sideCarOptions?.IsInheritStates == true) { + var hasIncludedStates = _sideCarOptions?.InheritStateKeys?.Any() == true; + var hasExcludedStates = _sideCarOptions?.ExcludedStateKeys?.Any() == true; var curStates = state.GetCurrentState(); + foreach (var pair in curStates) { var endNode = pair.Value.Values.LastOrDefault(); if (endNode == null) continue; - if (_sideCarOptions?.InheritStateKeys?.Any() == true - && !_sideCarOptions.InheritStateKeys.Contains(pair.Key)) + if ((hasIncludedStates && !_sideCarOptions.InheritStateKeys.Contains(pair.Key)) + || (hasExcludedStates && _sideCarOptions.ExcludedStateKeys.Contains(pair.Key))) { continue; } - if (innerStates.ContainsKey(pair.Key)) + if (innerStates.ContainsKey(pair.Key) && innerStates[pair.Key].Versioning) { innerStates[pair.Key].Values.Add(endNode); } @@ -223,6 +231,51 @@ public class BotSharpConversationSideCar : IConversationSideCar } } + AccumulateLlmStats(state, prevStates, innerStates); state.SetCurrentState(innerStates); } + + private void AccumulateLlmStats(IConversationStateService state, ConversationState prevState, ConversationState curState) + { + var dict = new Dictionary + { + { "prompt_total", typeof(int) }, + { "completion_total", typeof(int) }, + { "llm_total_cost", typeof(float) } + }; + + foreach (var pair in dict) + { + var preVal = prevState.GetValueOrDefault(pair.Key)?.Values?.LastOrDefault()?.Data; + var curVal = state.GetState(pair.Key); + + object data = pair.Value switch + { + Type t when t == typeof(int) => ParseNumber(preVal) + ParseNumber(curVal), + Type t when t == typeof(float) => ParseNumber(preVal) + ParseNumber(curVal), + _ => default + }; + + var cur = curState.GetValueOrDefault(pair.Key); + if (cur?.Values?.LastOrDefault() != null) + { + cur.Values.Last().Data = $"{data}"; + } + } + } + + private T ParseNumber(string? data) where T : struct + { + if (string.IsNullOrEmpty(data)) + { + return default; + } + + return typeof(T) switch + { + Type t when t == typeof(int) => (T)(object)(int.TryParse(data, out var i) ? i : 0), + Type t when t == typeof(float) => (T)(object)(float.TryParse(data, out var f) ? f : 0), + _ => default + }; + } } \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Core/BotSharp.Core.csproj b/src/Infrastructure/BotSharp.Core/BotSharp.Core.csproj index ddcf68cb..6833fdfc 100644 --- a/src/Infrastructure/BotSharp.Core/BotSharp.Core.csproj +++ b/src/Infrastructure/BotSharp.Core/BotSharp.Core.csproj @@ -86,7 +86,6 @@ - @@ -97,7 +96,9 @@ + + @@ -188,7 +189,7 @@ PreserveNewest - + PreserveNewest @@ -211,6 +212,9 @@ PreserveNewest + + PreserveNewest + @@ -242,5 +246,4 @@ true - diff --git a/src/Infrastructure/BotSharp.Core/BotSharpCoreExtensions.cs b/src/Infrastructure/BotSharp.Core/BotSharpCoreExtensions.cs index 5c00de0d..bfae45ba 100644 --- a/src/Infrastructure/BotSharp.Core/BotSharpCoreExtensions.cs +++ b/src/Infrastructure/BotSharp.Core/BotSharpCoreExtensions.cs @@ -125,7 +125,6 @@ public static class BotSharpCoreExtensions { var pluginSettings = new PluginSettings(); config.Bind("PluginLoader", pluginSettings); - var excludedFunctions = pluginSettings.ExcludedFunctions ?? []; services.AddScoped(provider => { @@ -143,8 +142,7 @@ public static class BotSharpCoreExtensions // Register function callback var functions = assembly.GetTypes() .Where(x => x.IsClass - && x.GetInterface(nameof(IFunctionCallback)) != null - && !excludedFunctions.Contains(x.Name)) + && x.GetInterface(nameof(IFunctionCallback)) != null) .ToArray(); foreach (var function in functions) diff --git a/src/Infrastructure/BotSharp.Core/Conversations/ConversationPlugin.cs b/src/Infrastructure/BotSharp.Core/Conversations/ConversationPlugin.cs index a559877f..61c23d1b 100644 --- a/src/Infrastructure/BotSharp.Core/Conversations/ConversationPlugin.cs +++ b/src/Infrastructure/BotSharp.Core/Conversations/ConversationPlugin.cs @@ -1,18 +1,23 @@ using BotSharp.Abstraction.Google.Settings; using BotSharp.Abstraction.Instructs; +using BotSharp.Abstraction.MessageHub; +using BotSharp.Abstraction.MessageHub.Observers; +using BotSharp.Abstraction.MessageHub.Services; using BotSharp.Abstraction.Messaging; using BotSharp.Abstraction.Planning; using BotSharp.Abstraction.Plugins.Models; using BotSharp.Abstraction.Settings; using BotSharp.Abstraction.Templating; using BotSharp.Core.Instructs; +using BotSharp.Core.MessageHub; +using BotSharp.Core.MessageHub.Observers; +using BotSharp.Core.MessageHub.Services; using BotSharp.Core.Messaging; using BotSharp.Core.Routing.Reasoning; using BotSharp.Core.Templating; using BotSharp.Core.Translation; -using BotSharp.Core.Observables.Queues; +using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Configuration; -using BotSharp.Abstraction.Observables.Models; namespace BotSharp.Core.Conversations; @@ -43,11 +48,14 @@ public class ConversationPlugin : IBotSharpPlugin return settingService.Bind("GoogleApi"); }); - services.AddSingleton>(); + // Observer and observable + services.AddSingleton>>(); + services.AddScoped>>(); + services.AddScoped>, ConversationObserver>(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationProgressService.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationProgressService.cs deleted file mode 100644 index d7e6440c..00000000 --- a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationProgressService.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace BotSharp.Core.Conversations.Services -{ - public class ConversationProgressService : IConversationProgressService - { - - public FunctionExecuting OnFunctionExecuting { get; set; } - - - public FunctionExecuted OnFunctionExecuted { get; set; } - } -} diff --git a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.cs index 676ea362..9e410b49 100644 --- a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.cs +++ b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.cs @@ -87,7 +87,7 @@ public partial class ConversationService : IConversationService } var db = _services.GetRequiredService(); - var conversations = db.GetConversations(filter); + var conversations = await db.GetConversations(filter); return conversations; } diff --git a/src/Infrastructure/BotSharp.Core/Demo/Functions/GetFunEventsFn.cs b/src/Infrastructure/BotSharp.Core/Demo/Functions/GetFunEventsFn.cs new file mode 100644 index 00000000..87ddb1ee --- /dev/null +++ b/src/Infrastructure/BotSharp.Core/Demo/Functions/GetFunEventsFn.cs @@ -0,0 +1,51 @@ +using BotSharp.Abstraction.Functions; +using BotSharp.Core.MessageHub; +using System.Text.Json.Serialization; + +namespace BotSharp.Core.Demo.Functions; + +public class GetFunEventsFn : IFunctionCallback +{ + private readonly IServiceProvider _services; + + public GetFunEventsFn(IServiceProvider services) + { + _services = services; + } + + public string Name => "get_fun_events"; + public string Indication => "Searching fun events"; + + public async Task Execute(RoleDialogModel message) + { + var args = JsonSerializer.Deserialize(message.FunctionArgs); + var conv = _services.GetRequiredService(); + var messageHub = _services.GetRequiredService>>(); + + await Task.Delay(1000); + + message.Indication = $"Start querying event data in {args?.City}"; + messageHub.Push(new() + { + EventName = ChatEvent.OnIndicationReceived, + Data = message, + RefId = conv.ConversationId + }); + + await Task.Delay(1500); + + message.Indication = $"Still searching events in {args?.City}"; + messageHub.Push(new() + { + EventName = ChatEvent.OnIndicationReceived, + Data = message, + RefId = conv.ConversationId + }); + + await Task.Delay(1500); + + message.Content = $"Here in {args?.City}, there are a lot of fun events in summer."; + message.StopCompletion = true; + return true; + } +} \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Core/Demo/Functions/GetWeatherFn.cs b/src/Infrastructure/BotSharp.Core/Demo/Functions/GetWeatherFn.cs index cb6bcd88..6465b88e 100644 --- a/src/Infrastructure/BotSharp.Core/Demo/Functions/GetWeatherFn.cs +++ b/src/Infrastructure/BotSharp.Core/Demo/Functions/GetWeatherFn.cs @@ -1,4 +1,8 @@ using BotSharp.Abstraction.Functions; +using BotSharp.Abstraction.Models; +using BotSharp.Abstraction.SideCar; +using BotSharp.Core.MessageHub; +using System.Text.Json.Serialization; namespace BotSharp.Core.Demo.Functions; @@ -16,8 +20,56 @@ public class GetWeatherFn : IFunctionCallback public async Task Execute(RoleDialogModel message) { + var args = JsonSerializer.Deserialize(message.FunctionArgs); + var conv = _services.GetRequiredService(); + var messageHub = _services.GetRequiredService>>(); + + await Task.Delay(1000); + + message.Indication = $"Start querying weather data in {args?.City}"; + messageHub.Push(new() + { + EventName = ChatEvent.OnIndicationReceived, + Data = message, + RefId = conv.ConversationId + }); + + await Task.Delay(1500); + + message.Indication = $"Still working on it... Hold on, {args?.City}"; + messageHub.Push(new() + { + EventName = ChatEvent.OnIndicationReceived, + Data = message, + RefId = conv.ConversationId + }); + + await Task.Delay(1500); + message.Content = $"It is a sunny day!"; message.StopCompletion = false; + +#if DEBUG + var sidecar = _services.GetService(); + if (sidecar != null) + { + var text = $"I want to know fun events in {args?.City}"; + var states = new List + { + new() { Key = "channel", Value = "email" } + }; + + var msg = await sidecar.SendMessage(message.CurrentAgentId, text, states: states); + message.Content = $"{message.Content} {msg.Content}"; + } +#endif + return true; } +} + +class WeatherLocation +{ + [JsonPropertyName("city")] + public string City { get; set; } } \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Core/Evaluations/EvaluationConversationHook.cs b/src/Infrastructure/BotSharp.Core/Evaluations/EvaluationConversationHook.cs index b1b4541e..80757219 100644 --- a/src/Infrastructure/BotSharp.Core/Evaluations/EvaluationConversationHook.cs +++ b/src/Infrastructure/BotSharp.Core/Evaluations/EvaluationConversationHook.cs @@ -1,4 +1,5 @@ using BotSharp.Abstraction.Evaluations; +using BotSharp.Abstraction.Routing.Models; namespace BotSharp.Core.Evaluations; @@ -22,13 +23,13 @@ public class EvaluationConversationHook : ConversationHookBase return base.OnMessageReceived(message); } - public override Task OnFunctionExecuted(RoleDialogModel message, string from = InvokeSource.Manual) + public override Task OnFunctionExecuted(RoleDialogModel message, InvokeFunctionOptions? options = null) { if (Conversation != null && _convSettings.EnableExecutionLog) { _logger.Append(Conversation.Id, $"[{DateTime.Now}] {message.Role}: {message.FunctionName}({message.FunctionArgs}) => {message.Content}"); } - return base.OnFunctionExecuted(message, from: from); + return base.OnFunctionExecuted(message, options); } public override Task OnResponseGenerated(RoleDialogModel message) diff --git a/src/Infrastructure/BotSharp.Core/Files/Services/Instruct/FileInstructService.SelectFile.cs b/src/Infrastructure/BotSharp.Core/Files/Services/Instruct/FileInstructService.SelectFile.cs index 13525fb1..e00f9158 100644 --- a/src/Infrastructure/BotSharp.Core/Files/Services/Instruct/FileInstructService.SelectFile.cs +++ b/src/Infrastructure/BotSharp.Core/Files/Services/Instruct/FileInstructService.SelectFile.cs @@ -65,7 +65,7 @@ public partial class FileInstructService }).ToList(); var agentId = !string.IsNullOrWhiteSpace(options.AgentId) ? options.AgentId : BuiltInAgentId.UtilityAssistant; - var template = !string.IsNullOrWhiteSpace(options.Template) ? options.Template : "select_file_prompt"; + var template = !string.IsNullOrWhiteSpace(options.Template) ? options.Template : "util-file-select_file_instruction"; var foundAgent = db.GetAgent(agentId); var prompt = db.GetAgentTemplate(BuiltInAgentId.UtilityAssistant, template); diff --git a/src/Infrastructure/BotSharp.Core/Loggers/Services/LoggerService.Instruction.cs b/src/Infrastructure/BotSharp.Core/Loggers/Services/LoggerService.Instruction.cs index 42ad45ab..965212ca 100644 --- a/src/Infrastructure/BotSharp.Core/Loggers/Services/LoggerService.Instruction.cs +++ b/src/Infrastructure/BotSharp.Core/Loggers/Services/LoggerService.Instruction.cs @@ -18,22 +18,18 @@ public partial class LoggerService filter.UserIds = !isAdmin && user?.Id != null ? [user.Id] : null; - var agents = new List(); var users = new List(); var db = _services.GetRequiredService(); - var logs = db.GetInstructionLogs(filter); + var agentService = _services.GetRequiredService(); + var logs = await db.GetInstructionLogs(filter); var agentIds = logs.Items.Where(x => !string.IsNullOrEmpty(x.AgentId)).Select(x => x.AgentId).ToList(); var userIds = logs.Items.Where(x => !string.IsNullOrEmpty(x.UserId)).Select(x => x.UserId).ToList(); - agents = db.GetAgents(new AgentFilter - { - AgentIds = agentIds, - Pager = new Pagination { Size = filter.Size } - }); + var agents = await agentService.GetAgentOptions(agentIds); if (isAdmin) { - users = db.GetUserByIds(userIds); + users = await userService.GetUsers(userIds); } var items = logs.Items.Select(x => diff --git a/src/Infrastructure/BotSharp.Core/Observables/Queues/MessageHub.cs b/src/Infrastructure/BotSharp.Core/MessageHub/MessageHub.cs similarity index 83% rename from src/Infrastructure/BotSharp.Core/Observables/Queues/MessageHub.cs rename to src/Infrastructure/BotSharp.Core/MessageHub/MessageHub.cs index affb142d..8f01612d 100644 --- a/src/Infrastructure/BotSharp.Core/Observables/Queues/MessageHub.cs +++ b/src/Infrastructure/BotSharp.Core/MessageHub/MessageHub.cs @@ -1,11 +1,11 @@ using System.Reactive.Subjects; -namespace BotSharp.Core.Observables.Queues; +namespace BotSharp.Core.MessageHub; -public class MessageHub where T : class +public class MessageHub where T : ObserveDataBase { private readonly ILogger> _logger; - private readonly ISubject _observable = new Subject(); + private readonly ISubject _observable = Subject.Synchronize(new Subject()); public IObservable Events => _observable; public MessageHub(ILogger> logger) diff --git a/src/Infrastructure/BotSharp.Core/MessageHub/Observers/ConversationObserver.cs b/src/Infrastructure/BotSharp.Core/MessageHub/Observers/ConversationObserver.cs new file mode 100644 index 00000000..e2588c6c --- /dev/null +++ b/src/Infrastructure/BotSharp.Core/MessageHub/Observers/ConversationObserver.cs @@ -0,0 +1,45 @@ +using BotSharp.Abstraction.MessageHub.Observers; + +namespace BotSharp.Core.MessageHub.Observers; + +public class ConversationObserver : BotSharpObserverBase> +{ + private readonly ILogger _logger; + private readonly IServiceProvider _services; + + public ConversationObserver( + IServiceProvider services, + ILogger logger) : base() + { + _services = services; + _logger = logger; + } + + public override string Name => nameof(ConversationObserver); + + public override void OnCompleted() + { + _logger.LogWarning($"{nameof(ConversationObserver)} receives complete notification."); + } + + public override void OnError(Exception error) + { + _logger.LogError(error, $"{nameof(ConversationObserver)} receives error notification: {error.Message}"); + } + + public override void OnNext(HubObserveData value) + { + var conv = _services.GetRequiredService(); + + if (value.EventName == ChatEvent.OnIndicationReceived) + { +#if DEBUG + _logger.LogCritical($"Receiving {value.EventName} ({value.Data.Indication}) in {nameof(ConversationObserver)} - {conv.ConversationId}"); +#endif + if (_listeners.TryGetValue(value.EventName, out var func) && func != null) + { + func(value).ConfigureAwait(false).GetAwaiter().GetResult(); + } + } + } +} diff --git a/src/Infrastructure/BotSharp.Core/MessageHub/Services/ObserverService.cs b/src/Infrastructure/BotSharp.Core/MessageHub/Services/ObserverService.cs new file mode 100644 index 00000000..4679235d --- /dev/null +++ b/src/Infrastructure/BotSharp.Core/MessageHub/Services/ObserverService.cs @@ -0,0 +1,86 @@ +using BotSharp.Abstraction.MessageHub.Observers; +using BotSharp.Abstraction.MessageHub.Services; +using System.Reactive.Linq; + +namespace BotSharp.Core.MessageHub.Services; + +public class ObserverService : IObserverService +{ + private readonly IServiceProvider _services; + private readonly ILogger _logger; + + public ObserverService( + IServiceProvider services, + ILogger logger) + { + _services = services; + _logger = logger; + } + + public IDisposable SubscribeObservers( + string refId, + IEnumerable? names = null, + Dictionary>? listeners = null) where T : ObserveDataBase + { + var container = _services.GetRequiredService>(); + var observers = _services.GetServices>() + .Where(x => !x.Active) + .ToList(); + + if (!names.IsNullOrEmpty()) + { + observers = observers.Where(x => names.Contains(x.Name)).ToList(); + } + + if (observers.IsNullOrEmpty()) + { + return container; + } + + if (!listeners.IsNullOrEmpty()) + { + foreach (var observer in observers) + { + observer.SetEventListeners(listeners ?? []); + } + } + +#if DEBUG + _logger.LogCritical($"Subscribe observers: {string.Join(",", observers.Select(x => x.Name))}"); +#endif + + var subscriptions = new List>(); + var messageHub = _services.GetRequiredService>(); + foreach (var observer in observers) + { + observer.Activate(); + var sub = messageHub.Events.Where(x => x.RefId == refId).Subscribe(observer); + subscriptions.Add(new() + { + Observer = observer, + Subscription = sub + }); + } + + container.Append(subscriptions); + return container; + } + + public void UnSubscribeObservers(IEnumerable? names = null) where T : ObserveDataBase + { + var container = _services.GetRequiredService>(); + var subscriptions = container.GetSubscriptions(names); + +#if DEBUG + _logger.LogCritical($"UnSubscribe observers: {string.Join(",", subscriptions.Select(x => x.Observer.Name))}"); +#endif + + foreach (var sub in subscriptions) + { + if (!sub.Observer.Active) continue; + + sub.UnSubscribe(); + } + container.Remove(names); + } +} diff --git a/src/Infrastructure/BotSharp.Core/MessageHub/Services/ObserverSubscriptionContainer.cs b/src/Infrastructure/BotSharp.Core/MessageHub/Services/ObserverSubscriptionContainer.cs new file mode 100644 index 00000000..59e33e2e --- /dev/null +++ b/src/Infrastructure/BotSharp.Core/MessageHub/Services/ObserverSubscriptionContainer.cs @@ -0,0 +1,64 @@ +namespace BotSharp.Core.MessageHub.Services; + +public class ObserverSubscriptionContainer : IDisposable +{ + private readonly ILogger> _logger; + + private List> _subscriptions = []; + private bool _disposed = false; + + public ObserverSubscriptionContainer( + ILogger> logger) + { + _logger = logger; + } + + public List> GetSubscriptions(IEnumerable? names = null) + { + if (!names.IsNullOrEmpty()) + { + return _subscriptions.Where(x => names.Contains(x.Observer.Name)).ToList(); + } + return _subscriptions; + } + + public void Append(List> subscriptions) + { + _subscriptions = _subscriptions.Concat(subscriptions).DistinctBy(x => x.Observer.Name).ToList(); + } + + public void Remove(IEnumerable? names = null) + { + if (!names.IsNullOrEmpty()) + { + _subscriptions = _subscriptions.Where(x => !names.Contains(x.Observer.Name)).ToList(); + return; + } + _subscriptions.Clear(); + } + + public void Clear() + { + _subscriptions.Clear(); + } + + public void Dispose() + { + if (!_disposed) + { +#if DEBUG + _logger.LogCritical($"Start disposing subscriptions..."); +#endif + // UnSubscribe all observers + foreach (var sub in _subscriptions) + { + sub.UnSubscribe(); + } + _subscriptions.Clear(); +#if DEBUG + _logger.LogCritical($"End disposing subscriptions..."); +#endif + _disposed = true; + } + } +} \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Core/Repository/BotSharpDbContext.cs b/src/Infrastructure/BotSharp.Core/Repository/BotSharpDbContext.cs index 492dcde3..c2dfc25f 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/BotSharpDbContext.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/BotSharpDbContext.cs @@ -1,202 +1,6 @@ -using BotSharp.Abstraction.Loggers.Models; -using BotSharp.Abstraction.Plugins.Models; -using BotSharp.Abstraction.Tasks.Models; -using BotSharp.Abstraction.Translation.Models; -using BotSharp.Abstraction.VectorStorage.Models; - namespace BotSharp.Core.Repository; public class BotSharpDbContext : Database, IBotSharpRepository { public IServiceProvider ServiceProvider => throw new NotImplementedException(); - - #region Plugin - public PluginConfig GetPluginConfig() => throw new NotImplementedException(); - public void SavePluginConfig(PluginConfig config) => throw new NotImplementedException(); - #endregion - - #region Agent - public Agent GetAgent(string agentId, bool basicsOnly = false) - => throw new NotImplementedException(); - - public List GetAgents(AgentFilter filter) - => throw new NotImplementedException(); - - public List GetUserAgents(string userId) - => throw new NotImplementedException(); - - public void UpdateAgent(Agent agent, AgentField field) - => throw new NotImplementedException(); - - public string GetAgentTemplate(string agentId, string templateName) - => throw new NotImplementedException(); - - public bool PatchAgentTemplate(string agentId, AgentTemplate template) - => throw new NotImplementedException(); - - public List GetAgentResponses(string agentId, string prefix, string intent) - => throw new NotImplementedException(); - - public void BulkInsertAgents(List agents) - => throw new NotImplementedException(); - - public void BulkInsertUserAgents(List userAgents) - => throw new NotImplementedException(); - - public bool DeleteAgents() - => throw new NotImplementedException(); - - public bool DeleteAgent(string agentId) - => throw new NotImplementedException(); - #endregion - - #region Agent Task - public PagedItems GetAgentTasks(AgentTaskFilter filter) - => throw new NotImplementedException(); - - public AgentTask? GetAgentTask(string agentId, string taskId) - => throw new NotImplementedException(); - - public void InsertAgentTask(AgentTask task) - => throw new NotImplementedException(); - - public void BulkInsertAgentTasks(List tasks) - => throw new NotImplementedException(); - - public void UpdateAgentTask(AgentTask task, AgentTaskField field) - => throw new NotImplementedException(); - - public bool DeleteAgentTask(string agentId, List taskIds) - => throw new NotImplementedException(); - - public bool DeleteAgentTasks() - => throw new NotImplementedException(); - #endregion - - #region Conversation - public void CreateNewConversation(Conversation conversation) - => throw new NotImplementedException(); - - public bool DeleteConversations(IEnumerable conversationIds) - => throw new NotImplementedException(); - - public Conversation GetConversation(string conversationId, bool isLoadStates = false) - => throw new NotImplementedException(); - - public PagedItems GetConversations(ConversationFilter filter) - => throw new NotImplementedException(); - - public List GetLastConversations() - => throw new NotImplementedException(); - - public List GetIdleConversations(int batchSize, int messageLimit, int bufferHours, IEnumerable excludeAgentIds) - => throw new NotImplementedException(); - - [SideCar] - public List GetConversationDialogs(string conversationId) - => throw new NotImplementedException(); - - public ConversationState GetConversationStates(string conversationId) - => throw new NotImplementedException(); - - [SideCar] - public void AppendConversationDialogs(string conversationId, List dialogs) - => throw new NotImplementedException(); - - public void UpdateConversationTitle(string conversationId, string title) - => throw new NotImplementedException(); - public void UpdateConversationTitleAlias(string conversationId, string titleAlias) - => throw new NotImplementedException(); - - public bool UpdateConversationTags(string conversationId, List tags) - => throw new NotImplementedException(); - - public bool AppendConversationTags(string conversationId, List tags) - => throw new NotImplementedException(); - - public bool UpdateConversationMessage(string conversationId, UpdateMessageRequest request) - => throw new NotImplementedException(); - - [SideCar] - public void UpdateConversationBreakpoint(string conversationId, ConversationBreakpoint breakpoint) - => throw new NotImplementedException(); - - [SideCar] - public ConversationBreakpoint? GetConversationBreakpoint(string conversationId) - => throw new NotImplementedException(); - - public void UpdateConversationStates(string conversationId, List states) - => throw new NotImplementedException(); - - public void UpdateConversationStatus(string conversationId, string status) - => throw new NotImplementedException(); - - public List TruncateConversation(string conversationId, string messageId, bool cleanLog = false) - => throw new NotImplementedException(); - #endregion - - #region LLM Completion Log - public void SaveLlmCompletionLog(LlmCompletionLog log) - { - throw new NotImplementedException(); - } - #endregion - - #region Conversation Content Log - public void SaveConversationContentLog(ContentLogOutputModel log) - { - throw new NotImplementedException(); - } - - public List GetConversationContentLogs(string conversationId) - { - throw new NotImplementedException(); - } - #endregion - - #region Conversation State Log - public void SaveConversationStateLog(ConversationStateLogModel log) - { - throw new NotImplementedException(); - } - - public List GetConversationStateLogs(string conversationId) - { - throw new NotImplementedException(); - } - #endregion - - #region Stats - public void IncrementConversationCount() - { - throw new NotImplementedException(); - } - #endregion - - #region Translation - public IEnumerable GetTranslationMemories(IEnumerable queries) - => throw new NotImplementedException(); - public bool SaveTranslationMemories(IEnumerable inputs) => - throw new NotImplementedException(); - #endregion - - #region KnowledgeBase - public bool AddKnowledgeCollectionConfigs(List configs, bool reset = false) => - throw new NotImplementedException(); - - public bool DeleteKnowledgeCollectionConfig(string collectionName) => - throw new NotImplementedException(); - - public IEnumerable GetKnowledgeCollectionConfigs(VectorCollectionConfigFilter filter) => - throw new NotImplementedException(); - - public bool SaveKnolwedgeBaseFileMeta(KnowledgeDocMetaData metaData) => - throw new NotImplementedException(); - - public bool DeleteKnolwedgeBaseFileMeta(string collectionName, string vectorStoreProvider, Guid? fileId = null) => - throw new NotImplementedException(); - - public PagedItems GetKnowledgeBaseFileMeta(string collectionName, string vectorStoreProvider, KnowledgeFileFilter filter) => - throw new NotImplementedException(); - #endregion } diff --git a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.AgentTask.cs b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.AgentTask.cs index 016e0c36..2b84f212 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.AgentTask.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.AgentTask.cs @@ -6,7 +6,7 @@ namespace BotSharp.Core.Repository; public partial class FileRepository { #region Task - public PagedItems GetAgentTasks(AgentTaskFilter filter) + public async ValueTask> GetAgentTasks(AgentTaskFilter filter) { if (filter == null) { diff --git a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Conversation.cs b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Conversation.cs index f772acb2..68881ea3 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Conversation.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Conversation.cs @@ -241,10 +241,9 @@ public partial class FileRepository if (!string.IsNullOrEmpty(convDir)) { var breakpointFile = Path.Combine(convDir, BREAKPOINT_FILE); - if (!File.Exists(breakpointFile)) { - File.Create(breakpointFile); + File.WriteAllText(breakpointFile, "[]"); } var content = File.ReadAllText(breakpointFile); @@ -285,7 +284,7 @@ public partial class FileRepository var breakpointFile = Path.Combine(convDir, BREAKPOINT_FILE); if (!File.Exists(breakpointFile)) { - File.Create(breakpointFile); + File.WriteAllText(breakpointFile, "[]"); } var content = File.ReadAllText(breakpointFile); @@ -380,7 +379,7 @@ public partial class FileRepository return record; } - public PagedItems GetConversations(ConversationFilter filter) + public async ValueTask> GetConversations(ConversationFilter filter) { if (filter == null) { @@ -453,6 +452,10 @@ public partial class FileRepository { matched = matched && record.CreatedTime >= filter.StartTime.Value; } + if (filter?.EndTime != null) + { + matched = matched && record.CreatedTime <= filter.EndTime.Value; + } if (filter?.Tags != null && filter.Tags.Any()) { matched = matched && !record.Tags.IsNullOrEmpty() && record.Tags.Exists(t => filter.Tags.Contains(t)); @@ -543,7 +546,7 @@ public partial class FileRepository return new PagedItems { Items = records.OrderByDescending(x => x.CreatedTime).Skip(pager.Offset).Take(pager.Size), - Count = records.Count(), + Count = records.Count() }; } @@ -705,7 +708,9 @@ public partial class FileRepository if (conv == null || states.IsNullOrEmpty() || (!filter.AgentIds.IsNullOrEmpty() && !filter.AgentIds.Contains(conv.AgentId)) - || (!filter.UserIds.IsNullOrEmpty() && !filter.UserIds.Contains(conv.UserId))) + || (!filter.UserIds.IsNullOrEmpty() && !filter.UserIds.Contains(conv.UserId)) + || (filter.StartTime.HasValue && conv.CreatedTime < filter.StartTime.Value) + || (filter.EndTime.HasValue && conv.CreatedTime > filter.EndTime.Value)) { continue; } @@ -920,7 +925,6 @@ public partial class FileRepository private bool SaveTruncatedDialogs(string dialogDir, List dialogs) { if (string.IsNullOrEmpty(dialogDir) || dialogs == null) return false; - if (!File.Exists(dialogDir)) File.Create(dialogDir); var texts = ParseDialogElements(dialogs); File.WriteAllText(dialogDir, texts); @@ -930,7 +934,6 @@ public partial class FileRepository private bool SaveTruncatedStates(string stateDir, List states) { if (string.IsNullOrEmpty(stateDir) || states == null) return false; - if (!File.Exists(stateDir)) File.Create(stateDir); var stateStr = JsonSerializer.Serialize(states, _options); File.WriteAllText(stateDir, stateStr); @@ -940,7 +943,6 @@ public partial class FileRepository private bool SaveTruncatedLatestStates(string latestStateDir, List states) { if (string.IsNullOrEmpty(latestStateDir) || states == null) return false; - if (!File.Exists(latestStateDir)) File.Create(latestStateDir); var latestStates = BuildLatestStates(states); var stateStr = JsonSerializer.Serialize(latestStates, _options); @@ -951,7 +953,6 @@ public partial class FileRepository private bool SaveTruncatedBreakpoints(string breakpointDir, List breakpoints) { if (string.IsNullOrEmpty(breakpointDir) || breakpoints == null) return false; - if (!File.Exists(breakpointDir)) File.Create(breakpointDir); var breakpointStr = JsonSerializer.Serialize(breakpoints, _options); File.WriteAllText(breakpointDir, breakpointStr); diff --git a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Crontab.cs b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Crontab.cs index 4a99027a..84f2794c 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Crontab.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Crontab.cs @@ -63,7 +63,7 @@ public partial class FileRepository } - public PagedItems GetCrontabItems(CrontabItemFilter filter) + public async ValueTask> GetCrontabItems(CrontabItemFilter filter) { if (filter == null) @@ -111,7 +111,7 @@ public partial class FileRepository return new PagedItems { Items = records.OrderByDescending(x => x.CreatedTime).Skip(filter.Offset).Take(filter.Size), - Count = records.Count(), + Count = records.Count() }; } } diff --git a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.KnowledgeBase.cs b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.KnowledgeBase.cs index b0a95027..ae9d0405 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.KnowledgeBase.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.KnowledgeBase.cs @@ -15,17 +15,17 @@ public partial class FileRepository } var configFile = Path.Combine(vectorDir, COLLECTION_CONFIG_FILE); + if (!File.Exists(configFile)) + { + File.WriteAllText(configFile, "[]"); + } + if (reset) { File.WriteAllText(configFile, JsonSerializer.Serialize(configs ?? new(), _options)); return true; } - if (!File.Exists(configFile)) - { - File.Create(configFile); - } - var str = File.ReadAllText(configFile); var savedConfigs = JsonSerializer.Deserialize>(str, _options) ?? new(); @@ -155,7 +155,7 @@ public partial class FileRepository return true; } - public PagedItems GetKnowledgeBaseFileMeta(string collectionName, string vectorStoreProvider, KnowledgeFileFilter filter) + public async ValueTask> GetKnowledgeBaseFileMeta(string collectionName, string vectorStoreProvider, KnowledgeFileFilter filter) { if (string.IsNullOrWhiteSpace(collectionName) || string.IsNullOrWhiteSpace(vectorStoreProvider)) diff --git a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Log.cs b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Log.cs index 3913a404..c0851401 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Log.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Log.cs @@ -160,7 +160,7 @@ namespace BotSharp.Core.Repository return true; } - public PagedItems GetInstructionLogs(InstructLogFilter filter) + public async ValueTask> GetInstructionLogs(InstructLogFilter filter) { if (filter == null) { @@ -202,6 +202,14 @@ namespace BotSharp.Core.Repository { matched = matched && filter.UserIds.Contains(log.UserId); } + if (filter.StartTime.HasValue) + { + matched = matched && log.CreatedTime >= filter.StartTime.Value; + } + if (filter.EndTime.HasValue) + { + matched = matched && log.CreatedTime <= filter.EndTime.Value; + } // Check states if (matched && filter != null && !filter.States.IsNullOrEmpty()) @@ -308,7 +316,9 @@ namespace BotSharp.Core.Repository if (log == null || log.InnerStates.IsNullOrEmpty() || (!filter.UserIds.IsNullOrEmpty() && !filter.UserIds.Contains(log.UserId)) - || (!filter.AgentIds.IsNullOrEmpty() && !filter.AgentIds.Contains(log.AgentId))) + || (!filter.AgentIds.IsNullOrEmpty() && !filter.AgentIds.Contains(log.AgentId)) + || (filter.StartTime.HasValue && log.CreatedTime < filter.StartTime.Value) + || (filter.EndTime.HasValue && log.CreatedTime > filter.EndTime.Value)) { continue; } diff --git a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.User.cs b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.User.cs index 7bd5a1a9..09212e20 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.User.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.User.cs @@ -105,7 +105,7 @@ public partial class FileRepository File.WriteAllText(path, JsonSerializer.Serialize(user, _options)); } - public PagedItems GetUsers(UserFilter filter) + public async ValueTask> GetUsers(UserFilter filter) { if (filter == null) { diff --git a/src/Infrastructure/BotSharp.Core/Routing/Reasoning/InstructExecutor.cs b/src/Infrastructure/BotSharp.Core/Routing/Reasoning/InstructExecutor.cs index e0ac69a2..9dd19c09 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Reasoning/InstructExecutor.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Reasoning/InstructExecutor.cs @@ -1,5 +1,6 @@ using BotSharp.Abstraction.Infrastructures.Enums; using BotSharp.Abstraction.Planning; +using BotSharp.Abstraction.Routing.Models; namespace BotSharp.Core.Routing.Reasoning; @@ -33,7 +34,7 @@ public class InstructExecutor : IExecutor if (message.FunctionName != null) { var msg = RoleDialogModel.From(message, role: AgentRole.Function); - await routing.InvokeFunction(message.FunctionName, msg, from: InvokeSource.Llm); + await routing.InvokeFunction(message.FunctionName, msg, options: new() { From = InvokeSource.Routing }); } var agentId = routing.Context.GetCurrentAgentId(); @@ -59,11 +60,12 @@ public class InstructExecutor : IExecutor { var state = _services.GetRequiredService(); var useStreamMsg = state.GetState("use_stream_message"); - var ret = await routing.InvokeAgent( - agentId, - dialogs, - from: InvokeSource.Routing, - useStream: bool.TryParse(useStreamMsg, out var useStream) && useStream); + var options = new InvokeAgentOptions() + { + From = InvokeSource.Routing, + UseStream = bool.TryParse(useStreamMsg, out var useStream) && useStream + }; + var ret = await routing.InvokeAgent(agentId, dialogs, options); } var response = dialogs.Last(); diff --git a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs index 6d2813d9..6acb58b2 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs @@ -1,3 +1,4 @@ +using BotSharp.Abstraction.Routing.Models; using BotSharp.Abstraction.Templating; namespace BotSharp.Core.Routing; @@ -7,9 +8,9 @@ public partial class RoutingService public async Task InvokeAgent( string agentId, List dialogs, - string from = InvokeSource.Manual, - bool useStream = false) + InvokeAgentOptions? options = null) { + options ??= InvokeAgentOptions.Default(); var agentService = _services.GetRequiredService(); var agent = await agentService.LoadAgent(agentId); @@ -36,7 +37,7 @@ public partial class RoutingService RoleDialogModel response; var message = dialogs.Last(); - if (useStream) + if (options?.UseStream == true) { response = await chatCompletion.GetChatCompletionsStreamingAsync(agent, dialogs); } @@ -59,7 +60,7 @@ public partial class RoutingService message.CurrentAgentId = agent.Id; message.IsStreaming = response.IsStreaming; - await InvokeFunction(message, dialogs, from: from, useStream: useStream); + await InvokeFunction(message, dialogs, options); } else { @@ -83,8 +84,7 @@ public partial class RoutingService private async Task InvokeFunction( RoleDialogModel message, List dialogs, - string from, - bool useStream) + InvokeAgentOptions? options = null) { // execute function // Save states @@ -93,7 +93,8 @@ public partial class RoutingService var routing = _services.GetRequiredService(); // Call functions - await routing.InvokeFunction(message.FunctionName, message, from: from); + var funcOptions = options != null ? new InvokeFunctionOptions() { From = options.From } : null; + await routing.InvokeFunction(message.FunctionName, message, options: funcOptions); // Pass execution result to LLM to get response if (!message.StopCompletion) @@ -120,7 +121,7 @@ public partial class RoutingService // Send to Next LLM var curAgentId = routing.Context.GetCurrentAgentId(); - await InvokeAgent(curAgentId, dialogs, from, useStream); + await InvokeAgent(curAgentId, dialogs, options); } } else diff --git a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeFunction.cs b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeFunction.cs index 20c691f8..dea0947d 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeFunction.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeFunction.cs @@ -1,12 +1,14 @@ -using BotSharp.Abstraction.Hooks; +using BotSharp.Abstraction.Routing.Models; +using BotSharp.Core.MessageHub; using BotSharp.Core.Routing.Executor; namespace BotSharp.Core.Routing; public partial class RoutingService { - public async Task InvokeFunction(string name, RoleDialogModel message, string from = InvokeSource.Manual) + public async Task InvokeFunction(string name, RoleDialogModel message, InvokeFunctionOptions? options = null) { + options ??= InvokeFunctionOptions.Default(); var currentAgentId = message.CurrentAgentId; var agentService = _services.GetRequiredService(); var agent = await agentService.GetAgent(currentAgentId); @@ -23,20 +25,22 @@ public partial class RoutingService // Clone message var clonedMessage = RoleDialogModel.From(message); clonedMessage.FunctionName = name; - - var progressService = _services.GetService(); clonedMessage.Indication = await funcExecutor.GetIndicatorAsync(message); - if (progressService?.OnFunctionExecuting != null) + var conv = _services.GetRequiredService(); + var messageHub = _services.GetRequiredService>>(); + messageHub.Push(new() { - await progressService.OnFunctionExecuting(clonedMessage); - } - + EventName = ChatEvent.OnIndicationReceived, + Data = clonedMessage, + RefId = conv.ConversationId + }); + var hooks = _services.GetHooksOrderByPriority(clonedMessage.CurrentAgentId); foreach (var hook in hooks) { hook.SetAgent(agent); - await hook.OnFunctionExecuting(clonedMessage, from: from); + await hook.OnFunctionExecuting(clonedMessage, options); } bool result = false; @@ -48,7 +52,7 @@ public partial class RoutingService // After functions have been executed foreach (var hook in hooks) { - await hook.OnFunctionExecuted(clonedMessage, from: from); + await hook.OnFunctionExecuted(clonedMessage, options); } // Set result to original message @@ -64,7 +68,7 @@ public partial class RoutingService } catch (JsonException ex) { - _logger.LogError($"The input does not contain any JSON tokens:\r\n{message.Content}\r\n{ex.Message}"); + _logger.LogError(ex, $"The input does not contain any JSON tokens:\r\n{message.Content}\r\n{ex.Message}"); message.StopCompletion = true; message.Content = ex.Message; } @@ -72,7 +76,7 @@ public partial class RoutingService { message.StopCompletion = true; message.Content = ex.Message; - _logger.LogError(ex.ToString()); + _logger.LogError(ex, ex.ToString()); } // Make sure content has been populated diff --git a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.cs b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.cs index c66ef4b2..4e43cbd5 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.cs @@ -53,11 +53,12 @@ public partial class RoutingService : IRoutingService { var state = _services.GetRequiredService(); var useStreamMsg = state.GetState("use_stream_message"); - var ret = await routing.InvokeAgent( - agentId, - dialogs, - from: InvokeSource.Routing, - useStream: bool.TryParse(useStreamMsg, out var useStream) && useStream); + var options = new InvokeAgentOptions() + { + From = InvokeSource.Routing, + UseStream = bool.TryParse(useStreamMsg, out var useStream) && useStream + }; + var ret = await routing.InvokeAgent(agentId, dialogs, options); } var response = dialogs.Last(); diff --git a/src/Infrastructure/BotSharp.Core/Tasks/Services/AgentTaskService.cs b/src/Infrastructure/BotSharp.Core/Tasks/Services/AgentTaskService.cs index ce5d72b8..5ae52a73 100644 --- a/src/Infrastructure/BotSharp.Core/Tasks/Services/AgentTaskService.cs +++ b/src/Infrastructure/BotSharp.Core/Tasks/Services/AgentTaskService.cs @@ -39,7 +39,7 @@ public class AgentTaskService : IAgentTaskService else { var db = _services.GetRequiredService(); - var pagedTasks = db.GetAgentTasks(filter); + var pagedTasks = await db.GetAgentTasks(filter); return await Task.FromResult(pagedTasks); } } diff --git a/src/Infrastructure/BotSharp.Core/Users/Services/UserService.cs b/src/Infrastructure/BotSharp.Core/Users/Services/UserService.cs index e4ebe311..f7dd61c6 100644 --- a/src/Infrastructure/BotSharp.Core/Users/Services/UserService.cs +++ b/src/Infrastructure/BotSharp.Core/Users/Services/UserService.cs @@ -411,10 +411,18 @@ public class UserService : IUserService return user; } + [SharpCache(10)] + public async Task> GetUsers(List ids) + { + var db = _services.GetRequiredService(); + var users = db.GetUserByIds(ids); + return users; + } + public async Task> GetUsers(UserFilter filter) { var db = _services.GetRequiredService(); - var users = db.GetUsers(filter); + var users = await db.GetUsers(filter); return users; } diff --git a/src/Infrastructure/BotSharp.Core/Using.cs b/src/Infrastructure/BotSharp.Core/Using.cs index 0e8e2762..f67baef0 100644 --- a/src/Infrastructure/BotSharp.Core/Using.cs +++ b/src/Infrastructure/BotSharp.Core/Using.cs @@ -36,6 +36,9 @@ global using BotSharp.Abstraction.Loggers.Services; global using BotSharp.Abstraction.Infrastructures.Events; global using BotSharp.Abstraction.Templating.Constants; global using BotSharp.Abstraction.Realtime.Models.Session; +global using BotSharp.Abstraction.Conversations.Enums; +global using BotSharp.Abstraction.Hooks; +global using BotSharp.Abstraction.MessageHub.Models; global using BotSharp.Core.Agents.Services; global using BotSharp.Core.Conversations.Services; global using BotSharp.Core.Infrastructures; diff --git a/src/Infrastructure/BotSharp.Core/data/agents/01e2fc5c-2c89-4ec7-8470-7688608b496c/functions/get_fun_events.json b/src/Infrastructure/BotSharp.Core/data/agents/01e2fc5c-2c89-4ec7-8470-7688608b496c/functions/get_fun_events.json new file mode 100644 index 00000000..94b6abbf --- /dev/null +++ b/src/Infrastructure/BotSharp.Core/data/agents/01e2fc5c-2c89-4ec7-8470-7688608b496c/functions/get_fun_events.json @@ -0,0 +1,15 @@ +{ + "name": "get_fun_events", + "description": "Get fun events information for user.", + "visibility_expression": "{% if states.channel == 'email' %}visible{% endif %}", + "parameters": { + "type": "object", + "properties": { + "city": { + "type": "string", + "description": "The city where user wants to find fun events." + } + }, + "required": [ "city" ] + } +} \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Core/data/agents/01e2fc5c-2c89-4ec7-8470-7688608b496c/functions/get_weather.json b/src/Infrastructure/BotSharp.Core/data/agents/01e2fc5c-2c89-4ec7-8470-7688608b496c/functions/get_weather.json index 0fd0a459..ab73340f 100644 --- a/src/Infrastructure/BotSharp.Core/data/agents/01e2fc5c-2c89-4ec7-8470-7688608b496c/functions/get_weather.json +++ b/src/Infrastructure/BotSharp.Core/data/agents/01e2fc5c-2c89-4ec7-8470-7688608b496c/functions/get_weather.json @@ -1,6 +1,7 @@ { "name": "get_weather", "description": "Get weather information for user.", + "visibility_expression": "{% if states.channel != 'email' %}visible{% endif %}", "parameters": { "type": "object", "properties": { diff --git a/src/Infrastructure/BotSharp.Core/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/select_file_prompt.liquid b/src/Infrastructure/BotSharp.Core/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-file-select_file_instruction.liquid similarity index 100% rename from src/Infrastructure/BotSharp.Core/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/select_file_prompt.liquid rename to src/Infrastructure/BotSharp.Core/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-file-select_file_instruction.liquid diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/AgentController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/AgentController.cs index 4543fe19..b6950e04 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/AgentController.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/AgentController.cs @@ -14,8 +14,7 @@ public class AgentController : ControllerBase public AgentController( IAgentService agentService, IUserIdentity user, - IServiceProvider services - ) + IServiceProvider services) { _agentService = agentService; _user = user; diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs index bd131ca1..45f53c0c 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs @@ -1,6 +1,8 @@ using BotSharp.Abstraction.Files.Constants; using BotSharp.Abstraction.Files.Enums; using BotSharp.Abstraction.Files.Utilities; +using BotSharp.Abstraction.MessageHub.Models; +using BotSharp.Abstraction.MessageHub.Services; using BotSharp.Abstraction.Options; using BotSharp.Abstraction.Routing; using BotSharp.Abstraction.Users.Dtos; @@ -64,11 +66,11 @@ public class ConversationController : ControllerBase var agents = await agentService.GetAgentOptions(agentIds); var userIds = list.Select(x => x.User.Id).ToList(); - var users = await userService.GetUsers(new UserFilter { UserIds = userIds, Size = filter.Pager.Size }); + var users = await userService.GetUsers(userIds); foreach (var item in list) { - user = users.Items.FirstOrDefault(x => x.Id == item.User.Id); + user = users.FirstOrDefault(x => x.Id == item.User.Id); item.User = UserViewModel.FromUser(user); var agent = agents.FirstOrDefault(x => x.Id == item.AgentId); item.AgentName = agent?.Name ?? "Unkown"; @@ -343,6 +345,9 @@ public class ConversationController : ControllerBase [FromRoute] string conversationId, [FromBody] NewMessageModel input) { + var observer = _services.GetRequiredService(); + using var container = observer.SubscribeObservers>(conversationId); + var conv = _services.GetRequiredService(); var inputMsg = new RoleDialogModel(AgentRole.User, input.Text) { @@ -357,7 +362,6 @@ public class ConversationController : ControllerBase SetStates(conv, input); var response = new ChatResponseModel(); - await conv.SendMessage(agentId, inputMsg, replyMessage: input.Postback, async msg => @@ -381,6 +385,12 @@ public class ConversationController : ControllerBase [HttpPost("/conversation/{agentId}/{conversationId}/sse")] public async Task SendMessageSse([FromRoute] string agentId, [FromRoute] string conversationId, [FromBody] NewMessageModel input) { + var observer = _services.GetRequiredService(); + using var container = observer.SubscribeObservers>(conversationId, listeners: new() + { + { ChatEvent.OnIndicationReceived, async data => await OnReceiveToolCallIndication(conversationId, data.Data) } + }); + var conv = _services.GetRequiredService(); var inputMsg = new RoleDialogModel(AgentRole.User, input.Text) { @@ -406,7 +416,6 @@ public class ConversationController : ControllerBase Response.Headers.Append(Microsoft.Net.Http.Headers.HeaderNames.ContentType, "text/event-stream"); Response.Headers.Append(Microsoft.Net.Http.Headers.HeaderNames.CacheControl, "no-cache"); Response.Headers.Append(Microsoft.Net.Http.Headers.HeaderNames.Connection, "keep-alive"); - InitProgressService(conversationId); await conv.SendMessage(agentId, inputMsg, replyMessage: input.Postback, @@ -430,23 +439,18 @@ public class ConversationController : ControllerBase // await OnEventCompleted(Response); } - private void InitProgressService(string conversationId) + private async Task OnReceiveToolCallIndication(string conversationId, RoleDialogModel msg) { - var progressService = _services.GetService(); - progressService.OnFunctionExecuting = async msg => + var indicator = new ChatResponseModel { - var indicator = new ChatResponseModel - { - ConversationId = conversationId, - MessageId = msg.MessageId, - Text = msg.Indication, - Function = "indicating", - Instruction = msg.Instruction, - States = new Dictionary() - }; - await OnChunkReceived(Response, indicator); + ConversationId = conversationId, + MessageId = msg.MessageId, + Text = msg.Indication, + Function = "indicating", + Instruction = msg.Instruction, + States = new Dictionary() }; - progressService.OnFunctionExecuted = async msg => { }; + await OnChunkReceived(Response, indicator); } #endregion diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/KnowledgeBaseController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/KnowledgeBaseController.cs index 0f7b440c..f8bcacef 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/KnowledgeBaseController.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/KnowledgeBaseController.cs @@ -2,6 +2,7 @@ using BotSharp.Abstraction.Files.Utilities; using BotSharp.Abstraction.Graph.Models; using BotSharp.Abstraction.VectorStorage.Models; using BotSharp.OpenAPI.ViewModels.Knowledges; +using BotSharp.OpenAPI.ViewModels.Knowledges.Request; namespace BotSharp.OpenAPI.Controllers; @@ -59,6 +60,7 @@ public class KnowledgeBaseController : ControllerBase var options = new VectorSearchOptions { Fields = request.Fields, + FilterGroups = request.FilterGroups, Limit = request.Limit ?? 5, Confidence = request.Confidence ?? 0.5f, WithVector = request.WithVector @@ -72,8 +74,7 @@ public class KnowledgeBaseController : ControllerBase public async Task> GetPagedVectorCollectionData([FromRoute] string collection, [FromBody] VectorFilter filter) { var data = await _knowledgeService.GetPagedVectorCollectionData(collection, filter); - var items = data.Items?.Select(x => VectorKnowledgeViewModel.From(x))? - .ToList() ?? new List(); + var items = data.Items?.Select(x => VectorKnowledgeViewModel.From(x))?.ToList() ?? []; return new StringIdPagedItems { @@ -97,6 +98,19 @@ public class KnowledgeBaseController : ControllerBase return created; } + [HttpGet("/knowledge/vector/{collection}/points")] + public async Task> GetVectorCollectionData([FromRoute] string collection, [FromQuery] QueryVectorDataRequest request) + { + var options = new VectorQueryOptions + { + WithPayload = request.WithPayload, + WithVector = request.WithVector + }; + + var points = await _knowledgeService.GetVectorCollectionData(collection, request.Ids, options); + return points.Select(x => VectorKnowledgeViewModel.From(x)); + } + [HttpPut("/knowledge/vector/{collection}/update")] public async Task UpdateVectorKnowledge([FromRoute] string collection, [FromBody] VectorKnowledgeUpdateRequest request) { @@ -126,6 +140,21 @@ public class KnowledgeBaseController : ControllerBase #endregion + #region Index + [HttpPost("/knowledge/vector/{collection}/payload/indexes")] + public async Task> CreateCollectionPayloadIndexes([FromRoute] string collection, [FromBody] CreateVectorCollectionIndexRequest request) + { + return await _knowledgeService.CreateVectorCollectionPayloadIndexes(collection, request.Options); + } + + [HttpDelete("/knowledge/vector/{collection}/payload/indexes")] + public async Task> DeleteCollectionPayloadIndexes([FromRoute] string collection, [FromBody] DeleteVectorCollectionIndexRequest request) + { + return await _knowledgeService.DeleteVectorCollectionPayloadIndexes(collection, request.Options); + } + #endregion + + #region Snapshot [HttpGet("/knowledge/vector/{collection}/snapshots")] public async Task> GetVectorCollectionSnapshots([FromRoute] string collection) @@ -263,6 +292,7 @@ public class KnowledgeBaseController : ControllerBase } #endregion + #region Private methods private FileStreamResult BuildFileResult(string fileName, BinaryData fileData) { diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/RealtimeController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/RealtimeController.cs index 61ae77a7..ff3aca46 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/RealtimeController.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/RealtimeController.cs @@ -26,7 +26,7 @@ public class RealtimeController : ControllerBase FunctionName = functionName, FunctionArgs = JsonSerializer.Serialize(args) }; - await routing.InvokeFunction(functionName, message, from: InvokeSource.Llm); + await routing.InvokeFunction(functionName, message, options: new() { From = InvokeSource.Llm }); return message.Content; } } diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/Request/QueryVectorDataRequest.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/Request/QueryVectorDataRequest.cs new file mode 100644 index 00000000..b3a3841b --- /dev/null +++ b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/Request/QueryVectorDataRequest.cs @@ -0,0 +1,8 @@ +namespace BotSharp.OpenAPI.ViewModels.Knowledges.Request; + +public class QueryVectorDataRequest +{ + public List Ids { get; set; } = []; + public bool WithVector { get; set; } + public bool WithPayload { get; set; } +} diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/Request/SearchVectorKnowledgeRequest.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/Request/SearchVectorKnowledgeRequest.cs index 3f11a486..50a2fc5c 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/Request/SearchVectorKnowledgeRequest.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/Request/SearchVectorKnowledgeRequest.cs @@ -1,3 +1,4 @@ +using BotSharp.Abstraction.VectorStorage.Models; using System.Text.Json.Serialization; namespace BotSharp.OpenAPI.ViewModels.Knowledges; @@ -10,6 +11,9 @@ public class SearchVectorKnowledgeRequest [JsonPropertyName("fields")] public IEnumerable? Fields { get; set; } + [JsonPropertyName("filter_groups")] + public IEnumerable? FilterGroups { get; set; } + [JsonPropertyName("limit")] public int? Limit { get; set; } = 5; diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/Request/VectorCollectionIndexRequest.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/Request/VectorCollectionIndexRequest.cs new file mode 100644 index 00000000..fada7032 --- /dev/null +++ b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/Request/VectorCollectionIndexRequest.cs @@ -0,0 +1,13 @@ +using BotSharp.Abstraction.VectorStorage.Models; + +namespace BotSharp.OpenAPI.ViewModels.Knowledges.Request; + +public class CreateVectorCollectionIndexRequest +{ + public IEnumerable Options { get; set; } = []; +} + +public class DeleteVectorCollectionIndexRequest +{ + public IEnumerable Options { get; set; } = []; +} \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/Request/VectorKnowledgeCreateRequest.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/Request/VectorKnowledgeCreateRequest.cs index 9477d65d..ddcee3c0 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/Request/VectorKnowledgeCreateRequest.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/Request/VectorKnowledgeCreateRequest.cs @@ -8,7 +8,7 @@ public class VectorKnowledgeCreateRequest public string Text { get; set; } [JsonPropertyName("data_source")] - public string DataSource { get; set; } = VectorDataSource.Api; + public string DataSource { get; set; } = string.Empty; [JsonPropertyName("payload")] public Dictionary? Payload { get; set; } diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/View/VectorKnowledgeViewModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/View/VectorKnowledgeViewModel.cs index bbd7dc3a..fb1e6cbf 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/View/VectorKnowledgeViewModel.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/View/VectorKnowledgeViewModel.cs @@ -15,9 +15,9 @@ public class VectorKnowledgeViewModel [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public double? Score { get; set; } - [JsonPropertyName("vector")] + [JsonPropertyName("vector_dimension")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public float[]? Vector { get; set; } + public int? VectorDimension { get; set; } public static VectorKnowledgeViewModel From(VectorSearchResult result) @@ -27,7 +27,17 @@ public class VectorKnowledgeViewModel Id = result.Id, Data = result.Data, Score = result.Score, - Vector = result.Vector + VectorDimension = result.Vector?.Length + }; + } + + public static VectorKnowledgeViewModel From(VectorCollectionData data) + { + return new VectorKnowledgeViewModel + { + Id = data.Id, + Data = data.Data, + VectorDimension = data.Vector?.Length }; } } diff --git a/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/Chat/ChatCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/Chat/ChatCompletionProvider.cs index 605bceb8..ca2a283e 100644 --- a/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/Chat/ChatCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/Chat/ChatCompletionProvider.cs @@ -1,9 +1,9 @@ -using Azure; +using BotSharp.Abstraction.Conversations.Enums; using BotSharp.Abstraction.Files.Utilities; using BotSharp.Abstraction.Hooks; -using BotSharp.Abstraction.Observables.Models; +using BotSharp.Abstraction.MessageHub.Models; using BotSharp.Core.Infrastructures.Streams; -using BotSharp.Core.Observables.Queues; +using BotSharp.Core.MessageHub; using OpenAI.Chat; using System.ClientModel; @@ -212,7 +212,8 @@ public class ChatCompletionProvider : IChatCompletion var chatClient = client.GetChatClient(_model); var (prompt, messages, options) = PrepareOptions(agent, conversations); - var hub = _services.GetRequiredService>(); + var hub = _services.GetRequiredService>>(); + var conv = _services.GetRequiredService(); var messageId = conversations.LastOrDefault()?.MessageId ?? string.Empty; var contentHooks = _services.GetHooks(agent.Id); @@ -224,8 +225,8 @@ public class ChatCompletionProvider : IChatCompletion hub.Push(new() { - ServiceProvider = _services, - EventName = "BeforeReceiveLlmStreamMessage", + EventName = ChatEvent.BeforeReceiveLlmStreamMessage, + RefId = conv.ConversationId, Data = new RoleDialogModel(AgentRole.Assistant, string.Empty) { CurrentAgentId = agent.Id, @@ -268,8 +269,8 @@ public class ChatCompletionProvider : IChatCompletion }; hub.Push(new() { - ServiceProvider = _services, - EventName = "OnReceiveLlmStreamMessage", + EventName = ChatEvent.OnReceiveLlmStreamMessage, + RefId = conv.ConversationId, Data = content }); } @@ -311,8 +312,8 @@ public class ChatCompletionProvider : IChatCompletion hub.Push(new() { - ServiceProvider = _services, - EventName = "AfterReceiveLlmStreamMessage", + EventName = ChatEvent.AfterReceiveLlmStreamMessage, + RefId = conv.ConversationId, Data = responseMessage }); diff --git a/src/Plugins/BotSharp.Plugin.ChartHandler/BotSharp.Plugin.ChartHandler.csproj b/src/Plugins/BotSharp.Plugin.ChartHandler/BotSharp.Plugin.ChartHandler.csproj new file mode 100644 index 00000000..784b2405 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.ChartHandler/BotSharp.Plugin.ChartHandler.csproj @@ -0,0 +1,34 @@ + + + + $(TargetFramework) + enable + $(LangVersion) + $(BotSharpVersion) + $(GeneratePackageOnBuild) + $(GenerateDocumentationFile) + $(SolutionDir)packages + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + + + + + diff --git a/src/Plugins/BotSharp.Plugin.ChartHandler/ChartHandlerPlugin.cs b/src/Plugins/BotSharp.Plugin.ChartHandler/ChartHandlerPlugin.cs new file mode 100644 index 00000000..1161f6e4 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.ChartHandler/ChartHandlerPlugin.cs @@ -0,0 +1,18 @@ +using Microsoft.Extensions.Configuration; + +namespace BotSharp.Plugin.ChartHandler; + +public class ChartHandlerPlugin : IBotSharpPlugin +{ + public string Id => "9dacac1d-2e29-4f01-9d66-b0201f05a9fa"; + public string Name => "Chart Plotter"; + public string Description => "AI plots chart"; + public string IconUrl => "https://cdn-icons-png.flaticon.com/512/423/423786.png"; + public string[] AgentIds => []; + + public void RegisterDI(IServiceCollection services, IConfiguration config) + { + services.AddScoped(); + } + +} diff --git a/src/Plugins/BotSharp.Plugin.ChartHandler/Enums/UtilityName.cs b/src/Plugins/BotSharp.Plugin.ChartHandler/Enums/UtilityName.cs new file mode 100644 index 00000000..ee97a4a5 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.ChartHandler/Enums/UtilityName.cs @@ -0,0 +1,6 @@ +namespace BotSharp.Plugin.ChartHandler.Enums; + +public class UtilityName +{ + public const string ChartPlotter = "chart-plotter"; +} diff --git a/src/Plugins/BotSharp.Plugin.ChartHandler/Functions/PlotChartFn.cs b/src/Plugins/BotSharp.Plugin.ChartHandler/Functions/PlotChartFn.cs new file mode 100644 index 00000000..ad8de0d7 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.ChartHandler/Functions/PlotChartFn.cs @@ -0,0 +1,82 @@ +using BotSharp.Abstraction.Messaging.Models.RichContent.Template; + +namespace BotSharp.Plugin.ChartHandler.Functions; + +public class PlotChartFn : IFunctionCallback +{ + private readonly IServiceProvider _services; + private readonly ILogger _logger; + + public string Name => "util-chart-plot_chart"; + public string Indication => "Plotting chart"; + + + public PlotChartFn( + IServiceProvider services, + ILogger logger) + { + _services = services; + _logger = logger; + } + + public async Task Execute(RoleDialogModel message) + { + var db = _services.GetRequiredService(); + var agentService = _services.GetRequiredService(); + var convService = _services.GetRequiredService(); + + var args = JsonSerializer.Deserialize(message.FunctionArgs); + var agent = await agentService.GetAgent(message.CurrentAgentId); + var inst = db.GetAgentTemplate(BuiltInAgentId.UtilityAssistant, "util-chart-plot_instruction"); + var innerAgent = new Agent + { + Id = agent.Id, + Name = agent.Name, + Instruction = inst, + TemplateDict = new Dictionary + { + { "plotting_requirement", args?.PlottingRequirement ?? string.Empty }, + { "chart_element_id", $"chart-{message.MessageId}" } + } + }; + + var response = await GetChatCompletion(innerAgent, [ + new RoleDialogModel(AgentRole.User, "Please follow the instruction to generate the javascript code.") + { + CurrentAgentId = message.CurrentAgentId, + MessageId = message.MessageId + } + ]); + + var obj = response.JsonContent(); + message.Content = obj?.GreetingMessage ?? "Here is the chart you ask for:"; + message.RichContent = new RichContent + { + Recipient = new Recipient { Id = convService.ConversationId }, + Message = new ProgramCodeTemplateMessage + { + Text = obj?.JsCode ?? string.Empty, + Language = "javascript" + } + }; + message.StopCompletion = true; + return true; + } + + private async Task GetChatCompletion(Agent agent, List dialogs) + { + try + { + var llmProviderService = _services.GetRequiredService(); + var completion = CompletionProvider.GetChatCompletion(_services, provider: "openai", model: "gpt-4.1"); + var response = await completion.GetChatCompletions(agent, dialogs); + return response.Content; + } + catch (Exception ex) + { + var error = $"Error when plotting chart. {ex.Message}"; + _logger.LogWarning(ex, error); + return error; + } + } +} diff --git a/src/Plugins/BotSharp.Plugin.ChartHandler/Hooks/ChartHandlerUtilityHook.cs b/src/Plugins/BotSharp.Plugin.ChartHandler/Hooks/ChartHandlerUtilityHook.cs new file mode 100644 index 00000000..02793ffe --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.ChartHandler/Hooks/ChartHandlerUtilityHook.cs @@ -0,0 +1,24 @@ +namespace BotSharp.Plugin.ChartHandler.Hooks; + +public class ChartHandlerUtilityHook : IAgentUtilityHook +{ + private const string PLOT_CHART_FN = "util-chart-plot_chart"; + + public void AddUtilities(List utilities) + { + var item = new AgentUtility + { + Category = "chart", + Name = UtilityName.ChartPlotter, + Items = [ + new UtilityItem + { + FunctionName = PLOT_CHART_FN, + TemplateName = $"{PLOT_CHART_FN}.fn" + } + ] + }; + + utilities.Add(item); + } +} diff --git a/src/Plugins/BotSharp.Plugin.ChartHandler/LlmContext/LlmContextIn.cs b/src/Plugins/BotSharp.Plugin.ChartHandler/LlmContext/LlmContextIn.cs new file mode 100644 index 00000000..daaef271 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.ChartHandler/LlmContext/LlmContextIn.cs @@ -0,0 +1,10 @@ +using System.Text.Json.Serialization; + +namespace BotSharp.Plugin.ChartHandler.LlmContext; + +public class LlmContextIn +{ + [JsonPropertyName("plotting_requirement")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? PlottingRequirement { get; set; } +} diff --git a/src/Plugins/BotSharp.Plugin.ChartHandler/LlmContext/LlmContextOut.cs b/src/Plugins/BotSharp.Plugin.ChartHandler/LlmContext/LlmContextOut.cs new file mode 100644 index 00000000..7109f72d --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.ChartHandler/LlmContext/LlmContextOut.cs @@ -0,0 +1,14 @@ +using System.Text.Json.Serialization; + +namespace BotSharp.Plugin.ChartHandler.LlmContext; + +public class LlmContextOut +{ + [JsonPropertyName("greeting_message")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? GreetingMessage { get; set; } + + [JsonPropertyName("js_code")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? JsCode { get; set; } +} diff --git a/src/Plugins/BotSharp.Plugin.ChartHandler/Using.cs b/src/Plugins/BotSharp.Plugin.ChartHandler/Using.cs new file mode 100644 index 00000000..fa94465c --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.ChartHandler/Using.cs @@ -0,0 +1,32 @@ +global using System; +global using System.Collections.Generic; +global using System.Text; +global using System.Linq; +global using System.Text.Json; +global using System.Net.Mime; +global using System.Threading.Tasks; +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.Logging; +global using BotSharp.Abstraction.Agents; +global using BotSharp.Abstraction.Conversations; +global using BotSharp.Abstraction.Plugins; +global using BotSharp.Abstraction.Conversations.Models; +global using BotSharp.Abstraction.Functions; +global using BotSharp.Abstraction.Agents.Models; +global using BotSharp.Abstraction.Agents.Enums; +global using BotSharp.Abstraction.Files.Enums; +global using BotSharp.Abstraction.Files.Models; +global using BotSharp.Abstraction.Files; +global using BotSharp.Abstraction.MLTasks; +global using BotSharp.Abstraction.Utilities; +global using BotSharp.Abstraction.Agents.Settings; +global using BotSharp.Abstraction.Functions.Models; +global using BotSharp.Abstraction.Repositories; +global using BotSharp.Abstraction.Settings; +global using BotSharp.Abstraction.Messaging; +global using BotSharp.Abstraction.Messaging.Models.RichContent; +global using BotSharp.Abstraction.Options; +global using BotSharp.Core.Infrastructures; +global using BotSharp.Plugin.ChartHandler.Enums; +global using BotSharp.Plugin.ChartHandler.LlmContext; +global using BotSharp.Plugin.ChartHandler.Hooks; \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.ChartHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-chart-plot_chart.json b/src/Plugins/BotSharp.Plugin.ChartHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-chart-plot_chart.json new file mode 100644 index 00000000..da950832 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.ChartHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-chart-plot_chart.json @@ -0,0 +1,14 @@ +{ + "name": "util-chart-plot_chart", + "description": "If the user requests you plotting or generating charts, you can call this function to generate charts in any format that user requested.", + "parameters": { + "type": "object", + "properties": { + "plotting_requirement": { + "type": "string", + "description": "The requirement that user posted for plotting or generating charts." + } + }, + "required": [ "plotting_requirement" ] + } +} \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.ChartHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-chart-plot_chart.fn.liquid b/src/Plugins/BotSharp.Plugin.ChartHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-chart-plot_chart.fn.liquid new file mode 100644 index 00000000..e54b3c4a --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.ChartHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-chart-plot_chart.fn.liquid @@ -0,0 +1 @@ +Please call function util-chart-plot_chart if user wants to plot or generate charts. \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.ChartHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-chart-plot_instruction.liquid b/src/Plugins/BotSharp.Plugin.ChartHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-chart-plot_instruction.liquid new file mode 100644 index 00000000..9bffa73f --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.ChartHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-chart-plot_instruction.liquid @@ -0,0 +1,22 @@ +Please take a look at "Plotting Requirement" and generate a javascript code that can be used to render the charts on an html element. + + +=== Plotting Requirement === +{{ plotting_requirement }} + + +***** Important ***** +** Your output must be a single block with everything needed inside. +** You need to import Plotly.js to plot the charts. The script source should be "https://cdn.plot.ly/plotly-3.0.1.min.js". +** You need to add the MODE bar for each chart you plot. +** You must render the charts on the div html element with id {{ chart_element_id }}. +** You must not create any new html element. +** You must not apply any styles on any html element. +** You must generate as less token as possible. + +*** Response Format *** +You must output the response in the following JSON format: +{ + "greeting_message": "A short polite message that informs user that the charts have been generated.", + "js_code": "The javascript code that can generate the charts as requested." +} \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.ChatHub/ChatHubPlugin.cs b/src/Plugins/BotSharp.Plugin.ChatHub/ChatHubPlugin.cs index d3c3f2ac..51466692 100644 --- a/src/Plugins/BotSharp.Plugin.ChatHub/ChatHubPlugin.cs +++ b/src/Plugins/BotSharp.Plugin.ChatHub/ChatHubPlugin.cs @@ -1,6 +1,8 @@ using BotSharp.Abstraction.Crontab; -using BotSharp.Abstraction.Observables.Models; -using BotSharp.Core.Observables.Queues; +using BotSharp.Abstraction.MessageHub.Models; +using BotSharp.Abstraction.MessageHub.Observers; +using BotSharp.Core.MessageHub; +using BotSharp.Core.MessageHub.Observers; using BotSharp.Plugin.ChatHub.Hooks; using BotSharp.Plugin.ChatHub.Observers; using Microsoft.AspNetCore.Builder; @@ -11,7 +13,7 @@ namespace BotSharp.Plugin.ChatHub; /// /// The dialogue channel connects users, AI assistants and customer service representatives. /// -public class ChatHubPlugin : IBotSharpPlugin, IBotSharpAppPlugin +public class ChatHubPlugin : IBotSharpPlugin { public string Id => "6e52d42d-1e23-406b-8599-36af36c83209"; public string Name => "Chat Hub"; @@ -24,6 +26,8 @@ public class ChatHubPlugin : IBotSharpPlugin, IBotSharpAppPlugin config.Bind("ChatHub", settings); services.AddSingleton(x => settings); + services.AddScoped>, ChatHubObserver>(); + // Register hooks services.AddScoped(); services.AddScoped(); @@ -32,12 +36,4 @@ public class ChatHubPlugin : IBotSharpPlugin, IBotSharpAppPlugin services.AddScoped(); services.AddScoped(); } - - public void Configure(IApplicationBuilder app) - { - var services = app.ApplicationServices; - var queue = services.GetRequiredService>(); - var logger = services.GetRequiredService>>(); - queue.Events.Subscribe(new ChatHubObserver(logger)); - } } diff --git a/src/Plugins/BotSharp.Plugin.ChatHub/Helpers/EventEmitter.cs b/src/Plugins/BotSharp.Plugin.ChatHub/Helpers/EventEmitter.cs new file mode 100644 index 00000000..821b173e --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.ChatHub/Helpers/EventEmitter.cs @@ -0,0 +1,39 @@ +using Microsoft.AspNetCore.SignalR; +using System.Runtime.CompilerServices; + +namespace BotSharp.Plugin.ChatHub.Helpers; + +internal class EventEmitter +{ + internal static async Task SendChatEvent( + IServiceProvider services, + ILogger logger, + string @event, + string conversationId, + string userId, + T data, + string callerClass = "", + [CallerMemberName] string callerMethod = "", + LogLevel logLevel = LogLevel.Warning) + { + try + { + var settings = services.GetRequiredService(); + var chatHub = services.GetRequiredService>(); + + switch (settings.EventDispatchBy) + { + case EventDispatchType.Group when !string.IsNullOrEmpty(conversationId): + await chatHub.Clients.Group(conversationId).SendAsync(@event, data); + break; + case EventDispatchType.User when !string.IsNullOrEmpty(userId): + await chatHub.Clients.User(userId).SendAsync(@event, data); + break; + } + } + catch (Exception ex) + { + logger.Log(logLevel, ex, $"Failed to send event '{@event}' in ({callerClass}-{callerMethod}) (conversation id: {conversationId})"); + } + } +} diff --git a/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/ChatHubConversationHook.cs b/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/ChatHubConversationHook.cs index 1d641554..3831ecd7 100644 --- a/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/ChatHubConversationHook.cs +++ b/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/ChatHubConversationHook.cs @@ -1,8 +1,11 @@ using BotSharp.Abstraction.Conversations.Dtos; +using BotSharp.Abstraction.Conversations.Enums; using BotSharp.Abstraction.Routing.Enums; +using BotSharp.Abstraction.Routing.Models; using BotSharp.Abstraction.SideCar; using BotSharp.Abstraction.Users.Dtos; using Microsoft.AspNetCore.SignalR; +using System.Runtime.CompilerServices; namespace BotSharp.Plugin.ChatHub.Hooks; @@ -15,15 +18,6 @@ public class ChatHubConversationHook : ConversationHookBase private readonly BotSharpOptions _options; private readonly ChatHubSettings _settings; - #region Events - private const string INIT_CLIENT_CONVERSATION = "OnConversationInitFromClient"; - private const string RECEIVE_CLIENT_MESSAGE = "OnMessageReceivedFromClient"; - 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( IServiceProvider services, IHubContext chatHub, @@ -51,7 +45,7 @@ public class ChatHubConversationHook : ConversationHookBase var user = await userService.GetUser(conv.User.Id); conv.User = UserDto.FromUser(user); - await InitClientConversation(conv.Id, conv); + await SendEvent(ChatEvent.OnConversationInitFromClient, conv.Id, conv); await base.OnConversationInitialized(conversation); } @@ -72,7 +66,7 @@ public class ChatHubConversationHook : ConversationHookBase Text = !string.IsNullOrEmpty(message.SecondaryContent) ? message.SecondaryContent : message.Content, Sender = UserDto.FromUser(sender) }; - await ReceiveClientMessage(conv.ConversationId, model); + await SendEvent(ChatEvent.OnMessageReceivedFromClient, conv.ConversationId, model); // Send typing-on to client var action = new ConversationSenderActionModel @@ -80,23 +74,13 @@ public class ChatHubConversationHook : ConversationHookBase ConversationId = conv.ConversationId, SenderAction = SenderActionEnum.TypingOn }; - - await GenerateSenderAction(conv.ConversationId, action); + await SendEvent(ChatEvent.OnSenderActionGenerated, conv.ConversationId, action); await base.OnMessageReceived(message); } - public override async Task OnFunctionExecuting(RoleDialogModel message, string from = InvokeSource.Manual) + public override async Task OnFunctionExecuting(RoleDialogModel message, InvokeFunctionOptions? options = null) { - var conv = _services.GetRequiredService(); - var action = new ConversationSenderActionModel - { - ConversationId = conv.ConversationId, - SenderAction = SenderActionEnum.TypingOn, - Indication = message.Indication - }; - - await GenerateSenderAction(conv.ConversationId, action); - await base.OnFunctionExecuting(message, from: from); + await base.OnFunctionExecuting(message, options); } public override async Task OnPostbackMessageReceived(RoleDialogModel message, PostbackMessageModel replyMsg) @@ -110,7 +94,7 @@ public class ChatHubConversationHook : ConversationHookBase var conv = _services.GetRequiredService(); var state = _services.GetRequiredService(); - var json = JsonSerializer.Serialize(new ChatResponseDto() + var data = new ChatResponseDto() { ConversationId = conv.ConversationId, MessageId = message.MessageId, @@ -126,7 +110,7 @@ public class ChatHubConversationHook : ConversationHookBase LastName = "Assistant", Role = AgentRole.Assistant } - }, _options.JsonSerializerOptions); + }; // Send typing-off to client var action = new ConversationSenderActionModel @@ -134,13 +118,9 @@ public class ChatHubConversationHook : ConversationHookBase ConversationId = conv.ConversationId, SenderAction = SenderActionEnum.TypingOff }; + await SendEvent(ChatEvent.OnSenderActionGenerated, conv.ConversationId, action); - if (!message.IsStreaming) - { - await GenerateSenderAction(conv.ConversationId, action); - } - - await ReceiveAssistantMessage(conv.ConversationId, json); + await SendEvent(ChatEvent.OnMessageReceivedFromAssistant, conv.ConversationId, data); await base.OnResponseGenerated(message); } @@ -148,7 +128,7 @@ public class ChatHubConversationHook : ConversationHookBase public override async Task OnNotificationGenerated(RoleDialogModel message) { var conv = _services.GetRequiredService(); - var json = JsonSerializer.Serialize(new ChatResponseDto() + var data = new ChatResponseDto() { ConversationId = conv.ConversationId, MessageId = message.MessageId, @@ -162,9 +142,9 @@ public class ChatHubConversationHook : ConversationHookBase LastName = "Assistant", Role = AgentRole.Assistant } - }, _options.JsonSerializerOptions); + }; - await GenerateNotification(conv.ConversationId, json); + await SendEvent(ChatEvent.OnNotificationGenerated, conv.ConversationId, data); await base.OnNotificationGenerated(message); } @@ -177,7 +157,7 @@ public class ChatHubConversationHook : ConversationHookBase MessageId = messageId }; - await DeleteMessage(conversationId, model); + await SendEvent(ChatEvent.OnMessageDeleted, conversationId, model); await base.OnMessageDeleted(conversationId, messageId); } @@ -188,119 +168,11 @@ public class ChatHubConversationHook : ConversationHookBase return sidecar == null || !sidecar.IsEnabled; } - private async Task InitClientConversation(string conversationId, ConversationDto conversation) + private async Task SendEvent(string @event, string conversationId, T data, [CallerMemberName] string callerName = "") { - try - { - if (_settings.EventDispatchBy == EventDispatchType.Group) - { - await _chatHub.Clients.Group(conversationId).SendAsync(INIT_CLIENT_CONVERSATION, conversation); - } - else - { - await _chatHub.Clients.User(_user.Id).SendAsync(INIT_CLIENT_CONVERSATION, conversation); - } - } - catch (Exception ex) - { - _logger.LogWarning(ex, $"Failed to init client conversation in {nameof(ChatHubConversationHook)} (conversation id: {conversationId})"); - } - } - - private async Task ReceiveClientMessage(string conversationId, ChatResponseDto model) - { - try - { - if (_settings.EventDispatchBy == EventDispatchType.Group) - { - await _chatHub.Clients.Group(conversationId).SendAsync(RECEIVE_CLIENT_MESSAGE, model); - } - else - { - await _chatHub.Clients.User(_user.Id).SendAsync(RECEIVE_CLIENT_MESSAGE, model); - } - } - catch (Exception ex) - { - _logger.LogWarning(ex, $"Failed to receive assistant message in {nameof(ChatHubConversationHook)} (conversation id: {conversationId})"); - } - } - - private async Task ReceiveAssistantMessage(string conversationId, string? json) - { - try - { - if (_settings.EventDispatchBy == EventDispatchType.Group) - { - await _chatHub.Clients.Group(conversationId).SendAsync(RECEIVE_ASSISTANT_MESSAGE, json); - } - else - { - await _chatHub.Clients.User(_user.Id).SendAsync(RECEIVE_ASSISTANT_MESSAGE, json); - } - } - catch (Exception ex) - { - _logger.LogWarning(ex, $"Failed to receive assistant message in {nameof(ChatHubConversationHook)} (conversation id: {conversationId})"); - } - - } - - private async Task GenerateSenderAction(string conversationId, ConversationSenderActionModel action) - { - try - { - if (_settings.EventDispatchBy == EventDispatchType.Group) - { - await _chatHub.Clients.Group(conversationId).SendAsync(GENERATE_SENDER_ACTION, action); - } - else - { - await _chatHub.Clients.User(_user.Id).SendAsync(GENERATE_SENDER_ACTION, action); - } - } - catch (Exception ex) - { - _logger.LogWarning(ex, $"Failed to generate sender action in {nameof(ChatHubConversationHook)} (conversation id: {conversationId})"); - } - } - - private async Task DeleteMessage(string conversationId, ChatResponseDto model) - { - try - { - if (_settings.EventDispatchBy == EventDispatchType.Group) - { - await _chatHub.Clients.Group(conversationId).SendAsync(DELETE_MESSAGE, model); - } - else - { - await _chatHub.Clients.User(_user.Id).SendAsync(DELETE_MESSAGE, model); - } - } - catch (Exception ex) - { - _logger.LogWarning(ex, $"Failed to delete message in {nameof(ChatHubConversationHook)} (conversation id: {conversationId})"); - } - } - - private async Task GenerateNotification(string conversationId, string? json) - { - try - { - if (_settings.EventDispatchBy == EventDispatchType.Group) - { - await _chatHub.Clients.Group(conversationId).SendAsync(GENERATE_NOTIFICATION, json); - } - else - { - await _chatHub.Clients.User(_user.Id).SendAsync(GENERATE_NOTIFICATION, json); - } - } - catch (Exception ex) - { - _logger.LogWarning(ex, $"Failed to generate notification in {nameof(ChatHubConversationHook)} (conversation id: {conversationId})"); - } + var user = _services.GetRequiredService(); + var json = JsonSerializer.Serialize(data, _options.JsonSerializerOptions); + await EventEmitter.SendChatEvent(_services, _logger, @event, conversationId, user?.Id, json, nameof(ChatHubConversationHook), callerName); } #endregion } diff --git a/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/ChatHubCrontabHook.cs b/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/ChatHubCrontabHook.cs index ef956670..dd584f17 100644 --- a/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/ChatHubCrontabHook.cs +++ b/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/ChatHubCrontabHook.cs @@ -1,4 +1,5 @@ using BotSharp.Abstraction.Conversations.Dtos; +using BotSharp.Abstraction.Conversations.Enums; using BotSharp.Abstraction.Crontab; using BotSharp.Abstraction.Crontab.Models; using Microsoft.AspNetCore.SignalR; @@ -14,11 +15,8 @@ public class ChatHubCrontabHook : ICrontabHook private readonly BotSharpOptions _options; private readonly ChatHubSettings _settings; - #region Events - private const string GENERATE_NOTIFICATION = "OnNotificationGenerated"; - #endregion - - public ChatHubCrontabHook(IServiceProvider services, + public ChatHubCrontabHook( + IServiceProvider services, IHubContext chatHub, ILogger logger, IUserIdentity user, @@ -35,7 +33,7 @@ public class ChatHubCrontabHook : ICrontabHook public async Task OnCronTriggered(CrontabItem item) { - var json = JsonSerializer.Serialize(new ChatResponseDto() + var data = new ChatResponseDto() { ConversationId = item.ConversationId, MessageId = Guid.NewGuid().ToString(), @@ -47,16 +45,17 @@ public class ChatHubCrontabHook : ICrontabHook LastName = "AI", Role = AgentRole.Assistant } - }, _options.JsonSerializerOptions); + }; - await SendEvent(item, json); + await SendEvent(item, data); } - private async Task SendEvent(CrontabItem item, string json) + private async Task SendEvent(CrontabItem item, ChatResponseDto data) { try { - await _chatHub.Clients.User(item.UserId).SendAsync(GENERATE_NOTIFICATION, json); + var json = JsonSerializer.Serialize(data, _options.JsonSerializerOptions); + await _chatHub.Clients.User(item.UserId).SendAsync(ChatEvent.OnNotificationGenerated, json); } catch { } } diff --git a/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/StreamingLogHook.cs b/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/StreamingLogHook.cs index 5dd89a2b..7156f29f 100644 --- a/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/StreamingLogHook.cs +++ b/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/StreamingLogHook.cs @@ -1,5 +1,8 @@ +using BotSharp.Abstraction.Conversations.Enums; using BotSharp.Abstraction.Routing.Enums; +using BotSharp.Abstraction.Routing.Models; using Microsoft.AspNetCore.SignalR; +using System.Runtime.CompilerServices; using System.Text.Encodings.Web; using System.Text.Unicode; @@ -19,13 +22,6 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR private readonly IAgentService _agentService; private readonly IRoutingContext _routingCtx; - #region Events - private const string CONTENT_LOG_GENERATED = "OnConversationContentLogGenerated"; - private const string STATE_LOG_GENERATED = "OnConversateStateLogGenerated"; - private const string AGENT_QUEUE_CHANGED = "OnAgentQueueChanged"; - private const string STATE_CHANGED = "OnStateChangeGenerated"; - #endregion - public StreamingLogHook( ConversationSetting convSettings, BotSharpOptions options, @@ -65,7 +61,7 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR Source = ContentLogSource.UserInput, Log = log }; - await SendContentLog(conversationId, input); + await SendEvent(ChatEvent.OnConversationContentLogGenerated, conversationId, BuildContentLog(input)); } public override async Task OnPostbackMessageReceived(RoleDialogModel message, PostbackMessageModel replyMsg) @@ -83,7 +79,7 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR Source = ContentLogSource.UserInput, Log = log }; - await SendContentLog(conversationId, input); + await SendEvent(ChatEvent.OnConversationContentLogGenerated, conversationId, BuildContentLog(input)); } public async Task OnSessionUpdated(Agent agent, string instruction, FunctionDef[] functions, bool isInit = false) @@ -112,7 +108,7 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR Source = ContentLogSource.Prompt, Log = log }; - await SendContentLog(conversationId, input); + await SendEvent(ChatEvent.OnConversationContentLogGenerated, conversationId, BuildContentLog(input)); } public async Task OnRenderingTemplate(Agent agent, string name, string content) @@ -134,7 +130,7 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR Source = ContentLogSource.HardRule, Log = log }; - await SendContentLog(conversationId, input); + await SendEvent(ChatEvent.OnConversationContentLogGenerated, conversationId, BuildContentLog(input)); } public async Task BeforeGenerating(Agent agent, List conversations) @@ -142,7 +138,7 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR if (!_convSettings.ShowVerboseLog) return; } - public override async Task OnFunctionExecuting(RoleDialogModel message, string from = InvokeSource.Manual) + public override async Task OnFunctionExecuting(RoleDialogModel message, InvokeFunctionOptions? options = null) { var conversationId = _state.GetConversationId(); if (string.IsNullOrEmpty(conversationId)) return; @@ -162,10 +158,10 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR Source = ContentLogSource.FunctionCall, Log = log }; - await SendContentLog(conversationId, input); + await SendEvent(ChatEvent.OnConversationContentLogGenerated, conversationId, BuildContentLog(input)); } - public override async Task OnFunctionExecuted(RoleDialogModel message, string from = InvokeSource.Manual) + public override async Task OnFunctionExecuted(RoleDialogModel message, InvokeFunctionOptions? options = null) { var conversationId = _state.GetConversationId(); if (string.IsNullOrEmpty(conversationId)) return; @@ -183,7 +179,7 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR Source = ContentLogSource.FunctionCall, Log = log }; - await SendContentLog(conversationId, input); + await SendEvent(ChatEvent.OnConversationContentLogGenerated, conversationId, BuildContentLog(input)); } /// @@ -210,7 +206,7 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR Source = ContentLogSource.Prompt, Log = log }; - await SendContentLog(conversationId, input); + await SendEvent(ChatEvent.OnConversationContentLogGenerated, conversationId, BuildContentLog(input)); } /// @@ -225,7 +221,8 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR var conv = _services.GetRequiredService(); var routingCtx = _services.GetRequiredService(); - await SendStateLog(conv.ConversationId, routingCtx.EntryAgentId, _state.GetStates(), message); + var stateLog = BuildStateLog(conv.ConversationId, routingCtx.EntryAgentId, _state.GetStates(), message); + await SendEvent(ChatEvent.OnConversateStateLogGenerated, conv.ConversationId, stateLog); if (message.Role == AgentRole.Assistant) { @@ -244,7 +241,7 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR Source = ContentLogSource.AgentResponse, Log = log }; - await SendContentLog(conversationId, input); + await SendEvent(ChatEvent.OnConversationContentLogGenerated, conversationId, BuildContentLog(input)); } } @@ -262,7 +259,7 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR Source = ContentLogSource.FunctionCall, Log = log }; - await SendContentLog(conversationId, input); + await SendEvent(ChatEvent.OnConversationContentLogGenerated, conversationId, BuildContentLog(input)); } public override async Task OnConversationEnding(RoleDialogModel message) @@ -279,7 +276,7 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR Source = ContentLogSource.FunctionCall, Log = log }; - await SendContentLog(conversationId, input); + await SendEvent(ChatEvent.OnConversationContentLogGenerated, conversationId, BuildContentLog(input)); } public override async Task OnBreakpointUpdated(string conversationId, bool resetStates) @@ -307,7 +304,7 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR }, Log = log }; - await SendContentLog(conversationId, input); + await SendEvent(ChatEvent.OnConversationContentLogGenerated, conversationId, BuildContentLog(input)); } public override async Task OnStateChanged(StateChangeModel stateChange) @@ -317,7 +314,7 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR if (stateChange == null) return; - await SendStateChange(conversationId, stateChange); + await SendEvent(ChatEvent.OnStateChangeGenerated, conversationId, BuildStateChangeLog(stateChange)); } #endregion @@ -331,7 +328,7 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR // Agent queue log var log = $"{agent.Name} is enqueued"; - await SendAgentQueueLog(conversationId, log); + await SendEvent(ChatEvent.OnAgentQueueChanged, conversationId, BuildAgentQueueChangedLog(conversationId, log)); // Content log log = $"{agent.Name} is enqueued{(reason != null ? $" ({reason})" : "")}"; @@ -346,7 +343,7 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR Source = ContentLogSource.HardRule, Log = log }; - await SendContentLog(conversationId, input); + await SendEvent(ChatEvent.OnConversationContentLogGenerated, conversationId, BuildContentLog(input)); } public async Task OnAgentDequeued(string agentId, string currentAgentId, string? reason = null) @@ -359,7 +356,7 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR // Agent queue log var log = $"{agent.Name} is dequeued"; - await SendAgentQueueLog(conversationId, log); + await SendEvent(ChatEvent.OnAgentQueueChanged, conversationId, BuildAgentQueueChangedLog(conversationId, log)); // Content log log = $"{agent.Name} is dequeued{(reason != null ? $" ({reason})" : "")}, current agent is {currentAgent?.Name}"; @@ -374,7 +371,7 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR Source = ContentLogSource.HardRule, Log = log }; - await SendContentLog(conversationId, input); + await SendEvent(ChatEvent.OnConversationContentLogGenerated, conversationId, BuildContentLog(input)); } public async Task OnAgentReplaced(string fromAgentId, string toAgentId, string? reason = null) @@ -387,7 +384,7 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR // Agent queue log var log = $"Agent queue is replaced from {fromAgent.Name} to {toAgent.Name}"; - await SendAgentQueueLog(conversationId, log); + await SendEvent(ChatEvent.OnAgentQueueChanged, conversationId, BuildAgentQueueChangedLog(conversationId, log)); // Content log log = $"{fromAgent.Name} is replaced to {toAgent.Name}{(reason != null ? $" ({reason})" : "")}"; @@ -402,7 +399,7 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR Source = ContentLogSource.HardRule, Log = log }; - await SendContentLog(conversationId, input); + await SendEvent(ChatEvent.OnConversationContentLogGenerated, conversationId, BuildContentLog(input)); } public async Task OnAgentQueueEmptied(string agentId, string? reason = null) @@ -412,7 +409,7 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR // Agent queue log var log = $"Agent queue is empty"; - await SendAgentQueueLog(conversationId, log); + await SendEvent(ChatEvent.OnAgentQueueChanged, conversationId, BuildAgentQueueChangedLog(conversationId, log)); // Content log log = reason ?? "Agent queue is cleared"; @@ -427,7 +424,7 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR Source = ContentLogSource.HardRule, Log = log }; - await SendContentLog(conversationId, input); + await SendEvent(ChatEvent.OnConversationContentLogGenerated, conversationId, BuildContentLog(input)); } public async Task OnRoutingInstructionReceived(FunctionCallFromLlm instruct, RoleDialogModel message) @@ -446,7 +443,7 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR Source = ContentLogSource.AgentResponse, Log = log }; - await SendContentLog(conversationId, input); + await SendEvent(ChatEvent.OnConversationContentLogGenerated, conversationId, BuildContentLog(input)); } public async Task OnRoutingInstructionRevised(FunctionCallFromLlm instruct, RoleDialogModel message) @@ -464,90 +461,20 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR Source = ContentLogSource.HardRule, Log = log }; - await SendContentLog(conversationId, input); + await SendEvent(ChatEvent.OnConversationContentLogGenerated, conversationId, BuildContentLog(input)); } #endregion #region Private methods - private async Task SendContentLog(string conversationId, ContentLogInputModel input) + private async Task SendEvent(string @event, string conversationId, T data, [CallerMemberName] string callerName = "") { - try - { - if (_settings.EventDispatchBy == EventDispatchType.Group) - { - await _chatHub.Clients.Group(conversationId).SendAsync(CONTENT_LOG_GENERATED, BuildContentLog(input)); - } - else - { - await _chatHub.Clients.User(_user.Id).SendAsync(CONTENT_LOG_GENERATED, BuildContentLog(input)); - } - } - catch (Exception ex) - { - _logger.LogWarning(ex, $"Failed to send content log in {nameof(StreamingLogHook)} (conversation id: {conversationId})."); - } + var user = _services.GetRequiredService(); + var json = JsonSerializer.Serialize(data, _options.JsonSerializerOptions); + await EventEmitter.SendChatEvent(_services, _logger, @event, conversationId, user?.Id, json, nameof(StreamingLogHook), callerName); } - private async Task SendStateLog(string conversationId, string agentId, Dictionary states, RoleDialogModel message) - { - try - { - if (_settings.EventDispatchBy == EventDispatchType.Group) - { - await _chatHub.Clients.Group(conversationId).SendAsync(STATE_LOG_GENERATED, BuildStateLog(conversationId, agentId, states, message)); - } - else - { - await _chatHub.Clients.User(_user.Id).SendAsync(STATE_LOG_GENERATED, BuildStateLog(conversationId, agentId, states, message)); - } - } - catch (Exception ex) - { - _logger.LogWarning(ex, $"Failed to send state log in {nameof(StreamingLogHook)} (conversation id: {conversationId})."); - } - } - - private async Task SendAgentQueueLog(string conversationId, string log) - { - try - { - if (_settings.EventDispatchBy == EventDispatchType.Group) - { - await _chatHub.Clients.Group(conversationId).SendAsync(AGENT_QUEUE_CHANGED, BuildAgentQueueChangedLog(conversationId, log)); - } - else - { - await _chatHub.Clients.User(_user.Id).SendAsync(AGENT_QUEUE_CHANGED, BuildAgentQueueChangedLog(conversationId, log)); - } - } - catch (Exception ex) - { - _logger.LogWarning(ex, $"Failed to send agent queue log in {nameof(StreamingLogHook)} (conversation id: {conversationId})."); - } - } - - private async Task SendStateChange(string conversationId, StateChangeModel stateChange) - { - try - { - if (_settings.EventDispatchBy == EventDispatchType.Group) - { - await _chatHub.Clients.Group(conversationId).SendAsync(STATE_CHANGED, BuildStateChangeLog(stateChange)); - } - else - { - await _chatHub.Clients.User(_user.Id).SendAsync(STATE_CHANGED, BuildStateChangeLog(stateChange)); - } - } - catch (Exception ex) - { - _logger.LogWarning(ex, $"Failed to send state change in {nameof(StreamingLogHook)} (conversation id: {conversationId})."); - } - } - - - private string BuildContentLog(ContentLogInputModel input) + private ContentLogOutputModel BuildContentLog(ContentLogInputModel input) { var output = new ContentLogOutputModel { @@ -561,8 +488,6 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR CreatedTime = DateTime.UtcNow }; - var json = JsonSerializer.Serialize(output, _options.JsonSerializerOptions); - var convSettings = _services.GetRequiredService(); if (convSettings.EnableContentLog) { @@ -570,10 +495,10 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR db.SaveConversationContentLog(output); } - return json; + return output; } - private string BuildStateLog(string conversationId, string agentId, Dictionary states, RoleDialogModel message) + private ConversationStateLogModel BuildStateLog(string conversationId, string agentId, Dictionary states, RoleDialogModel message) { var log = new ConversationStateLogModel { @@ -591,10 +516,10 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR db.SaveConversationStateLog(log); } - return JsonSerializer.Serialize(log, _options.JsonSerializerOptions); + return log; } - private string BuildStateChangeLog(StateChangeModel stateChange) + private StateChangeOutputModel BuildStateChangeLog(StateChangeModel stateChange) { var log = new StateChangeOutputModel { @@ -611,10 +536,10 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR CreateTime = DateTime.UtcNow }; - return JsonSerializer.Serialize(log, _options.JsonSerializerOptions); + return log; } - private string BuildAgentQueueChangedLog(string conversationId, string log) + private AgentQueueChangedLogModel BuildAgentQueueChangedLog(string conversationId, string log) { var model = new AgentQueueChangedLogModel { @@ -623,7 +548,7 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR CreatedTime = DateTime.UtcNow }; - return JsonSerializer.Serialize(model, _options.JsonSerializerOptions); + return model; } private string GetMessageContent(RoleDialogModel message) diff --git a/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/WelcomeHook.cs b/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/WelcomeHook.cs index 3ae8b313..3fbd852e 100644 --- a/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/WelcomeHook.cs +++ b/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/WelcomeHook.cs @@ -1,5 +1,7 @@ using BotSharp.Abstraction.Conversations.Dtos; +using BotSharp.Abstraction.Conversations.Enums; using Microsoft.AspNetCore.SignalR; +using System.Runtime.CompilerServices; namespace BotSharp.Plugin.ChatHub.Hooks; @@ -13,11 +15,8 @@ public class WelcomeHook : ConversationHookBase private readonly BotSharpOptions _options; private readonly ChatHubSettings _settings; - #region Events - private const string RECEIVE_ASSISTANT_MESSAGE = "OnMessageReceivedFromAssistant"; - #endregion - - public WelcomeHook(IServiceProvider services, + public WelcomeHook( + IServiceProvider services, IHubContext chatHub, ILogger logger, IUserIdentity user, @@ -64,7 +63,7 @@ public class WelcomeHook : ConversationHookBase RichContent = richContent }; - var json = JsonSerializer.Serialize(new ChatResponseDto() + var data = new ChatResponseDto() { ConversationId = conversation.Id, MessageId = dialog.MessageId, @@ -76,35 +75,21 @@ public class WelcomeHook : ConversationHookBase LastName = "", Role = AgentRole.Assistant } - }, _options.JsonSerializerOptions); + }; await Task.Delay(300); - _storage.Append(conversation.Id, dialog); - - await SendEvent(conversation.Id, json); + await SendEvent(ChatEvent.OnMessageReceivedFromAssistant, conversation.Id, data); } } await base.OnUserAgentConnectedInitially(conversation); } - private async Task SendEvent(string conversationId, string json) + private async Task SendEvent(string @event, string conversationId, T data, [CallerMemberName] string callerName = "") { - try - { - if (_settings.EventDispatchBy == EventDispatchType.Group) - { - await _chatHub.Clients.Group(conversationId).SendAsync(RECEIVE_ASSISTANT_MESSAGE, json); - } - else - { - await _chatHub.Clients.User(_user.Id).SendAsync(RECEIVE_ASSISTANT_MESSAGE, json); - } - } - catch (Exception ex) - { - _logger.LogWarning(ex, $"Failed to send event in {nameof(WelcomeHook)} (conversation id: {conversationId})."); - } + var user = _services.GetRequiredService(); + var json = JsonSerializer.Serialize(data, _options.JsonSerializerOptions); + await EventEmitter.SendChatEvent(_services, _logger, @event, conversationId, user?.Id, json, nameof(WelcomeHook), callerName); } } diff --git a/src/Plugins/BotSharp.Plugin.ChatHub/Observers/ChatHubObserver.cs b/src/Plugins/BotSharp.Plugin.ChatHub/Observers/ChatHubObserver.cs index c15419ed..03a56d47 100644 --- a/src/Plugins/BotSharp.Plugin.ChatHub/Observers/ChatHubObserver.cs +++ b/src/Plugins/BotSharp.Plugin.ChatHub/Observers/ChatHubObserver.cs @@ -1,113 +1,129 @@ using BotSharp.Abstraction.Conversations.Dtos; -using BotSharp.Abstraction.Observables.Models; +using BotSharp.Abstraction.Conversations.Enums; +using BotSharp.Abstraction.MessageHub.Models; +using BotSharp.Abstraction.MessageHub.Observers; using BotSharp.Abstraction.SideCar; -using BotSharp.Plugin.ChatHub.Hooks; -using Microsoft.AspNetCore.SignalR; +using System.Runtime.CompilerServices; namespace BotSharp.Plugin.ChatHub.Observers; -public class ChatHubObserver : IObserver +public class ChatHubObserver : BotSharpObserverBase> { + private readonly IServiceProvider _services; + private readonly BotSharpOptions _options; private readonly ILogger _logger; - private IServiceProvider _services; - private const string BEFORE_RECEIVE_LLM_STREAM_MESSAGE = "BeforeReceiveLlmStreamMessage"; - private const string ON_RECEIVE_LLM_STREAM_MESSAGE = "OnReceiveLlmStreamMessage"; - private const string AFTER_RECEIVE_LLM_STREAM_MESSAGE = "AfterReceiveLlmStreamMessage"; - private const string GENERATE_SENDER_ACTION = "OnSenderActionGenerated"; - - public ChatHubObserver(ILogger logger) + public ChatHubObserver( + IServiceProvider services, + BotSharpOptions options, + ILogger logger) : base() { + _services = services; + _options = options; _logger = logger; } - public void OnCompleted() + public override string Name => nameof(ChatHubObserver); + + public override void OnCompleted() { _logger.LogWarning($"{nameof(ChatHubObserver)} receives complete notification."); } - public void OnError(Exception error) + public override void OnError(Exception error) { _logger.LogError(error, $"{nameof(ChatHubObserver)} receives error notification: {error.Message}"); } - public void OnNext(HubObserveData value) + public override void OnNext(HubObserveData value) { - _services = value.ServiceProvider; - - if (!AllowSendingMessage()) return; - var message = value.Data; var model = new ChatResponseDto(); - if (value.EventName == BEFORE_RECEIVE_LLM_STREAM_MESSAGE) + var action = new ConversationSenderActionModel(); + var conv = _services.GetRequiredService(); + + switch (value.EventName) { - var conv = _services.GetRequiredService(); - model = new ChatResponseDto() - { - ConversationId = conv.ConversationId, - MessageId = message.MessageId, - Text = string.Empty, - Sender = new() + case ChatEvent.BeforeReceiveLlmStreamMessage: + if (!AllowSendingMessage()) return; + + model = new ChatResponseDto() { - FirstName = "AI", - LastName = "Assistant", - Role = AgentRole.Assistant - } - }; + ConversationId = conv.ConversationId, + MessageId = message.MessageId, + Text = string.Empty, + Sender = new() + { + FirstName = "AI", + LastName = "Assistant", + Role = AgentRole.Assistant + } + }; - var action = new ConversationSenderActionModel - { - ConversationId = conv.ConversationId, - SenderAction = SenderActionEnum.TypingOn - }; - - GenerateSenderAction(conv.ConversationId, action); - } - else if (value.EventName == AFTER_RECEIVE_LLM_STREAM_MESSAGE && message.IsStreaming) - { - var conv = _services.GetRequiredService(); - model = new ChatResponseDto() - { - ConversationId = conv.ConversationId, - MessageId = message.MessageId, - Text = message.Content, - Sender = new() + action = new ConversationSenderActionModel { - FirstName = "AI", - LastName = "Assistant", - Role = AgentRole.Assistant - } - }; + ConversationId = conv.ConversationId, + SenderAction = SenderActionEnum.TypingOn + }; - var action = new ConversationSenderActionModel - { - ConversationId = conv.ConversationId, - SenderAction = SenderActionEnum.TypingOff - }; + SendEvent(ChatEvent.OnSenderActionGenerated, conv.ConversationId, action); + break; + case ChatEvent.OnReceiveLlmStreamMessage: + if (!AllowSendingMessage()) return; - GenerateSenderAction(conv.ConversationId, action); - } - else if (value.EventName == ON_RECEIVE_LLM_STREAM_MESSAGE) - { - var conv = _services.GetRequiredService(); - model = new ChatResponseDto() - { - 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() + model = new ChatResponseDto() { - FirstName = "AI", - LastName = "Assistant", - Role = AgentRole.Assistant - } - }; + 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() + { + FirstName = "AI", + LastName = "Assistant", + Role = AgentRole.Assistant + } + }; + break; + case ChatEvent.AfterReceiveLlmStreamMessage: + if (!AllowSendingMessage()) return; + + model = new ChatResponseDto() + { + ConversationId = conv.ConversationId, + MessageId = message.MessageId, + Text = message.Content, + Sender = new() + { + FirstName = "AI", + LastName = "Assistant", + Role = AgentRole.Assistant + } + }; + break; + case ChatEvent.OnIndicationReceived: + model = new ChatResponseDto + { + ConversationId = conv.ConversationId, + MessageId = message.MessageId, + Indication = message.Indication, + Sender = new() + { + FirstName = "AI", + LastName = "Assistant", + Role = AgentRole.Assistant + } + }; + +#if DEBUG + _logger.LogCritical($"Receiving {value.EventName} ({value.Data.Indication}) in {nameof(ChatHubObserver)} - {conv.ConversationId}"); +#endif + break; } - OnReceiveAssistantMessage(value.EventName, model.ConversationId, model); + SendEvent(value.EventName, model.ConversationId, model); } private bool AllowSendingMessage() @@ -116,48 +132,13 @@ public class ChatHubObserver : IObserver return sidecar == null || !sidecar.IsEnabled; } - private void OnReceiveAssistantMessage(string @event, string conversationId, ChatResponseDto model) + #region Private methods + private void SendEvent(string @event, string conversationId, T data, [CallerMemberName] string callerName = "") { - try - { - var settings = _services.GetRequiredService(); - var chatHub = _services.GetRequiredService>(); - - if (settings.EventDispatchBy == EventDispatchType.Group) - { - chatHub.Clients.Group(conversationId).SendAsync(@event, model).ConfigureAwait(false).GetAwaiter().GetResult(); - } - else - { - var user = _services.GetRequiredService(); - chatHub.Clients.User(user.Id).SendAsync(@event, model).ConfigureAwait(false).GetAwaiter().GetResult(); - } - } - catch (Exception ex) - { - _logger.LogWarning(ex, $"Failed to receive assistant message in {nameof(ChatHubConversationHook)} (conversation id: {conversationId})"); - } - } - - private void GenerateSenderAction(string conversationId, ConversationSenderActionModel action) - { - try - { - var settings = _services.GetRequiredService(); - var chatHub = _services.GetRequiredService>(); - if (settings.EventDispatchBy == EventDispatchType.Group) - { - chatHub.Clients.Group(conversationId).SendAsync(GENERATE_SENDER_ACTION, action).ConfigureAwait(false).GetAwaiter().GetResult(); - } - else - { - var user = _services.GetRequiredService(); - chatHub.Clients.User(user.Id).SendAsync(GENERATE_SENDER_ACTION, action).ConfigureAwait(false).GetAwaiter().GetResult(); - } - } - catch (Exception ex) - { - _logger.LogWarning(ex, $"Failed to generate sender action in {nameof(ChatHubConversationHook)} (conversation id: {conversationId})"); - } + var user = _services.GetRequiredService(); + var json = JsonSerializer.Serialize(data, _options.JsonSerializerOptions); + EventEmitter.SendChatEvent(_services, _logger, @event, conversationId, user?.Id, json, nameof(ChatHubObserver), callerName) + .ConfigureAwait(false).GetAwaiter().GetResult(); } + #endregion } diff --git a/src/Plugins/BotSharp.Plugin.ChatHub/Using.cs b/src/Plugins/BotSharp.Plugin.ChatHub/Using.cs index 4710509c..3473bb6c 100644 --- a/src/Plugins/BotSharp.Plugin.ChatHub/Using.cs +++ b/src/Plugins/BotSharp.Plugin.ChatHub/Using.cs @@ -34,4 +34,5 @@ global using BotSharp.Abstraction.Realtime; global using BotSharp.Abstraction.Realtime.Models; global using BotSharp.Plugin.ChatHub.Settings; global using BotSharp.Plugin.ChatHub.Enums; -global using BotSharp.Plugin.ChatHub.Models.Stream; \ No newline at end of file +global using BotSharp.Plugin.ChatHub.Models.Stream; +global using BotSharp.Plugin.ChatHub.Helpers; \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.DeepSeekAI/Providers/Chat/ChatCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.DeepSeekAI/Providers/Chat/ChatCompletionProvider.cs index 1335d4dc..f29dd77a 100644 --- a/src/Plugins/BotSharp.Plugin.DeepSeekAI/Providers/Chat/ChatCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.DeepSeekAI/Providers/Chat/ChatCompletionProvider.cs @@ -1,8 +1,9 @@ +using BotSharp.Abstraction.Conversations.Enums; using BotSharp.Abstraction.Files; using BotSharp.Abstraction.Hooks; -using BotSharp.Abstraction.Observables.Models; +using BotSharp.Abstraction.MessageHub.Models; using BotSharp.Core.Infrastructures.Streams; -using BotSharp.Core.Observables.Queues; +using BotSharp.Core.MessageHub; using BotSharp.Plugin.DeepSeek.Providers; using Microsoft.Extensions.Logging; using OpenAI.Chat; @@ -179,7 +180,8 @@ public class ChatCompletionProvider : IChatCompletion var chatClient = client.GetChatClient(_model); var (prompt, messages, options) = PrepareOptions(agent, conversations); - var hub = _services.GetRequiredService>(); + var hub = _services.GetRequiredService>>(); + var conv = _services.GetRequiredService(); var messageId = conversations.LastOrDefault()?.MessageId ?? string.Empty; var contentHooks = _services.GetHooks(agent.Id); @@ -191,8 +193,8 @@ public class ChatCompletionProvider : IChatCompletion hub.Push(new() { - ServiceProvider = _services, - EventName = "BeforeReceiveLlmStreamMessage", + EventName = ChatEvent.BeforeReceiveLlmStreamMessage, + RefId = conv.ConversationId, Data = new RoleDialogModel(AgentRole.Assistant, string.Empty) { CurrentAgentId = agent.Id, @@ -235,8 +237,8 @@ public class ChatCompletionProvider : IChatCompletion }; hub.Push(new() { - ServiceProvider = _services, - EventName = "OnReceiveLlmStreamMessage", + EventName = ChatEvent.OnReceiveLlmStreamMessage, + RefId = conv.ConversationId, Data = content }); } @@ -278,8 +280,8 @@ public class ChatCompletionProvider : IChatCompletion hub.Push(new() { - ServiceProvider = _services, - EventName = "AfterReceiveLlmStreamMessage", + EventName = ChatEvent.AfterReceiveLlmStreamMessage, + RefId = conv.ConversationId, Data = responseMessage }); diff --git a/src/Plugins/BotSharp.Plugin.FileHandler/BotSharp.Plugin.FileHandler.csproj b/src/Plugins/BotSharp.Plugin.FileHandler/BotSharp.Plugin.FileHandler.csproj index 2147d178..2b343db0 100644 --- a/src/Plugins/BotSharp.Plugin.FileHandler/BotSharp.Plugin.FileHandler.csproj +++ b/src/Plugins/BotSharp.Plugin.FileHandler/BotSharp.Plugin.FileHandler.csproj @@ -1,4 +1,4 @@ - + $(TargetFramework) @@ -9,10 +9,6 @@ $(GenerateDocumentationFile) $(SolutionDir)packages - - - - diff --git a/src/Plugins/BotSharp.Plugin.FileHandler/LlmContexts/LlmContextIn.cs b/src/Plugins/BotSharp.Plugin.FileHandler/LlmContexts/LlmContextIn.cs index a35e83bd..9280bfd7 100644 --- a/src/Plugins/BotSharp.Plugin.FileHandler/LlmContexts/LlmContextIn.cs +++ b/src/Plugins/BotSharp.Plugin.FileHandler/LlmContexts/LlmContextIn.cs @@ -12,10 +12,6 @@ public class LlmContextIn [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? ImageDescription { get; set; } - //[JsonPropertyName("image_url")] - //[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - //public string? ImageUrl { get; set; } - [JsonPropertyName("image_urls")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public IEnumerable? ImageUrls { get; set; } diff --git a/src/Plugins/BotSharp.Plugin.KnowledgeBase/MemVecDb/MemoryVectorDb.cs b/src/Plugins/BotSharp.Plugin.KnowledgeBase/MemVecDb/MemoryVectorDb.cs index f766bb7e..27823202 100644 --- a/src/Plugins/BotSharp.Plugin.KnowledgeBase/MemVecDb/MemoryVectorDb.cs +++ b/src/Plugins/BotSharp.Plugin.KnowledgeBase/MemVecDb/MemoryVectorDb.cs @@ -1,3 +1,4 @@ +using System.Linq; using Tensorflow.NumPy; namespace BotSharp.Plugin.KnowledgeBase.MemVecDb; @@ -38,30 +39,29 @@ public class MemoryVectorDb : IVectorDb throw new NotImplementedException(); } - public Task> GetCollectionData(string collectionName, IEnumerable ids, - bool withPayload = false, bool withVector = false) + public Task> GetCollectionData(string collectionName, IEnumerable ids, VectorQueryOptions? options = null) { throw new NotImplementedException(); } - public async Task> Search(string collectionName, float[] vector, - IEnumerable? fields, int limit = 5, float confidence = 0.5f, bool withVector = false) + public async Task> Search(string collectionName, float[] vector, VectorSearchOptions? options = null) { if (!_vectors.ContainsKey(collectionName)) { return new List(); } + options ??= VectorSearchOptions.Default(); var similarities = VectorHelper.CalCosineSimilarity(vector, _vectors[collectionName]); var results = np.argsort(similarities).ToArray() .Reverse() - .Take(limit) + .Take(options.Limit.GetValueOrDefault()) .Select(i => new VectorCollectionData { Data = new Dictionary { { "text", _vectors[collectionName][i].Text } }, Score = similarities[i], - Vector = withVector ? _vectors[collectionName][i].Vector : null, + Vector = options.WithVector ? _vectors[collectionName][i].Vector : null, }) .ToList(); diff --git a/src/Plugins/BotSharp.Plugin.KnowledgeBase/Services/KnowledgeService.Document.cs b/src/Plugins/BotSharp.Plugin.KnowledgeBase/Services/KnowledgeService.Document.cs index 491261d8..3f4853e2 100644 --- a/src/Plugins/BotSharp.Plugin.KnowledgeBase/Services/KnowledgeService.Document.cs +++ b/src/Plugins/BotSharp.Plugin.KnowledgeBase/Services/KnowledgeService.Document.cs @@ -170,7 +170,7 @@ public partial class KnowledgeService } catch (Exception ex) { - _logger.LogWarning(ex, $"Error when importing doc content to knowledgebase ({collectionName}-{fileName})"); + _logger.LogError(ex, $"Error when importing doc content to knowledgebase ({collectionName}-{fileName})"); return false; } } @@ -191,7 +191,7 @@ public partial class KnowledgeService var vectorStoreProvider = _settings.VectorDb.Provider; // Get doc meta data - var pageData = db.GetKnowledgeBaseFileMeta(collectionName, vectorStoreProvider, new KnowledgeFileFilter + var pageData = await db.GetKnowledgeBaseFileMeta(collectionName, vectorStoreProvider, new KnowledgeFileFilter { Size = 1, FileIds = [ fileId ] @@ -212,7 +212,7 @@ public partial class KnowledgeService } catch (Exception ex) { - _logger.LogWarning(ex, $"Error when deleting knowledge document " + + _logger.LogError(ex, $"Error when deleting knowledge document " + $"(Collection: {collectionName}, File id: {fileId})"); return false; } @@ -281,7 +281,7 @@ public partial class KnowledgeService var vectorStoreProvider = _settings.VectorDb.Provider; // Get doc meta data - var pagedData = db.GetKnowledgeBaseFileMeta(collectionName, vectorStoreProvider, filter); + var pagedData = await db.GetKnowledgeBaseFileMeta(collectionName, vectorStoreProvider, filter); var files = pagedData.Items?.Select(x => new KnowledgeFileModel { @@ -308,7 +308,7 @@ public partial class KnowledgeService var vectorStoreProvider = _settings.VectorDb.Provider; // Get doc binary data - var pageData = db.GetKnowledgeBaseFileMeta(collectionName, vectorStoreProvider, new KnowledgeFileFilter + var pageData = await db.GetKnowledgeBaseFileMeta(collectionName, vectorStoreProvider, new KnowledgeFileFilter { Size = 1, FileIds = [ fileId ] diff --git a/src/Plugins/BotSharp.Plugin.KnowledgeBase/Services/KnowledgeService.Graph.cs b/src/Plugins/BotSharp.Plugin.KnowledgeBase/Services/KnowledgeService.Graph.cs index dbbe8885..7207bace 100644 --- a/src/Plugins/BotSharp.Plugin.KnowledgeBase/Services/KnowledgeService.Graph.cs +++ b/src/Plugins/BotSharp.Plugin.KnowledgeBase/Services/KnowledgeService.Graph.cs @@ -15,7 +15,7 @@ public partial class KnowledgeService } catch (Exception ex) { - _logger.LogWarning(ex, $"Error when searching graph knowledge (Query: {query})."); + _logger.LogError(ex, $"Error when searching graph knowledge (Query: {query})."); return new GraphSearchResult(); } } diff --git a/src/Plugins/BotSharp.Plugin.KnowledgeBase/Services/KnowledgeService.Index.cs b/src/Plugins/BotSharp.Plugin.KnowledgeBase/Services/KnowledgeService.Index.cs new file mode 100644 index 00000000..640464de --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.KnowledgeBase/Services/KnowledgeService.Index.cs @@ -0,0 +1,74 @@ +using BotSharp.Abstraction.Models; + +namespace BotSharp.Plugin.KnowledgeBase.Services; + +public partial class KnowledgeService +{ + public async Task> CreateVectorCollectionPayloadIndexes(string collectionName, IEnumerable options) + { + try + { + if (string.IsNullOrWhiteSpace(collectionName) || options.IsNullOrEmpty()) + { + return new(); + } + + var response = new SuccessFailResponse(); + var vectorDb = GetVectorDb(); + foreach (var option in options) + { + var created = await vectorDb.CreateCollectionPayloadIndex(collectionName, option); + var field = $"{option.FieldName} ({option.FieldSchemaType})"; + if (created) + { + response.Success.Add(field); + } + else + { + _logger.LogError($"Failed to create vector collection payload index ({collectionName} => {field})."); + response.Fail.Add(field); + } + } + return response; + } + catch (Exception ex) + { + _logger.LogError(ex, $"Error when creating vector collection payload index ({collectionName})."); + return new(); + } + } + + public async Task> DeleteVectorCollectionPayloadIndexes(string collectionName, IEnumerable options) + { + try + { + if (string.IsNullOrWhiteSpace(collectionName) || options.IsNullOrEmpty()) + { + return new(); + } + + var response = new SuccessFailResponse(); + var vectorDb = GetVectorDb(); + foreach (var option in options) + { + var deleted = await vectorDb.DeleteCollectionPayloadIndex(collectionName, option); + var field = $"{option.FieldName}"; + if (deleted) + { + response.Success.Add(field); + } + else + { + _logger.LogError($"Failed to deleting vector collection payload index ({collectionName}-{field})."); + response.Fail.Add(field); + } + } + return response; + } + catch (Exception ex) + { + _logger.LogError(ex, $"Error when deleting vector collection payload index ({collectionName})."); + return new(); + } + } +} diff --git a/src/Plugins/BotSharp.Plugin.KnowledgeBase/Services/KnowledgeService.Vector.cs b/src/Plugins/BotSharp.Plugin.KnowledgeBase/Services/KnowledgeService.Vector.cs index c230078d..67d546e1 100644 --- a/src/Plugins/BotSharp.Plugin.KnowledgeBase/Services/KnowledgeService.Vector.cs +++ b/src/Plugins/BotSharp.Plugin.KnowledgeBase/Services/KnowledgeService.Vector.cs @@ -1,6 +1,6 @@ using BotSharp.Abstraction.Files; using BotSharp.Abstraction.VectorStorage.Enums; -using System; +using static Microsoft.EntityFrameworkCore.DbLoggerCategory; namespace BotSharp.Plugin.KnowledgeBase.Services; @@ -33,38 +33,37 @@ public partial class KnowledgeService return false; } - var vectorDb = GetVectorDb(); - var created = await vectorDb.CreateCollection(collectionName, dimension); + var db = _services.GetRequiredService(); + var created = db.AddKnowledgeCollectionConfigs(new List + { + new VectorCollectionConfig + { + Name = collectionName, + Type = collectionType, + VectorStore = new VectorStoreConfig + { + Provider = _settings.VectorDb.Provider + }, + TextEmbedding = new KnowledgeEmbeddingConfig + { + Provider = provider, + Model = model, + Dimension = dimension + } + } + }); + if (created) { - var db = _services.GetRequiredService(); - var userId = await GetUserId(); - - db.AddKnowledgeCollectionConfigs(new List - { - new VectorCollectionConfig - { - Name = collectionName, - Type = collectionType, - VectorStore = new VectorStoreConfig - { - Provider = _settings.VectorDb.Provider - }, - TextEmbedding = new KnowledgeEmbeddingConfig - { - Provider = provider, - Model = model, - Dimension = dimension - } - } - }); + var vectorDb = GetVectorDb(); + created = await vectorDb.CreateCollection(collectionName, dimension); } return created; } catch (Exception ex) { - _logger.LogWarning(ex, $"Error when creating a vector collection ({collectionName})."); + _logger.LogError(ex, $"Error when creating a vector collection ({collectionName})."); return false; } } @@ -86,8 +85,8 @@ public partial class KnowledgeService } catch (Exception ex) { - _logger.LogWarning(ex, $"Error when getting vector db collections."); - return Enumerable.Empty(); + _logger.LogError(ex, $"Error when getting vector db collections."); + return []; } } @@ -113,7 +112,7 @@ public partial class KnowledgeService } catch (Exception ex) { - _logger.LogWarning(ex, $"Error when getting vector db collection details."); + _logger.LogError(ex, $"Error when getting vector db collection details."); return null; } } @@ -145,7 +144,7 @@ public partial class KnowledgeService } catch (Exception ex) { - _logger.LogWarning(ex, $"Error when deleting collection ({collectionName})."); + _logger.LogError(ex, $"Error when deleting collection ({collectionName})."); return false; } } @@ -167,17 +166,23 @@ public partial class KnowledgeService var db = GetVectorDb(); var guid = Guid.NewGuid(); var payload = create.Payload ?? new(); - payload[KnowledgePayloadName.DataSource] = !string.IsNullOrWhiteSpace(create.DataSource) ? create.DataSource : VectorDataSource.Api; + + if (!payload.TryGetValue(KnowledgePayloadName.DataSource, out _)) + { + payload[KnowledgePayloadName.DataSource] = !string.IsNullOrWhiteSpace(create.DataSource) ? + create.DataSource : VectorDataSource.Api; + } return await db.Upsert(collectionName, guid, vector, create.Text, payload); } catch (Exception ex) { - _logger.LogWarning(ex, $"Error when creating vector collection data."); + _logger.LogError(ex, $"Error when creating vector collection data."); return false; } } + public async Task UpdateVectorCollectionData(string collectionName, VectorUpdateModel update) { try @@ -199,13 +204,18 @@ public partial class KnowledgeService var textEmbedding = GetTextEmbedding(collectionName); var vector = await textEmbedding.GetVectorAsync(update.Text); var payload = update.Payload ?? new(); - payload[KnowledgePayloadName.DataSource] = !string.IsNullOrWhiteSpace(update.DataSource) ? update.DataSource : VectorDataSource.Api; + + if (!payload.TryGetValue(KnowledgePayloadName.DataSource, out _)) + { + payload[KnowledgePayloadName.DataSource] = !string.IsNullOrWhiteSpace(update.DataSource) ? + update.DataSource : VectorDataSource.Api; + } return await db.Upsert(collectionName, guid, vector, update.Text, payload); } catch (Exception ex) { - _logger.LogWarning(ex, $"Error when updating vector collection data."); + _logger.LogError(ex, $"Error when updating vector collection data."); return false; } } @@ -222,7 +232,7 @@ public partial class KnowledgeService } var db = GetVectorDb(); - var found = await db.GetCollectionData(collectionName, [guid], withVector: true, withPayload: true); + var found = await db.GetCollectionData(collectionName, [guid], options: new() { WithVector = true, WithPayload = true }); if (!found.IsNullOrEmpty()) { if (found.First().Data[KnowledgePayloadName.Text].ToString() == update.Text) @@ -241,7 +251,7 @@ public partial class KnowledgeService } catch (Exception ex) { - _logger.LogWarning(ex, $"Error when updating vector collection data."); + _logger.LogError(ex, $"Error when updating vector collection data."); return false; } } @@ -260,7 +270,7 @@ public partial class KnowledgeService } catch (Exception ex) { - _logger.LogWarning(ex, $"Error when deleting vector collection data ({collectionName}-{id})."); + _logger.LogError(ex, $"Error when deleting vector collection data ({collectionName}-{id})."); return false; } } @@ -275,7 +285,7 @@ public partial class KnowledgeService } catch (Exception ex) { - _logger.LogWarning(ex, $"Error when deleting vector collection data ({collectionName})."); + _logger.LogError(ex, $"Error when deleting vector collection data ({collectionName})."); return false; } } @@ -295,11 +305,36 @@ public partial class KnowledgeService } catch (Exception ex) { - _logger.LogWarning(ex, $"Error when getting vector knowledge collection data ({collectionName})."); + _logger.LogError(ex, $"Error when getting vector knowledge collection data ({collectionName})."); return new StringIdPagedItems(); } } + public async Task> GetVectorCollectionData(string collectionName, IEnumerable ids, VectorQueryOptions? options = null) + { + try + { + if (string.IsNullOrWhiteSpace(collectionName) || ids.IsNullOrEmpty()) + { + return []; + } + + var pointIds = ids.Select(x => new { Id = x, IsValid = Guid.TryParse(x, out var guid), ParseResult = guid }) + .Where(x => x.IsValid) + .Select(x => x.ParseResult) + .ToList(); + + var db = GetVectorDb(); + var points = await db.GetCollectionData(collectionName, pointIds, options); + return points; + } + catch (Exception ex) + { + _logger.LogError(ex, $"Error when querying vector collection {collectionName} points."); + return []; + } + } + public async Task> SearchVectorKnowledge(string query, string collectionName, VectorSearchOptions options) { try @@ -309,15 +344,15 @@ public partial class KnowledgeService // Vector search var db = GetVectorDb(); - var found = await db.Search(collectionName, vector, options.Fields, limit: options.Limit ?? 5, confidence: options.Confidence ?? 0.5f, withVector: options.WithVector); + var found = await db.Search(collectionName, vector, options); var results = found.Select(x => VectorSearchResult.CopyFrom(x)).ToList(); return results; } catch (Exception ex) { - _logger.LogWarning(ex, $"Error when searching vector knowledge ({collectionName})."); - return Enumerable.Empty(); + _logger.LogError(ex, $"Error when searching vector knowledge ({collectionName})."); + return []; } } #endregion diff --git a/src/Plugins/BotSharp.Plugin.LLamaSharp/Providers/ChatCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.LLamaSharp/Providers/ChatCompletionProvider.cs index 321b9aee..08b6c676 100644 --- a/src/Plugins/BotSharp.Plugin.LLamaSharp/Providers/ChatCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.LLamaSharp/Providers/ChatCompletionProvider.cs @@ -1,9 +1,10 @@ using BotSharp.Abstraction.Agents; +using BotSharp.Abstraction.Conversations.Enums; using BotSharp.Abstraction.Hooks; using BotSharp.Abstraction.Loggers; -using BotSharp.Abstraction.Observables.Models; +using BotSharp.Abstraction.MessageHub.Models; using BotSharp.Core.Infrastructures.Streams; -using BotSharp.Core.Observables.Queues; +using BotSharp.Core.MessageHub; using Microsoft.AspNetCore.SignalR; using static LLama.Common.ChatHistory; using static System.Net.Mime.MediaTypeNames; @@ -183,13 +184,14 @@ public class ChatCompletionProvider : IChatCompletion _logger.LogInformation(agent.Instruction); } - var hub = _services.GetRequiredService>(); + var hub = _services.GetRequiredService>>(); + var conv = _services.GetRequiredService(); var messageId = conversations.LastOrDefault()?.MessageId ?? string.Empty; hub.Push(new() { - ServiceProvider = _services, - EventName = "BeforeReceiveLlmStreamMessage", + EventName = ChatEvent.BeforeReceiveLlmStreamMessage, + RefId = conv.ConversationId, Data = new RoleDialogModel(AgentRole.Assistant, string.Empty) { CurrentAgentId = agent.Id, @@ -216,8 +218,8 @@ public class ChatCompletionProvider : IChatCompletion }; hub.Push(new() { - ServiceProvider = _services, - EventName = "OnReceiveLlmStreamMessage", + EventName = ChatEvent.OnReceiveLlmStreamMessage, + RefId = conv.ConversationId, Data = content }); } @@ -231,8 +233,8 @@ public class ChatCompletionProvider : IChatCompletion hub.Push(new() { - ServiceProvider = _services, - EventName = "AfterReceiveLlmStreamMessage", + EventName = ChatEvent.AfterReceiveLlmStreamMessage, + RefId = conv.ConversationId, Data = responseMessage }); diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentLlmConfigMongoElement.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentLlmConfigMongoElement.cs index b626d7da..63bc3b20 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentLlmConfigMongoElement.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentLlmConfigMongoElement.cs @@ -10,6 +10,7 @@ public class AgentLlmConfigMongoElement public bool IsInherit { get; set; } public int MaxRecursionDepth { get; set; } public int? MaxOutputTokens { get; set; } + public string? ReasoningEffortLevel { get; set; } public static AgentLlmConfigMongoElement? ToMongoElement(AgentLlmConfig? config) { @@ -22,6 +23,7 @@ public class AgentLlmConfigMongoElement IsInherit = config.IsInherit, MaxRecursionDepth = config.MaxRecursionDepth, MaxOutputTokens = config.MaxOutputTokens, + ReasoningEffortLevel = config.ReasoningEffortLevel }; } @@ -36,6 +38,7 @@ public class AgentLlmConfigMongoElement IsInherit = config.IsInherit, MaxRecursionDepth = config.MaxRecursionDepth, MaxOutputTokens = config.MaxOutputTokens, + ReasoningEffortLevel = config.ReasoningEffortLevel }; } } diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.AgentTask.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.AgentTask.cs index db3cdfc6..85f14215 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.AgentTask.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.AgentTask.cs @@ -6,7 +6,7 @@ namespace BotSharp.Plugin.MongoStorage.Repository; public partial class MongoRepository { #region Task - public PagedItems GetAgentTasks(AgentTaskFilter filter) + public async ValueTask> GetAgentTasks(AgentTaskFilter filter) { if (filter == null) { @@ -34,13 +34,23 @@ public partial class MongoRepository var filterDef = builder.And(filters); var sortDef = Builders.Sort.Descending(x => x.CreatedTime); - var totalTasks = _dc.AgentTasks.CountDocuments(filterDef); - var taskDocs = _dc.AgentTasks.Find(filterDef).Sort(sortDef).Skip(pager.Offset).Limit(pager.Size).ToList(); - var agentIds = taskDocs.Select(x => x.AgentId).Distinct().ToList(); + var docsTask = _dc.AgentTasks.FindAsync(filterDef, options: new() + { + Sort = sortDef, + Skip = pager.Offset, + Limit = pager.Size + }); + var countTask = _dc.AgentTasks.CountDocumentsAsync(filterDef); + await Task.WhenAll([docsTask, countTask]); + + var docs = docsTask.Result.ToList(); + var count = countTask.Result; + + var agentIds = docs.Select(x => x.AgentId).Distinct().ToList(); var agents = GetAgents(new AgentFilter { AgentIds = agentIds }); - var tasks = taskDocs.Select(x => + var tasks = docs.Select(x => { var task = AgentTaskDocument.ToDomainModel(x); task.Agent = agents.FirstOrDefault(a => a.Id == x.AgentId); @@ -50,7 +60,7 @@ public partial class MongoRepository return new PagedItems { Items = tasks, - Count = (int)totalTasks + Count = count }; } diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Conversation.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Conversation.cs index d0da6fbf..6611c352 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Conversation.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Conversation.cs @@ -336,7 +336,7 @@ public partial class MongoRepository }; } - public PagedItems GetConversations(ConversationFilter filter) + public async ValueTask> GetConversations(ConversationFilter filter) { if (filter == null) { @@ -393,6 +393,10 @@ public partial class MongoRepository { convFilters.Add(convBuilder.Gte(x => x.CreatedTime, filter.StartTime.Value)); } + if (filter?.EndTime != null) + { + convFilters.Add(convBuilder.Lte(x => x.CreatedTime, filter.EndTime.Value)); + } if (filter?.Tags != null && filter.Tags.Any()) { convFilters.Add(convBuilder.AnyIn(x => x.Tags, filter.Tags)); @@ -462,10 +466,19 @@ public partial class MongoRepository } } - var conversationDocs = _dc.Conversations.Find(filterDef).Sort(sortDef).Skip(pager.Offset).Limit(pager.Size).ToList(); - var count = _dc.Conversations.CountDocuments(filterDef); + var docsTask = _dc.Conversations.FindAsync(filterDef, options: new() + { + Sort = sortDef, + Skip = pager.Offset, + Limit = pager.Size + }); + var countTask = _dc.Conversations.CountDocumentsAsync(filterDef); + await Task.WhenAll([docsTask, countTask]); - var conversations = conversationDocs.Select(x => + var docs = docsTask.Result.ToList(); + var count = countTask.Result; + + var conversations = docs.Select(x => { var states = new Dictionary(); if (filter.IsLoadLatestStates) @@ -498,7 +511,7 @@ public partial class MongoRepository return new PagedItems { Items = conversations, - Count = (int)count + Count = count }; } @@ -689,11 +702,18 @@ public partial class MongoRepository { filters.Add(builder.In(x => x.AgentId, filter.AgentIds)); } - if (!filter.UserIds.IsNullOrEmpty()) { filters.Add(builder.In(x => x.UserId, filter.UserIds)); } + if (filter.StartTime.HasValue) + { + filters.Add(builder.Gte(x => x.CreatedTime, filter.StartTime.Value)); + } + if (filter.EndTime.HasValue) + { + filters.Add(builder.Lte(x => x.CreatedTime, filter.EndTime.Value)); + } var convDocs = _dc.Conversations.Find(builder.And(filters)) .Sort(sortDef) diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Crontab.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Crontab.cs index af10c373..4c307222 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Crontab.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Crontab.cs @@ -45,7 +45,7 @@ public partial class MongoRepository } - public PagedItems GetCrontabItems(CrontabItemFilter filter) + public async ValueTask> GetCrontabItems(CrontabItemFilter filter) { if (filter == null) { @@ -73,15 +73,24 @@ public partial class MongoRepository var filterDef = cronBuilder.And(cronFilters); var sortDef = Builders.Sort.Descending(x => x.CreatedTime); - var cronDocs = _dc.CrontabItems.Find(filterDef).Sort(sortDef).Skip(filter.Offset).Limit(filter.Size).ToList(); - var count = _dc.CrontabItems.CountDocuments(filterDef); + var docsTask = _dc.CrontabItems.FindAsync(filterDef, options: new() + { + Sort = sortDef, + Skip = filter.Offset, + Limit = filter.Size + }); + var countTask = _dc.CrontabItems.CountDocumentsAsync(filterDef); + await Task.WhenAll([docsTask, countTask]); - var crontabItems = cronDocs.Select(x => CrontabItemDocument.ToDomainModel(x)).ToList(); + var docs = docsTask.Result.ToList(); + var count = countTask.Result; + + var crontabItems = docs.Select(x => CrontabItemDocument.ToDomainModel(x)).ToList(); return new PagedItems { Items = crontabItems, - Count = (int)count + Count = count }; } } diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.KnowledgeBase.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.KnowledgeBase.cs index e47ed1c3..f6bcba89 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.KnowledgeBase.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.KnowledgeBase.cs @@ -168,7 +168,7 @@ public partial class MongoRepository return res.DeletedCount > 0; } - public PagedItems GetKnowledgeBaseFileMeta(string collectionName, string vectorStoreProvider, KnowledgeFileFilter filter) + public async ValueTask> GetKnowledgeBaseFileMeta(string collectionName, string vectorStoreProvider, KnowledgeFileFilter filter) { if (string.IsNullOrWhiteSpace(collectionName) || string.IsNullOrWhiteSpace(vectorStoreProvider)) @@ -209,8 +209,18 @@ public partial class MongoRepository var filterDef = builder.And(docFilters); var sortDef = Builders.Sort.Descending(x => x.CreatedDate); - var docs = _dc.KnowledgeCollectionFileMeta.Find(filterDef).Sort(sortDef).Skip(filter.Offset).Limit(filter.Size).ToList(); - var count = _dc.KnowledgeCollectionFileMeta.CountDocuments(filterDef); + + var docsTask = _dc.KnowledgeCollectionFileMeta.FindAsync(filterDef, options: new() + { + Sort = sortDef, + Skip = filter.Offset, + Limit = filter.Size + }); + var countTask = _dc.KnowledgeCollectionFileMeta.CountDocumentsAsync(filterDef); + await Task.WhenAll([docsTask, countTask]); + + var docs = docsTask.Result.ToList(); + var count = countTask.Result; var files = docs?.Select(x => new KnowledgeDocMetaData { @@ -229,7 +239,7 @@ public partial class MongoRepository return new PagedItems { Items = files, - Count = (int)count + Count = count }; } #endregion diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Log.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Log.cs index 8cd6e158..9a236277 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Log.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Log.cs @@ -169,7 +169,7 @@ public partial class MongoRepository return true; } - public PagedItems GetInstructionLogs(InstructLogFilter filter) + public async ValueTask> GetInstructionLogs(InstructLogFilter filter) { if (filter == null) { @@ -196,6 +196,14 @@ public partial class MongoRepository { logFilters.Add(logBuilder.In(x => x.TemplateName, filter.TemplateNames)); } + if (filter.StartTime.HasValue) + { + logFilters.Add(logBuilder.Gte(x => x.CreatedTime, filter.StartTime.Value)); + } + if (filter.EndTime.HasValue) + { + logFilters.Add(logBuilder.Lte(x => x.CreatedTime, filter.EndTime.Value)); + } // Filter states if (filter != null && !filter.States.IsNullOrEmpty()) @@ -243,8 +251,18 @@ public partial class MongoRepository var filterDef = logBuilder.And(logFilters); var sortDef = Builders.Sort.Descending(x => x.CreatedTime); - var docs = _dc.InstructionLogs.Find(filterDef).Sort(sortDef).Skip(filter.Offset).Limit(filter.Size).ToList(); - var count = _dc.InstructionLogs.CountDocuments(filterDef); + + var docsTask = _dc.InstructionLogs.FindAsync(filterDef, options: new() + { + Sort = sortDef, + Skip = filter.Offset, + Limit = filter.Size + }); + var countTask = _dc.InstructionLogs.CountDocumentsAsync(filterDef); + await Task.WhenAll([docsTask, countTask]); + + var docs = docsTask.Result.ToList(); + var count = countTask.Result; var logs = docs.Select(x => { @@ -262,7 +280,7 @@ public partial class MongoRepository return new PagedItems { Items = logs, - Count = (int)count + Count = count }; } @@ -280,11 +298,18 @@ public partial class MongoRepository { filters.Add(builder.In(x => x.AgentId, filter.AgentIds)); } - if (!filter.UserIds.IsNullOrEmpty()) { filters.Add(builder.In(x => x.UserId, filter.UserIds)); } + if (filter.StartTime.HasValue) + { + filters.Add(builder.Gte(x => x.CreatedTime, filter.StartTime.Value)); + } + if (filter.EndTime.HasValue) + { + filters.Add(builder.Lte(x => x.CreatedTime, filter.EndTime.Value)); + } var convDocs = _dc.InstructionLogs.Find(builder.And(filters)) .Sort(sortDef) diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.User.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.User.cs index a6f316f2..f30e52fc 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.User.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.User.cs @@ -205,7 +205,7 @@ public partial class MongoRepository } } - public PagedItems GetUsers(UserFilter filter) + public async ValueTask> GetUsers(UserFilter filter) { if (filter == null) { @@ -246,14 +246,23 @@ public partial class MongoRepository var sortDef = Builders.Sort.Descending(x => x.CreatedTime); // Search - var userDocs = _dc.Users.Find(filterDef).Sort(sortDef).Skip(filter.Offset).Limit(filter.Size).ToList(); - var count = _dc.Users.CountDocuments(filterDef); + var docsTask = _dc.Users.FindAsync(filterDef, options: new() + { + Sort = sortDef, + Skip = filter.Offset, + Limit = filter.Size + }); + var countTask = _dc.Users.CountDocumentsAsync(filterDef); + await Task.WhenAll([docsTask, countTask]); - var users = userDocs.Select(x => x.ToUser()).ToList(); + var docs = docsTask.Result.ToList(); + var count = countTask.Result; + + var users = docs.Select(x => x.ToUser()).ToList(); return new PagedItems { Items = users, - Count = (int)count + Count = count }; } diff --git a/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Chat/ChatCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Chat/ChatCompletionProvider.cs index 57b44252..99de556f 100644 --- a/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Chat/ChatCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Chat/ChatCompletionProvider.cs @@ -1,10 +1,8 @@ -using Azure; +#pragma warning disable OPENAI001 using BotSharp.Abstraction.Hooks; -using BotSharp.Abstraction.Observables.Models; +using BotSharp.Abstraction.MessageHub.Models; using BotSharp.Core.Infrastructures.Streams; -using BotSharp.Core.Observables.Queues; -using BotSharp.Plugin.OpenAI.Models.Realtime; -using Fluid; +using BotSharp.Core.MessageHub; using OpenAI.Chat; namespace BotSharp.Plugin.OpenAI.Providers.Chat; @@ -18,11 +16,6 @@ public class ChatCompletionProvider : IChatCompletion protected string _model; private List renderedInstructions = []; - private readonly Dictionary _defaultTemperature = new() - { - { "o4-mini", 1.0f } - }; - public virtual string Provider => "openai"; public string Model => _model; @@ -191,7 +184,8 @@ public class ChatCompletionProvider : IChatCompletion var chatClient = client.GetChatClient(_model); var (prompt, messages, options) = PrepareOptions(agent, conversations); - var hub = _services.GetRequiredService>(); + var hub = _services.GetRequiredService>>(); + var conv = _services.GetRequiredService(); var messageId = conversations.LastOrDefault()?.MessageId ?? string.Empty; var contentHooks = _services.GetHooks(agent.Id); @@ -203,8 +197,8 @@ public class ChatCompletionProvider : IChatCompletion hub.Push(new() { - ServiceProvider = _services, - EventName = "BeforeReceiveLlmStreamMessage", + EventName = ChatEvent.BeforeReceiveLlmStreamMessage, + RefId = conv.ConversationId, Data = new RoleDialogModel(AgentRole.Assistant, string.Empty) { CurrentAgentId = agent.Id, @@ -247,8 +241,8 @@ public class ChatCompletionProvider : IChatCompletion }; hub.Push(new() { - ServiceProvider = _services, - EventName = "OnReceiveLlmStreamMessage", + EventName = ChatEvent.OnReceiveLlmStreamMessage, + RefId = conv.ConversationId, Data = content }); } @@ -290,8 +284,8 @@ public class ChatCompletionProvider : IChatCompletion hub.Push(new() { - ServiceProvider = _services, - EventName = "AfterReceiveLlmStreamMessage", + EventName = ChatEvent.AfterReceiveLlmStreamMessage, + RefId = conv.ConversationId, Data = responseMessage }); @@ -490,11 +484,18 @@ public class ChatCompletionProvider : IChatCompletion private ChatCompletionOptions InitChatCompletionOption(Agent agent) { var state = _services.GetRequiredService(); + var settingsService = _services.GetRequiredService(); + var settings = settingsService.GetSetting(Provider, _model); + ChatReasoningEffortLevel? reasoningEffortLevel = null; var temperature = float.Parse(state.GetState("temperature", "0.0")); - if (_defaultTemperature.ContainsKey(_model)) + if (settings?.Reasoning != null) { - temperature = _defaultTemperature[_model]; + temperature = settings.Reasoning.Temperature; + var level = state.GetState("reasoning_effort_level") + .IfNullOrEmptyAs(agent?.LlmConfig?.ReasoningEffortLevel ?? string.Empty) + .IfNullOrEmptyAs(settings?.Reasoning?.EffortLevel ?? string.Empty); + reasoningEffortLevel = ParseReasoningEffortLevel(level); } var maxTokens = int.TryParse(state.GetState("max_tokens"), out var tokens) @@ -504,10 +505,37 @@ public class ChatCompletionProvider : IChatCompletion return new ChatCompletionOptions() { Temperature = temperature, - MaxOutputTokenCount = maxTokens + MaxOutputTokenCount = maxTokens, + ReasoningEffortLevel = reasoningEffortLevel }; } + private ChatReasoningEffortLevel? ParseReasoningEffortLevel(string? level) + { + if (string.IsNullOrWhiteSpace(level)) + { + return null; + } + + var effortLevel = new ChatReasoningEffortLevel("minimal"); + switch (level.ToLower()) + { + case "low": + effortLevel = ChatReasoningEffortLevel.Low; + break; + case "medium": + effortLevel = ChatReasoningEffortLevel.Medium; + break; + case "high": + effortLevel = ChatReasoningEffortLevel.High; + break; + default: + break; + } + + return effortLevel; + } + public void SetModelName(string model) { _model = model; diff --git a/src/Plugins/BotSharp.Plugin.OsDriver/WinOSDriver.cs b/src/Plugins/BotSharp.Plugin.OsDriver/WinOSDriver.cs index 45a2572d..2ed39994 100644 --- a/src/Plugins/BotSharp.Plugin.OsDriver/WinOSDriver.cs +++ b/src/Plugins/BotSharp.Plugin.OsDriver/WinOSDriver.cs @@ -29,6 +29,10 @@ public class WinOSDriver : IComputerUse case "right_click": WindowsMouseController.RightClick(); break; + case "cursor_position": + var (x, y) = WindowsMouseController.GetCurrentCursorPosition(); + Console.WriteLine($"Current cursor position: ({x}, {y})"); + break; default: throw new ToolError($"Action {action} is not supported"); } diff --git a/src/Plugins/BotSharp.Plugin.OsDriver/WindowsMouseController.cs b/src/Plugins/BotSharp.Plugin.OsDriver/WindowsMouseController.cs index c604780f..8e54d8c1 100644 --- a/src/Plugins/BotSharp.Plugin.OsDriver/WindowsMouseController.cs +++ b/src/Plugins/BotSharp.Plugin.OsDriver/WindowsMouseController.cs @@ -13,6 +13,13 @@ public class WindowsMouseController public int Left, Top, Right, Bottom; } + [StructLayout(LayoutKind.Sequential)] + public struct POINT + { + public int X; + public int Y; + } + // Define MONITORINFO structure [StructLayout(LayoutKind.Sequential)] public struct MONITORINFO @@ -34,6 +41,10 @@ public class WindowsMouseController [DllImport("user32.dll", CharSet = CharSet.Auto)] private static extern bool GetMonitorInfo(IntPtr hMonitor, ref MONITORINFO lpmi); + [DllImport("user32.dll")] + private static extern bool GetCursorPos(out POINT lpPoint); + + // Class to hold monitor information public class MonitorInfo { @@ -41,6 +52,13 @@ public class WindowsMouseController public RECT MonitorArea; } + // Method to get current cursor position + public static (int x, int y) GetCurrentCursorPosition() + { + GetCursorPos(out POINT cursorPos); + return (cursorPos.X, cursorPos.Y); + } + // Method to get all connected monitors public static List GetMonitors() { diff --git a/src/Plugins/BotSharp.Plugin.Qdrant/QdrantDb.cs b/src/Plugins/BotSharp.Plugin.Qdrant/QdrantDb.cs index 5c91301d..eb83ca0e 100644 --- a/src/Plugins/BotSharp.Plugin.Qdrant/QdrantDb.cs +++ b/src/Plugins/BotSharp.Plugin.Qdrant/QdrantDb.cs @@ -1,3 +1,4 @@ +using BotSharp.Abstraction.Models; using BotSharp.Abstraction.Options; using BotSharp.Abstraction.Utilities; using BotSharp.Abstraction.VectorStorage.Models; @@ -6,6 +7,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Qdrant.Client; using Qdrant.Client.Grpc; +using System.Collections; using System.Net.Http; using System.Net.Mime; using System.Text.Json; @@ -140,50 +142,27 @@ public class QdrantDb : IVectorDb return new StringIdPagedItems(); } - // Build query filter - Filter? queryFilter = null; - if (!filter.SearchPairs.IsNullOrEmpty()) - { - var conditions = filter.SearchPairs.Select(x => new Condition - { - Field = new FieldCondition - { - Key = x.Key, - Match = new Match { Text = x.Value }, - } - }); - - queryFilter = new Filter - { - Should = - { - conditions - } - }; - } - - // Build payload selector - WithPayloadSelector? payloadSelector = null; - if (!filter.IncludedPayloads.IsNullOrEmpty()) - { - payloadSelector = new WithPayloadSelector - { - Enable = true, - Include = new PayloadIncludeSelector - { - Fields = { filter.IncludedPayloads.ToArray() } - } - }; - } + Filter? queryFilter = BuildQueryFilter(filter.FilterGroups); + WithPayloadSelector? payloadSelector = BuildPayloadSelector(filter.Fields); + OrderBy? orderBy = BuildOrderBy(filter.OrderBy); var client = GetClient(); - var totalPointCount = await client.CountAsync(collectionName, filter: queryFilter); - var response = await client.ScrollAsync(collectionName, limit: (uint)filter.Size, + + var totalCountTask = client.CountAsync(collectionName, filter: queryFilter); + var dataResponseTask = client.ScrollAsync( + collectionName, + limit: (uint)filter.Size, offset: !string.IsNullOrWhiteSpace(filter.StartId) ? new PointId { Uuid = filter.StartId } : null, filter: queryFilter, + orderBy: orderBy, payloadSelector: payloadSelector, vectorsSelector: filter.WithVector); + await Task.WhenAll([totalCountTask, dataResponseTask]); + + var totalPointCount = totalCountTask.Result; + var response = dataResponseTask.Result; + var points = response?.Result?.Select(x => new VectorCollectionData { Id = x.Id?.Uuid ?? string.Empty, @@ -192,6 +171,7 @@ public class QdrantDb : IVectorDb Value.KindOneofCase.StringValue => p.Value.StringValue, Value.KindOneofCase.BoolValue => p.Value.BoolValue, Value.KindOneofCase.IntegerValue => p.Value.IntegerValue, + Value.KindOneofCase.DoubleValue => p.Value.DoubleValue, _ => new object() }), Vector = filter.WithVector ? x.Vectors?.Vector?.Data?.ToArray() : null @@ -206,8 +186,7 @@ public class QdrantDb : IVectorDb } - public async Task> GetCollectionData(string collectionName, IEnumerable ids, - bool withPayload = false, bool withVector = false) + public async Task> GetCollectionData(string collectionName, IEnumerable ids, VectorQueryOptions? options = null) { if (ids.IsNullOrEmpty()) { @@ -222,7 +201,7 @@ public class QdrantDb : IVectorDb var client = GetClient(); var pointIds = ids.Select(x => new PointId { Uuid = x.ToString() }).Distinct().ToList(); - var points = await client.RetrieveAsync(collectionName, pointIds, withPayload, withVector); + var points = await client.RetrieveAsync(collectionName, pointIds, options?.WithPayload ?? false, options?.WithVector ?? false); return points.Select(x => new VectorCollectionData { Id = x.Id?.Uuid ?? string.Empty, @@ -231,6 +210,7 @@ public class QdrantDb : IVectorDb Value.KindOneofCase.StringValue => p.Value.StringValue, Value.KindOneofCase.BoolValue => p.Value.BoolValue, Value.KindOneofCase.IntegerValue => p.Value.IntegerValue, + Value.KindOneofCase.DoubleValue => p.Value.DoubleValue, _ => new object() }) ?? new(), Vector = x.Vectors?.Vector?.Data?.ToArray() @@ -258,7 +238,10 @@ public class QdrantDb : IVectorDb foreach (var item in payload) { var value = item.Value?.ToString(); - if (value == null) continue; + if (value == null || item.Key.IsEqualTo(KnowledgePayloadName.Text)) + { + continue; + } if (bool.TryParse(value, out var b)) { @@ -308,8 +291,7 @@ public class QdrantDb : IVectorDb return result.Status == UpdateStatus.Completed; } - public async Task> Search(string collectionName, float[] vector, - IEnumerable? fields, int limit = 5, float confidence = 0.5f, bool withVector = false) + public async Task> Search(string collectionName, float[] vector, VectorSearchOptions? options = null) { var results = new List(); @@ -319,19 +301,18 @@ public class QdrantDb : IVectorDb return results; } - var payloadSelector = new WithPayloadSelector { Enable = true }; - if (fields != null) - { - payloadSelector.Include = new PayloadIncludeSelector { Fields = { fields.ToArray() } }; - } + options ??= VectorSearchOptions.Default(); + Filter? queryFilter = BuildQueryFilter(options.FilterGroups); + WithPayloadSelector? payloadSelector = BuildPayloadSelector(options.Fields); var client = GetClient(); var points = await client.SearchAsync(collectionName, vector, - limit: (ulong)limit, - scoreThreshold: confidence, + limit: (ulong)options.Limit.GetValueOrDefault(), + scoreThreshold: options.Confidence, + filter: queryFilter, payloadSelector: payloadSelector, - vectorsSelector: withVector); + vectorsSelector: options.WithVector); results = points.Select(x => new VectorCollectionData { @@ -341,6 +322,7 @@ public class QdrantDb : IVectorDb Value.KindOneofCase.StringValue => p.Value.StringValue, Value.KindOneofCase.BoolValue => p.Value.BoolValue, Value.KindOneofCase.IntegerValue => p.Value.IntegerValue, + Value.KindOneofCase.DoubleValue => p.Value.DoubleValue, _ => new object() }), Score = x.Score, @@ -379,6 +361,35 @@ public class QdrantDb : IVectorDb } #endregion + #region Payload index + public async Task CreateCollectionPayloadIndex(string collectionName, CreateVectorCollectionIndexOptions options) + { + var exist = await DoesCollectionExist(collectionName); + if (!exist) + { + return false; + } + + var client = GetClient(); + var schemaType = ConvertPayloadSchemaType(options.FieldSchemaType); + var result = await client.CreatePayloadIndexAsync(collectionName, options.FieldName, schemaType); + return result.Status == UpdateStatus.Completed; + } + + public async Task DeleteCollectionPayloadIndex(string collectionName, DeleteVectorCollectionIndexOptions options) + { + var exist = await DoesCollectionExist(collectionName); + if (!exist) + { + return false; + } + + var client = GetClient(); + var result = await client.DeletePayloadIndexAsync(collectionName, options.FieldName); + return result.Status == UpdateStatus.Completed; + } + #endregion + #region Snapshots public async Task> GetCollectionSnapshots(string collectionName) { @@ -523,4 +534,140 @@ public class QdrantDb : IVectorDb } } #endregion + + + #region Private methods + private Filter? BuildQueryFilter(IEnumerable? filterGroups) + { + Filter? queryFilter = null; + + if (filterGroups.IsNullOrEmpty()) + { + return queryFilter; + } + + var conditions = filterGroups.Where(x => !x.Filters.IsNullOrEmpty()).Select(x => + { + Filter filter; + var innerConditions = x.Filters.Select(f => + { + var field = new FieldCondition + { + Key = f.Key, + Match = new Match { Text = f.Value } + }; + + if (bool.TryParse(f.Value, out var boolVal)) + { + field.Match = new Match { Boolean = boolVal }; + } + else if (long.TryParse(f.Value, out var intVal)) + { + field.Match = new Match { Integer = intVal }; + } + + return new Condition { Field = field }; + }); + + if (x.FilterOperator.IsEqualTo("and")) + { + filter = new Filter + { + Must = { innerConditions } + }; + } + else + { + filter = new Filter + { + Should = { innerConditions } + }; + } + + return new Condition + { + Filter = filter + }; + }); + + queryFilter = new Filter + { + Must = + { + conditions + } + }; + + return queryFilter; + } + + private WithPayloadSelector? BuildPayloadSelector(IEnumerable? payloads) + { + WithPayloadSelector? payloadSelector = null; + if (!payloads.IsNullOrEmpty()) + { + payloadSelector = new WithPayloadSelector + { + Enable = true, + Include = new PayloadIncludeSelector + { + Fields = { payloads.ToArray() } + } + }; + } + + return payloadSelector; + } + + private OrderBy? BuildOrderBy(VectorSort? sort) + { + if (string.IsNullOrWhiteSpace(sort?.Field)) + { + return null; + } + + return new OrderBy + { + Key = sort.Field, + Direction = sort.Order.IsEqualTo("asc") ? Direction.Asc : Direction.Desc + }; + } + + private PayloadSchemaType ConvertPayloadSchemaType(string schemaType) + { + PayloadSchemaType res; + switch (schemaType.ToLower()) + { + case "text": + res = PayloadSchemaType.Text; + break; + case "keyword": + res = PayloadSchemaType.Keyword; + break; + case "integer": + res = PayloadSchemaType.Integer; + break; + case "float": + res = PayloadSchemaType.Float; + break; + case "bool": + res = PayloadSchemaType.Bool; + break; + case "geo": + res = PayloadSchemaType.Geo; + break; + case "datetime": + res = PayloadSchemaType.Datetime; + break; + case "uuid": + res = PayloadSchemaType.Uuid; + break; + default: + res = PayloadSchemaType.UnknownType; + break; + } + + return res; + } + #endregion } diff --git a/src/Plugins/BotSharp.Plugin.SemanticKernel/SemanticKernelMemoryStoreProvider.cs b/src/Plugins/BotSharp.Plugin.SemanticKernel/SemanticKernelMemoryStoreProvider.cs index 194d1a58..e87ea423 100644 --- a/src/Plugins/BotSharp.Plugin.SemanticKernel/SemanticKernelMemoryStoreProvider.cs +++ b/src/Plugins/BotSharp.Plugin.SemanticKernel/SemanticKernelMemoryStoreProvider.cs @@ -48,8 +48,7 @@ namespace BotSharp.Plugin.SemanticKernel throw new NotImplementedException(); } - public Task> GetCollectionData(string collectionName, IEnumerable ids, - bool withPayload = false, bool withVector = false) + public Task> GetCollectionData(string collectionName, IEnumerable ids, VectorQueryOptions? options = null) { throw new NotImplementedException(); } @@ -64,10 +63,10 @@ namespace BotSharp.Plugin.SemanticKernel return result; } - public async Task> Search(string collectionName, float[] vector, - IEnumerable? fields, int limit = 5, float confidence = 0.5f, bool withVector = false) + public async Task> Search(string collectionName, float[] vector, VectorSearchOptions? options = null) { - var results = _memoryStore.GetNearestMatchesAsync(collectionName, vector, limit); + options ??= VectorSearchOptions.Default(); + var results = _memoryStore.GetNearestMatchesAsync(collectionName, vector, options.Limit.GetValueOrDefault()); var resultTexts = new List(); await foreach (var (record, score) in results) @@ -76,7 +75,7 @@ namespace BotSharp.Plugin.SemanticKernel { Data = new Dictionary { { "text", record.Metadata.Text } }, Score = score, - Vector = withVector ? record.Embedding.ToArray() : null + Vector = options.WithVector ? record.Embedding.ToArray() : null }); } diff --git a/src/Plugins/BotSharp.Plugin.SparkDesk/Providers/ChatCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.SparkDesk/Providers/ChatCompletionProvider.cs index 035e03e0..a1d45dcc 100644 --- a/src/Plugins/BotSharp.Plugin.SparkDesk/Providers/ChatCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.SparkDesk/Providers/ChatCompletionProvider.cs @@ -1,10 +1,11 @@ using BotSharp.Abstraction.Agents; using BotSharp.Abstraction.Agents.Enums; +using BotSharp.Abstraction.Conversations; +using BotSharp.Abstraction.Conversations.Enums; using BotSharp.Abstraction.Loggers; -using BotSharp.Abstraction.Observables.Models; +using BotSharp.Abstraction.MessageHub.Models; using BotSharp.Core.Infrastructures.Streams; -using BotSharp.Core.Observables.Queues; -using Microsoft.AspNetCore.SignalR; +using BotSharp.Core.MessageHub; namespace BotSharp.Plugin.SparkDesk.Providers; @@ -152,12 +153,13 @@ public class ChatCompletionProvider : IChatCompletion var client = new SparkDeskClient(appId: _settings.AppId, apiKey: _settings.ApiKey, apiSecret: _settings.ApiSecret); var (prompt, messages, funcall) = PrepareOptions(agent, conversations); var messageId = conversations.LastOrDefault()?.MessageId ?? string.Empty; - var hub = _services.GetRequiredService>(); + var hub = _services.GetRequiredService>>(); + var conv = _services.GetRequiredService(); hub.Push(new() { - ServiceProvider = _services, - EventName = "BeforeReceiveLlmStreamMessage", + EventName = ChatEvent.BeforeReceiveLlmStreamMessage, + RefId = conv.ConversationId, Data = new RoleDialogModel(AgentRole.Assistant, string.Empty) { CurrentAgentId = agent.Id, @@ -197,8 +199,8 @@ public class ChatCompletionProvider : IChatCompletion hub.Push(new() { - ServiceProvider = _services, - EventName = "OnReceiveLlmStreamMessage", + EventName = ChatEvent.OnReceiveLlmStreamMessage, + RefId = conv.ConversationId, Data = responseMessage }); } @@ -212,8 +214,8 @@ public class ChatCompletionProvider : IChatCompletion hub.Push(new() { - ServiceProvider = _services, - EventName = "AfterReceiveLlmStreamMessage", + EventName = ChatEvent.AfterReceiveLlmStreamMessage, + RefId = conv.ConversationId, Data = responseMessage }); diff --git a/src/Plugins/BotSharp.Plugin.Twilio/Hooks/TwilioConversationHook.cs b/src/Plugins/BotSharp.Plugin.Twilio/Hooks/TwilioConversationHook.cs index 11bd8d36..bf856ee4 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/Hooks/TwilioConversationHook.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/Hooks/TwilioConversationHook.cs @@ -1,6 +1,7 @@ using BotSharp.Abstraction.Hooks; using BotSharp.Abstraction.Routing; using BotSharp.Abstraction.Routing.Enums; +using BotSharp.Abstraction.Routing.Models; using BotSharp.Plugin.Twilio.Interfaces; using BotSharp.Plugin.Twilio.Models; using Twilio.Rest.Api.V2010.Account; @@ -23,7 +24,7 @@ public class TwilioConversationHook : ConversationHookBase, IConversationHook _logger = logger; } - public override async Task OnFunctionExecuted(RoleDialogModel message, string from = InvokeSource.Manual) + public override async Task OnFunctionExecuted(RoleDialogModel message, InvokeFunctionOptions? options = null) { var hooks = _services.GetHooks(message.CurrentAgentId); diff --git a/src/Plugins/BotSharp.Plugin.Twilio/Services/TwilioMessageQueueService.cs b/src/Plugins/BotSharp.Plugin.Twilio/Services/TwilioMessageQueueService.cs index 84f88b5b..28360518 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/Services/TwilioMessageQueueService.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/Services/TwilioMessageQueueService.cs @@ -1,4 +1,6 @@ using BotSharp.Abstraction.Files; +using BotSharp.Abstraction.MessageHub.Models; +using BotSharp.Abstraction.MessageHub.Services; using BotSharp.Abstraction.Realtime; using BotSharp.Abstraction.Routing; using BotSharp.Core.Infrastructures; @@ -82,8 +84,12 @@ public class TwilioMessageQueueService : BackgroundService var routing = sp.GetRequiredService(); var config = sp.GetRequiredService(); var sessionManager = sp.GetRequiredService(); - var progressService = sp.GetRequiredService(); - InitProgressService(message, sessionManager, progressService); + var observer = sp.GetRequiredService(); + + using var container = observer.SubscribeObservers>(message.ConversationId, listeners: new() + { + { ChatEvent.OnIndicationReceived, async data => await OnReceiveToolCallIndication(data.Data, message, sessionManager) } + }); InitConversation(message, inputMsg, conv, routing); // Need to consider Inbound and Outbound call @@ -185,15 +191,11 @@ public class TwilioMessageQueueService : BackgroundService return string.Join(", ", hints.Select(x => x.ToLower()).Distinct().Reverse()); } - private static void InitProgressService(CallerMessage message, ITwilioSessionManager sessionManager, IConversationProgressService progressService) + private static async Task OnReceiveToolCallIndication(RoleDialogModel msg, CallerMessage message, ITwilioSessionManager sessionManager) { - progressService.OnFunctionExecuting = async msg => + if (!string.IsNullOrEmpty(msg.Indication)) { - if (!string.IsNullOrEmpty(msg.Indication)) - { - await sessionManager.SetReplyIndicationAsync(message.ConversationId, message.SeqNumber, msg.Indication); - } - }; - progressService.OnFunctionExecuted = async msg => { }; + await sessionManager.SetReplyIndicationAsync(message.ConversationId, message.SeqNumber, msg.Indication); + } } } diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.ExtractData.cs b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.ExtractData.cs index b716b7ba..ff3025bd 100644 --- a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.ExtractData.cs +++ b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.ExtractData.cs @@ -17,4 +17,11 @@ public partial class PlaywrightWebDriver return answer; } + public string GetCurrentUrl(MessageInfo message) + { + var page = _instance.GetPage(message.ContextId); + if (page == null) + return string.Empty; + return page.Url; + } } diff --git a/src/WebStarter/WebStarter.csproj b/src/WebStarter/WebStarter.csproj index 5db5199f..2610d4f1 100644 --- a/src/WebStarter/WebStarter.csproj +++ b/src/WebStarter/WebStarter.csproj @@ -36,11 +36,11 @@ - + @@ -75,6 +75,7 @@ + diff --git a/src/WebStarter/appsettings.json b/src/WebStarter/appsettings.json index 2b217318..1e0365ee 100644 --- a/src/WebStarter/appsettings.json +++ b/src/WebStarter/appsettings.json @@ -539,10 +539,8 @@ "BotSharp.Plugin.FileHandler", "BotSharp.Plugin.EmailHandler", "BotSharp.Plugin.AudioHandler", + "BotSharp.Plugin.ChartHandler", "BotSharp.Plugin.TencentCos" - ], - "ExcludedFunctions": [ - "McpToolAdapter" ] } } diff --git a/tests/BotSharp.LLM.Tests/Core/TestAgentService.cs b/tests/BotSharp.LLM.Tests/Core/TestAgentService.cs index 50d34701..5d4b07d0 100644 --- a/tests/BotSharp.LLM.Tests/Core/TestAgentService.cs +++ b/tests/BotSharp.LLM.Tests/Core/TestAgentService.cs @@ -16,7 +16,7 @@ namespace BotSharp.Plugin.Google.Core return Task.FromResult(new Agent()); } - public Task RefreshAgents() + public Task RefreshAgents(IEnumerable? agentIds = null) { return Task.FromResult("Refreshed successfully"); }