mem-llm 1.0.2__py3-none-any.whl → 2.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 +71 -8
- mem_llm/api_server.py +595 -0
- mem_llm/base_llm_client.py +201 -0
- mem_llm/builtin_tools.py +311 -0
- mem_llm/builtin_tools_async.py +170 -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 +1 -1
- mem_llm/config_from_docs.py +1 -1
- mem_llm/config_manager.py +5 -3
- mem_llm/conversation_summarizer.py +372 -0
- mem_llm/data_export_import.py +640 -0
- mem_llm/dynamic_prompt.py +298 -0
- mem_llm/llm_client.py +77 -14
- mem_llm/llm_client_factory.py +260 -0
- mem_llm/logger.py +129 -0
- mem_llm/mem_agent.py +1178 -87
- mem_llm/memory_db.py +290 -59
- mem_llm/memory_manager.py +60 -1
- 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 +537 -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.1.0.dist-info/METADATA +753 -0
- mem_llm-2.1.0.dist-info/RECORD +40 -0
- {mem_llm-1.0.2.dist-info → mem_llm-2.1.0.dist-info}/WHEEL +1 -1
- mem_llm-2.1.0.dist-info/entry_points.txt +3 -0
- mem_llm/prompt_templates.py +0 -244
- mem_llm-1.0.2.dist-info/METADATA +0 -382
- mem_llm-1.0.2.dist-info/RECORD +0 -15
- {mem_llm-1.0.2.dist-info → mem_llm-2.1.0.dist-info}/top_level.txt +0 -0
mem_llm/mem_agent.py
CHANGED
|
@@ -29,23 +29,28 @@ agent = MemAgent(
|
|
|
29
29
|
```
|
|
30
30
|
"""
|
|
31
31
|
|
|
32
|
-
from typing import Optional, Dict, List, Any, Union
|
|
32
|
+
from typing import Optional, Dict, List, Any, Union, Iterator
|
|
33
33
|
from datetime import datetime
|
|
34
34
|
import logging
|
|
35
35
|
import json
|
|
36
36
|
import os
|
|
37
|
+
import time
|
|
37
38
|
|
|
38
39
|
# Core dependencies
|
|
39
40
|
from .memory_manager import MemoryManager
|
|
40
|
-
from .llm_client import OllamaClient
|
|
41
|
+
from .llm_client import OllamaClient # Backward compatibility
|
|
42
|
+
from .llm_client_factory import LLMClientFactory
|
|
43
|
+
from .base_llm_client import BaseLLMClient
|
|
44
|
+
from .response_metrics import ChatResponse, ResponseMetricsAnalyzer, calculate_confidence
|
|
45
|
+
from .tool_system import ToolRegistry, ToolCallParser, format_tools_for_prompt
|
|
41
46
|
|
|
42
47
|
# Advanced features (optional)
|
|
43
48
|
try:
|
|
44
49
|
from .memory_db import SQLMemoryManager
|
|
45
|
-
from .prompt_templates import prompt_manager
|
|
46
50
|
from .knowledge_loader import KnowledgeLoader
|
|
47
51
|
from .config_manager import get_config
|
|
48
52
|
from .memory_tools import ToolExecutor, MemoryTools
|
|
53
|
+
from .dynamic_prompt import dynamic_prompt_builder
|
|
49
54
|
ADVANCED_AVAILABLE = True
|
|
50
55
|
except ImportError:
|
|
51
56
|
ADVANCED_AVAILABLE = False
|
|
@@ -60,21 +65,69 @@ class MemAgent:
|
|
|
60
65
|
"""
|
|
61
66
|
|
|
62
67
|
def __init__(self,
|
|
63
|
-
model: str = "granite4:
|
|
68
|
+
model: str = "granite4:3b",
|
|
69
|
+
backend: str = "ollama",
|
|
64
70
|
config_file: Optional[str] = None,
|
|
65
71
|
use_sql: bool = True,
|
|
66
72
|
memory_dir: Optional[str] = None,
|
|
73
|
+
db_path: Optional[str] = None,
|
|
67
74
|
load_knowledge_base: bool = True,
|
|
68
|
-
ollama_url: str = "http://localhost:11434"
|
|
75
|
+
ollama_url: str = "http://localhost:11434",
|
|
76
|
+
base_url: Optional[str] = None,
|
|
77
|
+
api_key: Optional[str] = None,
|
|
78
|
+
auto_detect_backend: bool = False,
|
|
79
|
+
check_connection: bool = False,
|
|
80
|
+
enable_security: bool = False,
|
|
81
|
+
enable_vector_search: bool = False,
|
|
82
|
+
embedding_model: str = "all-MiniLM-L6-v2",
|
|
83
|
+
enable_tools: bool = False,
|
|
84
|
+
tools: Optional[List] = None,
|
|
85
|
+
**llm_kwargs):
|
|
69
86
|
"""
|
|
70
87
|
Args:
|
|
71
88
|
model: LLM model to use
|
|
89
|
+
backend: LLM backend ('ollama', 'lmstudio') - NEW in v1.3.0
|
|
72
90
|
config_file: Configuration file (optional)
|
|
73
91
|
use_sql: Use SQL database (True) or JSON (False)
|
|
74
|
-
memory_dir: Memory directory
|
|
92
|
+
memory_dir: Memory directory (for JSON mode or if db_path not specified)
|
|
93
|
+
db_path: SQLite database path (for SQL mode, e.g., ":memory:" or "path/to/db.db")
|
|
75
94
|
load_knowledge_base: Automatically load knowledge base
|
|
76
|
-
ollama_url: Ollama API URL
|
|
95
|
+
ollama_url: Ollama API URL (backward compatibility, use base_url instead)
|
|
96
|
+
base_url: Backend API URL (for local backends) - NEW in v1.3.0
|
|
97
|
+
auto_detect_backend: Auto-detect available LLM backend - NEW in v1.3.0
|
|
98
|
+
check_connection: Verify LLM connection on startup (default: False)
|
|
99
|
+
enable_security: Enable prompt injection protection (v1.1.0+, default: False for backward compatibility)
|
|
100
|
+
enable_vector_search: Enable semantic/vector search for KB (v1.3.2+, requires chromadb) - NEW
|
|
101
|
+
embedding_model: Embedding model for vector search (default: "all-MiniLM-L6-v2") - NEW
|
|
102
|
+
**llm_kwargs: Additional backend-specific parameters
|
|
103
|
+
|
|
104
|
+
Examples:
|
|
105
|
+
# Default Ollama
|
|
106
|
+
agent = MemAgent()
|
|
107
|
+
|
|
108
|
+
# LM Studio
|
|
109
|
+
agent = MemAgent(backend='lmstudio', model='llama-3-8b')
|
|
110
|
+
|
|
111
|
+
# Auto-detect
|
|
112
|
+
agent = MemAgent(auto_detect_backend=True)
|
|
77
113
|
"""
|
|
114
|
+
|
|
115
|
+
# Setup logging first
|
|
116
|
+
self._setup_logging()
|
|
117
|
+
|
|
118
|
+
# Security features (v1.1.0+)
|
|
119
|
+
self.enable_security = enable_security
|
|
120
|
+
self.security_detector = None
|
|
121
|
+
self.security_sanitizer = None
|
|
122
|
+
|
|
123
|
+
if enable_security:
|
|
124
|
+
try:
|
|
125
|
+
from .prompt_security import PromptInjectionDetector, InputSanitizer
|
|
126
|
+
self.security_detector = PromptInjectionDetector()
|
|
127
|
+
self.security_sanitizer = InputSanitizer()
|
|
128
|
+
self.logger.info("🔒 Security features enabled (prompt injection protection)")
|
|
129
|
+
except ImportError:
|
|
130
|
+
self.logger.warning("⚠️ Security features requested but not available")
|
|
78
131
|
|
|
79
132
|
# Load configuration
|
|
80
133
|
self.config = None
|
|
@@ -95,39 +148,182 @@ class MemAgent:
|
|
|
95
148
|
# No config file
|
|
96
149
|
self.usage_mode = "personal"
|
|
97
150
|
|
|
98
|
-
#
|
|
99
|
-
self.
|
|
100
|
-
|
|
101
|
-
|
|
151
|
+
# Initialize flags first
|
|
152
|
+
self.has_knowledge_base: bool = False # Track KB status
|
|
153
|
+
self.has_tools: bool = False # Track tools status (v1.3.x)
|
|
154
|
+
|
|
155
|
+
# Tool system (v2.0.0+)
|
|
156
|
+
self.enable_tools = enable_tools
|
|
157
|
+
self.tool_registry = None
|
|
158
|
+
if enable_tools:
|
|
159
|
+
self.tool_registry = ToolRegistry()
|
|
160
|
+
self.has_tools = True
|
|
161
|
+
|
|
162
|
+
# Register custom tools if provided
|
|
163
|
+
if tools:
|
|
164
|
+
for tool in tools:
|
|
165
|
+
self.tool_registry.register_function(tool)
|
|
166
|
+
self.logger.info(f"🔧 Registered {len(tools)} custom tools")
|
|
167
|
+
|
|
168
|
+
builtin_count = len(self.tool_registry.tools)
|
|
169
|
+
self.logger.info(f"🛠️ Tool system enabled ({builtin_count} tools available)")
|
|
170
|
+
|
|
171
|
+
# Memory system
|
|
102
172
|
if use_sql and ADVANCED_AVAILABLE:
|
|
103
173
|
# SQL memory (advanced)
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
174
|
+
# Determine database path
|
|
175
|
+
if db_path:
|
|
176
|
+
# Use provided db_path (can be ":memory:" for in-memory DB)
|
|
177
|
+
final_db_path = db_path
|
|
178
|
+
elif memory_dir:
|
|
179
|
+
final_db_path = memory_dir
|
|
180
|
+
elif self.config:
|
|
181
|
+
final_db_path = self.config.get("memory.db_path", "memories/memories.db")
|
|
182
|
+
else:
|
|
183
|
+
final_db_path = "memories/memories.db"
|
|
184
|
+
|
|
185
|
+
# Get vector search settings from config or parameters
|
|
186
|
+
vector_search_enabled = enable_vector_search
|
|
187
|
+
vector_model = embedding_model
|
|
188
|
+
|
|
189
|
+
if self.config:
|
|
190
|
+
vector_search_enabled = self.config.get("knowledge_base.enable_vector_search", vector_search_enabled)
|
|
191
|
+
vector_model = self.config.get("knowledge_base.embedding_model", vector_model)
|
|
192
|
+
|
|
193
|
+
# Ensure memories directory exists (skip for :memory:)
|
|
194
|
+
import os
|
|
195
|
+
if final_db_path != ":memory:":
|
|
196
|
+
db_dir = os.path.dirname(final_db_path)
|
|
197
|
+
if db_dir and not os.path.exists(db_dir):
|
|
198
|
+
os.makedirs(db_dir, exist_ok=True)
|
|
199
|
+
|
|
200
|
+
self.memory = SQLMemoryManager(
|
|
201
|
+
final_db_path,
|
|
202
|
+
enable_vector_search=vector_search_enabled,
|
|
203
|
+
embedding_model=vector_model
|
|
204
|
+
)
|
|
205
|
+
self.logger.info(f"SQL memory system active: {final_db_path}")
|
|
206
|
+
if vector_search_enabled:
|
|
207
|
+
self.logger.info(f"🔍 Vector search enabled (model: {vector_model})")
|
|
107
208
|
else:
|
|
108
209
|
# JSON memory (simple)
|
|
109
210
|
json_dir = memory_dir or self.config.get("memory.json_dir", "memories") if self.config else "memories"
|
|
110
211
|
self.memory = MemoryManager(json_dir)
|
|
111
212
|
self.logger.info(f"JSON memory system active: {json_dir}")
|
|
112
213
|
|
|
214
|
+
# Active user and system prompt
|
|
215
|
+
self.current_user: Optional[str] = None
|
|
216
|
+
self.current_system_prompt: Optional[str] = None
|
|
217
|
+
|
|
113
218
|
# LLM client
|
|
114
219
|
self.model = model # Store model name
|
|
220
|
+
self.backend = backend # Store backend name
|
|
115
221
|
self.use_sql = use_sql # Store SQL usage flag
|
|
116
|
-
|
|
117
|
-
|
|
222
|
+
|
|
223
|
+
# Initialize LLM client (v1.3.0: Multi-backend support)
|
|
224
|
+
# Prepare backend configuration
|
|
225
|
+
llm_config = llm_kwargs.copy()
|
|
226
|
+
|
|
227
|
+
# Handle backward compatibility: ollama_url -> base_url
|
|
228
|
+
if base_url is None and backend == "ollama":
|
|
229
|
+
base_url = ollama_url
|
|
230
|
+
|
|
231
|
+
# Add base_url for local backends
|
|
232
|
+
if base_url and backend in ['ollama', 'lmstudio']:
|
|
233
|
+
llm_config['base_url'] = base_url
|
|
234
|
+
|
|
235
|
+
# Add api_key for cloud backends
|
|
236
|
+
# Auto-detect backend if requested
|
|
237
|
+
if auto_detect_backend:
|
|
238
|
+
self.logger.info("🔍 Auto-detecting available LLM backend...")
|
|
239
|
+
self.llm = LLMClientFactory.auto_detect()
|
|
240
|
+
if self.llm:
|
|
241
|
+
detected_backend = self.llm.__class__.__name__
|
|
242
|
+
self.logger.info(f"✅ Detected and using: {detected_backend}")
|
|
243
|
+
else:
|
|
244
|
+
self.logger.error("❌ No LLM backend available.")
|
|
245
|
+
raise RuntimeError(
|
|
246
|
+
"No LLM backend detected. Please start a local LLM service (Ollama or LM Studio)."
|
|
247
|
+
)
|
|
248
|
+
else:
|
|
249
|
+
# Create client using factory
|
|
250
|
+
try:
|
|
251
|
+
self.llm = LLMClientFactory.create(
|
|
252
|
+
backend=backend,
|
|
253
|
+
model=model,
|
|
254
|
+
**llm_config
|
|
255
|
+
)
|
|
256
|
+
self.logger.info(f"✅ Initialized {backend} backend with model: {model}")
|
|
257
|
+
except Exception as e:
|
|
258
|
+
self.logger.error(f"❌ Failed to initialize {backend} backend: {e}")
|
|
259
|
+
raise
|
|
260
|
+
|
|
261
|
+
# Optional connection check on startup
|
|
262
|
+
if check_connection:
|
|
263
|
+
backend_name = backend if not auto_detect_backend else "LLM service"
|
|
264
|
+
self.logger.info(f"Checking {backend_name} connection...")
|
|
265
|
+
if not self.llm.check_connection():
|
|
266
|
+
error_msg = f"❌ ERROR: Cannot connect to {backend_name}!\n"
|
|
267
|
+
|
|
268
|
+
if backend == "ollama":
|
|
269
|
+
error_msg += (
|
|
270
|
+
" \n"
|
|
271
|
+
" Solutions:\n"
|
|
272
|
+
" 1. Start Ollama: ollama serve\n"
|
|
273
|
+
" 2. Check if Ollama is running: http://localhost:11434\n"
|
|
274
|
+
" 3. Verify base_url parameter is correct\n"
|
|
275
|
+
)
|
|
276
|
+
elif backend == "lmstudio":
|
|
277
|
+
error_msg += (
|
|
278
|
+
" \n"
|
|
279
|
+
" Solutions:\n"
|
|
280
|
+
" 1. Start LM Studio\n"
|
|
281
|
+
" 2. Load a model in LM Studio\n"
|
|
282
|
+
" 3. Start local server (default: http://localhost:1234)\n"
|
|
283
|
+
" 4. Verify base_url parameter is correct\n"
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
error_msg += " \n To skip this check, use: MemAgent(check_connection=False)"
|
|
287
|
+
self.logger.error(error_msg)
|
|
288
|
+
raise ConnectionError(f"{backend_name} not available")
|
|
289
|
+
|
|
290
|
+
# Check if model exists (for backends that support listing)
|
|
291
|
+
try:
|
|
292
|
+
available_models = self.llm.list_models()
|
|
293
|
+
if available_models and model not in available_models:
|
|
294
|
+
error_msg = (
|
|
295
|
+
f"❌ ERROR: Model '{model}' not found in {backend}!\n"
|
|
296
|
+
f" \n"
|
|
297
|
+
f" Available models: {', '.join(available_models[:5])}\n"
|
|
298
|
+
f" Total: {len(available_models)} models available\n"
|
|
299
|
+
f" \n"
|
|
300
|
+
f" To skip this check, use: MemAgent(check_connection=False)"
|
|
301
|
+
)
|
|
302
|
+
self.logger.error(error_msg)
|
|
303
|
+
raise ValueError(f"Model '{model}' not available")
|
|
304
|
+
except:
|
|
305
|
+
# Some backends may not support list_models, skip check
|
|
306
|
+
pass
|
|
307
|
+
|
|
308
|
+
self.logger.info(f"✅ {backend_name} connection verified, model '{model}' ready")
|
|
309
|
+
|
|
310
|
+
self.logger.info(f"LLM client ready: {model} on {backend}")
|
|
311
|
+
|
|
118
312
|
|
|
119
313
|
# Advanced features (if available)
|
|
120
314
|
if ADVANCED_AVAILABLE:
|
|
121
315
|
self._setup_advanced_features(load_knowledge_base)
|
|
122
316
|
else:
|
|
123
317
|
print("⚠️ Load additional packages for advanced features")
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
self.current_user: Optional[str] = None
|
|
127
|
-
self.current_system_prompt: Optional[str] = None
|
|
318
|
+
# Build basic prompt even without advanced features
|
|
319
|
+
self._build_dynamic_system_prompt()
|
|
128
320
|
|
|
129
321
|
# Tool system (always available)
|
|
130
322
|
self.tool_executor = ToolExecutor(self.memory)
|
|
323
|
+
|
|
324
|
+
# Metrics tracking system (v1.3.1+)
|
|
325
|
+
self.metrics_analyzer = ResponseMetricsAnalyzer()
|
|
326
|
+
self.track_metrics = True # Can be disabled if needed
|
|
131
327
|
|
|
132
328
|
self.logger.info("MemAgent successfully initialized")
|
|
133
329
|
|
|
@@ -139,17 +335,23 @@ class MemAgent:
|
|
|
139
335
|
if ADVANCED_AVAILABLE and hasattr(self, 'config') and self.config:
|
|
140
336
|
log_config = self.config.get("logging", {})
|
|
141
337
|
|
|
338
|
+
# Default to WARNING level to keep console clean (users can override in config)
|
|
339
|
+
default_level = "WARNING"
|
|
340
|
+
|
|
142
341
|
if log_config.get("enabled", True):
|
|
342
|
+
# Only console logging (no file) - keep workspace clean
|
|
143
343
|
logging.basicConfig(
|
|
144
|
-
level=getattr(logging, log_config.get("level",
|
|
344
|
+
level=getattr(logging, log_config.get("level", default_level)),
|
|
145
345
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|
146
346
|
handlers=[
|
|
147
|
-
logging.
|
|
148
|
-
logging.StreamHandler()
|
|
347
|
+
logging.StreamHandler() # Console only
|
|
149
348
|
]
|
|
150
349
|
)
|
|
151
350
|
|
|
152
351
|
self.logger = logging.getLogger("MemAgent")
|
|
352
|
+
|
|
353
|
+
# Set default level for mem_llm loggers
|
|
354
|
+
logging.getLogger("mem_llm").setLevel(getattr(logging, log_config.get("level", default_level)))
|
|
153
355
|
|
|
154
356
|
def _setup_advanced_features(self, load_knowledge_base: bool) -> None:
|
|
155
357
|
"""Setup advanced features"""
|
|
@@ -171,60 +373,79 @@ class MemAgent:
|
|
|
171
373
|
if default_kb == "ecommerce":
|
|
172
374
|
count = kb_loader.load_default_ecommerce_kb()
|
|
173
375
|
self.logger.info(f"E-commerce knowledge base loaded: {count} records")
|
|
376
|
+
self.has_knowledge_base = True # KB loaded!
|
|
174
377
|
elif default_kb == "tech_support":
|
|
175
378
|
count = kb_loader.load_default_tech_support_kb()
|
|
176
379
|
self.logger.info(f"Technical support knowledge base loaded: {count} records")
|
|
380
|
+
self.has_knowledge_base = True # KB loaded!
|
|
177
381
|
elif default_kb == "business_tech_support":
|
|
178
382
|
count = kb_loader.load_default_tech_support_kb()
|
|
179
383
|
self.logger.info(f"Corporate technical support knowledge base loaded: {count} records")
|
|
384
|
+
self.has_knowledge_base = True # KB loaded!
|
|
180
385
|
elif default_kb == "personal_learning":
|
|
181
386
|
# Simple KB for personal learning
|
|
182
387
|
count = kb_loader.load_default_ecommerce_kb() # Temporarily use the same KB
|
|
183
388
|
self.logger.info(f"Personal learning knowledge base loaded: {count} records")
|
|
389
|
+
self.has_knowledge_base = True # KB loaded!
|
|
184
390
|
except Exception as e:
|
|
185
391
|
self.logger.error(f"Knowledge base loading error: {e}")
|
|
186
|
-
|
|
187
|
-
|
|
392
|
+
self.has_knowledge_base = False
|
|
393
|
+
|
|
394
|
+
# Build dynamic system prompt based on active features
|
|
395
|
+
self._build_dynamic_system_prompt()
|
|
396
|
+
|
|
397
|
+
def _build_dynamic_system_prompt(self) -> None:
|
|
398
|
+
"""Build dynamic system prompt based on active features"""
|
|
399
|
+
if not ADVANCED_AVAILABLE:
|
|
400
|
+
# Fallback simple prompt
|
|
401
|
+
self.current_system_prompt = "You are a helpful AI assistant."
|
|
402
|
+
return
|
|
403
|
+
|
|
404
|
+
# Get config data
|
|
405
|
+
business_config = None
|
|
406
|
+
personal_config = None
|
|
407
|
+
|
|
188
408
|
if hasattr(self, 'config') and self.config:
|
|
189
|
-
prompt_config = self.config.get("prompt", {})
|
|
190
|
-
|
|
191
|
-
# Select default template according to usage mode
|
|
192
|
-
if self.usage_mode == "business":
|
|
193
|
-
default_template = "business_customer_service"
|
|
194
|
-
else: # personal
|
|
195
|
-
default_template = "personal_assistant"
|
|
196
|
-
|
|
197
|
-
template_name = prompt_config.get("template", default_template)
|
|
198
|
-
variables = prompt_config.get("variables", {})
|
|
199
|
-
|
|
200
|
-
# Additional variables for business mode
|
|
201
409
|
if self.usage_mode == "business":
|
|
202
410
|
business_config = self.config.get("business", {})
|
|
203
|
-
|
|
204
|
-
"company_name": business_config.get("company_name", "Our Company"),
|
|
205
|
-
"founded_year": business_config.get("founded_year", "2010"),
|
|
206
|
-
"employee_count": business_config.get("employee_count", "100+"),
|
|
207
|
-
"industry": business_config.get("industry", "Teknoloji")
|
|
208
|
-
})
|
|
209
|
-
else: # personal
|
|
411
|
+
else:
|
|
210
412
|
personal_config = self.config.get("personal", {})
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
self.
|
|
220
|
-
|
|
221
|
-
self.
|
|
222
|
-
#
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
413
|
+
|
|
414
|
+
# Check if tools are enabled (future feature)
|
|
415
|
+
# For now, tools are always available but not advertised in prompt
|
|
416
|
+
# self.has_tools = False # Will be enabled when tool system is ready
|
|
417
|
+
|
|
418
|
+
# Build prompt using dynamic builder
|
|
419
|
+
try:
|
|
420
|
+
self.current_system_prompt = dynamic_prompt_builder.build_prompt(
|
|
421
|
+
usage_mode=self.usage_mode,
|
|
422
|
+
has_knowledge_base=self.has_knowledge_base,
|
|
423
|
+
has_tools=self.enable_tools, # Now advertised when enabled (v2.0+)
|
|
424
|
+
is_multi_user=False, # Always False for now, per-session state
|
|
425
|
+
business_config=business_config,
|
|
426
|
+
personal_config=personal_config,
|
|
427
|
+
memory_type="sql" if self.use_sql else "json"
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
# Add tool information to prompt if tools are enabled (v2.0+)
|
|
431
|
+
if self.enable_tools and self.tool_registry:
|
|
432
|
+
tools_list = self.tool_registry.list_tools()
|
|
433
|
+
tools_prompt = format_tools_for_prompt(tools_list)
|
|
434
|
+
self.current_system_prompt += f"\n\n{tools_prompt}"
|
|
435
|
+
|
|
436
|
+
# Log feature summary
|
|
437
|
+
feature_summary = dynamic_prompt_builder.get_feature_summary(
|
|
438
|
+
has_knowledge_base=self.has_knowledge_base,
|
|
439
|
+
has_tools=self.enable_tools,
|
|
440
|
+
is_multi_user=False,
|
|
441
|
+
memory_type="sql" if self.use_sql else "json"
|
|
442
|
+
)
|
|
443
|
+
self.logger.info(f"Dynamic prompt built: {feature_summary}")
|
|
444
|
+
|
|
445
|
+
except Exception as e:
|
|
446
|
+
self.logger.error(f"Dynamic prompt building error: {e}")
|
|
447
|
+
# Fallback
|
|
448
|
+
self.current_system_prompt = "You are a helpful AI assistant."
|
|
228
449
|
|
|
229
450
|
def check_setup(self) -> Dict[str, Any]:
|
|
230
451
|
"""Check system setup"""
|
|
@@ -282,9 +503,130 @@ class MemAgent:
|
|
|
282
503
|
self.memory.update_user_profile(user_id, {"name": name})
|
|
283
504
|
|
|
284
505
|
self.logger.debug(f"Active user set: {user_id}")
|
|
506
|
+
|
|
507
|
+
def _execute_tool_calls(self, response_text: str, max_iterations: int = 3) -> str:
|
|
508
|
+
"""
|
|
509
|
+
Execute tool calls found in LLM response and get results.
|
|
510
|
+
|
|
511
|
+
Args:
|
|
512
|
+
response_text: LLM response that may contain tool calls
|
|
513
|
+
max_iterations: Maximum number of tool execution iterations
|
|
514
|
+
|
|
515
|
+
Returns:
|
|
516
|
+
Final response after all tool executions
|
|
517
|
+
"""
|
|
518
|
+
iteration = 0
|
|
519
|
+
current_text = response_text
|
|
520
|
+
|
|
521
|
+
while iteration < max_iterations:
|
|
522
|
+
# Check if response contains tool calls
|
|
523
|
+
if not ToolCallParser.has_tool_call(current_text):
|
|
524
|
+
break
|
|
525
|
+
|
|
526
|
+
# Parse tool calls
|
|
527
|
+
tool_calls = ToolCallParser.parse(current_text)
|
|
528
|
+
if not tool_calls:
|
|
529
|
+
break
|
|
530
|
+
|
|
531
|
+
self.logger.info(f"🔧 Detected {len(tool_calls)} tool call(s)")
|
|
532
|
+
|
|
533
|
+
# Execute each tool
|
|
534
|
+
tool_results = []
|
|
535
|
+
for call in tool_calls:
|
|
536
|
+
tool_name = call["tool"]
|
|
537
|
+
arguments = call["arguments"]
|
|
538
|
+
|
|
539
|
+
self.logger.info(f" Executing: {tool_name}({arguments})")
|
|
540
|
+
|
|
541
|
+
# Execute tool
|
|
542
|
+
result = self.tool_registry.execute(tool_name, **arguments)
|
|
543
|
+
|
|
544
|
+
# Handle memory-specific tools
|
|
545
|
+
if result.status.value == "success" and isinstance(result.result, str):
|
|
546
|
+
if result.result.startswith("MEMORY_SEARCH:"):
|
|
547
|
+
keyword = result.result.split(":", 1)[1]
|
|
548
|
+
try:
|
|
549
|
+
search_results = self.memory_manager.search_conversations(keyword)
|
|
550
|
+
if search_results:
|
|
551
|
+
formatted = f"Found {len(search_results)} results for '{keyword}':\n"
|
|
552
|
+
for idx, conv in enumerate(search_results[:5], 1):
|
|
553
|
+
formatted += f"{idx}. {conv.get('user', 'N/A')}: {conv.get('message', 'N/A')[:100]}...\n"
|
|
554
|
+
result.result = formatted
|
|
555
|
+
else:
|
|
556
|
+
result.result = f"No conversations found containing '{keyword}'"
|
|
557
|
+
except Exception as e:
|
|
558
|
+
result.result = f"Memory search error: {e}"
|
|
559
|
+
|
|
560
|
+
elif result.result == "MEMORY_USER_INFO":
|
|
561
|
+
try:
|
|
562
|
+
user_info = f"Current user: {self.current_user or 'Not set'}"
|
|
563
|
+
if self.current_user:
|
|
564
|
+
conv_count = len(self.memory_manager.get_conversation_history(self.current_user))
|
|
565
|
+
user_info += f"\nTotal conversations: {conv_count}"
|
|
566
|
+
result.result = user_info
|
|
567
|
+
except Exception as e:
|
|
568
|
+
result.result = f"User info error: {e}"
|
|
569
|
+
|
|
570
|
+
elif result.result.startswith("MEMORY_LIST_CONVERSATIONS:"):
|
|
571
|
+
try:
|
|
572
|
+
limit = int(result.result.split(":", 1)[1])
|
|
573
|
+
history = self.memory_manager.get_conversation_history(self.current_user or "default", limit=limit)
|
|
574
|
+
if history:
|
|
575
|
+
formatted = f"Last {len(history)} conversations:\n"
|
|
576
|
+
for idx, conv in enumerate(history, 1):
|
|
577
|
+
role = conv.get('role', 'unknown')
|
|
578
|
+
msg = conv.get('content', '')[:80]
|
|
579
|
+
formatted += f"{idx}. [{role}] {msg}...\n"
|
|
580
|
+
result.result = formatted
|
|
581
|
+
else:
|
|
582
|
+
result.result = "No conversation history found"
|
|
583
|
+
except Exception as e:
|
|
584
|
+
result.result = f"Conversation list error: {e}"
|
|
585
|
+
|
|
586
|
+
if result.status.value == "success": # Compare with enum value
|
|
587
|
+
self.logger.info(f" ✅ Success: {result.result}")
|
|
588
|
+
tool_results.append(f"Tool '{tool_name}' returned: {result.result}")
|
|
589
|
+
else:
|
|
590
|
+
self.logger.warning(f" ❌ Error: {result.error}")
|
|
591
|
+
tool_results.append(f"Tool '{tool_name}' failed with error: {result.error}")
|
|
592
|
+
|
|
593
|
+
# Remove tool call syntax from response
|
|
594
|
+
clean_text = ToolCallParser.remove_tool_calls(current_text)
|
|
595
|
+
|
|
596
|
+
# If we have tool results, ask LLM to continue with the results
|
|
597
|
+
if tool_results:
|
|
598
|
+
results_text = "\n".join(tool_results)
|
|
599
|
+
|
|
600
|
+
# Build follow-up message for LLM
|
|
601
|
+
follow_up = f"{clean_text}\n\nTool Results:\n{results_text}\n\nPlease provide the final answer to the user based on these results."
|
|
602
|
+
|
|
603
|
+
# Get LLM response with tool results
|
|
604
|
+
try:
|
|
605
|
+
messages = [
|
|
606
|
+
{"role": "system", "content": "You are a helpful assistant. Use the tool results to answer the user's question."},
|
|
607
|
+
{"role": "user", "content": follow_up}
|
|
608
|
+
]
|
|
609
|
+
|
|
610
|
+
llm_response = self.llm.chat(
|
|
611
|
+
messages=messages,
|
|
612
|
+
temperature=0.7,
|
|
613
|
+
max_tokens=500
|
|
614
|
+
)
|
|
615
|
+
|
|
616
|
+
current_text = llm_response
|
|
617
|
+
iteration += 1
|
|
618
|
+
except Exception as e:
|
|
619
|
+
self.logger.error(f"Error getting follow-up response: {e}")
|
|
620
|
+
# Return what we have
|
|
621
|
+
return f"{clean_text}\n\n{results_text}"
|
|
622
|
+
else:
|
|
623
|
+
# No tool results, return clean text
|
|
624
|
+
return clean_text
|
|
625
|
+
|
|
626
|
+
return current_text
|
|
285
627
|
|
|
286
628
|
def chat(self, message: str, user_id: Optional[str] = None,
|
|
287
|
-
metadata: Optional[Dict] = None) -> str:
|
|
629
|
+
metadata: Optional[Dict] = None, return_metrics: bool = False) -> Union[str, ChatResponse]:
|
|
288
630
|
"""
|
|
289
631
|
Chat with user
|
|
290
632
|
|
|
@@ -292,37 +634,103 @@ class MemAgent:
|
|
|
292
634
|
message: User's message
|
|
293
635
|
user_id: User ID (optional)
|
|
294
636
|
metadata: Additional information
|
|
637
|
+
return_metrics: If True, returns ChatResponse with metrics; if False, returns only text (default)
|
|
295
638
|
|
|
296
639
|
Returns:
|
|
297
|
-
Bot's response
|
|
640
|
+
Bot's response (str) or ChatResponse object with metrics
|
|
298
641
|
"""
|
|
642
|
+
# Start timing
|
|
643
|
+
start_time = time.time()
|
|
299
644
|
# Determine user
|
|
300
645
|
if user_id:
|
|
301
646
|
self.set_user(user_id)
|
|
302
647
|
elif not self.current_user:
|
|
303
|
-
|
|
648
|
+
error_response = "Error: User ID not specified."
|
|
649
|
+
if return_metrics:
|
|
650
|
+
return ChatResponse(
|
|
651
|
+
text=error_response,
|
|
652
|
+
confidence=1.0,
|
|
653
|
+
source="tool",
|
|
654
|
+
latency=(time.time() - start_time) * 1000,
|
|
655
|
+
timestamp=datetime.now(),
|
|
656
|
+
kb_results_count=0,
|
|
657
|
+
metadata={"error": True}
|
|
658
|
+
)
|
|
659
|
+
return error_response
|
|
304
660
|
|
|
305
661
|
user_id = self.current_user
|
|
662
|
+
|
|
663
|
+
# Initialize tracking variables
|
|
664
|
+
kb_results_count = 0
|
|
665
|
+
used_kb = False
|
|
666
|
+
used_memory = False
|
|
667
|
+
response_source = "model" # Default source
|
|
668
|
+
|
|
669
|
+
# Security check (v1.1.0+) - opt-in
|
|
670
|
+
security_info = {}
|
|
671
|
+
if self.enable_security and self.security_detector and self.security_sanitizer:
|
|
672
|
+
# Detect injection attempts
|
|
673
|
+
risk_level = self.security_detector.get_risk_level(message)
|
|
674
|
+
is_suspicious, patterns = self.security_detector.detect(message)
|
|
675
|
+
|
|
676
|
+
if risk_level in ["high", "critical"]:
|
|
677
|
+
self.logger.warning(f"🚨 Blocked {risk_level} risk input from {user_id}: {len(patterns)} patterns detected")
|
|
678
|
+
return f"⚠️ Your message was blocked due to security concerns. Please rephrase your request."
|
|
679
|
+
|
|
680
|
+
if is_suspicious:
|
|
681
|
+
self.logger.info(f"⚠️ Suspicious input from {user_id} (risk: {risk_level}): {len(patterns)} patterns")
|
|
682
|
+
|
|
683
|
+
# Sanitize input
|
|
684
|
+
original_message = message
|
|
685
|
+
message = self.security_sanitizer.sanitize(message, aggressive=(risk_level == "medium"))
|
|
686
|
+
|
|
687
|
+
if message != original_message:
|
|
688
|
+
self.logger.debug(f"Input sanitized for {user_id}")
|
|
689
|
+
|
|
690
|
+
security_info = {
|
|
691
|
+
"risk_level": risk_level,
|
|
692
|
+
"sanitized": message != original_message,
|
|
693
|
+
"patterns_detected": len(patterns)
|
|
694
|
+
}
|
|
306
695
|
|
|
307
696
|
# Check tool commands first
|
|
308
697
|
tool_result = self.tool_executor.execute_user_command(message, user_id)
|
|
309
698
|
if tool_result:
|
|
699
|
+
latency = (time.time() - start_time) * 1000
|
|
700
|
+
if return_metrics:
|
|
701
|
+
return ChatResponse(
|
|
702
|
+
text=tool_result,
|
|
703
|
+
confidence=0.95, # Tools are deterministic
|
|
704
|
+
source="tool",
|
|
705
|
+
latency=latency,
|
|
706
|
+
timestamp=datetime.now(),
|
|
707
|
+
kb_results_count=0,
|
|
708
|
+
metadata={"tool_command": True}
|
|
709
|
+
)
|
|
310
710
|
return tool_result
|
|
311
711
|
|
|
312
712
|
# Knowledge base search (if using SQL)
|
|
313
713
|
kb_context = ""
|
|
314
|
-
if ADVANCED_AVAILABLE and isinstance(self.memory, SQLMemoryManager)
|
|
315
|
-
if
|
|
714
|
+
if ADVANCED_AVAILABLE and isinstance(self.memory, SQLMemoryManager):
|
|
715
|
+
# Check config only if it exists, otherwise always use KB
|
|
716
|
+
use_kb = True
|
|
717
|
+
kb_limit = 5
|
|
718
|
+
|
|
719
|
+
if hasattr(self, 'config') and self.config:
|
|
720
|
+
use_kb = self.config.get("response.use_knowledge_base", True)
|
|
721
|
+
kb_limit = self.config.get("knowledge_base.search_limit", 5)
|
|
722
|
+
|
|
723
|
+
if use_kb:
|
|
316
724
|
try:
|
|
317
|
-
kb_results = self.memory.search_knowledge(
|
|
318
|
-
query=message,
|
|
319
|
-
limit=self.config.get("knowledge_base.search_limit", 5)
|
|
320
|
-
)
|
|
725
|
+
kb_results = self.memory.search_knowledge(query=message, limit=kb_limit)
|
|
321
726
|
|
|
322
727
|
if kb_results:
|
|
323
|
-
|
|
728
|
+
kb_results_count = len(kb_results)
|
|
729
|
+
used_kb = True
|
|
730
|
+
kb_context = "\n\n📚 RELEVANT KNOWLEDGE BASE:\n"
|
|
324
731
|
for i, result in enumerate(kb_results, 1):
|
|
325
|
-
kb_context += f"{i}.
|
|
732
|
+
kb_context += f"{i}. Q: {result['question']}\n A: {result['answer']}\n"
|
|
733
|
+
kb_context += "\n⚠️ USE THIS INFORMATION TO ANSWER! Be brief but accurate.\n"
|
|
326
734
|
except Exception as e:
|
|
327
735
|
self.logger.error(f"Knowledge base search error: {e}")
|
|
328
736
|
|
|
@@ -337,6 +745,9 @@ class MemAgent:
|
|
|
337
745
|
recent_limit = self.config.get("response.recent_conversations_limit", 5) if hasattr(self, 'config') and self.config else 5
|
|
338
746
|
recent_convs = self.memory.get_recent_conversations(user_id, recent_limit)
|
|
339
747
|
|
|
748
|
+
if recent_convs:
|
|
749
|
+
used_memory = True
|
|
750
|
+
|
|
340
751
|
# Add conversations in chronological order (oldest first)
|
|
341
752
|
for conv in recent_convs:
|
|
342
753
|
messages.append({"role": "user", "content": conv.get('user_message', '')})
|
|
@@ -344,27 +755,86 @@ class MemAgent:
|
|
|
344
755
|
except Exception as e:
|
|
345
756
|
self.logger.error(f"Memory history loading error: {e}")
|
|
346
757
|
|
|
347
|
-
# Add knowledge base context
|
|
758
|
+
# Add current message WITH knowledge base context (if available)
|
|
759
|
+
final_message = message
|
|
348
760
|
if kb_context:
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
# Add current message
|
|
355
|
-
messages.append({"role": "user", "content": message})
|
|
761
|
+
# Inject KB directly into user message for maximum visibility
|
|
762
|
+
final_message = f"{kb_context}\n\nUser Question: {message}"
|
|
763
|
+
|
|
764
|
+
messages.append({"role": "user", "content": final_message})
|
|
356
765
|
|
|
357
766
|
# Get response from LLM
|
|
767
|
+
temperature = self.config.get("llm.temperature", 0.2) if hasattr(self, 'config') and self.config else 0.2
|
|
358
768
|
try:
|
|
359
769
|
response = self.llm.chat(
|
|
360
770
|
messages=messages,
|
|
361
|
-
temperature=
|
|
362
|
-
max_tokens=self.config.get("llm.max_tokens",
|
|
771
|
+
temperature=temperature,
|
|
772
|
+
max_tokens=self.config.get("llm.max_tokens", 2000) if hasattr(self, 'config') and self.config else 2000 # Enough tokens for thinking models
|
|
363
773
|
)
|
|
774
|
+
|
|
775
|
+
# Fallback: If response is empty (can happen with thinking models)
|
|
776
|
+
if not response or response.strip() == "":
|
|
777
|
+
self.logger.warning(f"Empty response from model {self.llm.model}, retrying with simpler prompt...")
|
|
778
|
+
|
|
779
|
+
# Retry with just the current message, no history
|
|
780
|
+
simple_messages = [
|
|
781
|
+
{"role": "system", "content": "You are a helpful assistant. Respond directly and concisely."},
|
|
782
|
+
{"role": "user", "content": message}
|
|
783
|
+
]
|
|
784
|
+
response = self.llm.chat(simple_messages, temperature=0.7, max_tokens=2000)
|
|
785
|
+
|
|
786
|
+
# If still empty, provide fallback
|
|
787
|
+
if not response or response.strip() == "":
|
|
788
|
+
response = "I'm having trouble responding right now. Could you rephrase your question?"
|
|
789
|
+
self.logger.error(f"Model {self.llm.model} returned empty response even after retry")
|
|
790
|
+
|
|
364
791
|
except Exception as e:
|
|
365
792
|
self.logger.error(f"LLM response error: {e}")
|
|
366
793
|
response = "Sorry, I cannot respond right now. Please try again later."
|
|
367
|
-
|
|
794
|
+
|
|
795
|
+
# Execute tool calls if tools are enabled (v2.0+)
|
|
796
|
+
if self.enable_tools and self.tool_registry and response:
|
|
797
|
+
try:
|
|
798
|
+
response = self._execute_tool_calls(response)
|
|
799
|
+
except Exception as e:
|
|
800
|
+
self.logger.error(f"Tool execution error: {e}")
|
|
801
|
+
# Continue with original response
|
|
802
|
+
|
|
803
|
+
# Calculate latency
|
|
804
|
+
latency = (time.time() - start_time) * 1000
|
|
805
|
+
|
|
806
|
+
# Determine response source
|
|
807
|
+
if used_kb and used_memory:
|
|
808
|
+
response_source = "hybrid"
|
|
809
|
+
elif used_kb:
|
|
810
|
+
response_source = "knowledge_base"
|
|
811
|
+
else:
|
|
812
|
+
response_source = "model"
|
|
813
|
+
|
|
814
|
+
# Calculate confidence score
|
|
815
|
+
confidence = calculate_confidence(
|
|
816
|
+
kb_results_count=kb_results_count,
|
|
817
|
+
temperature=temperature,
|
|
818
|
+
used_memory=used_memory,
|
|
819
|
+
response_length=len(response)
|
|
820
|
+
)
|
|
821
|
+
|
|
822
|
+
# Build enriched metadata with response metrics
|
|
823
|
+
enriched_metadata = {}
|
|
824
|
+
if metadata:
|
|
825
|
+
enriched_metadata.update(metadata)
|
|
826
|
+
enriched_metadata.update({
|
|
827
|
+
"confidence": round(confidence, 3),
|
|
828
|
+
"source": response_source,
|
|
829
|
+
"latency_ms": round(latency, 1),
|
|
830
|
+
"kb_results_count": kb_results_count,
|
|
831
|
+
"used_memory": used_memory,
|
|
832
|
+
"used_kb": used_kb,
|
|
833
|
+
"response_length": len(response),
|
|
834
|
+
"model": self.model,
|
|
835
|
+
"temperature": temperature
|
|
836
|
+
})
|
|
837
|
+
|
|
368
838
|
# Save interaction
|
|
369
839
|
try:
|
|
370
840
|
if hasattr(self.memory, 'add_interaction'):
|
|
@@ -372,13 +842,532 @@ class MemAgent:
|
|
|
372
842
|
user_id=user_id,
|
|
373
843
|
user_message=message,
|
|
374
844
|
bot_response=response,
|
|
375
|
-
metadata=
|
|
845
|
+
metadata=enriched_metadata
|
|
376
846
|
)
|
|
847
|
+
|
|
848
|
+
# Extract and save user info to profile
|
|
849
|
+
self._update_user_profile(user_id, message, response)
|
|
850
|
+
|
|
851
|
+
# Always update summary after each conversation (JSON mode)
|
|
852
|
+
if not self.use_sql and hasattr(self.memory, 'conversations'):
|
|
853
|
+
self._update_conversation_summary(user_id)
|
|
854
|
+
# Save summary update
|
|
855
|
+
if user_id in self.memory.user_profiles:
|
|
856
|
+
self.memory.save_memory(user_id)
|
|
377
857
|
except Exception as e:
|
|
378
858
|
self.logger.error(f"Interaction saving error: {e}")
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
859
|
+
|
|
860
|
+
# Create response metrics object
|
|
861
|
+
chat_response = ChatResponse(
|
|
862
|
+
text=response,
|
|
863
|
+
confidence=confidence,
|
|
864
|
+
source=response_source,
|
|
865
|
+
latency=latency,
|
|
866
|
+
timestamp=datetime.now(),
|
|
867
|
+
kb_results_count=kb_results_count,
|
|
868
|
+
metadata={
|
|
869
|
+
"model": self.model,
|
|
870
|
+
"temperature": temperature,
|
|
871
|
+
"used_memory": used_memory,
|
|
872
|
+
"used_kb": used_kb,
|
|
873
|
+
"user_id": user_id
|
|
874
|
+
}
|
|
875
|
+
)
|
|
876
|
+
|
|
877
|
+
# Track metrics if enabled
|
|
878
|
+
if self.track_metrics:
|
|
879
|
+
self.metrics_analyzer.add_metric(chat_response)
|
|
880
|
+
|
|
881
|
+
# Return based on user preference
|
|
882
|
+
if return_metrics:
|
|
883
|
+
return chat_response
|
|
884
|
+
else:
|
|
885
|
+
return response
|
|
886
|
+
|
|
887
|
+
def chat_stream(self, message: str, user_id: Optional[str] = None, metadata: Optional[Dict] = None) -> Iterator[str]:
|
|
888
|
+
"""
|
|
889
|
+
Chat with user using streaming response (real-time)
|
|
890
|
+
|
|
891
|
+
This method streams the response as it's generated, providing a better UX
|
|
892
|
+
for longer responses (like ChatGPT's typing effect).
|
|
893
|
+
|
|
894
|
+
Args:
|
|
895
|
+
message: User's message
|
|
896
|
+
user_id: User ID (optional)
|
|
897
|
+
metadata: Additional information
|
|
898
|
+
|
|
899
|
+
Yields:
|
|
900
|
+
Response text chunks as they arrive from the LLM
|
|
901
|
+
|
|
902
|
+
Example:
|
|
903
|
+
>>> agent = MemAgent()
|
|
904
|
+
>>> agent.set_user("alice")
|
|
905
|
+
>>> for chunk in agent.chat_stream("Python nedir?"):
|
|
906
|
+
... print(chunk, end='', flush=True)
|
|
907
|
+
Python bir programlama dilidir...
|
|
908
|
+
"""
|
|
909
|
+
# Start timing
|
|
910
|
+
start_time = time.time()
|
|
911
|
+
|
|
912
|
+
# Determine user
|
|
913
|
+
if user_id:
|
|
914
|
+
self.set_user(user_id)
|
|
915
|
+
elif not self.current_user:
|
|
916
|
+
yield "Error: User ID not specified."
|
|
917
|
+
return
|
|
918
|
+
|
|
919
|
+
user_id = self.current_user
|
|
920
|
+
|
|
921
|
+
# Initialize tracking variables
|
|
922
|
+
kb_results_count = 0
|
|
923
|
+
used_kb = False
|
|
924
|
+
used_memory = False
|
|
925
|
+
|
|
926
|
+
# Security check (v1.1.0+) - opt-in
|
|
927
|
+
if self.enable_security and self.security_detector and self.security_sanitizer:
|
|
928
|
+
risk_level = self.security_detector.get_risk_level(message)
|
|
929
|
+
is_suspicious, patterns = self.security_detector.detect(message)
|
|
930
|
+
|
|
931
|
+
if risk_level in ["high", "critical"]:
|
|
932
|
+
self.logger.warning(f"🚨 Blocked {risk_level} risk input from {user_id}")
|
|
933
|
+
yield f"⚠️ Your message was blocked due to security concerns. Please rephrase your request."
|
|
934
|
+
return
|
|
935
|
+
|
|
936
|
+
# Sanitize input
|
|
937
|
+
message = self.security_sanitizer.sanitize(message, aggressive=(risk_level == "medium"))
|
|
938
|
+
|
|
939
|
+
# Check tool commands first
|
|
940
|
+
tool_result = self.tool_executor.execute_user_command(message, user_id)
|
|
941
|
+
if tool_result:
|
|
942
|
+
yield tool_result
|
|
943
|
+
return
|
|
944
|
+
|
|
945
|
+
# Knowledge base search (if using SQL)
|
|
946
|
+
kb_context = ""
|
|
947
|
+
if ADVANCED_AVAILABLE and isinstance(self.memory, SQLMemoryManager):
|
|
948
|
+
use_kb = True
|
|
949
|
+
kb_limit = 5
|
|
950
|
+
|
|
951
|
+
if hasattr(self, 'config') and self.config:
|
|
952
|
+
use_kb = self.config.get("response.use_knowledge_base", True)
|
|
953
|
+
kb_limit = self.config.get("knowledge_base.search_limit", 5)
|
|
954
|
+
|
|
955
|
+
if use_kb:
|
|
956
|
+
try:
|
|
957
|
+
kb_results = self.memory.search_knowledge(query=message, limit=kb_limit)
|
|
958
|
+
|
|
959
|
+
if kb_results:
|
|
960
|
+
kb_results_count = len(kb_results)
|
|
961
|
+
used_kb = True
|
|
962
|
+
kb_context = "\n\n📚 RELEVANT KNOWLEDGE BASE:\n"
|
|
963
|
+
for i, result in enumerate(kb_results, 1):
|
|
964
|
+
kb_context += f"{i}. Q: {result['question']}\n A: {result['answer']}\n"
|
|
965
|
+
kb_context += "\n⚠️ USE THIS INFORMATION TO ANSWER! Be brief but accurate.\n"
|
|
966
|
+
except Exception as e:
|
|
967
|
+
self.logger.error(f"Knowledge base search error: {e}")
|
|
968
|
+
|
|
969
|
+
# Get conversation history
|
|
970
|
+
messages = []
|
|
971
|
+
if self.current_system_prompt:
|
|
972
|
+
messages.append({"role": "system", "content": self.current_system_prompt})
|
|
973
|
+
|
|
974
|
+
# Add memory history
|
|
975
|
+
try:
|
|
976
|
+
if hasattr(self.memory, 'get_recent_conversations'):
|
|
977
|
+
recent_limit = self.config.get("response.recent_conversations_limit", 5) if hasattr(self, 'config') and self.config else 5
|
|
978
|
+
recent_convs = self.memory.get_recent_conversations(user_id, recent_limit)
|
|
979
|
+
|
|
980
|
+
if recent_convs:
|
|
981
|
+
used_memory = True
|
|
982
|
+
|
|
983
|
+
# Add conversations in chronological order
|
|
984
|
+
for conv in recent_convs:
|
|
985
|
+
messages.append({"role": "user", "content": conv.get('user_message', '')})
|
|
986
|
+
messages.append({"role": "assistant", "content": conv.get('bot_response', '')})
|
|
987
|
+
except Exception as e:
|
|
988
|
+
self.logger.error(f"Memory history loading error: {e}")
|
|
989
|
+
|
|
990
|
+
# Add current message WITH knowledge base context (if available)
|
|
991
|
+
final_message = message
|
|
992
|
+
if kb_context:
|
|
993
|
+
final_message = f"{kb_context}\n\nUser Question: {message}"
|
|
994
|
+
|
|
995
|
+
messages.append({"role": "user", "content": final_message})
|
|
996
|
+
|
|
997
|
+
# Get streaming response from LLM
|
|
998
|
+
temperature = self.config.get("llm.temperature", 0.2) if hasattr(self, 'config') and self.config else 0.2
|
|
999
|
+
max_tokens = self.config.get("llm.max_tokens", 2000) if hasattr(self, 'config') and self.config else 2000
|
|
1000
|
+
|
|
1001
|
+
# Collect full response for saving
|
|
1002
|
+
full_response = ""
|
|
1003
|
+
|
|
1004
|
+
try:
|
|
1005
|
+
# Stream chunks from LLM
|
|
1006
|
+
for chunk in self.llm.chat_stream(
|
|
1007
|
+
messages=messages,
|
|
1008
|
+
temperature=temperature,
|
|
1009
|
+
max_tokens=max_tokens
|
|
1010
|
+
):
|
|
1011
|
+
full_response += chunk
|
|
1012
|
+
yield chunk
|
|
1013
|
+
|
|
1014
|
+
except Exception as e:
|
|
1015
|
+
error_msg = f"Streaming error: {str(e)}"
|
|
1016
|
+
self.logger.error(error_msg)
|
|
1017
|
+
yield f"\n\n⚠️ {error_msg}"
|
|
1018
|
+
return
|
|
1019
|
+
|
|
1020
|
+
# Calculate latency
|
|
1021
|
+
latency = (time.time() - start_time) * 1000
|
|
1022
|
+
|
|
1023
|
+
# Determine response source
|
|
1024
|
+
response_source = "model"
|
|
1025
|
+
if used_memory and used_kb:
|
|
1026
|
+
response_source = "hybrid"
|
|
1027
|
+
elif used_kb:
|
|
1028
|
+
response_source = "knowledge_base"
|
|
1029
|
+
|
|
1030
|
+
# Calculate confidence
|
|
1031
|
+
confidence = calculate_confidence(
|
|
1032
|
+
kb_results_count=kb_results_count,
|
|
1033
|
+
temperature=temperature,
|
|
1034
|
+
used_memory=used_memory,
|
|
1035
|
+
response_length=len(full_response)
|
|
1036
|
+
)
|
|
1037
|
+
|
|
1038
|
+
# Build enriched metadata
|
|
1039
|
+
enriched_metadata = {}
|
|
1040
|
+
if metadata:
|
|
1041
|
+
enriched_metadata.update(metadata)
|
|
1042
|
+
enriched_metadata.update({
|
|
1043
|
+
"confidence": round(confidence, 3),
|
|
1044
|
+
"source": response_source,
|
|
1045
|
+
"latency_ms": round(latency, 1),
|
|
1046
|
+
"kb_results_count": kb_results_count,
|
|
1047
|
+
"used_memory": used_memory,
|
|
1048
|
+
"used_kb": used_kb,
|
|
1049
|
+
"response_length": len(full_response),
|
|
1050
|
+
"model": self.model,
|
|
1051
|
+
"temperature": temperature,
|
|
1052
|
+
"streaming": True
|
|
1053
|
+
})
|
|
1054
|
+
|
|
1055
|
+
# Save interaction
|
|
1056
|
+
try:
|
|
1057
|
+
if hasattr(self.memory, 'add_interaction'):
|
|
1058
|
+
self.memory.add_interaction(
|
|
1059
|
+
user_id=user_id,
|
|
1060
|
+
user_message=message,
|
|
1061
|
+
bot_response=full_response,
|
|
1062
|
+
metadata=enriched_metadata
|
|
1063
|
+
)
|
|
1064
|
+
|
|
1065
|
+
# Extract and save user info to profile
|
|
1066
|
+
self._update_user_profile(user_id, message, full_response)
|
|
1067
|
+
|
|
1068
|
+
# Update summary (JSON mode)
|
|
1069
|
+
if not self.use_sql and hasattr(self.memory, 'conversations'):
|
|
1070
|
+
self._update_conversation_summary(user_id)
|
|
1071
|
+
if user_id in self.memory.user_profiles:
|
|
1072
|
+
self.memory.save_memory(user_id)
|
|
1073
|
+
except Exception as e:
|
|
1074
|
+
self.logger.error(f"Interaction saving error: {e}")
|
|
1075
|
+
|
|
1076
|
+
# Track metrics if enabled
|
|
1077
|
+
if self.track_metrics:
|
|
1078
|
+
chat_response = ChatResponse(
|
|
1079
|
+
text=full_response,
|
|
1080
|
+
confidence=confidence,
|
|
1081
|
+
source=response_source,
|
|
1082
|
+
latency=latency,
|
|
1083
|
+
timestamp=datetime.now(),
|
|
1084
|
+
kb_results_count=kb_results_count,
|
|
1085
|
+
metadata={
|
|
1086
|
+
"model": self.model,
|
|
1087
|
+
"temperature": temperature,
|
|
1088
|
+
"used_memory": used_memory,
|
|
1089
|
+
"used_kb": used_kb,
|
|
1090
|
+
"user_id": user_id,
|
|
1091
|
+
"streaming": True
|
|
1092
|
+
}
|
|
1093
|
+
)
|
|
1094
|
+
self.metrics_analyzer.add_metric(chat_response)
|
|
1095
|
+
|
|
1096
|
+
def _update_user_profile(self, user_id: str, message: str, response: str):
|
|
1097
|
+
"""Extract user info from conversation and update profile"""
|
|
1098
|
+
msg_lower = message.lower()
|
|
1099
|
+
|
|
1100
|
+
# Extract information
|
|
1101
|
+
extracted = {}
|
|
1102
|
+
|
|
1103
|
+
# Extract name
|
|
1104
|
+
if "my name is" in msg_lower or "i am" in msg_lower or "i'm" in msg_lower or "adım" in msg_lower or "ismim" in msg_lower:
|
|
1105
|
+
for phrase in ["my name is ", "i am ", "i'm ", "adım ", "ismim ", "benim adım "]:
|
|
1106
|
+
if phrase in msg_lower:
|
|
1107
|
+
name_part = message[msg_lower.index(phrase) + len(phrase):].strip()
|
|
1108
|
+
name = name_part.split()[0] if name_part else None
|
|
1109
|
+
if name and len(name) > 1:
|
|
1110
|
+
extracted['name'] = name.strip('.,!?')
|
|
1111
|
+
break
|
|
1112
|
+
|
|
1113
|
+
# Extract favorite food
|
|
1114
|
+
if "favorite food" in msg_lower or "favourite food" in msg_lower or "sevdiğim yemek" in msg_lower or "en sevdiğim" in msg_lower:
|
|
1115
|
+
if "is" in msg_lower or ":" in msg_lower:
|
|
1116
|
+
food = msg_lower.split("is")[-1].strip() if "is" in msg_lower else msg_lower.split(":")[-1].strip()
|
|
1117
|
+
food = food.strip('.,!?')
|
|
1118
|
+
if food and len(food) < 50:
|
|
1119
|
+
extracted['favorite_food'] = food
|
|
1120
|
+
|
|
1121
|
+
# Extract location
|
|
1122
|
+
if "i live in" in msg_lower or "i'm from" in msg_lower or "yaşıyorum" in msg_lower or "yaşadığım" in msg_lower:
|
|
1123
|
+
for phrase in ["i live in ", "i'm from ", "from ", "yaşıyorum", "yaşadığım yer", "yaşadığım şehir"]:
|
|
1124
|
+
if phrase in msg_lower:
|
|
1125
|
+
loc = message[msg_lower.index(phrase) + len(phrase):].strip()
|
|
1126
|
+
location = loc.split()[0] if loc else None
|
|
1127
|
+
if location and len(location) > 2:
|
|
1128
|
+
extracted['location'] = location.strip('.,!?')
|
|
1129
|
+
break
|
|
1130
|
+
|
|
1131
|
+
# Save updates
|
|
1132
|
+
if extracted:
|
|
1133
|
+
try:
|
|
1134
|
+
# SQL memory - store in preferences JSON
|
|
1135
|
+
if hasattr(self.memory, 'update_user_profile'):
|
|
1136
|
+
# Get current profile
|
|
1137
|
+
profile = self.memory.get_user_profile(user_id) or {}
|
|
1138
|
+
|
|
1139
|
+
# Update name directly if extracted
|
|
1140
|
+
updates = {}
|
|
1141
|
+
if 'name' in extracted:
|
|
1142
|
+
updates['name'] = extracted.pop('name')
|
|
1143
|
+
|
|
1144
|
+
# Store other info in preferences
|
|
1145
|
+
if extracted:
|
|
1146
|
+
current_prefs = profile.get('preferences')
|
|
1147
|
+
if current_prefs:
|
|
1148
|
+
try:
|
|
1149
|
+
prefs = json.loads(current_prefs) if isinstance(current_prefs, str) else current_prefs
|
|
1150
|
+
except:
|
|
1151
|
+
prefs = {}
|
|
1152
|
+
else:
|
|
1153
|
+
prefs = {}
|
|
1154
|
+
|
|
1155
|
+
prefs.update(extracted)
|
|
1156
|
+
updates['preferences'] = json.dumps(prefs)
|
|
1157
|
+
|
|
1158
|
+
if updates:
|
|
1159
|
+
self.memory.update_user_profile(user_id, updates)
|
|
1160
|
+
self.logger.debug(f"Profile updated for {user_id}: {extracted}")
|
|
1161
|
+
|
|
1162
|
+
# JSON memory - direct update
|
|
1163
|
+
elif hasattr(self.memory, 'update_profile'):
|
|
1164
|
+
# Load memory if not already loaded
|
|
1165
|
+
if user_id not in self.memory.user_profiles:
|
|
1166
|
+
self.memory.load_memory(user_id)
|
|
1167
|
+
|
|
1168
|
+
# For JSON memory, merge into preferences
|
|
1169
|
+
current_profile = self.memory.user_profiles.get(user_id, {})
|
|
1170
|
+
current_prefs = current_profile.get('preferences', {})
|
|
1171
|
+
|
|
1172
|
+
# Handle case where preferences might be a JSON string
|
|
1173
|
+
if isinstance(current_prefs, str):
|
|
1174
|
+
try:
|
|
1175
|
+
current_prefs = json.loads(current_prefs)
|
|
1176
|
+
except:
|
|
1177
|
+
current_prefs = {}
|
|
1178
|
+
|
|
1179
|
+
# Update preferences
|
|
1180
|
+
if extracted:
|
|
1181
|
+
current_prefs.update(extracted)
|
|
1182
|
+
self.memory.user_profiles[user_id]['preferences'] = current_prefs
|
|
1183
|
+
|
|
1184
|
+
# Update name if extracted
|
|
1185
|
+
if 'name' in extracted:
|
|
1186
|
+
self.memory.user_profiles[user_id]['name'] = extracted['name']
|
|
1187
|
+
|
|
1188
|
+
# Auto-generate summary from conversation history
|
|
1189
|
+
self._update_conversation_summary(user_id)
|
|
1190
|
+
|
|
1191
|
+
# Save to disk
|
|
1192
|
+
self.memory.save_memory(user_id)
|
|
1193
|
+
self.logger.debug(f"Profile updated for {user_id}: {extracted}")
|
|
1194
|
+
except Exception as e:
|
|
1195
|
+
self.logger.error(f"Error updating profile: {e}")
|
|
1196
|
+
|
|
1197
|
+
def _update_conversation_summary(self, user_id: str) -> None:
|
|
1198
|
+
"""
|
|
1199
|
+
Auto-generate conversation summary for user profile
|
|
1200
|
+
|
|
1201
|
+
Args:
|
|
1202
|
+
user_id: User ID
|
|
1203
|
+
"""
|
|
1204
|
+
try:
|
|
1205
|
+
if not hasattr(self.memory, 'conversations'):
|
|
1206
|
+
return
|
|
1207
|
+
|
|
1208
|
+
# Ensure memory is loaded
|
|
1209
|
+
if user_id not in self.memory.conversations:
|
|
1210
|
+
self.memory.load_memory(user_id)
|
|
1211
|
+
|
|
1212
|
+
conversations = self.memory.conversations.get(user_id, [])
|
|
1213
|
+
if not conversations:
|
|
1214
|
+
return
|
|
1215
|
+
|
|
1216
|
+
# Get recent conversations for summary
|
|
1217
|
+
recent_convs = conversations[-10:] # Last 10 conversations
|
|
1218
|
+
|
|
1219
|
+
# Extract topics/interests
|
|
1220
|
+
all_messages = " ".join([c.get('user_message', '') for c in recent_convs])
|
|
1221
|
+
topics = self._extract_topics(all_messages)
|
|
1222
|
+
|
|
1223
|
+
# Calculate engagement stats
|
|
1224
|
+
total_interactions = len(conversations)
|
|
1225
|
+
avg_response_length = sum(len(c.get('bot_response', '')) for c in recent_convs) / len(recent_convs) if recent_convs else 0
|
|
1226
|
+
|
|
1227
|
+
# Build summary
|
|
1228
|
+
summary = {
|
|
1229
|
+
"total_interactions": total_interactions,
|
|
1230
|
+
"topics_of_interest": topics[:5] if topics else [], # Top 5 topics
|
|
1231
|
+
"avg_response_length": round(avg_response_length, 0),
|
|
1232
|
+
"last_active": recent_convs[-1].get('timestamp') if recent_convs else None,
|
|
1233
|
+
"engagement_level": "high" if total_interactions > 20 else ("medium" if total_interactions > 5 else "low")
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
# Update profile summary (JSON mode)
|
|
1237
|
+
if user_id in self.memory.user_profiles:
|
|
1238
|
+
self.memory.user_profiles[user_id]['summary'] = summary
|
|
1239
|
+
|
|
1240
|
+
except Exception as e:
|
|
1241
|
+
self.logger.debug(f"Summary generation skipped: {e}")
|
|
1242
|
+
|
|
1243
|
+
def _extract_topics(self, text: str) -> List[str]:
|
|
1244
|
+
"""
|
|
1245
|
+
Extract key topics/interests from conversation text
|
|
1246
|
+
|
|
1247
|
+
Args:
|
|
1248
|
+
text: Combined conversation text
|
|
1249
|
+
|
|
1250
|
+
Returns:
|
|
1251
|
+
List of extracted topics
|
|
1252
|
+
"""
|
|
1253
|
+
# Simple keyword extraction (can be enhanced with NLP)
|
|
1254
|
+
keywords_map = {
|
|
1255
|
+
"python": "Python Programming",
|
|
1256
|
+
"javascript": "JavaScript",
|
|
1257
|
+
"coding": "Programming",
|
|
1258
|
+
"weather": "Weather",
|
|
1259
|
+
"food": "Food & Dining",
|
|
1260
|
+
"music": "Music",
|
|
1261
|
+
"sport": "Sports",
|
|
1262
|
+
"travel": "Travel",
|
|
1263
|
+
"work": "Work",
|
|
1264
|
+
"help": "Support",
|
|
1265
|
+
"problem": "Problem Solving",
|
|
1266
|
+
"question": "Questions",
|
|
1267
|
+
"chat": "Chatting"
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
text_lower = text.lower()
|
|
1271
|
+
found_topics = []
|
|
1272
|
+
|
|
1273
|
+
for keyword, topic in keywords_map.items():
|
|
1274
|
+
if keyword in text_lower:
|
|
1275
|
+
found_topics.append(topic)
|
|
1276
|
+
|
|
1277
|
+
# Remove duplicates while preserving order
|
|
1278
|
+
seen = set()
|
|
1279
|
+
unique_topics = []
|
|
1280
|
+
for topic in found_topics:
|
|
1281
|
+
if topic not in seen:
|
|
1282
|
+
seen.add(topic)
|
|
1283
|
+
unique_topics.append(topic)
|
|
1284
|
+
|
|
1285
|
+
return unique_topics
|
|
1286
|
+
|
|
1287
|
+
def get_user_profile(self, user_id: Optional[str] = None) -> Dict:
|
|
1288
|
+
"""
|
|
1289
|
+
Get user's profile info
|
|
1290
|
+
|
|
1291
|
+
Args:
|
|
1292
|
+
user_id: User ID (uses current_user if not specified)
|
|
1293
|
+
|
|
1294
|
+
Returns:
|
|
1295
|
+
User profile dictionary with all info (name, favorite_food, location, etc.)
|
|
1296
|
+
"""
|
|
1297
|
+
uid = user_id or self.current_user
|
|
1298
|
+
if not uid:
|
|
1299
|
+
return {}
|
|
1300
|
+
|
|
1301
|
+
try:
|
|
1302
|
+
# Check if SQL or JSON memory - SQL has SQLMemoryManager type
|
|
1303
|
+
if ADVANCED_AVAILABLE and isinstance(self.memory, SQLMemoryManager):
|
|
1304
|
+
# SQL memory - merge preferences into main dict
|
|
1305
|
+
profile = self.memory.get_user_profile(uid)
|
|
1306
|
+
if not profile:
|
|
1307
|
+
return {}
|
|
1308
|
+
|
|
1309
|
+
# Parse preferences JSON if exists
|
|
1310
|
+
result = {
|
|
1311
|
+
'user_id': profile.get('user_id'),
|
|
1312
|
+
'name': profile.get('name'),
|
|
1313
|
+
'first_seen': profile.get('first_seen'),
|
|
1314
|
+
'last_interaction': profile.get('last_interaction'),
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
# Merge preferences
|
|
1318
|
+
prefs_str = profile.get('preferences')
|
|
1319
|
+
if prefs_str:
|
|
1320
|
+
try:
|
|
1321
|
+
prefs = json.loads(prefs_str) if isinstance(prefs_str, str) else prefs_str
|
|
1322
|
+
result.update(prefs) # Add favorite_food, location, etc.
|
|
1323
|
+
except:
|
|
1324
|
+
pass
|
|
1325
|
+
|
|
1326
|
+
return result
|
|
1327
|
+
else:
|
|
1328
|
+
# JSON memory - reload from disk to get latest data
|
|
1329
|
+
memory_data = self.memory.load_memory(uid)
|
|
1330
|
+
profile = memory_data.get('profile', {}).copy() # Make a copy to avoid modifying cached data
|
|
1331
|
+
|
|
1332
|
+
# Parse preferences if it's a JSON string
|
|
1333
|
+
if isinstance(profile.get('preferences'), str):
|
|
1334
|
+
try:
|
|
1335
|
+
profile['preferences'] = json.loads(profile['preferences'])
|
|
1336
|
+
except:
|
|
1337
|
+
profile['preferences'] = {}
|
|
1338
|
+
|
|
1339
|
+
# Return profile as-is (summary should already be there if it was generated)
|
|
1340
|
+
# Only regenerate if truly missing
|
|
1341
|
+
summary_value = profile.get('summary')
|
|
1342
|
+
summary_is_empty = (not summary_value or
|
|
1343
|
+
(isinstance(summary_value, dict) and len(summary_value) == 0))
|
|
1344
|
+
|
|
1345
|
+
if summary_is_empty:
|
|
1346
|
+
# Try to regenerate summary if missing (for old users)
|
|
1347
|
+
# Ensure conversations are loaded
|
|
1348
|
+
if uid not in self.memory.conversations:
|
|
1349
|
+
self.memory.load_memory(uid)
|
|
1350
|
+
|
|
1351
|
+
if uid in self.memory.conversations and len(self.memory.conversations[uid]) > 0:
|
|
1352
|
+
self._update_conversation_summary(uid)
|
|
1353
|
+
# Save the updated summary
|
|
1354
|
+
if uid in self.memory.user_profiles:
|
|
1355
|
+
self.memory.save_memory(uid)
|
|
1356
|
+
# Reload to get updated summary
|
|
1357
|
+
memory_data = self.memory.load_memory(uid)
|
|
1358
|
+
profile = memory_data.get('profile', {}).copy()
|
|
1359
|
+
# Parse preferences again after reload
|
|
1360
|
+
if isinstance(profile.get('preferences'), str):
|
|
1361
|
+
try:
|
|
1362
|
+
profile['preferences'] = json.loads(profile['preferences'])
|
|
1363
|
+
except:
|
|
1364
|
+
profile['preferences'] = {}
|
|
1365
|
+
|
|
1366
|
+
return profile
|
|
1367
|
+
except Exception as e:
|
|
1368
|
+
self.logger.error(f"Error getting user profile: {e}")
|
|
1369
|
+
return {}
|
|
1370
|
+
|
|
382
1371
|
def add_knowledge(self, category: str, question: str, answer: str,
|
|
383
1372
|
keywords: Optional[List[str]] = None, priority: int = 0) -> int:
|
|
384
1373
|
"""Add new record to knowledge base"""
|
|
@@ -505,6 +1494,108 @@ class MemAgent:
|
|
|
505
1494
|
return self.tool_executor.memory_tools.list_available_tools()
|
|
506
1495
|
else:
|
|
507
1496
|
return "Tool system not available."
|
|
1497
|
+
|
|
1498
|
+
# === METRICS & ANALYTICS METHODS (v1.3.1+) ===
|
|
1499
|
+
|
|
1500
|
+
def get_response_metrics(self, last_n: Optional[int] = None) -> Dict[str, Any]:
|
|
1501
|
+
"""
|
|
1502
|
+
Get response quality metrics summary
|
|
1503
|
+
|
|
1504
|
+
Args:
|
|
1505
|
+
last_n: Analyze only last N responses (None = all)
|
|
1506
|
+
|
|
1507
|
+
Returns:
|
|
1508
|
+
Metrics summary dictionary
|
|
1509
|
+
|
|
1510
|
+
Example:
|
|
1511
|
+
>>> agent.get_response_metrics(last_n=10)
|
|
1512
|
+
{
|
|
1513
|
+
'total_responses': 10,
|
|
1514
|
+
'avg_latency_ms': 245.3,
|
|
1515
|
+
'avg_confidence': 0.82,
|
|
1516
|
+
'kb_usage_rate': 0.6,
|
|
1517
|
+
'source_distribution': {'knowledge_base': 6, 'model': 4},
|
|
1518
|
+
'fast_response_rate': 0.9
|
|
1519
|
+
}
|
|
1520
|
+
"""
|
|
1521
|
+
return self.metrics_analyzer.get_summary(last_n)
|
|
1522
|
+
|
|
1523
|
+
def get_latest_response_metric(self) -> Optional[ChatResponse]:
|
|
1524
|
+
"""
|
|
1525
|
+
Get the most recent response metric
|
|
1526
|
+
|
|
1527
|
+
Returns:
|
|
1528
|
+
Latest ChatResponse object or None if no metrics
|
|
1529
|
+
"""
|
|
1530
|
+
if not self.metrics_analyzer.metrics_history:
|
|
1531
|
+
return None
|
|
1532
|
+
return self.metrics_analyzer.metrics_history[-1]
|
|
1533
|
+
|
|
1534
|
+
def get_average_confidence(self, last_n: Optional[int] = None) -> float:
|
|
1535
|
+
"""
|
|
1536
|
+
Get average confidence score
|
|
1537
|
+
|
|
1538
|
+
Args:
|
|
1539
|
+
last_n: Analyze only last N responses (None = all)
|
|
1540
|
+
|
|
1541
|
+
Returns:
|
|
1542
|
+
Average confidence (0.0-1.0)
|
|
1543
|
+
"""
|
|
1544
|
+
return self.metrics_analyzer.get_average_confidence(last_n)
|
|
1545
|
+
|
|
1546
|
+
def get_kb_usage_rate(self, last_n: Optional[int] = None) -> float:
|
|
1547
|
+
"""
|
|
1548
|
+
Get knowledge base usage rate
|
|
1549
|
+
|
|
1550
|
+
Args:
|
|
1551
|
+
last_n: Analyze only last N responses (None = all)
|
|
1552
|
+
|
|
1553
|
+
Returns:
|
|
1554
|
+
KB usage rate (0.0-1.0)
|
|
1555
|
+
"""
|
|
1556
|
+
return self.metrics_analyzer.get_kb_usage_rate(last_n)
|
|
1557
|
+
|
|
1558
|
+
def clear_metrics(self) -> None:
|
|
1559
|
+
"""Clear all metrics history"""
|
|
1560
|
+
self.metrics_analyzer.clear_history()
|
|
1561
|
+
self.logger.info("Metrics history cleared")
|
|
1562
|
+
|
|
1563
|
+
def export_metrics(self, format: str = "json") -> str:
|
|
1564
|
+
"""
|
|
1565
|
+
Export metrics data
|
|
1566
|
+
|
|
1567
|
+
Args:
|
|
1568
|
+
format: Export format ('json' or 'summary')
|
|
1569
|
+
|
|
1570
|
+
Returns:
|
|
1571
|
+
Formatted metrics data
|
|
1572
|
+
"""
|
|
1573
|
+
summary = self.get_response_metrics()
|
|
1574
|
+
|
|
1575
|
+
if format == "json":
|
|
1576
|
+
return json.dumps(summary, ensure_ascii=False, indent=2)
|
|
1577
|
+
elif format == "summary":
|
|
1578
|
+
lines = [
|
|
1579
|
+
"📊 RESPONSE METRICS SUMMARY",
|
|
1580
|
+
"=" * 60,
|
|
1581
|
+
f"Total Responses: {summary['total_responses']}",
|
|
1582
|
+
f"Avg Latency: {summary['avg_latency_ms']:.1f} ms",
|
|
1583
|
+
f"Avg Confidence: {summary['avg_confidence']:.2%}",
|
|
1584
|
+
f"KB Usage Rate: {summary['kb_usage_rate']:.2%}",
|
|
1585
|
+
f"Fast Response Rate: {summary['fast_response_rate']:.2%}",
|
|
1586
|
+
"",
|
|
1587
|
+
"Source Distribution:",
|
|
1588
|
+
]
|
|
1589
|
+
for source, count in summary['source_distribution'].items():
|
|
1590
|
+
lines.append(f" - {source:20s}: {count}")
|
|
1591
|
+
|
|
1592
|
+
lines.extend(["", "Quality Distribution:"])
|
|
1593
|
+
for quality, count in summary.get('quality_distribution', {}).items():
|
|
1594
|
+
lines.append(f" - {quality:20s}: {count}")
|
|
1595
|
+
|
|
1596
|
+
return "\n".join(lines)
|
|
1597
|
+
else:
|
|
1598
|
+
return "Unsupported format. Use 'json' or 'summary'."
|
|
508
1599
|
|
|
509
1600
|
def close(self) -> None:
|
|
510
1601
|
"""Clean up resources"""
|