* feat(extensions): allow constructor kwargs on config-declared middlewares
extensions.middlewares entries may be a class-path string or {class, kwargs}.
String entries keep the zero-argument constructor. Unknown fields and blank
class paths fail at config validation. Constructor errors still fail at
agent creation.
Fixes #5311
* fix(extensions): coerce middleware kwargs to JSON types
YAML timestamps became datetime objects while JSON kept strings, so
constructors and to_file_dict() json.dump saw different types. Validate
kwargs as JSON types at config load, stringify dates, reject NaN and
other non-JSON values, and cover the raw-dict loader branch.
* style(extensions): wrap middleware Field description for ruff E501
make lint failed: the middlewares description was 289 chars (limit 240).
Wrap it and run ruff format on the two files this PR last touched.
* docs: compact configured middleware guidance to satisfy size limit
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
12 KiB
Contributing to DeerFlow Backend
Thank you for your interest in contributing to DeerFlow! This document provides guidelines and instructions for contributing to the backend codebase.
Table of Contents
- Getting Started
- Development Setup
- Project Structure
- Code Style
- Making Changes
- Testing
- Pull Request Process
- Architecture Guidelines
Getting Started
Prerequisites
- Python 3.12 or higher
- uv package manager
- Git
- Docker (optional, for Docker sandbox testing)
Fork and Clone
- Fork the repository on GitHub
- Clone your fork locally:
git clone https://github.com/YOUR_USERNAME/deer-flow.git cd deer-flow
Development Setup
Install Dependencies
# From project root
cp config.example.yaml config.yaml
# Install backend dependencies
cd backend
make install
Configure Environment
Set up your API keys for testing:
export OPENAI_API_KEY="your-api-key"
# Add other keys as needed
Run the Development Server
# Gateway API + embedded agent runtime
make dev
Project Structure
backend/
├── packages/harness/deerflow/ # deerflow-harness package (import: deerflow.*)
│ ├── agents/ # Agent system
│ │ ├── lead_agent/ # Main agent (agent.py factory, prompt.py)
│ │ ├── middlewares/ # Agent middleware chain
│ │ ├── memory/ # Memory extraction & storage
│ │ └── thread_state.py # Thread state definition
│ ├── sandbox/ # Sandbox execution
│ │ ├── local/ # Local sandbox provider
│ │ ├── sandbox.py # Abstract interface
│ │ ├── tools.py # Sandbox tools (bash, file ops)
│ │ └── middleware.py # Sandbox lifecycle
│ ├── subagents/ # Subagent delegation
│ ├── tools/builtins/ # Built-in tools
│ ├── mcp/ # MCP integration
│ ├── models/ # Model factory
│ ├── skills/ # Skills system
│ ├── config/ # Configuration system
│ ├── runtime/ # Embedded run execution (RunManager, StreamBridge)
│ ├── persistence/ # Checkpointer/store engines & schema migrations
│ ├── guardrails/ # Pre-tool-call authorization providers
│ ├── tracing/ # Tracer factory & trace metadata
│ ├── uploads/ # Uploads manager
│ ├── tui/ # Terminal UI (`deerflow` console script)
│ ├── community/ # Community tools (tavily/, jina_ai/, firecrawl/, …)
│ ├── reflection/ # Dynamic module loading
│ └── utils/ # Utilities
└── app/ # FastAPI Gateway + IM channels (import: app.*)
├── gateway/ # Gateway API
│ ├── app.py # FastAPI application
│ └── routers/ # Route handlers (threads, models, mcp, skills, uploads, …)
└── channels/ # IM channel integrations (Feishu, Slack, Telegram, …)
See AGENTS.md for the full module-by-module breakdown.
Code Style
Linting and Formatting
We use ruff for both linting and formatting:
# Check for issues
make lint
# Auto-fix and format
make format
Style Guidelines
- Line length: 240 characters maximum
- Python version: 3.12+ features allowed
- Type hints: Use type hints for function signatures
- Quotes: Double quotes for strings
- Indentation: 4 spaces (no tabs)
- Imports: Group by standard library, third-party, local
Docstrings
Use docstrings for public functions and classes:
def create_chat_model(name: str, thinking_enabled: bool = False) -> BaseChatModel:
"""Create a chat model instance from configuration.
Args:
name: The model name as defined in config.yaml
thinking_enabled: Whether to enable extended thinking
Returns:
A configured LangChain chat model instance
Raises:
ValueError: If the model name is not found in configuration
"""
...
Making Changes
Branch Naming
Use descriptive branch names:
feature/add-new-tool- New featuresfix/sandbox-timeout- Bug fixesdocs/update-readme- Documentationrefactor/config-system- Code refactoring
Commit Messages
Write clear, concise commit messages:
feat: add support for Claude 3.5 model
- Add model configuration in config.yaml
- Update model factory to handle Claude-specific settings
- Add tests for new model
Prefix types:
feat:- New featurefix:- Bug fixdocs:- Documentationrefactor:- Code refactoringtest:- Testschore:- Build/config changes
Testing
Running Tests
uv run pytest
Writing Tests
Place tests in the tests/ directory mirroring the source structure:
tests/
├── test_models/
│ └── test_factory.py
├── test_sandbox/
│ └── test_local.py
└── test_gateway/
└── test_models_router.py
Example test:
import pytest
from deerflow.models.factory import create_chat_model
def test_create_chat_model_with_valid_name():
"""Test that a valid model name creates a model instance."""
model = create_chat_model("gpt-4")
assert model is not None
def test_create_chat_model_with_invalid_name():
"""Test that an invalid model name raises ValueError."""
with pytest.raises(ValueError):
create_chat_model("nonexistent-model")
Pull Request Process
Before Submitting
- Ensure tests pass:
uv run pytest - Run linter:
make lint - Format code:
make format - Update documentation if needed
PR Description
Include in your PR description:
- What: Brief description of changes
- Why: Motivation for the change
- How: Implementation approach
- Testing: How you tested the changes
Review Process
- Submit PR with clear description
- Address review feedback
- Ensure CI passes
- Maintainer will merge when approved
Architecture Guidelines
Adding New Tools
- Create tool in
packages/harness/deerflow/tools/builtins/orpackages/harness/deerflow/community/:
# packages/harness/deerflow/tools/builtins/my_tool.py
from langchain_core.tools import tool
@tool
def my_tool(param: str) -> str:
"""Tool description for the agent.
Args:
param: Description of the parameter
Returns:
Description of return value
"""
return f"Result: {param}"
- Register in
config.yaml:
tools:
- name: my_tool
group: my_group
use: deerflow.tools.builtins.my_tool:my_tool
Adding New Middleware
- Create middleware in
packages/harness/deerflow/agents/middlewares/:
# packages/harness/deerflow/agents/middlewares/my_middleware.py
from langchain.agents import AgentState
from langchain.agents.middleware import AgentMiddleware
from langgraph.runtime import Runtime
class MyMiddleware(AgentMiddleware[AgentState]):
"""Middleware description."""
def before_model(self, state: AgentState, runtime: Runtime) -> dict | None:
"""Run before each model call."""
print(f"Model input contains {len(state.get('messages', []))} messages")
return None
def after_model(self, state: AgentState, runtime: Runtime) -> dict | None:
"""Run after each model call."""
messages = state.get("messages", [])
last_message = messages[-1] if messages else None
print(f"Last message type: {type(last_message).__name__ if last_message else 'none'}")
return None
Lifecycle hooks can return a dictionary of state updates, which LangChain merges
into the agent state, or None when they only observe state.
- Register the middleware class in
config.yaml. A class path uses the zero-argument constructor;{class, kwargs}passes constructor arguments.kwargsvalues must be JSON types (object, array, string, number, boolean, or null); YAML dates and timestamps are coerced to ISO strings so they match JSON:
extensions:
middlewares:
- deerflow.agents.middlewares.my_middleware:MyMiddleware
- class: deerflow.agents.middlewares.my_middleware:MyMiddleware
kwargs:
max_tool_calls: 5
Configured middleware runs after the built-in middleware and optional loop/token
guards. On the lead-agent pipeline, it runs before the terminal-response,
model-length, safety, and clarification tail; subagents have no
terminal-response, model-length, or clarification stage, so configured middleware is
followed by the optional safety guard, DurableContextMiddleware, optional
SummarizationMiddleware, then SubagentDateContextMiddleware and
SystemMessageCoalescingMiddleware. Treat middleware class paths as trusted
operator configuration because loading one executes Python code.
Embedded callers can instead use DeerFlowClient(middlewares=[...]), which
builds the full lead-agent chain and places middleware before its
terminal-response, model-length, safety, and clarification tail.
create_deerflow_agent(extra_middleware=[...]) instead builds a smaller
feature-based lead-agent chain; unanchored extras are placed immediately before
ClarificationMiddleware (anchored extras follow their @Next/@Prev
placement, but the anchor must be present in this smaller chain). Neither API
forwards middleware to subagents.
Choose the registration path by ownership and placement. The fixed-slot (not deprecated)
extensions.middlewares list is accepted in config.yaml and
extensions_config.json (config.yaml wins) and applies to both lead and
subagent pipelines. Packaged extensions registered through the top-level
plugins: list contribute middleware at semantic extension points. Contributor
code that needs committed, programmatic lead-only wiring can use
build_middlewares(..., custom_middlewares=[MyMiddleware()]) at the
build_middlewares call in
packages/harness/deerflow/agents/lead_agent/agent.py (reached through
make_lead_agent).
Adding New API Endpoints
- Create router in
app/gateway/routers/:
# app/gateway/routers/my_router.py
from fastapi import APIRouter
router = APIRouter(prefix="/my-endpoint", tags=["my-endpoint"])
@router.get("/")
async def get_items():
"""Get all items."""
return {"items": []}
@router.post("/")
async def create_item(data: dict):
"""Create a new item."""
return {"created": data}
- Register in
app/gateway/app.py:
from app.gateway.routers import my_router
app.include_router(my_router.router)
Configuration Changes
When adding new configuration options:
- Update
packages/harness/deerflow/config/app_config.pywith new fields - Add default values in
config.example.yaml - Document in
docs/CONFIGURATION.md
MCP Server Integration
To add support for a new MCP server:
- Add configuration in
extensions_config.json:
{
"mcpServers": {
"my-server": {
"enabled": true,
"type": "stdio",
"command": "npx",
"args": ["-y", "@my-org/mcp-server"],
"description": "My MCP Server"
}
}
}
- Update
extensions_config.example.jsonwith the new server
Skills Development
To create a new skill:
- Create directory in
skills/public/orskills/custom/:
skills/public/my-skill/
└── SKILL.md
- Write
SKILL.mdwith YAML front matter:
---
name: My Skill
description: What this skill does
license: MIT
allowed-tools:
- read_file
- write_file
- bash
---
# My Skill
Instructions for the agent when this skill is enabled...
Questions?
If you have questions about contributing:
- Check existing documentation in
docs/ - Look for similar issues or PRs on GitHub
- Open a discussion or issue on GitHub
Thank you for contributing to DeerFlow!