Agent Skills: MADSci Manager Services

Working with MADSci manager services (Event, Experiment, Resource, Data, Workcell, Location, Lab). Use when creating, modifying, debugging, or configuring manager servers.

UncategorizedID: AD-SDL/MADSci/madsci-managers

Install this agent skill to your local

pnpm dlx add-skill https://github.com/AD-SDL/MADSci/tree/HEAD/src/madsci_common/madsci/common/bundled_templates/_skills/madsci-managers

Skill Files

Browse the full folder contents for madsci-managers.

Download Skill

Loading file tree…

src/madsci_common/madsci/common/bundled_templates/_skills/madsci-managers/SKILL.md

Skill Metadata

Name
madsci-managers
Description
Working with MADSci manager services (Event, Experiment, Resource, Data, Workcell, Location, Lab). Use when creating, modifying, debugging, or configuring manager servers.

MADSci Manager Services

MADSci has 7 manager services, all built on AbstractManagerBase[SettingsT] with FastAPI via classy_fastapi.Routable. Each follows the pattern: Settings class -> Server class -> Client class.

Reference Files

Detailed material is in reference/. Read the relevant file when the task touches that area:

  • Per-Manager Notes — domain-specific notes for each of the 7 managers (Event/Experiment/Resource/Data/Workcell/Location/Lab). See reference/managers.md.
  • Database Handler Reference — the 4 handler ABCs and their real + in-memory implementations. See reference/db-handlers.md.

Key Files

