# services/comfy-agent/app/nats_client.py import json import asyncio import hashlib from nats.aio.client import Client as NATS from .config import settings from .jobs import JOB_STORE from .worker import enqueue from . import idempotency def _hash_payload(gen_type: str, workflow: dict) -> str: normalized = json.dumps( {"type": gen_type, "workflow": workflow}, sort_keys=True, separators=(",", ":"), ) return hashlib.sha256(normalized.encode("utf-8")).hexdigest() async def start_nats() -> NATS: nc = NATS() await nc.connect(servers=[settings.NATS_URL]) async def handle(msg): subj = msg.subject reply = msg.reply payload = json.loads(msg.data.decode("utf-8")) # payload contract (MVP): # { "type": "text-to-image|text-to-video", "workflow": {...} } gen_type = payload.get("type", "text-to-image") workflow = payload.get("workflow") if not workflow: if reply: await nc.publish(reply, json.dumps({"error": "missing_workflow"}).encode()) return idem_key = (payload.get("idempotency_key") or "").strip() if idem_key and idempotency.IDEMPOTENCY_STORE is not None: result = idempotency.IDEMPOTENCY_STORE.reserve( idem_key=idem_key, gen_type=gen_type, req_hash=_hash_payload(gen_type, workflow), job_id=JOB_STORE.new_job_id(), ) if result.decision == "conflict": if reply: await nc.publish(reply, json.dumps({"error": "idempotency_key_reused_with_different_payload"}).encode()) return job = JOB_STORE.get(result.job_id) or JOB_STORE.create(gen_type, job_id=result.job_id) if result.decision == "created": enqueue(job.job_id, gen_type, workflow) else: job = JOB_STORE.create(gen_type) enqueue(job.job_id, gen_type, workflow) if reply: await nc.publish(reply, json.dumps({"job_id": job.job_id}).encode()) await nc.subscribe(settings.NATS_SUBJECT_INVOKE, cb=handle) await nc.subscribe(settings.NATS_SUBJECT_IMAGE, cb=handle) await nc.subscribe(settings.NATS_SUBJECT_VIDEO, cb=handle) return nc