Skip to content
Open
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
1cfa48e
add Agent.to_web() method and web chat UI module #3295
dsfaccini Nov 17, 2025
d83caa5
add "web" group
dsfaccini Nov 17, 2025
bfffd4b
try import create_chat_app
dsfaccini Nov 17, 2025
055e120
fix tests to run on CI
dsfaccini Nov 17, 2025
6bc8b16
fix example to use tool_plain
dsfaccini Nov 17, 2025
32f7e1d
add clai web - tested with uv run clai web
dsfaccini Nov 18, 2025
cf0e177
wip: remove agent discovery and fix tests
dsfaccini Nov 20, 2025
e7f44eb
rename command
dsfaccini Nov 20, 2025
c4ffde3
rename function
dsfaccini Nov 20, 2025
0595c27
- define builtin tool ids
dsfaccini Nov 21, 2025
0d24941
fix tests
dsfaccini Nov 21, 2025
e5b30c2
- update CLI commands and improve agent loading mechanism
dsfaccini Nov 21, 2025
f2dd19a
Merge branch 'main' into clai-chat
dsfaccini Nov 21, 2025
8ca7a27
import sorting
dsfaccini Nov 21, 2025
fa3bb5f
more import sorting
dsfaccini Nov 21, 2025
e45c93f
covergae?
dsfaccini Nov 22, 2025
f90b570
Merge upstream/main into clai-chat
dsfaccini Nov 22, 2025
da8032c
Merge upstream/main into clai-chat
dsfaccini Nov 26, 2025
0ab07b6
- remove agent_options - add supported_builtin_tools - swap fastapi f…
dsfaccini Nov 26, 2025
191897f
refactor from --web to web, adjust flags, add tests, update docs
dsfaccini Nov 26, 2025
1faae3f
remove clai import on test
dsfaccini Nov 26, 2025
8423e89
coverage
dsfaccini Nov 26, 2025
a32c15a
- consolidate web docs in own doc
dsfaccini Nov 27, 2025
91f9533
remove memory cache and swap prints for consoles
dsfaccini Nov 27, 2025
441d6a0
use snapshots where it makes sense
dsfaccini Nov 27, 2025
558985f
Merge branch 'main' into clai-chat
dsfaccini Nov 27, 2025
85f63bc
make _web a private module
dsfaccini Nov 28, 2025
cd91e81
move supported_builtin_tools to abstract tool set type
dsfaccini Nov 28, 2025
a132f26
warning about memory tool
dsfaccini Nov 29, 2025
943c7a8
move name formatting to model method
dsfaccini Nov 29, 2025
831bdf3
move to toolsets
dsfaccini Nov 29, 2025
125c059
update docstring
dsfaccini Nov 29, 2025
025f1b5
take agent's own model into account
dsfaccini Nov 29, 2025
0701b22
bit of docs
dsfaccini Nov 29, 2025
40b5c78
move builtin tool support to profile and move check to base model class
dsfaccini Nov 29, 2025
0673277
fix test profile classes to avoid ci api key error
dsfaccini Nov 29, 2025
974bac2
Merge branch 'main' into clai-chat
dsfaccini Nov 29, 2025
4b58307
coverage
dsfaccini Nov 29, 2025
74f09e9
add anthropic prefix
dsfaccini Dec 1, 2025
897e4e4
fix naming
dsfaccini Dec 1, 2025
10d32fe
remove get agent utility and make supported builtin tools a classmethod
dsfaccini Dec 1, 2025
1c93cf0
remove test
dsfaccini Dec 1, 2025
b60f2ff
Add screenshot link for web chat UI
dsfaccini Dec 3, 2025
ee21cfb
- remove builtin_tools suport check for specific models, base model t…
dsfaccini Dec 3, 2025
cf1da90
support double instructions
dsfaccini Dec 3, 2025
372db8b
remove program check, tie tool arg options to constant and set up aut…
dsfaccini Dec 4, 2025
32cc5c5
merge
dsfaccini Dec 4, 2025
48094d2
add support for model instances, deps and settings
dsfaccini Dec 4, 2025
cd1e683
fix tests after cli adjustments
dsfaccini Dec 4, 2025
c1ba780
separate chat command
dsfaccini Dec 4, 2025
6984dfd
Merge branch 'main' into clai-chat
dsfaccini Dec 4, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 69 additions & 1 deletion clai/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,73 @@ Either way, running `clai` will start an interactive session where you can chat
- `/multiline`: Toggle multiline input mode (use Ctrl+D to submit)
- `/cp`: Copy the last response to clipboard

## Web Chat UI

Launch a web-based chat interface for your agent:

```bash
clai --web --agent module:agent_variable
```

For example, if you have an agent defined in `my_agent.py`:

```python
from pydantic_ai import Agent

my_agent = Agent('openai:gpt-5', system_prompt='You are a helpful assistant.')
```

Launch the web UI with:

```bash
clai --web --agent my_agent:my_agent
```

