Polling Cognigy.AI Model Training Jobs with TypeScript

Polling Cognigy.AI Model Training Jobs with TypeScript

What You Will Build

  • A TypeScript module that polls Cognigy.AI NLU training jobs using atomic GET requests, validates polling intervals, enforces abort directives, tracks latency, generates audit logs, and exposes callbacks for external orchestration.
  • The implementation uses the Cognigy.AI REST API v2 surface for training job lifecycle management.
  • The code is written in TypeScript and runs on Node.js 18 or later.

Prerequisites

  • OAuth2 client credentials with the cognigy.ai:api:read scope
  • Cognigy.AI API v2
  • Node.js 18+ with native fetch support
  • Dependencies: typescript, zod, dotenv, @types/node
  • Project initialized with npm init -y and compiled with ts-node or tsc

Authentication Setup

Cognigy.AI requires a bearer token for all training API calls. The client credentials flow exchanges your client ID and secret for a short-lived token. The following code demonstrates token acquisition, in-memory caching, and automatic refresh when the token expires.

import dotenv from 'dotenv';
dotenv.config();

export interface CognigyTokenResponse {
  access_token: string;
  token_type: string;
  expires_in: number;
  scope: string;
}

const COGNIGY_BASE_URL = process.env.COGNIGY_HOST || 'https://your-instance.cognigy.ai';
const AUTH_ENDPOINT = `${COGNIGY_BASE_URL}/api/v2/auth/token`;

export class CognigyAuthClient {
  private token: string | null = null;
  private expiry: number = 0;

  async getToken(): Promise<string> {
    if (this.token && Date.now() < this.expiry) {
      return this.token;
    }

    const params = new URLSearchParams({
      grant_type: 'client_credentials',
      client_id: process.env.COGNIGY_CLIENT_ID || '',
      client_secret: process.env.COGNIGY_CLIENT_SECRET || '',
      scope: 'cognigy.ai:api:read'
    });

    const response = await fetch(AUTH_ENDPOINT, {
      method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      body: params
    });

    if (!response.ok) {
      const errorBody = await response.text();
      throw new Error(`Token acquisition failed (${response.status}): ${errorBody}`);
    }

    const data: CognigyTokenResponse = await response.json();
    this.token = data.access_token;
    this.expiry = Date.now() + (data.expires_in * 1000) - 5000; // 5s buffer
    return this.token;
  }
}

Implementation

Step 1: Poll Configuration Schema & Validation

The polling engine requires a structured configuration containing the job UUID, checkpoint interval matrix, and abort directives. We validate this configuration against Cognigy.AI training service constraints to prevent polling failure and enforce maximum frequency limits.

import { z } from 'zod';

const MIN_INTERVAL_MS = 5000; // Cognigy.AI enforces minimum 5s between status checks
const MAX_CHECKPOINTS = 10;

export const PollConfigSchema = z.object({
  jobId: z.string().uuid('jobId must be a valid UUID'),
  checkpointIntervals: z.array(z.number().int().positive())
    .min(1, 'At least one checkpoint interval is required')
    .max(MAX_CHECKPOINTS, `Maximum ${MAX_CHECKPOINTS} checkpoints allowed`)
    .refine(arr => arr.every(interval => interval >= MIN_INTERVAL_MS), {
      message: `Each interval must be at least ${MIN_INTERVAL_MS}ms to respect API rate limits`
    }),
  abortOnThreshold: z.number().int().positive(),
  maxRetries: z.number().int().nonnegative().default(3)
});

export type PollConfig = z.infer<typeof PollConfigSchema>;

export function validatePollConfig(config: unknown): PollConfig {
  const result = PollConfigSchema.safeParse(config);
  if (!result.success) {
    const errors = result.error.errors.map(e => `${e.path.join('.')}: ${e.message}`).join('; ');
    throw new Error(`Poll configuration validation failed: ${errors}`);
  }
  return result.data;
}

Step 2: Atomic GET Status Check & Abort Logic

Status checks must be atomic to avoid race conditions during Cognigy.AI scaling events. We use an AbortController to enforce the abort directive and verify the response payload format before processing.

interface TrainingStatusResponse {
  status: 'PENDING' | 'RUNNING' | 'COMPLETED' | 'FAILED' | 'CANCELLED';
  progress: number;
  checkpoint?: number;
  error?: string;
}

export async function fetchTrainingStatus(
  jobId: string,
  token: string,
  signal: AbortSignal
): Promise<TrainingStatusResponse> {
  const url = `${COGNIGY_BASE_URL}/api/v2/nlu/training/${jobId}`;
  
  const response = await fetch(url, {
    method: 'GET',
    headers: {
      'Authorization': `Bearer ${token}`,
      'Content-Type': 'application/json',
      'Accept': 'application/json'
    },
    signal
  });

  if (response.status === 401) throw new Error('UNAUTHORIZED: Token expired or invalid');
  if (response.status === 403) throw new Error('FORBIDDEN: Insufficient OAuth scopes');
  if (response.status === 429) throw new Error('RATE_LIMITED: 429 Too Many Requests');
  if (response.status >= 500) throw new Error(`SERVER_ERROR: ${response.status}`);

  if (!response.ok) {
    const errText = await response.text();
    throw new Error(`STATUS_CHECK_FAILED: ${response.status} - ${errText}`);
  }

  const data: unknown = await response.json();
  
  // Format verification
  if (!data || typeof data !== 'object' || !('status' in data)) {
    throw new Error('PAYLOAD_MISMATCH: Response does not match TrainingStatusResponse schema');
  }

  return data as TrainingStatusResponse;
}

