diff --git a/src/Infrastructure/BotSharp.Abstraction/Coding/ICodeProcessor.cs b/src/Infrastructure/BotSharp.Abstraction/Coding/ICodeProcessor.cs index 0ca0c54d3..3f550faa6 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Coding/ICodeProcessor.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Coding/ICodeProcessor.cs @@ -1,6 +1,7 @@ using BotSharp.Abstraction.Coding.Models; using BotSharp.Abstraction.Coding.Options; using BotSharp.Abstraction.Coding.Responses; +using System.Threading; namespace BotSharp.Abstraction.Coding; @@ -11,11 +12,12 @@ public interface ICodeProcessor /// /// Run code script /// - /// The code scirpt to run + /// The code script to run /// Code script execution options + /// The cancellation token /// /// - Task RunAsync(string codeScript, CodeInterpretOptions? options = null) + Task RunAsync(string codeScript, CodeInterpretOptions? options = null, CancellationToken cancellationToken = default) => throw new NotImplementedException(); /// diff --git a/src/Infrastructure/BotSharp.Abstraction/Coding/Options/CodeInterpretOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Coding/Options/CodeInterpretOptions.cs index 5dec8367e..b392d6351 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Coding/Options/CodeInterpretOptions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Coding/Options/CodeInterpretOptions.cs @@ -6,7 +6,6 @@ public class CodeInterpretOptions { public string? ScriptName { get; set; } public IEnumerable? Arguments { get; set; } - public bool UseMutex { get; set; } + public bool UseLock { get; set; } public bool UseProcess { get; set; } - public CancellationToken? CancellationToken { get; set; } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Coding/Responses/CodeInterpretResponse.cs b/src/Infrastructure/BotSharp.Abstraction/Coding/Responses/CodeInterpretResponse.cs index 2e46e3857..405028699 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Coding/Responses/CodeInterpretResponse.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Coding/Responses/CodeInterpretResponse.cs @@ -6,6 +6,6 @@ public class CodeInterpretResponse : ResponseBase public override string ToString() { - return Result ?? ErrorMsg ?? $"Success: {Success}"; + return Result.IfNullOrEmptyAs(ErrorMsg ?? $"Success: {Success}")!; } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Instructs/Options/CodeInstructOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Instructs/Options/CodeInstructOptions.cs index 182196753..25f63949c 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Instructs/Options/CodeInstructOptions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Instructs/Options/CodeInstructOptions.cs @@ -29,4 +29,11 @@ public class CodeInstructOptions [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("arguments")] public List? Arguments { get; set; } + + /// + /// Timeout in seconds + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("timeout_seconds")] + public int? TimeoutSeconds { get; set; } } diff --git a/src/Infrastructure/BotSharp.Core/Instructs/Services/InstructService.Execute.cs b/src/Infrastructure/BotSharp.Core/Instructs/Services/InstructService.Execute.cs index 00fd02b6d..535547005 100644 --- a/src/Infrastructure/BotSharp.Core/Instructs/Services/InstructService.Execute.cs +++ b/src/Infrastructure/BotSharp.Core/Instructs/Services/InstructService.Execute.cs @@ -254,13 +254,13 @@ await hook.OnResponseGenerated(new InstructResponseModel } // Run code script - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3)); + var seconds = codeOptions?.TimeoutSeconds > 0 ? codeOptions.TimeoutSeconds.Value : 3; + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(seconds)); var codeResponse = await codeProcessor.RunAsync(context.CodeScript, options: new() { ScriptName = scriptName, - Arguments = context.Arguments, - CancellationToken = cts.Token - }); + Arguments = context.Arguments + }, cancellationToken: cts.Token); if (codeResponse == null || !codeResponse.Success) { diff --git a/src/Plugins/BotSharp.Plugin.PythonInterpreter/Services/PyCodeInterpreter.cs b/src/Plugins/BotSharp.Plugin.PythonInterpreter/Services/PyCodeInterpreter.cs index 107644742..212d8c0cb 100644 --- a/src/Plugins/BotSharp.Plugin.PythonInterpreter/Services/PyCodeInterpreter.cs +++ b/src/Plugins/BotSharp.Plugin.PythonInterpreter/Services/PyCodeInterpreter.cs @@ -30,17 +30,17 @@ public PyCodeInterpreter( public string Provider => "botsharp-py-interpreter"; - public async Task RunAsync(string codeScript, CodeInterpretOptions? options = null) + public async Task RunAsync(string codeScript, CodeInterpretOptions? options = null, CancellationToken cancellationToken = default) { - if (options?.UseMutex == true) + if (options?.UseLock == true) { return await _executor.ExecuteAsync(async () => { - return await InnerRunCode(codeScript, options); - }, cancellationToken: options?.CancellationToken ?? CancellationToken.None); + return await InnerRunCode(codeScript, options, cancellationToken); + }, cancellationToken: cancellationToken); } - return await InnerRunCode(codeScript, options); + return await InnerRunCode(codeScript, options, cancellationToken); } public async Task GenerateCodeScriptAsync(string text, CodeGenerationOptions? options = null) @@ -98,7 +98,7 @@ public async Task GenerateCodeScriptAsync(string text, Cod #region Private methods - private async Task InnerRunCode(string codeScript, CodeInterpretOptions? options = null) + private async Task InnerRunCode(string codeScript, CodeInterpretOptions? options = null, CancellationToken cancellationToken = default) { var response = new CodeInterpretResponse(); var scriptName = options?.ScriptName ?? codeScript.SubstringMax(30); @@ -109,11 +109,11 @@ private async Task InnerRunCode(string codeScript, CodeIn if (options?.UseProcess == true) { - response = await CoreRunProcess(codeScript, options); + response = await CoreRunProcess(codeScript, options, cancellationToken); } else { - response = await CoreRunScript(codeScript, options); + response = await CoreRunScript(codeScript, options, cancellationToken); } _logger.LogWarning($"End running python code script in {Provider}: {scriptName}"); @@ -134,88 +134,90 @@ private async Task InnerRunCode(string codeScript, CodeIn } } - private async Task CoreRunScript(string codeScript, CodeInterpretOptions? options = null) + private async Task CoreRunScript(string codeScript, CodeInterpretOptions? options = null, CancellationToken cancellationToken = default) { _logger.LogWarning($"Begin {nameof(CoreRunScript)} in {Provider}: ${options?.ScriptName}"); - var token = options?.CancellationToken ?? CancellationToken.None; - token.ThrowIfCancellationRequested(); + cancellationToken.ThrowIfCancellationRequested(); - using (Py.GIL()) + var execTask = Task.Factory.StartNew(() => { - token.ThrowIfCancellationRequested(); - - // Import necessary Python modules - dynamic sys = Py.Import("sys"); - dynamic io = Py.Import("io"); - - try + using (Py.GIL()) { - // Redirect standard output/error to capture it - dynamic stringIO = io.StringIO(); - sys.stdout = stringIO; - sys.stderr = stringIO; - - // Set global items - using var globals = new PyDict(); - if (codeScript.Contains("__main__") == true) - { - globals.SetItem("__name__", new PyString("__main__")); - } + // Import necessary Python modules + dynamic sys = Py.Import("sys"); + dynamic io = Py.Import("io"); - // Set arguments - var list = new PyList(); - if (options?.Arguments?.Any() == true) + try { - list.Append(new PyString(options?.ScriptName ?? "script.py")); + // Redirect standard output/error to capture it + dynamic outIO = io.StringIO(); + dynamic errIO = io.StringIO(); + sys.stdout = outIO; + sys.stderr = errIO; + + // Set global items + using var globals = new PyDict(); + if (codeScript.Contains("__main__") == true) + { + globals.SetItem("__name__", new PyString("__main__")); + } - foreach (var arg in options!.Arguments) + // Set arguments + var list = new PyList(); + if (options?.Arguments?.Any() == true) { - if (!string.IsNullOrWhiteSpace(arg.Key) && !string.IsNullOrWhiteSpace(arg.Value)) + list.Append(new PyString(options?.ScriptName ?? "script.py")); + + foreach (var arg in options!.Arguments) { - list.Append(new PyString($"--{arg.Key}")); - list.Append(new PyString($"{arg.Value}")); + if (!string.IsNullOrWhiteSpace(arg.Key) && !string.IsNullOrWhiteSpace(arg.Value)) + { + list.Append(new PyString($"--{arg.Key}")); + list.Append(new PyString($"{arg.Value}")); + } } } - } - sys.argv = list; + sys.argv = list; - token.ThrowIfCancellationRequested(); + cancellationToken.ThrowIfCancellationRequested(); - // Execute Python script - PythonEngine.Exec(codeScript, globals); + // Execute Python script + PythonEngine.Exec(codeScript, globals); - // Get result - var result = stringIO.getvalue()?.ToString() as string; + // Get result + var stdout = outIO.getvalue()?.ToString() as string; + var stderr = errIO.getvalue()?.ToString() as string; - token.ThrowIfCancellationRequested(); + cancellationToken.ThrowIfCancellationRequested(); - return new CodeInterpretResponse + return new CodeInterpretResponse + { + Result = stdout?.TrimEnd('\r', '\n') ?? string.Empty, + Success = true + }; + } + catch (Exception ex) { - Result = result?.TrimEnd('\r', '\n') ?? string.Empty, - Success = true - }; - } - catch (Exception ex) - { - _logger.LogError(ex, $"Error in {nameof(CoreRunScript)} in {Provider}."); - throw; - } - finally - { - // Restore the original stdout/stderr/argv - sys.stdout = sys.__stdout__; - sys.stderr = sys.__stderr__; - sys.argv = new PyList(); - } - }; + _logger.LogError(ex, $"Error in {nameof(CoreRunScript)} in {Provider}."); + return new() { ErrorMsg = ex.Message }; + } + finally + { + // Restore the original stdout/stderr/argv + sys.stdout = sys.__stdout__; + sys.stderr = sys.__stderr__; + sys.argv = new PyList(); + } + }; + }, cancellationToken); + + return await execTask.WaitAsync(cancellationToken); } - private async Task CoreRunProcess(string codeScript, CodeInterpretOptions? options = null) + private async Task CoreRunProcess(string codeScript, CodeInterpretOptions? options = null, CancellationToken cancellationToken = default) { - var token = options?.CancellationToken ?? CancellationToken.None; - var psi = new ProcessStartInfo { FileName = "python", @@ -252,7 +254,7 @@ private async Task CoreRunProcess(string codeScript, Code try { - using var reg = token.Register(() => + using var reg = cancellationToken.Register(() => { try { @@ -264,12 +266,12 @@ private async Task CoreRunProcess(string codeScript, Code catch { } }); - var stdoutTask = proc.StandardOutput.ReadToEndAsync(token); - var stderrTask = proc.StandardError.ReadToEndAsync(token); + var stdoutTask = proc.StandardOutput.ReadToEndAsync(cancellationToken); + var stderrTask = proc.StandardError.ReadToEndAsync(cancellationToken); - await Task.WhenAll([proc.WaitForExitAsync(token), stdoutTask, stderrTask]); + await Task.WhenAll([proc.WaitForExitAsync(cancellationToken), stdoutTask, stderrTask]); - token.ThrowIfCancellationRequested(); + cancellationToken.ThrowIfCancellationRequested(); return new CodeInterpretResponse {