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.
- mem_llm/__init__.py +98 -0
- mem_llm/api_server.py +595 -0
- mem_llm/base_llm_client.py +201 -0
- mem_llm/builtin_tools.py +311 -0
- mem_llm/cli.py +254 -0
- mem_llm/clients/__init__.py +22 -0
- mem_llm/clients/lmstudio_client.py +393 -0
- mem_llm/clients/ollama_client.py +354 -0
- mem_llm/config.yaml.example +52 -0
- mem_llm/config_from_docs.py +180 -0
- mem_llm/config_manager.py +231 -0
- mem_llm/conversation_summarizer.py +372 -0
- mem_llm/data_export_import.py +640 -0
- mem_llm/dynamic_prompt.py +298 -0
- mem_llm/knowledge_loader.py +88 -0
- mem_llm/llm_client.py +225 -0
- mem_llm/llm_client_factory.py +260 -0
- mem_llm/logger.py +129 -0
- mem_llm/mem_agent.py +1611 -0
- mem_llm/memory_db.py +612 -0
- mem_llm/memory_manager.py +321 -0
- mem_llm/memory_tools.py +253 -0
- mem_llm/prompt_security.py +304 -0
- mem_llm/response_metrics.py +221 -0
- mem_llm/retry_handler.py +193 -0
- mem_llm/thread_safe_db.py +301 -0
- mem_llm/tool_system.py +429 -0
- mem_llm/vector_store.py +278 -0
- mem_llm/web_launcher.py +129 -0
- mem_llm/web_ui/README.md +44 -0
- mem_llm/web_ui/__init__.py +7 -0
- mem_llm/web_ui/index.html +641 -0
- mem_llm/web_ui/memory.html +569 -0
- mem_llm/web_ui/metrics.html +75 -0
- mem_llm-2.0.0.dist-info/METADATA +667 -0
- mem_llm-2.0.0.dist-info/RECORD +39 -0
- mem_llm-2.0.0.dist-info/WHEEL +5 -0
- mem_llm-2.0.0.dist-info/entry_points.txt +3 -0
- mem_llm-2.0.0.dist-info/top_level.txt +1 -0
|
@@ -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
|