mem-llm 1.0.11__py3-none-any.whl → 1.1.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/__init__.py CHANGED
@@ -24,7 +24,26 @@ try:
24
24
  except ImportError:
25
25
  __all_pro__ = []
26
26
 
27
- __version__ = "1.0.11"
27
+ # Security features (optional, v1.1.0+)
28
+ try:
29
+ from .prompt_security import (
30
+ PromptInjectionDetector,
31
+ InputSanitizer,
32
+ SecurePromptBuilder
33
+ )
34
+ __all_security__ = ["PromptInjectionDetector", "InputSanitizer", "SecurePromptBuilder"]
35
+ except ImportError:
36
+ __all_security__ = []
37
+
38
+ # Enhanced features (v1.1.0+)
39
+ try:
40
+ from .logger import get_logger, MemLLMLogger
41
+ from .retry_handler import exponential_backoff_retry, SafeExecutor
42
+ __all_enhanced__ = ["get_logger", "MemLLMLogger", "exponential_backoff_retry", "SafeExecutor"]
43
+ except ImportError:
44
+ __all_enhanced__ = []
45
+
46
+ __version__ = "1.1.0"
28
47
  __author__ = "C. Emre Karataş"
29
48
 
30
49
  # CLI
@@ -38,4 +57,4 @@ __all__ = [
38
57
  "MemAgent",
39
58
  "MemoryManager",
40
59
  "OllamaClient",
41
- ] + __all_tools__ + __all_pro__ + __all_cli__
60
+ ] + __all_tools__ + __all_pro__ + __all_cli__ + __all_security__ + __all_enhanced__
mem_llm/llm_client.py CHANGED
@@ -5,6 +5,7 @@ Works with Granite4:tiny-h model
5
5
 
6
6
  import requests
7
7
  import json
8
+ import time
8
9
  from typing import List, Dict, Optional
9
10
 
10
11
 
@@ -79,14 +80,32 @@ class OllamaClient:
79
80
  if system_prompt:
80
81
  payload["system"] = system_prompt
81
82
 
82
- try:
83
- response = requests.post(self.api_url, json=payload, timeout=60)
84
- if response.status_code == 200:
85
- return response.json().get('response', '').strip()
86
- else:
87
- return f"Error: {response.status_code} - {response.text}"
88
- except Exception as e:
89
- return f"Connection error: {str(e)}"
83
+ max_retries = 3
84
+ for attempt in range(max_retries):
85
+ try:
86
+ response = requests.post(self.api_url, json=payload, timeout=60)
87
+ if response.status_code == 200:
88
+ return response.json().get('response', '').strip()
89
+ else:
90
+ if attempt < max_retries - 1:
91
+ time.sleep(1.0 * (2 ** attempt)) # Exponential backoff
92
+ continue
93
+ return f"Error: {response.status_code} - {response.text}"
94
+ except requests.exceptions.Timeout:
95
+ if attempt < max_retries - 1:
96
+ time.sleep(2.0 * (2 ** attempt))
97
+ continue
98
+ return "Error: Request timeout. Please check if Ollama is running."
99
+ except requests.exceptions.ConnectionError:
100
+ if attempt < max_retries - 1:
101
+ time.sleep(1.0 * (2 ** attempt))
102
+ continue
103
+ return "Error: Cannot connect to Ollama. Make sure Ollama service is running."
104
+ except Exception as e:
105
+ if attempt < max_retries - 1:
106
+ time.sleep(1.0 * (2 ** attempt))
107
+ continue
108
+ return f"Connection error: {str(e)}"
90
109
 
91
110
  def chat(self, messages: List[Dict[str, str]],
92
111
  temperature: float = 0.7, max_tokens: int = 2000) -> str:
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
@@ -66,7 +66,8 @@ class MemAgent:
66
66
  memory_dir: Optional[str] = None,
67
67
  load_knowledge_base: bool = True,
68
68
  ollama_url: str = "http://localhost:11434",
69
- check_connection: bool = False):
69
+ check_connection: bool = False,
70
+ enable_security: bool = False):
70
71
  """
71
72
  Args:
72
73
  model: LLM model to use
@@ -76,7 +77,25 @@ class MemAgent:
76
77
  load_knowledge_base: Automatically load knowledge base
77
78
  ollama_url: Ollama API URL
78
79
  check_connection: Verify Ollama connection on startup (default: False)
80
+ enable_security: Enable prompt injection protection (v1.1.0+, default: False for backward compatibility)
79
81
  """
