mem-llm 1.0.11__py3-none-any.whl → 1.2.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.

Potentially problematic release.


This version of mem-llm might be problematic. Click here for more details.

mem_llm/logger.py ADDED
@@ -0,0 +1,129 @@
1
+ """
2
+ Enhanced Logging System for Mem-LLM
3
+ ====================================
4
+ Provides structured logging with different levels and output formats.
5
+ """
6
+
7
+ import logging
8
+ import sys
9
+ from datetime import datetime
10
+ from pathlib import Path
11
+ from typing import Optional
12
+
13
+
14
+ class MemLLMLogger:
15
+ """Structured logger for Mem-LLM with file and console output"""
16
+
17
+ def __init__(self,
18
+ name: str = "mem_llm",
19
+ log_file: Optional[str] = None,
20
+ log_level: str = "INFO",
21
+ console_output: bool = True):
22
+ """
23
+ Initialize logger
24
+
25
+ Args:
26
+ name: Logger name
27
+ log_file: Path to log file (optional)
28
+ log_level: Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
29
+ console_output: Enable console output
30
+ """
31
+ self.logger = logging.getLogger(name)
32
+ self.logger.setLevel(getattr(logging, log_level.upper()))
33
+
34
+ # Clear existing handlers
35
+ self.logger.handlers = []
36
+
37
+ # Formatter
38
+ formatter = logging.Formatter(
39
+ '%(asctime)s | %(name)s | %(levelname)s | %(message)s',
40
+ datefmt='%Y-%m-%d %H:%M:%S'
41
+ )
42
+
43
+ # Console handler
44
+ if console_output:
45
+ console_handler = logging.StreamHandler(sys.stdout)
46
+ console_handler.setFormatter(formatter)
47
+ self.logger.addHandler(console_handler)
48
+
49
+ # File handler
50
+ if log_file:
51
+ log_path = Path(log_file)
52
+ log_path.parent.mkdir(parents=True, exist_ok=True)
53
+ file_handler = logging.FileHandler(log_file, encoding='utf-8')
54
+ file_handler.setFormatter(formatter)
55
+ self.logger.addHandler(file_handler)
56
+
57
+ def debug(self, message: str, **kwargs):
58
+ """Debug level log"""
59
+ extra_info = " | ".join(f"{k}={v}" for k, v in kwargs.items())
60
+ full_message = f"{message} | {extra_info}" if extra_info else message
61
+ self.logger.debug(full_message)
62
+
63
+ def info(self, message: str, **kwargs):
64
+ """Info level log"""
65
+ extra_info = " | ".join(f"{k}={v}" for k, v in kwargs.items())
66
+ full_message = f"{message} | {extra_info}" if extra_info else message
67
+ self.logger.info(full_message)
68
+
69
+ def warning(self, message: str, **kwargs):
70
+ """Warning level log"""
71
+ extra_info = " | ".join(f"{k}={v}" for k, v in kwargs.items())
72
+ full_message = f"{message} | {extra_info}" if extra_info else message
73
+ self.logger.warning(full_message)
74
+
75
+ def error(self, message: str, **kwargs):
76
+ """Error level log"""
77
+ extra_info = " | ".join(f"{k}={v}" for k, v in kwargs.items())
78
+ full_message = f"{message} | {extra_info}" if extra_info else message
79
+ self.logger.error(full_message)
80
+
81
+ def critical(self, message: str, **kwargs):
82
+ """Critical level log"""
83
+ extra_info = " | ".join(f"{k}={v}" for k, v in kwargs.items())
84
+ full_message = f"{message} | {extra_info}" if extra_info else message
85
+ self.logger.critical(full_message)
86
+
87
+ def log_llm_call(self, model: str, prompt_length: int, response_length: int, duration: float):
88
+ """Log LLM API call with metrics"""
89
+ self.info(
90
+ "LLM API Call",
91
+ model=model,
92
+ prompt_tokens=prompt_length,
93
+ response_tokens=response_length,
94
+ duration_ms=f"{duration*1000:.2f}"
95
+ )
96
+
97
+ def log_memory_operation(self, operation: str, user_id: str, success: bool, details: str = ""):
98
+ """Log memory operations"""
99
+ level = self.info if success else self.error
100
+ level(
101
+ f"Memory {operation}",
102
+ user_id=user_id,
103
+ success=success,
104
+ details=details
105
+ )
106
+
107
+ def log_error_with_context(self, error: Exception, context: dict):
108
+ """Log error with full context"""
109
+ self.error(
110
+ f"Exception: {type(error).__name__}: {str(error)}",
111
+ **context
112
+ )
113
+
114
+
115
+ def get_logger(name: str = "mem_llm",
116
+ log_file: Optional[str] = "logs/mem_llm.log",
117
+ log_level: str = "INFO") -> MemLLMLogger:
118
+ """
119
+ Get or create logger instance
120
+
121
+ Args:
122
+ name: Logger name
123
+ log_file: Log file path
124
+ log_level: Logging level
125
+
126
+ Returns:
127
+ MemLLMLogger instance
128
+ """
129
+ return MemLLMLogger(name=name, log_file=log_file, log_level=log_level)
mem_llm/mem_agent.py CHANGED
@@ -64,19 +64,40 @@ class MemAgent:
64
64
  config_file: Optional[str] = None,
65
65
  use_sql: bool = True,
66
66
  memory_dir: Optional[str] = None,
67
+ db_path: Optional[str] = None,
67
68
  load_knowledge_base: bool = True,
68
69
  ollama_url: str = "http://localhost:11434",
69
- check_connection: bool = False):
70
+ check_connection: bool = False,
71
+ enable_security: bool = False):
70
72
  """
71
73
  Args:
72
74
  model: LLM model to use
73
75
  config_file: Configuration file (optional)
74
76
  use_sql: Use SQL database (True) or JSON (False)
75
- memory_dir: Memory directory
77
+ memory_dir: Memory directory (for JSON mode or if db_path not specified)
78
+ db_path: SQLite database path (for SQL mode, e.g., ":memory:" or "path/to/db.db")
76
79
  load_knowledge_base: Automatically load knowledge base
77
80
  ollama_url: Ollama API URL
78
81
  check_connection: Verify Ollama connection on startup (default: False)
82
+ enable_security: Enable prompt injection protection (v1.1.0+, default: False for backward compatibility)
79
83
  """
84
+
85
+ # Setup logging first
86
+ self._setup_logging()
87
+
88
+ # Security features (v1.1.0+)
89
+ self.enable_security = enable_security
90
+ self.security_detector = None
91
+ self.security_sanitizer = None
92
+
93
+ if enable_security:
94
+ try:
95
+ from .prompt_security import PromptInjectionDetector, InputSanitizer
96
+ self.security_detector = PromptInjectionDetector()
97
+ self.security_sanitizer = InputSanitizer()
98
+ self.logger.info("🔒 Security features enabled (prompt injection protection)")
99
+ except ImportError:
100
+ self.logger.warning("⚠️ Security features requested but not available")
80
101
 
81
102
  # Load configuration
82
103
  self.config = None
@@ -97,19 +118,33 @@ class MemAgent:
97
118
  # No config file
98
119
  self.usage_mode = "personal"
99
120
 
100
- # Setup logging
101
- self._setup_logging()
102
-
103
121
  # Initialize flags first
104
122
  self.has_knowledge_base: bool = False # Track KB status
105
123
  self.has_tools: bool = False # Track tools status
106
124
 
107
- # Memory system selection
125
+ # Memory system
108
126
  if use_sql and ADVANCED_AVAILABLE:
109
127
  # SQL memory (advanced)
110
- db_path = memory_dir or self.config.get("memory.db_path", "memories.db") if self.config else "memories.db"
111
- self.memory = SQLMemoryManager(db_path)
112
- self.logger.info(f"SQL memory system active: {db_path}")
128
+ # Determine database path
129
+ if db_path:
130
+ # Use provided db_path (can be ":memory:" for in-memory DB)
131
+ final_db_path = db_path
132
+ elif memory_dir:
133
+ final_db_path = memory_dir
134
+ elif self.config:
135
+ final_db_path = self.config.get("memory.db_path", "memories/memories.db")
136
+ else:
137
+ final_db_path = "memories/memories.db"
138
+
139
+ # Ensure memories directory exists (skip for :memory:)
140
+ import os
141
+ if final_db_path != ":memory:":
142
+ db_dir = os.path.dirname(final_db_path)
143
+ if db_dir and not os.path.exists(db_dir):
144
+ os.makedirs(db_dir, exist_ok=True)
145
+
146
+ self.memory = SQLMemoryManager(final_db_path)
147
+ self.logger.info(f"SQL memory system active: {final_db_path}")
113
148
  else:
114
149
  # JSON memory (simple)
115
150
  json_dir = memory_dir or self.config.get("memory.json_dir", "memories") if self.config else "memories"
@@ -187,10 +222,13 @@ class MemAgent:
187
222
  if ADVANCED_AVAILABLE and hasattr(self, 'config') and self.config:
188
223
  log_config = self.config.get("logging", {})
189
224
 
225
+ # Default to WARNING level to keep console clean (users can override in config)
226
+ default_level = "WARNING"
227
+
190
228
  if log_config.get("enabled", True):
191
229
  # Only console logging (no file) - keep workspace clean
