2026-01-14 07:15:58 +08:00
|
|
|
import os
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
from typing import Self
|
|
|
|
|
|
|
|
|
|
import yaml
|
2026-01-14 23:29:18 +08:00
|
|
|
from dotenv import load_dotenv
|
2026-01-14 07:15:58 +08:00
|
|
|
from pydantic import BaseModel, ConfigDict, Field
|
|
|
|
|
|
|
|
|
|
from src.config.model_config import ModelConfig
|
|
|
|
|
from src.config.sandbox_config import SandboxConfig
|
2026-01-16 14:44:51 +08:00
|
|
|
from src.config.skills_config import SkillsConfig
|
2026-01-19 16:17:31 +08:00
|
|
|
from src.config.summarization_config import load_summarization_config_from_dict
|
2026-01-14 23:29:18 +08:00
|
|
|
from src.config.title_config import load_title_config_from_dict
|
2026-01-14 07:15:58 +08:00
|
|
|
from src.config.tool_config import ToolConfig, ToolGroupConfig
|
|
|
|
|
|
2026-01-14 23:29:18 +08:00
|
|
|
load_dotenv()
|
|
|
|
|
|
2026-01-14 07:15:58 +08:00
|
|
|
|
|
|
|
|
class AppConfig(BaseModel):
|
|
|
|
|
"""Config for the DeerFlow application"""
|
|
|
|
|
|
2026-01-14 09:08:20 +08:00
|
|
|
models: list[ModelConfig] = Field(default_factory=list, description="Available models")
|
2026-01-14 07:15:58 +08:00
|
|
|
sandbox: SandboxConfig = Field(description="Sandbox configuration")
|
|
|
|
|
tools: list[ToolConfig] = Field(default_factory=list, description="Available tools")
|
2026-01-14 09:08:20 +08:00
|
|
|
tool_groups: list[ToolGroupConfig] = Field(default_factory=list, description="Available tool groups")
|
2026-01-16 14:44:51 +08:00
|
|
|
skills: SkillsConfig = Field(default_factory=SkillsConfig, description="Skills configuration")
|
2026-01-14 07:15:58 +08:00
|
|
|
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.
|
2026-01-14 12:32:34 +08:00
|
|
|
3. Otherwise, first check the `config.yaml` in the current directory, then fallback to `config.yaml` in the parent directory.
|
2026-01-14 07:15:58 +08:00
|
|
|
"""
|
|
|
|
|
if config_path:
|
|
|
|
|
path = Path(config_path)
|
|
|
|
|
if not Path.exists(path):
|
2026-01-14 09:08:20 +08:00
|
|
|
raise FileNotFoundError(f"Config file specified by param `config_path` not found at {path}")
|
2026-01-14 07:15:58 +08:00
|
|
|
return path
|
|
|
|
|
elif os.getenv("DEER_FLOW_CONFIG_PATH"):
|
|
|
|
|
path = Path(os.getenv("DEER_FLOW_CONFIG_PATH"))
|
|
|
|
|
if not Path.exists(path):
|
2026-01-14 09:08:20 +08:00
|
|
|
raise FileNotFoundError(f"Config file specified by environment variable `DEER_FLOW_CONFIG_PATH` not found at {path}")
|
2026-01-14 07:15:58 +08:00
|
|
|
return path
|
|
|
|
|
else:
|
2026-01-14 12:32:34 +08:00
|
|
|
# Check if the config.yaml is in the current directory
|
|
|
|
|
path = Path(os.getcwd()) / "config.yaml"
|
2026-01-14 07:15:58 +08:00
|
|
|
if not path.exists():
|
2026-01-14 12:32:34 +08:00
|
|
|
# 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")
|
2026-01-14 07:15:58 +08:00
|
|
|
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)
|
2026-01-14 09:08:20 +08:00
|
|
|
with open(resolved_path) as f:
|
2026-01-14 07:15:58 +08:00
|
|
|
config_data = yaml.safe_load(f)
|
|
|
|
|
cls.resolve_env_variables(config_data)
|
2026-01-14 23:29:18 +08:00
|
|
|
|
|
|
|
|
# Load title config if present
|
|
|
|
|
if "title" in config_data:
|
|
|
|
|
load_title_config_from_dict(config_data["title"])
|
|
|
|
|
|
2026-01-19 16:17:31 +08:00
|
|
|
# Load summarization config if present
|
|
|
|
|
if "summarization" in config_data:
|
|
|
|
|
load_summarization_config_from_dict(config_data["summarization"])
|
|
|
|
|
|
2026-01-14 07:15:58 +08:00
|
|
|
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:
|
2026-01-17 23:23:12 +08:00
|
|
|
"""Get the DeerFlow config instance.
|
|
|
|
|
|
|
|
|
|
Returns a cached singleton instance. Use `reload_app_config()` to reload
|
|
|
|
|
from file, or `reset_app_config()` to clear the cache.
|
|
|
|
|
"""
|
2026-01-14 07:15:58 +08:00
|
|
|
global _app_config
|
|
|
|
|
if _app_config is None:
|
|
|
|
|
_app_config = AppConfig.from_file()
|
|
|
|
|
return _app_config
|
2026-01-17 23:23:12 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def reload_app_config(config_path: str | None = None) -> AppConfig:
|
|
|
|
|
"""Reload the config from file and update the cached instance.
|
|
|
|
|
|
|
|
|
|
This is useful when the config file has been modified and you want
|
|
|
|
|
to pick up the changes without restarting the application.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
config_path: Optional path to config file. If not provided,
|
|
|
|
|
uses the default resolution strategy.
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
The newly loaded AppConfig instance.
|
|
|
|
|
"""
|
|
|
|
|
global _app_config
|
|
|
|
|
_app_config = AppConfig.from_file(config_path)
|
|
|
|
|
return _app_config
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def reset_app_config() -> None:
|
|
|
|
|
"""Reset the cached config instance.
|
|
|
|
|
|
|
|
|
|
This clears the singleton cache, causing the next call to
|
|
|
|
|
`get_app_config()` to reload from file. Useful for testing
|
|
|
|
|
or when switching between different configurations.
|
|
|
|
|
"""
|
|
|
|
|
global _app_config
|
|
|
|
|
_app_config = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def set_app_config(config: AppConfig) -> None:
|
|
|
|
|
"""Set a custom config instance.
|
|
|
|
|
|
|
|
|
|
This allows injecting a custom or mock config for testing purposes.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
config: The AppConfig instance to use.
|
|
|
|
|
"""
|
|
|
|
|
global _app_config
|
|
|
|
|
_app_config = config
|