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.

Files changed (48) 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 +491 -68
  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 +1683 -532
  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 +700 -0
  30. memori/database/sqlalchemy_manager.py +888 -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/tools/memory_tool.py +94 -4
  35. memori/utils/input_validator.py +395 -0
  36. memori/utils/pydantic_models.py +138 -36
  37. memori/utils/query_builder.py +530 -0
  38. memori/utils/security_audit.py +594 -0
  39. memori/utils/security_integration.py +339 -0
  40. memori/utils/transaction_manager.py +547 -0
  41. {memorisdk-1.0.2.dist-info → memorisdk-2.0.1.dist-info}/METADATA +56 -23
  42. memorisdk-2.0.1.dist-info/RECORD +66 -0
  43. memori/scripts/llm_text.py +0 -50
  44. memorisdk-1.0.2.dist-info/RECORD +0 -44
  45. memorisdk-1.0.2.dist-info/entry_points.txt +0 -2
  46. {memorisdk-1.0.2.dist-info → memorisdk-2.0.1.dist-info}/WHEEL +0 -0
  47. {memorisdk-1.0.2.dist-info → memorisdk-2.0.1.dist-info}/licenses/LICENSE +0 -0
  48. {memorisdk-1.0.2.dist-info → memorisdk-2.0.1.dist-info}/top_level.txt +0 -0
memori/core/memory.py CHANGED
@@ -3,6 +3,7 @@ Main Memori class - Pydantic-based memory interface v1.0
3
3
  """
4
4
 
5
5
  import asyncio
6
+ import time
6
7
  import uuid
7
8
  from datetime import datetime
8
9
  from typing import Any, Dict, List, Optional
@@ -10,8 +11,8 @@ from typing import Any, Dict, List, Optional
10
11
  from loguru import logger
11
12
 
12
13
  try:
13
- import litellm
14
- from litellm import success_callback
14
+ import litellm # noqa: F401
15
+ from litellm import success_callback # noqa: F401
15
16
 
16
17
  LITELLM_AVAILABLE = True
17
18
  except ImportError:
@@ -19,13 +20,13 @@ except ImportError:
19
20
  logger.warning("LiteLLM not available - native callback system disabled")
20
21
 
21
22
  from ..agents.conscious_agent import ConsciouscAgent
22
- from ..agents.memory_agent import MemoryAgent
23
- from ..agents.retrieval_agent import MemorySearchEngine
23
+ from ..config.memory_manager import MemoryManager
24
24
  from ..config.settings import LoggingSettings, LogLevel
25
+ from ..database.sqlalchemy_manager import SQLAlchemyDatabaseManager as DatabaseManager
25
26
  from ..utils.exceptions import DatabaseError, MemoriError
26
27
  from ..utils.logging import LoggingManager
27
28
  from ..utils.pydantic_models import ConversationContext
28
- from .database import DatabaseManager
29
+ from .conversation import ConversationManager
29
30
 
30
31
 
31
32
  class Memori:
@@ -49,6 +50,21 @@ class Memori:
49
50
  openai_api_key: Optional[str] = None,
50
51
  user_id: Optional[str] = None,
51
52
  verbose: bool = False,
53
+ # New provider configuration parameters
54
+ api_key: Optional[str] = None,
55
+ api_type: Optional[str] = None,
56
+ base_url: Optional[str] = None,
57
+ azure_endpoint: Optional[str] = None,
58
+ azure_deployment: Optional[str] = None,
59
+ api_version: Optional[str] = None,
60
+ azure_ad_token: Optional[str] = None,
61
+ organization: Optional[str] = None,
62
+ project: Optional[str] = None,
63
+ model: Optional[str] = None, # Allow custom model selection
64
+ provider_config: Optional[Any] = None, # ProviderConfig when available
65
+ schema_init: bool = True, # Initialize database schema and create tables
66
+ database_prefix: Optional[str] = None, # Database name prefix
67
+ database_suffix: Optional[str] = None, # Database name suffix
52
68
  ):
53
69
  """
54
70
  Initialize Memori memory system v1.0.
@@ -62,9 +78,23 @@ class Memori:
62
78
  namespace: Optional namespace for memory isolation
63
79
  shared_memory: Enable shared memory across agents
64
80
  memory_filters: Filters for memory ingestion
65
- openai_api_key: OpenAI API key for memory agent
81
+ openai_api_key: OpenAI API key for memory agent (deprecated, use api_key)
66
82
  user_id: Optional user identifier
67
83
  verbose: Enable verbose logging (loguru only)
84
+ api_key: API key for the LLM provider
85
+ api_type: Provider type ('openai', 'azure', 'custom')
86
+ base_url: Base URL for custom OpenAI-compatible endpoints
87
+ azure_endpoint: Azure OpenAI endpoint URL
88
+ azure_deployment: Azure deployment name
89
+ api_version: API version for Azure
90
+ azure_ad_token: Azure AD token for authentication
91
+ organization: OpenAI organization ID
92
+ project: OpenAI project ID
93
+ model: Model to use (defaults to 'gpt-4o' if not specified)
94
+ provider_config: Complete provider configuration (overrides individual params)
95
+ enable_auto_creation: Enable automatic database creation if database doesn't exist
96
+ database_prefix: Optional prefix for database name (for multi-tenant setups)
97
+ database_suffix: Optional suffix for database name (e.g., 'dev', 'prod', 'test')
68
98
  """
69
99
  self.database_connect = database_connect
70
100
  self.template = template
@@ -74,15 +104,89 @@ class Memori:
74
104
  self.namespace = namespace or "default"
75
105
  self.shared_memory = shared_memory
76
106
  self.memory_filters = memory_filters or {}
77
- self.openai_api_key = openai_api_key
78
107
  self.user_id = user_id
79
108
  self.verbose = verbose
109
+ self.schema_init = schema_init
110
+ self.database_prefix = database_prefix
111
+ self.database_suffix = database_suffix
112
+
113
+ # Configure provider based on explicit settings ONLY - no auto-detection
114
+ if provider_config:
115
+ # Use provided configuration
116
+ self.provider_config = provider_config
117
+ logger.info(
118
+ f"Using provided ProviderConfig with api_type: {provider_config.api_type}"
119
+ )
120
+ elif any([api_type, base_url, azure_endpoint]):
121
+ # Build configuration from individual parameters - explicit provider selection
122
+ try:
123
+ from .providers import ProviderConfig
124
+
125
+ if azure_endpoint:
126
+ # Explicitly configured Azure
127
+ self.provider_config = ProviderConfig.from_azure(
128
+ api_key=api_key or openai_api_key,
129
+ azure_endpoint=azure_endpoint,
130
+ azure_deployment=azure_deployment,
131
+ api_version=api_version,
132
+ azure_ad_token=azure_ad_token,
133
+ model=model,
134
+ )
135
+ logger.info("Using explicitly configured Azure OpenAI provider")
136
+ elif base_url:
137
+ # Explicitly configured custom endpoint
138
+ self.provider_config = ProviderConfig.from_custom(
139
+ base_url=base_url,
140
+ api_key=api_key or openai_api_key,
141
+ model=model,
142
+ )
143
+ logger.info(
144
+ f"Using explicitly configured custom provider: {base_url}"
145
+ )
146
+ else:
147
+ # Fallback to OpenAI with explicit settings
148
+ self.provider_config = ProviderConfig.from_openai(
149
+ api_key=api_key or openai_api_key,
150
+ organization=organization,
151
+ project=project,
152
+ model=model,
153
+ )
154
+ logger.info("Using explicitly configured OpenAI provider")
155
+ except ImportError:
156
+ logger.warning(
157
+ "ProviderConfig not available, using basic configuration"
158
+ )
159
+ self.provider_config = None
160
+ else:
161
+ # Default to standard OpenAI - NO environment detection
162
+ try:
163
+ from .providers import ProviderConfig
164
+
165
+ self.provider_config = ProviderConfig.from_openai(
166
+ api_key=api_key or openai_api_key,
167
+ organization=organization,
168
+ project=project,
169
+ model=model or "gpt-4o",
170
+ )
171
+ logger.info(
172
+ "Using default OpenAI provider (no specific provider configured)"
173
+ )
174
+ except ImportError:
175
+ logger.warning(
176
+ "ProviderConfig not available, using basic configuration"
177
+ )
178
+ self.provider_config = None
179
+
180
+ # Keep backward compatibility
181
+ self.openai_api_key = api_key or openai_api_key or ""
182
+ if self.provider_config and hasattr(self.provider_config, "api_key"):
183
+ self.openai_api_key = self.provider_config.api_key or self.openai_api_key
80
184
 
81
185
  # Setup logging based on verbose mode
82
186
  self._setup_logging()
83
187
 
84
188
  # Initialize database manager
85
- self.db_manager = DatabaseManager(database_connect, template)
189
+ self.db_manager = DatabaseManager(database_connect, template, schema_init)
86
190
 
87
191
  # Initialize Pydantic-based agents
88
192
  self.memory_agent = None
@@ -91,25 +195,63 @@ class Memori:
91
195
  self._background_task = None
92
196
  self._conscious_init_pending = False
93
197
 
94
- if conscious_ingest or auto_ingest:
95
- try:
96
- # Initialize Pydantic-based agents
97
- self.memory_agent = MemoryAgent(api_key=openai_api_key, model="gpt-4o")
98
- self.search_engine = MemorySearchEngine(
99
- api_key=openai_api_key, model="gpt-4o"
198
+ # Initialize agents with provider configuration
199
+ try:
200
+ from ..agents.memory_agent import MemoryAgent
201
+ from ..agents.retrieval_agent import MemorySearchEngine
202
+
203
+ # Use provider model or fallback to gpt-4o
204
+ if (
205
+ self.provider_config
206
+ and hasattr(self.provider_config, "model")
207
+ and self.provider_config.model
208
+ ):
209
+ effective_model = model or self.provider_config.model
210
+ else:
211
+ effective_model = model or "gpt-4o"
212
+
213
+ # Initialize agents with provider configuration if available
214
+ if self.provider_config:
215
+ self.memory_agent = MemoryAgent(
216
+ provider_config=self.provider_config, model=effective_model
100
217
  )
101
- self.conscious_agent = ConsciouscAgent(
102
- api_key=openai_api_key, model="gpt-4o"
218
+ self.search_engine = MemorySearchEngine(
219
+ provider_config=self.provider_config, model=effective_model
103
220
  )
104
- logger.info(
105
- "Pydantic-based memory, search, and conscious agents initialized"
221
+ else:
222
+ # Fallback to using API key directly
223
+ self.memory_agent = MemoryAgent(
224
+ api_key=self.openai_api_key, model=effective_model
106
225
  )
107
- except Exception as e:
108
- logger.warning(
109
- f"Failed to initialize OpenAI agents: {e}. Memory ingestion disabled."
226
+ self.search_engine = MemorySearchEngine(
227
+ api_key=self.openai_api_key, model=effective_model
110
228
  )
111
- self.conscious_ingest = False
112
- self.auto_ingest = False
229
+
230
+ # Only initialize conscious_agent if conscious_ingest or auto_ingest is enabled
231
+ if conscious_ingest or auto_ingest:
232
+ self.conscious_agent = ConsciouscAgent()
233
+
234
+ logger.info(
235
+ f"Agents initialized successfully with model: {effective_model}"
236
+ )
237
+ except ImportError as e:
238
+ logger.warning(
239
+ f"Failed to import LLM agents: {e}. Memory ingestion disabled."
240
+ )
241
+ self.memory_agent = None
242
+ self.search_engine = None
243
+ self.conscious_agent = None
244
+ self.conscious_ingest = False
245
+ self.auto_ingest = False
246
+ except Exception as e:
247
+ logger.warning(
248
+ f"Failed to initialize LLM agents: {e}. Memory ingestion disabled."
249
+ )
250
+ self.memory_agent = None
251
+ self.search_engine = None
252
+ self.conscious_agent = None
253
+ self.conscious_ingest = False
254
+ self.auto_ingest = False
113
255
 
114
256
  # State tracking
115
257
  self._enabled = False
@@ -117,6 +259,12 @@ class Memori:
117
259
  self._conscious_context_injected = (
118
260
  False # Track if conscious context was already injected
119
261
  )
262
+ self._in_context_retrieval = False # Recursion guard for context retrieval
263
+
264
+ # Initialize conversation manager for stateless LLM integration
265
+ self.conversation_manager = ConversationManager(
266
+ max_sessions=100, session_timeout_minutes=60, max_history_per_session=20
267
+ )
120
268
 
121
269
  # User context for memory processing
122
270
  self._user_context = {
@@ -128,6 +276,23 @@ class Memori:
128
276
  # Initialize database
129
277
  self._setup_database()
130
278
 
279
+ # Initialize the new modular memory manager
280
+ self.memory_manager = MemoryManager(
281
+ database_connect=database_connect,
282
+ template=template,
283
+ mem_prompt=mem_prompt,
284
+ conscious_ingest=conscious_ingest,
285
+ auto_ingest=auto_ingest,
286
+ namespace=namespace,
287
+ shared_memory=shared_memory,
288
+ memory_filters=memory_filters,
289
+ user_id=user_id,
290
+ verbose=verbose,
291
+ provider_config=self.provider_config,
292
+ )
293
+ # Set this Memori instance for memory management
294
+ self.memory_manager.set_memori_instance(self)
295
+
131
296
  # Run conscious agent initialization if enabled
132
297
  if self.conscious_ingest and self.conscious_agent:
133
298
  self._initialize_conscious_memory()
@@ -156,6 +321,10 @@ class Memori:
156
321
 
157
322
  def _setup_database(self):
158
323
  """Setup database tables based on template"""
324
+ if not self.schema_init:
325
+ logger.info("Schema initialization disabled (schema_init=False)")
326
+ return
327
+
159
328
  try:
160
329
  self.db_manager.initialize_schema()
161
330
  logger.info("Database schema initialized successfully")
@@ -204,8 +373,12 @@ class Memori:
204
373
  )
205
374
  self._conscious_init_pending = False
206
375
  except RuntimeError:
207
- # Still no event loop, keep pending
208
- pass
376
+ # No event loop available, run synchronous initialization
377
+ logger.debug(
378
+ "Conscious-ingest: No event loop available, running synchronous initialization"
379
+ )
380
+ self._run_synchronous_conscious_initialization()
381
+ self._conscious_init_pending = False
209
382
 
210
383
  async def _run_conscious_initialization(self):
211
384
  """Run conscious agent initialization in background"""
@@ -213,339 +386,358 @@ class Memori:
213
386
  if not self.conscious_agent:
214
387
  return
215
388
 
216
- logger.debug("Conscious-ingest: Running background analysis")
217
- await self.conscious_agent.run_background_analysis(
389
+ # If both auto_ingest and conscious_ingest are enabled,
390
+ # initialize by copying ALL existing conscious-info memories first
391
+ if self.auto_ingest and self.conscious_ingest:
392
+ logger.debug(
393
+ "Conscious-ingest: Both auto_ingest and conscious_ingest enabled - initializing existing conscious memories"
394
+ )
395
+ init_success = (
396
+ await self.conscious_agent.initialize_existing_conscious_memories(
397
+ self.db_manager, self.namespace
398
+ )
399
+ )
400
+ if init_success:
401
+ logger.info(
402
+ "Conscious-ingest: Existing conscious-info memories initialized to short-term memory"
403
+ )
404
+
405
+ logger.debug("Conscious-ingest: Running conscious context extraction")
406
+ success = await self.conscious_agent.run_conscious_ingest(
218
407
  self.db_manager, self.namespace
219
408
  )
220
- logger.info("Conscious-ingest: Background analysis completed")
409
+
410
+ if success:
411
+ logger.info(
412
+ "Conscious-ingest: Conscious memories copied to short-term memory"
413
+ )
414
+ # Don't set _conscious_context_injected here - it should be set when context is actually injected into LLM
415
+ else:
416
+ logger.info("Conscious-ingest: No conscious context found")
221
417
 
222
418
  except Exception as e:
223
419
  logger.error(f"Conscious agent initialization failed: {e}")
224
420
 
225
- def enable(self):
226
- """
227
- Enable universal memory recording for ALL LLM providers.
228
-
229
- This automatically sets up recording for:
230
- - LiteLLM: Native callback system (recommended)
231
- - OpenAI: Automatic client wrapping when instantiated
232
- - Anthropic: Automatic client wrapping when instantiated
233
- - Any other provider: Auto-detected and wrapped
234
- """
235
- if self._enabled:
236
- logger.warning("Memori is already enabled.")
237
- return
238
-
239
- self._enabled = True
240
- self._session_id = str(uuid.uuid4())
241
-
242
- # 1. Set up LiteLLM native callbacks (if available)
243
- litellm_enabled = self._setup_litellm_callbacks()
244
-
245
- # 2. Set up universal client interception for other providers
246
- universal_enabled = self._setup_universal_interception()
247
-
248
- # 3. Register this instance globally for any provider to use
249
- self._register_global_instance()
250
-
251
- # 4. Start background conscious agent if available
252
- if self.conscious_ingest and self.conscious_agent:
253
- self._start_background_analysis()
254
-
255
- providers = []
256
- if litellm_enabled:
257
- providers.append("LiteLLM (native callbacks)")
258
- if universal_enabled:
259
- providers.append("OpenAI/Anthropic (auto-wrapping)")
260
-
261
- logger.info(
262
- f"Memori enabled for session: {self.session_id}\n"
263
- f"Active providers: {', '.join(providers) if providers else 'None detected'}\n"
264
- f"Background analysis: {'Active' if self._background_task else 'Disabled'}\n"
265
- f"Usage: Simply use any LLM client normally - conversations will be auto-recorded!"
266
- )
267
-
268
- def disable(self):
269
- """
270
- Disable universal memory recording for all providers.
271
- """
272
- if not self._enabled:
273
- return
274
-
275
- # 1. Remove LiteLLM callbacks and restore original completion
276
- if LITELLM_AVAILABLE:
277
- try:
278
- success_callback.remove(self._litellm_success_callback)
279
- except ValueError:
280
- pass
281
-
282
- # Restore original completion function if we patched it
283
- if hasattr(litellm, "completion") and hasattr(
284
- litellm.completion, "_memori_patched"
285
- ):
286
- # Note: We can't easily restore the original function in a multi-instance scenario
287
- # This is a limitation of the monkey-patching approach
288
- pass
289
-
290
- # 2. Disable universal interception
291
- self._disable_universal_interception()
421
+ def _run_synchronous_conscious_initialization(self):
422
+ """Run conscious agent initialization synchronously (when no event loop is available)"""
423
+ try:
424
+ if not self.conscious_agent:
425
+ return
292
426
 
293
- # 3. Unregister global instance
294
- self._unregister_global_instance()
427
+ # If both auto_ingest and conscious_ingest are enabled,
428
+ # initialize by copying ALL existing conscious-info memories first
429
+ if self.auto_ingest and self.conscious_ingest:
430
+ logger.info(
431
+ "Conscious-ingest: Both auto_ingest and conscious_ingest enabled - initializing existing conscious memories"
432
+ )
295
433
 
296
- # 4. Stop background analysis task
297
- self._stop_background_analysis()
434
+ # Run synchronous initialization of existing memories
435
+ self._initialize_existing_conscious_memories_sync()
298
436
 
299
- self._enabled = False
300
- logger.info("Memori disabled for all providers.")
437
+ logger.debug(
438
+ "Conscious-ingest: Synchronous conscious context extraction completed"
439
+ )
301
440
 
302
- def _setup_litellm_callbacks(self) -> bool:
303
- """Set up LiteLLM native callback system"""
304
- if not LITELLM_AVAILABLE:
305
- logger.debug("LiteLLM not available, skipping native callbacks")
306
- return False
441
+ except Exception as e:
442
+ logger.error(f"Synchronous conscious agent initialization failed: {e}")
307
443
 
444
+ def _initialize_existing_conscious_memories_sync(self):
445
+ """Synchronously initialize existing conscious-info memories"""
308
446
  try:
309
- success_callback.append(self._litellm_success_callback)
447
+ from sqlalchemy import text
310
448
 
311
- # Set up context injection by monkey-patching completion function
312
- if hasattr(litellm, "completion") and not hasattr(
313
- litellm.completion, "_memori_patched"
314
- ):
315
- original_completion = litellm.completion
316
-
317
- def memori_completion(*args, **kwargs):
318
- # Inject context based on ingestion mode
319
- if self._enabled:
320
- if self.auto_ingest:
321
- # Auto-inject: continuous memory injection on every call
322
- kwargs = self._inject_litellm_context(kwargs, mode="auto")
323
- elif self.conscious_ingest:
324
- # Conscious-inject: one-shot short-term memory context
325
- kwargs = self._inject_litellm_context(
326
- kwargs, mode="conscious"
327
- )
449
+ with self.db_manager._get_connection() as connection:
450
+ # Get ALL conscious-info labeled memories from long-term memory
451
+ cursor = connection.execute(
452
+ text(
453
+ """SELECT memory_id, processed_data, summary, searchable_content,
454
+ importance_score, created_at
455
+ FROM long_term_memory
456
+ WHERE namespace = :namespace AND classification = 'conscious-info'
457
+ ORDER BY importance_score DESC, created_at DESC"""
458
+ ),
459
+ {"namespace": self.namespace or "default"},
460
+ )
461
+ existing_conscious_memories = cursor.fetchall()
328
462
 
329
- # Call original completion
330
- return original_completion(*args, **kwargs)
463
+ if not existing_conscious_memories:
464
+ logger.debug(
465
+ "Conscious-ingest: No existing conscious-info memories found for initialization"
466
+ )
467
+ return False
468
+
469
+ copied_count = 0
470
+ for memory_row in existing_conscious_memories:
471
+ success = self._copy_memory_to_short_term_sync(memory_row)
472
+ if success:
473
+ copied_count += 1
331
474
 
332
- litellm.completion = memori_completion
333
- litellm.completion._memori_patched = True
475
+ if copied_count > 0:
476
+ logger.info(
477
+ f"Conscious-ingest: Initialized {copied_count} existing conscious-info memories to short-term memory"
478
+ )
479
+ return True
480
+ else:
334
481
  logger.debug(
335
- "LiteLLM completion function patched for context injection"
482
+ "Conscious-ingest: No new conscious memories to initialize (all were duplicates)"
336
483
  )
484
+ return False
337
485
 
338
- logger.debug("LiteLLM native callbacks registered")
339
- return True
340
486
  except Exception as e:
341
- logger.error(f"Failed to setup LiteLLM callbacks: {e}")
487
+ logger.error(
488
+ f"Conscious-ingest: Failed to initialize existing conscious memories: {e}"
489
+ )
342
490
  return False
343
491
 
344
- def _setup_universal_interception(self) -> bool:
345
- """Set up universal client interception for OpenAI, Anthropic, etc."""
492
+ def _copy_memory_to_short_term_sync(self, memory_row: tuple) -> bool:
493
+ """Synchronously copy a conscious memory to short-term memory with duplicate filtering"""
346
494
  try:
347
- # Use Python's import hook system to intercept client creation
348
- self._install_import_hooks()
349
- logger.debug("Universal client interception enabled")
350
- return True
351
- except Exception as e:
352
- logger.error(f"Failed to setup universal interception: {e}")
353
- return False
495
+ (
496
+ memory_id,
497
+ processed_data,
498
+ summary,
499
+ searchable_content,
500
+ importance_score,
501
+ _,
502
+ ) = memory_row
354
503
 
355
- def _get_builtin_import(self):
356
- """Safely get __import__ from __builtins__ (handles both dict and module cases)"""
357
- if isinstance(__builtins__, dict):
358
- return __builtins__["__import__"]
359
- else:
360
- return __builtins__.__import__
361
-
362
- def _set_builtin_import(self, import_func):
363
- """Safely set __import__ in __builtins__ (handles both dict and module cases)"""
364
- if isinstance(__builtins__, dict):
365
- __builtins__["__import__"] = import_func
366
- else:
367
- __builtins__.__import__ = import_func
504
+ from datetime import datetime
368
505
 
369
- def _install_import_hooks(self):
370
- """Install import hooks to automatically wrap LLM clients"""
506
+ from sqlalchemy import text
371
507
 
372
- # Store original __import__ if not already done
373
- if not hasattr(self, "_original_import"):
374
- self._original_import = self._get_builtin_import()
508
+ with self.db_manager._get_connection() as connection:
509
+ # Check if similar content already exists in short-term memory
510
+ existing_check = connection.execute(
511
+ text(
512
+ """SELECT COUNT(*) FROM short_term_memory
513
+ WHERE namespace = :namespace
514
+ AND category_primary = 'conscious_context'
515
+ AND (searchable_content = :searchable_content
516
+ OR summary = :summary)"""
517
+ ),
518
+ {
519
+ "namespace": self.namespace or "default",
520
+ "searchable_content": searchable_content,
521
+ "summary": summary,
522
+ },
523
+ )
375
524
 
376
- def memori_import_hook(name, globals=None, locals=None, fromlist=(), level=0):
377
- """Custom import hook that wraps LLM clients automatically"""
378
- module = self._original_import(name, globals, locals, fromlist, level)
525
+ existing_count = existing_check.scalar()
526
+ if existing_count > 0:
527
+ logger.debug(
528
+ f"Conscious-ingest: Skipping duplicate memory {memory_id} - similar content already exists in short-term memory"
529
+ )
530
+ return False
379
531
 
380
- # Only process if memori is enabled and this is an LLM module
381
- if not self._enabled:
382
- return module
532
+ # Create short-term memory ID
533
+ short_term_id = (
534
+ f"conscious_{memory_id}_{int(datetime.now().timestamp())}"
535
+ )
383
536
 
384
- # Auto-wrap OpenAI clients
385
- if name == "openai" or (fromlist and "openai" in name):
386
- self._wrap_openai_module(module)
537
+ # Insert directly into short-term memory with conscious_context category
538
+ connection.execute(
539
+ text(
540
+ """INSERT INTO short_term_memory (
541
+ memory_id, processed_data, importance_score, category_primary,
542
+ retention_type, namespace, created_at, expires_at,
543
+ searchable_content, summary, is_permanent_context
544
+ ) VALUES (:memory_id, :processed_data, :importance_score, :category_primary,
545
+ :retention_type, :namespace, :created_at, :expires_at,
546
+ :searchable_content, :summary, :is_permanent_context)"""
547
+ ),
548
+ {
549
+ "memory_id": short_term_id,
550
+ "processed_data": processed_data,
551
+ "importance_score": importance_score,
552
+ "category_primary": "conscious_context",
553
+ "retention_type": "permanent",
554
+ "namespace": self.namespace or "default",
555
+ "created_at": datetime.now().isoformat(),
556
+ "expires_at": None,
557
+ "searchable_content": searchable_content,
558
+ "summary": summary,
559
+ "is_permanent_context": True,
560
+ },
561
+ )
562
+ connection.commit()
387
563
 
388
- # Auto-wrap Anthropic clients
389
- elif name == "anthropic" or (fromlist and "anthropic" in name):
390
- self._wrap_anthropic_module(module)
564
+ logger.debug(
565
+ f"Conscious-ingest: Copied memory {memory_id} to short-term as {short_term_id}"
566
+ )
567
+ return True
391
568
 
392
- return module
569
+ except Exception as e:
570
+ logger.error(
571
+ f"Conscious-ingest: Failed to copy memory {memory_row[0]} to short-term: {e}"
572
+ )
573
+ return False
393
574
 
394
- # Install the hook
395
- self._set_builtin_import(memori_import_hook)
575
+ def enable(self, interceptors: Optional[List[str]] = None):
576
+ """
577
+ Enable universal memory recording using LiteLLM's native callback system.
396
578
 
