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/__init__.py
CHANGED
|
@@ -24,7 +24,40 @@ 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
|
+
# Conversation Summarization (v1.2.0+)
|
|
47
|
+
try:
|
|
48
|
+
from .conversation_summarizer import ConversationSummarizer, AutoSummarizer
|
|
49
|
+
__all_summarizer__ = ["ConversationSummarizer", "AutoSummarizer"]
|
|
50
|
+
except ImportError:
|
|
51
|
+
__all_summarizer__ = []
|
|
52
|
+
|
|
53
|
+
# Data Export/Import (v1.2.0+)
|
|
54
|
+
try:
|
|
55
|
+
from .data_export_import import DataExporter, DataImporter
|
|
56
|
+
__all_export_import__ = ["DataExporter", "DataImporter"]
|
|
57
|
+
except ImportError:
|
|
58
|
+
__all_export_import__ = []
|
|
59
|
+
|
|
60
|
+
__version__ = "1.2.0"
|
|
28
61
|
__author__ = "C. Emre Karataş"
|
|
29
62
|
|
|
30
63
|
# CLI
|
|
@@ -38,4 +71,4 @@ __all__ = [
|
|
|
38
71
|
"MemAgent",
|
|
39
72
|
"MemoryManager",
|
|
40
73
|
"OllamaClient",
|
|
41
|
-
] + __all_tools__ + __all_pro__ + __all_cli__
|
|
74
|
+
] + __all_tools__ + __all_pro__ + __all_cli__ + __all_security__ + __all_enhanced__ + __all_summarizer__ + __all_export_import__
|
mem_llm/config_manager.py
CHANGED
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Conversation Summarizer
|
|
3
|
+
=======================
|
|
4
|
+
|
|
5
|
+
Automatically summarizes long conversation histories to optimize context window usage.
|
|
6
|
+
|
|
7
|
+
Features:
|
|
8
|
+
- Summarizes last N conversations
|
|
9
|
+
- Extracts key facts and context
|
|
10
|
+
- Saves tokens by condensing history
|
|
11
|
+
- Periodic auto-summary updates
|
|
12
|
+
- User profile extraction from summaries
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
```python
|
|
16
|
+
from mem_llm import ConversationSummarizer
|
|
17
|
+
|
|
18
|
+
summarizer = ConversationSummarizer(llm_client)
|
|
19
|
+
summary = summarizer.summarize_conversations(conversations, user_id="alice")
|
|
20
|
+
```
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from typing import List, Dict, Optional, Any
|
|
24
|
+
from datetime import datetime
|
|
25
|
+
import json
|
|
26
|
+
import logging
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ConversationSummarizer:
|
|
30
|
+
"""Summarizes conversation histories to optimize context"""
|
|
31
|
+
|
|
32
|
+
def __init__(self, llm_client, logger: Optional[logging.Logger] = None):
|
|
33
|
+
"""
|
|
34
|
+
Initialize summarizer
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
llm_client: OllamaClient instance for generating summaries
|
|
38
|
+
logger: Logger instance (optional)
|
|
39
|
+
"""
|
|
40
|
+
self.llm = llm_client
|
|
41
|
+
self.logger = logger or logging.getLogger(__name__)
|
|
42
|
+
|
|
43
|
+
def summarize_conversations(
|
|
44
|
+
self,
|
|
45
|
+
conversations: List[Dict],
|
|
46
|
+
user_id: str,
|
|
47
|
+
max_conversations: int = 20,
|
|
48
|
+
include_facts: bool = True
|
|
49
|
+
) -> Dict[str, Any]:
|
|
50
|
+
"""
|
|
51
|
+
Summarize a list of conversations
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
conversations: List of conversation dicts with user_message and bot_response
|
|
55
|
+
user_id: User identifier
|
|
56
|
+
max_conversations: Maximum number of conversations to summarize
|
|
57
|
+
include_facts: Extract key facts about the user
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
Summary dict with text, facts, and metadata
|
|
61
|
+
"""
|
|
62
|
+
if not conversations:
|
|
63
|
+
return {
|
|
64
|
+
"summary": "No conversation history available.",
|
|
65
|
+
"key_facts": [],
|
|
66
|
+
"conversation_count": 0,
|
|
67
|
+
"user_id": user_id,
|
|
68
|
+
"generated_at": datetime.now().isoformat()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
# Limit conversations
|
|
72
|
+
convs_to_summarize = conversations[-max_conversations:] if len(conversations) > max_conversations else conversations
|
|
73
|
+
|
|
74
|
+
# Build prompt
|
|
75
|
+
prompt = self._build_summary_prompt(convs_to_summarize, user_id, include_facts)
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
# Generate summary
|
|
79
|
+
self.logger.info(f"Generating summary for {user_id}: {len(convs_to_summarize)} conversations")
|
|
80
|
+
|
|
81
|
+
response = self.llm.chat(
|
|
82
|
+
messages=[{"role": "user", "content": prompt}],
|
|
83
|
+
temperature=0.3, # Lower temperature for consistent summaries
|
|
84
|
+
max_tokens=500
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# Parse response
|
|
88
|
+
summary_data = self._parse_summary_response(response, convs_to_summarize, user_id)
|
|
89
|
+
|
|
90
|
+
self.logger.info(f"✅ Summary generated: {len(summary_data['summary'])} chars")
|
|
91
|
+
return summary_data
|
|
92
|
+
|
|
93
|
+
except Exception as e:
|
|
94
|
+
self.logger.error(f"Summary generation failed: {e}")
|
|
95
|
+
return {
|
|
96
|
+
"summary": f"Error generating summary: {str(e)}",
|
|
97
|
+
"key_facts": [],
|
|
98
|
+
"conversation_count": len(convs_to_summarize),
|
|
99
|
+
"user_id": user_id,
|
|
100
|
+
"generated_at": datetime.now().isoformat(),
|
|
101
|
+
"error": str(e)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
def _build_summary_prompt(
|
|
105
|
+
self,
|
|
106
|
+
conversations: List[Dict],
|
|
107
|
+
user_id: str,
|
|
108
|
+
include_facts: bool
|
|
109
|
+
) -> str:
|
|
110
|
+
"""Build the summarization prompt"""
|
|
111
|
+
|
|
112
|
+
# Format conversations
|
|
113
|
+
conv_text = ""
|
|
114
|
+
for i, conv in enumerate(conversations, 1):
|
|
115
|
+
user_msg = conv.get('user_message', '')
|
|
116
|
+
bot_msg = conv.get('bot_response', '')
|
|
117
|
+
conv_text += f"\n{i}. User: {user_msg}\n Bot: {bot_msg}\n"
|
|
118
|
+
|
|
119
|
+
prompt = f"""You are a conversation summarizer. Summarize the following conversations for user '{user_id}'.
|
|
120
|
+
|
|
121
|
+
CONVERSATIONS:
|
|
122
|
+
{conv_text}
|
|
123
|
+
|
|
124
|
+
TASK:
|
|
125
|
+
Create a concise summary (max 200 words) that captures:
|
|
126
|
+
1. Main topics discussed
|
|
127
|
+
2. User's questions and concerns
|
|
128
|
+
3. Important context for future conversations"""
|
|
129
|
+
|
|
130
|
+
if include_facts:
|
|
131
|
+
prompt += """
|
|
132
|
+
4. Key facts about the user (preferences, background, needs)
|
|
133
|
+
|
|
134
|
+
FORMAT YOUR RESPONSE AS:
|
|
135
|
+
SUMMARY: [Your summary here]
|
|
136
|
+
KEY_FACTS: [Comma-separated list of facts about the user]
|
|
137
|
+
"""
|
|
138
|
+
else:
|
|
139
|
+
prompt += "\n\nProvide only the summary."
|
|
140
|
+
|
|
141
|
+
return prompt
|
|
142
|
+
|
|
143
|
+
def _parse_summary_response(
|
|
144
|
+
self,
|
|
145
|
+
response: str,
|
|
146
|
+
conversations: List[Dict],
|
|
147
|
+
user_id: str
|
|
148
|
+
) -> Dict[str, Any]:
|
|
149
|
+
"""Parse LLM response into structured summary"""
|
|
150
|
+
|
|
151
|
+
summary_text = ""
|
|
152
|
+
key_facts = []
|
|
153
|
+
|
|
154
|
+
# Try to parse structured format
|
|
155
|
+
if "SUMMARY:" in response and "KEY_FACTS:" in response:
|
|
156
|
+
parts = response.split("KEY_FACTS:")
|
|
157
|
+
summary_part = parts[0].replace("SUMMARY:", "").strip()
|
|
158
|
+
facts_part = parts[1].strip() if len(parts) > 1 else ""
|
|
159
|
+
|
|
160
|
+
summary_text = summary_part
|
|
161
|
+
|
|
162
|
+
# Parse facts
|
|
163
|
+
if facts_part:
|
|
164
|
+
# Split by common delimiters
|
|
165
|
+
facts_raw = facts_part.replace("\n", ",").split(",")
|
|
166
|
+
key_facts = [f.strip() for f in facts_raw if f.strip() and len(f.strip()) > 3]
|
|
167
|
+
else:
|
|
168
|
+
# Fallback: use entire response as summary
|
|
169
|
+
summary_text = response.strip()
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
"summary": summary_text,
|
|
173
|
+
"key_facts": key_facts,
|
|
174
|
+
"conversation_count": len(conversations),
|
|
175
|
+
"user_id": user_id,
|
|
176
|
+
"generated_at": datetime.now().isoformat()
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
def should_update_summary(
|
|
180
|
+
self,
|
|
181
|
+
last_summary_time: Optional[str],
|
|
182
|
+
new_conversations_count: int,
|
|
183
|
+
update_threshold: int = 10
|
|
184
|
+
) -> bool:
|
|
185
|
+
"""
|
|
186
|
+
Determine if summary should be updated
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
last_summary_time: ISO timestamp of last summary
|
|
190
|
+
new_conversations_count: Number of new conversations since last summary
|
|
191
|
+
update_threshold: Minimum conversations before update
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
True if summary should be updated
|
|
195
|
+
"""
|
|
196
|
+
# Always update if no previous summary
|
|
197
|
+
if not last_summary_time:
|
|
198
|
+
return new_conversations_count >= 5 # Need at least 5 convs for meaningful summary
|
|
199
|
+
|
|
200
|
+
# Update if threshold reached
|
|
201
|
+
return new_conversations_count >= update_threshold
|
|
202
|
+
|
|
203
|
+
def extract_user_insights(self, summary: str) -> Dict[str, Any]:
|
|
204
|
+
"""
|
|
205
|
+
Extract structured insights from summary
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
summary: Summary text
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
Insights dict with topics, preferences, etc.
|
|
212
|
+
"""
|
|
213
|
+
insights = {
|
|
214
|
+
"topics": [],
|
|
215
|
+
"preferences": [],
|
|
216
|
+
"needs": [],
|
|
217
|
+
"background": []
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
# Simple keyword-based extraction
|
|
221
|
+
# (Could be enhanced with NER or another LLM call)
|
|
222
|
+
|
|
223
|
+
summary_lower = summary.lower()
|
|
224
|
+
|
|
225
|
+
# Common topic keywords
|
|
226
|
+
topic_keywords = {
|
|
227
|
+
"programming": ["python", "javascript", "code", "programming", "development"],
|
|
228
|
+
"business": ["business", "startup", "company", "market"],
|
|
229
|
+
"technical": ["technical", "bug", "error", "issue", "problem"],
|
|
230
|
+
"personal": ["personal", "preference", "like", "prefer"]
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
for topic, keywords in topic_keywords.items():
|
|
234
|
+
if any(kw in summary_lower for kw in keywords):
|
|
235
|
+
insights["topics"].append(topic)
|
|
236
|
+
|
|
237
|
+
return insights
|
|
238
|
+
|
|
239
|
+
def get_summary_stats(self, original_text: str, summary_text: str) -> Dict[str, Any]:
|
|
240
|
+
"""
|
|
241
|
+
Calculate compression statistics
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
original_text: Original conversation text
|
|
245
|
+
summary_text: Summarized text
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
Stats dict with compression ratio, token savings, etc.
|
|
249
|
+
"""
|
|
250
|
+
orig_length = len(original_text)
|
|
251
|
+
summary_length = len(summary_text)
|
|
252
|
+
|
|
253
|
+
# Rough token estimation (1 token ≈ 4 chars)
|
|
254
|
+
orig_tokens = orig_length // 4
|
|
255
|
+
summary_tokens = summary_length // 4
|
|
256
|
+
|
|
257
|
+
compression_ratio = (1 - summary_length / orig_length) * 100 if orig_length > 0 else 0
|
|
258
|
+
|
|
259
|
+
return {
|
|
260
|
+
"original_length": orig_length,
|
|
261
|
+
"summary_length": summary_length,
|
|
262
|
+
"compression_ratio": round(compression_ratio, 2),
|
|
263
|
+
"original_tokens_est": orig_tokens,
|
|
264
|
+
"summary_tokens_est": summary_tokens,
|
|
265
|
+
"tokens_saved": orig_tokens - summary_tokens
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
class AutoSummarizer:
|
|
270
|
+
"""Automatically manages conversation summaries with periodic updates"""
|
|
271
|
+
|
|
272
|
+
def __init__(
|
|
273
|
+
self,
|
|
274
|
+
summarizer: ConversationSummarizer,
|
|
275
|
+
memory_manager,
|
|
276
|
+
update_threshold: int = 10,
|
|
277
|
+
logger: Optional[logging.Logger] = None
|
|
278
|
+
):
|
|
279
|
+
"""
|
|
280
|
+
Initialize auto-summarizer
|
|
281
|
+
|
|
282
|
+
Args:
|
|
283
|
+
summarizer: ConversationSummarizer instance
|
|
284
|
+
memory_manager: Memory manager (SQL or JSON)
|
|
285
|
+
update_threshold: Update summary every N conversations
|
|
286
|
+
logger: Logger instance
|
|
287
|
+
"""
|
|
288
|
+
self.summarizer = summarizer
|
|
289
|
+
self.memory = memory_manager
|
|
290
|
+
self.update_threshold = update_threshold
|
|
291
|
+
self.logger = logger or logging.getLogger(__name__)
|
|
292
|
+
|
|
293
|
+
# Track summaries per user
|
|
294
|
+
self.summaries = {} # {user_id: summary_data}
|
|
295
|
+
self.conversation_counts = {} # {user_id: count_since_last_summary}
|
|
296
|
+
|
|
297
|
+
def check_and_update(self, user_id: str) -> Optional[Dict[str, Any]]:
|
|
298
|
+
"""
|
|
299
|
+
Check if summary needs updating and update if necessary
|
|
300
|
+
|
|
301
|
+
Args:
|
|
302
|
+
user_id: User identifier
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
New summary if updated, None otherwise
|
|
306
|
+
"""
|
|
307
|
+
# Get conversation count since last summary
|
|
308
|
+
count_since_last = self.conversation_counts.get(user_id, 0)
|
|
309
|
+
last_summary_time = self.summaries.get(user_id, {}).get("generated_at")
|
|
310
|
+
|
|
311
|
+
if self.summarizer.should_update_summary(last_summary_time, count_since_last, self.update_threshold):
|
|
312
|
+
return self.update_summary(user_id)
|
|
313
|
+
|
|
314
|
+
return None
|
|
315
|
+
|
|
316
|
+
def update_summary(self, user_id: str, max_conversations: int = 20) -> Dict[str, Any]:
|
|
317
|
+
"""
|
|
318
|
+
Force update summary for user
|
|
319
|
+
|
|
320
|
+
Args:
|
|
321
|
+
user_id: User identifier
|
|
322
|
+
max_conversations: Max conversations to summarize
|
|
323
|
+
|
|
324
|
+
Returns:
|
|
325
|
+
Summary data
|
|
326
|
+
"""
|
|
327
|
+
try:
|
|
328
|
+
# Get recent conversations
|
|
329
|
+
if hasattr(self.memory, 'get_recent_conversations'):
|
|
330
|
+
conversations = self.memory.get_recent_conversations(user_id, max_conversations)
|
|
331
|
+
else:
|
|
332
|
+
conversations = []
|
|
333
|
+
|
|
334
|
+
# Generate summary
|
|
335
|
+
summary = self.summarizer.summarize_conversations(
|
|
336
|
+
conversations,
|
|
337
|
+
user_id,
|
|
338
|
+
max_conversations=max_conversations
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
# Store summary
|
|
342
|
+
self.summaries[user_id] = summary
|
|
343
|
+
self.conversation_counts[user_id] = 0
|
|
344
|
+
|
|
345
|
+
self.logger.info(f"✅ Auto-summary updated for {user_id}")
|
|
346
|
+
return summary
|
|
347
|
+
|
|
348
|
+
except Exception as e:
|
|
349
|
+
self.logger.error(f"Auto-summary update failed for {user_id}: {e}")
|
|
350
|
+
return {}
|
|
351
|
+
|
|
352
|
+
def get_summary(self, user_id: str) -> Optional[Dict[str, Any]]:
|
|
353
|
+
"""
|
|
354
|
+
Get current summary for user
|
|
355
|
+
|
|
356
|
+
Args:
|
|
357
|
+
user_id: User identifier
|
|
358
|
+
|
|
359
|
+
Returns:
|
|
360
|
+
Summary data or None
|
|
361
|
+
"""
|
|
362
|
+
return self.summaries.get(user_id)
|
|
363
|
+
|
|
364
|
+
def increment_conversation_count(self, user_id: str):
|
|
365
|
+
"""Increment conversation count for user"""
|
|
366
|
+
self.conversation_counts[user_id] = self.conversation_counts.get(user_id, 0) + 1
|
|
367
|
+
|
|
368
|
+
def reset_summary(self, user_id: str):
|
|
369
|
+
"""Reset summary for user"""
|
|
370
|
+
if user_id in self.summaries:
|
|
371
|
+
del self.summaries[user_id]
|
|
372
|
+
self.conversation_counts[user_id] = 0
|