Skip to content

Commit

Permalink
Update TooServer & Fixs summarize
Browse files Browse the repository at this point in the history
  • Loading branch information
luyaxi committed Oct 20, 2023
1 parent 96f26b3 commit 5e9b716
Show file tree
Hide file tree
Showing 16 changed files with 202 additions and 126 deletions.
11 changes: 6 additions & 5 deletions ToolServer/ToolServerManager/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import asyncio
import traceback
import datetime
import subprocess
import docker.types

from fastapi import FastAPI, Cookie,Request,HTTPException,Response
from fastapi.responses import JSONResponse,RedirectResponse
Expand Down Expand Up @@ -98,8 +98,9 @@ async def read_cookie_info():
response.headers["Server"] = "ToolServerManager/" + CONFIG['version']

# create a docker container
container = docker_client.containers.run(CONFIG['node']['image'],detach=True,network=CONFIG['node']['network'],
privileged=CONFIG['node']['privileged'],)
container = docker_client.containers.run(
device_requests=[docker.types.DeviceRequest(**req) for req in CONFIG['node']['device_requests']] if CONFIG['node']['device_requests'] else None,
**(CONFIG['node']['creation_kwargs']),)
logger.info("Node created: " + container.id)
response.set_cookie(key="node_id", value=container.id)
container.reload()
Expand All @@ -110,7 +111,7 @@ async def read_cookie_info():
(container.id,
container.short_id,
container.attrs["State"]["Status"],
container.attrs["NetworkSettings"]["Networks"][CONFIG['node']['network']]["IPAddress"],
container.attrs["NetworkSettings"]["Networks"][CONFIG['node']['creation_kwargs']['network']]["IPAddress"],
datetime.datetime.utcnow().isoformat()),
container.attrs['State']['Health']['Status'])
db.commit()
Expand All @@ -120,7 +121,7 @@ async def read_cookie_info():
'node_id':container.id,
'node_short_id':container.short_id,
'node_status':container.attrs["State"]["Status"],
'node_ip':container.attrs["NetworkSettings"]["Networks"][CONFIG['node']['network']]["IPAddress"],
'node_ip':container.attrs["NetworkSettings"]["Networks"][CONFIG['node']['creation_kwargs']['network']]["IPAddress"],
'node_last_req_time':datetime.datetime.utcnow().isoformat(),
'node_health':container.attrs['State']['Health']['Status']
})
Expand Down
158 changes: 86 additions & 72 deletions ToolServer/ToolServerNode/core/envs/filesystem.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import os
import fnmatch
from typing import Any, Dict
from collections import defaultdict

from core.register import toolwrapper
from core.base import BaseEnv
Expand All @@ -15,7 +16,7 @@ def __init__(self, config: Dict[str, Any] = None):

self.ignored_list = filesystem_config["ignored_list"]
self.work_directory = filesystem_config["work_directory"]

self.max_entry_nums_for_level = filesystem_config["max_entry_nums_for_level"]
if not os.path.exists(self.work_directory):
os.mkdir(self.work_directory,mode=0o777)

Expand All @@ -42,7 +43,7 @@ def _is_path_exist(self,path:str)->bool:
raise ValueError(f"Path {path} is not within workspace.")
return os.path.exists(full_path)