192
230
  logging.basicConfig(
193
- level=getattr(logging, log_config.get("level", "INFO")),
231
+ level=getattr(logging, log_config.get("level", default_level)),
194
232
  format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
195
233
  handlers=[
196
234
  logging.StreamHandler() # Console only
@@ -198,6 +236,9 @@ class MemAgent:
198
236
  )
199
237
 
200
238
  self.logger = logging.getLogger("MemAgent")
239
+
240
+ # Set default level for mem_llm loggers
241
+ logging.getLogger("mem_llm").setLevel(getattr(logging, log_config.get("level", default_level)))
201
242
 
202
243
  def _setup_advanced_features(self, load_knowledge_base: bool) -> None:
203
244
  """Setup advanced features"""
@@ -364,6 +405,33 @@ class MemAgent:
364
405
  return "Error: User ID not specified."
365
406
 
366
407
  user_id = self.current_user
408
+
409
+ # Security check (v1.1.0+) - opt-in
410
+ security_info = {}
411
+ if self.enable_security and self.security_detector and self.security_sanitizer:
412
+ # Detect injection attempts
413
+ risk_level = self.security_detector.get_risk_level(message)
414
+ is_suspicious, patterns = self.security_detector.detect(message)
415
+
416
+ if risk_level in ["high", "critical"]:
417
+ self.logger.warning(f"🚨 Blocked {risk_level} risk input from {user_id}: {len(patterns)} patterns detected")
418
+ return f"⚠️ Your message was blocked due to security concerns. Please rephrase your request."
419
+
420
+ if is_suspicious:
421
+ self.logger.info(f"⚠️ Suspicious input from {user_id} (risk: {risk_level}): {len(patterns)} patterns")
422
+
423
+ # Sanitize input
424
+ original_message = message
425
+ message = self.security_sanitizer.sanitize(message, aggressive=(risk_level == "medium"))
426
+
427
+ if message != original_message:
428
+ self.logger.debug(f"Input sanitized for {user_id}")
429
+
430
+ security_info = {
431
+ "risk_level": risk_level,
432
+ "sanitized": message != original_message,
433
+ "patterns_detected": len(patterns)
434
+ }
367
435
 
368
436
  # Check tool commands first
369
437
  tool_result = self.tool_executor.execute_user_command(message, user_id)
mem_llm/memory_db.py CHANGED
@@ -5,28 +5,47 @@ Stores memory data using SQLite - Production-ready
5
5
 
6
6
  import sqlite3
7
7
  import json
8
+ import threading
8
9
  from datetime import datetime
9
10
  from typing import Dict, List, Optional, Tuple
10
11
  from pathlib import Path
11
12
 
12
13
 
13
14
  class SQLMemoryManager:
14
- """SQLite-based memory management system"""
15
+ """SQLite-based memory management system with thread-safety"""
15
16
 
16
- def __init__(self, db_path: str = "memories.db"):
17
+ def __init__(self, db_path: str = "memories/memories.db"):
17
18
  """
18
19
  Args:
19
20
  db_path: SQLite database file path
20
21
  """
21
22
  self.db_path = Path(db_path)
23
+
24
+ # Ensure directory exists
25
+ db_dir = self.db_path.parent
26
+ if not db_dir.exists():
27
+ db_dir.mkdir(parents=True, exist_ok=True)
28
+
22
29
  self.conn = None
30
+ self._lock = threading.RLock() # Reentrant lock for thread safety
23
31
  self._init_database()
24
32
 
25
33
  def _init_database(self) -> None:
26
34
  """Create database and tables"""
27
- self.conn = sqlite3.connect(str(self.db_path), check_same_thread=False)
35
+ self.conn = sqlite3.connect(
36
+ str(self.db_path),
37
+ check_same_thread=False,
38
+ timeout=30.0, # 30 second timeout for busy database
39
+ isolation_level=None # Autocommit mode
40
+ )
28
41
  self.conn.row_factory = sqlite3.Row
29
42
 
43
+ # Enable WAL mode for better concurrency
44
+ self.conn.execute("PRAGMA journal_mode=WAL")
45
+ self.conn.execute("PRAGMA synchronous=NORMAL")
46
+ self.conn.execute("PRAGMA cache_size=-64000") # 64MB cache
47
+ self.conn.execute("PRAGMA busy_timeout=30000") # 30 second busy timeout
48
+
30
49
  cursor = self.conn.cursor()
31
50
 
32
51
  # User profiles table
@@ -106,28 +125,28 @@ class SQLMemoryManager:
106
125
  def add_user(self, user_id: str, name: Optional[str] = None,
107
126
  metadata: Optional[Dict] = None) -> None:
108
127
  """
109
- Add new user or update existing
128
+ Add new user or update existing (thread-safe)
110
129
 
111
130
  Args:
112
131
  user_id: User ID
113
132
  name: User name
114
133
  metadata: Additional information
115
134
  """
116
- cursor = self.conn.cursor()
117
- cursor.execute("""
118
- INSERT INTO users (user_id, name, metadata)
119
- VALUES (?, ?, ?)
120
- ON CONFLICT(user_id) DO UPDATE SET
121
- name = COALESCE(excluded.name, users.name),
122
- metadata = COALESCE(excluded.metadata, users.metadata)
123
- """, (user_id, name, json.dumps(metadata or {})))
124
- self.conn.commit()
135
+ with self._lock:
136
+ cursor = self.conn.cursor()
137
+ cursor.execute("""
138
+ INSERT INTO users (user_id, name, metadata)
139
+ VALUES (?, ?, ?)
140
+ ON CONFLICT(user_id) DO UPDATE SET
141
+ name = COALESCE(excluded.name, users.name),
142
+ metadata = COALESCE(excluded.metadata, users.metadata)
143
+ """, (user_id, name, json.dumps(metadata or {})))
125
144
 
126
145
  def add_interaction(self, user_id: str, user_message: str,
127
146
  bot_response: str, metadata: Optional[Dict] = None,
128
147
  resolved: bool = False) -> int:
129
148
  """
130
- Record new interaction
149
+ Record new interaction (thread-safe)
131
150
 
132
151
  Args:
133
152
  user_id: User ID
@@ -139,30 +158,33 @@ class SQLMemoryManager:
139
158
  Returns:
140
159
  Added record ID
141
160
  """
142
- cursor = self.conn.cursor()
143
-
144
- # Create user if not exists
145
- self.add_user(user_id)
146
-
147
- # Record interaction
148
- cursor.execute("""
149
- INSERT INTO conversations
150
- (user_id, user_message, bot_response, metadata, resolved)
151
- VALUES (?, ?, ?, ?, ?)
152
- """, (user_id, user_message, bot_response,
153
- json.dumps(metadata or {}), resolved))
154
-
155
- interaction_id = cursor.lastrowid
156
-
157
- # Update user's last interaction time
158
- cursor.execute("""
159
- UPDATE users
160
- SET last_interaction = CURRENT_TIMESTAMP
161
- WHERE user_id = ?
162
- """, (user_id,))
161
+ if not user_message or not bot_response:
162
+ raise ValueError("user_message and bot_response cannot be None or empty")
163
163
 
164
- self.conn.commit()
165
- return interaction_id
164
+ with self._lock:
165
+ cursor = self.conn.cursor()
166
+
167
+ # Create user if not exists
168
+ self.add_user(user_id)
169
+
170
+ # Record interaction
171
+ cursor.execute("""
172
+ INSERT INTO conversations
173
+ (user_id, user_message, bot_response, metadata, resolved)
174
+ VALUES (?, ?, ?, ?, ?)
175
+ """, (user_id, user_message, bot_response,
176
+ json.dumps(metadata or {}), resolved))
177
+
178
+ interaction_id = cursor.lastrowid
179
+
180
+ # Update user's last interaction time
181
+ cursor.execute("""
182
+ UPDATE users
183
+ SET last_interaction = CURRENT_TIMESTAMP
184
+ WHERE user_id = ?
185
+ """, (user_id,))
186
+
187
+ return interaction_id
166
188
 
167
189
  # Alias for compatibility
168
190
  def add_conversation(self, user_id: str, user_message: str, bot_response: str, metadata: Optional[Dict] = None) -> int:
@@ -171,7 +193,7 @@ class SQLMemoryManager:
171
193
 
172
194
  def get_recent_conversations(self, user_id: str, limit: int = 10) -> List[Dict]:
173
195
  """
174
- Kullanıcının son konuşmalarını getirir
196
+ Kullanıcının son konuşmalarını getirir (thread-safe)
175
197
 
176
198
  Args:
177
199
  user_id: Kullanıcı kimliği
@@ -180,21 +202,22 @@ class SQLMemoryManager:
180
202
  Returns:
181
203
  Konuşmalar listesi
182
204
  """
183
- cursor = self.conn.cursor()
184
- cursor.execute("""
185
- SELECT timestamp, user_message, bot_response, metadata, resolved
186
- FROM conversations
187
- WHERE user_id = ?
188
- ORDER BY timestamp DESC
189
- LIMIT ?
190
- """, (user_id, limit))
191
-
192
- rows = cursor.fetchall()
193
- return [dict(row) for row in rows]
205
+ with self._lock:
206
+ cursor = self.conn.cursor()
207
+ cursor.execute("""
208
+ SELECT timestamp, user_message, bot_response, metadata, resolved
209
+ FROM conversations
210
+ WHERE user_id = ?
211
+ ORDER BY timestamp DESC
212
+ LIMIT ?
213
+ """, (user_id, limit))
214
+
215
+ rows = cursor.fetchall()
216
+ return [dict(row) for row in rows]
194
217
 
195
218
  def search_conversations(self, user_id: str, keyword: str) -> List[Dict]:
196
219
  """
197
- Konuşmalarda anahtar kelime arar
220
+ Konuşmalarda anahtar kelime arar (thread-safe)
198
221
 
199
222
  Args:
200
223
  user_id: Kullanıcı kimliği