From 03880b06769f95e44a57b1352eb8cd922d6eb2c2 Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Thu, 13 Nov 2025 16:27:25 -0600 Subject: [PATCH 1/2] refine py interpreter --- .../Coding/ICodeProcessor.cs | 2 +- .../Coding/Settings/CodingSettings.cs | 1 - .../BotSharp.Core.Rules/Engines/RuleEngine.cs | 2 +- .../Coding/CodeScriptExecutor.cs | 39 ----------- .../BotSharp.Core/Coding/CodingPlugin.cs | 2 - .../Conversations/ConversationPlugin.cs | 1 - .../Services/InstructService.Execute.cs | 2 +- .../Functions/PyProgrammerFn.cs | 6 +- .../Services/PyCodeInterpreter.cs | 69 +++++++++---------- src/WebStarter/appsettings.json | 3 +- 10 files changed, 41 insertions(+), 86 deletions(-) delete mode 100644 src/Infrastructure/BotSharp.Core/Coding/CodeScriptExecutor.cs diff --git a/src/Infrastructure/BotSharp.Abstraction/Coding/ICodeProcessor.cs b/src/Infrastructure/BotSharp.Abstraction/Coding/ICodeProcessor.cs index 3f550faa6..bd34eb257 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Coding/ICodeProcessor.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Coding/ICodeProcessor.cs @@ -17,7 +17,7 @@ public interface ICodeProcessor /// The cancellation token /// /// - Task RunAsync(string codeScript, CodeInterpretOptions? options = null, CancellationToken cancellationToken = default) + CodeInterpretResponse Run(string codeScript, CodeInterpretOptions? options = null, CancellationToken cancellationToken = default) => throw new NotImplementedException(); /// diff --git a/src/Infrastructure/BotSharp.Abstraction/Coding/Settings/CodingSettings.cs b/src/Infrastructure/BotSharp.Abstraction/Coding/Settings/CodingSettings.cs index 38f635222..49747798e 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Coding/Settings/CodingSettings.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Coding/Settings/CodingSettings.cs @@ -19,5 +19,4 @@ public class CodeScriptExecutionSettings public bool UseLock { get; set; } public bool UseProcess { get; set; } public int TimeoutSeconds { get; set; } = 3; - public int MaxConcurrency { get; set; } = 1; } \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs index 5442134cf..442f64fea 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs @@ -140,7 +140,7 @@ private async Task TriggerCodeScript(Agent agent, string triggerName, Rule var (useLock, useProcess, timeoutSeconds) = GetCodeExecutionConfig(); using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(timeoutSeconds)); - var response = await processor.RunAsync(codeScript.Content, options: new() + var response = processor.Run(codeScript.Content, options: new() { ScriptName = scriptName, Arguments = arguments, diff --git a/src/Infrastructure/BotSharp.Core/Coding/CodeScriptExecutor.cs b/src/Infrastructure/BotSharp.Core/Coding/CodeScriptExecutor.cs deleted file mode 100644 index a735c4dc3..000000000 --- a/src/Infrastructure/BotSharp.Core/Coding/CodeScriptExecutor.cs +++ /dev/null @@ -1,39 +0,0 @@ -namespace BotSharp.Core.Coding; - -public class CodeScriptExecutor -{ - private const int DEFAULT_MAX_CONCURRENCY = 1; - - private readonly CodingSettings _settings; - private readonly ILogger _logger; - private readonly SemaphoreSlim _semLock = new(initialCount: DEFAULT_MAX_CONCURRENCY, maxCount: DEFAULT_MAX_CONCURRENCY); - - public CodeScriptExecutor( - CodingSettings settings, - ILogger logger) - { - _settings = settings; - _logger = logger; - - var maxConcurrency = settings.CodeExecution?.MaxConcurrency > 0 ? settings.CodeExecution.MaxConcurrency : DEFAULT_MAX_CONCURRENCY; - _semLock = new(initialCount: maxConcurrency, maxCount: maxConcurrency); - } - - public async Task ExecuteAsync(Func> func, CancellationToken cancellationToken = default) - { - try - { - await _semLock.WaitAsync(cancellationToken); - return await func(); - } - catch (Exception ex) - { - _logger.LogError(ex, $"Error in {nameof(CodeScriptExecutor)}."); - return default(T); - } - finally - { - _semLock.Release(); - } - } -} diff --git a/src/Infrastructure/BotSharp.Core/Coding/CodingPlugin.cs b/src/Infrastructure/BotSharp.Core/Coding/CodingPlugin.cs index 4e0ccdda3..efbe13371 100644 --- a/src/Infrastructure/BotSharp.Core/Coding/CodingPlugin.cs +++ b/src/Infrastructure/BotSharp.Core/Coding/CodingPlugin.cs @@ -13,7 +13,5 @@ public void RegisterDI(IServiceCollection services, IConfiguration config) var coding = new CodingSettings(); config.Bind("Coding", coding); services.AddSingleton(provider => coding); - - services.AddSingleton(); } } \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Core/Conversations/ConversationPlugin.cs b/src/Infrastructure/BotSharp.Core/Conversations/ConversationPlugin.cs index e1c44a9a1..6de7da9d6 100644 --- a/src/Infrastructure/BotSharp.Core/Conversations/ConversationPlugin.cs +++ b/src/Infrastructure/BotSharp.Core/Conversations/ConversationPlugin.cs @@ -69,7 +69,6 @@ public void RegisterDI(IServiceCollection services, IConfiguration config) services.AddScoped(); services.AddScoped(); - services.AddSingleton(); } public bool AttachMenu(List menu) diff --git a/src/Infrastructure/BotSharp.Core/Instructs/Services/InstructService.Execute.cs b/src/Infrastructure/BotSharp.Core/Instructs/Services/InstructService.Execute.cs index 8e4d3ec6d..c2dca0d9e 100644 --- a/src/Infrastructure/BotSharp.Core/Instructs/Services/InstructService.Execute.cs +++ b/src/Infrastructure/BotSharp.Core/Instructs/Services/InstructService.Execute.cs @@ -258,7 +258,7 @@ await hook.OnResponseGenerated(new InstructResponseModel // Run code script var (useLock, useProcess, timeoutSeconds) = GetCodeExecutionConfig(codingSettings); using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(timeoutSeconds)); - var codeResponse = await codeProcessor.RunAsync(context.CodeScript?.Content ?? string.Empty, options: new() + var codeResponse = codeProcessor.Run(context.CodeScript?.Content ?? string.Empty, options: new() { ScriptName = context.CodeScript?.Name, Arguments = context.Arguments, diff --git a/src/Plugins/BotSharp.Plugin.PythonInterpreter/Functions/PyProgrammerFn.cs b/src/Plugins/BotSharp.Plugin.PythonInterpreter/Functions/PyProgrammerFn.cs index bdbebcab1..642336aaf 100644 --- a/src/Plugins/BotSharp.Plugin.PythonInterpreter/Functions/PyProgrammerFn.cs +++ b/src/Plugins/BotSharp.Plugin.PythonInterpreter/Functions/PyProgrammerFn.cs @@ -71,7 +71,7 @@ public async Task Execute(RoleDialogModel message) try { - var (isSuccess, result) = await InnerRunCode(ret.PythonCode); + var (isSuccess, result) = InnerRunCode(ret.PythonCode); if (isSuccess) { message.Content = result; @@ -107,7 +107,7 @@ public async Task Execute(RoleDialogModel message) /// /// /// - private async Task<(bool, string)> InnerRunCode(string codeScript) + private (bool, string) InnerRunCode(string codeScript) { var codeProvider = _codingSettings.CodeExecution?.Processor; codeProvider = !string.IsNullOrEmpty(codeProvider) ? codeProvider : BuiltInCodeProcessor.PyInterpreter; @@ -122,7 +122,7 @@ public async Task Execute(RoleDialogModel message) var (useLock, useProcess, timeoutSeconds) = GetCodeExecutionConfig(); using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(timeoutSeconds)); - var response = await processor.RunAsync(codeScript, options: new() + var response = processor.Run(codeScript, options: new() { UseLock = useLock, UseProcess = useProcess diff --git a/src/Plugins/BotSharp.Plugin.PythonInterpreter/Services/PyCodeInterpreter.cs b/src/Plugins/BotSharp.Plugin.PythonInterpreter/Services/PyCodeInterpreter.cs index 815fab72d..db6a9feff 100644 --- a/src/Plugins/BotSharp.Plugin.PythonInterpreter/Services/PyCodeInterpreter.cs +++ b/src/Plugins/BotSharp.Plugin.PythonInterpreter/Services/PyCodeInterpreter.cs @@ -12,42 +12,37 @@ public class PyCodeInterpreter : ICodeProcessor { private readonly IServiceProvider _services; private readonly ILogger _logger; - private readonly CodeScriptExecutor _executor; private readonly CodingSettings _settings; + private static SemaphoreSlim _semLock = new(initialCount: 1, maxCount: 1); public PyCodeInterpreter( IServiceProvider services, ILogger logger, - CodeScriptExecutor executor, CodingSettings settings) { _services = services; _logger = logger; - _executor = executor; _settings = settings; } public string Provider => BuiltInCodeProcessor.PyInterpreter; - public async Task RunAsync(string codeScript, CodeInterpretOptions? options = null, CancellationToken cancellationToken = default) + public CodeInterpretResponse Run(string codeScript, CodeInterpretOptions? options = null, CancellationToken cancellationToken = default) { if (options?.UseLock == true) { try { - return await _executor.ExecuteAsync(async () => - { - return await InnerRunCode(codeScript, options, cancellationToken); - }, cancellationToken: cancellationToken); + return InnerRunWithLock(codeScript, options, cancellationToken); } catch (Exception ex) { - _logger.LogError(ex, "Error when using code script executor."); + _logger.LogError(ex, $"Error when using code script with {nameof(SemaphoreSlim)}."); return new() { ErrorMsg = ex.Message }; } } - return await InnerRunCode(codeScript, options, cancellationToken); + return InnerRunCode(codeScript, options, cancellationToken); } public async Task GenerateCodeScriptAsync(string text, CodeGenerationOptions? options = null) @@ -105,7 +100,30 @@ public async Task GenerateCodeScriptAsync(string text, Cod #region Private methods - private async Task InnerRunCode(string codeScript, CodeInterpretOptions? options = null, CancellationToken cancellationToken = default) + private CodeInterpretResponse InnerRunWithLock(string codeScript, CodeInterpretOptions? options = null, CancellationToken cancellationToken = default) + { + var lockAcquired = false; + try + { + _semLock.Wait(cancellationToken); + lockAcquired = true; + return InnerRunCode(codeScript, options, cancellationToken); + } + catch (Exception ex) + { + _logger.LogError(ex, $"Error in {nameof(InnerRunWithLock)}"); + return new() { ErrorMsg = ex.Message }; + } + finally + { + if (lockAcquired) + { + _semLock.Release(); + } + } + } + + private CodeInterpretResponse InnerRunCode(string codeScript, CodeInterpretOptions? options = null, CancellationToken cancellationToken = default) { var response = new CodeInterpretResponse(); var scriptName = options?.ScriptName ?? codeScript.SubstringMax(30); @@ -116,11 +134,11 @@ private async Task InnerRunCode(string codeScript, CodeIn if (options?.UseProcess == true) { - response = await CoreRunProcess(codeScript, options, cancellationToken); + response = CoreRunProcess(codeScript, options, cancellationToken).ConfigureAwait(false).GetAwaiter().GetResult(); } else { - response = await CoreRunScript(codeScript, options, cancellationToken); + response = CoreRunScript(codeScript, options, cancellationToken); } _logger.LogWarning($"End running python code script in {Provider}: {scriptName}"); @@ -141,7 +159,7 @@ private async Task InnerRunCode(string codeScript, CodeIn } } - private async Task CoreRunScript(string codeScript, CodeInterpretOptions? options = null, CancellationToken cancellationToken = default) + private CodeInterpretResponse CoreRunScript(string codeScript, CodeInterpretOptions? options = null, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); @@ -161,21 +179,6 @@ private async Task CoreRunScript(string codeScript, CodeI try { - // Capture the Python thread ID for the current thread executing under the GIL - var pythonThreadId = PythonEngine.GetPythonThreadID(); - using var reg = cancellationToken.Register(() => - { - try - { - PythonEngine.Interrupt(pythonThreadId); - _logger.LogWarning($"Cancellation requested: issued PythonEngine.Interrupt for thread {pythonThreadId} (request {requestId})"); - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to interrupt Python execution on cancellation."); - } - }); - // Redirect standard output/error to capture it dynamic outIO = io.StringIO(); dynamic errIO = io.StringIO(); @@ -206,8 +209,6 @@ private async Task CoreRunScript(string codeScript, CodeI } sys.argv = list; - cancellationToken.ThrowIfCancellationRequested(); - // Execute Python script PythonEngine.Exec(codeScript, globals); @@ -217,8 +218,6 @@ private async Task CoreRunScript(string codeScript, CodeI var stdout = outIO.getvalue()?.ToString() as string; var stderr = errIO.getvalue()?.ToString() as string; - cancellationToken.ThrowIfCancellationRequested(); - return new CodeInterpretResponse { Result = stdout?.TrimEnd('\r', '\n') ?? string.Empty, @@ -237,10 +236,10 @@ private async Task CoreRunScript(string codeScript, CodeI sys.stderr = sys.__stderr__; sys.argv = new PyList(); } - }; + } }, cancellationToken); - return await execTask.WaitAsync(cancellationToken); + return execTask.WaitAsync(cancellationToken).ConfigureAwait(false).GetAwaiter().GetResult(); } diff --git a/src/WebStarter/appsettings.json b/src/WebStarter/appsettings.json index be70e0d47..dce34aac8 100644 --- a/src/WebStarter/appsettings.json +++ b/src/WebStarter/appsettings.json @@ -527,8 +527,7 @@ "Processor": null, "UseLock": false, "UseProcess": false, - "TimeoutSeconds": 3, - "MaxConcurrency": 1 + "TimeoutSeconds": 3 } }, From cd6eb1c908a51da72be57c53dd6357e887703317 Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Thu, 13 Nov 2025 16:35:25 -0600 Subject: [PATCH 2/2] minor change --- .../Services/PyCodeInterpreter.cs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/Plugins/BotSharp.Plugin.PythonInterpreter/Services/PyCodeInterpreter.cs b/src/Plugins/BotSharp.Plugin.PythonInterpreter/Services/PyCodeInterpreter.cs index db6a9feff..88d57a18b 100644 --- a/src/Plugins/BotSharp.Plugin.PythonInterpreter/Services/PyCodeInterpreter.cs +++ b/src/Plugins/BotSharp.Plugin.PythonInterpreter/Services/PyCodeInterpreter.cs @@ -31,15 +31,7 @@ public CodeInterpretResponse Run(string codeScript, CodeInterpretOptions? option { if (options?.UseLock == true) { - try - { - return InnerRunWithLock(codeScript, options, cancellationToken); - } - catch (Exception ex) - { - _logger.LogError(ex, $"Error when using code script with {nameof(SemaphoreSlim)}."); - return new() { ErrorMsg = ex.Message }; - } + return InnerRunWithLock(codeScript, options, cancellationToken); } return InnerRunCode(codeScript, options, cancellationToken);