224 lines
7.3 KiB
Python
224 lines
7.3 KiB
Python
# this env is designed for workflow owner filesystem, support file/directory operations
|
|
|
|
import json
|
|
import subprocess
|
|
import tempfile
|
|
import threading
|
|
import traceback
|
|
import time
|
|
import ast
|
|
import sys
|
|
import os
|
|
import re
|
|
import asyncio
|
|
import aiofiles.os
|
|
import chardet
|
|
|
|
from .environment import Environment,EnvironmentEvent
|
|
from .ai_function import AIFunction,SimpleAIFunction
|
|
|
|
|
|
class CodeInterpreter:
|
|
def __init__(self, language, debug_mode):
|
|
self.language = language
|
|
self.proc = None
|
|
self.active_line = None
|
|
self.debug_mode = debug_mode
|
|
|
|
def start_process(self):
|
|
start_cmd = sys.executable + " -i -q -u"
|
|
self.proc = subprocess.Popen(start_cmd.split(),
|
|
stdin=subprocess.PIPE,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
text=True,
|
|
bufsize=0)
|
|
|
|
# Start watching ^ its `stdout` and `stderr` streams
|
|
threading.Thread(target=self.save_and_display_stream,
|
|
args=(self.proc.stdout, False), # Passes False to is_error_stream
|
|
daemon=True).start()
|
|
threading.Thread(target=self.save_and_display_stream,
|
|
args=(self.proc.stderr, True), # Passes True to is_error_stream
|
|
daemon=True).start()
|
|
|
|
def warp_code(self,pycode:str)->str:
|
|
# Add import traceback
|
|
code = "import traceback\n" + pycode
|
|
# Parse the input code into an AST
|
|
parsed_code = ast.parse(code)
|
|
# Wrap the entire code's AST in a single try-except block
|
|
try_except = ast.Try(
|
|
body=parsed_code.body,
|
|
handlers=[
|
|
ast.ExceptHandler(
|
|
type=ast.Name(id="Exception", ctx=ast.Load()),
|
|
name=None,
|
|
body=[
|
|
ast.Expr(
|
|
value=ast.Call(
|
|
func=ast.Attribute(value=ast.Name(id="traceback", ctx=ast.Load()), attr="print_exc", ctx=ast.Load()),
|
|
args=[],
|
|
keywords=[]
|
|
)
|
|
),
|
|
]
|
|
)
|
|
],
|
|
orelse=[],
|
|
finalbody=[]
|
|
)
|
|
|
|
parsed_code.body = [try_except]
|
|
return ast.unparse(parsed_code)
|
|
|
|
def run(self,py_code:str):
|
|
"""
|
|
Executes code.
|
|
"""
|
|
# Get code to execute
|
|
self.code = py_code
|
|
|
|
# Start the subprocess if it hasn't been started
|
|
if not self.proc:
|
|
try:
|
|
self.start_process()
|
|
except Exception as e:
|
|
# Sometimes start_process will fail!
|
|
# Like if they don't have `node` installed or something.
|
|
|
|
traceback_string = traceback.format_exc()
|
|
self.output = traceback_string
|
|
# Before you return, wait for the display to catch up?
|
|
# (I'm not sure why this works)
|
|
time.sleep(0.1)
|
|
|
|
return self.output
|
|
|
|
self.output = ""
|
|
|
|
self.print_cmd = 'print("{}")'
|
|
code = self.warp_code(py_code)
|
|
|
|
if self.debug_mode:
|
|
print("Running code:")
|
|
print(code)
|
|
print("---")
|
|
|
|
self.done = threading.Event()
|
|
self.done.clear()
|
|
|
|
# Write code to stdin of the process
|
|
try:
|
|
self.proc.stdin.write(code + "\n")
|
|
self.proc.stdin.flush()
|
|
except BrokenPipeError:
|
|
return
|
|
self.done.wait()
|
|
time.sleep(0.1)
|
|
return self.output
|
|
|
|
def save_and_display_stream(self, stream, is_error_stream):
|
|
|
|
for line in iter(stream.readline, ''):
|
|
if self.debug_mode:
|
|
print("Recieved output line:")
|
|
print(line)
|
|
print("---")
|
|
|
|
line = line.strip()
|
|
if is_error_stream and "KeyboardInterrupt" in line:
|
|
raise KeyboardInterrupt
|
|
elif "END_OF_EXECUTION" in line:
|
|
self.done.set()
|
|
self.active_line = None
|
|
else:
|
|
self.output += "\n" + line
|
|
self.output = self.output.strip()
|
|
|
|
|
|
|
|
class WorkspaceEnvironment(Environment):
|
|
def __init__(self, env_id: str) -> None:
|
|
super().__init__(env_id)
|
|
|
|
operator_param = {
|
|
"command": "command will execute",
|
|
}
|
|
self.add_ai_function(SimpleAIFunction("shell_exec",
|
|
"execute shell command in linux bash",
|
|
self.shell_exec,operator_param))
|
|
|
|
#run_code_param = {
|
|
# "pycode": "python code will execute",
|
|
#}
|
|
#self.add_ai_function(SimpleAIFunction("run_code",
|
|
# "execute python code",
|
|
# self.run_code,run_code_param))
|
|
|
|
|
|
async def shell_exec(self,command:str) -> str:
|
|
import asyncio.subprocess
|
|
process = await asyncio.create_subprocess_shell(
|
|
command,
|
|
stdout=asyncio.subprocess.PIPE,
|
|
stderr=asyncio.subprocess.PIPE
|
|
)
|
|
stdout, stderr = await process.communicate()
|
|
returncode = process.returncode
|
|
if returncode == 0:
|
|
return f"Execute success! stdout is:\n{stdout}\n"
|
|
else:
|
|
return f"Execute failed! stderr is:\n{stderr}\n"
|
|
|
|
async def run_code(self,pycode:str) -> str:
|
|
interpreter = CodeInterpreter("python",True)
|
|
return interpreter.run(pycode)
|
|
|
|
|
|
|
|
class KnowledgeBaseFileSystemEnvironment(Environment):
|
|
def __init__(self, env_id: str) -> None:
|
|
super().__init__(env_id)
|
|
self.root_path = "."
|
|
|
|
operator_param = {
|
|
"path": "full path of target directory",
|
|
}
|
|
self.add_ai_function(SimpleAIFunction("list",
|
|
"list the files and sub directory in target directory,result is a json array",
|
|
self.list,operator_param))
|
|
|
|
operator_param = {
|
|
"path": "full path of target file",
|
|
}
|
|
self.add_ai_function(SimpleAIFunction("cat",
|
|
"cat the file content in target path,result is a string",
|
|
self.cat,operator_param))
|
|
|
|
def set_root_path(self,path:str):
|
|
self.root_path = path
|
|
|
|
|
|
async def list(self,path:str) -> str:
|
|
directory_path = self.root_path + path
|
|
items = []
|
|
|
|
with await aiofiles.os.scandir(directory_path) as entries:
|
|
async for entry in entries:
|
|
item_type = "directory" if entry.is_dir() else "file"
|
|
items.append({"name": entry.name, "type": item_type})
|
|
|
|
return json.dumps(items)
|
|
|
|
async def cat(self,path:str) -> str:
|
|
file_path = self.root_path + path
|
|
cur_encode = "utf-8"
|
|
async with aiofiles.open(file_path,'rb') as f:
|
|
cur_encode = chardet.detect(await f.read())['encoding']
|
|
|
|
async with aiofiles.open(file_path, mode='r', encoding=cur_encode) as f:
|
|
content = await f.read(2048)
|
|
return content
|
|
|