Project-Local Tools

Drop a Python file in .praisonai/tools/ and praisonai run picks it up — zero config

Drop a Python file in .praisonai/tools/ and praisonai run auto-loads every public function as a tool — no --tools flag.

python
from praisonaiagents import Agent

agent = Agent(
    name="Greeter",
    instructions="Greet users by name.",
    tools=["greet.greet"],
)
agent.start("Greet Ada")

The tool name uses the namespaced form — <module>.<function> — so greet.py::greet becomes greet.greet.

Diagram

Project-Local Tools

📄 .praisonai/tools/*.py

🔍 discovery

🤖 agent

Quick Start

  1. Scaffold the convention

    praisonai init writes a commented @tool stub at .praisonai/tools/example.py:

    python
    # .praisonai/tools/example.py
    """Project-local tools for this .praisonai/ project.
    
    Every public callable in this directory is auto-discovered and made available
    to the agent on `praisonai run` — no --tools flag required.
    """
    
    # from praisonaiagents import tool
    #
    #
    # @tool
    # def greet(name: str) -> str:
    #     """Return a friendly greeting for the given name."""
    #     return f"Hello, {name}!"
  2. Drop a plain function and run

    Add a plain function at .praisonai/tools/greet.py:

    python
    # .praisonai/tools/greet.py
    def greet(name: str) -> str:
        """Return a friendly greeting for the given name."""
        return f"Hello, {name}!"

    Run — no --tools flag needed, just the opt-in:

    bash
    # Either — per-invocation flag (recommended)
    praisonai run --allow-local-tools "use the greet tool to greet Ada"
    # Or — shell-scoped env var
    PRAISONAI_ALLOW_LOCAL_TOOLS=true praisonai run "use the greet tool to greet Ada"
  3. Promote to @tool for a rich schema

    Decorate a function with @tool when you want a typed schema for the LLM:

    python
    # .praisonai/tools/math.py
    from praisonaiagents import tool
    
    @tool
    def add(a: int, b: int) -> int:
        """Add two numbers."""
        return a + b

    Run it with the opt-in as a first-class flag:

    bash
    # Either
    praisonai run --allow-local-tools "use the add tool: 2+3"
    # Or
    PRAISONAI_ALLOW_LOCAL_TOOLS=true praisonai run "use the add tool: 2+3"

How It Works

praisonai run walks .praisonai/tools/, loads each module behind the security gate, and hands the callables to the agent.

Diagram
Agent_safe_loader (env gate)CustomDefinitionsDiscoverypraisonai runUserAgent_safe_loader (env gate)CustomDefinitionsDiscoverypraisonai runUserprints hint naming files found +how to enable (env var / –allow-local-tools)alt[PRAISONAI_ALLOW_LOCAL_TOOLS=true (or –allow-local-tools)][opt-in unset]praisonai run “…”discover_project_tools()load_user_module(path)module → tool callables[namespaced tools]tools += discoveredskip (no exec)

Discovery Rules

The rules below come straight from the SDK’s CustomDefinitionsDiscovery and _load_tools.

RuleBehaviour
Location.praisonai/tools/*.py (user-global ~/.praisonai/tools/ + project walk-up to git root)
PrecedenceLater wins on collision (user-global < project; nested closer to CWD < nested further)
Naming (via praisonai run auto-discovery)<module-filename>.<function-name> (e.g. greet.py::greet → greet.greet)
Naming (via YAML agents.yaml)Flat tool name — @tool(name=...) if given, else the function __name__ (matches how YAML tools: entries are written)
Public onlyCallables whose name doesn’t start with _
Module filterFiles starting with _ (e.g. _helpers.py) are skipped
Mixed file ruleIf any function in the file is @tool-decorated, only decorated functions are exported
Security gatePRAISONAI_ALLOW_LOCAL_TOOLS=true (or --allow-local-tools) required — gate unset → empty discovery + a one-line hint naming the tool files found and how to enable them
Interaction with --tools / YAML tools:Additive; explicit --tools items and YAML tools: names both compose with .praisonai/tools/ discovery, dedup’d by callable identity. YAML path uses the raw tool name (@tool(name=…) or __name__); direct praisonai run uses the module-namespaced form.
Interaction with --agentSame behaviour as default run — frontmatter tools: list is unioned with --tools/--toolset and auto-discovered .praisonai/tools/*.py, dedup’d by callable identity. Fixed in #3047.
Opt-out--no-tools on praisonai run skips discovery entirely

YAML Agents (agents.yaml)

YAML-defined agents pick up @tool functions dropped into .praisonai/tools/*.py by their flat tool name — no tools: entry required.

Start with an agent that names no tools in YAML:

yaml
# agents.yaml
framework: praisonai
topic: Weather
roles:
  reporter:
    role: Weather reporter
    goal: Report the weather for a city
    backstory: A helpful assistant.
    tasks:
      report:
        description: Report the weather for London.
        expected_output: A one-line weather summary.

Drop a @tool function beside it:

python
# .praisonai/tools/weather.py
from praisonaiagents import tool

@tool
def weather(city: str) -> str:
    """Return the current weather for a city."""
    return f"sunny in {city}"

Run with the opt-in — the agent sees the tool as weather:

bash
praisonai run --allow-local-tools agents.yaml

Naming the tool explicitly

Pass name= to @tool to control the flat name the agent resolves:

python
# .praisonai/tools/weather.py
from praisonaiagents import tool

@tool(name="weather_lookup")
def weather(city: str) -> str:
    """Return the current weather for a city."""
    return f"sunny in {city}"

The agent now sees the tool as weather_lookup. If your agents.yaml lists tools explicitly, reference it by that flat name.

Additive with YAML-named tools

A YAML-named built-in and a .praisonai/tools/ @tool function resolve together on the same agent:

yaml
# agents.yaml
framework: praisonai
topic: Weather
roles:
  reporter:
    role: Weather reporter
    goal: Report the weather for a city
    backstory: A helpful assistant.
    tools: [duckduckgo]
    tasks:
      report:
        description: Report the weather for London.
        expected_output: A one-line weather summary.
python
# .praisonai/tools/weather.py
from praisonaiagents import tool

@tool(name="weather_lookup")
def weather(city: str) -> str:
    """Return the current weather for a city."""
    return f"sunny in {city}"

duckduckgo still resolves from the built-in tools, and weather_lookup is added on top from the discovered file.

How tools merge on the YAML path

ToolResolver.resolve_all_from_yaml resolves YAML-named built-ins, layers cwd tools.py / tools/ classes, then additively merges the discovered .praisonai/tools/*.py @tool functions — all gated by the same opt-in.

Diagram

YAML Tool Resolution

📄 agents.yaml
tools: [duckduckgo]

🔍 ToolResolver.resolve_all_from_yaml

📄 cwd tools.py / tools/
BaseTool classes

📄 .praisonai/tools/*.py
@tool functions

🤖 Agent


Enabling on run

Two equivalent ways to opt in for a praisonai run invocation.

–allow-local-tools (per-invocation)

Discoverable via praisonai run --help. Scoped strictly to the single invocation.

bash
praisonai run --allow-local-tools "use the greet tool to greet Ada"

PRAISONAI_ALLOW_LOCAL_TOOLS=true (shell scope)

Persists for the shell session. Use when several back-to-back runs need it.

bash
export PRAISONAI_ALLOW_LOCAL_TOOLS=true
praisonai run "use the greet tool to greet Ada"

Accepts only true (case-insensitive). Values like 1, yes, on, false, 0 are not truthy for this variable.

Discovery hint when the opt-in is unset

When .praisonai/tools/.py (or ~/.praisonai/tools/.py) files are present but the opt-in is unset, praisonai run prints a one-line hint on the default path (not just --verbose):

Found 3 local tool files in .praisonai/tools/ and ~/.praisonai/tools/ but local tools are disabled. Enable with PRAISONAI_ALLOW_LOCAL_TOOLS=true or --allow-local-tools.

The location(s) named in the hint reflect where files were actually found — .praisonai/tools/, ~/.praisonai/tools/, or both. The hint is silent when there are no local tool files.

Enabling on run --agent

run --agent <name> auto-discovers .praisonai/tools/*.py too — the frontmatter tools: list, --tools/--toolset, and discovered tools all merge, dedup’d by callable identity.

bash
# Frontmatter tools + --tools flag + .praisonai/tools/ all merged
praisonai run --agent researcher --tools github --allow-local-tools "..."

The same PRAISONAI_ALLOW_LOCAL_TOOLS=true / --allow-local-tools opt-in applies — the security gate is identical to default run. Fixed in #3047.


Using from YAML agents (agents.yaml)

YAML-defined agents also pick up @tool functions from .praisonai/tools/*.py — reference them by tool name from any tools: list.

python
# .praisonai/tools/math.py
from praisonaiagents import tool

@tool
def add(a: int, b: int) -> int:
    """Add two numbers."""
    return a + b
yaml
# agents.yaml
roles:
  mathematician:
    role: Mathematician
    goal: Do arithmetic on request.
    backstory: A calm arithmetician.
    tools:
      - add          # resolved from .praisonai/tools/math.py
      - duckduckgo   # resolved from the built-in chain
    tasks:
      t1:
        description: What is 2 + 3?
        expected_output: The answer.

Run it with the same opt-in that gates praisonai run:

bash
PRAISONAI_ALLOW_LOCAL_TOOLS=true praisonai --agent-file agents.yaml

Naming rule: the tool key is @tool(name="…") if given, else the function __name__ — not the module-namespaced form (math.add) that direct praisonai run uses. The two paths differ only in the key.

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

Fixed in #3107.


@tool-decorated vs Plain Callable

Both a plain function and a @tool-decorated function become tools — but if a file has any @tool, only the decorated functions win.

Plain function

python
# .praisonai/tools/greet.py
def greet(name: str) -> str:
    """Return a friendly greeting for the given name."""
    return f"Hello, {name}!"

Exposed as greet.greet.

@tool-decorated

python
# .praisonai/tools/math.py
from praisonaiagents import tool

@tool
def add(a: int, b: int) -> int:
    """Add two numbers."""
    return a + b

def _round(x: float) -> int:
    """Private helper — never exported."""
    return round(x)

Exposed as math.add. The plain _round helper is skipped (leading underscore), and if any non-underscore plain helper existed it would be dropped too because add is @tool-decorated.


User-Global vs Project-Local

Two locations feed discovery, and they differ on the working-directory boundary.


Best Practices

Keep private helpers underscore-prefixed

Prefix helper functions with _ so they stay internal. Underscore names are never exported, and modules whose filename starts with _ are skipped entirely.

Use @tool when you want a rich schema

Decorate with @tool from praisonaiagents to give the LLM a typed schema. In a mixed file, only the @tool functions are exported — plain helpers are dropped.

Prefer –allow-local-tools over exporting the env var

Loading a tool module executes its code. Prefer --allow-local-tools on the individual praisonai run invocation over exporting PRAISONAI_ALLOW_LOCAL_TOOLS=true in your shell — the CLI flag is scoped to the single run and cannot leak to a later in-process invocation. Export the env var only when many back-to-back runs need the opt-in in a trusted shell.

Use –no-tools for one-off deterministic runs

Pass --no-tools to praisonai run to skip auto-discovery entirely when you want a run with no local tools loaded.


Related