This will start a web server (default: http://127.0.0.1:8000) with a chat interface for your agent.

### Web Command Options

- `--host`: Host to bind the server to (default: 127.0.0.1)
- `--port`: Port to bind the server to (default: 8000)
- `--config`: Path to custom `agent_options.py` config file
- `--no-auto-config`: Disable auto-discovery of `agent_options.py` in current directory

### Configuring Models and Tools

You can customize which AI models and builtin tools are available in the web UI by creating an `agent_options.py` file. For example:

```python
from pydantic_ai.ui.web import AIModel, BuiltinToolDef
from pydantic_ai.builtin_tools import WebSearchTool

models = [
AIModel(id='openai:gpt-5', name='GPT 5', builtin_tools=['web_search']),
]

builtin_tool_definitions = [
BuiltinToolDef(id='web_search', name='Web Search', tool=WebSearchTool()),
]
```

See the [default configuration](https://github.com/pydantic/pydantic-ai/blob/main/pydantic_ai_slim/pydantic_ai/ui/web/agent_options.py) for more examples.

If an `agent_options.py` file exists in your current directory, it will be automatically loaded when you run `clai --web`. You can also specify a custom config path with `--config`.

You can also launch the web UI directly from an `Agent` instance using `Agent.to_web()`:

```python
from pydantic_ai import Agent

agent = Agent('openai:gpt-5')
app = agent.to_web() # Returns a FastAPI application
```

## Help

```
usage: clai [-h] [-m [MODEL]] [-a AGENT] [-l] [-t [CODE_THEME]] [--no-stream] [--version] [prompt]
usage: clai [-h] [-m [MODEL]] [-a AGENT] [-l] [-t [CODE_THEME]] [--no-stream] [--version] [--web] [--host HOST] [--port PORT] [--config CONFIG]
[--no-auto-config]
[prompt]

Pydantic AI CLI v...

Expand All @@ -78,4 +141,9 @@ options:
Which colors to use for code, can be "dark", "light" or any theme from pygments.org/styles/. Defaults to "dark" which works well on dark terminals.
--no-stream Disable streaming from the model
--version Show version and exit
--web Launch web chat UI for the agent (requires --agent)
--host HOST Host to bind the server to (default: 127.0.0.1)
--port PORT Port to bind the server to (default: 8000)
--config CONFIG Path to agent_options.py config file (overrides auto-discovery)
--no-auto-config Disable auto-discovery of agent_options.py in current directory
```
5 changes: 5 additions & 0 deletions clai/clai/web/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""Chat UI module for clai."""

from .cli import run_web_command

__all__ = ['run_web_command']
127 changes: 127 additions & 0 deletions clai/clai/web/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
"""CLI command for launching a web chat UI for discovered agents."""

from __future__ import annotations

import importlib.util
import sys
from pathlib import Path

from pydantic import BaseModel, ImportString, ValidationError

from pydantic_ai import Agent
from pydantic_ai.ui.web import AIModel, BuiltinToolDef, create_web_app


def load_agent_options(
config_path: Path,
) -> tuple[list[AIModel] | None, list[BuiltinToolDef] | None]:
"""Load agent options from a config file.
Args:
config_path: Path to the config file (e.g., agent_options.py)
"""
if not config_path.exists():
return None, None

try:
spec = importlib.util.spec_from_file_location('agent_options_config', config_path)
if spec is None or spec.loader is None:
print(f'Warning: Could not load config from {config_path}')
return None, None

module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)

models = getattr(module, 'models', None)
builtin_tool_defs = getattr(module, 'builtin_tool_definitions', None)

return models, builtin_tool_defs

except Exception as e:
print(f'Warning: Error loading config from {config_path}: {e}')
return None, None


class _AgentLoader(BaseModel):
"""Helper model for loading agents using Pydantic's ImportString."""

agent: ImportString # type: ignore[valid-type]


def load_agent(agent_path: str) -> Agent | None:
"""Load an agent from module path in uvicorn style.
Args:
agent_path: Path in format 'module:variable', e.g. 'test_agent:my_agent'
Returns:
Agent instance or None if loading fails
"""
sys.path.insert(0, str(Path.cwd()))

try:
loader = _AgentLoader(agent=agent_path)
agent = loader.agent # type: ignore[reportUnknownVariableType]

if not isinstance(agent, Agent):
print(f'Error: {agent_path} is not an Agent instance')
return None

return agent # pyright: ignore[reportUnknownVariableType]

except ValidationError as e:
print(f'Error loading agent from {agent_path}: {e}')
return None


def run_web_command(
agent_path: str,
host: str = '127.0.0.1',
port: int = 8000,
config_path: Path | None = None,
auto_config: bool = True,
) -> int:
"""Run the chat command to serve an agent via web UI.
Args:
agent_path: Agent path in 'module:variable' format, e.g. 'test_agent:my_agent'
host: Host to bind the server to
port: Port to bind the server to
config_path: Path to agent_options.py config file
auto_config: Auto-discover agent_options.py in current directory
"""
agent = load_agent(agent_path)
if agent is None:
return 1

models, builtin_tool_defs = None, None
if config_path:
print(f'Loading config from {config_path}...')
models, builtin_tool_defs = load_agent_options(config_path)
elif auto_config:
default_config = Path.cwd() / 'agent_options.py'
if default_config.exists():
print(f'Found config file: {default_config}')
models, builtin_tool_defs = load_agent_options(default_config)

app = create_web_app(agent, models=models, builtin_tool_defs=builtin_tool_defs)

print(f'\nStarting chat UI for {agent_path}...')
print(f'Open your browser at: http://{host}:{port}')
print('Press Ctrl+C to stop the server\n')

try:
import uvicorn

uvicorn.run(app, host=host, port=port)
return 0
except KeyboardInterrupt:
print('\nServer stopped.')
return 0
except ImportError:
print('Error: uvicorn is required to run the chat UI')
print('Install it with: pip install uvicorn')
return 1
except Exception as e:
print(f'Error starting server: {e}')
return 1
91 changes: 88 additions & 3 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,91 @@ Either way, running `clai` will start an interactive session where you can chat
- `/multiline`: Toggle multiline input mode (use Ctrl+D to submit)
- `/cp`: Copy the last response to clipboard

### Web Chat UI

Launch a web-based chat interface for your agent:

```bash
clai --web --agent module:agent_variable
```

For example, if you have an agent defined in `my_agent.py`:

```python
from pydantic_ai import Agent

my_agent = Agent('openai:gpt-5', system_prompt='You are a helpful assistant.')
```

Launch the web UI with:

```bash
clai --web --agent my_agent:my_agent
```

This will start a web server (default: http://127.0.0.1:8000) with a chat interface for your agent.

#### Web Command Options

- `--host`: Host to bind the server to (default: 127.0.0.1)
- `--port`: Port to bind the server to (default: 8000)
- `--config`: Path to custom `agent_options.py` config file
- `--no-auto-config`: Disable auto-discovery of `agent_options.py` in current directory

#### Configuring Models and Tools

You can customize which AI models and builtin tools are available in the web UI by creating an `agent_options.py` file:

```python title="agent_options.py"
from pydantic_ai.builtin_tools import CodeExecutionTool, WebSearchTool
from pydantic_ai.ui.web import AIModel, BuiltinToolDef

models = [
AIModel(
id='openai:gpt-5',
name='GPT 5',
builtin_tools=['web_search', 'code_execution'],
),
AIModel(
id='anthropic:claude-sonnet-4-5',
name='Claude Sonnet 4.5',
builtin_tools=['web_search'],
),
]

builtin_tool_definitions = [
BuiltinToolDef(
id='web_search',
name='Web Search',
tool=WebSearchTool(),
),
BuiltinToolDef(
id='code_execution',
name='Code Execution',
tool=CodeExecutionTool(),
),
]
```

If an `agent_options.py` file exists in your current directory, it will be automatically loaded when you run `clai --web`. You can also specify a custom config path with `--config`.

You can also launch the web UI directly from an `Agent` instance using [`Agent.to_web()`][pydantic_ai.Agent.to_web]:

```python
from pydantic_ai import Agent

agent = Agent('openai:gpt-5')
app = agent.to_web() # Returns a FastAPI application
```

The returned FastAPI app can be run with your preferred ASGI server (uvicorn, hypercorn, etc.):

```bash
# If you saved the code above in my_agent.py and created an app variable:
# app = agent.to_web()
uvicorn my_agent:app --host 0.0.0.0 --port 8080
```

### Help

To get help on the CLI, use the `--help` flag:
Expand All @@ -73,7 +158,7 @@ You can specify a custom agent using the `--agent` flag with a module path and v
```python {title="custom_agent.py" test="skip"}
from pydantic_ai import Agent

agent = Agent('openai:gpt-5', instructions='You always respond in Italian.')
agent = Agent('openai:gpt-5', system_prompt='You always respond in Italian.')
```

Then run:
Expand All @@ -92,7 +177,7 @@ Additionally, you can directly launch CLI mode from an `Agent` instance using `A
```python {title="agent_to_cli_sync.py" test="skip" hl_lines=4}
from pydantic_ai import Agent

agent = Agent('openai:gpt-5', instructions='You always respond in Italian.')
agent = Agent('openai:gpt-5', system_prompt='You always respond in Italian.')
agent.to_cli_sync()
```

Expand All @@ -101,7 +186,7 @@ You can also use the async interface with `Agent.to_cli()`:
```python {title="agent_to_cli.py" test="skip" hl_lines=6}
from pydantic_ai import Agent

agent = Agent('openai:gpt-5', instructions='You always respond in Italian.')
agent = Agent('openai:gpt-5', system_prompt='You always respond in Italian.')

async def main():
await agent.to_cli()
Expand Down
Loading