Current section
Files
Jump to
Current section
Files
lib/gepa/code_execution.ex
defmodule GEPA.CodeExecution do
@moduledoc """
Utilities for evaluating Elixir code snippets with captured outputs.
The upstream Python project exposes code-execution helpers for
`optimize_anything`. This module provides the Elixir equivalent with two
execution modes:
* `:in_process` evaluates code in the current BEAM process with `Code.eval_string/3`.
* `:subprocess` evaluates code in a separate `elixir` OS process.
Both modes return a structured result map instead of raising for user-code
failures.
"""
@type mode :: :in_process | :subprocess
@type result :: %{
required(:ok) => boolean(),
optional(:result) => term(),
optional(:stdout) => String.t(),
optional(:stderr) => String.t(),
optional(:error) => term(),
optional(:bindings) => keyword()
}
@doc """
Execute Elixir code and return a structured result.
## Options
* `:mode` - `:in_process` or `:subprocess` (default: `:in_process`)
* `:timeout` - timeout in milliseconds for subprocess execution (default: `5000`)
* `:bindings` - bindings passed to `Code.eval_string/3` in-process
* `:return_bindings` - include final bindings in the result (default: `false`)
"""
@spec execute(String.t(), keyword()) :: result()
def execute(code, opts \\ []) when is_binary(code) do
case Keyword.get(opts, :mode, :in_process) do
:in_process -> execute_in_process(code, opts)
:subprocess -> execute_subprocess(code, opts)
other -> %{ok: false, error: {:unsupported_mode, other}, stdout: "", stderr: ""}
end
end
@doc """
Convert side-info values into serializable values suitable for prompts.
"""
@spec side_info_to_data(term()) :: term()
def side_info_to_data(value) when is_map(value) do
Map.new(value, fn {key, val} -> {to_string(key), side_info_to_data(val)} end)
end
def side_info_to_data(value) when is_list(value), do: Enum.map(value, &side_info_to_data/1)
def side_info_to_data(value) when is_tuple(value),
do: value |> Tuple.to_list() |> side_info_to_data()
def side_info_to_data(value) when is_binary(value) or is_number(value) or is_boolean(value),
do: value
def side_info_to_data(nil), do: nil
def side_info_to_data(value), do: inspect(value)
defp execute_in_process(code, opts) do
bindings = Keyword.get(opts, :bindings, [])
{io_result, stdout} =
capture_io(fn ->
try do
{result, final_bindings} =
Code.eval_string(code, bindings, file: "gepa_code_execution.exs")
{:ok, result, final_bindings}
rescue
exception ->
{:error, Exception.format(:error, exception, __STACKTRACE__)}
catch
kind, reason ->
{:error, Exception.format(kind, reason, __STACKTRACE__)}
end
end)
case io_result do
{:ok, result, final_bindings} ->
base = %{ok: true, result: result, stdout: stdout, stderr: ""}
if Keyword.get(opts, :return_bindings, false) do
Map.put(base, :bindings, final_bindings)
else
base
end
{:error, error} ->
%{ok: false, error: error, stdout: stdout, stderr: ""}
end
end
defp execute_subprocess(code, opts) do
timeout = Keyword.get(opts, :timeout, 5_000)
dir = System.tmp_dir!()
path = Path.join(dir, "gepa_code_#{System.unique_integer([:positive])}.exs")
File.write!(path, code)
try do
task = Task.async(fn -> System.cmd("elixir", [path], stderr_to_stdout: true) end)
case Task.yield(task, timeout) || Task.shutdown(task, :brutal_kill) do
nil ->
%{ok: false, error: :timeout, stdout: "", stderr: ""}
{:ok, {output, 0}} ->
%{ok: true, result: nil, stdout: output, stderr: ""}
{:ok, {output, status}} ->
%{ok: false, error: {:exit_status, status}, stdout: output, stderr: ""}
end
catch
:exit, reason ->
%{ok: false, error: reason, stdout: "", stderr: ""}
after
File.rm(path)
end
end
defp capture_io(fun) do
{:ok, io} = StringIO.open("")
previous = Process.group_leader()
Process.group_leader(self(), io)
try do
result = fun.()
{_input, output} = StringIO.contents(io)
{result, output}
after
Process.group_leader(self(), previous)
end
end
end