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
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,32 @@ python myagent.py start

Runs the agent with production-ready optimizations.

### Checking your setup

```shell
python agent.py doctor
```

Runs local diagnostics without starting a worker. Replace `agent.py` with your agent entrypoint. Use this first when setup fails or before deploying to confirm required packages, environment variables, credentials, and downloadable model files are in place.

Useful options:

- `--json`: output a machine-readable report for CI or support tickets.
- `--online`: include a safe LiveKit network connectivity check.
- `--deep`: run plugin-declared checks that are safe to execute.
- `--strict`: exit non-zero on warnings as well as errors.

### CLI modes

| Command | Use for | Requires LiveKit server | Notes |
| --- | --- | --- | --- |
| `console` | Local terminal testing | Yes for the default Cloud Inference examples | Uses local audio or text input; fastest way to iterate on agent behavior. |
| `dev` | Development with clients | Yes | Starts a worker with dev defaults and auto-reload. |
| `start` | Production workers | Yes | Runs with production defaults. |
| `connect` | Attach an agent to a room | Yes | Creates or joins the room and simulates a job for that room. |
| `download-files` | Preload plugin assets | No | Downloads files required by registered plugins, such as local model assets. |
| `doctor` | Diagnose setup issues | No by default | Add `--online` for network and credential checks. |

## Contributing

