Spaces:
Build error
Build error
from __future__ import annotations | |
from abc import ABC, abstractmethod | |
from contextlib import contextmanager | |
from typing import Any | |
from pydantic import BaseModel | |
from openhands.controller.state.state import State | |
from openhands.core.config.condenser_config import CondenserConfig | |
from openhands.events.action.agent import CondensationAction | |
from openhands.memory.view import View | |
CONDENSER_METADATA_KEY = 'condenser_meta' | |
"""Key identifying where metadata is stored in a `State` object's `extra_data` field.""" | |
def get_condensation_metadata(state: State) -> list[dict[str, Any]]: | |
"""Utility function to retrieve a list of metadata batches from a `State`. | |
Args: | |
state: The state to retrieve metadata from. | |
Returns: | |
list[dict[str, Any]]: A list of metadata batches, each representing a condensation. | |
""" | |
if CONDENSER_METADATA_KEY in state.extra_data: | |
return state.extra_data[CONDENSER_METADATA_KEY] | |
return [] | |
CONDENSER_REGISTRY: dict[type[CondenserConfig], type[Condenser]] = {} | |
"""Registry of condenser configurations to their corresponding condenser classes.""" | |
class Condensation(BaseModel): | |
"""Produced by a condenser to indicate the history has been condensed.""" | |
action: CondensationAction | |
class Condenser(ABC): | |
"""Abstract condenser interface. | |
Condensers take a list of `Event` objects and reduce them into a potentially smaller list. | |
Agents can use condensers to reduce the amount of events they need to consider when deciding which action to take. To use a condenser, agents can call the `condensed_history` method on the current `State` being considered and use the results instead of the full history. | |
If the condenser returns a `Condensation` instead of a `View`, the agent should return `Condensation.action` instead of producing its own action. On the next agent step the condenser will use that condensation event to produce a new `View`. | |
""" | |
def __init__(self): | |
self._metadata_batch: dict[str, Any] = {} | |
self._llm_metadata: dict[str, Any] = {} | |
def add_metadata(self, key: str, value: Any) -> None: | |
"""Add information to the current metadata batch. | |
Any key/value pairs added to the metadata batch will be recorded in the `State` at the end of the current condensation. | |
Args: | |
key: The key to store the metadata under. | |
value: The metadata to store. | |
""" | |
self._metadata_batch[key] = value | |
def write_metadata(self, state: State) -> None: | |
"""Write the current batch of metadata to the `State`. | |
Resets the current metadata batch: any metadata added after this call will be stored in a new batch and written to the `State` at the end of the next condensation. | |
""" | |
if CONDENSER_METADATA_KEY not in state.extra_data: | |
state.extra_data[CONDENSER_METADATA_KEY] = [] | |
if self._metadata_batch: | |
state.extra_data[CONDENSER_METADATA_KEY].append(self._metadata_batch) | |
# Since the batch has been written, clear it for the next condensation | |
self._metadata_batch = {} | |
def metadata_batch(self, state: State): | |
"""Context manager to ensure batched metadata is always written to the `State`.""" | |
try: | |
yield | |
finally: | |
self.write_metadata(state) | |
def condense(self, View) -> View | Condensation: | |
"""Condense a sequence of events into a potentially smaller list. | |
New condenser strategies should override this method to implement their own condensation logic. Call `self.add_metadata` in the implementation to record any relevant per-condensation diagnostic information. | |
Args: | |
View: A view of the history containing all events that should be condensed. | |
Returns: | |
View | Condensation: A condensed view of the events or an event indicating the history has been condensed. | |
""" | |
def condensed_history(self, state: State) -> View | Condensation: | |
"""Condense the state's history.""" | |
self._llm_metadata = state.to_llm_metadata('condenser') | |
with self.metadata_batch(state): | |
return self.condense(state.view) | |
def register_config(cls, configuration_type: type[CondenserConfig]) -> None: | |
"""Register a new condenser configuration type. | |
Instances of registered configuration types can be passed to `from_config` to create instances of the corresponding condenser. | |
Args: | |
configuration_type: The type of configuration used to create instances of the condenser. | |
Raises: | |
ValueError: If the configuration type is already registered. | |
""" | |
if configuration_type in CONDENSER_REGISTRY: | |
raise ValueError( | |
f'Condenser configuration {configuration_type} is already registered' | |
) | |
CONDENSER_REGISTRY[configuration_type] = cls | |
def from_config(cls, config: CondenserConfig) -> Condenser: | |
"""Create a condenser from a configuration object. | |
Args: | |
config: Configuration for the condenser. | |
Returns: | |
Condenser: A condenser instance. | |
Raises: | |
ValueError: If the condenser type is not recognized. | |
""" | |
try: | |
condenser_class = CONDENSER_REGISTRY[type(config)] | |
return condenser_class.from_config(config) | |
except KeyError: | |
raise ValueError(f'Unknown condenser config: {config}') | |
class RollingCondenser(Condenser, ABC): | |
"""Base class for a specialized condenser strategy that applies condensation to a rolling history. | |
The rolling history is generated by `View.from_events`, which analyzes all events in the history and produces a `View` object representing what will be sent to the LLM. | |
If `should_condense` says so, the condenser is then responsible for generating a `Condensation` object from the `View` object. This will be added to the event history which should -- when given to `get_view` -- produce the condensed `View` to be passed to the LLM. | |
""" | |
def should_condense(self, view: View) -> bool: | |
"""Determine if a view should be condensed.""" | |
def get_condensation(self, view: View) -> Condensation: | |
"""Get the condensation from a view.""" | |
def condense(self, view: View) -> View | Condensation: | |
# If we trigger the condenser-specific condensation threshold, compute and return | |
# the condensation. | |
if self.should_condense(view): | |
return self.get_condensation(view) | |
# Otherwise we're safe to just return the view. | |
else: | |
return view | |