memorisdk 1.0.0__py3-none-any.whl

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

Potentially problematic release.


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

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