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 +21 -2
- mem_llm/llm_client.py +27 -8
- mem_llm/logger.py +129 -0
- mem_llm/mem_agent.py +47 -4
- mem_llm/memory_db.py +66 -49
- mem_llm/prompt_security.py +304 -0
- mem_llm/retry_handler.py +193 -0
- mem_llm/thread_safe_db.py +295 -0
- {mem_llm-1.0.11.dist-info → mem_llm-1.1.0.dist-info}/METADATA +78 -5
- mem_llm-1.1.0.dist-info/RECORD +21 -0
- mem_llm-1.0.11.dist-info/RECORD +0 -17
- {mem_llm-1.0.11.dist-info → mem_llm-1.1.0.dist-info}/WHEEL +0 -0
- {mem_llm-1.0.11.dist-info → mem_llm-1.1.0.dist-info}/entry_points.txt +0 -0
- {mem_llm-1.0.11.dist-info → mem_llm-1.1.0.dist-info}/top_level.txt +0 -0
mem_llm/__init__.py
CHANGED
|
@@ -24,7 +24,26 @@ try:
|
|
|
24
24
|
except ImportError:
|
|
25
25
|
__all_pro__ = []
|
|
26
26
|
|
|
27
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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(
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|