Unify linter behaviour across CI and pre-commit-hook (#1071)

* CI: Add autopep8 linter

Currently, we have autopep8 as part of pre-commit-hook. To ensure
consistent behaviour, we should have it in CI as well.

Moreover, pre-commit-hook contains a double-quote-string-fixer hook
which changes all double quotes to single quotes, but I do observe
some PRs with massive changes that do the opposite way. I suspect
that these authors 1) disable or circumvent the pre-commit-hook,
and 2) have other linters such as black in their IDE, which
automatically change all single quotes to double quotes. This
has caused a lot of unnecessary diff, made review really hard,
and led to a lot of conflicts.

* Use -diff for autopep8

* autopep8: Freeze version in CI

* Ultimate fix

* Remove pep8 long line disable workaround

* Fix lint.yml

* Fix all files under opendevin and agenthub
This commit is contained in:
Boxuan Li
2024-04-13 21:19:56 -07:00
committed by GitHub
parent d4ce4ea541
commit dd32fa6f4a
42 changed files with 208 additions and 214 deletions

View File

@@ -32,11 +32,7 @@ jobs:
uses: actions/setup-python@v5
with:
python-version: 3.11
- name: Create mypy cache directory
run: mkdir -p .mypy_cache
- name: Install dependencies
run: pip install ruff mypy==1.9.0 types-PyYAML types-toml
- name: Run mypy
run: python -m mypy --install-types --non-interactive --config-file dev_config/python/mypy.ini opendevin/ agenthub/
- name: Run ruff
run: ruff check --config dev_config/python/ruff.toml opendevin/ agenthub/
- name: Install pre-commit
run: pip install pre-commit
- name: Run pre-commit hooks
run: pre-commit run --files opendevin/**/* agenthub/**/* --show-diff-on-failure --config ./dev_config/python/.pre-commit-config.yaml

View File

@@ -2,8 +2,8 @@ from dotenv import load_dotenv
load_dotenv()
# Import agents after environment variables are loaded
from . import monologue_agent # noqa: E402
from . import codeact_agent # noqa: E402
from . import planner_agent # noqa: E402
from . import monologue_agent # noqa: E402
from . import codeact_agent # noqa: E402
from . import planner_agent # noqa: E402
__all__ = ['monologue_agent', 'codeact_agent', 'planner_agent']

View File

