Creating Custom Function Tools for ChatDev Agents: A Complete Implementation Guide
You can create custom function tools for ChatDev agents by adding Python files to the function calling directory or using the POST /api/tools/local endpoint, which automatically registers callable functions via FunctionManager and generates OpenAI-compatible JSON schemas through FunctionCatalog.
ChatDev enables agents to invoke external capabilities through function-calling tools—ordinary Python functions that are dynamically discovered and exposed via a FastAPI interface. According to the OpenBMB/ChatDev source code, this runtime-extensible architecture allows developers to add new capabilities by simply dropping .py files into a designated folder, with immediate availability to all agents without server restarts.
How Function Discovery Works in ChatDev
The foundation of ChatDev's tool system rests on dynamic module loading. When the system initializes, it scans a configurable directory for Python files, imports them as modules, and registers any callable functions for agent use.
The FunctionManager Class
Located in utils/function_manager.py, the FunctionManager class handles the discovery and registration pipeline. It resolves the tools directory using the environment variable MAC_FUNCTIONS_DIR (falling back to functions/function_calling):
_FUNCTION_CALLING_ENV = "MAC_FUNCTIONS_DIR"
_DEFAULT_FUNCTION_CALLING_DIR = Path("functions") / "function_calling"
FUNCTION_CALLING_DIR = _resolve_dir(_DEFAULT_FUNCTION_CALLING_DIR,
_FUNCTION_CALLING_ENV).resolve()
The manager walks the directory tree using rglob("*.py"), skips private files (those starting with _ or named __init__.py), and imports each module with a unique identifier:
def load_functions(self) -> None:
for file in self.functions_dir.rglob("*.py"):
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
for name, obj in inspect.getmembers(module, inspect.isfunction):
if name.startswith("_"): continue
self.functions[name] = obj
The manager caches instances per directory via _function_managers and provides access through get_function_manager().
Building OpenAI-Compatible Tool Schemas
Once functions are loaded, FunctionCatalog (from utils/function_catalog.py) inspects them to build metadata required for LLM function calling. This includes generating JSON Schema definitions from Python type hints and docstrings.
Metadata Extraction and Type Inspection
The catalog uses inspect.signature() to analyze parameters and _resolve_annotations() to handle complex types including Annotated, Union, Literal, and custom enums:
def _build_function_metadata(name: str, fn: Any, functions_dir: Path) -> FunctionMetadata:
signature = inspect.signature(fn)
annotations = _resolve_annotations(fn)
description = _extract_description(fn)
schema = _build_parameters_schema(signature, annotations)
Key implementation details:
- Descriptions are extracted from the first paragraph of the function docstring, trimmed to 600 characters
- Parameters follow strict JSON-Schema rules via
_build_parameters_schema - The catalog lazy-loads on first access and caches results in
self._loaded
Refresh the catalog after adding new files:
catalog = get_function_catalog()
catalog.refresh()
HTTP API for Tool Management
The FastAPI routes in server/routes/tools.py expose two critical endpoints for tool management, enabling both inspection and runtime extension of the toolset.
Listing Available Tools (GET /api/tools/local)
The GET endpoint retrieves all registered tools with their schemas:
@router.get("/api/tools/local")
def list_local_tools():
catalog = get_function_catalog()
metadata = catalog.list_metadata()
tools = [
{
"name": name,
"description": meta.description,
"parameters": meta.parameters_schema,
"module": meta.module_name,
"file_path": meta.file_path,
}
for name, meta in metadata.items()
]
return {
"success": True,
"count": len(tools),
"tools": tools,
"load_error": str(catalog.load_error) if catalog.load_error else None,
}
This returns a JSON array containing each tool's name, description, parameter schema, and source file location.
Creating New Tools via API (POST /api/tools/local)
The POST endpoint enables runtime tool creation without filesystem access:
@router.post("/api/tools/local")
def create_local_tool(payload: LocalToolCreateRequest):
filename = payload.filename.strip()
if not re.match(r"^[A-Za-z0-9_-]+(\.py)?$", filename):
raise HTTPException(400, "filename must be alphanumeric")
tools_dir = Path(FUNCTION_CALLING_DIR).resolve()
target_path = (tools_dir / (filename if filename.endswith(".py") else f"{filename}.py")).resolve()
target_path.relative_to(tools_dir) # Security check
target_path.write_text(payload.content, encoding="utf-8")
catalog = get_function_catalog()
catalog.refresh()
Security features:
- Filename validation using regex (
^[A-Za-z0-9_-]+(\.py)?$) - Path traversal prevention via
relative_to()check - Automatic catalog refresh after file creation
Implementing a Custom Tool: Step-by-Step Example
To create a functional tool, define a Python function with type hints and a descriptive docstring in the functions/function_calling/ directory:
# functions/function_calling/hello_world.py
def hello_world(name: str) -> str:
"""
Greet the user with a friendly message.
Parameters
----------
name: str
The name of the person to greet.
Returns
-------
str
A greeting sentence.
"""
return f"Hello, {name}! 👋"
After saving, the function appears in API listings with a generated schema marking name as a required string parameter.
Uploading Tools via HTTP
Alternatively, upload tools programmatically:
curl -X POST http://localhost:8000/api/tools/local \
-H "Content-Type: application/json" \
-d '{
"filename": "adder.py",
"content": "def adder(a: int, b: int) -> int:\n \"\"\"Return a+b.\"\"\"\n return a + b",
"overwrite": true
}'
A successful response includes load_error: null, confirming the tool is immediately available.
How Agents Execute Custom Functions
When an LLM requests a function call, the agent executor (runtime/node/executor/agent_executor.py) retrieves and executes the function:
func = runtime.context.function_manager.get_function("adder")
result = func(a=3, b=5) # Returns 8
The executor handles parameter validation and returns results to the conversation context, enabling seamless integration between agent reasoning and external capabilities.
Summary
- FunctionManager (
utils/function_manager.py) dynamically loads Python files fromMAC_FUNCTIONS_DIR, registering public functions for agent use - FunctionCatalog (
utils/function_catalog.py) inspects registered functions to build OpenAI-compatible JSON schemas from type hints and docstrings - API endpoints (
server/routes/tools.py) provide HTTP interfaces for listing tools (GET) and creating new ones (POST) with automatic validation and security checks - Runtime execution occurs through
context.function_manager.get_function()in the agent executor, enabling immediate invocation of newly added tools - Configuration defaults to
functions/function_calling/but is overrideable via theMAC_FUNCTIONS_DIRenvironment variable
Frequently Asked Questions
Where should I place custom tool files in ChatDev?
Place Python files in the directory specified by the MAC_FUNCTIONS_DIR environment variable, or in the default location functions/function_calling/. The FunctionManager automatically discovers all .py files in this directory tree during startup and catalog refreshes, excluding private files that start with an underscore.
What Python types are supported for tool parameters?
ChatDev's FunctionCatalog supports standard JSON-Schema convertible types including int, str, float, bool, list, and dict. It also handles advanced typing constructs via _annotation_to_schema(), including Annotated, Union, Literal, Optional, custom enums, and nested collections. All parameters require type hints for proper schema generation.
How do I update an existing tool without restarting the server?
Overwrite the Python file and trigger a catalog refresh. If using the filesystem, modify the file in FUNCTION_CALLING_DIR and call get_function_catalog().refresh(). When using the API, set overwrite: true in your POST request to /api/tools/local—the endpoint automatically refreshes the catalog after writing the file, making the updated tool immediately available to agents.
Can I restrict which functions are exposed to agents?
Yes. The FunctionManager automatically excludes any function starting with an underscore (_) or defined in files starting with an underscore. To hide specific functions from the catalog, prefix them with an underscore in their definition. For additional security, modify server/routes/tools.py to implement authentication middleware or validate tool contents before allowing the POST endpoint to write files.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →