Skip to content

Configuration Loading and Management API

This section documents the HoloDeck configuration system, including YAML loading, validation, environment variable substitution, and default configuration management.

Overview

The configuration system is built on three pillars:

  1. Loading: Parse YAML agent configuration files
  2. Validation: Validate against Pydantic models with detailed error messages
  3. Merging: Combine default settings, user config, and environment overrides

ConfigLoader

The main entry point for loading HoloDeck agent configurations.

ConfigLoader()

Loads and validates agent configuration from YAML files.

This class handles: - Parsing YAML files into Python dictionaries - Loading global configuration from ~/.holodeck/config.yaml - Merging configurations with proper precedence - Resolving file references (instructions, tools) - Converting validation errors into human-readable messages - Environment variable substitution

Initialize the ConfigLoader.

Source code in src/holodeck/config/loader.py
151
152
153
def __init__(self) -> None:
    """Initialize the ConfigLoader."""
    pass

Environment Variable Utilities

Support for dynamic configuration using environment variables with ${VAR_NAME} pattern.

substitute_env_vars(text)

Substitute environment variables in text using ${VAR_NAME} pattern.

Replaces all occurrences of ${VAR_NAME} with the corresponding environment variable value. Raises ConfigError if a referenced variable does not exist.

Parameters:

Name Type Description Default
text str

Text potentially containing ${VAR_NAME} patterns

required

Returns:

Type Description
str

Text with all environment variables substituted

Raises:

Type Description
ConfigError

If a referenced environment variable does not exist

Example

import os os.environ["API_KEY"] = "secret123" substitute_env_vars("key: ${API_KEY}") 'key: secret123'

Source code in src/holodeck/config/env_loader.py
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
def substitute_env_vars(text: str) -> str:
    """Substitute environment variables in text using ${VAR_NAME} pattern.

    Replaces all occurrences of ${VAR_NAME} with the corresponding environment
    variable value. Raises ConfigError if a referenced variable does not exist.

    Args:
        text: Text potentially containing ${VAR_NAME} patterns

    Returns:
        Text with all environment variables substituted

    Raises:
        ConfigError: If a referenced environment variable does not exist

    Example:
        >>> import os
        >>> os.environ["API_KEY"] = "secret123"
        >>> substitute_env_vars("key: ${API_KEY}")
        'key: secret123'
    """
    # Pattern to match ${VAR_NAME} - captures alphanumeric, underscore
    pattern = r"\$\{([A-Za-z_][A-Za-z0-9_]*)\}"

    def replace_var(match: re.Match[str]) -> str:
        """Replace a single ${VAR_NAME} pattern with env value.

        Args:
            match: Regex match object for ${VAR_NAME}

        Returns:
            Environment variable value

        Raises:
            ConfigError: If variable does not exist
        """
        var_name = match.group(1)
        if var_name not in os.environ:
            raise ConfigError(
                var_name,
                f"Environment variable '{var_name}' not found. "
                f"Please set it and try again.",
            )
        return os.environ[var_name]

    return re.sub(pattern, replace_var, text)

get_env_var(key, default=None)

Get environment variable with optional default.

Parameters:

Name Type Description Default
key str

Environment variable name

required
default Any

Default value if variable not set

None

Returns:

Type Description
Any

Environment variable value or default

Source code in src/holodeck/config/env_loader.py
58
59
60
61
62
63
64
65
66
67
68
def get_env_var(key: str, default: Any = None) -> Any:
    """Get environment variable with optional default.

    Args:
        key: Environment variable name
        default: Default value if variable not set

    Returns:
        Environment variable value or default
    """
    return os.environ.get(key, default)

load_env_file(path)

Load environment variables from a .env file.

Parameters:

Name Type Description Default
path str

Path to .env file

required

Returns:

Type Description
dict[str, str]

Dictionary of loaded environment variables

Raises:

Type Description
ConfigError

If file cannot be read

Source code in src/holodeck/config/env_loader.py
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
def load_env_file(path: str) -> dict[str, str]:
    """Load environment variables from a .env file.

    Args:
        path: Path to .env file

    Returns:
        Dictionary of loaded environment variables

    Raises:
        ConfigError: If file cannot be read
    """
    try:
        env_vars = {}
        with open(path) as f:
            for line in f:
                line = line.strip()
                if not line or line.startswith("#"):
                    continue
                if "=" in line:
                    key, value = line.split("=", 1)
                    env_vars[key.strip()] = value.strip()
        return env_vars
    except OSError as e:
        raise ConfigError("env_file", f"Cannot read environment file: {e}") from e

Configuration Validation

Schema validation and error normalization utilities for configuration validation.

normalize_errors(errors)

Convert raw error messages to human-readable format.

Processes error messages to be more user-friendly and actionable, removing technical jargon where possible.

Parameters:

Name Type Description Default
errors list[str]

List of error message strings

required

Returns:

Type Description
list[str]

List of normalized, human-readable error messages

Source code in src/holodeck/config/validator.py
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
def normalize_errors(errors: list[str]) -> list[str]:
    """Convert raw error messages to human-readable format.

    Processes error messages to be more user-friendly and actionable,
    removing technical jargon where possible.

    Args:
        errors: List of error message strings

    Returns:
        List of normalized, human-readable error messages
    """
    normalized: list[str] = []

    for error in errors:
        # Remove common technical prefixes
        msg = error
        if msg.startswith("value_error"):
            msg = msg.replace("value_error", "").strip()
        if msg.startswith("type_error"):
            msg = msg.replace("type_error", "").strip()

        # Improve message readability
        if msg:
            normalized.append(msg)

    return normalized if normalized else ["An unknown validation error occurred"]

flatten_pydantic_errors(exc)

Flatten Pydantic ValidationError into human-readable messages.

Converts Pydantic's nested error structure into a flat list of user-friendly error messages that include field names and descriptions.

Parameters:

Name Type Description Default
exc ValidationError

Pydantic ValidationError exception

required

Returns:

Type Description
list[str]

List of human-readable error messages, one per field error

Example

from pydantic import BaseModel, ValidationError class Model(BaseModel): ... name: str try: ... Model(name=123) ... except ValidationError as e: ... msgs = flatten_pydantic_errors(e) ... # msgs contains human-readable descriptions

Source code in src/holodeck/config/validator.py
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
def flatten_pydantic_errors(exc: PydanticValidationError) -> list[str]:
    """Flatten Pydantic ValidationError into human-readable messages.

    Converts Pydantic's nested error structure into a flat list of
    user-friendly error messages that include field names and descriptions.

    Args:
        exc: Pydantic ValidationError exception

    Returns:
        List of human-readable error messages, one per field error

    Example:
        >>> from pydantic import BaseModel, ValidationError
        >>> class Model(BaseModel):
        ...     name: str
        >>> try:
        ...     Model(name=123)
        ... except ValidationError as e:
        ...     msgs = flatten_pydantic_errors(e)
        ...     # msgs contains human-readable descriptions
    """
    errors: list[str] = []

    for error in exc.errors():
        # Extract location (field path)
        loc = error.get("loc", ())
        field_path = ".".join(str(item) for item in loc) if loc else "unknown"

        # Extract error message
        msg = error.get("msg", "Unknown error")
        error_type = error.get("type", "")

        # Format the error message
        if error_type == "value_error":
            # For value errors, include what was provided
            input_val = error.get("input")
            formatted = f"Field '{field_path}': {msg} (received: {input_val!r})"
        else:
            formatted = f"Field '{field_path}': {msg}"

        errors.append(formatted)

    return errors if errors else ["Validation failed with unknown error"]

validate_field_exists(data, field, field_type)

Validate that a required field exists and has correct type.

Parameters:

