Skip to content

Commit 5ccfdb7

Browse files
Feat/tool decorator (#744)
* add @fast.tool decorator for inline function tool registration Allows users to register Python functions as tools using @fast.tool (bare or parameterized with name/description). Global tools are available to all agents by default; agents with explicit function_tools only see those tools. Made-with: Cursor * remove root test script in favor of proper unit tests The demonstration script was committed to the repo root; the proper unit tests live in tests/unit/core/test_tool_decorator.py. Made-with: Cursor * Add detailed documentation for the @fast.tool decorator in README.md This update includes examples of registering Python functions as tools using the @fast.tool decorator, highlighting both synchronous and asynchronous support. It also explains the global availability of tools and how to restrict them to specific agents using the function_tools parameter. * Add examples for using @fast.tool decorator in README.md * Enhance documentation for agent-specific tools in README.md and examples. Introduce @agent.tool decorator for scoping tools to individual agents, clarifying the distinction between global and agent-specific tools. Update examples to reflect new functionality and improve clarity on tool registration and usage. * small tidy-ups for type safety, custom agent decorator handling * stop shared function overwrites * tweak custom * add naming notes to add future refactor --------- Co-authored-by: evalstate <1936278+evalstate@users.noreply.github.com>
1 parent 5c933f6 commit 5ccfdb7

13 files changed

Lines changed: 1044 additions & 77 deletions

File tree

README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -603,6 +603,40 @@ agent["greeter"].send("Good Evening!") # Dictionary access is supported
603603
)
604604
```
605605
606+
### Function Tools
607+
608+
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=`.
609+
610+
**Per-agent tools (`@agent.tool`)** — scope a tool to a specific agent:
611+
612+
```python
613+
@fast.agent(name="writer", instruction="You write things.")
614+
async def writer(): ...
615+
616+
@writer.tool
617+
def translate(text: str, language: str) -> str:
618+
"""Translate text to the given language."""
619+
return f"[{language}] {text}"
620+
621+
@writer.tool(name="summarize", description="Produce a one-line summary")
622+
def summarize(text: str) -> str:
623+
return f"Summary: {text[:80]}..."
624+
```
625+
626+
**Global tools (`@fast.tool`)** — available to all agents that don't declare their own tools:
627+
628+
```python
629+
@fast.tool
630+
def get_weather(city: str) -> str:
631+
"""Return the current weather for a city."""
632+
return f"Sunny in {city}"
633+
634+
@fast.agent(name="assistant", instruction="You are helpful.")
635+
# assistant gets get_weather (global @fast.tool)
636+
```
637+
638+
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.
639+
606640
### Multimodal Support
607641
608642
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:

examples/function-tools/basic.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
"""
2+
Basic @fast.tool example.
3+
4+
Register Python functions as tools using the @fast.tool decorator.
5+
Tools are automatically available to all agents.
6+
7+
Run with: uv run examples/function-tools/basic.py
8+
"""
9+
10+
import asyncio
11+
12+
from fast_agent import FastAgent
13+
14+
fast = FastAgent("Function Tools Example")
15+
16+
17+
@fast.tool
18+
def get_weather(city: str) -> str:
19+
"""Return the current weather for a city."""
20+
return f"Currently sunny and 22°C in {city}"
21+
22+
23+
@fast.tool(name="add", description="Add two numbers together")
24+
def add_numbers(a: int, b: int) -> int:
25+
return a + b
26+
27+
28+
@fast.agent(instruction="You are a helpful assistant with access to tools.")
29+
async def main() -> None:
30+
async with fast.run() as agent:
31+
await agent.interactive()
32+
33+
34+
if __name__ == "__main__":
35+
asyncio.run(main())

examples/function-tools/scoping.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
"""
2+
@agent.tool scoping example.
3+
4+
Demonstrates how tools can be scoped to individual agents using
5+
@agent_func.tool, and how @fast.tool broadcasts globally.
6+
7+
Run with: uv run examples/function-tools/scoping.py
8+
"""
9+
10+
import asyncio
11+
12+
from fast_agent import FastAgent
13+
14+
fast = FastAgent("Tool Scoping Example")
15+
16+
17+
@fast.agent(
18+
name="writer",
19+
instruction="You are a writing assistant with translation and summarization tools.",
20+
default=True,
21+
)
22+
async def writer() -> None:
23+
pass
24+
25+
26+
@fast.agent(
27+
name="analyst",
28+
instruction="You analyse text. You can only count words.",
29+
)
30+
async def analyst() -> None:
31+
pass
32+
33+
34+
@writer.tool
35+
def translate(text: str, language: str) -> str:
36+
"""Translate text to the given language."""
37+
return f"[{language}] {text}"
38+
39+
40+
@writer.tool
41+
def summarize(text: str) -> str:
42+
"""Produce a one-line summary."""
43+
return f"Summary: {text[:80]}..."
44+
45+
46+
@analyst.tool(name="word_count", description="Count words in text")
47+
def count_words(text: str) -> int:
48+
"""Count the number of words in text."""
49+
return len(text.split())
50+
51+
52+
async def main() -> None:
53+
async with fast.run() as agent:
54+
# "writer" sees translate and summarize (its own @writer.tool tools)
55+
# "analyst" sees only word_count (its own @analyst.tool tool)
56+
await agent.interactive()
57+
58+
59+
if __name__ == "__main__":
60+
asyncio.run(main())

