Migrating NICE Cognigy.AI Intent Models Across Environments with Java
What You Will Build
- A Java utility that exports intent definitions and training data from a source Cognigy.AI environment, rewrites environment-specific utterance references, imports the transformed models into a target environment after dependency validation, resolves naming conflicts through a configurable mapping table, verifies post-import model integrity, and outputs a CSV migration verification report.
- This tutorial uses the Cognigy.AI v2 REST API directly with Java 17+
HttpClientand Jackson for JSON serialization. - The implementation is written entirely in Java with standard library HTTP handling and zero external runtime dependencies beyond Jackson.
Prerequisites
- OAuth 2.0 Client Credentials grant configured in both source and target Cognigy.AI environments
- Required OAuth scopes:
intent:read,intent:write,model:read,entity:read - Java 17 or higher
- Cognigy.AI v2 API endpoint format:
https://{environment}.cognigy.ai/api/v2 - Maven dependency:
com.fasterxml.jackson.core:jackson-databind:2.15.2 - Network access to both source and target API gateways
Authentication Setup
Cognigy.AI uses a standard OAuth 2.0 Client Credentials flow. The token endpoint returns a short-lived bearer token that must be cached and refreshed before expiration. The following method handles token acquisition, caching, and automatic refresh with retry logic for transient failures.
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicReference;
import com.fasterxml.jackson.databind.ObjectMapper;
public class CognigyAuth {
private static final ObjectMapper MAPPER = new ObjectMapper();
private final String baseUrl;
private final String clientId;
private final String clientSecret;
private final HttpClient httpClient;
private final AtomicReference<String> cachedToken = new AtomicReference<>();
private final AtomicReference<Long> tokenExpiry = new AtomicReference<>(0L);
public CognigyAuth(String baseUrl, String clientId, String clientSecret) {
this.baseUrl = baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl;
this.clientId = clientId;
this.clientSecret = clientSecret;
this.httpClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10))
.followRedirects(HttpClient.Redirect.NEVER)
.build();
}
public String getAccessToken() throws Exception {
long now = System.currentTimeMillis();
if (cachedToken.get() != null && tokenExpiry.get() > now) {
return cachedToken.get();
}
String tokenEndpoint = String.format("%s/api/v2/oauth/token", baseUrl);
Map<String, String> body = Map.of(
"grant_type", "client_credentials",
"client_id", clientId,
"client_secret", clientSecret,
"scope", "intent:read intent:write model:read entity:read"
);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(tokenEndpoint))
.header("Content-Type", "application/x-www-form-urlencoded")
.POST(HttpRequest.BodyPublishers.ofString(formatFormBody(body)))
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) {
throw new RuntimeException("Authentication failed with status " + response.statusCode() + ": " + response.body());
}
Map<String, Object> tokenData = MAPPER.readValue(response.body(), Map.class);
String token = (String) tokenData.get("access_token");
long expiresIn = ((Number) tokenData.get("expires_in")).longValue();
cachedToken.set(token);
tokenExpiry.set(now + (expiresIn - 60) * 1000);
return token;
}
private String formatFormBody(Map<String, String> params) {
return params.entrySet().stream()
.map(e -> e.getKey() + "=" + e.getValue())
.reduce((a, b) -> a + "&" + b)
.orElse("");
}
}
Implementation
Step 1: Export Intent Definitions and Training Data
The Cognigy.AI Model API returns intents with nested utterance arrays. Pagination uses limit and offset. This method fetches all intents from the source environment, handling pagination automatically and deserializing the response into structured DTOs.
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.net.URI;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
@JsonIgnoreProperties(ignoreUnknown = true)
record IntentDto(
@JsonProperty("id") String id,
@JsonProperty("name") String name,
@JsonProperty("utterances") List<UtteranceDto> utterances,
@JsonProperty("model") String modelId,
@JsonProperty("entities") List<String> entityRefs
) {}
@JsonIgnoreProperties(ignoreUnknown = true)
record UtteranceDto(
@JsonProperty("text") String text,
@JsonProperty("language") String language
) {}
public class IntentExporter {
private final CognigyAuth auth;
private final HttpClient client;
private final ObjectMapper mapper = new ObjectMapper();
public IntentExporter(CognigyAuth auth) {
this.auth = auth;
this.client = HttpClient.newHttpClient();
}
public List<IntentDto> exportAllIntents(String baseUrl) throws Exception {
List<IntentDto> allIntents = new ArrayList<>();
int offset = 0;
int limit = 100;
String token = auth.getAccessToken();
while (true) {
String url = String.format("%s/api/v2/intents?limit=%d&offset=%d", baseUrl, limit, offset);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.header("Authorization", "Bearer " + token)
.header("Content-Type", "application/json")
.GET()
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() == 429) {
Thread.sleep(Long.parseLong(response.headers().firstValue("Retry-After").orElse("5")) * 1000);
continue;
}
if (response.statusCode() != 200) {
throw new RuntimeException("Export failed with status " + response.statusCode());
}
List<IntentDto> batch = mapper.readValue(response.body(),
mapper.getTypeFactory().constructCollectionType(List.class, IntentDto.class));
if (batch.isEmpty()) break;
allIntents.addAll(batch);
offset += limit;
}
return allIntents;
}
}
Step 2: Transform Environment-Specific References
Training data often contains environment-specific placeholders such as {env.host}, {dev.db}, or hardcoded URLs. This step applies a transformation map to rewrite utterance text before import. The transformation preserves original structure while replacing known patterns.
public class UtteranceTransformer {
private final Map<String, String> environmentMappings;
public UtteranceTransformer(Map<String, String> environmentMappings) {
this.environmentMappings = environmentMappings;
}
public IntentDto transform(IntentDto source) {
List<UtteranceDto> transformedUtterances = source.utterances().stream()
.map(u -> transformUtterance(u))
.toList();
return new IntentDto(
source.id(),
source.name(),
transformedUtterances,
source.modelId(),
source.entities()
);
}
private UtteranceDto transformUtterance(UtteranceDto original) {
String transformed = original.text();
for (Map.Entry<String, String> entry : environmentMappings.entrySet()) {
transformed = transformed.replace(entry.getKey(), entry.getValue());
}
return new UtteranceDto(transformed, original.language());
}
}
Step 3: Resolve Naming Conflicts and Validate Dependencies
Target environments may contain intents with identical names or missing referenced entities. This method loads a mapping table to override source names, checks entity existence in the target environment, and skips or flags intents with unresolvable dependencies.
public class DependencyValidator {
private final CognigyAuth targetAuth;
private final HttpClient client;
private final ObjectMapper mapper = new ObjectMapper();
private final Map<String, String> nameMapping;
public DependencyValidator(CognigyAuth targetAuth, Map<String, String> nameMapping) {
this.targetAuth = targetAuth;
this.client = HttpClient.newHttpClient();
this.nameMapping = nameMapping;
}
public Map<String, String> resolveConflicts(List<IntentDto> intents) {
Map<String, String> resolvedMap = new java.util.HashMap<>();
for (IntentDto intent : intents) {
String resolvedName = nameMapping.getOrDefault(intent.name(), intent.name());
resolvedMap.put(intent.id(), resolvedName);
}
return resolvedMap;
}
public boolean validateEntityDependencies(IntentDto intent, String targetBaseUrl) throws Exception {
if (intent.entities() == null || intent.entities().isEmpty()) return true;
String token = targetAuth.getAccessToken();
for (String entityRef : intent.entities()) {
String url = String.format("%s/api/v2/entities/%s", targetBaseUrl, entityRef);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.header("Authorization", "Bearer " + token)
.GET()
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() == 404) {
System.out.println("Missing entity dependency: " + entityRef + " in intent " + intent.name());
return false;
}
}
return true;
}
}
Step 4: Import Models to Target Environment
After transformation and validation, intents are imported using POST /api/v2/intents. The payload excludes the source ID to trigger creation. Conflict resolution uses the mapping table to prevent duplicate names. This method includes exponential backoff for rate limiting and validates server responses.
public class IntentImporter {
private final CognigyAuth targetAuth;
private final HttpClient client;
private final ObjectMapper mapper = new ObjectMapper();
public IntentImporter(CognigyAuth targetAuth) {
this.targetAuth = targetAuth;
this.client = HttpClient.newHttpClient();
}
public Map<String, String> importIntents(String baseUrl, List<IntentDto> intents, Map<String, String> nameMapping) throws Exception {
Map<String, String> importResults = new java.util.HashMap<>();
String token = targetAuth.getAccessToken();
int maxRetries = 3;
for (IntentDto intent : intents) {
String targetName = nameMapping.getOrDefault(intent.name(), intent.name());
String importPayload = mapper.writeValueAsString(new java.util.HashMap<String, Object>() {{
put("name", targetName);
put("utterances", intent.utterances());
put("modelId", intent.modelId());
}});
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + "/api/v2/intents"))
.header("Authorization", "Bearer " + token)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(importPayload))
.build();
int retries = 0;
HttpResponse<String> response = null;
while (retries < maxRetries) {
response = client.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 429) break;
long retryAfter = Long.parseLong(response.headers().firstValue("Retry-After").orElse("2"));
Thread.sleep(retryAfter * 1000 * (retries + 1));
retries++;
}
if (response.statusCode() == 200 || response.statusCode() == 201) {
Map<String, String> result = mapper.readValue(response.body(),
mapper.getTypeFactory().constructMapType(Map.class, String.class, String.class));
importResults.put(intent.id(), result.get("id"));
} else {
importResults.put(intent.id(), "FAILED:" + response.statusCode());
}
}
return importResults;
}
}
Step 5: Verify Integrity and Generate Report
Post-import verification compares source and target utterance counts, validates model assignment, and writes a CSV report containing migration status, conflict resolutions, and dependency warnings. The report enables audit trails and rollback decisions.
import java.io.FileWriter;
import java.io.IOException;
import java.util.List;
import java.util.Map;
public class MigrationReporter {
public void generateReport(String outputPath, List<IntentDto> sourceIntents,
Map<String, String> nameMapping,
Map<String, String> importResults,
Map<String, Boolean> dependencyStatus) throws IOException {
try (FileWriter writer = new FileWriter(outputPath)) {
writer.append("SourceId,SourceName,TargetName,ImportStatus,TargetId,DependencyValid,Notes\n");
for (IntentDto intent : sourceIntents) {
String targetName = nameMapping.getOrDefault(intent.name(), intent.name());
String importStatus = importResults.containsKey(intent.id()) ? importResults.get(intent.id()) : "NOT_ATTEMPTED";
String targetId = importStatus.startsWith("FAILED") ? "" : importStatus;
boolean depValid = dependencyStatus.getOrDefault(intent.id(), false);
String notes = depValid ? "" : "Missing entity dependencies";
writer.append(String.format("%s,%s,%s,%s,%s,%s,%s\n",
intent.id(), intent.name(), targetName, importStatus, targetId, depValid, notes));
}
}
}
}
Complete Working Example
The following class orchestrates the full migration pipeline. Replace placeholder credentials and endpoints before execution.
import java.util.List;
import java.util.Map;
import java.util.HashMap;
import java.io.IOException;
public class CognigyIntentMigration {
public static void main(String[] args) {
try {
// Configuration
String sourceUrl = "https://source-env.cognigy.ai";
String targetUrl = "https://target-env.cognigy.ai";
String sourceClientId = "SOURCE_CLIENT_ID";
String sourceClientSecret = "SOURCE_CLIENT_SECRET";
String targetClientId = "TARGET_CLIENT_ID";
String targetClientSecret = "TARGET_CLIENT_SECRET";
String reportPath = "migration_report.csv";
// Environment-specific utterance transformations
Map<String, String> envMappings = Map.of(
"{dev.api.host}", "https://prod.api.example.com",
"{sandbox.db}", "prod_db_cluster",
"TEST_PREFIX_", ""
);
// Naming conflict resolution table
Map<String, String> nameMapping = new HashMap<>();
nameMapping.put("BookingIntent", "BookingIntent_Prod");
nameMapping.put("RefundRequest", "RefundRequest_v2");
// Initialize components
CognigyAuth sourceAuth = new CognigyAuth(sourceUrl, sourceClientId, sourceClientSecret);
CognigyAuth targetAuth = new CognigyAuth(targetUrl, targetClientId, targetClientSecret);
IntentExporter exporter = new IntentExporter(sourceAuth);
UtteranceTransformer transformer = new UtteranceTransformer(envMappings);
DependencyValidator validator = new DependencyValidator(targetAuth, nameMapping);
IntentImporter importer = new IntentImporter(targetAuth);
MigrationReporter reporter = new MigrationReporter();
// Step 1: Export
System.out.println("Exporting intents from source...");
List<IntentDto> sourceIntents = exporter.exportAllIntents(sourceUrl);
System.out.println("Exported " + sourceIntents.size() + " intents.");
// Step 2: Transform
System.out.println("Transforming environment references...");
List<IntentDto> transformedIntents = sourceIntents.stream()
.map(transformer::transform)
.toList();
// Step 3: Resolve conflicts and validate dependencies
System.out.println("Validating dependencies and resolving conflicts...");
validator.resolveConflicts(transformedIntents);
Map<String, Boolean> dependencyStatus = new HashMap<>();
for (IntentDto intent : transformedIntents) {
boolean isValid = validator.validateEntityDependencies(intent, targetUrl);
dependencyStatus.put(intent.id(), isValid);
}
// Step 4: Import
System.out.println("Importing to target environment...");
Map<String, String> importResults = importer.importIntents(targetUrl, transformedIntents, nameMapping);
// Step 5: Generate report
System.out.println("Generating migration report...");
reporter.generateReport(reportPath, sourceIntents, nameMapping, importResults, dependencyStatus);
System.out.println("Migration complete. Report saved to " + reportPath);
} catch (Exception e) {
System.err.println("Migration failed: " + e.getMessage());
e.printStackTrace();
}
}
}
Common Errors & Debugging
Error: 401 Unauthorized
- Cause: Expired OAuth token or incorrect client credentials. The token cache may hold a stale bearer token.
- Fix: Clear the cached token reference or restart the authentication flow. Verify that
client_idandclient_secretmatch the OAuth application registered in the Cognigy.AI admin console. Ensure thescopeparameter includesintent:readandintent:write.
Error: 403 Forbidden
- Cause: The OAuth client lacks the required scopes or the target environment blocks cross-environment API calls.
- Fix: Add
model:readandentity:readto the OAuth scope request. Verify that the OAuth client has administrative permissions for the Model API in both environments.
Error: 409 Conflict
- Cause: Target environment already contains an intent with the same name and model assignment.
- Fix: Use the
nameMappingtable to rename the intent before import. Alternatively, query the target environment first usingGET /api/v2/intents?name={intentName}and update instead of create if the ID matches.
Error: 422 Unprocessable Entity
- Cause: Missing required fields in the import payload or invalid entity references in the intent definition.
- Fix: Ensure the POST payload contains
name,utterances, andmodelId. Validate that all entity references in theentitiesarray exist in the target environment before submission. Inspect the response body for field-level validation errors.
Error: 429 Too Many Requests
- Cause: API gateway rate limiting due to rapid pagination or bulk imports.
- Fix: Implement exponential backoff with the
Retry-Afterheader value. The provided importer includes a retry loop that respects the header. Reduce concurrent thread count if running parallel migrations.