82
+
83
+ # Setup logging first
84
+ self._setup_logging()
85
+
86
+ # Security features (v1.1.0+)
87
+ self.enable_security = enable_security
88
+ self.security_detector = None
89
+ self.security_sanitizer = None
90
+
91
+ if enable_security:
92
+ try:
93
+ from .prompt_security import PromptInjectionDetector, InputSanitizer
94
+ self.security_detector = PromptInjectionDetector()
95
+ self.security_sanitizer = InputSanitizer()
96
+ self.logger.info("🔒 Security features enabled (prompt injection protection)")
97
+ except ImportError:
98
+ self.logger.warning("⚠️ Security features requested but not available")
80
99
 
81
100
  # Load configuration
82
101
  self.config = None
@@ -97,9 +116,6 @@ class MemAgent:
97
116
  # No config file
98
117
  self.usage_mode = "personal"
99
118
 
100
- # Setup logging
101
- self._setup_logging()
102
-
103
119
  # Initialize flags first
104
120
  self.has_knowledge_base: bool = False # Track KB status
105
121
  self.has_tools: bool = False # Track tools status
@@ -364,6 +380,33 @@ class MemAgent:
364
380
  return "Error: User ID not specified."
365
381
 
366
382
  user_id = self.current_user
383
+
384
+ # Security check (v1.1.0+) - opt-in
385
+ security_info = {}
386
+ if self.enable_security and self.security_detector and self.security_sanitizer:
387
+ # Detect injection attempts
388
+ risk_level = self.security_detector.get_risk_level(message)
389
+ is_suspicious, patterns = self.security_detector.detect(message)
390
+
391
+ if risk_level in ["high", "critical"]:
392
+ self.logger.warning(f"🚨 Blocked {risk_level} risk input from {user_id}: {len(patterns)} patterns detected")
393
+ return f"⚠️ Your message was blocked due to security concerns. Please rephrase your request."
394
+
395
+ if is_suspicious:
396
+ self.logger.info(f"⚠️ Suspicious input from {user_id} (risk: {risk_level}): {len(patterns)} patterns")
397
+
398
+ # Sanitize input
399
+ original_message = message
400
+ message = self.security_sanitizer.sanitize(message, aggressive=(risk_level == "medium"))
401
+
402
+ if message != original_message:
403
+ self.logger.debug(f"Input sanitized for {user_id}")
404
+
405
+ security_info = {
406
+ "risk_level": risk_level,
407
+ "sanitized": message != original_message,
408
+ "patterns_detected": len(patterns)
409
+ }
367
410
 
368
411
  # Check tool commands first
369
412
  tool_result = self.tool_executor.execute_user_command(message, user_id)
mem_llm/memory_db.py CHANGED
@@ -5,13 +5,14 @@ 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
17
  def __init__(self, db_path: str = "memories.db"):
17
18
  """
@@ -20,13 +21,25 @@ class SQLMemoryManager:
20
21
  """
21
22
  self.db_path = Path(db_path)
22
23
  self.conn = None
24
+ self._lock = threading.RLock() # Reentrant lock for thread safety
23
25
  self._init_database()
24
26
 
25
27
  def _init_database(self) -> None:
26
28
  """Create database and tables"""
27
- self.conn = sqlite3.connect(str(self.db_path), check_same_thread=False)
29
+ self.conn = sqlite3.connect(
30
+ str(self.db_path),
31
+ check_same_thread=False,
32
+ timeout=30.0, # 30 second timeout for busy database
33
+ isolation_level=None # Autocommit mode
34
+ )
28
35
  self.conn.row_factory = sqlite3.Row
29
36
 
37
+ # Enable WAL mode for better concurrency
38
+ self.conn.execute("PRAGMA journal_mode=WAL")
39
+ self.conn.execute("PRAGMA synchronous=NORMAL")
40
+ self.conn.execute("PRAGMA cache_size=-64000") # 64MB cache
41
+ self.conn.execute("PRAGMA busy_timeout=30000") # 30 second busy timeout
42
+
30
43
  cursor = self.conn.cursor()
