|
3 | 3 | Tests UiPathChatAnthropic with both vertexai and awsbedrock vendor_types. |
4 | 4 | """ |
5 | 5 |
|
| 6 | +from collections.abc import Iterable |
6 | 7 | from typing import Any |
7 | 8 |
|
8 | 9 | import pytest |
9 | 10 | 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 |
12 | 12 |
|
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 |
14 | 15 |
|
15 | 16 |
|
16 | 17 | @pytest.mark.asyncio |
17 | 18 | @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): |
43 | 20 | @pytest.fixture(autouse=True) |
44 | 21 | def skip_on_specific_configs( |
45 | 22 | self, |
46 | 23 | request: pytest.FixtureRequest, |
47 | 24 | completions_config: tuple[type[BaseChatModel], dict[str, Any]], |
48 | 25 | ) -> None: |
49 | | - model_class, model_kwargs = completions_config |
| 26 | + _, model_kwargs = completions_config |
50 | 27 | model_name = model_kwargs.get("model", "") |
51 | 28 | test_name = request.node.originalname |
52 | 29 | has_thinking = "thinking" in model_kwargs |
53 | 30 | 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 |
58 | 33 |
|
59 | 34 | # Claude via Vertex AI: streaming bugged (502 / empty content) |
60 | 35 | if is_vertex and test_name in [ |
@@ -111,102 +86,29 @@ def skip_on_specific_configs( |
111 | 86 | ]: |
112 | 87 | pytest.skip(f"Skipping {test_name}: URL image sources not supported via gateway") |
113 | 88 |
|
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 | + ), |
175 | 114 | ) |
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