Name Type Description Default
data dict[str, Any]

Dictionary to validate

required
field str

Field name to check

required
field_type type

Expected type for the field

required

Raises:

Type Description
ValueError

If field is missing or has wrong type

Source code in src/holodeck/config/validator.py
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
def validate_field_exists(data: dict[str, Any], field: str, field_type: type) -> None:
    """Validate that a required field exists and has correct type.

    Args:
        data: Dictionary to validate
        field: Field name to check
        field_type: Expected type for the field

    Raises:
        ValueError: If field is missing or has wrong type
    """
    if field not in data:
        raise ValueError(f"Required field '{field}' is missing")
    if not isinstance(data[field], field_type):
        raise ValueError(
            f"Field '{field}' must be {field_type.__name__}, "
            f"got {type(data[field]).__name__}"
        )

validate_mutually_exclusive(data, fields)

Validate that exactly one of the given fields is present.

Parameters:

Name Type Description Default
data dict[str, Any]

Dictionary to validate

required
fields list[str]

List of mutually exclusive field names

required

Raises:

Type Description
ValueError

If not exactly one field is present

Source code in src/holodeck/config/validator.py
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
def validate_mutually_exclusive(data: dict[str, Any], fields: list[str]) -> None:
    """Validate that exactly one of the given fields is present.

    Args:
        data: Dictionary to validate
        fields: List of mutually exclusive field names

    Raises:
        ValueError: If not exactly one field is present
    """
    present = [f for f in fields if f in data and data[f] is not None]
    if len(present) == 0:
        raise ValueError(f"Exactly one of {fields} must be provided")
    if len(present) > 1:
        raise ValueError(f"Only one of {fields} can be provided, got {present}")

validate_range(value, min_val, max_val, name='value')

Validate that a numeric value is within a range.

Parameters:

Name Type Description Default
value float

Value to validate

required
min_val float

Minimum allowed value (inclusive)

required
max_val float

Maximum allowed value (inclusive)

required
name str

Name of the field for error messages

'value'

Raises:

Type Description
ValueError

If value is outside the range

Source code in src/holodeck/config/validator.py
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
def validate_range(
    value: float, min_val: float, max_val: float, name: str = "value"
) -> None:
    """Validate that a numeric value is within a range.

    Args:
        value: Value to validate
        min_val: Minimum allowed value (inclusive)
        max_val: Maximum allowed value (inclusive)
        name: Name of the field for error messages

    Raises:
        ValueError: If value is outside the range
    """
    if not (min_val <= value <= max_val):
        raise ValueError(f"{name} must be between {min_val} and {max_val}, got {value}")

validate_enum(value, allowed, name='value')

Validate that a string is one of allowed values.

Parameters:

Name Type Description Default
value str

Value to validate

required
allowed list[str]

List of allowed values

required
name str

Name of the field for error messages

'value'

Raises:

Type Description
ValueError

If value is not in allowed list

Source code in src/holodeck/config/validator.py
138
139
140
141
142
143
144
145
146
147
148
149
150
def validate_enum(value: str, allowed: list[str], name: str = "value") -> None:
    """Validate that a string is one of allowed values.

    Args:
        value: Value to validate
        allowed: List of allowed values
        name: Name of the field for error messages

    Raises:
        ValueError: If value is not in allowed list
    """
    if value not in allowed:
        raise ValueError(f"{name} must be one of {allowed}, got '{value}'")

validate_path_exists(path, description='file')

Validate that a file or directory exists.

Parameters:

Name Type Description Default
path str

Path to validate

required
description str

Description of path for error messages

'file'

Raises:

Type Description
ValueError

If path does not exist

Source code in src/holodeck/config/validator.py
153
154
155
156
157
158
159
160
161
162
163
164
165
166
def validate_path_exists(path: str, description: str = "file") -> None:
    """Validate that a file or directory exists.

    Args:
        path: Path to validate
        description: Description of path for error messages

    Raises:
        ValueError: If path does not exist
    """
    from pathlib import Path

    if not Path(path).exists():
        raise ValueError(f"Path does not exist: {path}")

