Spaces:
Build error
Build error
from __future__ import annotations | |
from openhands.core.config.condenser_config import LLMSummarizingCondenserConfig | |
from openhands.core.message import Message, TextContent | |
from openhands.events.action.agent import CondensationAction | |
from openhands.events.observation.agent import AgentCondensationObservation | |
from openhands.events.serialization.event import truncate_content | |
from openhands.llm import LLM | |
from openhands.memory.condenser.condenser import ( | |
Condensation, | |
RollingCondenser, | |
View, | |
) | |
class LLMSummarizingCondenser(RollingCondenser): | |
"""A condenser that summarizes forgotten events. | |
Maintains a condensed history and forgets old events when it grows too large, | |
keeping a special summarization event after the prefix that summarizes all previous summarizations | |
and newly forgotten events. | |
""" | |
def __init__( | |
self, | |
llm: LLM, | |
max_size: int = 100, | |
keep_first: int = 1, | |
max_event_length: int = 10_000, | |
): | |
if keep_first >= max_size // 2: | |
raise ValueError( | |
f'keep_first ({keep_first}) must be less than half of max_size ({max_size})' | |
) | |
if keep_first < 0: | |
raise ValueError(f'keep_first ({keep_first}) cannot be negative') | |
if max_size < 1: | |
raise ValueError(f'max_size ({max_size}) cannot be non-positive') | |
self.max_size = max_size | |
self.keep_first = keep_first | |
self.max_event_length = max_event_length | |
self.llm = llm | |
super().__init__() | |
def _truncate(self, content: str) -> str: | |
"""Truncate the content to fit within the specified maximum event length.""" | |
return truncate_content(content, max_chars=self.max_event_length) | |
def get_condensation(self, view: View) -> Condensation: | |
head = view[: self.keep_first] | |
target_size = self.max_size // 2 | |
# Number of events to keep from the tail -- target size, minus however many | |
# prefix events from the head, minus one for the summarization event | |
events_from_tail = target_size - len(head) - 1 | |
summary_event = ( | |
view[self.keep_first] | |
if isinstance(view[self.keep_first], AgentCondensationObservation) | |
else AgentCondensationObservation('No events summarized') | |
) | |
# Identify events to be forgotten (those not in head or tail) | |
forgotten_events = [] | |
for event in view[self.keep_first : -events_from_tail]: | |
if not isinstance(event, AgentCondensationObservation): | |
forgotten_events.append(event) | |
# Construct prompt for summarization | |
prompt = """You are maintaining a context-aware state summary for an interactive agent. You will be given a list of events corresponding to actions taken by the agent, and the most recent previous summary if one exists. Track: | |
USER_CONTEXT: (Preserve essential user requirements, goals, and clarifications in concise form) | |
COMPLETED: (Tasks completed so far, with brief results) | |
PENDING: (Tasks that still need to be done) | |
CURRENT_STATE: (Current variables, data structures, or relevant state) | |
For code-specific tasks, also include: | |
CODE_STATE: {File paths, function signatures, data structures} | |
TESTS: {Failing cases, error messages, outputs} | |
CHANGES: {Code edits, variable updates} | |
DEPS: {Dependencies, imports, external calls} | |
VERSION_CONTROL_STATUS: {Repository state, current branch, PR status, commit history} | |
PRIORITIZE: | |
1. Adapt tracking format to match the actual task type | |
2. Capture key user requirements and goals | |
3. Distinguish between completed and pending tasks | |
4. Keep all sections concise and relevant | |
SKIP: Tracking irrelevant details for the current task type | |
Example formats: | |
For code tasks: | |
USER_CONTEXT: Fix FITS card float representation issue | |
COMPLETED: Modified mod_float() in card.py, all tests passing | |
PENDING: Create PR, update documentation | |
CODE_STATE: mod_float() in card.py updated | |
TESTS: test_format() passed | |
CHANGES: str(val) replaces f"{val:.16G}" | |
DEPS: None modified | |
VERSION_CONTROL_STATUS: Branch: fix-float-precision, Latest commit: a1b2c3d | |
For other tasks: | |
USER_CONTEXT: Write 20 haikus based on coin flip results | |
COMPLETED: 15 haikus written for results [T,H,T,H,T,H,T,T,H,T,H,T,H,T,H] | |
PENDING: 5 more haikus needed | |
CURRENT_STATE: Last flip: Heads, Haiku count: 15/20""" | |
prompt += '\n\n' | |
# Add the previous summary if it exists. We'll always have a summary | |
# event, but the types aren't precise enought to guarantee that it has a | |
# message attribute. | |
summary_event_content = self._truncate( | |
summary_event.message if summary_event.message else '' | |
) | |
prompt += f'<PREVIOUS SUMMARY>\n{summary_event_content}\n</PREVIOUS SUMMARY>\n' | |
prompt += '\n\n' | |
# Add all events that are being forgotten. We use the string | |
# representation defined by the event, and truncate it if necessary. | |
for forgotten_event in forgotten_events: | |
event_content = self._truncate(str(forgotten_event)) | |
prompt += f'<EVENT id={forgotten_event.id}>\n{event_content}\n</EVENT>\n' | |
prompt += 'Now summarize the events using the rules above.' | |
messages = [Message(role='user', content=[TextContent(text=prompt)])] | |
response = self.llm.completion( | |
messages=self.llm.format_messages_for_llm(messages), | |
extra_body={'metadata': self._llm_metadata}, | |
) | |
summary = response.choices[0].message.content | |
self.add_metadata('response', response.model_dump()) | |
self.add_metadata('metrics', self.llm.metrics.get()) | |
return Condensation( | |
action=CondensationAction( | |
forgotten_events_start_id=min(event.id for event in forgotten_events), | |
forgotten_events_end_id=max(event.id for event in forgotten_events), | |
summary=summary, | |
summary_offset=self.keep_first, | |
) | |
) | |
def should_condense(self, view: View) -> bool: | |
return len(view) > self.max_size | |
def from_config( | |
cls, config: LLMSummarizingCondenserConfig | |
) -> LLMSummarizingCondenser: | |
# This condenser cannot take advantage of prompt caching. If it happens | |
# to be set, we'll pay for the cache writes but never get a chance to | |
# save on a read. | |
llm_config = config.llm_config.model_copy() | |
llm_config.caching_prompt = False | |
return LLMSummarizingCondenser( | |
llm=LLM(config=llm_config), | |
max_size=config.max_size, | |
keep_first=config.keep_first, | |
max_event_length=config.max_event_length, | |
) | |
LLMSummarizingCondenser.register_config(LLMSummarizingCondenserConfig) | |