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
6 changes: 4 additions & 2 deletions dspy/primitives/python_interpreter.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import os


class InterpreterError(ValueError):
class InterpreterError(RuntimeError):
pass


Expand Down Expand Up @@ -119,8 +119,10 @@ def execute(
error_type = result.get("errorType", "Sandbox Error")
if error_type == "SyntaxError":
raise SyntaxError(f"Invalid Python syntax. message: {error_msg}")
elif error_type == "FinalAnswer":
return result.get("errorArgs")
else:
raise InterpreterError(f"{error_type}: {error_msg}")
raise InterpreterError(f"{error_type}: {result.get('errorArgs') or error_msg}")

# If there's no error, return the "output" field
return result.get("output", None)
Expand Down
43 changes: 36 additions & 7 deletions dspy/primitives/runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ for await (const line of readLines(Deno.stdin)) {
}

const code = input.code || "";

// Wrap execution in a try/catch so we can handle syntax errors, etc.
try {
await pyodide.loadPackagesFromImports(code);
Expand All @@ -53,20 +53,35 @@ sys.stdout = buf_stdout
sys.stderr = buf_stderr
`);

// 2. Run the user's code asynchronously
// 2. Setup proper exception arguments extractor and FinalAnswer bridge
// The idea is borrowed from `smolagents` that uses the exception to simulate non-local exit
pyodide.runPython(`
import json

def last_exception_args():
return json.dumps(sys.last_exc.args) if sys.last_exc else None

class FinalAnswer(Exception):
pass

def final_answer(*args):
raise FinalAnswer(*args)
`);

// 3. Run the user's code asynchronously
const result = await pyodide.runPythonAsync(code);

// 3. Retrieve captured stdout/stderr
// 4. Retrieve captured stdout/stderr
const capturedStdout = pyodide.runPython("buf_stdout.getvalue()");
const capturedStderr = pyodide.runPython("buf_stderr.getvalue()");

// 4. Restore original stdout/stderr
// 5. Restore original stdout/stderr
pyodide.runPython(`
sys.stdout = old_stdout
sys.stderr = old_stderr
`);

// 5. Build our output object according to the rules:
// 6. Build our output object according to the rules:
// - If result is None (or Python "None" => JS null), output all prints
// - Else output the result only
// Note: `None` in Python becomes `null` in JS.
Expand All @@ -75,7 +90,7 @@ sys.stderr = old_stderr
// The final statement was None or no return => deliver printed output
// If you want to combine capturedStderr as well, you can append it
// But here we'll just do stdout for clarity
output = capturedStdout;
output = capturedStdout;
// If there's something in stderr, you might want to include that or log it
// output += capturedStderr;
} else {
Expand All @@ -90,9 +105,23 @@ sys.stderr = old_stderr
const errorType = error.type || "Error";
// error.message is mostly blank.
const errorMessage = (error.message || "").trim();
// The arguments of the exception are stored in sys.last_exc.args,
// which is always helpful but pyodide don't extract them for us.
// Use a bridge function to get them.
let errorArgs = [];
if (errorType !== "SyntaxError") {
// Only python exceptions have args.
const last_exception_args = pyodide.globals.get("last_exception_args");
// Regarding https://pyodide.org/en/stable/usage/type-conversions.html#type-translations-errors,
// we do a addtional `json.dumps` and `JSON.parse` on the values, to avoid the possible memory leak.
errorArgs = JSON.parse(last_exception_args()) || [];
}


console.log(JSON.stringify({
error: errorMessage,
errorArgs: errorArgs,
errorType: errorType
}));
}
}
}
20 changes: 20 additions & 0 deletions tests/primitives/test_python_interpreter.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import shutil

import pytest
import random


from dspy.primitives.python_interpreter import InterpreterError, PythonInterpreter

Expand Down Expand Up @@ -42,3 +44,21 @@ def test_failure_zero_division():
code = "1+0/0"
with pytest.raises(InterpreterError, match="ZeroDivisionError"):
interpreter.execute(code)


def test_exception_args():
with PythonInterpreter() as interpreter:
token = random.randint(1, 10**9)
code = f"raise ValueError({token})"
with pytest.raises(InterpreterError, match=rf"ValueError: \[{token}\]"):
interpreter.execute(code)


def test_final_answer_trick():
with PythonInterpreter() as interpreter:
token = random.randint(1, 10**9)
code = f"final_answer('The result is', {token})"
result = interpreter(code)

# They should matain the same order
assert result == ["The result is", token], "The returned results are differ, `final_answer` trick doesn't work"
Loading