Tool Resolver

Single source of truth for loading tools.py — functions and BaseTool classes alike

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

python
import os
from praisonaiagents import Agent

os.environ["PRAISONAI_ALLOW_LOCAL_TOOLS"] = "true"

agent = Agent(
    name="Tool User",
    instructions="Use available tools to help the user",
)
agent.start("Calculate something using my tools")

The user asks for a calculation; ToolResolver loads callables and BaseTool classes from tools.py for the agent.

Diagram

Tool Resolution Flow

📄 tools.py

🔧 ToolResolver

🎯 get_local_callables

🏗️ get_local_tool_classes

🤖 Agent

🤖 Agent

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:

    bash
    export PRAISONAI_ALLOW_LOCAL_TOOLS=true
    python
    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:

    python
    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:

Diagram

Found

Not found

Found

Not found

Found

Not found

Found

Not found

Found

Not found

🔍 Tool Search

Unsupported markdown: list

✅ Return tool

Unsupported markdown: list

✅ Return tool

Unsupported markdown: list

✅ Return tool

Unsupported markdown: list

✅ Return tool

Unsupported markdown: list

✅ Return tool

❌ Tool not found

CLI, YAML, recipes, templates, and Python all share this resolution chain. See CLI Reference for --tools usage.


Registering Tools at Runtime

Register custom tools through the ToolRegistry for YAML pipeline access:

python
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:

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:

python
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

Diagram
ModuleSafeLoaderToolResolverUserModuleSafeLoaderToolResolverUserget_local_callables()load_user_moduleCheck PRAISONAI_ALLOW_LOCAL_TOOLSCheck CWD path constraintLoad tools.pyModule objectModule objectExtract callables/classesCache as MappingProxyTypeList[Callable] or Dict[str, ToolInstance]

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.

