pydantic-ai-slim 0.3.1__tar.gz → 0.3.3__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 (82) hide show
  1. {pydantic_ai_slim-0.3.1 → pydantic_ai_slim-0.3.3}/.gitignore +1 -0
  2. {pydantic_ai_slim-0.3.1 → pydantic_ai_slim-0.3.3}/PKG-INFO +4 -4
  3. {pydantic_ai_slim-0.3.1 → pydantic_ai_slim-0.3.3}/pydantic_ai/__init__.py +5 -2
  4. {pydantic_ai_slim-0.3.1 → pydantic_ai_slim-0.3.3}/pydantic_ai/_agent_graph.py +33 -15
  5. {pydantic_ai_slim-0.3.1 → pydantic_ai_slim-0.3.3}/pydantic_ai/_cli.py +7 -3
  6. {pydantic_ai_slim-0.3.1 → pydantic_ai_slim-0.3.3}/pydantic_ai/_function_schema.py +1 -4
  7. pydantic_ai_slim-0.3.3/pydantic_ai/_mcp.py +123 -0
  8. pydantic_ai_slim-0.3.3/pydantic_ai/_output.py +934 -0
  9. pydantic_ai_slim-0.3.3/pydantic_ai/_run_context.py +56 -0
  10. {pydantic_ai_slim-0.3.1 → pydantic_ai_slim-0.3.3}/pydantic_ai/_system_prompt.py +2 -1
  11. {pydantic_ai_slim-0.3.1 → pydantic_ai_slim-0.3.3}/pydantic_ai/_utils.py +111 -1
  12. {pydantic_ai_slim-0.3.1 → pydantic_ai_slim-0.3.3}/pydantic_ai/agent.py +66 -35
  13. {pydantic_ai_slim-0.3.1 → pydantic_ai_slim-0.3.3}/pydantic_ai/mcp.py +144 -115
  14. {pydantic_ai_slim-0.3.1 → pydantic_ai_slim-0.3.3}/pydantic_ai/models/__init__.py +21 -2
  15. {pydantic_ai_slim-0.3.1 → pydantic_ai_slim-0.3.3}/pydantic_ai/models/function.py +21 -3
  16. {pydantic_ai_slim-0.3.1 → pydantic_ai_slim-0.3.3}/pydantic_ai/models/gemini.py +27 -4
  17. {pydantic_ai_slim-0.3.1 → pydantic_ai_slim-0.3.3}/pydantic_ai/models/google.py +29 -4
  18. pydantic_ai_slim-0.3.3/pydantic_ai/models/mcp_sampling.py +95 -0
  19. {pydantic_ai_slim-0.3.1 → pydantic_ai_slim-0.3.3}/pydantic_ai/models/mistral.py +5 -1
  20. {pydantic_ai_slim-0.3.1 → pydantic_ai_slim-0.3.3}/pydantic_ai/models/openai.py +70 -9
  21. {pydantic_ai_slim-0.3.1 → pydantic_ai_slim-0.3.3}/pydantic_ai/models/test.py +1 -1
  22. {pydantic_ai_slim-0.3.1 → pydantic_ai_slim-0.3.3}/pydantic_ai/models/wrapper.py +6 -0
  23. pydantic_ai_slim-0.3.3/pydantic_ai/output.py +288 -0
  24. {pydantic_ai_slim-0.3.1 → pydantic_ai_slim-0.3.3}/pydantic_ai/profiles/__init__.py +21 -0
  25. {pydantic_ai_slim-0.3.1 → pydantic_ai_slim-0.3.3}/pydantic_ai/profiles/_json_schema.py +1 -1
  26. {pydantic_ai_slim-0.3.1 → pydantic_ai_slim-0.3.3}/pydantic_ai/profiles/google.py +6 -2
  27. {pydantic_ai_slim-0.3.1 → pydantic_ai_slim-0.3.3}/pydantic_ai/profiles/openai.py +5 -0
  28. {pydantic_ai_slim-0.3.1 → pydantic_ai_slim-0.3.3}/pydantic_ai/result.py +52 -26
  29. {pydantic_ai_slim-0.3.1 → pydantic_ai_slim-0.3.3}/pydantic_ai/settings.py +1 -0
  30. {pydantic_ai_slim-0.3.1 → pydantic_ai_slim-0.3.3}/pydantic_ai/tools.py +2 -47
  31. {pydantic_ai_slim-0.3.1 → pydantic_ai_slim-0.3.3}/pyproject.toml +1 -1
  32. pydantic_ai_slim-0.3.1/pydantic_ai/_output.py +0 -439
  33. {pydantic_ai_slim-0.3.1 → pydantic_ai_slim-0.3.3}/LICENSE +0 -0
  34. {pydantic_ai_slim-0.3.1 → pydantic_ai_slim-0.3.3}/README.md +0 -0
  35. {pydantic_ai_slim-0.3.1 → pydantic_ai_slim-0.3.3}/pydantic_ai/__main__.py +0 -0
  36. {pydantic_ai_slim-0.3.1 → pydantic_ai_slim-0.3.3}/pydantic_ai/_a2a.py +0 -0
  37. {pydantic_ai_slim-0.3.1 → pydantic_ai_slim-0.3.3}/pydantic_ai/_griffe.py +0 -0
  38. {pydantic_ai_slim-0.3.1 → pydantic_ai_slim-0.3.3}/pydantic_ai/_parts_manager.py +0 -0
  39. {pydantic_ai_slim-0.3.1 → pydantic_ai_slim-0.3.3}/pydantic_ai/_thinking_part.py +0 -0
  40. {pydantic_ai_slim-0.3.1 → pydantic_ai_slim-0.3.3}/pydantic_ai/common_tools/__init__.py +0 -0
  41. {pydantic_ai_slim-0.3.1 → pydantic_ai_slim-0.3.3}/pydantic_ai/common_tools/duckduckgo.py +0 -0
  42. {pydantic_ai_slim-0.3.1 → pydantic_ai_slim-0.3.3}/pydantic_ai/common_tools/tavily.py +0 -0
  43. {pydantic_ai_slim-0.3.1 → pydantic_ai_slim-0.3.3}/pydantic_ai/direct.py +0 -0
  44. {pydantic_ai_slim-0.3.1 → pydantic_ai_slim-0.3.3}/pydantic_ai/exceptions.py +0 -0
  45. {pydantic_ai_slim-0.3.1 → pydantic_ai_slim-0.3.3}/pydantic_ai/ext/__init__.py +0 -0
  46. {pydantic_ai_slim-0.3.1 → pydantic_ai_slim-0.3.3}/pydantic_ai/ext/langchain.py +0 -0
  47. {pydantic_ai_slim-0.3.1 → pydantic_ai_slim-0.3.3}/pydantic_ai/format_as_xml.py +0 -0
  48. {pydantic_ai_slim-0.3.1 → pydantic_ai_slim-0.3.3}/pydantic_ai/format_prompt.py +0 -0
  49. {pydantic_ai_slim-0.3.1 → pydantic_ai_slim-0.3.3}/pydantic_ai/messages.py +0 -0
  50. {pydantic_ai_slim-0.3.1 → pydantic_ai_slim-0.3.3}/pydantic_ai/models/anthropic.py +0 -0
  51. {pydantic_ai_slim-0.3.1 → pydantic_ai_slim-0.3.3}/pydantic_ai/models/bedrock.py +0 -0
  52. {pydantic_ai_slim-0.3.1 → pydantic_ai_slim-0.3.3}/pydantic_ai/models/cohere.py +0 -0
  53. {pydantic_ai_slim-0.3.1 → pydantic_ai_slim-0.3.3}/pydantic_ai/models/fallback.py +0 -0
  54. {pydantic_ai_slim-0.3.1 → pydantic_ai_slim-0.3.3}/pydantic_ai/models/groq.py +0 -0
  55. {pydantic_ai_slim-0.3.1 → pydantic_ai_slim-0.3.3}/pydantic_ai/models/instrumented.py +0 -0
  56. {pydantic_ai_slim-0.3.1 → pydantic_ai_slim-0.3.3}/pydantic_ai/profiles/amazon.py +0 -0
  57. {pydantic_ai_slim-0.3.1 → pydantic_ai_slim-0.3.3}/pydantic_ai/profiles/anthropic.py +0 -0
  58. {pydantic_ai_slim-0.3.1 → pydantic_ai_slim-0.3.3}/pydantic_ai/profiles/cohere.py +0 -0
  59. {pydantic_ai_slim-0.3.1 → pydantic_ai_slim-0.3.3}/pydantic_ai/profiles/deepseek.py +0 -0
  60. {pydantic_ai_slim-0.3.1 → pydantic_ai_slim-0.3.3}/pydantic_ai/profiles/grok.py +0 -0
  61. {pydantic_ai_slim-0.3.1 → pydantic_ai_slim-0.3.3}/pydantic_ai/profiles/meta.py +0 -0
  62. {pydantic_ai_slim-0.3.1 → pydantic_ai_slim-0.3.3}/pydantic_ai/profiles/mistral.py +0 -0
  63. {pydantic_ai_slim-0.3.1 → pydantic_ai_slim-0.3.3}/pydantic_ai/profiles/qwen.py +0 -0
  64. {pydantic_ai_slim-0.3.1 → pydantic_ai_slim-0.3.3}/pydantic_ai/providers/__init__.py +0 -0
  65. {pydantic_ai_slim-0.3.1 → pydantic_ai_slim-0.3.3}/pydantic_ai/providers/anthropic.py +0 -0
  66. {pydantic_ai_slim-0.3.1 → pydantic_ai_slim-0.3.3}/pydantic_ai/providers/azure.py +0 -0
  67. {pydantic_ai_slim-0.3.1 → pydantic_ai_slim-0.3.3}/pydantic_ai/providers/bedrock.py +0 -0
  68. {pydantic_ai_slim-0.3.1 → pydantic_ai_slim-0.3.3}/pydantic_ai/providers/cohere.py +0 -0
  69. {pydantic_ai_slim-0.3.1 → pydantic_ai_slim-0.3.3}/pydantic_ai/providers/deepseek.py +0 -0
  70. {pydantic_ai_slim-0.3.1 → pydantic_ai_slim-0.3.3}/pydantic_ai/providers/fireworks.py +0 -0
  71. {pydantic_ai_slim-0.3.1 → pydantic_ai_slim-0.3.3}/pydantic_ai/providers/google.py +0 -0
  72. {pydantic_ai_slim-0.3.1 → pydantic_ai_slim-0.3.3}/pydantic_ai/providers/google_gla.py +0 -0
  73. {pydantic_ai_slim-0.3.1 → pydantic_ai_slim-0.3.3}/pydantic_ai/providers/google_vertex.py +0 -0
  74. {pydantic_ai_slim-0.3.1 → pydantic_ai_slim-0.3.3}/pydantic_ai/providers/grok.py +0 -0
  75. {pydantic_ai_slim-0.3.1 → pydantic_ai_slim-0.3.3}/pydantic_ai/providers/groq.py +0 -0
  76. {pydantic_ai_slim-0.3.1 → pydantic_ai_slim-0.3.3}/pydantic_ai/providers/heroku.py +0 -0
  77. {pydantic_ai_slim-0.3.1 → pydantic_ai_slim-0.3.3}/pydantic_ai/providers/mistral.py +0 -0
  78. {pydantic_ai_slim-0.3.1 → pydantic_ai_slim-0.3.3}/pydantic_ai/providers/openai.py +0 -0
  79. {pydantic_ai_slim-0.3.1 → pydantic_ai_slim-0.3.3}/pydantic_ai/providers/openrouter.py +0 -0
  80. {pydantic_ai_slim-0.3.1 → pydantic_ai_slim-0.3.3}/pydantic_ai/providers/together.py +0 -0
  81. {pydantic_ai_slim-0.3.1 → pydantic_ai_slim-0.3.3}/pydantic_ai/py.typed +0 -0
  82. {pydantic_ai_slim-0.3.1 → pydantic_ai_slim-0.3.3}/pydantic_ai/usage.py +0 -0
