Coverage for tests/models/test_anthropic.py: 98.31%

227 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-03-28 17:27 +0000

1from __future__ import annotations as _annotations 

2 

3import json 

4import os 

5from collections.abc import Sequence 

6from dataclasses import dataclass, field 

7from datetime import timezone 

8from functools import cached_property 

9from typing import Any, TypeVar, Union, cast 

10 

11import httpx 

12import pytest 

13from inline_snapshot import snapshot 

14 

15from pydantic_ai import Agent, ModelHTTPError, ModelRetry 

16from pydantic_ai.messages import ( 

17 BinaryContent, 

18 DocumentUrl, 

19 ImageUrl, 

20 ModelRequest, 

21 ModelResponse, 

22 RetryPromptPart, 

23 SystemPromptPart, 

24 TextPart, 

25 ToolCallPart, 

26 ToolReturnPart, 

27 UserPromptPart, 

28) 

29from pydantic_ai.result import Usage 

30from pydantic_ai.settings import ModelSettings 

31 

32from ..conftest import IsDatetime, IsNow, IsStr, TestEnv, raise_if_exception, try_import 

33from .mock_async_stream import MockAsyncStream 

34 

35with try_import() as imports_successful: 

36 from anthropic import NOT_GIVEN, APIStatusError, AsyncAnthropic 

37 from anthropic.types import ( 

38 ContentBlock, 

39 InputJSONDelta, 

40 Message as AnthropicMessage, 

41 MessageDeltaUsage, 

42 RawContentBlockDeltaEvent, 

43 RawContentBlockStartEvent, 

44 RawContentBlockStopEvent, 

45 RawMessageDeltaEvent, 

46 RawMessageStartEvent, 

47 RawMessageStopEvent, 

48 RawMessageStreamEvent, 

49 TextBlock, 

50 ToolUseBlock, 

51 Usage as AnthropicUsage, 

52 ) 

53 from anthropic.types.raw_message_delta_event import Delta 

54 

55 from pydantic_ai.models.anthropic import AnthropicModel, AnthropicModelSettings 

56 from pydantic_ai.providers.anthropic import AnthropicProvider 

57 

58 # note: we use Union here so that casting works with Python 3.9 

59 MockAnthropicMessage = Union[AnthropicMessage, Exception] 

60 MockRawMessageStreamEvent = Union[RawMessageStreamEvent, Exception] 

61 

62pytestmark = [ 

63 pytest.mark.skipif(not imports_successful(), reason='anthropic not installed'), 

64 pytest.mark.anyio, 

65] 

66 

67# Type variable for generic AsyncStream 

68T = TypeVar('T') 

69 

70 

71def test_init(): 

72 m = AnthropicModel('claude-3-5-haiku-latest', provider=AnthropicProvider(api_key='foobar')) 

73 assert m.client.api_key == 'foobar' 

74 assert m.model_name == 'claude-3-5-haiku-latest' 

75 assert m.system == 'anthropic' 

76 assert m.base_url == 'https://api.anthropic.com' 

77 

78 

79@dataclass 

80class MockAnthropic: 

81 messages_: MockAnthropicMessage | Sequence[MockAnthropicMessage] | None = None 

82 stream: Sequence[MockRawMessageStreamEvent] | Sequence[Sequence[MockRawMessageStreamEvent]] | None = None 

83 index = 0 

84 chat_completion_kwargs: list[dict[str, Any]] = field(default_factory=list) 

85 base_url: str | None = None 

86 

87 @cached_property 

88 def messages(self) -> Any: 

89 return type('Messages', (), {'create': self.messages_create}) 

90 

91 @classmethod 

92 def create_mock(cls, messages_: MockAnthropicMessage | Sequence[MockAnthropicMessage]) -> AsyncAnthropic: 

93 return cast(AsyncAnthropic, cls(messages_=messages_)) 

94 

95 @classmethod 

96 def create_stream_mock( 

97 cls, stream: Sequence[MockRawMessageStreamEvent] | Sequence[Sequence[MockRawMessageStreamEvent]] 

98 ) -> AsyncAnthropic: 

99 return cast(AsyncAnthropic, cls(stream=stream)) 

100 

101 async def messages_create( 

102 self, *_args: Any, stream: bool = False, **kwargs: Any 

103 ) -> AnthropicMessage | MockAsyncStream[MockRawMessageStreamEvent]: 

104 self.chat_completion_kwargs.append({k: v for k, v in kwargs.items() if v is not NOT_GIVEN}) 

105 

106 if stream: 

107 assert self.stream is not None, 'you can only use `stream=True` if `stream` is provided' 

108 if isinstance(self.stream[0], Sequence): 108 ↛ 111line 108 didn't jump to line 111 because the condition on line 108 was always true

109 response = MockAsyncStream(iter(cast(list[MockRawMessageStreamEvent], self.stream[self.index]))) 

110 else: 

111 response = MockAsyncStream(iter(cast(list[MockRawMessageStreamEvent], self.stream))) 

112 else: 

113 assert self.messages_ is not None, '`messages` must be provided' 

114 if isinstance(self.messages_, Sequence): 

115 raise_if_exception(self.messages_[self.index]) 

116 response = cast(AnthropicMessage, self.messages_[self.index]) 

117 else: 

118 raise_if_exception(self.messages_) 

119 response = cast(AnthropicMessage, self.messages_) 

120 self.index += 1 

121 return response 

122 

123 

124def completion_message(content: list[ContentBlock], usage: AnthropicUsage) -> AnthropicMessage: 

125 return AnthropicMessage( 

126 id='123', 

127 content=content, 

128 model='claude-3-5-haiku-123', 

129 role='assistant', 

130 stop_reason='end_turn', 

131 type='message', 

132 usage=usage, 

133 ) 

134 

135 

136async def test_sync_request_text_response(allow_model_requests: None): 

137 c = completion_message([TextBlock(text='world', type='text')], AnthropicUsage(input_tokens=5, output_tokens=10)) 

138 mock_client = MockAnthropic.create_mock(c) 

139 m = AnthropicModel('claude-3-5-haiku-latest', provider=AnthropicProvider(anthropic_client=mock_client)) 

140 agent = Agent(m) 

141 

142 result = await agent.run('hello') 

143 assert result.data == 'world' 

144 assert result.usage() == snapshot(Usage(requests=1, request_tokens=5, response_tokens=10, total_tokens=15)) 

145 

146 # reset the index so we get the same response again 

147 mock_client.index = 0 # type: ignore 

148 

149 result = await agent.run('hello', message_history=result.new_messages()) 

150 assert result.data == 'world' 

151 assert result.usage() == snapshot(Usage(requests=1, request_tokens=5, response_tokens=10, total_tokens=15)) 

152 assert result.all_messages() == snapshot( 

153 [ 

154 ModelRequest(parts=[UserPromptPart(content='hello', timestamp=IsNow(tz=timezone.utc))]), 

155 ModelResponse( 

156 parts=[TextPart(content='world')], 

157 model_name='claude-3-5-haiku-123', 

158 timestamp=IsNow(tz=timezone.utc), 

159 ), 

160 ModelRequest(parts=[UserPromptPart(content='hello', timestamp=IsNow(tz=timezone.utc))]), 

161 ModelResponse( 

162 parts=[TextPart(content='world')], 

163 model_name='claude-3-5-haiku-123', 

164 timestamp=IsNow(tz=timezone.utc), 

165 ), 

166 ] 

167 ) 

168 

169 

170async def test_async_request_text_response(allow_model_requests: None): 

171 c = completion_message( 

172 [TextBlock(text='world', type='text')], 

173 usage=AnthropicUsage(input_tokens=3, output_tokens=5), 

174 ) 

175 mock_client = MockAnthropic.create_mock(c) 

176 m = AnthropicModel('claude-3-5-haiku-latest', provider=AnthropicProvider(anthropic_client=mock_client)) 

177 agent = Agent(m) 

178 

179 result = await agent.run('hello') 

180 assert result.data == 'world' 

181 assert result.usage() == snapshot(Usage(requests=1, request_tokens=3, response_tokens=5, total_tokens=8)) 

182 

183 

184async def test_request_structured_response(allow_model_requests: None): 

185 c = completion_message( 

186 [ToolUseBlock(id='123', input={'response': [1, 2, 3]}, name='final_result', type='tool_use')], 

187 usage=AnthropicUsage(input_tokens=3, output_tokens=5), 

188 ) 

189 mock_client = MockAnthropic.create_mock(c) 

190 m = AnthropicModel('claude-3-5-haiku-latest', provider=AnthropicProvider(anthropic_client=mock_client)) 

191 agent = Agent(m, result_type=list[int]) 

192 

193 result = await agent.run('hello') 

194 assert result.data == [1, 2, 3] 

195 assert result.all_messages() == snapshot( 

196 [ 

197 ModelRequest(parts=[UserPromptPart(content='hello', timestamp=IsNow(tz=timezone.utc))]), 

198 ModelResponse( 

199 parts=[ 

200 ToolCallPart( 

201 tool_name='final_result', 

202 args={'response': [1, 2, 3]}, 

203 tool_call_id='123', 

204 ) 

205 ], 

206 model_name='claude-3-5-haiku-123', 

207 timestamp=IsNow(tz=timezone.utc), 

208 ), 

209 ModelRequest( 

210 parts=[ 

211 ToolReturnPart( 

212 tool_name='final_result', 

213 content='Final result processed.', 

214 tool_call_id='123', 

215 timestamp=IsNow(tz=timezone.utc), 

216 ) 

217 ] 

218 ), 

219 ] 

220 ) 

221 

222 

223async def test_request_tool_call(allow_model_requests: None): 

224 responses = [ 

225 completion_message( 

226 [ToolUseBlock(id='1', input={'loc_name': 'San Francisco'}, name='get_location', type='tool_use')], 

227 usage=AnthropicUsage(input_tokens=2, output_tokens=1), 

228 ), 

229 completion_message( 

230 [ToolUseBlock(id='2', input={'loc_name': 'London'}, name='get_location', type='tool_use')], 

231 usage=AnthropicUsage(input_tokens=3, output_tokens=2), 

232 ), 

233 completion_message( 

234 [TextBlock(text='final response', type='text')], 

235 usage=AnthropicUsage(input_tokens=3, output_tokens=5), 

236 ), 

237 ] 

238 

239 mock_client = MockAnthropic.create_mock(responses) 

240 m = AnthropicModel('claude-3-5-haiku-latest', provider=AnthropicProvider(anthropic_client=mock_client)) 

241 agent = Agent(m, system_prompt='this is the system prompt') 

242 

243 @agent.tool_plain 

244 async def get_location(loc_name: str) -> str: 

245 if loc_name == 'London': 

246 return json.dumps({'lat': 51, 'lng': 0}) 

247 else: 

248 raise ModelRetry('Wrong location, please try again') 

249 

250 result = await agent.run('hello') 

251 assert result.data == 'final response' 

252 assert result.all_messages() == snapshot( 

253 [ 

254 ModelRequest( 

255 parts=[ 

256 SystemPromptPart(content='this is the system prompt', timestamp=IsNow(tz=timezone.utc)), 

257 UserPromptPart(content='hello', timestamp=IsNow(tz=timezone.utc)), 

258 ] 

259 ), 

260 ModelResponse( 

261 parts=[ 

262 ToolCallPart( 

263 tool_name='get_location', 

264 args={'loc_name': 'San Francisco'}, 

265 tool_call_id='1', 

266 ) 

267 ], 

268 model_name='claude-3-5-haiku-123', 

269 timestamp=IsNow(tz=timezone.utc), 

270 ), 

271 ModelRequest( 

272 parts=[ 

273 RetryPromptPart( 

274 content='Wrong location, please try again', 

275 tool_name='get_location', 

276 tool_call_id='1', 

277 timestamp=IsNow(tz=timezone.utc), 

278 ) 

279 ] 

280 ), 

281 ModelResponse( 

282 parts=[ 

283 ToolCallPart( 

284 tool_name='get_location', 

285 args={'loc_name': 'London'}, 

286 tool_call_id='2', 

287 ) 

288 ], 

289 model_name='claude-3-5-haiku-123', 

290 timestamp=IsNow(tz=timezone.utc), 

291 ), 

292 ModelRequest( 

293 parts=[ 

294 ToolReturnPart( 

295 tool_name='get_location', 

296 content='{"lat": 51, "lng": 0}', 

297 tool_call_id='2', 

298 timestamp=IsNow(tz=timezone.utc), 

299 ) 

300 ] 

301 ), 

302 ModelResponse( 

303 parts=[TextPart(content='final response')], 

304 model_name='claude-3-5-haiku-123', 

305 timestamp=IsNow(tz=timezone.utc), 

306 ), 

307 ] 

308 ) 

309 

310 

311def get_mock_chat_completion_kwargs(async_anthropic: AsyncAnthropic) -> list[dict[str, Any]]: 

312 if isinstance(async_anthropic, MockAnthropic): 

313 return async_anthropic.chat_completion_kwargs 

314 else: # pragma: no cover 

315 raise RuntimeError('Not a MockOpenAI instance') 

316 

317 

318@pytest.mark.parametrize('parallel_tool_calls', [True, False]) 

319async def test_parallel_tool_calls(allow_model_requests: None, parallel_tool_calls: bool) -> None: 

320 responses = [ 

321 completion_message( 

322 [ToolUseBlock(id='1', input={'loc_name': 'San Francisco'}, name='get_location', type='tool_use')], 

323 usage=AnthropicUsage(input_tokens=2, output_tokens=1), 

324 ), 

325 completion_message( 

326 [TextBlock(text='final response', type='text')], 

327 usage=AnthropicUsage(input_tokens=3, output_tokens=5), 

328 ), 

329 ] 

330 

331 mock_client = MockAnthropic.create_mock(responses) 

332 m = AnthropicModel('claude-3-5-haiku-latest', provider=AnthropicProvider(anthropic_client=mock_client)) 

333 agent = Agent(m, model_settings=ModelSettings(parallel_tool_calls=parallel_tool_calls)) 

334 

335 @agent.tool_plain 

336 async def get_location(loc_name: str) -> str: 

337 if loc_name == 'London': 337 ↛ 338line 337 didn't jump to line 338 because the condition on line 337 was never true

338 return json.dumps({'lat': 51, 'lng': 0}) 

339 else: 

340 raise ModelRetry('Wrong location, please try again') 

341 

342 await agent.run('hello') 

343 assert get_mock_chat_completion_kwargs(mock_client)[0]['tool_choice']['disable_parallel_tool_use'] == ( 

344 not parallel_tool_calls 

345 ) 

346 

347 

348@pytest.mark.vcr 

349async def test_multiple_parallel_tool_calls(allow_model_requests: None): 

350 async def retrieve_entity_info(name: str) -> str: 

351 """Get the knowledge about the given entity.""" 

352 data = { 

353 'alice': "alice is bob's wife", 

354 'bob': "bob is alice's husband", 

355 'charlie': "charlie is alice's son", 

356 'daisy': "daisy is bob's daughter and charlie's younger sister", 

357 } 

358 return data[name.lower()] 

359 

360 system_prompt = """ 

361 Use the `retrieve_entity_info` tool to get information about a specific person. 

362 If you need to use `retrieve_entity_info` to get information about multiple people, try 

363 to call them in parallel as much as possible. 

364 Think step by step and then provide a single most probable concise answer. 

365 """ 

366 

367 # If we don't provide some value for the API key, the anthropic SDK will raise an error. 

368 # However, we do want to use the environment variable if present when rewriting VCR cassettes. 

369 api_key = os.environ.get('ANTHROPIC_API_KEY', 'mock-value') 

370 agent = Agent( 

371 AnthropicModel('claude-3-5-haiku-latest', provider=AnthropicProvider(api_key=api_key)), 

372 system_prompt=system_prompt, 

373 tools=[retrieve_entity_info], 

374 ) 

375 

376 result = await agent.run('Alice, Bob, Charlie and Daisy are a family. Who is the youngest?') 

377 assert 'Daisy is the youngest' in result.data 

378 

379 all_messages = result.all_messages() 

380 first_response = all_messages[1] 

381 second_request = all_messages[2] 

382 assert first_response.parts == [ 

383 TextPart( 

384 content="I'll retrieve the information about each family member to determine their ages.", 

385 part_kind='text', 

386 ), 

387 ToolCallPart( 

388 tool_name='retrieve_entity_info', args={'name': 'Alice'}, tool_call_id=IsStr(), part_kind='tool-call' 

389 ), 

390 ToolCallPart( 

391 tool_name='retrieve_entity_info', args={'name': 'Bob'}, tool_call_id=IsStr(), part_kind='tool-call' 

392 ), 

393 ToolCallPart( 

394 tool_name='retrieve_entity_info', args={'name': 'Charlie'}, tool_call_id=IsStr(), part_kind='tool-call' 

395 ), 

396 ToolCallPart( 

397 tool_name='retrieve_entity_info', args={'name': 'Daisy'}, tool_call_id=IsStr(), part_kind='tool-call' 

398 ), 

399 ] 

400 assert second_request.parts == [ 

401 ToolReturnPart( 

402 tool_name='retrieve_entity_info', 

403 content="alice is bob's wife", 

404 tool_call_id=IsStr(), 

405 timestamp=IsDatetime(), 

406 part_kind='tool-return', 

407 ), 

408 ToolReturnPart( 

409 tool_name='retrieve_entity_info', 

410 content="bob is alice's husband", 

411 tool_call_id=IsStr(), 

412 timestamp=IsDatetime(), 

413 part_kind='tool-return', 

414 ), 

415 ToolReturnPart( 

416 tool_name='retrieve_entity_info', 

417 content="charlie is alice's son", 

418 tool_call_id=IsStr(), 

419 timestamp=IsDatetime(), 

420 part_kind='tool-return', 

421 ), 

422 ToolReturnPart( 

423 tool_name='retrieve_entity_info', 

424 content="daisy is bob's daughter and charlie's younger sister", 

425 tool_call_id=IsStr(), 

426 timestamp=IsDatetime(), 

427 part_kind='tool-return', 

428 ), 

429 ] 

430 

431 # Ensure the tool call IDs match between the tool calls and the tool returns 

432 tool_call_part_ids = [part.tool_call_id for part in first_response.parts if part.part_kind == 'tool-call'] 

433 tool_return_part_ids = [part.tool_call_id for part in second_request.parts if part.part_kind == 'tool-return'] 

434 assert len(set(tool_call_part_ids)) == 4 # ensure they are all unique 

435 assert tool_call_part_ids == tool_return_part_ids 

436 

437 

438async def test_anthropic_specific_metadata(allow_model_requests: None) -> None: 

439 c = completion_message([TextBlock(text='world', type='text')], AnthropicUsage(input_tokens=5, output_tokens=10)) 

440 mock_client = MockAnthropic.create_mock(c) 

441 m = AnthropicModel('claude-3-5-haiku-latest', provider=AnthropicProvider(anthropic_client=mock_client)) 

442 agent = Agent(m) 

443 

444 result = await agent.run('hello', model_settings=AnthropicModelSettings(anthropic_metadata={'user_id': '123'})) 

445 assert result.data == 'world' 

446 assert get_mock_chat_completion_kwargs(mock_client)[0]['metadata']['user_id'] == '123' 

447 

448 

449async def test_stream_structured(allow_model_requests: None): 

450 """Test streaming structured responses with Anthropic's API. 

451 

452 This test simulates how Anthropic streams tool calls: 

453 1. Message start 

454 2. Tool block start with initial data 

455 3. Tool block delta with additional data 

456 4. Tool block stop 

457 5. Update usage 

458 6. Message stop 

459 """ 

460 stream = [ 

461 RawMessageStartEvent( 

462 type='message_start', 

463 message=AnthropicMessage( 

464 id='msg_123', 

465 model='claude-3-5-haiku-latest', 

466 role='assistant', 

467 type='message', 

468 content=[], 

469 stop_reason=None, 

470 usage=AnthropicUsage(input_tokens=20, output_tokens=0), 

471 ), 

472 ), 

473 # Start tool block with initial data 

474 RawContentBlockStartEvent( 

475 type='content_block_start', 

476 index=0, 

477 content_block=ToolUseBlock(type='tool_use', id='tool_1', name='my_tool', input={'first': 'One'}), 

478 ), 

479 # Add more data through an incomplete JSON delta 

480 RawContentBlockDeltaEvent( 

481 type='content_block_delta', 

482 index=0, 

483 delta=InputJSONDelta(type='input_json_delta', partial_json='{"second":'), 

484 ), 

485 RawContentBlockDeltaEvent( 

486 type='content_block_delta', 

487 index=0, 

488 delta=InputJSONDelta(type='input_json_delta', partial_json='"Two"}'), 

489 ), 

490 # Mark tool block as complete 

491 RawContentBlockStopEvent(type='content_block_stop', index=0), 

492 # Update the top-level message with usage 

493 RawMessageDeltaEvent( 

494 type='message_delta', 

495 delta=Delta( 

496 stop_reason='end_turn', 

497 ), 

498 usage=MessageDeltaUsage( 

499 output_tokens=5, 

500 ), 

501 ), 

502 # Mark message as complete 

503 RawMessageStopEvent(type='message_stop'), 

504 ] 

505 

506 done_stream = [ 

507 RawMessageStartEvent( 

508 type='message_start', 

509 message=AnthropicMessage( 

510 id='msg_123', 

511 model='claude-3-5-haiku-latest', 

512 role='assistant', 

513 type='message', 

514 content=[], 

515 stop_reason=None, 

516 usage=AnthropicUsage(input_tokens=0, output_tokens=0), 

517 ), 

518 ), 

519 # Text block with final data 

520 RawContentBlockStartEvent( 

521 type='content_block_start', 

522 index=0, 

523 content_block=TextBlock(type='text', text='FINAL_PAYLOAD'), 

524 ), 

525 RawContentBlockStopEvent(type='content_block_stop', index=0), 

526 RawMessageStopEvent(type='message_stop'), 

527 ] 

528 

529 mock_client = MockAnthropic.create_stream_mock([stream, done_stream]) 

530 m = AnthropicModel('claude-3-5-haiku-latest', provider=AnthropicProvider(anthropic_client=mock_client)) 

531 agent = Agent(m) 

532 

533 tool_called = False 

534 

535 @agent.tool_plain 

536 async def my_tool(first: str, second: str) -> int: 

537 nonlocal tool_called 

538 tool_called = True 

539 return len(first) + len(second) 

540 

541 async with agent.run_stream('') as result: 

542 assert not result.is_complete 

543 chunks = [c async for c in result.stream(debounce_by=None)] 

544 

545 # The tool output doesn't echo any content to the stream, so we only get the final payload once when 

546 # the block starts and once when it ends. 

547 assert chunks == snapshot( 

548 [ 

549 'FINAL_PAYLOAD', 

550 'FINAL_PAYLOAD', 

551 ] 

552 ) 

553 assert result.is_complete 

554 assert result.usage() == snapshot(Usage(requests=2, request_tokens=20, response_tokens=5, total_tokens=25)) 

555 assert tool_called 

556 

557 

558@pytest.mark.vcr() 

559async def test_image_url_input(allow_model_requests: None, anthropic_api_key: str): 

560 m = AnthropicModel('claude-3-5-haiku-latest', provider=AnthropicProvider(api_key=anthropic_api_key)) 

561 agent = Agent(m) 

562 

563 result = await agent.run( 

564 [ 

565 'What is this vegetable?', 

566 ImageUrl(url='https://t3.ftcdn.net/jpg/00/85/79/92/360_F_85799278_0BBGV9OAdQDTLnKwAPBCcg1J7QtiieJY.jpg'), 

567 ] 

568 ) 

569 assert result.data == snapshot("""\ 

570This is a potato. It's a yellow-skinned potato with a somewhat oblong or oval shape. The surface is covered in small eyes or dimples, which is typical of potato skin. The color is a golden-yellow, and the potato appears to be clean and fresh, photographed against a white background. 

571 

572Potatoes are root vegetables that are staple foods in many cuisines around the world. They can be prepared in numerous ways such as boiling, baking, roasting, frying, or mashing. This particular potato looks like it could be a Yukon Gold or a similar yellow-fleshed variety.\ 

573""") 

574 

575 

576@pytest.mark.vcr() 

577async def test_image_url_input_invalid_mime_type(allow_model_requests: None, anthropic_api_key: str): 

578 m = AnthropicModel('claude-3-5-haiku-latest', provider=AnthropicProvider(api_key=anthropic_api_key)) 

579 agent = Agent(m) 

580 

581 result = await agent.run( 

582 [ 

583 'What animal is this?', 

584 ImageUrl( 

585 url='https://lh3.googleusercontent.com/proxy/YngsuS8jQJysXxeucAgVBcSgIdwZlSQ-HvsNxGjHS0SrUKXI161bNKh6SOcMsNUGsnxoOrS3AYX--MT4T3S3SoCgSD1xKrtBwwItcgexaX_7W-qHo-VupmYgjjzWO-BuORLp9-pj8Kjr' 

586 ), 

587 ] 

588 ) 

589 assert result.data == snapshot( 

590 'This is a Great Horned Owl (Bubo virginianus), a large and powerful owl species. It has distinctive ear tufts (the "horns"), large yellow eyes, and a mottled gray-brown plumage that provides excellent camouflage. In this image, the owl is perched on a branch, surrounded by soft yellow and green vegetation, which creates a beautiful, slightly blurred background that highlights the owl\'s sharp features. Great Horned Owls are known for their adaptability, wide distribution across the Americas, and their status as powerful nocturnal predators.' 

591 ) 

592 

593 

594@pytest.mark.parametrize('media_type', ('audio/wav', 'audio/mpeg')) 

595async def test_audio_as_binary_content_input(allow_model_requests: None, media_type: str): 

596 c = completion_message([TextBlock(text='world', type='text')], AnthropicUsage(input_tokens=5, output_tokens=10)) 

597 mock_client = MockAnthropic.create_mock(c) 

598 m = AnthropicModel('claude-3-5-haiku-latest', provider=AnthropicProvider(anthropic_client=mock_client)) 

599 agent = Agent(m) 

600 

601 base64_content = b'//uQZ' 

602 

603 with pytest.raises(RuntimeError, match='Only images and PDFs are supported for binary content'): 

604 await agent.run(['hello', BinaryContent(data=base64_content, media_type=media_type)]) 

605 

606 

607def test_model_status_error(allow_model_requests: None) -> None: 

608 mock_client = MockAnthropic.create_mock( 

609 APIStatusError( 

610 'test error', 

611 response=httpx.Response(status_code=500, request=httpx.Request('POST', 'https://example.com/v1')), 

612 body={'error': 'test error'}, 

613 ) 

614 ) 

