Merging Overlapping NICE Cognigy Intents via REST API with PHP

Merging Overlapping NICE Cognigy Intents via REST API with PHP

What You Will Build

A PHP service that programmatically identifies overlapping Cognigy intents, constructs validated merge payloads with entity matrices and priority directives, executes atomic consolidation via POST, triggers automatic model training, and records audit logs with latency metrics. This tutorial uses the NICE Cognigy REST API v1. Programming language: PHP 8.1+ with GuzzleHttp.

Prerequisites

  • Cognigy tenant API access with OAuth2 client credentials or API key
  • Required scopes: intent:read, intent:write, model:read, model:write, training:write
  • PHP 8.1 or later
  • Composer dependencies: guzzlehttp/guzzle, monolog/monolog, ramsey/uuid
  • Basic familiarity with NLP intent classification and entity extraction workflows

Authentication Setup

Cognigy authenticates API requests using Bearer tokens. The following code demonstrates token acquisition, file-based caching, and automatic refresh logic. The token is cached for the duration specified in the expires_in claim to prevent unnecessary authentication calls.

<?php
require 'vendor/autoload.php';

use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\RequestOptions;

class CognigyAuth
{
    private Client $http;
    private string $tokenCachePath;
    private string $clientId;
    private string $clientSecret;
    private string $authUrl;

    public function __construct(string $clientId, string $clientSecret, string $tenantUrl)
    {
        $this->clientId = $clientId;
        $this->clientSecret = $clientSecret;
        $this->authUrl = rtrim($tenantUrl, '/') . '/api/v1/auth/token';
        $this->tokenCachePath = sys_get_temp_dir() . '/cognigy_token_' . md5($clientId);
        $this->http = new Client([
            RequestOptions::TIMEOUT => 10,
            RequestOptions::CONNECT_TIMEOUT => 5,
        ]);
    }

    public function getBearerToken(): string
    {
        if (file_exists($this->tokenCachePath)) {
            $cacheData = json_decode(file_get_contents($this->tokenCachePath), true);
            if (isset($cacheData['expires_at']) && $cacheData['expires_at'] > time()) {
                return $cacheData['token'];
            }
        }

        try {
            $response = $this->http->post($this->authUrl, [
                RequestOptions::FORM_PARAMS => [
                    'grant_type' => 'client_credentials',
                    'client_id' => $this->clientId,
                    'client_secret' => $this->clientSecret,
                    'scope' => 'intent:read intent:write model:read model:write training:write'
                ]
            ]);

            $body = json_decode($response->getBody(), true);
            $token = $body['access_token'];
            $expiresAt = time() + (int)($body['expires_in'] ?? 3600);

            file_put_contents($this->tokenCachePath, json_encode([
                'token' => $token,
                'expires_at' => $expiresAt
            ]));

            return $token;
        } catch (RequestException $e) {
            $statusCode = $e->getResponse()->getStatusCode();
            if ($statusCode === 401 || $statusCode === 403) {
                throw new RuntimeException('Authentication failed: invalid credentials or insufficient scopes.');
            }
            throw new RuntimeException('Token acquisition failed: ' . $e->getMessage());
        }
    }
}
?>

Implementation

Step 1: Fetch Intents and Run Validation Pipeline

The first step retrieves all active intents and evaluates them for semantic overlap and false positive risk. Cognigy intents contain training examples and entity bindings. The validation pipeline calculates a Jaccard similarity score between intent phrase sets and verifies that the false positive rate remains below a configurable threshold. It also checks the maximum intent hierarchy depth to prevent NLP engine constraint violations.

Required Scope: intent:read
Endpoint: GET /api/v1/intents

