Small refactoring (#1614)

* move MemoryCondenser, LongTermMemory, json, out of the monologue

* PlannerAgent and Microagents use the custom json.loads/dumps

* Move short term history out of monologue agent...

* move memory in their package

* add __init__
This commit is contained in:
Engel Nyst 2024-05-11 17:15:19 +02:00 committed by GitHub
parent 5277c43c49
commit 98adbf54ec
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 115 additions and 120 deletions

View File

@ -1,10 +1,11 @@
import json
from json import JSONDecodeError
from jinja2 import BaseLoader, Environment
from opendevin.controller.agent import Agent
from opendevin.controller.state.state import State
from opendevin.core.exceptions import LLMOutputError
from opendevin.core.utils import json
from opendevin.events.action import Action, action_from_dict
from opendevin.llm.llm import LLM
@ -28,32 +29,18 @@ def parse_response(orig_response: str) -> Action:
action_dict = json.loads(response)
action = action_from_dict(action_dict)
return action
except json.JSONDecodeError as e:
except JSONDecodeError as e:
raise LLMOutputError(
'Invalid JSON in response. Please make sure the response is a valid JSON object.'
) from e
raise LLMOutputError('No valid JSON object found in response.')
def my_encoder(obj):
"""
Encodes objects as dictionaries
Parameters:
- obj (Object): An object that will be converted
Returns:
- dict: If the object can be converted it is returned in dict format
"""
if hasattr(obj, 'to_dict'):
return obj.to_dict()
def to_json(obj, **kwargs):
"""
Serialize an object to str format
"""
return json.dumps(obj, default=my_encoder, **kwargs)
return json.dumps(obj, **kwargs)
class MicroAgent(Agent):

View File

@ -1,5 +1,4 @@
import agenthub.monologue_agent.utils.prompts as prompts
from agenthub.monologue_agent.utils.monologue import Monologue
from opendevin.controller.agent import Agent
from opendevin.controller.state.state import State
from opendevin.core.config import config
@ -24,9 +23,11 @@ from opendevin.events.observation import (
Observation,
)
from opendevin.llm.llm import LLM
from opendevin.memory.condenser import MemoryCondenser
from opendevin.memory.history import ShortTermHistory
if config.agent.memory_enabled:
from agenthub.monologue_agent.utils.memory import LongTermMemory
from opendevin.memory.memory import LongTermMemory
MAX_TOKEN_COUNT_PADDING = 512
MAX_OUTPUT_LENGTH = 5000
@ -85,8 +86,9 @@ class MonologueAgent(Agent):
"""
_initialized = False
monologue: Monologue
monologue: ShortTermHistory
memory: 'LongTermMemory | None'
memory_condenser: MemoryCondenser
def __init__(self, llm: LLM):
"""
@ -97,7 +99,7 @@ class MonologueAgent(Agent):
"""
super().__init__(llm)
def _add_event(self, event: dict):
def _add_event(self, event_dict: dict):
"""
Adds a new event to the agent's monologue and memory.
Monologue automatically condenses when it gets too large.
@ -107,29 +109,33 @@ class MonologueAgent(Agent):
"""
if (
'args' in event
and 'output' in event['args']
and len(event['args']['output']) > MAX_OUTPUT_LENGTH
'args' in event_dict
and 'output' in event_dict['args']
and len(event_dict['args']['output']) > MAX_OUTPUT_LENGTH
):
event['args']['output'] = (
event['args']['output'][:MAX_OUTPUT_LENGTH] + '...'
event_dict['args']['output'] = (
event_dict['args']['output'][:MAX_OUTPUT_LENGTH] + '...'
)
self.monologue.add_event(event)
self.monologue.add_event(event_dict)
if self.memory is not None:
self.memory.add_event(event)
self.memory.add_event(event_dict)
# Test monologue token length
prompt = prompts.get_request_action_prompt(
'',
self.monologue.get_thoughts(),
self.monologue.get_events(),
[],
)
messages = [{'content': prompt, 'role': 'user'}]
token_count = self.llm.get_token_count(messages)
if token_count + MAX_TOKEN_COUNT_PADDING > self.llm.max_input_tokens:
self.monologue.condense(self.llm)
prompt = prompts.get_summarize_monologue_prompt(self.monologue.events)
summary_response = self.memory_condenser.condense(
summarize_prompt=prompt, llm=self.llm
)
self.monologue.events = prompts.parse_summary_response(summary_response)
def _initialize(self, task: str):
"""
@ -151,12 +157,14 @@ class MonologueAgent(Agent):
if task is None or task == '':
raise AgentNoInstructionError()
self.monologue = Monologue()
self.monologue = ShortTermHistory()
if config.agent.memory_enabled:
self.memory = LongTermMemory()
else:
self.memory = None
self.memory_condenser = MemoryCondenser()
self._add_initial_thoughts(task)
self._initialized = True
@ -226,7 +234,7 @@ class MonologueAgent(Agent):
prompt = prompts.get_request_action_prompt(
state.plan.main_goal,
self.monologue.get_thoughts(),
self.monologue.get_events(),
state.background_commands_obs,
)
messages = [{'content': prompt, 'role': 'user'}]

View File

@ -1,79 +0,0 @@
import agenthub.monologue_agent.utils.json as json
import agenthub.monologue_agent.utils.prompts as prompts
from opendevin.core.exceptions import AgentEventTypeError
from opendevin.core.logger import opendevin_logger as logger
from opendevin.llm.llm import LLM
class Monologue:
"""
The monologue is a representation for the agent's internal monologue where it can think.
The agent has the capability of using this monologue for whatever it wants.
"""
def __init__(self):
"""
Initialize the empty list of thoughts
"""
self.thoughts = []
def add_event(self, t: dict):
"""
Adds an event to memory if it is a valid event.
Parameters:
- t (dict): The thought that we want to add to memory
Raises:
- AgentEventTypeError: If t is not a dict
"""
if not isinstance(t, dict):
raise AgentEventTypeError()
self.thoughts.append(t)
def get_thoughts(self):
"""
Get the current thoughts of the agent.
Returns:
- list: The list of thoughts that the agent has.
"""
return self.thoughts
def get_total_length(self):
"""
Gives the total number of characters in all thoughts
Returns:
- Int: Total number of chars in thoughts.
"""
total_length = 0
for t in self.thoughts:
try:
total_length += len(json.dumps(t))
except TypeError as e:
logger.error('Error serializing thought: %s', str(e), exc_info=False)
return total_length
def condense(self, llm: LLM):
"""
Attempts to condense the monologue by using the llm
Parameters:
- llm (LLM): llm to be used for summarization
Raises:
- Exception: the same exception as it got from the llm or processing the response
"""
try:
prompt = prompts.get_summarize_monologue_prompt(self.thoughts)
messages = [{'content': prompt, 'role': 'user'}]
resp = llm.completion(messages=messages)
summary_resp = resp['choices'][0]['message']['content']
self.thoughts = prompts.parse_summary_response(summary_resp)
except Exception as e:
logger.error('Error condensing thoughts: %s', str(e), exc_info=False)
# TODO If the llm fails with ContextWindowExceededError, we can try to condense the monologue chunk by chunk
raise

View File

@ -3,6 +3,7 @@ from json import JSONDecodeError
from opendevin.core.config import config
from opendevin.core.exceptions import LLMOutputError
from opendevin.core.utils import json
from opendevin.events.action import (
Action,
action_from_dict,
@ -11,8 +12,6 @@ from opendevin.events.observation import (
CmdOutputObservation,
)
from . import json
ACTION_PROMPT = """
You're a thoughtful robot. Your main task is this:
%(task)s

View File

@ -1,8 +1,7 @@
import json
from opendevin.controller.state.plan import Plan
from opendevin.core.logger import opendevin_logger as logger
from opendevin.core.schema import ActionType
from opendevin.core.utils import json
from opendevin.events.action import (
Action,
NullAction,
@ -176,9 +175,6 @@ def parse_response(response: str) -> Action:
Returns:
- Action: A valid next action to perform from model output
"""
json_start = response.find('{')
json_end = response.rfind('}') + 1
response = response[json_start:json_end]
action_dict = json.loads(response)
if 'contents' in action_dict:
# The LLM gets confused here. Might as well be robust

View File

@ -0,0 +1,5 @@
from .condenser import MemoryCondenser
from .history import ShortTermHistory
from .memory import LongTermMemory
__all__ = ['LongTermMemory', 'ShortTermHistory', 'MemoryCondenser']

View File

@ -0,0 +1,26 @@
from opendevin.core.logger import opendevin_logger as logger
from opendevin.llm.llm import LLM
class MemoryCondenser:
def condense(self, summarize_prompt: str, llm: LLM):
"""
Attempts to condense the monologue by using the llm
Parameters:
- llm (LLM): llm to be used for summarization
Raises:
- Exception: the same exception as it got from the llm or processing the response
"""
try:
messages = [{'content': summarize_prompt, 'role': 'user'}]
resp = llm.completion(messages=messages)
summary_response = resp['choices'][0]['message']['content']
return summary_response
except Exception as e:
logger.error('Error condensing thoughts: %s', str(e), exc_info=False)
# TODO If the llm fails with ContextWindowExceededError, we can try to condense the monologue chunk by chunk
raise

View File

@ -0,0 +1,54 @@
import opendevin.core.utils.json as json
from opendevin.core.exceptions import AgentEventTypeError
from opendevin.core.logger import opendevin_logger as logger
class ShortTermHistory:
"""
The short term history is the most recent series of events.
An agent can send this in the prompt or use it for other purpose.
"""
def __init__(self):
"""
Initialize the empty list of events
"""
self.events = []
def add_event(self, event_dict: dict):
"""
Adds an event to memory if it is a valid event.
Parameters:
- event_dict (dict): The event that we want to add to memory
Raises:
- AgentEventTypeError: If event_dict is not a dict
"""
if not isinstance(event_dict, dict):
raise AgentEventTypeError()
self.events.append(event_dict)
def get_events(self):
"""
Get the events in the agent's recent history.
Returns:
- List: The list of events that the agent remembers easily.
"""
return self.events
def get_total_length(self):
"""
Gives the total number of characters in all history
Returns:
- Int: Total number of characters of the recent history.
"""
total_length = 0
for t in self.events:
try:
total_length += len(json.dumps(t))
except TypeError as e:
logger.error('Error serializing event: %s', str(e), exc_info=False)
return total_length

View File

@ -15,8 +15,7 @@ from tenacity import (
from opendevin.core.config import config
from opendevin.core.logger import opendevin_logger as logger
from . import json
from opendevin.core.utils import json
num_retries = config.llm.num_retries
retry_min_wait = config.llm.retry_min_wait