-
Notifications
You must be signed in to change notification settings - Fork 11
Add support for pydantic model registration with BlueAPI #1458
Description
When testing with Blueapi, I have a pydantic model loaded and ready to give to my plan / device client side.
>>> region1
VGScientaRegion[LensMode, PassEnergy](name='New_Region', enabled=True, slices=1, iterations=1, excitation_energy_source=<SelectedSource.SOURCE1: 'source1'>, lens_mode=<LensMode.ANGULAR45: 'Angular45'>, pass_energy=<PassEnergy.E10: '10'>, acquisition_mode=<AcquisitionMode.SWEPT: 'Swept'>, low_energy=149.0, centre_energy=150.0, high_energy=151.0, acquire_time=1.0, energy_step=0.2, energy_mode=<EnergyMode.KINETIC: 'Kinetic'>, id='_bMnQkCkaEfG3xKiZ1VupOQ', total_steps=15.0, total_time=15.0, min_x=1, sensor_max_size_x=1000, min_y=101, sensor_max_size_y=800, detector_mode=<DetectorMode.ADC: 'ADC'>)
>>> plans.set_absolute(devs.ew4000, region1)When trying to set my device with these settings
>>> plans.set_absolute(devs.ew4000, region1)I can see these logs:
2026-03-26 15:05:25,816 WARNING blueapi.worker.task_worker Invalid correlation id detected: None
2026-03-26 15:05:25,817 INFO blueapi.service.main 10.36.0.26:49436 POST /tasks 201
2026-03-26 15:05:25,830 INFO blueapi.worker.task_worker Submitting: task_id='3ea512cf-4a5c-4349-9bcd-39a6ec357e3f' task=Task(name='set_absolute', params={'movable': 'ew4000', 'value': {'name': 'New_Region', 'enabled': True, 'slices': 1, 'iterations': 1, 'excitation_energy_source': 'source1', 'lens_mode': 'Angular45', 'pass_energy': '10', 'acquisition_mode': 'Swept', 'low_energy': 149.0, 'centre_energy': 150.0, 'high_energy': 151.0, 'acquire_time': 1.0, 'energy_step': 0.2, 'energy_mode': 'Kinetic', 'id': '_bMnQkCkaEfG3xKiZ1VupOQ', 'total_steps': 15.0, 'total_time': 15.0, 'min_x': 1, 'sensor_max_size_x': 1000, 'min_y': 101, 'sensor_max_size_y': 800, 'detector_mode': 'ADC'}}, metadata={'user': 'xol73553', 'instrument_session': 'cm12345-1'}) request_id=None is_complete=False is_pending=True errors=[] outcome=None
2026-03-26 15:05:25,831 INFO blueapi.worker.task_worker Got new task: task_id='3ea512cf-4a5c-4349-9bcd-39a6ec357e3f' task=Task(name='set_absolute', params={'movable': 'ew4000', 'value': {'name': 'New_Region', 'enabled': True, 'slices': 1, 'iterations': 1, 'excitation_energy_source': 'source1', 'lens_mode': 'Angular45', 'pass_energy': '10', 'acquisition_mode': 'Swept', 'low_energy': 149.0, 'centre_energy': 150.0, 'high_energy': 151.0, 'acquire_time': 1.0, 'energy_step': 0.2, 'energy_mode': 'Kinetic', 'id': '_bMnQkCkaEfG3xKiZ1VupOQ', 'total_steps': 15.0, 'total_time': 15.0, 'min_x': 1, 'sensor_max_size_x': 1000, 'min_y': 101, 'sensor_max_size_y': 800, 'detector_mode': 'ADC'}}, metadata={'user': 'xol73553', 'instrument_session': 'cm12345-1'}) request_id=None is_complete=False is_pending=True errors=[] outcome=None
2026-03-26 15:05:25,831 INFO blueapi.worker.task Asked to run plan set_absolute with {'movable': 'ew4000', 'value': {'name': 'New_Region', 'enabled': True, 'slices': 1, 'iterations': 1, 'excitation_energy_source': 'source1', 'lens_mode': 'Angular45', 'pass_energy': '10', 'acquisition_mode': 'Swept', 'low_energy': 149.0, 'centre_energy': 150.0, 'high_energy': 151.0, 'acquire_time': 1.0, 'energy_step': 0.2, 'energy_mode': 'Kinetic', 'id': '_bMnQkCkaEfG3xKiZ1VupOQ', 'total_steps': 15.0, 'total_time': 15.0, 'min_x': 1, 'sensor_max_size_x': 1000, 'min_y': 101, 'sensor_max_size_y': 800, 'detector_mode': 'ADC'}} and metadata {'user': 'xol73553', 'instrument_session': 'cm12345-1'} for all runs
2026-03-26 15:05:25,831 INFO bluesky Executing plan <generator object set_absolute at 0x7fbbf3c32240>
2026-03-26 15:05:25,832 INFO bluesky.RE.state Change state on <bluesky.run_engine.RunEngine object at 0x7fbbf993af50> from 'idle' -> 'running'
2026-03-26 15:05:25,836 WARNING opentelemetry.attributes Invalid type dict in attribute 'msg.args' value sequence. Expected one of ['bool', 'str', 'bytes', 'int', 'float'] or None
2026-03-26 15:05:25,836 INFO bluesky.RE.state Change state on <bluesky.run_engine.RunEngine object at 0x7fbbf993af50> from 'running' -> 'idle'
2026-03-26 15:05:25,837 INFO bluesky Cleaned up from plan <generator object set_absolute at 0x7fbbf3c32240>
2026-03-26 15:05:25,837 INFO blueapi.worker.task_worker Task ran successfully - returned: <AsyncStatus, device: ew4000, task: <coroutine object AsyncStatusBase.__init__.<locals>.wait_with_error_message at 0x7fbbf3c30a40>, errored: AttributeError("'dict' object has no attribute 'excitation_energy_source'")>
2026-03-26 15:05:25,838 INFO blueapi.worker.task_worker Awaiting task
2026-03-26 15:05:25,838 INFO blueapi.service.main 10.36.0.26:49436 PUT /worker/task 200With key bit being
2026-03-26 15:05:25,836 WARNING opentelemetry.attributes Invalid type dict in attribute 'msg.args' value sequence.
With the key parts being
Expected one of ['bool', 'str', 'bytes', 'int', 'float'] or None
ew4000, task: <coroutine object AsyncStatusBase.__init__.<locals>.wait_with_error_message at 0x7fbbf3c30a40>, errored: AttributeError("'dict' object has no attribute 'excitation_energy_source'")>
So my device expects the pydantic model
@AsyncStatus.wrap
async def set(self, region: TAbstractBaseRegion) -> None:So the issue is that Blueapi does the following
Python object -> JSON -> Python dic
So my device set now fails because it expects the model and not the python dict.
When creating devices in dodal, we were told to make it as strongly typed as possible but now were needing to regress as we either have to make the device handle a dict or create a custom set plan for this device to convert the dict into a pydantic model before we give it to the device.
It would be good if we could add Blueapi support to be able to register pydantic models in the blueapi service values.yaml. Then we retain the type information during serialisation, something like "__type__": "dodal.my.pydantic.model.Implementation" for example so that then on server side when executing the plan, it can attempt to look up registered models and convert back into the pydantic model so that we don't need to write wrapper plans for every pydantic object.