Skip to content

Commit e936154

Browse files
andreitava-uipcotovanu-cristian
authored andcommitted
fix: annotations, add tests and version bump
1 parent 03b1005 commit e936154

5 files changed

Lines changed: 402 additions & 4 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.4.26"
3+
version = "0.4.27"
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/agent/messages/message_utils.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from langchain.messages import AIMessage, ToolCall
2+
from langchain_core.messages.content import ContentBlock, create_tool_call
23

34

45
def replace_tool_calls(message: AIMessage, tool_calls: list[ToolCall]) -> AIMessage:
@@ -17,11 +18,20 @@ def replace_tool_calls(message: AIMessage, tool_calls: list[ToolCall]) -> AIMess
1718
"output_version": "v1", # we have to set this otherwise anthropic clients do not denormalize
1819
}
1920

20-
content_blocks = [
21+
# ToolCall from langchain.messages is not the same type as ToolCall from langchain_core.messages.content
22+
# they are both TypedDicts with the same fields, so it would be possible to just cast, but it's safer to map
23+
tool_call_blocks = [
24+
create_tool_call(
25+
name=tool_call["name"], args=tool_call["args"], id=tool_call["id"]
26+
)
27+
for tool_call in tool_calls
28+
]
29+
30+
content_blocks: list[ContentBlock] = [
2131
block for block in message.content_blocks if block["type"] != "tool_call"
2232
]
2333

24-
content_blocks.extend(tool_calls)
34+
content_blocks.extend(tool_call_blocks)
2535

2636
return AIMessage(
2737
content_blocks=content_blocks,
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
"""Tests for agent/messages/message_utils.py module."""
2+
3+
from langchain.messages import AIMessage, ToolCall
4+
from langchain_core.messages.content import (
5+
ContentBlock,
6+
create_text_block,
7+
create_tool_call,
8+
)
9+
10+
from uipath_langchain.agent.messages.message_utils import replace_tool_calls
11+
12+
13+
class TestReplaceToolCalls:
14+
"""Test cases for replace_tool_calls function."""
15+
16+
def test_replace_tool_calls_basic(self):
17+
"""Test basic tool call replacement."""
18+
original_tool_calls = [
19+
ToolCall(name="old_tool", args={"param": "value"}, id="old_id")
20+
]
21+
original_content_blocks: list[ContentBlock] = [
22+
create_text_block("Test message"),
23+
create_tool_call(name="old_tool", args={"param": "value"}, id="old_id"),
24+
]
25+
original_message = AIMessage(
26+
content_blocks=original_content_blocks, tool_calls=original_tool_calls
27+
)
28+
29+
new_tool_calls = [
30+
ToolCall(name="new_tool", args={"new_param": "new_value"}, id="new_id")
31+
]
32+
33+
result = replace_tool_calls(original_message, new_tool_calls)
34+
35+
assert len(result.tool_calls) == len(new_tool_calls)
36+
assert result.tool_calls[0]["name"] == "new_tool"
37+
assert result.tool_calls[0]["args"] == {"new_param": "new_value"}
38+
assert result.tool_calls[0]["id"] == "new_id"
39+
40+
# Check content blocks
41+
tool_call_blocks = [
42+
block for block in result.content_blocks if block["type"] == "tool_call"
43+
]
44+
assert len(tool_call_blocks) == 1
45+
assert tool_call_blocks[0]["name"] == "new_tool"
46+
assert tool_call_blocks[0]["args"] == {"new_param": "new_value"}
47+
assert tool_call_blocks[0]["id"] == "new_id"
48+
49+
def test_replace_tool_calls_empty_list(self):
50+
"""Test replacing with empty tool calls list."""
51+
original_tool_calls = [ToolCall(name="tool", args={}, id="id")]
52+
original_content_blocks: list[ContentBlock] = [
53+
create_text_block("Test message"),
54+
create_tool_call(name="tool", args={}, id="id"),
55+
]
56+
original_message = AIMessage(
57+
content_blocks=original_content_blocks, tool_calls=original_tool_calls
58+
)
59+
60+
new_tool_calls: list[ToolCall] = []
61+
62+
result = replace_tool_calls(original_message, new_tool_calls)
63+
64+
assert result.tool_calls == []
65+
66+
# Check content blocks - should have no tool call blocks
67+
tool_call_blocks = [
68+
block for block in result.content_blocks if block["type"] == "tool_call"
69+
]
70+
assert len(tool_call_blocks) == 0
71+
72+
def test_replace_tool_calls_multiple(self):
73+
"""Test replacing with multiple tool calls."""
74+
original_tool_calls = [
75+
ToolCall(name="old_tool1", args={"param1": "value1"}, id="old_id1")
76+
]
77+
original_content_blocks: list[ContentBlock] = [
78+
create_text_block("Test message"),
79+
create_tool_call(name="old_tool1", args={"param1": "value1"}, id="old_id1"),
80+
]
81+
original_message = AIMessage(
82+
content_blocks=original_content_blocks, tool_calls=original_tool_calls
83+
)
84+
85+
new_tool_calls = [
86+
ToolCall(name="new_tool1", args={"param1": "value1"}, id="new_id1"),
87+
ToolCall(name="new_tool2", args={"param2": "value2"}, id="new_id2"),
88+
]
89+
90+
result = replace_tool_calls(original_message, new_tool_calls)
91+
92+
assert len(result.tool_calls) == 2
93+
assert result.tool_calls[0]["name"] == "new_tool1"
94+
assert result.tool_calls[1]["name"] == "new_tool2"
95+
96+
# Check content blocks
97+
tool_call_blocks = [
98+
block for block in result.content_blocks if block["type"] == "tool_call"
99+
]
100+
assert len(tool_call_blocks) == 2
101+
assert tool_call_blocks[0]["name"] == "new_tool1"
102+
assert tool_call_blocks[1]["name"] == "new_tool2"
103+
104+
def test_replace_tool_calls_preserves_text_content(self):
105+
"""Test that text content is preserved when replacing tool calls."""
106+
original_tool_calls = [ToolCall(name="old_tool", args={}, id="old_id")]
107+
original_content_blocks: list[ContentBlock] = [
108+
create_text_block("This is important text content"),
109+
create_tool_call(name="old_tool", args={}, id="old_id"),
110+
]
111+
original_message = AIMessage(
112+
content_blocks=original_content_blocks, tool_calls=original_tool_calls
113+
)
114+
115+
new_tool_calls = [ToolCall(name="new_tool", args={}, id="new_id")]
116+
117+
result = replace_tool_calls(original_message, new_tool_calls)
118+
119+
assert "This is important text content" in str(result.content)
120+
121+
# Check content blocks
122+
tool_call_blocks = [
123+
block for block in result.content_blocks if block["type"] == "tool_call"
124+
]
125+
assert len(tool_call_blocks) == 1
126+
assert tool_call_blocks[0]["name"] == "new_tool"
127+
128+
def test_replace_tool_calls_with_response_metadata(self):
129+
"""Test that response metadata is preserved and updated."""
130+
original_tool_calls = [ToolCall(name="old_tool", args={}, id="old_id")]
131+
original_content_blocks: list[ContentBlock] = [
132+
create_text_block("Test message"),
133+
create_tool_call(name="old_tool", args={}, id="old_id"),
134+
]
135+
original_metadata = {"existing_key": "existing_value"}
136+
original_message = AIMessage(
137+
content_blocks=original_content_blocks,
138+
tool_calls=original_tool_calls,
139+
response_metadata=original_metadata,
140+
)
141+
142+
new_tool_calls = [ToolCall(name="new_tool", args={}, id="new_id")]
143+
144+
result = replace_tool_calls(original_message, new_tool_calls)
145+
146+
assert result.response_metadata["existing_key"] == "existing_value"
147+
assert result.response_metadata["output_version"] == "v1"
148+
149+
# Check content blocks
150+
tool_call_blocks = [
151+
block for block in result.content_blocks if block["type"] == "tool_call"
152+
]
153+
assert len(tool_call_blocks) == 1
154+
assert tool_call_blocks[0]["name"] == "new_tool"
155+
156+
def test_replace_tool_calls_no_original_metadata(self):
157+
"""Test behavior when original message has no response metadata."""
158+
original_tool_calls = [ToolCall(name="old_tool", args={}, id="old_id")]
159+
original_content_blocks: list[ContentBlock] = [
160+
create_text_block("Test message"),
161+
create_tool_call(name="old_tool", args={}, id="old_id"),
162+
]
163+
original_message = AIMessage(
164+
content_blocks=original_content_blocks,
165+
tool_calls=original_tool_calls,
166+
response_metadata={},
167+
)
168+
169+
new_tool_calls = [ToolCall(name="new_tool", args={}, id="new_id")]
170+
171+
result = replace_tool_calls(original_message, new_tool_calls)
172+
173+
assert result.response_metadata["output_version"] == "v1"
174+
175+
# Check content blocks
176+
tool_call_blocks = [
177+
block for block in result.content_blocks if block["type"] == "tool_call"
178+
]
179+
assert len(tool_call_blocks) == 1
180+
assert tool_call_blocks[0]["name"] == "new_tool"
181+
182+
def test_replace_tool_calls_content_blocks(self):
183+
"""Test that non-tool content blocks are preserved."""
184+
original_tool_calls = [ToolCall(name="old_tool", args={}, id="old_id")]
185+
186+
text_block = create_text_block("Some text content")
187+
tool_call_block = create_tool_call(name="old_tool", args={}, id="old_id")
188+
189+
original_message = AIMessage(
190+
content_blocks=[text_block, tool_call_block], tool_calls=original_tool_calls
191+
)
192+
193+
new_tool_calls = [ToolCall(name="new_tool", args={"new": "args"}, id="new_id")]
194+
195+
result = replace_tool_calls(original_message, new_tool_calls)
196+
197+
# Should preserve text block and replace tool call block
198+
text_blocks = [
199+
block for block in result.content_blocks if block["type"] == "text"
200+
]
201+
tool_call_blocks = [
202+
block for block in result.content_blocks if block["type"] == "tool_call"
203+
]
204+
205+
assert len(text_blocks) == 1
206+
assert text_blocks[0]["text"] == "Some text content"
207+
208+
assert len(tool_call_blocks) == 1
209+
assert tool_call_blocks[0]["name"] == "new_tool"
210+
assert tool_call_blocks[0]["args"] == {"new": "args"}

0 commit comments

Comments
 (0)