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 CHANGED
@@ -63,7 +63,14 @@ try:
63
63
  except ImportError:
64
64
  __all_export_import__ = []
65
65
 
66
- __version__ = "1.3.0"
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
- enable_security: bool = False,
78
- **llm_kwargs):
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(final_db_path)
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
- return "Error: User ID not specified."
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=self.config.get("llm.temperature", 0.2) if hasattr(self, 'config') and self.config else 0.2, # Very focused
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=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
- return response
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
- self.memory.update_profile(user_id, extracted)
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 hasattr(self.memory, 'get_user_profile'):
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
- return memory_data.get('profile', {})
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"""