Step 3: Quota Checking, Error Classification & Latency Tracking

We implement a sliding window quota checker to prevent orphaned jobs during scaling. Error codes are classified into retryable, fatal, or abort categories. Latency and checkpoint success rates are tracked for poll efficiency metrics.

export interface PollMetrics {
  totalLatencyMs: number;
  checkpointSuccessCount: number;
  checkpointTotalCount: number;
  requestsInWindow: number;
  windowStart: number;
}

export class PollValidator {
  private metrics: PollMetrics;
  private readonly windowMs = 60000; // 60s window for rate limiting
  private readonly maxRequestsPerWindow = 12; // Cognigy.AI safe limit

  constructor() {
    this.metrics = {
      totalLatencyMs: 0,
      checkpointSuccessCount: 0,
      checkpointTotalCount: 0,
      requestsInWindow: 0,
      windowStart: Date.now()
    };
  }

  checkQuota(): boolean {
    const now = Date.now();
    if (now - this.metrics.windowStart > this.windowMs) {
      this.metrics.requestsInWindow = 0;
      this.metrics.windowStart = now;
    }
    this.metrics.requestsInWindow++;
    return this.metrics.requestsInWindow <= this.maxRequestsPerWindow;
  }

  classifyError(status: number): 'RETRY' | 'ABORT' | 'FATAL' {
    if (status === 429 || status === 502 || status === 503 || status === 504) return 'RETRY';
    if (status === 400 || status === 404) return 'ABORT';
    if (status === 401 || status === 403) return 'FATAL';
    return 'RETRY';
  }

  recordLatency(ms: number) {
    this.metrics.totalLatencyMs += ms;
  }

  recordCheckpoint(success: boolean) {
    this.metrics.checkpointTotalCount++;
    if (success) this.metrics.checkpointSuccessCount++;
  }

  getMetrics(): PollMetrics {
    return { ...this.metrics };
  }
}

Step 4: Callback Synchronization & Audit Logging

External ML orchestration tools require synchronized status callbacks. We expose a callback interface and generate structured audit logs for AI governance compliance.

export type StatusCallback = (jobId: string, status: string, progress: number, checkpoint?: number) => void;
export type AuditLogCallback = (entry: AuditEntry) => void;

export interface AuditEntry {
  timestamp: string;
  jobId: string;
  action: 'POLL_START' | 'STATUS_CHECK' | 'CHECKPOINT_UPDATE' | 'ABORT_TRIGGERED' | 'POLL_COMPLETE';
  status: string;
  latencyMs: number;
  error?: string;
}

export function createAuditLogger(jobId: string): AuditLogCallback {
  return (entry: AuditEntry) => {
    const logLine = JSON.stringify({
      ...entry,
      timestamp: new Date().toISOString(),
      governance: true
    });
    console.log(`[AUDIT] ${logLine}`);
  };
}

Step 5: Complete Job Poller Class

The poller ties validation, atomic GET operations, quota enforcement, and callbacks into a single executable module. It respects checkpoint intervals, triggers automatic cancellation on failure thresholds, and safely iterates until completion.

export class CognigyTrainingPoller {
  private validator: PollValidator;
  private auth: CognigyAuthClient;
  private auditLogger: AuditLogCallback;

  constructor(auth: CognigyAuthClient, jobId: string) {
    this.validator = new PollValidator();
    this.auth = auth;
    this.auditLogger = createAuditLogger(jobId);
  }

