agent-cli 0.70.5__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 (196) hide show
  1. agent_cli/__init__.py +5 -0
  2. agent_cli/__main__.py +6 -0
  3. agent_cli/_extras.json +14 -0
  4. agent_cli/_requirements/.gitkeep +0 -0
  5. agent_cli/_requirements/audio.txt +79 -0
  6. agent_cli/_requirements/faster-whisper.txt +215 -0
  7. agent_cli/_requirements/kokoro.txt +425 -0
  8. agent_cli/_requirements/llm.txt +183 -0
  9. agent_cli/_requirements/memory.txt +355 -0
  10. agent_cli/_requirements/mlx-whisper.txt +222 -0
  11. agent_cli/_requirements/piper.txt +176 -0
  12. agent_cli/_requirements/rag.txt +402 -0
  13. agent_cli/_requirements/server.txt +154 -0
  14. agent_cli/_requirements/speed.txt +77 -0
  15. agent_cli/_requirements/vad.txt +155 -0
  16. agent_cli/_requirements/wyoming.txt +71 -0
  17. agent_cli/_tools.py +368 -0
  18. agent_cli/agents/__init__.py +23 -0
  19. agent_cli/agents/_voice_agent_common.py +136 -0
  20. agent_cli/agents/assistant.py +383 -0
  21. agent_cli/agents/autocorrect.py +284 -0
  22. agent_cli/agents/chat.py +496 -0
  23. agent_cli/agents/memory/__init__.py +31 -0
  24. agent_cli/agents/memory/add.py +190 -0
  25. agent_cli/agents/memory/proxy.py +160 -0
  26. agent_cli/agents/rag_proxy.py +128 -0
  27. agent_cli/agents/speak.py +209 -0
  28. agent_cli/agents/transcribe.py +671 -0
  29. agent_cli/agents/transcribe_daemon.py +499 -0
  30. agent_cli/agents/voice_edit.py +291 -0
  31. agent_cli/api.py +22 -0
  32. agent_cli/cli.py +106 -0
  33. agent_cli/config.py +503 -0
  34. agent_cli/config_cmd.py +307 -0
  35. agent_cli/constants.py +27 -0
  36. agent_cli/core/__init__.py +1 -0
  37. agent_cli/core/audio.py +461 -0
  38. agent_cli/core/audio_format.py +299 -0
  39. agent_cli/core/chroma.py +88 -0
  40. agent_cli/core/deps.py +191 -0
  41. agent_cli/core/openai_proxy.py +139 -0
  42. agent_cli/core/process.py +195 -0
  43. agent_cli/core/reranker.py +120 -0
  44. agent_cli/core/sse.py +87 -0
  45. agent_cli/core/transcription_logger.py +70 -0
  46. agent_cli/core/utils.py +526 -0
  47. agent_cli/core/vad.py +175 -0
  48. agent_cli/core/watch.py +65 -0
  49. agent_cli/dev/__init__.py +14 -0
  50. agent_cli/dev/cli.py +1588 -0
  51. agent_cli/dev/coding_agents/__init__.py +19 -0
  52. agent_cli/dev/coding_agents/aider.py +24 -0
  53. agent_cli/dev/coding_agents/base.py +167 -0
  54. agent_cli/dev/coding_agents/claude.py +39 -0
  55. agent_cli/dev/coding_agents/codex.py +24 -0
  56. agent_cli/dev/coding_agents/continue_dev.py +15 -0
  57. agent_cli/dev/coding_agents/copilot.py +24 -0
  58. agent_cli/dev/coding_agents/cursor_agent.py +48 -0
  59. agent_cli/dev/coding_agents/gemini.py +28 -0
  60. agent_cli/dev/coding_agents/opencode.py +15 -0
  61. agent_cli/dev/coding_agents/registry.py +49 -0
  62. agent_cli/dev/editors/__init__.py +19 -0
  63. agent_cli/dev/editors/base.py +89 -0
  64. agent_cli/dev/editors/cursor.py +15 -0
  65. agent_cli/dev/editors/emacs.py +46 -0
  66. agent_cli/dev/editors/jetbrains.py +56 -0
  67. agent_cli/dev/editors/nano.py +31 -0
  68. agent_cli/dev/editors/neovim.py +33 -0
  69. agent_cli/dev/editors/registry.py +59 -0
  70. agent_cli/dev/editors/sublime.py +20 -0
  71. agent_cli/dev/editors/vim.py +42 -0
  72. agent_cli/dev/editors/vscode.py +15 -0
  73. agent_cli/dev/editors/zed.py +20 -0
  74. agent_cli/dev/project.py +568 -0
  75. agent_cli/dev/registry.py +52 -0
  76. agent_cli/dev/skill/SKILL.md +141 -0
  77. agent_cli/dev/skill/examples.md +571 -0
  78. agent_cli/dev/terminals/__init__.py +19 -0
  79. agent_cli/dev/terminals/apple_terminal.py +82 -0
  80. agent_cli/dev/terminals/base.py +56 -0
  81. agent_cli/dev/terminals/gnome.py +51 -0
  82. agent_cli/dev/terminals/iterm2.py +84 -0
  83. agent_cli/dev/terminals/kitty.py +77 -0
  84. agent_cli/dev/terminals/registry.py +48 -0
  85. agent_cli/dev/terminals/tmux.py +58 -0
  86. agent_cli/dev/terminals/warp.py +132 -0
  87. agent_cli/dev/terminals/zellij.py +78 -0
  88. agent_cli/dev/worktree.py +856 -0
  89. agent_cli/docs_gen.py +417 -0
  90. agent_cli/example-config.toml +185 -0
  91. agent_cli/install/__init__.py +5 -0
  92. agent_cli/install/common.py +89 -0
  93. agent_cli/install/extras.py +174 -0
  94. agent_cli/install/hotkeys.py +48 -0
  95. agent_cli/install/services.py +87 -0
  96. agent_cli/memory/__init__.py +7 -0
  97. agent_cli/memory/_files.py +250 -0
  98. agent_cli/memory/_filters.py +63 -0
  99. agent_cli/memory/_git.py +157 -0
  100. agent_cli/memory/_indexer.py +142 -0
  101. agent_cli/memory/_ingest.py +408 -0
  102. agent_cli/memory/_persistence.py +182 -0
  103. agent_cli/memory/_prompt.py +91 -0
  104. agent_cli/memory/_retrieval.py +294 -0
  105. agent_cli/memory/_store.py +169 -0
  106. agent_cli/memory/_streaming.py +44 -0
  107. agent_cli/memory/_tasks.py +48 -0
  108. agent_cli/memory/api.py +113 -0
  109. agent_cli/memory/client.py +272 -0
  110. agent_cli/memory/engine.py +361 -0
  111. agent_cli/memory/entities.py +43 -0
  112. agent_cli/memory/models.py +112 -0
  113. agent_cli/opts.py +433 -0
  114. agent_cli/py.typed +0 -0
  115. agent_cli/rag/__init__.py +3 -0
  116. agent_cli/rag/_indexer.py +67 -0
  117. agent_cli/rag/_indexing.py +226 -0
  118. agent_cli/rag/_prompt.py +30 -0
  119. agent_cli/rag/_retriever.py +156 -0
  120. agent_cli/rag/_store.py +48 -0
  121. agent_cli/rag/_utils.py +218 -0
  122. agent_cli/rag/api.py +175 -0
  123. agent_cli/rag/client.py +299 -0
  124. agent_cli/rag/engine.py +302 -0
  125. agent_cli/rag/models.py +55 -0
  126. agent_cli/scripts/.runtime/.gitkeep +0 -0
  127. agent_cli/scripts/__init__.py +1 -0
  128. agent_cli/scripts/check_plugin_skill_sync.py +50 -0
  129. agent_cli/scripts/linux-hotkeys/README.md +63 -0
  130. agent_cli/scripts/linux-hotkeys/toggle-autocorrect.sh +45 -0
  131. agent_cli/scripts/linux-hotkeys/toggle-transcription.sh +58 -0
  132. agent_cli/scripts/linux-hotkeys/toggle-voice-edit.sh +58 -0
  133. agent_cli/scripts/macos-hotkeys/README.md +45 -0
  134. agent_cli/scripts/macos-hotkeys/skhd-config-example +5 -0
  135. agent_cli/scripts/macos-hotkeys/toggle-autocorrect.sh +12 -0
  136. agent_cli/scripts/macos-hotkeys/toggle-transcription.sh +37 -0
  137. agent_cli/scripts/macos-hotkeys/toggle-voice-edit.sh +37 -0
  138. agent_cli/scripts/nvidia-asr-server/README.md +99 -0
  139. agent_cli/scripts/nvidia-asr-server/pyproject.toml +27 -0
  140. agent_cli/scripts/nvidia-asr-server/server.py +255 -0
  141. agent_cli/scripts/nvidia-asr-server/shell.nix +32 -0
  142. agent_cli/scripts/nvidia-asr-server/uv.lock +4654 -0
  143. agent_cli/scripts/run-openwakeword.sh +11 -0
  144. agent_cli/scripts/run-piper-windows.ps1 +30 -0
  145. agent_cli/scripts/run-piper.sh +24 -0
  146. agent_cli/scripts/run-whisper-linux.sh +40 -0
  147. agent_cli/scripts/run-whisper-macos.sh +6 -0
  148. agent_cli/scripts/run-whisper-windows.ps1 +51 -0
  149. agent_cli/scripts/run-whisper.sh +9 -0
  150. agent_cli/scripts/run_faster_whisper_server.py +136 -0
  151. agent_cli/scripts/setup-linux-hotkeys.sh +72 -0
  152. agent_cli/scripts/setup-linux.sh +108 -0
  153. agent_cli/scripts/setup-macos-hotkeys.sh +61 -0
  154. agent_cli/scripts/setup-macos.sh +76 -0
  155. agent_cli/scripts/setup-windows.ps1 +63 -0
  156. agent_cli/scripts/start-all-services-windows.ps1 +53 -0
  157. agent_cli/scripts/start-all-services.sh +178 -0
  158. agent_cli/scripts/sync_extras.py +138 -0
  159. agent_cli/server/__init__.py +3 -0
  160. agent_cli/server/cli.py +721 -0
  161. agent_cli/server/common.py +222 -0
  162. agent_cli/server/model_manager.py +288 -0
  163. agent_cli/server/model_registry.py +225 -0
  164. agent_cli/server/proxy/__init__.py +3 -0
  165. agent_cli/server/proxy/api.py +444 -0
  166. agent_cli/server/streaming.py +67 -0
  167. agent_cli/server/tts/__init__.py +3 -0
  168. agent_cli/server/tts/api.py +335 -0
  169. agent_cli/server/tts/backends/__init__.py +82 -0
  170. agent_cli/server/tts/backends/base.py +139 -0
  171. agent_cli/server/tts/backends/kokoro.py +403 -0
  172. agent_cli/server/tts/backends/piper.py +253 -0
  173. agent_cli/server/tts/model_manager.py +201 -0
  174. agent_cli/server/tts/model_registry.py +28 -0
  175. agent_cli/server/tts/wyoming_handler.py +249 -0
  176. agent_cli/server/whisper/__init__.py +3 -0
  177. agent_cli/server/whisper/api.py +413 -0
  178. agent_cli/server/whisper/backends/__init__.py +89 -0
  179. agent_cli/server/whisper/backends/base.py +97 -0
  180. agent_cli/server/whisper/backends/faster_whisper.py +225 -0
  181. agent_cli/server/whisper/backends/mlx.py +270 -0
  182. agent_cli/server/whisper/languages.py +116 -0
  183. agent_cli/server/whisper/model_manager.py +157 -0
  184. agent_cli/server/whisper/model_registry.py +28 -0
  185. agent_cli/server/whisper/wyoming_handler.py +203 -0
  186. agent_cli/services/__init__.py +343 -0
  187. agent_cli/services/_wyoming_utils.py +64 -0
  188. agent_cli/services/asr.py +506 -0
  189. agent_cli/services/llm.py +228 -0
  190. agent_cli/services/tts.py +450 -0
  191. agent_cli/services/wake_word.py +142 -0
  192. agent_cli-0.70.5.dist-info/METADATA +2118 -0
  193. agent_cli-0.70.5.dist-info/RECORD +196 -0
  194. agent_cli-0.70.5.dist-info/WHEEL +4 -0
  195. agent_cli-0.70.5.dist-info/entry_points.txt +4 -0
  196. agent_cli-0.70.5.dist-info/licenses/LICENSE +21 -0
