Tools Override
Load custom tools from files and directories
Overview
The Tool Override system allows loading custom tools from Python files, modules, and directories at runtime. This enables extending PraisonAI with custom functionality without modifying the core package.
Python API
ToolOverrideLoader
from praisonai.templates import ToolOverrideLoader
loader = ToolOverrideLoader()
# Load tools from a Python file
tools = loader.load_from_file("./my_tools.py")
print(f"Loaded tools: {list(tools.keys())}")
# Load tools from a module — requires PRAISONAI_ALLOW_LOCAL_TOOLS=true.
# Importing a module runs its code, so this raises PermissionError when unset.
tools = loader.load_from_module("mypackage.tools")
# Load tools from a directory
tools = loader.load_from_directory("~/.praisonai/tools")Both load_from_file and load_from_module funnel through the same PRAISONAI_ALLOW_LOCAL_TOOLS=true gate as the runtime loader. load_from_module raises PermissionError when the env var is unset:
PermissionError: PRAISONAI_ALLOW_LOCAL_TOOLS=true is required to import user modules (refusing to load 'mypackage.tools').Context Manager Pattern
Use the context manager for temporary tool overrides:
from praisonai.templates import ToolOverrideLoader
loader = ToolOverrideLoader()
with loader.override_context(
files=["./custom_tools.py"],
directories=["./more_tools"]
) as tools:
# Tools are available within this context
print(f"Available tools: {list(tools.keys())}")
# Tools are cleaned up after context exitsCreating Tool Registry with Overrides
from praisonai.templates.tool_override import create_tool_registry_with_overrides
# Create registry with custom tools
registry = create_tool_registry_with_overrides(
override_files=["./my_tools.py"],
override_dirs=["~/.praisonai/tools"],
include_defaults=True # Include built-in tools
)
# Wrapper-owned resolution order (highest priority first):
# 1. Override files (explicit CLI --tools)
# 2. Override directories (explicit CLI --tools-dir)
# 3. Template tools_sources (from TEMPLATE.yaml)
# 4. Template-local tools.py / cwd tools.py (autoload — requires
# PRAISONAI_ALLOW_TEMPLATE_TOOLS=1)
# 5. Default custom dirs (~/.praison/tools, ~/.config/praison/tools)
#
# A tools_source (step 3) that resolves to a dotted module is denied when
# PRAISONAI_ALLOW_LOCAL_TOOLS is unset. The skip is now logged at warning
# (logger.warning("skipping tools_source ...")) instead of being swallowed
# silently.
#
# praisonai-tools package discovery and praisonaiagents built-ins are
# NOT re-implemented here (fix: #3122). They are owned by
# praisonai_code.tool_resolver.ToolResolver, which resolve_tools()
# consults after this registry. See /docs/features/tool-resolver.Default Custom Tool Directories
PraisonAI loads custom tools from two distinct groups.
Autoload-gated files load only when PRAISONAI_ALLOW_TEMPLATE_TOOLS=1:
| Path | Description |
|---|---|
./tools.py | Current working directory autoload |
<template_dir>/tools.py | Template-local autoload (recipe directory) |
Default custom dirs are always scanned when include_defaults=True:
| Path | Description |
|---|---|
~/.praison/tools | Primary user tools |
~/.config/praison/tools | XDG-friendly location |
loader = ToolOverrideLoader()
default_dirs = loader.get_default_tool_dirs()
for d in default_dirs:
print(f"Default dir: {d}")What the wrapper does not own
create_tool_registry_with_overrides() used to re-implement praisonai-tools package discovery and a praisonaiagents built-ins hook of its own. As of PraisonAI#3122 those are handled only by the canonical ToolResolver, so:
- You do not need to install
praisonai-toolsdifferently —resolve_tools()still finds it via the canonical resolver. - A tool that resolved through the wrapper’s old package-discovery block still resolves — just one implementation away, in the resolver instead.
- The template/CWD
tools.pyautoload stays in the wrapper on purpose. It carries thePRAISONAI_ALLOW_TEMPLATE_TOOLSgate (with a skip-on-error contract and explicit-template-dir-outside-CWD support) that the resolver’sPRAISONAI_ALLOW_LOCAL_TOOLS+ CWD-boundary gate does not provide.
Discovering Tools Without Execution
Discover tool names without importing/executing code:
loader = ToolOverrideLoader()
# Uses AST parsing - safe, no code execution
tool_names = loader.discover_tools_in_directory("./tools")
print(f"Found tools: {tool_names}")Security
Local Paths Only
Remote URLs are rejected by default:
from praisonai.templates import ToolOverrideLoader, SecurityError
loader = ToolOverrideLoader()
try:
# This will raise SecurityError
loader.load_from_file("https://example.com/tools.py")
except SecurityError as e:
print(f"Blocked: {e}")Safe Defaults
- Only local file paths are allowed
- Both
load_from_fileandload_from_modulerequirePRAISONAI_ALLOW_LOCAL_TOOLS=true(same gate as the runtime loader) —load_from_moduleraisesPermissionErrorwhen it is unset - Implicit
tools.pyautoload from CWD or template directories is disabled by default — opt in withPRAISONAI_ALLOW_TEMPLATE_TOOLS=1 - Discovery uses AST parsing (no execution)
- Context manager ensures cleanup
Opting in to implicit tools.py autoload
For legacy workflows that depend on automatic tools.py loading:
export PRAISONAI_ALLOW_TEMPLATE_TOOLS=1Accepted truthy values: 1, true, yes, on (case-insensitive, whitespace-stripped)
Recommended approach: Use explicit override_files, override_dirs, or tools_sources configuration instead of relying on implicit autoload. See Security Environment Variables for more details.
Custom Tool File Format
Create a Python file with functions:
# my_tools.py
def my_search_tool(query: str) -> str:
"""Search for information."""
return f"Results for: {query}"
def my_calculator(expression: str) -> float:
"""Evaluate a math expression."""
raise ValueError("Use AST safe math — see /features/custom-tool-safe-eval")
# Private functions (starting with _) are ignored
def _helper():
pass