code-puppy 0.0.348__py3-none-any.whl → 0.0.372__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/agents/__init__.py +8 -0
- code_puppy/agents/agent_manager.py +272 -1
- code_puppy/agents/agent_pack_leader.py +383 -0
- code_puppy/agents/agent_qa_kitten.py +12 -7
- code_puppy/agents/agent_terminal_qa.py +323 -0
- code_puppy/agents/base_agent.py +11 -8
- code_puppy/agents/event_stream_handler.py +101 -8
- code_puppy/agents/pack/__init__.py +34 -0
- code_puppy/agents/pack/bloodhound.py +304 -0
- code_puppy/agents/pack/husky.py +321 -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/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 +446 -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 +74 -0
- code_puppy/api/routers/sessions.py +232 -0
- code_puppy/api/templates/terminal.html +361 -0
- code_puppy/api/websocket.py +154 -0
- code_puppy/callbacks.py +73 -0
- code_puppy/chatgpt_codex_client.py +53 -0
- code_puppy/claude_cache_client.py +294 -41
- code_puppy/command_line/add_model_menu.py +13 -4
- code_puppy/command_line/agent_menu.py +662 -0
- code_puppy/command_line/core_commands.py +89 -112
- code_puppy/command_line/model_picker_completion.py +3 -20
- code_puppy/command_line/model_settings_menu.py +21 -3
- code_puppy/config.py +145 -70
- code_puppy/gemini_model.py +706 -0
- code_puppy/http_utils.py +6 -3
- code_puppy/messaging/__init__.py +15 -0
- code_puppy/messaging/messages.py +27 -0
- code_puppy/messaging/queue_console.py +1 -1
- code_puppy/messaging/rich_renderer.py +36 -1
- code_puppy/messaging/spinner/__init__.py +20 -2
- code_puppy/messaging/subagent_console.py +461 -0
- code_puppy/model_factory.py +50 -16
- code_puppy/model_switching.py +63 -0
- code_puppy/model_utils.py +27 -24
- code_puppy/models.json +12 -12
- code_puppy/plugins/antigravity_oauth/antigravity_model.py +206 -172
- code_puppy/plugins/antigravity_oauth/register_callbacks.py +15 -8
- code_puppy/plugins/antigravity_oauth/transport.py +236 -45
- code_puppy/plugins/chatgpt_oauth/register_callbacks.py +2 -2
- code_puppy/plugins/claude_code_oauth/register_callbacks.py +2 -30
- code_puppy/plugins/claude_code_oauth/utils.py +4 -1
- 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/prompts/antigravity_system_prompt.md +1 -0
- code_puppy/pydantic_patches.py +52 -0
- code_puppy/status_display.py +6 -2
- code_puppy/tools/__init__.py +37 -1
- code_puppy/tools/agent_tools.py +83 -33
- code_puppy/tools/browser/__init__.py +37 -0
- code_puppy/tools/browser/browser_control.py +6 -6
- code_puppy/tools/browser/browser_interactions.py +21 -20
- code_puppy/tools/browser/browser_locators.py +9 -9
- code_puppy/tools/browser/browser_manager.py +316 -0
- code_puppy/tools/browser/browser_navigation.py +7 -7
- code_puppy/tools/browser/browser_screenshot.py +78 -140
- code_puppy/tools/browser/browser_scripts.py +15 -13
- code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
- code_puppy/tools/browser/terminal_command_tools.py +521 -0
- code_puppy/tools/browser/terminal_screenshot_tools.py +556 -0
- code_puppy/tools/browser/terminal_tools.py +525 -0
- code_puppy/tools/command_runner.py +292 -101
- code_puppy/tools/common.py +176 -1
- code_puppy/tools/display.py +84 -0
- code_puppy/tools/subagent_context.py +158 -0
- {code_puppy-0.0.348.data → code_puppy-0.0.372.data}/data/code_puppy/models.json +12 -12
- {code_puppy-0.0.348.dist-info → code_puppy-0.0.372.dist-info}/METADATA +17 -16
- {code_puppy-0.0.348.dist-info → code_puppy-0.0.372.dist-info}/RECORD +84 -51
- code_puppy/prompts/codex_system_prompt.md +0 -310
- code_puppy/tools/browser/camoufox_manager.py +0 -235
- code_puppy/tools/browser/vqa_agent.py +0 -90
- {code_puppy-0.0.348.data → code_puppy-0.0.372.data}/data/code_puppy/models_dev_api.json +0 -0
- {code_puppy-0.0.348.dist-info → code_puppy-0.0.372.dist-info}/WHEEL +0 -0
- {code_puppy-0.0.348.dist-info → code_puppy-0.0.372.dist-info}/entry_points.txt +0 -0
- {code_puppy-0.0.348.dist-info → code_puppy-0.0.372.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,235 +0,0 @@
|
|
|
1
|
-
"""Camoufox browser manager - privacy-focused Firefox automation."""
|
|
2
|
-
|
|
3
|
-
import os
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
from typing import Optional
|
|
6
|
-
|
|
7
|
-
from playwright.async_api import Browser, BrowserContext, Page
|
|
8
|
-
|
|
9
|
-
from code_puppy import config
|
|
10
|
-
from code_puppy.messaging import emit_info, emit_success, emit_warning
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class CamoufoxManager:
|
|
14
|
-
"""Singleton browser manager for Camoufox (privacy-focused Firefox) automation."""
|
|
15
|
-
|
|
16
|
-
_instance: Optional["CamoufoxManager"] = None
|
|
17
|
-
_browser: Optional[Browser] = None
|
|
18
|
-
_context: Optional[BrowserContext] = None
|
|
19
|
-
_initialized: bool = False
|
|
20
|
-
|
|
21
|
-
def __new__(cls):
|
|
22
|
-
if cls._instance is None:
|
|
23
|
-
cls._instance = super().__new__(cls)
|
|
24
|
-
return cls._instance
|
|
25
|
-
|
|
26
|
-
def __init__(self):
|
|
27
|
-
# Only initialize once
|
|
28
|
-
if hasattr(self, "_init_done"):
|
|
29
|
-
return
|
|
30
|
-
self._init_done = True
|
|
31
|
-
|
|
32
|
-
# Default to headless=True (no browser spam during tests)
|
|
33
|
-
# Override with BROWSER_HEADLESS=false to see the browser
|
|
34
|
-
self.headless = os.getenv("BROWSER_HEADLESS", "true").lower() != "false"
|
|
35
|
-
self.homepage = "https://www.google.com"
|
|
36
|
-
# Camoufox-specific settings
|
|
37
|
-
self.geoip = True # Enable GeoIP spoofing
|
|
38
|
-
self.block_webrtc = True # Block WebRTC for privacy
|
|
39
|
-
self.humanize = True # Add human-like behavior
|
|
40
|
-
|
|
41
|
-
# Persistent profile directory for consistent browser state across runs
|
|
42
|
-
self.profile_dir = self._get_profile_directory()
|
|
43
|
-
|
|
44
|
-
@classmethod
|
|
45
|
-
def get_instance(cls) -> "CamoufoxManager":
|
|
46
|
-
"""Get the singleton instance."""
|
|
47
|
-
if cls._instance is None:
|
|
48
|
-
cls._instance = cls()
|
|
49
|
-
return cls._instance
|
|
50
|
-
|
|
51
|
-
def _get_profile_directory(self) -> Path:
|
|
52
|
-
"""Get or create the persistent profile directory (uses XDG_CACHE_HOME).
|
|
53
|
-
|
|
54
|
-
Returns a Path object pointing to XDG_CACHE_HOME/code_puppy/camoufox_profile
|
|
55
|
-
where browser data (cookies, history, bookmarks, etc.) will be stored.
|
|
56
|
-
"""
|
|
57
|
-
cache_dir = Path(config.CACHE_DIR)
|
|
58
|
-
profile_path = cache_dir / "camoufox_profile"
|
|
59
|
-
profile_path.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
60
|
-
return profile_path
|
|
61
|
-
|
|
62
|
-
async def async_initialize(self) -> None:
|
|
63
|
-
"""Initialize Camoufox browser."""
|
|
64
|
-
if self._initialized:
|
|
65
|
-
return
|
|
66
|
-
|
|
67
|
-
try:
|
|
68
|
-
emit_info("Initializing Camoufox (privacy Firefox)...")
|
|
69
|
-
|
|
70
|
-
# Ensure Camoufox binary and dependencies are fetched before launching
|
|
71
|
-
await self._prefetch_camoufox()
|
|
72
|
-
|
|
73
|
-
await self._initialize_camoufox()
|
|
74
|
-
# emit_info(
|
|
75
|
-
# "[green]✅ Camoufox initialized successfully (privacy-focused Firefox)[/green]"
|
|
76
|
-
# ) # Removed to reduce console spam
|
|
77
|
-
self._initialized = True
|
|
78
|
-
|
|
79
|
-
except Exception:
|
|
80
|
-
await self._cleanup()
|
|
81
|
-
raise
|
|
82
|
-
|
|
83
|
-
async def _initialize_camoufox(self) -> None:
|
|
84
|
-
"""Try to start Camoufox with the configured privacy settings."""
|
|
85
|
-
emit_info(f"Using persistent profile: {self.profile_dir}")
|
|
86
|
-
# Lazy import camoufox to avoid triggering heavy optional deps at import time
|
|
87
|
-
try:
|
|
88
|
-
import camoufox
|
|
89
|
-
from camoufox.addons import DefaultAddons
|
|
90
|
-
|
|
91
|
-
camoufox_instance = camoufox.AsyncCamoufox(
|
|
92
|
-
headless=self.headless,
|
|
93
|
-
block_webrtc=self.block_webrtc,
|
|
94
|
-
humanize=self.humanize,
|
|
95
|
-
exclude_addons=list(DefaultAddons),
|
|
96
|
-
persistent_context=True,
|
|
97
|
-
user_data_dir=str(self.profile_dir),
|
|
98
|
-
addons=[],
|
|
99
|
-
)
|
|
100
|
-
|
|
101
|
-
self._browser = camoufox_instance.browser
|
|
102
|
-
if not self._initialized:
|
|
103
|
-
self._context = await camoufox_instance.start()
|
|
104
|
-
self._initialized = True
|
|
105
|
-
except Exception:
|
|
106
|
-
from playwright.async_api import async_playwright
|
|
107
|
-
|
|
108
|
-
emit_warning(
|
|
109
|
-
"Camoufox no disponible. Usando Playwright (Chromium) como alternativa."
|
|
110
|
-
)
|
|
111
|
-
pw = await async_playwright().start()
|
|
112
|
-
# Use persistent context directory for Chromium to emulate previous behavior
|
|
113
|
-
context = await pw.chromium.launch_persistent_context(
|
|
114
|
-
user_data_dir=str(self.profile_dir), headless=self.headless
|
|
115
|
-
)
|
|
116
|
-
self._context = context
|
|
117
|
-
self._browser = context.browser
|
|
118
|
-
self._initialized = True
|
|
119
|
-
|
|
120
|
-
async def get_current_page(self) -> Optional[Page]:
|
|
121
|
-
"""Get the currently active page. Lazily creates one if none exist."""
|
|
122
|
-
if not self._initialized or not self._context:
|
|
123
|
-
await self.async_initialize()
|
|
124
|
-
|
|
125
|
-
if not self._context:
|
|
126
|
-
return None
|
|
127
|
-
|
|
128
|
-
pages = self._context.pages
|
|
129
|
-
if pages:
|
|
130
|
-
return pages[0]
|
|
131
|
-
|
|
132
|
-
# Lazily create a new blank page without navigation
|
|
133
|
-
return await self._context.new_page()
|
|
134
|
-
|
|
135
|
-
async def new_page(self, url: Optional[str] = None) -> Page:
|
|
136
|
-
"""Create a new page and optionally navigate to URL."""
|
|
137
|
-
if not self._initialized:
|
|
138
|
-
await self.async_initialize()
|
|
139
|
-
|
|
140
|
-
page = await self._context.new_page()
|
|
141
|
-
if url:
|
|
142
|
-
await page.goto(url)
|
|
143
|
-
return page
|
|
144
|
-
|
|
145
|
-
async def _prefetch_camoufox(self) -> None:
|
|
146
|
-
"""Prefetch Camoufox binary and dependencies."""
|
|
147
|
-
emit_info("Ensuring Camoufox binary and dependencies are up-to-date...")
|
|
148
|
-
|
|
149
|
-
# Lazy import camoufox utilities to avoid side effects during module import
|
|
150
|
-
try:
|
|
151
|
-
from camoufox.exceptions import CamoufoxNotInstalled, UnsupportedVersion
|
|
152
|
-
from camoufox.locale import ALLOW_GEOIP, download_mmdb
|
|
153
|
-
from camoufox.pkgman import CamoufoxFetcher, camoufox_path
|
|
154
|
-
except Exception:
|
|
155
|
-
emit_warning(
|
|
156
|
-
"Camoufox no disponible. Omitiendo prefetch y preparándose para usar Playwright."
|
|
157
|
-
)
|
|
158
|
-
return
|
|
159
|
-
|
|
160
|
-
needs_install = False
|
|
161
|
-
try:
|
|
162
|
-
camoufox_path(download_if_missing=False)
|
|
163
|
-
emit_info("Using cached Camoufox installation")
|
|
164
|
-
except (CamoufoxNotInstalled, FileNotFoundError):
|
|
165
|
-
emit_info("Camoufox not found, installing fresh copy")
|
|
166
|
-
needs_install = True
|
|
167
|
-
except UnsupportedVersion:
|
|
168
|
-
emit_info("Camoufox update required, reinstalling")
|
|
169
|
-
needs_install = True
|
|
170
|
-
|
|
171
|
-
if needs_install:
|
|
172
|
-
CamoufoxFetcher().install()
|
|
173
|
-
|
|
174
|
-
# Fetch GeoIP database if enabled
|
|
175
|
-
if ALLOW_GEOIP:
|
|
176
|
-
download_mmdb()
|
|
177
|
-
|
|
178
|
-
emit_info("Camoufox dependencies ready")
|
|
179
|
-
|
|
180
|
-
async def close_page(self, page: Page) -> None:
|
|
181
|
-
"""Close a specific page."""
|
|
182
|
-
await page.close()
|
|
183
|
-
|
|
184
|
-
async def get_all_pages(self) -> list[Page]:
|
|
185
|
-
"""Get all open pages."""
|
|
186
|
-
if not self._context:
|
|
187
|
-
return []
|
|
188
|
-
return self._context.pages
|
|
189
|
-
|
|
190
|
-
async def _cleanup(self) -> None:
|
|
191
|
-
"""Clean up browser resources and save persistent state."""
|
|
192
|
-
try:
|
|
193
|
-
# Save browser state before closing (cookies, localStorage, etc.)
|
|
194
|
-
if self._context:
|
|
195
|
-
try:
|
|
196
|
-
storage_state_path = self.profile_dir / "storage_state.json"
|
|
197
|
-
await self._context.storage_state(path=str(storage_state_path))
|
|
198
|
-
emit_success(f"Browser state saved to {storage_state_path}")
|
|
199
|
-
except Exception as e:
|
|
200
|
-
emit_warning(f"Could not save storage state: {e}")
|
|
201
|
-
|
|
202
|
-
await self._context.close()
|
|
203
|
-
self._context = None
|
|
204
|
-
if self._browser:
|
|
205
|
-
await self._browser.close()
|
|
206
|
-
self._browser = None
|
|
207
|
-
self._initialized = False
|
|
208
|
-
except Exception as e:
|
|
209
|
-
emit_warning(f"Warning during cleanup: {e}")
|
|
210
|
-
|
|
211
|
-
async def close(self) -> None:
|
|
212
|
-
"""Close the browser and clean up resources."""
|
|
213
|
-
await self._cleanup()
|
|
214
|
-
emit_info("Camoufox browser closed")
|
|
215
|
-
|
|
216
|
-
def __del__(self):
|
|
217
|
-
"""Ensure cleanup on object destruction."""
|
|
218
|
-
# Note: Can't use async in __del__, so this is just a fallback
|
|
219
|
-
if self._initialized:
|
|
220
|
-
import asyncio
|
|
221
|
-
|
|
222
|
-
try:
|
|
223
|
-
loop = asyncio.get_event_loop()
|
|
224
|
-
if loop.is_running():
|
|
225
|
-
loop.create_task(self._cleanup())
|
|
226
|
-
else:
|
|
227
|
-
loop.run_until_complete(self._cleanup())
|
|
228
|
-
except Exception:
|
|
229
|
-
pass # Best effort cleanup
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
# Convenience function for getting the singleton instance
|
|
233
|
-
def get_camoufox_manager() -> CamoufoxManager:
|
|
234
|
-
"""Get the singleton CamoufoxManager instance."""
|
|
235
|
-
return CamoufoxManager.get_instance()
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
"""Utilities for running visual question-answering via pydantic-ai."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from functools import lru_cache
|
|
6
|
-
|
|
7
|
-
from pydantic import BaseModel, Field
|
|
8
|
-
from pydantic_ai import Agent, BinaryContent
|
|
9
|
-
|
|
10
|
-
from code_puppy.config import get_use_dbos, get_vqa_model_name
|
|
11
|
-
from code_puppy.model_factory import ModelFactory
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class VisualAnalysisResult(BaseModel):
|
|
15
|
-
"""Structured response from the VQA agent."""
|
|
16
|
-
|
|
17
|
-
answer: str
|
|
18
|
-
confidence: float = Field(ge=0.0, le=1.0)
|
|
19
|
-
observations: str
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def _get_vqa_instructions() -> str:
|
|
23
|
-
"""Get the system instructions for the VQA agent."""
|
|
24
|
-
return (
|
|
25
|
-
"You are a visual analysis specialist. Answer the user's question about the provided image. "
|
|
26
|
-
"Always respond using the structured schema: answer, confidence (0-1 float), observations. "
|
|
27
|
-
"Confidence reflects how certain you are about the answer. Observations should include useful, concise context."
|
|
28
|
-
)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
@lru_cache(maxsize=1)
|
|
32
|
-
def _load_vqa_agent(model_name: str) -> Agent[None, VisualAnalysisResult]:
|
|
33
|
-
"""Create a cached agent instance for visual analysis."""
|
|
34
|
-
from code_puppy.model_utils import prepare_prompt_for_model
|
|
35
|
-
|
|
36
|
-
models_config = ModelFactory.load_config()
|
|
37
|
-
model = ModelFactory.get_model(model_name, models_config)
|
|
38
|
-
|
|
39
|
-
# Handle claude-code models: swap instructions (prompt prepending happens in run_vqa_analysis)
|
|
40
|
-
instructions = _get_vqa_instructions()
|
|
41
|
-
prepared = prepare_prompt_for_model(
|
|
42
|
-
model_name, instructions, "", prepend_system_to_user=False
|
|
43
|
-
)
|
|
44
|
-
instructions = prepared.instructions
|
|
45
|
-
|
|
46
|
-
vqa_agent = Agent(
|
|
47
|
-
model=model,
|
|
48
|
-
instructions=instructions,
|
|
49
|
-
output_type=VisualAnalysisResult,
|
|
50
|
-
retries=2,
|
|
51
|
-
)
|
|
52
|
-
|
|
53
|
-
if get_use_dbos():
|
|
54
|
-
from pydantic_ai.durable_exec.dbos import DBOSAgent
|
|
55
|
-
|
|
56
|
-
dbos_agent = DBOSAgent(vqa_agent, name="vqa-agent")
|
|
57
|
-
return dbos_agent
|
|
58
|
-
|
|
59
|
-
return vqa_agent
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
def _get_vqa_agent() -> Agent[None, VisualAnalysisResult]:
|
|
63
|
-
"""Return a cached VQA agent configured with the current model."""
|
|
64
|
-
model_name = get_vqa_model_name()
|
|
65
|
-
# lru_cache keyed by model_name ensures refresh when configuration changes
|
|
66
|
-
return _load_vqa_agent(model_name)
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
def run_vqa_analysis(
|
|
70
|
-
question: str,
|
|
71
|
-
image_bytes: bytes,
|
|
72
|
-
media_type: str = "image/png",
|
|
73
|
-
) -> VisualAnalysisResult:
|
|
74
|
-
"""Execute the VQA agent synchronously against screenshot bytes."""
|
|
75
|
-
from code_puppy.model_utils import prepare_prompt_for_model
|
|
76
|
-
|
|
77
|
-
agent = _get_vqa_agent()
|
|
78
|
-
|
|
79
|
-
# Handle claude-code models: prepend system prompt to user question
|
|
80
|
-
model_name = get_vqa_model_name()
|
|
81
|
-
prepared = prepare_prompt_for_model(model_name, _get_vqa_instructions(), question)
|
|
82
|
-
question = prepared.user_prompt
|
|
83
|
-
|
|
84
|
-
result = agent.run_sync(
|
|
85
|
-
[
|
|
86
|
-
question,
|
|
87
|
-
BinaryContent(data=image_bytes, media_type=media_type),
|
|
88
|
-
]
|
|
89
|
-
)
|
|
90
|
-
return result.output
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|