swarms 7.7.4__py3-none-any.whl → 7.7.6__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.
- swarms/communication/__init__.py +0 -0
- swarms/communication/duckdb_wrap.py +1046 -0
- swarms/communication/sqlite_wrap.py +813 -0
- swarms/prompts/max_loop_prompt.py +48 -0
- swarms/prompts/react_base_prompt.py +41 -0
- swarms/structs/agent.py +70 -109
- swarms/structs/concurrent_workflow.py +329 -214
- swarms/structs/conversation.py +123 -6
- swarms/structs/groupchat.py +0 -12
- swarms/structs/hybrid_hiearchical_peer_swarm.py +19 -2
- swarms/structs/multi_model_gpu_manager.py +0 -1
- swarms/structs/swarm_arange.py +2 -2
- {swarms-7.7.4.dist-info → swarms-7.7.6.dist-info}/METADATA +1 -1
- {swarms-7.7.4.dist-info → swarms-7.7.6.dist-info}/RECORD +17 -12
- {swarms-7.7.4.dist-info → swarms-7.7.6.dist-info}/LICENSE +0 -0
- {swarms-7.7.4.dist-info → swarms-7.7.6.dist-info}/WHEEL +0 -0
- {swarms-7.7.4.dist-info → swarms-7.7.6.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,1046 @@
|
|
1
|
+
import duckdb
|
2
|
+
import json
|
3
|
+
import datetime
|
4
|
+
from typing import List, Optional, Union, Dict
|
5
|
+
from pathlib import Path
|
6
|
+
import threading
|
7
|
+
from contextlib import contextmanager
|
8
|
+
import logging
|
9
|
+
from dataclasses import dataclass
|
10
|
+
from enum import Enum
|
11
|
+
import uuid
|
12
|
+
import yaml
|
13
|
+
|
14
|
+
try:
|
15
|
+
from loguru import logger
|
16
|
+
|
17
|
+
LOGURU_AVAILABLE = True
|
18
|
+
except ImportError:
|
19
|
+
LOGURU_AVAILABLE = False
|
20
|
+
|
21
|
+
|
22
|
+
class MessageType(Enum):
|
23
|
+
"""Enum for different types of messages in the conversation."""
|
24
|
+
|
25
|
+
SYSTEM = "system"
|
26
|
+
USER = "user"
|
27
|
+
ASSISTANT = "assistant"
|
28
|
+
FUNCTION = "function"
|
29
|
+
TOOL = "tool"
|
30
|
+
|
31
|
+
|
32
|
+
@dataclass
|
33
|
+
class Message:
|
34
|
+
"""Data class representing a message in the conversation."""
|
35
|
+
|
36
|
+
role: str
|
37
|
+
content: Union[str, dict, list]
|
38
|
+
timestamp: Optional[str] = None
|
39
|
+
message_type: Optional[MessageType] = None
|
40
|
+
metadata: Optional[Dict] = None
|
41
|
+
token_count: Optional[int] = None
|
42
|
+
|
43
|
+
class Config:
|
44
|
+
arbitrary_types_allowed = True
|
45
|
+
|
46
|
+
|
47
|
+
class DateTimeEncoder(json.JSONEncoder):
|
48
|
+
"""Custom JSON encoder for handling datetime objects."""
|
49
|
+
|
50
|
+
def default(self, obj):
|
51
|
+
if isinstance(obj, datetime.datetime):
|
52
|
+
return obj.isoformat()
|
53
|
+
return super().default(obj)
|
54
|
+
|
55
|
+
|
56
|
+
class DuckDBConversation:
|
57
|
+
"""
|
58
|
+
A production-grade DuckDB wrapper class for managing conversation history.
|
59
|
+
This class provides persistent storage for conversations with various features
|
60
|
+
like message tracking, timestamps, and metadata support.
|
61
|
+
|
62
|
+
Attributes:
|
63
|
+
db_path (str): Path to the DuckDB database file
|
64
|
+
table_name (str): Name of the table to store conversations
|
65
|
+
enable_timestamps (bool): Whether to track message timestamps
|
66
|
+
enable_logging (bool): Whether to enable logging
|
67
|
+
use_loguru (bool): Whether to use loguru for logging
|
68
|
+
max_retries (int): Maximum number of retries for database operations
|
69
|
+
connection_timeout (float): Timeout for database connections
|
70
|
+
current_conversation_id (str): Current active conversation ID
|
71
|
+
"""
|
72
|
+
|
73
|
+
def __init__(
|
74
|
+
self,
|
75
|
+
db_path: Union[str, Path] = "conversations.duckdb",
|
76
|
+
table_name: str = "conversations",
|
77
|
+
enable_timestamps: bool = True,
|
78
|
+
enable_logging: bool = True,
|
79
|
+
use_loguru: bool = True,
|
80
|
+
max_retries: int = 3,
|
81
|
+
connection_timeout: float = 5.0,
|
82
|
+
):
|
83
|
+
self.db_path = Path(db_path)
|
84
|
+
self.table_name = table_name
|
85
|
+
self.enable_timestamps = enable_timestamps
|
86
|
+
self.enable_logging = enable_logging
|
87
|
+
self.use_loguru = use_loguru and LOGURU_AVAILABLE
|
88
|
+
self.max_retries = max_retries
|
89
|
+
self.connection_timeout = connection_timeout
|
90
|
+
self.current_conversation_id = None
|
91
|
+
self._lock = threading.Lock()
|
92
|
+
|
93
|
+
# Setup logging
|
94
|
+
if self.enable_logging:
|
95
|
+
if self.use_loguru:
|
96
|
+
self.logger = logger
|
97
|
+
else:
|
98
|
+
self.logger = logging.getLogger(__name__)
|
99
|
+
handler = logging.StreamHandler()
|
100
|
+
formatter = logging.Formatter(
|
101
|
+
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
102
|
+
)
|
103
|
+
handler.setFormatter(formatter)
|
104
|
+
self.logger.addHandler(handler)
|
105
|
+
self.logger.setLevel(logging.INFO)
|
106
|
+
|
107
|
+
self._init_db()
|
108
|
+
self.start_new_conversation()
|
109
|
+
|
110
|
+
def _generate_conversation_id(self) -> str:
|
111
|
+
"""Generate a unique conversation ID using UUID and timestamp."""
|
112
|
+
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
113
|
+
unique_id = str(uuid.uuid4())[:8]
|
114
|
+
return f"conv_{timestamp}_{unique_id}"
|
115
|
+
|
116
|
+
def start_new_conversation(self) -> str:
|
117
|
+
"""
|
118
|
+
Start a new conversation and return its ID.
|
119
|
+
|
120
|
+
Returns:
|
121
|
+
str: The new conversation ID
|
122
|
+
"""
|
123
|
+
self.current_conversation_id = (
|
124
|
+
self._generate_conversation_id()
|
125
|
+
)
|
126
|
+
return self.current_conversation_id
|
127
|
+
|
128
|
+
def _init_db(self):
|
129
|
+
"""Initialize the database and create necessary tables."""
|
130
|
+
with self._get_connection() as conn:
|
131
|
+
conn.execute(
|
132
|
+
f"""
|
133
|
+
CREATE TABLE IF NOT EXISTS {self.table_name} (
|
134
|
+
id BIGINT PRIMARY KEY,
|
135
|
+
role VARCHAR NOT NULL,
|
136
|
+
content VARCHAR NOT NULL,
|
137
|
+
timestamp TIMESTAMP,
|
138
|
+
message_type VARCHAR,
|
139
|
+
metadata VARCHAR,
|
140
|
+
token_count INTEGER,
|
141
|
+
conversation_id VARCHAR,
|
142
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
143
|
+
)
|
144
|
+
"""
|
145
|
+
)
|
146
|
+
|
147
|
+
@contextmanager
|
148
|
+
def _get_connection(self):
|
149
|
+
"""Context manager for database connections with retry logic."""
|
150
|
+
conn = None
|
151
|
+
for attempt in range(self.max_retries):
|
152
|
+
try:
|
153
|
+
conn = duckdb.connect(str(self.db_path))
|
154
|
+
yield conn
|
155
|
+
break
|
156
|
+
except Exception as e:
|
157
|
+
if attempt == self.max_retries - 1:
|
158
|
+
raise
|
159
|
+
if self.enable_logging:
|
160
|
+
self.logger.warning(
|
161
|
+
f"Database connection attempt {attempt + 1} failed: {e}"
|
162
|
+
)
|
163
|
+
if conn:
|
164
|
+
conn.close()
|
165
|
+
conn = None
|
166
|
+
|
167
|
+
def add(
|
168
|
+
self,
|
169
|
+
role: str,
|
170
|
+
content: Union[str, dict, list],
|
171
|
+
message_type: Optional[MessageType] = None,
|
172
|
+
metadata: Optional[Dict] = None,
|
173
|
+
token_count: Optional[int] = None,
|
174
|
+
) -> int:
|
175
|
+
"""
|
176
|
+
Add a message to the current conversation.
|
177
|
+
|
178
|
+
Args:
|
179
|
+
role (str): The role of the speaker
|
180
|
+
content (Union[str, dict, list]): The content of the message
|
181
|
+
message_type (Optional[MessageType]): Type of the message
|
182
|
+
metadata (Optional[Dict]): Additional metadata for the message
|
183
|
+
token_count (Optional[int]): Number of tokens in the message
|
184
|
+
|
185
|
+
Returns:
|
186
|
+
int: The ID of the inserted message
|
187
|
+
"""
|
188
|
+
timestamp = (
|
189
|
+
datetime.datetime.now().isoformat()
|
190
|
+
if self.enable_timestamps
|
191
|
+
else None
|
192
|
+
)
|
193
|
+
|
194
|
+
if isinstance(content, (dict, list)):
|
195
|
+
content = json.dumps(content)
|
196
|
+
|
197
|
+
with self._get_connection() as conn:
|
198
|
+
# Get the next ID
|
199
|
+
result = conn.execute(
|
200
|
+
f"SELECT COALESCE(MAX(id), 0) + 1 as next_id FROM {self.table_name}"
|
201
|
+
).fetchone()
|
202
|
+
next_id = result[0]
|
203
|
+
|
204
|
+
# Insert the message
|
205
|
+
conn.execute(
|
206
|
+
f"""
|
207
|
+
INSERT INTO {self.table_name}
|
208
|
+
(id, role, content, timestamp, message_type, metadata, token_count, conversation_id)
|
209
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
210
|
+
""",
|
211
|
+
(
|
212
|
+
next_id,
|
213
|
+
role,
|
214
|
+
content,
|
215
|
+
timestamp,
|
216
|
+
message_type.value if message_type else None,
|
217
|
+
json.dumps(metadata) if metadata else None,
|
218
|
+
token_count,
|
219
|
+
self.current_conversation_id,
|
220
|
+
),
|
221
|
+
)
|
222
|
+
return next_id
|
223
|
+
|
224
|
+
def batch_add(self, messages: List[Message]) -> List[int]:
|
225
|
+
"""
|
226
|
+
Add multiple messages to the current conversation.
|
227
|
+
|
228
|
+
Args:
|
229
|
+
messages (List[Message]): List of messages to add
|
230
|
+
|
231
|
+
Returns:
|
232
|
+
List[int]: List of inserted message IDs
|
233
|
+
"""
|
234
|
+
with self._get_connection() as conn:
|
235
|
+
message_ids = []
|
236
|
+
|
237
|
+
# Get the starting ID
|
238
|
+
result = conn.execute(
|
239
|
+
f"SELECT COALESCE(MAX(id), 0) + 1 as next_id FROM {self.table_name}"
|
240
|
+
).fetchone()
|
241
|
+
next_id = result[0]
|
242
|
+
|
243
|
+
for i, message in enumerate(messages):
|
244
|
+
content = message.content
|
245
|
+
if isinstance(content, (dict, list)):
|
246
|
+
content = json.dumps(content)
|
247
|
+
|
248
|
+
conn.execute(
|
249
|
+
f"""
|
250
|
+
INSERT INTO {self.table_name}
|
251
|
+
(id, role, content, timestamp, message_type, metadata, token_count, conversation_id)
|
252
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
253
|
+
""",
|
254
|
+
(
|
255
|
+
next_id + i,
|
256
|
+
message.role,
|
257
|
+
content,
|
258
|
+
(
|
259
|
+
message.timestamp.isoformat()
|
260
|
+
if message.timestamp
|
261
|
+
else None
|
262
|
+
),
|
263
|
+
(
|
264
|
+
message.message_type.value
|
265
|
+
if message.message_type
|
266
|
+
else None
|
267
|
+
),
|
268
|
+
(
|
269
|
+
json.dumps(message.metadata)
|
270
|
+
if message.metadata
|
271
|
+
else None
|
272
|
+
),
|
273
|
+
message.token_count,
|
274
|
+
self.current_conversation_id,
|
275
|
+
),
|
276
|
+
)
|
277
|
+
message_ids.append(next_id + i)
|
278
|
+
|
279
|
+
return message_ids
|
280
|
+
|
281
|
+
def get_str(self) -> str:
|
282
|
+
"""
|
283
|
+
Get the current conversation history as a formatted string.
|
284
|
+
|
285
|
+
Returns:
|
286
|
+
str: Formatted conversation history
|
287
|
+
"""
|
288
|
+
with self._get_connection() as conn:
|
289
|
+
result = conn.execute(
|
290
|
+
f"""
|
291
|
+
SELECT * FROM {self.table_name}
|
292
|
+
WHERE conversation_id = ?
|
293
|
+
ORDER BY id ASC
|
294
|
+
""",
|
295
|
+
(self.current_conversation_id,),
|
296
|
+
).fetchall()
|
297
|
+
|
298
|
+
messages = []
|
299
|
+
for row in result:
|
300
|
+
content = row[2] # content column
|
301
|
+
try:
|
302
|
+
content = json.loads(content)
|
303
|
+
except json.JSONDecodeError:
|
304
|
+
pass
|
305
|
+
|
306
|
+
timestamp = (
|
307
|
+
f"[{row[3]}] " if row[3] else ""
|
308
|
+
) # timestamp column
|
309
|
+
messages.append(
|
310
|
+
f"{timestamp}{row[1]}: {content}"
|
311
|
+
) # role column
|
312
|
+
|
313
|
+
return "\n".join(messages)
|
314
|
+
|
315
|
+
def get_messages(
|
316
|
+
self,
|
317
|
+
limit: Optional[int] = None,
|
318
|
+
offset: Optional[int] = None,
|
319
|
+
) -> List[Dict]:
|
320
|
+
"""
|
321
|
+
Get messages from the current conversation with optional pagination.
|
322
|
+
|
323
|
+
Args:
|
324
|
+
limit (Optional[int]): Maximum number of messages to return
|
325
|
+
offset (Optional[int]): Number of messages to skip
|
326
|
+
|
327
|
+
Returns:
|
328
|
+
List[Dict]: List of message dictionaries
|
329
|
+
"""
|
330
|
+
with self._get_connection() as conn:
|
331
|
+
query = f"""
|
332
|
+
SELECT * FROM {self.table_name}
|
333
|
+
WHERE conversation_id = ?
|
334
|
+
ORDER BY id ASC
|
335
|
+
"""
|
336
|
+
params = [self.current_conversation_id]
|
337
|
+
|
338
|
+
if limit is not None:
|
339
|
+
query += " LIMIT ?"
|
340
|
+
params.append(limit)
|
341
|
+
|
342
|
+
if offset is not None:
|
343
|
+
query += " OFFSET ?"
|
344
|
+
params.append(offset)
|
345
|
+
|
346
|
+
result = conn.execute(query, params).fetchall()
|
347
|
+
messages = []
|
348
|
+
for row in result:
|
349
|
+
content = row[2] # content column
|
350
|
+
try:
|
351
|
+
content = json.loads(content)
|
352
|
+
except json.JSONDecodeError:
|
353
|
+
pass
|
354
|
+
|
355
|
+
message = {
|
356
|
+
"role": row[1], # role column
|
357
|
+
"content": content,
|
358
|
+
}
|
359
|
+
|
360
|
+
if row[3]: # timestamp column
|
361
|
+
message["timestamp"] = row[3]
|
362
|
+
if row[4]: # message_type column
|
363
|
+
message["message_type"] = row[4]
|
364
|
+
if row[5]: # metadata column
|
365
|
+
message["metadata"] = json.loads(row[5])
|
366
|
+
if row[6]: # token_count column
|
367
|
+
message["token_count"] = row[6]
|
368
|
+
|
369
|
+
messages.append(message)
|
370
|
+
|
371
|
+
return messages
|
372
|
+
|
373
|
+
def delete_current_conversation(self) -> bool:
|
374
|
+
"""
|
375
|
+
Delete the current conversation.
|
376
|
+
|
377
|
+
Returns:
|
378
|
+
bool: True if deletion was successful
|
379
|
+
"""
|
380
|
+
with self._get_connection() as conn:
|
381
|
+
result = conn.execute(
|
382
|
+
f"DELETE FROM {self.table_name} WHERE conversation_id = ?",
|
383
|
+
(self.current_conversation_id,),
|
384
|
+
)
|
385
|
+
return result.rowcount > 0
|
386
|
+
|
387
|
+
def update_message(
|
388
|
+
self,
|
389
|
+
message_id: int,
|
390
|
+
content: Union[str, dict, list],
|
391
|
+
metadata: Optional[Dict] = None,
|
392
|
+
) -> bool:
|
393
|
+
"""
|
394
|
+
Update an existing message in the current conversation.
|
395
|
+
|
396
|
+
Args:
|
397
|
+
message_id (int): ID of the message to update
|
398
|
+
content (Union[str, dict, list]): New content for the message
|
399
|
+
metadata (Optional[Dict]): New metadata for the message
|
400
|
+
|
401
|
+
Returns:
|
402
|
+
bool: True if update was successful
|
403
|
+
"""
|
404
|
+
if isinstance(content, (dict, list)):
|
405
|
+
content = json.dumps(content)
|
406
|
+
|
407
|
+
with self._get_connection() as conn:
|
408
|
+
result = conn.execute(
|
409
|
+
f"""
|
410
|
+
UPDATE {self.table_name}
|
411
|
+
SET content = ?, metadata = ?
|
412
|
+
WHERE id = ? AND conversation_id = ?
|
413
|
+
""",
|
414
|
+
(
|
415
|
+
content,
|
416
|
+
json.dumps(metadata) if metadata else None,
|
417
|
+
message_id,
|
418
|
+
self.current_conversation_id,
|
419
|
+
),
|
420
|
+
)
|
421
|
+
return result.rowcount > 0
|
422
|
+
|
423
|
+
def search_messages(self, query: str) -> List[Dict]:
|
424
|
+
"""
|
425
|
+
Search for messages containing specific text in the current conversation.
|
426
|
+
|
427
|
+
Args:
|
428
|
+
query (str): Text to search for
|
429
|
+
|
430
|
+
Returns:
|
431
|
+
List[Dict]: List of matching messages
|
432
|
+
"""
|
433
|
+
with self._get_connection() as conn:
|
434
|
+
result = conn.execute(
|
435
|
+
f"""
|
436
|
+
SELECT * FROM {self.table_name}
|
437
|
+
WHERE conversation_id = ? AND content LIKE ?
|
438
|
+
""",
|
439
|
+
(self.current_conversation_id, f"%{query}%"),
|
440
|
+
).fetchall()
|
441
|
+
|
442
|
+
messages = []
|
443
|
+
for row in result:
|
444
|
+
content = row[2] # content column
|
445
|
+
try:
|
446
|
+
content = json.loads(content)
|
447
|
+
except json.JSONDecodeError:
|
448
|
+
pass
|
449
|
+
|
450
|
+
message = {
|
451
|
+
"role": row[1], # role column
|
452
|
+
"content": content,
|
453
|
+
}
|
454
|
+
|
455
|
+
if row[3]: # timestamp column
|
456
|
+
message["timestamp"] = row[3]
|
457
|
+
if row[4]: # message_type column
|
458
|
+
message["message_type"] = row[4]
|
459
|
+
if row[5]: # metadata column
|
460
|
+
message["metadata"] = json.loads(row[5])
|
461
|
+
if row[6]: # token_count column
|
462
|
+
message["token_count"] = row[6]
|
463
|
+
|
464
|
+
messages.append(message)
|
465
|
+
|
466
|
+
return messages
|
467
|
+
|
468
|
+
def get_statistics(self) -> Dict:
|
469
|
+
"""
|
470
|
+
Get statistics about the current conversation.
|
471
|
+
|
472
|
+
Returns:
|
473
|
+
Dict: Statistics about the conversation
|
474
|
+
"""
|
475
|
+
with self._get_connection() as conn:
|
476
|
+
result = conn.execute(
|
477
|
+
f"""
|
478
|
+
SELECT
|
479
|
+
COUNT(*) as total_messages,
|
480
|
+
COUNT(DISTINCT role) as unique_roles,
|
481
|
+
SUM(token_count) as total_tokens,
|
482
|
+
MIN(timestamp) as first_message,
|
483
|
+
MAX(timestamp) as last_message
|
484
|
+
FROM {self.table_name}
|
485
|
+
WHERE conversation_id = ?
|
486
|
+
""",
|
487
|
+
(self.current_conversation_id,),
|
488
|
+
).fetchone()
|
489
|
+
|
490
|
+
return {
|
491
|
+
"total_messages": result[0],
|
492
|
+
"unique_roles": result[1],
|
493
|
+
"total_tokens": result[2],
|
494
|
+
"first_message": result[3],
|
495
|
+
"last_message": result[4],
|
496
|
+
}
|
497
|
+
|
498
|
+
def clear_all(self) -> bool:
|
499
|
+
"""
|
500
|
+
Clear all messages from the database.
|
501
|
+
|
502
|
+
Returns:
|
503
|
+
bool: True if clearing was successful
|
504
|
+
"""
|
505
|
+
with self._get_connection() as conn:
|
506
|
+
conn.execute(f"DELETE FROM {self.table_name}")
|
507
|
+
return True
|
508
|
+
|
509
|
+
def get_conversation_id(self) -> str:
|
510
|
+
"""
|
511
|
+
Get the current conversation ID.
|
512
|
+
|
513
|
+
Returns:
|
514
|
+
str: The current conversation ID
|
515
|
+
"""
|
516
|
+
return self.current_conversation_id
|
517
|
+
|
518
|
+
def to_dict(self) -> List[Dict]:
|
519
|
+
"""
|
520
|
+
Convert the current conversation to a list of dictionaries.
|
521
|
+
|
522
|
+
Returns:
|
523
|
+
List[Dict]: List of message dictionaries
|
524
|
+
"""
|
525
|
+
with self._get_connection() as conn:
|
526
|
+
result = conn.execute(
|
527
|
+
f"""
|
528
|
+
SELECT role, content, timestamp, message_type, metadata, token_count
|
529
|
+
FROM {self.table_name}
|
530
|
+
WHERE conversation_id = ?
|
531
|
+
ORDER BY id ASC
|
532
|
+
""",
|
533
|
+
(self.current_conversation_id,),
|
534
|
+
).fetchall()
|
535
|
+
|
536
|
+
messages = []
|
537
|
+
for row in result:
|
538
|
+
content = row[1] # content column
|
539
|
+
try:
|
540
|
+
content = json.loads(content)
|
541
|
+
except json.JSONDecodeError:
|
542
|
+
pass
|
543
|
+
|
544
|
+
message = {
|
545
|
+
"role": row[0],
|
546
|
+
"content": content,
|
547
|
+
} # role column
|
548
|
+
|
549
|
+
if row[2]: # timestamp column
|
550
|
+
message["timestamp"] = row[2]
|
551
|
+
if row[3]: # message_type column
|
552
|
+
message["message_type"] = row[3]
|
553
|
+
if row[4]: # metadata column
|
554
|
+
message["metadata"] = json.loads(row[4])
|
555
|
+
if row[5]: # token_count column
|
556
|
+
message["token_count"] = row[5]
|
557
|
+
|
558
|
+
messages.append(message)
|
559
|
+
|
560
|
+
return messages
|
561
|
+
|
562
|
+
def to_json(self) -> str:
|
563
|
+
"""
|
564
|
+
Convert the current conversation to a JSON string.
|
565
|
+
|
566
|
+
Returns:
|
567
|
+
str: JSON string representation of the conversation
|
568
|
+
"""
|
569
|
+
return json.dumps(
|
570
|
+
self.to_dict(), indent=2, cls=DateTimeEncoder
|
571
|
+
)
|
572
|
+
|
573
|
+
def to_yaml(self) -> str:
|
574
|
+
"""
|
575
|
+
Convert the current conversation to a YAML string.
|
576
|
+
|
577
|
+
Returns:
|
578
|
+
str: YAML string representation of the conversation
|
579
|
+
"""
|
580
|
+
return yaml.dump(self.to_dict())
|
581
|
+
|
582
|
+
def save_as_json(self, filename: str) -> bool:
|
583
|
+
"""
|
584
|
+
Save the current conversation to a JSON file.
|
585
|
+
|
586
|
+
Args:
|
587
|
+
filename (str): Path to save the JSON file
|
588
|
+
|
589
|
+
Returns:
|
590
|
+
bool: True if save was successful
|
591
|
+
"""
|
592
|
+
try:
|
593
|
+
with open(filename, "w") as f:
|
594
|
+
json.dump(
|
595
|
+
self.to_dict(), f, indent=2, cls=DateTimeEncoder
|
596
|
+
)
|
597
|
+
return True
|
598
|
+
except Exception as e:
|
599
|
+
if self.enable_logging:
|
600
|
+
self.logger.error(
|
601
|
+
f"Failed to save conversation to JSON: {e}"
|
602
|
+
)
|
603
|
+
return False
|
604
|
+
|
605
|
+
def load_from_json(self, filename: str) -> bool:
|
606
|
+
"""
|
607
|
+
Load a conversation from a JSON file.
|
608
|
+
|
609
|
+
Args:
|
610
|
+
filename (str): Path to the JSON file
|
611
|
+
|
612
|
+
Returns:
|
613
|
+
bool: True if load was successful
|
614
|
+
"""
|
615
|
+
try:
|
616
|
+
with open(filename, "r") as f:
|
617
|
+
messages = json.load(f)
|
618
|
+
|
619
|
+
# Start a new conversation
|
620
|
+
self.start_new_conversation()
|
621
|
+
|
622
|
+
# Add all messages
|
623
|
+
for message in messages:
|
624
|
+
# Convert timestamp string back to datetime if it exists
|
625
|
+
if "timestamp" in message:
|
626
|
+
try:
|
627
|
+
datetime.datetime.fromisoformat(
|
628
|
+
message["timestamp"]
|
629
|
+
)
|
630
|
+
except (ValueError, TypeError):
|
631
|
+
message["timestamp"]
|
632
|
+
|
633
|
+
self.add(
|
634
|
+
role=message["role"],
|
635
|
+
content=message["content"],
|
636
|
+
message_type=(
|
637
|
+
MessageType(message["message_type"])
|
638
|
+
if "message_type" in message
|
639
|
+
else None
|
640
|
+
),
|
641
|
+
metadata=message.get("metadata"),
|
642
|
+
token_count=message.get("token_count"),
|
643
|
+
)
|
644
|
+
return True
|
645
|
+
except Exception as e:
|
646
|
+
if self.enable_logging:
|
647
|
+
self.logger.error(
|
648
|
+
f"Failed to load conversation from JSON: {e}"
|
649
|
+
)
|
650
|
+
return False
|
651
|
+
|
652
|
+
def get_last_message(self) -> Optional[Dict]:
|
653
|
+
"""
|
654
|
+
Get the last message from the current conversation.
|
655
|
+
|
656
|
+
Returns:
|
657
|
+
Optional[Dict]: The last message or None if conversation is empty
|
658
|
+
"""
|
659
|
+
with self._get_connection() as conn:
|
660
|
+
result = conn.execute(
|
661
|
+
f"""
|
662
|
+
SELECT * FROM {self.table_name}
|
663
|
+
WHERE conversation_id = ?
|
664
|
+
ORDER BY id DESC
|
665
|
+
LIMIT 1
|
666
|
+
""",
|
667
|
+
(self.current_conversation_id,),
|
668
|
+
).fetchone()
|
669
|
+
|
670
|
+
if not result:
|
671
|
+
return None
|
672
|
+
|
673
|
+
content = result[2] # content column
|
674
|
+
try:
|
675
|
+
content = json.loads(content)
|
676
|
+
except json.JSONDecodeError:
|
677
|
+
pass
|
678
|
+
|
679
|
+
message = {
|
680
|
+
"role": result[1], # role column
|
681
|
+
"content": content,
|
682
|
+
}
|
683
|
+
|
684
|
+
if result[3]: # timestamp column
|
685
|
+
message["timestamp"] = result[3]
|
686
|
+
if result[4]: # message_type column
|
687
|
+
message["message_type"] = result[4]
|
688
|
+
if result[5]: # metadata column
|
689
|
+
message["metadata"] = json.loads(result[5])
|
690
|
+
if result[6]: # token_count column
|
691
|
+
message["token_count"] = result[6]
|
692
|
+
|
693
|
+
return message
|
694
|
+
|
695
|
+
def get_last_message_as_string(self) -> str:
|
696
|
+
"""
|
697
|
+
Get the last message as a formatted string.
|
698
|
+
|
699
|
+
Returns:
|
700
|
+
str: Formatted string of the last message
|
701
|
+
"""
|
702
|
+
last_message = self.get_last_message()
|
703
|
+
if not last_message:
|
704
|
+
return ""
|
705
|
+
|
706
|
+
timestamp = (
|
707
|
+
f"[{last_message['timestamp']}] "
|
708
|
+
if "timestamp" in last_message
|
709
|
+
else ""
|
710
|
+
)
|
711
|
+
return f"{timestamp}{last_message['role']}: {last_message['content']}"
|
712
|
+
|
713
|
+
def count_messages_by_role(self) -> Dict[str, int]:
|
714
|
+
"""
|
715
|
+
Count messages by role in the current conversation.
|
716
|
+
|
717
|
+
Returns:
|
718
|
+
Dict[str, int]: Dictionary with role counts
|
719
|
+
"""
|
720
|
+
with self._get_connection() as conn:
|
721
|
+
result = conn.execute(
|
722
|
+
f"""
|
723
|
+
SELECT role, COUNT(*) as count
|
724
|
+
FROM {self.table_name}
|
725
|
+
WHERE conversation_id = ?
|
726
|
+
GROUP BY role
|
727
|
+
""",
|
728
|
+
(self.current_conversation_id,),
|
729
|
+
).fetchall()
|
730
|
+
|
731
|
+
return {row[0]: row[1] for row in result}
|
732
|
+
|
733
|
+
def get_messages_by_role(self, role: str) -> List[Dict]:
|
734
|
+
"""
|
735
|
+
Get all messages from a specific role in the current conversation.
|
736
|
+
|
737
|
+
Args:
|
738
|
+
role (str): Role to filter messages by
|
739
|
+
|
740
|
+
Returns:
|
741
|
+
List[Dict]: List of messages from the specified role
|
742
|
+
"""
|
743
|
+
with self._get_connection() as conn:
|
744
|
+
result = conn.execute(
|
745
|
+
f"""
|
746
|
+
SELECT * FROM {self.table_name}
|
747
|
+
WHERE conversation_id = ? AND role = ?
|
748
|
+
ORDER BY id ASC
|
749
|
+
""",
|
750
|
+
(self.current_conversation_id, role),
|
751
|
+
).fetchall()
|
752
|
+
|
753
|
+
messages = []
|
754
|
+
for row in result:
|
755
|
+
content = row[2] # content column
|
756
|
+
try:
|
757
|
+
content = json.loads(content)
|
758
|
+
except json.JSONDecodeError:
|
759
|
+
pass
|
760
|
+
|
761
|
+
message = {
|
762
|
+
"role": row[1], # role column
|
763
|
+
"content": content,
|
764
|
+
}
|
765
|
+
|
766
|
+
if row[3]: # timestamp column
|
767
|
+
message["timestamp"] = row[3]
|
768
|
+
if row[4]: # message_type column
|
769
|
+
message["message_type"] = row[4]
|
770
|
+
if row[5]: # metadata column
|
771
|
+
message["metadata"] = json.loads(row[5])
|
772
|
+
if row[6]: # token_count column
|
773
|
+
message["token_count"] = row[6]
|
774
|
+
|
775
|
+
messages.append(message)
|
776
|
+
|
777
|
+
return messages
|
778
|
+
|
779
|
+
def get_conversation_summary(self) -> Dict:
|
780
|
+
"""
|
781
|
+
Get a summary of the current conversation.
|
782
|
+
|
783
|
+
Returns:
|
784
|
+
Dict: Summary of the conversation including message counts, roles, and time range
|
785
|
+
"""
|
786
|
+
with self._get_connection() as conn:
|
787
|
+
result = conn.execute(
|
788
|
+
f"""
|
789
|
+
SELECT
|
790
|
+
COUNT(*) as total_messages,
|
791
|
+
COUNT(DISTINCT role) as unique_roles,
|
792
|
+
MIN(timestamp) as first_message_time,
|
793
|
+
MAX(timestamp) as last_message_time,
|
794
|
+
SUM(token_count) as total_tokens
|
795
|
+
FROM {self.table_name}
|
796
|
+
WHERE conversation_id = ?
|
797
|
+
""",
|
798
|
+
(self.current_conversation_id,),
|
799
|
+
).fetchone()
|
800
|
+
|
801
|
+
return {
|
802
|
+
"conversation_id": self.current_conversation_id,
|
803
|
+
"total_messages": result[0],
|
804
|
+
"unique_roles": result[1],
|
805
|
+
"first_message_time": result[2],
|
806
|
+
"last_message_time": result[3],
|
807
|
+
"total_tokens": result[4],
|
808
|
+
"roles": self.count_messages_by_role(),
|
809
|
+
}
|
810
|
+
|
811
|
+
def get_conversation_as_dict(self) -> Dict:
|
812
|
+
"""
|
813
|
+
Get the entire conversation as a dictionary with messages and metadata.
|
814
|
+
|
815
|
+
Returns:
|
816
|
+
Dict: Dictionary containing conversation ID, messages, and metadata
|
817
|
+
"""
|
818
|
+
messages = self.get_messages()
|
819
|
+
stats = self.get_statistics()
|
820
|
+
|
821
|
+
return {
|
822
|
+
"conversation_id": self.current_conversation_id,
|
823
|
+
"messages": messages,
|
824
|
+
"metadata": {
|
825
|
+
"total_messages": stats["total_messages"],
|
826
|
+
"unique_roles": stats["unique_roles"],
|
827
|
+
"total_tokens": stats["total_tokens"],
|
828
|
+
"first_message": stats["first_message"],
|
829
|
+
"last_message": stats["last_message"],
|
830
|
+
"roles": self.count_messages_by_role(),
|
831
|
+
},
|
832
|
+
}
|
833
|
+
|
834
|
+
def get_conversation_by_role_dict(self) -> Dict[str, List[Dict]]:
|
835
|
+
"""
|
836
|
+
Get the conversation organized by roles.
|
837
|
+
|
838
|
+
Returns:
|
839
|
+
Dict[str, List[Dict]]: Dictionary with roles as keys and lists of messages as values
|
840
|
+
"""
|
841
|
+
with self._get_connection() as conn:
|
842
|
+
result = conn.execute(
|
843
|
+
f"""
|
844
|
+
SELECT role, content, timestamp, message_type, metadata, token_count
|
845
|
+
FROM {self.table_name}
|
846
|
+
WHERE conversation_id = ?
|
847
|
+
ORDER BY id ASC
|
848
|
+
""",
|
849
|
+
(self.current_conversation_id,),
|
850
|
+
).fetchall()
|
851
|
+
|
852
|
+
role_dict = {}
|
853
|
+
for row in result:
|
854
|
+
role = row[0]
|
855
|
+
content = row[1]
|
856
|
+
try:
|
857
|
+
content = json.loads(content)
|
858
|
+
except json.JSONDecodeError:
|
859
|
+
pass
|
860
|
+
|
861
|
+
message = {
|
862
|
+
"content": content,
|
863
|
+
"timestamp": row[2],
|
864
|
+
"message_type": row[3],
|
865
|
+
"metadata": (
|
866
|
+
json.loads(row[4]) if row[4] else None
|
867
|
+
),
|
868
|
+
"token_count": row[5],
|
869
|
+
}
|
870
|
+
|
871
|
+
if role not in role_dict:
|
872
|
+
role_dict[role] = []
|
873
|
+
role_dict[role].append(message)
|
874
|
+
|
875
|
+
return role_dict
|
876
|
+
|
877
|
+
def get_conversation_timeline_dict(self) -> Dict[str, List[Dict]]:
|
878
|
+
"""
|
879
|
+
Get the conversation organized by timestamps.
|
880
|
+
|
881
|
+
Returns:
|
882
|
+
Dict[str, List[Dict]]: Dictionary with dates as keys and lists of messages as values
|
883
|
+
"""
|
884
|
+
with self._get_connection() as conn:
|
885
|
+
result = conn.execute(
|
886
|
+
f"""
|
887
|
+
SELECT
|
888
|
+
DATE(timestamp) as date,
|
889
|
+
role,
|
890
|
+
content,
|
891
|
+
timestamp,
|
892
|
+
message_type,
|
893
|
+
metadata,
|
894
|
+
token_count
|
895
|
+
FROM {self.table_name}
|
896
|
+
WHERE conversation_id = ?
|
897
|
+
ORDER BY timestamp ASC
|
898
|
+
""",
|
899
|
+
(self.current_conversation_id,),
|
900
|
+
).fetchall()
|
901
|
+
|
902
|
+
timeline_dict = {}
|
903
|
+
for row in result:
|
904
|
+
date = row[0]
|
905
|
+
content = row[2]
|
906
|
+
try:
|
907
|
+
content = json.loads(content)
|
908
|
+
except json.JSONDecodeError:
|
909
|
+
pass
|
910
|
+
|
911
|
+
message = {
|
912
|
+
"role": row[1],
|
913
|
+
"content": content,
|
914
|
+
"timestamp": row[3],
|
915
|
+
"message_type": row[4],
|
916
|
+
"metadata": (
|
917
|
+
json.loads(row[5]) if row[5] else None
|
918
|
+
),
|
919
|
+
"token_count": row[6],
|
920
|
+
}
|
921
|
+
|
922
|
+
if date not in timeline_dict:
|
923
|
+
timeline_dict[date] = []
|
924
|
+
timeline_dict[date].append(message)
|
925
|
+
|
926
|
+
return timeline_dict
|
927
|
+
|
928
|
+
def get_conversation_metadata_dict(self) -> Dict:
|
929
|
+
"""
|
930
|
+
Get detailed metadata about the conversation.
|
931
|
+
|
932
|
+
Returns:
|
933
|
+
Dict: Dictionary containing detailed conversation metadata
|
934
|
+
"""
|
935
|
+
with self._get_connection() as conn:
|
936
|
+
# Get basic statistics
|
937
|
+
stats = self.get_statistics()
|
938
|
+
|
939
|
+
# Get message type distribution
|
940
|
+
type_dist = conn.execute(
|
941
|
+
f"""
|
942
|
+
SELECT message_type, COUNT(*) as count
|
943
|
+
FROM {self.table_name}
|
944
|
+
WHERE conversation_id = ?
|
945
|
+
GROUP BY message_type
|
946
|
+
""",
|
947
|
+
(self.current_conversation_id,),
|
948
|
+
).fetchall()
|
949
|
+
|
950
|
+
# Get average tokens per message
|
951
|
+
avg_tokens = conn.execute(
|
952
|
+
f"""
|
953
|
+
SELECT AVG(token_count) as avg_tokens
|
954
|
+
FROM {self.table_name}
|
955
|
+
WHERE conversation_id = ? AND token_count IS NOT NULL
|
956
|
+
""",
|
957
|
+
(self.current_conversation_id,),
|
958
|
+
).fetchone()
|
959
|
+
|
960
|
+
# Get message frequency by hour
|
961
|
+
hourly_freq = conn.execute(
|
962
|
+
f"""
|
963
|
+
SELECT
|
964
|
+
EXTRACT(HOUR FROM timestamp) as hour,
|
965
|
+
COUNT(*) as count
|
966
|
+
FROM {self.table_name}
|
967
|
+
WHERE conversation_id = ?
|
968
|
+
GROUP BY hour
|
969
|
+
ORDER BY hour
|
970
|
+
""",
|
971
|
+
(self.current_conversation_id,),
|
972
|
+
).fetchall()
|
973
|
+
|
974
|
+
return {
|
975
|
+
"conversation_id": self.current_conversation_id,
|
976
|
+
"basic_stats": stats,
|
977
|
+
"message_type_distribution": {
|
978
|
+
row[0]: row[1] for row in type_dist
|
979
|
+
},
|
980
|
+
"average_tokens_per_message": (
|
981
|
+
avg_tokens[0] if avg_tokens[0] is not None else 0
|
982
|
+
),
|
983
|
+
"hourly_message_frequency": {
|
984
|
+
row[0]: row[1] for row in hourly_freq
|
985
|
+
},
|
986
|
+
"role_distribution": self.count_messages_by_role(),
|
987
|
+
}
|
988
|
+
|
989
|
+
def save_as_yaml(self, filename: str) -> bool:
|
990
|
+
"""
|
991
|
+
Save the current conversation to a YAML file.
|
992
|
+
|
993
|
+
Args:
|
994
|
+
filename (str): Path to save the YAML file
|
995
|
+
|
996
|
+
Returns:
|
997
|
+
bool: True if save was successful
|
998
|
+
"""
|
999
|
+
try:
|
1000
|
+
with open(filename, "w") as f:
|
1001
|
+
yaml.dump(self.to_dict(), f)
|
1002
|
+
return True
|
1003
|
+
except Exception as e:
|
1004
|
+
if self.enable_logging:
|
1005
|
+
self.logger.error(
|
1006
|
+
f"Failed to save conversation to YAML: {e}"
|
1007
|
+
)
|
1008
|
+
return False
|
1009
|
+
|
1010
|
+
def load_from_yaml(self, filename: str) -> bool:
|
1011
|
+
"""
|
1012
|
+
Load a conversation from a YAML file.
|
1013
|
+
|
1014
|
+
Args:
|
1015
|
+
filename (str): Path to the YAML file
|
1016
|
+
|
1017
|
+
Returns:
|
1018
|
+
bool: True if load was successful
|
1019
|
+
"""
|
1020
|
+
try:
|
1021
|
+
with open(filename, "r") as f:
|
1022
|
+
messages = yaml.safe_load(f)
|
1023
|
+
|
1024
|
+
# Start a new conversation
|
1025
|
+
self.start_new_conversation()
|
1026
|
+
|
1027
|
+
# Add all messages
|
1028
|
+
for message in messages:
|
1029
|
+
self.add(
|
1030
|
+
role=message["role"],
|
1031
|
+
content=message["content"],
|
1032
|
+
message_type=(
|
1033
|
+
MessageType(message["message_type"])
|
1034
|
+
if "message_type" in message
|
1035
|
+
else None
|
1036
|
+
),
|
1037
|
+
metadata=message.get("metadata"),
|
1038
|
+
token_count=message.get("token_count"),
|
1039
|
+
)
|
1040
|
+
return True
|
1041
|
+
except Exception as e:
|
1042
|
+
if self.enable_logging:
|
1043
|
+
self.logger.error(
|
1044
|
+
f"Failed to load conversation from YAML: {e}"
|
1045
|
+
)
|
1046
|
+
return False
|