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
@@ -0,0 +1,496 @@
1
+ """An chat agent that you can talk to.
2
+
3
+ This agent will:
4
+ - Listen for your voice command.
5
+ - Transcribe the command.
6
+ - Send the transcription to an LLM.
7
+ - Speak the LLM's response.
8
+ - Remember the conversation history.
9
+ - Attach timestamps to the saved conversation.
10
+ - Format timestamps as "ago" when sending to the LLM.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import asyncio
16
+ import json
17
+ import logging
18
+ import os
19
+ import time
20
+ from contextlib import suppress
21
+ from datetime import UTC, datetime
22
+ from pathlib import Path
23
+ from typing import TYPE_CHECKING, TypedDict
24
+
25
+ import typer
26
+
27
+ from agent_cli import config, opts
28
+ from agent_cli._tools import tools
29
+ from agent_cli.cli import app
30
+ from agent_cli.core import process
31
+ from agent_cli.core.audio import setup_devices
32
+ from agent_cli.core.deps import requires_extras
33
+ from agent_cli.core.utils import (
34
+ InteractiveStopEvent,
35
+ console,
36
+ format_timedelta_to_ago,
37
+ live_timer,
38
+ maybe_live,
39
+ print_command_line_args,
40
+ print_input_panel,
41
+ print_output_panel,
42
+ print_with_style,
43
+ setup_logging,
44
+ signal_handling_context,
45
+ stop_or_status_or_toggle,
46
+ )
47
+ from agent_cli.services import asr
48
+ from agent_cli.services.llm import get_llm_response
49
+ from agent_cli.services.tts import handle_tts_playback
50
+
51
+ if TYPE_CHECKING:
52
+ from rich.live import Live
53
+
54
+
55
+ LOGGER = logging.getLogger(__name__)
56
+
57
+ # --- Conversation History ---
58
+
59
+
60
+ class ConversationEntry(TypedDict):
61
+ """A single entry in the conversation."""
62
+
63
+ role: str
64
+ content: str
65
+ timestamp: str
66
+
67
+
68
+ # --- LLM Prompts ---
69
+
70
+ SYSTEM_PROMPT = """\
71
+ You are a helpful and friendly conversational AI with long-term memory. Your role is to assist the user with their questions and tasks.
72
+
73
+ You have access to the following tools:
74
+ - read_file: Read the content of a file.
75
+ - execute_code: Execute a shell command.
76
+ - add_memory: Add important information to long-term memory for future recall.
77
+ - search_memory: Search your long-term memory for relevant information.
78
+ - update_memory: Modify existing memories by ID when information changes.
79
+ - list_all_memories: Show all stored memories with their IDs and details.
80
+ - list_memory_categories: See what types of information you've remembered.
81
+ - duckduckgo_search: Search the web for current information.
82
+
83
+ Memory Guidelines:
84
+ - When the user shares personal information, preferences, or important facts, offer to add them to memory.
85
+ - Before answering questions, consider searching your memory for relevant context.
86
+ - Use categories like: personal, preferences, facts, tasks, projects, etc.
87
+ - Always ask for permission before adding sensitive or personal information to memory.
88
+
89
+ - The user is interacting with you through voice, so keep your responses concise and natural.
90
+ - A summary of the previous conversation is provided for context. This context may or may not be relevant to the current query.
91
+ - Do not repeat information from the previous conversation unless it is necessary to answer the current question.
92
+ - Do not ask "How can I help you?" at the end of your response.
93
+ """
94
+
95
+ AGENT_INSTRUCTIONS = """\
96
+ A summary of the previous conversation is provided in the <previous-conversation> tag.
97
+ The user's current message is in the <user-message> tag.
98
+
99
+ - If the user's message is a continuation of the previous conversation, use the context to inform your response.
100
+ - If the user's message is a new topic, ignore the previous conversation.
101
+
102
+ Your response should be helpful and directly address the user's message.
103
+ """
104
+
105
+ USER_MESSAGE_WITH_CONTEXT_TEMPLATE = """
106
+ <previous-conversation>
107
+ {formatted_history}
108
+ </previous-conversation>
109
+ <user-message>
110
+ {instruction}
111
+ </user-message>
112
+ """
113
+
114
+ # --- Helper Functions ---
115
+
116
+
117
+ def _load_conversation_history(history_file: Path, last_n_messages: int) -> list[ConversationEntry]:
118
+ if last_n_messages == 0:
119
+ return []
120
+ if history_file.exists():
121
+ with history_file.open("r") as f:
122
+ history = json.load(f)
123
+ if last_n_messages > 0:
124
+ return history[-last_n_messages:]
125
+ return history
126
+ return []
127
+
128
+
129
+ def _save_conversation_history(history_file: Path, history: list[ConversationEntry]) -> None:
130
+ with history_file.open("w") as f:
131
+ json.dump(history, f, indent=2)
132
+
133
+
134
+ def _format_conversation_for_llm(history: list[ConversationEntry]) -> str:
135
+ """Format the conversation history for the LLM."""
136
+ if not history:
137
+ return "No previous conversation."
138
+
139
+ now = datetime.now(UTC)
140
+ formatted_lines = []
141
+ for entry in history:
142
+ timestamp = datetime.fromisoformat(entry["timestamp"])
143
+ ago = format_timedelta_to_ago(now - timestamp)
144
+ formatted_lines.append(f"{entry['role']} ({ago}): {entry['content']}")
145
+ return "\n".join(formatted_lines)
146
+
147
+
148
+ async def _handle_conversation_turn(
149
+ *,
150
+ stop_event: InteractiveStopEvent,
151
+ conversation_history: list[ConversationEntry],
152
+ provider_cfg: config.ProviderSelection,
153
+ general_cfg: config.General,
154
+ history_cfg: config.History,
155
+ audio_in_cfg: config.AudioInput,
156
+ wyoming_asr_cfg: config.WyomingASR,
157
+ openai_asr_cfg: config.OpenAIASR,
158
+ gemini_asr_cfg: config.GeminiASR,
159
+ ollama_cfg: config.Ollama,
160
+ openai_llm_cfg: config.OpenAILLM,
161
+ gemini_llm_cfg: config.GeminiLLM,
162
+ audio_out_cfg: config.AudioOutput,
163
+ wyoming_tts_cfg: config.WyomingTTS,
164
+ openai_tts_cfg: config.OpenAITTS,
165
+ kokoro_tts_cfg: config.KokoroTTS,
166
+ gemini_tts_cfg: config.GeminiTTS,
167
+ live: Live,
168
+ ) -> None:
169
+ """Handles a single turn of the conversation."""
170
+ # 1. Transcribe user's command
171
+ start_time = time.monotonic()
172
+ transcriber = asr.create_transcriber(
173
+ provider_cfg,
174
+ audio_in_cfg,
175
+ wyoming_asr_cfg,
176
+ openai_asr_cfg,
177
+ gemini_asr_cfg,
178
+ )
179
+ instruction = await transcriber(
180
+ stop_event=stop_event,
181
+ quiet=general_cfg.quiet,
182
+ live=live,
183
+ logger=LOGGER,
184
+ )
185
+ elapsed = time.monotonic() - start_time
186
+
187
+ # Clear the stop event after ASR completes - it was only meant to stop recording
188
+ stop_event.clear()
189
+
190
+ if not instruction or not instruction.strip():
191
+ if not general_cfg.quiet:
192
+ print_with_style(
193
+ "No instruction, listening again.",
194
+ style="yellow",
195
+ )
196
+ return
197
+
198
+ if not general_cfg.quiet:
199
+ print_input_panel(instruction, title="👤 You", subtitle=f"took {elapsed:.2f}s")
200
+
201
+ # 2. Add user message to history
202
+ conversation_history.append(
203
+ {
204
+ "role": "user",
205
+ "content": instruction,
206
+ "timestamp": datetime.now(UTC).isoformat(),
207
+ },
208
+ )
209
+
210
+ # 3. Format conversation for LLM
211
+ formatted_history = _format_conversation_for_llm(conversation_history)
212
+ user_message_with_context = USER_MESSAGE_WITH_CONTEXT_TEMPLATE.format(
213
+ formatted_history=formatted_history,
214
+ instruction=instruction,
215
+ )
216
+
217
+ # 4. Get LLM response with timing
218
+
219
+ start_time = time.monotonic()
220
+
221
+ if provider_cfg.llm_provider == "ollama":
222
+ model_name = ollama_cfg.llm_ollama_model
223
+ elif provider_cfg.llm_provider == "openai":
224
+ model_name = openai_llm_cfg.llm_openai_model
225
+ elif provider_cfg.llm_provider == "gemini":
226
+ model_name = gemini_llm_cfg.llm_gemini_model
227
+ async with live_timer(
228
+ live,
229
+ f"🤖 Processing with {model_name}",
230
+ style="bold yellow",
231
+ quiet=general_cfg.quiet,
232
+ stop_event=stop_event,
233
+ ):
234
+ response_text = await get_llm_response(
235
+ system_prompt=SYSTEM_PROMPT,
236
+ agent_instructions=AGENT_INSTRUCTIONS,
237
+ user_input=user_message_with_context,
238
+ provider_cfg=provider_cfg,
239
+ ollama_cfg=ollama_cfg,
240
+ openai_cfg=openai_llm_cfg,
241
+ gemini_cfg=gemini_llm_cfg,
242
+ logger=LOGGER,
243
+ tools=tools(),
244
+ quiet=True, # Suppress internal output since we're showing our own timer
245
+ live=live,
246
+ )
247
+
248
+ elapsed = time.monotonic() - start_time
249
+
250
+ if not response_text:
251
+ if not general_cfg.quiet:
252
+ print_with_style("No response from LLM.", style="yellow")
253
+ return
254
+
255
+ if not general_cfg.quiet:
256
+ print_output_panel(
257
+ response_text,
258
+ title="🤖 AI",
259
+ subtitle=f"[dim]took {elapsed:.2f}s[/dim]",
260
+ )
261
+
262
+ # 5. Add AI response to history
263
+ conversation_history.append(
264
+ {
265
+ "role": "assistant",
266
+ "content": response_text,
267
+ "timestamp": datetime.now(UTC).isoformat(),
268
+ },
269
+ )
270
+
271
+ # 6. Save history
272
+ if history_cfg.history_dir:
273
+ history_path = Path(history_cfg.history_dir).expanduser()
274
+ history_path.mkdir(parents=True, exist_ok=True)
275
+ # Share the history directory with the memory tools
276
+ os.environ["AGENT_CLI_HISTORY_DIR"] = str(history_path)
277
+ history_file = history_path / "conversation.json"
278
+ _save_conversation_history(history_file, conversation_history)
279
+
280
+ # 7. Handle TTS playback
281
+ if audio_out_cfg.enable_tts:
282
+ await handle_tts_playback(
283
+ text=response_text,
284
+ provider_cfg=provider_cfg,
285
+ audio_output_cfg=audio_out_cfg,
286
+ wyoming_tts_cfg=wyoming_tts_cfg,
287
+ openai_tts_cfg=openai_tts_cfg,
288
+ kokoro_tts_cfg=kokoro_tts_cfg,
289
+ gemini_tts_cfg=gemini_tts_cfg,
290
+ save_file=general_cfg.save_file,
291
+ quiet=general_cfg.quiet,
292
+ logger=LOGGER,
293
+ play_audio=not general_cfg.save_file,
294
+ stop_event=stop_event,
295
+ live=live,
296
+ )
297
+
298
+ # Reset stop_event for next iteration
299
+ stop_event.clear()
300
+
301
+
302
+ # --- Main Application Logic ---
303
+
304
+
305
+ async def _async_main(
306
+ *,
307
+ provider_cfg: config.ProviderSelection,
308
+ general_cfg: config.General,
309
+ history_cfg: config.History,
310
+ audio_in_cfg: config.AudioInput,
311
+ wyoming_asr_cfg: config.WyomingASR,
312
+ openai_asr_cfg: config.OpenAIASR,
313
+ gemini_asr_cfg: config.GeminiASR,
314
+ ollama_cfg: config.Ollama,
315
+ openai_llm_cfg: config.OpenAILLM,
316
+ gemini_llm_cfg: config.GeminiLLM,
317
+ audio_out_cfg: config.AudioOutput,
318
+ wyoming_tts_cfg: config.WyomingTTS,
319
+ openai_tts_cfg: config.OpenAITTS,
320
+ kokoro_tts_cfg: config.KokoroTTS,
321
+ gemini_tts_cfg: config.GeminiTTS,
322
+ ) -> None:
323
+ """Main async function, consumes parsed arguments."""
324
+ try:
325
+ device_info = setup_devices(general_cfg, audio_in_cfg, audio_out_cfg)
326
+ if device_info is None:
327
+ return
328
+ input_device_index, _, tts_output_device_index = device_info
329
+ audio_in_cfg.input_device_index = input_device_index
330
+ if audio_out_cfg.enable_tts:
331
+ audio_out_cfg.output_device_index = tts_output_device_index
332
+
333
+ # Load conversation history
334
+ conversation_history = []
335
+ if history_cfg.history_dir:
336
+ history_path = Path(history_cfg.history_dir).expanduser()
337
+ history_path.mkdir(parents=True, exist_ok=True)
338
+ # Share the history directory with the memory tools
339
+ os.environ["AGENT_CLI_HISTORY_DIR"] = str(history_path)
340
+ history_file = history_path / "conversation.json"
341
+ conversation_history = _load_conversation_history(
342
+ history_file,
343
+ history_cfg.last_n_messages,
344
+ )
345
+
346
+ with (
347
+ maybe_live(not general_cfg.quiet) as live,
348
+ signal_handling_context(LOGGER, general_cfg.quiet) as stop_event,
349
+ ):
350
+ while not stop_event.is_set():
351
+ await _handle_conversation_turn(
352
+ stop_event=stop_event,
353
+ conversation_history=conversation_history,
354
+ provider_cfg=provider_cfg,
355
+ general_cfg=general_cfg,
356
+ history_cfg=history_cfg,
357
+ audio_in_cfg=audio_in_cfg,
358
+ wyoming_asr_cfg=wyoming_asr_cfg,
359
+ openai_asr_cfg=openai_asr_cfg,
360
+ gemini_asr_cfg=gemini_asr_cfg,
361
+ ollama_cfg=ollama_cfg,
362
+ openai_llm_cfg=openai_llm_cfg,
363
+ gemini_llm_cfg=gemini_llm_cfg,
364
+ audio_out_cfg=audio_out_cfg,
365
+ wyoming_tts_cfg=wyoming_tts_cfg,
366
+ openai_tts_cfg=openai_tts_cfg,
367
+ kokoro_tts_cfg=kokoro_tts_cfg,
368
+ gemini_tts_cfg=gemini_tts_cfg,
369
+ live=live,
370
+ )
371
+ except Exception:
372
+ if not general_cfg.quiet:
373
+ console.print_exception()
374
+ raise
375
+
376
+
377
+ @app.command("chat", rich_help_panel="Voice Commands")
378
+ @requires_extras("audio", "llm")
379
+ def chat(
380
+ *,
381
+ # --- Provider Selection ---
382
+ asr_provider: str = opts.ASR_PROVIDER,
383
+ llm_provider: str = opts.LLM_PROVIDER,
384
+ tts_provider: str = opts.TTS_PROVIDER,
385
+ # --- ASR (Audio) Configuration ---
386
+ input_device_index: int | None = opts.INPUT_DEVICE_INDEX,
387
+ input_device_name: str | None = opts.INPUT_DEVICE_NAME,
388
+ asr_wyoming_ip: str = opts.ASR_WYOMING_IP,
389
+ asr_wyoming_port: int = opts.ASR_WYOMING_PORT,
390
+ asr_openai_model: str = opts.ASR_OPENAI_MODEL,
391
+ asr_openai_base_url: str | None = opts.ASR_OPENAI_BASE_URL,
392
+ asr_openai_prompt: str | None = opts.ASR_OPENAI_PROMPT,
393
+ asr_gemini_model: str = opts.ASR_GEMINI_MODEL,
394
+ # --- LLM Configuration ---
395
+ llm_ollama_model: str = opts.LLM_OLLAMA_MODEL,
396
+ llm_ollama_host: str = opts.LLM_OLLAMA_HOST,
397
+ llm_openai_model: str = opts.LLM_OPENAI_MODEL,
398
+ openai_api_key: str | None = opts.OPENAI_API_KEY,
399
+ openai_base_url: str | None = opts.OPENAI_BASE_URL,
400
+ llm_gemini_model: str = opts.LLM_GEMINI_MODEL,
401
+ gemini_api_key: str | None = opts.GEMINI_API_KEY,
402
+ # --- TTS Configuration ---
403
+ enable_tts: bool = opts.ENABLE_TTS,
404
+ output_device_index: int | None = opts.OUTPUT_DEVICE_INDEX,
405
+ output_device_name: str | None = opts.OUTPUT_DEVICE_NAME,
406
+ tts_speed: float = opts.TTS_SPEED,
407
+ tts_wyoming_ip: str = opts.TTS_WYOMING_IP,
408
+ tts_wyoming_port: int = opts.TTS_WYOMING_PORT,
409
+ tts_wyoming_voice: str | None = opts.TTS_WYOMING_VOICE,
410
+ tts_wyoming_language: str | None = opts.TTS_WYOMING_LANGUAGE,
411
+ tts_wyoming_speaker: str | None = opts.TTS_WYOMING_SPEAKER,
412
+ tts_openai_model: str = opts.TTS_OPENAI_MODEL,
413
+ tts_openai_voice: str = opts.TTS_OPENAI_VOICE,
414
+ tts_openai_base_url: str | None = opts.TTS_OPENAI_BASE_URL,
415
+ tts_kokoro_model: str = opts.TTS_KOKORO_MODEL,
416
+ tts_kokoro_voice: str = opts.TTS_KOKORO_VOICE,
417
+ tts_kokoro_host: str = opts.TTS_KOKORO_HOST,
418
+ tts_gemini_model: str = opts.TTS_GEMINI_MODEL,
419
+ tts_gemini_voice: str = opts.TTS_GEMINI_VOICE,
420
+ # --- Process Management ---
421
+ stop: bool = opts.STOP,
422
+ status: bool = opts.STATUS,
423
+ toggle: bool = opts.TOGGLE,
424
+ # --- History Options ---
425
+ history_dir: Path = typer.Option( # noqa: B008
426
+ "~/.config/agent-cli/history",
427
+ "--history-dir",
428
+ help="Directory to store conversation history.",
429
+ rich_help_panel="History Options",
430
+ ),
431
+ last_n_messages: int = typer.Option(
432
+ 50,
433
+ "--last-n-messages",
434
+ help="Number of messages to include in the conversation history."
435
+ " Set to 0 to disable history.",
436
+ rich_help_panel="History Options",
437
+ ),
438
+ # --- General Options ---
439
+ save_file: Path | None = opts.SAVE_FILE,
440
+ log_level: opts.LogLevel = opts.LOG_LEVEL,
441
+ log_file: str | None = opts.LOG_FILE,
442
+ list_devices: bool = opts.LIST_DEVICES,
443
+ quiet: bool = opts.QUIET,
444
+ config_file: str | None = opts.CONFIG_FILE,
445
+ print_args: bool = opts.PRINT_ARGS,
446
+ ) -> None:
447
+ """An chat agent that you can talk to."""
448
+ if print_args:
449
+ print_command_line_args(locals())
450
+
451
+ setup_logging(log_level, log_file, quiet=quiet)
452
+ general_cfg = config.General(
453
+ log_level=log_level,
454
+ log_file=log_file,
455
+ quiet=quiet,
456
+ list_devices=list_devices,
457
+ clipboard=False, # Not used in chat mode
458
+ save_file=save_file,
459
+ )
460
+ process_name = "chat"
461
+ if stop_or_status_or_toggle(
462
+ process_name,
463
+ "chat agent",
464
+ stop,
465
+ status,
466
+ toggle,
467
+ quiet=general_cfg.quiet,
468
+ ):
469
+ return
470
+
471
+ with process.pid_file_context(process_name), suppress(KeyboardInterrupt):
472
+ cfgs = config.create_provider_configs_from_locals(locals())
473
+ history_cfg = config.History(
474
+ history_dir=history_dir,
475
+ last_n_messages=last_n_messages,
476
+ )
477
+
478
+ asyncio.run(
479
+ _async_main(
480
+ provider_cfg=cfgs.provider,
481
+ general_cfg=general_cfg,
482
+ history_cfg=history_cfg,
483
+ audio_in_cfg=cfgs.audio_in,
484
+ wyoming_asr_cfg=cfgs.wyoming_asr,
485
+ openai_asr_cfg=cfgs.openai_asr,
486
+ gemini_asr_cfg=cfgs.gemini_asr,
487
+ ollama_cfg=cfgs.ollama,
488
+ openai_llm_cfg=cfgs.openai_llm,
489
+ gemini_llm_cfg=cfgs.gemini_llm,
490
+ audio_out_cfg=cfgs.audio_out,
491
+ wyoming_tts_cfg=cfgs.wyoming_tts,
492
+ openai_tts_cfg=cfgs.openai_tts,
493
+ kokoro_tts_cfg=cfgs.kokoro_tts,
494
+ gemini_tts_cfg=cfgs.gemini_tts,
495
+ ),
496
+ )
@@ -0,0 +1,31 @@
1
+ """Memory system CLI commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typer
6
+
7
+ from agent_cli.cli import app
8
+ from agent_cli.core.process import set_process_title
9
+
10
+ memory_app = typer.Typer(
11
+ name="memory",
12
+ help="Memory system operations (add, proxy, etc.).",
13
+ add_completion=True,
14
+ rich_markup_mode="markdown",
15
+ no_args_is_help=True,
16
+ )
17
+
18
+ app.add_typer(memory_app, name="memory", rich_help_panel="Servers")
19
+
20
+
21
+ @memory_app.callback()
22
+ def memory_callback(ctx: typer.Context) -> None:
23
+ """Memory command group callback."""
24
+ if ctx.invoked_subcommand is not None:
25
+ set_process_title(f"memory-{ctx.invoked_subcommand}")
26
+
27
+
28
+ # Import subcommands to register them with memory_app
29
+ from agent_cli.agents.memory import add, proxy # noqa: E402
30
+
31
+ __all__ = ["add", "memory_app", "proxy"]