mem-llm 2.0.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.
@@ -0,0 +1,231 @@
1
+ """
2
+ Configuration Manager
3
+ Reads and manages configuration from YAML file
4
+ """
5
+
6
+ import yaml
7
+ from pathlib import Path
8
+ from typing import Any, Dict, Optional
9
+ import os
10
+
11
+
12
+ class ConfigManager:
13
+ """Manages configuration file"""
14
+
15
+ def __init__(self, config_file: str = "config.yaml"):
16
+ """
17
+ Args:
18
+ config_file: Configuration file path
19
+ """
20
+ self.config_file = Path(config_file)
21
+ self.config: Dict[str, Any] = {}
22
+ self._load_config()
23
+
24
+ def _load_config(self) -> None:
25
+ """Load configuration file"""
26
+ if self.config_file.exists():
27
+ with open(self.config_file, 'r', encoding='utf-8') as f:
28
+ self.config = yaml.safe_load(f) or {}
29
+ else:
30
+ # Default configuration
31
+ self.config = self._get_default_config()
32
+ self.save_config()
33
+
34
+ def _get_default_config(self) -> Dict[str, Any]:
35
+ """Returns default configuration"""
36
+ return {
37
+ "llm": {
38
+ "model": "granite4:3b",
39
+ "base_url": "http://localhost:11434",
40
+ "temperature": 0.7,
41
+ "max_tokens": 500
42
+ },
43
+ "memory": {
44
+ "backend": "sql",
45
+ "json_dir": "memories",
46
+ "db_path": "memories/memories.db",
47
+ "max_conversations_per_user": 1000,
48
+ "auto_cleanup": True,
49
+ "cleanup_after_days": 90
50
+ },
51
+ "prompt": {
52
+ "template": "customer_service",
53
+ "variables": {
54
+ "company_name": "Our Company",
55
+ "tone": "friendly and professional"
56
+ },
57
+ "custom_prompt": None
58
+ },
59
+ "knowledge_base": {
60
+ "enabled": True,
61
+ "auto_load": True,
62
+ "default_kb": "ecommerce",
63
+ "custom_kb_file": None,
64
+ "search_limit": 5,
65
+ "min_relevance_score": 0.3,
66
+ "enable_vector_search": False, # v1.3.2+ - Optional semantic search
67
+ "embedding_model": "all-MiniLM-L6-v2" # Sentence transformers model
68
+ },
69
+ "response": {
70
+ "use_knowledge_base": True,
71
+ "use_memory": True,
72
+ "recent_conversations_limit": 5,
73
+ "format": {
74
+ "include_greeting": True,
75
+ "include_follow_up": True,
76
+ "max_length": 500
77
+ }
78
+ },
79
+ "security": {
80
+ "filter_sensitive_data": True,
81
+ "sensitive_keywords": [
82
+ "credit card", "password", "passcode", "CVV", "TR ID"
83
+ ],
84
+ "rate_limit": {
85
+ "enabled": True,
86
+ "max_requests_per_minute": 60,
87
+ "max_requests_per_user_per_minute": 10
88
+ }
89
+ },
90
+ "logging": {
91
+ "enabled": True,
92
+ "level": "INFO",
93
+ "file": "mem_agent.log",
94
+ "max_size_mb": 10,
95
+ "backup_count": 5,
96
+ "log_user_messages": True,
97
+ "log_bot_responses": True,
98
+ "mask_sensitive": True
99
+ },
100
+ "performance": {
101
+ "enable_cache": True,
102
+ "cache_ttl_seconds": 3600,
103
+ "enable_parallel": False,
104
+ "max_workers": 4
105
+ },
106
+ "analytics": {
107
+ "enabled": True,
108
+ "track_response_time": True,
109
+ "track_user_satisfaction": False,
110
+ "track_conversation_length": True,
111
+ "export_interval_hours": 24,
112
+ "export_path": "analytics"
113
+ }
114
+ }
115
+
116
+ def get(self, key_path: str, default: Any = None) -> Any:
117
+ """
118
+ Get configuration value with dot notation
119
+
120
+ Args:
121
+ key_path: Key path (e.g: "llm.model")
122
+ default: Value to return if not found
123
+
124
+ Returns:
125
+ Configuration value
126
+ """
127
+ keys = key_path.split('.')
128
+ value = self.config
129
+
130
+ for key in keys:
131
+ if isinstance(value, dict) and key in value:
132
+ value = value[key]
133
+ else:
134
+ return default
135
+
136
+ return value
137
+
138
+ def set(self, key_path: str, value: Any) -> None:
139
+ """
140
+ Set configuration value with dot notation
141
+
142
+ Args:
143
+ key_path: Key path (e.g: "llm.model")
144
+ value: Value to set
145
+ """
146
+ keys = key_path.split('.')
147
+ config = self.config
148
+
149
+ for key in keys[:-1]:
150
+ if key not in config or not isinstance(config[key], dict):
151
+ config[key] = {}
152
+ config = config[key]
153
+
154
+ config[keys[-1]] = value
155
+
156
+ def save_config(self) -> None:
157
+ """Save configuration to file"""
158
+ with open(self.config_file, 'w', encoding='utf-8') as f:
159
+ yaml.dump(self.config, f, default_flow_style=False,
160
+ allow_unicode=True, sort_keys=False)
161
+
162
+ def reload(self) -> None:
163
+ """Reload configuration"""
164
+ self._load_config()
165
+
166
+ def get_llm_config(self) -> Dict[str, Any]:
167
+ """Returns LLM configuration"""
168
+ return self.get("llm", {})
169
+
170
+ def get_memory_config(self) -> Dict[str, Any]:
171
+ """Returns memory configuration"""
172
+ return self.get("memory", {})
173
+
174
+ def get_prompt_config(self) -> Dict[str, Any]:
175
+ """Returns prompt configuration"""
176
+ return self.get("prompt", {})
177
+
178
+ def get_kb_config(self) -> Dict[str, Any]:
179
+ """Returns knowledge base configuration"""
180
+ return self.get("knowledge_base", {})
181
+
182
+ def is_kb_enabled(self) -> bool:
183
+ """Is knowledge base enabled?"""
184
+ return self.get("knowledge_base.enabled", True)
185
+
186
+ def is_memory_enabled(self) -> bool:
187
+ """Is memory enabled?"""
188
+ return self.get("response.use_memory", True)
189
+
190
+ def get_memory_backend(self) -> str:
191
+ """Returns memory backend type (json or sql)"""
192
+ return self.get("memory.backend", "sql")
193
+
194
+ def get_db_path(self) -> str:
195
+ """Returns database file path"""
196
+ return self.get("memory.db_path", "memories.db")
197
+
198
+ def get_json_dir(self) -> str:
199
+ """Returns JSON memory directory"""
200
+ return self.get("memory.json_dir", "memories")
201
+
202
+ def __repr__(self) -> str:
203
+ return f"ConfigManager(file='{self.config_file}')"
204
+
205
+
206
+ # Global instance
207
+ _config_manager: Optional[ConfigManager] = None
208
+
209
+
210
+ def get_config(config_file: str = "config.yaml") -> ConfigManager:
211
+ """
212
+ Returns global configuration manager
213
+
214
+ Args:
215
+ config_file: Configuration file
216
+
217
+ Returns:
218
+ ConfigManager instance
219
+ """
220
+ global _config_manager
221
+ if _config_manager is None:
222
+ _config_manager = ConfigManager(config_file)
223
+ return _config_manager
224
+
225
+
226
+ def reload_config() -> None:
227
+ """Reloads global configuration"""
228
+ global _config_manager
229
+ if _config_manager:
230
+ _config_manager.reload()
231
+
@@ -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