mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-26 05:48:36 +08:00
Migrate to new folder structure in preparation for refactor (#1531)
* fix up folder structure * update docs * fix imports * fix imports * fix imoprt * fix imports * fix imports * fix imports * fix test import * fix tests * fix main import
This commit is contained in:
parent
ce7c7eaae4
commit
fadcdc117e
2
.github/workflows/dummy-agent-test.yml
vendored
2
.github/workflows/dummy-agent-test.yml
vendored
@ -27,4 +27,4 @@ jobs:
|
||||
wget https://huggingface.co/BAAI/bge-small-en-v1.5/raw/main/1_Pooling/config.json -P /tmp/llama_index/models--BAAI--bge-small-en-v1.5/snapshots/5c38ec7c405ec4b44b94cc5a9bb96e735b38267a/1_Pooling/
|
||||
- name: Run tests
|
||||
run: |
|
||||
poetry run python opendevin/main.py -t "do a flip" -m ollama/not-a-model -d ./workspace/ -c DummyAgent
|
||||
poetry run python opendevin/core/main.py -t "do a flip" -m ollama/not-a-model -d ./workspace/ -c DummyAgent
|
||||
|
||||
2
.github/workflows/review-pr.yml
vendored
2
.github/workflows/review-pr.yml
vendored
@ -49,7 +49,7 @@ jobs:
|
||||
LLM_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
SANDBOX_TYPE: exec
|
||||
run: |
|
||||
WORKSPACE_MOUNT_PATH=$GITHUB_WORKSPACE python ./opendevin/main.py -i 50 -f task.txt -d $GITHUB_WORKSPACE
|
||||
WORKSPACE_MOUNT_PATH=$GITHUB_WORKSPACE python ./opendevin/core/main.py -i 50 -f task.txt -d $GITHUB_WORKSPACE
|
||||
rm task.txt
|
||||
|
||||
- name: Check if review file is non-empty
|
||||
|
||||
2
.github/workflows/solve-issue.yml
vendored
2
.github/workflows/solve-issue.yml
vendored
@ -43,7 +43,7 @@ jobs:
|
||||
LLM_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
SANDBOX_TYPE: exec
|
||||
run: |
|
||||
WORKSPACE_MOUNT_PATH=$GITHUB_WORKSPACE python ./opendevin/main.py -i 50 -f task.txt -d $GITHUB_WORKSPACE
|
||||
WORKSPACE_MOUNT_PATH=$GITHUB_WORKSPACE python ./opendevin/core/main.py -i 50 -f task.txt -d $GITHUB_WORKSPACE
|
||||
rm task.txt
|
||||
|
||||
- name: Setup Git, Create Branch, and Commit Changes
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
from opendevin.agent import Agent
|
||||
from opendevin.controller.agent import Agent
|
||||
|
||||
from .agent import SWEAgent
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
from typing import List
|
||||
|
||||
from opendevin.agent import Agent
|
||||
from opendevin.controller.agent import Agent
|
||||
from opendevin.controller.state.state import State
|
||||
from opendevin.events.action import (
|
||||
Action,
|
||||
AgentThinkAction,
|
||||
@ -9,7 +10,6 @@ from opendevin.events.action import (
|
||||
)
|
||||
from opendevin.events.observation import Observation
|
||||
from opendevin.llm.llm import LLM
|
||||
from opendevin.state import State
|
||||
|
||||
from .parser import parse_command
|
||||
from .prompts import (
|
||||
@ -48,9 +48,12 @@ class SWEAgent(Agent):
|
||||
)
|
||||
action_resp = resp['choices'][0]['message']['content']
|
||||
print(f"\033[1m\033[91m{resp['usage']}\033[0m")
|
||||
print('\n==== RAW OUTPUT ====',
|
||||
f'\033[96m{action_resp}\033[0m',
|
||||
'==== END RAW ====\n', sep='\n')
|
||||
print(
|
||||
'\n==== RAW OUTPUT ====',
|
||||
f'\033[96m{action_resp}\033[0m',
|
||||
'==== END RAW ====\n',
|
||||
sep='\n',
|
||||
)
|
||||
return parse_command(action_resp, self.cur_file, self.cur_line)
|
||||
|
||||
def _update(self, action: Action) -> None:
|
||||
@ -68,22 +71,15 @@ class SWEAgent(Agent):
|
||||
for prev_action, obs in state.updated_info:
|
||||
self._remember(prev_action, obs)
|
||||
|
||||
prompt = STEP_PROMPT(
|
||||
state.plan.main_goal,
|
||||
self.cur_file,
|
||||
self.cur_line
|
||||
)
|
||||
prompt = STEP_PROMPT(state.plan.main_goal, self.cur_file, self.cur_line)
|
||||
|
||||
msgs = [
|
||||
{'content': SYSTEM_MESSAGE, 'role': 'system'},
|
||||
{'content': prompt, 'role': 'user'}
|
||||
{'content': prompt, 'role': 'user'},
|
||||
]
|
||||
|
||||
if len(self.running_memory) > 0:
|
||||
context = CONTEXT_PROMPT(
|
||||
self.running_memory,
|
||||
self.memory_window
|
||||
)
|
||||
context = CONTEXT_PROMPT(self.running_memory, self.memory_window)
|
||||
msgs.insert(1, {'content': context, 'role': 'user'})
|
||||
# clrs = [''] * (len(msgs)-2) + ['\033[0;36m', '\033[0;35m']
|
||||
# print('\n\n'.join([c+m['content']+'\033[0m' for c, m in zip(clrs, msgs)]))
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from opendevin.agent import Agent
|
||||
from opendevin.controller.agent import Agent
|
||||
|
||||
from .micro.agent import MicroAgent
|
||||
from .micro.registry import all_microagents
|
||||
@ -8,7 +8,6 @@ from .micro.registry import all_microagents
|
||||
load_dotenv()
|
||||
|
||||
|
||||
|
||||
from . import ( # noqa: E402
|
||||
SWE_agent,
|
||||
codeact_agent,
|
||||
@ -18,18 +17,26 @@ from . import ( # noqa: E402
|
||||
planner_agent,
|
||||
)
|
||||
|
||||
__all__ = ['monologue_agent', 'codeact_agent',
|
||||
'planner_agent', 'SWE_agent',
|
||||
'delegator_agent',
|
||||
'dummy_agent']
|
||||
__all__ = [
|
||||
'monologue_agent',
|
||||
'codeact_agent',
|
||||
'planner_agent',
|
||||
'SWE_agent',
|
||||
'delegator_agent',
|
||||
'dummy_agent',
|
||||
]
|
||||
|
||||
for agent in all_microagents.values():
|
||||
name = agent['name']
|
||||
prompt = agent['prompt']
|
||||
|
||||
anon_class = type(name, (MicroAgent,), {
|
||||
'prompt': prompt,
|
||||
'agent_definition': agent,
|
||||
})
|
||||
anon_class = type(
|
||||
name,
|
||||
(MicroAgent,),
|
||||
{
|
||||
'prompt': prompt,
|
||||
'agent_definition': agent,
|
||||
},
|
||||
)
|
||||
|
||||
Agent.register(name, anon_class)
|
||||
|
||||
@ -9,7 +9,7 @@ This folder implements the [CodeAct idea](https://arxiv.org/abs/2402.13463) that
|
||||
|
||||
```bash
|
||||
mkdir workspace
|
||||
PYTHONPATH=`pwd`:$PYTHONPATH python3 opendevin/main.py -d ./workspace -c CodeActAgent -t "Please write a flask app that returns 'Hello, World\!' at the root URL, then start the app on port 5000. python3 has already been installed for you."
|
||||
PYTHONPATH=`pwd`:$PYTHONPATH python3 opendevin/core/main.py -d ./workspace -c CodeActAgent -t "Please write a flask app that returns 'Hello, World\!' at the root URL, then start the app on port 5000. python3 has already been installed for you."
|
||||
```
|
||||
|
||||
Example: prompts `gpt-4-0125-preview` to write a flask server, install `flask` library, and start the server.
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
from opendevin.agent import Agent
|
||||
from opendevin.controller.agent import Agent
|
||||
|
||||
from .codeact_agent import CodeActAgent
|
||||
|
||||
|
||||
@ -2,7 +2,8 @@ import re
|
||||
from typing import List, Mapping
|
||||
|
||||
from agenthub.codeact_agent.prompt import EXAMPLES, SYSTEM_MESSAGE
|
||||
from opendevin.agent import Agent
|
||||
from opendevin.controller.agent import Agent
|
||||
from opendevin.controller.state.state import State
|
||||
from opendevin.events.action import (
|
||||
Action,
|
||||
AgentEchoAction,
|
||||
@ -19,12 +20,11 @@ from opendevin.events.observation import (
|
||||
UserMessageObservation,
|
||||
)
|
||||
from opendevin.llm.llm import LLM
|
||||
from opendevin.sandbox.plugins import (
|
||||
from opendevin.runtime.plugins import (
|
||||
JupyterRequirement,
|
||||
PluginRequirement,
|
||||
SWEAgentCommandsRequirement,
|
||||
)
|
||||
from opendevin.state import State
|
||||
|
||||
|
||||
def parse_response(response) -> str:
|
||||
@ -34,14 +34,20 @@ def parse_response(response) -> str:
|
||||
action += f'</execute_{lang}>'
|
||||
return action
|
||||
|
||||
def truncate_observation(observation: str, max_chars: int=5000) -> str:
|
||||
|
||||
def truncate_observation(observation: str, max_chars: int = 5000) -> str:
|
||||
"""
|
||||
Truncate the middle of the observation if it is too long.
|
||||
"""
|
||||
if len(observation) <= max_chars:
|
||||
return observation
|
||||
half = max_chars // 2
|
||||
return observation[:half] + '\n[... Observation truncated due to length ...]\n' + observation[-half:]
|
||||
return (
|
||||
observation[:half]
|
||||
+ '\n[... Observation truncated due to length ...]\n'
|
||||
+ observation[-half:]
|
||||
)
|
||||
|
||||
|
||||
class CodeActAgent(Agent):
|
||||
"""
|
||||
@ -49,19 +55,22 @@ class CodeActAgent(Agent):
|
||||
The agent works by passing the model a list of action-observation pairs and prompting the model to take the next step.
|
||||
"""
|
||||
|
||||
sandbox_plugins: List[PluginRequirement] = [JupyterRequirement(), SWEAgentCommandsRequirement()]
|
||||
sandbox_plugins: List[PluginRequirement] = [
|
||||
JupyterRequirement(),
|
||||
SWEAgentCommandsRequirement(),
|
||||
]
|
||||
SUPPORTED_ACTIONS = (
|
||||
CmdRunAction,
|
||||
IPythonRunCellAction,
|
||||
AgentEchoAction,
|
||||
AgentTalkAction,
|
||||
NullAction
|
||||
NullAction,
|
||||
)
|
||||
SUPPORTED_OBSERVATIONS = (
|
||||
AgentMessageObservation,
|
||||
UserMessageObservation,
|
||||
CmdOutputObservation,
|
||||
IPythonRunCellObservation
|
||||
IPythonRunCellObservation,
|
||||
)
|
||||
|
||||
def __init__(
|
||||
@ -102,7 +111,7 @@ class CodeActAgent(Agent):
|
||||
'content': (
|
||||
f'Here is an example of how you can interact with the environment for task solving:\n{EXAMPLES}\n\n'
|
||||
f"NOW, LET'S START!\n\n{state.plan.main_goal}"
|
||||
)
|
||||
),
|
||||
},
|
||||
]
|
||||
updated_info = state.updated_info
|
||||
@ -118,8 +127,7 @@ class CodeActAgent(Agent):
|
||||
obs, self.SUPPORTED_OBSERVATIONS
|
||||
), f'{obs.__class__} is not supported (supported: {self.SUPPORTED_OBSERVATIONS})'
|
||||
if isinstance(obs, (AgentMessageObservation, UserMessageObservation)):
|
||||
self.messages.append(
|
||||
{'role': 'user', 'content': obs.content})
|
||||
self.messages.append({'role': 'user', 'content': obs.content})
|
||||
|
||||
# User wants to exit
|
||||
if obs.content.strip() == '/exit':
|
||||
@ -135,7 +143,9 @@ class CodeActAgent(Agent):
|
||||
splited = content.split('\n')
|
||||
for i, line in enumerate(splited):
|
||||
if ' already displayed to user'
|
||||
splited[i] = (
|
||||
' already displayed to user'
|
||||
)
|
||||
content = '\n'.join(splited)
|
||||
content = truncate_observation(content)
|
||||
self.messages.append({'role': 'user', 'content': content})
|
||||
@ -150,7 +160,7 @@ class CodeActAgent(Agent):
|
||||
'</execute_ipython>',
|
||||
'</execute_bash>',
|
||||
],
|
||||
temperature=0.0
|
||||
temperature=0.0,
|
||||
)
|
||||
action_str: str = parse_response(response)
|
||||
state.num_of_chars += sum(
|
||||
@ -158,7 +168,9 @@ class CodeActAgent(Agent):
|
||||
) + len(action_str)
|
||||
self.messages.append({'role': 'assistant', 'content': action_str})
|
||||
|
||||
if bash_command := re.search(r'<execute_bash>(.*)</execute_bash>', action_str, re.DOTALL):
|
||||
if bash_command := re.search(
|
||||
r'<execute_bash>(.*)</execute_bash>', action_str, re.DOTALL
|
||||
):
|
||||
# remove the command from the action string to get thought
|
||||
thought = action_str.replace(bash_command.group(0), '').strip()
|
||||
# a command was found
|
||||
@ -166,7 +178,9 @@ class CodeActAgent(Agent):
|
||||
if command_group.strip() == 'exit':
|
||||
return AgentFinishAction()
|
||||
return CmdRunAction(command=command_group, thought=thought)
|
||||
elif python_code := re.search(r'<execute_ipython>(.*)</execute_ipython>', action_str, re.DOTALL):
|
||||
elif python_code := re.search(
|
||||
r'<execute_ipython>(.*)</execute_ipython>', action_str, re.DOTALL
|
||||
):
|
||||
# a code block was found
|
||||
code_group = python_code.group(1).strip()
|
||||
thought = action_str.replace(python_code.group(0), '').strip()
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
from opendevin.sandbox.plugins import SWEAgentCommandsRequirement
|
||||
from opendevin.runtime.plugins import SWEAgentCommandsRequirement
|
||||
|
||||
_SWEAGENT_BASH_DOCS = '\n'.join(
|
||||
filter(
|
||||
lambda x: not x.startswith('submit'),
|
||||
SWEAgentCommandsRequirement.documentation.split('\n')
|
||||
SWEAgentCommandsRequirement.documentation.split('\n'),
|
||||
)
|
||||
)
|
||||
# _SWEAGENT_BASH_DOCS content below:
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
from opendevin.agent import Agent
|
||||
from opendevin.controller.agent import Agent
|
||||
|
||||
from .agent import DelegatorAgent
|
||||
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
from typing import List
|
||||
|
||||
from opendevin.agent import Agent
|
||||
from opendevin.controller.agent import Agent
|
||||
from opendevin.controller.state.state import State
|
||||
from opendevin.events.action import Action, AgentDelegateAction, AgentFinishAction
|
||||
from opendevin.events.observation import AgentDelegateObservation
|
||||
from opendevin.llm.llm import LLM
|
||||
from opendevin.state import State
|
||||
|
||||
|
||||
class DelegatorAgent(Agent):
|
||||
@ -12,6 +12,7 @@ class DelegatorAgent(Agent):
|
||||
The planner agent utilizes a special prompting strategy to create long term plans for solving problems.
|
||||
The agent is given its previous action-observation pairs, current task, and hint based on last action taken at every step.
|
||||
"""
|
||||
|
||||
current_delegate: str = ''
|
||||
|
||||
def __init__(self, llm: LLM):
|
||||
@ -37,9 +38,9 @@ class DelegatorAgent(Agent):
|
||||
"""
|
||||
if self.current_delegate == '':
|
||||
self.current_delegate = 'study'
|
||||
return AgentDelegateAction(agent='StudyRepoForTaskAgent', inputs={
|
||||
'task': state.plan.main_goal
|
||||
})
|
||||
return AgentDelegateAction(
|
||||
agent='StudyRepoForTaskAgent', inputs={'task': state.plan.main_goal}
|
||||
)
|
||||
|
||||
lastObservation = state.history[-1][1]
|
||||
if not isinstance(lastObservation, AgentDelegateObservation):
|
||||
@ -47,24 +48,36 @@ class DelegatorAgent(Agent):
|
||||
|
||||
if self.current_delegate == 'study':
|
||||
self.current_delegate = 'coder'
|
||||
return AgentDelegateAction(agent='Coder', inputs={
|
||||
'task': state.plan.main_goal,
|
||||
'summary': lastObservation.outputs['summary'],
|
||||
})
|
||||
return AgentDelegateAction(
|
||||
agent='Coder',
|
||||
inputs={
|
||||
'task': state.plan.main_goal,
|
||||
'summary': lastObservation.outputs['summary'],
|
||||
},
|
||||
)
|
||||
elif self.current_delegate == 'coder':
|
||||
self.current_delegate = 'verifier'
|
||||
return AgentDelegateAction(agent='Verifier', inputs={
|
||||
'task': state.plan.main_goal,
|
||||
})
|
||||
return AgentDelegateAction(
|
||||
agent='Verifier',
|
||||
inputs={
|
||||
'task': state.plan.main_goal,
|
||||
},
|
||||
)
|
||||
elif self.current_delegate == 'verifier':
|
||||
if 'completed' in lastObservation.outputs and lastObservation.outputs['completed']:
|
||||
if (
|
||||
'completed' in lastObservation.outputs
|
||||
and lastObservation.outputs['completed']
|
||||
):
|
||||
return AgentFinishAction()
|
||||
else:
|
||||
self.current_delegate = 'coder'
|
||||
return AgentDelegateAction(agent='Coder', inputs={
|
||||
'task': state.plan.main_goal,
|
||||
'summary': lastObservation.outputs['summary'],
|
||||
})
|
||||
return AgentDelegateAction(
|
||||
agent='Coder',
|
||||
inputs={
|
||||
'task': state.plan.main_goal,
|
||||
'summary': lastObservation.outputs['summary'],
|
||||
},
|
||||
)
|
||||
else:
|
||||
raise Exception('Invalid delegate state')
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
from opendevin.agent import Agent
|
||||
from opendevin.controller.agent import Agent
|
||||
|
||||
from .agent import DummyAgent
|
||||
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import time
|
||||
from typing import List, TypedDict
|
||||
|
||||
from opendevin.agent import Agent
|
||||
from opendevin.controller.agent import Agent
|
||||
from opendevin.controller.state.state import State
|
||||
from opendevin.events.action import (
|
||||
Action,
|
||||
AddTaskAction,
|
||||
@ -23,7 +24,6 @@ from opendevin.events.observation import (
|
||||
Observation,
|
||||
)
|
||||
from opendevin.llm.llm import LLM
|
||||
from opendevin.state import State
|
||||
|
||||
"""
|
||||
FIXME: There are a few problems this surfaced
|
||||
@ -33,7 +33,9 @@ FIXME: There are a few problems this surfaced
|
||||
* Browser not working
|
||||
"""
|
||||
|
||||
ActionObs = TypedDict('ActionObs', {'action': Action, 'observations': List[Observation]})
|
||||
ActionObs = TypedDict(
|
||||
'ActionObs', {'action': Action, 'observations': List[Observation]}
|
||||
)
|
||||
|
||||
BACKGROUND_CMD = 'echo "This is in the background" && sleep .1 && echo "This too"'
|
||||
|
||||
@ -46,51 +48,82 @@ class DummyAgent(Agent):
|
||||
|
||||
def __init__(self, llm: LLM):
|
||||
super().__init__(llm)
|
||||
self.steps: List[ActionObs] = [{
|
||||
'action': AddTaskAction(parent='0', goal='check the current directory'),
|
||||
'observations': [NullObservation('')],
|
||||
}, {
|
||||
'action': AddTaskAction(parent='0.0', goal='run ls'),
|
||||
'observations': [NullObservation('')],
|
||||
}, {
|
||||
'action': ModifyTaskAction(id='0.0', state='in_progress'),
|
||||
'observations': [NullObservation('')],
|
||||
}, {
|
||||
'action': AgentThinkAction(thought='Time to get started!'),
|
||||
'observations': [NullObservation('')],
|
||||
}, {
|
||||
'action': CmdRunAction(command='echo "foo"'),
|
||||
'observations': [CmdOutputObservation('foo', command_id=-1, command='echo "foo"')],
|
||||
}, {
|
||||
'action': FileWriteAction(content='echo "Hello, World!"', path='hello.sh'),
|
||||
'observations': [FileWriteObservation('', path='hello.sh')],
|
||||
}, {
|
||||
'action': FileReadAction(path='hello.sh'),
|
||||
'observations': [FileReadObservation('echo "Hello, World!"\n', path='hello.sh')],
|
||||
}, {
|
||||
'action': CmdRunAction(command='bash hello.sh'),
|
||||
'observations': [CmdOutputObservation('Hello, World!', command_id=-1, command='bash hello.sh')],
|
||||
}, {
|
||||
'action': CmdRunAction(command=BACKGROUND_CMD, background=True),
|
||||
'observations': [
|
||||
CmdOutputObservation('Background command started. To stop it, send a `kill` action with id 42', command_id='42', command=BACKGROUND_CMD), # type: ignore[arg-type]
|
||||
CmdOutputObservation('This is in the background\nThis too\n', command_id='42', command=BACKGROUND_CMD), # type: ignore[arg-type]
|
||||
]
|
||||
}, {
|
||||
'action': AgentRecallAction(query='who am I?'),
|
||||
'observations': [
|
||||
AgentRecallObservation('', memories=['I am a computer.']),
|
||||
# CmdOutputObservation('This too\n', command_id='42', command=BACKGROUND_CMD),
|
||||
],
|
||||
}, {
|
||||
'action': BrowseURLAction(url='https://google.com'),
|
||||
'observations': [
|
||||
# BrowserOutputObservation('<html></html>', url='https://google.com', screenshot=""),
|
||||
],
|
||||
}, {
|
||||
'action': AgentFinishAction(),
|
||||
'observations': [],
|
||||
}]
|
||||
self.steps: List[ActionObs] = [
|
||||
{
|
||||
'action': AddTaskAction(parent='0', goal='check the current directory'),
|
||||
'observations': [NullObservation('')],
|
||||
},
|
||||
{
|
||||
'action': AddTaskAction(parent='0.0', goal='run ls'),
|
||||
'observations': [NullObservation('')],
|
||||
},
|
||||
{
|
||||
'action': ModifyTaskAction(id='0.0', state='in_progress'),
|
||||
'observations': [NullObservation('')],
|
||||
},
|
||||
{
|
||||
'action': AgentThinkAction(thought='Time to get started!'),
|
||||
'observations': [NullObservation('')],
|
||||
},
|
||||
{
|
||||
'action': CmdRunAction(command='echo "foo"'),
|
||||
'observations': [
|
||||
CmdOutputObservation('foo', command_id=-1, command='echo "foo"')
|
||||
],
|
||||
},
|
||||
{
|
||||
'action': FileWriteAction(
|
||||
content='echo "Hello, World!"', path='hello.sh'
|
||||
),
|
||||
'observations': [FileWriteObservation('', path='hello.sh')],
|
||||
},
|
||||
{
|
||||
'action': FileReadAction(path='hello.sh'),
|
||||
'observations': [
|
||||
FileReadObservation('echo "Hello, World!"\n', path='hello.sh')
|
||||
],
|
||||
},
|
||||
{
|
||||
'action': CmdRunAction(command='bash hello.sh'),
|
||||
'observations': [
|
||||
CmdOutputObservation(
|
||||
'Hello, World!', command_id=-1, command='bash hello.sh'
|
||||
)
|
||||
],
|
||||
},
|
||||
{
|
||||
'action': CmdRunAction(command=BACKGROUND_CMD, background=True),
|
||||
'observations': [
|
||||
CmdOutputObservation(
|
||||
'Background command started. To stop it, send a `kill` action with id 42',
|
||||
command_id='42', # type: ignore[arg-type]
|
||||
command=BACKGROUND_CMD,
|
||||
),
|
||||
CmdOutputObservation(
|
||||
'This is in the background\nThis too\n',
|
||||
command_id='42', # type: ignore[arg-type]
|
||||
command=BACKGROUND_CMD,
|
||||
),
|
||||
],
|
||||
},
|
||||
{
|
||||
'action': AgentRecallAction(query='who am I?'),
|
||||
'observations': [
|
||||
AgentRecallObservation('', memories=['I am a computer.']),
|
||||
# CmdOutputObservation('This too\n', command_id='42', command=BACKGROUND_CMD),
|
||||
],
|
||||
},
|
||||
{
|
||||
'action': BrowseURLAction(url='https://google.com'),
|
||||
'observations': [
|
||||
# BrowserOutputObservation('<html></html>', url='https://google.com', screenshot=""),
|
||||
],
|
||||
},
|
||||
{
|
||||
'action': AgentFinishAction(),
|
||||
'observations': [],
|
||||
},
|
||||
]
|
||||
|
||||
def step(self, state: State) -> Action:
|
||||
time.sleep(0.1)
|
||||
@ -102,16 +135,24 @@ class DummyAgent(Agent):
|
||||
for i in range(len(expected_observations)):
|
||||
hist_obs = state.history[hist_start + i][1].to_dict()
|
||||
expected_obs = expected_observations[i].to_dict()
|
||||
if 'command_id' in hist_obs['extras'] and hist_obs['extras']['command_id'] != -1:
|
||||
if (
|
||||
'command_id' in hist_obs['extras']
|
||||
and hist_obs['extras']['command_id'] != -1
|
||||
):
|
||||
del hist_obs['extras']['command_id']
|
||||
hist_obs['content'] = ''
|
||||
if 'command_id' in expected_obs['extras'] and expected_obs['extras']['command_id'] != -1:
|
||||
if (
|
||||
'command_id' in expected_obs['extras']
|
||||
and expected_obs['extras']['command_id'] != -1
|
||||
):
|
||||
del expected_obs['extras']['command_id']
|
||||
expected_obs['content'] = ''
|
||||
if hist_obs != expected_obs:
|
||||
print('\nactual', hist_obs)
|
||||
print('\nexpect', expected_obs)
|
||||
assert hist_obs == expected_obs, f'Expected observation {expected_obs}, got {hist_obs}'
|
||||
assert (
|
||||
hist_obs == expected_obs
|
||||
), f'Expected observation {expected_obs}, got {hist_obs}'
|
||||
return self.steps[state.iteration]['action']
|
||||
|
||||
def search_memory(self, query: str) -> List[str]:
|
||||
|
||||
@ -3,11 +3,11 @@ from typing import Dict, List
|
||||
|
||||
from jinja2 import BaseLoader, Environment
|
||||
|
||||
from opendevin.agent import Agent
|
||||
from opendevin.controller.agent import Agent
|
||||
from opendevin.controller.state.state import State
|
||||
from opendevin.core.exceptions import LLMOutputError
|
||||
from opendevin.events.action import Action, action_from_dict
|
||||
from opendevin.exceptions import LLMOutputError
|
||||
from opendevin.llm.llm import LLM
|
||||
from opendevin.state import State
|
||||
|
||||
from .instructions import instructions
|
||||
from .registry import all_microagents
|
||||
@ -65,7 +65,8 @@ class MicroAgent(Agent):
|
||||
state=state,
|
||||
instructions=instructions,
|
||||
to_json=to_json,
|
||||
delegates=self.delegates)
|
||||
delegates=self.delegates,
|
||||
)
|
||||
messages = [{'content': prompt, 'role': 'user'}]
|
||||
resp = self.llm.completion(messages=messages)
|
||||
action_resp = resp['choices'][0]['message']['content']
|
||||
|
||||
@ -4,7 +4,7 @@ CommitWriterAgent can help write git commit message. Example:
|
||||
|
||||
```bash
|
||||
WORKSPACE_MOUNT_PATH="`PWD`" SANDBOX_TYPE="exec" \
|
||||
poetry run python opendevin/main.py -t "dummy task" -c CommitWriterAgent -d ./
|
||||
poetry run python opendevin/core/main.py -t "dummy task" -c CommitWriterAgent -d ./
|
||||
```
|
||||
|
||||
This agent is special in the sense that it doesn't need a task. Once called,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
from opendevin.agent import Agent
|
||||
from opendevin.controller.agent import Agent
|
||||
|
||||
from .agent import MonologueAgent
|
||||
|
||||
|
||||
@ -2,8 +2,12 @@ from typing import List
|
||||
|
||||
import agenthub.monologue_agent.utils.prompts as prompts
|
||||
from agenthub.monologue_agent.utils.monologue import Monologue
|
||||
from opendevin import config
|
||||
from opendevin.agent import Agent
|
||||
from opendevin.controller.agent import Agent
|
||||
from opendevin.controller.state.state import State
|
||||
from opendevin.core import config
|
||||
from opendevin.core.exceptions import AgentNoInstructionError
|
||||
from opendevin.core.schema import ActionType
|
||||
from opendevin.core.schema.config import ConfigType
|
||||
from opendevin.events.action import (
|
||||
Action,
|
||||
AgentRecallAction,
|
||||
@ -23,11 +27,7 @@ from opendevin.events.observation import (
|
||||
NullObservation,
|
||||
Observation,
|
||||
)
|
||||
from opendevin.exceptions import AgentNoInstructionError
|
||||
from opendevin.llm.llm import LLM
|
||||
from opendevin.schema import ActionType
|
||||
from opendevin.schema.config import ConfigType
|
||||
from opendevin.state import State
|
||||
|
||||
if config.get(ConfigType.AGENT_MEMORY_ENABLED):
|
||||
from agenthub.monologue_agent.utils.memory import LongTermMemory
|
||||
@ -56,7 +56,7 @@ INITIAL_THOUGHTS = [
|
||||
'RUN echo "hello world"',
|
||||
'hello world',
|
||||
'Cool! I bet I can write files too using the write action.',
|
||||
"WRITE echo \"console.log('hello world')\" > test.js",
|
||||
'WRITE echo "console.log(\'hello world\')" > test.js',
|
||||
'',
|
||||
"I just created test.js. I'll try and run it now.",
|
||||
'RUN node test.js',
|
||||
@ -173,8 +173,7 @@ class MonologueAgent(Agent):
|
||||
elif previous_action == ActionType.READ:
|
||||
observation = FileReadObservation(content=thought, path='')
|
||||
elif previous_action == ActionType.RECALL:
|
||||
observation = AgentRecallObservation(
|
||||
content=thought, memories=[])
|
||||
observation = AgentRecallObservation(content=thought, memories=[])
|
||||
elif previous_action == ActionType.BROWSE:
|
||||
observation = BrowserOutputObservation(
|
||||
content=thought, url='', screenshot=''
|
||||
|
||||
@ -13,9 +13,9 @@ from tenacity import (
|
||||
wait_random_exponential,
|
||||
)
|
||||
|
||||
from opendevin import config
|
||||
from opendevin.logger import opendevin_logger as logger
|
||||
from opendevin.schema.config import ConfigType
|
||||
from opendevin.core import config
|
||||
from opendevin.core.logger import opendevin_logger as logger
|
||||
from opendevin.core.schema.config import ConfigType
|
||||
|
||||
from . import json
|
||||
|
||||
@ -37,15 +37,22 @@ else:
|
||||
|
||||
|
||||
def attempt_on_error(retry_state):
|
||||
logger.error(f'{retry_state.outcome.exception()}. Attempt #{retry_state.attempt_number} | You can customize these settings in the configuration.', exc_info=False)
|
||||
logger.error(
|
||||
f'{retry_state.outcome.exception()}. Attempt #{retry_state.attempt_number} | You can customize these settings in the configuration.',
|
||||
exc_info=False,
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
@retry(reraise=True,
|
||||
stop=stop_after_attempt(num_retries),
|
||||
wait=wait_random_exponential(min=retry_min_wait, max=retry_max_wait),
|
||||
retry=retry_if_exception_type((RateLimitError, APIConnectionError, InternalServerError)),
|
||||
after=attempt_on_error)
|
||||
@retry(
|
||||
reraise=True,
|
||||
stop=stop_after_attempt(num_retries),
|
||||
wait=wait_random_exponential(min=retry_min_wait, max=retry_max_wait),
|
||||
retry=retry_if_exception_type(
|
||||
(RateLimitError, APIConnectionError, InternalServerError)
|
||||
),
|
||||
after=attempt_on_error,
|
||||
)
|
||||
def wrapper_get_embeddings(*args, **kwargs):
|
||||
return original_get_embeddings(*args, **kwargs)
|
||||
|
||||
@ -56,9 +63,16 @@ embedding_strategy = config.get(ConfigType.LLM_EMBEDDING_MODEL)
|
||||
|
||||
# TODO: More embeddings: https://docs.llamaindex.ai/en/stable/examples/embeddings/OpenAI/
|
||||
# There's probably a more programmatic way to do this.
|
||||
supported_ollama_embed_models = ['llama2', 'mxbai-embed-large', 'nomic-embed-text', 'all-minilm', 'stable-code']
|
||||
supported_ollama_embed_models = [
|
||||
'llama2',
|
||||
'mxbai-embed-large',
|
||||
'nomic-embed-text',
|
||||
'all-minilm',
|
||||
'stable-code',
|
||||
]
|
||||
if embedding_strategy in supported_ollama_embed_models:
|
||||
from llama_index.embeddings.ollama import OllamaEmbedding
|
||||
|
||||
embed_model = OllamaEmbedding(
|
||||
model_name=embedding_strategy,
|
||||
base_url=config.get(ConfigType.LLM_EMBEDDING_BASE_URL, required=True),
|
||||
@ -66,16 +80,20 @@ if embedding_strategy in supported_ollama_embed_models:
|
||||
)
|
||||
elif embedding_strategy == 'openai':
|
||||
from llama_index.embeddings.openai import OpenAIEmbedding
|
||||
|
||||
embed_model = OpenAIEmbedding(
|
||||
model='text-embedding-ada-002',
|
||||
api_key=config.get(ConfigType.LLM_API_KEY, required=True)
|
||||
api_key=config.get(ConfigType.LLM_API_KEY, required=True),
|
||||
)
|
||||
elif embedding_strategy == 'azureopenai':
|
||||
# Need to instruct to set these env variables in documentation
|
||||
from llama_index.embeddings.azure_openai import AzureOpenAIEmbedding
|
||||
|
||||
embed_model = AzureOpenAIEmbedding(
|
||||
model='text-embedding-ada-002',
|
||||
deployment_name=config.get(ConfigType.LLM_EMBEDDING_DEPLOYMENT_NAME, required=True),
|
||||
deployment_name=config.get(
|
||||
ConfigType.LLM_EMBEDDING_DEPLOYMENT_NAME, required=True
|
||||
),
|
||||
api_key=config.get(ConfigType.LLM_API_KEY, required=True),
|
||||
azure_endpoint=config.get(ConfigType.LLM_BASE_URL, required=True),
|
||||
api_version=config.get(ConfigType.LLM_API_VERSION, required=True),
|
||||
@ -87,9 +105,8 @@ elif (embedding_strategy is not None) and (embedding_strategy.lower() == 'none')
|
||||
embed_model = None
|
||||
else:
|
||||
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
|
||||
embed_model = HuggingFaceEmbedding(
|
||||
model_name='BAAI/bge-small-en-v1.5'
|
||||
)
|
||||
|
||||
embed_model = HuggingFaceEmbedding(model_name='BAAI/bge-small-en-v1.5')
|
||||
|
||||
|
||||
sema = threading.Semaphore(value=config.get(ConfigType.AGENT_MEMORY_MAX_THREADS))
|
||||
@ -109,7 +126,8 @@ class LongTermMemory:
|
||||
self.collection = db.get_or_create_collection(name='memories')
|
||||
vector_store = ChromaVectorStore(chroma_collection=self.collection)
|
||||
self.index = VectorStoreIndex.from_vector_store(
|
||||
vector_store, embed_model=embed_model)
|
||||
vector_store, embed_model=embed_model
|
||||
)
|
||||
self.thought_idx = 0
|
||||
self._add_threads = []
|
||||
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
|
||||
import agenthub.monologue_agent.utils.json as json
|
||||
import agenthub.monologue_agent.utils.prompts as prompts
|
||||
from opendevin.exceptions import AgentEventTypeError
|
||||
from opendevin.core.exceptions import AgentEventTypeError
|
||||
from opendevin.core.logger import opendevin_logger as logger
|
||||
from opendevin.llm.llm import LLM
|
||||
from opendevin.logger import opendevin_logger as logger
|
||||
|
||||
|
||||
class Monologue:
|
||||
|
||||
@ -2,7 +2,9 @@ import re
|
||||
from json import JSONDecodeError
|
||||
from typing import List
|
||||
|
||||
from opendevin import config
|
||||
from opendevin.core import config
|
||||
from opendevin.core.exceptions import LLMOutputError
|
||||
from opendevin.core.schema.config import ConfigType
|
||||
from opendevin.events.action import (
|
||||
Action,
|
||||
action_from_dict,
|
||||
@ -10,8 +12,6 @@ from opendevin.events.action import (
|
||||
from opendevin.events.observation import (
|
||||
CmdOutputObservation,
|
||||
)
|
||||
from opendevin.exceptions import LLMOutputError
|
||||
from opendevin.schema.config import ConfigType
|
||||
|
||||
from . import json
|
||||
|
||||
@ -158,7 +158,9 @@ def get_request_action_prompt(
|
||||
'hint': hint,
|
||||
'user': user,
|
||||
'timeout': config.get(ConfigType.SANDBOX_TIMEOUT),
|
||||
'WORKSPACE_MOUNT_PATH_IN_SANDBOX': config.get(ConfigType.WORKSPACE_MOUNT_PATH_IN_SANDBOX),
|
||||
'WORKSPACE_MOUNT_PATH_IN_SANDBOX': config.get(
|
||||
ConfigType.WORKSPACE_MOUNT_PATH_IN_SANDBOX
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@ -178,12 +180,18 @@ def parse_action_response(response: str) -> Action:
|
||||
# Find response-looking json in the output and use the more promising one. Helps with weak llms
|
||||
response_json_matches = re.finditer(
|
||||
r"""{\s*\"action\":\s?\"(\w+)\"(?:,?|,\s*\"args\":\s?{((?:.|\s)*?)})\s*}""",
|
||||
response) # Find all response-looking strings
|
||||
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
|
||||
action_dict = json.loads(
|
||||
max(response_json_matches, key=rank)[0]
|
||||
) # Use the highest ranked response
|
||||
except (ValueError, JSONDecodeError):
|
||||
raise LLMOutputError(
|
||||
'Invalid JSON, the response must be well-formed JSON as specified in the prompt.'
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
from opendevin.agent import Agent
|
||||
from opendevin.controller.agent import Agent
|
||||
|
||||
from .agent import PlannerAgent
|
||||
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
from typing import List
|
||||
|
||||
from opendevin.agent import Agent
|
||||
from opendevin.controller.agent import Agent
|
||||
from opendevin.controller.state.state import State
|
||||
from opendevin.events.action import Action, AgentFinishAction
|
||||
from opendevin.llm.llm import LLM
|
||||
from opendevin.state import State
|
||||
|
||||
from .prompt import get_prompt, parse_response
|
||||
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
import json
|
||||
from typing import Dict, List, Tuple, Type
|
||||
|
||||
from opendevin.controller.state.plan import Plan
|
||||
from opendevin.core.logger import opendevin_logger as logger
|
||||
from opendevin.core.schema import ActionType
|
||||
from opendevin.events.action import (
|
||||
Action,
|
||||
AddTaskAction,
|
||||
@ -21,9 +24,6 @@ from opendevin.events.observation import (
|
||||
NullObservation,
|
||||
Observation,
|
||||
)
|
||||
from opendevin.logger import opendevin_logger as logger
|
||||
from opendevin.plan import Plan
|
||||
from opendevin.schema import ActionType
|
||||
|
||||
ACTION_TYPE_TO_CLASS: Dict[str, Type[Action]] = {
|
||||
ActionType.RUN: CmdRunAction,
|
||||
@ -131,7 +131,7 @@ What is your next thought or action? Again, you must reply with JSON, and only w
|
||||
|
||||
|
||||
def get_hint(latest_action_id: str) -> str:
|
||||
""" Returns action type hint based on given action_id """
|
||||
"""Returns action type hint based on given action_id"""
|
||||
|
||||
hints = {
|
||||
'': "You haven't taken any actions yet. Start by using `ls` to check out what files you're working with.",
|
||||
|
||||
121
docs/modules/python/opendevin/controller/agent.md
Normal file
121
docs/modules/python/opendevin/controller/agent.md
Normal file
@ -0,0 +1,121 @@
|
||||
---
|
||||
sidebar_label: agent
|
||||
title: opendevin.controller.agent
|
||||
---
|
||||
|
||||
## Agent Objects
|
||||
|
||||
```python
|
||||
class Agent(ABC)
|
||||
```
|
||||
|
||||
This abstract base class is an general interface for an agent dedicated to
|
||||
executing a specific instruction and allowing human interaction with the
|
||||
agent during execution.
|
||||
It tracks the execution status and maintains a history of interactions.
|
||||
|
||||
#### complete
|
||||
|
||||
```python
|
||||
@property
|
||||
def complete() -> bool
|
||||
```
|
||||
|
||||
Indicates whether the current instruction execution is complete.
|
||||
|
||||
**Returns**:
|
||||
|
||||
- complete (bool): True if execution is complete; False otherwise.
|
||||
|
||||
#### step
|
||||
|
||||
```python
|
||||
@abstractmethod
|
||||
def step(state: 'State') -> 'Action'
|
||||
```
|
||||
|
||||
Starts the execution of the assigned instruction. This method should
|
||||
be implemented by subclasses to define the specific execution logic.
|
||||
|
||||
#### search\_memory
|
||||
|
||||
```python
|
||||
@abstractmethod
|
||||
def search_memory(query: str) -> List[str]
|
||||
```
|
||||
|
||||
Searches the agent's memory for information relevant to the given query.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
- query (str): The query to search for in the agent's memory.
|
||||
|
||||
|
||||
**Returns**:
|
||||
|
||||
- response (str): The response to the query.
|
||||
|
||||
#### reset
|
||||
|
||||
```python
|
||||
def reset() -> None
|
||||
```
|
||||
|
||||
Resets the agent's execution status and clears the history. This method can be used
|
||||
to prepare the agent for restarting the instruction or cleaning up before destruction.
|
||||
|
||||
#### register
|
||||
|
||||
```python
|
||||
@classmethod
|
||||
def register(cls, name: str, agent_cls: Type['Agent'])
|
||||
```
|
||||
|
||||
Registers an agent class in the registry.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
- name (str): The name to register the class under.
|
||||
- agent_cls (Type['Agent']): The class to register.
|
||||
|
||||
|
||||
**Raises**:
|
||||
|
||||
- AgentAlreadyRegisteredError: If name already registered
|
||||
|
||||
#### get\_cls
|
||||
|
||||
```python
|
||||
@classmethod
|
||||
def get_cls(cls, name: str) -> Type['Agent']
|
||||
```
|
||||
|
||||
Retrieves an agent class from the registry.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
- name (str): The name of the class to retrieve
|
||||
|
||||
|
||||
**Returns**:
|
||||
|
||||
- agent_cls (Type['Agent']): The class registered under the specified name.
|
||||
|
||||
|
||||
**Raises**:
|
||||
|
||||
- AgentNotRegisteredError: If name not registered
|
||||
|
||||
#### list\_agents
|
||||
|
||||
```python
|
||||
@classmethod
|
||||
def list_agents(cls) -> list[str]
|
||||
```
|
||||
|
||||
Retrieves the list of all agent names from the registry.
|
||||
|
||||
**Raises**:
|
||||
|
||||
- AgentNotRegisteredError: If no agent is registered
|
||||
|
||||
182
docs/modules/python/opendevin/controller/state/plan.md
Normal file
182
docs/modules/python/opendevin/controller/state/plan.md
Normal file
@ -0,0 +1,182 @@
|
||||
---
|
||||
sidebar_label: plan
|
||||
title: opendevin.controller.state.plan
|
||||
---
|
||||
|
||||
## Task Objects
|
||||
|
||||
```python
|
||||
class Task()
|
||||
```
|
||||
|
||||
#### \_\_init\_\_
|
||||
|
||||
```python
|
||||
def __init__(parent: 'Task | None',
|
||||
goal: str,
|
||||
state: str = OPEN_STATE,
|
||||
subtasks: List = [])
|
||||
```
|
||||
|
||||
Initializes a new instance of the Task class.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
- `parent` - The parent task, or None if it is the root task.
|
||||
- `goal` - The goal of the task.
|
||||
- `state` - The initial state of the task.
|
||||
- `subtasks` - A list of subtasks associated with this task.
|
||||
|
||||
#### to\_string
|
||||
|
||||
```python
|
||||
def to_string(indent='')
|
||||
```
|
||||
|
||||
Returns a string representation of the task and its subtasks.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
- `indent` - The indentation string for formatting the output.
|
||||
|
||||
|
||||
**Returns**:
|
||||
|
||||
A string representation of the task and its subtasks.
|
||||
|
||||
#### to\_dict
|
||||
|
||||
```python
|
||||
def to_dict()
|
||||
```
|
||||
|
||||
Returns a dictionary representation of the task.
|
||||
|
||||
**Returns**:
|
||||
|
||||
A dictionary containing the task's attributes.
|
||||
|
||||
#### set\_state
|
||||
|
||||
```python
|
||||
def set_state(state)
|
||||
```
|
||||
|
||||
Sets the state of the task and its subtasks.
|
||||
|
||||
Args: state: The new state of the task.
|
||||
|
||||
**Raises**:
|
||||
|
||||
- `PlanInvalidStateError` - If the provided state is invalid.
|
||||
|
||||
#### get\_current\_task
|
||||
|
||||
```python
|
||||
def get_current_task() -> 'Task | None'
|
||||
```
|
||||
|
||||
Retrieves the current task in progress.
|
||||
|
||||
**Returns**:
|
||||
|
||||
The current task in progress, or None if no task is in progress.
|
||||
|
||||
## Plan Objects
|
||||
|
||||
```python
|
||||
class Plan()
|
||||
```
|
||||
|
||||
Represents a plan consisting of tasks.
|
||||
|
||||
**Attributes**:
|
||||
|
||||
- `main_goal` - The main goal of the plan.
|
||||
- `task` - The root task of the plan.
|
||||
|
||||
#### \_\_init\_\_
|
||||
|
||||
```python
|
||||
def __init__(task: str)
|
||||
```
|
||||
|
||||
Initializes a new instance of the Plan class.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
- `task` - The main goal of the plan.
|
||||
|
||||
#### \_\_str\_\_
|
||||
|
||||
```python
|
||||
def __str__()
|
||||
```
|
||||
|
||||
Returns a string representation of the plan.
|
||||
|
||||
**Returns**:
|
||||
|
||||
A string representation of the plan.
|
||||
|
||||
#### get\_task\_by\_id
|
||||
|
||||
```python
|
||||
def get_task_by_id(id: str) -> Task
|
||||
```
|
||||
|
||||
Retrieves a task by its ID.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
- `id` - The ID of the task.
|
||||
|
||||
|
||||
**Returns**:
|
||||
|
||||
The task with the specified ID.
|
||||
|
||||
|
||||
**Raises**:
|
||||
|
||||
- `ValueError` - If the provided task ID is invalid or does not exist.
|
||||
|
||||
#### add\_subtask
|
||||
|
||||
```python
|
||||
def add_subtask(parent_id: str, goal: str, subtasks: List = [])
|
||||
```
|
||||
|
||||
Adds a subtask to a parent task.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
- `parent_id` - The ID of the parent task.
|
||||
- `goal` - The goal of the subtask.
|
||||
- `subtasks` - A list of subtasks associated with the new subtask.
|
||||
|
||||
#### set\_subtask\_state
|
||||
|
||||
```python
|
||||
def set_subtask_state(id: str, state: str)
|
||||
```
|
||||
|
||||
Sets the state of a subtask.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
- `id` - The ID of the subtask.
|
||||
- `state` - The new state of the subtask.
|
||||
|
||||
#### get\_current\_task
|
||||
|
||||
```python
|
||||
def get_current_task()
|
||||
```
|
||||
|
||||
Retrieves the current task in progress.
|
||||
|
||||
**Returns**:
|
||||
|
||||
The current task in progress, or None if no task is in progress.
|
||||
|
||||
13
docs/modules/python/opendevin/core/config.md
Normal file
13
docs/modules/python/opendevin/core/config.md
Normal file
@ -0,0 +1,13 @@
|
||||
---
|
||||
sidebar_label: config
|
||||
title: opendevin.core.config
|
||||
---
|
||||
|
||||
#### get
|
||||
|
||||
```python
|
||||
def get(key: ConfigType, required: bool = False)
|
||||
```
|
||||
|
||||
Get a key from the environment variables or config.toml or default configs.
|
||||
|
||||
92
docs/modules/python/opendevin/core/logger.md
Normal file
92
docs/modules/python/opendevin/core/logger.md
Normal file
@ -0,0 +1,92 @@
|
||||
---
|
||||
sidebar_label: logger
|
||||
title: opendevin.core.logger
|
||||
---
|
||||
|
||||
#### get\_console\_handler
|
||||
|
||||
```python
|
||||
def get_console_handler()
|
||||
```
|
||||
|
||||
Returns a console handler for logging.
|
||||
|
||||
#### get\_file\_handler
|
||||
|
||||
```python
|
||||
def get_file_handler()
|
||||
```
|
||||
|
||||
Returns a file handler for logging.
|
||||
|
||||
#### log\_uncaught\_exceptions
|
||||
|
||||
```python
|
||||
def log_uncaught_exceptions(ex_cls, ex, tb)
|
||||
```
|
||||
|
||||
Logs uncaught exceptions along with the traceback.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
- `ex_cls` _type_ - The type of the exception.
|
||||
- `ex` _Exception_ - The exception instance.
|
||||
- `tb` _traceback_ - The traceback object.
|
||||
|
||||
|
||||
**Returns**:
|
||||
|
||||
None
|
||||
|
||||
## LlmFileHandler Objects
|
||||
|
||||
```python
|
||||
class LlmFileHandler(logging.FileHandler)
|
||||
```
|
||||
|
||||
__LLM prompt and response logging__
|
||||
|
||||
|
||||
#### \_\_init\_\_
|
||||
|
||||
```python
|
||||
def __init__(filename, mode='a', encoding='utf-8', delay=False)
|
||||
```
|
||||
|
||||
Initializes an instance of LlmFileHandler.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
- `filename` _str_ - The name of the log file.
|
||||
- `mode` _str, optional_ - The file mode. Defaults to 'a'.
|
||||
- `encoding` _str, optional_ - The file encoding. Defaults to None.
|
||||
- `delay` _bool, optional_ - Whether to delay file opening. Defaults to False.
|
||||
|
||||
#### emit
|
||||
|
||||
```python
|
||||
def emit(record)
|
||||
```
|
||||
|
||||
Emits a log record.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
- `record` _logging.LogRecord_ - The log record to emit.
|
||||
|
||||
#### get\_llm\_prompt\_file\_handler
|
||||
|
||||
```python
|
||||
def get_llm_prompt_file_handler()
|
||||
```
|
||||
|
||||
Returns a file handler for LLM prompt logging.
|
||||
|
||||
#### get\_llm\_response\_file\_handler
|
||||
|
||||
```python
|
||||
def get_llm_response_file_handler()
|
||||
```
|
||||
|
||||
Returns a file handler for LLM response logging.
|
||||
|
||||
29
docs/modules/python/opendevin/core/main.md
Normal file
29
docs/modules/python/opendevin/core/main.md
Normal file
@ -0,0 +1,29 @@
|
||||
---
|
||||
sidebar_label: main
|
||||
title: opendevin.core.main
|
||||
---
|
||||
|
||||
#### read\_task\_from\_file
|
||||
|
||||
```python
|
||||
def read_task_from_file(file_path: str) -> str
|
||||
```
|
||||
|
||||
Read task from the specified file.
|
||||
|
||||
#### read\_task\_from\_stdin
|
||||
|
||||
```python
|
||||
def read_task_from_stdin() -> str
|
||||
```
|
||||
|
||||
Read task from stdin.
|
||||
|
||||
#### main
|
||||
|
||||
```python
|
||||
async def main(task_str: str = '')
|
||||
```
|
||||
|
||||
Main coroutine to run the agent controller with task input flexibility.
|
||||
|
||||
88
docs/modules/python/opendevin/core/schema/action.md
Normal file
88
docs/modules/python/opendevin/core/schema/action.md
Normal file
@ -0,0 +1,88 @@
|
||||
---
|
||||
sidebar_label: action
|
||||
title: opendevin.core.schema.action
|
||||
---
|
||||
|
||||
## ActionTypeSchema Objects
|
||||
|
||||
```python
|
||||
class ActionTypeSchema(BaseModel)
|
||||
```
|
||||
|
||||
#### INIT
|
||||
|
||||
Initializes the agent. Only sent by client.
|
||||
|
||||
#### USER\_MESSAGE
|
||||
|
||||
Sends a message from the user. Only sent by the client.
|
||||
|
||||
#### START
|
||||
|
||||
Starts a new development task OR send chat from the user. Only sent by the client.
|
||||
|
||||
#### READ
|
||||
|
||||
Reads the content of a file.
|
||||
|
||||
#### WRITE
|
||||
|
||||
Writes the content to a file.
|
||||
|
||||
#### RUN
|
||||
|
||||
Runs a command.
|
||||
|
||||
#### RUN\_IPYTHON
|
||||
|
||||
Runs a IPython cell.
|
||||
|
||||
#### KILL
|
||||
|
||||
Kills a background command.
|
||||
|
||||
#### BROWSE
|
||||
|
||||
Opens a web page.
|
||||
|
||||
#### RECALL
|
||||
|
||||
Searches long-term memory
|
||||
|
||||
#### THINK
|
||||
|
||||
Allows the agent to make a plan, set a goal, or record thoughts
|
||||
|
||||
#### TALK
|
||||
|
||||
Allows the agent to respond to the user.
|
||||
|
||||
#### DELEGATE
|
||||
|
||||
Delegates a task to another agent.
|
||||
|
||||
#### FINISH
|
||||
|
||||
If you're absolutely certain that you've completed your task and have tested your work,
|
||||
use the finish action to stop working.
|
||||
|
||||
#### PAUSE
|
||||
|
||||
Pauses the task.
|
||||
|
||||
#### RESUME
|
||||
|
||||
Resumes the task.
|
||||
|
||||
#### STOP
|
||||
|
||||
Stops the task. Must send a start action to restart a new task.
|
||||
|
||||
#### PUSH
|
||||
|
||||
Push a branch to github.
|
||||
|
||||
#### SEND\_PR
|
||||
|
||||
Send a PR to github.
|
||||
|
||||
39
docs/modules/python/opendevin/core/schema/observation.md
Normal file
39
docs/modules/python/opendevin/core/schema/observation.md
Normal file
@ -0,0 +1,39 @@
|
||||
---
|
||||
sidebar_label: observation
|
||||
title: opendevin.core.schema.observation
|
||||
---
|
||||
|
||||
## ObservationTypeSchema Objects
|
||||
|
||||
```python
|
||||
class ObservationTypeSchema(BaseModel)
|
||||
```
|
||||
|
||||
#### READ
|
||||
|
||||
The content of a file
|
||||
|
||||
#### BROWSE
|
||||
|
||||
The HTML content of a URL
|
||||
|
||||
#### RUN
|
||||
|
||||
The output of a command
|
||||
|
||||
#### RUN\_IPYTHON
|
||||
|
||||
Runs a IPython cell.
|
||||
|
||||
#### RECALL
|
||||
|
||||
The result of a search
|
||||
|
||||
#### CHAT
|
||||
|
||||
A message from the user
|
||||
|
||||
#### DELEGATE
|
||||
|
||||
The result of a task delegated to another agent
|
||||
|
||||
61
docs/modules/python/opendevin/core/schema/task.md
Normal file
61
docs/modules/python/opendevin/core/schema/task.md
Normal file
@ -0,0 +1,61 @@
|
||||
---
|
||||
sidebar_label: task
|
||||
title: opendevin.core.schema.task
|
||||
---
|
||||
|
||||
## TaskState Objects
|
||||
|
||||
```python
|
||||
class TaskState(str, Enum)
|
||||
```
|
||||
|
||||
#### INIT
|
||||
|
||||
Initial state of the task.
|
||||
|
||||
#### RUNNING
|
||||
|
||||
The task is running.
|
||||
|
||||
#### AWAITING\_USER\_INPUT
|
||||
|
||||
The task is awaiting user input.
|
||||
|
||||
#### PAUSED
|
||||
|
||||
The task is paused.
|
||||
|
||||
#### STOPPED
|
||||
|
||||
The task is stopped.
|
||||
|
||||
#### FINISHED
|
||||
|
||||
The task is finished.
|
||||
|
||||
#### ERROR
|
||||
|
||||
An error occurred during the task.
|
||||
|
||||
## TaskStateAction Objects
|
||||
|
||||
```python
|
||||
class TaskStateAction(str, Enum)
|
||||
```
|
||||
|
||||
#### START
|
||||
|
||||
Starts the task.
|
||||
|
||||
#### PAUSE
|
||||
|
||||
Pauses the task.
|
||||
|
||||
#### RESUME
|
||||
|
||||
Resumes the task.
|
||||
|
||||
#### STOP
|
||||
|
||||
Stops the task.
|
||||
|
||||
20
docs/modules/python/opendevin/runtime/browser/browser_env.md
Normal file
20
docs/modules/python/opendevin/runtime/browser/browser_env.md
Normal file
@ -0,0 +1,20 @@
|
||||
---
|
||||
sidebar_label: browser_env
|
||||
title: opendevin.runtime.browser.browser_env
|
||||
---
|
||||
|
||||
## BrowserEnv Objects
|
||||
|
||||
```python
|
||||
class BrowserEnv()
|
||||
```
|
||||
|
||||
#### image\_to\_png\_base64\_url
|
||||
|
||||
```python
|
||||
@staticmethod
|
||||
def image_to_png_base64_url(image: np.ndarray | Image.Image)
|
||||
```
|
||||
|
||||
Convert a numpy array to a base64 encoded png image url.
|
||||
|
||||
BIN
docs/modules/python/opendevin/runtime/docker/process.md
Normal file
BIN
docs/modules/python/opendevin/runtime/docker/process.md
Normal file
Binary file not shown.
19
docs/modules/python/opendevin/runtime/e2b/sandbox.md
Normal file
19
docs/modules/python/opendevin/runtime/e2b/sandbox.md
Normal file
@ -0,0 +1,19 @@
|
||||
---
|
||||
sidebar_label: sandbox
|
||||
title: opendevin.runtime.e2b.sandbox
|
||||
---
|
||||
|
||||
## E2BBox Objects
|
||||
|
||||
```python
|
||||
class E2BBox(Sandbox)
|
||||
```
|
||||
|
||||
#### copy\_to
|
||||
|
||||
```python
|
||||
def copy_to(host_src: str, sandbox_dest: str, recursive: bool = False)
|
||||
```
|
||||
|
||||
Copies a local file or directory to the sandbox.
|
||||
|
||||
40
docs/modules/python/opendevin/runtime/files.md
Normal file
40
docs/modules/python/opendevin/runtime/files.md
Normal file
@ -0,0 +1,40 @@
|
||||
---
|
||||
sidebar_label: files
|
||||
title: opendevin.runtime.files
|
||||
---
|
||||
|
||||
## WorkspaceFile Objects
|
||||
|
||||
```python
|
||||
class WorkspaceFile()
|
||||
```
|
||||
|
||||
#### to\_dict
|
||||
|
||||
```python
|
||||
def to_dict() -> Dict[str, Any]
|
||||
```
|
||||
|
||||
Converts the File object to a dictionary.
|
||||
|
||||
**Returns**:
|
||||
|
||||
The dictionary representation of the File object.
|
||||
|
||||
#### get\_folder\_structure
|
||||
|
||||
```python
|
||||
def get_folder_structure(workdir: Path) -> WorkspaceFile
|
||||
```
|
||||
|
||||
Gets the folder structure of a directory.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
- `workdir` - The directory path.
|
||||
|
||||
|
||||
**Returns**:
|
||||
|
||||
The folder structure.
|
||||
|
||||
@ -0,0 +1,16 @@
|
||||
---
|
||||
sidebar_label: jupyter
|
||||
title: opendevin.runtime.plugins.jupyter
|
||||
---
|
||||
|
||||
## JupyterRequirement Objects
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class JupyterRequirement(PluginRequirement)
|
||||
```
|
||||
|
||||
#### host\_src
|
||||
|
||||
The directory of this file (sandbox/plugins/jupyter)
|
||||
|
||||
21
docs/modules/python/opendevin/runtime/plugins/mixin.md
Normal file
21
docs/modules/python/opendevin/runtime/plugins/mixin.md
Normal file
@ -0,0 +1,21 @@
|
||||
---
|
||||
sidebar_label: mixin
|
||||
title: opendevin.runtime.plugins.mixin
|
||||
---
|
||||
|
||||
## PluginMixin Objects
|
||||
|
||||
```python
|
||||
class PluginMixin()
|
||||
```
|
||||
|
||||
Mixin for Sandbox to support plugins.
|
||||
|
||||
#### init\_plugins
|
||||
|
||||
```python
|
||||
def init_plugins(requirements: List[PluginRequirement])
|
||||
```
|
||||
|
||||
Load a plugin into the sandbox.
|
||||
|
||||
14
docs/modules/python/opendevin/runtime/plugins/requirement.md
Normal file
14
docs/modules/python/opendevin/runtime/plugins/requirement.md
Normal file
@ -0,0 +1,14 @@
|
||||
---
|
||||
sidebar_label: requirement
|
||||
title: opendevin.runtime.plugins.requirement
|
||||
---
|
||||
|
||||
## PluginRequirement Objects
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class PluginRequirement()
|
||||
```
|
||||
|
||||
Requirement for a plugin.
|
||||
|
||||
13
docs/modules/python/opendevin/runtime/utils/system.md
Normal file
13
docs/modules/python/opendevin/runtime/utils/system.md
Normal file
@ -0,0 +1,13 @@
|
||||
---
|
||||
sidebar_label: system
|
||||
title: opendevin.runtime.utils.system
|
||||
---
|
||||
|
||||
#### find\_available\_tcp\_port
|
||||
|
||||
```python
|
||||
def find_available_tcp_port() -> int
|
||||
```
|
||||
|
||||
Find an available TCP port, return -1 if none available.
|
||||
|
||||
@ -71,29 +71,70 @@
|
||||
"items": [
|
||||
{
|
||||
"items": [
|
||||
"python/opendevin/action/__init__",
|
||||
"python/opendevin/action/base",
|
||||
"python/opendevin/action/fileop",
|
||||
"python/opendevin/action/github",
|
||||
"python/opendevin/action/tasks"
|
||||
],
|
||||
"label": "opendevin.action",
|
||||
"type": "category"
|
||||
},
|
||||
{
|
||||
"items": [
|
||||
"python/opendevin/browser/browser_env"
|
||||
],
|
||||
"label": "opendevin.browser",
|
||||
"type": "category"
|
||||
},
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"items": [
|
||||
"python/opendevin/controller/state/plan"
|
||||
],
|
||||
"label": "opendevin.controller.state",
|
||||
"type": "category"
|
||||
},
|
||||
"python/opendevin/controller/agent",
|
||||
"python/opendevin/controller/agent_controller"
|
||||
],
|
||||
"label": "opendevin.controller",
|
||||
"type": "category"
|
||||
},
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"items": [
|
||||
"python/opendevin/core/schema/action",
|
||||
"python/opendevin/core/schema/observation",
|
||||
"python/opendevin/core/schema/task"
|
||||
],
|
||||
"label": "opendevin.core.schema",
|
||||
"type": "category"
|
||||
},
|
||||
"python/opendevin/core/config",
|
||||
"python/opendevin/core/logger",
|
||||
"python/opendevin/core/main"
|
||||
],
|
||||
"label": "opendevin.core",
|
||||
"type": "category"
|
||||
},
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"items": [
|
||||
"python/opendevin/events/action/__init__",
|
||||
"python/opendevin/events/action/empty",
|
||||
"python/opendevin/events/action/files",
|
||||
"python/opendevin/events/action/github",
|
||||
"python/opendevin/events/action/tasks"
|
||||
],
|
||||
"label": "opendevin.events.action",
|
||||
"type": "category"
|
||||
},
|
||||
{
|
||||
"items": [
|
||||
"python/opendevin/events/observation/__init__",
|
||||
"python/opendevin/events/observation/browse",
|
||||
"python/opendevin/events/observation/commands",
|
||||
"python/opendevin/events/observation/delegate",
|
||||
"python/opendevin/events/observation/empty",
|
||||
"python/opendevin/events/observation/error",
|
||||
"python/opendevin/events/observation/files",
|
||||
"python/opendevin/events/observation/message",
|
||||
"python/opendevin/events/observation/observation",
|
||||
"python/opendevin/events/observation/recall"
|
||||
],
|
||||
"label": "opendevin.events.observation",
|
||||
"type": "category"
|
||||
}
|
||||
],
|
||||
"label": "opendevin.events",
|
||||
"type": "category"
|
||||
},
|
||||
{
|
||||
"items": [
|
||||
"python/opendevin/llm/llm"
|
||||
@ -101,63 +142,54 @@
|
||||
"label": "opendevin.llm",
|
||||
"type": "category"
|
||||
},
|
||||
{
|
||||
"items": [
|
||||
"python/opendevin/observation/__init__",
|
||||
"python/opendevin/observation/base",
|
||||
"python/opendevin/observation/browse",
|
||||
"python/opendevin/observation/delegate",
|
||||
"python/opendevin/observation/error",
|
||||
"python/opendevin/observation/files",
|
||||
"python/opendevin/observation/message",
|
||||
"python/opendevin/observation/recall",
|
||||
"python/opendevin/observation/run"
|
||||
],
|
||||
"label": "opendevin.observation",
|
||||
"type": "category"
|
||||
},
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"items": [
|
||||
"python/opendevin/sandbox/docker/process"
|
||||
"python/opendevin/runtime/browser/browser_env"
|
||||
],
|
||||
"label": "opendevin.sandbox.docker",
|
||||
"label": "opendevin.runtime.browser",
|
||||
"type": "category"
|
||||
},
|
||||
{
|
||||
"items": [
|
||||
"python/opendevin/sandbox/e2b/sandbox"
|
||||
"python/opendevin/runtime/docker/process"
|
||||
],
|
||||
"label": "opendevin.sandbox.e2b",
|
||||
"label": "opendevin.runtime.docker",
|
||||
"type": "category"
|
||||
},
|
||||
{
|
||||
"items": [
|
||||
"python/opendevin/runtime/e2b/sandbox"
|
||||
],
|
||||
"label": "opendevin.runtime.e2b",
|
||||
"type": "category"
|
||||
},
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"items": [
|
||||
"python/opendevin/sandbox/plugins/jupyter/__init__"
|
||||
"python/opendevin/runtime/plugins/jupyter/__init__"
|
||||
],
|
||||
"label": "opendevin.sandbox.plugins.jupyter",
|
||||
"label": "opendevin.runtime.plugins.jupyter",
|
||||
"type": "category"
|
||||
},
|
||||
"python/opendevin/sandbox/plugins/mixin",
|
||||
"python/opendevin/sandbox/plugins/requirement"
|
||||
"python/opendevin/runtime/plugins/mixin",
|
||||
"python/opendevin/runtime/plugins/requirement"
|
||||
],
|
||||
"label": "opendevin.sandbox.plugins",
|
||||
"label": "opendevin.runtime.plugins",
|
||||
"type": "category"
|
||||
}
|
||||
},
|
||||
{
|
||||
"items": [
|
||||
"python/opendevin/runtime/utils/system"
|
||||
],
|
||||
"label": "opendevin.runtime.utils",
|
||||
"type": "category"
|
||||
},
|
||||
"python/opendevin/runtime/files"
|
||||
],
|
||||
"label": "opendevin.sandbox",
|
||||
"type": "category"
|
||||
},
|
||||
{
|
||||
"items": [
|
||||
"python/opendevin/schema/action",
|
||||
"python/opendevin/schema/observation",
|
||||
"python/opendevin/schema/task"
|
||||
],
|
||||
"label": "opendevin.schema",
|
||||
"label": "opendevin.runtime",
|
||||
"type": "category"
|
||||
},
|
||||
{
|
||||
@ -190,20 +222,7 @@
|
||||
],
|
||||
"label": "opendevin.server",
|
||||
"type": "category"
|
||||
},
|
||||
{
|
||||
"items": [
|
||||
"python/opendevin/utils/system"
|
||||
],
|
||||
"label": "opendevin.utils",
|
||||
"type": "category"
|
||||
},
|
||||
"python/opendevin/agent",
|
||||
"python/opendevin/config",
|
||||
"python/opendevin/files",
|
||||
"python/opendevin/logger",
|
||||
"python/opendevin/main",
|
||||
"python/opendevin/plan"
|
||||
}
|
||||
],
|
||||
"label": "opendevin",
|
||||
"type": "category"
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
from typing import List
|
||||
|
||||
from opendevin import config
|
||||
from opendevin.core import config
|
||||
from opendevin.core.schema import ConfigType
|
||||
from opendevin.events.action import (
|
||||
Action,
|
||||
)
|
||||
@ -9,9 +10,14 @@ from opendevin.events.observation import (
|
||||
CmdOutputObservation,
|
||||
Observation,
|
||||
)
|
||||
from opendevin.sandbox import DockerExecBox, DockerSSHBox, E2BBox, LocalBox, Sandbox
|
||||
from opendevin.sandbox.plugins import PluginRequirement
|
||||
from opendevin.schema import ConfigType
|
||||
from opendevin.runtime import (
|
||||
DockerExecBox,
|
||||
DockerSSHBox,
|
||||
E2BBox,
|
||||
LocalBox,
|
||||
Sandbox,
|
||||
)
|
||||
from opendevin.runtime.plugins import PluginRequirement
|
||||
|
||||
|
||||
class ActionManager:
|
||||
|
||||
@ -2,11 +2,14 @@ from abc import ABC, abstractmethod
|
||||
from typing import TYPE_CHECKING, Dict, List, Type
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from opendevin.controller.state.state import State
|
||||
from opendevin.events.action import Action
|
||||
from opendevin.state import State
|
||||
from opendevin.exceptions import AgentAlreadyRegisteredError, AgentNotRegisteredError
|
||||
from opendevin.core.exceptions import (
|
||||
AgentAlreadyRegisteredError,
|
||||
AgentNotRegisteredError,
|
||||
)
|
||||
from opendevin.llm.llm import LLM
|
||||
from opendevin.sandbox.plugins import PluginRequirement
|
||||
from opendevin.runtime.plugins import PluginRequirement
|
||||
|
||||
|
||||
class Agent(ABC):
|
||||
@ -21,8 +24,8 @@ class Agent(ABC):
|
||||
sandbox_plugins: List[PluginRequirement] = []
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
llm: LLM,
|
||||
self,
|
||||
llm: LLM,
|
||||
):
|
||||
self.llm = llm
|
||||
self._complete = False
|
||||
@ -2,9 +2,20 @@ import asyncio
|
||||
from typing import Callable, List, Type
|
||||
|
||||
from agenthub.codeact_agent.codeact_agent import CodeActAgent
|
||||
from opendevin import config
|
||||
from opendevin.agent import Agent
|
||||
from opendevin.controller.action_manager import ActionManager
|
||||
from opendevin.controller.agent import Agent
|
||||
from opendevin.controller.state.plan import Plan
|
||||
from opendevin.controller.state.state import State
|
||||
from opendevin.core import config
|
||||
from opendevin.core.exceptions import (
|
||||
AgentMalformedActionError,
|
||||
AgentNoActionError,
|
||||
LLMOutputError,
|
||||
MaxCharsExceedError,
|
||||
)
|
||||
from opendevin.core.logger import opendevin_logger as logger
|
||||
from opendevin.core.schema import TaskState
|
||||
from opendevin.core.schema.config import ConfigType
|
||||
from opendevin.events.action import (
|
||||
Action,
|
||||
AgentDelegateAction,
|
||||
@ -20,21 +31,8 @@ from opendevin.events.observation import (
|
||||
Observation,
|
||||
UserMessageObservation,
|
||||
)
|
||||
from opendevin.agent import Agent
|
||||
from opendevin.browser.browser_env import BrowserEnv
|
||||
from opendevin.controller.action_manager import ActionManager
|
||||
from opendevin.exceptions import (
|
||||
AgentMalformedActionError,
|
||||
AgentNoActionError,
|
||||
LLMOutputError,
|
||||
MaxCharsExceedError,
|
||||
)
|
||||
from opendevin.logger import opendevin_logger as logger
|
||||
from opendevin.plan import Plan
|
||||
from opendevin.sandbox import DockerSSHBox
|
||||
from opendevin.schema import TaskState
|
||||
from opendevin.schema.config import ConfigType
|
||||
from opendevin.state import State
|
||||
from opendevin.runtime import DockerSSHBox
|
||||
from opendevin.runtime.browser.browser_env import BrowserEnv
|
||||
|
||||
MAX_ITERATIONS = config.get(ConfigType.MAX_ITERATIONS)
|
||||
MAX_CHARS = config.get(ConfigType.MAX_CHARS)
|
||||
@ -82,7 +80,6 @@ class AgentController:
|
||||
# Initialize browser environment
|
||||
self.browser = BrowserEnv()
|
||||
|
||||
|
||||
if isinstance(agent, CodeActAgent) and not isinstance(
|
||||
self.action_manager.sandbox, DockerSSHBox
|
||||
):
|
||||
|
||||
@ -1,15 +1,20 @@
|
||||
from typing import List
|
||||
|
||||
from opendevin.exceptions import PlanInvalidStateError
|
||||
from opendevin.logger import opendevin_logger as logger
|
||||
from opendevin.core.exceptions import PlanInvalidStateError
|
||||
from opendevin.core.logger import opendevin_logger as logger
|
||||
|
||||
OPEN_STATE = 'open'
|
||||
COMPLETED_STATE = 'completed'
|
||||
ABANDONED_STATE = 'abandoned'
|
||||
IN_PROGRESS_STATE = 'in_progress'
|
||||
VERIFIED_STATE = 'verified'
|
||||
STATES = [OPEN_STATE, COMPLETED_STATE,
|
||||
ABANDONED_STATE, IN_PROGRESS_STATE, VERIFIED_STATE]
|
||||
STATES = [
|
||||
OPEN_STATE,
|
||||
COMPLETED_STATE,
|
||||
ABANDONED_STATE,
|
||||
IN_PROGRESS_STATE,
|
||||
VERIFIED_STATE,
|
||||
]
|
||||
|
||||
|
||||
class Task:
|
||||
@ -18,7 +23,13 @@ class Task:
|
||||
parent: 'Task | None'
|
||||
subtasks: List['Task']
|
||||
|
||||
def __init__(self, parent: 'Task | None', goal: str, state: str = OPEN_STATE, subtasks: List = []):
|
||||
def __init__(
|
||||
self,
|
||||
parent: 'Task | None',
|
||||
goal: str,
|
||||
state: str = OPEN_STATE,
|
||||
subtasks: List = [],
|
||||
):
|
||||
"""Initializes a new instance of the Task class.
|
||||
|
||||
Args:
|
||||
@ -34,7 +45,7 @@ class Task:
|
||||
self.parent = parent
|
||||
self.goal = goal
|
||||
self.subtasks = []
|
||||
for subtask in (subtasks or []):
|
||||
for subtask in subtasks or []:
|
||||
if isinstance(subtask, Task):
|
||||
self.subtasks.append(subtask)
|
||||
else:
|
||||
@ -80,7 +91,7 @@ class Task:
|
||||
'id': self.id,
|
||||
'goal': self.goal,
|
||||
'state': self.state,
|
||||
'subtasks': [t.to_dict() for t in self.subtasks]
|
||||
'subtasks': [t.to_dict() for t in self.subtasks],
|
||||
}
|
||||
|
||||
def set_state(self, state):
|
||||
@ -95,7 +106,11 @@ class Task:
|
||||
logger.error('Invalid state: %s', state)
|
||||
raise PlanInvalidStateError(state)
|
||||
self.state = state
|
||||
if state == COMPLETED_STATE or state == ABANDONED_STATE or state == VERIFIED_STATE:
|
||||
if (
|
||||
state == COMPLETED_STATE
|
||||
or state == ABANDONED_STATE
|
||||
or state == VERIFIED_STATE
|
||||
):
|
||||
for subtask in self.subtasks:
|
||||
if subtask.state != ABANDONED_STATE:
|
||||
subtask.set_state(state)
|
||||
@ -124,6 +139,7 @@ class Plan:
|
||||
main_goal: The main goal of the plan.
|
||||
task: The root task of the plan.
|
||||
"""
|
||||
|
||||
main_goal: str
|
||||
task: Task
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Dict, List, Tuple
|
||||
|
||||
from opendevin.controller.state.plan import Plan
|
||||
from opendevin.events.action import (
|
||||
Action,
|
||||
)
|
||||
@ -8,7 +9,6 @@ from opendevin.events.observation import (
|
||||
CmdOutputObservation,
|
||||
Observation,
|
||||
)
|
||||
from opendevin.plan import Plan
|
||||
|
||||
|
||||
@dataclass
|
||||
@ -17,10 +17,8 @@ class State:
|
||||
iteration: int = 0
|
||||
# number of characters we have sent to and received from LLM so far for current task
|
||||
num_of_chars: int = 0
|
||||
background_commands_obs: List[CmdOutputObservation] = field(
|
||||
default_factory=list)
|
||||
background_commands_obs: List[CmdOutputObservation] = field(default_factory=list)
|
||||
history: List[Tuple[Action, Observation]] = field(default_factory=list)
|
||||
updated_info: List[Tuple[Action, Observation]
|
||||
] = field(default_factory=list)
|
||||
updated_info: List[Tuple[Action, Observation]] = field(default_factory=list)
|
||||
inputs: Dict = field(default_factory=dict)
|
||||
outputs: Dict = field(default_factory=dict)
|
||||
@ -7,7 +7,7 @@ import platform
|
||||
import toml
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from opendevin.schema import ConfigType
|
||||
from opendevin.core.schema import ConfigType
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -55,7 +55,7 @@ DEFAULT_CONFIG: dict = {
|
||||
ConfigType.SANDBOX_USER_ID: os.getuid() if hasattr(os, 'getuid') else None,
|
||||
ConfigType.SANDBOX_TIMEOUT: 120,
|
||||
ConfigType.GITHUB_TOKEN: None,
|
||||
ConfigType.SANDBOX_USER_ID: None
|
||||
ConfigType.SANDBOX_USER_ID: None,
|
||||
}
|
||||
|
||||
config_str = ''
|
||||
@ -69,7 +69,9 @@ def int_value(value, default, config_key):
|
||||
try:
|
||||
return int(value)
|
||||
except ValueError:
|
||||
logger.warning(f'Invalid value for {config_key}: {value} not applied. Using default value {default}')
|
||||
logger.warning(
|
||||
f'Invalid value for {config_key}: {value} not applied. Using default value {default}'
|
||||
)
|
||||
return default
|
||||
|
||||
|
||||
@ -80,16 +82,22 @@ for k, v in config.items():
|
||||
config[k] = os.environ[k]
|
||||
elif k in tomlConfig:
|
||||
config[k] = tomlConfig[k]
|
||||
if k in [ConfigType.LLM_NUM_RETRIES, ConfigType.LLM_RETRY_MIN_WAIT, ConfigType.LLM_RETRY_MAX_WAIT]:
|
||||
if k in [
|
||||
ConfigType.LLM_NUM_RETRIES,
|
||||
ConfigType.LLM_RETRY_MIN_WAIT,
|
||||
ConfigType.LLM_RETRY_MAX_WAIT,
|
||||
]:
|
||||
config[k] = int_value(config[k], v, config_key=k)
|
||||
|
||||
# In local there is no sandbox, the workspace will have the same pwd as the host
|
||||
if config[ConfigType.SANDBOX_TYPE] == 'local':
|
||||
config[ConfigType.WORKSPACE_MOUNT_PATH_IN_SANDBOX] = config[ConfigType.WORKSPACE_MOUNT_PATH]
|
||||
config[ConfigType.WORKSPACE_MOUNT_PATH_IN_SANDBOX] = config[
|
||||
ConfigType.WORKSPACE_MOUNT_PATH
|
||||
]
|
||||
|
||||
|
||||
def get_parser():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Run an agent with a specific task')
|
||||
parser = argparse.ArgumentParser(description='Run an agent with a specific task')
|
||||
parser.add_argument(
|
||||
'-d',
|
||||
'--directory',
|
||||
@ -149,13 +157,17 @@ args = parse_arguments()
|
||||
|
||||
|
||||
def finalize_config():
|
||||
if config.get(ConfigType.WORKSPACE_MOUNT_REWRITE) and not config.get(ConfigType.WORKSPACE_MOUNT_PATH):
|
||||
if config.get(ConfigType.WORKSPACE_MOUNT_REWRITE) and not config.get(
|
||||
ConfigType.WORKSPACE_MOUNT_PATH
|
||||
):
|
||||
base = config.get(ConfigType.WORKSPACE_BASE) or os.getcwd()
|
||||
parts = config[ConfigType.WORKSPACE_MOUNT_REWRITE].split(':')
|
||||
config[ConfigType.WORKSPACE_MOUNT_PATH] = base.replace(parts[0], parts[1])
|
||||
|
||||
if config.get(ConfigType.WORKSPACE_MOUNT_PATH) is None:
|
||||
config[ConfigType.WORKSPACE_MOUNT_PATH] = os.path.abspath(config[ConfigType.WORKSPACE_BASE])
|
||||
config[ConfigType.WORKSPACE_MOUNT_PATH] = os.path.abspath(
|
||||
config[ConfigType.WORKSPACE_BASE]
|
||||
)
|
||||
|
||||
if config.get(ConfigType.LLM_EMBEDDING_BASE_URL) is None:
|
||||
config[ConfigType.LLM_EMBEDDING_BASE_URL] = config.get(ConfigType.LLM_BASE_URL)
|
||||
@ -171,6 +183,7 @@ def finalize_config():
|
||||
if config.get(ConfigType.WORKSPACE_MOUNT_PATH) is None:
|
||||
config[ConfigType.WORKSPACE_MOUNT_PATH] = config.get(ConfigType.WORKSPACE_BASE)
|
||||
|
||||
|
||||
finalize_config()
|
||||
|
||||
|
||||
@ -7,12 +7,10 @@ from typing import Literal, Mapping
|
||||
|
||||
from termcolor import colored
|
||||
|
||||
from opendevin import config
|
||||
from opendevin.schema.config import ConfigType
|
||||
from opendevin.core import config
|
||||
from opendevin.core.schema.config import ConfigType
|
||||
|
||||
DISABLE_COLOR_PRINTING = (
|
||||
config.get(ConfigType.DISABLE_COLOR).lower() == 'true'
|
||||
)
|
||||
DISABLE_COLOR_PRINTING = config.get(ConfigType.DISABLE_COLOR).lower() == 'true'
|
||||
|
||||
ColorType = Literal[
|
||||
'red',
|
||||
@ -48,7 +46,9 @@ class ColoredFormatter(logging.Formatter):
|
||||
if msg_type in LOG_COLORS and not DISABLE_COLOR_PRINTING:
|
||||
msg_type_color = colored(msg_type, LOG_COLORS[msg_type])
|
||||
msg = colored(record.msg, LOG_COLORS[msg_type])
|
||||
time_str = colored(self.formatTime(record, self.datefmt), LOG_COLORS[msg_type])
|
||||
time_str = colored(
|
||||
self.formatTime(record, self.datefmt), LOG_COLORS[msg_type]
|
||||
)
|
||||
name_str = colored(record.name, 'cyan')
|
||||
level_str = colored(record.levelname, 'yellow')
|
||||
if msg_type in ['ERROR', 'INFO']:
|
||||
@ -69,9 +69,7 @@ file_formatter = logging.Formatter(
|
||||
'%(asctime)s - %(name)s:%(levelname)s: %(filename)s:%(lineno)s - %(message)s',
|
||||
datefmt='%H:%M:%S',
|
||||
)
|
||||
llm_formatter = logging.Formatter(
|
||||
'%(message)s'
|
||||
)
|
||||
llm_formatter = logging.Formatter('%(message)s')
|
||||
|
||||
|
||||
def get_console_handler():
|
||||
@ -126,8 +124,9 @@ opendevin_logger.addHandler(get_file_handler())
|
||||
opendevin_logger.addHandler(get_console_handler())
|
||||
opendevin_logger.propagate = False
|
||||
opendevin_logger.debug('Logging initialized')
|
||||
opendevin_logger.debug('Logging to %s', os.path.join(
|
||||
os.getcwd(), 'logs', 'opendevin.log'))
|
||||
opendevin_logger.debug(
|
||||
'Logging to %s', os.path.join(os.getcwd(), 'logs', 'opendevin.log')
|
||||
)
|
||||
|
||||
# Exclude LiteLLM from logging output
|
||||
logging.getLogger('LiteLLM').disabled = True
|
||||
@ -3,9 +3,9 @@ import sys
|
||||
from typing import Type
|
||||
|
||||
import agenthub # noqa F401 (we import this to get the agents registered)
|
||||
from opendevin.agent import Agent
|
||||
from opendevin.config import args
|
||||
from opendevin.controller import AgentController
|
||||
from opendevin.controller.agent import Agent
|
||||
from opendevin.core.config import args
|
||||
from opendevin.llm.llm import LLM
|
||||
|
||||
|
||||
@ -33,8 +33,7 @@ async def main(task_str: str = ''):
|
||||
elif not sys.stdin.isatty():
|
||||
task = read_task_from_stdin()
|
||||
else:
|
||||
raise ValueError(
|
||||
'No task provided. Please specify a task through -t, -f.')
|
||||
raise ValueError('No task provided. Please specify a task through -t, -f.')
|
||||
|
||||
print(
|
||||
f'Running agent {args.agent_cls} (model: {args.model_name}) with task: "{task}"'
|
||||
@ -3,4 +3,10 @@ from .config import ConfigType
|
||||
from .observation import ObservationType
|
||||
from .task import TaskState, TaskStateAction
|
||||
|
||||
__all__ = ['ActionType', 'ObservationType', 'ConfigType', 'TaskState', 'TaskStateAction']
|
||||
__all__ = [
|
||||
'ActionType',
|
||||
'ObservationType',
|
||||
'ConfigType',
|
||||
'TaskState',
|
||||
'TaskStateAction',
|
||||
]
|
||||
@ -1,8 +1,6 @@
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
__all__ = [
|
||||
'ActionType'
|
||||
]
|
||||
__all__ = ['ActionType']
|
||||
|
||||
|
||||
class ActionTypeSchema(BaseModel):
|
||||
@ -1,8 +1,6 @@
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
__all__ = [
|
||||
'ObservationType'
|
||||
]
|
||||
__all__ = ['ObservationType']
|
||||
|
||||
|
||||
class ObservationTypeSchema(BaseModel):
|
||||
@ -1,5 +1,4 @@
|
||||
|
||||
from opendevin.exceptions import AgentMalformedActionError
|
||||
from opendevin.core.exceptions import AgentMalformedActionError
|
||||
|
||||
from .action import Action
|
||||
from .agent import (
|
||||
@ -76,5 +75,5 @@ __all__ = [
|
||||
'AddTaskAction',
|
||||
'ModifyTaskAction',
|
||||
'TaskStateChangedAction',
|
||||
'IPythonRunCellAction'
|
||||
'IPythonRunCellAction',
|
||||
]
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
from dataclasses import dataclass, field
|
||||
from typing import TYPE_CHECKING, Dict
|
||||
|
||||
from opendevin.core.schema import ActionType
|
||||
from opendevin.events.observation import (
|
||||
AgentMessageObservation,
|
||||
AgentRecallObservation,
|
||||
NullObservation,
|
||||
Observation,
|
||||
)
|
||||
from opendevin.schema import ActionType
|
||||
|
||||
from .action import Action
|
||||
|
||||
|
||||
@ -2,9 +2,8 @@ import os
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
|
||||
from opendevin.core.schema import ActionType
|
||||
from opendevin.events.observation import BrowserOutputObservation
|
||||
from opendevin.schema import ActionType
|
||||
|
||||
from .action import Action
|
||||
|
||||
@ -33,7 +32,9 @@ class BrowseURLAction(Action):
|
||||
active_page_index=obs['active_page_index'], # index of the active page
|
||||
dom_object=obs['dom_object'], # DOM object
|
||||
axtree_object=obs['axtree_object'], # accessibility tree object
|
||||
last_browser_action=obs['last_action'], # last browser env action performed
|
||||
last_browser_action=obs[
|
||||
'last_action'
|
||||
], # last browser env action performed
|
||||
focused_element_bid=obs['focused_element_bid'], # focused element bid
|
||||
screenshot=obs['screenshot'], # base64-encoded screenshot, png
|
||||
url=asked_url,
|
||||
|
||||
@ -3,8 +3,8 @@ import pathlib
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from opendevin import config
|
||||
from opendevin.schema import ActionType, ConfigType
|
||||
from opendevin.core import config
|
||||
from opendevin.core.schema import ActionType, ConfigType
|
||||
|
||||
from .action import Action
|
||||
|
||||
@ -64,8 +64,7 @@ class IPythonRunCellAction(Action):
|
||||
# echo "import math" | execute_cli
|
||||
# write code to a temporary file and pass it to `execute_cli` via stdin
|
||||
tmp_filepath = os.path.join(
|
||||
config.get(ConfigType.WORKSPACE_BASE),
|
||||
'.tmp', '.ipython_execution_tmp.py'
|
||||
config.get(ConfigType.WORKSPACE_BASE), '.tmp', '.ipython_execution_tmp.py'
|
||||
)
|
||||
pathlib.Path(os.path.dirname(tmp_filepath)).mkdir(parents=True, exist_ok=True)
|
||||
with open(tmp_filepath, 'w') as tmp_file:
|
||||
@ -73,16 +72,13 @@ class IPythonRunCellAction(Action):
|
||||
|
||||
tmp_filepath_inside_sandbox = os.path.join(
|
||||
config.get(ConfigType.WORKSPACE_MOUNT_PATH_IN_SANDBOX),
|
||||
'.tmp', '.ipython_execution_tmp.py'
|
||||
'.tmp',
|
||||
'.ipython_execution_tmp.py',
|
||||
)
|
||||
obs = controller.action_manager.run_command(
|
||||
f'execute_cli < {tmp_filepath_inside_sandbox}',
|
||||
background=False
|
||||
)
|
||||
return IPythonRunCellObservation(
|
||||
content=obs.content,
|
||||
code=self.code
|
||||
f'execute_cli < {tmp_filepath_inside_sandbox}', background=False
|
||||
)
|
||||
return IPythonRunCellObservation(content=obs.content, code=self.code)
|
||||
|
||||
def __str__(self) -> str:
|
||||
ret = '**IPythonRunCellAction**\n'
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
from opendevin.schema import ActionType
|
||||
from opendevin.core.schema import ActionType
|
||||
|
||||
from .action import Action
|
||||
|
||||
|
||||
@dataclass
|
||||
class NullAction(Action):
|
||||
"""An action that does nothing.
|
||||
"""
|
||||
"""An action that does nothing."""
|
||||
|
||||
action: str = ActionType.NULL
|
||||
|
||||
@property
|
||||
|
||||
@ -2,16 +2,16 @@ import os
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
from opendevin import config
|
||||
from opendevin.core import config
|
||||
from opendevin.core.schema import ActionType
|
||||
from opendevin.core.schema.config import ConfigType
|
||||
from opendevin.events.observation import (
|
||||
AgentErrorObservation,
|
||||
FileReadObservation,
|
||||
FileWriteObservation,
|
||||
Observation,
|
||||
)
|
||||
from opendevin.sandbox import E2BBox
|
||||
from opendevin.schema import ActionType
|
||||
from opendevin.schema.config import ConfigType
|
||||
from opendevin.runtime import E2BBox
|
||||
|
||||
from .action import Action
|
||||
|
||||
@ -28,14 +28,20 @@ def resolve_path(file_path, working_directory):
|
||||
abs_path_in_sandbox = path_in_sandbox.resolve()
|
||||
|
||||
# If the path is outside the workspace, deny it
|
||||
if not abs_path_in_sandbox.is_relative_to(config.get(ConfigType.WORKSPACE_MOUNT_PATH_IN_SANDBOX)):
|
||||
if not abs_path_in_sandbox.is_relative_to(
|
||||
config.get(ConfigType.WORKSPACE_MOUNT_PATH_IN_SANDBOX)
|
||||
):
|
||||
raise PermissionError(f'File access not permitted: {file_path}')
|
||||
|
||||
# Get path relative to the root of the workspace inside the sandbox
|
||||
path_in_workspace = abs_path_in_sandbox.relative_to(Path(config.get(ConfigType.WORKSPACE_MOUNT_PATH_IN_SANDBOX)))
|
||||
path_in_workspace = abs_path_in_sandbox.relative_to(
|
||||
Path(config.get(ConfigType.WORKSPACE_MOUNT_PATH_IN_SANDBOX))
|
||||
)
|
||||
|
||||
# Get path relative to host
|
||||
path_in_host_workspace = Path(config.get(ConfigType.WORKSPACE_BASE)) / path_in_workspace
|
||||
path_in_host_workspace = (
|
||||
Path(config.get(ConfigType.WORKSPACE_BASE)) / path_in_workspace
|
||||
)
|
||||
|
||||
return path_in_host_workspace
|
||||
|
||||
@ -47,6 +53,7 @@ class FileReadAction(Action):
|
||||
Can be set to read specific lines using start and end
|
||||
Default lines 0:-1 (whole file)
|
||||
"""
|
||||
|
||||
path: str
|
||||
start: int = 0
|
||||
end: int = -1
|
||||
@ -58,7 +65,7 @@ class FileReadAction(Action):
|
||||
if self.start == 0:
|
||||
return all_lines
|
||||
else:
|
||||
return all_lines[self.start:]
|
||||
return all_lines[self.start :]
|
||||
else:
|
||||
num_lines = len(all_lines)
|
||||
begin = max(0, min(self.start, num_lines - 2))
|
||||
@ -67,13 +74,14 @@ class FileReadAction(Action):
|
||||
|
||||
async def run(self, controller) -> Observation:
|
||||
if isinstance(controller.action_manager.sandbox, E2BBox):
|
||||
content = controller.action_manager.sandbox.filesystem.read(
|
||||
self.path)
|
||||
content = controller.action_manager.sandbox.filesystem.read(self.path)
|
||||
read_lines = self._read_lines(content.split('\n'))
|
||||
code_view = ''.join(read_lines)
|
||||
else:
|
||||
try:
|
||||
whole_path = resolve_path(self.path, controller.action_manager.sandbox.get_working_directory())
|
||||
whole_path = resolve_path(
|
||||
self.path, controller.action_manager.sandbox.get_working_directory()
|
||||
)
|
||||
self.start = max(self.start, 0)
|
||||
try:
|
||||
with open(whole_path, 'r', encoding='utf-8') as file:
|
||||
@ -82,11 +90,17 @@ class FileReadAction(Action):
|
||||
except FileNotFoundError:
|
||||
return AgentErrorObservation(f'File not found: {self.path}')
|
||||
except UnicodeDecodeError:
|
||||
return AgentErrorObservation(f'File could not be decoded as utf-8: {self.path}')
|
||||
return AgentErrorObservation(
|
||||
f'File could not be decoded as utf-8: {self.path}'
|
||||
)
|
||||
except IsADirectoryError:
|
||||
return AgentErrorObservation(f'Path is a directory: {self.path}. You can only read files')
|
||||
return AgentErrorObservation(
|
||||
f'Path is a directory: {self.path}. You can only read files'
|
||||
)
|
||||
except PermissionError:
|
||||
return AgentErrorObservation(f'Malformed paths not permitted: {self.path}')
|
||||
return AgentErrorObservation(
|
||||
f'Malformed paths not permitted: {self.path}'
|
||||
)
|
||||
return FileReadObservation(path=self.path, content=code_view)
|
||||
|
||||
@property
|
||||
@ -107,9 +121,9 @@ class FileWriteAction(Action):
|
||||
"""
|
||||
Insert the new content to the original content based on self.start and self.end
|
||||
"""
|
||||
new_lines = [''] if self.start == 0 else original[:self.start]
|
||||
new_lines = [''] if self.start == 0 else original[: self.start]
|
||||
new_lines += [i + '\n' for i in to_insert]
|
||||
new_lines += [''] if self.end == -1 else original[self.end:]
|
||||
new_lines += [''] if self.end == -1 else original[self.end :]
|
||||
return new_lines
|
||||
|
||||
async def run(self, controller) -> Observation:
|
||||
@ -120,12 +134,16 @@ class FileWriteAction(Action):
|
||||
if self.path in files:
|
||||
all_lines = controller.action_manager.sandbox.filesystem.read(self.path)
|
||||
new_file = self._insert_lines(self.content.split('\n'), all_lines)
|
||||
controller.action_manager.sandbox.filesystem.write(self.path, ''.join(new_file))
|
||||
controller.action_manager.sandbox.filesystem.write(
|
||||
self.path, ''.join(new_file)
|
||||
)
|
||||
else:
|
||||
return AgentErrorObservation(f'File not found: {self.path}')
|
||||
else:
|
||||
try:
|
||||
whole_path = resolve_path(self.path, controller.action_manager.sandbox.get_working_directory())
|
||||
whole_path = resolve_path(
|
||||
self.path, controller.action_manager.sandbox.get_working_directory()
|
||||
)
|
||||
mode = 'w' if not os.path.exists(whole_path) else 'r+'
|
||||
try:
|
||||
with open(whole_path, mode, encoding='utf-8') as file:
|
||||
@ -141,11 +159,17 @@ class FileWriteAction(Action):
|
||||
except FileNotFoundError:
|
||||
return AgentErrorObservation(f'File not found: {self.path}')
|
||||
except IsADirectoryError:
|
||||
return AgentErrorObservation(f'Path is a directory: {self.path}. You can only write to files')
|
||||
return AgentErrorObservation(
|
||||
f'Path is a directory: {self.path}. You can only write to files'
|
||||
)
|
||||
except UnicodeDecodeError:
|
||||
return AgentErrorObservation(f'File could not be decoded as utf-8: {self.path}')
|
||||
return AgentErrorObservation(
|
||||
f'File could not be decoded as utf-8: {self.path}'
|
||||
)
|
||||
except PermissionError:
|
||||
return AgentErrorObservation(f'Malformed paths not permitted: {self.path}')
|
||||
return AgentErrorObservation(
|
||||
f'Malformed paths not permitted: {self.path}'
|
||||
)
|
||||
return FileWriteObservation(content='', path=self.path)
|
||||
|
||||
@property
|
||||
|
||||
@ -5,15 +5,15 @@ from typing import TYPE_CHECKING
|
||||
|
||||
import requests
|
||||
|
||||
from opendevin import config
|
||||
from opendevin.core import config
|
||||
from opendevin.core.schema import ActionType
|
||||
from opendevin.core.schema.config import ConfigType
|
||||
from opendevin.events.observation import (
|
||||
AgentErrorObservation,
|
||||
AgentMessageObservation,
|
||||
CmdOutputObservation,
|
||||
Observation,
|
||||
)
|
||||
from opendevin.schema import ActionType
|
||||
from opendevin.schema.config import ConfigType
|
||||
|
||||
from .action import Action
|
||||
|
||||
@ -44,9 +44,7 @@ class GitHubPushAction(Action):
|
||||
async def run(self, controller: 'AgentController') -> Observation:
|
||||
github_token = config.get(ConfigType.GITHUB_TOKEN)
|
||||
if not github_token:
|
||||
return AgentErrorObservation(
|
||||
'GITHUB_TOKEN is not set'
|
||||
)
|
||||
return AgentErrorObservation('GITHUB_TOKEN is not set')
|
||||
|
||||
# Create a random short string to use as a temporary remote
|
||||
random_remote = ''.join(
|
||||
@ -115,9 +113,7 @@ class GitHubSendPRAction(Action):
|
||||
async def run(self, controller: 'AgentController') -> Observation:
|
||||
github_token = config.get(ConfigType.GITHUB_TOKEN)
|
||||
if not github_token:
|
||||
return AgentErrorObservation(
|
||||
'GITHUB_TOKEN is not set'
|
||||
)
|
||||
return AgentErrorObservation('GITHUB_TOKEN is not set')
|
||||
|
||||
# API URL to create the pull request
|
||||
url = f'https://api.github.com/repos/{self.owner}/{self.repo}/pulls'
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
from dataclasses import dataclass, field
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from opendevin.core.schema import ActionType
|
||||
from opendevin.events.observation import NullObservation
|
||||
from opendevin.schema import ActionType
|
||||
|
||||
from .action import Action
|
||||
|
||||
@ -48,6 +48,7 @@ class ModifyTaskAction(Action):
|
||||
@dataclass
|
||||
class TaskStateChangedAction(Action):
|
||||
"""Fake action, just to notify the client that a task state has changed."""
|
||||
|
||||
task_state: str
|
||||
thought: str = ''
|
||||
action: str = ActionType.CHANGE_TASK_STATE
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from opendevin.schema import ObservationType
|
||||
from opendevin.core.schema import ObservationType
|
||||
|
||||
from .observation import Observation
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
from opendevin.schema import ObservationType
|
||||
from opendevin.core.schema import ObservationType
|
||||
|
||||
from .observation import Observation
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
from opendevin.schema import ObservationType
|
||||
from opendevin.core.schema import ObservationType
|
||||
|
||||
from .observation import Observation
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
from opendevin.schema import ObservationType
|
||||
from opendevin.core.schema import ObservationType
|
||||
|
||||
from .observation import Observation
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
from opendevin.schema import ObservationType
|
||||
from opendevin.core.schema import ObservationType
|
||||
|
||||
from .observation import Observation
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
from opendevin.schema import ObservationType
|
||||
from opendevin.core.schema import ObservationType
|
||||
|
||||
from .observation import Observation
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
from opendevin.schema import ObservationType
|
||||
from opendevin.core.schema import ObservationType
|
||||
|
||||
from .observation import Observation
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import List
|
||||
|
||||
from opendevin.schema import ObservationType
|
||||
from opendevin.core.schema import ObservationType
|
||||
|
||||
from .observation import Observation
|
||||
|
||||
|
||||
@ -13,10 +13,10 @@ from tenacity import (
|
||||
wait_random_exponential,
|
||||
)
|
||||
|
||||
from opendevin import config
|
||||
from opendevin.logger import llm_prompt_logger, llm_response_logger
|
||||
from opendevin.logger import opendevin_logger as logger
|
||||
from opendevin.schema import ConfigType
|
||||
from opendevin.core import config
|
||||
from opendevin.core.logger import llm_prompt_logger, llm_response_logger
|
||||
from opendevin.core.logger import opendevin_logger as logger
|
||||
from opendevin.core.schema import ConfigType
|
||||
|
||||
DEFAULT_API_KEY = config.get(ConfigType.LLM_API_KEY)
|
||||
DEFAULT_BASE_URL = config.get(ConfigType.LLM_BASE_URL)
|
||||
|
||||
@ -4,10 +4,4 @@ from .docker.ssh_box import DockerSSHBox
|
||||
from .e2b.sandbox import E2BBox
|
||||
from .sandbox import Sandbox
|
||||
|
||||
__all__ = [
|
||||
'Sandbox',
|
||||
'DockerSSHBox',
|
||||
'DockerExecBox',
|
||||
'E2BBox',
|
||||
'LocalBox'
|
||||
]
|
||||
__all__ = ['Sandbox', 'DockerSSHBox', 'DockerExecBox', 'E2BBox', 'LocalBox']
|
||||
@ -12,14 +12,14 @@ import numpy as np
|
||||
from browsergym.utils.obs import flatten_dom_to_str
|
||||
from PIL import Image
|
||||
|
||||
from opendevin.logger import opendevin_logger as logger
|
||||
from opendevin.core.logger import opendevin_logger as logger
|
||||
|
||||
|
||||
class BrowserException(Exception):
|
||||
pass
|
||||
|
||||
class BrowserEnv:
|
||||
|
||||
class BrowserEnv:
|
||||
def __init__(self):
|
||||
self.html_text_converter = html2text.HTML2Text()
|
||||
# ignore links and images
|
||||
@ -32,7 +32,9 @@ class BrowserEnv:
|
||||
# Initialize browser environment process
|
||||
multiprocessing.set_start_method('spawn', force=True)
|
||||
self.browser_side, self.agent_side = multiprocessing.Pipe()
|
||||
self.process = multiprocessing.Process(target=self.browser_process,)
|
||||
self.process = multiprocessing.Process(
|
||||
target=self.browser_process,
|
||||
)
|
||||
logger.info('Starting browser env...')
|
||||
self.process.start()
|
||||
atexit.register(self.close)
|
||||
@ -50,7 +52,7 @@ class BrowserEnv:
|
||||
while True:
|
||||
try:
|
||||
if self.browser_side.poll(timeout=0.01):
|
||||
unique_request_id , action_data = self.browser_side.recv()
|
||||
unique_request_id, action_data = self.browser_side.recv()
|
||||
# shutdown the browser environment
|
||||
if unique_request_id == 'SHUTDOWN':
|
||||
env.close()
|
||||
0
opendevin/runtime/docker/__init__.py
Normal file
0
opendevin/runtime/docker/__init__.py
Normal file
@ -11,13 +11,12 @@ from typing import Dict, List, Tuple
|
||||
|
||||
import docker
|
||||
|
||||
from opendevin import config
|
||||
from opendevin.exceptions import SandboxInvalidBackgroundCommandError
|
||||
from opendevin.logger import opendevin_logger as logger
|
||||
from opendevin.sandbox.docker.process import DockerProcess
|
||||
from opendevin.sandbox.process import Process
|
||||
from opendevin.sandbox.sandbox import Sandbox
|
||||
from opendevin.schema import ConfigType
|
||||
from opendevin.core import config
|
||||
from opendevin.core.exceptions import SandboxInvalidBackgroundCommandError
|
||||
from opendevin.core.logger import opendevin_logger as logger
|
||||
from opendevin.core.schema import ConfigType
|
||||
from opendevin.runtime.docker.process import DockerProcess, Process
|
||||
from opendevin.runtime.sandbox import Sandbox
|
||||
|
||||
InputType = namedtuple('InputType', ['content'])
|
||||
OutputType = namedtuple('OutputType', ['content'])
|
||||
@ -47,17 +46,19 @@ class DockerExecBox(Sandbox):
|
||||
background_commands: Dict[int, Process] = {}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
container_image: str | None = None,
|
||||
timeout: int = 120,
|
||||
sid: str | None = None,
|
||||
self,
|
||||
container_image: str | None = None,
|
||||
timeout: int = 120,
|
||||
sid: str | None = None,
|
||||
):
|
||||
# Initialize docker client. Throws an exception if Docker is not reachable.
|
||||
try:
|
||||
self.docker_client = docker.from_env()
|
||||
except Exception as ex:
|
||||
logger.exception(
|
||||
'Error creating controller. Please check Docker is running and visit `https://opendevin.github.io/OpenDevin/modules/usage/troubleshooting` for more debugging information.', exc_info=False)
|
||||
'Error creating controller. Please check Docker is running and visit `https://opendevin.github.io/OpenDevin/modules/usage/troubleshooting` for more debugging information.',
|
||||
exc_info=False,
|
||||
)
|
||||
raise ex
|
||||
|
||||
self.instance_id = sid if sid is not None else str(uuid.uuid4())
|
||||
@ -67,7 +68,9 @@ class DockerExecBox(Sandbox):
|
||||
# command to finish (e.g. apt-get update)
|
||||
# if it is too long, the user may have to wait for a unnecessary long time
|
||||
self.timeout = timeout
|
||||
self.container_image = CONTAINER_IMAGE if container_image is None else container_image
|
||||
self.container_image = (
|
||||
CONTAINER_IMAGE if container_image is None else container_image
|
||||
)
|
||||
self.container_name = self.container_name_prefix + self.instance_id
|
||||
|
||||
# always restart the container, cuz the initial be regarded as a new session
|
||||
@ -116,11 +119,13 @@ class DockerExecBox(Sandbox):
|
||||
exit_code, logs = future.result(timeout=self.timeout)
|
||||
except concurrent.futures.TimeoutError:
|
||||
logger.exception(
|
||||
'Command timed out, killing process...', exc_info=False)
|
||||
'Command timed out, killing process...', exc_info=False
|
||||
)
|
||||
pid = self.get_pid(cmd)
|
||||
if pid is not None:
|
||||
self.container.exec_run(
|
||||
f'kill -9 {pid}', workdir=SANDBOX_WORKSPACE_DIR)
|
||||
f'kill -9 {pid}', workdir=SANDBOX_WORKSPACE_DIR
|
||||
)
|
||||
return -1, f'Command: "{cmd}" timed out'
|
||||
logs_out = logs.decode('utf-8')
|
||||
if logs_out.endswith('\n'):
|
||||
@ -135,18 +140,25 @@ class DockerExecBox(Sandbox):
|
||||
)
|
||||
if exit_code != 0:
|
||||
raise Exception(
|
||||
f'Failed to create directory {sandbox_dest} in sandbox: {logs}')
|
||||
f'Failed to create directory {sandbox_dest} in sandbox: {logs}'
|
||||
)
|
||||
|
||||
if recursive:
|
||||
assert os.path.isdir(host_src), 'Source must be a directory when recursive is True'
|
||||
assert os.path.isdir(
|
||||
host_src
|
||||
), 'Source must be a directory when recursive is True'
|
||||
files = glob(host_src + '/**/*', recursive=True)
|
||||
srcname = os.path.basename(host_src)
|
||||
tar_filename = os.path.join(os.path.dirname(host_src), srcname + '.tar')
|
||||
with tarfile.open(tar_filename, mode='w') as tar:
|
||||
for file in files:
|
||||
tar.add(file, arcname=os.path.relpath(file, os.path.dirname(host_src)))
|
||||
tar.add(
|
||||
file, arcname=os.path.relpath(file, os.path.dirname(host_src))
|
||||
)
|
||||
else:
|
||||
assert os.path.isfile(host_src), 'Source must be a file when recursive is False'
|
||||
assert os.path.isfile(
|
||||
host_src
|
||||
), 'Source must be a file when recursive is False'
|
||||
srcname = os.path.basename(host_src)
|
||||
tar_filename = os.path.join(os.path.dirname(host_src), srcname + '.tar')
|
||||
with tarfile.open(tar_filename, mode='w') as tar:
|
||||
@ -186,7 +198,8 @@ class DockerExecBox(Sandbox):
|
||||
bg_cmd = self.background_commands[id]
|
||||
if bg_cmd.pid is not None:
|
||||
self.container.exec_run(
|
||||
f'kill -9 {bg_cmd.pid}', workdir=SANDBOX_WORKSPACE_DIR)
|
||||
f'kill -9 {bg_cmd.pid}', workdir=SANDBOX_WORKSPACE_DIR
|
||||
)
|
||||
assert isinstance(bg_cmd, DockerProcess)
|
||||
bg_cmd.result.output.close()
|
||||
self.background_commands.pop(id)
|
||||
@ -203,8 +216,7 @@ class DockerExecBox(Sandbox):
|
||||
elapsed += 1
|
||||
if elapsed > self.timeout:
|
||||
break
|
||||
container = self.docker_client.containers.get(
|
||||
self.container_name)
|
||||
container = self.docker_client.containers.get(self.container_name)
|
||||
except docker.errors.NotFound:
|
||||
pass
|
||||
|
||||
@ -236,8 +248,7 @@ class DockerExecBox(Sandbox):
|
||||
working_dir=SANDBOX_WORKSPACE_DIR,
|
||||
name=self.container_name,
|
||||
detach=True,
|
||||
volumes={mount_dir: {
|
||||
'bind': SANDBOX_WORKSPACE_DIR, 'mode': 'rw'}},
|
||||
volumes={mount_dir: {'bind': SANDBOX_WORKSPACE_DIR, 'mode': 'rw'}},
|
||||
)
|
||||
logger.info('Container started')
|
||||
except Exception as ex:
|
||||
@ -254,8 +265,7 @@ class DockerExecBox(Sandbox):
|
||||
break
|
||||
time.sleep(1)
|
||||
elapsed += 1
|
||||
self.container = self.docker_client.containers.get(
|
||||
self.container_name)
|
||||
self.container = self.docker_client.containers.get(self.container_name)
|
||||
if elapsed > self.timeout:
|
||||
break
|
||||
if self.container.status != 'running':
|
||||
@ -283,7 +293,8 @@ if __name__ == '__main__':
|
||||
sys.exit(1)
|
||||
|
||||
logger.info(
|
||||
"Interactive Docker container started. Type 'exit' or use Ctrl+C to exit.")
|
||||
"Interactive Docker container started. Type 'exit' or use Ctrl+C to exit."
|
||||
)
|
||||
|
||||
bg_cmd = exec_box.execute_in_background(
|
||||
"while true; do echo -n '.' && sleep 1; done"
|
||||
@ -4,12 +4,11 @@ import subprocess
|
||||
import sys
|
||||
from typing import Dict, Tuple
|
||||
|
||||
from opendevin import config
|
||||
from opendevin.logger import opendevin_logger as logger
|
||||
from opendevin.sandbox.docker.process import DockerProcess
|
||||
from opendevin.sandbox.process import Process
|
||||
from opendevin.sandbox.sandbox import Sandbox
|
||||
from opendevin.schema.config import ConfigType
|
||||
from opendevin.core import config
|
||||
from opendevin.core.logger import opendevin_logger as logger
|
||||
from opendevin.core.schema.config import ConfigType
|
||||
from opendevin.runtime.docker.process import DockerProcess, Process
|
||||
from opendevin.runtime.sandbox import Sandbox
|
||||
|
||||
# ===============================================================================
|
||||
# ** WARNING **
|
||||
@ -38,8 +37,12 @@ class LocalBox(Sandbox):
|
||||
def execute(self, cmd: str) -> Tuple[int, str]:
|
||||
try:
|
||||
completed_process = subprocess.run(
|
||||
cmd, shell=True, text=True, capture_output=True,
|
||||
timeout=self.timeout, cwd=config.get(ConfigType.WORKSPACE_BASE)
|
||||
cmd,
|
||||
shell=True,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
timeout=self.timeout,
|
||||
cwd=config.get(ConfigType.WORKSPACE_BASE),
|
||||
)
|
||||
return completed_process.returncode, completed_process.stdout.strip()
|
||||
except subprocess.TimeoutExpired:
|
||||
@ -47,27 +50,46 @@ class LocalBox(Sandbox):
|
||||
|
||||
def copy_to(self, host_src: str, sandbox_dest: str, recursive: bool = False):
|
||||
# mkdir -p sandbox_dest if it doesn't exist
|
||||
res = subprocess.run(f'mkdir -p {sandbox_dest}', shell=True, text=True, cwd=config.get(ConfigType.WORKSPACE_BASE))
|
||||
res = subprocess.run(
|
||||
f'mkdir -p {sandbox_dest}',
|
||||
shell=True,
|
||||
text=True,
|
||||
cwd=config.get(ConfigType.WORKSPACE_BASE),
|
||||
)
|
||||
if res.returncode != 0:
|
||||
raise RuntimeError(f'Failed to create directory {sandbox_dest} in sandbox')
|
||||
|
||||
if recursive:
|
||||
res = subprocess.run(
|
||||
f'cp -r {host_src} {sandbox_dest}', shell=True, text=True, cwd=config.get(ConfigType.WORKSPACE_BASE)
|
||||
f'cp -r {host_src} {sandbox_dest}',
|
||||
shell=True,
|
||||
text=True,
|
||||
cwd=config.get(ConfigType.WORKSPACE_BASE),
|
||||
)
|
||||
if res.returncode != 0:
|
||||
raise RuntimeError(f'Failed to copy {host_src} to {sandbox_dest} in sandbox')
|
||||
raise RuntimeError(
|
||||
f'Failed to copy {host_src} to {sandbox_dest} in sandbox'
|
||||
)
|
||||
else:
|
||||
res = subprocess.run(
|
||||
f'cp {host_src} {sandbox_dest}', shell=True, text=True, cwd=config.get(ConfigType.WORKSPACE_BASE)
|
||||
f'cp {host_src} {sandbox_dest}',
|
||||
shell=True,
|
||||
text=True,
|
||||
cwd=config.get(ConfigType.WORKSPACE_BASE),
|
||||
)
|
||||
if res.returncode != 0:
|
||||
raise RuntimeError(f'Failed to copy {host_src} to {sandbox_dest} in sandbox')
|
||||
raise RuntimeError(
|
||||
f'Failed to copy {host_src} to {sandbox_dest} in sandbox'
|
||||
)
|
||||
|
||||
def execute_in_background(self, cmd: str) -> Process:
|
||||
process = subprocess.Popen(
|
||||
cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
||||
text=True, cwd=config.get(ConfigType.WORKSPACE_BASE)
|
||||
cmd,
|
||||
shell=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
cwd=config.get(ConfigType.WORKSPACE_BASE),
|
||||
)
|
||||
bg_cmd = DockerProcess(
|
||||
id=self.cur_background_id, command=cmd, result=process, pid=process.pid
|
||||
@ -105,7 +127,6 @@ class LocalBox(Sandbox):
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
local_box = LocalBox()
|
||||
bg_cmd = local_box.execute_in_background(
|
||||
"while true; do echo 'dot ' && sleep 10; done"
|
||||
@ -2,7 +2,7 @@ import select
|
||||
import sys
|
||||
from typing import Tuple
|
||||
|
||||
from opendevin.sandbox.process import Process
|
||||
from opendevin.runtime.process import Process
|
||||
|
||||
|
||||
class DockerProcess(Process):
|
||||
@ -68,7 +68,7 @@ class DockerProcess(Process):
|
||||
i = 0
|
||||
byte_order = sys.byteorder
|
||||
while i < len(logs):
|
||||
prefix = logs[i: i + 8]
|
||||
prefix = logs[i : i + 8]
|
||||
if len(prefix) < 8:
|
||||
msg_type = prefix[0:1]
|
||||
if msg_type in [b'\x00', b'\x01', b'\x02', b'\x03']:
|
||||
@ -82,10 +82,10 @@ class DockerProcess(Process):
|
||||
and padding == b'\x00\x00\x00'
|
||||
):
|
||||
msg_length = int.from_bytes(prefix[4:8], byteorder=byte_order)
|
||||
res += logs[i + 8: i + 8 + msg_length]
|
||||
res += logs[i + 8 : i + 8 + msg_length]
|
||||
i += 8 + msg_length
|
||||
else:
|
||||
res += logs[i: i + 1]
|
||||
res += logs[i : i + 1]
|
||||
i += 1
|
||||
return res, tail
|
||||
|
||||
@ -118,14 +118,12 @@ class DockerProcess(Process):
|
||||
logs = b''
|
||||
last_remains = b''
|
||||
while True:
|
||||
ready_to_read, _, _ = select.select(
|
||||
[self.result.output], [], [], 0.1) # type: ignore[has-type]
|
||||
ready_to_read, _, _ = select.select([self.result.output], [], [], 0.1) # type: ignore[has-type]
|
||||
if ready_to_read:
|
||||
data = self.result.output.read(4096) # type: ignore[has-type]
|
||||
if not data:
|
||||
break
|
||||
chunk, last_remains = self.parse_docker_exec_output(
|
||||
last_remains + data)
|
||||
chunk, last_remains = self.parse_docker_exec_output(last_remains + data)
|
||||
logs += chunk
|
||||
else:
|
||||
break
|
||||
@ -11,15 +11,17 @@ from typing import Dict, List, Tuple, Union
|
||||
import docker
|
||||
from pexpect import pxssh
|
||||
|
||||
from opendevin import config
|
||||
from opendevin.exceptions import SandboxInvalidBackgroundCommandError
|
||||
from opendevin.logger import opendevin_logger as logger
|
||||
from opendevin.sandbox.docker.process import DockerProcess
|
||||
from opendevin.sandbox.plugins import JupyterRequirement, SWEAgentCommandsRequirement
|
||||
from opendevin.sandbox.process import Process
|
||||
from opendevin.sandbox.sandbox import Sandbox
|
||||
from opendevin.schema import ConfigType
|
||||
from opendevin.utils import find_available_tcp_port
|
||||
from opendevin.core import config
|
||||
from opendevin.core.exceptions import SandboxInvalidBackgroundCommandError
|
||||
from opendevin.core.logger import opendevin_logger as logger
|
||||
from opendevin.core.schema import ConfigType
|
||||
from opendevin.runtime.docker.process import DockerProcess, Process
|
||||
from opendevin.runtime.plugins import (
|
||||
JupyterRequirement,
|
||||
SWEAgentCommandsRequirement,
|
||||
)
|
||||
from opendevin.runtime.sandbox import Sandbox
|
||||
from opendevin.runtime.utils import find_available_tcp_port
|
||||
|
||||
InputType = namedtuple('InputType', ['content'])
|
||||
OutputType = namedtuple('OutputType', ['content'])
|
||||
@ -41,6 +43,7 @@ if SANDBOX_USER_ID := config.get(ConfigType.SANDBOX_USER_ID):
|
||||
elif hasattr(os, 'getuid'):
|
||||
USER_ID = os.getuid()
|
||||
|
||||
|
||||
class DockerSSHBox(Sandbox):
|
||||
instance_id: str
|
||||
container_image: str
|
||||
@ -61,13 +64,17 @@ class DockerSSHBox(Sandbox):
|
||||
timeout: int = 120,
|
||||
sid: str | None = None,
|
||||
):
|
||||
logger.info(f'SSHBox is running as {"opendevin" if RUN_AS_DEVIN else "root"} user with USER_ID={USER_ID} in the sandbox')
|
||||
logger.info(
|
||||
f'SSHBox is running as {"opendevin" if RUN_AS_DEVIN else "root"} user with USER_ID={USER_ID} in the sandbox'
|
||||
)
|
||||
# Initialize docker client. Throws an exception if Docker is not reachable.
|
||||
try:
|
||||
self.docker_client = docker.from_env()
|
||||
except Exception as ex:
|
||||
logger.exception(
|
||||
'Error creating controller. Please check Docker is running and visit `https://opendevin.github.io/OpenDevin/modules/usage/troubleshooting` for more debugging information.', exc_info=False)
|
||||
'Error creating controller. Please check Docker is running and visit `https://opendevin.github.io/OpenDevin/modules/usage/troubleshooting` for more debugging information.',
|
||||
exc_info=False,
|
||||
)
|
||||
raise ex
|
||||
|
||||
self.instance_id = sid if sid is not None else str(uuid.uuid4())
|
||||
@ -77,7 +84,9 @@ class DockerSSHBox(Sandbox):
|
||||
# command to finish (e.g. apt-get update)
|
||||
# if it is too long, the user may have to wait for a unnecessary long time
|
||||
self.timeout = timeout
|
||||
self.container_image = CONTAINER_IMAGE if container_image is None else container_image
|
||||
self.container_image = (
|
||||
CONTAINER_IMAGE if container_image is None else container_image
|
||||
)
|
||||
self.container_name = self.container_name_prefix + self.instance_id
|
||||
|
||||
# set up random user password
|
||||
@ -92,17 +101,16 @@ class DockerSSHBox(Sandbox):
|
||||
atexit.register(self.close)
|
||||
|
||||
def setup_user(self):
|
||||
|
||||
# Make users sudoers passwordless
|
||||
# TODO(sandbox): add this line in the Dockerfile for next minor version of docker image
|
||||
exit_code, logs = self.container.exec_run(
|
||||
['/bin/bash', '-c',
|
||||
r"echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers"],
|
||||
['/bin/bash', '-c', r"echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers"],
|
||||
workdir=SANDBOX_WORKSPACE_DIR,
|
||||
)
|
||||
if exit_code != 0:
|
||||
raise Exception(
|
||||
f'Failed to make all users passwordless sudoers in sandbox: {logs}')
|
||||
f'Failed to make all users passwordless sudoers in sandbox: {logs}'
|
||||
)
|
||||
|
||||
# Check if the opendevin user exists
|
||||
exit_code, logs = self.container.exec_run(
|
||||
@ -116,22 +124,26 @@ class DockerSSHBox(Sandbox):
|
||||
workdir=SANDBOX_WORKSPACE_DIR,
|
||||
)
|
||||
if exit_code != 0:
|
||||
raise Exception(
|
||||
f'Failed to remove opendevin user in sandbox: {logs}')
|
||||
raise Exception(f'Failed to remove opendevin user in sandbox: {logs}')
|
||||
|
||||
if RUN_AS_DEVIN:
|
||||
# Create the opendevin user
|
||||
exit_code, logs = self.container.exec_run(
|
||||
['/bin/bash', '-c',
|
||||
f'useradd -rm -d /home/opendevin -s /bin/bash -g root -G sudo -u {USER_ID} opendevin'],
|
||||
[
|
||||
'/bin/bash',
|
||||
'-c',
|
||||
f'useradd -rm -d /home/opendevin -s /bin/bash -g root -G sudo -u {USER_ID} opendevin',
|
||||
],
|
||||
workdir=SANDBOX_WORKSPACE_DIR,
|
||||
)
|
||||
if exit_code != 0:
|
||||
raise Exception(
|
||||
f'Failed to create opendevin user in sandbox: {logs}')
|
||||
raise Exception(f'Failed to create opendevin user in sandbox: {logs}')
|
||||
exit_code, logs = self.container.exec_run(
|
||||
['/bin/bash', '-c',
|
||||
f"echo 'opendevin:{self._ssh_password}' | chpasswd"],
|
||||
[
|
||||
'/bin/bash',
|
||||
'-c',
|
||||
f"echo 'opendevin:{self._ssh_password}' | chpasswd",
|
||||
],
|
||||
workdir=SANDBOX_WORKSPACE_DIR,
|
||||
)
|
||||
if exit_code != 0:
|
||||
@ -144,7 +156,8 @@ class DockerSSHBox(Sandbox):
|
||||
)
|
||||
if exit_code != 0:
|
||||
raise Exception(
|
||||
f'Failed to chown home directory for opendevin in sandbox: {logs}')
|
||||
f'Failed to chown home directory for opendevin in sandbox: {logs}'
|
||||
)
|
||||
exit_code, logs = self.container.exec_run(
|
||||
['/bin/bash', '-c', f'chown opendevin:root {SANDBOX_WORKSPACE_DIR}'],
|
||||
workdir=SANDBOX_WORKSPACE_DIR,
|
||||
@ -157,13 +170,11 @@ class DockerSSHBox(Sandbox):
|
||||
else:
|
||||
exit_code, logs = self.container.exec_run(
|
||||
# change password for root
|
||||
['/bin/bash', '-c',
|
||||
f"echo 'root:{self._ssh_password}' | chpasswd"],
|
||||
['/bin/bash', '-c', f"echo 'root:{self._ssh_password}' | chpasswd"],
|
||||
workdir=SANDBOX_WORKSPACE_DIR,
|
||||
)
|
||||
if exit_code != 0:
|
||||
raise Exception(
|
||||
f'Failed to set password for root in sandbox: {logs}')
|
||||
raise Exception(f'Failed to set password for root in sandbox: {logs}')
|
||||
exit_code, logs = self.container.exec_run(
|
||||
['/bin/bash', '-c', "echo 'opendevin-sandbox' > /etc/hostname"],
|
||||
workdir=SANDBOX_WORKSPACE_DIR,
|
||||
@ -178,12 +189,11 @@ class DockerSSHBox(Sandbox):
|
||||
else:
|
||||
username = 'root'
|
||||
logger.info(
|
||||
f"Connecting to {username}@{hostname} via ssh. "
|
||||
f'Connecting to {username}@{hostname} via ssh. '
|
||||
f"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. "
|
||||
f"If you started OpenDevin with `docker run`, you should try `ssh -v -p {self._ssh_port} {username}@localhost` with the password '{self._ssh_password} on the host machine (where you started the container)."
|
||||
)
|
||||
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'")
|
||||
@ -210,14 +220,15 @@ class DockerSSHBox(Sandbox):
|
||||
self.ssh.sendline(cmd)
|
||||
success = self.ssh.prompt(timeout=self.timeout)
|
||||
if not success:
|
||||
logger.exception(
|
||||
'Command timed out, killing process...', exc_info=False)
|
||||
logger.exception('Command timed out, killing process...', exc_info=False)
|
||||
# send a SIGINT to the process
|
||||
self.ssh.sendintr()
|
||||
self.ssh.prompt()
|
||||
command_output = self.ssh.before.decode(
|
||||
'utf-8').lstrip(cmd).strip()
|
||||
return -1, f'Command: "{cmd}" timed out. Sending SIGINT to the process: {command_output}'
|
||||
command_output = self.ssh.before.decode('utf-8').lstrip(cmd).strip()
|
||||
return (
|
||||
-1,
|
||||
f'Command: "{cmd}" timed out. Sending SIGINT to the process: {command_output}',
|
||||
)
|
||||
command_output = self.ssh.before.decode('utf-8').strip()
|
||||
|
||||
# once out, make sure that we have *every* output, we while loop until we get an empty output
|
||||
@ -230,7 +241,9 @@ class DockerSSHBox(Sandbox):
|
||||
break
|
||||
logger.debug('WAITING FOR .before')
|
||||
output = self.ssh.before.decode('utf-8').strip()
|
||||
logger.debug(f'WAITING FOR END OF command output ({bool(output)}): {output}')
|
||||
logger.debug(
|
||||
f'WAITING FOR END OF command output ({bool(output)}): {output}'
|
||||
)
|
||||
if output == '':
|
||||
break
|
||||
command_output += output
|
||||
@ -255,18 +268,25 @@ class DockerSSHBox(Sandbox):
|
||||
)
|
||||
if exit_code != 0:
|
||||
raise Exception(
|
||||
f'Failed to create directory {sandbox_dest} in sandbox: {logs}')
|
||||
f'Failed to create directory {sandbox_dest} in sandbox: {logs}'
|
||||
)
|
||||
|
||||
if recursive:
|
||||
assert os.path.isdir(host_src), 'Source must be a directory when recursive is True'
|
||||
assert os.path.isdir(
|
||||
host_src
|
||||
), 'Source must be a directory when recursive is True'
|
||||
files = glob(host_src + '/**/*', recursive=True)
|
||||
srcname = os.path.basename(host_src)
|
||||
tar_filename = os.path.join(os.path.dirname(host_src), srcname + '.tar')
|
||||
with tarfile.open(tar_filename, mode='w') as tar:
|
||||
for file in files:
|
||||
tar.add(file, arcname=os.path.relpath(file, os.path.dirname(host_src)))
|
||||
tar.add(
|
||||
file, arcname=os.path.relpath(file, os.path.dirname(host_src))
|
||||
)
|
||||
else:
|
||||
assert os.path.isfile(host_src), 'Source must be a file when recursive is False'
|
||||
assert os.path.isfile(
|
||||
host_src
|
||||
), 'Source must be a file when recursive is False'
|
||||
srcname = os.path.basename(host_src)
|
||||
tar_filename = os.path.join(os.path.dirname(host_src), srcname + '.tar')
|
||||
with tarfile.open(tar_filename, mode='w') as tar:
|
||||
@ -306,7 +326,8 @@ class DockerSSHBox(Sandbox):
|
||||
bg_cmd = self.background_commands[id]
|
||||
if bg_cmd.pid is not None:
|
||||
self.container.exec_run(
|
||||
f'kill -9 {bg_cmd.pid}', workdir=SANDBOX_WORKSPACE_DIR)
|
||||
f'kill -9 {bg_cmd.pid}', workdir=SANDBOX_WORKSPACE_DIR
|
||||
)
|
||||
assert isinstance(bg_cmd, DockerProcess)
|
||||
bg_cmd.result.output.close()
|
||||
self.background_commands.pop(id)
|
||||
@ -323,8 +344,7 @@ class DockerSSHBox(Sandbox):
|
||||
elapsed += 1
|
||||
if elapsed > self.timeout:
|
||||
break
|
||||
container = self.docker_client.containers.get(
|
||||
self.container_name)
|
||||
container = self.docker_client.containers.get(self.container_name)
|
||||
except docker.errors.NotFound:
|
||||
pass
|
||||
|
||||
@ -360,10 +380,11 @@ class DockerSSHBox(Sandbox):
|
||||
# FIXME: This is a temporary workaround for Mac OS
|
||||
network_kwargs['ports'] = {f'{self._ssh_port}/tcp': self._ssh_port}
|
||||
logger.warning(
|
||||
('Using port forwarding for Mac OS. '
|
||||
'Server started by OpenDevin will not be accessible from the host machine at the moment. '
|
||||
'See https://github.com/OpenDevin/OpenDevin/issues/897 for more information.'
|
||||
)
|
||||
(
|
||||
'Using port forwarding for Mac OS. '
|
||||
'Server started by OpenDevin will not be accessible from the host machine at the moment. '
|
||||
'See https://github.com/OpenDevin/OpenDevin/issues/897 for more information.'
|
||||
)
|
||||
)
|
||||
|
||||
mount_dir = config.get(ConfigType.WORKSPACE_MOUNT_PATH)
|
||||
@ -378,14 +399,13 @@ class DockerSSHBox(Sandbox):
|
||||
name=self.container_name,
|
||||
detach=True,
|
||||
volumes={
|
||||
mount_dir: {
|
||||
'bind': SANDBOX_WORKSPACE_DIR,
|
||||
'mode': 'rw'
|
||||
},
|
||||
mount_dir: {'bind': SANDBOX_WORKSPACE_DIR, 'mode': 'rw'},
|
||||
# mount cache directory to /home/opendevin/.cache for pip cache reuse
|
||||
config.get(ConfigType.CACHE_DIR): {
|
||||
'bind': '/home/opendevin/.cache' if RUN_AS_DEVIN else '/root/.cache',
|
||||
'mode': 'rw'
|
||||
'bind': '/home/opendevin/.cache'
|
||||
if RUN_AS_DEVIN
|
||||
else '/root/.cache',
|
||||
'mode': 'rw',
|
||||
},
|
||||
},
|
||||
)
|
||||
@ -404,10 +424,10 @@ class DockerSSHBox(Sandbox):
|
||||
break
|
||||
time.sleep(1)
|
||||
elapsed += 1
|
||||
self.container = self.docker_client.containers.get(
|
||||
self.container_name)
|
||||
self.container = self.docker_client.containers.get(self.container_name)
|
||||
logger.info(
|
||||
f'waiting for container to start: {elapsed}, container status: {self.container.status}')
|
||||
f'waiting for container to start: {elapsed}, container status: {self.container.status}'
|
||||
)
|
||||
if elapsed > self.timeout:
|
||||
break
|
||||
if self.container.status != 'running':
|
||||
@ -425,7 +445,6 @@ class DockerSSHBox(Sandbox):
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
try:
|
||||
ssh_box = DockerSSHBox()
|
||||
except Exception as e:
|
||||
@ -433,7 +452,8 @@ if __name__ == '__main__':
|
||||
sys.exit(1)
|
||||
|
||||
logger.info(
|
||||
"Interactive Docker container started. Type 'exit' or use Ctrl+C to exit.")
|
||||
"Interactive Docker container started. Type 'exit' or use Ctrl+C to exit."
|
||||
)
|
||||
|
||||
# Initialize required plugins
|
||||
ssh_box.init_plugins([JupyterRequirement(), SWEAgentCommandsRequirement()])
|
||||
@ -1,6 +1,6 @@
|
||||
from e2b import Process as E2BSandboxProcess
|
||||
|
||||
from opendevin.sandbox.process import Process
|
||||
from opendevin.runtime.docker.process import Process
|
||||
|
||||
|
||||
class E2BProcess(Process):
|
||||
@ -8,12 +8,12 @@ from e2b.sandbox.exception import (
|
||||
TimeoutException,
|
||||
)
|
||||
|
||||
from opendevin import config
|
||||
from opendevin.logger import opendevin_logger as logger
|
||||
from opendevin.sandbox.e2b.process import E2BProcess
|
||||
from opendevin.sandbox.process import Process
|
||||
from opendevin.sandbox.sandbox import Sandbox
|
||||
from opendevin.schema.config import ConfigType
|
||||
from opendevin.core import config
|
||||
from opendevin.core.logger import opendevin_logger as logger
|
||||
from opendevin.core.schema.config import ConfigType
|
||||
from opendevin.runtime.e2b.process import E2BProcess
|
||||
from opendevin.runtime.process import Process
|
||||
from opendevin.runtime.sandbox import Sandbox
|
||||
|
||||
|
||||
class E2BBox(Sandbox):
|
||||
@ -44,15 +44,21 @@ class E2BBox(Sandbox):
|
||||
|
||||
def _archive(self, host_src: str, recursive: bool = False):
|
||||
if recursive:
|
||||
assert os.path.isdir(host_src), 'Source must be a directory when recursive is True'
|
||||
assert os.path.isdir(
|
||||
host_src
|
||||
), 'Source must be a directory when recursive is True'
|
||||
files = glob(host_src + '/**/*', recursive=True)
|
||||
srcname = os.path.basename(host_src)
|
||||
tar_filename = os.path.join(os.path.dirname(host_src), srcname + '.tar')
|
||||
with tarfile.open(tar_filename, mode='w') as tar:
|
||||
for file in files:
|
||||
tar.add(file, arcname=os.path.relpath(file, os.path.dirname(host_src)))
|
||||
tar.add(
|
||||
file, arcname=os.path.relpath(file, os.path.dirname(host_src))
|
||||
)
|
||||
else:
|
||||
assert os.path.isfile(host_src), 'Source must be a file when recursive is False'
|
||||
assert os.path.isfile(
|
||||
host_src
|
||||
), 'Source must be a file when recursive is False'
|
||||
srcname = os.path.basename(host_src)
|
||||
tar_filename = os.path.join(os.path.dirname(host_src), srcname + '.tar')
|
||||
with tarfile.open(tar_filename, mode='w') as tar:
|
||||
@ -101,9 +107,13 @@ class E2BBox(Sandbox):
|
||||
self.sandbox.filesystem.make_dir(sandbox_dest)
|
||||
|
||||
# Extract the archive into the destination and delete the archive
|
||||
process = self.sandbox.process.start_and_wait(f'sudo tar -xf {uploaded_path} -C {sandbox_dest} && sudo rm {uploaded_path}')
|
||||
process = self.sandbox.process.start_and_wait(
|
||||
f'sudo tar -xf {uploaded_path} -C {sandbox_dest} && sudo rm {uploaded_path}'
|
||||
)
|
||||
if process.exit_code != 0:
|
||||
raise Exception(f'Failed to extract {uploaded_path} to {sandbox_dest}: {process.stderr}')
|
||||
raise Exception(
|
||||
f'Failed to extract {uploaded_path} to {sandbox_dest}: {process.stderr}'
|
||||
)
|
||||
|
||||
# Delete the local archive
|
||||
os.remove(tar_filename)
|
||||
@ -4,4 +4,9 @@ from .mixin import PluginMixin
|
||||
from .requirement import PluginRequirement
|
||||
from .swe_agent_commands import SWEAgentCommandsRequirement
|
||||
|
||||
__all__ = ['PluginMixin', 'PluginRequirement', 'JupyterRequirement', 'SWEAgentCommandsRequirement']
|
||||
__all__ = [
|
||||
'PluginMixin',
|
||||
'PluginRequirement',
|
||||
'JupyterRequirement',
|
||||
'SWEAgentCommandsRequirement',
|
||||
]
|
||||
@ -1,12 +1,14 @@
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
|
||||
from opendevin.sandbox.plugins.requirement import PluginRequirement
|
||||
from opendevin.runtime.plugins.requirement import PluginRequirement
|
||||
|
||||
|
||||
@dataclass
|
||||
class JupyterRequirement(PluginRequirement):
|
||||
name: str = 'jupyter'
|
||||
host_src: str = os.path.dirname(os.path.abspath(__file__)) # The directory of this file (sandbox/plugins/jupyter)
|
||||
host_src: str = os.path.dirname(
|
||||
os.path.abspath(__file__)
|
||||
) # The directory of this file (sandbox/plugins/jupyter)
|
||||
sandbox_dest: str = '/opendevin/plugins/jupyter'
|
||||
bash_script_path: str = 'setup.sh'
|
||||
0
opendevin/sandbox/plugins/jupyter/execute_cli → opendevin/runtime/plugins/jupyter/execute_cli
Executable file → Normal file
0
opendevin/sandbox/plugins/jupyter/execute_cli → opendevin/runtime/plugins/jupyter/execute_cli
Executable file → Normal file
37
opendevin/sandbox/plugins/jupyter/execute_server → opendevin/runtime/plugins/jupyter/execute_server
Executable file → Normal file
37
opendevin/sandbox/plugins/jupyter/execute_server → opendevin/runtime/plugins/jupyter/execute_server
Executable file → Normal file
@ -51,19 +51,16 @@ def strip_ansi(o: str) -> str:
|
||||
|
||||
|
||||
class JupyterKernel:
|
||||
def __init__(
|
||||
self,
|
||||
url_suffix,
|
||||
convid,
|
||||
lang='python'
|
||||
):
|
||||
def __init__(self, url_suffix, convid, lang='python'):
|
||||
self.base_url = f'http://{url_suffix}'
|
||||
self.base_ws_url = f'ws://{url_suffix}'
|
||||
self.lang = lang
|
||||
self.kernel_id = None
|
||||
self.ws = None
|
||||
self.convid = convid
|
||||
logging.info(f'Jupyter kernel created for conversation {convid} at {url_suffix}')
|
||||
logging.info(
|
||||
f'Jupyter kernel created for conversation {convid} at {url_suffix}'
|
||||
)
|
||||
|
||||
self.heartbeat_interval = 10000 # 10 seconds
|
||||
self.heartbeat_callback = None
|
||||
@ -89,7 +86,9 @@ class JupyterKernel:
|
||||
try:
|
||||
await self._connect()
|
||||
except ConnectionRefusedError:
|
||||
logging.info('ConnectionRefusedError: Failed to reconnect to kernel websocket - Is the kernel still running?')
|
||||
logging.info(
|
||||
'ConnectionRefusedError: Failed to reconnect to kernel websocket - Is the kernel still running?'
|
||||
)
|
||||
|
||||
async def _connect(self):
|
||||
if self.ws:
|
||||
@ -128,7 +127,9 @@ class JupyterKernel:
|
||||
# Setup heartbeat
|
||||
if self.heartbeat_callback:
|
||||
self.heartbeat_callback.stop()
|
||||
self.heartbeat_callback = PeriodicCallback(self._send_heartbeat, self.heartbeat_interval)
|
||||
self.heartbeat_callback = PeriodicCallback(
|
||||
self._send_heartbeat, self.heartbeat_interval
|
||||
)
|
||||
self.heartbeat_callback.start()
|
||||
|
||||
async def execute(self, code, timeout=60):
|
||||
@ -175,7 +176,9 @@ class JupyterKernel:
|
||||
continue
|
||||
|
||||
if os.environ.get('DEBUG', False):
|
||||
logging.info(f"MSG TYPE: {msg_type.upper()} DONE:{execution_done}\nCONTENT: {msg['content']}")
|
||||
logging.info(
|
||||
f"MSG TYPE: {msg_type.upper()} DONE:{execution_done}\nCONTENT: {msg['content']}"
|
||||
)
|
||||
|
||||
if msg_type == 'error':
|
||||
traceback = '\n'.join(msg['content']['traceback'])
|
||||
@ -187,7 +190,9 @@ class JupyterKernel:
|
||||
outputs.append(msg['content']['data']['text/plain'])
|
||||
if 'image/png' in msg['content']['data']:
|
||||
# use markdone to display image (in case of large image)
|
||||
outputs.append(f"\n\n")
|
||||
outputs.append(
|
||||
f"\n\n"
|
||||
)
|
||||
|
||||
elif msg_type == 'execute_reply':
|
||||
execution_done = True
|
||||
@ -254,13 +259,15 @@ class ExecuteHandler(tornado.web.RequestHandler):
|
||||
def make_app():
|
||||
jupyter_kernel = JupyterKernel(
|
||||
f"localhost:{os.environ.get('JUPYTER_GATEWAY_PORT')}",
|
||||
os.environ.get('JUPYTER_GATEWAY_KERNEL_ID')
|
||||
os.environ.get('JUPYTER_GATEWAY_KERNEL_ID'),
|
||||
)
|
||||
asyncio.get_event_loop().run_until_complete(jupyter_kernel.initialize())
|
||||
|
||||
return tornado.web.Application([
|
||||
(r'/execute', ExecuteHandler, {'jupyter_kernel': jupyter_kernel}),
|
||||
])
|
||||
return tornado.web.Application(
|
||||
[
|
||||
(r'/execute', ExecuteHandler, {'jupyter_kernel': jupyter_kernel}),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
0
opendevin/sandbox/plugins/jupyter/setup.sh → opendevin/runtime/plugins/jupyter/setup.sh
Executable file → Normal file
0
opendevin/sandbox/plugins/jupyter/setup.sh → opendevin/runtime/plugins/jupyter/setup.sh
Executable file → Normal file
50
opendevin/runtime/plugins/mixin.py
Normal file
50
opendevin/runtime/plugins/mixin.py
Normal file
@ -0,0 +1,50 @@
|
||||
import os
|
||||
from typing import List, Protocol, Tuple
|
||||
|
||||
from opendevin.core.logger import opendevin_logger as logger
|
||||
from opendevin.runtime.plugins.requirement import PluginRequirement
|
||||
|
||||
|
||||
class SandboxProtocol(Protocol):
|
||||
# https://stackoverflow.com/questions/51930339/how-do-i-correctly-add-type-hints-to-mixin-classes
|
||||
|
||||
def execute(self, cmd: str) -> Tuple[int, str]: ...
|
||||
|
||||
def copy_to(self, host_src: str, sandbox_dest: str, recursive: bool = False): ...
|
||||
|
||||
|
||||
class PluginMixin:
|
||||
"""Mixin for Sandbox to support plugins."""
|
||||
|
||||
def init_plugins(self: SandboxProtocol, requirements: List[PluginRequirement]):
|
||||
"""Load a plugin into the sandbox."""
|
||||
for requirement in requirements:
|
||||
# copy over the files
|
||||
self.copy_to(requirement.host_src, requirement.sandbox_dest, recursive=True)
|
||||
logger.info(
|
||||
f'Copied files from [{requirement.host_src}] to [{requirement.sandbox_dest}] inside sandbox.'
|
||||
)
|
||||
|
||||
# Execute the bash script
|
||||
abs_path_to_bash_script = os.path.join(
|
||||
requirement.sandbox_dest, requirement.bash_script_path
|
||||
)
|
||||
logger.info(
|
||||
f'Initalizing plugin [{requirement.name}] by executing [{abs_path_to_bash_script}] in the sandbox.'
|
||||
)
|
||||
exit_code, output = self.execute(abs_path_to_bash_script)
|
||||
if exit_code != 0:
|
||||
raise RuntimeError(
|
||||
f'Failed to initialize plugin {requirement.name} with exit code {exit_code} and output {output}'
|
||||
)
|
||||
logger.info(
|
||||
f'Plugin {requirement.name} initialized successfully\n:{output}'
|
||||
)
|
||||
|
||||
if len(requirements) > 0:
|
||||
exit_code, output = self.execute('source ~/.bashrc')
|
||||
if exit_code != 0:
|
||||
raise RuntimeError(
|
||||
f'Failed to source ~/.bashrc with exit code {exit_code} and output {output}'
|
||||
)
|
||||
logger.info('Sourced ~/.bashrc successfully')
|
||||
@ -2,17 +2,14 @@ import os
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List
|
||||
|
||||
from opendevin.sandbox.plugins.requirement import PluginRequirement
|
||||
from opendevin.sandbox.plugins.swe_agent_commands.parse_commands import (
|
||||
from opendevin.runtime.plugins.requirement import PluginRequirement
|
||||
from opendevin.runtime.plugins.swe_agent_commands.parse_commands import (
|
||||
parse_command_file,
|
||||
)
|
||||
|
||||
|
||||
def _resolve_to_cur_dir(filename):
|
||||
return os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)),
|
||||
filename
|
||||
)
|
||||
return os.path.join(os.path.dirname(os.path.abspath(__file__)), filename)
|
||||
|
||||
|
||||
def check_and_parse_command_file(filepath) -> str:
|
||||
@ -26,11 +23,13 @@ DEFAULT_SCRIPT_FILEPATHS = [
|
||||
_resolve_to_cur_dir('search.sh'),
|
||||
_resolve_to_cur_dir('edit_linting.sh'),
|
||||
]
|
||||
DEFAULT_DOCUMENTATION = ''.join([
|
||||
check_and_parse_command_file(filepath)
|
||||
for filepath in DEFAULT_SCRIPT_FILEPATHS
|
||||
if filepath is not None
|
||||
])
|
||||
DEFAULT_DOCUMENTATION = ''.join(
|
||||
[
|
||||
check_and_parse_command_file(filepath)
|
||||
for filepath in DEFAULT_SCRIPT_FILEPATHS
|
||||
if filepath is not None
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
@ -40,7 +39,9 @@ class SWEAgentCommandsRequirement(PluginRequirement):
|
||||
sandbox_dest: str = '/opendevin/plugins/swe_agent_commands'
|
||||
bash_script_path: str = 'setup_default.sh'
|
||||
|
||||
scripts_filepaths: List[str | None] = field(default_factory=lambda: DEFAULT_SCRIPT_FILEPATHS)
|
||||
scripts_filepaths: List[str | None] = field(
|
||||
default_factory=lambda: DEFAULT_SCRIPT_FILEPATHS
|
||||
)
|
||||
documentation: str = DEFAULT_DOCUMENTATION
|
||||
|
||||
|
||||
@ -49,11 +50,13 @@ CURSOR_SCRIPT_FILEPATHS = [
|
||||
_resolve_to_cur_dir('cursors_edit_linting.sh'),
|
||||
_resolve_to_cur_dir('search.sh'),
|
||||
]
|
||||
CURSOR_DOCUMENTATION = ''.join([
|
||||
check_and_parse_command_file(filepath)
|
||||
for filepath in CURSOR_SCRIPT_FILEPATHS
|
||||
if filepath is not None
|
||||
])
|
||||
CURSOR_DOCUMENTATION = ''.join(
|
||||
[
|
||||
check_and_parse_command_file(filepath)
|
||||
for filepath in CURSOR_SCRIPT_FILEPATHS
|
||||
if filepath is not None
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
@ -63,5 +66,7 @@ class SWEAgentCursorCommandsRequirement(PluginRequirement):
|
||||
sandbox_dest: str = '/opendevin/plugins/swe_agent_commands'
|
||||
bash_script_path: str = 'setup_cursor_mode.sh'
|
||||
|
||||
scripts_filepaths: List[str | None] = field(default_factory=lambda: CURSOR_SCRIPT_FILEPATHS)
|
||||
scripts_filepaths: List[str | None] = field(
|
||||
default_factory=lambda: CURSOR_SCRIPT_FILEPATHS
|
||||
)
|
||||
documentation: str = CURSOR_DOCUMENTATION
|
||||
0
opendevin/sandbox/plugins/swe_agent_commands/_split_string → opendevin/runtime/plugins/swe_agent_commands/_split_string
Executable file → Normal file
0
opendevin/sandbox/plugins/swe_agent_commands/_split_string → opendevin/runtime/plugins/swe_agent_commands/_split_string
Executable file → Normal file
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user