diff --git a/BotSharp.sln b/BotSharp.sln index 76d19498..645b6060 100644 --- a/BotSharp.sln +++ b/BotSharp.sln @@ -87,6 +87,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BotSharp.Plugin.SparkDesk", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Plugin.MetaGLM", "src\Plugins\BotSharp.Plugin.MetaGLM\BotSharp.Plugin.MetaGLM.csproj", "{CCF745F2-0C95-4ED0-983B-507C528B39EA}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BotSharp.Plugin.AnthropicAI", "src\Plugins\BotSharp.Plugin.AnthropicAI\BotSharp.Plugin.AnthropicAI.csproj", "{806A0B0E-FEFF-420E-B5B2-C9FCBF890A8C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -343,6 +345,14 @@ Global {CCF745F2-0C95-4ED0-983B-507C528B39EA}.Release|Any CPU.Build.0 = Release|Any CPU {CCF745F2-0C95-4ED0-983B-507C528B39EA}.Release|x64.ActiveCfg = Release|Any CPU {CCF745F2-0C95-4ED0-983B-507C528B39EA}.Release|x64.Build.0 = Release|Any CPU + {806A0B0E-FEFF-420E-B5B2-C9FCBF890A8C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {806A0B0E-FEFF-420E-B5B2-C9FCBF890A8C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {806A0B0E-FEFF-420E-B5B2-C9FCBF890A8C}.Debug|x64.ActiveCfg = Debug|Any CPU + {806A0B0E-FEFF-420E-B5B2-C9FCBF890A8C}.Debug|x64.Build.0 = Debug|Any CPU + {806A0B0E-FEFF-420E-B5B2-C9FCBF890A8C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {806A0B0E-FEFF-420E-B5B2-C9FCBF890A8C}.Release|Any CPU.Build.0 = Release|Any CPU + {806A0B0E-FEFF-420E-B5B2-C9FCBF890A8C}.Release|x64.ActiveCfg = Release|Any CPU + {806A0B0E-FEFF-420E-B5B2-C9FCBF890A8C}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -386,6 +396,7 @@ Global {267998C1-55C2-4ADC-8361-2CDFA5EA6D6C} = {51AFE054-AE99-497D-A593-69BAEFB5106F} {289E25C8-63F1-4D52-9909-207724DB40CB} = {D5293208-2BEF-42FC-A64C-5954F61720BA} {CCF745F2-0C95-4ED0-983B-507C528B39EA} = {D5293208-2BEF-42FC-A64C-5954F61720BA} + {806A0B0E-FEFF-420E-B5B2-C9FCBF890A8C} = {D5293208-2BEF-42FC-A64C-5954F61720BA} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {A9969D89-C98B-40A5-A12B-FC87E55B3A19} diff --git a/Directory.Build.props b/Directory.Build.props index 56e1a108..4ca87cf3 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -2,7 +2,7 @@ net8.0 10.0 - 1.2.1 + 1.5.1 true false diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/IAgentService.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/IAgentService.cs index c8aee271..b435a4d1 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/IAgentService.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/IAgentService.cs @@ -10,7 +10,7 @@ namespace BotSharp.Abstraction.Agents; public interface IAgentService { Task CreateAgent(Agent agent); - Task RefreshAgents(); + Task RefreshAgents(); Task> GetAgents(AgentFilter filter); /// @@ -26,6 +26,8 @@ public interface IAgentService bool RenderFunction(Agent agent, FunctionDef def); + FunctionParametersDef? RenderFunctionProperty(Agent agent, FunctionDef def); + /// /// Get agent detail without trigger any hook. /// @@ -35,9 +37,18 @@ public interface IAgentService Task DeleteAgent(string id); Task UpdateAgent(Agent agent, AgentField updateField); - Task UpdateAgentFromFile(string id); + + /// + /// Path existing templates of agent, cannot create new or delete templates + /// + /// + /// + Task PatchAgentTemplate(Agent agent); + Task UpdateAgentFromFile(string id); string GetDataDir(); string GetAgentDataDir(string agentId); + List GetAgentsByUser(string userId); + PluginDef GetPlugin(string agentId); } diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/Settings/AgentSettings.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/Settings/AgentSettings.cs index e95f72bc..fefda240 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/Settings/AgentSettings.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/Settings/AgentSettings.cs @@ -5,6 +5,8 @@ public class AgentSettings public string DataDir { get; set; } = string.Empty; public string TemplateFormat { get; set; } = "liquid"; public string HostAgentId { get; set; } = string.Empty; + public bool EnableTranslator { get; set; } = false; + public bool EnableHttpHandler { get; set; } = false; /// /// This is the default LLM config for agent diff --git a/src/Infrastructure/BotSharp.Abstraction/BotSharp.Abstraction.csproj b/src/Infrastructure/BotSharp.Abstraction/BotSharp.Abstraction.csproj index bbade11e..65424adb 100644 --- a/src/Infrastructure/BotSharp.Abstraction/BotSharp.Abstraction.csproj +++ b/src/Infrastructure/BotSharp.Abstraction/BotSharp.Abstraction.csproj @@ -22,6 +22,7 @@ + diff --git a/src/Infrastructure/BotSharp.Abstraction/Browsing/Enums/BroswerActionEnum.cs b/src/Infrastructure/BotSharp.Abstraction/Browsing/Enums/BroswerActionEnum.cs new file mode 100644 index 00000000..6dd0fb63 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Browsing/Enums/BroswerActionEnum.cs @@ -0,0 +1,7 @@ +namespace BotSharp.Abstraction.Browsing.Enums; + +public enum BroswerActionEnum +{ + Click = 1, + InputText = 2, +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Browsing/IWebBrowser.cs b/src/Infrastructure/BotSharp.Abstraction/Browsing/IWebBrowser.cs index 376c7cac..e2581ba9 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Browsing/IWebBrowser.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Browsing/IWebBrowser.cs @@ -4,8 +4,8 @@ namespace BotSharp.Abstraction.Browsing; public interface IWebBrowser { - Task LaunchBrowser(string conversationId, string? url); - Task ScreenshotAsync(string conversationId, string path); + Task LaunchBrowser(string contextId, string? url); + Task ScreenshotAsync(string contextId, string path); Task ScrollPageAsync(BrowserActionParams actionParams); Task ActionOnElement(MessageInfo message, ElementLocatingArgs location, ElementActionArgs action); @@ -19,10 +19,11 @@ public interface IWebBrowser Task ChangeListValue(BrowserActionParams actionParams); Task CheckRadioButton(BrowserActionParams actionParams); Task ChangeCheckbox(BrowserActionParams actionParams); - Task GoToPage(string conversationId, string url); + Task GoToPage(string contextId, string url, bool openNewTab = false); Task ExtractData(BrowserActionParams actionParams); - Task EvaluateScript(string conversationId, string script); - Task CloseBrowser(string conversationId); - Task SendHttpRequest(BrowserActionParams actionParams); - Task GetAttributeValue(MessageInfo message, ElementLocatingArgs location, BrowserActionResult result); + Task EvaluateScript(string contextId, string script); + Task CloseBrowser(string contextId); + Task CloseCurrentPage(string contextId); + Task SendHttpRequest(MessageInfo message, HttpRequestParams actionParams); + Task GetAttributeValue(MessageInfo message, ElementLocatingArgs location); } diff --git a/src/Infrastructure/BotSharp.Abstraction/Browsing/Models/BrowserActionParams.cs b/src/Infrastructure/BotSharp.Abstraction/Browsing/Models/BrowserActionParams.cs index 37c820a2..04ba82b2 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Browsing/Models/BrowserActionParams.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Browsing/Models/BrowserActionParams.cs @@ -4,14 +4,14 @@ public class BrowserActionParams { public Agent Agent { get; set; } public BrowsingContextIn Context { get; set; } - public string ConversationId { get; set; } + public string ContextId { get; set; } public string MessageId { get; set; } - public BrowserActionParams(Agent agent, BrowsingContextIn context, string conversationId, string messageId) + public BrowserActionParams(Agent agent, BrowsingContextIn context, string contextId, string messageId) { Agent = agent; Context = context; - ConversationId = conversationId; + ContextId = contextId; MessageId = messageId; } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Browsing/Models/BrowserActionResult.cs b/src/Infrastructure/BotSharp.Abstraction/Browsing/Models/BrowserActionResult.cs index 9e45cf97..b3576b90 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Browsing/Models/BrowserActionResult.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Browsing/Models/BrowserActionResult.cs @@ -3,8 +3,14 @@ namespace BotSharp.Abstraction.Browsing.Models; public class BrowserActionResult { public bool IsSuccess { get; set; } - public string ErrorMessage { get; set; } - public string StackTrace { get; set; } + public string? Message { get; set; } + public string? StackTrace { get; set; } public string Selector { get; set; } public string Body { get; set; } + public bool IsHighlighted { get; set; } + + public override string ToString() + { + return $"{IsSuccess} - {Selector}"; + } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Browsing/Models/BrowsingContextIn.cs b/src/Infrastructure/BotSharp.Abstraction/Browsing/Models/BrowsingContextIn.cs index f613738b..ff3b94f3 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Browsing/Models/BrowsingContextIn.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Browsing/Models/BrowsingContextIn.cs @@ -40,10 +40,4 @@ public class BrowsingContextIn [JsonPropertyName("direction")] public string? Direction { get; set; } - - /// - /// Http request payload - /// - [JsonPropertyName("payload")] - public string? Payload { get; set; } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Browsing/Models/ElementActionArgs.cs b/src/Infrastructure/BotSharp.Abstraction/Browsing/Models/ElementActionArgs.cs index 32d13c5c..0d44066a 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Browsing/Models/ElementActionArgs.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Browsing/Models/ElementActionArgs.cs @@ -1,12 +1,34 @@ +using BotSharp.Abstraction.Browsing.Enums; + namespace BotSharp.Abstraction.Browsing.Models; public class ElementActionArgs { - private string _action; - public string Action => _action; + public BroswerActionEnum Action { get; set; } - public ElementActionArgs(string action) + public string? Content { get; set; } + + public ElementPosition? Position { get; set; } + + public string? PressKey { get; set; } + + /// + /// Required for deserialization + /// + public ElementActionArgs() { - _action = action; + + } + + public ElementActionArgs(BroswerActionEnum action, ElementPosition? position = null) + { + Action = action; + Position = position; + } + + public ElementActionArgs(BroswerActionEnum action, string content) + { + Action = action; + Content = content; } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Browsing/Models/ElementLocatingArgs.cs b/src/Infrastructure/BotSharp.Abstraction/Browsing/Models/ElementLocatingArgs.cs index d043ca54..3b96c760 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Browsing/Models/ElementLocatingArgs.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Browsing/Models/ElementLocatingArgs.cs @@ -5,6 +5,9 @@ public class ElementLocatingArgs [JsonPropertyName("match_rule")] public string MatchRule { get; set; } = string.Empty; + [JsonPropertyName("tag")] + public string? Tag { get; set; } = null!; + [JsonPropertyName("text")] public string? Text { get; set; } @@ -20,5 +23,12 @@ public class ElementLocatingArgs [JsonPropertyName("selector")] public string? Selector { get; set; } + public bool Parent { get; set; } + public bool FailIfMultiple { get; set; } + + /// + /// Draw outline around the element + /// + public bool Highlight { get; set; } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Browsing/Models/ElementPosition.cs b/src/Infrastructure/BotSharp.Abstraction/Browsing/Models/ElementPosition.cs new file mode 100644 index 00000000..fd95bd66 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Browsing/Models/ElementPosition.cs @@ -0,0 +1,8 @@ +namespace BotSharp.Abstraction.Browsing.Models; + +public class ElementPosition +{ + public float X { get; set; } = default!; + + public float Y { get; set; } = default!; +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Browsing/Models/HttpRequestParams.cs b/src/Infrastructure/BotSharp.Abstraction/Browsing/Models/HttpRequestParams.cs new file mode 100644 index 00000000..e6d24a8e --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Browsing/Models/HttpRequestParams.cs @@ -0,0 +1,25 @@ +using System.Net.Http; + +namespace BotSharp.Abstraction.Browsing.Models; + +public class HttpRequestParams +{ + [JsonPropertyName("url")] + public string Url { get; set; } = string.Empty; + + [JsonPropertyName("method")] + public HttpMethod Method { get; set; } + + /// + /// Http request payload + /// + [JsonPropertyName("payload")] + public string? Payload { get; set; } + + public HttpRequestParams(string url, HttpMethod method, string? payload = null) + { + Method = method; + Url = url; + Payload = payload; + } +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Browsing/Models/MessageInfo.cs b/src/Infrastructure/BotSharp.Abstraction/Browsing/Models/MessageInfo.cs index dc530e6d..336d255c 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Browsing/Models/MessageInfo.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Browsing/Models/MessageInfo.cs @@ -3,6 +3,6 @@ namespace BotSharp.Abstraction.Browsing.Models; public class MessageInfo { public string AgentId { get; set; } - public string ConversationId { get; set; } + public string ContextId { get; set; } public string MessageId { get; set; } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Browsing/Settings/WebBrowsingSettings.cs b/src/Infrastructure/BotSharp.Abstraction/Browsing/Settings/WebBrowsingSettings.cs new file mode 100644 index 00000000..25149fe0 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Browsing/Settings/WebBrowsingSettings.cs @@ -0,0 +1,6 @@ +namespace BotSharp.Abstraction.Browsing.Settings; + +public class WebBrowsingSettings +{ + public string Driver { get; set; } = "Playwright"; +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Conversations/ConversationHookBase.cs b/src/Infrastructure/BotSharp.Abstraction/Conversations/ConversationHookBase.cs index 36f065fd..06a97174 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Conversations/ConversationHookBase.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Conversations/ConversationHookBase.cs @@ -44,6 +44,9 @@ public abstract class ConversationHookBase : IConversationHook public virtual Task OnConversationEnding(RoleDialogModel message) => Task.CompletedTask; + public virtual Task OnNewTaskDetected(RoleDialogModel message, string reason) + => Task.CompletedTask; + public virtual Task OnTaskCompleted(RoleDialogModel message) => Task.CompletedTask; diff --git a/src/Infrastructure/BotSharp.Abstraction/Conversations/Enums/StateDataType.cs b/src/Infrastructure/BotSharp.Abstraction/Conversations/Enums/StateDataType.cs new file mode 100644 index 00000000..20d635f4 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Conversations/Enums/StateDataType.cs @@ -0,0 +1,10 @@ +namespace BotSharp.Abstraction.Conversations.Enums; + +public class StateDataType +{ + public const string String = "string"; + public const string Boolean = "boolean"; + public const string Number = "number"; + public const string Currency = "currency"; + public const string Date = "date"; +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Conversations/Enums/StateSource.cs b/src/Infrastructure/BotSharp.Abstraction/Conversations/Enums/StateSource.cs new file mode 100644 index 00000000..31f32e16 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Conversations/Enums/StateSource.cs @@ -0,0 +1,8 @@ +namespace BotSharp.Abstraction.Conversations.Enums; + +public class StateSource +{ + public const string External = "external"; + public const string Application = "application"; + public const string User = "user"; +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationAttachmentService.cs b/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationAttachmentService.cs deleted file mode 100644 index feec3fab..00000000 --- a/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationAttachmentService.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace BotSharp.Abstraction.Conversations; - -public interface IConversationAttachmentService -{ - string GetDirectory(string conversationId); -} diff --git a/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationHook.cs b/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationHook.cs index b9f0f3b8..9ed47c61 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationHook.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationHook.cs @@ -64,6 +64,13 @@ public interface IConversationHook Task OnResponseGenerated(RoleDialogModel message); + /// + /// LLM detected user requested a new task different from previous topic. + /// + /// + /// + Task OnNewTaskDetected(RoleDialogModel message, string reason); + /// /// LLM detected the current task is completed. /// It's useful for the situation of multiple tasks in the same conversation. diff --git a/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationService.cs b/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationService.cs index effb8a94..c8b997ec 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationService.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationService.cs @@ -15,7 +15,15 @@ public interface IConversationService Task> GetLastConversations(); Task> GetIdleConversations(int batchSize, int messageLimit, int bufferHours); Task DeleteConversations(IEnumerable ids); - Task TruncateConversation(string conversationId, string messageId); + + /// + /// Truncate conversation + /// + /// Target conversation id + /// Target message id to delete + /// If not null, delete messages while input a new message; otherwise delete messages only + /// + Task TruncateConversation(string conversationId, string messageId, string? newMessageId = null); Task> GetConversationContentLogs(string conversationId); Task> GetConversationStateLogs(string conversationId); @@ -42,6 +50,10 @@ public interface IConversationService /// Use this feature when you want to hide some context from LLM. /// /// Whether to reset all states + /// Append user init words + /// /// - Task UpdateBreakpoint(bool resetStates = false); + Task UpdateBreakpoint(bool resetStates = false, string? reason = null, params string[] excludedStates); + + Task GetConversationSummary(IEnumerable conversationId); } diff --git a/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationStateService.cs b/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationStateService.cs index 07eab6c1..de26ef5b 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationStateService.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationStateService.cs @@ -1,3 +1,4 @@ +using BotSharp.Abstraction.Conversations.Enums; using System.Text.Json; namespace BotSharp.Abstraction.Conversations; @@ -8,12 +9,14 @@ namespace BotSharp.Abstraction.Conversations; public interface IConversationStateService { string GetConversationId(); - Dictionary Load(string conversationId); + Dictionary Load(string conversationId, bool isReadOnly = false); string GetState(string name, string defaultValue = ""); bool ContainsState(string name); Dictionary GetStates(); - IConversationStateService SetState(string name, T value, bool isNeedVersion = true, int activeRounds = -1); + IConversationStateService SetState(string name, T value, bool isNeedVersion = true, + int activeRounds = -1, string valueType = StateDataType.String, string source = StateSource.User, bool readOnly = false); void SaveStateByArgs(JsonDocument args); - void CleanStates(); + bool RemoveState(string name); + void CleanStates(params string[] excludedStates); void Save(); } diff --git a/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/Conversation.cs b/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/Conversation.cs index 530f425d..ad7ffd04 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/Conversation.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/Conversation.cs @@ -32,34 +32,63 @@ public class Conversation public class DialogElement { + [JsonPropertyName("meta_data")] public DialogMetaData MetaData { get; set; } + + [JsonPropertyName("content")] public string Content { get; set; } + + [JsonPropertyName("secondary_content")] + public string? SecondaryContent { get; set; } + + [JsonPropertyName("rich_content")] public string? RichContent { get; set; } + [JsonPropertyName("secondary_rich_content")] + public string? SecondaryRichContent { get; set; } + + [JsonPropertyName("payload")] + public string? Payload { get; set; } + public DialogElement() { } - public DialogElement(DialogMetaData meta, string content, string? richContent = null) + public DialogElement(DialogMetaData meta, string content, string? richContent = null, + string? secondaryContent = null, string? secondaryRichContent = null, string? payload = null) { MetaData = meta; Content = content; RichContent = richContent; + SecondaryContent = secondaryContent; + SecondaryRichContent = secondaryRichContent; + Payload = payload; } public override string ToString() { - return $"{MetaData.Role}: {Content} [{MetaData.CreateTime}]"; + return $"{MetaData.Role}: {Content} [{MetaData?.CreateTime}]"; } } public class DialogMetaData { + [JsonPropertyName("role")] public string Role { get; set; } + + [JsonPropertyName("agent_id")] public string AgentId { get; set; } + + [JsonPropertyName("message_id")] public string MessageId { get; set; } + + [JsonPropertyName("function_name")] public string? FunctionName { get; set; } + + [JsonPropertyName("sender_id")] public string? SenderId { get; set; } + + [JsonPropertyName("create_at")] public DateTime CreateTime { get; set; } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/ConversationBreakpoint.cs b/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/ConversationBreakpoint.cs index 69a88d13..0d819353 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/ConversationBreakpoint.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/ConversationBreakpoint.cs @@ -10,4 +10,7 @@ public class ConversationBreakpoint [JsonPropertyName("created_time")] public DateTime CreatedTime { get; set; } = DateTime.UtcNow; + + [JsonPropertyName("reason")] + public string? Reason { get; set; } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/ConversationSenderActionModel.cs b/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/ConversationSenderActionModel.cs index a153472c..b49fc55d 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/ConversationSenderActionModel.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/ConversationSenderActionModel.cs @@ -6,6 +6,10 @@ public class ConversationSenderActionModel { [JsonPropertyName("conversation_id")] public string ConversationId { get; set; } + [JsonPropertyName("sender_action")] public SenderActionEnum SenderAction { get; set; } + + [JsonPropertyName("indication")] + public string? Indication { get; set; } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/IncomingMessageModel.cs b/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/IncomingMessageModel.cs index 2176c7fa..cd4aad61 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/IncomingMessageModel.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/IncomingMessageModel.cs @@ -9,4 +9,6 @@ public class IncomingMessageModel : MessageConfig /// Postback message /// public PostbackMessageModel? Postback { get; set; } + + public List Files { get; set; } = new List(); } diff --git a/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/RoleDialogModel.cs b/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/RoleDialogModel.cs index 96d933f5..a4aba84d 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/RoleDialogModel.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/RoleDialogModel.cs @@ -26,6 +26,20 @@ public class RoleDialogModel : ITrackableMessage public string Content { get; set; } + public string? SecondaryContent { get; set; } + + /// + /// Postback content + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("payload")] + public string? Payload { get; set; } + + /// + /// Indicator message used to provide UI feedback for function execution + /// + public string? Indication { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string CurrentAgentId { get; set; } @@ -35,6 +49,12 @@ public class RoleDialogModel : ITrackableMessage [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? FunctionName { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? ToolCallId { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? PostbackFunctionName { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? FunctionArgs { get; set; } @@ -54,6 +74,9 @@ public class RoleDialogModel : ITrackableMessage [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public RichContent? RichContent { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public RichContent? SecondaryRichContent { get; set; } + /// /// Stop conversation completion /// @@ -62,6 +85,8 @@ public class RoleDialogModel : ITrackableMessage public FunctionCallFromLlm Instruction { get; set; } + public List Files { get; set; } = new List(); + private RoleDialogModel() { } @@ -95,6 +120,8 @@ public class RoleDialogModel : ITrackableMessage MessageId = source.MessageId, FunctionArgs = source.FunctionArgs, FunctionName = source.FunctionName, + ToolCallId = source.ToolCallId, + PostbackFunctionName = source.PostbackFunctionName, RichContent = source.RichContent, StopCompletion = source.StopCompletion, Instruction = source.Instruction, diff --git a/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/StateChangeModel.cs b/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/StateChangeModel.cs index f98a896d..de84692f 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/StateChangeModel.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/StateChangeModel.cs @@ -12,14 +12,23 @@ public class StateChangeModel public string Name { get; set; } [JsonPropertyName("before_value")] - public string BeforeValue { get; set; } + public string? BeforeValue { get; set; } [JsonPropertyName("before_active_rounds")] public int? BeforeActiveRounds { get; set; } [JsonPropertyName("after_value")] - public string AfterValue { get; set; } + public string? AfterValue { get; set; } [JsonPropertyName("after_active_rounds")] public int? AfterActiveRounds { get; set; } + + [JsonPropertyName("data_type")] + public string DataType { get; set; } + + [JsonPropertyName("source")] + public string Source { get; set; } + + [JsonPropertyName("readonly")] + public bool Readonly { get; set; } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/StateKeyValue.cs b/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/StateKeyValue.cs index 53618242..4afea9da 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/StateKeyValue.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/StateKeyValue.cs @@ -1,9 +1,12 @@ +using BotSharp.Abstraction.Conversations.Enums; + namespace BotSharp.Abstraction.Conversations.Models; public class StateKeyValue { public string Key { get; set; } public bool Versioning { get; set; } + public bool Readonly { get; set; } public List Values { get; set; } = new List(); public StateKeyValue() @@ -16,6 +19,12 @@ public class StateKeyValue Key = key; Values = values; } + + public override string ToString() + { + var lastValue = Values.LastOrDefault(); + return $"{Key} => ({lastValue?.ToString()})"; + } } public class StateValue @@ -30,6 +39,12 @@ public class StateValue [JsonPropertyName("active_rounds")] public int ActiveRounds { get; set; } + [JsonPropertyName("data_type")] + public string DataType { get; set; } = StateDataType.String; + + [JsonPropertyName("source")] + public string Source { get; set; } + [JsonPropertyName("update_time")] public DateTime UpdateTime { get; set; } @@ -37,4 +52,11 @@ public class StateValue { } + + public override string ToString() + { + var isActive = Active ? "Yes" : "No"; + var activeRounds = ActiveRounds <= 0 ? "infinity" : ActiveRounds.ToString(); + return $"Data: {Data}, Active: {isActive}, Active rounds: {activeRounds}, Source: {Source}"; + } } \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Abstraction/Conversations/Settings/RateLimitSetting.cs b/src/Infrastructure/BotSharp.Abstraction/Conversations/Settings/RateLimitSetting.cs index 0c9a7436..566f7bef 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Conversations/Settings/RateLimitSetting.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Conversations/Settings/RateLimitSetting.cs @@ -3,6 +3,6 @@ namespace BotSharp.Abstraction.Conversations.Settings; public class RateLimitSetting { public int MaxConversationPerDay { get; set; } = 100; - public int MaxInputLengthPerRequest { get; set; } = 256; + public int MaxInputLengthPerRequest { get; set; } = 512; public int MinTimeSecondsBetweenMessages { get; set; } = 2; } diff --git a/src/Infrastructure/BotSharp.Abstraction/Files/IBotSharpFileService.cs b/src/Infrastructure/BotSharp.Abstraction/Files/IBotSharpFileService.cs new file mode 100644 index 00000000..16197b9c --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Files/IBotSharpFileService.cs @@ -0,0 +1,31 @@ +namespace BotSharp.Abstraction.Files; + +public interface IBotSharpFileService +{ + string GetDirectory(string conversationId); + IEnumerable GetChatImages(string conversationId, List conversations, int offset = 2); + IEnumerable GetMessageFiles(string conversationId, IEnumerable messageIds, bool imageOnly = false); + string GetMessageFile(string conversationId, string messageId, string fileName); + bool SaveMessageFiles(string conversationId, string messageId, List files); + + string GetUserAvatar(); + bool SaveUserAvatar(BotSharpFile file); + + /// + /// Delete files under messages + /// + /// Conversation Id + /// Files in these messages will be deleted + /// The starting message to delete + /// If not null, delete messages while input a new message; otherwise, delete messages only + /// + bool DeleteMessageFiles(string conversationId, IEnumerable messageIds, string targetMessageId, string? newMessageId = null); + bool DeleteConversationFiles(IEnumerable conversationIds); + + /// + /// Get file bytes and content type from data, e.g., "data:image/png;base64,aaaaaaaaa" + /// + /// + /// + (string, byte[]) GetFileInfoFromData(string data); +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Files/Models/BotSharpFile.cs b/src/Infrastructure/BotSharp.Abstraction/Files/Models/BotSharpFile.cs new file mode 100644 index 00000000..de226f58 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Files/Models/BotSharpFile.cs @@ -0,0 +1,17 @@ + +namespace BotSharp.Abstraction.Files.Models; + +public class BotSharpFile +{ + [JsonPropertyName("file_name")] + public string FileName { get; set; } = string.Empty; + + /// + /// File data, e.g., "data:image/png;base64,aaaaaaaa" + /// + [JsonPropertyName("file_data")] + public string FileData { get; set; } = string.Empty; + + [JsonPropertyName("file_url")] + public string FileUrl { get; set; } = string.Empty; +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Files/Models/MessageFileModel.cs b/src/Infrastructure/BotSharp.Abstraction/Files/Models/MessageFileModel.cs new file mode 100644 index 00000000..3ec63fd8 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Files/Models/MessageFileModel.cs @@ -0,0 +1,32 @@ +namespace BotSharp.Abstraction.Files.Models; + +public class MessageFileModel +{ + [JsonPropertyName("message_id")] + public string MessageId { get; set; } + + [JsonPropertyName("file_url")] + public string FileUrl { get; set; } + + [JsonPropertyName("file_storage_url")] + public string FileStorageUrl { get; set; } + + [JsonPropertyName("file_name")] + public string FileName { get; set; } + + [JsonPropertyName("file_type")] + public string FileType { get; set; } + + [JsonPropertyName("content_type")] + public string ContentType { get; set; } + + public MessageFileModel() + { + + } + + public override string ToString() + { + return $"File name: {FileName}, File type: {FileType}, Content type: {ContentType}"; + } +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Functions/IFunctionCallback.cs b/src/Infrastructure/BotSharp.Abstraction/Functions/IFunctionCallback.cs index 3973cb9d..ffc25f33 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Functions/IFunctionCallback.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Functions/IFunctionCallback.cs @@ -3,5 +3,11 @@ namespace BotSharp.Abstraction.Functions; public interface IFunctionCallback { string Name { get; } + + /// + /// Indicator message used to provide UI feedback for function execution + /// + string Indication => string.Empty; + Task Execute(RoleDialogModel message); } diff --git a/src/Infrastructure/BotSharp.Abstraction/Functions/Models/FunctionCallFromLlm.cs b/src/Infrastructure/BotSharp.Abstraction/Functions/Models/FunctionCallFromLlm.cs index fefade10..661a1437 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Functions/Models/FunctionCallFromLlm.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Functions/Models/FunctionCallFromLlm.cs @@ -1,4 +1,3 @@ -using BotSharp.Abstraction.Routing.Models; using System.Text.Json; namespace BotSharp.Abstraction.Functions.Models; diff --git a/src/Infrastructure/BotSharp.Abstraction/Functions/Models/FunctionCallingResponse.cs b/src/Infrastructure/BotSharp.Abstraction/Functions/Models/FunctionCallingResponse.cs index d5bd5979..d437d425 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Functions/Models/FunctionCallingResponse.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Functions/Models/FunctionCallingResponse.cs @@ -13,7 +13,7 @@ public class FunctionCallingResponse [JsonPropertyName("content")] public string? Content { get; set; } - [JsonPropertyName("function_name")] + [JsonPropertyName("function")] public string? FunctionName { get; set; } [JsonPropertyName("args")] diff --git a/src/Infrastructure/BotSharp.Abstraction/Functions/Models/FunctionDef.cs b/src/Infrastructure/BotSharp.Abstraction/Functions/Models/FunctionDef.cs index 8458adf6..4366d323 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Functions/Models/FunctionDef.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Functions/Models/FunctionDef.cs @@ -2,8 +2,11 @@ namespace BotSharp.Abstraction.Functions.Models; public class FunctionDef { - public string Name { get; set; } - public string Description { get; set; } + [JsonPropertyName("name")] + public string Name { get; set; } = null!; + + [JsonPropertyName("description")] + public string Description { get; set; } = null!; [JsonPropertyName("visibility_expression")] public string? VisibilityExpression { get; set; } @@ -11,6 +14,7 @@ public class FunctionDef [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Impact { get; set; } + [JsonPropertyName("parameters")] public FunctionParametersDef Parameters { get; set; } = new FunctionParametersDef(); public override string ToString() diff --git a/src/Infrastructure/BotSharp.Abstraction/Functions/Models/FunctionParametersDef.cs b/src/Infrastructure/BotSharp.Abstraction/Functions/Models/FunctionParametersDef.cs index 975a17d6..12c313fd 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Functions/Models/FunctionParametersDef.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Functions/Models/FunctionParametersDef.cs @@ -19,6 +19,11 @@ public class FunctionParametersDef [JsonPropertyName("required")] public List Required { get; set; } = new List(); + public override string ToString() + { + return $"{{\"type\":\"{Type}\", \"properties\":{JsonSerializer.Serialize(Properties)}, \"required\":[{string.Join(",", Required.Select(x => "\"" + x + "\""))}]}}"; + } + public FunctionParametersDef() { diff --git a/src/Infrastructure/BotSharp.Abstraction/Functions/Models/ParameterPropertyDef.cs b/src/Infrastructure/BotSharp.Abstraction/Functions/Models/ParameterPropertyDef.cs index fd927bbc..4f948aa1 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Functions/Models/ParameterPropertyDef.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Functions/Models/ParameterPropertyDef.cs @@ -2,10 +2,11 @@ namespace BotSharp.Abstraction.Functions.Models; public class ParameterPropertyDef : NameDesc { - public ParameterPropertyDef(string name, string description, string type = "string") + public ParameterPropertyDef(string name, string description, string type = "string", bool required = false) : base(name, description) { Type = type; + Required = required; } [JsonPropertyName("required")] diff --git a/src/Infrastructure/BotSharp.Abstraction/Google/Models/GoogleVideoResult.cs b/src/Infrastructure/BotSharp.Abstraction/Google/Models/GoogleVideoResult.cs index 442290aa..49e50c30 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Google/Models/GoogleVideoResult.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Google/Models/GoogleVideoResult.cs @@ -3,7 +3,7 @@ namespace BotSharp.Abstraction.Google.Models; public class GoogleVideoResult { public string Kind { get; set; } - public IList Items { get; set; } = new List(); + public List Items { get; set; } = new List(); } public class VideoItem @@ -25,6 +25,7 @@ public class VideoSnippet { public string Title { get; set; } public string Description { get; set; } + public string ChannelId { get; set; } public string ChannelTitle { get; set; } public VideoThumbnails Thumbnails { get; set; } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Google/Settings/GoogleApiSettings.cs b/src/Infrastructure/BotSharp.Abstraction/Google/Settings/GoogleApiSettings.cs index be7ef6a2..dfa66f60 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Google/Settings/GoogleApiSettings.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Google/Settings/GoogleApiSettings.cs @@ -19,4 +19,5 @@ public class YoutubeSettings public string Endpoint { get; set; } public string Part { get; set; } public string RegionCode { get; set; } + public IList Channels { get; set; } } \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Abstraction/Http/Settings/HttpSettings.cs b/src/Infrastructure/BotSharp.Abstraction/Http/Settings/HttpSettings.cs new file mode 100644 index 00000000..8fc14988 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Http/Settings/HttpSettings.cs @@ -0,0 +1,7 @@ +namespace BotSharp.Abstraction.Http.Settings; + +public class HttpSettings +{ + public string BaseAddress { get; set; } = string.Empty; + public string Origin { get; set; } = string.Empty; +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Infrastructures/Enums/LanguageType.cs b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/Enums/LanguageType.cs new file mode 100644 index 00000000..64351e45 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/Enums/LanguageType.cs @@ -0,0 +1,9 @@ +namespace BotSharp.Abstraction.Infrastructures.Enums; + +public class LanguageType +{ + public const string UNKNOWN = "Unknown"; + public const string ENGLISH = "English"; + public const string SPANISH = "Spanish"; + public const string CHINESE = "Chinese"; +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Infrastructures/Enums/StateConst.cs b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/Enums/StateConst.cs index a4039bf7..442c8ef8 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Infrastructures/Enums/StateConst.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/Enums/StateConst.cs @@ -5,5 +5,8 @@ public class StateConst public const string EXPECTED_ACTION_AGENT = "expected_next_action_agent"; public const string EXPECTED_GOAL_AGENT = "expected_user_goal_agent"; public const string NEXT_ACTION_AGENT = "next_action_agent"; + public const string NEXT_ACTION_REASON = "next_action_reason"; public const string USER_GOAL_AGENT = "user_goal_agent"; + + public const string LANGUAGE = "language"; } diff --git a/src/Infrastructure/BotSharp.Abstraction/MLTasks/ILlmProviderService.cs b/src/Infrastructure/BotSharp.Abstraction/MLTasks/ILlmProviderService.cs index 75fd60e6..20762fe0 100644 --- a/src/Infrastructure/BotSharp.Abstraction/MLTasks/ILlmProviderService.cs +++ b/src/Infrastructure/BotSharp.Abstraction/MLTasks/ILlmProviderService.cs @@ -6,6 +6,6 @@ public interface ILlmProviderService { LlmModelSetting GetSetting(string provider, string model); List GetProviders(); - LlmModelSetting GetProviderModel(string provider, string id); + LlmModelSetting GetProviderModel(string provider, string id, bool? multiModal = null); List GetProviderModels(string provider); } diff --git a/src/Infrastructure/BotSharp.Abstraction/MLTasks/Settings/LlmModelSetting.cs b/src/Infrastructure/BotSharp.Abstraction/MLTasks/Settings/LlmModelSetting.cs index 1faf3c52..b86578fe 100644 --- a/src/Infrastructure/BotSharp.Abstraction/MLTasks/Settings/LlmModelSetting.cs +++ b/src/Infrastructure/BotSharp.Abstraction/MLTasks/Settings/LlmModelSetting.cs @@ -27,6 +27,11 @@ public class LlmModelSetting public string Endpoint { get; set; } public LlmModelType Type { get; set; } = LlmModelType.Chat; + /// + /// If true, allow sending images/vidoes to this model + /// + public bool MultiModal { get; set; } + /// /// Prompt cost per 1K token /// diff --git a/src/Infrastructure/BotSharp.Abstraction/Messaging/MessageParser.cs b/src/Infrastructure/BotSharp.Abstraction/Messaging/BotSharpMessageParser.cs similarity index 55% rename from src/Infrastructure/BotSharp.Abstraction/Messaging/MessageParser.cs rename to src/Infrastructure/BotSharp.Abstraction/Messaging/BotSharpMessageParser.cs index b739709c..1f495a44 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Messaging/MessageParser.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Messaging/BotSharpMessageParser.cs @@ -1,17 +1,17 @@ -using BotSharp.Abstraction.Messaging; -using BotSharp.Abstraction.Messaging.Enums; using BotSharp.Abstraction.Messaging.Models.RichContent.Template; using BotSharp.Abstraction.Messaging.Models.RichContent; using System.Text.Json; +using System.Reflection; -namespace BotSharp.Core.Messaging; +namespace BotSharp.Abstraction.Messaging; -public static class MessageParser +public static class BotSharpMessageParser { public static IRichMessage? ParseRichMessage(JsonElement root, JsonSerializerOptions options) { IRichMessage? res = null; + Type? targetType = null; JsonElement element; var jsonText = root.GetRawText(); @@ -20,47 +20,52 @@ public static class MessageParser var richType = element.GetString(); if (richType == RichTypeEnum.ButtonTemplate) { - res = JsonSerializer.Deserialize(jsonText, options); + targetType = typeof(ButtonTemplateMessage); } else if (richType == RichTypeEnum.MultiSelectTemplate) { - res = JsonSerializer.Deserialize(jsonText, options); + targetType = typeof(MultiSelectTemplateMessage); } else if (richType == RichTypeEnum.QuickReply) { - res = JsonSerializer.Deserialize(jsonText, options); + targetType = typeof(QuickReplyMessage); } else if (richType == RichTypeEnum.CouponTemplate) { - res = JsonSerializer.Deserialize(jsonText, options); + targetType = typeof(CouponTemplateMessage); } else if (richType == RichTypeEnum.Text) { - res = JsonSerializer.Deserialize(jsonText, options); + targetType = typeof(TextMessage); } else if (richType == RichTypeEnum.GenericTemplate) { if (root.TryGetProperty("element_type", out element)) { var elementType = element.GetString(); - if (elementType == typeof(GenericElement).Name) + var wrapperType = typeof(GenericTemplateMessage<>); + var genericType = Assembly.GetExecutingAssembly().GetTypes().FirstOrDefault(x => x.Name == elementType); + + if (wrapperType != null && genericType != null) { - res = JsonSerializer.Deserialize>(jsonText, options); - } - else if (elementType == typeof(ButtonElement).Name) - { - res = JsonSerializer.Deserialize>(jsonText, options); + targetType = wrapperType.MakeGenericType(genericType); } } } } + if (targetType != null) + { + res = JsonSerializer.Deserialize(jsonText, targetType, options) as IRichMessage; + } + return res; } public static ITemplateMessage? ParseTemplateMessage(JsonElement root, JsonSerializerOptions options) { ITemplateMessage? res = null; + Type? targetType = null; JsonElement element; var jsonText = root.GetRawText(); @@ -69,37 +74,41 @@ public static class MessageParser var templateType = element.GetString(); if (templateType == TemplateTypeEnum.Button) { - res = JsonSerializer.Deserialize(jsonText, options); + targetType = typeof(ButtonTemplateMessage); } else if (templateType == TemplateTypeEnum.MultiSelect) { - res = JsonSerializer.Deserialize(jsonText, options); + targetType = typeof(MultiSelectTemplateMessage); } else if (templateType == TemplateTypeEnum.Coupon) { - res = JsonSerializer.Deserialize(jsonText, options); + targetType = typeof(CouponTemplateMessage); } else if (templateType == TemplateTypeEnum.Product) { - res = JsonSerializer.Deserialize(jsonText, options); + targetType = typeof(ProductTemplateMessage); } else if (templateType == TemplateTypeEnum.Generic) { if (root.TryGetProperty("element_type", out element)) { var elementType = element.GetString(); - if (elementType == typeof(GenericElement).Name) + var wrapperType = typeof(GenericTemplateMessage<>); + var genericType = Assembly.GetExecutingAssembly().GetTypes().FirstOrDefault(x => x.Name == elementType); + + if (wrapperType != null && genericType != null) { - res = JsonSerializer.Deserialize>(jsonText, options); - } - else if (elementType == typeof(ButtonElement).Name) - { - res = JsonSerializer.Deserialize>(jsonText, options); + targetType = wrapperType.MakeGenericType(genericType); } } } } + if (targetType != null) + { + res = JsonSerializer.Deserialize(jsonText, targetType, options) as ITemplateMessage; + } + return res; } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Messaging/Enums/EditorTypeEnum.cs b/src/Infrastructure/BotSharp.Abstraction/Messaging/Enums/EditorTypeEnum.cs index f190e5e5..9516bb13 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Messaging/Enums/EditorTypeEnum.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Messaging/Enums/EditorTypeEnum.cs @@ -12,6 +12,7 @@ public static class EditorTypeEnum public const string DateTimePicker = "datetime-picker"; public const string DateTimeRangePicker = "datetime-range-picker"; public const string Email = "email"; + public const string File = "file"; /// /// Regex, set the expression in editor_attributes diff --git a/src/Infrastructure/BotSharp.Abstraction/Messaging/IRichMessage.cs b/src/Infrastructure/BotSharp.Abstraction/Messaging/IRichMessage.cs index e04692e6..605fc3f7 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Messaging/IRichMessage.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Messaging/IRichMessage.cs @@ -1,5 +1,3 @@ -using BotSharp.Abstraction.Messaging.Enums; - namespace BotSharp.Abstraction.Messaging; public interface IRichMessage diff --git a/src/Infrastructure/BotSharp.Abstraction/Messaging/JsonConverters/RichContentJsonConverter .cs b/src/Infrastructure/BotSharp.Abstraction/Messaging/JsonConverters/RichContentJsonConverter .cs index 502b0785..bc456aca 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Messaging/JsonConverters/RichContentJsonConverter .cs +++ b/src/Infrastructure/BotSharp.Abstraction/Messaging/JsonConverters/RichContentJsonConverter .cs @@ -1,4 +1,3 @@ -using BotSharp.Core.Messaging; using System.Text.Json; namespace BotSharp.Abstraction.Messaging.JsonConverters; @@ -9,8 +8,7 @@ public class RichContentJsonConverter : JsonConverter { using var jsonDoc = JsonDocument.ParseValue(ref reader); var root = jsonDoc.RootElement; - var jsonText = root.GetRawText(); - var res = MessageParser.ParseRichMessage(root, options); + var res = BotSharpMessageParser.ParseRichMessage(root, options); return res; } diff --git a/src/Infrastructure/BotSharp.Abstraction/Messaging/JsonConverters/TemplateMessageJsonConverter.cs b/src/Infrastructure/BotSharp.Abstraction/Messaging/JsonConverters/TemplateMessageJsonConverter.cs index f0f14730..69fb6c28 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Messaging/JsonConverters/TemplateMessageJsonConverter.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Messaging/JsonConverters/TemplateMessageJsonConverter.cs @@ -1,4 +1,3 @@ -using BotSharp.Core.Messaging; using System.Text.Json; namespace BotSharp.Abstraction.Messaging.JsonConverters; @@ -9,8 +8,7 @@ public class TemplateMessageJsonConverter : JsonConverter { using var jsonDoc = JsonDocument.ParseValue(ref reader); var root = jsonDoc.RootElement; - var jsonText = root.GetRawText(); - var res = MessageParser.ParseTemplateMessage(root, options); + var res = BotSharpMessageParser.ParseTemplateMessage(root, options); return res; } diff --git a/src/Infrastructure/BotSharp.Abstraction/Messaging/Models/RichContent/ElementButton.cs b/src/Infrastructure/BotSharp.Abstraction/Messaging/Models/RichContent/ElementButton.cs index b3642194..daf3de65 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Messaging/Models/RichContent/ElementButton.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Messaging/Models/RichContent/ElementButton.cs @@ -5,13 +5,30 @@ namespace BotSharp.Abstraction.Messaging.Models.RichContent; /// public class ElementButton { - public string Type { get; set; } + public string Type { get; set; } = "web_url"; [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string Url { get; set; } + public string? Url { get; set; } - public string Title { get; set; } + [Translate] + public string Title { get; set; } = string.Empty; + + [JsonPropertyName("description")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [Translate] + public string? Description { get; set; } [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string Payload { get; set; } + public string? Payload { get; set; } + + [JsonPropertyName("is_primary")] + public bool IsPrimary { get; set; } + + [JsonPropertyName("is_secondary")] + public bool IsSecondary { get; set; } + + [JsonPropertyName("post_action_disclaimer")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [Translate] + public string? PostActionDisclaimer { get; set; } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Messaging/Models/RichContent/QuickReplyMessage.cs b/src/Infrastructure/BotSharp.Abstraction/Messaging/Models/RichContent/QuickReplyMessage.cs index efb177f0..c81fe73e 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Messaging/Models/RichContent/QuickReplyMessage.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Messaging/Models/RichContent/QuickReplyMessage.cs @@ -1,5 +1,3 @@ -using BotSharp.Abstraction.Messaging.Enums; - namespace BotSharp.Abstraction.Messaging.Models.RichContent; public class QuickReplyMessage : IRichMessage diff --git a/src/Infrastructure/BotSharp.Abstraction/Messaging/Models/RichContent/RichContent.cs b/src/Infrastructure/BotSharp.Abstraction/Messaging/Models/RichContent/RichContent.cs index c8c3b3fe..1f812bf8 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Messaging/Models/RichContent/RichContent.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Messaging/Models/RichContent/RichContent.cs @@ -1,5 +1,3 @@ -using BotSharp.Abstraction.Messaging.Enums; - namespace BotSharp.Abstraction.Messaging.Models.RichContent; public class RichContent where T : IRichMessage diff --git a/src/Infrastructure/BotSharp.Abstraction/Messaging/Models/RichContent/Template/ButtonTemplateMessage.cs b/src/Infrastructure/BotSharp.Abstraction/Messaging/Models/RichContent/Template/ButtonTemplateMessage.cs index 4c92a3fb..0d8470d7 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Messaging/Models/RichContent/Template/ButtonTemplateMessage.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Messaging/Models/RichContent/Template/ButtonTemplateMessage.cs @@ -1,5 +1,3 @@ -using BotSharp.Abstraction.Messaging.Enums; - namespace BotSharp.Abstraction.Messaging.Models.RichContent.Template; /// @@ -11,33 +9,15 @@ public class ButtonTemplateMessage : IRichMessage, ITemplateMessage public string RichType => RichTypeEnum.ButtonTemplate; [JsonPropertyName("text")] + [Translate] public string Text { get; set; } = string.Empty; [JsonPropertyName("template_type")] public string TemplateType => TemplateTypeEnum.Button; [JsonPropertyName("buttons")] - public ButtonElement[] Buttons { get; set; } = new ButtonElement[0]; + public ElementButton[] Buttons { get; set; } = new ElementButton[0]; [JsonPropertyName("is_horizontal")] public bool IsHorizontal { get; set; } } - -public class ButtonElement -{ - /// - /// web_url, postback, phone_number - /// - public string Type { get; set; } = "web_url"; - - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? Url { get; set; } - - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? Payload { get; set; } - - public string Title { get; set; } = string.Empty; - - [JsonPropertyName("is_primary")] - public bool IsPrimary { get; set; } -} diff --git a/src/Infrastructure/BotSharp.Abstraction/Messaging/Models/RichContent/Template/CouponTemplateMessage.cs b/src/Infrastructure/BotSharp.Abstraction/Messaging/Models/RichContent/Template/CouponTemplateMessage.cs index 1ca47b5b..a3bf116d 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Messaging/Models/RichContent/Template/CouponTemplateMessage.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Messaging/Models/RichContent/Template/CouponTemplateMessage.cs @@ -1,5 +1,3 @@ -using BotSharp.Abstraction.Messaging.Enums; - namespace BotSharp.Abstraction.Messaging.Models.RichContent.Template; /// @@ -10,6 +8,7 @@ public class CouponTemplateMessage : IRichMessage, ITemplateMessage { [JsonPropertyName("rich_type")] public string RichType => RichTypeEnum.CouponTemplate; + [JsonPropertyName("text")] public string Text { get; set; } public string Title { get; set; } diff --git a/src/Infrastructure/BotSharp.Abstraction/Messaging/Models/RichContent/Template/GenericTemplateMessage.cs b/src/Infrastructure/BotSharp.Abstraction/Messaging/Models/RichContent/Template/GenericTemplateMessage.cs index c86bca59..f165dbc1 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Messaging/Models/RichContent/Template/GenericTemplateMessage.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Messaging/Models/RichContent/Template/GenericTemplateMessage.cs @@ -1,5 +1,3 @@ -using BotSharp.Abstraction.Messaging.Enums; - namespace BotSharp.Abstraction.Messaging.Models.RichContent.Template; public class GenericTemplateMessage : IRichMessage, ITemplateMessage @@ -8,6 +6,7 @@ public class GenericTemplateMessage : IRichMessage, ITemplateMessage public string RichType => RichTypeEnum.GenericTemplate; [JsonPropertyName("text")] + [Translate] public string Text { get; set; } = string.Empty; [JsonPropertyName("template_type")] @@ -19,16 +18,24 @@ public class GenericTemplateMessage : IRichMessage, ITemplateMessage [JsonPropertyName("is_horizontal")] public bool IsHorizontal { get; set; } + [JsonPropertyName("is_popup")] + public bool IsPopup { get; set; } + [JsonPropertyName("element_type")] public string ElementType => typeof(T).Name; } public class GenericElement { + [Translate] public string Title { get; set; } + + [Translate] public string Subtitle { get; set; } + [JsonPropertyName("image_url")] public string ImageUrl { get; set; } + [JsonPropertyName("default_action")] public ElementAction DefaultAction { get; set; } public ElementButton[] Buttons { get; set; } diff --git a/src/Infrastructure/BotSharp.Abstraction/Messaging/Models/RichContent/Template/MultiSelectTemplateMessage.cs b/src/Infrastructure/BotSharp.Abstraction/Messaging/Models/RichContent/Template/MultiSelectTemplateMessage.cs index 6919a1a4..4ef62fc1 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Messaging/Models/RichContent/Template/MultiSelectTemplateMessage.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Messaging/Models/RichContent/Template/MultiSelectTemplateMessage.cs @@ -1,12 +1,12 @@ -using BotSharp.Abstraction.Messaging.Enums; - namespace BotSharp.Abstraction.Messaging.Models.RichContent.Template; public class MultiSelectTemplateMessage : IRichMessage, ITemplateMessage { [JsonPropertyName("rich_type")] public string RichType => RichTypeEnum.MultiSelectTemplate; + [JsonPropertyName("text")] + [Translate] public string Text { get; set; } = string.Empty; [JsonPropertyName("template_type")] @@ -21,6 +21,7 @@ public class MultiSelectTemplateMessage : IRichMessage, ITemplateMessage public class OptionElement { + [Translate] public string Title { get; set; } = string.Empty; public string Type { get; set; } = string.Empty; public string? Payload { get; set; } diff --git a/src/Infrastructure/BotSharp.Abstraction/Messaging/Models/RichContent/Template/ProductTemplateMessage.cs b/src/Infrastructure/BotSharp.Abstraction/Messaging/Models/RichContent/Template/ProductTemplateMessage.cs index 59a1e911..05af097a 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Messaging/Models/RichContent/Template/ProductTemplateMessage.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Messaging/Models/RichContent/Template/ProductTemplateMessage.cs @@ -1,5 +1,3 @@ -using BotSharp.Abstraction.Messaging.Enums; - namespace BotSharp.Abstraction.Messaging.Models.RichContent.Template; public class ProductTemplateMessage : IRichMessage, ITemplateMessage @@ -8,6 +6,7 @@ public class ProductTemplateMessage : IRichMessage, ITemplateMessage public string RichType => RichTypeEnum.GenericTemplate; [JsonPropertyName("text")] + [Translate] public string Text { get; set; } = string.Empty; [JsonPropertyName("template_type")] diff --git a/src/Infrastructure/BotSharp.Abstraction/Messaging/Models/RichContent/TextMessage.cs b/src/Infrastructure/BotSharp.Abstraction/Messaging/Models/RichContent/TextMessage.cs index 899a9265..3a37ad54 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Messaging/Models/RichContent/TextMessage.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Messaging/Models/RichContent/TextMessage.cs @@ -1,5 +1,3 @@ -using BotSharp.Abstraction.Messaging.Enums; - namespace BotSharp.Abstraction.Messaging.Models.RichContent; public class TextMessage : IRichMessage @@ -7,6 +5,7 @@ public class TextMessage : IRichMessage [JsonPropertyName("rich_type")] public string RichType => RichTypeEnum.Text; + [Translate] public string Text { get; set; } = string.Empty; public TextMessage(string text) diff --git a/src/Infrastructure/BotSharp.Abstraction/Plugins/Models/PluginMenuDef.cs b/src/Infrastructure/BotSharp.Abstraction/Plugins/Models/PluginMenuDef.cs index bd5a1d88..5f8a59b9 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Plugins/Models/PluginMenuDef.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Plugins/Models/PluginMenuDef.cs @@ -19,6 +19,9 @@ public class PluginMenuDef [JsonIgnore] public int Weight { get; set; } + [JsonIgnore] + public List? Roles { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public List? SubMenu { get; set; } diff --git a/src/Infrastructure/BotSharp.Abstraction/Repositories/BotSharpDatabaseSettings.cs b/src/Infrastructure/BotSharp.Abstraction/Repositories/BotSharpDatabaseSettings.cs index 458aa83e..ff077296 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Repositories/BotSharpDatabaseSettings.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Repositories/BotSharpDatabaseSettings.cs @@ -7,6 +7,7 @@ public class BotSharpDatabaseSettings : DatabaseBasicSettings public string BotSharpMongoDb { get; set; } public string TablePrefix { get; set; } public DbConnectionSetting BotSharp { get; set; } + public string Redis { get; set; } } public class DatabaseBasicSettings diff --git a/src/Infrastructure/BotSharp.Abstraction/Repositories/Enums/RepositoryEnum.cs b/src/Infrastructure/BotSharp.Abstraction/Repositories/Enums/RepositoryEnum.cs new file mode 100644 index 00000000..f71845a5 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Repositories/Enums/RepositoryEnum.cs @@ -0,0 +1,7 @@ +namespace BotSharp.Abstraction.Repositories.Enums; + +public static class RepositoryEnum +{ + public const string FileRepository = nameof(FileRepository); + public const string MongoRepository = nameof(MongoRepository); +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs b/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs index 64e92a66..6adcc8ad 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs @@ -1,7 +1,6 @@ using BotSharp.Abstraction.Loggers.Models; using BotSharp.Abstraction.Plugins.Models; using BotSharp.Abstraction.Repositories.Filters; -using BotSharp.Abstraction.Repositories.Models; using BotSharp.Abstraction.Tasks.Models; using BotSharp.Abstraction.Users.Models; @@ -18,10 +17,11 @@ public interface IBotSharpRepository #endregion #region User - User? GetUserByEmail(string email); - User? GetUserById(string id); - User? GetUserByUserName(string userName); - void CreateUser(User user); + User? GetUserByEmail(string email) => throw new NotImplementedException(); + User? GetUserById(string id) => throw new NotImplementedException(); + User? GetUserByUserName(string userName) => throw new NotImplementedException(); + void CreateUser(User user) => throw new NotImplementedException(); + void UpdateUserVerified(string userId) => throw new NotImplementedException(); #endregion #region Agent @@ -32,8 +32,10 @@ public interface IBotSharpRepository void BulkInsertAgents(List agents); void BulkInsertUserAgents(List userAgents); bool DeleteAgents(); + bool DeleteAgent(string agentId); List GetAgentResponses(string agentId, string prefix, string intent); string GetAgentTemplate(string agentId, string templateName); + bool PatchAgentTemplate(string agentId, AgentTemplate template); #endregion #region Agent Task @@ -42,7 +44,7 @@ public interface IBotSharpRepository void InsertAgentTask(AgentTask task); void BulkInsertAgentTasks(List tasks); void UpdateAgentTask(AgentTask task, AgentTaskField field); - bool DeleteAgentTask(string agentId, string taskId); + bool DeleteAgentTask(string agentId, List taskIds); bool DeleteAgentTasks(); #endregion @@ -50,7 +52,6 @@ public interface IBotSharpRepository void CreateNewConversation(Conversation conversation); bool DeleteConversations(IEnumerable conversationIds); List GetConversationDialogs(string conversationId); - void UpdateConversationDialogElements(string conversationId, List updateElements); void AppendConversationDialogs(string conversationId, List dialogs); ConversationState GetConversationStates(string conversationId); void UpdateConversationStates(string conversationId, List states); @@ -58,11 +59,11 @@ public interface IBotSharpRepository Conversation GetConversation(string conversationId); PagedItems GetConversations(ConversationFilter filter); void UpdateConversationTitle(string conversationId, string title); - void UpdateConversationBreakpoint(string conversationId, string messageId, DateTime breakpoint); - DateTime GetConversationBreakpoint(string conversationId); + void UpdateConversationBreakpoint(string conversationId, ConversationBreakpoint breakpoint); + ConversationBreakpoint? GetConversationBreakpoint(string conversationId); List GetLastConversations(); List GetIdleConversations(int batchSize, int messageLimit, int bufferHours); - bool TruncateConversation(string conversationId, string messageId, bool cleanLog = false); + IEnumerable TruncateConversation(string conversationId, string messageId, bool cleanLog = false); #endregion #region Execution Log diff --git a/src/Infrastructure/BotSharp.Abstraction/Routing/IRoutingContext.cs b/src/Infrastructure/BotSharp.Abstraction/Routing/IRoutingContext.cs index 2c0aa15e..3c93976b 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Routing/IRoutingContext.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Routing/IRoutingContext.cs @@ -3,7 +3,8 @@ namespace BotSharp.Abstraction.Routing; public interface IRoutingContext { string GetCurrentAgentId(); - string PreviousAgentId(); + string FirstGoalAgentId(); + bool ContainsAgentId(string agentId); string OriginAgentId { get; } string ConversationId { get; } string MessageId { get; } @@ -13,6 +14,7 @@ public interface IRoutingContext int AgentCount { get; } void Push(string agentId, string? reason = null); void Pop(string? reason = null); + void PopTo(string agentId, string reason); void Replace(string agentId, string? reason = null); void Empty(string? reason = null); } diff --git a/src/Infrastructure/BotSharp.Abstraction/Routing/IRoutingHandler.cs b/src/Infrastructure/BotSharp.Abstraction/Routing/IRoutingHandler.cs index 95cddf2e..e82ea706 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Routing/IRoutingHandler.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Routing/IRoutingHandler.cs @@ -16,5 +16,5 @@ public interface IRoutingHandler void SetDialogs(List dialogs); - Task Handle(IRoutingService routing, FunctionCallFromLlm inst, RoleDialogModel message); + Task Handle(IRoutingService routing, FunctionCallFromLlm inst, RoleDialogModel message, Func onFunctionExecuting); } diff --git a/src/Infrastructure/BotSharp.Abstraction/Routing/IRoutingService.cs b/src/Infrastructure/BotSharp.Abstraction/Routing/IRoutingService.cs index da84ffa3..5a12c0ed 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Routing/IRoutingService.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Routing/IRoutingService.cs @@ -1,5 +1,3 @@ -using BotSharp.Abstraction.Routing.Models; - namespace BotSharp.Abstraction.Routing; public interface IRoutingService @@ -30,9 +28,9 @@ public interface IRoutingService List GetHandlers(Agent router); void ResetRecursiveCounter(); - Task InvokeAgent(string agentId, List dialogs); - Task InvokeFunction(string name, RoleDialogModel message); - Task InstructLoop(RoleDialogModel message, List dialogs); + Task InvokeAgent(string agentId, List dialogs, Func onFunctionExecuting); + Task InvokeFunction(string name, RoleDialogModel messages, Func? onFunctionExecuting = null); + Task InstructLoop(RoleDialogModel message, List dialogs, Func onFunctionExecuting); /// /// Talk to a specific Agent directly, bypassing the Router @@ -44,5 +42,5 @@ public interface IRoutingService Task GetConversationContent(List dialogs, int maxDialogCount = 50); - bool HasMissingRequiredField(RoleDialogModel message, out string agentId); + (bool, string) HasMissingRequiredField(RoleDialogModel message, out string agentId); } diff --git a/src/Infrastructure/BotSharp.Abstraction/Routing/Models/RoutingArgs.cs b/src/Infrastructure/BotSharp.Abstraction/Routing/Models/RoutingArgs.cs index 82de1a81..dad45cb9 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Routing/Models/RoutingArgs.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Routing/Models/RoutingArgs.cs @@ -3,7 +3,7 @@ namespace BotSharp.Abstraction.Routing.Models; public class RoutingArgs { [JsonPropertyName("function")] - public string Function { get; set; } + public string Function { get; set; } = string.Empty; /// /// The reason why you select this function or agent @@ -19,30 +19,36 @@ public class RoutingArgs [JsonPropertyName("conversation_end")] public bool ConversationEnd { get; set; } + [JsonPropertyName("task_completed")] + public bool TaskCompleted { get; set; } + + [JsonPropertyName("is_new_task")] + public bool IsNewTask { get; set; } + /// /// The content of replying to user /// [JsonPropertyName("response")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string Response { get; set; } + public string Response { get; set; } = string.Empty; /// /// Agent for next action based on user latest response /// [JsonPropertyName("next_action_agent")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string AgentName { get; set; } + public string AgentName { get; set; } = string.Empty; /// /// Agent who can achieve user original goal /// [JsonPropertyName("user_goal_agent")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string OriginalAgent { get; set; } + public string OriginalAgent { get; set; } = string.Empty; [JsonPropertyName("user_goal_description")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string UserGoal { get; set; } + public string UserGoal { get; set; } = string.Empty; public override string ToString() { diff --git a/src/Infrastructure/BotSharp.Abstraction/Routing/Planning/IExecutor.cs b/src/Infrastructure/BotSharp.Abstraction/Routing/Planning/IExecutor.cs index c8bffe6c..14c02034 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Routing/Planning/IExecutor.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Routing/Planning/IExecutor.cs @@ -7,5 +7,6 @@ public interface IExecutor Task Execute(IRoutingService routing, FunctionCallFromLlm inst, RoleDialogModel message, - List dialogs); + List dialogs, + Func onFunctionExecuting); } diff --git a/src/Infrastructure/BotSharp.Abstraction/Translation/Attributes/TranslateAttribute.cs b/src/Infrastructure/BotSharp.Abstraction/Translation/Attributes/TranslateAttribute.cs new file mode 100644 index 00000000..1c39c415 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Translation/Attributes/TranslateAttribute.cs @@ -0,0 +1,10 @@ +namespace BotSharp.Abstraction.Translation.Attributes; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, Inherited = false)] +public class TranslateAttribute : Attribute +{ + public TranslateAttribute() + { + + } +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Translation/ITranslationService.cs b/src/Infrastructure/BotSharp.Abstraction/Translation/ITranslationService.cs new file mode 100644 index 00000000..e69e35b1 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Translation/ITranslationService.cs @@ -0,0 +1,6 @@ +namespace BotSharp.Abstraction.Translation; + +public interface ITranslationService +{ + Task Translate(Agent router, string messageId, T data, string language = "Spanish", bool clone = true) where T : class; +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Translation/Models/TranslationInput.cs b/src/Infrastructure/BotSharp.Abstraction/Translation/Models/TranslationInput.cs new file mode 100644 index 00000000..6897ca42 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Translation/Models/TranslationInput.cs @@ -0,0 +1,10 @@ +namespace BotSharp.Abstraction.Translation.Models; + +public class TranslationInput +{ + [JsonPropertyName("id")] + public int Id { get; set; } = -1; + + [JsonPropertyName("text")] + public string Text { get; set; } = null!; +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Translation/Models/TranslationOutput.cs b/src/Infrastructure/BotSharp.Abstraction/Translation/Models/TranslationOutput.cs new file mode 100644 index 00000000..b15bfef4 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Translation/Models/TranslationOutput.cs @@ -0,0 +1,13 @@ +namespace BotSharp.Abstraction.Translation.Models; + +public class TranslationOutput +{ + [JsonPropertyName("input_lang")] + public string InputLanguage { get; set; } = null!; + + [JsonPropertyName("output_lang")] + public string OutputLanguage { get; set; } = LanguageType.ENGLISH; + + [JsonPropertyName("texts")] + public TranslationInput[] Texts { get; set; } = Array.Empty(); +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Users/IAuthenticationHook.cs b/src/Infrastructure/BotSharp.Abstraction/Users/IAuthenticationHook.cs index 33b8086f..0de10328 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Users/IAuthenticationHook.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Users/IAuthenticationHook.cs @@ -8,4 +8,5 @@ public interface IAuthenticationHook Task Authenticate(string id, string password); void AddClaims(List claims); void BeforeSending(Token token); + Task UserCreated(User user); } diff --git a/src/Infrastructure/BotSharp.Abstraction/Users/IUserIdentity.cs b/src/Infrastructure/BotSharp.Abstraction/Users/IUserIdentity.cs index a0998117..4fecb48c 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Users/IUserIdentity.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Users/IUserIdentity.cs @@ -8,4 +8,5 @@ public interface IUserIdentity string FirstName { get; } string LastName { get; } string FullName { get; } + string? UserLanguage { get; } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Users/IUserService.cs b/src/Infrastructure/BotSharp.Abstraction/Users/IUserService.cs index 35d74aa4..722917cc 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Users/IUserService.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Users/IUserService.cs @@ -1,4 +1,5 @@ using BotSharp.Abstraction.Users.Models; +using BotSharp.OpenAPI.ViewModels.Users; namespace BotSharp.Abstraction.Users; @@ -6,6 +7,9 @@ public interface IUserService { Task GetUser(string id); Task CreateUser(User user); - Task GetToken(string authorization); + Task ActiveUser(UserActivationModel model); + Task GetToken(string authorization); Task GetMyProfile(); + Task VerifyUserNameExisting(string userName); + Task VerifyEmailExisting(string email); } \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Abstraction/Users/Models/User.cs b/src/Infrastructure/BotSharp.Abstraction/Users/Models/User.cs index 7ca441fe..4e6ca265 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Users/Models/User.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Users/Models/User.cs @@ -14,6 +14,8 @@ public class User public string Source { get; set; } = "internal"; public string? ExternalId { get; set; } public string Role { get; set; } = UserRole.Client; + public string? VerificationCode { get; set; } + public bool Verified { get; set; } public DateTime UpdatedTime { get; set; } = DateTime.UtcNow; public DateTime CreatedTime { get; set; } = DateTime.UtcNow; } diff --git a/src/Infrastructure/BotSharp.Abstraction/Users/Models/UserActivationModel.cs b/src/Infrastructure/BotSharp.Abstraction/Users/Models/UserActivationModel.cs new file mode 100644 index 00000000..904bd936 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Users/Models/UserActivationModel.cs @@ -0,0 +1,7 @@ +namespace BotSharp.OpenAPI.ViewModels.Users; + +public class UserActivationModel +{ + public string UserName { get; set; } + public string VerificationCode { get; set; } +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Users/Settings/AccountSetting.cs b/src/Infrastructure/BotSharp.Abstraction/Users/Settings/AccountSetting.cs new file mode 100644 index 00000000..06176ce6 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Users/Settings/AccountSetting.cs @@ -0,0 +1,9 @@ +namespace BotSharp.Abstraction.Users.Settings; + +public class AccountSetting +{ + /// + /// Whether to enable verification code to verify the authenticity of new users + /// + public bool NewUserVerification { get; set; } +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Using.cs b/src/Infrastructure/BotSharp.Abstraction/Using.cs index f9b708dc..a5bbc188 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Using.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Using.cs @@ -14,3 +14,6 @@ global using BotSharp.Abstraction.Models; global using BotSharp.Abstraction.Routing.Models; global using BotSharp.Abstraction.Routing.Planning; global using BotSharp.Abstraction.Templating; +global using BotSharp.Abstraction.Translation.Attributes; +global using BotSharp.Abstraction.Messaging.Enums; +global using BotSharp.Abstraction.Files.Models; \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Abstraction/Utilities/Pagination.cs b/src/Infrastructure/BotSharp.Abstraction/Utilities/Pagination.cs index d11752f6..9ada5f63 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Utilities/Pagination.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Utilities/Pagination.cs @@ -13,19 +13,26 @@ public class Pagination public int Size { - get + get { - if (_size <= 0) return 20; - if (_size > 100) return 100; - - return _size; - } - set + return _size > 0 ? _size : 1; + } + set { _size = value; - } + } } + /// + /// Sort by field + /// + public string? Sort { get; set; } + + /// + /// Sort order: asc or desc + /// + public string Order { get; set; } = "asc"; + public int Offset { get { return (Page - 1) * Size; } diff --git a/src/Infrastructure/BotSharp.Core/Agents/AgentPlugin.cs b/src/Infrastructure/BotSharp.Core/Agents/AgentPlugin.cs index 7df5fa02..83ce823c 100644 --- a/src/Infrastructure/BotSharp.Core/Agents/AgentPlugin.cs +++ b/src/Infrastructure/BotSharp.Core/Agents/AgentPlugin.cs @@ -1,6 +1,7 @@ using BotSharp.Abstraction.MLTasks; using BotSharp.Abstraction.Plugins.Models; using BotSharp.Abstraction.Settings; +using BotSharp.Abstraction.Users.Enums; using Microsoft.Extensions.Configuration; namespace BotSharp.Core.Agents; @@ -14,6 +15,13 @@ public class AgentPlugin : IBotSharpPlugin public SettingsMeta Settings => new SettingsMeta("Agent"); + public string[] AgentIds => new string[] + { + "01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a", + "01e2fc5c-2c89-4ec7-8470-7688608b496c", + "01dcc3e5-0af7-49e6-ad7a-a760bd12dc4b" + }; + public object GetNewSettingsInstance() => new AgentSettings(); @@ -36,8 +44,8 @@ public class AgentPlugin : IBotSharpPlugin { SubMenu = new List { - new PluginMenuDef("Routing", link: "page/agent/router"), // icon: "bx bx-map-pin" - new PluginMenuDef("Evaluating", link: "page/agent/evaluator"), // icon: "bx bx-task" + new PluginMenuDef("Routing", link: "page/agent/router") { Roles = new List { UserRole.Admin } }, // icon: "bx bx-map-pin" + new PluginMenuDef("Evaluating", link: "page/agent/evaluator") { Roles = new List { UserRole.Admin } }, // icon: "bx bx-task" new PluginMenuDef("Agents", link: "page/agent"), // icon: "bx bx-bot" } }); diff --git a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.CreateAgent.cs b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.CreateAgent.cs index ce6512af..ae429484 100644 --- a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.CreateAgent.cs +++ b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.CreateAgent.cs @@ -1,8 +1,4 @@ -using BotSharp.Abstraction.Agents.Models; -using BotSharp.Abstraction.Functions.Models; -using BotSharp.Abstraction.Repositories; using BotSharp.Abstraction.Tasks.Models; -using BotSharp.Abstraction.Users.Models; using System.IO; using System.Text.RegularExpressions; @@ -26,32 +22,13 @@ public partial class AgentService var dbSettings = _services.GetRequiredService(); var agentSettings = _services.GetRequiredService(); - var filePath = Path.Combine(dbSettings.FileRepository, agentSettings.DataDir); - var foundAgent = FetchAgentFileByName(agent.Name, filePath); - - if (foundAgent != null) - { - agentRecord.SetId(foundAgent.Id) - .SetName(foundAgent.Name) - .SetDescription(foundAgent.Description) - .SetIsPublic(foundAgent.IsPublic) - .SetDisabled(foundAgent.Disabled) - .SetAgentType(foundAgent.Type) - .SetProfiles(foundAgent.Profiles) - .SetRoutingRules(foundAgent.RoutingRules) - .SetInstruction(foundAgent.Instruction) - .SetTemplates(foundAgent.Templates) - .SetFunctions(foundAgent.Functions) - .SetResponses(foundAgent.Responses) - .SetLlmConfig(foundAgent.LlmConfig); - } var user = _db.GetUserById(_user.Id); var userAgentRecord = new UserAgent { Id = Guid.NewGuid().ToString(), UserId = user.Id, - AgentId = foundAgent?.Id ?? agentRecord.Id, + AgentId = agentRecord.Id, Editable = false, CreatedTime = DateTime.UtcNow, UpdatedTime = DateTime.UtcNow @@ -65,7 +42,7 @@ public partial class AgentService Utilities.ClearCache(); - return agentRecord; + return await Task.FromResult(agentRecord); } private Agent FetchAgentFileByName(string agentName, string filePath) diff --git a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.DeleteAgent.cs b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.DeleteAgent.cs index 23111fd2..1fe5c6e1 100644 --- a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.DeleteAgent.cs +++ b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.DeleteAgent.cs @@ -1,9 +1,20 @@ +using BotSharp.Abstraction.Users.Enums; + namespace BotSharp.Core.Agents.Services; public partial class AgentService { public async Task DeleteAgent(string id) { - throw new NotImplementedException(); + var user = _db.GetUserById(_user.Id); + var agent = _db.GetAgentsByUser(_user.Id).FirstOrDefault(x => x.Id.IsEqualTo(id)); + + if (user?.Role != UserRole.Admin && agent == null) + { + return false; + } + + var deleted = _db.DeleteAgent(id); + return await Task.FromResult(deleted); } } diff --git a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.RefreshAgents.cs b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.RefreshAgents.cs index 9c3b34f9..0b61977e 100644 --- a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.RefreshAgents.cs +++ b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.RefreshAgents.cs @@ -1,55 +1,89 @@ -using BotSharp.Abstraction.Tasks.Models; +using BotSharp.Abstraction.Repositories.Enums; using System.IO; namespace BotSharp.Core.Agents.Services; public partial class AgentService { - public async Task RefreshAgents() + public async Task RefreshAgents() { - var isAgentDeleted = _db.DeleteAgents(); - var isTaskDeleted = _db.DeleteAgentTasks(); - if (!isAgentDeleted) return; - + string refreshResult; var dbSettings = _services.GetRequiredService(); + if (dbSettings.Default == RepositoryEnum.FileRepository) + { + refreshResult = $"Invalid database repository setting: {dbSettings.Default}"; + _logger.LogWarning(refreshResult); + return refreshResult; + } + var agentDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, dbSettings.FileRepository, _agentSettings.DataDir); + if (!Directory.Exists(agentDir)) + { + refreshResult = $"Cannot find the directory: {agentDir}"; + return refreshResult; + } + var user = _db.GetUserById(_user.Id); - var agents = new List(); - var userAgents = new List(); - var agentTasks = new List(); + var refreshedAgents = new List(); foreach (var dir in Directory.GetDirectories(agentDir)) { - var agentJson = File.ReadAllText(Path.Combine(dir, "agent.json")); - var agent = JsonSerializer.Deserialize(agentJson, _options); - if (agent == null) continue; + try + { + var agentJson = File.ReadAllText(Path.Combine(dir, "agent.json")); + var agent = JsonSerializer.Deserialize(agentJson, _options); + + if (agent == null) + { + _logger.LogError($"Cannot find agent in file directory: {dir}"); + continue; + } - var functions = FetchFunctionsFromFile(dir); - var instruction = FetchInstructionFromFile(dir); - var responses = FetchResponsesFromFile(dir); - var templates = FetchTemplatesFromFile(dir); - var samples = FetchSamplesFromFile(dir); - agent.SetInstruction(instruction) - .SetTemplates(templates) - .SetFunctions(functions) - .SetResponses(responses) - .SetSamples(samples); - agents.Add(agent); + var functions = FetchFunctionsFromFile(dir); + var instruction = FetchInstructionFromFile(dir); + var responses = FetchResponsesFromFile(dir); + var templates = FetchTemplatesFromFile(dir); + var samples = FetchSamplesFromFile(dir); + agent.SetInstruction(instruction) + .SetTemplates(templates) + .SetFunctions(functions) + .SetResponses(responses) + .SetSamples(samples); - var userAgent = BuildUserAgent(agent.Id, user.Id); - userAgents.Add(userAgent); + var userAgent = BuildUserAgent(agent.Id, user.Id); + var tasks = FetchTasksFromFile(dir); - var tasks = FetchTasksFromFile(dir); - agentTasks.AddRange(tasks); + var isAgentDeleted = _db.DeleteAgent(agent.Id); + if (isAgentDeleted) + { + await Task.Delay(100); + _db.BulkInsertAgents(new List { agent }); + _db.BulkInsertUserAgents(new List { userAgent }); + _db.BulkInsertAgentTasks(tasks); + refreshedAgents.Add(agent.Name); + _logger.LogInformation($"Agent {agent.Name} has been migrated."); + } + } + catch (Exception ex) + { + _logger.LogError($"Failed to migrate agent in file directory: {dir}\r\nError: {ex.Message}"); + } } - _db.BulkInsertAgents(agents); - _db.BulkInsertUserAgents(userAgents); - _db.BulkInsertAgentTasks(agentTasks); + if (!refreshedAgents.IsNullOrEmpty()) + { + Utilities.ClearCache(); + refreshResult = $"Agents are migrated!\r\n{string.Join("\r\n", refreshedAgents)}"; + } + else + { + refreshResult = "No agent gets refreshed!"; + } - Utilities.ClearCache(); + _logger.LogInformation(refreshResult); + return refreshResult; } } diff --git a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.Rendering.cs b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.Rendering.cs index 9ce7a7a8..a0b2f130 100644 --- a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.Rendering.cs +++ b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.Rendering.cs @@ -1,5 +1,6 @@ using BotSharp.Abstraction.Loggers; using BotSharp.Abstraction.Templating; +using Newtonsoft.Json.Linq; namespace BotSharp.Core.Agents.Services; @@ -32,6 +33,64 @@ public partial class AgentService return true; } + public FunctionParametersDef? RenderFunctionProperty(Agent agent, FunctionDef def) + { + var parameterDef = def?.Parameters; + var propertyDef = parameterDef?.Properties; + if (propertyDef == null) return null; + + var visibleExpress = "visibility_expression"; + var root = propertyDef.RootElement; + var iterator = root.EnumerateObject(); + var visibleProps = new List(); + while (iterator.MoveNext()) + { + var prop = iterator.Current; + var name = prop.Name; + var node = prop.Value; + var matched = true; + if (node.TryGetProperty(visibleExpress, out var element)) + { + var expression = element.GetString(); + var render = _services.GetRequiredService(); + var result = render.Render(expression, new Dictionary + { + { "states", agent.TemplateDict } + }); + matched = result == "visible"; + } + + if (matched) + { + visibleProps.Add(name); + } + } + + var rootObject = JObject.Parse(root.GetRawText()); + var clonedRoot = rootObject.DeepClone() as JObject; + var required = parameterDef?.Required ?? new List(); + foreach (var property in rootObject.Properties()) + { + if (visibleProps.Contains(property.Name)) + { + var value = clonedRoot.GetValue(property.Name) as JObject; + if (value != null && value.ContainsKey(visibleExpress)) + { + value.Remove(visibleExpress); + } + } + else + { + clonedRoot.Remove(property.Name); + required.Remove(property.Name); + } + } + + parameterDef.Properties = JsonSerializer.Deserialize(clonedRoot.ToString()); + parameterDef.Required = required; + return parameterDef; ; + } + public string RenderedTemplate(Agent agent, string templateName) { // render liquid template diff --git a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.UpdateAgent.cs b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.UpdateAgent.cs index 4577af6d..af6cb65e 100644 --- a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.UpdateAgent.cs +++ b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.UpdateAgent.cs @@ -1,7 +1,6 @@ -using BotSharp.Abstraction.Agents.Models; -using BotSharp.Abstraction.Functions.Models; -using BotSharp.Abstraction.Repositories; +using BotSharp.Abstraction.Repositories.Enums; using BotSharp.Abstraction.Routing.Models; +using BotSharp.Abstraction.Users.Enums; using System.IO; namespace BotSharp.Core.Agents.Services; @@ -10,6 +9,10 @@ public partial class AgentService { public async Task UpdateAgent(Agent agent, AgentField updateField) { + var userService = _services.GetRequiredService(); + var user = await userService.GetUser(_user.Id); + if (user?.Role != UserRole.Admin) return; + if (agent == null || string.IsNullOrEmpty(agent.Id)) return; var record = _db.GetAgent(agent.Id); @@ -39,21 +42,41 @@ public partial class AgentService await Task.CompletedTask; } - public async Task UpdateAgentFromFile(string id) + public async Task UpdateAgentFromFile(string id) { - var agent = _db.GetAgent(id); - - if (agent == null) return; - + string updateResult; var dbSettings = _services.GetRequiredService(); var agentSettings = _services.GetRequiredService(); + + if (dbSettings.Default == RepositoryEnum.FileRepository) + { + updateResult = $"Invalid database repository setting: {dbSettings.Default}"; + _logger.LogWarning(updateResult); + return updateResult; + } + + var agent = _db.GetAgent(id); + if (agent == null) + { + updateResult = $"Cannot find agent ${id}"; + _logger.LogError(updateResult); + return updateResult; + } + var filePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, dbSettings.FileRepository, agentSettings.DataDir); var clonedAgent = Agent.Clone(agent); var foundAgent = FetchAgentFileById(agent.Id, filePath); - if (foundAgent != null) + if (foundAgent == null) + { + updateResult = $"Cannot find agent {agent.Name} in file directory: {filePath}"; + _logger.LogError(updateResult); + return updateResult; + } + + try { clonedAgent.SetId(foundAgent.Id) .SetName(foundAgent.Name) @@ -71,15 +94,77 @@ public partial class AgentService .SetLlmConfig(foundAgent.LlmConfig); _db.UpdateAgent(clonedAgent, AgentField.All); - Utilities.ClearCache(); - } - await Task.CompletedTask; + updateResult = $"Agent {agent.Name} has been migrated!"; + _logger.LogInformation(updateResult); + return updateResult; + } + catch (Exception ex) + { + updateResult = $"Failed to migrate agent {agent.Name} in file directory {filePath}.\r\nError: {ex.Message}"; + _logger.LogError(updateResult); + return updateResult; + } } - private Agent FetchAgentFileById(string agentId, string filePath) + + public async Task PatchAgentTemplate(Agent agent) { + var patchResult = string.Empty; + if (agent == null || agent.Templates.IsNullOrEmpty()) + { + patchResult = $"Null agent instance or empty input templates"; + _logger.LogWarning(patchResult); + return patchResult; + } + + var record = _db.GetAgent(agent.Id); + if (record == null) + { + patchResult = $"Cannot find agent {agent.Id}"; + _logger.LogWarning(patchResult); + return patchResult; + } + + var successTemplates = new List(); + var failTemplates = new List(); + foreach (var template in agent.Templates) + { + if (template == null) continue; + + var result = _db.PatchAgentTemplate(agent.Id, template); + if (result) + { + successTemplates.Add(template.Name); + _logger.LogInformation($"Template {template.Name} is updated successfully!"); + } + else + { + failTemplates.Add(template.Name); + _logger.LogWarning($"Template {template.Name} is failed to be updated!"); + } + } + + Utilities.ClearCache(); + + if (!successTemplates.IsNullOrEmpty()) + { + patchResult += $"Success templates:\n{string.Join('\n', successTemplates)}\n\n"; + } + + if (!failTemplates.IsNullOrEmpty()) + { + patchResult += $"Failed templates:\n{string.Join('\n', failTemplates)}"; + } + + return patchResult; + } + + private Agent? FetchAgentFileById(string agentId, string filePath) + { + if (!Directory.Exists(filePath)) return null; + foreach (var dir in Directory.GetDirectories(filePath)) { var agentJson = File.ReadAllText(Path.Combine(dir, "agent.json")); diff --git a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.cs b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.cs index bd009db0..da1b8cf1 100644 --- a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.cs +++ b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.cs @@ -47,4 +47,10 @@ public partial class AgentService : IAgentService } return dir; } + + public List GetAgentsByUser(string userId) + { + var agents = _db.GetAgentsByUser(userId); + return agents; + } } diff --git a/src/Infrastructure/BotSharp.Core/BotSharp.Core.csproj b/src/Infrastructure/BotSharp.Core/BotSharp.Core.csproj index da47ccb5..10e21b3c 100644 --- a/src/Infrastructure/BotSharp.Core/BotSharp.Core.csproj +++ b/src/Infrastructure/BotSharp.Core/BotSharp.Core.csproj @@ -46,11 +46,17 @@ + + + + + + @@ -59,6 +65,7 @@ + @@ -67,6 +74,21 @@ + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + PreserveNewest @@ -106,6 +128,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest @@ -118,6 +143,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest @@ -133,9 +161,11 @@ - - + + + + diff --git a/src/Infrastructure/BotSharp.Core/BotSharpCoreExtensions.cs b/src/Infrastructure/BotSharp.Core/BotSharpCoreExtensions.cs index 2b56281c..2dd2dc7f 100644 --- a/src/Infrastructure/BotSharp.Core/BotSharpCoreExtensions.cs +++ b/src/Infrastructure/BotSharp.Core/BotSharpCoreExtensions.cs @@ -5,6 +5,7 @@ using BotSharp.Core.Plugins; using BotSharp.Abstraction.Settings; using BotSharp.Abstraction.Options; using BotSharp.Abstraction.Messaging.JsonConverters; +using BotSharp.Abstraction.Users.Settings; namespace BotSharp.Core; @@ -14,9 +15,11 @@ public static class BotSharpCoreExtensions { services.AddScoped(); services.AddScoped(); + services.AddSingleton(); RegisterPlugins(services, config); ConfigureBotSharpOptions(services, configOptions); + return services; } @@ -82,6 +85,10 @@ public static class BotSharpCoreExtensions return settingService.Bind("PluginLoader"); }); + var accountSettings = new AccountSetting(); + config.Bind("Account", accountSettings); + services.AddScoped(x => accountSettings); + var loader = new PluginLoader(services, config, pluginSettings); loader.Load(assembly => { diff --git a/src/Infrastructure/BotSharp.Core/Conversations/ConversationPlugin.cs b/src/Infrastructure/BotSharp.Core/Conversations/ConversationPlugin.cs index 5c215884..90eb3298 100644 --- a/src/Infrastructure/BotSharp.Core/Conversations/ConversationPlugin.cs +++ b/src/Infrastructure/BotSharp.Core/Conversations/ConversationPlugin.cs @@ -1,13 +1,17 @@ +using BotSharp.Abstraction.Files; +using BotSharp.Abstraction.Google.Settings; using BotSharp.Abstraction.Instructs; using BotSharp.Abstraction.Messaging; using BotSharp.Abstraction.Plugins.Models; using BotSharp.Abstraction.Routing.Planning; using BotSharp.Abstraction.Settings; using BotSharp.Abstraction.Templating; +using BotSharp.Core.Files; using BotSharp.Core.Instructs; using BotSharp.Core.Messaging; using BotSharp.Core.Routing.Planning; using BotSharp.Core.Templating; +using BotSharp.Core.Translation; using Microsoft.Extensions.Configuration; namespace BotSharp.Core.Conversations; @@ -31,10 +35,17 @@ public class ConversationPlugin : IBotSharpPlugin return settingService.Bind("Conversation"); }); + services.AddScoped(provider => + { + var settingService = provider.GetRequiredService(); + return settingService.Bind("GoogleApi"); + }); + services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); + services.AddScoped(); + services.AddScoped(); // Rich content messaging services.AddScoped(); diff --git a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationAttachmentService.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationAttachmentService.cs deleted file mode 100644 index b0625fa8..00000000 --- a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationAttachmentService.cs +++ /dev/null @@ -1,28 +0,0 @@ -using BotSharp.Abstraction.Repositories; -using System.IO; - -namespace BotSharp.Core.Conversations.Services; - -public class ConversationAttachmentService : IConversationAttachmentService -{ - private readonly BotSharpDatabaseSettings _dbSettings; - private readonly IServiceProvider _services; - - public ConversationAttachmentService( - BotSharpDatabaseSettings dbSettings, - IServiceProvider services) - { - _dbSettings = dbSettings; - _services = services; - } - - public string GetDirectory(string conversationId) - { - var dir = Path.Combine(_dbSettings.FileRepository, "conversations", conversationId, "attachments"); - if (!Directory.Exists(dir)) - { - Directory.CreateDirectory(dir); - } - return dir; - } -} diff --git a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.SendMessage.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.SendMessage.cs index 9d01f355..d6940fe1 100644 --- a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.SendMessage.cs +++ b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.SendMessage.cs @@ -27,7 +27,6 @@ public partial class ConversationService #endif message.CurrentAgentId = agent.Id; - message.CreatedAt = DateTime.UtcNow; if (string.IsNullOrEmpty(message.SenderId)) { message.SenderId = _user.Id; @@ -47,6 +46,17 @@ public partial class ConversationService routing.Context.SetMessageId(_conversationId, message.MessageId); routing.Context.Push(agent.Id); + // Save message files + var fileService = _services.GetRequiredService(); + fileService.SaveMessageFiles(_conversationId, message.MessageId, message.Files); + message.Files?.Clear(); + + // Save payload + if (replyMessage != null && !string.IsNullOrEmpty(replyMessage.Payload)) + { + message.Payload = replyMessage.Payload; + } + // Before chat completion hook foreach (var hook in hooks) { @@ -71,19 +81,13 @@ public partial class ConversationService } } - // Persist to storage - _storage.Append(_conversationId, message); - - // Add to thread - dialogs.Add(RoleDialogModel.From(message)); - if (!stopCompletion) { // Routing with reasoning var settings = _services.GetRequiredService(); response = agent.Type == AgentType.Routing ? - await routing.InstructLoop(message, dialogs) : + await routing.InstructLoop(message, dialogs, onFunctionExecuting) : await routing.InstructDirect(agent, message); routing.ResetRecursiveCounter(); @@ -136,7 +140,7 @@ public partial class ConversationService response.RichContent is RichContent template && string.IsNullOrEmpty(template.Message.Text)) { - template.Message.Text = response.Content; + template.Message.Text = response.SecondaryContent ?? response.Content; } // Only read content from RichContent for UI rendering. When richContent is null, create a basic text message for richContent. @@ -144,30 +148,40 @@ public partial class ConversationService response.RichContent = response.RichContent ?? new RichContent { Recipient = new Recipient { Id = state.GetConversationId() }, - Message = new TextMessage(response.Content) + Message = new TextMessage(response.SecondaryContent ?? response.Content) }; - var hooks = _services.GetServices().ToList(); + // Patch return function name + if (response.PostbackFunctionName != null) + { + response.FunctionName = response.PostbackFunctionName; + } if (response.Instruction != null) { var conversation = _services.GetRequiredService(); var updatedConversation = await conversation.UpdateConversationTitle(_conversationId, response.Instruction.NextActionReason); + // Emit conversation task completed hook + if (response.Instruction.TaskCompleted) + { + await HookEmitter.Emit(_services, async hook => + await hook.OnTaskCompleted(response) + ); + } + // Emit conversation ending hook if (response.Instruction.ConversationEnd) { - foreach (var hook in hooks) - { - await hook.OnConversationEnding(response); - } + await HookEmitter.Emit(_services, async hook => + await hook.OnConversationEnding(response) + ); } } - foreach (var hook in hooks) - { - await hook.OnResponseGenerated(response); - } + await HookEmitter.Emit(_services, async hook => + await hook.OnResponseGenerated(response) + ); await onResponseReceived(response); diff --git a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.Summary.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.Summary.cs new file mode 100644 index 00000000..2e7657ba --- /dev/null +++ b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.Summary.cs @@ -0,0 +1,119 @@ +using BotSharp.Abstraction.MLTasks; +using BotSharp.Abstraction.Templating; + +namespace BotSharp.Core.Conversations.Services; + +public partial class ConversationService +{ + public async Task GetConversationSummary(IEnumerable conversationIds) + { + if (conversationIds.IsNullOrEmpty()) return string.Empty; + + var routing = _services.GetRequiredService(); + var agentService = _services.GetRequiredService(); + + var contents = new List(); + foreach ( var conversationId in conversationIds) + { + if (string.IsNullOrEmpty(conversationId)) continue; + + var dialogs = _storage.GetDialogs(conversationId); + + if (dialogs.IsNullOrEmpty()) continue; + + var content = GetConversationContent(dialogs); + if (string.IsNullOrWhiteSpace(content)) continue; + + contents.Add(content); + } + + if (contents.IsNullOrEmpty()) return string.Empty; + + var router = await agentService.LoadAgent(AIAssistant); + var prompt = GetPrompt(router, contents); + var summary = await Summarize(router, prompt); + + return summary; + } + + private string GetPrompt(Agent agent, List contents) + { + var template = agent.Templates.First(x => x.Name == "conversation.summary").Content; + var render = _services.GetRequiredService(); + + var texts = new List(); + for (int i = 0; i < contents.Count; i++) + { + texts.Add($"{contents[i]}"); + } + + return render.Render(template, new Dictionary + { + { "texts", texts } + }); + } + + private async Task Summarize(Agent agent, string prompt) + { + var provider = "openai"; + string? model; + + var providerService = _services.GetRequiredService(); + var modelSettings = providerService.GetProviderModels(provider); + var modelSetting = modelSettings.FirstOrDefault(x => x.Name.IsEqualTo("gpt4-turbo") || x.Name.IsEqualTo("gpt-4o")); + + if (modelSetting != null) + { + model = modelSetting.Name; + } + else + { + provider = agent?.LlmConfig?.Provider; + model = agent?.LlmConfig?.Model; + if (provider == null || model == null) + { + var agentSettings = _services.GetRequiredService(); + provider = agentSettings.LlmConfig.Provider; + model = agentSettings.LlmConfig.Model; + } + } + + var chatCompletion = CompletionProvider.GetChatCompletion(_services, provider, model); + var response = await chatCompletion.GetChatCompletions(new Agent + { + Id = agent.Id, + Name = agent.Name, + Instruction = prompt + }, new List + { + new RoleDialogModel(AgentRole.User, "Please summarize the conversations.") + }); + + return response.Content; + } + + private string GetConversationContent(List dialogs, int maxDialogCount = 50) + { + var conversation = ""; + + foreach (var dialog in dialogs.TakeLast(maxDialogCount)) + { + var role = dialog.Role; + if (role == AgentRole.Function) continue; + + if (role != AgentRole.User) + { + role = AgentRole.Assistant; + } + + conversation += $"{role}: {dialog.Payload ?? dialog.Content}\r\n"; + } + + if (string.IsNullOrEmpty(conversation)) + { + return string.Empty; + } + + return conversation + "\r\n"; + } +} diff --git a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.TruncateMessage.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.TruncateMessage.cs index e4b81f31..451cdeed 100644 --- a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.TruncateMessage.cs +++ b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.TruncateMessage.cs @@ -2,15 +2,19 @@ namespace BotSharp.Core.Conversations.Services; public partial class ConversationService : IConversationService { - public async Task TruncateConversation(string conversationId, string messageId) + public async Task TruncateConversation(string conversationId, string messageId, string? newMessageId = null) { var db = _services.GetRequiredService(); - var isSaved = db.TruncateConversation(conversationId, messageId, true); + var fileService = _services.GetRequiredService(); + var deleteMessageIds = db.TruncateConversation(conversationId, messageId, cleanLog: true); + + fileService.DeleteMessageFiles(conversationId, deleteMessageIds, messageId, newMessageId); + var hooks = _services.GetServices().ToList(); foreach (var hook in hooks) { await hook.OnMessageDeleted(conversationId, messageId); } - return await Task.FromResult(isSaved); + return await Task.FromResult(true); } } diff --git a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.UpdateBreakpoint.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.UpdateBreakpoint.cs index ea10dac0..8f88f44f 100644 --- a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.UpdateBreakpoint.cs +++ b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.UpdateBreakpoint.cs @@ -1,19 +1,34 @@ +using BotSharp.Abstraction.Infrastructures.Enums; + namespace BotSharp.Core.Conversations.Services; public partial class ConversationService : IConversationService { - public async Task UpdateBreakpoint(bool resetStates = false) + public async Task UpdateBreakpoint(bool resetStates = false, string? reason = null, params string[] excludedStates) { var db = _services.GetRequiredService(); var routingCtx = _services.GetRequiredService(); var messageId = routingCtx.MessageId; - db.UpdateConversationBreakpoint(_conversationId, messageId, DateTime.UtcNow); + + db.UpdateConversationBreakpoint(_conversationId, new ConversationBreakpoint + { + MessageId = messageId, + Breakpoint = DateTime.UtcNow, + Reason = reason + }); // Reset states if (resetStates) { var states = _services.GetRequiredService(); - states.CleanStates(); + // keep language state + if (excludedStates == null) excludedStates = new string[] { }; + if (!excludedStates.Contains(StateConst.LANGUAGE)) + { + excludedStates = excludedStates.Append(StateConst.LANGUAGE).ToArray(); + } + + states.CleanStates(excludedStates); } var hooks = _services.GetServices() diff --git a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.cs index 8b3bb0fc..8e113226 100644 --- a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.cs +++ b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.cs @@ -1,3 +1,4 @@ +using BotSharp.Abstraction.Conversations.Enums; using BotSharp.Abstraction.Models; namespace BotSharp.Core.Conversations.Services; @@ -11,6 +12,8 @@ public partial class ConversationService : IConversationService private readonly IConversationStorage _storage; private readonly IConversationStateService _state; private string _conversationId; + private const string AIAssistant = "01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a"; + public string ConversationId => _conversationId; public IConversationStateService States => _state; @@ -34,7 +37,9 @@ public partial class ConversationService : IConversationService public async Task DeleteConversations(IEnumerable ids) { var db = _services.GetRequiredService(); + var fileService = _services.GetRequiredService(); var isDeleted = db.DeleteConversations(ids); + fileService.DeleteConversationFiles(ids); return await Task.FromResult(isDeleted); } @@ -114,7 +119,14 @@ public partial class ConversationService : IConversationService { var db = _services.GetRequiredService(); var breakpoint = db.GetConversationBreakpoint(_conversationId); - dialogs = dialogs.Where(x => x.CreatedAt >= breakpoint).ToList(); + if (breakpoint != null) + { + dialogs = dialogs.Where(x => x.CreatedAt >= breakpoint.Breakpoint).ToList(); + if (!string.IsNullOrEmpty(breakpoint.Reason)) + { + dialogs.Insert(0, new RoleDialogModel(AgentRole.User, breakpoint.Reason)); + } + } } return dialogs @@ -126,6 +138,6 @@ public partial class ConversationService : IConversationService { _conversationId = conversationId; _state.Load(_conversationId); - states.ForEach(x => _state.SetState(x.Key, x.Value, activeRounds: x.ActiveRounds)); + states.ForEach(x => _state.SetState(x.Key, x.Value, activeRounds: x.ActiveRounds, source: StateSource.External)); } } diff --git a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStateService.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStateService.cs index 9356e2f5..57f09c71 100644 --- a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStateService.cs +++ b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStateService.cs @@ -1,3 +1,4 @@ +using BotSharp.Abstraction.Conversations.Enums; using BotSharp.Abstraction.Users.Enums; namespace BotSharp.Core.Conversations.Services; @@ -9,9 +10,16 @@ public class ConversationStateService : IConversationStateService, IDisposable { private readonly ILogger _logger; private readonly IServiceProvider _services; - private ConversationState _states; - private string _conversationId; private readonly IBotSharpRepository _db; + private string _conversationId; + /// + /// States in the current round of conversation + /// + private ConversationState _curStates; + /// + /// States in the previous rounds of conversation + /// + private ConversationState _historyStates; public ConversationStateService(ILogger logger, IServiceProvider services, @@ -20,7 +28,8 @@ public class ConversationStateService : IConversationStateService, IDisposable _logger = logger; _services = services; _db = db; - _states = new ConversationState(); + _curStates = new ConversationState(); + _historyStates = new ConversationState(); } public string GetConversationId() => _conversationId; @@ -33,7 +42,8 @@ public class ConversationStateService : IConversationStateService, IDisposable /// /// whether the state is related to message or not /// - public IConversationStateService SetState(string name, T value, bool isNeedVersion = true, int activeRounds = -1) + public IConversationStateService SetState(string name, T value, bool isNeedVersion = true, + int activeRounds = -1, string valueType = StateDataType.String, string source = StateSource.User, bool readOnly = false) { if (value == null) { @@ -46,34 +56,41 @@ public class ConversationStateService : IConversationStateService, IDisposable var curActiveRounds = activeRounds > 0 ? activeRounds : -1; int? preActiveRounds = null; - if (ContainsState(name) && _states.TryGetValue(name, out var pair)) + if (ContainsState(name) && _curStates.TryGetValue(name, out var pair)) { - var lastNode = pair?.Values?.LastOrDefault(); - preActiveRounds = lastNode?.ActiveRounds; - preValue = lastNode?.Data ?? string.Empty; + var leafNode = pair?.Values?.LastOrDefault(); + preActiveRounds = leafNode?.ActiveRounds; + preValue = leafNode?.Data ?? string.Empty; } _logger.LogInformation($"[STATE] {name} = {value}"); var routingCtx = _services.GetRequiredService(); - foreach (var hook in hooks) + if (!ContainsState(name) || preValue != currentValue || preActiveRounds != curActiveRounds) { - hook.OnStateChanged(new StateChangeModel + foreach (var hook in hooks) { - ConversationId = _conversationId, - MessageId = routingCtx.MessageId, - Name = name, - BeforeValue = preValue, - BeforeActiveRounds = preActiveRounds, - AfterValue = currentValue, - AfterActiveRounds = curActiveRounds - }).Wait(); + hook.OnStateChanged(new StateChangeModel + { + ConversationId = _conversationId, + MessageId = routingCtx.MessageId, + Name = name, + BeforeValue = preValue, + BeforeActiveRounds = preActiveRounds, + AfterValue = currentValue, + AfterActiveRounds = curActiveRounds, + DataType = valueType, + Source = source, + Readonly = readOnly + }).Wait(); + } } var newPair = new StateKeyValue { Key = name, - Versioning = isNeedVersion + Versioning = isNeedVersion, + Readonly = readOnly }; var newValue = new StateValue @@ -82,76 +99,94 @@ public class ConversationStateService : IConversationStateService, IDisposable MessageId = routingCtx.MessageId, Active = true, ActiveRounds = curActiveRounds, + DataType = valueType, + Source = source, UpdateTime = DateTime.UtcNow, }; - if (!isNeedVersion || !_states.ContainsKey(name)) + if (!isNeedVersion || !_curStates.ContainsKey(name)) { newPair.Values = new List { newValue }; - _states[name] = newPair; + _curStates[name] = newPair; } else { - _states[name].Values.Add(newValue); + _curStates[name].Values.Add(newValue); } return this; } - public Dictionary Load(string conversationId) + public Dictionary Load(string conversationId, bool isReadOnly = false) { - _conversationId = conversationId; + _conversationId = !isReadOnly ? conversationId : null; var routingCtx = _services.GetRequiredService(); var curMsgId = routingCtx.MessageId; - _states = _db.GetConversationStates(_conversationId); - var dialogs = _db.GetConversationDialogs(_conversationId); + + _historyStates = _db.GetConversationStates(conversationId); + var dialogs = _db.GetConversationDialogs(conversationId); var userDialogs = dialogs.Where(x => x.MetaData?.Role == AgentRole.User || x.MetaData?.Role == UserRole.Client) + .GroupBy(x => x.MetaData?.MessageId) + .Select(g => g.First()) .OrderBy(x => x.MetaData?.CreateTime) .ToList(); - var curMsgIndex = userDialogs.FindIndex(x => !string.IsNullOrEmpty(curMsgId) && x.MetaData?.MessageId == curMsgId); curMsgIndex = curMsgIndex < 0 ? userDialogs.Count() : curMsgIndex; - var curStates = new Dictionary(); - if (!_states.IsNullOrEmpty()) + var endNodes = new Dictionary(); + if (_historyStates.IsNullOrEmpty()) return endNodes; + + foreach (var state in _historyStates) { - foreach (var state in _states) + var key = state.Key; + var value = state.Value; + var leafNode = value?.Values?.LastOrDefault(); + if (leafNode == null) continue; + + _curStates[key] = new StateKeyValue { - var value = state.Value?.Values?.LastOrDefault(); - if (value == null || !value.Active) continue; + Key = key, + Versioning = value.Versioning, + Readonly = value.Readonly, + Values = new List { leafNode } + }; - if (value.ActiveRounds > 0) + if (!leafNode.Active) continue; + + // Handle state active rounds + if (leafNode.ActiveRounds > 0) + { + var stateMsgIndex = userDialogs.FindIndex(x => !string.IsNullOrEmpty(x.MetaData?.MessageId) && x.MetaData.MessageId == leafNode.MessageId); + if (stateMsgIndex >= 0 && curMsgIndex - stateMsgIndex >= leafNode.ActiveRounds) { - var stateMsgIndex = userDialogs.FindIndex(x => !string.IsNullOrEmpty(x.MetaData?.MessageId) && x.MetaData.MessageId == value.MessageId); - if (stateMsgIndex >= 0 && curMsgIndex - stateMsgIndex >= value.ActiveRounds) + _curStates[key].Values.Add(new StateValue { - state.Value.Values.Add(new StateValue - { - Data = value.Data, - MessageId = curMsgId, - Active = false, - ActiveRounds = value.ActiveRounds, - UpdateTime = DateTime.UtcNow - }); - continue; - } + Data = leafNode.Data, + MessageId = curMsgId, + Active = false, + ActiveRounds = leafNode.ActiveRounds, + DataType = leafNode.DataType, + Source = leafNode.Source, + UpdateTime = DateTime.UtcNow + }); + continue; } - - var data = value.Data ?? string.Empty; - curStates[state.Key] = data; - _logger.LogInformation($"[STATE] {state.Key} : {data}"); } + + var data = leafNode.Data ?? string.Empty; + endNodes[state.Key] = data; + _logger.LogInformation($"[STATE] {key} : {data}"); } - _logger.LogInformation($"Loaded conversation states: {_conversationId}"); + _logger.LogInformation($"Loaded conversation states: {conversationId}"); var hooks = _services.GetServices(); foreach (var hook in hooks) { - hook.OnStateLoaded(_states).Wait(); + hook.OnStateLoaded(_curStates).Wait(); } - return curStates; + return endNodes; } public void Save() @@ -163,35 +198,106 @@ public class ConversationStateService : IConversationStateService, IDisposable var states = new List(); - foreach (var dic in _states) + foreach (var pair in _curStates) { - states.Add(dic.Value); + var key = pair.Key; + var curValue = pair.Value; + + if (!_historyStates.TryGetValue(key, out var historyValue) + || historyValue == null + || historyValue.Values.IsNullOrEmpty() + || !curValue.Versioning) + { + states.Add(curValue); + } + else + { + var historyValues = historyValue.Values.Take(historyValue.Values.Count - 1).ToList(); + var newValues = historyValues.Concat(curValue.Values).ToList(); + var updatedNode = new StateKeyValue + { + Key = pair.Key, + Versioning = curValue.Versioning, + Readonly = curValue.Readonly, + Values = newValues + }; + states.Add(updatedNode); + } } _db.UpdateConversationStates(_conversationId, states); _logger.LogInformation($"Saved states of conversation {_conversationId}"); } - public void CleanStates() + public bool RemoveState(string name) + { + if (!ContainsState(name)) return false; + + var routingCtx = _services.GetRequiredService(); + var value = _curStates[name]; + var leafNode = value?.Values?.LastOrDefault(); + if (value == null || !value.Versioning || leafNode == null) return false; + + _curStates[name].Values.Add(new StateValue + { + Data = leafNode.Data, + MessageId = routingCtx.MessageId, + Active = false, + ActiveRounds = leafNode.ActiveRounds, + DataType = leafNode.DataType, + Source = leafNode.Source, + UpdateTime = DateTime.UtcNow + }); + + var hooks = _services.GetServices(); + foreach (var hook in hooks) + { + hook.OnStateChanged(new StateChangeModel + { + ConversationId = _conversationId, + MessageId = routingCtx.MessageId, + Name = name, + BeforeValue = leafNode.Data, + BeforeActiveRounds = leafNode.ActiveRounds, + AfterValue = null, + AfterActiveRounds = leafNode.ActiveRounds, + DataType = leafNode.DataType, + Source = leafNode.Source, + Readonly = value.Readonly + }).Wait(); + } + + return true; + } + + public void CleanStates(params string[] excludedStates) { var routingCtx = _services.GetRequiredService(); var curMsgId = routingCtx.MessageId; var utcNow = DateTime.UtcNow; - foreach (var key in _states.Keys) + foreach (var key in _curStates.Keys) { - var value = _states[key]; + // skip state + if (excludedStates.Contains(key)) + { + continue; + } + + var value = _curStates[key]; if (value == null || !value.Versioning || value.Values.IsNullOrEmpty()) continue; - var lastValue = value.Values.LastOrDefault(); - if (lastValue == null || !lastValue.Active) continue; + var leafNode = value.Values.LastOrDefault(); + if (leafNode == null || !leafNode.Active) continue; value.Values.Add(new StateValue { - Data = lastValue.Data, + Data = leafNode.Data, MessageId = curMsgId, Active = false, - ActiveRounds = lastValue.ActiveRounds, + ActiveRounds = leafNode.ActiveRounds, + DataType = leafNode.DataType, + Source = leafNode.Source, UpdateTime = utcNow }); } @@ -199,25 +305,25 @@ public class ConversationStateService : IConversationStateService, IDisposable public Dictionary GetStates() { - var curStates = new Dictionary(); - foreach (var state in _states) + var endNodes = new Dictionary(); + foreach (var state in _curStates) { var value = state.Value?.Values?.LastOrDefault(); if (value == null || !value.Active) continue; - curStates[state.Key] = value.Data ?? string.Empty; + endNodes[state.Key] = value.Data ?? string.Empty; } - return curStates; + return endNodes; } public string GetState(string name, string defaultValue = "") { - if (!_states.ContainsKey(name) || _states[name].Values.IsNullOrEmpty() || !_states[name].Values.Last().Active) + if (!_curStates.ContainsKey(name) || _curStates[name].Values.IsNullOrEmpty() || !_curStates[name].Values.Last().Active) { return defaultValue; } - return _states[name].Values.Last().Data; + return _curStates[name].Values.Last().Data; } public void Dispose() @@ -227,10 +333,10 @@ public class ConversationStateService : IConversationStateService, IDisposable public bool ContainsState(string name) { - return _states.ContainsKey(name) - && !_states[name].Values.IsNullOrEmpty() - && _states[name].Values.LastOrDefault()?.Active == true - && !string.IsNullOrEmpty(_states[name].Values.Last().Data); + return _curStates.ContainsKey(name) + && !_curStates[name].Values.IsNullOrEmpty() + && _curStates[name].Values.LastOrDefault()?.Active == true + && !string.IsNullOrEmpty(_curStates[name].Values.Last().Data); } public void SaveStateByArgs(JsonDocument args) @@ -244,9 +350,17 @@ public class ConversationStateService : IConversationStateService, IDisposable { foreach (JsonProperty property in root.EnumerateObject()) { - if (!string.IsNullOrEmpty(property.Value.ToString())) + var propertyValue = property.Value; + var stateValue = propertyValue.ToString(); + if (!string.IsNullOrEmpty(stateValue)) { - SetState(property.Name, property.Value); + if (propertyValue.ValueKind == JsonValueKind.True || + propertyValue.ValueKind == JsonValueKind.False) + { + stateValue = stateValue?.ToLower(); + } + + SetState(property.Name, stateValue, source: StateSource.Application); } } } diff --git a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStorage.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStorage.cs index e523ce48..5abc4fc5 100644 --- a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStorage.cs +++ b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStorage.cs @@ -8,8 +8,8 @@ namespace BotSharp.Core.Conversations.Services; public class ConversationStorage : IConversationStorage { private readonly BotSharpDatabaseSettings _dbSettings; + private readonly BotSharpOptions _options; private readonly IServiceProvider _services; - private readonly JsonSerializerOptions _jsonOptions; public ConversationStorage( BotSharpDatabaseSettings dbSettings, @@ -18,7 +18,7 @@ public class ConversationStorage : IConversationStorage { _dbSettings = dbSettings; _services = services; - _jsonOptions = InitJsonSerilizerOptions(options); + _options = options; } public void Append(string conversationId, RoleDialogModel dialog) @@ -28,11 +28,11 @@ public class ConversationStorage : IConversationStorage var dialogElements = new List(); // Prevent duplicate record to be inserted - var dialogs = db.GetConversationDialogs(conversationId); + /*var dialogs = db.GetConversationDialogs(conversationId); if (dialogs.Any(x => x.MetaData.MessageId == dialog.MessageId && x.Content == dialog.Content)) { return; - } + }*/ if (dialog.Role == AgentRole.Function) { @@ -50,7 +50,13 @@ public class ConversationStorage : IConversationStorage { return; } - dialogElements.Add(new DialogElement(meta, content)); + dialogElements.Add(new DialogElement + { + MetaData = meta, + Content = dialog.Content, + SecondaryContent = dialog.SecondaryContent, + Payload = dialog.Payload + }); } else { @@ -70,8 +76,17 @@ public class ConversationStorage : IConversationStorage return; } - var richContent = dialog.RichContent != null ? JsonSerializer.Serialize(dialog.RichContent, _jsonOptions) : null; - dialogElements.Add(new DialogElement(meta, content, richContent)); + var richContent = dialog.RichContent != null ? JsonSerializer.Serialize(dialog.RichContent, _options.JsonSerializerOptions) : null; + var secondaryRichContent = dialog.SecondaryRichContent != null ? JsonSerializer.Serialize(dialog.SecondaryRichContent, _options.JsonSerializerOptions) : null; + dialogElements.Add(new DialogElement + { + MetaData = meta, + Content = dialog.Content, + SecondaryContent = dialog.SecondaryContent, + RichContent = richContent, + SecondaryRichContent = secondaryRichContent, + Payload = dialog.Payload + }); } db.AppendConversationDialogs(conversationId, dialogElements); @@ -88,6 +103,8 @@ public class ConversationStorage : IConversationStorage { var meta = dialog.MetaData; var content = dialog.Content; + var secondaryContent = dialog.SecondaryContent; + var payload = string.IsNullOrEmpty(dialog.Payload) ? null : dialog.Payload; var role = meta.Role; var currentAgentId = meta.AgentId; var messageId = meta.MessageId; @@ -95,7 +112,9 @@ public class ConversationStorage : IConversationStorage var senderId = role == AgentRole.Function ? currentAgentId : meta.SenderId; var createdAt = meta.CreateTime; var richContent = !string.IsNullOrEmpty(dialog.RichContent) ? - JsonSerializer.Deserialize>(dialog.RichContent, _jsonOptions) : null; + JsonSerializer.Deserialize>(dialog.RichContent, _options.JsonSerializerOptions) : null; + var secondaryRichContent = !string.IsNullOrEmpty(dialog.SecondaryRichContent) ? + JsonSerializer.Deserialize>(dialog.SecondaryRichContent, _options.JsonSerializerOptions) : null; var record = new RoleDialogModel(role, content) { @@ -104,7 +123,10 @@ public class ConversationStorage : IConversationStorage CreatedAt = createdAt, SenderId = senderId, FunctionName = function, - RichContent = richContent + RichContent = richContent, + SecondaryContent = secondaryContent, + SecondaryRichContent = secondaryRichContent, + Payload = payload }; results.Add(record); @@ -140,21 +162,4 @@ public class ConversationStorage : IConversationStorage } return Path.Combine(dir, "dialogs.txt"); } - - private JsonSerializerOptions InitJsonSerilizerOptions(BotSharpOptions botSharOptions) - { - var options = botSharOptions.JsonSerializerOptions; - var jsonOptions = new JsonSerializerOptions - { - PropertyNameCaseInsensitive = options.PropertyNameCaseInsensitive, - PropertyNamingPolicy = options.PropertyNamingPolicy ?? JsonNamingPolicy.CamelCase, - AllowTrailingCommas = options.AllowTrailingCommas, - }; - - foreach (var converter in options.Converters) - { - jsonOptions.Converters.Add(converter); - } - return jsonOptions; - } } diff --git a/src/Infrastructure/BotSharp.Core/Conversations/Services/TokenStatistics.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/TokenStatistics.cs index 1ee38e2e..ca230ff3 100644 --- a/src/Infrastructure/BotSharp.Core/Conversations/Services/TokenStatistics.cs +++ b/src/Infrastructure/BotSharp.Core/Conversations/Services/TokenStatistics.cs @@ -1,3 +1,4 @@ +using BotSharp.Abstraction.Conversations.Enums; using BotSharp.Abstraction.MLTasks; using System.Diagnostics; using System.Drawing; @@ -47,14 +48,14 @@ public class TokenStatistics : ITokenStatistics // Accumulated Token var stat = _services.GetRequiredService(); var inputCount = int.Parse(stat.GetState("prompt_total", "0")); - stat.SetState("prompt_total", stats.PromptCount + inputCount, false); + stat.SetState("prompt_total", stats.PromptCount + inputCount, isNeedVersion: false, source: StateSource.Application); var outputCount = int.Parse(stat.GetState("completion_total", "0")); - stat.SetState("completion_total", stats.CompletionCount + outputCount, false); + stat.SetState("completion_total", stats.CompletionCount + outputCount, isNeedVersion: false, source: StateSource.Application); // Total cost var total_cost = float.Parse(stat.GetState("llm_total_cost", "0")); total_cost += Cost; - stat.SetState("llm_total_cost", total_cost, false); + stat.SetState("llm_total_cost", total_cost, isNeedVersion: false, source: StateSource.Application); } public void PrintStatistics() diff --git a/src/Infrastructure/BotSharp.Core/Files/BotSharpFileService.Conversation.cs b/src/Infrastructure/BotSharp.Core/Files/BotSharpFileService.Conversation.cs new file mode 100644 index 00000000..71e34549 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core/Files/BotSharpFileService.Conversation.cs @@ -0,0 +1,188 @@ +using Microsoft.AspNetCore.StaticFiles; +using System.IO; +using System.Threading; + +namespace BotSharp.Core.Files; + +public partial class BotSharpFileService +{ + public IEnumerable GetChatImages(string conversationId, List conversations, int offset = 1) + { + var files = new List(); + if (string.IsNullOrEmpty(conversationId) || conversations.IsNullOrEmpty()) + { + return files; + } + + if (offset <= 0) + { + offset = MIN_OFFSET; + } + else if (offset > MAX_OFFSET) + { + offset = MAX_OFFSET; + } + + var messageIds = conversations.Select(x => x.MessageId).Distinct().TakeLast(offset).ToList(); + files = GetMessageFiles(conversationId, messageIds, imageOnly: true).ToList(); + return files; + } + + public IEnumerable GetMessageFiles(string conversationId, IEnumerable messageIds, bool imageOnly = false) + { + var files = new List(); + if (messageIds.IsNullOrEmpty()) return files; + + foreach (var messageId in messageIds) + { + var dir = GetConversationFileDirectory(conversationId, messageId); + if (!ExistDirectory(dir)) + { + continue; + } + + foreach (var file in Directory.GetFiles(dir)) + { + var contentType = GetFileContentType(file); + if (imageOnly && !_allowedTypes.Contains(contentType)) + { + continue; + } + + var fileName = Path.GetFileNameWithoutExtension(file); + var extension = Path.GetExtension(file); + var fileType = extension.Substring(1); + + var model = new MessageFileModel() + { + MessageId = messageId, + FileUrl = $"/conversation/{conversationId}/message/{messageId}/file/{fileName}", + FileStorageUrl = file, + FileName = fileName, + FileType = fileType, + ContentType = contentType + }; + files.Add(model); + } + } + + return files; + } + + public string GetMessageFile(string conversationId, string messageId, string fileName) + { + var dir = GetConversationFileDirectory(conversationId, messageId); + if (!ExistDirectory(dir)) + { + return string.Empty; + } + + var found = Directory.GetFiles(dir).FirstOrDefault(f => Path.GetFileNameWithoutExtension(f).IsEqualTo(fileName)); + return found; + } + + public bool SaveMessageFiles(string conversationId, string messageId, List files) + { + if (files.IsNullOrEmpty()) return false; + + var dir = GetConversationFileDirectory(conversationId, messageId, createNewDir: true); + if (!ExistDirectory(dir)) return false; + + try + { + for (int i = 0; i < files.Count; i++) + { + var file = files[i]; + if (string.IsNullOrEmpty(file.FileData)) + { + continue; + } + + var (_, bytes) = GetFileInfoFromData(file.FileData); + var fileType = Path.GetExtension(file.FileName); + var fileName = $"{i + 1}{fileType}"; + Thread.Sleep(100); + File.WriteAllBytes(Path.Combine(dir, fileName), bytes); + } + return true; + } + catch (Exception ex) + { + _logger.LogWarning($"Error when saving conversation files: {ex.Message}"); + return false; + } + } + + + + public bool DeleteMessageFiles(string conversationId, IEnumerable messageIds, string targetMessageId, string? newMessageId = null) + { + if (string.IsNullOrEmpty(conversationId) || messageIds == null) return false; + + if (!string.IsNullOrEmpty(targetMessageId) && !string.IsNullOrEmpty(newMessageId)) + { + var prevDir = GetConversationFileDirectory(conversationId, targetMessageId); + var newDir = Path.Combine(_baseDir, CONVERSATION_FOLDER, conversationId, FILE_FOLDER, newMessageId); + + if (ExistDirectory(prevDir)) + { + if (ExistDirectory(newDir)) + { + Directory.Delete(newDir, true); + } + + Directory.Move(prevDir, newDir); + } + } + + foreach (var messageId in messageIds) + { + var dir = GetConversationFileDirectory(conversationId, messageId); + if (!ExistDirectory(dir)) continue; + + Thread.Sleep(100); + Directory.Delete(dir, true); + } + + return true; + } + + public bool DeleteConversationFiles(IEnumerable conversationIds) + { + if (conversationIds.IsNullOrEmpty()) return false; + + foreach (var conversationId in conversationIds) + { + var convDir = FindConversationDirectory(conversationId); + if (!ExistDirectory(convDir)) continue; + + Directory.Delete(convDir, true); + } + return true; + } + + #region Private methods + private string GetConversationFileDirectory(string? conversationId, string? messageId, bool createNewDir = false) + { + if (string.IsNullOrEmpty(conversationId) || string.IsNullOrEmpty(messageId)) + { + return string.Empty; + } + + var dir = Path.Combine(_baseDir, CONVERSATION_FOLDER, conversationId, FILE_FOLDER, messageId); + if (!Directory.Exists(dir) && createNewDir) + { + Directory.CreateDirectory(dir); + } + return dir; + } + + private string? FindConversationDirectory(string conversationId) + { + if (string.IsNullOrEmpty(conversationId)) return null; + + var dir = Path.Combine(_baseDir, CONVERSATION_FOLDER, conversationId); + return dir; + } + #endregion +} diff --git a/src/Infrastructure/BotSharp.Core/Files/BotSharpFileService.User.cs b/src/Infrastructure/BotSharp.Core/Files/BotSharpFileService.User.cs new file mode 100644 index 00000000..b6a87993 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core/Files/BotSharpFileService.User.cs @@ -0,0 +1,65 @@ +using System.IO; + +namespace BotSharp.Core.Files; + +public partial class BotSharpFileService +{ + public string GetUserAvatar() + { + var db = _services.GetRequiredService(); + var user = db.GetUserById(_user.Id); + var dir = GetUserAvatarDir(user?.Id); + + if (!ExistDirectory(dir)) return string.Empty; + + var found = Directory.GetFiles(dir).FirstOrDefault() ?? string.Empty; + return found; + } + + public bool SaveUserAvatar(BotSharpFile file) + { + if (file == null || string.IsNullOrEmpty(file.FileData)) return false; + + try + { + var db = _services.GetRequiredService(); + var user = db.GetUserById(_user.Id); + var dir = GetUserAvatarDir(user?.Id); + + if (string.IsNullOrEmpty(dir)) return false; + + if (Directory.Exists(dir)) + { + Directory.Delete(dir, true); + } + + dir = GetUserAvatarDir(user?.Id, createNewDir: true); + var (_, bytes) = GetFileInfoFromData(file.FileData); + File.WriteAllBytes(Path.Combine(dir, file.FileName), bytes); + return true; + } + catch (Exception ex) + { + _logger.LogWarning($"Error when saving user avatar: {ex.Message}"); + return false; + } + } + + + #region Private methods + private string GetUserAvatarDir(string? userId, bool createNewDir = false) + { + if (string.IsNullOrEmpty(userId)) + { + return string.Empty; + } + + var dir = Path.Combine(_baseDir, USERS_FOLDER, userId, USER_AVATAR_FOLDER); + if (!Directory.Exists(dir) && createNewDir) + { + Directory.CreateDirectory(dir); + } + return dir; + } + #endregion +} diff --git a/src/Infrastructure/BotSharp.Core/Files/BotSharpFileService.cs b/src/Infrastructure/BotSharp.Core/Files/BotSharpFileService.cs new file mode 100644 index 00000000..76d26dbc --- /dev/null +++ b/src/Infrastructure/BotSharp.Core/Files/BotSharpFileService.cs @@ -0,0 +1,83 @@ +using Microsoft.AspNetCore.StaticFiles; +using System; +using System.IO; +using System.Threading; + +namespace BotSharp.Core.Files; + +public partial class BotSharpFileService : IBotSharpFileService +{ + private readonly BotSharpDatabaseSettings _dbSettings; + private readonly IServiceProvider _services; + private readonly IUserIdentity _user; + private readonly ILogger _logger; + private readonly string _baseDir; + private readonly IEnumerable _allowedTypes = new List { "image/png", "image/jpeg" }; + + private const string CONVERSATION_FOLDER = "conversations"; + private const string FILE_FOLDER = "files"; + private const string USERS_FOLDER = "users"; + private const string USER_AVATAR_FOLDER = "avatar"; + + private const int MIN_OFFSET = 1; + private const int MAX_OFFSET = 5; + + public BotSharpFileService( + BotSharpDatabaseSettings dbSettings, + IUserIdentity user, + ILogger logger, + IServiceProvider services) + { + _dbSettings = dbSettings; + _user = user; + _logger = logger; + _services = services; + _baseDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, dbSettings.FileRepository); + } + + public string GetDirectory(string conversationId) + { + var dir = Path.Combine(_dbSettings.FileRepository, CONVERSATION_FOLDER, conversationId, "attachments"); + if (!Directory.Exists(dir)) + { + Directory.CreateDirectory(dir); + } + return dir; + } + + public (string, byte[]) GetFileInfoFromData(string data) + { + if (string.IsNullOrEmpty(data)) + { + return (string.Empty, new byte[0]); + } + + var typeStartIdx = data.IndexOf(':'); + var typeEndIdx = data.IndexOf(';'); + var contentType = data.Substring(typeStartIdx + 1, typeEndIdx - typeStartIdx - 1); + + var base64startIdx = data.IndexOf(','); + var base64Str = data.Substring(base64startIdx + 1); + + return (contentType, Convert.FromBase64String(base64Str)); + } + + #region Private methods + private string GetFileContentType(string filePath) + { + string contentType; + var provider = new FileExtensionContentTypeProvider(); + if (!provider.TryGetContentType(filePath, out contentType)) + { + contentType = string.Empty; + } + + return contentType; + } + + private bool ExistDirectory(string? dir) + { + return !string.IsNullOrEmpty(dir) && Directory.Exists(dir); + } + #endregion +} diff --git a/src/Infrastructure/BotSharp.Core/Infrastructures/CompletionProvider.cs b/src/Infrastructure/BotSharp.Core/Infrastructures/CompletionProvider.cs index c55ed9a8..4655b1de 100644 --- a/src/Infrastructure/BotSharp.Core/Infrastructures/CompletionProvider.cs +++ b/src/Infrastructure/BotSharp.Core/Infrastructures/CompletionProvider.cs @@ -35,10 +35,13 @@ public class CompletionProvider public static IChatCompletion GetChatCompletion(IServiceProvider services, string? provider = null, string? model = null, + string? modelId = null, + bool? multiModal = null, AgentLlmConfig? agentConfig = null) { var completions = services.GetServices(); - (provider, model) = GetProviderAndModel(services, provider: provider, model: model, agentConfig: agentConfig); + (provider, model) = GetProviderAndModel(services, provider: provider, model: model, modelId: modelId, + multiModal: multiModal, agentConfig: agentConfig); var completer = completions.FirstOrDefault(x => x.Provider == provider); if (completer == null) @@ -47,7 +50,7 @@ public class CompletionProvider logger.LogError($"Can't resolve completion provider by {provider}"); } - completer.SetModelName(model); + completer?.SetModelName(model); return completer; } @@ -55,6 +58,8 @@ public class CompletionProvider private static (string, string) GetProviderAndModel(IServiceProvider services, string? provider = null, string? model = null, + string? modelId = null, + bool? multiModal = null, AgentLlmConfig? agentConfig = null) { var agentSetting = services.GetRequiredService(); @@ -73,11 +78,11 @@ public class CompletionProvider { model = state.GetState("model", model ?? "gpt-35-turbo-4k"); } - else if (state.ContainsState("model_id")) + else if (state.ContainsState("model_id") || !string.IsNullOrEmpty(modelId)) { - var modelId = state.GetState("model_id"); + var modelIdentity = state.ContainsState("model_id") ? state.GetState("model_id") : modelId; var llmProviderService = services.GetRequiredService(); - model = llmProviderService.GetProviderModel(provider, modelId)?.Name; + model = llmProviderService.GetProviderModel(provider, modelIdentity, multiModal: multiModal)?.Name; } } diff --git a/src/Infrastructure/BotSharp.Core/Infrastructures/DistributedLocker.cs b/src/Infrastructure/BotSharp.Core/Infrastructures/DistributedLocker.cs new file mode 100644 index 00000000..82b655c7 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core/Infrastructures/DistributedLocker.cs @@ -0,0 +1,50 @@ +using RedLockNet; +using RedLockNet.SERedis; +using RedLockNet.SERedis.Configuration; +using StackExchange.Redis; + +namespace BotSharp.Core.Infrastructures; + +public class DistributedLocker +{ + private readonly BotSharpDatabaseSettings _settings; + private readonly RedLockFactory _lockFactory; + + public DistributedLocker(/*BotSharpDatabaseSettings settings*/) + { + // _settings = settings; + + var multiplexers = new List(); + foreach (var x in "".Split(';')) + { + var option = new ConfigurationOptions + { + AbortOnConnectFail = false, + EndPoints = { x } + }; + var _connMuliplexer = ConnectionMultiplexer.Connect(option); + multiplexers.Add(_connMuliplexer); + } + + _lockFactory = RedLockFactory.Create(multiplexers); + } + + public async Task Lock(string resource, Func action) + { + var expiry = TimeSpan.FromSeconds(60); + var wait = TimeSpan.FromSeconds(30); + var retry = TimeSpan.FromSeconds(3); + + await using (var redLock = await _lockFactory.CreateLockAsync(resource, expiry, wait, retry)) + { + if (redLock.IsAcquired) + { + await action(); + } + else + { + Console.WriteLine($"Acquire locak failed due to {resource} after {wait}s timeout."); + } + } + } +} diff --git a/src/Infrastructure/BotSharp.Core/Infrastructures/LlmProviderService.cs b/src/Infrastructure/BotSharp.Core/Infrastructures/LlmProviderService.cs index eb92ac51..8320bdb7 100644 --- a/src/Infrastructure/BotSharp.Core/Infrastructures/LlmProviderService.cs +++ b/src/Infrastructure/BotSharp.Core/Infrastructures/LlmProviderService.cs @@ -44,11 +44,15 @@ public class LlmProviderService : ILlmProviderService ?.Models ?? new List(); } - public LlmModelSetting GetProviderModel(string provider, string id) + public LlmModelSetting GetProviderModel(string provider, string id, bool? multiModal = null) { var models = GetProviderModels(provider) - .Where(x => x.Id == id) - .ToList(); + .Where(x => x.Id == id); + + if (multiModal.HasValue) + { + models = models.Where(x => x.MultiModal == multiModal); + } var random = new Random(); var index = random.Next(0, models.Count()); diff --git a/src/Infrastructure/BotSharp.Core/Plugins/PluginLoader.cs b/src/Infrastructure/BotSharp.Core/Plugins/PluginLoader.cs index 0932041d..1b883657 100644 --- a/src/Infrastructure/BotSharp.Core/Plugins/PluginLoader.cs +++ b/src/Infrastructure/BotSharp.Core/Plugins/PluginLoader.cs @@ -5,7 +5,6 @@ using System.Drawing; using System.IO; using System.Reflection; using System.Xml; -using BotSharp.Abstraction.Repositories; namespace BotSharp.Core.Plugins; @@ -144,15 +143,19 @@ public class PluginLoader var config = db.GetPluginConfig(); if (enable) { - if (!config.EnabledPlugins.Exists(x => x == id)) + var dependentPlugins = new HashSet(); + var dependentAgentIds = new HashSet(); + FindPluginDependency(id, enable, ref dependentPlugins, ref dependentAgentIds); + var missingPlugins = dependentPlugins.Where(x => !config.EnabledPlugins.Contains(x)).ToList(); + if (!missingPlugins.IsNullOrEmpty()) { - config.EnabledPlugins.Add(id); + config.EnabledPlugins.AddRange(missingPlugins); db.SavePluginConfig(config); } // enable agents var agentService = services.GetRequiredService(); - foreach (var agentId in plugin.AgentIds) + foreach (var agentId in dependentAgentIds) { var agent = agentService.LoadAgent(agentId).Result; agent.Disabled = false; @@ -186,6 +189,43 @@ public class PluginLoader return plugin; } + private void FindPluginDependency(string pluginId, bool enabled, ref HashSet dependentPlugins, ref HashSet dependentAgentIds) + { + var pluginDef = _plugins.FirstOrDefault(x => x.Id == pluginId); + if (pluginDef == null) return; + + if (!pluginDef.IsCore) + { + pluginDef.Enabled = enabled; + dependentPlugins.Add(pluginId); + if (!pluginDef.AgentIds.IsNullOrEmpty()) + { + foreach (var agentId in pluginDef.AgentIds) + { + dependentAgentIds.Add(agentId); + } + } + } + + var foundPlugin = _modules.FirstOrDefault(x => x.Id == pluginId); + if (foundPlugin == null) return; + + var attr = foundPlugin.GetType().GetCustomAttribute(); + if (attr != null && !attr.PluginNames.IsNullOrEmpty()) + { + foreach (var name in attr.PluginNames) + { + var plugins = _plugins.Where(x => x.Assembly == name).ToList(); + if (plugins.IsNullOrEmpty()) return; + + foreach (var plugin in plugins) + { + FindPluginDependency(plugin.Id, enabled, ref dependentPlugins, ref dependentAgentIds); + } + } + } + } + public string GetSummaryComment(Type member) { string summary = string.Empty; @@ -229,4 +269,20 @@ public class PluginLoader } }); } + + public List GetPluginMenuByRoles(List plugins, string userRole) + { + if (plugins.IsNullOrEmpty()) return plugins; + + var filtered = new List(); + foreach (var plugin in plugins) + { + if (plugin.Roles.IsNullOrEmpty() || plugin.Roles.Contains(userRole)) + { + plugin.SubMenu = GetPluginMenuByRoles(plugin.SubMenu, userRole); + filtered.Add(plugin); + } + } + return filtered; + } } diff --git a/src/Infrastructure/BotSharp.Core/Repository/BotSharpDbContext.cs b/src/Infrastructure/BotSharp.Core/Repository/BotSharpDbContext.cs index 904048a8..6bba6c2a 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/BotSharpDbContext.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/BotSharpDbContext.cs @@ -1,6 +1,5 @@ using BotSharp.Abstraction.Loggers.Models; using BotSharp.Abstraction.Plugins.Models; -using BotSharp.Abstraction.Repositories.Models; using BotSharp.Abstraction.Tasks.Models; using BotSharp.Abstraction.Users.Models; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -73,86 +72,60 @@ public class BotSharpDbContext : Database, IBotSharpRepository #region Agent public Agent GetAgent(string agentId) - { - throw new NotImplementedException(); - } + => throw new NotImplementedException(); public List GetAgents(AgentFilter filter) - { - throw new NotImplementedException(); - } + => throw new NotImplementedException(); public List GetAgentsByUser(string userId) - { - throw new NotImplementedException(); - } + => throw new NotImplementedException(); public void UpdateAgent(Agent agent, AgentField field) - { - throw new NotImplementedException(); - } + => throw new NotImplementedException(); public string GetAgentTemplate(string agentId, string templateName) - { - throw new NotImplementedException(); - } + => 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(); - } + => throw new NotImplementedException(); public void BulkInsertAgents(List agents) - { - throw new NotImplementedException(); - } + => throw new NotImplementedException(); public void BulkInsertUserAgents(List userAgents) - { - throw new NotImplementedException(); - } + => throw new NotImplementedException(); public bool DeleteAgents() - { - throw new NotImplementedException(); - } + => throw new NotImplementedException(); + + public bool DeleteAgent(string agentId) + => throw new NotImplementedException(); #endregion #region Agent Task public PagedItems GetAgentTasks(AgentTaskFilter filter) - { - throw new NotImplementedException(); - } + => throw new NotImplementedException(); public AgentTask? GetAgentTask(string agentId, string taskId) - { - throw new NotImplementedException(); - } + => throw new NotImplementedException(); public void InsertAgentTask(AgentTask task) - { - throw new NotImplementedException(); - } + => throw new NotImplementedException(); public void BulkInsertAgentTasks(List tasks) - { - throw new NotImplementedException(); - } + => throw new NotImplementedException(); public void UpdateAgentTask(AgentTask task, AgentTaskField field) - { - throw new NotImplementedException(); - } + => throw new NotImplementedException(); - public bool DeleteAgentTask(string agentId, string taskId) - { - throw new NotImplementedException(); - } + public bool DeleteAgentTask(string agentId, List taskIds) + => throw new NotImplementedException(); public bool DeleteAgentTasks() - { - throw new NotImplementedException(); - } + => throw new NotImplementedException(); #endregion #region Conversation @@ -177,9 +150,6 @@ public class BotSharpDbContext : Database, IBotSharpRepository public List GetConversationDialogs(string conversationId) => throw new NotImplementedException(); - public void UpdateConversationDialogElements(string conversationId, List updateElements) - => new NotImplementedException(); - public ConversationState GetConversationStates(string conversationId) => throw new NotImplementedException(); @@ -189,10 +159,10 @@ public class BotSharpDbContext : Database, IBotSharpRepository public void UpdateConversationTitle(string conversationId, string title) => new NotImplementedException(); - public void UpdateConversationBreakpoint(string conversationId, string messageId, DateTime breakpoint) + public void UpdateConversationBreakpoint(string conversationId, ConversationBreakpoint breakpoint) => new NotImplementedException(); - public DateTime GetConversationBreakpoint(string conversationId) + public ConversationBreakpoint? GetConversationBreakpoint(string conversationId) => throw new NotImplementedException(); public void UpdateConversationStates(string conversationId, List states) @@ -201,21 +171,7 @@ public class BotSharpDbContext : Database, IBotSharpRepository public void UpdateConversationStatus(string conversationId, string status) => new NotImplementedException(); - public bool TruncateConversation(string conversationId, string messageId, bool cleanLog = false) - => throw new NotImplementedException(); - #endregion - - #region User - public User? GetUserByEmail(string email) - => throw new NotImplementedException(); - - public User? GetUserById(string id) - => throw new NotImplementedException(); - - public User? GetUserByUserName(string userName) - => throw new NotImplementedException(); - - public void CreateUser(User user) + public IEnumerable TruncateConversation(string conversationId, string messageId, bool cleanLog = false) => throw new NotImplementedException(); #endregion diff --git a/src/Infrastructure/BotSharp.Core/Repository/DataContextHelper.cs b/src/Infrastructure/BotSharp.Core/Repository/DataContextHelper.cs index 28b447c5..edcbced8 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/DataContextHelper.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/DataContextHelper.cs @@ -1,4 +1,3 @@ -using BotSharp.Abstraction.Repositories; using Microsoft.Data.SqlClient; using MySqlConnector; using System.Data.Common; diff --git a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Agent.cs b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Agent.cs index 1fbca99c..b7141a5b 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Agent.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Agent.cs @@ -1,9 +1,4 @@ -using BotSharp.Abstraction.Agents.Models; -using BotSharp.Abstraction.Functions.Models; -using BotSharp.Abstraction.Repositories.Filters; using BotSharp.Abstraction.Routing.Models; -using BotSharp.Abstraction.Tasks.Models; -using Microsoft.Extensions.Logging; using System.IO; namespace BotSharp.Core.Repository @@ -406,6 +401,25 @@ namespace BotSharp.Core.Repository return string.Empty; } + public bool PatchAgentTemplate(string agentId, AgentTemplate template) + { + if (string.IsNullOrEmpty(agentId) || template == null) return false; + + var dir = Path.Combine(_dbSettings.FileRepository, _agentSettings.DataDir, agentId, "templates"); + if (!Directory.Exists(dir)) return false; + + var foundTemplate = Directory.GetFiles(dir).FirstOrDefault(f => + { + var fileName = Path.GetFileNameWithoutExtension(f); + var extension = Path.GetExtension(f).Substring(1); + return fileName.IsEqualTo(template.Name) && extension.IsEqualTo(_agentSettings.TemplateFormat); + }); + + if (foundTemplate == null) return false; + + File.WriteAllText(foundTemplate, template.Content); + return true; + } public void BulkInsertAgents(List agents) { @@ -419,5 +433,43 @@ namespace BotSharp.Core.Repository { return false; } + + public bool DeleteAgent(string agentId) + { + if (string.IsNullOrEmpty(agentId)) return false; + + try + { + var agentDir = GetAgentDataDir(agentId); + if (string.IsNullOrEmpty(agentDir)) return false; + + // Delete agent user relationships + var usersDir = Path.Combine(_dbSettings.FileRepository, "users"); + if (Directory.Exists(usersDir)) + { + foreach (var userDir in Directory.GetDirectories(usersDir)) + { + var userAgentFile = Directory.GetFiles(userDir).FirstOrDefault(x => Path.GetFileName(x) == USER_AGENT_FILE); + if (string.IsNullOrEmpty(userAgentFile)) continue; + + var text = File.ReadAllText(userAgentFile); + var userAgents = JsonSerializer.Deserialize>(text, _options); + if (userAgents.IsNullOrEmpty()) continue; + + userAgents = userAgents.Where(x => x.AgentId != agentId).ToList(); + File.WriteAllText(userAgentFile, JsonSerializer.Serialize(userAgents, _options)); + } + } + + // Delete agent folder + Directory.Delete(agentDir, true); + + return true; + } + catch + { + return false; + } + } } } diff --git a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.AgentTask.cs b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.AgentTask.cs index 73a8e3ed..d542b28b 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.AgentTask.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.AgentTask.cs @@ -1,7 +1,5 @@ -using BotSharp.Abstraction.Repositories.Filters; using BotSharp.Abstraction.Tasks.Models; using System.IO; -using System.Threading.Tasks; namespace BotSharp.Core.Repository; @@ -192,19 +190,25 @@ public partial class FileRepository File.WriteAllText(taskFile, fileContent); } - public bool DeleteAgentTask(string agentId, string taskId) + public bool DeleteAgentTask(string agentId, List taskIds) { var agentDir = Path.Combine(_dbSettings.FileRepository, _agentSettings.DataDir, agentId); - if (!Directory.Exists(agentDir)) return false; + if (!Directory.Exists(agentDir) || taskIds.IsNullOrEmpty()) return false; var taskDir = Path.Combine(agentDir, "tasks"); if (!Directory.Exists(taskDir)) return false; - var taskFile = FindTaskFileById(taskDir, taskId); - if (string.IsNullOrWhiteSpace(taskFile)) return false; + var deletedTasks = new List(); + foreach (var taskId in taskIds) + { + var taskFile = FindTaskFileById(taskDir, taskId); + if (string.IsNullOrWhiteSpace(taskFile)) continue; - File.Delete(taskFile); - return true; + File.Delete(taskFile); + deletedTasks.Add(taskId); + } + + return deletedTasks.Any(); } public bool DeleteAgentTasks() diff --git a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Conversation.cs b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Conversation.cs index bb477391..5ccd3777 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Conversation.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Conversation.cs @@ -1,5 +1,4 @@ using BotSharp.Abstraction.Loggers.Models; -using BotSharp.Abstraction.Repositories.Filters; using BotSharp.Abstraction.Repositories.Models; using System.Globalization; using System.IO; @@ -29,36 +28,19 @@ namespace BotSharp.Core.Repository var dialogFile = Path.Combine(dir, DIALOG_FILE); if (!File.Exists(dialogFile)) { - File.WriteAllText(dialogFile, string.Empty); + File.WriteAllText(dialogFile, "[]"); } var stateFile = Path.Combine(dir, STATE_FILE); if (!File.Exists(stateFile)) { - var states = conversation.States ?? new Dictionary(); - var initialStates = states.Select(x => new StateKeyValue - { - Key = x.Key, - Values = new List - { - new StateValue { Data = x.Value, UpdateTime = DateTime.UtcNow } - } - }).ToList(); - File.WriteAllText(stateFile, JsonSerializer.Serialize(initialStates, _options)); + File.WriteAllText(stateFile, JsonSerializer.Serialize(new List(), _options)); } var breakpointFile = Path.Combine(dir, BREAKPOINT_FILE); if (!File.Exists(breakpointFile)) { - var initialBreakpoints = new List - { - new ConversationBreakpoint() - { - Breakpoint = utcNow.AddMilliseconds(-100), - CreatedTime = DateTime.UtcNow - } - }; - File.WriteAllText(breakpointFile, JsonSerializer.Serialize(initialBreakpoints, _options)); + File.WriteAllText(breakpointFile, JsonSerializer.Serialize(new List(), _options)); } } @@ -83,39 +65,20 @@ namespace BotSharp.Core.Repository if (!string.IsNullOrEmpty(convDir)) { var dialogDir = Path.Combine(convDir, DIALOG_FILE); - dialogs = CollectDialogElements(dialogDir); + var texts = File.ReadAllText(dialogDir); + try + { + dialogs = JsonSerializer.Deserialize>(texts, _options) ?? new List(); + } + catch + { + dialogs = new List(); + } } return dialogs; } - public void UpdateConversationDialogElements(string conversationId, List updateElements) - { - var dialogElements = GetConversationDialogs(conversationId); - if (dialogElements.IsNullOrEmpty() || updateElements.IsNullOrEmpty()) return; - - var convDir = FindConversationDirectory(conversationId); - if (!string.IsNullOrEmpty(convDir)) - { - var dialogDir = Path.Combine(convDir, DIALOG_FILE); - if (File.Exists(dialogDir)) - { - var updated = dialogElements.Select((x, idx) => - { - var found = updateElements.FirstOrDefault(e => e.Index == idx); - if (found != null) - { - x.Content = found.UpdateContent; - } - return x; - }).ToList(); - - var texts = ParseDialogElements(updated); - File.WriteAllLines(dialogDir, texts); - } - } - } - public void AppendConversationDialogs(string conversationId, List dialogs) { var convDir = FindConversationDirectory(conversationId); @@ -124,8 +87,18 @@ namespace BotSharp.Core.Repository var dialogFile = Path.Combine(convDir, DIALOG_FILE); if (File.Exists(dialogFile)) { - var texts = ParseDialogElements(dialogs); - File.AppendAllLines(dialogFile, texts); + var prevDialogs = File.ReadAllText(dialogFile); + var elements = JsonSerializer.Deserialize>(prevDialogs, _options); + if (elements != null) + { + elements.AddRange(dialogs); + } + else + { + elements = elements ?? new List(); + } + + File.WriteAllText(dialogFile, JsonSerializer.Serialize(elements, _options)); } var convFile = Path.Combine(convDir, CONVERSATION_FILE); @@ -160,7 +133,7 @@ namespace BotSharp.Core.Repository } } - public void UpdateConversationBreakpoint(string conversationId, string messageId, DateTime breakpoint) + public void UpdateConversationBreakpoint(string conversationId, ConversationBreakpoint breakpoint) { var convDir = FindConversationDirectory(conversationId); if (!string.IsNullOrEmpty(convDir)) @@ -178,9 +151,10 @@ namespace BotSharp.Core.Repository { new ConversationBreakpoint { - MessageId = messageId, - Breakpoint = breakpoint, - CreatedTime = DateTime.UtcNow + MessageId = breakpoint.MessageId, + Breakpoint = breakpoint.Breakpoint, + Reason = breakpoint.Reason, + CreatedTime = DateTime.UtcNow, } }; @@ -197,12 +171,12 @@ namespace BotSharp.Core.Repository } } - public DateTime GetConversationBreakpoint(string conversationId) + public ConversationBreakpoint? GetConversationBreakpoint(string conversationId) { var convDir = FindConversationDirectory(conversationId); if (string.IsNullOrEmpty(convDir)) { - return default; + return null; } var breakpointFile = Path.Combine(convDir, BREAKPOINT_FILE); @@ -214,7 +188,7 @@ namespace BotSharp.Core.Repository var content = File.ReadAllText(breakpointFile); var records = JsonSerializer.Deserialize>(content, _options); - return records?.LastOrDefault()?.Breakpoint ?? default; + return records?.LastOrDefault(); } public ConversationState GetConversationStates(string conversationId) @@ -463,24 +437,40 @@ namespace BotSharp.Core.Repository } - public bool TruncateConversation(string conversationId, string messageId, bool cleanLog = false) + public IEnumerable TruncateConversation(string conversationId, string messageId, bool cleanLog = false) { - if (string.IsNullOrEmpty(conversationId) || string.IsNullOrEmpty(messageId)) return false; + var deletedMessageIds = new List(); + if (string.IsNullOrEmpty(conversationId) || string.IsNullOrEmpty(messageId)) + { + return deletedMessageIds; + } var dialogs = new List(); + var convDir = FindConversationDirectory(conversationId); - if (string.IsNullOrEmpty(convDir)) return false; + if (string.IsNullOrEmpty(convDir)) + { + return deletedMessageIds; + } var dialogDir = Path.Combine(convDir, DIALOG_FILE); dialogs = CollectDialogElements(dialogDir); - if (dialogs.IsNullOrEmpty()) return false; + if (dialogs.IsNullOrEmpty()) + { + return deletedMessageIds; + } var foundIdx = dialogs.FindIndex(x => x.MetaData?.MessageId == messageId); - if (foundIdx < 0) return false; + if (foundIdx < 0) + { + return deletedMessageIds; + } + + deletedMessageIds = dialogs.Where((x, idx) => idx >= foundIdx && !string.IsNullOrEmpty(x.MetaData?.MessageId)) + .Select(x => x.MetaData.MessageId).Distinct().ToList(); // Handle truncated dialogs var isSaved = HandleTruncatedDialogs(convDir, dialogDir, dialogs, foundIdx); - if (!isSaved) return false; // Handle truncated states var refTime = dialogs.ElementAt(foundIdx).MetaData.CreateTime; @@ -491,7 +481,7 @@ namespace BotSharp.Core.Repository // Handle truncated breakpoints var breakpointDir = Path.Combine(convDir, BREAKPOINT_FILE); var breakpoints = CollectConversationBreakpoints(breakpointDir); - isSaved = HandleTruncatedBreakpoints(breakpointDir, breakpoints, messageId); + isSaved = HandleTruncatedBreakpoints(breakpointDir, breakpoints, refTime); // Remove logs if (cleanLog) @@ -499,7 +489,7 @@ namespace BotSharp.Core.Repository HandleTruncatedLogs(convDir, refTime); } - return isSaved; + return deletedMessageIds; } @@ -520,47 +510,16 @@ namespace BotSharp.Core.Repository if (!File.Exists(dialogDir)) return dialogs; - var rawDialogs = File.ReadAllLines(dialogDir); - if (!rawDialogs.IsNullOrEmpty()) - { - for (int i = 0; i < rawDialogs.Count(); i += 2) - { - var blocks = rawDialogs[i].Split("|"); - var content = rawDialogs[i + 1]; - var trimmed = content.Substring(4); - var meta = new DialogMetaData - { - Role = blocks[1], - AgentId = blocks[2], - MessageId = blocks[3], - SenderId = !string.IsNullOrWhiteSpace(blocks[4]) ? blocks[4] : null, - FunctionName = !string.IsNullOrWhiteSpace(blocks[5]) ? blocks[5] : null, - CreateTime = DateTime.Parse(blocks[0]) - }; - - var richContent = blocks.Count() > 6 ? blocks[6] : null; - dialogs.Add(new DialogElement(meta, trimmed, richContent)); - } - } + var texts = File.ReadAllText(dialogDir); + dialogs = JsonSerializer.Deserialize>(texts) ?? new List(); return dialogs; } - private List ParseDialogElements(List dialogs) + private string ParseDialogElements(List dialogs) { - var dialogTexts = new List(); - if (dialogs.IsNullOrEmpty()) return dialogTexts; + if (dialogs.IsNullOrEmpty()) return "[]"; - foreach (var element in dialogs) - { - var meta = element.MetaData; - var createTime = meta.CreateTime.ToString("MM/dd/yyyy hh:mm:ss.ffffff tt", CultureInfo.InvariantCulture); - var metaStr = $"{createTime}|{meta.Role}|{meta.AgentId}|{meta.MessageId}|{meta.SenderId}|{meta.FunctionName}|{element.RichContent}"; - dialogTexts.Add(metaStr); - var content = $" - {element.Content}"; - dialogTexts.Add(content); - } - - return dialogTexts; + return JsonSerializer.Serialize(dialogs, _options) ?? "[]"; } private List CollectConversationStates(string stateFile) @@ -626,10 +585,9 @@ namespace BotSharp.Core.Repository return isSaved; } - private bool HandleTruncatedBreakpoints(string breakpointDir, List breakpoints, string refMessageId) + private bool HandleTruncatedBreakpoints(string breakpointDir, List breakpoints, DateTime refTime) { - var targetIdx = breakpoints.FindIndex(x => x.MessageId == refMessageId); - var truncatedBreakpoints = breakpoints?.Where((x, idx) => idx < targetIdx)? + var truncatedBreakpoints = breakpoints?.Where(x => x.CreatedTime < refTime)? .ToList() ?? new List(); var isSaved = SaveTruncatedBreakpoints(breakpointDir, truncatedBreakpoints); @@ -680,7 +638,7 @@ namespace BotSharp.Core.Repository if (!File.Exists(dialogDir)) File.Create(dialogDir); var texts = ParseDialogElements(dialogs); - File.WriteAllLines(dialogDir, texts); + File.WriteAllText(dialogDir, texts); return true; } @@ -703,6 +661,24 @@ namespace BotSharp.Core.Repository File.WriteAllText(breakpointDir, breakpointStr); return true; } + + private string? EncodeText(string? text) + { + if (string.IsNullOrEmpty(text)) return text; + + var bytes = Encoding.UTF8.GetBytes(text); + var encoded = Convert.ToBase64String(bytes); + return encoded; + } + + private string? DecodeText(string? text) + { + if (string.IsNullOrEmpty(text)) return text; + + var decoded = Convert.FromBase64String(text); + var origin = Encoding.UTF8.GetString(decoded); + return origin; + } #endregion } } diff --git a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.User.cs b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.User.cs index bc992a08..e7566002 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.User.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.User.cs @@ -1,3 +1,4 @@ +using BotSharp.Abstraction.Users.Enums; using BotSharp.Abstraction.Users.Models; using System.IO; @@ -32,4 +33,13 @@ public partial class FileRepository var path = Path.Combine(dir, "user.json"); File.WriteAllText(path, JsonSerializer.Serialize(user, _options)); } + + public void UpdateUserVerified(string userId) + { + var user = GetUserById(userId); + user.Verified = true; + var dir = Path.Combine(_dbSettings.FileRepository, "users", user.Id); + var path = Path.Combine(dir, "user.json"); + File.WriteAllText(path, JsonSerializer.Serialize(user, _options)); + } } diff --git a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.cs b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.cs index 2d2c7889..2094b821 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.cs @@ -27,7 +27,7 @@ public partial class FileRepository : IBotSharpRepository private const string USER_AGENT_FILE = "agents.json"; private const string CONVERSATION_FILE = "conversation.json"; private const string STATS_FILE = "stats.json"; - private const string DIALOG_FILE = "dialogs.txt"; + private const string DIALOG_FILE = "dialogs.json"; private const string STATE_FILE = "state.json"; private const string BREAKPOINT_FILE = "breakpoint.json"; private const string EXECUTION_LOG_FILE = "execution.log"; diff --git a/src/Infrastructure/BotSharp.Core/Repository/RepositoryPlugin.cs b/src/Infrastructure/BotSharp.Core/Repository/RepositoryPlugin.cs index ee397e38..3160597a 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/RepositoryPlugin.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/RepositoryPlugin.cs @@ -1,3 +1,4 @@ +using BotSharp.Abstraction.Repositories.Enums; using BotSharp.Abstraction.Settings; using Microsoft.Extensions.Configuration; @@ -32,7 +33,7 @@ public class RepositoryPlugin : IBotSharpPlugin var myDatabaseSettings = new BotSharpDatabaseSettings(); config.Bind("Database", myDatabaseSettings); - if (myDatabaseSettings.Default == "FileRepository") + if (myDatabaseSettings.Default == RepositoryEnum.FileRepository) { services.AddScoped(); } diff --git a/src/Infrastructure/BotSharp.Core/Routing/Functions/FallbackToRouterFn.cs b/src/Infrastructure/BotSharp.Core/Routing/Functions/FallbackToRouterFn.cs index 59685719..16deb178 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Functions/FallbackToRouterFn.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Functions/FallbackToRouterFn.cs @@ -34,7 +34,7 @@ public class FallbackToRouterFn : IFunctionCallback routing.Context.Replace(targetAgent.Id); message.CurrentAgentId = targetAgent.Id; - var response = await routing.InstructLoop(message, dialogs); + var response = await routing.InstructLoop(message, dialogs, null); message.Content = response.Content; message.StopCompletion = true; diff --git a/src/Infrastructure/BotSharp.Core/Routing/Functions/HumanInterventionNeededFn.cs b/src/Infrastructure/BotSharp.Core/Routing/Functions/HumanInterventionNeededFn.cs new file mode 100644 index 00000000..d0c26be0 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core/Routing/Functions/HumanInterventionNeededFn.cs @@ -0,0 +1,29 @@ +using BotSharp.Abstraction.Functions; + +namespace BotSharp.Core.Routing.Functions; + +public class HumanInterventionNeededFn : IFunctionCallback +{ + public string Name => "human_intervention_needed"; + + private readonly IServiceProvider _services; + + public HumanInterventionNeededFn(IServiceProvider services) + { + _services = services; + } + + public async Task Execute(RoleDialogModel message) + { + var hooks = _services.GetServices() + .OrderBy(x => x.Priority) + .ToList(); + + foreach (var hook in hooks) + { + await hook.OnHumanInterventionNeeded(message); + } + + return true; + } +} diff --git a/src/Infrastructure/BotSharp.Core/Routing/Functions/RouteToAgentFn.cs b/src/Infrastructure/BotSharp.Core/Routing/Functions/RouteToAgentFn.cs index 6eaaef3a..639c907b 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Functions/RouteToAgentFn.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Functions/RouteToAgentFn.cs @@ -1,4 +1,5 @@ using BotSharp.Abstraction.Functions; +using BotSharp.Abstraction.Infrastructures.Enums; using BotSharp.Abstraction.Routing.Models; namespace BotSharp.Core.Routing; @@ -50,7 +51,7 @@ public partial class RouteToAgentFn : IFunctionCallback var originalAgent = db.GetAgents(filter).FirstOrDefault(); if (originalAgent != null) { - _context.Push(originalAgent.Id, $"user goal agent{(correctToOriginalAgent ? " & is corrected" : "")}"); + _context.Push(originalAgent.Id, $"user goal agent{(correctToOriginalAgent ? " " + originalAgent.Name + " & is corrected" : "")}"); } } @@ -58,7 +59,7 @@ public partial class RouteToAgentFn : IFunctionCallback if (!string.IsNullOrEmpty(args.AgentName) && args.AgentName.Length < 32) { _context.Push(args.AgentName, args.NextActionReason); - states.SetState("next_action_agent", args.AgentName, isNeedVersion: true); + states.SetState(StateConst.NEXT_ACTION_AGENT, args.AgentName, isNeedVersion: true); } if (string.IsNullOrEmpty(args.AgentName)) @@ -82,11 +83,11 @@ public partial class RouteToAgentFn : IFunctionCallback } var routing = _services.GetRequiredService(); - var missingfield = routing.HasMissingRequiredField(message, out var agentId); + var (missingfield, reason) = routing.HasMissingRequiredField(message, out var agentId); if (missingfield && message.CurrentAgentId != agentId) { // Stack redirection agent - _context.Push(agentId, reason: $"REDIRECTION {message.Content}"); + _context.Push(agentId, reason: $"REDIRECTION {reason}"); } } diff --git a/src/Infrastructure/BotSharp.Core/Routing/Handlers/ConversationEndRoutingHandler.cs b/src/Infrastructure/BotSharp.Core/Routing/Handlers/ConversationEndRoutingHandler.cs deleted file mode 100644 index bbd4fbb1..00000000 --- a/src/Infrastructure/BotSharp.Core/Routing/Handlers/ConversationEndRoutingHandler.cs +++ /dev/null @@ -1,49 +0,0 @@ -using BotSharp.Abstraction.Routing.Settings; - -namespace BotSharp.Core.Routing.Handlers; - -public class ConversationEndRoutingHandler : RoutingHandlerBase, IRoutingHandler -{ - public string Name => "conversation_end"; - - public string Description => "User completed his task and wants to end the conversation."; - - public List Parameters => new List - { - new ParameterPropertyDef("reason", "why end conversation"), - new ParameterPropertyDef("response", "response content to user") - }; - - public List Planers => new List - { - }; - - public ConversationEndRoutingHandler(IServiceProvider services, ILogger logger, RoutingSettings settings) - : base(services, logger, settings) - { - } - - public async Task Handle(IRoutingService routing, FunctionCallFromLlm inst, RoleDialogModel message) - { - var response = new RoleDialogModel(AgentRole.Assistant, inst.Response) - { - CurrentAgentId = message.CurrentAgentId, - MessageId = message.MessageId, - StopCompletion = true, - FunctionName = inst.Function - }; - - _dialogs.Add(response); - - var hooks = _services.GetServices() - .OrderBy(x => x.Priority) - .ToList(); - - foreach (var hook in hooks) - { - await hook.OnConversationEnding(response); - } - - return true; - } -} diff --git a/src/Infrastructure/BotSharp.Core/Routing/Handlers/HumanInterventionNeededHandler.cs b/src/Infrastructure/BotSharp.Core/Routing/Handlers/HumanInterventionNeededHandler.cs deleted file mode 100644 index f0de3a9c..00000000 --- a/src/Infrastructure/BotSharp.Core/Routing/Handlers/HumanInterventionNeededHandler.cs +++ /dev/null @@ -1,43 +0,0 @@ -using BotSharp.Abstraction.Routing.Settings; - -namespace BotSharp.Core.Routing.Handlers; - -public class HumanInterventionNeededHandler : RoutingHandlerBase, IRoutingHandler -{ - public string Name => "human_intervention_needed"; - - public string Description => "Reach out to human being, customer service or customer representative."; - - public List Parameters => new List - { - new ParameterPropertyDef("reason", "why need customer service"), - new ParameterPropertyDef("summary", "the whole conversation summary with important information"), - new ParameterPropertyDef("response", "asking user whether to connect with customer service representative") - }; - - public HumanInterventionNeededHandler(IServiceProvider services, ILogger logger, RoutingSettings settings) - : base(services, logger, settings) - { - - } - - public async Task Handle(IRoutingService routing, FunctionCallFromLlm inst, RoleDialogModel message) - { - var response = RoleDialogModel.From(message, - role: AgentRole.Assistant, - content: inst.Response); - - _dialogs.Add(response); - - var hooks = _services.GetServices() - .OrderBy(x => x.Priority) - .ToList(); - - foreach (var hook in hooks) - { - await hook.OnHumanInterventionNeeded(response); - } - - return true; - } -} diff --git a/src/Infrastructure/BotSharp.Core/Routing/Handlers/ResponseToUserRoutingHandler.cs b/src/Infrastructure/BotSharp.Core/Routing/Handlers/ResponseToUserRoutingHandler.cs deleted file mode 100644 index ad6eaf8d..00000000 --- a/src/Infrastructure/BotSharp.Core/Routing/Handlers/ResponseToUserRoutingHandler.cs +++ /dev/null @@ -1,36 +0,0 @@ -using BotSharp.Abstraction.Routing.Settings; - -namespace BotSharp.Core.Routing.Handlers; - -public class ResponseToUserRoutingHandler : RoutingHandlerBase, IRoutingHandler -{ - public string Name => "response_to_user"; - - public string Description => "When you can handle the conversation without asking specific agent."; - - public List Parameters => new List - { - new ParameterPropertyDef("reason", "why response to user directly without go to other agents"), - new ParameterPropertyDef("response", "response content to user in courteous words. If the user wants to end the conversation, you must set conversation_end to true and response politely."), - new ParameterPropertyDef("conversation_end", "whether to end this conversation, true or false", type: "boolean") - }; - - public ResponseToUserRoutingHandler(IServiceProvider services, ILogger logger, RoutingSettings settings) - : base(services, logger, settings) - { - } - - public async Task Handle(IRoutingService routing, FunctionCallFromLlm inst, RoleDialogModel message) - { - var response = new RoleDialogModel(AgentRole.Assistant, inst.Response) - { - CurrentAgentId = message.CurrentAgentId, - MessageId = message.MessageId, - StopCompletion = true - }; - - _dialogs.Add(response); - - return true; - } -} diff --git a/src/Infrastructure/BotSharp.Core/Routing/Handlers/RetrieveDataFromAgentRoutingHandler.cs b/src/Infrastructure/BotSharp.Core/Routing/Handlers/RetrieveDataFromAgentRoutingHandler.cs index ad4d3cab..90d754dc 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Handlers/RetrieveDataFromAgentRoutingHandler.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Handlers/RetrieveDataFromAgentRoutingHandler.cs @@ -34,7 +34,7 @@ public class RetrieveDataFromAgentRoutingHandler : RoutingHandlerBase, IRoutingH { } - public async Task Handle(IRoutingService routing, FunctionCallFromLlm inst, RoleDialogModel message) + public async Task Handle(IRoutingService routing, FunctionCallFromLlm inst, RoleDialogModel message, Func onFunctionExecuting) { var context = _services.GetRequiredService(); var agentId = context.GetCurrentAgentId(); @@ -47,7 +47,7 @@ public class RetrieveDataFromAgentRoutingHandler : RoutingHandlerBase, IRoutingH } }; - var ret = await routing.InvokeAgent(agentId, dialogs); + var ret = await routing.InvokeAgent(agentId, dialogs, onFunctionExecuting); var response = dialogs.Last(); inst.Response = response.Content; diff --git a/src/Infrastructure/BotSharp.Core/Routing/Handlers/RouteToAgentRoutingHandler.cs b/src/Infrastructure/BotSharp.Core/Routing/Handlers/RouteToAgentRoutingHandler.cs index cfbfe31e..9f768696 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Handlers/RouteToAgentRoutingHandler.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Handlers/RouteToAgentRoutingHandler.cs @@ -11,26 +11,24 @@ public class RouteToAgentRoutingHandler : RoutingHandlerBase, IRoutingHandler public List Parameters => new List { - new ParameterPropertyDef("next_action_reason", "the reason why route to this virtual agent") - { - Required = true - }, - new ParameterPropertyDef("next_action_agent", "agent for next action based on user latest response, if user is replying last agent's question, you must route to this agent") - { - Required = true - }, - new ParameterPropertyDef("user_goal_description", "user goal based on user initial task.") - { - Required = true - }, - new ParameterPropertyDef("user_goal_agent", "agent who can acheive user initial task, must align with user_goal_description ") - { - Required = true - }, - new ParameterPropertyDef("args", "useful parameters of next action agent, format: { }") - { - Type = "object" - } + new ParameterPropertyDef("next_action_reason", + "the reason why route to this virtual agent.", + required: true), + new ParameterPropertyDef("next_action_agent", + "agent for next action based on user latest response, if user is replying last agent's question, you must route to this agent.", + required: true), + new ParameterPropertyDef("args", + "useful parameters of next action agent, format: { }", + type: "object"), + new ParameterPropertyDef("user_goal_description", + "user goal based on user initial task.", + required: true), + new ParameterPropertyDef("user_goal_agent", + "agent who can acheive user initial task, must align with user_goal_description.", + required: true), + new ParameterPropertyDef("is_new_task", + "whether the user is requesting a new task that is different from the previous topic.", + type: "boolean") }; public RouteToAgentRoutingHandler(IServiceProvider services, ILogger logger, RoutingSettings settings) @@ -38,7 +36,7 @@ public class RouteToAgentRoutingHandler : RoutingHandlerBase, IRoutingHandler { } - public async Task Handle(IRoutingService routing, FunctionCallFromLlm inst, RoleDialogModel message) + public async Task Handle(IRoutingService routing, FunctionCallFromLlm inst, RoleDialogModel message, Func onFunctionExecuting) { var states = _services.GetRequiredService(); var goalAgent = states.GetState(StateConst.EXPECTED_GOAL_AGENT); @@ -51,6 +49,13 @@ public class RouteToAgentRoutingHandler : RoutingHandlerBase, IRoutingHandler ); } + if (inst.IsNewTask) + { + await HookEmitter.Emit(_services, async hook => + await hook.OnNewTaskDetected(message, inst.NextActionReason) + ); + } + message.FunctionArgs = JsonSerializer.Serialize(inst); var ret = await routing.InvokeFunction(message.FunctionName, message); @@ -77,7 +82,7 @@ public class RouteToAgentRoutingHandler : RoutingHandlerBase, IRoutingHandler } else { - ret = await routing.InvokeAgent(agentId, _dialogs); + ret = await routing.InvokeAgent(agentId, _dialogs, onFunctionExecuting); } var response = _dialogs.Last(); diff --git a/src/Infrastructure/BotSharp.Core/Routing/Handlers/TaskCompletedRoutingHandler.cs b/src/Infrastructure/BotSharp.Core/Routing/Handlers/TaskCompletedRoutingHandler.cs deleted file mode 100644 index 21b7ea13..00000000 --- a/src/Infrastructure/BotSharp.Core/Routing/Handlers/TaskCompletedRoutingHandler.cs +++ /dev/null @@ -1,64 +0,0 @@ -using BotSharp.Abstraction.Routing.Settings; -using BotSharp.Core.Routing.Planning; - -namespace BotSharp.Core.Routing.Handlers; - -public class TaskCompletedRoutingHandler : RoutingHandlerBase, IRoutingHandler -{ - public string Name => "task_completed"; - - public string Description => "User task is completed."; - - public List Parameters => new List - { - new ParameterPropertyDef("reason", "why the task is completed") - { - Required = true - }, - new ParameterPropertyDef("response", "polite response when the task is completed") - { - Required = true - }, - new ParameterPropertyDef("conversation_end", "whether to end this conversation, true or false") - { - Required = true, - Type = "boolean" - }, - new ParameterPropertyDef("abandoned_arguments", "the arguments next task can't reuse") - }; - - public List Planers => new List - { - nameof(HFPlanner) - }; - - public TaskCompletedRoutingHandler(IServiceProvider services, ILogger logger, RoutingSettings settings) - : base(services, logger, settings) - { - } - - public async Task Handle(IRoutingService routing, FunctionCallFromLlm inst, RoleDialogModel message) - { - var response = new RoleDialogModel(AgentRole.Assistant, inst.Response) - { - CurrentAgentId = message.CurrentAgentId, - MessageId = message.MessageId, - StopCompletion = true, - FunctionName = inst.Function, - Instruction = inst, - }; - - _dialogs.Add(response); - - var hooks = _services.GetServices() - .OrderBy(x => x.Priority) - .ToList(); - - foreach (var hook in hooks) - { - await hook.OnTaskCompleted(response); - } - - return true; - } -} diff --git a/src/Infrastructure/BotSharp.Core/Routing/Hooks/RoutingAgentHook.cs b/src/Infrastructure/BotSharp.Core/Routing/Hooks/RoutingAgentHook.cs index fa8c5db3..a847f078 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Hooks/RoutingAgentHook.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Hooks/RoutingAgentHook.cs @@ -1,8 +1,6 @@ -using BotSharp.Abstraction.Functions.Models; -using BotSharp.Abstraction.Routing; +using BotSharp.Abstraction.Functions; using BotSharp.Abstraction.Routing.Enums; using BotSharp.Abstraction.Routing.Settings; -using System.Diagnostics.Metrics; namespace BotSharp.Core.Routing.Hooks; diff --git a/src/Infrastructure/BotSharp.Core/Routing/Planning/InstructExecutor.cs b/src/Infrastructure/BotSharp.Core/Routing/Planning/InstructExecutor.cs index 0f436edd..5b1c8fcb 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Planning/InstructExecutor.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Planning/InstructExecutor.cs @@ -1,5 +1,3 @@ -using BotSharp.Abstraction.Functions.Models; -using BotSharp.Abstraction.Routing; using BotSharp.Abstraction.Routing.Planning; namespace BotSharp.Core.Routing.Planning; @@ -18,7 +16,8 @@ public class InstructExecutor : IExecutor public async Task Execute(IRoutingService routing, FunctionCallFromLlm inst, RoleDialogModel message, - List dialogs) + List dialogs, + Func onFunctionExecuting) { message.Instruction = inst; @@ -26,7 +25,7 @@ public class InstructExecutor : IExecutor var handler = handlers.FirstOrDefault(x => x.Name == inst.Function); handler.SetDialogs(dialogs); - var handled = await handler.Handle(routing, inst, message); + var handled = await handler.Handle(routing, inst, message, onFunctionExecuting); // For client display purpose var response = dialogs.Last(); diff --git a/src/Infrastructure/BotSharp.Core/Routing/Planning/NaivePlanner.cs b/src/Infrastructure/BotSharp.Core/Routing/Planning/NaivePlanner.cs index eefb88f7..aeed1a78 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Planning/NaivePlanner.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Planning/NaivePlanner.cs @@ -165,6 +165,22 @@ public class NaivePlanner : IPlaner malformed = true; } + // Agent Name is contaminated. + if (args.Function == "route_to_agent") + { + // Action agent name + if (!agents.Any(x => x.Name == args.AgentName)) + { + args.AgentName = agents.FirstOrDefault(x => args.AgentName.Contains(x.Name))?.Name ?? args.AgentName; + } + + // Goal agent name + if (!agents.Any(x => x.Name == args.OriginalAgent)) + { + args.OriginalAgent = agents.FirstOrDefault(x => args.OriginalAgent.Contains(x.Name))?.Name ?? args.OriginalAgent; + } + } + if (malformed) { _logger.LogWarning($"Captured LLM malformed response"); diff --git a/src/Infrastructure/BotSharp.Core/Routing/RoutingContext.cs b/src/Infrastructure/BotSharp.Core/Routing/RoutingContext.cs index 87e55840..31caa932 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/RoutingContext.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/RoutingContext.cs @@ -1,4 +1,5 @@ using BotSharp.Abstraction.Routing.Settings; +using BotSharp.Abstraction.Utilities; namespace BotSharp.Core.Routing; @@ -117,6 +118,7 @@ public class RoutingContext : IRoutingContext var message = new RoleDialogModel(AgentRole.User, $"Try to route to agent {agent.Name}") { + CurrentAgentId = currentAgentId, FunctionName = "route_to_agent", FunctionArgs = JsonSerializer.Serialize(new FunctionCallFromLlm { @@ -127,7 +129,7 @@ public class RoutingContext : IRoutingContext }; var routing = _services.GetRequiredService(); - var missingfield = routing.HasMissingRequiredField(message, out agentId); + var (missingfield, _) = routing.HasMissingRequiredField(message, out agentId); if (missingfield) { if (currentAgentId != agentId) @@ -137,7 +139,18 @@ public class RoutingContext : IRoutingContext } } - public string PreviousAgentId() + public void PopTo(string agentId, string reason) + { + var currentAgentId = GetCurrentAgentId(); + while (!string.IsNullOrEmpty(currentAgentId) && + currentAgentId != agentId) + { + Pop(reason); + currentAgentId = GetCurrentAgentId(); + } + } + + public string FirstGoalAgentId() { if (_stack.Count == 1) { @@ -151,6 +164,11 @@ public class RoutingContext : IRoutingContext return string.Empty; } + public bool ContainsAgentId(string agentId) + { + return _stack.ToArray().Contains(agentId); + } + public void Replace(string agentId, string? reason = null) { var fromAgent = agentId; diff --git a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.GetConversationContent.cs b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.GetConversationContent.cs index bd883314..2757d5d8 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.GetConversationContent.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.GetConversationContent.cs @@ -16,7 +16,7 @@ public partial class RoutingService role = agent.Name; } - conversation += $"{role}: {dialog.Content}\r\n"; + conversation += $"{role}: {dialog.Payload ?? dialog.Content}\r\n"; } return conversation; diff --git a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.HasMissingRequiredField.cs b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.HasMissingRequiredField.cs index 20d75a95..60f3f331 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.HasMissingRequiredField.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.HasMissingRequiredField.cs @@ -1,3 +1,4 @@ +using BotSharp.Abstraction.Conversations.Enums; using BotSharp.Abstraction.Routing.Models; using System.Drawing; @@ -9,8 +10,9 @@ public partial class RoutingService /// If the target agent needs some required fields but the /// /// - public bool HasMissingRequiredField(RoleDialogModel message, out string agentId) + public (bool, string) HasMissingRequiredField(RoleDialogModel message, out string agentId) { + var reason = string.Empty; var args = JsonSerializer.Deserialize(message.FunctionArgs); var routing = _services.GetRequiredService(); @@ -19,7 +21,7 @@ public partial class RoutingService if (routingRules == null || !routingRules.Any()) { agentId = message.CurrentAgentId; - return false; + return (false, reason); } agentId = routingRules.First().AgentId; @@ -49,6 +51,17 @@ public partial class RoutingService if (!string.IsNullOrEmpty(states.GetState(field))) { var value = states.GetState(field); + + // Check if the value is correct data type + var rule = routingRules.First(x => x.Field == field); + if (rule.FieldType == "number") + { + if (!long.TryParse(value, out var longValue)) + { + states.SetState(field, "", isNeedVersion: true, source: StateSource.Application); + continue; + } + } message.FunctionArgs = AppendPropertyToArgs(message.FunctionArgs, field, value); missingFields.Remove(field); } @@ -56,9 +69,13 @@ public partial class RoutingService if (missingFields.Any()) { + var logger = _services.GetRequiredService>(); + // Add field to args message.FunctionArgs = AppendPropertyToArgs(message.FunctionArgs, "missing_fields", missingFields); - message.Content = $"missing some information: {string.Join(", ", missingFields)}"; + reason = $"missing some information: {string.Join(", ", missingFields)}"; + // message.Content = reason; + logger.LogWarning(reason); // Handle redirect var routingRule = routingRules.FirstOrDefault(x => missingFields.Contains(x.Field)); @@ -70,7 +87,6 @@ public partial class RoutingService // Add redirected agent message.FunctionArgs = AppendPropertyToArgs(message.FunctionArgs, "redirect_to", record.Name); agentId = routingRule.RedirectTo; - var logger = _services.GetRequiredService>(); #if DEBUG Console.WriteLine($"*** Routing redirect to {record.Name.ToUpper()} ***", Color.Yellow); #else @@ -84,7 +100,7 @@ public partial class RoutingService } } - return missingFields.Any(); + return (missingFields.Any(), reason); } private string AppendPropertyToArgs(string args, string key, string value) diff --git a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs index 38912ef7..6046c5bb 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs @@ -5,7 +5,7 @@ namespace BotSharp.Core.Routing; public partial class RoutingService { private int _currentRecursionDepth = 0; - public async Task InvokeAgent(string agentId, List dialogs) + public async Task InvokeAgent(string agentId, List dialogs, Func onFunctionExecuting) { var agentService = _services.GetRequiredService(); var agent = await agentService.LoadAgent(agentId); @@ -17,8 +17,19 @@ public partial class RoutingService return false; } + var provider = agent.LlmConfig.Provider; + var model = agent.LlmConfig.Model; + + if (provider == null || model == null) + { + var agentSettings = _services.GetRequiredService(); + provider = agentSettings.LlmConfig.Provider; + model = agentSettings.LlmConfig.Model; + } + var chatCompletion = CompletionProvider.GetChatCompletion(_services, - agentConfig: agent.LlmConfig); + provider: provider, + model: model); var message = dialogs.Last(); var response = await chatCompletion.GetChatCompletions(agent, dialogs); @@ -31,10 +42,12 @@ public partial class RoutingService { response.FunctionName = response.FunctionName.Split("/").Last(); } + message.ToolCallId = response.ToolCallId; message.FunctionName = response.FunctionName; message.FunctionArgs = response.FunctionArgs; message.CurrentAgentId = agent.Id; - await InvokeFunction(message, dialogs); + + await InvokeFunction(message, dialogs, onFunctionExecuting); } else { @@ -54,7 +67,7 @@ public partial class RoutingService return true; } - private async Task InvokeFunction(RoleDialogModel message, List dialogs) + private async Task InvokeFunction(RoleDialogModel message, List dialogs, Func? onFunctionExecuting = null) { // execute function // Save states @@ -63,7 +76,7 @@ public partial class RoutingService var routing = _services.GetRequiredService(); // Call functions - await routing.InvokeFunction(message.FunctionName, message); + await routing.InvokeFunction(message.FunctionName, message, onFunctionExecuting); // Pass execution result to LLM to get response if (!message.StopCompletion) @@ -88,7 +101,7 @@ public partial class RoutingService // Send to Next LLM var agentId = routing.Context.GetCurrentAgentId(); - await InvokeAgent(agentId, dialogs); + await InvokeAgent(agentId, dialogs, onFunctionExecuting); } } else diff --git a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeFunction.cs b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeFunction.cs index 6ad2ae75..2bc11f76 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeFunction.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeFunction.cs @@ -4,45 +4,56 @@ namespace BotSharp.Core.Routing; public partial class RoutingService { - private List _functionCallStack = new List(); - public async Task InvokeFunction(string name, RoleDialogModel message) + public async Task InvokeFunction(string name, RoleDialogModel message, Func? onFunctionExecuting = null) { var function = _services.GetServices().FirstOrDefault(x => x.Name == name); if (function == null) { message.StopCompletion = true; - message.Content = $"Can't find function implementation of {message.FunctionName}."; + message.Content = $"Can't find function implementation of {name}."; _logger.LogError(message.Content); return false; } - var originalFunctionName = message.FunctionName; - message.FunctionName = name; - message.Role = AgentRole.Function; - message.FunctionArgs = message.FunctionArgs; + // Clone message + var clonedMessage = RoleDialogModel.From(message); + clonedMessage.FunctionName = name; var hooks = _services.GetServices() .OrderBy(x => x.Priority) .ToList(); // Before executing functions + clonedMessage.Indication = function.Indication; + if (onFunctionExecuting != null) + { + await onFunctionExecuting(clonedMessage); + } + foreach (var hook in hooks) { - await hook.OnFunctionExecuting(message); + await hook.OnFunctionExecuting(clonedMessage); } bool result = false; try { - result = await function.Execute(message); - _functionCallStack.Add(new FunctionCallingResponse + result = await function.Execute(clonedMessage); + + // After functions have been executed + foreach (var hook in hooks) { - Role = AgentRole.Function, - FunctionName = message.FunctionName, - Args = JsonDocument.Parse(message.FunctionArgs ?? "{}"), - Content = message.Content - }); + await hook.OnFunctionExecuted(clonedMessage); + } + + // Set result to original message + message.PostbackFunctionName = clonedMessage.PostbackFunctionName; + message.CurrentAgentId = clonedMessage.CurrentAgentId; + message.Content = clonedMessage.Content; + message.StopCompletion = clonedMessage.StopCompletion; + message.RichContent = clonedMessage.RichContent; + message.Data = clonedMessage.Data; } catch (JsonException ex) { @@ -63,25 +74,12 @@ public partial class RoutingService message.Content = JsonSerializer.Serialize(message.Data); } - // After functions have been executed - foreach (var hook in hooks) - { - await hook.OnFunctionExecuted(message); - } - - // restore original function name - if (!message.StopCompletion && - message.FunctionName != originalFunctionName) - { - message.FunctionName = originalFunctionName; - } - // Save to Storage as well - if (!message.StopCompletion && message.FunctionName != "route_to_agent") + /*if (!message.StopCompletion && message.FunctionName != "route_to_agent") { var storage = _services.GetRequiredService(); storage.Append(Context.ConversationId, message); - } + }*/ return result; } diff --git a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.cs b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.cs index 4a17f67a..1a9ce427 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.cs @@ -1,3 +1,4 @@ +using BotSharp.Abstraction.Infrastructures.Enums; using BotSharp.Abstraction.Routing.Models; using BotSharp.Abstraction.Routing.Planning; using BotSharp.Abstraction.Routing.Settings; @@ -39,15 +40,10 @@ public partial class RoutingService : IRoutingService var handler = handlers.FirstOrDefault(x => x.Name == "route_to_agent"); var conv = _services.GetRequiredService(); - var dialogs = new List(); - if (conv.States.GetState("hide_context", "false") == "true") - { - dialogs.Add(message); - } - else - { - dialogs = conv.GetDialogHistory(); - } + var storage = _services.GetRequiredService(); + storage.Append(conv.ConversationId, message); + + var dialogs = conv.GetDialogHistory(); handler.SetDialogs(dialogs); var inst = new FunctionCallFromLlm @@ -60,7 +56,7 @@ public partial class RoutingService : IRoutingService ExecutingDirectly = true }; - var result = await handler.Handle(this, inst, message); + var result = await handler.Handle(this, inst, message, null); var response = dialogs.Last(); response.MessageId = message.MessageId; @@ -69,13 +65,16 @@ public partial class RoutingService : IRoutingService return response; } - public async Task InstructLoop(RoleDialogModel message, List dialogs) + public async Task InstructLoop(RoleDialogModel message, List dialogs, Func onFunctionExecuting) { - var agentService = _services.GetRequiredService(); - _router = await agentService.LoadAgent(message.CurrentAgentId); - RoleDialogModel response = default; + var agentService = _services.GetRequiredService(); + var convService = _services.GetRequiredService(); + var storage = _services.GetRequiredService(); + + _router = await agentService.LoadAgent(message.CurrentAgentId); + var states = _services.GetRequiredService(); var executor = _services.GetRequiredService(); @@ -83,17 +82,32 @@ public partial class RoutingService : IRoutingService _context.Push(_router.Id); - int loopCount = 0; - while (loopCount < planner.MaxLoopCount && !_context.IsEmpty) + // Handle multi-language for input + var agentSettings = _services.GetRequiredService(); + if (agentSettings.EnableTranslator) { - loopCount++; + var translator = _services.GetRequiredService(); - var conversation = await GetConversationContent(dialogs); - _router.TemplateDict["conversation"] = conversation; + var language = states.GetState(StateConst.LANGUAGE, LanguageType.ENGLISH); + if (language != LanguageType.ENGLISH) + { + message.SecondaryContent = message.Content; + message.Content = await translator.Translate(_router, message.MessageId, message.Content, + language: LanguageType.ENGLISH, + clone: false); + } + } - // Get instruction from Planner - var inst = await planner.GetNextInstruction(_router, message.MessageId, dialogs); + dialogs.Add(message); + storage.Append(convService.ConversationId, message); + // Get first instruction + _router.TemplateDict["conversation"] = await GetConversationContent(dialogs); + var inst = await planner.GetNextInstruction(_router, message.MessageId, dialogs); + + int loopCount = 1; + while (true) + { await HookEmitter.Emit(_services, async hook => await hook.OnRoutingInstructionReceived(inst, message) ); @@ -112,15 +126,25 @@ public partial class RoutingService : IRoutingService if (inst.HandleDialogsByPlanner) { var dialogWithoutContext = planner.BeforeHandleContext(inst, message, dialogs); - response = await executor.Execute(this, inst, message, dialogWithoutContext); + response = await executor.Execute(this, inst, message, dialogWithoutContext, onFunctionExecuting); planner.AfterHandleContext(dialogs, dialogWithoutContext); } else { - response = await executor.Execute(this, inst, message, dialogs); + response = await executor.Execute(this, inst, message, dialogs, onFunctionExecuting); } await planner.AgentExecuted(_router, inst, response, dialogs); + + if (loopCount >= planner.MaxLoopCount || _context.IsEmpty) + { + break; + } + + // Get next instruction from Planner + _router.TemplateDict["conversation"] = await GetConversationContent(dialogs); + inst = await planner.GetNextInstruction(_router, message.MessageId, dialogs); + loopCount++; } return response; diff --git a/src/Infrastructure/BotSharp.Core/Tasks/Services/AgentTaskService.cs b/src/Infrastructure/BotSharp.Core/Tasks/Services/AgentTaskService.cs index 1e7cdfdf..13ffc646 100644 --- a/src/Infrastructure/BotSharp.Core/Tasks/Services/AgentTaskService.cs +++ b/src/Infrastructure/BotSharp.Core/Tasks/Services/AgentTaskService.cs @@ -72,7 +72,7 @@ public class AgentTaskService : IAgentTaskService public async Task DeleteTask(string agentId, string taskId) { var db = _services.GetRequiredService(); - var isDeleted = db.DeleteAgentTask(agentId, taskId); + var isDeleted = db.DeleteAgentTask(agentId, new List { taskId }); return await Task.FromResult(isDeleted); } } diff --git a/src/Infrastructure/BotSharp.Core/Tasks/TaskPlugin.cs b/src/Infrastructure/BotSharp.Core/Tasks/TaskPlugin.cs index ddf709ec..27c55ab5 100644 --- a/src/Infrastructure/BotSharp.Core/Tasks/TaskPlugin.cs +++ b/src/Infrastructure/BotSharp.Core/Tasks/TaskPlugin.cs @@ -1,5 +1,6 @@ using BotSharp.Abstraction.Plugins.Models; using BotSharp.Abstraction.Tasks; +using BotSharp.Abstraction.Users.Enums; using BotSharp.Core.Tasks.Services; using Microsoft.Extensions.Configuration; @@ -19,7 +20,10 @@ public class TaskPlugin : IBotSharpPlugin public bool AttachMenu(List menu) { var section = menu.First(x => x.Label == "Apps"); - menu.Add(new PluginMenuDef("Task", link: "page/task", icon: "bx bx-task", weight: section.Weight + 8)); + menu.Add(new PluginMenuDef("Task", link: "page/task", icon: "bx bx-task", weight: section.Weight + 8) + { + Roles = new List { UserRole.Admin } + }); return true; } diff --git a/src/Infrastructure/BotSharp.Core/Templating/ResponseTemplateService.cs b/src/Infrastructure/BotSharp.Core/Templating/ResponseTemplateService.cs index 5119bc91..463013bf 100644 --- a/src/Infrastructure/BotSharp.Core/Templating/ResponseTemplateService.cs +++ b/src/Infrastructure/BotSharp.Core/Templating/ResponseTemplateService.cs @@ -1,5 +1,3 @@ -using BotSharp.Abstraction.Repositories; -using BotSharp.Abstraction.Routing.Models; using BotSharp.Abstraction.Templating; using System.Reflection; diff --git a/src/Infrastructure/BotSharp.Core/Templating/TemplateRender.cs b/src/Infrastructure/BotSharp.Core/Templating/TemplateRender.cs index a5aef0f4..33b27177 100644 --- a/src/Infrastructure/BotSharp.Core/Templating/TemplateRender.cs +++ b/src/Infrastructure/BotSharp.Core/Templating/TemplateRender.cs @@ -3,6 +3,7 @@ using BotSharp.Abstraction.Functions.Models; using BotSharp.Abstraction.Models; using BotSharp.Abstraction.Routing.Models; using BotSharp.Abstraction.Templating; +using BotSharp.Abstraction.Translation.Models; using Fluid; namespace BotSharp.Core.Templating; @@ -27,7 +28,10 @@ public class TemplateRender : ITemplateRender _options.MemberAccessStrategy.Register(); _options.MemberAccessStrategy.Register(); _options.MemberAccessStrategy.Register(); + _options.MemberAccessStrategy.Register(); + _options.MemberAccessStrategy.Register(); _options.MemberAccessStrategy.Register(); + _options.MemberAccessStrategy.Register(); } public string Render(string template, Dictionary dict) diff --git a/src/Infrastructure/BotSharp.Core/Translation/TranslationPlugin.cs b/src/Infrastructure/BotSharp.Core/Translation/TranslationPlugin.cs new file mode 100644 index 00000000..448fbd59 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core/Translation/TranslationPlugin.cs @@ -0,0 +1,16 @@ +using BotSharp.Logger.Hooks; +using Microsoft.Extensions.Configuration; + +namespace BotSharp.Core.Translation; + +public class TranslationPlugin : IBotSharpPlugin +{ + public string Id => "a81997c3-5d3a-4f18-bae0-be7a81d233ba"; + public string Name => "Multi-language Translator"; + public string Description => "Output the corresponding language response according to the user language"; + + public void RegisterDI(IServiceCollection services, IConfiguration config) + { + services.AddScoped(); + } +} diff --git a/src/Infrastructure/BotSharp.Core/Translation/TranslationResponseHook.cs b/src/Infrastructure/BotSharp.Core/Translation/TranslationResponseHook.cs new file mode 100644 index 00000000..755aa110 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core/Translation/TranslationResponseHook.cs @@ -0,0 +1,61 @@ +using BotSharp.Abstraction.Agents; +using BotSharp.Abstraction.Infrastructures.Enums; +using BotSharp.Abstraction.Translation; +using System; +using System.Collections.Generic; +using System.Text; + +namespace BotSharp.Logger.Hooks +{ + public class TranslationResponseHook : ConversationHookBase + { + private readonly IServiceProvider _services; + private readonly IConversationStateService _states; + private const string AIAssistant = "01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a"; + + public TranslationResponseHook(IServiceProvider services, + IConversationStateService states) + { + _services = services; + _states = states; + } + public override async Task OnResponseGenerated(RoleDialogModel message) + { + var agentSettings = _services.GetRequiredService(); + if (!agentSettings.EnableTranslator) + { + return; + } + + // Handle multi-language for output + var agentService = _services.GetRequiredService(); + var router = await agentService.LoadAgent(AIAssistant); + var translator = _services.GetRequiredService(); + var language = _states.GetState(StateConst.LANGUAGE, LanguageType.ENGLISH); + if (language != LanguageType.UNKNOWN && language != LanguageType.ENGLISH) + { + if (message.RichContent != null) + { + if (string.IsNullOrEmpty(message.RichContent.Message.Text)) + { + message.RichContent.Message.Text = message.Content; + } + + message.SecondaryRichContent = await translator.Translate(router, + message.MessageId, + message.RichContent, + language: language); + message.SecondaryContent = message.SecondaryRichContent.Message.Text; + } + else + { + message.SecondaryContent = await translator.Translate(router, + message.MessageId, + message.Content, + language: language); + } + } + await base.OnResponseGenerated(message); + } + } +} diff --git a/src/Infrastructure/BotSharp.Core/Translation/TranslationService.cs b/src/Infrastructure/BotSharp.Core/Translation/TranslationService.cs new file mode 100644 index 00000000..97e997c7 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core/Translation/TranslationService.cs @@ -0,0 +1,375 @@ +using Amazon.Runtime.Internal.Transform; +using BotSharp.Abstraction.Infrastructures.Enums; +using BotSharp.Abstraction.MLTasks; +using BotSharp.Abstraction.Options; +using BotSharp.Abstraction.Templating; +using BotSharp.Abstraction.Translation.Models; +using System.Collections; +using System.Reflection; +using System.Text.Encodings.Web; + +namespace BotSharp.Core.Translation; + +public class TranslationService : ITranslationService +{ + private readonly IServiceProvider _services; + private readonly ILogger _logger; + private readonly BotSharpOptions _options; + private Agent _router; + private string _messageId; + private IChatCompletion _completion; + + public TranslationService(IServiceProvider services, + ILogger logger, + BotSharpOptions options) + { + _services = services; + _logger = logger; + _options = options; + } + + public async Task Translate(Agent router, string messageId, T data, string language = "Spanish", bool clone = true) where T : class + { + _router = router; + _messageId = messageId; + + var unique = new HashSet(); + Collect(data, ref unique); + if (unique.IsNullOrEmpty()) + { + return data; + } + + var clonedData = data; + if (clone) + { + clonedData = Clone(data); + if (clonedData == null) + { + return data; + } + } + + // chat completion + _completion = CompletionProvider.GetChatCompletion(_services, + provider: _router?.LlmConfig?.Provider, + model: _router?.LlmConfig?.Model); + var template = _router.Templates.First(x => x.Name == "translation_prompt").Content; + + var keys = unique.ToArray(); + var texts = unique.ToArray() + .Select((text, i) => new TranslationInput + { + Id = i + 1, + Text = text + }).ToList(); + + try + { + var translatedStringList = await InnerTranslate(texts, language, template); + + int retry = 0; + while (translatedStringList.Texts.Length != texts.Count && retry < 3) + { + translatedStringList = await InnerTranslate(texts, language, template); + retry++; + } + + // Override language if it's Unknown, it's used to output the corresponding language. + var states = _services.GetRequiredService(); + if (!states.ContainsState(StateConst.LANGUAGE)) + { + var inputLanguage = string.IsNullOrEmpty(translatedStringList.InputLanguage) ? LanguageType.ENGLISH : translatedStringList.InputLanguage; + states.SetState(StateConst.LANGUAGE, inputLanguage, activeRounds: 1); + } + + var translatedTexts = translatedStringList.Texts; + var map = new Dictionary(); + + for (var i = 0; i < texts.Count; i++) + { + map[keys[i]] = translatedTexts[i].Text; + } + + clonedData = Assign(clonedData, map); + } + catch (Exception ex) + { + _logger.LogError(ex.Message); + } + + return clonedData; + } + + private T Clone(T data) where T : class + { + if (data == null) return data; + + var str = JsonSerializer.Serialize(data, _options.JsonSerializerOptions); + var cloned = JsonSerializer.Deserialize(str, _options.JsonSerializerOptions); + return cloned; + } + + /// + /// Collect unique strings in data + /// + /// + /// + /// + private void Collect(T data, ref HashSet res) where T : class + { + if (data == null) return; + + var dataType = data.GetType(); + if (IsStringType(dataType)) + { + res.Add(data.ToString()); + return; + } + + if (IsDictionaryType(dataType)) + { + return; + } + + if (IsListType(dataType)) + { + var elementType = dataType.IsArray ? dataType.GetElementType() : dataType.GetGenericArguments().FirstOrDefault(); + if (IsStringType(elementType)) + { + foreach (var item in (data as IEnumerable)) + { + if (item == null) continue; + res.Add(item); + } + } + else if (IsTrackToNextLevel(elementType)) + { + foreach (var item in (data as IEnumerable)) + { + if (item == null) continue; + Collect(item, ref res); + } + } + return; + } + + + var props = dataType.GetProperties(); + foreach (var prop in props) + { + var value = prop.GetValue(data, null); + var propType = prop.PropertyType; + var translate = prop.GetCustomAttributes(true).FirstOrDefault(x => x.GetType() == typeof(TranslateAttribute)); + + if (value == null) continue; + + if (IsStringType(propType)) + { + if (translate != null) + { + Collect(value, ref res); + } + } + else if (IsTrackToNextLevel(propType)) + { + if (IsDictionaryType(propType)) + { + Collect(value, ref res); + } + else if (IsListType(propType)) + { + var elementType = propType.IsArray ? propType.GetElementType() : propType.GetGenericArguments().FirstOrDefault(); + if (IsStringType(elementType)) + { + if (translate != null) + { + Collect(value, ref res); + } + } + else if (IsTrackToNextLevel(elementType)) + { + Collect(value, ref res); + } + } + else + { + Collect(value, ref res); + } + } + } + } + + /// + /// Assign translated values to corresponding attributes + /// + /// + /// + /// + /// + private T Assign(T data, Dictionary map) where T : class + { + if (data == null) return data; + + var dataType = data.GetType(); + if (IsStringType(dataType) && map.TryGetValue(data.ToString(), out var target)) + { + return target as T; + } + + if (IsDictionaryType(dataType)) + { + return data; + } + + if (IsListType(dataType)) + { + var elementType = dataType.IsArray ? dataType.GetElementType() : dataType.GetGenericArguments().FirstOrDefault(); + if (IsStringType(elementType)) + { + var list = new List(); + foreach (var item in (data as IEnumerable)) + { + if (map.TryGetValue(item, out target)) + { + list.Add(target); + } + else + { + list.Add(item?.ToString()); + } + } + + data = dataType.IsArray ? list.ToArray() as T : list as T; + } + else if (IsTrackToNextLevel(elementType)) + { + foreach (var item in (data as IEnumerable)) + { + if (item == null) continue; + Assign(item, map); + } + } + return data; + } + + + var props = dataType.GetProperties(); + foreach (var prop in props) + { + var value = prop.GetValue(data, null); + var propType = prop.PropertyType; + var translate = prop.GetCustomAttributes(true).FirstOrDefault(x => x.GetType() == typeof(TranslateAttribute)); + + if (value == null) continue; + + if (IsStringType(propType)) + { + if (translate != null) + { + prop.SetValue(data, Assign(value, map)); + } + } + else if (IsTrackToNextLevel(propType)) + { + if (IsDictionaryType(propType)) + { + Assign(value, map); + } + else if (IsListType(propType)) + { + var elementType = propType.IsArray ? propType.GetElementType() : propType.GetGenericArguments().FirstOrDefault(); + if (IsStringType(elementType)) + { + if (translate != null) + { + var json = JsonSerializer.Serialize(Assign(value, map), _options.JsonSerializerOptions); + var targetValue = JsonSerializer.Deserialize(json, propType, _options.JsonSerializerOptions); + prop.SetValue(data, targetValue); + } + } + else if (IsTrackToNextLevel(elementType)) + { + prop.SetValue(data, Assign(value, map)); + } + } + else + { + Assign(value, map); + } + } + } + + return data; + } + + /// + /// Translate + /// + /// + /// + /// + private async Task InnerTranslate(List texts, string language, string template) + { + var options = new JsonSerializerOptions() { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }; + var jsonString = JsonSerializer.Serialize(texts, options); + var translator = new Agent + { + Id = Guid.Empty.ToString(), + Name = "Translator", + Instruction = "You are a translation expert.", + TemplateDict = new Dictionary + { + { "text_list", jsonString }, + { "text_list_size", texts.Count }, + { StateConst.LANGUAGE, language } + } + }; + + var render = _services.GetRequiredService(); + var prompt = render.Render(template, translator.TemplateDict); + + var translationDialogs = new List + { + new RoleDialogModel(AgentRole.User, prompt) + { + FunctionName = "translation_prompt", + MessageId = _messageId + } + }; + var response = await _completion.GetChatCompletions(translator, translationDialogs); + return response.Content.JsonContent(); + } + + #region Type methods + private static bool IsStringType(Type? type) + { + if (type == null) return false; + + return type == typeof(string); + } + + private static bool IsListType(Type? type) + { + if (type == null) return false; + + var interfaces = type.GetTypeInfo().ImplementedInterfaces; + return type.IsArray || interfaces.Any(x => x.Name == typeof(IEnumerable).Name); + } + + private static bool IsDictionaryType(Type? type) + { + if (type == null) return false; + + var underlyingInterfaces = type.UnderlyingSystemType.GetTypeInfo().ImplementedInterfaces; + return underlyingInterfaces.Any(x => x.Name == typeof(IDictionary).Name); + } + + private static bool IsTrackToNextLevel(Type? type) + { + if (type == null) return false; + + return type.IsClass || type.IsInterface || type.IsAbstract; + } + #endregion +} diff --git a/src/Infrastructure/BotSharp.Core/Users/Services/UserIdentity.cs b/src/Infrastructure/BotSharp.Core/Users/Services/UserIdentity.cs index b8acdeea..a8eaac5e 100644 --- a/src/Infrastructure/BotSharp.Core/Users/Services/UserIdentity.cs +++ b/src/Infrastructure/BotSharp.Core/Users/Services/UserIdentity.cs @@ -56,4 +56,14 @@ public class UserIdentity : IUserIdentity return $"{FirstName} {LastName}".Trim(); } } + + [JsonPropertyName("user_language")] + public string? UserLanguage + { + get + { + _contextAccessor.HttpContext.Request.Headers.TryGetValue("User-Language", out var languages); + return languages.FirstOrDefault(); + } + } } diff --git a/src/Infrastructure/BotSharp.Core/Users/Services/UserService.cs b/src/Infrastructure/BotSharp.Core/Users/Services/UserService.cs index 6e856b9f..3afea13f 100644 --- a/src/Infrastructure/BotSharp.Core/Users/Services/UserService.cs +++ b/src/Infrastructure/BotSharp.Core/Users/Services/UserService.cs @@ -1,5 +1,6 @@ -using BotSharp.Abstraction.Repositories; using BotSharp.Abstraction.Users.Models; +using BotSharp.Abstraction.Users.Settings; +using BotSharp.OpenAPI.ViewModels.Users; using Microsoft.Extensions.Configuration; using Microsoft.IdentityModel.Tokens; using NanoidDotNet; @@ -13,12 +14,17 @@ public class UserService : IUserService private readonly IServiceProvider _services; private readonly IUserIdentity _user; private readonly ILogger _logger; + private readonly AccountSetting _setting; - public UserService(IServiceProvider services, IUserIdentity user, ILogger logger) + public UserService(IServiceProvider services, + IUserIdentity user, + ILogger logger, + AccountSetting setting) { _services = services; _user = user; _logger = logger; + _setting = setting; } public async Task CreateUser(User user) @@ -52,15 +58,27 @@ public class UserService : IUserService record.Salt = Guid.NewGuid().ToString("N"); record.Password = Utilities.HashText(user.Password, record.Salt); + if (_setting.NewUserVerification) + { + record.VerificationCode = Nanoid.Generate(alphabet: "0123456789", size: 6); + record.Verified = false; + } + db.CreateUser(record); _logger.LogWarning($"Created new user account: {record.Id} {record.UserName}"); Utilities.ClearCache(); + var hooks = _services.GetServices(); + foreach (var hook in hooks) + { + await hook.UserCreated(record); + } + return record; } - public async Task GetToken(string authorization) + public async Task GetToken(string authorization) { var base64 = Encoding.UTF8.GetString(Convert.FromBase64String(authorization)); var (id, password) = base64.SplitAsTuple(":"); @@ -72,13 +90,14 @@ public class UserService : IUserService record = db.GetUserByUserName(id); } + User? user = record; var hooks = _services.GetServices(); - if (record == null || record.Source != "internal") + if (record == null || record.Source != "internal") { // check 3rd party user foreach (var hook in hooks) { - var user = await hook.Authenticate(id, password); + user = await hook.Authenticate(id, password); if (user == null) { continue; @@ -109,7 +128,12 @@ public class UserService : IUserService } } - if (record == null) + if ((!hooks.IsNullOrEmpty() && user == null) || record == null) + { + return default; + } + + if (_setting.NewUserVerification && !record.Verified) { return default; } @@ -182,7 +206,12 @@ public class UserService : IUserService { var db = _services.GetRequiredService(); User user = default; - if (_user.UserName != null) + + if (_user.Id != null) + { + user = db.GetUserById(_user.Id); + } + else if (_user.UserName != null) { user = db.GetUserByUserName(_user.UserName); } @@ -193,11 +222,76 @@ public class UserService : IUserService return user; } - [MemoryCache(10 * 60)] + [MemoryCache(10 * 60, perInstanceCache: true)] public async Task GetUser(string id) { var db = _services.GetRequiredService(); var user = db.GetUserById(id); return user; } + + public async Task ActiveUser(UserActivationModel model) + { + var id = model.UserName; + var db = _services.GetRequiredService(); + var record = id.Contains("@") ? db.GetUserByEmail(id) : db.GetUserByUserName(id); + if (record == null) + { + record = db.GetUserByUserName(id); + } + + if (record == null) + { + return default; + } + + if (record.VerificationCode != model.VerificationCode) + { + return default; + } + + if (record.Verified) + { + return default; + } + + db.UpdateUserVerified(record.Id); + + var accessToken = GenerateJwtToken(record); + var jwt = new JwtSecurityTokenHandler().ReadJwtToken(accessToken); + var token = new Token + { + AccessToken = accessToken, + ExpireTime = jwt.Payload.Exp.Value, + TokenType = "Bearer", + Scope = "api" + }; + return token; + } + + public async Task VerifyUserNameExisting(string userName) + { + if (string.IsNullOrEmpty(userName)) + return true; + + var db = _services.GetRequiredService(); + var user = db.GetUserByUserName(userName); + if (user != null) + return true; + + return false; + } + + public async Task VerifyEmailExisting(string email) + { + if (string.IsNullOrEmpty(email)) + return true; + + var db = _services.GetRequiredService(); + var emailName = db.GetUserByEmail(email); + if (emailName != null) + return true; + + return false; + } } diff --git a/src/Infrastructure/BotSharp.Core/Using.cs b/src/Infrastructure/BotSharp.Core/Using.cs index fbb1c8df..a54df964 100644 --- a/src/Infrastructure/BotSharp.Core/Using.cs +++ b/src/Infrastructure/BotSharp.Core/Using.cs @@ -22,6 +22,12 @@ global using BotSharp.Abstraction.Agents.Models; global using BotSharp.Abstraction.Functions.Models; global using BotSharp.Abstraction.Repositories; global using BotSharp.Abstraction.Repositories.Filters; +global using BotSharp.Abstraction.Translation; +global using BotSharp.Abstraction.Files; +global using BotSharp.Abstraction.Files.Models; +global using BotSharp.Abstraction.Translation.Attributes; +global using BotSharp.Abstraction.Messaging.Enums; +global using BotSharp.Abstraction.Http.Settings; global using BotSharp.Core.Repository; global using BotSharp.Core.Routing; global using BotSharp.Core.Agents.Services; diff --git a/src/Infrastructure/BotSharp.Core/data/agents/01dcc3e5-0af7-49e6-ad7a-a760bd12dc4b/agent.json b/src/Infrastructure/BotSharp.Core/data/agents/01dcc3e5-0af7-49e6-ad7a-a760bd12dc4b/agent.json new file mode 100644 index 00000000..24508fcc --- /dev/null +++ b/src/Infrastructure/BotSharp.Core/data/agents/01dcc3e5-0af7-49e6-ad7a-a760bd12dc4b/agent.json @@ -0,0 +1,11 @@ +{ + "id": "01dcc3e5-0af7-49e6-ad7a-a760bd12dc4b", + "name": "Human Support", + "description": "Reach out to human customer service.", + "type": "task", + "createdDateTime": "2024-04-22T10:00:00Z", + "updatedDateTime": "2024-04-22T10:00:00Z", + "disabled": false, + "isPublic": true, + "profiles": [ "human" ] +} diff --git a/src/Infrastructure/BotSharp.Core/data/agents/01dcc3e5-0af7-49e6-ad7a-a760bd12dc4b/functions.json b/src/Infrastructure/BotSharp.Core/data/agents/01dcc3e5-0af7-49e6-ad7a-a760bd12dc4b/functions.json new file mode 100644 index 00000000..240f5b7c --- /dev/null +++ b/src/Infrastructure/BotSharp.Core/data/agents/01dcc3e5-0af7-49e6-ad7a-a760bd12dc4b/functions.json @@ -0,0 +1,20 @@ +[ + { + "name": "human_intervention_needed", + "description": "If user wants to speak to human customer service.", + "parameters": { + "type": "object", + "properties": { + "reason": { + "type": "string", + "description": "why customer needs customer service." + }, + "summary": { + "type": "string", + "description": "the whole conversation summary with important information" + } + }, + "required": [ "reason", "summary" ] + } + } +] diff --git a/src/Infrastructure/BotSharp.Core/data/agents/01dcc3e5-0af7-49e6-ad7a-a760bd12dc4b/instruction.liquid b/src/Infrastructure/BotSharp.Core/data/agents/01dcc3e5-0af7-49e6-ad7a-a760bd12dc4b/instruction.liquid new file mode 100644 index 00000000..58795499 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core/data/agents/01dcc3e5-0af7-49e6-ad7a-a760bd12dc4b/instruction.liquid @@ -0,0 +1,2 @@ +You are a human customer service connection program. +When other AI customer service agents cannot solve user problems, you know how to call the API to transfer users to human customer service for answers. diff --git a/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-0af7-49e6-ad7a-a760bd12dc4d/agent.json b/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-0af7-49e6-ad7a-a760bd12dc4d/agent.json new file mode 100644 index 00000000..a7fd580e --- /dev/null +++ b/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-0af7-49e6-ad7a-a760bd12dc4d/agent.json @@ -0,0 +1,11 @@ +{ + "id": "01fcc3e5-0af7-49e6-ad7a-a760bd12dc4d", + "name": "Fallback Agent", + "description": "Don't have sufficient confidence to trigger any of existing agent.", + "type": "task", + "createdDateTime": "2024-05-07T10:00:00Z", + "updatedDateTime": "2024-05-07T10:00:00Z", + "disabled": false, + "isPublic": true, + "profiles": [ "fallback" ] +} diff --git a/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-0af7-49e6-ad7a-a760bd12dc4d/instruction.liquid b/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-0af7-49e6-ad7a-a760bd12dc4d/instruction.liquid new file mode 100644 index 00000000..ced0264c --- /dev/null +++ b/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-0af7-49e6-ad7a-a760bd12dc4d/instruction.liquid @@ -0,0 +1 @@ +You are a smart AI Assistant. \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/instruction.liquid b/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/instruction.liquid index 9caf041d..66fef298 100644 --- a/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/instruction.liquid +++ b/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/instruction.liquid @@ -1,11 +1,11 @@ -You're {{router.name}} ({{router.description}}). Follow these steps to handle user request: +You're {{router.name}} ({{router.description}}). +You can understand messages sent by users in different languages. +Follow these steps to handle user request: 1. Read the [CONVERSATION] content. -2. Select a appropriate function from [FUNCTIONS]. -3. Determine which agent is suitable to handle this conversation. -4. Re-think on whether the function you chose matches the reason. -5. For agent required arguments, think carefully, leave it as blank object if user doesn't provide specific arguments. -6. Please do not make up any parameters when there is no exact value provided, you must set the parameter value as null. -7. Response must be in JSON format. +2. Determine which agent is suitable to handle this conversation. +3. For agent required arguments, think carefully, leave it as blank object if user didn't provide the specific arguments. +4. You must include all required args for the selected agent, but you must not make up any parameters when there is no exact value provided, those parameters must set value as null if not declared. +5. Response must be in JSON format. {% if routing_requirements and routing_requirements != empty %} [REQUIREMENTS] diff --git a/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/templates/conversation.summary.liquid b/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/templates/conversation.summary.liquid new file mode 100644 index 00000000..eb626316 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/templates/conversation.summary.liquid @@ -0,0 +1,17 @@ +Please read each conversation in the [CONVERSATIONS] section and provide a summary. + +*** Super Important! Please consider every conversation. Do not only consider the recent sentences. *** +** Please do not respond to the latest conversation. +** If there are different topics in the conversations, please summarize each topic in different sentences and list them in bullets. +** Please summarize each conversation separately. +* Please do not exceed 20 words when summarizing each topic. +* Please do not contain general information in each topic summary. +* Please include some but not excessive details in each topic summary. +* Please do not use "user", "I", "you", "he" or "she". + +[CONVERSATIONS] + +{% for text in texts -%} +[CONVERSATION] +{{ text }}{{ "\r\n" }} +{%- endfor %} \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/templates/planner_prompt.naive.liquid b/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/templates/planner_prompt.naive.liquid index 2b42efaa..3fa0a7f7 100644 --- a/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/templates/planner_prompt.naive.liquid +++ b/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/templates/planner_prompt.naive.liquid @@ -9,6 +9,4 @@ Next action agent is inferred based on user lastest response. Expected user goal agent is {{ expected_user_goal_agent }}. {%- else -%} User goal agent is inferred based on user initial request. -{%- endif %} -If user wants to speak to customer service, use function human_intervention_needed. -If user wants to or is processing with a specific task that can be handled by agents, respond in appropriate output format defined to let proper agent to handle the task. \ No newline at end of file +{%- endif %} \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/templates/response_with_function.liquid b/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/templates/response_with_function.liquid index d45771b3..81ca0f86 100644 --- a/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/templates/response_with_function.liquid +++ b/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/templates/response_with_function.liquid @@ -1,9 +1,14 @@ -[Output Requirements] -1. Read the [Functions] definition, you can utilize the function to retrieve data or execute actions. -2. Think step by step, check if specific function will provider data to help complete user request based on the conversation. -3. If you need to call a function to decide how to response user, - response in format: {"role": "function", "reason":"why choose this function", "function_name": "", "args": {}}, - otherwise response in format: {"role": "assistant", "reason":"why response to user", "content":"next step question"}. -4. If the conversation already contains the function execution result, don't need to call it again. -5. If user mentioned some specific requirment, don't ask this question in your response. -6. Don't repeat the same question in your response. \ No newline at end of file +{% if functions and functions != empty %} +[FUNCTIONS] +{% for fn in functions -%} +{{ fn.name }}: {{ fn.description }} +{{ fn.parameters }} +{{ "\r\n" }} +{%- endfor %} +response_to_user: response to user directly without using any function. +{"type": "object", "properties": {"content":{"type": "string", "description": "The content responsed to user"}}, "required":["content"]} + +[RESPONSE OUTPUT REQUIREMENTS] +* Pick the appropriate function and populate the arguments defined in properties. +* Output the JSON {"function": "", "args":{}} without other text +{% endif %} \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/templates/translation_prompt.liquid b/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/templates/translation_prompt.liquid new file mode 100644 index 00000000..6b3a8677 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/templates/translation_prompt.liquid @@ -0,0 +1,6 @@ +{{ text_list }} + +===== +Translate all the above sentences into {{ language }}. +Output the translated text in JSON {"input_lang":"original text language", "output_count": {{ text_list_size }}, "output_lang":"{{ language }}", "texts":[{"id": 1, "text":""},{"id": 2, "text":""}]}. +The "output_count" must equal the length of the "texts" array in the output. diff --git a/src/Infrastructure/BotSharp.Core/data/agents/dfd9b46d-d00c-40af-8a75-3fbdc2b89869/instruction.liquid b/src/Infrastructure/BotSharp.Core/data/agents/dfd9b46d-d00c-40af-8a75-3fbdc2b89869/instruction.liquid index d0bed0db..bc80b14c 100644 --- a/src/Infrastructure/BotSharp.Core/data/agents/dfd9b46d-d00c-40af-8a75-3fbdc2b89869/instruction.liquid +++ b/src/Infrastructure/BotSharp.Core/data/agents/dfd9b46d-d00c-40af-8a75-3fbdc2b89869/instruction.liquid @@ -1,7 +1,7 @@ -This is a model evaluation program, which interactive with model to complete a certain task based on the background information given to you. +This is a model evaluation program, which interacts with model to complete a certain task based on the background information given to you. {{ task_prompt }} user: Hi! assistant: Hello, How can I help you? -user: \ No newline at end of file +user: diff --git a/src/Infrastructure/BotSharp.OpenAPI/BotSharp.OpenAPI.csproj b/src/Infrastructure/BotSharp.OpenAPI/BotSharp.OpenAPI.csproj index f3495bb9..8f0e9fcc 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/BotSharp.OpenAPI.csproj +++ b/src/Infrastructure/BotSharp.OpenAPI/BotSharp.OpenAPI.csproj @@ -18,21 +18,23 @@ - + - + - - + + + + diff --git a/src/Infrastructure/BotSharp.OpenAPI/BotSharpOpenApiExtensions.cs b/src/Infrastructure/BotSharp.OpenAPI/BotSharpOpenApiExtensions.cs index 63a9bc92..a1a6dad2 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/BotSharpOpenApiExtensions.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/BotSharpOpenApiExtensions.cs @@ -11,8 +11,6 @@ using Microsoft.Net.Http.Headers; using Microsoft.OpenApi.Models; using Microsoft.IdentityModel.JsonWebTokens; using BotSharp.OpenAPI.BackgroundServices; -using BotSharp.Abstraction.Settings; -using BotSharp.Abstraction.Google.Settings; namespace BotSharp.OpenAPI; @@ -34,12 +32,6 @@ public static class BotSharpOpenApiExtensions services.AddScoped(); services.AddHostedService(); - services.AddScoped(provider => - { - var settingService = provider.GetRequiredService(); - return settingService.Bind("GoogleApi"); - }); - // Add bearer authentication var schema = "MIXED_SCHEME"; var builder = services.AddAuthentication(options => @@ -126,6 +118,20 @@ public static class BotSharpOpenApiExtensions }); } + // Wexin OAuth + if (!string.IsNullOrWhiteSpace(config["OAuth:Wexin:ClientId"]) && !string.IsNullOrWhiteSpace(config["OAuth:Wexin:ClientSecret"])) + { + builder = builder.AddWeixin(options => + { + options.ClientId = config["OAuth:GitHub:ClientId"]; + options.ClientSecret = config["OAuth:GitHub:ClientSecret"]; + options.Scope.Add("user:email"); + options.Backchannel = builder.Services.BuildServiceProvider() + .GetRequiredService() + .CreateClient(); + }); + } + // Add services to the container. services.AddControllers() .AddJsonOptions(options => diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/AgentController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/AgentController.cs index 5d7f7b56..a7b5652d 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/AgentController.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/AgentController.cs @@ -1,4 +1,5 @@ using BotSharp.Abstraction.Agents.Models; +using BotSharp.Abstraction.Users.Enums; namespace BotSharp.OpenAPI.Controllers; @@ -7,11 +8,13 @@ namespace BotSharp.OpenAPI.Controllers; public class AgentController : ControllerBase { private readonly IAgentService _agentService; + private readonly IUserIdentity _user; private readonly IServiceProvider _services; - public AgentController(IAgentService agentService, IServiceProvider services) + public AgentController(IAgentService agentService, IUserIdentity user, IServiceProvider services) { _agentService = agentService; + _user = user; _services = services; } @@ -23,7 +26,7 @@ public class AgentController : ControllerBase } [HttpGet("/agent/{id}")] - public async Task GetAgent([FromRoute] string id) + public async Task GetAgent([FromRoute] string id) { var agents = await GetAgents(new AgentFilter { @@ -31,6 +34,8 @@ public class AgentController : ControllerBase }); var targetAgent = agents.Items.FirstOrDefault(); + if (targetAgent == null) return null; + var redirectAgentIds = targetAgent.RoutingRules .Where(x => !string.IsNullOrEmpty(x.RedirectTo)) .Select(x => x.RedirectTo).ToList(); @@ -45,6 +50,17 @@ public class AgentController : ControllerBase rule.RedirectToAgentName = found.Name; } + + var editable = true; + var userService = _services.GetRequiredService(); + var user = await userService.GetUser(_user.Id); + if (user?.Role != UserRole.Admin) + { + var userAgents = _agentService.GetAgentsByUser(user?.Id); + editable = userAgents?.Select(x => x.Id)?.Contains(targetAgent.Id) ?? false; + } + + targetAgent.Editable = editable; return targetAgent; } @@ -84,15 +100,15 @@ public class AgentController : ControllerBase } [HttpPost("/refresh-agents")] - public async Task RefreshAgents() + public async Task RefreshAgents() { - await _agentService.RefreshAgents(); + return await _agentService.RefreshAgents(); } [HttpPut("/agent/file/{agentId}")] - public async Task UpdateAgentFromFile([FromRoute] string agentId) + public async Task UpdateAgentFromFile([FromRoute] string agentId) { - await _agentService.UpdateAgentFromFile(agentId); + return await _agentService.UpdateAgentFromFile(agentId); } [HttpPut("/agent/{agentId}")] @@ -110,4 +126,18 @@ public class AgentController : ControllerBase model.Id = agentId; await _agentService.UpdateAgent(model, field); } + + [HttpPatch("/agent/{agentId}/templates")] + public async Task PatchAgentTemplates([FromRoute] string agentId, [FromBody] AgentTemplatePatchModel agent) + { + var model = agent.ToAgent(); + model.Id = agentId; + return await _agentService.PatchAgentTemplate(model); + } + + [HttpDelete("/agent/{agentId}")] + public async Task DeleteAgent([FromRoute] string agentId) + { + return await _agentService.DeleteAgent(agentId); + } } \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs index daf2b116..c8f573a6 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs @@ -1,5 +1,6 @@ +using BotSharp.Abstraction.Options; using BotSharp.Abstraction.Routing; -using BotSharp.Abstraction.Users.Models; +using BotSharp.Abstraction.Users.Enums; namespace BotSharp.OpenAPI.Controllers; @@ -9,12 +10,16 @@ public class ConversationController : ControllerBase { private readonly IServiceProvider _services; private readonly IUserIdentity _user; + private readonly JsonSerializerOptions _jsonOptions; public ConversationController(IServiceProvider services, - IUserIdentity user) + IUserIdentity user, + BotSharpOptions options) { _services = services; _user = user; + _jsonOptions = InitJsonOptions(options); + } [HttpPost("/conversation/{agentId}")] @@ -25,7 +30,6 @@ public class ConversationController : ControllerBase { AgentId = agentId, Channel = ConversationChannel.OpenAPI, - UserId = _user.Id, TaskId = config.TaskId }; conv = await service.NewConversation(conv); @@ -37,20 +41,23 @@ public class ConversationController : ControllerBase [HttpPost("/conversations")] public async Task> GetConversations([FromBody] ConversationFilter filter) { - var service = _services.GetRequiredService(); - var conversations = await service.GetConversations(filter); - + var convService = _services.GetRequiredService(); var userService = _services.GetRequiredService(); + var user = await userService.GetUser(_user.Id); + if (user == null) + { + return new PagedItems(); + } + + filter.UserId = user.Role != UserRole.Admin ? user.Id : null; + var conversations = await convService.GetConversations(filter); var agentService = _services.GetRequiredService(); - var list = conversations.Items - .Select(x => ConversationViewModel.FromSession(x)) - .ToList(); + var list = conversations.Items.Select(x => ConversationViewModel.FromSession(x)).ToList(); foreach (var item in list) { - var user = await userService.GetUser(item.User.Id); + user = await userService.GetUser(item.User.Id); item.User = UserViewModel.FromUser(user); - var agent = await agentService.GetAgent(item.AgentId); item.AgentName = agent?.Name; } @@ -84,9 +91,10 @@ public class ConversationController : ControllerBase ConversationId = conversationId, MessageId = message.MessageId, CreatedAt = message.CreatedAt, - Text = message.Content, + Text = !string.IsNullOrEmpty(message.SecondaryContent) ? message.SecondaryContent : message.Content, Data = message.Data, - Sender = UserViewModel.FromUser(user) + Sender = UserViewModel.FromUser(user), + Payload = message.Payload }); } else if (message.Role == AgentRole.Assistant) @@ -97,7 +105,7 @@ public class ConversationController : ControllerBase ConversationId = conversationId, MessageId = message.MessageId, CreatedAt = message.CreatedAt, - Text = message.Content, + Text = !string.IsNullOrEmpty(message.SecondaryContent) ? message.SecondaryContent : message.Content, Function = message.FunctionName, Data = message.Data, Sender = new UserViewModel @@ -105,7 +113,7 @@ public class ConversationController : ControllerBase FirstName = agent.Name, Role = message.Role, }, - RichContent = message.RichContent + RichContent = message.SecondaryRichContent ?? message.RichContent }); } } @@ -114,7 +122,44 @@ public class ConversationController : ControllerBase } [HttpGet("/conversation/{conversationId}")] - public async Task GetConversation([FromRoute] string conversationId) + public async Task GetConversation([FromRoute] string conversationId) + { + var service = _services.GetRequiredService(); + var userService = _services.GetRequiredService(); + var user = await userService.GetUser(_user.Id); + if (user == null) + { + return null; + } + + var filter = new ConversationFilter + { + Id = conversationId, + UserId = user.Role != UserRole.Admin ? user.Id : null + }; + var conversations = await service.GetConversations(filter); + if (conversations.Items.IsNullOrEmpty()) + { + return null; + } + + var result = ConversationViewModel.FromSession(conversations.Items.First()); + var state = _services.GetRequiredService(); + result.States = state.Load(conversationId, isReadOnly: true); + result.User = UserViewModel.FromUser(user); + + return result; + } + + [HttpPost("/conversation/summary")] + public async Task GetConversationSummary([FromBody] ConversationSummaryModel input) + { + var service = _services.GetRequiredService(); + return await service.GetConversationSummary(input.ConversationIds); + } + + [HttpGet("/conversation/{conversationId}/user")] + public async Task GetConversationUser([FromRoute] string conversationId) { var service = _services.GetRequiredService(); var conversations = await service.GetConversations(new ConversationFilter @@ -123,21 +168,44 @@ public class ConversationController : ControllerBase }); var userService = _services.GetRequiredService(); - var result = ConversationViewModel.FromSession(conversations.Items.First()); + var conversation = conversations?.Items?.FirstOrDefault(); + var userId = conversation == null ? _user.Id : conversation.UserId; + var user = await userService.GetUser(userId); + if (user == null) + { + return new UserViewModel + { + Id = _user.Id, + UserName = _user.UserName, + FirstName = _user.FirstName, + LastName = _user.LastName, + Email = _user.Email, + Source = "Unknown" + }; + } - var state = _services.GetRequiredService(); - result.States = state.Load(conversationId); - - var user = await userService.GetUser(result.User.Id); - result.User = UserViewModel.FromUser(user); - - return result; + return UserViewModel.FromUser(user); } [HttpDelete("/conversation/{conversationId}")] public async Task DeleteConversation([FromRoute] string conversationId) { + var userService = _services.GetRequiredService(); var conversationService = _services.GetRequiredService(); + + var user = await userService.GetUser(_user.Id); + var filter = new ConversationFilter + { + Id = conversationId, + UserId = user.Role != UserRole.Admin ? user.Id : null + }; + var conversations = await conversationService.GetConversations(filter); + + if (conversations.Items.IsNullOrEmpty()) + { + return false; + } + var response = await conversationService.DeleteConversations(new List { conversationId }); return response; } @@ -150,27 +218,29 @@ public class ConversationController : ControllerBase return response; } + #region Send message [HttpPost("/conversation/{agentId}/{conversationId}")] public async Task SendMessage([FromRoute] string agentId, [FromRoute] string conversationId, [FromBody] NewMessageModel input) { var conv = _services.GetRequiredService(); + var inputMsg = new RoleDialogModel(AgentRole.User, input.Text) + { + Files = input.Files, + CreatedAt = DateTime.UtcNow + }; + if (!string.IsNullOrEmpty(input.TruncateMessageId)) { - await conv.TruncateConversation(conversationId, input.TruncateMessageId); + await conv.TruncateConversation(conversationId, input.TruncateMessageId, inputMsg.MessageId); } - var inputMsg = new RoleDialogModel(AgentRole.User, input.Text); var routing = _services.GetRequiredService(); routing.Context.SetMessageId(conversationId, inputMsg.MessageId); conv.SetConversationId(conversationId, input.States); - conv.States.SetState("channel", input.Channel) - .SetState("provider", input.Provider) - .SetState("model", input.Model) - .SetState("temperature", input.Temperature) - .SetState("sampling_factor", input.SamplingFactor); + SetStates(conv, input); var response = new ChatResponseModel(); @@ -178,9 +248,9 @@ public class ConversationController : ControllerBase replyMessage: input.Postback, async msg => { - response.Text = msg.Content; + response.Text = !string.IsNullOrEmpty(msg.SecondaryContent) ? msg.SecondaryContent : msg.Content; response.Function = msg.FunctionName; - response.RichContent = msg.RichContent; + response.RichContent = msg.SecondaryRichContent ?? msg.RichContent; response.Instruction = msg.Instruction; response.Data = msg.Data; }, @@ -195,14 +265,93 @@ public class ConversationController : ControllerBase return response; } + [HttpPost("/conversation/{agentId}/{conversationId}/sse")] + public async Task SendMessageSse([FromRoute] string agentId, + [FromRoute] string conversationId, + [FromBody] NewMessageModel input) + { + var conv = _services.GetRequiredService(); + var inputMsg = new RoleDialogModel(AgentRole.User, input.Text) + { + Files = input.Files, + CreatedAt = DateTime.UtcNow + }; + + if (!string.IsNullOrEmpty(input.TruncateMessageId)) + { + await conv.TruncateConversation(conversationId, input.TruncateMessageId, inputMsg.MessageId); + } + + var state = _services.GetRequiredService(); + + var routing = _services.GetRequiredService(); + routing.Context.SetMessageId(conversationId, inputMsg.MessageId); + + conv.SetConversationId(conversationId, input.States); + SetStates(conv, input); + + var response = new ChatResponseModel + { + ConversationId = conversationId, + MessageId = inputMsg.MessageId, + }; + + Response.StatusCode = 200; + 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"); + + await conv.SendMessage(agentId, inputMsg, + replyMessage: input.Postback, + // responsed generated + async msg => + { + response.Text = !string.IsNullOrEmpty(msg.SecondaryContent) ? msg.SecondaryContent : msg.Content; + response.Function = msg.FunctionName; + response.RichContent = msg.SecondaryRichContent ?? msg.RichContent; + response.Instruction = msg.Instruction; + response.Data = msg.Data; + response.States = state.GetStates(); + + await OnChunkReceived(Response, response); + }, + // executing + async msg => + { + var indicator = new ChatResponseModel + { + ConversationId = conversationId, + MessageId = msg.MessageId, + Text = msg.Indication, + Function = "indicating", + Instruction = msg.Instruction, + States = new Dictionary() + }; + await OnChunkReceived(Response, indicator); + }, + // executed + async msg => + { + + }); + + response.States = state.GetStates(); + response.MessageId = inputMsg.MessageId; + response.ConversationId = conversationId; + + // await OnEventCompleted(Response); + } + #endregion + + #region Files and attachments [HttpPost("/conversation/{conversationId}/attachments")] - public IActionResult UploadAttachments([FromRoute] string conversationId, + public IActionResult UploadAttachments([FromRoute] string conversationId, IFormFile[] files) { if (files != null && files.Length > 0) { - var attachmentService = _services.GetRequiredService(); - var dir = attachmentService.GetDirectory(conversationId); + var fileService = _services.GetRequiredService(); + var dir = fileService.GetDirectory(conversationId); foreach (var file in files) { // Save the file, process it, etc. @@ -220,4 +369,85 @@ public class ConversationController : ControllerBase return BadRequest(new { message = "Invalid file." }); } + + [HttpGet("/conversation/{conversationId}/files/{messageId}")] + public IEnumerable GetMessageFiles([FromRoute] string conversationId, [FromRoute] string messageId) + { + var fileService = _services.GetRequiredService(); + var files = fileService.GetMessageFiles(conversationId, new List { messageId }); + return files?.Select(x => MessageFileViewModel.Transform(x))?.ToList() ?? new List(); + } + + [HttpGet("/conversation/{conversationId}/message/{messageId}/file/{fileName}")] + public IActionResult GetMessageFile([FromRoute] string conversationId, [FromRoute] string messageId, [FromRoute] string fileName) + { + var fileService = _services.GetRequiredService(); + var file = fileService.GetMessageFile(conversationId, messageId, fileName); + if (string.IsNullOrEmpty(file)) + { + return NotFound(); + } + return BuildFileResult(file); + } + #endregion + + #region Private methods + private void SetStates(IConversationService conv, NewMessageModel input) + { + conv.States.SetState("channel", input.Channel, source: StateSource.External) + .SetState("provider", input.Provider, source: StateSource.External) + .SetState("model", input.Model, source: StateSource.External) + .SetState("temperature", input.Temperature, source: StateSource.External) + .SetState("sampling_factor", input.SamplingFactor, source: StateSource.External); + } + + private FileContentResult BuildFileResult(string file) + { + using Stream stream = System.IO.File.Open(file, FileMode.Open, FileAccess.Read, FileShare.Read); + var bytes = new byte[stream.Length]; + stream.Read(bytes, 0, (int)stream.Length); + return File(bytes, "application/octet-stream", Path.GetFileName(file)); + } + + private async Task OnChunkReceived(HttpResponse response, ChatResponseModel message) + { + var json = JsonSerializer.Serialize(message, _jsonOptions); + + var buffer = Encoding.UTF8.GetBytes($"data:{json}\n"); + await response.Body.WriteAsync(buffer, 0, buffer.Length); + await Task.Delay(10); + + buffer = Encoding.UTF8.GetBytes("\n"); + await response.Body.WriteAsync(buffer, 0, buffer.Length); + } + + private async Task OnEventCompleted(HttpResponse response) + { + var buffer = Encoding.UTF8.GetBytes("data:[DONE]\n"); + await response.Body.WriteAsync(buffer, 0, buffer.Length); + + buffer = Encoding.UTF8.GetBytes("\n"); + await response.Body.WriteAsync(buffer, 0, buffer.Length); + } + + private JsonSerializerOptions InitJsonOptions(BotSharpOptions options) + { + var jsonOption = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + AllowTrailingCommas = true + }; + + if (options?.JsonSerializerOptions != null) + { + foreach (var option in options.JsonSerializerOptions.Converters) + { + jsonOption.Converters.Add(option); + } + } + + return jsonOption; + } + #endregion } diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/FileController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/FileController.cs new file mode 100644 index 00000000..36dddebd --- /dev/null +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/FileController.cs @@ -0,0 +1,13 @@ +namespace BotSharp.OpenAPI.Controllers; + +[Authorize] +[ApiController] +public class FileController : ControllerBase +{ + private readonly IServiceProvider _services; + + public FileController(IServiceProvider services) + { + _services = services; + } +} diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/InstructModeController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/InstructModeController.cs index 725b6e2b..45120a26 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/InstructModeController.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/InstructModeController.cs @@ -11,10 +11,12 @@ namespace BotSharp.OpenAPI.Controllers; public class InstructModeController : ControllerBase { private readonly IServiceProvider _services; + private readonly ILogger _logger; - public InstructModeController(IServiceProvider services) + public InstructModeController(IServiceProvider services, ILogger logger) { _services = services; + _logger = logger; } [HttpPost("/instruct/{agentId}")] @@ -22,12 +24,12 @@ public class InstructModeController : ControllerBase [FromBody] InstructMessageModel input) { var state = _services.GetRequiredService(); - input.States.ForEach(x => state.SetState(x.Key, x.Value, activeRounds: x.ActiveRounds)); - state.SetState("provider", input.Provider) - .SetState("model", input.Model) - .SetState("model_id", input.ModelId) - .SetState("instruction", input.Instruction) - .SetState("input_text", input.Text); + input.States.ForEach(x => state.SetState(x.Key, x.Value, activeRounds: x.ActiveRounds, source: StateSource.External)); + state.SetState("provider", input.Provider, source: StateSource.External) + .SetState("model", input.Model, source: StateSource.External) + .SetState("model_id", input.ModelId, source: StateSource.External) + .SetState("instruction", input.Instruction, source: StateSource.External) + .SetState("input_text", input.Text,source: StateSource.External); var instructor = _services.GetRequiredService(); var result = await instructor.Execute(agentId, @@ -44,10 +46,10 @@ public class InstructModeController : ControllerBase public async Task TextCompletion([FromBody] IncomingMessageModel input) { var state = _services.GetRequiredService(); - input.States.ForEach(x => state.SetState(x.Key, x.Value, activeRounds: x.ActiveRounds)); - state.SetState("provider", input.Provider) - .SetState("model", input.Model) - .SetState("model_id", input.ModelId); + input.States.ForEach(x => state.SetState(x.Key, x.Value, activeRounds: x.ActiveRounds, source: StateSource.External)); + state.SetState("provider", input.Provider, source: StateSource.External) + .SetState("model", input.Model, source: StateSource.External) + .SetState("model_id", input.ModelId, source: StateSource.External); var textCompletion = CompletionProvider.GetTextCompletion(_services); return await textCompletion.GetCompletion(input.Text, Guid.Empty.ToString(), Guid.NewGuid().ToString()); @@ -57,10 +59,10 @@ public class InstructModeController : ControllerBase public async Task ChatCompletion([FromBody] IncomingMessageModel input) { var state = _services.GetRequiredService(); - input.States.ForEach(x => state.SetState(x.Key, x.Value, activeRounds: x.ActiveRounds)); - state.SetState("provider", input.Provider) - .SetState("model", input.Model) - .SetState("model_id", input.ModelId); + input.States.ForEach(x => state.SetState(x.Key, x.Value, activeRounds: x.ActiveRounds, source: StateSource.External)); + state.SetState("provider", input.Provider, source: StateSource.External) + .SetState("model", input.Model, source: StateSource.External) + .SetState("model_id", input.ModelId, source: StateSource.External); var textCompletion = CompletionProvider.GetChatCompletion(_services); var message = await textCompletion.GetChatCompletions(new Agent() @@ -72,4 +74,33 @@ public class InstructModeController : ControllerBase }); return message.Content; } + + [HttpPost("/instruct/multi-modal")] + public async Task MultiModalCompletion([FromBody] IncomingMessageModel input) + { + var state = _services.GetRequiredService(); + input.States.ForEach(x => state.SetState(x.Key, x.Value, activeRounds: x.ActiveRounds, source: StateSource.External)); + + try + { + var completion = CompletionProvider.GetChatCompletion(_services, provider: input.Provider ?? "openai", + modelId: input.ModelId ?? "gpt-4", multiModal: true); + var message = await completion.GetChatCompletions(new Agent() + { + Id = Guid.Empty.ToString(), + }, new List + { + new RoleDialogModel(AgentRole.User, input.Text) + { + Files = input.Files + } + }); + return message.Content; + } + catch (Exception ex) + { + _logger.LogError($"Error in analyzing files. {ex.Message}"); + return $"Error in analyzing files."; + } + } } diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/PluginController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/PluginController.cs index 2c231e16..342f39fb 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/PluginController.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/PluginController.cs @@ -1,4 +1,5 @@ using BotSharp.Abstraction.Plugins.Models; +using BotSharp.Abstraction.Users.Enums; using BotSharp.Core.Plugins; namespace BotSharp.OpenAPI.Controllers; @@ -8,23 +9,32 @@ namespace BotSharp.OpenAPI.Controllers; public class PluginController : ControllerBase { private readonly IServiceProvider _services; + private readonly IUserIdentity _user; private readonly PluginSettings _settings; - public PluginController(IServiceProvider services, PluginSettings settings) + public PluginController(IServiceProvider services, IUserIdentity user, PluginSettings settings) { _services = services; + _user = user; _settings = settings; } [HttpGet("/plugins")] - public PagedItems GetPlugins([FromQuery] PluginFilter filter) + public async Task> GetPlugins([FromQuery] PluginFilter filter) { + var userService = _services.GetRequiredService(); + var user = await userService.GetUser(_user.Id); + if (user?.Role != UserRole.Admin) + { + return new PagedItems(); + } + var loader = _services.GetRequiredService(); return loader.GetPagedPlugins(_services, filter); } [HttpGet("/plugin/menu")] - public List GetPluginMenu() + public async Task> GetPluginMenu() { var menu = new List { @@ -33,11 +43,18 @@ public class PluginController : ControllerBase IsHeader = true, }, new PluginMenuDef("System", weight: 30) - { - IsHeader = true + { + IsHeader = true, + Roles = new List { UserRole.Admin } }, - new PluginMenuDef("Plugins", link: "page/plugin", icon: "bx bx-plug", weight: 31), - new PluginMenuDef("Settings", link: "page/setting", icon: "bx bx-cog", weight: 32), + new PluginMenuDef("Plugins", link: "page/plugin", icon: "bx bx-plug", weight: 31) + { + Roles = new List { UserRole.Admin } + }, + new PluginMenuDef("Settings", link: "page/setting", icon: "bx bx-cog", weight: 32) + { + Roles = new List { UserRole.Admin } + } }; var loader = _services.GetRequiredService(); @@ -49,6 +66,10 @@ public class PluginController : ControllerBase } plugin.Module.AttachMenu(menu); } + + var userService = _services.GetRequiredService(); + var user = await userService.GetUser(_user.Id); + menu = loader.GetPluginMenuByRoles(menu, user?.Role); menu = menu.OrderBy(x => x.Weight).ToList(); return menu; } diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/UserController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/UserController.cs index e4201aa8..9012e0d1 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/UserController.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/UserController.cs @@ -36,7 +36,7 @@ public class UserController : ControllerBase [AllowAnonymous] [HttpGet("/sso/{provider}")] - public async Task Authorize([FromRoute] string provider,string redirectUrl) + public async Task Authorize([FromRoute] string provider, string redirectUrl) { return Challenge(new AuthenticationProperties { RedirectUri = redirectUrl }, provider); } @@ -61,6 +61,18 @@ public class UserController : ControllerBase return UserViewModel.FromUser(createdUser); } + [AllowAnonymous] + [HttpPost("/user/activate")] + public async Task> ActivateUser(UserActivationModel model) + { + var token = await _userService.ActiveUser(model); + if (token == null) + { + return Unauthorized(); + } + return Ok(token); + } + [HttpGet("/user/me")] public async Task GetMyUserProfile() { @@ -82,4 +94,48 @@ public class UserController : ControllerBase } return UserViewModel.FromUser(user); } + + [HttpGet("/user/name/existing")] + public async Task VerifyUserNameExisting([FromQuery] string userName) + { + return await _userService.VerifyUserNameExisting(userName); + } + + [HttpGet("/user/email/existing")] + public async Task VerifyEmailExisting([FromQuery] string email) + { + return await _userService.VerifyEmailExisting(email); + } + + #region Avatar + [HttpPost("/user/avatar")] + public bool UploadUserAvatar([FromBody] BotSharpFile file) + { + var fileService = _services.GetRequiredService(); + return fileService.SaveUserAvatar(file); + } + + [HttpGet("/user/avatar")] + public IActionResult GetUserAvatar() + { + var fileService = _services.GetRequiredService(); + var file = fileService.GetUserAvatar(); + if (string.IsNullOrEmpty(file)) + { + return NotFound(); + } + return BuildFileResult(file); + } + #endregion + + + #region Private methods + private FileContentResult BuildFileResult(string file) + { + using Stream stream = System.IO.File.Open(file, FileMode.Open, FileAccess.Read, FileShare.Read); + var bytes = new byte[stream.Length]; + stream.Read(bytes, 0, (int)stream.Length); + return File(bytes, "application/octet-stream", Path.GetFileName(file)); + } + #endregion } diff --git a/src/Infrastructure/BotSharp.OpenAPI/Using.cs b/src/Infrastructure/BotSharp.OpenAPI/Using.cs index 201bf7b1..8771b81c 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Using.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Using.cs @@ -24,6 +24,9 @@ global using BotSharp.Abstraction.Conversations.Enums; global using BotSharp.Abstraction.Conversations.Models; global using BotSharp.Abstraction.Models; global using BotSharp.Abstraction.Repositories.Filters; +global using BotSharp.Abstraction.Files.Models; +global using BotSharp.Abstraction.Files; global using BotSharp.OpenAPI.ViewModels.Conversations; global using BotSharp.OpenAPI.ViewModels.Users; -global using BotSharp.OpenAPI.ViewModels.Agents; \ No newline at end of file +global using BotSharp.OpenAPI.ViewModels.Agents; +global using BotSharp.OpenAPI.ViewModels.Files; \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/AgentTemplatePatchModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/AgentTemplatePatchModel.cs new file mode 100644 index 00000000..2d1beba6 --- /dev/null +++ b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/AgentTemplatePatchModel.cs @@ -0,0 +1,23 @@ +using BotSharp.Abstraction.Agents.Models; + +namespace BotSharp.OpenAPI.ViewModels.Agents; + +public class AgentTemplatePatchModel +{ + public List? Templates { get; set; } + + public AgentTemplatePatchModel() + { + + } + + public Agent ToAgent() + { + var agent = new Agent() + { + Templates = Templates ?? new List(), + }; + + return agent; + } +} diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/AgentViewModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/AgentViewModel.cs index 536f5b5d..9d87a3e2 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/AgentViewModel.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/AgentViewModel.cs @@ -42,6 +42,8 @@ public class AgentViewModel public PluginDef Plugin { get; set; } + public bool Editable { get; set; } + [JsonPropertyName("created_datetime")] public DateTime CreatedDateTime { get; set; } diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/ChatResponseModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/ChatResponseModel.cs index cf71b8f9..a03f1211 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/ChatResponseModel.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/ChatResponseModel.cs @@ -28,6 +28,10 @@ public class ChatResponseModel : InstructResult [JsonPropertyName("rich_content")] public RichContent? RichContent { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("payload")] + public string? Payload { get; set; } + [JsonPropertyName("created_at")] public DateTime CreatedAt { get; set; } = DateTime.UtcNow; } diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/ConversationSummaryModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/ConversationSummaryModel.cs new file mode 100644 index 00000000..0854ab2a --- /dev/null +++ b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/ConversationSummaryModel.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace BotSharp.OpenAPI.ViewModels.Conversations; + +public class ConversationSummaryModel +{ + [JsonPropertyName("conversation_ids")] + public List ConversationIds { get; set; } = new List(); +} diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Files/MessageFileViewModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Files/MessageFileViewModel.cs new file mode 100644 index 00000000..a9eb33bd --- /dev/null +++ b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Files/MessageFileViewModel.cs @@ -0,0 +1,34 @@ +using System.Text.Json.Serialization; + +namespace BotSharp.OpenAPI.ViewModels.Files; + +public class MessageFileViewModel +{ + [JsonPropertyName("file_url")] + public string FileUrl { get; set; } + + [JsonPropertyName("file_name")] + public string FileName { get; set; } + + [JsonPropertyName("file_type")] + public string FileType { get; set; } + + [JsonPropertyName("content_type")] + public string ContentType { get; set; } + + public MessageFileViewModel() + { + + } + + public static MessageFileViewModel Transform(MessageFileModel model) + { + return new MessageFileViewModel + { + FileUrl = model.FileUrl, + FileName = model.FileName, + FileType = model.FileType, + ContentType = model.ContentType + }; + } +} diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserViewModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserViewModel.cs index eeb1a8a2..8d28cf03 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserViewModel.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserViewModel.cs @@ -19,6 +19,7 @@ public class UserViewModel public string Source { get; set; } [JsonPropertyName("external_id")] public string? ExternalId { get; set; } + public string Avatar { get; set; } = "/user/avatar"; [JsonPropertyName("create_date")] public DateTime CreateDate { get; set; } [JsonPropertyName("update_date")] @@ -47,7 +48,8 @@ public class UserViewModel Source = user.Source, ExternalId = user.ExternalId, CreateDate = user.CreatedTime, - UpdateDate = user.UpdatedTime + UpdateDate = user.UpdatedTime, + Avatar = "/user/avatar" }; } } diff --git a/src/Plugins/BotSharp.Plugin.AnthropicAI/AnthropicPlugin.cs b/src/Plugins/BotSharp.Plugin.AnthropicAI/AnthropicPlugin.cs new file mode 100644 index 00000000..e0bd67e3 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.AnthropicAI/AnthropicPlugin.cs @@ -0,0 +1,27 @@ +using BotSharp.Abstraction.Plugins; +using BotSharp.Plugin.AnthropicAI.Providers; +using Microsoft.Extensions.Configuration; + +namespace BotSharp.Plugin.AnthropicAI; + +public class AnthropicPlugin : IBotSharpPlugin +{ + public string Id => "012119da-8367-4be8-9a75-ab6ae55071e6"; + public string Name => "Anthropic AI"; + public string Description => "Anthropic is an AI safety and research company"; + public string? IconUrl => "https://www.anthropic.com/images/icons/safari-pinned-tab.svg"; + + public void RegisterDI(IServiceCollection services, IConfiguration config) + { + var settings = new AnthropicSettings(); + config.Bind("AnthropicAi", settings); + services.AddSingleton(x => + { + // Console.WriteLine($"Loaded Anthropic settings: {settings.Claude.ApiKey.SubstringMax(4)}"); + return settings; + }); + + services.AddScoped(); + // services.AddScoped(); + } +} diff --git a/src/Plugins/BotSharp.Plugin.AnthropicAI/BotSharp.Plugin.AnthropicAI.csproj b/src/Plugins/BotSharp.Plugin.AnthropicAI/BotSharp.Plugin.AnthropicAI.csproj new file mode 100644 index 00000000..40f54f60 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.AnthropicAI/BotSharp.Plugin.AnthropicAI.csproj @@ -0,0 +1,21 @@ + + + + netstandard2.1 + enable + $(LangVersion) + $(BotSharpVersion) + $(GeneratePackageOnBuild) + $(GenerateDocumentationFile) + $(SolutionDir)packages + + + + + + + + + + + diff --git a/src/Plugins/BotSharp.Plugin.AnthropicAI/Providers/ChatCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.AnthropicAI/Providers/ChatCompletionProvider.cs new file mode 100644 index 00000000..87202109 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.AnthropicAI/Providers/ChatCompletionProvider.cs @@ -0,0 +1,267 @@ +using Anthropic.SDK.Common; +using BotSharp.Abstraction.MLTasks.Settings; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; + +namespace BotSharp.Plugin.AnthropicAI.Providers; + +public class ChatCompletionProvider : IChatCompletion +{ + public string Provider => "anthropic"; + + protected readonly AnthropicSettings _settings; + protected readonly IServiceProvider _services; + protected readonly ILogger _logger; + + protected string _model; + + public ChatCompletionProvider(AnthropicSettings settings, + ILogger logger, + IServiceProvider services) + { + _settings = settings; + _logger = logger; + _services = services; + } + + public async Task GetChatCompletions(Agent agent, List conversations) + { + var contentHooks = _services.GetServices().ToList(); + + // Before chat completion hook + foreach (var hook in contentHooks) + { + await hook.BeforeGenerating(agent, conversations); + } + + var settingsService = _services.GetRequiredService(); + var settings = settingsService.GetSetting("anthropic", agent.LlmConfig?.Model ?? "claude-3-haiku"); + + var client = new AnthropicClient(new APIAuthentication(settings.ApiKey)); + var (prompt, parameters) = PrepareOptions(agent, conversations, settings); + + var response = await client.Messages.GetClaudeMessageAsync(parameters); + + RoleDialogModel responseMessage; + + if (response.StopReason == "tool_use") + { + var toolResult = response.Content.OfType().First(); + + responseMessage = new RoleDialogModel(AgentRole.Function, response.FirstMessage?.Text) + { + CurrentAgentId = agent.Id, + MessageId = conversations.Last().MessageId, + ToolCallId = toolResult.Id, + FunctionName = toolResult.Name, + FunctionArgs = JsonSerializer.Serialize(toolResult.Input) + }; + } + else + { + var message = response.FirstMessage; + responseMessage = new RoleDialogModel(AgentRole.Assistant, message.Text) + { + CurrentAgentId = agent.Id, + MessageId = conversations.Last().MessageId + }; + } + + // After chat completion hook + foreach (var hook in contentHooks) + { + await hook.AfterGenerated(responseMessage, new TokenStatsModel + { + Prompt = prompt, + Provider = Provider, + Model = _model, + PromptCount = response.Usage.InputTokens, + CompletionCount = response.Usage.OutputTokens + }); + } + + return responseMessage; + } + + public Task GetChatCompletionsAsync(Agent agent, List conversations, Func onMessageReceived, Func onFunctionExecuting) + { + throw new NotImplementedException(); + } + + public Task GetChatCompletionsStreamingAsync(Agent agent, List conversations, Func onMessageReceived) + { + throw new NotImplementedException(); + } + + private (string, MessageParameters) PrepareOptions(Agent agent, List conversations, LlmModelSetting settings) + { + var instruction = ""; + + var agentService = _services.GetRequiredService(); + + if (!string.IsNullOrEmpty(agent.Instruction)) + { + instruction += agentService.RenderedInstruction(agent); + } + + /*var routing = _services.GetRequiredService(); + var router = routing.Router; + + var render = _services.GetRequiredService(); + var template = router.Templates.FirstOrDefault(x => x.Name == "response_with_function").Content; + + var response_with_function = render.Render(template, new Dictionary + { + { "functions", agent.Functions } + }); + + prompt += "\r\n\r\n" + response_with_function;*/ + + var messages = new List(); + foreach (var conv in conversations) + { + if (conv.Role == AgentRole.User) + { + messages.Add(new Message(RoleType.User, conv.Payload ?? conv.Content)); + } + else if (conv.Role == AgentRole.Assistant) + { + messages.Add(new Message(RoleType.Assistant, conv.Content)); + } + else if (conv.Role == AgentRole.Function) + { + messages.Add(new Message + { + Role = RoleType.Assistant, + Content = new List + { + new ToolUseContent() + { + Id = conv.ToolCallId, + Name = conv.FunctionName, + Input = JsonNode.Parse(conv.FunctionArgs ?? "{}") + } + } + }); + + messages.Add(new Message() + { + Role = RoleType.User, + Content = new List + { + new ToolResultContent() + { + ToolUseId = conv.ToolCallId, + Content = conv.Content + } + } + }); + } + } + + var parameters = new MessageParameters() + { + Messages = messages, + MaxTokens = 256, + Model = settings.Version, // AnthropicModels.Claude3Haiku + Stream = false, + Temperature = 0m, + SystemMessage = instruction, + Tools = new List() { } + }; + + JsonSerializerOptions jsonSerializationOptions = new() + { + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + Converters = { new JsonStringEnumConverter() }, + ReferenceHandler = ReferenceHandler.IgnoreCycles, + }; + + foreach (var fn in agent.Functions) + { + /*var inputschema = new InputSchema() + { + Type = fn.Parameters.Type, + Properties = new Dictionary() + { + { "location", new Property() { Type = "string", Description = "The location of the weather" } }, + { + "tempType", new Property() + { + Type = "string", Enum = Enum.GetNames(typeof(TempType)), + Description = "The unit of temperature, celsius or fahrenheit" + } + } + }, + Required = fn.Parameters.Required + };*/ + + string jsonString = JsonSerializer.Serialize(fn.Parameters, jsonSerializationOptions); + parameters.Tools.Add(new Function(fn.Name, fn.Description, + JsonNode.Parse(jsonString))); + } + + var prompt = GetPrompt(parameters); + + return (prompt, parameters); + } + + private string GetPrompt(MessageParameters parameters) + { + var prompt = $"{parameters.SystemMessage}\r\n"; + prompt += "\r\n[CONVERSATION]"; + + var verbose = string.Join("\r\n", parameters.Messages + .Select(x => + { + var role = x.Role.ToString().ToLower(); + + if (x.Role == RoleType.User) + { + var content = string.Join("\r\n", x.Content.Select(c => + { + if (c is TextContent text) + return text.Text; + else if (c is ToolResultContent tool) + return $"{tool.Content}"; + else + return string.Empty; + })); + return $"{role}: {content}"; + } + else if (x.Role == RoleType.Assistant) + { + var content = string.Join("\r\n", x.Content.Select(c => + { + if (c is TextContent text) + return text.Text; + else if (c is ToolUseContent tool) + return $"Call function {tool.Name}({JsonSerializer.Serialize(tool.Input)})"; + else + return string.Empty; + })); + return $"{role}: {content}"; + } + return string.Empty; + })); + + prompt += $"\r\n{verbose}\r\n"; + + if (parameters.Tools != null && parameters.Tools.Count > 0) + { + var functions = string.Join("\r\n", parameters.Tools.Select(x => + { + return $"\r\n{x.Name}: {x.Description}\r\n{JsonSerializer.Serialize(x.Parameters)}"; + })); + prompt += $"\r\n[FUNCTIONS]\r\n{functions}\r\n"; + } + + return prompt; + } + + public void SetModelName(string model) + { + _model = model; + } +} diff --git a/src/Plugins/BotSharp.Plugin.AnthropicAI/Settings/AnthropicSettings.cs b/src/Plugins/BotSharp.Plugin.AnthropicAI/Settings/AnthropicSettings.cs new file mode 100644 index 00000000..12012528 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.AnthropicAI/Settings/AnthropicSettings.cs @@ -0,0 +1,6 @@ +namespace BotSharp.Plugin.AnthropicAI.Settings; + +public class AnthropicSettings +{ + public ClaudeSetting Claude { get; set; } +} diff --git a/src/Plugins/BotSharp.Plugin.AnthropicAI/Settings/ClaudeSetting.cs b/src/Plugins/BotSharp.Plugin.AnthropicAI/Settings/ClaudeSetting.cs new file mode 100644 index 00000000..4952754d --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.AnthropicAI/Settings/ClaudeSetting.cs @@ -0,0 +1,5 @@ +namespace BotSharp.Plugin.AnthropicAI.Settings; + +public class ClaudeSetting +{ +} diff --git a/src/Plugins/BotSharp.Plugin.AnthropicAI/Using.cs b/src/Plugins/BotSharp.Plugin.AnthropicAI/Using.cs new file mode 100644 index 00000000..d00446fc --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.AnthropicAI/Using.cs @@ -0,0 +1,22 @@ +global using System; +global using System.Collections.Generic; +global using System.Text; +global using System.Threading.Tasks; +global using System.Linq; +global using System.Text.Json; +global using Anthropic.SDK; +global using Anthropic.SDK.Constants; +global using Anthropic.SDK.Messaging; +global using BotSharp.Abstraction.Agents; +global using BotSharp.Abstraction.Agents.Enums; +global using BotSharp.Abstraction.Agents.Models; +global using BotSharp.Abstraction.Conversations.Models; +global using BotSharp.Abstraction.Functions.Models; +global using BotSharp.Abstraction.Loggers; +global using BotSharp.Abstraction.MLTasks; +global using BotSharp.Abstraction.Routing; +global using BotSharp.Abstraction.Templating; +global using BotSharp.Plugin.AnthropicAI.Settings; +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.Logging; +global using BotSharp.Abstraction.Utilities; diff --git a/src/Plugins/BotSharp.Plugin.AzureOpenAI/BotSharp.Plugin.AzureOpenAI.csproj b/src/Plugins/BotSharp.Plugin.AzureOpenAI/BotSharp.Plugin.AzureOpenAI.csproj index 00fb4060..6c76a9fb 100644 --- a/src/Plugins/BotSharp.Plugin.AzureOpenAI/BotSharp.Plugin.AzureOpenAI.csproj +++ b/src/Plugins/BotSharp.Plugin.AzureOpenAI/BotSharp.Plugin.AzureOpenAI.csproj @@ -1,4 +1,4 @@ - + netstandard2.1 @@ -11,7 +11,7 @@ - + diff --git a/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/ChatCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/ChatCompletionProvider.cs index e3f6afca..990a74ab 100644 --- a/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/ChatCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/ChatCompletionProvider.cs @@ -4,14 +4,19 @@ using BotSharp.Abstraction.Agents.Enums; using BotSharp.Abstraction.Agents.Models; using BotSharp.Abstraction.Conversations; using BotSharp.Abstraction.Conversations.Models; +using BotSharp.Abstraction.Files; +using BotSharp.Abstraction.Files.Models; using BotSharp.Abstraction.Loggers; using BotSharp.Abstraction.MLTasks; +using BotSharp.Abstraction.Utilities; using BotSharp.Plugin.AzureOpenAI.Settings; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; +using System.IO; using System.Linq; +using System.Runtime.InteropServices.ComTypes; using System.Threading.Tasks; namespace BotSharp.Plugin.AzureOpenAI.Providers; @@ -52,11 +57,7 @@ public class ChatCompletionProvider : IChatCompletion var choice = response.Value.Choices[0]; var message = choice.Message; - var responseMessage = new RoleDialogModel(AgentRole.Assistant, message.Content) - { - CurrentAgentId = agent.Id, - MessageId = conversations.Last().MessageId - }; + RoleDialogModel responseMessage; if (choice.FinishReason == CompletionsFinishReason.FunctionCall) { @@ -74,6 +75,33 @@ public class ChatCompletionProvider : IChatCompletion responseMessage.FunctionName = responseMessage.FunctionName.Split('.').Last(); } } + else if (choice.FinishReason == CompletionsFinishReason.ToolCalls) + { + // Add the assistant message with tool calls to the conversation history + // ChatRequestAssistantMessage toolCallHistoryMessage = new(message); + // chatCompletionsOptions.Messages.Add(toolCallHistoryMessage); + + // Add a new tool message for each tool call that is resolved + var toolCall = message.ToolCalls.First() as ChatCompletionsFunctionToolCall; + // var toolCallResponseMessage = GetToolCallResponseMessage(toolCall); + // Now make a new request with all the messages thus far, including the original + + responseMessage = new RoleDialogModel(AgentRole.Function, message.Content) + { + CurrentAgentId = agent.Id, + MessageId = conversations.Last().MessageId, + FunctionName = toolCall.Name, + FunctionArgs = toolCall.Arguments + }; + } + else + { + responseMessage = new RoleDialogModel(AgentRole.Assistant, message.Content) + { + CurrentAgentId = agent.Id, + MessageId = conversations.Last().MessageId + }; + } // After chat completion hook foreach(var hook in contentHooks) @@ -195,6 +223,17 @@ public class ChatCompletionProvider : IChatCompletion protected (string, ChatCompletionsOptions) PrepareOptions(Agent agent, List conversations) { var agentService = _services.GetRequiredService(); + var fileService = _services.GetRequiredService(); + var state = _services.GetRequiredService(); + var settingsService = _services.GetRequiredService(); + var settings = settingsService.GetSetting(Provider, _model); + var allowMultiModal = settings != null && settings.MultiModal; + + var chatFiles = new List(); + if (allowMultiModal) + { + chatFiles = fileService.GetChatImages(state.GetConversationId(), conversations, offset: 2).ToList(); + } var chatCompletionsOptions = new ChatCompletionsOptions(); @@ -221,11 +260,22 @@ public class ChatCompletionProvider : IChatCompletion { if (agentService.RenderFunction(agent, function)) { - chatCompletionsOptions.Functions.Add(new FunctionDefinition + var property = agentService.RenderFunctionProperty(agent, function); + + // legacy function call + /*chatCompletionsOptions.Functions.Add(new FunctionDefinition { Name = function.Name, Description = function.Description, - Parameters = BinaryData.FromObjectAsJson(function.Parameters) + Parameters = BinaryData.FromObjectAsJson(property) + });*/ + + // new chat tool + chatCompletionsOptions.Tools.Add(new ChatCompletionsFunctionToolDefinition + { + Name = function.Name, + Description = function.Description, + Parameters = BinaryData.FromObjectAsJson(property) }); } } @@ -240,20 +290,68 @@ public class ChatCompletionProvider : IChatCompletion }); chatCompletionsOptions.Messages.Add(new ChatRequestFunctionMessage(message.FunctionName, message.Content)); + // chatCompletionsOptions.Messages.Add(new ChatRequestToolMessage(message.Content, message.ToolCallId)); } else if (message.Role == ChatRole.User) { - var userMessage = new ChatRequestUserMessage(message.Content) - { - // To display Planner name in log - Name = message.FunctionName, - }; + var text = !string.IsNullOrWhiteSpace(message.Payload) ? message.Payload : message.Content; - if (!string.IsNullOrEmpty(message.ImageUrl)) + ChatRequestUserMessage userMessage = null; + if (allowMultiModal) { - var uri = new Uri(message.ImageUrl); - userMessage.MultimodalContentItems.Add( - new ChatMessageImageContentItem(uri, ChatMessageImageDetailLevel.Low)); + var chatItems = new List() + { + new ChatMessageTextContentItem(text) + }; + + var files = chatFiles.Where(x => x.MessageId == message.MessageId).ToList(); + if (!files.IsNullOrEmpty()) + { + foreach (var file in files) + { + using var stream = File.OpenRead(file.FileStorageUrl); + chatItems.Add(new ChatMessageImageContentItem(stream, file.ContentType, ChatMessageImageDetailLevel.Low)); + } + } + + if (!message.Files.IsNullOrEmpty()) + { + foreach (var file in message.Files) + { + if (!string.IsNullOrEmpty(file.FileUrl)) + { + var uri = new Uri(file.FileUrl); + chatItems.Add(new ChatMessageImageContentItem(uri, ChatMessageImageDetailLevel.Low)); + } + else if (!string.IsNullOrEmpty(file.FileData)) + { + var (contentType, bytes) = fileService.GetFileInfoFromData(file.FileData); + using var stream = new MemoryStream(bytes, 0, bytes.Length); + chatItems.Add(new ChatMessageImageContentItem(stream, contentType, ChatMessageImageDetailLevel.Low)); + } + } + } + + //if (!string.IsNullOrEmpty(message.ImageUrl)) + //{ + // var uri = new Uri(message.ImageUrl); + // userMessage.MultimodalContentItems.Add( + // new ChatMessageImageContentItem(uri, ChatMessageImageDetailLevel.Low)); + //} + + userMessage = new ChatRequestUserMessage(chatItems) + { + // To display Planner name in log + Name = message.FunctionName, + }; + } + else + { + userMessage = new ChatRequestUserMessage(text) + { + // To display Planner name in log + Name = message.FunctionName, + }; } chatCompletionsOptions.Messages.Add(userMessage); @@ -265,7 +363,7 @@ public class ChatCompletionProvider : IChatCompletion } // https://community.openai.com/t/cheat-sheet-mastering-temperature-and-top-p-in-chatgpt-api-a-few-tips-and-tricks-on-controlling-the-creativity-deterministic-output-of-prompt-responses/172683 - var state = _services.GetRequiredService(); + //var state = _services.GetRequiredService(); var temperature = float.Parse(state.GetState("temperature", "0.0")); var samplingFactor = float.Parse(state.GetState("sampling_factor", "0.0")); chatCompletionsOptions.Temperature = temperature; @@ -299,6 +397,7 @@ public class ChatCompletionProvider : IChatCompletion })); prompt += $"{verbose}\r\n"; + prompt += "\r\n[CONVERSATION]"; verbose = string.Join("\r\n", chatCompletionsOptions.Messages .Where(x => x.Role != AgentRole.System).Select(x => { @@ -310,9 +409,12 @@ public class ChatCompletionProvider : IChatCompletion else if (x.Role == ChatRole.User) { var m = x as ChatRequestUserMessage; - return !string.IsNullOrEmpty(m.Name) ? - $"{m.Name}: {m.Content}" : - $"{m.Role}: {m.Content}"; + var content = m.Content ?? string.Join(", ", m.MultimodalContentItems + .Where(m => m is ChatMessageTextContentItem) + .Select(m => (m as ChatMessageTextContentItem)?.Text)); + return !string.IsNullOrEmpty(m.Name) && m.Name != "route_to_agent" ? + $"{m.Name}: {content}" : + $"{m.Role}: {content}"; } else if (x.Role == ChatRole.Assistant) { @@ -329,13 +431,14 @@ public class ChatCompletionProvider : IChatCompletion prompt += $"\r\n{verbose}\r\n"; } - if (chatCompletionsOptions.Functions.Count > 0) + if (chatCompletionsOptions.Tools.Count > 0) { - var functions = string.Join("\r\n", chatCompletionsOptions.Functions.Select(x => + var functions = string.Join("\r\n", chatCompletionsOptions.Tools.Select(x => { - return $"\r\n{x.Name}: {x.Description}\r\n{x.Parameters}"; + var fn = x as ChatCompletionsFunctionToolDefinition; + return $"\r\n{fn.Name}: {fn.Description}\r\n{fn.Parameters}"; })); - prompt += $"\r\n[FUNCTIONS]\r\n{functions}\r\n"; + prompt += $"\r\n[FUNCTIONS]{functions}\r\n"; } return prompt; @@ -345,4 +448,15 @@ public class ChatCompletionProvider : IChatCompletion { _model = model; } + + ChatRequestToolMessage GetToolCallResponseMessage(ChatCompletionsToolCall toolCall) + { + var functionToolCall = toolCall as ChatCompletionsFunctionToolCall; + // Validate and process the JSON arguments for the function call + string unvalidatedArguments = functionToolCall.Arguments; + var functionResultData = (object)null; // GetYourFunctionResultData(unvalidatedArguments); + // Here, replacing with an example as if returned from "GetYourFunctionResultData" + functionResultData = "31 celsius"; + return new ChatRequestToolMessage(functionResultData.ToString(), toolCall.Id); + } } diff --git a/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/ChatHubConversationHook.cs b/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/ChatHubConversationHook.cs index 0ebff14c..13d308e1 100644 --- a/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/ChatHubConversationHook.cs +++ b/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/ChatHubConversationHook.cs @@ -45,7 +45,7 @@ public class ChatHubConversationHook : ConversationHookBase { ConversationId = conv.ConversationId, MessageId = message.MessageId, - Text = message.Content, + Text = !string.IsNullOrEmpty(message.SecondaryContent) ? message.SecondaryContent : message.Content, Sender = UserViewModel.FromUser(sender) }); @@ -59,6 +59,20 @@ public class ChatHubConversationHook : ConversationHookBase await base.OnMessageReceived(message); } + public override async Task OnFunctionExecuting(RoleDialogModel message) + { + var conv = _services.GetRequiredService(); + + await _chatHub.Clients.User(_user.Id).SendAsync("OnSenderActionGenerated", new ConversationSenderActionModel + { + ConversationId = conv.ConversationId, + SenderAction = SenderActionEnum.TypingOn, + Indication = message.Indication + }); + + await base.OnFunctionExecuting(message); + } + public override async Task OnPostbackMessageReceived(RoleDialogModel message, PostbackMessageModel replyMsg) { await this.OnMessageReceived(message); @@ -71,9 +85,9 @@ public class ChatHubConversationHook : ConversationHookBase { ConversationId = conv.ConversationId, MessageId = message.MessageId, - Text = message.Content, + Text = !string.IsNullOrEmpty(message.SecondaryContent) ? message.SecondaryContent : message.Content, Function = message.FunctionName, - RichContent = message.RichContent, + RichContent = message.SecondaryRichContent ?? message.RichContent, Data = message.Data, Sender = new UserViewModel() { diff --git a/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/StreamingLogHook.cs b/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/StreamingLogHook.cs index 2162bcb9..0d578f99 100644 --- a/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/StreamingLogHook.cs +++ b/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/StreamingLogHook.cs @@ -1,4 +1,5 @@ using BotSharp.Abstraction.Agents.Models; +using BotSharp.Abstraction.Conversations.Models; using BotSharp.Abstraction.Functions.Models; using BotSharp.Abstraction.Loggers; using BotSharp.Abstraction.Loggers.Enums; @@ -7,6 +8,8 @@ using BotSharp.Abstraction.Options; using BotSharp.Abstraction.Repositories; using BotSharp.Abstraction.Routing; using Microsoft.AspNetCore.SignalR; +using System.Text.Encodings.Web; +using System.Text.Unicode; namespace BotSharp.Plugin.ChatHub.Hooks; @@ -14,6 +17,7 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR { private readonly ConversationSetting _convSettings; private readonly BotSharpOptions _options; + private readonly JsonSerializerOptions _localJsonOptions; private readonly IServiceProvider _services; private readonly IHubContext _chatHub; private readonly IConversationStateService _state; @@ -39,13 +43,16 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR _user = user; _agentService = agentService; _routingCtx = routingCtx; + _localJsonOptions = InitLocalJsonOptions(options); } #region IConversationHook public override async Task OnMessageReceived(RoleDialogModel message) { var conversationId = _state.GetConversationId(); - var log = $"{message.Content}"; + if (string.IsNullOrEmpty(conversationId)) return; + + var log = $"{GetMessageContent(message)}"; var input = new ContentLogInputModel(conversationId, message) { @@ -59,7 +66,9 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR public override async Task OnPostbackMessageReceived(RoleDialogModel message, PostbackMessageModel replyMsg) { var conversationId = _state.GetConversationId(); - var log = $"{message.Content}"; + if (string.IsNullOrEmpty(conversationId)) return; + + var log = $"{GetMessageContent(message)}"; var replyContent = JsonSerializer.Serialize(replyMsg, _options.JsonSerializerOptions); log += $"\r\n```json\r\n{replyContent}\r\n```"; @@ -77,6 +86,7 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR if (!_convSettings.ShowVerboseLog) return; var conversationId = _state.GetConversationId(); + if (string.IsNullOrEmpty(conversationId)) return; var log = $"{agent.Name} is using template {name}"; var message = new RoleDialogModel(AgentRole.System, log) @@ -98,18 +108,39 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR if (!_convSettings.ShowVerboseLog) return; } - public override async Task OnFunctionExecuted(RoleDialogModel message) + public override async Task OnFunctionExecuting(RoleDialogModel message) { - if (message.FunctionName == "route_to_agent") - { - return; - } - var conversationId = _state.GetConversationId(); + if (string.IsNullOrEmpty(conversationId)) return; + + if (message.FunctionName == "route_to_agent") return; + var agent = await _agentService.LoadAgent(message.CurrentAgentId); message.FunctionArgs = message.FunctionArgs ?? "{}"; var args = JsonSerializer.Serialize(JsonDocument.Parse(message.FunctionArgs), _options.JsonSerializerOptions); - var log = $"*{message.FunctionName}*\r\n```json\r\n{args}\r\n```\r\n=> {message.Content?.Trim()}"; + var log = $"{message.FunctionName} executing\r\n```json\r\n{args}\r\n```"; + + var input = new ContentLogInputModel(conversationId, message) + { + Name = agent?.Name, + AgentId = agent?.Id, + Source = ContentLogSource.FunctionCall, + Log = log + }; + await _chatHub.Clients.User(_user.Id).SendAsync("OnConversationContentLogGenerated", BuildContentLog(input)); + } + + public override async Task OnFunctionExecuted(RoleDialogModel message) + { + var conversationId = _state.GetConversationId(); + if (string.IsNullOrEmpty(conversationId)) return; + + if (message.FunctionName == "route_to_agent") return; + + var agent = await _agentService.LoadAgent(message.CurrentAgentId); + message.FunctionArgs = message.FunctionArgs ?? "{}"; + // var args = JsonSerializer.Serialize(JsonDocument.Parse(message.FunctionArgs), _options.JsonSerializerOptions); + var log = $"{message.FunctionName} =>\r\n*{message.Content?.Trim()}*"; var input = new ContentLogInputModel(conversationId, message) { @@ -132,6 +163,8 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR if (!_convSettings.ShowVerboseLog) return; var conversationId = _state.GetConversationId(); + if (string.IsNullOrEmpty(conversationId)) return; + var agent = await _agentService.LoadAgent(message.CurrentAgentId); var log = tokenStats.Prompt; @@ -153,17 +186,19 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR /// public override async Task OnResponseGenerated(RoleDialogModel message) { - var conv = _services.GetRequiredService(); + var conversationId = _state.GetConversationId(); + if (string.IsNullOrEmpty(conversationId)) return; + var conv = _services.GetRequiredService(); await _chatHub.Clients.User(_user.Id).SendAsync("OnConversateStateLogGenerated", BuildStateLog(conv.ConversationId, _state.GetStates(), message)); if (message.Role == AgentRole.Assistant) { var agent = await _agentService.LoadAgent(message.CurrentAgentId); - var log = $"{message.Content}"; - if (message.RichContent != null) + var log = $"{GetMessageContent(message)}"; + if (message.RichContent != null || message.SecondaryRichContent != null) { - var richContent = JsonSerializer.Serialize(message.RichContent, _options.JsonSerializerOptions); + var richContent = JsonSerializer.Serialize(message.SecondaryRichContent ?? message.RichContent, _localJsonOptions); log += $"\r\n```json\r\n{richContent}\r\n```"; } @@ -181,7 +216,9 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR public override async Task OnTaskCompleted(RoleDialogModel message) { var conversationId = _state.GetConversationId(); - var log = $"{message.Content}"; + if (string.IsNullOrEmpty(conversationId)) return; + + var log = $"{GetMessageContent(message)}"; var agent = await _agentService.LoadAgent(message.CurrentAgentId); var input = new ContentLogInputModel(conversationId, message) @@ -196,6 +233,8 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR public override async Task OnConversationEnding(RoleDialogModel message) { var conversationId = _state.GetConversationId(); + if (string.IsNullOrEmpty(conversationId)) return; + var log = $"Conversation ended"; var agent = await _agentService.LoadAgent(message.CurrentAgentId); @@ -210,6 +249,8 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR public override async Task OnBreakpointUpdated(string conversationId, bool resetStates) { + if (string.IsNullOrEmpty(conversationId)) return; + var log = $"Conversation breakpoint is updated"; if (resetStates) { @@ -236,6 +277,9 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR public override async Task OnStateChanged(StateChangeModel stateChange) { + var conversationId = _state.GetConversationId(); + if (string.IsNullOrEmpty(conversationId)) return; + if (stateChange == null) return; await _chatHub.Clients.User(_user.Id).SendAsync("OnStateChangeGenerated", BuildStateChangeLog(stateChange)); @@ -246,6 +290,8 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR public async Task OnAgentEnqueued(string agentId, string preAgentId, string? reason = null) { var conversationId = _state.GetConversationId(); + if (string.IsNullOrEmpty(conversationId)) return; + var agent = await _agentService.LoadAgent(agentId); // Agent queue log @@ -271,6 +317,8 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR public async Task OnAgentDequeued(string agentId, string currentAgentId, string? reason = null) { var conversationId = _state.GetConversationId(); + if (string.IsNullOrEmpty(conversationId)) return; + var agent = await _agentService.LoadAgent(agentId); var currentAgent = await _agentService.LoadAgent(currentAgentId); @@ -297,6 +345,8 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR public async Task OnAgentReplaced(string fromAgentId, string toAgentId, string? reason = null) { var conversationId = _state.GetConversationId(); + if (string.IsNullOrEmpty(conversationId)) return; + var fromAgent = await _agentService.LoadAgent(fromAgentId); var toAgent = await _agentService.LoadAgent(toAgentId); @@ -323,6 +373,7 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR public async Task OnAgentQueueEmptied(string agentId, string? reason = null) { var conversationId = _state.GetConversationId(); + if (string.IsNullOrEmpty(conversationId)) return; // Agent queue log var log = $"Agent queue is empty"; @@ -347,6 +398,8 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR public async Task OnRoutingInstructionReceived(FunctionCallFromLlm instruct, RoleDialogModel message) { var conversationId = _state.GetConversationId(); + if (string.IsNullOrEmpty(conversationId)) return; + var agent = await _agentService.LoadAgent(message.CurrentAgentId); var log = JsonSerializer.Serialize(instruct, _options.JsonSerializerOptions); log = $"```json\r\n{log}\r\n```"; @@ -364,6 +417,8 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR public async Task OnRoutingInstructionRevised(FunctionCallFromLlm instruct, RoleDialogModel message) { var conversationId = _state.GetConversationId(); + if (string.IsNullOrEmpty(conversationId)) return; + var agent = await _agentService.LoadAgent(message.CurrentAgentId); var log = $"Revised user goal agent to {instruct.OriginalAgent}"; @@ -390,7 +445,7 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR Role = input.Message.Role, Content = input.Log, Source = input.Source, - CreateTime = input.Message.CreatedAt + CreateTime = DateTime.UtcNow }; var json = JsonSerializer.Serialize(output, _options.JsonSerializerOptions); @@ -412,7 +467,7 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR ConversationId = conversationId, MessageId = message.MessageId, States = states, - CreateTime = message.CreatedAt + CreateTime = DateTime.UtcNow }; var convSettings = _services.GetRequiredService(); @@ -436,6 +491,9 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR BeforeActiveRounds = stateChange.BeforeActiveRounds, AfterValue = stateChange.AfterValue, AfterActiveRounds = stateChange.AfterActiveRounds, + DataType = stateChange.DataType, + Source = stateChange.Source, + Readonly = stateChange.Readonly, CreateTime = DateTime.UtcNow }; @@ -453,4 +511,31 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR return JsonSerializer.Serialize(model, _options.JsonSerializerOptions); } + + private string GetMessageContent(RoleDialogModel message) + { + return !string.IsNullOrEmpty(message.SecondaryContent) ? message.SecondaryContent : message.Content; + } + + private JsonSerializerOptions InitLocalJsonOptions(BotSharpOptions options) + { + var localOptions = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + AllowTrailingCommas = true, + WriteIndented = true, + Encoder = JavaScriptEncoder.Create(UnicodeRanges.All) + }; + + if (options?.JsonSerializerOptions != null && !options.JsonSerializerOptions.Converters.IsNullOrEmpty()) + { + foreach (var converter in options.JsonSerializerOptions.Converters) + { + localOptions.Converters.Add(converter); + } + } + + return localOptions; + } } diff --git a/src/Plugins/BotSharp.Plugin.ChatHub/WebSocketsMiddleware.cs b/src/Plugins/BotSharp.Plugin.ChatHub/WebSocketsMiddleware.cs index bd3e90aa..79cb803a 100644 --- a/src/Plugins/BotSharp.Plugin.ChatHub/WebSocketsMiddleware.cs +++ b/src/Plugins/BotSharp.Plugin.ChatHub/WebSocketsMiddleware.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Http; +using System.Text.RegularExpressions; namespace BotSharp.Plugin.ChatHub; @@ -17,7 +18,7 @@ public class WebSocketsMiddleware // web sockets cannot pass headers so we must take the access token from query param and // add it to the header before authentication middleware runs - if (request.Path.StartsWithSegments("/chatHub", StringComparison.OrdinalIgnoreCase) && + if ((VerifyChatHubRequest(request) || VerifyGetRequest(request)) && request.Query.TryGetValue("access_token", out var accessToken)) { request.Headers["Authorization"] = $"Bearer {accessToken}"; @@ -25,4 +26,20 @@ public class WebSocketsMiddleware await _next(httpContext); } + + private bool VerifyChatHubRequest(HttpRequest request) + { + return request.Path.StartsWithSegments("/chatHub", StringComparison.OrdinalIgnoreCase); + } + + private bool VerifyGetRequest(HttpRequest request) + { + var regexes = new List + { + new Regex(@"/conversation/[a-z0-9-]+/message/[a-z0-9-]+/file/[a-z0-9-]+", RegexOptions.IgnoreCase), + new Regex(@"/user/avatar", RegexOptions.IgnoreCase) + }; + + return request.Method.IsEqualTo("GET") && regexes.Any(x => x.IsMatch(request.Path.Value ?? string.Empty)); + } } diff --git a/src/Plugins/BotSharp.Plugin.HttpHandler/BotSharp.Plugin.HttpHandler.csproj b/src/Plugins/BotSharp.Plugin.HttpHandler/BotSharp.Plugin.HttpHandler.csproj index 15ceb9d3..2607cb15 100644 --- a/src/Plugins/BotSharp.Plugin.HttpHandler/BotSharp.Plugin.HttpHandler.csproj +++ b/src/Plugins/BotSharp.Plugin.HttpHandler/BotSharp.Plugin.HttpHandler.csproj @@ -28,6 +28,10 @@ + + + + diff --git a/src/Plugins/BotSharp.Plugin.HttpHandler/Functions/HandleHttpRequest.cs b/src/Plugins/BotSharp.Plugin.HttpHandler/Functions/HandleHttpRequest.cs new file mode 100644 index 00000000..238a1904 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.HttpHandler/Functions/HandleHttpRequest.cs @@ -0,0 +1,210 @@ +using System.Net.Http; +using BotSharp.Plugin.HttpHandler.LlmContexts; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; + +namespace BotSharp.Plugin.HttpHandler.Functions; + +public class HandleHttpRequest : IFunctionCallback +{ + public string Name => "handle_http_request"; + public string Indication => "Handling http request"; + + private readonly IServiceProvider _services; + private readonly ILogger _logger; + private readonly IHttpClientFactory _httpClientFactory; + private readonly IHttpContextAccessor _context; + private readonly BotSharpOptions _options; + + public HandleHttpRequest(IServiceProvider services, + ILogger logger, + IHttpClientFactory httpClientFactory, + IHttpContextAccessor context, + BotSharpOptions options) + { + _services = services; + _logger = logger; + _httpClientFactory = httpClientFactory; + _context = context; + _options = options; + } + + public async Task Execute(RoleDialogModel message) + { + var args = JsonSerializer.Deserialize(message.FunctionArgs, _options.JsonSerializerOptions); + var url = args?.RequestUrl; + var method = args?.HttpMethod; + var content = args?.RequestContent; + + try + { + var response = await SendHttpRequest(url, method, content); + var responseContent = await HandleHttpResponse(response); + message.RichContent = BuildRichContent(responseContent); + return await Task.FromResult(true); + } + catch (Exception ex) + { + var msg = $"Fail when sending http request. Url: {url}, method: {method}, content: {content}"; + _logger.LogWarning($"{msg}\n(Error: {ex.Message})"); + message.RichContent = BuildRichContent($"{msg}"); + return await Task.FromResult(false); + } + } + + private async Task SendHttpRequest(string? url, string? method, string? content) + { + if (string.IsNullOrEmpty(url)) return null; + + var settings = _services.GetRequiredService(); + using var client = _httpClientFactory.CreateClient(); + AddRequestHeaders(client); + + var (uri, request) = BuildHttpRequest(url, method, content); + if (string.IsNullOrEmpty(uri.Host)) + { + client.BaseAddress = new Uri(settings.BaseAddress); + } + + var response = await client.SendAsync(request); + + if (response == null || !response.IsSuccessStatusCode) + { + throw new Exception($"Status code: {response?.StatusCode}"); + } + + return response; + } + + private void AddRequestHeaders(HttpClient client) + { + client.DefaultRequestHeaders.Add("Authorization", $"{_context.HttpContext.Request.Headers["Authorization"]}"); + + var settings = _services.GetRequiredService(); + var origin = !string.IsNullOrEmpty(settings.Origin) ? settings.Origin : $"{_context.HttpContext.Request.Headers["Origin"]}"; + if (!string.IsNullOrEmpty(origin)) + { + client.DefaultRequestHeaders.Add("Origin", origin); + } + } + + private (Uri, HttpRequestMessage) BuildHttpRequest(string url, string? method, string? content) + { + var httpMethod = GetHttpMethod(method); + StringContent httpContent; + + if (httpMethod == HttpMethod.Get) + { + httpContent = BuildHttpContent("{}"); + } + else + { + httpContent = BuildHttpContent(content); + } + + var requestUrl = BuildQuery(url, content); + var uri = new Uri(requestUrl); + return (uri, new HttpRequestMessage + { + RequestUri = uri, + Method = httpMethod, + Content = httpContent + }); + } + + private HttpMethod GetHttpMethod(string? method) + { + var localMethod = method?.Trim()?.ToUpper(); + HttpMethod matchMethod; + + switch (localMethod) + { + case "GET": + matchMethod = HttpMethod.Get; + break; + case "DELETE": + matchMethod = HttpMethod.Delete; + break; + case "PUT": + matchMethod = HttpMethod.Put; + break; + case "Patch": + matchMethod = HttpMethod.Patch; + break; + default: + matchMethod = HttpMethod.Post; + break; + } + return matchMethod; + } + + private StringContent BuildHttpContent(string? content) + { + var str = string.Empty; + try + { + var json = JsonSerializer.Deserialize(content ?? "{}", _options.JsonSerializerOptions); + str = JsonSerializer.Serialize(json, _options.JsonSerializerOptions); + } + catch (Exception ex) + { + _logger.LogWarning($"Error when build http content: {content}\n(Error: {ex.Message})"); + } + + return new StringContent(str, Encoding.UTF8, "application/json"); + } + + private string BuildQuery(string url, string? content) + { + if (string.IsNullOrEmpty(url) || string.IsNullOrEmpty(content)) return url; + + try + { + var queries = new List(); + var json = JsonSerializer.Deserialize(content, _options.JsonSerializerOptions); + var root = json.RootElement; + foreach (var prop in root.EnumerateObject()) + { + var name = prop.Name.Trim(); + var value = prop.Value.ToString().Trim(); + if (string.IsNullOrEmpty(name) || string.IsNullOrEmpty(value)) + { + continue; + } + + queries.Add($"{name}={value}"); + } + + if (!queries.IsNullOrEmpty()) + { + url += $"?{string.Join('&', queries)}"; + } + return url; + } + catch (Exception ex) + { + _logger.LogWarning($"Error when building url query. Url: {url}, Content: {content}\n(Error: {ex.Message})"); + return url; + } + } + + private async Task HandleHttpResponse(HttpResponseMessage? response) + { + if (response == null) return string.Empty; + + return await response.Content.ReadAsStringAsync(); + } + + private RichContent BuildRichContent(string? content) + { + var state = _services.GetRequiredService(); + + var text = !string.IsNullOrEmpty(content) ? content : "Cannot get any response from the http request."; + return new RichContent + { + Recipient = new Recipient { Id = state.GetConversationId() }, + Editor = EditorTypeEnum.Text, + Message = new TextMessage(text) + }; + } +} diff --git a/src/Plugins/BotSharp.Plugin.HttpHandler/HttpHandlerPlugin.cs b/src/Plugins/BotSharp.Plugin.HttpHandler/HttpHandlerPlugin.cs index b64a3ad8..3e31c096 100644 --- a/src/Plugins/BotSharp.Plugin.HttpHandler/HttpHandlerPlugin.cs +++ b/src/Plugins/BotSharp.Plugin.HttpHandler/HttpHandlerPlugin.cs @@ -1,3 +1,5 @@ +using BotSharp.Abstraction.Http.Settings; +using BotSharp.Abstraction.Settings; using Microsoft.Extensions.Configuration; namespace BotSharp.Plugin.HttpHandler; @@ -12,6 +14,10 @@ public class HttpHandlerPlugin : IBotSharpPlugin public void RegisterDI(IServiceCollection services, IConfiguration config) { - + services.AddScoped(provider => + { + var settingService = provider.GetRequiredService(); + return settingService.Bind("Http"); + }); } } diff --git a/src/Plugins/BotSharp.Plugin.HttpHandler/LlmContexts/LlmContextIn.cs b/src/Plugins/BotSharp.Plugin.HttpHandler/LlmContexts/LlmContextIn.cs new file mode 100644 index 00000000..8d7f506e --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.HttpHandler/LlmContexts/LlmContextIn.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace BotSharp.Plugin.HttpHandler.LlmContexts; + +public class LlmContextIn +{ + [JsonPropertyName("request_url")] + public string? RequestUrl { get; set; } + + [JsonPropertyName("http_method")] + public string? HttpMethod { get; set; } + + [JsonPropertyName("request_content")] + public string? RequestContent { get; set; } +} diff --git a/src/Plugins/BotSharp.Plugin.HttpHandler/Using.cs b/src/Plugins/BotSharp.Plugin.HttpHandler/Using.cs index f813420b..4344b430 100644 --- a/src/Plugins/BotSharp.Plugin.HttpHandler/Using.cs +++ b/src/Plugins/BotSharp.Plugin.HttpHandler/Using.cs @@ -11,4 +11,9 @@ global using BotSharp.Abstraction.Agents.Models; global using BotSharp.Abstraction.Templating; global using Microsoft.Extensions.DependencyInjection; global using System.Linq; -global using BotSharp.Abstraction.Utilities; \ No newline at end of file +global using BotSharp.Abstraction.Utilities; +global using BotSharp.Abstraction.Messaging; +global using BotSharp.Abstraction.Messaging.Models.RichContent; +global using BotSharp.Abstraction.Options; +global using BotSharp.Abstraction.Http.Settings; +global using BotSharp.Abstraction.Messaging.Enums; \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.LLamaSharp/BotSharp.Plugin.LLamaSharp.csproj b/src/Plugins/BotSharp.Plugin.LLamaSharp/BotSharp.Plugin.LLamaSharp.csproj index 5bdf5944..722dfa41 100644 --- a/src/Plugins/BotSharp.Plugin.LLamaSharp/BotSharp.Plugin.LLamaSharp.csproj +++ b/src/Plugins/BotSharp.Plugin.LLamaSharp/BotSharp.Plugin.LLamaSharp.csproj @@ -11,7 +11,7 @@ - + diff --git a/src/Plugins/BotSharp.Plugin.LLamaSharp/Providers/TextEmbeddingProvider.cs b/src/Plugins/BotSharp.Plugin.LLamaSharp/Providers/TextEmbeddingProvider.cs index 12006342..9a562000 100644 --- a/src/Plugins/BotSharp.Plugin.LLamaSharp/Providers/TextEmbeddingProvider.cs +++ b/src/Plugins/BotSharp.Plugin.LLamaSharp/Providers/TextEmbeddingProvider.cs @@ -27,7 +27,7 @@ public class TextEmbeddingProvider : ITextEmbedding _embedder = new LLamaEmbedder(weights, @params); } - return Task.FromResult(_embedder.GetEmbeddings(text)); + return _embedder.GetEmbeddings(text); } public Task> GetVectorsAsync(List texts) diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/BotSharp.Plugin.MongoStorage.csproj b/src/Plugins/BotSharp.Plugin.MongoStorage/BotSharp.Plugin.MongoStorage.csproj index 41c63ad9..9b1d2178 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/BotSharp.Plugin.MongoStorage.csproj +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/BotSharp.Plugin.MongoStorage.csproj @@ -11,7 +11,7 @@ - + diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Collections/ConversationStateDocument.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Collections/ConversationStateDocument.cs index 42331616..1f8f0e90 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Collections/ConversationStateDocument.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Collections/ConversationStateDocument.cs @@ -5,6 +5,6 @@ namespace BotSharp.Plugin.MongoStorage.Collections; public class ConversationStateDocument : MongoBase { public string ConversationId { get; set; } - public List States { get; set; } - public List Breakpoints { get; set; } + public List States { get; set; } = new List(); + public List Breakpoints { get; set; } = new List(); } diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Collections/UserDocument.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Collections/UserDocument.cs index c21a6e08..c277b9f3 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Collections/UserDocument.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Collections/UserDocument.cs @@ -13,7 +13,8 @@ public class UserDocument : MongoBase public string Source { get; set; } = "internal"; public string? ExternalId { get; set; } public string Role { get; set; } - + public string? VerificationCode { get; set; } + public bool Verified { get; set; } public DateTime CreatedTime { get; set; } public DateTime UpdatedTime { get; set; } @@ -30,7 +31,9 @@ public class UserDocument : MongoBase Salt = Salt, Source = Source, ExternalId = ExternalId, - Role = Role + Role = Role, + VerificationCode = VerificationCode, + Verified = Verified, }; } } \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Models/BreakpointMongoElement.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Models/BreakpointMongoElement.cs index 96c1fac2..db17d353 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Models/BreakpointMongoElement.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Models/BreakpointMongoElement.cs @@ -5,4 +5,5 @@ public class BreakpointMongoElement public string? MessageId { get; set; } public DateTime Breakpoint { get; set; } public DateTime CreatedTime { get; set; } + public string? Reason { get; set; } } diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Models/DialogMongoElement.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Models/DialogMongoElement.cs index a35d3fc1..5c2a1698 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Models/DialogMongoElement.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Models/DialogMongoElement.cs @@ -6,7 +6,10 @@ public class DialogMongoElement { public DialogMetaDataMongoElement MetaData { get; set; } public string Content { get; set; } + public string? SecondaryContent { get; set; } public string? RichContent { get; set; } + public string? SecondaryRichContent { get; set; } + public string? Payload { get; set; } public DialogMongoElement() { @@ -19,7 +22,10 @@ public class DialogMongoElement { MetaData = DialogMetaDataMongoElement.ToMongoElement(dialog.MetaData), Content = dialog.Content, - RichContent = dialog.RichContent + SecondaryContent = dialog.SecondaryContent, + RichContent = dialog.RichContent, + SecondaryRichContent = dialog.SecondaryRichContent, + Payload = dialog.Payload }; } @@ -29,7 +35,10 @@ public class DialogMongoElement { MetaData = DialogMetaDataMongoElement.ToDomainElement(dialog.MetaData), Content = dialog.Content, - RichContent = dialog.RichContent + SecondaryContent = dialog.SecondaryContent, + RichContent = dialog.RichContent, + SecondaryRichContent = dialog.SecondaryRichContent, + Payload = dialog.Payload }; } } diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Models/RoutingRuleMongoElement.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Models/RoutingRuleMongoElement.cs index 571c6d09..293ffc47 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Models/RoutingRuleMongoElement.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Models/RoutingRuleMongoElement.cs @@ -10,6 +10,7 @@ public class RoutingRuleMongoElement public bool Required { get; set; } public string? RedirectTo { get; set; } public string Type { get; set; } + public string FieldType { get; set; } public RoutingRuleMongoElement() { @@ -25,6 +26,7 @@ public class RoutingRuleMongoElement Required = routingRule.Required, RedirectTo = routingRule.RedirectTo, Type = routingRule.Type, + FieldType = routingRule.FieldType }; } @@ -39,6 +41,12 @@ public class RoutingRuleMongoElement Required = rule.Required, RedirectTo = rule.RedirectTo, Type = rule.Type, + FieldType= rule.FieldType }; } + + public override string ToString() + { + return $"{Field} - {FieldType}, Required: {Required} ({Type})"; + } } diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Models/StateMongoElement.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Models/StateMongoElement.cs index b56483e1..a939f59c 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Models/StateMongoElement.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Models/StateMongoElement.cs @@ -6,6 +6,7 @@ public class StateMongoElement { public string Key { get; set; } public bool Versioning { get; set; } + public bool Readonly { get; set; } public List Values { get; set; } public static StateMongoElement ToMongoElement(StateKeyValue state) @@ -14,6 +15,7 @@ public class StateMongoElement { Key = state.Key, Versioning = state.Versioning, + Readonly = state.Readonly, Values = state.Values?.Select(x => StateValueMongoElement.ToMongoElement(x))?.ToList() ?? new List() }; } @@ -24,6 +26,7 @@ public class StateMongoElement { Key = state.Key, Versioning = state.Versioning, + Readonly = state.Readonly, Values = state.Values?.Select(x => StateValueMongoElement.ToDomainElement(x))?.ToList() ?? new List() }; } @@ -35,6 +38,9 @@ public class StateValueMongoElement public string? MessageId { get; set; } public bool Active { get; set; } public int ActiveRounds { get; set; } + public string DataType { get; set; } + public string Source { get; set; } + public DateTime UpdateTime { get; set; } public static StateValueMongoElement ToMongoElement(StateValue element) @@ -45,6 +51,8 @@ public class StateValueMongoElement MessageId = element.MessageId, Active = element.Active, ActiveRounds = element.ActiveRounds, + DataType = element.DataType, + Source = element.Source, UpdateTime = element.UpdateTime }; } @@ -57,6 +65,8 @@ public class StateValueMongoElement MessageId = element.MessageId, Active = element.Active, ActiveRounds = element.ActiveRounds, + DataType= element.DataType, + Source = element.Source, UpdateTime = element.UpdateTime }; } diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/MongoStoragePlugin.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/MongoStoragePlugin.cs index a7a574fa..7d47a98c 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/MongoStoragePlugin.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/MongoStoragePlugin.cs @@ -1,4 +1,6 @@ using BotSharp.Abstraction.Plugins.Models; +using BotSharp.Abstraction.Repositories.Enums; +using BotSharp.Abstraction.Users.Enums; using BotSharp.Plugin.MongoStorage.Repository; namespace BotSharp.Plugin.MongoStorage; @@ -18,7 +20,7 @@ public class MongoStoragePlugin : IBotSharpPlugin var dbSettings = new BotSharpDatabaseSettings(); config.Bind("Database", dbSettings); - if (dbSettings.Default == "MongoRepository") + if (dbSettings.Default == RepositoryEnum.MongoRepository) { services.AddScoped((IServiceProvider x) => { @@ -33,7 +35,10 @@ public class MongoStoragePlugin : IBotSharpPlugin public bool AttachMenu(List menu) { var section = menu.First(x => x.Label == "Apps"); - menu.Add(new PluginMenuDef("MongoDB", icon: "bx bx-data", link: "page/mongodb", weight: section.Weight + 10)); + menu.Add(new PluginMenuDef("MongoDB", icon: "bx bx-data", link: "page/mongodb", weight: section.Weight + 10) + { + Roles = new List { UserRole.Admin } + }); return true; } } diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Agent.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Agent.cs index 3272930b..8cffb61f 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Agent.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Agent.cs @@ -332,6 +332,23 @@ public partial class MongoRepository return agent.Templates?.FirstOrDefault(x => x.Name == templateName.ToLower())?.Content ?? string.Empty; } + public bool PatchAgentTemplate(string agentId, AgentTemplate template) + { + if (string.IsNullOrEmpty(agentId) || template == null) return false; + + var filter = Builders.Filter.Eq(x => x.Id, agentId); + var agent = _dc.Agents.Find(filter).FirstOrDefault(); + if (agent == null || agent.Templates.IsNullOrEmpty()) return false; + + var foundTemplate = agent.Templates.FirstOrDefault(x => x.Name.IsEqualTo(template.Name)); + if (foundTemplate == null) return false; + + foundTemplate.Content = template.Content; + var update = Builders.Update.Set(x => x.Templates, agent.Templates); + _dc.Agents.UpdateOne(filter, update); + return true; + } + public void BulkInsertAgents(List agents) { if (agents.IsNullOrEmpty()) return; @@ -398,7 +415,27 @@ public partial class MongoRepository { return false; } + } + public bool DeleteAgent(string agentId) + { + try + { + if (string.IsNullOrEmpty(agentId)) return false; + + var agentFilter = Builders.Filter.Eq(x => x.Id, agentId); + var agentUserFilter = Builders.Filter.Eq(x => x.AgentId, agentId); + var agentTaskFilter = Builders.Filter.Eq(x => x.AgentId, agentId); + + _dc.Agents.DeleteOne(agentFilter); + _dc.UserAgents.DeleteMany(agentUserFilter); + _dc.AgentTasks.DeleteMany(agentTaskFilter); + return true; + } + catch + { + return false; + } } private Agent TransformAgentDocument(AgentDocument? agentDoc) diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.AgentTask.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.AgentTask.cs index 125f3cd8..a3df61da 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.AgentTask.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.AgentTask.cs @@ -155,12 +155,16 @@ public partial class MongoRepository _dc.AgentTasks.ReplaceOne(filter, taskDoc); } - public bool DeleteAgentTask(string agentId, string taskId) + public bool DeleteAgentTask(string agentId, List taskIds) { - if (string.IsNullOrEmpty(taskId)) return false; + if (taskIds.IsNullOrEmpty()) return false; - var filter = Builders.Filter.Eq(x => x.Id, taskId); - var taskDeleted = _dc.AgentTasks.DeleteOne(filter); + var builder = Builders.Filter; + var filters = new List> + { + builder.In(x => x.Id, taskIds) + }; + var taskDeleted = _dc.AgentTasks.DeleteMany(builder.And(filters)); return taskDeleted.DeletedCount > 0; } diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Conversation.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Conversation.cs index 7d9eaea7..aefd0db5 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Conversation.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Conversation.cs @@ -1,9 +1,9 @@ using BotSharp.Abstraction.Conversations.Models; +using BotSharp.Abstraction.Files; using BotSharp.Abstraction.Repositories.Filters; using BotSharp.Abstraction.Repositories.Models; using BotSharp.Plugin.MongoStorage.Collections; using BotSharp.Plugin.MongoStorage.Models; -using System.Text.RegularExpressions; namespace BotSharp.Plugin.MongoStorage.Repository; @@ -34,31 +34,12 @@ public partial class MongoRepository Dialogs = new List() }; - var states = conversation.States ?? new Dictionary(); - var initialStates = states.Select(x => new StateMongoElement - { - Key = x.Key, - Values = new List - { - new StateValueMongoElement { Data = x.Value, UpdateTime = DateTime.UtcNow } - } - }).ToList(); - - var initialBreakpoints = new List() - { - new BreakpointMongoElement - { - Breakpoint = utcNow.AddMilliseconds(-100), - CreatedTime = utcNow - } - }; - var stateDoc = new ConversationStateDocument { Id = Guid.NewGuid().ToString(), ConversationId = convDoc.Id, - States = initialStates, - Breakpoints = initialBreakpoints + States = new List(), + Breakpoints = new List() }; _dc.Conversations.InsertOne(convDoc); @@ -104,27 +85,6 @@ public partial class MongoRepository return formattedDialog ?? new List(); } - public void UpdateConversationDialogElements(string conversationId, List updateElements) - { - if (string.IsNullOrEmpty(conversationId) || updateElements.IsNullOrEmpty()) return; - - var filterDialog = Builders.Filter.Eq(x => x.ConversationId, conversationId); - var foundDialog = _dc.ConversationDialogs.Find(filterDialog).FirstOrDefault(); - if (foundDialog == null || foundDialog.Dialogs.IsNullOrEmpty()) return; - - foundDialog.Dialogs = foundDialog.Dialogs.Select((x, idx) => - { - var found = updateElements.FirstOrDefault(e => e.Index == idx); - if (found != null) - { - x.Content = found.UpdateContent; - } - return x; - }).ToList(); - - _dc.ConversationDialogs.ReplaceOne(filterDialog, foundDialog); - } - public void AppendConversationDialogs(string conversationId, List dialogs) { if (string.IsNullOrEmpty(conversationId)) return; @@ -152,15 +112,16 @@ public partial class MongoRepository _dc.Conversations.UpdateOne(filterConv, updateConv); } - public void UpdateConversationBreakpoint(string conversationId, string messageId, DateTime breakpoint) + public void UpdateConversationBreakpoint(string conversationId, ConversationBreakpoint breakpoint) { if (string.IsNullOrEmpty(conversationId)) return; var newBreakpoint = new BreakpointMongoElement() { - MessageId = messageId, - Breakpoint = breakpoint, - CreatedTime = DateTime.UtcNow + MessageId = breakpoint.MessageId, + Breakpoint = breakpoint.Breakpoint, + CreatedTime = DateTime.UtcNow, + Reason = breakpoint.Reason }; var filterState = Builders.Filter.Eq(x => x.ConversationId, conversationId); var updateState = Builders.Update.Push(x => x.Breakpoints, newBreakpoint); @@ -168,22 +129,29 @@ public partial class MongoRepository _dc.ConversationStates.UpdateOne(filterState, updateState); } - public DateTime GetConversationBreakpoint(string conversationId) + public ConversationBreakpoint? GetConversationBreakpoint(string conversationId) { if (string.IsNullOrEmpty(conversationId)) { - return default; + return null; } var filter = Builders.Filter.Eq(x => x.ConversationId, conversationId); var state = _dc.ConversationStates.Find(filter).FirstOrDefault(); + var leafNode = state?.Breakpoints?.LastOrDefault(); - if (state == null || state.Breakpoints.IsNullOrEmpty()) + if (leafNode == null) { - return default; + return null; } - return state.Breakpoints.LastOrDefault()?.Breakpoint ?? default; + return new ConversationBreakpoint + { + Breakpoint = leafNode.Breakpoint, + MessageId = leafNode.MessageId, + Reason = leafNode.Reason, + CreatedTime = leafNode.CreatedTime, + }; } public ConversationState GetConversationStates(string conversationId) @@ -203,14 +171,11 @@ public partial class MongoRepository { if (string.IsNullOrEmpty(conversationId) || states == null) return; - var filterConv = Builders.Filter.Eq(x => x.Id, conversationId); var filterStates = Builders.Filter.Eq(x => x.ConversationId, conversationId); var saveStates = states.Select(x => StateMongoElement.ToMongoElement(x)).ToList(); var updateStates = Builders.Update.Set(x => x.States, saveStates); - var updateConv = Builders.Update.Set(x => x.UpdatedTime, DateTime.UtcNow); _dc.ConversationStates.UpdateOne(filterStates, updateStates); - _dc.Conversations.UpdateOne(filterConv, updateConv); } public void UpdateConversationStatus(string conversationId, string status) @@ -403,7 +368,7 @@ public partial class MongoRepository { var skip = (page - 1) * batchSize; var candidates = _dc.Conversations.AsQueryable() - .Where(x => (x.DialogCount <= messageLimit) && x.UpdatedTime <= utcNow.AddHours(-bufferHours)) + .Where(x => x.DialogCount <= messageLimit && x.UpdatedTime <= utcNow.AddHours(-bufferHours)) .Skip(skip) .Take(batchSize) .Select(x => x.Id) @@ -426,16 +391,29 @@ public partial class MongoRepository return conversationIds.Take(batchSize).ToList(); } - public bool TruncateConversation(string conversationId, string messageId, bool cleanLog = false) + public IEnumerable TruncateConversation(string conversationId, string messageId, bool cleanLog = false) { - if (string.IsNullOrEmpty(conversationId) || string.IsNullOrEmpty(messageId)) return false; + var deletedMessageIds = new List(); + if (string.IsNullOrEmpty(conversationId) || string.IsNullOrEmpty(messageId)) + { + return deletedMessageIds; + } var dialogFilter = Builders.Filter.Eq(x => x.ConversationId, conversationId); var foundDialog = _dc.ConversationDialogs.Find(dialogFilter).FirstOrDefault(); - if (foundDialog == null || foundDialog.Dialogs.IsNullOrEmpty()) return false; + if (foundDialog == null || foundDialog.Dialogs.IsNullOrEmpty()) + { + return deletedMessageIds; + } var foundIdx = foundDialog.Dialogs.FindIndex(x => x.MetaData?.MessageId == messageId); - if (foundIdx < 0) return false; + if (foundIdx < 0) + { + return deletedMessageIds; + } + + deletedMessageIds = foundDialog.Dialogs.Where((x, idx) => idx >= foundIdx && !string.IsNullOrEmpty(x.MetaData?.MessageId)) + .Select(x => x.MetaData.MessageId).Distinct().ToList(); // Handle truncated dialogs var truncatedDialogs = foundDialog.Dialogs.Where((x, idx) => idx < foundIdx).ToList(); @@ -474,8 +452,7 @@ public partial class MongoRepository if (!foundStates.Breakpoints.IsNullOrEmpty()) { var breakpoints = foundStates.Breakpoints ?? new List(); - var targetIdx = breakpoints.FindIndex(x => x.MessageId == messageId); - var truncatedBreakpoints = breakpoints.Where((x, idx) => idx < targetIdx).ToList(); + var truncatedBreakpoints = breakpoints.Where(x => x.CreatedTime < refTime).ToList(); foundStates.Breakpoints = truncatedBreakpoints; } @@ -514,6 +491,6 @@ public partial class MongoRepository _dc.StateLogs.DeleteMany(stateLogBuilder.And(stateLogFilters)); } - return true; + return deletedMessageIds; } } diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.User.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.User.cs index 1b6b8b77..1a5211f4 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.User.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.User.cs @@ -40,10 +40,20 @@ public partial class MongoRepository Source = user.Source, ExternalId = user.ExternalId, Role = user.Role, + VerificationCode = user.VerificationCode, + Verified = user.Verified, CreatedTime = DateTime.UtcNow, UpdatedTime = DateTime.UtcNow }; _dc.Users.InsertOne(userCollection); } + + public void UpdateUserVerified(string userId) + { + var filter = Builders.Filter.Eq(x => x.Id, userId); + var update = Builders.Update.Set(x => x.Verified, true) + .Set(x => x.UpdatedTime, DateTime.UtcNow); + _dc.Users.UpdateOne(filter, update); + } } diff --git a/src/Plugins/BotSharp.Plugin.PaddleSharp/BotSharp.Plugin.PaddleSharp.csproj b/src/Plugins/BotSharp.Plugin.PaddleSharp/BotSharp.Plugin.PaddleSharp.csproj index 6dcaf95e..ef637413 100644 --- a/src/Plugins/BotSharp.Plugin.PaddleSharp/BotSharp.Plugin.PaddleSharp.csproj +++ b/src/Plugins/BotSharp.Plugin.PaddleSharp/BotSharp.Plugin.PaddleSharp.csproj @@ -1,4 +1,4 @@ - + netstandard2.1 @@ -12,14 +12,14 @@ - - + + - + diff --git a/src/Plugins/BotSharp.Plugin.Qdrant/BotSharp.Plugin.Qdrant.csproj b/src/Plugins/BotSharp.Plugin.Qdrant/BotSharp.Plugin.Qdrant.csproj index 693c7885..bdcd40d5 100644 --- a/src/Plugins/BotSharp.Plugin.Qdrant/BotSharp.Plugin.Qdrant.csproj +++ b/src/Plugins/BotSharp.Plugin.Qdrant/BotSharp.Plugin.Qdrant.csproj @@ -11,7 +11,7 @@ - + diff --git a/src/Plugins/BotSharp.Plugin.SemanticKernel/BotSharp.Plugin.SemanticKernel.csproj b/src/Plugins/BotSharp.Plugin.SemanticKernel/BotSharp.Plugin.SemanticKernel.csproj index 9d790284..c4051422 100644 --- a/src/Plugins/BotSharp.Plugin.SemanticKernel/BotSharp.Plugin.SemanticKernel.csproj +++ b/src/Plugins/BotSharp.Plugin.SemanticKernel/BotSharp.Plugin.SemanticKernel.csproj @@ -1,4 +1,4 @@ - + netstandard2.1 @@ -11,8 +11,8 @@ - - + + diff --git a/src/Plugins/BotSharp.Plugin.SemanticKernel/SemanticKernelMemoryStoreProvider.cs b/src/Plugins/BotSharp.Plugin.SemanticKernel/SemanticKernelMemoryStoreProvider.cs index 444cab1e..7df2c0cd 100644 --- a/src/Plugins/BotSharp.Plugin.SemanticKernel/SemanticKernelMemoryStoreProvider.cs +++ b/src/Plugins/BotSharp.Plugin.SemanticKernel/SemanticKernelMemoryStoreProvider.cs @@ -9,13 +9,13 @@ namespace BotSharp.Plugin.SemanticKernel { internal class SemanticKernelMemoryStoreProvider : IVectorDb { -#pragma warning disable SKEXP0003 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. +#pragma warning disable SKEXP0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. private readonly IMemoryStore _memoryStore; -#pragma warning restore SKEXP0003 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. +#pragma warning restore SKEXP0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. -#pragma warning disable SKEXP0003 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. +#pragma warning disable SKEXP0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. public SemanticKernelMemoryStoreProvider(IMemoryStore memoryStore) -#pragma warning restore SKEXP0003 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. +#pragma warning restore SKEXP0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. { this._memoryStore = memoryStore; } @@ -50,9 +50,9 @@ namespace BotSharp.Plugin.SemanticKernel public async Task Upsert(string collectionName, int id, float[] vector, string text) { -#pragma warning disable SKEXP0003 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. +#pragma warning disable SKEXP0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. await _memoryStore.UpsertAsync(collectionName, MemoryRecord.LocalRecord(id.ToString(), text, null, vector)); -#pragma warning restore SKEXP0003 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. +#pragma warning restore SKEXP0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. } } } diff --git a/src/Plugins/BotSharp.Plugin.SparkDesk/Providers/ChatCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.SparkDesk/Providers/ChatCompletionProvider.cs index 3423347e..7d529df1 100644 --- a/src/Plugins/BotSharp.Plugin.SparkDesk/Providers/ChatCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.SparkDesk/Providers/ChatCompletionProvider.cs @@ -222,7 +222,7 @@ public class ChatCompletionProvider : IChatCompletion return (prompt, messages.ToArray(), functions.ToArray()); } - private string GetPrompt(List messages,List functions) + private string GetPrompt(List messages, List functions) { var prompt = string.Empty; diff --git a/src/Plugins/BotSharp.Plugin.SqlDriver/BotSharp.Plugin.SqlDriver.csproj b/src/Plugins/BotSharp.Plugin.SqlDriver/BotSharp.Plugin.SqlDriver.csproj index 171ba4cd..afb53ae8 100644 --- a/src/Plugins/BotSharp.Plugin.SqlDriver/BotSharp.Plugin.SqlDriver.csproj +++ b/src/Plugins/BotSharp.Plugin.SqlDriver/BotSharp.Plugin.SqlDriver.csproj @@ -1,4 +1,4 @@ - + netstandard2.1 @@ -33,8 +33,7 @@ - - + diff --git a/src/Plugins/BotSharp.Plugin.Twilio/BotSharp.Plugin.Twilio.csproj b/src/Plugins/BotSharp.Plugin.Twilio/BotSharp.Plugin.Twilio.csproj index c8f1a6f5..ed99a926 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/BotSharp.Plugin.Twilio.csproj +++ b/src/Plugins/BotSharp.Plugin.Twilio/BotSharp.Plugin.Twilio.csproj @@ -9,7 +9,7 @@ - + diff --git a/src/Plugins/BotSharp.Plugin.WeChat/BotSharp.Plugin.WeChat.csproj b/src/Plugins/BotSharp.Plugin.WeChat/BotSharp.Plugin.WeChat.csproj index b2c19ae1..85c04581 100644 --- a/src/Plugins/BotSharp.Plugin.WeChat/BotSharp.Plugin.WeChat.csproj +++ b/src/Plugins/BotSharp.Plugin.WeChat/BotSharp.Plugin.WeChat.csproj @@ -27,7 +27,7 @@ - + diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/BotSharp.Plugin.WebDriver.csproj b/src/Plugins/BotSharp.Plugin.WebDriver/BotSharp.Plugin.WebDriver.csproj index 7c4d6a76..96f97705 100644 --- a/src/Plugins/BotSharp.Plugin.WebDriver/BotSharp.Plugin.WebDriver.csproj +++ b/src/Plugins/BotSharp.Plugin.WebDriver/BotSharp.Plugin.WebDriver.csproj @@ -1,4 +1,4 @@ - + netstandard2.1 @@ -11,7 +11,14 @@ - + + + + + + + + diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightInstance.cs b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightInstance.cs index fdbc29cb..e7fd5908 100644 --- a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightInstance.cs +++ b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightInstance.cs @@ -6,6 +6,7 @@ public class PlaywrightInstance : IDisposable { IPlaywright _playwright; Dictionary _contexts = new Dictionary(); + public Dictionary Contexts => _contexts; public IPage GetPage(string id) { @@ -13,24 +14,22 @@ public class PlaywrightInstance : IDisposable return _contexts[id].Pages.LastOrDefault(); } - public async Task InitInstance(string id) + public async Task InitInstance(string id) { if (_playwright == null) { _playwright = await Playwright.CreateAsync(); } - await InitContext(id); + return await InitContext(id); } - public async Task InitContext(string id) + public async Task InitContext(string id) { if (_contexts.ContainsKey(id)) - return; -#if DEBUG - string tempFolderPath = $"{Path.GetTempPath()}\\playwright"; -#else + return _contexts[id]; + string tempFolderPath = $"{Path.GetTempPath()}\\playwright\\{id}"; -#endif + _contexts[id] = await _playwright.Chromium.LaunchPersistentContextAsync(tempFolderPath, new BrowserTypeLaunchPersistentContextOptions { #if DEBUG @@ -41,13 +40,14 @@ public class PlaywrightInstance : IDisposable Channel = "chrome", IgnoreDefaultArgs = new[] { - "--disable-infobars" - }, + "--enable-automation", + }, Args = new[] { - "--disable-infobars", - // "--start-maximized" - } + "--disable-infobars", + "--no-sandbox", + // "--start-maximized" + } }); _contexts[id].Page += async (sender, e) => @@ -65,6 +65,8 @@ public class PlaywrightInstance : IDisposable Serilog.Log.Warning($"Playwright browser context is closed"); _contexts.Remove(id); }; + + return _contexts[id]; } public async Task NewPage(string id) @@ -92,9 +94,17 @@ public class PlaywrightInstance : IDisposable } } + public async Task CloseCurrentPage(string id) + { + if (_contexts.ContainsKey(id)) + { + await GetPage(id).CloseAsync(); + } + } + public void Dispose() { _contexts.Clear(); - _playwright.Dispose(); + _playwright?.Dispose(); } } diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.ActionOnElement.cs b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.ActionOnElement.cs index f8d4d1d9..67a46f03 100644 --- a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.ActionOnElement.cs +++ b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.ActionOnElement.cs @@ -4,7 +4,7 @@ public partial class PlaywrightWebDriver { public async Task ActionOnElement(MessageInfo message, ElementLocatingArgs location, ElementActionArgs action) { - await _instance.Wait(message.ConversationId); + await _instance.Wait(message.ContextId); var result = await LocateElement(message, location); if (result.IsSuccess) { diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.ChangeCheckbox.cs b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.ChangeCheckbox.cs index 7611f1d1..d15597bc 100644 --- a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.ChangeCheckbox.cs +++ b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.ChangeCheckbox.cs @@ -1,5 +1,3 @@ -using System.Text.RegularExpressions; - namespace BotSharp.Plugin.WebDriver.Drivers.PlaywrightDriver; public partial class PlaywrightWebDriver @@ -8,7 +6,7 @@ public partial class PlaywrightWebDriver { var result = new BrowserActionResult(); - await _instance.Wait(actionParams.ConversationId); + await _instance.Wait(actionParams.ContextId); // Retrieve the page raw html and infer the element path var regexExpression = actionParams.Context.MatchRule.ToLower() switch @@ -19,19 +17,19 @@ public partial class PlaywrightWebDriver _ => $"^{actionParams.Context.ElementText}$" }; var regex = new Regex(regexExpression, RegexOptions.IgnoreCase); - var elements = _instance.GetPage(actionParams.ConversationId).GetByText(regex); + var elements = _instance.GetPage(actionParams.ContextId).GetByText(regex); var count = await elements.CountAsync(); var errorMessage = $"Can't locate element by keyword {actionParams.Context.ElementText}"; if (count == 0) { - result.ErrorMessage = errorMessage; + result.Message = errorMessage; return result; } else if (count > 1) { - result.ErrorMessage = $"Located multiple elements by {actionParams.Context.ElementText}"; - _logger.LogError(result.ErrorMessage); + result.Message = $"Located multiple elements by {actionParams.Context.ElementText}"; + _logger.LogError(result.Message); var allElements = await elements.AllAsync(); foreach (var element in allElements) { @@ -44,7 +42,7 @@ public partial class PlaywrightWebDriver count = await parentElement.CountAsync(); if (count == 0) { - result.ErrorMessage = errorMessage; + result.Message = errorMessage; return result; } @@ -55,19 +53,19 @@ public partial class PlaywrightWebDriver } else { - elements = _instance.GetPage(actionParams.ConversationId).Locator($"#{id}"); + elements = _instance.GetPage(actionParams.ContextId).Locator($"#{id}"); } count = await elements.CountAsync(); if (count == 0) { - result.ErrorMessage = errorMessage; + result.Message = errorMessage; return result; } else if (count > 1) { - result.ErrorMessage = $"Located multiple elements by {actionParams.Context.ElementText}"; - _logger.LogError(result.ErrorMessage); + result.Message = $"Located multiple elements by {actionParams.Context.ElementText}"; + _logger.LogError(result.Message); return result; } @@ -87,7 +85,7 @@ public partial class PlaywrightWebDriver } catch (Exception ex) { - result.ErrorMessage = ex.Message; + result.Message = ex.Message; result.StackTrace = ex.StackTrace; _logger.LogError(ex.Message); } diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.ChangeListValue.cs b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.ChangeListValue.cs index d8b81393..f3d58f97 100644 --- a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.ChangeListValue.cs +++ b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.ChangeListValue.cs @@ -5,10 +5,10 @@ public partial class PlaywrightWebDriver public async Task ChangeListValue(BrowserActionParams actionParams) { var result = new BrowserActionResult(); - await _instance.Wait(actionParams.ConversationId); + await _instance.Wait(actionParams.ContextId); // Retrieve the page raw html and infer the element path - var body = await _instance.GetPage(actionParams.ConversationId).QuerySelectorAsync("body"); + var body = await _instance.GetPage(actionParams.ContextId).QuerySelectorAsync("body"); var str = new List(); var inputs = await body.QuerySelectorAllAsync("select"); @@ -61,7 +61,7 @@ public partial class PlaywrightWebDriver string.Join("", str), actionParams.Context.ElementName, actionParams.MessageId); - ILocator element = Locator(actionParams.ConversationId, htmlElementContextOut); + ILocator? element = Locator(actionParams.ContextId, htmlElementContextOut); try { @@ -70,11 +70,11 @@ public partial class PlaywrightWebDriver if (!isVisible) { // Select the element you want to make visible (replace with your own selector) - var control = await _instance.GetPage(actionParams.ConversationId) + var control = await _instance.GetPage(actionParams.ContextId) .QuerySelectorAsync($"#{htmlElementContextOut.ElementId}"); // Show the element by modifying its CSS styles - await _instance.GetPage(actionParams.ConversationId) + await _instance.GetPage(actionParams.ContextId) .EvaluateAsync(@"(element) => { element.style.display = 'block'; element.style.visibility = 'visible'; @@ -92,11 +92,11 @@ public partial class PlaywrightWebDriver if (!isVisible) { // Select the element you want to make visible (replace with your own selector) - var control = await _instance.GetPage(actionParams.ConversationId) + var control = await _instance.GetPage(actionParams.ContextId) .QuerySelectorAsync($"#{htmlElementContextOut.ElementId}"); // Show the element by modifying its CSS styles - await _instance.GetPage(actionParams.ConversationId).EvaluateAsync(@"(element) => { + await _instance.GetPage(actionParams.ContextId).EvaluateAsync(@"(element) => { element.style.display = 'none'; element.style.visibility = 'hidden'; }", control); @@ -106,7 +106,7 @@ public partial class PlaywrightWebDriver } catch (Exception ex) { - result.ErrorMessage = ex.Message; + result.Message = ex.Message; result.StackTrace = ex.StackTrace; _logger.LogError(ex.Message); } diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.CheckRadioButton.cs b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.CheckRadioButton.cs index 6a5f8663..9df8d3bc 100644 --- a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.CheckRadioButton.cs +++ b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.CheckRadioButton.cs @@ -5,7 +5,7 @@ public partial class PlaywrightWebDriver public async Task CheckRadioButton(BrowserActionParams actionParams) { var result = new BrowserActionResult(); - await _instance.Wait(actionParams.ConversationId); + await _instance.Wait(actionParams.ContextId); // Retrieve the page raw html and infer the element path var regexExpression = actionParams.Context.MatchRule.ToLower() switch @@ -16,13 +16,13 @@ public partial class PlaywrightWebDriver _ => $"^{actionParams.Context.ElementText}$" }; var regex = new Regex(regexExpression, RegexOptions.IgnoreCase); - var elements = _instance.GetPage(actionParams.ConversationId).GetByText(regex); + var elements = _instance.GetPage(actionParams.ContextId).GetByText(regex); var count = await elements.CountAsync(); var errorMessage = $"Can't locate element by keyword {actionParams.Context.ElementText}"; if (count == 0) { - result.ErrorMessage = errorMessage; + result.Message = errorMessage; return result; } @@ -30,7 +30,7 @@ public partial class PlaywrightWebDriver count = await parentElement.CountAsync(); if (count == 0) { - result.ErrorMessage = errorMessage; + result.Message = errorMessage; return result; } @@ -40,7 +40,7 @@ public partial class PlaywrightWebDriver if (count == 0) { - result.ErrorMessage = errorMessage; + result.Message = errorMessage; return result; } @@ -53,7 +53,7 @@ public partial class PlaywrightWebDriver } catch (Exception ex) { - result.ErrorMessage = ex.Message; + result.Message = ex.Message; result.StackTrace = ex.StackTrace; _logger.LogError(ex.Message); } diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.ClickButton.cs b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.ClickButton.cs index 1f2e3ec3..db0aad60 100644 --- a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.ClickButton.cs +++ b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.ClickButton.cs @@ -5,10 +5,10 @@ public partial class PlaywrightWebDriver public async Task ClickButton(BrowserActionParams actionParams) { var result = new BrowserActionResult(); - await _instance.Wait(actionParams.ConversationId); + await _instance.Wait(actionParams.ContextId); // Find by text exactly match - var elements = _instance.GetPage(actionParams.ConversationId) + var elements = _instance.GetPage(actionParams.ContextId) .GetByRole(AriaRole.Button, new PageGetByRoleOptions { Name = actionParams.Context.ElementName @@ -17,7 +17,7 @@ public partial class PlaywrightWebDriver if (count == 0) { - elements = _instance.GetPage(actionParams.ConversationId) + elements = _instance.GetPage(actionParams.ContextId) .GetByRole(AriaRole.Link, new PageGetByRoleOptions { Name = actionParams.Context.ElementName @@ -27,7 +27,7 @@ public partial class PlaywrightWebDriver if (count == 0) { - elements = _instance.GetPage(actionParams.ConversationId) + elements = _instance.GetPage(actionParams.ContextId) .GetByText(actionParams.Context.ElementName); count = await elements.CountAsync(); } @@ -36,17 +36,17 @@ public partial class PlaywrightWebDriver { // Infer element if not found var driverService = _services.GetRequiredService(); - var html = await FilteredButtonHtml(actionParams.ConversationId); + var html = await FilteredButtonHtml(actionParams.ContextId); var htmlElementContextOut = await driverService.InferElement(actionParams.Agent, html, actionParams.Context.ElementName, actionParams.MessageId); - elements = Locator(actionParams.ConversationId, htmlElementContextOut); + elements = Locator(actionParams.ContextId, htmlElementContextOut); if (elements == null) { var errorMessage = $"Can't locate element by keyword {actionParams.Context.ElementName}"; - result.ErrorMessage = errorMessage; + result.Message = errorMessage; return result; } } @@ -54,25 +54,25 @@ public partial class PlaywrightWebDriver try { await elements.ClickAsync(); - await _instance.Wait(actionParams.ConversationId); + await _instance.Wait(actionParams.ContextId); result.IsSuccess = true; } catch (Exception ex) { - result.ErrorMessage = ex.Message; + result.Message = ex.Message; result.StackTrace = ex.StackTrace; _logger.LogError(ex.Message); } return result; } - private async Task FilteredButtonHtml(string conversationId) + private async Task FilteredButtonHtml(string contextId) { var driverService = _services.GetRequiredService(); // Retrieve the page raw html and infer the element path - var body = await _instance.GetPage(conversationId).QuerySelectorAsync("body"); + var body = await _instance.GetPage(contextId).QuerySelectorAsync("body"); var str = new List(); /*var anchors = await body.QuerySelectorAllAsync("a"); diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.ClickElement.cs b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.ClickElement.cs index 0ee8b24e..c477f805 100644 --- a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.ClickElement.cs +++ b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.ClickElement.cs @@ -5,9 +5,9 @@ public partial class PlaywrightWebDriver public async Task ClickElement(BrowserActionParams actionParams) { var result = new BrowserActionResult(); - await _instance.Wait(actionParams.ConversationId); + await _instance.Wait(actionParams.ContextId); - var page = _instance.GetPage(actionParams.ConversationId); + var page = _instance.GetPage(actionParams.ContextId); ILocator locator = default; int count = 0; @@ -42,8 +42,8 @@ public partial class PlaywrightWebDriver if (count == 0) { - result.ErrorMessage = $"Can't locate element by keyword {actionParams.Context.ElementText}"; - _logger.LogError(result.ErrorMessage); + result.Message = $"Can't locate element by keyword {actionParams.Context.ElementText}"; + _logger.LogError(result.Message); } else if (count == 1) { @@ -51,14 +51,14 @@ public partial class PlaywrightWebDriver await locator.ClickAsync(); // Triggered ajax - await _instance.Wait(actionParams.ConversationId); + await _instance.Wait(actionParams.ContextId); result.IsSuccess = true; } else if (count > 1) { - result.ErrorMessage = $"Multiple elements are found by keyword {actionParams.Context.ElementText}"; - _logger.LogWarning(result.ErrorMessage); + result.Message = $"Multiple elements are found by keyword {actionParams.Context.ElementText}"; + _logger.LogWarning(result.Message); var all = await locator.AllAsync(); foreach (var element in all) { diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.CloseBrowser.cs b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.CloseBrowser.cs index 8ab94880..1e3240ea 100644 --- a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.CloseBrowser.cs +++ b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.CloseBrowser.cs @@ -2,8 +2,8 @@ namespace BotSharp.Plugin.WebDriver.Drivers.PlaywrightDriver; public partial class PlaywrightWebDriver { - public async Task CloseBrowser(string conversationId) + public async Task CloseBrowser(string contextId) { - await _instance.Close(conversationId); + await _instance.Close(contextId); } } diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.CloseCurrentPage.cs b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.CloseCurrentPage.cs new file mode 100644 index 00000000..c5ed1700 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.CloseCurrentPage.cs @@ -0,0 +1,9 @@ +namespace BotSharp.Plugin.WebDriver.Drivers.PlaywrightDriver; + +public partial class PlaywrightWebDriver +{ + public async Task CloseCurrentPage(string contextId) + { + await _instance.CloseCurrentPage(contextId); + } +} diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.DoAction.cs b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.DoAction.cs index d3311eee..91a99aa8 100644 --- a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.DoAction.cs +++ b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.DoAction.cs @@ -4,12 +4,35 @@ public partial class PlaywrightWebDriver { public async Task DoAction(MessageInfo message, ElementActionArgs action, BrowserActionResult result) { - var page = _instance.GetPage(message.ConversationId); + var page = _instance.GetPage(message.ContextId); ILocator locator = page.Locator(result.Selector); - if (action.Action == "click") + if (action.Action == BroswerActionEnum.Click) { - await locator.ClickAsync(); + if (action.Position == null) + { + await locator.ClickAsync(); + } + else + { + await locator.ClickAsync(new LocatorClickOptions + { + Position = new Position + { + X = action.Position.X, + Y = action.Position.Y + } + }); + } + } + else if (action.Action == BroswerActionEnum.InputText) + { + await locator.FillAsync(action.Content); + + if (action.PressKey != null) + { + await locator.PressAsync(action.PressKey); + } } } } diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.EvaluateScript.cs b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.EvaluateScript.cs index 8a72c311..a8a7eda4 100644 --- a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.EvaluateScript.cs +++ b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.EvaluateScript.cs @@ -2,10 +2,10 @@ namespace BotSharp.Plugin.WebDriver.Drivers.PlaywrightDriver; public partial class PlaywrightWebDriver { - public async Task EvaluateScript(string conversationId, string script) + public async Task EvaluateScript(string contextId, string script) { - await _instance.Wait(conversationId); + await _instance.Wait(contextId); - return await _instance.GetPage(conversationId).EvaluateAsync(script); + return await _instance.GetPage(contextId).EvaluateAsync(script); } } 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 86fae9e4..b716b7ba 100644 --- a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.ExtractData.cs +++ b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.ExtractData.cs @@ -4,12 +4,12 @@ public partial class PlaywrightWebDriver { public async Task ExtractData(BrowserActionParams actionParams) { - await _instance.Wait(actionParams.ConversationId); + await _instance.Wait(actionParams.ContextId); await Task.Delay(3000); // Retrieve the page raw html and infer the element path - var body = await _instance.GetPage(actionParams.ConversationId).QuerySelectorAsync("body"); + var body = await _instance.GetPage(actionParams.ContextId).QuerySelectorAsync("body"); var content = await body.InnerTextAsync(); var driverService = _services.GetRequiredService(); diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.GetAttributeValue.cs b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.GetAttributeValue.cs index 8b87c2c8..035bcf3b 100644 --- a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.GetAttributeValue.cs +++ b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.GetAttributeValue.cs @@ -2,10 +2,10 @@ namespace BotSharp.Plugin.WebDriver.Drivers.PlaywrightDriver; public partial class PlaywrightWebDriver { - public async Task GetAttributeValue(MessageInfo message, ElementLocatingArgs location, BrowserActionResult result) + public async Task GetAttributeValue(MessageInfo message, ElementLocatingArgs location) { - var page = _instance.GetPage(message.ConversationId); - ILocator locator = page.Locator(result.Selector); + var page = _instance.GetPage(message.ContextId); + ILocator locator = page.Locator(location.Selector); var value = string.Empty; if (!string.IsNullOrEmpty(location?.AttributeName)) @@ -13,6 +13,10 @@ public partial class PlaywrightWebDriver value = await locator.GetAttributeAsync(location.AttributeName); } - return value ?? string.Empty; + return new BrowserActionResult + { + IsSuccess = true, + Body = value ?? string.Empty + }; } } diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.GoToPage.cs b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.GoToPage.cs index ffcec537..f830bc9f 100644 --- a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.GoToPage.cs +++ b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.GoToPage.cs @@ -2,29 +2,46 @@ namespace BotSharp.Plugin.WebDriver.Drivers.PlaywrightDriver; public partial class PlaywrightWebDriver { - public async Task GoToPage(string conversationId, string url) + public async Task GoToPage(string contextId, string url, bool openNewTab = false) { var result = new BrowserActionResult(); + var context = await _instance.InitInstance(contextId); try { - var response = await _instance.GetPage(conversationId).GotoAsync(url); - await _instance.GetPage(conversationId).WaitForLoadStateAsync(LoadState.DOMContentLoaded); - await _instance.GetPage(conversationId).WaitForLoadStateAsync(LoadState.NetworkIdle); + // Check if the page is already open + foreach (var p in context.Pages) + { + if (p.Url == url) + { + result.Body = await p.ContentAsync(); + result.IsSuccess = true; + await p.BringToFrontAsync(); + return result; + } + } + + var page = openNewTab ? await _instance.NewPage(contextId) : + _instance.GetPage(contextId); + var response = await page.GotoAsync(url); + await page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + await page.WaitForLoadStateAsync(LoadState.NetworkIdle, new PageWaitForLoadStateOptions + { + Timeout = 1000 * 60 * 5 + }); if (response.Status == 200) { - var page = _instance.GetPage(conversationId); result.Body = await page.ContentAsync(); result.IsSuccess = true; } else { - result.ErrorMessage = response.StatusText; + result.Message = response.StatusText; } } catch (Exception ex) { - result.ErrorMessage = ex.Message; + result.Message = ex.Message; result.StackTrace = ex.StackTrace; _logger.LogError(ex.Message); } diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.HttpRequest.cs b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.HttpRequest.cs index 404fd3c9..a769332b 100644 --- a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.HttpRequest.cs +++ b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.HttpRequest.cs @@ -1,19 +1,25 @@ +using System.Net.Http; + namespace BotSharp.Plugin.WebDriver.Drivers.PlaywrightDriver; public partial class PlaywrightWebDriver { - public async Task SendHttpRequest(BrowserActionParams actionParams) + public async Task SendHttpRequest(MessageInfo message, HttpRequestParams args) { var result = new BrowserActionResult(); + + var body = args.Method == HttpMethod.Post ? + $"body: '{args.Payload}'" : string.Empty; + // Send AJAX request string script = $@" (async () => {{ - const response = await fetch('{actionParams.Context.Url}', {{ - method: 'POST', + const response = await fetch('{args.Url}', {{ + method: '{args.Method}', headers: {{ 'Content-Type': 'application/json' }}, - body: '{actionParams.Context.Payload}' + {body} }}); return await response.json(); }})(); @@ -21,13 +27,14 @@ public partial class PlaywrightWebDriver try { - var response = await EvaluateScript(actionParams.ConversationId, script); + _logger.LogInformation($"SendHttpRequest: {args.Url}"); + var response = await EvaluateScript(message.ContextId, script); result.IsSuccess = true; result.Body = JsonSerializer.Serialize(response); } catch (Exception ex) { - result.ErrorMessage = ex.Message; + result.Message = ex.Message; result.StackTrace = ex.StackTrace; _logger.LogError(ex.Message); } diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.InputUserPassword.cs b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.InputUserPassword.cs index c4e8c2fc..9f302701 100644 --- a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.InputUserPassword.cs +++ b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.InputUserPassword.cs @@ -5,10 +5,10 @@ public partial class PlaywrightWebDriver public async Task InputUserPassword(BrowserActionParams actionParams) { var result = new BrowserActionResult(); - await _instance.Wait(actionParams.ConversationId); + await _instance.Wait(actionParams.ContextId); // Retrieve the page raw html and infer the element path - var body = await _instance.GetPage(actionParams.ConversationId) + var body = await _instance.GetPage(actionParams.ContextId) .QuerySelectorAsync("body"); var inputs = await body.QuerySelectorAllAsync("input"); @@ -16,8 +16,8 @@ public partial class PlaywrightWebDriver if (password == null) { - result.ErrorMessage = $"Can't locate the password element by '{actionParams.Context.ElementName}'"; - _logger.LogError(result.ErrorMessage); + result.Message = $"Can't locate the password element by '{actionParams.Context.ElementName}'"; + _logger.LogError(result.Message); return result; } @@ -29,7 +29,7 @@ public partial class PlaywrightWebDriver } catch (Exception ex) { - result.ErrorMessage = ex.Message; + result.Message = ex.Message; result.StackTrace = ex.StackTrace; _logger.LogError(ex.Message); } diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.InputUserText.cs b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.InputUserText.cs index 49850499..dded5f9a 100644 --- a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.InputUserText.cs +++ b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.InputUserText.cs @@ -5,9 +5,9 @@ public partial class PlaywrightWebDriver public async Task InputUserText(BrowserActionParams actionParams) { var result = new BrowserActionResult(); - await _instance.Wait(actionParams.ConversationId); + await _instance.Wait(actionParams.ContextId); - var page = _instance.GetPage(actionParams.ConversationId); + var page = _instance.GetPage(actionParams.ContextId); ILocator locator = default; int count = 0; @@ -37,12 +37,12 @@ public partial class PlaywrightWebDriver if (count == 0) { var driverService = _services.GetRequiredService(); - var html = await FilteredInputHtml(actionParams.ConversationId); + var html = await FilteredInputHtml(actionParams.ContextId); var htmlElementContextOut = await driverService.InferElement(actionParams.Agent, html, actionParams.Context.ElementText, actionParams.MessageId); - locator = Locator(actionParams.ConversationId, htmlElementContextOut); + locator = Locator(actionParams.ContextId, htmlElementContextOut); count = await locator.CountAsync(); } else if (count > 0) @@ -56,12 +56,12 @@ public partial class PlaywrightWebDriver } // Triggered ajax - await _instance.Wait(actionParams.ConversationId); + await _instance.Wait(actionParams.ContextId); result.IsSuccess = true; } catch (Exception ex) { - result.ErrorMessage = ex.Message; + result.Message = ex.Message; result.StackTrace = ex.StackTrace; _logger.LogError(ex.Message); } diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.LaunchBrowser.cs b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.LaunchBrowser.cs index 533bce08..df844828 100644 --- a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.LaunchBrowser.cs +++ b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.LaunchBrowser.cs @@ -2,35 +2,42 @@ namespace BotSharp.Plugin.WebDriver.Drivers.PlaywrightDriver; public partial class PlaywrightWebDriver { - public async Task LaunchBrowser(string conversationId, string? url) + public async Task LaunchBrowser(string contextId, string? url) { var result = new BrowserActionResult() { IsSuccess = true }; - await _instance.InitInstance(conversationId); + var context = await _instance.InitInstance(contextId); if (!string.IsNullOrEmpty(url)) { - var page = await _instance.NewPage(conversationId); - - if (!string.IsNullOrEmpty(url)) + // Check if the page is already open + foreach (var p in context.Pages) { - try + if (p.Url == url) { - var response = await page.GotoAsync(url, new PageGotoOptions - { - Timeout = 15 * 1000 - }); - await page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); - result.IsSuccess = response.Status == 200; + await p.BringToFrontAsync(); + return result; } - catch(Exception ex) + } + + var page = await _instance.NewPage(contextId); + + try + { + var response = await page.GotoAsync(url, new PageGotoOptions { - result.ErrorMessage = ex.Message; - result.StackTrace = ex.StackTrace; - _logger.LogError(ex.Message); - } + Timeout = 15 * 1000 + }); + await page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + result.IsSuccess = response.Status == 200; + } + catch (Exception ex) + { + result.Message = ex.Message; + result.StackTrace = ex.StackTrace; + _logger.LogError(ex.Message); } } diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.LocateElement.cs b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.LocateElement.cs index 9ec68d20..60e0ce95 100644 --- a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.LocateElement.cs +++ b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.LocateElement.cs @@ -1,23 +1,37 @@ +using System.Xml.Linq; + namespace BotSharp.Plugin.WebDriver.Drivers.PlaywrightDriver; public partial class PlaywrightWebDriver { + /// + /// Using attributes or text to locate element and return the selector + /// + /// + /// + /// public async Task LocateElement(MessageInfo message, ElementLocatingArgs location) { var result = new BrowserActionResult(); - var page = _instance.GetPage(message.ConversationId); + var page = _instance.GetPage(message.ContextId); ILocator locator = page.Locator("body"); int count = 0; // check if selector is specified if (location.Selector != null) { - locator = page.Locator(location.Selector); + locator = locator.Locator(location.Selector); + count = await locator.CountAsync(); + } + + if (location.Tag != null) + { + locator = page.Locator(location.Tag); count = await locator.CountAsync(); } // try attribute - if (count == 0 && !string.IsNullOrEmpty(location.AttributeName)) + if (!string.IsNullOrEmpty(location.AttributeName)) { locator = locator.Locator($"[{location.AttributeName}='{location.AttributeValue}']"); count = await locator.CountAsync(); @@ -53,22 +67,47 @@ public partial class PlaywrightWebDriver if (count == 0) { - result.ErrorMessage = $"Can't locate element by keyword {location.Text}"; - _logger.LogError(result.ErrorMessage); + result.Message = $"Can't locate element by keyword {location.Text}"; + _logger.LogError(result.Message); } else if (count == 1) { + if (location.Parent) + { + locator = locator.Locator(".."); + } + result.Selector = locator.ToString().Split('@').Last(); - var text = await locator.InnerTextAsync(); - result.Body = text; + + // Make sure the element is visible + /*if (!await locator.IsVisibleAsync()) + { + await locator.EvaluateAsync("element => element.style.height = '15px'"); + await locator.EvaluateAsync("element => element.style.width = '15px'"); + await locator.EvaluateAsync("element => element.style.opacity = '1.0'"); + }*/ + + var html = await locator.InnerHTMLAsync(); + result.Body = html; result.IsSuccess = true; } else if (count > 1) { + // Make sure the element is visible + foreach (var element in await locator.AllAsync()) + { + if (!await element.IsVisibleAsync()) + { + await element.EvaluateAsync("element => element.style.height = '10px'"); + await element.EvaluateAsync("element => element.style.width = '10px'"); + await element.EvaluateAsync("element => element.style.opacity = '1.0'"); + } + } + if (location.FailIfMultiple) { - result.ErrorMessage = $"Multiple elements are found by {locator}"; - _logger.LogError(result.ErrorMessage); + result.Message = $"Multiple elements are found by {locator}"; + _logger.LogError(result.Message); foreach (var element in await locator.AllAsync()) { @@ -83,6 +122,19 @@ public partial class PlaywrightWebDriver } } + // Hightlight the element + if (result.IsSuccess && location.Highlight) + { + var handle = await page.QuerySelectorAsync(result.Selector); + + await page.EvaluateAsync($@" + (element) => {{ + element.style.outline = '2px solid red'; + }}", handle); + + result.IsHighlighted = true; + } + return result; } } diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.Screenshot.cs b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.Screenshot.cs index f0c1fffa..07fea145 100644 --- a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.Screenshot.cs +++ b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.Screenshot.cs @@ -2,12 +2,12 @@ namespace BotSharp.Plugin.WebDriver.Drivers.PlaywrightDriver; public partial class PlaywrightWebDriver { - public async Task ScreenshotAsync(string conversationId, string path) + public async Task ScreenshotAsync(string contextId, string path) { var result = new BrowserActionResult(); - await _instance.Wait(conversationId); - var page = _instance.GetPage(conversationId); + await _instance.Wait(contextId); + var page = _instance.GetPage(contextId); await Task.Delay(500); var bytes = await page.ScreenshotAsync(new PageScreenshotOptions diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.ScrollPage.cs b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.ScrollPage.cs index 2d9cabdf..5c76c6e7 100644 --- a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.ScrollPage.cs +++ b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.ScrollPage.cs @@ -5,9 +5,9 @@ public partial class PlaywrightWebDriver public async Task ScrollPageAsync(BrowserActionParams actionParams) { var result = new BrowserActionResult(); - await _instance.Wait(actionParams.ConversationId); + await _instance.Wait(actionParams.ContextId); - var page = _instance.GetPage(actionParams.ConversationId); + var page = _instance.GetPage(actionParams.ContextId); if(actionParams.Context.Direction == "down") await page.EvaluateAsync("window.scrollBy(0, window.innerHeight - 200)"); diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.cs b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.cs index 779bbb4c..dab45548 100644 --- a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.cs +++ b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.cs @@ -22,12 +22,12 @@ public partial class PlaywrightWebDriver : IWebBrowser _agent = agent; } - private ILocator? Locator(string conversationId, HtmlElementContextOut context) + private ILocator? Locator(string contextId, HtmlElementContextOut context) { ILocator element = default; if (!string.IsNullOrEmpty(context.ElementId)) { - element = _instance.GetPage(conversationId).Locator($"#{context.ElementId}"); + element = _instance.GetPage(contextId).Locator($"#{context.ElementId}"); } else if (!string.IsNullOrEmpty(context.ElementName)) { @@ -38,7 +38,7 @@ public partial class PlaywrightWebDriver : IWebBrowser "button" => AriaRole.Button, _ => AriaRole.Generic }; - element = _instance.GetPage(conversationId).Locator($"[name='{context.ElementName}']"); + element = _instance.GetPage(contextId).Locator($"[name='{context.ElementName}']"); var count = element.CountAsync().Result; if (count == 0) { @@ -58,7 +58,7 @@ public partial class PlaywrightWebDriver : IWebBrowser _logger.LogError($"Can't locate the web element {context.Index}."); return null; } - element = _instance.GetPage(conversationId).Locator(context.TagName).Nth(context.Index); + element = _instance.GetPage(contextId).Locator(context.TagName).Nth(context.Index); } return element; diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/SeleniumDriver/SeleniumInstance.cs b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/SeleniumDriver/SeleniumInstance.cs new file mode 100644 index 00000000..5e1f5eaf --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/SeleniumDriver/SeleniumInstance.cs @@ -0,0 +1,92 @@ +using OpenQA.Selenium.Chrome; +using System.IO; + +namespace BotSharp.Plugin.WebDriver.Drivers.SeleniumDriver; + +public class SeleniumInstance : IDisposable +{ + Dictionary _contexts = new Dictionary(); + + public Dictionary Contexts => _contexts; + + public INavigation GetPage(string id) + { + InitInstance(id).Wait(); + return _contexts[id].Navigate(); + } + + public string GetPageContent(string id) + { + InitInstance(id).Wait(); + return _contexts[id].PageSource; + } + + public async Task InitInstance(string id) + { + return await InitContext(id); + } + + public async Task InitContext(string id) + { + if (_contexts.ContainsKey(id)) + return _contexts[id]; + + string tempFolderPath = $"{Path.GetTempPath()}\\_selenium\\{id}"; + + var options = new ChromeOptions + { + // DebuggerAddress = "localhost:9222", + // BrowserVersion = "123.0.6312.46" + }; + options.AddArgument("disable-infobars"); + options.AddArgument($"--user-data-dir={tempFolderPath}"); + var selenium = new ChromeDriver(options); + // selenium.Manage().Window.Maximize(); + selenium.Navigate().GoToUrl("about:blank"); + _contexts[id] = selenium; + + return _contexts[id]; + } + + public async Task NewPage(string id) + { + await InitContext(id); + var selenium = _contexts[id]; + selenium.Navigate().GoToUrl("about:blank"); + return _contexts[id].Navigate(); + } + + public async Task Wait(string id) + { + if (_contexts.ContainsKey(id)) + { + _contexts[id].Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(10); + } + await Task.Delay(100); + } + + public async Task Close(string id) + { + if (_contexts.ContainsKey(id)) + { + _contexts[id].Quit(); + _contexts.Remove(id); + } + } + + public async Task CloseCurrentPage(string id) + { + if (_contexts.ContainsKey(id)) + { + } + } + + public void Dispose() + { + foreach(var context in _contexts) + { + context.Value.Quit(); + } + _contexts.Clear(); + } +} diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/SeleniumDriver/SeleniumWebDriver.DoAction.cs b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/SeleniumDriver/SeleniumWebDriver.DoAction.cs new file mode 100644 index 00000000..bb9afd87 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/SeleniumDriver/SeleniumWebDriver.DoAction.cs @@ -0,0 +1,43 @@ +using OpenQA.Selenium; +using OpenQA.Selenium.Interactions; + +namespace BotSharp.Plugin.WebDriver.Drivers.SeleniumDriver; + +public partial class SeleniumWebDriver +{ + public async Task DoAction(MessageInfo message, ElementActionArgs action, BrowserActionResult result) + { + var driver = await _instance.InitInstance(message.ContextId); + IWebElement element = default; + if (result.Selector.StartsWith("//")) + { + element = driver.FindElement(By.XPath(result.Selector)); + } + else + { + element = driver.FindElement(By.CssSelector(result.Selector)); + } + + + if (action.Action == BroswerActionEnum.Click) + { + if (action.Position == null) + { + element.Click(); + } + else + { + var size = element.Size; + var actions = new Actions(driver); + actions.MoveToElement(element) + .MoveByOffset((int)action.Position.X - size.Width / 2, (int)action.Position.Y - size.Height / 2) + .Click() + .Perform(); + } + } + else if (action.Action == BroswerActionEnum.InputText) + { + element.SendKeys(action.Content); + } + } +} diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/SeleniumDriver/SeleniumWebDriver.EvaluateScript.cs b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/SeleniumDriver/SeleniumWebDriver.EvaluateScript.cs new file mode 100644 index 00000000..9a59cb81 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/SeleniumDriver/SeleniumWebDriver.EvaluateScript.cs @@ -0,0 +1,13 @@ +namespace BotSharp.Plugin.WebDriver.Drivers.SeleniumDriver; + +public partial class SeleniumWebDriver +{ + public async Task EvaluateScript(string contextId, string script) + { + await _instance.Wait(contextId); + var driver = await _instance.InitContext(contextId); + var jsExecutor = (IJavaScriptExecutor)driver; + var result = jsExecutor.ExecuteAsyncScript(script); + return (T)result; + } +} diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/SeleniumDriver/SeleniumWebDriver.GetAttributeValue.cs b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/SeleniumDriver/SeleniumWebDriver.GetAttributeValue.cs new file mode 100644 index 00000000..0402b2db --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/SeleniumDriver/SeleniumWebDriver.GetAttributeValue.cs @@ -0,0 +1,22 @@ +namespace BotSharp.Plugin.WebDriver.Drivers.SeleniumDriver; + +public partial class SeleniumWebDriver +{ + public async Task GetAttributeValue(MessageInfo message, ElementLocatingArgs location) + { + var driver = await _instance.InitInstance(message.ContextId); + var locator = driver.FindElement(By.CssSelector(location.Selector)); + var value = string.Empty; + + if (!string.IsNullOrEmpty(location?.AttributeName)) + { + value = locator.GetAttribute(location.AttributeName); + } + + return new BrowserActionResult + { + IsSuccess = true, + Body = value ?? string.Empty + }; + } +} diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/SeleniumDriver/SeleniumWebDriver.GoToPage.cs b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/SeleniumDriver/SeleniumWebDriver.GoToPage.cs new file mode 100644 index 00000000..abe51210 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/SeleniumDriver/SeleniumWebDriver.GoToPage.cs @@ -0,0 +1,27 @@ +namespace BotSharp.Plugin.WebDriver.Drivers.SeleniumDriver; + +public partial class SeleniumWebDriver +{ + public async Task GoToPage(string contextId, string url, bool openNewTab = false) + { + var result = new BrowserActionResult(); + try + { + var page = openNewTab ? await _instance.NewPage(contextId) : + _instance.GetPage(contextId); + page.GoToUrl(url); + await _instance.Wait(contextId); + + result.Body = _instance.GetPageContent(contextId); + result.IsSuccess = true; + } + catch (Exception ex) + { + result.Message = ex.Message; + result.StackTrace = ex.StackTrace; + _logger.LogError(ex.Message); + } + + return result; + } +} diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/SeleniumDriver/SeleniumWebDriver.HttpRequest.cs b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/SeleniumDriver/SeleniumWebDriver.HttpRequest.cs new file mode 100644 index 00000000..c9940318 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/SeleniumDriver/SeleniumWebDriver.HttpRequest.cs @@ -0,0 +1,43 @@ +using System.Net.Http; + +namespace BotSharp.Plugin.WebDriver.Drivers.SeleniumDriver; + +public partial class SeleniumWebDriver +{ + public async Task SendHttpRequest(MessageInfo message, HttpRequestParams args) + { + var result = new BrowserActionResult(); + + var body = args.Method == HttpMethod.Post ? + $"body: '{args.Payload}'" : string.Empty; + + // Send AJAX request + string script = $@" + (async () => {{ + const response = await fetch('{args.Url}', {{ + method: '{args.Method}', + headers: {{ + 'Content-Type': 'application/json' + }}, + {body} + }}); + return await response.json(); + }})(); + "; + + try + { + var response = await EvaluateScript(message.ContextId, script); + result.IsSuccess = true; + result.Body = JsonSerializer.Serialize(response); + } + catch (Exception ex) + { + result.Message = ex.Message; + result.StackTrace = ex.StackTrace; + _logger.LogError(ex.Message); + } + + return result; + } +} diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/SeleniumDriver/SeleniumWebDriver.LaunchBrowser.cs b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/SeleniumDriver/SeleniumWebDriver.LaunchBrowser.cs new file mode 100644 index 00000000..73cfa9ee --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/SeleniumDriver/SeleniumWebDriver.LaunchBrowser.cs @@ -0,0 +1,33 @@ +namespace BotSharp.Plugin.WebDriver.Drivers.SeleniumDriver; + +public partial class SeleniumWebDriver +{ + public async Task LaunchBrowser(string contextId, string? url) + { + var result = new BrowserActionResult() + { + IsSuccess = true + }; + var context = await _instance.InitInstance(contextId); + + if (!string.IsNullOrEmpty(url)) + { + // Check if the page is already open + var page = await _instance.NewPage(contextId); + + try + { + page.GoToUrl(url); + result.IsSuccess = true; + } + catch (Exception ex) + { + result.Message = ex.Message; + result.StackTrace = ex.StackTrace; + _logger.LogError(ex.Message); + } + } + + return result; + } +} diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/SeleniumDriver/SeleniumWebDriver.LocateElement.cs b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/SeleniumDriver/SeleniumWebDriver.LocateElement.cs new file mode 100644 index 00000000..4d96b9a6 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/SeleniumDriver/SeleniumWebDriver.LocateElement.cs @@ -0,0 +1,119 @@ +using OpenQA.Selenium; +using System.Collections.ObjectModel; + +namespace BotSharp.Plugin.WebDriver.Drivers.SeleniumDriver; + +public partial class SeleniumWebDriver +{ + /// + /// Using attributes or text to locate element and return the selector + /// + /// + /// + /// + public async Task LocateElement(MessageInfo message, ElementLocatingArgs location) + { + var result = new BrowserActionResult(); + var driver = await _instance.InitInstance(message.ContextId); + + IWebElement locator = driver.FindElement(By.TagName("body")); + ReadOnlyCollection elements = default; + string selector = string.Empty; + int count = 0; + + // check if selector is specified + if (location.Selector != null) + { + selector = location.Selector; + elements = driver.FindElements(By.CssSelector(location.Selector)); + count = elements.Count; + } + + // try attribute + if (count == 0 && !string.IsNullOrEmpty(location.AttributeName)) + { + selector = $"[{location.AttributeName}='{location.AttributeValue}']"; + elements = driver.FindElements(By.CssSelector(selector)); + count = elements.Count; + } + + // Retrieve the page raw html and infer the element path + if (!string.IsNullOrEmpty(location.Text)) + { + var regexExpression = location.MatchRule.ToLower() switch + { + "startwith" => $"^{location.Text}", + "endwith" => $"{location.Text}$", + "contains" => $"{location.Text}", + _ => $"^{location.Text}$" + }; + var regex = new Regex(regexExpression, RegexOptions.IgnoreCase); + + selector = $"//*[text() = '{location.Text}']"; + elements = driver.FindElements(By.XPath(selector)); + count = elements.Count; + + // try placeholder + if (count == 0) + { + selector = $"[placeholder='{location.Text}']"; + elements = driver.FindElements(By.CssSelector(selector)); + count = elements.Count; + } + } + + if (location.Index >= 0) + { + locator = elements[location.Index]; + count = 1; + } + + if (count == 0) + { + result.Message = $"Can't locate element by keyword {location.Text}"; + _logger.LogError(result.Message); + } + else if (count == 1) + { + locator = elements[0]; + result.Selector = selector; + var text = locator.Text; + result.Body = text; + result.IsSuccess = true; + } + else if (count > 1) + { + if (location.FailIfMultiple) + { + result.Message = $"Multiple elements are found by {locator}"; + _logger.LogError(result.Message); + + /*foreach (var element in await locator.AllAsync()) + { + var content = await element.InnerHTMLAsync(); + _logger.LogError(content); + }*/ + } + else + { + result.Selector = locator.ToString(); + result.IsSuccess = true; + } + } + + // Hightlight the element + if (result.IsSuccess && location.Highlight) + { + /*var handle = await page.QuerySelectorAsync(result.Selector); + + await page.EvaluateAsync($@" + (element) => {{ + element.style.outline = '2px solid red'; + }}", handle); + + result.IsHighlighted = true;*/ + } + + return result; + } +} diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/SeleniumDriver/SeleniumWebDriver.cs b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/SeleniumDriver/SeleniumWebDriver.cs new file mode 100644 index 00000000..40eb70d7 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/SeleniumDriver/SeleniumWebDriver.cs @@ -0,0 +1,84 @@ +namespace BotSharp.Plugin.WebDriver.Drivers.SeleniumDriver; + +public partial class SeleniumWebDriver : IWebBrowser +{ + private readonly IServiceProvider _services; + private readonly SeleniumInstance _instance; + private readonly ILogger _logger; + public SeleniumInstance Instance => _instance; + + public Agent Agent => _agent; + private Agent _agent; + + public SeleniumWebDriver(IServiceProvider services, SeleniumInstance instance, ILogger logger) + { + _services = services; + _instance = instance; + _logger = logger; + } + + public Task ActionOnElement(MessageInfo message, ElementLocatingArgs location, ElementActionArgs action) + { + throw new NotImplementedException(); + } + + public Task ChangeCheckbox(BrowserActionParams actionParams) + { + throw new NotImplementedException(); + } + + public Task ChangeListValue(BrowserActionParams actionParams) + { + throw new NotImplementedException(); + } + + public Task CheckRadioButton(BrowserActionParams actionParams) + { + throw new NotImplementedException(); + } + + public Task ClickButton(BrowserActionParams actionParams) + { + throw new NotImplementedException(); + } + + public Task ClickElement(BrowserActionParams actionParams) + { + throw new NotImplementedException(); + } + + public Task CloseBrowser(string contextId) + { + throw new NotImplementedException(); + } + + public Task CloseCurrentPage(string contextId) + { + throw new NotImplementedException(); + } + + public Task ExtractData(BrowserActionParams actionParams) + { + throw new NotImplementedException(); + } + + public Task InputUserPassword(BrowserActionParams actionParams) + { + throw new NotImplementedException(); + } + + public Task InputUserText(BrowserActionParams actionParams) + { + throw new NotImplementedException(); + } + + public Task ScreenshotAsync(string contextId, string path) + { + throw new NotImplementedException(); + } + + public Task ScrollPageAsync(BrowserActionParams actionParams) + { + throw new NotImplementedException(); + } +} diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/Functions/ChangeCheckboxFn.cs b/src/Plugins/BotSharp.Plugin.WebDriver/Functions/ChangeCheckboxFn.cs index 0776b107..57d0851a 100644 --- a/src/Plugins/BotSharp.Plugin.WebDriver/Functions/ChangeCheckboxFn.cs +++ b/src/Plugins/BotSharp.Plugin.WebDriver/Functions/ChangeCheckboxFn.cs @@ -26,7 +26,7 @@ public class ChangeCheckboxFn : IFunctionCallback var content = $"{(args.UpdateValue == "check" ? "Check" : "Uncheck")} checkbox of '{args.ElementText}'"; message.Content = result.IsSuccess ? $"{content} successfully" : - $"{content} failed. {result.ErrorMessage}"; + $"{content} failed. {result.Message}"; var webDriverService = _services.GetRequiredService(); var path = webDriverService.GetScreenshotFilePath(message.MessageId); diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/Functions/ChangeListValueFn.cs b/src/Plugins/BotSharp.Plugin.WebDriver/Functions/ChangeListValueFn.cs index f1c0c134..8d0ec144 100644 --- a/src/Plugins/BotSharp.Plugin.WebDriver/Functions/ChangeListValueFn.cs +++ b/src/Plugins/BotSharp.Plugin.WebDriver/Functions/ChangeListValueFn.cs @@ -26,7 +26,7 @@ public class ChangeListValueFn : IFunctionCallback var content = $"Change value to '{args.UpdateValue}' for {args.ElementName}"; message.Content = result.IsSuccess ? $"{content} successfully" : - $"{content} failed. {result.ErrorMessage}"; + $"{content} failed. {result.Message}"; var webDriverService = _services.GetRequiredService(); var path = webDriverService.GetScreenshotFilePath(message.MessageId); diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/Functions/CheckRadioButtonFn.cs b/src/Plugins/BotSharp.Plugin.WebDriver/Functions/CheckRadioButtonFn.cs index 8c3027d9..c12430df 100644 --- a/src/Plugins/BotSharp.Plugin.WebDriver/Functions/CheckRadioButtonFn.cs +++ b/src/Plugins/BotSharp.Plugin.WebDriver/Functions/CheckRadioButtonFn.cs @@ -26,7 +26,7 @@ public class CheckRadioButtonFn : IFunctionCallback var content = $"Check value of '{args.UpdateValue}' for radio button '{args.ElementName}'"; message.Content = result.IsSuccess ? $"{content} successfully" : - $"{content} failed. {result.ErrorMessage}"; + $"{content} failed. {result.Message}"; var webDriverService = _services.GetRequiredService(); var path = webDriverService.GetScreenshotFilePath(message.MessageId); diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/Functions/ClickButtonFn.cs b/src/Plugins/BotSharp.Plugin.WebDriver/Functions/ClickButtonFn.cs index 67215a1b..060e5055 100644 --- a/src/Plugins/BotSharp.Plugin.WebDriver/Functions/ClickButtonFn.cs +++ b/src/Plugins/BotSharp.Plugin.WebDriver/Functions/ClickButtonFn.cs @@ -26,7 +26,7 @@ public class ClickButtonFn : IFunctionCallback var content = $"Click button of '{args.ElementName}'"; message.Content = result.IsSuccess ? $"{content} successfully" : - $"{content} failed. {result.ErrorMessage}"; + $"{content} failed. {result.Message}"; var webDriverService = _services.GetRequiredService(); var path = webDriverService.GetScreenshotFilePath(message.MessageId); diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/Functions/ClickElementFn.cs b/src/Plugins/BotSharp.Plugin.WebDriver/Functions/ClickElementFn.cs index d87a2c0b..612416a3 100644 --- a/src/Plugins/BotSharp.Plugin.WebDriver/Functions/ClickElementFn.cs +++ b/src/Plugins/BotSharp.Plugin.WebDriver/Functions/ClickElementFn.cs @@ -26,7 +26,7 @@ public class ClickElementFn : IFunctionCallback var content = $"Click element {args.MatchRule} text '{args.ElementText}'"; message.Content = result.IsSuccess ? $"{content} successfully" : - $"{content} failed. {result.ErrorMessage}"; + $"{content} failed. {result.Message}"; var webDriverService = _services.GetRequiredService(); var path = webDriverService.GetScreenshotFilePath(message.MessageId); diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/Functions/GoToPageFn.cs b/src/Plugins/BotSharp.Plugin.WebDriver/Functions/GoToPageFn.cs index 8a3c850b..7ffe4026 100644 --- a/src/Plugins/BotSharp.Plugin.WebDriver/Functions/GoToPageFn.cs +++ b/src/Plugins/BotSharp.Plugin.WebDriver/Functions/GoToPageFn.cs @@ -27,7 +27,7 @@ public class GoToPageFn : IFunctionCallback url = url.Replace("https://https://", "https://"); var result = await _browser.GoToPage(convService.ConversationId, url); - message.Content = result.IsSuccess ? $"Page {url} is open." : $"Page {url} open failed. {result.ErrorMessage}"; + message.Content = result.IsSuccess ? $"Page {url} is open." : $"Page {url} open failed. {result.Message}"; var path = webDriverService.GetScreenshotFilePath(message.MessageId); diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/Functions/HttpRequestFn.cs b/src/Plugins/BotSharp.Plugin.WebDriver/Functions/HttpRequestFn.cs index 17b84a1b..fff2c240 100644 --- a/src/Plugins/BotSharp.Plugin.WebDriver/Functions/HttpRequestFn.cs +++ b/src/Plugins/BotSharp.Plugin.WebDriver/Functions/HttpRequestFn.cs @@ -17,15 +17,20 @@ public class HttpRequestFn : IFunctionCallback public async Task Execute(RoleDialogModel message) { var convService = _services.GetRequiredService(); - var args = JsonSerializer.Deserialize(message.FunctionArgs); + var args = JsonSerializer.Deserialize(message.FunctionArgs); var agentService = _services.GetRequiredService(); var agent = await agentService.LoadAgent(message.CurrentAgentId); - var result = await _browser.SendHttpRequest(new BrowserActionParams(agent, args, convService.ConversationId, message.MessageId)); + var result = await _browser.SendHttpRequest(new MessageInfo + { + AgentId = agent.Id, + MessageId = message.MessageId, + ContextId = convService.ConversationId + }, args); message.Content = result.IsSuccess ? result.Body : - $"Http request failed. {result.ErrorMessage}"; + $"Http request failed. {result.Message}"; return true; } diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/Functions/InputUserTextFn.cs b/src/Plugins/BotSharp.Plugin.WebDriver/Functions/InputUserTextFn.cs index d0ac76b8..028d369d 100644 --- a/src/Plugins/BotSharp.Plugin.WebDriver/Functions/InputUserTextFn.cs +++ b/src/Plugins/BotSharp.Plugin.WebDriver/Functions/InputUserTextFn.cs @@ -31,7 +31,7 @@ public class InputUserTextFn : IFunctionCallback message.Content = result.IsSuccess ? content + " successfully" : - content + $" failed. {result.ErrorMessage}"; + content + $" failed. {result.Message}"; var webDriverService = _services.GetRequiredService(); var path = webDriverService.GetScreenshotFilePath(message.MessageId); diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/Functions/OpenBrowserFn.cs b/src/Plugins/BotSharp.Plugin.WebDriver/Functions/OpenBrowserFn.cs index 1a9c5e13..8d0cff8e 100644 --- a/src/Plugins/BotSharp.Plugin.WebDriver/Functions/OpenBrowserFn.cs +++ b/src/Plugins/BotSharp.Plugin.WebDriver/Functions/OpenBrowserFn.cs @@ -31,7 +31,7 @@ public class OpenBrowserFn : IFunctionCallback } else { - message.Content = $"Launch browser failed. {result.ErrorMessage}"; + message.Content = $"Launch browser failed. {result.Message}"; } var path = webDriverService.GetScreenshotFilePath(message.MessageId); diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/Using.cs b/src/Plugins/BotSharp.Plugin.WebDriver/Using.cs index 3c8c8c94..619f0e23 100644 --- a/src/Plugins/BotSharp.Plugin.WebDriver/Using.cs +++ b/src/Plugins/BotSharp.Plugin.WebDriver/Using.cs @@ -9,6 +9,8 @@ global using Microsoft.Playwright; global using Microsoft.Extensions.Configuration; global using Microsoft.Extensions.DependencyInjection; global using Microsoft.Extensions.Logging; +global using OpenQA.Selenium; +global using BotSharp.Abstraction.Browsing.Enums; global using BotSharp.Abstraction.Conversations; global using BotSharp.Abstraction.Plugins; global using BotSharp.Abstraction.Conversations.Models; diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/WebDriverPlugin.cs b/src/Plugins/BotSharp.Plugin.WebDriver/WebDriverPlugin.cs index 2c7a4133..12dd0090 100644 --- a/src/Plugins/BotSharp.Plugin.WebDriver/WebDriverPlugin.cs +++ b/src/Plugins/BotSharp.Plugin.WebDriver/WebDriverPlugin.cs @@ -1,4 +1,7 @@ +using BotSharp.Abstraction.Browsing.Settings; +using BotSharp.Abstraction.Settings; using BotSharp.Plugin.WebDriver.Drivers.PlaywrightDriver; +using BotSharp.Plugin.WebDriver.Drivers.SeleniumDriver; using BotSharp.Plugin.WebDriver.Hooks; namespace BotSharp.Plugin.Playwrights; @@ -13,8 +16,28 @@ public class WebDriverPlugin : IBotSharpPlugin public void RegisterDI(IServiceCollection services, IConfiguration config) { - services.AddScoped(); + var settings = new WebBrowsingSettings(); + config.Bind("WebBrowsing", settings); + + services.AddScoped(provider => + { + var settingService = provider.GetRequiredService(); + return settings; + }); + + services.AddScoped(); services.AddSingleton(); + + services.AddScoped(); + services.AddSingleton(); + + services.AddScoped(provider => settings.Driver switch + { + "Playwright" => provider.GetRequiredService(), + "Selenium" => provider.GetRequiredService(), + _ => provider.GetRequiredService(), + }); + services.AddScoped(); services.AddScoped(); } diff --git a/src/WebStarter/WebStarter.csproj b/src/WebStarter/WebStarter.csproj index 6e6e1bde..c6912c14 100644 --- a/src/WebStarter/WebStarter.csproj +++ b/src/WebStarter/WebStarter.csproj @@ -1,4 +1,4 @@ - + $(TargetFramework) @@ -22,7 +22,7 @@ - + @@ -36,9 +36,11 @@ + + @@ -55,6 +57,7 @@ + diff --git a/src/WebStarter/appsettings.json b/src/WebStarter/appsettings.json index 91c429c7..3618bcf2 100644 --- a/src/WebStarter/appsettings.json +++ b/src/WebStarter/appsettings.json @@ -33,6 +33,10 @@ "ClientId": "", "ClientSecret": "", "Version": 22 + }, + "Weixin": { + "AppId": "", + "AppSecret": "" } }, @@ -118,6 +122,7 @@ "DataDir": "agents", "TemplateFormat": "liquid", "HostAgentId": "01e2fc5c-2c89-4ec7-8470-7688608b496c", + "EnableTranslator": false, "LlmConfig": { "Provider": "azure-openai", "Model": "gpt-35-turbo" @@ -144,6 +149,15 @@ } }, + "WebBrowsing": { + "Driver": "Playwright" + }, + + "Http": { + "BaseAddress": "", + "Origin": "" + }, + "Statistics": { "DataDir": "stats" }, @@ -159,6 +173,12 @@ "AzureOpenAi": { }, + "AnthropicAi": { + "Claude": { + + } + }, + "GoogleAi": { "PaLM": { "Endpoint": "https://generativelanguage.googleapis.com", @@ -247,7 +267,8 @@ "Youtube": { "Endpoint": "https://www.googleapis.com/youtube/v3/search", "RegionCode": "US", - "Part": "id,snippet" + "Part": "id,snippet", + "Channels": [] } }, @@ -258,6 +279,7 @@ "BotSharp.Plugin.MongoStorage", "BotSharp.Plugin.Dashboard", "BotSharp.Plugin.AzureOpenAI", + "BotSharp.Plugin.AnthropicAI", "BotSharp.Plugin.GoogleAI", "BotSharp.Plugin.MetaAI", "BotSharp.Plugin.MetaMessenger", diff --git a/tests/BotSharp.Plugin.PizzaBot/Functions/GetPizzaTypesFn.cs b/tests/BotSharp.Plugin.PizzaBot/Functions/GetPizzaTypesFn.cs index 1842f557..9a239fd2 100644 --- a/tests/BotSharp.Plugin.PizzaBot/Functions/GetPizzaTypesFn.cs +++ b/tests/BotSharp.Plugin.PizzaBot/Functions/GetPizzaTypesFn.cs @@ -38,7 +38,7 @@ public class GetPizzaTypesFn : IFunctionCallback Message = new ButtonTemplateMessage { Text = "Please select a pizza type", - Buttons = pizzaTypes.Select(x => new ButtonElement + Buttons = pizzaTypes.Select(x => new ElementButton { Type = "text", Title = x, diff --git a/tests/BotSharp.Plugin.SemanticKernel.UnitTests/SemanticKernelPluginTests.cs b/tests/BotSharp.Plugin.SemanticKernel.UnitTests/SemanticKernelPluginTests.cs index 99d41024..62646015 100644 --- a/tests/BotSharp.Plugin.SemanticKernel.UnitTests/SemanticKernelPluginTests.cs +++ b/tests/BotSharp.Plugin.SemanticKernel.UnitTests/SemanticKernelPluginTests.cs @@ -22,9 +22,9 @@ namespace BotSharp.Plugin.SemanticKernel.Tests var plugin = new SemanticKernelPlugin(); services.AddScoped(x => Mock.Of()); services.AddScoped(x => Mock.Of()); -#pragma warning disable SKEXP0003 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. +#pragma warning disable SKEXP0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. services.AddScoped(x => Mock.Of()); -#pragma warning restore SKEXP0003 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. +#pragma warning restore SKEXP0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. #pragma warning disable SKEXP0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. services.AddScoped(x => Mock.Of()); #pragma warning restore SKEXP0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.