diff --git a/BotSharp.sln b/BotSharp.sln index 634ab10c5..92f8c7fd2 100644 --- a/BotSharp.sln +++ b/BotSharp.sln @@ -141,6 +141,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.LLM.Tests", "tests EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Test.RealtimeVoice", "tests\BotSharp.Test.RealtimeVoice\BotSharp.Test.RealtimeVoice.csproj", "{B067B126-88CD-4282-BEEF-7369B64423EF}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Plugin.Sandbox.Tests", "tests\BotSharp.Plugin.Sandbox.Tests\BotSharp.Plugin.Sandbox.Tests.csproj", "{B5DF4E3B-2B1F-4A96-8E5D-93B5FA1B2E9C}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Plugin.ChartHandler", "src\Plugins\BotSharp.Plugin.ChartHandler\BotSharp.Plugin.ChartHandler.csproj", "{0428DEAA-E4FE-4259-A6D8-6EDD1A9D0702}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Plugin.ExcelHandler", "src\Plugins\BotSharp.Plugin.ExcelHandler\BotSharp.Plugin.ExcelHandler.csproj", "{FC63C875-E880-D8BB-B8B5-978AB7B62983}" @@ -151,6 +153,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Plugin.ImageHandle EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Plugin.FuzzySharp", "src\Plugins\BotSharp.Plugin.FuzzySharp\BotSharp.Plugin.FuzzySharp.csproj", "{E7C243B9-E751-B3B4-8F16-95C76CA90D31}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Plugin.Sandbox", "src\Plugins\BotSharp.Plugin.Sandbox\BotSharp.Plugin.Sandbox.csproj", "{6F2E7F85-20E7-4A24-8B6E-B50D1779FAC4}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Plugin.MMPEmbedding", "src\Plugins\BotSharp.Plugin.MMPEmbedding\BotSharp.Plugin.MMPEmbedding.csproj", "{394B858B-9C26-B977-A2DA-8CC7BE5914CB}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Plugin.Membase", "src\Plugins\BotSharp.Plugin.Membase\BotSharp.Plugin.Membase.csproj", "{13223C71-9EAC-9835-28ED-5A4833E6F915}" @@ -643,6 +647,22 @@ Global {E7C243B9-E751-B3B4-8F16-95C76CA90D31}.Release|Any CPU.Build.0 = Release|Any CPU {E7C243B9-E751-B3B4-8F16-95C76CA90D31}.Release|x64.ActiveCfg = Release|Any CPU {E7C243B9-E751-B3B4-8F16-95C76CA90D31}.Release|x64.Build.0 = Release|Any CPU + {6F2E7F85-20E7-4A24-8B6E-B50D1779FAC4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6F2E7F85-20E7-4A24-8B6E-B50D1779FAC4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6F2E7F85-20E7-4A24-8B6E-B50D1779FAC4}.Debug|x64.ActiveCfg = Debug|Any CPU + {6F2E7F85-20E7-4A24-8B6E-B50D1779FAC4}.Debug|x64.Build.0 = Debug|Any CPU + {6F2E7F85-20E7-4A24-8B6E-B50D1779FAC4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6F2E7F85-20E7-4A24-8B6E-B50D1779FAC4}.Release|Any CPU.Build.0 = Release|Any CPU + {6F2E7F85-20E7-4A24-8B6E-B50D1779FAC4}.Release|x64.ActiveCfg = Release|Any CPU + {6F2E7F85-20E7-4A24-8B6E-B50D1779FAC4}.Release|x64.Build.0 = Release|Any CPU + {B5DF4E3B-2B1F-4A96-8E5D-93B5FA1B2E9C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B5DF4E3B-2B1F-4A96-8E5D-93B5FA1B2E9C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B5DF4E3B-2B1F-4A96-8E5D-93B5FA1B2E9C}.Debug|x64.ActiveCfg = Debug|Any CPU + {B5DF4E3B-2B1F-4A96-8E5D-93B5FA1B2E9C}.Debug|x64.Build.0 = Debug|Any CPU + {B5DF4E3B-2B1F-4A96-8E5D-93B5FA1B2E9C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B5DF4E3B-2B1F-4A96-8E5D-93B5FA1B2E9C}.Release|Any CPU.Build.0 = Release|Any CPU + {B5DF4E3B-2B1F-4A96-8E5D-93B5FA1B2E9C}.Release|x64.ActiveCfg = Release|Any CPU + {B5DF4E3B-2B1F-4A96-8E5D-93B5FA1B2E9C}.Release|x64.Build.0 = Release|Any CPU {394B858B-9C26-B977-A2DA-8CC7BE5914CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {394B858B-9C26-B977-A2DA-8CC7BE5914CB}.Debug|Any CPU.Build.0 = Debug|Any CPU {394B858B-9C26-B977-A2DA-8CC7BE5914CB}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -721,6 +741,7 @@ Global {AF329442-B48E-4B48-A18A-1C869D1BA6F5} = {D5293208-2BEF-42FC-A64C-5954F61720BA} {781F1465-365C-0F22-1775-25025DAFA4C7} = {E29DC6C4-5E57-48C5-BCB0-6B8F84782749} {8D2AD45F-836A-516F-DE6A-71443CEBB18A} = {32FAFFFE-A4CB-4FEE-BF7C-84518BBC6DCC} + {B5DF4E3B-2B1F-4A96-8E5D-93B5FA1B2E9C} = {32FAFFFE-A4CB-4FEE-BF7C-84518BBC6DCC} {C19D9AC1-97DD-8E65-E8DB-D295A095AA2D} = {32FAFFFE-A4CB-4FEE-BF7C-84518BBC6DCC} {B268E2F0-060F-8466-7D81-ABA4D735CA59} = {51AFE054-AE99-497D-A593-69BAEFB5106F} {970BE341-9AC8-99A5-6572-E703C1E02FCB} = {E29DC6C4-5E57-48C5-BCB0-6B8F84782749} @@ -732,6 +753,7 @@ Global {50B57066-3267-1D10-0F72-D2F5CC494F2C} = {D5293208-2BEF-42FC-A64C-5954F61720BA} {242F2D93-FCCE-4982-8075-F3052ECCA92C} = {51AFE054-AE99-497D-A593-69BAEFB5106F} {E7C243B9-E751-B3B4-8F16-95C76CA90D31} = {51AFE054-AE99-497D-A593-69BAEFB5106F} + {6F2E7F85-20E7-4A24-8B6E-B50D1779FAC4} = {51AFE054-AE99-497D-A593-69BAEFB5106F} {394B858B-9C26-B977-A2DA-8CC7BE5914CB} = {4F346DCE-087F-4368-AF88-EE9C720D0E69} {13223C71-9EAC-9835-28ED-5A4833E6F915} = {53E7CD86-0D19-40D9-A0FA-AB4613837E89} EndGlobalSection diff --git a/dockerfiles/appsettings.json b/dockerfiles/appsettings.json index ff53ffb4c..c840d8bc0 100644 --- a/dockerfiles/appsettings.json +++ b/dockerfiles/appsettings.json @@ -203,7 +203,8 @@ "BotSharp.Plugin.WeChat", "BotSharp.Plugin.PizzaBot", "BotSharp.Plugin.WebDriver", - "BotSharp.Plugin.LLamaSharp" + "BotSharp.Plugin.LLamaSharp", + "BotSharp.Plugin.Sandbox" ] } -} \ No newline at end of file +} diff --git a/src/Plugins/BotSharp.Plugin.Sandbox/BotSharp.Plugin.Sandbox.csproj b/src/Plugins/BotSharp.Plugin.Sandbox/BotSharp.Plugin.Sandbox.csproj new file mode 100644 index 000000000..90042cb98 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.Sandbox/BotSharp.Plugin.Sandbox.csproj @@ -0,0 +1,23 @@ + + + + $(TargetFramework) + enable + $(LangVersion) + $(BotSharpVersion) + $(GeneratePackageOnBuild) + $(GenerateDocumentationFile) + $(SolutionDir)packages + + + + + PreserveNewest + + + + + + + + diff --git a/src/Plugins/BotSharp.Plugin.Sandbox/Functions/SandboxBrowserScreenshotFn.cs b/src/Plugins/BotSharp.Plugin.Sandbox/Functions/SandboxBrowserScreenshotFn.cs new file mode 100644 index 000000000..7666934fd --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.Sandbox/Functions/SandboxBrowserScreenshotFn.cs @@ -0,0 +1,20 @@ +namespace BotSharp.Plugin.Sandbox.Functions; + +public class SandboxBrowserScreenshotFn : IFunctionCallback +{ + public string Name => SandboxFunctionNames.BrowserScreenshot; + public string Indication => "Capturing sandbox browser screenshot."; + + private readonly SandboxApiClient _client; + + public SandboxBrowserScreenshotFn(SandboxApiClient client) + { + _client = client; + } + + public async Task Execute(RoleDialogModel message) + { + message.Content = await _client.PostAsync("/v1/browser/screenshot", message.FunctionArgs); + return true; + } +} diff --git a/src/Plugins/BotSharp.Plugin.Sandbox/Functions/SandboxCheckPackagesFn.cs b/src/Plugins/BotSharp.Plugin.Sandbox/Functions/SandboxCheckPackagesFn.cs new file mode 100644 index 000000000..3d7f3c858 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.Sandbox/Functions/SandboxCheckPackagesFn.cs @@ -0,0 +1,27 @@ +namespace BotSharp.Plugin.Sandbox.Functions; + +public class SandboxCheckPackagesFn : IFunctionCallback +{ + public string Name => SandboxFunctionNames.CheckPackages; + public string Indication => "Checking installed sandbox packages."; + + private readonly SandboxApiClient _client; + + public SandboxCheckPackagesFn(SandboxApiClient client) + { + _client = client; + } + + public async Task Execute(RoleDialogModel message) + { + var lang = SandboxPayloadHelper.ReadString(message.FunctionArgs, "lang"); + var endpoint = "/v1/sandbox/packages"; + if (!string.IsNullOrWhiteSpace(lang)) + { + endpoint = $"{endpoint}/{Uri.EscapeDataString(lang)}"; + } + + message.Content = await _client.GetAsync(endpoint); + return true; + } +} diff --git a/src/Plugins/BotSharp.Plugin.Sandbox/Functions/SandboxFileEditorFn.cs b/src/Plugins/BotSharp.Plugin.Sandbox/Functions/SandboxFileEditorFn.cs new file mode 100644 index 000000000..4a86a6e6a --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.Sandbox/Functions/SandboxFileEditorFn.cs @@ -0,0 +1,20 @@ +namespace BotSharp.Plugin.Sandbox.Functions; + +public class SandboxFileEditorFn : IFunctionCallback +{ + public string Name => SandboxFunctionNames.FileEditor; + public string Indication => "Applying structured file edits in sandbox."; + + private readonly SandboxApiClient _client; + + public SandboxFileEditorFn(SandboxApiClient client) + { + _client = client; + } + + public async Task Execute(RoleDialogModel message) + { + message.Content = await _client.PostAsync("/v1/file/str_replace_editor", message.FunctionArgs); + return true; + } +} diff --git a/src/Plugins/BotSharp.Plugin.Sandbox/Functions/SandboxFileListFn.cs b/src/Plugins/BotSharp.Plugin.Sandbox/Functions/SandboxFileListFn.cs new file mode 100644 index 000000000..5319d553f --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.Sandbox/Functions/SandboxFileListFn.cs @@ -0,0 +1,20 @@ +namespace BotSharp.Plugin.Sandbox.Functions; + +public class SandboxFileListFn : IFunctionCallback +{ + public string Name => SandboxFunctionNames.FileList; + public string Indication => "Listing sandbox directory content."; + + private readonly SandboxApiClient _client; + + public SandboxFileListFn(SandboxApiClient client) + { + _client = client; + } + + public async Task Execute(RoleDialogModel message) + { + message.Content = await _client.PostAsync("/v1/file/list", message.FunctionArgs); + return true; + } +} diff --git a/src/Plugins/BotSharp.Plugin.Sandbox/Functions/SandboxFileReadFn.cs b/src/Plugins/BotSharp.Plugin.Sandbox/Functions/SandboxFileReadFn.cs new file mode 100644 index 000000000..8625b47fe --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.Sandbox/Functions/SandboxFileReadFn.cs @@ -0,0 +1,20 @@ +namespace BotSharp.Plugin.Sandbox.Functions; + +public class SandboxFileReadFn : IFunctionCallback +{ + public string Name => SandboxFunctionNames.FileRead; + public string Indication => "Reading file content from sandbox."; + + private readonly SandboxApiClient _client; + + public SandboxFileReadFn(SandboxApiClient client) + { + _client = client; + } + + public async Task Execute(RoleDialogModel message) + { + message.Content = await _client.PostAsync("/v1/file/read", message.FunctionArgs); + return true; + } +} diff --git a/src/Plugins/BotSharp.Plugin.Sandbox/Functions/SandboxFileSearchFn.cs b/src/Plugins/BotSharp.Plugin.Sandbox/Functions/SandboxFileSearchFn.cs new file mode 100644 index 000000000..b54f911f7 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.Sandbox/Functions/SandboxFileSearchFn.cs @@ -0,0 +1,20 @@ +namespace BotSharp.Plugin.Sandbox.Functions; + +public class SandboxFileSearchFn : IFunctionCallback +{ + public string Name => SandboxFunctionNames.FileSearch; + public string Indication => "Searching text inside sandbox files."; + + private readonly SandboxApiClient _client; + + public SandboxFileSearchFn(SandboxApiClient client) + { + _client = client; + } + + public async Task Execute(RoleDialogModel message) + { + message.Content = await _client.PostAsync("/v1/file/search", message.FunctionArgs); + return true; + } +} diff --git a/src/Plugins/BotSharp.Plugin.Sandbox/Functions/SandboxFileWriteFn.cs b/src/Plugins/BotSharp.Plugin.Sandbox/Functions/SandboxFileWriteFn.cs new file mode 100644 index 000000000..19c96a747 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.Sandbox/Functions/SandboxFileWriteFn.cs @@ -0,0 +1,20 @@ +namespace BotSharp.Plugin.Sandbox.Functions; + +public class SandboxFileWriteFn : IFunctionCallback +{ + public string Name => SandboxFunctionNames.FileWrite; + public string Indication => "Writing file content inside sandbox."; + + private readonly SandboxApiClient _client; + + public SandboxFileWriteFn(SandboxApiClient client) + { + _client = client; + } + + public async Task Execute(RoleDialogModel message) + { + message.Content = await _client.PostAsync("/v1/file/write", message.FunctionArgs); + return true; + } +} diff --git a/src/Plugins/BotSharp.Plugin.Sandbox/Functions/SandboxGetContextFn.cs b/src/Plugins/BotSharp.Plugin.Sandbox/Functions/SandboxGetContextFn.cs new file mode 100644 index 000000000..0c23f392a --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.Sandbox/Functions/SandboxGetContextFn.cs @@ -0,0 +1,20 @@ +namespace BotSharp.Plugin.Sandbox.Functions; + +public class SandboxGetContextFn : IFunctionCallback +{ + public string Name => SandboxFunctionNames.GetContext; + public string Indication => "Inspecting sandbox environment."; + + private readonly SandboxApiClient _client; + + public SandboxGetContextFn(SandboxApiClient client) + { + _client = client; + } + + public async Task Execute(RoleDialogModel message) + { + message.Content = await _client.GetAsync("/v1/sandbox"); + return true; + } +} diff --git a/src/Plugins/BotSharp.Plugin.Sandbox/Functions/SandboxJupyterCreateSessionFn.cs b/src/Plugins/BotSharp.Plugin.Sandbox/Functions/SandboxJupyterCreateSessionFn.cs new file mode 100644 index 000000000..71fd1afec --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.Sandbox/Functions/SandboxJupyterCreateSessionFn.cs @@ -0,0 +1,20 @@ +namespace BotSharp.Plugin.Sandbox.Functions; + +public class SandboxJupyterCreateSessionFn : IFunctionCallback +{ + public string Name => SandboxFunctionNames.JupyterCreateSession; + public string Indication => "Creating sandbox Jupyter session."; + + private readonly SandboxApiClient _client; + + public SandboxJupyterCreateSessionFn(SandboxApiClient client) + { + _client = client; + } + + public async Task Execute(RoleDialogModel message) + { + message.Content = await _client.PostAsync("/v1/jupyter/sessions/create", message.FunctionArgs); + return true; + } +} diff --git a/src/Plugins/BotSharp.Plugin.Sandbox/Functions/SandboxJupyterExecFn.cs b/src/Plugins/BotSharp.Plugin.Sandbox/Functions/SandboxJupyterExecFn.cs new file mode 100644 index 000000000..f4a2ed353 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.Sandbox/Functions/SandboxJupyterExecFn.cs @@ -0,0 +1,20 @@ +namespace BotSharp.Plugin.Sandbox.Functions; + +public class SandboxJupyterExecFn : IFunctionCallback +{ + public string Name => SandboxFunctionNames.JupyterExec; + public string Indication => "Running code via sandbox Jupyter runtime."; + + private readonly SandboxApiClient _client; + + public SandboxJupyterExecFn(SandboxApiClient client) + { + _client = client; + } + + public async Task Execute(RoleDialogModel message) + { + message.Content = await _client.PostAsync("/v1/jupyter/execute", message.FunctionArgs); + return true; + } +} diff --git a/src/Plugins/BotSharp.Plugin.Sandbox/Functions/SandboxShellExecFn.cs b/src/Plugins/BotSharp.Plugin.Sandbox/Functions/SandboxShellExecFn.cs new file mode 100644 index 000000000..fe1664b86 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.Sandbox/Functions/SandboxShellExecFn.cs @@ -0,0 +1,20 @@ +namespace BotSharp.Plugin.Sandbox.Functions; + +public class SandboxShellExecFn : IFunctionCallback +{ + public string Name => SandboxFunctionNames.ShellExec; + public string Indication => "Executing shell command inside the sandbox."; + + private readonly SandboxApiClient _client; + + public SandboxShellExecFn(SandboxApiClient client) + { + _client = client; + } + + public async Task Execute(RoleDialogModel message) + { + message.Content = await _client.PostAsync("/v1/shell/exec", message.FunctionArgs); + return true; + } +} diff --git a/src/Plugins/BotSharp.Plugin.Sandbox/Functions/SandboxShellWaitFn.cs b/src/Plugins/BotSharp.Plugin.Sandbox/Functions/SandboxShellWaitFn.cs new file mode 100644 index 000000000..30aab4f90 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.Sandbox/Functions/SandboxShellWaitFn.cs @@ -0,0 +1,20 @@ +namespace BotSharp.Plugin.Sandbox.Functions; + +public class SandboxShellWaitFn : IFunctionCallback +{ + public string Name => SandboxFunctionNames.ShellWait; + public string Indication => "Waiting for sandbox shell task completion."; + + private readonly SandboxApiClient _client; + + public SandboxShellWaitFn(SandboxApiClient client) + { + _client = client; + } + + public async Task Execute(RoleDialogModel message) + { + message.Content = await _client.PostAsync("/v1/shell/wait", message.FunctionArgs); + return true; + } +} diff --git a/src/Plugins/BotSharp.Plugin.Sandbox/Hooks/SandboxUtilityHook.cs b/src/Plugins/BotSharp.Plugin.Sandbox/Hooks/SandboxUtilityHook.cs new file mode 100644 index 000000000..ff30e1457 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.Sandbox/Hooks/SandboxUtilityHook.cs @@ -0,0 +1,30 @@ +namespace BotSharp.Plugin.Sandbox.Hooks; + +public class SandboxUtilityHook : IAgentUtilityHook +{ + public void AddUtilities(List utilities) + { + var items = new List + { + new() { FunctionName = SandboxFunctionNames.GetContext }, + new() { FunctionName = SandboxFunctionNames.ShellExec }, + new() { FunctionName = SandboxFunctionNames.ShellWait }, + new() { FunctionName = SandboxFunctionNames.FileRead }, + new() { FunctionName = SandboxFunctionNames.FileWrite }, + new() { FunctionName = SandboxFunctionNames.FileList }, + new() { FunctionName = SandboxFunctionNames.FileSearch }, + new() { FunctionName = SandboxFunctionNames.FileEditor }, + new() { FunctionName = SandboxFunctionNames.BrowserScreenshot }, + new() { FunctionName = SandboxFunctionNames.JupyterExec }, + new() { FunctionName = SandboxFunctionNames.JupyterCreateSession }, + new() { FunctionName = SandboxFunctionNames.CheckPackages } + }; + + utilities.Add(new AgentUtility + { + Category = "sandbox", + Name = "aio_sandbox", + Items = items + }); + } +} diff --git a/src/Plugins/BotSharp.Plugin.Sandbox/Models/SandboxFunctionNames.cs b/src/Plugins/BotSharp.Plugin.Sandbox/Models/SandboxFunctionNames.cs new file mode 100644 index 000000000..489fe6c55 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.Sandbox/Models/SandboxFunctionNames.cs @@ -0,0 +1,43 @@ +namespace BotSharp.Plugin.Sandbox.Models; + +public static class SandboxFunctionNames +{ + public const string GetContext = "util-sandbox-get_context"; + public const string ShellExec = "util-sandbox-shell_exec"; + public const string ShellWait = "util-sandbox-shell_wait"; + public const string FileRead = "util-sandbox-file_read"; + public const string FileWrite = "util-sandbox-file_write"; + public const string FileList = "util-sandbox-file_list"; + public const string FileSearch = "util-sandbox-file_search"; + public const string FileEditor = "util-sandbox-file_editor"; + public const string BrowserScreenshot = "util-sandbox-browser_screenshot"; + public const string JupyterExec = "util-sandbox-jupyter_exec"; + public const string JupyterCreateSession = "util-sandbox-jupyter_create_session"; + public const string CheckPackages = "util-sandbox-check_packages"; +} + +public static class SandboxPayloadHelper +{ + public static string ReadString(string? json, string propertyName) + { + if (string.IsNullOrWhiteSpace(json) || string.IsNullOrWhiteSpace(propertyName)) + { + return string.Empty; + } + + try + { + using var doc = JsonDocument.Parse(json); + var root = doc.RootElement; + if (root.ValueKind == JsonValueKind.Object && root.TryGetProperty(propertyName, out var node)) + { + return node.ValueKind == JsonValueKind.String ? node.GetString() ?? string.Empty : node.ToString(); + } + } + catch + { + } + + return string.Empty; + } +} diff --git a/src/Plugins/BotSharp.Plugin.Sandbox/README.md b/src/Plugins/BotSharp.Plugin.Sandbox/README.md new file mode 100644 index 000000000..c4b97a767 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.Sandbox/README.md @@ -0,0 +1,33 @@ +# BotSharp.Plugin.Sandbox + +Provides AIO Sandbox sidecar utilities so agents can safely execute isolated shell/file/browser/Jupyter operations via HTTP. + +## Configuration + +Configure in `appsettings.json`: + +```json +"Sandbox": { + "BaseUrl": "http://localhost:3000", + "ApiKeyHeader": "Authorization", + "ApiKey": "Bearer ", + "ResponseMaxLength": 6000 +} +``` + +Enable the plugin in `PluginLoader:Assemblies` (already added in defaults). + +## Exposed utility functions + +- `util-sandbox-get_context` +- `util-sandbox-shell_exec` +- `util-sandbox-shell_wait` +- `util-sandbox-file_read` +- `util-sandbox-file_write` +- `util-sandbox-file_list` +- `util-sandbox-file_search` +- `util-sandbox-file_editor` +- `util-sandbox-browser_screenshot` +- `util-sandbox-jupyter_exec` +- `util-sandbox-jupyter_create_session` +- `util-sandbox-check_packages` diff --git a/src/Plugins/BotSharp.Plugin.Sandbox/SandboxPlugin.cs b/src/Plugins/BotSharp.Plugin.Sandbox/SandboxPlugin.cs new file mode 100644 index 000000000..c621449ac --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.Sandbox/SandboxPlugin.cs @@ -0,0 +1,25 @@ +namespace BotSharp.Plugin.Sandbox; + +public class SandboxPlugin : IBotSharpPlugin +{ + public string Id => "e0c83b27-3d7a-4c77-9e22-9a626e63ef2b"; + public string Name => "AIO Sandbox"; + public string Description => "Sidecar sandbox utilities exposed as agent tools."; + public string? IconUrl => "https://avatars.githubusercontent.com/u/173528136?s=200&v=4"; + public string[] AgentIds => new[] { BuiltInAgentId.UtilityAssistant }; + + public SettingsMeta Settings => new("Sandbox"); + public object GetNewSettingsInstance() => new SandboxSettings(); + + public void RegisterDI(IServiceCollection services, IConfiguration config) + { + services.AddScoped(provider => + { + var settingService = provider.GetRequiredService(); + return settingService.Bind("Sandbox"); + }); + + services.AddHttpClient(); + services.AddScoped(); + } +} diff --git a/src/Plugins/BotSharp.Plugin.Sandbox/Services/SandboxApiClient.cs b/src/Plugins/BotSharp.Plugin.Sandbox/Services/SandboxApiClient.cs new file mode 100644 index 000000000..efcd98ede --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.Sandbox/Services/SandboxApiClient.cs @@ -0,0 +1,96 @@ +namespace BotSharp.Plugin.Sandbox.Services; + +public class SandboxApiClient +{ + private readonly IHttpClientFactory _clientFactory; + private readonly SandboxSettings _settings; + private readonly ILogger _logger; + + public SandboxApiClient(IHttpClientFactory clientFactory, + SandboxSettings settings, + ILogger logger) + { + _clientFactory = clientFactory; + _settings = settings; + _logger = logger; + } + + public Task GetAsync(string path) + => SendAsync(new HttpRequestMessage(HttpMethod.Get, BuildUri(path))); + + public Task PostAsync(string path, string? payload) + { + var body = string.IsNullOrWhiteSpace(payload) ? "{}" : payload; + var request = new HttpRequestMessage(HttpMethod.Post, BuildUri(path)) + { + Content = new StringContent(body, Encoding.UTF8, MediaTypeNames.Application.Json) + }; + return SendAsync(request); + } + + private async Task SendAsync(HttpRequestMessage request) + { + try + { + AppendAuthorization(request); + using var client = _clientFactory.CreateClient(nameof(SandboxApiClient)); + var response = await client.SendAsync(request); + var content = response.Content == null ? string.Empty : await response.Content.ReadAsStringAsync(); + if (!response.IsSuccessStatusCode) + { + return $"Sandbox request failed ({(int)response.StatusCode}): {Truncate(content)}"; + } + + return Truncate(content); + } + catch (Exception ex) + { + _logger.LogError(ex, "Sandbox request error: {Message}", ex.Message); + return $"Sandbox request error: {ex.Message}"; + } + } + + private void AppendAuthorization(HttpRequestMessage request) + { + if (string.IsNullOrWhiteSpace(_settings.ApiKey)) + { + return; + } + + var headerName = string.IsNullOrWhiteSpace(_settings.ApiKeyHeader) ? "Authorization" : _settings.ApiKeyHeader; + if (request.Headers.Contains(headerName)) + { + return; + } + + var headerValue = headerName.Equals("Authorization", StringComparison.OrdinalIgnoreCase) + ? $"Bearer {_settings.ApiKey}" + : _settings.ApiKey; + + request.Headers.TryAddWithoutValidation(headerName, headerValue); + } + + private Uri BuildUri(string path) + { + var baseUrl = string.IsNullOrWhiteSpace(_settings.BaseUrl) ? "http://localhost:3000" : _settings.BaseUrl; + var normalizedBase = baseUrl.TrimEnd('/'); + var normalizedPath = path.StartsWith("/") ? path[1..] : path; + return new Uri($"{normalizedBase}/{normalizedPath}"); + } + + private string Truncate(string? content) + { + if (string.IsNullOrEmpty(content)) + { + return string.Empty; + } + + var maxLength = _settings.ResponseMaxLength > 0 ? _settings.ResponseMaxLength : content.Length; + if (content.Length > maxLength) + { + return $"{content[..maxLength]}...(truncated)"; + } + + return content; + } +} diff --git a/src/Plugins/BotSharp.Plugin.Sandbox/Settings/SandboxSettings.cs b/src/Plugins/BotSharp.Plugin.Sandbox/Settings/SandboxSettings.cs new file mode 100644 index 000000000..a8eedadf2 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.Sandbox/Settings/SandboxSettings.cs @@ -0,0 +1,12 @@ +namespace BotSharp.Plugin.Sandbox.Settings; + +public class SandboxSettings +{ + public string BaseUrl { get; set; } = "http://localhost:3000"; + + public string ApiKey { get; set; } = string.Empty; + + public string ApiKeyHeader { get; set; } = "Authorization"; + + public int ResponseMaxLength { get; set; } = 6000; +} diff --git a/src/Plugins/BotSharp.Plugin.Sandbox/Using.cs b/src/Plugins/BotSharp.Plugin.Sandbox/Using.cs new file mode 100644 index 000000000..61bdf5e71 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.Sandbox/Using.cs @@ -0,0 +1,27 @@ +global using System; +global using System.Collections.Generic; +global using System.Net.Http; +global using System.Net.Mime; +global using System.Text; +global using System.Text.Json; +global using System.Threading.Tasks; + +global using Microsoft.Extensions.Configuration; +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.Logging; + +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; +global using BotSharp.Abstraction.Functions.Models; +global using BotSharp.Abstraction.Plugins; +global using BotSharp.Abstraction.Settings; +global using BotSharp.Abstraction.Utilities; + +global using BotSharp.Plugin.Sandbox.Models; +global using BotSharp.Plugin.Sandbox.Services; +global using BotSharp.Plugin.Sandbox.Settings; +global using BotSharp.Plugin.Sandbox.Functions; +global using BotSharp.Plugin.Sandbox.Hooks; diff --git a/src/Plugins/BotSharp.Plugin.Sandbox/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-sandbox-browser_screenshot.json b/src/Plugins/BotSharp.Plugin.Sandbox/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-sandbox-browser_screenshot.json new file mode 100644 index 000000000..36ae039b7 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.Sandbox/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-sandbox-browser_screenshot.json @@ -0,0 +1,13 @@ +{ + "name": "util-sandbox-browser_screenshot", + "description": "Capture the current sandbox browser viewport as a screenshot.", + "parameters": { + "type": "object", + "properties": { + "full_page": { + "type": "boolean", + "description": "Capture full page when true instead of viewport." + } + } + } +} diff --git a/src/Plugins/BotSharp.Plugin.Sandbox/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-sandbox-check_packages.json b/src/Plugins/BotSharp.Plugin.Sandbox/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-sandbox-check_packages.json new file mode 100644 index 000000000..70b9064c1 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.Sandbox/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-sandbox-check_packages.json @@ -0,0 +1,14 @@ +{ + "name": "util-sandbox-check_packages", + "description": "Check installed packages inside the sandbox for the given language runtime.", + "parameters": { + "type": "object", + "properties": { + "lang": { + "type": "string", + "description": "Language to inspect, such as python or nodejs." + } + }, + "required": [ "lang" ] + } +} diff --git a/src/Plugins/BotSharp.Plugin.Sandbox/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-sandbox-file_editor.json b/src/Plugins/BotSharp.Plugin.Sandbox/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-sandbox-file_editor.json new file mode 100644 index 000000000..37d6e9013 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.Sandbox/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-sandbox-file_editor.json @@ -0,0 +1,26 @@ +{ + "name": "util-sandbox-file_editor", + "description": "Apply precise string replace edits to a sandbox file.", + "parameters": { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "Target file path inside the sandbox." + }, + "search_text": { + "type": "string", + "description": "Text to match." + }, + "replacement": { + "type": "string", + "description": "Replacement text." + }, + "use_regex": { + "type": "boolean", + "description": "Interpret search_text as regex." + } + }, + "required": [ "path", "search_text", "replacement" ] + } +} diff --git a/src/Plugins/BotSharp.Plugin.Sandbox/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-sandbox-file_list.json b/src/Plugins/BotSharp.Plugin.Sandbox/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-sandbox-file_list.json new file mode 100644 index 000000000..a24dfd8cf --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.Sandbox/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-sandbox-file_list.json @@ -0,0 +1,18 @@ +{ + "name": "util-sandbox-file_list", + "description": "List files and folders under a sandbox directory.", + "parameters": { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "Directory path inside the sandbox." + }, + "recursive": { + "type": "boolean", + "description": "Whether to traverse subdirectories." + } + }, + "required": [ "path" ] + } +} diff --git a/src/Plugins/BotSharp.Plugin.Sandbox/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-sandbox-file_read.json b/src/Plugins/BotSharp.Plugin.Sandbox/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-sandbox-file_read.json new file mode 100644 index 000000000..6e92cba4d --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.Sandbox/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-sandbox-file_read.json @@ -0,0 +1,22 @@ +{ + "name": "util-sandbox-file_read", + "description": "Read the content of a file from the sandbox filesystem.", + "parameters": { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "Absolute file path inside the sandbox." + }, + "offset": { + "type": "integer", + "description": "Optional byte offset to start reading from." + }, + "limit": { + "type": "integer", + "description": "Optional max number of bytes to read." + } + }, + "required": [ "path" ] + } +} diff --git a/src/Plugins/BotSharp.Plugin.Sandbox/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-sandbox-file_search.json b/src/Plugins/BotSharp.Plugin.Sandbox/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-sandbox-file_search.json new file mode 100644 index 000000000..93d5c0d6c --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.Sandbox/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-sandbox-file_search.json @@ -0,0 +1,22 @@ +{ + "name": "util-sandbox-file_search", + "description": "Search for a string within files in the sandbox.", + "parameters": { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "Directory or file path to search." + }, + "query": { + "type": "string", + "description": "Text or pattern to search for." + }, + "is_regex": { + "type": "boolean", + "description": "Treat query as a regular expression when true." + } + }, + "required": [ "path", "query" ] + } +} diff --git a/src/Plugins/BotSharp.Plugin.Sandbox/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-sandbox-file_write.json b/src/Plugins/BotSharp.Plugin.Sandbox/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-sandbox-file_write.json new file mode 100644 index 000000000..c8025cfa8 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.Sandbox/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-sandbox-file_write.json @@ -0,0 +1,22 @@ +{ + "name": "util-sandbox-file_write", + "description": "Write text content to a sandbox file path.", + "parameters": { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "Absolute file path inside the sandbox." + }, + "content": { + "type": "string", + "description": "Plain text content to write." + }, + "append": { + "type": "boolean", + "description": "Append instead of overwrite when true." + } + }, + "required": [ "path", "content" ] + } +} diff --git a/src/Plugins/BotSharp.Plugin.Sandbox/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-sandbox-get_context.json b/src/Plugins/BotSharp.Plugin.Sandbox/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-sandbox-get_context.json new file mode 100644 index 000000000..9f86e080d --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.Sandbox/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-sandbox-get_context.json @@ -0,0 +1,8 @@ +{ + "name": "util-sandbox-get_context", + "description": "Retrieve sandbox environment information such as version and resource limits.", + "parameters": { + "type": "object", + "properties": {} + } +} diff --git a/src/Plugins/BotSharp.Plugin.Sandbox/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-sandbox-jupyter_create_session.json b/src/Plugins/BotSharp.Plugin.Sandbox/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-sandbox-jupyter_create_session.json new file mode 100644 index 000000000..2b48c05e8 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.Sandbox/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-sandbox-jupyter_create_session.json @@ -0,0 +1,13 @@ +{ + "name": "util-sandbox-jupyter_create_session", + "description": "Create a new interactive Jupyter session inside the sandbox.", + "parameters": { + "type": "object", + "properties": { + "kernel": { + "type": "string", + "description": "Preferred kernel (e.g., python)." + } + } + } +} diff --git a/src/Plugins/BotSharp.Plugin.Sandbox/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-sandbox-jupyter_exec.json b/src/Plugins/BotSharp.Plugin.Sandbox/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-sandbox-jupyter_exec.json new file mode 100644 index 000000000..25bc4b61e --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.Sandbox/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-sandbox-jupyter_exec.json @@ -0,0 +1,22 @@ +{ + "name": "util-sandbox-jupyter_exec", + "description": "Execute code through the sandbox Jupyter kernel.", + "parameters": { + "type": "object", + "properties": { + "code": { + "type": "string", + "description": "Python or notebook code to run." + }, + "session_id": { + "type": "string", + "description": "Existing Jupyter session id. Leave empty to run in a transient session." + }, + "timeout": { + "type": "integer", + "description": "Optional timeout seconds for execution." + } + }, + "required": [ "code" ] + } +} diff --git a/src/Plugins/BotSharp.Plugin.Sandbox/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-sandbox-shell_exec.json b/src/Plugins/BotSharp.Plugin.Sandbox/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-sandbox-shell_exec.json new file mode 100644 index 000000000..b97830253 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.Sandbox/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-sandbox-shell_exec.json @@ -0,0 +1,26 @@ +{ + "name": "util-sandbox-shell_exec", + "description": "Execute a shell command inside the isolated AIO sandbox container.", + "parameters": { + "type": "object", + "properties": { + "command": { + "type": "string", + "description": "Shell command to run." + }, + "timeout": { + "type": "integer", + "description": "Optional timeout seconds for the command." + }, + "workdir": { + "type": "string", + "description": "Working directory inside the sandbox." + }, + "async": { + "type": "boolean", + "description": "Whether to run asynchronously and return a task id." + } + }, + "required": [ "command" ] + } +} diff --git a/src/Plugins/BotSharp.Plugin.Sandbox/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-sandbox-shell_wait.json b/src/Plugins/BotSharp.Plugin.Sandbox/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-sandbox-shell_wait.json new file mode 100644 index 000000000..babaa191d --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.Sandbox/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-sandbox-shell_wait.json @@ -0,0 +1,18 @@ +{ + "name": "util-sandbox-shell_wait", + "description": "Wait for an asynchronous sandbox shell execution to finish and return its result.", + "parameters": { + "type": "object", + "properties": { + "task_id": { + "type": "string", + "description": "Task id returned by util-sandbox-shell_exec." + }, + "timeout": { + "type": "integer", + "description": "Optional timeout seconds for waiting." + } + }, + "required": [ "task_id" ] + } +} diff --git a/src/WebStarter/appsettings.json b/src/WebStarter/appsettings.json index 3fd5a88da..4c3d3b385 100644 --- a/src/WebStarter/appsettings.json +++ b/src/WebStarter/appsettings.json @@ -1089,7 +1089,8 @@ "BotSharp.Plugin.SqlDriver", "BotSharp.Plugin.TencentCos", "BotSharp.Plugin.PythonInterpreter", - "BotSharp.Plugin.FuzzySharp" + "BotSharp.Plugin.FuzzySharp", + "BotSharp.Plugin.Sandbox" ] } } diff --git a/tests/BotSharp.Plugin.Sandbox.Tests/BotSharp.Plugin.Sandbox.Tests.csproj b/tests/BotSharp.Plugin.Sandbox.Tests/BotSharp.Plugin.Sandbox.Tests.csproj new file mode 100644 index 000000000..2670d1396 --- /dev/null +++ b/tests/BotSharp.Plugin.Sandbox.Tests/BotSharp.Plugin.Sandbox.Tests.csproj @@ -0,0 +1,19 @@ + + + + net8.0 + enable + false + + + + + + + + + + + + + diff --git a/tests/BotSharp.Plugin.Sandbox.Tests/SandboxUtilityHookTests.cs b/tests/BotSharp.Plugin.Sandbox.Tests/SandboxUtilityHookTests.cs new file mode 100644 index 000000000..defd48ab4 --- /dev/null +++ b/tests/BotSharp.Plugin.Sandbox.Tests/SandboxUtilityHookTests.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; +using System.Linq; +using BotSharp.Abstraction.Agents.Models; +using BotSharp.Plugin.Sandbox.Hooks; +using BotSharp.Plugin.Sandbox.Models; + +namespace BotSharp.Plugin.Sandbox.Tests; + +[TestClass] +public class SandboxUtilityHookTests +{ + [TestMethod] + public void AddUtilities_ShouldExposeAllSandboxFunctions() + { + var utilities = new List(); + var hook = new SandboxUtilityHook(); + + hook.AddUtilities(utilities); + + Assert.AreEqual(1, utilities.Count); + var functionNames = utilities.Single().Items.Select(x => x.FunctionName).ToList(); + + var expected = new[] + { + SandboxFunctionNames.GetContext, + SandboxFunctionNames.ShellExec, + SandboxFunctionNames.ShellWait, + SandboxFunctionNames.FileRead, + SandboxFunctionNames.FileWrite, + SandboxFunctionNames.FileList, + SandboxFunctionNames.FileSearch, + SandboxFunctionNames.FileEditor, + SandboxFunctionNames.BrowserScreenshot, + SandboxFunctionNames.JupyterExec, + SandboxFunctionNames.JupyterCreateSession, + SandboxFunctionNames.CheckPackages + }; + + CollectionAssert.AreEquivalent(expected, functionNames); + } +} diff --git a/tests/BotSharp.Plugin.Sandbox.Tests/Usings.cs b/tests/BotSharp.Plugin.Sandbox.Tests/Usings.cs new file mode 100644 index 000000000..540383dcf --- /dev/null +++ b/tests/BotSharp.Plugin.Sandbox.Tests/Usings.cs @@ -0,0 +1 @@ +global using Microsoft.VisualStudio.TestTools.UnitTesting;