add code interpreter from open-interpreter
Browse files- code_interpreters/__init__.py +0 -0
- code_interpreters/base_code_interpreter.py +14 -0
- code_interpreters/create_code_interpreter.py +11 -0
- code_interpreters/language_map.py +19 -0
- code_interpreters/languages/__init__.py +0 -0
- code_interpreters/languages/applescript.py +64 -0
- code_interpreters/languages/html.py +21 -0
- code_interpreters/languages/javascript.py +64 -0
- code_interpreters/languages/powershell.py +68 -0
- code_interpreters/languages/python.py +157 -0
- code_interpreters/languages/r.py +67 -0
- code_interpreters/languages/shell.py +57 -0
- code_interpreters/subprocess_code_interpreter.py +138 -0
code_interpreters/__init__.py
ADDED
File without changes
|
code_interpreters/base_code_interpreter.py
ADDED
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
|
2 |
+
|
3 |
+
class BaseCodeInterpreter:
|
4 |
+
"""
|
5 |
+
.run is a generator that yields a dict with attributes: active_line, output
|
6 |
+
"""
|
7 |
+
def __init__(self):
|
8 |
+
pass
|
9 |
+
|
10 |
+
def run(self, code):
|
11 |
+
pass
|
12 |
+
|
13 |
+
def terminate(self):
|
14 |
+
pass
|
code_interpreters/create_code_interpreter.py
ADDED
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from .language_map import language_map
|
2 |
+
|
3 |
+
def create_code_interpreter(language):
|
4 |
+
# Case in-sensitive
|
5 |
+
language = language.lower()
|
6 |
+
|
7 |
+
try:
|
8 |
+
CodeInterpreter = language_map[language]
|
9 |
+
return CodeInterpreter()
|
10 |
+
except KeyError:
|
11 |
+
raise ValueError(f"Unknown or unsupported language: {language}")
|
code_interpreters/language_map.py
ADDED
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from .languages.python import Python
|
2 |
+
from .languages.shell import Shell
|
3 |
+
from .languages.javascript import JavaScript
|
4 |
+
from .languages.html import HTML
|
5 |
+
from .languages.applescript import AppleScript
|
6 |
+
from .languages.r import R
|
7 |
+
from .languages.powershell import PowerShell
|
8 |
+
|
9 |
+
|
10 |
+
language_map = {
|
11 |
+
"python": Python,
|
12 |
+
"bash": Shell,
|
13 |
+
"shell": Shell,
|
14 |
+
"javascript": JavaScript,
|
15 |
+
"html": HTML,
|
16 |
+
"applescript": AppleScript,
|
17 |
+
"r": R,
|
18 |
+
"powershell": PowerShell,
|
19 |
+
}
|
code_interpreters/languages/__init__.py
ADDED
File without changes
|
code_interpreters/languages/applescript.py
ADDED
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
from ..subprocess_code_interpreter import SubprocessCodeInterpreter
|
3 |
+
|
4 |
+
class AppleScript(SubprocessCodeInterpreter):
|
5 |
+
file_extension = "applescript"
|
6 |
+
proper_name = "AppleScript"
|
7 |
+
|
8 |
+
def __init__(self):
|
9 |
+
super().__init__()
|
10 |
+
self.start_cmd = os.environ.get('SHELL', '/bin/zsh')
|
11 |
+
|
12 |
+
def preprocess_code(self, code):
|
13 |
+
"""
|
14 |
+
Inserts an end_of_execution marker and adds active line indicators.
|
15 |
+
"""
|
16 |
+
# Add active line indicators to the code
|
17 |
+
code = self.add_active_line_indicators(code)
|
18 |
+
|
19 |
+
# Escape double quotes
|
20 |
+
code = code.replace('"', r'\"')
|
21 |
+
|
22 |
+
# Wrap in double quotes
|
23 |
+
code = '"' + code + '"'
|
24 |
+
|
25 |
+
# Prepend start command for AppleScript
|
26 |
+
code = "osascript -e " + code
|
27 |
+
|
28 |
+
# Append end of execution indicator
|
29 |
+
code += '; echo "## end_of_execution ##"'
|
30 |
+
|
31 |
+
return code
|
32 |
+
|
33 |
+
def add_active_line_indicators(self, code):
|
34 |
+
"""
|
35 |
+
Adds log commands to indicate the active line of execution in the AppleScript.
|
36 |
+
"""
|
37 |
+
modified_lines = []
|
38 |
+
lines = code.split('\n')
|
39 |
+
|
40 |
+
for idx, line in enumerate(lines):
|
41 |
+
# Add log command to indicate the line number
|
42 |
+
if line.strip(): # Only add if line is not empty
|
43 |
+
modified_lines.append(f'log "## active_line {idx + 1} ##"')
|
44 |
+
modified_lines.append(line)
|
45 |
+
|
46 |
+
return '\n'.join(modified_lines)
|
47 |
+
|
48 |
+
def detect_active_line(self, line):
|
49 |
+
"""
|
50 |
+
Detects active line indicator in the output.
|
51 |
+
"""
|
52 |
+
prefix = "## active_line "
|
53 |
+
if prefix in line:
|
54 |
+
try:
|
55 |
+
return int(line.split(prefix)[1].split()[0])
|
56 |
+
except:
|
57 |
+
pass
|
58 |
+
return None
|
59 |
+
|
60 |
+
def detect_end_of_execution(self, line):
|
61 |
+
"""
|
62 |
+
Detects end of execution marker in the output.
|
63 |
+
"""
|
64 |
+
return "## end_of_execution ##" in line
|
code_interpreters/languages/html.py
ADDED
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import webbrowser
|
2 |
+
import tempfile
|
3 |
+
import os
|
4 |
+
from ..base_code_interpreter import BaseCodeInterpreter
|
5 |
+
|
6 |
+
class HTML(BaseCodeInterpreter):
|
7 |
+
file_extension = "html"
|
8 |
+
proper_name = "HTML"
|
9 |
+
|
10 |
+
def __init__(self):
|
11 |
+
super().__init__()
|
12 |
+
|
13 |
+
def run(self, code):
|
14 |
+
# Create a temporary HTML file with the content
|
15 |
+
with tempfile.NamedTemporaryFile(delete=False, suffix=".html") as f:
|
16 |
+
f.write(code.encode())
|
17 |
+
|
18 |
+
# Open the HTML file with the default web browser
|
19 |
+
webbrowser.open('file://' + os.path.realpath(f.name))
|
20 |
+
|
21 |
+
yield {"output": f"Saved to {os.path.realpath(f.name)} and opened with the user's default web browser."}
|
code_interpreters/languages/javascript.py
ADDED
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from ..subprocess_code_interpreter import SubprocessCodeInterpreter
|
2 |
+
import re
|
3 |
+
|
4 |
+
class JavaScript(SubprocessCodeInterpreter):
|
5 |
+
file_extension = "js"
|
6 |
+
proper_name = "JavaScript"
|
7 |
+
|
8 |
+
def __init__(self):
|
9 |
+
super().__init__()
|
10 |
+
self.start_cmd = "node -i"
|
11 |
+
|
12 |
+
def preprocess_code(self, code):
|
13 |
+
return preprocess_javascript(code)
|
14 |
+
|
15 |
+
def line_postprocessor(self, line):
|
16 |
+
# Node's interactive REPL outputs a billion things
|
17 |
+
# So we clean it up:
|
18 |
+
if "Welcome to Node.js" in line:
|
19 |
+
return None
|
20 |
+
if line.strip() in ["undefined", 'Type ".help" for more information.']:
|
21 |
+
return None
|
22 |
+
# Remove trailing ">"s
|
23 |
+
line = re.sub(r'^\s*(>\s*)+', '', line)
|
24 |
+
return line
|
25 |
+
|
26 |
+
def detect_active_line(self, line):
|
27 |
+
if "## active_line " in line:
|
28 |
+
return int(line.split("## active_line ")[1].split(" ##")[0])
|
29 |
+
return None
|
30 |
+
|
31 |
+
def detect_end_of_execution(self, line):
|
32 |
+
return "## end_of_execution ##" in line
|
33 |
+
|
34 |
+
|
35 |
+
def preprocess_javascript(code):
|
36 |
+
"""
|
37 |
+
Add active line markers
|
38 |
+
Wrap in a try catch
|
39 |
+
Add end of execution marker
|
40 |
+
"""
|
41 |
+
|
42 |
+
# Split code into lines
|
43 |
+
lines = code.split("\n")
|
44 |
+
processed_lines = []
|
45 |
+
|
46 |
+
for i, line in enumerate(lines, 1):
|
47 |
+
# Add active line print
|
48 |
+
processed_lines.append(f'console.log("## active_line {i} ##");')
|
49 |
+
processed_lines.append(line)
|
50 |
+
|
51 |
+
# Join lines to form the processed code
|
52 |
+
processed_code = "\n".join(processed_lines)
|
53 |
+
|
54 |
+
# Wrap in a try-catch and add end of execution marker
|
55 |
+
processed_code = f"""
|
56 |
+
try {{
|
57 |
+
{processed_code}
|
58 |
+
}} catch (e) {{
|
59 |
+
console.log(e);
|
60 |
+
}}
|
61 |
+
console.log("## end_of_execution ##");
|
62 |
+
"""
|
63 |
+
|
64 |
+
return processed_code
|
code_interpreters/languages/powershell.py
ADDED
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import platform
|
2 |
+
import os
|
3 |
+
from ..subprocess_code_interpreter import SubprocessCodeInterpreter
|
4 |
+
|
5 |
+
class PowerShell(SubprocessCodeInterpreter):
|
6 |
+
file_extension = "ps1"
|
7 |
+
proper_name = "PowerShell"
|
8 |
+
|
9 |
+
def __init__(self):
|
10 |
+
super().__init__()
|
11 |
+
|
12 |
+
# Determine the start command based on the platform (use "powershell" for Windows)
|
13 |
+
if platform.system() == 'Windows':
|
14 |
+
self.start_cmd = 'powershell.exe'
|
15 |
+
#self.start_cmd = os.environ.get('SHELL', 'powershell.exe')
|
16 |
+
else:
|
17 |
+
self.start_cmd = os.environ.get('SHELL', 'bash')
|
18 |
+
|
19 |
+
def preprocess_code(self, code):
|
20 |
+
return preprocess_powershell(code)
|
21 |
+
|
22 |
+
def line_postprocessor(self, line):
|
23 |
+
return line
|
24 |
+
|
25 |
+
def detect_active_line(self, line):
|
26 |
+
if "## active_line " in line:
|
27 |
+
return int(line.split("## active_line ")[1].split(" ##")[0])
|
28 |
+
return None
|
29 |
+
|
30 |
+
def detect_end_of_execution(self, line):
|
31 |
+
return "## end_of_execution ##" in line
|
32 |
+
|
33 |
+
def preprocess_powershell(code):
|
34 |
+
"""
|
35 |
+
Add active line markers
|
36 |
+
Wrap in try-catch block
|
37 |
+
Add end of execution marker
|
38 |
+
"""
|
39 |
+
# Add commands that tell us what the active line is
|
40 |
+
code = add_active_line_prints(code)
|
41 |
+
|
42 |
+
# Wrap in try-catch block for error handling
|
43 |
+
code = wrap_in_try_catch(code)
|
44 |
+
|
45 |
+
# Add end marker (we'll be listening for this to know when it ends)
|
46 |
+
code += '\nWrite-Output "## end_of_execution ##"'
|
47 |
+
|
48 |
+
return code
|
49 |
+
|
50 |
+
def add_active_line_prints(code):
|
51 |
+
"""
|
52 |
+
Add Write-Output statements indicating line numbers to a PowerShell script.
|
53 |
+
"""
|
54 |
+
lines = code.split('\n')
|
55 |
+
for index, line in enumerate(lines):
|
56 |
+
# Insert the Write-Output command before the actual line
|
57 |
+
lines[index] = f'Write-Output "## active_line {index + 1} ##"\n{line}'
|
58 |
+
return '\n'.join(lines)
|
59 |
+
|
60 |
+
def wrap_in_try_catch(code):
|
61 |
+
"""
|
62 |
+
Wrap PowerShell code in a try-catch block to catch errors and display them.
|
63 |
+
"""
|
64 |
+
try_catch_code = """
|
65 |
+
try {
|
66 |
+
$ErrorActionPreference = "Stop"
|
67 |
+
"""
|
68 |
+
return try_catch_code + code + "\n} catch {\n Write-Error $_\n}\n"
|
code_interpreters/languages/python.py
ADDED
@@ -0,0 +1,157 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import sys
|
3 |
+
from ..subprocess_code_interpreter import SubprocessCodeInterpreter
|
4 |
+
import ast
|
5 |
+
import re
|
6 |
+
import shlex
|
7 |
+
|
8 |
+
class Python(SubprocessCodeInterpreter):
|
9 |
+
file_extension = "py"
|
10 |
+
proper_name = "Python"
|
11 |
+
|
12 |
+
def __init__(self):
|
13 |
+
super().__init__()
|
14 |
+
executable = sys.executable
|
15 |
+
if os.name != 'nt': # not Windows
|
16 |
+
executable = shlex.quote(executable)
|
17 |
+
self.start_cmd = executable + " -i -q -u"
|
18 |
+
|
19 |
+
def preprocess_code(self, code):
|
20 |
+
return preprocess_python(code)
|
21 |
+
|
22 |
+
def line_postprocessor(self, line):
|
23 |
+
if re.match(r'^(\s*>>>\s*|\s*\.\.\.\s*)', line):
|
24 |
+
return None
|
25 |
+
return line
|
26 |
+
|
27 |
+
def detect_active_line(self, line):
|
28 |
+
if "## active_line " in line:
|
29 |
+
return int(line.split("## active_line ")[1].split(" ##")[0])
|
30 |
+
return None
|
31 |
+
|
32 |
+
def detect_end_of_execution(self, line):
|
33 |
+
return "## end_of_execution ##" in line
|
34 |
+
|
35 |
+
|
36 |
+
def preprocess_python(code):
|
37 |
+
"""
|
38 |
+
Add active line markers
|
39 |
+
Wrap in a try except
|
40 |
+
Add end of execution marker
|
41 |
+
"""
|
42 |
+
|
43 |
+
# Add print commands that tell us what the active line is
|
44 |
+
code = add_active_line_prints(code)
|
45 |
+
|
46 |
+
# Wrap in a try except
|
47 |
+
code = wrap_in_try_except(code)
|
48 |
+
|
49 |
+
# Remove any whitespace lines, as this will break indented blocks
|
50 |
+
# (are we sure about this? test this)
|
51 |
+
code_lines = code.split("\n")
|
52 |
+
code_lines = [c for c in code_lines if c.strip() != ""]
|
53 |
+
code = "\n".join(code_lines)
|
54 |
+
|
55 |
+
# Add end command (we'll be listening for this so we know when it ends)
|
56 |
+
code += '\n\nprint("## end_of_execution ##")'
|
57 |
+
|
58 |
+
return code
|
59 |
+
|
60 |
+
|
61 |
+
def add_active_line_prints(code):
|
62 |
+
"""
|
63 |
+
Add print statements indicating line numbers to a python string.
|
64 |
+
"""
|
65 |
+
tree = ast.parse(code)
|
66 |
+
transformer = AddLinePrints()
|
67 |
+
new_tree = transformer.visit(tree)
|
68 |
+
return ast.unparse(new_tree)
|
69 |
+
|
70 |
+
|
71 |
+
class AddLinePrints(ast.NodeTransformer):
|
72 |
+
"""
|
73 |
+
Transformer to insert print statements indicating the line number
|
74 |
+
before every executable line in the AST.
|
75 |
+
"""
|
76 |
+
|
77 |
+
def insert_print_statement(self, line_number):
|
78 |
+
"""Inserts a print statement for a given line number."""
|
79 |
+
return ast.Expr(
|
80 |
+
value=ast.Call(
|
81 |
+
func=ast.Name(id='print', ctx=ast.Load()),
|
82 |
+
args=[ast.Constant(value=f"## active_line {line_number} ##")],
|
83 |
+
keywords=[]
|
84 |
+
)
|
85 |
+
)
|
86 |
+
|
87 |
+
def process_body(self, body):
|
88 |
+
"""Processes a block of statements, adding print calls."""
|
89 |
+
new_body = []
|
90 |
+
|
91 |
+
# In case it's not iterable:
|
92 |
+
if not isinstance(body, list):
|
93 |
+
body = [body]
|
94 |
+
|
95 |
+
for sub_node in body:
|
96 |
+
if hasattr(sub_node, 'lineno'):
|
97 |
+
new_body.append(self.insert_print_statement(sub_node.lineno))
|
98 |
+
new_body.append(sub_node)
|
99 |
+
|
100 |
+
return new_body
|
101 |
+
|
102 |
+
def visit(self, node):
|
103 |
+
"""Overridden visit to transform nodes."""
|
104 |
+
new_node = super().visit(node)
|
105 |
+
|
106 |
+
# If node has a body, process it
|
107 |
+
if hasattr(new_node, 'body'):
|
108 |
+
new_node.body = self.process_body(new_node.body)
|
109 |
+
|
110 |
+
# If node has an orelse block (like in for, while, if), process it
|
111 |
+
if hasattr(new_node, 'orelse') and new_node.orelse:
|
112 |
+
new_node.orelse = self.process_body(new_node.orelse)
|
113 |
+
|
114 |
+
# Special case for Try nodes as they have multiple blocks
|
115 |
+
if isinstance(new_node, ast.Try):
|
116 |
+
for handler in new_node.handlers:
|
117 |
+
handler.body = self.process_body(handler.body)
|
118 |
+
if new_node.finalbody:
|
119 |
+
new_node.finalbody = self.process_body(new_node.finalbody)
|
120 |
+
|
121 |
+
return new_node
|
122 |
+
|
123 |
+
|
124 |
+
def wrap_in_try_except(code):
|
125 |
+
# Add import traceback
|
126 |
+
code = "import traceback\n" + code
|
127 |
+
|
128 |
+
# Parse the input code into an AST
|
129 |
+
parsed_code = ast.parse(code)
|
130 |
+
|
131 |
+
# Wrap the entire code's AST in a single try-except block
|
132 |
+
try_except = ast.Try(
|
133 |
+
body=parsed_code.body,
|
134 |
+
handlers=[
|
135 |
+
ast.ExceptHandler(
|
136 |
+
type=ast.Name(id="Exception", ctx=ast.Load()),
|
137 |
+
name=None,
|
138 |
+
body=[
|
139 |
+
ast.Expr(
|
140 |
+
value=ast.Call(
|
141 |
+
func=ast.Attribute(value=ast.Name(id="traceback", ctx=ast.Load()), attr="print_exc", ctx=ast.Load()),
|
142 |
+
args=[],
|
143 |
+
keywords=[]
|
144 |
+
)
|
145 |
+
),
|
146 |
+
]
|
147 |
+
)
|
148 |
+
],
|
149 |
+
orelse=[],
|
150 |
+
finalbody=[]
|
151 |
+
)
|
152 |
+
|
153 |
+
# Assign the try-except block as the new body
|
154 |
+
parsed_code.body = [try_except]
|
155 |
+
|
156 |
+
# Convert the modified AST back to source code
|
157 |
+
return ast.unparse(parsed_code)
|
code_interpreters/languages/r.py
ADDED
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from ..subprocess_code_interpreter import SubprocessCodeInterpreter
|
2 |
+
import re
|
3 |
+
|
4 |
+
class R(SubprocessCodeInterpreter):
|
5 |
+
file_extension = "r"
|
6 |
+
proper_name = "R"
|
7 |
+
|
8 |
+
def __init__(self):
|
9 |
+
super().__init__()
|
10 |
+
self.start_cmd = "R -q --vanilla" # Start R in quiet and vanilla mode
|
11 |
+
|
12 |
+
def preprocess_code(self, code):
|
13 |
+
"""
|
14 |
+
Add active line markers
|
15 |
+
Wrap in a tryCatch for better error handling in R
|
16 |
+
Add end of execution marker
|
17 |
+
"""
|
18 |
+
|
19 |
+
lines = code.split("\n")
|
20 |
+
processed_lines = []
|
21 |
+
|
22 |
+
for i, line in enumerate(lines, 1):
|
23 |
+
# Add active line print
|
24 |
+
processed_lines.append(f'cat("## active_line {i} ##\\n");{line}')
|
25 |
+
|
26 |
+
# Join lines to form the processed code
|
27 |
+
processed_code = "\n".join(processed_lines)
|
28 |
+
|
29 |
+
# Wrap in a tryCatch for error handling and add end of execution marker
|
30 |
+
processed_code = f"""
|
31 |
+
tryCatch({{
|
32 |
+
{processed_code}
|
33 |
+
}}, error=function(e){{
|
34 |
+
cat("## execution_error ##\\n", conditionMessage(e), "\\n");
|
35 |
+
}})
|
36 |
+
cat("## end_of_execution ##\\n");
|
37 |
+
"""
|
38 |
+
# Count the number of lines of processed_code
|
39 |
+
# (R echoes all code back for some reason, but we can skip it if we track this!)
|
40 |
+
self.code_line_count = len(processed_code.split("\n")) - 1
|
41 |
+
|
42 |
+
return processed_code
|
43 |
+
|
44 |
+
def line_postprocessor(self, line):
|
45 |
+
# If the line count attribute is set and non-zero, decrement and skip the line
|
46 |
+
if hasattr(self, "code_line_count") and self.code_line_count > 0:
|
47 |
+
self.code_line_count -= 1
|
48 |
+
return None
|
49 |
+
|
50 |
+
if re.match(r'^(\s*>>>\s*|\s*\.\.\.\s*|\s*>\s*|\s*\+\s*|\s*)$', line):
|
51 |
+
return None
|
52 |
+
if "R version" in line: # Startup message
|
53 |
+
return None
|
54 |
+
if line.strip().startswith("[1] \"") and line.endswith("\""): # For strings, trim quotation marks
|
55 |
+
return line[5:-1].strip()
|
56 |
+
if line.strip().startswith("[1]"): # Normal R output prefix for non-string outputs
|
57 |
+
return line[4:].strip()
|
58 |
+
|
59 |
+
return line
|
60 |
+
|
61 |
+
def detect_active_line(self, line):
|
62 |
+
if "## active_line " in line:
|
63 |
+
return int(line.split("## active_line ")[1].split(" ##")[0])
|
64 |
+
return None
|
65 |
+
|
66 |
+
def detect_end_of_execution(self, line):
|
67 |
+
return "## end_of_execution ##" in line or "## execution_error ##" in line
|
code_interpreters/languages/shell.py
ADDED
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import platform
|
2 |
+
from ..subprocess_code_interpreter import SubprocessCodeInterpreter
|
3 |
+
import os
|
4 |
+
|
5 |
+
class Shell(SubprocessCodeInterpreter):
|
6 |
+
file_extension = "sh"
|
7 |
+
proper_name = "Shell"
|
8 |
+
|
9 |
+
def __init__(self):
|
10 |
+
super().__init__()
|
11 |
+
|
12 |
+
# Determine the start command based on the platform
|
13 |
+
if platform.system() == 'Windows':
|
14 |
+
self.start_cmd = 'cmd.exe'
|
15 |
+
else:
|
16 |
+
self.start_cmd = os.environ.get('SHELL', 'bash')
|
17 |
+
|
18 |
+
def preprocess_code(self, code):
|
19 |
+
return preprocess_shell(code)
|
20 |
+
|
21 |
+
def line_postprocessor(self, line):
|
22 |
+
return line
|
23 |
+
|
24 |
+
def detect_active_line(self, line):
|
25 |
+
if "## active_line " in line:
|
26 |
+
return int(line.split("## active_line ")[1].split(" ##")[0])
|
27 |
+
return None
|
28 |
+
|
29 |
+
def detect_end_of_execution(self, line):
|
30 |
+
return "## end_of_execution ##" in line
|
31 |
+
|
32 |
+
|
33 |
+
def preprocess_shell(code):
|
34 |
+
"""
|
35 |
+
Add active line markers
|
36 |
+
Wrap in a try except (trap in shell)
|
37 |
+
Add end of execution marker
|
38 |
+
"""
|
39 |
+
|
40 |
+
# Add commands that tell us what the active line is
|
41 |
+
code = add_active_line_prints(code)
|
42 |
+
|
43 |
+
# Add end command (we'll be listening for this so we know when it ends)
|
44 |
+
code += '\necho "## end_of_execution ##"'
|
45 |
+
|
46 |
+
return code
|
47 |
+
|
48 |
+
|
49 |
+
def add_active_line_prints(code):
|
50 |
+
"""
|
51 |
+
Add echo statements indicating line numbers to a shell string.
|
52 |
+
"""
|
53 |
+
lines = code.split('\n')
|
54 |
+
for index, line in enumerate(lines):
|
55 |
+
# Insert the echo command before the actual line
|
56 |
+
lines[index] = f'echo "## active_line {index + 1} ##"\n{line}'
|
57 |
+
return '\n'.join(lines)
|
code_interpreters/subprocess_code_interpreter.py
ADDED
@@ -0,0 +1,138 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
|
2 |
+
|
3 |
+
import subprocess
|
4 |
+
import threading
|
5 |
+
import queue
|
6 |
+
import time
|
7 |
+
import traceback
|
8 |
+
from .base_code_interpreter import BaseCodeInterpreter
|
9 |
+
|
10 |
+
class SubprocessCodeInterpreter(BaseCodeInterpreter):
|
11 |
+
def __init__(self):
|
12 |
+
self.start_cmd = ""
|
13 |
+
self.process = None
|
14 |
+
self.debug_mode = False
|
15 |
+
self.output_queue = queue.Queue()
|
16 |
+
self.done = threading.Event()
|
17 |
+
|
18 |
+
def detect_active_line(self, line):
|
19 |
+
return None
|
20 |
+
|
21 |
+
def detect_end_of_execution(self, line):
|
22 |
+
return None
|
23 |
+
|
24 |
+
def line_postprocessor(self, line):
|
25 |
+
return line
|
26 |
+
|
27 |
+
def preprocess_code(self, code):
|
28 |
+
"""
|
29 |
+
This needs to insert an end_of_execution marker of some kind,
|
30 |
+
which can be detected by detect_end_of_execution.
|
31 |
+
|
32 |
+
Optionally, add active line markers for detect_active_line.
|
33 |
+
"""
|
34 |
+
return code
|
35 |
+
|
36 |
+
def terminate(self):
|
37 |
+
self.process.terminate()
|
38 |
+
|
39 |
+
def start_process(self):
|
40 |
+
if self.process:
|
41 |
+
self.terminate()
|
42 |
+
|
43 |
+
self.process = subprocess.Popen(self.start_cmd.split(),
|
44 |
+
stdin=subprocess.PIPE,
|
45 |
+
stdout=subprocess.PIPE,
|
46 |
+
stderr=subprocess.PIPE,
|
47 |
+
text=True,
|
48 |
+
bufsize=0,
|
49 |
+
universal_newlines=True)
|
50 |
+
threading.Thread(target=self.handle_stream_output,
|
51 |
+
args=(self.process.stdout, False),
|
52 |
+
daemon=True).start()
|
53 |
+
threading.Thread(target=self.handle_stream_output,
|
54 |
+
args=(self.process.stderr, True),
|
55 |
+
daemon=True).start()
|
56 |
+
|
57 |
+
def run(self, code):
|
58 |
+
retry_count = 0
|
59 |
+
max_retries = 3
|
60 |
+
|
61 |
+
# Setup
|
62 |
+
try:
|
63 |
+
code = self.preprocess_code(code)
|
64 |
+
if not self.process:
|
65 |
+
self.start_process()
|
66 |
+
except:
|
67 |
+
yield {"output": traceback.format_exc()}
|
68 |
+
return
|
69 |
+
|
70 |
+
|
71 |
+
while retry_count <= max_retries:
|
72 |
+
if self.debug_mode:
|
73 |
+
print(f"Running code:\n{code}\n---")
|
74 |
+
|
75 |
+
self.done.clear()
|
76 |
+
|
77 |
+
try:
|
78 |
+
self.process.stdin.write(code + "\n")
|
79 |
+
self.process.stdin.flush()
|
80 |
+
break
|
81 |
+
except:
|
82 |
+
if retry_count != 0:
|
83 |
+
# For UX, I like to hide this if it happens once. Obviously feels better to not see errors
|
84 |
+
# Most of the time it doesn't matter, but we should figure out why it happens frequently with:
|
85 |
+
# applescript
|
86 |
+
yield {"output": traceback.format_exc()}
|
87 |
+
yield {"output": f"Retrying... ({retry_count}/{max_retries})"}
|
88 |
+
yield {"output": "Restarting process."}
|
89 |
+
|
90 |
+
self.start_process()
|
91 |
+
|
92 |
+
retry_count += 1
|
93 |
+
if retry_count > max_retries:
|
94 |
+
yield {"output": "Maximum retries reached. Could not execute code."}
|
95 |
+
return
|
96 |
+
|
97 |
+
while True:
|
98 |
+
if not self.output_queue.empty():
|
99 |
+
yield self.output_queue.get()
|
100 |
+
else:
|
101 |
+
time.sleep(0.1)
|
102 |
+
try:
|
103 |
+
output = self.output_queue.get(timeout=0.3) # Waits for 0.3 seconds
|
104 |
+
yield output
|
105 |
+
except queue.Empty:
|
106 |
+
if self.done.is_set():
|
107 |
+
# Try to yank 3 more times from it... maybe there's something in there...
|
108 |
+
# (I don't know if this actually helps. Maybe we just need to yank 1 more time)
|
109 |
+
for _ in range(3):
|
110 |
+
if not self.output_queue.empty():
|
111 |
+
yield self.output_queue.get()
|
112 |
+
time.sleep(0.2)
|
113 |
+
break
|
114 |
+
|
115 |
+
def handle_stream_output(self, stream, is_error_stream):
|
116 |
+
for line in iter(stream.readline, ''):
|
117 |
+
if self.debug_mode:
|
118 |
+
print(f"Received output line:\n{line}\n---")
|
119 |
+
|
120 |
+
line = self.line_postprocessor(line)
|
121 |
+
|
122 |
+
if line is None:
|
123 |
+
continue # `line = None` is the postprocessor's signal to discard completely
|
124 |
+
|
125 |
+
if self.detect_active_line(line):
|
126 |
+
active_line = self.detect_active_line(line)
|
127 |
+
self.output_queue.put({"active_line": active_line})
|
128 |
+
elif self.detect_end_of_execution(line):
|
129 |
+
self.output_queue.put({"active_line": None})
|
130 |
+
time.sleep(0.1)
|
131 |
+
self.done.set()
|
132 |
+
elif is_error_stream and "KeyboardInterrupt" in line:
|
133 |
+
self.output_queue.put({"output": "KeyboardInterrupt"})
|
134 |
+
time.sleep(0.1)
|
135 |
+
self.done.set()
|
136 |
+
else:
|
137 |
+
self.output_queue.put({"output": line})
|
138 |
+
|