Authoring Operators#
Operators are pieces of code that perform work on data coming from other operators. You chain them together to build a data pipeline.
Getting operators into podman storage#
Premade operators live under operators/. Podman needs to know about them before a pipeline can run.
docker bake + podman pull#
We can use docker bake and podman pull for operators defined in docker-bake.hcl.
cd interactEM
make setup # no need to run this if you already have
# Build all operators and pull them into podman storage
make operators
# Or build a specific operator and pull it into podman storage
make operator target=center-of-mass-partial
For faster iteration during development, you can omit the --build-base flag by using the lower-level bake.sh script directly:
./operators/bake.sh --push-local --pull-local --target center-of-mass-partial
If you create an operator, you can add it to the HCL file so it becomes part of this build process. This does not happen automatically—update the file manually when you are ready.
podman build#
If you create an operator using the instructions below, you can also use a regular podman build.
For example, if you have an operator named my-operator in the my-operator/ directory:
cd my-operator
# tag should match what is found in the "image" field in your operator.json
podman build -t ghcr.io/nersc/interactem/my-operator:latest .
Creating an operator#
At minimum, each operator needs three files:
The CLI ships with templates to get you started:
uv sync
uv run interactem operator new
or with poetry:
poetry install
poetry run interactem operator new
Or, if you prefer pip:
pip install .
interactem operator new
Note: Operators refresh only in the operators panel during a UI refresh. Existing pipelines are not updated automatically—replace operators manually if needed.
run.py#
Implement the operator logic in run.py. Incoming messages are BytesMessages that carry data, metadata, and tracking information. Before building, you can quickly check importability with:
python -m py_compile run.py
Containerfile#
Use the operator base image as the parent image for your Containerfile. For example, the distiller-streaming image includes utilities for processing 4D Camera frames, so other operators can build FROM it.
Specification#
Operator specifications live in operator.json. The schema is defined in spec.py, and an example is provided in operators/center-of-mass-partial/operator.json.
Examples (docs only)#
@operator
def com_partial(
inputs: BytesMessage | None, parameters: dict[str, Any]
) -> BytesMessage | None:
if not inputs:
logger.warning("No input provided to the subtract operator.")
return None
center = None
init_center_x = parameters.get("init_center_x")
init_center_y = parameters.get("init_center_y")
if init_center_x is not None and init_center_y is not None:
center = (init_center_x, init_center_y)
crop = None
crop_to_x = parameters.get("crop_to_x")
crop_to_y = parameters.get("crop_to_y")
if crop_to_x is not None and crop_to_y is not None:
crop = (crop_to_x, crop_to_y)
batch = BatchedFrames.from_bytes_message(inputs)
com = com_sparse(batch, init_center=center, crop_to=crop, replace_nans=False)
return COMPartial(header=batch.header, array=com).to_bytes_message()
Containerfile examples#
FROM interactem-operator
WORKDIR /app
COPY ./pyproject.toml ./poetry.lock ./README.md /app/
# Base image installs interactem-core at /interactem/core.
# Locally, the project uses ../../backend/core which
# resolves to /backend/core inside the container. We symlink it here
# so poetry can find it.
RUN mkdir -p /backend && \
if [ -d /interactem/core ]; then \
ln -sfn /interactem/core /backend/core; \
fi && \
if [ -d /interactem/operators ]; then \
ln -sfn /interactem/operators /backend/operators; \
fi
RUN poetry install --no-root --without dev
COPY ./distiller_streaming/ /app/distiller_streaming/
RUN poetry install --only-root
FROM distiller-streaming
COPY ./run.py /app/run.py
Specification model#
class OperatorSpec(BaseModel):
id: OperatorSpecID
label: str # Human readable name of the operator
description: str # Human readable description of the operator
image: str # Contain image for operator
inputs: list[OperatorSpecInput] | None = None # List of inputs
outputs: list[OperatorSpecOutput] | None = None # List of outputs
parameters: list[OperatorSpecParameter] | None = None # List of parameters
tags: list[OperatorSpecTag] | None = None # List of tags to match on
parallel_config: ParallelConfig | None = None # Parallel execution config
triggers: list[OperatorSpecTrigger] | None = None # List of triggers
operator.json#{
"id": "70dd71a7-5ebf-4515-8bf9-941d1284328c",
"image": "ghcr.io/nersc/interactem/center-of-mass-partial",
"label": "Partial Center of Mass",
"description": "Calculates the center of mass for a frame",
"inputs": [
{
"name": "in",
"label": "The input",
"type": "frame",
"description": "Input frame"
}
],
"outputs": [
{
"name": "com_partial",
"label": "The output",
"type": "com_partial",
"description": "Partial center of mass"
}
],
"parameters": [
{
"name": "crop_to_x",
"label": "Crop To X",
"type": "int",
"default": "255",
"description": "X-coordinate to crop to",
"required": false
},
{
"name": "crop_to_y",
"label": "Crop To Y",
"type": "int",
"default": "255",
"description": "Y-coordinate to crop to",
"required": false
},
{
"name": "init_center_x",
"label": "Initial Center X",
"default": "255",
"type": "int",
"description": "Initial Center X-coordinate for center of mass calculation",
"required": false
},
{
"name": "init_center_y",
"label": "Initial Center Y",
"default": "255",
"type": "int",
"description": "Initial Center Y-coordinate for center of mass calculation",
"required": false
}
],
"parallel_config": {
"type": "embarrassing"
}
}