Migrating Dialogflow Intents to NICE Cognigy.AI Using Java

Migrating Dialogflow Intents to NICE Cognigy.AI Using Java

What You Will Build

A Java-based converter that exports Dialogflow agent data via the REST API, parses JSON structures to extract intents, entities, and training phrases, maps Dialogflow elements to Cognigy dialog nodes and slot types using a transformation engine, generates Cognigy project files, validates the generated project against schema rules, and implements a dry-run mode to preview changes before deployment. This tutorial uses the Dialogflow v2 REST API and the NICE Cognigy.AI v1 API. The implementation is written in Java 17.

Prerequisites

  • Dialogflow service account or OAuth client with scopes https://www.googleapis.com/auth/dialogflow and https://www.googleapis.com/auth/cloud-platform
  • Cognigy.AI API key with admin or developer role
  • Java 17 or later
  • Maven dependencies: com.fasterxml.jackson.core:jackson-databind:2.15.2, com.networknt:json-schema-validator:1.0.97, org.slf4j:slf4j-simple:2.0.7
  • Access to the Dialogflow REST API base https://dialogflow.googleapis.com/v2
  • Access to the Cognigy.AI REST API base https://api.cognigy.ai/v1

Authentication Setup

Dialogflow requires OAuth 2.0 access tokens for API calls. The converter fetches a token using the client credentials flow and caches it until expiration. Cognigy.AI accepts API keys passed as bearer tokens in the Authorization header.

import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Instant;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

public class AuthManager {
    private static final String DIALOGFLOW_TOKEN_URL = "https://oauth2.googleapis.com/token";
    private static final String COGNIGY_BASE_URL = "https://api.cognigy.ai/v1";
    
    private final HttpClient httpClient;
    private final ObjectMapper mapper;
    private String cachedToken;
    private Instant tokenExpiry;
    private final String clientId;
    private final String clientSecret;
    private final String cognigyApiKey;

    public AuthManager(String clientId, String clientSecret, String cognigyApiKey) {
        this.httpClient = HttpClient.newBuilder()
                .connectTimeout(java.time.Duration.ofSeconds(10))
                .build();
        this.mapper = new ObjectMapper();
        this.clientId = clientId;
        this.clientSecret = clientSecret;
        this.cognigyApiKey = cognigyApiKey;
        this.tokenExpiry = Instant.now();
    }

    public String getDialogflowToken() throws IOException, InterruptedException {
        if (cachedToken != null && Instant.now().isBefore(tokenExpiry)) {
            return cachedToken;
        }

        String requestBody = String.format(
            "grant_type=client_credentials&client_id=%s&client_secret=%s",
            clientId, clientSecret
        );

        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create(DIALOGFLOW_TOKEN_URL))
                .header("Content-Type", "application/x-www-form-urlencoded")
                .POST(HttpRequest.BodyPublishers.ofString(requestBody))
                .build();

        HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
        
        if (response.statusCode() != 200) {
            throw new IOException("Dialogflow token fetch failed with status " + response.statusCode());
        }

        JsonNode json = mapper.readTree(response.body());
        this.cachedToken = json.get("access_token").asText();
        this.tokenExpiry = Instant.now().plusSeconds(json.get("expires_in").asLong());
        return cachedToken;
    }

    public String getCognigyToken() {
        return cognigyApiKey;
    }
}

The AuthManager caches the Dialogflow token and refreshes it only when expired. The Cognigy API key is returned directly. Both tokens are used in subsequent API calls.

Implementation

Step 1: Export Dialogflow Agent Data

The converter retrieves intents from Dialogflow using the v2 REST API. The endpoint supports pagination via pageToken. The request includes exponential backoff for 429 rate limit responses.

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.ArrayList;
import java.util.List;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

public class DialogflowExporter {
    private final HttpClient httpClient;
    private final ObjectMapper mapper;
    private final AuthManager authManager;
    private final String projectId;

    public DialogflowExporter(HttpClient httpClient, ObjectMapper mapper, AuthManager authManager, String projectId) {
        this.httpClient = httpClient;
        this.mapper = mapper;
        this.authManager = authManager;
        this.projectId = projectId;
    }

    public List<JsonNode> exportIntents() throws Exception {
        List<JsonNode> allIntents = new ArrayList<>();
        String pageToken = null;
        int maxRetries = 3;

        do {
            String url = String.format(
                "https://dialogflow.googleapis.com/v2/projects/%s/locations/global/intents",
                projectId
            );
            if (pageToken != null) {
                url += "?pageToken=" + pageToken;
            }

            String token = authManager.getDialogflowToken();
            HttpRequest request = HttpRequest.newBuilder()
                    .uri(URI.create(url))
                    .header("Authorization", "Bearer " + token)
                    .header("Content-Type", "application/json")
                    .GET()
                    .build();

            int retryCount = 0;
            HttpResponse<String> response = null;

            while (retryCount < maxRetries) {
                response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
                if (response.statusCode() == 429) {
                    long delay = 1000L * (long) Math.pow(2, retryCount);
                    Thread.sleep(delay);
                    retryCount++;
                } else {
                    break;
                }
            }

            if (response.statusCode() != 200) {
                throw new RuntimeException("Dialogflow API failed with status " + response.statusCode() + ": " + response.body());
            }

            JsonNode root = mapper.readTree(response.body());
            JsonNode intentsNode = root.get("intents");
            if (intentsNode != null && intentsNode.isArray()) {
                for (JsonNode intent : intentsNode) {
                    allIntents.add(intent);
                }
            }

            pageToken = root.has("nextPageToken") ? root.get("nextPageToken").asText() : null;
        } while (pageToken != null);

        return allIntents;
    }
}

The exporter handles pagination automatically. The pageToken loop continues until Dialogflow returns an empty token. The retry logic sleeps exponentially on 429 responses. The required OAuth scope is https://www.googleapis.com/auth/dialogflow.

Step 2: Parse and Extract Intents, Entities, and Training Phrases

Dialogflow returns intents with nested training phrases and parameters. The parser extracts these fields into flat Java records for transformation.

import java.util.ArrayList;
import java.util.List;
import com.fasterxml.jackson.databind.JsonNode;

public record ParsedIntent(String displayName, List<String> trainingPhrases, List<ParsedParameter> parameters) {}
public record ParsedParameter(String name, String type, List<String> values) {}

public class IntentParser {
    private final com.fasterxml.jackson.databind.ObjectMapper mapper;

    public IntentParser(com.fasterxml.jackson.databind.ObjectMapper mapper) {
        this.mapper = mapper;
    }

    public List<ParsedIntent> parse(List<JsonNode> dialogflowIntents) {
        List<ParsedIntent> parsed = new ArrayList<>();
        
        for (JsonNode node : dialogflowIntents) {
            String name = node.has("displayName") ? node.get("displayName").asText() : "unnamed_intent";
            List<String> phrases = extractTrainingPhrases(node);
            List<ParsedParameter> params = extractParameters(node);
            parsed.add(new ParsedIntent(name, phrases, params));
        }
        
        return parsed;
    }

    private List<String> extractTrainingPhrases(JsonNode intentNode) {
        List<String> phrases = new ArrayList<>();
        JsonNode trainingPhrases = intentNode.get("trainingPhrases");
        if (trainingPhrases != null && trainingPhrases.isArray()) {
            for (JsonNode phrase : trainingPhrases) {
                JsonNode parts = phrase.get("parts");
                if (parts != null && parts.isArray()) {
                    StringBuilder sb = new StringBuilder();
                    for (JsonNode part : parts) {
                        sb.append(part.get("text").asText());
                    }
                    phrases.add(sb.toString());
                }
            }
        }
        return phrases;
    }

    private List<ParsedParameter> extractParameters(JsonNode intentNode) {
        List<ParsedParameter> params = new ArrayList<>();
        JsonNode parameters = intentNode.get("parameters");
        if (parameters != null && parameters.isObject()) {
            parameters.fields().forEachRemaining(entry -> {
                String paramName = entry.getKey();
                JsonNode paramNode = entry.getValue();
                String type = paramNode.has("type") ? paramNode.get("type").asText() : "@sys.any";
                List<String> values = new ArrayList<>();
                
                if (paramNode.has("value")) {
                    String val = paramNode.get("value").asText();
                    if (!val.isEmpty()) {
                        values.add(val);
                    }
                }
                
                params.add(new ParsedParameter(paramName, type, values));
            });
        }
        return params;
    }
}

The parser flattens Dialogflow’s nested trainingPhrases array into a single list of strings. It extracts parameters into ParsedParameter records. This structure prepares the data for Cognigy mapping.

Step 3: Transformation Engine

The transformation engine maps Dialogflow elements to Cognigy.AI structures. Dialogflow intents become Cognigy dialog nodes. Dialogflow parameters become Cognigy slot types. The engine generates JSON payloads compatible with Cognigy’s import schema.

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.databind.ObjectMapper;

public class CognigyTransformer {
    private final ObjectMapper mapper;

    public CognigyTransformer(ObjectMapper mapper) {
        this.mapper = mapper;
    }

