feat(observatory): ship modular AI inference labs
This commit is contained in:
+108
-64
@@ -42,11 +42,19 @@ from k1link.observatory import (
|
||||
ObservatoryRunPreparationLedger,
|
||||
load_observatory_run_preparation_ledger,
|
||||
)
|
||||
from k1link.observatory.composition_runs import CompositionRunError, CompositionRunStore
|
||||
from k1link.observatory.domain_ontology import (
|
||||
ObservatoryDomainOntology,
|
||||
ObservatoryOntologyError,
|
||||
)
|
||||
from k1link.observatory.lab_view_profiles import LabViewProfileError, LabViewProfileStore
|
||||
from k1link.observatory.m49_queue_binding import (
|
||||
M49QueueBindingConfig,
|
||||
M49QueueBindingError,
|
||||
M49RecordedQueueBindingService,
|
||||
)
|
||||
from k1link.observatory.modular_composition import CompositionError, ModuleRegistry
|
||||
from k1link.observatory.modular_composition_store import ModularCompositionStore
|
||||
from k1link.observatory.portable_publication_reconciler import (
|
||||
PortablePublicationReconciler,
|
||||
)
|
||||
@@ -196,6 +204,7 @@ from k1link.web.map_api import (
|
||||
build_map_router,
|
||||
)
|
||||
from k1link.web.map_view_api import build_map_view_router
|
||||
from k1link.web.modular_observatory_api import build_modular_observatory_router
|
||||
from k1link.web.observatory_api import build_observatory_router
|
||||
from k1link.web.observatory_worker_api import (
|
||||
ObservatoryWorkerAuthentication,
|
||||
@@ -288,6 +297,37 @@ plugin_environment = load_installed_device_plugins(REPOSITORY_ROOT)
|
||||
plugin_catalog: DevicePluginCatalog = plugin_environment.catalog
|
||||
plugin_dispatcher: DevicePluginDispatcher = plugin_environment.dispatcher
|
||||
session_store = SessionStore(REPOSITORY_ROOT)
|
||||
OBSERVATORY_AI_COMPOSITIONS: ModularCompositionStore | None
|
||||
OBSERVATORY_AI_COMPOSITION_RUNS: CompositionRunStore | None
|
||||
OBSERVATORY_DOMAIN_ONTOLOGY: ObservatoryDomainOntology | None
|
||||
OBSERVATORY_LAB_VIEW_PROFILES: LabViewProfileStore | None
|
||||
try:
|
||||
OBSERVATORY_DOMAIN_ONTOLOGY = ObservatoryDomainOntology.from_file(
|
||||
REPOSITORY_ROOT / "config" / "observatory-domain-ontology.json"
|
||||
)
|
||||
OBSERVATORY_AI_COMPOSITIONS = ModularCompositionStore(
|
||||
session_store.data_dir / "observatory-ai-compositions",
|
||||
ModuleRegistry.from_file(REPOSITORY_ROOT / "config" / "observatory-ai-modules.json"),
|
||||
)
|
||||
OBSERVATORY_AI_COMPOSITION_RUNS = CompositionRunStore(
|
||||
session_store.data_dir / "observatory-ai-composition-runs"
|
||||
)
|
||||
OBSERVATORY_LAB_VIEW_PROFILES = LabViewProfileStore(
|
||||
session_store.data_dir / "observatory-lab-view-profiles"
|
||||
)
|
||||
except (
|
||||
CompositionError,
|
||||
CompositionRunError,
|
||||
LabViewProfileError,
|
||||
ObservatoryOntologyError,
|
||||
OSError,
|
||||
ValueError,
|
||||
):
|
||||
# A modular catalog failure cannot disable recordings, existing LABs or Legacy.
|
||||
OBSERVATORY_AI_COMPOSITIONS = None
|
||||
OBSERVATORY_AI_COMPOSITION_RUNS = None
|
||||
OBSERVATORY_DOMAIN_ONTOLOGY = None
|
||||
OBSERVATORY_LAB_VIEW_PROFILES = None
|
||||
|
||||
OBSERVATORY_PORTABLE_DEFINITION_REGISTRY: PortableRunDefinitionRegistry | None
|
||||
OBSERVATORY_PORTABLE_DEFINITION_REGISTRY_ERROR: str | None
|
||||
@@ -322,9 +362,7 @@ def _resolve_observatory_calculation_profile(
|
||||
summary: SessionSummary,
|
||||
) -> dict[str, object] | None:
|
||||
if OBSERVATORY_LABORATORY_SETUP_REGISTRY is not None:
|
||||
legacy = OBSERVATORY_LABORATORY_SETUP_REGISTRY.observatory_calculation_profile(
|
||||
summary
|
||||
)
|
||||
legacy = OBSERVATORY_LABORATORY_SETUP_REGISTRY.observatory_calculation_profile(summary)
|
||||
if legacy is not None:
|
||||
return legacy
|
||||
if (
|
||||
@@ -437,47 +475,30 @@ try:
|
||||
or "portable definition registry is unavailable"
|
||||
)
|
||||
if OBSERVATORY_PORTABLE_CALCULATION_PROFILES is None:
|
||||
raise PortableWorkerIntegrationError(
|
||||
"portable calculation profile registry is unavailable"
|
||||
)
|
||||
raise PortableWorkerIntegrationError("portable calculation profile registry is unavailable")
|
||||
if OBSERVATORY_PORTABLE_RESULT_VALIDATORS is None:
|
||||
raise PortableWorkerIntegrationError(
|
||||
"portable result validator registry is unavailable"
|
||||
)
|
||||
raise PortableWorkerIntegrationError("portable result validator registry is unavailable")
|
||||
if OBSERVATORY_RECORDED_JOB_QUEUE is None:
|
||||
raise PortableWorkerIntegrationError(
|
||||
OBSERVATORY_RECORDED_JOB_QUEUE_ERROR
|
||||
or "Observatory recorded-job queue is unavailable"
|
||||
OBSERVATORY_RECORDED_JOB_QUEUE_ERROR or "Observatory recorded-job queue is unavailable"
|
||||
)
|
||||
if session_artifact_gateway is None:
|
||||
raise PortableWorkerIntegrationError(
|
||||
"central artifact store is not configured"
|
||||
)
|
||||
raise PortableWorkerIntegrationError("central artifact store is not configured")
|
||||
if session_artifact_gateway.status().central_status != "ready":
|
||||
raise PortableWorkerIntegrationError(
|
||||
"central artifact store is unavailable"
|
||||
)
|
||||
OBSERVATORY_PORTABLE_WORKER_STORAGE_ROOTS = (
|
||||
PortableWorkerStorageRoots.from_environment(
|
||||
artifact_store_root=session_artifact_gateway.store.root,
|
||||
)
|
||||
raise PortableWorkerIntegrationError("central artifact store is unavailable")
|
||||
OBSERVATORY_PORTABLE_WORKER_STORAGE_ROOTS = PortableWorkerStorageRoots.from_environment(
|
||||
artifact_store_root=session_artifact_gateway.store.root,
|
||||
)
|
||||
OBSERVATORY_PORTABLE_WORKER_INTEGRATION = (
|
||||
build_portable_observatory_worker_integration(
|
||||
queue=OBSERVATORY_RECORDED_JOB_QUEUE,
|
||||
session_store=session_store,
|
||||
media_inspector=session_recorded_media_inspector,
|
||||
definitions=OBSERVATORY_PORTABLE_DEFINITION_REGISTRY,
|
||||
artifact_store=session_artifact_gateway.store,
|
||||
calculation_profiles=OBSERVATORY_PORTABLE_CALCULATION_PROFILES,
|
||||
validators=OBSERVATORY_PORTABLE_RESULT_VALIDATORS,
|
||||
source_cas_root=(
|
||||
OBSERVATORY_PORTABLE_WORKER_STORAGE_ROOTS.source_cas_root
|
||||
),
|
||||
result_staging_root=(
|
||||
OBSERVATORY_PORTABLE_WORKER_STORAGE_ROOTS.result_staging_root
|
||||
),
|
||||
)
|
||||
OBSERVATORY_PORTABLE_WORKER_INTEGRATION = build_portable_observatory_worker_integration(
|
||||
queue=OBSERVATORY_RECORDED_JOB_QUEUE,
|
||||
session_store=session_store,
|
||||
media_inspector=session_recorded_media_inspector,
|
||||
definitions=OBSERVATORY_PORTABLE_DEFINITION_REGISTRY,
|
||||
artifact_store=session_artifact_gateway.store,
|
||||
calculation_profiles=OBSERVATORY_PORTABLE_CALCULATION_PROFILES,
|
||||
validators=OBSERVATORY_PORTABLE_RESULT_VALIDATORS,
|
||||
source_cas_root=(OBSERVATORY_PORTABLE_WORKER_STORAGE_ROOTS.source_cas_root),
|
||||
result_staging_root=(OBSERVATORY_PORTABLE_WORKER_STORAGE_ROOTS.result_staging_root),
|
||||
)
|
||||
OBSERVATORY_PORTABLE_WORKER_INTEGRATION_ERROR = None
|
||||
except (PortableWorkerIntegrationError, OSError, ValueError) as exc:
|
||||
@@ -487,10 +508,7 @@ except (PortableWorkerIntegrationError, OSError, ValueError) as exc:
|
||||
OBSERVATORY_PORTABLE_WORKER_INTEGRATION_ERROR = str(exc)
|
||||
OBSERVATORY_PUBLICATION_RECONCILER = (
|
||||
None
|
||||
if (
|
||||
OBSERVATORY_RECORDED_JOB_QUEUE is None
|
||||
or OBSERVATORY_PORTABLE_WORKER_INTEGRATION is None
|
||||
)
|
||||
if (OBSERVATORY_RECORDED_JOB_QUEUE is None or OBSERVATORY_PORTABLE_WORKER_INTEGRATION is None)
|
||||
else PortablePublicationReconciler(
|
||||
queue=OBSERVATORY_RECORDED_JOB_QUEUE,
|
||||
artifact_transport=OBSERVATORY_PORTABLE_WORKER_INTEGRATION.artifact_transport,
|
||||
@@ -499,12 +517,10 @@ OBSERVATORY_PUBLICATION_RECONCILER = (
|
||||
)
|
||||
OBSERVATORY_WORKER_API_GATE_ENABLED = OBSERVATORY_WORKER_LOCAL_ENABLED
|
||||
OBSERVATORY_WORKER_CLAIM_LEASE_READY = (
|
||||
OBSERVATORY_WORKER_API_GATE_ENABLED
|
||||
and OBSERVATORY_RECORDED_JOB_QUEUE is not None
|
||||
OBSERVATORY_WORKER_API_GATE_ENABLED and OBSERVATORY_RECORDED_JOB_QUEUE is not None
|
||||
)
|
||||
OBSERVATORY_WORKER_VERIFIED_RESULT_PUBLISHER_READY = (
|
||||
OBSERVATORY_WORKER_API_GATE_ENABLED
|
||||
and OBSERVATORY_PORTABLE_WORKER_INTEGRATION is not None
|
||||
OBSERVATORY_WORKER_API_GATE_ENABLED and OBSERVATORY_PORTABLE_WORKER_INTEGRATION is not None
|
||||
)
|
||||
OBSERVATORY_WORKER_DISPATCH_READY = (
|
||||
OBSERVATORY_WORKER_CLAIM_LEASE_READY
|
||||
@@ -527,17 +543,13 @@ else:
|
||||
)
|
||||
if OBSERVATORY_WORKER_AUTHENTICATION_ERROR is not None:
|
||||
worker_api_errors.append(
|
||||
"authentication unavailable: "
|
||||
f"{OBSERVATORY_WORKER_AUTHENTICATION_ERROR}"
|
||||
f"authentication unavailable: {OBSERVATORY_WORKER_AUTHENTICATION_ERROR}"
|
||||
)
|
||||
if OBSERVATORY_PORTABLE_WORKER_INTEGRATION_ERROR is not None:
|
||||
worker_api_errors.append(
|
||||
"integration unavailable: "
|
||||
f"{OBSERVATORY_PORTABLE_WORKER_INTEGRATION_ERROR}"
|
||||
f"integration unavailable: {OBSERVATORY_PORTABLE_WORKER_INTEGRATION_ERROR}"
|
||||
)
|
||||
OBSERVATORY_WORKER_API_ERROR = "Worker pull API is disabled; " + "; ".join(
|
||||
worker_api_errors
|
||||
)
|
||||
OBSERVATORY_WORKER_API_ERROR = "Worker pull API is disabled; " + "; ".join(worker_api_errors)
|
||||
OBSERVATORY_PORTABLE_BINDING_SERVICE: PortableRecordedQueueBindingService | None
|
||||
OBSERVATORY_PORTABLE_SETUP_PROJECTOR: PortableSetupProjector | None
|
||||
OBSERVATORY_PORTABLE_SETUP_PROJECTOR_ERROR: str | None
|
||||
@@ -554,7 +566,8 @@ try:
|
||||
and OBSERVATORY_PORTABLE_CALCULATION_PROFILES is not None
|
||||
):
|
||||
OBSERVATORY_PORTABLE_RESULT_CACHE = PortableResultCache(
|
||||
sessions=session_store, artifacts=session_artifact_gateway.store,
|
||||
sessions=session_store,
|
||||
artifacts=session_artifact_gateway.store,
|
||||
queue=OBSERVATORY_RECORDED_JOB_QUEUE,
|
||||
definitions=OBSERVATORY_PORTABLE_DEFINITION_REGISTRY,
|
||||
calculation_profiles=OBSERVATORY_PORTABLE_CALCULATION_PROFILES,
|
||||
@@ -566,7 +579,8 @@ try:
|
||||
definitions=OBSERVATORY_PORTABLE_DEFINITION_REGISTRY,
|
||||
queue=OBSERVATORY_RECORDED_JOB_QUEUE,
|
||||
published_result_available=(
|
||||
None if OBSERVATORY_PORTABLE_RESULT_CACHE is None
|
||||
None
|
||||
if OBSERVATORY_PORTABLE_RESULT_CACHE is None
|
||||
else OBSERVATORY_PORTABLE_RESULT_CACHE.available
|
||||
),
|
||||
)
|
||||
@@ -874,10 +888,19 @@ async def _portable_result_publication_reconciler() -> None:
|
||||
await asyncio.sleep(15.0)
|
||||
|
||||
|
||||
async def _recorded_blueprint_resource_reaper() -> None:
|
||||
from k1link.viewer.recorded import recorded_blueprint_sessions
|
||||
|
||||
while True:
|
||||
await asyncio.sleep(30.0)
|
||||
await asyncio.to_thread(recorded_blueprint_sessions.expire)
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def app_lifespan(_: FastAPI) -> AsyncIterator[None]:
|
||||
reconciler: asyncio.Task[None] | None = None
|
||||
publication_reconciler: asyncio.Task[None] | None = None
|
||||
blueprint_reaper: asyncio.Task[None] | None = None
|
||||
try:
|
||||
configure_scanner_diagnostics(session_store.data_dir / "logs")
|
||||
session_recording_preparation_manager.start()
|
||||
@@ -891,11 +914,17 @@ async def app_lifespan(_: FastAPI) -> AsyncIterator[None]:
|
||||
# expensive on field captures. Start it immediately in the background
|
||||
# instead of holding the ASGI startup gate.
|
||||
reconciler = asyncio.create_task(_recording_preparation_reconciler())
|
||||
publication_reconciler = asyncio.create_task(
|
||||
_portable_result_publication_reconciler()
|
||||
)
|
||||
publication_reconciler = asyncio.create_task(_portable_result_publication_reconciler())
|
||||
blueprint_reaper = asyncio.create_task(_recorded_blueprint_resource_reaper())
|
||||
yield
|
||||
finally:
|
||||
from k1link.viewer.recorded import recorded_blueprint_sessions
|
||||
|
||||
if blueprint_reaper is not None:
|
||||
blueprint_reaper.cancel()
|
||||
with suppress(asyncio.CancelledError):
|
||||
await blueprint_reaper
|
||||
await asyncio.to_thread(recorded_blueprint_sessions.close)
|
||||
await map_gateway_proxy.close()
|
||||
if reconciler is not None:
|
||||
reconciler.cancel()
|
||||
@@ -1066,7 +1095,9 @@ if session_artifact_gateway is not None and _ffmpeg is not None:
|
||||
media=session_recorded_media_inspector,
|
||||
recording_source=_canonical_lab_recording_source,
|
||||
ffmpeg_path=_ffmpeg,
|
||||
)
|
||||
),
|
||||
composition_runs=OBSERVATORY_AI_COMPOSITION_RUNS,
|
||||
queue=OBSERVATORY_RECORDED_JOB_QUEUE,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1128,6 +1159,23 @@ app.include_router(
|
||||
),
|
||||
)
|
||||
)
|
||||
if (
|
||||
OBSERVATORY_AI_COMPOSITIONS is not None
|
||||
and OBSERVATORY_AI_COMPOSITION_RUNS is not None
|
||||
and OBSERVATORY_DOMAIN_ONTOLOGY is not None
|
||||
):
|
||||
app.include_router(
|
||||
build_modular_observatory_router(
|
||||
store=session_store,
|
||||
compositions=OBSERVATORY_AI_COMPOSITIONS,
|
||||
composition_runs=OBSERVATORY_AI_COMPOSITION_RUNS,
|
||||
ontology=OBSERVATORY_DOMAIN_ONTOLOGY,
|
||||
definitions=OBSERVATORY_PORTABLE_DEFINITION_REGISTRY,
|
||||
binding=OBSERVATORY_PORTABLE_BINDING_SERVICE,
|
||||
queue=OBSERVATORY_RECORDED_JOB_QUEUE,
|
||||
view_profiles=OBSERVATORY_LAB_VIEW_PROFILES,
|
||||
)
|
||||
)
|
||||
if OBSERVATORY_WORKER_DISPATCH_READY:
|
||||
assert OBSERVATORY_RECORDED_JOB_QUEUE is not None
|
||||
assert OBSERVATORY_WORKER_AUTHENTICATION is not None
|
||||
@@ -1136,12 +1184,8 @@ if OBSERVATORY_WORKER_DISPATCH_READY:
|
||||
build_observatory_worker_router(
|
||||
OBSERVATORY_RECORDED_JOB_QUEUE,
|
||||
authentication=OBSERVATORY_WORKER_AUTHENTICATION,
|
||||
artifact_transport=(
|
||||
OBSERVATORY_PORTABLE_WORKER_INTEGRATION.artifact_transport
|
||||
),
|
||||
result_publisher=(
|
||||
OBSERVATORY_PORTABLE_WORKER_INTEGRATION.result_publisher
|
||||
),
|
||||
artifact_transport=(OBSERVATORY_PORTABLE_WORKER_INTEGRATION.artifact_transport),
|
||||
result_publisher=(OBSERVATORY_PORTABLE_WORKER_INTEGRATION.result_publisher),
|
||||
)
|
||||
)
|
||||
app.include_router(
|
||||
|
||||
Reference in New Issue
Block a user