Capture exceptions raised from `PyFunctionLibrary` functions for debuggability.
Capture exceptions raised from `PyFunctionLibrary` functions for debuggability.

Before this change, when there is an error raised inside a `PyFunctionLibrary` method, the program would crash with a long stack trace, something like the following:

```
<super-long c++ stack trace with no meaningful information>

libc++abi: terminating due to uncaught exception of type pybind11::error_already_set: ValueError: Failed to run graph for post-training quantization calibration.
```

The information in the raised exception is lost in this case, hindering debuggability.

This change instead returns regular value `None` back to the c++ layer to indicate error, and prints the exception chain from the python side.
It prints out the exact error message one would see when an exception is raised from regular python script.

The return values of `PyFunctionLibrary` methods are all changed to `std::optional<T>`, where `T` is the successful return value and `std::nullopt` indicates error.
To avoid conflict btw erroneous `None`, methods that do not essentially return a value now returns a dummy `True` to indicate successful run.
