Skip to content

Commit 85f63bc

Browse files
committed
make _web a private module
1 parent 558985f commit 85f63bc

File tree

8 files changed

+194
-320
lines changed

8 files changed

+194
-320
lines changed

clai/clai/web/__init__.py

Lines changed: 0 additions & 5 deletions
This file was deleted.

clai/clai/web/cli.py

Lines changed: 0 additions & 141 deletions
This file was deleted.

pydantic_ai_slim/pydantic_ai/_cli.py renamed to pydantic_ai_slim/pydantic_ai/_cli/__init__.py

Lines changed: 15 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,15 @@
1010
from pathlib import Path
1111
from typing import Any, cast
1212

13-
from pydantic import BaseModel, ImportString, ValidationError
1413
from typing_inspection.introspection import get_literal_values
1514

16-
from . import __version__
17-
from ._run_context import AgentDepsT
18-
from .agent import AbstractAgent, Agent
19-
from .exceptions import UserError
20-
from .messages import ModelMessage, ModelResponse
21-
from .models import KnownModelName, infer_model
22-
from .output import OutputDataT
15+
from .. import __version__
16+
from .._run_context import AgentDepsT
17+
from ..agent import AbstractAgent, Agent
18+
from ..exceptions import UserError
19+
from ..messages import ModelMessage, ModelResponse
20+
from ..models import KnownModelName, infer_model
21+
from ..output import OutputDataT
2322

2423
try:
2524
import argcomplete
@@ -46,32 +45,6 @@
4645
__all__ = 'cli', 'cli_exit'
4746

4847

