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,190 @@
1
+ """Add memories directly to the memory store without LLM extraction."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import re
7
+ import sys
8
+ from datetime import UTC, datetime
9
+ from pathlib import Path # noqa: TC003
10
+ from typing import TYPE_CHECKING, Any
11
+
12
+ import typer
13
+
14
+ from agent_cli import opts
15
+ from agent_cli.agents.memory import memory_app
16
+ from agent_cli.core.deps import requires_extras
17
+ from agent_cli.core.utils import console, print_command_line_args
18
+
19
+ if TYPE_CHECKING:
20
+ from agent_cli.memory._files import MemoryFileRecord
21
+
22
+ # Matches markdown list prefixes: "- ", "* ", "+ ", "1. ", "2. ", etc.
23
+ _LIST_PREFIX_RE = re.compile(r"^(?:[-*+]|\d+\.)\s+")
24
+
25
+
26
+ def _strip_list_prefix(line: str) -> str:
27
+ """Strip markdown/text list prefixes from a line."""
28
+ return _LIST_PREFIX_RE.sub("", line)
29
+
30
+
31
+ def _parse_json_items(
32
+ items: list[str | dict[str, Any]],
33
+ default_conversation_id: str,
34
+ ) -> list[tuple[str, str]]:
35
+ """Parse a JSON list of items into (content, conversation_id) tuples."""
36
+ results: list[tuple[str, str]] = []
37
+ for item in items:
38
+ if isinstance(item, str):
39
+ results.append((item, default_conversation_id))
40
+ else:
41
+ results.append((item["content"], item.get("conversation_id", default_conversation_id)))
42
+ return results
43
+
44
+
45
+ def _parse_memories(
46
+ memories: list[str],
47
+ file: Path | None,
48
+ default_conversation_id: str,
49
+ ) -> list[tuple[str, str]]:
50
+ """Parse memories from arguments, file, or stdin."""
51
+ results: list[tuple[str, str]] = []
52
+
53
+ if file:
54
+ text = sys.stdin.read() if str(file) == "-" else file.read_text()
55
+ text = text.strip()
56
+
57
+ parsed_json = False
58
+ if text.startswith(("[", "{")):
59
+ try:
60
+ data = json.loads(text)
61
+ if isinstance(data, list):
62
+ results.extend(_parse_json_items(data, default_conversation_id))
63
+ parsed_json = True
64
+ elif isinstance(data, dict) and "memories" in data:
65
+ results.extend(_parse_json_items(data["memories"], default_conversation_id))
66
+ parsed_json = True
67
+ except json.JSONDecodeError:
68
+ pass # Fall through to plain text parsing
69
+
70
+ if not parsed_json:
71
+ for line in text.splitlines():
72
+ stripped = line.strip()
73
+ if stripped:
74
+ content = _strip_list_prefix(stripped)
75
+ if content:
76
+ results.append((content, default_conversation_id))
77
+
78
+ results.extend((m, default_conversation_id) for m in memories)
79
+ return results
80
+
81
+
82
+ def _write_memories(
83
+ memory_path: Path,
84
+ memories: list[tuple[str, str]],
85
+ git_versioning: bool,
86
+ ) -> list[MemoryFileRecord]:
87
+ """Write memories to disk and optionally commit to git."""
88
+ import asyncio # noqa: PLC0415
89
+
90
+ from agent_cli.memory._files import write_memory_file # noqa: PLC0415
91
+ from agent_cli.memory._git import commit_changes, init_repo # noqa: PLC0415
92
+
93
+ if git_versioning:
94
+ init_repo(memory_path)
95
+
96
+ records = []
97
+ for content, conversation_id in memories:
98
+ record = write_memory_file(
99
+ memory_path,
100
+ conversation_id=conversation_id,
101
+ role="memory",
102
+ created_at=datetime.now(tz=UTC).isoformat(),
103
+ content=content,
104
+ )
105
+ records.append(record)
106
+
107
+ if git_versioning and records:
108
+ asyncio.run(commit_changes(memory_path, f"Add {len(records)} memories directly"))
109
+
110
+ return records
111
+
112
+
113
+ @memory_app.command("add")
114
+ @requires_extras("memory")
115
+ def add(
116
+ memories: list[str] = typer.Argument( # noqa: B008
117
+ None,
118
+ help="Memories to add. Each argument becomes one fact.",
119
+ ),
120
+ file: Path | None = typer.Option( # noqa: B008
121
+ None,
122
+ "--file",
123
+ "-f",
124
+ help="Read memories from file. Use '-' for stdin. Supports JSON array, JSON object with 'memories' key, or plain text (one per line).",
125
+ ),
126
+ conversation_id: str = typer.Option(
127
+ "default",
128
+ "--conversation-id",
129
+ "-c",
130
+ help="Conversation ID to add memories to.",
131
+ ),
132
+ memory_path: Path = typer.Option( # noqa: B008
133
+ "./memory_db",
134
+ "--memory-path",
135
+ help="Path to the memory store.",
136
+ ),
137
+ git_versioning: bool = typer.Option(
138
+ True, # noqa: FBT003
139
+ "--git-versioning/--no-git-versioning",
140
+ help="Commit changes to git.",
141
+ ),
142
+ quiet: bool = opts.QUIET,
143
+ config_file: str | None = opts.CONFIG_FILE,
144
+ print_args: bool = opts.PRINT_ARGS,
145
+ ) -> None:
146
+ """Add memories directly without LLM extraction.
147
+
148
+ This writes facts directly to the memory store, bypassing the LLM-based
149
+ fact extraction. Useful for bulk imports or seeding memories.
150
+
151
+ The memory proxy file watcher (if running) will auto-index new files.
152
+ Otherwise, they'll be indexed on next memory proxy startup.
153
+
154
+ Examples::
155
+
156
+ # Add single memories as arguments
157
+ agent-cli memory add "User likes coffee" "User lives in Amsterdam"
158
+
159
+ # Read from JSON file
160
+ agent-cli memory add -f memories.json
161
+
162
+ # Read from stdin (plain text, one per line)
163
+ echo "User prefers dark mode" | agent-cli memory add -f -
164
+
165
+ # Read JSON from stdin
166
+ echo '["Fact one", "Fact two"]' | agent-cli memory add -f -
167
+
168
+ # Specify conversation ID
169
+ agent-cli memory add -c work "Project deadline is Friday"
170
+
171
+ """
172
+ if print_args:
173
+ print_command_line_args(locals())
174
+
175
+ parsed = _parse_memories(memories or [], file, conversation_id)
176
+
177
+ if not parsed:
178
+ console.print("[red]No memories provided. Use arguments or --file.[/red]")
179
+ raise typer.Exit(1)
180
+
181
+ memory_path = memory_path.resolve()
182
+ records = _write_memories(memory_path, parsed, git_versioning)
183
+
184
+ if not quiet:
185
+ console.print(f"[green]Added {len(records)} memories to {memory_path}[/green]")
186
+ max_preview = 60
187
+ for record in records:
188
+ preview = record.content[:max_preview]
189
+ ellipsis = "..." if len(record.content) > max_preview else ""
190
+ console.print(f" - [dim]{preview}{ellipsis}[/dim]")
@@ -0,0 +1,160 @@
1
+ """Memory Proxy agent command (long-term memory with Chroma)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from pathlib import Path # noqa: TC003
7
+
8
+ import typer
9
+ from rich.logging import RichHandler
10
+
11
+ from agent_cli import constants, opts
12
+ from agent_cli.agents.memory import memory_app
13
+ from agent_cli.core.deps import requires_extras
14
+ from agent_cli.core.utils import console, print_command_line_args
15
+
16
+
17
+ @memory_app.command("proxy")
18
+ @requires_extras("memory")
19
+ def proxy(
20
+ memory_path: Path = typer.Option( # noqa: B008
21
+ "./memory_db",
22
+ help="Path to the memory store (files + derived vector index).",
23
+ rich_help_panel="Memory Configuration",
24
+ ),
25
+ openai_base_url: str | None = opts.OPENAI_BASE_URL,
26
+ embedding_model: str = opts.EMBEDDING_MODEL,
27
+ openai_api_key: str | None = opts.OPENAI_API_KEY,
28
+ default_top_k: int = typer.Option(
29
+ 5,
30
+ help="Number of memory entries to retrieve per query.",
31
+ rich_help_panel="Memory Configuration",
32
+ ),
33
+ host: str = opts.SERVER_HOST,
34
+ port: int = typer.Option(
35
+ 8100,
36
+ help="Port to bind to",
37
+ rich_help_panel="Server Configuration",
38
+ ),
39
+ max_entries: int = typer.Option(
40
+ 500,
41
+ help="Maximum stored memory entries per conversation (excluding summary).",
42
+ rich_help_panel="Memory Configuration",
43
+ ),
44
+ mmr_lambda: float = typer.Option(
45
+ 0.7,
46
+ help="MMR lambda (0-1): higher favors relevance, lower favors diversity.",
47
+ rich_help_panel="Memory Configuration",
48
+ ),
49
+ recency_weight: float = typer.Option(
50
+ 0.2,
51
+ help="Recency score weight (0.0-1.0). Controls freshness vs. relevance. Default 0.2 (20% recency, 80% semantic relevance).",
52
+ rich_help_panel="Memory Configuration",
53
+ ),
54
+ score_threshold: float = typer.Option(
55
+ 0.35,
56
+ help="Minimum semantic relevance threshold (0.0-1.0). Memories below this score are discarded to reduce noise.",
57
+ rich_help_panel="Memory Configuration",
58
+ ),
59
+ summarization: bool = typer.Option(
60
+ True, # noqa: FBT003
61
+ "--summarization/--no-summarization",
62
+ help="Enable automatic fact extraction and summaries.",
63
+ rich_help_panel="Memory Configuration",
64
+ ),
65
+ git_versioning: bool = typer.Option(
66
+ True, # noqa: FBT003
67
+ "--git-versioning/--no-git-versioning",
68
+ help="Enable automatic git commit of memory changes.",
69
+ rich_help_panel="Memory Configuration",
70
+ ),
71
+ log_level: opts.LogLevel = opts.LOG_LEVEL,
72
+ config_file: str | None = opts.CONFIG_FILE,
73
+ print_args: bool = opts.PRINT_ARGS,
74
+ ) -> None:
75
+ """Start the memory-backed chat proxy server.
76
+
77
+ This server acts as a middleware between your chat client (e.g., a web UI,
78
+ CLI, or IDE plugin) and an OpenAI-compatible LLM provider (e.g., OpenAI,
79
+ Ollama, vLLM).
80
+
81
+ Key Features:
82
+
83
+ - **Simple Markdown Files:** Memories are stored as human-readable Markdown
84
+ files, serving as the ultimate source of truth.
85
+ - **Automatic Version Control:** Built-in Git integration automatically
86
+ commits changes, providing a full history of memory evolution.
87
+ - **Lightweight & Local:** Minimal dependencies and runs entirely on your
88
+ machine.
89
+ - **Proxy Middleware:** Works transparently with any OpenAI-compatible
90
+ `/chat/completions` endpoint.
91
+
92
+ How it works:
93
+
94
+ 1. Intercepts `POST /v1/chat/completions` requests.
95
+ 2. **Retrieves** relevant memories (facts, previous conversations) from a
96
+ local vector database (ChromaDB) based on the user's query.
97
+ 3. **Injects** these memories into the system prompt.
98
+ 4. **Forwards** the augmented request to the real LLM (`--openai-base-url`).
99
+ 5. **Extracts** new facts from the conversation in the background and
100
+ updates the long-term memory store (including handling contradictions).
101
+
102
+ Use this to give "long-term memory" to any OpenAI-compatible application.
103
+ Point your client's base URL to `http://localhost:8100/v1`.
104
+ """
105
+ if print_args:
106
+ print_command_line_args(locals())
107
+
108
+ import uvicorn # noqa: PLC0415
109
+
110
+ from agent_cli.memory._files import ensure_store_dirs # noqa: PLC0415
111
+ from agent_cli.memory.api import create_app # noqa: PLC0415
112
+
113
+ logging.basicConfig(
114
+ level=log_level.upper(),
115
+ format="%(message)s",
116
+ datefmt="[%X]",
117
+ handlers=[RichHandler(console=console, rich_tracebacks=True)],
118
+ force=True,
119
+ )
120
+
121
+ logging.getLogger("httpx").setLevel(logging.WARNING)
122
+ logging.getLogger("chromadb").setLevel(logging.WARNING)
123
+ logging.getLogger("uvicorn.access").setLevel(logging.WARNING)
124
+
125
+ memory_path = memory_path.resolve()
126
+ entries_dir, _ = ensure_store_dirs(memory_path)
127
+ if openai_base_url is None:
128
+ openai_base_url = constants.DEFAULT_OPENAI_BASE_URL
129
+
130
+ console.print(f"[bold green]Starting Memory Proxy on {host}:{port}[/bold green]")
131
+ console.print(f" 💾 Memory store: [blue]{memory_path}[/blue]")
132
+ console.print(f" 📁 Entries: [blue]{entries_dir}[/blue]")
133
+ console.print(f" 🤖 Backend: [blue]{openai_base_url}[/blue]")
134
+ console.print(f" 🧠 Embeddings: Using [blue]{embedding_model}[/blue]")
135
+ console.print(f" 🔍 Memory top_k: [blue]{default_top_k}[/blue] entries per query")
136
+ console.print(f" 🧹 Max entries per conversation: [blue]{max_entries}[/blue]")
137
+ console.print(
138
+ f" ⚖️ Scoring: MMR λ=[blue]{mmr_lambda}[/blue], Recency w=[blue]{recency_weight}[/blue], Threshold=[blue]{score_threshold}[/blue]",
139
+ )
140
+ if not summarization:
141
+ console.print(" ⚙️ Summaries: [red]disabled[/red]")
142
+ if git_versioning:
143
+ console.print(" 📝 Git Versioning: [green]enabled[/green]")
144
+
145
+ fastapi_app = create_app(
146
+ memory_path,
147
+ openai_base_url,
148
+ embedding_model=embedding_model,
149
+ embedding_api_key=openai_api_key,
150
+ chat_api_key=openai_api_key,
151
+ default_top_k=default_top_k,
152
+ enable_summarization=summarization,
153
+ max_entries=max_entries,
154
+ mmr_lambda=mmr_lambda,
155
+ recency_weight=recency_weight,
156
+ score_threshold=score_threshold,
157
+ enable_git_versioning=git_versioning,
158
+ )
159
+
160
+ uvicorn.run(fastapi_app, host=host, port=port, log_config=None)
@@ -0,0 +1,128 @@
1
+ """RAG Proxy agent command."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from pathlib import Path # noqa: TC003
7
+
8
+ import typer
9
+ from rich.logging import RichHandler
10
+
11
+ from agent_cli import constants, opts
12
+ from agent_cli.cli import app
13
+ from agent_cli.core.deps import requires_extras
14
+ from agent_cli.core.utils import (
15
+ console,
16
+ print_command_line_args,
17
+ print_error_message,
18
+ )
19
+
20
+
21
+ @app.command("rag-proxy", rich_help_panel="Servers")
22
+ @requires_extras("rag")
23
+ def rag_proxy(
24
+ docs_folder: Path = typer.Option( # noqa: B008
25
+ "./rag_docs",
26
+ help="Folder to watch for documents",
27
+ rich_help_panel="RAG Configuration",
28
+ ),
29
+ chroma_path: Path = typer.Option( # noqa: B008
30
+ "./rag_db",
31
+ help="Path to ChromaDB persistence directory",
32
+ rich_help_panel="RAG Configuration",
33
+ ),
34
+ openai_base_url: str | None = opts.OPENAI_BASE_URL,
35
+ embedding_model: str = opts.EMBEDDING_MODEL,
36
+ openai_api_key: str | None = opts.OPENAI_API_KEY,
37
+ limit: int = typer.Option(
38
+ 3,
39
+ help="Number of document chunks to retrieve per query.",
40
+ rich_help_panel="RAG Configuration",
41
+ ),
42
+ host: str = opts.SERVER_HOST,
43
+ port: int = typer.Option(
44
+ 8000,
45
+ help="Port to bind to",
46
+ rich_help_panel="Server Configuration",
47
+ ),
48
+ log_level: opts.LogLevel = opts.LOG_LEVEL,
49
+ config_file: str | None = opts.CONFIG_FILE,
50
+ print_args: bool = opts.PRINT_ARGS,
51
+ enable_rag_tools: bool = typer.Option(
52
+ True, # noqa: FBT003
53
+ "--rag-tools/--no-rag-tools",
54
+ help="Allow agent to fetch full documents when snippets are insufficient.",
55
+ rich_help_panel="RAG Configuration",
56
+ ),
57
+ ) -> None:
58
+ """Start the RAG (Retrieval-Augmented Generation) Proxy Server.
59
+
60
+ This server watches a folder for documents, indexes them, and provides
61
+ an OpenAI-compatible API that proxies requests to a backend LLM (like llama.cpp),
62
+ injecting relevant context from the documents.
63
+ """
64
+ if print_args:
65
+ print_command_line_args(locals())
66
+ # Configure logging
67
+ logging.basicConfig(
68
+ level=log_level.upper(),
69
+ format="%(message)s",
70
+ datefmt="[%X]",
71
+ handlers=[RichHandler(console=console, rich_tracebacks=True)],
72
+ force=True,
73
+ )
74
+
75
+ # Suppress noisy logs from libraries
76
+ logging.getLogger("httpx").setLevel(logging.WARNING)
77
+ logging.getLogger("chromadb").setLevel(logging.WARNING)
78
+ logging.getLogger("uvicorn.access").setLevel(logging.WARNING)
79
+
80
+ import uvicorn # noqa: PLC0415
81
+
82
+ from agent_cli.rag.api import create_app # noqa: PLC0415
83
+
84
+ docs_folder = docs_folder.resolve()
85
+ chroma_path = chroma_path.resolve()
86
+
87
+ # Validate paths don't overlap - mixing docs and DB causes corruption
88
+ if docs_folder == chroma_path:
89
+ print_error_message(
90
+ "docs-folder and chroma-path cannot be the same directory.\n"
91
+ "ChromaDB creates internal files that would be indexed as documents.",
92
+ )
93
+ raise typer.Exit(1)
94
+ if chroma_path in docs_folder.parents:
95
+ print_error_message(
96
+ f"docs-folder ({docs_folder}) is inside chroma-path ({chroma_path}).\n"
97
+ "ChromaDB creates internal files that would be indexed as documents.",
98
+ )
99
+ raise typer.Exit(1)
100
+ if docs_folder in chroma_path.parents:
101
+ print_error_message(
102
+ f"chroma-path ({chroma_path}) is inside docs-folder ({docs_folder}).\n"
103
+ "ChromaDB files may be accidentally deleted when managing documents.",
104
+ )
105
+ raise typer.Exit(1)
106
+
107
+ if openai_base_url is None:
108
+ openai_base_url = constants.DEFAULT_OPENAI_BASE_URL
109
+
110
+ console.print(f"[bold green]Starting RAG Proxy on {host}:{port}[/bold green]")
111
+ console.print(f" 📂 Docs: [blue]{docs_folder}[/blue]")
112
+ console.print(f" 💾 DB: [blue]{chroma_path}[/blue]")
113
+ console.print(f" 🤖 Backend: [blue]{openai_base_url}[/blue]")
114
+ console.print(f" 🧠 Embeddings: Using [blue]{embedding_model}[/blue]")
115
+ console.print(f" 🔍 Limit: [blue]{limit}[/blue] chunks per query")
116
+
117
+ fastapi_app = create_app(
118
+ docs_folder,
119
+ chroma_path,
120
+ openai_base_url,
121
+ embedding_model,
122
+ openai_api_key,
123
+ openai_api_key,
124
+ limit,
125
+ enable_rag_tools=enable_rag_tools,
126
+ )
127
+
128
+ uvicorn.run(fastapi_app, host=host, port=port, log_config=None)
@@ -0,0 +1,209 @@
1
+ """Wyoming TTS Client for converting text to speech."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import json
7
+ import logging
8
+ from contextlib import suppress
9
+ from pathlib import Path # noqa: TC003
10
+
11
+ import typer
12
+
13
+ from agent_cli import config, opts
14
+ from agent_cli.cli import app
15
+ from agent_cli.core import process
16
+ from agent_cli.core.audio import setup_devices
17
+ from agent_cli.core.deps import requires_extras
18
+ from agent_cli.core.utils import (
19
+ enable_json_mode,
20
+ get_clipboard_text,
21
+ maybe_live,
22
+ print_command_line_args,
23
+ print_input_panel,
24
+ setup_logging,
25
+ stop_or_status_or_toggle,
26
+ )
27
+ from agent_cli.services.tts import handle_tts_playback
28
+
29
+ LOGGER = logging.getLogger()
30
+
31
+
32
+ async def _async_main(
33
+ *,
34
+ general_cfg: config.General,
35
+ text: str | None,
36
+ provider_cfg: config.ProviderSelection,
37
+ audio_out_cfg: config.AudioOutput,
38
+ wyoming_tts_cfg: config.WyomingTTS,
39
+ openai_tts_cfg: config.OpenAITTS,
40
+ kokoro_tts_cfg: config.KokoroTTS,
41
+ gemini_tts_cfg: config.GeminiTTS | None = None,
42
+ ) -> str | None:
43
+ """Async entry point for the speak command."""
44
+ # We only use setup_devices for its output device handling
45
+ device_info = setup_devices(general_cfg, None, audio_out_cfg)
46
+ if device_info is None:
47
+ return None
48
+ _, _, output_device_index = device_info
49
+ audio_out_cfg.output_device_index = output_device_index
50
+
51
+ # Get text from argument or clipboard
52
+ if text is None:
53
+ text = get_clipboard_text(quiet=general_cfg.quiet)
54
+ if not text:
55
+ return None
56
+ if not general_cfg.quiet:
57
+ print_input_panel(text, title="📋 Text from Clipboard")
58
+ elif not general_cfg.quiet:
59
+ print_input_panel(text, title="📝 Text to Speak")
60
+
61
+ # Handle TTS playback and saving
62
+ with maybe_live(not general_cfg.quiet) as live:
63
+ await handle_tts_playback(
64
+ text=text,
65
+ provider_cfg=provider_cfg,
66
+ audio_output_cfg=audio_out_cfg,
67
+ wyoming_tts_cfg=wyoming_tts_cfg,
68
+ openai_tts_cfg=openai_tts_cfg,
69
+ kokoro_tts_cfg=kokoro_tts_cfg,
70
+ gemini_tts_cfg=gemini_tts_cfg,
71
+ save_file=general_cfg.save_file,
72
+ quiet=general_cfg.quiet,
73
+ logger=LOGGER,
74
+ play_audio=not general_cfg.save_file, # Don't play if saving to file
75
+ status_message="🔊 Synthesizing speech...",
76
+ description="Audio",
77
+ live=live,
78
+ )
79
+
80
+ return text
81
+
82
+
83
+ @app.command("speak", rich_help_panel="Text Commands")
84
+ @requires_extras("audio")
85
+ def speak(
86
+ *,
87
+ text: str | None = typer.Argument(
88
+ None,
89
+ help="Text to speak. Reads from clipboard if not provided.",
90
+ rich_help_panel="General Options",
91
+ ),
92
+ # --- Provider Selection ---
93
+ tts_provider: str = opts.TTS_PROVIDER,
94
+ # --- TTS Configuration ---
95
+ # General
96
+ output_device_index: int | None = opts.OUTPUT_DEVICE_INDEX,
97
+ output_device_name: str | None = opts.OUTPUT_DEVICE_NAME,
98
+ tts_speed: float = opts.TTS_SPEED,
99
+ # Wyoming (local service)
100
+ tts_wyoming_ip: str = opts.TTS_WYOMING_IP,
101
+ tts_wyoming_port: int = opts.TTS_WYOMING_PORT,
102
+ tts_wyoming_voice: str | None = opts.TTS_WYOMING_VOICE,
103
+ tts_wyoming_language: str | None = opts.TTS_WYOMING_LANGUAGE,
104
+ tts_wyoming_speaker: str | None = opts.TTS_WYOMING_SPEAKER,
105
+ # OpenAI
106
+ tts_openai_model: str = opts.TTS_OPENAI_MODEL,
107
+ tts_openai_voice: str = opts.TTS_OPENAI_VOICE,
108
+ tts_openai_base_url: str | None = opts.TTS_OPENAI_BASE_URL,
109
+ # Kokoro
110
+ tts_kokoro_model: str = opts.TTS_KOKORO_MODEL,
111
+ tts_kokoro_voice: str = opts.TTS_KOKORO_VOICE,
112
+ tts_kokoro_host: str = opts.TTS_KOKORO_HOST,
113
+ # Gemini
114
+ tts_gemini_model: str = opts.TTS_GEMINI_MODEL,
115
+ tts_gemini_voice: str = opts.TTS_GEMINI_VOICE,
116
+ gemini_api_key: str | None = opts.GEMINI_API_KEY,
117
+ # --- General Options ---
118
+ list_devices: bool = opts.LIST_DEVICES,
119
+ save_file: Path | None = opts.SAVE_FILE,
120
+ stop: bool = opts.STOP,
121
+ status: bool = opts.STATUS,
122
+ toggle: bool = opts.TOGGLE,
123
+ log_level: opts.LogLevel = opts.LOG_LEVEL,
124
+ log_file: str | None = opts.LOG_FILE,
125
+ quiet: bool = opts.QUIET,
126
+ json_output: bool = opts.JSON_OUTPUT,
127
+ config_file: str | None = opts.CONFIG_FILE,
128
+ print_args: bool = opts.PRINT_ARGS,
129
+ ) -> None:
130
+ """Convert text to speech using Wyoming or OpenAI-compatible TTS server."""
131
+ if print_args:
132
+ print_command_line_args(locals())
133
+
134
+ effective_quiet = quiet or json_output
135
+ if json_output:
136
+ enable_json_mode()
137
+
138
+ setup_logging(log_level, log_file, quiet=effective_quiet)
139
+ general_cfg = config.General(
140
+ log_level=log_level,
141
+ log_file=log_file,
142
+ quiet=effective_quiet,
143
+ list_devices=list_devices,
144
+ save_file=save_file,
145
+ )
146
+ process_name = "speak"
147
+ if stop_or_status_or_toggle(
148
+ process_name,
149
+ "speak process",
150
+ stop,
151
+ status,
152
+ toggle,
153
+ quiet=general_cfg.quiet,
154
+ ):
155
+ return
156
+
157
+ # Use context manager for PID file management
158
+ with process.pid_file_context(process_name), suppress(KeyboardInterrupt):
159
+ provider_cfg = config.ProviderSelection(
160
+ tts_provider=tts_provider,
161
+ asr_provider="wyoming", # Not used
162
+ llm_provider="ollama", # Not used
163
+ )
164
+ audio_out_cfg = config.AudioOutput(
165
+ output_device_index=output_device_index,
166
+ output_device_name=output_device_name,
167
+ tts_speed=tts_speed,
168
+ enable_tts=True, # Implied for speak command
169
+ )
170
+ wyoming_tts_cfg = config.WyomingTTS(
171
+ tts_wyoming_ip=tts_wyoming_ip,
172
+ tts_wyoming_port=tts_wyoming_port,
173
+ tts_wyoming_voice=tts_wyoming_voice,
174
+ tts_wyoming_language=tts_wyoming_language,
175
+ tts_wyoming_speaker=tts_wyoming_speaker,
176
+ )
177
+ openai_tts_cfg = config.OpenAITTS(
178
+ tts_openai_model=tts_openai_model,
179
+ tts_openai_voice=tts_openai_voice,
180
+ tts_openai_base_url=tts_openai_base_url,
181
+ )
182
+ kokoro_tts_cfg = config.KokoroTTS(
183
+ tts_kokoro_model=tts_kokoro_model,
184
+ tts_kokoro_voice=tts_kokoro_voice,
185
+ tts_kokoro_host=tts_kokoro_host,
186
+ )
187
+ gemini_tts_cfg = config.GeminiTTS(
188
+ tts_gemini_model=tts_gemini_model,
189
+ tts_gemini_voice=tts_gemini_voice,
190
+ gemini_api_key=gemini_api_key,
191
+ )
192
+
193
+ spoken_text = asyncio.run(
194
+ _async_main(
195
+ general_cfg=general_cfg,
196
+ text=text,
197
+ provider_cfg=provider_cfg,
198
+ audio_out_cfg=audio_out_cfg,
199
+ wyoming_tts_cfg=wyoming_tts_cfg,
200
+ openai_tts_cfg=openai_tts_cfg,
201
+ kokoro_tts_cfg=kokoro_tts_cfg,
202
+ gemini_tts_cfg=gemini_tts_cfg,
203
+ ),
204
+ )
205
+ if json_output:
206
+ result = {"text": spoken_text}
207
+ if save_file:
208
+ result["file"] = str(save_file)
209
+ print(json.dumps(result))