Skip to main content
ToolResolver is the one place PraisonAI looks for tools.py — whether it ships callables or BaseTool classes.

Quick Start

1

Basic Usage

Drop a tools.py next to your YAML/script, set the environment variable, and the resolver picks both kinds up automatically:
export PRAISONAI_ALLOW_LOCAL_TOOLS=true
from praisonaiagents import Agent

# Tools from tools.py are automatically loaded
agent = Agent(
    name="Tool User",
    instructions="Use available tools to help the user"
)

agent.start("Calculate something using my tools")
2

Direct Python Usage

When embedding PraisonAI in your own Python code:
from praisonai.tool_resolver import ToolResolver

resolver = ToolResolver()  # defaults to ./tools.py
callables = resolver.get_local_callables()       # path A: functions
tool_classes = resolver.get_local_tool_classes() # path B: BaseTool instances

print(f"Found {len(callables)} functions")
print(f"Found {len(tool_classes)} tool classes")

Resolution Order

Tools are resolved in a specific order, with the first match winning:

Registering Tools at Runtime

Register custom tools through the ToolRegistry for YAML pipeline access:
from praisonai import PraisonAI
from praisonai.tool_registry import ToolRegistry

def my_search(query: str) -> str:
    """Custom search function."""
    return f"results for {query}"

praison = PraisonAI(agent_file="agents.yaml")
praison.agents_generator.tool_registry.register_function("my_search", my_search)
praison.run()
Then reference in your agents.yaml:
roles:
  researcher:
    backstory: "You are a research specialist"
    goal: "Find information using available tools"
    tools:
      - my_search  # Now resolvable through the registry
For bulk registration from a module:
import my_tools_module

# Register all public functions from a module
praison.agents_generator.tool_registry.register_from_module(my_tools_module)

How It Works

The resolver delegates to _safe_loader.load_user_module for consistent environment variable checking and CWD path-traversal guard. The loaded module is reflected to extract either plain functions or tool class instances, then cached as an immutable view for thread safety. The wrapper now invokes resolve() once per YAML-referenced tool name, with results cached via the resolve cache to avoid repeated lookups. Directory mode iterates each .py file and unions the results using the same security gates and extraction logic.

Multi-tenant usage

ToolResolver resolves tools.py eagerly at construction time using Path.resolve(). The path captured is the CWD when the resolver was created — not the CWD when resolve() is called later. This makes behaviour predictable in multi-tenant gateways where each tenant has a different working directory.
from praisonai.tool_resolver import ToolResolver, reset_default_resolver

# Tenant A's request
os.chdir("/tenants/a")
resolver_a = ToolResolver()              # binds to /tenants/a/tools.py
tools_a = resolver_a.get_local_callables()

# Tenant B's request — must reset the process-wide default
os.chdir("/tenants/b")
reset_default_resolver()                 # clear the shared default
resolver_b = ToolResolver()              # binds to /tenants/b/tools.py
tools_b = resolver_b.get_local_callables()
If you use the module-level helpers (resolve(), resolve_many(), list_available(), etc.) they share a single process-default ToolResolver. Call reset_default_resolver() between tenants or after os.chdir() — otherwise the first caller’s tools.py will be served to everyone.

When to call reset_default_resolver()

SituationCall it?
Single-tenant CLI❌ No
Multi-tenant gateway switching CWDs per request✅ Yes, before each tenant
Long-running server with hot-reload of tools✅ Yes, after tools change
Test setup/teardown✅ Yes, in a fixture
You always pass an explicit tools_py_path❌ No

Two Flavours of tools.py

What’s in tools.pyMethodReturned shape
Plain Python functionsget_local_callables()List[Callable]
praisonai_tools.BaseTool / praisonai.tools.BaseTool / langchain_community.tools.* classesget_local_tool_classes()Dict[str, ToolInstance] (instantiated)
A directory of *.py files (each may contain BaseTool subclasses)get_local_tool_classes_from_dir(tools_dir)Dict[str, ToolInstance] (merged across files)
Example tools.py with functions:
# tools.py - Plain functions
def calculate_sum(a: int, b: int) -> int:
    """Add two numbers together."""
    return a + b

def get_weather(city: str) -> str:
    """Get weather for a city."""
    return f"Weather in {city}: sunny"
Example tools.py with BaseTool classes:
# tools.py - BaseTool classes
from praisonai_tools import BaseTool

class CalculatorTool(BaseTool):
    name = "calculator"
    description = "Perform basic math operations"
    
    def _run(self, operation: str) -> str:
        # Implementation here
        return "42"

class WeatherTool(BaseTool):
    name = "weather"
    description = "Get weather information"
    
    def _run(self, city: str) -> str:
        return f"Weather in {city}: sunny"

Configuration Options

ParameterTypeDefaultDescription
tools_py_pathOptional[str]"tools.py"Path to tools.py file to load
# Load from non-default path
resolver = ToolResolver(tools_py_path="/abs/path/to/my_tools.py")
The tools/ directory case takes an explicit tools_dir argument and is not bound to the constructor’s tools_py_path.

Common Patterns

