> ## Documentation Index
> Fetch the complete documentation index at: https://docs.praison.ai/llms.txt
> Use this file to discover all available pages before exploring further.

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

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    subgraph "Tool Resolution Flow"
        TP[📄 tools.py] --> TR[🔧 ToolResolver]
        TR --> GC[🎯 get_local_callables]
        TR --> GTC[🏗️ get_local_tool_classes]
        GC --> Agent1[🤖 Agent]
        GTC --> Agent2[🤖 Agent]
    end
    
    classDef file fill:#6366F1,stroke:#7C90A0,color:#fff
    classDef resolver fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef methods fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef agent fill:#10B981,stroke:#7C90A0,color:#fff
    
    class TP file
    class TR resolver
    class GC,GTC methods
    class Agent1,Agent2 agent
```

## Quick Start

<Steps>
  <Step title="Basic Usage">
    Drop a `tools.py` next to your YAML/script, set the environment variable, and the resolver picks both kinds up automatically:

    ```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    export PRAISONAI_ALLOW_LOCAL_TOOLS=true
    ```

    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    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")
    ```
  </Step>

  <Step title="Direct Python Usage">
    When embedding PraisonAI in your own Python code:

    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    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")
    ```
  </Step>
</Steps>

***

## Resolution Order

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

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph TB
    Search[🔍 Tool Search] --> Step1{1. Local tools.py}
    Step1 -->|Found| Success1[✅ Return tool]
    Step1 -->|Not found| Step2{2. praisonaiagents.tools}
    Step2 -->|Found| Success2[✅ Return tool]
    Step2 -->|Not found| Step3{3. praisonai-tools package}
    Step3 -->|Found| Success3[✅ Return tool] 
    Step3 -->|Not found| Step4{4. Tool registry}
    Step4 -->|Found| Success4[✅ Return tool]
    Step4 -->|Not found| NotFound[❌ Tool not found]
    
    classDef step fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef success fill:#10B981,stroke:#7C90A0,color:#fff
    classDef notfound fill:#8B0000,stroke:#7C90A0,color:#fff
    
    class Step1,Step2,Step3,Step4 step
    class Success1,Success2,Success3,Success4 success
    class NotFound notfound
```

***

## Registering Tools at Runtime

Register custom tools through the `ToolRegistry` for YAML pipeline access:

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
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 theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
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 theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
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

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sequenceDiagram
    participant User
    participant ToolResolver
    participant SafeLoader
    participant Module
    
    User->>ToolResolver: get_local_callables()
    ToolResolver->>SafeLoader: load_user_module
    SafeLoader->>SafeLoader: Check PRAISONAI_ALLOW_LOCAL_TOOLS
    SafeLoader->>SafeLoader: Check CWD path constraint
    SafeLoader->>Module: Load tools.py
    Module-->>SafeLoader: Module object
    SafeLoader-->>ToolResolver: Module object
    ToolResolver->>ToolResolver: Extract callables/classes
    ToolResolver->>ToolResolver: Cache as MappingProxyType
    ToolResolver-->>User: List[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.

***

## 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 theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
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()
```

<Warning>
  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.
</Warning>

### When to call `reset_default_resolver()`

| Situation                                       | Call 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                      |

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sequenceDiagram
    participant Tenant A
    participant Process
    participant Tenant B

    Tenant A->>Process: chdir(/tenants/a) + ToolResolver()
    Note over Process: path bound to /tenants/a/tools.py
    Tenant A->>Process: resolve("my_tool") → /tenants/a/tools.py

    Tenant B->>Process: chdir(/tenants/b)
    Tenant B->>Process: resolve("my_tool") ❌ still /tenants/a
    Tenant B->>Process: reset_default_resolver()
    Tenant B->>Process: resolve("my_tool") ✅ /tenants/b/tools.py
```

***

## Two Flavours of tools.py

| What's in `tools.py`                                                                            | Method                                       | Returned shape                                  |
| ----------------------------------------------------------------------------------------------- | -------------------------------------------- | ----------------------------------------------- |
| Plain Python functions                                                                          | `get_local_callables()`                      | `List[Callable]`                                |
| `praisonai_tools.BaseTool` / `praisonai.tools.BaseTool` / `langchain_community.tools.*` classes | `get_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) |

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph TB
    Start[Which method?] --> Q1{Does tools.py export<br/>plain functions?}
    Q1 -->|Yes| A1["get_local_callables()<br/>→ List[Callable]"]
    Q1 -->|No| Q2{Does it export<br/>BaseTool subclasses?}
    Q2 -->|Yes| A2["get_local_tool_classes()<br/>→ Dict[str, ToolInstance]"]
    Q2 -->|Both| A3["Call both methods"]
    
    classDef question fill:#6366F1,stroke:#7C90A0,color:#fff
    classDef answer fill:#10B981,stroke:#7C90A0,color:#fff
    
    class Start,Q1,Q2 question
    class A1,A2,A3 answer
```

**Example tools.py with functions:**

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
# 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 theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
# 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

| Parameter       | Type            | Default      | Description                   |
| --------------- | --------------- | ------------ | ----------------------------- |
| `tools_py_path` | `Optional[str]` | `"tools.py"` | Path to tools.py file to load |

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
# Load from non-default path
resolver = ToolResolver(tools_py_path="/abs/path/to/my_tools.py")
```

<Note>
  The `tools/` directory case takes an explicit `tools_dir` argument and is not bound to the constructor's `tools_py_path`.
</Note>

***

## Common Patterns

<Tabs>
  <Tab title="Loading from Custom Path">
    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    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()
    ```
  </Tab>

  <Tab title="Reloading After Edits">
    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    # In a long-lived process after editing tools.py
    resolver.clear_cache()
    updated_callables = resolver.get_local_callables()
    ```
  </Tab>

  <Tab title="Mixed Function and Class Tools">
    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    # 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())}")
    ```
  </Tab>

  <Tab title="Loading from tools/ Directory">
    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    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())}")
    ```
  </Tab>
</Tabs>

***

## 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](/docs/features/security-environment-variables#praisonai_allow_local_tools) for details.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
# 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 theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
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 call** | Daemons or IDE plugins that switch the working directory between projects                          |
| **What it does** | Clears the cached `ToolResolver` in the current context so the next call re-anchors to the new CWD |
| **Import**       | `from 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

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
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.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
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

<AccordionGroup>
  <Accordion title="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 theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    # Good structure
    project/
    ├── agents.yaml
    ├── tools.py          # ✅ In CWD
    └── utils/
        └── helpers.py

    # Bad structure  
    project/
    ├── agents.yaml
    └── ../tools/
        └── tools.py      # ❌ Outside CWD
    ```
  </Accordion>

  <Accordion title="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 theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    # 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
    ```
  </Accordion>

  <Accordion title="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 theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    # Correct
    from praisonai.tool_resolver import ToolResolver

    # Incorrect - won't work
    from praisonaiagents.tool_resolver import ToolResolver  # ❌
    ```
  </Accordion>

  <Accordion title="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 theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    # Development
    export PRAISONAI_ALLOW_LOCAL_TOOLS=true

    # Production - keep disabled unless absolutely necessary
    # unset PRAISONAI_ALLOW_LOCAL_TOOLS
    ```
  </Accordion>
</AccordionGroup>

***

## Related

<CardGroup cols={2}>
  <Card title="ToolRegistry API Reference" icon="code" href="/docs/sdk/reference/praisonai/classes/ToolRegistry">
    Complete API reference for ToolRegistry
  </Card>

  <Card title="Security Environment Variables" icon="shield-check" href="/docs/features/security-environment-variables">
    Environment variable security controls
  </Card>

  <Card title="Tools" icon="wrench" href="/docs/tools">
    General tools documentation
  </Card>
</CardGroup>
