Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improving how function docstring gets converted to tool's jsonschema for FastMCP #226

Open
salman1993 opened this issue Feb 21, 2025 · 0 comments

Comments

@salman1993
Copy link
Contributor

salman1993 commented Feb 21, 2025

Is your feature request related to a problem? Please describe.
Tool descriptions are not parsed as expected from the function docstring when using FastMCP. This affects tool calling performance.

Currently, FastMCP does some function inspection to create the docstring here:

  • func_arg_metadata = func_metadata(
    fn,
    skip_names=[context_kwarg] if context_kwarg is not None else [],
    )
    parameters = func_arg_metadata.arg_model.model_json_schema()
  • def func_metadata(func: Callable, skip_names: Sequence[str] = ()) -> FuncMetadata:
    """Given a function, return metadata including a pydantic model representing its
    signature.
    The use case for this is
    ```
    meta = func_to_pyd(func)
    validated_args = meta.arg_model.model_validate(some_raw_data_dict)
    return func(**validated_args.model_dump_one_level())
    ```
    **critically** it also provides pre-parse helper to attempt to parse things from
    JSON.
    Args:
    func: The function to convert to a pydantic model
    skip_names: A list of parameter names to skip. These will not be included in
    the model.
    Returns:
    A pydantic model representing the function's signature.
    """
    sig = _get_typed_signature(func)
    params = sig.parameters
    dynamic_pydantic_model_params: dict[str, Any] = {}
    globalns = getattr(func, "__globals__", {})
    for param in params.values():
    if param.name.startswith("_"):
    raise InvalidSignature(
    f"Parameter {param.name} of {func.__name__} cannot start with '_'"
    )
    if param.name in skip_names:
    continue
    annotation = param.annotation
    # `x: None` / `x: None = None`
    if annotation is None:
    annotation = Annotated[
    None,
    Field(
    default=param.default
    if param.default is not inspect.Parameter.empty
    else PydanticUndefined
    ),
    ]
    # Untyped field
    if annotation is inspect.Parameter.empty:
    annotation = Annotated[
    Any,
    Field(),
    # 🤷
    WithJsonSchema({"title": param.name, "type": "string"}),
    ]
    field_info = FieldInfo.from_annotated_attribute(
    _get_typed_annotation(annotation, globalns),
    param.default
    if param.default is not inspect.Parameter.empty
    else PydanticUndefined,
    )
    dynamic_pydantic_model_params[param.name] = (field_info.annotation, field_info)
    continue
    arguments_model = create_model(
    f"{func.__name__}Arguments",
    **dynamic_pydantic_model_params,
    __base__=ArgModelBase,
    )
    resp = FuncMetadata(arg_model=arguments_model)
    return resp

From my understanding, it creates a FuncMetadata model in pydantic which then gets converted to jsonschema.

Current behaviour:
If we have a tool such as:

def add_numbers(a: float, b: float) -> float:
    """
    Adds two numbers and returns the result.

    Args:
        a (float): The first number.
        b (float): The second number.

    Returns:
        float: The sum of a and b.
    """
    return a + b

it gets parsed into:

>>> func_arg_metadata = func_metadata(add_numbers)
>>> parameters = func_arg_metadata.arg_model.model_json_schema()
>>> parameters
{'properties': {'a': {'title': 'A', 'type': 'number'}, 'b': {'title': 'B', 'type': 'number'}}, 'required': ['a', 'b'], 'title': 'add_numbersArguments', 'type': 'object'}

>>> add_numbers.__doc__  
'\nAdds two numbers and returns the result.\n\nArgs:\n    a (float): The first number.\n    b (float): The second number.\n\nReturns:\n    float: The sum of a and b.\n'

Describe the solution you'd like
It'd be nicer to follow one of the python docstring styles and parse out the argument descriptions from the docstring.

{
  "name": "add_numbers",
  "description": "Adds two numbers and returns the sum.",
  "parameters": {
    "type": "object",
    "properties": {
      "a": {
        "type": "number",
        "description": "The first number to add."
      },
      "b": {
        "type": "number",
        "description": "The second number to add."
      }
    },
    "required": ["a", "b"]
  }
}

Describe alternatives you've considered
we used to do this in a previous python version of goose: https://github.com/block/goose/blob/eccb1b22614f39b751db4e5efd73d728d9ca40fc/packages/exchange/src/exchange/utils.py#L82-L107

here are some test examples: https://github.com/block/goose/blob/eccb1b22614f39b751db4e5efd73d728d9ca40fc/packages/exchange/tests/test_utils.py#L32-L136

Additional context
I am happy to add this in - wanted to post this first to check that you're okay with enforcing a docstring style ("google", "numpy", "sphinx") & adding griffe as a dependency.

@salman1993 salman1993 changed the title Improving the jsonschema created from the docstring in FastMCP Improving how function docstring gets converted to jsonschema when using FastMCP Feb 21, 2025
@salman1993 salman1993 changed the title Improving how function docstring gets converted to jsonschema when using FastMCP Improving how function docstring gets converted to tool's jsonschema when using FastMCP Feb 21, 2025
@salman1993 salman1993 changed the title Improving how function docstring gets converted to tool's jsonschema when using FastMCP Improving how function docstring gets converted to tool's jsonschema for FastMCP Feb 21, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant