Skip to content
Open
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
25 changes: 19 additions & 6 deletions src/uipath_langchain/agent/react/job_attachments.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
"""Job attachment utilities for ReAct Agent."""

import copy
import logging
import uuid
from typing import Any, Sequence

from jsonpath_ng import parse # type: ignore[import-untyped]
from langchain_core.messages import BaseMessage, HumanMessage
from pydantic import BaseModel
from pydantic import BaseModel, ValidationError
from uipath.platform.attachments import Attachment

from .json_utils import extract_values_by_paths, get_json_paths_by_type

logger = logging.getLogger(__name__)


def get_job_attachments(
schema: type[BaseModel],
Expand All @@ -28,11 +31,21 @@ def get_job_attachments(
job_attachment_paths = get_job_attachment_paths(schema)
job_attachments = extract_values_by_paths(data, job_attachment_paths)

result = [
Attachment.model_validate(att, from_attributes=True)
for att in job_attachments
if att
]
result: list[Attachment] = []
for att in job_attachments:
if not att:
continue
try:
result.append(Attachment.model_validate(att, from_attributes=True))
except ValidationError:
att_id = att.get("ID", "unknown") if isinstance(att, dict) else "unknown"
logger.warning(
"Skipping invalid job attachment (ID=%s): "
"could not validate as Attachment. "
"This may happen when file input IDs have not been "
"resolved to Orchestrator UUIDs.",
att_id,
)

return result

Expand Down
117 changes: 117 additions & 0 deletions tests/agent/react/test_job_attachments.py
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,123 @@ def test_filters_out_none_attachments_in_array(self):
assert str(result[1].id) == uuid2
assert result[1].full_name == "file2.docx"

def test_skips_attachment_with_non_uuid_id(self):
"""Regression: should skip attachments with non-UUID IDs instead of crashing.

This tests the fix for AE-1071 where eval set file inputs stored as
StudioWeb paths (e.g., 'evaluationFiles/doc.pdf') would crash the entire
eval run because Attachment.model_validate requires UUID format for ID.
"""
schema = {
"type": "object",
"properties": {"attachment": {"$ref": "#/definitions/job-attachment"}},
"definitions": {
"job-attachment": {
"type": "object",
"properties": {
"ID": {"type": "string"},
"FullName": {"type": "string"},
"MimeType": {"type": "string"},
},
"required": ["ID"],
}
},
}
model = create_model(schema)
data = {
"attachment": {
"ID": "evaluationFiles/document.pdf",
"FullName": "document.pdf",
"MimeType": "application/pdf",
}
}

result = get_job_attachments(model, data)

# Should return empty list instead of crashing
assert result == []

def test_skips_invalid_uuid_keeps_valid_ones(self):
"""Regression: should skip only invalid attachments, keep valid ones."""
schema = {
"type": "object",
"properties": {
"attachments": {
"type": "array",
"items": {"$ref": "#/definitions/job-attachment"},
}
},
"definitions": {
"job-attachment": {
"type": "object",
"properties": {
"ID": {"type": "string"},
"FullName": {"type": "string"},
"MimeType": {"type": "string"},
},
"required": ["ID"],
}
},
}
model = create_model(schema)
valid_uuid = "550e8400-e29b-41d4-a716-446655440000"
data = {
"attachments": [
{
"ID": valid_uuid,
"FullName": "valid.pdf",
"MimeType": "application/pdf",
},
{
"ID": "evaluationFiles/invalid.pdf",
"FullName": "invalid.pdf",
"MimeType": "application/pdf",
},
{
"ID": "not-a-uuid-at-all",
"FullName": "also_invalid.pdf",
"MimeType": "application/pdf",
},
]
}

result = get_job_attachments(model, data)

# Should only return the valid attachment
assert len(result) == 1
assert str(result[0].id) == valid_uuid
assert result[0].full_name == "valid.pdf"

def test_skips_attachment_missing_required_fields(self):
"""Regression: should skip attachments missing required Attachment fields."""
schema = {
"type": "object",
"properties": {"attachment": {"$ref": "#/definitions/job-attachment"}},
"definitions": {
"job-attachment": {
"type": "object",
"properties": {
"ID": {"type": "string"},
"FullName": {"type": "string"},
"MimeType": {"type": "string"},
},
"required": ["ID"],
}
},
}
model = create_model(schema)
# Missing FullName and MimeType which are required by Attachment model
data = {
"attachment": {
"ID": "550e8400-e29b-41d4-a716-446655440000",
}
}

result = get_job_attachments(model, data)

# Should skip instead of crashing (Attachment requires full_name and mime_type)
assert result == []


class TestMergeDicts:
"""Test dictionary merging."""
Expand Down