code-puppy 0.0.97__py3-none-any.whl → 0.0.118__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 +2 -5
- code_puppy/__main__.py +10 -0
- code_puppy/agent.py +125 -40
- code_puppy/agent_prompts.py +30 -24
- code_puppy/callbacks.py +152 -0
- code_puppy/command_line/command_handler.py +359 -0
- code_puppy/command_line/load_context_completion.py +59 -0
- code_puppy/command_line/model_picker_completion.py +14 -21
- code_puppy/command_line/motd.py +44 -28
- code_puppy/command_line/prompt_toolkit_completion.py +42 -23
- code_puppy/config.py +266 -26
- code_puppy/http_utils.py +122 -0
- code_puppy/main.py +570 -383
- code_puppy/message_history_processor.py +195 -104
- code_puppy/messaging/__init__.py +46 -0
- code_puppy/messaging/message_queue.py +288 -0
- code_puppy/messaging/queue_console.py +293 -0
- code_puppy/messaging/renderers.py +305 -0
- code_puppy/messaging/spinner/__init__.py +55 -0
- code_puppy/messaging/spinner/console_spinner.py +200 -0
- code_puppy/messaging/spinner/spinner_base.py +66 -0
- code_puppy/messaging/spinner/textual_spinner.py +97 -0
- code_puppy/model_factory.py +73 -105
- code_puppy/plugins/__init__.py +32 -0
- code_puppy/reopenable_async_client.py +225 -0
- code_puppy/state_management.py +60 -21
- code_puppy/summarization_agent.py +56 -35
- code_puppy/token_utils.py +7 -9
- code_puppy/tools/__init__.py +1 -4
- code_puppy/tools/command_runner.py +187 -32
- code_puppy/tools/common.py +44 -35
- code_puppy/tools/file_modifications.py +335 -118
- code_puppy/tools/file_operations.py +368 -95
- code_puppy/tools/token_check.py +27 -11
- code_puppy/tools/tools_content.py +53 -0
- code_puppy/tui/__init__.py +10 -0
- code_puppy/tui/app.py +1050 -0
- code_puppy/tui/components/__init__.py +21 -0
- code_puppy/tui/components/chat_view.py +512 -0
- code_puppy/tui/components/command_history_modal.py +218 -0
- code_puppy/tui/components/copy_button.py +139 -0
- code_puppy/tui/components/custom_widgets.py +58 -0
- code_puppy/tui/components/input_area.py +167 -0
- code_puppy/tui/components/sidebar.py +309 -0
- code_puppy/tui/components/status_bar.py +182 -0
- code_puppy/tui/messages.py +27 -0
- code_puppy/tui/models/__init__.py +8 -0
- code_puppy/tui/models/chat_message.py +25 -0
- code_puppy/tui/models/command_history.py +89 -0
- code_puppy/tui/models/enums.py +24 -0
- code_puppy/tui/screens/__init__.py +13 -0
- code_puppy/tui/screens/help.py +130 -0
- code_puppy/tui/screens/settings.py +256 -0
- code_puppy/tui/screens/tools.py +74 -0
- code_puppy/tui/tests/__init__.py +1 -0
- code_puppy/tui/tests/test_chat_message.py +28 -0
- code_puppy/tui/tests/test_chat_view.py +88 -0
- code_puppy/tui/tests/test_command_history.py +89 -0
- code_puppy/tui/tests/test_copy_button.py +191 -0
- code_puppy/tui/tests/test_custom_widgets.py +27 -0
- code_puppy/tui/tests/test_disclaimer.py +27 -0
- code_puppy/tui/tests/test_enums.py +15 -0
- code_puppy/tui/tests/test_file_browser.py +60 -0
- code_puppy/tui/tests/test_help.py +38 -0
- code_puppy/tui/tests/test_history_file_reader.py +107 -0
- code_puppy/tui/tests/test_input_area.py +33 -0
- code_puppy/tui/tests/test_settings.py +44 -0
- code_puppy/tui/tests/test_sidebar.py +33 -0
- code_puppy/tui/tests/test_sidebar_history.py +153 -0
- code_puppy/tui/tests/test_sidebar_history_navigation.py +132 -0
- code_puppy/tui/tests/test_status_bar.py +54 -0
- code_puppy/tui/tests/test_timestamped_history.py +52 -0
- code_puppy/tui/tests/test_tools.py +82 -0
- code_puppy/version_checker.py +26 -3
- {code_puppy-0.0.97.dist-info → code_puppy-0.0.118.dist-info}/METADATA +9 -2
- code_puppy-0.0.118.dist-info/RECORD +86 -0
- code_puppy-0.0.97.dist-info/RECORD +0 -32
- {code_puppy-0.0.97.data → code_puppy-0.0.118.data}/data/code_puppy/models.json +0 -0
- {code_puppy-0.0.97.dist-info → code_puppy-0.0.118.dist-info}/WHEEL +0 -0
- {code_puppy-0.0.97.dist-info → code_puppy-0.0.118.dist-info}/entry_points.txt +0 -0
- {code_puppy-0.0.97.dist-info → code_puppy-0.0.118.dist-info}/licenses/LICENSE +0 -0
code_puppy/__init__.py
CHANGED
code_puppy/__main__.py
ADDED
code_puppy/agent.py
CHANGED
|
@@ -1,24 +1,29 @@
|
|
|
1
|
-
import os
|
|
2
1
|
from pathlib import Path
|
|
2
|
+
from typing import Dict, Optional
|
|
3
3
|
|
|
4
|
-
import pydantic
|
|
5
4
|
from pydantic_ai import Agent
|
|
6
|
-
from pydantic_ai.mcp import MCPServerSSE
|
|
5
|
+
from pydantic_ai.mcp import MCPServerSSE, MCPServerStdio, MCPServerStreamableHTTP
|
|
6
|
+
from pydantic_ai.settings import ModelSettings
|
|
7
|
+
from pydantic_ai.usage import UsageLimits
|
|
7
8
|
|
|
8
9
|
from code_puppy.agent_prompts import get_system_prompt
|
|
10
|
+
from code_puppy.http_utils import (
|
|
11
|
+
create_reopenable_async_client,
|
|
12
|
+
resolve_env_var_in_header,
|
|
13
|
+
)
|
|
14
|
+
from code_puppy.message_history_processor import (
|
|
15
|
+
get_model_context_length,
|
|
16
|
+
message_history_accumulator,
|
|
17
|
+
)
|
|
18
|
+
from code_puppy.messaging.message_queue import (
|
|
19
|
+
emit_error,
|
|
20
|
+
emit_info,
|
|
21
|
+
emit_system_message,
|
|
22
|
+
)
|
|
9
23
|
from code_puppy.model_factory import ModelFactory
|
|
10
|
-
from code_puppy.state_management import message_history_accumulator
|
|
11
24
|
from code_puppy.tools import register_all_tools
|
|
12
25
|
from code_puppy.tools.common import console
|
|
13
26
|
|
|
14
|
-
# Environment variables used in this module:
|
|
15
|
-
# - MODELS_JSON_PATH: Optional path to a custom models.json configuration file.
|
|
16
|
-
# If not set, uses the default file in the package directory.
|
|
17
|
-
# - MODEL_NAME: The model to use for code generation. Defaults to "gpt-4o".
|
|
18
|
-
# Must match a key in the models.json configuration.
|
|
19
|
-
|
|
20
|
-
MODELS_JSON_PATH = os.environ.get("MODELS_JSON_PATH", None)
|
|
21
|
-
|
|
22
27
|
|
|
23
28
|
def load_puppy_rules():
|
|
24
29
|
global PUPPY_RULES
|
|
@@ -31,59 +36,130 @@ def load_puppy_rules():
|
|
|
31
36
|
|
|
32
37
|
# Load at import
|
|
33
38
|
PUPPY_RULES = load_puppy_rules()
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
class AgentResponse(pydantic.BaseModel):
|
|
37
|
-
"""Represents a response from the agent."""
|
|
38
|
-
|
|
39
|
-
output_message: str = pydantic.Field(
|
|
40
|
-
..., description="The final output message to display to the user"
|
|
41
|
-
)
|
|
42
|
-
awaiting_user_input: bool = pydantic.Field(
|
|
43
|
-
False, description="True if user input is needed to continue the task"
|
|
44
|
-
)
|
|
45
|
-
|
|
46
|
-
|
|
39
|
+
_LAST_MODEL_NAME = None
|
|
47
40
|
_code_generation_agent = None
|
|
48
41
|
|
|
49
42
|
|
|
50
|
-
def _load_mcp_servers():
|
|
51
|
-
from code_puppy.config import load_mcp_server_configs
|
|
43
|
+
def _load_mcp_servers(extra_headers: Optional[Dict[str, str]] = None):
|
|
44
|
+
from code_puppy.config import get_value, load_mcp_server_configs
|
|
45
|
+
|
|
46
|
+
# Check if MCP servers are disabled
|
|
47
|
+
mcp_disabled = get_value("disable_mcp_servers")
|
|
48
|
+
if mcp_disabled and str(mcp_disabled).lower() in ("1", "true", "yes", "on"):
|
|
49
|
+
emit_system_message("[dim]MCP servers disabled via config[/dim]")
|
|
50
|
+
return []
|
|
52
51
|
|
|
53
52
|
configs = load_mcp_server_configs()
|
|
53
|
+
if not configs:
|
|
54
|
+
emit_system_message("[dim]No MCP servers configured[/dim]")
|
|
55
|
+
return []
|
|
54
56
|
servers = []
|
|
55
57
|
for name, conf in configs.items():
|
|
58
|
+
server_type = conf.get("type", "sse")
|
|
56
59
|
url = conf.get("url")
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
+
timeout = conf.get("timeout", 30)
|
|
61
|
+
server_headers = {}
|
|
62
|
+
if extra_headers:
|
|
63
|
+
server_headers.update(extra_headers)
|
|
64
|
+
user_headers = conf.get("headers") or {}
|
|
65
|
+
if isinstance(user_headers, dict) and user_headers:
|
|
66
|
+
try:
|
|
67
|
+
user_headers = resolve_env_var_in_header(user_headers)
|
|
68
|
+
except Exception:
|
|
69
|
+
pass
|
|
70
|
+
server_headers.update(user_headers)
|
|
71
|
+
http_client = None
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
if server_type == "http" and url:
|
|
75
|
+
emit_system_message(
|
|
76
|
+
f"Registering MCP Server (HTTP) - {url} (timeout: {timeout}s, headers: {bool(server_headers)})"
|
|
77
|
+
)
|
|
78
|
+
http_client = create_reopenable_async_client(
|
|
79
|
+
timeout=timeout, headers=server_headers or None, verify=False
|
|
80
|
+
)
|
|
81
|
+
servers.append(
|
|
82
|
+
MCPServerStreamableHTTP(url=url, http_client=http_client)
|
|
83
|
+
)
|
|
84
|
+
elif (
|
|
85
|
+
server_type == "stdio"
|
|
86
|
+
): # Fixed: was "stdios" (plural), should be "stdio" (singular)
|
|
87
|
+
command = conf.get("command")
|
|
88
|
+
args = conf.get("args", [])
|
|
89
|
+
timeout = conf.get(
|
|
90
|
+
"timeout", 30
|
|
91
|
+
) # Default 30 seconds for stdio servers (npm downloads can be slow)
|
|
92
|
+
if command:
|
|
93
|
+
emit_system_message(
|
|
94
|
+
f"Registering MCP Server (Stdio) - {command} {args} (timeout: {timeout}s)"
|
|
95
|
+
)
|
|
96
|
+
servers.append(MCPServerStdio(command, args=args, timeout=timeout))
|
|
97
|
+
else:
|
|
98
|
+
emit_error(f"MCP Server '{name}' missing required 'command' field")
|
|
99
|
+
elif server_type == "sse" and url:
|
|
100
|
+
emit_system_message(
|
|
101
|
+
f"Registering MCP Server (SSE) - {url} (timeout: {timeout}s, headers: {bool(server_headers)})"
|
|
102
|
+
)
|
|
103
|
+
# For SSE, allow long reads; only bound connect timeout
|
|
104
|
+
http_client = create_reopenable_async_client(
|
|
105
|
+
timeout=30, headers=server_headers or None, verify=False
|
|
106
|
+
)
|
|
107
|
+
servers.append(MCPServerSSE(url=url, http_client=http_client))
|
|
108
|
+
else:
|
|
109
|
+
emit_error(
|
|
110
|
+
f"Invalid type '{server_type}' or missing URL for MCP server '{name}'"
|
|
111
|
+
)
|
|
112
|
+
except Exception as e:
|
|
113
|
+
emit_error(f"Failed to register MCP server '{name}': {str(e)}")
|
|
114
|
+
emit_info(f"Skipping server '{name}' and continuing with other servers...")
|
|
115
|
+
# Continue with other servers instead of crashing
|
|
116
|
+
continue
|
|
117
|
+
|
|
118
|
+
if servers:
|
|
119
|
+
emit_system_message(
|
|
120
|
+
f"[green]Successfully registered {len(servers)} MCP server(s)[/green]"
|
|
121
|
+
)
|
|
122
|
+
else:
|
|
123
|
+
emit_system_message(
|
|
124
|
+
"[yellow]No MCP servers were successfully registered[/yellow]"
|
|
125
|
+
)
|
|
126
|
+
|
|
60
127
|
return servers
|
|
61
128
|
|
|
62
129
|
|
|
63
130
|
def reload_code_generation_agent():
|
|
64
131
|
"""Force-reload the agent, usually after a model change."""
|
|
65
132
|
global _code_generation_agent, _LAST_MODEL_NAME
|
|
66
|
-
from code_puppy.config import get_model_name
|
|
133
|
+
from code_puppy.config import clear_model_cache, get_model_name
|
|
134
|
+
|
|
135
|
+
# Clear both ModelFactory cache and config cache when force reloading
|
|
136
|
+
clear_model_cache()
|
|
67
137
|
|
|
68
138
|
model_name = get_model_name()
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if MODELS_JSON_PATH
|
|
73
|
-
else Path(__file__).parent / "models.json"
|
|
74
|
-
)
|
|
75
|
-
model = ModelFactory.get_model(model_name, ModelFactory.load_config(models_path))
|
|
139
|
+
emit_info(f"[bold cyan]Loading Model: {model_name}[/bold cyan]")
|
|
140
|
+
models_config = ModelFactory.load_config()
|
|
141
|
+
model = ModelFactory.get_model(model_name, models_config)
|
|
76
142
|
instructions = get_system_prompt()
|
|
77
143
|
if PUPPY_RULES:
|
|
78
144
|
instructions += f"\n{PUPPY_RULES}"
|
|
79
145
|
|
|
146
|
+
mcp_servers = _load_mcp_servers()
|
|
147
|
+
|
|
148
|
+
# Configure model settings with max_tokens if set
|
|
149
|
+
model_settings_dict = {"seed": 42}
|
|
150
|
+
output_tokens = min(int(0.05 * get_model_context_length()) - 1024, 16384)
|
|
151
|
+
console.print(f"Max output tokens per message: {output_tokens}")
|
|
152
|
+
model_settings_dict["max_tokens"] = output_tokens
|
|
153
|
+
|
|
154
|
+
model_settings = ModelSettings(**model_settings_dict)
|
|
80
155
|
agent = Agent(
|
|
81
156
|
model=model,
|
|
82
157
|
instructions=instructions,
|
|
83
158
|
output_type=str,
|
|
84
159
|
retries=3,
|
|
160
|
+
mcp_servers=mcp_servers,
|
|
85
161
|
history_processors=[message_history_accumulator],
|
|
86
|
-
|
|
162
|
+
model_settings=model_settings,
|
|
87
163
|
)
|
|
88
164
|
register_all_tools(agent)
|
|
89
165
|
_code_generation_agent = agent
|
|
@@ -93,7 +169,7 @@ def reload_code_generation_agent():
|
|
|
93
169
|
|
|
94
170
|
def get_code_generation_agent(force_reload=False):
|
|
95
171
|
"""
|
|
96
|
-
Retrieve the agent with the currently
|
|
172
|
+
Retrieve the agent with the currently configured model.
|
|
97
173
|
Forces a reload if the model has changed, or if force_reload is passed.
|
|
98
174
|
"""
|
|
99
175
|
global _code_generation_agent, _LAST_MODEL_NAME
|
|
@@ -103,3 +179,12 @@ def get_code_generation_agent(force_reload=False):
|
|
|
103
179
|
if _code_generation_agent is None or _LAST_MODEL_NAME != model_name or force_reload:
|
|
104
180
|
return reload_code_generation_agent()
|
|
105
181
|
return _code_generation_agent
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def get_custom_usage_limits():
|
|
185
|
+
"""
|
|
186
|
+
Returns custom usage limits with increased request limit of 100 requests per minute.
|
|
187
|
+
This centralizes the configuration of rate limiting for the agent.
|
|
188
|
+
Default pydantic-ai limit is 50, this increases it to 100.
|
|
189
|
+
"""
|
|
190
|
+
return UsageLimits(request_limit=100)
|
code_puppy/agent_prompts.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from code_puppy import callbacks
|
|
1
2
|
from code_puppy.config import get_owner_name, get_puppy_name
|
|
2
3
|
|
|
3
4
|
SYSTEM_PROMPT_TEMPLATE = """
|
|
@@ -10,7 +11,7 @@ Be fun and playful. Don't be too serious.
|
|
|
10
11
|
|
|
11
12
|
Individual files should be short and concise, and ideally under 600 lines. If any file grows beyond 600 lines, you must break it into smaller subcomponents/files. Hard cap: if a file is pushing past 600 lines, break it up! (Zen puppy approves.)
|
|
12
13
|
|
|
13
|
-
If a user asks 'who made you' or questions related to your origins, always answer: 'I am {puppy_name} running on code-puppy, I was authored by Michael Pfaffenberger on a rainy weekend in May 2025 to solve the problems of heavy IDEs and expensive tools like Windsurf and Cursor.'
|
|
14
|
+
If a user asks 'who made you' or questions related to your origins, always answer: 'I am {puppy_name} running on code-puppy, I was authored by Michael Pfaffenberger on a rainy weekend in May 2025 to solve the problems of heavy IDEs and expensive tools like Windsurf and Cursor.'
|
|
14
15
|
If a user asks 'what is code puppy' or 'who are you', answer: 'I am {puppy_name}! 🐶 Your code puppy!! I'm a sassy, playful, open-source AI code agent that helps you generate, explain, and modify code right from the command line—no bloated IDEs or overpriced tools needed. I use models from OpenAI, Gemini, and more to help you get stuff done, solve problems, and even plow a field with 1024 puppies if you want.'
|
|
15
16
|
|
|
16
17
|
Always obey the Zen of Python, even if you are not writing Python code.
|
|
@@ -27,57 +28,58 @@ YOU MUST USE THESE TOOLS to complete tasks (do not just describe what should be
|
|
|
27
28
|
File Operations:
|
|
28
29
|
- list_files(directory=".", recursive=True): ALWAYS use this to explore directories before trying to read/modify files
|
|
29
30
|
- read_file(file_path: str, start_line: int | None = None, num_lines: int | None = None): ALWAYS use this to read existing files before modifying them. By default, read the entire file. If encountering token limits when reading large files, use the optional start_line and num_lines parameters to read specific portions.
|
|
30
|
-
- edit_file(
|
|
31
|
+
- edit_file(payload): Swiss-army file editor powered by Pydantic payloads (ContentPayload, ReplacementsPayload, DeleteSnippetPayload).
|
|
31
32
|
- delete_file(file_path): Use this to remove files when needed
|
|
32
33
|
- grep(search_string, directory="."): Use this to recursively search for a string across files starting from the specified directory, capping results at 200 matches.
|
|
33
34
|
|
|
34
35
|
Tool Usage Instructions:
|
|
35
36
|
|
|
36
37
|
## edit_file
|
|
37
|
-
This is an all-in-one file-modification tool. It supports the following
|
|
38
|
-
1. {{ "content": "…", "overwrite": true|false }} →
|
|
39
|
-
2. {{ "
|
|
40
|
-
3.
|
|
41
|
-
4. {{ "delete_snippet": "…" }} → Remove a snippet of text from an existing file.
|
|
38
|
+
This is an all-in-one file-modification tool. It supports the following Pydantic Object payload types:
|
|
39
|
+
1. ContentPayload: {{ file_path="example.py", "content": "…", "overwrite": true|false }} → Create or overwrite a file with the provided content.
|
|
40
|
+
2. ReplacementsPayload: {{ file_path="example.py", "replacements": [ {{ "old_str": "…", "new_str": "…" }}, … ] }} → Perform exact text replacements inside an existing file.
|
|
41
|
+
3. DeleteSnippetPayload: {{ file_path="example.py", "delete_snippet": "…" }} → Remove a snippet of text from an existing file.
|
|
42
42
|
|
|
43
43
|
Arguments:
|
|
44
|
-
-
|
|
45
|
-
- diff (required): One of the payloads above (raw string or JSON string).
|
|
44
|
+
- payload (required): One of the Pydantic payload types above.
|
|
46
45
|
|
|
47
46
|
Example (create):
|
|
48
|
-
```
|
|
49
|
-
edit_file("
|
|
47
|
+
```python
|
|
48
|
+
edit_file(payload={{file_path="example.py" "content": "print('hello')\n"}})
|
|
50
49
|
```
|
|
51
50
|
|
|
52
51
|
Example (replacement): -- YOU SHOULD PREFER THIS AS THE PRIMARY WAY TO EDIT FILES.
|
|
53
|
-
```
|
|
52
|
+
```python
|
|
54
53
|
edit_file(
|
|
55
|
-
"
|
|
56
|
-
|
|
54
|
+
payload={{file_path="example.py", "replacements": [{{"old_str": "foo", "new_str": "bar"}}]}}
|
|
55
|
+
)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Example (delete snippet):
|
|
59
|
+
```python
|
|
60
|
+
edit_file(
|
|
61
|
+
payload={{file_path="example.py", "delete_snippet": "# TODO: remove this line"}}
|
|
57
62
|
)
|
|
58
63
|
```
|
|
59
64
|
|
|
60
65
|
NEVER output an entire file – this is very expensive.
|
|
61
66
|
You may not edit file extensions: [.ipynb]
|
|
62
|
-
You should specify the following arguments before the others: [TargetFile]
|
|
63
|
-
|
|
64
|
-
Remember: ONE argument = ONE JSON string.
|
|
65
67
|
|
|
66
68
|
Best-practice guidelines for `edit_file`:
|
|
67
|
-
• Keep each diff small – ideally between 100-300 lines.
|
|
68
|
-
• Apply multiple sequential `edit_file` calls when you need to refactor large files instead of sending one massive diff.
|
|
69
|
-
• Never paste an entire file inside `old_str`; target only the minimal snippet you want changed.
|
|
69
|
+
• Keep each diff small – ideally between 100-300 lines.
|
|
70
|
+
• Apply multiple sequential `edit_file` calls when you need to refactor large files instead of sending one massive diff.
|
|
71
|
+
• Never paste an entire file inside `old_str`; target only the minimal snippet you want changed.
|
|
70
72
|
• If the resulting file would grow beyond 600 lines, split logic into additional files and create them with separate `edit_file` calls.
|
|
71
73
|
|
|
72
74
|
|
|
73
75
|
System Operations:
|
|
74
76
|
- run_shell_command(command, cwd=None, timeout=60): Use this to execute commands, run tests, or start services
|
|
75
77
|
|
|
76
|
-
For running shell commands, in the event that a user asks you to run tests - it is necessary to suppress output, when
|
|
77
|
-
you are running the entire test suite.
|
|
78
|
+
For running shell commands, in the event that a user asks you to run tests - it is necessary to suppress output, when
|
|
79
|
+
you are running the entire test suite.
|
|
78
80
|
so for example:
|
|
79
81
|
instead of `npm run test`
|
|
80
|
-
use `npm run test -- --silent`
|
|
82
|
+
use `npm run test -- --silent`
|
|
81
83
|
This applies for any JS / TS testing, but not for other languages.
|
|
82
84
|
You can safely run pytest without the --silent flag (it doesn't exist anyway).
|
|
83
85
|
|
|
@@ -107,6 +109,10 @@ Return your final response as a string output
|
|
|
107
109
|
|
|
108
110
|
def get_system_prompt():
|
|
109
111
|
"""Returns the main system prompt, populated with current puppy and owner name."""
|
|
110
|
-
|
|
112
|
+
prompt_additions = callbacks.on_load_prompt()
|
|
113
|
+
main_prompt = SYSTEM_PROMPT_TEMPLATE.format(
|
|
111
114
|
puppy_name=get_puppy_name(), owner_name=get_owner_name()
|
|
112
115
|
)
|
|
116
|
+
if len(prompt_additions):
|
|
117
|
+
main_prompt += "\n".join(prompt_additions)
|
|
118
|
+
return main_prompt
|
code_puppy/callbacks.py
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
import traceback
|
|
4
|
+
from typing import Any, Callable, Dict, List, Literal, Optional
|
|
5
|
+
|
|
6
|
+
PhaseType = Literal[
|
|
7
|
+
"startup",
|
|
8
|
+
"shutdown",
|
|
9
|
+
"invoke_agent",
|
|
10
|
+
"agent_exception",
|
|
11
|
+
"version_check",
|
|
12
|
+
"load_model_config",
|
|
13
|
+
"load_prompt",
|
|
14
|
+
]
|
|
15
|
+
CallbackFunc = Callable[..., Any]
|
|
16
|
+
|
|
17
|
+
_callbacks: Dict[PhaseType, List[CallbackFunc]] = {
|
|
18
|
+
"startup": [],
|
|
19
|
+
"shutdown": [],
|
|
20
|
+
"invoke_agent": [],
|
|
21
|
+
"agent_exception": [],
|
|
22
|
+
"version_check": [],
|
|
23
|
+
"load_model_config": [],
|
|
24
|
+
"load_prompt": [],
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def register_callback(phase: PhaseType, func: CallbackFunc) -> None:
|
|
31
|
+
if phase not in _callbacks:
|
|
32
|
+
raise ValueError(
|
|
33
|
+
f"Unsupported phase: {phase}. Supported phases: {list(_callbacks.keys())}"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
if not callable(func):
|
|
37
|
+
raise TypeError(f"Callback must be callable, got {type(func)}")
|
|
38
|
+
|
|
39
|
+
_callbacks[phase].append(func)
|
|
40
|
+
logger.debug(f"Registered async callback {func.__name__} for phase '{phase}'")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def unregister_callback(phase: PhaseType, func: CallbackFunc) -> bool:
|
|
44
|
+
if phase not in _callbacks:
|
|
45
|
+
return False
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
_callbacks[phase].remove(func)
|
|
49
|
+
logger.debug(
|
|
50
|
+
f"Unregistered async callback {func.__name__} from phase '{phase}'"
|
|
51
|
+
)
|
|
52
|
+
return True
|
|
53
|
+
except ValueError:
|
|
54
|
+
return False
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def clear_callbacks(phase: Optional[PhaseType] = None) -> None:
|
|
58
|
+
if phase is None:
|
|
59
|
+
for p in _callbacks:
|
|
60
|
+
_callbacks[p].clear()
|
|
61
|
+
logger.debug("Cleared all async callbacks")
|
|
62
|
+
else:
|
|
63
|
+
if phase in _callbacks:
|
|
64
|
+
_callbacks[phase].clear()
|
|
65
|
+
logger.debug(f"Cleared async callbacks for phase '{phase}'")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def get_callbacks(phase: PhaseType) -> List[CallbackFunc]:
|
|
69
|
+
return _callbacks.get(phase, []).copy()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def count_callbacks(phase: Optional[PhaseType] = None) -> int:
|
|
73
|
+
if phase is None:
|
|
74
|
+
return sum(len(callbacks) for callbacks in _callbacks.values())
|
|
75
|
+
return len(_callbacks.get(phase, []))
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _trigger_callbacks_sync(phase: PhaseType, *args, **kwargs) -> List[Any]:
|
|
79
|
+
callbacks = get_callbacks(phase)
|
|
80
|
+
if not callbacks:
|
|
81
|
+
logger.debug(f"No callbacks registered for phase '{phase}'")
|
|
82
|
+
return []
|
|
83
|
+
|
|
84
|
+
results = []
|
|
85
|
+
for callback in callbacks:
|
|
86
|
+
try:
|
|
87
|
+
result = callback(*args, **kwargs)
|
|
88
|
+
results.append(result)
|
|
89
|
+
logger.debug(f"Successfully executed async callback {callback.__name__}")
|
|
90
|
+
except Exception as e:
|
|
91
|
+
logger.error(
|
|
92
|
+
f"Async callback {callback.__name__} failed in phase '{phase}': {e}\n"
|
|
93
|
+
f"{traceback.format_exc()}"
|
|
94
|
+
)
|
|
95
|
+
results.append(None)
|
|
96
|
+
|
|
97
|
+
return results
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
async def _trigger_callbacks(phase: PhaseType, *args, **kwargs) -> List[Any]:
|
|
101
|
+
callbacks = get_callbacks(phase)
|
|
102
|
+
|
|
103
|
+
if not callbacks:
|
|
104
|
+
logger.debug(f"No callbacks registered for phase '{phase}'")
|
|
105
|
+
return []
|
|
106
|
+
|
|
107
|
+
logger.debug(f"Triggering {len(callbacks)} async callbacks for phase '{phase}'")
|
|
108
|
+
|
|
109
|
+
results = []
|
|
110
|
+
for callback in callbacks:
|
|
111
|
+
try:
|
|
112
|
+
result = callback(*args, **kwargs)
|
|
113
|
+
if asyncio.iscoroutine(result):
|
|
114
|
+
result = await result
|
|
115
|
+
results.append(result)
|
|
116
|
+
logger.debug(f"Successfully executed async callback {callback.__name__}")
|
|
117
|
+
except Exception as e:
|
|
118
|
+
logger.error(
|
|
119
|
+
f"Async callback {callback.__name__} failed in phase '{phase}': {e}\n"
|
|
120
|
+
f"{traceback.format_exc()}"
|
|
121
|
+
)
|
|
122
|
+
results.append(None)
|
|
123
|
+
|
|
124
|
+
return results
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
async def on_startup() -> List[Any]:
|
|
128
|
+
return await _trigger_callbacks("startup")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
async def on_shutdown() -> List[Any]:
|
|
132
|
+
return await _trigger_callbacks("shutdown")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
async def on_invoke_agent(*args, **kwargs) -> List[Any]:
|
|
136
|
+
return await _trigger_callbacks("invoke_agent", *args, **kwargs)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
async def on_agent_exception(exception: Exception, *args, **kwargs) -> List[Any]:
|
|
140
|
+
return await _trigger_callbacks("agent_exception", exception, *args, **kwargs)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
async def on_version_check(*args, **kwargs) -> List[Any]:
|
|
144
|
+
return await _trigger_callbacks("version_check", *args, **kwargs)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def on_load_model_config(*args, **kwargs) -> List[Any]:
|
|
148
|
+
return _trigger_callbacks_sync("load_model_config", *args, **kwargs)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def on_load_prompt():
|
|
152
|
+
return _trigger_callbacks_sync("load_prompt")
|