From 3a40e5066055f764074d0c328a262307c9eb3907 Mon Sep 17 00:00:00 2001 From: Yash Wagle Date: Wed, 29 Apr 2026 14:34:21 -0700 Subject: [PATCH] fix: correct the orchestrator api used for file upload --- pyproject.toml | 2 +- .../internal_tools/analyze_files_tool.py | 8 ++++---- .../agent/tools/internal_tools/pii_masker.py | 19 ++++++++++++++----- .../tools/internal_tools/test_pii_masker.py | 7 ++++--- uv.lock | 2 +- 5 files changed, 24 insertions(+), 14 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 84eb00578..bb36cc976 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-langchain" -version = "0.10.10" +version = "0.10.11" 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/tools/internal_tools/analyze_files_tool.py b/src/uipath_langchain/agent/tools/internal_tools/analyze_files_tool.py index 6627400b4..230996eb5 100644 --- a/src/uipath_langchain/agent/tools/internal_tools/analyze_files_tool.py +++ b/src/uipath_langchain/agent/tools/internal_tools/analyze_files_tool.py @@ -40,7 +40,7 @@ from uipath_langchain.agent.react.jsonschema_pydantic_converter import create_model from uipath_langchain.agent.tools.internal_tools.pii_masker import ( PiiMasker, - _masked_name_for, + masked_name_for, ) from uipath_langchain.agent.tools.structured_tool_with_argument_properties import ( StructuredToolWithArgumentProperties, @@ -120,7 +120,7 @@ def _llm_call_attachments_payload(files: list[FileInfo]) -> str | None: att_id = file.masked_attachment_id or _masked_attachment_id( file.masked_attachment_url ) - name = _masked_name_for(file.name) + name = masked_name_for(file.name) else: att_id = _original_attachment_id(file) name = file.name @@ -192,7 +192,7 @@ def _emit_pii_masking_attachments(span: otel_trace.Span, files: list[FileInfo]) masked_id = file.masked_attachment_id or _masked_attachment_id( file.masked_attachment_url ) - masked_name = _masked_name_for(file.name) + masked_name = masked_name_for(file.name) attachments.append( SpanAttachment( id=masked_id, @@ -415,7 +415,7 @@ async def add_files_to_message( llm_file = ( FileInfo( url=file.masked_attachment_url, - name=_masked_name_for(file.name), + name=masked_name_for(file.name), mime_type=file.mime_type, ) if file.masked_attachment_url diff --git a/src/uipath_langchain/agent/tools/internal_tools/pii_masker.py b/src/uipath_langchain/agent/tools/internal_tools/pii_masker.py index 929c5dd8c..48cd9e0a0 100644 --- a/src/uipath_langchain/agent/tools/internal_tools/pii_masker.py +++ b/src/uipath_langchain/agent/tools/internal_tools/pii_masker.py @@ -26,7 +26,7 @@ _FEATURE_FLAG = "FilePiiMaskingEnabled" -def _masked_name_for(name: str) -> str: +def masked_name_for(name: str) -> str: """Apply the ``pii_masked_`` filename prefix for re-uploaded masked files.""" if "." in name: base, ext = name.rsplit(".", 1) @@ -193,8 +193,10 @@ async def _upload_masked_to_orchestrator(self, file: FileInfo) -> str | None: The PII service returns a blob URL that LLMOps has no way to resolve, so clicking the masked attachment in the trace viewer fails. Fetching the - bytes and uploading them via ``client.attachments.upload_async`` gives - us a real orchestrator UUID that the UI knows how to download. + bytes and uploading them via ``client.jobs.create_attachment_async`` + gives us a real orchestrator UUID that the UI knows how to download, + and links the attachment to the current job so it shows up under the + job's attachments (job_key falls back to the running job's instance_key). Returns the uploaded attachment id, or ``None`` on failure (callers fall back to a synthesized uuid5 — the trace still shows the file, just not @@ -206,9 +208,16 @@ async def _upload_masked_to_orchestrator(self, file: FileInfo) -> str | None: content = base64.b64decode( await download_file_base64(file.masked_attachment_url) ) - attachment_key = await self._client.attachments.upload_async( - name=_masked_name_for(file.name), + masked_name = masked_name_for(file.name) + attachment_key = await self._client.jobs.create_attachment_async( + name=masked_name, content=content, + category="pii masked", + ) + logger.info( + "Uploaded masked attachment '%s' as id=%s", + masked_name, + attachment_key, ) return str(attachment_key) except Exception: diff --git a/tests/agent/tools/internal_tools/test_pii_masker.py b/tests/agent/tools/internal_tools/test_pii_masker.py index 8f163d9a3..45dcbbc61 100644 --- a/tests/agent/tools/internal_tools/test_pii_masker.py +++ b/tests/agent/tools/internal_tools/test_pii_masker.py @@ -55,8 +55,8 @@ def _make_client( client = Mock() client.semantic_proxy = Mock() client.semantic_proxy.detect_pii_async = AsyncMock(return_value=response) - client.attachments = Mock() - client.attachments.upload_async = AsyncMock( + client.jobs = Mock() + client.jobs.create_attachment_async = AsyncMock( return_value=upload_result or uuid.uuid4() ) return client @@ -245,9 +245,10 @@ async def test_masks_prompt_and_annotates_files(self, httpx_mock): assert request.files[0].file_name == "doc.pdf" assert request.files[0].file_type == "pdf" - upload_call = client.attachments.upload_async.call_args + upload_call = client.jobs.create_attachment_async.call_args assert upload_call.kwargs["name"] == "pii_masked_doc.pdf" assert upload_call.kwargs["content"] == b"redacted-bytes" + assert upload_call.kwargs["category"] == "pii masked" async def test_returns_original_files_when_no_redactions(self): response = _make_pii_response(masked_prompt="clean prompt") diff --git a/uv.lock b/uv.lock index 911291179..d6bd8be24 100644 --- a/uv.lock +++ b/uv.lock @@ -4375,7 +4375,7 @@ wheels = [ [[package]] name = "uipath-langchain" -version = "0.10.10" +version = "0.10.11" source = { editable = "." } dependencies = [ { name = "a2a-sdk" },