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
5 changes: 5 additions & 0 deletions packages/uipath_langchain_client/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

All notable changes to `uipath_langchain_client` will be documented in this file.

## [1.5.4] - 2026-03-19

### Fix
- Fix bedrock clients with file attachments

## [1.5.3] - 2026-03-18

### Fix
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
__title__ = "UiPath LangChain Client"
__description__ = "A Python client for interacting with UiPath's LLM services via LangChain."
__version__ = "1.5.3"
__version__ = "1.5.4"
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,32 @@
try:
from anthropic import AnthropicBedrock, AsyncAnthropicBedrock
from langchain_aws.chat_models import ChatBedrock, ChatBedrockConverse
from langchain_aws.chat_models import bedrock as _bedrock_module
from langchain_aws.chat_models.anthropic import ChatAnthropicBedrock

from uipath_langchain_client.clients.bedrock.utils import WrappedBotoClient

_original_format_data_content_block = _bedrock_module._format_data_content_block

def _patched_format_data_content_block(block: dict) -> dict:
"""Extended version that also handles file/document blocks for Anthropic API."""
if block["type"] == "file":
if "base64" not in block and block.get("source_type") != "base64":
raise ValueError("File data only supported through in-line base64 format.")
if "mime_type" not in block:
raise ValueError("mime_type key is required for base64 data.")
return {
"type": "document",
"source": {
"type": "base64",
"media_type": block["mime_type"],
"data": block.get("base64") or block.get("data", ""),
},
}
return _original_format_data_content_block(block)

_bedrock_module._format_data_content_block = _patched_format_data_content_block

except ImportError as e:
raise ImportError(
"The 'aws' extra is required to use UiPathBedrockChatModel and UiPathBedrockChatModelConverse. "
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,22 @@ def __init__(self, region_name: str = "PLACEHOLDER"):
self.events = _MockEventHooks()


def _serialize_bytes(obj: Any) -> Any:
"""Recursively encode bytes values to base64 strings for JSON serialization.

This mimics boto3's serializer which re-encodes bytes to base64 before
sending as JSON. Needed because LangChain's ChatBedrockConverse decodes
base64 content (images, PDFs) into raw bytes objects.
"""
if isinstance(obj, bytes):
return base64.b64encode(obj).decode("utf-8")
if isinstance(obj, dict):
return {k: _serialize_bytes(v) for k, v in obj.items()}
if isinstance(obj, list):
return [_serialize_bytes(item) for item in obj]
return obj


class WrappedBotoClient:
def __init__(self, httpx_client: Client | None = None, region_name: str = "PLACEHOLDER"):
self.httpx_client = httpx_client
Expand All @@ -37,7 +53,7 @@ def __init__(self, httpx_client: Client | None = None, region_name: str = "PLACE
def _stream_generator(self, request_body: dict[str, Any]) -> Iterator[dict[str, Any]]:
if self.httpx_client is None:
raise ValueError("httpx_client is not set")
with self.httpx_client.stream("POST", "/", json=request_body) as response:
with self.httpx_client.stream("POST", "/", json=_serialize_bytes(request_body)) as response:
buffer = EventStreamBuffer()
for chunk in response.iter_bytes():
buffer.add_data(chunk)
Expand Down Expand Up @@ -69,11 +85,13 @@ def converse(
raise ValueError("httpx_client is not set")
return self.httpx_client.post(
"/",
json={
"messages": messages,
"system": system,
**params,
},
json=_serialize_bytes(
{
"messages": messages,
"system": system,
**params,
}
),
).json()

def converse_stream(
Expand Down