Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New Agent, Action, Observation Abstraction and with updated Controller #105

Merged
merged 62 commits into from
Mar 25, 2024
Merged
Show file tree
Hide file tree
Changes from 38 commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
d3c0ff6
rearrange workspace_dir and max_step as arguments to controller
xingyaoww Mar 23, 2024
42675f2
remove unused output
xingyaoww Mar 23, 2024
a1fc2c4
abstract each action into dataclass
xingyaoww Mar 23, 2024
d99ee8a
move actions
xingyaoww Mar 23, 2024
e602bfb
fix action import
xingyaoww Mar 23, 2024
ae677e8
move cmd manager and change method to private
xingyaoww Mar 23, 2024
37a8d9a
move controller
xingyaoww Mar 23, 2024
0027cb5
rename action folder
xingyaoww Mar 23, 2024
6bca774
add state
xingyaoww Mar 23, 2024
a7707c3
a draft of Controller & new agent abstraction
xingyaoww Mar 23, 2024
f974b8d
add agent actions
xingyaoww Mar 23, 2024
45c948c
remove controller file
xingyaoww Mar 23, 2024
0e2d969
add observation to perform a refractor on langchains agent
xingyaoww Mar 23, 2024
9a3368b
revert to make this compatible via translation
xingyaoww Mar 23, 2024
819df04
fix typo and translate error
xingyaoww Mar 23, 2024
723825b
add error to observation
xingyaoww Mar 23, 2024
01ddb5a
index thought as dict
xingyaoww Mar 23, 2024
a746390
refractor controller
xingyaoww Mar 23, 2024
2c57973
fix circular dependency caused by type hint
xingyaoww Mar 23, 2024
3e76c67
add runnable attribute to agent
xingyaoww Mar 24, 2024
e912823
add mixin to denote executable
xingyaoww Mar 24, 2024
1761175
change baseclass
xingyaoww Mar 24, 2024
4f22b3d
make file read/write action compatible w/ docker directory
xingyaoww Mar 24, 2024
5073240
remove event
xingyaoww Mar 24, 2024
b05ede0
Merge commit 'fb1822123a598e01d73bdfa26b5f35de1ae744f3' into codeact-…
xingyaoww Mar 24, 2024
fcee6af
fix some merge issue
xingyaoww Mar 24, 2024
87810fa
fix sandbox w/ permission issue
xingyaoww Mar 24, 2024
ba81d12
cleanup history abstraction since langchains agent is not really usin…
xingyaoww Mar 24, 2024
40501e3
tweak to make langchains agent working
xingyaoww Mar 24, 2024
ab585ad
make all actions return observation
xingyaoww Mar 24, 2024
14bbbb3
fix missing import
xingyaoww Mar 24, 2024
1f479ef
add echo action for agent
xingyaoww Mar 24, 2024
8ca8547
add error code to cmd output obs
xingyaoww Mar 24, 2024
6ef558d
make cmd manager returns cmd output obs
xingyaoww Mar 24, 2024
5c1c762
fix codeact agent to make it work
xingyaoww Mar 24, 2024
43440c6
fix all ruff issue
xingyaoww Mar 24, 2024
ae90a7b
fix mypy
xingyaoww Mar 24, 2024
0e9c7f3
add import agenthub back
xingyaoww Mar 24, 2024
2e343ee
Merge commit '4aa24eb41d51392b17bb4a2df50554fd73f89834' into codeact-…
xingyaoww Mar 24, 2024
2a7a4d7
add message for Action attribute (migrate from previous event)
xingyaoww Mar 24, 2024
0d86b2e
fix typo
xingyaoww Mar 24, 2024
2ecc981
fix instruction setting
xingyaoww Mar 24, 2024
fb10f2d
fix instruction setting
xingyaoww Mar 24, 2024
c405f21
attempt to fix session
xingyaoww Mar 24, 2024
932fb38
ruff fix
xingyaoww Mar 24, 2024
ac9378d
Merge commit '8a64d7c9120653c37087afe925bc9322b1936bbd' into codeact-…
xingyaoww Mar 25, 2024
e6106fa
add .to_dict method for base and observation
xingyaoww Mar 25, 2024
e48c291
add message for recall
xingyaoww Mar 25, 2024
c184e4e
try to simplify the state_updated_info with tuple of action and obs
xingyaoww Mar 25, 2024
bb36f15
update_info to Tuple[Action, Observation]
xingyaoww Mar 25, 2024
d3807dd
make codeact agent and langchains compatible with Tuple[Action, Obser…
xingyaoww Mar 25, 2024
787a012
fix ruff
xingyaoww Mar 25, 2024
c1e386e
fix ruff
xingyaoww Mar 25, 2024
9e07ec0
change to base path to fix minimal langchains agent
xingyaoww Mar 25, 2024
0d204e2
add NullAction to potentially handle for chat scenario
xingyaoww Mar 25, 2024
ff48ea0
Update opendevin/controller/command_manager.py
xingyaoww Mar 25, 2024
9829de5
fix event args
xingyaoww Mar 25, 2024
1fe6557
set the default workspace to "workspace"
xingyaoww Mar 25, 2024
9a1b087
make directory relative (so it does not show up to agent in File*Action)
xingyaoww Mar 25, 2024
c5c0a0f
fix typo
xingyaoww Mar 25, 2024
adf5edd
await to yield for sending observation
xingyaoww Mar 25, 2024
6c78ae0
fix message format
xingyaoww Mar 25, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 73 additions & 68 deletions agenthub/codeact_agent/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,21 @@
import re
from litellm import completion
from termcolor import colored
from typing import List, Dict
from typing import List

from opendevin.agent import Agent
from opendevin.state import State
from opendevin.action import (
Action,
CmdRunAction,
AgentEchoAction,
AgentFinishAction,
)
from opendevin.observation import (
CmdOutputObservation,
AgentMessageObservation,
)

from opendevin.agent import Agent, Message, Role
from opendevin.lib.event import Event
from opendevin.lib.command_manager import CommandManager
from opendevin.sandbox.sandbox import DockerInteractive

assert (
"OPENAI_API_KEY" in os.environ
Expand Down Expand Up @@ -54,9 +63,7 @@ class CodeActAgent(Agent):
def __init__(
self,
instruction: str,
workspace_dir: str,
model_name: str,
max_steps: int = 100
model_name: str
) -> None:
"""
Initializes a new instance of the CodeActAgent class.
Expand All @@ -65,69 +72,67 @@ def __init__(
- instruction (str): The instruction for the agent to execute.
- max_steps (int): The maximum number of steps to run the agent.
"""
super().__init__(instruction, workspace_dir, model_name, max_steps)
self._history = [Message(Role.SYSTEM, SYSTEM_MESSAGE)]
self._history.append(Message(Role.USER, instruction))
self.env = DockerInteractive(workspace_dir=workspace_dir)
super().__init__(instruction, model_name)
self.messages = [
{"role": "system", "content": SYSTEM_MESSAGE},
{"role": "user", "content": instruction},
]
print(colored("===USER:===\n" + instruction, "green"))

def _history_to_messages(self) -> List[Dict]:
return [message.to_dict() for message in self._history]

def run(self) -> None:
"""
Starts the execution of the assigned instruction. This method should
be implemented by subclasses to define the specific execution logic.
"""
for _ in range(self.max_steps):
response = completion(
messages=self._history_to_messages(),
model=self.model_name,
stop=["</execute>"],
temperature=0.0,
seed=42,
)
action = parse_response(response)
self._history.append(Message(Role.ASSISTANT, action))
print(colored("===ASSISTANT:===\n" + action, "yellow"))

command = re.search(r"<execute>(.*)</execute>", action, re.DOTALL)
if command is not None:
# a command was found
command_group = command.group(1)
if command_group.strip() == "exit":
print(colored("Exit received. Exiting...", "red"))
break
# execute the code
# TODO: does exit_code get loaded into Message?
exit_code, observation = self.env.execute(command_group)
self._history.append(Message(Role.ASSISTANT, observation))
print(colored("===ENV OBSERVATION:===\n" + observation, "blue"))
else:
# we could provide a error message for the model to continue similar to
# https://github.com/xingyaoww/mint-bench/blob/main/mint/envs/general_env.py#L18-L23
observation = INVALID_INPUT_MESSAGE
self._history.append(Message(Role.ASSISTANT, observation))
print(colored("===ENV OBSERVATION:===\n" + observation, "blue"))

self.env.close()

def chat(self, message: str) -> None:
"""
Optional method for interactive communication with the agent during its execution. Implementations
can use this method to modify the agent's behavior or state based on chat inputs.

Parameters:
- message (str): The chat message or command.
"""
raise NotImplementedError

# TODO: implement these abstract methods
def add_event(self, event: Event) -> None:
raise NotImplementedError("Implement this abstract method")
def step(self, state: State) -> Action:
updated_info = state.updated_info

if updated_info:

for item in updated_info:
if isinstance(item, Action):
assert isinstance(item, (CmdRunAction, AgentEchoAction)), "Expecting CmdRunAction or AgentEchoAction for Action"

elif isinstance(item, AgentMessageObservation): # warning message from itself
self.messages.append({"role": "user", "content": item.content})
print(colored("===USER:===\n" + item.content, "green"))

elif isinstance(item, CmdOutputObservation):
content = "OBSERVATION:\n" + item.content
content += f"\n[Command {item.command_id} finished with exit code {item.exit_code}]]"
self.messages.append({"role": "user", "content": content})
print(colored("===ENV OBSERVATION:===\n" + content, "blue"))

else:
raise NotImplementedError(f"Unknown observation type: {item}")

response = completion(
messages=self.messages,
model=self.model_name,
stop=["</execute>"],
temperature=0.0,
seed=42,
)
action = parse_response(response)
self.messages.append({"role": "assistant", "content": action})
print(colored("===ASSISTANT:===\n" + action, "yellow"))

command = re.search(r"<execute>(.*)</execute>", action, re.DOTALL)
if command is not None:
# a command was found
command_group = command.group(1)
if command_group.strip() == "exit":
print(colored("Exit received. Exiting...", "red"))
return AgentFinishAction()
return CmdRunAction(command = command_group)
# # execute the code
# # TODO: does exit_code get loaded into Message?
# exit_code, observation = self.env.execute(command_group)
# self._history.append(Message(Role.ASSISTANT, observation))
# print(colored("===ENV OBSERVATION:===\n" + observation, "blue"))
else:
# we could provide a error message for the model to continue similar to
# https://github.com/xingyaoww/mint-bench/blob/main/mint/envs/general_env.py#L18-L23
# observation = INVALID_INPUT_MESSAGE
# self._history.append(Message(Role.ASSISTANT, observation))
# print(colored("===ENV OBSERVATION:===\n" + observation, "blue"))
return AgentEchoAction(content=INVALID_INPUT_MESSAGE) # warning message to itself

def step(self, cmd_mgr: CommandManager) -> Event:
raise NotImplementedError("Implement this abstract method")

def search_memory(self, query: str) -> List[str]:
raise NotImplementedError("Implement this abstract method")
Expand Down
145 changes: 120 additions & 25 deletions agenthub/langchains_agent/__init__.py
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed the old LangchainsAgentImpl and directly integrate it into one class for simplicity.

Original file line number Diff line number Diff line change
@@ -1,9 +1,28 @@
from typing import List
from typing import List, Dict, Type

import agenthub.langchains_agent.utils.llm as llm
from opendevin.agent import Agent
from opendevin.action import (
Action,
CmdRunAction,
CmdKillAction,
BrowseURLAction,
FileReadAction,
FileWriteAction,
AgentRecallAction,
AgentThinkAction,
AgentFinishAction,
)
from opendevin.observation import (
Observation,
CmdOutputObservation,
BrowserOutputObservation,
)
from opendevin.state import State

from agenthub.langchains_agent.utils.monologue import Monologue
from agenthub.langchains_agent.utils.memory import LongTermMemory

from agenthub.langchains_agent.utils.agent import Agent as LangchainsAgentImpl
from opendevin.lib.event import Event

INITIAL_THOUGHTS = [
"I exist!",
Expand Down Expand Up @@ -44,55 +63,131 @@
]


MAX_OUTPUT_LENGTH = 5000
MAX_MONOLOGUE_LENGTH = 20000


ACTION_TYPE_TO_CLASS: Dict[str, Type[Action]] = {
"run": CmdRunAction,
"kill": CmdKillAction,
"browse": BrowseURLAction,
"read": FileReadAction,
"write": FileWriteAction,
"recall": AgentRecallAction,
"think": AgentThinkAction,
"finish": AgentFinishAction,
}

CLASS_TO_ACTION_TYPE: Dict[Type[Action], str] = {v: k for k, v in ACTION_TYPE_TO_CLASS.items()}

class LangchainsAgent(Agent):
_initialized = False

def __init__(self, instruction: str, model_name: str):
super().__init__(instruction, model_name)
self.monologue = Monologue(self.model_name)
self.memory = LongTermMemory()

def _add_event(self, event: dict):
if 'output' in event['args']:
event['args']['output'] = event['args']['output'][:MAX_OUTPUT_LENGTH] + "..."

self.monologue.add_event(event)
self.memory.add_event(event)
if self.monologue.get_total_length() > MAX_MONOLOGUE_LENGTH:
self.monologue.condense()

def _initialize(self):
if self._initialized:
return
self.agent = LangchainsAgentImpl(self.instruction, self.model_name)
next_is_output = False
for thought in INITIAL_THOUGHTS:
thought = thought.replace("$TASK", self.instruction)
if next_is_output:
event = Event("output", {"output": thought})
d = {"action": "output", "args": {"output": thought}}
next_is_output = False
else:
if thought.startswith("RUN"):
command = thought.split("RUN ")[1]
event = Event("run", {"command": command})
d = {"action": "run", "args": {"command": command}}
next_is_output = True

elif thought.startswith("RECALL"):
query = thought.split("RECALL ")[1]
event = Event("recall", {"query": query})
d = {"action": "recall", "args": {"query": query}}
next_is_output = True

elif thought.startswith("BROWSE"):
url = thought.split("BROWSE ")[1]
event = Event("browse", {"url": url})
d = {"action": "browse", "args": {"url": url}}
next_is_output = True
else:
event = Event("think", {"thought": thought})
self.agent.add_event(event)
self._initialized = True
d = {"action": "think", "args": {"thought": thought}}

def add_event(self, event: Event) -> None:
self.agent.add_event(event)
self._add_event(d)
self._initialized = True

def step(self, cmd_mgr) -> Event:
def step(self, state: State) -> Action:
self._initialize()
return self.agent.get_next_action(cmd_mgr)
# TODO: make langchains agent use Action & Observation
# completly from ground up

def search_memory(self, query: str) -> List[str]:
return self.agent.memory.search(query)
# Translate state to action_dict
for info in state.updated_info:
if isinstance(info, Observation):
if isinstance(info, CmdOutputObservation):
if info.error:
d = {"action": "error", "args": {"output": info.content}}
else:
d = {"action": "output", "args": {"output": info.content}}
# elif isinstance(info, UserMessageObservation):
# d = {"action": "output", "args": {"output": info.message}}
# elif isinstance(info, AgentMessageObservation):
# d = {"action": "output", "args": {"output": info.message}}
elif isinstance(info, BrowserOutputObservation):
d = {"action": "output", "args": {"output": info.content}}
else:
raise NotImplementedError(f"Unknown observation type: {info}")
self._add_event(d)
elif isinstance(info, Action):
if isinstance(info, CmdRunAction):
d = {"action": "run", "args": {"command": info.command}}
elif isinstance(info, CmdKillAction):
d = {"action": "kill", "args": {"id": info.id}}
elif isinstance(info, BrowseURLAction):
d = {"action": "browse", "args": {"url": info.url}}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This Action <-> JSON conversion is going to be very important, so it's probably something we want to abstract.

For one, the server and client are going to need to send Actions and Observations back and forth, and will need to serialize them. Ditto for agent <-> LLM interactions.

Could d = info.to_dict() simplify this?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think so! But I feel we should probably prioritize getting this abstraction merged, then start a separate PR to re-write the inner workings of langchains agent completely to adopt this Action and Observation (and the serialization via to_dict) -- otherwise it will be pretty challenge for me to keep chasing with all the new commits keeping pushing to main everyday hahah

elif isinstance(info, FileReadAction):
d = {"action": "read", "args": {"file": info.path}}
elif isinstance(info, FileWriteAction):
d = {"action": "write", "args": {"file": info.path, "content": info.contents}}
elif isinstance(info, AgentRecallAction):
d = {"action": "recall", "args": {"query": info.query}}
elif isinstance(info, AgentThinkAction):
d = {"action": "think", "args": {"thought": info.thought}}
elif isinstance(info, AgentFinishAction):
d = {"action": "finish"}
else:
raise NotImplementedError(f"Unknown action type: {info}")
self._add_event(d)

state.updated_info = []

action_dict = llm.request_action(
self.instruction,
self.monologue.get_thoughts(),
self.model_name,
state.background_commands_obs,
)
if action_dict is None:
action_dict = {"action": "think", "args": {"thought": "..."}}

def chat(self, message: str) -> None:
"""
Optional method for interactive communication with the agent during its execution. Implementations
can use this method to modify the agent's behavior or state based on chat inputs.
# Translate action_dict to Action
action = ACTION_TYPE_TO_CLASS[action_dict["action"]](**action_dict["args"])
self.latest_action = action
return action

def search_memory(self, query: str) -> List[str]:
return self.memory.search(query)

Parameters:
- message (str): The chat message or command.
"""
raise NotImplementedError

Agent.register("LangchainsAgent", LangchainsAgent)
37 changes: 0 additions & 37 deletions agenthub/langchains_agent/utils/agent.py

This file was deleted.

Loading