memorisdk 1.0.2__py3-none-any.whl → 2.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of memorisdk might be problematic. Click here for more details.
- memori/__init__.py +24 -8
- memori/agents/conscious_agent.py +252 -414
- memori/agents/memory_agent.py +487 -224
- memori/agents/retrieval_agent.py +491 -68
- memori/config/memory_manager.py +323 -0
- memori/core/conversation.py +393 -0
- memori/core/database.py +386 -371
- memori/core/memory.py +1683 -532
- memori/core/providers.py +217 -0
- memori/database/adapters/__init__.py +10 -0
- memori/database/adapters/mysql_adapter.py +331 -0
- memori/database/adapters/postgresql_adapter.py +291 -0
- memori/database/adapters/sqlite_adapter.py +229 -0
- memori/database/auto_creator.py +320 -0
- memori/database/connection_utils.py +207 -0
- memori/database/connectors/base_connector.py +283 -0
- memori/database/connectors/mysql_connector.py +240 -18
- memori/database/connectors/postgres_connector.py +277 -4
- memori/database/connectors/sqlite_connector.py +178 -3
- memori/database/models.py +400 -0
- memori/database/queries/base_queries.py +1 -1
- memori/database/queries/memory_queries.py +91 -2
- memori/database/query_translator.py +222 -0
- memori/database/schema_generators/__init__.py +7 -0
- memori/database/schema_generators/mysql_schema_generator.py +215 -0
- memori/database/search/__init__.py +8 -0
- memori/database/search/mysql_search_adapter.py +255 -0
- memori/database/search/sqlite_search_adapter.py +180 -0
- memori/database/search_service.py +700 -0
- memori/database/sqlalchemy_manager.py +888 -0
- memori/integrations/__init__.py +36 -11
- memori/integrations/litellm_integration.py +340 -6
- memori/integrations/openai_integration.py +506 -240
- memori/tools/memory_tool.py +94 -4
- memori/utils/input_validator.py +395 -0
- memori/utils/pydantic_models.py +138 -36
- memori/utils/query_builder.py +530 -0
- memori/utils/security_audit.py +594 -0
- memori/utils/security_integration.py +339 -0
- memori/utils/transaction_manager.py +547 -0
- {memorisdk-1.0.2.dist-info → memorisdk-2.0.1.dist-info}/METADATA +56 -23
- memorisdk-2.0.1.dist-info/RECORD +66 -0
- memori/scripts/llm_text.py +0 -50
- memorisdk-1.0.2.dist-info/RECORD +0 -44
- memorisdk-1.0.2.dist-info/entry_points.txt +0 -2
- {memorisdk-1.0.2.dist-info → memorisdk-2.0.1.dist-info}/WHEEL +0 -0
- {memorisdk-1.0.2.dist-info → memorisdk-2.0.1.dist-info}/licenses/LICENSE +0 -0
- {memorisdk-1.0.2.dist-info → memorisdk-2.0.1.dist-info}/top_level.txt +0 -0
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 ..
|
|
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 .
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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.
|
|
102
|
-
|
|
218
|
+
self.search_engine = MemorySearchEngine(
|
|
219
|
+
provider_config=self.provider_config, model=effective_model
|
|
103
220
|
)
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
108
|
-
|
|
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
|
-
|
|
112
|
-
|
|
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
|
-
#
|
|
208
|
-
|
|
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
|
-
|
|
217
|
-
|
|
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
|
-
|
|
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
|
|
226
|
-
"""
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
|
|
294
|
-
|
|
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
|
-
|
|
297
|
-
|
|
434
|
+
# Run synchronous initialization of existing memories
|
|
435
|
+
self._initialize_existing_conscious_memories_sync()
|
|
298
436
|
|
|
299
|
-
|
|
300
|
-
|
|
437
|
+
logger.debug(
|
|
438
|
+
"Conscious-ingest: Synchronous conscious context extraction completed"
|
|
439
|
+
)
|
|
301
440
|
|
|
302
|
-
|
|
303
|
-
|
|
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
|
-
|
|
447
|
+
from sqlalchemy import text
|
|
310
448
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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
|
-
|
|
330
|
-
|
|
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
|
-
|
|
333
|
-
|
|
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
|
-
"
|
|
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(
|
|
487
|
+
logger.error(
|
|
488
|
+
f"Conscious-ingest: Failed to initialize existing conscious memories: {e}"
|
|
489
|
+
)
|
|
342
490
|
return False
|
|
343
491
|
|
|
344
|
-
def
|
|
345
|
-
"""
|
|
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
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
495
|
+
(
|
|
496
|
+
memory_id,
|
|
497
|
+
processed_data,
|
|
498
|
+
summary,
|
|
499
|
+
searchable_content,
|
|
500
|
+
importance_score,
|
|
501
|
+
_,
|
|
502
|
+
) = memory_row
|
|
354
503
|
|
|
355
|
-
|
|
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
|
-
|
|
370
|
-
"""Install import hooks to automatically wrap LLM clients"""
|
|
506
|
+
from sqlalchemy import text
|
|
371
507
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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
|
-
|
|
381
|
-
|
|
382
|
-
|
|
532
|
+
# Create short-term memory ID
|
|
533
|
+
short_term_id = (
|
|
534
|
+
f"conscious_{memory_id}_{int(datetime.now().timestamp())}"
|
|
535
|
+
)
|
|
383
536
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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
|
-
|
|
389
|
-
|
|
390
|
-
|
|
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
|
-
|
|
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
|
-
|
|
395
|
-
|
|
575
|
+
def enable(self, interceptors: Optional[List[str]] = None):
|
|
576
|
+
"""
|
|
577
|
+
Enable universal memory recording using LiteLLM's native callback system.
|
|
396
578
|
|
|
397
|
-
|
|
398
|
-
|
|
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
|
-
|
|
406
|
-
|
|
407
|
-
|
|
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
|
-
|
|
410
|
-
|
|
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
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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
|
-
|
|
421
|
-
|
|
596
|
+
register_memori_instance(self)
|
|
597
|
+
except ImportError:
|
|
598
|
+
logger.debug("OpenAI integration not available for automatic interception")
|
|
422
599
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
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
|
-
|
|
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
|
-
|
|
610
|
+
# Start background conscious agent if available
|
|
611
|
+
if self.conscious_ingest and self.conscious_agent:
|
|
612
|
+
self._start_background_analysis()
|
|
430
613
|
|
|
431
|
-
|
|
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
|
-
|
|
434
|
-
module.OpenAI._memori_wrapped = True
|
|
435
|
-
logger.debug("OpenAI client auto-wrapping enabled")
|
|
631
|
+
logger.info("\n".join(status_info))
|
|
436
632
|
|
|
437
|
-
|
|
438
|
-
|
|
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
|
-
|
|
441
|
-
"""Automatically wrap Anthropic client when imported"""
|
|
640
|
+
# Unregister from automatic OpenAI interception
|
|
442
641
|
try:
|
|
443
|
-
|
|
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
|
-
|
|
457
|
-
|
|
458
|
-
|
|
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
|
-
|
|
462
|
-
|
|
648
|
+
# Use memory manager for clean disable
|
|
649
|
+
results = self.memory_manager.disable()
|
|
463
650
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
self._record_anthropic_conversation(kwargs, response)
|
|
651
|
+
# Stop background analysis task
|
|
652
|
+
self._stop_background_analysis()
|
|
467
653
|
|
|
468
|
-
|
|
654
|
+
self._enabled = False
|
|
469
655
|
|
|
470
|
-
|
|
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
|
-
|
|
664
|
+
logger.info(status_message)
|
|
473
665
|
|
|
474
|
-
|
|
475
|
-
module.Anthropic._memori_wrapped = True
|
|
476
|
-
logger.debug("Anthropic client auto-wrapping enabled")
|
|
666
|
+
# Memory system status and control methods
|
|
477
667
|
|
|
478
|
-
|
|
479
|
-
|
|
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
|
|
482
|
-
"""
|
|
483
|
-
|
|
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
|
|
493
|
-
"""
|
|
494
|
-
#
|
|
495
|
-
|
|
496
|
-
|
|
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
|
|
500
|
-
"""
|
|
501
|
-
|
|
502
|
-
|
|
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
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
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
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
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"
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
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(
|
|
824
|
+
logger.debug(
|
|
825
|
+
f"Anthropic: Injected context with {len(context)} items"
|
|
826
|
+
)
|
|
586
827
|
except Exception as e:
|
|
587
|
-
logger.error(f"
|
|
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
|
|
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.
|
|
857
|
+
logger.info(
|
|
858
|
+
f"Conscious-ingest: Injected {len(context)} short-term memories as initial context"
|
|
859
|
+
)
|
|
617
860
|
else:
|
|
618
|
-
context =
|
|
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
|
-
|
|
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
|
-
|
|
634
|
-
|
|
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()}] {
|
|
902
|
+
context_prompt += f"[{category.upper()}] {content}\n"
|
|
639
903
|
else:
|
|
640
|
-
context_prompt += f"- {
|
|
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
|
|
691
|
-
This represents the 'working memory'
|
|
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
|
-
|
|
695
|
-
cursor = conn.cursor()
|
|
965
|
+
from sqlalchemy import text
|
|
696
966
|
|
|
697
|
-
|
|
698
|
-
|
|
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 =
|
|
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
|
-
|
|
707
|
-
|
|
708
|
-
|
|
979
|
+
"""
|
|
980
|
+
),
|
|
981
|
+
{"namespace": self.namespace, "current_time": datetime.now()},
|
|
709
982
|
)
|
|
710
983
|
|
|
711
984
|
memories = []
|
|
712
|
-
for row in
|
|
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
|
-
|
|
743
|
-
|
|
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
|
-
#
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
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
|
-
|
|
755
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
775
|
-
|
|
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
|
-
|
|
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
|
-
|
|
782
|
-
|
|
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
|
-
|
|
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
|
|
1384
|
+
logger.error(f"Failed to record Anthropic conversation: {e}")
|
|
800
1385
|
|
|
801
|
-
def
|
|
802
|
-
"""
|
|
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
|
-
|
|
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
|
-
|
|
813
|
-
|
|
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
|
-
|
|
826
|
-
|
|
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
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
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
|
-
|
|
840
|
-
|
|
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.
|
|
1543
|
+
logger.debug(f"Error extracting Anthropic metadata: {e}")
|
|
860
1544
|
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
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
|
-
|
|
868
|
-
for
|
|
869
|
-
if
|
|
870
|
-
user_input =
|
|
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
|
-
|
|
874
|
-
|
|
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
|
-
#
|
|
877
|
-
tokens_used = 0
|
|
1584
|
+
# Add token usage if available
|
|
878
1585
|
if hasattr(response, "usage") and response.usage:
|
|
879
|
-
|
|
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
|
-
#
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
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
|
-
|
|
887
|
-
|
|
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
|
-
|
|
897
|
-
|
|
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
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
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"
|
|
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
|
|
923
|
-
model: str =
|
|
1686
|
+
ai_output=None,
|
|
1687
|
+
model: str = None,
|
|
924
1688
|
metadata: Optional[Dict[str, Any]] = None,
|
|
925
1689
|
) -> str:
|
|
926
1690
|
"""
|
|
927
|
-
|
|
1691
|
+
Record a conversation.
|
|
928
1692
|
|
|
929
1693
|
Args:
|
|
930
|
-
user_input:
|
|
931
|
-
ai_output:
|
|
932
|
-
model:
|
|
933
|
-
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
|
|
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
|
-
#
|
|
942
|
-
|
|
943
|
-
|
|
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
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
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
|
-
|
|
962
|
-
|
|
963
|
-
|
|
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
|
-
|
|
966
|
-
|
|
1731
|
+
logger.debug(f"Recorded conversation: {chat_id}")
|
|
1732
|
+
return chat_id
|
|
967
1733
|
|
|
968
|
-
|
|
969
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
#
|
|
992
|
-
|
|
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
|
-
|
|
998
|
-
|
|
1783
|
+
existing_memories=(
|
|
1784
|
+
[mem.summary for mem in existing_memories[:10]]
|
|
1785
|
+
if existing_memories
|
|
1786
|
+
else []
|
|
1787
|
+
),
|
|
999
1788
|
)
|
|
1000
1789
|
|
|
1001
|
-
#
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
)
|
|
1790
|
+
# Check for duplicates
|
|
1791
|
+
duplicate_id = await self.memory_agent.detect_duplicates(
|
|
1792
|
+
processed_memory, existing_memories
|
|
1793
|
+
)
|
|
1006
1794
|
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
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.
|
|
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
|
|
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": "
|
|
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
|
-
"
|
|
1142
|
-
"
|
|
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
|
-
"
|
|
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": "
|
|
1158
|
-
"
|
|
1159
|
-
|
|
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": "
|
|
1168
|
-
"
|
|
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": "
|
|
1178
|
-
"
|
|
1179
|
-
|
|
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": "
|
|
1188
|
-
"
|
|
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
|
-
"""
|
|
1288
|
-
|
|
2184
|
+
"""Background analysis loop for memory processing"""
|
|
2185
|
+
try:
|
|
2186
|
+
logger.debug("Background analysis loop started")
|
|
1289
2187
|
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
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
|
-
|
|
1298
|
-
|
|
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
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
2228
|
+
self.conscious_agent.run_conscious_ingest(
|
|
1322
2229
|
self.db_manager, self.namespace
|
|
1323
2230
|
)
|
|
1324
2231
|
)
|
|
1325
|
-
logger.info("Conscious
|
|
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.
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
2410
|
+
result = connection.execute(
|
|
2411
|
+
text(query), {"namespace": self.namespace, "limit": limit}
|
|
2412
|
+
)
|
|
1365
2413
|
|
|
1366
2414
|
essential_conversations = []
|
|
1367
|
-
for row in
|
|
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
|