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/tools/cron.py ADDED
@@ -0,0 +1,218 @@
1
+ """Cron/scheduling tools for proactive messaging."""
2
+
3
+ import json
4
+ from datetime import datetime, timedelta
5
+ from typing import Optional
6
+
7
+ from ..config import CRON_FILE
8
+ from .base import Tool
9
+
10
+
11
+ def load_cron_jobs() -> list[dict]:
12
+ """Load cron jobs from file."""
13
+ if not CRON_FILE.exists():
14
+ return []
15
+ try:
16
+ with open(CRON_FILE, "r") as f:
17
+ return json.load(f)
18
+ except (json.JSONDecodeError, IOError):
19
+ return []
20
+
21
+
22
+ def save_cron_jobs(jobs: list[dict]) -> None:
23
+ """Save cron jobs to file."""
24
+ with open(CRON_FILE, "w") as f:
25
+ json.dump(jobs, f, indent=2, default=str)
26
+
27
+
28
+ class CronCreateTool(Tool):
29
+ """Create a scheduled reminder/task."""
30
+
31
+ @property
32
+ def name(self) -> str:
33
+ return "cron_create"
34
+
35
+ @property
36
+ def description(self) -> str:
37
+ return "Create a scheduled task. The agent will execute the task (using tools if needed) at the specified time and send the result to the user."
38
+
39
+ @property
40
+ def parameters(self) -> dict:
41
+ return {
42
+ "type": "object",
43
+ "properties": {
44
+ "message": {
45
+ "type": "string",
46
+ "description": "Task/prompt for the agent to execute when triggered (e.g., 'Check TechCrunch for latest news and summarize')",
47
+ },
48
+ "delay_minutes": {
49
+ "type": "integer",
50
+ "description": "Minutes from now to trigger (for one-time reminders)",
51
+ },
52
+ "interval_seconds": {
53
+ "type": "integer",
54
+ "description": "Repeat every N seconds (for recurring interval tasks, e.g., 20 for every 20 seconds)",
55
+ },
56
+ "cron_expression": {
57
+ "type": "string",
58
+ "description": "Cron expression for recurring tasks (e.g., '0 9 * * *' for daily at 9am)",
59
+ },
60
+ "recurring": {
61
+ "type": "boolean",
62
+ "description": "Whether this is a recurring task",
63
+ "default": False,
64
+ },
65
+ },
66
+ "required": ["message"],
67
+ }
68
+
69
+ async def execute(
70
+ self,
71
+ message: str,
72
+ delay_minutes: Optional[int] = None,
73
+ interval_seconds: Optional[int] = None,
74
+ cron_expression: Optional[str] = None,
75
+ recurring: bool = False,
76
+ ) -> str:
77
+ jobs = load_cron_jobs()
78
+
79
+ job = {
80
+ "id": len(jobs) + 1,
81
+ "message": message,
82
+ "created_at": datetime.now().isoformat(),
83
+ "recurring": recurring,
84
+ "enabled": True,
85
+ }
86
+
87
+ if delay_minutes:
88
+ trigger_at = datetime.now() + timedelta(minutes=delay_minutes)
89
+ job["trigger_at"] = trigger_at.isoformat()
90
+ job["type"] = "one_time"
91
+ elif interval_seconds:
92
+ job["interval_seconds"] = interval_seconds
93
+ job["type"] = "interval"
94
+ job["recurring"] = True
95
+ # Set next trigger time
96
+ job["next_trigger"] = (
97
+ datetime.now() + timedelta(seconds=interval_seconds)
98
+ ).isoformat()
99
+ elif cron_expression:
100
+ job["cron_expression"] = cron_expression
101
+ job["type"] = "cron"
102
+ else:
103
+ return "Error: Must specify delay_minutes, interval_seconds, or cron_expression"
104
+
105
+ jobs.append(job)
106
+ save_cron_jobs(jobs)
107
+
108
+ if delay_minutes:
109
+ return f"Reminder set for {delay_minutes} minutes from now (id={job['id']}): {message}"
110
+ elif interval_seconds:
111
+ return f"Interval task scheduled every {interval_seconds} seconds (id={job['id']}): {message}"
112
+ else:
113
+ return f"Recurring task scheduled with cron '{cron_expression}' (id={job['id']}): {message}"
114
+
115
+
116
+ class CronListTool(Tool):
117
+ """List scheduled tasks."""
118
+
119
+ @property
120
+ def name(self) -> str:
121
+ return "cron_list"
122
+
123
+ @property
124
+ def description(self) -> str:
125
+ return "List all scheduled reminders and tasks."
126
+
127
+ @property
128
+ def parameters(self) -> dict:
129
+ return {"type": "object", "properties": {}, "required": []}
130
+
131
+ async def execute(self) -> str:
132
+ jobs = load_cron_jobs()
133
+ if not jobs:
134
+ return "No scheduled tasks."
135
+
136
+ lines = [f"Scheduled tasks ({len(jobs)}):"]
137
+ for job in jobs:
138
+ status = "enabled" if job.get("enabled", True) else "disabled"
139
+ job_type = job.get("type", "unknown")
140
+
141
+ if job_type == "one_time":
142
+ lines.append(
143
+ f"- [{job['id']}] {status} | At {job['trigger_at']}: {job['message']}"
144
+ )
145
+ elif job_type == "interval":
146
+ lines.append(
147
+ f"- [{job['id']}] {status} | Every {job['interval_seconds']}s: {job['message']}"
148
+ )
149
+ elif job_type == "cron":
150
+ lines.append(
151
+ f"- [{job['id']}] {status} | Cron {job['cron_expression']}: {job['message']}"
152
+ )
153
+ else:
154
+ lines.append(f"- [{job['id']}] {status} | {job['message']}")
155
+
156
+ return "\n".join(lines)
157
+
158
+
159
+ class CronDeleteTool(Tool):
160
+ """Delete a scheduled task."""
161
+
162
+ @property
163
+ def name(self) -> str:
164
+ return "cron_delete"
165
+
166
+ @property
167
+ def description(self) -> str:
168
+ return "Delete a scheduled task by ID. Use cron_list first to see job IDs. Use cron_clear to delete ALL jobs."
169
+
170
+ @property
171
+ def parameters(self) -> dict:
172
+ return {
173
+ "type": "object",
174
+ "properties": {
175
+ "job_id": {"type": "integer", "description": "ID of the job to delete"}
176
+ },
177
+ "required": ["job_id"],
178
+ }
179
+
180
+ async def execute(self, job_id: int) -> str:
181
+ # Ensure job_id is an integer (LLM might pass string)
182
+ job_id = int(job_id)
183
+
184
+ jobs = load_cron_jobs()
185
+ original_len = len(jobs)
186
+ jobs = [j for j in jobs if j["id"] != job_id]
187
+
188
+ if len(jobs) == original_len:
189
+ return f"No job found with id={job_id}"
190
+
191
+ save_cron_jobs(jobs)
192
+ return f"Deleted job id={job_id}"
193
+
194
+
195
+ class CronClearTool(Tool):
196
+ """Clear all scheduled tasks."""
197
+
198
+ @property
199
+ def name(self) -> str:
200
+ return "cron_clear"
201
+
202
+ @property
203
+ def description(self) -> str:
204
+ return "Delete ALL scheduled tasks. Use this to clear all cron jobs at once."
205
+
206
+ @property
207
+ def parameters(self) -> dict:
208
+ return {"type": "object", "properties": {}, "required": []}
209
+
210
+ async def execute(self) -> str:
211
+ jobs = load_cron_jobs()
212
+ count = len(jobs)
213
+
214
+ if count == 0:
215
+ return "No scheduled tasks to clear."
216
+
217
+ save_cron_jobs([])
218
+ return f"Cleared all {count} scheduled task(s)."
@@ -0,0 +1,152 @@
1
+ """Memory tools using SQLite with vector search."""
2
+
3
+ from ..memory_db import (add_memory, delete_memory, load_all_memories,
4
+ search_memory, search_memory_semantic)
5
+ from .base import Tool
6
+
7
+
8
+ class MemoryAddTool(Tool):
9
+ """Add information to persistent memory."""
10
+
11
+ @property
12
+ def name(self) -> str:
13
+ return "memory_add"
14
+
15
+ @property
16
+ def description(self) -> str:
17
+ return "Store information in persistent memory with semantic search support. Use this to remember facts, preferences, or important details."
18
+
19
+ @property
20
+ def parameters(self) -> dict:
21
+ return {
22
+ "type": "object",
23
+ "properties": {
24
+ "content": {
25
+ "type": "string",
26
+ "description": "The information to remember",
27
+ },
28
+ "category": {
29
+ "type": "string",
30
+ "description": "Optional category (e.g., 'preference', 'fact', 'task')",
31
+ },
32
+ },
33
+ "required": ["content"],
34
+ }
35
+
36
+ async def execute(self, content: str, category: str = None) -> str:
37
+ entry = await add_memory(content, category)
38
+ return f"Stored in memory (id={entry['id']}): {content}"
39
+
40
+
41
+ class MemorySearchTool(Tool):
42
+ """Search persistent memory using semantic similarity."""
43
+
44
+ @property
45
+ def name(self) -> str:
46
+ return "memory_search"
47
+
48
+ @property
49
+ def description(self) -> str:
50
+ return "Search persistent memory using semantic similarity. Finds related memories even if exact words don't match."
51
+
52
+ @property
53
+ def parameters(self) -> dict:
54
+ return {
55
+ "type": "object",
56
+ "properties": {
57
+ "query": {"type": "string", "description": "Search query"},
58
+ "semantic": {
59
+ "type": "boolean",
60
+ "description": "Use semantic search (default true)",
61
+ "default": True,
62
+ },
63
+ },
64
+ "required": ["query"],
65
+ }
66
+
67
+ async def execute(self, query: str, semantic: bool = True) -> str:
68
+ if semantic:
69
+ results = await search_memory_semantic(query)
70
+ else:
71
+ results = await search_memory(query)
72
+
73
+ if not results:
74
+ return f"No memory entries found for: {query}"
75
+
76
+ lines = [f"Found {len(results)} memories:"]
77
+ for r in results:
78
+ cat = f"[{r['category']}] " if r.get("category") else ""
79
+ sim = f" (similarity: {r['similarity']:.2f})" if "similarity" in r else ""
80
+ lines.append(f"- {cat}{r['content']}{sim}")
81
+
82
+ return "\n".join(lines)
83
+
84
+
85
+ class MemoryListTool(Tool):
86
+ """List all memories."""
87
+
88
+ @property
89
+ def name(self) -> str:
90
+ return "memory_list"
91
+
92
+ @property
93
+ def description(self) -> str:
94
+ return "List all stored memories."
95
+
96
+ @property
97
+ def parameters(self) -> dict:
98
+ return {
99
+ "type": "object",
100
+ "properties": {
101
+ "limit": {
102
+ "type": "integer",
103
+ "description": "Maximum number of memories to return",
104
+ "default": 20,
105
+ }
106
+ },
107
+ "required": [],
108
+ }
109
+
110
+ async def execute(self, limit: int = 20) -> str:
111
+ entries = await load_all_memories(limit)
112
+
113
+ if not entries:
114
+ return "No memories stored yet."
115
+
116
+ lines = [f"Total memories: {len(entries)}"]
117
+ for e in entries:
118
+ cat = f"[{e['category']}] " if e.get("category") else ""
119
+ lines.append(f"- [{e['id']}] {cat}{e['content']}")
120
+
121
+ return "\n".join(lines)
122
+
123
+
124
+ class MemoryDeleteTool(Tool):
125
+ """Delete a memory by ID."""
126
+
127
+ @property
128
+ def name(self) -> str:
129
+ return "memory_delete"
130
+
131
+ @property
132
+ def description(self) -> str:
133
+ return "Delete a specific memory by its ID."
134
+
135
+ @property
136
+ def parameters(self) -> dict:
137
+ return {
138
+ "type": "object",
139
+ "properties": {
140
+ "memory_id": {
141
+ "type": "integer",
142
+ "description": "ID of the memory to delete",
143
+ }
144
+ },
145
+ "required": ["memory_id"],
146
+ }
147
+
148
+ async def execute(self, memory_id: int) -> str:
149
+ success = await delete_memory(memory_id)
150
+ if success:
151
+ return f"Deleted memory id={memory_id}"
152
+ return f"No memory found with id={memory_id}"
@@ -0,0 +1,50 @@
1
+ """Web search tool using DuckDuckGo."""
2
+
3
+ from duckduckgo_search import DDGS
4
+
5
+ from .base import Tool
6
+
7
+
8
+ class WebSearchTool(Tool):
9
+ """Search the web using DuckDuckGo."""
10
+
11
+ @property
12
+ def name(self) -> str:
13
+ return "web_search"
14
+
15
+ @property
16
+ def description(self) -> str:
17
+ return "Search the web for general information using DuckDuckGo. Use this for general queries. For visiting a SPECIFIC website (like techcrunch.com), use browser_navigate + browser_get_text instead."
18
+
19
+ @property
20
+ def parameters(self) -> dict:
21
+ return {
22
+ "type": "object",
23
+ "properties": {
24
+ "query": {"type": "string", "description": "Search query"},
25
+ "max_results": {
26
+ "type": "integer",
27
+ "description": "Maximum number of results (default 5)",
28
+ "default": 5,
29
+ },
30
+ },
31
+ "required": ["query"],
32
+ }
33
+
34
+ async def execute(self, query: str, max_results: int = 5) -> str:
35
+ try:
36
+ with DDGS() as ddgs:
37
+ results = list(ddgs.text(query, max_results=max_results))
38
+
39
+ if not results:
40
+ return f"No results found for: {query}"
41
+
42
+ lines = [f"Search results for: {query}\n"]
43
+ for i, r in enumerate(results, 1):
44
+ lines.append(f"{i}. {r['title']}")
45
+ lines.append(f" URL: {r['href']}")
46
+ lines.append(f" {r['body']}\n")
47
+
48
+ return "\n".join(lines)
49
+ except Exception as e:
50
+ return f"Search error: {str(e)}"