agent-cli 0.66.2__py3-none-any.whl → 0.67.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. agent_cli/_extras.json +13 -0
  2. agent_cli/_requirements/audio.txt +71 -0
  3. agent_cli/_requirements/{whisper.txt → faster-whisper.txt} +5 -113
  4. agent_cli/_requirements/{tts-kokoro.txt → kokoro.txt} +5 -108
  5. agent_cli/_requirements/llm.txt +176 -0
  6. agent_cli/_requirements/memory.txt +7 -98
  7. agent_cli/_requirements/{whisper-mlx.txt → mlx-whisper.txt} +62 -125
  8. agent_cli/_requirements/{tts.txt → piper.txt} +6 -124
  9. agent_cli/_requirements/rag.txt +10 -97
  10. agent_cli/_requirements/server.txt +4 -122
  11. agent_cli/_requirements/speed.txt +7 -131
  12. agent_cli/_requirements/vad.txt +6 -130
  13. agent_cli/agents/assistant.py +2 -0
  14. agent_cli/agents/autocorrect.py +2 -0
  15. agent_cli/agents/chat.py +2 -0
  16. agent_cli/agents/memory/add.py +2 -0
  17. agent_cli/agents/memory/proxy.py +2 -0
  18. agent_cli/agents/rag_proxy.py +2 -0
  19. agent_cli/agents/speak.py +2 -0
  20. agent_cli/agents/transcribe.py +2 -0
  21. agent_cli/agents/transcribe_daemon.py +2 -0
  22. agent_cli/agents/voice_edit.py +2 -0
  23. agent_cli/core/deps.py +130 -14
  24. agent_cli/dev/skill/SKILL.md +2 -2
  25. agent_cli/docs_gen.py +0 -42
  26. agent_cli/install/extras.py +6 -14
  27. agent_cli/memory/__init__.py +1 -18
  28. agent_cli/rag/__init__.py +0 -19
  29. agent_cli/scripts/sync_extras.py +138 -0
  30. agent_cli/server/cli.py +4 -0
  31. agent_cli/services/_wyoming_utils.py +4 -2
  32. agent_cli/services/asr.py +13 -3
  33. agent_cli/services/tts.py +5 -2
  34. agent_cli/services/wake_word.py +6 -3
  35. {agent_cli-0.66.2.dist-info → agent_cli-0.67.1.dist-info}/METADATA +35 -30
  36. {agent_cli-0.66.2.dist-info → agent_cli-0.67.1.dist-info}/RECORD +39 -35
  37. {agent_cli-0.66.2.dist-info → agent_cli-0.67.1.dist-info}/WHEEL +0 -0
  38. {agent_cli-0.66.2.dist-info → agent_cli-0.67.1.dist-info}/entry_points.txt +0 -0
  39. {agent_cli-0.66.2.dist-info → agent_cli-0.67.1.dist-info}/licenses/LICENSE +0 -0
agent_cli/core/deps.py CHANGED
@@ -2,22 +2,138 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import functools
6
+ import json
5
7
  from importlib.util import find_spec
8
+ from pathlib import Path
9
+ from typing import TYPE_CHECKING, TypeVar
6
10
 
11
+ import typer
7
12
 