The Agents framework is under active development in a rapidly evolving field. We welcome and appreciate contributions of any kind, be it feedback, bugfixes, features, new plugins and tools, or better documentation. You can file issues under this repo, open a PR, or chat with us in the [LiveKit community](https://docs.livekit.io/intro/community/).
Expand Down
30 changes: 29 additions & 1 deletion examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,15 @@ session = AgentSession(

**Note:** Realtime models (e.g., `openai.realtime.RealtimeModel`) are not supported by LiveKit Inference and must use the plugin directly. See the [Real-time Models](#-real-time-models) examples in `voice_agents/`.

### Choosing model access

| Option | Choose when | Credentials |
| --- | --- | --- |
| LiveKit Cloud Inference | You want the default examples to work with one LiveKit Cloud project and a unified model API. | `LIVEKIT_API_KEY` and `LIVEKIT_API_SECRET` |
| Provider plugin | You need provider-specific features, models, or account controls. | Provider API key, such as `OPENAI_API_KEY` |
| Realtime model | You need a provider's realtime speech-to-speech API. | Provider API key |
| Self-hosted model | You run model services in your own infrastructure. | Your service URL and any service-specific credentials |

## 📁 Example Categories

### 🎙️ [Voice Agents](./voice_agents/)
Expand Down Expand Up @@ -54,7 +63,7 @@ To run the examples, you'll need:

- A [LiveKit Cloud](https://cloud.livekit.io) account or a local [LiveKit server](https://github.com/livekit/livekit)
- API keys for the model providers you want to use in a `.env` file
- Python 3.9 or higher
- Python 3.10 or higher
- [uv](https://docs.astral.sh/uv/)

### Environment file
Expand Down Expand Up @@ -84,6 +93,14 @@ uv sync --all-extras --dev

### Running an individual example

Check your setup before running an example:

```bash
uv run examples/voice_agents/basic_agent.py doctor
```

Use `--json` for machine-readable output, `--online` for a safe LiveKit network check, `--deep` for plugin-declared checks, and `--strict` to fail on warnings.

Run an example agent:

```bash
Expand All @@ -94,6 +111,17 @@ Your agent is now running in the console.

For frontend support, use the [Agents playground](https://agents-playground.livekit.io) or the [starter apps](https://docs.livekit.io/agents/start/frontend/#starter-apps).

### CLI modes

| Command | Use for |
| --- | --- |
| `console` | Test the agent locally in the terminal. |
| `dev` | Run a development worker for LiveKit clients with auto-reload. |
| `start` | Run a production worker. |
| `connect` | Attach the agent to a specific LiveKit room. |
| `download-files` | Download plugin assets before first run or container startup. |
| `doctor` | Diagnose local setup, credentials, and optional online checks. |

## 📖 Additional Resources

- [LiveKit Documentation](https://docs.livekit.io/)
Expand Down
174 changes: 172 additions & 2 deletions livekit-agents/livekit/agents/cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,14 @@

from .. import llm
from .._exceptions import CLIError
from ..diagnostics import (
DiagnosticContext,
DiagnosticSeverity,
build_diagnostics_table,
collect_diagnostics,
diagnostic_report_to_json,
report_exit_code,
)
from ..job import JobExecutorType
from ..log import logger
from ..plugin import Plugin
Expand Down Expand Up @@ -1501,6 +1509,14 @@ def _run_console(
c.record = record

_configure_logger(c, log_level)
_run_preflight(
server=server,
mode="console",
console=c,
console_audio=mode == "audio",
input_device=input_device,
output_device=output_device,
)
c.print("Starting console mode 🚀", tag="Agents")

if c.record:
Expand All @@ -1519,7 +1535,8 @@ def _run_console(
# c.print(f"Importing from {import_data.module_data.extra_sys_path}")
# c.print(" ")

c._validate_device_or_raise(input_device=input_device, output_device=output_device)
if mode == "audio":
c._validate_device_or_raise(input_device=input_device, output_device=output_device)

exit_triggered = False

Expand Down Expand Up @@ -1592,6 +1609,17 @@ def _handle_exit(sig: int, frame: FrameType | None) -> None:
signal.signal(sig, _handle_exit)

_configure_logger(c, args.log_level)
_apply_cli_server_options(
server,
url=args.url,
api_key=args.api_key,
api_secret=args.api_secret,
)
_run_preflight(
server=server,
mode="dev" if args.devmode else "start",
console=c,
)

loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
Expand Down Expand Up @@ -1663,6 +1691,97 @@ class LogLevel(str, enum.Enum):
critical = "CRITICAL"


def _apply_cli_server_options(
server: AgentServer,
*,
url: str | None = None,
api_key: str | None = None,
api_secret: str | None = None,
) -> None:
update_kwargs: dict[str, Any] = {}
if url:
update_kwargs["ws_url"] = url
if api_key:
update_kwargs["api_key"] = api_key
if api_secret:
update_kwargs["api_secret"] = api_secret

if update_kwargs:
server.update_options(**update_kwargs)


def _resolved_livekit_api_options(
server: AgentServer,
*,
url: str | None = None,
api_key: str | None = None,
api_secret: str | None = None,
) -> tuple[str, str, str]:
return (
str(url or server._ws_url or ""),
str(api_key or server._api_key or ""),
str(api_secret or server._api_secret or ""),
)


def _diagnostic_context(
server: AgentServer,
*,
mode: Literal["doctor", "console", "dev", "start", "connect"] = "doctor",
online: bool = False,
deep: bool = False,
strict: bool = False,
console_audio: bool = True,
input_device: str | int | None = None,
output_device: str | int | None = None,
) -> DiagnosticContext:
return DiagnosticContext(
mode=mode,
online=online,
deep=deep,
strict=strict,
env=os.environ.copy(),
registered_plugins=tuple(Plugin.registered_plugins),
server=server,
console_audio=console_audio,
input_device=input_device,
output_device=output_device,
)


def _run_preflight(
*,
server: AgentServer,
mode: Literal["console", "dev", "start", "connect"],
console: AgentsConsole | None,
console_audio: bool = True,
input_device: str | int | None = None,
output_device: str | int | None = None,
) -> None:
report = collect_diagnostics(
_diagnostic_context(
server,
mode=mode,
console_audio=console_audio,
input_device=input_device,
output_device=output_device,
)
)
blocking = [result for result in report.results if result.severity == DiagnosticSeverity.FATAL]
notable = [result for result in report.results if result.severity != DiagnosticSeverity.OK]

if notable:
if console is None:
Console().print(build_diagnostics_table(report))
else:
console.print(build_diagnostics_table(report))

# Preflight only blocks on fatal setup errors. Non-fatal errors still surface
# in the table but should not prevent an otherwise valid worker from starting.
if blocking:
raise typer.Exit(code=1)


def _build_cli(server: AgentServer) -> typer.Typer:
app = typer.Typer(rich_markup_mode="rich")

Expand All @@ -1677,6 +1796,45 @@ def _set_dev_mode(ctx: typer.Context) -> None:
_start_log_default = LogLevel(ServerEnvOption.getvalue(server.log_level, False))
_dev_log_default = LogLevel(ServerEnvOption.getvalue(server.log_level, True))

@app.command()
def doctor(
*,
json_output: Annotated[
bool,
typer.Option("--json", help="Emit a stable JSON diagnostics report."),
] = False,
online: Annotated[
bool,
typer.Option(help="Run safe online connectivity checks."),
] = False,
deep: Annotated[
bool,
typer.Option(help="Run plugin-declared deep checks that are safe to execute."),
] = False,
strict: Annotated[
bool,
typer.Option(help="Return a failing exit code for warnings."),
] = False,
) -> None:
"""
Run [bold]LiveKit Agents[/bold] first-run diagnostics.
"""
report = collect_diagnostics(
_diagnostic_context(
server,
mode="doctor",
online=online,
deep=deep,
strict=strict,
)
)
if json_output:
typer.echo(diagnostic_report_to_json(report))
else:
Console().print(build_diagnostics_table(report))

raise typer.Exit(code=report_exit_code(report))

@app.command()
def console(
*,
Expand Down Expand Up @@ -1907,6 +2065,16 @@ def connect(

c = AgentsConsole.get_instance()
_configure_logger(c, log_level.value)
_apply_cli_server_options(server, url=url, api_key=api_key, api_secret=api_secret)
_run_preflight(server=server, mode="connect", console=c)
resolved_url, resolved_api_key, resolved_api_secret = _resolved_livekit_api_options(
server,
url=url,
api_key=api_key,
api_secret=api_secret,
)
if not (resolved_url and resolved_api_key and resolved_api_secret):
raise CLIError("LiveKit credentials are required for connect mode")

loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
Expand All @@ -1917,7 +2085,9 @@ def _simulate_job() -> None:
nonlocal _task

async def simulate_job() -> None:
async with api.LiveKitAPI(url, api_key, api_secret) as lk_api:
async with api.LiveKitAPI(
resolved_url, resolved_api_key, resolved_api_secret
) as lk_api:
room_request = api.ListRoomsRequest(names=[room])
active_room = await lk_api.room.list_rooms(room_request)

Expand Down
Loading