Skip to content

feat(tools): support per-parameter descriptions via Annotated[T, Field(description=...)]#4962

Open
ecanlar wants to merge 2 commits intogoogle:mainfrom
ecanlar:feat/support-annotated-field-description
Open

feat(tools): support per-parameter descriptions via Annotated[T, Field(description=...)]#4962
ecanlar wants to merge 2 commits intogoogle:mainfrom
ecanlar:feat/support-annotated-field-description

Conversation

@ecanlar
Copy link

@ecanlar ecanlar commented Mar 23, 2026

Summary

This PR implements Option A from issue #4552, adding support for per-parameter descriptions in FunctionTool using the Annotated type hint with pydantic.Field(description=...).

  • Extracts parameter descriptions from Annotated[T, Field(description=...)] syntax
  • Unwraps base types from Annotated[T, ...] for proper model creation
  • Includes comprehensive unit tests for the new functionality

Motivation

Per-parameter descriptions enable contextual prompting at the tool level without polluting agent system prompts. This allows developers to:

  • Specify expected parameter formats (e.g., PROJECT-123)
  • Tell the model where to source values (e.g., "use the result of tool X")
  • Prevent hallucination ("do NOT invent this value, ask the user")
  • Mark parameters as conditional

Example Usage

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:
    '''Create a new task in the repository.'''
    ...

Changes

Modified Files

  • src/google/adk/tools/_automatic_function_calling_util.py:
    • Added _extract_field_info_from_annotated() helper function
    • Added _extract_base_type_from_annotated() helper function
    • Updated _get_fields_dict() to extract and use parameter descriptions

New Files

  • tests/unittests/tools/test_annotated_parameter_descriptions.py: Comprehensive tests for the new functionality

Testing

All new and existing tests pass:

  • 16 new tests for annotated parameter descriptions
  • 28 existing function tool tests continue to pass

Closes #4552

…d(description=...)]

This change implements Option A from issue google#4552, adding support for
per-parameter descriptions in FunctionTool using the Annotated type hint
with pydantic.Field(description=...).

Changes:
- Add _extract_field_info_from_annotated() to extract FieldInfo from Annotated
- Add _extract_base_type_from_annotated() to unwrap base types from Annotated
- Update _get_fields_dict() to use descriptions from Annotated[T, Field(...)]
- Add comprehensive tests for the new functionality

This enables developers to provide contextual guidance for LLM parameter
selection without embedding all information in the tool docstring:

  from typing import Annotated
  from pydantic import Field

  async def create_task(
      repository: Annotated[str, Field(
          description='Repository URL. MUST be obtained from get_repository_info.'
      )],
  ) -> dict:
      ...

Closes google#4552
@rohityan rohityan self-assigned this Mar 23, 2026
@edpowers
Copy link

edpowers commented Mar 23, 2026

Does this handle nested pydantic objects? I noticed the original PR didn't. If it helps, I can add code for that.

@ecanlar
Copy link
Author

ecanlar commented Mar 24, 2026

Hi @edpowers!

You're absolutely right - I just tested the implementation and confirmed that nested Pydantic objects are NOT handled correctly.

The issue is that Pydantic generates JSON schemas with $ref pointers to $defs for nested BaseModel classes, so the Field descriptions from nested models end up in a separate $defs section rather than inline. This means the LLM/API might not see those descriptions when generating function calls.

Thank you for catching this!

…ptions

This commit adds _resolve_pydantic_refs() to inline nested BaseModel
properties and their Field descriptions, ensuring that LLMs receive
complete parameter documentation even for complex nested structures.

Key changes:
- Add _resolve_pydantic_refs() to resolve $ref pointers from Pydantic schemas
- Integrate reference resolution into _get_pydantic_schema()
- Handle allOf wrappers (Pydantic v2 pattern)
- Support multi-level nesting with circular reference protection
- Preserve parameter-level descriptions over model docstrings
- Add 8 comprehensive tests for nested models including:
  * Single-level nested models
  * Multi-level nested models (e.g., Person -> ContactInfo -> email)
  * List[BaseModel] support
  * Optional[BaseModel] support
  * Mixed simple and nested parameters
  * Circular reference handling

This addresses the limitation identified by @edpowers in PR google#4962 where
nested Pydantic BaseModel Field descriptions were not accessible to LLMs
because they remained in $defs instead of being inlined.
@ecanlar
Copy link
Author

ecanlar commented Mar 24, 2026

Hi @edpowers! 👋

You're absolutely right - I just verified the implementation and nested Pydantic objects were NOT handled correctly in the original PR. Thanks for catching this!

The Problem

Pydantic v2 generates JSON schemas with $ref pointers to $defs for nested BaseModel classes:

{
  "properties": {
    "user": {
      "allOf": [{"$ref": "#/$defs/Person"}],
      "description": "User info"
    }
  },
  "$defs": {
    "Person": {
      "properties": {
        "name": {"description": "Person's full name"},
        "age": {"description": "Person's age in years"}
      }
    }
  }
}

The LLM/API would not see the nested Field descriptions ("Person's full name", "Person's age in years") because they're in a separate $defs section, not inline with the properties.

The Solution

I've just pushed a commit that adds full support for nested Pydantic models!

What I implemented:

  1. _resolve_pydantic_refs() function - Resolves all $ref pointers and inlines nested properties

    • Handles Pydantic v2's allOf wrapper pattern
    • Recursively resolves multi-level nesting (e.g., PersonContactInfoemail)
    • Prevents infinite loops from circular references
    • Preserves parameter-level descriptions over model docstrings
  2. Integration - Modified _get_pydantic_schema() to automatically resolve refs

  3. Comprehensive tests - Added 8 new tests covering:

    • ✅ Single-level nested models
    • ✅ Multi-level nested models (doubly-nested)
    • List[BaseModel] support
    • Optional[BaseModel] support
    • ✅ Mixed simple + nested parameters
    • ✅ Circular reference handling
    • ✅ Full integration with build_function_declaration()

Example - Now this works correctly:

class ContactInfo(BaseModel):
    email: str = Field(description="Email in format [email protected]")
    phone: str = Field(description="Phone with country code")

class Person(BaseModel):
    name: str = Field(description="Person's full name")
    contact: ContactInfo = Field(description="Contact information")

def create_user(
    person: Annotated[Person, Field(description="User personal information")],
) -> dict:
    """Create a new user."""
    pass

The generated FunctionDeclaration will now have all nested descriptions inlined:

  • person.name → "Person's full name" ✅
  • person.contact.email → "Email in format [email protected]" ✅
  • person.contact.phone → "Phone with country code" ✅

Inspiration

The implementation is based on ADK's existing _resolve_references() function in openapi_spec_parser.py, which already handles similar reference resolution for OpenAPI tools. I adapted it for Pydantic's specific schema structure.


Let me know if you'd like me to add any additional test cases or edge cases!

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

Successfully merging this pull request may close these issues.

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

3 participants