Skip to content
Merged
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
101 changes: 94 additions & 7 deletions src/uipath_langchain/agent/react/file_type_handler.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import base64
import re
from enum import StrEnum
from typing import Any

Expand Down Expand Up @@ -49,13 +50,34 @@ def detect_provider(model_name: str) -> LlmProvider:
raise ValueError(f"Unsupported model: {model_name}")


async def _download_file(url: str) -> str:
"""Download a file from a URL and return its content as a base64 string."""
def sanitize_filename_for_anthropic(filename: str) -> str:
"""Sanitize a filename to conform to Anthropic's document naming requirements."""
if not filename or filename.isspace():
return "document"

sanitized = re.sub(r"[^a-zA-Z0-9_\s\-\(\)\[\]\.]", "_", filename)

sanitized = re.sub(r"\s+", " ", sanitized)

sanitized = sanitized.strip()

return sanitized if sanitized else "document"


async def _download_file_bytes(url: str) -> bytes:
"""Download a file from a URL and return its content bytes."""
async with httpx.AsyncClient(**get_httpx_client_kwargs()) as client:
response = await client.get(url)
response.raise_for_status()
file_content = response.content

return file_content


async def _download_file(url: str) -> str:
"""Download a file from a URL and return its content as a base64 string."""
file_content = await _download_file_bytes(url)

return base64.b64encode(file_content).decode("utf-8")


Expand All @@ -72,19 +94,55 @@ async def build_message_content_part_from_data(
provider = detect_provider(model)

if provider == LlmProvider.BEDROCK:
raise ValueError("Anthropic models are not yet supported for file attachments")
return await _build_bedrock_content_part_from_data(url, mime_type, filename)

if provider == LlmProvider.OPENAI:
return await _build_openai_content_part_from_data(
url, mime_type, filename, False
)

if provider == LlmProvider.VERTEX:
raise ValueError("Gemini models are not yet supported for file attachments")
return await _build_vertex_content_part_from_data(url, mime_type, False)

raise ValueError(f"Unsupported provider: {provider}")


async def _build_bedrock_content_part_from_data(
url: str,
mime_type: str,
filename: str,
) -> dict[str, Any]:
"""Build a content part for AWS Bedrock (Anthropic Claude models)."""
if is_pdf(mime_type):
file_bytes = await _download_file_bytes(url)
name = filename.rsplit(".", 1)[0] if "." in filename else filename
sanitized_name = sanitize_filename_for_anthropic(name)
return {
"type": "document",
"document": {
"format": "pdf",
"name": sanitized_name,
"source": {
"bytes": file_bytes,
},
"citations": {"enabled": True},
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how does this impact accuracy/latency/consumption?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and functionality

},
}

if is_image(mime_type):
base64_content = await _download_file(url)
return {
"type": "image",
"source": {
"type": "base64",
"media_type": mime_type,
"data": base64_content,
},
}

raise ValueError(f"Unsupported mime_type: {mime_type}")


async def _build_openai_content_part_from_data(
url: str,
mime_type: str,
Expand All @@ -102,10 +160,13 @@ async def _build_openai_content_part_from_data(
}

if is_pdf(mime_type):
data = f"data:application/pdf;base64,{base64_content}"
return {
"type": "input_file",
"filename": filename,
"file_data": base64_content,
"type": "file",
"file": {
"filename": filename,
"file_data": data,
},
}

elif is_image(mime_type):
Expand All @@ -121,3 +182,29 @@ async def _build_openai_content_part_from_data(
}

raise ValueError(f"Unsupported mime_type: {mime_type}")


async def _build_vertex_content_part_from_data(
url: str,
mime_type: str,
download_file: bool,
) -> dict[str, Any]:
"""Build a content part for Google Vertex AI / Gemini models."""
if download_file:
base64_content = await _download_file(url)
if is_image(mime_type) or is_pdf(mime_type):
return {
"type": "file",
"source_type": "base64",
"mime_type": mime_type,
"data": base64_content,
}

elif is_image(mime_type) or is_pdf(mime_type):
return {
"type": "media",
"file_uri": url,
"mime_type": mime_type,
}

raise ValueError(f"Unsupported mime_type: {mime_type}")