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