Skip to content

Commit

Permalink
fix behavior, 3 attempts then fail
Browse files Browse the repository at this point in the history
  • Loading branch information
enyst committed May 14, 2024
1 parent 39a5c9a commit cb4eef8
Show file tree
Hide file tree
Showing 4 changed files with 79 additions and 110 deletions.
3 changes: 1 addition & 2 deletions agenthub/monologue_agent/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ def _add_core_event(self, event_dict: dict):
Parameters:
- event (dict): The event that will be added to monologue and memory
"""
self.monologue.add_core_event(event_dict)
self.monologue.add_default_event(event_dict)
if self.memory is not None:
self.memory.add_event(event_dict)

Expand Down Expand Up @@ -180,7 +180,6 @@ def _initialize(self, task: str):

self.memory_condenser = MemoryCondenser(
action_prompt=prompts.get_action_prompt,
action_prompt_with_defaults=prompts.generate_action_prompt_with_defaults,
summarize_prompt=prompts.get_summarize_prompt,
)

Expand Down
2 changes: 1 addition & 1 deletion agenthub/monologue_agent/utils/prompts.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ def generate_action_prompt_with_defaults(**kwargs):
for key, value in kwargs.items():
if key in ['default_events', 'recent_events'] and value is not None:
monologue.extend(value)
elif key == 'background_commands' and value is not None:
elif key == 'background_commands':
formatted_kwargs[key] = format_background_commands(value)
else:
formatted_kwargs[key] = value
Expand Down
169 changes: 68 additions & 101 deletions opendevin/memory/condenser.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from opendevin.core.utils import json
from opendevin.llm.llm import LLM

MAX_TOKEN_COUNT_PADDING = 16000 # FIXME debug value
MAX_TOKEN_COUNT_PADDING = 127000 # FIXME debug value


class MemoryCondenser:
Expand All @@ -15,7 +15,6 @@ class MemoryCondenser:
def __init__(
self,
action_prompt: Callable[..., str],
action_prompt_with_defaults: Callable[..., str],
summarize_prompt: Callable[[list[dict], list[dict]], str],
):
"""
Expand All @@ -31,7 +30,6 @@ def __init__(
- summarize_prompt (Callable): The function to generate a summarize prompt. The function should accept core events and recent events as arguments.
"""
self.action_prompt = action_prompt
self.action_prompt_with_defaults = action_prompt_with_defaults
self.summarize_prompt = summarize_prompt

def condense(
Expand All @@ -40,146 +38,115 @@ def condense(
default_events: list[dict],
recent_events: list[dict],
background_commands: list,
) -> tuple[list[dict], bool]:
) -> list[dict] | bool:
"""
Attempts to condense the recent events of the monologue by using the llm, if necessary. Returns unmodified prompt if it is already short enough.
Attempts to condense the recent events of the monologue by using the llm, if necessary. Returns the condensed recent events if successful, or False if not.
It includes default events for context, but does not alter them.
Condenses the monologue with action and summary prompts using the llm when necessary.
It includes default events in the prompt for context, but does not alter them.
Condenses the monologue using a summary prompt.
Checks if the action_prompt (including events) needs condensation based on token count, and doesn't attempt condensing if not.
Returns unmodified list of recent events if it is already short enough.
Parameters:
- llm (LLM): LLM to be used for summarization.
- default_events (list[dict]): List of default events that should remain unchanged.
- recent_events (list[dict]): List of recent events that may be condensed.
- background_commands (list): List of background commands to be included in the prompt.
Returns:
- tuple: A tuple containing the condensed string, or the unmodified string if it wasn't performed, and a boolean indicating if condensation was performed.
- list[dict] | bool: The condensed recent events if successful, unmodified list if unnecessary, or False if condensation failed.
"""

action_prompt = self.action_prompt(
'task', default_events, recent_events, background_commands
'', default_events, recent_events, background_commands
)
summarize_prompt = self.summarize_prompt(default_events, recent_events)

# test prompt token length
if not self.needs_condense(llm, default_events, recent_events):
return action_prompt, False
if not self.needs_condense(llm, action_prompt):
return recent_events

try:
return self.process_in_chunks(llm, default_events, recent_events), True
except Exception as e:
logger.error('Condensation failed: %s', str(e), exc_info=False)
return action_prompt, False
# try 3 times to condense
attempt_count = 0
failed = False

def needs_condense(
self, llm: LLM, default_events: list[dict], recent_events: list[dict]
) -> bool:
"""
Checks if the prompt needs to be condensed based on the token count against the limits of the llm passed in the call.
while attempt_count < 3 and not failed:
# attempt to condense the recent events
new_recent_events = self.attempt_condense(
llm, default_events, recent_events
)

Parameters:
- llm (LLM): The llm to use for checking the token count.
- default_events (list[dict]): List of core events that should remain unchanged.
- recent_events (list[dict]): List of recent events that may be condensed.
if not new_recent_events:
logger.debug('Condensation failed: new_recent_events is empty')
return False

Returns:
- bool: True if the prompt needs to be condensed, False otherwise.
"""
action_prompt = self.action_prompt('', default_events, recent_events, [])
# re-generate the action prompt with the condensed events
new_action_prompt = self.action_prompt(
'', default_events, new_recent_events, background_commands
)

token_count = llm.get_token_count([{'content': action_prompt, 'role': 'user'}])
return token_count >= self.get_token_limit(llm)
# check if the new prompt still needs to be condensed
if self.needs_condense(llm, new_action_prompt):
attempt_count += 1
continue

def process_in_chunks(
self,
llm: LLM,
default_events: list[dict],
recent_events: list[dict],
) -> list[dict]:
"""
Condenses recent events in chunks, while preserving default events for context.
"""
# Initial part of the prompt includes default memories
initial_prompt = self.action_prompt_with_defaults(default_events=default_events)
return self.attempt_condense(
llm, default_events, recent_events, initial_prompt, 0
)
# the new prompt is within the token limit
return new_recent_events

except Exception as e:
logger.error('Condensation failed: %s', str(e), exc_info=False)
return False
return False

def attempt_condense(
self,
llm: LLM,
default_events: list[dict],
recent_events: list[dict],
action_prompt: str,
attempt_count: int,
) -> list[dict]:
if attempt_count >= 5 or not recent_events:
return recent_events # FIXME

# get the summarize prompt to use
summarize_prompt = self.summarize_prompt(default_events, recent_events)

) -> list[dict] | None:
# Split events
midpoint = len(recent_events) // 2
first_half = recent_events[:midpoint]
second_half = recent_events[midpoint:]

# Try to condense the first half
# FIXME it summarized the default events
condensed_events = self.process_events(
llm,
default_events=default_events,
recent_events=first_half,
summarize_prompt=summarize_prompt,
)
# attempt to condense the first half of the recent events
summarize_prompt = self.summarize_prompt(default_events, first_half)

new_prompt = self.action_prompt_with_defaults(
default_events=default_events, recent_events=condensed_events
)
new_token_count = llm.get_token_count([{'content': new_prompt, 'role': 'user'}])

if new_token_count < self.get_token_limit(llm):
return condensed_events
else:
# If not successful, attempt again
# FIXME first half of the second half
return self.attempt_condense(
llm=llm,
default_events=default_events,
recent_events=second_half,
action_prompt=new_prompt,
attempt_count=attempt_count + 1,
)

def process_events(
self,
llm: LLM,
default_events: list[dict],
recent_events: list[dict],
summarize_prompt: str,
) -> list[dict]:
# send the summarize prompt to the LLM
messages = [{'content': summarize_prompt, 'role': 'user'}]
response = llm.completion(messages=messages)
response_content = response['choices'][0]['message']['content']
parsed_summary = json.loads(response_content)

# the new list of recent events will be source events or summarize actions
# in the 'new_monologue' key
condensed_events = parsed_summary['new_monologue']

# new recent events list
if (
not condensed_events
or not isinstance(condensed_events, list)
or len(condensed_events) == 0
):
return None

condensed_events.extend(second_half)
return condensed_events

def needs_condense(self, llm: LLM, action_prompt: str) -> bool:
"""
Send a list of events to the LLM with the specific summary prompt.
Checks if the prompt needs to be condensed based on the token count against the limits of the llm passed in the call.
Parameters:
- llm (LLM): The LLM to use for processing the events.
- recent_events (list[dict]): The events to be processed, where each event is a dictionary containing various attributes.
- summarize_prompt (str): The initial prompt used for summarization.
- llm (LLM): The llm to use for checking the token count.
- action_prompt (str): The prompt to check for token count.
Returns:
- list[dict]: The new list of recent events.
- bool: True if the prompt needs to be condensed, False otherwise.
"""
# apply the prompt template to the events
summarize_prompt = self.summarize_prompt(default_events, recent_events)

# send the combined prompt to the LLM
messages = [{'content': f'{summarize_prompt}', 'role': 'user'}]
response = llm.completion(messages=messages)
response_content = response['choices'][0]['message']['content']
parsed_summary = json.loads(response_content)
return parsed_summary['new_monologue']

token_count = llm.get_token_count([{'content': action_prompt, 'role': 'user'}])
return token_count >= self.get_token_limit(llm)

def get_token_limit(self, llm: LLM) -> int:
"""
Expand Down
15 changes: 9 additions & 6 deletions opendevin/memory/history.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ def __init__(self):
# core events are events that the agent learned in the initial prompt
self.default_events = []

def add_core_event(self, event_dict: dict):
def add_default_event(self, event_dict: dict):
"""
Adds an event to core memory if it is a valid event.
Adds an event to the default memory (sent in every prompt), if it is a valid event.
Parameters:
- event_dict (dict): The event that we want to add to memory
Expand Down Expand Up @@ -69,19 +69,22 @@ def get_default_events(self):
"""
return self.default_events

def get_recent_events(self, num_events=5):
def get_recent_events(self, num_events=None):
"""
Get the most recent events in the agent's short term history.
Will not return core events.
Will not return default events.
Parameters:
- num_events (int): The number of recent events to return
- num_events (int): The number of recent events to return, defaults to all events.
Returns:
- List: The list of the most recent events.
"""
return self.recent_events[-num_events:]
if num_events is None:
return self.recent_events
else:
return self.recent_events[-num_events:]

def get_total_length(self):
"""
Expand Down

0 comments on commit cb4eef8

Please sign in to comment.