Create a CustomTool by inferring metadata from a decorated function. - slug: from ``fn.__name__.upper()`` - name: from ``fn.__name__`` humanized - description: from ``fn.__doc__`` - input_params: from the first parameter with a BaseModel type annotation
(
fn: t.Callable[..., t.Any],
*,
slug: t.Optional[str] = None,
name: t.Optional[str] = None,
description: t.Optional[str] = None,
extends_toolkit: t.Optional[str] = None,
output_params: t.Optional[t.Type[BaseModel]] = None,
preload: t.Optional[bool] = None,
annotation_locals: t.Optional[t.Mapping[str, t.Any]] = None,
)
| 262 | |
| 263 | |
| 264 | def _infer_tool_from_function( |
| 265 | fn: t.Callable[..., t.Any], |
| 266 | *, |
| 267 | slug: t.Optional[str] = None, |
| 268 | name: t.Optional[str] = None, |
| 269 | description: t.Optional[str] = None, |
| 270 | extends_toolkit: t.Optional[str] = None, |
| 271 | output_params: t.Optional[t.Type[BaseModel]] = None, |
| 272 | preload: t.Optional[bool] = None, |
| 273 | annotation_locals: t.Optional[t.Mapping[str, t.Any]] = None, |
| 274 | ) -> CustomTool: |
| 275 | """Create a CustomTool by inferring metadata from a decorated function. |
| 276 | |
| 277 | - slug: from ``fn.__name__.upper()`` |
| 278 | - name: from ``fn.__name__`` humanized |
| 279 | - description: from ``fn.__doc__`` |
| 280 | - input_params: from the first parameter with a BaseModel type annotation |
| 281 | """ |
| 282 | # Infer slug |
| 283 | actual_slug = slug or fn.__name__.upper() |
| 284 | actual_name = name or fn.__name__.replace("_", " ").title() |
| 285 | actual_description = description or inspect.cleandoc(fn.__doc__ or "") |
| 286 | |
| 287 | if not actual_description: |
| 288 | raise ValidationError( |
| 289 | f"experimental.tool: description is required. " |
| 290 | f'Add a docstring to "{fn.__name__}" or pass description=...' |
| 291 | ) |
| 292 | |
| 293 | # Validate and infer function signature. |
| 294 | # Accepted shapes (consistent with TS CustomToolExecuteFn): |
| 295 | # (input: BaseModel) — no session context needed |
| 296 | # (input: BaseModel, ctx) — with session context |
| 297 | # The first param MUST be annotated with a BaseModel subclass. |
| 298 | # Reject async before wrapping (wrapper would hide it from _create_tool) |
| 299 | if asyncio.iscoroutinefunction(fn): |
| 300 | raise ValidationError( |
| 301 | f'experimental.tool: "{fn.__name__}" is async. ' |
| 302 | f"The Composio Python SDK is synchronous — use a regular " |
| 303 | f"'def {fn.__name__}(input, ctx)' instead of 'async def'." |
| 304 | ) |
| 305 | |
| 306 | sig = inspect.signature(fn) |
| 307 | params = list(sig.parameters.values()) |
| 308 | resolved_annotations = _resolve_function_annotations( |
| 309 | fn, |
| 310 | localns=annotation_locals, |
| 311 | ) |
| 312 | |
| 313 | if not params: |
| 314 | raise ValidationError( |
| 315 | f'experimental.tool: "{fn.__name__}" must accept at least one parameter ' |
| 316 | f"annotated with a Pydantic BaseModel subclass, e.g. " |
| 317 | f"def {fn.__name__}(input: MyInput, ctx): ..." |
| 318 | ) |
| 319 | |
| 320 | if len(params) > 2: |
| 321 | raise ValidationError( |
no test coverage detected
searching dependent graphs…