public function fetchAndValidateIntents(string $token, float $similarityThreshold = 0.75, float $maxFalsePositiveRate = 0.15, int $maxHierarchyDepth = 3): array
{
    $response = $this->http->get('/api/v1/intents', [
        RequestOptions::HEADERS => ['Authorization' => 'Bearer ' . $token],
        RequestOptions::TIMEOUT => 15,
    ]);

    $intents = json_decode($response->getBody(), true)['data'] ?? [];
    $mergeCandidates = [];

    for ($i = 0; $i < count($intents); $i++) {
        for ($j = $i + 1; $j < count($intents); $j++) {
            $intentA = $intents[$i];
            $intentB = $intents[$j];

            $similarity = $this->calculatePhraseSimilarity($intentA['examples'], $intentB['examples']);
            $falsePositiveRate = $this->estimateFalsePositiveRate($intentA, $intentB);
            $hierarchyDepth = $this->getIntentHierarchyDepth($intentA);

            if ($similarity >= $similarityThreshold && $falsePositiveRate <= $maxFalsePositiveRate && $hierarchyDepth < $maxHierarchyDepth) {
                $mergeCandidates[] = [
                    'target' => $intentA['id'],
                    'source' => $intentB['id'],
                    'similarity' => $similarity,
                    'falsePositiveRate' => $falsePositiveRate,
                    'hierarchyDepth' => $hierarchyDepth,
                ];
            }
        }
    }

    return $mergeCandidates;
}

private function calculatePhraseSimilarity(array $examplesA, array $examplesB): float
{
    $union = count(array_unique(array_merge($examplesA, $examplesB)));
    $intersection = count(array_intersect($examplesA, $examplesB));
    return $union === 0 ? 0.0 : $intersection / $union;
}

private function estimateFalsePositiveRate(array $intentA, array $intentB): float
{
    $totalExamples = count($intentA['examples']) + count($intentB['examples']);
    $overlappingEntities = count(array_intersect_key($intentA['entities'] ?? [], $intentB['entities'] ?? []));
    return $totalExamples === 0 ? 0.0 : $overlappingEntities / $totalExamples;
}

private function getIntentHierarchyDepth(array $intent): int
{
    $depth = 0;
    $current = $intent;
    while (isset($current['parentIntentId']) && $current['parentIntentId'] !== null) {
        $depth++;
        $current = ['id' => $current['parentIntentId'], 'parentIntentId' => null];
    }
    return $depth;
}

Step 2: Construct Merge Payload with Entity Matrix and Priority Directive

Once candidates are identified, the payload must comply with Cognigy NLP engine constraints. The payload references intent IDs, defines an entity merge matrix, and specifies a priority directive to resolve training example conflicts. The schema validation ensures that entity strategies (union, intersection, overwrite) align with supported NLP operations and that the hierarchy limit is not breached.

Required Scope: intent:write
Endpoint: POST /api/v1/intents/merge

public function buildMergePayload(array $candidates, string $modelId): array
{
    $payloads = [];

    foreach ($candidates as $candidate) {
        $entityMatrix = $this->resolveEntityMatrix($candidate['target'], $candidate['source']);
        
        $payload = [
            'targetIntentId' => $candidate['target'],
            'sourceIntentIds' => [$candidate['source']],
            'modelId' => $modelId,
            'entityMatrix' => $entityMatrix,
            'priorityDirective' => 'preserve_target_examples',
            'validationFlags' => [
                'checkHierarchyLimit' => true,
                'maxDepth' => 3,
                'verifyEntityConflicts' => true,
                'enforceSemanticConsistency' => true,
            ],
            'auditMetadata' => [
                'similarityScore' => $candidate['similarity'],
                'falsePositiveRate' => $candidate['falsePositiveRate'],
                'initiatedBy' => 'automated_merger',
                'timestamp' => date('c'),
            ]
        ];

        $this->validatePayloadSchema($payload);
        $payloads[] = $payload;
    }

    return $payloads;
}

private function resolveEntityMatrix(string $targetId, string $sourceId): array
{
    return [
        'location' => ['mergeStrategy' => 'union', 'priority' => 'target'],
        'product_type' => ['mergeStrategy' => 'intersection', 'priority' => 'source_highest_confidence'],
        'customer_segment' => ['mergeStrategy' => 'overwrite', 'priority' => 'target'],
    ];
}

private function validatePayloadSchema(array $payload): void
{
    $requiredKeys = ['targetIntentId', 'sourceIntentIds', 'modelId', 'entityMatrix', 'priorityDirective', 'validationFlags'];
    foreach ($requiredKeys as $key) {
        if (!array_key_exists($key, $payload)) {
            throw new InvalidArgumentException('Missing required payload key: ' . $key);
        }
    }

    $validStrategies = ['union', 'intersection', 'overwrite'];
    foreach ($payload['entityMatrix'] as $entity => $config) {
        if (!in_array($config['mergeStrategy'], $validStrategies, true)) {
            throw new InvalidArgumentException('Invalid entity merge strategy: ' . $config['mergeStrategy']);
        }
    }

    if (!is_array($payload['sourceIntentIds']) || count($payload['sourceIntentIds']) === 0) {
        throw new InvalidArgumentException('sourceIntentIds must be a non-empty array.');
    }
}

Step 3: Execute Atomic POST and Handle Model Consolidation

The merge operation is atomic. Cognigy rejects partial merges if any validation constraint fails. The request includes format verification headers and implements retry logic for 429 rate limit responses. The response returns a consolidation job identifier used for tracking.

Required Scope: intent:write, model:write
Endpoint: POST /api/v1/intents/merge

public function executeMerge(string $token, array $payload, string $modelId): array
{
    $startTime = microtime(true);
    $maxRetries = 3;
    $retryCount = 0;

    while ($retryCount < $maxRetries) {
        try {
            $response = $this->http->post('/api/v1/intents/merge', [
                RequestOptions::HEADERS => [
                    'Authorization' => 'Bearer ' . $token,
                    'Content-Type' => 'application/json',
                    'X-Format-Verification' => 'strict',
                ],
                RequestOptions::JSON => $payload,
                RequestOptions::TIMEOUT => 20,
            ]);

            $result = json_decode($response->getBody(), true);
            $latency = microtime(true) - $startTime;

            return [
                'success' => true,
                'jobId' => $result['jobId'] ?? null,
                'status' => $result['status'] ?? 'queued',
                'latencyMs' => round($latency * 1000, 2),
                'modelId' => $modelId,
            ];
        } catch (RequestException $e) {
            $statusCode = $e->getResponse()->getStatusCode();
            $latency = microtime(true) - $startTime;

            if ($statusCode === 429 && $retryCount < $maxRetries - 1) {
                $retryAfter = (int)($e->getResponse()->getHeaderLine('Retry-After') ?? 2);
                sleep($retryAfter);
                $retryCount++;
                continue;
            }

            if ($statusCode === 400) {
                throw new RuntimeException('Schema validation failed. Check entity matrix and priority directive.');
            }
            if ($statusCode === 409) {
                throw new RuntimeException('Hierarchy limit exceeded or intent conflict detected.');
            }
            if ($statusCode === 401 || $statusCode === 403) {
                throw new RuntimeException('Authentication or authorization failed.');
            }

            throw new RuntimeException('Merge execution failed (HTTP ' . $statusCode . '): ' . $e->getMessage());
        }
    }

    throw new RuntimeException('Maximum retry limit reached for merge operation.');
}

Step 4: Trigger Training Queue and Monitor Completion

After successful consolidation, the NLP model requires retraining. The training queue accepts atomic triggers and returns a training job identifier. Polling continues until the job reaches a terminal state. The pipeline records success rates and consolidation latency for governance reporting.

Required Scope: training:write, model:read
Endpoints: POST /api/v1/models/{modelId}/trainings, GET /api/v1/models/{modelId}/trainings/{trainingId}

public function triggerAndMonitorTraining(string $token, string $modelId, string $jobId): array
{
    $trainingStart = microtime(true);

    $triggerResponse = $this->http->post("/api/v1/models/{$modelId}/trainings", [
        RequestOptions::HEADERS => ['Authorization' => 'Bearer ' . $token],
        RequestOptions::JSON => ['sourceJobId' => $jobId, 'mode' => 'incremental'],
        RequestOptions::TIMEOUT => 15,
    ]);

    $trainingData = json_decode($triggerResponse->getBody(), true);
    $trainingId = $trainingData['trainingId'] ?? null;

    if (!$trainingId) {
        throw new RuntimeException('Training queue trigger failed: no training ID returned.');
    }

    $pollInterval = 5;
    $maxPolls = 60;
    $pollCount = 0;

    while ($pollCount < $maxPolls) {
        sleep($pollInterval);
        $pollCount++;

        $statusResponse = $this->http->get("/api/v1/models/{$modelId}/trainings/{$trainingId}", [
            RequestOptions::HEADERS => ['Authorization' => 'Bearer ' . $token],
            RequestOptions::TIMEOUT => 10,
        ]);

        $statusData = json_decode($statusResponse->getBody(), true);
        $state = $statusData['state'] ?? 'unknown';

        if (in_array($state, ['completed', 'failed', 'cancelled'], true)) {
            $latency = round((microtime(true) - $trainingStart) * 1000, 2);
            return [
                'trainingId' => $trainingId,
                'finalState' => $state,
                'latencyMs' => $latency,
                'successRate' => $state === 'completed' ? 1.0 : 0.0,
                'pollCount' => $pollCount,
            ];
        }
    }

    return [
        'trainingId' => $trainingId,
        'finalState' => 'timed_out',
        'latencyMs' => round((microtime(true) - $trainingStart) * 1000, 2),
        'successRate' => 0.0,
        'pollCount' => $pollCount,
    ];
}

Step 5: Synchronize Events, Track Metrics, and Generate Audit Logs

The merger exposes webhook synchronization for external analytics dashboards. It calculates consolidation success rates, records latency metrics, and writes structured audit logs for bot governance. The audit log contains intent references, merge outcomes, and training results.

public function generateAuditAndNotify(array $mergeResult, array $trainingResult, string $webhookUrl): void
{
    $auditLog = [
        'event' => 'intent_merge_consolidation',
        'timestamp' => date('c'),
        'mergeJobId' => $mergeResult['jobId'],
        'targetIntentId' => $mergeResult['payload']['targetIntentId'] ?? 'unknown',
        'sourceIntentIds' => $mergeResult['payload']['sourceIntentIds'] ?? [],
        'mergeLatencyMs' => $mergeResult['latencyMs'],
        'trainingId' => $trainingResult['trainingId'],
        'trainingState' => $trainingResult['finalState'],
        'trainingLatencyMs' => $trainingResult['latencyMs'],
        'consolidationSuccessRate' => $trainingResult['successRate'],
        'governanceStatus' => $trainingResult['finalState'] === 'completed' ? 'compliant' : 'review_required',
    ];

    $this->logger->info('Intent merge audit logged', $auditLog);

    try {
        $this->http->post($webhookUrl, [
            RequestOptions::JSON => $auditLog,
            RequestOptions::TIMEOUT => 5,
            RequestOptions::HTTP_ERRORS => false,
        ]);
    } catch (RequestException $e) {
        $this->logger->warning('Webhook notification failed', ['url' => $webhookUrl, 'error' => $e->getMessage()]);
    }
}

Complete Working Example

The following script combines all components into a runnable service. Replace the placeholder credentials and URLs with your Cognigy tenant details.

<?php
require 'vendor/autoload.php';

use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\RequestOptions;
use Monolog\Logger;
use Monolog\Handler\StreamHandler;

class CognigyIntentMerger
{
    private Client $http;
    private Logger $logger;
    private string $baseUrl;

    public function __construct(string $baseUrl, string $logPath = 'merger_audit.log')
    {
        $this->baseUrl = rtrim($baseUrl, '/');
        $this->http = new Client([
            RequestOptions::BASE_URI => $this->baseUrl,
            RequestOptions::TIMEOUT => 15,
            RequestOptions::CONNECT_TIMEOUT => 5,
        ]);

        $this->logger = new Logger('cognigy_merger');
        $this->logger->pushHandler(new StreamHandler($logPath, Logger::INFO));
    }

    public function runMergePipeline(string $token, string $modelId, string $webhookUrl): array
    {
        $startTime = microtime(true);
        $candidates = $this->fetchAndValidateIntents($token);
        $payloads = $this->buildMergePayload($candidates, $modelId);

        $results = [];
        foreach ($payloads as $payload) {
            try {
                $mergeResult = $this->executeMerge($token, $payload, $modelId);
                $mergeResult['payload'] = $payload;
                
                $trainingResult = $this->triggerAndMonitorTraining($token, $modelId, $mergeResult['jobId']);
                
                $this->generateAuditAndNotify($mergeResult, $trainingResult, $webhookUrl);
                
                $results[] = [
                    'status' => 'completed',
                    'merge' => $mergeResult,
                    'training' => $trainingResult,
                    'totalLatencyMs' => round((microtime(true) - $startTime) * 1000, 2),
                ];
            } catch (\Throwable $e) {
                $results[] = [
                    'status' => 'failed',
                    'error' => $e->getMessage(),
                    'latencyMs' => round((microtime(true) - $startTime) * 1000, 2),
                ];
            }
        }

        return $results;
    }

    // Methods from Steps 1-5 are included here in production code.
    // For brevity in this tutorial, they match the implementations above.
    // Copy the fetchAndValidateIntents, buildMergePayload, executeMerge, 
    // triggerAndMonitorTraining, and generateAuditAndNotify methods into this class.
}

// Execution block
try {
    $auth = new CognigyAuth('YOUR_CLIENT_ID', 'YOUR_CLIENT_SECRET', 'https://tenant.cognigy.com');
    $token = $auth->getBearerToken();

    $merger = new CognigyIntentMerger('https://tenant.cognigy.com/api/v1');
    $pipelineResults = $merger->runMergePipeline($token, 'model_prod_001', 'https://analytics.internal/webhooks/cognigy-merger');

    foreach ($pipelineResults as $result) {
        echo json_encode($result, JSON_PRETTY_PRINT) . PHP_EOL;
    }
} catch (\Throwable $e) {
    error_log('Pipeline execution failed: ' . $e->getMessage());
    exit(1);
}
?>

Common Errors & Debugging

Error: HTTP 400 Bad Request

  • Cause: Payload schema mismatch, invalid entity merge strategy, or missing priority directive.
  • Fix: Verify that entityMatrix values use union, intersection, or overwrite. Ensure sourceIntentIds is an array. Check validationFlags structure.
  • Code Fix: Add strict type checking before POST. Use json_validate() or a JSON Schema validator to verify the payload structure.

Error: HTTP 409 Conflict

  • Cause: Intent hierarchy depth exceeds the maximum allowed limit, or a source intent is already a child of the target intent.
  • Fix: Reduce maxDepth in validationFlags or restructure the intent tree before merging. Verify parent-child relationships in the Cognigy console.
  • Code Fix: Increase hierarchy validation depth checks in fetchAndValidateIntents and reject candidates that violate tree constraints.

Error: HTTP 429 Too Many Requests

  • Cause: Rate limiting triggered by rapid merge requests or training polling.
  • Fix: Implement exponential backoff. Respect the Retry-After header. Space out polling intervals.
  • Code Fix: The executeMerge method already includes a retry loop with Retry-After parsing. Extend this pattern to training polling if needed.

Error: HTTP 500 Internal Server Error

  • Cause: NLP engine timeout during consolidation or training queue saturation.
  • Fix: Wait for the training queue to clear. Reduce batch size. Verify that entity matrices do not contain conflicting strategies that cause engine deadlocks.
  • Code Fix: Add a circuit breaker pattern to halt merges after consecutive 5xx responses. Log detailed payloads for Cognigy support analysis.

Official References