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

Integrating MemGPT-like Functionality #2937

Draft
wants to merge 42 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
fd5adae
Edited Makefile
khushvind Jul 6, 2024
78be88f
updated num_retries
khushvind Jul 6, 2024
efc1c55
Merge remote-tracking branch 'upstream/main' into MemGPT
khushvind Jul 7, 2024
9ee63e1
Fix ruff issues and added summarizer
khushvind Jul 9, 2024
a4e2a18
Merge remote-tracking branch 'upstream/main'
khushvind Jul 10, 2024
507a67e
Merge remote-tracking branch 'upstream/main' into MemGPT
khushvind Jul 10, 2024
ccffdcc
added summarize function
khushvind Jul 14, 2024
4c3482f
Integrated MemGPT like functionality
khushvind Jul 14, 2024
4b1bf53
Integrated MemGPT like Functionality
khushvind Jul 14, 2024
de1b94b
Merge remote-tracking branch 'upstream/main'
khushvind Jul 15, 2024
5fc4ce4
Merge remote-tracking branch 'upstream/main' into MemGPT_Summarize_St…
khushvind Jul 15, 2024
85a8f4b
Retriving Summary
khushvind Jul 15, 2024
2845c2c
removed bugs
khushvind Jul 15, 2024
7a8299b
removed pip install -q -U google-generativeai from Makefile
khushvind Jul 16, 2024
a7a4c8a
removed bugs
khushvind Jul 18, 2024
0428dcc
Merge remote-tracking branch 'upstream/main' into MemGPT
khushvind Jul 19, 2024
bd9d14f
corrected the max_input_token
khushvind Jul 19, 2024
22e92d7
moved condenser configs to LLMConfig
khushvind Jul 19, 2024
d2b1ae1
fixed issue causing error in test on linux
khushvind Jul 20, 2024
1afd574
Merge remote-tracking branch 'upstream/main' into MemGPT
khushvind Jul 20, 2024
c43ed97
converted each message to Message class with additional attributes
khushvind Jul 21, 2024
6080071
Moved condenser functions to LLM class
khushvind Jul 22, 2024
515e038
removed condenser.py file
khushvind Jul 23, 2024
44d3c9d
Removed ContextWindowExceededError - TokenLimitExceededError already …
khushvind Jul 23, 2024
d93f5ee
Merge remote-tracking branch 'upstream/main' into MemGPT
khushvind Jul 25, 2024
0fece3f
Merge branch 'main' into MemGPT
khushvind Jul 25, 2024
d59be73
build condenser as mixin class
khushvind Jul 26, 2024
85b715d
build condenser as mixin class
khushvind Jul 26, 2024
7c5606d
Merge remote-tracking branch 'origin' into MemGPT
khushvind Jul 26, 2024
d9b3aae
Merge remote-tracking branch 'origin/MemGPT' into MemGPT
khushvind Jul 26, 2024
2a81073
Merge remote-tracking branch 'upstream/main' into MemGPT
khushvind Jul 26, 2024
140253c
replaced get_response with the original llm.completion
khushvind Jul 27, 2024
c7a3713
returning summarize_action to agent controller to add to memory
khushvind Jul 28, 2024
754a9c3
Merge remote-tracking branch 'upstream/main' into MemGPT
khushvind Jul 28, 2024
490b192
removed bug - pass summary in prompt
khushvind Jul 30, 2024
2162c91
modified summarize_messages
khushvind Jul 30, 2024
a90edc8
Merge remote-tracking branch 'upstream/main' into MemGPT
khushvind Jul 31, 2024
8d7bc30
Merge remote-tracking branch 'upstream/main' into MemGPT
khushvind Jul 31, 2024
34caa62
Merged with latest main
khushvind Aug 20, 2024
f30a572
updated prompt for condenser
khushvind Aug 21, 2024
13a9f64
removed print summary message
khushvind Aug 22, 2024
d25e19e
Modified how agent_controller handles summarization actions
khushvind Aug 30, 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
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ install-python-dependencies:
poetry run pip install chroma-hnswlib; \
fi
@poetry install
@poetry run pip install -q -U google-generativeai
khushvind marked this conversation as resolved.
Show resolved Hide resolved
@if [ -f "/etc/manjaro-release" ]; then \
echo "$(BLUE)Detected Manjaro Linux. Installing Playwright dependencies...$(RESET)"; \
poetry run pip install playwright; \
Expand Down
166 changes: 154 additions & 12 deletions agenthub/codeact_agent/codeact_agent.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
#######