CLI and recipes use the same resolver (PR #1857, PR #2059, PR #2499, PR #2935)

praisonai --tools tavily_search,my_tool "..." goes through ToolResolver.resolve(name, instantiate=True), identical to the YAML path. Recipe and template tool loading via resolve_tools() also delegates to ToolResolver (PR #2059), so wrapper ToolRegistry registrations, praisonai-tools package tools, and core SDK plugin tools are reachable from recipes and templates — not just the agent build path. As of PR #2499 the same chain also handles --rewrite-tools (query-rewrite), --expand-tools (prompt-expansion), and research --tools. As of PR #2935, the file-path branches of the same four flags also route through ToolResolver.load_functions_from_module (previously each site hand-rolled _safe_loader.load_user_module + inspect.getmembers). As of PraisonAI#3122 the wrapper no longer re-implements praisonai-tools package discovery — that source is now owned exclusively by ToolResolver._resolve_from_praisonai_tools, keeping a single implementation for external-tool discovery. The wrapper still builds its unique sources (default dirs, TEMPLATE.yaml tools_sources, --tools/--tools-dir, and the PRAISONAI_ALLOW_TEMPLATE_TOOLS-gated tools.py autoload) and hands them to the resolver as a registry.

Reachable from the CLI (default, YAML, and Python surfaces). The --tools flag on default run, YAML tools: lists, and Agent(tools=[...]) in Python all resolve names through this same chain. As of PR #2681, run --output actions also honours --tools/--toolset — previously they were dropped in actions mode. A name unknown to every source prints Warning: Unknown tool '<name>' and is skipped. See Run and the CLI reference.


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.

python
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()

Once you have your per-tenant resolver, the _safe_loader will not leak module state between tenants either — even the process-global sys.modules slot is per-load-unique, so two tenants loading their own tools.py never share a module object. See Concurrent loads stay isolated.

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
Explicit resolver injection into AgentsGenerator(tool_resolver=...)❌ Not needed — you already own the instance
Diagram
Tenant BProcessTenant ATenant BProcessTenant Apath bound to /tenants/a/tools.pychdir(/tenants/a) + ToolResolver()resolve(“my_tool”) → /tenants/a/tools.pychdir(/tenants/b)resolve(“my_tool”) ❌ still /tenants/areset_default_resolver()resolve(“my_tool”) ✅ /tenants/b/tools.py

Sharing one resolver across the run

Tool discovery — scanning tools.py, praisonaiagents.tools, praisonai-tools, wrapper ToolRegistry registrations, and the core plugin registry — is not cheap. PraisonAI reuses one resolver per context (per agent / per request / per CLI run) so this discovery happens once, not once per generator construction.

python
from praisonai.tool_resolver import _get_default_resolver

resolver = _get_default_resolver()   # cached in a ContextVar
tools_dict = resolver.list_available()

Under the hood, _get_default_resolver() is backed by a contextvars.ContextVar. Every code path that used to build a fresh ToolResolver() now flows through this helper by default:

CallerBehaviour
BaseAutoGenerator._get_tool_resolver() — used by AutoGenerator, WorkflowAutoGenerator, JobWorkflowAutoGenerator for the --auto planner promptReuses the context-local resolver.
AgentsGenerator.__init__(...) (no tool_resolver= passed)Reuses the context-local resolver.
AgentsGenerator.__init__(..., tool_resolver=my_resolver)Uses your explicit resolver — for tests or multi-tenant runtimes.
Module-level helpers resolve(), resolve_many(), list_available(), has_tool(), validate_yaml_tools()Already used the context-local resolver — unchanged.

Passing an explicit resolver (DI)

For tests or multi-tenant code, pass your own resolver into AgentsGenerator:

python
from praisonai.agents_generator import AgentsGenerator
from praisonai.tool_resolver import ToolResolver

my_resolver = ToolResolver(tools_py_path="tools.py")

gen = AgentsGenerator(
    agent_file="agents.yaml",
    framework="praisonai",
    config_list=[{"model": "gpt-4o", "api_key": "sk-..."}],
    tool_resolver=my_resolver,   # ← new in PR #3067
)

This composes with the existing adapter_registry= and tool_timeout_executor= DI parameters and works identically for the sync (generate_crew_and_kickoff) and async (agenerate_crew_and_kickoff) paths.

The resolve_tools() helper used by recipes and templates has its own small memoisation layer on top of this — a bounded LRU keyed on the registry object so per-agent calls in a workflow build share one resolver. See Tools Resolve → Resolver memoisation.


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)
Diagram

Yes

No

Yes

Both

Which method?

Does tools.py export
plain functions?

get_local_callables()
→ List[Callable]

Does it export
BaseTool subclasses?

get_local_tool_classes()
→ Dict[str, ToolInstance]

Call both methods

If you need to resolve class tools by name (e.g. from a YAML tools: list), call resolver.resolve("ToolClassName", instantiate=True) — see Common Patterns → Resolving Class Tools below.

Example tools.py with functions:

python
# 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:

python
# 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"

High-Level Loading Methods

For embedders and advanced users, ToolResolver exposes four convenience methods that combine resolution, instantiation, and security gating in a single call.

Resolve everything in a YAML config

python
import yaml
from praisonai.tool_resolver import ToolResolver

with open("agents.yaml") as f:
    config = yaml.safe_load(f)

resolver = ToolResolver()
tools_dict = resolver.resolve_all_from_yaml(config)
# {"my_search": <function>, "CalculatorTool": <instance>, ...}

Load functions from a module

python
# Simple: accept every function or callable, including private names
funcs = resolver.load_functions_from_module("./helpers/my_tools.py")
# {"calculate_sum": <function>, "get_weather": <function>}

# CLI-style: only public plain functions (what --tools file.py uses)
funcs = resolver.load_functions_from_module(
    "./helpers/my_tools.py",
    functions_only=True,
    skip_private=True,
)

Load BaseTool classes from a module

python
classes = resolver.load_classes_from_module("./tools.py")
# {"CalculatorTool": <CalculatorTool>, "WeatherTool": <WeatherTool>}

Load functions from a package directory

python
funcs = resolver.load_functions_from_package("./tools_pkg")
# Recursively unions every *.py (except __*.py) in the directory.

All four methods enforce the PRAISONAI_ALLOW_LOCAL_TOOLS=true gate and CWD path constraints. See Security.

Diagram
LocalFilesCustomDefinitionsDiscoverySafeLoaderToolResolverUserLocalFilesCustomDefinitionsDiscoverySafeLoaderToolResolverUserloop[For eachtool name]short-circuit — no directory walk, no git subprocessalt[gate on][gate off (default)]resolve_all_from_yaml(config)Walk roles[*].tools + tasks[*].toolsresolve(name)cwd tools.py XOR tools/ classesclasses (or empty)gate: PRAISONAI_ALLOW_LOCAL_TOOLS?discover_project_tools() (.praisonai/tools/*.py)@tool callables keyed by tool name{name: callable} dict
MethodReturnsPurpose
resolve_all_from_yaml(yaml_config)Dict[str, Callable]One call returns every tool referenced in a parsed YAML config. Additively merges (a) cwd tools.py or cwd tools/ classes (mutual-XOR, unchanged) and (b) .praisonai/tools/*.py @tool-decorated functions keyed by their tool name — gated by PRAISONAI_ALLOW_LOCAL_TOOLS=true. See #3107. Also invoked by praisonai.scheduler.yaml_loader.create_agent_from_config, so a scheduled agents.yaml resolves tools identically to praisonai run (fixed in #3420).
load_functions_from_module(path, *, functions_only=False, skip_private=False, module_name="tools_module")Dict[str, Callable]Load callables from a single .py file. Pass functions_only=True to keep only inspect.isfunction members (drops callable class instances). Pass skip_private=True to drop members whose name starts with _. module_name names the module inside sys.modules so multiple simultaneous loads (e.g. --tools + --rewrite-tools) don’t shadow each other.
load_classes_from_module(path)Dict[str, Callable]Load and instantiate BaseTool subclasses and langchain_community.tools.* classes from a single .py file.
load_functions_from_package(path)Dict[str, Callable]Iterate .py in a directory (skipping __.py) and union the functions.
list_available_sources()Dict[str, str]Returns {name: source} where source is one of "local", "builtin", "external", "registered". Reflects the chain resolve() would actually use — used by praisonai tools list / tools info for authoritative source attribution.

validate_yaml_tools — same keys, same locals

validate_yaml_tools(yaml_config) returns the tool names a config references that nothing can resolve. It is what praisonai tools validate and the gateway pre-flight call. Since PR #5754 it walks the same YAML keys as resolve_all_from_yaml and accepts the same local tool sources, so a tool the build can resolve is never reported missing.

Diagram
tools.py / tools/ / .praisonai/tools/Resolution chainToolResolverUsertools.py / tools/ / .praisonai/tools/Resolution chainToolResolverUserWalk roles[*].tools + tasks[*].toolsloop[For each tool name]alt[any unresolved]validate_yaml_tools(config)has_tool(name)load local tool sourceslocal tool namesdrop local names from missingtruly unresolved names
YAML keyresolve_all_from_yaml (build)validate_yaml_tools
agents[].tools (or roles[].tools)✓✓
agents[].tasks[].tools✓✓ (since PR #5754)
agents[*].toolsets✗✓ (toolset names only)

Agent-level planning_tools and global tools / toolsets are not walked here — praisonai validate covers those. See Validate CLI.

Already have a loaded module? Use the module-level helper

load_functions_from_module(path, ...) takes a file path and loads the module itself. If you have already loaded a module object, call the sibling module-level extract_functions_from_loaded_module(module, ...) instead — it runs the same extraction walk without re-loading (re-loading produces a distinct object and breaks inspect.getmodule(obj) is my_module origin filters).

python
from praisonai_code._safe_loader import load_user_module
from praisonai_code.tool_resolver import extract_functions_from_loaded_module

module = load_user_module("tools.py", name="my_tools")
tools = extract_functions_from_loaded_module(module, functions_only=True, skip_private=True)

Already loaded a module? Use the module-level helper. Only have a path? Use ToolResolver.load_functions_from_module. See Module Tool Extraction for the full guide.

Naming key: YAML vs praisonai run

The key used to reference a .praisonai/tools/ callable differs between the two surfaces — this is the single most common source of “why doesn’t my tool show up?” confusion.

PathKey used to reference the callable
praisonai run (direct-prompt / --agent)<module>.<function> — e.g. math.add, greet.greet
YAML resolve_all_from_yaml (agents.yaml tools: list)Tool name only — @tool(name="…") or __name__ — e.g. add, weather_lookup

The YAML resolver keys everything by tool name so YAML lists stay flat, so a @tool(name="weather_lookup") is referenceable as weather_lookup, not weather.weather_lookup. Fixed in #3107.


Configuration Options

ParameterWhereTypeDefaultDescription
tools_py_pathToolResolver(...) constructorOptional[str]"tools.py"Path to the tools.py file to load (resolved eagerly against the current working directory).
instantiateresolver.resolve(name, ...)boolFalseWhen True, class tools (BaseTool subclasses, langchain_community.tools.* classes) are instantiated before being returned. Plain function tools are returned as-is. Safe with cache warm-up via has_tool() (since PR #1858).
source_registryToolResolver(...) constructorOptional[ToolSourceRegistry]Process-default (lazy, discovers praisonai.tool_sources entry points)Injects a registry supplying third-party tool sources. See Tool Source Registry. Pass ToolSourceRegistry(discover_entry_points=False) to opt out of third-party sources.
python
# Load from non-default path
resolver = ToolResolver(tools_py_path="/abs/path/to/my_tools.py")

Common Patterns

Loading from Custom Path

python
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()

Reloading After Edits

python
# In a long-lived process after editing tools.py
resolver.clear_cache()
updated_callables = resolver.get_local_callables()

Mixed Function and Class Tools

python
# Handle both types from the same tools.py
resolver = ToolResolver()
functions = resolver.get_local_callables()
tool_classes = resolver.get_local_tool_classes()

print(f"Functions: {[f.__name__ for f in functions]}")
print(f"Tool classes: {list(tool_classes.keys())}")

Loading from tools/ Directory

python
from praisonai.tool_resolver import ToolResolver

resolver = ToolResolver()
tools = resolver.get_local_tool_classes_from_dir("./tools")

print(f"Found {len(tools)} tools from directory")
print(f"Tool names: {list(tools.keys())}")

Resolving Class Tools

python
from praisonaiagents import Agent
from praisonai.tool_resolver import ToolResolver

# tools.py exports a BaseTool subclass (e.g. CalculatorTool)
resolver = ToolResolver()
calc = resolver.resolve("CalculatorTool", instantiate=True)

agent = Agent(
    name="Math Helper",
    instructions="Use the calculator to answer math questions",
    tools=[calc],
)

agent.start("What is 21 * 2?")

Local Tool Override Warning

When a class-based tool in your local tools/ directory shares a name with a tool already resolved from the resolution chain (wrapper registry, praisonaiagents.tools, etc.), resolve_all_from_yaml() logs a warning:

text
WARNING praisonai.tool_resolver: Local tool 'serper' overrides a tool already
resolved from the resolution chain

This warns you when a local definition silently wins over a built-in or registered tool of the same name. To resolve it, either rename your local tool or remove the conflicting source from the chain.


Hot-Reload: invalidate_local_tools_dir()

In long-running processes (file watchers, dev servers), the tools/ directory class scan is cached per-resolver. After editing tools/*.py, trigger a re-scan:

python
from praisonai.tool_resolver import ToolResolver

resolver = ToolResolver()

# ... user edits tools/my_tool.py ...
resolver.invalidate_local_tools_dir()
# next resolve_all_from_yaml() re-scans the directory

clear_cache() also clears the directory cache alongside the resolve cache.


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.

python
# 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.

python
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
  • Cacheable failures: A tool genuinely absent from every source (local tools.py, praisonaiagents.tools, praisonai-tools, registry) is cached as None so repeated lookups don’t walk the ladder again
  • Transient failures are NOT cached: If praisonaiagents fails to import (ImportError) or an entry in TOOL_MAPPINGS exists but its optional dependency failed to load, the lookup is retried on the next call. Install the missing package and the next resolve() picks it up — no clear_cache() needed
  • invalidate(name=None): Clear one tool or the entire resolve cache. ToolRegistry.set_resolver() appends to a weak-ref list (PR #2122) — all registered resolvers are notified on register_function() / clear(). Resolvers held only by ToolRegistry are GC’d; dead refs are cleaned lazily on each invalidation. Multi-tenant gateways can wire one resolver per tenant without overwriting each other.
  • Thread safety: Uses _resolve_cache_lock for concurrent access. Fixed in PR #2147 — an earlier lock-free fast-path read could race with ToolRegistry.invalidate() and miss freshly-registered tools; the cache lookup now happens inside the lock.
Diagram

Yes

No

ImportError or optional dep missing

Genuinely absent

resolve name

Tool found?

Cache instance

Transient failure?

Do not cache — retry next call

Cache None

python
resolver = ToolResolver()

# First resolve() walks sources and caches result
tool1 = resolver.resolve("tavily_search")

# Invalidate one tool after registry change
resolver.invalidate("tavily_search")
tool2 = resolver.resolve("tavily_search")  # Re-resolves from sources

# Or clear everything
resolver.invalidate()

ToolSource protocol

ToolSource is a supported extensibility hook — pass custom sources via ToolResolver(sources=[...]) to fully control resolution order.

resolve() walks self._sources when custom sources are provided. Each source’s lookup(name) is called in order; the first non-None result wins. A source that raises an exception does not poison the cache — allow_none_cache=False ensures the next call after a dependency installs can still resolve the tool.

python
from praisonai.tool_resolver import ToolResolver, ToolSource

class MyToolSource:
    name = "my_tools"
    def lookup(self, name: str):
        if name == "my_custom_tool":
            return my_custom_tool
        return None

resolver = ToolResolver(sources=[MyToolSource()])
tool = resolver.resolve("my_custom_tool")

Entry-point tool sources

Third-party packages can register ToolSource implementations via the praisonai.tool_sources entry-point group — ToolResolver appends them after the default 5-step chain. See Tool Source Registry for the full pattern, plugin authoring, and opt-out.

python
from praisonai.tool_resolver import ToolResolver, ToolSourceRegistry

# Opt out of installed third-party tool sources
resolver = ToolResolver(
    source_registry=ToolSourceRegistry(discover_entry_points=False),
)

The built-in chain (local tools.py → wrapper registry → praisonaiagents → praisonai-tools → core registry) is owned by ToolResolver.default_sources() and is unaffected by the registry — entry-point sources are always appended, never inserted, so a third-party plugin cannot silently shadow a first-party tool.

Composing custom and default sources:

The default_sources(registry=None) method returns the historical 5-step resolution chain as a list of ToolSource objects (local-tools.py → wrapper-registry → praisonaiagents → praisonai-tools → core-registry). Prepend your custom source to search it first while keeping all built-in sources:

python
from praisonai.tool_resolver import ToolResolver
from praisonai.tool_registry import ToolRegistry

registry = ToolRegistry()
resolver = ToolResolver(
    registry=registry,
    sources=[MyToolSource(), *ToolResolver(registry=registry).default_sources(registry)],
)

Auto-wiring with registry=:

Passing registry= to ToolResolver(...) automatically calls registry.set_resolver(self). This means register_function() on the registry will automatically invalidate the resolver’s cache — no manual set_resolver() call needed:

python
from praisonai.tool_resolver import ToolResolver
from praisonai.tool_registry import ToolRegistry

registry = ToolRegistry()
resolver = ToolResolver(registry=registry)  # auto-wires: registry.set_resolver(resolver)

registry.register_function("new_tool", my_tool)  # automatically invalidates resolver cache
tool = resolver.resolve("new_tool")              # re-resolves from sources

Class tools and the cache: When tools.py exports BaseTool subclasses (rather than plain functions), pass instantiate=True to get a ready-to-use instance back. The cache returns the same instantiation path whether or not has_tool() or validate_yaml_tools() warmed the cache first — so YAML validation flows that check tools exist before resolving them no longer return uninstantiated classes (fixed in PR #1858).

python
from praisonai.tool_resolver import ToolResolver

resolver = ToolResolver()

# Warming the cache via has_tool() first is safe — the next resolve(instantiate=True)
# will still hand back an instance, not the raw class.
if resolver.has_tool("CalculatorTool"):
    calc = resolver.resolve("CalculatorTool", instantiate=True)
    # calc is an instance of CalculatorTool, ready to run

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.

python
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

Keep tools.py in your CWD

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.

bash
# Good structure
project/
├── agents.yaml
├── tools.py          # ✅ In CWD
└── utils/
    └── helpers.py

# Bad structure  
project/
├── agents.yaml
└── ../tools/
    └── tools.py      # ❌ Outside CWD
Prefer functions for praisonaiagents

Use plain Python functions for praisonaiagents agents. Reserve BaseTool classes for crewai-style flows or when you need complex tool state management.

python
# 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
Import from the wrapper package

Don’t import ToolResolver from praisonaiagents — it lives in the wrapper at praisonai.tool_resolver. The wrapper handles YAML-based tool resolution.

python
# Correct
from praisonai.tool_resolver import ToolResolver

# Incorrect - won't work
from praisonaiagents.tool_resolver import ToolResolver  # ❌
Set environment variable only in trusted environments

Set PRAISONAI_ALLOW_LOCAL_TOOLS=true only in development or trusted deployment environments. This prevents arbitrary code execution from untrusted working directories.

bash
# Development
export PRAISONAI_ALLOW_LOCAL_TOOLS=true

# Production - keep disabled unless absolutely necessary
# unset PRAISONAI_ALLOW_LOCAL_TOOLS

Related