squidbot 0.1.0__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.
@@ -0,0 +1,233 @@
1
+ """Playwright browser check module.
2
+
3
+ Verifies that Playwright and Chromium browser are properly installed
4
+ and working before server startup.
5
+ """
6
+
7
+ import asyncio
8
+ import logging
9
+ import sys
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class PlaywrightCheckError(Exception):
15
+ """Error raised when Playwright check fails."""
16
+
17
+ pass
18
+
19
+
20
+ async def check_playwright_installation() -> tuple[bool, str]:
21
+ """
22
+ Check if Playwright is installed and Chromium browser is available.
23
+
24
+ Returns:
25
+ tuple[bool, str]: (success, message)
26
+ """
27
+ try:
28
+ from playwright.async_api import async_playwright
29
+ except ImportError as e:
30
+ return False, f"Playwright not installed: {e}\nRun: pip install playwright"
31
+
32
+ playwright = None
33
+ browser = None
34
+ try:
35
+ playwright = await async_playwright().start()
36
+
37
+ # Try to launch Chromium
38
+ browser = await playwright.chromium.launch(headless=True)
39
+
40
+ # Create a test page and verify it works
41
+ page = await browser.new_page()
42
+ await page.goto("about:blank")
43
+ title = await page.title()
44
+ await page.close()
45
+
46
+ return True, "Playwright and Chromium browser are working correctly"
47
+
48
+ except Exception as e:
49
+ error_msg = str(e)
50
+
51
+ # Check for common error patterns
52
+ if "Executable doesn't exist" in error_msg or "browserType.launch" in error_msg:
53
+ return False, (
54
+ f"Chromium browser not installed for Playwright.\n"
55
+ f"Run: playwright install chromium\n"
56
+ f"Or for all browsers: playwright install\n"
57
+ f"Error: {error_msg}"
58
+ )
59
+ elif "PLAYWRIGHT_BROWSERS_PATH" in error_msg:
60
+ return False, (
61
+ f"Playwright browser path issue.\n"
62
+ f"Run: playwright install chromium\n"
63
+ f"Error: {error_msg}"
64
+ )
65
+ else:
66
+ return False, f"Playwright check failed: {error_msg}"
67
+
68
+ finally:
69
+ if browser:
70
+ await browser.close()
71
+ if playwright:
72
+ await playwright.stop()
73
+
74
+
75
+ async def check_web_browsing() -> tuple[bool, str]:
76
+ """
77
+ Test that web browsing actually works by fetching a simple page.
78
+
79
+ Returns:
80
+ tuple[bool, str]: (success, message)
81
+ """
82
+ try:
83
+ from playwright.async_api import async_playwright
84
+ except ImportError:
85
+ return False, "Playwright not installed"
86
+
87
+ playwright = None
88
+ browser = None
89
+ try:
90
+ playwright = await async_playwright().start()
91
+ browser = await playwright.chromium.launch(headless=True)
92
+ page = await browser.new_page()
93
+
94
+ # Try to navigate to a simple, reliable URL
95
+ # Use example.com as it's specifically designed for testing
96
+ response = await page.goto("https://example.com", timeout=30000)
97
+
98
+ if response is None:
99
+ return False, "No response received from test page"
100
+
101
+ if response.status != 200:
102
+ return False, f"Test page returned status {response.status}"
103
+
104
+ # Verify we got actual content
105
+ title = await page.title()
106
+ if not title:
107
+ return False, "Could not read page title"
108
+
109
+ text = await page.inner_text("body")
110
+ if not text or len(text) < 10:
111
+ return False, "Could not read page content"
112
+
113
+ await page.close()
114
+
115
+ return True, f"Web browsing verified (loaded: example.com, title: {title})"
116
+
117
+ except Exception as e:
118
+ error_msg = str(e)
119
+ if "net::ERR_" in error_msg:
120
+ return False, f"Network error - check internet connection: {error_msg}"
121
+ elif "Timeout" in error_msg:
122
+ return False, f"Timeout while loading test page: {error_msg}"
123
+ else:
124
+ return False, f"Web browsing test failed: {error_msg}"
125
+
126
+ finally:
127
+ if browser:
128
+ await browser.close()
129
+ if playwright:
130
+ await playwright.stop()
131
+
132
+
133
+ async def run_startup_checks(skip_web_test: bool = False) -> tuple[bool, list[str]]:
134
+ """
135
+ Run all Playwright startup checks.
136
+
137
+ Args:
138
+ skip_web_test: If True, skip the web browsing test (useful for offline mode)
139
+
140
+ Returns:
141
+ tuple[bool, list[str]]: (all_passed, list of messages)
142
+ """
143
+ messages = []
144
+ all_passed = True
145
+
146
+ # Check 1: Playwright installation
147
+ logger.info("Checking Playwright installation...")
148
+ success, msg = await check_playwright_installation()
149
+ messages.append(f"[{'OK' if success else 'FAIL'}] Playwright: {msg}")
150
+ if not success:
151
+ all_passed = False
152
+ return all_passed, messages
153
+
154
+ # Check 2: Web browsing (optional)
155
+ if not skip_web_test:
156
+ logger.info("Checking web browsing capability...")
157
+ success, msg = await check_web_browsing()
158
+ messages.append(f"[{'OK' if success else 'FAIL'}] Web browsing: {msg}")
159
+ if not success:
160
+ all_passed = False
161
+ else:
162
+ messages.append("[SKIP] Web browsing test skipped")
163
+
164
+ return all_passed, messages
165
+
166
+
167
+ def check_playwright_sync() -> tuple[bool, list[str]]:
168
+ """
169
+ Synchronous wrapper for startup checks.
170
+
171
+ Returns:
172
+ tuple[bool, list[str]]: (all_passed, list of messages)
173
+ """
174
+ return asyncio.run(run_startup_checks())
175
+
176
+
177
+ def require_playwright_or_exit(skip_web_test: bool = False):
178
+ """
179
+ Check Playwright and exit with error if not working.
180
+
181
+ This function should be called at server/daemon startup.
182
+ It will print error messages and exit(1) if Playwright is not properly configured.
183
+
184
+ Args:
185
+ skip_web_test: If True, skip the web browsing test
186
+ """
187
+ print("\n=== Playwright Browser Check ===")
188
+
189
+ try:
190
+ all_passed, messages = asyncio.run(run_startup_checks(skip_web_test))
191
+ except KeyboardInterrupt:
192
+ print("\nCheck cancelled by user")
193
+ sys.exit(1)
194
+ except Exception as e:
195
+ print(f"\nUnexpected error during Playwright check: {e}")
196
+ print("\nPlease ensure Playwright is installed:")
197
+ print(" pip install playwright")
198
+ print(" playwright install chromium")
199
+ sys.exit(1)
200
+
201
+ for msg in messages:
202
+ print(msg)
203
+
204
+ if not all_passed:
205
+ print("\n" + "=" * 50)
206
+ print("PLAYWRIGHT CHECK FAILED")
207
+ print("=" * 50)
208
+ print("\nSquidBot requires a working Playwright browser for web browsing.")
209
+ print("\nTo fix this, run the following commands:")
210
+ print(" pip install playwright")
211
+ print(" playwright install chromium")
212
+ print("\nIf you're using Poetry:")
213
+ print(" poetry add playwright")
214
+ print(" poetry run playwright install chromium")
215
+ print("\nServer will not start until browser is properly configured.")
216
+ print("=" * 50 + "\n")
217
+ sys.exit(1)
218
+
219
+ print("=== All checks passed ===\n")
220
+ return True
221
+
222
+
223
+ if __name__ == "__main__":
224
+ """Run checks directly for testing."""
225
+ import argparse
226
+
227
+ parser = argparse.ArgumentParser(description="Check Playwright installation")
228
+ parser.add_argument(
229
+ "--skip-web", action="store_true", help="Skip web browsing test"
230
+ )
231
+ args = parser.parse_args()
232
+
233
+ require_playwright_or_exit(skip_web_test=args.skip_web)
@@ -0,0 +1,47 @@
1
+ """
2
+ SquidBot Plugin System
3
+
4
+ Provides a modular plugin architecture for extending SquidBot functionality.
5
+ """
6
+
7
+ from .base import Plugin, PluginApi, PluginManifest
8
+ from .hooks import AfterToolCallEvent # Event types; Global instances
9
+ from .hooks import (AgentEndEvent, BeforeAgentStartEvent,
10
+ BeforeAgentStartResult, BeforeToolCallEvent,
11
+ BeforeToolCallResult, HookContext, HookName, HookRegistry,
12
+ HookRunner, MessageReceivedEvent, MessageSendingEvent,
13
+ MessageSendingResult, MessageSentEvent, SessionEndEvent,
14
+ SessionStartEvent, get_hook_registry, get_hook_runner)
15
+ from .loader import (PluginRegistry, get_registry, load_builtin_plugins,
16
+ load_external_plugins)
17
+
18
+ __all__ = [
19
+ # Base
20
+ "Plugin",
21
+ "PluginApi",
22
+ "PluginManifest",
23
+ # Hooks
24
+ "HookName",
25
+ "HookContext",
26
+ "HookRegistry",
27
+ "HookRunner",
28
+ "BeforeAgentStartEvent",
29
+ "BeforeAgentStartResult",
30
+ "AgentEndEvent",
31
+ "MessageReceivedEvent",
32
+ "MessageSendingEvent",
33
+ "MessageSendingResult",
34
+ "MessageSentEvent",
35
+ "BeforeToolCallEvent",
36
+ "BeforeToolCallResult",
37
+ "AfterToolCallEvent",
38
+ "SessionStartEvent",
39
+ "SessionEndEvent",
40
+ "get_hook_registry",
41
+ "get_hook_runner",
42
+ # Loader
43
+ "PluginRegistry",
44
+ "get_registry",
45
+ "load_builtin_plugins",
46
+ "load_external_plugins",
47
+ ]
@@ -0,0 +1,96 @@
1
+ """
2
+ Plugin Base Classes
3
+
4
+ Defines the plugin interface and base classes for SquidBot plugins.
5
+ Inspired by OpenClaw's plugin architecture.
6
+ """
7
+
8
+ from abc import ABC, abstractmethod
9
+ from dataclasses import dataclass, field
10
+ from typing import TYPE_CHECKING, Any, Awaitable, Callable
11
+
12
+ from ..tools.base import Tool
13
+
14
+ if TYPE_CHECKING:
15
+ from .hooks import HookName, HookRegistry
16
+
17
+
18
+ @dataclass
19
+ class PluginManifest:
20
+ """Plugin metadata and configuration."""
21
+
22
+ id: str
23
+ name: str
24
+ description: str
25
+ version: str = "1.0.0"
26
+ author: str = ""
27
+ config_schema: dict = field(default_factory=dict)
28
+
29
+
30
+ class PluginApi:
31
+ """API provided to plugins for registration."""
32
+
33
+ def __init__(self, plugin_id: str, hook_registry: "HookRegistry"):
34
+ self._plugin_id = plugin_id
35
+ self._hook_registry = hook_registry
36
+
37
+ def on(
38
+ self,
39
+ hook_name: "HookName",
40
+ handler: Callable[..., Awaitable[Any] | Any],
41
+ priority: int = 0,
42
+ ) -> None:
43
+ """Register a hook handler.
44
+
45
+ Args:
46
+ hook_name: The hook to listen for
47
+ handler: Async or sync function(event, context) -> result
48
+ priority: Higher priority handlers run first (default: 0)
49
+ """
50
+ self._hook_registry.register(
51
+ plugin_id=self._plugin_id,
52
+ hook_name=hook_name,
53
+ handler=handler,
54
+ priority=priority,
55
+ )
56
+
57
+
58
+ class Plugin(ABC):
59
+ """Base class for all plugins."""
60
+
61
+ @property
62
+ @abstractmethod
63
+ def manifest(self) -> PluginManifest:
64
+ """Return plugin manifest with metadata."""
65
+ pass
66
+
67
+ @abstractmethod
68
+ def get_tools(self) -> list[Tool]:
69
+ """Return list of tools provided by this plugin."""
70
+ pass
71
+
72
+ def register_hooks(self, api: PluginApi) -> None:
73
+ """Register hooks using the plugin API. Override to add hooks.
74
+
75
+ Example:
76
+ def register_hooks(self, api: PluginApi) -> None:
77
+ api.on(HookName.BEFORE_TOOL_CALL, self.on_before_tool, priority=10)
78
+ api.on(HookName.AFTER_TOOL_CALL, self.on_after_tool)
79
+ """
80
+ pass
81
+
82
+ def activate(self) -> None:
83
+ """Called when plugin is activated. Override for initialization."""
84
+ pass
85
+
86
+ def deactivate(self) -> None:
87
+ """Called when plugin is deactivated. Override for cleanup."""
88
+ pass
89
+
90
+ def validate_config(self, config: dict) -> tuple[bool, str | None]:
91
+ """Validate plugin configuration. Returns (is_valid, error_message)."""
92
+ return True, None
93
+
94
+ def get_config_defaults(self) -> dict[str, Any]:
95
+ """Return default configuration values."""
96
+ return {}