Default Configuration

Utilities for generating default configuration templates for common components.

get_default_model_config(provider='openai')

Get default model configuration for a provider.

Parameters:

Name Type Description Default
provider str

LLM provider name (openai, azure_openai, anthropic, ollama)

'openai'

Returns:

Type Description
dict[str, Any]

Dictionary with default model configuration

Source code in src/holodeck/config/defaults.py
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
def get_default_model_config(provider: str = "openai") -> dict[str, Any]:
    """Get default model configuration for a provider.

    Args:
        provider: LLM provider name (openai, azure_openai, anthropic, ollama)

    Returns:
        Dictionary with default model configuration
    """
    defaults = {
        "openai": {
            "provider": "openai",
            "name": "gpt-4o-mini",
            "temperature": 0.7,
            "max_tokens": 2048,
        },
        "azure_openai": {
            "provider": "azure_openai",
            "name": "gpt-4o",
            "temperature": 0.7,
            "max_tokens": 2048,
        },
        "anthropic": {
            "provider": "anthropic",
            "name": "claude-3-haiku-20240307",
            "temperature": 0.7,
            "max_tokens": 2048,
        },
        "ollama": {
            "provider": "ollama",
            "endpoint": "http://localhost:11434",
            "temperature": 0.3,
            "max_tokens": 1000,
            "top_p": None,
            "api_key": None,
        },
    }
    return defaults.get(provider, defaults["openai"])

get_default_tool_config(tool_type=None)

Get default configuration template for a tool type.

Parameters:

Name Type Description Default
tool_type str | None

Tool type (vectorstore, function, mcp, prompt). If None, returns generic.

None

Returns:

Type Description
dict[str, Any]

Dictionary with default tool configuration

Source code in src/holodeck/config/defaults.py
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
def get_default_tool_config(tool_type: str | None = None) -> dict[str, Any]:
    """Get default configuration template for a tool type.

    Args:
        tool_type: Tool type (vectorstore, function, mcp, prompt).
            If None, returns generic.

    Returns:
        Dictionary with default tool configuration
    """
    if tool_type is None:
        return {"type": "function"}

    defaults: dict[str, dict[str, Any]] = {
        "vectorstore": {
            "type": "vectorstore",
            "source": "",
            "embedding_model": "text-embedding-3-small",
        },
        "function": {
            "type": "function",
            "file": "",
            "function": "",
        },
        "mcp": {
            "type": "mcp",
            "server": "",
        },
        "prompt": {
            "type": "prompt",
            "template": "",
            "parameters": {},
        },
    }
    return defaults.get(tool_type, {})

get_default_evaluation_config(metric_name=None)

Get default evaluation configuration.

Parameters:

Name Type Description Default
metric_name str | None

Specific metric name. If None, returns generic structure.

None

Returns:

Type Description
dict[str, Any]

Dictionary with default evaluation configuration

Source code in src/holodeck/config/defaults.py
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
def get_default_evaluation_config(metric_name: str | None = None) -> dict[str, Any]:
    """Get default evaluation configuration.

    Args:
        metric_name: Specific metric name. If None, returns generic structure.

    Returns:
        Dictionary with default evaluation configuration
    """
    # Default per-metric configs
    metric_defaults = {
        "groundedness": {
            "metric": "groundedness",
            "threshold": 4.0,
            "enabled": True,
            "scale": 5,
        },
        "relevance": {
            "metric": "relevance",
            "threshold": 4.0,
            "enabled": True,
            "scale": 5,
        },
        "coherence": {
            "metric": "coherence",
            "threshold": 3.5,
            "enabled": True,
            "scale": 5,
        },
        "safety": {
            "metric": "safety",
            "threshold": 4.0,
            "enabled": True,
            "scale": 5,
        },
        "f1_score": {
            "metric": "f1_score",
            "threshold": 0.85,
            "enabled": True,
        },
        "bleu": {
            "metric": "bleu",
            "threshold": 0.7,
            "enabled": True,
        },
        "rouge": {
            "metric": "rouge",
            "threshold": 0.7,
            "enabled": True,
        },
    }
    if metric_name is None:
        return {
            "metrics": [
                {"metric": "groundedness", "threshold": 4.0},
                {"metric": "relevance", "threshold": 4.0},
            ]
        }
    return metric_defaults.get(metric_name, {})

