Skip to content

Support per-parameter descriptions in FunctionTool via Annotated[T, Field(description=...)] #4552

@ecanlar

Description

@ecanlar

[Feature Request] Support per-parameter descriptions in FunctionTool via Annotated[T, Field(description=...)]

Summary

FunctionTool (automatic function calling) currently ignores per-parameter semantic descriptions. All parameter descriptions are hardcoded to None in _get_fields_dict, regardless of whether the developer provided them via Annotated + pydantic.Field or via the Args: section of the docstring. This prevents developers from giving the LLM the contextual guidance it needs to correctly populate each argument.


Current behaviour

In google/adk/tools/_automatic_function_calling_util.py, the helper _get_fields_dict explicitly drops parameter descriptions:

# google/adk/tools/_automatic_function_calling_util.py  (v1.14.1 – v1.25.1)

def _get_fields_dict(func: Callable) -> Dict:
    param_signature = dict(inspect.signature(func).parameters)
    fields_dict = {
        name: (
            # 1. We infer the argument type here …
            (param.annotation if param.annotation != inspect.Parameter.empty else Any),
            pydantic.Field(
                # 2. We do not support default values for now.
                default=( … ),
                # 3. Do not support parameter description for now.
                description=None,          # <-- always None
            ),
        )
        for name, param in param_signature.items()
        …
    }
    return fields_dict

As a consequence, the FunctionDeclaration sent to the model contains parameter schemas with no description field, even when the developer explicitly annotates their function like this:

from typing import Annotated
from pydantic import Field

async def create_task(
    repository: Annotated[str, Field(
        description=(
            "Full GitLab repository URL. "
            "MUST be obtained from get_repository_info. "
            "Format: https://gitlab.com/group/project"
        )
    )],
    base_branch: Annotated[str, Field(
        description=(
            "Base branch for development (e.g. 'main', 'develop'). "
            "MUST be obtained from get_repository_info. "
            "Do NOT default to 'main' without calling that tool first."
        )
    )],
) -> dict:
    ...

Attempting this today raises a ValueError:

ValueError: Failed to parse the parameter repository:
typing.Annotated[str, FieldInfo(annotation=NoneType, required=True,
description='Full GitLab repository URL …')] of function create_task
for automatic function calling.

Expected behaviour

  1. Annotated[T, Field(description="...")] — when a parameter is annotated with a pydantic Field carrying a description, that description should be forwarded into the generated Schema.description for that parameter in the FunctionDeclaration.

  2. Args: docstring section (alternative / complementary) — the per-parameter descriptions already written in Google-style docstrings (Args:\n param: description) should be parsed and also forwarded as Schema.description.

Either approach (or both) would close this gap. The first is more explicit and IDE-friendly; the second requires no additional imports and works with existing codebases.


Why this matters

Per-parameter descriptions are the primary mechanism by which developers can do contextual prompting at the tool level — without polluting the agent system prompt. Concrete use cases:

What the developer wants to express Currently possible?
Specify the expected format of a parameter (e.g. PROJECT-123) No
Tell the model where to source a value (e.g. "use the result of tool X") No
Prevent hallucination ("do NOT invent this value, ask the user") No
Mark a parameter as conditional ("only include if explicitly provided") No

Without this, the only workaround is to embed all such guidance in the top-level tool docstring (which becomes the FunctionDeclaration.description). This conflates tool-level intent with parameter-level constraints, produces a verbose and hard-to-maintain description blob, and — most importantly — is less effective because the model cannot associate a constraint with a specific parameter.


Proposed implementation

The change is localised to _get_fields_dict in _automatic_function_calling_util.py.

Option A — read FieldInfo from Annotated metadata

import inspect
from typing import Annotated, get_args, get_origin
import pydantic
from pydantic import fields as pydantic_fields
from pydantic.fields import FieldInfo

def _extract_field_description(annotation) -> str | None:
    """Return the pydantic Field description from Annotated[T, Field(...)] if present."""
    if get_origin(annotation) is Annotated:
        for metadata in get_args(annotation)[1:]:
            if isinstance(metadata, FieldInfo) and metadata.description:
                return metadata.description
    return None

def _get_fields_dict(func: Callable) -> Dict:
    param_signature = dict(inspect.signature(func).parameters)
    fields_dict = {
        name: (
            (param.annotation if param.annotation != inspect.Parameter.empty else Any),
            pydantic.Field(
                default=(
                    param.default
                    if param.default != inspect.Parameter.empty
                    else pydantic_fields.PydanticUndefined
                ),
                description=_extract_field_description(param.annotation),  # <-- NEW
            ),
        )
        for name, param in param_signature.items()
        if param.kind in (
            inspect.Parameter.POSITIONAL_OR_KEYWORD,
            inspect.Parameter.KEYWORD_ONLY,
            inspect.Parameter.POSITIONAL_ONLY,
        )
    }
    return fields_dict

Annotated[str, Field(description="...")] would need to be unwrapped to its base type (str) before being passed as the annotation to pydantic's create_model, to avoid the current ValueError.

Option B — parse Args: from the docstring

import inspect
import re

def _parse_docstring_param_descriptions(func: Callable) -> dict[str, str]:
    """Extract per-parameter descriptions from a Google-style docstring Args section."""
    doc = inspect.getdoc(func) or ""
    descriptions: dict[str, str] = {}
    in_args = False
    current_param: str | None = None
    current_lines: list[str] = []

    for line in doc.splitlines():
        if re.match(r"^\s*Args\s*:", line):
            in_args = True
            continue
        if in_args and re.match(r"^\s*\w+\s*:", line) and not line.startswith(" " * 8):
            # New top-level section (Returns, Raises, …) — stop
            if current_param:
                descriptions[current_param] = " ".join(current_lines).strip()
            break
        if in_args:
            m = re.match(r"^\s{4}(\w+)\s*:(.*)", line)
            if m:
                if current_param:
                    descriptions[current_param] = " ".join(current_lines).strip()
                current_param = m.group(1)
                current_lines = [m.group(2).strip()]
            elif current_param and line.strip():
                current_lines.append(line.strip())

    if current_param and current_lines:
        descriptions[current_param] = " ".join(current_lines).strip()

    return descriptions

Version range

Confirmed present (unchanged) from v1.0.0 through v1.25.1 (latest at time of writing).

  • File: google/adk/tools/_automatic_function_calling_util.py
  • Function: _get_fields_dict
  • Line (v1.25.1): comment # 3. Do not support parameter description for now. + description=None

Alternatives considered

  • Embed all guidance in the top-level docstring — the current workaround. Functional but unstructured; the model cannot associate a constraint with a specific parameter.
  • Manually build a FunctionDeclaration and wrap it in a BaseTool subclass — possible but defeats the purpose of automatic function calling and requires significant boilerplate per tool.

Additional context

This feature is standard in other tool-calling frameworks:

  • OpenAI function calling — the JSON schema for each parameter accepts a description field natively.
  • LangChain @tool — uses Annotated[T, Field(description="...")] to populate parameter descriptions in the generated schema.
  • Anthropic tool use — the tool input schema is a JSON Schema object where each property can carry a description.

Supporting this in ADK would bring it to parity with the ecosystem and significantly improve the developer experience for production-grade agents.

Metadata

Metadata

Assignees

No one assigned

    Labels

    tools[Component] This issue is related to tools

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions