swarms 7.7.8__py3-none-any.whl → 7.8.0__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/__init__.py +0 -1
- swarms/agents/cort_agent.py +206 -0
- swarms/agents/react_agent.py +173 -0
- swarms/agents/self_agent_builder.py +40 -0
- swarms/communication/base_communication.py +290 -0
- swarms/communication/duckdb_wrap.py +369 -72
- swarms/communication/pulsar_struct.py +691 -0
- swarms/communication/redis_wrap.py +1362 -0
- swarms/communication/sqlite_wrap.py +547 -44
- swarms/prompts/agent_self_builder_prompt.py +103 -0
- swarms/prompts/safety_prompt.py +50 -0
- swarms/schemas/__init__.py +6 -1
- swarms/schemas/agent_class_schema.py +91 -0
- swarms/schemas/agent_mcp_errors.py +18 -0
- swarms/schemas/agent_tool_schema.py +13 -0
- swarms/schemas/llm_agent_schema.py +92 -0
- swarms/schemas/mcp_schemas.py +43 -0
- swarms/structs/__init__.py +4 -0
- swarms/structs/agent.py +315 -267
- swarms/structs/aop.py +3 -1
- swarms/structs/batch_agent_execution.py +64 -0
- swarms/structs/conversation.py +261 -57
- swarms/structs/council_judge.py +542 -0
- swarms/structs/deep_research_swarm.py +19 -22
- swarms/structs/long_agent.py +424 -0
- swarms/structs/ma_utils.py +11 -8
- swarms/structs/malt.py +30 -28
- swarms/structs/multi_model_gpu_manager.py +1 -1
- swarms/structs/output_types.py +1 -1
- swarms/structs/swarm_router.py +70 -15
- swarms/tools/__init__.py +12 -0
- swarms/tools/base_tool.py +2840 -264
- swarms/tools/create_agent_tool.py +104 -0
- swarms/tools/mcp_client_call.py +504 -0
- swarms/tools/py_func_to_openai_func_str.py +45 -7
- swarms/tools/pydantic_to_json.py +10 -27
- swarms/utils/audio_processing.py +343 -0
- swarms/utils/history_output_formatter.py +5 -5
- swarms/utils/index.py +226 -0
- swarms/utils/litellm_wrapper.py +65 -67
- swarms/utils/try_except_wrapper.py +2 -2
- swarms/utils/xml_utils.py +42 -0
- {swarms-7.7.8.dist-info → swarms-7.8.0.dist-info}/METADATA +5 -4
- {swarms-7.7.8.dist-info → swarms-7.8.0.dist-info}/RECORD +47 -30
- {swarms-7.7.8.dist-info → swarms-7.8.0.dist-info}/WHEEL +1 -1
- swarms/client/__init__.py +0 -15
- swarms/client/main.py +0 -407
- swarms/tools/mcp_client.py +0 -246
- swarms/tools/mcp_integration.py +0 -340
- {swarms-7.7.8.dist-info → swarms-7.8.0.dist-info}/LICENSE +0 -0
- {swarms-7.7.8.dist-info → swarms-7.8.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,691 @@
|
|
1
|
+
import json
|
2
|
+
import yaml
|
3
|
+
import threading
|
4
|
+
from typing import Any, Dict, List, Optional, Union
|
5
|
+
from datetime import datetime
|
6
|
+
import uuid
|
7
|
+
from loguru import logger
|
8
|
+
from swarms.communication.base_communication import (
|
9
|
+
BaseCommunication,
|
10
|
+
Message,
|
11
|
+
MessageType,
|
12
|
+
)
|
13
|
+
|
14
|
+
|
15
|
+
# Check if Pulsar is available
|
16
|
+
try:
|
17
|
+
import pulsar
|
18
|
+
|
19
|
+
PULSAR_AVAILABLE = True
|
20
|
+
logger.info("Apache Pulsar client library is available")
|
21
|
+
except ImportError as e:
|
22
|
+
PULSAR_AVAILABLE = False
|
23
|
+
logger.error(
|
24
|
+
f"Apache Pulsar client library is not installed: {e}"
|
25
|
+
)
|
26
|
+
logger.error("Please install it using: pip install pulsar-client")
|
27
|
+
|
28
|
+
|
29
|
+
class PulsarConnectionError(Exception):
|
30
|
+
"""Exception raised for Pulsar connection errors."""
|
31
|
+
|
32
|
+
pass
|
33
|
+
|
34
|
+
|
35
|
+
class PulsarOperationError(Exception):
|
36
|
+
"""Exception raised for Pulsar operation errors."""
|
37
|
+
|
38
|
+
pass
|
39
|
+
|
40
|
+
|
41
|
+
class PulsarConversation(BaseCommunication):
|
42
|
+
"""
|
43
|
+
A Pulsar-based implementation of the conversation interface.
|
44
|
+
Uses Apache Pulsar for message storage and retrieval.
|
45
|
+
|
46
|
+
Attributes:
|
47
|
+
client (pulsar.Client): The Pulsar client instance
|
48
|
+
producer (pulsar.Producer): The Pulsar producer for sending messages
|
49
|
+
consumer (pulsar.Consumer): The Pulsar consumer for receiving messages
|
50
|
+
topic (str): The Pulsar topic name
|
51
|
+
subscription_name (str): The subscription name for the consumer
|
52
|
+
conversation_id (str): Unique identifier for the conversation
|
53
|
+
cache_enabled (bool): Flag to enable prompt caching
|
54
|
+
cache_stats (dict): Statistics about cache usage
|
55
|
+
cache_lock (threading.Lock): Lock for thread-safe cache operations
|
56
|
+
"""
|
57
|
+
|
58
|
+
def __init__(
|
59
|
+
self,
|
60
|
+
system_prompt: Optional[str] = None,
|
61
|
+
time_enabled: bool = False,
|
62
|
+
autosave: bool = False,
|
63
|
+
save_filepath: str = None,
|
64
|
+
tokenizer: Any = None,
|
65
|
+
context_length: int = 8192,
|
66
|
+
rules: str = None,
|
67
|
+
custom_rules_prompt: str = None,
|
68
|
+
user: str = "User:",
|
69
|
+
auto_save: bool = True,
|
70
|
+
save_as_yaml: bool = True,
|
71
|
+
save_as_json_bool: bool = False,
|
72
|
+
token_count: bool = True,
|
73
|
+
cache_enabled: bool = True,
|
74
|
+
pulsar_host: str = "pulsar://localhost:6650",
|
75
|
+
topic: str = "conversation",
|
76
|
+
*args,
|
77
|
+
**kwargs,
|
78
|
+
):
|
79
|
+
"""Initialize the Pulsar conversation interface."""
|
80
|
+
if not PULSAR_AVAILABLE:
|
81
|
+
raise ImportError(
|
82
|
+
"Apache Pulsar client library is not installed. "
|
83
|
+
"Please install it using: pip install pulsar-client"
|
84
|
+
)
|
85
|
+
|
86
|
+
logger.info(
|
87
|
+
f"Initializing PulsarConversation with host: {pulsar_host}"
|
88
|
+
)
|
89
|
+
|
90
|
+
self.conversation_id = str(uuid.uuid4())
|
91
|
+
self.topic = f"{topic}-{self.conversation_id}"
|
92
|
+
self.subscription_name = f"sub-{self.conversation_id}"
|
93
|
+
|
94
|
+
try:
|
95
|
+
# Initialize Pulsar client and producer/consumer
|
96
|
+
logger.debug(
|
97
|
+
f"Connecting to Pulsar broker at {pulsar_host}"
|
98
|
+
)
|
99
|
+
self.client = pulsar.Client(pulsar_host)
|
100
|
+
|
101
|
+
logger.debug(f"Creating producer for topic: {self.topic}")
|
102
|
+
self.producer = self.client.create_producer(self.topic)
|
103
|
+
|
104
|
+
logger.debug(
|
105
|
+
f"Creating consumer with subscription: {self.subscription_name}"
|
106
|
+
)
|
107
|
+
self.consumer = self.client.subscribe(
|
108
|
+
self.topic, self.subscription_name
|
109
|
+
)
|
110
|
+
logger.info("Successfully connected to Pulsar broker")
|
111
|
+
|
112
|
+
except pulsar.ConnectError as e:
|
113
|
+
error_msg = f"Failed to connect to Pulsar broker at {pulsar_host}: {str(e)}"
|
114
|
+
logger.error(error_msg)
|
115
|
+
raise PulsarConnectionError(error_msg)
|
116
|
+
except Exception as e:
|
117
|
+
error_msg = f"Unexpected error while initializing Pulsar connection: {str(e)}"
|
118
|
+
logger.error(error_msg)
|
119
|
+
raise PulsarOperationError(error_msg)
|
120
|
+
|
121
|
+
# Store configuration
|
122
|
+
self.system_prompt = system_prompt
|
123
|
+
self.time_enabled = time_enabled
|
124
|
+
self.autosave = autosave
|
125
|
+
self.save_filepath = save_filepath
|
126
|
+
self.tokenizer = tokenizer
|
127
|
+
self.context_length = context_length
|
128
|
+
self.rules = rules
|
129
|
+
self.custom_rules_prompt = custom_rules_prompt
|
130
|
+
self.user = user
|
131
|
+
self.auto_save = auto_save
|
132
|
+
self.save_as_yaml = save_as_yaml
|
133
|
+
self.save_as_json_bool = save_as_json_bool
|
134
|
+
self.token_count = token_count
|
135
|
+
|
136
|
+
# Cache configuration
|
137
|
+
self.cache_enabled = cache_enabled
|
138
|
+
self.cache_stats = {
|
139
|
+
"hits": 0,
|
140
|
+
"misses": 0,
|
141
|
+
"cached_tokens": 0,
|
142
|
+
"total_tokens": 0,
|
143
|
+
}
|
144
|
+
self.cache_lock = threading.Lock()
|
145
|
+
|
146
|
+
# Add system prompt if provided
|
147
|
+
if system_prompt:
|
148
|
+
logger.debug("Adding system prompt to conversation")
|
149
|
+
self.add("system", system_prompt, MessageType.SYSTEM)
|
150
|
+
|
151
|
+
# Add rules if provided
|
152
|
+
if rules:
|
153
|
+
logger.debug("Adding rules to conversation")
|
154
|
+
self.add("system", rules, MessageType.SYSTEM)
|
155
|
+
|
156
|
+
# Add custom rules prompt if provided
|
157
|
+
if custom_rules_prompt:
|
158
|
+
logger.debug("Adding custom rules prompt to conversation")
|
159
|
+
self.add(user, custom_rules_prompt, MessageType.USER)
|
160
|
+
|
161
|
+
logger.info(
|
162
|
+
f"PulsarConversation initialized with ID: {self.conversation_id}"
|
163
|
+
)
|
164
|
+
|
165
|
+
def add(
|
166
|
+
self,
|
167
|
+
role: str,
|
168
|
+
content: Union[str, dict, list],
|
169
|
+
message_type: Optional[MessageType] = None,
|
170
|
+
metadata: Optional[Dict] = None,
|
171
|
+
token_count: Optional[int] = None,
|
172
|
+
) -> int:
|
173
|
+
"""Add a message to the conversation."""
|
174
|
+
try:
|
175
|
+
message = {
|
176
|
+
"id": str(uuid.uuid4()),
|
177
|
+
"role": role,
|
178
|
+
"content": content,
|
179
|
+
"timestamp": datetime.now().isoformat(),
|
180
|
+
"message_type": (
|
181
|
+
message_type.value if message_type else None
|
182
|
+
),
|
183
|
+
"metadata": metadata or {},
|
184
|
+
"token_count": token_count,
|
185
|
+
"conversation_id": self.conversation_id,
|
186
|
+
}
|
187
|
+
|
188
|
+
logger.debug(
|
189
|
+
f"Adding message with ID {message['id']} from role: {role}"
|
190
|
+
)
|
191
|
+
|
192
|
+
# Send message to Pulsar
|
193
|
+
message_data = json.dumps(message).encode("utf-8")
|
194
|
+
self.producer.send(message_data)
|
195
|
+
|
196
|
+
logger.debug(
|
197
|
+
f"Successfully added message with ID: {message['id']}"
|
198
|
+
)
|
199
|
+
return message["id"]
|
200
|
+
|
201
|
+
except pulsar.ConnectError as e:
|
202
|
+
error_msg = f"Failed to send message to Pulsar: Connection error: {str(e)}"
|
203
|
+
logger.error(error_msg)
|
204
|
+
raise PulsarConnectionError(error_msg)
|
205
|
+
except Exception as e:
|
206
|
+
error_msg = f"Failed to add message: {str(e)}"
|
207
|
+
logger.error(error_msg)
|
208
|
+
raise PulsarOperationError(error_msg)
|
209
|
+
|
210
|
+
def batch_add(self, messages: List[Message]) -> List[int]:
|
211
|
+
"""Add multiple messages to the conversation."""
|
212
|
+
message_ids = []
|
213
|
+
for message in messages:
|
214
|
+
msg_id = self.add(
|
215
|
+
message.role,
|
216
|
+
message.content,
|
217
|
+
message.message_type,
|
218
|
+
message.metadata,
|
219
|
+
message.token_count,
|
220
|
+
)
|
221
|
+
message_ids.append(msg_id)
|
222
|
+
return message_ids
|
223
|
+
|
224
|
+
def get_messages(
|
225
|
+
self,
|
226
|
+
limit: Optional[int] = None,
|
227
|
+
offset: Optional[int] = None,
|
228
|
+
) -> List[Dict]:
|
229
|
+
"""Get messages with optional pagination."""
|
230
|
+
messages = []
|
231
|
+
try:
|
232
|
+
logger.debug("Retrieving messages from Pulsar")
|
233
|
+
while True:
|
234
|
+
try:
|
235
|
+
msg = self.consumer.receive(timeout_millis=1000)
|
236
|
+
messages.append(json.loads(msg.data()))
|
237
|
+
self.consumer.acknowledge(msg)
|
238
|
+
except pulsar.Timeout:
|
239
|
+
break # No more messages available
|
240
|
+
except json.JSONDecodeError as e:
|
241
|
+
logger.error(f"Failed to decode message: {e}")
|
242
|
+
continue
|
243
|
+
|
244
|
+
logger.debug(f"Retrieved {len(messages)} messages")
|
245
|
+
|
246
|
+
if offset is not None:
|
247
|
+
messages = messages[offset:]
|
248
|
+
if limit is not None:
|
249
|
+
messages = messages[:limit]
|
250
|
+
|
251
|
+
return messages
|
252
|
+
|
253
|
+
except pulsar.ConnectError as e:
|
254
|
+
error_msg = f"Failed to receive messages from Pulsar: Connection error: {str(e)}"
|
255
|
+
logger.error(error_msg)
|
256
|
+
raise PulsarConnectionError(error_msg)
|
257
|
+
except Exception as e:
|
258
|
+
error_msg = f"Failed to get messages: {str(e)}"
|
259
|
+
logger.error(error_msg)
|
260
|
+
raise PulsarOperationError(error_msg)
|
261
|
+
|
262
|
+
def delete(self, message_id: str):
|
263
|
+
"""Delete a message from the conversation."""
|
264
|
+
# In Pulsar, messages cannot be deleted individually
|
265
|
+
# We would need to implement a soft delete by marking messages
|
266
|
+
pass
|
267
|
+
|
268
|
+
def update(
|
269
|
+
self, message_id: str, role: str, content: Union[str, dict]
|
270
|
+
):
|
271
|
+
"""Update a message in the conversation."""
|
272
|
+
# In Pulsar, messages are immutable
|
273
|
+
# We would need to implement updates as new messages with update metadata
|
274
|
+
new_message = {
|
275
|
+
"id": str(uuid.uuid4()),
|
276
|
+
"role": role,
|
277
|
+
"content": content,
|
278
|
+
"timestamp": datetime.now().isoformat(),
|
279
|
+
"updates": message_id,
|
280
|
+
"conversation_id": self.conversation_id,
|
281
|
+
}
|
282
|
+
self.producer.send(json.dumps(new_message).encode("utf-8"))
|
283
|
+
|
284
|
+
def query(self, message_id: str) -> Dict:
|
285
|
+
"""Query a message in the conversation."""
|
286
|
+
messages = self.get_messages()
|
287
|
+
for message in messages:
|
288
|
+
if message["id"] == message_id:
|
289
|
+
return message
|
290
|
+
return None
|
291
|
+
|
292
|
+
def search(self, keyword: str) -> List[Dict]:
|
293
|
+
"""Search for messages containing a keyword."""
|
294
|
+
messages = self.get_messages()
|
295
|
+
return [
|
296
|
+
msg for msg in messages if keyword in str(msg["content"])
|
297
|
+
]
|
298
|
+
|
299
|
+
def get_str(self) -> str:
|
300
|
+
"""Get the conversation history as a string."""
|
301
|
+
messages = self.get_messages()
|
302
|
+
return "\n".join(
|
303
|
+
[f"{msg['role']}: {msg['content']}" for msg in messages]
|
304
|
+
)
|
305
|
+
|
306
|
+
def display_conversation(self, detailed: bool = False):
|
307
|
+
"""Display the conversation history."""
|
308
|
+
messages = self.get_messages()
|
309
|
+
for msg in messages:
|
310
|
+
if detailed:
|
311
|
+
print(f"ID: {msg['id']}")
|
312
|
+
print(f"Role: {msg['role']}")
|
313
|
+
print(f"Content: {msg['content']}")
|
314
|
+
print(f"Timestamp: {msg['timestamp']}")
|
315
|
+
print("---")
|
316
|
+
else:
|
317
|
+
print(f"{msg['role']}: {msg['content']}")
|
318
|
+
|
319
|
+
def export_conversation(self, filename: str):
|
320
|
+
"""Export the conversation history to a file."""
|
321
|
+
messages = self.get_messages()
|
322
|
+
with open(filename, "w") as f:
|
323
|
+
json.dump(messages, f, indent=2)
|
324
|
+
|
325
|
+
def import_conversation(self, filename: str):
|
326
|
+
"""Import a conversation history from a file."""
|
327
|
+
with open(filename, "r") as f:
|
328
|
+
messages = json.load(f)
|
329
|
+
for msg in messages:
|
330
|
+
self.add(
|
331
|
+
msg["role"],
|
332
|
+
msg["content"],
|
333
|
+
(
|
334
|
+
MessageType(msg["message_type"])
|
335
|
+
if msg.get("message_type")
|
336
|
+
else None
|
337
|
+
),
|
338
|
+
msg.get("metadata"),
|
339
|
+
msg.get("token_count"),
|
340
|
+
)
|
341
|
+
|
342
|
+
def count_messages_by_role(self) -> Dict[str, int]:
|
343
|
+
"""Count messages by role."""
|
344
|
+
messages = self.get_messages()
|
345
|
+
counts = {}
|
346
|
+
for msg in messages:
|
347
|
+
role = msg["role"]
|
348
|
+
counts[role] = counts.get(role, 0) + 1
|
349
|
+
return counts
|
350
|
+
|
351
|
+
def return_history_as_string(self) -> str:
|
352
|
+
"""Return the conversation history as a string."""
|
353
|
+
return self.get_str()
|
354
|
+
|
355
|
+
def clear(self):
|
356
|
+
"""Clear the conversation history."""
|
357
|
+
try:
|
358
|
+
logger.info(
|
359
|
+
f"Clearing conversation with ID: {self.conversation_id}"
|
360
|
+
)
|
361
|
+
|
362
|
+
# Close existing producer and consumer
|
363
|
+
if hasattr(self, "consumer"):
|
364
|
+
self.consumer.close()
|
365
|
+
if hasattr(self, "producer"):
|
366
|
+
self.producer.close()
|
367
|
+
|
368
|
+
# Create new conversation ID and topic
|
369
|
+
self.conversation_id = str(uuid.uuid4())
|
370
|
+
self.topic = f"conversation-{self.conversation_id}"
|
371
|
+
self.subscription_name = f"sub-{self.conversation_id}"
|
372
|
+
|
373
|
+
# Recreate producer and consumer
|
374
|
+
logger.debug(
|
375
|
+
f"Creating new producer for topic: {self.topic}"
|
376
|
+
)
|
377
|
+
self.producer = self.client.create_producer(self.topic)
|
378
|
+
|
379
|
+
logger.debug(
|
380
|
+
f"Creating new consumer with subscription: {self.subscription_name}"
|
381
|
+
)
|
382
|
+
self.consumer = self.client.subscribe(
|
383
|
+
self.topic, self.subscription_name
|
384
|
+
)
|
385
|
+
|
386
|
+
logger.info(
|
387
|
+
f"Successfully cleared conversation. New ID: {self.conversation_id}"
|
388
|
+
)
|
389
|
+
|
390
|
+
except pulsar.ConnectError as e:
|
391
|
+
error_msg = f"Failed to clear conversation: Connection error: {str(e)}"
|
392
|
+
logger.error(error_msg)
|
393
|
+
raise PulsarConnectionError(error_msg)
|
394
|
+
except Exception as e:
|
395
|
+
error_msg = f"Failed to clear conversation: {str(e)}"
|
396
|
+
logger.error(error_msg)
|
397
|
+
raise PulsarOperationError(error_msg)
|
398
|
+
|
399
|
+
def to_dict(self) -> List[Dict]:
|
400
|
+
"""Convert the conversation history to a dictionary."""
|
401
|
+
return self.get_messages()
|
402
|
+
|
403
|
+
def to_json(self) -> str:
|
404
|
+
"""Convert the conversation history to a JSON string."""
|
405
|
+
return json.dumps(self.to_dict(), indent=2)
|
406
|
+
|
407
|
+
def to_yaml(self) -> str:
|
408
|
+
"""Convert the conversation history to a YAML string."""
|
409
|
+
return yaml.dump(self.to_dict())
|
410
|
+
|
411
|
+
def save_as_json(self, filename: str):
|
412
|
+
"""Save the conversation history as a JSON file."""
|
413
|
+
with open(filename, "w") as f:
|
414
|
+
json.dump(self.to_dict(), f, indent=2)
|
415
|
+
|
416
|
+
def load_from_json(self, filename: str):
|
417
|
+
"""Load the conversation history from a JSON file."""
|
418
|
+
self.import_conversation(filename)
|
419
|
+
|
420
|
+
def save_as_yaml(self, filename: str):
|
421
|
+
"""Save the conversation history as a YAML file."""
|
422
|
+
with open(filename, "w") as f:
|
423
|
+
yaml.dump(self.to_dict(), f)
|
424
|
+
|
425
|
+
def load_from_yaml(self, filename: str):
|
426
|
+
"""Load the conversation history from a YAML file."""
|
427
|
+
with open(filename, "r") as f:
|
428
|
+
messages = yaml.safe_load(f)
|
429
|
+
for msg in messages:
|
430
|
+
self.add(
|
431
|
+
msg["role"],
|
432
|
+
msg["content"],
|
433
|
+
(
|
434
|
+
MessageType(msg["message_type"])
|
435
|
+
if msg.get("message_type")
|
436
|
+
else None
|
437
|
+
),
|
438
|
+
msg.get("metadata"),
|
439
|
+
msg.get("token_count"),
|
440
|
+
)
|
441
|
+
|
442
|
+
def get_last_message(self) -> Optional[Dict]:
|
443
|
+
"""Get the last message from the conversation history."""
|
444
|
+
messages = self.get_messages()
|
445
|
+
return messages[-1] if messages else None
|
446
|
+
|
447
|
+
def get_last_message_as_string(self) -> str:
|
448
|
+
"""Get the last message as a formatted string."""
|
449
|
+
last_message = self.get_last_message()
|
450
|
+
if last_message:
|
451
|
+
return (
|
452
|
+
f"{last_message['role']}: {last_message['content']}"
|
453
|
+
)
|
454
|
+
return ""
|
455
|
+
|
456
|
+
def get_messages_by_role(self, role: str) -> List[Dict]:
|
457
|
+
"""Get all messages from a specific role."""
|
458
|
+
messages = self.get_messages()
|
459
|
+
return [msg for msg in messages if msg["role"] == role]
|
460
|
+
|
461
|
+
def get_conversation_summary(self) -> Dict:
|
462
|
+
"""Get a summary of the conversation."""
|
463
|
+
messages = self.get_messages()
|
464
|
+
return {
|
465
|
+
"conversation_id": self.conversation_id,
|
466
|
+
"message_count": len(messages),
|
467
|
+
"roles": list(set(msg["role"] for msg in messages)),
|
468
|
+
"start_time": (
|
469
|
+
messages[0]["timestamp"] if messages else None
|
470
|
+
),
|
471
|
+
"end_time": (
|
472
|
+
messages[-1]["timestamp"] if messages else None
|
473
|
+
),
|
474
|
+
}
|
475
|
+
|
476
|
+
def get_statistics(self) -> Dict:
|
477
|
+
"""Get statistics about the conversation."""
|
478
|
+
messages = self.get_messages()
|
479
|
+
return {
|
480
|
+
"total_messages": len(messages),
|
481
|
+
"messages_by_role": self.count_messages_by_role(),
|
482
|
+
"cache_stats": self.get_cache_stats(),
|
483
|
+
}
|
484
|
+
|
485
|
+
def get_conversation_id(self) -> str:
|
486
|
+
"""Get the current conversation ID."""
|
487
|
+
return self.conversation_id
|
488
|
+
|
489
|
+
def start_new_conversation(self) -> str:
|
490
|
+
"""Start a new conversation and return its ID."""
|
491
|
+
self.clear()
|
492
|
+
return self.conversation_id
|
493
|
+
|
494
|
+
def delete_current_conversation(self) -> bool:
|
495
|
+
"""Delete the current conversation."""
|
496
|
+
self.clear()
|
497
|
+
return True
|
498
|
+
|
499
|
+
def search_messages(self, query: str) -> List[Dict]:
|
500
|
+
"""Search for messages containing specific text."""
|
501
|
+
return self.search(query)
|
502
|
+
|
503
|
+
def update_message(
|
504
|
+
self,
|
505
|
+
message_id: int,
|
506
|
+
content: Union[str, dict, list],
|
507
|
+
metadata: Optional[Dict] = None,
|
508
|
+
) -> bool:
|
509
|
+
"""Update an existing message."""
|
510
|
+
message = self.query(message_id)
|
511
|
+
if message:
|
512
|
+
self.update(message_id, message["role"], content)
|
513
|
+
return True
|
514
|
+
return False
|
515
|
+
|
516
|
+
def get_conversation_metadata_dict(self) -> Dict:
|
517
|
+
"""Get detailed metadata about the conversation."""
|
518
|
+
return self.get_conversation_summary()
|
519
|
+
|
520
|
+
def get_conversation_timeline_dict(self) -> Dict[str, List[Dict]]:
|
521
|
+
"""Get the conversation organized by timestamps."""
|
522
|
+
messages = self.get_messages()
|
523
|
+
timeline = {}
|
524
|
+
for msg in messages:
|
525
|
+
date = msg["timestamp"].split("T")[0]
|
526
|
+
if date not in timeline:
|
527
|
+
timeline[date] = []
|
528
|
+
timeline[date].append(msg)
|
529
|
+
return timeline
|
530
|
+
|
531
|
+
def get_conversation_by_role_dict(self) -> Dict[str, List[Dict]]:
|
532
|
+
"""Get the conversation organized by roles."""
|
533
|
+
messages = self.get_messages()
|
534
|
+
by_role = {}
|
535
|
+
for msg in messages:
|
536
|
+
role = msg["role"]
|
537
|
+
if role not in by_role:
|
538
|
+
by_role[role] = []
|
539
|
+
by_role[role].append(msg)
|
540
|
+
return by_role
|
541
|
+
|
542
|
+
def get_conversation_as_dict(self) -> Dict:
|
543
|
+
"""Get the entire conversation as a dictionary with messages and metadata."""
|
544
|
+
return {
|
545
|
+
"metadata": self.get_conversation_metadata_dict(),
|
546
|
+
"messages": self.get_messages(),
|
547
|
+
"statistics": self.get_statistics(),
|
548
|
+
}
|
549
|
+
|
550
|
+
def truncate_memory_with_tokenizer(self):
|
551
|
+
"""Truncate the conversation history based on token count."""
|
552
|
+
if not self.tokenizer:
|
553
|
+
return
|
554
|
+
|
555
|
+
messages = self.get_messages()
|
556
|
+
total_tokens = 0
|
557
|
+
truncated_messages = []
|
558
|
+
|
559
|
+
for msg in messages:
|
560
|
+
content = msg["content"]
|
561
|
+
tokens = self.tokenizer.count_tokens(str(content))
|
562
|
+
|
563
|
+
if total_tokens + tokens <= self.context_length:
|
564
|
+
truncated_messages.append(msg)
|
565
|
+
total_tokens += tokens
|
566
|
+
else:
|
567
|
+
break
|
568
|
+
|
569
|
+
# Clear and re-add truncated messages
|
570
|
+
self.clear()
|
571
|
+
for msg in truncated_messages:
|
572
|
+
self.add(
|
573
|
+
msg["role"],
|
574
|
+
msg["content"],
|
575
|
+
(
|
576
|
+
MessageType(msg["message_type"])
|
577
|
+
if msg.get("message_type")
|
578
|
+
else None
|
579
|
+
),
|
580
|
+
msg.get("metadata"),
|
581
|
+
msg.get("token_count"),
|
582
|
+
)
|
583
|
+
|
584
|
+
def get_cache_stats(self) -> Dict[str, int]:
|
585
|
+
"""Get statistics about cache usage."""
|
586
|
+
with self.cache_lock:
|
587
|
+
return {
|
588
|
+
"hits": self.cache_stats["hits"],
|
589
|
+
"misses": self.cache_stats["misses"],
|
590
|
+
"cached_tokens": self.cache_stats["cached_tokens"],
|
591
|
+
"total_tokens": self.cache_stats["total_tokens"],
|
592
|
+
"hit_rate": (
|
593
|
+
self.cache_stats["hits"]
|
594
|
+
/ (
|
595
|
+
self.cache_stats["hits"]
|
596
|
+
+ self.cache_stats["misses"]
|
597
|
+
)
|
598
|
+
if (
|
599
|
+
self.cache_stats["hits"]
|
600
|
+
+ self.cache_stats["misses"]
|
601
|
+
)
|
602
|
+
> 0
|
603
|
+
else 0
|
604
|
+
),
|
605
|
+
}
|
606
|
+
|
607
|
+
def __del__(self):
|
608
|
+
"""Cleanup Pulsar resources."""
|
609
|
+
try:
|
610
|
+
logger.debug("Cleaning up Pulsar resources")
|
611
|
+
if hasattr(self, "consumer"):
|
612
|
+
self.consumer.close()
|
613
|
+
if hasattr(self, "producer"):
|
614
|
+
self.producer.close()
|
615
|
+
if hasattr(self, "client"):
|
616
|
+
self.client.close()
|
617
|
+
logger.info("Successfully cleaned up Pulsar resources")
|
618
|
+
except Exception as e:
|
619
|
+
logger.error(f"Error during cleanup: {str(e)}")
|
620
|
+
|
621
|
+
@classmethod
|
622
|
+
def check_pulsar_availability(
|
623
|
+
cls, pulsar_host: str = "pulsar://localhost:6650"
|
624
|
+
) -> bool:
|
625
|
+
"""
|
626
|
+
Check if Pulsar is available and accessible.
|
627
|
+
|
628
|
+
Args:
|
629
|
+
pulsar_host (str): The Pulsar host to check
|
630
|
+
|
631
|
+
Returns:
|
632
|
+
bool: True if Pulsar is available and accessible, False otherwise
|
633
|
+
"""
|
634
|
+
if not PULSAR_AVAILABLE:
|
635
|
+
logger.error("Pulsar client library is not installed")
|
636
|
+
return False
|
637
|
+
|
638
|
+
try:
|
639
|
+
logger.debug(
|
640
|
+
f"Checking Pulsar availability at {pulsar_host}"
|
641
|
+
)
|
642
|
+
client = pulsar.Client(pulsar_host)
|
643
|
+
client.close()
|
644
|
+
logger.info("Pulsar is available and accessible")
|
645
|
+
return True
|
646
|
+
except Exception as e:
|
647
|
+
logger.error(f"Pulsar is not accessible: {str(e)}")
|
648
|
+
return False
|
649
|
+
|
650
|
+
def health_check(self) -> Dict[str, bool]:
|
651
|
+
"""
|
652
|
+
Perform a health check of the Pulsar connection and components.
|
653
|
+
|
654
|
+
Returns:
|
655
|
+
Dict[str, bool]: Health status of different components
|
656
|
+
"""
|
657
|
+
health = {
|
658
|
+
"client_connected": False,
|
659
|
+
"producer_active": False,
|
660
|
+
"consumer_active": False,
|
661
|
+
}
|
662
|
+
|
663
|
+
try:
|
664
|
+
# Check client
|
665
|
+
if hasattr(self, "client"):
|
666
|
+
health["client_connected"] = True
|
667
|
+
|
668
|
+
# Check producer
|
669
|
+
if hasattr(self, "producer"):
|
670
|
+
# Try to send a test message
|
671
|
+
test_msg = json.dumps(
|
672
|
+
{"type": "health_check"}
|
673
|
+
).encode("utf-8")
|
674
|
+
self.producer.send(test_msg)
|
675
|
+
health["producer_active"] = True
|
676
|
+
|
677
|
+
# Check consumer
|
678
|
+
if hasattr(self, "consumer"):
|
679
|
+
try:
|
680
|
+
msg = self.consumer.receive(timeout_millis=1000)
|
681
|
+
self.consumer.acknowledge(msg)
|
682
|
+
health["consumer_active"] = True
|
683
|
+
except pulsar.Timeout:
|
684
|
+
pass
|
685
|
+
|
686
|
+
logger.info(f"Health check results: {health}")
|
687
|
+
return health
|
688
|
+
|
689
|
+
except Exception as e:
|
690
|
+
logger.error(f"Health check failed: {str(e)}")
|
691
|
+
return health
|