8
- def ensure_optional_dependencies(
9
- required: dict[str, str],
10
- *,
11
- extra_name: str,
12
- install_hint: str | None = None,
13
- ) -> None:
14
- """Ensure optional dependencies are present, otherwise raise ImportError."""
15
- missing = [
16
- pkg_name for module_name, pkg_name in required.items() if find_spec(module_name) is None
13
+ from agent_cli.core.utils import print_error_message
14
+
15
+ if TYPE_CHECKING:
16
+ from collections.abc import Callable
17
+
18
+ F = TypeVar("F", bound="Callable[..., object]")
19
+
20
+ # Load extras from JSON file
21
+ _EXTRAS_FILE = Path(__file__).parent.parent / "_extras.json"
22
+ EXTRAS: dict[str, tuple[str, list[str]]] = {
23
+ k: (v[0], v[1]) for k, v in json.loads(_EXTRAS_FILE.read_text()).items()
24
+ }
25
+
26
+
27
+ def _check_package_installed(pkg: str) -> bool:
28
+ """Check if a single package is installed."""
29
+ top_module = pkg.split(".")[0]
30
+ try:
31
+ return find_spec(top_module) is not None
32
+ except (ValueError, ModuleNotFoundError):
33
+ return False
34
+
35
+
36
+ def check_extra_installed(extra: str) -> bool:
37
+ """Check if packages for an extra are installed using find_spec (no actual import).
38
+
39
+ Supports `|` syntax for alternatives: "piper|kokoro" means ANY of these extras.
40
+ For regular extras, ALL packages must be installed.
41
+ """
42
+ # Handle "extra1|extra2" syntax - any of these extras is sufficient
43
+ if "|" in extra:
44
+ return any(check_extra_installed(e) for e in extra.split("|"))
45
+
46
+ if extra not in EXTRAS:
47
+ return True # Unknown extra, assume OK
48
+ _, packages = EXTRAS[extra]
49
+
50
+ # All packages must be installed
51
+ return all(_check_package_installed(pkg) for pkg in packages)
52
+
53
+
54
+ def get_install_hint(extra: str) -> str:
55
+ """Get install command hint for an extra.
56
+
57
+ Supports `|` syntax for alternatives: "piper|kokoro" shows both options.
58
+ """
59
+ # Handle "extra1|extra2" syntax - show all options
60
+ if "|" in extra:
61
+ alternatives = extra.split("|")
62
+ options = []
63
+ for alt in alternatives:
64
+ desc, _ = EXTRAS.get(alt, ("", []))
65
+ options.append((alt, desc))
66
+
67
+ lines = ["This command requires one of:"]
68
+ for alt, desc in options:
69
+ if desc:
70
+ lines.append(f" - '{alt}' ({desc})")
71
+ else:
72
+ lines.append(f" - '{alt}'")
73
+ lines.append("")
74
+ lines.append("Install one with:")
75
+ for alt, _ in options:
76
+ lines.append(f' uv tool install "agent-cli[{alt}]" -p 3.13')
77
+ lines.append(" # or")
78
+ for alt, _ in options:
79
+ lines.append(f" agent-cli install-extras {alt}")
80
+ return "\n".join(lines)
81
+
82
+ desc, _ = EXTRAS.get(extra, ("", []))
83
+ lines = [
84
+ f"This command requires the '{extra}' extra",
17
85
  ]
18
- if not missing:
19
- return
86
+ if desc:
87
+ lines[0] += f" ({desc})"
88
+ lines[0] += "."
89
+ lines.append("")
90
+ lines.append("Install with:")
91
+ lines.append(f' uv tool install "agent-cli[{extra}]" -p 3.13')
92
+ lines.append(" # or")
93
+ lines.append(f" agent-cli install-extras {extra}")
94
+ return "\n".join(lines)
95
+
96
+
97
+ def requires_extras(*extras: str) -> Callable[[F], F]:
98
+ """Decorator to declare required extras for a command.
99
+
100
+ When a required extra is missing, the decorator prints a helpful error
101
+ message and exits with code 1.
102
+
103
+ The decorator stores the required extras on the function for test validation.
104
+
105
+ Process management flags (--stop, --status, --toggle) skip the dependency
106
+ check since they just manage running processes without using the actual
107
+ dependencies.
108
+
109
+ Example:
110
+ @app.command("rag-proxy")
111
+ @requires_extras("rag")
112
+ def rag_proxy(...):
113
+ ...
114
+
115
+ """
116
+
117
+ def decorator(func: F) -> F:
118
+ # Store extras on function for test introspection
119
+ func._required_extras = extras # type: ignore[attr-defined]
120
+
121
+ @functools.wraps(func)
122
+ def wrapper(*args: object, **kwargs: object) -> object:
123
+ # Skip dependency check for process management and info operations
124
+ # These don't need the actual dependencies, just manage processes or list info
125
+ if any(kwargs.get(flag) for flag in ("stop", "status", "toggle", "list_devices")):
126
+ return func(*args, **kwargs)
127
+
128
+ missing = [e for e in extras if not check_extra_installed(e)]
129
+ if missing:
130
+ for extra in missing:
131
+ print_error_message(get_install_hint(extra))
132
+ raise typer.Exit(1)
133
+ return func(*args, **kwargs)
134
+
135
+ # Preserve the extras on wrapper too
136
+ wrapper._required_extras = extras # type: ignore[attr-defined]
137
+ return wrapper # type: ignore[return-value]
20
138
 
21
- hint = install_hint or f"`pip install agent-cli[{extra_name}]`"
22
- msg = f"Missing required dependencies for {extra_name}: {', '.join(missing)}. Please install with {hint}."
23
- raise ImportError(msg)
139
+ return decorator
@@ -13,10 +13,10 @@ If `agent-cli` is not available, install it first:
13
13
 
14
14
  ```bash
15
15
  # Install globally
16
- uv tool install agent-cli
16
+ uv tool install agent-cli -p 3.13
17
17
 
18
18
  # Or run directly without installing
19
- uvx agent-cli dev new <branch-name> --agent --prompt "..."
19
+ uvx --python 3.13 agent-cli dev new <branch-name> --agent --prompt "..."
20
20
  ```
21
21
 
22
22
  ## When to spawn parallel agents
agent_cli/docs_gen.py CHANGED
@@ -16,7 +16,6 @@ Example usage in Markdown files:
16
16
 
17
17
  from __future__ import annotations
18
18
 
19
- from pathlib import Path
20
19
  from typing import Any, get_origin
21
20
 
22
21
  import click
@@ -369,47 +368,6 @@ def config_example(command_path: str | None = None) -> str:
369
368
  return "\n".join(lines)
370
369
 
371
370
 
372
- def readme_section(section_name: str) -> str:
373
- """Extract a section from README.md for reuse in other docs.
374
-
375
- Sections are marked with HTML comments like:
376
- <!-- SECTION:section_name:START -->
377
- Content here...
378
- <!-- SECTION:section_name:END -->
379
-
380
- Args:
381
- section_name: The name of the section to extract (e.g., "why-i-built-this")
382
-
383
- Returns:
384
- The content between the section markers (without the markers themselves)
385
-
386
- """
387
- # Find the README.md relative to this module
388
- readme_path = Path(__file__).parent.parent / "README.md"
389
- if not readme_path.exists():
390
- return f"*Could not find README.md at {readme_path}*"
391
-
392
- content = readme_path.read_text()
393
-
394
- # Look for section markers
395
- start_marker = f"<!-- SECTION:{section_name}:START -->"
396
- end_marker = f"<!-- SECTION:{section_name}:END -->"
397
-
398
- start_idx = content.find(start_marker)
399
- if start_idx == -1:
400
- return f"*Section '{section_name}' not found in README.md*"
401
-
402
- end_idx = content.find(end_marker, start_idx)
403
- if end_idx == -1:
404
- return f"*End marker for section '{section_name}' not found in README.md*"
405
-
406
- # Extract content between markers (excluding the markers themselves)
407
- section_content = content[start_idx + len(start_marker) : end_idx]
408
-
409
- # Strip leading/trailing whitespace but preserve internal formatting
410
- return section_content.strip()
411
-
412
-
413
371
  def all_options_for_docs(command_path: str) -> str:
414
372
  """Generate complete options documentation for a command page.
415
373
 
@@ -12,20 +12,11 @@ from typing import Annotated
12
12
  import typer
13
13
 
14
14
  from agent_cli.cli import app
15
+ from agent_cli.core.deps import EXTRAS as _EXTRAS_META
15
16
  from agent_cli.core.utils import console, print_error_message
16
17
 
17
- # Extra name -> description mapping (used for docs generation)
18
- EXTRAS: dict[str, str] = {
19
- "rag": "RAG proxy (ChromaDB, embeddings)",
20
- "memory": "Long-term memory proxy",
21
- "vad": "Voice Activity Detection (silero-vad)",
22
- "whisper": "Local Whisper ASR (faster-whisper)",
23
- "whisper-mlx": "MLX Whisper for Apple Silicon",
24
- "tts": "Local Piper TTS",
25
- "tts-kokoro": "Kokoro neural TTS",
26
- "server": "FastAPI server components",
27
- "speed": "Audio speed adjustment (audiostretchy)",
28
- }
18
+ # Extract descriptions from the centralized EXTRAS metadata
19
+ EXTRAS: dict[str, str] = {name: desc for name, (desc, _) in _EXTRAS_META.items()}
29
20
 
30
21
 
31
22
  def _requirements_dir() -> Path:
@@ -72,8 +63,9 @@ def _install_via_uv_tool(extras: list[str]) -> bool:
72
63
  """Reinstall agent-cli via uv tool with the specified extras."""
73
64
  extras_str = ",".join(extras)
74
65
  package_spec = f"agent-cli[{extras_str}]"
75
- console.print(f"Reinstalling via uv tool: [cyan]{package_spec}[/]")
76
- cmd = ["uv", "tool", "install", package_spec, "--force"]
66
+ python_version = f"{sys.version_info.major}.{sys.version_info.minor}"
67
+ console.print(f"Reinstalling via uv tool: [cyan]{package_spec}[/] (Python {python_version})")
68
+ cmd = ["uv", "tool", "install", package_spec, "--force", "--python", python_version]
77
69
  result = subprocess.run(cmd, check=False)
78
70
  return result.returncode == 0
79
71
 
@@ -2,23 +2,6 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from agent_cli.core.deps import ensure_optional_dependencies
6
-
7
- _REQUIRED_DEPS = {
8
- "chromadb": "chromadb",
9
- "fastapi": "fastapi",
10
- "uvicorn": "uvicorn",
11
- "onnxruntime": "onnxruntime",
12
- "huggingface_hub": "huggingface-hub",
13
- "transformers": "transformers",
14
- }
15
-
16
- ensure_optional_dependencies(
17
- _REQUIRED_DEPS,
18
- extra_name="memory",
19
- install_hint="`pip install agent-cli[memory]` or `uv sync --extra memory`",
20
- )
21
-
22
- from agent_cli.memory.client import MemoryClient # noqa: E402
5
+ from agent_cli.memory.client import MemoryClient
23
6
 
24
7
  __all__ = ["MemoryClient"]
agent_cli/rag/__init__.py CHANGED
@@ -1,22 +1,3 @@
1
1
  """RAG module."""
2
2
 
3
3
  from __future__ import annotations
4
-
5
- from agent_cli.core.deps import ensure_optional_dependencies
6
-
7
- _REQUIRED_DEPS = {
8
- "chromadb": "chromadb",
9
- "watchfiles": "watchfiles",
10
- "markitdown": "markitdown",
11
- "fastapi": "fastapi",
12
- "uvicorn": "uvicorn",
13
- "onnxruntime": "onnxruntime",
14
- "huggingface_hub": "huggingface-hub",
15
- "transformers": "transformers",
16
- }
17
-
18
- ensure_optional_dependencies(
19
- _REQUIRED_DEPS,
20
- extra_name="rag",
21
- install_hint="`pip install agent-cli[rag]` or `uv sync --extra rag`",
22
- )
@@ -0,0 +1,138 @@
1
+ #!/usr/bin/env python3
2
+ """Generate _extras.json from pyproject.toml.
3
+
4
+ This script parses the optional-dependencies in pyproject.toml and generates
5
+ the agent_cli/_extras.json file with package-to-import mappings.
6
+
7
+ Usage:
8
+ python scripts/sync_extras.py
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import re
15
+ import sys
16
+ import tomllib
17
+ from pathlib import Path
18
+
19
+ REPO_ROOT = Path(__file__).parent.parent
20
+ PYPROJECT = REPO_ROOT / "pyproject.toml"
21
+ EXTRAS_FILE = REPO_ROOT / "agent_cli" / "_extras.json"
22
+
23
+ # Extras to skip (dev/test dependencies, not runtime installable)
24
+ SKIP_EXTRAS = {"dev", "test"}
25
+
26
+ # Manual mapping of extra name -> (description, list of import names)
27
+ # Import names should be the Python module name (how you import it)
28
+ # Bundle extras (voice, cloud, full) have empty import lists since they just install other extras
29
+ EXTRA_METADATA: dict[str, tuple[str, list[str]]] = {
30
+ # Provider extras (base dependencies now optional)
31
+ "audio": ("Audio recording/playback", ["sounddevice"]),
32
+ "wyoming": ("Wyoming protocol support", ["wyoming"]),
33
+ "openai": ("OpenAI API provider", ["openai"]),
34
+ "gemini": ("Google Gemini provider", ["google.genai"]),
35
+ "llm": ("LLM framework (pydantic-ai)", ["pydantic_ai"]),
36
+ # Feature extras
37
+ "rag": ("RAG proxy (ChromaDB, embeddings)", ["chromadb"]),
38
+ "memory": ("Long-term memory proxy", ["chromadb", "yaml"]),
39
+ "vad": ("Voice Activity Detection (silero-vad)", ["silero_vad"]),
40
+ "whisper": ("Local Whisper ASR (faster-whisper)", ["faster_whisper"]),
41
+ "whisper-mlx": ("MLX Whisper for Apple Silicon", ["mlx_whisper"]),
42
+ "tts": ("Local Piper TTS", ["piper"]),
43
+ "tts-kokoro": ("Kokoro neural TTS", ["kokoro"]),
44
+ "server": ("FastAPI server components", ["fastapi"]),
45
+ "speed": ("Audio speed adjustment (audiostretchy)", ["audiostretchy"]),
46
+ }
47
+
48
+
49
+ def get_extras_from_pyproject() -> set[str]:
50
+ """Parse optional-dependencies from pyproject.toml."""
51
+ with PYPROJECT.open("rb") as f:
52
+ data = tomllib.load(f)
53
+ all_extras = set(data.get("project", {}).get("optional-dependencies", {}).keys())
54
+ return all_extras - SKIP_EXTRAS
55
+
56
+
57
+ def extract_package_name(dep: str) -> str:
58
+ """Extract the package name from a dependency specification.
59
+
60
+ Examples:
61
+ "chromadb>=0.4.22" -> "chromadb"
62
+ "pydantic-ai-slim[openai,duckduckgo]" -> "pydantic-ai-slim"
63
+ 'mlx-whisper>=0.4.0; sys_platform == "darwin"' -> "mlx-whisper"
64
+
65
+ """
66
+ # Remove markers (;...) and extras ([...])
67
+ dep = re.split(r"[;\[]", dep)[0]
68
+ # Remove version specifiers
69
+ dep = re.split(r"[<>=!~]", dep)[0]
70
+ return dep.strip()
71
+
72
+
73
+ def package_to_import_name(package: str) -> str:
74
+ """Convert a package name to its Python import name.
75
+
76
+ Examples:
77
+ "google-genai" -> "google.genai"
78
+ "pydantic-ai-slim" -> "pydantic_ai"
79
+ "silero-vad" -> "silero_vad"
80
+ "faster-whisper" -> "faster_whisper"
81
+
82
+ """
83
+ # Special cases where the import name differs significantly
84
+ special_cases = {
85
+ "google-genai": "google.genai",
86
+ "pydantic-ai-slim": "pydantic_ai",
87
+ "silero-vad": "silero_vad",
88
+ "faster-whisper": "faster_whisper",
89
+ "mlx-whisper": "mlx_whisper",
90
+ "piper-tts": "piper",
91
+ "huggingface-hub": "huggingface_hub",
92
+ "fastapi": "fastapi",
93
+ "audiostretchy": "audiostretchy",
94
+ }
95
+ if package in special_cases:
96
+ return special_cases[package]
97
+ # Default: replace hyphens with underscores
98
+ return package.replace("-", "_")
99
+
100
+
101
+ def generate_extras_json(extras: set[str]) -> dict[str, list]:
102
+ """Generate the content for _extras.json."""
103
+ result = {}
104
+ for extra in sorted(extras):
105
+ if extra in EXTRA_METADATA:
106
+ desc, imports = EXTRA_METADATA[extra]
107
+ result[extra] = [desc, imports]
108
+ else:
109
+ # Unknown extra - add a placeholder
110
+ result[extra] = ["TODO: add description", []]
111
+ return result
112
+
113
+
114
+ def check_missing_metadata(extras: set[str]) -> list[str]:
115
+ """Check for extras that don't have metadata defined."""
116
+ return [e for e in extras if e not in EXTRA_METADATA]
117
+
118
+
119
+ def main() -> int:
120
+ """Generate _extras.json from pyproject.toml."""
121
+ extras = get_extras_from_pyproject()
122
+
123
+ # Check for missing metadata
124
+ missing = check_missing_metadata(extras)
125
+ if missing:
126
+ print(f"Warning: The following extras need metadata in EXTRA_METADATA: {missing}")
127
+ print("Please update EXTRA_METADATA in scripts/sync_extras.py")
128
+
129
+ # Generate the file
130
+ content = generate_extras_json(extras)
131
+ EXTRAS_FILE.write_text(json.dumps(content, indent=2) + "\n")
132
+ print(f"Generated {EXTRAS_FILE}")
133
+
134
+ return 0
135
+
136
+
137
+ if __name__ == "__main__":
138
+ sys.exit(main())
agent_cli/server/cli.py CHANGED
@@ -12,6 +12,7 @@ import typer
12
12
  from rich.console import Console
13
13
 
14
14
  from agent_cli.cli import app as main_app
15
+ from agent_cli.core.deps import requires_extras
15
16
  from agent_cli.core.process import set_process_title
16
17
  from agent_cli.server.common import setup_rich_logging
17
18
 
@@ -163,6 +164,7 @@ def _check_whisper_deps(backend: str, *, download_only: bool = False) -> None:
163
164
 
164
165
 
165
166
  @app.command("whisper")
167
+ @requires_extras("server", "faster-whisper|mlx-whisper")
166
168
  def whisper_cmd( # noqa: PLR0912, PLR0915
167
169
  model: Annotated[
168
170
  list[str] | None,
@@ -422,6 +424,7 @@ def whisper_cmd( # noqa: PLR0912, PLR0915
422
424
 
423
425
 
424
426
  @app.command("transcription-proxy")
427
+ @requires_extras("server")
425
428
  def transcription_proxy_cmd(
426
429
  host: Annotated[
427
430
  str,
@@ -475,6 +478,7 @@ def transcription_proxy_cmd(
475
478
 
476
479
 
477
480
  @app.command("tts")
481
+ @requires_extras("server", "piper|kokoro")
478
482
  def tts_cmd( # noqa: PLR0915
479
483
  model: Annotated[
480
484
  list[str] | None,
@@ -5,14 +5,14 @@ from __future__ import annotations
5
5
  from contextlib import asynccontextmanager
6
6
  from typing import TYPE_CHECKING
7
7
 
8
- from wyoming.client import AsyncClient
9
-
10
8
  from agent_cli.core.utils import print_error_message
11
9
 
12
10
  if TYPE_CHECKING:
13
11
  import logging
14
12
  from collections.abc import AsyncGenerator
15
13
 
14
+ from wyoming.client import AsyncClient
15
+
16
16
 
17
17
  @asynccontextmanager
18
18
  async def wyoming_client_context(
@@ -40,6 +40,8 @@ async def wyoming_client_context(
40
40
  Exception: For other connection errors
41
41
 
42
42
  """
43
+ from wyoming.client import AsyncClient # noqa: PLC0415
44
+
43
45
  uri = f"tcp://{server_ip}:{server_port}"
44
46
  logger.info("Connecting to Wyoming %s server at %s", server_type, uri)
45
47
 
agent_cli/services/asr.py CHANGED
@@ -10,9 +10,6 @@ from functools import partial
10
10
  from pathlib import Path
11
11
  from typing import TYPE_CHECKING
12
12
 
13
- from wyoming.asr import Transcribe, Transcript, TranscriptChunk, TranscriptStart, TranscriptStop
14
- from wyoming.audio import AudioChunk, AudioStart, AudioStop
15
-
16
13
  from agent_cli import constants
17
14
  from agent_cli.core.audio import (
18
15
  open_audio_stream,
@@ -225,6 +222,9 @@ async def _send_audio(
225
222
  initial_prompt: str | None = None,
226
223
  ) -> None:
227
224
  """Read from mic and send to Wyoming server."""
225
+ from wyoming.asr import Transcribe # noqa: PLC0415
226
+ from wyoming.audio import AudioChunk, AudioStart, AudioStop # noqa: PLC0415
227
+
228
228
  # Build context with initial_prompt if provided
229
229
  context = {"initial_prompt": initial_prompt} if initial_prompt else None
230
230
  await client.write_event(Transcribe(context=context).event())
@@ -282,6 +282,13 @@ async def _receive_transcript(
282
282
  final_callback: Callable[[str], None] | None = None,
283
283
  ) -> str:
284
284
  """Receive transcription events and return the final transcript."""
285
+ from wyoming.asr import ( # noqa: PLC0415
286
+ Transcript,
287
+ TranscriptChunk,
288
+ TranscriptStart,
289
+ TranscriptStop,
290
+ )
291
+
285
292
  transcript_text = ""
286
293
  while True:
287
294
  event = await client.read_event()
@@ -370,6 +377,9 @@ async def _transcribe_recorded_audio_wyoming(
370
377
  **_kwargs: object,
371
378
  ) -> str:
372
379
  """Process pre-recorded audio data with Wyoming ASR server."""
380
+ from wyoming.asr import Transcribe # noqa: PLC0415
381
+ from wyoming.audio import AudioChunk, AudioStart, AudioStop # noqa: PLC0415
382
+
373
383
  try:
374
384
  async with wyoming_client_context(
375
385
  wyoming_asr_cfg.asr_wyoming_ip,
agent_cli/services/tts.py CHANGED
@@ -10,8 +10,6 @@ from pathlib import Path
10
10
  from typing import TYPE_CHECKING
11
11
 
12
12
  from rich.live import Live
13
- from wyoming.audio import AudioChunk, AudioStart, AudioStop
14
- from wyoming.tts import Synthesize, SynthesizeVoice
15
13
 
16
14
  from agent_cli import config, constants
17
15
  from agent_cli.core.audio import open_audio_stream, setup_output_stream
@@ -32,6 +30,7 @@ if TYPE_CHECKING:
32
30
 
33
31
  from rich.live import Live
34
32
  from wyoming.client import AsyncClient
33
+ from wyoming.tts import Synthesize
35
34
 
36
35
 
37
36
  has_audiostretchy = importlib.util.find_spec("audiostretchy") is not None
@@ -134,6 +133,8 @@ def _create_synthesis_request(
134
133
  speaker: str | None = None,
135
134
  ) -> Synthesize:
136
135
  """Create a synthesis request with optional voice parameters."""
136
+ from wyoming.tts import Synthesize, SynthesizeVoice # noqa: PLC0415
137
+
137
138
  synthesize_event = Synthesize(text=text)
138
139
 
139
140
  # Add voice parameters if specified
@@ -152,6 +153,8 @@ async def _process_audio_events(
152
153
  logger: logging.Logger,
153
154
  ) -> tuple[bytes, int | None, int | None, int | None]:
154
155
  """Process audio events from TTS server and return audio data with metadata."""
156
+ from wyoming.audio import AudioChunk, AudioStart, AudioStop # noqa: PLC0415
157
+
155
158
  audio_data = io.BytesIO()
156
159
  sample_rate = None
157
160
  sample_width = None
@@ -6,9 +6,6 @@ import asyncio
6
6
  from functools import partial
7
7
  from typing import TYPE_CHECKING
8
8
 
9
- from wyoming.audio import AudioChunk, AudioStart, AudioStop
10
- from wyoming.wake import Detect, Detection, NotDetected
11
-
12
9
  from agent_cli import config, constants
13
10
  from agent_cli.core.audio import read_from_queue
14
11
  from agent_cli.core.utils import manage_send_receive_tasks
@@ -38,6 +35,8 @@ async def _send_audio_from_queue_for_wake_detection(
38
35
  progress_message: str,
39
36
  ) -> None:
40
37
  """Read from a queue and send to Wyoming wake word server."""
38
+ from wyoming.audio import AudioChunk, AudioStart, AudioStop # noqa: PLC0415
39
+
41
40
  await client.write_event(AudioStart(**constants.WYOMING_AUDIO_CONFIG).event())
42
41
  seconds_streamed = 0.0
43
42
 
@@ -76,6 +75,8 @@ async def _receive_wake_detection(
76
75
  Name of detected wake word or None if no detection
77
76
 
78
77
  """
78
+ from wyoming.wake import Detection, NotDetected # noqa: PLC0415
79
+
79
80
  while True:
80
81
  event = await client.read_event()
81
82
  if event is None:
@@ -108,6 +109,8 @@ async def _detect_wake_word_from_queue(
108
109
  progress_message: str = "Listening for wake word",
109
110
  ) -> str | None:
110
111
  """Detect wake word from an audio queue."""
112
+ from wyoming.wake import Detect # noqa: PLC0415
113
+
111
114
  try:
112
115
  async with wyoming_client_context(
113
116
  wake_word_cfg.wake_server_ip,