diff --git a/src/uipath_langchain/agent/react/job_attachments.py b/src/uipath_langchain/agent/react/job_attachments.py index 5047f0bd2..13b9c6d91 100644 --- a/src/uipath_langchain/agent/react/job_attachments.py +++ b/src/uipath_langchain/agent/react/job_attachments.py @@ -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], @@ -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 diff --git a/tests/agent/react/test_job_attachments.py b/tests/agent/react/test_job_attachments.py index 0f0629a2d..740dbcad9 100644 --- a/tests/agent/react/test_job_attachments.py +++ b/tests/agent/react/test_job_attachments.py @@ -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."""