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.
@@ -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