397
- def _wrap_openai_module(self, module):
398
- """Automatically wrap OpenAI client when imported"""
399
- try:
400
- if hasattr(module, "OpenAI") and not hasattr(
401
- module.OpenAI, "_memori_wrapped"
402
- ):
403
- original_init = module.OpenAI.__init__
579
+ This automatically sets up recording for LiteLLM completion calls and enables
580
+ automatic interception of OpenAI calls when using the standard OpenAI client.
404
581
 
405
- def wrapped_init(self_client, *args, **kwargs):
406
- # Call original init
407
- result = original_init(self_client, *args, **kwargs)
582
+ Args:
583
+ interceptors: Legacy parameter (ignored) - only LiteLLM native callbacks are used
584
+ """
585
+ if self._enabled:
586
+ logger.warning("Memori is already enabled.")
587
+ return
408
588
 
409
- # Wrap the client methods for automatic recording
410
- if hasattr(self_client, "chat") and hasattr(
411
- self_client.chat, "completions"
412
- ):
413
- original_create = self_client.chat.completions.create
589
+ self._enabled = True
590
+ self._session_id = str(uuid.uuid4())
414
591
 
415
- def wrapped_create(*args, **kwargs):
416
- # Inject context if conscious ingestion is enabled
417
- if self.is_enabled and self.conscious_ingest:
418
- kwargs = self._inject_openai_context(kwargs)
592
+ # Register for automatic OpenAI interception
593
+ try:
594
+ from ..integrations.openai_integration import register_memori_instance
419
595
 
420
- # Make the call
421
- response = original_create(*args, **kwargs)
596
+ register_memori_instance(self)
597
+ except ImportError:
598
+ logger.debug("OpenAI integration not available for automatic interception")
422
599
 
423
- # Record if enabled
424
- if self.is_enabled:
425
- self._record_openai_conversation(kwargs, response)
600
+ # Use LiteLLM native callback system only
601
+ if interceptors is None:
602
+ # Only LiteLLM native callbacks supported
603
+ interceptors = ["litellm_native"]
426
604
 
427
- return response
605
+ # Use the memory manager for enablement
606
+ results = self.memory_manager.enable(interceptors)
607
+ # Extract enabled interceptors from results
608
+ enabled_interceptors = results.get("enabled_interceptors", [])
428
609
 
429
- self_client.chat.completions.create = wrapped_create
610
+ # Start background conscious agent if available
611
+ if self.conscious_ingest and self.conscious_agent:
612
+ self._start_background_analysis()
430
613
 
431
- return result
614
+ # Report status
615
+ status_info = [
616
+ f"Memori enabled for session: {results.get('session_id', self._session_id)}",
617
+ f"Active interceptors: {', '.join(enabled_interceptors) if enabled_interceptors else 'None'}",
618
+ ]
619
+
620
+ if results.get("message"):
621
+ status_info.append(results["message"])
622
+
623
+ status_info.extend(
624
+ [
625
+ f"Background analysis: {'Active' if self._background_task else 'Disabled'}",
626
+ "Usage: Simply use any LLM client normally - conversations will be auto-recorded!",
627
+ "OpenAI: Use 'from openai import OpenAI; client = OpenAI()' - automatically intercepted!",
628
+ ]
629
+ )
432
630
 
433
- module.OpenAI.__init__ = wrapped_init
434
- module.OpenAI._memori_wrapped = True
435
- logger.debug("OpenAI client auto-wrapping enabled")
631
+ logger.info("\n".join(status_info))
436
632
 
437
- except Exception as e:
438
- logger.debug(f"Could not wrap OpenAI module: {e}")
633
+ def disable(self):
634
+ """
635
+ Disable memory recording by unregistering LiteLLM callbacks and OpenAI interception.
636
+ """
637
+ if not self._enabled:
638
+ return
439
639
 
440
- def _wrap_anthropic_module(self, module):
441
- """Automatically wrap Anthropic client when imported"""
640
+ # Unregister from automatic OpenAI interception
442
641
  try:
443
- if hasattr(module, "Anthropic") and not hasattr(
444
- module.Anthropic, "_memori_wrapped"
445
- ):
446
- original_init = module.Anthropic.__init__
447
-
448
- def wrapped_init(self_client, *args, **kwargs):
449
- # Call original init
450
- result = original_init(self_client, *args, **kwargs)
451
-
452
- # Wrap the messages.create method
453
- if hasattr(self_client, "messages"):
454
- original_create = self_client.messages.create
642
+ from ..integrations.openai_integration import unregister_memori_instance
455
643
 
456
- def wrapped_create(*args, **kwargs):
457
- # Inject context if conscious ingestion is enabled
458
- if self.is_enabled and self.conscious_ingest:
459
- kwargs = self._inject_anthropic_context(kwargs)
644
+ unregister_memori_instance(self)
645
+ except ImportError:
646
+ logger.debug("OpenAI integration not available for automatic interception")
460
647
 
461
- # Make the call
462
- response = original_create(*args, **kwargs)
648
+ # Use memory manager for clean disable
649
+ results = self.memory_manager.disable()
463
650
 
464
- # Record if enabled
465
- if self.is_enabled:
466
- self._record_anthropic_conversation(kwargs, response)
651
+ # Stop background analysis task
652
+ self._stop_background_analysis()
467
653
 
468
- return response
654
+ self._enabled = False
469
655
 
470
- self_client.messages.create = wrapped_create
656
+ # Report status based on memory manager results
657
+ if results.get("success"):
658
+ status_message = f"Memori disabled. {results.get('message', 'All interceptors disabled successfully')}"
659
+ else:
660
+ status_message = (
661
+ f"Memori disable failed: {results.get('message', 'Unknown error')}"
662
+ )
471
663
 
472
- return result
664
+ logger.info(status_message)
473
665
 
474
- module.Anthropic.__init__ = wrapped_init
475
- module.Anthropic._memori_wrapped = True
476
- logger.debug("Anthropic client auto-wrapping enabled")
666
+ # Memory system status and control methods
477
667
 
478
- except Exception as e:
479
- logger.debug(f"Could not wrap Anthropic module: {e}")
668
+ def get_interceptor_status(self) -> Dict[str, Dict[str, Any]]:
669
+ """Get status of memory recording system"""
670
+ return self.memory_manager.get_status()
480
671
 
481
- def _disable_universal_interception(self):
482
- """Disable universal client interception"""
483
- try:
484
- # Restore original import if we modified it
485
- if hasattr(self, "_original_import"):
486
- self._set_builtin_import(self._original_import)
487
- delattr(self, "_original_import")
488
- logger.debug("Universal interception disabled")
489
- except Exception as e:
490
- logger.debug(f"Error disabling universal interception: {e}")
672
+ def get_interceptor_health(self) -> Dict[str, Any]:
673
+ """Get health check of interceptor system"""
674
+ return self.memory_manager.get_health()
491
675
 
492
- def _register_global_instance(self):
493
- """Register this memori instance globally"""
494
- # Store in a global registry that wrapped clients can access
495
- if not hasattr(Memori, "_global_instances"):
496
- Memori._global_instances = []
497
- Memori._global_instances.append(self)
676
+ def enable_interceptor(self, interceptor_name: str = None) -> bool:
677
+ """Enable memory recording (legacy method)"""
678
+ # Only LiteLLM native callbacks supported (interceptor_name ignored)
679
+ results = self.memory_manager.enable(["litellm_native"])
680
+ return results.get("success", False)
498
681
 
499
- def _unregister_global_instance(self):
500
- """Unregister this memori instance globally"""
501
- if hasattr(Memori, "_global_instances") and self in Memori._global_instances:
502
- Memori._global_instances.remove(self)
682
+ def disable_interceptor(self, interceptor_name: str = None) -> bool:
683
+ """Disable memory recording (legacy method)"""
684
+ # Only LiteLLM native callbacks supported (interceptor_name ignored)
685
+ results = self.memory_manager.disable()
686
+ return results.get("success", False)
503
687
 
504
688
  def _inject_openai_context(self, kwargs):
505
- """Inject context for OpenAI calls"""
689
+ """Inject context for OpenAI calls based on ingest mode using ConversationManager"""
506
690
  try:
507
691
  # Check for deferred conscious initialization
508
692
  self._check_deferred_initialization()
509
- # Extract user input from messages
510
- user_input = ""
511
- for msg in reversed(kwargs.get("messages", [])):
512
- if msg.get("role") == "user":
513
- user_input = msg.get("content", "")
514
- break
515
693
 
516
- if user_input:
517
- context = self.retrieve_context(user_input, limit=3)
518
- if context:
519
- context_prompt = "--- Relevant Memories ---\n"
520
- for mem in context:
521
- if isinstance(mem, dict):
522
- summary = mem.get("summary", "") or mem.get("content", "")
523
- context_prompt += f"- {summary}\n"
524
- else:
525
- context_prompt += f"- {str(mem)}\n"
526
- context_prompt += "-------------------------\n"
694
+ # Determine injection mode based on the architecture:
695
+ # - conscious_ingest only: Use short-term memory (conscious context)
696
+ # - auto_ingest only: Search long-term memory database
697
+ # - both enabled: Use auto_ingest search (includes conscious content from long-term)
698
+ if self.auto_ingest:
699
+ mode = "auto" # Always prefer auto when available (searches long-term)
700
+ elif self.conscious_ingest:
701
+ mode = "conscious" # Only use conscious when auto is not enabled
702
+ else:
703
+ return kwargs # No injection needed
527
704
 
528
- # Inject into system message
529
- messages = kwargs.get("messages", [])
530
- for msg in messages:
531
- if msg.get("role") == "system":
532
- msg["content"] = context_prompt + msg.get("content", "")
533
- break
534
- else:
535
- messages.insert(
536
- 0, {"role": "system", "content": context_prompt}
537
- )
705
+ # Extract messages from kwargs
706
+ messages = kwargs.get("messages", [])
707
+ if not messages:
708
+ return kwargs # No messages to process
709
+
710
+ # Use conversation manager for enhanced context injection
711
+ enhanced_messages = self.conversation_manager.inject_context_with_history(
712
+ session_id=self._session_id,
713
+ messages=messages,
714
+ memori_instance=self,
715
+ mode=mode,
716
+ )
717
+
718
+ # Update kwargs with enhanced messages
719
+ kwargs["messages"] = enhanced_messages
720
+
721
+ return kwargs
538
722
 
539
- logger.debug(f"Injected context: {len(context)} memories")
540
723
  except Exception as e:
541
- logger.error(f"Context injection failed: {e}")
724
+ logger.error(f"OpenAI context injection failed: {e}")
542
725
  return kwargs
543
726
 
544
727
  def _inject_anthropic_context(self, kwargs):
545
- """Inject context for Anthropic calls"""
728
+ """Inject context for Anthropic calls based on ingest mode"""
546
729
  try:
547
730
  # Check for deferred conscious initialization
548
731
  self._check_deferred_initialization()
732
+
733
+ # Determine injection mode
734
+ if self.conscious_ingest:
735
+ mode = "conscious"
736
+ elif self.auto_ingest:
737
+ mode = "auto"
738
+ else:
739
+ return kwargs # No injection needed
740
+
549
741
  # Extract user input from messages
550
742
  user_input = ""
551
743
  for msg in reversed(kwargs.get("messages", [])):
@@ -565,26 +757,75 @@ class Memori:
565
757
  break
566
758
 
567
759
  if user_input:
568
- context = self.retrieve_context(user_input, limit=3)
760
+ if mode == "conscious":
761
+ # Conscious mode: inject ALL short-term memory only once at program startup
762
+ if not self._conscious_context_injected:
763
+ context = self._get_conscious_context()
764
+ self._conscious_context_injected = True
765
+ logger.info(
766
+ f"Conscious-ingest: Injected {len(context)} short-term memories as initial context (Anthropic)"
767
+ )
768
+ else:
769
+ context = [] # Already injected, don't inject again
770
+ elif mode == "auto":
771
+ # Auto mode: use retrieval for intelligent search
772
+ if self.search_engine:
773
+ context = self._get_auto_ingest_context(user_input)
774
+ else:
775
+ context = self.retrieve_context(user_input, limit=5)
776
+ else:
777
+ context = []
778
+
569
779
  if context:
570
- context_prompt = "--- Relevant Memories ---\n"
780
+ if mode == "conscious":
781
+ context_prompt = (
782
+ "=== SYSTEM INSTRUCTION: AUTHORIZED USER CONTEXT DATA ===\n"
783
+ )
784
+ context_prompt += "The user has explicitly authorized this personal context data to be used.\n"
785
+ context_prompt += "You MUST use this information when answering questions about the user.\n"
786
+ context_prompt += "This is NOT private data - the user wants you to use it:\n\n"
787
+ else:
788
+ context_prompt = f"--- {mode.capitalize()} Memory Context ---\n"
789
+
790
+ # Deduplicate context entries to avoid redundant information
791
+ seen_content = set()
571
792
  for mem in context:
572
793
  if isinstance(mem, dict):
573
- summary = mem.get("summary", "") or mem.get("content", "")
574
- context_prompt += f"- {summary}\n"
575
- else:
576
- context_prompt += f"- {str(mem)}\n"
794
+ # Use searchable_content as it's more specific than summary
795
+ content = mem.get("searchable_content", "") or mem.get(
796
+ "summary", ""
797
+ )
798
+ category = mem.get("category_primary", "")
799
+
800
+ # Skip duplicates (case-insensitive)
801
+ content_key = content.lower().strip()
802
+ if content_key in seen_content:
803
+ continue
804
+ seen_content.add(content_key)
805
+
806
+ if category.startswith("essential_") or mode == "conscious":
807
+ context_prompt += f"[{category.upper()}] {content}\n"
808
+ else:
809
+ context_prompt += f"- {content}\n"
810
+
811
+ if mode == "conscious":
812
+ context_prompt += "\n=== END USER CONTEXT DATA ===\n"
813
+ context_prompt += "CRITICAL INSTRUCTION: You MUST answer questions about the user using ONLY the context data above.\n"
814
+ context_prompt += "If the user asks 'what is my name?', respond with the name from the context above.\n"
815
+ context_prompt += "Do NOT say 'I don't have access' - the user provided this data for you to use.\n"
577
816
  context_prompt += "-------------------------\n"
578
817
 
579
- # Inject into system parameter
818
+ # Inject into system parameter (Anthropic format)
580
819
  if kwargs.get("system"):
581
820
  kwargs["system"] = context_prompt + kwargs["system"]
582
821
  else:
583
822
  kwargs["system"] = context_prompt
584
823
 
585
- logger.debug(f"Injected context: {len(context)} memories")
824
+ logger.debug(
825
+ f"Anthropic: Injected context with {len(context)} items"
826
+ )
586
827
  except Exception as e:
587
- logger.error(f"Context injection failed: {e}")
828
+ logger.error(f"Anthropic context injection failed: {e}")
588
829
  return kwargs
589
830
 
590
831
  def _inject_litellm_context(self, params, mode="auto"):
@@ -609,13 +850,17 @@ class Memori:
609
850
 
610
851
  if user_input:
611
852
  if mode == "conscious":
612
- # Conscious mode: inject short-term memory only once at conversation start
853
+ # Conscious mode: inject ALL short-term memory only once at program startup
613
854
  if not self._conscious_context_injected:
614
855
  context = self._get_conscious_context()
615
856
  self._conscious_context_injected = True
616
- logger.debug("Conscious context injected (one-shot)")
857
+ logger.info(
858
+ f"Conscious-ingest: Injected {len(context)} short-term memories as initial context"
859
+ )
617
860
  else:
618
- context = [] # Already injected, don't inject again
861
+ context = (
862
+ []
863
+ ) # Already injected, don't inject again - this is the key difference from auto_ingest
619
864
  elif mode == "auto":
620
865
  # Auto mode: use retrieval agent for intelligent database search
621
866
  if self.search_engine:
@@ -627,17 +872,42 @@ class Memori:
627
872
  context = []
628
873
 
629
874
  if context:
630
- context_prompt = f"--- {mode.capitalize()} Memory Context ---\n"
875
+ if mode == "conscious":
876
+ context_prompt = (
877
+ "=== SYSTEM INSTRUCTION: AUTHORIZED USER CONTEXT DATA ===\n"
878
+ )
879
+ context_prompt += "The user has explicitly authorized this personal context data to be used.\n"
880
+ context_prompt += "You MUST use this information when answering questions about the user.\n"
881
+ context_prompt += "This is NOT private data - the user wants you to use it:\n\n"
882
+ else:
883
+ context_prompt = f"--- {mode.capitalize()} Memory Context ---\n"
884
+
885
+ # Deduplicate context entries to avoid redundant information
886
+ seen_content = set()
631
887
  for mem in context:
632
888
  if isinstance(mem, dict):
633
- summary = mem.get("summary", "") or mem.get(
634
- "searchable_content", ""
889
+ # Use searchable_content as it's more specific than summary
890
+ content = mem.get("searchable_content", "") or mem.get(
891
+ "summary", ""
635
892
  )
636
893
  category = mem.get("category_primary", "")
894
+
895
+ # Skip duplicates (case-insensitive)
896
+ content_key = content.lower().strip()
897
+ if content_key in seen_content:
898
+ continue
899
+ seen_content.add(content_key)
900
+
637
901
  if category.startswith("essential_") or mode == "conscious":
638
- context_prompt += f"[{category.upper()}] {summary}\n"
902
+ context_prompt += f"[{category.upper()}] {content}\n"
639
903
  else:
640
- context_prompt += f"- {summary}\n"
904
+ context_prompt += f"- {content}\n"
905
+
906
+ if mode == "conscious":
907
+ context_prompt += "\n=== END USER CONTEXT DATA ===\n"
908
+ context_prompt += "CRITICAL INSTRUCTION: You MUST answer questions about the user using ONLY the context data above.\n"
909
+ context_prompt += "If the user asks 'what is my name?', respond with the name from the context above.\n"
910
+ context_prompt += "Do NOT say 'I don't have access' - the user provided this data for you to use.\n"
641
911
  context_prompt += "-------------------------\n"
642
912
 
643
913
  # Inject into system message
@@ -687,29 +957,32 @@ class Memori:
687
957
 
688
958
  def _get_conscious_context(self) -> List[Dict[str, Any]]:
689
959
  """