def print_filesys_struture(self,recursive=True,return_root=False)->str:
def print_filesys_struture(self,return_root=False)->str:
"""Return a tree-like structure for all files and folders in the workspace. Use this tool if you are not sure what files are in the workspace.
This function recursively walks through all the directories in the workspace
Expand All @@ -65,40 +66,48 @@ def print_filesys_struture(self,recursive=True,return_root=False)->str:
if return_root:
full_repr += f'Global Root Work Directory: {self.work_directory}\n'

if recursive:
for root, dirs, files in os.walk(self.work_directory):
if self._check_ignorement(root):
continue
level = root.replace(self.work_directory, '').count(os.sep)
indent = ' ' * 4 * (level)
full_repr += f'{indent}{os.path.basename(root)}/\n'
subindent = ' ' * 4 * (level + 1)
for f in files:
if self._check_ignorement(f):
continue
full_repr += f'{subindent}{f}\n'
else:
for file in os.listdir(self.work_directory):
if self._check_ignorement(file):
folder_counts = defaultdict(lambda: 0)
for root, dirs, files in os.walk(self.work_directory):
if self._check_ignorement(root):
continue
level = root.replace(self.work_directory, '').count(os.sep)
indent = ' ' * 4 * (level)

folder_counts[root] += 1
if folder_counts[root] > self.max_entry_nums_for_level:
full_repr += f'{indent}`wrapped`\n'

full_repr += f'{indent}- {os.path.basename(root)}/\n'

idx = 0
subindent = ' ' * 4 * (level + 1) + '- '
for f in files:
if self._check_ignorement(f):
continue
indent = ' ' * 4
if os.path.isdir(file):
full_repr += f'{indent}{file}/*wrapped*\n'
else:
full_repr += f'{indent}{file}\n'

idx += 1
if idx > self.max_entry_nums_for_level:
full_repr += f'{subindent}`wrapped`\n'
break
full_repr += f'{subindent}{f}\n'


return full_repr

def read_from_file(self,filepath:str,start_index:int = 0)->str:
def read_from_file(self,filepath:str,line_number:int = 1)->str:
"""Open and read the textual file content in the workspace, you will see the content of the target file.
Don't use this if the give `filepath` is writen or modified before, the content in `filepath` should be already returned.
:param string filepath: The path to the file to be opened, always use relative path to the workspace root.
:param integer? start_index: The starting line number of the content to be opened. Defaults to 0.
:param integer? line_number: The starting line number of the content to be opened. Defaults to 1.
:return string: The content of the file.
"""
if not filepath.startswith(self.work_directory):
filepath = filepath.strip('/')
full_path = os.path.join(self.work_directory, filepath)
full_path = os.path.join(self.work_directory, filepath)
else:
full_path = filepath

if self._check_ignorement(full_path) or not os.path.isfile(full_path):
raise FileNotFoundError(f"File {filepath} not found in workspace.")
if not self._is_path_within_workspace(full_path):
Expand All @@ -108,78 +117,83 @@ def read_from_file(self,filepath:str,start_index:int = 0)->str:

content = ''
with open(full_path, 'r') as f:
lines = f.readlines(int(1e6))
lines = f.readlines(int(1e5))

read_count = 0
index = start_index if start_index >= 0 else len(lines) + start_index
indexed_lines = lines[start_index:]
if not (abs(line_number) - 1 < len(lines)):
raise ValueError(f"Line number {line_number} is out of range.")
index = line_number if line_number >= 0 else len(lines) + line_number
if index == 0:
index = 1

if line_number == 0:
indexed_lines = lines
elif line_number > 0:
indexed_lines = lines[line_number-1:]
else:
indexed_lines = lines[line_number:]

for line in indexed_lines:
content += f'{index}'.rjust(5) + ': '
content += line
read_count += len(line)
index += 1
return content

def write_to_file(self, filepath:str, content:str):
"""Write the textual file in the workspace with the content provided.
Will automatically create the file if it does not exist. Also overwrite the file content if it already exists. If you want to append content to the file, use `modify_file` instead.
Better check if the file exists before directly writing to it.
Return content of the file after writing.
:param string filepath: The path to the file to be saved, always use relative path to the workspace root.
:param string content: The content to be saved.
"""
if not filepath.startswith(self.work_directory):
filepath = filepath.strip('/')
full_path = os.path.join(self.work_directory, filepath)

# if not os.path.isfile(full_path):
# raise FileNotFoundError(f"File {filepath} not found in workspace.")

if not self._is_path_within_workspace(full_path):
raise ValueError(f"File {filepath} is not within workspace.")

os.makedirs(os.path.split(full_path)[0],exist_ok=True)
with open(full_path, 'w') as f:
f.write(content)

return self.read_from_file(filepath)

def modify_file(self, filepath:str,new_content:str, start_index:int = None, end_index:int = None)->str:
"""Modify the textual file lines in slice [start_index:end_index] based on `new_content` provided. Return content of the file after modification so no further need to call `read_from_file`.
filepath_content_lines[start_index:end_index] = new_content
def write_to_file(self, filepath:str,content:str,truncating:bool = False,line_number:int = None, overwrite:bool = False)->str:
"""Write or modify the textual file lines based on `content` provided.
Return updated content of the file after modification so no further need to call `read_from_file` for this file. Create file if not exists.
Example:
```
In[0]: modify_file('test.txt', 'Hello World!') # This will insert a new line `Hello World!` at the end of the file `test.txt`.
In[1]: modify_file('test.txt', 'Hello World!', 0) # This will insert a new line `Hello World!` at the begin of the file `test.txt`.
In[2]: modify_file('test.txt', 'Hello World!', 0, 1) # This will replace the first line of the file `test.txt` with `Hello World!`.
In[0]: write_to_file('test.txt', 'Hello World!\\nA new line!')
Out[0]: '1: Hello World!\\n2: A new line!'
In[1]: write_to_file('test.txt', 'Hello World 1!', 2)
Out[1]: '1: Hello World!\\n2: Hello World 1!\\n3: A new line!'
In[2]: write_to_file('test.txt', 'Hello World 2!', 2, overwrite=True)
Out[2]: '1: Hello World!\\n2: Hello World 2!\\n3: A new line!'
```
:param string filepath: The path to the file to be modified, always use relative path to the workspace root.
:param string new_content: The new content to be replaced with the old content.
:param integer? start_index: The start position in slice to modified file lines. Defaults to `None`, which means insert the new content at the end of the file. So do not provide this if you want to append the new content to the file.
:param integer? end_index: The end posistion in slice to modified file lines. Defaults to the value of `start_index`, which means if `start_index` provided, insert the new content at the `start_index` line.
:param boolean? truncating: If `True`, the file will be truncated before writing, else will read current content before writing. Defaults to `False`.
:param integer? line_number: The start line to modified file. Defaults to `None`, which means insert the new content at the end of the file. So do not provide this if you want to append the new content to the file.
:param boolean? overwrite: If `True`, the new content will overwrite content started from `line_number` line. Defaults to `False`, which insert the new content at the `line_number` line.
:param string content: The new content to be replaced with the old content.
"""
if not filepath.startswith(self.work_directory):
filepath = filepath.strip('/')
full_path = os.path.join(self.work_directory, filepath)
if self._check_ignorement(full_path) or not os.path.isfile(full_path) and not ((start_index is None or start_index==0)and end_index is None):
raise FileNotFoundError(f"File {filepath} not found in workspace.")

if not self._is_path_within_workspace(full_path):
if not self._is_path_within_workspace(full_path) or self._check_ignorement(full_path):
raise ValueError(f"File {filepath} is not within workspace.")

if not os.path.exists(full_path):
if line_number is None or line_number==0 or line_number == 1:
os.makedirs(os.path.split(full_path)[0],exist_ok=True)
open(full_path, 'w+').close()
else:
raise FileNotFoundError(f"File {filepath} not found in workspace.")
elif not os.path.isfile(full_path):
raise ValueError(f"File {filepath} is not a file.")

# protential overflow
with open(full_path, 'r') as f:
lines = f.readlines()
if truncating:
lines = []
else:
with open(full_path, 'r') as f:
lines = f.readlines()


if start_index is None:
lines.extend(new_content.splitlines(keepends=True))
new_lines = content.splitlines(keepends=True)
if line_number is None:
lines.extend(new_lines)
else:
if end_index is None:
end_index = start_index
lines[start_index: end_index] = new_content.splitlines(keepends=True)
if line_number >= 1:
line_number -= 1
if overwrite:
lines[line_number: line_number+len(new_lines)] = new_lines
else:
lines[line_number: line_number] = new_lines

for idx, _ in enumerate(lines):
if not lines[idx].endswith('\n'):
Expand Down
12 changes: 7 additions & 5 deletions ToolServer/ToolServerNode/core/envs/pycoding.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from typing import Dict,Any
from nbclient import NotebookClient
from nbclient.exceptions import CellExecutionError
from nbclient.exceptions import CellExecutionError,DeadKernelError
from nbclient.client import ensure_async

