Skip to content
Open
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
33 changes: 1 addition & 32 deletions src/uipath_langchain/runtime/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,8 @@ def _map_messages_internal(
self, messages: list[UiPathConversationMessage]
) -> list[BaseMessage]:
"""
Converts UiPathConversationMessage list to LangChain messages (UserMessage/AIMessage/ToolMessage list).
Converts UiPathConversationMessage list to LangChain messages (UserMessage/AIMessage list).
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

Docstring says the mapper returns "UserMessage" instances, but the implementation actually constructs HumanMessage for user role messages. Update the wording to reflect the actual LangChain message types returned (e.g., HumanMessage/AIMessage) to avoid misleading documentation.

Suggested change
Converts UiPathConversationMessage list to LangChain messages (UserMessage/AIMessage list).
Converts UiPathConversationMessage list to LangChain messages
(e.g., HumanMessage/AIMessage BaseMessage instances).

Copilot uses AI. Check for mistakes.
- All content parts are combined into content_blocks
- Tool calls are converted to LangChain ToolCall format, with results stored as ToolMessage
- Metadata includes message_id, role, timestamps
"""
converted_messages: list[BaseMessage] = []
Expand Down Expand Up @@ -179,7 +178,6 @@ def _map_messages_internal(
elif role == "assistant":
# Convert tool calls to LangChain format
tool_calls: list[ToolCall] = []
tool_messages: list[ToolMessage] = []
if uipath_message.tool_calls:
for uipath_tool_call in uipath_message.tool_calls:
tool_call = ToolCall(
Expand All @@ -189,34 +187,6 @@ def _map_messages_internal(
)
tool_calls.append(tool_call)

tool_call_output = (
uipath_tool_call.result.output
if uipath_tool_call.result
else None
)
tool_call_status = (
"success"
if uipath_tool_call.result
and not uipath_tool_call.result.is_error
else "error"
)

# Serialize output to string if needed
if tool_call_output is None:
content = ""
elif isinstance(tool_call_output, str):
content = tool_call_output
else:
content = json.dumps(tool_call_output)

tool_messages.append(
ToolMessage(
content=content,
status=tool_call_status,
tool_call_id=uipath_tool_call.tool_call_id,
)
)

# 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.
# When needed, we can switch to content_blocks but need to work out a common ID strategy across models for the content-block IDs.
converted_messages.append(
Expand All @@ -230,7 +200,6 @@ def _map_messages_internal(
additional_kwargs=metadata,
)
)
converted_messages.extend(tool_messages)

return converted_messages

Expand Down
81 changes: 28 additions & 53 deletions tests/runtime/chat_message_mapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ def test_map_messages_handles_assistant_message_without_tool_calls(self):
assert msg.additional_kwargs["message_id"] == "msg-1"

def test_map_messages_handles_tool_calls_without_results(self):
"""Should include tool calls without results with empty content and error status."""
"""Should include tool calls on AIMessage without creating ToolMessages."""
mapper = UiPathChatMessagesMapper("test-runtime", None)
from uipath.core.chat import UiPathConversationToolCall

Expand Down Expand Up @@ -335,22 +335,16 @@ def test_map_messages_handles_tool_calls_without_results(self):

result = mapper.map_messages([uipath_msg])

# AIMessage + ToolMessage
assert len(result) == 2
# Only AIMessage - ToolMessages are produced by tool_node during execution
assert len(result) == 1
ai_msg = result[0]
assert isinstance(ai_msg, AIMessage)
assert len(ai_msg.tool_calls) == 1
assert ai_msg.tool_calls[0]["id"] == "call-123"
assert ai_msg.tool_calls[0]["name"] == "search_database"

tool_msg = result[1]
assert isinstance(tool_msg, ToolMessage)
assert tool_msg.tool_call_id == "call-123"
assert tool_msg.content == "" # Empty content for tool without result
assert tool_msg.status == "error" # Error status for tool without result

def test_map_messages_includes_tool_calls_with_results(self):
"""Should create AIMessage with tool_calls AND ToolMessage for completed tool calls."""
"""Should create AIMessage with tool_calls but no ToolMessages."""
mapper = UiPathChatMessagesMapper("test-runtime", None)
from uipath.core.chat import (
UiPathConversationToolCall,
Expand Down Expand Up @@ -391,8 +385,8 @@ def test_map_messages_includes_tool_calls_with_results(self):

result = mapper.map_messages([uipath_msg])

# Should have AIMessage + ToolMessage
assert len(result) == 2
# Only AIMessage - ToolMessages are produced by tool_node during execution
assert len(result) == 1

# Check AIMessage
ai_msg = result[0]
Expand All @@ -406,15 +400,8 @@ def test_map_messages_includes_tool_calls_with_results(self):
assert tool_call["args"] == {"query": "test query"}
assert tool_call["id"] == "call-123"

# Check ToolMessage
tool_msg = result[1]
assert isinstance(tool_msg, ToolMessage)
assert tool_msg.tool_call_id == "call-123"
assert tool_msg.content == '{"results": ["item1", "item2"]}'
assert tool_msg.status == "success"

def test_map_messages_includes_tool_calls_with_error_results(self):
"""Should create ToolMessage with error status for failed tool calls."""
"""Should create AIMessage with tool_calls for failed tool calls, no ToolMessages."""
mapper = UiPathChatMessagesMapper("test-runtime", None)
from uipath.core.chat import (
UiPathConversationToolCall,
Expand Down Expand Up @@ -446,14 +433,14 @@ def test_map_messages_includes_tool_calls_with_error_results(self):

result = mapper.map_messages([uipath_msg])

assert len(result) == 2
tool_msg = result[1]
assert isinstance(tool_msg, ToolMessage)
assert tool_msg.status == "error"
assert tool_msg.content == "Tool execution failed"
assert len(result) == 1
ai_msg = result[0]
assert isinstance(ai_msg, AIMessage)
assert len(ai_msg.tool_calls) == 1
assert ai_msg.tool_calls[0]["id"] == "call-456"

def test_map_messages_includes_tool_calls_with_string_output(self):
"""Should handle string output in tool results without JSON serialization."""
"""Should create AIMessage with tool_calls for string output results."""
mapper = UiPathChatMessagesMapper("test-runtime", None)
from uipath.core.chat import (
UiPathConversationToolCall,
Expand Down Expand Up @@ -485,13 +472,14 @@ def test_map_messages_includes_tool_calls_with_string_output(self):

result = mapper.map_messages([uipath_msg])

assert len(result) == 2
tool_msg = result[1]
assert isinstance(tool_msg, ToolMessage)
assert tool_msg.content == "plain text result"
assert len(result) == 1
ai_msg = result[0]
assert isinstance(ai_msg, AIMessage)
assert len(ai_msg.tool_calls) == 1
assert ai_msg.tool_calls[0]["id"] == "call-789"

def test_map_messages_includes_tool_calls_with_none_output(self):
"""Should handle None output in tool results as empty string."""
"""Should create AIMessage with tool_calls for None output results."""
mapper = UiPathChatMessagesMapper("test-runtime", None)
from uipath.core.chat import (
UiPathConversationToolCall,
Expand Down Expand Up @@ -523,13 +511,14 @@ def test_map_messages_includes_tool_calls_with_none_output(self):

result = mapper.map_messages([uipath_msg])

assert len(result) == 2
tool_msg = result[1]
assert isinstance(tool_msg, ToolMessage)
assert tool_msg.content == ""
assert len(result) == 1
ai_msg = result[0]
assert isinstance(ai_msg, AIMessage)
assert len(ai_msg.tool_calls) == 1
assert ai_msg.tool_calls[0]["id"] == "call-999"

def test_map_messages_includes_multiple_tool_calls_with_mixed_results(self):
"""Should handle multiple tool calls, some with results and some without."""
"""Should handle multiple tool calls on AIMessage, no ToolMessages created."""
mapper = UiPathChatMessagesMapper("test-runtime", None)
from uipath.core.chat import (
UiPathConversationToolCall,
Expand Down Expand Up @@ -571,8 +560,8 @@ def test_map_messages_includes_multiple_tool_calls_with_mixed_results(self):

result = mapper.map_messages([uipath_msg])

# Should have AIMessage + 2 ToolMessages (for both tool calls)
assert len(result) == 3
# Only AIMessage - ToolMessages are produced by tool_node during execution
assert len(result) == 1

ai_msg = result[0]
assert isinstance(ai_msg, AIMessage)
Expand All @@ -583,20 +572,6 @@ def test_map_messages_includes_multiple_tool_calls_with_mixed_results(self):
assert ai_msg.tool_calls[1]["id"] == "call-2"
assert ai_msg.tool_calls[1]["name"] == "tool_without_result"

# First ToolMessage (with result)
tool_msg_1 = result[1]
assert isinstance(tool_msg_1, ToolMessage)
assert tool_msg_1.tool_call_id == "call-1"
assert tool_msg_1.content == '{"status": "done"}'
assert tool_msg_1.status == "success"

# Second ToolMessage (without result)
tool_msg_2 = result[2]
assert isinstance(tool_msg_2, ToolMessage)
assert tool_msg_2.tool_call_id == "call-2"
assert tool_msg_2.content == "" # Empty content for tool without result
assert tool_msg_2.status == "error" # Error status for tool without result

def test_map_messages_handles_tool_calls_without_input(self):
"""Should handle tool call with None input and with result."""
mapper = UiPathChatMessagesMapper("test-runtime", None)
Expand Down Expand Up @@ -630,7 +605,7 @@ def test_map_messages_handles_tool_calls_without_input(self):

result = mapper.map_messages([uipath_msg])

assert len(result) == 2
assert len(result) == 1
msg = result[0]
assert isinstance(msg, AIMessage)
assert len(msg.tool_calls) == 1
Expand Down