pydantic-ai-slim 0.6.0__py3-none-any.whl → 0.6.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pydantic_ai/__init__.py +5 -0
- pydantic_ai/_agent_graph.py +7 -0
- pydantic_ai/_utils.py +7 -1
- pydantic_ai/agent.py +9 -0
- pydantic_ai/builtin_tools.py +105 -0
- pydantic_ai/messages.py +73 -11
- pydantic_ai/models/__init__.py +17 -6
- pydantic_ai/models/anthropic.py +120 -12
- pydantic_ai/models/bedrock.py +31 -5
- pydantic_ai/models/cohere.py +10 -0
- pydantic_ai/models/function.py +9 -0
- pydantic_ai/models/gemini.py +5 -0
- pydantic_ai/models/google.py +36 -2
- pydantic_ai/models/groq.py +36 -3
- pydantic_ai/models/huggingface.py +9 -0
- pydantic_ai/models/mistral.py +16 -0
- pydantic_ai/models/openai.py +73 -12
- pydantic_ai/models/test.py +9 -0
- pydantic_ai/profiles/__init__.py +9 -1
- pydantic_ai/profiles/amazon.py +1 -2
- pydantic_ai/profiles/groq.py +23 -0
- pydantic_ai/profiles/meta.py +1 -2
- pydantic_ai/profiles/qwen.py +1 -2
- pydantic_ai/providers/bedrock.py +4 -3
- pydantic_ai/providers/groq.py +2 -0
- {pydantic_ai_slim-0.6.0.dist-info → pydantic_ai_slim-0.6.2.dist-info}/METADATA +5 -5
- {pydantic_ai_slim-0.6.0.dist-info → pydantic_ai_slim-0.6.2.dist-info}/RECORD +30 -28
- {pydantic_ai_slim-0.6.0.dist-info → pydantic_ai_slim-0.6.2.dist-info}/WHEEL +0 -0
- {pydantic_ai_slim-0.6.0.dist-info → pydantic_ai_slim-0.6.2.dist-info}/entry_points.txt +0 -0
- {pydantic_ai_slim-0.6.0.dist-info → pydantic_ai_slim-0.6.2.dist-info}/licenses/LICENSE +0 -0
pydantic_ai/models/bedrock.py
CHANGED
|
@@ -15,9 +15,12 @@ import anyio.to_thread
|
|
|
15
15
|
from typing_extensions import ParamSpec, assert_never
|
|
16
16
|
|
|
17
17
|
from pydantic_ai import _utils, usage
|
|
18
|
+
from pydantic_ai.exceptions import UserError
|
|
18
19
|
from pydantic_ai.messages import (
|
|
19
20
|
AudioUrl,
|
|
20
21
|
BinaryContent,
|
|
22
|
+
BuiltinToolCallPart,
|
|
23
|
+
BuiltinToolReturnPart,
|
|
21
24
|
DocumentUrl,
|
|
22
25
|
ImageUrl,
|
|
23
26
|
ModelMessage,
|
|
@@ -59,6 +62,8 @@ if TYPE_CHECKING:
|
|
|
59
62
|
MessageUnionTypeDef,
|
|
60
63
|
PerformanceConfigurationTypeDef,
|
|
61
64
|
PromptVariableValuesTypeDef,
|
|
65
|
+
ReasoningContentBlockOutputTypeDef,
|
|
66
|
+
ReasoningTextBlockTypeDef,
|
|
62
67
|
SystemContentBlockTypeDef,
|
|
63
68
|
ToolChoiceTypeDef,
|
|
64
69
|
ToolConfigurationTypeDef,
|
|
@@ -276,9 +281,10 @@ class BedrockConverseModel(Model):
|
|
|
276
281
|
if reasoning_content := item.get('reasoningContent'):
|
|
277
282
|
reasoning_text = reasoning_content.get('reasoningText')
|
|
278
283
|
if reasoning_text: # pragma: no branch
|
|
279
|
-
thinking_part = ThinkingPart(
|
|
280
|
-
|
|
281
|
-
|
|
284
|
+
thinking_part = ThinkingPart(
|
|
285
|
+
content=reasoning_text['text'],
|
|
286
|
+
signature=reasoning_text.get('signature'),
|
|
287
|
+
)
|
|
282
288
|
items.append(thinking_part)
|
|
283
289
|
if text := item.get('text'):
|
|
284
290
|
items.append(TextPart(content=text))
|
|
@@ -339,6 +345,9 @@ class BedrockConverseModel(Model):
|
|
|
339
345
|
if tool_config:
|
|
340
346
|
params['toolConfig'] = tool_config
|
|
341
347
|
|
|
348
|
+
if model_request_parameters.builtin_tools:
|
|
349
|
+
raise UserError('Bedrock does not support built-in tools')
|
|
350
|
+
|
|
342
351
|
# Bedrock supports a set of specific extra parameters
|
|
343
352
|
if model_settings:
|
|
344
353
|
if guardrail_config := model_settings.get('bedrock_guardrail_config', None):
|
|
@@ -462,7 +471,20 @@ class BedrockConverseModel(Model):
|
|
|
462
471
|
if isinstance(item, TextPart):
|
|
463
472
|
content.append({'text': item.content})
|
|
464
473
|
elif isinstance(item, ThinkingPart):
|
|
465
|
-
|
|
474
|
+
if BedrockModelProfile.from_profile(self.profile).bedrock_send_back_thinking_parts:
|
|
475
|
+
reasoning_text: ReasoningTextBlockTypeDef = {
|
|
476
|
+
'text': item.content,
|
|
477
|
+
}
|
|
478
|
+
if item.signature:
|
|
479
|
+
reasoning_text['signature'] = item.signature
|
|
480
|
+
reasoning_content: ReasoningContentBlockOutputTypeDef = {
|
|
481
|
+
'reasoningText': reasoning_text,
|
|
482
|
+
}
|
|
483
|
+
content.append({'reasoningContent': reasoning_content})
|
|
484
|
+
else:
|
|
485
|
+
# NOTE: We don't pass the thinking part to Bedrock for models other than Claude since it raises an error.
|
|
486
|
+
pass
|
|
487
|
+
elif isinstance(item, (BuiltinToolCallPart, BuiltinToolReturnPart)):
|
|
466
488
|
pass
|
|
467
489
|
else:
|
|
468
490
|
assert isinstance(item, ToolCallPart)
|
|
@@ -610,7 +632,11 @@ class BedrockStreamedResponse(StreamedResponse):
|
|
|
610
632
|
delta = chunk['contentBlockDelta']['delta']
|
|
611
633
|
if 'reasoningContent' in delta:
|
|
612
634
|
if text := delta['reasoningContent'].get('text'):
|
|
613
|
-
yield self._parts_manager.handle_thinking_delta(
|
|
635
|
+
yield self._parts_manager.handle_thinking_delta(
|
|
636
|
+
vendor_part_id=index,
|
|
637
|
+
content=text,
|
|
638
|
+
signature=delta['reasoningContent'].get('signature'),
|
|
639
|
+
)
|
|
614
640
|
else: # pragma: no cover
|
|
615
641
|
warnings.warn(
|
|
616
642
|
f'Only text reasoning content is supported yet, but you got {delta["reasoningContent"]}. '
|
pydantic_ai/models/cohere.py
CHANGED
|
@@ -7,10 +7,13 @@ from typing import Literal, Union, cast
|
|
|
7
7
|
from typing_extensions import assert_never
|
|
8
8
|
|
|
9
9
|
from pydantic_ai._thinking_part import split_content_into_text_and_thinking
|
|
10
|
+
from pydantic_ai.exceptions import UserError
|
|
10
11
|
|
|
11
12
|
from .. import ModelHTTPError, usage
|
|
12
13
|
from .._utils import generate_tool_call_id as _generate_tool_call_id, guard_tool_call_id as _guard_tool_call_id
|
|
13
14
|
from ..messages import (
|
|
15
|
+
BuiltinToolCallPart,
|
|
16
|
+
BuiltinToolReturnPart,
|
|
14
17
|
ModelMessage,
|
|
15
18
|
ModelRequest,
|
|
16
19
|
ModelResponse,
|
|
@@ -166,6 +169,10 @@ class CohereModel(Model):
|
|
|
166
169
|
model_request_parameters: ModelRequestParameters,
|
|
167
170
|
) -> V2ChatResponse:
|
|
168
171
|
tools = self._get_tools(model_request_parameters)
|
|
172
|
+
|
|
173
|
+
if model_request_parameters.builtin_tools:
|
|
174
|
+
raise UserError('Cohere does not support built-in tools')
|
|
175
|
+
|
|
169
176
|
cohere_messages = self._map_messages(messages)
|
|
170
177
|
try:
|
|
171
178
|
return await self.client.chat(
|
|
@@ -223,6 +230,9 @@ class CohereModel(Model):
|
|
|
223
230
|
pass
|
|
224
231
|
elif isinstance(item, ToolCallPart):
|
|
225
232
|
tool_calls.append(self._map_tool_call(item))
|
|
233
|
+
elif isinstance(item, (BuiltinToolCallPart, BuiltinToolReturnPart)): # pragma: no cover
|
|
234
|
+
# This is currently never returned from cohere
|
|
235
|
+
pass
|
|
226
236
|
else:
|
|
227
237
|
assert_never(item)
|
|
228
238
|
message_param = AssistantChatMessageV2(role='assistant')
|
pydantic_ai/models/function.py
CHANGED
|
@@ -17,6 +17,8 @@ from .. import _utils, usage
|
|
|
17
17
|
from .._utils import PeekableAsyncStream
|
|
18
18
|
from ..messages import (
|
|
19
19
|
BinaryContent,
|
|
20
|
+
BuiltinToolCallPart,
|
|
21
|
+
BuiltinToolReturnPart,
|
|
20
22
|
ModelMessage,
|
|
21
23
|
ModelRequest,
|
|
22
24
|
ModelResponse,
|
|
@@ -331,6 +333,13 @@ def _estimate_usage(messages: Iterable[ModelMessage]) -> usage.Usage:
|
|
|
331
333
|
response_tokens += _estimate_string_tokens(part.content)
|
|
332
334
|
elif isinstance(part, ToolCallPart):
|
|
333
335
|
response_tokens += 1 + _estimate_string_tokens(part.args_as_json_str())
|
|
336
|
+
# TODO(Marcelo): We need to add coverage here.
|
|
337
|
+
elif isinstance(part, BuiltinToolCallPart): # pragma: no cover
|
|
338
|
+
call = part
|
|
339
|
+
response_tokens += 1 + _estimate_string_tokens(call.args_as_json_str())
|
|
340
|
+
# TODO(Marcelo): We need to add coverage here.
|
|
341
|
+
elif isinstance(part, BuiltinToolReturnPart): # pragma: no cover
|
|
342
|
+
response_tokens += _estimate_string_tokens(part.model_response_str())
|
|
334
343
|
else:
|
|
335
344
|
assert_never(part)
|
|
336
345
|
else:
|
pydantic_ai/models/gemini.py
CHANGED
|
@@ -20,6 +20,8 @@ from .._output import OutputObjectDefinition
|
|
|
20
20
|
from ..exceptions import UserError
|
|
21
21
|
from ..messages import (
|
|
22
22
|
BinaryContent,
|
|
23
|
+
BuiltinToolCallPart,
|
|
24
|
+
BuiltinToolReturnPart,
|
|
23
25
|
FileUrl,
|
|
24
26
|
ModelMessage,
|
|
25
27
|
ModelRequest,
|
|
@@ -610,6 +612,9 @@ def _content_model_response(m: ModelResponse) -> _GeminiContent:
|
|
|
610
612
|
elif isinstance(item, TextPart):
|
|
611
613
|
if item.content:
|
|
612
614
|
parts.append(_GeminiTextPart(text=item.content))
|
|
615
|
+
elif isinstance(item, (BuiltinToolCallPart, BuiltinToolReturnPart)): # pragma: no cover
|
|
616
|
+
# This is currently never returned from gemini
|
|
617
|
+
pass
|
|
613
618
|
else:
|
|
614
619
|
assert_never(item)
|
|
615
620
|
return _GeminiContent(role='model', parts=parts)
|
pydantic_ai/models/google.py
CHANGED
|
@@ -8,13 +8,17 @@ from datetime import datetime
|
|
|
8
8
|
from typing import Any, Literal, Union, cast, overload
|
|
9
9
|
from uuid import uuid4
|
|
10
10
|
|
|
11
|
+
from google.genai.types import ExecutableCodeDict
|
|
11
12
|
from typing_extensions import assert_never
|
|
12
13
|
|
|
13
14
|
from .. import UnexpectedModelBehavior, _utils, usage
|
|
14
15
|
from .._output import OutputObjectDefinition
|
|
16
|
+
from ..builtin_tools import CodeExecutionTool, WebSearchTool
|
|
15
17
|
from ..exceptions import UserError
|
|
16
18
|
from ..messages import (
|
|
17
19
|
BinaryContent,
|
|
20
|
+
BuiltinToolCallPart,
|
|
21
|
+
BuiltinToolReturnPart,
|
|
18
22
|
FileUrl,
|
|
19
23
|
ModelMessage,
|
|
20
24
|
ModelRequest,
|
|
@@ -54,12 +58,14 @@ try:
|
|
|
54
58
|
FunctionDeclarationDict,
|
|
55
59
|
GenerateContentConfigDict,
|
|
56
60
|
GenerateContentResponse,
|
|
61
|
+
GoogleSearchDict,
|
|
57
62
|
HttpOptionsDict,
|
|
58
63
|
MediaResolution,
|
|
59
64
|
Part,
|
|
60
65
|
PartDict,
|
|
61
66
|
SafetySettingDict,
|
|
62
67
|
ThinkingConfigDict,
|
|
68
|
+
ToolCodeExecutionDict,
|
|
63
69
|
ToolConfigDict,
|
|
64
70
|
ToolDict,
|
|
65
71
|
ToolListUnionDict,
|
|
@@ -213,6 +219,11 @@ class GoogleModel(Model):
|
|
|
213
219
|
ToolDict(function_declarations=[_function_declaration_from_tool(t)])
|
|
214
220
|
for t in model_request_parameters.output_tools
|
|
215
221
|
]
|
|
222
|
+
for tool in model_request_parameters.builtin_tools:
|
|
223
|
+
if isinstance(tool, WebSearchTool):
|
|
224
|
+
tools.append(ToolDict(google_search=GoogleSearchDict()))
|
|
225
|
+
elif isinstance(tool, CodeExecutionTool): # pragma: no branch
|
|
226
|
+
tools.append(ToolDict(code_execution=ToolCodeExecutionDict()))
|
|
216
227
|
return tools or None
|
|
217
228
|
|
|
218
229
|
def _get_tool_config(
|
|
@@ -499,6 +510,14 @@ def _content_model_response(m: ModelResponse) -> ContentDict:
|
|
|
499
510
|
# please open an issue. The below code is the code to send thinking to the provider.
|
|
500
511
|
# parts.append({'text': item.content, 'thought': True})
|
|
501
512
|
pass
|
|
513
|
+
elif isinstance(item, BuiltinToolCallPart):
|
|
514
|
+
if item.provider_name == 'google':
|
|
515
|
+
if item.tool_name == 'code_execution': # pragma: no branch
|
|
516
|
+
parts.append({'executable_code': cast(ExecutableCodeDict, item.args)})
|
|
517
|
+
elif isinstance(item, BuiltinToolReturnPart):
|
|
518
|
+
if item.provider_name == 'google':
|
|
519
|
+
if item.tool_name == 'code_execution': # pragma: no branch
|
|
520
|
+
parts.append({'code_execution_result': item.content})
|
|
502
521
|
else:
|
|
503
522
|
assert_never(item)
|
|
504
523
|
return ContentDict(role='model', parts=parts)
|
|
@@ -513,7 +532,22 @@ def _process_response_from_parts(
|
|
|
513
532
|
) -> ModelResponse:
|
|
514
533
|
items: list[ModelResponsePart] = []
|
|
515
534
|
for part in parts:
|
|
516
|
-
if part.
|
|
535
|
+
if part.executable_code is not None:
|
|
536
|
+
items.append(
|
|
537
|
+
BuiltinToolCallPart(
|
|
538
|
+
provider_name='google', args=part.executable_code.model_dump(), tool_name='code_execution'
|
|
539
|
+
)
|
|
540
|
+
)
|
|
541
|
+
elif part.code_execution_result is not None:
|
|
542
|
+
items.append(
|
|
543
|
+
BuiltinToolReturnPart(
|
|
544
|
+
provider_name='google',
|
|
545
|
+
tool_name='code_execution',
|
|
546
|
+
content=part.code_execution_result,
|
|
547
|
+
tool_call_id='not_provided',
|
|
548
|
+
)
|
|
549
|
+
)
|
|
550
|
+
elif part.text is not None:
|
|
517
551
|
if part.thought:
|
|
518
552
|
items.append(ThinkingPart(content=part.text))
|
|
519
553
|
else:
|
|
@@ -563,7 +597,7 @@ def _metadata_as_usage(response: GenerateContentResponse) -> usage.Usage:
|
|
|
563
597
|
details['thoughts_tokens'] = thoughts_token_count
|
|
564
598
|
|
|
565
599
|
if tool_use_prompt_token_count := metadata.get('tool_use_prompt_token_count'):
|
|
566
|
-
details['tool_use_prompt_tokens'] = tool_use_prompt_token_count
|
|
600
|
+
details['tool_use_prompt_tokens'] = tool_use_prompt_token_count
|
|
567
601
|
|
|
568
602
|
for key, metadata_details in metadata.items():
|
|
569
603
|
if key.endswith('_details') and metadata_details:
|
pydantic_ai/models/groq.py
CHANGED
|
@@ -10,11 +10,16 @@ from typing import Literal, Union, cast, overload
|
|
|
10
10
|
from typing_extensions import assert_never
|
|
11
11
|
|
|
12
12
|
from pydantic_ai._thinking_part import split_content_into_text_and_thinking
|
|
13
|
+
from pydantic_ai.exceptions import UserError
|
|
14
|
+
from pydantic_ai.profiles.groq import GroqModelProfile
|
|
13
15
|
|
|
14
16
|
from .. import ModelHTTPError, UnexpectedModelBehavior, _utils, usage
|
|
15
|
-
from .._utils import guard_tool_call_id as _guard_tool_call_id, number_to_datetime
|
|
17
|
+
from .._utils import generate_tool_call_id, guard_tool_call_id as _guard_tool_call_id, number_to_datetime
|
|
18
|
+
from ..builtin_tools import CodeExecutionTool, WebSearchTool
|
|
16
19
|
from ..messages import (
|
|
17
20
|
BinaryContent,
|
|
21
|
+
BuiltinToolCallPart,
|
|
22
|
+
BuiltinToolReturnPart,
|
|
18
23
|
DocumentUrl,
|
|
19
24
|
ImageUrl,
|
|
20
25
|
ModelMessage,
|
|
@@ -212,7 +217,7 @@ class GroqModel(Model):
|
|
|
212
217
|
model_request_parameters: ModelRequestParameters,
|
|
213
218
|
) -> chat.ChatCompletion | AsyncStream[chat.ChatCompletionChunk]:
|
|
214
219
|
tools = self._get_tools(model_request_parameters)
|
|
215
|
-
|
|
220
|
+
tools += self._get_builtin_tools(model_request_parameters)
|
|
216
221
|
if not tools:
|
|
217
222
|
tool_choice: Literal['none', 'required', 'auto'] | None = None
|
|
218
223
|
elif not model_request_parameters.allow_text_output:
|
|
@@ -226,7 +231,7 @@ class GroqModel(Model):
|
|
|
226
231
|
extra_headers = model_settings.get('extra_headers', {})
|
|
227
232
|
extra_headers.setdefault('User-Agent', get_user_agent())
|
|
228
233
|
return await self.client.chat.completions.create(
|
|
229
|
-
model=
|
|
234
|
+
model=self._model_name,
|
|
230
235
|
messages=groq_messages,
|
|
231
236
|
n=1,
|
|
232
237
|
parallel_tool_calls=model_settings.get('parallel_tool_calls', NOT_GIVEN),
|
|
@@ -256,6 +261,19 @@ class GroqModel(Model):
|
|
|
256
261
|
timestamp = number_to_datetime(response.created)
|
|
257
262
|
choice = response.choices[0]
|
|
258
263
|
items: list[ModelResponsePart] = []
|
|
264
|
+
if choice.message.executed_tools:
|
|
265
|
+
for tool in choice.message.executed_tools:
|
|
266
|
+
tool_call_id = generate_tool_call_id()
|
|
267
|
+
items.append(
|
|
268
|
+
BuiltinToolCallPart(
|
|
269
|
+
tool_name=tool.type, args=tool.arguments, provider_name='groq', tool_call_id=tool_call_id
|
|
270
|
+
)
|
|
271
|
+
)
|
|
272
|
+
items.append(
|
|
273
|
+
BuiltinToolReturnPart(
|
|
274
|
+
provider_name='groq', tool_name=tool.type, content=tool.output, tool_call_id=tool_call_id
|
|
275
|
+
)
|
|
276
|
+
)
|
|
259
277
|
# NOTE: The `reasoning` field is only present if `groq_reasoning_format` is set to `parsed`.
|
|
260
278
|
if choice.message.reasoning is not None:
|
|
261
279
|
items.append(ThinkingPart(content=choice.message.reasoning))
|
|
@@ -291,6 +309,18 @@ class GroqModel(Model):
|
|
|
291
309
|
tools += [self._map_tool_definition(r) for r in model_request_parameters.output_tools]
|
|
292
310
|
return tools
|
|
293
311
|
|
|
312
|
+
def _get_builtin_tools(
|
|
313
|
+
self, model_request_parameters: ModelRequestParameters
|
|
314
|
+
) -> list[chat.ChatCompletionToolParam]:
|
|
315
|
+
tools: list[chat.ChatCompletionToolParam] = []
|
|
316
|
+
for tool in model_request_parameters.builtin_tools:
|
|
317
|
+
if isinstance(tool, WebSearchTool):
|
|
318
|
+
if not GroqModelProfile.from_profile(self.profile).groq_always_has_web_search_builtin_tool:
|
|
319
|
+
raise UserError('`WebSearchTool` is not supported by Groq') # pragma: no cover
|
|
320
|
+
elif isinstance(tool, CodeExecutionTool): # pragma: no branch
|
|
321
|
+
raise UserError('`CodeExecutionTool` is not supported by Groq')
|
|
322
|
+
return tools
|
|
323
|
+
|
|
294
324
|
def _map_messages(self, messages: list[ModelMessage]) -> list[chat.ChatCompletionMessageParam]:
|
|
295
325
|
"""Just maps a `pydantic_ai.Message` to a `groq.types.ChatCompletionMessageParam`."""
|
|
296
326
|
groq_messages: list[chat.ChatCompletionMessageParam] = []
|
|
@@ -308,6 +338,9 @@ class GroqModel(Model):
|
|
|
308
338
|
elif isinstance(item, ThinkingPart):
|
|
309
339
|
# Skip thinking parts when mapping to Groq messages
|
|
310
340
|
continue
|
|
341
|
+
elif isinstance(item, (BuiltinToolCallPart, BuiltinToolReturnPart)): # pragma: no cover
|
|
342
|
+
# This is currently never returned from groq
|
|
343
|
+
pass
|
|
311
344
|
else:
|
|
312
345
|
assert_never(item)
|
|
313
346
|
message_param = chat.ChatCompletionAssistantMessageParam(role='assistant')
|
|
@@ -10,6 +10,7 @@ from typing import Literal, Union, cast, overload
|
|
|
10
10
|
from typing_extensions import assert_never
|
|
11
11
|
|
|
12
12
|
from pydantic_ai._thinking_part import split_content_into_text_and_thinking
|
|
13
|
+
from pydantic_ai.exceptions import UserError
|
|
13
14
|
from pydantic_ai.providers import Provider, infer_provider
|
|
14
15
|
|
|
15
16
|
from .. import ModelHTTPError, UnexpectedModelBehavior, _utils, usage
|
|
@@ -17,6 +18,8 @@ from .._utils import guard_tool_call_id as _guard_tool_call_id, now_utc as _now_
|
|
|
17
18
|
from ..messages import (
|
|
18
19
|
AudioUrl,
|
|
19
20
|
BinaryContent,
|
|
21
|
+
BuiltinToolCallPart,
|
|
22
|
+
BuiltinToolReturnPart,
|
|
20
23
|
DocumentUrl,
|
|
21
24
|
ImageUrl,
|
|
22
25
|
ModelMessage,
|
|
@@ -198,6 +201,9 @@ class HuggingFaceModel(Model):
|
|
|
198
201
|
else:
|
|
199
202
|
tool_choice = 'auto'
|
|
200
203
|
|
|
204
|
+
if model_request_parameters.builtin_tools:
|
|
205
|
+
raise UserError('HuggingFace does not support built-in tools')
|
|
206
|
+
|
|
201
207
|
hf_messages = await self._map_messages(messages)
|
|
202
208
|
|
|
203
209
|
try:
|
|
@@ -301,6 +307,9 @@ class HuggingFaceModel(Model):
|
|
|
301
307
|
# please open an issue. The below code is the code to send thinking to the provider.
|
|
302
308
|
# texts.append(f'<think>\n{item.content}\n</think>')
|
|
303
309
|
pass
|
|
310
|
+
elif isinstance(item, (BuiltinToolCallPart, BuiltinToolReturnPart)): # pragma: no cover
|
|
311
|
+
# This is currently never returned from huggingface
|
|
312
|
+
pass
|
|
304
313
|
else:
|
|
305
314
|
assert_never(item)
|
|
306
315
|
message_param = ChatCompletionInputMessage(role='assistant') # type: ignore
|
pydantic_ai/models/mistral.py
CHANGED
|
@@ -12,11 +12,14 @@ from httpx import Timeout
|
|
|
12
12
|
from typing_extensions import assert_never
|
|
13
13
|
|
|
14
14
|
from pydantic_ai._thinking_part import split_content_into_text_and_thinking
|
|
15
|
+
from pydantic_ai.exceptions import UserError
|
|
15
16
|
|
|
16
17
|
from .. import ModelHTTPError, UnexpectedModelBehavior, _utils
|
|
17
18
|
from .._utils import generate_tool_call_id as _generate_tool_call_id, now_utc as _now_utc, number_to_datetime
|
|
18
19
|
from ..messages import (
|
|
19
20
|
BinaryContent,
|
|
21
|
+
BuiltinToolCallPart,
|
|
22
|
+
BuiltinToolReturnPart,
|
|
20
23
|
DocumentUrl,
|
|
21
24
|
ImageUrl,
|
|
22
25
|
ModelMessage,
|
|
@@ -199,6 +202,11 @@ class MistralModel(Model):
|
|
|
199
202
|
model_request_parameters: ModelRequestParameters,
|
|
200
203
|
) -> MistralChatCompletionResponse:
|
|
201
204
|
"""Make a non-streaming request to the model."""
|
|
205
|
+
# TODO(Marcelo): We need to replace the current MistralAI client to use the beta client.
|
|
206
|
+
# See https://docs.mistral.ai/agents/connectors/websearch/ to support web search.
|
|
207
|
+
if model_request_parameters.builtin_tools:
|
|
208
|
+
raise UserError('Mistral does not support built-in tools')
|
|
209
|
+
|
|
202
210
|
try:
|
|
203
211
|
response = await self.client.chat.complete_async(
|
|
204
212
|
model=str(self._model_name),
|
|
@@ -233,6 +241,11 @@ class MistralModel(Model):
|
|
|
233
241
|
response: MistralEventStreamAsync[MistralCompletionEvent] | None
|
|
234
242
|
mistral_messages = self._map_messages(messages)
|
|
235
243
|
|
|
244
|
+
# TODO(Marcelo): We need to replace the current MistralAI client to use the beta client.
|
|
245
|
+
# See https://docs.mistral.ai/agents/connectors/websearch/ to support web search.
|
|
246
|
+
if model_request_parameters.builtin_tools:
|
|
247
|
+
raise UserError('Mistral does not support built-in tools')
|
|
248
|
+
|
|
236
249
|
if (
|
|
237
250
|
model_request_parameters.output_tools
|
|
238
251
|
and model_request_parameters.function_tools
|
|
@@ -502,6 +515,9 @@ class MistralModel(Model):
|
|
|
502
515
|
pass
|
|
503
516
|
elif isinstance(part, ToolCallPart):
|
|
504
517
|
tool_calls.append(self._map_tool_call(part))
|
|
518
|
+
elif isinstance(part, (BuiltinToolCallPart, BuiltinToolReturnPart)): # pragma: no cover
|
|
519
|
+
# This is currently never returned from mistral
|
|
520
|
+
pass
|
|
505
521
|
else:
|
|
506
522
|
assert_never(part)
|
|
507
523
|
mistral_messages.append(MistralAssistantMessage(content=content_chunks, tool_calls=tool_calls))
|
pydantic_ai/models/openai.py
CHANGED
|
@@ -11,16 +11,18 @@ from typing import Any, Literal, Union, cast, overload
|
|
|
11
11
|
from pydantic import ValidationError
|
|
12
12
|
from typing_extensions import assert_never
|
|
13
13
|
|
|
14
|
-
from pydantic_ai.
|
|
15
|
-
from pydantic_ai.profiles.openai import OpenAIModelProfile
|
|
16
|
-
from pydantic_ai.providers import Provider, infer_provider
|
|
14
|
+
from pydantic_ai.exceptions import UserError
|
|
17
15
|
|
|
18
16
|
from .. import ModelHTTPError, UnexpectedModelBehavior, _utils, usage
|
|
19
17
|
from .._output import DEFAULT_OUTPUT_TOOL_NAME, OutputObjectDefinition
|
|
18
|
+
from .._thinking_part import split_content_into_text_and_thinking
|
|
20
19
|
from .._utils import guard_tool_call_id as _guard_tool_call_id, now_utc as _now_utc, number_to_datetime
|
|
20
|
+
from ..builtin_tools import CodeExecutionTool, WebSearchTool
|
|
21
21
|
from ..messages import (
|
|
22
22
|
AudioUrl,
|
|
23
23
|
BinaryContent,
|
|
24
|
+
BuiltinToolCallPart,
|
|
25
|
+
BuiltinToolReturnPart,
|
|
24
26
|
DocumentUrl,
|
|
25
27
|
ImageUrl,
|
|
26
28
|
ModelMessage,
|
|
@@ -38,16 +40,11 @@ from ..messages import (
|
|
|
38
40
|
VideoUrl,
|
|
39
41
|
)
|
|
40
42
|
from ..profiles import ModelProfile, ModelProfileSpec
|
|
43
|
+
from ..profiles.openai import OpenAIModelProfile
|
|
44
|
+
from ..providers import Provider, infer_provider
|
|
41
45
|
from ..settings import ModelSettings
|
|
42
46
|
from ..tools import ToolDefinition
|
|
43
|
-
from . import
|
|
44
|
-
Model,
|
|
45
|
-
ModelRequestParameters,
|
|
46
|
-
StreamedResponse,
|
|
47
|
-
check_allow_model_requests,
|
|
48
|
-
download_item,
|
|
49
|
-
get_user_agent,
|
|
50
|
-
)
|
|
47
|
+
from . import Model, ModelRequestParameters, StreamedResponse, check_allow_model_requests, download_item, get_user_agent
|
|
51
48
|
|
|
52
49
|
try:
|
|
53
50
|
from openai import NOT_GIVEN, APIStatusError, AsyncOpenAI, AsyncStream, NotGiven
|
|
@@ -63,6 +60,11 @@ try:
|
|
|
63
60
|
from openai.types.chat.chat_completion_content_part_input_audio_param import InputAudio
|
|
64
61
|
from openai.types.chat.chat_completion_content_part_param import File, FileFile
|
|
65
62
|
from openai.types.chat.chat_completion_prediction_content_param import ChatCompletionPredictionContentParam
|
|
63
|
+
from openai.types.chat.completion_create_params import (
|
|
64
|
+
WebSearchOptions,
|
|
65
|
+
WebSearchOptionsUserLocation,
|
|
66
|
+
WebSearchOptionsUserLocationApproximate,
|
|
67
|
+
)
|
|
66
68
|
from openai.types.responses import ComputerToolParam, FileSearchToolParam, WebSearchToolParam
|
|
67
69
|
from openai.types.responses.response_input_param import FunctionCallOutput, Message
|
|
68
70
|
from openai.types.shared import ReasoningEffort
|
|
@@ -298,6 +300,8 @@ class OpenAIModel(Model):
|
|
|
298
300
|
model_request_parameters: ModelRequestParameters,
|
|
299
301
|
) -> chat.ChatCompletion | AsyncStream[ChatCompletionChunk]:
|
|
300
302
|
tools = self._get_tools(model_request_parameters)
|
|
303
|
+
web_search_options = self._get_web_search_options(model_request_parameters)
|
|
304
|
+
|
|
301
305
|
if not tools:
|
|
302
306
|
tool_choice: Literal['none', 'required', 'auto'] | None = None
|
|
303
307
|
elif (
|
|
@@ -344,6 +348,7 @@ class OpenAIModel(Model):
|
|
|
344
348
|
seed=model_settings.get('seed', NOT_GIVEN),
|
|
345
349
|
reasoning_effort=model_settings.get('openai_reasoning_effort', NOT_GIVEN),
|
|
346
350
|
user=model_settings.get('openai_user', NOT_GIVEN),
|
|
351
|
+
web_search_options=web_search_options or NOT_GIVEN,
|
|
347
352
|
service_tier=model_settings.get('openai_service_tier', NOT_GIVEN),
|
|
348
353
|
prediction=model_settings.get('openai_prediction', NOT_GIVEN),
|
|
349
354
|
temperature=sampling_settings.get('temperature', NOT_GIVEN),
|
|
@@ -444,6 +449,21 @@ class OpenAIModel(Model):
|
|
|
444
449
|
tools += [self._map_tool_definition(r) for r in model_request_parameters.output_tools]
|
|
445
450
|
return tools
|
|
446
451
|
|
|
452
|
+
def _get_web_search_options(self, model_request_parameters: ModelRequestParameters) -> WebSearchOptions | None:
|
|
453
|
+
for tool in model_request_parameters.builtin_tools:
|
|
454
|
+
if isinstance(tool, WebSearchTool): # pragma: no branch
|
|
455
|
+
if tool.user_location:
|
|
456
|
+
return WebSearchOptions(
|
|
457
|
+
search_context_size=tool.search_context_size,
|
|
458
|
+
user_location=WebSearchOptionsUserLocation(
|
|
459
|
+
type='approximate',
|
|
460
|
+
approximate=WebSearchOptionsUserLocationApproximate(**tool.user_location),
|
|
461
|
+
),
|
|
462
|
+
)
|
|
463
|
+
return WebSearchOptions(search_context_size=tool.search_context_size)
|
|
464
|
+
elif isinstance(tool, CodeExecutionTool): # pragma: no branch
|
|
465
|
+
raise UserError('`CodeExecutionTool` is not supported by OpenAI')
|
|
466
|
+
|
|
447
467
|
async def _map_messages(self, messages: list[ModelMessage]) -> list[chat.ChatCompletionMessageParam]:
|
|
448
468
|
"""Just maps a `pydantic_ai.Message` to a `openai.types.ChatCompletionMessageParam`."""
|
|
449
469
|
openai_messages: list[chat.ChatCompletionMessageParam] = []
|
|
@@ -464,6 +484,9 @@ class OpenAIModel(Model):
|
|
|
464
484
|
pass
|
|
465
485
|
elif isinstance(item, ToolCallPart):
|
|
466
486
|
tool_calls.append(self._map_tool_call(item))
|
|
487
|
+
# OpenAI doesn't return built-in tool calls
|
|
488
|
+
elif isinstance(item, (BuiltinToolCallPart, BuiltinToolReturnPart)): # pragma: no cover
|
|
489
|
+
pass
|
|
467
490
|
else:
|
|
468
491
|
assert_never(item)
|
|
469
492
|
message_param = chat.ChatCompletionAssistantMessageParam(role='assistant')
|
|
@@ -753,7 +776,7 @@ class OpenAIResponsesModel(Model):
|
|
|
753
776
|
model_request_parameters: ModelRequestParameters,
|
|
754
777
|
) -> responses.Response | AsyncStream[responses.ResponseStreamEvent]:
|
|
755
778
|
tools = self._get_tools(model_request_parameters)
|
|
756
|
-
tools =
|
|
779
|
+
tools = self._get_builtin_tools(model_request_parameters) + tools
|
|
757
780
|
|
|
758
781
|
if not tools:
|
|
759
782
|
tool_choice: Literal['none', 'required', 'auto'] | None = None
|
|
@@ -841,6 +864,22 @@ class OpenAIResponsesModel(Model):
|
|
|
841
864
|
tools += [self._map_tool_definition(r) for r in model_request_parameters.output_tools]
|
|
842
865
|
return tools
|
|
843
866
|
|
|
867
|
+
def _get_builtin_tools(self, model_request_parameters: ModelRequestParameters) -> list[responses.ToolParam]:
|
|
868
|
+
tools: list[responses.ToolParam] = []
|
|
869
|
+
for tool in model_request_parameters.builtin_tools:
|
|
870
|
+
if isinstance(tool, WebSearchTool):
|
|
871
|
+
web_search_tool = responses.WebSearchToolParam(
|
|
872
|
+
type='web_search_preview', search_context_size=tool.search_context_size
|
|
873
|
+
)
|
|
874
|
+
if tool.user_location:
|
|
875
|
+
web_search_tool['user_location'] = responses.web_search_tool_param.UserLocation(
|
|
876
|
+
type='approximate', **tool.user_location
|
|
877
|
+
)
|
|
878
|
+
tools.append(web_search_tool)
|
|
879
|
+
elif isinstance(tool, CodeExecutionTool): # pragma: no branch
|
|
880
|
+
tools.append({'type': 'code_interpreter', 'container': {'type': 'auto'}})
|
|
881
|
+
return tools
|
|
882
|
+
|
|
844
883
|
def _map_tool_definition(self, f: ToolDefinition) -> responses.FunctionToolParam:
|
|
845
884
|
return {
|
|
846
885
|
'name': f.name,
|
|
@@ -895,6 +934,9 @@ class OpenAIResponsesModel(Model):
|
|
|
895
934
|
openai_messages.append(responses.EasyInputMessageParam(role='assistant', content=item.content))
|
|
896
935
|
elif isinstance(item, ToolCallPart):
|
|
897
936
|
openai_messages.append(self._map_tool_call(item))
|
|
937
|
+
# OpenAI doesn't return built-in tool calls
|
|
938
|
+
elif isinstance(item, (BuiltinToolCallPart, BuiltinToolReturnPart)):
|
|
939
|
+
pass
|
|
898
940
|
elif isinstance(item, ThinkingPart):
|
|
899
941
|
# NOTE: We don't send ThinkingPart to the providers yet. If you are unsatisfied with this,
|
|
900
942
|
# please open an issue. The below code is the code to send thinking to the provider.
|
|
@@ -1071,6 +1113,7 @@ class OpenAIResponsesStreamedResponse(StreamedResponse):
|
|
|
1071
1113
|
|
|
1072
1114
|
async def _get_event_iterator(self) -> AsyncIterator[ModelResponseStreamEvent]: # noqa: C901
|
|
1073
1115
|
async for chunk in self._response:
|
|
1116
|
+
# NOTE: You can inspect the builtin tools used checking the `ResponseCompletedEvent`.
|
|
1074
1117
|
if isinstance(chunk, responses.ResponseCompletedEvent):
|
|
1075
1118
|
self._usage += _map_usage(chunk.response)
|
|
1076
1119
|
|
|
@@ -1122,6 +1165,8 @@ class OpenAIResponsesStreamedResponse(StreamedResponse):
|
|
|
1122
1165
|
)
|
|
1123
1166
|
elif isinstance(chunk.item, responses.ResponseOutputMessage):
|
|
1124
1167
|
pass
|
|
1168
|
+
elif isinstance(chunk.item, responses.ResponseFunctionWebSearch):
|
|
1169
|
+
pass
|
|
1125
1170
|
else:
|
|
1126
1171
|
warnings.warn( # pragma: no cover
|
|
1127
1172
|
f'Handling of this item type is not yet implemented. Please report on our GitHub: {chunk}',
|
|
@@ -1148,6 +1193,10 @@ class OpenAIResponsesStreamedResponse(StreamedResponse):
|
|
|
1148
1193
|
signature=chunk.item_id,
|
|
1149
1194
|
)
|
|
1150
1195
|
|
|
1196
|
+
# TODO(Marcelo): We should support annotations in the future.
|
|
1197
|
+
elif isinstance(chunk, responses.ResponseOutputTextAnnotationAddedEvent):
|
|
1198
|
+
pass # there's nothing we need to do here
|
|
1199
|
+
|
|
1151
1200
|
elif isinstance(chunk, responses.ResponseTextDeltaEvent):
|
|
1152
1201
|
maybe_event = self._parts_manager.handle_text_delta(
|
|
1153
1202
|
vendor_part_id=chunk.content_index, content=chunk.delta
|
|
@@ -1158,6 +1207,18 @@ class OpenAIResponsesStreamedResponse(StreamedResponse):
|
|
|
1158
1207
|
elif isinstance(chunk, responses.ResponseTextDoneEvent):
|
|
1159
1208
|
pass # there's nothing we need to do here
|
|
1160
1209
|
|
|
1210
|
+
elif isinstance(chunk, responses.ResponseWebSearchCallInProgressEvent):
|
|
1211
|
+
pass # there's nothing we need to do here
|
|
1212
|
+
|
|
1213
|
+
elif isinstance(chunk, responses.ResponseWebSearchCallSearchingEvent):
|
|
1214
|
+
pass # there's nothing we need to do here
|
|
1215
|
+
|
|
1216
|
+
elif isinstance(chunk, responses.ResponseWebSearchCallCompletedEvent):
|
|
1217
|
+
pass # there's nothing we need to do here
|
|
1218
|
+
|
|
1219
|
+
elif isinstance(chunk, responses.ResponseAudioDeltaEvent): # pragma: lax no cover
|
|
1220
|
+
pass # there's nothing we need to do here
|
|
1221
|
+
|
|
1161
1222
|
else: # pragma: no cover
|
|
1162
1223
|
warnings.warn(
|
|
1163
1224
|
f'Handling of this event type is not yet implemented. Please report on our GitHub: {chunk}',
|
pydantic_ai/models/test.py
CHANGED
|
@@ -12,7 +12,10 @@ import pydantic_core
|
|
|
12
12
|
from typing_extensions import assert_never
|
|
13
13
|
|
|
14
14
|
from .. import _utils
|
|
15
|
+
from ..exceptions import UserError
|
|
15
16
|
from ..messages import (
|
|
17
|
+
BuiltinToolCallPart,
|
|
18
|
+
BuiltinToolReturnPart,
|
|
16
19
|
ModelMessage,
|
|
17
20
|
ModelRequest,
|
|
18
21
|
ModelResponse,
|
|
@@ -179,6 +182,9 @@ class TestModel(Model):
|
|
|
179
182
|
model_settings: ModelSettings | None,
|
|
180
183
|
model_request_parameters: ModelRequestParameters,
|
|
181
184
|
) -> ModelResponse:
|
|
185
|
+
if model_request_parameters.builtin_tools:
|
|
186
|
+
raise UserError('TestModel does not support built-in tools')
|
|
187
|
+
|
|
182
188
|
tool_calls = self._get_tool_calls(model_request_parameters)
|
|
183
189
|
output_wrapper = self._get_output(model_request_parameters)
|
|
184
190
|
output_tools = model_request_parameters.output_tools
|
|
@@ -283,6 +289,9 @@ class TestStreamedResponse(StreamedResponse):
|
|
|
283
289
|
yield self._parts_manager.handle_tool_call_part(
|
|
284
290
|
vendor_part_id=i, tool_name=part.tool_name, args=part.args, tool_call_id=part.tool_call_id
|
|
285
291
|
)
|
|
292
|
+
elif isinstance(part, (BuiltinToolCallPart, BuiltinToolReturnPart)): # pragma: no cover
|
|
293
|
+
# NOTE: These parts are not generated by TestModel, but we need to handle them for type checking
|
|
294
|
+
assert False, f'Unexpected part type in TestModel: {type(part).__name__}'
|
|
286
295
|
elif isinstance(part, ThinkingPart): # pragma: no cover
|
|
287
296
|
# NOTE: There's no way to reach this part of the code, since we don't generate ThinkingPart on TestModel.
|
|
288
297
|
assert False, "This should be unreachable — we don't generate ThinkingPart on TestModel."
|
pydantic_ai/profiles/__init__.py
CHANGED
|
@@ -7,7 +7,15 @@ from typing import Callable, Union
|
|
|
7
7
|
from typing_extensions import Self
|
|
8
8
|
|
|
9
9
|
from ..output import StructuredOutputMode
|
|
10
|
-
from ._json_schema import JsonSchemaTransformer
|
|
10
|
+
from ._json_schema import InlineDefsJsonSchemaTransformer, JsonSchemaTransformer
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
'ModelProfile',
|
|
14
|
+
'ModelProfileSpec',
|
|
15
|
+
'DEFAULT_PROFILE',
|
|
16
|
+
'InlineDefsJsonSchemaTransformer',
|
|
17
|
+
'JsonSchemaTransformer',
|
|
18
|
+
]
|
|
11
19
|
|
|
12
20
|
|
|
13
21
|
@dataclass
|
pydantic_ai/profiles/amazon.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations as _annotations
|
|
2
2
|
|
|
3
|
-
from . import ModelProfile
|
|
4
|
-
from ._json_schema import InlineDefsJsonSchemaTransformer
|
|
3
|
+
from . import InlineDefsJsonSchemaTransformer, ModelProfile
|
|
5
4
|
|
|
6
5
|
|
|
7
6
|
def amazon_model_profile(model_name: str) -> ModelProfile | None:
|