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/__init__.py +35 -2
- mem_llm/config_manager.py +1 -1
- mem_llm/conversation_summarizer.py +372 -0
- mem_llm/data_export_import.py +640 -0
- mem_llm/llm_client.py +27 -8
- mem_llm/logger.py +129 -0
- mem_llm/mem_agent.py +78 -10
- mem_llm/memory_db.py +73 -50
- mem_llm/prompt_security.py +304 -0
- mem_llm/retry_handler.py +193 -0
- mem_llm/thread_safe_db.py +301 -0
- {mem_llm-1.0.11.dist-info → mem_llm-1.2.0.dist-info}/METADATA +140 -94
- mem_llm-1.2.0.dist-info/RECORD +23 -0
- mem_llm-1.0.11.dist-info/RECORD +0 -17
- {mem_llm-1.0.11.dist-info → mem_llm-1.2.0.dist-info}/WHEEL +0 -0
- {mem_llm-1.0.11.dist-info → mem_llm-1.2.0.dist-info}/entry_points.txt +0 -0
- {mem_llm-1.0.11.dist-info → mem_llm-1.2.0.dist-info}/top_level.txt +0 -0
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
|
|
125
|
+
# Memory system
|
|
108
126
|
if use_sql and ADVANCED_AVAILABLE:
|
|
109
127
|
# SQL memory (advanced)
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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",
|
|
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(
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
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.
|
|
165
|
-
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|