Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -11,11 +12,12 @@ public interface ICodeProcessor
/// <summary>
/// Run code script
/// </summary>
/// <param name="codeScript">The code scirpt to run</param>
/// <param name="codeScript">The code script to run</param>
/// <param name="options">Code script execution options</param>
/// <param name="cancellationToken">The cancellation token</param>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
Task<CodeInterpretResponse> RunAsync(string codeScript, CodeInterpretOptions? options = null)
Task<CodeInterpretResponse> RunAsync(string codeScript, CodeInterpretOptions? options = null, CancellationToken cancellationToken = default)
=> throw new NotImplementedException();

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ public class CodeInterpretOptions
{
public string? ScriptName { get; set; }
public IEnumerable<KeyValue>? Arguments { get; set; }
public bool UseMutex { get; set; }
public bool UseLock { get; set; }
public bool UseProcess { get; set; }
public CancellationToken? CancellationToken { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ public class CodeInterpretResponse : ResponseBase

public override string ToString()
{
return Result ?? ErrorMsg ?? $"Success: {Success}";
return Result.IfNullOrEmptyAs(ErrorMsg ?? $"Success: {Success}")!;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,11 @@ public class CodeInstructOptions
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("arguments")]
public List<KeyValue>? Arguments { get; set; }

/// <summary>
/// Timeout in seconds
/// </summary>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("timeout_seconds")]
public int? TimeoutSeconds { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,17 @@ public PyCodeInterpreter(

public string Provider => "botsharp-py-interpreter";

public async Task<CodeInterpretResponse> RunAsync(string codeScript, CodeInterpretOptions? options = null)
public async Task<CodeInterpretResponse> 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<CodeGenerationResult> GenerateCodeScriptAsync(string text, CodeGenerationOptions? options = null)
Expand Down Expand Up @@ -98,7 +98,7 @@ public async Task<CodeGenerationResult> GenerateCodeScriptAsync(string text, Cod


#region Private methods
private async Task<CodeInterpretResponse> InnerRunCode(string codeScript, CodeInterpretOptions? options = null)
private async Task<CodeInterpretResponse> InnerRunCode(string codeScript, CodeInterpretOptions? options = null, CancellationToken cancellationToken = default)
{
var response = new CodeInterpretResponse();
var scriptName = options?.ScriptName ?? codeScript.SubstringMax(30);
Expand All @@ -109,11 +109,11 @@ private async Task<CodeInterpretResponse> 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}");
Expand All @@ -134,88 +134,90 @@ private async Task<CodeInterpretResponse> InnerRunCode(string codeScript, CodeIn
}
}

private async Task<CodeInterpretResponse> CoreRunScript(string codeScript, CodeInterpretOptions? options = null)
private async Task<CodeInterpretResponse> 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<CodeInterpretResponse> CoreRunProcess(string codeScript, CodeInterpretOptions? options = null)
private async Task<CodeInterpretResponse> CoreRunProcess(string codeScript, CodeInterpretOptions? options = null, CancellationToken cancellationToken = default)
{
var token = options?.CancellationToken ?? CancellationToken.None;

var psi = new ProcessStartInfo
{
FileName = "python",
Expand Down Expand Up @@ -252,7 +254,7 @@ private async Task<CodeInterpretResponse> CoreRunProcess(string codeScript, Code

try
{
using var reg = token.Register(() =>
using var reg = cancellationToken.Register(() =>
{
try
{
Expand All @@ -264,12 +266,12 @@ private async Task<CodeInterpretResponse> 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
{
Expand Down
Loading