mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-26 05:48:36 +08:00
Refactor agent_config loading from toml (#6967)
This commit is contained in:
parent
544e756f5f
commit
4b7cca9bdf
@ -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
|
||||
|
||||
@ -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}'
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user