Tachi67 commited on
Commit
fcdf91d
·
1 Parent(s): 3f6b5c2

add code interpreter from open-interpreter

Browse files
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
+