diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f38236de9e..afd2c63d9a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -163,16 +163,10 @@ jobs: enable-cache: true cache-suffix: ${{ matrix.install.name }} - - uses: denoland/setup-deno@v2 - with: - deno-version: v2.5.x - - run: mkdir .coverage - run: uv sync --only-dev - - run: uv run mcp-run-python example --deps=numpy - - name: cache HuggingFace models uses: actions/cache@v4 with: @@ -214,16 +208,10 @@ jobs: enable-cache: true cache-suffix: lowest-versions - - uses: denoland/setup-deno@v2 - with: - deno-version: v2.5.x - - run: mkdir .coverage - run: uv sync --group dev - - run: uv run mcp-run-python example --deps=numpy - - name: cache HuggingFace models uses: actions/cache@v4 with: diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index cf58d18e3b..7ff3952f92 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -39,10 +39,6 @@ jobs: enable-cache: true cache-suffix: claude-code - - uses: denoland/setup-deno@v2 - with: - deno-version: v2.5.x - - run: uv tool install pre-commit - run: make install diff --git a/docs/contributing.md b/docs/contributing.md index b376712895..683ef4d483 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -9,11 +9,10 @@ git clone git@github.com:/pydantic-ai.git cd pydantic-ai ``` -Install `uv` (version 0.4.30 or later), `pre-commit` and `deno`: +Install `uv` (version 0.4.30 or later) and `pre-commit`: - [`uv` install docs](https://docs.astral.sh/uv/getting-started/installation/) - [`pre-commit` install docs](https://pre-commit.com/#install) -- [`deno` install docs](https://docs.deno.com/runtime/getting_started/installation/) To install `pre-commit` you can run the following command: @@ -21,14 +20,6 @@ To install `pre-commit` you can run the following command: uv tool install pre-commit ``` -For `deno`, you can run the following, or check -[their documentation](https://docs.deno.com/runtime/getting_started/installation/) for alternative -installation methods: - -```bash -curl -fsSL https://deno.land/install.sh | sh -``` - Install `pydantic-ai`, all dependencies and pre-commit hooks ```bash diff --git a/docs/mcp/client.md b/docs/mcp/client.md index 817ab21f92..63c21a0b83 100644 --- a/docs/mcp/client.md +++ b/docs/mcp/client.md @@ -134,26 +134,22 @@ _(This example is complete, it can be run "as is" — you'll need to add `asynci MCP also offers [stdio transport](https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/transports/#stdio) where the server is run as a subprocess and communicates with the client over `stdin` and `stdout`. In this case, you'd use the [`MCPServerStdio`][pydantic_ai.mcp.MCPServerStdio] class. -In this example [mcp-run-python](https://github.com/pydantic/mcp-run-python) is used as the MCP server. +In this example we use a simple MCP server that provides weather tools. ```python {title="mcp_stdio_client.py"} from pydantic_ai import Agent from pydantic_ai.mcp import MCPServerStdio -server = MCPServerStdio( # (1)! - 'uv', args=['run', 'mcp-run-python', 'stdio'], timeout=10 -) +server = MCPServerStdio('python', args=['mcp_server.py'], timeout=10) agent = Agent('openai:gpt-5', toolsets=[server]) async def main(): - result = await agent.run('How many days between 2000-01-01 and 2025-03-18?') + result = await agent.run('What is the weather in Paris?') print(result.output) - #> There are 9,208 days between January 1, 2000, and March 18, 2025. + #> The weather in Paris is sunny and 26 degrees Celsius. ``` -1. See [MCP Run Python](https://github.com/pydantic/mcp-run-python) for more information. - ## Loading MCP Servers from Configuration Instead of creating MCP server instances individually in code, you can load multiple servers from a JSON configuration file using [`load_mcp_servers()`][pydantic_ai.mcp.load_mcp_servers]. @@ -168,8 +164,12 @@ The configuration file should be a JSON file with an `mcpServers` object contain { "mcpServers": { "python-runner": { - "command": "uv", - "args": ["run", "mcp-run-python", "stdio"] + "command": "uv", + "args": ["run", "mcp-run-python", "stdio"] + }, + "weather": { + "command": "python", + "args": ["mcp_server.py"] }, "weather-api": { "url": "http://localhost:3001/sse" diff --git a/docs/mcp/fastmcp-client.md b/docs/mcp/fastmcp-client.md index 538b2afbbb..e3377796f0 100644 --- a/docs/mcp/fastmcp-client.md +++ b/docs/mcp/fastmcp-client.md @@ -20,12 +20,12 @@ A `FastMCPToolset` can then be created from: - A FastMCP Server: `#!python FastMCPToolset(fastmcp.FastMCP('my_server'))` - A FastMCP Client: `#!python FastMCPToolset(fastmcp.Client(...))` -- A FastMCP Transport: `#!python FastMCPToolset(fastmcp.StdioTransport(command='uvx', args=['mcp-run-python', 'stdio']))` +- A FastMCP Transport: `#!python FastMCPToolset(fastmcp.StdioTransport(command='python', args=['mcp_server.py']))` - A Streamable HTTP URL: `#!python FastMCPToolset('http://localhost:8000/mcp')` - An HTTP SSE URL: `#!python FastMCPToolset('http://localhost:8000/sse')` - A Python Script: `#!python FastMCPToolset('my_server.py')` - A Node.js Script: `#!python FastMCPToolset('my_server.js')` -- A JSON MCP Configuration: `#!python FastMCPToolset({'mcpServers': {'my_server': {'command': 'uvx', 'args': ['mcp-run-python', 'stdio']}}})` +- A JSON MCP Configuration: `#!python FastMCPToolset({'mcpServers': {'my_server': {'command': 'python', 'args': ['mcp_server.py']}}})` If you already have a [FastMCP Server](https://gofastmcp.com/servers) in the same codebase as your Pydantic AI agent, you can create a `FastMCPToolset` directly from it and save agent a network round trip: @@ -76,6 +76,10 @@ mcp_config = { 'time_mcp_server': { 'command': 'uvx', 'args': ['mcp-run-python', 'stdio'] + }, + 'weather_server': { + 'command': 'python', + 'args': ['mcp_server.py'] } } } diff --git a/pydantic_ai_slim/pydantic_ai/models/cerebras.py b/pydantic_ai_slim/pydantic_ai/models/cerebras.py index 867d965087..ee66a9a168 100644 --- a/pydantic_ai_slim/pydantic_ai/models/cerebras.py +++ b/pydantic_ai_slim/pydantic_ai/models/cerebras.py @@ -16,7 +16,7 @@ from openai import AsyncOpenAI from .openai import OpenAIChatModel, OpenAIChatModelSettings -except ImportError as _import_error: # pragma: no cover +except ImportError as _import_error: raise ImportError( 'Please install the `openai` package to use the Cerebras model, ' 'you can use the `cerebras` optional group — `pip install "pydantic-ai-slim[cerebras]"' diff --git a/tests/example_modules/mcp_server.py b/tests/example_modules/mcp_server.py index 2c611048a9..d468b951bc 100644 --- a/tests/example_modules/mcp_server.py +++ b/tests/example_modules/mcp_server.py @@ -7,6 +7,12 @@ mcp = FastMCP('Pydantic AI MCP Server') +@mcp.tool() +async def get_weather_forecast(location: str) -> str: + """Get the weather forecast for a location.""" + return f'The weather in {location} is sunny and 26 degrees Celsius.' + + @mcp.tool() async def echo_deps(ctx: Context[ServerSessionT, LifespanContextT, RequestT]) -> dict[str, Any]: """Echo the run context. diff --git a/tests/models/anthropic/conftest.py b/tests/models/anthropic/conftest.py index 6edb7d19f2..7bc1666277 100644 --- a/tests/models/anthropic/conftest.py +++ b/tests/models/anthropic/conftest.py @@ -18,7 +18,7 @@ from pydantic_ai.models.anthropic import AnthropicModel from pydantic_ai.providers.anthropic import AnthropicProvider -AnthropicModelFactory = Callable[..., AnthropicModel] + AnthropicModelFactory = Callable[..., AnthropicModel] # Model factory fixture for live API tests diff --git a/tests/models/anthropic/test_output.py b/tests/models/anthropic/test_output.py index 714cbdc435..3ac87f0c34 100644 --- a/tests/models/anthropic/test_output.py +++ b/tests/models/anthropic/test_output.py @@ -12,7 +12,7 @@ from __future__ import annotations as _annotations from collections.abc import Callable -from typing import Annotated +from typing import TYPE_CHECKING, Annotated import httpx import pytest @@ -33,6 +33,11 @@ from pydantic_ai.models.anthropic import AnthropicModel from pydantic_ai.providers.anthropic import AnthropicProvider +if TYPE_CHECKING: + from pydantic_ai.models.anthropic import AnthropicModel + + ANTHROPIC_MODEL_FIXTURE = Callable[..., AnthropicModel] + from ..test_anthropic import completion_message pytestmark = [ @@ -231,9 +236,6 @@ async def verify_headers(request: httpx.Request): return verify_headers -ANTHROPIC_MODEL_FIXTURE = Callable[..., AnthropicModel] - - # ============================================================================= # Supported Model Tests (claude-sonnet-4-5) # ============================================================================= diff --git a/tests/models/test_bedrock.py b/tests/models/test_bedrock.py index 9647a0eb7c..d8f3df4d59 100644 --- a/tests/models/test_bedrock.py +++ b/tests/models/test_bedrock.py @@ -5,9 +5,7 @@ from typing import Any import pytest -from botocore.exceptions import ClientError from inline_snapshot import snapshot -from mypy_boto3_bedrock_runtime.type_defs import MessageUnionTypeDef, SystemContentBlockTypeDef, ToolTypeDef from typing_extensions import TypedDict from pydantic_ai import ( @@ -49,6 +47,9 @@ from ..conftest import IsDatetime, IsInstance, IsStr, try_import with try_import() as imports_successful: + from botocore.exceptions import ClientError + from mypy_boto3_bedrock_runtime.type_defs import MessageUnionTypeDef, SystemContentBlockTypeDef, ToolTypeDef + from pydantic_ai.models.bedrock import BedrockConverseModel, BedrockModelName, BedrockModelSettings from pydantic_ai.models.openai import OpenAIResponsesModel, OpenAIResponsesModelSettings from pydantic_ai.providers.bedrock import BedrockProvider diff --git a/tests/models/test_google.py b/tests/models/test_google.py index c82afbff34..93dbed8e96 100644 --- a/tests/models/test_google.py +++ b/tests/models/test_google.py @@ -94,6 +94,12 @@ from pydantic_ai.providers.google import GoogleProvider from pydantic_ai.providers.openai import OpenAIProvider +if not imports_successful(): + # Define placeholder errors module so parametrize decorators can be parsed + from types import SimpleNamespace + + errors = SimpleNamespace(ServerError=Exception, ClientError=Exception, APIError=Exception) + pytestmark = [ pytest.mark.skipif(not imports_successful(), reason='google-genai not installed'), pytest.mark.anyio, @@ -4660,7 +4666,7 @@ async def test_google_api_errors_are_handled( allow_model_requests: None, google_provider: GoogleProvider, mocker: MockerFixture, - error_class: type[errors.APIError], + error_class: Any, error_response: dict[str, Any], expected_status: int, ): diff --git a/tests/models/test_huggingface.py b/tests/models/test_huggingface.py index ed99de4e56..a144130139 100644 --- a/tests/models/test_huggingface.py +++ b/tests/models/test_huggingface.py @@ -8,25 +8,7 @@ from typing import Any, Literal, cast from unittest.mock import Mock -import aiohttp import pytest -from huggingface_hub import ( - AsyncInferenceClient, - ChatCompletionInputMessage, - ChatCompletionOutput, - ChatCompletionOutputComplete, - ChatCompletionOutputFunctionDefinition, - ChatCompletionOutputMessage, - ChatCompletionOutputToolCall, - ChatCompletionOutputUsage, - ChatCompletionStreamOutput, - ChatCompletionStreamOutputChoice, - ChatCompletionStreamOutputDelta, - ChatCompletionStreamOutputDeltaToolCall, - ChatCompletionStreamOutputFunction, - ChatCompletionStreamOutputUsage, -) -from huggingface_hub.errors import HfHubHTTPError from inline_snapshot import snapshot from typing_extensions import TypedDict @@ -50,8 +32,6 @@ VideoUrl, ) from pydantic_ai.exceptions import ModelHTTPError -from pydantic_ai.models.huggingface import HuggingFaceModel -from pydantic_ai.providers.huggingface import HuggingFaceProvider from pydantic_ai.result import RunUsage from pydantic_ai.run import AgentRunResult, AgentRunResultEvent from pydantic_ai.settings import ModelSettings @@ -62,10 +42,30 @@ from .mock_async_stream import MockAsyncStream with try_import() as imports_successful: - pass + import aiohttp + from huggingface_hub import ( + AsyncInferenceClient, + ChatCompletionInputMessage, + ChatCompletionOutput, + ChatCompletionOutputComplete, + ChatCompletionOutputFunctionDefinition, + ChatCompletionOutputMessage, + ChatCompletionOutputToolCall, + ChatCompletionOutputUsage, + ChatCompletionStreamOutput, + ChatCompletionStreamOutputChoice, + ChatCompletionStreamOutputDelta, + ChatCompletionStreamOutputDeltaToolCall, + ChatCompletionStreamOutputFunction, + ChatCompletionStreamOutputUsage, + ) + from huggingface_hub.errors import HfHubHTTPError + + from pydantic_ai.models.huggingface import HuggingFaceModel + from pydantic_ai.providers.huggingface import HuggingFaceProvider -MockChatCompletion = ChatCompletionOutput | Exception -MockStreamEvent = ChatCompletionStreamOutput | Exception + MockChatCompletion = ChatCompletionOutput | Exception + MockStreamEvent = ChatCompletionStreamOutput | Exception pytestmark = [ pytest.mark.skipif(not imports_successful(), reason='huggingface_hub not installed'), diff --git a/tests/models/test_model_names.py b/tests/models/test_model_names.py index a2b3e8130e..d8fc08ce8e 100644 --- a/tests/models/test_model_names.py +++ b/tests/models/test_model_names.py @@ -25,6 +25,12 @@ from pydantic_ai.providers.grok import GrokModelName from pydantic_ai.providers.moonshotai import MoonshotAIModelName +if not imports_successful(): + # Define placeholders so the module can be loaded for test collection + AnthropicModelName = BedrockModelName = CohereModelName = GoogleModelName = None + GroqModelName = HuggingFaceModelName = MistralModelName = OpenAIModelName = None + DeepSeekModelName = GrokModelName = MoonshotAIModelName = None + pytestmark = [ pytest.mark.skipif(not imports_successful(), reason='some model package was not installed'), pytest.mark.vcr, diff --git a/tests/models/test_openai_responses.py b/tests/models/test_openai_responses.py index 18016ccf4c..28b6ca3a68 100644 --- a/tests/models/test_openai_responses.py +++ b/tests/models/test_openai_responses.py @@ -45,10 +45,6 @@ BuiltinToolResultEvent, # pyright: ignore[reportDeprecated] ) from pydantic_ai.models import ModelRequestParameters -from pydantic_ai.models.openai import ( - OpenAIResponsesModelSettings, - _resolve_openai_image_generation_size, # pyright: ignore[reportPrivateUsage] -) from pydantic_ai.output import NativeOutput, PromptedOutput, TextOutput, ToolOutput from pydantic_ai.profiles.openai import openai_model_profile from pydantic_ai.tools import ToolDefinition @@ -68,7 +64,11 @@ from openai.types.responses.response_usage import ResponseUsage from pydantic_ai.models.anthropic import AnthropicModel, AnthropicModelSettings - from pydantic_ai.models.openai import OpenAIResponsesModel, OpenAIResponsesModelSettings + from pydantic_ai.models.openai import ( + OpenAIResponsesModel, + OpenAIResponsesModelSettings, + _resolve_openai_image_generation_size, # pyright: ignore[reportPrivateUsage] + ) from pydantic_ai.providers.anthropic import AnthropicProvider from pydantic_ai.providers.openai import OpenAIProvider diff --git a/tests/profiles/test_anthropic.py b/tests/profiles/test_anthropic.py index 365d201da4..62ed0fa404 100644 --- a/tests/profiles/test_anthropic.py +++ b/tests/profiles/test_anthropic.py @@ -22,12 +22,11 @@ from inline_snapshot import snapshot from pydantic import BaseModel, Field -from pydantic_ai.providers.anthropic import AnthropicJsonSchemaTransformer - from ..conftest import try_import with try_import() as imports_successful: from pydantic_ai.profiles.anthropic import anthropic_model_profile + from pydantic_ai.providers.anthropic import AnthropicJsonSchemaTransformer pytestmark = [ pytest.mark.skipif(not imports_successful(), reason='anthropic not installed'), diff --git a/tests/providers/test_bedrock.py b/tests/providers/test_bedrock.py index 784c390204..8d5ada9b44 100644 --- a/tests/providers/test_bedrock.py +++ b/tests/providers/test_bedrock.py @@ -19,6 +19,8 @@ from pydantic_ai.models.bedrock import LatestBedrockModelNames from pydantic_ai.providers.bedrock import BEDROCK_GEO_PREFIXES, BedrockModelProfile, BedrockProvider +if not imports_successful(): + BEDROCK_GEO_PREFIXES: tuple[str, ...] = () # type: ignore[no-redef] pytestmark = pytest.mark.skipif(not imports_successful(), reason='bedrock not installed') diff --git a/tests/test_examples.py b/tests/test_examples.py index ccf2cb3174..b18f48c642 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -149,6 +149,7 @@ def print(self, *args: Any, **kwargs: Any) -> None: mocker.patch('pydantic_ai.mcp.MCPServerSSE', return_value=MockMCPServer()) mocker.patch('pydantic_ai.mcp.MCPServerStreamableHTTP', return_value=MockMCPServer()) + mocker.patch('pydantic_ai.toolsets.fastmcp.FastMCPToolset', return_value=MockMCPServer()) mocker.patch('mcp.server.fastmcp.FastMCP') env.set('OPENAI_API_KEY', 'testing') @@ -301,6 +302,9 @@ def rich_prompt_ask(prompt: str, *_args: Any, **_kwargs: Any) -> str: class MockMCPServer(AbstractToolset[Any]): + def __init__(self, *args: Any, **kwargs: Any) -> None: + pass + @property def id(self) -> str | None: return None # pragma: no cover @@ -336,6 +340,9 @@ async def call_tool( 'What will the weather be like in Paris on Tuesday?': ToolCallPart( tool_name='weather_forecast', args={'location': 'Paris', 'forecast_date': '2030-01-01'}, tool_call_id='0001' ), + 'What is the weather in Paris?': ToolCallPart( + tool_name='get_weather_forecast', args={'location': 'Paris'}, tool_call_id='0001' + ), 'Tell me a joke.': 'Did you hear about the toothpaste scandal? They called it Colgate.', 'Tell me a different joke.': 'No.', 'Explain?': 'This is an excellent joke invented by Samuel Colvin, it needs no explanation.', @@ -807,6 +814,8 @@ async def model_logic( # noqa: C901 return ModelResponse(parts=[TextPart('The current time is 10:45 PM on April 17, 2025.')]) elif isinstance(m, ToolReturnPart) and m.tool_name == 'get_user': return ModelResponse(parts=[TextPart("The user's name is John.")]) + elif isinstance(m, ToolReturnPart) and m.tool_name == 'get_weather_forecast': + return ModelResponse(parts=[TextPart(m.content)]) elif isinstance(m, ToolReturnPart) and m.tool_name == 'get_company_logo': return ModelResponse(parts=[TextPart('The company name in the logo is "Pydantic."')]) elif isinstance(m, ToolReturnPart) and m.tool_name == 'get_document': diff --git a/tests/test_ui_web.py b/tests/test_ui_web.py index ab9fa1066e..57b17e933a 100644 --- a/tests/test_ui_web.py +++ b/tests/test_ui_web.py @@ -19,6 +19,8 @@ from pydantic_ai.builtin_tools import WebSearchTool from pydantic_ai.ui._web import create_web_app +with try_import() as openai_import_successful: + import openai # noqa: F401 # pyright: ignore[reportUnusedImport] pytestmark = [ pytest.mark.skipif(not starlette_import_successful(), reason='starlette not installed'), @@ -168,6 +170,7 @@ def test_chat_app_configure_endpoint_empty(): ) +@pytest.mark.skipif(not openai_import_successful(), reason='openai not installed') def test_chat_app_configure_preserves_chat_vs_responses(monkeypatch: pytest.MonkeyPatch): """Test that openai-chat: and openai-responses: models are kept as separate entries.""" monkeypatch.setenv('OPENAI_API_KEY', 'test-key')