From 9386ecfebadbd4b83c93f65cb10485641c325291 Mon Sep 17 00:00:00 2001 From: radu-mocanu Date: Mon, 12 Jan 2026 21:32:33 +0200 Subject: [PATCH] feat: infer attachments --- pyproject.toml | 6 +- src/uipath_langchain/runtime/schema.py | 129 ++------- tests/runtime/test_schema.py | 379 +++++++++++++++++++------ uv.lock | 28 +- 4 files changed, 336 insertions(+), 206 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f826b132..67968055 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,12 +1,12 @@ [project] name = "uipath-langchain" -version = "0.3.3" +version = "0.3.4" 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" dependencies = [ - "uipath>=2.4.0, <2.5.0", - "uipath-runtime>=0.4.0, <0.5.0", + "uipath>=2.4.14, <2.5.0", + "uipath-runtime>=0.4.1, <0.5.0", "langgraph>=1.0.0, <2.0.0", "langchain-core>=1.2.5, <2.0.0", "aiosqlite==0.21.0", diff --git a/src/uipath_langchain/runtime/schema.py b/src/uipath_langchain/runtime/schema.py index 4e425aff..db7e0367 100644 --- a/src/uipath_langchain/runtime/schema.py +++ b/src/uipath_langchain/runtime/schema.py @@ -14,6 +14,9 @@ UiPathRuntimeEdge, UiPathRuntimeGraph, UiPathRuntimeNode, + transform_attachments, + transform_nullable_types, + transform_references, ) try: @@ -322,120 +325,38 @@ def get_entrypoints_schema( } if hasattr(graph, "input_schema"): - if hasattr(graph.input_schema, "model_json_schema"): - input_schema = graph.input_schema.model_json_schema() - unpacked_ref_def_properties, input_circular_dependency = _resolve_refs( - input_schema - ) + input_schema = graph.get_input_jsonschema() + unpacked_ref_def_properties, input_circular_dependency = transform_references( + input_schema + ) - # Process the schema to handle nullable types - processed_properties = _process_nullable_types( - unpacked_ref_def_properties.get("properties", {}) - ) + # Process the schema to handle nullable types + processed_properties = transform_nullable_types( + unpacked_ref_def_properties.get("properties", {}) + ) - schema["input"]["properties"] = processed_properties - schema["input"]["required"] = unpacked_ref_def_properties.get( - "required", [] - ) + schema["input"]["properties"] = processed_properties + schema["input"]["required"] = unpacked_ref_def_properties.get("required", []) + schema["input"] = transform_attachments(schema["input"]) if hasattr(graph, "output_schema"): - if hasattr(graph.output_schema, "model_json_schema"): - output_schema = graph.output_schema.model_json_schema() - unpacked_ref_def_properties, output_circular_dependency = _resolve_refs( - output_schema - ) + output_schema = graph.get_output_jsonschema() + unpacked_ref_def_properties, output_circular_dependency = transform_references( + output_schema + ) - # Process the schema to handle nullable types - processed_properties = _process_nullable_types( - unpacked_ref_def_properties.get("properties", {}) - ) + # Process the schema to handle nullable types + processed_properties = transform_nullable_types( + unpacked_ref_def_properties.get("properties", {}) + ) - schema["output"]["properties"] = processed_properties - schema["output"]["required"] = unpacked_ref_def_properties.get( - "required", [] - ) + schema["output"]["properties"] = processed_properties + schema["output"]["required"] = unpacked_ref_def_properties.get("required", []) + schema["output"] = transform_attachments(schema["output"]) return SchemaDetails(schema, input_circular_dependency, output_circular_dependency) -def _resolve_refs(schema, root=None, visited=None): - """Recursively resolves $ref references in a JSON schema, handling circular references. - - Returns: - tuple: (resolved_schema, has_circular_dependency) - """ - if root is None: - root = schema - - if visited is None: - visited = set() - - has_circular = False - - if isinstance(schema, dict): - if "$ref" in schema: - ref_path = schema["$ref"] - - if ref_path in visited: - # Circular dependency detected - return { - "type": "object", - "description": f"Circular reference to {ref_path}", - }, True - - visited.add(ref_path) - - # Resolve the reference - ref_parts = ref_path.lstrip("#/").split("/") - ref_schema = root - for part in ref_parts: - ref_schema = ref_schema.get(part, {}) - - result, circular = _resolve_refs(ref_schema, root, visited) - has_circular = has_circular or circular - - # Remove from visited after resolution (allows the same ref in different branches) - visited.discard(ref_path) - - return result, has_circular - - resolved_dict = {} - for k, v in schema.items(): - resolved_value, circular = _resolve_refs(v, root, visited) - resolved_dict[k] = resolved_value - has_circular = has_circular or circular - return resolved_dict, has_circular - - elif isinstance(schema, list): - resolved_list = [] - for item in schema: - resolved_item, circular = _resolve_refs(item, root, visited) - resolved_list.append(resolved_item) - has_circular = has_circular or circular - return resolved_list, has_circular - - return schema, False - - -def _process_nullable_types( - schema: dict[str, Any] | list[Any] | Any, -) -> dict[str, Any] | list[Any]: - """Process the schema to handle nullable types by removing anyOf with null and keeping the base type.""" - if isinstance(schema, dict): - if "anyOf" in schema and len(schema["anyOf"]) == 2: - types = [t.get("type") for t in schema["anyOf"]] - if "null" in types: - non_null_type = next( - t for t in schema["anyOf"] if t.get("type") != "null" - ) - return non_null_type - - return {k: _process_nullable_types(v) for k, v in schema.items()} - elif isinstance(schema, list): - return [_process_nullable_types(item) for item in schema] - return schema - - __all__ = [ "get_graph_schema", "get_entrypoints_schema", diff --git a/tests/runtime/test_schema.py b/tests/runtime/test_schema.py index 571ca7f1..abba4fcd 100644 --- a/tests/runtime/test_schema.py +++ b/tests/runtime/test_schema.py @@ -1,13 +1,32 @@ """Tests for schema utility functions.""" -from unittest.mock import MagicMock +from dataclasses import dataclass, field +from typing import Any +import pytest +from langgraph.graph import END, START, StateGraph from pydantic import BaseModel, Field +from typing_extensions import TypedDict +from uipath.runtime.schema import transform_references -from uipath_langchain.runtime.schema import ( - _resolve_refs, - get_entrypoints_schema, -) +from uipath_langchain.runtime import UiPathLangGraphRuntime + + +def generate_simple_langgraph_graph(input_schema: type, output_schema: type): + # note: type ignore are needed here since mypy can t validate a dynamically created object's type + def node(state: input_schema) -> input_schema: # type: ignore + return state + + builder = StateGraph( + state_schema=input_schema, + input_schema=input_schema, + output_schema=output_schema, + ) # type: ignore + builder.add_node("node", node) + builder.add_edge(START, "node") + builder.add_edge("node", END) + graph = builder.compile() + return graph class TestResolveRefs: @@ -20,7 +39,7 @@ def test_simple_schema_without_refs(self): "properties": {"name": {"type": "string"}, "age": {"type": "integer"}}, } - result, has_circular = _resolve_refs(schema) + result, has_circular = transform_references(schema) assert result == schema assert has_circular is False @@ -37,7 +56,7 @@ def test_simple_ref_resolution(self): }, } - result, has_circular = _resolve_refs(schema) + result, has_circular = transform_references(schema) assert result["properties"]["user"]["type"] == "object" assert result["properties"]["user"]["properties"]["name"]["type"] == "string" @@ -58,7 +77,7 @@ def test_circular_dependency_detection(self): }, } - result, has_circular = _resolve_refs(schema) + result, has_circular = transform_references(schema) assert has_circular is True # Check that circular ref was replaced with simplified schema @@ -87,7 +106,7 @@ def test_nested_refs_in_properties(self): }, } - result, has_circular = _resolve_refs(schema) + result, has_circular = transform_references(schema) assert result["properties"]["person"]["type"] == "object" assert result["properties"]["person"]["properties"]["name"]["type"] == "string" @@ -114,7 +133,7 @@ def test_refs_in_arrays(self): }, } - result, has_circular = _resolve_refs(schema) + result, has_circular = transform_references(schema) assert result["properties"]["users"]["items"]["type"] == "object" assert ( @@ -140,7 +159,7 @@ def test_multiple_circular_dependencies(self): }, } - result, has_circular = _resolve_refs(schema) + result, has_circular = transform_references(schema) assert has_circular is True @@ -148,28 +167,7 @@ def test_multiple_circular_dependencies(self): class TestGenerateSchemaFromGraph: """Tests for the generate_schema_from_graph function.""" - def test_graph_without_schemas(self): - """Should return empty schemas when graph has no input/output schemas.""" - mock_graph = MagicMock() - del mock_graph.input_schema - del mock_graph.output_schema - - result = get_entrypoints_schema(mock_graph) - - assert result.schema["input"] == { - "type": "object", - "properties": {}, - "required": [], - } - assert result.schema["output"] == { - "type": "object", - "properties": {}, - "required": [], - } - assert result.has_input_circular_dependency is False - assert result.has_output_circular_dependency is False - - def test_graph_with_simple_schemas(self): + async def test_graph_with_simple_schemas(self): """Should extract input and output schemas from graph.""" class InputModel(BaseModel): @@ -179,88 +177,289 @@ class InputModel(BaseModel): class OutputModel(BaseModel): response: str = Field(description="Agent response") - mock_graph = MagicMock() - mock_graph.input_schema = InputModel - mock_graph.output_schema = OutputModel + runtime = UiPathLangGraphRuntime( + graph=generate_simple_langgraph_graph( + input_schema=InputModel, output_schema=OutputModel + ), + entrypoint="test_entrypoint", + ) - result = get_entrypoints_schema(mock_graph) + entire_schema = await runtime.get_schema() + input_schema = entire_schema.input + output_schema = entire_schema.output - assert "query" in result.schema["input"]["properties"] - assert "max_results" in result.schema["input"]["properties"] - assert result.schema["input"]["properties"]["query"]["type"] == "string" - assert "response" in result.schema["output"]["properties"] - assert result.schema["output"]["properties"]["response"]["type"] == "string" - assert result.has_input_circular_dependency is False - assert result.has_output_circular_dependency is False + assert "query" in input_schema["properties"] + assert "max_results" in input_schema["properties"] + assert input_schema["properties"]["query"]["type"] == "string" + assert "response" in output_schema["properties"] + assert output_schema["properties"]["response"]["type"] == "string" - def test_graph_with_circular_input_schema(self): - """Should detect circular dependencies in input schema.""" + async def test_graph_with_complex_pydantic_schemas(self): + """Should extract complex nested input and output schemas from graph.""" - class NodeInput(BaseModel): - value: str - children: list["NodeInput"] = Field(default_factory=list) + class Address(BaseModel): + street: str + city: str + zip_code: str | None = None - mock_graph = MagicMock() - mock_graph.input_schema = NodeInput - del mock_graph.output_schema + class User(BaseModel): + name: str + age: int + email: str | None = None + addresses: list[Address] = Field(default_factory=list) - result = get_entrypoints_schema(mock_graph) + class InputModel(BaseModel): + user: User + tags: list[str] = Field(default_factory=list) + metadata: dict[str, Any] = Field(default_factory=dict) + priority: int = Field(default=5, ge=1, le=10) - assert result.has_input_circular_dependency is True - assert result.has_output_circular_dependency is False - assert "value" in result.schema["input"]["properties"] + class ResultItem(BaseModel): + id: str + score: float + data: dict[str, Any] - def test_graph_with_circular_output_schema(self): - """Should detect circular dependencies in output schema.""" + class OutputModel(BaseModel): + results: list[ResultItem] + total_count: int + success: bool = True + + runtime = UiPathLangGraphRuntime( + graph=generate_simple_langgraph_graph( + input_schema=InputModel, output_schema=OutputModel + ), + entrypoint="test_entrypoint", + ) - class TreeOutput(BaseModel): + entire_schema = await runtime.get_schema() + input_schema = entire_schema.input + output_schema = entire_schema.output + + assert "user" in input_schema["properties"] + assert "tags" in input_schema["properties"] + assert "metadata" in input_schema["properties"] + assert "priority" in input_schema["properties"] + assert input_schema["properties"]["tags"]["type"] == "array" + assert input_schema["properties"]["metadata"]["type"] == "object" + + assert "results" in output_schema["properties"] + assert "total_count" in output_schema["properties"] + assert "success" in output_schema["properties"] + assert output_schema["properties"]["results"]["type"] == "array" + assert output_schema["properties"]["total_count"]["type"] == "integer" + + async def test_graph_with_complex_dataclass_schemas(self): + """Should extract complex nested dataclass input and output schemas from graph.""" + + @dataclass + class Address: + street: str + city: str + zip_code: str | None = None + + @dataclass + class User: name: str - parent: "TreeOutput | None" = None + age: int + email: str | None = None + addresses: list[Address] = field(default_factory=list) + + @dataclass + class InputModel: + user: User + tags: list[str] = field(default_factory=list) + metadata: dict[str, Any] = field(default_factory=dict) + priority: int = 5 + + @dataclass + class ResultItem: + id: str + score: float + data: dict[str, Any] + + @dataclass + class OutputModel: + results: list[ResultItem] + total_count: int + success: bool = True + + runtime = UiPathLangGraphRuntime( + graph=generate_simple_langgraph_graph( + input_schema=InputModel, output_schema=OutputModel + ), + entrypoint="test_entrypoint", + ) - mock_graph = MagicMock() - del mock_graph.input_schema - mock_graph.output_schema = TreeOutput + entire_schema = await runtime.get_schema() + input_schema = entire_schema.input + output_schema = entire_schema.output - result = get_entrypoints_schema(mock_graph) + assert "user" in input_schema["properties"] + assert "tags" in input_schema["properties"] + assert "metadata" in input_schema["properties"] + assert "priority" in input_schema["properties"] + assert input_schema["properties"]["tags"]["type"] == "array" + assert input_schema["properties"]["metadata"]["type"] == "object" - assert result.has_input_circular_dependency is False - assert result.has_output_circular_dependency is True - assert "name" in result.schema["output"]["properties"] + assert "results" in output_schema["properties"] + assert "total_count" in output_schema["properties"] + assert "success" in output_schema["properties"] + assert output_schema["properties"]["results"]["type"] == "array" + assert output_schema["properties"]["total_count"]["type"] == "integer" - def test_graph_with_both_circular_schemas(self): - """Should detect circular dependencies in both input and output schemas.""" + async def test_graph_with_complex_typeddict_schemas(self): + """Should extract complex nested TypedDict input and output schemas from graph.""" - class CircularInput(BaseModel): - data: str - ref: "CircularInput | None" = None + class Address(TypedDict): + street: str + city: str + zip_code: str | None - class CircularOutput(BaseModel): - result: str - next: "CircularOutput | None" = None + class User(TypedDict): + name: str + age: int + email: str | None + addresses: list[Address] + + class InputModel(TypedDict): + user: User + tags: list[str] + metadata: dict[str, Any] + priority: int + + class ResultItem(TypedDict): + id: str + score: float + data: dict[str, Any] + + class OutputModel(TypedDict): + results: list[ResultItem] + total_count: int + success: bool + + runtime = UiPathLangGraphRuntime( + graph=generate_simple_langgraph_graph( + input_schema=InputModel, output_schema=OutputModel + ), + entrypoint="test_entrypoint", + ) - mock_graph = MagicMock() - mock_graph.input_schema = CircularInput - mock_graph.output_schema = CircularOutput + entire_schema = await runtime.get_schema() + input_schema = entire_schema.input + output_schema = entire_schema.output - result = get_entrypoints_schema(mock_graph) + assert "user" in input_schema["properties"] + assert "tags" in input_schema["properties"] + assert "metadata" in input_schema["properties"] + assert "priority" in input_schema["properties"] + assert input_schema["properties"]["tags"]["type"] == "array" + assert input_schema["properties"]["metadata"]["type"] == "object" - assert result.has_input_circular_dependency is True - assert result.has_output_circular_dependency is True - assert "data" in result.schema["input"]["properties"] - assert "result" in result.schema["output"]["properties"] + assert "results" in output_schema["properties"] + assert "total_count" in output_schema["properties"] + assert "success" in output_schema["properties"] + assert output_schema["properties"]["results"]["type"] == "array" + assert output_schema["properties"]["total_count"]["type"] == "integer" - def test_graph_with_required_fields(self): + async def test_graph_with_required_fields(self): """Should extract required fields from schemas.""" class StrictModel(BaseModel): required_field: str optional_field: str | None = None - mock_graph = MagicMock() - mock_graph.input_schema = StrictModel - del mock_graph.output_schema + runtime = UiPathLangGraphRuntime( + graph=generate_simple_langgraph_graph( + input_schema=StrictModel, output_schema=StrictModel + ), + entrypoint="test_entrypoint", + ) + + entire_schema = await runtime.get_schema() + input_schema = entire_schema.input + output_schema = entire_schema.output - result = get_entrypoints_schema(mock_graph) + assert "required_field" in input_schema["required"] + assert "optional_field" not in input_schema["required"] + + assert "required_field" in output_schema["required"] + assert "optional_field" not in output_schema["required"] + + +class TestSchemaGeneration: + @pytest.mark.parametrize( + "input_model_code", + [ + """ +# pydantic BaseModel + +from pydantic import BaseModel, Field +from uipath.platform.attachments import Attachment + +class InputModel(BaseModel): + input_file: Attachment + other_field: int | None = Field(default=None)""", + """ +# dataclass + +from uipath.platform.attachments import Attachment +from dataclasses import dataclass +@dataclass +class InputModel: + input_file: Attachment + other_field: int | None = None""", + """ +# TypedDict + +from typing_extensions import TypedDict +from typing_extensions import NotRequired +from uipath.platform.attachments import Attachment +class InputModel(TypedDict): + input_file: Attachment + other_field: NotRequired[int | None] + """, + ], + ) + async def test_schema_generation_resolves_attachments( + self, input_model_code: str + ) -> None: + """Test that attachments are resolved in runtime schema""" + + # execute model code to get its schema + exec_globals: dict[str, Any] = {} + exec(input_model_code, exec_globals) + InputModel = exec_globals["InputModel"] + + runtime = UiPathLangGraphRuntime( + graph=generate_simple_langgraph_graph( + input_schema=InputModel, output_schema=InputModel + ), + entrypoint="test_entrypoint", + ) - assert "required_field" in result.schema["input"]["required"] - assert "optional_field" not in result.schema["input"]["required"] + def check_attachment_in_schema(schema: dict[str, Any]): + assert "definitions" in schema + assert "job-attachment" in schema["definitions"] + assert schema["definitions"]["job-attachment"]["type"] == "object" + assert ( + schema["definitions"]["job-attachment"]["x-uipath-resource-kind"] + == "JobAttachment" + ) + assert all( + prop_name in schema["definitions"]["job-attachment"]["properties"] + for prop_name in ["ID", "FullName", "MimeType", "Metadata"] + ) + + assert len(schema["properties"]) == 2 + assert all( + prop_name in schema["properties"] + for prop_name in ["input_file", "other_field"] + ) + assert schema["required"] == ["input_file"] + + entire_schema = await runtime.get_schema() + + input_schema = entire_schema.input + output_schema = entire_schema.output + check_attachment_in_schema(input_schema) + check_attachment_in_schema(output_schema) diff --git a/uv.lock b/uv.lock index 812eed82..dc389255 100644 --- a/uv.lock +++ b/uv.lock @@ -166,6 +166,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7f/9c/36c5c37947ebfb8c7f22e0eb6e4d188ee2d53aa3880f3f2744fb894f0cb1/anyio-4.12.0-py3-none-any.whl", hash = "sha256:dad2376a628f98eeca4881fc56cd06affd18f659b17a747d3ff0307ced94b1bb", size = 113362, upload-time = "2025-11-28T23:36:57.897Z" }, ] +[[package]] +name = "applicationinsights" +version = "0.11.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/f2/46a75ac6096d60da0e71a068015b610206e697de01fa2fb5bba8564b0798/applicationinsights-0.11.10.tar.gz", hash = "sha256:0b761f3ef0680acf4731906dfc1807faa6f2a57168ae74592db0084a6099f7b3", size = 44722, upload-time = "2021-04-22T23:22:45.71Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/0d/cb6b23164eb55eebaa5f9f302dfe557cfa751bd7b2779863f1abd0343b6b/applicationinsights-0.11.10-py2.py3-none-any.whl", hash = "sha256:e89a890db1c6906b6a7d0bcfd617dac83974773c64573147c8d6654f9cf2a6ea", size = 55068, upload-time = "2021-04-22T23:22:44.451Z" }, +] + [[package]] name = "attrs" version = "25.4.0" @@ -3241,9 +3250,10 @@ wheels = [ [[package]] name = "uipath" -version = "2.4.0" +version = "2.4.14" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "applicationinsights" }, { name = "click" }, { name = "coverage" }, { name = "httpx" }, @@ -3261,9 +3271,9 @@ dependencies = [ { name = "uipath-core" }, { name = "uipath-runtime" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/99/d1/939761d99f8f4a85bf25e5bcd2649ff6e52d1703f7911ef135633fbeaa16/uipath-2.4.0.tar.gz", hash = "sha256:1eacb14c53a7dad2505baf05e73482667681d53253691ddafa20d316898a20b3", size = 3410258, upload-time = "2026-01-03T06:17:23.776Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2b/a7/31ab1ead2d40bf966476a888803f8b4d213b7248a2259aa2cf1ebd05f7b9/uipath-2.4.14.tar.gz", hash = "sha256:8fa1fd39c87ea61fccf7dd61add0ba574912ea2b2b600f33baf433f651759b19", size = 3653781, upload-time = "2026-01-13T16:30:43.236Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/da/f722c3320262e9a13fda0d381608e36683bc09c19a07490be3697d28f4c8/uipath-2.4.0-py3-none-any.whl", hash = "sha256:deef0dca5fde9f6a48651f27c672e34f6c7cc1c41ffbd281a4a16134ffb813dc", size = 402801, upload-time = "2026-01-03T06:17:21.822Z" }, + { url = "https://files.pythonhosted.org/packages/97/f0/321e006b7af7a84195332a7d40f26ed0d07812c92512b90aa38fd2d1e827/uipath-2.4.14-py3-none-any.whl", hash = "sha256:613f4af6ed5823c2e60e973364e47798e8d187393290fb1f95d7267455b42614", size = 431605, upload-time = "2026-01-13T16:30:40.989Z" }, ] [[package]] @@ -3282,7 +3292,7 @@ wheels = [ [[package]] name = "uipath-langchain" -version = "0.3.3" +version = "0.3.4" source = { editable = "." } dependencies = [ { name = "aiosqlite" }, @@ -3347,8 +3357,8 @@ requires-dist = [ { name = "openinference-instrumentation-langchain", specifier = ">=0.1.56" }, { name = "pydantic-settings", specifier = ">=2.6.0" }, { name = "python-dotenv", specifier = ">=1.0.1" }, - { name = "uipath", specifier = ">=2.4.0,<2.5.0" }, - { name = "uipath-runtime", specifier = ">=0.4.0,<0.5.0" }, + { name = "uipath", specifier = ">=2.4.14,<2.5.0" }, + { name = "uipath-runtime", specifier = ">=0.4.1,<0.5.0" }, ] provides-extras = ["vertex", "bedrock"] @@ -3368,14 +3378,14 @@ dev = [ [[package]] name = "uipath-runtime" -version = "0.4.0" +version = "0.4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "uipath-core" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/78/6f/683b258720c18f8ec0e68ec712a05f42ede6ecf63e75710aa555b8d52092/uipath_runtime-0.4.0.tar.gz", hash = "sha256:129933b08c6f589d13c2c0e7045ddf61ca144029340c1482134d127dd15563e3", size = 99934, upload-time = "2026-01-03T05:44:33.712Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/2a/8373a1c1118442b000c5a89e864a61e8548e6e1575c30fb21501b0e60652/uipath_runtime-0.4.1.tar.gz", hash = "sha256:ddcb26c02833993432a4c19c3306a55858a14afecaffcf32195601564bb44585", size = 102875, upload-time = "2026-01-13T13:34:50.803Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/45/46/402708653a197c7f0b1d9de66b235f8a5798f814c775bab575cd2d7e2539/uipath_runtime-0.4.0-py3-none-any.whl", hash = "sha256:f49a23ed24f7cfaa736f99a5763bcf314234c67b727c40ec891a0a3d10140027", size = 38359, upload-time = "2026-01-03T05:44:31.817Z" }, + { url = "https://files.pythonhosted.org/packages/4a/58/14c89ba528c4e69683d0e19b43026bc8102dab02ec66c4a0d9f2a0fc4ae9/uipath_runtime-0.4.1-py3-none-any.whl", hash = "sha256:b10c7072246066c8e525eb602a9e04f5497a7da6af871d1bd27693fb9e910d7b", size = 39834, upload-time = "2026-01-13T13:34:49.421Z" }, ] [[package]]