Train NICE Cognigy.AI Entity Recognition Models via REST API
What You Will Build
- A Python module that submits entity training jobs to NICE Cognigy.AI, validates payloads against compute and epoch constraints, and monitors loss convergence with automatic early stopping.
- The implementation uses the Cognigy.AI v2 REST API for atomic training submissions, status polling, and webhook synchronization.
- The code is written in Python 3.9+ using
httpxfor async HTTP operations andpydanticfor strict schema validation.
Prerequisites
- Cognigy.AI tenant URL and OAuth2 client credentials (
clientId,clientSecret) - Required OAuth2 scopes:
model:write,model:read,train:execute - Cognigy.AI API v2 (REST)
- Python 3.9 or higher
- External dependencies:
httpx,pydantic,pytz,aiofiles
pip install httpx pydantic pytz aiofiles
Authentication Setup
Cognigy.AI uses standard OAuth2 client credentials flow. The token endpoint returns a bearer token that expires after a fixed duration. Production implementations require token caching and automatic refresh before expiration.
import httpx
import time
from typing import Optional
class CognigyAuthManager:
def __init__(self, tenant_url: str, client_id: str, client_secret: str):
self.base_url = tenant_url.rstrip("/")
self.client_id = client_id
self.client_secret = client_secret
self.token: Optional[str] = None
self.token_expiry: float = 0.0
self.http_client = httpx.AsyncClient(timeout=30.0)
async def get_token(self) -> str:
if self.token and time.time() < self.token_expiry - 60:
return self.token
url = f"{self.base_url}/api/v2/oauth/token"
payload = {
"grant_type": "client_credentials",
"client_id": self.client_id,
"client_secret": self.client_secret,
"scope": "model:write model:read train:execute"
}
response = await self.http_client.post(url, data=payload)
response.raise_for_status()
data = response.json()
self.token = data["access_token"]
self.token_expiry = time.time() + data["expires_in"]
return self.token
async def close(self):
await self.http_client.aclose()
Implementation
Step 1: Payload Construction and Schema Validation
Training payloads must include an entityRef identifier, an exampleMatrix containing annotated utterances, and a fit directive controlling compute allocation and epoch limits. The API rejects payloads that exceed tokenization limits or violate compute constraints. This step validates the schema, calculates token counts, and checks class distribution before submission.
from pydantic import BaseModel, Field, validator
from typing import List, Dict, Any
import re
class TrainingExample(BaseModel):
utterance: str
entities: List[Dict[str, Any]]
@validator("utterance")
def check_token_limit(cls, v: str) -> str:
# Cognigy.AI limits training examples to 512 tokens per utterance
tokens = re.findall(r"\b\w+\b", v.lower())
if len(tokens) > 512:
raise ValueError(f"Utterance exceeds 512 token limit: {len(tokens)} tokens found")
return v
class FitDirective(BaseModel):
maxEpochs: int = Field(..., ge=1, le=100)
computeUnits: int = Field(..., ge=1, le=8)
earlyStoppingPatience: int = Field(..., ge=1, le=20)
class TrainingPayload(BaseModel):
entityRef: str
exampleMatrix: List[TrainingExample]
fit: FitDirective
@validator("exampleMatrix")
def check_class_balance(cls, v: List[TrainingExample]) -> List[TrainingExample]:
if len(v) < 10:
raise ValueError("Minimum 10 training examples required")
# Extract intent/entity class distribution
class_counts: Dict[str, int] = {}
for ex in v:
for ent in ex.entities:
label = ent.get("type", "unlabeled")
class_counts[label] = class_counts.get(label, 0) + 1
if class_counts:
min_count = min(class_counts.values())
max_count = max(class_counts.values())
imbalance_ratio = max_count / min_count if min_count > 0 else float("inf")
if imbalance_ratio > 10.0:
raise ValueError(f"Severe class imbalance detected (ratio: {imbalance_ratio:.2f}). Resample data before training.")
return v
Step 2: Atomic Training Submission and Retry Logic
The training job is submitted via an atomic POST operation. The API returns a jobId immediately. Network instability or tenant load can trigger 429 Too Many Requests responses. This step implements exponential backoff retry logic and tracks submission latency.
import asyncio
import json
from datetime import datetime, timezone
class CognigyEntityTrainer:
def __init__(self, auth: CognigyAuthManager, registry_webhook_url: str):
self.auth = auth
self.registry_url = registry_webhook_url
self.http_client = httpx.AsyncClient(timeout=60.0)
self.metrics = {"latencies": [], "success_count": 0, "failure_count": 0}
async def _retry_on_rate_limit(self, func, *args, max_retries: int = 5, **kwargs) -> httpx.Response:
for attempt in range(max_retries):
response = await func(*args, **kwargs)
if response.status_code != 429:
return response
retry_after = int(response.headers.get("Retry-After", 2 ** attempt))
print(f"Rate limited (429). Retrying in {retry_after}s (attempt {attempt + 1})")
await asyncio.sleep(retry_after)
return response
async def submit_training(self, payload: TrainingPayload) -> str:
token = await self.auth.get_token()
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}
url = f"{self.auth.base_url}/api/v2/model/train/entity"
start_time = time.time()
response = await self._retry_on_rate_limit(
self.http_client.post, url, headers=headers, json=payload.model_dump()
)
latency = time.time() - start_time
self.metrics["latencies"].append(latency)
if response.status_code == 401:
raise PermissionError("Invalid or expired OAuth token. Refresh credentials.")
if response.status_code == 403:
raise PermissionError("Insufficient scopes. Verify model:write and train:execute are granted.")
if response.status_code >= 500:
raise RuntimeError(f"Server error during training submission: {response.status_code} {response.text}")
response.raise_for_status()
self.metrics["success_count"] += 1
job_data = response.json()
job_id = job_data.get("jobId") or job_data.get("id")
print(f"Training job submitted successfully. Job ID: {job_id}")
return job_id
Step 3: Loss Convergence Monitoring and Early Stopping
After submission, the trainer polls the status endpoint to evaluate loss convergence. The API returns epoch-level loss values. This step calculates the loss delta between epochs, triggers early stopping if the loss plateaus beyond the configured patience window, and verifies cross-validation scores to prevent overfitting.
async def monitor_training(self, job_id: str, fit: FitDirective) -> Dict[str, Any]:
url = f"{self.auth.base_url}/api/v2/model/train/status/{job_id}"
token = await self.auth.get_token()
headers = {"Authorization": f"Bearer {token}"}
consecutive_plateaus = 0
last_loss = None
status_data: Dict[str, Any] = {}
while True:
await asyncio.sleep(5)
response = await self.http_client.get(url, headers=headers)
if response.status_code == 404:
raise RuntimeError(f"Training job {job_id} not found or expired.")
response.raise_for_status()
status_data = response.json()
state = status_data.get("state", "RUNNING")
if state in ("COMPLETED", "FAILED", "CANCELLED"):
break
# Evaluate loss convergence
loss_history = status_data.get("lossHistory", [])
if len(loss_history) >= 2:
current_loss = loss_history[-1]
if last_loss is not None:
delta = abs(last_loss - current_loss)
if delta < 1e-4:
consecutive_plateaus += 1
else:
consecutive_plateaus = 0
last_loss = current_loss
# Automatic early stopping trigger
if consecutive_plateaus >= fit.earlyStoppingPatience:
print(f"Early stopping triggered after {consecutive_plateaus} epochs of plateau.")
await self.cancel_training(job_id)
status_data["state"] = "CANCELLED"
status_data["earlyStopped"] = True
break
# Cross-validation score verification
cv_score = status_data.get("crossValidationScore")
if cv_score is not None and cv_score < 0.6:
print(f"Warning: Low CV score ({cv_score}). Model may be underfitting.")
return status_data
async def cancel_training(self, job_id: str):
token = await self.auth.get_token()
url = f"{self.auth.base_url}/api/v2/model/train/cancel/{job_id}"
headers = {"Authorization": f"Bearer {token}"}
response = await self.http_client.post(url, headers=headers)
response.raise_for_status()
print(f"Training job {job_id} cancelled successfully.")
Step 4: Registry Synchronization and Audit Logging
Upon job completion, the trainer synchronizes the final metrics with an external model registry via webhook and writes a structured audit log for AI governance. The log includes timestamps, job identifiers, compute allocation, loss metrics, and validation results.
async def sync_and_audit(self, job_id: str, status: Dict[str, Any], payload: TrainingPayload):
# Synchronize with external model registry
sync_payload = {
"jobId": job_id,
"entityRef": payload.entityRef,
"state": status.get("state"),
"finalLoss": status.get("lossHistory", [-1])[-1],
"cvScore": status.get("crossValidationScore"),
"computeUnits": payload.fit.computeUnits,
"epochsCompleted": len(status.get("lossHistory", [])),
"timestamp": datetime.now(timezone.utc).isoformat()
}
try:
await self.http_client.post(self.registry_url, json=sync_payload)
print(f"Model registry synchronized for job {job_id}")
except httpx.HTTPStatusError as e:
print(f"Registry sync failed: {e.response.status_code} {e.response.text}")
# Generate audit log
audit_entry = {
"timestamp": datetime.now(timezone.utc).isoformat(),
"event": "TRAINING_COMPLETED",
"jobId": job_id,
"entityRef": payload.entityRef,
"metrics": {
"latency_seconds": self.metrics["latencies"][-1] if self.metrics["latencies"] else 0,
"success_rate": self.metrics["success_count"] / max(1, self.metrics["success_count"] + self.metrics["failure_count"]),
"final_loss": sync_payload["finalLoss"],
"cv_score": sync_payload["cvScore"],
"early_stopped": status.get("earlyStopped", False)
}
}
log_line = json.dumps(audit_entry)
async with aiofiles.open("training_audit.log", mode="a") as f:
await f.write(log_line + "\n")
print(f"Audit log written for job {job_id}")
Complete Working Example
The following script combines all components into a single executable module. Replace the placeholder credentials and URLs with your tenant values before execution.
import asyncio
import time
import httpx
import aiofiles
import json
from datetime import datetime, timezone
from typing import Dict, Any
# Import classes from previous steps (combined here for copy-paste execution)
# [Insert CognigyAuthManager, TrainingExample, FitDirective, TrainingPayload, CognigyEntityTrainer here]
async def main():
# Configuration
TENANT_URL = "https://your-tenant.cognigy.ai"
CLIENT_ID = "your-client-id"
CLIENT_SECRET = "your-client-secret"
REGISTRY_WEBHOOK = "https://registry.yourcompany.com/api/v1/models/sync"
ENTITY_REF = "entity-uuid-1234567890abcdef"
# Initialize authentication
auth = CognigyAuthManager(TENANT_URL, CLIENT_ID, CLIENT_SECRET)
trainer = CognigyEntityTrainer(auth, REGISTRY_WEBHOOK)
# Construct training payload
examples = [
TrainingExample(utterance="book a flight to paris", entities=[{"start": 13, "end": 18, "type": "LOCATION"}]),
TrainingExample(utterance="reserve a hotel in tokyo", entities=[{"start": 22, "end": 27, "type": "LOCATION"}]),
TrainingExample(utterance="find restaurants in london", entities=[{"start": 21, "end": 27, "type": "LOCATION"}]),
# Add additional examples to meet minimum threshold and balance classes
]
# Pad examples for demonstration purposes
for i in range(7):
examples.append(TrainingExample(utterance=f"query {i}", entities=[{"start": 6, "end": 9, "type": "QUERY"}]))
payload = TrainingPayload(
entityRef=ENTITY_REF,
exampleMatrix=examples,
fit=FitDirective(maxEpochs=50, computeUnits=4, earlyStoppingPatience=5)
)
try:
# Submit training job
job_id = await trainer.submit_training(payload)
# Monitor convergence and handle early stopping
status = await trainer.monitor_training(job_id, payload.fit)
# Synchronize and audit
await trainer.sync_and_audit(job_id, status, payload)
print(f"Training workflow completed. Final state: {status.get('state')}")
except Exception as e:
trainer.metrics["failure_count"] += 1
print(f"Training workflow failed: {e}")
finally:
await trainer.http_client.aclose()
await auth.close()
if __name__ == "__main__":
asyncio.run(main())
Common Errors & Debugging
Error: 401 Unauthorized
- Cause: OAuth token expired, invalid client credentials, or missing
train:executescope. - Fix: Verify the client secret matches the registered application. Ensure the token refresh logic runs before expiration. Check the
scopeparameter in the token request matches the API documentation. - Code fix: The
CognigyAuthManagerautomatically refreshes tokens 60 seconds before expiration. If the error persists, log the raw token response to verify scope inclusion.
Error: 403 Forbidden
- Cause: The OAuth application lacks permission to write models or execute training jobs.
- Fix: Navigate to the Cognigy.AI developer console, locate the OAuth client, and add
model:write,model:read, andtrain:executeto the allowed scopes. Revoke and regenerate the client secret if scope changes were made previously.
Error: 429 Too Many Requests
- Cause: Tenant-level rate limiting triggered by concurrent training submissions or status polling.
- Fix: Implement exponential backoff. The
_retry_on_rate_limitmethod reads theRetry-Afterheader and sleeps accordingly. Reduce polling frequency from 5 seconds to 15 seconds if the tenant enforces strict quotas.
Error: 500 Internal Server Error or Validation Failure
- Cause: Payload exceeds tokenization limits, class imbalance ratio exceeds thresholds, or
fitdirective violates compute constraints. - Fix: Validate the
exampleMatrixagainst the 512-token limit before submission. Resample training data to achieve a class distribution ratio below 10:1. EnsuremaxEpochsdoes not exceed 100 andcomputeUnitsdoes not exceed tenant allocation. The Pydantic validators inTrainingPayloadwill catch these issues locally before the HTTP request.