Skip to content

Commit 202136e

Browse files
cosminachoclaude
andauthored
test(langchain): add file-input integration tests across providers (#85)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent def5f9a commit 202136e

23 files changed

Lines changed: 830 additions & 684 deletions

pyproject.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ dev = [
4545
"uipath-llm-client[all]",
4646
"uipath_langchain_client[all]",
4747
"openinference-instrumentation-langchain>=0.1.63,<1.0.0",
48+
"python-docx>=1.1.2,<2.0.0",
49+
"openpyxl>=3.1.5,<4.0.0",
50+
"reportlab>=4.2.5,<5.0.0",
51+
"Pillow>=12.0.0,<13.0.0",
4852
]
4953

5054
[tool.uv]

tests/cassettes.db

816 KB
Binary file not shown.

tests/langchain/clients/anthropic/test_integration.py

Lines changed: 33 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -3,58 +3,33 @@
33
Tests UiPathChatAnthropic with both vertexai and awsbedrock vendor_types.
44
"""
55

6+
from collections.abc import Iterable
67
from typing import Any
78

89
import pytest
910
from langchain_core.language_models.chat_models import BaseChatModel
10-
from langchain_core.messages import AIMessageChunk
11-
from langchain_tests.integration_tests import ChatModelIntegrationTests
11+
from langchain_core.runnables import Runnable
1212

13-
from tests.langchain.utils import search_accommodation, search_attractions, search_flights
13+
from tests.langchain.file_fixtures import IMAGE_FORMATS, PDF_FORMATS
14+
from tests.langchain.integration_tests import UiPathChatModelIntegrationTests
1415

1516

1617
@pytest.mark.asyncio
1718
@pytest.mark.vcr
18-
class TestAnthropicIntegrationChatModel(ChatModelIntegrationTests):
19-
@pytest.fixture(autouse=True)
20-
def setup_models(self, completions_config: tuple[type[BaseChatModel], dict[str, Any]]):
21-
self._completions_class, self.completions_kwargs = completions_config
22-
23-
@property
24-
def supports_image_inputs(self) -> bool:
25-
return True
26-
27-
@property
28-
def supports_image_tool_message(self) -> bool:
29-
return True
30-
31-
@property
32-
def supports_image_urls(self) -> bool:
33-
return True
34-
35-
@property
36-
def supports_pdf_inputs(self) -> bool:
37-
return True
38-
39-
@property
40-
def supports_pdf_tool_message(self) -> bool:
41-
return True
42-
19+
class TestAnthropicIntegrationChatModel(UiPathChatModelIntegrationTests):
4320
@pytest.fixture(autouse=True)
4421
def skip_on_specific_configs(
4522
self,
4623
request: pytest.FixtureRequest,
4724
completions_config: tuple[type[BaseChatModel], dict[str, Any]],
4825
) -> None:
49-
model_class, model_kwargs = completions_config
26+
_, model_kwargs = completions_config
5027
model_name = model_kwargs.get("model", "")
5128
test_name = request.node.originalname
5229
has_thinking = "thinking" in model_kwargs
5330
is_vertex = model_kwargs.get("vendor_type") == "vertexai" or "@" in model_name
54-
55-
# Useless framework tests
56-
if test_name in ["test_no_overrides_DO_NOT_OVERRIDE", "test_unicode_tool_call_integration"]:
57-
pytest.skip(f"Skipping {test_name}: not relevant")
31+
callspec = getattr(request.node, "callspec", None)
32+
fmt = callspec.params.get("fmt") if callspec else None
5833

5934
# Claude via Vertex AI: streaming bugged (502 / empty content)
6035
if is_vertex and test_name in [
@@ -111,102 +86,29 @@ def skip_on_specific_configs(
11186
]:
11287
pytest.skip(f"Skipping {test_name}: URL image sources not supported via gateway")
11388

114-
@property
115-
def chat_model_class(self) -> type[BaseChatModel]:
116-
return self._completions_class
117-
118-
@property
119-
def chat_model_params(self) -> dict[str, Any]:
120-
return self.completions_kwargs
121-
122-
@pytest.mark.parametrize("model", [{}, {"output_version": "v1"}], indirect=True)
123-
def test_stream(self, model: BaseChatModel) -> None:
124-
chunks: list[AIMessageChunk] = []
125-
full: AIMessageChunk | None = None
126-
for chunk in model.stream("Hello"):
127-
assert chunk is not None
128-
assert isinstance(chunk, AIMessageChunk)
129-
assert isinstance(chunk.content, str | list)
130-
chunks.append(chunk)
131-
full = chunk if full is None else full + chunk
132-
assert len(chunks) > 0
133-
assert isinstance(full, AIMessageChunk)
134-
assert full.content
135-
text_blocks = [block for block in full.content_blocks if block["type"] == "text"]
136-
assert len(text_blocks) == 1
137-
138-
last_chunk = chunks[-1]
139-
assert last_chunk.chunk_position == "last", (
140-
f"Final chunk must have chunk_position='last', got {last_chunk.chunk_position!r}"
141-
)
142-
143-
@pytest.mark.parametrize("model", [{}, {"output_version": "v1"}], indirect=True)
144-
async def test_astream(self, model: BaseChatModel) -> None:
145-
chunks: list[AIMessageChunk] = []
146-
full: AIMessageChunk | None = None
147-
async for chunk in model.astream("Hello"):
148-
assert chunk is not None
149-
assert isinstance(chunk, AIMessageChunk)
150-
assert isinstance(chunk.content, str | list)
151-
chunks.append(chunk)
152-
full = chunk if full is None else full + chunk
153-
assert len(chunks) > 0
154-
assert isinstance(full, AIMessageChunk)
155-
assert full.content
156-
text_blocks = [block for block in full.content_blocks if block["type"] == "text"]
157-
assert len(text_blocks) == 1
158-
159-
last_chunk = chunks[-1]
160-
assert last_chunk.chunk_position == "last", (
161-
f"Final chunk must have chunk_position='last', got {last_chunk.chunk_position!r}"
162-
)
163-
164-
def test_parallel_and_sequential_tool_calling(self, model: BaseChatModel) -> None:
165-
"""Test parallel tool calling for Claude models."""
166-
tools = [search_accommodation, search_flights, search_attractions]
167-
prompt = (
168-
"I want to plan a trip to Paris from New York. "
169-
"I need to find flights for March 15th, accommodation from March 15th to March 20th, and things to do there.",
170-
"Search for accomodations, flights and attractions in parallel. Don't repeat the same tool call.",
171-
)
172-
model_with_tools_parallel = model.bind_tools(
173-
tools,
174-
tool_choice={"type": "any", "disable_parallel_tool_use": False}, # type: ignore
89+
# File-input matrix: structured output forces tool_choice (incompatible with
90+
# thinking); image/PDF blocks don't round-trip through the Anthropic gateway.
91+
if test_name in ("test_file_inputs", "test_file_inputs_async"):
92+
if has_thinking:
93+
pytest.skip(
94+
"Structured output forces tool_choice, which is incompatible with thinking"
95+
)
96+
if fmt in (IMAGE_FORMATS | PDF_FORMATS):
97+
pytest.skip(
98+
"Image/PDF content blocks are not supported via the Anthropic gateway path"
99+
)
100+
101+
def _bind_parallel_and_sequential(
102+
self, model: BaseChatModel, tools: Iterable[Any]
103+
) -> tuple[Runnable, Runnable]:
104+
tools_list = list(tools)
105+
return (
106+
model.bind_tools(
107+
tools_list,
108+
tool_choice={"type": "any", "disable_parallel_tool_use": False}, # type: ignore
109+
),
110+
model.bind_tools(
111+
tools_list,
112+
tool_choice={"type": "any", "disable_parallel_tool_use": True}, # type: ignore
113+
),
175114
)
176-
model_with_tools_sequential = model.bind_tools(
177-
tools,
178-
tool_choice={"type": "any", "disable_parallel_tool_use": True}, # type: ignore
179-
)
180-
181-
parallel_response = model_with_tools_parallel.invoke(prompt)
182-
sequential_response = model_with_tools_sequential.invoke(prompt)
183-
184-
assert parallel_response.tool_calls is not None
185-
assert sequential_response.tool_calls is not None
186-
assert len(parallel_response.tool_calls) == len(tools)
187-
assert len(sequential_response.tool_calls) == 1
188-
189-
async def test_parallel_and_sequential_tool_calling_async(self, model: BaseChatModel) -> None:
190-
"""Test parallel and sequential tool calling async for Claude."""
191-
tools = [search_accommodation, search_flights, search_attractions]
192-
prompt = (
193-
"I want to plan a trip to Paris from New York. "
194-
"I need to find flights for March 15th, accommodation from March 15th to March 20th, and things to do there.",
195-
"Search for accomodations, flights and attractions in parallel. Don't repeat the same tool call.",
196-
)
197-
model_with_tools_parallel = model.bind_tools(
198-
tools,
199-
tool_choice={"type": "any", "disable_parallel_tool_use": False}, # type: ignore
200-
)
201-
model_with_tools_sequential = model.bind_tools(
202-
tools,
203-
tool_choice={"type": "any", "disable_parallel_tool_use": True}, # type: ignore
204-
)
205-
206-
parallel_response = await model_with_tools_parallel.ainvoke(prompt)
207-
sequential_response = await model_with_tools_sequential.ainvoke(prompt)
208-
209-
assert parallel_response.tool_calls is not None
210-
assert sequential_response.tool_calls is not None
211-
assert len(parallel_response.tool_calls) == len(tools)
212-
assert len(sequential_response.tool_calls) == 1

0 commit comments

Comments
 (0)