Skip to content

Commit ee21cfb

Browse files
committed
- remove builtin_tools suport check for specific models, base model takes care
- swap builtin tool getters for public constans and inline checks - move agent loader and private web module into locally public scopes
1 parent b60f2ff commit ee21cfb

File tree

11 files changed

+137
-126
lines changed

11 files changed

+137
-126
lines changed

pydantic_ai_slim/pydantic_ai/_cli/__init__.py

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from pathlib import Path
1111
from typing import Any, cast
1212

13+
from pydantic import ImportString, TypeAdapter, ValidationError
1314
from typing_inspection.introspection import get_literal_values
1415

1516
from .. import __version__
@@ -83,6 +84,27 @@ def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderR
8384

8485
cli_agent = Agent()
8586

87+
_import_string_adapter: TypeAdapter[Any] = TypeAdapter(ImportString)
88+
89+
90+
def load_agent(agent_path: str) -> Agent[Any, Any] | None:
91+
"""Load an agent from module path in uvicorn style.
92+
93+
Args:
94+
agent_path: Path in format 'module:variable', e.g. 'test_agent:my_agent'
95+
96+
Returns:
97+
Agent instance or None if loading fails
98+
"""
99+
sys.path.insert(0, str(Path.cwd()))
100+
try:
101+
obj = _import_string_adapter.validate_python(agent_path)
102+
if not isinstance(obj, Agent):
103+
return None
104+
return obj # pyright: ignore[reportUnknownVariableType]
105+
except ValidationError:
106+
return None
107+
86108

