1- from typing import Literal
1+ from typing import Literal , cast
22
3- from pydantic import BaseModel , Field
3+ from pydantic import BaseModel , Field , ValidationError
44
5+ from openhands .core import logger
56from openhands .core .config .llm_config import LLMConfig
67
78
@@ -10,17 +11,21 @@ class NoOpCondenserConfig(BaseModel):
1011
1112 type : Literal ['noop' ] = Field ('noop' )
1213
14+ model_config = {'extra' : 'forbid' }
15+
1316
1417class ObservationMaskingCondenserConfig (BaseModel ):
1518 """Configuration for ObservationMaskingCondenser."""
1619
1720 type : Literal ['observation_masking' ] = Field ('observation_masking' )
1821 attention_window : int = Field (
19- default = 10 ,
22+ default = 100 ,
2023 description = 'The number of most-recent events where observations will not be masked.' ,
2124 ge = 1 ,
2225 )
2326
27+ model_config = {'extra' : 'forbid' }
28+
2429
2530class RecentEventsCondenserConfig (BaseModel ):
2631 """Configuration for RecentEventsCondenser."""
@@ -34,9 +39,11 @@ class RecentEventsCondenserConfig(BaseModel):
3439 ge = 0 ,
3540 )
3641 max_events : int = Field (
37- default = 10 , description = 'Maximum number of events to keep.' , ge = 1
42+ default = 100 , description = 'Maximum number of events to keep.' , ge = 1
3843 )
3944
45+ model_config = {'extra' : 'forbid' }
46+
4047
4148class LLMSummarizingCondenserConfig (BaseModel ):
4249 """Configuration for LLMCondenser."""
@@ -49,13 +56,17 @@ class LLMSummarizingCondenserConfig(BaseModel):
4956 # at least one event by default, because the best guess is that it's the user task
5057 keep_first : int = Field (
5158 default = 1 ,
52- description = 'The number of initial events to condense .' ,
59+ description = 'Number of initial events to always keep in history .' ,
5360 ge = 0 ,
5461 )
5562 max_size : int = Field (
56- default = 10 , description = 'Maximum number of events to keep.' , ge = 1
63+ default = 100 ,
64+ description = 'Maximum size of the condensed history before triggering forgetting.' ,
65+ ge = 2 ,
5766 )
5867
68+ model_config = {'extra' : 'forbid' }
69+
5970
6071class AmortizedForgettingCondenserConfig (BaseModel ):
6172 """Configuration for AmortizedForgettingCondenser."""
@@ -74,6 +85,8 @@ class AmortizedForgettingCondenserConfig(BaseModel):
7485 ge = 0 ,
7586 )
7687
88+ model_config = {'extra' : 'forbid' }
89+
7790
7891class LLMAttentionCondenserConfig (BaseModel ):
7992 """Configuration for LLMAttentionCondenser."""
@@ -95,7 +108,10 @@ class LLMAttentionCondenserConfig(BaseModel):
95108 ge = 0 ,
96109 )
97110
111+ model_config = {'extra' : 'forbid' }
98112
113+
114+ # Type alias for convenience
99115CondenserConfig = (
100116 NoOpCondenserConfig
101117 | ObservationMaskingCondenserConfig
@@ -104,3 +120,121 @@ class LLMAttentionCondenserConfig(BaseModel):
104120 | AmortizedForgettingCondenserConfig
105121 | LLMAttentionCondenserConfig
106122)
123+
124+
125+ def condenser_config_from_toml_section (
126+ data : dict , llm_configs : dict | None = None
127+ ) -> dict [str , CondenserConfig ]:
128+ """
129+ Create a CondenserConfig instance from a toml dictionary representing the [condenser] section.
130+
131+ For CondenserConfig, the handling is different since it's a union type. The type of condenser
132+ is determined by the 'type' field in the section.
133+
134+ Example:
135+ Parse condenser config like:
136+ [condenser]
137+ type = "noop"
138+
139+ For condensers that require an LLM config, you can specify the name of an LLM config:
140+ [condenser]
141+ type = "llm"
142+ llm_config = "my_llm" # References [llm.my_llm] section
143+
144+ Args:
145+ data: The TOML dictionary representing the [condenser] section.
146+ llm_configs: Optional dictionary of LLMConfig objects keyed by name.
147+
148+ Returns:
149+ dict[str, CondenserConfig]: A mapping where the key "condenser" corresponds to the configuration.
150+ """
151+
152+ # Initialize the result mapping
153+ condenser_mapping : dict [str , CondenserConfig ] = {}
154+
155+ # Process config
156+ try :
157+ # Determine which condenser type to use based on 'type' field
158+ condenser_type = data .get ('type' , 'noop' )
159+
160+ # Handle LLM config reference if needed
161+ if (
162+ condenser_type in ('llm' , 'llm_attention' )
163+ and 'llm_config' in data
164+ and isinstance (data ['llm_config' ], str )
165+ ):
166+ llm_config_name = data ['llm_config' ]
167+ if llm_configs and llm_config_name in llm_configs :
168+ # Replace the string reference with the actual LLMConfig object
169+ data_copy = data .copy ()
170+ data_copy ['llm_config' ] = llm_configs [llm_config_name ]
171+ config = create_condenser_config (condenser_type , data_copy )
172+ else :
173+ logger .openhands_logger .warning (
174+ f"LLM config '{ llm_config_name } ' not found for condenser. Using default LLMConfig."
175+ )
176+ # Create a default LLMConfig if the referenced one doesn't exist
177+ data_copy = data .copy ()
178+ # Try to use the fallback 'llm' config
179+ if llm_configs is not None :
180+ data_copy ['llm_config' ] = llm_configs .get ('llm' )
181+ config = create_condenser_config (condenser_type , data_copy )
182+ else :
183+ config = create_condenser_config (condenser_type , data )
184+
185+ condenser_mapping ['condenser' ] = config
186+ except (ValidationError , ValueError ) as e :
187+ logger .openhands_logger .warning (
188+ f'Invalid condenser configuration: { e } . Using NoOpCondenserConfig.'
189+ )
190+ # Default to NoOpCondenserConfig if config fails
191+ config = NoOpCondenserConfig ()
192+ condenser_mapping ['condenser' ] = config
193+
194+ return condenser_mapping
195+
196+
197+ # For backward compatibility
198+ from_toml_section = condenser_config_from_toml_section
199+
200+
201+ def create_condenser_config (condenser_type : str , data : dict ) -> CondenserConfig :
202+ """
203+ Create a CondenserConfig instance based on the specified type.
204+
205+ Args:
206+ condenser_type: The type of condenser to create.
207+ data: The configuration data.
208+
209+ Returns:
210+ A CondenserConfig instance.
211+
212+ Raises:
213+ ValueError: If the condenser type is unknown.
214+ ValidationError: If the provided data fails validation for the condenser type.
215+ """
216+ # Mapping of condenser types to their config classes
217+ condenser_classes = {
218+ 'noop' : NoOpCondenserConfig ,
219+ 'observation_masking' : ObservationMaskingCondenserConfig ,
220+ 'recent' : RecentEventsCondenserConfig ,
221+ 'llm' : LLMSummarizingCondenserConfig ,
222+ 'amortized' : AmortizedForgettingCondenserConfig ,
223+ 'llm_attention' : LLMAttentionCondenserConfig ,
224+ }
225+
226+ if condenser_type not in condenser_classes :
227+ raise ValueError (f'Unknown condenser type: { condenser_type } ' )
228+
229+ # Create and validate the config using direct instantiation
230+ # Explicitly handle ValidationError to provide more context
231+ try :
232+ config_class = condenser_classes [condenser_type ]
233+ # Use type casting to help mypy understand the return type
234+ return cast (CondenserConfig , config_class (** data ))
235+ except ValidationError as e :
236+ # Just re-raise with a more descriptive message, but don't try to pass the errors
237+ # which can cause compatibility issues with different pydantic versions
238+ raise ValueError (
239+ f"Validation failed for condenser type '{ condenser_type } ': { e } "
240+ )
0 commit comments