code-puppy 0.0.287__py3-none-any.whl → 0.0.323__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.
- code_puppy/__init__.py +3 -1
- code_puppy/agents/agent_code_puppy.py +5 -4
- code_puppy/agents/agent_creator_agent.py +22 -18
- code_puppy/agents/agent_manager.py +2 -2
- code_puppy/agents/base_agent.py +496 -102
- code_puppy/callbacks.py +8 -0
- code_puppy/chatgpt_codex_client.py +283 -0
- code_puppy/cli_runner.py +795 -0
- code_puppy/command_line/add_model_menu.py +19 -16
- code_puppy/command_line/attachments.py +10 -5
- code_puppy/command_line/autosave_menu.py +269 -41
- code_puppy/command_line/colors_menu.py +515 -0
- code_puppy/command_line/command_handler.py +10 -24
- code_puppy/command_line/config_commands.py +106 -25
- code_puppy/command_line/core_commands.py +32 -20
- code_puppy/command_line/mcp/add_command.py +3 -16
- code_puppy/command_line/mcp/base.py +0 -3
- code_puppy/command_line/mcp/catalog_server_installer.py +15 -15
- code_puppy/command_line/mcp/custom_server_form.py +66 -5
- code_puppy/command_line/mcp/custom_server_installer.py +17 -17
- code_puppy/command_line/mcp/edit_command.py +15 -22
- code_puppy/command_line/mcp/handler.py +7 -2
- code_puppy/command_line/mcp/help_command.py +2 -2
- code_puppy/command_line/mcp/install_command.py +10 -14
- code_puppy/command_line/mcp/install_menu.py +2 -6
- code_puppy/command_line/mcp/list_command.py +2 -2
- code_puppy/command_line/mcp/logs_command.py +174 -65
- code_puppy/command_line/mcp/remove_command.py +2 -2
- code_puppy/command_line/mcp/restart_command.py +7 -2
- code_puppy/command_line/mcp/search_command.py +16 -10
- code_puppy/command_line/mcp/start_all_command.py +16 -6
- code_puppy/command_line/mcp/start_command.py +12 -10
- code_puppy/command_line/mcp/status_command.py +4 -5
- code_puppy/command_line/mcp/stop_all_command.py +5 -1
- code_puppy/command_line/mcp/stop_command.py +6 -4
- code_puppy/command_line/mcp/test_command.py +2 -2
- code_puppy/command_line/mcp/wizard_utils.py +20 -16
- code_puppy/command_line/model_settings_menu.py +53 -7
- code_puppy/command_line/motd.py +1 -1
- code_puppy/command_line/pin_command_completion.py +82 -7
- code_puppy/command_line/prompt_toolkit_completion.py +32 -9
- code_puppy/command_line/session_commands.py +11 -4
- code_puppy/config.py +217 -53
- code_puppy/error_logging.py +118 -0
- code_puppy/gemini_code_assist.py +385 -0
- code_puppy/keymap.py +126 -0
- code_puppy/main.py +5 -745
- code_puppy/mcp_/__init__.py +17 -0
- code_puppy/mcp_/blocking_startup.py +63 -36
- code_puppy/mcp_/captured_stdio_server.py +1 -1
- code_puppy/mcp_/config_wizard.py +4 -4
- code_puppy/mcp_/dashboard.py +15 -6
- code_puppy/mcp_/managed_server.py +25 -5
- code_puppy/mcp_/manager.py +65 -0
- code_puppy/mcp_/mcp_logs.py +224 -0
- code_puppy/mcp_/registry.py +6 -6
- code_puppy/messaging/__init__.py +184 -2
- code_puppy/messaging/bus.py +610 -0
- code_puppy/messaging/commands.py +167 -0
- code_puppy/messaging/markdown_patches.py +57 -0
- code_puppy/messaging/message_queue.py +3 -3
- code_puppy/messaging/messages.py +470 -0
- code_puppy/messaging/renderers.py +43 -141
- code_puppy/messaging/rich_renderer.py +900 -0
- code_puppy/messaging/spinner/console_spinner.py +39 -2
- code_puppy/model_factory.py +292 -53
- code_puppy/model_utils.py +57 -48
- code_puppy/models.json +19 -5
- code_puppy/plugins/__init__.py +152 -10
- code_puppy/plugins/chatgpt_oauth/config.py +20 -12
- code_puppy/plugins/chatgpt_oauth/oauth_flow.py +5 -6
- code_puppy/plugins/chatgpt_oauth/register_callbacks.py +3 -3
- code_puppy/plugins/chatgpt_oauth/test_plugin.py +30 -13
- code_puppy/plugins/chatgpt_oauth/utils.py +180 -65
- code_puppy/plugins/claude_code_oauth/config.py +15 -11
- code_puppy/plugins/claude_code_oauth/register_callbacks.py +28 -0
- code_puppy/plugins/claude_code_oauth/utils.py +6 -1
- code_puppy/plugins/example_custom_command/register_callbacks.py +2 -2
- code_puppy/plugins/oauth_puppy_html.py +3 -0
- code_puppy/plugins/shell_safety/agent_shell_safety.py +1 -134
- code_puppy/plugins/shell_safety/command_cache.py +156 -0
- code_puppy/plugins/shell_safety/register_callbacks.py +77 -3
- code_puppy/prompts/codex_system_prompt.md +310 -0
- code_puppy/pydantic_patches.py +131 -0
- code_puppy/session_storage.py +2 -1
- code_puppy/status_display.py +7 -5
- code_puppy/terminal_utils.py +126 -0
- code_puppy/tools/agent_tools.py +131 -70
- code_puppy/tools/browser/browser_control.py +10 -14
- code_puppy/tools/browser/browser_interactions.py +20 -28
- code_puppy/tools/browser/browser_locators.py +27 -29
- code_puppy/tools/browser/browser_navigation.py +9 -9
- code_puppy/tools/browser/browser_screenshot.py +12 -14
- code_puppy/tools/browser/browser_scripts.py +17 -29
- code_puppy/tools/browser/browser_workflows.py +24 -25
- code_puppy/tools/browser/camoufox_manager.py +22 -26
- code_puppy/tools/command_runner.py +410 -88
- code_puppy/tools/common.py +51 -38
- code_puppy/tools/file_modifications.py +98 -24
- code_puppy/tools/file_operations.py +113 -202
- code_puppy/version_checker.py +28 -13
- {code_puppy-0.0.287.data → code_puppy-0.0.323.data}/data/code_puppy/models.json +19 -5
- {code_puppy-0.0.287.dist-info → code_puppy-0.0.323.dist-info}/METADATA +3 -8
- code_puppy-0.0.323.dist-info/RECORD +168 -0
- code_puppy/tui_state.py +0 -55
- code_puppy-0.0.287.dist-info/RECORD +0 -153
- {code_puppy-0.0.287.data → code_puppy-0.0.323.data}/data/code_puppy/models_dev_api.json +0 -0
- {code_puppy-0.0.287.dist-info → code_puppy-0.0.323.dist-info}/WHEEL +0 -0
- {code_puppy-0.0.287.dist-info → code_puppy-0.0.323.dist-info}/entry_points.txt +0 -0
- {code_puppy-0.0.287.dist-info → code_puppy-0.0.323.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
"""Gemini Code Assist Model for pydantic_ai.
|
|
2
|
+
|
|
3
|
+
This module provides a custom Model implementation that uses Google's
|
|
4
|
+
Code Assist API (cloudcode-pa.googleapis.com) instead of the standard
|
|
5
|
+
Generative Language API. The Code Assist API supports OAuth authentication
|
|
6
|
+
and has a different request/response format.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import logging
|
|
13
|
+
import uuid
|
|
14
|
+
from collections.abc import AsyncIterator
|
|
15
|
+
from contextlib import asynccontextmanager
|
|
16
|
+
from datetime import datetime, timezone
|
|
17
|
+
from typing import Any, Dict, Optional
|
|
18
|
+
|
|
19
|
+
import httpx
|
|
20
|
+
from pydantic_ai.messages import (
|
|
21
|
+
ModelMessage,
|
|
22
|
+
ModelRequest,
|
|
23
|
+
ModelResponse,
|
|
24
|
+
ModelResponsePart,
|
|
25
|
+
SystemPromptPart,
|
|
26
|
+
TextPart,
|
|
27
|
+
ToolCallPart,
|
|
28
|
+
ToolReturnPart,
|
|
29
|
+
UserPromptPart,
|
|
30
|
+
)
|
|
31
|
+
from pydantic_ai.models import Model, ModelRequestParameters
|
|
32
|
+
from pydantic_ai.settings import ModelSettings
|
|
33
|
+
from pydantic_ai.tools import ToolDefinition
|
|
34
|
+
from pydantic_ai.usage import RequestUsage
|
|
35
|
+
|
|
36
|
+
logger = logging.getLogger(__name__)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class GeminiCodeAssistModel(Model):
|
|
40
|
+
"""Model implementation for Google's Code Assist API.
|
|
41
|
+
|
|
42
|
+
This uses the cloudcode-pa.googleapis.com endpoint which accepts OAuth
|
|
43
|
+
tokens and has a wrapped request/response format.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
model_name: str,
|
|
49
|
+
access_token: str,
|
|
50
|
+
project_id: str,
|
|
51
|
+
api_base_url: str = "https://cloudcode-pa.googleapis.com",
|
|
52
|
+
api_version: str = "v1internal",
|
|
53
|
+
):
|
|
54
|
+
self._model_name = model_name
|
|
55
|
+
self.access_token = access_token
|
|
56
|
+
self.project_id = project_id
|
|
57
|
+
self.api_base_url = api_base_url
|
|
58
|
+
self.api_version = api_version
|
|
59
|
+
|
|
60
|
+
def model_name(self) -> str:
|
|
61
|
+
"""Return the model name."""
|
|
62
|
+
return self._model_name
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def system(self) -> str:
|
|
66
|
+
return "google"
|
|
67
|
+
|
|
68
|
+
async def request(
|
|
69
|
+
self,
|
|
70
|
+
messages: list[ModelMessage],
|
|
71
|
+
model_settings: ModelSettings | None,
|
|
72
|
+
model_request_parameters: ModelRequestParameters,
|
|
73
|
+
) -> ModelResponse:
|
|
74
|
+
"""Make a non-streaming request to the Code Assist API."""
|
|
75
|
+
request_body = self._build_request(
|
|
76
|
+
messages, model_settings, model_request_parameters
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
url = f"{self.api_base_url}/{self.api_version}:generateContent"
|
|
80
|
+
headers = self._get_headers()
|
|
81
|
+
|
|
82
|
+
async with httpx.AsyncClient(timeout=180) as client:
|
|
83
|
+
response = await client.post(url, json=request_body, headers=headers)
|
|
84
|
+
|
|
85
|
+
if response.status_code != 200:
|
|
86
|
+
error_text = response.text
|
|
87
|
+
raise RuntimeError(
|
|
88
|
+
f"Code Assist API error {response.status_code}: {error_text}"
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
data = response.json()
|
|
92
|
+
|
|
93
|
+
return self._parse_response(data)
|
|
94
|
+
|
|
95
|
+
@asynccontextmanager
|
|
96
|
+
async def request_stream(
|
|
97
|
+
self,
|
|
98
|
+
messages: list[ModelMessage],
|
|
99
|
+
model_settings: ModelSettings | None,
|
|
100
|
+
model_request_parameters: ModelRequestParameters,
|
|
101
|
+
) -> AsyncIterator[StreamedResponse]:
|
|
102
|
+
"""Make a streaming request to the Code Assist API."""
|
|
103
|
+
request_body = self._build_request(
|
|
104
|
+
messages, model_settings, model_request_parameters
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
url = f"{self.api_base_url}/{self.api_version}:streamGenerateContent?alt=sse"
|
|
108
|
+
headers = self._get_headers()
|
|
109
|
+
|
|
110
|
+
async with httpx.AsyncClient(timeout=180) as client:
|
|
111
|
+
async with client.stream(
|
|
112
|
+
"POST", url, json=request_body, headers=headers
|
|
113
|
+
) as response:
|
|
114
|
+
if response.status_code != 200:
|
|
115
|
+
error_text = await response.aread()
|
|
116
|
+
raise RuntimeError(
|
|
117
|
+
f"Code Assist API error {response.status_code}: {error_text.decode()}"
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
yield StreamedResponse(response, self._model_name)
|
|
121
|
+
|
|
122
|
+
def _get_headers(self) -> Dict[str, str]:
|
|
123
|
+
"""Get HTTP headers for the request."""
|
|
124
|
+
return {
|
|
125
|
+
"Authorization": f"Bearer {self.access_token}",
|
|
126
|
+
"Content-Type": "application/json",
|
|
127
|
+
"Accept": "application/json",
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
def _build_request(
|
|
131
|
+
self,
|
|
132
|
+
messages: list[ModelMessage],
|
|
133
|
+
model_settings: ModelSettings | None,
|
|
134
|
+
model_request_parameters: ModelRequestParameters,
|
|
135
|
+
) -> Dict[str, Any]:
|
|
136
|
+
"""Build the Code Assist API request body."""
|
|
137
|
+
contents = []
|
|
138
|
+
system_instruction = None
|
|
139
|
+
|
|
140
|
+
for msg in messages:
|
|
141
|
+
if isinstance(msg, ModelRequest):
|
|
142
|
+
for part in msg.parts:
|
|
143
|
+
if isinstance(part, SystemPromptPart):
|
|
144
|
+
# Collect system prompt
|
|
145
|
+
if system_instruction is None:
|
|
146
|
+
system_instruction = {
|
|
147
|
+
"role": "user",
|
|
148
|
+
"parts": [{"text": part.content}],
|
|
149
|
+
}
|
|
150
|
+
else:
|
|
151
|
+
system_instruction["parts"].append({"text": part.content})
|
|
152
|
+
elif isinstance(part, UserPromptPart):
|
|
153
|
+
contents.append(
|
|
154
|
+
{
|
|
155
|
+
"role": "user",
|
|
156
|
+
"parts": [{"text": part.content}],
|
|
157
|
+
}
|
|
158
|
+
)
|
|
159
|
+
elif isinstance(part, ToolReturnPart):
|
|
160
|
+
# Serialize content to string if it's not already
|
|
161
|
+
content = part.content
|
|
162
|
+
if not isinstance(content, (str, int, float, bool, type(None))):
|
|
163
|
+
try:
|
|
164
|
+
content = json.dumps(content, default=str)
|
|
165
|
+
except (TypeError, ValueError):
|
|
166
|
+
content = str(content)
|
|
167
|
+
contents.append(
|
|
168
|
+
{
|
|
169
|
+
"role": "user",
|
|
170
|
+
"parts": [
|
|
171
|
+
{
|
|
172
|
+
"functionResponse": {
|
|
173
|
+
"name": part.tool_name,
|
|
174
|
+
"response": {"result": content},
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
],
|
|
178
|
+
}
|
|
179
|
+
)
|
|
180
|
+
elif isinstance(msg, ModelResponse):
|
|
181
|
+
parts = []
|
|
182
|
+
first_func_call = True
|
|
183
|
+
for part in msg.parts:
|
|
184
|
+
if isinstance(part, TextPart):
|
|
185
|
+
parts.append({"text": part.content})
|
|
186
|
+
elif isinstance(part, ToolCallPart):
|
|
187
|
+
func_call_part = {
|
|
188
|
+
"functionCall": {
|
|
189
|
+
"name": part.tool_name,
|
|
190
|
+
"args": part.args_as_dict(),
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
# Code Assist API requires thoughtSignature on function calls
|
|
194
|
+
# Use synthetic signature to skip validation
|
|
195
|
+
if first_func_call:
|
|
196
|
+
func_call_part["thoughtSignature"] = (
|
|
197
|
+
"skip_thought_signature_validator"
|
|
198
|
+
)
|
|
199
|
+
first_func_call = False
|
|
200
|
+
parts.append(func_call_part)
|
|
201
|
+
if parts:
|
|
202
|
+
contents.append({"role": "model", "parts": parts})
|
|
203
|
+
|
|
204
|
+
# Build the inner request (Vertex-style format)
|
|
205
|
+
inner_request: Dict[str, Any] = {
|
|
206
|
+
"contents": contents,
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if system_instruction:
|
|
210
|
+
inner_request["systemInstruction"] = system_instruction
|
|
211
|
+
|
|
212
|
+
# Add tools if available
|
|
213
|
+
if model_request_parameters.function_tools:
|
|
214
|
+
inner_request["tools"] = [
|
|
215
|
+
self._build_tools(model_request_parameters.function_tools)
|
|
216
|
+
]
|
|
217
|
+
|
|
218
|
+
# Add generation config
|
|
219
|
+
generation_config = self._build_generation_config(model_settings)
|
|
220
|
+
if generation_config:
|
|
221
|
+
inner_request["generationConfig"] = generation_config
|
|
222
|
+
|
|
223
|
+
# Wrap in Code Assist format
|
|
224
|
+
return {
|
|
225
|
+
"model": self._model_name,
|
|
226
|
+
"project": self.project_id,
|
|
227
|
+
"user_prompt_id": str(uuid.uuid4()),
|
|
228
|
+
"request": inner_request,
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
def _build_tools(self, tools: list[ToolDefinition]) -> Dict[str, Any]:
|
|
232
|
+
"""Build tool definitions for the API."""
|
|
233
|
+
function_declarations = []
|
|
234
|
+
|
|
235
|
+
for tool in tools:
|
|
236
|
+
func_decl: Dict[str, Any] = {
|
|
237
|
+
"name": tool.name,
|
|
238
|
+
"description": tool.description or "",
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if tool.parameters_json_schema:
|
|
242
|
+
func_decl["parametersJsonSchema"] = tool.parameters_json_schema
|
|
243
|
+
|
|
244
|
+
function_declarations.append(func_decl)
|
|
245
|
+
|
|
246
|
+
return {"functionDeclarations": function_declarations}
|
|
247
|
+
|
|
248
|
+
def _build_generation_config(
|
|
249
|
+
self, model_settings: ModelSettings | None
|
|
250
|
+
) -> Optional[Dict[str, Any]]:
|
|
251
|
+
"""Build generation config from model settings."""
|
|
252
|
+
if not model_settings:
|
|
253
|
+
return None
|
|
254
|
+
|
|
255
|
+
config: Dict[str, Any] = {}
|
|
256
|
+
|
|
257
|
+
if (
|
|
258
|
+
hasattr(model_settings, "temperature")
|
|
259
|
+
and model_settings.temperature is not None
|
|
260
|
+
):
|
|
261
|
+
config["temperature"] = model_settings.temperature
|
|
262
|
+
|
|
263
|
+
if hasattr(model_settings, "top_p") and model_settings.top_p is not None:
|
|
264
|
+
config["topP"] = model_settings.top_p
|
|
265
|
+
|
|
266
|
+
if (
|
|
267
|
+
hasattr(model_settings, "max_tokens")
|
|
268
|
+
and model_settings.max_tokens is not None
|
|
269
|
+
):
|
|
270
|
+
config["maxOutputTokens"] = model_settings.max_tokens
|
|
271
|
+
|
|
272
|
+
return config if config else None
|
|
273
|
+
|
|
274
|
+
def _parse_response(self, data: Dict[str, Any]) -> ModelResponse:
|
|
275
|
+
"""Parse the Code Assist API response."""
|
|
276
|
+
# Unwrap the Code Assist response format
|
|
277
|
+
inner_response = data.get("response", data)
|
|
278
|
+
|
|
279
|
+
candidates = inner_response.get("candidates", [])
|
|
280
|
+
if not candidates:
|
|
281
|
+
raise RuntimeError("No candidates in response")
|
|
282
|
+
|
|
283
|
+
candidate = candidates[0]
|
|
284
|
+
content = candidate.get("content", {})
|
|
285
|
+
parts = content.get("parts", [])
|
|
286
|
+
|
|
287
|
+
response_parts: list[ModelResponsePart] = []
|
|
288
|
+
|
|
289
|
+
for part in parts:
|
|
290
|
+
if "text" in part:
|
|
291
|
+
response_parts.append(TextPart(content=part["text"]))
|
|
292
|
+
elif "functionCall" in part:
|
|
293
|
+
func_call = part["functionCall"]
|
|
294
|
+
response_parts.append(
|
|
295
|
+
ToolCallPart(
|
|
296
|
+
tool_name=func_call["name"],
|
|
297
|
+
args=func_call.get("args", {}),
|
|
298
|
+
tool_call_id=str(uuid.uuid4()),
|
|
299
|
+
)
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
# Extract usage metadata
|
|
303
|
+
usage_meta = inner_response.get("usageMetadata", {})
|
|
304
|
+
usage = RequestUsage(
|
|
305
|
+
input_tokens=usage_meta.get("promptTokenCount", 0),
|
|
306
|
+
output_tokens=usage_meta.get("candidatesTokenCount", 0),
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
return ModelResponse(
|
|
310
|
+
parts=response_parts, model_name=self._model_name, usage=usage
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
class StreamedResponse:
|
|
315
|
+
"""Handler for streaming responses from Code Assist API."""
|
|
316
|
+
|
|
317
|
+
def __init__(self, response: httpx.Response, model_name: str):
|
|
318
|
+
self._response = response
|
|
319
|
+
self._model_name = model_name
|
|
320
|
+
self._usage: Optional[RequestUsage] = None
|
|
321
|
+
self._timestamp = datetime.now(timezone.utc)
|
|
322
|
+
|
|
323
|
+
def __aiter__(self) -> AsyncIterator[str]:
|
|
324
|
+
return self._iter_chunks()
|
|
325
|
+
|
|
326
|
+
async def _iter_chunks(self) -> AsyncIterator[str]:
|
|
327
|
+
"""Iterate over SSE chunks from the response."""
|
|
328
|
+
async for line in self._response.aiter_lines():
|
|
329
|
+
line = line.strip()
|
|
330
|
+
|
|
331
|
+
if line.startswith("data: "):
|
|
332
|
+
data_str = line[6:]
|
|
333
|
+
if data_str == "[DONE]":
|
|
334
|
+
break
|
|
335
|
+
|
|
336
|
+
try:
|
|
337
|
+
data = json.loads(data_str)
|
|
338
|
+
# Unwrap Code Assist format
|
|
339
|
+
inner = data.get("response", data)
|
|
340
|
+
|
|
341
|
+
# Extract usage if available
|
|
342
|
+
if "usageMetadata" in inner:
|
|
343
|
+
meta = inner["usageMetadata"]
|
|
344
|
+
self._usage = RequestUsage(
|
|
345
|
+
input_tokens=meta.get("promptTokenCount", 0),
|
|
346
|
+
output_tokens=meta.get("candidatesTokenCount", 0),
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
# Extract text from candidates
|
|
350
|
+
for candidate in inner.get("candidates", []):
|
|
351
|
+
content = candidate.get("content", {})
|
|
352
|
+
for part in content.get("parts", []):
|
|
353
|
+
if "text" in part:
|
|
354
|
+
yield part["text"]
|
|
355
|
+
|
|
356
|
+
except json.JSONDecodeError:
|
|
357
|
+
logger.warning("Failed to parse SSE data: %s", data_str)
|
|
358
|
+
continue
|
|
359
|
+
|
|
360
|
+
async def get_response_parts(self) -> list[ModelResponsePart]:
|
|
361
|
+
"""Get all response parts after streaming is complete."""
|
|
362
|
+
text_content = ""
|
|
363
|
+
tool_calls = []
|
|
364
|
+
|
|
365
|
+
async for chunk in self:
|
|
366
|
+
text_content += chunk
|
|
367
|
+
|
|
368
|
+
parts: list[ModelResponsePart] = []
|
|
369
|
+
if text_content:
|
|
370
|
+
parts.append(TextPart(content=text_content))
|
|
371
|
+
parts.extend(tool_calls)
|
|
372
|
+
|
|
373
|
+
return parts
|
|
374
|
+
|
|
375
|
+
def usage(self) -> RequestUsage:
|
|
376
|
+
"""Get usage statistics."""
|
|
377
|
+
return self._usage or RequestUsage()
|
|
378
|
+
|
|
379
|
+
def model_name(self) -> str:
|
|
380
|
+
"""Get the model name."""
|
|
381
|
+
return self._model_name
|
|
382
|
+
|
|
383
|
+
def timestamp(self) -> datetime:
|
|
384
|
+
"""Get the response timestamp."""
|
|
385
|
+
return self._timestamp
|
code_puppy/keymap.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Keymap configuration for code-puppy.
|
|
2
|
+
|
|
3
|
+
This module handles configurable keyboard shortcuts, starting with the
|
|
4
|
+
cancel_agent_key feature that allows users to override Ctrl+C with a
|
|
5
|
+
different key for cancelling agent tasks.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
# Character codes for Ctrl+letter combinations (Ctrl+A = 0x01, Ctrl+Z = 0x1A)
|
|
9
|
+
KEY_CODES: dict[str, str] = {
|
|
10
|
+
"ctrl+a": "\x01",
|
|
11
|
+
"ctrl+b": "\x02",
|
|
12
|
+
"ctrl+c": "\x03",
|
|
13
|
+
"ctrl+d": "\x04",
|
|
14
|
+
"ctrl+e": "\x05",
|
|
15
|
+
"ctrl+f": "\x06",
|
|
16
|
+
"ctrl+g": "\x07",
|
|
17
|
+
"ctrl+h": "\x08",
|
|
18
|
+
"ctrl+i": "\x09",
|
|
19
|
+
"ctrl+j": "\x0a",
|
|
20
|
+
"ctrl+k": "\x0b",
|
|
21
|
+
"ctrl+l": "\x0c",
|
|
22
|
+
"ctrl+m": "\x0d",
|
|
23
|
+
"ctrl+n": "\x0e",
|
|
24
|
+
"ctrl+o": "\x0f",
|
|
25
|
+
"ctrl+p": "\x10",
|
|
26
|
+
"ctrl+q": "\x11",
|
|
27
|
+
"ctrl+r": "\x12",
|
|
28
|
+
"ctrl+s": "\x13",
|
|
29
|
+
"ctrl+t": "\x14",
|
|
30
|
+
"ctrl+u": "\x15",
|
|
31
|
+
"ctrl+v": "\x16",
|
|
32
|
+
"ctrl+w": "\x17",
|
|
33
|
+
"ctrl+x": "\x18",
|
|
34
|
+
"ctrl+y": "\x19",
|
|
35
|
+
"ctrl+z": "\x1a",
|
|
36
|
+
"escape": "\x1b",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
# Valid keys for cancel_agent_key configuration
|
|
40
|
+
# NOTE: "escape" is excluded because it conflicts with ANSI escape sequences
|
|
41
|
+
# (arrow keys, F-keys, etc. all start with \x1b)
|
|
42
|
+
VALID_CANCEL_KEYS: set[str] = {
|
|
43
|
+
"ctrl+c",
|
|
44
|
+
"ctrl+k",
|
|
45
|
+
"ctrl+q",
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
DEFAULT_CANCEL_AGENT_KEY: str = "ctrl+c"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class KeymapError(Exception):
|
|
52
|
+
"""Exception raised for keymap configuration errors."""
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def get_cancel_agent_key() -> str:
|
|
56
|
+
"""Get the configured cancel agent key from config.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
The key name (e.g., "ctrl+c", "ctrl+k") from config,
|
|
60
|
+
or the default if not configured.
|
|
61
|
+
"""
|
|
62
|
+
from code_puppy.config import get_value
|
|
63
|
+
|
|
64
|
+
key = get_value("cancel_agent_key")
|
|
65
|
+
if key is None or key.strip() == "":
|
|
66
|
+
return DEFAULT_CANCEL_AGENT_KEY
|
|
67
|
+
return key.strip().lower()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def validate_cancel_agent_key() -> None:
|
|
71
|
+
"""Validate the configured cancel agent key.
|
|
72
|
+
|
|
73
|
+
Raises:
|
|
74
|
+
KeymapError: If the configured key is invalid.
|
|
75
|
+
"""
|
|
76
|
+
key = get_cancel_agent_key()
|
|
77
|
+
if key not in VALID_CANCEL_KEYS:
|
|
78
|
+
valid_keys_str = ", ".join(sorted(VALID_CANCEL_KEYS))
|
|
79
|
+
raise KeymapError(
|
|
80
|
+
f"Invalid cancel_agent_key '{key}' in puppy.cfg. "
|
|
81
|
+
f"Valid options are: {valid_keys_str}"
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def cancel_agent_uses_signal() -> bool:
|
|
86
|
+
"""Check if the cancel agent key uses SIGINT (Ctrl+C).
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
True if the cancel key is ctrl+c AND we're not on Windows
|
|
90
|
+
(uses SIGINT handler), False if it uses keyboard listener approach.
|
|
91
|
+
"""
|
|
92
|
+
import sys
|
|
93
|
+
|
|
94
|
+
# On Windows, always use keyboard listener - SIGINT is unreliable
|
|
95
|
+
if sys.platform == "win32":
|
|
96
|
+
return False
|
|
97
|
+
|
|
98
|
+
return get_cancel_agent_key() == "ctrl+c"
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def get_cancel_agent_char_code() -> str:
|
|
102
|
+
"""Get the character code for the cancel agent key.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
The character code (e.g., "\x0b" for ctrl+k).
|
|
106
|
+
|
|
107
|
+
Raises:
|
|
108
|
+
KeymapError: If the key is not found in KEY_CODES.
|
|
109
|
+
"""
|
|
110
|
+
key = get_cancel_agent_key()
|
|
111
|
+
if key not in KEY_CODES:
|
|
112
|
+
raise KeymapError(f"Unknown key '{key}' - no character code mapping found.")
|
|
113
|
+
return KEY_CODES[key]
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def get_cancel_agent_display_name() -> str:
|
|
117
|
+
"""Get a human-readable display name for the cancel agent key.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
A formatted display name like "Ctrl+K".
|
|
121
|
+
"""
|
|
122
|
+
key = get_cancel_agent_key()
|
|
123
|
+
if key.startswith("ctrl+"):
|
|
124
|
+
letter = key.split("+")[1].upper()
|
|
125
|
+
return f"Ctrl+{letter}"
|
|
126
|
+
return key.upper()
|