praisonaiagents 0.0.141__py3-none-any.whl → 0.0.143__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.
@@ -5,6 +5,7 @@ import time
5
5
  import shutil
6
6
  from typing import Any, Dict, List, Optional, Union, Literal
7
7
  import logging
8
+ from datetime import datetime
8
9
 
9
10
  # Disable litellm telemetry before any imports
10
11
  os.environ["LITELLM_TELEMETRY"] = "False"
@@ -39,6 +40,13 @@ try:
39
40
  except ImportError:
40
41
  LITELLM_AVAILABLE = False
41
42
 
43
+ try:
44
+ import pymongo
45
+ from pymongo import MongoClient
46
+ PYMONGO_AVAILABLE = True
47
+ except ImportError:
48
+ PYMONGO_AVAILABLE = False
49
+
42
50
 
43
51
 
44
52
 
@@ -55,7 +63,7 @@ class Memory:
55
63
 
56
64
  Config example:
57
65
  {
58
- "provider": "rag" or "mem0" or "none",
66
+ "provider": "rag" or "mem0" or "mongodb" or "none",
59
67
  "use_embedding": True,
60
68
  "short_db": "short_term.db",
61
69
  "long_db": "long_term.db",
@@ -65,6 +73,15 @@ class Memory:
65
73
  "org_id": "...",
66
74
  "project_id": "...",
67
75
 
76
+ # MongoDB configuration (if provider is "mongodb")
77
+ "connection_string": "mongodb://localhost:27017/" or "mongodb+srv://user:pass@cluster.mongodb.net/",
78
+ "database": "praisonai",
79
+ "use_vector_search": True, # Enable Atlas Vector Search
80
+ "max_pool_size": 50,
81
+ "min_pool_size": 10,
82
+ "max_idle_time": 30000,
83
+ "server_selection_timeout": 5000,
84
+
68
85
  # Graph memory configuration (optional)
69
86
  "graph_store": {
70
87
  "provider": "neo4j" or "memgraph",
@@ -115,6 +132,7 @@ class Memory:
115
132
  self.provider = self.cfg.get("provider", "rag")
116
133
  self.use_mem0 = (self.provider.lower() == "mem0") and MEM0_AVAILABLE
117
134
  self.use_rag = (self.provider.lower() == "rag") and CHROMADB_AVAILABLE and self.cfg.get("use_embedding", False)
135
+ self.use_mongodb = (self.provider.lower() == "mongodb") and PYMONGO_AVAILABLE
118
136
  self.graph_enabled = False # Initialize graph support flag
119
137
 
120
138
  # Extract embedding model from config
@@ -127,6 +145,10 @@ class Memory:
127
145
 
128
146
  self._log_verbose(f"Using embedding model: {self.embedding_model}")
129
147
 
148
+ # Determine embedding dimensions based on model
149
+ self.embedding_dimensions = self._get_embedding_dimensions(self.embedding_model)
150
+ self._log_verbose(f"Using embedding dimensions: {self.embedding_dimensions}")
151
+
130
152
  # Create .praison directory if it doesn't exist
131
153
  os.makedirs(".praison", exist_ok=True)
132
154
 
@@ -138,9 +160,11 @@ class Memory:
138
160
  self.long_db = self.cfg.get("long_db", ".praison/long_term.db")
139
161
  self._init_ltm()
140
162
 
141
- # Conditionally init Mem0 or local RAG
163
+ # Conditionally init Mem0, MongoDB, or local RAG
142
164
  if self.use_mem0:
143
165
  self._init_mem0()
166
+ elif self.use_mongodb:
167
+ self._init_mongodb()
144
168
  elif self.use_rag:
145
169
  self._init_chroma()
146
170
 
@@ -261,6 +285,143 @@ class Memory:
261
285
  self._log_verbose(f"Failed to initialize ChromaDB: {e}", logging.ERROR)
262
286
  self.use_rag = False
263
287
 
288
+ def _init_mongodb(self):
289
+ """Initialize MongoDB client for memory storage."""
290
+ try:
291
+ mongo_cfg = self.cfg.get("config", {})
292
+ self.connection_string = mongo_cfg.get("connection_string", "mongodb://localhost:27017/")
293
+ self.database_name = mongo_cfg.get("database", "praisonai")
294
+ self.use_vector_search = mongo_cfg.get("use_vector_search", False)
295
+
296
+ # Initialize MongoDB client
297
+ self.mongo_client = MongoClient(
298
+ self.connection_string,
299
+ maxPoolSize=mongo_cfg.get("max_pool_size", 50),
300
+ minPoolSize=mongo_cfg.get("min_pool_size", 10),
301
+ maxIdleTimeMS=mongo_cfg.get("max_idle_time", 30000),
302
+ serverSelectionTimeoutMS=mongo_cfg.get("server_selection_timeout", 5000),
303
+ retryWrites=True,
304
+ retryReads=True
305
+ )
306
+
307
+ # Test connection
308
+ self.mongo_client.admin.command('ping')
309
+
310
+ # Setup database and collections
311
+ self.mongo_db = self.mongo_client[self.database_name]
312
+ self.mongo_short_term = self.mongo_db.short_term_memory
313
+ self.mongo_long_term = self.mongo_db.long_term_memory
314
+ self.mongo_entities = self.mongo_db.entity_memory
315
+ self.mongo_users = self.mongo_db.user_memory
316
+
317
+ # Create indexes for better performance
318
+ self._create_mongodb_indexes()
319
+
320
+ self._log_verbose("MongoDB initialized successfully")
321
+
322
+ except Exception as e:
323
+ self._log_verbose(f"Failed to initialize MongoDB: {e}", logging.ERROR)
324
+ self.use_mongodb = False
325
+
326
+ def _create_mongodb_indexes(self):
327
+ """Create MongoDB indexes for better performance."""
328
+ try:
329
+ # Text search indexes
330
+ self.mongo_short_term.create_index([("content", "text")])
331
+ self.mongo_long_term.create_index([("content", "text")])
332
+
333
+ # Compound indexes for filtering
334
+ self.mongo_short_term.create_index([("created_at", -1), ("metadata.quality", -1)])
335
+ self.mongo_long_term.create_index([("created_at", -1), ("metadata.quality", -1)])
336
+
337
+ # User-specific indexes
338
+ self.mongo_users.create_index([("user_id", 1), ("created_at", -1)])
339
+
340
+ # Entity indexes
341
+ self.mongo_entities.create_index([("entity_name", 1), ("entity_type", 1)])
342
+
343
+ # Vector search indexes for Atlas (if enabled)
344
+ if self.use_vector_search:
345
+ self._create_vector_search_indexes()
346
+
347
+ except Exception as e:
348
+ self._log_verbose(f"Warning: Could not create MongoDB indexes: {e}", logging.WARNING)
349
+
350
+ def _create_vector_search_indexes(self):
351
+ """Create vector search indexes for Atlas."""
352
+ try:
353
+ vector_index_def = {
354
+ "mappings": {
355
+ "dynamic": True,
356
+ "fields": {
357
+ "embedding": {
358
+ "type": "knnVector",
359
+ "dimensions": self.embedding_dimensions,
360
+ "similarity": "cosine"
361
+ }
362
+ }
363
+ }
364
+ }
365
+
366
+ # Create vector indexes for both short and long term collections
367
+ try:
368
+ self.mongo_short_term.create_search_index(vector_index_def, "vector_index")
369
+ self.mongo_long_term.create_search_index(vector_index_def, "vector_index")
370
+ self._log_verbose("Vector search indexes created successfully")
371
+ except Exception as e:
372
+ self._log_verbose(f"Could not create vector search indexes: {e}", logging.WARNING)
373
+
374
+ except Exception as e:
375
+ self._log_verbose(f"Error creating vector search indexes: {e}", logging.WARNING)
376
+
377
+ def _get_embedding(self, text: str) -> List[float]:
378
+ """Get embedding for text using available embedding services."""
379
+ try:
380
+ if LITELLM_AVAILABLE:
381
+ # Use LiteLLM for consistency with the rest of the codebase
382
+ import litellm
383
+
384
+ response = litellm.embedding(
385
+ model=self.embedding_model,
386
+ input=text
387
+ )
388
+ return response.data[0]["embedding"]
389
+ elif OPENAI_AVAILABLE:
390
+ # Fallback to OpenAI client
391
+ from openai import OpenAI
392
+ client = OpenAI()
393
+
394
+ response = client.embeddings.create(
395
+ input=text,
396
+ model=self.embedding_model
397
+ )
398
+ return response.data[0].embedding
399
+ else:
400
+ self._log_verbose("Neither litellm nor openai available for embeddings", logging.WARNING)
401
+ return None
402
+ except Exception as e:
403
+ self._log_verbose(f"Error getting embedding: {e}", logging.ERROR)
404
+ return None
405
+
406
+ def _get_embedding_dimensions(self, model_name: str) -> int:
407
+ """Get embedding dimensions based on model name."""
408
+ # Common embedding model dimensions
409
+ model_dimensions = {
410
+ "text-embedding-3-small": 1536,
411
+ "text-embedding-3-large": 3072,
412
+ "text-embedding-ada-002": 1536,
413
+ "text-embedding-002": 1536,
414
+ # Add more models as needed
415
+ }
416
+
417
+ # Check if model name contains known model identifiers
418
+ for model_key, dimensions in model_dimensions.items():
419
+ if model_key in model_name.lower():
420
+ return dimensions
421
+
422
+ # Default to 1536 for unknown models (OpenAI standard)
423
+ return 1536
424
+
264
425
  # -------------------------------------------------------------------------
265
426
  # Basic Quality Score Computation
266
427
  # -------------------------------------------------------------------------
@@ -323,20 +484,40 @@ class Memory:
323
484
  )
324
485
  logger.info(f"Processed metadata: {metadata}")
325
486
 
326
- # Existing store logic
487
+ # Generate unique ID and timestamp once
488
+ ident = str(time.time_ns())
489
+ created_at = time.time()
490
+
491
+ # Store in MongoDB if enabled
492
+ if self.use_mongodb and hasattr(self, "mongo_short_term"):
493
+ try:
494
+ doc = {
495
+ "_id": ident,
496
+ "content": text,
497
+ "metadata": metadata,
498
+ "created_at": datetime.utcnow(),
499
+ "memory_type": "short_term"
500
+ }
501
+ self.mongo_short_term.insert_one(doc)
502
+ logger.info(f"Successfully stored in MongoDB short-term memory with ID: {ident}")
503
+ except Exception as e:
504
+ logger.error(f"Failed to store in MongoDB short-term memory: {e}")
505
+ raise
506
+
507
+ # Existing SQLite store logic
327
508
  try:
328
509
  conn = sqlite3.connect(self.short_db)
329
- ident = str(time.time_ns())
330
510
  conn.execute(
331
511
  "INSERT INTO short_mem (id, content, meta, created_at) VALUES (?,?,?,?)",
332
- (ident, text, json.dumps(metadata), time.time())
512
+ (ident, text, json.dumps(metadata), created_at)
333
513
  )
334
514
  conn.commit()
335
515
  conn.close()
336
- logger.info(f"Successfully stored in short-term memory with ID: {ident}")
516
+ logger.info(f"Successfully stored in SQLite short-term memory with ID: {ident}")
337
517
  except Exception as e:
338
- logger.error(f"Failed to store in short-term memory: {e}")
339
- raise
518
+ logger.error(f"Failed to store in SQLite short-term memory: {e}")
519
+ if not self.use_mongodb: # Only raise if we're not using MongoDB as fallback
520
+ raise
340
521
 
341
522
  def search_short_term(
342
523
  self,
@@ -358,6 +539,67 @@ class Memory:
358
539
  filtered = [r for r in results if r.get("score", 1.0) >= relevance_cutoff]
359
540
  return filtered
360
541
 
542
+ elif self.use_mongodb and hasattr(self, "mongo_short_term"):
543
+ try:
544
+ results = []
545
+
546
+ # If vector search is enabled and we have embeddings
547
+ if self.use_vector_search and hasattr(self, "_get_embedding"):
548
+ embedding = self._get_embedding(query)
549
+ if embedding:
550
+ # Vector search pipeline
551
+ pipeline = [
552
+ {
553
+ "$vectorSearch": {
554
+ "index": "vector_index",
555
+ "path": "embedding",
556
+ "queryVector": embedding,
557
+ "numCandidates": limit * 10,
558
+ "limit": limit
559
+ }
560
+ },
561
+ {
562
+ "$addFields": {
563
+ "score": {"$meta": "vectorSearchScore"}
564
+ }
565
+ },
566
+ {
567
+ "$match": {
568
+ "metadata.quality": {"$gte": min_quality},
569
+ "score": {"$gte": relevance_cutoff}
570
+ }
571
+ }
572
+ ]
573
+
574
+ for doc in self.mongo_short_term.aggregate(pipeline):
575
+ results.append({
576
+ "id": str(doc["_id"]),
577
+ "text": doc["content"],
578
+ "metadata": doc.get("metadata", {}),
579
+ "score": doc.get("score", 1.0)
580
+ })
581
+
582
+ # Fallback to text search if no vector results
583
+ if not results:
584
+ search_filter = {
585
+ "$text": {"$search": query},
586
+ "metadata.quality": {"$gte": min_quality}
587
+ }
588
+
589
+ for doc in self.mongo_short_term.find(search_filter).limit(limit):
590
+ results.append({
591
+ "id": str(doc["_id"]),
592
+ "text": doc["content"],
593
+ "metadata": doc.get("metadata", {}),
594
+ "score": 1.0 # Default score for text search
595
+ })
596
+
597
+ return results
598
+
599
+ except Exception as e:
600
+ self._log_verbose(f"Error searching MongoDB short-term memory: {e}", logging.ERROR)
601
+ return []
602
+
361
603
  elif self.use_rag and hasattr(self, "chroma_col"):
362
604
  try:
363
605
  if LITELLM_AVAILABLE:
@@ -481,6 +723,29 @@ class Memory:
481
723
  ident = str(time.time_ns())
482
724
  created = time.time()
483
725
 
726
+ # Store in MongoDB if enabled (first priority)
727
+ if self.use_mongodb and hasattr(self, "mongo_long_term"):
728
+ try:
729
+ doc = {
730
+ "_id": ident,
731
+ "content": text,
732
+ "metadata": metadata,
733
+ "created_at": datetime.utcnow(),
734
+ "memory_type": "long_term"
735
+ }
736
+
737
+ # Add embedding if vector search is enabled
738
+ if self.use_vector_search:
739
+ embedding = self._get_embedding(text)
740
+ if embedding:
741
+ doc["embedding"] = embedding
742
+
743
+ self.mongo_long_term.insert_one(doc)
744
+ logger.info(f"Successfully stored in MongoDB long-term memory with ID: {ident}")
745
+ except Exception as e:
746
+ logger.error(f"Failed to store in MongoDB long-term memory: {e}")
747
+ # Continue to SQLite fallback
748
+
484
749
  # Store in SQLite
485
750
  try:
486
751
  conn = sqlite3.connect(self.long_db)
@@ -493,7 +758,9 @@ class Memory:
493
758
  logger.info(f"Successfully stored in SQLite with ID: {ident}")
494
759
  except Exception as e:
495
760
  logger.error(f"Error storing in SQLite: {e}")
496
- return
761
+ if not (self.use_mongodb and hasattr(self, "mongo_long_term")):
762
+ # Only raise if MongoDB is not available as fallback
763
+ return
497
764
 
498
765
  # Store in vector database if enabled
499
766
  if self.use_rag and hasattr(self, "chroma_col"):
@@ -579,6 +846,76 @@ class Memory:
579
846
  logger.info(f"Found {len(filtered)} results in Mem0")
580
847
  return filtered
581
848
 
849
+ elif self.use_mongodb and hasattr(self, "mongo_long_term"):
850
+ try:
851
+ results = []
852
+
853
+ # If vector search is enabled and we have embeddings
854
+ if self.use_vector_search:
855
+ embedding = self._get_embedding(query)
856
+ if embedding:
857
+ # Vector search pipeline
858
+ pipeline = [
859
+ {
860
+ "$vectorSearch": {
861
+ "index": "vector_index",
862
+ "path": "embedding",
863
+ "queryVector": embedding,
864
+ "numCandidates": limit * 10,
865
+ "limit": limit
866
+ }
867
+ },
868
+ {
869
+ "$addFields": {
870
+ "score": {"$meta": "vectorSearchScore"}
871
+ }
872
+ },
873
+ {
874
+ "$match": {
875
+ "metadata.quality": {"$gte": min_quality},
876
+ "score": {"$gte": relevance_cutoff}
877
+ }
878
+ }
879
+ ]
880
+
881
+ for doc in self.mongo_long_term.aggregate(pipeline):
882
+ text = doc["content"]
883
+ # Add memory record citation
884
+ if "(Memory record:" not in text:
885
+ text = f"{text} (Memory record: {str(doc['_id'])})"
886
+ results.append({
887
+ "id": str(doc["_id"]),
888
+ "text": text,
889
+ "metadata": doc.get("metadata", {}),
890
+ "score": doc.get("score", 1.0)
891
+ })
892
+
893
+ # Fallback to text search if no vector results
894
+ if not results:
895
+ search_filter = {
896
+ "$text": {"$search": query},
897
+ "metadata.quality": {"$gte": min_quality}
898
+ }
899
+
900
+ for doc in self.mongo_long_term.find(search_filter).limit(limit):
901
+ text = doc["content"]
902
+ # Add memory record citation
903
+ if "(Memory record:" not in text:
904
+ text = f"{text} (Memory record: {str(doc['_id'])})"
905
+ results.append({
906
+ "id": str(doc["_id"]),
907
+ "text": text,
908
+ "metadata": doc.get("metadata", {}),
909
+ "score": 1.0 # Default score for text search
910
+ })
911
+
912
+ logger.info(f"Found {len(results)} results in MongoDB")
913
+ return results
914
+
915
+ except Exception as e:
916
+ self._log_verbose(f"Error searching MongoDB long-term memory: {e}", logging.ERROR)
917
+ # Fall through to SQLite search
918
+
582
919
  elif self.use_rag and hasattr(self, "chroma_col"):
583
920
  try:
584
921
  if LITELLM_AVAILABLE:
@@ -617,7 +954,7 @@ class Memory:
617
954
  metadata = resp["metadatas"][0][i] if "metadatas" in resp else {}
618
955
  text = resp["documents"][0][i]
619
956
  # Add memory record citation
620
- text = f"{text} (Memory record: {text})"
957
+ text = f"{text} (Memory record: {resp['ids'][0][i]})"
621
958
  found.append({
622
959
  "id": resp["ids"][0][i],
623
960
  "text": text,
@@ -673,7 +1010,7 @@ class Memory:
673
1010
  return results[:limit]
674
1011
 
675
1012
  def reset_long_term(self):
676
- """Clear local LTM DB, plus Chroma or mem0 if in use."""
1013
+ """Clear local LTM DB, plus Chroma, MongoDB, or mem0 if in use."""
677
1014
  conn = sqlite3.connect(self.long_db)
678
1015
  conn.execute("DELETE FROM long_mem")
679
1016
  conn.commit()
@@ -682,6 +1019,12 @@ class Memory:
682
1019
  if self.use_mem0 and hasattr(self, "mem0_client"):
683
1020
  # Mem0 has no universal reset API. Could implement partial or no-op.
684
1021
  pass
1022
+ if self.use_mongodb and hasattr(self, "mongo_long_term"):
1023
+ try:
1024
+ self.mongo_long_term.delete_many({})
1025
+ self._log_verbose("MongoDB long-term memory cleared")
1026
+ except Exception as e:
1027
+ self._log_verbose(f"Error clearing MongoDB long-term memory: {e}", logging.ERROR)
685
1028
  if self.use_rag and hasattr(self, "chroma_client"):
686
1029
  self.chroma_client.reset() # entire DB
687
1030
  self._init_chroma() # re-init fresh
@@ -730,6 +1073,21 @@ class Memory:
730
1073
 
731
1074
  if self.use_mem0 and hasattr(self, "mem0_client"):
732
1075
  self.mem0_client.add(text, user_id=user_id, metadata=meta)
1076
+ elif self.use_mongodb and hasattr(self, "mongo_users"):
1077
+ try:
1078
+ from datetime import datetime
1079
+ ident = str(time.time_ns())
1080
+ doc = {
1081
+ "_id": ident,
1082
+ "user_id": user_id,
1083
+ "content": text,
1084
+ "metadata": meta,
1085
+ "created_at": datetime.utcnow()
1086
+ }
1087
+ self.mongo_users.insert_one(doc)
1088
+ self._log_verbose(f"Successfully stored user memory for {user_id}")
1089
+ except Exception as e:
1090
+ self._log_verbose(f"Error storing user memory: {e}", logging.ERROR)
733
1091
  else:
734
1092
  self.store_long_term(text, metadata=meta)
735
1093
 
@@ -742,6 +1100,26 @@ class Memory:
742
1100
  search_params = {"query": query, "limit": limit, "user_id": user_id, "rerank": rerank}
743
1101
  search_params.update(kwargs)
744
1102
  return self.mem0_client.search(**search_params)
1103
+ elif self.use_mongodb and hasattr(self, "mongo_users"):
1104
+ try:
1105
+ results = []
1106
+ search_filter = {
1107
+ "user_id": user_id,
1108
+ "$text": {"$search": query}
1109
+ }
1110
+
1111
+ for doc in self.mongo_users.find(search_filter).limit(limit):
1112
+ results.append({
1113
+ "id": str(doc["_id"]),
1114
+ "text": doc["content"],
1115
+ "metadata": doc.get("metadata", {}),
1116
+ "score": 1.0
1117
+ })
1118
+
1119
+ return results
1120
+ except Exception as e:
1121
+ self._log_verbose(f"Error searching MongoDB user memory: {e}", logging.ERROR)
1122
+ return []
745
1123
  else:
746
1124
  hits = self.search_long_term(query, limit=20)
747
1125
  filtered = []
@@ -790,7 +1168,7 @@ class Memory:
790
1168
 
791
1169
  return self.mem0_client.search(**search_params)
792
1170
 
793
- # For local memory, use specific search methods
1171
+ # For MongoDB or local memory, use specific search methods
794
1172
  if user_id:
795
1173
  # Use user-specific search
796
1174
  return self.search_user_memory(user_id, query, limit=limit, rerank=rerank, **kwargs)
@@ -24,6 +24,7 @@ __all__ = [
24
24
  'get_telemetry',
25
25
  'enable_telemetry',
26
26
  'disable_telemetry',
27
+ 'force_shutdown_telemetry',
27
28
  'MinimalTelemetry',
28
29
  'TelemetryCollector', # For backward compatibility
29
30
  ]
@@ -47,6 +48,12 @@ def disable_telemetry():
47
48
  _disable_telemetry()
48
49
 
49
50
 
51
+ def force_shutdown_telemetry():
52
+ """Force shutdown of telemetry system with comprehensive cleanup."""
53
+ from .telemetry import force_shutdown_telemetry as _force_shutdown_telemetry
54
+ _force_shutdown_telemetry()
55
+
56
+
50
57
  # Auto-instrumentation and cleanup setup
51
58
  _initialized = False
52
59
  _atexit_registered = False
@@ -65,8 +72,8 @@ def _ensure_atexit():
65
72
  ])
66
73
 
67
74
  if not telemetry_disabled:
68
- # Register atexit handler to flush telemetry on exit
69
- atexit.register(lambda: get_telemetry().flush())
75
+ # Register atexit handler to properly shutdown telemetry on exit
76
+ atexit.register(lambda: get_telemetry().shutdown())
70
77
  _atexit_registered = True
71
78
 
72
79
  def _initialize_telemetry():