diff --git a/.agents/skills/openrtc-python/SKILL.md b/.agents/skills/openrtc-python/SKILL.md index 4c1b397..bdd8a61 100644 --- a/.agents/skills/openrtc-python/SKILL.md +++ b/.agents/skills/openrtc-python/SKILL.md @@ -112,18 +112,21 @@ with `_`. Fix and re-run `openrtc list` until all agents appear. ```bash # Development mode (auto-reload) — set LIVEKIT_* env vars first -openrtc dev --agents-dir ./agents +openrtc dev ./agents # Production mode -openrtc start --agents-dir ./agents +openrtc start ./agents # Same LiveKit subcommands as python agent.py: console, connect, download-files -# openrtc console --agents-dir ./agents -# openrtc connect --agents-dir ./agents --room my-room +# openrtc console ./agents +# openrtc connect ./agents --room my-room +# openrtc list ./agents +# openrtc download-files ./agents # Optional: JSON Lines metrics + sidecar TUI (pip install 'openrtc[cli,tui]') -# openrtc dev --agents-dir ./agents --metrics-jsonl ./metrics.jsonl -# openrtc tui --watch ./metrics.jsonl +# openrtc dev ./agents ./openrtc-metrics.jsonl +# openrtc tui +# openrtc tui ./other-metrics.jsonl # Or run the entrypoint directly python main.py dev diff --git a/AGENTS.md b/AGENTS.md index 2623e4e..6d6bf6d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -409,13 +409,13 @@ All commands are documented in `CONTRIBUTING.md`. Quick reference: - **Lint:** `uv run ruff check .` - **Format check:** `uv run ruff format --check .` - **Type check:** `uv run mypy src/` (must pass clean; also runs in `.github/workflows/lint.yml`) -- **CLI demo:** `uv run openrtc list --agents-dir ./examples/agents --default-stt "deepgram/nova-3:multi" --default-llm "openai/gpt-4.1-mini" --default-tts "cartesia/sonic-3"` +- **CLI demo:** `uv run openrtc list ./examples/agents --default-stt "deepgram/nova-3:multi" --default-llm "openai/gpt-4.1-mini" --default-tts "cartesia/sonic-3"` (same as `--agents-dir ./examples/agents`) ### Non-obvious notes - The `tests/conftest.py` shim targets the `livekit-agents` pin in `pyproject.toml` (~1.4.x today) and only implements APIs OpenRTC uses. When upgrading LiveKit or adding new `livekit.agents` usage, extend the shim or confirm tests pass with the real SDK (`uv sync` + `uv run pytest`). If imports behave oddly, check whether the shim path is active vs. the real package. - Version is derived from git tags via `hatch-vcs`. In a dev checkout the version will be something like `0.0.9.dev0+g`. - `mypy` is enforced in CI alongside Ruff; run `uv run mypy src/` before pushing type-sensitive changes. -- Running `openrtc start` or `openrtc dev` requires a running LiveKit server and provider API keys. For development validation, use `openrtc list` which exercises discovery and routing without network dependencies. The optional sidecar metrics TUI (`openrtc tui --watch`, requires `openrtc[tui]` / dev deps) tails `--metrics-jsonl` from a worker in another terminal. +- Running `openrtc start` or `openrtc dev` requires a running LiveKit server and provider API keys. For development validation, use `openrtc list` which exercises discovery and routing without network dependencies. The optional sidecar metrics TUI (`openrtc tui`, requires `openrtc[tui]` / dev deps) tails `./openrtc-metrics.jsonl` by default (same path as `--metrics-jsonl` on the worker; override with `--watch`). - `pytest-cov` is in the dev dependency group; CI uses `--cov-fail-under=80`; run `uv run pytest --cov=openrtc --cov-report=xml --cov-fail-under=80` to match. diff --git a/README.md b/README.md index f85fb87..0ea811d 100644 --- a/README.md +++ b/README.md @@ -226,13 +226,13 @@ If you pass strings such as `openai/gpt-4.1-mini`, OpenRTC leaves them as-is and ## CLI and TUI -Install `openrtc[cli]` to get `openrtc` on your PATH. Subcommands follow the LiveKit Agents CLI shape (`dev`, `start`, `console`, `connect`, `download-files`), plus `list` and `tui`. +Install `openrtc[cli]` to get `openrtc` on your PATH. Subcommands follow the LiveKit Agents CLI shape (`dev`, `start`, `console`, `connect`, `download-files`), plus `list` and `tui`. For most commands you can pass the agents directory (or, for `tui`, the metrics JSONL file) as the first path argument instead of `--agents-dir` / `--watch`. **List what discovery would register** (defaults are string passthroughs for `livekit-agents`, not constructed provider objects): ```bash openrtc list \ - --agents-dir ./agents \ + ./agents \ --default-stt openai/gpt-4o-mini-transcribe \ --default-llm openai/gpt-4.1-mini \ --default-tts openai/gpt-4o-mini-tts @@ -241,16 +241,18 @@ openrtc list \ **Run a production worker** (after exporting `LIVEKIT_*`): ```bash -openrtc start --agents-dir ./agents +openrtc start ./agents ``` **Run a development worker**: ```bash -openrtc dev --agents-dir ./agents +openrtc dev ./agents ``` -Optional visibility: `--dashboard` prints a Rich summary in the terminal. `--metrics-json-file ./runtime.json` overwrites a JSON snapshot on each tick. Use that for scripts, dashboards, or CI. For JSON Lines plus a separate terminal UI, use `--metrics-jsonl ./metrics.jsonl` with `openrtc tui --watch ./metrics.jsonl` after `pip install 'openrtc[cli,tui]'`. +Same as ``openrtc dev --agents-dir ./agents``. The metrics JSONL file is **optional**: add a second path only when you want JSONL output (same as ``--metrics-jsonl``), e.g. ``openrtc dev ./agents ./openrtc-metrics.jsonl`` for ``openrtc tui``. + +Optional visibility: `--dashboard` prints a Rich summary in the terminal. `--metrics-json-file ./runtime.json` overwrites a JSON snapshot on each tick. Use that for scripts, dashboards, or CI. For JSON Lines plus a separate terminal UI, use `--metrics-jsonl ./openrtc-metrics.jsonl` on the worker and `openrtc tui` in another terminal (it tails `./openrtc-metrics.jsonl` by default; override with `--watch`) after `pip install 'openrtc[cli,tui]'`. Stable machine output: `openrtc list --json` and `--plain`. Combine `--resources` when you want footprint hints. OpenRTC-only flags are stripped before the handoff to LiveKit’s CLI parser. diff --git a/docs/cli.md b/docs/cli.md index 68de67a..2a0a0bb 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -41,12 +41,18 @@ with `1`. export LIVEKIT_API_SECRET=secret ``` -2. Run a worker subcommand with **only** `--agents-dir` (plus any provider - defaults your agents need): +2. Run a worker subcommand with an agents directory (plus any provider defaults + your agents need). You can pass **`--agents-dir`** or use the **first + positional argument** on ``start`` / ``dev`` / ``console``. A **second** + positional is **optional** and only sets ``--metrics-jsonl`` when you want JSONL + metrics (e.g. for ``openrtc tui``); skip it if you only need the agents + directory (unless you already passed ``--metrics-jsonl``). ```bash - openrtc dev --agents-dir ./agents - # or + openrtc dev ./agents + openrtc dev ./agents ./openrtc-metrics.jsonl + # equivalent to: + openrtc dev --agents-dir ./agents --metrics-jsonl ./openrtc-metrics.jsonl openrtc start --agents-dir ./agents ``` @@ -74,6 +80,12 @@ flag. ## Commands +Across **list**, **connect**, **download-files**, **start** / **dev** / **console**, +and **tui**, you can often pass paths **positionally** instead of `--agents-dir`, +`--metrics-jsonl`, or `--watch` (see each command below). The first non-flag +token after the subcommand is rewritten before parsing; use `--agents-dir` / +`--watch` when you need a different argument order. + ### `openrtc list` Discovers agent modules and prints each agent’s resolved settings. @@ -89,9 +101,9 @@ Discovers agent modules and prints each agent’s resolved settings. `--help`). ```bash -openrtc list --agents-dir ./agents +openrtc list ./agents openrtc list --agents-dir ./agents --plain -openrtc list --agents-dir ./agents --json +openrtc list ./agents --json ``` ### `openrtc start` @@ -99,7 +111,7 @@ openrtc list --agents-dir ./agents --json Production-style worker (same role as `python agent.py start`). ```bash -openrtc start --agents-dir ./agents +openrtc start ./agents ``` ### `openrtc dev` @@ -107,7 +119,7 @@ openrtc start --agents-dir ./agents Development worker with reload (same role as `python agent.py dev`). ```bash -openrtc dev --agents-dir ./agents +openrtc dev ./agents ``` ### `openrtc console` @@ -115,7 +127,7 @@ openrtc dev --agents-dir ./agents Local console session (same role as `python agent.py console`). ```bash -openrtc console --agents-dir ./agents +openrtc console ./agents ``` ### `openrtc connect` @@ -124,7 +136,7 @@ Connect the worker to an existing room (LiveKit `connect`). Requires `--room`. ```bash -openrtc connect --agents-dir ./agents --room my-room +openrtc connect ./agents --room my-room ``` ### `openrtc download-files` @@ -134,7 +146,7 @@ directory (for a valid worker entrypoint) plus connection settings—**no** `--default-stt` / `--default-llm` / `--default-tts` / `--default-greeting`. ```bash -openrtc download-files --agents-dir ./agents +openrtc download-files ./agents ``` ### `openrtc tui` @@ -142,12 +154,22 @@ openrtc download-files --agents-dir ./agents Sidecar Textual UI that tails a **JSON Lines** metrics file written by the worker (`--metrics-jsonl`). Requires `openrtc[tui]`. +With no flags, the TUI tails **`./openrtc-metrics.jsonl`** in the current working +directory. Pass **`--watch PATH`** or a **positional path** to use another file +(it must match `--metrics-jsonl` on the worker). + ```bash # Terminal 1 -openrtc dev --agents-dir ./agents --metrics-jsonl ./openrtc-metrics.jsonl +openrtc dev ./agents ./openrtc-metrics.jsonl + +# Terminal 2 (same default file as above) +openrtc tui + +# Or pass the file positionally: +# openrtc tui ./openrtc-metrics.jsonl -# Terminal 2 -openrtc tui --watch ./openrtc-metrics.jsonl +# Equivalent explicit form: +# openrtc tui --watch ./openrtc-metrics.jsonl ``` Use **`--from-start`** (under **Advanced**) to read the file from the beginning @@ -164,7 +186,7 @@ instead of tailing from EOF. (`snapshot` or `event`), `seq`, `wall_time_unix`, `payload`. Snapshots match `PoolRuntimeSnapshot.to_dict()`; events carry session lifecycle hints (`session_started`, `session_finished`, `session_failed`). Intended for - `openrtc tui --watch` and other tail consumers. + `openrtc tui` and other tail consumers. - **`--dashboard-refresh`** — Interval in seconds for dashboard, metrics file, and JSONL when `--metrics-jsonl-interval` is not set (**Advanced**). - **`--metrics-jsonl-interval`** — Override JSONL cadence only (**Advanced**). @@ -244,7 +266,8 @@ openrtc list --agents-dir ./examples/agents --resources --json --metrics-jsonl ./openrtc-metrics.jsonl ``` -3. Watch the dashboard (or `openrtc tui --watch ./openrtc-metrics.jsonl`) for +3. Watch the dashboard (or run `openrtc tui` in another terminal for the same + default JSONL file) for worker RSS, active sessions, routing, and errors. 4. Use `runtime.json` or the JSONL stream for automation or scraping. diff --git a/docs/getting-started.md b/docs/getting-started.md index d22c45b..a35f0f2 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -32,7 +32,7 @@ Install the **Typer/Rich CLI** (`openrtc list`, `openrtc start`, `openrtc dev`, pip install 'openrtc[cli]' ``` -Install the optional **Textual sidecar** for `openrtc tui --watch` with: +Install the optional **Textual sidecar** for `openrtc tui` with: ```bash pip install 'openrtc[cli,tui]' @@ -56,7 +56,7 @@ With `LIVEKIT_URL`, `LIVEKIT_API_KEY`, and `LIVEKIT_API_SECRET` set, the minimal worker invocation is: ```bash -openrtc dev --agents-dir ./agents +openrtc dev ./agents ``` Use `openrtc start` for production-style runs. See [CLI](./cli) for `console`, diff --git a/src/openrtc/cli_app.py b/src/openrtc/cli_app.py index 8dc726f..41ca56e 100644 --- a/src/openrtc/cli_app.py +++ b/src/openrtc/cli_app.py @@ -5,6 +5,7 @@ import json import logging import sys +from pathlib import Path from typing import Annotated import typer @@ -23,6 +24,7 @@ _run_connect_handoff, _run_pool_with_reporting, _strip_openrtc_only_flags_for_livekit, + inject_cli_positional_paths, ) from openrtc.cli_params import SharedLiveKitWorkerOptions, agent_provider_kwargs from openrtc.cli_reporter import RuntimeReporter @@ -48,6 +50,7 @@ TuiFromStartArg, TuiWatchPathArg, ) +from openrtc.metrics_stream import DEFAULT_METRICS_JSONL_FILENAME from openrtc.pool import AgentPool logger = logging.getLogger("openrtc") @@ -55,7 +58,9 @@ _QUICKSTART_EPILOG = ( "[bold]Typical usage[/bold]: set [code]LIVEKIT_URL[/code], [code]LIVEKIT_API_KEY[/code], " "and [code]LIVEKIT_API_SECRET[/code], then run " - "[code]openrtc dev --agents-dir PATH[/code] (or [code]start[/code] in production). " + "[code]openrtc dev ./agents[/code] (agents dir only) or add a second path for " + "[code]--metrics-jsonl[/code]; or use [code]--agents-dir[/code]. " + "[code]start[/code] for production. " "Defaults are conservative (e.g. no dashboard, 1s refresh); tuning flags are under " "the [bold]Advanced[/bold] group in each command's [code]--help[/code]." ) @@ -66,7 +71,11 @@ "Run multiple LiveKit voice agents from one shared worker. Commands match " "LiveKit Agents ([code]dev[/code], [code]start[/code], [code]console[/code], " "[code]connect[/code], [code]download-files[/code]) plus [code]list[/code] and " - "[code]tui[/code]. Only [code]--agents-dir[/code] is required for worker commands; " + "[code]tui[/code]. Most commands accept the agents directory as the first " + "positional argument instead of [code]--agents-dir[/code]; " + "[code]start[/code]/[code]dev[/code]/[code]console[/code] also accept a " + "second path for [code]--metrics-jsonl[/code], and [code]tui[/code] can " + "take a metrics file path as the first positional instead of [code]--watch[/code]; " "credentials use [code]LIVEKIT_*[/code] env vars by default (CLI flags optional)." ), epilog=_QUICKSTART_EPILOG, @@ -137,18 +146,28 @@ def list_command( print_resource_summary_rich(discovered) +_WORKER_POSITIONAL_HELP = ( + " Use [code]openrtc {name} ./agents[/code] or [code]--agents-dir ./agents[/code]; " + "add a second path only when you want JSONL metrics " + f"([code]--metrics-jsonl[/code], e.g. [code]./{DEFAULT_METRICS_JSONL_FILENAME}[/code] for " + "[code]openrtc tui[/code])." +) + _STANDARD_LIVEKIT_WORKER_SPECS: tuple[tuple[str, str], ...] = ( ( "start", - "Run the worker (same role as [code]python agent.py start[/code] with LiveKit).", + "Run the worker (same role as [code]python agent.py start[/code] with LiveKit)." + + _WORKER_POSITIONAL_HELP.format(name="start"), ), ( "dev", - "Development worker with reload (same role as [code]python agent.py dev[/code]).", + "Development worker with reload (same role as [code]python agent.py dev[/code])." + + _WORKER_POSITIONAL_HELP.format(name="dev"), ), ( "console", - "Local console session (same role as [code]python agent.py console[/code]).", + "Local console session (same role as [code]python agent.py console[/code])." + + _WORKER_POSITIONAL_HELP.format(name="console"), ), ) @@ -273,10 +292,14 @@ def download_files_command( @app.command("tui") def tui_command( - watch: TuiWatchPathArg, + watch: TuiWatchPathArg = Path(DEFAULT_METRICS_JSONL_FILENAME), from_start: TuiFromStartArg = False, ) -> None: - """Sidecar Textual UI for a --metrics-jsonl stream (requires the ``tui`` extra).""" + """Sidecar Textual UI tailing JSONL metrics (requires the ``tui`` extra). + + With no ``--watch``, tails ``./openrtc-metrics.jsonl`` in the current directory; + start the worker with ``--metrics-jsonl`` set to that same path. + """ try: from openrtc.tui_app import run_metrics_tui except ImportError as exc: @@ -285,7 +308,11 @@ def tui_command( "(the cli extra is required for the openrtc command)." ) raise typer.Exit(code=1) from exc - run_metrics_tui(watch, from_start=from_start) + try: + run_metrics_tui(watch, from_start=from_start) + except ValueError as exc: + logger.error("%s", exc) + raise typer.Exit(code=1) from None def main(argv: list[str] | None = None) -> int: @@ -307,8 +334,19 @@ def main(argv: list[str] | None = None) -> int: previous_argv = sys.argv try: if argv is not None: - cli.main(args=list(argv), prog_name="openrtc", standalone_mode=True) + injected_args = inject_cli_positional_paths(list(argv)) + # Mirror a real CLI invocation so LiveKit handoff logic that + # inspects sys.argv sees the injected arguments (e.g. --reload). + sys.argv = [previous_argv[0], *injected_args] + cli.main( + args=injected_args, + prog_name="openrtc", + standalone_mode=True, + ) else: + if len(sys.argv) >= 2: + tail = inject_cli_positional_paths(list(sys.argv[1:])) + sys.argv = [sys.argv[0], *tail] cli.main(args=None, prog_name="openrtc", standalone_mode=True) except SystemExit as exc: code = exc.code diff --git a/src/openrtc/cli_livekit.py b/src/openrtc/cli_livekit.py index f180c6c..700e833 100644 --- a/src/openrtc/cli_livekit.py +++ b/src/openrtc/cli_livekit.py @@ -78,6 +78,69 @@ def _strip_openrtc_only_flags_for_livekit(argv_tail: list[str]) -> list[str]: return out +def _inject_agents_dir_positional(argv: list[str]) -> list[str]: + """`` ./agents ...`` → `` --agents-dir ./agents ...`` when flag absent.""" + rest = argv[1:] + if not rest or rest[0].startswith("-"): + return argv + if any(t == "--agents-dir" or t.startswith("--agents-dir=") for t in rest): + return argv + return [argv[0], "--agents-dir", rest[0], *rest[1:]] + + +def _inject_worker_start_dev_console(argv: list[str]) -> list[str]: + """``dev|start|console ./agents [./metrics.jsonl]`` into ``--agents-dir`` / ``--metrics-jsonl``.""" + rest = argv[1:] + if not rest or rest[0].startswith("-"): + return argv + if any(t == "--agents-dir" or t.startswith("--agents-dir=") for t in rest): + return argv + has_metrics_flag = any( + t == "--metrics-jsonl" or t.startswith("--metrics-jsonl=") for t in rest + ) + agents_token = rest[0] + out = [argv[0], "--agents-dir", agents_token] + pos = 1 + if not has_metrics_flag and pos < len(rest) and not rest[pos].startswith("-"): + out.extend(["--metrics-jsonl", rest[pos]]) + pos += 1 + out.extend(rest[pos:]) + return out + + +def _inject_tui_watch_positional(argv: list[str]) -> list[str]: + """``tui ./metrics.jsonl`` → ``tui --watch ./metrics.jsonl`` when ``--watch`` absent.""" + rest = argv[1:] + if not rest or rest[0].startswith("-"): + return argv + if any(t == "--watch" or t.startswith("--watch=") for t in rest): + return argv + return [argv[0], "--watch", rest[0], *rest[1:]] + + +def inject_cli_positional_paths(argv: list[str]) -> list[str]: + """Rewrite common positional shortcuts into explicit flags before Typer parses. + + Pass-through tokens (e.g. ``--reload`` for LiveKit) must never be treated as + paths; only a leading non-flag token after the subcommand is rewritten. + """ + if not argv: + return argv + sub = argv[0] + if sub in {"start", "dev", "console"}: + return _inject_worker_start_dev_console(argv) + if sub in {"list", "connect", "download-files"}: + return _inject_agents_dir_positional(argv) + if sub == "tui": + return _inject_tui_watch_positional(argv) + return argv + + +def inject_worker_positional_paths(argv: list[str]) -> list[str]: + """Backward-compatible alias for :func:`inject_cli_positional_paths`.""" + return inject_cli_positional_paths(argv) + + def _livekit_sys_argv(subcommand: str) -> None: """Set ``sys.argv`` for ``livekit.agents.cli.run_app``. diff --git a/src/openrtc/cli_types.py b/src/openrtc/cli_types.py index b310bba..bc513ec 100644 --- a/src/openrtc/cli_types.py +++ b/src/openrtc/cli_types.py @@ -7,6 +7,8 @@ import typer +from openrtc.metrics_stream import DEFAULT_METRICS_JSONL_FILENAME + PANEL_OPENRTC = "OpenRTC" PANEL_LIVEKIT = "Connection" PANEL_ADVANCED = "Advanced" @@ -15,7 +17,12 @@ Path, typer.Option( "--agents-dir", - help="Directory of agent modules to load (only required flag for most workflows).", + help=( + "Directory of agent modules to load. Pass the same path as the first " + "positional argument instead of this flag where supported (e.g. " + "openrtc list ./agents or openrtc dev ./agents). On start/dev/console " + "only, an optional second positional sets --metrics-jsonl." + ), exists=False, resolve_path=True, path_type=Path, @@ -106,8 +113,11 @@ typer.Option( "--metrics-jsonl", help=( - "Append JSON Lines for ``openrtc tui --watch`` (off by default; " - "truncates when the worker starts)." + "Append JSON Lines for the sidecar TUI (off by default; truncates when " + "the worker starts). For the default ``openrtc tui`` file, use " + f"``./{DEFAULT_METRICS_JSONL_FILENAME}`` here. On ``start``/``dev``/``console`` " + "you may pass that path as the **second** positional after the agents directory " + "(optional—omit it if you only need to point at the agents folder)." ), resolve_path=True, path_type=Path, @@ -129,7 +139,13 @@ Path, typer.Option( "--watch", - help="JSONL file written by the worker's --metrics-jsonl.", + show_default=True, + help=( + "JSONL file the worker writes with --metrics-jsonl (not your " + f"--agents-dir). Defaults to ./{DEFAULT_METRICS_JSONL_FILENAME}; pass " + "the same path to --metrics-jsonl on the worker, or pass PATH as the " + "first positional argument instead of --watch." + ), resolve_path=True, path_type=Path, rich_help_panel=PANEL_OPENRTC, diff --git a/src/openrtc/metrics_stream.py b/src/openrtc/metrics_stream.py index 155ddac..57993b4 100644 --- a/src/openrtc/metrics_stream.py +++ b/src/openrtc/metrics_stream.py @@ -1,7 +1,8 @@ """Sidecar metrics stream for workers (JSON Lines over a file or socket). Each line is one JSON object (envelope) so a separate TUI or script can tail the -file. This is the contract for ``openrtc tui --watch``. +file. This is the contract for ``openrtc tui`` (default ``--watch``) and +``openrtc tui --watch ``. **Envelope (schema version 1)** @@ -29,6 +30,10 @@ KIND_SNAPSHOT = "snapshot" KIND_EVENT = "event" +# Default tail target for ``openrtc tui`` with no ``--watch``. Use the same path +# on the worker (``--metrics-jsonl ./openrtc-metrics.jsonl``) for a two-terminal flow. +DEFAULT_METRICS_JSONL_FILENAME = "openrtc-metrics.jsonl" + def snapshot_envelope(*, seq: int, snapshot: PoolRuntimeSnapshot) -> dict[str, Any]: """Build a versioned JSON object for one line of the metrics stream.""" diff --git a/src/openrtc/tui_app.py b/src/openrtc/tui_app.py index a5837f9..273dfb8 100644 --- a/src/openrtc/tui_app.py +++ b/src/openrtc/tui_app.py @@ -12,6 +12,18 @@ from openrtc.metrics_stream import KIND_EVENT, KIND_SNAPSHOT, parse_metrics_jsonl_line +def validate_metrics_watch_path(path: Path) -> None: + """Ensure *path* can be used as the metrics JSONL file (not a directory).""" + resolved = path.resolve() + if resolved.exists() and resolved.is_dir(): + raise ValueError( + "The metrics watch path must be a JSONL file path (the same path you " + "pass to '--metrics-jsonl' on the OpenRTC worker). This value is a " + "directory — for example, use a file such as ./metrics.jsonl, not your " + f"agents folder. Got: {resolved}" + ) + + class MetricsTuiApp(App[None]): """Tail ``--metrics-jsonl`` and show live pool metrics.""" @@ -21,6 +33,7 @@ class MetricsTuiApp(App[None]): def __init__(self, watch_path: Path, *, from_start: bool = False) -> None: super().__init__() self._path = watch_path.resolve() + validate_metrics_watch_path(self._path) self._from_start = from_start self._fh: TextIO | None = None self._buf = "" @@ -32,7 +45,8 @@ def __init__(self, watch_path: Path, *, from_start: bool = False) -> None: def compose(self) -> ComposeResult: yield Header(show_clock=True) yield Static( - "Waiting for JSONL metrics (run the worker with --metrics-jsonl)…", + f"Waiting for JSONL metrics at {self._path} (run the worker with " + "--metrics-jsonl set to this path)…", id="status", ) yield Static("", id="event") diff --git a/tests/test_cli.py b/tests/test_cli.py index 9c7bfa4..bd7e3d7 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -275,6 +275,70 @@ def test_list_exits_cleanly_when_agents_dir_does_not_exist( assert "does not exist" in caplog.text +def test_inject_cli_positional_paths_rewrites_shortcuts() -> None: + from openrtc.cli_livekit import inject_cli_positional_paths + + assert inject_cli_positional_paths( + ["dev", "./agents", "./openrtc-metrics.jsonl", "--reload"], + ) == [ + "dev", + "--agents-dir", + "./agents", + "--metrics-jsonl", + "./openrtc-metrics.jsonl", + "--reload", + ] + assert inject_cli_positional_paths( + ["dev", "./agents", "--reload"], + ) == ["dev", "--agents-dir", "./agents", "--reload"] + assert inject_cli_positional_paths(["dev", "./agents"]) == [ + "dev", + "--agents-dir", + "./agents", + ] + assert inject_cli_positional_paths( + ["dev", "--agents-dir", "./agents", "--reload"], + ) == ["dev", "--agents-dir", "./agents", "--reload"] + assert inject_cli_positional_paths( + ["list", "./agents", "--json"], + ) == ["list", "--agents-dir", "./agents", "--json"] + assert inject_cli_positional_paths( + ["connect", "./agents", "--room", "demo"], + ) == ["connect", "--agents-dir", "./agents", "--room", "demo"] + assert inject_cli_positional_paths( + ["download-files", "./agents"], + ) == ["download-files", "--agents-dir", "./agents"] + assert inject_cli_positional_paths( + ["tui", "./m.jsonl", "--from-start"], + ) == ["tui", "--watch", "./m.jsonl", "--from-start"] + assert inject_cli_positional_paths(["tui"]) == ["tui"] + from openrtc.cli_livekit import inject_worker_positional_paths + + assert inject_worker_positional_paths( + ["list", "./agents"] + ) == inject_cli_positional_paths( + ["list", "./agents"], + ) + + +def test_dev_positional_agents_rewrites_before_typer( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, +) -> None: + """``openrtc dev ./agents`` is rewritten to ``--agents-dir`` in :func:`main`.""" + import openrtc.cli_livekit as cli_livekit_mod + + agents = tmp_path / "agents" + agents.mkdir() + stub_pool = StubPool(discovered=[StubConfig(name="a", agent_cls=StubAgent)]) + monkeypatch.setattr(cli_livekit_mod, "AgentPool", lambda **kwargs: stub_pool) + monkeypatch.setattr( + cli_livekit_mod, "_run_pool_with_reporting", lambda *a, **k: None + ) + exit_code = main(["dev", str(agents)]) + assert exit_code == 0 + + def test_strip_openrtc_only_flags_for_livekit_removes_openrtc_options() -> None: """LiveKit ``run_app`` must not see OpenRTC-only flags (see ``_livekit_sys_argv``).""" from openrtc.cli_app import _strip_openrtc_only_flags_for_livekit @@ -612,3 +676,52 @@ def guard(name: str, *args: object, **kwargs: object) -> object: assert result.exit_code == 1 assert "Textual" in caplog.text assert "openrtc[tui]" in caplog.text + + +def test_tui_help_documents_default_watch_path() -> None: + runner = CliRunner() + result = runner.invoke(app, ["tui", "--help"], catch_exceptions=False) + assert result.exit_code == 0 + assert "openrtc-metrics.jsonl" in result.output + + +def test_tui_command_without_watch_uses_default_metrics_path( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + pytest.importorskip("textual") + import openrtc.tui_app as tu + from openrtc.tui_app import MetricsTuiApp + + seen: list[Path] = [] + + def fake_run(self: MetricsTuiApp) -> None: + seen.append(self._path) + + monkeypatch.chdir(tmp_path) + monkeypatch.setattr(tu.MetricsTuiApp, "run", fake_run) + runner = CliRunner() + result = runner.invoke(app, ["tui"], catch_exceptions=False) + assert result.exit_code == 0 + assert len(seen) == 1 + assert seen[0] == (tmp_path / "openrtc-metrics.jsonl").resolve() + + +def test_tui_command_rejects_watch_path_that_is_directory( + tmp_path: Path, + caplog: pytest.LogCaptureFixture, +) -> None: + """``--watch`` must be the metrics JSONL file, not a folder such as ``agents``.""" + pytest.importorskip("textual") + agents_dir = tmp_path / "agents" + agents_dir.mkdir() + runner = CliRunner() + with caplog.at_level(logging.ERROR, logger="openrtc"): + result = runner.invoke( + app, + ["tui", "--watch", str(agents_dir)], + catch_exceptions=False, + ) + assert result.exit_code == 1 + combined = caplog.text + (result.output or "") + assert "directory" in combined.lower() diff --git a/tests/test_tui_app.py b/tests/test_tui_app.py index 841a402..e4f77be 100644 --- a/tests/test_tui_app.py +++ b/tests/test_tui_app.py @@ -14,6 +14,15 @@ pytest.importorskip("textual") +def test_validate_metrics_watch_path_rejects_existing_directory(tmp_path: Path) -> None: + from openrtc.tui_app import validate_metrics_watch_path + + d = tmp_path / "agents" + d.mkdir() + with pytest.raises(ValueError, match="directory"): + validate_metrics_watch_path(d) + + @pytest.mark.asyncio async def test_metrics_tui_displays_event_line(tmp_path) -> None: from openrtc.metrics_stream import event_envelope