mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-26 05:48:36 +08:00
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:
parent
5277c43c49
commit
98adbf54ec
@ -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):
|
||||
|
||||
@ -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'}]
|
||||
|
||||
@ -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
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
5
opendevin/memory/__init__.py
Normal file
5
opendevin/memory/__init__.py
Normal file
@ -0,0 +1,5 @@
|
||||
from .condenser import MemoryCondenser
|
||||
from .history import ShortTermHistory
|
||||
from .memory import LongTermMemory
|
||||
|
||||
__all__ = ['LongTermMemory', 'ShortTermHistory', 'MemoryCondenser']
|
||||
26
opendevin/memory/condenser.py
Normal file
26
opendevin/memory/condenser.py
Normal 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
|
||||
54
opendevin/memory/history.py
Normal file
54
opendevin/memory/history.py
Normal 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
|
||||
@ -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
|
||||
Loading…
x
Reference in New Issue
Block a user