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:
greatmengqi
2026-04-13 23:49:31 +08:00
parent c4d273a68a
commit edf345cd72
111 changed files with 4848 additions and 4079 deletions
+17 -11
View File
@@ -11,7 +11,13 @@ from deerflow.agents.memory.storage import (
create_empty_memory,
get_memory_storage,
)
from deerflow.config.app_config import AppConfig
from deerflow.config.memory_config import MemoryConfig
from deerflow.config.sandbox_config import SandboxConfig
def _app_config(**memory_overrides) -> AppConfig:
return AppConfig(sandbox=SandboxConfig(use="test"), memory=MemoryConfig(**memory_overrides))
class TestCreateEmptyMemory:
@@ -53,7 +59,7 @@ class TestFileMemoryStorage:
return mock_paths
with patch("deerflow.agents.memory.storage.get_paths", side_effect=mock_get_paths):
with patch("deerflow.agents.memory.storage.get_memory_config", return_value=MemoryConfig(storage_path="")):
with patch.object(AppConfig, "current", return_value=_app_config(storage_path="")):
storage = FileMemoryStorage()
path = storage._get_memory_file_path(None)
assert path == tmp_path / "memory.json"
@@ -87,7 +93,7 @@ class TestFileMemoryStorage:
return mock_paths
with patch("deerflow.agents.memory.storage.get_paths", side_effect=mock_get_paths):
with patch("deerflow.agents.memory.storage.get_memory_config", return_value=MemoryConfig(storage_path="")):
with patch.object(AppConfig, "current", return_value=_app_config(storage_path="")):
storage = FileMemoryStorage()
memory = storage.load()
assert isinstance(memory, dict)
@@ -103,7 +109,7 @@ class TestFileMemoryStorage:
return mock_paths
with patch("deerflow.agents.memory.storage.get_paths", side_effect=mock_get_paths):
with patch("deerflow.agents.memory.storage.get_memory_config", return_value=MemoryConfig(storage_path="")):
with patch.object(AppConfig, "current", return_value=_app_config(storage_path="")):
storage = FileMemoryStorage()
test_memory = {"version": "1.0", "facts": [{"content": "test fact"}]}
result = storage.save(test_memory)
@@ -122,7 +128,7 @@ class TestFileMemoryStorage:
return mock_paths
with patch("deerflow.agents.memory.storage.get_paths", side_effect=mock_get_paths):
with patch("deerflow.agents.memory.storage.get_memory_config", return_value=MemoryConfig(storage_path="")):
with patch.object(AppConfig, "current", return_value=_app_config(storage_path="")):
storage = FileMemoryStorage()
# First load
memory1 = storage.load()
@@ -150,19 +156,19 @@ class TestGetMemoryStorage:
def test_returns_file_memory_storage_by_default(self):
"""Should return FileMemoryStorage by default."""
with patch("deerflow.agents.memory.storage.get_memory_config", return_value=MemoryConfig(storage_class="deerflow.agents.memory.storage.FileMemoryStorage")):
with patch.object(AppConfig, "current", return_value=_app_config(storage_class="deerflow.agents.memory.storage.FileMemoryStorage")):
storage = get_memory_storage()
assert isinstance(storage, FileMemoryStorage)
def test_falls_back_to_file_memory_storage_on_error(self):
"""Should fall back to FileMemoryStorage if configured storage fails to load."""
with patch("deerflow.agents.memory.storage.get_memory_config", return_value=MemoryConfig(storage_class="non.existent.StorageClass")):
with patch.object(AppConfig, "current", return_value=_app_config(storage_class="non.existent.StorageClass")):
storage = get_memory_storage()
assert isinstance(storage, FileMemoryStorage)
def test_returns_singleton_instance(self):
"""Should return the same instance on subsequent calls."""
with patch("deerflow.agents.memory.storage.get_memory_config", return_value=MemoryConfig(storage_class="deerflow.agents.memory.storage.FileMemoryStorage")):
with patch.object(AppConfig, "current", return_value=_app_config(storage_class="deerflow.agents.memory.storage.FileMemoryStorage")):
storage1 = get_memory_storage()
storage2 = get_memory_storage()
assert storage1 is storage2
@@ -173,11 +179,11 @@ class TestGetMemoryStorage:
def get_storage():
# get_memory_storage is called concurrently from multiple threads while
# get_memory_config is patched once around thread creation. This verifies
# AppConfig.get is patched once around thread creation. This verifies
# that the singleton initialization remains thread-safe.
results.append(get_memory_storage())
with patch("deerflow.agents.memory.storage.get_memory_config", return_value=MemoryConfig(storage_class="deerflow.agents.memory.storage.FileMemoryStorage")):
with patch.object(AppConfig, "current", return_value=_app_config(storage_class="deerflow.agents.memory.storage.FileMemoryStorage")):
threads = [threading.Thread(target=get_storage) for _ in range(10)]
for t in threads:
t.start()
@@ -191,13 +197,13 @@ class TestGetMemoryStorage:
def test_get_memory_storage_invalid_class_fallback(self):
"""Should fall back to FileMemoryStorage if the configured class is not actually a class."""
# Using a built-in function instead of a class
with patch("deerflow.agents.memory.storage.get_memory_config", return_value=MemoryConfig(storage_class="os.path.join")):
with patch.object(AppConfig, "current", return_value=_app_config(storage_class="os.path.join")):
storage = get_memory_storage()
assert isinstance(storage, FileMemoryStorage)
def test_get_memory_storage_non_subclass_fallback(self):
"""Should fall back to FileMemoryStorage if the configured class is not a subclass of MemoryStorage."""
# Using 'dict' as a class that is not a MemoryStorage subclass
with patch("deerflow.agents.memory.storage.get_memory_config", return_value=MemoryConfig(storage_class="builtins.dict")):
with patch.object(AppConfig, "current", return_value=_app_config(storage_class="builtins.dict")):
storage = get_memory_storage()
assert isinstance(storage, FileMemoryStorage)