asyncapi: 2.6.0

info:
  title: Jope Inference Server · ZMQ Channel
  version: 0.1.0
  description: |
    Hot-path inference channel between the Jope.SMB Operator Console (C# / NetMQ)
    and the stateless Python Inference Server.

    * **Pattern** — ZMQ REQ-REP (synchronous, single in-flight per client socket).
    * **Serialization** — MessagePack binary, wrapping a JSON-shape envelope.
    * **Latency budget** — `predict` round-trip ≤ 20 ms p95; `ping` ≤ 5 ms p95.

    Cold-path management (model load, training jobs, health) uses a separate
    HTTP/JSON API — see the OpenAPI specification.

    > **Why MessagePack** — 2048 float intensities + envelope collapses to
    > ~16 KB binary vs ~60 KB JSON. Under a 20 ms budget this matters.
  contact:
    name: Jope Technology Co., Ltd.
    url: https://jope-docs.pages.dev
  license:
    name: Proprietary — Jope internal use only
  tags:
    - name: hot-path
      description: Inference round-trip measured in tens of ms.
    - name: diagnostics
      description: Low-frequency metadata queries.

defaultContentType: application/msgpack

servers:
  production:
    url: tcp://{inferenceHost}:5555
    protocol: zeromq
    description: Plant-LAN Inference Host REP socket.
    variables:
      inferenceHost:
        default: 10.0.1.42
        description: IP or DNS name of the dedicated Inference Host on the plant LAN.
  mock:
    url: wss://jope-docs.pages.dev/mock-zmq
    protocol: wss
    description: |
      Mock server (WebSocket bridge) used by the API Explorer's **Try It**
      button. Not a real ZMQ endpoint — only for documentation purposes.

channels:
  inference:
    description: |
      Single REQ-REP channel carrying all hot-path traffic. The server handles
      one message at a time per connection; clients should reuse one long-lived
      socket.
    publish:
      operationId: sendRequest
      summary: Client sends a request envelope.
      message:
        oneOf:
          - $ref: '#/components/messages/PingRequest'
          - $ref: '#/components/messages/PredictRequest'
          - $ref: '#/components/messages/ModelInfoRequest'
    subscribe:
      operationId: receiveReply
      summary: Client receives a reply envelope correlated by `correlation_id`.
      message:
        oneOf:
          - $ref: '#/components/messages/PongReply'
          - $ref: '#/components/messages/PredictReply'
          - $ref: '#/components/messages/ModelInfoReply'

components:

  messages:

    PingRequest:
      name: PingRequest
      title: ping · Heartbeat request
      summary: Console sends every 2 s. Three consecutive timeouts → alarm.
      contentType: application/msgpack
      tags: [{name: hot-path}]
      payload:
        allOf:
          - $ref: '#/components/schemas/RequestEnvelope'
          - type: object
            properties:
              type: {const: ping}
              body:
                type: object
                description: Empty object.

    PongReply:
      name: PongReply
      title: pong · Heartbeat reply
      contentType: application/msgpack
      tags: [{name: hot-path}]
      payload:
        allOf:
          - $ref: '#/components/schemas/ReplyEnvelope'
          - type: object
            properties:
              type: {const: pong}
              body: {$ref: '#/components/schemas/PongBody'}
      examples:
        - name: healthy
          payload:
            v: 1
            type: pong
            id: "e9a2-7d11-..."
            correlation_id: "c7f3-6b21-..."
            ts: 1713760200.135
            body:
              server_version: 0.3.1
              protocol_version: 1
              model_version: v5
              uptime_seconds: 3847
              python_version: 3.11.4
            error: null

    PredictRequest:
      name: PredictRequest
      title: predict · Core inference request
      summary: |
        One Raman scan → concentration prediction. The server is stateless;
        `context` fields are for logging only.
      contentType: application/msgpack
      tags: [{name: hot-path}]
      payload:
        allOf:
          - $ref: '#/components/schemas/RequestEnvelope'
          - type: object
            properties:
              type: {const: predict}
              body: {$ref: '#/components/schemas/PredictRequestBody'}
      examples:
        - name: extractPort1
          payload:
            v: 1
            type: predict
            id: "c7f3-6b21-..."
            ts: 1713760200.123
            body:
              spectrum:
                wavenumbers: [200.0, 201.5, "…", 3200.0]
                intensities: [0.0123, 0.0098, "…"]
                channel: 1
                integration_ms: 1000
                scan_seq: 42847
              context:
                batch_id: PR-2026-0487
                port: extract-E1
                column_index: 3

    PredictReply:
      name: PredictReply
      title: predict_reply · Prediction result
      contentType: application/msgpack
      tags: [{name: hot-path}]
      payload:
        allOf:
          - $ref: '#/components/schemas/ReplyEnvelope'
          - type: object
            properties:
              type: {const: predict_reply}
              body: {$ref: '#/components/schemas/PredictReplyBody'}
      examples:
        - name: success
          payload:
            v: 1
            type: predict_reply
            id: "e9a2-..."
            correlation_id: "c7f3-..."
            ts: 1713760200.135
            body:
              concentrations: {EPA: 5.234, DHA: 3.187, DPA: 1.823}
              confidence:     {EPA: 0.95, DHA: 0.92, DPA: 0.88}
              model_version: v5
              inference_ms: 8.3
            error: null
        - name: modelNotLoaded
          payload:
            v: 1
            type: predict_reply
            id: "e9a2-..."
            correlation_id: "c7f3-..."
            ts: 1713760200.135
            body: null
            error:
              code: MODEL_NOT_LOADED
              message: "No active model. Upload and load a model first via REST /model/load."
              retryable: false

    ModelInfoRequest:
      name: ModelInfoRequest
      title: model_info · Metadata query
      summary: Called when Operator switches to Model Select page.
      contentType: application/msgpack
      tags: [{name: diagnostics}]
      payload:
        allOf:
          - $ref: '#/components/schemas/RequestEnvelope'
          - type: object
            properties:
              type: {const: model_info}
              body:
                type: object
                description: Empty object.

    ModelInfoReply:
      name: ModelInfoReply
      title: model_info_reply · Metadata result
      contentType: application/msgpack
      tags: [{name: diagnostics}]
      payload:
        allOf:
          - $ref: '#/components/schemas/ReplyEnvelope'
          - type: object
            properties:
              type: {const: model_info_reply}
              body: {$ref: '#/components/schemas/ModelInfoBody'}

  schemas:

    RequestEnvelope:
      type: object
      required: [v, type, id, ts, body]
      properties:
        v:
          type: integer
          const: 1
          description: Envelope protocol version. Bump on breaking changes.
        type:
          type: string
          description: Message type discriminator.
          enum: [ping, predict, model_info]
        id:
          type: string
          format: uuid
          description: Unique request id (uuid4).
          example: c7f3-6b21-9a04-...
        ts:
          type: number
          format: double
          description: Unix timestamp (fractional seconds) on sender clock.
          example: 1713760200.123
        body:
          description: Type-specific payload. Shape depends on `type`.

    ReplyEnvelope:
      type: object
      required: [v, type, id, correlation_id, ts]
      properties:
        v: {type: integer, const: 1}
        type:
          type: string
          enum: [pong, predict_reply, model_info_reply]
        id: {type: string, format: uuid, description: Unique reply id.}
        correlation_id:
          type: string
          format: uuid
          description: Matches the originating request's `id`.
        ts: {type: number, format: double}
        body:
          nullable: true
          description: Type-specific payload, `null` when `error` is set.
        error:
          nullable: true
          $ref: '#/components/schemas/Error'

    PongBody:
      type: object
      required: [server_version, protocol_version]
      properties:
        server_version: {type: string, example: 0.3.1}
        protocol_version:
          type: integer
          description: Envelope version the server speaks. Must match client's `v`.
          example: 1
        model_version:
          type: string
          nullable: true
          example: v5
        uptime_seconds: {type: integer, minimum: 0, example: 3847}
        python_version: {type: string, example: 3.11.4}

    PredictRequestBody:
      type: object
      required: [spectrum]
      properties:
        spectrum: {$ref: '#/components/schemas/Spectrum'}
        context:
          type: object
          description: Logging-only metadata. Server does not read these fields.
          properties:
            batch_id: {type: string, example: PR-2026-0487}
            port:     {type: string, example: extract-E1}
            column_index: {type: integer, minimum: 1, example: 3}

    Spectrum:
      type: object
      required: [wavenumbers, intensities, channel, integration_ms, scan_seq]
      properties:
        wavenumbers:
          type: array
          items: {type: number}
          minItems: 1
          description: Wavenumbers in cm⁻¹. Typically 2048 values for RS2000.
        intensities:
          type: array
          items: {type: number}
          minItems: 1
          description: Same length as `wavenumbers`; counts or arbitrary intensity unit.
        channel:
          type: integer
          enum: [1, 2]
          description: RS2000 dual-probe channel index.
        integration_ms:
          type: integer
          minimum: 1
          description: Exposure time (ms).
          example: 1000
        scan_seq:
          type: integer
          minimum: 0
          description: Console-assigned sequence (used to join Historian rows).
          example: 42847

    PredictReplyBody:
      type: object
      required: [concentrations, model_version, inference_ms]
      properties:
        concentrations:
          type: object
          description: Predicted concentration per component (g/L).
          properties:
            EPA: {type: number, example: 5.234}
            DHA: {type: number, example: 3.187}
            DPA: {type: number, example: 1.823}
        confidence:
          type: object
          description: 0–1 confidence per component (Q-residual based).
          properties:
            EPA: {type: number, minimum: 0, maximum: 1, example: 0.95}
            DHA: {type: number, minimum: 0, maximum: 1, example: 0.92}
            DPA: {type: number, minimum: 0, maximum: 1, example: 0.88}
        model_version: {type: string, example: v5}
        inference_ms:
          type: number
          description: Server-side processing time (excludes wire time).
          example: 8.3

    ModelInfoBody:
      type: object
      properties:
        active_version: {type: string, example: v5}
        algorithm: {type: string, example: PLS+Ridge}
        latent_components: {type: integer, minimum: 1, example: 5}
        trained_at: {type: string, format: date-time}
        trained_samples: {type: integer, example: 187}
        rmse:
          type: object
          properties:
            EPA: {type: number, example: 0.047}
            DHA: {type: number, example: 0.052}
            DPA: {type: number, example: 0.061}
        r2:
          type: object
          properties:
            EPA: {type: number, example: 0.958}
            DHA: {type: number, example: 0.942}
            DPA: {type: number, example: 0.913}
        wavenumber_range:
          type: array
          items: {type: number}
          minItems: 2
          maxItems: 2
          example: [200.0, 3200.0]
        wavenumber_count: {type: integer, example: 2048}

    Error:
      type: object
      required: [code, message]
      properties:
        code:
          type: string
          description: Machine-readable error code.
          enum:
            - MODEL_NOT_LOADED
            - INVALID_SPECTRUM
            - SPECTRUM_OUT_OF_RANGE
            - PROTOCOL_VERSION_MISMATCH
            - INTERNAL_ERROR
          example: MODEL_NOT_LOADED
        message:
          type: string
          description: Human-readable explanation for logs / Operator alarm text.
        retryable:
          type: boolean
          default: false
          description: |
            Client hint. `true` → transient (Console retries once). `false` → deterministic
            error, surface to Operator; requires signature to override.
