Metadata-Version: 2.4
Name: rhylthyme-galago
Version: 0.2.0a0
Summary: Drive galago-tools lab instruments from Rhylthyme programs
Author: Rhylthyme
License-Expression: Apache-2.0
Project-URL: Homepage, https://rhylthyme.com
Project-URL: Source, https://github.com/rhylthyme/rhylthyme-galago
Requires-Python: >=3.12
Description-Content-Type: text/markdown
License-File: LICENSE
License-File: NOTICE
License-File: LICENSES/galago-tools-LICENSE
Requires-Dist: grpcio>=1.84.0
Requires-Dist: protobuf<8,>=7.35.1
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == "dev"
Requires-Dist: grpcio-tools==1.84.0; extra == "dev"
Dynamic: license-file

# rhylthyme-galago

[![License: Apache-2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](LICENSE)
[![PyPI](https://img.shields.io/pypi/v/rhylthyme-galago)](https://pypi.org/project/rhylthyme-galago/)

Run lab instruments from [Rhylthyme](https://rhylthyme.com) programs through
**[galago-tools](https://github.com/sciencecorp/galago-tools)**, Science
Corporation's open-source (Apache-2.0) gRPC drivers for more than 20 kinds of
instrument: shakers, incubators, plate readers, imagers, liquid handlers and
robot arms. galago-tools is the driver layer of Science's
[Galago](https://github.com/sciencecorp/galago-core) lab-automation stack; this
package lets Rhylthyme's planner and runner be the scheduler on top of it. It
is an independent project, not affiliated with or endorsed by Science
Corporation.

![rhylthyme run with three galago tools: the shake step is waiting on the shaker](https://raw.githubusercontent.com/rhylthyme/rhylthyme-galago/main/docs/images/terminal-running.png)

A step names a galago command; the runner sends it when the step starts, and the
step ends when the instrument replies:

```json
{
  "stepId": "shake",
  "name": "Shake at 1000 rpm",
  "instrument": {
    "tool": "shaker",
    "toolType": "bioshake",
    "command": "start_shake",
    "params": { "speed": 1000, "acceleration": 5, "duration": 5 }
  },
  "startTrigger": { "type": "afterStep", "stepId": "load-plate" }
}
```

Programs name tools by role. A local **workcell** file says where each tool
listens, and stays on the lab machine:

```json
{
  "id": "simulated-bench",
  "tools": [
    { "name": "shaker", "type": "bioshake", "host": "localhost", "port": 50710,
      "config": { "com_port": "COM3" } }
  ]
}
```

Status: alpha ([plan](https://github.com/rhylthyme/rhylthyme-galago/issues/1)).
Tools run in galago's simulated mode unless you pass `--live`.

## Quick start (simulated, no hardware)

galago-tools needs Python 3.9; Rhylthyme needs 3.12+. They talk over gRPC, so each
gets its own environment.

```bash
# 1. A simulated Bioshake from galago-tools
python3.9 -m venv .venv-galago
.venv-galago/bin/pip install galago-tools
.venv-galago/bin/galago-serve --tool bioshake --port 50710 &

# 2. Rhylthyme with instrument support
python3.12 -m venv .venv && source .venv/bin/activate
pip install "rhylthyme[galago]"

# 3. The example program, and a workcell pointing at that Bioshake
curl -sO https://raw.githubusercontent.com/rhylthyme/rhylthyme-galago/main/examples/shake-plate.json
curl -sO https://raw.githubusercontent.com/rhylthyme/rhylthyme-galago/main/examples/workcell-simulated.json

# 4. Run it: the tools are configured (simulated) and the run starts;
#    p pauses, q quits
rhylthyme run shake-plate.json --workcell workcell-simulated.json
```

While the command runs, the runner shows the step with a `[shaker]` badge and
"waiting on shaker" (screenshot above), and the shaker is held as a resource
until it replies. The run record (`rhylthyme runs`) marks it
`endedBy: "instrument"` and keeps every reply, retries included, with any data
the tool returned:

```json
"instrument": {
  "tool": "shaker", "command": "start_shake",
  "replies": [
    { "attempt": 1, "at": 3.0, "code": "DRIVER_ERROR", "errorMessage": "lid open" },
    { "attempt": 2, "at": 41.2, "code": "SUCCESS", "metadata": { "wells": { "A1": 0.41 } } }
  ]
}
```

## A three-tool example

`examples/passage-check.json` (in this repo) fetches a plate from a Liconic incubator, shakes it
on a Bioshake, images it on a Cytation, has someone check the media, and stores
it again, while media is warmed and aliquoted by hand on a second track:

```bash
for t in liconic:50721 bioshake:50722 cytation:50723; do
  .venv-galago/bin/galago-serve --tool ${t%%:*} --port ${t##*:} &
done
rhylthyme run examples/passage-check.json --workcell examples/workcell-cell-culture.json
```

It runs end to end in CI against these simulated servers
(`tests/test_integration.py`). The screenshot at the top of this page is this
program, a few seconds in: the plate has been fetched, the Bioshake is running,
and someone is warming media on the other track.

## On the web

Programs with instrument steps validate, visualize and publish on
[rhylthyme.com](https://rhylthyme.com) and through the
[Rhylthyme MCP server](https://mcp.rhylthyme.com/mcp) like any other: instrument
steps carry a `[tool]` badge, and bars whose length is an estimate have a
dotted outline. Workcells are never uploaded.

![The web player: the shake step carries a [shaker] badge and a dotted outline](https://raw.githubusercontent.com/rhylthyme/rhylthyme-galago/main/docs/images/web-player.png)

galago-tools 0.19.9 cannot simulate an Opentrons `run_program` (its simulated
dispatch passes an argument `RunProgram` does not take, so the tool answers
`DRIVER_ERROR`); other Opentrons commands, and real OT-2 runs, are unaffected.

## Real hardware

```bash
rhylthyme run shake.json --workcell lab.json --live
```

`--live` first lists each tool (address and current status, read without
configuring anything) and every instrument command the run will send, then asks
you to type `live`. Only then are the tools configured for real; the run starts
only if every tool reports READY. Scripts pass `--confirm-live` instead of
answering; without a terminal and without that flag, a live run is refused.

Workcell files stay on the lab machine: `rhylthyme publish` and `rhylthyme
analyze` refuse them, programs name tools only by role, and run records hold no
tool addresses.

## From a browser: `rhylthyme bridge`

```bash
rhylthyme login
rhylthyme bridge --workcell lab.json            # wait for runs started on the web
rhylthyme bridge run.json --workcell lab.json   # or run one program and publish it
```

The bridge lists the lab machine on the **Bridges** page of
[rhylthyme.com](https://rhylthyme.com/bridges), for your account only. From there
you can watch a run's timeline live, pause and resume it, answer a failed
instrument step (retry, skip or abort), and start any program saved in your
library on that workcell. The terminal shows the same runner UI, plus every
command that came from the web; Ctrl-C at the lab machine always wins.

- **Outbound only.** The bridge opens no port and needs no tunnel; it makes
  HTTPS calls to rhylthyme.com with your own login.
- **Nothing about the lab network leaves it.** Tools are published by name,
  type and status only; addresses, ports and configs never are, including in
  error messages.
- **The lab machine decides.** It rejects commands older than 30 seconds,
  acts on each command once, refuses programs with code blocks or that do not
  validate on its workcell, and configures the tools before accepting a start
  (a tool that is not ready is a refusal the browser sees).
- **Live from the web needs two keys.** Start the bridge with `--allow-live`;
  the browser then shows the tools and every instrument command in the program
  and asks you to type `live`. Without `--allow-live`, web starts are simulated
  only.

## Tools are resources

Each tool a program uses is a resource of capacity 1, so two steps never send
commands to the same shaker at once: the second waits until the first is done.
A tool needs no person (actor) while it works, so hand steps and instrument
steps run side by side. To let a tool take more than one command at a time,
declare a constraint with its name:

```json
"resourceConstraints": [{ "task": "shaker", "maxConcurrent": 2, "description": "two-deck shaker" }]
```

## When an instrument fails

Any reply other than SUCCESS (a driver error, a tool that is unreachable or
not ready) and any command that outlives its `timeoutSeconds` marks the step
**FAILED**. Nothing new starts; steps already running on other tools finish. The
runner shows what failed (tool, command, response code, message) and waits:

- `r` resends the command; if it succeeds the program carries on.
- `x` marks the step done by hand (`endedBy: "skipped"`) and releases what depends on it.
- `A` twice aborts the program; the reason is kept in the run record (`context.abortReason`).

Ctrl-C stops the run at once and names any command still in flight.

![A timed-out imaging step: FAILED in red, the banner names the tool, command and code, and media warming carries on](https://raw.githubusercontent.com/rhylthyme/rhylthyme-galago/main/docs/images/terminal-failed.png)

## Planning with instrument durations

An instrument step may leave out its `duration`: at run time it ends when the
tool replies. For planning, `rhylthyme plan` and `rhylthyme analyze` fill one in
and flag it in `metadata.durationEstimate`:

```text
$ rhylthyme plan shake.json planned.json --workcell lab.json
Estimated instrument durations:
  shake: 5 s (from shaker EstimateDuration)
Makespan (by start triggers and durations): 11 s
```

![rhylthyme render of the three-tool example; dotted bars are estimated](https://raw.githubusercontent.com/rhylthyme/rhylthyme-galago/main/docs/images/passage-check-timeline.svg)

The number comes from the tool's own `EstimateDuration` (with `--workcell`, for
tools that are already configured; planning never configures a tool), else a
duration-like command param (`duration`, `timeout`, ...), else 60 s. Authored
durations are never changed.

## Checking programs

`rhylthyme validate` checks every instrument step's command and params against
galago's own definitions, naming the step, tool, command and field:

```text
$ rhylthyme validate shake.json --workcell lab.json
  - [instrument_invalid_command] Step 'shake': shaker (bioshake) start_shake: unknown param 'rpm' (takes speed, acceleration, duration)
```

Without `--workcell`, steps are checked against their `toolType`; steps with
neither get an `instrument_unchecked` warning. With `--workcell`, it also reports
tools the workcell lacks and `toolType`s that disagree with it. `rhylthyme run`
runs the same checks before configuring any tool.

From Python: `rhylthyme_galago.validate_command(tool_type, command, params)` and
`check_program(program, workcell=None)`.

## Development

```bash
pip install -e ".[dev]"
pytest                      # unit tests; integration tests need galago-serve
GALAGO_SERVE=/path/to/galago-serve pytest -m integration
```

The integration tests find `galago-serve` through `GALAGO_SERVE`, then
`.venv-galago/bin/galago-serve`, then `PATH`, and are skipped when none exists.

### galago protos

`proto/galago/` holds galago-tools' `.proto` files, copied verbatim and pinned in
`proto/galago/UPSTREAM.json`. The Python stubs in `src/rhylthyme_galago/_gen/` are
generated from them under this package's namespace (the wire format is galago's own):

```bash
python scripts/refresh_protos.py --ref <galago-tools commit or tag>  # re-vendor + regenerate
python scripts/refresh_protos.py                                      # regenerate only
```

## License

rhylthyme-galago is licensed under the [Apache License 2.0](LICENSE).

It includes material from [galago-tools](https://github.com/sciencecorp/galago-tools),
Copyright 2025 - Science Corporation, also licensed under the Apache License
2.0 ([copy](LICENSES/galago-tools-LICENSE)):

- `proto/galago/`: galago-tools' `.proto` files, unmodified, pinned in
  `proto/galago/UPSTREAM.json`;
- `src/rhylthyme_galago/_gen/`: Python generated from them, with the proto
  import paths moved under this package (each file says so in its header);
- `catalog/galago-commands.json`: a JSON description of their commands.

[NOTICE](NOTICE) lists these; the wheel and sdist carry `LICENSE`, `NOTICE` and
`LICENSES/galago-tools-LICENSE`. galago-tools itself is not bundled: you install
it separately, from Science Corporation.
