Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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