2025-05-20 11:38:24 +00:00
using System.Text ;
using Elsa.Expressions.Helpers ;
using Elsa.Expressions.Models ;
using Elsa.Mediator.Contracts ;
2025-05-20 11:52:19 +00:00
using Elsa.Expressions.Python.Contracts ;
using Elsa.Expressions.Python.Models ;
using Elsa.Expressions.Python.Notifications ;
2026-05-20 12:19:47 +00:00
using Elsa.Expressions.Python.Options ;
using Microsoft.Extensions.Options ;
2025-05-20 11:38:24 +00:00
using Python.Runtime ;
2025-05-20 11:52:19 +00:00
namespace Elsa.Expressions.Python.Services ;
2025-05-20 11:38:24 +00:00
/// <summary>
/// Evaluates Python expressions using IronPython.
/// </summary>
public class PythonNetPythonEvaluator : IPythonEvaluator
{
private const string ReturnVarName = "elsa_python_result_variable_name" ;
private readonly INotificationSender _notificationSender ;
2026-05-20 12:19:47 +00:00
private readonly IOptions < PythonOptions > _options ;
2025-05-20 11:38:24 +00:00
/// <summary>
/// Initializes a new instance of the <see cref="PythonNetPythonEvaluator"/> class.
/// </summary>
2026-05-20 12:19:47 +00:00
public PythonNetPythonEvaluator ( INotificationSender notificationSender ) : this ( notificationSender , Microsoft . Extensions . Options . Options . Create ( new PythonOptions ( ) ) )
{
}
/// <summary>
/// Initializes a new instance of the <see cref="PythonNetPythonEvaluator"/> class.
/// </summary>
public PythonNetPythonEvaluator ( INotificationSender notificationSender , IOptions < PythonOptions > options )
2025-05-20 11:38:24 +00:00
{
_notificationSender = notificationSender ;
2026-05-20 12:19:47 +00:00
_options = options ;
2025-05-20 11:38:24 +00:00
}
/// <inheritdoc />
public async Task < object? > EvaluateAsync ( string expression , Type returnType , ExpressionExecutionContext context , CancellationToken cancellationToken = default )
{
2026-05-20 12:19:47 +00:00
if ( ! _options . Value . AllowHostCodeExecution )
throw new InvalidOperationException ( "Python.NET workflow expression execution is disabled. Set PythonOptions.AllowHostCodeExecution to true only for trusted workflow authors; Python.NET is not a sandbox." ) ;
2025-05-20 11:38:24 +00:00
using var gil = Py . GIL ( ) ;
using var scope = Py . CreateScope ( ) ;
var notification = new EvaluatingPython ( scope , context ) ;
scope . Import ( "System" ) ;
// Add globals.
scope . Set ( "execution_context" , new ExecutionContextProxy ( context ) ) ;
scope . Set ( "input" , new InputProxy ( context ) ) ;
scope . Set ( "output" , new OutputProxy ( context ) ) ;
scope . Set ( "outcome" , new OutcomeProxy ( context ) ) ;
await _notificationSender . SendAsync ( notification , cancellationToken ) ;
var wrappedScript = WrapInExecuteScriptFunction ( expression ) ;
scope . Exec ( wrappedScript ) ;
var result = scope . Get < object > ( ReturnVarName ) ;
return result . ConvertTo ( returnType ) ;
}
/// <summary>
/// Wraps the user script in a function called execute_script() and returns the result of that function.
/// </summary>
private static string WrapInExecuteScriptFunction ( string userScript , int indentationLevel = 1 )
{
var lines = userScript . Split ( '\n' ) ;
var wrappedScript = new StringBuilder ( ) ;
var indentation = new string ( ' ' , 4 * indentationLevel ) ;
wrappedScript . AppendLine ( "def execute_script():" ) ;
foreach ( var line in lines . Take ( lines . Length - 1 ) )
wrappedScript . AppendLine ( indentation + line ) ;
var lastLine = lines . LastOrDefault ( ) ? ? "" ;
if ( ! lastLine . StartsWith ( "return" ) )
lastLine = $"return {lastLine}" ;
wrappedScript . AppendLine ( indentation + lastLine ) ;
wrappedScript . AppendLine ( $"{ReturnVarName} = execute_script()" ) ;
return wrappedScript . ToString ( ) ;
}
2026-05-20 12:19:47 +00:00
}