src/fast_agent/agents/agent_types.py

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,16 @@ class AgentType(StrEnum):
4646
# Function tools can be:
4747
# - A callable (Python function)
4848
# - A string spec like "module.py:function_name" (for dynamic loading)
49-
FunctionToolConfig: TypeAlias = Callable[..., Any] | str
49+
@dataclass(frozen=True)
50+
class ScopedFunctionToolConfig:
51+
"""A single local Python tool registration with scoped metadata."""
52+
53+
function: Callable[..., Any]
54+
name: str | None = None
55+
description: str | None = None
56+
57+
58+
FunctionToolConfig: TypeAlias = Callable[..., Any] | str | ScopedFunctionToolConfig
5059

5160
FunctionToolsConfig: TypeAlias = list[FunctionToolConfig] | None
5261

@@ -69,16 +78,23 @@ class MCPConnectTarget:
6978

7079
@dataclass
7180
class AgentConfig:
72-
"""Configuration for an Agent instance"""
81+
"""Configuration for an Agent instance.
82+
83+
Naming note:
84+
- ``tools`` filters MCP-discovered tools by server name.
85+
- ``function_tools`` configures local Python function tools.
86+
- Runtime constructors such as ``ToolAgent(..., tools=...)`` use ``tools``
87+
for the resolved executable function-tool objects, not these MCP filters.
88+
"""
7389

7490
name: str
7591
instruction: str = DEFAULT_AGENT_INSTRUCTION
7692
description: str | None = None
7793
tool_input_schema: dict[str, Any] | None = None
7894
servers: list[str] = field(default_factory=list)
79-
tools: dict[str, list[str]] = field(default_factory=dict) # filters for tools
80-
resources: dict[str, list[str]] = field(default_factory=dict) # filters for resources
81-
prompts: dict[str, list[str]] = field(default_factory=dict) # filters for prompts
95+
tools: dict[str, list[str]] = field(default_factory=dict) # MCP tool filters by server
96+
resources: dict[str, list[str]] = field(default_factory=dict) # MCP resource filters by server
97+
prompts: dict[str, list[str]] = field(default_factory=dict) # MCP prompt filters by server
8298
skills: SkillConfig = SKILLS_DEFAULT
8399
skill_manifests: list[SkillManifest] = field(default_factory=list, repr=False)
84100
model: str | None = None
@@ -90,7 +106,7 @@ class AgentConfig:
90106
tool_only: bool = False
91107
elicitation_handler: ElicitationFnT | None = None
92108
api_key: str | None = None
93-
function_tools: FunctionToolsConfig = None
109+
function_tools: FunctionToolsConfig = None # Local Python function tools
94110
shell: bool = False
95111
cwd: Path | None = None
96112
tool_hooks: ToolHooksConfig = None

src/fast_agent/agents/tool_agent.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,11 @@ class ToolAgent(LlmAgent, _ToolLoopAgent):
9292
Pass either:
9393
- native FastMCP FunctionTool objects
9494
- regular Python functions (wrapped as FunctionTools)
95+
96+
Naming note:
97+
``tools`` here means executable local/function tools available to the
98+
agent. It does not refer to ``AgentConfig.tools``, which is the MCP
99+
filter map used by ``McpAgent``.
95100
"""
96101

97102
def __init__(
@@ -100,6 +105,14 @@ def __init__(
100105
tools: Sequence[FunctionTool | Callable[..., Any]] = (),
101106
context: Context | None = None,
102107
) -> None:
108+
"""Create a tool-capable agent.
109+
110+
Args:
111+
config: Agent configuration. ``config.tools`` remains the MCP
112+
filter map; it is separate from this ``tools`` argument.
113+
tools: Executable local/function tools to expose on the agent.
114+
context: Optional runtime context.
115+
"""
103116
super().__init__(config=config, context=context)
104117

105118
self._execution_tools: dict[str, FunctionTool] = {}

0 commit comments

Comments
 (0)