diff --git a/pyproject.toml b/pyproject.toml index a0053aa9c..277cf622a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-langchain" -version = "0.11.16" +version = "0.11.17" description = "Python SDK that enables developers to build and deploy LangGraph agents to the UiPath Cloud Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/src/uipath_langchain/agent/react/json_utils.py b/src/uipath_langchain/agent/react/json_utils.py index 6c2c77ede..fe1570771 100644 --- a/src/uipath_langchain/agent/react/json_utils.py +++ b/src/uipath_langchain/agent/react/json_utils.py @@ -160,16 +160,18 @@ def _create_type_matcher(type_name: str, target_type: Any) -> Any: """ def matches_type(annotation: Any) -> bool: - """Check if an annotation matches the target type name.""" + """Whether ``annotation`` refers to ``type_name``, by name or identity.""" if isinstance(annotation, ForwardRef): return annotation.__forward_arg__ == type_name if isinstance(annotation, str): return annotation == type_name - if hasattr(annotation, "__name__") and annotation.__name__ == type_name: - return True - if target_type is not None and annotation is target_type: - return True - return False + # prefer the per-class marker: identity/target_type break when several + # dynamic models are built (they share the same module). + return ( + getattr(annotation, "__uipath_marker_name__", None) == type_name + or getattr(annotation, "__name__", None) == type_name + or (target_type is not None and annotation is target_type) + ) return matches_type diff --git a/src/uipath_langchain/agent/react/jsonschema_pydantic_converter.py b/src/uipath_langchain/agent/react/jsonschema_pydantic_converter.py index e0b9fdb6a..ea3e539fa 100644 --- a/src/uipath_langchain/agent/react/jsonschema_pydantic_converter.py +++ b/src/uipath_langchain/agent/react/jsonschema_pydantic_converter.py @@ -1,7 +1,7 @@ import inspect import sys from types import ModuleType -from typing import Any, Type +from typing import Any, Type, cast from jsonschema_pydantic_converter import transform_with_modules from pydantic import BaseModel, PydanticUndefinedAnnotation @@ -53,6 +53,8 @@ def create_model( setattr(pseudo_module, type_name, type_def) if inspect.isclass(type_def) and issubclass(type_def, BaseModel): type_def.__module__ = _DYNAMIC_MODULE_NAME + # per-class marker; survives the shared module being overwritten. + cast(Any, type_def).__uipath_marker_name__ = type_name setattr(pseudo_module, model.__name__, model) model.__module__ = _DYNAMIC_MODULE_NAME diff --git a/tests/agent/react/test_json_utils.py b/tests/agent/react/test_json_utils.py index af01dacb0..f068b6f9f 100644 --- a/tests/agent/react/test_json_utils.py +++ b/tests/agent/react/test_json_utils.py @@ -477,3 +477,28 @@ class Parent(BaseModel): ) assert result["child"]["value"] == '{"x": 1}' assert result["child"]["data"] == {"y": 2} + + +class TestBuildOrderIndependence: + """Detection must not depend on which dynamic model was built last.""" + + _SCHEMA = { + "type": "object", + "properties": {"doc": {"$ref": "#/$defs/Job_attachment"}}, + "$defs": { + "Job_attachment": { + "type": "object", + "properties": {"ID": {"type": "string"}}, + } + }, + } + + def test_first_model_still_detected_after_second_build(self) -> None: + """Building a second attachment model must not blank out the first.""" + first = create_model(self._SCHEMA) + assert get_json_paths_by_type(first, "__Job_attachment") == ["$.doc"] + + # The second build overwrites the shared module's marker; the first + # model's per-class marker keeps its detection working. + create_model(self._SCHEMA) + assert get_json_paths_by_type(first, "__Job_attachment") == ["$.doc"] diff --git a/uv.lock b/uv.lock index a425af33d..3bacdc8ae 100644 --- a/uv.lock +++ b/uv.lock @@ -4388,7 +4388,7 @@ wheels = [ [[package]] name = "uipath-langchain" -version = "0.11.16" +version = "0.11.17" source = { editable = "." } dependencies = [ { name = "a2a-sdk" },