Configuration Merging

ConfigMerger

Merges configurations with proper precedence and inheritance rules.

merge_agent_with_global(agent_config, global_config) staticmethod

Merge agent configuration with global configuration.

Agent-level settings take precedence. When inherit_global is False, only agent settings are used.

Parameters:

Name Type Description Default
agent_config dict[str, Any]

Agent configuration from agent.yaml

required
global_config GlobalConfig | None

Merged global configuration (user + project level)

required

Returns:

Type Description
dict[str, Any]

Merged configuration dict with agent settings taking precedence

Source code in src/holodeck/config/merge.py
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
@staticmethod
def merge_agent_with_global(
    agent_config: dict[str, Any], global_config: GlobalConfig | None
) -> dict[str, Any]:
    """Merge agent configuration with global configuration.

    Agent-level settings take precedence. When inherit_global is False,
    only agent settings are used.

    Args:
        agent_config: Agent configuration from agent.yaml
        global_config: Merged global configuration (user + project level)

    Returns:
        Merged configuration dict with agent settings taking precedence
    """
    # If inherit_global is explicitly false, return agent config as-is
    if agent_config.get("inherit_global") is False:
        logger.info("inherit_global set to false; using only agent configuration")
        # Remove the inherit_global flag from the config
        merged = dict(agent_config)
        merged.pop("inherit_global", None)
        return merged

    # If no global config or agent has explicit config, use agent config
    if global_config is None:
        merged = dict(agent_config)
        merged.pop("inherit_global", None)
        return merged

    # Merge global config (base) with agent config (override)
    global_dict = global_config.model_dump()
    agent_dict = dict(agent_config)
    agent_dict.pop("inherit_global", None)

    # For agent-level properties (response_format, tools, etc),
    # agent config completely overrides global
    merged_dict = ConfigMerger._deep_merge_dicts(global_dict, agent_dict)

    return merged_dict

merge_global_configs(user_config, project_config) staticmethod

Merge user-level and project-level global configurations.

Project-level config overrides user-level config when both are present.

Parameters:

Name Type Description Default
user_config GlobalConfig | None

Global configuration from ~/.holodeck/config.yml|yaml

required
project_config GlobalConfig | None

Global configuration from project root config.yml|yaml

required

Returns:

Type Description
GlobalConfig | None

Merged GlobalConfig instance, or None if neither config exists

Source code in src/holodeck/config/merge.py
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
@staticmethod
def merge_global_configs(
    user_config: GlobalConfig | None, project_config: GlobalConfig | None
) -> GlobalConfig | None:
    """Merge user-level and project-level global configurations.

    Project-level config overrides user-level config when both are present.

    Args:
        user_config: Global configuration from ~/.holodeck/config.yml|yaml
        project_config: Global configuration from project root config.yml|yaml

    Returns:
        Merged GlobalConfig instance, or None if neither config exists
    """
    if user_config is None and project_config is None:
        return None

    if user_config is None:
        return project_config

    if project_config is None:
        return user_config

    # Merge project config (override) into user config (base)
    user_dict = user_config.model_dump()
    project_dict = project_config.model_dump()

    merged_dict = ConfigMerger._deep_merge_dicts(user_dict, project_dict)
    return GlobalConfig(**merged_dict)