Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
19 changes: 14 additions & 5 deletions src/uipath_langchain/agent/tools/internal_tools/pii_masker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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:
Expand Down
7 changes: 4 additions & 3 deletions tests/agent/tools/internal_tools/test_pii_masker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading