@@ -314,26 +314,99 @@ def handle_get_memories(
314314 return GetMemoryResponse (message = "Memories retrieved successfully" , data = filtered_results )
315315
316316
317+ def _build_quick_delete_constraints (delete_mem_req : DeleteMemoryRequest ) -> dict [str , Any ]:
318+ """Build fast-delete constraints from request-level fields."""
319+ constraints : dict [str , Any ] = {}
320+ if delete_mem_req .user_id is not None :
321+ constraints ["user_id" ] = delete_mem_req .user_id
322+ if delete_mem_req .session_id is not None :
323+ constraints ["session_id" ] = delete_mem_req .session_id
324+ return constraints
325+
326+
327+ def _merge_delete_filter (
328+ base_filter : dict [str , Any ] | None ,
329+ constraints : dict [str , Any ],
330+ ) -> dict [str , Any ]:
331+ """Merge user/session constraints into an existing filter."""
332+ if not constraints :
333+ return base_filter or {}
334+ if base_filter is None :
335+ return {"and" : [constraints .copy ()]}
336+
337+ if not base_filter :
338+ return {"and" : [constraints .copy ()]}
339+
340+ if "and" in base_filter :
341+ and_conditions = base_filter .get ("and" )
342+ if not isinstance (and_conditions , list ):
343+ raise ValueError ("Invalid filter format: 'and' must be a list" )
344+ return {"and" : [* and_conditions , constraints .copy ()]}
345+
346+ if "or" in base_filter :
347+ or_conditions = base_filter .get ("or" )
348+ if not isinstance (or_conditions , list ):
349+ raise ValueError ("Invalid filter format: 'or' must be a list" )
350+
351+ merged_or_conditions : list [dict [str , Any ]] = []
352+ for condition in or_conditions :
353+ if not isinstance (condition , dict ):
354+ raise ValueError ("Invalid filter format: each 'or' condition must be a dict" )
355+ merged_condition = condition .copy ()
356+ for key , value in constraints .items ():
357+ if key in merged_condition and merged_condition [key ] != value :
358+ raise ValueError (
359+ f"Conflicting filter condition for '{ key } '. "
360+ "Please merge it manually into request.filter."
361+ )
362+ merged_condition [key ] = value
363+ merged_or_conditions .append (merged_condition )
364+
365+ return {"or" : merged_or_conditions }
366+
367+ # For plain dict filters, keep strict AND semantics explicitly.
368+ return {"and" : [base_filter .copy (), constraints .copy ()]}
369+
370+
317371def handle_delete_memories (delete_mem_req : DeleteMemoryRequest , naive_mem_cube : NaiveMemCube ):
318372 """
319373 Handler for deleting memories.
320374 Now unified to delete from text_mem only (includes preferences).
321375 """
322376 logger .info (
323- "[Delete memory request] writable_cube_ids: %s, memory_ids: %s, auto_cleanup_working: %s" ,
377+ "[Delete memory request] writable_cube_ids: %s, memory_ids: %s, file_ids: %s, auto_cleanup_working: %s"
378+ "has_filter: %s, user_id: %s, session_id: %s" ,
324379 delete_mem_req .writable_cube_ids ,
325380 delete_mem_req .memory_ids ,
381+ delete_mem_req .file_ids ,
382+ delete_mem_req .filter is not None ,
383+ delete_mem_req .user_id ,
384+ delete_mem_req .session_id ,
326385 getattr (delete_mem_req , "auto_cleanup_working" , False ),
327386 )
328- # Validate that only one of memory_ids, file_ids, or filter is provided
387+ quick_constraints = _build_quick_delete_constraints (delete_mem_req )
388+ has_non_empty_filter = bool (delete_mem_req .filter )
389+ has_filter_mode = has_non_empty_filter or bool (quick_constraints )
390+
391+ # Reject empty filter dict when no quick constraints are provided.
392+ if delete_mem_req .filter is not None and not has_non_empty_filter and not quick_constraints :
393+ return DeleteMemoryResponse (
394+ message = "filter cannot be empty. Provide a non-empty filter or user_id/session_id." ,
395+ data = {"status" : "failure" },
396+ )
397+
398+ # Validate that only one mode is provided: memory_ids, file_ids, or filter-mode.
329399 provided_params = [
330400 delete_mem_req .memory_ids is not None ,
331401 delete_mem_req .file_ids is not None ,
332- delete_mem_req . filter is not None ,
402+ has_filter_mode ,
333403 ]
334404 if sum (provided_params ) != 1 :
335405 return DeleteMemoryResponse (
336- message = "Exactly one of memory_ids, file_ids, or filter must be provided" ,
406+ message = (
407+ "Exactly one delete mode must be provided: "
408+ "memory_ids, file_ids, or filter/user_id/session_id."
409+ ),
337410 data = {"status" : "failure" },
338411 )
339412
@@ -370,8 +443,14 @@ def handle_delete_memories(delete_mem_req: DeleteMemoryRequest, naive_mem_cube:
370443 naive_mem_cube .text_mem .delete_by_filter (
371444 writable_cube_ids = delete_mem_req .writable_cube_ids , file_ids = delete_mem_req .file_ids
372445 )
373- elif delete_mem_req .filter is not None :
374- naive_mem_cube .text_mem .delete_by_filter (filter = delete_mem_req .filter )
446+ elif has_filter_mode :
447+ merged_filter = _merge_delete_filter (delete_mem_req .filter , quick_constraints )
448+ naive_mem_cube .text_mem .delete_by_filter (
449+ writable_cube_ids = delete_mem_req .writable_cube_ids ,
450+ filter = merged_filter ,
451+ )
452+ if naive_mem_cube .pref_mem is not None :
453+ naive_mem_cube .pref_mem .delete_by_filter (filter = merged_filter )
375454
376455 # After main deletion, optionally clean up related WorkingMemory nodes.
377456 if working_ids_to_delete :
0 commit comments