49-
class _AgentLoader(BaseModel):
50-
"""Helper model for loading agents using Pydantic ImportString."""
51-
52-
agent: ImportString # type: ignore[valid-type]
53-
54-
55-
def _load_agent(agent_path: str) -> Agent | None:
56-
"""Load an agent from module path in uvicorn style.
57-
58-
Args:
59-
agent_path: Path in format 'module:variable', e.g. 'test_agent:my_agent'
60-
61-
Returns:
62-
Agent instance or None if loading fails
63-
"""
64-
sys.path.insert(0, str(Path.cwd()))
65-
try:
66-
loader = _AgentLoader(agent=agent_path)
67-
agent = loader.agent # pyright: ignore[reportUnknownVariableType,reportUnknownMemberType]
68-
if not isinstance(agent, Agent):
69-
return None
70-
return agent # pyright: ignore[reportUnknownVariableType]
71-
except ValidationError:
72-
return None
73-
74-
7548
PYDANTIC_AI_HOME = Path.home() / '.pydantic-ai'
7649
"""The home directory for Pydantic AI CLI.
7750
@@ -198,18 +171,17 @@ def cli( # noqa: C901
198171
'--model',
199172
action='append',
200173
dest='models',
201-
help='Model to make available (can be repeated, e.g., -m gpt-5 -m claude-sonnet-4-5). '
202-
'Format: "provider:model_name" (e.g., "openai:gpt-5"). '
203-
'Prefix-less names allowed for: gpt*, o1, o3, claude*, gemini*. '
204-
'First model is default, subsequent ones are UI options.',
174+
help='Model to make available (can be repeated, e.g., -m openai:gpt-5 -m anthropic:claude-sonnet-4-5). '
175+
'Format: "provider:model_name". Prefix-less names (gpt-5, claude-sonnet-4-5, gemini-2.5-pro) '
176+
'auto-infer provider. First model is preselected in UI; additional models appear as options.',
205177
)
206178
web_parser.add_argument(
207179
'-t',
208180
'--tool',
209181
action='append',
210182
dest='tools',
211183
help='Builtin tool to enable (can be repeated, e.g., -t web_search -t code_execution). '
212-
'Available: web_search, code_execution, image_generation, web_fetch, memory.',
184+
'Available: web_search, code_execution, image_generation, web_fetch.',
213185
)
214186
web_parser.add_argument(
215187
'-i',
@@ -228,9 +200,9 @@ def cli( # noqa: C901
228200
args = parser.parse_args(args_list)
229201

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

233-
return run_web_command(
205+
return _run_web_command(
234206
agent_path=args.agent,
235207
host=args.host,
236208
port=args.port,
@@ -253,6 +225,8 @@ def cli( # noqa: C901
253225

254226
agent: Agent[None, str] = cli_agent
255227
if args.agent:
228+
from ._web import _load_agent # pyright: ignore[reportPrivateUsage]
229+
256230
loaded = _load_agent(args.agent)
257231
if loaded is None:
258232
console.print(f'[red]Error: Could not load agent from {args.agent}[/red]')
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
"""CLI command for launching a web chat UI for agents."""
2+
3+
from __future__ import annotations
4+
5+
import sys
6+
from pathlib import Path
7+
8+
from pydantic import BaseModel, ImportString, ValidationError
9+
from rich.console import Console
10+
11+
from pydantic_ai import Agent
12+
from pydantic_ai.builtin_tools import AbstractBuiltinTool, get_builtin_tool_cls
13+
from pydantic_ai.models import infer_model
14+
from pydantic_ai.ui._web import create_web_app, load_mcp_server_tools
15+
16+
__all__ = ['_run_web_command']
17+
18+
19+
class _AgentLoader(BaseModel):
20+
"""Helper model for loading agents using Pydantic ImportString."""
21+
22+
agent: ImportString # type: ignore[valid-type]
23+
24+
25+
def _load_agent(agent_path: str) -> Agent | None:
26+
"""Load an agent from module path in uvicorn style.
27+
28+
Args:
29+
agent_path: Path in format 'module:variable', e.g. 'test_agent:my_agent'
30+
31+
Returns:
32+
Agent instance or None if loading fails
33+
"""
34+
sys.path.insert(0, str(Path.cwd()))
35+
try:
36+
loader = _AgentLoader(agent=agent_path)
37+
agent = loader.agent # pyright: ignore[reportUnknownVariableType,reportUnknownMemberType]
38+
if not isinstance(agent, Agent):
39+
return None
40+
return agent # pyright: ignore[reportUnknownVariableType]
41+
except ValidationError:
42+
return None
43+
44+
45+
def _run_web_command( # noqa: C901
46+
agent_path: str | None = None,
47+
host: str = '127.0.0.1',
48+
port: int = 7932,
49+
models: list[str] | None = None,
50+
tools: list[str] | None = None,
51+
instructions: str | None = None,
52+
mcp: str | None = None,
53+
) -> int:
54+
"""Run the web command to serve an agent via web UI.
55+
56+
Args:
57+
agent_path: Agent path in 'module:variable' format. If None, creates generic agent.
58+
host: Host to bind the server to.
59+
port: Port to bind the server to.
60+
models: List of model strings (e.g., ['openai:gpt-5', 'claude-sonnet-4-5']).
61+
tools: List of builtin tool IDs (e.g., ['web_search', 'code_execution']).
62+
instructions: System instructions for generic agent.
63+
mcp: Path to JSON file with MCP server configurations.
64+
"""
65+
console = Console()
66+
67+
if agent_path:
68+
agent = _load_agent(agent_path)
69+
if agent is None:
70+
console.print(f'[red]Error: Could not load agent from {agent_path}[/red]')
71+
return 1
72+
else:
73+
agent = Agent()
74+
75+
if instructions:
76+
77+
@agent.system_prompt
78+
def system_prompt() -> str: # pyright: ignore[reportUnusedFunction]
79+
return instructions # pragma: no cover
80+
81+
if agent.model is None and not models:
82+
console.print('[red]Error: At least one model (-m) is required when agent has no model[/red]')
83+
return 1
84+
85+
# If no CLI models provided but agent has a model, use agent's model
86+
if not models and agent.model is not None:
87+
resolved_model = infer_model(agent.model)
88+
models = [f'{resolved_model.system}:{resolved_model.model_name}']
89+
90+
# Collect builtin tools: agent's own + CLI-provided
91+
all_tool_instances: list[AbstractBuiltinTool] = []
92+
93+
# Add agent's own builtin tools first (these are always enabled)
94+
all_tool_instances.extend(agent._builtin_tools) # pyright: ignore[reportPrivateUsage]
95+
96+
# Parse and add CLI tools
97+
if tools:
98+
for tool_id in tools:
99+
tool_cls = get_builtin_tool_cls(tool_id)
100+
if tool_cls is None or tool_id in ('url_context', 'mcp_server'):
101+
console.print(f'[yellow]Warning: Unknown tool "{tool_id}", skipping[/yellow]')
102+
continue
103+
if tool_id == 'memory':
104+
console.print('[yellow]Warning: MemoryTool requires agent to have memory configured, skipping[/yellow]')
105+
continue
106+
all_tool_instances.append(tool_cls())
107+
108+
# Load MCP server tools if specified
109+
if mcp:
110+
try:
111+
mcp_tools = load_mcp_server_tools(mcp)
112+
all_tool_instances.extend(mcp_tools)
113+
console.print(f'[dim]Loaded {len(mcp_tools)} MCP server(s) from {mcp}[/dim]')
114+
except FileNotFoundError as e:
115+
console.print(f'[red]Error: {e}[/red]')
116+
return 1
117+
except ValidationError as e:
118+
console.print(f'[red]Error parsing MCP config: {e}[/red]')
119+
return 1
120+
except ValueError as e: # pragma: no cover
121+
console.print(f'[red]Error: {e}[/red]')
122+
return 1
123+
124+
app = create_web_app(
125+
agent,
126+
models=models,
127+
builtin_tools=all_tool_instances if all_tool_instances else None,
128+
)
129+
130+
agent_desc = agent_path if agent_path else 'generic agent'
131+
console.print(f'\n[green]Starting chat UI for {agent_desc}...[/green]')
132+
console.print(f'Open your browser at: [link=http://{host}:{port}]http://{host}:{port}[/link]')
133+
console.print('[dim]Press Ctrl+C to stop the server[/dim]\n')
134+
135+
try:
136+
import uvicorn
137+
138+
uvicorn.run(app, host=host, port=port)
139+
return 0 # pragma: no cover
140+
except KeyboardInterrupt: # pragma: no cover
141+
console.print('\n[dim]Server stopped.[/dim]')
142+
return 0
143+
except ImportError: # pragma: no cover
144+
console.print('[red]Error: uvicorn is required to run the chat UI[/red]')
145+
console.print('[dim]Install it with: pip install uvicorn[/dim]')
146+
return 1
147+
except Exception as e: # pragma: no cover
148+
console.print(f'[red]Error starting server: {e}[/red]')
149+
return 1

0 commit comments

Comments
 (0)