├── .gitmodules ├── backend ├── core │ ├── README.md │ ├── interactem │ │ ├── core │ │ │ ├── __init__.py │ │ │ ├── events │ │ │ │ └── __init__.py │ │ │ ├── models │ │ │ │ ├── __init__.py │ │ │ │ ├── README.md │ │ │ │ ├── triggers.py │ │ │ │ ├── _export.py │ │ │ │ ├── logs.py │ │ │ │ ├── base.py │ │ │ │ └── messages.py │ │ │ ├── util.py │ │ │ ├── constants │ │ │ │ └── mounts.py │ │ │ ├── nats │ │ │ │ └── storage.py │ │ │ └── config.py │ │ └── __init__.py │ ├── .vscode │ │ └── settings.json │ ├── scripts │ │ └── pydantic_to_ts.py │ └── pyproject.toml ├── launcher │ ├── README.md │ ├── .python-version │ ├── interactem │ │ ├── launcher │ │ │ ├── __init__.py │ │ │ ├── constants.py │ │ │ ├── templates │ │ │ │ ├── launch_agent.sh.j2 │ │ │ │ ├── header.sh.j2 │ │ │ │ └── ascii_header.sh.j2 │ │ │ └── config.py │ │ └── __init__.py │ ├── scripts │ │ └── test.sh │ ├── run.py │ ├── tests │ │ ├── .env.example │ │ ├── expected_script.sh │ │ └── test_rendering.py │ ├── .env.example │ └── pyproject.toml ├── operators │ ├── README.md │ ├── interactem │ │ ├── operators │ │ │ ├── __init__.py │ │ │ ├── messengers │ │ │ │ ├── __init__.py │ │ │ │ ├── zeromq │ │ │ │ │ └── __init__.py │ │ │ │ └── base.py │ │ │ └── config.py │ │ └── __init__.py │ ├── pyproject.toml │ └── .vscode │ │ └── settings.json ├── .dockerignore ├── orchestrator │ ├── README.md │ ├── interactem │ │ ├── orchestrator │ │ │ ├── __init__.py │ │ │ ├── types.py │ │ │ ├── constants.py │ │ │ └── config.py │ │ └── __init__.py │ ├── .env.example │ ├── run.py │ ├── pyproject.toml │ └── .vscode │ │ └── settings.json ├── sfapi_models │ ├── README.md │ ├── .python-version │ ├── interactem │ │ └── __init__.py │ └── pyproject.toml ├── agent │ ├── .python-version │ ├── interactem │ │ ├── agent │ │ │ ├── __init__.py │ │ │ ├── entrypoint.py │ │ │ └── util.py │ │ └── __init__.py │ ├── .env.example │ ├── .vscode │ │ └── settings.json │ └── pyproject.toml ├── app │ ├── .python-version │ ├── interactem │ │ ├── app │ │ │ ├── __init__.py │ │ │ ├── api │ │ │ │ ├── __init__.py │ │ │ │ ├── routes │ │ │ │ │ ├── __init__.py │ │ │ │ │ ├── agents.py │ │ │ │ │ └── operators.py │ │ │ │ └── main.py │ │ │ ├── core │ │ │ │ ├── __init__.py │ │ │ │ ├── util.py │ │ │ │ └── security.py │ │ │ ├── events │ │ │ │ └── __init__.py │ │ │ ├── tests │ │ │ │ ├── __init__.py │ │ │ │ ├── api │ │ │ │ │ ├── __init__.py │ │ │ │ │ └── routes │ │ │ │ │ │ ├── __init__.py │ │ │ │ │ │ ├── test.http │ │ │ │ │ │ └── test_login.py │ │ │ │ ├── crud │ │ │ │ │ └── __init__.py │ │ │ │ ├── scripts │ │ │ │ │ ├── __init__.py │ │ │ │ │ ├── test_test_pre_start.py │ │ │ │ │ └── test_backend_pre_start.py │ │ │ │ ├── utils │ │ │ │ │ ├── __init__.py │ │ │ │ │ └── utils.py │ │ │ │ └── conftest.py │ │ │ ├── alembic │ │ │ │ ├── versions │ │ │ │ │ ├── .keep │ │ │ │ │ ├── d1149e7679f9_add_is_external_to_user.py │ │ │ │ │ ├── b76f7b29a6e7_add_operator_positions_to_pipeline_.py │ │ │ │ │ ├── 60a2bc7c4aea_add_pipeline_name.py │ │ │ │ │ ├── ac46c9f37d67_add_pipelines_table.py │ │ │ │ │ ├── 1a31ce608336_add_cascade_delete_relationships.py │ │ │ │ │ ├── 516457e800b5_remove_items.py │ │ │ │ │ └── ae4ed8e4c67b_current_revision_id.py │ │ │ │ ├── README │ │ │ │ └── script.py.mako │ │ │ ├── initial_data.py │ │ │ ├── tests_pre_start.py │ │ │ └── backend_pre_start.py │ │ └── __init__.py │ ├── scripts │ │ ├── format.sh │ │ ├── lint.sh │ │ └── test.sh │ ├── .gitignore │ ├── .dockerignore │ ├── tests-start.sh │ ├── prestart.sh │ └── .vscode │ │ └── settings.json ├── metrics │ ├── interactem │ │ ├── __init__.py │ │ └── metrics │ │ │ ├── __init__.py │ │ │ └── config.py │ ├── run.py │ ├── monitoring-conf │ │ ├── prometheus.yml │ │ └── grafana │ │ │ └── provisioning │ │ │ ├── dashboards │ │ │ └── dashboard.yml │ │ │ └── datasources │ │ │ └── datasource.yml │ ├── pyproject.toml │ └── README.md ├── callout │ ├── test │ │ ├── .env.example │ │ └── run.py │ └── service │ │ ├── .env.example │ │ └── go.mod └── rdma │ ├── libs │ ├── thallium │ │ ├── src │ │ │ ├── eng_registry.cpp │ │ │ ├── eng_dispatcher.cpp │ │ │ ├── eng_utils.cpp │ │ │ └── eng_provider.cpp │ │ └── CMakeLists.txt │ ├── argobots │ │ ├── CMakeLists.txt │ │ └── include │ │ │ ├── abt_config.hpp │ │ │ └── abt_manager.hpp │ └── CMakeLists.txt │ ├── src │ ├── prx_transporter.cpp │ └── CMakeLists.txt │ ├── common │ ├── CMakeLists.txt │ └── include │ │ ├── op_mode.hpp │ │ └── abt_type.hpp │ ├── include │ ├── op_sender.hpp │ ├── op_receiver.hpp │ └── RdmaProxyService.hpp │ └── CMakeLists.txt ├── operators ├── .gitignore ├── distiller-streaming │ ├── README.md │ ├── distiller_streaming │ │ ├── __init__.py │ │ └── com.py │ ├── .env.example │ ├── Containerfile │ └── pyproject.toml ├── error │ ├── Containerfile │ ├── operator.json │ └── run.py ├── vars-base.hcl ├── virtual-bfdf │ └── Containerfile ├── benchmark-receiver │ ├── Containerfile │ └── operator.json ├── bin-sparse-partial │ ├── Containerfile │ └── operator.json ├── distiller-grabber │ └── Containerfile ├── center-of-mass-partial │ └── Containerfile ├── center-of-mass-reduce │ ├── Containerfile │ └── operator.json ├── detstream-aggregator │ ├── Containerfile │ └── operator.json ├── detstream-assembler │ ├── Containerfile │ └── operator.json ├── distiller-state-client │ ├── Containerfile │ └── operator.json ├── pva-converter │ ├── .env.example │ ├── Containerfile │ └── operator.json ├── vars-local.hcl ├── diffraction-pattern-accumulator │ ├── Containerfile │ └── operator.json ├── dpc │ ├── Containerfile │ └── operator.json ├── random-table │ ├── Containerfile │ └── operator.json ├── benchmark-sender │ ├── Containerfile │ └── operator.json ├── detstream-producer │ ├── Containerfile │ ├── operator.json │ └── config.json ├── center-of-mass-plot │ ├── Containerfile │ └── operator.json ├── detstream-state-server │ ├── Containerfile │ ├── config.json │ └── operator.json ├── electron-count-save │ ├── Containerfile │ └── operator.json ├── image-display │ ├── Containerfile │ └── operator.json ├── random-image │ ├── Containerfile │ ├── run.py │ └── operator.json ├── data-replay │ ├── Containerfile │ └── operator.json ├── distiller-counted-data-reader │ └── Containerfile ├── sparse-frame-image-converter │ └── Containerfile ├── array-image-converter │ └── Containerfile ├── table-display │ ├── Containerfile │ └── operator.json ├── vars-prod.hcl ├── .env.example ├── vars-ci.hcl ├── pvapy-ad-sim-server │ ├── Containerfile │ ├── operator.json │ └── run.sh ├── read-tem-data │ ├── Containerfile │ └── operator.json ├── quantem-direct-ptycho │ └── Containerfile ├── beam-compensation │ ├── Containerfile │ └── operator.json ├── login_ghcr.sh └── electron-count │ ├── Containerfile │ └── operator.json ├── docker ├── Dockerfile.base.dockerignore ├── Dockerfile.callout.dockerignore ├── Dockerfile.fastapi.dockerignore ├── Dockerfile.launcher.dockerignore ├── Dockerfile.operator.dockerignore ├── Dockerfile.orchestrator.dockerignore ├── docker-bake-arm64.hcl ├── docker-bake-amd64.hcl ├── Dockerfile.frontend.dockerignore ├── Dockerfile.frontend ├── docker-bake-ci.hcl ├── Dockerfile.metrics ├── Dockerfile.orchestrator ├── Dockerfile.launcher ├── Dockerfile.operator ├── bake.sh ├── Dockerfile.fastapi ├── Dockerfile.callout └── Dockerfile.base ├── frontend ├── interactEM │ ├── .dockerignore │ ├── src │ │ ├── vite-env.d.ts │ │ ├── client │ │ │ └── generated │ │ │ │ ├── index.ts │ │ │ │ └── client.gen.ts │ │ ├── index.ts │ │ ├── constants │ │ │ └── tanstack.ts │ │ ├── App.tsx │ │ ├── types │ │ │ ├── pipeline.ts │ │ │ ├── operator.ts │ │ │ ├── triggers.ts │ │ │ └── agent.ts │ │ ├── main.tsx │ │ ├── components │ │ │ ├── pipelines │ │ │ │ ├── hud.tsx │ │ │ │ ├── hudlistbutton.tsx │ │ │ │ └── viewmodetoggle.tsx │ │ │ ├── nodes │ │ │ │ ├── handles.tsx │ │ │ │ ├── header.tsx │ │ │ │ ├── parametersbutton.tsx │ │ │ │ ├── table.tsx │ │ │ │ └── image.tsx │ │ │ ├── statusdot.tsx │ │ │ ├── image.tsx │ │ │ ├── logs │ │ │ │ └── agentdialog.tsx │ │ │ ├── notificationstoast.ts │ │ │ └── agents │ │ │ │ └── chip.tsx │ │ ├── contexts │ │ │ ├── nats │ │ │ │ ├── allstatus.tsx │ │ │ │ └── agentstatus.tsx │ │ │ └── dnd.tsx │ │ ├── auth │ │ │ ├── base.tsx │ │ │ └── api.ts │ │ ├── hooks │ │ │ ├── nats │ │ │ │ ├── useAgents.ts │ │ │ │ ├── useImage.ts │ │ │ │ ├── useTableData.ts │ │ │ │ └── useBucket.ts │ │ │ └── api │ │ │ │ ├── usePipelineQuery.ts │ │ │ │ └── useOperatorSpecs.ts │ │ ├── utils │ │ │ ├── statusColor.ts │ │ │ └── deployments.ts │ │ └── config │ │ │ └── index.ts │ ├── .env.development │ ├── tsconfig.node.json │ ├── tests │ │ └── e2e │ │ │ ├── smoke.spec.ts │ │ │ └── fixtures │ │ │ └── auth.ts │ ├── openapi-ts.config.ts │ ├── index.html │ ├── vite.config.app.ts │ ├── .gitignore │ ├── public │ │ └── microscope.svg │ ├── biome.json │ ├── tsconfig.json │ ├── playwright.config.ts │ └── vite.config.ts └── .vscode │ └── settings.json ├── CLAUDE.md ├── conf └── nats-conf │ ├── websocket.conf │ ├── nats1.conf │ ├── nats2.conf │ └── nats3.conf ├── .gitattributes ├── cli ├── interactem │ └── cli │ │ ├── __init__.py │ │ ├── templates │ │ └── Containerfile.j2 │ │ ├── main.py │ │ ├── settings.py │ │ └── models.py ├── .env.example ├── README.md └── pyproject.toml ├── docs ├── .gitignore ├── source │ ├── index.rst │ ├── _static │ │ ├── js │ │ │ └── version-picker.js │ │ └── css │ │ │ └── custom.css │ ├── getting-started.md │ └── launch-agent.md ├── Makefile ├── pyproject.toml └── README.md ├── .vscode ├── extensions.json └── settings.json ├── .github ├── workflows │ ├── ruff.yml │ └── version-check.yml └── dependabot.yml ├── .pre-commit-config.yaml ├── scripts ├── test.sh ├── check-docker-permission.sh ├── setup-docker-registry.sh ├── generate-client.sh ├── copy-dotenv.sh ├── ensure-nats-credentials.sh └── setup-podman-socket.sh ├── docker-compose.override.yml ├── .devcontainer ├── devcontainer.json ├── orchestrator-container │ └── devcontainer.json ├── backend-app-container │ └── devcontainer.json └── operator-container │ └── devcontainer.json ├── .ruff.toml └── interactEM.code-workspace /.gitmodules: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/core/README.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/launcher/README.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/operators/README.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /operators/.gitignore: -------------------------------------------------------------------------------- 1 | .env -------------------------------------------------------------------------------- /backend/.dockerignore: -------------------------------------------------------------------------------- 1 | **/*.env* -------------------------------------------------------------------------------- /backend/orchestrator/README.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/sfapi_models/README.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/agent/.python-version: -------------------------------------------------------------------------------- 1 | 3.10 2 | -------------------------------------------------------------------------------- /backend/app/.python-version: -------------------------------------------------------------------------------- 1 | 3.10 2 | -------------------------------------------------------------------------------- /backend/app/interactem/app/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/agent/interactem/agent/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/app/interactem/app/api/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/app/interactem/app/core/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/core/interactem/core/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/launcher/.python-version: -------------------------------------------------------------------------------- 1 | 3.12 2 | -------------------------------------------------------------------------------- /backend/sfapi_models/.python-version: -------------------------------------------------------------------------------- 1 | 3.10 -------------------------------------------------------------------------------- /docker/Dockerfile.base.dockerignore: -------------------------------------------------------------------------------- 1 | **/.env* -------------------------------------------------------------------------------- /frontend/interactEM/.dockerignore: -------------------------------------------------------------------------------- 1 | **/*.env* -------------------------------------------------------------------------------- /operators/distiller-streaming/README.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- 1 | # Hi claude 2 | 3 | @AGENTS.md 4 | -------------------------------------------------------------------------------- /backend/app/interactem/app/events/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/app/interactem/app/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/app/interactem/app/tests/api/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/core/interactem/core/events/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/launcher/interactem/launcher/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docker/Dockerfile.callout.dockerignore: -------------------------------------------------------------------------------- 1 | **/.env* -------------------------------------------------------------------------------- /docker/Dockerfile.fastapi.dockerignore: -------------------------------------------------------------------------------- 1 | **/.env* -------------------------------------------------------------------------------- /docker/Dockerfile.launcher.dockerignore: -------------------------------------------------------------------------------- 1 | **/.env* -------------------------------------------------------------------------------- /docker/Dockerfile.operator.dockerignore: -------------------------------------------------------------------------------- 1 | **/.env* -------------------------------------------------------------------------------- /backend/app/interactem/app/alembic/versions/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/app/interactem/app/api/routes/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/app/interactem/app/tests/crud/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/app/interactem/app/tests/scripts/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/app/interactem/app/tests/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/operators/interactem/operators/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docker/Dockerfile.orchestrator.dockerignore: -------------------------------------------------------------------------------- 1 | **/.env* -------------------------------------------------------------------------------- /backend/app/interactem/app/tests/api/routes/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/orchestrator/interactem/orchestrator/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/operators/interactem/operators/messengers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /operators/distiller-streaming/distiller_streaming/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/operators/interactem/operators/messengers/zeromq/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /conf/nats-conf/websocket.conf: -------------------------------------------------------------------------------- 1 | websocket { 2 | port: 9222 3 | no_tls: true 4 | } -------------------------------------------------------------------------------- /frontend/interactEM/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /backend/app/interactem/app/alembic/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text eol=lf 3 | -------------------------------------------------------------------------------- /docker/docker-bake-arm64.hcl: -------------------------------------------------------------------------------- 1 | target "platform" { 2 | platforms = ["linux/arm64"] 3 | } 4 | -------------------------------------------------------------------------------- /operators/error/Containerfile: -------------------------------------------------------------------------------- 1 | FROM interactem-operator 2 | 3 | COPY ./run.py /app/run.py 4 | -------------------------------------------------------------------------------- /operators/vars-base.hcl: -------------------------------------------------------------------------------- 1 | variable "REGISTRY" { 2 | default = "ghcr.io/nersc/interactem" 3 | } -------------------------------------------------------------------------------- /cli/interactem/cli/__init__.py: -------------------------------------------------------------------------------- 1 | __path__ = __import__("pkgutil").extend_path(__path__, __name__) -------------------------------------------------------------------------------- /docker/docker-bake-amd64.hcl: -------------------------------------------------------------------------------- 1 | target "platform" { 2 | platforms = ["linux/amd64"] 3 | } 4 | 5 | -------------------------------------------------------------------------------- /operators/virtual-bfdf/Containerfile: -------------------------------------------------------------------------------- 1 | FROM distiller-streaming 2 | 3 | COPY ./run.py /app/run.py -------------------------------------------------------------------------------- /backend/app/interactem/__init__.py: -------------------------------------------------------------------------------- 1 | __path__ = __import__("pkgutil").extend_path(__path__, __name__) 2 | -------------------------------------------------------------------------------- /operators/benchmark-receiver/Containerfile: -------------------------------------------------------------------------------- 1 | FROM interactem-operator 2 | 3 | COPY ./run.py /app/run.py -------------------------------------------------------------------------------- /operators/bin-sparse-partial/Containerfile: -------------------------------------------------------------------------------- 1 | FROM distiller-streaming 2 | 3 | COPY ./run.py /app/run.py -------------------------------------------------------------------------------- /operators/distiller-grabber/Containerfile: -------------------------------------------------------------------------------- 1 | FROM distiller-streaming 2 | 3 | COPY ./run.py /app/run.py -------------------------------------------------------------------------------- /operators/distiller-streaming/.env.example: -------------------------------------------------------------------------------- 1 | NATS_CREDS_FILE=../../conf/nats-conf/out_jwt/backend.creds -------------------------------------------------------------------------------- /backend/agent/interactem/__init__.py: -------------------------------------------------------------------------------- 1 | __path__ = __import__("pkgutil").extend_path(__path__, __name__) 2 | -------------------------------------------------------------------------------- /backend/core/interactem/__init__.py: -------------------------------------------------------------------------------- 1 | __path__ = __import__("pkgutil").extend_path(__path__, __name__) 2 | -------------------------------------------------------------------------------- /backend/launcher/interactem/__init__.py: -------------------------------------------------------------------------------- 1 | __path__ = __import__("pkgutil").extend_path(__path__, __name__) 2 | -------------------------------------------------------------------------------- /backend/metrics/interactem/__init__.py: -------------------------------------------------------------------------------- 1 | __path__ = __import__("pkgutil").extend_path(__path__, __name__) 2 | -------------------------------------------------------------------------------- /operators/center-of-mass-partial/Containerfile: -------------------------------------------------------------------------------- 1 | FROM distiller-streaming 2 | 3 | COPY ./run.py /app/run.py -------------------------------------------------------------------------------- /operators/center-of-mass-reduce/Containerfile: -------------------------------------------------------------------------------- 1 | FROM distiller-streaming 2 | 3 | COPY ./run.py /app/run.py -------------------------------------------------------------------------------- /operators/detstream-aggregator/Containerfile: -------------------------------------------------------------------------------- 1 | FROM interactem-operator 2 | 3 | RUN poetry add msgpack pyzmq -------------------------------------------------------------------------------- /operators/detstream-assembler/Containerfile: -------------------------------------------------------------------------------- 1 | FROM interactem-operator 2 | 3 | RUN poetry add msgpack pyzmq -------------------------------------------------------------------------------- /operators/distiller-state-client/Containerfile: -------------------------------------------------------------------------------- 1 | FROM distiller-streaming 2 | 3 | COPY ./run.py /app/run.py -------------------------------------------------------------------------------- /operators/pva-converter/.env.example: -------------------------------------------------------------------------------- 1 | inputChannel="pvapy:image" 2 | EPICS_PVA_NAME_SERVERS="127.0.0.1:11111" -------------------------------------------------------------------------------- /backend/callout/test/.env.example: -------------------------------------------------------------------------------- 1 | DISTILLER_TOKEN=THE_DISTILLER_TOKEN 2 | NKEYS_SEED_FILE=/path/to/app_user.nk -------------------------------------------------------------------------------- /backend/metrics/interactem/metrics/__init__.py: -------------------------------------------------------------------------------- 1 | __path__ = __import__("pkgutil").extend_path(__path__, __name__) -------------------------------------------------------------------------------- /backend/operators/interactem/__init__.py: -------------------------------------------------------------------------------- 1 | __path__ = __import__("pkgutil").extend_path(__path__, __name__) 2 | -------------------------------------------------------------------------------- /backend/orchestrator/interactem/__init__.py: -------------------------------------------------------------------------------- 1 | __path__ = __import__("pkgutil").extend_path(__path__, __name__) 2 | -------------------------------------------------------------------------------- /backend/sfapi_models/interactem/__init__.py: -------------------------------------------------------------------------------- 1 | __path__ = __import__("pkgutil").extend_path(__path__, __name__) 2 | -------------------------------------------------------------------------------- /operators/vars-local.hcl: -------------------------------------------------------------------------------- 1 | variable "REGISTRY" { 2 | default = "localhost:5001/ghcr.io/nersc/interactem" 3 | } -------------------------------------------------------------------------------- /operators/diffraction-pattern-accumulator/Containerfile: -------------------------------------------------------------------------------- 1 | FROM distiller-streaming 2 | 3 | COPY ./run.py /app/run.py -------------------------------------------------------------------------------- /backend/app/scripts/format.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | set -x 3 | 4 | ruff check app scripts --fix 5 | ruff format app scripts 6 | -------------------------------------------------------------------------------- /backend/app/.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | app.egg-info 3 | *.pyc 4 | .mypy_cache 5 | .coverage 6 | htmlcov 7 | .cache 8 | .venv 9 | -------------------------------------------------------------------------------- /backend/orchestrator/.env.example: -------------------------------------------------------------------------------- 1 | NATS_CREDS_FILE=../../conf/nats-conf/out_jwt/backend.creds 2 | ORCHESTRATOR_API_KEY=changethis -------------------------------------------------------------------------------- /operators/dpc/Containerfile: -------------------------------------------------------------------------------- 1 | FROM distiller-streaming 2 | 3 | RUN poetry add matplotlib cffi 4 | 5 | COPY ./run.py /app/run.py -------------------------------------------------------------------------------- /backend/app/.dockerignore: -------------------------------------------------------------------------------- 1 | # Python 2 | __pycache__ 3 | app.egg-info 4 | *.pyc 5 | .mypy_cache 6 | .coverage 7 | htmlcov 8 | .venv 9 | -------------------------------------------------------------------------------- /docker/Dockerfile.frontend.dockerignore: -------------------------------------------------------------------------------- 1 | **/.env* 2 | .log* 3 | bundle-visualization.html 4 | dist/ 5 | dist_app/ 6 | node_modules/ -------------------------------------------------------------------------------- /frontend/interactEM/.env.development: -------------------------------------------------------------------------------- 1 | VITE_REACT_APP_API_BASE_URL=http://localhost:8080/ 2 | VITE_NATS_SERVER_URL="ws://localhost:9222" -------------------------------------------------------------------------------- /operators/random-table/Containerfile: -------------------------------------------------------------------------------- 1 | FROM interactem-operator 2 | 3 | RUN poetry add pandas 4 | 5 | COPY ./run.py /app/run.py 6 | -------------------------------------------------------------------------------- /backend/launcher/interactem/launcher/constants.py: -------------------------------------------------------------------------------- 1 | HEADER_TEMPLATE = "header.sh.j2" 2 | LAUNCH_AGENT_TEMPLATE = "launch_agent.sh.j2" 3 | -------------------------------------------------------------------------------- /operators/benchmark-sender/Containerfile: -------------------------------------------------------------------------------- 1 | FROM interactem-operator 2 | 3 | RUN poetry add numpy~=2.2.6 4 | 5 | COPY ./run.py /app/run.py -------------------------------------------------------------------------------- /operators/detstream-producer/Containerfile: -------------------------------------------------------------------------------- 1 | FROM samwelborn/detstream-worker:852f169 2 | 3 | ENTRYPOINT [ "/usr/local/bin/producer_sim" ] -------------------------------------------------------------------------------- /backend/app/scripts/lint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | set -x 5 | 6 | mypy app 7 | ruff check app 8 | ruff format app --check 9 | -------------------------------------------------------------------------------- /backend/launcher/scripts/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | THIS_DIR=$(dirname $0) 4 | dotenv -f $THIS_DIR/../tests/.env run pytest -s -vv ./tests -------------------------------------------------------------------------------- /operators/center-of-mass-plot/Containerfile: -------------------------------------------------------------------------------- 1 | FROM distiller-streaming 2 | 3 | RUN poetry add matplotlib cffi 4 | 5 | COPY ./run.py /app/run.py -------------------------------------------------------------------------------- /operators/detstream-state-server/Containerfile: -------------------------------------------------------------------------------- 1 | FROM samwelborn/detstream-worker:852f169 2 | 3 | ENTRYPOINT [ "/usr/local/bin/state_server" ] -------------------------------------------------------------------------------- /operators/electron-count-save/Containerfile: -------------------------------------------------------------------------------- 1 | FROM interactem-operator 2 | 3 | RUN poetry add stempy h5py 4 | 5 | COPY ./run.py /app/run.py 6 | -------------------------------------------------------------------------------- /operators/image-display/Containerfile: -------------------------------------------------------------------------------- 1 | FROM interactem-operator 2 | 3 | COPY ./run.py /app/run.py 4 | 5 | ENTRYPOINT [ "python", "/app/run.py" ] -------------------------------------------------------------------------------- /operators/pva-converter/Containerfile: -------------------------------------------------------------------------------- 1 | FROM interactem-operator 2 | 3 | RUN poetry add pvapy 4 | 5 | EXPOSE 11111 6 | 7 | COPY ./run.py /app/run.py -------------------------------------------------------------------------------- /operators/random-image/Containerfile: -------------------------------------------------------------------------------- 1 | FROM interactem-operator 2 | 3 | RUN poetry add numpy~=2.2.6 pillow 4 | 5 | COPY ./run.py /app/run.py 6 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | # Sphinx build outputs 2 | _build/ 3 | 4 | # Python 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | *.so 9 | .Python 10 | -------------------------------------------------------------------------------- /operators/data-replay/Containerfile: -------------------------------------------------------------------------------- 1 | FROM distiller-streaming 2 | 3 | COPY ./run.py /app/run.py 4 | 5 | RUN mkdir -p /output 6 | RUN mkdir -p /raw_data -------------------------------------------------------------------------------- /operators/distiller-counted-data-reader/Containerfile: -------------------------------------------------------------------------------- 1 | FROM distiller-streaming 2 | 3 | ENV HDF5_USE_FILE_LOCKING=FALSE 4 | 5 | COPY ./run.py /app/run.py -------------------------------------------------------------------------------- /operators/sparse-frame-image-converter/Containerfile: -------------------------------------------------------------------------------- 1 | FROM distiller-streaming 2 | 3 | RUN poetry add matplotlib 4 | 5 | COPY ./run.py /app/run.py 6 | -------------------------------------------------------------------------------- /backend/app/tests-start.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | set -e 3 | set -x 4 | 5 | python /app/app/tests_pre_start.py 6 | 7 | bash ./scripts/test.sh "$@" 8 | -------------------------------------------------------------------------------- /backend/metrics/run.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from interactem.metrics.metrics import main 4 | 5 | if __name__ == "__main__": 6 | asyncio.run(main()) 7 | -------------------------------------------------------------------------------- /backend/orchestrator/interactem/orchestrator/types.py: -------------------------------------------------------------------------------- 1 | from interactem.core.models.base import IdType 2 | 3 | DeploymentID = IdType 4 | AgentID = IdType 5 | -------------------------------------------------------------------------------- /backend/launcher/run.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from interactem.launcher.launcher import main 4 | 5 | if __name__ == "__main__": 6 | asyncio.run(main()) 7 | -------------------------------------------------------------------------------- /operators/array-image-converter/Containerfile: -------------------------------------------------------------------------------- 1 | FROM interactem-operator 2 | 3 | RUN poetry add numpy@~2.2.6 pillow matplotlib 4 | 5 | COPY ./run.py /app/run.py 6 | -------------------------------------------------------------------------------- /frontend/interactEM/src/client/generated/index.ts: -------------------------------------------------------------------------------- 1 | // This file is auto-generated by @hey-api/openapi-ts 2 | export * from "./types.gen" 3 | export * from "./sdk.gen" 4 | -------------------------------------------------------------------------------- /operators/table-display/Containerfile: -------------------------------------------------------------------------------- 1 | FROM interactem-operator 2 | 3 | RUN poetry add pandas 4 | 5 | COPY ./run.py /app/run.py 6 | 7 | ENTRYPOINT [ "python", "/app/run.py" ] -------------------------------------------------------------------------------- /operators/vars-prod.hcl: -------------------------------------------------------------------------------- 1 | variable "REGISTRY" { 2 | default = "ghcr.io/nersc/interactem" 3 | } 4 | 5 | target "common" { 6 | platforms = ["linux/amd64", "linux/arm64"] 7 | } 8 | -------------------------------------------------------------------------------- /backend/launcher/tests/.env.example: -------------------------------------------------------------------------------- 1 | NATS_SERVER_URL="nats://localhost:4222" 2 | SFAPI_KEY_PATH="~/.superfacility/key.pem" 3 | CONDA_ENV="interactem" 4 | ENV_FILE_PATH="/path/to/.env/file" -------------------------------------------------------------------------------- /frontend/interactEM/src/index.ts: -------------------------------------------------------------------------------- 1 | export { interactemQueryClient, loginInteractem } from "./auth/api" 2 | 3 | import InteractEM from "./pages/interactem" 4 | 5 | export { InteractEM } 6 | -------------------------------------------------------------------------------- /operators/.env.example: -------------------------------------------------------------------------------- 1 | PODMAN_SERVICE_URI=unix:/// 2 | BAKE_FILE=./docker-bake.hcl 3 | VERBOSE=false 4 | REGISTRY=host.containers.internal:5001/ghcr.io/nersc/interactem 5 | -------------------------------------------------------------------------------- /cli/.env.example: -------------------------------------------------------------------------------- 1 | INTERACTEM_USERNAME=admin@example.com 2 | INTERACTEM_PASSWORD=changethis 3 | API_BASE_URL=http://localhost:8080/api/v1 4 | NATS_CREDS_FILE=../conf/nats-conf/out_jwt/backend.creds -------------------------------------------------------------------------------- /frontend/interactEM/src/constants/tanstack.ts: -------------------------------------------------------------------------------- 1 | export const AUTH_QUERY_KEYS = { 2 | externalToken: ["token", "external"] as const, 3 | internalToken: ["token", "internal"] as const, 4 | } 5 | -------------------------------------------------------------------------------- /backend/app/scripts/test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | set -x 5 | 6 | coverage run --source=app -m pytest 7 | coverage report --show-missing 8 | coverage html --title "${@-coverage}" 9 | -------------------------------------------------------------------------------- /operators/detstream-state-server/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "server": { 3 | "connect": { 4 | "state_hostname": "192.168.127.2", 5 | "state_port": 15000 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /operators/vars-ci.hcl: -------------------------------------------------------------------------------- 1 | target "output" { 2 | output = [ 3 | { 4 | type="image", 5 | push-by-digest=true, 6 | name-canonical=true, 7 | push=true 8 | } 9 | ] 10 | } -------------------------------------------------------------------------------- /operators/pvapy-ad-sim-server/Containerfile: -------------------------------------------------------------------------------- 1 | FROM interactem-operator 2 | 3 | RUN poetry add pvapy 4 | 5 | ENV EPICS_PVA_SERVER_PORT=11111 6 | 7 | COPY ./run.sh /app/run.sh 8 | 9 | ENTRYPOINT [ "/app/run.sh" ] -------------------------------------------------------------------------------- /backend/metrics/monitoring-conf/prometheus.yml: -------------------------------------------------------------------------------- 1 | scrape_configs: 2 | - job_name: 'interactem-metrics' 3 | static_configs: 4 | - targets: ['metrics:8001'] 5 | scrape_interval: 2s 6 | metrics_path: /metrics -------------------------------------------------------------------------------- /operators/error/operator.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "93345668-1234-1234-1234-1234567890ab", 3 | "image": "ghcr.io/nersc/interactem/error:latest", 4 | "label": "Error test", 5 | "description": "Generates an error" 6 | } 7 | -------------------------------------------------------------------------------- /operators/read-tem-data/Containerfile: -------------------------------------------------------------------------------- 1 | FROM interactem-operator 2 | 3 | # Install additional dependencies 4 | RUN poetry add ncempy numpy@~2.2.6 h5py 5 | 6 | # Copy the operator script 7 | COPY ./run.py /app/run.py 8 | -------------------------------------------------------------------------------- /frontend/interactEM/src/App.tsx: -------------------------------------------------------------------------------- 1 | import InteractEM from "./pages/interactem" 2 | 3 | export default function App() { 4 | return ( 5 | <> 6 | 7 | 8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /operators/quantem-direct-ptycho/Containerfile: -------------------------------------------------------------------------------- 1 | FROM distiller-streaming 2 | 3 | # Install additional dependencies 4 | RUN pip install --no-cache-dir quantem 5 | 6 | # Copy the operator script 7 | COPY ./run.py /app/run.py 8 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "ms-python.python", 4 | "ms-python.black-formatter", 5 | "ms-python.isort", 6 | "charliermarsh.ruff", 7 | "ms-python.debugpy" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. include:: introduction.md 2 | :parser: myst_parser.sphinx_ 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | :hidden: 7 | 8 | getting-started 9 | launch-agent 10 | authoring-operators 11 | -------------------------------------------------------------------------------- /backend/rdma/libs/thallium/src/eng_registry.cpp: -------------------------------------------------------------------------------- 1 | #include "eng_registry.hpp" 2 | 3 | namespace interactEM { 4 | 5 | // Definition of the static member variable 6 | std::unordered_map EngRegistry::cxi_addresses_; 7 | 8 | } -------------------------------------------------------------------------------- /operators/pvapy-ad-sim-server/operator.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "12345678-1234-1234-1234-1432563830cd", 3 | "image": "ghcr.io/nersc/interactem/pvapy-ad-sim-server", 4 | "label": "PVA Area Detector Server", 5 | "description": "Generates PVAs" 6 | } 7 | -------------------------------------------------------------------------------- /backend/metrics/monitoring-conf/grafana/provisioning/dashboards/dashboard.yml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | 3 | providers: 4 | - name: 'interactem-dashboards' 5 | folder: 'InteractEM' 6 | type: file 7 | options: 8 | path: /var/lib/grafana/dashboards -------------------------------------------------------------------------------- /cli/README.md: -------------------------------------------------------------------------------- 1 | # InteractEM - CLI 2 | 3 | ## Installation 4 | 5 | In your Python environment, from inside the cli folder (where pyproject.toml is located), install the CLI package: 6 | 7 | ```bash 8 | pip install . 9 | interactem --help 10 | ``` 11 | -------------------------------------------------------------------------------- /frontend/interactEM/src/types/pipeline.ts: -------------------------------------------------------------------------------- 1 | import type { CanonicalOperator, CanonicalPipelineData } from "../client" 2 | 3 | export type OperatorNodeData = Omit 4 | export interface PipelineJSON { 5 | data: CanonicalPipelineData 6 | } 7 | -------------------------------------------------------------------------------- /cli/interactem/cli/templates/Containerfile.j2: -------------------------------------------------------------------------------- 1 | FROM {{ base_image }} 2 | {% if additional_packages %} 3 | # Install additional dependencies 4 | RUN poetry add {{ additional_packages | join(' ') }} 5 | {% endif %} 6 | # Copy the operator script 7 | COPY ./run.py /app/run.py -------------------------------------------------------------------------------- /backend/launcher/.env.example: -------------------------------------------------------------------------------- 1 | NATS_SERVER_URL="nats://localhost:4222" 2 | SFAPI_KEY_PATH="~/.superfacility/key.pem" 3 | AGENT_PROJECT_DIR="/path/to/interactEM/backend/agent" 4 | ENV_FILE_PATH="/path/to/.env/file" 5 | SFAPI_ACCOUNT="PUT_THE_ACCOUNT_HERE" 6 | SFAPI_QOS="debug" -------------------------------------------------------------------------------- /docker/Dockerfile.frontend: -------------------------------------------------------------------------------- 1 | FROM node:23 AS build 2 | 3 | WORKDIR /app 4 | COPY ./interactEM/package*.json ./ 5 | RUN npm install 6 | COPY ./interactEM/ . 7 | RUN npm run build:app 8 | 9 | FROM nginx:alpine 10 | COPY --from=build /app/dist_app /usr/share/nginx/html -------------------------------------------------------------------------------- /operators/detstream-state-server/operator.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "12345678-0000-0001-0000-1234567890ab", 3 | "image": "ghcr.io/nersc/interactem/detstream-state-server:latest", 4 | "label": "State Server", 5 | "description": "State Server for the Detstream pipeline" 6 | } 7 | -------------------------------------------------------------------------------- /cli/interactem/cli/main.py: -------------------------------------------------------------------------------- 1 | import typer 2 | 3 | from interactem.cli.operators import operator_app 4 | from interactem.cli.pipeline import pipeline_app 5 | 6 | app = typer.Typer() 7 | app.add_typer(pipeline_app, name="pipeline") 8 | app.add_typer(operator_app, name="operator") 9 | -------------------------------------------------------------------------------- /backend/core/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "editor.formatOnSaveMode": "modifications", 4 | "editor.defaultFormatter": "charliermarsh.ruff", 5 | "editor.codeActionsOnSave": { 6 | "source.organizeImports": "explicit" 7 | }, 8 | } -------------------------------------------------------------------------------- /backend/orchestrator/interactem/orchestrator/constants.py: -------------------------------------------------------------------------------- 1 | ERROR_PUBLISHER_CTX_NAME = "error_pub" 2 | INFO_PUBLISHER_CTX_NAME = "info_pub" 3 | DEPLOYMENT_ID_CTX_NAME = "deployment_id" 4 | ORCHESTRATOR_STATE_CTX_NAME = "state" 5 | JS_CTX_NAME = "broker.config.connection_state.stream" 6 | -------------------------------------------------------------------------------- /backend/launcher/interactem/launcher/templates/launch_agent.sh.j2: -------------------------------------------------------------------------------- 1 | {% include "header.sh.j2" %} 2 | 3 | export HDF5_USE_FILE_LOCKING=FALSE 4 | cd {{settings.ENV_FILE_DIR}} 5 | srun --nodes={{job.num_nodes}} --ntasks-per-node=1 uv run --project {{settings.AGENT_PROJECT_DIR}} interactem-agent -------------------------------------------------------------------------------- /operators/beam-compensation/Containerfile: -------------------------------------------------------------------------------- 1 | FROM distiller-streaming 2 | 3 | # had to add cffi because ncempy messes with that package when adding, and nkeys acts up... 4 | RUN poetry add ncempy cffi 5 | 6 | COPY ./run.py /app/run.py 7 | 8 | RUN mkdir -p /output 9 | RUN mkdir -p /vacuum_scan -------------------------------------------------------------------------------- /backend/app/prestart.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | # Let the DB start 4 | python /interactem/app/interactem/app/backend_pre_start.py 5 | 6 | # Run migrations 7 | alembic upgrade head 8 | 9 | # Create initial data in DB 10 | python /interactem/app/interactem/app/initial_data.py 11 | -------------------------------------------------------------------------------- /frontend/interactEM/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /backend/rdma/libs/thallium/src/eng_dispatcher.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include "eng_dispatcher.hpp" 3 | 4 | namespace interactEM { 5 | 6 | EngDispatcher::~EngDispatcher() { 7 | this->stop(); 8 | } 9 | 10 | void EngDispatcher::stop() { 11 | getEngine().pop_finalize_callback(this); 12 | } 13 | 14 | } -------------------------------------------------------------------------------- /docker/docker-bake-ci.hcl: -------------------------------------------------------------------------------- 1 | // For CI push-by-digest builds, override generate_tags() to use minimal tags 2 | // Full tags are applied later via docker buildx imagetools in merge-manifests 3 | 4 | function "generate_tags" { 5 | params = [service_name] 6 | result = ["${REGISTRY}/${service_name}"] 7 | } 8 | -------------------------------------------------------------------------------- /backend/metrics/monitoring-conf/grafana/provisioning/datasources/datasource.yml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | 3 | datasources: 4 | - name: Prometheus 5 | type: prometheus 6 | access: proxy 7 | url: http://prometheus:9090 8 | isDefault: true 9 | httpMethod: GET 10 | jsonData: 11 | timeInterval: 2s -------------------------------------------------------------------------------- /frontend/interactEM/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import ReactDOM from "react-dom/client" 3 | 4 | import App from "./App" 5 | 6 | import "./index.css" 7 | 8 | ReactDOM.createRoot(document.getElementById("root")!).render( 9 | 10 | 11 | , 12 | ) 13 | -------------------------------------------------------------------------------- /frontend/interactEM/tests/e2e/smoke.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "@playwright/test" 2 | import { test } from "./fixtures/auth" 3 | 4 | test("loads composer page", async ({ authPage }) => { 5 | await authPage.waitForSelector(".composer-page", { timeout: 20_000 }) 6 | await expect(authPage.locator(".composer-page")).toBeVisible() 7 | }) 8 | -------------------------------------------------------------------------------- /backend/rdma/src/prx_transporter.cpp: -------------------------------------------------------------------------------- 1 | #include "prx_transporter.hpp" 2 | 3 | namespace interactEM { 4 | 5 | void ProxyTransporter::stop() { 6 | eng_registry_->stop(); 7 | eng_provider_->stop(); 8 | // eng_dispatcher_.stop(); 9 | } 10 | 11 | ProxyTransporter::~ProxyTransporter() { 12 | this->stop(); 13 | } 14 | 15 | } 16 | 17 | -------------------------------------------------------------------------------- /backend/rdma/common/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Common utilities library (header-only for now) 2 | add_library(common_utils INTERFACE) 3 | 4 | # Include directories for common utilities 5 | target_include_directories(common_utils INTERFACE 6 | ${CMAKE_CURRENT_SOURCE_DIR}/include 7 | ) 8 | 9 | target_compile_features(common_utils INTERFACE cxx_std_17) 10 | -------------------------------------------------------------------------------- /conf/nats-conf/nats1.conf: -------------------------------------------------------------------------------- 1 | server_name=n1-c1 2 | listen=4222 3 | 4 | include ./auth.conf 5 | 6 | jetstream { 7 | store_dir=/nats/storage 8 | } 9 | 10 | cluster { 11 | name: C1 12 | listen: 0.0.0.0:6222 13 | routes: [ 14 | nats://nats2:6222 15 | nats://nats3:6222 16 | ] 17 | } 18 | 19 | include ./websocket.conf 20 | 21 | http_port: 8222 -------------------------------------------------------------------------------- /conf/nats-conf/nats2.conf: -------------------------------------------------------------------------------- 1 | server_name=n2-c1 2 | listen=4222 3 | 4 | include ./auth.conf 5 | 6 | jetstream { 7 | store_dir=/nats/storage 8 | } 9 | 10 | cluster { 11 | name: C1 12 | listen: 0.0.0.0:6222 13 | routes: [ 14 | nats://nats1:6222 15 | nats://nats3:6222 16 | ] 17 | } 18 | 19 | include ./websocket.conf 20 | 21 | http_port: 8222 -------------------------------------------------------------------------------- /conf/nats-conf/nats3.conf: -------------------------------------------------------------------------------- 1 | server_name=n3-c1 2 | listen=4222 3 | 4 | include ./auth.conf 5 | 6 | jetstream { 7 | store_dir=/nats/storage 8 | } 9 | 10 | cluster { 11 | name: C1 12 | listen: 0.0.0.0:6222 13 | routes: [ 14 | nats://nats1:6222 15 | nats://nats2:6222 16 | ] 17 | } 18 | 19 | include ./websocket.conf 20 | 21 | http_port: 8222 -------------------------------------------------------------------------------- /docker/Dockerfile.metrics: -------------------------------------------------------------------------------- 1 | FROM interactem-base 2 | 3 | WORKDIR /interactem/metrics/ 4 | 5 | COPY ./metrics/README.md ./metrics/pyproject.toml ./metrics/poetry.lock* ./ 6 | 7 | RUN poetry install --no-root 8 | 9 | COPY ./metrics/interactem/ ./interactem/ 10 | RUN poetry install 11 | 12 | COPY ./metrics/run.py ./run.py 13 | 14 | CMD ["python", "run.py"] -------------------------------------------------------------------------------- /operators/pvapy-ad-sim-server/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | DET_SIZE_X=2560 4 | DET_SIZE_Y=2160 5 | FRAMES_PER_SECOND=1 6 | DATA_TYPE=uint8 7 | PV_NAME=pvapy:image 8 | REPORT_TIME=30000 9 | CACHE_SIZE=100 10 | 11 | pvapy-ad-sim-server -cn $PV_NAME -nx $DET_SIZE_X -ny $DET_SIZE_Y -dt $DATA_TYPE --disable-curses -rt $REPORT_TIME -fps $FRAMES_PER_SECOND -cs $CACHE_SIZE -------------------------------------------------------------------------------- /docker/Dockerfile.orchestrator: -------------------------------------------------------------------------------- 1 | FROM interactem-base 2 | 3 | WORKDIR /interactem/orchestrator/ 4 | COPY ./orchestrator/README.md ./orchestrator/pyproject.toml ./orchestrator/poetry.lock* ./ 5 | RUN poetry install --no-root --without dev 6 | 7 | COPY ./orchestrator/interactem/ ./interactem/ 8 | RUN poetry install --only-root 9 | 10 | COPY ./orchestrator/run.py ./ 11 | 12 | CMD ["python", "run.py"] -------------------------------------------------------------------------------- /backend/launcher/interactem/launcher/templates/header.sh.j2: -------------------------------------------------------------------------------- 1 | {% include "ascii_header.sh.j2" %} 2 | 3 | #SBATCH --qos={{job.qos}} 4 | #SBATCH --constraint={{job.constraint}} 5 | #SBATCH --time={{job.walltime}} 6 | #SBATCH --account={{job.account}} 7 | #SBATCH --nodes={{job.num_nodes}} 8 | #SBATCH --exclusive 9 | {%- if job.reservation %} 10 | #SBATCH --reservation={{job.reservation}} 11 | {%- endif %} -------------------------------------------------------------------------------- /backend/orchestrator/run.py: -------------------------------------------------------------------------------- 1 | import anyio 2 | 3 | from interactem.core.logger import get_logger 4 | from interactem.orchestrator.app import app 5 | 6 | logger = get_logger() 7 | 8 | 9 | if __name__ == "__main__": 10 | try: 11 | anyio.run(app.run) 12 | except KeyboardInterrupt: 13 | logger.info("Orchestrator stopped by user") 14 | logger.info("Orchestrator process exited") 15 | -------------------------------------------------------------------------------- /operators/image-display/operator.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "12345678-0000-0002-0000-1234567890ac", 3 | "image": "ghcr.io/nersc/interactem/image-display:latest", 4 | "label": "Image", 5 | "description": "Display an image", 6 | "inputs": [ 7 | { 8 | "name": "in", 9 | "label": "The input", 10 | "type": "image", 11 | "description": "This image to display" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /operators/login_ghcr.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export GH_USERNAME= 4 | export CR_PAT=$(cat ~/.ghcr_pat) # PAT with read:packages and write:packages scopes 5 | DOCKER=podman # or docker 6 | 7 | 8 | if [ -z "$CR_PAT" ]; then 9 | echo "Error: CR_PAT is empty. Please check your GitHub PAT." 10 | exit 1 11 | fi 12 | 13 | echo $CR_PAT | $DOCKER login ghcr.io -u $GH_USERNAME --password-stdin -------------------------------------------------------------------------------- /operators/table-display/operator.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "4acb255b-b4bc-4d56-b4ae-8014a4bd1b67", 3 | "image": "ghcr.io/nersc/interactem/table-display:latest", 4 | "label": "Table", 5 | "description": "Display a table", 6 | "inputs": [ 7 | { 8 | "name": "in", 9 | "label": "The input", 10 | "type": "table", 11 | "description": "This table to display" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /backend/metrics/interactem/metrics/config.py: -------------------------------------------------------------------------------- 1 | from pydantic import AnyWebsocketUrl, NatsDsn 2 | from pydantic_settings import BaseSettings, SettingsConfigDict 3 | 4 | 5 | class Settings(BaseSettings): 6 | model_config = SettingsConfigDict(env_file=None) 7 | NATS_SERVER_URL: AnyWebsocketUrl | NatsDsn = NatsDsn("nats://localhost:4222") 8 | PROMETHEUS_PORT: int = 8001 9 | 10 | 11 | cfg = Settings() 12 | -------------------------------------------------------------------------------- /frontend/interactEM/openapi-ts.config.ts: -------------------------------------------------------------------------------- 1 | import { defaultPlugins, defineConfig } from "@hey-api/openapi-ts" 2 | 3 | export default defineConfig({ 4 | input: "openapi.json", 5 | output: { 6 | format: "biome", 7 | path: "src/client/generated", 8 | }, 9 | plugins: [ 10 | ...defaultPlugins, 11 | "@hey-api/client-axios", 12 | "@tanstack/react-query", 13 | "zod", 14 | ], 15 | }) 16 | -------------------------------------------------------------------------------- /.github/workflows/ruff.yml: -------------------------------------------------------------------------------- 1 | name: Ruff 2 | on: [push, pull_request] 3 | 4 | concurrency: 5 | group: ${{ github.workflow }}-${{ github.ref }} 6 | cancel-in-progress: true 7 | 8 | permissions: 9 | contents: read 10 | jobs: 11 | ruff: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: astral-sh/ruff-action@v3 16 | with: 17 | args: "check ." 18 | -------------------------------------------------------------------------------- /frontend/interactEM/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | interactEM 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /frontend/interactEM/src/components/pipelines/hud.tsx: -------------------------------------------------------------------------------- 1 | import type React from "react" 2 | import { ViewMode, useViewModeStore } from "../../stores" 3 | import { HudComposer } from "./hudcomposer" 4 | import { HudRunning } from "./hudrunning" 5 | 6 | export const PipelineHud: React.FC = () => { 7 | const { viewMode } = useViewModeStore() 8 | 9 | return viewMode === ViewMode.Composer ? : 10 | } 11 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Frontend npm dependencies 4 | - package-ecosystem: "npm" 5 | directory: "/frontend/interactEM" 6 | schedule: 7 | interval: "weekly" 8 | day: "monday" 9 | time: "03:00" 10 | open-pull-requests-limit: 10 11 | reviewers: 12 | - "swelborn" 13 | commit-message: 14 | prefix: "chore(deps)" 15 | prefix-development: "chore(deps-dev)" 16 | -------------------------------------------------------------------------------- /docker/Dockerfile.launcher: -------------------------------------------------------------------------------- 1 | FROM interactem-base 2 | 3 | COPY ./sfapi_models/ /interactem/sfapi_models/ 4 | 5 | WORKDIR /interactem/launcher/ 6 | COPY ./launcher/README.md ./launcher/pyproject.toml ./launcher/poetry.lock* ./ 7 | RUN poetry install --no-root --without dev 8 | 9 | COPY ./launcher/interactem/ ./interactem/ 10 | RUN poetry install --only-root 11 | 12 | COPY ./launcher/run.py ./run.py 13 | 14 | CMD ["python", "./run.py"] -------------------------------------------------------------------------------- /operators/detstream-aggregator/operator.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "12345678-1234-1234-1234-1234567890ab", 3 | "image": "ghcr.io/nersc/interactem/detstream-aggregator", 4 | "label": "Aggregator", 5 | "description": "Aggregator for the Detstream pipeline", 6 | "outputs": [ 7 | { 8 | "name": "out", 9 | "label": "The output", 10 | "type": "partial frame", 11 | "description": "Partial frame" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | repos: 4 | - repo: local 5 | hooks: 6 | - id: sync-readmes 7 | name: sync-readmes 8 | entry: uv run --project docs/ docs/scripts/sync_readmes.py 9 | language: system 10 | pass_filenames: false 11 | files: ^(docs/|README.md|operators/README\.md|backend/agent/README\.md) 12 | -------------------------------------------------------------------------------- /backend/operators/interactem/operators/config.py: -------------------------------------------------------------------------------- 1 | from pydantic import AnyWebsocketUrl, NatsDsn 2 | from pydantic_settings import BaseSettings, SettingsConfigDict 3 | 4 | 5 | class Settings(BaseSettings): 6 | model_config = SettingsConfigDict(env_file=None, extra="ignore") 7 | NATS_SERVER_URL: AnyWebsocketUrl | NatsDsn = NatsDsn("nats://localhost:4222") 8 | ZMQ_BIND_HOSTNAME: str 9 | ZMQ_BIND_INTERFACE: str 10 | 11 | cfg = Settings() 12 | -------------------------------------------------------------------------------- /backend/agent/interactem/agent/entrypoint.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from interactem.agent.broker import app, logger 4 | 5 | 6 | async def main(): 7 | try: 8 | await app.run() 9 | except KeyboardInterrupt: 10 | pass 11 | except Exception as e: 12 | logger.exception(f"Unexpected error: {e}") 13 | finally: 14 | logger.info("Application terminated.") 15 | 16 | 17 | def entrypoint(): 18 | asyncio.run(main()) 19 | -------------------------------------------------------------------------------- /backend/callout/service/.env.example: -------------------------------------------------------------------------------- 1 | CALLOUT_ACCOUNT_NKEY_FILE=/nats-conf/CALLOUT.nk 2 | CALLOUT_ACCOUNT_SIGNING_KEY_FILE=/nats-conf/CALLOUT_sk.nk 3 | CALLOUT_ACCOUNT_XKEY_FILE=/nats-conf/CALLOUT_xkey.nk 4 | CALLOUT_USER_CREDS_FILE=/nats-conf/callout.creds 5 | APP_ACCOUNT_NKEY_FILE=/nats-conf/APP.nk 6 | APP_ACCOUNT_SIGNING_KEY_FILE=/nats-conf/APP_sk.nk 7 | SERVER_URL=nats://nats1:4222 8 | TOKEN_EXPIRATION_TIME_S=600 9 | JWT_SECRET_KEYS=changethis 10 | JWT_ALGORITHM=HS256 -------------------------------------------------------------------------------- /frontend/interactEM/vite.config.app.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from "path" 2 | import react from "@vitejs/plugin-react" 3 | import { defineConfig } from "vite" 4 | 5 | const __dirname = resolve() 6 | 7 | export default defineConfig({ 8 | plugins: [react()], 9 | build: { 10 | outDir: "dist_app", 11 | assetsDir: "assets", 12 | rollupOptions: { 13 | input: { 14 | main: resolve(__dirname, "index.html"), 15 | }, 16 | }, 17 | }, 18 | }) 19 | -------------------------------------------------------------------------------- /operators/benchmark-receiver/operator.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "648d6b2f-badc-42a6-b60b-cc1d195fc5af", 3 | "image": "ghcr.io/nersc/interactem/benchmark-receiver:latest", 4 | "label": "Benchmark Receiver", 5 | "description": "Receive random frames for benchmarking", 6 | "inputs": [ 7 | { 8 | "name": "in", 9 | "label": "The input", 10 | "type": "partial frame", 11 | "description": "Partial frame" 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /backend/orchestrator/interactem/orchestrator/config.py: -------------------------------------------------------------------------------- 1 | from pydantic import AnyWebsocketUrl, NatsDsn 2 | from pydantic_settings import BaseSettings, SettingsConfigDict 3 | 4 | 5 | class Settings(BaseSettings): 6 | model_config = SettingsConfigDict(env_file=None) 7 | NATS_SERVER_URL: AnyWebsocketUrl | NatsDsn = NatsDsn("nats://localhost:4222") 8 | ORCHESTRATOR_API_KEY: str = "changeme" 9 | NUM_PARALLEL_OPERATORS: int = 4 10 | 11 | 12 | cfg = Settings() 13 | -------------------------------------------------------------------------------- /frontend/interactEM/src/contexts/nats/allstatus.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from "react" 2 | import { AgentStatusProvider } from "./agentstatus" 3 | import { OperatorStatusProvider } from "./operatorstatus" 4 | 5 | export const StatusProvider: React.FC<{ children: ReactNode }> = ({ 6 | children, 7 | }) => { 8 | return ( 9 | 10 | {children} 11 | 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /backend/rdma/libs/thallium/src/eng_utils.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include "eng_utils.hpp" 3 | 4 | namespace interactEM { 5 | 6 | // Specialized version for std::vector 7 | std::vector> EngUtils::create_segments(const std::vector& data) { 8 | std::vector> segments(1); 9 | segments[0].first = const_cast(static_cast(data.data())); 10 | segments[0].second = data.size(); 11 | return segments; 12 | } 13 | 14 | } -------------------------------------------------------------------------------- /backend/core/interactem/core/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .canonical import ( 2 | CanonicalOperator, 3 | CanonicalPipeline, 4 | CanonicalEdge, 5 | CanonicalPort, 6 | ) 7 | from .runtime import ( 8 | RuntimeOperator, 9 | RuntimePipeline, 10 | RuntimeEdge, 11 | RuntimePort, 12 | ) 13 | from .base import ( 14 | CommBackend, 15 | IdType, 16 | NodeType, 17 | PortType, 18 | Protocol, 19 | URILocation, 20 | ) 21 | from .uri import URI, ZMQAddress 22 | -------------------------------------------------------------------------------- /operators/detstream-producer/operator.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "12345678-0000-0000-0000-1234567890ab", 3 | "image": "ghcr.io/nersc/interactem/detstream-producer", 4 | "label": "Producer", 5 | "description": "Producer for the Detstream pipeline", 6 | "parameters": [ 7 | { 8 | "name": "command", 9 | "label": "The command to run", 10 | "type": "str", 11 | "default": "0", 12 | "description": "A command", 13 | "required": true 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /scripts/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | GIT_ROOT_DIR=$(git rev-parse --show-toplevel) 4 | cd $GIT_ROOT_DIR 5 | 6 | export NATS_CREDS_FILE=$(pwd)/conf/nats-conf/out_jwt/backend.creds 7 | 8 | cd $GIT_ROOT_DIR/backend/core 9 | poetry install 10 | poetry env info 11 | poetry run pytest 12 | 13 | cd $GIT_ROOT_DIR/backend/orchestrator 14 | poetry install 15 | poetry env info 16 | poetry run pytest 17 | 18 | cd $GIT_ROOT_DIR/operators/distiller-streaming 19 | poetry install 20 | poetry env info 21 | poetry run pytest -------------------------------------------------------------------------------- /scripts/check-docker-permission.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Check if the current user can run docker commands 3 | 4 | if ! docker ps > /dev/null 2>&1; then 5 | echo "Error: Unable to run 'docker ps' as current user" >&2 6 | echo "" >&2 7 | echo "You have two options:" >&2 8 | echo "1. Run docker commands with sudo (e.g., 'sudo make docker-up')" >&2 9 | echo "2. Configure Docker to run without sudo by following: https://docs.docker.com/engine/install/linux-postinstall/" >&2 10 | echo "" >&2 11 | exit 1 12 | fi 13 | -------------------------------------------------------------------------------- /frontend/interactEM/src/types/operator.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod" 2 | import type { OperatorVal } from "./gen" 3 | import { OperatorStatus } from "./gen" 4 | 5 | export const OperatorStatusSchema = z.nativeEnum(OperatorStatus) 6 | 7 | export const OperatorValSchema = z.object({ 8 | id: z.string().uuid(), 9 | canonical_id: z.string().uuid(), 10 | canonical_pipeline_id: z.string().uuid(), 11 | runtime_pipeline_id: z.string().uuid(), 12 | status: OperatorStatusSchema, 13 | }) satisfies z.ZodType 14 | -------------------------------------------------------------------------------- /backend/core/interactem/core/models/README.md: -------------------------------------------------------------------------------- 1 | # Model Descriptions 2 | 3 | Pipelines are a DAG of Operators and Ports 4 | We have to have some way of saying that 5 | 6 | OperatorA -> OutputPortA -> InputPortB -> OperatorB 7 | 8 | and 9 | 10 | OperatorA -> OutputPortB -> InputPortC -> OperatorC 11 | 12 | In other words, we cannot have a direct connection between 13 | two operators because there needs to be some way of identifying 14 | a specific output port of an operator to a specific input port of another operator 15 | -------------------------------------------------------------------------------- /docker/Dockerfile.operator: -------------------------------------------------------------------------------- 1 | FROM interactem-base 2 | 3 | # --- Install interactem operators deps --- 4 | WORKDIR /interactem/operators 5 | RUN touch README.md 6 | COPY ./operators/pyproject.toml ./operators/poetry.lock* ./ 7 | RUN poetry install --no-root 8 | 9 | # --- Install interactem operators --- 10 | COPY ./operators/interactem/ ./interactem 11 | RUN poetry install --only-root 12 | 13 | COPY ./operators/startup.py /app/startup.py 14 | 15 | ENV PYTHONPATH=/interactem/operators 16 | 17 | ENTRYPOINT [ "python", "/app/startup.py" ] -------------------------------------------------------------------------------- /backend/app/interactem/app/initial_data.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from sqlmodel import Session 4 | 5 | from interactem.app.core.db import engine, init_db 6 | 7 | logging.basicConfig(level=logging.INFO) 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | def init() -> None: 12 | with Session(engine) as session: 13 | init_db(session) 14 | 15 | 16 | def main() -> None: 17 | logger.info("Creating initial data") 18 | init() 19 | logger.info("Initial data created") 20 | 21 | 22 | if __name__ == "__main__": 23 | main() 24 | -------------------------------------------------------------------------------- /frontend/interactEM/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs (log files only, not the logs component folder) 2 | *.log 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | pnpm-debug.log* 7 | lerna-debug.log* 8 | 9 | node_modules 10 | dist 11 | dist_app 12 | dist-ssr 13 | *.local 14 | 15 | # Playwright artifacts 16 | playwright-report 17 | playwright 18 | test-results 19 | 20 | # Editor directories and files 21 | .vscode/* 22 | !.vscode/extensions.json 23 | .idea 24 | .DS_Store 25 | *.suo 26 | *.ntvs* 27 | *.njsproj 28 | *.sln 29 | *.sw? 30 | 31 | *.development 32 | -------------------------------------------------------------------------------- /backend/core/interactem/core/util.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import sys 3 | from collections.abc import Coroutine 4 | 5 | if sys.version_info >= (3, 11): 6 | from builtins import BaseExceptionGroup 7 | else: 8 | from exceptiongroup import BaseExceptionGroup 9 | 10 | __all__ = ["BaseExceptionGroup", "create_task_with_ref"] 11 | 12 | def create_task_with_ref(task_refs: set[asyncio.Task], coro: Coroutine) -> asyncio.Task: 13 | task = asyncio.create_task(coro) 14 | task_refs.add(task) 15 | task.add_done_callback(task_refs.discard) # Clean up after completion 16 | return task 17 | -------------------------------------------------------------------------------- /backend/launcher/interactem/launcher/templates/ascii_header.sh.j2: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # ██╗███╗ ██╗████████╗███████╗██████╗ █████╗ ██████╗████████╗███████╗███╗ ███╗ 4 | # ██║████╗ ██║╚══██╔══╝██╔════╝██╔══██╗██╔══██╗██╔════╝╚══██╔══╝██╔════╝████╗ ████║ 5 | # ██║██╔██╗ ██║ ██║ █████╗ ██████╔╝███████║██║ ██║ █████╗ ██╔████╔██║ 6 | # ██║██║╚██╗██║ ██║ ██╔══╝ ██╔══██╗██╔══██║██║ ██║ ██╔══╝ ██║╚██╔╝██║ 7 | # ██║██║ ╚████║ ██║ ███████╗██║ ██║██║ ██║╚██████╗ ██║ ███████╗██║ ╚═╝ ██║ 8 | # ╚═╝╚═╝ ╚═══╝ ╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚══════╝╚═╝ ╚═╝ -------------------------------------------------------------------------------- /docker/bake.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | SCRIPT_DIR=$(dirname "$0") 4 | ROOT_DIR=$(git rev-parse --show-toplevel) 5 | TAG=$(git rev-parse --short=6 HEAD) 6 | DOCKER=${1:-docker} 7 | 8 | cd $ROOT_DIR 9 | 10 | # Build all images using Docker Bake 11 | echo "Building all images with tag $TAG..." 12 | TAG=${TAG} BUILDX_BAKE_ENTITLEMENTS_FS=0 $DOCKER buildx bake \ 13 | --set *.cache-from="" --set *.cache-to="" \ 14 | --file $ROOT_DIR/docker/docker-bake.hcl prod 15 | 16 | if [ $? -ne 0 ]; then 17 | echo "Failed to build images" 18 | exit 1 19 | fi 20 | 21 | echo "All builds completed successfully" -------------------------------------------------------------------------------- /backend/app/interactem/app/api/routes/agents.py: -------------------------------------------------------------------------------- 1 | 2 | from fastapi import APIRouter 3 | 4 | from interactem.app.api.deps import CurrentUser 5 | from interactem.app.events.producer import publish_sfapi_submit_event 6 | from interactem.core.logger import get_logger 7 | from interactem.sfapi_models import AgentCreateEvent 8 | 9 | logger = get_logger() 10 | router = APIRouter() 11 | 12 | @router.post("/launch") 13 | async def launch_agent(current_user: CurrentUser, agent_req: AgentCreateEvent) -> None: 14 | """ 15 | Launch an agent remotely. 16 | """ 17 | await publish_sfapi_submit_event(agent_req) 18 | -------------------------------------------------------------------------------- /operators/detstream-assembler/operator.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "10000008-1234-1234-1234-1000000890ab", 3 | "image": "ghcr.io/nersc/interactem/detstream-assembler", 4 | "label": "Assembler", 5 | "description": "Assembler for the Detstream pipeline", 6 | "inputs": [ 7 | { 8 | "name": "in", 9 | "label": "The input", 10 | "type": "partial frame", 11 | "description": "Partial frame" 12 | } 13 | ], 14 | "outputs": [ 15 | { 16 | "name": "out", 17 | "label": "The output", 18 | "type": "frame", 19 | "description": "Full frame" 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /backend/core/interactem/core/constants/mounts.py: -------------------------------------------------------------------------------- 1 | from ..config import cfg 2 | from ..models.containers import PodmanMount, PodmanMountType 3 | from . import PACKAGE_DIR_IN_CONTAINER 4 | 5 | CORE_MOUNT = PodmanMount( 6 | type=PodmanMountType.bind, 7 | source=str((cfg.CORE_PACKAGE_DIR / "core").resolve()), 8 | target=f"{PACKAGE_DIR_IN_CONTAINER}/core/interactem/core", 9 | ) 10 | 11 | OPERATORS_MOUNT = PodmanMount( 12 | type=PodmanMountType.bind, 13 | source=str((cfg.OPERATORS_PACKAGE_DIR / "operators").resolve()), 14 | target=f"{PACKAGE_DIR_IN_CONTAINER}/operators/interactem/operators", 15 | ) 16 | -------------------------------------------------------------------------------- /backend/rdma/libs/argobots/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Argobots utility library 2 | add_library(argobots_utils SHARED 3 | src/abt_manager.cpp 4 | # src/abt_pool.cpp 5 | # src/abt_scheduler.cpp 6 | ) 7 | 8 | # Include directories for Argobots utility library 9 | target_include_directories(argobots_utils PUBLIC 10 | ${CMAKE_CURRENT_SOURCE_DIR}/../../common/include 11 | ${CMAKE_CURRENT_SOURCE_DIR}/../argobots/include 12 | ) 13 | 14 | # Link external thallium dependency 15 | target_link_libraries(argobots_utils PUBLIC 16 | thallium 17 | ) 18 | 19 | target_compile_features(argobots_utils PUBLIC cxx_std_17) 20 | -------------------------------------------------------------------------------- /frontend/interactEM/src/components/nodes/handles.tsx: -------------------------------------------------------------------------------- 1 | import { Handle, Position } from "@xyflow/react" 2 | import type React from "react" 3 | 4 | interface HandlesProps { 5 | inputs?: string[] 6 | outputs?: string[] 7 | } 8 | 9 | const Handles: React.FC = ({ inputs, outputs }) => ( 10 | <> 11 | {inputs && inputs.length > 0 && ( 12 | 13 | )} 14 | {outputs && outputs.length > 0 && ( 15 | 16 | )} 17 | 18 | ) 19 | 20 | export default Handles 21 | -------------------------------------------------------------------------------- /backend/app/interactem/app/core/util.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | from fastapi import HTTPException 4 | 5 | from interactem.app.api.deps import CurrentUser 6 | from interactem.app.models import Pipeline 7 | 8 | 9 | def check_present_and_authorized( 10 | pipeline: Pipeline | None, current_user: CurrentUser, id: uuid.UUID 11 | ) -> Pipeline: 12 | if not pipeline: 13 | raise HTTPException(status_code=404, detail="Pipeline not found") 14 | if not current_user.is_superuser and (pipeline.owner_id != current_user.id): 15 | raise HTTPException(status_code=404, detail="Pipeline not found") 16 | return pipeline 17 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "editor.formatOnSaveMode": "modifications", 4 | "editor.defaultFormatter": "charliermarsh.ruff", 5 | "python.envFile": "", 6 | "[go]": { 7 | "editor.insertSpaces": true, 8 | "editor.formatOnSave": true, 9 | "editor.codeActionsOnSave": { 10 | "source.organizeImports": "always", 11 | "source.fixAll": "always" 12 | }, 13 | "editor.defaultFormatter": "golang.go", 14 | }, 15 | "biome.enabled": false, 16 | "python.analysis.extraPaths": [ 17 | "./backend/agent/thirdparty/podman-hpc-py" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /backend/core/interactem/core/models/triggers.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | from pydantic import BaseModel 4 | 5 | from interactem.core.models.canonical import CanonicalOperatorID 6 | 7 | 8 | class TriggerInvocation(BaseModel): 9 | canonical_operator_id: CanonicalOperatorID 10 | trigger: str 11 | 12 | 13 | class TriggerInvocationRequest(BaseModel): 14 | trigger: str 15 | 16 | 17 | class TriggerInvocationResponseStatus(str, Enum): 18 | OK = "ok" 19 | ERROR = "error" 20 | 21 | 22 | class TriggerInvocationResponse(BaseModel): 23 | status: TriggerInvocationResponseStatus 24 | message: str | None = None 25 | -------------------------------------------------------------------------------- /frontend/interactEM/src/types/triggers.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod" 2 | import type { OperatorSpecTrigger } from "../client" 3 | 4 | export type { OperatorSpecTrigger } 5 | 6 | export const TriggerInvocationResponseSchema = z.object({ 7 | status: z.enum(["ok", "error"]), 8 | message: z.string().nullable().optional(), 9 | }) 10 | export type TriggerInvocationResponse = z.infer< 11 | typeof TriggerInvocationResponseSchema 12 | > 13 | 14 | export const TriggerInvocationRequestSchema = z.object({ 15 | trigger: z.string(), 16 | }) 17 | export type TriggerInvocationRequest = z.infer< 18 | typeof TriggerInvocationRequestSchema 19 | > 20 | -------------------------------------------------------------------------------- /backend/core/interactem/core/models/_export.py: -------------------------------------------------------------------------------- 1 | # ruff: noqa: F401 2 | 3 | from .kvs import AgentVal, OperatorVal, PortVal 4 | from .logs import AgentLog, OperatorLog 5 | from .runtime import ( 6 | RuntimeEdge, 7 | RuntimeOperator, 8 | RuntimeOperatorParameter, 9 | RuntimeOperatorParameterAck, 10 | RuntimeOperatorParameterUpdate, 11 | RuntimePipeline, 12 | RuntimePort, 13 | ) 14 | from .spec import ExportParameterSpecType, TriggerInvocationMode 15 | from .triggers import ( 16 | TriggerInvocation, 17 | TriggerInvocationRequest, 18 | TriggerInvocationResponse, 19 | TriggerInvocationResponseStatus, 20 | ) 21 | -------------------------------------------------------------------------------- /backend/core/interactem/core/nats/storage.py: -------------------------------------------------------------------------------- 1 | """NATS stream storage type configuration. 2 | 3 | This is separated from the main NATS config to allow stream configs to be defined 4 | without triggering full NATS credential validation. 5 | """ 6 | 7 | from nats.js.api import StorageType 8 | from pydantic_settings import BaseSettings, SettingsConfigDict 9 | 10 | 11 | class StorageConfig(BaseSettings): 12 | """Minimal config for just the stream storage type.""" 13 | 14 | model_config = SettingsConfigDict(env_file=".env", extra="ignore") 15 | NATS_STREAM_STORAGE_TYPE: StorageType = StorageType.MEMORY 16 | 17 | cfg = StorageConfig() 18 | -------------------------------------------------------------------------------- /docker/Dockerfile.fastapi: -------------------------------------------------------------------------------- 1 | FROM interactem-base 2 | 3 | WORKDIR /interactem/app 4 | COPY ./app/pyproject.toml ./app/poetry.lock* ./ 5 | RUN touch README.md 6 | COPY ./sfapi_models/ /interactem/sfapi_models/ 7 | RUN poetry install --no-root --without dev 8 | 9 | ENV PYTHONPATH=/interactem/app 10 | 11 | ENV MODULE_NAME=interactem.app.main 12 | 13 | COPY ./app/scripts ./scripts 14 | 15 | COPY ./app/alembic.ini ./ 16 | 17 | COPY ./app/prestart.sh ./ 18 | 19 | COPY ./app/tests-start.sh ./ 20 | 21 | COPY ./app/interactem ./interactem 22 | 23 | RUN poetry install --only-root 24 | 25 | CMD ["fastapi", "run", "--workers", "4", "interactem/app/main.py"] -------------------------------------------------------------------------------- /backend/rdma/common/include/op_mode.hpp: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * @file op_mode.hpp 4 | * @brief Defines operation modes for RDMA proxy service 5 | * 6 | * Contains enumeration for different operation modes (SERVER, CLIENT, UNKNOWN) 7 | * using Thallium constants to determine the proxy's operational behavior. 8 | */ 9 | 10 | #pragma once 11 | 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | namespace interactEM { 19 | 20 | enum class OperationMode { 21 | SERVER = THALLIUM_SERVER_MODE, 22 | CLIENT = THALLIUM_CLIENT_MODE 23 | }; 24 | 25 | } 26 | 27 | 28 | -------------------------------------------------------------------------------- /backend/rdma/include/op_sender.hpp: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * @file op_sender.hpp 4 | * @brief Handles message sending from operators on a node to the RDMA proxy 5 | * 6 | * OpSender manages local operator-to-proxy communication, implementing message queuing 7 | * and forwarding from local operators to the RDMA proxy service. 8 | */ 9 | 10 | #include 11 | #include 12 | #include 13 | 14 | namespace interactEM { 15 | 16 | class OperatorSender { 17 | 18 | private: 19 | 20 | public: 21 | OperatorSender() = default; 22 | 23 | // void initialize(); 24 | // void stop(); 25 | 26 | }; 27 | 28 | } -------------------------------------------------------------------------------- /backend/rdma/libs/argobots/include/abt_config.hpp: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include "abt_type.hpp" 7 | 8 | namespace interactEM { 9 | 10 | namespace tl = thallium; 11 | 12 | class AbtConfig { 13 | 14 | private: 15 | int num_xstreams_; // Number of execution streams 16 | int num_pools_; // Number of Argobots pools 17 | tl::pool::access pool_access_; // Access type for the Argobots pool 18 | tl::scheduler::predef scheduler_type_; // Predefined scheduler type 19 | 20 | public: 21 | AbtConfig() = default; 22 | 23 | }; 24 | 25 | } -------------------------------------------------------------------------------- /backend/agent/.env.example: -------------------------------------------------------------------------------- 1 | DOCKER_COMPATIBILITY_MODE=true 2 | LOCAL=true 3 | NATS_SERVER_URL=nats://localhost:4222 4 | NATS_SERVER_URL_IN_CONTAINER=nats://host.containers.internal:4222 5 | PODMAN_SERVICE_URI=unix:///var/folders/np/......./something.sock 6 | MOUNT_LOCAL_REPO=true 7 | NATS_CREDS_FILE=../../conf/nats-conf/out_jwt/backend.creds 8 | OPERATOR_CREDS_FILE=../../conf/nats-conf/out_jwt/operator.creds 9 | ZMQ_BIND_HOSTNAME="localhost" 10 | ZMQ_BIND_INTERFACE="lo" 11 | AGENT_NETWORKS='["perlmutter"]' 12 | AGENT_NAME="SecretAgentMan" 13 | NATS_STREAM_STORAGE_TYPE=file 14 | 15 | # Vector log aggregation 16 | VECTOR_AGGREGATOR_ADDR=host.containers.internal:6000 17 | -------------------------------------------------------------------------------- /backend/core/scripts/pydantic_to_ts.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | 3 | from pydantic2ts import generate_typescript_defs 4 | 5 | # 0. Determine the repository root relative to this file 6 | repo_root = pathlib.Path(__file__).resolve().parents[3] 7 | output_path = repo_root / "frontend" / "interactEM" / "src" / "types" / "gen.ts" 8 | json2ts_cmd = "json2ts --inferStringEnumKeysFromValues --enableConstEnums false" 9 | 10 | # 1. Generate the TypeScript file from Pydantic models 11 | print("Generating TypeScript definitions...") 12 | generate_typescript_defs( 13 | "interactem.core.models._export", str(output_path), (), json2ts_cmd 14 | ) 15 | print("Generation complete.") 16 | -------------------------------------------------------------------------------- /backend/app/interactem/app/alembic/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | import sqlmodel.sql.sqltypes 11 | ${imports if imports else ""} 12 | 13 | # revision identifiers, used by Alembic. 14 | revision = ${repr(up_revision)} 15 | down_revision = ${repr(down_revision)} 16 | branch_labels = ${repr(branch_labels)} 17 | depends_on = ${repr(depends_on)} 18 | 19 | 20 | def upgrade(): 21 | ${upgrades if upgrades else "pass"} 22 | 23 | 24 | def downgrade(): 25 | ${downgrades if downgrades else "pass"} 26 | -------------------------------------------------------------------------------- /docker-compose.override.yml: -------------------------------------------------------------------------------- 1 | services: 2 | db: 3 | restart: "no" 4 | 5 | backend: 6 | restart: "no" 7 | ports: 8 | - "8888:8888" 9 | volumes: 10 | - ./backend/app/:/interactem/app 11 | - ./backend/core/:/interactem/core 12 | - ./backend/sfapi_models/:/interactem/sfapi_models 13 | - ./operators/:/operators 14 | command: 15 | - fastapi 16 | - run 17 | - --reload 18 | - "interactem/app/main.py" 19 | 20 | orchestrator: 21 | volumes: 22 | - ./backend/orchestrator/:/interactem/orchestrator 23 | - ./backend/core/:/interactem/core 24 | - ./backend/sfapi_models/:/interactem/sfapi_models -------------------------------------------------------------------------------- /backend/sfapi_models/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "interactem-sfapi-models" 3 | version = "0.1.0" 4 | description = "" 5 | authors = [ 6 | {name = "Sam Welborn", email = "swelborn@lbl.gov"}, 7 | {name = "Chris Harris", email = "cjh@lbl.gov"} 8 | ] 9 | requires-python = ">=3.10" 10 | readme = "README.md" 11 | dependencies = [ 12 | "sfapi-client>=0.4,<1", 13 | "pydantic>=2.12.3,<3", 14 | "h11>=0.16.0,<1", 15 | ] 16 | 17 | [tool.poetry] 18 | packages = [{ include = "interactem" }] 19 | 20 | [build-system] 21 | requires = ["poetry-core"] 22 | build-backend = "poetry.core.masonry.api" 23 | 24 | [tool.ruff] 25 | target-version = "py310" 26 | extend = "../../.ruff.toml" -------------------------------------------------------------------------------- /operators/random-table/operator.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "bb45f14c-bce3-445d-8930-e7c6633a3aca", 3 | "image": "ghcr.io/nersc/interactem/random-table:latest", 4 | "label": "Random Table", 5 | "description": "Generates a random table", 6 | "outputs": [ 7 | { 8 | "name": "out", 9 | "label": "The table", 10 | "type": "table", 11 | "description": "The table" 12 | } 13 | ], 14 | "parameters": [ 15 | { 16 | "name": "update_interval", 17 | "label": "Update Interval", 18 | "type": "int", 19 | "default": "3", 20 | "description": "The interval at which to update the table", 21 | "required": true 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /backend/rdma/libs/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | 2 | add_subdirectory(thallium) 3 | add_subdirectory(argobots) 4 | 5 | # Create a meta-library that aggregates all external library dependencies 6 | add_library(rdma_external_libs INTERFACE) 7 | 8 | # Link all external library wrappers to the meta-library 9 | target_link_libraries(rdma_external_libs INTERFACE 10 | thallium_engine 11 | argobots_utils 12 | ) 13 | 14 | # Export include directories for consumers 15 | target_include_directories(rdma_external_libs INTERFACE 16 | ${CMAKE_CURRENT_SOURCE_DIR}/thallium/include 17 | ${CMAKE_CURRENT_SOURCE_DIR}/argobots/include 18 | ) 19 | 20 | target_compile_features(rdma_external_libs INTERFACE cxx_std_17) -------------------------------------------------------------------------------- /operators/benchmark-sender/operator.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "648d6b2f-badc-42a6-b60b-cc1d195fc5af", 3 | "image": "ghcr.io/nersc/interactem/benchmark-sender:latest", 4 | "label": "Benchmark Sender", 5 | "description": "Sends random frames for benchmarking", 6 | "outputs": [ 7 | { 8 | "name": "out", 9 | "label": "The output", 10 | "type": "partial frame", 11 | "description": "Partial frame" 12 | } 13 | ], 14 | "parameters": [ 15 | { 16 | "name": "interval", 17 | "label": "Interval", 18 | "type": "int", 19 | "default": "2", 20 | "description": "The interval at which to send the frame", 21 | "required": true 22 | } 23 | ] 24 | } -------------------------------------------------------------------------------- /docker/Dockerfile.callout: -------------------------------------------------------------------------------- 1 | # Note: dockerfile created based on instructions on 2 | # https://hub.docker.com/_/golang 3 | 4 | FROM golang:1.24-alpine3.21 5 | 6 | WORKDIR /usr/src/app 7 | 8 | # Copy go.mod and go.sum for cache purposes 9 | COPY callout/service/go.mod callout/service/go.sum ./ 10 | 11 | # Download dependencies 12 | RUN go mod download && go mod verify 13 | 14 | # Copy the rest of your application source code 15 | COPY callout/service/main.go . 16 | 17 | # Build the Go application 18 | RUN go build -v -o /usr/local/bin/app ./... 19 | 20 | WORKDIR / 21 | RUN touch .env 22 | # Command to run the application 23 | CMD ["app"] 24 | 25 | # Note: We need to mount in .env or set env vars. See .env for example -------------------------------------------------------------------------------- /backend/rdma/include/op_receiver.hpp: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * @file op_receiver.hpp 4 | * @brief Handles incoming messages that reach the RDMA proxy and manages routing to destination operators 5 | * 6 | * OpReceiver is responsible for processing messages received by the RDMA proxy from remote sources 7 | * and ensuring proper delivery to the appropriate destination operators on the local node. 8 | */ 9 | 10 | #include 11 | #include 12 | #include 13 | 14 | namespace interactEM { 15 | 16 | class OperatorReceiver { 17 | 18 | private: 19 | 20 | public: 21 | OperatorReceiver() = default; 22 | 23 | // void initialize(); 24 | // void stop(); 25 | }; 26 | 27 | } -------------------------------------------------------------------------------- /frontend/interactEM/src/auth/base.tsx: -------------------------------------------------------------------------------- 1 | import { type ReactNode, createContext, useContext } from "react" 2 | 3 | // Both internal/external auth implement this 4 | export type AuthState = { 5 | token: string | null 6 | natsJwt: string | null 7 | isAuthenticated: boolean 8 | isLoading: boolean 9 | error: Error | null 10 | } 11 | 12 | export type AuthProviderProps = { 13 | children: ReactNode 14 | apiBaseUrl?: string 15 | } 16 | 17 | export const AuthContext = createContext(undefined) 18 | 19 | export function useAuth() { 20 | const context = useContext(AuthContext) 21 | if (!context) { 22 | throw new Error("useAuth must be used within an AuthProvider") 23 | } 24 | return context 25 | } 26 | -------------------------------------------------------------------------------- /scripts/setup-docker-registry.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | # Set up local Docker registry for operator builds 5 | echo "Setting up local Docker registry for operator builds..." 6 | 7 | REGISTRY_NAME="docker-registry" 8 | REGISTRY_PORT="5001" 9 | REGISTRY_CONTAINER_PORT="5000" 10 | 11 | if docker ps --filter "name=$REGISTRY_NAME" --format "{{.Names}}" | grep -q "$REGISTRY_NAME"; then 12 | echo "✓ Docker registry already running on localhost:$REGISTRY_PORT" 13 | else 14 | docker run -d \ 15 | -p "$REGISTRY_PORT:$REGISTRY_CONTAINER_PORT" \ 16 | --restart always \ 17 | --name "$REGISTRY_NAME" \ 18 | registry:3 19 | echo "✓ Docker registry started on localhost:$REGISTRY_PORT" 20 | fi -------------------------------------------------------------------------------- /frontend/interactEM/public/microscope.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /scripts/generate-client.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | set -e 4 | set -x 5 | 6 | # activate backend/app environment using poetry shell command before running 7 | THIS_DIR=$(dirname "$0") 8 | ROOT_DIR=$(realpath $THIS_DIR/..) 9 | cd $ROOT_DIR 10 | poetry run -P backend/app -- \ 11 | python -c "import interactem.app.main; import json; print(json.dumps(interactem.app.main.app.openapi()))" \ 12 | > $ROOT_DIR/openapi.json 13 | cd $ROOT_DIR 14 | mv openapi.json frontend/interactEM/openapi.json 15 | cd frontend/interactEM 16 | npm run generate-client 17 | npx biome check \ 18 | --formatter-enabled=true \ 19 | --linter-enabled=true \ 20 | --organize-imports-enabled=true \ 21 | --write \ 22 | ./src/client/ ./openapi.json -------------------------------------------------------------------------------- /operators/pva-converter/operator.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "12345678-1234-1234-1234-1432567830cd", 3 | "image": "ghcr.io/nersc/interactem/pva-converter", 4 | "label": "PVA Converter", 5 | "description": "Consumes PVA and sends it to the next operator", 6 | "outputs": [ 7 | { 8 | "name": "out", 9 | "label": "The output", 10 | "type": "PvObject", 11 | "description": "Sparse scan" 12 | } 13 | ], 14 | "parameters": [ 15 | { 16 | "name": "env_file", 17 | "label": ".env file", 18 | "type": "mount", 19 | "default": "~/Documents/gits/interactEM/operators/pva-converter/.env", 20 | "description": "Environment file for pvaPy Consumer", 21 | "required": true 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /docs/source/_static/js/version-picker.js: -------------------------------------------------------------------------------- 1 | function toggleVersionDropdown() { 2 | const dropdown = document.getElementById("version-dropdown"); 3 | const arrow = document.querySelector(".dropdown-arrow"); 4 | const isVisible = dropdown.style.display === "block"; 5 | 6 | dropdown.style.display = isVisible ? "none" : "block"; 7 | arrow.style.transform = isVisible ? "rotate(0deg)" : "rotate(180deg)"; 8 | } 9 | 10 | // Close dropdown when clicking outside 11 | document.addEventListener("click", function (event) { 12 | if (!event.target.closest(".version-dropdown")) { 13 | document.getElementById("version-dropdown").style.display = "none"; 14 | document.querySelector(".dropdown-arrow").style.transform = "rotate(0deg)"; 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /operators/electron-count/Containerfile: -------------------------------------------------------------------------------- 1 | FROM interactem-operator AS operator-base 2 | 3 | # Main build using stempy as the base 4 | FROM docker.io/samwelborn/stempy:py310-f685f4d 5 | 6 | WORKDIR /app/ 7 | 8 | # Copy the interactem operator files from the base image 9 | COPY --from=operator-base /interactem/operators /interactem/operators 10 | COPY --from=operator-base /interactem/core /interactem/core 11 | 12 | RUN pip install /interactem/core /interactem/operators scipy 13 | 14 | # Copy the electron-count specific run script 15 | COPY ./run.py /app/run.py 16 | 17 | # Set Python path 18 | ENV PYTHONUNBUFFERED=1 19 | 20 | # Copy the startup script 21 | COPY --from=operator-base /app/startup.py /app/startup.py 22 | 23 | ENTRYPOINT ["python", "/app/startup.py"] -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/docker-existing-dockerfile 3 | 4 | { 5 | "name": "works-on-mac-arm", 6 | "build": { 7 | "context": "..", 8 | "dockerfile": "./Dockerfile" 9 | }, 10 | "runArgs": [ 11 | "--privileged" 12 | ], 13 | "features": { 14 | "ghcr.io/devcontainers/features/git:1": {} 15 | }, 16 | "customizations": { 17 | "vscode": { 18 | "extensions": [ 19 | "charliermarsh.ruff", 20 | "ms-azuretools.vscode-docker", 21 | "ms-python.black-formatter", 22 | "ms-python.debugpy", 23 | "ms-python.isort", 24 | "ms-python.python" 25 | ] 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /backend/app/interactem/app/api/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | 3 | from interactem.app.api.routes import ( 4 | agents, 5 | deployments, 6 | login, 7 | operators, 8 | pipelines, 9 | users, 10 | ) 11 | 12 | api_router = APIRouter() 13 | api_router.include_router(login.router, tags=["login"]) 14 | api_router.include_router(users.router, prefix="/users", tags=["users"]) 15 | api_router.include_router(pipelines.router, prefix="/pipelines", tags=["pipelines"]) 16 | api_router.include_router( 17 | deployments.router, prefix="/deployments", tags=["deployments"] 18 | ) 19 | api_router.include_router(operators.router, prefix="/operators", tags=["operators"]) 20 | api_router.include_router(agents.router, prefix="/agents", tags=["agents"]) 21 | -------------------------------------------------------------------------------- /operators/distiller-streaming/distiller_streaming/com.py: -------------------------------------------------------------------------------- 1 | from stempy.image import com_v1_kernel 2 | 3 | from distiller_streaming.models import BatchedFrames 4 | 5 | 6 | def com_sparse( 7 | batch: BatchedFrames, 8 | crop_to: tuple[int, int] | None = None, 9 | init_center: tuple[int, int] | None = None, 10 | replace_nans: bool = True, 11 | ): 12 | scan_shape = batch.header.scan_shape 13 | frame_shape = batch.header.frame_shape 14 | all_events_concat, position_indices = batch.get_frame_arrays_with_positions() 15 | 16 | return com_v1_kernel( 17 | all_events_concat, 18 | position_indices, 19 | scan_shape, 20 | frame_shape, 21 | crop_to, 22 | init_center, 23 | replace_nans, 24 | ) 25 | -------------------------------------------------------------------------------- /backend/metrics/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "interactem-metrics" 3 | version = "0.1.0" 4 | description = "" 5 | authors = [{name = "Sam Welborn", email = "swelborn@lbl.gov"}] 6 | readme = "README.md" 7 | requires-python = ">=3.10" 8 | dependencies = [ 9 | "interactem-core", 10 | "prometheus-client>=0.22.1,<1", 11 | ] 12 | 13 | [tool.uv.sources] 14 | interactem-core = { path = "../core", editable = true } 15 | 16 | [tool.poetry] 17 | packages = [{ include = "interactem" }] 18 | 19 | [tool.poetry.dependencies] 20 | interactem-core = {path = "../core", develop = true} 21 | 22 | 23 | [build-system] 24 | requires = ["poetry-core"] 25 | build-backend = "poetry.core.masonry.api" 26 | 27 | [tool.ruff] 28 | target-version = "py310" 29 | extend = "../../.ruff.toml" -------------------------------------------------------------------------------- /docs/source/getting-started.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | Use this path if you just want to see interactEM running locally. 4 | 5 | ## 1. Bring up the core stack 6 | 7 | Follow the [running locally](introduction.md#running-locally) steps to install prerequisites, generate your `.env`, and start Docker services. 8 | 9 | ## 2. Launch an agent 10 | 11 | Once the web UI is up, start an agent so operators can be scheduled. Instructions live on the [launching an agent](launch-agent.md) page. 12 | 13 | ## 3. Build or pull operators 14 | 15 | - Use `make operators` to build all bundled operators into Podman storage. 16 | - Or build a specific operator target with `make operator target=`. 17 | - To create your own, follow [authoring operators](authoring-operators.md). 18 | -------------------------------------------------------------------------------- /frontend/interactEM/src/hooks/nats/useAgents.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from "react" 2 | import { useAgentStatusContext } from "../../contexts/nats/agentstatus" 3 | 4 | export const useAgent = (id: string) => { 5 | const { agents, agentsLoading, agentsError } = useAgentStatusContext() 6 | 7 | const agent = useMemo( 8 | () => (id ? agents.find((a) => a.uri.id === id) || null : null), 9 | [agents, id], 10 | ) 11 | 12 | return { 13 | agent, 14 | isLoading: agentsLoading, 15 | error: agentsError, 16 | } 17 | } 18 | 19 | export const useAllAgents = () => { 20 | const { agents, agentsLoading, agentsError } = useAgentStatusContext() 21 | 22 | return { 23 | agents, 24 | isLoading: agentsLoading, 25 | error: agentsError, 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /docker/Dockerfile.base: -------------------------------------------------------------------------------- 1 | FROM python:3.12-slim-bookworm 2 | 3 | # --- Install Poetry --- 4 | ENV POETRY_HOME=/opt/poetry 5 | ENV POETRY_NO_INTERACTION=1 6 | ENV POETRY_VIRTUALENVS_IN_PROJECT=0 7 | ENV POETRY_VIRTUALENVS_CREATE=0 8 | ENV PYTHONDONTWRITEBYTECODE=1 9 | ENV PYTHONUNBUFFERED=1 10 | ENV POETRY_CACHE_DIR=/opt/.cache 11 | 12 | RUN pip install "poetry>=2.2.0" 13 | 14 | # --- Install interactem core deps --- 15 | WORKDIR /interactem/core 16 | RUN touch README.md 17 | COPY ./core/pyproject.toml ./core/poetry.lock* ./ 18 | RUN poetry install --no-root --without dev 19 | 20 | # --- Install interactem core --- 21 | COPY ./core/interactem/ ./interactem 22 | RUN poetry install --only-root 23 | 24 | # --- Add logs directory to all containers --- 25 | RUN mkdir -p /interactem/logs -------------------------------------------------------------------------------- /frontend/interactEM/src/utils/statusColor.ts: -------------------------------------------------------------------------------- 1 | import { AgentStatus } from "../types/gen" 2 | 3 | export function getAgentStatusColor( 4 | status: AgentStatus, 5 | ): "info" | "success" | "warning" | "error" | "default" { 6 | switch (status) { 7 | case AgentStatus.initializing: 8 | return "info" 9 | case AgentStatus.idle: 10 | return "success" 11 | case AgentStatus.deployment_error: 12 | return "error" 13 | case AgentStatus.operators_starting: 14 | return "info" 15 | case AgentStatus.cleaning_operators: 16 | return "warning" 17 | case AgentStatus.running_deployment: 18 | return "success" 19 | case AgentStatus.shutting_down: 20 | return "default" 21 | default: 22 | return "default" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /frontend/interactEM/src/components/nodes/header.tsx: -------------------------------------------------------------------------------- 1 | import type React from "react" 2 | 3 | interface OperatorHeaderProps { 4 | id: string 5 | label: string 6 | } 7 | 8 | const OperatorHeader: React.FC = ({ label }) => { 9 | // TODO: reimplement error handling - from OperatorVal (status bucket) 10 | return ( 11 |
12 | {label} 13 | {/* {operatorEvent?.type === OperatorEventType.error && ( 14 | 18 | 19 | 20 | )} */} 21 |
22 | ) 23 | } 24 | 25 | export default OperatorHeader 26 | -------------------------------------------------------------------------------- /frontend/interactEM/src/components/statusdot.tsx: -------------------------------------------------------------------------------- 1 | import { Badge, Tooltip } from "@mui/material" 2 | import type { AgentStatus } from "../types/gen" 3 | import { getAgentStatusColor } from "../utils/statusColor" 4 | 5 | type StatusDotProps = { 6 | status: AgentStatus 7 | tooltipContent?: React.ReactNode 8 | } 9 | 10 | export const StatusDot = ({ status, tooltipContent }: StatusDotProps) => { 11 | const color = getAgentStatusColor(status) 12 | 13 | const badgeElement = 14 | 15 | if (tooltipContent) { 16 | return ( 17 | 18 |
{badgeElement}
19 |
20 | ) 21 | } 22 | 23 | return badgeElement 24 | } 25 | -------------------------------------------------------------------------------- /operators/distiller-streaming/Containerfile: -------------------------------------------------------------------------------- 1 | FROM interactem-operator 2 | 3 | WORKDIR /app 4 | COPY ./pyproject.toml ./poetry.lock ./README.md /app/ 5 | 6 | # Base image installs interactem-core at /interactem/core. 7 | # Locally, the project uses ../../backend/core which 8 | # resolves to /backend/core inside the container. We symlink it here 9 | # so poetry can find it. 10 | RUN mkdir -p /backend && \ 11 | if [ -d /interactem/core ]; then \ 12 | ln -sfn /interactem/core /backend/core; \ 13 | fi && \ 14 | if [ -d /interactem/operators ]; then \ 15 | ln -sfn /interactem/operators /backend/operators; \ 16 | fi 17 | 18 | RUN poetry install --no-root --without dev 19 | 20 | COPY ./distiller_streaming/ /app/distiller_streaming/ 21 | RUN poetry install --only-root -------------------------------------------------------------------------------- /backend/core/interactem/core/config.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | from enum import Enum 3 | 4 | from pydantic_settings import BaseSettings, SettingsConfigDict 5 | 6 | 7 | class LogLevel(str, Enum): 8 | INFO = "INFO" 9 | DEBUG = "DEBUG" 10 | WARNING = "WARNING" 11 | ERROR = "ERROR" 12 | CRITICAL = "CRITICAL" 13 | 14 | 15 | class Settings(BaseSettings): 16 | model_config = SettingsConfigDict(env_file=".env", extra="ignore") 17 | CORE_PACKAGE_DIR: pathlib.Path = pathlib.Path(__file__).parent.parent 18 | OPERATORS_PACKAGE_DIR: pathlib.Path = ( 19 | pathlib.Path(__file__).parent.parent.parent.parent / "operators" / "interactem" 20 | ) 21 | LOG_LEVEL: LogLevel = LogLevel.INFO 22 | PARALLEL_EXPANSION_FACTOR: int = 4 23 | 24 | 25 | cfg = Settings() 26 | -------------------------------------------------------------------------------- /frontend/interactEM/src/config/index.ts: -------------------------------------------------------------------------------- 1 | interface Config { 2 | NATS_SERVER_URL: string 3 | API_BASE_URL: string 4 | } 5 | 6 | function buildConfig(): Config { 7 | // Use .env.development variables in development 8 | if (import.meta.env.DEV) { 9 | return { 10 | NATS_SERVER_URL: import.meta.env.VITE_NATS_SERVER_URL || "", 11 | API_BASE_URL: import.meta.env.VITE_REACT_APP_API_BASE_URL || "", 12 | } 13 | } 14 | 15 | const protocol = window.location.protocol === "https:" ? "wss:" : "ws:" 16 | const host = window.location.host 17 | 18 | return { 19 | NATS_SERVER_URL: `${protocol}//${host}/nats`, 20 | API_BASE_URL: `${window.location.protocol}//${host}/`, 21 | } 22 | } 23 | 24 | const config: Config = buildConfig() 25 | 26 | export default config 27 | -------------------------------------------------------------------------------- /frontend/interactEM/src/hooks/api/usePipelineQuery.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from "@tanstack/react-query" 2 | import { pipelinesReadPipelineOptions } from "../../client/generated/@tanstack/react-query.gen" 3 | 4 | export const usePipeline = (pipelineId: string) => { 5 | return useQuery({ 6 | ...pipelinesReadPipelineOptions({ 7 | path: { id: pipelineId }, 8 | }), 9 | }) 10 | } 11 | 12 | export const usePipelineName = (pipelineId: string) => { 13 | const { data: pipeline, isFetching } = usePipeline(pipelineId) 14 | 15 | if (isFetching) { 16 | return "Loading..." 17 | } 18 | 19 | // Return first 8 of uuid if no name available 20 | if (!pipeline || !pipeline.name) { 21 | return `${pipelineId.substring(0, 8)}` 22 | } 23 | 24 | return pipeline.name 25 | } 26 | -------------------------------------------------------------------------------- /backend/operators/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "interactem-operators" 3 | version = "0.1.0" 4 | description = "" 5 | authors = [{name = "Sam Welborn", email = "swelborn@lbl.gov"}] 6 | readme = "README.md" 7 | requires-python = ">=3.10" 8 | dependencies = [ 9 | "interactem-core", 10 | "pyzmq>=27.1.0,<28", 11 | "aiohttp>=3.10.5,<4", 12 | ] 13 | 14 | [tool.uv.sources] 15 | interactem-core = { path = "../core", editable = true } 16 | 17 | [tool.poetry] 18 | packages = [{ include = "interactem" }] 19 | 20 | [tool.poetry.dependencies] 21 | interactem-core = {path = "../core", develop = true} 22 | 23 | 24 | [build-system] 25 | requires = ["poetry-core"] 26 | build-backend = "poetry.core.masonry.api" 27 | 28 | [tool.ruff] 29 | target-version = "py310" 30 | extend = "../../.ruff.toml" -------------------------------------------------------------------------------- /backend/rdma/src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # RDMA Proxy core library 2 | add_library(rdma_proxy_core SHARED 3 | # RdmaProxyService.cpp 4 | # op_receiver.cpp 5 | # op_sender.cpp 6 | prx_transporter.cpp 7 | ) 8 | 9 | # Include directories for the core library 10 | target_include_directories(rdma_proxy_core PUBLIC 11 | ${CMAKE_CURRENT_SOURCE_DIR}/../include 12 | ${CMAKE_CURRENT_SOURCE_DIR}/../common/include 13 | ${CMAKE_CURRENT_SOURCE_DIR}/../libs/thallium/include 14 | ${CMAKE_CURRENT_SOURCE_DIR}/../libs/argobots/include 15 | ) 16 | 17 | # Link dependencies 18 | target_link_libraries(rdma_proxy_core PUBLIC 19 | common_utils 20 | rdma_external_libs 21 | thallium 22 | ) 23 | 24 | # Set compile features 25 | target_compile_features(rdma_proxy_core PUBLIC cxx_std_17) 26 | -------------------------------------------------------------------------------- /operators/distiller-state-client/operator.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "ffc731fc-2a95-46d8-b513-a5efd47701d4", 3 | "image": "ghcr.io/nersc/interactem/distiller-state-client:latest", 4 | "label": "Distiller Pipeline State", 5 | "description": "Connect to the distiller pipeline state server and display the state of the pipeline.", 6 | "outputs": [ 7 | { 8 | "name": "out", 9 | "label": "The output", 10 | "type": "table", 11 | "description": "This table to display" 12 | } 13 | ], 14 | "parameters": [ 15 | { 16 | "name": "pub_address", 17 | "label": "Publisher Address", 18 | "type": "str", 19 | "default": "tcp://localhost:7082", 20 | "description": "This is the address to get the state from", 21 | "required": true 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /operators/error/run.py: -------------------------------------------------------------------------------- 1 | import random 2 | from typing import Any 3 | 4 | from interactem.core.logger import get_logger 5 | from interactem.core.models.messages import BytesMessage, MessageHeader, MessageSubject 6 | from interactem.operators.operator import operator 7 | 8 | logger = get_logger() 9 | 10 | 11 | @operator 12 | def error(inputs: BytesMessage | None, parameters: dict[str, Any]) -> BytesMessage: 13 | raise_exception = random.choice([True, False]) 14 | # raise_exception = True 15 | logger.info(f"Error state: { raise_exception }") 16 | if raise_exception: 17 | raise Exception("This is an error") 18 | else: 19 | return BytesMessage( 20 | header=MessageHeader(subject=MessageSubject.BYTES, meta={}), 21 | data=b"Hello, World!", 22 | ) 23 | -------------------------------------------------------------------------------- /.github/workflows/version-check.yml: -------------------------------------------------------------------------------- 1 | name: Version Guard 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | workflow_dispatch: 9 | 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.ref }} 12 | cancel-in-progress: true 13 | 14 | permissions: 15 | contents: read 16 | 17 | jobs: 18 | version-guard: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - name: Checkout repository 22 | uses: actions/checkout@v4 23 | 24 | - name: Install uv 25 | uses: astral-sh/setup-uv@v7 26 | 27 | - name: Set up Python 28 | run: uv python install 29 | 30 | - name: bump-my-version dry run (check only) 31 | run: uvx bump-my-version@latest bump patch --dry-run --config-file ./pyproject.toml --allow-dirty 32 | -------------------------------------------------------------------------------- /frontend/interactEM/src/hooks/nats/useImage.ts: -------------------------------------------------------------------------------- 1 | import { STREAM_IMAGES } from "../../constants/nats" 2 | import { useOperatorInSelectedPipeline } from "./useOperatorStatus" 3 | import { useStreamMessage } from "./useStreamMessage" 4 | 5 | export const useImage = (operatorID: string): Uint8Array | null => { 6 | const subject = `${STREAM_IMAGES}.${operatorID}` 7 | const { isInRunningPipeline } = useOperatorInSelectedPipeline(operatorID) 8 | 9 | const { data } = useStreamMessage({ 10 | streamName: STREAM_IMAGES, 11 | subject, 12 | enabled: isInRunningPipeline, 13 | transform: (_, originalMessage) => { 14 | // Return the raw binary data instead of parsing as JSON 15 | return originalMessage.data 16 | }, 17 | }) 18 | 19 | return isInRunningPipeline ? data : null 20 | } 21 | -------------------------------------------------------------------------------- /frontend/interactEM/biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.6.1/schema.json", 3 | "organizeImports": { 4 | "enabled": true 5 | }, 6 | "files": { 7 | "ignore": ["node_modules", "dist"] 8 | }, 9 | "linter": { 10 | "enabled": true, 11 | "rules": { 12 | "recommended": true, 13 | "correctness": { 14 | "noUnusedImports": { 15 | "level": "error" 16 | } 17 | }, 18 | "suspicious": { 19 | "noExplicitAny": "off", 20 | "noArrayIndexKey": "off" 21 | }, 22 | "style": { 23 | "noNonNullAssertion": "off" 24 | } 25 | } 26 | }, 27 | "formatter": { 28 | "indentStyle": "space" 29 | }, 30 | "javascript": { 31 | "formatter": { 32 | "quoteStyle": "double", 33 | "semicolons": "asNeeded" 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /backend/app/interactem/app/tests/utils/utils.py: -------------------------------------------------------------------------------- 1 | import random 2 | import string 3 | 4 | from fastapi.testclient import TestClient 5 | 6 | from interactem.app.core.config import settings 7 | 8 | 9 | def random_lower_string() -> str: 10 | return "".join(random.choices(string.ascii_lowercase, k=32)) 11 | 12 | 13 | def random_username() -> str: 14 | return f"user_{random_lower_string()[:8]}" 15 | 16 | 17 | def get_superuser_token_headers(client: TestClient) -> dict[str, str]: 18 | login_data = { 19 | "username": settings.FIRST_SUPERUSER_USERNAME, 20 | "password": settings.FIRST_SUPERUSER_PASSWORD, 21 | } 22 | r = client.post(f"{settings.API_V1_STR}/login/access-token", data=login_data) 23 | tokens = r.json() 24 | a_token = tokens["access_token"] 25 | headers = {"Authorization": f"Bearer {a_token}"} 26 | return headers 27 | -------------------------------------------------------------------------------- /backend/rdma/common/include/abt_type.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file abt_type.hpp 3 | * @brief Argobots-related type definitions for threading and scheduling 4 | * 5 | * Contains type definitions for Argobots lightweight threading support, 6 | * including schedule types (FIFO, ROUND_ROBIN, PRIORITY) and pool types 7 | * (FAST, SLOW, HIGH_PRIORITY) for concurrent operation management. 8 | */ 9 | 10 | #pragma once 11 | 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | namespace interactEM { 18 | 19 | enum class ScheduleType { 20 | SCHEDULE_TYPE_DEFAULT, 21 | SCHEDULE_TYPE_FIFO, 22 | SCHEDULE_TYPE_ROUND_ROBIN, 23 | SCHEDULE_TYPE_PRIORITY 24 | }; 25 | 26 | enum class PoolType { 27 | POOL_TYPE_DEFAULT, 28 | POOL_TYPE_FAST, 29 | POOL_TYPE_SLOW, 30 | POOL_TYPE_HIGH_PRIORITY 31 | }; 32 | 33 | }; -------------------------------------------------------------------------------- /operators/center-of-mass-reduce/operator.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "4e4abf43-0464-42b7-8020-b6acea28a6f6", 3 | "image": "ghcr.io/nersc/interactem/center-of-mass-reduce", 4 | "label": "Reduce Center of Mass", 5 | "description": "Reduces partial centers of mass for a particular scan", 6 | "inputs": [ 7 | { 8 | "name": "in", 9 | "label": "The input", 10 | "type": "frame", 11 | "description": "inputs frame" 12 | } 13 | ], 14 | "outputs": [ 15 | { 16 | "name": "com", 17 | "label": "Output com", 18 | "type": "array", 19 | "description": "Center of mass" 20 | } 21 | ], 22 | "parameters": [ 23 | { 24 | "name": "emit_every", 25 | "type": "int", 26 | "description": "Emit every N frames", 27 | "default": "50", 28 | "label": "Emit Every", 29 | "required": true 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /cli/interactem/cli/settings.py: -------------------------------------------------------------------------------- 1 | import typer 2 | from pydantic import ValidationError 3 | from pydantic_settings import BaseSettings, SettingsConfigDict 4 | from rich import print 5 | 6 | 7 | class Settings(BaseSettings): 8 | interactem_username: str 9 | interactem_password: str 10 | api_base_url: str = "http://localhost:8080/api/v1" 11 | 12 | model_config = SettingsConfigDict( 13 | env_file=".env", env_file_encoding="utf-8", case_sensitive=False 14 | ) 15 | 16 | 17 | def get_settings() -> Settings: 18 | try: 19 | return Settings() # type: ignore[call-arg] 20 | except ValidationError as e: 21 | print("[red]Configuration error:[/red]") 22 | for error in e.errors(): 23 | field = error["loc"][0] 24 | msg = error["msg"] 25 | print(f"[red] - {field}: {msg}[/red]") 26 | raise typer.Exit(1) 27 | -------------------------------------------------------------------------------- /backend/app/interactem/app/tests/api/routes/test.http: -------------------------------------------------------------------------------- 1 | ### Get an access token 2 | POST http://localhost:8080/api/v1/login/access-token 3 | Content-Type: application/x-www-form-urlencoded 4 | 5 | username=admin@example.com&password=changethis 6 | 7 | ### Get an access token from external account 8 | POST http://localhost:8080/api/v1/login/external-token 9 | Content-Type: application/json 10 | Authorization: bearer 11 | 12 | ### Create an agent 13 | POST http://localhost:8080/api/v1/agents/launch 14 | Content-Type: application/json 15 | Authorization: bearer put_the_access_token_here 16 | 17 | { 18 | "machine": "perlmutter", 19 | "num_nodes": 1, 20 | "qos": "debug", 21 | "constraint": "cpu", 22 | "walltime": "00:00:01", 23 | "account": "PUT_AN_ACCOUNT_HERE" 24 | } 25 | 26 | ### 27 | GET http://localhost:80/api/v1/pipelines 28 | Authorization: bearer put_the_access_token_here -------------------------------------------------------------------------------- /frontend/interactEM/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true, 22 | // Recommended setting (not in strict, but should be) 23 | "noUncheckedIndexedAccess": true, 24 | "types": ["node"] 25 | }, 26 | "include": ["src", "tests", "playwright.config.ts"], 27 | "references": [{ "path": "./tsconfig.node.json" }] 28 | } 29 | -------------------------------------------------------------------------------- /.devcontainer/orchestrator-container/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/docker-existing-dockerfile 3 | 4 | { 5 | "name": "Orchestrator Container", 6 | "dockerComposeFile": ["../../docker-compose.yml", "../../docker-compose.override.yml"], 7 | "service": "orchestrator", 8 | "shutdownAction": "none", 9 | "workspaceFolder": "/app", 10 | "features": { 11 | "ghcr.io/devcontainers/features/git:1": {} 12 | }, 13 | "customizations": { 14 | "vscode": { 15 | "extensions": [ 16 | "charliermarsh.ruff", 17 | "ms-azuretools.vscode-docker", 18 | "ms-python.debugpy", 19 | "ms-python.python", 20 | "johnpapa.vscode-peacock" 21 | ] 22 | } 23 | } 24 | } 25 | 26 | -------------------------------------------------------------------------------- /backend/app/interactem/app/alembic/versions/d1149e7679f9_add_is_external_to_user.py: -------------------------------------------------------------------------------- 1 | """Add is_external to User 2 | 3 | Revision ID: d1149e7679f9 4 | Revises: 516457e800b5 5 | Create Date: 2025-01-15 15:09:21.905064 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | import sqlmodel.sql.sqltypes 11 | 12 | 13 | # revision identifiers, used by Alembic. 14 | revision = 'd1149e7679f9' 15 | down_revision = '516457e800b5' 16 | branch_labels = None 17 | depends_on = None 18 | 19 | 20 | def upgrade(): 21 | # Add column with default value as false (not using autogenerated) 22 | op.add_column('user', sa.Column('is_external', sa.Boolean(), server_default="f", nullable=False)) 23 | # ### end Alembic commands ### 24 | 25 | 26 | def downgrade(): 27 | # ### commands auto generated by Alembic - please adjust! ### 28 | op.drop_column('user', 'is_external') 29 | # ### end Alembic commands ### 30 | -------------------------------------------------------------------------------- /backend/core/interactem/core/models/logs.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from enum import Enum 3 | 4 | from pydantic import BaseModel, ConfigDict 5 | 6 | from .runtime import IdType, RuntimeOperatorID 7 | 8 | 9 | class LogType(str, Enum): 10 | AGENT = "agent" 11 | OPERATOR = "operator" 12 | VECTOR = "vector" 13 | 14 | 15 | class AgentLog(BaseModel): 16 | model_config: ConfigDict = ConfigDict(extra="ignore") 17 | agent_id: IdType 18 | host: str 19 | log_type: LogType 20 | level: str 21 | log: str 22 | module: str 23 | timestamp: datetime 24 | 25 | 26 | class OperatorLog(BaseModel): 27 | model_config: ConfigDict = ConfigDict(extra="ignore") 28 | agent_id: IdType 29 | deployment_id: IdType 30 | operator_id: RuntimeOperatorID 31 | host: str 32 | level: str 33 | log: str 34 | log_type: LogType 35 | module: str 36 | timestamp: datetime 37 | -------------------------------------------------------------------------------- /backend/launcher/tests/expected_script.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # ██╗███╗ ██╗████████╗███████╗██████╗ █████╗ ██████╗████████╗███████╗███╗ ███╗ 4 | # ██║████╗ ██║╚══██╔══╝██╔════╝██╔══██╗██╔══██╗██╔════╝╚══██╔══╝██╔════╝████╗ ████║ 5 | # ██║██╔██╗ ██║ ██║ █████╗ ██████╔╝███████║██║ ██║ █████╗ ██╔████╔██║ 6 | # ██║██║╚██╗██║ ██║ ██╔══╝ ██╔══██╗██╔══██║██║ ██║ ██╔══╝ ██║╚██╔╝██║ 7 | # ██║██║ ╚████║ ██║ ███████╗██║ ██║██║ ██║╚██████╗ ██║ ███████╗██║ ╚═╝ ██║ 8 | # ╚═╝╚═╝ ╚═══╝ ╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚══════╝╚═╝ ╚═╝ 9 | 10 | #SBATCH --qos=normal 11 | #SBATCH --constraint=gpu 12 | #SBATCH --time=01:30:00 13 | #SBATCH --account=test_account 14 | #SBATCH --nodes=2 15 | #SBATCH --exclusive 16 | 17 | export HDF5_USE_FILE_LOCKING=FALSE 18 | cd /path/to/.env 19 | srun --nodes=2 --ntasks-per-node=1 uv run --project /path/to/interactEM/backend/agent interactem-agent -------------------------------------------------------------------------------- /backend/app/interactem/app/core/security.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta, timezone 2 | from typing import Any 3 | 4 | import jwt 5 | from passlib.context import CryptContext 6 | 7 | from interactem.app.core.config import settings 8 | 9 | pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") 10 | 11 | 12 | ALGORITHM = "HS256" 13 | 14 | 15 | def create_access_token(subject: str | Any, expires_delta: timedelta) -> str: 16 | expire = datetime.now(timezone.utc) + expires_delta 17 | to_encode = {"exp": expire, "sub": str(subject)} 18 | encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=ALGORITHM) 19 | return encoded_jwt 20 | 21 | 22 | def verify_password(plain_password: str, hashed_password: str) -> bool: 23 | return pwd_context.verify(plain_password, hashed_password) 24 | 25 | 26 | def get_password_hash(password: str) -> str: 27 | return pwd_context.hash(password) 28 | -------------------------------------------------------------------------------- /.ruff.toml: -------------------------------------------------------------------------------- 1 | target-version = "py310" 2 | exclude = [ 3 | "backend/app/interactem/app/alembic/**", 4 | "backend/agent/thirdparty/**", 5 | "conftest.py", 6 | "tests/**" 7 | ] 8 | 9 | [lint] 10 | exclude = ["**/__init__.py"] 11 | select = [ 12 | "E", # pycodestyle errors 13 | "W", # pycodestyle warnings 14 | "F", # pyflakes 15 | "I", # isort 16 | "B", # flake8-bugbear 17 | "C4", # flake8-comprehensions 18 | "UP", # pyupgrade 19 | ] 20 | ignore = [ 21 | "E501", # line too long, handled by black 22 | "B008", # do not perform function calls in argument defaults 23 | "W191", # indentation contains tabs 24 | "B904", # Allow raising exceptions without from e, for HTTPException 25 | ] 26 | 27 | [lint.pyupgrade] 28 | # Preserve types, even if a file imports `from __future__ import annotations`. 29 | keep-runtime-typing = true 30 | 31 | [lint.isort] 32 | known-first-party = ["interactem"] -------------------------------------------------------------------------------- /.devcontainer/backend-app-container/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/docker-existing-dockerfile 3 | 4 | { 5 | "name": "Backend App Container", 6 | "dockerComposeFile": ["../../docker-compose.yml", "../../docker-compose.override.yml"], 7 | "service": "backend", 8 | "shutdownAction": "none", 9 | "workspaceFolder": "/app", 10 | "features": { 11 | "ghcr.io/devcontainers/features/git:1": {} 12 | }, 13 | "customizations": { 14 | "vscode": { 15 | "extensions": [ 16 | "charliermarsh.ruff", 17 | "ms-azuretools.vscode-docker", 18 | "ms-python.debugpy", 19 | "ms-python.python", 20 | "humao.rest-client", 21 | "johnpapa.vscode-peacock" 22 | ] 23 | } 24 | } 25 | } 26 | 27 | -------------------------------------------------------------------------------- /.devcontainer/operator-container/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/docker-existing-dockerfile 3 | 4 | { 5 | "name": "Operator Container", 6 | "dockerComposeFile": ["../../docker-compose.yml", "../../docker-compose.override.yml", "../../docker-compose.operator.yml"], 7 | "service": "operator", 8 | "shutdownAction": "none", 9 | "workspaceFolder": "/app", 10 | "features": { 11 | "ghcr.io/devcontainers/features/git:1": {} 12 | }, 13 | "customizations": { 14 | "vscode": { 15 | "extensions": [ 16 | "charliermarsh.ruff", 17 | "ms-azuretools.vscode-docker", 18 | "ms-python.debugpy", 19 | "ms-python.python", 20 | "humao.rest-client", 21 | "johnpapa.vscode-peacock" 22 | ] 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /backend/app/interactem/app/alembic/versions/b76f7b29a6e7_add_operator_positions_to_pipeline_.py: -------------------------------------------------------------------------------- 1 | """add_operator_positions_to_pipeline_revision 2 | 3 | Revision ID: b76f7b29a6e7 4 | Revises: d259ec167e13 5 | Create Date: 2025-10-22 03:10:20.279675 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | import sqlmodel.sql.sqltypes 11 | 12 | 13 | # revision identifiers, used by Alembic. 14 | revision = 'b76f7b29a6e7' 15 | down_revision = 'd259ec167e13' 16 | branch_labels = None 17 | depends_on = None 18 | 19 | 20 | def upgrade(): 21 | # ### commands auto generated by Alembic - please adjust! ### 22 | op.add_column('pipelinerevision', sa.Column('positions', sa.JSON(), nullable=False, server_default='[]')) 23 | # ### end Alembic commands ### 24 | 25 | 26 | def downgrade(): 27 | # ### commands auto generated by Alembic - please adjust! ### 28 | op.drop_column('pipelinerevision', 'positions') 29 | # ### end Alembic commands ### 30 | -------------------------------------------------------------------------------- /cli/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "interactem-cli" 3 | version = "0.1.0" 4 | description = "Command-line interface for InteractEM pipeline management" 5 | authors = [{name = "Rajat Bhattarai", email = "basistharajat@gmail.com"}] 6 | readme = "README.md" 7 | requires-python = ">=3.10" 8 | dependencies = [ 9 | "interactem-core", 10 | "typer>=0.19.2,<1", 11 | "rich>=13.7.1,<14", 12 | "httpx>=0.28.1,<1", 13 | "jinja2>=3.1.0,<4", 14 | ] 15 | 16 | [project.scripts] 17 | interactem = "interactem.cli.main:app" 18 | 19 | [tool.uv.sources] 20 | interactem-core = { path = "../backend/core", editable = true } 21 | 22 | [tool.poetry] 23 | packages = [{include = "interactem"}] 24 | 25 | [tool.poetry.scripts] 26 | interactem = "interactem.cli.main:app" 27 | 28 | [tool.poetry.dependencies] 29 | interactem-core = {path = "../backend/core", develop = true} 30 | 31 | [build-system] 32 | requires = ["poetry-core"] 33 | build-backend = "poetry.core.masonry.api" -------------------------------------------------------------------------------- /frontend/interactEM/src/utils/deployments.ts: -------------------------------------------------------------------------------- 1 | import type { PipelineDeploymentState } from "../client" 2 | 3 | export const getDeploymentStateColor = ( 4 | state: PipelineDeploymentState, 5 | ): "default" | "primary" | "secondary" | "error" => { 6 | const STATE_COLORS: Record< 7 | PipelineDeploymentState, 8 | "default" | "primary" | "secondary" | "error" 9 | > = { 10 | pending: "secondary", 11 | running: "primary", 12 | cancelled: "default", 13 | failed_to_start: "error", 14 | failure_on_agent: "error", 15 | assigned_agents: "secondary", 16 | } 17 | 18 | return STATE_COLORS[state] || "default" 19 | } 20 | 21 | export const isActiveDeploymentState = ( 22 | state: PipelineDeploymentState, 23 | ): boolean => { 24 | return state === "running" || state === "pending" 25 | } 26 | 27 | export const formatDeploymentState = ( 28 | state: PipelineDeploymentState, 29 | ): string => { 30 | return state.replace("_", " ") 31 | } 32 | -------------------------------------------------------------------------------- /docs/source/_static/css/custom.css: -------------------------------------------------------------------------------- 1 | .muted-card { 2 | opacity: 0.5; 3 | color: #6c757d !important; 4 | } 5 | 6 | .muted-card .card-title { 7 | color: #6c757d !important; 8 | } 9 | 10 | .headerlink { 11 | font-size: 0.8em; 12 | vertical-align: middle; 13 | } 14 | 15 | /* Make h2 and below headers smaller */ 16 | .content h2, 17 | article h2, 18 | .rst-content h2 { 19 | font-size: 1.5rem !important; 20 | } 21 | 22 | .content h3, 23 | article h3, 24 | .rst-content h3 { 25 | font-size: 1.3rem !important; 26 | } 27 | 28 | .content h4, 29 | article h4, 30 | .rst-content h4 { 31 | font-size: 1.1rem !important; 32 | } 33 | 34 | .content h5, 35 | article h5, 36 | .rst-content h5 { 37 | font-size: 1rem !important; 38 | } 39 | 40 | .content h6, 41 | article h6, 42 | .rst-content h6 { 43 | font-size: 0.9rem !important; 44 | } 45 | 46 | .readme-only { 47 | display: none; 48 | } 49 | 50 | .video-wrapper { 51 | margin: 1rem 0; 52 | } 53 | -------------------------------------------------------------------------------- /backend/rdma/include/RdmaProxyService.hpp: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * @file RdmaProxyService.hpp 4 | * @brief Main service orchestrator that coordinates all RDMA operations 5 | * 6 | * RdmaProxyService serves as the primary interface for RDMA proxy functionality, 7 | * managing sender, receiver, and transporter components to facilitate efficient 8 | * inter-node communication in HPC environments. 9 | */ 10 | 11 | #include 12 | #include 13 | #include 14 | #include "op_receiver.hpp" 15 | #include "op_sender.hpp" 16 | #include "prx_transporter.hpp" 17 | 18 | namespace interactEM { 19 | 20 | class RdmaProxyService { 21 | 22 | private: 23 | OpSender sender_; 24 | PrxTransporter transporter_; 25 | OpReceiver receiver_; 26 | 27 | public: 28 | RdmaProxyService() = default; 29 | ~RdmaProxyService() = default; 30 | 31 | // void initialize(); 32 | void stop(); 33 | 34 | }; 35 | 36 | } -------------------------------------------------------------------------------- /cli/interactem/cli/models.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from pydantic import BaseModel 4 | 5 | from interactem.core.models.spec import OperatorSpec 6 | 7 | 8 | class PipelineData(BaseModel): 9 | operators: list[dict] = [] 10 | ports: list[dict] = [] 11 | edges: list[dict] = [] 12 | 13 | 14 | class PipelinePayload(BaseModel): 15 | data: PipelineData 16 | 17 | 18 | class PipelineResponse(BaseModel): 19 | id: str 20 | name: str 21 | data: PipelineData 22 | owner_id: str 23 | created_at: datetime 24 | updated_at: datetime 25 | current_revision_id: int 26 | 27 | 28 | class PipelinesListResponse(BaseModel): 29 | data: list[PipelineResponse] 30 | count: int 31 | 32 | 33 | class TemplateContext(BaseModel): 34 | """Context data for Jinja2 templates.""" 35 | 36 | spec: OperatorSpec 37 | name: str 38 | function_name: str 39 | base_image: str 40 | additional_packages: list[str] | None = None 41 | -------------------------------------------------------------------------------- /operators/center-of-mass-plot/operator.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "287f81c7-4549-4fe6-aa1e-c915761a02e8", 3 | "image": "ghcr.io/nersc/interactem/center-of-mass-plot", 4 | "label": "Center of Mass Plot", 5 | "description": "Plots the center of mass for a particular scan", 6 | "inputs": [ 7 | { 8 | "name": "in", 9 | "label": "The input", 10 | "type": "com", 11 | "description": "Center of Mass" 12 | } 13 | ], 14 | "outputs": [ 15 | { 16 | "name": "image", 17 | "label": "Output matplotlib image", 18 | "type": "image", 19 | "description": "Center of mass image" 20 | } 21 | ], 22 | "parameters": [ 23 | { 24 | "name": "xy_rtheta", 25 | "type": "str-enum", 26 | "description": "Colormap to use for the plot", 27 | "options": [ 28 | "xy", 29 | "rtheta" 30 | ], 31 | "default": "xy", 32 | "label": "Mode", 33 | "required": true 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /backend/orchestrator/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "interactem-orchestrator" 3 | version = "0.1.0" 4 | description = "" 5 | authors = [{name = "Sam Welborn", email = "swelborn@lbl.gov"}] 6 | readme = "README.md" 7 | requires-python = ">=3.10" 8 | dependencies = [ 9 | "requests>=2.32.4,<3", 10 | "interactem-core", 11 | "pydantic-settings>=2.4,<3", 12 | "aiohttp>=3.10.5,<4", 13 | "transitions>=0.9.3,<1", 14 | ] 15 | 16 | [tool.uv.sources] 17 | interactem-core = { path = "../core", editable = true } 18 | 19 | [dependency-groups] 20 | dev = [ 21 | "pytest>=9.0.1,<10", 22 | "pytest-mock>=3.14.0,<4", 23 | ] 24 | 25 | [tool.poetry] 26 | packages = [{ include = "interactem" }] 27 | 28 | [tool.poetry.dependencies] 29 | interactem-core = {path = "../core", develop = true} 30 | 31 | [build-system] 32 | requires = ["poetry-core"] 33 | build-backend = "poetry.core.masonry.api" 34 | 35 | [tool.ruff] 36 | target-version = "py310" 37 | extend = "../../.ruff.toml" -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: help install build clean serve autobuild sync-readmes 2 | 3 | UV := uv 4 | 5 | help: 6 | @echo "Please use \`make ' where is one of" 7 | @echo " build to build the HTML documentation" 8 | @echo " clean to remove generated documentation files" 9 | @echo " serve to build and serve the documentation" 10 | @echo " autobuild to auto-rebuild on file changes" 11 | @echo " sync-readmes to regenerate README files from docs" 12 | 13 | sync-readmes: 14 | $(UV) run scripts/sync_readmes.py 15 | 16 | build: sync-readmes 17 | $(UV) run sphinx-build -n -b html source _build/html 18 | 19 | sync-check: 20 | $(UV) run scripts/sync_readmes.py --check 21 | 22 | clean: 23 | rm -rf _build 24 | 25 | serve: build 26 | @echo "Serving documentation at http://localhost:8000" 27 | @cd _build/html && $(UV) run python -m http.server 28 | 29 | autobuild: 30 | $(UV) run sphinx-autobuild source _build/html --open-browser --fresh-env 31 | -------------------------------------------------------------------------------- /frontend/interactEM/src/contexts/dnd.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | type FC, 3 | type ReactNode, 4 | createContext, 5 | useContext, 6 | useState, 7 | } from "react" 8 | 9 | type DnDContextType = [ 10 | T | null, 11 | React.Dispatch> | null, 12 | ] 13 | 14 | export const DnDContext = createContext< 15 | [any | null, React.Dispatch> | null] 16 | >([null, null]) 17 | 18 | interface DnDProviderProps { 19 | children: ReactNode 20 | } 21 | 22 | export const DnDProvider: FC<{ children: ReactNode }> = ({ 23 | children, 24 | }: DnDProviderProps) => { 25 | const [value, setValue] = useState(null) 26 | 27 | return ( 28 | }> 29 | {children} 30 | 31 | ) 32 | } 33 | 34 | export default DnDContext 35 | 36 | export const useDnD = () => { 37 | return useContext(DnDContext) as DnDContextType 38 | } 39 | -------------------------------------------------------------------------------- /backend/metrics/README.md: -------------------------------------------------------------------------------- 1 | # InteractEM - Metrics 2 | 3 | A Prometheus-based metrics collection system for monitoring InteractEM pipeline performance, operator efficiency, and system status with real-time visualization in Grafana. 4 | 5 | ## Architecture 6 | - **InteractEM** (with Metrics Server on port 8001) exposes metrics 7 | - **Prometheus** (port 9090) scrapes metrics every 5 seconds 8 | - **Grafana** (port 3000) queries Prometheus and displays dashboards 9 | 10 | ## Local Development Environment 11 | 12 | | Service | URL | Description | 13 | |---------|-----|-------------| 14 | | **Metrics Server** | `http://localhost:8001` | Prometheus metrics endpoint | 15 | | **Prometheus** | `http://localhost:9090` | Time-series database and monitoring | 16 | | **Grafana** | `http://localhost:3000` | Visualization and dashboards | 17 | 18 | **Access the Dashboard:** 19 | 1. Navigate to Grafana at `http://localhost:3000` 20 | 2. Go to **Dashboards** → **InteractEM** → **Interactem Pipeline Monitoring** -------------------------------------------------------------------------------- /backend/rdma/libs/thallium/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Fetch the nlohmann_json library 2 | include(FetchContent) 3 | FetchContent_Declare( 4 | json 5 | GIT_REPOSITORY https://github.com/nlohmann/json.git 6 | GIT_TAG v3.11.2 # specify a version tag 7 | ) 8 | FetchContent_MakeAvailable(json) 9 | 10 | # Thallium engine library 11 | add_library(thallium_engine SHARED 12 | src/eng_utils.cpp 13 | src/eng_provider.cpp 14 | # src/eng_dispatcher.cpp 15 | src/eng_registry.cpp 16 | ) 17 | 18 | # Include directories for thallium library 19 | target_include_directories(thallium_engine PUBLIC 20 | ${CMAKE_CURRENT_SOURCE_DIR}/../../common/include 21 | ${CMAKE_CURRENT_SOURCE_DIR}/../argobots/include 22 | ${CMAKE_CURRENT_SOURCE_DIR}/../thallium/include 23 | ) 24 | 25 | # Link external thallium dependency 26 | target_link_libraries(thallium_engine PUBLIC 27 | thallium 28 | nlohmann_json::nlohmann_json 29 | ) 30 | 31 | target_compile_features(thallium_engine PUBLIC cxx_std_17) 32 | -------------------------------------------------------------------------------- /frontend/interactEM/src/client/generated/client.gen.ts: -------------------------------------------------------------------------------- 1 | // This file is auto-generated by @hey-api/openapi-ts 2 | 3 | import { 4 | type Config, 5 | type ClientOptions as DefaultClientOptions, 6 | createClient, 7 | createConfig, 8 | } from "@hey-api/client-axios" 9 | import type { ClientOptions } from "./types.gen" 10 | 11 | /** 12 | * The `createClientConfig()` function will be called on client initialization 13 | * and the returned object will become the client's initial configuration. 14 | * 15 | * You may want to initialize your client this way instead of calling 16 | * `setConfig()`. This is useful for example if you're using Next.js 17 | * to ensure your client always has the correct values. 18 | */ 19 | export type CreateClientConfig = 20 | ( 21 | override?: Config, 22 | ) => Config & T> 23 | 24 | export const client = createClient(createConfig()) 25 | -------------------------------------------------------------------------------- /operators/bin-sparse-partial/operator.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "2af8232a-b0e0-40ea-9757-d63d1ae28555", 3 | "image": "ghcr.io/nersc/interactem/bin-sparse-partial", 4 | "label": "Partial sparse frame binning", 5 | "description": "Bins sparse frames to reduce their overall size", 6 | "inputs": [ 7 | { 8 | "name": "in", 9 | "label": "The input", 10 | "type": "frame", 11 | "description": "A batch of sparse frames" 12 | } 13 | ], 14 | "outputs": [ 15 | { 16 | "name": "frame_bin_partial", 17 | "label": "The output", 18 | "type": "com_partial", 19 | "description": "A batch of binned frames" 20 | } 21 | ], 22 | "parameters": [ 23 | { 24 | "name": "bin_value", 25 | "label": "Binning value", 26 | "type": "int", 27 | "default": "2", 28 | "description": "The value to bin each frame by", 29 | "required": true 30 | } 31 | ], 32 | "parallel_config": { 33 | "type": "embarrassing" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /backend/app/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "workbench.colorCustomizations": { 3 | "activityBar.activeBackground": "#65c89b", 4 | "activityBar.background": "#65c89b", 5 | "activityBar.foreground": "#15202b", 6 | "activityBar.inactiveForeground": "#15202b99", 7 | "activityBarBadge.background": "#945bc4", 8 | "activityBarBadge.foreground": "#e7e7e7", 9 | "commandCenter.border": "#15202b99", 10 | "sash.hoverBorder": "#65c89b", 11 | "statusBar.background": "#42b883", 12 | "statusBar.foreground": "#15202b", 13 | "statusBarItem.hoverBackground": "#359268", 14 | "statusBarItem.remoteBackground": "#42b883", 15 | "statusBarItem.remoteForeground": "#15202b", 16 | "titleBar.activeBackground": "#42b883", 17 | "titleBar.activeForeground": "#15202b", 18 | "titleBar.inactiveBackground": "#42b88399", 19 | "titleBar.inactiveForeground": "#15202b99" 20 | }, 21 | "peacock.remoteColor": "#42b883" 22 | } -------------------------------------------------------------------------------- /backend/rdma/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | 2 | cmake_minimum_required(VERSION 3.15...3.26) 3 | 4 | project(thallium-mochi-rdma VERSION 0.1.0 LANGUAGES CXX) 5 | 6 | # Set C++ standard 7 | set(CMAKE_CXX_STANDARD 17) 8 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 9 | 10 | find_package(thallium REQUIRED) 11 | find_package(PkgConfig REQUIRED) 12 | 13 | add_subdirectory(common) 14 | add_subdirectory(libs) 15 | add_subdirectory(src) 16 | 17 | # Main executable 18 | # add_executable(rdma_proxy main.cpp bindings.cpp) 19 | add_executable(rdma_proxy main.cpp) 20 | 21 | # Include directories for main executable 22 | target_include_directories(rdma_proxy PRIVATE 23 | ${CMAKE_CURRENT_SOURCE_DIR}/include 24 | ${CMAKE_CURRENT_SOURCE_DIR}/common/include 25 | ${CMAKE_CURRENT_SOURCE_DIR}/libs/thallium/include 26 | ${CMAKE_CURRENT_SOURCE_DIR}/libs/argobots/include 27 | ) 28 | 29 | # Link libraries to main executable 30 | target_link_libraries(rdma_proxy PRIVATE 31 | rdma_proxy_core 32 | rdma_external_libs 33 | thallium 34 | ) -------------------------------------------------------------------------------- /operators/electron-count-save/operator.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "12345678-1234-1234-1234-1432567890cd", 3 | "image": "ghcr.io/nersc/interactem/electron-count-save:latest", 4 | "label": "Electron Count Saver", 5 | "description": "Saves electron count to a file", 6 | "inputs": [ 7 | { 8 | "name": "in", 9 | "label": "The input", 10 | "type": "scan", 11 | "description": "Sparse scan" 12 | } 13 | ], 14 | "parameters": [ 15 | { 16 | "name": "output_dir", 17 | "label": "Output directory", 18 | "type": "mount", 19 | "default": "~/ncem_raw_data/counted_data/", 20 | "description": "This is where the electron counted data is saved", 21 | "required": true 22 | }, 23 | { 24 | "name": "suffix", 25 | "label": "Filename suffix", 26 | "type": "str-enum", 27 | "default": "", 28 | "description": "Suffix to add to the filename", 29 | "required": true, 30 | "options": ["", "_counted"] 31 | } 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /frontend/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "biome.enabled": true, 3 | "[javascript]": { 4 | "editor.defaultFormatter": "biomejs.biome", 5 | "editor.formatOnSave": true, 6 | "editor.codeActionsOnSave": { 7 | "source.organizeImports": "explicit" 8 | } 9 | }, 10 | "[javascriptreact]": { 11 | "editor.defaultFormatter": "biomejs.biome", 12 | "editor.formatOnSave": true, 13 | "editor.codeActionsOnSave": { 14 | "source.organizeImports": "explicit" 15 | } 16 | }, 17 | "[typescript]": { 18 | "editor.defaultFormatter": "biomejs.biome", 19 | "editor.formatOnSave": true, 20 | "editor.codeActionsOnSave": { 21 | "source.organizeImports": "explicit" 22 | } 23 | }, 24 | "[typescriptreact]": { 25 | "editor.defaultFormatter": "biomejs.biome", 26 | "editor.formatOnSave": true, 27 | "editor.codeActionsOnSave": { 28 | "source.organizeImports": "explicit" 29 | } 30 | }, 31 | "biome.lsp.bin": "interactEM/node_modules/@biomejs/biome/bin/biome" 32 | } 33 | -------------------------------------------------------------------------------- /operators/read-tem-data/operator.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "389712a9-edb9-4dcb-89d8-c13264a0c356", 3 | "label": "Read TEM data", 4 | "description": "This reads data from disk using ncempy and sends it on.", 5 | "image": "ghcr.io/nersc/interactem/read-tem-data:latest", 6 | "outputs": [ 7 | { 8 | "name": "data", 9 | "label": "Output array", 10 | "description": "Array output", 11 | "type": "bytes" 12 | } 13 | ], 14 | "parameters": [ 15 | { 16 | "name": "raw_data_dir", 17 | "label": "Raw data directory", 18 | "description": "The directory containing the files to read.", 19 | "type": "mount", 20 | "default": "~/test_data", 21 | "required": true 22 | }, 23 | { 24 | "name": "file", 25 | "label": "File name", 26 | "description": "The name of the file to read.", 27 | "type": "str", 28 | "default": "file.emd", 29 | "required": true 30 | } 31 | ], 32 | "parallel_config": { 33 | "type": "none" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /frontend/interactEM/src/components/pipelines/hudlistbutton.tsx: -------------------------------------------------------------------------------- 1 | import { IconButton, Tooltip } from "@mui/material" 2 | import type React from "react" 3 | 4 | interface HudListButtonProps { 5 | tooltip: string 6 | icon: React.ReactNode 7 | onClick: () => void 8 | active?: boolean 9 | } 10 | 11 | export const HudListButton: React.FC = ({ 12 | tooltip, 13 | icon, 14 | onClick, 15 | active = false, 16 | }) => { 17 | return ( 18 | 19 | 32 | {icon} 33 | 34 | 35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /docs/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "interactem-docs" 3 | version = "0.1.0" 4 | description = "interactEM documentation" 5 | authors = [{name = "interactEM developers"}] 6 | readme = "README.md" 7 | requires-python = ">=3.12" 8 | dependencies = [ 9 | "sphinx>=8.1.3,<9", 10 | "sphinx-multiversion>=0.2.4,<1", 11 | "myst-parser>=4.0.0,<5", 12 | "sphinxcontrib-mermaid>=1.0.0,<2", 13 | "sphinx-design>=0.6.1,<1", 14 | "furo>=2025.7.19,<2026", 15 | "sphinx-copybutton>=0.5.2,<1", 16 | "markdown-code-symlinks", 17 | ] 18 | 19 | [tool.uv.sources] 20 | markdown-code-symlinks = { git = "https://github.com/SymbiFlow/sphinxcontrib-markdown-symlinks.git", rev = "4ead1c22270188cd0529741679769c56d9c341bf" } 21 | 22 | [dependency-groups] 23 | dev = [ 24 | "sphinx-autobuild>=2024.10.3,<2025", 25 | ] 26 | 27 | [tool.poetry] 28 | package-mode = false 29 | 30 | [tool.poetry.dependencies] 31 | markdown-code-symlinks = { git = "https://github.com/SymbiFlow/sphinxcontrib-markdown-symlinks.git", rev = "4ead1c22270188cd0529741679769c56d9c341bf" } -------------------------------------------------------------------------------- /backend/app/interactem/app/alembic/versions/60a2bc7c4aea_add_pipeline_name.py: -------------------------------------------------------------------------------- 1 | """add pipeline name 2 | 3 | Revision ID: 60a2bc7c4aea 4 | Revises: 8254e462f00f 5 | Create Date: 2025-04-19 13:04:23.374238 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | import sqlmodel.sql.sqltypes 11 | 12 | 13 | # revision identifiers, used by Alembic. 14 | revision = '60a2bc7c4aea' 15 | down_revision = '8254e462f00f' 16 | branch_labels = None 17 | depends_on = None 18 | 19 | 20 | def upgrade(): 21 | # ### commands auto generated by Alembic - please adjust! ### 22 | op.add_column('pipeline', sa.Column('name', sqlmodel.sql.sqltypes.AutoString(length=128), nullable=True)) 23 | op.create_index(op.f('ix_pipeline_name'), 'pipeline', ['name'], unique=False) 24 | # ### end Alembic commands ### 25 | 26 | 27 | def downgrade(): 28 | # ### commands auto generated by Alembic - please adjust! ### 29 | op.drop_index(op.f('ix_pipeline_name'), table_name='pipeline') 30 | op.drop_column('pipeline', 'name') 31 | # ### end Alembic commands ### 32 | -------------------------------------------------------------------------------- /backend/core/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "interactem-core" 3 | version = "0.1.0" 4 | description = "" 5 | readme = "README.md" 6 | authors = [ 7 | {name = "Sam Welborn", email = "swelborn@lbl.gov"}, 8 | {name = "Chris Harris", email = "cjh@lbl.gov"} 9 | ] 10 | requires-python = ">=3.10" 11 | dependencies = [ 12 | "pydantic>=2.12.3,<3", 13 | "networkx>=3.3,<3.5", 14 | "nats-py>=2.11.0,<3", 15 | "nkeys>=0.2.1,<0.3", 16 | "tenacity>=9.0.0,<10", 17 | "pydantic-settings>=2.10.1,<3", 18 | "faststream[nats] @ git+https://github.com/fil1n/faststream.git@8c14951daec416f22469deaed805ff5db27e3b44", 19 | "anyio>=4.11.0,<5", 20 | ] 21 | 22 | [dependency-groups] 23 | dev = [ 24 | "pytest>=9.0.1,<10", 25 | "pydantic-to-typescript>=2.0.0,<3", 26 | ] 27 | 28 | [tool.poetry] 29 | packages = [{ include = "interactem" }] 30 | 31 | [build-system] 32 | requires = ["poetry-core"] 33 | build-backend = "poetry.core.masonry.api" 34 | 35 | [tool.ruff] 36 | target-version = "py310" 37 | extend = "../../.ruff.toml" 38 | extend-exclude = ["_export.py"] 39 | -------------------------------------------------------------------------------- /scripts/copy-dotenv.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Script to copy all .env.example files to .env in their respective folders 4 | # Only copies if .env doesn't already exist 5 | 6 | set -euo pipefail 7 | 8 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 9 | REPO_ROOT="$(dirname "$SCRIPT_DIR")" 10 | 11 | echo "Searching for .env.example files in $REPO_ROOT..." 12 | echo "" 13 | 14 | copied_count=0 15 | skipped_count=0 16 | 17 | # Find all .env.example files and copy them 18 | while IFS= read -r -d '' env_example_file; do 19 | env_dir="$(dirname "$env_example_file")" 20 | env_file="$env_dir/.env" 21 | 22 | if [ -f "$env_file" ]; then 23 | echo "⊘ Skipped: $env_file (already exists)" 24 | skipped_count=$((skipped_count + 1)) 25 | else 26 | cp "$env_example_file" "$env_file" 27 | echo "✓ Copied: $env_example_file → $env_file" 28 | copied_count=$((copied_count + 1)) 29 | fi 30 | done < <(find "$REPO_ROOT" -name ".env.example" -type f -print0) 31 | 32 | echo "" 33 | echo "Summary:" 34 | echo " Copied: $copied_count" 35 | echo " Skipped: $skipped_count" 36 | -------------------------------------------------------------------------------- /operators/random-image/run.py: -------------------------------------------------------------------------------- 1 | import io 2 | import time 3 | from typing import Any 4 | 5 | import numpy as np 6 | from PIL import Image 7 | 8 | from interactem.core.logger import get_logger 9 | from interactem.core.models.messages import BytesMessage, MessageHeader, MessageSubject 10 | from interactem.operators.operator import operator 11 | 12 | logger = get_logger() 13 | 14 | 15 | @operator 16 | def random_image( 17 | inputs: BytesMessage | None, parameters: dict[str, Any] 18 | ) -> BytesMessage | None: 19 | width = int(parameters.get("width", 100)) 20 | height = int(parameters.get("height", 100)) 21 | interval = int(parameters.get("interval", 2)) 22 | 23 | time.sleep(interval) 24 | 25 | random_data = np.random.randint(0, 256, (height, width, 3), dtype=np.uint8) 26 | image = Image.fromarray(random_data, "RGB") 27 | byte_array = io.BytesIO() 28 | image.save(byte_array, format="JPEG") 29 | byte_array.seek(0) 30 | header = MessageHeader(subject=MessageSubject.BYTES, meta={}) 31 | 32 | return BytesMessage(header=header, data=byte_array.getvalue()) 33 | -------------------------------------------------------------------------------- /operators/random-image/operator.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "92345678-1234-1234-1234-1234567890ab", 3 | "image": "ghcr.io/nersc/interactem/random-image:latest", 4 | "label": "Random Image", 5 | "description": "Generates a random image", 6 | "outputs": [ 7 | { 8 | "name": "out", 9 | "label": "The image", 10 | "type": "image", 11 | "description": "The image" 12 | } 13 | ], 14 | "parameters": [ 15 | { 16 | "name": "width", 17 | "label": "Image width", 18 | "type": "int", 19 | "default": "100", 20 | "description": "The width of the image", 21 | "required": true 22 | }, 23 | { 24 | "name": "height", 25 | "label": "Image height", 26 | "type": "int", 27 | "default": "100", 28 | "description": "The height of the image", 29 | "required": true 30 | }, 31 | { 32 | "name": "interval", 33 | "label": "Interval", 34 | "type": "int", 35 | "default": "2", 36 | "description": "The interval at which to generate the image", 37 | "required": true 38 | } 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /backend/callout/service/go.mod: -------------------------------------------------------------------------------- 1 | module distiller-callout 2 | 3 | go 1.24.0 4 | 5 | require ( 6 | github.com/aricart/callout.go v0.2.0 7 | github.com/go-playground/validator/v10 v10.25.0 8 | github.com/golang-jwt/jwt/v5 v5.2.2 9 | github.com/joho/godotenv v1.5.1 10 | github.com/nats-io/jwt/v2 v2.7.3 11 | github.com/nats-io/nats-server/v2 v2.11.1 12 | github.com/nats-io/nats.go v1.39.1 13 | github.com/nats-io/nkeys v0.4.10 14 | ) 15 | 16 | require ( 17 | github.com/gabriel-vasile/mimetype v1.4.8 // indirect 18 | github.com/go-playground/locales v0.14.1 // indirect 19 | github.com/go-playground/universal-translator v0.18.1 // indirect 20 | github.com/google/go-tpm v0.9.3 // indirect 21 | github.com/klauspost/compress v1.18.0 // indirect 22 | github.com/leodido/go-urn v1.4.0 // indirect 23 | github.com/minio/highwayhash v1.0.3 // indirect 24 | github.com/nats-io/nuid v1.0.1 // indirect 25 | golang.org/x/crypto v0.45.0 // indirect 26 | golang.org/x/net v0.47.0 // indirect 27 | golang.org/x/sys v0.38.0 // indirect 28 | golang.org/x/text v0.31.0 // indirect 29 | golang.org/x/time v0.11.0 // indirect 30 | ) 31 | -------------------------------------------------------------------------------- /backend/app/interactem/app/api/routes/operators.py: -------------------------------------------------------------------------------- 1 | 2 | from fastapi import APIRouter, Query 3 | 4 | from interactem.app.api.deps import CurrentUser 5 | from interactem.app.models import OperatorSpecs 6 | from interactem.app.operators import fetch_operators 7 | from interactem.core.logger import get_logger 8 | 9 | logger = get_logger() 10 | router = APIRouter() 11 | 12 | _operators = None 13 | 14 | 15 | @router.get("/", response_model=OperatorSpecs) 16 | async def read_operators( 17 | current_user: CurrentUser, 18 | refresh: bool = Query(False, description="Force refresh of operators cache"), 19 | ) -> OperatorSpecs: 20 | """ 21 | Retrieve available operators. Use refresh=true to invalidate cache and fetch fresh data. 22 | """ 23 | global _operators 24 | 25 | if refresh or _operators is None: 26 | if refresh: 27 | logger.info("Refreshing operators cache due to refresh parameter") 28 | _operators = await fetch_operators() 29 | 30 | ops = OperatorSpecs(data=_operators) 31 | logger.info(f"Operators found: {[op.image for op in ops.data]}") 32 | return ops 33 | -------------------------------------------------------------------------------- /frontend/interactEM/src/hooks/nats/useTableData.ts: -------------------------------------------------------------------------------- 1 | import { STREAM_TABLES } from "../../constants/nats" 2 | import { useStreamMessage } from "./useStreamMessage" 3 | 4 | export interface TableRow { 5 | [key: string]: string | number | boolean | null 6 | } 7 | 8 | export interface TablesDict { 9 | [tableName: string]: TableRow[] 10 | } 11 | 12 | export interface TablePayload { 13 | [tables: string]: TablesDict 14 | } 15 | 16 | export const useTableData = (operatorID: string): TablePayload | null => { 17 | const subject = `${STREAM_TABLES}.${operatorID}` 18 | 19 | const { data } = useStreamMessage({ 20 | streamName: STREAM_TABLES, 21 | subject, 22 | transform: (jsonData) => { 23 | // Basic validation: Check if it's a non-null object 24 | if (typeof jsonData !== "object" || jsonData === null) { 25 | console.error( 26 | `Received invalid table data structure for ${operatorID}: Expected an object, got ${typeof jsonData}.`, 27 | ) 28 | return null 29 | } 30 | return jsonData as TablePayload 31 | }, 32 | }) 33 | 34 | return data 35 | } 36 | -------------------------------------------------------------------------------- /backend/app/interactem/app/tests_pre_start.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from sqlalchemy import Engine 4 | from sqlmodel import Session, select 5 | from tenacity import after_log, before_log, retry, stop_after_attempt, wait_fixed 6 | 7 | from interactem.app.core.db import engine 8 | 9 | logging.basicConfig(level=logging.INFO) 10 | logger = logging.getLogger(__name__) 11 | 12 | max_tries = 60 * 5 # 5 minutes 13 | wait_seconds = 1 14 | 15 | 16 | @retry( 17 | stop=stop_after_attempt(max_tries), 18 | wait=wait_fixed(wait_seconds), 19 | before=before_log(logger, logging.INFO), 20 | after=after_log(logger, logging.WARN), 21 | ) 22 | def init(db_engine: Engine) -> None: 23 | try: 24 | # Try to create session to check if DB is awake 25 | with Session(db_engine) as session: 26 | session.exec(select(1)) 27 | except Exception as e: 28 | logger.error(e) 29 | raise e 30 | 31 | 32 | def main() -> None: 33 | logger.info("Initializing service") 34 | init(engine) 35 | logger.info("Service finished initializing") 36 | 37 | 38 | if __name__ == "__main__": 39 | main() 40 | -------------------------------------------------------------------------------- /backend/callout/test/run.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from core.nats import create_or_update_stream, nc 4 | from core.nats.streams import AGENTS_STREAM_CONFIG 5 | from nats.aio.client import Client as NATSClient 6 | from pydantic_settings import BaseSettings, SettingsConfigDict 7 | 8 | 9 | class Settings(BaseSettings): 10 | model_config = SettingsConfigDict(env_file=".env", extra="ignore") 11 | DISTILLER_TOKEN: str 12 | 13 | 14 | async def main(): 15 | client = await nc(servers=["nats://localhost:4222"], name="test-nkeys") 16 | js = client.jetstream() 17 | await create_or_update_stream(AGENTS_STREAM_CONFIG, js) 18 | await client.close() 19 | cfg = Settings() # type: ignore 20 | 21 | client = NATSClient() 22 | await client.connect( 23 | servers=["nats://localhost:4222"], name="test-token", token=cfg.DISTILLER_TOKEN 24 | ) 25 | js = client.jetstream() 26 | await create_or_update_stream(AGENTS_STREAM_CONFIG, js) 27 | 28 | await asyncio.sleep(3) 29 | await create_or_update_stream(AGENTS_STREAM_CONFIG, js) 30 | 31 | 32 | if __name__ == "__main__": 33 | asyncio.run(main()) 34 | -------------------------------------------------------------------------------- /backend/app/interactem/app/alembic/versions/ac46c9f37d67_add_pipelines_table.py: -------------------------------------------------------------------------------- 1 | """Add pipelines table 2 | 3 | Revision ID: ac46c9f37d67 4 | Revises: 1a31ce608336 5 | Create Date: 2024-08-09 14:13:21.725389 6 | 7 | """ 8 | import sqlalchemy as sa 9 | from alembic import op 10 | 11 | # revision identifiers, used by Alembic. 12 | revision = 'ac46c9f37d67' 13 | down_revision = '1a31ce608336' 14 | branch_labels = None 15 | depends_on = None 16 | 17 | 18 | def upgrade(): 19 | # ### commands auto generated by Alembic - please adjust! ### 20 | op.create_table('pipeline', 21 | sa.Column('data', sa.JSON(), nullable=True), 22 | sa.Column('running', sa.Boolean(), nullable=False), 23 | sa.Column('id', sa.Uuid(), nullable=False), 24 | sa.Column('owner_id', sa.Uuid(), nullable=False), 25 | sa.ForeignKeyConstraint(['owner_id'], ['user.id'], ondelete='CASCADE'), 26 | sa.PrimaryKeyConstraint('id') 27 | ) 28 | # ### end Alembic commands ### 29 | 30 | 31 | def downgrade(): 32 | # ### commands auto generated by Alembic - please adjust! ### 33 | op.drop_table('pipeline') 34 | # ### end Alembic commands ### 35 | -------------------------------------------------------------------------------- /backend/app/interactem/app/backend_pre_start.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from sqlalchemy import Engine 4 | from sqlmodel import Session, select 5 | from tenacity import after_log, before_log, retry, stop_after_attempt, wait_fixed 6 | 7 | from interactem.app.core.db import engine 8 | 9 | logging.basicConfig(level=logging.INFO) 10 | logger = logging.getLogger(__name__) 11 | 12 | max_tries = 60 * 5 # 5 minutes 13 | wait_seconds = 1 14 | 15 | 16 | @retry( 17 | stop=stop_after_attempt(max_tries), 18 | wait=wait_fixed(wait_seconds), 19 | before=before_log(logger, logging.INFO), 20 | after=after_log(logger, logging.WARN), 21 | ) 22 | def init(db_engine: Engine) -> None: 23 | try: 24 | with Session(db_engine) as session: 25 | # Try to create session to check if DB is awake 26 | session.exec(select(1)) 27 | except Exception as e: 28 | logger.error(e) 29 | raise e 30 | 31 | 32 | def main() -> None: 33 | logger.info("Initializing service") 34 | init(engine) 35 | logger.info("Service finished initializing") 36 | 37 | 38 | if __name__ == "__main__": 39 | main() 40 | -------------------------------------------------------------------------------- /frontend/interactEM/src/components/image.tsx: -------------------------------------------------------------------------------- 1 | import { styled } from "@mui/material/styles" 2 | import type React from "react" 3 | import { useEffect, useState } from "react" 4 | 5 | interface ImageProps { 6 | imageData: Uint8Array | null 7 | } 8 | 9 | interface ImgProps { 10 | width?: string 11 | height?: string 12 | } 13 | 14 | const Img = styled("img")(({ width = "100%", height = "100%" }) => ({ 15 | width, 16 | height, 17 | objectFit: "contain", 18 | })) 19 | 20 | const Image: React.FC = ({ imageData }) => { 21 | const [imageSrc, setImageSrc] = useState(null) 22 | 23 | useEffect(() => { 24 | if (imageData) { 25 | const url = URL.createObjectURL( 26 | // TODO: Pass the MIME type with the image data 27 | new Blob([new Uint8Array(imageData)], { type: "image/jpeg" }), 28 | ) 29 | setImageSrc(url) 30 | 31 | return () => { 32 | URL.revokeObjectURL(url) 33 | } 34 | } 35 | }, [imageData]) 36 | 37 | return ( 38 |
{imageSrc ? :

Waiting...

}
39 | ) 40 | } 41 | 42 | export default Image 43 | -------------------------------------------------------------------------------- /frontend/interactEM/src/auth/api.ts: -------------------------------------------------------------------------------- 1 | import { QueryClient } from "@tanstack/react-query" 2 | import { AUTH_QUERY_KEYS } from "../constants/tanstack" 3 | 4 | const queryClient = new QueryClient() 5 | 6 | type LoginResult = { 7 | success: boolean 8 | error?: Error 9 | } 10 | 11 | export async function loginInteractem( 12 | external_token: string, 13 | ): Promise { 14 | try { 15 | queryClient.setQueryData(AUTH_QUERY_KEYS.externalToken, external_token) 16 | // Trigger a refresh of the internal auth 17 | await queryClient.invalidateQueries({ 18 | queryKey: AUTH_QUERY_KEYS.internalToken, 19 | }) 20 | 21 | return { success: true } 22 | } catch (error) { 23 | console.error("Failed to login to InteractEM:", error) 24 | return { 25 | success: false, 26 | error: 27 | error instanceof Error ? error : new Error("Unknown error occurred"), 28 | } 29 | } 30 | } 31 | 32 | // We want to use the interactemQueryClient since we cannot use useQueryClient() 33 | // outside of a component (i.e., inside of the async function that we want to call) 34 | export { queryClient as interactemQueryClient } 35 | -------------------------------------------------------------------------------- /backend/launcher/interactem/launcher/config.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from pydantic import AnyWebsocketUrl, NatsDsn, model_validator 4 | from pydantic_settings import BaseSettings, SettingsConfigDict 5 | 6 | 7 | class Settings(BaseSettings): 8 | model_config = SettingsConfigDict(env_file=".env", extra="ignore") 9 | NATS_SERVER_URL: AnyWebsocketUrl | NatsDsn = NatsDsn("nats://localhost:4222") 10 | SFAPI_KEY_PATH: Path = Path("/secrets/sfapi.pem") 11 | AGENT_PROJECT_DIR: Path 12 | ENV_FILE_PATH: Path 13 | ENV_FILE_DIR: Path | None = None 14 | SFAPI_ACCOUNT: str 15 | SFAPI_QOS: str 16 | 17 | @model_validator(mode="after") 18 | def resolve_path(self) -> "Settings": 19 | self.SFAPI_KEY_PATH = self.SFAPI_KEY_PATH.expanduser().resolve() 20 | if not self.SFAPI_KEY_PATH.is_file(): 21 | raise ValueError(f"File not found: {self.SFAPI_KEY_PATH}") 22 | return self 23 | 24 | @model_validator(mode="after") 25 | def env_file_parent(self) -> "Settings": 26 | self.ENV_FILE_DIR = self.ENV_FILE_PATH.parent 27 | return self 28 | 29 | 30 | cfg = Settings() # type: ignore 31 | -------------------------------------------------------------------------------- /frontend/interactEM/tests/e2e/fixtures/auth.ts: -------------------------------------------------------------------------------- 1 | import { expect, test as base, type Page } from "@playwright/test" 2 | 3 | const username = process.env.FIRST_SUPERUSER_USERNAME 4 | const password = process.env.FIRST_SUPERUSER_PASSWORD 5 | 6 | async function login(page: Page) { 7 | if (!username || !password) { 8 | throw new Error( 9 | "FIRST_SUPERUSER_USERNAME and FIRST_SUPERUSER_PASSWORD must be set for Playwright tests", 10 | ) 11 | } 12 | 13 | await page.goto("/", { waitUntil: "domcontentloaded" }) 14 | await page.waitForSelector("text=Login") 15 | await page.getByLabel("Username").fill(username) 16 | await page.getByLabel("Password").fill(password) 17 | await page.getByRole("button", { name: /login/i }).click() 18 | await page.waitForSelector(".composer-page", { timeout: 20_000 }) 19 | await expect(page.locator(".composer-page")).toBeVisible() 20 | } 21 | 22 | type Fixtures = { 23 | authPage: Page 24 | } 25 | 26 | export const test = base.extend({ 27 | authPage: async ({ page }, use) => { 28 | await login(page) 29 | await use(page) 30 | }, 31 | }) 32 | 33 | export { expect } from "@playwright/test" 34 | -------------------------------------------------------------------------------- /operators/data-replay/operator.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "12345678-0001-0001-0000-1234567890ab", 3 | "image": "ghcr.io/nersc/interactem/data-replay", 4 | "label": "Data Replayer", 5 | "description": "Data replayer for NCEM data. Choose a raw data directory to mount, and replay it through the pipeline", 6 | "outputs": [ 7 | { 8 | "name": "out", 9 | "label": "The output", 10 | "type": "frame", 11 | "description": "Full frame" 12 | } 13 | ], 14 | "parameters": [ 15 | { 16 | "name": "raw_data_dir", 17 | "label": "Raw data directory", 18 | "type": "mount", 19 | "default": "~/ncem_raw_data", 20 | "description": "This is where the raw data files (*.data) are located", 21 | "required": true 22 | }, 23 | { 24 | "name": "scan_num", 25 | "label": "Scan number", 26 | "type": "int", 27 | "default": "0", 28 | "description": "The scan number to be processed", 29 | "required": true 30 | } 31 | ], 32 | "tags": [ 33 | { 34 | "value": "ncem-4dcamera", 35 | "description": "Required to run at the edge near the 4d camera." 36 | } 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /backend/app/interactem/app/tests/scripts/test_test_pre_start.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import MagicMock, patch 2 | 3 | from sqlmodel import select 4 | 5 | from interactem.app.tests_pre_start import init, logger 6 | 7 | 8 | def test_init_successful_connection() -> None: 9 | engine_mock = MagicMock() 10 | 11 | session_mock = MagicMock() 12 | exec_mock = MagicMock(return_value=True) 13 | session_mock.configure_mock(**{"exec.return_value": exec_mock}) 14 | 15 | with ( 16 | patch("sqlmodel.Session", return_value=session_mock), 17 | patch.object(logger, "info"), 18 | patch.object(logger, "error"), 19 | patch.object(logger, "warn"), 20 | ): 21 | try: 22 | init(engine_mock) 23 | connection_successful = True 24 | except Exception: 25 | connection_successful = False 26 | 27 | assert ( 28 | connection_successful 29 | ), "The database connection should be successful and not raise an exception." 30 | 31 | assert session_mock.exec.called_once_with( 32 | select(1) 33 | ), "The session should execute a select statement once." 34 | -------------------------------------------------------------------------------- /backend/operators/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "editor.formatOnSaveMode": "modifications", 4 | "editor.defaultFormatter": "charliermarsh.ruff", 5 | "workbench.colorCustomizations": { 6 | "activityBar.activeBackground": "#3399ff", 7 | "activityBar.background": "#3399ff", 8 | "activityBar.foreground": "#15202b", 9 | "activityBar.inactiveForeground": "#15202b99", 10 | "activityBarBadge.background": "#bf0060", 11 | "activityBarBadge.foreground": "#e7e7e7", 12 | "commandCenter.border": "#e7e7e799", 13 | "sash.hoverBorder": "#3399ff", 14 | "statusBar.background": "#007fff", 15 | "statusBar.foreground": "#e7e7e7", 16 | "statusBarItem.hoverBackground": "#3399ff", 17 | "statusBarItem.remoteBackground": "#007fff", 18 | "statusBarItem.remoteForeground": "#e7e7e7", 19 | "titleBar.activeBackground": "#007fff", 20 | "titleBar.activeForeground": "#e7e7e7", 21 | "titleBar.inactiveBackground": "#007fff99", 22 | "titleBar.inactiveForeground": "#e7e7e799" 23 | }, 24 | "peacock.remoteColor": "#007fff", 25 | } -------------------------------------------------------------------------------- /backend/app/interactem/app/tests/scripts/test_backend_pre_start.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import MagicMock, patch 2 | 3 | from sqlmodel import select 4 | 5 | from interactem.app.backend_pre_start import init, logger 6 | 7 | 8 | def test_init_successful_connection() -> None: 9 | engine_mock = MagicMock() 10 | 11 | session_mock = MagicMock() 12 | exec_mock = MagicMock(return_value=True) 13 | session_mock.configure_mock(**{"exec.return_value": exec_mock}) 14 | 15 | with ( 16 | patch("sqlmodel.Session", return_value=session_mock), 17 | patch.object(logger, "info"), 18 | patch.object(logger, "error"), 19 | patch.object(logger, "warn"), 20 | ): 21 | try: 22 | init(engine_mock) 23 | connection_successful = True 24 | except Exception: 25 | connection_successful = False 26 | 27 | assert ( 28 | connection_successful 29 | ), "The database connection should be successful and not raise an exception." 30 | 31 | assert session_mock.exec.called_once_with( 32 | select(1) 33 | ), "The session should execute a select statement once." 34 | -------------------------------------------------------------------------------- /backend/agent/interactem/agent/util.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | import os 4 | import shutil 5 | import subprocess 6 | 7 | 8 | # GPU utils 9 | # ============================================================================= 10 | def detect_gpu_enabled() -> bool: 11 | visible_devices = ( 12 | os.getenv("CUDA_VISIBLE_DEVICES") or os.getenv("NVIDIA_VISIBLE_DEVICES") or "" 13 | ).strip() 14 | if visible_devices and visible_devices.lower() not in {"none", "void", "-1"}: 15 | return True 16 | 17 | if os.path.isdir("/proc/driver/nvidia/gpus"): 18 | return True 19 | 20 | for path in ("/dev/nvidiactl", "/dev/nvidia0", "/dev/nvidia-uvm"): 21 | if os.path.exists(path): 22 | return True 23 | 24 | nvidia_smi = shutil.which("nvidia-smi") 25 | if not nvidia_smi: 26 | return False 27 | 28 | try: 29 | proc = subprocess.run( 30 | [nvidia_smi, "-L"], 31 | stdout=subprocess.PIPE, 32 | stderr=subprocess.DEVNULL, 33 | check=False, 34 | timeout=1.5, 35 | ) 36 | except (subprocess.TimeoutExpired, OSError): 37 | return False 38 | 39 | return proc.returncode == 0 and bool(proc.stdout.strip()) 40 | -------------------------------------------------------------------------------- /frontend/interactEM/src/components/nodes/parametersbutton.tsx: -------------------------------------------------------------------------------- 1 | import SettingsIcon from "@mui/icons-material/Settings" 2 | import { Typography } from "@mui/material" 3 | import type React from "react" 4 | import type { OperatorSpecParameter } from "../../client" 5 | import NodeModalButton from "./nodemodalbutton" 6 | import ParameterUpdater from "./parameterupdater" 7 | 8 | const ParametersButton: React.FC<{ 9 | operatorID: string 10 | parameters: OperatorSpecParameter[] 11 | nodeRef: React.RefObject 12 | }> = ({ operatorID, parameters, nodeRef }) => { 13 | return ( 14 | } 17 | label="Parameters" 18 | title={null} 19 | > 20 | {parameters.map((param) => ( 21 | 26 | ))} 27 | {parameters.length === 0 && ( 28 | 29 | No parameters available. 30 | 31 | )} 32 | 33 | ) 34 | } 35 | 36 | export default ParametersButton 37 | -------------------------------------------------------------------------------- /operators/detstream-producer/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "producer": { 3 | "client_type": "producer", 4 | "connect": { 5 | "state_hostname": "192.168.127.2", 6 | "state_port": 15000 7 | }, 8 | "process": { 9 | "num_processes": 4, 10 | "num_threads_per_process": 1, 11 | "io_thread_affinity": true 12 | }, 13 | "upstream": { 14 | "client_type": "none" 15 | }, 16 | "downstream": { 17 | "first_bound_port": 6001, 18 | "hostname_bind": [ 19 | "192.168.127.2", 20 | "192.168.127.2", 21 | "192.168.127.2", 22 | "192.168.127.2" 23 | ], 24 | "max_num_downstream_processes": 1 25 | }, 26 | "sockopts": { 27 | "sndhwm": 1000, 28 | "io_thread_affinity": true, 29 | "sndtimeo": 10000 30 | }, 31 | "ctxopts": { 32 | "scheduler": "SCHED_OTHER", 33 | "io_threads": 1 34 | }, 35 | "filepaths": { 36 | "status_json_output_dir": "/mnt/data/", 37 | "save_raw_data_dir": "/output" 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /backend/app/interactem/app/alembic/versions/1a31ce608336_add_cascade_delete_relationships.py: -------------------------------------------------------------------------------- 1 | """Add cascade delete relationships 2 | 3 | Revision ID: 1a31ce608336 4 | Revises: d98dd8ec85a3 5 | Create Date: 2024-07-31 22:24:34.447891 6 | 7 | """ 8 | 9 | import sqlalchemy as sa 10 | from alembic import op 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "1a31ce608336" 14 | down_revision = "d98dd8ec85a3" 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.alter_column("item", "owner_id", existing_type=sa.UUID(), nullable=False) 22 | op.drop_constraint("item_owner_id_fkey", "item", type_="foreignkey") 23 | op.create_foreign_key( 24 | None, "item", "user", ["owner_id"], ["id"], ondelete="CASCADE" 25 | ) 26 | # ### end Alembic commands ### 27 | 28 | 29 | def downgrade(): 30 | # ### commands auto generated by Alembic - please adjust! ### 31 | op.drop_constraint(None, "item", type_="foreignkey") 32 | op.create_foreign_key("item_owner_id_fkey", "item", "user", ["owner_id"], ["id"]) 33 | op.alter_column("item", "owner_id", existing_type=sa.UUID(), nullable=True) 34 | # ### end Alembic commands ### 35 | -------------------------------------------------------------------------------- /backend/launcher/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "interactem-launcher" 3 | version = "0.1.0" 4 | description = "" 5 | authors = [ 6 | {name = "Sam Welborn", email = "swelborn@lbl.gov"}, 7 | {name = "Chris Harris", email = "cjh@lbl.gov"} 8 | ] 9 | requires-python = ">=3.10" 10 | readme = "README.md" 11 | dependencies = [ 12 | "interactem-core", 13 | "interactem-sfapi-models", 14 | "pydantic-settings>=2.4.0,<3", 15 | "sfapi-client>=0.4,<1", 16 | "h11>=0.16.0,<1", 17 | "jinja2>=3.1,<4", 18 | ] 19 | 20 | [tool.uv.sources] 21 | interactem-core = { path = "../core", editable = true } 22 | interactem-sfapi-models = { path = "../sfapi_models", editable = true } 23 | 24 | [dependency-groups] 25 | dev = [ 26 | "pytest>=9.0.1,<10", 27 | "pytest-asyncio>=1.0.0,<2", 28 | "python-dotenv[cli]>=1.0.1,<2", 29 | ] 30 | 31 | [tool.poetry] 32 | packages = [{ include = "interactem" }] 33 | 34 | [tool.poetry.dependencies] 35 | interactem-core = {path = "../core", develop = true} 36 | interactem-sfapi-models = {path = "../sfapi_models", develop = true} 37 | 38 | [build-system] 39 | requires = ["poetry-core"] 40 | build-backend = "poetry.core.masonry.api" 41 | 42 | [tool.ruff] 43 | target-version = "py310" 44 | extend = "../../.ruff.toml" -------------------------------------------------------------------------------- /backend/agent/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "editor.formatOnSaveMode": "modifications", 4 | "editor.defaultFormatter": "charliermarsh.ruff", 5 | "editor.codeActionsOnSave": { 6 | "source.organizeImports": "explicit" 7 | }, 8 | "workbench.colorCustomizations": { 9 | "activityBar.activeBackground": "#28693c", 10 | "activityBar.background": "#28693c", 11 | "activityBar.foreground": "#e7e7e7", 12 | "activityBar.inactiveForeground": "#e7e7e799", 13 | "activityBarBadge.background": "#35235c", 14 | "activityBarBadge.foreground": "#e7e7e7", 15 | "commandCenter.border": "#e7e7e799", 16 | "sash.hoverBorder": "#28693c", 17 | "statusBar.background": "#1a4427", 18 | "statusBar.foreground": "#e7e7e7", 19 | "statusBarItem.hoverBackground": "#28693c", 20 | "statusBarItem.remoteBackground": "#1a4427", 21 | "statusBarItem.remoteForeground": "#e7e7e7", 22 | "titleBar.activeBackground": "#1a4427", 23 | "titleBar.activeForeground": "#e7e7e7", 24 | "titleBar.inactiveBackground": "#1a442799", 25 | "titleBar.inactiveForeground": "#e7e7e799" 26 | }, 27 | "peacock.remoteColor": "#1a4427" 28 | } -------------------------------------------------------------------------------- /backend/rdma/libs/thallium/src/eng_provider.cpp: -------------------------------------------------------------------------------- 1 | #include "eng_provider.hpp" 2 | 3 | namespace interactEM { 4 | 5 | EngProvider::~EngProvider(){ 6 | this->stop(); 7 | } 8 | 9 | // void EngProvider::initialize(){ 10 | // m_registry_.pushCxiAddress("default_agent", "default_operator", "default_cxi_address"); 11 | // } 12 | 13 | // Stop the RDMA engine 14 | void EngProvider::stop() { 15 | m_rdma_pull.deregister(); 16 | getEngine().pop_finalize_callback(this); 17 | } 18 | 19 | // RDMA push operation 20 | void EngProvider::rdma_pull(const tl::request& req, tl::bulk& b) { 21 | // std::cout << "Executing RDMA push operation..." << std::endl; 22 | tl::endpoint ep = req.get_endpoint(); 23 | std::vector v(b.size()); 24 | std::vector> segments = EngUtils::create_segments(v); 25 | tl::bulk local = getEngine().expose(segments, tl::bulk_mode::write_only); 26 | // std::cout << "Exposing bulk with size: " << b.size() << std::endl; 27 | b.on(ep) >> local; 28 | // std::cout << "Server received bulk: "; 29 | // for(auto c : v) std::cout << c; 30 | // std::cout << std::endl; 31 | // std::cout << "RDMA push operation completed successfully." << std::endl; 32 | req.respond(); 33 | } 34 | 35 | } -------------------------------------------------------------------------------- /frontend/interactEM/playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { devices, defineConfig } from '@playwright/test'; 2 | 3 | const port = process.env.PLAYWRIGHT_PORT ?? '5174'; 4 | const baseURL = process.env.PLAYWRIGHT_BASE_URL ?? `http://localhost:${port}`; 5 | 6 | export default defineConfig({ 7 | testDir: './tests', 8 | fullyParallel: true, 9 | forbidOnly: !!process.env.CI, 10 | retries: process.env.CI ? 2 : 0, 11 | workers: process.env.CI ? 1 : undefined, 12 | reporter: 13 | process.env.CI === 'true' 14 | ? [['line'], ['github']] 15 | : [ 16 | ['line'], 17 | ['html', { open: 'on-failure', outputFolder: 'playwright-report' }], 18 | ], 19 | use: { 20 | baseURL, 21 | trace: 'on-first-retry', 22 | }, 23 | webServer: { 24 | command: `npm run dev -- --host --port ${port}`, 25 | url: baseURL, 26 | reuseExistingServer: !process.env.CI, 27 | timeout: 60_000, 28 | }, 29 | projects: [ 30 | { 31 | name: 'chromium', 32 | use: { ...devices['Desktop Chrome'] }, 33 | }, 34 | { 35 | name: 'firefox', 36 | use: { ...devices['Desktop Firefox'] }, 37 | }, 38 | { 39 | name: 'webkit', 40 | use: { ...devices['Desktop Safari'] }, 41 | }, 42 | ], 43 | }); 44 | -------------------------------------------------------------------------------- /backend/orchestrator/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "editor.formatOnSaveMode": "modifications", 4 | "editor.defaultFormatter": "charliermarsh.ruff", 5 | "editor.codeActionsOnSave": { 6 | "source.organizeImports": "explicit" 7 | }, 8 | "workbench.colorCustomizations": { 9 | "activityBar.activeBackground": "#cc3100", 10 | "activityBar.background": "#cc3100", 11 | "activityBar.foreground": "#e7e7e7", 12 | "activityBar.inactiveForeground": "#e7e7e799", 13 | "activityBarBadge.background": "#00bf2e", 14 | "activityBarBadge.foreground": "#e7e7e7", 15 | "commandCenter.border": "#e7e7e799", 16 | "sash.hoverBorder": "#cc3100", 17 | "statusBar.background": "#992500", 18 | "statusBar.foreground": "#e7e7e7", 19 | "statusBarItem.hoverBackground": "#cc3100", 20 | "statusBarItem.remoteBackground": "#992500", 21 | "statusBarItem.remoteForeground": "#e7e7e7", 22 | "titleBar.activeBackground": "#992500", 23 | "titleBar.activeForeground": "#e7e7e7", 24 | "titleBar.inactiveBackground": "#99250099", 25 | "titleBar.inactiveForeground": "#e7e7e799" 26 | }, 27 | "peacock.remoteColor": "#992500", 28 | } -------------------------------------------------------------------------------- /frontend/interactEM/src/components/nodes/table.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Typography } from "@mui/material" 2 | import type { NodeProps } from "@xyflow/react" 3 | import { useRef } from "react" 4 | import { useRuntimeOperatorStatusStyles } from "../../hooks/nats/useOperatorStatus" 5 | import { useTableData } from "../../hooks/nats/useTableData" 6 | import type { TableNodeType } from "../../types/nodes" 7 | import TableView from "../table" 8 | import Handles from "./handles" 9 | 10 | interface TableNodeBaseProps extends NodeProps { 11 | className?: string 12 | } 13 | 14 | const TableNodeBase = ({ id, data, className = "" }: TableNodeBaseProps) => { 15 | const nodeRef = useRef(null) 16 | const tablePayload = useTableData(id) 17 | const { statusClass } = useRuntimeOperatorStatusStyles(id) 18 | 19 | return ( 20 | 21 | 22 | 23 | {tablePayload ? "" : "Waiting for table data..."} 24 | 25 | 26 | 27 | ) 28 | } 29 | 30 | const TableNode = TableNodeBase 31 | 32 | export default TableNode 33 | -------------------------------------------------------------------------------- /operators/dpc/operator.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "585e9605-088e-4190-8b21-a8338cddd39b", 3 | "image": "ghcr.io/nersc/interactem/dpc", 4 | "label": "DPC", 5 | "description": "Differential Phase Contrast", 6 | "inputs": [ 7 | { 8 | "name": "in", 9 | "label": "The input", 10 | "type": "com", 11 | "description": "Center of Mass" 12 | } 13 | ], 14 | "outputs": [ 15 | { 16 | "name": "image", 17 | "label": "Output matplotlib image", 18 | "type": "image", 19 | "description": "Differential Phase Contrast image" 20 | } 21 | ], 22 | "parameters": [ 23 | { 24 | "name": "flip", 25 | "type": "bool", 26 | "description": "Whether to flip the image", 27 | "default": "true", 28 | "label": "Flip", 29 | "required": true 30 | }, 31 | { 32 | "name": "theta", 33 | "type": "float", 34 | "description": "Angle to rotate the image", 35 | "default": "-9", 36 | "label": "Theta", 37 | "required": true 38 | }, 39 | { 40 | "name": "reg", 41 | "type": "float", 42 | "description": "Regularization parameter", 43 | "default": "0.1", 44 | "label": "Regularization", 45 | "required": true 46 | } 47 | ] 48 | } 49 | -------------------------------------------------------------------------------- /backend/app/interactem/app/alembic/versions/516457e800b5_remove_items.py: -------------------------------------------------------------------------------- 1 | """Remove items 2 | 3 | Revision ID: 516457e800b5 4 | Revises: ac46c9f37d67 5 | Create Date: 2024-08-22 14:06:51.334874 6 | 7 | """ 8 | import sqlalchemy as sa 9 | from alembic import op 10 | 11 | # revision identifiers, used by Alembic. 12 | revision = '516457e800b5' 13 | down_revision = 'ac46c9f37d67' 14 | branch_labels = None 15 | depends_on = None 16 | 17 | 18 | def upgrade(): 19 | # ### commands auto generated by Alembic - please adjust! ### 20 | op.drop_table('item') 21 | # ### end Alembic commands ### 22 | 23 | 24 | def downgrade(): 25 | # ### commands auto generated by Alembic - please adjust! ### 26 | op.create_table('item', 27 | sa.Column('description', sa.VARCHAR(length=255), autoincrement=False, nullable=True), 28 | sa.Column('title', sa.VARCHAR(length=255), autoincrement=False, nullable=False), 29 | sa.Column('id', sa.UUID(), autoincrement=False, nullable=False), 30 | sa.Column('owner_id', sa.UUID(), autoincrement=False, nullable=False), 31 | sa.ForeignKeyConstraint(['owner_id'], ['user.id'], name='item_owner_id_fkey', ondelete='CASCADE'), 32 | sa.PrimaryKeyConstraint('id', name='item_pkey') 33 | ) 34 | # ### end Alembic commands ### 35 | -------------------------------------------------------------------------------- /frontend/interactEM/src/hooks/api/useOperatorSpecs.ts: -------------------------------------------------------------------------------- 1 | import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" 2 | import { operatorsReadOperatorsOptions } from "../../client" 3 | import { zOperatorSpecs } from "../../client/generated/zod.gen" 4 | 5 | export const useOperatorSpecs = () => { 6 | const queryClient = useQueryClient() 7 | const operatorsQuery = useQuery({ 8 | ...operatorsReadOperatorsOptions(), 9 | }) 10 | 11 | // Mutation to handle explicit refresh 12 | const refreshMutation = useMutation({ 13 | mutationFn: () => { 14 | return queryClient.fetchQuery({ 15 | ...operatorsReadOperatorsOptions({ 16 | query: { refresh: true }, 17 | }), 18 | }) 19 | }, 20 | onSuccess: () => { 21 | queryClient.invalidateQueries({ 22 | queryKey: operatorsReadOperatorsOptions().queryKey, 23 | }) 24 | }, 25 | }) 26 | 27 | const response = zOperatorSpecs.safeParse(operatorsQuery.data) 28 | const operatorSpecs = response.data?.data 29 | 30 | return { 31 | operatorSpecs, 32 | isRefreshing: refreshMutation.isPending, 33 | isLoading: operatorsQuery.isLoading, 34 | refetch: () => refreshMutation.mutate(), 35 | } 36 | } 37 | 38 | export default useOperatorSpecs 39 | -------------------------------------------------------------------------------- /operators/electron-count/operator.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "12345678-1234-1234-1234-1234567890cd", 3 | "image": "ghcr.io/nersc/interactem/electron-count:latest", 4 | "label": "Electron Counter", 5 | "description": "Counts electrons in a frame", 6 | "inputs": [ 7 | { 8 | "name": "in", 9 | "label": "The input", 10 | "type": "frame", 11 | "description": "Full frame" 12 | } 13 | ], 14 | "outputs": [ 15 | { 16 | "name": "out", 17 | "label": "The output", 18 | "type": "sparse frame", 19 | "description": "Sparsified data" 20 | } 21 | ], 22 | "parameters": [ 23 | { 24 | "name": "xray_threshold", 25 | "label": "X-ray threshold", 26 | "type": "float", 27 | "default": "2000.0", 28 | "description": "Removal of X-rays above this threshold", 29 | "required": true 30 | }, 31 | { 32 | "name": "background_threshold", 33 | "label": "Background threshold", 34 | "type": "float", 35 | "default": "28.0", 36 | "description": "Removal of background below this threshold", 37 | "required": true 38 | } 39 | ], 40 | "tags": [ 41 | { 42 | "value": "cpu", 43 | "description": "This operator should be run on a CPU node." 44 | } 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /frontend/interactEM/src/components/pipelines/viewmodetoggle.tsx: -------------------------------------------------------------------------------- 1 | import { Edit, PlayArrow } from "@mui/icons-material" 2 | import { IconButton, Tooltip } from "@mui/material" 3 | import type React from "react" 4 | import { ViewMode, useViewModeStore } from "../../stores" 5 | 6 | export const ViewModeToggle: React.FC = () => { 7 | const { viewMode, setViewMode } = useViewModeStore() 8 | 9 | const handleToggle = () => { 10 | const newMode = 11 | viewMode === ViewMode.Composer ? ViewMode.Runtime : ViewMode.Composer 12 | setViewMode(newMode) 13 | } 14 | 15 | const isComposer = viewMode === ViewMode.Composer 16 | 17 | return ( 18 | 21 | 33 | {isComposer ? ( 34 | 35 | ) : ( 36 | 37 | )} 38 | 39 | 40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /frontend/interactEM/src/hooks/nats/useBucket.ts: -------------------------------------------------------------------------------- 1 | import type { KV } from "@nats-io/kv" 2 | import { DrainingConnectionError } from "@nats-io/nats-core" 3 | import { useEffect, useRef, useState } from "react" 4 | import { useNats } from "../../contexts/nats" 5 | 6 | export const useBucket = (bucketName: string): KV | null => { 7 | const { keyValueManager } = useNats() 8 | const [bucket, setBucket] = useState(null) 9 | const isMounted = useRef(true) 10 | 11 | useEffect(() => { 12 | isMounted.current = true 13 | 14 | const openBucket = async () => { 15 | if (keyValueManager && !bucket) { 16 | try { 17 | const openedBucket = await keyValueManager.open(bucketName) 18 | if (isMounted.current) { 19 | setBucket(openedBucket) 20 | } 21 | } catch (error) { 22 | if (error instanceof DrainingConnectionError) { 23 | // quietly ignore if connection is draining 24 | return 25 | } 26 | console.error(`Failed to open bucket "${bucketName}":`, error) 27 | } 28 | } 29 | } 30 | 31 | openBucket() 32 | 33 | return () => { 34 | isMounted.current = false 35 | } 36 | }, [keyValueManager, bucket, bucketName]) 37 | 38 | return bucket 39 | } 40 | -------------------------------------------------------------------------------- /backend/app/interactem/app/tests/api/routes/test_login.py: -------------------------------------------------------------------------------- 1 | from fastapi.testclient import TestClient 2 | 3 | from interactem.app.core.config import settings 4 | 5 | 6 | def test_get_access_token(client: TestClient) -> None: 7 | login_data = { 8 | "username": settings.FIRST_SUPERUSER_USERNAME, 9 | "password": settings.FIRST_SUPERUSER_PASSWORD, 10 | } 11 | r = client.post(f"{settings.API_V1_STR}/login/access-token", data=login_data) 12 | tokens = r.json() 13 | assert r.status_code == 200 14 | assert "access_token" in tokens 15 | assert tokens["access_token"] 16 | 17 | 18 | def test_get_access_token_incorrect_password(client: TestClient) -> None: 19 | login_data = { 20 | "username": settings.FIRST_SUPERUSER_USERNAME, 21 | "password": "incorrect", 22 | } 23 | r = client.post(f"{settings.API_V1_STR}/login/access-token", data=login_data) 24 | assert r.status_code == 400 25 | 26 | 27 | def test_use_access_token( 28 | client: TestClient, superuser_token_headers: dict[str, str] 29 | ) -> None: 30 | r = client.post( 31 | f"{settings.API_V1_STR}/login/test-token", 32 | headers=superuser_token_headers, 33 | ) 34 | result = r.json() 35 | assert r.status_code == 200 36 | assert "username" in result 37 | -------------------------------------------------------------------------------- /backend/operators/interactem/operators/messengers/base.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | from nats.js import JetStreamContext 4 | 5 | from interactem.core.models.messages import BytesMessage 6 | from interactem.core.models.runtime import ( 7 | RuntimeInput, 8 | RuntimeOperatorID, 9 | RuntimeOutput, 10 | ) 11 | 12 | 13 | class BaseMessenger(ABC): 14 | @abstractmethod 15 | def __init__(self, operator_id: RuntimeOperatorID, js: JetStreamContext): 16 | pass 17 | 18 | @property 19 | @abstractmethod 20 | def ready(self) -> bool: 21 | pass 22 | 23 | @property 24 | @abstractmethod 25 | def type(self) -> str: 26 | pass 27 | 28 | @property 29 | @abstractmethod 30 | def input_ports(self) -> list[RuntimeInput]: 31 | pass 32 | 33 | @property 34 | @abstractmethod 35 | def output_ports(self) -> list[RuntimeOutput]: 36 | pass 37 | 38 | @abstractmethod 39 | async def send(self, message: BytesMessage): 40 | pass 41 | 42 | @abstractmethod 43 | async def recv(self) -> BytesMessage | None: 44 | pass 45 | 46 | @abstractmethod 47 | async def start(self, pipeline): 48 | pass 49 | 50 | @abstractmethod 51 | async def stop(self): 52 | pass 53 | -------------------------------------------------------------------------------- /backend/core/interactem/core/models/base.py: -------------------------------------------------------------------------------- 1 | import abc 2 | from enum import Enum 3 | from uuid import UUID 4 | 5 | IdType = UUID 6 | 7 | class PipelineDeploymentState(str, Enum): 8 | PENDING = "pending" 9 | AGENTS_ASSIGNED = "assigned_agents" 10 | FAILED_TO_START = "failed_to_start" 11 | FAILURE_ON_AGENT = "failure_on_agent" 12 | RUNNING = "running" 13 | CANCELLED = "cancelled" 14 | 15 | TERMINAL_DEPLOYMENT_STATES = [ 16 | PipelineDeploymentState.CANCELLED, 17 | PipelineDeploymentState.FAILED_TO_START, 18 | ] 19 | 20 | RUNNING_DEPLOYMENT_STATES = [ 21 | PipelineDeploymentState.RUNNING, 22 | ] 23 | 24 | 25 | class KvKeyMixin(abc.ABC): 26 | @abc.abstractmethod 27 | def key(self) -> str: ... 28 | 29 | 30 | class CommBackend(str, Enum): 31 | ZMQ = "zmq" 32 | MPI = "mpi" 33 | NATS = "nats" 34 | 35 | 36 | class Protocol(str, Enum): 37 | tcp = "tcp" 38 | inproc = "inproc" 39 | ipc = "ipc" 40 | 41 | 42 | class URILocation(str, Enum): 43 | operator = "operator" 44 | port = "port" 45 | agent = "agent" 46 | orchestrator = "orchestrator" 47 | 48 | 49 | class PortType(str, Enum): 50 | input = "input" 51 | output = "output" 52 | 53 | 54 | class NodeType(str, Enum): 55 | operator = "operator" 56 | port = "port" 57 | -------------------------------------------------------------------------------- /interactEM.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "name": "Root", 5 | "path": "." 6 | }, 7 | { 8 | "name": "Agent", 9 | "path": "backend/agent" 10 | }, 11 | { 12 | "name": "App", 13 | "path": "backend/app" 14 | }, 15 | { 16 | "name": "Callout", 17 | "path": "backend/callout" 18 | }, 19 | { 20 | "name": "Core", 21 | "path": "backend/core" 22 | }, 23 | { 24 | "name": "Launcher", 25 | "path": "backend/launcher" 26 | }, 27 | { 28 | "name": "Metrics", 29 | "path": "backend/metrics" 30 | }, 31 | { 32 | "name": "Operators", 33 | "path": "backend/operators" 34 | }, 35 | { 36 | "name": "Orchestrator", 37 | "path": "backend/orchestrator" 38 | }, 39 | { 40 | "name": "SFAPI Models", 41 | "path": "backend/sfapi_models" 42 | }, 43 | { 44 | "name": "CLI", 45 | "path": "cli" 46 | }, 47 | { 48 | "name": "Frontend", 49 | "path": "frontend" 50 | } 51 | ] 52 | } -------------------------------------------------------------------------------- /backend/launcher/tests/test_rendering.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | from pathlib import Path 3 | 4 | import pytest 5 | from jinja2 import Environment, PackageLoader 6 | from sfapi_client.compute import Machine 7 | 8 | from interactem.launcher.config import cfg 9 | from interactem.launcher.constants import LAUNCH_AGENT_TEMPLATE 10 | from interactem.sfapi_models import JobSubmitEvent 11 | 12 | HERE = Path(__file__).parent 13 | 14 | 15 | @pytest.fixture 16 | def expected_script() -> str: 17 | with open(HERE / "expected_script.sh") as f: 18 | return f.read() 19 | 20 | 21 | @pytest.mark.asyncio 22 | async def test_submit_rendering(expected_script: str): 23 | job_req = JobSubmitEvent( 24 | machine=Machine.perlmutter, 25 | account="test_account", 26 | qos="normal", 27 | constraint="gpu", 28 | walltime=timedelta(hours=1, minutes=30), 29 | reservation=None, 30 | num_nodes=2, 31 | ) 32 | 33 | jinja_env = Environment( 34 | loader=PackageLoader("interactem.launcher"), enable_async=True 35 | ) 36 | template = jinja_env.get_template(LAUNCH_AGENT_TEMPLATE) 37 | 38 | script = await template.render_async( 39 | job=job_req.model_dump(), settings=cfg.model_dump() 40 | ) 41 | 42 | assert script == expected_script 43 | -------------------------------------------------------------------------------- /backend/rdma/libs/argobots/include/abt_manager.hpp: -------------------------------------------------------------------------------- 1 | 2 | #pragma once 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include "abt_config.hpp" 10 | 11 | namespace interactEM { 12 | 13 | namespace tl = thallium; 14 | 15 | class AbtManager { 16 | 17 | private: 18 | // AbtPool pool_; // Custom Argobots pool 19 | // AbtScheduler scheduler_; // Custom Argobots scheduler 20 | std::vector> execution_streams_; 21 | tl::managed thread_pool_; 22 | bool is_initialized_; 23 | 24 | static tl::abt* global_abt_scope_; 25 | static int instance_count_; 26 | 27 | public: 28 | AbtManager() : is_initialized_(false) {} 29 | AbtManager(const AbtConfig& config) {} 30 | ~AbtManager(); 31 | 32 | AbtManager(const AbtManager& other) = delete; 33 | AbtManager& operator=(const AbtManager& other) = delete; 34 | 35 | AbtManager(AbtManager&& other) = default; 36 | AbtManager& operator=(AbtManager&& other) = default; 37 | 38 | void initialize(); 39 | void finalize(); 40 | tl::pool& getPool() {return *thread_pool_;} 41 | }; 42 | 43 | } 44 | -------------------------------------------------------------------------------- /operators/diffraction-pattern-accumulator/operator.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "dedeba48-cb84-48dc-b6ca-4c11d94e9375", 3 | "image": "ghcr.io/nersc/interactem/diffraction-pattern-accumulator:latest", 4 | "label": "Diffraction Pattern Accumulator", 5 | "description": "Accumulates diffraction patterns from sparse arrays.", 6 | "inputs": [ 7 | { 8 | "name": "counted_data", 9 | "label": "Counted Data", 10 | "type": "sparse_frame", 11 | "description": "Sparse frame of counted data" 12 | } 13 | ], 14 | "outputs": [ 15 | { 16 | "name": "image", 17 | "label": "Output Image", 18 | "type": "image", 19 | "description": "Dense image" 20 | } 21 | ], 22 | "parameters": [ 23 | { 24 | "name": "update_frequency", 25 | "label": "Update Frequency", 26 | "type": "int", 27 | "default": "100", 28 | "description": "The number of frames to accumulate before sending out a frame.", 29 | "required": true 30 | }, 31 | { 32 | "name": "max_concurrent_scans", 33 | "label": "Max Concurrent Scans", 34 | "type": "int", 35 | "default": "1", 36 | "description": "Maximum number of scans to keep in memory simultaneously. Oldest scans are evicted when this limit is exceeded.", 37 | "required": false 38 | } 39 | ] 40 | } -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # interactEM Documentation 2 | 3 | This directory contains the documentation for interactEM, built with Sphinx and the Furo theme. 4 | 5 | ## Building the Documentation 6 | 7 | Build the HTML documentation: 8 | 9 | ```bash 10 | make build 11 | ``` 12 | 13 | The built documentation will be in `_build/html/`. 14 | 15 | ## Development 16 | 17 | For live auto-rebuilding during development: 18 | 19 | ```bash 20 | make autobuild 21 | ``` 22 | 23 | This will start a local server and automatically rebuild the docs when you make changes. 24 | 25 | ### Syncing repository READMEs 26 | 27 | The Markdown files in `docs/source/` are the source of truth. Run the helper to regenerate the top-level README files after editing docs: 28 | 29 | ```bash 30 | make sync-readmes 31 | ``` 32 | 33 | The script strips `` sections so Sphinx-only snippets stay out of the GitHub READMEs. 34 | 35 | ## Structure 36 | 37 | - `source/` - Documentation source files (RST and Markdown) 38 | - `source/_static/` - Static assets (CSS, JS, images) 39 | - `source/_templates/` - Custom Sphinx templates 40 | - `source/conf.py` - Sphinx configuration 41 | 42 | ## Theme 43 | 44 | This documentation uses the Furo theme with custom styling inspired by the [iceoryx2-book](https://github.com/ekxide/iceoryx2-book) project. 45 | -------------------------------------------------------------------------------- /frontend/interactEM/src/components/nodes/image.tsx: -------------------------------------------------------------------------------- 1 | import type { NodeProps } from "@xyflow/react" 2 | import { useRef } from "react" 3 | import { useImage } from "../../hooks/nats/useImage" 4 | import { useRuntimeOperatorStatusStyles } from "../../hooks/nats/useOperatorStatus" 5 | import type { ImageNodeType } from "../../types/nodes" 6 | import Image from "../image" 7 | import Handles from "./handles" 8 | import OperatorToolbar from "./toolbar" 9 | 10 | interface ImageNodeBaseProps extends NodeProps { 11 | className?: string 12 | } 13 | 14 | const ImageNodeBase = ({ id, data, className = "" }: ImageNodeBaseProps) => { 15 | const nodeRef = useRef(null) 16 | const imageData = useImage(id) 17 | const { statusClass } = useRuntimeOperatorStatusStyles(id) 18 | 19 | // TODO: the data containing the positions causes a re-render of the node. 20 | 21 | return ( 22 |
23 | 24 | 25 | 31 |
32 | ) 33 | } 34 | 35 | const ImageNode = ImageNodeBase 36 | 37 | export default ImageNode 38 | -------------------------------------------------------------------------------- /frontend/interactEM/src/components/logs/agentdialog.tsx: -------------------------------------------------------------------------------- 1 | import CloseIcon from "@mui/icons-material/Close" 2 | import { Box, Dialog, DialogContent, DialogTitle } from "@mui/material" 3 | import React from "react" 4 | import { useAgentLogs } from "../../hooks/nats/useAgentLogs" 5 | import LogsList from "./list" 6 | import { CloseDialogButton, LogsPanel } from "./styles" 7 | 8 | interface AgentLogsDialogProps { 9 | open: boolean 10 | onClose: () => void 11 | agentId: string 12 | agentLabel: string 13 | } 14 | 15 | const AgentLogsDialog: React.FC = ({ 16 | open, 17 | onClose, 18 | agentId, 19 | agentLabel, 20 | }) => { 21 | const { logs } = useAgentLogs({ 22 | id: agentId, 23 | }) 24 | 25 | return ( 26 | 27 | 28 | Agent Logs: {agentLabel} 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | ) 42 | } 43 | 44 | export default React.memo(AgentLogsDialog) 45 | -------------------------------------------------------------------------------- /frontend/interactEM/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from "path" 2 | import react from "@vitejs/plugin-react" 3 | import { visualizer } from "rollup-plugin-visualizer" 4 | import { defineConfig } from "vite" 5 | import dts from "vite-plugin-dts" 6 | 7 | const __dirname = resolve() 8 | 9 | // https://vitejs.dev/config/ 10 | export default defineConfig({ 11 | server: { 12 | strictPort: true, 13 | port: 5173, 14 | }, 15 | plugins: [ 16 | react(), 17 | dts({ 18 | rollupTypes: true, 19 | tsconfigPath: resolve(__dirname, "tsconfig.json"), 20 | }), 21 | visualizer({ open: false, filename: "bundle-visualization.html" }), 22 | ], 23 | build: { 24 | lib: { 25 | entry: resolve(__dirname, "src/index.ts"), 26 | name: "InteractEM", 27 | fileName: "interactem", 28 | }, 29 | rollupOptions: { 30 | // this is critical for react-query. TODO: figure out why... 31 | // https://github.com/TanStack/query/issues/7927 32 | // potentially explore https://www.npmjs.com/package/@tanstack/config 33 | external: ["react", "react-dom", "@tanstack/react-query"], 34 | output: { 35 | globals: { 36 | react: "React", 37 | "react-dom": "ReactDOM", 38 | "@tanstack/react-query": "ReactQuery", 39 | }, 40 | }, 41 | treeshake: true, 42 | }, 43 | }, 44 | }) 45 | -------------------------------------------------------------------------------- /frontend/interactEM/src/components/notificationstoast.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AckPolicy, 3 | DeliverPolicy, 4 | type JsMsg, 5 | ReplayPolicy, 6 | } from "@nats-io/jetstream" 7 | import { useCallback, useMemo } from "react" 8 | import { type TypeOptions, toast } from "react-toastify" 9 | import { 10 | STREAM_NOTIFICATIONS, 11 | SUBJECT_NOTIFICATIONS_ERRORS, 12 | } from "../constants/nats" 13 | import { useConsumeMessages } from "../hooks/nats/useConsumeMessages" 14 | import { useConsumer } from "../hooks/nats/useConsumer" 15 | 16 | export default function NotificationsToast() { 17 | const config = useMemo( 18 | () => ({ 19 | filter_subjects: [`${STREAM_NOTIFICATIONS}.>`], 20 | ack_policy: AckPolicy.All, 21 | deliver_policy: DeliverPolicy.New, 22 | replay_policy: ReplayPolicy.Instant, 23 | }), 24 | [], 25 | ) 26 | const consumer = useConsumer({ 27 | stream: `${STREAM_NOTIFICATIONS}`, 28 | config: config, 29 | }) 30 | 31 | const handleMessage = useCallback(async (m: JsMsg) => { 32 | let toastType: TypeOptions = "info" 33 | 34 | if (m.subject === `${SUBJECT_NOTIFICATIONS_ERRORS}`) { 35 | toastType = "error" 36 | } 37 | 38 | const notification = m.string() 39 | toast(notification, { type: toastType }) 40 | }, []) 41 | 42 | useConsumeMessages({ consumer, handleMessage }) 43 | 44 | return null 45 | } 46 | -------------------------------------------------------------------------------- /operators/beam-compensation/operator.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "10010008-1234-1234-1234-1000000890ab", 3 | "image": "ghcr.io/nersc/interactem/beam-compensation", 4 | "label": "Beam Compensation (vacuum scan)", 5 | "description": "Compensates for beam movement inside of a frame using vacuum background", 6 | "inputs": [ 7 | { 8 | "name": "in", 9 | "label": "The input", 10 | "type": "partial frame", 11 | "description": "Partial frame" 12 | } 13 | ], 14 | "outputs": [ 15 | { 16 | "name": "out", 17 | "label": "The output", 18 | "type": "frame", 19 | "description": "Full frame" 20 | } 21 | ], 22 | "parameters": [ 23 | { 24 | "name": "offsets.emd", 25 | "label": "Offsets EMD file", 26 | "type": "mount", 27 | "default": "~/FOURD_241002_0852_20132_00714_offsets.emd", 28 | "description": "Offsets EMD file -- you should create one of these for data the day of an experiment", 29 | "required": true 30 | }, 31 | { 32 | "name": "method", 33 | "label": "Background subtr. method", 34 | "type": "str-enum", 35 | "default": "interp", 36 | "description": "Method to use for background subtraction", 37 | "options": ["plane", "interp"], 38 | "required": true 39 | } 40 | ], 41 | "parallel_config": { 42 | "type": "embarrassing" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /operators/distiller-streaming/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "distiller_streaming" 3 | version = "0.0.1" 4 | description = "" 5 | authors = [ 6 | {name = "swelborn", email = "swelborn@lbl.gov"} 7 | ] 8 | readme = "README.md" 9 | requires-python = ">=3.10" 10 | dependencies = [ 11 | "pyzmq>=27.0.1,<28.0.0", 12 | "msgpack>=1.1.0,<2.0.0", 13 | "pandas>=2.3.1,<3.0.0", 14 | "pydantic>=2.11.7,<3.0.0", 15 | "stempy>=3.4.0,<4.0.0", 16 | "numpy>=2.2.6,<2.3.0", 17 | "msgspec>=0.19.0,<0.20.0", 18 | "interactem-core", 19 | "interactem-operators", 20 | ] 21 | 22 | [dependency-groups] 23 | dev = [ 24 | "pytest>=9.0.1,<10", 25 | ] 26 | 27 | [build-system] 28 | requires = ["poetry-core>=2.0.0,<3.0.0"] 29 | build-backend = "poetry.core.masonry.api" 30 | 31 | [tool.uv.sources] 32 | interactem-core = { path = "../../backend/core", editable = true } 33 | interactem-operators = { path = "../../backend/operators", editable = true } 34 | 35 | [tool.poetry.dependencies] 36 | interactem-core = {path = "../../backend/core", develop = true} 37 | interactem-operators = {path = "../../backend/operators", develop = true} 38 | 39 | [tool.ruff] 40 | target-version = "py310" 41 | extend = "../../.ruff.toml" 42 | exclude = [] 43 | 44 | 45 | [tool.ruff.lint] 46 | exclude = [] 47 | 48 | [tool.ruff.lint.isort] 49 | known-first-party = ["distiller_streaming", "interactem"] 50 | -------------------------------------------------------------------------------- /backend/core/interactem/core/models/messages.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from enum import Enum 3 | from typing import Any 4 | 5 | from pydantic import BaseModel, Field 6 | 7 | from interactem.core.models.base import IdType 8 | 9 | 10 | class TrackingMetadataBase(BaseModel): 11 | id: IdType 12 | 13 | 14 | class InputPortTrackingMetadata(TrackingMetadataBase): 15 | time_after_header_validate: datetime 16 | 17 | 18 | class OutputPortTrackingMetadata(TrackingMetadataBase): 19 | time_before_send: datetime 20 | 21 | 22 | class OperatorTrackingMetadata(TrackingMetadataBase): 23 | time_before_operate: datetime 24 | time_after_operate: datetime 25 | 26 | class TrackingMetadatas(BaseModel): 27 | metadatas: list[ 28 | OperatorTrackingMetadata 29 | | OutputPortTrackingMetadata 30 | | InputPortTrackingMetadata 31 | ] = Field(default_factory=list) 32 | 33 | 34 | class MessageSubject(str, Enum): 35 | BYTES = "bytes" 36 | SHM = "shm" 37 | 38 | 39 | class MessageHeader(BaseModel): 40 | subject: MessageSubject 41 | meta: bytes | dict[str, Any] = b"{}" 42 | tracking: TrackingMetadatas | None = None 43 | 44 | class BaseMessage(BaseModel): 45 | header: MessageHeader 46 | 47 | 48 | class BytesMessage(BaseMessage): 49 | data: bytes 50 | 51 | 52 | class ShmMessage(BaseMessage): 53 | shm_meta: dict[str, Any] = {} 54 | -------------------------------------------------------------------------------- /scripts/ensure-nats-credentials.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Ensure credential files exist as files (not directories) before docker-compose mount 3 | # This prevents Docker from creating directories during bind mounting of non-existent files 4 | 5 | set -euo pipefail 6 | 7 | # Find the git repository root 8 | GIT_ROOT=$(git rev-parse --show-toplevel 2>/dev/null) || { 9 | echo "Error: Not in a git repository" >&2 10 | exit 1 11 | } 12 | 13 | CREDS_DIR="$GIT_ROOT/conf/nats-conf/out_jwt" 14 | 15 | # Credential files that are bind-mounted in docker-compose 16 | credential_files=( 17 | "backend.creds" 18 | "frontend.creds" 19 | "operator.creds" 20 | "callout.creds" 21 | ) 22 | 23 | # Create parent directory if it doesn't exist 24 | mkdir -p "$CREDS_DIR" 25 | 26 | # Check and fix credential files 27 | for file in "${credential_files[@]}"; do 28 | filepath="$CREDS_DIR/$file" 29 | 30 | if [ -d "$filepath" ]; then 31 | # If it's a directory, remove it and create as file 32 | echo "⚠ Found directory instead of file: $filepath, fixing..." 33 | rm -rf "$filepath" 34 | touch "$filepath" 35 | echo "✓ Converted directory to file: $filepath" 36 | elif [ ! -e "$filepath" ]; then 37 | # If it doesn't exist, create as empty file 38 | touch "$filepath" 39 | echo "✓ Created placeholder file: $filepath" 40 | fi 41 | done 42 | -------------------------------------------------------------------------------- /frontend/interactEM/src/components/agents/chip.tsx: -------------------------------------------------------------------------------- 1 | import Chip from "@mui/material/Chip" 2 | import Tooltip from "@mui/material/Tooltip" 3 | import { useState } from "react" 4 | import type { AgentVal } from "../../types/gen" 5 | import { getAgentStatusColor } from "../../utils/statusColor" 6 | import AgentLogsDialog from "../logs/agentdialog" 7 | import { StatusDot } from "../statusdot" 8 | import AgentTooltip from "./tooltip" 9 | 10 | interface AgentChipProps { 11 | agent: AgentVal 12 | } 13 | 14 | export default function AgentChip({ agent }: AgentChipProps) { 15 | const [open, setOpen] = useState(false) 16 | const shortId = agent.uri.id.substring(0, 6) 17 | const displayName = agent.name?.trim() ? agent.name : shortId 18 | 19 | return ( 20 | <> 21 | } arrow> 22 | } 24 | label={displayName} 25 | color={getAgentStatusColor(agent.status)} 26 | variant="outlined" 27 | onClick={() => setOpen(true)} 28 | clickable 29 | sx={{ fontWeight: 500, fontSize: "1rem" }} 30 | /> 31 | 32 | 33 | setOpen(false)} 36 | agentId={agent.uri.id} 37 | agentLabel={displayName} 38 | /> 39 | 40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /frontend/interactEM/src/types/agent.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod" 2 | import { 3 | AgentStatus, 4 | type AgentVal, 5 | CommBackend, 6 | type ErrorMessage, 7 | type URI, 8 | URILocation, 9 | } from "./gen" 10 | 11 | const zURI = z.object({ 12 | id: z.string(), 13 | location: z.nativeEnum(URILocation), 14 | hostname: z.string(), 15 | comm_backend: z.nativeEnum(CommBackend), 16 | query: z.record(z.string(), z.array(z.string())).optional(), 17 | }) satisfies z.ZodType 18 | 19 | // For AgentErrorMessage, handle manually due to index signature 20 | const zErrorMessage = z.object({ 21 | message: z.string(), 22 | timestamp: z.number(), 23 | }) satisfies z.ZodType 24 | 25 | export const AgentValSchema = z.object({ 26 | name: z.string().nullable().optional(), 27 | uri: zURI, 28 | status: z.nativeEnum(AgentStatus), 29 | status_message: z.string().nullable().optional(), 30 | tags: z.array(z.string()).optional(), 31 | networks: z.array(z.string()), 32 | pipeline_id: z.string().nullable().optional(), 33 | operator_assignments: z.array(z.string()).nullable().optional(), 34 | uptime: z.number(), 35 | error_messages: z.array(zErrorMessage).optional(), 36 | }) satisfies z.ZodType 37 | 38 | // Re-export this because AgentVal is an interface and we need this for 39 | // the AgentNode type 40 | export type AgentValType = z.infer 41 | -------------------------------------------------------------------------------- /scripts/setup-podman-socket.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Script to update PODMAN_SERVICE_URI in all .env files with the actual podman socket path 4 | 5 | set -euo pipefail 6 | 7 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 8 | REPO_ROOT="$(dirname "$SCRIPT_DIR")" 9 | 10 | echo "Setting up PODMAN_SERVICE_URI in .env files..." 11 | 12 | # Get the podman socket path in the unix:// format 13 | PODMAN_SERVICE_URI=$("$SCRIPT_DIR/podman-socket-path.sh") 14 | 15 | if [ -z "$PODMAN_SERVICE_URI" ]; then 16 | echo "Error: Could not determine podman socket URI" >&2 17 | exit 1 18 | fi 19 | 20 | echo "Using podman socket URI: $PODMAN_SERVICE_URI" 21 | echo "" 22 | 23 | # Find all .env files and update PODMAN_SERVICE_URI 24 | updated_count=0 25 | 26 | while IFS= read -r -d '' env_file; do 27 | if grep -q "PODMAN_SERVICE_URI=" "$env_file"; then 28 | # Use sed to replace the PODMAN_SERVICE_URI line 29 | # This handles both placeholder values and existing values 30 | sed -i.bak "s|^PODMAN_SERVICE_URI=.*|PODMAN_SERVICE_URI=$PODMAN_SERVICE_URI|" "$env_file" 31 | echo "✓ Updated: $env_file" 32 | # Clean up backup file 33 | rm -f "${env_file}.bak" 34 | updated_count=$((updated_count + 1)) 35 | fi 36 | done < <(find "$REPO_ROOT" -name ".env" -type f -print0) 37 | 38 | echo "" 39 | echo "Summary:" 40 | echo " Updated: $updated_count .env file(s)" 41 | -------------------------------------------------------------------------------- /frontend/interactEM/src/contexts/nats/agentstatus.tsx: -------------------------------------------------------------------------------- 1 | import { type ReactNode, createContext, useContext, useMemo } from "react" 2 | import { AGENTS, BUCKET_STATUS } from "../../constants/nats" 3 | import { useBucketWatch } from "../../hooks/nats/useBucketWatch" 4 | import { AgentValSchema } from "../../types/agent" 5 | import type { AgentVal } from "../../types/gen" 6 | 7 | interface AgentStatusContextType { 8 | agents: AgentVal[] 9 | agentsLoading: boolean 10 | agentsError: string | null 11 | } 12 | 13 | const AgentStatusContext = createContext({ 14 | agents: [], 15 | agentsLoading: true, 16 | agentsError: null, 17 | }) 18 | 19 | export const AgentStatusProvider: React.FC<{ children: ReactNode }> = ({ 20 | children, 21 | }) => { 22 | const { 23 | items: agents, 24 | isLoading: agentsLoading, 25 | error: agentsError, 26 | } = useBucketWatch({ 27 | bucketName: BUCKET_STATUS, 28 | schema: AgentValSchema, 29 | keyFilter: `${AGENTS}.>`, 30 | stripPrefix: AGENTS, 31 | }) 32 | 33 | const contextValue = useMemo( 34 | () => ({ agents, agentsLoading, agentsError }), 35 | [agents, agentsLoading, agentsError], 36 | ) 37 | 38 | return ( 39 | 40 | {children} 41 | 42 | ) 43 | } 44 | 45 | export const useAgentStatusContext = () => useContext(AgentStatusContext) 46 | -------------------------------------------------------------------------------- /docs/source/launch-agent.md: -------------------------------------------------------------------------------- 1 | # Launch an agent 2 | 3 | The agent is the process that starts operator containers, coordinates their lifecycle, and talks to the platform over NATS. You need at least one running agent before operators can launch. 4 | 5 | ## Prerequisites 6 | 7 | Have a Python environment manager available (e.g., [`poetry`](https://python-poetry.org/docs/) or [`uv`](https://github.com/astral-sh/uv)). 8 | 9 | ## Configure the environment 10 | 11 | You should already have run `make setup` in the repository root to create a base `.env`. Then: 12 | 13 | ```bash 14 | cd backend/agent 15 | ``` 16 | 17 | - Copy `.env.example` if you have not already, and update values as needed. 18 | - Set the agent's display name in `AGENT_NAME`: 19 | 20 | ```bash 21 | AGENT_NAME=SecretAgentMan # change to how you want it to display in the frontend 22 | ``` 23 | 24 | - If you want to target specific resources, set tags: 25 | 26 | ```bash 27 | AGENT_TAGS='["ncem-4dcamera","gpu"]' 28 | ``` 29 | 30 | ## Install and run 31 | 32 | Install dependencies with your preferred tool: 33 | 34 | ```bash 35 | poetry install 36 | ``` 37 | 38 | or 39 | 40 | ```bash 41 | uv sync 42 | ``` 43 | 44 | Then activate your virtual environment and start the agent from the directory containing your `.env`: 45 | 46 | ```bash 47 | cd backend/agent 48 | interactem-agent 49 | ``` 50 | 51 | or with `uv`: 52 | 53 | ```bash 54 | cd backend/agent 55 | uv run interactem-agent 56 | ``` 57 | -------------------------------------------------------------------------------- /backend/agent/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "interactem-agent" 3 | version = "0.1.0" 4 | description = "Agent for interactem" 5 | readme = "README.md" 6 | authors = [ 7 | {name = "Sam Welborn", email = "swelborn@lbl.gov"}, 8 | {name = "Chris Harris", email = "cjh@lbl.gov"} 9 | ] 10 | requires-python = ">=3.10" 11 | dependencies = [ 12 | "interactem-core", 13 | "podman>=5.0.0,<6", 14 | "pydantic-settings>=2.4,<3", 15 | "nkeys>=0.2.1,<0.3", 16 | "aiohttp>=3.11.12,<4", 17 | "netifaces2>=0.0.22,<0.1", 18 | "stamina>=25.1.0,<26", 19 | "jinja2>=3.1.6,<4", 20 | ] 21 | 22 | [project.optional-dependencies] 23 | hpc = [ 24 | "podman-hpc @ git+https://github.com/cjh1/podman-hpc.git@d01773a8ae14489af7654d799d9738a2c5629248", 25 | "podman-hpc-py @ git+https://github.com/cjh1/podman-hpc-py.git@577e467ab5bf10840df81234e477a3ebbd436f7e", 26 | ] 27 | 28 | 29 | [project.scripts] 30 | interactem-agent = "interactem.agent.entrypoint:entrypoint" 31 | 32 | [tool.uv.sources] 33 | interactem-core = { path = "../core", editable = true } 34 | 35 | 36 | [tool.poetry] 37 | packages = [{ include = "interactem" }] 38 | 39 | [tool.poetry.dependencies] 40 | interactem-core = {path = "../core", develop = true} 41 | 42 | [build-system] 43 | requires = ["poetry-core"] 44 | build-backend = "poetry.core.masonry.api" 45 | 46 | [tool.ruff] 47 | target-version = "py310" 48 | extend = "../../.ruff.toml" 49 | extend-exclude = ["thirdparty/"] 50 | -------------------------------------------------------------------------------- /backend/app/interactem/app/alembic/versions/ae4ed8e4c67b_current_revision_id.py: -------------------------------------------------------------------------------- 1 | """current_revision_id 2 | 3 | Revision ID: ae4ed8e4c67b 4 | Revises: 60a2bc7c4aea 5 | Create Date: 2025-04-19 18:06:17.099537 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | import sqlmodel.sql.sqltypes 11 | 12 | 13 | # revision identifiers, used by Alembic. 14 | revision = 'ae4ed8e4c67b' 15 | down_revision = '60a2bc7c4aea' 16 | branch_labels = None 17 | depends_on = None 18 | 19 | 20 | def upgrade(): 21 | # ### commands auto generated by Alembic - please adjust! ### 22 | op.add_column('pipeline', sa.Column('current_revision_id', sa.Integer(), nullable=True)) 23 | 24 | # Update existing NULL values to 0 25 | op.execute('UPDATE pipeline SET current_revision_id = 0 WHERE current_revision_id IS NULL') 26 | 27 | # Alter the column to be non-nullable 28 | op.alter_column('pipeline', 'current_revision_id', 29 | existing_type=sa.Integer(), 30 | nullable=False) 31 | 32 | op.create_index(op.f('ix_pipeline_current_revision_id'), 'pipeline', ['current_revision_id'], unique=False) 33 | # ### end Alembic commands ### 34 | 35 | 36 | def downgrade(): 37 | # ### commands auto generated by Alembic - please adjust! ### 38 | op.drop_index(op.f('ix_pipeline_current_revision_id'), table_name='pipeline') 39 | op.drop_column('pipeline', 'current_revision_id') 40 | # ### end Alembic commands ### 41 | -------------------------------------------------------------------------------- /backend/app/interactem/app/tests/conftest.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Generator 2 | 3 | import pytest 4 | from fastapi.testclient import TestClient 5 | from sqlmodel import Session, delete 6 | 7 | from interactem.app.core.db import engine, init_db 8 | from interactem.app.main import app 9 | from interactem.app.models import Pipeline, User 10 | from interactem.app.tests.utils.user import authentication_token_from_username 11 | from interactem.app.tests.utils.utils import get_superuser_token_headers 12 | 13 | 14 | @pytest.fixture(scope="session", autouse=True) 15 | def db() -> Generator[Session, None, None]: 16 | with Session(engine) as session: 17 | init_db(session) 18 | yield session 19 | statement = delete(Pipeline) 20 | session.execute(statement) 21 | statement = delete(User) 22 | session.execute(statement) 23 | session.commit() 24 | 25 | 26 | @pytest.fixture(scope="module") 27 | def client() -> Generator[TestClient, None, None]: 28 | with TestClient(app) as c: 29 | yield c 30 | 31 | 32 | @pytest.fixture(scope="module") 33 | def superuser_token_headers(client: TestClient) -> dict[str, str]: 34 | return get_superuser_token_headers(client) 35 | 36 | 37 | @pytest.fixture(scope="module") 38 | def normal_user_token_headers(client: TestClient, db: Session) -> dict[str, str]: 39 | return authentication_token_from_username( 40 | client=client, username="test_user", db=db 41 | ) 42 | --------------------------------------------------------------------------------