diff --git a/hindsight-api-slim/hindsight_api/api/http.py b/hindsight-api-slim/hindsight_api/api/http.py index 08896f355d..defa87797e 100644 --- a/hindsight-api-slim/hindsight_api/api/http.py +++ b/hindsight-api-slim/hindsight_api/api/http.py @@ -2412,6 +2412,50 @@ async def api_get_memory( logger.error(f"Error in /v1/default/banks/{bank_id}/memories/{memory_id}: {error_detail}") raise HTTPException(status_code=500, detail=str(e)) + + @app.delete( + "/v1/default/banks/{bank_id}/memories/{memory_id}", + response_model=DeleteResponse, + summary="Delete a memory unit", + description="Delete a single memory unit and all its associated links, entity associations, " + "and derived observations.\n\n" + "Due to CASCADE DELETE constraints, this will automatically delete:\n" + "- All links from/to this memory unit\n" + "- All entity associations\n" + "- All derived observations (remaining source memories are reset for re-consolidation)\n\n" + "This operation cannot be undone.", + operation_id="delete_memory", + tags=["Memory"], + ) + @audited("delete_memory", request_param=None) + async def api_delete_memory( + bank_id: str, + memory_id: str, + request_context: RequestContext = Depends(get_request_context), + ): + """Delete a single memory unit and all its associated data.""" + try: + result = await app.state.memory.delete_memory_unit( + memory_id, + request_context=request_context, + ) + if not result.get("success"): + raise HTTPException(status_code=404, detail=f"Memory unit '{memory_id}' not found") + return DeleteResponse( + success=True, + message=result.get("message", "Memory unit deleted successfully"), + ) + except OperationValidationError as e: + raise HTTPException(status_code=e.status_code, detail=e.reason) + except (AuthenticationError, HTTPException): + raise + except Exception as e: + import traceback + + error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}" + logger.error(f"Error in DELETE /v1/default/banks/{bank_id}/memories/{memory_id}: {error_detail}") + raise HTTPException(status_code=500, detail=str(e)) + @app.get( "/v1/default/banks/{bank_id}/memories/{memory_id}/history", summary="Get observation history", diff --git a/hindsight-api-slim/tests/test_http_api_integration.py b/hindsight-api-slim/tests/test_http_api_integration.py index a2b2b54377..d73a11ae74 100644 --- a/hindsight-api-slim/tests/test_http_api_integration.py +++ b/hindsight-api-slim/tests/test_http_api_integration.py @@ -503,6 +503,81 @@ async def test_document_deletion_with_slashes_in_id(api_client): await api_client.delete(f"/v1/default/banks/{test_bank_id}") + +@pytest.mark.asyncio +async def test_delete_memory_unit(api_client): + """Test deleting a single memory unit via DELETE endpoint. + + Workflow: + 1. Create a bank by storing memories + 2. List memories and pick one + 3. Delete that single memory unit + 4. Verify it returns 404 on GET + 5. Verify other memories still exist + 6. Delete again returns 404 + """ + test_bank_id = f"delete_memory_test_{datetime.now().timestamp()}" + + # 1. Store two memories + response = await api_client.post( + f"/v1/default/banks/{test_bank_id}/memories", + json={ + "items": [ + { + "content": "Alice is a software engineer who loves hiking.", + "context": "team info", + }, + { + "content": "Bob is a data scientist who enjoys painting.", + "context": "team info", + }, + ] + }, + ) + assert response.status_code == 200 + assert response.json()["success"] is True + + # 2. List memories and pick one to delete + response = await api_client.post( + f"/v1/default/banks/{test_bank_id}/memories/list", + json={}, + ) + assert response.status_code == 200 + memories = response.json()["items"] + assert len(memories) >= 2 + memory_to_delete = memories[0]["id"] + other_memory = memories[1]["id"] + + # 3. Delete the memory unit + response = await api_client.delete( + f"/v1/default/banks/{test_bank_id}/memories/{memory_to_delete}" + ) + assert response.status_code == 200 + delete_result = response.json() + assert delete_result["success"] is True + + # 4. Verify deleted memory returns 404 + response = await api_client.get( + f"/v1/default/banks/{test_bank_id}/memories/{memory_to_delete}" + ) + assert response.status_code == 404 + + # 5. Verify other memory still exists + response = await api_client.get( + f"/v1/default/banks/{test_bank_id}/memories/{other_memory}" + ) + assert response.status_code == 200 + + # 6. Try to delete again (should return 404) + response = await api_client.delete( + f"/v1/default/banks/{test_bank_id}/memories/{memory_to_delete}" + ) + assert response.status_code == 404 + + # Cleanup + await api_client.delete(f"/v1/default/banks/{test_bank_id}") + + @pytest.mark.asyncio async def test_delete_bank(api_client): """Test delete bank endpoint.