memorisdk 1.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.
- memoriai/__init__.py +140 -0
- memoriai/agents/__init__.py +7 -0
- memoriai/agents/conscious_agent.py +506 -0
- memoriai/agents/memory_agent.py +322 -0
- memoriai/agents/retrieval_agent.py +579 -0
- memoriai/config/__init__.py +14 -0
- memoriai/config/manager.py +281 -0
- memoriai/config/settings.py +287 -0
- memoriai/core/__init__.py +6 -0
- memoriai/core/database.py +966 -0
- memoriai/core/memory.py +1349 -0
- memoriai/database/__init__.py +5 -0
- memoriai/database/connectors/__init__.py +9 -0
- memoriai/database/connectors/mysql_connector.py +159 -0
- memoriai/database/connectors/postgres_connector.py +158 -0
- memoriai/database/connectors/sqlite_connector.py +148 -0
- memoriai/database/queries/__init__.py +15 -0
- memoriai/database/queries/base_queries.py +204 -0
- memoriai/database/queries/chat_queries.py +157 -0
- memoriai/database/queries/entity_queries.py +236 -0
- memoriai/database/queries/memory_queries.py +178 -0
- memoriai/database/templates/__init__.py +0 -0
- memoriai/database/templates/basic_template.py +0 -0
- memoriai/database/templates/schemas/__init__.py +0 -0
- memoriai/integrations/__init__.py +68 -0
- memoriai/integrations/anthropic_integration.py +194 -0
- memoriai/integrations/litellm_integration.py +11 -0
- memoriai/integrations/openai_integration.py +273 -0
- memoriai/scripts/llm_text.py +50 -0
- memoriai/tools/__init__.py +5 -0
- memoriai/tools/memory_tool.py +544 -0
- memoriai/utils/__init__.py +89 -0
- memoriai/utils/exceptions.py +418 -0
- memoriai/utils/helpers.py +433 -0
- memoriai/utils/logging.py +204 -0
- memoriai/utils/pydantic_models.py +258 -0
- memoriai/utils/schemas.py +0 -0
- memoriai/utils/validators.py +339 -0
- memorisdk-1.0.0.dist-info/METADATA +386 -0
- memorisdk-1.0.0.dist-info/RECORD +44 -0
- memorisdk-1.0.0.dist-info/WHEEL +5 -0
- memorisdk-1.0.0.dist-info/entry_points.txt +2 -0
- memorisdk-1.0.0.dist-info/licenses/LICENSE +203 -0
- memorisdk-1.0.0.dist-info/top_level.txt +1 -0
memoriai/core/memory.py
ADDED
|
@@ -0,0 +1,1349 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Main Memori class - Pydantic-based memory interface v1.0
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import uuid
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from typing import Any, Dict, List, Optional
|
|
9
|
+
|
|
10
|
+
from loguru import logger
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
import litellm
|
|
14
|
+
from litellm import success_callback
|
|
15
|
+
|
|
16
|
+
LITELLM_AVAILABLE = True
|
|
17
|
+
except ImportError:
|
|
18
|
+
LITELLM_AVAILABLE = False
|
|
19
|
+
logger.warning("LiteLLM not available - native callback system disabled")
|
|
20
|
+
|
|
21
|
+
from ..agents.conscious_agent import ConsciouscAgent
|
|
22
|
+
from ..agents.memory_agent import MemoryAgent
|
|
23
|
+
from ..agents.retrieval_agent import MemorySearchEngine
|
|
24
|
+
from ..config.settings import LoggingSettings, LogLevel
|
|
25
|
+
from ..utils.exceptions import DatabaseError, MemoriError
|
|
26
|
+
from ..utils.logging import LoggingManager
|
|
27
|
+
from ..utils.pydantic_models import ConversationContext
|
|
28
|
+
from .database import DatabaseManager
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Memori:
|
|
32
|
+
"""
|
|
33
|
+
The main Memori memory layer for AI agents.
|
|
34
|
+
|
|
35
|
+
Provides persistent memory storage, categorization, and retrieval
|
|
36
|
+
for AI conversations and agent interactions.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
database_connect: str = "sqlite:///memori.db",
|
|
42
|
+
template: str = "basic",
|
|
43
|
+
mem_prompt: Optional[str] = None,
|
|
44
|
+
conscious_ingest: bool = False,
|
|
45
|
+
auto_ingest: bool = False,
|
|
46
|
+
namespace: Optional[str] = None,
|
|
47
|
+
shared_memory: bool = False,
|
|
48
|
+
memory_filters: Optional[Dict[str, Any]] = None,
|
|
49
|
+
openai_api_key: Optional[str] = None,
|
|
50
|
+
user_id: Optional[str] = None,
|
|
51
|
+
verbose: bool = False,
|
|
52
|
+
):
|
|
53
|
+
"""
|
|
54
|
+
Initialize Memori memory system v1.0.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
database_connect: Database connection string
|
|
58
|
+
template: Memory template to use ('basic')
|
|
59
|
+
mem_prompt: Optional prompt to guide memory recording
|
|
60
|
+
conscious_ingest: Enable one-shot short-term memory context injection at conversation start
|
|
61
|
+
auto_ingest: Enable automatic memory injection on every LLM call
|
|
62
|
+
namespace: Optional namespace for memory isolation
|
|
63
|
+
shared_memory: Enable shared memory across agents
|
|
64
|
+
memory_filters: Filters for memory ingestion
|
|
65
|
+
openai_api_key: OpenAI API key for memory agent
|
|
66
|
+
user_id: Optional user identifier
|
|
67
|
+
verbose: Enable verbose logging (loguru only)
|
|
68
|
+
"""
|
|
69
|
+
self.database_connect = database_connect
|
|
70
|
+
self.template = template
|
|
71
|
+
self.mem_prompt = mem_prompt
|
|
72
|
+
self.conscious_ingest = conscious_ingest
|
|
73
|
+
self.auto_ingest = auto_ingest
|
|
74
|
+
self.namespace = namespace or "default"
|
|
75
|
+
self.shared_memory = shared_memory
|
|
76
|
+
self.memory_filters = memory_filters or {}
|
|
77
|
+
self.openai_api_key = openai_api_key
|
|
78
|
+
self.user_id = user_id
|
|
79
|
+
self.verbose = verbose
|
|
80
|
+
|
|
81
|
+
# Setup logging based on verbose mode
|
|
82
|
+
self._setup_logging()
|
|
83
|
+
|
|
84
|
+
# Initialize database manager
|
|
85
|
+
self.db_manager = DatabaseManager(database_connect, template)
|
|
86
|
+
|
|
87
|
+
# Initialize Pydantic-based agents
|
|
88
|
+
self.memory_agent = None
|
|
89
|
+
self.search_engine = None
|
|
90
|
+
self.conscious_agent = None
|
|
91
|
+
self._background_task = None
|
|
92
|
+
|
|
93
|
+
if conscious_ingest or auto_ingest:
|
|
94
|
+
try:
|
|
95
|
+
# Initialize Pydantic-based agents
|
|
96
|
+
self.memory_agent = MemoryAgent(api_key=openai_api_key, model="gpt-4o")
|
|
97
|
+
self.search_engine = MemorySearchEngine(
|
|
98
|
+
api_key=openai_api_key, model="gpt-4o"
|
|
99
|
+
)
|
|
100
|
+
self.conscious_agent = ConsciouscAgent(
|
|
101
|
+
api_key=openai_api_key, model="gpt-4o"
|
|
102
|
+
)
|
|
103
|
+
logger.info(
|
|
104
|
+
"Pydantic-based memory, search, and conscious agents initialized"
|
|
105
|
+
)
|
|
106
|
+
except Exception as e:
|
|
107
|
+
logger.warning(
|
|
108
|
+
f"Failed to initialize OpenAI agents: {e}. Memory ingestion disabled."
|
|
109
|
+
)
|
|
110
|
+
self.conscious_ingest = False
|
|
111
|
+
self.auto_ingest = False
|
|
112
|
+
|
|
113
|
+
# State tracking
|
|
114
|
+
self._enabled = False
|
|
115
|
+
self._session_id = str(uuid.uuid4())
|
|
116
|
+
self._conscious_context_injected = (
|
|
117
|
+
False # Track if conscious context was already injected
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# User context for memory processing
|
|
121
|
+
self._user_context = {
|
|
122
|
+
"current_projects": [],
|
|
123
|
+
"relevant_skills": [],
|
|
124
|
+
"user_preferences": [],
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
# Initialize database
|
|
128
|
+
self._setup_database()
|
|
129
|
+
|
|
130
|
+
# Run conscious agent initialization if enabled
|
|
131
|
+
if self.conscious_ingest and self.conscious_agent:
|
|
132
|
+
self._initialize_conscious_memory()
|
|
133
|
+
|
|
134
|
+
logger.info(
|
|
135
|
+
f"Memori v1.0 initialized with template: {template}, namespace: {namespace}"
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
def _setup_logging(self):
|
|
139
|
+
"""Setup logging configuration based on verbose mode"""
|
|
140
|
+
if not LoggingManager.is_initialized():
|
|
141
|
+
# Create default logging settings
|
|
142
|
+
logging_settings = LoggingSettings()
|
|
143
|
+
|
|
144
|
+
# If verbose mode is enabled, set logging level to DEBUG
|
|
145
|
+
if self.verbose:
|
|
146
|
+
logging_settings.level = LogLevel.DEBUG
|
|
147
|
+
|
|
148
|
+
# Setup logging with verbose mode
|
|
149
|
+
LoggingManager.setup_logging(logging_settings, verbose=self.verbose)
|
|
150
|
+
|
|
151
|
+
if self.verbose:
|
|
152
|
+
logger.info(
|
|
153
|
+
"Verbose logging enabled - only loguru logs will be displayed"
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
def _setup_database(self):
|
|
157
|
+
"""Setup database tables based on template"""
|
|
158
|
+
try:
|
|
159
|
+
self.db_manager.initialize_schema()
|
|
160
|
+
logger.info("Database schema initialized successfully")
|
|
161
|
+
except Exception as e:
|
|
162
|
+
raise DatabaseError(f"Failed to setup database: {e}")
|
|
163
|
+
|
|
164
|
+
def _initialize_conscious_memory(self):
|
|
165
|
+
"""Initialize conscious memory by running conscious agent analysis"""
|
|
166
|
+
try:
|
|
167
|
+
logger.info(
|
|
168
|
+
"Conscious-ingest: Starting conscious agent analysis at startup"
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
# Run conscious agent analysis in background
|
|
172
|
+
if self._background_task is None or self._background_task.done():
|
|
173
|
+
self._background_task = asyncio.create_task(
|
|
174
|
+
self._run_conscious_initialization()
|
|
175
|
+
)
|
|
176
|
+
logger.debug("Conscious-ingest: Background initialization task started")
|
|
177
|
+
|
|
178
|
+
except Exception as e:
|
|
179
|
+
logger.error(f"Failed to initialize conscious memory: {e}")
|
|
180
|
+
|
|
181
|
+
async def _run_conscious_initialization(self):
|
|
182
|
+
"""Run conscious agent initialization in background"""
|
|
183
|
+
try:
|
|
184
|
+
if not self.conscious_agent:
|
|
185
|
+
return
|
|
186
|
+
|
|
187
|
+
logger.debug("Conscious-ingest: Running background analysis")
|
|
188
|
+
await self.conscious_agent.run_background_analysis(
|
|
189
|
+
self.db_manager, self.namespace
|
|
190
|
+
)
|
|
191
|
+
logger.info("Conscious-ingest: Background analysis completed")
|
|
192
|
+
|
|
193
|
+
except Exception as e:
|
|
194
|
+
logger.error(f"Conscious agent initialization failed: {e}")
|
|
195
|
+
|
|
196
|
+
def enable(self):
|
|
197
|
+
"""
|
|
198
|
+
Enable universal memory recording for ALL LLM providers.
|
|
199
|
+
|
|
200
|
+
This automatically sets up recording for:
|
|
201
|
+
- LiteLLM: Native callback system (recommended)
|
|
202
|
+
- OpenAI: Automatic client wrapping when instantiated
|
|
203
|
+
- Anthropic: Automatic client wrapping when instantiated
|
|
204
|
+
- Any other provider: Auto-detected and wrapped
|
|
205
|
+
"""
|
|
206
|
+
if self._enabled:
|
|
207
|
+
logger.warning("Memori is already enabled.")
|
|
208
|
+
return
|
|
209
|
+
|
|
210
|
+
self._enabled = True
|
|
211
|
+
self._session_id = str(uuid.uuid4())
|
|
212
|
+
|
|
213
|
+
# 1. Set up LiteLLM native callbacks (if available)
|
|
214
|
+
litellm_enabled = self._setup_litellm_callbacks()
|
|
215
|
+
|
|
216
|
+
# 2. Set up universal client interception for other providers
|
|
217
|
+
universal_enabled = self._setup_universal_interception()
|
|
218
|
+
|
|
219
|
+
# 3. Register this instance globally for any provider to use
|
|
220
|
+
self._register_global_instance()
|
|
221
|
+
|
|
222
|
+
# 4. Start background conscious agent if available
|
|
223
|
+
if self.conscious_ingest and self.conscious_agent:
|
|
224
|
+
self._start_background_analysis()
|
|
225
|
+
|
|
226
|
+
providers = []
|
|
227
|
+
if litellm_enabled:
|
|
228
|
+
providers.append("LiteLLM (native callbacks)")
|
|
229
|
+
if universal_enabled:
|
|
230
|
+
providers.append("OpenAI/Anthropic (auto-wrapping)")
|
|
231
|
+
|
|
232
|
+
logger.info(
|
|
233
|
+
f"Memori enabled for session: {self.session_id}\n"
|
|
234
|
+
f"Active providers: {', '.join(providers) if providers else 'None detected'}\n"
|
|
235
|
+
f"Background analysis: {'Active' if self._background_task else 'Disabled'}\n"
|
|
236
|
+
f"Usage: Simply use any LLM client normally - conversations will be auto-recorded!"
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
def disable(self):
|
|
240
|
+
"""
|
|
241
|
+
Disable universal memory recording for all providers.
|
|
242
|
+
"""
|
|
243
|
+
if not self._enabled:
|
|
244
|
+
return
|
|
245
|
+
|
|
246
|
+
# 1. Remove LiteLLM callbacks and restore original completion
|
|
247
|
+
if LITELLM_AVAILABLE:
|
|
248
|
+
try:
|
|
249
|
+
success_callback.remove(self._litellm_success_callback)
|
|
250
|
+
except ValueError:
|
|
251
|
+
pass
|
|
252
|
+
|
|
253
|
+
# Restore original completion function if we patched it
|
|
254
|
+
if hasattr(litellm, "completion") and hasattr(
|
|
255
|
+
litellm.completion, "_memori_patched"
|
|
256
|
+
):
|
|
257
|
+
# Note: We can't easily restore the original function in a multi-instance scenario
|
|
258
|
+
# This is a limitation of the monkey-patching approach
|
|
259
|
+
pass
|
|
260
|
+
|
|
261
|
+
# 2. Disable universal interception
|
|
262
|
+
self._disable_universal_interception()
|
|
263
|
+
|
|
264
|
+
# 3. Unregister global instance
|
|
265
|
+
self._unregister_global_instance()
|
|
266
|
+
|
|
267
|
+
# 4. Stop background analysis task
|
|
268
|
+
self._stop_background_analysis()
|
|
269
|
+
|
|
270
|
+
self._enabled = False
|
|
271
|
+
logger.info("Memori disabled for all providers.")
|
|
272
|
+
|
|
273
|
+
def _setup_litellm_callbacks(self) -> bool:
|
|
274
|
+
"""Set up LiteLLM native callback system"""
|
|
275
|
+
if not LITELLM_AVAILABLE:
|
|
276
|
+
logger.debug("LiteLLM not available, skipping native callbacks")
|
|
277
|
+
return False
|
|
278
|
+
|
|
279
|
+
try:
|
|
280
|
+
success_callback.append(self._litellm_success_callback)
|
|
281
|
+
|
|
282
|
+
# Set up context injection by monkey-patching completion function
|
|
283
|
+
if hasattr(litellm, "completion") and not hasattr(
|
|
284
|
+
litellm.completion, "_memori_patched"
|
|
285
|
+
):
|
|
286
|
+
original_completion = litellm.completion
|
|
287
|
+
|
|
288
|
+
def memori_completion(*args, **kwargs):
|
|
289
|
+
# Inject context based on ingestion mode
|
|
290
|
+
if self._enabled:
|
|
291
|
+
if self.auto_ingest:
|
|
292
|
+
# Auto-inject: continuous memory injection on every call
|
|
293
|
+
kwargs = self._inject_litellm_context(kwargs, mode="auto")
|
|
294
|
+
elif self.conscious_ingest:
|
|
295
|
+
# Conscious-inject: one-shot short-term memory context
|
|
296
|
+
kwargs = self._inject_litellm_context(
|
|
297
|
+
kwargs, mode="conscious"
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
# Call original completion
|
|
301
|
+
return original_completion(*args, **kwargs)
|
|
302
|
+
|
|
303
|
+
litellm.completion = memori_completion
|
|
304
|
+
litellm.completion._memori_patched = True
|
|
305
|
+
logger.debug(
|
|
306
|
+
"LiteLLM completion function patched for context injection"
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
logger.debug("LiteLLM native callbacks registered")
|
|
310
|
+
return True
|
|
311
|
+
except Exception as e:
|
|
312
|
+
logger.error(f"Failed to setup LiteLLM callbacks: {e}")
|
|
313
|
+
return False
|
|
314
|
+
|
|
315
|
+
def _setup_universal_interception(self) -> bool:
|
|
316
|
+
"""Set up universal client interception for OpenAI, Anthropic, etc."""
|
|
317
|
+
try:
|
|
318
|
+
# Use Python's import hook system to intercept client creation
|
|
319
|
+
self._install_import_hooks()
|
|
320
|
+
logger.debug("Universal client interception enabled")
|
|
321
|
+
return True
|
|
322
|
+
except Exception as e:
|
|
323
|
+
logger.error(f"Failed to setup universal interception: {e}")
|
|
324
|
+
return False
|
|
325
|
+
|
|
326
|
+
def _get_builtin_import(self):
|
|
327
|
+
"""Safely get __import__ from __builtins__ (handles both dict and module cases)"""
|
|
328
|
+
if isinstance(__builtins__, dict):
|
|
329
|
+
return __builtins__["__import__"]
|
|
330
|
+
else:
|
|
331
|
+
return __builtins__.__import__
|
|
332
|
+
|
|
333
|
+
def _set_builtin_import(self, import_func):
|
|
334
|
+
"""Safely set __import__ in __builtins__ (handles both dict and module cases)"""
|
|
335
|
+
if isinstance(__builtins__, dict):
|
|
336
|
+
__builtins__["__import__"] = import_func
|
|
337
|
+
else:
|
|
338
|
+
__builtins__.__import__ = import_func
|
|
339
|
+
|
|
340
|
+
def _install_import_hooks(self):
|
|
341
|
+
"""Install import hooks to automatically wrap LLM clients"""
|
|
342
|
+
|
|
343
|
+
# Store original __import__ if not already done
|
|
344
|
+
if not hasattr(self, "_original_import"):
|
|
345
|
+
self._original_import = self._get_builtin_import()
|
|
346
|
+
|
|
347
|
+
def memori_import_hook(name, globals=None, locals=None, fromlist=(), level=0):
|
|
348
|
+
"""Custom import hook that wraps LLM clients automatically"""
|
|
349
|
+
module = self._original_import(name, globals, locals, fromlist, level)
|
|
350
|
+
|
|
351
|
+
# Only process if memori is enabled and this is an LLM module
|
|
352
|
+
if not self._enabled:
|
|
353
|
+
return module
|
|
354
|
+
|
|
355
|
+
# Auto-wrap OpenAI clients
|
|
356
|
+
if name == "openai" or (fromlist and "openai" in name):
|
|
357
|
+
self._wrap_openai_module(module)
|
|
358
|
+
|
|
359
|
+
# Auto-wrap Anthropic clients
|
|
360
|
+
elif name == "anthropic" or (fromlist and "anthropic" in name):
|
|
361
|
+
self._wrap_anthropic_module(module)
|
|
362
|
+
|
|
363
|
+
return module
|
|
364
|
+
|
|
365
|
+
# Install the hook
|
|
366
|
+
self._set_builtin_import(memori_import_hook)
|
|
367
|
+
|
|
368
|
+
def _wrap_openai_module(self, module):
|
|
369
|
+
"""Automatically wrap OpenAI client when imported"""
|
|
370
|
+
try:
|
|
371
|
+
if hasattr(module, "OpenAI") and not hasattr(
|
|
372
|
+
module.OpenAI, "_memori_wrapped"
|
|
373
|
+
):
|
|
374
|
+
original_init = module.OpenAI.__init__
|
|
375
|
+
|
|
376
|
+
def wrapped_init(self_client, *args, **kwargs):
|
|
377
|
+
# Call original init
|
|
378
|
+
result = original_init(self_client, *args, **kwargs)
|
|
379
|
+
|
|
380
|
+
# Wrap the client methods for automatic recording
|
|
381
|
+
if hasattr(self_client, "chat") and hasattr(
|
|
382
|
+
self_client.chat, "completions"
|
|
383
|
+
):
|
|
384
|
+
original_create = self_client.chat.completions.create
|
|
385
|
+
|
|
386
|
+
def wrapped_create(*args, **kwargs):
|
|
387
|
+
# Inject context if conscious ingestion is enabled
|
|
388
|
+
if self.is_enabled and self.conscious_ingest:
|
|
389
|
+
kwargs = self._inject_openai_context(kwargs)
|
|
390
|
+
|
|
391
|
+
# Make the call
|
|
392
|
+
response = original_create(*args, **kwargs)
|
|
393
|
+
|
|
394
|
+
# Record if enabled
|
|
395
|
+
if self.is_enabled:
|
|
396
|
+
self._record_openai_conversation(kwargs, response)
|
|
397
|
+
|
|
398
|
+
return response
|
|
399
|
+
|
|
400
|
+
self_client.chat.completions.create = wrapped_create
|
|
401
|
+
|
|
402
|
+
return result
|
|
403
|
+
|
|
404
|
+
module.OpenAI.__init__ = wrapped_init
|
|
405
|
+
module.OpenAI._memori_wrapped = True
|
|
406
|
+
logger.debug("OpenAI client auto-wrapping enabled")
|
|
407
|
+
|
|
408
|
+
except Exception as e:
|
|
409
|
+
logger.debug(f"Could not wrap OpenAI module: {e}")
|
|
410
|
+
|
|
411
|
+
def _wrap_anthropic_module(self, module):
|
|
412
|
+
"""Automatically wrap Anthropic client when imported"""
|
|
413
|
+
try:
|
|
414
|
+
if hasattr(module, "Anthropic") and not hasattr(
|
|
415
|
+
module.Anthropic, "_memori_wrapped"
|
|
416
|
+
):
|
|
417
|
+
original_init = module.Anthropic.__init__
|
|
418
|
+
|
|
419
|
+
def wrapped_init(self_client, *args, **kwargs):
|
|
420
|
+
# Call original init
|
|
421
|
+
result = original_init(self_client, *args, **kwargs)
|
|
422
|
+
|
|
423
|
+
# Wrap the messages.create method
|
|
424
|
+
if hasattr(self_client, "messages"):
|
|
425
|
+
original_create = self_client.messages.create
|
|
426
|
+
|
|
427
|
+
def wrapped_create(*args, **kwargs):
|
|
428
|
+
# Inject context if conscious ingestion is enabled
|
|
429
|
+
if self.is_enabled and self.conscious_ingest:
|
|
430
|
+
kwargs = self._inject_anthropic_context(kwargs)
|
|
431
|
+
|
|
432
|
+
# Make the call
|
|
433
|
+
response = original_create(*args, **kwargs)
|
|
434
|
+
|
|
435
|
+
# Record if enabled
|
|
436
|
+
if self.is_enabled:
|
|
437
|
+
self._record_anthropic_conversation(kwargs, response)
|
|
438
|
+
|
|
439
|
+
return response
|
|
440
|
+
|
|
441
|
+
self_client.messages.create = wrapped_create
|
|
442
|
+
|
|
443
|
+
return result
|
|
444
|
+
|
|
445
|
+
module.Anthropic.__init__ = wrapped_init
|
|
446
|
+
module.Anthropic._memori_wrapped = True
|
|
447
|
+
logger.debug("Anthropic client auto-wrapping enabled")
|
|
448
|
+
|
|
449
|
+
except Exception as e:
|
|
450
|
+
logger.debug(f"Could not wrap Anthropic module: {e}")
|
|
451
|
+
|
|
452
|
+
def _disable_universal_interception(self):
|
|
453
|
+
"""Disable universal client interception"""
|
|
454
|
+
try:
|
|
455
|
+
# Restore original import if we modified it
|
|
456
|
+
if hasattr(self, "_original_import"):
|
|
457
|
+
self._set_builtin_import(self._original_import)
|
|
458
|
+
delattr(self, "_original_import")
|
|
459
|
+
logger.debug("Universal interception disabled")
|
|
460
|
+
except Exception as e:
|
|
461
|
+
logger.debug(f"Error disabling universal interception: {e}")
|
|
462
|
+
|
|
463
|
+
def _register_global_instance(self):
|
|
464
|
+
"""Register this memori instance globally"""
|
|
465
|
+
# Store in a global registry that wrapped clients can access
|
|
466
|
+
if not hasattr(Memori, "_global_instances"):
|
|
467
|
+
Memori._global_instances = []
|
|
468
|
+
Memori._global_instances.append(self)
|
|
469
|
+
|
|
470
|
+
def _unregister_global_instance(self):
|
|
471
|
+
"""Unregister this memori instance globally"""
|
|
472
|
+
if hasattr(Memori, "_global_instances") and self in Memori._global_instances:
|
|
473
|
+
Memori._global_instances.remove(self)
|
|
474
|
+
|
|
475
|
+
def _inject_openai_context(self, kwargs):
|
|
476
|
+
"""Inject context for OpenAI calls"""
|
|
477
|
+
try:
|
|
478
|
+
# Extract user input from messages
|
|
479
|
+
user_input = ""
|
|
480
|
+
for msg in reversed(kwargs.get("messages", [])):
|
|
481
|
+
if msg.get("role") == "user":
|
|
482
|
+
user_input = msg.get("content", "")
|
|
483
|
+
break
|
|
484
|
+
|
|
485
|
+
if user_input:
|
|
486
|
+
context = self.retrieve_context(user_input, limit=3)
|
|
487
|
+
if context:
|
|
488
|
+
context_prompt = "--- Relevant Memories ---\n"
|
|
489
|
+
for mem in context:
|
|
490
|
+
if isinstance(mem, dict):
|
|
491
|
+
summary = mem.get("summary", "") or mem.get("content", "")
|
|
492
|
+
context_prompt += f"- {summary}\n"
|
|
493
|
+
else:
|
|
494
|
+
context_prompt += f"- {str(mem)}\n"
|
|
495
|
+
context_prompt += "-------------------------\n"
|
|
496
|
+
|
|
497
|
+
# Inject into system message
|
|
498
|
+
messages = kwargs.get("messages", [])
|
|
499
|
+
for msg in messages:
|
|
500
|
+
if msg.get("role") == "system":
|
|
501
|
+
msg["content"] = context_prompt + msg.get("content", "")
|
|
502
|
+
break
|
|
503
|
+
else:
|
|
504
|
+
messages.insert(
|
|
505
|
+
0, {"role": "system", "content": context_prompt}
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
logger.debug(f"Injected context: {len(context)} memories")
|
|
509
|
+
except Exception as e:
|
|
510
|
+
logger.error(f"Context injection failed: {e}")
|
|
511
|
+
return kwargs
|
|
512
|
+
|
|
513
|
+
def _inject_anthropic_context(self, kwargs):
|
|
514
|
+
"""Inject context for Anthropic calls"""
|
|
515
|
+
try:
|
|
516
|
+
# Extract user input from messages
|
|
517
|
+
user_input = ""
|
|
518
|
+
for msg in reversed(kwargs.get("messages", [])):
|
|
519
|
+
if msg.get("role") == "user":
|
|
520
|
+
content = msg.get("content", "")
|
|
521
|
+
if isinstance(content, list):
|
|
522
|
+
user_input = " ".join(
|
|
523
|
+
[
|
|
524
|
+
block.get("text", "")
|
|
525
|
+
for block in content
|
|
526
|
+
if isinstance(block, dict)
|
|
527
|
+
and block.get("type") == "text"
|
|
528
|
+
]
|
|
529
|
+
)
|
|
530
|
+
else:
|
|
531
|
+
user_input = content
|
|
532
|
+
break
|
|
533
|
+
|
|
534
|
+
if user_input:
|
|
535
|
+
context = self.retrieve_context(user_input, limit=3)
|
|
536
|
+
if context:
|
|
537
|
+
context_prompt = "--- Relevant Memories ---\n"
|
|
538
|
+
for mem in context:
|
|
539
|
+
if isinstance(mem, dict):
|
|
540
|
+
summary = mem.get("summary", "") or mem.get("content", "")
|
|
541
|
+
context_prompt += f"- {summary}\n"
|
|
542
|
+
else:
|
|
543
|
+
context_prompt += f"- {str(mem)}\n"
|
|
544
|
+
context_prompt += "-------------------------\n"
|
|
545
|
+
|
|
546
|
+
# Inject into system parameter
|
|
547
|
+
if kwargs.get("system"):
|
|
548
|
+
kwargs["system"] = context_prompt + kwargs["system"]
|
|
549
|
+
else:
|
|
550
|
+
kwargs["system"] = context_prompt
|
|
551
|
+
|
|
552
|
+
logger.debug(f"Injected context: {len(context)} memories")
|
|
553
|
+
except Exception as e:
|
|
554
|
+
logger.error(f"Context injection failed: {e}")
|
|
555
|
+
return kwargs
|
|
556
|
+
|
|
557
|
+
def _inject_litellm_context(self, params, mode="auto"):
|
|
558
|
+
"""
|
|
559
|
+
Inject context for LiteLLM calls based on mode
|
|
560
|
+
|
|
561
|
+
Args:
|
|
562
|
+
params: LiteLLM parameters
|
|
563
|
+
mode: "conscious" (one-shot short-term) or "auto" (continuous retrieval)
|
|
564
|
+
"""
|
|
565
|
+
try:
|
|
566
|
+
# Extract user input from messages
|
|
567
|
+
user_input = ""
|
|
568
|
+
messages = params.get("messages", [])
|
|
569
|
+
|
|
570
|
+
for msg in reversed(messages):
|
|
571
|
+
if msg.get("role") == "user":
|
|
572
|
+
user_input = msg.get("content", "")
|
|
573
|
+
break
|
|
574
|
+
|
|
575
|
+
if user_input:
|
|
576
|
+
if mode == "conscious":
|
|
577
|
+
# Conscious mode: inject short-term memory only once at conversation start
|
|
578
|
+
if not self._conscious_context_injected:
|
|
579
|
+
context = self._get_conscious_context()
|
|
580
|
+
self._conscious_context_injected = True
|
|
581
|
+
logger.debug("Conscious context injected (one-shot)")
|
|
582
|
+
else:
|
|
583
|
+
context = [] # Already injected, don't inject again
|
|
584
|
+
elif mode == "auto":
|
|
585
|
+
# Auto mode: use retrieval agent for intelligent database search
|
|
586
|
+
if self.search_engine:
|
|
587
|
+
context = self._get_auto_ingest_context(user_input)
|
|
588
|
+
else:
|
|
589
|
+
# Fallback to basic retrieval
|
|
590
|
+
context = self.retrieve_context(user_input, limit=5)
|
|
591
|
+
else:
|
|
592
|
+
context = []
|
|
593
|
+
|
|
594
|
+
if context:
|
|
595
|
+
context_prompt = f"--- {mode.capitalize()} Memory Context ---\n"
|
|
596
|
+
for mem in context:
|
|
597
|
+
if isinstance(mem, dict):
|
|
598
|
+
summary = mem.get("summary", "") or mem.get(
|
|
599
|
+
"searchable_content", ""
|
|
600
|
+
)
|
|
601
|
+
category = mem.get("category_primary", "")
|
|
602
|
+
if category.startswith("essential_") or mode == "conscious":
|
|
603
|
+
context_prompt += f"[{category.upper()}] {summary}\n"
|
|
604
|
+
else:
|
|
605
|
+
context_prompt += f"- {summary}\n"
|
|
606
|
+
context_prompt += "-------------------------\n"
|
|
607
|
+
|
|
608
|
+
# Inject into system message
|
|
609
|
+
for msg in messages:
|
|
610
|
+
if msg.get("role") == "system":
|
|
611
|
+
msg["content"] = context_prompt + msg.get("content", "")
|
|
612
|
+
break
|
|
613
|
+
else:
|
|
614
|
+
# No system message exists, add one
|
|
615
|
+
messages.insert(
|
|
616
|
+
0, {"role": "system", "content": context_prompt}
|
|
617
|
+
)
|
|
618
|
+
|
|
619
|
+
logger.debug(f"LiteLLM: Injected context with {len(context)} items")
|
|
620
|
+
else:
|
|
621
|
+
# No user input, but still inject essential conversations if available
|
|
622
|
+
if self.conscious_ingest:
|
|
623
|
+
essential_conversations = self.get_essential_conversations(limit=3)
|
|
624
|
+
if essential_conversations:
|
|
625
|
+
context_prompt = "--- Your Context ---\n"
|
|
626
|
+
for conv in essential_conversations:
|
|
627
|
+
summary = conv.get("summary", "") or conv.get(
|
|
628
|
+
"searchable_content", ""
|
|
629
|
+
)
|
|
630
|
+
context_prompt += f"[ESSENTIAL] {summary}\n"
|
|
631
|
+
context_prompt += "-------------------------\n"
|
|
632
|
+
|
|
633
|
+
# Inject into system message
|
|
634
|
+
for msg in messages:
|
|
635
|
+
if msg.get("role") == "system":
|
|
636
|
+
msg["content"] = context_prompt + msg.get("content", "")
|
|
637
|
+
break
|
|
638
|
+
else:
|
|
639
|
+
# No system message exists, add one
|
|
640
|
+
messages.insert(
|
|
641
|
+
0, {"role": "system", "content": context_prompt}
|
|
642
|
+
)
|
|
643
|
+
|
|
644
|
+
logger.debug(
|
|
645
|
+
f"LiteLLM: Injected {len(essential_conversations)} essential conversations"
|
|
646
|
+
)
|
|
647
|
+
|
|
648
|
+
except Exception as e:
|
|
649
|
+
logger.error(f"LiteLLM context injection failed: {e}")
|
|
650
|
+
|
|
651
|
+
return params
|
|
652
|
+
|
|
653
|
+
def _get_conscious_context(self) -> List[Dict[str, Any]]:
|
|
654
|
+
"""
|
|
655
|
+
Get conscious context from short-term memory only.
|
|
656
|
+
This represents the 'working memory' or conscious thoughts.
|
|
657
|
+
"""
|
|
658
|
+
try:
|
|
659
|
+
with self.db_manager._get_connection() as conn:
|
|
660
|
+
cursor = conn.cursor()
|
|
661
|
+
|
|
662
|
+
# Get recent short-term memories ordered by importance and recency
|
|
663
|
+
cursor.execute(
|
|
664
|
+
"""
|
|
665
|
+
SELECT memory_id, processed_data, importance_score,
|
|
666
|
+
category_primary, summary, searchable_content,
|
|
667
|
+
created_at, access_count
|
|
668
|
+
FROM short_term_memory
|
|
669
|
+
WHERE namespace = ? AND (expires_at IS NULL OR expires_at > ?)
|
|
670
|
+
ORDER BY importance_score DESC, created_at DESC
|
|
671
|
+
LIMIT 10
|
|
672
|
+
""",
|
|
673
|
+
(self.namespace, datetime.now()),
|
|
674
|
+
)
|
|
675
|
+
|
|
676
|
+
memories = []
|
|
677
|
+
for row in cursor.fetchall():
|
|
678
|
+
memories.append(
|
|
679
|
+
{
|
|
680
|
+
"memory_id": row[0],
|
|
681
|
+
"processed_data": row[1],
|
|
682
|
+
"importance_score": row[2],
|
|
683
|
+
"category_primary": row[3],
|
|
684
|
+
"summary": row[4],
|
|
685
|
+
"searchable_content": row[5],
|
|
686
|
+
"created_at": row[6],
|
|
687
|
+
"access_count": row[7],
|
|
688
|
+
"memory_type": "short_term",
|
|
689
|
+
}
|
|
690
|
+
)
|
|
691
|
+
|
|
692
|
+
logger.debug(
|
|
693
|
+
f"Retrieved {len(memories)} conscious memories from short-term storage"
|
|
694
|
+
)
|
|
695
|
+
return memories
|
|
696
|
+
|
|
697
|
+
except Exception as e:
|
|
698
|
+
logger.error(f"Failed to get conscious context: {e}")
|
|
699
|
+
return []
|
|
700
|
+
|
|
701
|
+
def _get_auto_ingest_context(self, user_input: str) -> List[Dict[str, Any]]:
|
|
702
|
+
"""
|
|
703
|
+
Get auto-ingest context using retrieval agent for intelligent search.
|
|
704
|
+
Searches through entire database for relevant memories.
|
|
705
|
+
"""
|
|
706
|
+
try:
|
|
707
|
+
if not self.search_engine:
|
|
708
|
+
logger.warning("Auto-ingest: No search engine available")
|
|
709
|
+
return []
|
|
710
|
+
|
|
711
|
+
# Use retrieval agent for intelligent search
|
|
712
|
+
results = self.search_engine.execute_search(
|
|
713
|
+
query=user_input,
|
|
714
|
+
db_manager=self.db_manager,
|
|
715
|
+
namespace=self.namespace,
|
|
716
|
+
limit=5,
|
|
717
|
+
)
|
|
718
|
+
|
|
719
|
+
logger.debug(f"Auto-ingest: Retrieved {len(results)} relevant memories")
|
|
720
|
+
return results
|
|
721
|
+
|
|
722
|
+
except Exception as e:
|
|
723
|
+
logger.error(f"Failed to get auto-ingest context: {e}")
|
|
724
|
+
return []
|
|
725
|
+
|
|
726
|
+
def _record_openai_conversation(self, kwargs, response):
|
|
727
|
+
"""Record OpenAI conversation"""
|
|
728
|
+
try:
|
|
729
|
+
messages = kwargs.get("messages", [])
|
|
730
|
+
model = kwargs.get("model", "unknown")
|
|
731
|
+
|
|
732
|
+
# Extract user input
|
|
733
|
+
user_input = ""
|
|
734
|
+
for message in reversed(messages):
|
|
735
|
+
if message.get("role") == "user":
|
|
736
|
+
user_input = message.get("content", "")
|
|
737
|
+
break
|
|
738
|
+
|
|
739
|
+
# Extract AI response
|
|
740
|
+
ai_output = ""
|
|
741
|
+
if hasattr(response, "choices") and response.choices:
|
|
742
|
+
choice = response.choices[0]
|
|
743
|
+
if hasattr(choice, "message") and choice.message:
|
|
744
|
+
ai_output = choice.message.content or ""
|
|
745
|
+
|
|
746
|
+
# Calculate tokens
|
|
747
|
+
tokens_used = 0
|
|
748
|
+
if hasattr(response, "usage") and response.usage:
|
|
749
|
+
tokens_used = getattr(response.usage, "total_tokens", 0)
|
|
750
|
+
|
|
751
|
+
# Record conversation
|
|
752
|
+
self.record_conversation(
|
|
753
|
+
user_input=user_input,
|
|
754
|
+
ai_output=ai_output,
|
|
755
|
+
model=model,
|
|
756
|
+
metadata={
|
|
757
|
+
"integration": "openai_auto",
|
|
758
|
+
"api_type": "chat_completions",
|
|
759
|
+
"tokens_used": tokens_used,
|
|
760
|
+
"auto_recorded": True,
|
|
761
|
+
},
|
|
762
|
+
)
|
|
763
|
+
except Exception as e:
|
|
764
|
+
logger.error(f"Failed to record OpenAI conversation: {e}")
|
|
765
|
+
|
|
766
|
+
def _record_anthropic_conversation(self, kwargs, response):
|
|
767
|
+
"""Record Anthropic conversation"""
|
|
768
|
+
try:
|
|
769
|
+
messages = kwargs.get("messages", [])
|
|
770
|
+
model = kwargs.get("model", "claude-unknown")
|
|
771
|
+
|
|
772
|
+
# Extract user input
|
|
773
|
+
user_input = ""
|
|
774
|
+
for message in reversed(messages):
|
|
775
|
+
if message.get("role") == "user":
|
|
776
|
+
content = message.get("content", "")
|
|
777
|
+
if isinstance(content, list):
|
|
778
|
+
user_input = " ".join(
|
|
779
|
+
[
|
|
780
|
+
block.get("text", "")
|
|
781
|
+
for block in content
|
|
782
|
+
if isinstance(block, dict)
|
|
783
|
+
and block.get("type") == "text"
|
|
784
|
+
]
|
|
785
|
+
)
|
|
786
|
+
else:
|
|
787
|
+
user_input = content
|
|
788
|
+
break
|
|
789
|
+
|
|
790
|
+
# Extract AI response
|
|
791
|
+
ai_output = ""
|
|
792
|
+
if hasattr(response, "content") and response.content:
|
|
793
|
+
if isinstance(response.content, list):
|
|
794
|
+
ai_output = " ".join(
|
|
795
|
+
[
|
|
796
|
+
block.text
|
|
797
|
+
for block in response.content
|
|
798
|
+
if hasattr(block, "text")
|
|
799
|
+
]
|
|
800
|
+
)
|
|
801
|
+
else:
|
|
802
|
+
ai_output = str(response.content)
|
|
803
|
+
|
|
804
|
+
# Calculate tokens
|
|
805
|
+
tokens_used = 0
|
|
806
|
+
if hasattr(response, "usage") and response.usage:
|
|
807
|
+
input_tokens = getattr(response.usage, "input_tokens", 0)
|
|
808
|
+
output_tokens = getattr(response.usage, "output_tokens", 0)
|
|
809
|
+
tokens_used = input_tokens + output_tokens
|
|
810
|
+
|
|
811
|
+
# Record conversation
|
|
812
|
+
self.record_conversation(
|
|
813
|
+
user_input=user_input,
|
|
814
|
+
ai_output=ai_output,
|
|
815
|
+
model=model,
|
|
816
|
+
metadata={
|
|
817
|
+
"integration": "anthropic_auto",
|
|
818
|
+
"api_type": "messages",
|
|
819
|
+
"tokens_used": tokens_used,
|
|
820
|
+
"auto_recorded": True,
|
|
821
|
+
},
|
|
822
|
+
)
|
|
823
|
+
except Exception as e:
|
|
824
|
+
logger.error(f"Failed to record Anthropic conversation: {e}")
|
|
825
|
+
|
|
826
|
+
def _litellm_success_callback(self, kwargs, response, start_time, end_time):
|
|
827
|
+
"""
|
|
828
|
+
This function is automatically called by LiteLLM after a successful completion.
|
|
829
|
+
"""
|
|
830
|
+
try:
|
|
831
|
+
user_input = ""
|
|
832
|
+
# Find the last user message
|
|
833
|
+
for msg in reversed(kwargs.get("messages", [])):
|
|
834
|
+
if msg.get("role") == "user":
|
|
835
|
+
user_input = msg.get("content", "")
|
|
836
|
+
break
|
|
837
|
+
|
|
838
|
+
ai_output = response.choices[0].message.content or ""
|
|
839
|
+
model = kwargs.get("model", "unknown")
|
|
840
|
+
|
|
841
|
+
# Calculate tokens used
|
|
842
|
+
tokens_used = 0
|
|
843
|
+
if hasattr(response, "usage") and response.usage:
|
|
844
|
+
tokens_used = getattr(response.usage, "total_tokens", 0)
|
|
845
|
+
|
|
846
|
+
# Handle timing data safely - convert any time objects to float/string
|
|
847
|
+
duration_ms = 0
|
|
848
|
+
start_time_str = None
|
|
849
|
+
end_time_str = None
|
|
850
|
+
|
|
851
|
+
try:
|
|
852
|
+
if start_time is not None and end_time is not None:
|
|
853
|
+
# Handle different types of time objects
|
|
854
|
+
if hasattr(start_time, "total_seconds"): # timedelta
|
|
855
|
+
duration_ms = start_time.total_seconds() * 1000
|
|
856
|
+
elif isinstance(start_time, (int, float)) and isinstance(
|
|
857
|
+
end_time, (int, float)
|
|
858
|
+
):
|
|
859
|
+
duration_ms = (end_time - start_time) * 1000
|
|
860
|
+
|
|
861
|
+
start_time_str = str(start_time)
|
|
862
|
+
end_time_str = str(end_time)
|
|
863
|
+
except Exception:
|
|
864
|
+
# If timing calculation fails, just skip it
|
|
865
|
+
pass
|
|
866
|
+
|
|
867
|
+
self.record_conversation(
|
|
868
|
+
user_input,
|
|
869
|
+
ai_output,
|
|
870
|
+
model,
|
|
871
|
+
metadata={
|
|
872
|
+
"integration": "litellm",
|
|
873
|
+
"api_type": "completion",
|
|
874
|
+
"tokens_used": tokens_used,
|
|
875
|
+
"auto_recorded": True,
|
|
876
|
+
"start_time_str": start_time_str,
|
|
877
|
+
"end_time_str": end_time_str,
|
|
878
|
+
"duration_ms": duration_ms,
|
|
879
|
+
},
|
|
880
|
+
)
|
|
881
|
+
except Exception as e:
|
|
882
|
+
logger.error(f"Memori callback failed: {e}")
|
|
883
|
+
|
|
884
|
+
def record_conversation(
|
|
885
|
+
self,
|
|
886
|
+
user_input: str,
|
|
887
|
+
ai_output: str,
|
|
888
|
+
model: str = "unknown",
|
|
889
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
890
|
+
) -> str:
|
|
891
|
+
"""
|
|
892
|
+
Manually record a conversation
|
|
893
|
+
|
|
894
|
+
Args:
|
|
895
|
+
user_input: The user's input message
|
|
896
|
+
ai_output: The AI's response
|
|
897
|
+
model: Model used for the response
|
|
898
|
+
metadata: Additional metadata
|
|
899
|
+
|
|
900
|
+
Returns:
|
|
901
|
+
chat_id: Unique identifier for this conversation
|
|
902
|
+
"""
|
|
903
|
+
if not self._enabled:
|
|
904
|
+
raise MemoriError("Memori is not enabled. Call enable() first.")
|
|
905
|
+
|
|
906
|
+
# Ensure ai_output is never None to avoid NOT NULL constraint errors
|
|
907
|
+
if ai_output is None:
|
|
908
|
+
ai_output = ""
|
|
909
|
+
|
|
910
|
+
chat_id = str(uuid.uuid4())
|
|
911
|
+
timestamp = datetime.now()
|
|
912
|
+
|
|
913
|
+
try:
|
|
914
|
+
# Store in chat history
|
|
915
|
+
self.db_manager.store_chat_history(
|
|
916
|
+
chat_id=chat_id,
|
|
917
|
+
user_input=user_input,
|
|
918
|
+
ai_output=ai_output,
|
|
919
|
+
model=model,
|
|
920
|
+
timestamp=timestamp,
|
|
921
|
+
session_id=self._session_id,
|
|
922
|
+
namespace=self.namespace,
|
|
923
|
+
metadata=metadata or {},
|
|
924
|
+
)
|
|
925
|
+
|
|
926
|
+
# Process for memory categorization
|
|
927
|
+
if self.conscious_ingest:
|
|
928
|
+
self._process_memory_ingestion(chat_id, user_input, ai_output, model)
|
|
929
|
+
|
|
930
|
+
logger.debug(f"Conversation recorded: {chat_id}")
|
|
931
|
+
return chat_id
|
|
932
|
+
|
|
933
|
+
except Exception as e:
|
|
934
|
+
raise MemoriError(f"Failed to record conversation: {e}")
|
|
935
|
+
|
|
936
|
+
def _process_memory_ingestion(
|
|
937
|
+
self, chat_id: str, user_input: str, ai_output: str, model: str = "unknown"
|
|
938
|
+
):
|
|
939
|
+
"""Process conversation for Pydantic-based memory categorization"""
|
|
940
|
+
if not self.memory_agent:
|
|
941
|
+
logger.warning("Memory agent not available, skipping memory ingestion")
|
|
942
|
+
return
|
|
943
|
+
|
|
944
|
+
try:
|
|
945
|
+
# Create conversation context
|
|
946
|
+
context = ConversationContext(
|
|
947
|
+
user_id=self.user_id,
|
|
948
|
+
session_id=self._session_id,
|
|
949
|
+
conversation_id=chat_id,
|
|
950
|
+
model_used=model,
|
|
951
|
+
user_preferences=self._user_context.get("user_preferences", []),
|
|
952
|
+
current_projects=self._user_context.get("current_projects", []),
|
|
953
|
+
relevant_skills=self._user_context.get("relevant_skills", []),
|
|
954
|
+
)
|
|
955
|
+
|
|
956
|
+
# Process conversation using Pydantic-based memory agent
|
|
957
|
+
processed_memory = self.memory_agent.process_conversation_sync(
|
|
958
|
+
chat_id=chat_id,
|
|
959
|
+
user_input=user_input,
|
|
960
|
+
ai_output=ai_output,
|
|
961
|
+
context=context,
|
|
962
|
+
mem_prompt=self.mem_prompt,
|
|
963
|
+
filters=self.memory_filters,
|
|
964
|
+
)
|
|
965
|
+
|
|
966
|
+
# Store processed memory with entity indexing
|
|
967
|
+
if processed_memory.should_store:
|
|
968
|
+
memory_id = self.db_manager.store_processed_memory(
|
|
969
|
+
memory=processed_memory, chat_id=chat_id, namespace=self.namespace
|
|
970
|
+
)
|
|
971
|
+
|
|
972
|
+
if memory_id:
|
|
973
|
+
logger.debug(
|
|
974
|
+
f"Stored processed memory {memory_id} for chat {chat_id}"
|
|
975
|
+
)
|
|
976
|
+
else:
|
|
977
|
+
logger.debug(
|
|
978
|
+
f"Memory not stored for chat {chat_id}: {processed_memory.storage_reasoning}"
|
|
979
|
+
)
|
|
980
|
+
else:
|
|
981
|
+
logger.debug(
|
|
982
|
+
f"Memory not stored for chat {chat_id}: {processed_memory.storage_reasoning}"
|
|
983
|
+
)
|
|
984
|
+
|
|
985
|
+
except Exception as e:
|
|
986
|
+
logger.error(f"Memory ingestion failed for {chat_id}: {e}")
|
|
987
|
+
|
|
988
|
+
def retrieve_context(self, query: str, limit: int = 5) -> List[Dict[str, Any]]:
|
|
989
|
+
"""
|
|
990
|
+
Retrieve relevant context for a query with priority on essential facts
|
|
991
|
+
|
|
992
|
+
Args:
|
|
993
|
+
query: The query to find context for
|
|
994
|
+
limit: Maximum number of context items to return
|
|
995
|
+
|
|
996
|
+
Returns:
|
|
997
|
+
List of relevant memory items with metadata, prioritizing essential facts
|
|
998
|
+
"""
|
|
999
|
+
try:
|
|
1000
|
+
context_items = []
|
|
1001
|
+
|
|
1002
|
+
if self.conscious_ingest:
|
|
1003
|
+
# First, get essential conversations from short-term memory (always relevant)
|
|
1004
|
+
essential_conversations = self.get_essential_conversations(limit=3)
|
|
1005
|
+
context_items.extend(essential_conversations)
|
|
1006
|
+
|
|
1007
|
+
# Calculate remaining slots for specific context
|
|
1008
|
+
remaining_limit = max(0, limit - len(essential_conversations))
|
|
1009
|
+
else:
|
|
1010
|
+
remaining_limit = limit
|
|
1011
|
+
|
|
1012
|
+
if remaining_limit > 0:
|
|
1013
|
+
# Get specific context using search engine or database
|
|
1014
|
+
if self.search_engine:
|
|
1015
|
+
specific_context = self.search_engine.execute_search(
|
|
1016
|
+
query=query,
|
|
1017
|
+
db_manager=self.db_manager,
|
|
1018
|
+
namespace=self.namespace,
|
|
1019
|
+
limit=remaining_limit,
|
|
1020
|
+
)
|
|
1021
|
+
else:
|
|
1022
|
+
# Fallback to database search
|
|
1023
|
+
specific_context = self.db_manager.search_memories(
|
|
1024
|
+
query=query, namespace=self.namespace, limit=remaining_limit
|
|
1025
|
+
)
|
|
1026
|
+
|
|
1027
|
+
# Add specific context, avoiding duplicates
|
|
1028
|
+
for item in specific_context:
|
|
1029
|
+
if not any(
|
|
1030
|
+
ctx.get("memory_id") == item.get("memory_id")
|
|
1031
|
+
for ctx in context_items
|
|
1032
|
+
):
|
|
1033
|
+
context_items.append(item)
|
|
1034
|
+
|
|
1035
|
+
logger.debug(
|
|
1036
|
+
f"Retrieved {len(context_items)} context items for query: {query} "
|
|
1037
|
+
f"(Essential conversations: {len(essential_conversations) if self.conscious_ingest else 0})"
|
|
1038
|
+
)
|
|
1039
|
+
return context_items
|
|
1040
|
+
|
|
1041
|
+
except Exception as e:
|
|
1042
|
+
logger.error(f"Context retrieval failed: {e}")
|
|
1043
|
+
return []
|
|
1044
|
+
|
|
1045
|
+
def get_conversation_history(self, limit: int = 10) -> List[Dict[str, Any]]:
|
|
1046
|
+
"""Get recent conversation history"""
|
|
1047
|
+
try:
|
|
1048
|
+
return self.db_manager.get_chat_history(
|
|
1049
|
+
namespace=self.namespace,
|
|
1050
|
+
session_id=self._session_id if not self.shared_memory else None,
|
|
1051
|
+
limit=limit,
|
|
1052
|
+
)
|
|
1053
|
+
except Exception as e:
|
|
1054
|
+
logger.error(f"Failed to get conversation history: {e}")
|
|
1055
|
+
return []
|
|
1056
|
+
|
|
1057
|
+
def clear_memory(self, memory_type: Optional[str] = None):
|
|
1058
|
+
"""
|
|
1059
|
+
Clear memory data
|
|
1060
|
+
|
|
1061
|
+
Args:
|
|
1062
|
+
memory_type: Type of memory to clear ('short_term', 'long_term', 'all')
|
|
1063
|
+
"""
|
|
1064
|
+
try:
|
|
1065
|
+
self.db_manager.clear_memory(self.namespace, memory_type)
|
|
1066
|
+
logger.info(
|
|
1067
|
+
f"Cleared {memory_type or 'all'} memory for namespace: {self.namespace}"
|
|
1068
|
+
)
|
|
1069
|
+
except Exception as e:
|
|
1070
|
+
raise MemoriError(f"Failed to clear memory: {e}")
|
|
1071
|
+
|
|
1072
|
+
def get_memory_stats(self) -> Dict[str, Any]:
|
|
1073
|
+
"""Get memory statistics"""
|
|
1074
|
+
try:
|
|
1075
|
+
return self.db_manager.get_memory_stats(self.namespace)
|
|
1076
|
+
except Exception as e:
|
|
1077
|
+
logger.error(f"Failed to get memory stats: {e}")
|
|
1078
|
+
return {}
|
|
1079
|
+
|
|
1080
|
+
@property
|
|
1081
|
+
def is_enabled(self) -> bool:
|
|
1082
|
+
"""Check if memory recording is enabled"""
|
|
1083
|
+
return self._enabled
|
|
1084
|
+
|
|
1085
|
+
@property
|
|
1086
|
+
def session_id(self) -> str:
|
|
1087
|
+
"""Get current session ID"""
|
|
1088
|
+
return self._session_id
|
|
1089
|
+
|
|
1090
|
+
def get_integration_stats(self) -> List[Dict[str, Any]]:
|
|
1091
|
+
"""Get statistics from the universal integration system"""
|
|
1092
|
+
try:
|
|
1093
|
+
stats = {
|
|
1094
|
+
"integration": "universal_auto_recording",
|
|
1095
|
+
"enabled": self._enabled,
|
|
1096
|
+
"session_id": self._session_id,
|
|
1097
|
+
"namespace": self.namespace,
|
|
1098
|
+
"providers": {},
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
# LiteLLM stats
|
|
1102
|
+
if LITELLM_AVAILABLE:
|
|
1103
|
+
stats["providers"]["litellm"] = {
|
|
1104
|
+
"available": True,
|
|
1105
|
+
"method": "native_callbacks",
|
|
1106
|
+
"callback_registered": self._enabled,
|
|
1107
|
+
"callbacks_count": len(success_callback) if self._enabled else 0,
|
|
1108
|
+
}
|
|
1109
|
+
else:
|
|
1110
|
+
stats["providers"]["litellm"] = {
|
|
1111
|
+
"available": False,
|
|
1112
|
+
"method": "native_callbacks",
|
|
1113
|
+
"callback_registered": False,
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
# OpenAI stats
|
|
1117
|
+
try:
|
|
1118
|
+
import openai
|
|
1119
|
+
|
|
1120
|
+
stats["providers"]["openai"] = {
|
|
1121
|
+
"available": True,
|
|
1122
|
+
"method": "auto_wrapping",
|
|
1123
|
+
"wrapped": (
|
|
1124
|
+
hasattr(openai.OpenAI, "_memori_wrapped")
|
|
1125
|
+
if hasattr(openai, "OpenAI")
|
|
1126
|
+
else False
|
|
1127
|
+
),
|
|
1128
|
+
}
|
|
1129
|
+
except ImportError:
|
|
1130
|
+
stats["providers"]["openai"] = {
|
|
1131
|
+
"available": False,
|
|
1132
|
+
"method": "auto_wrapping",
|
|
1133
|
+
"wrapped": False,
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
# Anthropic stats
|
|
1137
|
+
try:
|
|
1138
|
+
import anthropic
|
|
1139
|
+
|
|
1140
|
+
stats["providers"]["anthropic"] = {
|
|
1141
|
+
"available": True,
|
|
1142
|
+
"method": "auto_wrapping",
|
|
1143
|
+
"wrapped": (
|
|
1144
|
+
hasattr(anthropic.Anthropic, "_memori_wrapped")
|
|
1145
|
+
if hasattr(anthropic, "Anthropic")
|
|
1146
|
+
else False
|
|
1147
|
+
),
|
|
1148
|
+
}
|
|
1149
|
+
except ImportError:
|
|
1150
|
+
stats["providers"]["anthropic"] = {
|
|
1151
|
+
"available": False,
|
|
1152
|
+
"method": "auto_wrapping",
|
|
1153
|
+
"wrapped": False,
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
return [stats]
|
|
1157
|
+
except Exception as e:
|
|
1158
|
+
logger.error(f"Failed to get integration stats: {e}")
|
|
1159
|
+
return []
|
|
1160
|
+
|
|
1161
|
+
def update_user_context(
|
|
1162
|
+
self,
|
|
1163
|
+
current_projects: Optional[List[str]] = None,
|
|
1164
|
+
relevant_skills: Optional[List[str]] = None,
|
|
1165
|
+
user_preferences: Optional[List[str]] = None,
|
|
1166
|
+
):
|
|
1167
|
+
"""Update user context for better memory processing"""
|
|
1168
|
+
if current_projects is not None:
|
|
1169
|
+
self._user_context["current_projects"] = current_projects
|
|
1170
|
+
if relevant_skills is not None:
|
|
1171
|
+
self._user_context["relevant_skills"] = relevant_skills
|
|
1172
|
+
if user_preferences is not None:
|
|
1173
|
+
self._user_context["user_preferences"] = user_preferences
|
|
1174
|
+
|
|
1175
|
+
logger.debug(f"Updated user context: {self._user_context}")
|
|
1176
|
+
|
|
1177
|
+
def search_memories_by_category(
|
|
1178
|
+
self, category: str, limit: int = 10
|
|
1179
|
+
) -> List[Dict[str, Any]]:
|
|
1180
|
+
"""Search memories by specific category"""
|
|
1181
|
+
try:
|
|
1182
|
+
return self.db_manager.search_memories(
|
|
1183
|
+
query="",
|
|
1184
|
+
namespace=self.namespace,
|
|
1185
|
+
category_filter=[category],
|
|
1186
|
+
limit=limit,
|
|
1187
|
+
)
|
|
1188
|
+
except Exception as e:
|
|
1189
|
+
logger.error(f"Category search failed: {e}")
|
|
1190
|
+
return []
|
|
1191
|
+
|
|
1192
|
+
def get_entity_memories(
|
|
1193
|
+
self, entity_value: str, entity_type: Optional[str] = None, limit: int = 10
|
|
1194
|
+
) -> List[Dict[str, Any]]:
|
|
1195
|
+
"""Get memories that contain a specific entity"""
|
|
1196
|
+
try:
|
|
1197
|
+
# This would use the entity index in the database
|
|
1198
|
+
# For now, use keyword search as fallback
|
|
1199
|
+
return self.db_manager.search_memories(
|
|
1200
|
+
query=entity_value, namespace=self.namespace, limit=limit
|
|
1201
|
+
)
|
|
1202
|
+
except Exception as e:
|
|
1203
|
+
logger.error(f"Entity search failed: {e}")
|
|
1204
|
+
return []
|
|
1205
|
+
|
|
1206
|
+
def _start_background_analysis(self):
|
|
1207
|
+
"""Start the background conscious agent analysis task"""
|
|
1208
|
+
try:
|
|
1209
|
+
if self._background_task and not self._background_task.done():
|
|
1210
|
+
logger.debug("Background analysis task already running")
|
|
1211
|
+
return
|
|
1212
|
+
|
|
1213
|
+
# Create event loop if it doesn't exist
|
|
1214
|
+
try:
|
|
1215
|
+
loop = asyncio.get_running_loop()
|
|
1216
|
+
except RuntimeError:
|
|
1217
|
+
# No event loop running, create a new thread for async tasks
|
|
1218
|
+
import threading
|
|
1219
|
+
|
|
1220
|
+
def run_background_loop():
|
|
1221
|
+
new_loop = asyncio.new_event_loop()
|
|
1222
|
+
asyncio.set_event_loop(new_loop)
|
|
1223
|
+
try:
|
|
1224
|
+
new_loop.run_until_complete(self._background_analysis_loop())
|
|
1225
|
+
except Exception as e:
|
|
1226
|
+
logger.error(f"Background analysis loop failed: {e}")
|
|
1227
|
+
finally:
|
|
1228
|
+
new_loop.close()
|
|
1229
|
+
|
|
1230
|
+
thread = threading.Thread(target=run_background_loop, daemon=True)
|
|
1231
|
+
thread.start()
|
|
1232
|
+
logger.info("Background analysis started in separate thread")
|
|
1233
|
+
return
|
|
1234
|
+
|
|
1235
|
+
# If we have a running loop, schedule the task
|
|
1236
|
+
self._background_task = loop.create_task(self._background_analysis_loop())
|
|
1237
|
+
logger.info("Background analysis task started")
|
|
1238
|
+
|
|
1239
|
+
except Exception as e:
|
|
1240
|
+
logger.error(f"Failed to start background analysis: {e}")
|
|
1241
|
+
|
|
1242
|
+
def _stop_background_analysis(self):
|
|
1243
|
+
"""Stop the background analysis task"""
|
|
1244
|
+
try:
|
|
1245
|
+
if self._background_task and not self._background_task.done():
|
|
1246
|
+
self._background_task.cancel()
|
|
1247
|
+
logger.info("Background analysis task stopped")
|
|
1248
|
+
except Exception as e:
|
|
1249
|
+
logger.error(f"Failed to stop background analysis: {e}")
|
|
1250
|
+
|
|
1251
|
+
async def _background_analysis_loop(self):
|
|
1252
|
+
"""Main background analysis loop"""
|
|
1253
|
+
logger.info("ConsciouscAgent: Background analysis loop started")
|
|
1254
|
+
|
|
1255
|
+
while self._enabled and self.conscious_ingest:
|
|
1256
|
+
try:
|
|
1257
|
+
if self.conscious_agent and self.conscious_agent.should_run_analysis():
|
|
1258
|
+
await self.conscious_agent.run_background_analysis(
|
|
1259
|
+
self.db_manager, self.namespace
|
|
1260
|
+
)
|
|
1261
|
+
|
|
1262
|
+
# Wait 30 minutes before next check
|
|
1263
|
+
await asyncio.sleep(1800) # 30 minutes
|
|
1264
|
+
|
|
1265
|
+
except asyncio.CancelledError:
|
|
1266
|
+
logger.info("ConsciouscAgent: Background analysis cancelled")
|
|
1267
|
+
break
|
|
1268
|
+
except Exception as e:
|
|
1269
|
+
logger.error(f"ConsciouscAgent: Background analysis error: {e}")
|
|
1270
|
+
# Wait 5 minutes before retrying on error
|
|
1271
|
+
await asyncio.sleep(300)
|
|
1272
|
+
|
|
1273
|
+
logger.info("ConsciouscAgent: Background analysis loop ended")
|
|
1274
|
+
|
|
1275
|
+
def trigger_conscious_analysis(self):
|
|
1276
|
+
"""Manually trigger conscious agent analysis (for testing/immediate analysis)"""
|
|
1277
|
+
if not self.conscious_ingest or not self.conscious_agent:
|
|
1278
|
+
logger.warning("Conscious ingestion not enabled or agent not available")
|
|
1279
|
+
return
|
|
1280
|
+
|
|
1281
|
+
try:
|
|
1282
|
+
# Try to run in existing event loop
|
|
1283
|
+
try:
|
|
1284
|
+
loop = asyncio.get_running_loop()
|
|
1285
|
+
task = loop.create_task(
|
|
1286
|
+
self.conscious_agent.run_background_analysis(
|
|
1287
|
+
self.db_manager, self.namespace
|
|
1288
|
+
)
|
|
1289
|
+
)
|
|
1290
|
+
logger.info("Conscious analysis triggered")
|
|
1291
|
+
return task
|
|
1292
|
+
except RuntimeError:
|
|
1293
|
+
# No event loop, run synchronously in thread
|
|
1294
|
+
import threading
|
|
1295
|
+
|
|
1296
|
+
def run_analysis():
|
|
1297
|
+
new_loop = asyncio.new_event_loop()
|
|
1298
|
+
asyncio.set_event_loop(new_loop)
|
|
1299
|
+
try:
|
|
1300
|
+
new_loop.run_until_complete(
|
|
1301
|
+
self.conscious_agent.run_background_analysis(
|
|
1302
|
+
self.db_manager, self.namespace
|
|
1303
|
+
)
|
|
1304
|
+
)
|
|
1305
|
+
finally:
|
|
1306
|
+
new_loop.close()
|
|
1307
|
+
|
|
1308
|
+
thread = threading.Thread(target=run_analysis)
|
|
1309
|
+
thread.start()
|
|
1310
|
+
logger.info("Conscious analysis triggered in separate thread")
|
|
1311
|
+
|
|
1312
|
+
except Exception as e:
|
|
1313
|
+
logger.error(f"Failed to trigger conscious analysis: {e}")
|
|
1314
|
+
|
|
1315
|
+
def get_essential_conversations(self, limit: int = 10) -> List[Dict[str, Any]]:
|
|
1316
|
+
"""Get essential conversations from short-term memory"""
|
|
1317
|
+
try:
|
|
1318
|
+
# Get all conversations marked as essential
|
|
1319
|
+
with self.db_manager._get_connection() as connection:
|
|
1320
|
+
query = """
|
|
1321
|
+
SELECT memory_id, summary, category_primary, importance_score,
|
|
1322
|
+
created_at, searchable_content, processed_data
|
|
1323
|
+
FROM short_term_memory
|
|
1324
|
+
WHERE namespace = ? AND category_primary LIKE 'essential_%'
|
|
1325
|
+
ORDER BY importance_score DESC, created_at DESC
|
|
1326
|
+
LIMIT ?
|
|
1327
|
+
"""
|
|
1328
|
+
|
|
1329
|
+
cursor = connection.execute(query, (self.namespace, limit))
|
|
1330
|
+
|
|
1331
|
+
essential_conversations = []
|
|
1332
|
+
for row in cursor.fetchall():
|
|
1333
|
+
essential_conversations.append(
|
|
1334
|
+
{
|
|
1335
|
+
"memory_id": row[0],
|
|
1336
|
+
"summary": row[1],
|
|
1337
|
+
"category_primary": row[2],
|
|
1338
|
+
"importance_score": row[3],
|
|
1339
|
+
"created_at": row[4],
|
|
1340
|
+
"searchable_content": row[5],
|
|
1341
|
+
"processed_data": row[6],
|
|
1342
|
+
}
|
|
1343
|
+
)
|
|
1344
|
+
|
|
1345
|
+
return essential_conversations
|
|
1346
|
+
|
|
1347
|
+
except Exception as e:
|
|
1348
|
+
logger.error(f"Failed to get essential conversations: {e}")
|
|
1349
|
+
return []
|