From 516d74b6dfa8d2e1ca4f48f3ffcf746fd16c8281 Mon Sep 17 00:00:00 2001 From: Mason Daugherty Date: Tue, 16 Dec 2025 14:08:01 -0500 Subject: [PATCH] fix(core): use `get_type_hints` for Python 3.14 `TypedDict` compatibility (#34390) Replace direct `__annotations__` access with `get_type_hints()` in `_convert_any_typed_dicts_to_pydantic` to handle [PEP 649](https://peps.python.org/pep-0649/) deferred annotations in Python 3.14: > [`Changed in version 3.14: Annotations are now lazily evaluated by default`](https://docs.python.org/3/reference/compound_stmts.html#annotations) Before: ```python class MyTool(TypedDict): name: str MyTool.__annotations__ # {'name': 'str'} - string, not type issubclass('str', ...) # TypeError: arg 1 must be a class ``` After: ```python get_type_hints(MyTool) # {'name': } - actual type ``` Fixes #34291 --- libs/core/langchain_core/tools/base.py | 3 ++- libs/core/langchain_core/utils/function_calling.py | 13 +++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/libs/core/langchain_core/tools/base.py b/libs/core/langchain_core/tools/base.py index ad770fea628..94f9e10e5a5 100644 --- a/libs/core/langchain_core/tools/base.py +++ b/libs/core/langchain_core/tools/base.py @@ -22,6 +22,7 @@ from typing import ( get_type_hints, ) +import typing_extensions from pydantic import ( BaseModel, ConfigDict, @@ -94,7 +95,7 @@ def _is_annotated_type(typ: type[Any]) -> bool: Returns: `True` if the type is an Annotated type, `False` otherwise. """ - return get_origin(typ) is typing.Annotated + return get_origin(typ) in (typing.Annotated, typing_extensions.Annotated) def _get_annotation_description(arg_type: type) -> str | None: diff --git a/libs/core/langchain_core/utils/function_calling.py b/libs/core/langchain_core/utils/function_calling.py index e64389a8200..32c20dc85de 100644 --- a/libs/core/langchain_core/utils/function_calling.py +++ b/libs/core/langchain_core/utils/function_calling.py @@ -18,8 +18,10 @@ from typing import ( cast, get_args, get_origin, + get_type_hints, ) +import typing_extensions from pydantic import BaseModel from pydantic.v1 import BaseModel as BaseModelV1 from pydantic.v1 import Field as Field_v1 @@ -232,13 +234,20 @@ def _convert_any_typed_dicts_to_pydantic( if is_typeddict(type_): typed_dict = type_ docstring = inspect.getdoc(typed_dict) - annotations_ = typed_dict.__annotations__ + # Use get_type_hints to properly resolve forward references and + # string annotations in Python 3.14+ (PEP 649 deferred annotations). + # include_extras=True preserves Annotated metadata. + try: + annotations_ = get_type_hints(typed_dict, include_extras=True) + except Exception: + # Fallback for edge cases where get_type_hints might fail + annotations_ = typed_dict.__annotations__ description, arg_descriptions = _parse_google_docstring( docstring, list(annotations_) ) fields: dict = {} for arg, arg_type in annotations_.items(): - if get_origin(arg_type) is Annotated: # type: ignore[comparison-overlap] + if get_origin(arg_type) in (Annotated, typing_extensions.Annotated): annotated_args = get_args(arg_type) new_arg_type = _convert_any_typed_dicts_to_pydantic( annotated_args[0], depth=depth + 1, visited=visited