Migrating NICE Cognigy.AI Intent Models Across Environments with Java

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+ HttpClient and 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_id and client_secret match the OAuth application registered in the Cognigy.AI admin console. Ensure the scope parameter includes intent:read and intent:write.

Error: 403 Forbidden

  • Cause: The OAuth client lacks the required scopes or the target environment blocks cross-environment API calls.
  • Fix: Add model:read and entity:read to 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 nameMapping table to rename the intent before import. Alternatively, query the target environment first using GET /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, and modelId. Validate that all entity references in the entities array 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-After header value. The provided importer includes a retry loop that respects the header. Reduce concurrent thread count if running parallel migrations.

Official References