memorisdk 1.0.2__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.

Files changed (46) hide show
  1. memori/__init__.py +24 -8
  2. memori/agents/conscious_agent.py +252 -414
  3. memori/agents/memory_agent.py +487 -224
  4. memori/agents/retrieval_agent.py +416 -60
  5. memori/config/memory_manager.py +323 -0
  6. memori/core/conversation.py +393 -0
  7. memori/core/database.py +386 -371
  8. memori/core/memory.py +1638 -531
  9. memori/core/providers.py +217 -0
  10. memori/database/adapters/__init__.py +10 -0
  11. memori/database/adapters/mysql_adapter.py +331 -0
  12. memori/database/adapters/postgresql_adapter.py +291 -0
  13. memori/database/adapters/sqlite_adapter.py +229 -0
  14. memori/database/auto_creator.py +320 -0
  15. memori/database/connection_utils.py +207 -0
  16. memori/database/connectors/base_connector.py +283 -0
  17. memori/database/connectors/mysql_connector.py +240 -18
  18. memori/database/connectors/postgres_connector.py +277 -4
  19. memori/database/connectors/sqlite_connector.py +178 -3
  20. memori/database/models.py +400 -0
  21. memori/database/queries/base_queries.py +1 -1
  22. memori/database/queries/memory_queries.py +91 -2
  23. memori/database/query_translator.py +222 -0
  24. memori/database/schema_generators/__init__.py +7 -0
  25. memori/database/schema_generators/mysql_schema_generator.py +215 -0
  26. memori/database/search/__init__.py +8 -0
  27. memori/database/search/mysql_search_adapter.py +255 -0
  28. memori/database/search/sqlite_search_adapter.py +180 -0
  29. memori/database/search_service.py +548 -0
  30. memori/database/sqlalchemy_manager.py +839 -0
  31. memori/integrations/__init__.py +36 -11
  32. memori/integrations/litellm_integration.py +340 -6
  33. memori/integrations/openai_integration.py +506 -240
  34. memori/utils/input_validator.py +395 -0
  35. memori/utils/pydantic_models.py +138 -36
  36. memori/utils/query_builder.py +530 -0
  37. memori/utils/security_audit.py +594 -0
  38. memori/utils/security_integration.py +339 -0
  39. memori/utils/transaction_manager.py +547 -0
  40. {memorisdk-1.0.2.dist-info → memorisdk-2.0.0.dist-info}/METADATA +44 -17
  41. memorisdk-2.0.0.dist-info/RECORD +67 -0
  42. memorisdk-1.0.2.dist-info/RECORD +0 -44
  43. memorisdk-1.0.2.dist-info/entry_points.txt +0 -2
  44. {memorisdk-1.0.2.dist-info → memorisdk-2.0.0.dist-info}/WHEEL +0 -0
  45. {memorisdk-1.0.2.dist-info → memorisdk-2.0.0.dist-info}/licenses/LICENSE +0 -0
  46. {memorisdk-1.0.2.dist-info → memorisdk-2.0.0.dist-info}/top_level.txt +0 -0
@@ -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__(self, api_key: Optional[str] = None, model: str = "gpt-4o"):
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: OpenAI API key (if None, uses environment variable)
62
- model: OpenAI model to use for query understanding
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
- self.client = openai.OpenAI(api_key=api_key)
65
- self.model = model
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
- # Call OpenAI Structured Outputs
106
- completion = self.client.beta.chat.completions.parse(
107
- model=self.model,
108
- messages=[
109
- {"role": "system", "content": self.SYSTEM_PROMPT},
110
- {
111
- "role": "user",
112
- "content": f"Analyze and plan memory search for this query:\n\n{prompt}",
113
- },
114
- ],
115
- response_format=MemorySearchQuery,
116
- temperature=0.1,
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
- # Handle potential refusal
120
- if completion.choices[0].message.refusal:
121
- logger.warning(
122
- f"Search planning refused: {completion.choices[0].message.refusal}"
123
- )
124
- return self._create_fallback_query(query)
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
- search_query = completion.choices[0].message.parsed
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 result.get("memory_id") not in seen_memory_ids:
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 result.get("memory_id") not in seen_memory_ids:
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 result.get("memory_id") not in seen_memory_ids:
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["search_strategy"] = "general_search"
223
- result["search_reasoning"] = "General content search"
224
- all_results.append(result)
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.sort(
228
- key=lambda x: (
229
- x.get("importance_score", 0) * 0.7 # Importance weight
230
- + (
231
- datetime.now().replace(tzinfo=None) # Ensure timezone-naive
232
- - datetime.fromisoformat(
233
- x.get("created_at", "2000-01-01")
234
- ).replace(tzinfo=None)
235
- ).days
236
- * -0.001 # Recency weight
237
- ),
238
- reverse=True,
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
- # Add search metadata
242
- for result in all_results:
243
- result["search_metadata"] = {
244
- "original_query": query,
245
- "interpreted_intent": search_plan.intent,
246
- "search_timestamp": datetime.now().isoformat(),
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
- return db_manager.search_memories(
273
- query=search_terms, namespace=namespace, limit=limit
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 = json.loads(result["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 result.get("memory_id") not in seen_memory_ids:
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 = json.loads(result["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", ""