Spaces:
Running
Running
Jeremiah Lowin commited on
Commit ·
ef81ec0
1
Parent(s): 0875611
Handle args/kwargs appropriately for various objects
Browse files- docs/servers/prompts.mdx +4 -0
- docs/servers/resources.mdx +4 -0
- docs/servers/tools.mdx +5 -1
- src/fastmcp/prompts/prompt.py +7 -0
- src/fastmcp/resources/template.py +24 -9
- src/fastmcp/tools/tool.py +8 -0
- tests/prompts/test_prompt_manager.py +24 -0
- tests/resources/test_resource_template.py +28 -3
- tests/server/test_server_interactions.py +15 -2
- tests/tools/test_tool.py +67 -4
- tests/tools/test_tool_manager.py +2 -2
docs/servers/prompts.mdx
CHANGED
|
@@ -53,6 +53,9 @@ def generate_code_request(language: str, task_description: str) -> UserMessage:
|
|
| 53 |
* **Inferred Metadata:** By default:
|
| 54 |
* Prompt Name: Taken from the function name (`ask_about_topic`).
|
| 55 |
* Prompt Description: Taken from the function's docstring.
|
|
|
|
|
|
|
|
|
|
| 56 |
|
| 57 |
### Return Values
|
| 58 |
|
|
@@ -105,6 +108,7 @@ def generate_content_request(
|
|
| 105 |
return prompt
|
| 106 |
```
|
| 107 |
|
|
|
|
| 108 |
### Required vs. Optional Parameters
|
| 109 |
|
| 110 |
Parameters in your function signature are considered **required** unless they have a default value.
|
|
|
|
| 53 |
* **Inferred Metadata:** By default:
|
| 54 |
* Prompt Name: Taken from the function name (`ask_about_topic`).
|
| 55 |
* Prompt Description: Taken from the function's docstring.
|
| 56 |
+
<Tip>
|
| 57 |
+
Functions with `*args` or `**kwargs` are not supported as prompts. This restriction exists because FastMCP needs to generate a complete parameter schema for the MCP protocol, which isn't possible with variable argument lists.
|
| 58 |
+
</Tip>
|
| 59 |
|
| 60 |
### Return Values
|
| 61 |
|
|
|
|
| 108 |
return prompt
|
| 109 |
```
|
| 110 |
|
| 111 |
+
|
| 112 |
### Required vs. Optional Parameters
|
| 113 |
|
| 114 |
Parameters in your function signature are considered **required** unless they have a default value.
|
docs/servers/resources.mdx
CHANGED
|
@@ -239,6 +239,10 @@ Resource templates share most configuration options with regular resources (name
|
|
| 239 |
|
| 240 |
Resource templates generate a new resource for each unique set of parameters, which means that resources can be dynamically created on-demand. For example, if the resource template `"user://profile/{name}"` is registered, MCP clients could request `"user://profile/ford"` or `"user://profile/marvin"` to retrieve either of those two user profiles as resources, without having to register each resource individually.
|
| 241 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 242 |
Here is a complete example that shows how to define two resource templates:
|
| 243 |
|
| 244 |
```python
|
|
|
|
| 239 |
|
| 240 |
Resource templates generate a new resource for each unique set of parameters, which means that resources can be dynamically created on-demand. For example, if the resource template `"user://profile/{name}"` is registered, MCP clients could request `"user://profile/ford"` or `"user://profile/marvin"` to retrieve either of those two user profiles as resources, without having to register each resource individually.
|
| 241 |
|
| 242 |
+
<Tip>
|
| 243 |
+
Functions with `*args` are not supported as resource templates. However, unlike tools and prompts, resource templates do support `**kwargs` because the URI template defines specific parameter names that will be collected and passed as keyword arguments.
|
| 244 |
+
</Tip>
|
| 245 |
+
|
| 246 |
Here is a complete example that shows how to define two resource templates:
|
| 247 |
|
| 248 |
```python
|
docs/servers/tools.mdx
CHANGED
|
@@ -43,9 +43,12 @@ When this tool is registered, FastMCP automatically:
|
|
| 43 |
- Generates an input schema based on the function's parameters and type annotations.
|
| 44 |
- Handles parameter validation and error reporting.
|
| 45 |
|
| 46 |
-
|
| 47 |
The way you define your Python function dictates how the tool appears and behaves for the LLM client.
|
| 48 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
### Parameters
|
| 50 |
|
| 51 |
#### Annotations
|
|
@@ -90,6 +93,7 @@ def process_image(
|
|
| 90 |
# Implementation...
|
| 91 |
```
|
| 92 |
|
|
|
|
| 93 |
You can also use the Field as a default value, though the Annotated approach is preferred:
|
| 94 |
|
| 95 |
```python
|
|
|
|
| 43 |
- Generates an input schema based on the function's parameters and type annotations.
|
| 44 |
- Handles parameter validation and error reporting.
|
| 45 |
|
|
|
|
| 46 |
The way you define your Python function dictates how the tool appears and behaves for the LLM client.
|
| 47 |
|
| 48 |
+
<Tip>
|
| 49 |
+
Functions with `*args` or `**kwargs` are not supported as tools. This restriction exists because FastMCP needs to generate a complete parameter schema for the MCP protocol, which isn't possible with variable argument lists.
|
| 50 |
+
</Tip>
|
| 51 |
+
|
| 52 |
### Parameters
|
| 53 |
|
| 54 |
#### Annotations
|
|
|
|
| 93 |
# Implementation...
|
| 94 |
```
|
| 95 |
|
| 96 |
+
|
| 97 |
You can also use the Field as a default value, though the Annotated approach is preferred:
|
| 98 |
|
| 99 |
```python
|
src/fastmcp/prompts/prompt.py
CHANGED
|
@@ -111,6 +111,13 @@ class Prompt(BaseModel):
|
|
| 111 |
|
| 112 |
if func_name == "<lambda>":
|
| 113 |
raise ValueError("You must provide a name for lambda functions")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 114 |
|
| 115 |
type_adapter = get_cached_typeadapter(fn)
|
| 116 |
parameters = type_adapter.json_schema()
|
|
|
|
| 111 |
|
| 112 |
if func_name == "<lambda>":
|
| 113 |
raise ValueError("You must provide a name for lambda functions")
|
| 114 |
+
# Reject functions with *args or **kwargs
|
| 115 |
+
sig = inspect.signature(fn)
|
| 116 |
+
for param in sig.parameters.values():
|
| 117 |
+
if param.kind == inspect.Parameter.VAR_POSITIONAL:
|
| 118 |
+
raise ValueError("Functions with *args are not supported as prompts")
|
| 119 |
+
if param.kind == inspect.Parameter.VAR_KEYWORD:
|
| 120 |
+
raise ValueError("Functions with **kwargs are not supported as prompts")
|
| 121 |
|
| 122 |
type_adapter = get_cached_typeadapter(fn)
|
| 123 |
parameters = type_adapter.json_schema()
|
src/fastmcp/resources/template.py
CHANGED
|
@@ -109,6 +109,15 @@ class ResourceTemplate(BaseModel):
|
|
| 109 |
if func_name == "<lambda>":
|
| 110 |
raise ValueError("You must provide a name for lambda functions")
|
| 111 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
# Auto-detect context parameter if not provided
|
| 113 |
if context_kwarg is None:
|
| 114 |
context_kwarg = find_kwarg_by_type(fn, kwarg_type=Context)
|
|
@@ -118,7 +127,7 @@ class ResourceTemplate(BaseModel):
|
|
| 118 |
if not uri_params:
|
| 119 |
raise ValueError("URI template must contain at least one parameter")
|
| 120 |
|
| 121 |
-
func_params = set(
|
| 122 |
if context_kwarg:
|
| 123 |
func_params.discard(context_kwarg)
|
| 124 |
|
|
@@ -126,20 +135,26 @@ class ResourceTemplate(BaseModel):
|
|
| 126 |
required_params = {
|
| 127 |
p
|
| 128 |
for p in func_params
|
| 129 |
-
if
|
|
|
|
|
|
|
| 130 |
}
|
| 131 |
-
if context_kwarg and context_kwarg in required_params:
|
| 132 |
-
required_params.discard(context_kwarg)
|
| 133 |
|
|
|
|
| 134 |
if not required_params.issubset(uri_params):
|
| 135 |
raise ValueError(
|
| 136 |
-
f"
|
| 137 |
)
|
| 138 |
|
| 139 |
-
if
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 143 |
|
| 144 |
# Get schema from TypeAdapter - will fail if function isn't properly typed
|
| 145 |
parameters = TypeAdapter(fn).json_schema()
|
|
|
|
| 109 |
if func_name == "<lambda>":
|
| 110 |
raise ValueError("You must provide a name for lambda functions")
|
| 111 |
|
| 112 |
+
# Reject functions with *args
|
| 113 |
+
# (**kwargs is allowed because the URI will define the parameter names)
|
| 114 |
+
sig = inspect.signature(fn)
|
| 115 |
+
for param in sig.parameters.values():
|
| 116 |
+
if param.kind == inspect.Parameter.VAR_POSITIONAL:
|
| 117 |
+
raise ValueError(
|
| 118 |
+
"Functions with *args are not supported as resource templates"
|
| 119 |
+
)
|
| 120 |
+
|
| 121 |
# Auto-detect context parameter if not provided
|
| 122 |
if context_kwarg is None:
|
| 123 |
context_kwarg = find_kwarg_by_type(fn, kwarg_type=Context)
|
|
|
|
| 127 |
if not uri_params:
|
| 128 |
raise ValueError("URI template must contain at least one parameter")
|
| 129 |
|
| 130 |
+
func_params = set(sig.parameters.keys())
|
| 131 |
if context_kwarg:
|
| 132 |
func_params.discard(context_kwarg)
|
| 133 |
|
|
|
|
| 135 |
required_params = {
|
| 136 |
p
|
| 137 |
for p in func_params
|
| 138 |
+
if sig.parameters[p].default is inspect.Parameter.empty
|
| 139 |
+
and sig.parameters[p].kind != inspect.Parameter.VAR_KEYWORD
|
| 140 |
+
and p != context_kwarg
|
| 141 |
}
|
|
|
|
|
|
|
| 142 |
|
| 143 |
+
# Check if required parameters are a subset of the URI parameters
|
| 144 |
if not required_params.issubset(uri_params):
|
| 145 |
raise ValueError(
|
| 146 |
+
f"Required function arguments {required_params} must be a subset of the URI parameters {uri_params}"
|
| 147 |
)
|
| 148 |
|
| 149 |
+
# CHeck if the URI parameters are a subset of the function parameters (skip if **kwargs present)
|
| 150 |
+
if not any(
|
| 151 |
+
param.kind == inspect.Parameter.VAR_KEYWORD
|
| 152 |
+
for param in sig.parameters.values()
|
| 153 |
+
):
|
| 154 |
+
if not uri_params.issubset(func_params):
|
| 155 |
+
raise ValueError(
|
| 156 |
+
f"URI parameters {uri_params} must be a subset of the function arguments: {func_params}"
|
| 157 |
+
)
|
| 158 |
|
| 159 |
# Get schema from TypeAdapter - will fail if function isn't properly typed
|
| 160 |
parameters = TypeAdapter(fn).json_schema()
|
src/fastmcp/tools/tool.py
CHANGED
|
@@ -67,6 +67,14 @@ class Tool(BaseModel):
|
|
| 67 |
"""Create a Tool from a function."""
|
| 68 |
from fastmcp import Context
|
| 69 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
func_name = name or fn.__name__
|
| 71 |
|
| 72 |
if func_name == "<lambda>":
|
|
|
|
| 67 |
"""Create a Tool from a function."""
|
| 68 |
from fastmcp import Context
|
| 69 |
|
| 70 |
+
# Reject functions with *args or **kwargs
|
| 71 |
+
sig = inspect.signature(fn)
|
| 72 |
+
for param in sig.parameters.values():
|
| 73 |
+
if param.kind == inspect.Parameter.VAR_POSITIONAL:
|
| 74 |
+
raise ValueError("Functions with *args are not supported as tools")
|
| 75 |
+
if param.kind == inspect.Parameter.VAR_KEYWORD:
|
| 76 |
+
raise ValueError("Functions with **kwargs are not supported as tools")
|
| 77 |
+
|
| 78 |
func_name = name or fn.__name__
|
| 79 |
|
| 80 |
if func_name == "<lambda>":
|
tests/prompts/test_prompt_manager.py
CHANGED
|
@@ -189,6 +189,30 @@ class TestPromptManager:
|
|
| 189 |
with pytest.raises(ValueError, match="Missing required arguments"):
|
| 190 |
await manager.render_prompt("fn")
|
| 191 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 192 |
|
| 193 |
class TestPromptTags:
|
| 194 |
"""Test functionality related to prompt tags."""
|
|
|
|
| 189 |
with pytest.raises(ValueError, match="Missing required arguments"):
|
| 190 |
await manager.render_prompt("fn")
|
| 191 |
|
| 192 |
+
async def test_prompt_with_varargs_not_allowed(self):
|
| 193 |
+
"""Test that a prompt with *args is not allowed."""
|
| 194 |
+
|
| 195 |
+
def fn(*args: int) -> str:
|
| 196 |
+
return f"Hello, {args}!"
|
| 197 |
+
|
| 198 |
+
manager = PromptManager()
|
| 199 |
+
with pytest.raises(
|
| 200 |
+
ValueError, match=r"Functions with \*args are not supported as prompts"
|
| 201 |
+
):
|
| 202 |
+
manager.add_prompt(Prompt.from_function(fn))
|
| 203 |
+
|
| 204 |
+
async def test_prompt_with_varkwargs_not_allowed(self):
|
| 205 |
+
"""Test that a prompt with **kwargs is not allowed."""
|
| 206 |
+
|
| 207 |
+
def fn(**kwargs: int) -> str:
|
| 208 |
+
return f"Hello, {kwargs}!"
|
| 209 |
+
|
| 210 |
+
manager = PromptManager()
|
| 211 |
+
with pytest.raises(
|
| 212 |
+
ValueError, match=r"Functions with \*\*kwargs are not supported as prompts"
|
| 213 |
+
):
|
| 214 |
+
manager.add_prompt(Prompt.from_function(fn))
|
| 215 |
+
|
| 216 |
|
| 217 |
class TestPromptTags:
|
| 218 |
"""Test functionality related to prompt tags."""
|
tests/resources/test_resource_template.py
CHANGED
|
@@ -104,7 +104,7 @@ class TestResourceTemplate:
|
|
| 104 |
# This should fail - 'unknown' is not a function parameter
|
| 105 |
with pytest.raises(
|
| 106 |
ValueError,
|
| 107 |
-
match="
|
| 108 |
):
|
| 109 |
ResourceTemplate.from_function(
|
| 110 |
fn=my_func,
|
|
@@ -132,7 +132,7 @@ class TestResourceTemplate:
|
|
| 132 |
# This should fail - required param is not in URI
|
| 133 |
with pytest.raises(
|
| 134 |
ValueError,
|
| 135 |
-
match="
|
| 136 |
):
|
| 137 |
ResourceTemplate.from_function(
|
| 138 |
fn=func_with_required,
|
|
@@ -157,7 +157,7 @@ class TestResourceTemplate:
|
|
| 157 |
# This fails - missing one required param
|
| 158 |
with pytest.raises(
|
| 159 |
ValueError,
|
| 160 |
-
match="
|
| 161 |
):
|
| 162 |
ResourceTemplate.from_function(
|
| 163 |
fn=multi_required,
|
|
@@ -360,6 +360,31 @@ class TestResourceTemplate:
|
|
| 360 |
params = template.matches("test://src/path/to/test.py")
|
| 361 |
assert params == {"prefix": "src", "path": "path/to/test.py"}
|
| 362 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 363 |
|
| 364 |
class TestMatchUriTemplate:
|
| 365 |
"""Test match_uri_template function."""
|
|
|
|
| 104 |
# This should fail - 'unknown' is not a function parameter
|
| 105 |
with pytest.raises(
|
| 106 |
ValueError,
|
| 107 |
+
match="Required function arguments .* must be a subset of the URI parameters",
|
| 108 |
):
|
| 109 |
ResourceTemplate.from_function(
|
| 110 |
fn=my_func,
|
|
|
|
| 132 |
# This should fail - required param is not in URI
|
| 133 |
with pytest.raises(
|
| 134 |
ValueError,
|
| 135 |
+
match="Required function arguments .* must be a subset of the URI parameters",
|
| 136 |
):
|
| 137 |
ResourceTemplate.from_function(
|
| 138 |
fn=func_with_required,
|
|
|
|
| 157 |
# This fails - missing one required param
|
| 158 |
with pytest.raises(
|
| 159 |
ValueError,
|
| 160 |
+
match="Required function arguments .* must be a subset of the URI parameters",
|
| 161 |
):
|
| 162 |
ResourceTemplate.from_function(
|
| 163 |
fn=multi_required,
|
|
|
|
| 360 |
params = template.matches("test://src/path/to/test.py")
|
| 361 |
assert params == {"prefix": "src", "path": "path/to/test.py"}
|
| 362 |
|
| 363 |
+
async def test_function_with_varargs_not_allowed(self):
|
| 364 |
+
def func(x: int, *args: int) -> int:
|
| 365 |
+
return x + sum(args)
|
| 366 |
+
|
| 367 |
+
with pytest.raises(
|
| 368 |
+
ValueError,
|
| 369 |
+
match=r"Functions with \*args are not supported as resource templates",
|
| 370 |
+
):
|
| 371 |
+
ResourceTemplate.from_function(
|
| 372 |
+
fn=func,
|
| 373 |
+
uri_template="test://{x}/{args*}",
|
| 374 |
+
name="test",
|
| 375 |
+
)
|
| 376 |
+
|
| 377 |
+
async def test_function_with_varkwargs_ok(self):
|
| 378 |
+
def func(x: int, **kwargs: int) -> int:
|
| 379 |
+
return x + sum(kwargs.values())
|
| 380 |
+
|
| 381 |
+
template = ResourceTemplate.from_function(
|
| 382 |
+
fn=func,
|
| 383 |
+
uri_template="test://{x}/{y}/{z}",
|
| 384 |
+
name="test",
|
| 385 |
+
)
|
| 386 |
+
assert template.uri_template == "test://{x}/{y}/{z}"
|
| 387 |
+
|
| 388 |
|
| 389 |
class TestMatchUriTemplate:
|
| 390 |
"""Test match_uri_template function."""
|
tests/server/test_server_interactions.py
CHANGED
|
@@ -930,7 +930,7 @@ class TestResourceTemplates:
|
|
| 930 |
|
| 931 |
with pytest.raises(
|
| 932 |
ValueError,
|
| 933 |
-
match="
|
| 934 |
):
|
| 935 |
|
| 936 |
@mcp.resource("resource://{name}/data")
|
|
@@ -958,7 +958,7 @@ class TestResourceTemplates:
|
|
| 958 |
|
| 959 |
with pytest.raises(
|
| 960 |
ValueError,
|
| 961 |
-
match="
|
| 962 |
):
|
| 963 |
|
| 964 |
@mcp.resource("resource://{org}/{repo}/data")
|
|
@@ -977,6 +977,19 @@ class TestResourceTemplates:
|
|
| 977 |
assert isinstance(result[0], TextResourceContents)
|
| 978 |
assert result[0].text == "Static data"
|
| 979 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 980 |
async def test_template_with_default_params(self):
|
| 981 |
"""Test that a template can have default parameters."""
|
| 982 |
mcp = FastMCP()
|
|
|
|
| 930 |
|
| 931 |
with pytest.raises(
|
| 932 |
ValueError,
|
| 933 |
+
match="Required function arguments .* must be a subset of the URI parameters",
|
| 934 |
):
|
| 935 |
|
| 936 |
@mcp.resource("resource://{name}/data")
|
|
|
|
| 958 |
|
| 959 |
with pytest.raises(
|
| 960 |
ValueError,
|
| 961 |
+
match="Required function arguments .* must be a subset of the URI parameters",
|
| 962 |
):
|
| 963 |
|
| 964 |
@mcp.resource("resource://{org}/{repo}/data")
|
|
|
|
| 977 |
assert isinstance(result[0], TextResourceContents)
|
| 978 |
assert result[0].text == "Static data"
|
| 979 |
|
| 980 |
+
async def test_template_with_varkwargs(self):
|
| 981 |
+
"""Test that a template can have **kwargs."""
|
| 982 |
+
mcp = FastMCP()
|
| 983 |
+
|
| 984 |
+
@mcp.resource("test://{x}/{y}/{z}")
|
| 985 |
+
def func(**kwargs: int) -> int:
|
| 986 |
+
return sum(kwargs.values())
|
| 987 |
+
|
| 988 |
+
async with Client(mcp) as client:
|
| 989 |
+
result = await client.read_resource(AnyUrl("test://1/2/3"))
|
| 990 |
+
assert isinstance(result[0], TextResourceContents)
|
| 991 |
+
assert result[0].text == "6"
|
| 992 |
+
|
| 993 |
async def test_template_with_default_params(self):
|
| 994 |
"""Test that a template can have default parameters."""
|
| 995 |
mcp = FastMCP()
|
tests/tools/test_tool.py
CHANGED
|
@@ -63,15 +63,15 @@ class TestToolFromFunction:
|
|
| 63 |
assert tool.parameters["properties"]["data"]["type"] == "string"
|
| 64 |
assert isinstance(result[0], ImageContent)
|
| 65 |
|
| 66 |
-
def
|
| 67 |
-
with pytest.raises(
|
| 68 |
Tool.from_function(1) # type: ignore
|
| 69 |
|
| 70 |
-
def
|
| 71 |
tool = Tool.from_function(lambda x: x, name="my_tool")
|
| 72 |
assert tool.name == "my_tool"
|
| 73 |
|
| 74 |
-
def
|
| 75 |
with pytest.raises(
|
| 76 |
ValueError, match="You must provide a name for lambda functions"
|
| 77 |
):
|
|
@@ -86,6 +86,69 @@ class TestToolFromFunction:
|
|
| 86 |
assert tool.parameters["properties"]["_a"]["type"] == "integer"
|
| 87 |
assert tool.parameters["properties"]["_b"]["type"] == "integer"
|
| 88 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
|
| 90 |
class TestToolJsonParsing:
|
| 91 |
"""Tests for Tool's JSON pre-parsing functionality."""
|
|
|
|
| 63 |
assert tool.parameters["properties"]["data"]["type"] == "string"
|
| 64 |
assert isinstance(result[0], ImageContent)
|
| 65 |
|
| 66 |
+
def test_non_callable_fn(self):
|
| 67 |
+
with pytest.raises(TypeError, match="not a callable object"):
|
| 68 |
Tool.from_function(1) # type: ignore
|
| 69 |
|
| 70 |
+
def test_lambda(self):
|
| 71 |
tool = Tool.from_function(lambda x: x, name="my_tool")
|
| 72 |
assert tool.name == "my_tool"
|
| 73 |
|
| 74 |
+
def test_lambda_with_no_name(self):
|
| 75 |
with pytest.raises(
|
| 76 |
ValueError, match="You must provide a name for lambda functions"
|
| 77 |
):
|
|
|
|
| 86 |
assert tool.parameters["properties"]["_a"]["type"] == "integer"
|
| 87 |
assert tool.parameters["properties"]["_b"]["type"] == "integer"
|
| 88 |
|
| 89 |
+
def test_tool_with_varargs_not_allowed(self):
|
| 90 |
+
def func(a: int, b: int, *args: int) -> int:
|
| 91 |
+
"""Add two numbers."""
|
| 92 |
+
return a + b
|
| 93 |
+
|
| 94 |
+
with pytest.raises(
|
| 95 |
+
ValueError, match=r"Functions with \*args are not supported as tools"
|
| 96 |
+
):
|
| 97 |
+
Tool.from_function(func)
|
| 98 |
+
|
| 99 |
+
def test_tool_with_varkwargs_not_allowed(self):
|
| 100 |
+
def func(a: int, b: int, **kwargs: int) -> int:
|
| 101 |
+
"""Add two numbers."""
|
| 102 |
+
return a + b
|
| 103 |
+
|
| 104 |
+
with pytest.raises(
|
| 105 |
+
ValueError, match=r"Functions with \*\*kwargs are not supported as tools"
|
| 106 |
+
):
|
| 107 |
+
Tool.from_function(func)
|
| 108 |
+
|
| 109 |
+
async def test_instance_method(self):
|
| 110 |
+
class MyClass:
|
| 111 |
+
def add(self, x: int, y: int) -> int:
|
| 112 |
+
"""Add two numbers."""
|
| 113 |
+
return x + y
|
| 114 |
+
|
| 115 |
+
obj = MyClass()
|
| 116 |
+
|
| 117 |
+
tool = Tool.from_function(obj.add)
|
| 118 |
+
assert tool.name == "add"
|
| 119 |
+
assert tool.description == "Add two numbers."
|
| 120 |
+
assert "self" not in tool.parameters["properties"]
|
| 121 |
+
|
| 122 |
+
async def test_instance_method_with_varargs_not_allowed(self):
|
| 123 |
+
class MyClass:
|
| 124 |
+
def add(self, x: int, y: int, *args: int) -> int:
|
| 125 |
+
"""Add two numbers."""
|
| 126 |
+
return x + y
|
| 127 |
+
|
| 128 |
+
obj = MyClass()
|
| 129 |
+
|
| 130 |
+
with pytest.raises(
|
| 131 |
+
ValueError, match=r"Functions with \*args are not supported as tools"
|
| 132 |
+
):
|
| 133 |
+
Tool.from_function(obj.add)
|
| 134 |
+
|
| 135 |
+
async def test_instance_method_with_varkwargs_not_allowed(self):
|
| 136 |
+
class MyClass:
|
| 137 |
+
def add(self, x: int, y: int, **kwargs: int) -> int:
|
| 138 |
+
"""Add two numbers."""
|
| 139 |
+
return x + y
|
| 140 |
+
|
| 141 |
+
obj = MyClass()
|
| 142 |
+
|
| 143 |
+
with pytest.raises(
|
| 144 |
+
ValueError, match=r"Functions with \*\*kwargs are not supported as tools"
|
| 145 |
+
):
|
| 146 |
+
Tool.from_function(obj.add)
|
| 147 |
+
|
| 148 |
+
async def test_classmethod(self):
|
| 149 |
+
class MyClass:
|
| 150 |
+
x: int = 10
|
| 151 |
+
|
| 152 |
|
| 153 |
class TestToolJsonParsing:
|
| 154 |
"""Tests for Tool's JSON pre-parsing functionality."""
|
tests/tools/test_tool_manager.py
CHANGED
|
@@ -84,9 +84,9 @@ class TestAddTools:
|
|
| 84 |
assert tool.parameters["properties"]["data"]["type"] == "string"
|
| 85 |
assert isinstance(result[0], ImageContent)
|
| 86 |
|
| 87 |
-
def
|
| 88 |
manager = ToolManager()
|
| 89 |
-
with pytest.raises(
|
| 90 |
manager.add_tool_from_fn(1) # type: ignore
|
| 91 |
|
| 92 |
def test_add_lambda(self):
|
|
|
|
| 84 |
assert tool.parameters["properties"]["data"]["type"] == "string"
|
| 85 |
assert isinstance(result[0], ImageContent)
|
| 86 |
|
| 87 |
+
def test_add_noncallable_tool(self):
|
| 88 |
manager = ToolManager()
|
| 89 |
+
with pytest.raises(TypeError, match="not a callable object"):
|
| 90 |
manager.add_tool_from_fn(1) # type: ignore
|
| 91 |
|
| 92 |
def test_add_lambda(self):
|