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.
Files changed (51) hide show
  1. swarms/__init__.py +0 -1
  2. swarms/agents/cort_agent.py +206 -0
  3. swarms/agents/react_agent.py +173 -0
  4. swarms/agents/self_agent_builder.py +40 -0
  5. swarms/communication/base_communication.py +290 -0
  6. swarms/communication/duckdb_wrap.py +369 -72
  7. swarms/communication/pulsar_struct.py +691 -0
  8. swarms/communication/redis_wrap.py +1362 -0
  9. swarms/communication/sqlite_wrap.py +547 -44
  10. swarms/prompts/agent_self_builder_prompt.py +103 -0
  11. swarms/prompts/safety_prompt.py +50 -0
  12. swarms/schemas/__init__.py +6 -1
  13. swarms/schemas/agent_class_schema.py +91 -0
  14. swarms/schemas/agent_mcp_errors.py +18 -0
  15. swarms/schemas/agent_tool_schema.py +13 -0
  16. swarms/schemas/llm_agent_schema.py +92 -0
  17. swarms/schemas/mcp_schemas.py +43 -0
  18. swarms/structs/__init__.py +4 -0
  19. swarms/structs/agent.py +315 -267
  20. swarms/structs/aop.py +3 -1
  21. swarms/structs/batch_agent_execution.py +64 -0
  22. swarms/structs/conversation.py +261 -57
  23. swarms/structs/council_judge.py +542 -0
  24. swarms/structs/deep_research_swarm.py +19 -22
  25. swarms/structs/long_agent.py +424 -0
  26. swarms/structs/ma_utils.py +11 -8
  27. swarms/structs/malt.py +30 -28
  28. swarms/structs/multi_model_gpu_manager.py +1 -1
  29. swarms/structs/output_types.py +1 -1
  30. swarms/structs/swarm_router.py +70 -15
  31. swarms/tools/__init__.py +12 -0
  32. swarms/tools/base_tool.py +2840 -264
  33. swarms/tools/create_agent_tool.py +104 -0
  34. swarms/tools/mcp_client_call.py +504 -0
  35. swarms/tools/py_func_to_openai_func_str.py +45 -7
  36. swarms/tools/pydantic_to_json.py +10 -27
  37. swarms/utils/audio_processing.py +343 -0
  38. swarms/utils/history_output_formatter.py +5 -5
  39. swarms/utils/index.py +226 -0
  40. swarms/utils/litellm_wrapper.py +65 -67
  41. swarms/utils/try_except_wrapper.py +2 -2
  42. swarms/utils/xml_utils.py +42 -0
  43. {swarms-7.7.8.dist-info → swarms-7.8.0.dist-info}/METADATA +5 -4
  44. {swarms-7.7.8.dist-info → swarms-7.8.0.dist-info}/RECORD +47 -30
  45. {swarms-7.7.8.dist-info → swarms-7.8.0.dist-info}/WHEEL +1 -1
  46. swarms/client/__init__.py +0 -15
  47. swarms/client/main.py +0 -407
  48. swarms/tools/mcp_client.py +0 -246
  49. swarms/tools/mcp_integration.py +0 -340
  50. {swarms-7.7.8.dist-info → swarms-7.8.0.dist-info}/LICENSE +0 -0
  51. {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