from litellm.exceptions import ContextWindowExceededError
khushvind marked this conversation as resolved.
Show resolved Hide resolved

from agenthub.codeact_agent.action_parser import CodeActResponseParser
from agenthub.codeact_agent.prompt import (
COMMAND_DOCS,
Expand All @@ -9,10 +13,16 @@
from opendevin.controller.agent import Agent
from opendevin.controller.state.state import State
from opendevin.core.config import config
from opendevin.core.exceptions import (
ContextWindowLimitExceededError,
SummarizeError,
TokenLimitExceededError,
)
from opendevin.events.action import (
Action,
AgentDelegateAction,
AgentFinishAction,
AgentSummarizeAction,
CmdRunAction,
IPythonRunCellAction,
MessageAction,
Expand All @@ -24,13 +34,17 @@
)
from opendevin.events.serialization.event import truncate_content
from opendevin.llm.llm import LLM
from opendevin.memory.condenser import MemoryCondenser
from opendevin.memory.history import ShortTermHistory
from opendevin.runtime.plugins import (
AgentSkillsRequirement,
JupyterRequirement,
PluginRequirement,
)
from opendevin.runtime.tools import RuntimeTool

#######

ENABLE_GITHUB = True


Expand All @@ -43,6 +57,8 @@ def action_to_str(action: Action) -> str:
return f'{action.thought}\n<execute_browse>\n{action.inputs["task"]}\n</execute_browse>'
elif isinstance(action, MessageAction):
return action.content
elif isinstance(action, AgentSummarizeAction):
return action.summarized_actions
return ''


Expand All @@ -52,6 +68,7 @@ def get_action_message(action: Action) -> dict[str, str] | None:
or isinstance(action, CmdRunAction)
or isinstance(action, IPythonRunCellAction)
or isinstance(action, MessageAction)
or isinstance(action, AgentSummarizeAction)
):
return {
'role': 'user' if action.source == 'user' else 'assistant',
Expand Down Expand Up @@ -87,6 +104,8 @@ def get_observation_message(obs) -> dict[str, str] | None:
str(obs.outputs), max_message_chars
)
return {'role': 'user', 'content': content}
elif isinstance(obs, AgentSummarizeAction):
return {'role': 'user', 'content': obs.summarized_observations}
return None


