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.
- squidbot/__init__.py +5 -0
- squidbot/agent.py +263 -0
- squidbot/channels.py +271 -0
- squidbot/character.py +83 -0
- squidbot/client.py +318 -0
- squidbot/config.py +148 -0
- squidbot/daemon.py +310 -0
- squidbot/lanes.py +41 -0
- squidbot/main.py +157 -0
- squidbot/memory_db.py +706 -0
- squidbot/playwright_check.py +233 -0
- squidbot/plugins/__init__.py +47 -0
- squidbot/plugins/base.py +96 -0
- squidbot/plugins/hooks.py +416 -0
- squidbot/plugins/loader.py +248 -0
- squidbot/plugins/web3_plugin.py +407 -0
- squidbot/scheduler.py +214 -0
- squidbot/server.py +487 -0
- squidbot/session.py +609 -0
- squidbot/skills.py +141 -0
- squidbot/skills_template/reminder/SKILL.md +13 -0
- squidbot/skills_template/search/SKILL.md +11 -0
- squidbot/skills_template/summarize/SKILL.md +14 -0
- squidbot/tools/__init__.py +100 -0
- squidbot/tools/base.py +42 -0
- squidbot/tools/browser.py +311 -0
- squidbot/tools/coding.py +599 -0
- squidbot/tools/cron.py +218 -0
- squidbot/tools/memory_tool.py +152 -0
- squidbot/tools/web_search.py +50 -0
- squidbot-0.1.0.dist-info/METADATA +542 -0
- squidbot-0.1.0.dist-info/RECORD +34 -0
- squidbot-0.1.0.dist-info/WHEEL +4 -0
- squidbot-0.1.0.dist-info/entry_points.txt +4 -0
|
@@ -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
|
+
]
|
squidbot/plugins/base.py
ADDED
|
@@ -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 {}
|