agent_cli/config.py ADDED
@@ -0,0 +1,503 @@
1
+ """Pydantic models for agent configurations, aligned with CLI option groups."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import tomllib
6
+ from pathlib import Path
7
+ from typing import Any, Literal
8
+
9
+ from pydantic import BaseModel, field_validator
10
+
11
+ from agent_cli.core.utils import console
12
+
13
+ USER_CONFIG_PATH = Path.home() / ".config" / "agent-cli" / "config.toml"
14
+
15
+ CONFIG_PATHS = [
16
+ Path("agent-cli-config.toml"),
17
+ USER_CONFIG_PATH,
18
+ ]
19
+
20
+
21
+ def _normalize_provider_value(field: str, value: str) -> str:
22
+ """Map deprecated provider names to their replacements."""
23
+ alias_map = _DEPRECATED_PROVIDER_ALIASES.get(field, {})
24
+ normalized = value.lower()
25
+ if normalized in alias_map:
26
+ replacement = alias_map[normalized]
27
+ console.print(
28
+ f"[yellow]Deprecated provider '{value}' for {field.replace('_', '-')}."
29
+ f" Using '{replacement}' instead.[/yellow]",
30
+ )
31
+ return replacement
32
+ return value
33
+
34
+
35
+ _DEPRECATED_PROVIDER_ALIASES: dict[str, dict[str, str]] = {
36
+ "llm_provider": {"local": "ollama"},
37
+ "asr_provider": {"local": "wyoming"},
38
+ "tts_provider": {"local": "wyoming"},
39
+ }
40
+
41
+ # --- Panel: Provider Selection ---
42
+
43
+
44
+ class ProviderSelection(BaseModel):
45
+ """Configuration for selecting service providers."""
46
+
47
+ llm_provider: Literal["ollama", "openai", "gemini"]
48
+ asr_provider: Literal["wyoming", "openai", "gemini"]
49
+ tts_provider: Literal["wyoming", "openai", "kokoro", "gemini"]
50
+
51
+ @field_validator("llm_provider", mode="before")
52
+ @classmethod
53
+ def _normalize_llm_provider(cls, v: str) -> str:
54
+ if isinstance(v, str):
55
+ return _normalize_provider_value("llm_provider", v)
56
+ return v
57
+
58
+ @field_validator("asr_provider", mode="before")
59
+ @classmethod
60
+ def _normalize_asr_provider(cls, v: str) -> str:
61
+ if isinstance(v, str):
62
+ return _normalize_provider_value("asr_provider", v)
63
+ return v
64
+
65
+ @field_validator("tts_provider", mode="before")
66
+ @classmethod
67
+ def _normalize_tts_provider(cls, v: str) -> str:
68
+ if isinstance(v, str):
69
+ return _normalize_provider_value("tts_provider", v)
70
+ return v
71
+
72
+
73
+ # --- Panel: LLM Configuration ---
74
+
75
+
76
+ class Ollama(BaseModel):
77
+ """Configuration for the local Ollama LLM provider."""
78
+
79
+ llm_ollama_model: str
80
+ llm_ollama_host: str
81
+
82
+
83
+ class OpenAILLM(BaseModel):
84
+ """Configuration for the OpenAI LLM provider."""
85
+
86
+ llm_openai_model: str
87
+ openai_api_key: str | None = None
88
+ openai_base_url: str | None = None
89
+
90
+
91
+ class GeminiLLM(BaseModel):
92
+ """Configuration for the Gemini LLM provider."""
93
+
94
+ llm_gemini_model: str
95
+ gemini_api_key: str | None = None
96
+
97
+
98
+ # --- Panel: ASR (Audio) Configuration ---
99
+
100
+
101
+ class AudioInput(BaseModel):
102
+ """Configuration for audio input devices."""
103
+
104
+ input_device_index: int | None = None
105
+ input_device_name: str | None = None
106
+
107
+
108
+ class WyomingASR(BaseModel):
109
+ """Configuration for the Wyoming ASR provider."""
110
+
111
+ asr_wyoming_ip: str
112
+ asr_wyoming_port: int
113
+ asr_wyoming_prompt: str | None = None
114
+
115
+ def get_effective_prompt(self, extra_instructions: str | None = None) -> str | None:
116
+ """Get the effective prompt, combining asr_wyoming_prompt with extra_instructions.
117
+
118
+ If both are set, asr_wyoming_prompt takes precedence and extra_instructions
119
+ is appended. If only one is set, that one is used.
120
+ """
121
+ if self.asr_wyoming_prompt and extra_instructions:
122
+ return f"{self.asr_wyoming_prompt}\n\n{extra_instructions}"
123
+ return self.asr_wyoming_prompt or extra_instructions
124
+
125
+
126
+ class OpenAIASR(BaseModel):
127
+ """Configuration for the OpenAI-compatible ASR provider."""
128
+
129
+ asr_openai_model: str
130
+ openai_api_key: str | None = None
131
+ openai_base_url: str | None = None
132
+ asr_openai_prompt: str | None = None
133
+
134
+ def get_effective_prompt(self, extra_instructions: str | None = None) -> str | None:
135
+ """Get the effective prompt, combining asr_openai_prompt with extra_instructions.
136
+
137
+ If both are set, asr_openai_prompt takes precedence and extra_instructions
138
+ is appended. If only one is set, that one is used.
139
+ """
140
+ if self.asr_openai_prompt and extra_instructions:
141
+ return f"{self.asr_openai_prompt}\n\n{extra_instructions}"
142
+ return self.asr_openai_prompt or extra_instructions
143
+
144
+
145
+ class GeminiASR(BaseModel):
146
+ """Configuration for the Gemini ASR provider."""
147
+
148
+ asr_gemini_model: str
149
+ gemini_api_key: str | None = None
150
+ asr_gemini_prompt: str | None = None
151
+
152
+ def get_effective_prompt(self, extra_instructions: str | None = None) -> str | None:
153
+ """Get the effective prompt, combining asr_gemini_prompt with extra_instructions.
154
+
155
+ If both are set, asr_gemini_prompt takes precedence and extra_instructions
156
+ is appended. If only one is set, that one is used.
157
+ """
158
+ if self.asr_gemini_prompt and extra_instructions:
159
+ return f"{self.asr_gemini_prompt}\n\n{extra_instructions}"
160
+ return self.asr_gemini_prompt or extra_instructions
161
+
162
+
163
+ # --- Panel: TTS (Text-to-Speech) Configuration ---
164
+
165
+
166
+ class AudioOutput(BaseModel):
167
+ """Configuration for audio output devices and TTS behavior."""
168
+
169
+ output_device_index: int | None = None
170
+ output_device_name: str | None = None
171
+ tts_speed: float = 1.0
172
+ enable_tts: bool = False
173
+
174
+
175
+ class WyomingTTS(BaseModel):
176
+ """Configuration for the Wyoming TTS provider."""
177
+
178
+ tts_wyoming_ip: str
179
+ tts_wyoming_port: int
180
+ tts_wyoming_voice: str | None = None
181
+ tts_wyoming_language: str | None = None
182
+ tts_wyoming_speaker: str | None = None
183
+
184
+
185
+ class OpenAITTS(BaseModel):
186
+ """Configuration for the OpenAI-compatible TTS provider."""
187
+
188
+ tts_openai_model: str
189
+ tts_openai_voice: str
190
+ openai_api_key: str | None = None
191
+ tts_openai_base_url: str | None = None
192
+
193
+
194
+ class KokoroTTS(BaseModel):
195
+ """Configuration for the Kokoro TTS provider."""
196
+
197
+ tts_kokoro_model: str
198
+ tts_kokoro_voice: str
199
+ tts_kokoro_host: str
200
+
201
+
202
+ class GeminiTTS(BaseModel):
203
+ """Configuration for the Gemini TTS provider."""
204
+
205
+ tts_gemini_model: str
206
+ tts_gemini_voice: str
207
+ gemini_api_key: str | None = None
208
+
209
+
210
+ # --- Panel: Wake Word Options ---
211
+
212
+
213
+ class WakeWord(BaseModel):
214
+ """Configuration for wake word detection."""
215
+
216
+ wake_server_ip: str
217
+ wake_server_port: int
218
+ wake_word: str
219
+
220
+
221
+ # --- Panel: General Options ---
222
+
223
+
224
+ class General(BaseModel):
225
+ """General configuration parameters for logging and I/O."""
226
+
227
+ log_level: str
228
+ log_file: str | None = None
229
+ quiet: bool
230
+ clipboard: bool = True
231
+ save_file: Path | None = None
232
+ list_devices: bool = False
233
+
234
+ @field_validator("save_file", mode="before")
235
+ @classmethod
236
+ def _expand_user_path(cls, v: str | None) -> Path | None:
237
+ if v:
238
+ return Path(v).expanduser()
239
+ return None
240
+
241
+
242
+ # --- Panel: History Options ---
243
+
244
+
245
+ class History(BaseModel):
246
+ """Configuration for conversation history."""
247
+
248
+ history_dir: Path | None = None
249
+ last_n_messages: int = 50
250
+
251
+ @field_validator("history_dir", mode="before")
252
+ @classmethod
253
+ def _expand_user_path(cls, v: str | None) -> Path | None:
254
+ if v:
255
+ return Path(v).expanduser()
256
+ return None
257
+
258
+
259
+ # --- Panel: Dev (Parallel Development) Options ---
260
+
261
+
262
+ class Dev(BaseModel):
263
+ """Configuration for parallel development environments (git worktrees)."""
264
+
265
+ default_agent: str | None = None
266
+ default_editor: str | None = None
267
+ agent_args: dict[str, list[str]] | None = (
268
+ None # Per-agent args, e.g. {"claude": ["--dangerously-skip-permissions"]}
269
+ )
270
+ setup: bool = True # Run project setup (npm install, etc.)
271
+ copy_env: bool = True # Copy .env files from main repo
272
+ fetch: bool = True # Git fetch before creating worktree
273
+
274
+
275
+ def _config_path(config_path_str: str | None = None) -> Path | None:
276
+ """Return a usable config path, expanding user directories."""
277
+ if config_path_str:
278
+ return Path(config_path_str).expanduser().resolve()
279
+
280
+ for path in CONFIG_PATHS:
281
+ candidate = path.expanduser()
282
+ if candidate.exists():
283
+ return candidate.resolve()
284
+ return None
285
+
286
+
287
+ def load_config(config_path_str: str | None = None) -> dict[str, Any]:
288
+ """Load the TOML configuration file and process it for nested structures.
289
+
290
+ Supports both flat sections like [autocorrect] and nested sections like
291
+ [memory.proxy]. Nested sections are flattened to dot-notation keys.
292
+ """
293
+ # Determine which config path to use
294
+ config_path = _config_path(config_path_str)
295
+ if config_path is None:
296
+ return {}
297
+ if config_path.exists():
298
+ with config_path.open("rb") as f:
299
+ cfg = tomllib.load(f)
300
+ # Flatten nested sections (e.g., [memory.proxy] -> "memory.proxy")
301
+ flattened = _flatten_nested_sections(cfg)
302
+ return {k: _replace_dashed_keys(v) for k, v in flattened.items()}
303
+ if config_path_str:
304
+ console.print(
305
+ f"[bold red]Config file not found at {config_path_str}[/bold red]",
306
+ )
307
+ return {}
308
+
309
+
310
+ def normalize_provider_defaults(cfg: dict[str, Any]) -> dict[str, Any]:
311
+ """Normalize deprecated provider names in a config section."""
312
+ normalized = dict(cfg)
313
+ for provider_key in ("llm_provider", "asr_provider", "tts_provider"):
314
+ if provider_key in normalized and isinstance(normalized[provider_key], str):
315
+ normalized[provider_key] = _normalize_provider_value(
316
+ provider_key,
317
+ normalized[provider_key],
318
+ )
319
+ return normalized
320
+
321
+
322
+ def _replace_dashed_keys(cfg: dict[str, Any]) -> dict[str, Any]:
323
+ return {k.replace("-", "_"): v for k, v in cfg.items()}
324
+
325
+
326
+ def _flatten_nested_sections(cfg: dict[str, Any], prefix: str = "") -> dict[str, Any]:
327
+ """Flatten nested TOML sections: {"a": {"b": {"x": 1}}} -> {"a.b": {"x": 1}}."""
328
+ result = {}
329
+ for key, value in cfg.items():
330
+ full_key = f"{prefix}.{key}" if prefix else key
331
+ if isinstance(value, dict) and any(isinstance(v, dict) for v in value.values()):
332
+ result.update(_flatten_nested_sections(value, full_key))
333
+ else:
334
+ result[full_key] = value
335
+ return result
336
+
337
+
338
+ # --- Common Config Bundle ---
339
+
340
+
341
+ class ProviderConfigs(BaseModel):
342
+ """Bundle of all provider-related configs constructed from CLI parameters."""
343
+
344
+ provider: ProviderSelection
345
+ audio_in: AudioInput
346
+ wyoming_asr: WyomingASR
347
+ openai_asr: OpenAIASR
348
+ gemini_asr: GeminiASR
349
+ ollama: Ollama
350
+ openai_llm: OpenAILLM
351
+ gemini_llm: GeminiLLM
352
+ audio_out: AudioOutput
353
+ wyoming_tts: WyomingTTS
354
+ openai_tts: OpenAITTS
355
+ kokoro_tts: KokoroTTS
356
+ gemini_tts: GeminiTTS
357
+
358
+
359
+ def create_provider_configs(
360
+ *,
361
+ # Provider selection
362
+ asr_provider: str,
363
+ llm_provider: str,
364
+ tts_provider: str,
365
+ # Audio input
366
+ input_device_index: int | None,
367
+ input_device_name: str | None,
368
+ # Wyoming ASR
369
+ asr_wyoming_ip: str,
370
+ asr_wyoming_port: int,
371
+ # OpenAI ASR
372
+ asr_openai_model: str,
373
+ asr_openai_base_url: str | None = None,
374
+ asr_openai_prompt: str | None = None,
375
+ # Gemini ASR
376
+ asr_gemini_model: str,
377
+ # Ollama LLM
378
+ llm_ollama_model: str,
379
+ llm_ollama_host: str,
380
+ # OpenAI LLM
381
+ llm_openai_model: str,
382
+ # Gemini LLM
383
+ llm_gemini_model: str,
384
+ # Shared API keys
385
+ openai_api_key: str | None,
386
+ openai_base_url: str | None,
387
+ gemini_api_key: str | None,
388
+ # Audio output
389
+ enable_tts: bool,
390
+ output_device_index: int | None,
391
+ output_device_name: str | None,
392
+ tts_speed: float,
393
+ # Wyoming TTS
394
+ tts_wyoming_ip: str,
395
+ tts_wyoming_port: int,
396
+ tts_wyoming_voice: str | None,
397
+ tts_wyoming_language: str | None,
398
+ tts_wyoming_speaker: str | None,
399
+ # OpenAI TTS
400
+ tts_openai_model: str,
401
+ tts_openai_voice: str,
402
+ tts_openai_base_url: str | None,
403
+ # Kokoro TTS
404
+ tts_kokoro_model: str,
405
+ tts_kokoro_voice: str,
406
+ tts_kokoro_host: str,
407
+ # Gemini TTS
408
+ tts_gemini_model: str,
409
+ tts_gemini_voice: str,
410
+ ) -> ProviderConfigs:
411
+ """Create all provider-related config objects from CLI parameters.
412
+
413
+ This factory function centralizes the construction of provider configs
414
+ to eliminate duplication across CLI commands.
415
+ """
416
+ return ProviderConfigs(
417
+ provider=ProviderSelection(
418
+ asr_provider=asr_provider,
419
+ llm_provider=llm_provider,
420
+ tts_provider=tts_provider,
421
+ ),
422
+ audio_in=AudioInput(
423
+ input_device_index=input_device_index,
424
+ input_device_name=input_device_name,
425
+ ),
426
+ wyoming_asr=WyomingASR(
427
+ asr_wyoming_ip=asr_wyoming_ip,
428
+ asr_wyoming_port=asr_wyoming_port,
429
+ ),
430
+ openai_asr=OpenAIASR(
431
+ asr_openai_model=asr_openai_model,
432
+ openai_api_key=openai_api_key,
433
+ openai_base_url=asr_openai_base_url or openai_base_url,
434
+ asr_openai_prompt=asr_openai_prompt,
435
+ ),
436
+ gemini_asr=GeminiASR(
437
+ asr_gemini_model=asr_gemini_model,
438
+ gemini_api_key=gemini_api_key,
439
+ ),
440
+ ollama=Ollama(
441
+ llm_ollama_model=llm_ollama_model,
442
+ llm_ollama_host=llm_ollama_host,
443
+ ),
444
+ openai_llm=OpenAILLM(
445
+ llm_openai_model=llm_openai_model,
446
+ openai_api_key=openai_api_key,
447
+ openai_base_url=openai_base_url,
448
+ ),
449
+ gemini_llm=GeminiLLM(
450
+ llm_gemini_model=llm_gemini_model,
451
+ gemini_api_key=gemini_api_key,
452
+ ),
453
+ audio_out=AudioOutput(
454
+ enable_tts=enable_tts,
455
+ output_device_index=output_device_index,
456
+ output_device_name=output_device_name,
457
+ tts_speed=tts_speed,
458
+ ),
459
+ wyoming_tts=WyomingTTS(
460
+ tts_wyoming_ip=tts_wyoming_ip,
461
+ tts_wyoming_port=tts_wyoming_port,
462
+ tts_wyoming_voice=tts_wyoming_voice,
463
+ tts_wyoming_language=tts_wyoming_language,
464
+ tts_wyoming_speaker=tts_wyoming_speaker,
465
+ ),
466
+ openai_tts=OpenAITTS(
467
+ tts_openai_model=tts_openai_model,
468
+ tts_openai_voice=tts_openai_voice,
469
+ openai_api_key=openai_api_key,
470
+ tts_openai_base_url=tts_openai_base_url,
471
+ ),
472
+ kokoro_tts=KokoroTTS(
473
+ tts_kokoro_model=tts_kokoro_model,
474
+ tts_kokoro_voice=tts_kokoro_voice,
475
+ tts_kokoro_host=tts_kokoro_host,
476
+ ),
477
+ gemini_tts=GeminiTTS(
478
+ tts_gemini_model=tts_gemini_model,
479
+ tts_gemini_voice=tts_gemini_voice,
480
+ gemini_api_key=gemini_api_key,
481
+ ),
482
+ )
483
+
484
+
485
+ # Parameter names used by create_provider_configs (all keyword-only)
486
+ _PROVIDER_CONFIG_PARAMS = frozenset(
487
+ create_provider_configs.__code__.co_varnames[
488
+ : create_provider_configs.__code__.co_kwonlyargcount
489
+ ],
490
+ )
491
+
492
+
493
+ def create_provider_configs_from_locals(local_vars: dict[str, Any]) -> ProviderConfigs:
494
+ """Create provider configs by extracting parameters from a locals() dict.
495
+
496
+ This helper enables one-line config creation in CLI commands by automatically
497
+ extracting the relevant parameters from the command's local variables.
498
+
499
+ Usage:
500
+ cfgs = config.create_provider_configs_from_locals(locals())
501
+ """
502
+ kwargs = {k: v for k, v in local_vars.items() if k in _PROVIDER_CONFIG_PARAMS}
503
+ return create_provider_configs(**kwargs)