615 m = AnthropicModel('claude-3-5-sonnet-latest', provider=AnthropicProvider(anthropic_client=mock_client)) 

616 agent = Agent(m) 

617 with pytest.raises(ModelHTTPError) as exc_info: 

618 agent.run_sync('hello') 

619 assert str(exc_info.value) == snapshot( 

620 "status_code: 500, model_name: claude-3-5-sonnet-latest, body: {'error': 'test error'}" 

621 ) 

622 

623 

624@pytest.mark.vcr() 

625async def test_document_binary_content_input( 

626 allow_model_requests: None, anthropic_api_key: str, document_content: BinaryContent 

627): 

628 m = AnthropicModel('claude-3-5-sonnet-latest', provider=AnthropicProvider(api_key=anthropic_api_key)) 

629 agent = Agent(m) 

630 

631 result = await agent.run(['What is the main content on this document?', document_content]) 

632 assert result.data == snapshot( 

633 'The document appears to be a simple PDF file with only the text "Dummy PDF file" displayed at the top. It appears to be mostly blank otherwise, likely serving as a template or placeholder document.' 

634 ) 

635 

636 

637@pytest.mark.vcr() 

638async def test_document_url_input(allow_model_requests: None, anthropic_api_key: str): 

639 m = AnthropicModel('claude-3-5-sonnet-latest', provider=AnthropicProvider(api_key=anthropic_api_key)) 

640 agent = Agent(m) 

641 

642 document_url = DocumentUrl(url='https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf') 

643 

644 result = await agent.run(['What is the main content on this document?', document_url]) 

645 assert result.data == snapshot( 

646 'The document appears to be a simple PDF file with only the text "Dummy PDF file" displayed at the top. It seems to be a blank or template document with minimal content.' 

647 ) 

648 

649 

650@pytest.mark.vcr() 

651async def test_text_document_url_input(allow_model_requests: None, anthropic_api_key: str): 

652 m = AnthropicModel('claude-3-5-sonnet-latest', provider=AnthropicProvider(api_key=anthropic_api_key)) 

653 agent = Agent(m) 

654 

655 text_document_url = DocumentUrl(url='https://example-files.online-convert.com/document/txt/example.txt') 

656 

657 result = await agent.run(['What is the main content on this document?', text_document_url]) 

658 assert result.data == snapshot("""\ 

659This document is a TXT test file that primarily contains information about the use of placeholder names, specifically focusing on "John Doe" and its variants. The main content explains how these placeholder names are used in legal contexts and popular culture, particularly in English-speaking countries. The text describes: 

660 

6611. The various placeholder names used: 

662- "John Doe" for males 

663- "Jane Doe" or "Jane Roe" for females 

664- "Jonnie Doe" and "Janie Doe" for children 

665- "Baby Doe" for unknown children 

666 

6672. The usage of these names in different English-speaking countries, noting that while common in the US and Canada, they're less used in the UK, where "Joe Bloggs" or "John Smith" are preferred. 

668 

6693. How these names are used in legal contexts, forms, and popular culture. 

670 

671The document is formatted as a test file with metadata including its purpose, file type, and version. It also includes attribution information indicating the content is from Wikipedia and is licensed under Attribution-ShareAlike 4.0.\ 

672""") 

673 

674 

675def test_init_with_provider(): 

676 provider = AnthropicProvider(api_key='api-key') 

677 model = AnthropicModel('claude-3-opus-latest', provider=provider) 

678 assert model.model_name == 'claude-3-opus-latest' 

679 assert model.client == provider.client 

680 

681 

682def test_init_with_provider_string(env: TestEnv): 

683 env.set('ANTHROPIC_API_KEY', 'env-api-key') 

684 model = AnthropicModel('claude-3-opus-latest', provider='anthropic') 

685 assert model.model_name == 'claude-3-opus-latest' 

686 assert model.client is not None