pydantic-ai-slim 0.0.6__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.

Potentially problematic release.


This version of pydantic-ai-slim might be problematic. Click here for more details.

pydantic_ai/result.py ADDED
@@ -0,0 +1,314 @@
1
+ from __future__ import annotations as _annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from collections.abc import AsyncIterator
5
+ from dataclasses import dataclass
6
+ from datetime import datetime
7
+ from typing import Generic, TypeVar, cast
8
+
9
+ import logfire_api
10
+
11
+ from . import _result, _utils, exceptions, messages, models
12
+ from .dependencies import AgentDeps
13
+
14
+ __all__ = (
15
+ 'ResultData',
16
+ 'Cost',
17
+ 'RunResult',
18
+ 'StreamedRunResult',
19
+ )
20
+
21
+
22
+ ResultData = TypeVar('ResultData')
23
+ """Type variable for the result data of a run."""
24
+
25
+ _logfire = logfire_api.Logfire(otel_scope='pydantic-ai')
26
+
27
+
28
+ @dataclass
29
+ class Cost:
30
+ """Cost of a request or run.
31
+
32
+ Responsibility for calculating costs is on the model used, PydanticAI simply sums the cost of requests.
33
+
34
+ You'll need to look up the documentation of the model you're using to convent "token count" costs to monetary costs.
35
+ """
36
+
37
+ request_tokens: int | None = None
38
+ """Tokens used in processing the request."""
39
+ response_tokens: int | None = None
40
+ """Tokens used in generating the response."""
41
+ total_tokens: int | None = None
42
+ """Total tokens used in the whole run, should generally be equal to `request_tokens + response_tokens`."""
43
+ details: dict[str, int] | None = None
44
+ """Any extra details returned by the model."""
45
+
46
+ def __add__(self, other: Cost) -> Cost:
47
+ """Add two costs together.
48
+
49
+ This is provided so it's trivial to sum costs from multiple requests and runs.
50
+ """
51
+ counts: dict[str, int] = {}
52
+ for field in 'request_tokens', 'response_tokens', 'total_tokens':
53
+ self_value = getattr(self, field)
54
+ other_value = getattr(other, field)
55
+ if self_value is not None or other_value is not None:
56
+ counts[field] = (self_value or 0) + (other_value or 0)
57
+
58
+ details = self.details.copy() if self.details is not None else None
59
+ if other.details is not None:
60
+ details = details or {}
61
+ for key, value in other.details.items():
62
+ details[key] = details.get(key, 0) + value
63
+
64
+ return Cost(**counts, details=details or None)
65
+
66
+
67
+ @dataclass
68
+ class _BaseRunResult(ABC, Generic[ResultData]):
69
+ """Base type for results.
70
+
71
+ You should not import or use this type directly, instead use its subclasses `RunResult` and `StreamedRunResult`.
72
+ """
73
+
74
+ _all_messages: list[messages.Message]
75
+ _new_message_index: int
76
+
77
+ def all_messages(self) -> list[messages.Message]:
78
+ """Return the history of messages."""
79
+ # this is a method to be consistent with the other methods
80
+ return self._all_messages
81
+
82
+ def all_messages_json(self) -> bytes:
83
+ """Return all messages from [`all_messages`][..all_messages] as JSON bytes."""
84
+ return messages.MessagesTypeAdapter.dump_json(self.all_messages())
85
+
86
+ def new_messages(self) -> list[messages.Message]:
87
+ """Return new messages associated with this run.
88
+
89
+ System prompts and any messages from older runs are excluded.
90
+ """
91
+ return self.all_messages()[self._new_message_index :]
92
+
93
+ def new_messages_json(self) -> bytes:
94
+ """Return new messages from [`new_messages`][..new_messages] as JSON bytes."""
95
+ return messages.MessagesTypeAdapter.dump_json(self.new_messages())
96
+
97
+ @abstractmethod
98
+ def cost(self) -> Cost:
99
+ raise NotImplementedError()
100
+
101
+
102
+ @dataclass
103
+ class RunResult(_BaseRunResult[ResultData]):
104
+ """Result of a non-streamed run."""
105
+
106
+ data: ResultData
107
+ """Data from the final response in the run."""
108
+ _cost: Cost
109
+
110
+ def cost(self) -> Cost:
111
+ """Return the cost of the whole run."""
112
+ return self._cost
113
+
114
+
115
+ @dataclass
116
+ class StreamedRunResult(_BaseRunResult[ResultData], Generic[AgentDeps, ResultData]):
117
+ """Result of a streamed run that returns structured data via a tool call."""
118
+
119
+ cost_so_far: Cost
120
+ """Cost of the run up until the last request."""
121
+ _stream_response: models.EitherStreamedResponse
122
+ _result_schema: _result.ResultSchema[ResultData] | None
123
+ _deps: AgentDeps
124
+ _result_validators: list[_result.ResultValidator[AgentDeps, ResultData]]
125
+ is_complete: bool = False
126
+ """Whether the stream has all been received.
127
+
128
+ This is set to `True` when one of
129
+ [`stream`][pydantic_ai.result.StreamedRunResult.stream],
130
+ [`stream_text`][pydantic_ai.result.StreamedRunResult.stream_text],
131
+ [`stream_structured`][pydantic_ai.result.StreamedRunResult.stream_structured] or
132
+ [`get_data`][pydantic_ai.result.StreamedRunResult.get_data] completes.
133
+ """
134
+
135
+ async def stream(self, *, debounce_by: float | None = 0.1) -> AsyncIterator[ResultData]:
136
+ """Stream the response as an async iterable.
137
+
138
+ The pydantic validator for structured data will be called in
139
+ [partial mode](https://docs.pydantic.dev/dev/concepts/experimental/#partial-validation)
140
+ on each iteration.
141
+
142
+ Args:
143
+ debounce_by: by how much (if at all) to debounce/group the response chunks by. `None` means no debouncing.
144
+ Debouncing is particularly important for long structured responses to reduce the overhead of
145
+ performing validation as each token is received.
146
+
147
+ Returns:
148
+ An async iterable of the response data.
149
+ """
150
+ if isinstance(self._stream_response, models.StreamTextResponse):
151
+ async for text in self.stream_text(debounce_by=debounce_by):
152
+ yield cast(ResultData, text)
153
+ else:
154
+ async for structured_message, is_last in self.stream_structured(debounce_by=debounce_by):
155
+ yield await self.validate_structured_result(structured_message, allow_partial=not is_last)
156
+
157
+ async def stream_text(self, *, delta: bool = False, debounce_by: float | None = 0.1) -> AsyncIterator[str]:
158
+ """Stream the text result as an async iterable.
159
+
160
+ !!! note
161
+ This method will fail if the response is structured,
162
+ e.g. if [`is_structured`][pydantic_ai.result.StreamedRunResult.is_structured] returns `True`.
163
+
164
+ !!! note
165
+ Result validators will NOT be called on the text result if `delta=True`.
166
+
167
+ Args:
168
+ delta: if `True`, yield each chunk of text as it is received, if `False` (default), yield the full text
169
+ up to the current point.
170
+ debounce_by: by how much (if at all) to debounce/group the response chunks by. `None` means no debouncing.
171
+ Debouncing is particularly important for long structured responses to reduce the overhead of
172
+ performing validation as each token is received.
173
+ """
174
+ with _logfire.span('response stream text') as lf_span:
175
+ if isinstance(self._stream_response, models.StreamStructuredResponse):
176
+ raise exceptions.UserError('stream_text() can only be used with text responses')
177
+ if delta:
178
+ async with _utils.group_by_temporal(self._stream_response, debounce_by) as group_iter:
179
+ async for _ in group_iter:
180
+ yield ''.join(self._stream_response.get())
181
+ final_delta = ''.join(self._stream_response.get(final=True))
182
+ if final_delta:
183
+ yield final_delta
184
+ else:
185
+ # a quick benchmark shows it's faster to build up a string with concat when we're
186
+ # yielding at each step
187
+ chunks: list[str] = []
188
+ combined = ''
189
+ async with _utils.group_by_temporal(self._stream_response, debounce_by) as group_iter:
190
+ async for _ in group_iter:
191
+ new = False
192
+ for chunk in self._stream_response.get():
193
+ chunks.append(chunk)
194
+ new = True
195
+ if new:
196
+ combined = await self._validate_text_result(''.join(chunks))
197
+ yield combined
198
+
199
+ new = False
200
+ for chunk in self._stream_response.get(final=True):
201
+ chunks.append(chunk)
202
+ new = True
203
+ if new:
204
+ combined = await self._validate_text_result(''.join(chunks))
205
+ yield combined
206
+ lf_span.set_attribute('combined_text', combined)
207
+ self._marked_completed(text=combined)
208
+
209
+ async def stream_structured(
210
+ self, *, debounce_by: float | None = 0.1
211
+ ) -> AsyncIterator[tuple[messages.ModelStructuredResponse, bool]]:
212
+ """Stream the response as an async iterable of Structured LLM Messages.
213
+
214
+ !!! note
215
+ This method will fail if the response is text,
216
+ e.g. if [`is_structured`][pydantic_ai.result.StreamedRunResult.is_structured] returns `False`.
217
+
218
+ Args:
219
+ debounce_by: by how much (if at all) to debounce/group the response chunks by. `None` means no debouncing.
220
+ Debouncing is particularly important for long structured responses to reduce the overhead of
221
+ performing validation as each token is received.
222
+
223
+ Returns:
224
+ An async iterable of the structured response message and whether that is the last message.
225
+ """
226
+ with _logfire.span('response stream structured') as lf_span:
227
+ if isinstance(self._stream_response, models.StreamTextResponse):
228
+ raise exceptions.UserError('stream_structured() can only be used with structured responses')
229
+ else:
230
+ # we should already have a message at this point, yield that first if it has any content
231
+ msg = self._stream_response.get()
232
+ if any(call.has_content() for call in msg.calls):
233
+ yield msg, False
234
+ async with _utils.group_by_temporal(self._stream_response, debounce_by) as group_iter:
235
+ async for _ in group_iter:
236
+ msg = self._stream_response.get()
237
+ if any(call.has_content() for call in msg.calls):
238
+ yield msg, False
239
+ msg = self._stream_response.get(final=True)
240
+ yield msg, True
241
+ lf_span.set_attribute('structured_response', msg)
242
+ self._marked_completed(structured_message=msg)
243
+
244
+ async def get_data(self) -> ResultData:
245
+ """Stream the whole response, validate and return it."""
246
+ async for _ in self._stream_response:
247
+ pass
248
+ if isinstance(self._stream_response, models.StreamTextResponse):
249
+ text = ''.join(self._stream_response.get(final=True))
250
+ text = await self._validate_text_result(text)
251
+ self._marked_completed(text=text)
252
+ return cast(ResultData, text)
253
+ else:
254
+ structured_message = self._stream_response.get(final=True)
255
+ self._marked_completed(structured_message=structured_message)
256
+ return await self.validate_structured_result(structured_message)
257
+
258
+ @property
259
+ def is_structured(self) -> bool:
260
+ """Return whether the stream response contains structured data (as opposed to text)."""
261
+ return isinstance(self._stream_response, models.StreamStructuredResponse)
262
+
263
+ def cost(self) -> Cost:
264
+ """Return the cost of the whole run.
265
+
266
+ !!! note
267
+ This won't return the full cost until the stream is finished.
268
+ """
269
+ return self.cost_so_far + self._stream_response.cost()
270
+
271
+ def timestamp(self) -> datetime:
272
+ """Get the timestamp of the response."""
273
+ return self._stream_response.timestamp()
274
+
275
+ async def validate_structured_result(
276
+ self, message: messages.ModelStructuredResponse, *, allow_partial: bool = False
277
+ ) -> ResultData:
278
+ """Validate a structured result message."""
279
+ assert self._result_schema is not None, 'Expected _result_schema to not be None'
280
+ match = self._result_schema.find_tool(message)
281
+ if match is None:
282
+ raise exceptions.UnexpectedModelBehavior(
283
+ f'Invalid message, unable to find tool: {self._result_schema.tool_names()}'
284
+ )
285
+
286
+ call, result_tool = match
287
+ result_data = result_tool.validate(call, allow_partial=allow_partial, wrap_validation_errors=False)
288
+
289
+ for validator in self._result_validators:
290
+ result_data = await validator.validate(result_data, self._deps, 0, call)
291
+ return result_data
292
+
293
+ async def _validate_text_result(self, text: str) -> str:
294
+ for validator in self._result_validators:
295
+ text = await validator.validate( # pyright: ignore[reportAssignmentType]
296
+ text, # pyright: ignore[reportArgumentType]
297
+ self._deps,
298
+ 0,
299
+ None,
300
+ )
301
+ return text
302
+
303
+ def _marked_completed(
304
+ self, *, text: str | None = None, structured_message: messages.ModelStructuredResponse | None = None
305
+ ) -> None:
306
+ self.is_complete = True
307
+ if text is not None:
308
+ assert structured_message is None, 'Either text or structured_message should provided, not both'
309
+ self._all_messages.append(
310
+ messages.ModelTextResponse(content=text, timestamp=self._stream_response.timestamp())
311
+ )
312
+ else:
313
+ assert structured_message is not None, 'Either text or structured_message should provided, not both'
314
+ self._all_messages.append(structured_message)
@@ -0,0 +1,49 @@
1
+ Metadata-Version: 2.3
2
+ Name: pydantic-ai-slim
3
+ Version: 0.0.6
4
+ Summary: Agent Framework / shim to use Pydantic with LLMs
5
+ Author-email: Samuel Colvin <samuel@pydantic.dev>
6
+ License: MIT
7
+ Classifier: Development Status :: 4 - Beta
8
+ Classifier: Environment :: Console
9
+ Classifier: Environment :: MacOS X
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Intended Audience :: Information Technology
12
+ Classifier: Intended Audience :: System Administrators
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: POSIX :: Linux
15
+ Classifier: Operating System :: Unix
16
+ Classifier: Programming Language :: Python
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3 :: Only
19
+ Classifier: Programming Language :: Python :: 3.9
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Topic :: Internet
25
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
26
+ Requires-Python: >=3.9
27
+ Requires-Dist: eval-type-backport>=0.2.0
28
+ Requires-Dist: griffe>=1.3.2
29
+ Requires-Dist: httpx>=0.27.2
30
+ Requires-Dist: logfire-api>=1.2.0
31
+ Requires-Dist: pydantic>=2.10
32
+ Provides-Extra: groq
33
+ Requires-Dist: groq>=0.12.0; extra == 'groq'
34
+ Provides-Extra: logfire
35
+ Requires-Dist: logfire>=2.3; extra == 'logfire'
36
+ Provides-Extra: openai
37
+ Requires-Dist: openai>=1.54.3; extra == 'openai'
38
+ Provides-Extra: vertexai
39
+ Requires-Dist: google-auth>=2.36.0; extra == 'vertexai'
40
+ Requires-Dist: requests>=2.32.3; extra == 'vertexai'
41
+ Description-Content-Type: text/markdown
42
+
43
+ # Coming soon
44
+
45
+ [![CI](https://github.com/pydantic/pydantic-ai/actions/workflows/ci.yml/badge.svg?event=push)](https://github.com/pydantic/pydantic-ai/actions/workflows/ci.yml?query=branch%3Amain)
46
+ [![Coverage](https://coverage-badge.samuelcolvin.workers.dev/pydantic/pydantic-ai.svg)](https://coverage-badge.samuelcolvin.workers.dev/redirect/pydantic/pydantic-ai)
47
+ [![PyPI](https://img.shields.io/pypi/v/pydantic-ai.svg)](https://pypi.python.org/pypi/pydantic-ai)
48
+ [![versions](https://img.shields.io/pypi/pyversions/pydantic-ai.svg)](https://github.com/pydantic/pydantic-ai)
49
+ [![license](https://img.shields.io/github/license/pydantic/pydantic-ai.svg?v)](https://github.com/pydantic/pydantic-ai/blob/main/LICENSE)
@@ -0,0 +1,23 @@
1
+ pydantic_ai/__init__.py,sha256=ghn5MI2Ygw6azR3i7RTw54B041pllt1E520ILK1kM5Y,319
2
+ pydantic_ai/_griffe.py,sha256=pRjCJ6B1hhx6k46XJgl9zF6aRYxRmqEZKFok8unp4Iw,3449
3
+ pydantic_ai/_pydantic.py,sha256=JslSZjj8Ni98oGfrmbS3RGM0B0_PdNZynvgAW2CnXeU,8056
4
+ pydantic_ai/_result.py,sha256=_qVaq_gmQyPWlyFn_9unafPqOiwSgoa23qxvUwMypo8,9683
5
+ pydantic_ai/_retriever.py,sha256=HxYDfpvcOrhv9gq_1CKRIRAFyLs0qquJGDO9LAWy8ok,4638
6
+ pydantic_ai/_system_prompt.py,sha256=PfNCuG45mM65HtoTiYKz6pWv_CQ9dtxPjl-u8BEn2z4,1094
7
+ pydantic_ai/_utils.py,sha256=7tPzgiiDUWAZGH6zmBZ91CUN3aFSv0szEkw0RDbPLJg,8128
8
+ pydantic_ai/agent.py,sha256=nFwHI3w8JpCqze5UX5TAxiZsSSyavsyKo2W5ITNke1g,34454
9
+ pydantic_ai/dependencies.py,sha256=6IawVRoLSefOtO6oCsxB3kndJRVEP1-DHSE9-2T3ZW8,2509
10
+ pydantic_ai/exceptions.py,sha256=bcde_Xg2yCnSYduhTbbtH3_9kQR6UuKqWJNEzfxiKyw,1566
11
+ pydantic_ai/messages.py,sha256=tmZOFCGVIEmSk8YYQ9qiyb6Q-6DTiqOzfWiryCHVQpY,7536
12
+ pydantic_ai/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
+ pydantic_ai/result.py,sha256=Gs2ZjuFJgONGJm8M5R2mGph5-lUFLBg7FxVrs2CVDPs,13525
14
+ pydantic_ai/models/__init__.py,sha256=gAH9jbSk6ezqwJpCoVHbdMwULz9mu5VuguARUqKfN8w,10140
15
+ pydantic_ai/models/function.py,sha256=dv1a1RhfdXqwXMFI_y8QzPWJj_YW6Q7twvKvO98JVUE,10124
16
+ pydantic_ai/models/gemini.py,sha256=vHg91s9UzqU3teNnvOJLXkYI8Gu9Wt1vhEu6zB3Ttd8,27677
17
+ pydantic_ai/models/groq.py,sha256=7zKpFg3Fvc-S2n_EroQg5AAD5cOj4y7SKQ7VjMiNSUs,14939
18
+ pydantic_ai/models/openai.py,sha256=_XZ0eDbk3Q3_V2MlMMaZArzYGSm2VuBR47lv4TmFgjY,14923
19
+ pydantic_ai/models/test.py,sha256=T53NKS7pQbCgzo7QIFGdObt3zbDs5t-tjSlkDNsKCuo,14523
20
+ pydantic_ai/models/vertexai.py,sha256=YiYB82WOB5Kp4NjYhweN0ZNjE39-dnpDseMyoyf6Evk,11294
21
+ pydantic_ai_slim-0.0.6.dist-info/METADATA,sha256=t8H1QGNz1C_fmcjDTfsgvX6hRt4RZBNs-uydcdNgwJA,2370
22
+ pydantic_ai_slim-0.0.6.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
23
+ pydantic_ai_slim-0.0.6.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.26.3
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any