"""Node Worker — NATS offload executor for cross-node inference.""" import logging import os from fastapi import FastAPI import config import worker logging.basicConfig(level=logging.INFO) logger = logging.getLogger("node-worker") app = FastAPI(title="Node Worker", version="1.0.0") _nats_client = None @app.get("/healthz") async def healthz(): connected = _nats_client is not None and _nats_client.is_connected if _nats_client else False return { "status": "ok" if connected else "degraded", "node_id": config.NODE_ID, "nats_connected": connected, "max_concurrency": config.MAX_CONCURRENCY, } @app.get("/metrics") async def metrics(): return worker.get_metrics() @app.get("/prom_metrics") async def prom_metrics(): from fastapi.responses import Response import fabric_metrics as fm data = fm.get_metrics_text() if data: return Response(content=data, media_type="text/plain; charset=utf-8") return {"error": "prometheus_client not installed"} @app.get("/caps") async def caps(): """Capability flags for NCS to aggregate. Semantic vs operational separation (contract): - capabilities.voice_* = semantic availability (provider configured). True as long as the provider is configured, regardless of NATS state. Routing decisions are based on this. - runtime.nats_subscriptions.voice_* = operational (NATS sub active). Used for health/telemetry only — NOT for routing. This prevents false-negatives during reconnects / restart races. """ import worker as _w nid = config.NODE_ID.lower() # Semantic: provider configured → capability is available voice_tts_cap = config.TTS_PROVIDER != "none" voice_stt_cap = config.STT_PROVIDER != "none" voice_llm_cap = True # LLM always available when node-worker is up # Operational: actual NATS subscription state (health/telemetry only) nats_voice_tts_active = f"node.{nid}.voice.tts.request" in _w._VOICE_SUBJECTS nats_voice_stt_active = f"node.{nid}.voice.stt.request" in _w._VOICE_SUBJECTS nats_voice_llm_active = f"node.{nid}.voice.llm.request" in _w._VOICE_SUBJECTS return { "node_id": config.NODE_ID, "capabilities": { "llm": True, "vision": True, "stt": config.STT_PROVIDER != "none", "tts": config.TTS_PROVIDER != "none", "ocr": config.OCR_PROVIDER != "none", "image": config.IMAGE_PROVIDER != "none", # Voice HA semantic capability flags (provider-based, not NATS-based) "voice_tts": voice_tts_cap, "voice_llm": voice_llm_cap, "voice_stt": voice_stt_cap, }, "providers": { "stt": config.STT_PROVIDER, "tts": config.TTS_PROVIDER, "ocr": config.OCR_PROVIDER, "image": config.IMAGE_PROVIDER, }, "defaults": { "llm": config.DEFAULT_LLM, "vision": config.DEFAULT_VISION, }, "concurrency": config.MAX_CONCURRENCY, "voice_concurrency": { "voice_tts": config.VOICE_MAX_CONCURRENT_TTS, "voice_llm": config.VOICE_MAX_CONCURRENT_LLM, "voice_stt": config.VOICE_MAX_CONCURRENT_STT, }, # Operational NATS subscription state — for health/monitoring only "runtime": { "nats_subscriptions": { "voice_tts_active": nats_voice_tts_active, "voice_stt_active": nats_voice_stt_active, "voice_llm_active": nats_voice_llm_active, } }, } @app.on_event("startup") async def startup(): global _nats_client try: import nats as nats_lib _nats_client = await nats_lib.connect(config.NATS_URL) logger.info(f"✅ NATS connected: {config.NATS_URL}") await worker.start(_nats_client) logger.info(f"✅ Node Worker ready: node={config.NODE_ID} concurrency={config.MAX_CONCURRENCY}") except Exception as e: logger.error(f"❌ Startup failed: {e}") @app.on_event("shutdown") async def shutdown(): if _nats_client: try: await _nats_client.close() except Exception: pass if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=config.PORT)