Skip to content

Python: Fix PydanticSchemaGenerationError when using from __future__ import annotations with @tool#4822

Open
giles17 wants to merge 6 commits intomicrosoft:mainfrom
giles17:agent/fix-4809-1
Open

Python: Fix PydanticSchemaGenerationError when using from __future__ import annotations with @tool#4822
giles17 wants to merge 6 commits intomicrosoft:mainfrom
giles17:agent/fix-4809-1

Conversation

@giles17
Copy link
Contributor

@giles17 giles17 commented Mar 20, 2026

Motivation and Context

When from __future__ import annotations (PEP 563) is active, all annotations become strings. _resolve_input_model passed these raw string annotations directly to Pydantic's create_model, causing PydanticSchemaGenerationError for complex types like Optional[int] and FunctionInvocationContext.

Fixes #4809

Description

The root cause was that _resolve_input_model in _tools.py used param.annotation directly from inspect.signature(), which under PEP 563 returns string representations instead of actual types. The fix mirrors the approach already used in _discover_injected_parameters by calling typing.get_type_hints() to resolve stringified annotations back to real types before passing them to Pydantic's create_model. Regression tests verify that @tool works correctly with PEP 563 for FunctionInvocationContext parameters, Optional types, and full invocation scenarios.

Contribution Checklist

  • The code builds clean without any errors or warnings
  • The PR follows the Contribution Guidelines
  • All unit tests pass, and I have added new tests where possible
  • Is this a breaking change? If yes, add "[BREAKING]" prefix to the title of the PR.

Note: PR autogenerated by giles17's agent

Copilot and others added 3 commits March 20, 2026 22:57
_resolve_input_model used raw param.annotation from inspect.signature(),
which returns string annotations when 'from __future__ import annotations'
is active (PEP 563). This caused Pydantic's create_model to fail for
complex types like Optional[int] or FunctionInvocationContext.

Use typing.get_type_hints() to resolve annotations to actual types before
passing them to create_model, matching the approach already used by
_discover_injected_parameters.

Fixes microsoft#4809

Co-authored-by: Copilot <[email protected]>
@giles17 giles17 self-assigned this Mar 20, 2026
Copilot AI review requested due to automatic review settings March 20, 2026 23:03
Copy link
Contributor Author

@giles17 giles17 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated Code Review

Reviewers: 4 | Confidence: 93%

✓ Correctness

This PR correctly removes unused imports (Optional, pytest, FunctionTool) from test_tools_future_annotations.py and deletes the temporary REPRODUCTION_REPORT.md file. All remaining imports are used in the test file. The test file uses int | None syntax rather than Optional[int], and none of the test functions reference pytest or FunctionTool directly. No correctness issues found.

✓ Security Reliability

This diff is pure cleanup: it removes a temporary investigation artifact (REPRODUCTION_REPORT.md) and three unused imports (Optional, pytest, FunctionTool) from the test file. The test file uses int | None syntax instead of Optional, has no pytest markers/fixtures/assertions, and doesn't reference FunctionTool. The actual fix to _resolve_input_model using typing.get_type_hints() is already present in _tools.py lines 469-472 and is not part of this diff. No security or reliability concerns.

✗ Test Coverage

The test file covers the key PEP 563 scenarios (context parameter exclusion, optional params, invocation) but has weak assertions that don't verify type correctness—the core of the bug. Tests only check parameter presence (e.g., 'limit' in params['properties']) rather than verifying the type was correctly resolved (e.g., checking that limit's schema includes integer/null types). A test that just checks key existence would pass even if types were still unresolved strings. Additionally, there is no test covering the get_type_hints() failure fallback path (the except Exception branch added in _tools.py:471-472), and no test using Optional[X] syntax from typing which was the exact failure in the original issue report.

✓ Design Approach

This diff removes an investigation artifact (REPRODUCTION_REPORT.md) that was committed by mistake and cleans up unused imports in the test file. The test coverage itself is solid: it exercises the four scenarios relevant to the PEP 563 fix (context-param exclusion in two orderings, Optional resolution, and async invocation). The removal of Optional in favour of int | None is consistent with the from __future__ import annotations context and modern Python style. No design issues are present.

Flagged Issues

  • Tests for test_tool_with_optional_param and test_tool_with_optional_param_and_context only assert that parameter keys exist in the schema properties, but do not verify that the types were correctly resolved. This is the core of the bug—types becoming unresolved strings under PEP 563—so the assertions should verify type information (e.g., that limit's schema contains anyOf with integer and null types). Without this, the tests would pass even if the fix were reverted.

Suggestions

  • Add a test that verifies the get_type_hints() failure fallback (the except Exception branch at _tools.py line 471-472), e.g., by using a function with an unresolvable forward reference annotation and confirming it still produces a schema (falling back to raw annotations).
  • Consider adding a test using typing.Optional[SomeCustomClass] syntax in addition to int | None, since Optional[CustomType] was the exact pattern that triggered the original PydanticUserError in the bug report.

Automated review by giles17's agents

@markwallace-microsoft
Copy link
Member

markwallace-microsoft commented Mar 20, 2026

Python Test Coverage

Python Test Coverage Report •
FileStmtsMissCoverMissing
packages/core/agent_framework
   _tools.py9518690%191–192, 367, 369, 382, 411–413, 421, 439, 453, 460, 467, 471–472, 490, 492, 499, 507, 542, 591, 595, 638–640, 642, 648, 693–695, 697, 720, 746, 788–790, 794, 816, 928–934, 970, 982, 984, 986, 989–992, 1013, 1017, 1021, 1035–1037, 1378, 1400, 1487–1493, 1622, 1626, 1672, 1821, 1841, 1843, 1899, 1962, 2146–2147, 2219–2220, 2275, 2348–2349, 2411, 2416, 2423
TOTAL27299322088% 

Python Unit Test Overview

Tests Skipped Failures Errors Time
5354 20 💤 0 ❌ 0 🔥 1m 24s ⏱️

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a Python tooling bug where @tool input-model generation fails under from __future__ import annotations because stringified annotations were being forwarded into Pydantic model creation.

Changes:

  • Resolve tool function annotations via typing.get_type_hints(..., include_extras=True) before building the Pydantic create_model field definitions.
  • Add regression tests covering FunctionInvocationContext exclusion from schema and Optional/union types under PEP 563 string annotations.
  • Add an end-to-end invocation test ensuring FunctionInvocationContext injection still works with future annotations enabled.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
python/packages/core/agent_framework/_tools.py Updates _resolve_input_model to use resolved type hints (instead of raw inspect.signature annotations) when constructing the input model for @tool.
python/packages/core/tests/core/test_tools_future_annotations.py Adds regression tests validating schema generation and invocation behavior with from __future__ import annotations.

Copilot and others added 3 commits March 20, 2026 23:10
…icrosoft#4809)

- Verify type correctness in schema assertions (not just key presence)
- Fix ctx annotation to FunctionInvocationContext | None for type consistency
- Add test for Optional[CustomType] pattern (original bug trigger)
- Add test for get_type_hints() fallback with unresolvable forward refs

Co-authored-by: Copilot <[email protected]>
…hemaGenerationError in FunctionInvocationContext
Copy link
Contributor Author

@giles17 giles17 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated Code Review

Reviewers: 4 | Confidence: 96% | Result: All clear

Reviewed: Correctness, Security Reliability, Test Coverage, Design Approach


Automated review by giles17's agents

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Python: [Bug]: PydanticSchemaGenerationError in FunctionInvocationContext

3 participants