mem-llm 1.3.0__py3-none-any.whl → 1.3.2__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 +9 -2
- mem_llm/config_manager.py +3 -1
- mem_llm/mem_agent.py +400 -16
- mem_llm/memory_db.py +186 -4
- mem_llm/memory_manager.py +10 -1
- mem_llm/response_metrics.py +221 -0
- mem_llm/vector_store.py +278 -0
- {mem_llm-1.3.0.dist-info → mem_llm-1.3.2.dist-info}/METADATA +109 -34
- {mem_llm-1.3.0.dist-info → mem_llm-1.3.2.dist-info}/RECORD +12 -10
- {mem_llm-1.3.0.dist-info → mem_llm-1.3.2.dist-info}/WHEEL +0 -0
- {mem_llm-1.3.0.dist-info → mem_llm-1.3.2.dist-info}/entry_points.txt +0 -0
- {mem_llm-1.3.0.dist-info → mem_llm-1.3.2.dist-info}/top_level.txt +0 -0
mem_llm/__init__.py
CHANGED
|
@@ -63,7 +63,14 @@ try:
|
|
|
63
63
|
except ImportError:
|
|
64
64
|
__all_export_import__ = []
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
# Response Metrics (v1.3.1+)
|
|
67
|
+
try:
|
|
68
|
+
from .response_metrics import ChatResponse, ResponseMetricsAnalyzer, calculate_confidence
|
|
69
|
+
__all_metrics__ = ["ChatResponse", "ResponseMetricsAnalyzer", "calculate_confidence"]
|
|
70
|
+
except ImportError:
|
|
71
|
+
__all_metrics__ = []
|
|
72
|
+
|
|
73
|
+
__version__ = "1.3.2"
|
|
67
74
|
__author__ = "C. Emre Karataş"
|
|
68
75
|
|
|
69
76
|
# Multi-backend LLM support (v1.3.0+)
|
|
@@ -80,4 +87,4 @@ __all__ = [
|
|
|
80
87
|
"MemAgent",
|
|
81
88
|
"MemoryManager",
|
|
82
89
|
"OllamaClient",
|
|
83
|
-
] + __all_llm_backends__ + __all_tools__ + __all_pro__ + __all_cli__ + __all_security__ + __all_enhanced__ + __all_summarizer__ + __all_export_import__
|
|
90
|
+
] + __all_llm_backends__ + __all_tools__ + __all_pro__ + __all_cli__ + __all_security__ + __all_enhanced__ + __all_summarizer__ + __all_export_import__ + __all_metrics__
|
mem_llm/config_manager.py
CHANGED
|
@@ -62,7 +62,9 @@ class ConfigManager:
|
|
|
62
62
|
"default_kb": "ecommerce",
|
|
63
63
|
"custom_kb_file": None,
|
|
64
64
|
"search_limit": 5,
|
|
65
|
-
"min_relevance_score": 0.3
|
|
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
|
|
66
68
|
},
|
|
67
69
|
"response": {
|
|
68
70
|
"use_knowledge_base": True,
|
mem_llm/mem_agent.py
CHANGED
|
@@ -34,12 +34,14 @@ 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
41
|
from .llm_client import OllamaClient # Backward compatibility
|
|
41
42
|
from .llm_client_factory import LLMClientFactory
|
|
42
43
|
from .base_llm_client import BaseLLMClient
|
|
44
|
+
from .response_metrics import ChatResponse, ResponseMetricsAnalyzer, calculate_confidence
|
|
43
45
|
|
|
44
46
|
# Advanced features (optional)
|
|
45
47
|
try:
|
|
@@ -74,8 +76,10 @@ class MemAgent:
|
|
|
74
76
|
api_key: Optional[str] = None,
|
|
75
77
|
auto_detect_backend: bool = False,
|
|
76
78
|
check_connection: bool = False,
|
|
77
|
-
|
|
78
|
-
|
|
79
|
+
enable_security: bool = False,
|
|
80
|
+
enable_vector_search: bool = False,
|
|
81
|
+
embedding_model: str = "all-MiniLM-L6-v2",
|
|
82
|
+
**llm_kwargs):
|
|
79
83
|
"""
|
|
80
84
|
Args:
|
|
81
85
|
model: LLM model to use
|
|
@@ -91,6 +95,8 @@ class MemAgent:
|
|
|
91
95
|
auto_detect_backend: Auto-detect available LLM backend - NEW in v1.3.0
|
|
92
96
|
check_connection: Verify LLM connection on startup (default: False)
|
|
93
97
|
enable_security: Enable prompt injection protection (v1.1.0+, default: False for backward compatibility)
|
|
98
|
+
enable_vector_search: Enable semantic/vector search for KB (v1.3.2+, requires chromadb) - NEW
|
|
99
|
+
embedding_model: Embedding model for vector search (default: "all-MiniLM-L6-v2") - NEW
|
|
94
100
|
**llm_kwargs: Additional backend-specific parameters
|
|
95
101
|
|
|
96
102
|
Examples:
|
|
@@ -161,6 +167,14 @@ class MemAgent:
|
|
|
161
167
|
else:
|
|
162
168
|
final_db_path = "memories/memories.db"
|
|
163
169
|
|
|
170
|
+
# Get vector search settings from config or parameters
|
|
171
|
+
vector_search_enabled = enable_vector_search
|
|
172
|
+
vector_model = embedding_model
|
|
173
|
+
|
|
174
|
+
if self.config:
|
|
175
|
+
vector_search_enabled = self.config.get("knowledge_base.enable_vector_search", vector_search_enabled)
|
|
176
|
+
vector_model = self.config.get("knowledge_base.embedding_model", vector_model)
|
|
177
|
+
|
|
164
178
|
# Ensure memories directory exists (skip for :memory:)
|
|
165
179
|
import os
|
|
166
180
|
if final_db_path != ":memory:":
|
|
@@ -168,8 +182,14 @@ class MemAgent:
|
|
|
168
182
|
if db_dir and not os.path.exists(db_dir):
|
|
169
183
|
os.makedirs(db_dir, exist_ok=True)
|
|
170
184
|
|
|
171
|
-
self.memory = SQLMemoryManager(
|
|
185
|
+
self.memory = SQLMemoryManager(
|
|
186
|
+
final_db_path,
|
|
187
|
+
enable_vector_search=vector_search_enabled,
|
|
188
|
+
embedding_model=vector_model
|
|
189
|
+
)
|
|
172
190
|
self.logger.info(f"SQL memory system active: {final_db_path}")
|
|
191
|
+
if vector_search_enabled:
|
|
192
|
+
self.logger.info(f"🔍 Vector search enabled (model: {vector_model})")
|
|
173
193
|
else:
|
|
174
194
|
# JSON memory (simple)
|
|
175
195
|
json_dir = memory_dir or self.config.get("memory.json_dir", "memories") if self.config else "memories"
|
|
@@ -297,6 +317,10 @@ class MemAgent:
|
|
|
297
317
|
|
|
298
318
|
# Tool system (always available)
|
|
299
319
|
self.tool_executor = ToolExecutor(self.memory)
|
|
320
|
+
|
|
321
|
+
# Metrics tracking system (v1.3.1+)
|
|
322
|
+
self.metrics_analyzer = ResponseMetricsAnalyzer()
|
|
323
|
+
self.track_metrics = True # Can be disabled if needed
|
|
300
324
|
|
|
301
325
|
self.logger.info("MemAgent successfully initialized")
|
|
302
326
|
|
|
@@ -472,7 +496,7 @@ class MemAgent:
|
|
|
472
496
|
self.logger.debug(f"Active user set: {user_id}")
|
|
473
497
|
|
|
474
498
|
def chat(self, message: str, user_id: Optional[str] = None,
|
|
475
|
-
metadata: Optional[Dict] = None) -> str:
|
|
499
|
+
metadata: Optional[Dict] = None, return_metrics: bool = False) -> Union[str, ChatResponse]:
|
|
476
500
|
"""
|
|
477
501
|
Chat with user
|
|
478
502
|
|
|
@@ -480,18 +504,38 @@ class MemAgent:
|
|
|
480
504
|
message: User's message
|
|
481
505
|
user_id: User ID (optional)
|
|
482
506
|
metadata: Additional information
|
|
507
|
+
return_metrics: If True, returns ChatResponse with metrics; if False, returns only text (default)
|
|
483
508
|
|
|
484
509
|
Returns:
|
|
485
|
-
Bot's response
|
|
510
|
+
Bot's response (str) or ChatResponse object with metrics
|
|
486
511
|
"""
|
|
512
|
+
# Start timing
|
|
513
|
+
start_time = time.time()
|
|
487
514
|
# Determine user
|
|
488
515
|
if user_id:
|
|
489
516
|
self.set_user(user_id)
|
|
490
517
|
elif not self.current_user:
|
|
491
|
-
|
|
518
|
+
error_response = "Error: User ID not specified."
|
|
519
|
+
if return_metrics:
|
|
520
|
+
return ChatResponse(
|
|
521
|
+
text=error_response,
|
|
522
|
+
confidence=1.0,
|
|
523
|
+
source="tool",
|
|
524
|
+
latency=(time.time() - start_time) * 1000,
|
|
525
|
+
timestamp=datetime.now(),
|
|
526
|
+
kb_results_count=0,
|
|
527
|
+
metadata={"error": True}
|
|
528
|
+
)
|
|
529
|
+
return error_response
|
|
492
530
|
|
|
493
531
|
user_id = self.current_user
|
|
494
532
|
|
|
533
|
+
# Initialize tracking variables
|
|
534
|
+
kb_results_count = 0
|
|
535
|
+
used_kb = False
|
|
536
|
+
used_memory = False
|
|
537
|
+
response_source = "model" # Default source
|
|
538
|
+
|
|
495
539
|
# Security check (v1.1.0+) - opt-in
|
|
496
540
|
security_info = {}
|
|
497
541
|
if self.enable_security and self.security_detector and self.security_sanitizer:
|
|
@@ -522,6 +566,17 @@ class MemAgent:
|
|
|
522
566
|
# Check tool commands first
|
|
523
567
|
tool_result = self.tool_executor.execute_user_command(message, user_id)
|
|
524
568
|
if tool_result:
|
|
569
|
+
latency = (time.time() - start_time) * 1000
|
|
570
|
+
if return_metrics:
|
|
571
|
+
return ChatResponse(
|
|
572
|
+
text=tool_result,
|
|
573
|
+
confidence=0.95, # Tools are deterministic
|
|
574
|
+
source="tool",
|
|
575
|
+
latency=latency,
|
|
576
|
+
timestamp=datetime.now(),
|
|
577
|
+
kb_results_count=0,
|
|
578
|
+
metadata={"tool_command": True}
|
|
579
|
+
)
|
|
525
580
|
return tool_result
|
|
526
581
|
|
|
527
582
|
# Knowledge base search (if using SQL)
|
|
@@ -540,6 +595,8 @@ class MemAgent:
|
|
|
540
595
|
kb_results = self.memory.search_knowledge(query=message, limit=kb_limit)
|
|
541
596
|
|
|
542
597
|
if kb_results:
|
|
598
|
+
kb_results_count = len(kb_results)
|
|
599
|
+
used_kb = True
|
|
543
600
|
kb_context = "\n\n📚 RELEVANT KNOWLEDGE BASE:\n"
|
|
544
601
|
for i, result in enumerate(kb_results, 1):
|
|
545
602
|
kb_context += f"{i}. Q: {result['question']}\n A: {result['answer']}\n"
|
|
@@ -558,6 +615,9 @@ class MemAgent:
|
|
|
558
615
|
recent_limit = self.config.get("response.recent_conversations_limit", 5) if hasattr(self, 'config') and self.config else 5
|
|
559
616
|
recent_convs = self.memory.get_recent_conversations(user_id, recent_limit)
|
|
560
617
|
|
|
618
|
+
if recent_convs:
|
|
619
|
+
used_memory = True
|
|
620
|
+
|
|
561
621
|
# Add conversations in chronological order (oldest first)
|
|
562
622
|
for conv in recent_convs:
|
|
563
623
|
messages.append({"role": "user", "content": conv.get('user_message', '')})
|
|
@@ -574,10 +634,11 @@ class MemAgent:
|
|
|
574
634
|
messages.append({"role": "user", "content": final_message})
|
|
575
635
|
|
|
576
636
|
# Get response from LLM
|
|
637
|
+
temperature = self.config.get("llm.temperature", 0.2) if hasattr(self, 'config') and self.config else 0.2
|
|
577
638
|
try:
|
|
578
639
|
response = self.llm.chat(
|
|
579
640
|
messages=messages,
|
|
580
|
-
temperature=
|
|
641
|
+
temperature=temperature,
|
|
581
642
|
max_tokens=self.config.get("llm.max_tokens", 2000) if hasattr(self, 'config') and self.config else 2000 # Enough tokens for thinking models
|
|
582
643
|
)
|
|
583
644
|
|
|
@@ -600,7 +661,42 @@ class MemAgent:
|
|
|
600
661
|
except Exception as e:
|
|
601
662
|
self.logger.error(f"LLM response error: {e}")
|
|
602
663
|
response = "Sorry, I cannot respond right now. Please try again later."
|
|
603
|
-
|
|
664
|
+
|
|
665
|
+
# Calculate latency
|
|
666
|
+
latency = (time.time() - start_time) * 1000
|
|
667
|
+
|
|
668
|
+
# Determine response source
|
|
669
|
+
if used_kb and used_memory:
|
|
670
|
+
response_source = "hybrid"
|
|
671
|
+
elif used_kb:
|
|
672
|
+
response_source = "knowledge_base"
|
|
673
|
+
else:
|
|
674
|
+
response_source = "model"
|
|
675
|
+
|
|
676
|
+
# Calculate confidence score
|
|
677
|
+
confidence = calculate_confidence(
|
|
678
|
+
kb_results_count=kb_results_count,
|
|
679
|
+
temperature=temperature,
|
|
680
|
+
used_memory=used_memory,
|
|
681
|
+
response_length=len(response)
|
|
682
|
+
)
|
|
683
|
+
|
|
684
|
+
# Build enriched metadata with response metrics
|
|
685
|
+
enriched_metadata = {}
|
|
686
|
+
if metadata:
|
|
687
|
+
enriched_metadata.update(metadata)
|
|
688
|
+
enriched_metadata.update({
|
|
689
|
+
"confidence": round(confidence, 3),
|
|
690
|
+
"source": response_source,
|
|
691
|
+
"latency_ms": round(latency, 1),
|
|
692
|
+
"kb_results_count": kb_results_count,
|
|
693
|
+
"used_memory": used_memory,
|
|
694
|
+
"used_kb": used_kb,
|
|
695
|
+
"response_length": len(response),
|
|
696
|
+
"model": self.model,
|
|
697
|
+
"temperature": temperature
|
|
698
|
+
})
|
|
699
|
+
|
|
604
700
|
# Save interaction
|
|
605
701
|
try:
|
|
606
702
|
if hasattr(self.memory, 'add_interaction'):
|
|
@@ -608,15 +704,47 @@ class MemAgent:
|
|
|
608
704
|
user_id=user_id,
|
|
609
705
|
user_message=message,
|
|
610
706
|
bot_response=response,
|
|
611
|
-
metadata=
|
|
707
|
+
metadata=enriched_metadata
|
|
612
708
|
)
|
|
613
709
|
|
|
614
710
|
# Extract and save user info to profile
|
|
615
711
|
self._update_user_profile(user_id, message, response)
|
|
712
|
+
|
|
713
|
+
# Always update summary after each conversation (JSON mode)
|
|
714
|
+
if not self.use_sql and hasattr(self.memory, 'conversations'):
|
|
715
|
+
self._update_conversation_summary(user_id)
|
|
716
|
+
# Save summary update
|
|
717
|
+
if user_id in self.memory.user_profiles:
|
|
718
|
+
self.memory.save_memory(user_id)
|
|
616
719
|
except Exception as e:
|
|
617
720
|
self.logger.error(f"Interaction saving error: {e}")
|
|
618
|
-
|
|
619
|
-
|
|
721
|
+
|
|
722
|
+
# Create response metrics object
|
|
723
|
+
chat_response = ChatResponse(
|
|
724
|
+
text=response,
|
|
725
|
+
confidence=confidence,
|
|
726
|
+
source=response_source,
|
|
727
|
+
latency=latency,
|
|
728
|
+
timestamp=datetime.now(),
|
|
729
|
+
kb_results_count=kb_results_count,
|
|
730
|
+
metadata={
|
|
731
|
+
"model": self.model,
|
|
732
|
+
"temperature": temperature,
|
|
733
|
+
"used_memory": used_memory,
|
|
734
|
+
"used_kb": used_kb,
|
|
735
|
+
"user_id": user_id
|
|
736
|
+
}
|
|
737
|
+
)
|
|
738
|
+
|
|
739
|
+
# Track metrics if enabled
|
|
740
|
+
if self.track_metrics:
|
|
741
|
+
self.metrics_analyzer.add_metric(chat_response)
|
|
742
|
+
|
|
743
|
+
# Return based on user preference
|
|
744
|
+
if return_metrics:
|
|
745
|
+
return chat_response
|
|
746
|
+
else:
|
|
747
|
+
return response
|
|
620
748
|
|
|
621
749
|
def _update_user_profile(self, user_id: str, message: str, response: str):
|
|
622
750
|
"""Extract user info from conversation and update profile"""
|
|
@@ -686,10 +814,128 @@ class MemAgent:
|
|
|
686
814
|
|
|
687
815
|
# JSON memory - direct update
|
|
688
816
|
elif hasattr(self.memory, 'update_profile'):
|
|
689
|
-
|
|
817
|
+
# Load memory if not already loaded
|
|
818
|
+
if user_id not in self.memory.user_profiles:
|
|
819
|
+
self.memory.load_memory(user_id)
|
|
820
|
+
|
|
821
|
+
# For JSON memory, merge into preferences
|
|
822
|
+
current_profile = self.memory.user_profiles.get(user_id, {})
|
|
823
|
+
current_prefs = current_profile.get('preferences', {})
|
|
824
|
+
|
|
825
|
+
# Handle case where preferences might be a JSON string
|
|
826
|
+
if isinstance(current_prefs, str):
|
|
827
|
+
try:
|
|
828
|
+
current_prefs = json.loads(current_prefs)
|
|
829
|
+
except:
|
|
830
|
+
current_prefs = {}
|
|
831
|
+
|
|
832
|
+
# Update preferences
|
|
833
|
+
if extracted:
|
|
834
|
+
current_prefs.update(extracted)
|
|
835
|
+
self.memory.user_profiles[user_id]['preferences'] = current_prefs
|
|
836
|
+
|
|
837
|
+
# Update name if extracted
|
|
838
|
+
if 'name' in extracted:
|
|
839
|
+
self.memory.user_profiles[user_id]['name'] = extracted['name']
|
|
840
|
+
|
|
841
|
+
# Auto-generate summary from conversation history
|
|
842
|
+
self._update_conversation_summary(user_id)
|
|
843
|
+
|
|
844
|
+
# Save to disk
|
|
845
|
+
self.memory.save_memory(user_id)
|
|
690
846
|
self.logger.debug(f"Profile updated for {user_id}: {extracted}")
|
|
691
847
|
except Exception as e:
|
|
692
848
|
self.logger.error(f"Error updating profile: {e}")
|
|
849
|
+
|
|
850
|
+
def _update_conversation_summary(self, user_id: str) -> None:
|
|
851
|
+
"""
|
|
852
|
+
Auto-generate conversation summary for user profile
|
|
853
|
+
|
|
854
|
+
Args:
|
|
855
|
+
user_id: User ID
|
|
856
|
+
"""
|
|
857
|
+
try:
|
|
858
|
+
if not hasattr(self.memory, 'conversations'):
|
|
859
|
+
return
|
|
860
|
+
|
|
861
|
+
# Ensure memory is loaded
|
|
862
|
+
if user_id not in self.memory.conversations:
|
|
863
|
+
self.memory.load_memory(user_id)
|
|
864
|
+
|
|
865
|
+
conversations = self.memory.conversations.get(user_id, [])
|
|
866
|
+
if not conversations:
|
|
867
|
+
return
|
|
868
|
+
|
|
869
|
+
# Get recent conversations for summary
|
|
870
|
+
recent_convs = conversations[-10:] # Last 10 conversations
|
|
871
|
+
|
|
872
|
+
# Extract topics/interests
|
|
873
|
+
all_messages = " ".join([c.get('user_message', '') for c in recent_convs])
|
|
874
|
+
topics = self._extract_topics(all_messages)
|
|
875
|
+
|
|
876
|
+
# Calculate engagement stats
|
|
877
|
+
total_interactions = len(conversations)
|
|
878
|
+
avg_response_length = sum(len(c.get('bot_response', '')) for c in recent_convs) / len(recent_convs) if recent_convs else 0
|
|
879
|
+
|
|
880
|
+
# Build summary
|
|
881
|
+
summary = {
|
|
882
|
+
"total_interactions": total_interactions,
|
|
883
|
+
"topics_of_interest": topics[:5] if topics else [], # Top 5 topics
|
|
884
|
+
"avg_response_length": round(avg_response_length, 0),
|
|
885
|
+
"last_active": recent_convs[-1].get('timestamp') if recent_convs else None,
|
|
886
|
+
"engagement_level": "high" if total_interactions > 20 else ("medium" if total_interactions > 5 else "low")
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
# Update profile summary (JSON mode)
|
|
890
|
+
if user_id in self.memory.user_profiles:
|
|
891
|
+
self.memory.user_profiles[user_id]['summary'] = summary
|
|
892
|
+
|
|
893
|
+
except Exception as e:
|
|
894
|
+
self.logger.debug(f"Summary generation skipped: {e}")
|
|
895
|
+
|
|
896
|
+
def _extract_topics(self, text: str) -> List[str]:
|
|
897
|
+
"""
|
|
898
|
+
Extract key topics/interests from conversation text
|
|
899
|
+
|
|
900
|
+
Args:
|
|
901
|
+
text: Combined conversation text
|
|
902
|
+
|
|
903
|
+
Returns:
|
|
904
|
+
List of extracted topics
|
|
905
|
+
"""
|
|
906
|
+
# Simple keyword extraction (can be enhanced with NLP)
|
|
907
|
+
keywords_map = {
|
|
908
|
+
"python": "Python Programming",
|
|
909
|
+
"javascript": "JavaScript",
|
|
910
|
+
"coding": "Programming",
|
|
911
|
+
"weather": "Weather",
|
|
912
|
+
"food": "Food & Dining",
|
|
913
|
+
"music": "Music",
|
|
914
|
+
"sport": "Sports",
|
|
915
|
+
"travel": "Travel",
|
|
916
|
+
"work": "Work",
|
|
917
|
+
"help": "Support",
|
|
918
|
+
"problem": "Problem Solving",
|
|
919
|
+
"question": "Questions",
|
|
920
|
+
"chat": "Chatting"
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
text_lower = text.lower()
|
|
924
|
+
found_topics = []
|
|
925
|
+
|
|
926
|
+
for keyword, topic in keywords_map.items():
|
|
927
|
+
if keyword in text_lower:
|
|
928
|
+
found_topics.append(topic)
|
|
929
|
+
|
|
930
|
+
# Remove duplicates while preserving order
|
|
931
|
+
seen = set()
|
|
932
|
+
unique_topics = []
|
|
933
|
+
for topic in found_topics:
|
|
934
|
+
if topic not in seen:
|
|
935
|
+
seen.add(topic)
|
|
936
|
+
unique_topics.append(topic)
|
|
937
|
+
|
|
938
|
+
return unique_topics
|
|
693
939
|
|
|
694
940
|
def get_user_profile(self, user_id: Optional[str] = None) -> Dict:
|
|
695
941
|
"""
|
|
@@ -706,8 +952,8 @@ class MemAgent:
|
|
|
706
952
|
return {}
|
|
707
953
|
|
|
708
954
|
try:
|
|
709
|
-
# Check if SQL or JSON memory
|
|
710
|
-
if
|
|
955
|
+
# Check if SQL or JSON memory - SQL has SQLMemoryManager type
|
|
956
|
+
if ADVANCED_AVAILABLE and isinstance(self.memory, SQLMemoryManager):
|
|
711
957
|
# SQL memory - merge preferences into main dict
|
|
712
958
|
profile = self.memory.get_user_profile(uid)
|
|
713
959
|
if not profile:
|
|
@@ -732,9 +978,45 @@ class MemAgent:
|
|
|
732
978
|
|
|
733
979
|
return result
|
|
734
980
|
else:
|
|
735
|
-
# JSON memory
|
|
981
|
+
# JSON memory - reload from disk to get latest data
|
|
736
982
|
memory_data = self.memory.load_memory(uid)
|
|
737
|
-
|
|
983
|
+
profile = memory_data.get('profile', {}).copy() # Make a copy to avoid modifying cached data
|
|
984
|
+
|
|
985
|
+
# Parse preferences if it's a JSON string
|
|
986
|
+
if isinstance(profile.get('preferences'), str):
|
|
987
|
+
try:
|
|
988
|
+
profile['preferences'] = json.loads(profile['preferences'])
|
|
989
|
+
except:
|
|
990
|
+
profile['preferences'] = {}
|
|
991
|
+
|
|
992
|
+
# Return profile as-is (summary should already be there if it was generated)
|
|
993
|
+
# Only regenerate if truly missing
|
|
994
|
+
summary_value = profile.get('summary')
|
|
995
|
+
summary_is_empty = (not summary_value or
|
|
996
|
+
(isinstance(summary_value, dict) and len(summary_value) == 0))
|
|
997
|
+
|
|
998
|
+
if summary_is_empty:
|
|
999
|
+
# Try to regenerate summary if missing (for old users)
|
|
1000
|
+
# Ensure conversations are loaded
|
|
1001
|
+
if uid not in self.memory.conversations:
|
|
1002
|
+
self.memory.load_memory(uid)
|
|
1003
|
+
|
|
1004
|
+
if uid in self.memory.conversations and len(self.memory.conversations[uid]) > 0:
|
|
1005
|
+
self._update_conversation_summary(uid)
|
|
1006
|
+
# Save the updated summary
|
|
1007
|
+
if uid in self.memory.user_profiles:
|
|
1008
|
+
self.memory.save_memory(uid)
|
|
1009
|
+
# Reload to get updated summary
|
|
1010
|
+
memory_data = self.memory.load_memory(uid)
|
|
1011
|
+
profile = memory_data.get('profile', {}).copy()
|
|
1012
|
+
# Parse preferences again after reload
|
|
1013
|
+
if isinstance(profile.get('preferences'), str):
|
|
1014
|
+
try:
|
|
1015
|
+
profile['preferences'] = json.loads(profile['preferences'])
|
|
1016
|
+
except:
|
|
1017
|
+
profile['preferences'] = {}
|
|
1018
|
+
|
|
1019
|
+
return profile
|
|
738
1020
|
except Exception as e:
|
|
739
1021
|
self.logger.error(f"Error getting user profile: {e}")
|
|
740
1022
|
return {}
|
|
@@ -865,6 +1147,108 @@ class MemAgent:
|
|
|
865
1147
|
return self.tool_executor.memory_tools.list_available_tools()
|
|
866
1148
|
else:
|
|
867
1149
|
return "Tool system not available."
|
|
1150
|
+
|
|
1151
|
+
# === METRICS & ANALYTICS METHODS (v1.3.1+) ===
|
|
1152
|
+
|
|
1153
|
+
def get_response_metrics(self, last_n: Optional[int] = None) -> Dict[str, Any]:
|
|
1154
|
+
"""
|
|
1155
|
+
Get response quality metrics summary
|
|
1156
|
+
|
|
1157
|
+
Args:
|
|
1158
|
+
last_n: Analyze only last N responses (None = all)
|
|
1159
|
+
|
|
1160
|
+
Returns:
|
|
1161
|
+
Metrics summary dictionary
|
|
1162
|
+
|
|
1163
|
+
Example:
|
|
1164
|
+
>>> agent.get_response_metrics(last_n=10)
|
|
1165
|
+
{
|
|
1166
|
+
'total_responses': 10,
|
|
1167
|
+
'avg_latency_ms': 245.3,
|
|
1168
|
+
'avg_confidence': 0.82,
|
|
1169
|
+
'kb_usage_rate': 0.6,
|
|
1170
|
+
'source_distribution': {'knowledge_base': 6, 'model': 4},
|
|
1171
|
+
'fast_response_rate': 0.9
|
|
1172
|
+
}
|
|
1173
|
+
"""
|
|
1174
|
+
return self.metrics_analyzer.get_summary(last_n)
|
|
1175
|
+
|
|
1176
|
+
def get_latest_response_metric(self) -> Optional[ChatResponse]:
|
|
1177
|
+
"""
|
|
1178
|
+
Get the most recent response metric
|
|
1179
|
+
|
|
1180
|
+
Returns:
|
|
1181
|
+
Latest ChatResponse object or None if no metrics
|
|
1182
|
+
"""
|
|
1183
|
+
if not self.metrics_analyzer.metrics_history:
|
|
1184
|
+
return None
|
|
1185
|
+
return self.metrics_analyzer.metrics_history[-1]
|
|
1186
|
+
|
|
1187
|
+
def get_average_confidence(self, last_n: Optional[int] = None) -> float:
|
|
1188
|
+
"""
|
|
1189
|
+
Get average confidence score
|
|
1190
|
+
|
|
1191
|
+
Args:
|
|
1192
|
+
last_n: Analyze only last N responses (None = all)
|
|
1193
|
+
|
|
1194
|
+
Returns:
|
|
1195
|
+
Average confidence (0.0-1.0)
|
|
1196
|
+
"""
|
|
1197
|
+
return self.metrics_analyzer.get_average_confidence(last_n)
|
|
1198
|
+
|
|
1199
|
+
def get_kb_usage_rate(self, last_n: Optional[int] = None) -> float:
|
|
1200
|
+
"""
|
|
1201
|
+
Get knowledge base usage rate
|
|
1202
|
+
|
|
1203
|
+
Args:
|
|
1204
|
+
last_n: Analyze only last N responses (None = all)
|
|
1205
|
+
|
|
1206
|
+
Returns:
|
|
1207
|
+
KB usage rate (0.0-1.0)
|
|
1208
|
+
"""
|
|
1209
|
+
return self.metrics_analyzer.get_kb_usage_rate(last_n)
|
|
1210
|
+
|
|
1211
|
+
def clear_metrics(self) -> None:
|
|
1212
|
+
"""Clear all metrics history"""
|
|
1213
|
+
self.metrics_analyzer.clear_history()
|
|
1214
|
+
self.logger.info("Metrics history cleared")
|
|
1215
|
+
|
|
1216
|
+
def export_metrics(self, format: str = "json") -> str:
|
|
1217
|
+
"""
|
|
1218
|
+
Export metrics data
|
|
1219
|
+
|
|
1220
|
+
Args:
|
|
1221
|
+
format: Export format ('json' or 'summary')
|
|
1222
|
+
|
|
1223
|
+
Returns:
|
|
1224
|
+
Formatted metrics data
|
|
1225
|
+
"""
|
|
1226
|
+
summary = self.get_response_metrics()
|
|
1227
|
+
|
|
1228
|
+
if format == "json":
|
|
1229
|
+
return json.dumps(summary, ensure_ascii=False, indent=2)
|
|
1230
|
+
elif format == "summary":
|
|
1231
|
+
lines = [
|
|
1232
|
+
"📊 RESPONSE METRICS SUMMARY",
|
|
1233
|
+
"=" * 60,
|
|
1234
|
+
f"Total Responses: {summary['total_responses']}",
|
|
1235
|
+
f"Avg Latency: {summary['avg_latency_ms']:.1f} ms",
|
|
1236
|
+
f"Avg Confidence: {summary['avg_confidence']:.2%}",
|
|
1237
|
+
f"KB Usage Rate: {summary['kb_usage_rate']:.2%}",
|
|
1238
|
+
f"Fast Response Rate: {summary['fast_response_rate']:.2%}",
|
|
1239
|
+
"",
|
|
1240
|
+
"Source Distribution:",
|
|
1241
|
+
]
|
|
1242
|
+
for source, count in summary['source_distribution'].items():
|
|
1243
|
+
lines.append(f" - {source:20s}: {count}")
|
|
1244
|
+
|
|
1245
|
+
lines.extend(["", "Quality Distribution:"])
|
|
1246
|
+
for quality, count in summary.get('quality_distribution', {}).items():
|
|
1247
|
+
lines.append(f" - {quality:20s}: {count}")
|
|
1248
|
+
|
|
1249
|
+
return "\n".join(lines)
|
|
1250
|
+
else:
|
|
1251
|
+
return "Unsupported format. Use 'json' or 'summary'."
|
|
868
1252
|
|
|
869
1253
|
def close(self) -> None:
|
|
870
1254
|
"""Clean up resources"""
|