  async poll(
    config: PollConfig,
    onStatusUpdate: StatusCallback
  ): Promise<void> {
    this.auditLogger({
      jobId: config.jobId,
      action: 'POLL_START',
      status: 'INITIATED',
      latencyMs: 0
    });

    const controller = new AbortController();
    let consecutiveFailures = 0;

    for (const interval of config.checkpointIntervals) {
      if (!this.validator.checkQuota()) {
        await new Promise(resolve => setTimeout(resolve, 10000));
        continue;
      }

      try {
        const token = await this.auth.getToken();
        const start = Date.now();
        const statusData = await fetchTrainingStatus(config.jobId, token, controller.signal);
        const latency = Date.now() - start;

        this.validator.recordLatency(latency);
        this.validator.recordCheckpoint(true);

        this.auditLogger({
          jobId: config.jobId,
          action: 'STATUS_CHECK',
          status: statusData.status,
          latencyMs: latency
        });

        onStatusUpdate(config.jobId, statusData.status, statusData.progress, statusData.checkpoint);

        consecutiveFailures = 0;

        if (statusData.status === 'COMPLETED' || statusData.status === 'CANCELLED') {
          this.auditLogger({
            jobId: config.jobId,
            action: 'POLL_COMPLETE',
            status: statusData.status,
            latencyMs: latency
          });
          return;
        }

        if (statusData.status === 'FAILED' || consecutiveFailures >= config.abortOnThreshold) {
          await this.triggerCancellation(config.jobId, controller);
          return;
        }

        await new Promise(resolve => setTimeout(resolve, interval));

      } catch (err) {
        const errorMessage = err instanceof Error ? err.message : String(err);
        this.validator.recordCheckpoint(false);
        
        if (errorMessage.includes('RATE_LIMITED') || errorMessage.includes('SERVER_ERROR')) {
          await new Promise(resolve => setTimeout(resolve, 2000 * Math.pow(2, consecutiveFailures)));
          consecutiveFailures++;
        } else if (errorMessage.includes('ABORT') || controller.signal.aborted) {
          this.auditLogger({
            jobId: config.jobId,
            action: 'ABORT_TRIGGERED',
            status: 'ABORTED',
            latencyMs: 0,
            error: errorMessage
          });
          return;
        } else {
          throw err;
        }
      }
    }

    this.auditLogger({
      jobId: config.jobId,
      action: 'POLL_COMPLETE',
      status: 'CHECKPOINTS_EXHAUSTED',
      latencyMs: 0
    });
  }

  private async triggerCancellation(jobId: string, controller: AbortController): Promise<void> {
    try {
      const token = await this.auth.getToken();
      await fetch(`${COGNIGY_BASE_URL}/api/v2/nlu/training/${jobId}/cancel`, {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${token}`,
          'Content-Type': 'application/json'
        }
      });
      controller.abort();
      this.auditLogger({
        jobId,
        action: 'ABORT_TRIGGERED',
        status: 'CANCELLED_BY_POLLER',
        latencyMs: 0
      });
    } catch (cancelErr) {
      console.error('Cancellation failed:', cancelErr);
    }
  }

  getMetrics() {
    return this.validator.getMetrics();
  }
}

Complete Working Example

The following script demonstrates end-to-end usage. Replace environment variables with your Cognigy.AI credentials and a valid training job UUID.

import { CognigyAuthClient } from './auth';
import { validatePollConfig, CognigyTrainingPoller, StatusCallback } from './poller';

async function main() {
  const auth = new CognigyAuthClient();
  
  const rawConfig = {
    jobId: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
    checkpointIntervals: [5000, 10000, 15000, 20000],
    abortOnThreshold: 3,
    maxRetries: 2
  };

  const config = validatePollConfig(rawConfig);

  const onStatusUpdate: StatusCallback = (jobId, status, progress, checkpoint) => {
    console.log(`[ORCHESTRATION] Job ${jobId} -> ${status} | Progress: ${progress}% | Checkpoint: ${checkpoint}`);
  };

  const poller = new CognigyTrainingPoller(auth, config.jobId);

  try {
    await poller.poll(config, onStatusUpdate);
    console.log('[POLLER] Polling cycle finished. Metrics:', poller.getMetrics());
  } catch (error) {
    console.error('[POLLER] Fatal error during polling:', error);
    process.exit(1);
  }
}

main();

Common Errors & Debugging

Error: 401 Unauthorized

  • Cause: The OAuth token has expired, the client credentials are incorrect, or the cognigy.ai:api:read scope is missing.
  • Fix: Verify environment variables. The CognigyAuthClient automatically refreshes tokens, but if the initial exchange fails, check the client secret. Ensure the token buffer in getToken() is not overriding a valid token.
  • Code: The auth client throws Token acquisition failed with the exact HTTP status. Log the response body to verify scope rejection.

Error: 429 Too Many Requests

  • Cause: Polling frequency exceeds Cognigy.AI rate limits. The checkpoint interval matrix contains values below 5000ms, or concurrent pollers hit the same quota window.
  • Fix: Enforce MIN_INTERVAL_MS in the Zod schema. The PollValidator tracks a 60-second sliding window and pauses requests when maxRequestsPerWindow is reached. Implement exponential backoff in the catch block.
  • Code: The error classifier returns RETRY for 429. The poller applies 2000 * Math.pow(2, consecutiveFailures) delay before resuming.

Error: 400 Bad Request or Payload Mismatch

  • Cause: The job UUID is invalid, the training job does not exist, or the API response structure changed.
  • Fix: Validate the UUID format before instantiation. The fetchTrainingStatus function performs strict format verification against TrainingStatusResponse. If the response lacks the status field, it throws PAYLOAD_MISMATCH.
  • Code: Catch the error and verify the job exists via GET /api/v2/nlu/models before polling.

Error: 503 Service Unavailable

  • Cause: Cognigy.AI AI training service is scaling or undergoing maintenance.
  • Fix: Classify as RETRY. The poller waits and retries. If consecutive failures exceed abortOnThreshold, the poller triggers automatic cancellation to prevent orphaned jobs.
  • Code: The classifyError method maps 503 to RETRY. The poll loop increments consecutiveFailures and applies backoff.

Official References