@@ -1,6 +1,6 @@
# CodeAct-based Agent Framework
This folder implements the [CodeAct idea](https://arxiv.org/abs/2402.13463) that relies on LLM to autonomously perform actions in a Bash shell. It requires more from the LLM itself: LLM needs to be capable enough to do all the stuff autonomously, instead of stuck in an infinite loop.
This folder implements the [CodeAct idea](https://arxiv.org/abs/2402.13463) that relies on LLM to autonomously perform actions in a Bash shell. It requires more from the LLM itself: LLM needs to be capable enough to do all the stuff autonomously, instead of stuck in an infinite loop.
**NOTE: This agent is still highly experimental and under active development to reach the capability described in the original paper & [repo](https://github.com/xingyaoww/code-act).**
@@ -18,6 +18,6 @@ Example: prompts `gpt-4-0125-preview` to write a flask server, install `flask` l
<img width="957" alt="image" src="https://github.com/OpenDevin/OpenDevin/assets/38853559/68ad10c1-744a-4e9d-bb29-0f163d665a0a">
Most of the things are working as expected, except at the end, the model did not follow the instruction to stop the interaction by outputting `<execute> exit </execute>` as instructed.
Most of the things are working as expected, except at the end, the model did not follow the instruction to stop the interaction by outputting `<execute> exit </execute>` as instructed.
**TODO**: This should be fixable by either (1) including a complete in-context example like [this](https://github.com/xingyaoww/mint-bench/blob/main/mint/tasks/in_context_examples/reasoning/with_tool.txt), OR (2) collect some interaction data like this and fine-tune a model (like [this](https://github.com/xingyaoww/code-act), a more complex route).

View File

@@ -1,4 +1,4 @@
from opendevin.agent import Agent
from .codeact_agent import CodeActAgent
Agent.register("CodeActAgent", CodeActAgent)
Agent.register('CodeActAgent', CodeActAgent)

View File

@@ -117,13 +117,11 @@ class CodeActAgent(Agent):
{'role': 'user', 'content': obs.content})
elif isinstance(obs, CmdOutputObservation):
content = 'OBSERVATION:\n' + obs.content
# FIXME: autopep8 and mypy are fighting each other on this line
# autopep8: off
content += f"\n[Command {obs.command_id} finished with exit code {obs.exit_code}]]"
content += f'\n[Command {obs.command_id} finished with exit code {obs.exit_code}]]'
self.messages.append({'role': 'user', 'content': content})
else:
raise NotImplementedError(
f"Unknown observation type: {obs.__class__}"
f'Unknown observation type: {obs.__class__}'
)
response = self.llm.completion(
messages=self.messages,

View File

@@ -6,4 +6,3 @@ There's a lot of low-hanging fruit for this agent:
* Improve memory condensing--condense earlier memories more aggressively
* Limit the time that `run` can wait (in case agent runs an interactive command and it's hanging)
* Figure out how to run background processes, e.g. `node server.js` to start a server

View File

@@ -1,4 +1,4 @@
from opendevin.agent import Agent
from .agent import MonologueAgent
Agent.register("MonologueAgent", MonologueAgent)
Agent.register('MonologueAgent', MonologueAgent)

View File

@@ -51,7 +51,7 @@ class Monologue:
try:
total_length += len(json.dumps(t))
except TypeError as e:
print(f"Error serializing thought: {e}")
print(f'Error serializing thought: {e}')
return total_length
def condense(self, llm: LLM):
@@ -73,4 +73,4 @@ class Monologue:
self.thoughts = prompts.parse_summary_response(summary_resp)
except Exception as e:
traceback.print_exc()
raise RuntimeError(f"Error condensing thoughts: {e}")
raise RuntimeError(f'Error condensing thoughts: {e}')

View File

@@ -93,13 +93,14 @@ def get_summarize_monologue_prompt(thoughts: List[dict]):
"""
Gets the prompt for summarizing the monologue
Returns:
Returns:
- str: A formatted string with the current monologue within the prompt
"""
return MONOLOGUE_SUMMARY_PROMPT % {
'monologue': json.dumps({'old_monologue': thoughts}, indent=2),
}
def get_request_action_prompt(
task: str,
thoughts: List[dict],
@@ -120,23 +121,23 @@ def get_request_action_prompt(
hint = ''
if len(thoughts) > 0:
latest_thought = thoughts[-1]
if "action" in latest_thought:
if latest_thought["action"] == "think":
if latest_thought["args"]["thought"].startswith("OK so my task is"):
if 'action' in latest_thought:
if latest_thought['action'] == 'think':
if latest_thought['args']['thought'].startswith('OK so my task is'):
hint = "You're just getting started! What should you do first?"
else:
hint = "You've been thinking a lot lately. Maybe it's time to take action?"
elif latest_thought["action"] == "error":
hint = "Looks like that last command failed. Maybe you need to fix it, or try something else."
elif latest_thought['action'] == 'error':
hint = 'Looks like that last command failed. Maybe you need to fix it, or try something else.'
bg_commands_message = ""
bg_commands_message = ''
if len(background_commands_obs) > 0:
bg_commands_message = "The following commands are running in the background:"
bg_commands_message = 'The following commands are running in the background:'
for command_obs in background_commands_obs:
bg_commands_message += (
f"\n`{command_obs.command_id}`: {command_obs.command}"
f'\n`{command_obs.command_id}`: {command_obs.command}'
)
bg_commands_message += "\nYou can end any process by sending a `kill` action with the numerical `id` above."
bg_commands_message += '\nYou can end any process by sending a `kill` action with the numerical `id` above.'
return ACTION_PROMPT % {
'task': task,
@@ -145,6 +146,7 @@ def get_request_action_prompt(
'hint': hint,
}
def parse_action_response(response: str) -> Action:
"""
Parses a string to find an action within it
@@ -162,17 +164,18 @@ def parse_action_response(response: str) -> Action:
response_json_matches = re.finditer(
r"""{\s*\"action\":\s?\"(\w+)\"(?:,?|,\s*\"args\":\s?{((?:.|\s)*?)})\s*}""",
response) # Find all response-looking strings
def rank(match):
return len(match[2]) if match[1] == "think" else 130 # Crudely rank multiple responses by length
return len(match[2]) if match[1] == 'think' else 130 # Crudely rank multiple responses by length
try:
action_dict = json.loads(max(response_json_matches, key=rank)[0]) # Use the highest ranked response
except ValueError as e:
raise ValueError(
"Output from the LLM isn't properly formatted. The model may be misconfigured."
) from e
if "content" in action_dict:
if 'content' in action_dict:
# The LLM gets confused here. Might as well be robust
action_dict["contents"] = action_dict.pop("content")
action_dict['contents'] = action_dict.pop('content')
return action_from_dict(action_dict)
@@ -187,4 +190,4 @@ def parse_summary_response(response: str) -> List[dict]:
- List[dict]: The list of summaries output by the model
"""
parsed = json.loads(response)
return parsed["new_monologue"]
return parsed['new_monologue']

View File

@@ -1,4 +1,4 @@
from opendevin.agent import Agent
from .agent import PlannerAgent
Agent.register("PlannerAgent", PlannerAgent)
Agent.register('PlannerAgent', PlannerAgent)

View File

@@ -155,14 +155,14 @@ def get_prompt(plan: Plan, history: List[Tuple[Action, Observation]]) -> str:
if not isinstance(observation, NullObservation):
observation_dict = observation.to_dict()
if (
"extras" in observation_dict
and "screenshot" in observation_dict["extras"]
'extras' in observation_dict
and 'screenshot' in observation_dict['extras']
):
del observation_dict["extras"]["screenshot"]
del observation_dict['extras']['screenshot']
history_dicts.append(observation_dict)
history_str = json.dumps(history_dicts, indent=2)
hint = ""
hint = ''
current_task = plan.get_current_task()
if current_task is not None:
plan_status = f"You're currently working on this task:\n{current_task.goal}."
@@ -172,39 +172,39 @@ def get_prompt(plan: Plan, history: List[Tuple[Action, Observation]]) -> str:
plan_status = "You're not currently working on any tasks. Your next action MUST be to mark a task as in_progress."
hint = plan_status
latest_action_id = latest_action.to_dict()["action"]
latest_action_id = latest_action.to_dict()['action']
if current_task is not None:
if latest_action_id == "":
if latest_action_id == '':
hint = "You haven't taken any actions yet. Start by using `ls` to check out what files you're working with."
elif latest_action_id == ActionType.RUN:
hint = "You should think about the command you just ran, what output it gave, and how that affects your plan."
hint = 'You should think about the command you just ran, what output it gave, and how that affects your plan.'
elif latest_action_id == ActionType.READ:
hint = "You should think about the file you just read, what you learned from it, and how that affects your plan."
hint = 'You should think about the file you just read, what you learned from it, and how that affects your plan.'
elif latest_action_id == ActionType.WRITE:
hint = "You just changed a file. You should think about how it affects your plan."
hint = 'You just changed a file. You should think about how it affects your plan.'
elif latest_action_id == ActionType.BROWSE:
hint = "You should think about the page you just visited, and what you learned from it."
hint = 'You should think about the page you just visited, and what you learned from it.'
elif latest_action_id == ActionType.THINK:
hint = "Look at your last thought in the history above. What does it suggest? Don't think anymore--take action."
elif latest_action_id == ActionType.RECALL:
hint = "You should think about the information you just recalled, and how it should affect your plan."
hint = 'You should think about the information you just recalled, and how it should affect your plan.'
elif latest_action_id == ActionType.ADD_TASK:
hint = "You should think about the next action to take."
hint = 'You should think about the next action to take.'
elif latest_action_id == ActionType.MODIFY_TASK:
hint = "You should think about the next action to take."
hint = 'You should think about the next action to take.'
elif latest_action_id == ActionType.SUMMARIZE:
hint = ""
hint = ''
elif latest_action_id == ActionType.FINISH:
hint = ""
hint = ''
print_with_color("HINT:\n" + hint, "INFO")
print_with_color('HINT:\n' + hint, 'INFO')
return prompt % {
"task": plan.main_goal,
"plan": plan_str,
"history": history_str,
"hint": hint,
"plan_status": plan_status,
'task': plan.main_goal,
'plan': plan_str,
'history': history_str,
'hint': hint,
'plan_status': plan_status,
}
@@ -218,12 +218,12 @@ def parse_response(response: str) -> Action:
Returns:
- Action: A valid next action to perform from model output
"""
json_start = response.find("{")
json_end = response.rfind("}") + 1
json_start = response.find('{')
json_end = response.rfind('}') + 1
response = response[json_start:json_end]
action_dict = json.loads(response)
if "contents" in action_dict:
if 'contents' in action_dict:
# The LLM gets confused here. Might as well be robust
action_dict["content"] = action_dict.pop("contents")
action_dict['content'] = action_dict.pop('contents')
action = action_from_dict(action_dict)
return action

View File

@@ -23,4 +23,3 @@ It will map `./workspace` into the docker container with the folder permission c
Example screenshot:
<img width="868" alt="image" src="https://github.com/OpenDevin/OpenDevin/assets/38853559/8dedcdee-437a-4469-870f-be29ca2b7c32">

View File

@@ -29,30 +29,30 @@ ACTION_TYPE_TO_CLASS = {action_class.action: action_class for action_class in ac
def action_from_dict(action: dict) -> Action:
action = action.copy()
if "action" not in action:
if 'action' not in action:
raise KeyError(f"'action' key is not found in {action=}")
action_class = ACTION_TYPE_TO_CLASS.get(action["action"])
action_class = ACTION_TYPE_TO_CLASS.get(action['action'])
if action_class is None:
raise KeyError(
f"'{action['action']=}' is not defined. Available actions: {ACTION_TYPE_TO_CLASS.keys()}"
)
args = action.get("args", {})
args = action.get('args', {})
return action_class(**args)
__all__ = [
"Action",
"NullAction",
"CmdRunAction",
"CmdKillAction",
"BrowseURLAction",
"FileReadAction",
"FileWriteAction",
"AgentRecallAction",
"AgentThinkAction",
"AgentFinishAction",
"AgentEchoAction",
"AgentSummarizeAction",
"AddTaskAction",
"ModifyTaskAction",
'Action',
'NullAction',
'CmdRunAction',
'CmdKillAction',
'BrowseURLAction',
'FileReadAction',
'FileWriteAction',
'AgentRecallAction',
'AgentThinkAction',
'AgentFinishAction',
'AgentEchoAction',
'AgentSummarizeAction',
'AddTaskAction',
'ModifyTaskAction',
]

View File

@@ -18,9 +18,9 @@ class AgentRecallAction(ExecutableAction):
query: str
action: str = ActionType.RECALL
def run(self, controller: "AgentController") -> AgentRecallObservation:
def run(self, controller: 'AgentController') -> AgentRecallObservation:
return AgentRecallObservation(
content="Recalling memories...",
content='Recalling memories...',
memories=controller.agent.search_memory(self.query),
)
@@ -34,7 +34,7 @@ class AgentThinkAction(NotExecutableAction):
thought: str
action: str = ActionType.THINK
def run(self, controller: "AgentController") -> "Observation":
def run(self, controller: 'AgentController') -> 'Observation':
raise NotImplementedError
@property
@@ -45,9 +45,9 @@ class AgentThinkAction(NotExecutableAction):
@dataclass
class AgentEchoAction(ExecutableAction):
content: str
action: str = "echo"
action: str = 'echo'
def run(self, controller: "AgentController") -> "Observation":
def run(self, controller: 'AgentController') -> 'Observation':
return AgentMessageObservation(self.content)
@property
@@ -70,7 +70,7 @@ class AgentSummarizeAction(NotExecutableAction):
class AgentFinishAction(NotExecutableAction):
action: str = ActionType.FINISH
def run(self, controller: "AgentController") -> "Observation":
def run(self, controller: 'AgentController') -> 'Observation':
raise NotImplementedError
@property

View File

@@ -9,16 +9,16 @@ if TYPE_CHECKING:
@dataclass
class Action:
def run(self, controller: "AgentController") -> "Observation":
def run(self, controller: 'AgentController') -> 'Observation':
raise NotImplementedError
def to_dict(self):
d = asdict(self)
try:
v = d.pop("action")
v = d.pop('action')
except KeyError:
raise NotImplementedError(f"{self=} does not have action attribute set")
return {"action": v, "args": d, "message": self.message}
raise NotImplementedError(f'{self=} does not have action attribute set')
return {'action': v, 'args': d, 'message': self.message}
@property
def executable(self) -> bool:
@@ -53,4 +53,4 @@ class NullAction(NotExecutableAction):
@property
def message(self) -> str:
return "No action"
return 'No action'

View File

@@ -15,12 +15,12 @@ class CmdRunAction(ExecutableAction):
background: bool = False
action: str = ActionType.RUN
def run(self, controller: "AgentController") -> "CmdOutputObservation":
def run(self, controller: 'AgentController') -> 'CmdOutputObservation':
return controller.command_manager.run_command(self.command, self.background)
@property
def message(self) -> str:
return f"Running command: {self.command}"
return f'Running command: {self.command}'
@dataclass
@@ -28,9 +28,9 @@ class CmdKillAction(ExecutableAction):
id: int
action: str = ActionType.KILL
def run(self, controller: "AgentController") -> "CmdOutputObservation":
def run(self, controller: 'AgentController') -> 'CmdOutputObservation':
return controller.command_manager.kill_command(self.id)
@property
def message(self) -> str:
return f"Killing command: {self.id}"
return f'Killing command: {self.id}'

View File

@@ -17,9 +17,9 @@ class BrowseURLAction(ExecutableAction):
url: str
action: str = ActionType.BROWSE
async def run(self, controller: "AgentController") -> BrowserOutputObservation: # type: ignore
async def run(self, controller: 'AgentController') -> BrowserOutputObservation: # type: ignore
asked_url = self.url
if not asked_url.startswith("http"):
if not asked_url.startswith('http'):
asked_url = os.path.abspath(os.curdir) + self.url
try:
async with async_playwright() as p:
@@ -27,11 +27,11 @@ class BrowseURLAction(ExecutableAction):
page = await browser.new_page()
response = await page.goto(asked_url)
# content = await page.content()
inner_text = await page.evaluate("() => document.body.innerText")
inner_text = await page.evaluate('() => document.body.innerText')
screenshot_bytes = await page.screenshot(full_page=True)
await browser.close()
screenshot_base64 = base64.b64encode(screenshot_bytes).decode("utf-8")
screenshot_base64 = base64.b64encode(screenshot_bytes).decode('utf-8')
return BrowserOutputObservation(
content=inner_text, # HTML content of the page
screenshot=screenshot_base64, # Base64-encoded screenshot
@@ -41,12 +41,11 @@ class BrowseURLAction(ExecutableAction):
except Exception as e:
return BrowserOutputObservation(
content=str(e),
screenshot="",
screenshot='',
error=True,
url=asked_url
)
@property
def message(self) -> str:
return f"Browsing URL: {self.url}"
return f'Browsing URL: {self.url}'

View File

@@ -8,12 +8,12 @@ from .base import ExecutableAction
# This is the path where the workspace is mounted in the container
# The LLM sometimes returns paths with this prefix, so we need to remove it
PATH_PREFIX = "/workspace/"
PATH_PREFIX = '/workspace/'
def resolve_path(base_path, file_path):
if file_path.startswith(PATH_PREFIX):
file_path = file_path[len(PATH_PREFIX) :]
file_path = file_path[len(PATH_PREFIX):]
return os.path.join(base_path, file_path)
@@ -24,12 +24,12 @@ class FileReadAction(ExecutableAction):
def run(self, controller) -> FileReadObservation:
path = resolve_path(controller.workdir, self.path)
with open(path, "r", encoding="utf-8") as file:
with open(path, 'r', encoding='utf-8') as file:
return FileReadObservation(path=path, content=file.read())
@property
def message(self) -> str:
return f"Reading file: {self.path}"
return f'Reading file: {self.path}'
@dataclass
@@ -40,10 +40,10 @@ class FileWriteAction(ExecutableAction):
def run(self, controller) -> FileWriteObservation:
whole_path = resolve_path(controller.workdir, self.path)
with open(whole_path, "w", encoding="utf-8") as file:
with open(whole_path, 'w', encoding='utf-8') as file:
file.write(self.content)
return FileWriteObservation(content="", path=self.path)
return FileWriteObservation(content='', path=self.path)
@property
def message(self) -> str:
return f"Writing file: {self.path}"
return f'Writing file: {self.path}'

View File

@@ -13,7 +13,7 @@ class AddTaskAction(NotExecutableAction):
@property
def message(self) -> str:
return f"Added task: {self.goal}"
return f'Added task: {self.goal}'
@dataclass
@@ -24,4 +24,4 @@ class ModifyTaskAction(NotExecutableAction):
@property
def message(self) -> str:
return f"Set task {self.id} to {self.state}"
return f'Set task {self.id} to {self.state}'

View File

@@ -206,7 +206,7 @@ class AgentController:
try:
callback(event)
except Exception as e:
logger.exception(f"Callback error: {e}, idx: {idx}")
logger.exception(f'Callback error: {e}, idx: {idx}')
await asyncio.sleep(
0.001
) # Give back control for a tick, so we can await in callbacks

View File

@@ -46,8 +46,6 @@ class CommandManager:
def _run_background(self, command: str) -> CmdOutputObservation:
bg_cmd = self.shell.execute_in_background(command)
# FIXME: autopep8 and mypy are fighting each other on this line
# autopep8: off
content = f'Background command started. To stop it, send a `kill` action with id {bg_cmd.id}'
return CmdOutputObservation(
content=content,

View File

@@ -1,9 +1,7 @@
class MaxCharsExceedError(Exception):
def __init__(self, num_of_chars=None, max_chars_limit=None):
if num_of_chars is not None and max_chars_limit is not None:
# FIXME: autopep8 and mypy are fighting each other on this line
# autopep8: off
message = f"Number of characters {num_of_chars} exceeds MAX_CHARS limit: {max_chars_limit}"
message = f'Number of characters {num_of_chars} exceeds MAX_CHARS limit: {max_chars_limit}'
else:
message = 'Number of characters exceeds MAX_CHARS limit'
super().__init__(message)

View File

@@ -4,9 +4,9 @@ from typing import Any, Dict, List
class WorkspaceFile:
name: str
children: List["WorkspaceFile"]
children: List['WorkspaceFile']
def __init__(self, name: str, children: List["WorkspaceFile"]):
def __init__(self, name: str, children: List['WorkspaceFile']):
self.name = name
self.children = children
@@ -17,8 +17,8 @@ class WorkspaceFile:
The dictionary representation of the File object.
"""
return {
"name": self.name,
"children": [child.to_dict() for child in self.children],
'name': self.name,
'children': [child.to_dict() for child in self.children],
}

View File

@@ -34,7 +34,7 @@ def get_file_handler():
log_dir = os.path.join(os.getcwd(), 'logs')
os.makedirs(log_dir, exist_ok=True)
timestamp = datetime.now().strftime('%Y-%m-%d_%H-%M-%S')
file_name = f"opendevin_{timestamp}.log"
file_name = f'opendevin_{timestamp}.log'
file_handler = logging.FileHandler(os.path.join(log_dir, file_name))
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(file_formatter)
@@ -98,7 +98,8 @@ class LlmFileHandler(logging.FileHandler):
self.log_directory = os.path.join(
os.getcwd(), 'logs', 'llm', self.session)
os.makedirs(self.log_directory, exist_ok=True)
self.baseFilename = os.path.join(self.log_directory, f"{self.filename}_{self.message_counter:03}.log")
self.baseFilename = os.path.join(
self.log_directory, f'{self.filename}_{self.message_counter:03}.log')
super().__init__(self.baseFilename, mode, encoding, delay)
def emit(self, record):
@@ -108,7 +109,8 @@ class LlmFileHandler(logging.FileHandler):
Args:
record (logging.LogRecord): The log record to emit.
"""
self.baseFilename = os.path.join(self.log_directory, f"{self.filename}_{self.message_counter:03}.log")
self.baseFilename = os.path.join(
self.log_directory, f'{self.filename}_{self.message_counter:03}.log')
self.stream = self._open()
super().emit(record)
self.stream.close

View File

@@ -89,8 +89,6 @@ async def main():
raise ValueError(
'No task provided. Please specify a task through -t, -f.')
# FIXME: autopep8 and mypy are fighting each other on this line
# autopep8: off
print(
f'Running agent {args.agent_cls} (model: {args.model_name}, directory: {args.directory}) with task: "{task}"'
)

View File

@@ -18,14 +18,14 @@ async def websocket_endpoint(websocket: WebSocket):
while True:
# receive message
data = await websocket.receive_json()
print(f"Received message: {data}")
print(f'Received message: {data}')
# send mock response to client
response = {'message': f"receive {data}"}
response = {'message': f'receive {data}'}
await websocket.send_json(response)
print(f"Sent message: {response}")
print(f'Sent message: {response}')
except Exception as e:
print(f"WebSocket Error: {e}")
print(f'WebSocket Error: {e}')
@app.get('/')

View File

@@ -17,30 +17,32 @@ observations = (
AgentErrorObservation,
)
OBSERVATION_TYPE_TO_CLASS = {observation_class.observation:observation_class for observation_class in observations} # type: ignore[attr-defined]
OBSERVATION_TYPE_TO_CLASS = {observation_class.observation: observation_class for observation_class in observations} # type: ignore[attr-defined]
def observation_from_dict(observation: dict) -> Observation:
observation = observation.copy()
if "observation" not in observation:
if 'observation' not in observation:
raise KeyError(f"'observation' key is not found in {observation=}")
observation_class = OBSERVATION_TYPE_TO_CLASS.get(observation["observation"])
observation_class = OBSERVATION_TYPE_TO_CLASS.get(observation['observation'])
if observation_class is None:
raise KeyError(f"'{observation['observation']=}' is not defined. Available observations: {OBSERVATION_TYPE_TO_CLASS.keys()}")
observation.pop("observation")
observation.pop("message", None)
content = observation.pop("content", "")
extras = observation.pop("extras", {})
observation.pop('observation')
observation.pop('message', None)
content = observation.pop('content', '')
extras = observation.pop('extras', {})
return observation_class(content=content, **extras)
__all__ = [
"Observation",
"NullObservation",
"CmdOutputObservation",
"BrowserOutputObservation",
"FileReadObservation",
"FileWriteObservation",
"UserMessageObservation",
"AgentMessageObservation",
"AgentRecallObservation",
"AgentErrorObservation",
'Observation',
'NullObservation',
'CmdOutputObservation',
'BrowserOutputObservation',
'FileReadObservation',
'FileWriteObservation',
'UserMessageObservation',
'AgentMessageObservation',
'AgentRecallObservation',
'AgentErrorObservation',
]

View File

@@ -17,19 +17,19 @@ class Observation:
def to_dict(self) -> dict:
"""Converts the observation to a dictionary."""
extras = copy.deepcopy(self.__dict__)
content = extras.pop("content", "")
observation = extras.pop("observation", "")
content = extras.pop('content', '')
observation = extras.pop('observation', '')
return {
"observation": observation,
"content": content,
"extras": extras,
"message": self.message,
'observation': observation,
'content': content,
'extras': extras,
'message': self.message,
}
@property
def message(self) -> str:
"""Returns a message describing the observation."""
return ""
return ''
@dataclass
@@ -43,4 +43,4 @@ class NullObservation(Observation):
@property
def message(self) -> str:
return ""
return ''

View File

@@ -18,4 +18,4 @@ class BrowserOutputObservation(Observation):
@property
def message(self) -> str:
return "Visited " + self.url
return 'Visited ' + self.url

View File

@@ -14,4 +14,4 @@ class AgentErrorObservation(Observation):
@property
def message(self) -> str:
return "Oops. Something went wrong: " + self.content
return 'Oops. Something went wrong: ' + self.content

View File

@@ -15,7 +15,7 @@ class FileReadObservation(Observation):
@property
def message(self) -> str:
return f"I read the file {self.path}."
return f'I read the file {self.path}.'
@dataclass
@@ -29,4 +29,4 @@ class FileWriteObservation(Observation):
@property
def message(self) -> str:
return f"I wrote to the file {self.path}."
return f'I wrote to the file {self.path}.'

View File

@@ -10,12 +10,12 @@ class UserMessageObservation(Observation):
This data class represents a message sent by the user.
"""
role: str = "user"
role: str = 'user'
observation: str = ObservationType.MESSAGE
@property
def message(self) -> str:
return ""
return ''
@dataclass
@@ -24,9 +24,9 @@ class AgentMessageObservation(Observation):
This data class represents a message sent by the agent.
"""
role: str = "assistant"
role: str = 'assistant'
observation: str = ObservationType.MESSAGE
@property
def message(self) -> str:
return ""
return ''

View File

@@ -12,9 +12,9 @@ class AgentRecallObservation(Observation):
"""
memories: List[str]
role: str = "assistant"
role: str = 'assistant'
observation: str = ObservationType.RECALL
@property
def message(self) -> str:
return "The agent recalled memories."
return 'The agent recalled memories.'

View File

@@ -21,4 +21,4 @@ class CmdOutputObservation(Observation):
@property
def message(self) -> str:
return f"Command `{self.command}` executed with exit code {self.exit_code}."
return f'Command `{self.command}` executed with exit code {self.exit_code}.'

View File

@@ -12,41 +12,41 @@ class Command:
def parse_command_file() -> str | None:
if not os.path.exists("commands.sh"):
if not os.path.exists('commands.sh'):
return None
content = open("commands.sh", "r").read()
lines = content.split("\n")
content = open('commands.sh', 'r').read()
lines = content.split('\n')
commands: list[Command] = []
idx = 0
docs: list[str] = []
while idx < len(lines):
line = lines[idx]
idx += 1
if line.startswith("# "):
if line.startswith('# '):
docs.append(line[2:])
elif line.strip().endswith("() {"):
elif line.strip().endswith('() {'):
name = line.split()[0][:-2]
while lines[idx].strip() != "}":
while lines[idx].strip() != '}':
idx += 1
docstring, signature = None, name
docs_dict = yaml.safe_load("\n".join(docs).replace("@yaml", ""))
docs_dict = yaml.safe_load('\n'.join(docs).replace('@yaml', ''))
if docs_dict is not None:
docstring = docs_dict.get("docstring")
arguments = docs_dict.get("arguments", None)
if "signature" in docs_dict:
signature = docs_dict["signature"]
docstring = docs_dict.get('docstring')
arguments = docs_dict.get('arguments', None)
if 'signature' in docs_dict:
signature = docs_dict['signature']
else:
if arguments is not None:
for param, settings in arguments.items():
if "required" in settings:
signature += f" <{param}>"
if 'required' in settings:
signature += f' <{param}>'
else:
signature += f" [<{param}>]"
signature += f' [<{param}>]'
command = Command(name, docstring, signature)
commands.append(command)
docs = []
function_docs = ""
function_docs = ''
for cmd in commands:
if cmd.docstring is not None:
function_docs += f"{cmd.signature or cmd.name} - {cmd.docstring}\n"
function_docs += f'{cmd.signature or cmd.name} - {cmd.docstring}\n'
return function_docs

View File

@@ -167,11 +167,10 @@ class DockerSSHBox(Sandbox):
else:
username = 'root'
logger.info(
# FIXME: mypy and autopep8 fight each other on this line
# autopep8: off
f"Connecting to {username}@{hostname} via ssh. If you encounter any issues, you can try `ssh -v -p {self._ssh_port} {username}@{hostname}` with the password '{self._ssh_password}' and report the issue on GitHub."
)
self.ssh.login(hostname, username, self._ssh_password, port=self._ssh_port)
self.ssh.login(hostname, username, self._ssh_password,
port=self._ssh_port)
# Fix: https://github.com/pexpect/pexpect/issues/669
self.ssh.sendline("bind 'set enable-bracketed-paste off'")

View File

@@ -2,53 +2,53 @@ from enum import Enum
class ActionType(str, Enum):
INIT = "initialize"
INIT = 'initialize'
"""Initializes the agent. Only sent by client.
"""
START = "start"
START = 'start'
"""Starts a new development task. Only sent by the client.
"""
READ = "read"
READ = 'read'
"""Reads the content of a file.
"""
WRITE = "write"
WRITE = 'write'
"""Writes the content to a file.
"""
RUN = "run"
RUN = 'run'
"""Runs a command.
"""
KILL = "kill"
KILL = 'kill'
"""Kills a background command.
"""
BROWSE = "browse"
BROWSE = 'browse'
"""Opens a web page.
"""
RECALL = "recall"
RECALL = 'recall'
"""Searches long-term memory
"""
THINK = "think"
THINK = 'think'
"""Allows the agent to make a plan, set a goal, or record thoughts
"""
FINISH = "finish"
"""If you're absolutely certain that you've completed your task and have tested your work,
FINISH = 'finish'
"""If you're absolutely certain that you've completed your task and have tested your work,
use the finish action to stop working.
"""
CHAT = "chat"
CHAT = 'chat'
SUMMARIZE = "summarize"
SUMMARIZE = 'summarize'
ADD_TASK = "add_task"
ADD_TASK = 'add_task'
MODIFY_TASK = "modify_task"
MODIFY_TASK = 'modify_task'
NULL = "null"
NULL = 'null'

View File

@@ -2,30 +2,30 @@ from enum import Enum
class ObservationType(str, Enum):
READ = "read"
READ = 'read'
"""The content of a file
"""
WRITE = "write"
WRITE = 'write'
BROWSE = "browse"
BROWSE = 'browse'
"""The HTML content of a URL
"""
RUN = "run"
RUN = 'run'
"""The output of a command
"""
RECALL = "recall"
RECALL = 'recall'
"""The result of a search
"""
CHAT = "chat"
CHAT = 'chat'
"""A message from the user
"""
MESSAGE = "message"
MESSAGE = 'message'
ERROR = "error"
ERROR = 'error'
NULL = "null"
NULL = 'null'

View File

@@ -35,7 +35,7 @@ class AgentManager:
await self.sid_to_agent[sid].dispatch(action, data)
def handle_signal(self, signum, _):
print(f"Received signal {signum}, exiting...")
print(f'Received signal {signum}, exiting...')
self.close()
exit(0)

View File

@@ -1,3 +1,3 @@
from .auth import get_sid_from_token, sign_token
__all__ = ["get_sid_from_token", "sign_token"]
__all__ = ['get_sid_from_token', 'sign_token']

View File

@@ -29,7 +29,7 @@ class SessionManager:
self._sessions[sid].update_connection(ws_conn)
async def loop_recv(self, sid: str, dispatch: Callable):
print(f"Starting loop_recv for sid: {sid}")
print(f'Starting loop_recv for sid: {sid}')
"""Starts listening for messages from the client."""
if sid not in self._sessions:
return
@@ -39,7 +39,7 @@ class SessionManager:
self._save_sessions()
def handle_signal(self, signum, _):
print(f"Received signal {signum}, exiting...")
print(f'Received signal {signum}, exiting...')
self.close()
exit(0)

View File

@@ -44,6 +44,9 @@ pytest = "*"
[tool.poetry.group.evaluation.dependencies]
torch = "*"
[tool.autopep8]
ignore = [ "E501" ]
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"