31
44
 
32
45
  # User profiles table
@@ -106,28 +119,28 @@ class SQLMemoryManager:
106
119
  def add_user(self, user_id: str, name: Optional[str] = None,
107
120
  metadata: Optional[Dict] = None) -> None:
108
121
  """
109
- Add new user or update existing
122
+ Add new user or update existing (thread-safe)
110
123
 
111
124
  Args:
112
125
  user_id: User ID
113
126
  name: User name
114
127
  metadata: Additional information
115
128
  """
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()
129
+ with self._lock:
130
+ cursor = self.conn.cursor()
131
+ cursor.execute("""
132
+ INSERT INTO users (user_id, name, metadata)
133
+ VALUES (?, ?, ?)
134
+ ON CONFLICT(user_id) DO UPDATE SET
135
+ name = COALESCE(excluded.name, users.name),
136
+ metadata = COALESCE(excluded.metadata, users.metadata)
137
+ """, (user_id, name, json.dumps(metadata or {})))
125
138
 
126
139
  def add_interaction(self, user_id: str, user_message: str,
127
140
  bot_response: str, metadata: Optional[Dict] = None,
128
141
  resolved: bool = False) -> int:
129
142
  """
130
- Record new interaction
143
+ Record new interaction (thread-safe)
131
144
 
132
145
  Args:
133
146
  user_id: User ID
@@ -139,30 +152,33 @@ class SQLMemoryManager:
139
152
  Returns:
140
153
  Added record ID
141
154
  """
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))
155
+ if not user_message or not bot_response:
156
+ raise ValueError("user_message and bot_response cannot be None or empty")
154
157
 
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,))
163
-
164
- self.conn.commit()
165
- return interaction_id
158
+ with self._lock:
159
+ cursor = self.conn.cursor()
160
+
161
+ # Create user if not exists
162
+ self.add_user(user_id)
163
+
164
+ # Record interaction
165
+ cursor.execute("""
166
+ INSERT INTO conversations
167
+ (user_id, user_message, bot_response, metadata, resolved)
168
+ VALUES (?, ?, ?, ?, ?)
169
+ """, (user_id, user_message, bot_response,
170
+ json.dumps(metadata or {}), resolved))
171
+
172
+ interaction_id = cursor.lastrowid
173
+
174
+ # Update user's last interaction time
175
+ cursor.execute("""
176
+ UPDATE users
177
+ SET last_interaction = CURRENT_TIMESTAMP
178
+ WHERE user_id = ?
179
+ """, (user_id,))
180
+
181
+ return interaction_id
166
182
 
167
183
  # Alias for compatibility
168
184
  def add_conversation(self, user_id: str, user_message: str, bot_response: str, metadata: Optional[Dict] = None) -> int:
@@ -171,7 +187,7 @@ class SQLMemoryManager:
171
187
 
172
188
  def get_recent_conversations(self, user_id: str, limit: int = 10) -> List[Dict]:
173
189
  """
174
- Kullanıcının son konuşmalarını getirir
190
+ Kullanıcının son konuşmalarını getirir (thread-safe)
175
191
 
176
192
  Args:
177
193
  user_id: Kullanıcı kimliği
@@ -180,21 +196,22 @@ class SQLMemoryManager:
180
196
  Returns:
181
197
  Konuşmalar listesi
182
198
  """
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]
199
+ with self._lock:
200
+ cursor = self.conn.cursor()
201
+ cursor.execute("""
202
+ SELECT timestamp, user_message, bot_response, metadata, resolved
203
+ FROM conversations
204
+ WHERE user_id = ?
205
+ ORDER BY timestamp DESC
206
+ LIMIT ?
207
+ """, (user_id, limit))
208
+
209
+ rows = cursor.fetchall()
210
+ return [dict(row) for row in rows]
194
211
 
195
212
  def search_conversations(self, user_id: str, keyword: str) -> List[Dict]:
196
213
  """
197
- Konuşmalarda anahtar kelime arar
214
+ Konuşmalarda anahtar kelime arar (thread-safe)
198
215
 
199
216
  Args:
200
217
  user_id: Kullanıcı kimliği