memorisdk 1.0.1__py3-none-any.whl → 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of memorisdk might be problematic. Click here for more details.

Files changed (46) hide show
  1. memori/__init__.py +24 -8
  2. memori/agents/conscious_agent.py +252 -414
  3. memori/agents/memory_agent.py +487 -224
  4. memori/agents/retrieval_agent.py +416 -60
  5. memori/config/memory_manager.py +323 -0
  6. memori/core/conversation.py +393 -0
  7. memori/core/database.py +386 -371
  8. memori/core/memory.py +1676 -534
  9. memori/core/providers.py +217 -0
  10. memori/database/adapters/__init__.py +10 -0
  11. memori/database/adapters/mysql_adapter.py +331 -0
  12. memori/database/adapters/postgresql_adapter.py +291 -0
  13. memori/database/adapters/sqlite_adapter.py +229 -0
  14. memori/database/auto_creator.py +320 -0
  15. memori/database/connection_utils.py +207 -0
  16. memori/database/connectors/base_connector.py +283 -0
  17. memori/database/connectors/mysql_connector.py +240 -18
  18. memori/database/connectors/postgres_connector.py +277 -4
  19. memori/database/connectors/sqlite_connector.py +178 -3
  20. memori/database/models.py +400 -0
  21. memori/database/queries/base_queries.py +1 -1
  22. memori/database/queries/memory_queries.py +91 -2
  23. memori/database/query_translator.py +222 -0
  24. memori/database/schema_generators/__init__.py +7 -0
  25. memori/database/schema_generators/mysql_schema_generator.py +215 -0
  26. memori/database/search/__init__.py +8 -0
  27. memori/database/search/mysql_search_adapter.py +255 -0
  28. memori/database/search/sqlite_search_adapter.py +180 -0
  29. memori/database/search_service.py +548 -0
  30. memori/database/sqlalchemy_manager.py +839 -0
  31. memori/integrations/__init__.py +36 -11
  32. memori/integrations/litellm_integration.py +340 -6
  33. memori/integrations/openai_integration.py +506 -240
  34. memori/utils/input_validator.py +395 -0
  35. memori/utils/pydantic_models.py +138 -36
  36. memori/utils/query_builder.py +530 -0
  37. memori/utils/security_audit.py +594 -0
  38. memori/utils/security_integration.py +339 -0
  39. memori/utils/transaction_manager.py +547 -0
  40. {memorisdk-1.0.1.dist-info → memorisdk-2.0.0.dist-info}/METADATA +144 -34
  41. memorisdk-2.0.0.dist-info/RECORD +67 -0
  42. memorisdk-1.0.1.dist-info/RECORD +0 -44
  43. memorisdk-1.0.1.dist-info/entry_points.txt +0 -2
  44. {memorisdk-1.0.1.dist-info → memorisdk-2.0.0.dist-info}/WHEEL +0 -0
  45. {memorisdk-1.0.1.dist-info → memorisdk-2.0.0.dist-info}/licenses/LICENSE +0 -0
  46. {memorisdk-1.0.1.dist-info → memorisdk-2.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,393 @@
1
+ """
2
+ Conversation Manager for Stateless LLM SDK Integration
3
+
4
+ This module provides conversation tracking and context management for stateless LLM SDKs
5
+ like OpenAI, Anthropic, etc. It bridges the gap between memori's stateful memory system
6
+ and stateless LLM API calls by maintaining conversation history and context.
7
+ """
8
+
9
+ import uuid
10
+ from dataclasses import dataclass, field
11
+ from datetime import datetime, timedelta
12
+ from typing import Any, Dict, List
13
+
14
+ from loguru import logger
15
+
16
+
17
+ @dataclass
18
+ class ConversationMessage:
19
+ """Represents a single message in a conversation"""
20
+
21
+ role: str # "user", "assistant", "system"
22
+ content: str
23
+ timestamp: datetime = field(default_factory=datetime.now)
24
+ metadata: Dict[str, Any] = field(default_factory=dict)
25
+
26
+
27
+ @dataclass
28
+ class ConversationSession:
29
+ """Represents an active conversation session"""
30
+
31
+ session_id: str
32
+ messages: List[ConversationMessage] = field(default_factory=list)
33
+ context_injected: bool = False
34
+ created_at: datetime = field(default_factory=datetime.now)
35
+ last_accessed: datetime = field(default_factory=datetime.now)
36
+ metadata: Dict[str, Any] = field(default_factory=dict)
37
+
38
+ def add_message(self, role: str, content: str, metadata: Dict[str, Any] = None):
39
+ """Add a message to the conversation"""
40
+ message = ConversationMessage(
41
+ role=role, content=content, metadata=metadata or {}
42
+ )
43
+ self.messages.append(message)
44
+ self.last_accessed = datetime.now()
45
+
46
+ def get_history_messages(self, limit: int = 10) -> List[Dict[str, str]]:
47
+ """Get conversation history in OpenAI message format"""
48
+ # Get recent messages (excluding system messages)
49
+ user_assistant_messages = [
50
+ msg for msg in self.messages if msg.role in ["user", "assistant"]
51
+ ]
52
+
53
+ # Limit to recent messages to prevent context overflow
54
+ recent_messages = (
55
+ user_assistant_messages[-limit:] if limit > 0 else user_assistant_messages
56
+ )
57
+
58
+ return [{"role": msg.role, "content": msg.content} for msg in recent_messages]
59
+
60
+
61
+ class ConversationManager:
62
+ """
63
+ Manages conversation sessions for stateless LLM integrations.
64
+
65
+ This class provides:
66
+ - Session-based conversation tracking
67
+ - Context injection with conversation history
68
+ - Automatic session cleanup
69
+ - Support for both conscious_ingest and auto_ingest modes
70
+ """
71
+
72
+ def __init__(
73
+ self,
74
+ max_sessions: int = 100,
75
+ session_timeout_minutes: int = 60,
76
+ max_history_per_session: int = 20,
77
+ ):
78
+ """
79
+ Initialize ConversationManager
80
+
81
+ Args:
82
+ max_sessions: Maximum number of active sessions
83
+ session_timeout_minutes: Session timeout in minutes
84
+ max_history_per_session: Maximum messages to keep per session
85
+ """
86
+ self.max_sessions = max_sessions
87
+ self.session_timeout = timedelta(minutes=session_timeout_minutes)
88
+ self.max_history_per_session = max_history_per_session
89
+
90
+ # Active conversation sessions
91
+ self.sessions: Dict[str, ConversationSession] = {}
92
+
93
+ logger.info(
94
+ f"ConversationManager initialized: max_sessions={max_sessions}, "
95
+ f"timeout={session_timeout_minutes}min, max_history={max_history_per_session}"
96
+ )
97
+
98
+ def get_or_create_session(self, session_id: str = None) -> ConversationSession:
99
+ """
100
+ Get existing session or create new one
101
+
102
+ Args:
103
+ session_id: Optional session ID. If None, generates new one.
104
+
105
+ Returns:
106
+ ConversationSession instance
107
+ """
108
+ if session_id is None:
109
+ session_id = str(uuid.uuid4())
110
+
111
+ # Clean up expired sessions first
112
+ self._cleanup_expired_sessions()
113
+
114
+ # Get existing session or create new one
115
+ if session_id not in self.sessions:
116
+ if len(self.sessions) >= self.max_sessions:
117
+ # Remove oldest session to make room
118
+ oldest_session_id = min(
119
+ self.sessions.keys(),
120
+ key=lambda sid: self.sessions[sid].last_accessed,
121
+ )
122
+ del self.sessions[oldest_session_id]
123
+ logger.debug(f"Removed oldest session {oldest_session_id} to make room")
124
+
125
+ self.sessions[session_id] = ConversationSession(session_id=session_id)
126
+ logger.debug(f"Created new conversation session: {session_id}")
127
+ else:
128
+ # Update last accessed time
129
+ self.sessions[session_id].last_accessed = datetime.now()
130
+
131
+ return self.sessions[session_id]
132
+
133
+ def add_user_message(
134
+ self, session_id: str, content: str, metadata: Dict[str, Any] = None
135
+ ):
136
+ """Add user message to conversation session"""
137
+ session = self.get_or_create_session(session_id)
138
+ session.add_message("user", content, metadata)
139
+
140
+ # Limit history to prevent memory bloat
141
+ if len(session.messages) > self.max_history_per_session:
142
+ # Keep system messages and recent messages
143
+ system_messages = [msg for msg in session.messages if msg.role == "system"]
144
+ other_messages = [msg for msg in session.messages if msg.role != "system"]
145
+
146
+ # Keep recent non-system messages
147
+ recent_messages = other_messages[
148
+ -(self.max_history_per_session - len(system_messages)) :
149
+ ]
150
+ session.messages = system_messages + recent_messages
151
+
152
+ logger.debug(f"Trimmed conversation history for session {session_id}")
153
+
154
+ def add_assistant_message(
155
+ self, session_id: str, content: str, metadata: Dict[str, Any] = None
156
+ ):
157
+ """Add assistant message to conversation session"""
158
+ session = self.get_or_create_session(session_id)
159
+ session.add_message("assistant", content, metadata)
160
+
161
+ def inject_context_with_history(
162
+ self,
163
+ session_id: str,
164
+ messages: List[Dict[str, str]],
165
+ memori_instance,
166
+ mode: str = "conscious",
167
+ ) -> List[Dict[str, str]]:
168
+ """
169
+ Inject context and conversation history into messages
170
+
171
+ Args:
172
+ session_id: Conversation session ID
173
+ messages: Original messages from API call
174
+ memori_instance: Memori instance for context retrieval
175
+ mode: Context injection mode ("conscious" or "auto")
176
+
177
+ Returns:
178
+ Modified messages with context and history injected
179
+ """
180
+ try:
181
+ session = self.get_or_create_session(session_id)
182
+
183
+ # Extract user input from current messages
184
+ user_input = ""
185
+ for msg in reversed(messages):
186
+ if msg.get("role") == "user":
187
+ user_input = msg.get("content", "")
188
+ break
189
+
190
+ # Add current user message to session history
191
+ if user_input:
192
+ self.add_user_message(session_id, user_input)
193
+
194
+ # Build context based on mode
195
+ context_prompt = ""
196
+
197
+ if mode == "conscious":
198
+ # Conscious mode: Always inject short-term memory context
199
+ # (Not just once - this fixes the original bug)
200
+ context = memori_instance._get_conscious_context()
201
+ if context:
202
+ context_prompt = self._build_conscious_context_prompt(context)
203
+ logger.debug(
204
+ f"Injected conscious context with {len(context)} items for session {session_id}"
205
+ )
206
+
207
+ elif mode == "auto":
208
+ # Auto mode: Search long-term memory database for relevant context
209
+ logger.debug(
210
+ f"Auto-ingest: Processing user input for long-term memory search: '{user_input[:50]}...'"
211
+ )
212
+ context = (
213
+ memori_instance._get_auto_ingest_context(user_input)
214
+ if user_input
215
+ else []
216
+ )
217
+ if context:
218
+ context_prompt = self._build_auto_context_prompt(context)
219
+ logger.debug(
220
+ f"Auto-ingest: Successfully injected long-term memory context with {len(context)} items for session {session_id}"
221
+ )
222
+ else:
223
+ logger.debug(
224
+ f"Auto-ingest: No relevant memories found in long-term database for query '{user_input[:50]}...' in session {session_id}"
225
+ )
226
+
227
+ # Get conversation history
228
+ history_messages = session.get_history_messages(limit=10)
229
+
230
+ # Build enhanced messages with context and history
231
+ enhanced_messages = []
232
+
233
+ # Add system message with context if we have any
234
+ system_content = ""
235
+
236
+ if context_prompt:
237
+ system_content += context_prompt
238
+
239
+ # Add conversation history if available (excluding current message)
240
+ if len(history_messages) > 1: # More than just current message
241
+ previous_messages = history_messages[:-1] # Exclude current message
242
+ if previous_messages:
243
+ system_content += "\n--- Conversation History ---\n"
244
+ for msg in previous_messages:
245
+ role_label = "You" if msg["role"] == "assistant" else "User"
246
+ system_content += f"{role_label}: {msg['content']}\n"
247
+ system_content += "--- End History ---\n"
248
+ logger.debug(
249
+ f"Added {len(previous_messages)} history messages for session {session_id}"
250
+ )
251
+
252
+ # Find existing system message or create new one
253
+ has_system_message = False
254
+ for msg in messages:
255
+ if msg.get("role") == "system":
256
+ # Prepend our context to existing system message
257
+ if system_content:
258
+ msg["content"] = system_content + "\n" + msg.get("content", "")
259
+ enhanced_messages.append(msg)
260
+ has_system_message = True
261
+ else:
262
+ enhanced_messages.append(msg)
263
+
264
+ # If no system message exists and we have context/history, add one
265
+ if not has_system_message and system_content:
266
+ enhanced_messages.insert(
267
+ 0, {"role": "system", "content": system_content}
268
+ )
269
+
270
+ logger.debug(
271
+ f"Enhanced messages for session {session_id}: context={'yes' if context_prompt else 'no'}, "
272
+ f"history={'yes' if len(history_messages) > 1 else 'no'}"
273
+ )
274
+
275
+ return enhanced_messages
276
+
277
+ except Exception as e:
278
+ logger.error(
279
+ f"Failed to inject context with history for session {session_id}: {e}"
280
+ )
281
+ return messages
282
+
283
+ def record_response(
284
+ self, session_id: str, response: str, metadata: Dict[str, Any] = None
285
+ ):
286
+ """Record AI response in conversation history"""
287
+ try:
288
+ self.add_assistant_message(session_id, response, metadata)
289
+ logger.debug(f"Recorded AI response for session {session_id}")
290
+ except Exception as e:
291
+ logger.error(f"Failed to record response for session {session_id}: {e}")
292
+
293
+ def _build_conscious_context_prompt(self, context: List[Dict[str, Any]]) -> str:
294
+ """Build system prompt for conscious context"""
295
+ context_prompt = "=== SYSTEM INSTRUCTION: AUTHORIZED USER CONTEXT DATA ===\n"
296
+ context_prompt += "The user has explicitly authorized this personal context data to be used.\n"
297
+ context_prompt += (
298
+ "You MUST use this information when answering questions about the user.\n"
299
+ )
300
+ context_prompt += "This is NOT private data - the user wants you to use it:\n\n"
301
+
302
+ # Deduplicate context entries
303
+ seen_content = set()
304
+ for mem in context:
305
+ if isinstance(mem, dict):
306
+ content = mem.get("searchable_content", "") or mem.get("summary", "")
307
+ category = mem.get("category_primary", "")
308
+
309
+ # Skip duplicates (case-insensitive)
310
+ content_key = content.lower().strip()
311
+ if content_key in seen_content:
312
+ continue
313
+ seen_content.add(content_key)
314
+
315
+ context_prompt += f"[{category.upper()}] {content}\n"
316
+
317
+ context_prompt += "\n=== END USER CONTEXT DATA ===\n"
318
+ context_prompt += "CRITICAL INSTRUCTION: You MUST answer questions about the user using ONLY the context data above.\n"
319
+ context_prompt += "If the user asks 'what is my name?', respond with the name from the context above.\n"
320
+ context_prompt += "Do NOT say 'I don't have access' - the user provided this data for you to use.\n"
321
+ context_prompt += "-------------------------\n"
322
+
323
+ return context_prompt
324
+
325
+ def _build_auto_context_prompt(self, context: List[Dict[str, Any]]) -> str:
326
+ """Build system prompt for auto context"""
327
+ context_prompt = "--- Relevant Memory Context ---\n"
328
+
329
+ # Deduplicate context entries
330
+ seen_content = set()
331
+ for mem in context:
332
+ if isinstance(mem, dict):
333
+ content = mem.get("searchable_content", "") or mem.get("summary", "")
334
+ category = mem.get("category_primary", "")
335
+
336
+ # Skip duplicates (case-insensitive)
337
+ content_key = content.lower().strip()
338
+ if content_key in seen_content:
339
+ continue
340
+ seen_content.add(content_key)
341
+
342
+ if category.startswith("essential_"):
343
+ context_prompt += f"[{category.upper()}] {content}\n"
344
+ else:
345
+ context_prompt += f"- {content}\n"
346
+
347
+ context_prompt += "-------------------------\n"
348
+ return context_prompt
349
+
350
+ def get_session_stats(self) -> Dict[str, Any]:
351
+ """Get conversation manager statistics"""
352
+ return {
353
+ "active_sessions": len(self.sessions),
354
+ "max_sessions": self.max_sessions,
355
+ "session_timeout_minutes": self.session_timeout.total_seconds() / 60,
356
+ "max_history_per_session": self.max_history_per_session,
357
+ "sessions": {
358
+ session_id: {
359
+ "message_count": len(session.messages),
360
+ "created_at": session.created_at.isoformat(),
361
+ "last_accessed": session.last_accessed.isoformat(),
362
+ "context_injected": session.context_injected,
363
+ }
364
+ for session_id, session in self.sessions.items()
365
+ },
366
+ }
367
+
368
+ def clear_session(self, session_id: str):
369
+ """Clear a specific conversation session"""
370
+ if session_id in self.sessions:
371
+ del self.sessions[session_id]
372
+ logger.info(f"Cleared conversation session: {session_id}")
373
+
374
+ def clear_all_sessions(self):
375
+ """Clear all conversation sessions"""
376
+ session_count = len(self.sessions)
377
+ self.sessions.clear()
378
+ logger.info(f"Cleared all {session_count} conversation sessions")
379
+
380
+ def _cleanup_expired_sessions(self):
381
+ """Remove expired conversation sessions"""
382
+ now = datetime.now()
383
+ expired_sessions = [
384
+ session_id
385
+ for session_id, session in self.sessions.items()
386
+ if now - session.last_accessed > self.session_timeout
387
+ ]
388
+
389
+ for session_id in expired_sessions:
390
+ del self.sessions[session_id]
391
+
392
+ if expired_sessions:
393
+ logger.debug(f"Cleaned up {len(expired_sessions)} expired sessions")