memorisdk 1.0.2__py3-none-any.whl → 2.0.1__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.
- memori/__init__.py +24 -8
- memori/agents/conscious_agent.py +252 -414
- memori/agents/memory_agent.py +487 -224
- memori/agents/retrieval_agent.py +491 -68
- memori/config/memory_manager.py +323 -0
- memori/core/conversation.py +393 -0
- memori/core/database.py +386 -371
- memori/core/memory.py +1683 -532
- memori/core/providers.py +217 -0
- memori/database/adapters/__init__.py +10 -0
- memori/database/adapters/mysql_adapter.py +331 -0
- memori/database/adapters/postgresql_adapter.py +291 -0
- memori/database/adapters/sqlite_adapter.py +229 -0
- memori/database/auto_creator.py +320 -0
- memori/database/connection_utils.py +207 -0
- memori/database/connectors/base_connector.py +283 -0
- memori/database/connectors/mysql_connector.py +240 -18
- memori/database/connectors/postgres_connector.py +277 -4
- memori/database/connectors/sqlite_connector.py +178 -3
- memori/database/models.py +400 -0
- memori/database/queries/base_queries.py +1 -1
- memori/database/queries/memory_queries.py +91 -2
- memori/database/query_translator.py +222 -0
- memori/database/schema_generators/__init__.py +7 -0
- memori/database/schema_generators/mysql_schema_generator.py +215 -0
- memori/database/search/__init__.py +8 -0
- memori/database/search/mysql_search_adapter.py +255 -0
- memori/database/search/sqlite_search_adapter.py +180 -0
- memori/database/search_service.py +700 -0
- memori/database/sqlalchemy_manager.py +888 -0
- memori/integrations/__init__.py +36 -11
- memori/integrations/litellm_integration.py +340 -6
- memori/integrations/openai_integration.py +506 -240
- memori/tools/memory_tool.py +94 -4
- memori/utils/input_validator.py +395 -0
- memori/utils/pydantic_models.py +138 -36
- memori/utils/query_builder.py +530 -0
- memori/utils/security_audit.py +594 -0
- memori/utils/security_integration.py +339 -0
- memori/utils/transaction_manager.py +547 -0
- {memorisdk-1.0.2.dist-info → memorisdk-2.0.1.dist-info}/METADATA +56 -23
- memorisdk-2.0.1.dist-info/RECORD +66 -0
- memori/scripts/llm_text.py +0 -50
- memorisdk-1.0.2.dist-info/RECORD +0 -44
- memorisdk-1.0.2.dist-info/entry_points.txt +0 -2
- {memorisdk-1.0.2.dist-info → memorisdk-2.0.1.dist-info}/WHEEL +0 -0
- {memorisdk-1.0.2.dist-info → memorisdk-2.0.1.dist-info}/licenses/LICENSE +0 -0
- {memorisdk-1.0.2.dist-info → memorisdk-2.0.1.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")
|