Skip to content

Commit de2d4b4

Browse files
committed
add ability for users to create arbitrary dynamic tools
1 parent ab88b25 commit de2d4b4

File tree

1 file changed

+54
-2
lines changed

1 file changed

+54
-2
lines changed

src/fenic/api/mcp/tool_generation.py

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,13 @@
1111

1212
from __future__ import annotations
1313

14+
import functools
1415
import hashlib
16+
import inspect
1517
import json
1618
import re
1719
from dataclasses import dataclass
18-
from typing import Dict, List, Optional, Union
20+
from typing import Callable, Dict, List, Optional, Union
1921

2022
import polars as pl
2123
from typing_extensions import Annotated
@@ -34,7 +36,7 @@
3436
from fenic.core._logical_plan.plans.base import LogicalPlan
3537
from fenic.core._utils.schema import convert_custom_dtype_to_polars
3638
from fenic.core.error import ConfigurationError, ValidationError
37-
from fenic.core.mcp.types import DynamicToolDefinition
39+
from fenic.core.mcp.types import DynamicToolDefinition, TableFormat
3840
from fenic.core.types.datatypes import (
3941
BooleanType,
4042
DoubleType,
@@ -89,6 +91,56 @@ def auto_generate_core_tools_from_tables(
8991
sql_max_rows=sql_max_rows,
9092
)
9193

94+
def dynamic_tool(
95+
tool_name: str,
96+
tool_description: str,
97+
max_result_limit: Optional[int] = None,
98+
default_table_format: TableFormat = "markdown",
99+
):
100+
"""Decorator to bind a DataFrame to a user-authored tool function.
101+
102+
Example:
103+
@dynamic_tool(tool_name="find_rust", tool_description="...")
104+
def find_rust(
105+
query: Annotated[str, "Natural language query"],
106+
limit: Annotated[int, "Max rows"] = 50,
107+
) -> DataFrame:
108+
pred = fc.semantic.predicate("Matches: {{q}}", q=query, bio=fc.col("bio"))
109+
return df.filter(pred).limit(limit)
110+
111+
Notes:
112+
- The decorated function MUST NOT use *args/**kwargs, and should annotate parameters with Annotated descriptions.
113+
- The decorated function MUST return a fenic DataFrame.
114+
- The returned object is a DynamicTool ready for registration.
115+
"""
116+
117+
def decorator(func: Callable[..., DataFrame]) -> DynamicToolDefinition:
118+
_ensure_no_var_args(func, func_label=tool_name)
119+
120+
@functools.wraps(func)
121+
def wrapper(*args, **kwargs) -> LogicalPlan:
122+
result_df = func(*args, **kwargs)
123+
return result_df._logical_plan
124+
125+
return DynamicToolDefinition(
126+
name=tool_name,
127+
description=tool_description,
128+
func=wrapper,
129+
max_result_limit=max_result_limit,
130+
default_table_format=default_table_format,
131+
)
132+
133+
return decorator
134+
135+
def _ensure_no_var_args(func: Callable[..., object], *, func_label: str) -> None:
136+
sig = inspect.signature(func)
137+
for p in sig.parameters.values():
138+
if p.kind.name in {"VAR_POSITIONAL", "VAR_KEYWORD"}:
139+
raise ValueError(
140+
f"{func_label} must not use *args or **kwargs for MCP tool introspection."
141+
)
142+
143+
92144
def _auto_generate_read_tool(
93145
datasets: List[DatasetSpec],
94146
session: Session,

0 commit comments

Comments
 (0)