pydantic-ai-slim 0.4.0__py3-none-any.whl → 0.4.1__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/_a2a.py CHANGED
@@ -1,11 +1,13 @@
1
1
  from __future__ import annotations, annotations as _annotations
2
2
 
3
+ import uuid
3
4
  from collections.abc import AsyncIterator, Sequence
4
5
  from contextlib import asynccontextmanager
5
6
  from dataclasses import dataclass
6
7
  from functools import partial
7
- from typing import Any, Generic
8
+ from typing import Any, Generic, TypeVar
8
9
 
10
+ from pydantic import TypeAdapter
9
11
  from typing_extensions import assert_never
10
12
 
11
13
  from pydantic_ai.messages import (
@@ -19,12 +21,17 @@ from pydantic_ai.messages import (
19
21
  ModelResponse,
20
22
  ModelResponsePart,
21
23
  TextPart,
24
+ ThinkingPart,
25
+ ToolCallPart,
22
26
  UserPromptPart,
23
27
  VideoUrl,
24
28
  )
25
29
 
26
30
  from .agent import Agent, AgentDepsT, OutputDataT
27
31
 
32
+ # AgentWorker output type needs to be invariant for use in both parameter and return positions
33
+ WorkerOutputT = TypeVar('WorkerOutputT')
34
+
28
35
  try:
29
36
  from starlette.middleware import Middleware
30
37
  from starlette.routing import Route
@@ -33,10 +40,11 @@ try:
33
40
  from fasta2a.applications import FastA2A
34
41
  from fasta2a.broker import Broker, InMemoryBroker
35
42
  from fasta2a.schema import (
43
+ AgentProvider,
36
44
  Artifact,
45
+ DataPart,
37
46
  Message,
38
47
  Part,
39
- Provider,
40
48
  Skill,
41
49
  TaskIdParams,
42
50
  TaskSendParams,
@@ -72,7 +80,7 @@ def agent_to_a2a(
72
80
  url: str = 'http://localhost:8000',
73
81
  version: str = '1.0.0',
74
82
  description: str | None = None,
75
- provider: Provider | None = None,
83
+ provider: AgentProvider | None = None,
76
84
  skills: list[Skill] | None = None,
77
85
  # Starlette
78
86
  debug: bool = False,
@@ -106,59 +114,121 @@ def agent_to_a2a(
106
114
 
107
115
 
108
116
  @dataclass
109
- class AgentWorker(Worker, Generic[AgentDepsT, OutputDataT]):
117
+ class AgentWorker(Worker[list[ModelMessage]], Generic[WorkerOutputT, AgentDepsT]):
110
118
  """A worker that uses an agent to execute tasks."""
111
119
 
112
- agent: Agent[AgentDepsT, OutputDataT]
120
+ agent: Agent[AgentDepsT, WorkerOutputT]
113
121
 
114
122
  async def run_task(self, params: TaskSendParams) -> None:
115
- task = await self.storage.load_task(params['id'], history_length=params.get('history_length'))
116
- assert task is not None, f'Task {params["id"]} not found'
117
- assert 'session_id' in task, 'Task must have a session_id'
123
+ task = await self.storage.load_task(params['id'])
124
+ if task is None:
125
+ raise ValueError(f'Task {params["id"]} not found') # pragma: no cover
126
+
127
+ # TODO(Marcelo): Should we lock `run_task` on the `context_id`?
128
+ # Ensure this task hasn't been run before
129
+ if task['status']['state'] != 'submitted':
130
+ raise ValueError( # pragma: no cover
131
+ f'Task {params["id"]} has already been processed (state: {task["status"]["state"]})'
132
+ )
118
133
 
119
134
  await self.storage.update_task(task['id'], state='working')
120
135
 
121
- # TODO(Marcelo): We need to have a way to communicate when the task is set to `input-required`. Maybe
122
- # a custom `output_type` with a `more_info_required` field, or something like that.
136
+ # Load context - contains pydantic-ai message history from previous tasks in this conversation
137
+ message_history = await self.storage.load_context(task['context_id']) or []
138
+ message_history.extend(self.build_message_history(task.get('history', [])))
139
+
140
+ try:
141
+ result = await self.agent.run(message_history=message_history) # type: ignore
123
142
 
124
- task_history = task.get('history', [])
125
- message_history = self.build_message_history(task_history=task_history)
143
+ await self.storage.update_context(task['context_id'], result.all_messages())
126
144
 
127
- # TODO(Marcelo): We need to make this more customizable e.g. pass deps.
128
- result = await self.agent.run(message_history=message_history) # type: ignore
145
+ # Convert new messages to A2A format for task history
146
+ a2a_messages: list[Message] = []
147
+
148
+ for message in result.new_messages():
149
+ if isinstance(message, ModelRequest):
150
+ # Skip user prompts - they're already in task history
151
+ continue
152
+ else:
153
+ # Convert response parts to A2A format
154
+ a2a_parts = self._response_parts_to_a2a(message.parts)
155
+ if a2a_parts: # Add if there are visible parts (text/thinking)
156
+ a2a_messages.append(
157
+ Message(role='agent', parts=a2a_parts, kind='message', message_id=str(uuid.uuid4()))
158
+ )
129
159
 
130
- artifacts = self.build_artifacts(result.output)
131
- await self.storage.update_task(task['id'], state='completed', artifacts=artifacts)
160
+ artifacts = self.build_artifacts(result.output)
161
+ except Exception:
162
+ await self.storage.update_task(task['id'], state='failed')
163
+ raise
164
+ else:
165
+ await self.storage.update_task(
166
+ task['id'], state='completed', new_artifacts=artifacts, new_messages=a2a_messages
167
+ )
132
168
 
133
169
  async def cancel_task(self, params: TaskIdParams) -> None:
134
170
  pass
135
171
 
136
- def build_artifacts(self, result: Any) -> list[Artifact]:
137
- # TODO(Marcelo): We need to send the json schema of the result on the metadata of the message.
138
- return [Artifact(name='result', index=0, parts=[A2ATextPart(type='text', text=str(result))])]
172
+ def build_artifacts(self, result: WorkerOutputT) -> list[Artifact]:
173
+ """Build artifacts from agent result.
139
174
 
140
- def build_message_history(self, task_history: list[Message]) -> list[ModelMessage]:
175
+ All agent outputs become artifacts to mark them as durable task outputs.
176
+ For string results, we use TextPart. For structured data, we use DataPart.
177
+ Metadata is included to preserve type information.
178
+ """
179
+ artifact_id = str(uuid.uuid4())
180
+ part = self._convert_result_to_part(result)
181
+ return [Artifact(artifact_id=artifact_id, name='result', parts=[part])]
182
+
183
+ def _convert_result_to_part(self, result: WorkerOutputT) -> Part:
184
+ """Convert agent result to a Part (TextPart or DataPart).
185
+
186
+ For string results, returns a TextPart.
187
+ For structured data, returns a DataPart with properly serialized data.
188
+ """
189
+ if isinstance(result, str):
190
+ return A2ATextPart(kind='text', text=result)
191
+ else:
192
+ output_type = type(result)
193
+ type_adapter = TypeAdapter(output_type)
194
+ data = type_adapter.dump_python(result, mode='json')
195
+ json_schema = type_adapter.json_schema(mode='serialization')
196
+ return DataPart(kind='data', data={'result': data}, metadata={'json_schema': json_schema})
197
+
198
+ def build_message_history(self, history: list[Message]) -> list[ModelMessage]:
141
199
  model_messages: list[ModelMessage] = []
142
- for message in task_history:
200
+ for message in history:
143
201
  if message['role'] == 'user':
144
- model_messages.append(ModelRequest(parts=self._map_request_parts(message['parts'])))
202
+ model_messages.append(ModelRequest(parts=self._request_parts_from_a2a(message['parts'])))
145
203
  else:
146
- model_messages.append(ModelResponse(parts=self._map_response_parts(message['parts'])))
204
+ model_messages.append(ModelResponse(parts=self._response_parts_from_a2a(message['parts'])))
147
205
  return model_messages
148
206
 
149
- def _map_request_parts(self, parts: list[Part]) -> list[ModelRequestPart]:
207
+ def _request_parts_from_a2a(self, parts: list[Part]) -> list[ModelRequestPart]:
208
+ """Convert A2A Part objects to pydantic-ai ModelRequestPart objects.
209
+
210
+ This handles the conversion from A2A protocol parts (text, file, data) to
211
+ pydantic-ai's internal request parts (UserPromptPart with various content types).
212
+
213
+ Args:
214
+ parts: List of A2A Part objects from incoming messages
215
+
216
+ Returns:
217
+ List of ModelRequestPart objects for the pydantic-ai agent
218
+ """
150
219
  model_parts: list[ModelRequestPart] = []
151
220
  for part in parts:
152
- if part['type'] == 'text':
221
+ if part['kind'] == 'text':
153
222
  model_parts.append(UserPromptPart(content=part['text']))
154
- elif part['type'] == 'file':
155
- file = part['file']
156
- if 'data' in file:
157
- data = file['data'].encode('utf-8')
158
- content = BinaryContent(data=data, media_type=file['mime_type'])
223
+ elif part['kind'] == 'file':
224
+ file_content = part['file']
225
+ if 'bytes' in file_content:
226
+ data = file_content['bytes'].encode('utf-8')
227
+ mime_type = file_content.get('mime_type', 'application/octet-stream')
228
+ content = BinaryContent(data=data, media_type=mime_type)
159
229
  model_parts.append(UserPromptPart(content=[content]))
160
230
  else:
161
- url = file['url']
231
+ url = file_content['uri']
162
232
  for url_cls in (DocumentUrl, AudioUrl, ImageUrl, VideoUrl):
163
233
  content = url_cls(url=url)
164
234
  try:
@@ -168,24 +238,68 @@ class AgentWorker(Worker, Generic[AgentDepsT, OutputDataT]):
168
238
  else:
169
239
  break
170
240
  else:
171
- raise ValueError(f'Unknown file type: {file["mime_type"]}') # pragma: no cover
241
+ raise ValueError(f'Unsupported file type: {url}') # pragma: no cover
172
242
  model_parts.append(UserPromptPart(content=[content]))
173
- elif part['type'] == 'data':
174
- # TODO(Marcelo): Maybe we should use this for `ToolReturnPart`, and `RetryPromptPart`.
243
+ elif part['kind'] == 'data':
175
244
  raise NotImplementedError('Data parts are not supported yet.')
176
245
  else:
177
246
  assert_never(part)
178
247
  return model_parts
179
248
 
180
- def _map_response_parts(self, parts: list[Part]) -> list[ModelResponsePart]:
249
+ def _response_parts_from_a2a(self, parts: list[Part]) -> list[ModelResponsePart]:
250
+ """Convert A2A Part objects to pydantic-ai ModelResponsePart objects.
251
+
252
+ This handles the conversion from A2A protocol parts (text, file, data) to
253
+ pydantic-ai's internal response parts. Currently only supports text parts
254
+ as agent responses in A2A are expected to be text-based.
255
+
256
+ Args:
257
+ parts: List of A2A Part objects from stored agent messages
258
+
259
+ Returns:
260
+ List of ModelResponsePart objects for message history
261
+ """
181
262
  model_parts: list[ModelResponsePart] = []
182
263
  for part in parts:
183
- if part['type'] == 'text':
264
+ if part['kind'] == 'text':
184
265
  model_parts.append(TextPart(content=part['text']))
185
- elif part['type'] == 'file': # pragma: no cover
266
+ elif part['kind'] == 'file': # pragma: no cover
186
267
  raise NotImplementedError('File parts are not supported yet.')
187
- elif part['type'] == 'data': # pragma: no cover
268
+ elif part['kind'] == 'data': # pragma: no cover
188
269
  raise NotImplementedError('Data parts are not supported yet.')
189
270
  else: # pragma: no cover
190
271
  assert_never(part)
191
272
  return model_parts
273
+
274
+ def _response_parts_to_a2a(self, parts: list[ModelResponsePart]) -> list[Part]:
275
+ """Convert pydantic-ai ModelResponsePart objects to A2A Part objects.
276
+
277
+ This handles the conversion from pydantic-ai's internal response parts to
278
+ A2A protocol parts. Different part types are handled as follows:
279
+ - TextPart: Converted directly to A2A TextPart
280
+ - ThinkingPart: Converted to TextPart with metadata indicating it's thinking
281
+ - ToolCallPart: Skipped (internal to agent execution)
282
+
283
+ Args:
284
+ parts: List of ModelResponsePart objects from agent response
285
+
286
+ Returns:
287
+ List of A2A Part objects suitable for sending via A2A protocol
288
+ """
289
+ a2a_parts: list[Part] = []
290
+ for part in parts:
291
+ if isinstance(part, TextPart):
292
+ a2a_parts.append(A2ATextPart(kind='text', text=part.content))
293
+ elif isinstance(part, ThinkingPart):
294
+ # Convert thinking to text with metadata
295
+ a2a_parts.append(
296
+ A2ATextPart(
297
+ kind='text',
298
+ text=part.content,
299
+ metadata={'type': 'thinking', 'thinking_id': part.id, 'signature': part.signature},
300
+ )
301
+ )
302
+ elif isinstance(part, ToolCallPart):
303
+ # Skip tool calls - they're internal to agent execution
304
+ pass
305
+ return a2a_parts
pydantic_ai/agent.py CHANGED
@@ -63,7 +63,7 @@ if TYPE_CHECKING:
63
63
 
64
64
  from fasta2a.applications import FastA2A
65
65
  from fasta2a.broker import Broker
66
- from fasta2a.schema import Provider, Skill
66
+ from fasta2a.schema import AgentProvider, Skill
67
67
  from fasta2a.storage import Storage
68
68
  from pydantic_ai.mcp import MCPServer
69
69
 
@@ -1764,7 +1764,7 @@ class Agent(Generic[AgentDepsT, OutputDataT]):
1764
1764
  url: str = 'http://localhost:8000',
1765
1765
  version: str = '1.0.0',
1766
1766
  description: str | None = None,
1767
- provider: Provider | None = None,
1767
+ provider: AgentProvider | None = None,
1768
1768
  skills: list[Skill] | None = None,
1769
1769
  # Starlette
1770
1770
  debug: bool = False,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pydantic-ai-slim
3
- Version: 0.4.0
3
+ Version: 0.4.1
4
4
  Summary: Agent Framework / shim to use Pydantic with LLMs, slim package
5
5
  Author-email: Samuel Colvin <samuel@pydantic.dev>, Marcelo Trylesinski <marcelotryle@gmail.com>, David Montague <david@pydantic.dev>, Alex Hall <alex@pydantic.dev>
6
6
  License-Expression: MIT
@@ -30,11 +30,11 @@ Requires-Dist: exceptiongroup; python_version < '3.11'
30
30
  Requires-Dist: griffe>=1.3.2
31
31
  Requires-Dist: httpx>=0.27
32
32
  Requires-Dist: opentelemetry-api>=1.28.0
33
- Requires-Dist: pydantic-graph==0.4.0
33
+ Requires-Dist: pydantic-graph==0.4.1
34
34
  Requires-Dist: pydantic>=2.10
35
35
  Requires-Dist: typing-inspection>=0.4.0
36
36
  Provides-Extra: a2a
37
- Requires-Dist: fasta2a==0.4.0; extra == 'a2a'
37
+ Requires-Dist: fasta2a==0.4.1; extra == 'a2a'
38
38
  Provides-Extra: anthropic
39
39
  Requires-Dist: anthropic>=0.52.0; extra == 'anthropic'
40
40
  Provides-Extra: bedrock
@@ -48,7 +48,7 @@ Requires-Dist: cohere>=5.13.11; (platform_system != 'Emscripten') and extra == '
48
48
  Provides-Extra: duckduckgo
49
49
  Requires-Dist: duckduckgo-search>=7.0.0; extra == 'duckduckgo'
50
50
  Provides-Extra: evals
51
- Requires-Dist: pydantic-evals==0.4.0; extra == 'evals'
51
+ Requires-Dist: pydantic-evals==0.4.1; extra == 'evals'
52
52
  Provides-Extra: google
53
53
  Requires-Dist: google-genai>=1.24.0; extra == 'google'
54
54
  Provides-Extra: groq
@@ -1,6 +1,6 @@
1
1
  pydantic_ai/__init__.py,sha256=Ns04g4Efqkzwccs8w2nGphfWbptMlIJYG8vIJbGGyG0,1262
2
2
  pydantic_ai/__main__.py,sha256=Q_zJU15DUA01YtlJ2mnaLCoId2YmgmreVEERGuQT-Y0,132
3
- pydantic_ai/_a2a.py,sha256=8nNtx6GENDt2Ej3f1ui9L-FuNQBYVELpJFfwz-y7fUw,7234
3
+ pydantic_ai/_a2a.py,sha256=G6W8zLRE5FNug19GieVkYuGPw5CA44YeZnS7GTN7M30,12068
4
4
  pydantic_ai/_agent_graph.py,sha256=rtzyBXN4bzEDBeRkRwF031ORktSMbuGz9toZmSqUxNI,42153
5
5
  pydantic_ai/_cli.py,sha256=R-sE-9gYqPxV5-5utso4g-bzAKMiTCdo33XOVqE0ZEg,13206
6
6
  pydantic_ai/_function_schema.py,sha256=BZus5y51eqiGQKxQIcCiDoSPml3AtAb12-st_aujU2k,10813
@@ -12,7 +12,7 @@ pydantic_ai/_run_context.py,sha256=zNkSyiQSH-YweO39ii3iB2taouUOodo3sTjz2Lrj4Pc,1
12
12
  pydantic_ai/_system_prompt.py,sha256=lUSq-gDZjlYTGtd6BUm54yEvTIvgdwBmJ8mLsNZZtYU,1142
13
13
  pydantic_ai/_thinking_part.py,sha256=mzx2RZSfiQxAKpljEflrcXRXmFKxtp6bKVyorY3UYZk,1554
14
14
  pydantic_ai/_utils.py,sha256=SGXEiGCnMae1Iz_eZKUs6ni_tGMPkDaJ4W3W3YMoP5w,15545
15
- pydantic_ai/agent.py,sha256=Fs-bm9eeCvanwiKiD-IS_XLcMmgNWucJylXgrIDH6WM,96186
15
+ pydantic_ai/agent.py,sha256=zvQgEG9eFG7entCTum3QSApHbNU8RvAE_ydscPaMAC4,96196
16
16
  pydantic_ai/direct.py,sha256=WRfgke3zm-eeR39LTuh9XI2TrdHXAqO81eDvFwih4Ko,14803
17
17
  pydantic_ai/exceptions.py,sha256=IdFw594Ou7Vn4YFa7xdZ040_j_6nmyA3MPANbC7sys4,3175
18
18
  pydantic_ai/format_as_xml.py,sha256=IINfh1evWDphGahqHNLBArB5dQ4NIqS3S-kru35ztGg,372
@@ -76,8 +76,8 @@ pydantic_ai/providers/mistral.py,sha256=EIUSENjFuGzBhvbdrarUTM4VPkesIMnZrzfnEKHO
76
76
  pydantic_ai/providers/openai.py,sha256=7iGij0EaFylab7dTZAZDgXr78tr-HsZrn9EI9AkWBNQ,3091
77
77
  pydantic_ai/providers/openrouter.py,sha256=NXjNdnlXIBrBMMqbzcWQnowXOuZh4NHikXenBn5h3mc,4061
78
78
  pydantic_ai/providers/together.py,sha256=zFVSMSm5jXbpkNouvBOTjWrPmlPpCp6sQS5LMSyVjrQ,3482
79
- pydantic_ai_slim-0.4.0.dist-info/METADATA,sha256=S-ygqOZ0lpsazK_VGyrj8B6l1H9Q7B2bCGYRtmmK4T8,3846
80
- pydantic_ai_slim-0.4.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
81
- pydantic_ai_slim-0.4.0.dist-info/entry_points.txt,sha256=kbKxe2VtDCYS06hsI7P3uZGxcVC08-FPt1rxeiMpIps,50
82
- pydantic_ai_slim-0.4.0.dist-info/licenses/LICENSE,sha256=vA6Jc482lEyBBuGUfD1pYx-cM7jxvLYOxPidZ30t_PQ,1100
83
- pydantic_ai_slim-0.4.0.dist-info/RECORD,,
79
+ pydantic_ai_slim-0.4.1.dist-info/METADATA,sha256=PqGrAd6qbv0rxMgiCa6N1Lo1mBvMb3XUC_FGaxuzeAY,3846
80
+ pydantic_ai_slim-0.4.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
81
+ pydantic_ai_slim-0.4.1.dist-info/entry_points.txt,sha256=kbKxe2VtDCYS06hsI7P3uZGxcVC08-FPt1rxeiMpIps,50
82
+ pydantic_ai_slim-0.4.1.dist-info/licenses/LICENSE,sha256=vA6Jc482lEyBBuGUfD1pYx-cM7jxvLYOxPidZ30t_PQ,1100
83
+ pydantic_ai_slim-0.4.1.dist-info/RECORD,,