    public Map<String, Object> transformToCognigy(List<ParsedIntent> intents) {
        Map<String, Object> project = new HashMap<>();
        List<Object> nodes = new ArrayList<>();
        List<Object> slots = new ArrayList<>();
        Map<String, Boolean> processedSlots = new HashMap<>();

        for (ParsedIntent intent : intents) {
            Map<String, Object> node = new HashMap<>();
            node.put("name", intent.displayName());
            node.put("type", "dialog");
            node.put("phrases", intent.trainingPhrases());
            
            List<Map<String, Object>> nodeSlots = new ArrayList<>();
            for (ParsedParameter param : intent.parameters()) {
                String slotKey = param.name();
                if (!processedSlots.containsKey(slotKey)) {
                    processedSlots.put(slotKey, true);
                    Map<String, Object> slot = new HashMap<>();
                    slot.put("name", slotKey);
                    slot.put("type", mapDialogflowTypeToCognigy(param.type()));
                    slot.put("values", param.values());
                    slots.add(slot);
                }
                
                Map<String, Object> slotRef = new HashMap<>();
                slotRef.put("name", slotKey);
                slotRef.put("required", true);
                nodeSlots.add(slotRef);
            }
            
            if (!nodeSlots.isEmpty()) {
                node.put("slots", nodeSlots);
            }
            nodes.add(node);
        }

        project.put("nodes", nodes);
        project.put("slots", slots);
        return project;
    }

    private String mapDialogflowTypeToCognigy(String dfType) {
        if (dfType.contains("number") || dfType.contains("integer")) return "number";
        if (dfType.contains("date") || dfType.contains("time")) return "date";
        if (dfType.contains("location") || dfType.contains("address")) return "location";
        return "text";
    }
}

The transformer creates Cognigy node objects with name, type, phrases, and slots. It deduplicates slots across intents to avoid redundant definitions. The mapDialogflowTypeToCognigy method translates Dialogflow system types to Cognigy slot types.

Step 4: Generate, Validate, and Deploy with Dry-Run Mode

The final step validates the transformed JSON against a Cognigy schema, supports a dry-run flag, and POSTs to the Cognigy API when enabled.

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.Map;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.networknt.schema.JsonSchema;
import com.networknt.schema.JsonSchemaFactory;
import com.networknt.schema.SpecVersion;
import com.networknt.schema.input.JsonSchemaFactoryLoader;

public class CognigyDeployer {
    private final HttpClient httpClient;
    private final ObjectMapper mapper;
    private final AuthManager authManager;
    private final String cognigyProjectId;
    private final boolean dryRun;
    private final JsonSchema cognigySchema;

    public CognigyDeployer(HttpClient httpClient, ObjectMapper mapper, AuthManager authManager, 
                           String cognigyProjectId, boolean dryRun) {
        this.httpClient = httpClient;
        this.mapper = mapper;
        this.authManager = authManager;
        this.cognigyProjectId = cognigyProjectId;
        this.dryRun = dryRun;
        this.cognigySchema = buildSchema();
    }

    private JsonSchema buildSchema() {
        String schemaJson = """
            {
              "type": "object",
              "required": ["nodes", "slots"],
              "properties": {
                "nodes": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "required": ["name", "type", "phrases"],
                    "properties": {
                      "name": {"type": "string"},
                      "type": {"type": "string", "enum": ["dialog", "menu", "form"]},
                      "phrases": {"type": "array", "items": {"type": "string"}},
                      "slots": {
                        "type": "array",
                        "items": {
                          "type": "object",
                          "required": ["name"],
                          "properties": {
                            "name": {"type": "string"},
                            "required": {"type": "boolean"}
                          }
                        }
                      }
                    }
                  }
                },
                "slots": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "required": ["name", "type", "values"],
                    "properties": {
                      "name": {"type": "string"},
                      "type": {"type": "string"},
                      "values": {"type": "array", "items": {"type": "string"}}
                    }
                  }
                }
              }
            }
            """;
        return JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7)
                .getSchema(schemaJson);
    }

    public void deploy(Map<String, Object> projectData) throws Exception {
        String jsonPayload = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(projectData);
        
        // Validate against schema
        JsonNode node = mapper.readTree(jsonPayload);
        var errors = cognigySchema.validate(node);
        if (!errors.isEmpty()) {
            throw new IllegalArgumentException("Cognigy schema validation failed: " + errors);
        }

        if (dryRun) {
            System.out.println("[DRY-RUN] Generated Cognigy project payload:");
            System.out.println(jsonPayload);
            return;
        }

        // Deploy slots
        if (projectData.containsKey("slots")) {
            for (Object slot : (java.util.List<?>) projectData.get("slots")) {
                postToCognigy("/slots", mapper.writeValueAsString(slot));
            }
        }

        // Deploy nodes
        if (projectData.containsKey("nodes")) {
            for (Object nodeObj : (java.util.List<?>) projectData.get("nodes")) {
                postToCognigy("/nodes", mapper.writeValueAsString(nodeObj));
            }
        }
    }

    private void postToCognigy(String path, String payload) throws Exception {
        String url = String.format("https://api.cognigy.ai/v1%s?projectId=%s", path, cognigyProjectId);
        String token = authManager.getCognigyToken();

        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create(url))
                .header("Authorization", "Bearer " + token)
                .header("Content-Type", "application/json")
                .POST(HttpRequest.BodyPublishers.ofString(payload))
                .build();

        HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
        
        if (response.statusCode() < 200 || response.statusCode() >= 300) {
            throw new RuntimeException("Cognigy API call failed with status " + response.statusCode() + ": " + response.body());
        }
    }
}

