pydantic-ai-slim 1.2.1__py3-none-any.whl → 1.10.0__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.
Files changed (67) hide show
  1. pydantic_ai/__init__.py +6 -0
  2. pydantic_ai/_agent_graph.py +67 -20
  3. pydantic_ai/_cli.py +2 -2
  4. pydantic_ai/_output.py +20 -12
  5. pydantic_ai/_run_context.py +6 -2
  6. pydantic_ai/_utils.py +26 -8
  7. pydantic_ai/ag_ui.py +50 -696
  8. pydantic_ai/agent/__init__.py +13 -25
  9. pydantic_ai/agent/abstract.py +146 -9
  10. pydantic_ai/builtin_tools.py +106 -4
  11. pydantic_ai/direct.py +16 -4
  12. pydantic_ai/durable_exec/dbos/_agent.py +3 -0
  13. pydantic_ai/durable_exec/prefect/_agent.py +3 -0
  14. pydantic_ai/durable_exec/temporal/__init__.py +11 -0
  15. pydantic_ai/durable_exec/temporal/_agent.py +3 -0
  16. pydantic_ai/durable_exec/temporal/_function_toolset.py +23 -72
  17. pydantic_ai/durable_exec/temporal/_mcp_server.py +30 -30
  18. pydantic_ai/durable_exec/temporal/_run_context.py +7 -2
  19. pydantic_ai/durable_exec/temporal/_toolset.py +67 -3
  20. pydantic_ai/exceptions.py +6 -1
  21. pydantic_ai/mcp.py +1 -22
  22. pydantic_ai/messages.py +46 -8
  23. pydantic_ai/models/__init__.py +87 -38
  24. pydantic_ai/models/anthropic.py +132 -11
  25. pydantic_ai/models/bedrock.py +4 -4
  26. pydantic_ai/models/cohere.py +0 -7
  27. pydantic_ai/models/gemini.py +9 -2
  28. pydantic_ai/models/google.py +26 -23
  29. pydantic_ai/models/groq.py +13 -5
  30. pydantic_ai/models/huggingface.py +2 -2
  31. pydantic_ai/models/openai.py +251 -52
  32. pydantic_ai/models/outlines.py +563 -0
  33. pydantic_ai/models/test.py +6 -3
  34. pydantic_ai/profiles/openai.py +7 -0
  35. pydantic_ai/providers/__init__.py +25 -12
  36. pydantic_ai/providers/anthropic.py +2 -2
  37. pydantic_ai/providers/bedrock.py +60 -16
  38. pydantic_ai/providers/gateway.py +60 -72
  39. pydantic_ai/providers/google.py +91 -24
  40. pydantic_ai/providers/openrouter.py +3 -0
  41. pydantic_ai/providers/outlines.py +40 -0
  42. pydantic_ai/providers/ovhcloud.py +95 -0
  43. pydantic_ai/result.py +173 -8
  44. pydantic_ai/run.py +40 -24
  45. pydantic_ai/settings.py +8 -0
  46. pydantic_ai/tools.py +10 -6
  47. pydantic_ai/toolsets/fastmcp.py +215 -0
  48. pydantic_ai/ui/__init__.py +16 -0
  49. pydantic_ai/ui/_adapter.py +386 -0
  50. pydantic_ai/ui/_event_stream.py +591 -0
  51. pydantic_ai/ui/_messages_builder.py +28 -0
  52. pydantic_ai/ui/ag_ui/__init__.py +9 -0
  53. pydantic_ai/ui/ag_ui/_adapter.py +187 -0
  54. pydantic_ai/ui/ag_ui/_event_stream.py +236 -0
  55. pydantic_ai/ui/ag_ui/app.py +148 -0
  56. pydantic_ai/ui/vercel_ai/__init__.py +16 -0
  57. pydantic_ai/ui/vercel_ai/_adapter.py +199 -0
  58. pydantic_ai/ui/vercel_ai/_event_stream.py +187 -0
  59. pydantic_ai/ui/vercel_ai/_utils.py +16 -0
  60. pydantic_ai/ui/vercel_ai/request_types.py +275 -0
  61. pydantic_ai/ui/vercel_ai/response_types.py +230 -0
  62. pydantic_ai/usage.py +13 -2
  63. {pydantic_ai_slim-1.2.1.dist-info → pydantic_ai_slim-1.10.0.dist-info}/METADATA +23 -5
  64. {pydantic_ai_slim-1.2.1.dist-info → pydantic_ai_slim-1.10.0.dist-info}/RECORD +67 -49
  65. {pydantic_ai_slim-1.2.1.dist-info → pydantic_ai_slim-1.10.0.dist-info}/WHEEL +0 -0
  66. {pydantic_ai_slim-1.2.1.dist-info → pydantic_ai_slim-1.10.0.dist-info}/entry_points.txt +0 -0
  67. {pydantic_ai_slim-1.2.1.dist-info → pydantic_ai_slim-1.10.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,56 +1,22 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from collections.abc import Callable
4
- from dataclasses import dataclass
5
- from typing import Annotated, Any, Literal, assert_never
4
+ from typing import Any, Literal
6
5
 
7
- from pydantic import ConfigDict, Discriminator, with_config
8
6
  from temporalio import activity, workflow
9
7
  from temporalio.workflow import ActivityConfig
10
8
 
11
9
  from pydantic_ai import FunctionToolset, ToolsetTool
12
- from pydantic_ai.exceptions import ApprovalRequired, CallDeferred, ModelRetry, UserError
10
+ from pydantic_ai.exceptions import UserError
13
11
  from pydantic_ai.tools import AgentDepsT, RunContext
14
12
  from pydantic_ai.toolsets.function import FunctionToolsetTool
15
13
 
16
14
  from ._run_context import TemporalRunContext
17
- from ._toolset import TemporalWrapperToolset
18
-
19
-
20
- @dataclass
21
- @with_config(ConfigDict(arbitrary_types_allowed=True))
22
- class _CallToolParams:
23
- name: str
24
- tool_args: dict[str, Any]
25
- serialized_run_context: Any
26
-
27
-
28
- @dataclass
29
- class _ApprovalRequired:
30
- kind: Literal['approval_required'] = 'approval_required'
31
-
32
-
33
- @dataclass
34
- class _CallDeferred:
35
- kind: Literal['call_deferred'] = 'call_deferred'
36
-
37
-
38
- @dataclass
39
- class _ModelRetry:
40
- message: str
41
- kind: Literal['model_retry'] = 'model_retry'
42
-
43
-
44
- @dataclass
45
- class _ToolReturn:
46
- result: Any
47
- kind: Literal['tool_return'] = 'tool_return'
48
-
49
-
50
- _CallToolResult = Annotated[
51
- _ApprovalRequired | _CallDeferred | _ModelRetry | _ToolReturn,
52
- Discriminator('kind'),
53
- ]
15
+ from ._toolset import (
16
+ CallToolParams,
17
+ CallToolResult,
18
+ TemporalWrapperToolset,
19
+ )
54
20
 
55
21
 
56
22
  class TemporalFunctionToolset(TemporalWrapperToolset[AgentDepsT]):
@@ -69,7 +35,7 @@ class TemporalFunctionToolset(TemporalWrapperToolset[AgentDepsT]):
69
35
  self.tool_activity_config = tool_activity_config
70
36
  self.run_context_type = run_context_type
71
37
 
72
- async def call_tool_activity(params: _CallToolParams, deps: AgentDepsT) -> _CallToolResult:
38
+ async def call_tool_activity(params: CallToolParams, deps: AgentDepsT) -> CallToolResult:
73
39
  name = params.name
74
40
  ctx = self.run_context_type.deserialize_run_context(params.serialized_run_context, deps=deps)
75
41
  try:
@@ -83,15 +49,7 @@ class TemporalFunctionToolset(TemporalWrapperToolset[AgentDepsT]):
83
49
  # The tool args will already have been validated into their proper types in the `ToolManager`,
84
50
  # but `execute_activity` would have turned them into simple Python types again, so we need to re-validate them.
85
51
  args_dict = tool.args_validator.validate_python(params.tool_args)
86
- try:
87
- result = await self.wrapped.call_tool(name, args_dict, ctx, tool)
88
- return _ToolReturn(result=result)
89
- except ApprovalRequired:
90
- return _ApprovalRequired()
91
- except CallDeferred:
92
- return _CallDeferred()
93
- except ModelRetry as e:
94
- return _ModelRetry(message=e.message)
52
+ return await self._wrap_call_tool_result(self.wrapped.call_tool(name, args_dict, ctx, tool))
95
53
 
96
54
  # Set type hint explicitly so that Temporal can take care of serialization and deserialization
97
55
  call_tool_activity.__annotations__['deps'] = deps_type
@@ -122,25 +80,18 @@ class TemporalFunctionToolset(TemporalWrapperToolset[AgentDepsT]):
122
80
 
123
81
  tool_activity_config = self.activity_config | tool_activity_config
124
82
  serialized_run_context = self.run_context_type.serialize_run_context(ctx)
125
- result = await workflow.execute_activity( # pyright: ignore[reportUnknownMemberType]
126
- activity=self.call_tool_activity,
127
- args=[
128
- _CallToolParams(
129
- name=name,
130
- tool_args=tool_args,
131
- serialized_run_context=serialized_run_context,
132
- ),
133
- ctx.deps,
134
- ],
135
- **tool_activity_config,
83
+ return self._unwrap_call_tool_result(
84
+ await workflow.execute_activity( # pyright: ignore[reportUnknownMemberType]
85
+ activity=self.call_tool_activity,
86
+ args=[
87
+ CallToolParams(
88
+ name=name,
89
+ tool_args=tool_args,
90
+ serialized_run_context=serialized_run_context,
91
+ tool_def=None,
92
+ ),
93
+ ctx.deps,
94
+ ],
95
+ **tool_activity_config,
96
+ )
136
97
  )
137
- if isinstance(result, _ApprovalRequired):
138
- raise ApprovalRequired()
139
- elif isinstance(result, _CallDeferred):
140
- raise CallDeferred()
141
- elif isinstance(result, _ModelRetry):
142
- raise ModelRetry(result.message)
143
- elif isinstance(result, _ToolReturn):
144
- return result.result
145
- else:
146
- assert_never(result)
@@ -11,11 +11,15 @@ from typing_extensions import Self
11
11
 
12
12
  from pydantic_ai import ToolsetTool
13
13
  from pydantic_ai.exceptions import UserError
14
- from pydantic_ai.mcp import MCPServer, ToolResult
14
+ from pydantic_ai.mcp import MCPServer
15
15
  from pydantic_ai.tools import AgentDepsT, RunContext, ToolDefinition
16
16
 
17
17
  from ._run_context import TemporalRunContext
18
- from ._toolset import TemporalWrapperToolset
18
+ from ._toolset import (
19
+ CallToolParams,
20
+ CallToolResult,
21
+ TemporalWrapperToolset,
22
+ )
19
23
 
20
24
 
21
25
  @dataclass
@@ -24,15 +28,6 @@ class _GetToolsParams:
24
28
  serialized_run_context: Any
25
29
 
26
30
 
27
- @dataclass
28
- @with_config(ConfigDict(arbitrary_types_allowed=True))
29
- class _CallToolParams:
30
- name: str
31
- tool_args: dict[str, Any]
32
- serialized_run_context: Any
33
- tool_def: ToolDefinition
34
-
35
-
36
31
  class TemporalMCPServer(TemporalWrapperToolset[AgentDepsT]):
37
32
  def __init__(
38
33
  self,
@@ -72,13 +67,16 @@ class TemporalMCPServer(TemporalWrapperToolset[AgentDepsT]):
72
67
  get_tools_activity
73
68
  )
74
69
 
75
- async def call_tool_activity(params: _CallToolParams, deps: AgentDepsT) -> ToolResult:
70
+ async def call_tool_activity(params: CallToolParams, deps: AgentDepsT) -> CallToolResult:
76
71
  run_context = self.run_context_type.deserialize_run_context(params.serialized_run_context, deps=deps)
77
- return await self.wrapped.call_tool(
78
- params.name,
79
- params.tool_args,
80
- run_context,
81
- self.tool_for_tool_def(params.tool_def),
72
+ assert isinstance(params.tool_def, ToolDefinition)
73
+ return await self._wrap_call_tool_result(
74
+ self.wrapped.call_tool(
75
+ params.name,
76
+ params.tool_args,
77
+ run_context,
78
+ self.tool_for_tool_def(params.tool_def),
79
+ )
82
80
  )
83
81
 
84
82
  # Set type hint explicitly so that Temporal can take care of serialization and deserialization
@@ -125,22 +123,24 @@ class TemporalMCPServer(TemporalWrapperToolset[AgentDepsT]):
125
123
  tool_args: dict[str, Any],
126
124
  ctx: RunContext[AgentDepsT],
127
125
  tool: ToolsetTool[AgentDepsT],
128
- ) -> ToolResult:
126
+ ) -> CallToolResult:
129
127
  if not workflow.in_workflow():
130
128
  return await super().call_tool(name, tool_args, ctx, tool)
131
129
 
132
130
  tool_activity_config = self.activity_config | self.tool_activity_config.get(name, {})
133
131
  serialized_run_context = self.run_context_type.serialize_run_context(ctx)
134
- return await workflow.execute_activity( # pyright: ignore[reportUnknownMemberType]
135
- activity=self.call_tool_activity,
136
- args=[
137
- _CallToolParams(
138
- name=name,
139
- tool_args=tool_args,
140
- serialized_run_context=serialized_run_context,
141
- tool_def=tool.tool_def,
142
- ),
143
- ctx.deps,
144
- ],
145
- **tool_activity_config,
132
+ return self._unwrap_call_tool_result(
133
+ await workflow.execute_activity( # pyright: ignore[reportUnknownMemberType]
134
+ activity=self.call_tool_activity,
135
+ args=[
136
+ CallToolParams(
137
+ name=name,
138
+ tool_args=tool_args,
139
+ serialized_run_context=serialized_run_context,
140
+ tool_def=tool.tool_def,
141
+ ),
142
+ ctx.deps,
143
+ ],
144
+ **tool_activity_config,
145
+ )
146
146
  )
@@ -2,8 +2,13 @@ from __future__ import annotations
2
2
 
3
3
  from typing import Any
4
4
 
5
+ from typing_extensions import TypeVar
6
+
5
7
  from pydantic_ai.exceptions import UserError
6
- from pydantic_ai.tools import AgentDepsT, RunContext
8
+ from pydantic_ai.tools import RunContext
9
+
10
+ AgentDepsT = TypeVar('AgentDepsT', default=None, covariant=True)
11
+ """Type variable for the agent dependencies in `RunContext`."""
7
12
 
8
13
 
9
14
  class TemporalRunContext(RunContext[AgentDepsT]):
@@ -47,6 +52,6 @@ class TemporalRunContext(RunContext[AgentDepsT]):
47
52
  }
48
53
 
49
54
  @classmethod
50
- def deserialize_run_context(cls, ctx: dict[str, Any], deps: AgentDepsT) -> TemporalRunContext[AgentDepsT]:
55
+ def deserialize_run_context(cls, ctx: dict[str, Any], deps: Any) -> TemporalRunContext[Any]:
51
56
  """Deserialize the run context from a `dict[str, Any]`."""
52
57
  return cls(**ctx, deps=deps)
@@ -1,17 +1,58 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from abc import ABC, abstractmethod
4
- from collections.abc import Callable
5
- from typing import Any, Literal
4
+ from collections.abc import Awaitable, Callable
5
+ from dataclasses import dataclass
6
+ from typing import Annotated, Any, Literal
6
7
 
8
+ from pydantic import ConfigDict, Discriminator, with_config
7
9
  from temporalio.workflow import ActivityConfig
10
+ from typing_extensions import assert_never
8
11
 
9
12
  from pydantic_ai import AbstractToolset, FunctionToolset, WrapperToolset
10
- from pydantic_ai.tools import AgentDepsT
13
+ from pydantic_ai.exceptions import ApprovalRequired, CallDeferred, ModelRetry
14
+ from pydantic_ai.tools import AgentDepsT, ToolDefinition
11
15
 
12
16
  from ._run_context import TemporalRunContext
13
17
 
14
18
 
19
+ @dataclass
20
+ @with_config(ConfigDict(arbitrary_types_allowed=True))
21
+ class CallToolParams:
22
+ name: str
23
+ tool_args: dict[str, Any]
24
+ serialized_run_context: Any
25
+ tool_def: ToolDefinition | None
26
+
27
+
28
+ @dataclass
29
+ class _ApprovalRequired:
30
+ kind: Literal['approval_required'] = 'approval_required'
31
+
32
+
33
+ @dataclass
34
+ class _CallDeferred:
35
+ kind: Literal['call_deferred'] = 'call_deferred'
36
+
37
+
38
+ @dataclass
39
+ class _ModelRetry:
40
+ message: str
41
+ kind: Literal['model_retry'] = 'model_retry'
42
+
43
+
44
+ @dataclass
45
+ class _ToolReturn:
46
+ result: Any
47
+ kind: Literal['tool_return'] = 'tool_return'
48
+
49
+
50
+ CallToolResult = Annotated[
51
+ _ApprovalRequired | _CallDeferred | _ModelRetry | _ToolReturn,
52
+ Discriminator('kind'),
53
+ ]
54
+
55
+
15
56
  class TemporalWrapperToolset(WrapperToolset[AgentDepsT], ABC):
16
57
  @property
17
58
  def id(self) -> str:
@@ -30,6 +71,29 @@ class TemporalWrapperToolset(WrapperToolset[AgentDepsT], ABC):
30
71
  # Temporalized toolsets cannot be swapped out after the fact.
31
72
  return self
32
73
 
74
+ async def _wrap_call_tool_result(self, coro: Awaitable[Any]) -> CallToolResult:
75
+ try:
76
+ result = await coro
77
+ return _ToolReturn(result=result)
78
+ except ApprovalRequired:
79
+ return _ApprovalRequired()
80
+ except CallDeferred:
81
+ return _CallDeferred()
82
+ except ModelRetry as e:
83
+ return _ModelRetry(message=e.message)
84
+
85
+ def _unwrap_call_tool_result(self, result: CallToolResult) -> Any:
86
+ if isinstance(result, _ToolReturn):
87
+ return result.result
88
+ elif isinstance(result, _ApprovalRequired):
89
+ raise ApprovalRequired()
90
+ elif isinstance(result, _CallDeferred):
91
+ raise CallDeferred()
92
+ elif isinstance(result, _ModelRetry):
93
+ raise ModelRetry(result.message)
94
+ else:
95
+ assert_never(result)
96
+
33
97
 
34
98
  def temporalize_toolset(
35
99
  toolset: AbstractToolset[AgentDepsT],
pydantic_ai/exceptions.py CHANGED
@@ -23,6 +23,7 @@ __all__ = (
23
23
  'UnexpectedModelBehavior',
24
24
  'UsageLimitExceeded',
25
25
  'ModelHTTPError',
26
+ 'IncompleteToolCall',
26
27
  'FallbackExceptionGroup',
27
28
  )
28
29
 
@@ -158,7 +159,7 @@ class ModelHTTPError(AgentRunError):
158
159
  super().__init__(message)
159
160
 
160
161
 
161
- class FallbackExceptionGroup(ExceptionGroup):
162
+ class FallbackExceptionGroup(ExceptionGroup[Any]):
162
163
  """A group of exceptions that can be raised when all fallback models fail."""
163
164
 
164
165
 
@@ -168,3 +169,7 @@ class ToolRetryError(Exception):
168
169
  def __init__(self, tool_retry: RetryPromptPart):
169
170
  self.tool_retry = tool_retry
170
171
  super().__init__()
172
+
173
+
174
+ class IncompleteToolCall(UnexpectedModelBehavior):
175
+ """Error raised when a model stops due to token limit while emitting a tool call."""
pydantic_ai/mcp.py CHANGED
@@ -441,14 +441,9 @@ class MCPServerStdio(MCPServer):
441
441
  'uv', args=['run', 'mcp-run-python', 'stdio'], timeout=10
442
442
  )
443
443
  agent = Agent('openai:gpt-4o', toolsets=[server])
444
-
445
- async def main():
446
- async with agent: # (2)!
447
- ...
448
444
  ```
449
445
 
450
446
  1. See [MCP Run Python](https://github.com/pydantic/mcp-run-python) for more information.
451
- 2. This will start the server as a subprocess and connect to it.
452
447
  """
453
448
 
454
449
  command: str
@@ -788,13 +783,7 @@ class MCPServerSSE(_MCPServerHTTP):
788
783
 
789
784
  server = MCPServerSSE('http://localhost:3001/sse')
790
785
  agent = Agent('openai:gpt-4o', toolsets=[server])
791
-
792
- async def main():
793
- async with agent: # (1)!
794
- ...
795
786
  ```
796
-
797
- 1. This will connect to a server running on `localhost:3001`.
798
787
  """
799
788
 
800
789
  @classmethod
@@ -837,13 +826,7 @@ class MCPServerHTTP(MCPServerSSE):
837
826
 
838
827
  server = MCPServerHTTP('http://localhost:3001/sse')
839
828
  agent = Agent('openai:gpt-4o', toolsets=[server])
840
-
841
- async def main():
842
- async with agent: # (2)!
843
- ...
844
829
  ```
845
-
846
- 1. This will connect to a server running on `localhost:3001`.
847
830
  """
848
831
 
849
832
 
@@ -862,12 +845,8 @@ class MCPServerStreamableHTTP(_MCPServerHTTP):
862
845
  from pydantic_ai import Agent
863
846
  from pydantic_ai.mcp import MCPServerStreamableHTTP
864
847
 
865
- server = MCPServerStreamableHTTP('http://localhost:8000/mcp') # (1)!
848
+ server = MCPServerStreamableHTTP('http://localhost:8000/mcp')
866
849
  agent = Agent('openai:gpt-4o', toolsets=[server])
867
-
868
- async def main():
869
- async with agent: # (2)!
870
- ...
871
850
  ```
872
851
  """
873
852
 
pydantic_ai/messages.py CHANGED
@@ -13,7 +13,7 @@ import pydantic
13
13
  import pydantic_core
14
14
  from genai_prices import calc_price, types as genai_types
15
15
  from opentelemetry._events import Event # pyright: ignore[reportPrivateImportUsage]
16
- from typing_extensions import Self, deprecated
16
+ from typing_extensions import deprecated
17
17
 
18
18
  from . import _otel_messages, _utils
19
19
  from ._utils import generate_tool_call_id as _generate_tool_call_id, now_utc as _now_utc
@@ -34,6 +34,7 @@ DocumentMediaType: TypeAlias = Literal[
34
34
  'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
35
35
  'text/html',
36
36
  'text/markdown',
37
+ 'application/msword',
37
38
  'application/vnd.ms-excel',
38
39
  ]
39
40
  VideoMediaType: TypeAlias = Literal[
@@ -434,8 +435,12 @@ class DocumentUrl(FileUrl):
434
435
  return 'application/pdf'
435
436
  elif self.url.endswith('.rtf'):
436
437
  return 'application/rtf'
438
+ elif self.url.endswith('.doc'):
439
+ return 'application/msword'
437
440
  elif self.url.endswith('.docx'):
438
441
  return 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
442
+ elif self.url.endswith('.xls'):
443
+ return 'application/vnd.ms-excel'
439
444
  elif self.url.endswith('.xlsx'):
440
445
  return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
441
446
 
@@ -480,7 +485,7 @@ class BinaryContent:
480
485
  """
481
486
 
482
487
  _identifier: Annotated[str | None, pydantic.Field(alias='identifier', default=None, exclude=True)] = field(
483
- compare=False, default=None
488
+ compare=False, default=None, repr=False
484
489
  )
485
490
 
486
491
  kind: Literal['binary'] = 'binary'
@@ -514,16 +519,16 @@ class BinaryContent:
514
519
  vendor_metadata=bc.vendor_metadata,
515
520
  )
516
521
  else:
517
- return bc # pragma: no cover
522
+ return bc
518
523
 
519
524
  @classmethod
520
- def from_data_uri(cls, data_uri: str) -> Self:
525
+ def from_data_uri(cls, data_uri: str) -> BinaryContent:
521
526
  """Create a `BinaryContent` from a data URI."""
522
527
  prefix = 'data:'
523
528
  if not data_uri.startswith(prefix):
524
- raise ValueError('Data URI must start with "data:"') # pragma: no cover
529
+ raise ValueError('Data URI must start with "data:"')
525
530
  media_type, data = data_uri[len(prefix) :].split(';base64,', 1)
526
- return cls(data=base64.b64decode(data), media_type=media_type)
531
+ return cls.narrow_type(cls(data=base64.b64decode(data), media_type=media_type))
527
532
 
528
533
  @pydantic.computed_field
529
534
  @property
@@ -645,6 +650,7 @@ _document_format_lookup: dict[str, DocumentFormat] = {
645
650
  'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'xlsx',
646
651
  'text/html': 'html',
647
652
  'text/markdown': 'md',
653
+ 'application/msword': 'doc',
648
654
  'application/vnd.ms-excel': 'xls',
649
655
  }
650
656
  _audio_format_lookup: dict[str, AudioFormat] = {
@@ -1612,6 +1618,14 @@ class PartStartEvent:
1612
1618
  part: ModelResponsePart
1613
1619
  """The newly started `ModelResponsePart`."""
1614
1620
 
1621
+ previous_part_kind: (
1622
+ Literal['text', 'thinking', 'tool-call', 'builtin-tool-call', 'builtin-tool-return', 'file'] | None
1623
+ ) = None
1624
+ """The kind of the previous part, if any.
1625
+
1626
+ This is useful for UI event streams to know whether to group parts of the same kind together when emitting events.
1627
+ """
1628
+
1615
1629
  event_kind: Literal['part_start'] = 'part_start'
1616
1630
  """Event type identifier, used as a discriminator."""
1617
1631
 
@@ -1634,6 +1648,30 @@ class PartDeltaEvent:
1634
1648
  __repr__ = _utils.dataclasses_no_defaults_repr
1635
1649
 
1636
1650
 
1651
+ @dataclass(repr=False, kw_only=True)
1652
+ class PartEndEvent:
1653
+ """An event indicating that a part is complete."""
1654
+
1655
+ index: int
1656
+ """The index of the part within the overall response parts list."""
1657
+
1658
+ part: ModelResponsePart
1659
+ """The complete `ModelResponsePart`."""
1660
+
1661
+ next_part_kind: (
1662
+ Literal['text', 'thinking', 'tool-call', 'builtin-tool-call', 'builtin-tool-return', 'file'] | None
1663
+ ) = None
1664
+ """The kind of the next part, if any.
1665
+
1666
+ This is useful for UI event streams to know whether to group parts of the same kind together when emitting events.
1667
+ """
1668
+
1669
+ event_kind: Literal['part_end'] = 'part_end'
1670
+ """Event type identifier, used as a discriminator."""
1671
+
1672
+ __repr__ = _utils.dataclasses_no_defaults_repr
1673
+
1674
+
1637
1675
  @dataclass(repr=False, kw_only=True)
1638
1676
  class FinalResultEvent:
1639
1677
  """An event indicating the response to the current model request matches the output schema and will produce a result."""
@@ -1649,9 +1687,9 @@ class FinalResultEvent:
1649
1687
 
1650
1688
 
1651
1689
  ModelResponseStreamEvent = Annotated[
1652
- PartStartEvent | PartDeltaEvent | FinalResultEvent, pydantic.Discriminator('event_kind')
1690
+ PartStartEvent | PartDeltaEvent | PartEndEvent | FinalResultEvent, pydantic.Discriminator('event_kind')
1653
1691
  ]
1654
- """An event in the model response stream, starting a new part, applying a delta to an existing one, or indicating the final result."""
1692
+ """An event in the model response stream, starting a new part, applying a delta to an existing one, indicating a part is complete, or indicating the final result."""
1655
1693
 
1656
1694
 
1657
1695
  @dataclass(repr=False)