# -*- coding: utf-8 -*- """The tool module utils.""" import inspect from typing import Any, Dict, Callable from docstring_parser import parse from pydantic import Field, create_model, ConfigDict def _remove_title_field(schema: dict) -> dict: """Remove the title field from the JSON schema to avoid misleading the LLM.""" # The top level title field if "title" in schema: schema.pop("title") # properties if "properties" in schema: for prop in schema["properties"].values(): if isinstance(prop, dict): _remove_title_field(prop) # items if "items" in schema and isinstance(schema["items"], dict): _remove_title_field(schema["items"]) # additionalProperties if "additionalProperties" in schema and isinstance( schema["additionalProperties"], dict, ): _remove_title_field(schema["additionalProperties"]) # $defs — referenced sub-schemas, e.g. Pydantic models used as parameter # types generate "$defs": {"SubModel": {"title": "SubModel", ...}}. # These titles are auto-generated noise just like property titles, and # should be removed for the same reason. if "$defs" in schema and isinstance(schema["$defs"], dict): for def_schema in schema["$defs"].values(): if isinstance(def_schema, dict): _remove_title_field(def_schema) return schema def _extract_func_description(docstring: str) -> str: """Extract the function description from the docstring. Args: docstring (`str`): The docstring to extract the function description from. Returns: `str`: The extracted function description. """ parsed_docstring = parse(docstring or "") descriptions = [] if parsed_docstring.short_description is not None: descriptions.append(parsed_docstring.short_description) if parsed_docstring.long_description is not None: descriptions.append(parsed_docstring.long_description) return "\n".join(descriptions) def _extract_input_schema( tool_func: Callable, include_var_positional: bool = False, include_var_keyword: bool = False, ) -> dict: """Extract input schema from the tool function's docstring Args: tool_func (`ToolFunction`): The tool function to extract the JSON schema from. include_var_positional (`bool`): Whether to include variable positional arguments in the JSON schema. include_var_keyword (`bool`): Whether to include variable keyword arguments in the JSON schema. Returns: `dict`: The extracted input JSON schema. """ docstring = parse(tool_func.__doc__ or "") params_docstring = {_.arg_name: _.description for _ in docstring.params} # Create a dynamic model with the function signature fields = {} for name, param in inspect.signature(tool_func).parameters.items(): # Skip the `self` and `cls` parameters if name in ["self", "cls"]: continue # Handle `**kwargs` if param.kind == inspect.Parameter.VAR_KEYWORD: if not include_var_keyword: continue fields[name] = ( Dict[str, Any] if param.annotation == inspect.Parameter.empty else Dict[str, param.annotation], # type: ignore Field( description=params_docstring.get( f"**{name}", params_docstring.get(name, None), ), default={} if param.default is param.empty else param.default, ), ) elif param.kind == inspect.Parameter.VAR_POSITIONAL: if not include_var_positional: continue fields[name] = ( list[Any] if param.annotation == inspect.Parameter.empty else list[param.annotation], # type: ignore Field( description=params_docstring.get( f"*{name}", params_docstring.get(name, None), ), default=[] if param.default is param.empty else param.default, ), ) else: fields[name] = ( Any if param.annotation == inspect.Parameter.empty else param.annotation, Field( description=params_docstring.get(name, None), default=... if param.default is param.empty else param.default, ), ) base_model = create_model( "_StructuredOutputDynamicClass", __config__=ConfigDict(arbitrary_types_allowed=True), **fields, ) params_json_schema = base_model.model_json_schema() # Remove the title from the json schema _remove_title_field(params_json_schema) return params_json_schema