Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,40 @@ agent["greeter"].send("Good Evening!") # Dictionary access is supported
)
```

### Function Tools

Register Python functions as tools directly in code — no MCP server or external file needed. Both sync and async functions are supported. The function name and docstring are used as the tool name and description by default, or you can override them with `name=` and `description=`.

**Per-agent tools (`@agent.tool`)** — scope a tool to a specific agent:

```python
@fast.agent(name="writer", instruction="You write things.")
async def writer(): ...

@writer.tool
def translate(text: str, language: str) -> str:
"""Translate text to the given language."""
return f"[{language}] {text}"

@writer.tool(name="summarize", description="Produce a one-line summary")
def summarize(text: str) -> str:
return f"Summary: {text[:80]}..."
```

**Global tools (`@fast.tool`)** — available to all agents that don't declare their own tools:

```python
@fast.tool
def get_weather(city: str) -> str:
"""Return the current weather for a city."""
return f"Sunny in {city}"

@fast.agent(name="assistant", instruction="You are helpful.")
# assistant gets get_weather (global @fast.tool)
```

Agents with `@agent.tool` or `function_tools=` only see their own tools — globals are not injected. Use `function_tools=[]` to explicitly opt out of globals with no tools.

### Multimodal Support

Add Resources to prompts using either the inbuilt `prompt-server` or MCP Types directly. Convenience class are made available to do so simply, for example:
Expand Down
35 changes: 35 additions & 0 deletions examples/function-tools/basic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""
Basic @fast.tool example.

Register Python functions as tools using the @fast.tool decorator.
Tools are automatically available to all agents.

Run with: uv run examples/function-tools/basic.py
"""

import asyncio

from fast_agent import FastAgent

fast = FastAgent("Function Tools Example")


@fast.tool
def get_weather(city: str) -> str:
"""Return the current weather for a city."""
return f"Currently sunny and 22°C in {city}"


@fast.tool(name="add", description="Add two numbers together")
def add_numbers(a: int, b: int) -> int:
return a + b


@fast.agent(instruction="You are a helpful assistant with access to tools.")
async def main() -> None:
async with fast.run() as agent:
await agent.interactive()


if __name__ == "__main__":
asyncio.run(main())
60 changes: 60 additions & 0 deletions examples/function-tools/scoping.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"""
@agent.tool scoping example.

Demonstrates how tools can be scoped to individual agents using
@agent_func.tool, and how @fast.tool broadcasts globally.

Run with: uv run examples/function-tools/scoping.py
"""

import asyncio

from fast_agent import FastAgent

fast = FastAgent("Tool Scoping Example")


@fast.agent(
name="writer",
instruction="You are a writing assistant with translation and summarization tools.",
default=True,
)
async def writer() -> None:
pass


@fast.agent(
name="analyst",
instruction="You analyse text. You can only count words.",
)
async def analyst() -> None:
pass


@writer.tool
def translate(text: str, language: str) -> str:
"""Translate text to the given language."""
return f"[{language}] {text}"


@writer.tool
def summarize(text: str) -> str:
"""Produce a one-line summary."""
return f"Summary: {text[:80]}..."


@analyst.tool(name="word_count", description="Count words in text")
def count_words(text: str) -> int:
"""Count the number of words in text."""
return len(text.split())


async def main() -> None:
async with fast.run() as agent:
# "writer" sees translate and summarize (its own @writer.tool tools)
# "analyst" sees only word_count (its own @analyst.tool tool)
await agent.interactive()


if __name__ == "__main__":
asyncio.run(main())
93 changes: 92 additions & 1 deletion src/fast_agent/core/direct_decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,23 @@
from collections.abc import Coroutine
from pathlib import Path
from typing import (
TYPE_CHECKING,
Any,
Awaitable,
Callable,
Literal,
ParamSpec,
Protocol,
TypeVar,
overload,
)

from mcp.client.session import ElicitationFnT
from pydantic import AnyUrl

if TYPE_CHECKING:
from fastmcp.tools import FunctionTool

from fast_agent.agents.agent_types import (
AgentConfig,
AgentType,
Expand Down Expand Up @@ -263,6 +268,48 @@ def decorator(func: Callable[P, Coroutine[Any, Any, R]]) -> Callable[P, Coroutin
for key, value in extra_kwargs.items():
setattr(func, f"_{key}", value)

@overload
def _agent_tool(fn: Callable[..., Any], /) -> Callable[..., Any]: ...

@overload
def _agent_tool(
*, name: str | None = None, description: str | None = None
) -> Callable[[Callable[..., Any]], Callable[..., Any]]: ...

def _agent_tool(
fn: Callable[..., Any] | None = None,
/,
*,
name: str | None = None,
description: str | None = None,
) -> Callable[..., Any] | Callable[[Callable[..., Any]], Callable[..., Any]]:
"""Register a tool scoped to this agent.

Supports bare and parameterized usage::

@my_agent.tool
def helper() -> str: ...

@my_agent.tool(name="add", description="Add two numbers")
def add(a: int, b: int) -> int: ...
"""

def _register(f: Callable[..., Any]) -> Callable[..., Any]:
if name is not None:
f._fast_tool_name = name # type: ignore[attr-defined]
if description is not None:
f._fast_tool_description = description # type: ignore[attr-defined]
if config.function_tools is None:
config.function_tools = []
config.function_tools.append(f)
return f

if fn is not None:
return _register(fn)
return _register

func.tool = _agent_tool # type: ignore[attr-defined]

return func

return decorator
Expand All @@ -280,8 +327,52 @@ class DecoratorMixin:
agent configurations.
"""

# Type hint for the agents dict (provided by host class)
# Type hints for attributes provided by host class
agents: dict[str, Any]
_registered_tools: "list[FunctionTool]"

@overload
def tool(self, func: Callable[..., Any], /) -> Callable[..., Any]: ...

@overload
def tool(
self,
*,
name: str | None = None,
description: str | None = None,
) -> Callable[[Callable[..., Any]], Callable[..., Any]]: ...

def tool(
self,
func: Callable[..., Any] | None = None,
/,
*,
name: str | None = None,
description: str | None = None,
) -> Callable[..., Any] | Callable[[Callable[..., Any]], Callable[..., Any]]:
"""Register a Python function as a tool available to agents.

Supports both bare and parameterized usage::

@fast.tool
def greet(name: str) -> str: ...

@fast.tool(name="add", description="Add two numbers")
def add_numbers(a: int, b: int) -> int: ...

Tools registered this way are available to all agents that do not
declare an explicit ``function_tools`` list.
"""
from fast_agent.tools.function_tool_loader import build_default_function_tool

def _register(fn: Callable[..., Any]) -> Callable[..., Any]:
tool = build_default_function_tool(fn, name=name, description=description)
self._registered_tools.append(tool)
return fn

if func is not None:
return _register(func)
return _register

def agent(
self,
Expand Down
27 changes: 24 additions & 3 deletions src/fast_agent/core/direct_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,27 @@ def _load_configured_function_tools(
return load_function_tools(tools_config, base_path)


def _resolve_function_tools_with_globals(
config: AgentConfig,
agent_data: Mapping[str, Any],
build_ctx: "AgentBuildContext",
) -> list[FunctionTool]:
"""Load per-agent function tools, falling back to global @fast.tool tools.

If the agent has explicit function_tools configured (including an empty list),
only those are used. Otherwise, globally registered tools from ``@fast.tool``
are provided.
"""
if config.function_tools is not None or agent_data.get("function_tools") is not None:
return _load_configured_function_tools(config, agent_data)

global_tools = getattr(build_ctx.app_instance, "_registered_tools", None)
if global_tools:
return list(global_tools)

return []


def _register_loaded_agent(
result_agents: AgentDict,
name: str,
Expand Down Expand Up @@ -226,7 +247,7 @@ def _build_agents_as_tools_inputs(
options = _build_agents_as_tools_options(agent_data)
return AgentsAsToolsBuildInputs(
config=config,
function_tools=_load_configured_function_tools(config, agent_data),
function_tools=_resolve_function_tools_with_globals(config, agent_data, build_ctx),
child_agents=_resolve_child_agents(
name,
child_names,
Expand Down Expand Up @@ -603,7 +624,7 @@ async def _create_basic_agent(
child_message_files=inputs.child_message_files,
)
else:
function_tools = _load_configured_function_tools(config, agent_data)
function_tools = _resolve_function_tools_with_globals(config, agent_data, build_ctx)
agent = _create_agent_with_ui_if_needed(
McpAgent,
config,
Expand Down Expand Up @@ -644,7 +665,7 @@ async def _create_smart_agent(
child_message_files=inputs.child_message_files,
)
else:
function_tools = _load_configured_function_tools(config, agent_data)
function_tools = _resolve_function_tools_with_globals(config, agent_data, build_ctx)

from fast_agent.agents.smart_agent import SmartAgent, SmartAgentWithUI

Expand Down
5 changes: 5 additions & 0 deletions src/fast_agent/core/fastagent.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@
from fast_agent.ui.usage_display import display_usage_report

if TYPE_CHECKING:
from fastmcp.tools import FunctionTool

from fast_agent.config import MCPServerSettings
from fast_agent.context import Context
from fast_agent.core.agent_card_loader import LoadedAgentCard
Expand Down Expand Up @@ -432,6 +434,8 @@ def __init__(

# Dictionary to store agent configurations from decorators
self.agents: dict[str, AgentCardData] = {}
# Global tool registry populated by @fast.tool decorator
self._registered_tools: list[FunctionTool] = []
# Tracking for AgentCard-loaded agents
self._agent_card_sources: dict[str, Path] = {}
self._agent_card_roots: dict[Path, set[str]] = {}
Expand Down Expand Up @@ -1491,6 +1495,7 @@ async def _instantiate_agent_instance(
app_override: AgentApp | None = None,
) -> AgentInstance:
async with runtime.instance_lock:
self.app._registered_tools = self._registered_tools # type: ignore[attr-defined]
agents_map = await create_agents_in_dependency_order(
self.app,
self.agents,
Expand Down
6 changes: 5 additions & 1 deletion src/fast_agent/tools/function_tool_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,11 @@ def load_function_tools(
for tool_spec in tools_config:
try:
if callable(tool_spec):
result.append(build_default_function_tool(tool_spec))
tool_name = getattr(tool_spec, "_fast_tool_name", None)
tool_desc = getattr(tool_spec, "_fast_tool_description", None)
result.append(
build_default_function_tool(tool_spec, name=tool_name, description=tool_desc)
)
elif isinstance(tool_spec, str):
result.append(
build_default_function_tool(load_function_from_spec(tool_spec, base_path))
Expand Down
Loading
Loading