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 CHANGED
@@ -24,7 +24,40 @@ 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
+ # 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
@@ -43,7 +43,7 @@ class ConfigManager:
43
43
  "memory": {
44
44
  "backend": "sql",
45
45
  "json_dir": "memories",
46
- "db_path": "memories.db",
46
+ "db_path": "memories/memories.db",
47
47
  "max_conversations_per_user": 1000,
48
48
  "auto_cleanup": True,
49
49
  "cleanup_after_days": 90
@@ -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