from __future__ import annotations from pydantic import Field from rich.text import Text from openhands.sdk.event.base import Event, LLMConvertibleEvent from openhands.sdk.event.types import EventID, SourceType from openhands.sdk.llm import Message, TextContent class Condensation(Event): """This action indicates a condensation of the conversation history is happening.""" forgotten_event_ids: set[EventID] = Field( default_factory=set, description="The IDs of the events that are being forgotten " "(removed from the `View` given to the LLM).", ) summary: str | None = Field( default=None, description="An optional summary of the events being forgotten." ) summary_offset: int | None = Field( default=None, ge=0, description="An optional offset to the start of the resulting view (after" " forgotten events have been removed) indicating where the summary should be" " inserted. If not provided, the summary will not be inserted into the view.", ) llm_response_id: EventID = Field( description=( "Completion or Response ID of the LLM response that generated this event" ), ) source: SourceType = "environment" @property def visualize(self) -> Text: text = Text() text.append("Auto Conversation Condensation Triggered.\n", style="bold") text.append(f"Forgetting {len(self.forgotten_event_ids)} events\n") if self.summary: text.append("\n[Summary of Events Being Forgotten]\n", style="bold") text.append(f"{self.summary}\n") return text @property def summary_event(self) -> CondensationSummaryEvent: """Generates a CondensationSummaryEvent. Since summary events are not part of the main event store and are generated dynamically, this property ensures the created event has a unique and consistent ID based on the condensation event's ID. Raises: ValueError: If no summary is present. """ if self.summary is None: raise ValueError("No summary present to generate CondensationSummaryEvent.") # Create a deterministic ID for the summary event. # This ID will be unique amongst all auto-generated IDs (by virtue of the # "-summary" suffix). # These events are not intended to be stored alongside regular events, but the # ID is still compatible with the file-based event store. summary_id = f"{self.id}-summary" return CondensationSummaryEvent( id=summary_id, summary=self.summary, source=self.source, ) @property def has_summary_metadata(self) -> bool: """Checks if both summary and summary_offset are present.""" return self.summary is not None and self.summary_offset is not None def apply(self, events: list[LLMConvertibleEvent]) -> list[LLMConvertibleEvent]: """Applies the condensation to a list of events. This method removes events that are marked to be forgotten and returns a new list of events. If the summary metadata is present (both summary and offset), the corresponding CondensationSummaryEvent will be inserted at the specified offset _after_ the forgotten events have been removed. """ output = [event for event in events if event.id not in self.forgotten_event_ids] if self.has_summary_metadata: assert self.summary_offset is not None summary_event = self.summary_event output.insert(self.summary_offset, summary_event) return output class CondensationRequest(Event): """This action is used to request a condensation of the conversation history. Attributes: action (str): The action type, namely ActionType.CONDENSATION_REQUEST. """ source: SourceType = "environment" @property def visualize(self) -> Text: text = Text() text.append("Conversation Condensation Requested\n", style="bold") message = ( "A condensation of the conversation history has been requested to " "manage context window usage.\n" ) text.append(message) return text class CondensationSummaryEvent(LLMConvertibleEvent): """This event represents a summary generated by a condenser.""" summary: str """The summary text.""" source: SourceType = "environment" def to_llm_message(self) -> Message: return Message( role="user", content=[TextContent(text=self.summary)], )