The deployer validates the transformed payload using networknt-json-schema-validator. The dry-run flag prevents API calls and prints the JSON instead. When active, it POSTs slots and nodes to Cognigy using the Authorization: Bearer <api_key> header. The Cognigy API does not require OAuth scopes; it uses API key authentication.

Complete Working Example

The following class orchestrates the full migration pipeline. Replace placeholder credentials before execution.

import java.net.http.HttpClient;
import java.util.List;
import com.fasterxml.jackson.databind.ObjectMapper;

public class DialogflowToCognigyConverter {
    public static void main(String[] args) {
        try {
            // Configuration
            String dialogflowClientId = "YOUR_DIALOGFLOW_CLIENT_ID";
            String dialogflowClientSecret = "YOUR_DIALOGFLOW_CLIENT_SECRET";
            String cognigyApiKey = "YOUR_COGNIGY_API_KEY";
            String dialogflowProjectId = "your-gcp-project-id";
            String cognigyProjectId = "your-cognigy-project-id";
            boolean dryRun = true; // Set to false to deploy

            // Initialize dependencies
            HttpClient httpClient = HttpClient.newBuilder()
                    .connectTimeout(java.time.Duration.ofSeconds(15))
                    .followRedirects(HttpClient.Redirect.NORMAL)
                    .build();
            ObjectMapper mapper = new ObjectMapper();
            AuthManager authManager = new AuthManager(dialogflowClientId, dialogflowClientSecret, cognigyApiKey);

            // Step 1: Export Dialogflow intents
            DialogflowExporter exporter = new DialogflowExporter(httpClient, mapper, authManager, dialogflowProjectId);
            List<com.fasterxml.jackson.databind.JsonNode> rawIntents = exporter.exportIntents();
            System.out.println("Exported " + rawIntents.size() + " Dialogflow intents.");

            // Step 2: Parse structures
            IntentParser parser = new IntentParser(mapper);
            List<ParsedIntent> parsedIntents = parser.parse(rawIntents);

            // Step 3: Transform to Cognigy format
            CognigyTransformer transformer = new CognigyTransformer(mapper);
            java.util.Map<String, Object> cognigyProject = transformer.transformToCognigy(parsedIntents);

            // Step 4: Validate and deploy
            CognigyDeployer deployer = new CognigyDeployer(httpClient, mapper, authManager, cognigyProjectId, dryRun);
            deployer.deploy(cognigyProject);

            System.out.println("Migration pipeline completed successfully.");
        } catch (Exception e) {
            System.err.println("Pipeline failed: " + e.getMessage());
            e.printStackTrace();
        }
    }
}

This script runs the full pipeline from extraction to deployment. Set dryRun to false only after verifying the output matches your Cognigy project requirements.

Common Errors & Debugging

Error: 401 Unauthorized

  • Cause: Expired Dialogflow token or invalid Cognigy API key.
  • Fix: Ensure the AuthManager refreshes tokens before expiration. Verify the Cognigy key has admin or developer permissions in the Cognigy portal.
  • Code: The AuthManager checks tokenExpiry before reuse. If the key is invalid, Cognigy returns 401. Regenerate the key under Settings > API Keys.

Error: 429 Too Many Requests

  • Cause: Dialogflow enforces per-minute rate limits on list endpoints.
  • Fix: The DialogflowExporter implements exponential backoff. If failures persist, reduce batch size or add a fixed delay between pages.
  • Code: The retry loop sleeps 1000 * 2^retryCount milliseconds. Increase maxRetries if your workload requires it.

Error: Schema Validation Failed

  • Cause: Missing required fields in the transformed JSON, such as name, type, or phrases.
  • Fix: Ensure Dialogflow intents contain valid training phrases. Empty intents cause validation failures. Filter out intents with zero phrases before transformation.
  • Code: Add a check in CognigyTransformer.transformToCognigy: if (intent.trainingPhrases().isEmpty()) continue;

Error: 400 Bad Request on Cognigy API

  • Cause: Slot names conflict with existing Cognigy slots, or node names exceed character limits.
  • Fix: Prefix migrated slots with a namespace like df_ to avoid collisions. Trim node names to 64 characters.
  • Code: Modify the transformer to sanitize names: slot.put("name", "df_" + slotKey.toLowerCase().replace(" ", "_"));

Official References