code-puppy 0.0.171__py3-none-any.whl → 0.0.173__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/agent.py +8 -8
- code_puppy/agents/agent_creator_agent.py +0 -3
- code_puppy/agents/agent_qa_kitten.py +203 -0
- code_puppy/agents/base_agent.py +398 -2
- code_puppy/command_line/command_handler.py +68 -28
- code_puppy/command_line/mcp/add_command.py +2 -2
- code_puppy/command_line/mcp/base.py +1 -1
- code_puppy/command_line/mcp/install_command.py +2 -2
- code_puppy/command_line/mcp/list_command.py +1 -1
- code_puppy/command_line/mcp/search_command.py +1 -1
- code_puppy/command_line/mcp/start_all_command.py +1 -1
- code_puppy/command_line/mcp/status_command.py +2 -2
- code_puppy/command_line/mcp/stop_all_command.py +1 -1
- code_puppy/command_line/mcp/utils.py +1 -1
- code_puppy/command_line/mcp/wizard_utils.py +2 -2
- code_puppy/config.py +141 -12
- code_puppy/http_utils.py +50 -24
- code_puppy/main.py +2 -1
- code_puppy/{mcp → mcp_}/config_wizard.py +1 -1
- code_puppy/{mcp → mcp_}/examples/retry_example.py +1 -1
- code_puppy/{mcp → mcp_}/managed_server.py +1 -1
- code_puppy/{mcp → mcp_}/server_registry_catalog.py +1 -3
- code_puppy/message_history_processor.py +83 -221
- code_puppy/messaging/message_queue.py +4 -4
- code_puppy/state_management.py +1 -100
- code_puppy/tools/__init__.py +103 -6
- code_puppy/tools/browser/__init__.py +0 -0
- code_puppy/tools/browser/browser_control.py +293 -0
- code_puppy/tools/browser/browser_interactions.py +552 -0
- code_puppy/tools/browser/browser_locators.py +642 -0
- code_puppy/tools/browser/browser_navigation.py +251 -0
- code_puppy/tools/browser/browser_screenshot.py +242 -0
- code_puppy/tools/browser/browser_scripts.py +478 -0
- code_puppy/tools/browser/browser_workflows.py +196 -0
- code_puppy/tools/browser/camoufox_manager.py +194 -0
- code_puppy/tools/browser/vqa_agent.py +66 -0
- code_puppy/tools/browser_control.py +293 -0
- code_puppy/tools/browser_interactions.py +552 -0
- code_puppy/tools/browser_locators.py +642 -0
- code_puppy/tools/browser_navigation.py +251 -0
- code_puppy/tools/browser_screenshot.py +278 -0
- code_puppy/tools/browser_scripts.py +478 -0
- code_puppy/tools/browser_workflows.py +215 -0
- code_puppy/tools/camoufox_manager.py +150 -0
- code_puppy/tools/command_runner.py +13 -8
- code_puppy/tools/file_operations.py +7 -7
- code_puppy/tui/app.py +1 -1
- code_puppy/tui/components/custom_widgets.py +1 -1
- code_puppy/tui/screens/mcp_install_wizard.py +8 -8
- code_puppy/tui_state.py +55 -0
- {code_puppy-0.0.171.dist-info → code_puppy-0.0.173.dist-info}/METADATA +3 -1
- code_puppy-0.0.173.dist-info/RECORD +132 -0
- code_puppy-0.0.171.dist-info/RECORD +0 -112
- /code_puppy/{mcp → mcp_}/__init__.py +0 -0
- /code_puppy/{mcp → mcp_}/async_lifecycle.py +0 -0
- /code_puppy/{mcp → mcp_}/blocking_startup.py +0 -0
- /code_puppy/{mcp → mcp_}/captured_stdio_server.py +0 -0
- /code_puppy/{mcp → mcp_}/circuit_breaker.py +0 -0
- /code_puppy/{mcp → mcp_}/dashboard.py +0 -0
- /code_puppy/{mcp → mcp_}/error_isolation.py +0 -0
- /code_puppy/{mcp → mcp_}/health_monitor.py +0 -0
- /code_puppy/{mcp → mcp_}/manager.py +0 -0
- /code_puppy/{mcp → mcp_}/registry.py +0 -0
- /code_puppy/{mcp → mcp_}/retry_manager.py +0 -0
- /code_puppy/{mcp → mcp_}/status_tracker.py +0 -0
- /code_puppy/{mcp → mcp_}/system_tools.py +0 -0
- {code_puppy-0.0.171.data → code_puppy-0.0.173.data}/data/code_puppy/models.json +0 -0
- {code_puppy-0.0.171.dist-info → code_puppy-0.0.173.dist-info}/WHEEL +0 -0
- {code_puppy-0.0.171.dist-info → code_puppy-0.0.173.dist-info}/entry_points.txt +0 -0
- {code_puppy-0.0.171.dist-info → code_puppy-0.0.173.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""Camoufox browser manager - privacy-focused Firefox automation."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
import camoufox
|
|
6
|
+
from playwright.async_api import Browser, BrowserContext, Page, Playwright, async_playwright
|
|
7
|
+
|
|
8
|
+
from code_puppy.messaging import emit_info
|
|
9
|
+
from camoufox.pkgman import CamoufoxFetcher
|
|
10
|
+
from camoufox.locale import ALLOW_GEOIP, download_mmdb
|
|
11
|
+
from camoufox.addons import maybe_download_addons, DefaultAddons
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CamoufoxManager:
|
|
15
|
+
"""Singleton browser manager for Camoufox (privacy-focused Firefox) automation."""
|
|
16
|
+
|
|
17
|
+
_instance: Optional["CamoufoxManager"] = None
|
|
18
|
+
_browser: Optional[Browser] = None
|
|
19
|
+
_context: Optional[BrowserContext] = None
|
|
20
|
+
_playwright: Optional[Playwright] = None
|
|
21
|
+
_initialized: bool = False
|
|
22
|
+
|
|
23
|
+
def __new__(cls):
|
|
24
|
+
if cls._instance is None:
|
|
25
|
+
cls._instance = super().__new__(cls)
|
|
26
|
+
return cls._instance
|
|
27
|
+
|
|
28
|
+
def __init__(self):
|
|
29
|
+
# Only initialize once
|
|
30
|
+
if hasattr(self, "_init_done"):
|
|
31
|
+
return
|
|
32
|
+
self._init_done = True
|
|
33
|
+
|
|
34
|
+
self.headless = 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
|
+
@classmethod
|
|
42
|
+
def get_instance(cls) -> "CamoufoxManager":
|
|
43
|
+
"""Get the singleton instance."""
|
|
44
|
+
if cls._instance is None:
|
|
45
|
+
cls._instance = cls()
|
|
46
|
+
return cls._instance
|
|
47
|
+
|
|
48
|
+
async def async_initialize(self) -> None:
|
|
49
|
+
"""Initialize Camoufox browser."""
|
|
50
|
+
if self._initialized:
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
emit_info("[yellow]Initializing Camoufox (privacy Firefox)...[/yellow]")
|
|
55
|
+
|
|
56
|
+
# Ensure Camoufox binary and dependencies are fetched before launching
|
|
57
|
+
await self._prefetch_camoufox()
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
await self._initialize_camoufox()
|
|
61
|
+
emit_info(
|
|
62
|
+
"[green]✅ Camoufox initialized successfully (privacy-focused Firefox)[/green]"
|
|
63
|
+
)
|
|
64
|
+
except Exception as camoufox_error:
|
|
65
|
+
error_reason = str(camoufox_error).splitlines()[0]
|
|
66
|
+
emit_info(
|
|
67
|
+
"[yellow]⚠️ Camoufox failed to initialize, falling back to Playwright Firefox[/yellow]"
|
|
68
|
+
)
|
|
69
|
+
await self._cleanup()
|
|
70
|
+
await self._initialize_playwright_firefox(error_reason)
|
|
71
|
+
|
|
72
|
+
self._initialized = True
|
|
73
|
+
|
|
74
|
+
except Exception as e:
|
|
75
|
+
emit_info(f"[red]❌ Failed to initialize browser: {e}[/red]")
|
|
76
|
+
await self._cleanup()
|
|
77
|
+
raise
|
|
78
|
+
|
|
79
|
+
async def _initialize_camoufox(self) -> None:
|
|
80
|
+
"""Try to start Camoufox with the configured privacy settings."""
|
|
81
|
+
camoufox_instance = camoufox.AsyncCamoufox(
|
|
82
|
+
headless=self.headless,
|
|
83
|
+
block_webrtc=self.block_webrtc,
|
|
84
|
+
humanize=self.humanize,
|
|
85
|
+
)
|
|
86
|
+
self._browser = await camoufox_instance.start()
|
|
87
|
+
self._context = await self._browser.new_context(
|
|
88
|
+
viewport={"width": 1920, "height": 1080},
|
|
89
|
+
ignore_https_errors=True,
|
|
90
|
+
)
|
|
91
|
+
page = await self._context.new_page()
|
|
92
|
+
await page.goto(self.homepage)
|
|
93
|
+
|
|
94
|
+
async def _initialize_playwright_firefox(self, error_reason: str) -> None:
|
|
95
|
+
"""Fallback to vanilla Playwright Firefox when Camoufox fails."""
|
|
96
|
+
self._playwright = await async_playwright().start()
|
|
97
|
+
self._browser = await self._playwright.firefox.launch(headless=self.headless)
|
|
98
|
+
self._context = await self._browser.new_context(
|
|
99
|
+
viewport={"width": 1920, "height": 1080},
|
|
100
|
+
ignore_https_errors=True,
|
|
101
|
+
)
|
|
102
|
+
page = await self._context.new_page()
|
|
103
|
+
await page.goto(self.homepage)
|
|
104
|
+
emit_info(
|
|
105
|
+
f"[green]✅ Playwright Firefox fallback ready (Camoufox error: {error_reason})[/green]"
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
async def get_current_page(self) -> Optional[Page]:
|
|
109
|
+
"""Get the currently active page."""
|
|
110
|
+
if not self._initialized or not self._context:
|
|
111
|
+
await self.async_initialize()
|
|
112
|
+
|
|
113
|
+
if self._context:
|
|
114
|
+
pages = self._context.pages
|
|
115
|
+
return pages[0] if pages else None
|
|
116
|
+
return None
|
|
117
|
+
|
|
118
|
+
async def new_page(self, url: Optional[str] = None) -> Page:
|
|
119
|
+
"""Create a new page and optionally navigate to URL."""
|
|
120
|
+
if not self._initialized:
|
|
121
|
+
await self.async_initialize()
|
|
122
|
+
|
|
123
|
+
page = await self._context.new_page()
|
|
124
|
+
if url:
|
|
125
|
+
await page.goto(url)
|
|
126
|
+
return page
|
|
127
|
+
|
|
128
|
+
async def _prefetch_camoufox(self) -> None:
|
|
129
|
+
"""Prefetch Camoufox binary and dependencies."""
|
|
130
|
+
emit_info("[cyan]🔍 Ensuring Camoufox binary and dependencies are up-to-date...[/cyan]")
|
|
131
|
+
|
|
132
|
+
# Fetch Camoufox binary if needed
|
|
133
|
+
CamoufoxFetcher().install()
|
|
134
|
+
|
|
135
|
+
# Fetch GeoIP database if enabled
|
|
136
|
+
if ALLOW_GEOIP:
|
|
137
|
+
download_mmdb()
|
|
138
|
+
|
|
139
|
+
# Download default addons
|
|
140
|
+
maybe_download_addons(list(DefaultAddons))
|
|
141
|
+
|
|
142
|
+
emit_info("[cyan]📦 Camoufox dependencies ready[/cyan]")
|
|
143
|
+
|
|
144
|
+
async def close_page(self, page: Page) -> None:
|
|
145
|
+
"""Close a specific page."""
|
|
146
|
+
await page.close()
|
|
147
|
+
|
|
148
|
+
async def get_all_pages(self) -> list[Page]:
|
|
149
|
+
"""Get all open pages."""
|
|
150
|
+
if not self._context:
|
|
151
|
+
return []
|
|
152
|
+
return self._context.pages
|
|
153
|
+
|
|
154
|
+
async def _cleanup(self) -> None:
|
|
155
|
+
"""Clean up browser resources."""
|
|
156
|
+
try:
|
|
157
|
+
if self._context:
|
|
158
|
+
await self._context.close()
|
|
159
|
+
self._context = None
|
|
160
|
+
if self._browser:
|
|
161
|
+
await self._browser.close()
|
|
162
|
+
self._browser = None
|
|
163
|
+
if self._playwright:
|
|
164
|
+
await self._playwright.stop()
|
|
165
|
+
self._playwright = None
|
|
166
|
+
self._initialized = False
|
|
167
|
+
except Exception as e:
|
|
168
|
+
emit_info(f"[yellow]Warning during cleanup: {e}[/yellow]")
|
|
169
|
+
|
|
170
|
+
async def close(self) -> None:
|
|
171
|
+
"""Close the browser and clean up resources."""
|
|
172
|
+
await self._cleanup()
|
|
173
|
+
emit_info("[yellow]Camoufox browser closed[/yellow]")
|
|
174
|
+
|
|
175
|
+
def __del__(self):
|
|
176
|
+
"""Ensure cleanup on object destruction."""
|
|
177
|
+
# Note: Can't use async in __del__, so this is just a fallback
|
|
178
|
+
if self._initialized:
|
|
179
|
+
import asyncio
|
|
180
|
+
|
|
181
|
+
try:
|
|
182
|
+
loop = asyncio.get_event_loop()
|
|
183
|
+
if loop.is_running():
|
|
184
|
+
loop.create_task(self._cleanup())
|
|
185
|
+
else:
|
|
186
|
+
loop.run_until_complete(self._cleanup())
|
|
187
|
+
except:
|
|
188
|
+
pass # Best effort cleanup
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
# Convenience function for getting the singleton instance
|
|
192
|
+
def get_camoufox_manager() -> CamoufoxManager:
|
|
193
|
+
"""Get the singleton CamoufoxManager instance."""
|
|
194
|
+
return CamoufoxManager.get_instance()
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Utilities for running visual question-answering via pydantic-ai."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from functools import lru_cache
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, Field
|
|
9
|
+
from pydantic_ai import Agent, BinaryContent, InstrumentationSettings
|
|
10
|
+
|
|
11
|
+
from code_puppy.config import get_vqa_model_name
|
|
12
|
+
from code_puppy.model_factory import ModelFactory
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class VisualAnalysisResult(BaseModel):
|
|
16
|
+
"""Structured response from the VQA agent."""
|
|
17
|
+
|
|
18
|
+
answer: str
|
|
19
|
+
confidence: float = Field(ge=0.0, le=1.0)
|
|
20
|
+
observations: str
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@lru_cache(maxsize=1)
|
|
24
|
+
def _load_vqa_agent(model_name: str) -> Agent[None, VisualAnalysisResult]:
|
|
25
|
+
"""Create a cached agent instance for visual analysis."""
|
|
26
|
+
models_config = ModelFactory.load_config()
|
|
27
|
+
model = ModelFactory.get_model(model_name, models_config)
|
|
28
|
+
|
|
29
|
+
instrumentation = InstrumentationSettings(include_binary_content=False)
|
|
30
|
+
|
|
31
|
+
instructions = (
|
|
32
|
+
"You are a visual analysis specialist. Answer the user's question about the provided image. "
|
|
33
|
+
"Always respond using the structured schema: answer, confidence (0-1 float), observations. "
|
|
34
|
+
"Confidence reflects how certain you are about the answer. Observations should include useful, concise context."
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
return Agent(
|
|
38
|
+
model=model,
|
|
39
|
+
instructions=instructions,
|
|
40
|
+
output_type=VisualAnalysisResult,
|
|
41
|
+
retries=2,
|
|
42
|
+
instrument=instrumentation,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _get_vqa_agent() -> Agent[None, VisualAnalysisResult]:
|
|
47
|
+
"""Return a cached VQA agent configured with the current model."""
|
|
48
|
+
model_name = get_vqa_model_name()
|
|
49
|
+
# lru_cache keyed by model_name ensures refresh when configuration changes
|
|
50
|
+
return _load_vqa_agent(model_name)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def run_vqa_analysis(
|
|
54
|
+
question: str,
|
|
55
|
+
image_bytes: bytes,
|
|
56
|
+
media_type: str = "image/png",
|
|
57
|
+
) -> VisualAnalysisResult:
|
|
58
|
+
"""Execute the VQA agent synchronously against screenshot bytes."""
|
|
59
|
+
agent = _get_vqa_agent()
|
|
60
|
+
result = agent.run_sync(
|
|
61
|
+
[
|
|
62
|
+
question,
|
|
63
|
+
BinaryContent(data=image_bytes, media_type=media_type),
|
|
64
|
+
]
|
|
65
|
+
)
|
|
66
|
+
return result.output
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
"""Browser initialization and control tools."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, Optional
|
|
4
|
+
|
|
5
|
+
from pydantic_ai import RunContext
|
|
6
|
+
|
|
7
|
+
from code_puppy.messaging import emit_info
|
|
8
|
+
from code_puppy.tools.common import generate_group_id
|
|
9
|
+
|
|
10
|
+
from .camoufox_manager import get_camoufox_manager
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
async def initialize_browser(
|
|
14
|
+
headless: bool = False,
|
|
15
|
+
browser_type: str = "chromium",
|
|
16
|
+
homepage: str = "https://www.google.com",
|
|
17
|
+
) -> Dict[str, Any]:
|
|
18
|
+
"""Initialize the browser with specified settings."""
|
|
19
|
+
group_id = generate_group_id("browser_initialize", f"{browser_type}_{homepage}")
|
|
20
|
+
emit_info(
|
|
21
|
+
f"[bold white on blue] BROWSER INITIALIZE [/bold white on blue] 🌐 {browser_type} → {homepage}",
|
|
22
|
+
message_group=group_id,
|
|
23
|
+
)
|
|
24
|
+
try:
|
|
25
|
+
browser_manager = get_camoufox_manager()
|
|
26
|
+
|
|
27
|
+
# Configure browser settings
|
|
28
|
+
browser_manager.headless = headless
|
|
29
|
+
browser_manager.browser_type = browser_type
|
|
30
|
+
browser_manager.homepage = homepage
|
|
31
|
+
|
|
32
|
+
# Initialize browser
|
|
33
|
+
await browser_manager.async_initialize()
|
|
34
|
+
|
|
35
|
+
# Get page info
|
|
36
|
+
page = await browser_manager.get_current_page()
|
|
37
|
+
if page:
|
|
38
|
+
url = page.url
|
|
39
|
+
title = await page.title()
|
|
40
|
+
else:
|
|
41
|
+
url = "Unknown"
|
|
42
|
+
title = "Unknown"
|
|
43
|
+
|
|
44
|
+
emit_info(
|
|
45
|
+
"[green]Browser initialized successfully[/green]", message_group=group_id
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
"success": True,
|
|
50
|
+
"browser_type": browser_type,
|
|
51
|
+
"headless": headless,
|
|
52
|
+
"homepage": homepage,
|
|
53
|
+
"current_url": url,
|
|
54
|
+
"current_title": title,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
except Exception as e:
|
|
58
|
+
emit_info(
|
|
59
|
+
f"[red]Browser initialization failed: {str(e)}[/red]",
|
|
60
|
+
message_group=group_id,
|
|
61
|
+
)
|
|
62
|
+
return {
|
|
63
|
+
"success": False,
|
|
64
|
+
"error": str(e),
|
|
65
|
+
"browser_type": browser_type,
|
|
66
|
+
"headless": headless,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
async def close_browser() -> Dict[str, Any]:
|
|
71
|
+
"""Close the browser and clean up resources."""
|
|
72
|
+
group_id = generate_group_id("browser_close")
|
|
73
|
+
emit_info(
|
|
74
|
+
"[bold white on blue] BROWSER CLOSE [/bold white on blue] 🔒",
|
|
75
|
+
message_group=group_id,
|
|
76
|
+
)
|
|
77
|
+
try:
|
|
78
|
+
browser_manager = get_camoufox_manager()
|
|
79
|
+
await browser_manager.close()
|
|
80
|
+
|
|
81
|
+
emit_info(
|
|
82
|
+
"[yellow]Browser closed successfully[/yellow]", message_group=group_id
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
return {"success": True, "message": "Browser closed"}
|
|
86
|
+
|
|
87
|
+
except Exception as e:
|
|
88
|
+
return {"success": False, "error": str(e)}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
async def get_browser_status() -> Dict[str, Any]:
|
|
92
|
+
"""Get current browser status and information."""
|
|
93
|
+
group_id = generate_group_id("browser_status")
|
|
94
|
+
emit_info(
|
|
95
|
+
"[bold white on blue] BROWSER STATUS [/bold white on blue] 📊",
|
|
96
|
+
message_group=group_id,
|
|
97
|
+
)
|
|
98
|
+
try:
|
|
99
|
+
browser_manager = get_camoufox_manager()
|
|
100
|
+
|
|
101
|
+
if not browser_manager._initialized:
|
|
102
|
+
return {
|
|
103
|
+
"success": True,
|
|
104
|
+
"status": "not_initialized",
|
|
105
|
+
"browser_type": browser_manager.browser_type,
|
|
106
|
+
"headless": browser_manager.headless,
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
page = await browser_manager.get_current_page()
|
|
110
|
+
if page:
|
|
111
|
+
url = page.url
|
|
112
|
+
title = await page.title()
|
|
113
|
+
|
|
114
|
+
# Get all pages
|
|
115
|
+
all_pages = await browser_manager.get_all_pages()
|
|
116
|
+
page_count = len(all_pages)
|
|
117
|
+
else:
|
|
118
|
+
url = None
|
|
119
|
+
title = None
|
|
120
|
+
page_count = 0
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
"success": True,
|
|
124
|
+
"status": "initialized",
|
|
125
|
+
"browser_type": browser_manager.browser_type,
|
|
126
|
+
"headless": browser_manager.headless,
|
|
127
|
+
"current_url": url,
|
|
128
|
+
"current_title": title,
|
|
129
|
+
"page_count": page_count,
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
except Exception as e:
|
|
133
|
+
return {"success": False, "error": str(e)}
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
async def create_new_page(url: Optional[str] = None) -> Dict[str, Any]:
|
|
137
|
+
"""Create a new browser page/tab."""
|
|
138
|
+
group_id = generate_group_id("browser_new_page", url or "blank")
|
|
139
|
+
emit_info(
|
|
140
|
+
f"[bold white on blue] BROWSER NEW PAGE [/bold white on blue] 📄 {url or 'blank page'}",
|
|
141
|
+
message_group=group_id,
|
|
142
|
+
)
|
|
143
|
+
try:
|
|
144
|
+
browser_manager = get_camoufox_manager()
|
|
145
|
+
|
|
146
|
+
if not browser_manager._initialized:
|
|
147
|
+
return {
|
|
148
|
+
"success": False,
|
|
149
|
+
"error": "Browser not initialized. Use browser_initialize first.",
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
page = await browser_manager.new_page(url)
|
|
153
|
+
|
|
154
|
+
final_url = page.url
|
|
155
|
+
title = await page.title()
|
|
156
|
+
|
|
157
|
+
emit_info(
|
|
158
|
+
f"[green]Created new page: {final_url}[/green]", message_group=group_id
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
return {"success": True, "url": final_url, "title": title, "requested_url": url}
|
|
162
|
+
|
|
163
|
+
except Exception as e:
|
|
164
|
+
return {"success": False, "error": str(e), "url": url}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
async def list_pages() -> Dict[str, Any]:
|
|
168
|
+
"""List all open browser pages/tabs."""
|
|
169
|
+
group_id = generate_group_id("browser_list_pages")
|
|
170
|
+
emit_info(
|
|
171
|
+
"[bold white on blue] BROWSER LIST PAGES [/bold white on blue] 📋",
|
|
172
|
+
message_group=group_id,
|
|
173
|
+
)
|
|
174
|
+
try:
|
|
175
|
+
browser_manager = get_camoufox_manager()
|
|
176
|
+
|
|
177
|
+
if not browser_manager._initialized:
|
|
178
|
+
return {"success": False, "error": "Browser not initialized"}
|
|
179
|
+
|
|
180
|
+
all_pages = await browser_manager.get_all_pages()
|
|
181
|
+
|
|
182
|
+
pages_info = []
|
|
183
|
+
for i, page in enumerate(all_pages):
|
|
184
|
+
try:
|
|
185
|
+
url = page.url
|
|
186
|
+
title = await page.title()
|
|
187
|
+
is_closed = page.is_closed()
|
|
188
|
+
|
|
189
|
+
pages_info.append(
|
|
190
|
+
{"index": i, "url": url, "title": title, "closed": is_closed}
|
|
191
|
+
)
|
|
192
|
+
except Exception as e:
|
|
193
|
+
pages_info.append(
|
|
194
|
+
{
|
|
195
|
+
"index": i,
|
|
196
|
+
"url": "Error",
|
|
197
|
+
"title": "Error",
|
|
198
|
+
"error": str(e),
|
|
199
|
+
"closed": True,
|
|
200
|
+
}
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
return {"success": True, "page_count": len(all_pages), "pages": pages_info}
|
|
204
|
+
|
|
205
|
+
except Exception as e:
|
|
206
|
+
return {"success": False, "error": str(e)}
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
# Tool registration functions
|
|
210
|
+
def register_initialize_browser(agent):
|
|
211
|
+
"""Register the browser initialization tool."""
|
|
212
|
+
|
|
213
|
+
@agent.tool
|
|
214
|
+
async def browser_initialize(
|
|
215
|
+
context: RunContext,
|
|
216
|
+
headless: bool = False,
|
|
217
|
+
browser_type: str = "chromium",
|
|
218
|
+
homepage: str = "https://www.google.com",
|
|
219
|
+
) -> Dict[str, Any]:
|
|
220
|
+
"""
|
|
221
|
+
Initialize the browser with specified settings. Must be called before using other browser tools.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
headless: Run browser in headless mode (no GUI)
|
|
225
|
+
browser_type: Browser engine (chromium, firefox, webkit)
|
|
226
|
+
homepage: Initial page to load
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
Dict with initialization results
|
|
230
|
+
"""
|
|
231
|
+
return await initialize_browser(headless, browser_type, homepage)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def register_close_browser(agent):
|
|
235
|
+
"""Register the browser close tool."""
|
|
236
|
+
|
|
237
|
+
@agent.tool
|
|
238
|
+
async def browser_close(context: RunContext) -> Dict[str, Any]:
|
|
239
|
+
"""
|
|
240
|
+
Close the browser and clean up all resources.
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
Dict with close results
|
|
244
|
+
"""
|
|
245
|
+
return await close_browser()
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def register_get_browser_status(agent):
|
|
249
|
+
"""Register the browser status tool."""
|
|
250
|
+
|
|
251
|
+
@agent.tool
|
|
252
|
+
async def browser_status(context: RunContext) -> Dict[str, Any]:
|
|
253
|
+
"""
|
|
254
|
+
Get current browser status and information.
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
Dict with browser status and metadata
|
|
258
|
+
"""
|
|
259
|
+
return await get_browser_status()
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def register_create_new_page(agent):
|
|
263
|
+
"""Register the new page creation tool."""
|
|
264
|
+
|
|
265
|
+
@agent.tool
|
|
266
|
+
async def browser_new_page(
|
|
267
|
+
context: RunContext,
|
|
268
|
+
url: Optional[str] = None,
|
|
269
|
+
) -> Dict[str, Any]:
|
|
270
|
+
"""
|
|
271
|
+
Create a new browser page/tab.
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
url: Optional URL to navigate to in the new page
|
|
275
|
+
|
|
276
|
+
Returns:
|
|
277
|
+
Dict with new page results
|
|
278
|
+
"""
|
|
279
|
+
return await create_new_page(url)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def register_list_pages(agent):
|
|
283
|
+
"""Register the list pages tool."""
|
|
284
|
+
|
|
285
|
+
@agent.tool
|
|
286
|
+
async def browser_list_pages(context: RunContext) -> Dict[str, Any]:
|
|
287
|
+
"""
|
|
288
|
+
List all open browser pages/tabs.
|
|
289
|
+
|
|
290
|
+
Returns:
|
|
291
|
+
Dict with information about all open pages
|
|
292
|
+
"""
|
|
293
|
+
return await list_pages()
|