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/rag/engine.py
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
"""Core RAG Engine Logic (Functional)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import time
|
|
7
|
+
from collections.abc import AsyncGenerator # noqa: TC003
|
|
8
|
+
from pathlib import Path # noqa: TC003
|
|
9
|
+
from typing import TYPE_CHECKING, Any
|
|
10
|
+
|
|
11
|
+
from fastapi.responses import StreamingResponse
|
|
12
|
+
|
|
13
|
+
from agent_cli.core.sse import format_chunk, format_done
|
|
14
|
+
from agent_cli.rag._prompt import RAG_PROMPT_NO_TOOLS, RAG_PROMPT_WITH_TOOLS
|
|
15
|
+
from agent_cli.rag._retriever import search_context
|
|
16
|
+
from agent_cli.rag._utils import load_document_text
|
|
17
|
+
from agent_cli.rag.models import Message, RetrievalResult # noqa: TC001
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from chromadb import Collection
|
|
21
|
+
from pydantic_ai import Agent
|
|
22
|
+
from pydantic_ai.messages import ModelRequest, ModelResponse
|
|
23
|
+
from pydantic_ai.result import RunResult
|
|
24
|
+
|
|
25
|
+
from agent_cli.core.reranker import OnnxCrossEncoder
|
|
26
|
+
from agent_cli.rag.models import ChatRequest
|
|
27
|
+
|
|
28
|
+
LOGGER = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
# Maximum context size in characters (~3000 tokens at 4 chars/token)
|
|
31
|
+
_MAX_CONTEXT_CHARS = 12000
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def truncate_context(context: str, max_chars: int = _MAX_CONTEXT_CHARS) -> str:
|
|
35
|
+
"""Truncate context to fit within token budget while keeping complete chunks.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
context: Raw context string with chunks separated by "---".
|
|
39
|
+
max_chars: Maximum characters to keep (default ~3000 tokens).
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Truncated context with complete chunks only.
|
|
43
|
+
|
|
44
|
+
"""
|
|
45
|
+
if len(context) <= max_chars:
|
|
46
|
+
return context
|
|
47
|
+
|
|
48
|
+
separator = "\n\n---\n\n"
|
|
49
|
+
chunks = context.split(separator)
|
|
50
|
+
result = []
|
|
51
|
+
total = 0
|
|
52
|
+
|
|
53
|
+
for chunk in chunks:
|
|
54
|
+
chunk_len = len(chunk) + len(separator)
|
|
55
|
+
if total + chunk_len > max_chars:
|
|
56
|
+
break
|
|
57
|
+
result.append(chunk)
|
|
58
|
+
total += chunk_len
|
|
59
|
+
|
|
60
|
+
return separator.join(result)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _is_path_safe(base: Path, requested: Path) -> bool:
|
|
64
|
+
"""Check if requested path is safely within base directory."""
|
|
65
|
+
try:
|
|
66
|
+
requested.resolve().relative_to(base.resolve())
|
|
67
|
+
return True
|
|
68
|
+
except ValueError:
|
|
69
|
+
return False
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _retrieve_context(
|
|
73
|
+
request: ChatRequest,
|
|
74
|
+
collection: Collection,
|
|
75
|
+
reranker_model: OnnxCrossEncoder,
|
|
76
|
+
default_top_k: int = 3,
|
|
77
|
+
) -> RetrievalResult | None:
|
|
78
|
+
"""Retrieve context for the request.
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
The retrieval result, or None if no retrieval was performed.
|
|
82
|
+
|
|
83
|
+
"""
|
|
84
|
+
# Get last user message
|
|
85
|
+
user_message = next(
|
|
86
|
+
(m.content for m in reversed(request.messages) if m.role == "user"),
|
|
87
|
+
None,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
if not user_message:
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
# Retrieve
|
|
94
|
+
top_k = request.rag_top_k if request.rag_top_k is not None else default_top_k
|
|
95
|
+
if top_k <= 0:
|
|
96
|
+
LOGGER.info("RAG retrieval disabled for this request (top_k=%s)", top_k)
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
retrieval = search_context(collection, reranker_model, user_message, top_k=top_k)
|
|
100
|
+
|
|
101
|
+
if not retrieval.context:
|
|
102
|
+
LOGGER.info("βΉοΈ No relevant context found for query: '%s'", user_message[:50]) # noqa: RUF001
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
LOGGER.info(
|
|
106
|
+
"β
Found %d relevant sources for query: '%s'",
|
|
107
|
+
len(retrieval.sources),
|
|
108
|
+
user_message[:50],
|
|
109
|
+
)
|
|
110
|
+
return retrieval
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _convert_messages(
|
|
114
|
+
messages: list[Message],
|
|
115
|
+
) -> tuple[list[ModelRequest | ModelResponse], str]:
|
|
116
|
+
"""Convert OpenAI messages to Pydantic AI messages and extract user prompt."""
|
|
117
|
+
from pydantic_ai.messages import ( # noqa: PLC0415
|
|
118
|
+
ModelRequest,
|
|
119
|
+
ModelResponse,
|
|
120
|
+
SystemPromptPart,
|
|
121
|
+
TextPart,
|
|
122
|
+
UserPromptPart,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
pyd_messages: list[ModelRequest | ModelResponse] = []
|
|
126
|
+
|
|
127
|
+
# Validation: Ensure there is at least one message
|
|
128
|
+
if not messages:
|
|
129
|
+
return [], ""
|
|
130
|
+
|
|
131
|
+
# Split history and last user prompt
|
|
132
|
+
history_msgs = messages[:-1]
|
|
133
|
+
last_msg = messages[-1]
|
|
134
|
+
|
|
135
|
+
for m in history_msgs:
|
|
136
|
+
if m.role == "system":
|
|
137
|
+
pyd_messages.append(ModelRequest(parts=[SystemPromptPart(content=m.content)]))
|
|
138
|
+
elif m.role == "user":
|
|
139
|
+
pyd_messages.append(ModelRequest(parts=[UserPromptPart(content=m.content)]))
|
|
140
|
+
elif m.role == "assistant":
|
|
141
|
+
pyd_messages.append(ModelResponse(parts=[TextPart(content=m.content)]))
|
|
142
|
+
|
|
143
|
+
return pyd_messages, last_msg.content
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _extract_model_settings(request: ChatRequest) -> dict[str, Any]:
|
|
147
|
+
"""Extract model settings from request."""
|
|
148
|
+
settings = {}
|
|
149
|
+
if request.temperature is not None:
|
|
150
|
+
settings["temperature"] = request.temperature
|
|
151
|
+
if request.max_tokens is not None:
|
|
152
|
+
settings["max_tokens"] = request.max_tokens
|
|
153
|
+
return settings
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _build_openai_response(
|
|
157
|
+
result: RunResult,
|
|
158
|
+
model_name: str,
|
|
159
|
+
retrieval: RetrievalResult | None,
|
|
160
|
+
) -> dict[str, Any]:
|
|
161
|
+
"""Format the Pydantic AI result as an OpenAI-compatible dict."""
|
|
162
|
+
usage = result.usage()
|
|
163
|
+
response: dict[str, Any] = {
|
|
164
|
+
"id": f"chatcmpl-{result.run_id}",
|
|
165
|
+
"object": "chat.completion",
|
|
166
|
+
"created": int(time.time()),
|
|
167
|
+
"model": model_name,
|
|
168
|
+
"choices": [
|
|
169
|
+
{
|
|
170
|
+
"index": 0,
|
|
171
|
+
"message": {"role": "assistant", "content": result.output},
|
|
172
|
+
"finish_reason": "stop",
|
|
173
|
+
},
|
|
174
|
+
],
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if usage:
|
|
178
|
+
response["usage"] = {
|
|
179
|
+
"prompt_tokens": usage.input_tokens or 0,
|
|
180
|
+
"completion_tokens": usage.output_tokens or 0,
|
|
181
|
+
"total_tokens": usage.total_tokens or 0,
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if retrieval:
|
|
185
|
+
response["rag_sources"] = retrieval.sources
|
|
186
|
+
|
|
187
|
+
return response
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
async def process_chat_request(
|
|
191
|
+
request: ChatRequest,
|
|
192
|
+
collection: Collection,
|
|
193
|
+
reranker_model: OnnxCrossEncoder,
|
|
194
|
+
openai_base_url: str,
|
|
195
|
+
docs_folder: Path,
|
|
196
|
+
default_top_k: int = 3,
|
|
197
|
+
api_key: str | None = None,
|
|
198
|
+
enable_rag_tools: bool = True,
|
|
199
|
+
) -> Any:
|
|
200
|
+
"""Process a chat request with RAG."""
|
|
201
|
+
# 1. Retrieve Context
|
|
202
|
+
retrieval = _retrieve_context(request, collection, reranker_model, default_top_k=default_top_k)
|
|
203
|
+
|
|
204
|
+
# 2. Define Tool
|
|
205
|
+
def read_full_document(file_path: str) -> str:
|
|
206
|
+
"""Read the full content of a document by its path.
|
|
207
|
+
|
|
208
|
+
Use this tool when the context snippet is insufficient and you need
|
|
209
|
+
the complete document to answer the user's question accurately.
|
|
210
|
+
The file_path should match one of the [Source: ...] paths from the context.
|
|
211
|
+
"""
|
|
212
|
+
try:
|
|
213
|
+
full_path = (docs_folder / file_path).resolve()
|
|
214
|
+
if not _is_path_safe(docs_folder, full_path):
|
|
215
|
+
return "Error: Access denied. Path is outside the document folder."
|
|
216
|
+
if not full_path.exists():
|
|
217
|
+
return f"Error: File not found: {file_path}"
|
|
218
|
+
|
|
219
|
+
text = load_document_text(full_path)
|
|
220
|
+
if text is None:
|
|
221
|
+
return "Error: Could not read file (unsupported format or encoding)."
|
|
222
|
+
return text
|
|
223
|
+
except Exception as e:
|
|
224
|
+
return f"Error reading file: {e}"
|
|
225
|
+
|
|
226
|
+
# 3. Define RAG System Prompt (additive - user's system prompt is preserved in history)
|
|
227
|
+
# Determine tool availability:
|
|
228
|
+
# - If CLI flag `enable_rag_tools` is False, tools are disabled globally.
|
|
229
|
+
# - If CLI flag is True, check request.rag_enable_tools (default True).
|
|
230
|
+
tools_allowed = enable_rag_tools and (request.rag_enable_tools is not False)
|
|
231
|
+
|
|
232
|
+
if retrieval and retrieval.context:
|
|
233
|
+
truncated = truncate_context(retrieval.context)
|
|
234
|
+
template = RAG_PROMPT_WITH_TOOLS if tools_allowed else RAG_PROMPT_NO_TOOLS
|
|
235
|
+
system_prompt = template.format(context=truncated)
|
|
236
|
+
else:
|
|
237
|
+
system_prompt = ""
|
|
238
|
+
|
|
239
|
+
# 4. Setup Agent
|
|
240
|
+
from pydantic_ai import Agent # noqa: PLC0415
|
|
241
|
+
from pydantic_ai.models.openai import OpenAIModel # noqa: PLC0415
|
|
242
|
+
from pydantic_ai.providers.openai import OpenAIProvider # noqa: PLC0415
|
|
243
|
+
|
|
244
|
+
provider = OpenAIProvider(base_url=openai_base_url, api_key=api_key or "dummy")
|
|
245
|
+
model = OpenAIModel(model_name=request.model, provider=provider)
|
|
246
|
+
|
|
247
|
+
tools = [read_full_document] if tools_allowed else []
|
|
248
|
+
agent = Agent(model=model, tools=tools, system_prompt=system_prompt)
|
|
249
|
+
|
|
250
|
+
# 5. Prepare Message History & Prompt
|
|
251
|
+
history, user_prompt = _convert_messages(request.messages)
|
|
252
|
+
|
|
253
|
+
# 6. Model Settings
|
|
254
|
+
model_settings = _extract_model_settings(request)
|
|
255
|
+
|
|
256
|
+
# 7. Run Agent
|
|
257
|
+
if request.stream:
|
|
258
|
+
return StreamingResponse(
|
|
259
|
+
_stream_generator(
|
|
260
|
+
agent,
|
|
261
|
+
user_prompt,
|
|
262
|
+
history,
|
|
263
|
+
model_settings,
|
|
264
|
+
request.model,
|
|
265
|
+
retrieval,
|
|
266
|
+
),
|
|
267
|
+
media_type="text/event-stream",
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
result = await agent.run(
|
|
271
|
+
user_prompt,
|
|
272
|
+
message_history=history,
|
|
273
|
+
model_settings=model_settings,
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
return _build_openai_response(result, request.model, retrieval)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
async def _stream_generator(
|
|
280
|
+
agent: Agent,
|
|
281
|
+
prompt: str,
|
|
282
|
+
history: list[ModelRequest | ModelResponse],
|
|
283
|
+
settings: dict[str, Any],
|
|
284
|
+
model_name: str,
|
|
285
|
+
retrieval: RetrievalResult | None,
|
|
286
|
+
) -> AsyncGenerator[str, None]:
|
|
287
|
+
"""Stream Pydantic AI result as OpenAI SSE."""
|
|
288
|
+
async with agent.run_stream(prompt, message_history=history, model_settings=settings) as result:
|
|
289
|
+
async for chunk in result.stream_text(delta=True):
|
|
290
|
+
yield format_chunk(result.run_id, model_name, content=chunk)
|
|
291
|
+
|
|
292
|
+
# Finish chunk with optional RAG sources
|
|
293
|
+
extra = None
|
|
294
|
+
if retrieval and retrieval.sources:
|
|
295
|
+
extra = {
|
|
296
|
+
"rag_sources": [
|
|
297
|
+
{"source": s.source, "path": s.path, "chunk_id": s.chunk_id, "score": s.score}
|
|
298
|
+
for s in retrieval.sources
|
|
299
|
+
],
|
|
300
|
+
}
|
|
301
|
+
yield format_chunk(result.run_id, model_name, finish_reason="stop", extra=extra)
|
|
302
|
+
yield format_done()
|
agent_cli/rag/models.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""RAG data models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, ConfigDict
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Message(BaseModel):
|
|
9
|
+
"""Chat message model."""
|
|
10
|
+
|
|
11
|
+
role: str
|
|
12
|
+
content: str
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ChatRequest(BaseModel):
|
|
16
|
+
"""Chat completion request model."""
|
|
17
|
+
|
|
18
|
+
model_config = ConfigDict(extra="allow")
|
|
19
|
+
|
|
20
|
+
model: str
|
|
21
|
+
messages: list[Message]
|
|
22
|
+
temperature: float | None = 0.7
|
|
23
|
+
max_tokens: int | None = 1000
|
|
24
|
+
stream: bool | None = False
|
|
25
|
+
rag_top_k: int | None = None
|
|
26
|
+
rag_enable_tools: bool | None = True
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class DocMetadata(BaseModel):
|
|
30
|
+
"""Metadata for an indexed document chunk."""
|
|
31
|
+
|
|
32
|
+
source: str
|
|
33
|
+
file_path: str
|
|
34
|
+
file_type: str
|
|
35
|
+
chunk_id: int
|
|
36
|
+
total_chunks: int
|
|
37
|
+
indexed_at: str
|
|
38
|
+
file_hash: str
|
|
39
|
+
file_mtime: float
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class RagSource(BaseModel):
|
|
43
|
+
"""Source information for RAG response."""
|
|
44
|
+
|
|
45
|
+
source: str
|
|
46
|
+
path: str
|
|
47
|
+
chunk_id: int
|
|
48
|
+
score: float
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class RetrievalResult(BaseModel):
|
|
52
|
+
"""Result of a RAG retrieval operation."""
|
|
53
|
+
|
|
54
|
+
context: str
|
|
55
|
+
sources: list[RagSource]
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Scripts package containing installation and service management scripts."""
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Check that plugin skill files are in sync with source files."""
|
|
3
|
+
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
SYNC_PAIRS = [
|
|
8
|
+
# Plugin marketplace distribution
|
|
9
|
+
("agent_cli/dev/skill/SKILL.md", ".claude-plugin/skills/agent-cli-dev/SKILL.md"),
|
|
10
|
+
("agent_cli/dev/skill/examples.md", ".claude-plugin/skills/agent-cli-dev/examples.md"),
|
|
11
|
+
# Project-local skill (for Claude Code working on this repo)
|
|
12
|
+
("agent_cli/dev/skill/SKILL.md", ".claude/skills/agent-cli-dev/SKILL.md"),
|
|
13
|
+
("agent_cli/dev/skill/examples.md", ".claude/skills/agent-cli-dev/examples.md"),
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def main() -> int:
|
|
18
|
+
"""Check that plugin skill files match source files."""
|
|
19
|
+
root = Path(__file__).parent.parent
|
|
20
|
+
out_of_sync = []
|
|
21
|
+
|
|
22
|
+
for source, target in SYNC_PAIRS:
|
|
23
|
+
source_path = root / source
|
|
24
|
+
target_path = root / target
|
|
25
|
+
|
|
26
|
+
if not source_path.exists():
|
|
27
|
+
print(f"Source not found: {source}")
|
|
28
|
+
continue
|
|
29
|
+
|
|
30
|
+
if not target_path.exists():
|
|
31
|
+
out_of_sync.append((source, target, "target missing"))
|
|
32
|
+
continue
|
|
33
|
+
|
|
34
|
+
if source_path.read_text() != target_path.read_text():
|
|
35
|
+
out_of_sync.append((source, target, "content differs"))
|
|
36
|
+
|
|
37
|
+
if out_of_sync:
|
|
38
|
+
print("Plugin skill files are out of sync:")
|
|
39
|
+
for source, target, reason in out_of_sync:
|
|
40
|
+
print(f" {source} -> {target} ({reason})")
|
|
41
|
+
print("\nRun:")
|
|
42
|
+
print(" cp agent_cli/dev/skill/*.md .claude-plugin/skills/agent-cli-dev/")
|
|
43
|
+
print(" cp agent_cli/dev/skill/*.md .claude/skills/agent-cli-dev/")
|
|
44
|
+
return 1
|
|
45
|
+
|
|
46
|
+
return 0
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
if __name__ == "__main__":
|
|
50
|
+
sys.exit(main())
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Linux Hotkeys
|
|
2
|
+
|
|
3
|
+
System-wide hotkeys for agent-cli voice AI features on Linux.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
./setup-linux-hotkeys.sh
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The setup script will:
|
|
12
|
+
1. Install notification support if missing
|
|
13
|
+
2. Show you the exact hotkey bindings to add to your desktop environment
|
|
14
|
+
3. Provide copy-paste ready configuration for popular desktop environments
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
- **`Super+Shift+R`** β Toggle voice transcription (start/stop with result)
|
|
19
|
+
- **`Super+Shift+A`** β Autocorrect clipboard text
|
|
20
|
+
- **`Super+Shift+V`** β Toggle voice edit mode for clipboard
|
|
21
|
+
|
|
22
|
+
Results appear in notifications and clipboard.
|
|
23
|
+
|
|
24
|
+
## Desktop Environment Support
|
|
25
|
+
|
|
26
|
+
The setup script provides copy-paste ready instructions for:
|
|
27
|
+
|
|
28
|
+
- **Hyprland**: Add bindings to `~/.config/hypr/hyprland.conf`
|
|
29
|
+
- **Sway**: Add bindings to `~/.config/sway/config`
|
|
30
|
+
- **i3**: Add bindings to `~/.config/i3/config`
|
|
31
|
+
- **GNOME**: Use Settings β Keyboard β Custom Shortcuts
|
|
32
|
+
- **KDE**: Use System Settings β Shortcuts β Custom Shortcuts
|
|
33
|
+
- **XFCE**: Use Settings Manager β Keyboard β Application Shortcuts
|
|
34
|
+
- **Other**: Manual hotkey configuration in your desktop environment
|
|
35
|
+
|
|
36
|
+
## Features
|
|
37
|
+
|
|
38
|
+
- **Manual configuration**: Simple setup with clear instructions for each desktop environment
|
|
39
|
+
- **Wayland support**: Includes clipboard syncing for Wayland compositors
|
|
40
|
+
- **Fallback notifications**: Uses `notify-send`, `dunstify`, or console output
|
|
41
|
+
- **Error handling**: Shows notifications for both success and failure cases
|
|
42
|
+
- **PATH handling**: Scripts automatically find agent-cli installation
|
|
43
|
+
|
|
44
|
+
## Troubleshooting
|
|
45
|
+
|
|
46
|
+
**Hotkeys not working?**
|
|
47
|
+
- Check your desktop's keyboard shortcut settings for conflicts
|
|
48
|
+
- Make sure you added the bindings to your desktop environment's config
|
|
49
|
+
- Verify the script paths are correct
|
|
50
|
+
|
|
51
|
+
**No notifications?**
|
|
52
|
+
```bash
|
|
53
|
+
sudo apt install libnotify-bin # Ubuntu/Debian
|
|
54
|
+
sudo dnf install libnotify # Fedora/RHEL
|
|
55
|
+
sudo pacman -S libnotify # Arch
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**Services not running?**
|
|
59
|
+
```bash
|
|
60
|
+
./start-all-services.sh
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
That's it! System-wide hotkeys for agent-cli on Linux.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
# Toggle script for agent-cli autocorrect on Linux
|
|
4
|
+
#
|
|
5
|
+
# This script corrects text from clipboard using AI:
|
|
6
|
+
# - Reads text from clipboard
|
|
7
|
+
# - Processes it with LLM for grammar/spelling corrections
|
|
8
|
+
# - Displays the corrected result
|
|
9
|
+
#
|
|
10
|
+
# Works across different Linux desktop environments
|
|
11
|
+
|
|
12
|
+
# Function to send notification
|
|
13
|
+
notify() {
|
|
14
|
+
local title="$1"
|
|
15
|
+
local message="$2"
|
|
16
|
+
local timeout="${3:-3000}"
|
|
17
|
+
|
|
18
|
+
if command -v notify-send &> /dev/null; then
|
|
19
|
+
notify-send -t "$timeout" "$title" "$message"
|
|
20
|
+
elif command -v dunstify &> /dev/null; then
|
|
21
|
+
dunstify -t "$timeout" "$title" "$message"
|
|
22
|
+
else
|
|
23
|
+
echo "$title: $message"
|
|
24
|
+
fi
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
# Function to sync clipboard (Wayland)
|
|
28
|
+
sync_clipboard() {
|
|
29
|
+
if command -v wl-paste &> /dev/null && command -v wl-copy &> /dev/null; then
|
|
30
|
+
wl-paste | wl-copy -p 2>/dev/null || true
|
|
31
|
+
fi
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
# Ensure agent-cli is in PATH
|
|
35
|
+
export PATH="$PATH:$HOME/.local/bin"
|
|
36
|
+
|
|
37
|
+
notify "π Autocorrect" "Processing clipboard text..."
|
|
38
|
+
|
|
39
|
+
OUTPUT=$(agent-cli autocorrect --quiet 2>/dev/null) && {
|
|
40
|
+
# Sync clipboard to primary selection (Wayland)
|
|
41
|
+
sync_clipboard
|
|
42
|
+
notify "β
Corrected" "$OUTPUT" 5000
|
|
43
|
+
} || {
|
|
44
|
+
notify "β Error" "No text to correct or processing failed" 3000
|
|
45
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
# Toggle script for agent-cli transcription on Linux
|
|
4
|
+
#
|
|
5
|
+
# This script provides a simple toggle mechanism for voice transcription:
|
|
6
|
+
# - First invocation: Starts transcription in the background
|
|
7
|
+
# - Second invocation: Stops transcription and displays the result
|
|
8
|
+
#
|
|
9
|
+
# Works across different Linux desktop environments
|
|
10
|
+
|
|
11
|
+
# Function to send notification
|
|
12
|
+
notify() {
|
|
13
|
+
local title="$1"
|
|
14
|
+
local message="$2"
|
|
15
|
+
local timeout="${3:-3000}"
|
|
16
|
+
|
|
17
|
+
if command -v notify-send &> /dev/null; then
|
|
18
|
+
notify-send -t "$timeout" "$title" "$message"
|
|
19
|
+
elif command -v dunstify &> /dev/null; then
|
|
20
|
+
dunstify -t "$timeout" "$title" "$message"
|
|
21
|
+
else
|
|
22
|
+
echo "$title: $message"
|
|
23
|
+
fi
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
# Function to sync clipboard (Wayland)
|
|
27
|
+
sync_clipboard() {
|
|
28
|
+
if command -v wl-paste &> /dev/null && command -v wl-copy &> /dev/null; then
|
|
29
|
+
wl-paste | wl-copy -p 2>/dev/null || true
|
|
30
|
+
fi
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
# Check if agent-cli transcribe is already running
|
|
34
|
+
if pgrep -f "agent-cli transcribe" > /dev/null; then
|
|
35
|
+
# Transcription is running - stop it
|
|
36
|
+
pkill -INT -f "agent-cli transcribe"
|
|
37
|
+
notify "π Transcription Stopped" "Processing results..."
|
|
38
|
+
else
|
|
39
|
+
# Transcription is not running - start it
|
|
40
|
+
|
|
41
|
+
# Ensure agent-cli is in PATH
|
|
42
|
+
export PATH="$PATH:$HOME/.local/bin"
|
|
43
|
+
|
|
44
|
+
# Notify user that recording has started
|
|
45
|
+
notify "ποΈ Transcription Started" "Listening in background..."
|
|
46
|
+
|
|
47
|
+
# Start transcription in background
|
|
48
|
+
(
|
|
49
|
+
OUTPUT=$(agent-cli transcribe --llm --quiet 2>/dev/null)
|
|
50
|
+
if [ -n "$OUTPUT" ]; then
|
|
51
|
+
# Sync clipboard to primary selection (Wayland)
|
|
52
|
+
sync_clipboard
|
|
53
|
+
notify "π Transcription Result" "$OUTPUT" 5000
|
|
54
|
+
else
|
|
55
|
+
notify "β Error" "No output" 3000
|
|
56
|
+
fi
|
|
57
|
+
) &
|
|
58
|
+
fi
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
# Toggle script for agent-cli voice-edit on Linux
|
|
4
|
+
#
|
|
5
|
+
# This script provides voice editing for clipboard text:
|
|
6
|
+
# - First invocation: Starts voice editing in the background
|
|
7
|
+
# - Second invocation: Stops voice editing and displays the result
|
|
8
|
+
#
|
|
9
|
+
# Works across different Linux desktop environments
|
|
10
|
+
|
|
11
|
+
# Function to send notification
|
|
12
|
+
notify() {
|
|
13
|
+
local title="$1"
|
|
14
|
+
local message="$2"
|
|
15
|
+
local timeout="${3:-3000}"
|
|
16
|
+
|
|
17
|
+
if command -v notify-send &> /dev/null; then
|
|
18
|
+
notify-send -t "$timeout" "$title" "$message"
|
|
19
|
+
elif command -v dunstify &> /dev/null; then
|
|
20
|
+
dunstify -t "$timeout" "$title" "$message"
|
|
21
|
+
else
|
|
22
|
+
echo "$title: $message"
|
|
23
|
+
fi
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
# Function to sync clipboard (Wayland)
|
|
27
|
+
sync_clipboard() {
|
|
28
|
+
if command -v wl-paste &> /dev/null && command -v wl-copy &> /dev/null; then
|
|
29
|
+
wl-paste | wl-copy -p 2>/dev/null || true
|
|
30
|
+
fi
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
# Check if agent-cli voice-edit is already running
|
|
34
|
+
if pgrep -f "agent-cli voice-edit" > /dev/null; then
|
|
35
|
+
# Voice edit is running - stop it
|
|
36
|
+
pkill -INT -f "agent-cli voice-edit"
|
|
37
|
+
notify "π Voice Edit Stopped" "Processing voice command..."
|
|
38
|
+
else
|
|
39
|
+
# Voice edit is not running - start it
|
|
40
|
+
|
|
41
|
+
# Ensure agent-cli is in PATH
|
|
42
|
+
export PATH="$PATH:$HOME/.local/bin"
|
|
43
|
+
|
|
44
|
+
# Notify user that recording has started
|
|
45
|
+
notify "ποΈ Voice Edit Started" "Listening for voice command..."
|
|
46
|
+
|
|
47
|
+
# Start voice edit in background
|
|
48
|
+
(
|
|
49
|
+
OUTPUT=$(agent-cli voice-edit --quiet 2>/dev/null)
|
|
50
|
+
if [ -n "$OUTPUT" ]; then
|
|
51
|
+
# Sync clipboard to primary selection (Wayland)
|
|
52
|
+
sync_clipboard
|
|
53
|
+
notify "β¨ Voice Edit Result" "$OUTPUT" 5000
|
|
54
|
+
else
|
|
55
|
+
notify "β Error" "No output" 3000
|
|
56
|
+
fi
|
|
57
|
+
) &
|
|
58
|
+
fi
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# macOS Hotkeys
|
|
2
|
+
|
|
3
|
+
System-wide hotkeys for agent-cli voice AI features on macOS.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
./setup-macos-hotkeys.sh
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
- **`Cmd+Shift+R`** β Toggle voice transcription (start/stop with result)
|
|
14
|
+
- **`Cmd+Shift+A`** β Autocorrect clipboard text
|
|
15
|
+
- **`Cmd+Shift+V`** β Toggle voice edit mode for clipboard
|
|
16
|
+
|
|
17
|
+
Results appear in notifications and clipboard.
|
|
18
|
+
|
|
19
|
+
> [!TIP]
|
|
20
|
+
> For a persistent "Listeningβ¦" indicator, open System Settings β Notifications β *terminal-notifier* and set the Alert style to **Persistent** (or choose **Alerts** on older macOS versions).
|
|
21
|
+
> Also enable "Allow notification when mirroring or sharing the display".
|
|
22
|
+
> The scripts keep that alert pinned while dismissing status/result notifications automatically.
|
|
23
|
+
|
|
24
|
+
## What it installs
|
|
25
|
+
|
|
26
|
+
- **skhd**: Hotkey manager
|
|
27
|
+
- **terminal-notifier**: Notifications
|
|
28
|
+
- **Configuration**: Automatic setup
|
|
29
|
+
|
|
30
|
+
## Troubleshooting
|
|
31
|
+
|
|
32
|
+
**Hotkey not working?**
|
|
33
|
+
- Grant accessibility permissions in System Settings
|
|
34
|
+
|
|
35
|
+
**No notifications?**
|
|
36
|
+
```bash
|
|
37
|
+
terminal-notifier -title "Test" -message "Hello"
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**Services not running?**
|
|
41
|
+
```bash
|
|
42
|
+
./start-all-services.sh
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
That's it! System-wide hotkeys for agent-cli on macOS.
|