memorisdk 1.0.1__py3-none-any.whl → 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of memorisdk might be problematic. Click here for more details.
- memori/__init__.py +24 -8
- memori/agents/conscious_agent.py +252 -414
- memori/agents/memory_agent.py +487 -224
- memori/agents/retrieval_agent.py +416 -60
- memori/config/memory_manager.py +323 -0
- memori/core/conversation.py +393 -0
- memori/core/database.py +386 -371
- memori/core/memory.py +1676 -534
- 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 +548 -0
- memori/database/sqlalchemy_manager.py +839 -0
- memori/integrations/__init__.py +36 -11
- memori/integrations/litellm_integration.py +340 -6
- memori/integrations/openai_integration.py +506 -240
- 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.1.dist-info → memorisdk-2.0.0.dist-info}/METADATA +144 -34
- memorisdk-2.0.0.dist-info/RECORD +67 -0
- memorisdk-1.0.1.dist-info/RECORD +0 -44
- memorisdk-1.0.1.dist-info/entry_points.txt +0 -2
- {memorisdk-1.0.1.dist-info → memorisdk-2.0.0.dist-info}/WHEEL +0 -0
- {memorisdk-1.0.1.dist-info → memorisdk-2.0.0.dist-info}/licenses/LICENSE +0 -0
- {memorisdk-1.0.1.dist-info → memorisdk-2.0.0.dist-info}/top_level.txt +0 -0
memori/agents/retrieval_agent.py
CHANGED
|
@@ -7,11 +7,14 @@ import json
|
|
|
7
7
|
import threading
|
|
8
8
|
import time
|
|
9
9
|
from datetime import datetime
|
|
10
|
-
from typing import Any, Dict, List, Optional
|
|
10
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Optional
|
|
11
11
|
|
|
12
12
|
import openai
|
|
13
13
|
from loguru import logger
|
|
14
14
|
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from ..core.providers import ProviderConfig
|
|
17
|
+
|
|
15
18
|
from ..utils.pydantic_models import MemorySearchQuery
|
|
16
19
|
|
|
17
20
|
|
|
@@ -53,16 +56,35 @@ Your primary functions:
|
|
|
53
56
|
|
|
54
57
|
Be strategic and comprehensive in your search planning."""
|
|
55
58
|
|
|
56
|
-
def __init__(
|
|
59
|
+
def __init__(
|
|
60
|
+
self,
|
|
61
|
+
api_key: Optional[str] = None,
|
|
62
|
+
model: Optional[str] = None,
|
|
63
|
+
provider_config: Optional["ProviderConfig"] = None,
|
|
64
|
+
):
|
|
57
65
|
"""
|
|
58
|
-
Initialize Memory Search Engine
|
|
66
|
+
Initialize Memory Search Engine with LLM provider configuration
|
|
59
67
|
|
|
60
68
|
Args:
|
|
61
|
-
api_key:
|
|
62
|
-
model:
|
|
69
|
+
api_key: API key (deprecated, use provider_config)
|
|
70
|
+
model: Model to use for query understanding (defaults to 'gpt-4o' if not specified)
|
|
71
|
+
provider_config: Provider configuration for LLM client
|
|
63
72
|
"""
|
|
64
|
-
|
|
65
|
-
|
|
73
|
+
if provider_config:
|
|
74
|
+
# Use provider configuration to create client
|
|
75
|
+
self.client = provider_config.create_client()
|
|
76
|
+
# Use provided model, fallback to provider config model, then default to gpt-4o
|
|
77
|
+
self.model = model or provider_config.model or "gpt-4o"
|
|
78
|
+
logger.debug(f"Search engine initialized with model: {self.model}")
|
|
79
|
+
self.provider_config = provider_config
|
|
80
|
+
else:
|
|
81
|
+
# Backward compatibility: use api_key directly
|
|
82
|
+
self.client = openai.OpenAI(api_key=api_key)
|
|
83
|
+
self.model = model or "gpt-4o"
|
|
84
|
+
self.provider_config = None
|
|
85
|
+
|
|
86
|
+
# Determine if we're using a local/custom endpoint that might not support structured outputs
|
|
87
|
+
self._supports_structured_outputs = self._detect_structured_output_support()
|
|
66
88
|
|
|
67
89
|
# Performance improvements
|
|
68
90
|
self._query_cache = {} # Cache for search plans
|
|
@@ -102,28 +124,46 @@ Be strategic and comprehensive in your search planning."""
|
|
|
102
124
|
if context:
|
|
103
125
|
prompt += f"\nAdditional context: {context}"
|
|
104
126
|
|
|
105
|
-
#
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
127
|
+
# Try structured outputs first, fall back to manual parsing
|
|
128
|
+
search_query = None
|
|
129
|
+
|
|
130
|
+
if self._supports_structured_outputs:
|
|
131
|
+
try:
|
|
132
|
+
# Call OpenAI Structured Outputs
|
|
133
|
+
completion = self.client.beta.chat.completions.parse(
|
|
134
|
+
model=self.model,
|
|
135
|
+
messages=[
|
|
136
|
+
{"role": "system", "content": self.SYSTEM_PROMPT},
|
|
137
|
+
{
|
|
138
|
+
"role": "user",
|
|
139
|
+
"content": prompt,
|
|
140
|
+
},
|
|
141
|
+
],
|
|
142
|
+
response_format=MemorySearchQuery,
|
|
143
|
+
temperature=0.1,
|
|
144
|
+
)
|
|
118
145
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
146
|
+
# Handle potential refusal
|
|
147
|
+
if completion.choices[0].message.refusal:
|
|
148
|
+
logger.warning(
|
|
149
|
+
f"Search planning refused: {completion.choices[0].message.refusal}"
|
|
150
|
+
)
|
|
151
|
+
return self._create_fallback_query(query)
|
|
125
152
|
|
|
126
|
-
|
|
153
|
+
search_query = completion.choices[0].message.parsed
|
|
154
|
+
|
|
155
|
+
except Exception as e:
|
|
156
|
+
logger.warning(
|
|
157
|
+
f"Structured outputs failed for search planning, falling back to manual parsing: {e}"
|
|
158
|
+
)
|
|
159
|
+
self._supports_structured_outputs = (
|
|
160
|
+
False # Disable for future calls
|
|
161
|
+
)
|
|
162
|
+
search_query = None
|
|
163
|
+
|
|
164
|
+
# Fallback to manual parsing if structured outputs failed or not supported
|
|
165
|
+
if search_query is None:
|
|
166
|
+
search_query = self._plan_search_with_fallback_parsing(query)
|
|
127
167
|
|
|
128
168
|
# Cache the result
|
|
129
169
|
with self._cache_lock:
|
|
@@ -158,6 +198,9 @@ Be strategic and comprehensive in your search planning."""
|
|
|
158
198
|
try:
|
|
159
199
|
# Plan the search
|
|
160
200
|
search_plan = self.plan_search(query)
|
|
201
|
+
logger.debug(
|
|
202
|
+
f"Search plan for '{query}': strategies={search_plan.search_strategy}, entities={search_plan.entity_filters}"
|
|
203
|
+
)
|
|
161
204
|
|
|
162
205
|
all_results = []
|
|
163
206
|
seen_memory_ids = set()
|
|
@@ -167,11 +210,19 @@ Be strategic and comprehensive in your search planning."""
|
|
|
167
210
|
search_plan.entity_filters
|
|
168
211
|
or "keyword_search" in search_plan.search_strategy
|
|
169
212
|
):
|
|
213
|
+
logger.debug(
|
|
214
|
+
f"Executing keyword search for: {search_plan.entity_filters}"
|
|
215
|
+
)
|
|
170
216
|
keyword_results = self._execute_keyword_search(
|
|
171
217
|
search_plan, db_manager, namespace, limit
|
|
172
218
|
)
|
|
219
|
+
logger.debug(f"Keyword search returned {len(keyword_results)} results")
|
|
220
|
+
|
|
173
221
|
for result in keyword_results:
|
|
174
|
-
if
|
|
222
|
+
if (
|
|
223
|
+
isinstance(result, dict)
|
|
224
|
+
and result.get("memory_id") not in seen_memory_ids
|
|
225
|
+
):
|
|
175
226
|
seen_memory_ids.add(result["memory_id"])
|
|
176
227
|
result["search_strategy"] = "keyword_search"
|
|
177
228
|
result["search_reasoning"] = (
|
|
@@ -184,11 +235,21 @@ Be strategic and comprehensive in your search planning."""
|
|
|
184
235
|
search_plan.category_filters
|
|
185
236
|
or "category_filter" in search_plan.search_strategy
|
|
186
237
|
):
|
|
238
|
+
logger.debug(
|
|
239
|
+
f"Executing category search for: {[c.value for c in search_plan.category_filters]}"
|
|
240
|
+
)
|
|
187
241
|
category_results = self._execute_category_search(
|
|
188
242
|
search_plan, db_manager, namespace, limit - len(all_results)
|
|
189
243
|
)
|
|
244
|
+
logger.debug(
|
|
245
|
+
f"Category search returned {len(category_results)} results"
|
|
246
|
+
)
|
|
247
|
+
|
|
190
248
|
for result in category_results:
|
|
191
|
-
if
|
|
249
|
+
if (
|
|
250
|
+
isinstance(result, dict)
|
|
251
|
+
and result.get("memory_id") not in seen_memory_ids
|
|
252
|
+
):
|
|
192
253
|
seen_memory_ids.add(result["memory_id"])
|
|
193
254
|
result["search_strategy"] = "category_filter"
|
|
194
255
|
result["search_reasoning"] = (
|
|
@@ -201,11 +262,21 @@ Be strategic and comprehensive in your search planning."""
|
|
|
201
262
|
search_plan.min_importance > 0.0
|
|
202
263
|
or "importance_filter" in search_plan.search_strategy
|
|
203
264
|
):
|
|
265
|
+
logger.debug(
|
|
266
|
+
f"Executing importance search with min_importance: {search_plan.min_importance}"
|
|
267
|
+
)
|
|
204
268
|
importance_results = self._execute_importance_search(
|
|
205
269
|
search_plan, db_manager, namespace, limit - len(all_results)
|
|
206
270
|
)
|
|
271
|
+
logger.debug(
|
|
272
|
+
f"Importance search returned {len(importance_results)} results"
|
|
273
|
+
)
|
|
274
|
+
|
|
207
275
|
for result in importance_results:
|
|
208
|
-
if
|
|
276
|
+
if (
|
|
277
|
+
isinstance(result, dict)
|
|
278
|
+
and result.get("memory_id") not in seen_memory_ids
|
|
279
|
+
):
|
|
209
280
|
seen_memory_ids.add(result["memory_id"])
|
|
210
281
|
result["search_strategy"] = "importance_filter"
|
|
211
282
|
result["search_reasoning"] = (
|
|
@@ -215,36 +286,70 @@ Be strategic and comprehensive in your search planning."""
|
|
|
215
286
|
|
|
216
287
|
# If no specific strategies worked, do a general search
|
|
217
288
|
if not all_results:
|
|
289
|
+
logger.debug(
|
|
290
|
+
"No results from specific strategies, executing general search"
|
|
291
|
+
)
|
|
218
292
|
general_results = db_manager.search_memories(
|
|
219
293
|
query=search_plan.query_text, namespace=namespace, limit=limit
|
|
220
294
|
)
|
|
295
|
+
logger.debug(f"General search returned {len(general_results)} results")
|
|
296
|
+
|
|
221
297
|
for result in general_results:
|
|
222
|
-
result
|
|
223
|
-
|
|
224
|
-
|
|
298
|
+
if isinstance(result, dict):
|
|
299
|
+
result["search_strategy"] = "general_search"
|
|
300
|
+
result["search_reasoning"] = "General content search"
|
|
301
|
+
all_results.append(result)
|
|
302
|
+
|
|
303
|
+
# Filter out any non-dictionary results before processing
|
|
304
|
+
valid_results = []
|
|
305
|
+
for result in all_results:
|
|
306
|
+
if isinstance(result, dict):
|
|
307
|
+
valid_results.append(result)
|
|
308
|
+
else:
|
|
309
|
+
logger.warning(
|
|
310
|
+
f"Filtering out non-dict search result: {type(result)}"
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
all_results = valid_results
|
|
225
314
|
|
|
226
315
|
# Sort by relevance (importance score + recency)
|
|
227
|
-
all_results
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
316
|
+
if all_results:
|
|
317
|
+
|
|
318
|
+
def safe_created_at_parse(created_at_value):
|
|
319
|
+
"""Safely parse created_at value to datetime"""
|
|
320
|
+
try:
|
|
321
|
+
if created_at_value is None:
|
|
322
|
+
return datetime.fromisoformat("2000-01-01")
|
|
323
|
+
if isinstance(created_at_value, str):
|
|
324
|
+
return datetime.fromisoformat(created_at_value)
|
|
325
|
+
if hasattr(created_at_value, "isoformat"): # datetime object
|
|
326
|
+
return created_at_value
|
|
327
|
+
# Fallback for any other type
|
|
328
|
+
return datetime.fromisoformat("2000-01-01")
|
|
329
|
+
except (ValueError, TypeError):
|
|
330
|
+
return datetime.fromisoformat("2000-01-01")
|
|
331
|
+
|
|
332
|
+
all_results.sort(
|
|
333
|
+
key=lambda x: (
|
|
334
|
+
x.get("importance_score", 0) * 0.7 # Importance weight
|
|
335
|
+
+ (
|
|
336
|
+
datetime.now().replace(tzinfo=None) # Ensure timezone-naive
|
|
337
|
+
- safe_created_at_parse(x.get("created_at")).replace(
|
|
338
|
+
tzinfo=None
|
|
339
|
+
)
|
|
340
|
+
).days
|
|
341
|
+
* -0.001 # Recency weight
|
|
342
|
+
),
|
|
343
|
+
reverse=True,
|
|
344
|
+
)
|
|
240
345
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
346
|
+
# Add search metadata
|
|
347
|
+
for result in all_results:
|
|
348
|
+
result["search_metadata"] = {
|
|
349
|
+
"original_query": query,
|
|
350
|
+
"interpreted_intent": search_plan.intent,
|
|
351
|
+
"search_timestamp": datetime.now().isoformat(),
|
|
352
|
+
}
|
|
248
353
|
|
|
249
354
|
logger.debug(
|
|
250
355
|
f"Search executed for '{query}': {len(all_results)} results found"
|
|
@@ -269,9 +374,27 @@ Be strategic and comprehensive in your search planning."""
|
|
|
269
374
|
]
|
|
270
375
|
|
|
271
376
|
search_terms = " ".join(keywords)
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
377
|
+
try:
|
|
378
|
+
results = db_manager.search_memories(
|
|
379
|
+
query=search_terms, namespace=namespace, limit=limit
|
|
380
|
+
)
|
|
381
|
+
# Ensure results is a list of dictionaries
|
|
382
|
+
if not isinstance(results, list):
|
|
383
|
+
logger.warning(f"Search returned non-list result: {type(results)}")
|
|
384
|
+
return []
|
|
385
|
+
|
|
386
|
+
# Filter out any non-dictionary items
|
|
387
|
+
valid_results = []
|
|
388
|
+
for result in results:
|
|
389
|
+
if isinstance(result, dict):
|
|
390
|
+
valid_results.append(result)
|
|
391
|
+
else:
|
|
392
|
+
logger.warning(f"Search returned non-dict item: {type(result)}")
|
|
393
|
+
|
|
394
|
+
return valid_results
|
|
395
|
+
except Exception as e:
|
|
396
|
+
logger.error(f"Keyword search failed: {e}")
|
|
397
|
+
return []
|
|
275
398
|
|
|
276
399
|
def _execute_category_search(
|
|
277
400
|
self, search_plan: MemorySearchQuery, db_manager, namespace: str, limit: int
|
|
@@ -297,7 +420,13 @@ Be strategic and comprehensive in your search planning."""
|
|
|
297
420
|
# Extract category from processed_data if it's stored as JSON
|
|
298
421
|
try:
|
|
299
422
|
if "processed_data" in result:
|
|
300
|
-
processed_data =
|
|
423
|
+
processed_data = result["processed_data"]
|
|
424
|
+
# Handle both dict and JSON string formats
|
|
425
|
+
if isinstance(processed_data, str):
|
|
426
|
+
processed_data = json.loads(processed_data)
|
|
427
|
+
elif not isinstance(processed_data, dict):
|
|
428
|
+
continue # Skip if neither dict nor string
|
|
429
|
+
|
|
301
430
|
memory_category = processed_data.get("category", {}).get(
|
|
302
431
|
"primary_category", ""
|
|
303
432
|
)
|
|
@@ -305,11 +434,221 @@ Be strategic and comprehensive in your search planning."""
|
|
|
305
434
|
filtered_results.append(result)
|
|
306
435
|
elif result.get("category") in categories:
|
|
307
436
|
filtered_results.append(result)
|
|
308
|
-
except (json.JSONDecodeError, KeyError):
|
|
437
|
+
except (json.JSONDecodeError, KeyError, AttributeError):
|
|
309
438
|
continue
|
|
310
439
|
|
|
311
440
|
return filtered_results[:limit]
|
|
312
441
|
|
|
442
|
+
def _detect_structured_output_support(self) -> bool:
|
|
443
|
+
"""
|
|
444
|
+
Detect if the current provider/endpoint supports OpenAI structured outputs
|
|
445
|
+
|
|
446
|
+
Returns:
|
|
447
|
+
True if structured outputs are likely supported, False otherwise
|
|
448
|
+
"""
|
|
449
|
+
try:
|
|
450
|
+
# Check if we have a provider config with custom base_url
|
|
451
|
+
if self.provider_config and hasattr(self.provider_config, "base_url"):
|
|
452
|
+
base_url = self.provider_config.base_url
|
|
453
|
+
if base_url:
|
|
454
|
+
# Local/custom endpoints typically don't support beta features
|
|
455
|
+
if "localhost" in base_url or "127.0.0.1" in base_url:
|
|
456
|
+
logger.debug(
|
|
457
|
+
f"Detected local endpoint ({base_url}), disabling structured outputs"
|
|
458
|
+
)
|
|
459
|
+
return False
|
|
460
|
+
# Custom endpoints that aren't OpenAI
|
|
461
|
+
if "api.openai.com" not in base_url:
|
|
462
|
+
logger.debug(
|
|
463
|
+
f"Detected custom endpoint ({base_url}), disabling structured outputs"
|
|
464
|
+
)
|
|
465
|
+
return False
|
|
466
|
+
|
|
467
|
+
# Check for Azure endpoints - test if they support structured outputs
|
|
468
|
+
if self.provider_config and hasattr(self.provider_config, "api_type"):
|
|
469
|
+
if self.provider_config.api_type == "azure":
|
|
470
|
+
return self._test_azure_structured_outputs_support()
|
|
471
|
+
elif self.provider_config.api_type in ["custom", "openai_compatible"]:
|
|
472
|
+
logger.debug(
|
|
473
|
+
f"Detected {self.provider_config.api_type} endpoint, disabling structured outputs"
|
|
474
|
+
)
|
|
475
|
+
return False
|
|
476
|
+
|
|
477
|
+
# Default: assume OpenAI endpoint supports structured outputs
|
|
478
|
+
logger.debug("Assuming OpenAI endpoint, enabling structured outputs")
|
|
479
|
+
return True
|
|
480
|
+
|
|
481
|
+
except Exception as e:
|
|
482
|
+
logger.debug(
|
|
483
|
+
f"Error detecting structured output support: {e}, defaulting to enabled"
|
|
484
|
+
)
|
|
485
|
+
return True
|
|
486
|
+
|
|
487
|
+
def _test_azure_structured_outputs_support(self) -> bool:
|
|
488
|
+
"""
|
|
489
|
+
Test if Azure OpenAI supports structured outputs by making a test call
|
|
490
|
+
|
|
491
|
+
Returns:
|
|
492
|
+
True if structured outputs are supported, False otherwise
|
|
493
|
+
"""
|
|
494
|
+
try:
|
|
495
|
+
from pydantic import BaseModel
|
|
496
|
+
|
|
497
|
+
# Simple test model
|
|
498
|
+
class TestModel(BaseModel):
|
|
499
|
+
test_field: str
|
|
500
|
+
|
|
501
|
+
# Try to make a structured output call
|
|
502
|
+
test_response = self.client.beta.chat.completions.parse(
|
|
503
|
+
model=self.model,
|
|
504
|
+
messages=[{"role": "user", "content": "Say hello"}],
|
|
505
|
+
response_format=TestModel,
|
|
506
|
+
max_tokens=10,
|
|
507
|
+
temperature=0,
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
if (
|
|
511
|
+
test_response
|
|
512
|
+
and hasattr(test_response, "choices")
|
|
513
|
+
and test_response.choices
|
|
514
|
+
):
|
|
515
|
+
logger.debug(
|
|
516
|
+
"Azure endpoint supports structured outputs - test successful"
|
|
517
|
+
)
|
|
518
|
+
return True
|
|
519
|
+
else:
|
|
520
|
+
logger.debug(
|
|
521
|
+
"Azure endpoint structured outputs test failed - response invalid"
|
|
522
|
+
)
|
|
523
|
+
return False
|
|
524
|
+
|
|
525
|
+
except Exception as e:
|
|
526
|
+
# If structured outputs fail, log the error and fall back to regular completions
|
|
527
|
+
logger.debug(f"Azure endpoint doesn't support structured outputs: {e}")
|
|
528
|
+
return False
|
|
529
|
+
|
|
530
|
+
def _plan_search_with_fallback_parsing(self, query: str) -> MemorySearchQuery:
|
|
531
|
+
"""
|
|
532
|
+
Plan search strategy using regular chat completions with manual JSON parsing
|
|
533
|
+
|
|
534
|
+
This method works with any OpenAI-compatible API that supports chat completions
|
|
535
|
+
but doesn't support structured outputs (like Ollama, local models, etc.)
|
|
536
|
+
"""
|
|
537
|
+
try:
|
|
538
|
+
# Prepare the prompt from raw query
|
|
539
|
+
prompt = f"User query: {query}"
|
|
540
|
+
|
|
541
|
+
# Enhanced system prompt for JSON output
|
|
542
|
+
json_system_prompt = (
|
|
543
|
+
self.SYSTEM_PROMPT
|
|
544
|
+
+ "\n\nIMPORTANT: You MUST respond with a valid JSON object that matches this exact schema:\n"
|
|
545
|
+
)
|
|
546
|
+
json_system_prompt += self._get_search_query_json_schema()
|
|
547
|
+
json_system_prompt += "\n\nRespond ONLY with the JSON object, no additional text or formatting."
|
|
548
|
+
|
|
549
|
+
# Call regular chat completions
|
|
550
|
+
completion = self.client.chat.completions.create(
|
|
551
|
+
model=self.model,
|
|
552
|
+
messages=[
|
|
553
|
+
{"role": "system", "content": json_system_prompt},
|
|
554
|
+
{
|
|
555
|
+
"role": "user",
|
|
556
|
+
"content": prompt,
|
|
557
|
+
},
|
|
558
|
+
],
|
|
559
|
+
temperature=0.1,
|
|
560
|
+
max_tokens=1000, # Ensure enough tokens for full response
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
# Extract and parse JSON response
|
|
564
|
+
response_text = completion.choices[0].message.content
|
|
565
|
+
if not response_text:
|
|
566
|
+
raise ValueError("Empty response from model")
|
|
567
|
+
|
|
568
|
+
# Clean up response (remove markdown formatting if present)
|
|
569
|
+
response_text = response_text.strip()
|
|
570
|
+
if response_text.startswith("```json"):
|
|
571
|
+
response_text = response_text[7:]
|
|
572
|
+
if response_text.startswith("```"):
|
|
573
|
+
response_text = response_text[3:]
|
|
574
|
+
if response_text.endswith("```"):
|
|
575
|
+
response_text = response_text[:-3]
|
|
576
|
+
response_text = response_text.strip()
|
|
577
|
+
|
|
578
|
+
# Parse JSON
|
|
579
|
+
try:
|
|
580
|
+
parsed_data = json.loads(response_text)
|
|
581
|
+
except json.JSONDecodeError as e:
|
|
582
|
+
logger.error(f"Failed to parse JSON response for search planning: {e}")
|
|
583
|
+
logger.debug(f"Raw response: {response_text}")
|
|
584
|
+
return self._create_fallback_query(query)
|
|
585
|
+
|
|
586
|
+
# Convert to MemorySearchQuery object with validation and defaults
|
|
587
|
+
search_query = self._create_search_query_from_dict(parsed_data, query)
|
|
588
|
+
|
|
589
|
+
logger.debug("Successfully parsed search query using fallback method")
|
|
590
|
+
return search_query
|
|
591
|
+
|
|
592
|
+
except Exception as e:
|
|
593
|
+
logger.error(f"Fallback search planning failed: {e}")
|
|
594
|
+
return self._create_fallback_query(query)
|
|
595
|
+
|
|
596
|
+
def _get_search_query_json_schema(self) -> str:
|
|
597
|
+
"""
|
|
598
|
+
Get JSON schema description for manual search query parsing
|
|
599
|
+
"""
|
|
600
|
+
return """{
|
|
601
|
+
"query_text": "string - Original query text",
|
|
602
|
+
"intent": "string - Interpreted intent of the query",
|
|
603
|
+
"entity_filters": ["array of strings - Specific entities to search for"],
|
|
604
|
+
"category_filters": ["array of strings - Memory categories: fact, preference, skill, context, rule"],
|
|
605
|
+
"time_range": "string or null - Time range for search (e.g., last_week)",
|
|
606
|
+
"min_importance": "number - Minimum importance score (0.0-1.0)",
|
|
607
|
+
"search_strategy": ["array of strings - Recommended search strategies"],
|
|
608
|
+
"expected_result_types": ["array of strings - Expected types of results"]
|
|
609
|
+
}"""
|
|
610
|
+
|
|
611
|
+
def _create_search_query_from_dict(
|
|
612
|
+
self, data: Dict[str, Any], original_query: str
|
|
613
|
+
) -> MemorySearchQuery:
|
|
614
|
+
"""
|
|
615
|
+
Create MemorySearchQuery from dictionary with proper validation and defaults
|
|
616
|
+
"""
|
|
617
|
+
try:
|
|
618
|
+
# Import here to avoid circular imports
|
|
619
|
+
from ..utils.pydantic_models import MemoryCategoryType
|
|
620
|
+
|
|
621
|
+
# Validate and convert category filters
|
|
622
|
+
category_filters = []
|
|
623
|
+
raw_categories = data.get("category_filters", [])
|
|
624
|
+
if isinstance(raw_categories, list):
|
|
625
|
+
for cat_str in raw_categories:
|
|
626
|
+
try:
|
|
627
|
+
category = MemoryCategoryType(cat_str.lower())
|
|
628
|
+
category_filters.append(category)
|
|
629
|
+
except ValueError:
|
|
630
|
+
logger.debug(f"Invalid category filter '{cat_str}', skipping")
|
|
631
|
+
|
|
632
|
+
# Create search query object with proper validation
|
|
633
|
+
search_query = MemorySearchQuery(
|
|
634
|
+
query_text=data.get("query_text", original_query),
|
|
635
|
+
intent=data.get("intent", "General search (fallback)"),
|
|
636
|
+
entity_filters=data.get("entity_filters", []),
|
|
637
|
+
category_filters=category_filters,
|
|
638
|
+
time_range=data.get("time_range"),
|
|
639
|
+
min_importance=max(
|
|
640
|
+
0.0, min(1.0, float(data.get("min_importance", 0.0)))
|
|
641
|
+
),
|
|
642
|
+
search_strategy=data.get("search_strategy", ["keyword_search"]),
|
|
643
|
+
expected_result_types=data.get("expected_result_types", ["any"]),
|
|
644
|
+
)
|
|
645
|
+
|
|
646
|
+
return search_query
|
|
647
|
+
|
|
648
|
+
except Exception as e:
|
|
649
|
+
logger.error(f"Error creating search query from dict: {e}")
|
|
650
|
+
return self._create_fallback_query(original_query)
|
|
651
|
+
|
|
313
652
|
def _execute_importance_search(
|
|
314
653
|
self, search_plan: MemorySearchQuery, db_manager, namespace: str, limit: int
|
|
315
654
|
) -> List[Dict[str, Any]]:
|
|
@@ -414,7 +753,10 @@ Be strategic and comprehensive in your search planning."""
|
|
|
414
753
|
continue
|
|
415
754
|
|
|
416
755
|
for result in results:
|
|
417
|
-
if
|
|
756
|
+
if (
|
|
757
|
+
isinstance(result, dict)
|
|
758
|
+
and result.get("memory_id") not in seen_memory_ids
|
|
759
|
+
):
|
|
418
760
|
seen_memory_ids.add(result["memory_id"])
|
|
419
761
|
all_results.append(result)
|
|
420
762
|
|
|
@@ -540,7 +882,21 @@ def smart_memory_search(query: str, memori_instance, limit: int = 5) -> str:
|
|
|
540
882
|
if "processed_data" in result:
|
|
541
883
|
import json
|
|
542
884
|
|
|
543
|
-
processed_data =
|
|
885
|
+
processed_data = result["processed_data"]
|
|
886
|
+
# Handle both dict and JSON string formats
|
|
887
|
+
if isinstance(processed_data, str):
|
|
888
|
+
processed_data = json.loads(processed_data)
|
|
889
|
+
elif isinstance(processed_data, dict):
|
|
890
|
+
pass # Already a dict, use as-is
|
|
891
|
+
else:
|
|
892
|
+
# Fallback to basic result fields
|
|
893
|
+
summary = result.get(
|
|
894
|
+
"summary",
|
|
895
|
+
result.get("searchable_content", "")[:100] + "...",
|
|
896
|
+
)
|
|
897
|
+
category = result.get("category_primary", "unknown")
|
|
898
|
+
continue
|
|
899
|
+
|
|
544
900
|
summary = processed_data.get("summary", "")
|
|
545
901
|
category = processed_data.get("category", {}).get(
|
|
546
902
|
"primary_category", ""
|