pydantic-ai-slim 0.0.48__tar.gz → 0.0.50__tar.gz
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.
Potentially problematic release.
This version of pydantic-ai-slim might be problematic. Click here for more details.
- {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/PKG-INFO +3 -3
- {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/_cli.py +82 -59
- {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/models/__init__.py +2 -0
- {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/models/anthropic.py +5 -41
- {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/models/cohere.py +1 -1
- {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/models/gemini.py +7 -5
- {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/models/openai.py +65 -18
- {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/.gitignore +0 -0
- {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/README.md +0 -0
- {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/__init__.py +0 -0
- {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/__main__.py +0 -0
- {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/_agent_graph.py +0 -0
- {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/_griffe.py +0 -0
- {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/_parts_manager.py +0 -0
- {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/_pydantic.py +0 -0
- {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/_result.py +0 -0
- {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/_system_prompt.py +0 -0
- {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/_utils.py +0 -0
- {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/agent.py +0 -0
- {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/common_tools/__init__.py +0 -0
- {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/common_tools/duckduckgo.py +0 -0
- {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/common_tools/tavily.py +0 -0
- {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/exceptions.py +0 -0
- {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/format_as_xml.py +0 -0
- {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/mcp.py +0 -0
- {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/messages.py +0 -0
- {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/models/bedrock.py +0 -0
- {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/models/fallback.py +0 -0
- {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/models/function.py +0 -0
- {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/models/groq.py +0 -0
- {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/models/instrumented.py +0 -0
- {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/models/mistral.py +0 -0
- {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/models/test.py +0 -0
- {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/models/wrapper.py +0 -0
- {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/providers/__init__.py +0 -0
- {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/providers/anthropic.py +0 -0
- {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/providers/azure.py +0 -0
- {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/providers/bedrock.py +0 -0
- {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/providers/cohere.py +0 -0
- {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/providers/deepseek.py +0 -0
- {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/providers/google_gla.py +0 -0
- {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/providers/google_vertex.py +0 -0
- {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/providers/groq.py +0 -0
- {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/providers/mistral.py +0 -0
- {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/providers/openai.py +0 -0
- {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/py.typed +0 -0
- {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/result.py +0 -0
- {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/settings.py +0 -0
- {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/tools.py +0 -0
- {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/usage.py +0 -0
- {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pyproject.toml +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pydantic-ai-slim
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.50
|
|
4
4
|
Summary: Agent Framework / shim to use Pydantic with LLMs, slim package
|
|
5
5
|
Author-email: Samuel Colvin <samuel@pydantic.dev>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -29,7 +29,7 @@ Requires-Dist: exceptiongroup; python_version < '3.11'
|
|
|
29
29
|
Requires-Dist: griffe>=1.3.2
|
|
30
30
|
Requires-Dist: httpx>=0.27
|
|
31
31
|
Requires-Dist: opentelemetry-api>=1.28.0
|
|
32
|
-
Requires-Dist: pydantic-graph==0.0.
|
|
32
|
+
Requires-Dist: pydantic-graph==0.0.50
|
|
33
33
|
Requires-Dist: pydantic>=2.10
|
|
34
34
|
Requires-Dist: typing-inspection>=0.4.0
|
|
35
35
|
Provides-Extra: anthropic
|
|
@@ -45,7 +45,7 @@ Requires-Dist: cohere>=5.13.11; (platform_system != 'Emscripten') and extra == '
|
|
|
45
45
|
Provides-Extra: duckduckgo
|
|
46
46
|
Requires-Dist: duckduckgo-search>=7.0.0; extra == 'duckduckgo'
|
|
47
47
|
Provides-Extra: evals
|
|
48
|
-
Requires-Dist: pydantic-evals==0.0.
|
|
48
|
+
Requires-Dist: pydantic-evals==0.0.50; extra == 'evals'
|
|
49
49
|
Provides-Extra: groq
|
|
50
50
|
Requires-Dist: groq>=0.15.0; extra == 'groq'
|
|
51
51
|
Provides-Extra: logfire
|
|
@@ -3,19 +3,20 @@ from __future__ import annotations as _annotations
|
|
|
3
3
|
import argparse
|
|
4
4
|
import asyncio
|
|
5
5
|
import sys
|
|
6
|
+
from asyncio import CancelledError
|
|
6
7
|
from collections.abc import Sequence
|
|
7
8
|
from contextlib import ExitStack
|
|
8
9
|
from datetime import datetime, timezone
|
|
9
10
|
from importlib.metadata import version
|
|
10
11
|
from pathlib import Path
|
|
11
|
-
from typing import cast
|
|
12
|
+
from typing import Any, cast
|
|
12
13
|
|
|
13
14
|
from typing_inspection.introspection import get_literal_values
|
|
14
15
|
|
|
15
16
|
from pydantic_ai.agent import Agent
|
|
16
17
|
from pydantic_ai.exceptions import UserError
|
|
17
18
|
from pydantic_ai.messages import ModelMessage, PartDeltaEvent, TextPartDelta
|
|
18
|
-
from pydantic_ai.models import KnownModelName
|
|
19
|
+
from pydantic_ai.models import KnownModelName, infer_model
|
|
19
20
|
|
|
20
21
|
try:
|
|
21
22
|
import argcomplete
|
|
@@ -47,7 +48,7 @@ class SimpleCodeBlock(CodeBlock):
|
|
|
47
48
|
This avoids a background color which messes up copy-pasting and sets the language name as dim prefix and suffix.
|
|
48
49
|
"""
|
|
49
50
|
|
|
50
|
-
def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
|
|
51
|
+
def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
|
|
51
52
|
code = str(self.text).rstrip()
|
|
52
53
|
yield Text(self.lexer_name, style='dim')
|
|
53
54
|
yield Syntax(code, self.lexer_name, theme=self.theme, background_color='default', word_wrap=True)
|
|
@@ -57,7 +58,7 @@ class SimpleCodeBlock(CodeBlock):
|
|
|
57
58
|
class LeftHeading(Heading):
|
|
58
59
|
"""Customised headings in markdown to stop centering and prepend markdown style hashes."""
|
|
59
60
|
|
|
60
|
-
def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
|
|
61
|
+
def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
|
|
61
62
|
# note we use `Style(bold=True)` not `self.style_name` here to disable underlining which is ugly IMHO
|
|
62
63
|
yield Text(f'{"#" * int(self.tag[1:])} {self.text.plain}', style=Style(bold=True))
|
|
63
64
|
|
|
@@ -68,7 +69,21 @@ Markdown.elements.update(
|
|
|
68
69
|
)
|
|
69
70
|
|
|
70
71
|
|
|
71
|
-
|
|
72
|
+
cli_agent = Agent()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@cli_agent.system_prompt
|
|
76
|
+
def cli_system_prompt() -> str:
|
|
77
|
+
now_utc = datetime.now(timezone.utc)
|
|
78
|
+
tzinfo = now_utc.astimezone().tzinfo
|
|
79
|
+
tzname = tzinfo.tzname(now_utc) if tzinfo else ''
|
|
80
|
+
return f"""\
|
|
81
|
+
Help the user by responding to their request, the output should be concise and always written in markdown.
|
|
82
|
+
The current date and time is {datetime.now()} {tzname}.
|
|
83
|
+
The user is running {sys.platform}."""
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def cli(args_list: Sequence[str] | None = None) -> int:
|
|
72
87
|
parser = argparse.ArgumentParser(
|
|
73
88
|
prog='pai',
|
|
74
89
|
description=f"""\
|
|
@@ -124,18 +139,10 @@ Special prompt:
|
|
|
124
139
|
console.print(f' {model}', highlight=False)
|
|
125
140
|
return 0
|
|
126
141
|
|
|
127
|
-
now_utc = datetime.now(timezone.utc)
|
|
128
|
-
tzname = now_utc.astimezone().tzinfo.tzname(now_utc) # type: ignore
|
|
129
142
|
try:
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
Help the user by responding to their request, the output should be concise and always written in markdown.
|
|
134
|
-
The current date and time is {datetime.now()} {tzname}.
|
|
135
|
-
The user is running {sys.platform}.""",
|
|
136
|
-
)
|
|
137
|
-
except UserError:
|
|
138
|
-
console.print(f'[red]Invalid model "{args.model}"[/red]')
|
|
143
|
+
cli_agent.model = infer_model(args.model)
|
|
144
|
+
except UserError as e:
|
|
145
|
+
console.print(f'Error initializing [magenta]{args.model}[/magenta]:\n[red]{e}[/red]')
|
|
139
146
|
return 1
|
|
140
147
|
|
|
141
148
|
stream = not args.no_stream
|
|
@@ -148,67 +155,44 @@ Special prompt:
|
|
|
148
155
|
|
|
149
156
|
if prompt := cast(str, args.prompt):
|
|
150
157
|
try:
|
|
151
|
-
asyncio.run(ask_agent(
|
|
158
|
+
asyncio.run(ask_agent(cli_agent, prompt, stream, console, code_theme))
|
|
152
159
|
except KeyboardInterrupt:
|
|
153
160
|
pass
|
|
154
161
|
return 0
|
|
155
162
|
|
|
156
163
|
history = Path.home() / '.pai-prompt-history.txt'
|
|
157
|
-
|
|
164
|
+
# doing this instead of `PromptSession[Any](history=` allows mocking of PromptSession in tests
|
|
165
|
+
session: PromptSession[Any] = PromptSession(history=FileHistory(str(history)))
|
|
166
|
+
try:
|
|
167
|
+
return asyncio.run(run_chat(session, stream, cli_agent, console, code_theme))
|
|
168
|
+
except KeyboardInterrupt: # pragma: no cover
|
|
169
|
+
return 0
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
async def run_chat(session: PromptSession[Any], stream: bool, agent: Agent, console: Console, code_theme: str) -> int:
|
|
158
173
|
multiline = False
|
|
159
174
|
messages: list[ModelMessage] = []
|
|
160
175
|
|
|
161
176
|
while True:
|
|
162
177
|
try:
|
|
163
178
|
auto_suggest = CustomAutoSuggest(['/markdown', '/multiline', '/exit'])
|
|
164
|
-
text =
|
|
165
|
-
except (KeyboardInterrupt, EOFError):
|
|
179
|
+
text = await session.prompt_async('pai ➤ ', auto_suggest=auto_suggest, multiline=multiline)
|
|
180
|
+
except (KeyboardInterrupt, EOFError): # pragma: no cover
|
|
166
181
|
return 0
|
|
167
182
|
|
|
168
183
|
if not text.strip():
|
|
169
184
|
continue
|
|
170
185
|
|
|
171
|
-
ident_prompt = text.lower().strip(
|
|
186
|
+
ident_prompt = text.lower().strip().replace(' ', '-')
|
|
172
187
|
if ident_prompt.startswith('/'):
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
except IndexError:
|
|
177
|
-
console.print('[dim]No markdown output available.[/dim]')
|
|
178
|
-
continue
|
|
179
|
-
console.print('[dim]Markdown output of last question:[/dim]\n')
|
|
180
|
-
for part in parts:
|
|
181
|
-
if part.part_kind == 'text':
|
|
182
|
-
console.print(
|
|
183
|
-
Syntax(
|
|
184
|
-
part.content,
|
|
185
|
-
lexer='markdown',
|
|
186
|
-
theme=code_theme,
|
|
187
|
-
word_wrap=True,
|
|
188
|
-
background_color='default',
|
|
189
|
-
)
|
|
190
|
-
)
|
|
191
|
-
|
|
192
|
-
elif ident_prompt == '/multiline':
|
|
193
|
-
multiline = not multiline
|
|
194
|
-
if multiline:
|
|
195
|
-
console.print(
|
|
196
|
-
'Enabling multiline mode. '
|
|
197
|
-
'[dim]Press [Meta+Enter] or [Esc] followed by [Enter] to accept input.[/dim]'
|
|
198
|
-
)
|
|
199
|
-
else:
|
|
200
|
-
console.print('Disabling multiline mode.')
|
|
201
|
-
elif ident_prompt == '/exit':
|
|
202
|
-
console.print('[dim]Exiting…[/dim]')
|
|
203
|
-
return 0
|
|
204
|
-
else:
|
|
205
|
-
console.print(f'[red]Unknown command[/red] [magenta]`{ident_prompt}`[/magenta]')
|
|
188
|
+
exit_value, multiline = handle_slash_command(ident_prompt, messages, multiline, console, code_theme)
|
|
189
|
+
if exit_value is not None:
|
|
190
|
+
return exit_value
|
|
206
191
|
else:
|
|
207
192
|
try:
|
|
208
|
-
messages =
|
|
209
|
-
except
|
|
193
|
+
messages = await ask_agent(agent, text, stream, console, code_theme, messages)
|
|
194
|
+
except CancelledError: # pragma: no cover
|
|
210
195
|
console.print('[dim]Interrupted[/dim]')
|
|
211
|
-
messages = []
|
|
212
196
|
|
|
213
197
|
|
|
214
198
|
async def ask_agent(
|
|
@@ -218,7 +202,7 @@ async def ask_agent(
|
|
|
218
202
|
console: Console,
|
|
219
203
|
code_theme: str,
|
|
220
204
|
messages: list[ModelMessage] | None = None,
|
|
221
|
-
) -> list[ModelMessage]:
|
|
205
|
+
) -> list[ModelMessage]:
|
|
222
206
|
status = Status('[dim]Working on it…[/dim]', console=console)
|
|
223
207
|
|
|
224
208
|
if not stream:
|
|
@@ -248,7 +232,7 @@ async def ask_agent(
|
|
|
248
232
|
|
|
249
233
|
|
|
250
234
|
class CustomAutoSuggest(AutoSuggestFromHistory):
|
|
251
|
-
def __init__(self, special_suggestions: list[str] | None = None):
|
|
235
|
+
def __init__(self, special_suggestions: list[str] | None = None):
|
|
252
236
|
super().__init__()
|
|
253
237
|
self.special_suggestions = special_suggestions or []
|
|
254
238
|
|
|
@@ -264,5 +248,44 @@ class CustomAutoSuggest(AutoSuggestFromHistory):
|
|
|
264
248
|
return suggestion
|
|
265
249
|
|
|
266
250
|
|
|
251
|
+
def handle_slash_command(
|
|
252
|
+
ident_prompt: str, messages: list[ModelMessage], multiline: bool, console: Console, code_theme: str
|
|
253
|
+
) -> tuple[int | None, bool]:
|
|
254
|
+
if ident_prompt == '/markdown':
|
|
255
|
+
try:
|
|
256
|
+
parts = messages[-1].parts
|
|
257
|
+
except IndexError:
|
|
258
|
+
console.print('[dim]No markdown output available.[/dim]')
|
|
259
|
+
else:
|
|
260
|
+
console.print('[dim]Markdown output of last question:[/dim]\n')
|
|
261
|
+
for part in parts:
|
|
262
|
+
if part.part_kind == 'text':
|
|
263
|
+
console.print(
|
|
264
|
+
Syntax(
|
|
265
|
+
part.content,
|
|
266
|
+
lexer='markdown',
|
|
267
|
+
theme=code_theme,
|
|
268
|
+
word_wrap=True,
|
|
269
|
+
background_color='default',
|
|
270
|
+
)
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
elif ident_prompt == '/multiline':
|
|
274
|
+
multiline = not multiline
|
|
275
|
+
if multiline:
|
|
276
|
+
console.print(
|
|
277
|
+
'Enabling multiline mode. [dim]Press [Meta+Enter] or [Esc] followed by [Enter] to accept input.[/dim]'
|
|
278
|
+
)
|
|
279
|
+
else:
|
|
280
|
+
console.print('Disabling multiline mode.')
|
|
281
|
+
return None, multiline
|
|
282
|
+
elif ident_prompt == '/exit':
|
|
283
|
+
console.print('[dim]Exiting…[/dim]')
|
|
284
|
+
return 0, multiline
|
|
285
|
+
else:
|
|
286
|
+
console.print(f'[red]Unknown command[/red] [magenta]`{ident_prompt}`[/magenta]')
|
|
287
|
+
return None, multiline
|
|
288
|
+
|
|
289
|
+
|
|
267
290
|
def app(): # pragma: no cover
|
|
268
291
|
sys.exit(cli())
|
|
@@ -106,6 +106,7 @@ KnownModelName = TypeAliasType(
|
|
|
106
106
|
'google-gla:gemini-2.0-flash',
|
|
107
107
|
'google-gla:gemini-2.0-flash-lite-preview-02-05',
|
|
108
108
|
'google-gla:gemini-2.0-pro-exp-02-05',
|
|
109
|
+
'google-gla:gemini-2.5-pro-exp-03-25',
|
|
109
110
|
'google-vertex:gemini-1.0-pro',
|
|
110
111
|
'google-vertex:gemini-1.5-flash',
|
|
111
112
|
'google-vertex:gemini-1.5-flash-8b',
|
|
@@ -116,6 +117,7 @@ KnownModelName = TypeAliasType(
|
|
|
116
117
|
'google-vertex:gemini-2.0-flash',
|
|
117
118
|
'google-vertex:gemini-2.0-flash-lite-preview-02-05',
|
|
118
119
|
'google-vertex:gemini-2.0-pro-exp-02-05',
|
|
120
|
+
'google-vertex:gemini-2.5-pro-exp-03-25',
|
|
119
121
|
'gpt-3.5-turbo',
|
|
120
122
|
'gpt-3.5-turbo-0125',
|
|
121
123
|
'gpt-3.5-turbo-0301',
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations as _annotations
|
|
2
2
|
|
|
3
|
-
import base64
|
|
4
3
|
import io
|
|
5
4
|
from collections.abc import AsyncGenerator, AsyncIterable, AsyncIterator
|
|
6
5
|
from contextlib import asynccontextmanager
|
|
@@ -9,7 +8,6 @@ from datetime import datetime, timezone
|
|
|
9
8
|
from json import JSONDecodeError, loads as json_loads
|
|
10
9
|
from typing import Any, Literal, Union, cast, overload
|
|
11
10
|
|
|
12
|
-
from anthropic.types import DocumentBlockParam
|
|
13
11
|
from typing_extensions import assert_never
|
|
14
12
|
|
|
15
13
|
from .. import ModelHTTPError, UnexpectedModelBehavior, _utils, usage
|
|
@@ -40,6 +38,7 @@ try:
|
|
|
40
38
|
from anthropic.types import (
|
|
41
39
|
Base64PDFSourceParam,
|
|
42
40
|
ContentBlock,
|
|
41
|
+
DocumentBlockParam,
|
|
43
42
|
ImageBlockParam,
|
|
44
43
|
Message as AnthropicMessage,
|
|
45
44
|
MessageParam,
|
|
@@ -354,48 +353,13 @@ class AnthropicModel(Model):
|
|
|
354
353
|
else:
|
|
355
354
|
raise RuntimeError('Only images and PDFs are supported for binary content')
|
|
356
355
|
elif isinstance(item, ImageUrl):
|
|
357
|
-
|
|
358
|
-
response = await cached_async_http_client().get(item.url)
|
|
359
|
-
response.raise_for_status()
|
|
360
|
-
yield ImageBlockParam(
|
|
361
|
-
source={
|
|
362
|
-
'data': io.BytesIO(response.content),
|
|
363
|
-
'media_type': item.media_type,
|
|
364
|
-
'type': 'base64',
|
|
365
|
-
},
|
|
366
|
-
type='image',
|
|
367
|
-
)
|
|
368
|
-
except ValueError:
|
|
369
|
-
# Download the file if can't find the mime type.
|
|
370
|
-
client = cached_async_http_client()
|
|
371
|
-
response = await client.get(item.url, follow_redirects=True)
|
|
372
|
-
response.raise_for_status()
|
|
373
|
-
base64_encoded = base64.b64encode(response.content).decode('utf-8')
|
|
374
|
-
if (mime_type := response.headers['Content-Type']) in (
|
|
375
|
-
'image/jpeg',
|
|
376
|
-
'image/png',
|
|
377
|
-
'image/gif',
|
|
378
|
-
'image/webp',
|
|
379
|
-
):
|
|
380
|
-
yield ImageBlockParam(
|
|
381
|
-
source={'data': base64_encoded, 'media_type': mime_type, 'type': 'base64'},
|
|
382
|
-
type='image',
|
|
383
|
-
)
|
|
384
|
-
else: # pragma: no cover
|
|
385
|
-
raise RuntimeError(f'Unsupported image type: {mime_type}')
|
|
356
|
+
yield ImageBlockParam(source={'type': 'url', 'url': item.url}, type='image')
|
|
386
357
|
elif isinstance(item, DocumentUrl):
|
|
387
|
-
response = await cached_async_http_client().get(item.url)
|
|
388
|
-
response.raise_for_status()
|
|
389
358
|
if item.media_type == 'application/pdf':
|
|
390
|
-
yield DocumentBlockParam(
|
|
391
|
-
source=Base64PDFSourceParam(
|
|
392
|
-
data=io.BytesIO(response.content),
|
|
393
|
-
media_type=item.media_type,
|
|
394
|
-
type='base64',
|
|
395
|
-
),
|
|
396
|
-
type='document',
|
|
397
|
-
)
|
|
359
|
+
yield DocumentBlockParam(source={'url': item.url, 'type': 'url'}, type='document')
|
|
398
360
|
elif item.media_type == 'text/plain':
|
|
361
|
+
response = await cached_async_http_client().get(item.url)
|
|
362
|
+
response.raise_for_status()
|
|
399
363
|
yield DocumentBlockParam(
|
|
400
364
|
source=PlainTextSourceParam(data=response.text, media_type=item.media_type, type='text'),
|
|
401
365
|
type='document',
|
|
@@ -5,7 +5,6 @@ from dataclasses import dataclass, field
|
|
|
5
5
|
from itertools import chain
|
|
6
6
|
from typing import Literal, Union, cast
|
|
7
7
|
|
|
8
|
-
from cohere import TextAssistantMessageContentItem
|
|
9
8
|
from typing_extensions import assert_never
|
|
10
9
|
|
|
11
10
|
from .. import ModelHTTPError, result
|
|
@@ -38,6 +37,7 @@ try:
|
|
|
38
37
|
ChatMessageV2,
|
|
39
38
|
ChatResponse,
|
|
40
39
|
SystemChatMessageV2,
|
|
40
|
+
TextAssistantMessageContentItem,
|
|
41
41
|
ToolCallV2,
|
|
42
42
|
ToolCallV2Function,
|
|
43
43
|
ToolChatMessageV2,
|
|
@@ -57,6 +57,7 @@ LatestGeminiModelNames = Literal[
|
|
|
57
57
|
'gemini-2.0-flash',
|
|
58
58
|
'gemini-2.0-flash-lite-preview-02-05',
|
|
59
59
|
'gemini-2.0-pro-exp-02-05',
|
|
60
|
+
'gemini-2.5-pro-exp-03-25',
|
|
60
61
|
]
|
|
61
62
|
"""Latest Gemini models."""
|
|
62
63
|
|
|
@@ -134,7 +135,8 @@ class GeminiModel(Model):
|
|
|
134
135
|
async with self._make_request(
|
|
135
136
|
messages, False, cast(GeminiModelSettings, model_settings or {}), model_request_parameters
|
|
136
137
|
) as http_response:
|
|
137
|
-
|
|
138
|
+
data = await http_response.aread()
|
|
139
|
+
response = _gemini_response_ta.validate_json(data)
|
|
138
140
|
return self._process_response(response), _metadata_as_usage(response)
|
|
139
141
|
|
|
140
142
|
@asynccontextmanager
|
|
@@ -639,10 +641,7 @@ class _GeminiFunction(TypedDict):
|
|
|
639
641
|
|
|
640
642
|
def _function_from_abstract_tool(tool: ToolDefinition) -> _GeminiFunction:
|
|
641
643
|
json_schema = _GeminiJsonSchema(tool.parameters_json_schema).simplify()
|
|
642
|
-
f = _GeminiFunction(
|
|
643
|
-
name=tool.name,
|
|
644
|
-
description=tool.description,
|
|
645
|
-
)
|
|
644
|
+
f = _GeminiFunction(name=tool.name, description=tool.description)
|
|
646
645
|
if json_schema.get('properties'):
|
|
647
646
|
f['parameters'] = json_schema
|
|
648
647
|
return f
|
|
@@ -769,6 +768,9 @@ class _GeminiJsonSchema:
|
|
|
769
768
|
def _simplify(self, schema: dict[str, Any], refs_stack: tuple[str, ...]) -> None:
|
|
770
769
|
schema.pop('title', None)
|
|
771
770
|
schema.pop('default', None)
|
|
771
|
+
schema.pop('$schema', None)
|
|
772
|
+
schema.pop('exclusiveMaximum', None)
|
|
773
|
+
schema.pop('exclusiveMinimum', None)
|
|
772
774
|
if ref := schema.pop('$ref', None):
|
|
773
775
|
# noinspection PyTypeChecker
|
|
774
776
|
key = re.sub(r'^#/\$defs/', '', ref)
|
|
@@ -2,14 +2,12 @@ from __future__ import annotations as _annotations
|
|
|
2
2
|
|
|
3
3
|
import base64
|
|
4
4
|
import warnings
|
|
5
|
-
from collections.abc import AsyncIterable, AsyncIterator
|
|
5
|
+
from collections.abc import AsyncIterable, AsyncIterator, Sequence
|
|
6
6
|
from contextlib import asynccontextmanager
|
|
7
7
|
from dataclasses import dataclass, field
|
|
8
8
|
from datetime import datetime, timezone
|
|
9
9
|
from typing import Literal, Union, cast, overload
|
|
10
10
|
|
|
11
|
-
from openai import NotGiven
|
|
12
|
-
from openai.types import Reasoning
|
|
13
11
|
from typing_extensions import assert_never
|
|
14
12
|
|
|
15
13
|
from pydantic_ai.providers import Provider, infer_provider
|
|
@@ -44,7 +42,7 @@ from . import (
|
|
|
44
42
|
)
|
|
45
43
|
|
|
46
44
|
try:
|
|
47
|
-
from openai import NOT_GIVEN, APIStatusError, AsyncOpenAI, AsyncStream
|
|
45
|
+
from openai import NOT_GIVEN, APIStatusError, AsyncOpenAI, AsyncStream, NotGiven
|
|
48
46
|
from openai.types import ChatModel, chat, responses
|
|
49
47
|
from openai.types.chat import (
|
|
50
48
|
ChatCompletionChunk,
|
|
@@ -55,6 +53,7 @@ try:
|
|
|
55
53
|
)
|
|
56
54
|
from openai.types.chat.chat_completion_content_part_image_param import ImageURL
|
|
57
55
|
from openai.types.chat.chat_completion_content_part_input_audio_param import InputAudio
|
|
56
|
+
from openai.types.responses import ComputerToolParam, FileSearchToolParam, WebSearchToolParam
|
|
58
57
|
from openai.types.responses.response_input_param import FunctionCallOutput, Message
|
|
59
58
|
from openai.types.shared import ReasoningEffort
|
|
60
59
|
from openai.types.shared_params import Reasoning
|
|
@@ -64,6 +63,14 @@ except ImportError as _import_error:
|
|
|
64
63
|
'you can use the `openai` optional group — `pip install "pydantic-ai-slim[openai]"`'
|
|
65
64
|
) from _import_error
|
|
66
65
|
|
|
66
|
+
__all__ = (
|
|
67
|
+
'OpenAIModel',
|
|
68
|
+
'OpenAIResponsesModel',
|
|
69
|
+
'OpenAIModelSettings',
|
|
70
|
+
'OpenAIResponsesModelSettings',
|
|
71
|
+
'OpenAIModelName',
|
|
72
|
+
)
|
|
73
|
+
|
|
67
74
|
OpenAIModelName = Union[str, ChatModel]
|
|
68
75
|
"""
|
|
69
76
|
Possible OpenAI model names.
|
|
@@ -86,8 +93,7 @@ class OpenAIModelSettings(ModelSettings, total=False):
|
|
|
86
93
|
"""
|
|
87
94
|
|
|
88
95
|
openai_reasoning_effort: ReasoningEffort
|
|
89
|
-
"""
|
|
90
|
-
Constrains effort on reasoning for [reasoning models](https://platform.openai.com/docs/guides/reasoning).
|
|
96
|
+
"""Constrains effort on reasoning for [reasoning models](https://platform.openai.com/docs/guides/reasoning).
|
|
91
97
|
|
|
92
98
|
Currently supported values are `low`, `medium`, and `high`. Reducing reasoning effort can
|
|
93
99
|
result in faster responses and fewer tokens used on reasoning in a response.
|
|
@@ -100,6 +106,40 @@ class OpenAIModelSettings(ModelSettings, total=False):
|
|
|
100
106
|
"""
|
|
101
107
|
|
|
102
108
|
|
|
109
|
+
class OpenAIResponsesModelSettings(OpenAIModelSettings, total=False):
|
|
110
|
+
"""Settings used for an OpenAI Responses model request.
|
|
111
|
+
|
|
112
|
+
ALL FIELDS MUST BE `openai_` PREFIXED SO YOU CAN MERGE THEM WITH OTHER MODELS.
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
openai_builtin_tools: Sequence[FileSearchToolParam | WebSearchToolParam | ComputerToolParam]
|
|
116
|
+
"""The provided OpenAI built-in tools to use.
|
|
117
|
+
|
|
118
|
+
See [OpenAI's built-in tools](https://platform.openai.com/docs/guides/tools?api-mode=responses) for more details.
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
openai_reasoning_generate_summary: Literal['detailed', 'concise']
|
|
122
|
+
"""A summary of the reasoning performed by the model.
|
|
123
|
+
|
|
124
|
+
This can be useful for debugging and understanding the model's reasoning process.
|
|
125
|
+
One of `concise` or `detailed`.
|
|
126
|
+
|
|
127
|
+
Check the [OpenAI Computer use documentation](https://platform.openai.com/docs/guides/tools-computer-use#1-send-a-request-to-the-model)
|
|
128
|
+
for more details.
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
openai_truncation: Literal['disabled', 'auto']
|
|
132
|
+
"""The truncation strategy to use for the model response.
|
|
133
|
+
|
|
134
|
+
It can be either:
|
|
135
|
+
- `disabled` (default): If a model response will exceed the context window size for a model, the
|
|
136
|
+
request will fail with a 400 error.
|
|
137
|
+
- `auto`: If the context of this response and previous ones exceeds the model's context window size,
|
|
138
|
+
the model will truncate the response to fit the context window by dropping input items in the
|
|
139
|
+
middle of the conversation.
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
|
|
103
143
|
@dataclass(init=False)
|
|
104
144
|
class OpenAIModel(Model):
|
|
105
145
|
"""A model that uses the OpenAI API.
|
|
@@ -417,6 +457,8 @@ class OpenAIResponsesModel(Model):
|
|
|
417
457
|
- [File search](https://platform.openai.com/docs/guides/tools-file-search)
|
|
418
458
|
- [Computer use](https://platform.openai.com/docs/guides/tools-computer-use)
|
|
419
459
|
|
|
460
|
+
Use the `openai_builtin_tools` setting to add these tools to your model.
|
|
461
|
+
|
|
420
462
|
If you are interested in the differences between the Responses API and the Chat Completions API,
|
|
421
463
|
see the [OpenAI API docs](https://platform.openai.com/docs/guides/responses-vs-chat-completions).
|
|
422
464
|
"""
|
|
@@ -462,7 +504,7 @@ class OpenAIResponsesModel(Model):
|
|
|
462
504
|
) -> tuple[ModelResponse, usage.Usage]:
|
|
463
505
|
check_allow_model_requests()
|
|
464
506
|
response = await self._responses_create(
|
|
465
|
-
messages, False, cast(
|
|
507
|
+
messages, False, cast(OpenAIResponsesModelSettings, model_settings or {}), model_request_parameters
|
|
466
508
|
)
|
|
467
509
|
return self._process_response(response), _map_usage(response)
|
|
468
510
|
|
|
@@ -475,7 +517,7 @@ class OpenAIResponsesModel(Model):
|
|
|
475
517
|
) -> AsyncIterator[StreamedResponse]:
|
|
476
518
|
check_allow_model_requests()
|
|
477
519
|
response = await self._responses_create(
|
|
478
|
-
messages, True, cast(
|
|
520
|
+
messages, True, cast(OpenAIResponsesModelSettings, model_settings or {}), model_request_parameters
|
|
479
521
|
)
|
|
480
522
|
async with response:
|
|
481
523
|
yield await self._process_streamed_response(response)
|
|
@@ -511,7 +553,7 @@ class OpenAIResponsesModel(Model):
|
|
|
511
553
|
self,
|
|
512
554
|
messages: list[ModelRequest | ModelResponse],
|
|
513
555
|
stream: Literal[False],
|
|
514
|
-
model_settings:
|
|
556
|
+
model_settings: OpenAIResponsesModelSettings,
|
|
515
557
|
model_request_parameters: ModelRequestParameters,
|
|
516
558
|
) -> responses.Response: ...
|
|
517
559
|
|
|
@@ -520,7 +562,7 @@ class OpenAIResponsesModel(Model):
|
|
|
520
562
|
self,
|
|
521
563
|
messages: list[ModelRequest | ModelResponse],
|
|
522
564
|
stream: Literal[True],
|
|
523
|
-
model_settings:
|
|
565
|
+
model_settings: OpenAIResponsesModelSettings,
|
|
524
566
|
model_request_parameters: ModelRequestParameters,
|
|
525
567
|
) -> AsyncStream[responses.ResponseStreamEvent]: ...
|
|
526
568
|
|
|
@@ -528,10 +570,11 @@ class OpenAIResponsesModel(Model):
|
|
|
528
570
|
self,
|
|
529
571
|
messages: list[ModelRequest | ModelResponse],
|
|
530
572
|
stream: bool,
|
|
531
|
-
model_settings:
|
|
573
|
+
model_settings: OpenAIResponsesModelSettings,
|
|
532
574
|
model_request_parameters: ModelRequestParameters,
|
|
533
575
|
) -> responses.Response | AsyncStream[responses.ResponseStreamEvent]:
|
|
534
576
|
tools = self._get_tools(model_request_parameters)
|
|
577
|
+
tools = list(model_settings.get('openai_builtin_tools', [])) + tools
|
|
535
578
|
|
|
536
579
|
# standalone function to make it easier to override
|
|
537
580
|
if not tools:
|
|
@@ -542,12 +585,7 @@ class OpenAIResponsesModel(Model):
|
|
|
542
585
|
tool_choice = 'auto'
|
|
543
586
|
|
|
544
587
|
system_prompt, openai_messages = await self._map_message(messages)
|
|
545
|
-
|
|
546
|
-
reasoning_effort = model_settings.get('openai_reasoning_effort', NOT_GIVEN)
|
|
547
|
-
if not isinstance(reasoning_effort, NotGiven):
|
|
548
|
-
reasoning = Reasoning(effort=reasoning_effort)
|
|
549
|
-
else:
|
|
550
|
-
reasoning = NOT_GIVEN
|
|
588
|
+
reasoning = self._get_reasoning(model_settings)
|
|
551
589
|
|
|
552
590
|
try:
|
|
553
591
|
return await self.client.responses.create(
|
|
@@ -561,6 +599,7 @@ class OpenAIResponsesModel(Model):
|
|
|
561
599
|
stream=stream,
|
|
562
600
|
temperature=model_settings.get('temperature', NOT_GIVEN),
|
|
563
601
|
top_p=model_settings.get('top_p', NOT_GIVEN),
|
|
602
|
+
truncation=model_settings.get('openai_truncation', NOT_GIVEN),
|
|
564
603
|
timeout=model_settings.get('timeout', NOT_GIVEN),
|
|
565
604
|
reasoning=reasoning,
|
|
566
605
|
user=model_settings.get('user', NOT_GIVEN),
|
|
@@ -570,6 +609,14 @@ class OpenAIResponsesModel(Model):
|
|
|
570
609
|
raise ModelHTTPError(status_code=status_code, model_name=self.model_name, body=e.body) from e
|
|
571
610
|
raise
|
|
572
611
|
|
|
612
|
+
def _get_reasoning(self, model_settings: OpenAIResponsesModelSettings) -> Reasoning | NotGiven:
|
|
613
|
+
reasoning_effort = model_settings.get('openai_reasoning_effort', None)
|
|
614
|
+
reasoning_generate_summary = model_settings.get('openai_reasoning_generate_summary', None)
|
|
615
|
+
|
|
616
|
+
if reasoning_effort is None and reasoning_generate_summary is None:
|
|
617
|
+
return NOT_GIVEN
|
|
618
|
+
return Reasoning(effort=reasoning_effort, generate_summary=reasoning_generate_summary)
|
|
619
|
+
|
|
573
620
|
def _get_tools(self, model_request_parameters: ModelRequestParameters) -> list[responses.FunctionToolParam]:
|
|
574
621
|
tools = [self._map_tool_definition(r) for r in model_request_parameters.function_tools]
|
|
575
622
|
if model_request_parameters.result_tools:
|
|
@@ -848,7 +895,7 @@ def _map_usage(response: chat.ChatCompletion | ChatCompletionChunk | responses.R
|
|
|
848
895
|
},
|
|
849
896
|
)
|
|
850
897
|
else:
|
|
851
|
-
details
|
|
898
|
+
details = {}
|
|
852
899
|
if response_usage.completion_tokens_details is not None:
|
|
853
900
|
details.update(response_usage.completion_tokens_details.model_dump(exclude_none=True))
|
|
854
901
|
if response_usage.prompt_tokens_details is not None:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|