pydantic-ai-slim 0.0.16__tar.gz → 0.0.17__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.

Files changed (29) hide show
  1. {pydantic_ai_slim-0.0.16 → pydantic_ai_slim-0.0.17}/PKG-INFO +1 -1
  2. {pydantic_ai_slim-0.0.16 → pydantic_ai_slim-0.0.17}/pydantic_ai/agent.py +16 -12
  3. pydantic_ai_slim-0.0.17/pydantic_ai/format_as_xml.py +115 -0
  4. {pydantic_ai_slim-0.0.16 → pydantic_ai_slim-0.0.17}/pydantic_ai/models/gemini.py +12 -21
  5. {pydantic_ai_slim-0.0.16 → pydantic_ai_slim-0.0.17}/pydantic_ai/models/vertexai.py +1 -1
  6. {pydantic_ai_slim-0.0.16 → pydantic_ai_slim-0.0.17}/pydantic_ai/result.py +92 -69
  7. pydantic_ai_slim-0.0.17/pydantic_ai/settings.py +81 -0
  8. {pydantic_ai_slim-0.0.16 → pydantic_ai_slim-0.0.17}/pydantic_ai/tools.py +7 -7
  9. pydantic_ai_slim-0.0.16/pydantic_ai/settings.py → pydantic_ai_slim-0.0.17/pydantic_ai/usage.py +46 -73
  10. {pydantic_ai_slim-0.0.16 → pydantic_ai_slim-0.0.17}/pyproject.toml +1 -1
  11. {pydantic_ai_slim-0.0.16 → pydantic_ai_slim-0.0.17}/.gitignore +0 -0
  12. {pydantic_ai_slim-0.0.16 → pydantic_ai_slim-0.0.17}/README.md +0 -0
  13. {pydantic_ai_slim-0.0.16 → pydantic_ai_slim-0.0.17}/pydantic_ai/__init__.py +0 -0
  14. {pydantic_ai_slim-0.0.16 → pydantic_ai_slim-0.0.17}/pydantic_ai/_griffe.py +0 -0
  15. {pydantic_ai_slim-0.0.16 → pydantic_ai_slim-0.0.17}/pydantic_ai/_pydantic.py +0 -0
  16. {pydantic_ai_slim-0.0.16 → pydantic_ai_slim-0.0.17}/pydantic_ai/_result.py +0 -0
  17. {pydantic_ai_slim-0.0.16 → pydantic_ai_slim-0.0.17}/pydantic_ai/_system_prompt.py +0 -0
  18. {pydantic_ai_slim-0.0.16 → pydantic_ai_slim-0.0.17}/pydantic_ai/_utils.py +0 -0
  19. {pydantic_ai_slim-0.0.16 → pydantic_ai_slim-0.0.17}/pydantic_ai/exceptions.py +0 -0
  20. {pydantic_ai_slim-0.0.16 → pydantic_ai_slim-0.0.17}/pydantic_ai/messages.py +0 -0
  21. {pydantic_ai_slim-0.0.16 → pydantic_ai_slim-0.0.17}/pydantic_ai/models/__init__.py +0 -0
  22. {pydantic_ai_slim-0.0.16 → pydantic_ai_slim-0.0.17}/pydantic_ai/models/anthropic.py +0 -0
  23. {pydantic_ai_slim-0.0.16 → pydantic_ai_slim-0.0.17}/pydantic_ai/models/function.py +0 -0
  24. {pydantic_ai_slim-0.0.16 → pydantic_ai_slim-0.0.17}/pydantic_ai/models/groq.py +0 -0
  25. {pydantic_ai_slim-0.0.16 → pydantic_ai_slim-0.0.17}/pydantic_ai/models/mistral.py +0 -0
  26. {pydantic_ai_slim-0.0.16 → pydantic_ai_slim-0.0.17}/pydantic_ai/models/ollama.py +0 -0
  27. {pydantic_ai_slim-0.0.16 → pydantic_ai_slim-0.0.17}/pydantic_ai/models/openai.py +0 -0
  28. {pydantic_ai_slim-0.0.16 → pydantic_ai_slim-0.0.17}/pydantic_ai/models/test.py +0 -0
  29. {pydantic_ai_slim-0.0.16 → pydantic_ai_slim-0.0.17}/pydantic_ai/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pydantic-ai-slim
3
- Version: 0.0.16
3
+ Version: 0.0.17
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
@@ -20,9 +20,10 @@ from . import (
20
20
  messages as _messages,
21
21
  models,
22
22
  result,
23
+ usage as _usage,
23
24
  )
24
25
  from .result import ResultData
25
- from .settings import ModelSettings, UsageLimits, merge_model_settings
26
+ from .settings import ModelSettings, merge_model_settings
26
27
  from .tools import (
27
28
  AgentDeps,
28
29
  RunContext,
@@ -192,8 +193,8 @@ class Agent(Generic[AgentDeps, ResultData]):
192
193
  model: models.Model | models.KnownModelName | None = None,
193
194
  deps: AgentDeps = None,
194
195
  model_settings: ModelSettings | None = None,
195
- usage_limits: UsageLimits | None = None,
196
- usage: result.Usage | None = None,
196
+ usage_limits: _usage.UsageLimits | None = None,
197
+ usage: _usage.Usage | None = None,
197
198
  infer_name: bool = True,
198
199
  ) -> result.RunResult[ResultData]:
199
200
  """Run the agent with a user prompt in async mode.
@@ -236,7 +237,7 @@ class Agent(Generic[AgentDeps, ResultData]):
236
237
  model_name=model_used.name(),
237
238
  agent_name=self.name or 'agent',
238
239
  ) as run_span:
239
- run_context = RunContext(deps, model_used, usage or result.Usage(), user_prompt)
240
+ run_context = RunContext(deps, model_used, usage or _usage.Usage(), user_prompt)
240
241
  messages = await self._prepare_messages(user_prompt, message_history, run_context)
241
242
  run_context.messages = messages
242
243
 
@@ -244,7 +245,7 @@ class Agent(Generic[AgentDeps, ResultData]):
244
245
  tool.current_retry = 0
245
246
 
246
247
  model_settings = merge_model_settings(self.model_settings, model_settings)
247
- usage_limits = usage_limits or UsageLimits()
248
+ usage_limits = usage_limits or _usage.UsageLimits()
248
249
 
249
250
  while True:
250
251
  usage_limits.check_before_request(run_context.usage)
@@ -272,11 +273,14 @@ class Agent(Generic[AgentDeps, ResultData]):
272
273
  # Check if we got a final result
273
274
  if final_result is not None:
274
275
  result_data = final_result.data
276
+ result_tool_name = final_result.tool_name
275
277
  run_span.set_attribute('all_messages', messages)
276
278
  run_span.set_attribute('usage', run_context.usage)
277
279
  handle_span.set_attribute('result', result_data)
278
280
  handle_span.message = 'handle model response -> final result'
279
- return result.RunResult(messages, new_message_index, result_data, run_context.usage)
281
+ return result.RunResult(
282
+ messages, new_message_index, result_data, result_tool_name, run_context.usage
283
+ )
280
284
  else:
281
285
  # continue the conversation
282
286
  handle_span.set_attribute('tool_responses', tool_responses)
@@ -291,8 +295,8 @@ class Agent(Generic[AgentDeps, ResultData]):
291
295
  model: models.Model | models.KnownModelName | None = None,
292
296
  deps: AgentDeps = None,
293
297
  model_settings: ModelSettings | None = None,
294
- usage_limits: UsageLimits | None = None,
295
- usage: result.Usage | None = None,
298
+ usage_limits: _usage.UsageLimits | None = None,
299
+ usage: _usage.Usage | None = None,
296
300
  infer_name: bool = True,
297
301
  ) -> result.RunResult[ResultData]:
298
302
  """Run the agent with a user prompt synchronously.
@@ -349,8 +353,8 @@ class Agent(Generic[AgentDeps, ResultData]):
349
353
  model: models.Model | models.KnownModelName | None = None,
350
354
  deps: AgentDeps = None,
351
355
  model_settings: ModelSettings | None = None,
352
- usage_limits: UsageLimits | None = None,
353
- usage: result.Usage | None = None,
356
+ usage_limits: _usage.UsageLimits | None = None,
357
+ usage: _usage.Usage | None = None,
354
358
  infer_name: bool = True,
355
359
  ) -> AsyncIterator[result.StreamedRunResult[AgentDeps, ResultData]]:
356
360
  """Run the agent with a user prompt in async mode, returning a streamed response.
@@ -396,7 +400,7 @@ class Agent(Generic[AgentDeps, ResultData]):
396
400
  model_name=model_used.name(),
397
401
  agent_name=self.name or 'agent',
398
402
  ) as run_span:
399
- run_context = RunContext(deps, model_used, usage or result.Usage(), user_prompt)
403
+ run_context = RunContext(deps, model_used, usage or _usage.Usage(), user_prompt)
400
404
  messages = await self._prepare_messages(user_prompt, message_history, run_context)
401
405
  run_context.messages = messages
402
406
 
@@ -404,7 +408,7 @@ class Agent(Generic[AgentDeps, ResultData]):
404
408
  tool.current_retry = 0
405
409
 
406
410
  model_settings = merge_model_settings(self.model_settings, model_settings)
407
- usage_limits = usage_limits or UsageLimits()
411
+ usage_limits = usage_limits or _usage.UsageLimits()
408
412
 
409
413
  while True:
410
414
  run_context.run_step += 1
@@ -0,0 +1,115 @@
1
+ from __future__ import annotations as _annotations
2
+
3
+ from collections.abc import Iterable, Iterator, Mapping
4
+ from dataclasses import asdict, dataclass, is_dataclass
5
+ from datetime import date
6
+ from typing import Any
7
+ from xml.etree import ElementTree
8
+
9
+ from pydantic import BaseModel
10
+
11
+ __all__ = ('format_as_xml',)
12
+
13
+
14
+ def format_as_xml(
15
+ obj: Any,
16
+ root_tag: str = 'examples',
17
+ item_tag: str = 'example',
18
+ include_root_tag: bool = True,
19
+ none_str: str = 'null',
20
+ indent: str | None = ' ',
21
+ ) -> str:
22
+ """Format a Python object as XML.
23
+
24
+ This is useful since LLMs often find it easier to read semi-structured data (e.g. examples) as XML,
25
+ rather than JSON etc.
26
+
27
+ Supports: `str`, `bytes`, `bytearray`, `bool`, `int`, `float`, `date`, `datetime`, `Mapping`,
28
+ `Iterable`, `dataclass`, and `BaseModel`.
29
+
30
+ Args:
31
+ obj: Python Object to serialize to XML.
32
+ root_tag: Outer tag to wrap the XML in, use `None` to omit the outer tag.
33
+ item_tag: Tag to use for each item in an iterable (e.g. list), this is overridden by the class name
34
+ for dataclasses and Pydantic models.
35
+ include_root_tag: Whether to include the root tag in the output
36
+ (The root tag is always included if it includes a body - e.g. when the input is a simple value).
37
+ none_str: String to use for `None` values.
38
+ indent: Indentation string to use for pretty printing.
39
+
40
+ Returns: XML representation of the object.
41
+
42
+ Example:
43
+ ```python {title="format_as_xml_example.py" lint="skip"}
44
+ from pydantic_ai.format_as_xml import format_as_xml
45
+
46
+ print(format_as_xml({'name': 'John', 'height': 6, 'weight': 200}, root_tag='user'))
47
+ '''
48
+ <user>
49
+ <name>John</name>
50
+ <height>6</height>
51
+ <weight>200</weight>
52
+ </user>
53
+ '''
54
+ ```
55
+ """
56
+ el = _ToXml(item_tag=item_tag, none_str=none_str).to_xml(obj, root_tag)
57
+ if not include_root_tag and el.text is None:
58
+ join = '' if indent is None else '\n'
59
+ return join.join(_rootless_xml_elements(el, indent))
60
+ else:
61
+ if indent is not None:
62
+ ElementTree.indent(el, space=indent)
63
+ return ElementTree.tostring(el, encoding='unicode')
64
+
65
+
66
+ @dataclass
67
+ class _ToXml:
68
+ item_tag: str
69
+ none_str: str
70
+
71
+ def to_xml(self, value: Any, tag: str | None) -> ElementTree.Element:
72
+ element = ElementTree.Element(self.item_tag if tag is None else tag)
73
+ if value is None:
74
+ element.text = self.none_str
75
+ elif isinstance(value, str):
76
+ element.text = value
77
+ elif isinstance(value, (bytes, bytearray)):
78
+ element.text = value.decode(errors='ignore')
79
+ elif isinstance(value, (bool, int, float)):
80
+ element.text = str(value)
81
+ elif isinstance(value, date):
82
+ element.text = value.isoformat()
83
+ elif isinstance(value, Mapping):
84
+ self._mapping_to_xml(element, value) # pyright: ignore[reportUnknownArgumentType]
85
+ elif is_dataclass(value) and not isinstance(value, type):
86
+ if tag is None:
87
+ element = ElementTree.Element(value.__class__.__name__)
88
+ dc_dict = asdict(value)
89
+ self._mapping_to_xml(element, dc_dict)
90
+ elif isinstance(value, BaseModel):
91
+ if tag is None:
92
+ element = ElementTree.Element(value.__class__.__name__)
93
+ self._mapping_to_xml(element, value.model_dump(mode='python'))
94
+ elif isinstance(value, Iterable):
95
+ for item in value: # pyright: ignore[reportUnknownVariableType]
96
+ item_el = self.to_xml(item, None)
97
+ element.append(item_el)
98
+ else:
99
+ raise TypeError(f'Unsupported type for XML formatting: {type(value)}')
100
+ return element
101
+
102
+ def _mapping_to_xml(self, element: ElementTree.Element, mapping: Mapping[Any, Any]) -> None:
103
+ for key, value in mapping.items():
104
+ if isinstance(key, int):
105
+ key = str(key)
106
+ elif not isinstance(key, str):
107
+ raise TypeError(f'Unsupported key type for XML formatting: {type(key)}, only str and int are allowed')
108
+ element.append(self.to_xml(value, key))
109
+
110
+
111
+ def _rootless_xml_elements(root: ElementTree.Element, indent: str | None) -> Iterator[str]:
112
+ for sub_element in root:
113
+ if indent is not None:
114
+ ElementTree.indent(sub_element, space=indent)
115
+ yield ElementTree.tostring(sub_element, encoding='unicode')
@@ -273,17 +273,26 @@ class GeminiAgentModel(AgentModel):
273
273
  contents: list[_GeminiContent] = []
274
274
  for m in messages:
275
275
  if isinstance(m, ModelRequest):
276
+ message_parts: list[_GeminiPartUnion] = []
277
+
276
278
  for part in m.parts:
277
279
  if isinstance(part, SystemPromptPart):
278
280
  sys_prompt_parts.append(_GeminiTextPart(text=part.content))
279
281
  elif isinstance(part, UserPromptPart):
280
- contents.append(_content_user_prompt(part))
282
+ message_parts.append(_GeminiTextPart(text=part.content))
281
283
  elif isinstance(part, ToolReturnPart):
282
- contents.append(_content_tool_return(part))
284
+ message_parts.append(_response_part_from_response(part.tool_name, part.model_response_object()))
283
285
  elif isinstance(part, RetryPromptPart):
284
- contents.append(_content_retry_prompt(part))
286
+ if part.tool_name is None:
287
+ message_parts.append(_GeminiTextPart(text=part.model_response()))
288
+ else:
289
+ response = {'call_error': part.model_response()}
290
+ message_parts.append(_response_part_from_response(part.tool_name, response))
285
291
  else:
286
292
  assert_never(part)
293
+
294
+ if message_parts:
295
+ contents.append(_GeminiContent(role='user', parts=message_parts))
287
296
  elif isinstance(m, ModelResponse):
288
297
  contents.append(_content_model_response(m))
289
298
  else:
@@ -420,24 +429,6 @@ class _GeminiContent(TypedDict):
420
429
  parts: list[_GeminiPartUnion]
421
430
 
422
431
 
423
- def _content_user_prompt(m: UserPromptPart) -> _GeminiContent:
424
- return _GeminiContent(role='user', parts=[_GeminiTextPart(text=m.content)])
425
-
426
-
427
- def _content_tool_return(m: ToolReturnPart) -> _GeminiContent:
428
- f_response = _response_part_from_response(m.tool_name, m.model_response_object())
429
- return _GeminiContent(role='user', parts=[f_response])
430
-
431
-
432
- def _content_retry_prompt(m: RetryPromptPart) -> _GeminiContent:
433
- if m.tool_name is None:
434
- part = _GeminiTextPart(text=m.model_response())
435
- else:
436
- response = {'call_error': m.model_response()}
437
- part = _response_part_from_response(m.tool_name, response)
438
- return _GeminiContent(role='user', parts=[part])
439
-
440
-
441
432
  def _content_model_response(m: ModelResponse) -> _GeminiContent:
442
433
  parts: list[_GeminiPartUnion] = []
443
434
  for item in m.parts:
@@ -178,7 +178,7 @@ def _creds_from_file(service_account_file: str | Path) -> ServiceAccountCredenti
178
178
  # pyright: reportUnknownVariableType=false
179
179
  # pyright: reportUnknownArgumentType=false
180
180
  async def _async_google_auth() -> tuple[BaseCredentials, str | None]:
181
- return await run_in_executor(google.auth.default)
181
+ return await run_in_executor(google.auth.default, scopes=['https://www.googleapis.com/auth/cloud-platform'])
182
182
 
183
183
 
184
184
  # default expiry is 3600 seconds
@@ -2,7 +2,7 @@ from __future__ import annotations as _annotations
2
2
 
3
3
  from abc import ABC, abstractmethod
4
4
  from collections.abc import AsyncIterator, Awaitable, Callable
5
- from copy import copy
5
+ from copy import deepcopy
6
6
  from dataclasses import dataclass, field
7
7
  from datetime import datetime
8
8
  from typing import Generic, Union, cast
@@ -11,16 +11,10 @@ import logfire_api
11
11
  from typing_extensions import TypeVar
12
12
 
13
13
  from . import _result, _utils, exceptions, messages as _messages, models
14
- from .settings import UsageLimits
15
14
  from .tools import AgentDeps, RunContext
15
+ from .usage import Usage, UsageLimits
16
16
 
17
- __all__ = (
18
- 'ResultData',
19
- 'ResultValidatorFunc',
20
- 'Usage',
21
- 'RunResult',
22
- 'StreamedRunResult',
23
- )
17
+ __all__ = 'ResultData', 'ResultValidatorFunc', 'RunResult', 'StreamedRunResult'
24
18
 
25
19
 
26
20
  ResultData = TypeVar('ResultData', default=str)
@@ -44,55 +38,6 @@ Usage `ResultValidatorFunc[AgentDeps, ResultData]`.
44
38
  _logfire = logfire_api.Logfire(otel_scope='pydantic-ai')
45
39
 
46
40
 
47
- @dataclass
48
- class Usage:
49
- """LLM usage associated with a request or run.
50
-
51
- Responsibility for calculating usage is on the model; PydanticAI simply sums the usage information across requests.
52
-
53
- You'll need to look up the documentation of the model you're using to convert usage to monetary costs.
54
- """
55
-
56
- requests: int = 0
57
- """Number of requests made to the LLM API."""
58
- request_tokens: int | None = None
59
- """Tokens used in processing requests."""
60
- response_tokens: int | None = None
61
- """Tokens used in generating responses."""
62
- total_tokens: int | None = None
63
- """Total tokens used in the whole run, should generally be equal to `request_tokens + response_tokens`."""
64
- details: dict[str, int] | None = None
65
- """Any extra details returned by the model."""
66
-
67
- def incr(self, incr_usage: Usage, *, requests: int = 0) -> None:
68
- """Increment the usage in place.
69
-
70
- Args:
71
- incr_usage: The usage to increment by.
72
- requests: The number of requests to increment by in addition to `incr_usage.requests`.
73
- """
74
- self.requests += requests
75
- for f in 'requests', 'request_tokens', 'response_tokens', 'total_tokens':
76
- self_value = getattr(self, f)
77
- other_value = getattr(incr_usage, f)
78
- if self_value is not None or other_value is not None:
79
- setattr(self, f, (self_value or 0) + (other_value or 0))
80
-
81
- if incr_usage.details:
82
- self.details = self.details or {}
83
- for key, value in incr_usage.details.items():
84
- self.details[key] = self.details.get(key, 0) + value
85
-
86
- def __add__(self, other: Usage) -> Usage:
87
- """Add two Usages together.
88
-
89
- This is provided so it's trivial to sum usage information from multiple requests and runs.
90
- """
91
- new_usage = copy(self)
92
- new_usage.incr(other)
93
- return new_usage
94
-
95
-
96
41
  @dataclass
97
42
  class _BaseRunResult(ABC, Generic[ResultData]):
98
43
  """Base type for results.
@@ -103,25 +48,70 @@ class _BaseRunResult(ABC, Generic[ResultData]):
103
48
  _all_messages: list[_messages.ModelMessage]
104
49
  _new_message_index: int
105
50
 
106
- def all_messages(self) -> list[_messages.ModelMessage]:
107
- """Return the history of _messages."""
51
+ def all_messages(self, *, result_tool_return_content: str | None = None) -> list[_messages.ModelMessage]:
52
+ """Return the history of _messages.
53
+
54
+ Args:
55
+ result_tool_return_content: The return content of the tool call to set in the last message.
56
+ This provides a convenient way to modify the content of the result tool call if you want to continue
57
+ the conversation and want to set the response to the result tool call. If `None`, the last message will
58
+ not be modified.
59
+
60
+ Returns:
61
+ List of messages.
62
+ """
108
63
  # this is a method to be consistent with the other methods
64
+ if result_tool_return_content is not None:
65
+ raise NotImplementedError('Setting result tool return content is not supported for this result type.')
109
66
  return self._all_messages
110
67
 
111
- def all_messages_json(self) -> bytes:
112
- """Return all messages from [`all_messages`][pydantic_ai.result._BaseRunResult.all_messages] as JSON bytes."""
113
- return _messages.ModelMessagesTypeAdapter.dump_json(self.all_messages())
68
+ def all_messages_json(self, *, result_tool_return_content: str | None = None) -> bytes:
69
+ """Return all messages from [`all_messages`][pydantic_ai.result._BaseRunResult.all_messages] as JSON bytes.
70
+
71
+ Args:
72
+ result_tool_return_content: The return content of the tool call to set in the last message.
73
+ This provides a convenient way to modify the content of the result tool call if you want to continue
74
+ the conversation and want to set the response to the result tool call. If `None`, the last message will
75
+ not be modified.
76
+
77
+ Returns:
78
+ JSON bytes representing the messages.
79
+ """
80
+ return _messages.ModelMessagesTypeAdapter.dump_json(
81
+ self.all_messages(result_tool_return_content=result_tool_return_content)
82
+ )
114
83
 
115
- def new_messages(self) -> list[_messages.ModelMessage]:
84
+ def new_messages(self, *, result_tool_return_content: str | None = None) -> list[_messages.ModelMessage]:
116
85
  """Return new messages associated with this run.
117
86
 
118
- System prompts and any messages from older runs are excluded.
87
+ Messages from older runs are excluded.
88
+
89
+ Args:
90
+ result_tool_return_content: The return content of the tool call to set in the last message.
91
+ This provides a convenient way to modify the content of the result tool call if you want to continue
92
+ the conversation and want to set the response to the result tool call. If `None`, the last message will
93
+ not be modified.
94
+
95
+ Returns:
96
+ List of new messages.
119
97
  """
120
- return self.all_messages()[self._new_message_index :]
98
+ return self.all_messages(result_tool_return_content=result_tool_return_content)[self._new_message_index :]
99
+
100
+ def new_messages_json(self, *, result_tool_return_content: str | None = None) -> bytes:
101
+ """Return new messages from [`new_messages`][pydantic_ai.result._BaseRunResult.new_messages] as JSON bytes.
121
102
 
122
- def new_messages_json(self) -> bytes:
123
- """Return new messages from [`new_messages`][pydantic_ai.result._BaseRunResult.new_messages] as JSON bytes."""
124
- return _messages.ModelMessagesTypeAdapter.dump_json(self.new_messages())
103
+ Args:
104
+ result_tool_return_content: The return content of the tool call to set in the last message.
105
+ This provides a convenient way to modify the content of the result tool call if you want to continue
106
+ the conversation and want to set the response to the result tool call. If `None`, the last message will
107
+ not be modified.
108
+
109
+ Returns:
110
+ JSON bytes representing the new messages.
111
+ """
112
+ return _messages.ModelMessagesTypeAdapter.dump_json(
113
+ self.new_messages(result_tool_return_content=result_tool_return_content)
114
+ )
125
115
 
126
116
  @abstractmethod
127
117
  def usage(self) -> Usage:
@@ -134,12 +124,45 @@ class RunResult(_BaseRunResult[ResultData]):
134
124
 
135
125
  data: ResultData
136
126
  """Data from the final response in the run."""
127
+ _result_tool_name: str | None
137
128
  _usage: Usage
138
129
 
139
130
  def usage(self) -> Usage:
140
131
  """Return the usage of the whole run."""
141
132
  return self._usage
142
133
 
134
+ def all_messages(self, *, result_tool_return_content: str | None = None) -> list[_messages.ModelMessage]:
135
+ """Return the history of _messages.
136
+
137
+ Args:
138
+ result_tool_return_content: The return content of the tool call to set in the last message.
139
+ This provides a convenient way to modify the content of the result tool call if you want to continue
140
+ the conversation and want to set the response to the result tool call. If `None`, the last message will
141
+ not be modified.
142
+
143
+ Returns:
144
+ List of messages.
145
+ """
146
+ if result_tool_return_content is not None:
147
+ return self._set_result_tool_return(result_tool_return_content)
148
+ else:
149
+ return self._all_messages
150
+
151
+ def _set_result_tool_return(self, return_content: str) -> list[_messages.ModelMessage]:
152
+ """Set return content for the result tool.
153
+
154
+ Useful if you want to continue the conversation and want to set the response to the result tool call.
155
+ """
156
+ if not self._result_tool_name:
157
+ raise ValueError('Cannot set result tool return content when the return type is `str`.')
158
+ messages = deepcopy(self._all_messages)
159
+ last_message = messages[-1]
160
+ for part in last_message.parts:
161
+ if isinstance(part, _messages.ToolReturnPart) and part.tool_name == self._result_tool_name:
162
+ part.content = return_content
163
+ return messages
164
+ raise LookupError(f'No tool call found with tool name {self._result_tool_name!r}.')
165
+
143
166
 
144
167
  @dataclass
145
168
  class StreamedRunResult(_BaseRunResult[ResultData], Generic[AgentDeps, ResultData]):
@@ -0,0 +1,81 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from httpx import Timeout
6
+ from typing_extensions import TypedDict
7
+
8
+ if TYPE_CHECKING:
9
+ pass
10
+
11
+
12
+ class ModelSettings(TypedDict, total=False):
13
+ """Settings to configure an LLM.
14
+
15
+ Here we include only settings which apply to multiple models / model providers.
16
+ """
17
+
18
+ max_tokens: int
19
+ """The maximum number of tokens to generate before stopping.
20
+
21
+ Supported by:
22
+
23
+ * Gemini
24
+ * Anthropic
25
+ * OpenAI
26
+ * Groq
27
+ """
28
+
29
+ temperature: float
30
+ """Amount of randomness injected into the response.
31
+
32
+ Use `temperature` closer to `0.0` for analytical / multiple choice, and closer to a model's
33
+ maximum `temperature` for creative and generative tasks.
34
+
35
+ Note that even with `temperature` of `0.0`, the results will not be fully deterministic.
36
+
37
+ Supported by:
38
+
39
+ * Gemini
40
+ * Anthropic
41
+ * OpenAI
42
+ * Groq
43
+ """
44
+
45
+ top_p: float
46
+ """An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass.
47
+
48
+ So 0.1 means only the tokens comprising the top 10% probability mass are considered.
49
+
50
+ You should either alter `temperature` or `top_p`, but not both.
51
+
52
+ Supported by:
53
+
54
+ * Gemini
55
+ * Anthropic
56
+ * OpenAI
57
+ * Groq
58
+ """
59
+
60
+ timeout: float | Timeout
61
+ """Override the client-level default timeout for a request, in seconds.
62
+
63
+ Supported by:
64
+
65
+ * Gemini
66
+ * Anthropic
67
+ * OpenAI
68
+ * Groq
69
+ """
70
+
71
+
72
+ def merge_model_settings(base: ModelSettings | None, overrides: ModelSettings | None) -> ModelSettings | None:
73
+ """Merge two sets of model settings, preferring the overrides.
74
+
75
+ A common use case is: merge_model_settings(<agent settings>, <run settings>)
76
+ """
77
+ # Note: we may want merge recursively if/when we add non-primitive values
78
+ if base and overrides:
79
+ return base | overrides
80
+ else:
81
+ return base or overrides
@@ -4,11 +4,11 @@ import dataclasses
4
4
  import inspect
5
5
  from collections.abc import Awaitable
6
6
  from dataclasses import dataclass, field
7
- from typing import TYPE_CHECKING, Any, Callable, Generic, TypeVar, Union, cast
7
+ from typing import TYPE_CHECKING, Any, Callable, Generic, Union, cast
8
8
 
9
9
  from pydantic import ValidationError
10
10
  from pydantic_core import SchemaValidator
11
- from typing_extensions import Concatenate, ParamSpec, TypeAlias
11
+ from typing_extensions import Concatenate, ParamSpec, TypeAlias, TypeVar
12
12
 
13
13
  from . import _pydantic, _utils, messages as _messages, models
14
14
  from .exceptions import ModelRetry, UnexpectedModelBehavior
@@ -30,7 +30,7 @@ __all__ = (
30
30
  'ToolDefinition',
31
31
  )
32
32
 
33
- AgentDeps = TypeVar('AgentDeps')
33
+ AgentDeps = TypeVar('AgentDeps', default=None)
34
34
  """Type variable for agent dependencies."""
35
35
 
36
36
 
@@ -67,7 +67,7 @@ class RunContext(Generic[AgentDeps]):
67
67
  return dataclasses.replace(self, **kwargs)
68
68
 
69
69
 
70
- ToolParams = ParamSpec('ToolParams')
70
+ ToolParams = ParamSpec('ToolParams', default=...)
71
71
  """Retrieval function param spec."""
72
72
 