@@ -19,3 +19,4 @@ examples/pydantic_ai_examples/.chat_app_messages.sqlite
19
19
  node_modules/
20
20
  **.idea/
21
21
  .coverage*
22
+ /test_tmp/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pydantic-ai-slim
3
- Version: 0.3.1
3
+ Version: 0.3.3
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.3.1
33
+ Requires-Dist: pydantic-graph==0.3.3
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.3.1; extra == 'a2a'
37
+ Requires-Dist: fasta2a==0.3.3; 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.3.1; extra == 'evals'
51
+ Requires-Dist: pydantic-evals==0.3.3; extra == 'evals'
52
52
  Provides-Extra: google
53
53
  Requires-Dist: google-genai>=1.15.0; extra == 'google'
54
54
  Provides-Extra: groq
@@ -12,7 +12,7 @@ from .exceptions import (
12
12
  )
13
13
  from .format_prompt import format_as_xml
14
14
  from .messages import AudioUrl, BinaryContent, DocumentUrl, ImageUrl, VideoUrl
15
- from .result import ToolOutput
15
+ from .output import NativeOutput, PromptedOutput, TextOutput, ToolOutput
16
16
  from .tools import RunContext, Tool
17
17
 
18
18
  __all__ = (
@@ -41,8 +41,11 @@ __all__ = (
41
41
  # tools
42
42
  'Tool',
43
43
  'RunContext',
44
- # result
44
+ # output
45
45
  'ToolOutput',
46
+ 'NativeOutput',
47
+ 'PromptedOutput',
48
+ 'TextOutput',
46
49
  # format_prompt
47
50
  'format_as_xml',
48
51
  )
@@ -18,7 +18,7 @@ from pydantic_graph import BaseNode, Graph, GraphRunContext
18
18
  from pydantic_graph.nodes import End, NodeRunEndT
19
19
 
20
20
  from . import _output, _system_prompt, exceptions, messages as _messages, models, result, usage as _usage
21
- from .result import OutputDataT
21
+ from .output import OutputDataT, OutputSpec
22
22
  from .settings import ModelSettings, merge_model_settings
23
23
  from .tools import RunContext, Tool, ToolDefinition, ToolsPrepareFunc
24
24
 
@@ -102,7 +102,7 @@ class GraphAgentDeps(Generic[DepsT, OutputDataT]):
102
102
  end_strategy: EndStrategy
103
103
  get_instructions: Callable[[RunContext[DepsT]], Awaitable[str | None]]
104
104
 
105
- output_schema: _output.OutputSchema[OutputDataT] | None
105
+ output_schema: _output.OutputSchema[OutputDataT]
106
106
  output_validators: list[_output.OutputValidator[DepsT, OutputDataT]]
107
107
 
108
108
  history_processors: Sequence[HistoryProcessor[DepsT]]
@@ -141,6 +141,8 @@ def is_agent_node(
141
141
 
142
142
  @dataclasses.dataclass
143
143
  class UserPromptNode(AgentNode[DepsT, NodeRunEndT]):
144
+ """The node that handles the user prompt and instructions."""
145
+
144
146
  user_prompt: str | Sequence[_messages.UserContent] | None
145
147
 
146
148
  instructions: str | None
@@ -284,16 +286,29 @@ async def _prepare_request_parameters(
284
286
  function_tool_defs = await ctx.deps.prepare_tools(run_context, function_tool_defs) or []
285
287
 
286
288
  output_schema = ctx.deps.output_schema
289
+
290
+ output_tools = []
291
+ output_object = None
292
+ if isinstance(output_schema, _output.ToolOutputSchema):
293
+ output_tools = output_schema.tool_defs()
294
+ elif isinstance(output_schema, _output.NativeOutputSchema):
295
+ output_object = output_schema.object_def
296
+
297
+ # ToolOrTextOutputSchema, NativeOutputSchema, and PromptedOutputSchema all inherit from TextOutputSchema
298
+ allow_text_output = isinstance(output_schema, _output.TextOutputSchema)
299
+
287
300
  return models.ModelRequestParameters(
288
301
  function_tools=function_tool_defs,
289
- allow_text_output=_output.allow_text_output(output_schema),
290
- output_tools=output_schema.tool_defs() if output_schema is not None else [],
302
+ output_mode=output_schema.mode,
303
+ output_tools=output_tools,
304
+ output_object=output_object,
305
+ allow_text_output=allow_text_output,
291
306
  )
292
307
 
293
308
 
294
309
  @dataclasses.dataclass
295
310
  class ModelRequestNode(AgentNode[DepsT, NodeRunEndT]):
296
- """Make a request to the model using the last message in state.message_history."""
311
+ """The node that makes a request to the model using the last message in state.message_history."""
297
312
 
298
313
  request: _messages.ModelRequest
299
314
 
@@ -412,7 +427,7 @@ class ModelRequestNode(AgentNode[DepsT, NodeRunEndT]):
412
427
 
413
428
  @dataclasses.dataclass
414
429
  class CallToolsNode(AgentNode[DepsT, NodeRunEndT]):
415
- """Process a model response, and decide whether to end the run or make a new request."""
430
+ """The node that processes a model response, and decides whether to end the run or make a new request."""
416
431
 
417
432
  model_response: _messages.ModelResponse
418
433
 
@@ -482,7 +497,7 @@ class CallToolsNode(AgentNode[DepsT, NodeRunEndT]):
482
497
  # when the model has already returned text along side tool calls
483
498
  # in this scenario, if text responses are allowed, we return text from the most recent model
484
499
  # response, if any
485
- if _output.allow_text_output(ctx.deps.output_schema):
500
+ if isinstance(ctx.deps.output_schema, _output.TextOutputSchema):
486
501
  for message in reversed(ctx.state.message_history):
487
502
  if isinstance(message, _messages.ModelResponse):
488
503
  last_texts = [p.content for p in message.parts if isinstance(p, _messages.TextPart)]
@@ -505,10 +520,11 @@ class CallToolsNode(AgentNode[DepsT, NodeRunEndT]):
505
520
  output_schema = ctx.deps.output_schema
506
521
  run_context = build_run_context(ctx)
507
522
 
508
- # first, look for the output tool call
509
523
  final_result: result.FinalResult[NodeRunEndT] | None = None
510
524
  parts: list[_messages.ModelRequestPart] = []
511
- if output_schema is not None:
525
+
526
+ # first, look for the output tool call
527
+ if isinstance(output_schema, _output.ToolOutputSchema):
512
528
  for call, output_tool in output_schema.find_tool(tool_calls):
513
529
  try:
514
530
  result_data = await output_tool.process(call, run_context)
@@ -566,9 +582,9 @@ class CallToolsNode(AgentNode[DepsT, NodeRunEndT]):
566
582
 
567
583
  text = '\n\n'.join(texts)
568
584
  try:
569
- if _output.allow_text_output(output_schema):
570
- # The following cast is safe because we know `str` is an allowed result type
571
- result_data = cast(NodeRunEndT, text)
585
+ if isinstance(output_schema, _output.TextOutputSchema):
586
+ run_context = build_run_context(ctx)
587
+ result_data = await output_schema.process(text, run_context)
572
588
  else:
573
589
  m = _messages.RetryPromptPart(
574
590
  content='Plain text responses are not permitted, please include your response in a tool call',
@@ -667,7 +683,7 @@ async def process_function_tools( # noqa C901
667
683
  yield event
668
684
  call_index_to_event_id[len(calls_to_run)] = event.call_id
669
685
  calls_to_run.append((mcp_tool, call))
670
- elif output_schema is not None and call.tool_name in output_schema.tools:
686
+ elif call.tool_name in output_schema.tools:
671
687
  # if tool_name is in output_schema, it means we found a output tool but an error occurred in
672
688
  # validation, we don't add another part here
673
689
  if output_tool_name is not None:
@@ -807,7 +823,9 @@ def _unknown_tool(
807
823
  ) -> _messages.RetryPromptPart:
808
824
  ctx.state.increment_retries(ctx.deps.max_result_retries)
809
825
  tool_names = list(ctx.deps.function_tools.keys())
810
- if output_schema := ctx.deps.output_schema:
826
+
827
+ output_schema = ctx.deps.output_schema
828
+ if isinstance(output_schema, _output.ToolOutputSchema):
811
829
  tool_names.extend(output_schema.tool_names())
812
830
 
813
831
  if tool_names:
@@ -884,7 +902,7 @@ def get_captured_run_messages() -> _RunMessages:
884
902
  def build_agent_graph(
885
903
  name: str | None,
886
904
  deps_type: type[DepsT],
887
- output_type: _output.OutputType[OutputT],
905
+ output_type: OutputSpec[OutputT],
888
906
  ) -> Graph[GraphAgentState, GraphAgentDeps[DepsT, result.FinalResult[OutputT]], result.FinalResult[OutputT]]:
889
907
  """Build the execution [Graph][pydantic_graph.Graph] for a given agent."""
890
908
  nodes = (
@@ -14,14 +14,13 @@ from typing import Any, cast
14
14
 
15
15
  from typing_inspection.introspection import get_literal_values
16
16
 
17
- from pydantic_ai.result import OutputDataT
18
- from pydantic_ai.tools import AgentDepsT
19
-
20
17
  from . import __version__
18
+ from ._run_context import AgentDepsT
21
19
  from .agent import Agent
22
20
  from .exceptions import UserError
23
21
  from .messages import ModelMessage
24
22
  from .models import KnownModelName, infer_model
23
+ from .output import OutputDataT
25
24
 
26
25
  try:
27
26
  import argcomplete
@@ -254,6 +253,11 @@ async def run_chat(
254
253
  messages = await ask_agent(agent, text, stream, console, code_theme, deps, messages)
255
254
  except CancelledError: # pragma: no cover
256
255
  console.print('[dim]Interrupted[/dim]')
256
+ except Exception as e: # pragma: no cover
257
+ cause = getattr(e, '__cause__', None)
258
+ console.print(f'\n[red]{type(e).__name__}:[/red] {e}')
259
+ if cause:
260
+ console.print(f'[dim]Caused by: {cause}[/dim]')
257
261
 
258
262
 
259
263
  async def ask_agent(
@@ -19,9 +19,8 @@ from pydantic.plugin._schema_validator import create_schema_validator
19
19
  from pydantic_core import SchemaValidator, core_schema
20
20
  from typing_extensions import Concatenate, ParamSpec, TypeIs, TypeVar, get_origin
21
21
 
22
- from pydantic_ai.tools import RunContext
23
-
24
22
  from ._griffe import doc_descriptions
23
+ from ._run_context import RunContext
25
24
  from ._utils import check_object_json_schema, is_async_callable, is_model_like, run_in_executor
26
25
 
27
26
  if TYPE_CHECKING:
@@ -281,6 +280,4 @@ def _build_schema(
281
280
 
282
281
  def _is_call_ctx(annotation: Any) -> bool:
283
282
  """Return whether the annotation is the `RunContext` class, parameterized or not."""
284
- from .tools import RunContext
285
-
286
283
  return annotation is RunContext or get_origin(annotation) is RunContext
@@ -0,0 +1,123 @@
1
+ import base64
2
+ from collections.abc import Sequence
3
+ from typing import Literal
4
+
5
+ from . import exceptions, messages
6
+
7
+ try:
8
+ from mcp import types as mcp_types
9
+ except ImportError as _import_error:
10
+ raise ImportError(
11
+ 'Please install the `mcp` package to use the MCP server, '
12
+ 'you can use the `mcp` optional group — `pip install "pydantic-ai-slim[mcp]"`'
13
+ ) from _import_error
14
+
15
+
16
+ def map_from_mcp_params(params: mcp_types.CreateMessageRequestParams) -> list[messages.ModelMessage]:
17
+ """Convert from MCP create message request parameters to pydantic-ai messages."""
18
+ pai_messages: list[messages.ModelMessage] = []
19
+ request_parts: list[messages.ModelRequestPart] = []
20
+ if params.systemPrompt:
21
+ request_parts.append(messages.SystemPromptPart(content=params.systemPrompt))
22
+ response_parts: list[messages.ModelResponsePart] = []
23
+ for msg in params.messages:
24
+ content = msg.content
25
+ if msg.role == 'user':
26
+ # if there are any response parts, add a response message wrapping them
27
+ if response_parts:
28
+ pai_messages.append(messages.ModelResponse(parts=response_parts))
29
+ response_parts = []
30
+
31
+ # TODO(Marcelo): We can reuse the `_map_tool_result_part` from the mcp module here.
32
+ if isinstance(content, mcp_types.TextContent):
33
+ user_part_content: str | Sequence[messages.UserContent] = content.text
34
+ else:
35
+ # image content
36
+ user_part_content = [
37
+ messages.BinaryContent(data=base64.b64decode(content.data), media_type=content.mimeType)
38
+ ]
39
+
40
+ request_parts.append(messages.UserPromptPart(content=user_part_content))
41
+ else:
42
+ # role is assistant
43
+ # if there are any request parts, add a request message wrapping them
44
+ if request_parts:
45
+ pai_messages.append(messages.ModelRequest(parts=request_parts))
46
+ request_parts = []
47
+
48
+ response_parts.append(map_from_sampling_content(content))
49
+
50
+ if response_parts:
51
+ pai_messages.append(messages.ModelResponse(parts=response_parts))
52
+ if request_parts:
53
+ pai_messages.append(messages.ModelRequest(parts=request_parts))
54
+ return pai_messages
55
+
56
+
57
+ def map_from_pai_messages(pai_messages: list[messages.ModelMessage]) -> tuple[str, list[mcp_types.SamplingMessage]]:
58
+ """Convert from pydantic-ai messages to MCP sampling messages.
59
+
60
+ Returns:
61
+ A tuple containing the system prompt and a list of sampling messages.
62
+ """
63
+ sampling_msgs: list[mcp_types.SamplingMessage] = []
64
+
65
+ def add_msg(
66
+ role: Literal['user', 'assistant'],
67
+ content: mcp_types.TextContent | mcp_types.ImageContent | mcp_types.AudioContent,
68
+ ):
69
+ sampling_msgs.append(mcp_types.SamplingMessage(role=role, content=content))
70
+
71
+ system_prompt: list[str] = []
72
+ for pai_message in pai_messages:
73
+ if isinstance(pai_message, messages.ModelRequest):
74
+ if pai_message.instructions is not None:
75
+ system_prompt.append(pai_message.instructions)
76
+
77
+ for part in pai_message.parts:
78
+ if isinstance(part, messages.SystemPromptPart):
79
+ system_prompt.append(part.content)
80
+ if isinstance(part, messages.UserPromptPart):
81
+ if isinstance(part.content, str):
82
+ add_msg('user', mcp_types.TextContent(type='text', text=part.content))
83
+ else:
84
+ for chunk in part.content:
85
+ if isinstance(chunk, str):
86
+ add_msg('user', mcp_types.TextContent(type='text', text=chunk))
87
+ elif isinstance(chunk, messages.BinaryContent) and chunk.is_image:
88
+ add_msg(
89
+ 'user',
90
+ mcp_types.ImageContent(
91
+ type='image',
92
+ data=base64.b64decode(chunk.data).decode(),
93
+ mimeType=chunk.media_type,
94
+ ),
95
+ )
96
+ # TODO(Marcelo): Add support for audio content.
97
+ else:
98
+ raise NotImplementedError(f'Unsupported content type: {type(chunk)}')
99
+ else:
100
+ add_msg('assistant', map_from_model_response(pai_message))
101
+ return ''.join(system_prompt), sampling_msgs
102
+
103
+
104
+ def map_from_model_response(model_response: messages.ModelResponse) -> mcp_types.TextContent:
105
+ """Convert from a model response to MCP text content."""
106
+ text_parts: list[str] = []
107
+ for part in model_response.parts:
108
+ if isinstance(part, messages.TextPart):
109
+ text_parts.append(part.content)
110
+ # TODO(Marcelo): We should ignore ThinkingPart here.
111
+ else:
112
+ raise exceptions.UnexpectedModelBehavior(f'Unexpected part type: {type(part).__name__}, expected TextPart')
113
+ return mcp_types.TextContent(type='text', text=''.join(text_parts))
114
+
115
+
116
+ def map_from_sampling_content(
117
+ content: mcp_types.TextContent | mcp_types.ImageContent | mcp_types.AudioContent,
118
+ ) -> messages.TextPart:
119
+ """Convert from sampling content to a pydantic-ai text part."""
120
+ if isinstance(content, mcp_types.TextContent): # pragma: no branch
121
+ return messages.TextPart(content=content.text)
122
+ else:
123
+ raise NotImplementedError('Image and Audio responses in sampling are not yet supported')