Expand Down Expand Up @@ -165,6 +184,8 @@ def __init__(
- llm (LLM): The llm to be used by this agent
"""
super().__init__(llm)
self.memory_condenser = MemoryCondenser(llm)
self.attempts_to_condense = 2
khushvind marked this conversation as resolved.
Show resolved Hide resolved
self.reset()

def reset(self) -> None:
Expand Down Expand Up @@ -194,27 +215,148 @@ def step(self, state: State) -> Action:
if latest_user_message and latest_user_message.strip() == '/exit':
return AgentFinishAction()

# prepare what we want to send to the LLM
messages: list[dict[str, str]] = self._get_messages(state)

response = self.llm.completion(
khushvind marked this conversation as resolved.
Show resolved Hide resolved
messages=messages,
stop=[
'</execute_ipython>',
'</execute_bash>',
'</execute_browse>',
],
temperature=0.0,
enyst marked this conversation as resolved.
Show resolved Hide resolved
)
response = None
# give it multiple chances to get a response
# if it fails, we'll try to condense memory
attempt = 0
while not response and attempt < self.attempts_to_condense:
# prepare what we want to send to the LLM
messages: list[dict[str, str]] = self._get_messages(state)
print('No of tokens, ' + str(self.llm.get_token_count(messages)) + '\n')
try:
if self.llm.is_over_token_limit(messages):
raise TokenLimitExceededError()
response = self.llm.completion(
khushvind marked this conversation as resolved.
Show resolved Hide resolved
messages=messages,
stop=[
'</execute_ipython>',
'</execute_bash>',
'</execute_browse>',
],
temperature=0.0,
)
except (ContextWindowExceededError, TokenLimitExceededError):
# Handle the specific exception
print('An error occurred: ')
attempt += 1
# If we got a context alert, try trimming the messages length, then try again
if self.llm.is_over_token_limit(messages):
# A separate call to run a summarizer
self.condense(state=state)
# Try step again
else:
print('step() failed with an unrecognized exception:')
raise ContextWindowLimitExceededError()

# TODO: Manage the response for exception.
return self.action_parser.parse(response)

def condense(
self,
state: State,
Copy link
Contributor

Choose a reason for hiding this comment

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

If you move this .condense function to the LLM class, you can make messages as the input argument. We might need to make each Message a Pydantic class with an additional attribute (e.g., condensable). This may also benefit the ongoing effort that tries to add vision capability in #2848 (comment) (cc @Kaushikdkrikhanu)

Copy link
Author

Choose a reason for hiding this comment

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

This can be done. But on moving .condense to LLM class, it won't be able to add summary to state.history directly. I might also need to pass state as arg to llm.completion along with messages.

If we are trying to make it cleaner (#2937 (comment)), I instead was thinking of moving .condense to MemoryCondenser class, after making each Message a pydantic class. That's where I think .condenser should belong 😅.
WDYT? Which approach seems better?

Copy link
Contributor

Choose a reason for hiding this comment

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

huh good point... maybe caller to LLM should make sure the summary got added to the State - like what we are doing now?

Copy link
Contributor

@xingyaoww xingyaoww Jul 19, 2024

Choose a reason for hiding this comment

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

On a second thought: maybe State is not the place to add these new summarizing actions - EventStream should be: https://github.com/OpenDevin/OpenDevin/blob/c555fb68400bb117011cda3e5d3d95beb5169000/opendevin/controller/agent_controller.py#L395

Maybe you could add an optional argument for LLM class, like condense_callback_fn, which will be called with the condensed Summarized action whenever a condensation is happening. And that callback will add this Summarize action to the event stream?

Copy link
Author

Choose a reason for hiding this comment

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

Can you elaborate on this? How does adding summarizing actions to EventStream help?
MemGPT stores only the summary of events from the start to some event.id. We only need to store one summary in the history. A summary event remains useful until we create a new summary, after which it becomes useless.

Btw, I've moved the .condenser functions to LLM class. MemoryCondenser class used methods of LLM class, so I completely removed it and moved one of its method to LLM class. This also achieves the target (#2937 (comment))

Copy link
Contributor

Choose a reason for hiding this comment

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

Basically, going forward, we hope to make EventStream a single source of truth for all our past message / history (i.e., you get all the history messages from the event stream), so ideally all your condensation actions also should goes into event stream.

But base on my understanding, your current implementation already emit "AgentSummarizeAction"? I think we just need to make sure that action got stored inside eventstream, we should be good to go on this bullet.

Copy link
Collaborator

Choose a reason for hiding this comment

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

@xingyaoww Oh interesting, I played with summaries in the event stream at some point. A couple of thoughts:

  • are they 'real events'? That is, if emitted with the same mechanism as others (which they currently were not), would they be propagated downstream to components listening
  • or, since they replace during runtime the real events they summarize, are they more like 'shadow events'? In which case they're not the truth, but a fleeting version of it, just necessary due to some runtime constraints; the source of truth holds the real events and is able to retrieve them when needed.

Ref: https://opendevin.slack.com/archives/C06QKSD9UBA/p1716737811950479?thread_ts=1716668475.888269&channel=C06QKSD9UBA&message_ts=1716737811.950479

  • they need to be saved I assume, even if it's one at a time, to retrieve at a disconnect or restart/restore etc. Do we save them along with other events or we save them in State? If we save them in the eventstream, then we may want to consider moving some filtering logic too.

On a side note, making it only one in a run simplifies the logic either way.

):
# Start past the system message, and example messages.,
# and collect messages for summarization until we reach the desired truncation token fraction (eg 50%)
# Do not allow truncation for in-context examples of function calling
history: ShortTermHistory = state.history
messages = self._get_messages(state=state)
token_counts = [self.llm.get_token_count([message]) for message in messages]
message_buffer_token_count = sum(
token_counts[2:]
) # no system and example message
MESSAGE_SUMMARY_TRUNC_TOKEN_FRAC = 0.75
khushvind marked this conversation as resolved.
Show resolved Hide resolved
desired_token_count_to_summarize = int(
message_buffer_token_count * MESSAGE_SUMMARY_TRUNC_TOKEN_FRAC
)

candidate_messages_to_summarize = []
last_summarized_event_id = history.last_summarized_event_id
tokens_so_far = 0
for event in history.get_events():
if isinstance(event, AgentSummarizeAction):
action_message = get_action_message(event)
if action_message:
candidate_messages_to_summarize.append(action_message)
tokens_so_far += self.llm.get_token_count([action_message])
observation_message = get_observation_message(event)
if observation_message:
candidate_messages_to_summarize.append(observation_message)
tokens_so_far += self.llm.get_token_count([observation_message])
continue
else:
khushvind marked this conversation as resolved.
Show resolved Hide resolved
message = (
get_action_message(event)
if isinstance(event, Action)
else get_observation_message(event)
)
if message:
candidate_messages_to_summarize.append(message)
tokens_so_far += self.llm.get_token_count([message])
if tokens_so_far > desired_token_count_to_summarize:
last_summarized_event_id = event.id
break

# TODO: Add functionality for preserving last N messages
# MESSAGE_SUMMARY_TRUNC_KEEP_N_LAST = 3
# if preserve_last_N_messages:
# candidate_messages_to_summarize = candidate_messages_to_summarize[:-MESSAGE_SUMMARY_TRUNC_KEEP_N_LAST]
# token_counts = token_counts[:-MESSAGE_SUMMARY_TRUNC_KEEP_N_LAST]

print(f'MESSAGE_SUMMARY_TRUNC_TOKEN_FRAC={MESSAGE_SUMMARY_TRUNC_TOKEN_FRAC}')
# print(f'MESSAGE_SUMMARY_TRUNC_KEEP_N_LAST={MESSAGE_SUMMARY_TRUNC_KEEP_N_LAST}')
print(f'token_counts={token_counts}')
print(f'message_buffer_token_count={message_buffer_token_count}')
print(f'desired_token_count_to_summarize={desired_token_count_to_summarize}')
print(
f'len(candidate_messages_to_summarize)={len(candidate_messages_to_summarize)}'
)
khushvind marked this conversation as resolved.
Show resolved Hide resolved

if len(candidate_messages_to_summarize) == 0:
raise SummarizeError(
f"Summarize error: tried to run summarize, but couldn't find enough messages to compress [len={len(messages)}]"
)

# TODO: Try to make an assistant message come after the cutoff

message_sequence_to_summarize = candidate_messages_to_summarize

if len(message_sequence_to_summarize) <= 1:
# This prevents a potential infinite loop of summarizing the same message over and over
raise SummarizeError(
f"Summarize error: tried to run summarize, but couldn't find enough messages to compress [len={len(message_sequence_to_summarize)} <= 1]"
)
else:
print(
f'Attempting to summarize with last summarized event id = {last_summarized_event_id}'
)

summary_action = self.memory_condenser.summarize_messages(
message_sequence_to_summarize=message_sequence_to_summarize
)
summary_action.last_summarized_event_id = last_summarized_event_id
print(f'Got summary: {summary_action}')
history.add_summary(summary_action)
print('Added summary to history')

def search_memory(self, query: str) -> list[str]:
raise NotImplementedError('Implement this abstract method')
enyst marked this conversation as resolved.
Show resolved Hide resolved

def _get_messages(self, state: State) -> list[dict[str, str]]:
messages = [
{'role': 'system', 'content': self.system_message},
{'role': 'user', 'content': self.in_context_example},
]

for event in state.history.get_events():
if isinstance(event, AgentSummarizeAction):
action_message = get_action_message(event)
if action_message:
messages.append(action_message)
observation_message = get_observation_message(event)
if observation_message:
messages.append(observation_message)
continue
khushvind marked this conversation as resolved.
Show resolved Hide resolved

# create a regular message from an event
message = (
get_action_message(event)
Expand Down
2 changes: 1 addition & 1 deletion agenthub/monologue_agent/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ def _initialize(self, task: str):
else:
self.memory = None

self.memory_condenser = MemoryCondenser()
self.memory_condenser = MemoryCondenser(llm=self.llm)

self._add_initial_thoughts(task)
self._initialized = True
Expand Down
2 changes: 1 addition & 1 deletion opendevin/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ class LLMConfig:
aws_access_key_id: str | None = None
aws_secret_access_key: str | None = None
aws_region_name: str | None = None
num_retries: int = 5
num_retries: int = 15
retry_min_wait: int = 3
retry_max_wait: int = 60
timeout: int | None = None
Expand Down
26 changes: 26 additions & 0 deletions opendevin/core/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,29 @@ def __init__(self, message='Agent must return an action'):
class LLMResponseError(Exception):
def __init__(self, message='Failed to retrieve action from LLM response'):
super().__init__(message)


class TokenLimitExceededError(Exception):
"""Exception raised when the user-defined max_input_tokens limit is exceeded."""

def __init__(self, message='User-defined token limit exceeded. Condensing memory.'):
super().__init__(message)


class ContextWindowLimitExceededError(Exception):
def __init__(
self, message='Context window limit exceeded. Unable to condense memory.'
):
super().__init__(message)


class SummarizeError(Exception):
"""Exception raised when message can't be Summarized."""

def __init__(self, message='Error Summarizing The Memory'):
super().__init__(message)


class InvalidSummaryResponseError(Exception):
def __init__(self, message='Invalid summary response'):
super().__init__(message)
37 changes: 34 additions & 3 deletions opendevin/events/action/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,49 @@ def message(self) -> str:
return f'Agent state changed to {self.agent_state}'


# @dataclass
# class AgentSummarizeAction(Action):
# summary: str
# action: str = ActionType.SUMMARIZE
# _chunk_start: int = -1
# _chunk_end: int = -1

# @property
# def message(self) -> str:
# return self.summary

# def __str__(self) -> str:
# ret = '**AgentSummarizeAction**\n'
# ret += f'SUMMARY: {self.summary}'
# return


@dataclass
class AgentSummarizeAction(Action):
summary: str
"""
Action to summarize a list of events.

Attributes:
- summarized_actions: A sentence summarizing all the actions.
- summarized_observations: A few sentences summarizing all the observations.
"""

summarized_actions: str = ''
summarized_observations: str = ''
action: str = ActionType.SUMMARIZE
# _chunk_start: int = -1
# _chunk_end: int = -1
last_summarized_event_id = None
is_delegate_summary: bool = False
khushvind marked this conversation as resolved.
Show resolved Hide resolved

@property
def message(self) -> str:
return self.summary
return self.summarized_observations

def __str__(self) -> str:
ret = '**AgentSummarizeAction**\n'
ret += f'SUMMARY: {self.summary}'
ret += f'SUMMARIZED ACTIONS: {self.summarized_actions}\n'
ret += f'SUMMARIZED OBSERVATIONS: {self.summarized_observations}\n'
return ret


Expand Down
2 changes: 2 additions & 0 deletions opendevin/events/serialization/action.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
AgentDelegateAction,
AgentFinishAction,
AgentRejectAction,
AgentSummarizeAction,
ChangeAgentStateAction,
)
from opendevin.events.action.browse import BrowseInteractiveAction, BrowseURLAction
Expand Down Expand Up @@ -31,6 +32,7 @@
ModifyTaskAction,
ChangeAgentStateAction,
MessageAction,
AgentSummarizeAction,
)

ACTION_TYPE_TO_CLASS = {action_class.action: action_class for action_class in actions} # type: ignore[attr-defined]
Expand Down
Loading