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/memory_db.py CHANGED
@@ -9,15 +9,32 @@ import threading
9
9
  from datetime import datetime
10
10
  from typing import Dict, List, Optional, Tuple
11
11
  from pathlib import Path
12
+ import logging
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ # Optional vector store support
17
+ try:
18
+ from .vector_store import create_vector_store, VectorStore
19
+ VECTOR_STORE_AVAILABLE = True
20
+ except ImportError:
21
+ VECTOR_STORE_AVAILABLE = False
22
+ VectorStore = None
12
23
 
13
24
 
14
25
  class SQLMemoryManager:
15
26
  """SQLite-based memory management system with thread-safety"""
16
27
 
17
- def __init__(self, db_path: str = "memories/memories.db"):
28
+ def __init__(self, db_path: str = "memories/memories.db",
29
+ enable_vector_search: bool = False,
30
+ vector_store_type: str = "chroma",
31
+ embedding_model: str = "all-MiniLM-L6-v2"):
18
32
  """
19
33
  Args:
20
34
  db_path: SQLite database file path
35
+ enable_vector_search: Enable vector/semantic search (optional)
36
+ vector_store_type: Type of vector store ('chroma', etc.)
37
+ embedding_model: Embedding model name (sentence-transformers)
21
38
  """
22
39
  self.db_path = Path(db_path)
23
40
 
@@ -29,6 +46,35 @@ class SQLMemoryManager:
29
46
  self.conn = None
30
47
  self._lock = threading.RLock() # Reentrant lock for thread safety
31
48
  self._init_database()
49
+
50
+ # Vector store (optional)
51
+ self.enable_vector_search = enable_vector_search
52
+ self.vector_store: Optional[VectorStore] = None
53
+
54
+ if enable_vector_search:
55
+ if not VECTOR_STORE_AVAILABLE:
56
+ logger.warning(
57
+ "Vector search requested but dependencies not available. "
58
+ "Install with: pip install chromadb sentence-transformers"
59
+ )
60
+ self.enable_vector_search = False
61
+ else:
62
+ try:
63
+ persist_dir = str(db_dir / "vector_store")
64
+ self.vector_store = create_vector_store(
65
+ store_type=vector_store_type,
66
+ collection_name="knowledge_base",
67
+ persist_directory=persist_dir,
68
+ embedding_model=embedding_model
69
+ )
70
+ if self.vector_store:
71
+ logger.info(f"Vector search enabled: {vector_store_type}")
72
+ else:
73
+ logger.warning("Failed to initialize vector store, falling back to keyword search")
74
+ self.enable_vector_search = False
75
+ except Exception as e:
76
+ logger.error(f"Error initializing vector store: {e}")
77
+ self.enable_vector_search = False
32
78
 
33
79
  def _init_database(self) -> None:
34
80
  """Create database and tables"""
@@ -312,22 +358,44 @@ class SQLMemoryManager:
312
358
  """, (category, question, answer,
313
359
  json.dumps(keywords or []), priority))
314
360
 
361
+ kb_id = cursor.lastrowid
315
362
  self.conn.commit()
316
- return cursor.lastrowid
363
+
364
+ # Sync to vector store if enabled
365
+ if self.enable_vector_search and self.vector_store:
366
+ try:
367
+ self._sync_to_vector_store(kb_id)
368
+ except Exception as e:
369
+ logger.warning(f"Failed to sync KB entry to vector store: {e}")
370
+
371
+ return kb_id
317
372
 
318
373
  def search_knowledge(self, query: str, category: Optional[str] = None,
319
- limit: int = 5) -> List[Dict]:
374
+ limit: int = 5, use_vector_search: Optional[bool] = None) -> List[Dict]:
320
375
  """
321
- Bilgi bankasında arama yapar (gelişmiş keyword matching)
376
+ Bilgi bankasında arama yapar (keyword matching veya semantic search)
322
377
 
323
378
  Args:
324
379
  query: Arama sorgusu
325
380
  category: Kategori filtresi (opsiyonel)
326
381
  limit: Maksimum sonuç sayısı
382
+ use_vector_search: Force vector search (None = auto-detect)
327
383
 
328
384
  Returns:
329
385
  Bulunan kayıtlar
330
386
  """
387
+ # Use vector search if enabled and available
388
+ if use_vector_search is None:
389
+ use_vector_search = self.enable_vector_search
390
+
391
+ if use_vector_search and self.vector_store:
392
+ return self._vector_search(query, category, limit)
393
+ else:
394
+ return self._keyword_search(query, category, limit)
395
+
396
+ def _keyword_search(self, query: str, category: Optional[str] = None,
397
+ limit: int = 5) -> List[Dict]:
398
+ """Traditional keyword-based search"""
331
399
  cursor = self.conn.cursor()
332
400
 
333
401
  # Extract important keywords from query (remove question words)
@@ -378,6 +446,120 @@ class SQLMemoryManager:
378
446
 
379
447
  return [dict(row) for row in cursor.fetchall()]
380
448
 
449
+ def _vector_search(self, query: str, category: Optional[str] = None,
450
+ limit: int = 5) -> List[Dict]:
451
+ """Vector-based semantic search"""
452
+ if not self.vector_store:
453
+ return []
454
+
455
+ # Prepare metadata filter
456
+ filter_metadata = None
457
+ if category:
458
+ filter_metadata = {"category": category}
459
+
460
+ # Search in vector store
461
+ vector_results = self.vector_store.search(
462
+ query=query,
463
+ limit=limit * 2, # Get more results to filter by category if needed
464
+ filter_metadata=filter_metadata
465
+ )
466
+
467
+ # Map vector results back to KB format
468
+ results = []
469
+ for result in vector_results[:limit]:
470
+ # Extract metadata
471
+ metadata = result.get('metadata', {})
472
+
473
+ results.append({
474
+ 'category': metadata.get('category', ''),
475
+ 'question': metadata.get('question', ''),
476
+ 'answer': result.get('text', ''),
477
+ 'priority': metadata.get('priority', 0),
478
+ 'score': result.get('score', 0.0), # Similarity score
479
+ 'vector_search': True
480
+ })
481
+
482
+ return results
483
+
484
+ def _sync_to_vector_store(self, kb_id: int) -> None:
485
+ """Sync a single KB entry to vector store"""
486
+ if not self.vector_store:
487
+ return
488
+
489
+ cursor = self.conn.cursor()
490
+ cursor.execute("""
491
+ SELECT id, category, question, answer, keywords, priority
492
+ FROM knowledge_base
493
+ WHERE id = ?
494
+ """, (kb_id,))
495
+
496
+ row = cursor.fetchone()
497
+ if row:
498
+ doc = {
499
+ 'id': str(row['id']),
500
+ 'text': f"{row['question']}\n{row['answer']}", # Combine for better search
501
+ 'metadata': {
502
+ 'category': row['category'],
503
+ 'question': row['question'],
504
+ 'answer': row['answer'],
505
+ 'keywords': row['keywords'],
506
+ 'priority': row['priority'],
507
+ 'kb_id': row['id']
508
+ }
509
+ }
510
+ self.vector_store.add_documents([doc])
511
+
512
+ def sync_all_kb_to_vector_store(self) -> int:
513
+ """
514
+ Sync all existing KB entries to vector store
515
+
516
+ Returns:
517
+ Number of entries synced
518
+ """
519
+ if not self.vector_store:
520
+ return 0
521
+
522
+ cursor = self.conn.cursor()
523
+ cursor.execute("""
524
+ SELECT id, category, question, answer, keywords, priority
525
+ FROM knowledge_base
526
+ WHERE active = 1
527
+ """)
528
+
529
+ rows = cursor.fetchall()
530
+ documents = []
531
+
532
+ for row in rows:
533
+ doc = {
534
+ 'id': str(row['id']),
535
+ 'text': f"{row['question']}\n{row['answer']}",
536
+ 'metadata': {
537
+ 'category': row['category'],
538
+ 'question': row['question'],
539
+ 'answer': row['answer'],
540
+ 'keywords': row['keywords'],
541
+ 'priority': row['priority'],
542
+ 'kb_id': row['id']
543
+ }
544
+ }
545
+ documents.append(doc)
546
+
547
+ if documents:
548
+ try:
549
+ # Add in batches for better performance
550
+ batch_size = 100
551
+ for i in range(0, len(documents), batch_size):
552
+ batch = documents[i:i + batch_size]
553
+ self.vector_store.add_documents(batch)
554
+ logger.debug(f"Synced {len(batch)} KB entries to vector store")
555
+
556
+ logger.info(f"Synced {len(documents)} KB entries to vector store")
557
+ except Exception as e:
558
+ logger.error(f"Error syncing KB to vector store: {e}")
559
+ return 0
560
+
561
+ return len(documents)
562
+
381
563
  def get_statistics(self) -> Dict:
382
564
  """
383
565
  Genel istatistikleri döndürür
mem_llm/memory_manager.py CHANGED
@@ -43,7 +43,16 @@ class MemoryManager:
43
43
  with open(user_file, 'r', encoding='utf-8') as f:
44
44
  data = json.load(f)
45
45
  self.conversations[user_id] = data.get('conversations', [])
46
- self.user_profiles[user_id] = data.get('profile', {})
46
+ profile = data.get('profile', {})
47
+
48
+ # Parse preferences if it's a JSON string (legacy format)
49
+ if isinstance(profile.get('preferences'), str):
50
+ try:
51
+ profile['preferences'] = json.loads(profile['preferences'])
52
+ except:
53
+ profile['preferences'] = {}
54
+
55
+ self.user_profiles[user_id] = profile
47
56
  return data
48
57
  else:
49
58
  # Create empty memory for new user
@@ -0,0 +1,221 @@
1
+ """
2
+ Response Metrics Module
3
+ =======================
4
+
5
+ Tracks and analyzes LLM response quality metrics including:
6
+ - Response latency
7
+ - Confidence scoring
8
+ - Knowledge base usage
9
+ - Source tracking
10
+ """
11
+
12
+ from dataclasses import dataclass, asdict
13
+ from datetime import datetime
14
+ from typing import Dict, Any, Optional, List
15
+ import json
16
+
17
+
18
+ @dataclass
19
+ class ChatResponse:
20
+ """
21
+ Comprehensive response object with quality metrics
22
+
23
+ Attributes:
24
+ text: The actual response text
25
+ confidence: Confidence score 0.0-1.0 (higher = more confident)
26
+ source: Response source ("knowledge_base", "model", "tool", "hybrid")
27
+ latency: Response time in milliseconds
28
+ timestamp: When the response was generated
29
+ kb_results_count: Number of KB results used (0 if none)
30
+ metadata: Additional context (model name, temperature, etc.)
31
+ """
32
+ text: str
33
+ confidence: float
34
+ source: str
35
+ latency: float
36
+ timestamp: datetime
37
+ kb_results_count: int = 0
38
+ metadata: Optional[Dict[str, Any]] = None
39
+
40
+ def __post_init__(self):
41
+ """Validate metrics after initialization"""
42
+ # Ensure confidence is in valid range
43
+ if not 0.0 <= self.confidence <= 1.0:
44
+ raise ValueError(f"Confidence must be between 0.0 and 1.0, got {self.confidence}")
45
+
46
+ # Validate source
47
+ valid_sources = ["knowledge_base", "model", "tool", "hybrid"]
48
+ if self.source not in valid_sources:
49
+ raise ValueError(f"Source must be one of {valid_sources}, got {self.source}")
50
+
51
+ # Ensure latency is positive
52
+ if self.latency < 0:
53
+ raise ValueError(f"Latency cannot be negative, got {self.latency}")
54
+
55
+ def to_dict(self) -> Dict[str, Any]:
56
+ """Convert to dictionary for JSON serialization"""
57
+ data = asdict(self)
58
+ data['timestamp'] = self.timestamp.isoformat()
59
+ return data
60
+
61
+ def to_json(self) -> str:
62
+ """Convert to JSON string"""
63
+ return json.dumps(self.to_dict(), ensure_ascii=False, indent=2)
64
+
65
+ @classmethod
66
+ def from_dict(cls, data: Dict[str, Any]) -> 'ChatResponse':
67
+ """Create ChatResponse from dictionary"""
68
+ data['timestamp'] = datetime.fromisoformat(data['timestamp'])
69
+ return cls(**data)
70
+
71
+ def get_quality_label(self) -> str:
72
+ """Get human-readable quality label"""
73
+ if self.confidence >= 0.90:
74
+ return "Excellent"
75
+ elif self.confidence >= 0.80:
76
+ return "High"
77
+ elif self.confidence >= 0.65:
78
+ return "Medium"
79
+ elif self.confidence >= 0.50:
80
+ return "Low"
81
+ else:
82
+ return "Very Low"
83
+
84
+ def is_fast(self, threshold_ms: float = 1000.0) -> bool:
85
+ """Check if response was fast (< threshold)"""
86
+ return self.latency < threshold_ms
87
+
88
+ def __str__(self) -> str:
89
+ """Human-readable string representation"""
90
+ return (
91
+ f"ChatResponse(text_length={len(self.text)}, "
92
+ f"confidence={self.confidence:.2f}, "
93
+ f"source={self.source}, "
94
+ f"latency={self.latency:.0f}ms, "
95
+ f"quality={self.get_quality_label()})"
96
+ )
97
+
98
+
99
+ class ResponseMetricsAnalyzer:
100
+ """Analyzes and aggregates response metrics over time"""
101
+
102
+ def __init__(self):
103
+ self.metrics_history: List[ChatResponse] = []
104
+
105
+ def add_metric(self, response: ChatResponse) -> None:
106
+ """Add a response metric to history"""
107
+ self.metrics_history.append(response)
108
+
109
+ def get_average_latency(self, last_n: Optional[int] = None) -> float:
110
+ """Calculate average latency for last N responses"""
111
+ metrics = self.metrics_history[-last_n:] if last_n else self.metrics_history
112
+ if not metrics:
113
+ return 0.0
114
+ return sum(m.latency for m in metrics) / len(metrics)
115
+
116
+ def get_average_confidence(self, last_n: Optional[int] = None) -> float:
117
+ """Calculate average confidence for last N responses"""
118
+ metrics = self.metrics_history[-last_n:] if last_n else self.metrics_history
119
+ if not metrics:
120
+ return 0.0
121
+ return sum(m.confidence for m in metrics) / len(metrics)
122
+
123
+ def get_kb_usage_rate(self, last_n: Optional[int] = None) -> float:
124
+ """Calculate knowledge base usage rate (0.0-1.0)"""
125
+ metrics = self.metrics_history[-last_n:] if last_n else self.metrics_history
126
+ if not metrics:
127
+ return 0.0
128
+ kb_used = sum(1 for m in metrics if m.kb_results_count > 0)
129
+ return kb_used / len(metrics)
130
+
131
+ def get_source_distribution(self, last_n: Optional[int] = None) -> Dict[str, int]:
132
+ """Get distribution of response sources"""
133
+ metrics = self.metrics_history[-last_n:] if last_n else self.metrics_history
134
+ distribution = {}
135
+ for metric in metrics:
136
+ distribution[metric.source] = distribution.get(metric.source, 0) + 1
137
+ return distribution
138
+
139
+ def get_summary(self, last_n: Optional[int] = None) -> Dict[str, Any]:
140
+ """Get comprehensive metrics summary"""
141
+ metrics = self.metrics_history[-last_n:] if last_n else self.metrics_history
142
+
143
+ if not metrics:
144
+ return {
145
+ "total_responses": 0,
146
+ "avg_latency_ms": 0.0,
147
+ "avg_confidence": 0.0,
148
+ "kb_usage_rate": 0.0,
149
+ "source_distribution": {},
150
+ "fast_response_rate": 0.0
151
+ }
152
+
153
+ fast_responses = sum(1 for m in metrics if m.is_fast())
154
+
155
+ return {
156
+ "total_responses": len(metrics),
157
+ "avg_latency_ms": round(self.get_average_latency(last_n), 2),
158
+ "avg_confidence": round(self.get_average_confidence(last_n), 3),
159
+ "kb_usage_rate": round(self.get_kb_usage_rate(last_n), 3),
160
+ "source_distribution": self.get_source_distribution(last_n),
161
+ "fast_response_rate": round(fast_responses / len(metrics), 3),
162
+ "quality_distribution": self._get_quality_distribution(metrics)
163
+ }
164
+
165
+ def _get_quality_distribution(self, metrics: List[ChatResponse]) -> Dict[str, int]:
166
+ """Get distribution of quality labels"""
167
+ distribution = {}
168
+ for metric in metrics:
169
+ quality = metric.get_quality_label()
170
+ distribution[quality] = distribution.get(quality, 0) + 1
171
+ return distribution
172
+
173
+ def clear_history(self) -> None:
174
+ """Clear all metrics history"""
175
+ self.metrics_history.clear()
176
+
177
+
178
+ def calculate_confidence(
179
+ kb_results_count: int,
180
+ temperature: float,
181
+ used_memory: bool,
182
+ response_length: int
183
+ ) -> float:
184
+ """
185
+ Calculate confidence score based on multiple factors
186
+
187
+ Args:
188
+ kb_results_count: Number of KB results used
189
+ temperature: Model temperature setting
190
+ used_memory: Whether conversation memory was used
191
+ response_length: Length of response in characters
192
+
193
+ Returns:
194
+ Confidence score between 0.0 and 1.0
195
+ """
196
+ base_confidence = 0.50
197
+
198
+ # KB contribution (0-0.35)
199
+ if kb_results_count > 0:
200
+ kb_boost = min(0.35, 0.10 + (kb_results_count * 0.05))
201
+ base_confidence += kb_boost
202
+
203
+ # Memory contribution (0-0.10)
204
+ if used_memory:
205
+ base_confidence += 0.10
206
+
207
+ # Temperature factor (lower temp = higher confidence)
208
+ # Temperature usually 0.0-1.0, we give 0-0.15 boost
209
+ temp_factor = (1.0 - min(temperature, 1.0)) * 0.15
210
+ base_confidence += temp_factor
211
+
212
+ # Response length factor (very short = lower confidence)
213
+ # Penalize very short responses (< 20 chars)
214
+ if response_length < 20:
215
+ base_confidence *= 0.8
216
+ elif response_length < 50:
217
+ base_confidence *= 0.9
218
+
219
+ # Ensure confidence stays in valid range
220
+ return max(0.0, min(1.0, base_confidence))
221
+