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.
- agent_cli/__init__.py +5 -0
- agent_cli/__main__.py +6 -0
- agent_cli/_extras.json +14 -0
- agent_cli/_requirements/.gitkeep +0 -0
- agent_cli/_requirements/audio.txt +79 -0
- agent_cli/_requirements/faster-whisper.txt +215 -0
- agent_cli/_requirements/kokoro.txt +425 -0
- agent_cli/_requirements/llm.txt +183 -0
- agent_cli/_requirements/memory.txt +355 -0
- agent_cli/_requirements/mlx-whisper.txt +222 -0
- agent_cli/_requirements/piper.txt +176 -0
- agent_cli/_requirements/rag.txt +402 -0
- agent_cli/_requirements/server.txt +154 -0
- agent_cli/_requirements/speed.txt +77 -0
- agent_cli/_requirements/vad.txt +155 -0
- agent_cli/_requirements/wyoming.txt +71 -0
- agent_cli/_tools.py +368 -0
- agent_cli/agents/__init__.py +23 -0
- agent_cli/agents/_voice_agent_common.py +136 -0
- agent_cli/agents/assistant.py +383 -0
- agent_cli/agents/autocorrect.py +284 -0
- agent_cli/agents/chat.py +496 -0
- agent_cli/agents/memory/__init__.py +31 -0
- agent_cli/agents/memory/add.py +190 -0
- agent_cli/agents/memory/proxy.py +160 -0
- agent_cli/agents/rag_proxy.py +128 -0
- agent_cli/agents/speak.py +209 -0
- agent_cli/agents/transcribe.py +671 -0
- agent_cli/agents/transcribe_daemon.py +499 -0
- agent_cli/agents/voice_edit.py +291 -0
- agent_cli/api.py +22 -0
- agent_cli/cli.py +106 -0
- agent_cli/config.py +503 -0
- agent_cli/config_cmd.py +307 -0
- agent_cli/constants.py +27 -0
- agent_cli/core/__init__.py +1 -0
- agent_cli/core/audio.py +461 -0
- agent_cli/core/audio_format.py +299 -0
- agent_cli/core/chroma.py +88 -0
- agent_cli/core/deps.py +191 -0
- agent_cli/core/openai_proxy.py +139 -0
- agent_cli/core/process.py +195 -0
- agent_cli/core/reranker.py +120 -0
- agent_cli/core/sse.py +87 -0
- agent_cli/core/transcription_logger.py +70 -0
- agent_cli/core/utils.py +526 -0
- agent_cli/core/vad.py +175 -0
- agent_cli/core/watch.py +65 -0
- agent_cli/dev/__init__.py +14 -0
- agent_cli/dev/cli.py +1588 -0
- agent_cli/dev/coding_agents/__init__.py +19 -0
- agent_cli/dev/coding_agents/aider.py +24 -0
- agent_cli/dev/coding_agents/base.py +167 -0
- agent_cli/dev/coding_agents/claude.py +39 -0
- agent_cli/dev/coding_agents/codex.py +24 -0
- agent_cli/dev/coding_agents/continue_dev.py +15 -0
- agent_cli/dev/coding_agents/copilot.py +24 -0
- agent_cli/dev/coding_agents/cursor_agent.py +48 -0
- agent_cli/dev/coding_agents/gemini.py +28 -0
- agent_cli/dev/coding_agents/opencode.py +15 -0
- agent_cli/dev/coding_agents/registry.py +49 -0
- agent_cli/dev/editors/__init__.py +19 -0
- agent_cli/dev/editors/base.py +89 -0
- agent_cli/dev/editors/cursor.py +15 -0
- agent_cli/dev/editors/emacs.py +46 -0
- agent_cli/dev/editors/jetbrains.py +56 -0
- agent_cli/dev/editors/nano.py +31 -0
- agent_cli/dev/editors/neovim.py +33 -0
- agent_cli/dev/editors/registry.py +59 -0
- agent_cli/dev/editors/sublime.py +20 -0
- agent_cli/dev/editors/vim.py +42 -0
- agent_cli/dev/editors/vscode.py +15 -0
- agent_cli/dev/editors/zed.py +20 -0
- agent_cli/dev/project.py +568 -0
- agent_cli/dev/registry.py +52 -0
- agent_cli/dev/skill/SKILL.md +141 -0
- agent_cli/dev/skill/examples.md +571 -0
- agent_cli/dev/terminals/__init__.py +19 -0
- agent_cli/dev/terminals/apple_terminal.py +82 -0
- agent_cli/dev/terminals/base.py +56 -0
- agent_cli/dev/terminals/gnome.py +51 -0
- agent_cli/dev/terminals/iterm2.py +84 -0
- agent_cli/dev/terminals/kitty.py +77 -0
- agent_cli/dev/terminals/registry.py +48 -0
- agent_cli/dev/terminals/tmux.py +58 -0
- agent_cli/dev/terminals/warp.py +132 -0
- agent_cli/dev/terminals/zellij.py +78 -0
- agent_cli/dev/worktree.py +856 -0
- agent_cli/docs_gen.py +417 -0
- agent_cli/example-config.toml +185 -0
- agent_cli/install/__init__.py +5 -0
- agent_cli/install/common.py +89 -0
- agent_cli/install/extras.py +174 -0
- agent_cli/install/hotkeys.py +48 -0
- agent_cli/install/services.py +87 -0
- agent_cli/memory/__init__.py +7 -0
- agent_cli/memory/_files.py +250 -0
- agent_cli/memory/_filters.py +63 -0
- agent_cli/memory/_git.py +157 -0
- agent_cli/memory/_indexer.py +142 -0
- agent_cli/memory/_ingest.py +408 -0
- agent_cli/memory/_persistence.py +182 -0
- agent_cli/memory/_prompt.py +91 -0
- agent_cli/memory/_retrieval.py +294 -0
- agent_cli/memory/_store.py +169 -0
- agent_cli/memory/_streaming.py +44 -0
- agent_cli/memory/_tasks.py +48 -0
- agent_cli/memory/api.py +113 -0
- agent_cli/memory/client.py +272 -0
- agent_cli/memory/engine.py +361 -0
- agent_cli/memory/entities.py +43 -0
- agent_cli/memory/models.py +112 -0
- agent_cli/opts.py +433 -0
- agent_cli/py.typed +0 -0
- agent_cli/rag/__init__.py +3 -0
- agent_cli/rag/_indexer.py +67 -0
- agent_cli/rag/_indexing.py +226 -0
- agent_cli/rag/_prompt.py +30 -0
- agent_cli/rag/_retriever.py +156 -0
- agent_cli/rag/_store.py +48 -0
- agent_cli/rag/_utils.py +218 -0
- agent_cli/rag/api.py +175 -0
- agent_cli/rag/client.py +299 -0
- agent_cli/rag/engine.py +302 -0
- agent_cli/rag/models.py +55 -0
- agent_cli/scripts/.runtime/.gitkeep +0 -0
- agent_cli/scripts/__init__.py +1 -0
- agent_cli/scripts/check_plugin_skill_sync.py +50 -0
- agent_cli/scripts/linux-hotkeys/README.md +63 -0
- agent_cli/scripts/linux-hotkeys/toggle-autocorrect.sh +45 -0
- agent_cli/scripts/linux-hotkeys/toggle-transcription.sh +58 -0
- agent_cli/scripts/linux-hotkeys/toggle-voice-edit.sh +58 -0
- agent_cli/scripts/macos-hotkeys/README.md +45 -0
- agent_cli/scripts/macos-hotkeys/skhd-config-example +5 -0
- agent_cli/scripts/macos-hotkeys/toggle-autocorrect.sh +12 -0
- agent_cli/scripts/macos-hotkeys/toggle-transcription.sh +37 -0
- agent_cli/scripts/macos-hotkeys/toggle-voice-edit.sh +37 -0
- agent_cli/scripts/nvidia-asr-server/README.md +99 -0
- agent_cli/scripts/nvidia-asr-server/pyproject.toml +27 -0
- agent_cli/scripts/nvidia-asr-server/server.py +255 -0
- agent_cli/scripts/nvidia-asr-server/shell.nix +32 -0
- agent_cli/scripts/nvidia-asr-server/uv.lock +4654 -0
- agent_cli/scripts/run-openwakeword.sh +11 -0
- agent_cli/scripts/run-piper-windows.ps1 +30 -0
- agent_cli/scripts/run-piper.sh +24 -0
- agent_cli/scripts/run-whisper-linux.sh +40 -0
- agent_cli/scripts/run-whisper-macos.sh +6 -0
- agent_cli/scripts/run-whisper-windows.ps1 +51 -0
- agent_cli/scripts/run-whisper.sh +9 -0
- agent_cli/scripts/run_faster_whisper_server.py +136 -0
- agent_cli/scripts/setup-linux-hotkeys.sh +72 -0
- agent_cli/scripts/setup-linux.sh +108 -0
- agent_cli/scripts/setup-macos-hotkeys.sh +61 -0
- agent_cli/scripts/setup-macos.sh +76 -0
- agent_cli/scripts/setup-windows.ps1 +63 -0
- agent_cli/scripts/start-all-services-windows.ps1 +53 -0
- agent_cli/scripts/start-all-services.sh +178 -0
- agent_cli/scripts/sync_extras.py +138 -0
- agent_cli/server/__init__.py +3 -0
- agent_cli/server/cli.py +721 -0
- agent_cli/server/common.py +222 -0
- agent_cli/server/model_manager.py +288 -0
- agent_cli/server/model_registry.py +225 -0
- agent_cli/server/proxy/__init__.py +3 -0
- agent_cli/server/proxy/api.py +444 -0
- agent_cli/server/streaming.py +67 -0
- agent_cli/server/tts/__init__.py +3 -0
- agent_cli/server/tts/api.py +335 -0
- agent_cli/server/tts/backends/__init__.py +82 -0
- agent_cli/server/tts/backends/base.py +139 -0
- agent_cli/server/tts/backends/kokoro.py +403 -0
- agent_cli/server/tts/backends/piper.py +253 -0
- agent_cli/server/tts/model_manager.py +201 -0
- agent_cli/server/tts/model_registry.py +28 -0
- agent_cli/server/tts/wyoming_handler.py +249 -0
- agent_cli/server/whisper/__init__.py +3 -0
- agent_cli/server/whisper/api.py +413 -0
- agent_cli/server/whisper/backends/__init__.py +89 -0
- agent_cli/server/whisper/backends/base.py +97 -0
- agent_cli/server/whisper/backends/faster_whisper.py +225 -0
- agent_cli/server/whisper/backends/mlx.py +270 -0
- agent_cli/server/whisper/languages.py +116 -0
- agent_cli/server/whisper/model_manager.py +157 -0
- agent_cli/server/whisper/model_registry.py +28 -0
- agent_cli/server/whisper/wyoming_handler.py +203 -0
- agent_cli/services/__init__.py +343 -0
- agent_cli/services/_wyoming_utils.py +64 -0
- agent_cli/services/asr.py +506 -0
- agent_cli/services/llm.py +228 -0
- agent_cli/services/tts.py +450 -0
- agent_cli/services/wake_word.py +142 -0
- agent_cli-0.70.5.dist-info/METADATA +2118 -0
- agent_cli-0.70.5.dist-info/RECORD +196 -0
- agent_cli-0.70.5.dist-info/WHEEL +4 -0
- agent_cli-0.70.5.dist-info/entry_points.txt +4 -0
- agent_cli-0.70.5.dist-info/licenses/LICENSE +21 -0
agent_cli/agents/chat.py
ADDED
|
@@ -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"]
|