codepp 0.0.437__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 +10 -0
- code_puppy/__main__.py +10 -0
- code_puppy/agents/__init__.py +31 -0
- code_puppy/agents/agent_c_reviewer.py +155 -0
- code_puppy/agents/agent_code_puppy.py +117 -0
- code_puppy/agents/agent_code_reviewer.py +90 -0
- code_puppy/agents/agent_cpp_reviewer.py +132 -0
- code_puppy/agents/agent_creator_agent.py +638 -0
- code_puppy/agents/agent_golang_reviewer.py +151 -0
- code_puppy/agents/agent_helios.py +124 -0
- code_puppy/agents/agent_javascript_reviewer.py +160 -0
- code_puppy/agents/agent_manager.py +742 -0
- code_puppy/agents/agent_pack_leader.py +385 -0
- code_puppy/agents/agent_planning.py +165 -0
- code_puppy/agents/agent_python_programmer.py +169 -0
- code_puppy/agents/agent_python_reviewer.py +90 -0
- code_puppy/agents/agent_qa_expert.py +163 -0
- code_puppy/agents/agent_qa_kitten.py +208 -0
- code_puppy/agents/agent_scheduler.py +121 -0
- code_puppy/agents/agent_security_auditor.py +181 -0
- code_puppy/agents/agent_terminal_qa.py +323 -0
- code_puppy/agents/agent_typescript_reviewer.py +166 -0
- code_puppy/agents/base_agent.py +2156 -0
- code_puppy/agents/event_stream_handler.py +348 -0
- code_puppy/agents/json_agent.py +202 -0
- code_puppy/agents/pack/__init__.py +34 -0
- code_puppy/agents/pack/bloodhound.py +304 -0
- code_puppy/agents/pack/husky.py +327 -0
- code_puppy/agents/pack/retriever.py +393 -0
- code_puppy/agents/pack/shepherd.py +348 -0
- code_puppy/agents/pack/terrier.py +287 -0
- code_puppy/agents/pack/watchdog.py +367 -0
- code_puppy/agents/prompt_reviewer.py +145 -0
- code_puppy/agents/subagent_stream_handler.py +276 -0
- code_puppy/api/__init__.py +13 -0
- code_puppy/api/app.py +169 -0
- code_puppy/api/main.py +21 -0
- code_puppy/api/pty_manager.py +453 -0
- code_puppy/api/routers/__init__.py +12 -0
- code_puppy/api/routers/agents.py +36 -0
- code_puppy/api/routers/commands.py +217 -0
- code_puppy/api/routers/config.py +75 -0
- code_puppy/api/routers/sessions.py +234 -0
- code_puppy/api/templates/terminal.html +361 -0
- code_puppy/api/websocket.py +154 -0
- code_puppy/callbacks.py +692 -0
- code_puppy/chatgpt_codex_client.py +338 -0
- code_puppy/claude_cache_client.py +672 -0
- code_puppy/cli_runner.py +1073 -0
- code_puppy/command_line/__init__.py +1 -0
- code_puppy/command_line/add_model_menu.py +1092 -0
- code_puppy/command_line/agent_menu.py +662 -0
- code_puppy/command_line/attachments.py +395 -0
- code_puppy/command_line/autosave_menu.py +704 -0
- code_puppy/command_line/clipboard.py +527 -0
- code_puppy/command_line/colors_menu.py +532 -0
- code_puppy/command_line/command_handler.py +293 -0
- code_puppy/command_line/command_registry.py +150 -0
- code_puppy/command_line/config_commands.py +719 -0
- code_puppy/command_line/core_commands.py +867 -0
- code_puppy/command_line/diff_menu.py +865 -0
- code_puppy/command_line/file_path_completion.py +73 -0
- code_puppy/command_line/load_context_completion.py +52 -0
- code_puppy/command_line/mcp/__init__.py +10 -0
- code_puppy/command_line/mcp/base.py +32 -0
- code_puppy/command_line/mcp/catalog_server_installer.py +175 -0
- code_puppy/command_line/mcp/custom_server_form.py +688 -0
- code_puppy/command_line/mcp/custom_server_installer.py +195 -0
- code_puppy/command_line/mcp/edit_command.py +148 -0
- code_puppy/command_line/mcp/handler.py +138 -0
- code_puppy/command_line/mcp/help_command.py +147 -0
- code_puppy/command_line/mcp/install_command.py +214 -0
- code_puppy/command_line/mcp/install_menu.py +705 -0
- code_puppy/command_line/mcp/list_command.py +94 -0
- code_puppy/command_line/mcp/logs_command.py +235 -0
- code_puppy/command_line/mcp/remove_command.py +82 -0
- code_puppy/command_line/mcp/restart_command.py +100 -0
- code_puppy/command_line/mcp/search_command.py +123 -0
- code_puppy/command_line/mcp/start_all_command.py +135 -0
- code_puppy/command_line/mcp/start_command.py +117 -0
- code_puppy/command_line/mcp/status_command.py +184 -0
- code_puppy/command_line/mcp/stop_all_command.py +112 -0
- code_puppy/command_line/mcp/stop_command.py +80 -0
- code_puppy/command_line/mcp/test_command.py +107 -0
- code_puppy/command_line/mcp/utils.py +129 -0
- code_puppy/command_line/mcp/wizard_utils.py +334 -0
- code_puppy/command_line/mcp_completion.py +174 -0
- code_puppy/command_line/model_picker_completion.py +197 -0
- code_puppy/command_line/model_settings_menu.py +932 -0
- code_puppy/command_line/motd.py +96 -0
- code_puppy/command_line/onboarding_slides.py +179 -0
- code_puppy/command_line/onboarding_wizard.py +342 -0
- code_puppy/command_line/pin_command_completion.py +329 -0
- code_puppy/command_line/prompt_toolkit_completion.py +846 -0
- code_puppy/command_line/session_commands.py +302 -0
- code_puppy/command_line/shell_passthrough.py +145 -0
- code_puppy/command_line/skills_completion.py +160 -0
- code_puppy/command_line/uc_menu.py +893 -0
- code_puppy/command_line/utils.py +93 -0
- code_puppy/command_line/wiggum_state.py +78 -0
- code_puppy/config.py +1770 -0
- code_puppy/error_logging.py +134 -0
- code_puppy/gemini_code_assist.py +385 -0
- code_puppy/gemini_model.py +754 -0
- code_puppy/hook_engine/README.md +105 -0
- code_puppy/hook_engine/__init__.py +21 -0
- code_puppy/hook_engine/aliases.py +155 -0
- code_puppy/hook_engine/engine.py +221 -0
- code_puppy/hook_engine/executor.py +296 -0
- code_puppy/hook_engine/matcher.py +156 -0
- code_puppy/hook_engine/models.py +240 -0
- code_puppy/hook_engine/registry.py +106 -0
- code_puppy/hook_engine/validator.py +144 -0
- code_puppy/http_utils.py +361 -0
- code_puppy/keymap.py +128 -0
- code_puppy/main.py +10 -0
- code_puppy/mcp_/__init__.py +66 -0
- code_puppy/mcp_/async_lifecycle.py +286 -0
- code_puppy/mcp_/blocking_startup.py +469 -0
- code_puppy/mcp_/captured_stdio_server.py +275 -0
- code_puppy/mcp_/circuit_breaker.py +290 -0
- code_puppy/mcp_/config_wizard.py +507 -0
- code_puppy/mcp_/dashboard.py +308 -0
- code_puppy/mcp_/error_isolation.py +407 -0
- code_puppy/mcp_/examples/retry_example.py +226 -0
- code_puppy/mcp_/health_monitor.py +589 -0
- code_puppy/mcp_/managed_server.py +428 -0
- code_puppy/mcp_/manager.py +807 -0
- code_puppy/mcp_/mcp_logs.py +224 -0
- code_puppy/mcp_/registry.py +451 -0
- code_puppy/mcp_/retry_manager.py +337 -0
- code_puppy/mcp_/server_registry_catalog.py +1126 -0
- code_puppy/mcp_/status_tracker.py +355 -0
- code_puppy/mcp_/system_tools.py +209 -0
- code_puppy/mcp_prompts/__init__.py +1 -0
- code_puppy/mcp_prompts/hook_creator.py +103 -0
- code_puppy/messaging/__init__.py +255 -0
- code_puppy/messaging/bus.py +613 -0
- code_puppy/messaging/commands.py +167 -0
- code_puppy/messaging/markdown_patches.py +57 -0
- code_puppy/messaging/message_queue.py +361 -0
- code_puppy/messaging/messages.py +569 -0
- code_puppy/messaging/queue_console.py +271 -0
- code_puppy/messaging/renderers.py +311 -0
- code_puppy/messaging/rich_renderer.py +1158 -0
- code_puppy/messaging/spinner/__init__.py +83 -0
- code_puppy/messaging/spinner/console_spinner.py +240 -0
- code_puppy/messaging/spinner/spinner_base.py +95 -0
- code_puppy/messaging/subagent_console.py +460 -0
- code_puppy/model_factory.py +848 -0
- code_puppy/model_switching.py +63 -0
- code_puppy/model_utils.py +168 -0
- code_puppy/models.json +174 -0
- code_puppy/models_dev_api.json +1 -0
- code_puppy/models_dev_parser.py +592 -0
- code_puppy/plugins/__init__.py +186 -0
- code_puppy/plugins/agent_skills/__init__.py +22 -0
- code_puppy/plugins/agent_skills/config.py +175 -0
- code_puppy/plugins/agent_skills/discovery.py +136 -0
- code_puppy/plugins/agent_skills/downloader.py +392 -0
- code_puppy/plugins/agent_skills/installer.py +22 -0
- code_puppy/plugins/agent_skills/metadata.py +219 -0
- code_puppy/plugins/agent_skills/prompt_builder.py +60 -0
- code_puppy/plugins/agent_skills/register_callbacks.py +241 -0
- code_puppy/plugins/agent_skills/remote_catalog.py +322 -0
- code_puppy/plugins/agent_skills/skill_catalog.py +257 -0
- code_puppy/plugins/agent_skills/skills_install_menu.py +664 -0
- code_puppy/plugins/agent_skills/skills_menu.py +781 -0
- code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
- code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
- code_puppy/plugins/antigravity_oauth/antigravity_model.py +706 -0
- code_puppy/plugins/antigravity_oauth/config.py +42 -0
- code_puppy/plugins/antigravity_oauth/constants.py +133 -0
- code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
- code_puppy/plugins/antigravity_oauth/register_callbacks.py +518 -0
- code_puppy/plugins/antigravity_oauth/storage.py +288 -0
- code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
- code_puppy/plugins/antigravity_oauth/token.py +167 -0
- code_puppy/plugins/antigravity_oauth/transport.py +863 -0
- code_puppy/plugins/antigravity_oauth/utils.py +168 -0
- code_puppy/plugins/chatgpt_oauth/__init__.py +8 -0
- code_puppy/plugins/chatgpt_oauth/config.py +52 -0
- code_puppy/plugins/chatgpt_oauth/oauth_flow.py +329 -0
- code_puppy/plugins/chatgpt_oauth/register_callbacks.py +176 -0
- code_puppy/plugins/chatgpt_oauth/test_plugin.py +301 -0
- code_puppy/plugins/chatgpt_oauth/utils.py +523 -0
- code_puppy/plugins/claude_code_hooks/__init__.py +1 -0
- code_puppy/plugins/claude_code_hooks/config.py +137 -0
- code_puppy/plugins/claude_code_hooks/register_callbacks.py +175 -0
- code_puppy/plugins/claude_code_oauth/README.md +167 -0
- code_puppy/plugins/claude_code_oauth/SETUP.md +93 -0
- code_puppy/plugins/claude_code_oauth/__init__.py +25 -0
- code_puppy/plugins/claude_code_oauth/config.py +52 -0
- code_puppy/plugins/claude_code_oauth/register_callbacks.py +453 -0
- code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
- code_puppy/plugins/claude_code_oauth/token_refresh_heartbeat.py +241 -0
- code_puppy/plugins/claude_code_oauth/utils.py +640 -0
- code_puppy/plugins/customizable_commands/__init__.py +0 -0
- code_puppy/plugins/customizable_commands/register_callbacks.py +152 -0
- code_puppy/plugins/example_custom_command/README.md +280 -0
- code_puppy/plugins/example_custom_command/register_callbacks.py +51 -0
- code_puppy/plugins/file_permission_handler/__init__.py +4 -0
- code_puppy/plugins/file_permission_handler/register_callbacks.py +470 -0
- code_puppy/plugins/frontend_emitter/__init__.py +25 -0
- code_puppy/plugins/frontend_emitter/emitter.py +121 -0
- code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
- code_puppy/plugins/hook_creator/__init__.py +1 -0
- code_puppy/plugins/hook_creator/register_callbacks.py +33 -0
- code_puppy/plugins/hook_manager/__init__.py +1 -0
- code_puppy/plugins/hook_manager/config.py +290 -0
- code_puppy/plugins/hook_manager/hooks_menu.py +564 -0
- code_puppy/plugins/hook_manager/register_callbacks.py +227 -0
- code_puppy/plugins/oauth_puppy_html.py +228 -0
- code_puppy/plugins/scheduler/__init__.py +1 -0
- code_puppy/plugins/scheduler/register_callbacks.py +88 -0
- code_puppy/plugins/scheduler/scheduler_menu.py +522 -0
- code_puppy/plugins/scheduler/scheduler_wizard.py +341 -0
- code_puppy/plugins/shell_safety/__init__.py +6 -0
- code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
- code_puppy/plugins/shell_safety/command_cache.py +156 -0
- code_puppy/plugins/shell_safety/register_callbacks.py +202 -0
- code_puppy/plugins/synthetic_status/__init__.py +1 -0
- code_puppy/plugins/synthetic_status/register_callbacks.py +132 -0
- code_puppy/plugins/synthetic_status/status_api.py +147 -0
- code_puppy/plugins/universal_constructor/__init__.py +13 -0
- code_puppy/plugins/universal_constructor/models.py +138 -0
- code_puppy/plugins/universal_constructor/register_callbacks.py +47 -0
- code_puppy/plugins/universal_constructor/registry.py +302 -0
- code_puppy/plugins/universal_constructor/sandbox.py +584 -0
- code_puppy/prompts/antigravity_system_prompt.md +1 -0
- code_puppy/pydantic_patches.py +356 -0
- code_puppy/reopenable_async_client.py +232 -0
- code_puppy/round_robin_model.py +150 -0
- code_puppy/scheduler/__init__.py +41 -0
- code_puppy/scheduler/__main__.py +9 -0
- code_puppy/scheduler/cli.py +118 -0
- code_puppy/scheduler/config.py +126 -0
- code_puppy/scheduler/daemon.py +280 -0
- code_puppy/scheduler/executor.py +155 -0
- code_puppy/scheduler/platform.py +19 -0
- code_puppy/scheduler/platform_unix.py +22 -0
- code_puppy/scheduler/platform_win.py +32 -0
- code_puppy/session_storage.py +338 -0
- code_puppy/status_display.py +257 -0
- code_puppy/summarization_agent.py +176 -0
- code_puppy/terminal_utils.py +418 -0
- code_puppy/tools/__init__.py +501 -0
- code_puppy/tools/agent_tools.py +603 -0
- code_puppy/tools/ask_user_question/__init__.py +26 -0
- code_puppy/tools/ask_user_question/constants.py +73 -0
- code_puppy/tools/ask_user_question/demo_tui.py +55 -0
- code_puppy/tools/ask_user_question/handler.py +232 -0
- code_puppy/tools/ask_user_question/models.py +304 -0
- code_puppy/tools/ask_user_question/registration.py +26 -0
- code_puppy/tools/ask_user_question/renderers.py +309 -0
- code_puppy/tools/ask_user_question/terminal_ui.py +329 -0
- code_puppy/tools/ask_user_question/theme.py +155 -0
- code_puppy/tools/ask_user_question/tui_loop.py +423 -0
- code_puppy/tools/browser/__init__.py +37 -0
- code_puppy/tools/browser/browser_control.py +289 -0
- code_puppy/tools/browser/browser_interactions.py +545 -0
- code_puppy/tools/browser/browser_locators.py +640 -0
- code_puppy/tools/browser/browser_manager.py +378 -0
- code_puppy/tools/browser/browser_navigation.py +251 -0
- code_puppy/tools/browser/browser_screenshot.py +179 -0
- code_puppy/tools/browser/browser_scripts.py +462 -0
- code_puppy/tools/browser/browser_workflows.py +221 -0
- code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
- code_puppy/tools/browser/terminal_command_tools.py +534 -0
- code_puppy/tools/browser/terminal_screenshot_tools.py +552 -0
- code_puppy/tools/browser/terminal_tools.py +525 -0
- code_puppy/tools/command_runner.py +1346 -0
- code_puppy/tools/common.py +1409 -0
- code_puppy/tools/display.py +84 -0
- code_puppy/tools/file_modifications.py +886 -0
- code_puppy/tools/file_operations.py +802 -0
- code_puppy/tools/scheduler_tools.py +412 -0
- code_puppy/tools/skills_tools.py +244 -0
- code_puppy/tools/subagent_context.py +158 -0
- code_puppy/tools/tools_content.py +51 -0
- code_puppy/tools/universal_constructor.py +889 -0
- code_puppy/uvx_detection.py +242 -0
- code_puppy/version_checker.py +82 -0
- codepp-0.0.437.dist-info/METADATA +766 -0
- codepp-0.0.437.dist-info/RECORD +288 -0
- codepp-0.0.437.dist-info/WHEEL +4 -0
- codepp-0.0.437.dist-info/entry_points.txt +3 -0
- codepp-0.0.437.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
from contextlib import asynccontextmanager, suppress
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any, AsyncIterator, List
|
|
5
|
+
|
|
6
|
+
from pydantic_ai._run_context import RunContext
|
|
7
|
+
from pydantic_ai.models import (
|
|
8
|
+
Model,
|
|
9
|
+
ModelMessage,
|
|
10
|
+
ModelRequestParameters,
|
|
11
|
+
ModelResponse,
|
|
12
|
+
ModelSettings,
|
|
13
|
+
StreamedResponse,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
from opentelemetry.context import get_current_span
|
|
18
|
+
except ImportError:
|
|
19
|
+
# If opentelemetry is not installed, provide a dummy implementation
|
|
20
|
+
def get_current_span():
|
|
21
|
+
class DummySpan:
|
|
22
|
+
def is_recording(self):
|
|
23
|
+
return False
|
|
24
|
+
|
|
25
|
+
def set_attributes(self, attributes):
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
return DummySpan()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(init=False)
|
|
32
|
+
class RoundRobinModel(Model):
|
|
33
|
+
"""A model that cycles through multiple models in a round-robin fashion.
|
|
34
|
+
|
|
35
|
+
This model distributes requests across multiple candidate models to help
|
|
36
|
+
overcome rate limits or distribute load.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
models: List[Model]
|
|
40
|
+
_current_index: int = field(default=0, repr=False)
|
|
41
|
+
_model_name: str = field(repr=False)
|
|
42
|
+
_rotate_every: int = field(default=1, repr=False)
|
|
43
|
+
_request_count: int = field(default=0, repr=False)
|
|
44
|
+
_lock: threading.Lock = field(default_factory=threading.Lock, repr=False)
|
|
45
|
+
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
*models: Model,
|
|
49
|
+
rotate_every: int = 1,
|
|
50
|
+
settings: ModelSettings | None = None,
|
|
51
|
+
):
|
|
52
|
+
"""Initialize a round-robin model instance.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
models: The model instances to cycle through.
|
|
56
|
+
rotate_every: Number of requests before rotating to the next model (default: 1).
|
|
57
|
+
settings: Model settings that will be used as defaults for this model.
|
|
58
|
+
"""
|
|
59
|
+
super().__init__(settings=settings)
|
|
60
|
+
if not models:
|
|
61
|
+
raise ValueError("At least one model must be provided")
|
|
62
|
+
if rotate_every < 1:
|
|
63
|
+
raise ValueError("rotate_every must be at least 1")
|
|
64
|
+
self.models = list(models)
|
|
65
|
+
self._current_index = 0
|
|
66
|
+
self._request_count = 0
|
|
67
|
+
self._rotate_every = rotate_every
|
|
68
|
+
self._lock = threading.Lock()
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def model_name(self) -> str:
|
|
72
|
+
"""The model name showing this is a round-robin model with its candidates."""
|
|
73
|
+
base_name = f"round_robin:{','.join(model.model_name for model in self.models)}"
|
|
74
|
+
if self._rotate_every != 1:
|
|
75
|
+
return f"{base_name}:rotate_every={self._rotate_every}"
|
|
76
|
+
return base_name
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def system(self) -> str:
|
|
80
|
+
"""System prompt from the current model."""
|
|
81
|
+
return self.models[self._current_index].system
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def base_url(self) -> str | None:
|
|
85
|
+
"""Base URL from the current model."""
|
|
86
|
+
return self.models[self._current_index].base_url
|
|
87
|
+
|
|
88
|
+
def _get_next_model(self) -> Model:
|
|
89
|
+
"""Get the next model in the round-robin sequence and update the index."""
|
|
90
|
+
with self._lock:
|
|
91
|
+
model = self.models[self._current_index]
|
|
92
|
+
self._request_count += 1
|
|
93
|
+
if self._request_count >= self._rotate_every:
|
|
94
|
+
self._current_index = (self._current_index + 1) % len(self.models)
|
|
95
|
+
self._request_count = 0
|
|
96
|
+
return model
|
|
97
|
+
|
|
98
|
+
async def request(
|
|
99
|
+
self,
|
|
100
|
+
messages: list[ModelMessage],
|
|
101
|
+
model_settings: ModelSettings | None,
|
|
102
|
+
model_request_parameters: ModelRequestParameters,
|
|
103
|
+
) -> ModelResponse:
|
|
104
|
+
"""Make a request using the next model in the round-robin sequence."""
|
|
105
|
+
current_model = self._get_next_model()
|
|
106
|
+
# Use prepare_request to merge settings and customize parameters
|
|
107
|
+
merged_settings, prepared_params = current_model.prepare_request(
|
|
108
|
+
model_settings, model_request_parameters
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
response = await current_model.request(
|
|
113
|
+
messages, merged_settings, prepared_params
|
|
114
|
+
)
|
|
115
|
+
self._set_span_attributes(current_model)
|
|
116
|
+
return response
|
|
117
|
+
except Exception:
|
|
118
|
+
# Unlike FallbackModel, we don't try other models here
|
|
119
|
+
# The round-robin strategy is about distribution, not failover
|
|
120
|
+
raise
|
|
121
|
+
|
|
122
|
+
@asynccontextmanager
|
|
123
|
+
async def request_stream(
|
|
124
|
+
self,
|
|
125
|
+
messages: list[ModelMessage],
|
|
126
|
+
model_settings: ModelSettings | None,
|
|
127
|
+
model_request_parameters: ModelRequestParameters,
|
|
128
|
+
run_context: RunContext[Any] | None = None,
|
|
129
|
+
) -> AsyncIterator[StreamedResponse]:
|
|
130
|
+
"""Make a streaming request using the next model in the round-robin sequence."""
|
|
131
|
+
current_model = self._get_next_model()
|
|
132
|
+
# Use prepare_request to merge settings and customize parameters
|
|
133
|
+
merged_settings, prepared_params = current_model.prepare_request(
|
|
134
|
+
model_settings, model_request_parameters
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
async with current_model.request_stream(
|
|
138
|
+
messages, merged_settings, prepared_params, run_context
|
|
139
|
+
) as response:
|
|
140
|
+
self._set_span_attributes(current_model)
|
|
141
|
+
yield response
|
|
142
|
+
|
|
143
|
+
def _set_span_attributes(self, model: Model):
|
|
144
|
+
"""Set span attributes for observability."""
|
|
145
|
+
with suppress(Exception):
|
|
146
|
+
span = get_current_span()
|
|
147
|
+
if span.is_recording():
|
|
148
|
+
attributes = getattr(span, "attributes", {})
|
|
149
|
+
if attributes.get("gen_ai.request.model") == self.model_name:
|
|
150
|
+
span.set_attributes(model.model_attributes(model))
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Code Puppy Scheduler - Run scheduled prompts automatically.
|
|
2
|
+
|
|
3
|
+
This module provides a cross-platform scheduler daemon that executes
|
|
4
|
+
Code Puppy prompts on configurable schedules (intervals, cron expressions).
|
|
5
|
+
|
|
6
|
+
Components:
|
|
7
|
+
- config: Task definitions and JSON persistence
|
|
8
|
+
- daemon: Background scheduler process
|
|
9
|
+
- executor: Task execution logic
|
|
10
|
+
- platform: Cross-platform daemon management
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from code_puppy.scheduler.config import (
|
|
14
|
+
SCHEDULER_LOG_DIR,
|
|
15
|
+
SCHEDULER_PID_FILE,
|
|
16
|
+
SCHEDULES_FILE,
|
|
17
|
+
ScheduledTask,
|
|
18
|
+
add_task,
|
|
19
|
+
delete_task,
|
|
20
|
+
get_task,
|
|
21
|
+
load_tasks,
|
|
22
|
+
save_tasks,
|
|
23
|
+
toggle_task,
|
|
24
|
+
update_task,
|
|
25
|
+
)
|
|
26
|
+
from code_puppy.scheduler.daemon import start_daemon_background
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"ScheduledTask",
|
|
30
|
+
"load_tasks",
|
|
31
|
+
"save_tasks",
|
|
32
|
+
"add_task",
|
|
33
|
+
"update_task",
|
|
34
|
+
"delete_task",
|
|
35
|
+
"get_task",
|
|
36
|
+
"toggle_task",
|
|
37
|
+
"start_daemon_background",
|
|
38
|
+
"SCHEDULES_FILE",
|
|
39
|
+
"SCHEDULER_PID_FILE",
|
|
40
|
+
"SCHEDULER_LOG_DIR",
|
|
41
|
+
]
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""CLI subcommands for the scheduler.
|
|
2
|
+
|
|
3
|
+
Handles command-line operations like starting/stopping the daemon,
|
|
4
|
+
listing tasks, and running tasks immediately.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from code_puppy.messaging import emit_error, emit_info, emit_success, emit_warning
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def handle_scheduler_start() -> bool:
|
|
11
|
+
"""Start the scheduler daemon in background."""
|
|
12
|
+
from code_puppy.scheduler.daemon import get_daemon_pid, start_daemon_background
|
|
13
|
+
|
|
14
|
+
pid = get_daemon_pid()
|
|
15
|
+
if pid:
|
|
16
|
+
emit_warning(f"Scheduler daemon already running (PID {pid})")
|
|
17
|
+
return True
|
|
18
|
+
|
|
19
|
+
emit_info("Starting scheduler daemon...")
|
|
20
|
+
|
|
21
|
+
if start_daemon_background():
|
|
22
|
+
pid = get_daemon_pid()
|
|
23
|
+
emit_success(f"Scheduler daemon started (PID {pid})")
|
|
24
|
+
return True
|
|
25
|
+
else:
|
|
26
|
+
emit_error("Failed to start scheduler daemon")
|
|
27
|
+
return False
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def handle_scheduler_stop() -> bool:
|
|
31
|
+
"""Stop the scheduler daemon."""
|
|
32
|
+
from code_puppy.scheduler.daemon import get_daemon_pid, stop_daemon
|
|
33
|
+
|
|
34
|
+
pid = get_daemon_pid()
|
|
35
|
+
if not pid:
|
|
36
|
+
emit_info("Scheduler daemon is not running")
|
|
37
|
+
return True
|
|
38
|
+
|
|
39
|
+
emit_info(f"Stopping scheduler daemon (PID {pid})...")
|
|
40
|
+
|
|
41
|
+
if stop_daemon():
|
|
42
|
+
emit_success("Scheduler daemon stopped")
|
|
43
|
+
return True
|
|
44
|
+
else:
|
|
45
|
+
emit_error("Failed to stop scheduler daemon")
|
|
46
|
+
return False
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def handle_scheduler_status() -> bool:
|
|
50
|
+
"""Show scheduler daemon status."""
|
|
51
|
+
from code_puppy.scheduler.config import load_tasks
|
|
52
|
+
from code_puppy.scheduler.daemon import get_daemon_pid
|
|
53
|
+
|
|
54
|
+
pid = get_daemon_pid()
|
|
55
|
+
if pid:
|
|
56
|
+
emit_success(f"🐕 Scheduler daemon: RUNNING (PID {pid})")
|
|
57
|
+
else:
|
|
58
|
+
emit_warning("🐕 Scheduler daemon: STOPPED")
|
|
59
|
+
|
|
60
|
+
tasks = load_tasks()
|
|
61
|
+
enabled_count = sum(1 for t in tasks if t.enabled)
|
|
62
|
+
|
|
63
|
+
emit_info(f"\n📅 Scheduled tasks: {len(tasks)} total, {enabled_count} enabled")
|
|
64
|
+
|
|
65
|
+
if tasks:
|
|
66
|
+
emit_info("\nTasks:")
|
|
67
|
+
for task in tasks:
|
|
68
|
+
status_icon = "🟢" if task.enabled else "🔴"
|
|
69
|
+
last_run = task.last_run[:19] if task.last_run else "never"
|
|
70
|
+
emit_info(
|
|
71
|
+
f" {status_icon} {task.name} ({task.schedule_type}: {task.schedule_value})"
|
|
72
|
+
)
|
|
73
|
+
emit_info(
|
|
74
|
+
f" Last run: {last_run}, Status: {task.last_status or 'pending'}"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
return True
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def handle_scheduler_list() -> bool:
|
|
81
|
+
"""List all scheduled tasks."""
|
|
82
|
+
from code_puppy.scheduler.config import load_tasks
|
|
83
|
+
|
|
84
|
+
tasks = load_tasks()
|
|
85
|
+
|
|
86
|
+
if not tasks:
|
|
87
|
+
emit_info("No scheduled tasks configured.")
|
|
88
|
+
emit_info("Use '/scheduler' to create one.")
|
|
89
|
+
return True
|
|
90
|
+
|
|
91
|
+
emit_info(f"📅 Scheduled Tasks ({len(tasks)}):\n")
|
|
92
|
+
|
|
93
|
+
for task in tasks:
|
|
94
|
+
status = "🟢 enabled" if task.enabled else "🔴 disabled"
|
|
95
|
+
emit_info(f" [{task.id}] {task.name}")
|
|
96
|
+
emit_info(f" Status: {status}")
|
|
97
|
+
emit_info(f" Schedule: {task.schedule_type} ({task.schedule_value})")
|
|
98
|
+
emit_info(f" Agent: {task.agent}, Model: {task.model or 'default'}")
|
|
99
|
+
if task.last_run:
|
|
100
|
+
emit_info(f" Last run: {task.last_run[:19]} ({task.last_status})")
|
|
101
|
+
emit_info("")
|
|
102
|
+
|
|
103
|
+
return True
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def handle_scheduler_run(task_id: str) -> bool:
|
|
107
|
+
"""Run a specific task immediately."""
|
|
108
|
+
from code_puppy.scheduler.executor import run_task_by_id
|
|
109
|
+
|
|
110
|
+
emit_info(f"Running task {task_id}...")
|
|
111
|
+
success, message = run_task_by_id(task_id)
|
|
112
|
+
|
|
113
|
+
if success:
|
|
114
|
+
emit_success(message)
|
|
115
|
+
else:
|
|
116
|
+
emit_error(message)
|
|
117
|
+
|
|
118
|
+
return success
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Scheduler configuration and task management.
|
|
2
|
+
|
|
3
|
+
Handles ScheduledTask dataclass definition and JSON persistence
|
|
4
|
+
for scheduled Code Puppy tasks.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import uuid
|
|
10
|
+
from dataclasses import asdict, dataclass, field
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import List, Optional
|
|
14
|
+
|
|
15
|
+
# Import from existing config
|
|
16
|
+
from code_puppy.config import DATA_DIR
|
|
17
|
+
|
|
18
|
+
SCHEDULES_FILE = os.path.join(DATA_DIR, "scheduled_tasks.json")
|
|
19
|
+
SCHEDULER_PID_FILE = os.path.join(DATA_DIR, "scheduler.pid")
|
|
20
|
+
SCHEDULER_LOG_DIR = os.path.join(DATA_DIR, "scheduler_logs")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class ScheduledTask:
|
|
25
|
+
"""A scheduled Code Puppy task."""
|
|
26
|
+
|
|
27
|
+
id: str = field(default_factory=lambda: str(uuid.uuid4())[:8])
|
|
28
|
+
name: str = ""
|
|
29
|
+
prompt: str = ""
|
|
30
|
+
agent: str = "code-puppy"
|
|
31
|
+
model: str = "" # Uses default if empty
|
|
32
|
+
schedule_type: str = "interval" # "interval", "cron", "daily", "hourly"
|
|
33
|
+
schedule_value: str = "1h" # e.g., "30m", "1h", "0 9 * * *" for cron
|
|
34
|
+
working_directory: str = "."
|
|
35
|
+
log_file: str = "" # Auto-generated if empty
|
|
36
|
+
enabled: bool = True
|
|
37
|
+
created_at: str = field(default_factory=lambda: datetime.now().isoformat())
|
|
38
|
+
last_run: Optional[str] = None
|
|
39
|
+
last_status: Optional[str] = None # "success", "failed", "running"
|
|
40
|
+
last_exit_code: Optional[int] = None
|
|
41
|
+
|
|
42
|
+
def __post_init__(self):
|
|
43
|
+
if not self.log_file:
|
|
44
|
+
self.log_file = os.path.join(SCHEDULER_LOG_DIR, f"{self.id}.log")
|
|
45
|
+
|
|
46
|
+
def to_dict(self) -> dict:
|
|
47
|
+
return asdict(self)
|
|
48
|
+
|
|
49
|
+
@classmethod
|
|
50
|
+
def from_dict(cls, data: dict) -> "ScheduledTask":
|
|
51
|
+
return cls(**{k: v for k, v in data.items() if k in cls.__dataclass_fields__})
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def ensure_scheduler_dirs() -> None:
|
|
55
|
+
"""Create scheduler directories if they don't exist."""
|
|
56
|
+
os.makedirs(SCHEDULER_LOG_DIR, mode=0o700, exist_ok=True)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def load_tasks() -> List[ScheduledTask]:
|
|
60
|
+
"""Load all scheduled tasks from JSON file."""
|
|
61
|
+
ensure_scheduler_dirs()
|
|
62
|
+
if not os.path.exists(SCHEDULES_FILE):
|
|
63
|
+
return []
|
|
64
|
+
try:
|
|
65
|
+
with open(SCHEDULES_FILE, "r") as f:
|
|
66
|
+
data = json.load(f)
|
|
67
|
+
return [ScheduledTask.from_dict(t) for t in data]
|
|
68
|
+
except (json.JSONDecodeError, IOError):
|
|
69
|
+
return []
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def save_tasks(tasks: List[ScheduledTask]) -> None:
|
|
73
|
+
"""Save all scheduled tasks to JSON file."""
|
|
74
|
+
ensure_scheduler_dirs()
|
|
75
|
+
temp_path = Path(SCHEDULES_FILE).with_suffix(".tmp")
|
|
76
|
+
with open(temp_path, "w", encoding="utf-8") as f:
|
|
77
|
+
json.dump([t.to_dict() for t in tasks], f, indent=2, ensure_ascii=False)
|
|
78
|
+
temp_path.replace(SCHEDULES_FILE)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def add_task(task: ScheduledTask) -> None:
|
|
82
|
+
"""Add a new scheduled task."""
|
|
83
|
+
tasks = load_tasks()
|
|
84
|
+
tasks.append(task)
|
|
85
|
+
save_tasks(tasks)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def update_task(task: ScheduledTask) -> bool:
|
|
89
|
+
"""Update an existing task. Returns True if found and updated."""
|
|
90
|
+
tasks = load_tasks()
|
|
91
|
+
for i, t in enumerate(tasks):
|
|
92
|
+
if t.id == task.id:
|
|
93
|
+
tasks[i] = task
|
|
94
|
+
save_tasks(tasks)
|
|
95
|
+
return True
|
|
96
|
+
return False
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def delete_task(task_id: str) -> bool:
|
|
100
|
+
"""Delete a task by ID. Returns True if found and deleted."""
|
|
101
|
+
tasks = load_tasks()
|
|
102
|
+
original_len = len(tasks)
|
|
103
|
+
tasks = [t for t in tasks if t.id != task_id]
|
|
104
|
+
if len(tasks) < original_len:
|
|
105
|
+
save_tasks(tasks)
|
|
106
|
+
return True
|
|
107
|
+
return False
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def get_task(task_id: str) -> Optional[ScheduledTask]:
|
|
111
|
+
"""Get a task by ID."""
|
|
112
|
+
tasks = load_tasks()
|
|
113
|
+
for t in tasks:
|
|
114
|
+
if t.id == task_id:
|
|
115
|
+
return t
|
|
116
|
+
return None
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def toggle_task(task_id: str) -> Optional[bool]:
|
|
120
|
+
"""Toggle a task's enabled state. Returns new state or None if not found."""
|
|
121
|
+
task = get_task(task_id)
|
|
122
|
+
if task:
|
|
123
|
+
task.enabled = not task.enabled
|
|
124
|
+
update_task(task)
|
|
125
|
+
return task.enabled
|
|
126
|
+
return None
|