lollmsbot 0.0.1__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.
- lollmsbot/__init__.py +1 -0
- lollmsbot/agent.py +1682 -0
- lollmsbot/channels/__init__.py +22 -0
- lollmsbot/channels/discord.py +408 -0
- lollmsbot/channels/http_api.py +449 -0
- lollmsbot/channels/telegram.py +272 -0
- lollmsbot/cli.py +217 -0
- lollmsbot/config.py +90 -0
- lollmsbot/gateway.py +606 -0
- lollmsbot/guardian.py +692 -0
- lollmsbot/heartbeat.py +826 -0
- lollmsbot/lollms_client.py +37 -0
- lollmsbot/skills.py +1483 -0
- lollmsbot/soul.py +482 -0
- lollmsbot/storage/__init__.py +245 -0
- lollmsbot/storage/sqlite_store.py +332 -0
- lollmsbot/tools/__init__.py +151 -0
- lollmsbot/tools/calendar.py +717 -0
- lollmsbot/tools/filesystem.py +663 -0
- lollmsbot/tools/http.py +498 -0
- lollmsbot/tools/shell.py +519 -0
- lollmsbot/ui/__init__.py +11 -0
- lollmsbot/ui/__main__.py +121 -0
- lollmsbot/ui/app.py +1122 -0
- lollmsbot/ui/routes.py +39 -0
- lollmsbot/wizard.py +1493 -0
- lollmsbot-0.0.1.dist-info/METADATA +25 -0
- lollmsbot-0.0.1.dist-info/RECORD +32 -0
- lollmsbot-0.0.1.dist-info/WHEEL +5 -0
- lollmsbot-0.0.1.dist-info/entry_points.txt +2 -0
- lollmsbot-0.0.1.dist-info/licenses/LICENSE +201 -0
- lollmsbot-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,717 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Calendar tool for LollmsBot.
|
|
3
|
+
|
|
4
|
+
This module provides the CalendarTool class for managing calendar events
|
|
5
|
+
with in-memory storage and optional ICS file export/import support.
|
|
6
|
+
All datetime operations are timezone-aware.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import uuid
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any, Dict, List, Optional
|
|
15
|
+
|
|
16
|
+
from lollmsbot.agent import Tool, ToolResult, ToolError
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class Event:
|
|
21
|
+
"""Represents a calendar event.
|
|
22
|
+
|
|
23
|
+
Attributes:
|
|
24
|
+
id: Unique identifier for the event (UUID string).
|
|
25
|
+
title: Event title/summary.
|
|
26
|
+
start: Start datetime (timezone-aware).
|
|
27
|
+
end: End datetime (timezone-aware).
|
|
28
|
+
description: Optional event description.
|
|
29
|
+
created_at: Timestamp when the event was created.
|
|
30
|
+
updated_at: Timestamp when the event was last updated.
|
|
31
|
+
"""
|
|
32
|
+
id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
|
33
|
+
title: str = ""
|
|
34
|
+
start: datetime = field(default_factory=datetime.now)
|
|
35
|
+
end: datetime = field(default_factory=datetime.now)
|
|
36
|
+
description: str = ""
|
|
37
|
+
created_at: datetime = field(default_factory=datetime.now)
|
|
38
|
+
updated_at: datetime = field(default_factory=datetime.now)
|
|
39
|
+
|
|
40
|
+
def __post_init__(self) -> None:
|
|
41
|
+
"""Ensure datetime fields are timezone-aware."""
|
|
42
|
+
if self.start.tzinfo is None:
|
|
43
|
+
from datetime import timezone
|
|
44
|
+
self.start = self.start.replace(tzinfo=timezone.utc)
|
|
45
|
+
if self.end.tzinfo is None:
|
|
46
|
+
from datetime import timezone
|
|
47
|
+
self.end = self.end.replace(tzinfo=timezone.utc)
|
|
48
|
+
if self.created_at.tzinfo is None:
|
|
49
|
+
from datetime import timezone
|
|
50
|
+
self.created_at = self.created_at.replace(tzinfo=timezone.utc)
|
|
51
|
+
if self.updated_at.tzinfo is None:
|
|
52
|
+
from datetime import timezone
|
|
53
|
+
self.updated_at = self.updated_at.replace(tzinfo=timezone.utc)
|
|
54
|
+
|
|
55
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
56
|
+
"""Convert event to dictionary representation.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
Dictionary with event data.
|
|
60
|
+
"""
|
|
61
|
+
return {
|
|
62
|
+
"id": self.id,
|
|
63
|
+
"title": self.title,
|
|
64
|
+
"start": self.start.isoformat(),
|
|
65
|
+
"end": self.end.isoformat(),
|
|
66
|
+
"description": self.description,
|
|
67
|
+
"created_at": self.created_at.isoformat(),
|
|
68
|
+
"updated_at": self.updated_at.isoformat(),
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
@classmethod
|
|
72
|
+
def from_dict(cls, data: Dict[str, Any]) -> "Event":
|
|
73
|
+
"""Create event from dictionary representation.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
data: Dictionary with event data.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
New Event instance.
|
|
80
|
+
"""
|
|
81
|
+
def parse_datetime(value: Any) -> datetime:
|
|
82
|
+
if isinstance(value, str):
|
|
83
|
+
return datetime.fromisoformat(value)
|
|
84
|
+
return value
|
|
85
|
+
|
|
86
|
+
return cls(
|
|
87
|
+
id=data.get("id", str(uuid.uuid4())),
|
|
88
|
+
title=data.get("title", ""),
|
|
89
|
+
start=parse_datetime(data.get("start", datetime.now())),
|
|
90
|
+
end=parse_datetime(data.get("end", datetime.now())),
|
|
91
|
+
description=data.get("description", ""),
|
|
92
|
+
created_at=parse_datetime(data.get("created_at", datetime.now())),
|
|
93
|
+
updated_at=parse_datetime(data.get("updated_at", datetime.now())),
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
def to_ics(self) -> str:
|
|
97
|
+
"""Convert event to ICS format string.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
ICS VEVENT component as string.
|
|
101
|
+
"""
|
|
102
|
+
from datetime import timezone
|
|
103
|
+
|
|
104
|
+
def format_datetime(dt: datetime) -> str:
|
|
105
|
+
"""Format datetime for ICS (UTC or local with Z suffix)."""
|
|
106
|
+
utc_dt = dt.astimezone(timezone.utc)
|
|
107
|
+
return utc_dt.strftime("%Y%m%dT%H%M%SZ")
|
|
108
|
+
|
|
109
|
+
ics_lines = [
|
|
110
|
+
"BEGIN:VEVENT",
|
|
111
|
+
f"UID:{self.id}",
|
|
112
|
+
f"SUMMARY:{self.title}",
|
|
113
|
+
f"DTSTART:{format_datetime(self.start)}",
|
|
114
|
+
f"DTEND:{format_datetime(self.end)}",
|
|
115
|
+
]
|
|
116
|
+
|
|
117
|
+
if self.description:
|
|
118
|
+
# Escape special characters in description
|
|
119
|
+
escaped_desc = self.description.replace("\\", "\\\\").replace("\n", "\\n").replace(",", "\\,").replace(";", "\\;")
|
|
120
|
+
ics_lines.append(f"DESCRIPTION:{escaped_desc}")
|
|
121
|
+
|
|
122
|
+
ics_lines.append(f"DTSTAMP:{format_datetime(datetime.now(timezone.utc))}")
|
|
123
|
+
ics_lines.append("END:VEVENT")
|
|
124
|
+
|
|
125
|
+
return "\r\n".join(ics_lines)
|
|
126
|
+
|
|
127
|
+
@classmethod
|
|
128
|
+
def from_ics(cls, ics_data: str) -> "Event":
|
|
129
|
+
"""Parse event from ICS format string.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
ics_data: ICS VEVENT component as string.
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
New Event instance.
|
|
136
|
+
"""
|
|
137
|
+
from datetime import timezone
|
|
138
|
+
|
|
139
|
+
lines = ics_data.replace("\r\n", "\n").split("\n")
|
|
140
|
+
data: Dict[str, str] = {}
|
|
141
|
+
|
|
142
|
+
for line in lines:
|
|
143
|
+
if ":" in line and not line.startswith("BEGIN:") and not line.startswith("END:"):
|
|
144
|
+
key, value = line.split(":", 1)
|
|
145
|
+
# Handle property parameters (e.g., DTSTART;TZID=...)
|
|
146
|
+
if ";" in key:
|
|
147
|
+
key = key.split(";")[0]
|
|
148
|
+
data[key] = value
|
|
149
|
+
|
|
150
|
+
def parse_ics_datetime(value: str) -> datetime:
|
|
151
|
+
"""Parse ICS datetime format."""
|
|
152
|
+
value = value.strip()
|
|
153
|
+
if value.endswith("Z"):
|
|
154
|
+
# UTC datetime
|
|
155
|
+
return datetime.strptime(value, "%Y%m%dT%H%M%SZ").replace(tzinfo=timezone.utc)
|
|
156
|
+
elif "T" in value:
|
|
157
|
+
# Local datetime (assume UTC if no tz specified)
|
|
158
|
+
dt = datetime.strptime(value, "%Y%m%dT%H%M%S")
|
|
159
|
+
return dt.replace(tzinfo=timezone.utc)
|
|
160
|
+
else:
|
|
161
|
+
# Date only
|
|
162
|
+
dt = datetime.strptime(value, "%Y%m%d")
|
|
163
|
+
return dt.replace(tzinfo=timezone.utc)
|
|
164
|
+
|
|
165
|
+
# Unescape description
|
|
166
|
+
description = data.get("DESCRIPTION", "")
|
|
167
|
+
description = description.replace("\\n", "\n").replace("\\,", ",").replace("\\;", ";").replace("\\\\", "\\")
|
|
168
|
+
|
|
169
|
+
return cls(
|
|
170
|
+
id=data.get("UID", str(uuid.uuid4())),
|
|
171
|
+
title=data.get("SUMMARY", ""),
|
|
172
|
+
start=parse_ics_datetime(data.get("DTSTART", "")),
|
|
173
|
+
end=parse_ics_datetime(data.get("DTEND", "")),
|
|
174
|
+
description=description,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class CalendarTool(Tool):
|
|
179
|
+
"""Tool for managing calendar events with in-memory storage.
|
|
180
|
+
|
|
181
|
+
This tool provides CRUD operations for calendar events with optional
|
|
182
|
+
ICS file import/export support. All datetime operations are timezone-aware.
|
|
183
|
+
|
|
184
|
+
Attributes:
|
|
185
|
+
name: Unique identifier for the tool.
|
|
186
|
+
description: Human-readable description of what the tool does.
|
|
187
|
+
parameters: JSON Schema describing expected parameters for each method.
|
|
188
|
+
events: In-memory storage of events indexed by ID.
|
|
189
|
+
ics_file_path: Optional path for ICS file persistence.
|
|
190
|
+
"""
|
|
191
|
+
|
|
192
|
+
name: str = "calendar"
|
|
193
|
+
description: str = (
|
|
194
|
+
"Manage calendar events including listing, adding, and deleting events. "
|
|
195
|
+
"Supports filtering by date range and optional ICS file export/import. "
|
|
196
|
+
"All datetimes are handled with timezone awareness."
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
parameters: dict[str, Any] = {
|
|
200
|
+
"type": "object",
|
|
201
|
+
"properties": {
|
|
202
|
+
"operation": {
|
|
203
|
+
"type": "string",
|
|
204
|
+
"enum": ["get_events", "add_event", "delete_event", "export_ics", "import_ics"],
|
|
205
|
+
"description": "The calendar operation to perform",
|
|
206
|
+
},
|
|
207
|
+
"start": {
|
|
208
|
+
"type": "string",
|
|
209
|
+
"description": "Start datetime in ISO format (required for get_events, add_event)",
|
|
210
|
+
},
|
|
211
|
+
"end": {
|
|
212
|
+
"type": "string",
|
|
213
|
+
"description": "End datetime in ISO format (required for get_events, add_event)",
|
|
214
|
+
},
|
|
215
|
+
"title": {
|
|
216
|
+
"type": "string",
|
|
217
|
+
"description": "Event title (required for add_event)",
|
|
218
|
+
},
|
|
219
|
+
"description": {
|
|
220
|
+
"type": "string",
|
|
221
|
+
"description": "Event description (optional for add_event)",
|
|
222
|
+
},
|
|
223
|
+
"event_id": {
|
|
224
|
+
"type": "string",
|
|
225
|
+
"description": "Event ID (required for delete_event)",
|
|
226
|
+
},
|
|
227
|
+
"ics_path": {
|
|
228
|
+
"type": "string",
|
|
229
|
+
"description": "Path to ICS file (required for export_ics, import_ics)",
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
"required": ["operation"],
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
def __init__(
|
|
236
|
+
self,
|
|
237
|
+
ics_file_path: Optional[str] = None,
|
|
238
|
+
default_timezone: str = "UTC",
|
|
239
|
+
) -> None:
|
|
240
|
+
"""Initialize the CalendarTool.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
ics_file_path: Optional path for ICS file persistence.
|
|
244
|
+
default_timezone: Default timezone for datetime operations.
|
|
245
|
+
"""
|
|
246
|
+
self.events: Dict[str, Event] = {}
|
|
247
|
+
self.ics_file_path: Optional[Path] = Path(ics_file_path) if ics_file_path else None
|
|
248
|
+
self.default_timezone: str = default_timezone
|
|
249
|
+
|
|
250
|
+
# Load existing events from ICS if available
|
|
251
|
+
if self.ics_file_path and self.ics_file_path.exists():
|
|
252
|
+
asyncio.create_task(self._load_from_ics_async())
|
|
253
|
+
|
|
254
|
+
def _get_timezone(self, tz_name: Optional[str] = None) -> Any:
|
|
255
|
+
"""Get timezone object by name.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
tz_name: Timezone name (e.g., 'UTC', 'America/New_York').
|
|
259
|
+
Defaults to default_timezone if not specified.
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
Timezone object.
|
|
263
|
+
"""
|
|
264
|
+
import zoneinfo
|
|
265
|
+
|
|
266
|
+
tz = tz_name or self.default_timezone
|
|
267
|
+
try:
|
|
268
|
+
return zoneinfo.ZoneInfo(tz)
|
|
269
|
+
except zoneinfo.ZoneInfoNotFoundError:
|
|
270
|
+
# Fallback to UTC if timezone not found
|
|
271
|
+
return zoneinfo.ZoneInfo("UTC")
|
|
272
|
+
|
|
273
|
+
def _parse_datetime(self, value: str | datetime) -> datetime:
|
|
274
|
+
"""Parse datetime from string or return datetime object.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
value: ISO format string or datetime object.
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
Timezone-aware datetime object.
|
|
281
|
+
"""
|
|
282
|
+
if isinstance(value, datetime):
|
|
283
|
+
if value.tzinfo is None:
|
|
284
|
+
from datetime import timezone
|
|
285
|
+
return value.replace(tzinfo=timezone.utc)
|
|
286
|
+
return value
|
|
287
|
+
|
|
288
|
+
# Try ISO format parsing
|
|
289
|
+
try:
|
|
290
|
+
dt = datetime.fromisoformat(value)
|
|
291
|
+
if dt.tzinfo is None:
|
|
292
|
+
from datetime import timezone
|
|
293
|
+
dt = dt.replace(tzinfo=timezone.utc)
|
|
294
|
+
return dt
|
|
295
|
+
except ValueError:
|
|
296
|
+
# Try common formats
|
|
297
|
+
formats = [
|
|
298
|
+
"%Y-%m-%d %H:%M:%S",
|
|
299
|
+
"%Y-%m-%d",
|
|
300
|
+
"%d/%m/%Y %H:%M:%S",
|
|
301
|
+
"%d/%m/%Y",
|
|
302
|
+
]
|
|
303
|
+
for fmt in formats:
|
|
304
|
+
try:
|
|
305
|
+
dt = datetime.strptime(value, fmt)
|
|
306
|
+
from datetime import timezone
|
|
307
|
+
return dt.replace(tzinfo=timezone.utc)
|
|
308
|
+
except ValueError:
|
|
309
|
+
continue
|
|
310
|
+
|
|
311
|
+
raise ValueError(f"Unable to parse datetime: {value}")
|
|
312
|
+
|
|
313
|
+
async def get_events(
|
|
314
|
+
self,
|
|
315
|
+
start: Optional[str | datetime] = None,
|
|
316
|
+
end: Optional[str | datetime] = None,
|
|
317
|
+
) -> ToolResult:
|
|
318
|
+
"""Get events within a date range.
|
|
319
|
+
|
|
320
|
+
Args:
|
|
321
|
+
start: Start of date range (inclusive). If None, no lower bound.
|
|
322
|
+
end: End of date range (inclusive). If None, no upper bound.
|
|
323
|
+
|
|
324
|
+
Returns:
|
|
325
|
+
ToolResult with list of matching events.
|
|
326
|
+
"""
|
|
327
|
+
try:
|
|
328
|
+
parsed_start: Optional[datetime] = None
|
|
329
|
+
parsed_end: Optional[datetime] = None
|
|
330
|
+
|
|
331
|
+
if start:
|
|
332
|
+
parsed_start = self._parse_datetime(start)
|
|
333
|
+
if end:
|
|
334
|
+
parsed_end = self._parse_datetime(end)
|
|
335
|
+
|
|
336
|
+
matching_events: List[Event] = []
|
|
337
|
+
|
|
338
|
+
for event in self.events.values():
|
|
339
|
+
# Check if event overlaps with range
|
|
340
|
+
event_in_range = True
|
|
341
|
+
|
|
342
|
+
if parsed_start and event.end < parsed_start:
|
|
343
|
+
event_in_range = False
|
|
344
|
+
if parsed_end and event.start > parsed_end:
|
|
345
|
+
event_in_range = False
|
|
346
|
+
|
|
347
|
+
if event_in_range:
|
|
348
|
+
matching_events.append(event)
|
|
349
|
+
|
|
350
|
+
# Sort by start time
|
|
351
|
+
matching_events.sort(key=lambda e: e.start)
|
|
352
|
+
|
|
353
|
+
return ToolResult(
|
|
354
|
+
success=True,
|
|
355
|
+
output={
|
|
356
|
+
"count": len(matching_events),
|
|
357
|
+
"events": [e.to_dict() for e in matching_events],
|
|
358
|
+
},
|
|
359
|
+
error=None,
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
except ValueError as exc:
|
|
363
|
+
return ToolResult(
|
|
364
|
+
success=False,
|
|
365
|
+
output=None,
|
|
366
|
+
error=f"Invalid datetime format: {str(exc)}",
|
|
367
|
+
)
|
|
368
|
+
except Exception as exc:
|
|
369
|
+
return ToolResult(
|
|
370
|
+
success=False,
|
|
371
|
+
output=None,
|
|
372
|
+
error=f"Error retrieving events: {str(exc)}",
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
async def add_event(
|
|
376
|
+
self,
|
|
377
|
+
title: str,
|
|
378
|
+
start: str | datetime,
|
|
379
|
+
end: str | datetime,
|
|
380
|
+
description: str = "",
|
|
381
|
+
) -> ToolResult:
|
|
382
|
+
"""Add a new calendar event.
|
|
383
|
+
|
|
384
|
+
Args:
|
|
385
|
+
title: Event title/summary.
|
|
386
|
+
start: Event start datetime.
|
|
387
|
+
end: Event end datetime.
|
|
388
|
+
description: Optional event description.
|
|
389
|
+
|
|
390
|
+
Returns:
|
|
391
|
+
ToolResult with the created event data.
|
|
392
|
+
"""
|
|
393
|
+
try:
|
|
394
|
+
if not title:
|
|
395
|
+
return ToolResult(
|
|
396
|
+
success=False,
|
|
397
|
+
output=None,
|
|
398
|
+
error="Event title is required",
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
parsed_start = self._parse_datetime(start)
|
|
402
|
+
parsed_end = self._parse_datetime(end)
|
|
403
|
+
|
|
404
|
+
if parsed_end <= parsed_start:
|
|
405
|
+
return ToolResult(
|
|
406
|
+
success=False,
|
|
407
|
+
output=None,
|
|
408
|
+
error="End time must be after start time",
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
event = Event(
|
|
412
|
+
title=title,
|
|
413
|
+
start=parsed_start,
|
|
414
|
+
end=parsed_end,
|
|
415
|
+
description=description,
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
self.events[event.id] = event
|
|
419
|
+
|
|
420
|
+
# Persist to ICS if path is configured
|
|
421
|
+
if self.ics_file_path:
|
|
422
|
+
await self._save_to_ics_async()
|
|
423
|
+
|
|
424
|
+
return ToolResult(
|
|
425
|
+
success=True,
|
|
426
|
+
output={
|
|
427
|
+
"message": f"Event '{title}' created successfully",
|
|
428
|
+
"event": event.to_dict(),
|
|
429
|
+
},
|
|
430
|
+
error=None,
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
except ValueError as exc:
|
|
434
|
+
return ToolResult(
|
|
435
|
+
success=False,
|
|
436
|
+
output=None,
|
|
437
|
+
error=f"Invalid datetime format: {str(exc)}",
|
|
438
|
+
)
|
|
439
|
+
except Exception as exc:
|
|
440
|
+
return ToolResult(
|
|
441
|
+
success=False,
|
|
442
|
+
output=None,
|
|
443
|
+
error=f"Error creating event: {str(exc)}",
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
async def delete_event(self, event_id: str) -> ToolResult:
|
|
447
|
+
"""Delete a calendar event by ID.
|
|
448
|
+
|
|
449
|
+
Args:
|
|
450
|
+
event_id: UUID of the event to delete.
|
|
451
|
+
|
|
452
|
+
Returns:
|
|
453
|
+
ToolResult indicating success or failure.
|
|
454
|
+
"""
|
|
455
|
+
try:
|
|
456
|
+
if event_id not in self.events:
|
|
457
|
+
return ToolResult(
|
|
458
|
+
success=False,
|
|
459
|
+
output=None,
|
|
460
|
+
error=f"Event with ID '{event_id}' not found",
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
event = self.events.pop(event_id)
|
|
464
|
+
|
|
465
|
+
# Persist to ICS if path is configured
|
|
466
|
+
if self.ics_file_path:
|
|
467
|
+
await self._save_to_ics_async()
|
|
468
|
+
|
|
469
|
+
return ToolResult(
|
|
470
|
+
success=True,
|
|
471
|
+
output={
|
|
472
|
+
"message": f"Event '{event.title}' deleted successfully",
|
|
473
|
+
"deleted_event": event.to_dict(),
|
|
474
|
+
},
|
|
475
|
+
error=None,
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
except Exception as exc:
|
|
479
|
+
return ToolResult(
|
|
480
|
+
success=False,
|
|
481
|
+
output=None,
|
|
482
|
+
error=f"Error deleting event: {str(exc)}",
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
async def export_ics(self, path: Optional[str] = None) -> ToolResult:
|
|
486
|
+
"""Export all events to ICS file.
|
|
487
|
+
|
|
488
|
+
Args:
|
|
489
|
+
path: Output file path. Uses configured ics_file_path if not specified.
|
|
490
|
+
|
|
491
|
+
Returns:
|
|
492
|
+
ToolResult with export status and file path.
|
|
493
|
+
"""
|
|
494
|
+
try:
|
|
495
|
+
output_path = Path(path) if path else self.ics_file_path
|
|
496
|
+
|
|
497
|
+
if not output_path:
|
|
498
|
+
return ToolResult(
|
|
499
|
+
success=False,
|
|
500
|
+
output=None,
|
|
501
|
+
error="No output path specified. Provide path parameter or configure ics_file_path.",
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
# Ensure parent directory exists
|
|
505
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
506
|
+
|
|
507
|
+
# Build ICS content
|
|
508
|
+
ics_lines = [
|
|
509
|
+
"BEGIN:VCALENDAR",
|
|
510
|
+
"VERSION:2.0",
|
|
511
|
+
"PRODID:-//LollmsBot//Calendar Tool//EN",
|
|
512
|
+
"CALSCALE:GREGORIAN",
|
|
513
|
+
"METHOD:PUBLISH",
|
|
514
|
+
]
|
|
515
|
+
|
|
516
|
+
for event in sorted(self.events.values(), key=lambda e: e.start):
|
|
517
|
+
ics_lines.append(event.to_ics())
|
|
518
|
+
|
|
519
|
+
ics_lines.append("END:VCALENDAR")
|
|
520
|
+
|
|
521
|
+
# Write file
|
|
522
|
+
ics_content = "\r\n".join(ics_lines)
|
|
523
|
+
|
|
524
|
+
loop = asyncio.get_event_loop()
|
|
525
|
+
await loop.run_in_executor(
|
|
526
|
+
None,
|
|
527
|
+
lambda: output_path.write_text(ics_content, encoding="utf-8"),
|
|
528
|
+
)
|
|
529
|
+
|
|
530
|
+
return ToolResult(
|
|
531
|
+
success=True,
|
|
532
|
+
output={
|
|
533
|
+
"message": f"Exported {len(self.events)} events to {output_path}",
|
|
534
|
+
"path": str(output_path),
|
|
535
|
+
"event_count": len(self.events),
|
|
536
|
+
},
|
|
537
|
+
error=None,
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
except Exception as exc:
|
|
541
|
+
return ToolResult(
|
|
542
|
+
success=False,
|
|
543
|
+
output=None,
|
|
544
|
+
error=f"Error exporting ICS file: {str(exc)}",
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
async def import_ics(self, path: Optional[str] = None) -> ToolResult:
|
|
548
|
+
"""Import events from ICS file.
|
|
549
|
+
|
|
550
|
+
Args:
|
|
551
|
+
path: Input file path. Uses configured ics_file_path if not specified.
|
|
552
|
+
|
|
553
|
+
Returns:
|
|
554
|
+
ToolResult with import status and count of imported events.
|
|
555
|
+
"""
|
|
556
|
+
try:
|
|
557
|
+
input_path = Path(path) if path else self.ics_file_path
|
|
558
|
+
|
|
559
|
+
if not input_path:
|
|
560
|
+
return ToolResult(
|
|
561
|
+
success=False,
|
|
562
|
+
output=None,
|
|
563
|
+
error="No input path specified. Provide path parameter or configure ics_file_path.",
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
if not input_path.exists():
|
|
567
|
+
return ToolResult(
|
|
568
|
+
success=False,
|
|
569
|
+
output=None,
|
|
570
|
+
error=f"ICS file not found: {input_path}",
|
|
571
|
+
)
|
|
572
|
+
|
|
573
|
+
# Read file
|
|
574
|
+
loop = asyncio.get_event_loop()
|
|
575
|
+
ics_content = await loop.run_in_executor(
|
|
576
|
+
None,
|
|
577
|
+
lambda: input_path.read_text(encoding="utf-8"),
|
|
578
|
+
)
|
|
579
|
+
|
|
580
|
+
# Parse VEVENT blocks
|
|
581
|
+
imported_count = 0
|
|
582
|
+
in_event = False
|
|
583
|
+
event_lines: List[str] = []
|
|
584
|
+
|
|
585
|
+
for line in ics_content.replace("\r\n", "\n").split("\n"):
|
|
586
|
+
if line.startswith("BEGIN:VEVENT"):
|
|
587
|
+
in_event = True
|
|
588
|
+
event_lines = [line]
|
|
589
|
+
elif line.startswith("END:VEVENT"):
|
|
590
|
+
in_event = False
|
|
591
|
+
event_lines.append(line)
|
|
592
|
+
try:
|
|
593
|
+
event = Event.from_ics("\n".join(event_lines))
|
|
594
|
+
self.events[event.id] = event
|
|
595
|
+
imported_count += 1
|
|
596
|
+
except Exception:
|
|
597
|
+
# Skip malformed events
|
|
598
|
+
pass
|
|
599
|
+
elif in_event:
|
|
600
|
+
event_lines.append(line)
|
|
601
|
+
|
|
602
|
+
return ToolResult(
|
|
603
|
+
success=True,
|
|
604
|
+
output={
|
|
605
|
+
"message": f"Imported {imported_count} events from {input_path}",
|
|
606
|
+
"path": str(input_path),
|
|
607
|
+
"imported_count": imported_count,
|
|
608
|
+
"total_events": len(self.events),
|
|
609
|
+
},
|
|
610
|
+
error=None,
|
|
611
|
+
)
|
|
612
|
+
|
|
613
|
+
except Exception as exc:
|
|
614
|
+
return ToolResult(
|
|
615
|
+
success=False,
|
|
616
|
+
output=None,
|
|
617
|
+
error=f"Error importing ICS file: {str(exc)}",
|
|
618
|
+
)
|
|
619
|
+
|
|
620
|
+
async def _save_to_ics_async(self) -> None:
|
|
621
|
+
"""Persist events to ICS file asynchronously."""
|
|
622
|
+
if self.ics_file_path:
|
|
623
|
+
await self.export_ics(str(self.ics_file_path))
|
|
624
|
+
|
|
625
|
+
async def _load_from_ics_async(self) -> None:
|
|
626
|
+
"""Load events from ICS file asynchronously."""
|
|
627
|
+
if self.ics_file_path and self.ics_file_path.exists():
|
|
628
|
+
await self.import_ics(str(self.ics_file_path))
|
|
629
|
+
|
|
630
|
+
async def execute(self, **params: Any) -> ToolResult:
|
|
631
|
+
"""Execute a calendar operation based on parameters.
|
|
632
|
+
|
|
633
|
+
This is the main entry point for the Tool base class. It dispatches
|
|
634
|
+
to the appropriate method based on the 'operation' parameter.
|
|
635
|
+
|
|
636
|
+
Args:
|
|
637
|
+
**params: Parameters must include:
|
|
638
|
+
- operation: One of 'get_events', 'add_event', 'delete_event',
|
|
639
|
+
'export_ics', 'import_ics'
|
|
640
|
+
- Additional parameters depend on the operation
|
|
641
|
+
|
|
642
|
+
Returns:
|
|
643
|
+
ToolResult from the executed operation.
|
|
644
|
+
|
|
645
|
+
Raises:
|
|
646
|
+
ToolError: If the operation is unknown or parameters are invalid.
|
|
647
|
+
"""
|
|
648
|
+
operation = params.get("operation")
|
|
649
|
+
|
|
650
|
+
if not operation:
|
|
651
|
+
return ToolResult(
|
|
652
|
+
success=False,
|
|
653
|
+
output=None,
|
|
654
|
+
error="Missing required parameter: 'operation'",
|
|
655
|
+
)
|
|
656
|
+
|
|
657
|
+
# Dispatch to appropriate method
|
|
658
|
+
if operation == "get_events":
|
|
659
|
+
return await self.get_events(
|
|
660
|
+
start=params.get("start"),
|
|
661
|
+
end=params.get("end"),
|
|
662
|
+
)
|
|
663
|
+
|
|
664
|
+
elif operation == "add_event":
|
|
665
|
+
title = params.get("title")
|
|
666
|
+
start = params.get("start")
|
|
667
|
+
end = params.get("end")
|
|
668
|
+
|
|
669
|
+
if not title:
|
|
670
|
+
return ToolResult(
|
|
671
|
+
success=False,
|
|
672
|
+
output=None,
|
|
673
|
+
error="Missing required parameter: 'title'",
|
|
674
|
+
)
|
|
675
|
+
if not start:
|
|
676
|
+
return ToolResult(
|
|
677
|
+
success=False,
|
|
678
|
+
output=None,
|
|
679
|
+
error="Missing required parameter: 'start'",
|
|
680
|
+
)
|
|
681
|
+
if not end:
|
|
682
|
+
return ToolResult(
|
|
683
|
+
success=False,
|
|
684
|
+
output=None,
|
|
685
|
+
error="Missing required parameter: 'end'",
|
|
686
|
+
)
|
|
687
|
+
|
|
688
|
+
return await self.add_event(
|
|
689
|
+
title=title,
|
|
690
|
+
start=start,
|
|
691
|
+
end=end,
|
|
692
|
+
description=params.get("description", ""),
|
|
693
|
+
)
|
|
694
|
+
|
|
695
|
+
elif operation == "delete_event":
|
|
696
|
+
event_id = params.get("event_id")
|
|
697
|
+
if not event_id:
|
|
698
|
+
return ToolResult(
|
|
699
|
+
success=False,
|
|
700
|
+
output=None,
|
|
701
|
+
error="Missing required parameter: 'event_id'",
|
|
702
|
+
)
|
|
703
|
+
return await self.delete_event(event_id)
|
|
704
|
+
|
|
705
|
+
elif operation == "export_ics":
|
|
706
|
+
return await self.export_ics(params.get("ics_path"))
|
|
707
|
+
|
|
708
|
+
elif operation == "import_ics":
|
|
709
|
+
return await self.import_ics(params.get("ics_path"))
|
|
710
|
+
|
|
711
|
+
else:
|
|
712
|
+
return ToolResult(
|
|
713
|
+
success=False,
|
|
714
|
+
output=None,
|
|
715
|
+
error=f"Unknown operation: '{operation}'. "
|
|
716
|
+
f"Valid operations are: get_events, add_event, delete_event, export_ics, import_ics",
|
|
717
|
+
)
|