Merge branch 'main' into fix-2788

This commit is contained in:
Willem Jiang
2026-05-27 08:29:21 +08:00
committed by GitHub
282 changed files with 25568 additions and 2124 deletions
@@ -20,6 +20,7 @@ from deerflow.config.memory_config import MemoryConfig, load_memory_config_from_
from deerflow.config.model_config import ModelConfig
from deerflow.config.run_events_config import RunEventsConfig
from deerflow.config.runtime_paths import existing_project_file
from deerflow.config.safety_finish_reason_config import SafetyFinishReasonConfig
from deerflow.config.sandbox_config import SandboxConfig
from deerflow.config.skill_evolution_config import SkillEvolutionConfig
from deerflow.config.skills_config import SkillsConfig
@@ -102,6 +103,7 @@ class AppConfig(BaseModel):
guardrails: GuardrailsConfig = Field(default_factory=GuardrailsConfig, description="Guardrail middleware configuration")
circuit_breaker: CircuitBreakerConfig = Field(default_factory=CircuitBreakerConfig, description="LLM circuit breaker configuration")
loop_detection: LoopDetectionConfig = Field(default_factory=LoopDetectionConfig, description="Loop detection middleware configuration")
safety_finish_reason: SafetyFinishReasonConfig = Field(default_factory=SafetyFinishReasonConfig, description="Provider safety-filter finish_reason interception middleware configuration")
model_config = ConfigDict(extra="allow")
database: DatabaseConfig = Field(default_factory=DatabaseConfig, description="Unified database backend configuration")
run_events: RunEventsConfig = Field(default_factory=RunEventsConfig, description="Run event storage configuration")
@@ -141,7 +141,7 @@ class ExtensionsConfig(BaseModel):
try:
with open(resolved_path, encoding="utf-8") as f:
config_data = json.load(f)
cls.resolve_env_variables(config_data)
config_data = cls.resolve_env_variables(config_data)
return cls.model_validate(config_data)
except json.JSONDecodeError as e:
raise ValueError(f"Extensions config file at {resolved_path} is not valid JSON: {e}") from e
@@ -149,7 +149,7 @@ class ExtensionsConfig(BaseModel):
raise RuntimeError(f"Failed to load extensions config from {resolved_path}: {e}") from e
@classmethod
def resolve_env_variables(cls, config: dict[str, Any]) -> dict[str, Any]:
def resolve_env_variables(cls, config: Any) -> Any:
"""Recursively resolve environment variables in the config.
Environment variables are resolved using the `os.getenv` function. Example: $OPENAI_API_KEY
@@ -160,23 +160,26 @@ class ExtensionsConfig(BaseModel):
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:])
if env_value is None:
# Unresolved placeholder — store empty string so downstream
# consumers (e.g. MCP servers) don't receive the literal "$VAR"
# token as an actual environment value.
config[key] = ""
else:
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) if isinstance(item, dict) else item for item in value]
if isinstance(config, str):
if not config.startswith("$"):
return config
env_value = os.getenv(config[1:])
if env_value is None:
# Unresolved placeholder — store empty string so downstream
# consumers (e.g. MCP servers) don't receive the literal "$VAR"
# token as an actual environment value.
return ""
return env_value
if isinstance(config, dict):
return {key: cls.resolve_env_variables(value) for key, value in config.items()}
if isinstance(config, list):
return [cls.resolve_env_variables(item) for item in config]
if isinstance(config, tuple):
return tuple(cls.resolve_env_variables(item) for item in config)
return config
def get_enabled_mcp_servers(self) -> dict[str, McpServerConfig]:
@@ -0,0 +1,47 @@
"""Configuration for SafetyFinishReasonMiddleware.
Mirrors the shape of GuardrailsConfig: detectors are loaded by class path
through ``deerflow.reflection.resolve_variable`` (same loader the
``guardrails.provider`` config uses) so users can drop in custom provider
detectors without modifying core code.
"""
from __future__ import annotations
from pydantic import BaseModel, Field
class SafetyDetectorConfig(BaseModel):
"""One detector entry under ``safety_finish_reason.detectors``."""
use: str = Field(
description=("Class path of a SafetyTerminationDetector implementation (e.g. 'deerflow.agents.middlewares.safety_termination_detectors:OpenAICompatibleContentFilterDetector')."),
)
config: dict = Field(
default_factory=dict,
description="Constructor kwargs passed to the detector class.",
)
class SafetyFinishReasonConfig(BaseModel):
"""Configuration for the SafetyFinishReasonMiddleware.
The middleware intercepts AIMessages where the provider signaled a
safety-related termination (e.g. OpenAI ``finish_reason='content_filter'``)
while still returning tool calls, and suppresses those tool calls so the
half-truncated arguments never execute.
"""
enabled: bool = Field(
default=True,
description="Master switch for the SafetyFinishReasonMiddleware.",
)
detectors: list[SafetyDetectorConfig] | None = Field(
default=None,
description=(
"Custom detector list. Leave unset (None) to use the built-in "
"set covering OpenAI-compatible content_filter, Anthropic "
"refusal, and Gemini SAFETY/BLOCKLIST/PROHIBITED_CONTENT/SPII/"
"RECITATION. Provide a non-null list to fully override."
),
)
@@ -51,3 +51,16 @@ def load_title_config_from_dict(config_dict: dict) -> None:
"""Load title configuration from a dictionary."""
global _title_config
_title_config = TitleConfig(**config_dict)
def reset_title_config() -> None:
"""Restore the title configuration to its pristine ``TitleConfig()`` default.
Public API so that tests do not have to reach into the private
``_title_config`` module attribute. ``AppConfig.from_file()`` calls
:func:`load_title_config_from_dict`, which permanently mutates the
singleton; tests that need a clean slate between cases should call
this between tests.
"""
global _title_config
_title_config = TitleConfig()
@@ -147,3 +147,15 @@ def validate_enabled_tracing_providers() -> None:
def is_tracing_enabled() -> bool:
"""Check if any tracing provider is enabled and fully configured."""
return get_tracing_config().is_configured
def reset_tracing_config() -> None:
"""Discard the cached :class:`TracingConfig` so the next call rebuilds it.
Public API so that tests do not have to reach into the private
``_tracing_config`` module attribute. A future internal rename would
silently break callers that mutate the attribute directly.
"""
global _tracing_config
with _config_lock:
_tracing_config = None