73
73
  SystemPromptFunc = Union[
@@ -92,7 +92,7 @@ ToolFuncPlain = Callable[ToolParams, Any]
92
92
  Usage `ToolPlainFunc[ToolParams]`.
93
93
  """
94
94
  ToolFuncEither = Union[ToolFuncContext[AgentDeps, ToolParams], ToolFuncPlain[ToolParams]]
95
- """Either part_kind of tool function.
95
+ """Either kind of tool function.
96
96
 
97
97
  This is just a union of [`ToolFuncContext`][pydantic_ai.tools.ToolFuncContext] and
98
98
  [`ToolFuncPlain`][pydantic_ai.tools.ToolFuncPlain].
@@ -134,7 +134,7 @@ A = TypeVar('A')
134
134
  class Tool(Generic[AgentDeps]):
135
135
  """A tool function for an agent."""
136
136
 
137
- function: ToolFuncEither[AgentDeps, ...]
137
+ function: ToolFuncEither[AgentDeps]
138
138
  takes_ctx: bool
139
139
  max_retries: int | None
140
140
  name: str
@@ -150,7 +150,7 @@ class Tool(Generic[AgentDeps]):
150
150
 
151
151
  def __init__(
152
152
  self,
153
- function: ToolFuncEither[AgentDeps, ...],
153
+ function: ToolFuncEither[AgentDeps],
154
154
  *,
155
155
  takes_ctx: bool | None = None,
156
156
  max_retries: int | None = None,
@@ -1,87 +1,60 @@
1
- from __future__ import annotations
1
+ from __future__ import annotations as _annotations
2
2
 
3
+ from copy import copy
3
4
  from dataclasses import dataclass
4
- from typing import TYPE_CHECKING
5
-
6
- from httpx import Timeout
7
- from typing_extensions import TypedDict
8
5
 
9
6
  from .exceptions import UsageLimitExceeded
10
7
 
11
- if TYPE_CHECKING:
12
- from .result import Usage
13
-
14
-
15
- class ModelSettings(TypedDict, total=False):
16
- """Settings to configure an LLM.
17
-
18
- Here we include only settings which apply to multiple models / model providers.
19
- """
20
-
21
- max_tokens: int
22
- """The maximum number of tokens to generate before stopping.
23
-
24
- Supported by:
25
-
26
- * Gemini
27
- * Anthropic
28
- * OpenAI
29
- * Groq
30
- """
31
-
32
- temperature: float
33
- """Amount of randomness injected into the response.
34
-
35
- Use `temperature` closer to `0.0` for analytical / multiple choice, and closer to a model's
36
- maximum `temperature` for creative and generative tasks.
37
-
38
- Note that even with `temperature` of `0.0`, the results will not be fully deterministic.
39
-
40
- Supported by:
8
+ __all__ = 'Usage', 'UsageLimits'
41
9
 
42
- * Gemini
43
- * Anthropic
44
- * OpenAI
45
- * Groq
46
- """
47
-
48
- top_p: float
49
- """An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass.
50
-
51
- So 0.1 means only the tokens comprising the top 10% probability mass are considered.
52
-
53
- You should either alter `temperature` or `top_p`, but not both.
54
-
55
- Supported by:
56
-
57
- * Gemini
58
- * Anthropic
59
- * OpenAI
60
- * Groq
61
- """
62
10
 
63
- timeout: float | Timeout
64
- """Override the client-level default timeout for a request, in seconds.
11
+ @dataclass
12
+ class Usage:
13
+ """LLM usage associated with a request or run.
65
14
 
66
- Supported by:
15
+ Responsibility for calculating usage is on the model; PydanticAI simply sums the usage information across requests.
67
16
 
68
- * Gemini
69
- * Anthropic
70
- * OpenAI
71
- * Groq
17
+ You'll need to look up the documentation of the model you're using to convert usage to monetary costs.
72
18
  """
73
19
 
74
-
75
- def merge_model_settings(base: ModelSettings | None, overrides: ModelSettings | None) -> ModelSettings | None:
76
- """Merge two sets of model settings, preferring the overrides.
77
-
78
- A common use case is: merge_model_settings(<agent settings>, <run settings>)
79
- """
80
- # Note: we may want merge recursively if/when we add non-primitive values
81
- if base and overrides:
82
- return base | overrides
83
- else:
84
- return base or overrides
20
+ requests: int = 0
21
+ """Number of requests made to the LLM API."""
22
+ request_tokens: int | None = None
23
+ """Tokens used in processing requests."""
24
+ response_tokens: int | None = None
25
+ """Tokens used in generating responses."""
26
+ total_tokens: int | None = None
27
+ """Total tokens used in the whole run, should generally be equal to `request_tokens + response_tokens`."""
28
+ details: dict[str, int] | None = None
29
+ """Any extra details returned by the model."""
30
+
31
+ def incr(self, incr_usage: Usage, *, requests: int = 0) -> None:
32
+ """Increment the usage in place.
33
+
34
+ Args:
35
+ incr_usage: The usage to increment by.
36
+ requests: The number of requests to increment by in addition to `incr_usage.requests`.
37
+ """
38
+ self.requests += requests
39
+ for f in 'requests', 'request_tokens', 'response_tokens', 'total_tokens':
40
+ self_value = getattr(self, f)
41
+ other_value = getattr(incr_usage, f)
42
+ if self_value is not None or other_value is not None:
43
+ setattr(self, f, (self_value or 0) + (other_value or 0))
44
+
45
+ if incr_usage.details:
46
+ self.details = self.details or {}
47
+ for key, value in incr_usage.details.items():
48
+ self.details[key] = self.details.get(key, 0) + value
49
+
50
+ def __add__(self, other: Usage) -> Usage:
51
+ """Add two Usages together.
52
+
53
+ This is provided so it's trivial to sum usage information from multiple requests and runs.
54
+ """
55
+ new_usage = copy(self)
56
+ new_usage.incr(other)
57
+ return new_usage
85
58
 
86
59
 
87
60
  @dataclass
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "pydantic-ai-slim"
7
- version = "0.0.16"
7
+ version = "0.0.17"
8
8
  description = "Agent Framework / shim to use Pydantic with LLMs, slim package"
9
9
  authors = [
10
10
  { name = "Samuel Colvin", email = "samuel@pydantic.dev" },