Refactor agent_config loading from toml (#6967)

This commit is contained in:
Engel Nyst 2025-02-26 23:06:10 +01:00 committed by GitHub
parent 544e756f5f
commit 4b7cca9bdf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 148 additions and 34 deletions

View File

@ -1,6 +1,9 @@
from pydantic import BaseModel, Field
from __future__ import annotations
from pydantic import BaseModel, Field, ValidationError
from openhands.core.config.condenser_config import CondenserConfig, NoOpCondenserConfig
from openhands.core.logger import openhands_logger as logger
class AgentConfig(BaseModel):
@ -11,25 +14,88 @@ class AgentConfig(BaseModel):
codeact_enable_browsing: Whether browsing delegate is enabled in the action space. Default is False. Only works with function calling.
codeact_enable_llm_editor: Whether LLM editor is enabled in the action space. Default is False. Only works with function calling.
codeact_enable_jupyter: Whether Jupyter is enabled in the action space. Default is False.
micro_agent_name: The name of the micro agent to use for this agent.
memory_enabled: Whether long-term memory (embeddings) is enabled.
memory_max_threads: The maximum number of threads indexing at the same time for embeddings.
memory_max_threads: The maximum number of threads indexing at the same time for embeddings. (deprecated)
llm_config: The name of the llm config to use. If specified, this will override global llm config.
enable_prompt_extensions: Whether to use prompt extensions (e.g., microagents, inject runtime info). Default is True.
disabled_microagents: A list of microagents to disable. Default is None.
disabled_microagents: A list of microagents to disable (by name, without .py extension, e.g. ["github", "lint"]). Default is None.
condenser: Configuration for the memory condenser. Default is NoOpCondenserConfig.
enable_history_truncation: If history should be truncated once LLM context limit is hit.
enable_history_truncation: Whether history should be truncated to continue the session when hitting LLM context length limit.
enable_som_visual_browsing: Whether to enable SoM (Set of Marks) visual browsing. Default is False.
"""
codeact_enable_browsing: bool = Field(default=True)
enable_som_visual_browsing: bool = Field(default=False)
codeact_enable_llm_editor: bool = Field(default=False)
codeact_enable_jupyter: bool = Field(default=True)
micro_agent_name: str | None = Field(default=None)
llm_config: str | None = Field(default=None)
memory_enabled: bool = Field(default=False)
memory_max_threads: int = Field(default=3)
llm_config: str | None = Field(default=None)
codeact_enable_browsing: bool = Field(default=True)
codeact_enable_llm_editor: bool = Field(default=False)
codeact_enable_jupyter: bool = Field(default=True)
enable_prompt_extensions: bool = Field(default=True)
disabled_microagents: list[str] | None = Field(default=None)
condenser: CondenserConfig = Field(default_factory=NoOpCondenserConfig)
disabled_microagents: list[str] = Field(default_factory=list)
enable_history_truncation: bool = Field(default=True)
enable_som_visual_browsing: bool = Field(default=False)
condenser: CondenserConfig = Field(default_factory=NoOpCondenserConfig)
model_config = {'extra': 'forbid'}
@classmethod
def from_toml_section(cls, data: dict) -> dict[str, AgentConfig]:
"""
Create a mapping of AgentConfig instances from a toml dictionary representing the [agent] section.
The default configuration is built from all non-dict keys in data.
Then, each key with a dict value is treated as a custom agent configuration, and its values override
the default configuration.
Example:
Apply generic agent config with custom agent overrides, e.g.
[agent]
memory_enabled = false
enable_prompt_extensions = true
[agent.BrowsingAgent]
memory_enabled = true
results in memory_enabled being true for BrowsingAgent but false for others.
Returns:
dict[str, AgentConfig]: A mapping where the key "agent" corresponds to the default configuration
and additional keys represent custom configurations.
"""
# Initialize the result mapping
agent_mapping: dict[str, AgentConfig] = {}
# Extract base config data (non-dict values)
base_data = {}
custom_sections: dict[str, dict] = {}
for key, value in data.items():
if isinstance(value, dict):
custom_sections[key] = value
else:
base_data[key] = value
# Try to create the base config
try:
base_config = cls.model_validate(base_data)
agent_mapping['agent'] = base_config
except ValidationError as e:
logger.warning(f'Invalid base agent configuration: {e}. Using defaults.')
# If base config fails, create a default one
base_config = cls()
# Still add it to the mapping
agent_mapping['agent'] = base_config
# Process each custom section independently
for name, overrides in custom_sections.items():
try:
# Merge base config with overrides
merged = {**base_config.model_dump(), **overrides}
custom_config = cls.model_validate(merged)
agent_mapping[name] = custom_config
except ValidationError as e:
logger.warning(
f'Invalid agent configuration for [{name}]: {e}. This section will be skipped.'
)
# Skip this custom section but continue with others
continue
return agent_mapping

View File

@ -144,27 +144,9 @@ def load_from_toml(cfg: AppConfig, toml_file: str = 'config.toml') -> None:
# Process agent section if present
if 'agent' in toml_config:
try:
value = toml_config['agent']
# Every entry here is either a field for the default `agent` config group, or itself a group
# The best way to tell the difference is to try to parse it as an AgentConfig object
agent_group_ids: set[str] = set()
for nested_key, nested_value in value.items():
if isinstance(nested_value, dict):
try:
agent_config = AgentConfig(**nested_value)
except ValidationError:
continue
agent_group_ids.add(nested_key)
cfg.set_agent_config(agent_config, nested_key)
logger.openhands_logger.debug(
'Attempt to load default agent config from config toml'
)
value_without_groups = {
k: v for k, v in value.items() if k not in agent_group_ids
}
agent_config = AgentConfig(**value_without_groups)
cfg.set_agent_config(agent_config, 'agent')
agent_mapping = AgentConfig.from_toml_section(toml_config['agent'])
for agent_key, agent_conf in agent_mapping.items():
cfg.set_agent_config(agent_conf, agent_key)
except (TypeError, KeyError, ValidationError) as e:
logger.openhands_logger.warning(
f'Cannot parse [agent] config from toml, values have not been applied.\nError: {e}'

View File

@ -781,3 +781,69 @@ memory_enabled = false
assert agent_config1.memory_enabled
agent_config2 = get_agent_config_arg('group2', temp_toml_file)
assert not agent_config2.memory_enabled
def test_agent_config_from_toml_section():
"""Test that AgentConfig.from_toml_section correctly parses agent configurations from TOML."""
from openhands.core.config.agent_config import AgentConfig
# Test with base config and custom configs
agent_section = {
'memory_enabled': True,
'memory_max_threads': 5,
'enable_prompt_extensions': True,
'CustomAgent1': {'memory_enabled': False, 'codeact_enable_browsing': False},
'CustomAgent2': {'memory_max_threads': 10, 'enable_prompt_extensions': False},
'InvalidAgent': {
'invalid_field': 'some_value' # This should be skipped but not affect others
},
}
# Parse the section
result = AgentConfig.from_toml_section(agent_section)
# Verify the base config was correctly parsed
assert 'agent' in result
assert result['agent'].memory_enabled is True
assert result['agent'].memory_max_threads == 5
assert result['agent'].enable_prompt_extensions is True
# Verify custom configs were correctly parsed and inherit from base
assert 'CustomAgent1' in result
assert result['CustomAgent1'].memory_enabled is False # Overridden
assert result['CustomAgent1'].memory_max_threads == 5 # Inherited
assert result['CustomAgent1'].codeact_enable_browsing is False # Overridden
assert result['CustomAgent1'].enable_prompt_extensions is True # Inherited
assert 'CustomAgent2' in result
assert result['CustomAgent2'].memory_enabled is True # Inherited
assert result['CustomAgent2'].memory_max_threads == 10 # Overridden
assert result['CustomAgent2'].enable_prompt_extensions is False # Overridden
# Verify the invalid config was skipped
assert 'InvalidAgent' not in result
def test_agent_config_from_toml_section_with_invalid_base():
"""Test that AgentConfig.from_toml_section handles invalid base configurations gracefully."""
from openhands.core.config.agent_config import AgentConfig
# Test with invalid base config but valid custom configs
agent_section = {
'invalid_field': 'some_value', # This should be ignored in base config
'memory_max_threads': 'not_an_int', # This should cause validation error
'CustomAgent': {'memory_enabled': True, 'memory_max_threads': 8},
}
# Parse the section
result = AgentConfig.from_toml_section(agent_section)
# Verify a default base config was created despite the invalid fields
assert 'agent' in result
assert result['agent'].memory_enabled is False # Default value
assert result['agent'].memory_max_threads == 3 # Default value
# Verify custom config was still processed correctly
assert 'CustomAgent' in result
assert result['CustomAgent'].memory_enabled is True
assert result['CustomAgent'].memory_max_threads == 8