swarms 7.7.3__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,813 @@
1
+ import sqlite3
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 SQLiteConversation:
48
+ """
49
+ A production-grade SQLite wrapper class for managing conversation history.
50
+ This class provides persistent storage for conversations with various features
51
+ like message tracking, timestamps, and metadata support.
52
+
53
+ Attributes:
54
+ db_path (str): Path to the SQLite database file
55
+ table_name (str): Name of the table to store conversations
56
+ enable_timestamps (bool): Whether to track message timestamps
57
+ enable_logging (bool): Whether to enable logging
58
+ use_loguru (bool): Whether to use loguru for logging
59
+ max_retries (int): Maximum number of retries for database operations
60
+ connection_timeout (float): Timeout for database connections
61
+ current_conversation_id (str): Current active conversation ID
62
+ """
63
+
64
+ def __init__(
65
+ self,
66
+ db_path: str = "conversations.db",
67
+ table_name: str = "conversations",
68
+ enable_timestamps: bool = True,
69
+ enable_logging: bool = True,
70
+ use_loguru: bool = True,
71
+ max_retries: int = 3,
72
+ connection_timeout: float = 5.0,
73
+ **kwargs,
74
+ ):
75
+ """
76
+ Initialize the SQLite conversation manager.
77
+
78
+ Args:
79
+ db_path (str): Path to the SQLite database file
80
+ table_name (str): Name of the table to store conversations
81
+ enable_timestamps (bool): Whether to track message timestamps
82
+ enable_logging (bool): Whether to enable logging
83
+ use_loguru (bool): Whether to use loguru for logging
84
+ max_retries (int): Maximum number of retries for database operations
85
+ connection_timeout (float): Timeout for database connections
86
+ """
87
+ self.db_path = Path(db_path)
88
+ self.table_name = table_name
89
+ self.enable_timestamps = enable_timestamps
90
+ self.enable_logging = enable_logging
91
+ self.use_loguru = use_loguru and LOGURU_AVAILABLE
92
+ self.max_retries = max_retries
93
+ self.connection_timeout = connection_timeout
94
+ self._lock = threading.Lock()
95
+ self.current_conversation_id = (
96
+ self._generate_conversation_id()
97
+ )
98
+
99
+ # Setup logging
100
+ if self.enable_logging:
101
+ if self.use_loguru:
102
+ self.logger = logger
103
+ else:
104
+ self.logger = logging.getLogger(__name__)
105
+ handler = logging.StreamHandler()
106
+ formatter = logging.Formatter(
107
+ "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
108
+ )
109
+ handler.setFormatter(formatter)
110
+ self.logger.addHandler(handler)
111
+ self.logger.setLevel(logging.INFO)
112
+
113
+ # Initialize database
114
+ self._init_db()
115
+
116
+ def _generate_conversation_id(self) -> str:
117
+ """Generate a unique conversation ID using UUID and timestamp."""
118
+ timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
119
+ unique_id = str(uuid.uuid4())[:8]
120
+ return f"conv_{timestamp}_{unique_id}"
121
+
122
+ def start_new_conversation(self) -> str:
123
+ """
124
+ Start a new conversation and return its ID.
125
+
126
+ Returns:
127
+ str: The new conversation ID
128
+ """
129
+ self.current_conversation_id = (
130
+ self._generate_conversation_id()
131
+ )
132
+ return self.current_conversation_id
133
+
134
+ def _init_db(self):
135
+ """Initialize the database and create necessary tables."""
136
+ with self._get_connection() as conn:
137
+ cursor = conn.cursor()
138
+ cursor.execute(
139
+ f"""
140
+ CREATE TABLE IF NOT EXISTS {self.table_name} (
141
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
142
+ role TEXT NOT NULL,
143
+ content TEXT NOT NULL,
144
+ timestamp TEXT,
145
+ message_type TEXT,
146
+ metadata TEXT,
147
+ token_count INTEGER,
148
+ conversation_id TEXT,
149
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP
150
+ )
151
+ """
152
+ )
153
+ conn.commit()
154
+
155
+ @contextmanager
156
+ def _get_connection(self):
157
+ """Context manager for database connections with retry logic."""
158
+ conn = None
159
+ for attempt in range(self.max_retries):
160
+ try:
161
+ conn = sqlite3.connect(
162
+ str(self.db_path), timeout=self.connection_timeout
163
+ )
164
+ conn.row_factory = sqlite3.Row
165
+ yield conn
166
+ break
167
+ except sqlite3.Error as e:
168
+ if attempt == self.max_retries - 1:
169
+ raise
170
+ if self.enable_logging:
171
+ self.logger.warning(
172
+ f"Database connection attempt {attempt + 1} failed: {e}"
173
+ )
174
+ finally:
175
+ if conn:
176
+ conn.close()
177
+
178
+ def add(
179
+ self,
180
+ role: str,
181
+ content: Union[str, dict, list],
182
+ message_type: Optional[MessageType] = None,
183
+ metadata: Optional[Dict] = None,
184
+ token_count: Optional[int] = None,
185
+ ) -> int:
186
+ """
187
+ Add a message to the current conversation.
188
+
189
+ Args:
190
+ role (str): The role of the speaker
191
+ content (Union[str, dict, list]): The content of the message
192
+ message_type (Optional[MessageType]): Type of the message
193
+ metadata (Optional[Dict]): Additional metadata for the message
194
+ token_count (Optional[int]): Number of tokens in the message
195
+
196
+ Returns:
197
+ int: The ID of the inserted message
198
+ """
199
+ timestamp = (
200
+ datetime.datetime.now().isoformat()
201
+ if self.enable_timestamps
202
+ else None
203
+ )
204
+
205
+ if isinstance(content, (dict, list)):
206
+ content = json.dumps(content)
207
+
208
+ with self._get_connection() as conn:
209
+ cursor = conn.cursor()
210
+ cursor.execute(
211
+ f"""
212
+ INSERT INTO {self.table_name}
213
+ (role, content, timestamp, message_type, metadata, token_count, conversation_id)
214
+ VALUES (?, ?, ?, ?, ?, ?, ?)
215
+ """,
216
+ (
217
+ role,
218
+ content,
219
+ timestamp,
220
+ message_type.value if message_type else None,
221
+ json.dumps(metadata) if metadata else None,
222
+ token_count,
223
+ self.current_conversation_id,
224
+ ),
225
+ )
226
+ conn.commit()
227
+ return cursor.lastrowid
228
+
229
+ def batch_add(self, messages: List[Message]) -> List[int]:
230
+ """
231
+ Add multiple messages to the current conversation.
232
+
233
+ Args:
234
+ messages (List[Message]): List of messages to add
235
+
236
+ Returns:
237
+ List[int]: List of inserted message IDs
238
+ """
239
+ with self._get_connection() as conn:
240
+ cursor = conn.cursor()
241
+ message_ids = []
242
+
243
+ for message in messages:
244
+ content = message.content
245
+ if isinstance(content, (dict, list)):
246
+ content = json.dumps(content)
247
+
248
+ cursor.execute(
249
+ f"""
250
+ INSERT INTO {self.table_name}
251
+ (role, content, timestamp, message_type, metadata, token_count, conversation_id)
252
+ VALUES (?, ?, ?, ?, ?, ?, ?)
253
+ """,
254
+ (
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(cursor.lastrowid)
277
+
278
+ conn.commit()
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
+ cursor = conn.cursor()
290
+ cursor.execute(
291
+ f"""
292
+ SELECT * FROM {self.table_name}
293
+ WHERE conversation_id = ?
294
+ ORDER BY id ASC
295
+ """,
296
+ (self.current_conversation_id,),
297
+ )
298
+
299
+ messages = []
300
+ for row in cursor.fetchall():
301
+ content = row["content"]
302
+ try:
303
+ content = json.loads(content)
304
+ except json.JSONDecodeError:
305
+ pass
306
+
307
+ timestamp = (
308
+ f"[{row['timestamp']}] "
309
+ if row["timestamp"]
310
+ else ""
311
+ )
312
+ messages.append(
313
+ f"{timestamp}{row['role']}: {content}"
314
+ )
315
+
316
+ return "\n".join(messages)
317
+
318
+ def get_messages(
319
+ self,
320
+ limit: Optional[int] = None,
321
+ offset: Optional[int] = None,
322
+ ) -> List[Dict]:
323
+ """
324
+ Get messages from the current conversation with optional pagination.
325
+
326
+ Args:
327
+ limit (Optional[int]): Maximum number of messages to return
328
+ offset (Optional[int]): Number of messages to skip
329
+
330
+ Returns:
331
+ List[Dict]: List of message dictionaries
332
+ """
333
+ with self._get_connection() as conn:
334
+ cursor = conn.cursor()
335
+ query = f"""
336
+ SELECT * FROM {self.table_name}
337
+ WHERE conversation_id = ?
338
+ ORDER BY id ASC
339
+ """
340
+ params = [self.current_conversation_id]
341
+
342
+ if limit is not None:
343
+ query += " LIMIT ?"
344
+ params.append(limit)
345
+
346
+ if offset is not None:
347
+ query += " OFFSET ?"
348
+ params.append(offset)
349
+
350
+ cursor.execute(query, params)
351
+ return [dict(row) for row in cursor.fetchall()]
352
+
353
+ def delete_current_conversation(self) -> bool:
354
+ """
355
+ Delete the current conversation.
356
+
357
+ Returns:
358
+ bool: True if deletion was successful
359
+ """
360
+ with self._get_connection() as conn:
361
+ cursor = conn.cursor()
362
+ cursor.execute(
363
+ f"DELETE FROM {self.table_name} WHERE conversation_id = ?",
364
+ (self.current_conversation_id,),
365
+ )
366
+ conn.commit()
367
+ return cursor.rowcount > 0
368
+
369
+ def update_message(
370
+ self,
371
+ message_id: int,
372
+ content: Union[str, dict, list],
373
+ metadata: Optional[Dict] = None,
374
+ ) -> bool:
375
+ """
376
+ Update an existing message in the current conversation.
377
+
378
+ Args:
379
+ message_id (int): ID of the message to update
380
+ content (Union[str, dict, list]): New content for the message
381
+ metadata (Optional[Dict]): New metadata for the message
382
+
383
+ Returns:
384
+ bool: True if update was successful
385
+ """
386
+ if isinstance(content, (dict, list)):
387
+ content = json.dumps(content)
388
+
389
+ with self._get_connection() as conn:
390
+ cursor = conn.cursor()
391
+ cursor.execute(
392
+ f"""
393
+ UPDATE {self.table_name}
394
+ SET content = ?, metadata = ?
395
+ WHERE id = ? AND conversation_id = ?
396
+ """,
397
+ (
398
+ content,
399
+ json.dumps(metadata) if metadata else None,
400
+ message_id,
401
+ self.current_conversation_id,
402
+ ),
403
+ )
404
+ conn.commit()
405
+ return cursor.rowcount > 0
406
+
407
+ def search_messages(self, query: str) -> List[Dict]:
408
+ """
409
+ Search for messages containing specific text in the current conversation.
410
+
411
+ Args:
412
+ query (str): Text to search for
413
+
414
+ Returns:
415
+ List[Dict]: List of matching messages
416
+ """
417
+ with self._get_connection() as conn:
418
+ cursor = conn.cursor()
419
+ cursor.execute(
420
+ f"""
421
+ SELECT * FROM {self.table_name}
422
+ WHERE conversation_id = ? AND content LIKE ?
423
+ """,
424
+ (self.current_conversation_id, f"%{query}%"),
425
+ )
426
+ return [dict(row) for row in cursor.fetchall()]
427
+
428
+ def get_statistics(self) -> Dict:
429
+ """
430
+ Get statistics about the current conversation.
431
+
432
+ Returns:
433
+ Dict: Statistics about the conversation
434
+ """
435
+ with self._get_connection() as conn:
436
+ cursor = conn.cursor()
437
+ cursor.execute(
438
+ f"""
439
+ SELECT
440
+ COUNT(*) as total_messages,
441
+ COUNT(DISTINCT role) as unique_roles,
442
+ SUM(token_count) as total_tokens,
443
+ MIN(timestamp) as first_message,
444
+ MAX(timestamp) as last_message
445
+ FROM {self.table_name}
446
+ WHERE conversation_id = ?
447
+ """,
448
+ (self.current_conversation_id,),
449
+ )
450
+ return dict(cursor.fetchone())
451
+
452
+ def clear_all(self) -> bool:
453
+ """
454
+ Clear all messages from the database.
455
+
456
+ Returns:
457
+ bool: True if clearing was successful
458
+ """
459
+ with self._get_connection() as conn:
460
+ cursor = conn.cursor()
461
+ cursor.execute(f"DELETE FROM {self.table_name}")
462
+ conn.commit()
463
+ return True
464
+
465
+ def get_conversation_id(self) -> str:
466
+ """
467
+ Get the current conversation ID.
468
+
469
+ Returns:
470
+ str: The current conversation ID
471
+ """
472
+ return self.current_conversation_id
473
+
474
+ def to_dict(self) -> List[Dict]:
475
+ """
476
+ Convert the current conversation to a list of dictionaries.
477
+
478
+ Returns:
479
+ List[Dict]: List of message dictionaries
480
+ """
481
+ with self._get_connection() as conn:
482
+ cursor = conn.cursor()
483
+ cursor.execute(
484
+ f"""
485
+ SELECT role, content, timestamp, message_type, metadata, token_count
486
+ FROM {self.table_name}
487
+ WHERE conversation_id = ?
488
+ ORDER BY id ASC
489
+ """,
490
+ (self.current_conversation_id,),
491
+ )
492
+
493
+ messages = []
494
+ for row in cursor.fetchall():
495
+ content = row["content"]
496
+ try:
497
+ content = json.loads(content)
498
+ except json.JSONDecodeError:
499
+ pass
500
+
501
+ message = {"role": row["role"], "content": content}
502
+
503
+ if row["timestamp"]:
504
+ message["timestamp"] = row["timestamp"]
505
+ if row["message_type"]:
506
+ message["message_type"] = row["message_type"]
507
+ if row["metadata"]:
508
+ message["metadata"] = json.loads(row["metadata"])
509
+ if row["token_count"]:
510
+ message["token_count"] = row["token_count"]
511
+
512
+ messages.append(message)
513
+
514
+ return messages
515
+
516
+ def to_json(self) -> str:
517
+ """
518
+ Convert the current conversation to a JSON string.
519
+
520
+ Returns:
521
+ str: JSON string representation of the conversation
522
+ """
523
+ return json.dumps(self.to_dict(), indent=2)
524
+
525
+ def to_yaml(self) -> str:
526
+ """
527
+ Convert the current conversation to a YAML string.
528
+
529
+ Returns:
530
+ str: YAML string representation of the conversation
531
+ """
532
+ return yaml.dump(self.to_dict())
533
+
534
+ def save_as_json(self, filename: str) -> bool:
535
+ """
536
+ Save the current conversation to a JSON file.
537
+
538
+ Args:
539
+ filename (str): Path to save the JSON file
540
+
541
+ Returns:
542
+ bool: True if save was successful
543
+ """
544
+ try:
545
+ with open(filename, "w") as f:
546
+ json.dump(self.to_dict(), f, indent=2)
547
+ return True
548
+ except Exception as e:
549
+ if self.enable_logging:
550
+ self.logger.error(
551
+ f"Failed to save conversation to JSON: {e}"
552
+ )
553
+ return False
554
+
555
+ def save_as_yaml(self, filename: str) -> bool:
556
+ """
557
+ Save the current conversation to a YAML file.
558
+
559
+ Args:
560
+ filename (str): Path to save the YAML file
561
+
562
+ Returns:
563
+ bool: True if save was successful
564
+ """
565
+ try:
566
+ with open(filename, "w") as f:
567
+ yaml.dump(self.to_dict(), f)
568
+ return True
569
+ except Exception as e:
570
+ if self.enable_logging:
571
+ self.logger.error(
572
+ f"Failed to save conversation to YAML: {e}"
573
+ )
574
+ return False
575
+
576
+ def load_from_json(self, filename: str) -> bool:
577
+ """
578
+ Load a conversation from a JSON file.
579
+
580
+ Args:
581
+ filename (str): Path to the JSON file
582
+
583
+ Returns:
584
+ bool: True if load was successful
585
+ """
586
+ try:
587
+ with open(filename, "r") as f:
588
+ messages = json.load(f)
589
+
590
+ # Start a new conversation
591
+ self.start_new_conversation()
592
+
593
+ # Add all messages
594
+ for message in messages:
595
+ self.add(
596
+ role=message["role"],
597
+ content=message["content"],
598
+ message_type=(
599
+ MessageType(message["message_type"])
600
+ if "message_type" in message
601
+ else None
602
+ ),
603
+ metadata=message.get("metadata"),
604
+ token_count=message.get("token_count"),
605
+ )
606
+ return True
607
+ except Exception as e:
608
+ if self.enable_logging:
609
+ self.logger.error(
610
+ f"Failed to load conversation from JSON: {e}"
611
+ )
612
+ return False
613
+
614
+ def load_from_yaml(self, filename: str) -> bool:
615
+ """
616
+ Load a conversation from a YAML file.
617
+
618
+ Args:
619
+ filename (str): Path to the YAML file
620
+
621
+ Returns:
622
+ bool: True if load was successful
623
+ """
624
+ try:
625
+ with open(filename, "r") as f:
626
+ messages = yaml.safe_load(f)
627
+
628
+ # Start a new conversation
629
+ self.start_new_conversation()
630
+
631
+ # Add all messages
632
+ for message in messages:
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 YAML: {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
+ cursor = conn.cursor()
661
+ cursor.execute(
662
+ f"""
663
+ SELECT role, content, timestamp, message_type, metadata, token_count
664
+ FROM {self.table_name}
665
+ WHERE conversation_id = ?
666
+ ORDER BY id DESC
667
+ LIMIT 1
668
+ """,
669
+ (self.current_conversation_id,),
670
+ )
671
+
672
+ row = cursor.fetchone()
673
+ if not row:
674
+ return None
675
+
676
+ content = row["content"]
677
+ try:
678
+ content = json.loads(content)
679
+ except json.JSONDecodeError:
680
+ pass
681
+
682
+ message = {"role": row["role"], "content": content}
683
+
684
+ if row["timestamp"]:
685
+ message["timestamp"] = row["timestamp"]
686
+ if row["message_type"]:
687
+ message["message_type"] = row["message_type"]
688
+ if row["metadata"]:
689
+ message["metadata"] = json.loads(row["metadata"])
690
+ if row["token_count"]:
691
+ message["token_count"] = row["token_count"]
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
+ cursor = conn.cursor()
722
+ cursor.execute(
723
+ f"""
724
+ SELECT role, COUNT(*) as count
725
+ FROM {self.table_name}
726
+ WHERE conversation_id = ?
727
+ GROUP BY role
728
+ """,
729
+ (self.current_conversation_id,),
730
+ )
731
+
732
+ return {
733
+ row["role"]: row["count"] for row in cursor.fetchall()
734
+ }
735
+
736
+ def get_messages_by_role(self, role: str) -> List[Dict]:
737
+ """
738
+ Get all messages from a specific role in the current conversation.
739
+
740
+ Args:
741
+ role (str): Role to filter messages by
742
+
743
+ Returns:
744
+ List[Dict]: List of messages from the specified role
745
+ """
746
+ with self._get_connection() as conn:
747
+ cursor = conn.cursor()
748
+ cursor.execute(
749
+ f"""
750
+ SELECT role, content, timestamp, message_type, metadata, token_count
751
+ FROM {self.table_name}
752
+ WHERE conversation_id = ? AND role = ?
753
+ ORDER BY id ASC
754
+ """,
755
+ (self.current_conversation_id, role),
756
+ )
757
+
758
+ messages = []
759
+ for row in cursor.fetchall():
760
+ content = row["content"]
761
+ try:
762
+ content = json.loads(content)
763
+ except json.JSONDecodeError:
764
+ pass
765
+
766
+ message = {"role": row["role"], "content": content}
767
+
768
+ if row["timestamp"]:
769
+ message["timestamp"] = row["timestamp"]
770
+ if row["message_type"]:
771
+ message["message_type"] = row["message_type"]
772
+ if row["metadata"]:
773
+ message["metadata"] = json.loads(row["metadata"])
774
+ if row["token_count"]:
775
+ message["token_count"] = row["token_count"]
776
+
777
+ messages.append(message)
778
+
779
+ return messages
780
+
781
+ def get_conversation_summary(self) -> Dict:
782
+ """
783
+ Get a summary of the current conversation.
784
+
785
+ Returns:
786
+ Dict: Summary of the conversation including message counts, roles, and time range
787
+ """
788
+ with self._get_connection() as conn:
789
+ cursor = conn.cursor()
790
+ cursor.execute(
791
+ f"""
792
+ SELECT
793
+ COUNT(*) as total_messages,
794
+ COUNT(DISTINCT role) as unique_roles,
795
+ MIN(timestamp) as first_message_time,
796
+ MAX(timestamp) as last_message_time,
797
+ SUM(token_count) as total_tokens
798
+ FROM {self.table_name}
799
+ WHERE conversation_id = ?
800
+ """,
801
+ (self.current_conversation_id,),
802
+ )
803
+
804
+ row = cursor.fetchone()
805
+ return {
806
+ "conversation_id": self.current_conversation_id,
807
+ "total_messages": row["total_messages"],
808
+ "unique_roles": row["unique_roles"],
809
+ "first_message_time": row["first_message_time"],
810
+ "last_message_time": row["last_message_time"],
811
+ "total_tokens": row["total_tokens"],
812
+ "roles": self.count_messages_by_role(),
813
+ }