mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-05-21 15:36:48 +00:00
refactor(config): eliminate global mutable state, wire DeerFlowContext into runtime
- Freeze all config models (AppConfig + 15 sub-configs) with frozen=True - Purify from_file() — remove 9 load_*_from_dict() side-effect calls - Replace mtime/reload/push/pop machinery with single ContextVar + init_app_config() - Delete 10 sub-module globals and their getters/setters/loaders - Migrate 50+ consumers from get_*_config() to get_app_config().xxx - Expand DeerFlowContext: app_config + thread_id + agent_name (frozen dataclass) - Wire into Gateway runtime (worker.py) and DeerFlowClient via context= parameter - Remove sandbox_id from runtime.context — flows through ThreadState.sandbox only - Middleware/tools access runtime.context directly via Runtime[DeerFlowContext] generic - resolve_context() retained at server entry points for LangGraph Server fallback
This commit is contained in:
@@ -18,6 +18,7 @@ from app.gateway.routers.models import ModelResponse, ModelsListResponse
|
||||
from app.gateway.routers.skills import SkillInstallResponse, SkillResponse, SkillsListResponse
|
||||
from app.gateway.routers.uploads import UploadResponse
|
||||
from deerflow.client import DeerFlowClient
|
||||
from deerflow.config.app_config import AppConfig
|
||||
from deerflow.config.paths import Paths
|
||||
from deerflow.uploads.manager import PathTraversalError
|
||||
|
||||
@@ -44,7 +45,7 @@ def mock_app_config():
|
||||
@pytest.fixture
|
||||
def client(mock_app_config):
|
||||
"""Create a DeerFlowClient with mocked config loading."""
|
||||
with patch("deerflow.client.get_app_config", return_value=mock_app_config):
|
||||
with patch.object(AppConfig, "current", return_value=mock_app_config):
|
||||
return DeerFlowClient()
|
||||
|
||||
|
||||
@@ -66,7 +67,7 @@ class TestClientInit:
|
||||
|
||||
def test_custom_params(self, mock_app_config):
|
||||
mock_middleware = MagicMock()
|
||||
with patch("deerflow.client.get_app_config", return_value=mock_app_config):
|
||||
with patch.object(AppConfig, "current", return_value=mock_app_config):
|
||||
c = DeerFlowClient(model_name="gpt-4", thinking_enabled=False, subagent_enabled=True, plan_mode=True, agent_name="test-agent", available_skills={"skill1", "skill2"}, middlewares=[mock_middleware])
|
||||
assert c._model_name == "gpt-4"
|
||||
assert c._thinking_enabled is False
|
||||
@@ -77,7 +78,7 @@ class TestClientInit:
|
||||
assert c._middlewares == [mock_middleware]
|
||||
|
||||
def test_invalid_agent_name(self, mock_app_config):
|
||||
with patch("deerflow.client.get_app_config", return_value=mock_app_config):
|
||||
with patch.object(AppConfig, "current", return_value=mock_app_config):
|
||||
with pytest.raises(ValueError, match="Invalid agent name"):
|
||||
DeerFlowClient(agent_name="invalid name with spaces!")
|
||||
with pytest.raises(ValueError, match="Invalid agent name"):
|
||||
@@ -85,15 +86,17 @@ class TestClientInit:
|
||||
|
||||
def test_custom_config_path(self, mock_app_config):
|
||||
with (
|
||||
patch("deerflow.client.reload_app_config") as mock_reload,
|
||||
patch("deerflow.client.get_app_config", return_value=mock_app_config),
|
||||
patch.object(AppConfig, "from_file", return_value=mock_app_config) as mock_from_file,
|
||||
patch.object(AppConfig, "init") as mock_init,
|
||||
patch.object(AppConfig, "current", return_value=mock_app_config),
|
||||
):
|
||||
DeerFlowClient(config_path="/tmp/custom.yaml")
|
||||
mock_reload.assert_called_once_with("/tmp/custom.yaml")
|
||||
mock_from_file.assert_called_once_with("/tmp/custom.yaml")
|
||||
mock_init.assert_called_once_with(mock_app_config)
|
||||
|
||||
def test_checkpointer_stored(self, mock_app_config):
|
||||
cp = MagicMock()
|
||||
with patch("deerflow.client.get_app_config", return_value=mock_app_config):
|
||||
with patch.object(AppConfig, "current", return_value=mock_app_config):
|
||||
c = DeerFlowClient(checkpointer=cp)
|
||||
assert c._checkpointer is cp
|
||||
|
||||
@@ -249,8 +252,8 @@ class TestStream:
|
||||
# Verify context passed to agent.stream
|
||||
agent.stream.assert_called_once()
|
||||
call_kwargs = agent.stream.call_args.kwargs
|
||||
assert call_kwargs["context"]["thread_id"] == "t1"
|
||||
assert call_kwargs["context"]["agent_name"] == "test-agent-1"
|
||||
ctx = call_kwargs["context"]
|
||||
assert ctx.app_config is client._app_config
|
||||
|
||||
def test_custom_mode_is_normalized_to_string(self, client):
|
||||
"""stream() forwards custom events even when the mode is not a plain string."""
|
||||
@@ -1089,7 +1092,7 @@ class TestMcpConfig:
|
||||
ext_config = MagicMock()
|
||||
ext_config.mcp_servers = {"github": server}
|
||||
|
||||
with patch("deerflow.client.get_extensions_config", return_value=ext_config):
|
||||
with patch.object(AppConfig, "current", return_value=MagicMock(extensions=ext_config)):
|
||||
result = client.get_mcp_config()
|
||||
|
||||
assert "mcp_servers" in result
|
||||
@@ -1114,10 +1117,12 @@ class TestMcpConfig:
|
||||
# Pre-set agent to verify it gets invalidated
|
||||
client._agent = MagicMock()
|
||||
|
||||
# Set initial AppConfig with current extensions
|
||||
AppConfig.init(MagicMock(extensions=current_config))
|
||||
|
||||
with (
|
||||
patch("deerflow.client.ExtensionsConfig.resolve_config_path", return_value=tmp_path),
|
||||
patch("deerflow.client.get_extensions_config", return_value=current_config),
|
||||
patch("deerflow.client.reload_extensions_config", return_value=reloaded_config),
|
||||
patch("deerflow.config.app_config.AppConfig.from_file", return_value=MagicMock(extensions=reloaded_config)),
|
||||
):
|
||||
result = client.update_mcp_config({"new-server": {"enabled": True, "type": "sse"}})
|
||||
|
||||
@@ -1179,8 +1184,8 @@ class TestSkillsManagement:
|
||||
with (
|
||||
patch("deerflow.skills.loader.load_skills", side_effect=[[skill], [updated_skill]]),
|
||||
patch("deerflow.client.ExtensionsConfig.resolve_config_path", return_value=tmp_path),
|
||||
patch("deerflow.client.get_extensions_config", return_value=ext_config),
|
||||
patch("deerflow.client.reload_extensions_config"),
|
||||
patch.object(AppConfig, "current", return_value=MagicMock(extensions=ext_config)),
|
||||
patch("deerflow.config.app_config.AppConfig.from_file", return_value=MagicMock()),
|
||||
):
|
||||
result = client.update_skill("test-skill", enabled=False)
|
||||
assert result["enabled"] is False
|
||||
@@ -1311,35 +1316,40 @@ class TestMemoryManagement:
|
||||
assert result == data
|
||||
|
||||
def test_get_memory_config(self, client):
|
||||
config = MagicMock()
|
||||
config.enabled = True
|
||||
config.storage_path = ".deer-flow/memory.json"
|
||||
config.debounce_seconds = 30
|
||||
config.max_facts = 100
|
||||
config.fact_confidence_threshold = 0.7
|
||||
config.injection_enabled = True
|
||||
config.max_injection_tokens = 2000
|
||||
mem_config = MagicMock()
|
||||
mem_config.enabled = True
|
||||
mem_config.storage_path = ".deer-flow/memory.json"
|
||||
mem_config.debounce_seconds = 30
|
||||
mem_config.max_facts = 100
|
||||
mem_config.fact_confidence_threshold = 0.7
|
||||
mem_config.injection_enabled = True
|
||||
mem_config.max_injection_tokens = 2000
|
||||
|
||||
with patch("deerflow.config.memory_config.get_memory_config", return_value=config):
|
||||
app_cfg = MagicMock()
|
||||
app_cfg.memory = mem_config
|
||||
|
||||
with patch.object(AppConfig, "current", return_value=app_cfg):
|
||||
result = client.get_memory_config()
|
||||
|
||||
assert result["enabled"] is True
|
||||
assert result["max_facts"] == 100
|
||||
|
||||
def test_get_memory_status(self, client):
|
||||
config = MagicMock()
|
||||
config.enabled = True
|
||||
config.storage_path = ".deer-flow/memory.json"
|
||||
config.debounce_seconds = 30
|
||||
config.max_facts = 100
|
||||
config.fact_confidence_threshold = 0.7
|
||||
config.injection_enabled = True
|
||||
config.max_injection_tokens = 2000
|
||||
mem_config = MagicMock()
|
||||
mem_config.enabled = True
|
||||
mem_config.storage_path = ".deer-flow/memory.json"
|
||||
mem_config.debounce_seconds = 30
|
||||
mem_config.max_facts = 100
|
||||
mem_config.fact_confidence_threshold = 0.7
|
||||
mem_config.injection_enabled = True
|
||||
mem_config.max_injection_tokens = 2000
|
||||
|
||||
app_cfg = MagicMock()
|
||||
app_cfg.memory = mem_config
|
||||
data = {"version": "1.0", "facts": []}
|
||||
|
||||
with (
|
||||
patch("deerflow.config.memory_config.get_memory_config", return_value=config),
|
||||
patch.object(AppConfig, "current", return_value=app_cfg),
|
||||
patch("deerflow.agents.memory.updater.get_memory_data", return_value=data),
|
||||
):
|
||||
result = client.get_memory_status()
|
||||
@@ -1783,10 +1793,10 @@ class TestScenarioConfigManagement:
|
||||
reloaded_config.mcp_servers = {"my-mcp": reloaded_server}
|
||||
|
||||
client._agent = MagicMock() # Simulate existing agent
|
||||
AppConfig.init(MagicMock(extensions=current_config))
|
||||
with (
|
||||
patch("deerflow.client.ExtensionsConfig.resolve_config_path", return_value=config_file),
|
||||
patch("deerflow.client.get_extensions_config", return_value=current_config),
|
||||
patch("deerflow.client.reload_extensions_config", return_value=reloaded_config),
|
||||
patch("deerflow.config.app_config.AppConfig.from_file", return_value=MagicMock(extensions=reloaded_config)),
|
||||
):
|
||||
mcp_result = client.update_mcp_config({"my-mcp": {"enabled": True}})
|
||||
assert "my-mcp" in mcp_result["mcp_servers"]
|
||||
@@ -1815,8 +1825,8 @@ class TestScenarioConfigManagement:
|
||||
with (
|
||||
patch("deerflow.skills.loader.load_skills", side_effect=[[skill], [toggled]]),
|
||||
patch("deerflow.client.ExtensionsConfig.resolve_config_path", return_value=config_file),
|
||||
patch("deerflow.client.get_extensions_config", return_value=ext_config),
|
||||
patch("deerflow.client.reload_extensions_config"),
|
||||
patch.object(AppConfig, "current", return_value=MagicMock(extensions=ext_config)),
|
||||
patch("deerflow.config.app_config.AppConfig.from_file", return_value=MagicMock()),
|
||||
):
|
||||
skill_result = client.update_skill("code-gen", enabled=False)
|
||||
assert skill_result["enabled"] is False
|
||||
@@ -2001,8 +2011,10 @@ class TestScenarioMemoryWorkflow:
|
||||
refreshed = client.reload_memory()
|
||||
assert len(refreshed["facts"]) == 2
|
||||
|
||||
app_cfg = MagicMock()
|
||||
app_cfg.memory = config
|
||||
with (
|
||||
patch("deerflow.config.memory_config.get_memory_config", return_value=config),
|
||||
patch.object(AppConfig, "current", return_value=app_cfg),
|
||||
patch("deerflow.agents.memory.updater.get_memory_data", return_value=updated_data),
|
||||
):
|
||||
status = client.get_memory_status()
|
||||
@@ -2065,8 +2077,8 @@ class TestScenarioSkillInstallAndUse:
|
||||
with (
|
||||
patch("deerflow.skills.loader.load_skills", side_effect=[[installed_skill], [disabled_skill]]),
|
||||
patch("deerflow.client.ExtensionsConfig.resolve_config_path", return_value=config_file),
|
||||
patch("deerflow.client.get_extensions_config", return_value=ext_config),
|
||||
patch("deerflow.client.reload_extensions_config"),
|
||||
patch.object(AppConfig, "current", return_value=MagicMock(extensions=ext_config)),
|
||||
patch("deerflow.config.app_config.AppConfig.from_file", return_value=MagicMock()),
|
||||
):
|
||||
toggled = client.update_skill("my-analyzer", enabled=False)
|
||||
assert toggled["enabled"] is False
|
||||
@@ -2198,7 +2210,7 @@ class TestGatewayConformance:
|
||||
model.supports_thinking = False
|
||||
mock_app_config.models = [model]
|
||||
|
||||
with patch("deerflow.client.get_app_config", return_value=mock_app_config):
|
||||
with patch.object(AppConfig, "current", return_value=mock_app_config):
|
||||
client = DeerFlowClient()
|
||||
|
||||
result = client.list_models()
|
||||
@@ -2217,7 +2229,7 @@ class TestGatewayConformance:
|
||||
mock_app_config.models = [model]
|
||||
mock_app_config.get_model_config.return_value = model
|
||||
|
||||
with patch("deerflow.client.get_app_config", return_value=mock_app_config):
|
||||
with patch.object(AppConfig, "current", return_value=mock_app_config):
|
||||
client = DeerFlowClient()
|
||||
|
||||
result = client.get_model("test-model")
|
||||
@@ -2287,7 +2299,7 @@ class TestGatewayConformance:
|
||||
ext_config = MagicMock()
|
||||
ext_config.mcp_servers = {"test": server}
|
||||
|
||||
with patch("deerflow.client.get_extensions_config", return_value=ext_config):
|
||||
with patch.object(AppConfig, "current", return_value=MagicMock(extensions=ext_config)):
|
||||
result = client.get_mcp_config()
|
||||
|
||||
parsed = McpConfigResponse(**result)
|
||||
@@ -2313,9 +2325,9 @@ class TestGatewayConformance:
|
||||
config_file.write_text("{}")
|
||||
|
||||
with (
|
||||
patch("deerflow.client.get_extensions_config", return_value=ext_config),
|
||||
patch.object(AppConfig, "current", return_value=MagicMock(extensions=ext_config)),
|
||||
patch("deerflow.client.ExtensionsConfig.resolve_config_path", return_value=config_file),
|
||||
patch("deerflow.client.reload_extensions_config", return_value=ext_config),
|
||||
patch("deerflow.config.app_config.AppConfig.from_file", return_value=MagicMock(extensions=ext_config)),
|
||||
):
|
||||
result = client.update_mcp_config({"srv": server.model_dump.return_value})
|
||||
|
||||
@@ -2346,7 +2358,10 @@ class TestGatewayConformance:
|
||||
mem_cfg.injection_enabled = True
|
||||
mem_cfg.max_injection_tokens = 2000
|
||||
|
||||
with patch("deerflow.config.memory_config.get_memory_config", return_value=mem_cfg):
|
||||
app_cfg = MagicMock()
|
||||
app_cfg.memory = mem_cfg
|
||||
|
||||
with patch.object(AppConfig, "current", return_value=app_cfg):
|
||||
result = client.get_memory_config()
|
||||
|
||||
parsed = MemoryConfigResponse(**result)
|
||||
@@ -2363,6 +2378,8 @@ class TestGatewayConformance:
|
||||
mem_cfg.injection_enabled = True
|
||||
mem_cfg.max_injection_tokens = 2000
|
||||
|
||||
app_cfg = MagicMock()
|
||||
app_cfg.memory = mem_cfg
|
||||
memory_data = {
|
||||
"version": "1.0",
|
||||
"lastUpdated": "",
|
||||
@@ -2380,7 +2397,7 @@ class TestGatewayConformance:
|
||||
}
|
||||
|
||||
with (
|
||||
patch("deerflow.config.memory_config.get_memory_config", return_value=mem_cfg),
|
||||
patch.object(AppConfig, "current", return_value=app_cfg),
|
||||
patch("deerflow.agents.memory.updater.get_memory_data", return_value=memory_data),
|
||||
):
|
||||
result = client.get_memory_status()
|
||||
@@ -2671,8 +2688,8 @@ class TestConfigUpdateErrors:
|
||||
with (
|
||||
patch("deerflow.skills.loader.load_skills", side_effect=[[skill], []]),
|
||||
patch("deerflow.client.ExtensionsConfig.resolve_config_path", return_value=config_file),
|
||||
patch("deerflow.client.get_extensions_config", return_value=ext_config),
|
||||
patch("deerflow.client.reload_extensions_config"),
|
||||
patch.object(AppConfig, "current", return_value=MagicMock(extensions=ext_config)),
|
||||
patch("deerflow.config.app_config.AppConfig.from_file", return_value=MagicMock()),
|
||||
):
|
||||
with pytest.raises(RuntimeError, match="disappeared"):
|
||||
client.update_skill("ghost-skill", enabled=False)
|
||||
@@ -3042,10 +3059,10 @@ class TestBugAgentInvalidationInconsistency:
|
||||
config_file = Path(tmp) / "ext.json"
|
||||
config_file.write_text("{}")
|
||||
|
||||
AppConfig.init(MagicMock(extensions=current_config))
|
||||
with (
|
||||
patch("deerflow.client.ExtensionsConfig.resolve_config_path", return_value=config_file),
|
||||
patch("deerflow.client.get_extensions_config", return_value=current_config),
|
||||
patch("deerflow.client.reload_extensions_config", return_value=reloaded),
|
||||
patch("deerflow.config.app_config.AppConfig.from_file", return_value=MagicMock(extensions=reloaded)),
|
||||
):
|
||||
client.update_mcp_config({})
|
||||
|
||||
@@ -3077,8 +3094,8 @@ class TestBugAgentInvalidationInconsistency:
|
||||
with (
|
||||
patch("deerflow.skills.loader.load_skills", side_effect=[[skill], [updated]]),
|
||||
patch("deerflow.client.ExtensionsConfig.resolve_config_path", return_value=config_file),
|
||||
patch("deerflow.client.get_extensions_config", return_value=ext_config),
|
||||
patch("deerflow.client.reload_extensions_config"),
|
||||
patch.object(AppConfig, "current", return_value=MagicMock(extensions=ext_config)),
|
||||
patch("deerflow.config.app_config.AppConfig.from_file", return_value=MagicMock()),
|
||||
):
|
||||
client.update_skill("s1", enabled=False)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user