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,245 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Storage package for LollmsBot.
|
|
3
|
+
|
|
4
|
+
This package provides persistence layer for conversation history and agent state.
|
|
5
|
+
It includes pluggable storage backends with SQLite as the default implementation,
|
|
6
|
+
allowing easy swapping for PostgreSQL, Redis, or custom storage solutions.
|
|
7
|
+
|
|
8
|
+
The storage layer supports:
|
|
9
|
+
- Conversation persistence per user with full message history
|
|
10
|
+
- Agent state serialization for checkpoint/resume capabilities
|
|
11
|
+
- Multiple backend registration and runtime selection
|
|
12
|
+
|
|
13
|
+
Example:
|
|
14
|
+
>>> from lollmsbot.storage import StorageRegistry, SqliteStore
|
|
15
|
+
>>> # Register default SQLite backend
|
|
16
|
+
>>> StorageRegistry.register("sqlite", SqliteStore("bot.db"))
|
|
17
|
+
>>> # Use in agent or gateway
|
|
18
|
+
>>> store = StorageRegistry.get("sqlite")
|
|
19
|
+
>>> store.save_conversation("user123", messages)
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from abc import ABC, abstractmethod
|
|
23
|
+
from dataclasses import dataclass
|
|
24
|
+
from datetime import datetime
|
|
25
|
+
from typing import Any, Dict, List, Optional, Protocol
|
|
26
|
+
|
|
27
|
+
from lollmsbot.storage.sqlite_store import SqliteStore
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class BaseStorage(ABC):
|
|
31
|
+
"""Abstract base class for storage backends.
|
|
32
|
+
|
|
33
|
+
All storage implementations must inherit from this class and implement
|
|
34
|
+
the core persistence methods for conversations and agent state.
|
|
35
|
+
|
|
36
|
+
The storage backend is responsible for:
|
|
37
|
+
- Persisting conversation history per user
|
|
38
|
+
- Saving and loading agent state for resumption
|
|
39
|
+
- Handling connection lifecycle and error conditions
|
|
40
|
+
|
|
41
|
+
Attributes:
|
|
42
|
+
backend_name: Identifier for the storage backend type.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
backend_name: str = "abstract"
|
|
46
|
+
|
|
47
|
+
@abstractmethod
|
|
48
|
+
async def save_conversation(self, user_id: str, messages: List[Dict[str, Any]]) -> bool:
|
|
49
|
+
"""Save or append conversation messages for a user.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
user_id: Unique identifier for the user.
|
|
53
|
+
messages: List of message dictionaries with 'role', 'content', 'timestamp' keys.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
True if save was successful, False otherwise.
|
|
57
|
+
"""
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
@abstractmethod
|
|
61
|
+
async def get_conversation(self, user_id: str, limit: Optional[int] = None) -> List[Dict[str, Any]]:
|
|
62
|
+
"""Retrieve conversation history for a user.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
user_id: Unique identifier for the user.
|
|
66
|
+
limit: Maximum number of recent messages to retrieve (None for all).
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
List of message dictionaries ordered chronologically.
|
|
70
|
+
"""
|
|
71
|
+
pass
|
|
72
|
+
|
|
73
|
+
@abstractmethod
|
|
74
|
+
async def save_agent_state(self, agent_id: str, state: Dict[str, Any]) -> bool:
|
|
75
|
+
"""Serialize and save agent state for later resumption.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
agent_id: Unique identifier for the agent.
|
|
79
|
+
state: Dictionary containing all serializable agent state.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
True if save was successful, False otherwise.
|
|
83
|
+
"""
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
@abstractmethod
|
|
87
|
+
async def load_agent_state(self, agent_id: str) -> Optional[Dict[str, Any]]:
|
|
88
|
+
"""Load previously saved agent state.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
agent_id: Unique identifier for the agent.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
State dictionary if found, None otherwise.
|
|
95
|
+
"""
|
|
96
|
+
pass
|
|
97
|
+
|
|
98
|
+
@abstractmethod
|
|
99
|
+
async def delete_conversation(self, user_id: str) -> bool:
|
|
100
|
+
"""Delete all conversation history for a user.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
user_id: Unique identifier for the user.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
True if deletion was successful, False otherwise.
|
|
107
|
+
"""
|
|
108
|
+
pass
|
|
109
|
+
|
|
110
|
+
@abstractmethod
|
|
111
|
+
async def delete_agent_state(self, agent_id: str) -> bool:
|
|
112
|
+
"""Delete saved state for an agent.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
agent_id: Unique identifier for the agent.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
True if deletion was successful, False otherwise.
|
|
119
|
+
"""
|
|
120
|
+
pass
|
|
121
|
+
|
|
122
|
+
@abstractmethod
|
|
123
|
+
async def close(self) -> None:
|
|
124
|
+
"""Close storage connections and cleanup resources."""
|
|
125
|
+
pass
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class StorageRegistry:
|
|
129
|
+
"""Registry for managing multiple storage backends.
|
|
130
|
+
|
|
131
|
+
Provides a centralized registry pattern for storage backend management,
|
|
132
|
+
allowing runtime selection and failover between different storage
|
|
133
|
+
implementations (SQLite, PostgreSQL, Redis, etc.).
|
|
134
|
+
|
|
135
|
+
The registry supports:
|
|
136
|
+
- Named backend registration
|
|
137
|
+
- Runtime backend retrieval
|
|
138
|
+
- Default backend assignment
|
|
139
|
+
- Bulk cleanup on shutdown
|
|
140
|
+
|
|
141
|
+
Example:
|
|
142
|
+
>>> StorageRegistry.register("primary", SqliteStore("primary.db"))
|
|
143
|
+
>>> StorageRegistry.register("cache", RedisStore("localhost:6379"))
|
|
144
|
+
>>> StorageRegistry.set_default("primary")
|
|
145
|
+
>>> store = StorageRegistry.get_default()
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
_backends: Dict[str, BaseStorage] = {}
|
|
149
|
+
_default: Optional[str] = None
|
|
150
|
+
|
|
151
|
+
@classmethod
|
|
152
|
+
def register(cls, name: str, backend: BaseStorage) -> None:
|
|
153
|
+
"""Register a storage backend with a unique name.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
name: Unique identifier for this backend instance.
|
|
157
|
+
backend: Configured storage backend instance.
|
|
158
|
+
|
|
159
|
+
Raises:
|
|
160
|
+
ValueError: If name is already registered.
|
|
161
|
+
"""
|
|
162
|
+
if name in cls._backends:
|
|
163
|
+
raise ValueError(f"Storage backend '{name}' is already registered")
|
|
164
|
+
cls._backends[name] = backend
|
|
165
|
+
|
|
166
|
+
# Set as default if first registration
|
|
167
|
+
if cls._default is None:
|
|
168
|
+
cls._default = name
|
|
169
|
+
|
|
170
|
+
@classmethod
|
|
171
|
+
def get(cls, name: str) -> Optional[BaseStorage]:
|
|
172
|
+
"""Retrieve a registered storage backend by name.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
name: Backend identifier used during registration.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
Storage backend instance or None if not found.
|
|
179
|
+
"""
|
|
180
|
+
return cls._backends.get(name)
|
|
181
|
+
|
|
182
|
+
@classmethod
|
|
183
|
+
def get_default(cls) -> Optional[BaseStorage]:
|
|
184
|
+
"""Retrieve the default storage backend.
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
Default backend instance or None if none set.
|
|
188
|
+
"""
|
|
189
|
+
if cls._default is None:
|
|
190
|
+
return None
|
|
191
|
+
return cls._backends.get(cls._default)
|
|
192
|
+
|
|
193
|
+
@classmethod
|
|
194
|
+
def set_default(cls, name: str) -> None:
|
|
195
|
+
"""Set the default storage backend.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
name: Name of registered backend to set as default.
|
|
199
|
+
|
|
200
|
+
Raises:
|
|
201
|
+
ValueError: If backend is not registered.
|
|
202
|
+
"""
|
|
203
|
+
if name not in cls._backends:
|
|
204
|
+
raise ValueError(f"Storage backend '{name}' is not registered")
|
|
205
|
+
cls._default = name
|
|
206
|
+
|
|
207
|
+
@classmethod
|
|
208
|
+
def unregister(cls, name: str) -> Optional[BaseStorage]:
|
|
209
|
+
"""Remove a registered backend and cleanup.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
name: Backend identifier to remove.
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
Removed backend instance or None if not found.
|
|
216
|
+
"""
|
|
217
|
+
backend = cls._backends.pop(name, None)
|
|
218
|
+
if backend and cls._default == name:
|
|
219
|
+
# Clear default if removed, set to another if available
|
|
220
|
+
cls._default = next(iter(cls._backends), None)
|
|
221
|
+
return backend
|
|
222
|
+
|
|
223
|
+
@classmethod
|
|
224
|
+
def list_backends(cls) -> List[str]:
|
|
225
|
+
"""List all registered backend names.
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
List of registered backend identifiers.
|
|
229
|
+
"""
|
|
230
|
+
return list(cls._backends.keys())
|
|
231
|
+
|
|
232
|
+
@classmethod
|
|
233
|
+
async def close_all(cls) -> None:
|
|
234
|
+
"""Close all registered storage backends."""
|
|
235
|
+
for backend in cls._backends.values():
|
|
236
|
+
await backend.close()
|
|
237
|
+
cls._backends.clear()
|
|
238
|
+
cls._default = None
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
__all__ = [
|
|
242
|
+
"BaseStorage",
|
|
243
|
+
"SqliteStore",
|
|
244
|
+
"StorageRegistry",
|
|
245
|
+
]
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SQLite storage backend for LollmsBot.
|
|
3
|
+
|
|
4
|
+
Provides persistent storage using SQLite with async support via aiosqlite.
|
|
5
|
+
Implements conversation history and agent state persistence.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import sqlite3
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, Dict, List, Optional
|
|
13
|
+
|
|
14
|
+
import aiosqlite
|
|
15
|
+
|
|
16
|
+
from lollmsbot.storage import BaseStorage
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SqliteStore(BaseStorage):
|
|
20
|
+
"""SQLite-based storage backend.
|
|
21
|
+
|
|
22
|
+
Implements BaseStorage interface using SQLite for persistence.
|
|
23
|
+
Supports async operations and JSON serialization for complex data.
|
|
24
|
+
|
|
25
|
+
Attributes:
|
|
26
|
+
db_path: Path to the SQLite database file.
|
|
27
|
+
_connection: Async database connection (initialized in _init_db).
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
backend_name: str = "sqlite"
|
|
31
|
+
|
|
32
|
+
def __init__(self, db_path: str = "lollmsbot.db") -> None:
|
|
33
|
+
"""Initialize SqliteStore with database path.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
db_path: Path to SQLite database file. Defaults to 'lollmsbot.db'.
|
|
37
|
+
"""
|
|
38
|
+
self.db_path: str = db_path
|
|
39
|
+
self._connection: Optional[aiosqlite.Connection] = None
|
|
40
|
+
|
|
41
|
+
async def _init_db(self) -> None:
|
|
42
|
+
"""Initialize database connection and create tables.
|
|
43
|
+
|
|
44
|
+
Creates required tables if they don't exist:
|
|
45
|
+
- conversations: Stores conversation metadata
|
|
46
|
+
- messages: Stores individual messages with foreign key to conversations
|
|
47
|
+
- agent_states: Stores serialized agent state
|
|
48
|
+
"""
|
|
49
|
+
self._connection = await aiosqlite.connect(self.db_path)
|
|
50
|
+
|
|
51
|
+
# Enable foreign keys
|
|
52
|
+
await self._connection.execute("PRAGMA foreign_keys = ON")
|
|
53
|
+
|
|
54
|
+
# Create conversations table
|
|
55
|
+
await self._connection.execute("""
|
|
56
|
+
CREATE TABLE IF NOT EXISTS conversations (
|
|
57
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
58
|
+
user_id TEXT NOT NULL,
|
|
59
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
60
|
+
)
|
|
61
|
+
""")
|
|
62
|
+
|
|
63
|
+
# Create messages table
|
|
64
|
+
await self._connection.execute("""
|
|
65
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
66
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
67
|
+
conversation_id INTEGER NOT NULL,
|
|
68
|
+
role TEXT NOT NULL,
|
|
69
|
+
content TEXT NOT NULL,
|
|
70
|
+
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
71
|
+
FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE
|
|
72
|
+
)
|
|
73
|
+
""")
|
|
74
|
+
|
|
75
|
+
# Create agent_states table
|
|
76
|
+
await self._connection.execute("""
|
|
77
|
+
CREATE TABLE IF NOT EXISTS agent_states (
|
|
78
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
79
|
+
agent_id TEXT NOT NULL UNIQUE,
|
|
80
|
+
state_json TEXT NOT NULL,
|
|
81
|
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
82
|
+
)
|
|
83
|
+
""")
|
|
84
|
+
|
|
85
|
+
# Create indexes for performance
|
|
86
|
+
await self._connection.execute("""
|
|
87
|
+
CREATE INDEX IF NOT EXISTS idx_conversations_user_id ON conversations(user_id)
|
|
88
|
+
""")
|
|
89
|
+
await self._connection.execute("""
|
|
90
|
+
CREATE INDEX IF NOT EXISTS idx_messages_conversation_id ON messages(conversation_id)
|
|
91
|
+
""")
|
|
92
|
+
await self._connection.execute("""
|
|
93
|
+
CREATE INDEX IF NOT EXISTS idx_agent_states_agent_id ON agent_states(agent_id)
|
|
94
|
+
""")
|
|
95
|
+
|
|
96
|
+
await self._connection.commit()
|
|
97
|
+
|
|
98
|
+
async def _ensure_initialized(self) -> aiosqlite.Connection:
|
|
99
|
+
"""Ensure database is initialized and return connection.
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
Active database connection.
|
|
103
|
+
"""
|
|
104
|
+
if self._connection is None:
|
|
105
|
+
await self._init_db()
|
|
106
|
+
return self._connection
|
|
107
|
+
|
|
108
|
+
async def save_conversation(self, user_id: str, messages: List[Dict[str, Any]]) -> bool:
|
|
109
|
+
"""Save or append conversation messages for a user.
|
|
110
|
+
|
|
111
|
+
Creates a new conversation entry and stores all messages.
|
|
112
|
+
Previous conversations for the user are preserved.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
user_id: Unique identifier for the user.
|
|
116
|
+
messages: List of message dictionaries with 'role', 'content', 'timestamp' keys.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
True if save was successful, False otherwise.
|
|
120
|
+
"""
|
|
121
|
+
try:
|
|
122
|
+
conn = await self._ensure_initialized()
|
|
123
|
+
|
|
124
|
+
async with conn.cursor() as cursor:
|
|
125
|
+
# Insert conversation record
|
|
126
|
+
await cursor.execute(
|
|
127
|
+
"INSERT INTO conversations (user_id, created_at) VALUES (?, ?)",
|
|
128
|
+
(user_id, datetime.now().isoformat())
|
|
129
|
+
)
|
|
130
|
+
conversation_id = cursor.lastrowid
|
|
131
|
+
|
|
132
|
+
# Insert all messages
|
|
133
|
+
for msg in messages:
|
|
134
|
+
timestamp = msg.get("timestamp")
|
|
135
|
+
if isinstance(timestamp, datetime):
|
|
136
|
+
timestamp = timestamp.isoformat()
|
|
137
|
+
elif timestamp is None:
|
|
138
|
+
timestamp = datetime.now().isoformat()
|
|
139
|
+
|
|
140
|
+
await cursor.execute(
|
|
141
|
+
"INSERT INTO messages (conversation_id, role, content, timestamp) VALUES (?, ?, ?, ?)",
|
|
142
|
+
(conversation_id, msg.get("role", "unknown"), msg.get("content", ""), timestamp)
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
await conn.commit()
|
|
146
|
+
return True
|
|
147
|
+
|
|
148
|
+
except Exception:
|
|
149
|
+
return False
|
|
150
|
+
|
|
151
|
+
async def get_conversation(self, user_id: str, limit: Optional[int] = None) -> List[Dict[str, Any]]:
|
|
152
|
+
"""Retrieve conversation history for a user.
|
|
153
|
+
|
|
154
|
+
Returns messages from the most recent conversation for the user.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
user_id: Unique identifier for the user.
|
|
158
|
+
limit: Maximum number of recent messages to retrieve (None for all).
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
List of message dictionaries ordered chronologically.
|
|
162
|
+
"""
|
|
163
|
+
try:
|
|
164
|
+
conn = await self._ensure_initialized()
|
|
165
|
+
|
|
166
|
+
async with conn.cursor() as cursor:
|
|
167
|
+
# Find the most recent conversation for this user
|
|
168
|
+
await cursor.execute(
|
|
169
|
+
"SELECT id FROM conversations WHERE user_id = ? ORDER BY created_at DESC LIMIT 1",
|
|
170
|
+
(user_id,)
|
|
171
|
+
)
|
|
172
|
+
row = await cursor.fetchone()
|
|
173
|
+
|
|
174
|
+
if row is None:
|
|
175
|
+
return []
|
|
176
|
+
|
|
177
|
+
conversation_id = row[0]
|
|
178
|
+
|
|
179
|
+
# Build query with optional limit
|
|
180
|
+
query = """
|
|
181
|
+
SELECT role, content, timestamp
|
|
182
|
+
FROM messages
|
|
183
|
+
WHERE conversation_id = ?
|
|
184
|
+
ORDER BY timestamp ASC
|
|
185
|
+
"""
|
|
186
|
+
params: List[Any] = [conversation_id]
|
|
187
|
+
|
|
188
|
+
if limit is not None:
|
|
189
|
+
query = f"""
|
|
190
|
+
SELECT role, content, timestamp
|
|
191
|
+
FROM messages
|
|
192
|
+
WHERE conversation_id = ?
|
|
193
|
+
ORDER BY timestamp DESC
|
|
194
|
+
LIMIT ?
|
|
195
|
+
"""
|
|
196
|
+
params.append(limit)
|
|
197
|
+
|
|
198
|
+
await cursor.execute(query, params)
|
|
199
|
+
rows = await cursor.fetchall()
|
|
200
|
+
|
|
201
|
+
# Reverse if we used DESC with limit to get chronological order
|
|
202
|
+
if limit is not None:
|
|
203
|
+
rows = list(reversed(rows))
|
|
204
|
+
|
|
205
|
+
messages = []
|
|
206
|
+
for role, content, timestamp in rows:
|
|
207
|
+
msg: Dict[str, Any] = {
|
|
208
|
+
"role": role,
|
|
209
|
+
"content": content,
|
|
210
|
+
}
|
|
211
|
+
if timestamp:
|
|
212
|
+
msg["timestamp"] = timestamp
|
|
213
|
+
messages.append(msg)
|
|
214
|
+
|
|
215
|
+
return messages
|
|
216
|
+
|
|
217
|
+
except Exception:
|
|
218
|
+
return []
|
|
219
|
+
|
|
220
|
+
async def save_agent_state(self, agent_id: str, state: Dict[str, Any]) -> bool:
|
|
221
|
+
"""Serialize and save agent state for later resumption.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
agent_id: Unique identifier for the agent.
|
|
225
|
+
state: Dictionary containing all serializable agent state.
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
True if save was successful, False otherwise.
|
|
229
|
+
"""
|
|
230
|
+
try:
|
|
231
|
+
conn = await self._ensure_initialized()
|
|
232
|
+
|
|
233
|
+
# Serialize state to JSON
|
|
234
|
+
state_json = json.dumps(state, default=str)
|
|
235
|
+
|
|
236
|
+
async with conn.cursor() as cursor:
|
|
237
|
+
# Use UPSERT pattern for SQLite
|
|
238
|
+
await cursor.execute("""
|
|
239
|
+
INSERT INTO agent_states (agent_id, state_json, updated_at)
|
|
240
|
+
VALUES (?, ?, ?)
|
|
241
|
+
ON CONFLICT(agent_id) DO UPDATE SET
|
|
242
|
+
state_json = excluded.state_json,
|
|
243
|
+
updated_at = excluded.updated_at
|
|
244
|
+
""", (agent_id, state_json, datetime.now().isoformat()))
|
|
245
|
+
|
|
246
|
+
await conn.commit()
|
|
247
|
+
return True
|
|
248
|
+
|
|
249
|
+
except Exception:
|
|
250
|
+
return False
|
|
251
|
+
|
|
252
|
+
async def load_agent_state(self, agent_id: str) -> Optional[Dict[str, Any]]:
|
|
253
|
+
"""Load previously saved agent state.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
agent_id: Unique identifier for the agent.
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
State dictionary if found, None otherwise.
|
|
260
|
+
"""
|
|
261
|
+
try:
|
|
262
|
+
conn = await self._ensure_initialized()
|
|
263
|
+
|
|
264
|
+
async with conn.cursor() as cursor:
|
|
265
|
+
await cursor.execute(
|
|
266
|
+
"SELECT state_json FROM agent_states WHERE agent_id = ?",
|
|
267
|
+
(agent_id,)
|
|
268
|
+
)
|
|
269
|
+
row = await cursor.fetchone()
|
|
270
|
+
|
|
271
|
+
if row is None:
|
|
272
|
+
return None
|
|
273
|
+
|
|
274
|
+
# Deserialize JSON
|
|
275
|
+
state: Dict[str, Any] = json.loads(row[0])
|
|
276
|
+
return state
|
|
277
|
+
|
|
278
|
+
except Exception:
|
|
279
|
+
return None
|
|
280
|
+
|
|
281
|
+
async def delete_conversation(self, user_id: str) -> bool:
|
|
282
|
+
"""Delete all conversation history for a user.
|
|
283
|
+
|
|
284
|
+
Args:
|
|
285
|
+
user_id: Unique identifier for the user.
|
|
286
|
+
|
|
287
|
+
Returns:
|
|
288
|
+
True if deletion was successful, False otherwise.
|
|
289
|
+
"""
|
|
290
|
+
try:
|
|
291
|
+
conn = await self._ensure_initialized()
|
|
292
|
+
|
|
293
|
+
async with conn.cursor() as cursor:
|
|
294
|
+
# Delete conversations (cascades to messages via foreign key)
|
|
295
|
+
await cursor.execute(
|
|
296
|
+
"DELETE FROM conversations WHERE user_id = ?",
|
|
297
|
+
(user_id,)
|
|
298
|
+
)
|
|
299
|
+
await conn.commit()
|
|
300
|
+
return True
|
|
301
|
+
|
|
302
|
+
except Exception:
|
|
303
|
+
return False
|
|
304
|
+
|
|
305
|
+
async def delete_agent_state(self, agent_id: str) -> bool:
|
|
306
|
+
"""Delete saved state for an agent.
|
|
307
|
+
|
|
308
|
+
Args:
|
|
309
|
+
agent_id: Unique identifier for the agent.
|
|
310
|
+
|
|
311
|
+
Returns:
|
|
312
|
+
True if deletion was successful, False otherwise.
|
|
313
|
+
"""
|
|
314
|
+
try:
|
|
315
|
+
conn = await self._ensure_initialized()
|
|
316
|
+
|
|
317
|
+
async with conn.cursor() as cursor:
|
|
318
|
+
await cursor.execute(
|
|
319
|
+
"DELETE FROM agent_states WHERE agent_id = ?",
|
|
320
|
+
(agent_id,)
|
|
321
|
+
)
|
|
322
|
+
await conn.commit()
|
|
323
|
+
return cursor.rowcount > 0
|
|
324
|
+
|
|
325
|
+
except Exception:
|
|
326
|
+
return False
|
|
327
|
+
|
|
328
|
+
async def close(self) -> None:
|
|
329
|
+
"""Close storage connections and cleanup resources."""
|
|
330
|
+
if self._connection is not None:
|
|
331
|
+
await self._connection.close()
|
|
332
|
+
self._connection = None
|