Skip to content

Commit 23dd8a4

Browse files
authored
fix: cas-langchain internal mapping not including ai/tool messages (#575)
1 parent d5c3a80 commit 23dd8a4

4 files changed

Lines changed: 607 additions & 59 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "uipath-langchain"
3-
version = "0.5.57"
3+
version = "0.5.58"
44
description = "Python SDK that enables developers to build and deploy LangGraph agents to the UiPath Cloud Platform"
55
readme = { file = "README.md", content-type = "text/markdown" }
66
requires-python = ">=3.11"

src/uipath_langchain/runtime/messages.py

Lines changed: 102 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,16 @@
55
from typing import Any, cast
66

77
from langchain_core.messages import (
8+
AIMessage,
89
AIMessageChunk,
910
BaseMessage,
11+
ContentBlock,
1012
HumanMessage,
1113
TextContentBlock,
1214
ToolCall,
1315
ToolMessage,
1416
)
17+
from langchain_core.messages.content import create_text_block
1518
from pydantic import ValidationError
1619
from uipath.core.chat import (
1720
UiPathConversationContentPartChunkEvent,
@@ -66,7 +69,7 @@ def map_messages(self, messages: list[Any]) -> list[Any]:
6669
"""Normalize any 'messages' list into LangChain messages.
6770
6871
- If already BaseMessage instances: return as-is.
69-
- If UiPathConversationMessage: convert to HumanMessage.
72+
- If UiPathConversationMessage: convert to LangChain message
7073
"""
7174
if not isinstance(messages, list):
7275
raise TypeError("messages must be a list")
@@ -104,46 +107,108 @@ def map_messages(self, messages: list[Any]) -> list[Any]:
104107

105108
def _map_messages_internal(
106109
self, messages: list[UiPathConversationMessage]
107-
) -> list[HumanMessage]:
110+
) -> list[BaseMessage]:
108111
"""
109-
Converts a UiPathConversationMessage into a list of HumanMessages for LangGraph.
110-
Supports multimodal content parts (text, external content) and preserves metadata.
112+
Converts UiPathConversationMessage list to LangChain messages (UserMessage/AIMessage/ToolMessage list).
113+
- All content parts are combined into content_blocks
114+
- Tool calls are converted to LangChain ToolCall format, with results stored as ToolMessage
115+
- Metadata includes message_id, role, timestamps
111116
"""
112-
human_messages: list[HumanMessage] = []
113-
114-
for uipath_msg in messages:
115-
# Loop over each content part
116-
if uipath_msg.content_parts:
117-
for part in uipath_msg.content_parts:
118-
data = part.data
119-
content = ""
120-
metadata: dict[str, Any] = {
121-
"message_id": uipath_msg.message_id,
122-
"content_part_id": part.content_part_id,
123-
"mime_type": part.mime_type,
124-
"created_at": uipath_msg.created_at,
125-
"updated_at": uipath_msg.updated_at,
126-
}
127-
128-
if isinstance(data, UiPathInlineValue):
129-
content = str(data.inline)
130-
131-
# Append a HumanMessage for this content part
132-
human_messages.append(
133-
HumanMessage(content=content, metadata=metadata)
117+
converted_messages: list[BaseMessage] = []
118+
119+
for uipath_message in messages:
120+
content_blocks: list[ContentBlock] = []
121+
122+
# Convert content_parts to content_blocks
123+
# TODO: Convert file-attachment content-parts to content_blocks as well
124+
if uipath_message.content_parts:
125+
for uipath_content_part in uipath_message.content_parts:
126+
data = uipath_content_part.data
127+
if uipath_content_part.mime_type.startswith("text/") and isinstance(
128+
data, UiPathInlineValue
129+
):
130+
text = str(data.inline)
131+
if text:
132+
content_blocks.append(
133+
create_text_block(
134+
text, id=uipath_content_part.content_part_id
135+
)
136+
)
137+
138+
# Metadata for the user/assistant message
139+
metadata = {
140+
"message_id": uipath_message.message_id,
141+
"created_at": uipath_message.created_at,
142+
"updated_at": uipath_message.updated_at,
143+
}
144+
145+
role = uipath_message.role
146+
if role == "user":
147+
converted_messages.append(
148+
HumanMessage(
149+
id=uipath_message.message_id,
150+
content_blocks=content_blocks,
151+
additional_kwargs=metadata,
134152
)
153+
)
135154

136-
# Handle the case where there are no content parts
137-
else:
138-
metadata = {
139-
"message_id": uipath_msg.message_id,
140-
"role": uipath_msg.role,
141-
"created_at": uipath_msg.created_at,
142-
"updated_at": uipath_msg.updated_at,
143-
}
144-
human_messages.append(HumanMessage(content="", metadata=metadata))
145-
146-
return human_messages
155+
elif role == "assistant":
156+
# Convert tool calls to LangChain format
157+
tool_calls: list[ToolCall] = []
158+
tool_messages: list[ToolMessage] = []
159+
if uipath_message.tool_calls:
160+
for uipath_tool_call in uipath_message.tool_calls:
161+
tool_call = ToolCall(
162+
name=uipath_tool_call.name.replace(" ", "_"),
163+
args=uipath_tool_call.input or {},
164+
id=uipath_tool_call.tool_call_id,
165+
)
166+
tool_calls.append(tool_call)
167+
168+
tool_call_output = (
169+
uipath_tool_call.result.output
170+
if uipath_tool_call.result
171+
else None
172+
)
173+
tool_call_status = (
174+
"success"
175+
if uipath_tool_call.result
176+
and not uipath_tool_call.result.is_error
177+
else "error"
178+
)
179+
180+
# Serialize output to string if needed
181+
if tool_call_output is None:
182+
content = ""
183+
elif isinstance(tool_call_output, str):
184+
content = tool_call_output
185+
else:
186+
content = json.dumps(tool_call_output)
187+
188+
tool_messages.append(
189+
ToolMessage(
190+
content=content,
191+
status=tool_call_status,
192+
tool_call_id=uipath_tool_call.tool_call_id,
193+
)
194+
)
195+
196+
# Ideally we pass in content_blocks here rather than string content, but when doing so, OpenAI errors unless a msg_ prefix is used for content-block IDs.
197+
# When needed, we can switch to content_blocks but need to work out a common ID strategy across models for the content-block IDs.
198+
converted_messages.append(
199+
AIMessage(
200+
id=uipath_message.message_id,
201+
# content_blocks=content_blocks,
202+
content=self._extract_text(content_blocks)
203+
if content_blocks
204+
else "",
205+
tool_calls=tool_calls,
206+
additional_kwargs=metadata,
207+
)
208+
)
209+
converted_messages.extend(tool_messages)
210+
211+
return converted_messages
147212

148213
async def map_event(
149214
self,

0 commit comments

Comments
 (0)