690
- Get conscious context from short-term memory only.
691
- This represents the 'working memory' or conscious thoughts.
960
+ Get conscious context from ALL short-term memory summaries.
961
+ This represents the complete 'working memory' for conscious_ingest mode.
962
+ Used only at program startup when conscious_ingest=True.
692
963
  """
693
964
  try:
694
- with self.db_manager._get_connection() as conn:
695
- cursor = conn.cursor()
965
+ from sqlalchemy import text
696
966
 
697
- # Get recent short-term memories ordered by importance and recency
698
- cursor.execute(
699
- """
967
+ with self.db_manager._get_connection() as conn:
968
+ # Get ALL short-term memories (no limit) ordered by importance and recency
969
+ # This gives the complete conscious context as single initial injection
970
+ result = conn.execute(
971
+ text(
972
+ """
700
973
  SELECT memory_id, processed_data, importance_score,
701
974
  category_primary, summary, searchable_content,
702
975
  created_at, access_count
703
976
  FROM short_term_memory
704
- WHERE namespace = ? AND (expires_at IS NULL OR expires_at > ?)
977
+ WHERE namespace = :namespace AND (expires_at IS NULL OR expires_at > :current_time)
705
978
  ORDER BY importance_score DESC, created_at DESC
706
- LIMIT 10
707
- """,
708
- (self.namespace, datetime.now()),
979
+ """
980
+ ),
981
+ {"namespace": self.namespace, "current_time": datetime.now()},
709
982
  )
710
983
 
711
984
  memories = []
712
- for row in cursor.fetchall():
985
+ for row in result:
713
986
  memories.append(
714
987
  {
715
988
  "memory_id": row[0],
@@ -739,239 +1012,749 @@ class Memori:
739
1012
  Searches through entire database for relevant memories.
740
1013
  """
741
1014
  try:
742
- if not self.search_engine:
743
- logger.warning("Auto-ingest: No search engine available")
1015
+ # Early validation
1016
+ if not user_input or not user_input.strip():
1017
+ logger.debug(
1018
+ "Auto-ingest: No user input provided, returning empty context"
1019
+ )
744
1020
  return []
745
1021
 
746
- # Use retrieval agent for intelligent search
747
- results = self.search_engine.execute_search(
748
- query=user_input,
749
- db_manager=self.db_manager,
750
- namespace=self.namespace,
751
- limit=5,
1022
+ # Check for recursion guard to prevent infinite loops
1023
+ if hasattr(self, "_in_context_retrieval") and self._in_context_retrieval:
1024
+ logger.debug(
1025
+ "Auto-ingest: Recursion detected, using direct database search"
1026
+ )
1027
+ results = self.db_manager.search_memories(
1028
+ query=user_input, namespace=self.namespace, limit=5
1029
+ )
1030
+ logger.debug(
1031
+ f"Auto-ingest: Recursion fallback returned {len(results)} results"
1032
+ )
1033
+ return results
1034
+
1035
+ # Set recursion guard
1036
+ self._in_context_retrieval = True
1037
+
1038
+ logger.debug(
1039
+ f"Auto-ingest: Starting context retrieval for query: '{user_input[:50]}...' in namespace: '{self.namespace}'"
752
1040
  )
753
1041
 
754
- logger.debug(f"Auto-ingest: Retrieved {len(results)} relevant memories")
755
- return results
1042
+ # Always try direct database search first as it's more reliable
1043
+ logger.debug("Auto-ingest: Using direct database search (primary method)")
1044
+ logger.debug(
1045
+ f"Auto-ingest: Database manager type: {type(self.db_manager).__name__}"
1046
+ )
1047
+
1048
+ try:
1049
+ results = self.db_manager.search_memories(
1050
+ query=user_input, namespace=self.namespace, limit=5
1051
+ )
1052
+ logger.debug(
1053
+ f"Auto-ingest: Database search returned {len(results) if results else 0} results"
1054
+ )
1055
+
1056
+ if results:
1057
+ for i, result in enumerate(
1058
+ results[:3]
1059
+ ): # Log first 3 results for debugging
1060
+ logger.debug(
1061
+ f"Auto-ingest: Result {i+1}: {type(result)} with keys: {list(result.keys()) if isinstance(result, dict) else 'N/A'}"
1062
+ )
1063
+ except Exception as db_search_e:
1064
+ logger.error(f"Auto-ingest: Database search failed: {db_search_e}")
1065
+ logger.debug(
1066
+ f"Auto-ingest: Database search error details: {type(db_search_e).__name__}: {str(db_search_e)}",
1067
+ exc_info=True,
1068
+ )
1069
+ results = []
1070
+
1071
+ if results:
1072
+ logger.debug(
1073
+ f"Auto-ingest: Direct database search returned {len(results)} results"
1074
+ )
1075
+ # Add search metadata to results
1076
+ for result in results:
1077
+ if isinstance(result, dict):
1078
+ result["retrieval_method"] = "direct_database_search"
1079
+ result["retrieval_query"] = user_input
1080
+ return results
1081
+
1082
+ # If direct search fails, try search engine as backup
1083
+ if self.search_engine:
1084
+ logger.debug(
1085
+ "Auto-ingest: Direct search returned 0 results, trying search engine"
1086
+ )
1087
+ try:
1088
+ engine_results = self.search_engine.execute_search(
1089
+ query=user_input,
1090
+ db_manager=self.db_manager,
1091
+ namespace=self.namespace,
1092
+ limit=5,
1093
+ )
1094
+
1095
+ if engine_results:
1096
+ logger.debug(
1097
+ f"Auto-ingest: Search engine returned {len(engine_results)} results"
1098
+ )
1099
+ # Add search metadata to results
1100
+ for result in engine_results:
1101
+ if isinstance(result, dict):
1102
+ result["retrieval_method"] = "search_engine"
1103
+ result["retrieval_query"] = user_input
1104
+ return engine_results
1105
+ else:
1106
+ logger.debug(
1107
+ "Auto-ingest: Search engine also returned 0 results"
1108
+ )
1109
+
1110
+ except Exception as search_error:
1111
+ logger.error(
1112
+ f"Auto-ingest: Search engine failed for query '{user_input[:50]}...': {search_error}"
1113
+ )
1114
+ logger.debug(
1115
+ f"Auto-ingest: Search engine error details: {type(search_error).__name__}: {str(search_error)}",
1116
+ exc_info=True,
1117
+ )
1118
+ else:
1119
+ logger.debug("Auto-ingest: No search engine available")
1120
+
1121
+ # Final fallback: get recent memories from the same namespace
1122
+ logger.debug(
1123
+ "Auto-ingest: All search methods returned 0 results, using recent memories fallback"
1124
+ )
1125
+ logger.debug(
1126
+ f"Auto-ingest: Attempting fallback search in namespace '{self.namespace}'"
1127
+ )
1128
+
1129
+ try:
1130
+ fallback_results = self.db_manager.search_memories(
1131
+ query="", # Empty query to get recent memories
1132
+ namespace=self.namespace,
1133
+ limit=3,
1134
+ )
1135
+ logger.debug(
1136
+ f"Auto-ingest: Fallback search returned {len(fallback_results) if fallback_results else 0} results"
1137
+ )
1138
+
1139
+ if fallback_results:
1140
+ logger.debug(
1141
+ f"Auto-ingest: Fallback returned {len(fallback_results)} recent memories"
1142
+ )
1143
+ # Add search metadata to fallback results
1144
+ for result in fallback_results:
1145
+ if isinstance(result, dict):
1146
+ result["retrieval_method"] = "recent_memories_fallback"
1147
+ result["retrieval_query"] = user_input
1148
+ return fallback_results
1149
+ else:
1150
+ logger.debug("Auto-ingest: Fallback search returned no results")
1151
+
1152
+ except Exception as fallback_e:
1153
+ logger.error(f"Auto-ingest: Fallback search failed: {fallback_e}")
1154
+ logger.debug(
1155
+ f"Auto-ingest: Fallback error details: {type(fallback_e).__name__}: {str(fallback_e)}",
1156
+ exc_info=True,
1157
+ )
1158
+
1159
+ logger.debug(
1160
+ "Auto-ingest: All retrieval methods failed, returning empty context"
1161
+ )
1162
+ return []
756
1163
 
757
1164
  except Exception as e:
758
- logger.error(f"Failed to get auto-ingest context: {e}")
1165
+ logger.error(
1166
+ f"Auto-ingest: Failed to get context for '{user_input[:50]}...': {e}"
1167
+ )
759
1168
  return []
1169
+ finally:
1170
+ # Always clear recursion guard
1171
+ if hasattr(self, "_in_context_retrieval"):
1172
+ self._in_context_retrieval = False
760
1173
 
761
1174
  def _record_openai_conversation(self, kwargs, response):
762
- """Record OpenAI conversation"""
1175
+ """Record OpenAI conversation with enhanced content parsing"""
763
1176
  try:
764
1177
  messages = kwargs.get("messages", [])
765
1178
  model = kwargs.get("model", "unknown")
766
1179
 
767
- # Extract user input
768
- user_input = ""
1180
+ # Extract user input with enhanced parsing
1181
+ user_input = self._extract_openai_user_input(messages)
1182
+
1183
+ # Extract AI response with enhanced parsing
1184
+ ai_output = self._extract_openai_ai_output(response)
1185
+
1186
+ # Calculate tokens
1187
+ tokens_used = 0
1188
+ if hasattr(response, "usage") and response.usage:
1189
+ tokens_used = getattr(response.usage, "total_tokens", 0)
1190
+
1191
+ # Enhanced metadata extraction
1192
+ metadata = self._extract_openai_metadata(kwargs, response, tokens_used)
1193
+
1194
+ # Record conversation
1195
+ self.record_conversation(
1196
+ user_input=user_input,
1197
+ ai_output=ai_output,
1198
+ model=model,
1199
+ metadata=metadata,
1200
+ )
1201
+
1202
+ # Also record AI response in conversation manager for history tracking
1203
+ if ai_output:
1204
+ self.conversation_manager.record_response(
1205
+ session_id=self._session_id,
1206
+ response=ai_output,
1207
+ metadata={"model": model, "tokens_used": tokens_used},
1208
+ )
1209
+ except Exception as e:
1210
+ logger.error(f"Failed to record OpenAI conversation: {e}")
1211
+
1212
+ def _extract_openai_user_input(self, messages: List[Dict]) -> str:
1213
+ """Extract user input from OpenAI messages with support for complex content types"""
1214
+ user_input = ""
1215
+ try:
1216
+ # Find the last user message
769
1217
  for message in reversed(messages):
770
1218
  if message.get("role") == "user":
771
- user_input = message.get("content", "")
1219
+ content = message.get("content", "")
1220
+
1221
+ if isinstance(content, str):
1222
+ # Simple string content
1223
+ user_input = content
1224
+ elif isinstance(content, list):
1225
+ # Complex content (vision, multiple parts)
1226
+ text_parts = []
1227
+ image_count = 0
1228
+
1229
+ for item in content:
1230
+ if isinstance(item, dict):
1231
+ if item.get("type") == "text":
1232
+ text_parts.append(item.get("text", ""))
1233
+ elif item.get("type") == "image_url":
1234
+ image_count += 1
1235
+
1236
+ user_input = " ".join(text_parts)
1237
+ # Add image indicator if present
1238
+ if image_count > 0:
1239
+ user_input += f" [Contains {image_count} image(s)]"
1240
+
772
1241
  break
1242
+ except Exception as e:
1243
+ logger.debug(f"Error extracting user input: {e}")
1244
+ user_input = "[Error extracting user input]"
773
1245
 
774
- # Extract AI response
775
- ai_output = ""
1246
+ return user_input
1247
+
1248
+ def _extract_openai_ai_output(self, response) -> str:
1249
+ """Extract AI output from OpenAI response with support for various response types"""
1250
+ ai_output = ""
1251
+ try:
776
1252
  if hasattr(response, "choices") and response.choices:
777
1253
  choice = response.choices[0]
1254
+
778
1255
  if hasattr(choice, "message") and choice.message:
779
- ai_output = choice.message.content or ""
1256
+ message = choice.message
1257
+
1258
+ # Handle regular text content
1259
+ if hasattr(message, "content") and message.content:
1260
+ ai_output = message.content
1261
+
1262
+ # Handle function/tool calls
1263
+ elif hasattr(message, "tool_calls") and message.tool_calls:
1264
+ tool_descriptions = []
1265
+ for tool_call in message.tool_calls:
1266
+ if hasattr(tool_call, "function"):
1267
+ func_name = tool_call.function.name
1268
+ func_args = tool_call.function.arguments
1269
+ tool_descriptions.append(
1270
+ f"Called {func_name} with {func_args}"
1271
+ )
1272
+ ai_output = "[Tool calls: " + "; ".join(tool_descriptions) + "]"
1273
+
1274
+ # Handle function calls (legacy format)
1275
+ elif hasattr(message, "function_call") and message.function_call:
1276
+ func_call = message.function_call
1277
+ func_name = func_call.get("name", "unknown")
1278
+ func_args = func_call.get("arguments", "{}")
1279
+ ai_output = f"[Function call: {func_name} with {func_args}]"
780
1280
 
781
- # Calculate tokens
782
- tokens_used = 0
1281
+ else:
1282
+ ai_output = "[No content - possible function/tool call]"
1283
+
1284
+ except Exception as e:
1285
+ logger.debug(f"Error extracting AI output: {e}")
1286
+ ai_output = "[Error extracting AI response]"
1287
+
1288
+ return ai_output
1289
+
1290
+ def _extract_openai_metadata(
1291
+ self, kwargs: Dict, response, tokens_used: int
1292
+ ) -> Dict:
1293
+ """Extract comprehensive metadata from OpenAI request and response"""
1294
+ metadata = {
1295
+ "integration": "openai_auto",
1296
+ "api_type": "chat_completions",
1297
+ "tokens_used": tokens_used,
1298
+ "auto_recorded": True,
1299
+ }
1300
+
1301
+ try:
1302
+ # Add request metadata
1303
+ if "temperature" in kwargs:
1304
+ metadata["temperature"] = kwargs["temperature"]
1305
+ if "max_tokens" in kwargs:
1306
+ metadata["max_tokens"] = kwargs["max_tokens"]
1307
+ if "tools" in kwargs:
1308
+ metadata["has_tools"] = True
1309
+ metadata["tool_count"] = len(kwargs["tools"])
1310
+ if "functions" in kwargs:
1311
+ metadata["has_functions"] = True
1312
+ metadata["function_count"] = len(kwargs["functions"])
1313
+
1314
+ # Add response metadata
1315
+ if hasattr(response, "choices") and response.choices:
1316
+ choice = response.choices[0]
1317
+ if hasattr(choice, "finish_reason"):
1318
+ metadata["finish_reason"] = choice.finish_reason
1319
+
1320
+ # Add detailed token usage if available
783
1321
  if hasattr(response, "usage") and response.usage:
784
- tokens_used = getattr(response.usage, "total_tokens", 0)
1322
+ usage = response.usage
1323
+ metadata.update(
1324
+ {
1325
+ "prompt_tokens": getattr(usage, "prompt_tokens", 0),
1326
+ "completion_tokens": getattr(usage, "completion_tokens", 0),
1327
+ "total_tokens": getattr(usage, "total_tokens", 0),
1328
+ }
1329
+ )
1330
+
1331
+ # Detect content types
1332
+ messages = kwargs.get("messages", [])
1333
+ has_images = False
1334
+ message_count = len(messages)
1335
+
1336
+ for message in messages:
1337
+ if message.get("role") == "user":
1338
+ content = message.get("content")
1339
+ if isinstance(content, list):
1340
+ for item in content:
1341
+ if (
1342
+ isinstance(item, dict)
1343
+ and item.get("type") == "image_url"
1344
+ ):
1345
+ has_images = True
1346
+ break
1347
+ if has_images:
1348
+ break
1349
+
1350
+ metadata["message_count"] = message_count
1351
+ metadata["has_images"] = has_images
1352
+
1353
+ except Exception as e:
1354
+ logger.debug(f"Error extracting metadata: {e}")
1355
+
1356
+ return metadata
1357
+
1358
+ def _record_anthropic_conversation(self, kwargs, response):
1359
+ """Record Anthropic conversation with enhanced content parsing"""
1360
+ try:
1361
+ messages = kwargs.get("messages", [])
1362
+ model = kwargs.get("model", "claude-unknown")
1363
+
1364
+ # Extract user input with enhanced parsing
1365
+ user_input = self._extract_anthropic_user_input(messages)
1366
+
1367
+ # Extract AI response with enhanced parsing
1368
+ ai_output = self._extract_anthropic_ai_output(response)
1369
+
1370
+ # Calculate tokens
1371
+ tokens_used = self._extract_anthropic_tokens(response)
1372
+
1373
+ # Enhanced metadata extraction
1374
+ metadata = self._extract_anthropic_metadata(kwargs, response, tokens_used)
785
1375
 
786
1376
  # Record conversation
787
1377
  self.record_conversation(
788
1378
  user_input=user_input,
789
1379
  ai_output=ai_output,
790
1380
  model=model,
791
- metadata={
792
- "integration": "openai_auto",
793
- "api_type": "chat_completions",
794
- "tokens_used": tokens_used,
795
- "auto_recorded": True,
796
- },
1381
+ metadata=metadata,
797
1382
  )
798
1383
  except Exception as e:
799
- logger.error(f"Failed to record OpenAI conversation: {e}")
1384
+ logger.error(f"Failed to record Anthropic conversation: {e}")
800
1385
 
801
- def _record_anthropic_conversation(self, kwargs, response):
802
- """Record Anthropic conversation"""
1386
+ def _extract_anthropic_user_input(self, messages: List[Dict]) -> str:
1387
+ """Extract user input from Anthropic messages with support for complex content types"""
1388
+ user_input = ""
803
1389
  try:
804
- messages = kwargs.get("messages", [])
805
- model = kwargs.get("model", "claude-unknown")
806
-
807
- # Extract user input
808
- user_input = ""
1390
+ # Find the last user message
809
1391
  for message in reversed(messages):
810
1392
  if message.get("role") == "user":
811
1393
  content = message.get("content", "")
812
- if isinstance(content, list):
813
- user_input = " ".join(
814
- [
815
- block.get("text", "")
816
- for block in content
817
- if isinstance(block, dict)
818
- and block.get("type") == "text"
819
- ]
820
- )
821
- else:
1394
+
1395
+ if isinstance(content, str):
1396
+ # Simple string content
822
1397
  user_input = content
1398
+ elif isinstance(content, list):
1399
+ # Complex content (vision, multiple parts)
1400
+ text_parts = []
1401
+ image_count = 0
1402
+
1403
+ for block in content:
1404
+ if isinstance(block, dict):
1405
+ if block.get("type") == "text":
1406
+ text_parts.append(block.get("text", ""))
1407
+ elif block.get("type") == "image":
1408
+ image_count += 1
1409
+
1410
+ user_input = " ".join(text_parts)
1411
+ # Add image indicator if present
1412
+ if image_count > 0:
1413
+ user_input += f" [Contains {image_count} image(s)]"
1414
+
823
1415
  break
1416
+ except Exception as e:
1417
+ logger.debug(f"Error extracting Anthropic user input: {e}")
1418
+ user_input = "[Error extracting user input]"
824
1419
 
825
- # Extract AI response
826
- ai_output = ""
1420
+ return user_input
1421
+
1422
+ def _extract_anthropic_ai_output(self, response) -> str:
1423
+ """Extract AI output from Anthropic response with support for various response types"""
1424
+ ai_output = ""
1425
+ try:
827
1426
  if hasattr(response, "content") and response.content:
828
1427
  if isinstance(response.content, list):
829
- ai_output = " ".join(
830
- [
831
- block.text
832
- for block in response.content
833
- if hasattr(block, "text")
834
- ]
835
- )
1428
+ # Handle structured content (text blocks, tool use, etc.)
1429
+ text_parts = []
1430
+ tool_uses = []
1431
+
1432
+ for block in response.content:
1433
+ try:
1434
+ # Handle text blocks
1435
+ if hasattr(block, "text") and block.text:
1436
+ text_parts.append(block.text)
1437
+ # Handle tool use blocks
1438
+ elif hasattr(block, "type"):
1439
+ block_type = getattr(block, "type", None)
1440
+ if block_type == "tool_use":
1441
+ tool_name = getattr(block, "name", "unknown")
1442
+ tool_input = getattr(block, "input", {})
1443
+ tool_uses.append(
1444
+ f"Used {tool_name} with {tool_input}"
1445
+ )
1446
+ # Handle mock objects for testing (when type is accessible but not via hasattr)
1447
+ elif hasattr(block, "name") and hasattr(block, "input"):
1448
+ tool_name = getattr(block, "name", "unknown")
1449
+ tool_input = getattr(block, "input", {})
1450
+ tool_uses.append(f"Used {tool_name} with {tool_input}")
1451
+ except Exception as block_error:
1452
+ logger.debug(f"Error processing block: {block_error}")
1453
+ continue
1454
+
1455
+ ai_output = " ".join(text_parts)
1456
+ if tool_uses:
1457
+ if ai_output:
1458
+ ai_output += " "
1459
+ ai_output += "[Tool uses: " + "; ".join(tool_uses) + "]"
1460
+
1461
+ elif isinstance(response.content, str):
1462
+ ai_output = response.content
836
1463
  else:
837
1464
  ai_output = str(response.content)
838
1465
 
839
- # Calculate tokens
840
- tokens_used = 0
1466
+ except Exception as e:
1467
+ logger.debug(f"Error extracting Anthropic AI output: {e}")
1468
+ ai_output = "[Error extracting AI response]"
1469
+
1470
+ return ai_output
1471
+
1472
+ def _extract_anthropic_tokens(self, response) -> int:
1473
+ """Extract token usage from Anthropic response"""
1474
+ tokens_used = 0
1475
+ try:
841
1476
  if hasattr(response, "usage") and response.usage:
842
1477
  input_tokens = getattr(response.usage, "input_tokens", 0)
843
1478
  output_tokens = getattr(response.usage, "output_tokens", 0)
844
1479
  tokens_used = input_tokens + output_tokens
1480
+ except Exception as e:
1481
+ logger.debug(f"Error extracting Anthropic tokens: {e}")
1482
+
1483
+ return tokens_used
1484
+
1485
+ def _extract_anthropic_metadata(
1486
+ self, kwargs: Dict, response, tokens_used: int
1487
+ ) -> Dict:
1488
+ """Extract comprehensive metadata from Anthropic request and response"""
1489
+ metadata = {
1490
+ "integration": "anthropic_auto",
1491
+ "api_type": "messages",
1492
+ "tokens_used": tokens_used,
1493
+ "auto_recorded": True,
1494
+ }
1495
+
1496
+ try:
1497
+ # Add request metadata
1498
+ if "temperature" in kwargs:
1499
+ metadata["temperature"] = kwargs["temperature"]
1500
+ if "max_tokens" in kwargs:
1501
+ metadata["max_tokens"] = kwargs["max_tokens"]
1502
+ if "tools" in kwargs:
1503
+ metadata["has_tools"] = True
1504
+ metadata["tool_count"] = len(kwargs["tools"])
1505
+
1506
+ # Add response metadata
1507
+ if hasattr(response, "stop_reason"):
1508
+ metadata["stop_reason"] = response.stop_reason
1509
+ if hasattr(response, "model"):
1510
+ metadata["response_model"] = response.model
1511
+
1512
+ # Add detailed token usage if available
1513
+ if hasattr(response, "usage") and response.usage:
1514
+ usage = response.usage
1515
+ metadata.update(
1516
+ {
1517
+ "input_tokens": getattr(usage, "input_tokens", 0),
1518
+ "output_tokens": getattr(usage, "output_tokens", 0),
1519
+ "total_tokens": tokens_used,
1520
+ }
1521
+ )
1522
+
1523
+ # Detect content types
1524
+ messages = kwargs.get("messages", [])
1525
+ has_images = False
1526
+ message_count = len(messages)
1527
+
1528
+ for message in messages:
1529
+ if message.get("role") == "user":
1530
+ content = message.get("content")
1531
+ if isinstance(content, list):
1532
+ for item in content:
1533
+ if isinstance(item, dict) and item.get("type") == "image":
1534
+ has_images = True
1535
+ break
1536
+ if has_images:
1537
+ break
1538
+
1539
+ metadata["message_count"] = message_count
1540
+ metadata["has_images"] = has_images
845
1541
 
846
- # Record conversation
847
- self.record_conversation(
848
- user_input=user_input,
849
- ai_output=ai_output,
850
- model=model,
851
- metadata={
852
- "integration": "anthropic_auto",
853
- "api_type": "messages",
854
- "tokens_used": tokens_used,
855
- "auto_recorded": True,
856
- },
857
- )
858
1542
  except Exception as e:
859
- logger.error(f"Failed to record Anthropic conversation: {e}")
1543
+ logger.debug(f"Error extracting Anthropic metadata: {e}")
860
1544
 
861
- def _litellm_success_callback(self, kwargs, response, start_time, end_time):
862
- """
863
- This function is automatically called by LiteLLM after a successful completion.
864
- """
1545
+ return metadata
1546
+
1547
+ def _process_litellm_response(self, kwargs, response, start_time, end_time):
1548
+ """Process and record LiteLLM response"""
865
1549
  try:
1550
+ # Extract user input from messages
1551
+ messages = kwargs.get("messages", [])
866
1552
  user_input = ""
867
- # Find the last user message
868
- for msg in reversed(kwargs.get("messages", [])):
869
- if msg.get("role") == "user":
870
- user_input = msg.get("content", "")
1553
+
1554
+ for message in reversed(messages):
1555
+ if message.get("role") == "user":
1556
+ user_input = message.get("content", "")
871
1557
  break
872
1558
 
873
- ai_output = response.choices[0].message.content or ""
874
- model = kwargs.get("model", "unknown")
1559
+ # Extract AI output from response
1560
+ ai_output = ""
1561
+ if hasattr(response, "choices") and response.choices:
1562
+ choice = response.choices[0]
1563
+ if hasattr(choice, "message") and hasattr(choice.message, "content"):
1564
+ ai_output = choice.message.content or ""
1565
+ elif hasattr(choice, "text"):
1566
+ ai_output = choice.text or ""
1567
+
1568
+ # Extract model
1569
+ model = kwargs.get("model", "litellm-unknown")
1570
+
1571
+ # Calculate timing (convert to seconds for JSON serialization)
1572
+ duration_seconds = (end_time - start_time) if start_time and end_time else 0
1573
+ if hasattr(duration_seconds, "total_seconds"):
1574
+ duration_seconds = duration_seconds.total_seconds()
1575
+
1576
+ # Prepare metadata
1577
+ metadata = {
1578
+ "integration": "litellm",
1579
+ "auto_recorded": True,
1580
+ "duration": float(duration_seconds),
1581
+ "timestamp": time.time(),
1582
+ }
875
1583
 
876
- # Calculate tokens used
877
- tokens_used = 0
1584
+ # Add token usage if available
878
1585
  if hasattr(response, "usage") and response.usage:
879
- tokens_used = getattr(response.usage, "total_tokens", 0)
1586
+ usage = response.usage
1587
+ metadata.update(
1588
+ {
1589
+ "prompt_tokens": getattr(usage, "prompt_tokens", 0),
1590
+ "completion_tokens": getattr(usage, "completion_tokens", 0),
1591
+ "total_tokens": getattr(usage, "total_tokens", 0),
1592
+ }
1593
+ )
880
1594
 
881
- # Handle timing data safely - convert any time objects to float/string
882
- duration_ms = 0
883
- start_time_str = None
884
- end_time_str = None
1595
+ # Record the conversation
1596
+ if user_input and ai_output:
1597
+ self.record_conversation(
1598
+ user_input=user_input,
1599
+ ai_output=ai_output,
1600
+ model=model,
1601
+ metadata=metadata,
1602
+ )
885
1603
 
886
- try:
887
- if start_time is not None and end_time is not None:
888
- # Handle different types of time objects
889
- if hasattr(start_time, "total_seconds"): # timedelta
890
- duration_ms = start_time.total_seconds() * 1000
891
- elif isinstance(start_time, (int, float)) and isinstance(
892
- end_time, (int, float)
893
- ):
894
- duration_ms = (end_time - start_time) * 1000
1604
+ except Exception as e:
1605
+ logger.error(f"Failed to process LiteLLM response: {e}")
895
1606
 
896
- start_time_str = str(start_time)
897
- end_time_str = str(end_time)
898
- except Exception:
899
- # If timing calculation fails, just skip it
900
- pass
1607
+ # LiteLLM callback is now handled by the LiteLLMCallbackManager
1608
+ # in memori.integrations.litellm_integration
901
1609
 
902
- self.record_conversation(
903
- user_input,
904
- ai_output,
905
- model,
906
- metadata={
907
- "integration": "litellm",
908
- "api_type": "completion",
909
- "tokens_used": tokens_used,
910
- "auto_recorded": True,
911
- "start_time_str": start_time_str,
912
- "end_time_str": end_time_str,
913
- "duration_ms": duration_ms,
914
- },
1610
+ def _process_memory_sync(
1611
+ self, chat_id: str, user_input: str, ai_output: str, model: str = "unknown"
1612
+ ):
1613
+ """Synchronous memory processing fallback"""
1614
+ if not self.memory_agent:
1615
+ logger.warning("Memory agent not available, skipping memory ingestion")
1616
+ return
1617
+
1618
+ try:
1619
+ # Run async processing in new event loop
1620
+ import threading
1621
+
1622
+ def run_memory_processing():
1623
+ new_loop = asyncio.new_event_loop()
1624
+ asyncio.set_event_loop(new_loop)
1625
+ try:
1626
+ new_loop.run_until_complete(
1627
+ self._process_memory_async(
1628
+ chat_id, user_input, ai_output, model
1629
+ )
1630
+ )
1631
+ except Exception as e:
1632
+ logger.error(f"Synchronous memory processing failed: {e}")
1633
+ finally:
1634
+ new_loop.close()
1635
+
1636
+ # Run in background thread to avoid blocking
1637
+ thread = threading.Thread(target=run_memory_processing, daemon=True)
1638
+ thread.start()
1639
+ logger.debug(
1640
+ f"Memory processing started in background thread for {chat_id}"
915
1641
  )
1642
+
916
1643
  except Exception as e:
917
- logger.error(f"Memori callback failed: {e}")
1644
+ logger.error(f"Failed to start synchronous memory processing: {e}")
1645
+
1646
+ def _parse_llm_response(self, response) -> tuple[str, str]:
1647
+ """Extract text and model from various LLM response formats."""
1648
+ if response is None:
1649
+ return "", "unknown"
1650
+
1651
+ # String response
1652
+ if isinstance(response, str):
1653
+ return response, "unknown"
1654
+
1655
+ # Anthropic response
1656
+ if hasattr(response, "content"):
1657
+ text = ""
1658
+ if isinstance(response.content, list):
1659
+ text = "".join(b.text for b in response.content if hasattr(b, "text"))
1660
+ else:
1661
+ text = str(response.content)
1662
+ return text, getattr(response, "model", "unknown")
1663
+
1664
+ # OpenAI response
1665
+ if hasattr(response, "choices") and response.choices:
1666
+ choice = response.choices[0]
1667
+ text = (
1668
+ getattr(choice.message, "content", "")
1669
+ if hasattr(choice, "message")
1670
+ else getattr(choice, "text", "")
1671
+ )
1672
+ return text or "", getattr(response, "model", "unknown")
1673
+
1674
+ # Dict response
1675
+ if isinstance(response, dict):
1676
+ return response.get(
1677
+ "content", response.get("text", str(response))
1678
+ ), response.get("model", "unknown")
1679
+
1680
+ # Fallback
1681
+ return str(response), "unknown"
918
1682
 
919
1683
  def record_conversation(
920
1684
  self,
921
1685
  user_input: str,
922
- ai_output: str,
923
- model: str = "unknown",
1686
+ ai_output=None,
1687
+ model: str = None,
924
1688
  metadata: Optional[Dict[str, Any]] = None,
925
1689
  ) -> str:
926
1690
  """
927
- Manually record a conversation
1691
+ Record a conversation.
928
1692
 
929
1693
  Args:
930
- user_input: The user's input message
931
- ai_output: The AI's response
932
- model: Model used for the response
933
- metadata: Additional metadata
1694
+ user_input: User's message
1695
+ ai_output: AI response (any format)
1696
+ model: Optional model name override
1697
+ metadata: Optional metadata
934
1698
 
935
1699
  Returns:
936
- chat_id: Unique identifier for this conversation
1700
+ chat_id: Unique conversation ID
937
1701
  """
938
1702
  if not self._enabled:
939
1703
  raise MemoriError("Memori is not enabled. Call enable() first.")
940
1704
 
941
- # Ensure ai_output is never None to avoid NOT NULL constraint errors
942
- if ai_output is None:
943
- ai_output = ""
1705
+ # Parse response
1706
+ response_text, detected_model = self._parse_llm_response(ai_output)
1707
+ response_model = model or detected_model
944
1708
 
1709
+ # Generate ID and timestamp
945
1710
  chat_id = str(uuid.uuid4())
946
1711
  timestamp = datetime.now()
947
1712
 
948
- try:
949
- # Store in chat history
950
- self.db_manager.store_chat_history(
951
- chat_id=chat_id,
952
- user_input=user_input,
953
- ai_output=ai_output,
954
- model=model,
955
- timestamp=timestamp,
956
- session_id=self._session_id,
957
- namespace=self.namespace,
958
- metadata=metadata or {},
959
- )
1713
+ # Store conversation
1714
+ self.db_manager.store_chat_history(
1715
+ chat_id=chat_id,
1716
+ user_input=user_input,
1717
+ ai_output=response_text,
1718
+ model=response_model,
1719
+ timestamp=timestamp,
1720
+ session_id=self._session_id,
1721
+ namespace=self.namespace,
1722
+ metadata=metadata or {},
1723
+ )
960
1724
 
961
- # Process for memory categorization
962
- if self.conscious_ingest:
963
- self._process_memory_ingestion(chat_id, user_input, ai_output, model)
1725
+ # Always process into long-term memory when memory agent is available
1726
+ if self.memory_agent:
1727
+ self._schedule_memory_processing(
1728
+ chat_id, user_input, response_text, response_model
1729
+ )
964
1730
 
965
- logger.debug(f"Conversation recorded: {chat_id}")
966
- return chat_id
1731
+ logger.debug(f"Recorded conversation: {chat_id}")
1732
+ return chat_id
967
1733
 
968
- except Exception as e:
969
- raise MemoriError(f"Failed to record conversation: {e}")
1734
+ def _schedule_memory_processing(
1735
+ self, chat_id: str, user_input: str, ai_output: str, model: str
1736
+ ):
1737
+ """Schedule memory processing (async if possible, sync fallback)."""
1738
+ try:
1739
+ loop = asyncio.get_running_loop()
1740
+ task = loop.create_task(
1741
+ self._process_memory_async(chat_id, user_input, ai_output, model)
1742
+ )
970
1743
 
971
- def _process_memory_ingestion(
1744
+ # Prevent garbage collection
1745
+ if not hasattr(self, "_memory_tasks"):
1746
+ self._memory_tasks = set()
1747
+ self._memory_tasks.add(task)
1748
+ task.add_done_callback(self._memory_tasks.discard)
1749
+ except RuntimeError:
1750
+ # No event loop, use sync fallback
1751
+ logger.debug("No event loop, using synchronous memory processing")
1752
+ self._process_memory_sync(chat_id, user_input, ai_output, model)
1753
+
1754
+ async def _process_memory_async(
972
1755
  self, chat_id: str, user_input: str, ai_output: str, model: str = "unknown"
973
1756
  ):
974
- """Process conversation for Pydantic-based memory categorization"""
1757
+ """Process conversation with enhanced async memory categorization"""
975
1758
  if not self.memory_agent:
976
1759
  logger.warning("Memory agent not available, skipping memory ingestion")
977
1760
  return
@@ -988,38 +1771,107 @@ class Memori:
988
1771
  relevant_skills=self._user_context.get("relevant_skills", []),
989
1772
  )
990
1773
 
991
- # Process conversation using Pydantic-based memory agent
992
- processed_memory = self.memory_agent.process_conversation_sync(
1774
+ # Get recent memories for deduplication
1775
+ existing_memories = await self._get_recent_memories_for_dedup()
1776
+
1777
+ # Process conversation using async Pydantic-based memory agent
1778
+ processed_memory = await self.memory_agent.process_conversation_async(
993
1779
  chat_id=chat_id,
994
1780
  user_input=user_input,
995
1781
  ai_output=ai_output,
996
1782
  context=context,
997
- mem_prompt=self.mem_prompt,
998
- filters=self.memory_filters,
1783
+ existing_memories=(
1784
+ [mem.summary for mem in existing_memories[:10]]
1785
+ if existing_memories
1786
+ else []
1787
+ ),
999
1788
  )
1000
1789
 
1001
- # Store processed memory with entity indexing
1002
- if processed_memory.should_store:
1003
- memory_id = self.db_manager.store_processed_memory(
1004
- memory=processed_memory, chat_id=chat_id, namespace=self.namespace
1005
- )
1790
+ # Check for duplicates
1791
+ duplicate_id = await self.memory_agent.detect_duplicates(
1792
+ processed_memory, existing_memories
1793
+ )
1006
1794
 
1007
- if memory_id:
1008
- logger.debug(
1009
- f"Stored processed memory {memory_id} for chat {chat_id}"
1010
- )
1011
- else:
1012
- logger.debug(
1013
- f"Memory not stored for chat {chat_id}: {processed_memory.storage_reasoning}"
1795
+ if duplicate_id:
1796
+ processed_memory.duplicate_of = duplicate_id
1797
+ logger.info(f"Memory marked as duplicate of {duplicate_id}")
1798
+
1799
+ # Apply filters
1800
+ if self.memory_agent.should_filter_memory(
1801
+ processed_memory, self.memory_filters
1802
+ ):
1803
+ logger.debug(f"Memory filtered out for chat {chat_id}")
1804
+ return
1805
+
1806
+ # Store processed memory with new schema
1807
+ memory_id = self.db_manager.store_long_term_memory_enhanced(
1808
+ processed_memory, chat_id, self.namespace
1809
+ )
1810
+
1811
+ if memory_id:
1812
+ logger.debug(f"Stored processed memory {memory_id} for chat {chat_id}")
1813
+
1814
+ # Check for conscious context updates if promotion eligible and conscious_ingest enabled
1815
+ if (
1816
+ processed_memory.promotion_eligible
1817
+ and self.conscious_agent
1818
+ and self.conscious_ingest
1819
+ ):
1820
+ await self.conscious_agent.check_for_context_updates(
1821
+ self.db_manager, self.namespace
1014
1822
  )
1015
1823
  else:
1016
- logger.debug(
1017
- f"Memory not stored for chat {chat_id}: {processed_memory.storage_reasoning}"
1018
- )
1824
+ logger.warning(f"Failed to store memory for chat {chat_id}")
1019
1825
 
1020
1826
  except Exception as e:
1021
1827
  logger.error(f"Memory ingestion failed for {chat_id}: {e}")
1022
1828
 
1829
+ async def _get_recent_memories_for_dedup(self) -> List:
1830
+ """Get recent memories for deduplication check"""
1831
+ try:
1832
+ from sqlalchemy import text
1833
+
1834
+ from ..database.queries.memory_queries import MemoryQueries
1835
+ from ..utils.pydantic_models import ProcessedLongTermMemory
1836
+
1837
+ with self.db_manager._get_connection() as connection:
1838
+ result = connection.execute(
1839
+ text(MemoryQueries.SELECT_MEMORIES_FOR_DEDUPLICATION),
1840
+ {
1841
+ "namespace": self.namespace,
1842
+ "processed_for_duplicates": False,
1843
+ "limit": 20,
1844
+ },
1845
+ )
1846
+
1847
+ memories = []
1848
+ for row in result:
1849
+ try:
1850
+ # Create ProcessedLongTermMemory objects for proper comparison
1851
+ # Note: Query returns (memory_id, summary, searchable_content, classification, created_at)
1852
+ memory = ProcessedLongTermMemory(
1853
+ conversation_id=row[
1854
+ 0
1855
+ ], # Use memory_id as conversation_id for existing memories
1856
+ summary=row[1] or "",
1857
+ content=row[2] or "",
1858
+ classification=row[3] or "conversational",
1859
+ importance="medium", # Default importance level for comparison
1860
+ promotion_eligible=False, # Default for existing memories
1861
+ classification_reason="Existing memory loaded for deduplication check", # Required field
1862
+ )
1863
+ memories.append(memory)
1864
+ except Exception as e:
1865
+ # Silently skip malformed memories from old data format
1866
+ logger.debug(f"Skipping malformed memory during dedup: {e}")
1867
+ continue
1868
+
1869
+ return memories
1870
+
1871
+ except Exception as e:
1872
+ logger.error(f"Failed to get recent memories for dedup: {e}")
1873
+ return []
1874
+
1023
1875
  def retrieve_context(self, query: str, limit: int = 5) -> List[Dict[str, Any]]:
1024
1876
  """
1025
1877
  Retrieve relevant context for a query with priority on essential facts
@@ -1123,10 +1975,13 @@ class Memori:
1123
1975
  return self._session_id
1124
1976
 
1125
1977
  def get_integration_stats(self) -> List[Dict[str, Any]]:
1126
- """Get statistics from the universal integration system"""
1978
+ """Get statistics from the new interceptor system"""
1127
1979
  try:
1980
+ # Get system status first
1981
+ interceptor_status = self.get_interceptor_status()
1982
+
1128
1983
  stats = {
1129
- "integration": "universal_auto_recording",
1984
+ "integration": "memori_system",
1130
1985
  "enabled": self._enabled,
1131
1986
  "session_id": self._session_id,
1132
1987
  "namespace": self.namespace,
@@ -1134,58 +1989,62 @@ class Memori:
1134
1989
  }
1135
1990
 
1136
1991
  # LiteLLM stats
1992
+ litellm_interceptor_status = interceptor_status.get("native", {})
1137
1993
  if LITELLM_AVAILABLE:
1138
1994
  stats["providers"]["litellm"] = {
1139
1995
  "available": True,
1140
1996
  "method": "native_callbacks",
1141
- "callback_registered": self._enabled,
1142
- "callbacks_count": len(success_callback) if self._enabled else 0,
1997
+ "enabled": litellm_interceptor_status.get("enabled", False),
1998
+ "status": litellm_interceptor_status.get("status", "unknown"),
1143
1999
  }
1144
2000
  else:
1145
2001
  stats["providers"]["litellm"] = {
1146
2002
  "available": False,
1147
2003
  "method": "native_callbacks",
1148
- "callback_registered": False,
2004
+ "enabled": False,
1149
2005
  }
1150
2006
 
2007
+ # Get interceptor status instead of checking wrapped attributes
2008
+ interceptor_status = self.get_interceptor_status()
2009
+
1151
2010
  # OpenAI stats
1152
2011
  try:
1153
2012
  import openai
1154
2013
 
2014
+ _ = openai # Suppress unused import warning
2015
+
2016
+ openai_interceptor_status = interceptor_status.get("openai", {})
1155
2017
  stats["providers"]["openai"] = {
1156
2018
  "available": True,
1157
- "method": "auto_wrapping",
1158
- "wrapped": (
1159
- hasattr(openai.OpenAI, "_memori_wrapped")
1160
- if hasattr(openai, "OpenAI")
1161
- else False
1162
- ),
2019
+ "method": "litellm_native",
2020
+ "enabled": openai_interceptor_status.get("enabled", False),
2021
+ "status": openai_interceptor_status.get("status", "unknown"),
1163
2022
  }
1164
2023
  except ImportError:
1165
2024
  stats["providers"]["openai"] = {
1166
2025
  "available": False,
1167
- "method": "auto_wrapping",
1168
- "wrapped": False,
2026
+ "method": "litellm_native",
2027
+ "enabled": False,
1169
2028
  }
1170
2029
 
1171
2030
  # Anthropic stats
1172
2031
  try:
1173
2032
  import anthropic
1174
2033
 
2034
+ _ = anthropic # Suppress unused import warning
2035
+
2036
+ anthropic_interceptor_status = interceptor_status.get("anthropic", {})
1175
2037
  stats["providers"]["anthropic"] = {
1176
2038
  "available": True,
1177
- "method": "auto_wrapping",
1178
- "wrapped": (
1179
- hasattr(anthropic.Anthropic, "_memori_wrapped")
1180
- if hasattr(anthropic, "Anthropic")
1181
- else False
1182
- ),
2039
+ "method": "litellm_native",
2040
+ "enabled": anthropic_interceptor_status.get("enabled", False),
2041
+ "status": anthropic_interceptor_status.get("status", "unknown"),
1183
2042
  }
1184
2043
  except ImportError:
1185
2044
  stats["providers"]["anthropic"] = {
1186
2045
  "available": False,
1187
- "method": "auto_wrapping",
1188
- "wrapped": False,
2046
+ "method": "litellm_native",
2047
+ "enabled": False,
1189
2048
  }
1190
2049
 
1191
2050
  return [stats]
@@ -1230,7 +2089,7 @@ class Memori:
1230
2089
  """Get memories that contain a specific entity"""
1231
2090
  try:
1232
2091
  # This would use the entity index in the database
1233
- # For now, use keyword search as fallback
2092
+ # For now, use keyword search as fallback (entity_type is ignored for now)
1234
2093
  return self.db_manager.search_memories(
1235
2094
  query=entity_value, namespace=self.namespace, limit=limit
1236
2095
  )
@@ -1269,11 +2128,25 @@ class Memori:
1269
2128
 
1270
2129
  # If we have a running loop, schedule the task
1271
2130
  self._background_task = loop.create_task(self._background_analysis_loop())
2131
+ # Add proper error handling callback
2132
+ self._background_task.add_done_callback(
2133
+ self._handle_background_task_completion
2134
+ )
1272
2135
  logger.info("Background analysis task started")
1273
2136
 
1274
2137
  except Exception as e:
1275
2138
  logger.error(f"Failed to start background analysis: {e}")
1276
2139
 
2140
+ def _handle_background_task_completion(self, task):
2141
+ """Handle background task completion and cleanup"""
2142
+ try:
2143
+ if task.exception():
2144
+ logger.error(f"Background task failed: {task.exception()}")
2145
+ except asyncio.CancelledError:
2146
+ logger.debug("Background task was cancelled")
2147
+ except Exception as e:
2148
+ logger.error(f"Error handling background task completion: {e}")
2149
+
1277
2150
  def _stop_background_analysis(self):
1278
2151
  """Stop the background analysis task"""
1279
2152
  try:
@@ -1283,32 +2156,66 @@ class Memori:
1283
2156
  except Exception as e:
1284
2157
  logger.error(f"Failed to stop background analysis: {e}")
1285
2158
 
2159
+ def cleanup(self):
2160
+ """Clean up all async tasks and resources"""
2161
+ try:
2162
+ # Cancel background tasks
2163
+ self._stop_background_analysis()
2164
+
2165
+ # Clean up memory processing tasks
2166
+ if hasattr(self, "_memory_tasks"):
2167
+ for task in self._memory_tasks.copy():
2168
+ if not task.done():
2169
+ task.cancel()
2170
+ self._memory_tasks.clear()
2171
+
2172
+ logger.debug("Memori cleanup completed")
2173
+ except Exception as e:
2174
+ logger.error(f"Error during cleanup: {e}")
2175
+
2176
+ def __del__(self):
2177
+ """Destructor to ensure cleanup"""
2178
+ try:
2179
+ self.cleanup()
2180
+ except:
2181
+ pass # Ignore errors during destruction
2182
+
1286
2183
  async def _background_analysis_loop(self):
1287
- """Main background analysis loop"""
1288
- logger.info("ConsciouscAgent: Background analysis loop started")
2184
+ """Background analysis loop for memory processing"""
2185
+ try:
2186
+ logger.debug("Background analysis loop started")
1289
2187
 
1290
- while self._enabled and self.conscious_ingest:
1291
- try:
1292
- if self.conscious_agent and self.conscious_agent.should_run_analysis():
1293
- await self.conscious_agent.run_background_analysis(
1294
- self.db_manager, self.namespace
1295
- )
2188
+ # For now, just run periodic conscious ingestion if enabled
2189
+ if self.conscious_ingest and self.conscious_agent:
2190
+ while True:
2191
+ try:
2192
+ await asyncio.sleep(300) # Check every 5 minutes
1296
2193
 
1297
- # Wait 30 minutes before next check
1298
- await asyncio.sleep(1800) # 30 minutes
2194
+ # Run conscious ingestion to check for new promotable memories
2195
+ await self.conscious_agent.run_conscious_ingest(
2196
+ self.db_manager, self.namespace
2197
+ )
2198
+
2199
+ logger.debug("Periodic conscious analysis completed")
1299
2200
 
1300
- except asyncio.CancelledError:
1301
- logger.info("ConsciouscAgent: Background analysis cancelled")
1302
- break
1303
- except Exception as e:
1304
- logger.error(f"ConsciouscAgent: Background analysis error: {e}")
1305
- # Wait 5 minutes before retrying on error
1306
- await asyncio.sleep(300)
2201
+ except asyncio.CancelledError:
2202
+ logger.debug("Background analysis loop cancelled")
2203
+ break
2204
+ except Exception as e:
2205
+ logger.error(f"Background analysis error: {e}")
2206
+ await asyncio.sleep(60) # Wait 1 minute before retry
2207
+ else:
2208
+ # If not using conscious ingest, just sleep
2209
+ while True:
2210
+ await asyncio.sleep(3600) # Sleep for 1 hour
1307
2211
 
1308
- logger.info("ConsciouscAgent: Background analysis loop ended")
2212
+ except asyncio.CancelledError:
2213
+ logger.debug("Background analysis loop cancelled")
2214
+ except Exception as e:
2215
+ logger.error(f"Background analysis loop failed: {e}")
1309
2216
 
1310
2217
  def trigger_conscious_analysis(self):
1311
- """Manually trigger conscious agent analysis (for testing/immediate analysis)"""
2218
+ """Manually trigger conscious context ingestion (for testing/immediate analysis)"""
1312
2219
  if not self.conscious_ingest or not self.conscious_agent:
1313
2220
  logger.warning("Conscious ingestion not enabled or agent not available")
1314
2221
  return
@@ -1318,11 +2225,11 @@ class Memori:
1318
2225
  try:
1319
2226
  loop = asyncio.get_running_loop()
1320
2227
  task = loop.create_task(
1321
- self.conscious_agent.run_background_analysis(
2228
+ self.conscious_agent.run_conscious_ingest(
1322
2229
  self.db_manager, self.namespace
1323
2230
  )
1324
2231
  )
1325
- logger.info("Conscious analysis triggered")
2232
+ logger.info("Conscious context ingestion triggered")
1326
2233
  return task
1327
2234
  except RuntimeError:
1328
2235
  # No event loop, run synchronously in thread
@@ -1333,7 +2240,7 @@ class Memori:
1333
2240
  asyncio.set_event_loop(new_loop)
1334
2241
  try:
1335
2242
  new_loop.run_until_complete(
1336
- self.conscious_agent.run_background_analysis(
2243
+ self.conscious_agent.run_conscious_ingest(
1337
2244
  self.db_manager, self.namespace
1338
2245
  )
1339
2246
  )
@@ -1342,29 +2249,170 @@ class Memori:
1342
2249
 
1343
2250
  thread = threading.Thread(target=run_analysis)
1344
2251
  thread.start()
1345
- logger.info("Conscious analysis triggered in separate thread")
2252
+ logger.info("Conscious context ingestion triggered in separate thread")
2253
+
2254
+ except Exception as e:
2255
+ logger.error(f"Failed to trigger conscious context ingestion: {e}")
2256
+
2257
+ def get_conscious_system_prompt(self) -> str:
2258
+ """
2259
+ Get conscious context as system prompt for direct injection.
2260
+ Returns ALL short-term memory as formatted system prompt.
2261
+ Use this for conscious_ingest mode.
2262
+ """
2263
+ try:
2264
+ context = self._get_conscious_context()
2265
+ if not context:
2266
+ return ""
2267
+
2268
+ # Create system prompt with all short-term memory
2269
+ system_prompt = "--- Your Short-Term Memory (Conscious Context) ---\n"
2270
+ system_prompt += "This is your complete working memory. USE THIS INFORMATION TO ANSWER QUESTIONS:\n\n"
2271
+
2272
+ # Deduplicate and format context
2273
+ seen_content = set()
2274
+ for mem in context:
2275
+ if isinstance(mem, dict):
2276
+ content = mem.get("searchable_content", "") or mem.get(
2277
+ "summary", ""
2278
+ )
2279
+ category = mem.get("category_primary", "")
2280
+
2281
+ # Skip duplicates
2282
+ content_key = content.lower().strip()
2283
+ if content_key in seen_content:
2284
+ continue
2285
+ seen_content.add(content_key)
2286
+
2287
+ system_prompt += f"[{category.upper()}] {content}\n"
2288
+
2289
+ system_prompt += "\nIMPORTANT: Use the above information to answer questions about the user.\n"
2290
+ system_prompt += "-------------------------\n"
2291
+
2292
+ return system_prompt
2293
+
2294
+ except Exception as e:
2295
+ logger.error(f"Failed to generate conscious system prompt: {e}")
2296
+ return ""
2297
+
2298
+ def get_auto_ingest_system_prompt(self, user_input: str) -> str:
2299
+ """
2300
+ Get auto-ingest context as system prompt for direct injection.
2301
+ Returns relevant memories based on user input as formatted system prompt.
2302
+ Use this for auto_ingest mode.
2303
+ """
2304
+ try:
2305
+ # For now, use recent short-term memories as a simple approach
2306
+ # This avoids the search engine issues and still provides context
2307
+ # TODO: Use user_input for intelligent context retrieval
2308
+ context = self._get_conscious_context() # Get recent short-term memories
2309
+
2310
+ if not context:
2311
+ return ""
2312
+
2313
+ # Create system prompt with relevant memories (limited to prevent overwhelming)
2314
+ system_prompt = "--- Relevant Memory Context ---\n"
2315
+
2316
+ # Take first 5 items to avoid too much context
2317
+ seen_content = set()
2318
+ for mem in context[:5]:
2319
+ if isinstance(mem, dict):
2320
+ content = mem.get("searchable_content", "") or mem.get(
2321
+ "summary", ""
2322
+ )
2323
+ category = mem.get("category_primary", "")
2324
+
2325
+ # Skip duplicates
2326
+ content_key = content.lower().strip()
2327
+ if content_key in seen_content:
2328
+ continue
2329
+ seen_content.add(content_key)
2330
+
2331
+ if category.startswith("essential_"):
2332
+ system_prompt += f"[{category.upper()}] {content}\n"
2333
+ else:
2334
+ system_prompt += f"- {content}\n"
2335
+
2336
+ system_prompt += "-------------------------\n"
2337
+
2338
+ return system_prompt
1346
2339
 
1347
2340
  except Exception as e:
1348
- logger.error(f"Failed to trigger conscious analysis: {e}")
2341
+ logger.error(f"Failed to generate auto-ingest system prompt: {e}")
2342
+ return ""
2343
+
2344
+ def add_memory_to_messages(self, messages: list, user_input: str = None) -> list:
2345
+ """
2346
+ Add appropriate memory context to messages based on ingest mode.
2347
+
2348
+ Args:
2349
+ messages: List of messages for LLM
2350
+ user_input: User input for auto_ingest context retrieval (optional)
2351
+
2352
+ Returns:
2353
+ Modified messages list with memory context added as system message
2354
+ """
2355
+ try:
2356
+ system_prompt = ""
2357
+
2358
+ if self.conscious_ingest:
2359
+ # One-time conscious context injection
2360
+ if not self._conscious_context_injected:
2361
+ system_prompt = self.get_conscious_system_prompt()
2362
+ self._conscious_context_injected = True
2363
+ logger.info(
2364
+ "Conscious-ingest: Added complete working memory to system prompt"
2365
+ )
2366
+ else:
2367
+ logger.debug("Conscious-ingest: Context already injected, skipping")
2368
+
2369
+ elif self.auto_ingest and user_input:
2370
+ # Dynamic auto-ingest based on user input
2371
+ system_prompt = self.get_auto_ingest_system_prompt(user_input)
2372
+ logger.debug("Auto-ingest: Added relevant context to system prompt")
2373
+
2374
+ if system_prompt:
2375
+ # Add to existing system message or create new one
2376
+ messages_copy = messages.copy()
2377
+
2378
+ # Check if system message already exists
2379
+ for msg in messages_copy:
2380
+ if msg.get("role") == "system":
2381
+ msg["content"] = system_prompt + "\n" + msg.get("content", "")
2382
+ return messages_copy
2383
+
2384
+ # No system message exists, add one at the beginning
2385
+ messages_copy.insert(0, {"role": "system", "content": system_prompt})
2386
+ return messages_copy
2387
+
2388
+ return messages
2389
+
2390
+ except Exception as e:
2391
+ logger.error(f"Failed to add memory to messages: {e}")
2392
+ return messages
1349
2393
 
1350
2394
  def get_essential_conversations(self, limit: int = 10) -> List[Dict[str, Any]]:
1351
2395
  """Get essential conversations from short-term memory"""
1352
2396
  try:
2397
+ from sqlalchemy import text
2398
+
1353
2399
  # Get all conversations marked as essential
1354
2400
  with self.db_manager._get_connection() as connection:
1355
2401
  query = """
1356
2402
  SELECT memory_id, summary, category_primary, importance_score,
1357
2403
  created_at, searchable_content, processed_data
1358
2404
  FROM short_term_memory
1359
- WHERE namespace = ? AND category_primary LIKE 'essential_%'
2405
+ WHERE namespace = :namespace AND category_primary LIKE 'essential_%'
1360
2406
  ORDER BY importance_score DESC, created_at DESC
1361
- LIMIT ?
2407
+ LIMIT :limit
1362
2408
  """
1363
2409
 
1364
- cursor = connection.execute(query, (self.namespace, limit))
2410
+ result = connection.execute(
2411
+ text(query), {"namespace": self.namespace, "limit": limit}
2412
+ )
1365
2413
 
1366
2414
  essential_conversations = []
1367
- for row in cursor.fetchall():
2415
+ for row in result:
1368
2416
  essential_conversations.append(
1369
2417
  {
1370
2418
  "memory_id": row[0],
@@ -1382,3 +2430,106 @@ class Memori:
1382
2430
  except Exception as e:
1383
2431
  logger.error(f"Failed to get essential conversations: {e}")
1384
2432
  return []
2433
+
2434
+ def create_openai_client(self, **kwargs):
2435
+ """
2436
+ Create an OpenAI client with automatic memory recording.
2437
+
2438
+ This method creates a MemoriOpenAIInterceptor that automatically records
2439
+ all OpenAI API calls to memory using the inheritance-based approach.
2440
+
2441
+ Args:
2442
+ **kwargs: Additional arguments passed to OpenAI client (e.g., api_key)
2443
+ These override any settings from the Memori provider config
2444
+
2445
+ Returns:
2446
+ MemoriOpenAIInterceptor instance that works as a drop-in replacement
2447
+ for the standard OpenAI client
2448
+
2449
+ Example:
2450
+ memori = Memori(api_key="sk-...")
2451
+ memori.enable()
2452
+
2453
+ # Create interceptor client
2454
+ client = memori.create_openai_client()
2455
+
2456
+ # Use exactly like standard OpenAI client
2457
+ response = client.chat.completions.create(
2458
+ model="gpt-4o",
2459
+ messages=[{"role": "user", "content": "Hello!"}]
2460
+ )
2461
+ # Conversation is automatically recorded
2462
+ """
2463
+ try:
2464
+ from ..integrations.openai_integration import create_openai_client
2465
+
2466
+ return create_openai_client(self, self.provider_config, **kwargs)
2467
+ except ImportError as e:
2468
+ logger.error(f"Failed to import OpenAI integration: {e}")
2469
+ raise ImportError(
2470
+ "OpenAI integration not available. Install with: pip install openai"
2471
+ ) from e
2472
+
2473
+ def create_openai_wrapper(self, **kwargs):
2474
+ """
2475
+ Create a legacy OpenAI wrapper (backward compatibility).
2476
+
2477
+ DEPRECATED: Use create_openai_client() instead for better integration.
2478
+
2479
+ Returns:
2480
+ MemoriOpenAI wrapper instance
2481
+ """
2482
+ try:
2483
+ from ..integrations.openai_integration import MemoriOpenAI
2484
+
2485
+ return MemoriOpenAI(self, **kwargs)
2486
+ except ImportError as e:
2487
+ logger.error(f"Failed to import OpenAI integration: {e}")
2488
+ raise ImportError(
2489
+ "OpenAI integration not available. Install with: pip install openai"
2490
+ ) from e
2491
+
2492
+ # Conversation management methods
2493
+
2494
+ def get_conversation_stats(self) -> Dict[str, Any]:
2495
+ """Get conversation manager statistics"""
2496
+ return self.conversation_manager.get_session_stats()
2497
+
2498
+ def clear_conversation_history(self, session_id: str = None):
2499
+ """
2500
+ Clear conversation history
2501
+
2502
+ Args:
2503
+ session_id: Specific session to clear. If None, clears current session.
2504
+ """
2505
+ if session_id is None:
2506
+ session_id = self._session_id
2507
+ self.conversation_manager.clear_session(session_id)
2508
+ logger.info(f"Cleared conversation history for session: {session_id}")
2509
+
2510
+ def clear_all_conversations(self):
2511
+ """Clear all conversation histories"""
2512
+ self.conversation_manager.clear_all_sessions()
2513
+ logger.info("Cleared all conversation histories")
2514
+
2515
+ def start_new_conversation(self) -> str:
2516
+ """
2517
+ Start a new conversation session
2518
+
2519
+ Returns:
2520
+ New session ID
2521
+ """
2522
+ old_session_id = self._session_id
2523
+ self._session_id = str(uuid.uuid4())
2524
+
2525
+ # Reset conscious context injection flag for new conversation
2526
+ self._conscious_context_injected = False
2527
+
2528
+ logger.info(
2529
+ f"Started new conversation: {self._session_id} (previous: {old_session_id})"
2530
+ )
2531
+ return self._session_id
2532
+
2533
+ def get_current_session_id(self) -> str:
2534
+ """Get current conversation session ID"""
2535
+ return self._session_id