Skip to content

Commit e7eb202

Browse files
fix: accumulate tool call chunks
1 parent 839f74d commit e7eb202

4 files changed

Lines changed: 73 additions & 48 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.1.35"
3+
version = "0.1.36"
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/chat/mapper.py

Lines changed: 60 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ class UiPathChatMessagesMapper:
4141
def __init__(self):
4242
"""Initialize the mapper with empty state."""
4343
self.tool_call_to_ai_message: dict[str, str] = {}
44+
self.current_message: AIMessageChunk
4445
self.seen_message_ids: set[str] = set()
4546

4647
def _extract_text(self, content: Any) -> str:
@@ -141,7 +142,7 @@ def _map_messages_internal(
141142
def map_event(
142143
self,
143144
message: BaseMessage,
144-
) -> UiPathConversationMessageEvent | None:
145+
) -> list[UiPathConversationMessageEvent] | None:
145146
"""Convert LangGraph BaseMessage (chunk or full) into a UiPathConversationMessageEvent.
146147
147148
Args:
@@ -168,16 +169,45 @@ def map_event(
168169

169170
# Check if this is the last chunk by examining chunk_position
170171
if message.chunk_position == "last":
172+
events: list[UiPathConversationMessageEvent] = []
173+
174+
# Loop through all content_blocks in current_message and create toolCallStart events for each tool_call_chunk
175+
if self.current_message and self.current_message.content_blocks:
176+
for block in self.current_message.content_blocks:
177+
if block.get("type") == "tool_call_chunk":
178+
tool_chunk_block = cast(ToolCallChunk, block)
179+
tool_call_id = tool_chunk_block.get("id")
180+
tool_name = tool_chunk_block.get("name")
181+
tool_args = tool_chunk_block.get("args")
182+
183+
if tool_call_id:
184+
tool_event = UiPathConversationMessageEvent(
185+
message_id=message.id,
186+
tool_call=UiPathConversationToolCallEvent(
187+
tool_call_id=tool_call_id,
188+
start=UiPathConversationToolCallStartEvent(
189+
tool_name=tool_name,
190+
timestamp=timestamp,
191+
input=UiPathInlineValue(inline=tool_args),
192+
),
193+
),
194+
)
195+
events.append(tool_event)
196+
197+
# Create the final event for the message
171198
msg_event.end = UiPathConversationMessageEndEvent(timestamp=timestamp)
172199
msg_event.content_part = UiPathConversationContentPartEvent(
173200
content_part_id=f"chunk-{message.id}-0",
174201
end=UiPathConversationContentPartEndEvent(),
175202
)
176-
return msg_event
203+
events.append(msg_event)
204+
205+
return events
177206

178207
# For every new message_id, start a new message
179208
if message.id not in self.seen_message_ids:
180209
self.seen_message_ids.add(message.id)
210+
self.current_message = message
181211
msg_event.start = UiPathConversationMessageStartEvent(
182212
role="assistant", timestamp=timestamp
183213
)
@@ -200,7 +230,6 @@ def map_event(
200230
content_part_id=f"chunk-{message.id}-0",
201231
chunk=UiPathConversationContentPartChunkEvent(
202232
data=text,
203-
content_part_sequence=0,
204233
),
205234
)
206235

@@ -210,19 +239,10 @@ def map_event(
210239
tool_call_id = tool_chunk_block.get("id")
211240
if tool_call_id:
212241
# Track tool_call_id -> ai_message_id mapping
213-
self.tool_call_to_ai_message[str(tool_call_id)] = message.id
214-
215-
args = tool_chunk_block.get("args") or ""
242+
self.tool_call_to_ai_message[tool_call_id] = message.id
216243

217-
msg_event.content_part = UiPathConversationContentPartEvent(
218-
content_part_id=f"chunk-{message.id}-0",
219-
chunk=UiPathConversationContentPartChunkEvent(
220-
data=args,
221-
content_part_sequence=0,
222-
),
223-
)
224-
# Continue so that multiple tool_call_chunks in the same block list
225-
# are handled correctly
244+
# Accumulate the message chunk
245+
self.current_message = self.current_message + message
226246
continue
227247

228248
# Fallback: raw string content on the chunk (rare when using content_blocks)
@@ -231,7 +251,6 @@ def map_event(
231251
content_part_id=f"content-{message.id}",
232252
chunk=UiPathConversationContentPartChunkEvent(
233253
data=message.content,
234-
content_part_sequence=0,
235254
),
236255
)
237256

@@ -241,7 +260,7 @@ def map_event(
241260
or msg_event.tool_call
242261
or msg_event.end
243262
):
244-
return msg_event
263+
return [msg_event]
245264

246265
return None
247266

@@ -275,35 +294,34 @@ def map_event(
275294
# Keep as string if not valid JSON
276295
pass
277296

278-
return UiPathConversationMessageEvent(
279-
message_id=result_message_id or str(uuid4()),
280-
tool_call=UiPathConversationToolCallEvent(
281-
tool_call_id=message.tool_call_id,
282-
start=UiPathConversationToolCallStartEvent(
283-
tool_name=message.name,
284-
arguments=None,
285-
timestamp=timestamp,
297+
return [
298+
UiPathConversationMessageEvent(
299+
message_id=result_message_id or str(uuid4()),
300+
tool_call=UiPathConversationToolCallEvent(
301+
tool_call_id=message.tool_call_id,
302+
end=UiPathConversationToolCallEndEvent(
303+
timestamp=timestamp,
304+
output=UiPathInlineValue(inline=content_value),
305+
),
286306
),
287-
end=UiPathConversationToolCallEndEvent(
288-
timestamp=timestamp,
289-
output=UiPathInlineValue(inline=content_value),
290-
),
291-
),
292-
)
307+
)
308+
]
293309

294310
# --- Fallback for other BaseMessage types ---
295311
text_content = self._extract_text(message.content)
296-
return UiPathConversationMessageEvent(
297-
message_id=message.id,
298-
start=UiPathConversationMessageStartEvent(
299-
role="assistant", timestamp=timestamp
300-
),
301-
content_part=UiPathConversationContentPartEvent(
302-
content_part_id=f"cp-{message.id}",
303-
chunk=UiPathConversationContentPartChunkEvent(data=text_content),
304-
),
305-
end=UiPathConversationMessageEndEvent(),
306-
)
312+
return [
313+
UiPathConversationMessageEvent(
314+
message_id=message.id,
315+
start=UiPathConversationMessageStartEvent(
316+
role="assistant", timestamp=timestamp
317+
),
318+
content_part=UiPathConversationContentPartEvent(
319+
content_part_id=f"cp-{message.id}",
320+
chunk=UiPathConversationContentPartChunkEvent(data=text_content),
321+
),
322+
end=UiPathConversationMessageEndEvent(),
323+
)
324+
]
307325

308326

309327
__all__ = ["UiPathChatMessagesMapper"]

src/uipath_langchain/runtime/runtime.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -135,10 +135,17 @@ async def stream(
135135
if chunk_type == "messages":
136136
if isinstance(data, tuple):
137137
message, _ = data
138-
event = UiPathRuntimeMessageEvent(
139-
payload=self.chat.map_event(message),
140-
)
141-
yield event
138+
try:
139+
events = self.chat.map_event(message)
140+
except Exception as e:
141+
logger.warning(f"Error mapping message event: {e}")
142+
events = None
143+
if events:
144+
for mapped_event in events:
145+
event = UiPathRuntimeMessageEvent(
146+
payload=mapped_event,
147+
)
148+
yield event
142149

143150
# Emit UiPathRuntimeStateEvent for state updates
144151
elif chunk_type == "updates":

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)