87109
@cli_agent.system_prompt
88110
def cli_system_prompt() -> str:
@@ -200,9 +222,9 @@ def cli( # noqa: C901
200222
args = parser.parse_args(args_list)
201223

202224
if prog_name == 'clai' and getattr(args, 'command', None) == 'web':
203-
from ._web import _run_web_command # pyright: ignore[reportPrivateUsage]
225+
from .web import run_web_command
204226

205-
return _run_web_command(
227+
return run_web_command(
206228
agent_path=args.agent,
207229
host=args.host,
208230
port=args.port,
@@ -225,9 +247,7 @@ def cli( # noqa: C901
225247

226248
agent: Agent[None, str] = cli_agent
227249
if args.agent:
228-
from ._web import _load_agent # pyright: ignore[reportPrivateUsage]
229-
230-
loaded = _load_agent(args.agent)
250+
loaded = load_agent(args.agent)
231251
if loaded is None:
232252
console.print(f'[red]Error: Could not load agent from {args.agent}[/red]')
233253
return 1
Lines changed: 21 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,26 @@
1-
"""CLI command for launching a web chat UI for agents."""
2-
31
from __future__ import annotations
42

5-
import sys
6-
from pathlib import Path
7-
from typing import Any
8-
9-
from pydantic import ImportString, TypeAdapter, ValidationError
3+
from pydantic import ValidationError
104
from rich.console import Console
115

126
from pydantic_ai import Agent
13-
from pydantic_ai.builtin_tools import AbstractBuiltinTool, get_builtin_tool_cls
7+
from pydantic_ai.builtin_tools import (
8+
BUILTIN_TOOL_TYPES,
9+
DEPRECATED_BUILTIN_TOOL_TYPES,
10+
AbstractBuiltinTool,
11+
)
1412
from pydantic_ai.mcp import MCPServerSSE, MCPServerStdio, MCPServerStreamableHTTP, load_mcp_servers
1513
from pydantic_ai.models import infer_model
1614
from pydantic_ai.ui._web import create_web_app
1715

18-
__all__ = ['_run_web_command']
19-
20-
_import_string_adapter: TypeAdapter[Any] = TypeAdapter(ImportString)
16+
from . import load_agent
2117

18+
# Tools that require configuration and cannot be enabled via CLI
19+
# (includes deprecated tools plus tools needing config like mcp_server and memory)
20+
UNSUPPORTED_CLI_TOOLS = DEPRECATED_BUILTIN_TOOL_TYPES | frozenset({'mcp_server', 'memory'})
2221

23-
def _load_agent(agent_path: str) -> Agent | None:
24-
"""Load an agent from module path in uvicorn style.
2522

26-
Args:
27-
agent_path: Path in format 'module:variable', e.g. 'test_agent:my_agent'
28-
29-
Returns:
30-
Agent instance or None if loading fails
31-
"""
32-
sys.path.insert(0, str(Path.cwd()))
33-
try:
34-
obj = _import_string_adapter.validate_python(agent_path)
35-
if not isinstance(obj, Agent):
36-
return None
37-
return obj # pyright: ignore[reportUnknownVariableType]
38-
except ValidationError:
39-
return None
40-
41-
42-
def _run_web_command( # noqa: C901
23+
def run_web_command( # noqa: C901
4324
agent_path: str | None = None,
4425
host: str = '127.0.0.1',
4526
port: int = 7932,
@@ -66,7 +47,7 @@ def _run_web_command( # noqa: C901
6647
console = Console()
6748

6849
if agent_path:
69-
agent = _load_agent(agent_path)
50+
agent = load_agent(agent_path)
7051
if agent is None:
7152
console.print(f'[red]Error: Could not load agent from {agent_path}[/red]')
7253
return 1
@@ -92,12 +73,14 @@ def _run_web_command( # noqa: C901
9273
# then CLI tools
9374
if tools:
9475
for tool_id in tools:
95-
tool_cls = get_builtin_tool_cls(tool_id)
96-
if tool_cls is None or tool_id in ('url_context', 'mcp_server'):
97-
console.print(f'[yellow]Warning: Unknown tool "{tool_id}", skipping[/yellow]')
76+
if tool_id in UNSUPPORTED_CLI_TOOLS:
77+
console.print(
78+
f'[yellow]Warning: "{tool_id}" requires configuration and cannot be enabled via CLI, skipping[/yellow]'
79+
)
9880
continue
99-
if tool_id == 'memory':
100-
console.print('[yellow]Warning: MemoryTool requires agent to have memory configured, skipping[/yellow]')
81+
tool_cls = BUILTIN_TOOL_TYPES.get(tool_id)
82+
if tool_cls is None:
83+
console.print(f'[yellow]Warning: Unknown tool "{tool_id}", skipping[/yellow]')
10184
continue
10285
all_tool_instances.append(tool_cls())
10386

@@ -120,8 +103,8 @@ def _run_web_command( # noqa: C901
120103
app = create_web_app(
121104
agent,
122105
models=models,
123-
builtin_tools=all_tool_instances if all_tool_instances else None,
124-
toolsets=mcp_servers if mcp_servers else None,
106+
builtin_tools=all_tool_instances or None,
107+
toolsets=mcp_servers or None,
125108
)
126109

127110
agent_desc = agent_path if agent_path else 'generic agent'

pydantic_ai_slim/pydantic_ai/builtin_tools.py

Lines changed: 15 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,19 @@
1818
'ImageGenerationTool',
1919
'MemoryTool',
2020
'MCPServerTool',
21-
'get_builtin_tool_ids',
21+
'BUILTIN_TOOL_TYPES',
22+
'DEPRECATED_BUILTIN_TOOL_TYPES',
2223
'get_builtin_tool_types',
23-
'get_builtin_tool_cls',
2424
)
2525

26-
_BUILTIN_TOOL_TYPES: dict[str, type[AbstractBuiltinTool]] = {}
26+
BUILTIN_TOOL_TYPES: dict[str, type[AbstractBuiltinTool]] = {}
27+
"""Registry of all builtin tool types, keyed by their kind string.
28+
29+
This dict is populated automatically via `__init_subclass__` when tool classes are defined.
30+
"""
31+
32+
DEPRECATED_BUILTIN_TOOL_TYPES: frozenset[str] = frozenset({'url_context'})
33+
"""Set of deprecated builtin tool IDs that should not be offered in new UIs."""
2734

2835

2936
@dataclass(kw_only=True)
@@ -56,7 +63,7 @@ def label(self) -> str:
5663

5764
def __init_subclass__(cls, **kwargs: Any) -> None:
5865
super().__init_subclass__(**kwargs)
59-
_BUILTIN_TOOL_TYPES[cls.kind] = cls
66+
BUILTIN_TOOL_TYPES[cls.kind] = cls
6067

6168
@classmethod
6269
def __get_pydantic_core_schema__(
@@ -65,7 +72,7 @@ def __get_pydantic_core_schema__(
6572
if cls is not AbstractBuiltinTool:
6673
return handler(cls)
6774

68-
tools = _BUILTIN_TOOL_TYPES.values()
75+
tools = BUILTIN_TOOL_TYPES.values()
6976
if len(tools) == 1: # pragma: no cover
7077
tools_type = next(iter(tools))
7178
else:
@@ -417,20 +424,7 @@ def _tool_discriminator(tool_data: dict[str, Any] | AbstractBuiltinTool) -> str:
417424
return tool_data.kind
418425

419426

420-
def get_builtin_tool_ids() -> frozenset[str]: # pragma: no cover
421-
"""Get the set of all builtin tool IDs (excluding deprecated tools like url_context)."""
422-
return frozenset(_BUILTIN_TOOL_TYPES.keys() - {'url_context'})
423-
424-
427+
# This function exists primarily as a clean default_factory for ModelProfile.supported_builtin_tools
425428
def get_builtin_tool_types() -> frozenset[type[AbstractBuiltinTool]]:
426-
"""Get the set of all builtin tool types (excluding deprecated tools like UrlContextTool)."""
427-
return frozenset(cls for kind, cls in _BUILTIN_TOOL_TYPES.items() if kind != 'url_context')
428-
429-
430-
def get_builtin_tool_cls(tool_id: str) -> type[AbstractBuiltinTool] | None:
431-
"""Get a builtin tool class by its ID.
432-
433-
Args:
434-
tool_id: The tool ID (e.g., 'web_search', 'code_execution')
435-
"""
436-
return _BUILTIN_TOOL_TYPES.get(tool_id)
429+
"""Get the set of all builtin tool types (excluding deprecated tools)."""
430+
return frozenset(cls for kind, cls in BUILTIN_TOOL_TYPES.items() if kind not in DEPRECATED_BUILTIN_TOOL_TYPES)

pydantic_ai_slim/pydantic_ai/models/anthropic.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,7 @@ def system(self) -> str:
263263

264264
@classmethod
265265
def supported_builtin_tools(cls) -> frozenset[type[AbstractBuiltinTool]]:
266-
"""Return the set of builtin tool types this model can handle."""
266+
"""The set of builtin tool types this model can handle."""
267267
return frozenset({WebSearchTool, CodeExecutionTool, WebFetchTool, MemoryTool, MCPServerTool})
268268

269269
async def request(

pydantic_ai_slim/pydantic_ai/models/bedrock.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
usage,
4141
)
4242
from pydantic_ai._run_context import RunContext
43-
from pydantic_ai.exceptions import ModelAPIError, ModelHTTPError, UserError
43+
from pydantic_ai.exceptions import ModelAPIError, ModelHTTPError
4444
from pydantic_ai.models import Model, ModelRequestParameters, StreamedResponse, download_item
4545
from pydantic_ai.providers import Provider, infer_provider
4646
from pydantic_ai.providers.bedrock import BedrockModelProfile
@@ -433,10 +433,6 @@ async def _messages_create(
433433
if tool_config:
434434
params['toolConfig'] = tool_config
435435

436-
if model_request_parameters.builtin_tools: # pragma: no cover
437-
# this check is done in the base Model class - leave this as a placeholder for when Bedrock supports built-in tools
438-
raise UserError('Bedrock does not support built-in tools')
439-
440436
# Bedrock supports a set of specific extra parameters
441437
if model_settings:
442438
if guardrail_config := model_settings.get('bedrock_guardrail_config', None):

pydantic_ai_slim/pydantic_ai/models/function.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -204,9 +204,9 @@ def system(self) -> str:
204204
@classmethod
205205
def supported_builtin_tools(cls) -> frozenset[type[AbstractBuiltinTool]]:
206206
"""FunctionModel supports all builtin tools for testing flexibility."""
207-
from ..builtin_tools import get_builtin_tool_types
207+
from ..builtin_tools import BUILTIN_TOOL_TYPES, DEPRECATED_BUILTIN_TOOL_TYPES
208208

209-
return get_builtin_tool_types()
209+
return frozenset(cls for kind, cls in BUILTIN_TOOL_TYPES.items() if kind not in DEPRECATED_BUILTIN_TOOL_TYPES)
210210

211211

212212
@dataclass(frozen=True, kw_only=True)

pydantic_ai_slim/pydantic_ai/models/huggingface.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
from .._run_context import RunContext
1313
from .._thinking_part import split_content_into_text_and_thinking
1414
from .._utils import guard_tool_call_id as _guard_tool_call_id, now_utc as _now_utc
15-
from ..exceptions import UserError
1615
from ..messages import (
1716
AudioUrl,
1817
BinaryContent,
@@ -229,10 +228,6 @@ async def _completions_create(
229228
else:
230229
tool_choice = 'auto'
231230

232-
if model_request_parameters.builtin_tools: # pragma: no cover
233-
# this check is done in the base Model class - leave this as a placeholder for when Bedrock supports built-in tools
234-
raise UserError('HuggingFace does not support built-in tools')
235-
236231
hf_messages = await self._map_messages(messages, model_request_parameters)
237232

238233
try:

pydantic_ai_slim/pydantic_ai/models/mistral.py

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from .. import ModelHTTPError, UnexpectedModelBehavior, _utils
1414
from .._run_context import RunContext
1515
from .._utils import generate_tool_call_id as _generate_tool_call_id, now_utc as _now_utc, number_to_datetime
16-
from ..exceptions import ModelAPIError, UserError
16+
from ..exceptions import ModelAPIError
1717
from ..messages import (
1818
BinaryContent,
1919
BuiltinToolCallPart,
@@ -224,10 +224,6 @@ async def _completions_create(
224224
"""Make a non-streaming request to the model."""
225225
# TODO(Marcelo): We need to replace the current MistralAI client to use the beta client.
226226
# See https://docs.mistral.ai/agents/connectors/websearch/ to support web search.
227-
if model_request_parameters.builtin_tools: # pragma: no cover
228-
# this check is done in the base Model class - leave this as a placeholder for when Bedrock supports built-in tools
229-
raise UserError('Mistral does not support built-in tools')
230-
231227
try:
232228
response = await self.client.chat.complete_async(
233229
model=str(self._model_name),
@@ -264,9 +260,6 @@ async def _stream_completions_create(
264260

265261
# TODO(Marcelo): We need to replace the current MistralAI client to use the beta client.
266262
# See https://docs.mistral.ai/agents/connectors/websearch/ to support web search.
267-
if model_request_parameters.builtin_tools: # pragma: no cover
268-
raise UserError('Mistral does not support built-in tools')
269-
270263
if model_request_parameters.function_tools:
271264
# Function Calling
272265
response = await self.client.chat.stream_async(

pydantic_ai_slim/pydantic_ai/models/test.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -161,9 +161,9 @@ def system(self) -> str:
161161
@classmethod
162162
def supported_builtin_tools(cls) -> frozenset[type[AbstractBuiltinTool]]:
163163
"""TestModel supports all builtin tools for testing flexibility."""
164-
from ..builtin_tools import get_builtin_tool_types
164+
from ..builtin_tools import BUILTIN_TOOL_TYPES, DEPRECATED_BUILTIN_TOOL_TYPES
165165

166-
return get_builtin_tool_types()
166+
return frozenset(c for kind, c in BUILTIN_TOOL_TYPES.items() if kind not in DEPRECATED_BUILTIN_TOOL_TYPES)
167167

168168
def gen_tool_args(self, tool_def: ToolDefinition) -> Any:
169169
return _JsonSchemaTestData(tool_def.parameters_json_schema, self.seed).generate()

0 commit comments

Comments
 (0)