Skip to content

Add a generic manual operator action framework - #1203

Open
j0nch wants to merge 15 commits into
PyLabRobot:mainfrom
j0nch:feat/manual-operator-actions
Open

Add a generic manual operator action framework#1203
j0nch wants to merge 15 commits into
PyLabRobot:mainfrom
j0nch:feat/manual-operator-actions

Conversation

@j0nch

@j0nch j0nch commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add a machine-agnostic ManualOperator frontend for awaiting work performed by a person
  • define a transport-neutral provider contract so terminal, GUI, HTTP, LIMS, or message-broker integrations can present and acknowledge the same request
  • distinguish completed, cancelled, and failed operator outcomes
  • include an asynchronous console provider that does not block the event loop
  • add move_resource() for acknowledged manual transfers with PLR resource-model reconciliation
  • emit native EventBus lifecycles for manual operations and document their semantics
  • add a Jupyter/chatterbox cookbook demonstrating an incubator-to-reader manual handoff

Why a dedicated ManualOperator?

A direct input() pause is often appropriate for a simple notebook workflow. ManualOperator provides a reusable PLR contract when a protocol needs a safer or more observable manual handoff:

  • keep protocol code independent of the acknowledgement transport, so the same action can later use a terminal, notebook, GUI, HTTP service, LIMS, Slack, or message broker
  • represent completed, cancelled, and failed acknowledgements explicitly instead of reducing every outcome to Enter being pressed
  • reconcile manual resource transfers with the PLR resource model: validate the declared source and destination before prompting, verify them again after acknowledgement, then update the model only when it remains consistent
  • emit a correlated EventBus lifecycle with the action, affected resources, endpoints, acknowledgement, and failure information when a subscriber is active

The built-in console provider remains deliberately simple for users who only need an interactive pause; richer providers are optional application integrations.

Operator providers

ManualOperator.perform() creates a structured OperatorActionRequest and awaits an OperatorActionProvider. Providers return an explicit completed, cancelled, or failed result; provider exceptions propagate unchanged. The built-in console provider runs blocking input in a worker thread so the protocol event loop remains available.

The request contract is independent of any one presentation transport. Applications can implement a provider for a terminal, notebook, GUI, HTTP service, LIMS, or message broker without changing protocol code.

Resource moves

ManualOperator.move_resource() validates the modeled source and destination before prompting, leaves the resource assigned to its source while the operator works, and updates the model only after successful acknowledgement. It revalidates before assignment to avoid overwriting state that changed while the request was pending. Cancellation, reported failure, and provider exceptions leave the resource model unchanged.

The operation uses PLR's standard resource-assignment machinery, including default ResourceHolder child positioning and optional explicit destination coordinates. Requests contain transport-safe resource names rather than live Resource objects.

EventBus integration

With an active EventBus subscriber, manual actions emit a correlated lifecycle named manual_operator.<action>.started, .completed, or .failed. Events use device_reference() for the ManualOperator, carry direct modeled resources through resource_reference(), preserve structured request details, and add provider completion metadata when available. Cancellation, provider-reported failure, invalid results, and provider exceptions produce the standard failed lifecycle without changing normal exception behavior.

move_resource() emits manual_operator.resource.move.* with the direct moved resource and its true source and destination. PLR's existing resource.unassigned and resource.assigned events independently record the subsequent model transition.

Testing

  • python3 -m unittest pylabrobot.events.bus_tests pylabrobot.manual_operator.manual_operator_tests -v
  • 24 focused EventBus/manual-operator tests passing
  • python3 -m compileall -q pylabrobot/events pylabrobot/manual_operator
  • git diff --check origin/main...HEAD
  • Manually validated a notebook-local action provider: the operator prompt was acknowledged successfully and the expected EventBus lifecycle event was emitted.
  • Executed the cookbook's chatterbox workflow with both EventBus observation enabled and disabled; the modeled plate is reconciled from incubator storage through the loading tray into the plate reader, then read successfully.

@j0nch
j0nch force-pushed the feat/manual-operator-actions branch 2 times, most recently from 63f9511 to 9a198d3 Compare August 18, 2026 23:35
@j0nch
j0nch marked this pull request as ready for review August 19, 2026 02:47
@j0nch
j0nch force-pushed the feat/manual-operator-actions branch from b749dd8 to 50cf8ad Compare August 20, 2026 06:16
Comment thread pylabrobot/manual_operator/standard.py Outdated
Comment on lines +10 to +15
class OperatorActionStatus(str, Enum):
"""Outcome reported by an operator-action provider."""

COMPLETED = "completed"
CANCELLED = "cancelled"
FAILED = "failed"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make this into a string literal?

Comment thread pylabrobot/manual_operator/standard.py Outdated
Comment on lines +36 to +47
def __post_init__(self) -> None:
for field_name in (
"operator_name",
"action",
"title",
"instructions",
"confirmation_text",
):
if not getattr(self, field_name).strip():
raise ValueError(f"{field_name} must not be empty")
object.__setattr__(self, "details", self.details.copy())
object.__setattr__(self, "resources", tuple(self.resources))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yikes

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants