import os from pathlib import Path from typing import Self import yaml from pydantic import BaseModel, ConfigDict, Field from src.config.model_config import ModelConfig from src.config.sandbox_config import SandboxConfig from src.config.tool_config import ToolConfig, ToolGroupConfig class AppConfig(BaseModel): """Config for the DeerFlow application""" models: list[ModelConfig] = Field(default_factory=list, description="Available models") sandbox: SandboxConfig = Field(description="Sandbox configuration") tools: list[ToolConfig] = Field(default_factory=list, description="Available tools") tool_groups: list[ToolGroupConfig] = Field(default_factory=list, description="Available tool groups") model_config = ConfigDict(extra="allow", frozen=False) @classmethod def resolve_config_path(cls, config_path: str | None = None) -> Path: """Resolve the config file path. Priority: 1. If provided `config_path` argument, use it. 2. If provided `DEER_FLOW_CONFIG_PATH` environment variable, use it. 3. Otherwise, first check the `config.yaml` in the current directory, then fallback to `config.yaml` in the parent directory. """ if config_path: path = Path(config_path) if not Path.exists(path): raise FileNotFoundError(f"Config file specified by param `config_path` not found at {path}") return path elif os.getenv("DEER_FLOW_CONFIG_PATH"): path = Path(os.getenv("DEER_FLOW_CONFIG_PATH")) if not Path.exists(path): raise FileNotFoundError(f"Config file specified by environment variable `DEER_FLOW_CONFIG_PATH` not found at {path}") return path else: # Check if the config.yaml is in the current directory path = Path(os.getcwd()) / "config.yaml" if not path.exists(): # Check if the config.yaml is in the parent directory of CWD path = Path(os.getcwd()).parent / "config.yaml" if not path.exists(): raise FileNotFoundError("`config.yaml` file not found at the current directory nor its parent directory") return path @classmethod def from_file(cls, config_path: str | None = None) -> Self: """Load config from YAML file. See `resolve_config_path` for more details. Args: config_path: Path to the config file. Returns: AppConfig: The loaded config. """ resolved_path = cls.resolve_config_path(config_path) with open(resolved_path) as f: config_data = yaml.safe_load(f) cls.resolve_env_variables(config_data) result = cls.model_validate(config_data) return result @classmethod def resolve_env_variables(cls, config: dict) -> dict: """Recursively resolve environment variables in the config. Environment variables are resolved using the `os.getenv` function. Example: $OPENAI_API_KEY Args: config: The config to resolve environment variables in. Returns: The config with environment variables resolved. """ for key, value in config.items(): if isinstance(value, str): if value.startswith("$"): env_value = os.getenv(value[1:], None) if env_value is not None: config[key] = env_value else: config[key] = value elif isinstance(value, dict): config[key] = cls.resolve_env_variables(value) elif isinstance(value, list): config[key] = [cls.resolve_env_variables(item) for item in value] return config def get_model_config(self, name: str) -> ModelConfig | None: """Get the model config by name. Args: name: The name of the model to get the config for. Returns: The model config if found, otherwise None. """ return next((model for model in self.models if model.name == name), None) def get_tool_config(self, name: str) -> ToolConfig | None: """Get the tool config by name. Args: name: The name of the tool to get the config for. Returns: The tool config if found, otherwise None. """ return next((tool for tool in self.tools if tool.name == name), None) def get_tool_group_config(self, name: str) -> ToolGroupConfig | None: """Get the tool group config by name. Args: name: The name of the tool group to get the config for. Returns: The tool group config if found, otherwise None. """ return next((group for group in self.tool_groups if group.name == name), None) _app_config: AppConfig | None = None def get_app_config() -> AppConfig: """Get the DeerFlow config instance.""" global _app_config if _app_config is None: _app_config = AppConfig.from_file() return _app_config