Skip to content
Merged
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
4 changes: 3 additions & 1 deletion aai_cli/agent/voices.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

from dataclasses import dataclass

from aai_cli.core import choices


@dataclass(frozen=True)
class Voice:
Expand Down Expand Up @@ -71,4 +73,4 @@ def format_voice_list() -> str:

def complete_voice(incomplete: str) -> list[str]:
"""Shell-completion callback for ``--voice``: known voice ids matching the prefix."""
return [name for name in VOICE_NAMES if name.startswith(incomplete)]
return choices.complete_prefix(VOICE_NAMES, incomplete)
3 changes: 2 additions & 1 deletion aai_cli/agent_framework/voices.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

from __future__ import annotations

from aai_cli.core import choices
from aai_cli.tts import voices as tts_voices

DEFAULT_VOICE = "jane"
Expand All @@ -29,7 +30,7 @@

def complete_voice(incomplete: str) -> list[str]:
"""Shell-completion callback for ``--voice``: catalog ids matching the prefix."""
return [name for name in VOICE_NAMES if name.startswith(incomplete)]
return choices.complete_prefix(VOICE_NAMES, incomplete)


def format_voice_list() -> str:
Expand Down
27 changes: 27 additions & 0 deletions aai_cli/app/agent_shared.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"""Run-logic shared by the two voice commands (`agent` and `agent-framework`).

Both build a live terminal conversation and resolve the persona the same way, so
the shared piece lives in the `app/` layer (the `doctor_checks`/`setup_exec`
precedent) rather than being copied between the two command packages.
"""

from __future__ import annotations

from pathlib import Path

from aai_cli.core.errors import CLIError


def resolve_system_prompt(system_prompt: str, system_prompt_file: Path | None) -> str:
"""The persona text: a --system-prompt-file (if given) overrides --system-prompt."""
if system_prompt_file is None:
return system_prompt
try:
return system_prompt_file.read_text(encoding="utf-8")
except OSError as exc:
raise CLIError(
f"Could not read --system-prompt-file {system_prompt_file}: {exc}",
error_type="file_not_found",
exit_code=2,
suggestion="Check the path and that the file is readable.",
) from exc
18 changes: 2 additions & 16 deletions aai_cli/commands/agent/_exec.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,10 @@
from aai_cli.agent.render import AgentRenderer
from aai_cli.agent.session import AgentRunConfig, run_session
from aai_cli.agent.voices import VOICE_NAMES
from aai_cli.app.agent_shared import resolve_system_prompt as _resolve_system_prompt
from aai_cli.app.context import AppState
from aai_cli.core import choices, client
from aai_cli.core.errors import CLIError, UsageError
from aai_cli.core.errors import UsageError
from aai_cli.streaming.session import resolve_output_modes
from aai_cli.streaming.sources import FileSource
from aai_cli.ui import output
Expand All @@ -48,21 +49,6 @@ class AgentOptions:
show_code: bool


def _resolve_system_prompt(system_prompt: str, system_prompt_file: Path | None) -> str:
"""The persona text: a --system-prompt-file (if given) overrides --system-prompt."""
if system_prompt_file is None:
return system_prompt
try:
return system_prompt_file.read_text(encoding="utf-8")
except OSError as exc:
raise CLIError(
f"Could not read --system-prompt-file {system_prompt_file}: {exc}",
error_type="file_not_found",
exit_code=2,
suggestion="Check the path and that the file is readable.",
) from exc


def _open_audio(
renderer: AgentRenderer,
*,
Expand Down
18 changes: 2 additions & 16 deletions aai_cli/commands/agent_framework/_exec.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@
from aai_cli.agent.render import AgentRenderer
from aai_cli.agent_framework import engine, voices
from aai_cli.agent_framework.config import CascadeConfig
from aai_cli.app.agent_shared import resolve_system_prompt as _resolve_system_prompt
from aai_cli.app.context import AppState
from aai_cli.core import choices, client
from aai_cli.core.errors import CLIError, UsageError
from aai_cli.core.errors import UsageError
from aai_cli.streaming.session import resolve_output_modes
from aai_cli.streaming.sources import FileSource
from aai_cli.tts import session as tts_session
Expand All @@ -46,21 +47,6 @@ class AgentFrameworkOptions:
output_field: choices.TextOrJson | None


def _resolve_system_prompt(system_prompt: str, system_prompt_file: Path | None) -> str:
"""The persona text: a --system-prompt-file (if given) overrides --system-prompt."""
if system_prompt_file is None:
return system_prompt
try:
return system_prompt_file.read_text(encoding="utf-8")
except OSError as exc:
raise CLIError(
f"Could not read --system-prompt-file {system_prompt_file}: {exc}",
error_type="file_not_found",
exit_code=2,
suggestion="Check the path and that the file is readable.",
) from exc


def _open_audio(
renderer: AgentRenderer,
*,
Expand Down
7 changes: 7 additions & 0 deletions aai_cli/core/choices.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
from __future__ import annotations

import enum
from collections.abc import Iterable


def complete_prefix(options: Iterable[str], incomplete: str) -> list[str]:
"""Shell-completion callback body: the ``options`` that start with the typed prefix."""
return [option for option in options if option.startswith(incomplete)]


# CLI-owned closed value sets for ``-o/--output``. ``StrEnum`` members *are* their
# string values: Typer renders them as choices in ``--help`` (e.g.
Expand Down
4 changes: 2 additions & 2 deletions aai_cli/core/llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from collections.abc import Sequence
from typing import TYPE_CHECKING

from aai_cli.core import environments
from aai_cli.core import choices, environments
from aai_cli.core.errors import APIError, UsageError, auth_failure

if TYPE_CHECKING:
Expand Down Expand Up @@ -41,7 +41,7 @@ def complete_model(incomplete: str) -> list[str]:
The gateway accepts more than this curated list, so completion only *suggests*
these — it never restricts what you can type.
"""
return [m for m in KNOWN_MODELS if m.startswith(incomplete)]
return choices.complete_prefix(KNOWN_MODELS, incomplete)


def parse_gateway_overrides(pairs: Sequence[str]) -> dict[str, object]:
Expand Down
2 changes: 2 additions & 0 deletions tests/test_agent_framework_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ def test_resolve_system_prompt_unreadable_file_errors(tmp_path):
with pytest.raises(CLIError, match="Could not read --system-prompt-file") as exc:
_exec._resolve_system_prompt("x", missing)
assert exc.value.exit_code == 2
assert exc.value.error_type == "file_not_found"
assert "readable" in (exc.value.suggestion or "")


# --- _open_audio -------------------------------------------------------------
Expand Down
7 changes: 7 additions & 0 deletions tests/test_completion.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,17 @@
import typer

from aai_cli.agent.voices import VOICE_NAMES, complete_voice
from aai_cli.core import choices
from aai_cli.core.llm import KNOWN_MODELS, complete_model
from aai_cli.main import app


def test_complete_prefix_keeps_only_matching_options():
# The shared completion body that complete_voice/complete_model delegate to: it
# filters to the prefix (dropping "banana") rather than echoing every option back.
assert choices.complete_prefix(["apple", "apricot", "banana"], "ap") == ["apple", "apricot"]


def test_shell_completion_is_enabled():
# add_completion=True registers Typer's --install-completion on the root command.
# Introspect the Click command rather than rendered --help text, which wraps at
Expand Down
Loading