| File | Purpose | |------|---------| | src/madsci_common/madsci/common/manager_base.py | AbstractManagerBase - common server lifecycle, health, CORS, OTEL | | src/madsci_common/madsci/common/types/manager_types.py | ManagerSettings, ManagerHealth base types | | src/madsci_common/madsci/common/types/base_types.py | MadsciBaseSettings, prefixed_alias_generator() | | src/madsci_common/madsci/common/db_handlers/ | 4 handler ABCs + real + in-memory implementations | | src/madsci_*/madsci/*/*_server.py | Each manager's server implementation | | src/madsci_client/madsci/client/ | All client implementations | | src/madsci_client/madsci/client/client_mixin.py | MadsciClientMixin for lazy client access |

The 7 Managers

| Manager | Port | Database | Handler Params | Key Domain | |---------|------|----------|----------------|------------| | Lab (Squid) | 8000 | None | N/A | Dashboard, service discovery, health aggregation | | Event | 8001 | FerretDB | document_handler | Distributed event logging, retention, analytics | | Experiment | 8002 | FerretDB | document_handler | Experiment lifecycle (start/pause/cancel/end) | | Resource | 8003 | PostgreSQL | postgres_handler | Inventory, containers (Queue/Stack/Slot), templates | | Data | 8004 | FerretDB + S3 | document_handler, object_storage_handler | DataPoints (value/file), dual storage | | Workcell | 8005 | FerretDB + Valkey | document_handler, cache_handler | Workflow execution, scheduling, node coordination | | Location | 8006 | FerretDB + Valkey | document_handler, cache_handler | Location management, transfer planning, representations |

Manager Implementation Pattern

1. Settings Class

from madsci.common.types.manager_types import ManagerSettings
from madsci.common.types.base_types import prefixed_alias_generator, prefixed_model_validator

class MyManagerSettings(ManagerSettings):
    model_config = SettingsConfigDict(
        env_prefix="MYMANAGER_",
        alias_generator=prefixed_alias_generator("mymanager"),
        populate_by_name=True,
    )
    _accept_prefixed_keys = prefixed_model_validator("mymanager")

    server_url: AnyUrl = Field(default="http://localhost:8007/")
    document_db_url: AnyUrl = Field(default="mongodb://localhost:27017/")
    database_name: str = Field(default="madsci_mymanager")

Key points:

  • env_prefix: Environment variable prefix (e.g., MYMANAGER_SERVER_URL)
  • prefixed_alias_generator: Enables prefixed keys in YAML (e.g., mymanager_server_url)
  • prefixed_model_validator: Accepts prefixed keys during model initialization
  • Code uses unprefixed names (server_url), YAML/export uses prefixed (mymanager_server_url)
  • model_dump(by_alias=True) -> prefixed keys; model_dump() -> unprefixed

2. Server Class

from madsci.common.manager_base import AbstractManagerBase

class MyManager(AbstractManagerBase[MyManagerSettings]):
    SETTINGS_CLASS = MyManagerSettings

    def initialize(self, **kwargs):
        """Called during __init__ after base setup. Set up DB connections, etc."""
        self._document_handler = kwargs.get("document_handler") or PyDocumentStorageHandler(
            url=str(self.settings.document_db_url),
            database_name=self.settings.database_name,
        )

    def get_health(self) -> MyManagerHealth:
        """Override to add DB connectivity checks."""
        try:
            connected = self._document_handler.ping()
            return MyManagerHealth(healthy=connected, db_connected=connected)
        except Exception as e:
            return MyManagerHealth(healthy=False, description=str(e))

    @get("/items")
    def list_items(self) -> list[Item]:
        """Custom endpoint using classy_fastapi @get/@post decorators."""
        ...

    @post("/items")
    def create_item(self, item: Item) -> Item:
        ...

AbstractManagerBase provides:

  • GET /health -> calls get_health()
  • GET /settings -> exports settings with secret redaction
  • CORS middleware (configurable)
  • Rate limiting middleware
  • OpenTelemetry instrumentation (if enabled)
  • Ownership context middleware
  • Registry identity resolution (stable IDs across restarts)
  • self.logger (EventClient) for structured logging
  • self.span(name, attributes) context manager for OTEL tracing
  • create_server() and run_server() for lifecycle management

3. Client Class

from madsci.common.utils import create_http_session
from pydantic import AnyUrl

class MyManagerClient:
    def __init__(self, server_url: AnyUrl, config=None, event_client=None):
        self.server_url = str(server_url)
        self._session = None  # Lazy HTTP session

    def list_items(self, timeout=None) -> list[Item]:
        response = self._get("/items", timeout=timeout)
        return [Item(**item) for item in response.json()]

    def create_item(self, item: Item, timeout=None) -> Item:
        response = self._post("/items", json=item.model_dump(), timeout=timeout)
        return Item(**response.json())

    def close(self):
        """Release HTTP session resources."""
        if self._session:
            self._session.close()

All clients have:

  • Lazy HTTP session initialization
  • close() method for cleanup
  • Optional EventClient injection for logging
  • Configurable retry behavior

Settings and Configuration

Prefixed Alias System

Each manager uses prefixed_alias_generator() for YAML-friendly configuration:

# settings.yaml (shared across managers)
event_server_url: http://localhost:8001/
event_database_name: madsci_events
resource_postgres_db_url: postgresql://localhost/resources
workcell_cache_url: redis://localhost:6379/
# In code, use unprefixed names:
self.settings.server_url       # "http://localhost:8001/"
self.settings.database_name    # "madsci_events"

Secret Marking and Redaction

# Type-based:
password: SecretStr

# Metadata-based:
api_key: str = Field(json_schema_extra={"secret": True})

# Safe export (redacts secrets):
settings.model_dump_safe()

# Settings endpoint uses:
get_settings_export(include_secrets=False)  # Default

Environment Variable Precedence

CLI args > init kwargs > env vars > .env > file secrets > JSON > TOML > YAML > field defaults

Deprecated: Manager Definition Types

All *ManagerDefinition classes (e.g., ManagerDefinition, LabManagerDefinition, WorkcellManagerDefinition) are deprecated as of v0.7.0 and will be removed. They emit MadsciDeprecationWarning on instantiation.

  • Use ManagerSettings subclasses for all configuration (e.g., EventManagerSettings, WorkcellManagerSettings)
  • Use WorkcellInfo for workcell runtime state (not WorkcellManagerDefinition)
  • Definition files (*.manager.yaml) are replaced by settings.yaml + environment variables
  • Run madsci migrate to convert legacy definition files

EventClient Dual Nature

EventClient serves as both a logging interface and an HTTP client:

# Logging (always works, even without Event Manager)
self.logger.info("Operation started", event_type=EventType.WORKFLOW_START, workflow_id=wf_id)
self.logger.warning("Threshold exceeded", value=42.5)
self.logger.error("Connection failed", error=str(e))

# HTTP client (requires Event Manager)
events = self.event_client.get_events(number=10)
event = self.event_client.get_event(event_id)

Structured logging best practice:

# Good: kwargs are queryable
self.logger.info("Step completed", step_index=3, duration_ms=150)

# Bad: f-string data is not queryable
self.logger.info(f"Step 3 completed in 150ms")

Health Check Pattern

def get_health(self) -> MyManagerHealth:
    health = MyManagerHealth(healthy=True)
    try:
        health.db_connected = self._document_handler.ping()
        health.total_items = self._get_count()
    except Exception as e:
        health.healthy = False
        health.description = f"Database error: {e}"
    return health

ManagerHealth uses model_config = ConfigDict(extra="allow") so subclasses can freely add fields.

Testing Managers

from madsci.common.db_handlers import (
    InMemoryDocumentStorageHandler,
    InMemoryCacheHandler,
    SQLiteHandler,
)

def test_event_manager():
    manager = EventManager(
        settings=EventManagerSettings(enable_registry_resolution=False),
        document_handler=InMemoryDocumentStorageHandler(),
    )
    app = manager.create_server()
    client = TestClient(app)

    # Test endpoints
    response = client.get("/health")
    assert response.status_code == 200
    assert response.json()["healthy"] is True

def test_workcell_manager():
    manager = WorkcellManager(
        settings=WorkcellManagerSettings(enable_registry_resolution=False),
        document_handler=InMemoryDocumentStorageHandler(),
        cache_handler=InMemoryCacheHandler(),
    )
    ...

def test_resource_manager():
    manager = ResourceManager(
        settings=ResourceManagerSettings(enable_registry_resolution=False),
        postgres_handler=SQLiteHandler(),
    )
    ...

Critical test pattern: Always set enable_registry_resolution=False to prevent registry lockfile usage in tests.

OpenTelemetry Integration

Per-manager configuration via environment:

EVENT_OTEL_ENABLED=true
EVENT_OTEL_SERVICE_NAME="madsci.event"
EVENT_OTEL_EXPORTER="otlp"           # or "console"
EVENT_OTEL_ENDPOINT="http://localhost:4317"
EVENT_OTEL_PROTOCOL="grpc"           # or "http"

In code:

with self.span("process_data", attributes={"data.size": 100}) as span:
    result = process(data)
    span.set_attribute("result.count", len(result))

Common Pitfalls

  • ULID not UUID: Use new_ulid_str() for all IDs
  • prefixed vs unprefixed: Code uses self.settings.server_url, YAML uses event_server_url
  • model_dump(by_alias=True): Produces prefixed keys for YAML export
  • Registry in tests: Always enable_registry_resolution=False
  • Handler injection: Pass handlers via constructor for dependency injection (production and test)
  • FerretDB, not MongoDB: The system uses FerretDB (MongoDB wire protocol on PostgreSQL). pymongo client works unchanged.
  • Valkey, not Redis: Drop-in Redis replacement. redis-py client works unchanged.
  • EventClient recursive logging: Event Manager's own EventClient must have event_server_url=None
  • AnyUrl trailing slash: All URLs stored via Pydantic AnyUrl get a trailing slash
  • Secret fields: Use json_schema_extra={"secret": True} or SecretStr type