from core.register import toolwrapper
Expand All @@ -29,7 +29,7 @@ def __init__(self, config: Dict[str, Any] = None):
# make a new notebook
self.nb = nbformat.v4.new_notebook(
metadata = {'kernelspec': {'name': 'python', 'language': 'python', 'display_name': 'python'}})
self.nbc = NotebookClient(self.nb)
self.nbc = NotebookClient(self.nb,timeout=self.nb_cfg['timeout'])

async def _running(self):
if self.nbc.kc is not None:
Expand Down Expand Up @@ -86,15 +86,17 @@ async def execute_cell(self,code:str,cell_index:int=None,reset:bool=False) -> st
self.nb.cells[cell_index] = nbformat.v4.new_code_cell(code)

try:
await asyncio.wait_for(self.nbc.async_execute_cell(self.nb.cells[-1],len(self.nb.cells)-1),self.nb_cfg['timeout'])
await self.nbc.async_execute_cell(self.nb.cells[-1],len(self.nb.cells)-1)
except CellExecutionError as e:
pass

except DeadKernelError as e:
await self._reset()

nbformat.write(self.nb,os.path.join(self.work_directory,self.nb_cfg['save_name']))

return self._format_outputs(self.nb.cells[cell_index].outputs,cell_index,reraise=True,return_binary=True)

def print_cells_outputs(self)->str:
def print_notebook(self)->str:
"""print all notebook cells' content and output.
:return string: all notebook cells description.
Expand Down
15 changes: 12 additions & 3 deletions ToolServer/config/manager.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,20 @@ builtin_monitor: false

node:
creation_wait_seconds: 30
image: "tool-server-node:0.2"
network: "tool-server-network"
idling_close_minutes: 30
health_check: true
privileged: true
creation_kwargs:
image: "tool-server-node:0.2"
network: "tool-server-network"
privileged: true
detach: true
device_requests:
# - driver:
# count: 1
# capabilities:
# -
# - gpu


redirect_to_node_path:
post:
Expand Down
3 changes: 2 additions & 1 deletion ToolServer/config/node.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,10 @@ filesystem:
- "*/.vs/*"
- "*/python*/site-packages/*"
work_directory: /app/workspace/
max_entry_nums_for_level: 20

notebook:
timeout: 60
timeout: 300
save_name: python_notebook.ipynb

rapidapi:
Expand Down
10 changes: 3 additions & 7 deletions ToolServer/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@ services:
DB_USERNAME: ${DB_USERNAME}
DB_PASSWORD: ${DB_PASSWORD}
DB_COLLECTION: ${DB_COLLECTION}
networks:
- tool-server-network
depends_on:
- db
command: ["--workers","8","-t","600"]
Expand All @@ -27,6 +25,8 @@ services:
build:
context: .
dockerfile: ./dockerfiles/ToolServerNode/Dockerfile
entrypoint: /bin/sh -c 'echo "Build complete. Exiting..." && exit 0'
command: echo "Build complete. Exiting..."

ToolServerMonitor:
image: tool-server-monitor:0.1
Expand All @@ -41,8 +41,6 @@ services:
DB_USERNAME: ${DB_USERNAME}
DB_PASSWORD: ${DB_PASSWORD}
DB_COLLECTION: ${DB_COLLECTION}
networks:
- tool-server-network
depends_on:
- db

Expand All @@ -54,15 +52,13 @@ services:
environment:
MONGO_INITDB_ROOT_USERNAME: ${DB_USERNAME}
MONGO_INITDB_ROOT_PASSWORD: ${DB_PASSWORD}
networks:
- tool-server-network
logging:
driver: "none"

volumes:
xagentmongodb:

networks:
tool-server-network:
default:
name: tool-server-network
driver: bridge
Loading

0 comments on commit 5e9b716

Please sign in to comment.