from praisonai.tool_resolver import ToolResolver

# Load from specific file
resolver = ToolResolver(tools_py_path="/project/utils/custom_tools.py")
callables = resolver.get_local_callables()

Security

Security enforcement is handled by _safe_loader.load_user_module:
  • Environment gate: Requires PRAISONAI_ALLOW_LOCAL_TOOLS=true
  • CWD constraint: Refuses paths outside current working directory
  • Path traversal protection: Prevents ../ style attacks
See Security Environment Variables for details.
# These will be refused even with PRAISONAI_ALLOW_LOCAL_TOOLS=true
resolver = ToolResolver(tools_py_path="../outside_cwd/tools.py")  # ❌
resolver = ToolResolver(tools_py_path="/tmp/tools.py")           # ❌

# This works when inside your project directory
resolver = ToolResolver(tools_py_path="./utils/tools.py")        # ✅

Per-Context Resolver (Multi-Project Safety)

When you call resolve_tool(name) without passing a resolver, PraisonAI now uses a context-local default resolver instead of a single process-wide singleton. Each agent/task/request anchors to its own working directory.
from praisonai.tool_resolver import resolve_tool, reset_default_resolver
import os

# Agent A in /project_a — resolver anchors to /project_a/tools.py
os.chdir("/project_a")
tool_a = resolve_tool("my_tool")

# Agent B in /project_b in a different context — its resolver anchors to /project_b/tools.py
os.chdir("/project_b")
tool_b = resolve_tool("my_tool")  # picks up /project_b/tools.py, not /project_a/tools.py

# Long-lived daemons switching projects can explicitly reset:
reset_default_resolver()

reset_default_resolver()

When to callDaemons or IDE plugins that switch the working directory between projects
What it doesClears the cached ToolResolver in the current context so the next call re-anchors to the new CWD
Importfrom praisonai.tool_resolver import reset_default_resolver

Caching Behaviour

  • Per-context cache: Each context caches its own tools.py content
  • First call: Loads and caches tools.py content
The ToolResolver maintains two separate caches for performance: Local tools.py cache:
  • First call: Loads and caches tools.py content
  • Subsequent calls: Returns cached immutable view (MappingProxyType)
  • Thread safety: Uses _local_tools_lock for concurrent access
Resolve cache:
  • Per-tool caching: Memoises resolve(name) results for each tool name
  • Negative results: Unknown tool names are cached too, so repeated lookups don’t walk every source
  • Thread safety: Uses _resolve_cache_lock for concurrent access
resolver = ToolResolver()

# First resolve() walks the 5-source ladder and caches result
tool1 = resolver.resolve("tavily_search")  # Loads from source

# Second resolve() returns cached result immediately  
tool2 = resolver.resolve("tavily_search")  # Uses cache

# Clear both caches
resolver.clear_cache()
tool3 = resolver.resolve("tavily_search")  # Loads from source again

Resetting the Default Resolver

Call reset_default_resolver() to clear the context-local resolver cache. Useful between tenants, on CWD change, or in test setup so that local tools.py resolution is not affected by previous calls.
import os
from praisonai.tool_resolver import reset_default_resolver

# Switching tenants / projects
os.chdir("/path/to/tenant_b")
reset_default_resolver()  # next ToolResolver() reload picks up the new CWD
This clears the context-local resolver cache, forcing the next tool resolution to create a fresh resolver anchored to the current working directory. clear_cache() now clears both caches — useful after editing tools.py and after registering new tools in the wrapper ToolRegistry at runtime.

Best Practices

Place tools.py in your current working directory. Paths outside CWD are refused even with the environment variable set. This prevents path traversal attacks from HTTP API callers.
# Good structure
project/
├── agents.yaml
├── tools.py          # ✅ In CWD
└── utils/
    └── helpers.py

# Bad structure  
project/
├── agents.yaml
└── ../tools/
    └── tools.py      # ❌ Outside CWD
Use plain Python functions for praisonaiagents agents. Reserve BaseTool classes for crewai-style flows or when you need complex tool state management.
# Simple and effective
def process_data(data: str) -> str:
    """Process some data."""
    return data.upper()

# Only when you need complex behavior
class DataProcessorTool(BaseTool):
    name = "data_processor"
    
    def __init__(self):
        self.state = {}  # Complex state management
Don’t import ToolResolver from praisonaiagents — it lives in the wrapper at praisonai.tool_resolver. The wrapper handles YAML-based tool resolution.
# Correct
from praisonai.tool_resolver import ToolResolver

# Incorrect - won't work
from praisonaiagents.tool_resolver import ToolResolver  # ❌
Set PRAISONAI_ALLOW_LOCAL_TOOLS=true only in development or trusted deployment environments. This prevents arbitrary code execution from untrusted working directories.
# Development
export PRAISONAI_ALLOW_LOCAL_TOOLS=true

# Production - keep disabled unless absolutely necessary
# unset PRAISONAI_ALLOW_LOCAL_TOOLS

ToolRegistry API Reference

Complete API reference for ToolRegistry

Security Environment Variables

Environment variable security controls

Tools

General tools documentation