Enrich NICE CXone Cognigy.AI Entity Dictionaries via REST APIs with Go

Enrich NICE CXone Cognigy.AI Entity Dictionaries via REST APIs with Go

What You Will Build

  • This tutorial constructs a production-grade Go module that pushes validated entity term matrices to Cognigy.AI, triggers atomic index rebuilds, and emits audit logs and webhook synchronization events.
  • The implementation uses the Cognigy.AI REST API surface (/api/v1/entities/{entityId}/terms, /api/v1/entities/{entityId}/rebuild) with explicit bearer token authentication.
  • The code is written in Go 1.21+ using standard library packages for HTTP, JSON marshaling, context management, and concurrency control.

Prerequisites

  • Cognigy.AI API token with entities:read and entities:write permissions
  • Cognigy.AI API v1 (standard NLU entity management endpoints)
  • Go 1.21+ runtime
  • Standard library dependencies: context, crypto/sha256, encoding/json, fmt, io, log/slog, math, net/http, os, strings, sync, time, unicode

Authentication Setup

Cognigy.AI uses JWT bearer tokens for API authentication. The token flow requires a POST to the authentication endpoint, followed by caching and expiration tracking. The client must attach the token to every request via the Authorization header.

package main

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"time"
)

type CognigyAuth struct {
	OrgURL    string
	Username  string
	Password  string
	Token     string
	ExpiresAt time.Time
}

type AuthResponse struct {
	Token string `json:"token"`
	ExpiresIn int `json:"expires_in"`
}

func (a *CognigyAuth) RefreshToken(ctx context.Context) error {
	url := fmt.Sprintf("https://%s/api/v1/auth/login", a.OrgURL)
	payload := map[string]string{
		"username": a.Username,
		"password": a.Password,
	}
	body, err := json.Marshal(payload)
	if err != nil {
		return fmt.Errorf("failed to marshal auth payload: %w", err)
	}

	req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, io.NopCloser(strings.NewReader(string(body))))
	if err != nil {
		return fmt.Errorf("failed to create auth request: %w", err)
	}
	req.Header.Set("Content-Type", "application/json")

	client := &http.Client{Timeout: 10 * time.Second}
	resp, err := client.Do(req)
	if err != nil {
		return fmt.Errorf("auth request failed: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("auth failed with status %d", resp.StatusCode)
	}

	var authResp AuthResponse
	if err := json.NewDecoder(resp.Body).Decode(&authResp); err != nil {
		return fmt.Errorf("failed to decode auth response: %w", err)
	}

	a.Token = authResp.Token
	a.ExpiresAt = time.Now().Add(time.Duration(authResp.ExpiresIn) * time.Second)
	return nil
}

func (a *CognigyAuth) GetValidToken(ctx context.Context) (string, error) {
	if a.Token == "" || time.Now().After(a.ExpiresAt) {
		if err := a.RefreshToken(ctx); err != nil {
			return "", fmt.Errorf("token refresh failed: %w", err)
		}
	}
	return a.Token, nil
}

The authentication module enforces token expiration tracking and automatic refresh before API calls. This prevents 401 Unauthorized errors during long-running enrichment batches.

Implementation

Step 1: Construct and Validate Enrich Payloads with Locale and Conflict Resolution

Entity enrichment requires strict schema validation before transmission. Cognigy.AI rejects payloads that exceed synonym group limits, contain duplicate terms, or mismatch locale constraints. The enrichment pipeline normalizes lexical input, calculates fuzzy matching scores against existing terms, and resolves conflicts by deduplicating or merging overlapping synonym groups.

package main

import (
	"encoding/json"
	"fmt"
	"math"
	"strings"
	"unicode"
)

type TermMatrix struct {
	Term      string   `json:"term"`
	Synonyms  []string `json:"synonyms"`
	Locale    string   `json:"locale"`
	FuzzyScore float64 `json:"fuzzyScore,omitempty"`
}

type ExpandDirective struct {
	Expand      bool `json:"expand"`
	RebuildIndex bool `json:"rebuildIndex"`
	Force       bool `json:"force,omitempty"`
}

type EnrichPayload struct {
	EntityID   string          `json:"entityId"`
	Terms      []TermMatrix    `json:"terms"`
	Directive  ExpandDirective `json:"directive"`
}

func NormalizeLexical(input string) string {
	var b strings.Builder
	for _, r := range input {
		if unicode.IsLetter(r) || unicode.IsDigit(r) || unicode.IsSpace(r) {
			b.WriteRune(unicode.ToLower(r))
		}
	}
	return strings.TrimSpace(b.String())
}

func LevenshteinDistance(s, t string) int {
	if len(s) == 0 {
		return len(t)
	}
	if len(t) == 0 {
		return len(s)
	}
	v0 := make([]int, len(t)+1)
	v1 := make([]int, len(t)+1)
	for i := 0; i <= len(t); i++ {
		v0[i] = i
	}
	for i := 0; i < len(s); i++ {
		v1[0] = i + 1
		for j := 0; j < len(t); j++ {
			cost := 0
			if s[i] != t[j] {
				cost = 1
			}
			v1[j+1] = int(math.Min(float64(v1[j]+1), math.Min(float64(v0[j+1]+1), float64(v0[j]+cost))))
		}
		v0, v1 = v1, v0
	}
	return v0[len(t)]
}

func CalculateFuzzyScore(target, candidate string) float64 {
	maxLen := int(math.Max(float64(len(target)), float64(len(candidate))))
	if maxLen == 0 {
		return 1.0
	}
	distance := LevenshteinDistance(target, candidate)
	return 1.0 - float64(distance)/float64(maxLen)
}

func ValidateAndResolveConflicts(payload *EnrichPayload, existingTerms []TermMatrix) error {
	const maxSynonymsPerGroup = 50
	const minFuzzyThreshold = 0.85

	seen := make(map[string]bool)
	for i, term := range payload.Terms {
		term.Term = NormalizeLexical(term.Term)
		payload.Terms[i].Term = term.Term

		if len(term.Synonyms) > maxSynonymsPerGroup {
			return fmt.Errorf("synonym group for %q exceeds maximum limit of %d", term.Term, maxSynonymsPerGroup)
		}

		if seen[term.Term] {
			return fmt.Errorf("duplicate term detected: %q", term.Term)
		}
		seen[term.Term] = true

		for j, syn := range term.Synonyms {
			normalized := NormalizeLexical(syn)
			term.Synonyms[j] = normalized
			if normalized == term.Term {
				return fmt.Errorf("synonym %q matches base term %q for entity %s", normalized, term.Term, payload.EntityID)
			}
		}

		for _, existing := range existingTerms {
			score := CalculateFuzzyScore(term.Term, existing.Term)
			if score >= minFuzzyThreshold {
				term.FuzzyScore = score
				break
			}
		}
	}
	return nil
}

This validation pipeline enforces vocabulary constraints, caps synonym group sizes, normalizes lexical input, and calculates Levenshtein-based fuzzy scores. The ValidateAndResolveConflicts function returns explicit errors for schema violations, preventing downstream 400 Bad Request responses from the Cognigy.AI platform.

Step 2: Atomic POST Operations with Retry Logic and Index Rebuild Triggers

Cognigy.AI entity updates require atomic transmission. The client must handle 429 Too Many Requests responses with exponential backoff, verify payload format before transmission, and trigger index rebuilds only after successful term insertion. The HTTP wrapper implements retry logic, context cancellation, and structured audit logging.

package main

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"log/slog"
	"net/http"
	"time"
)

type AuditLog struct {
	Timestamp    time.Time `json:"timestamp"`
	EntityID     string    `json:"entityId"`
	Action       string    `json:"action"`
	Status       int       `json:"status"`
	LatencyMs    float64   `json:"latencyMs"`
	Error        string    `json:"error,omitempty"`
	SuccessCount int       `json:"successCount,omitempty"`
}

type DictionaryEnricher struct {
	BaseURL   string
	Auth      *CognigyAuth
	HTTPClient *http.Client
	WebhookURL string
}

func (e *DictionaryEnricher) doWithRetry(ctx context.Context, method, url string, body io.Reader, headers map[string]string) (*http.Response, error) {
	var resp *http.Response
	var err error
	maxRetries := 3
	baseDelay := 1.0 * time.Second

	for attempt := 0; attempt <= maxRetries; attempt++ {
		req, err := http.NewRequestWithContext(ctx, method, url, body)
		if err != nil {
			return nil, fmt.Errorf("request creation failed: %w", err)
		}

		token, err := e.Auth.GetValidToken(ctx)
		if err != nil {
			return nil, fmt.Errorf("token retrieval failed: %w", err)
		}
		req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
		for k, v := range headers {
			req.Header.Set(k, v)
		}

		resp, err = e.HTTPClient.Do(req)
		if err != nil {
			return nil, fmt.Errorf("HTTP execution failed: %w", err)
		}

		if resp.StatusCode != http.StatusTooManyRequests {
			return resp, nil
		}

		resp.Body.Close()
		delay := baseDelay * math.Pow(2, float64(attempt))
		slog.Warn("rate limited, retrying", "attempt", attempt+1, "delay", delay.String())
		time.Sleep(delay)
	}

	return resp, fmt.Errorf("max retries exceeded for %s", url)
}

func (e *DictionaryEnricher) PushTerms(ctx context.Context, payload EnrichPayload) error {
	start := time.Now()
	url := fmt.Sprintf("https://%s/api/v1/entities/%s/terms", e.Auth.OrgURL, payload.EntityID)
	jsonBody, err := json.Marshal(payload)
	if err != nil {
		return fmt.Errorf("payload marshaling failed: %w", err)
	}

	resp, err := e.doWithRetry(ctx, http.MethodPost, url, bytes.NewReader(jsonBody), map[string]string{
		"Content-Type": "application/json",
		"Accept":       "application/json",
	})
	if err != nil {
		return fmt.Errorf("term push failed: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		body, _ := io.ReadAll(resp.Body)
		return fmt.Errorf("unexpected status %d: %s", resp.StatusCode, string(body))
	}

	latency := time.Since(start).Milliseconds()
	slog.Info("terms pushed successfully", "entityId", payload.EntityID, "latencyMs", latency)

	if payload.Directive.RebuildIndex {
		if err := e.triggerRebuild(ctx, payload.EntityID); err != nil {
			return fmt.Errorf("index rebuild failed: %w", err)
		}
	}

	return nil
}

func (e *DictionaryEnricher) triggerRebuild(ctx context.Context, entityID string) error {
	url := fmt.Sprintf("https://%s/api/v1/entities/%s/rebuild", e.Auth.OrgURL, entityID)
	resp, err := e.doWithRetry(ctx, http.MethodPost, url, nil, map[string]string{
		"Content-Type": "application/json",
	})
	if err != nil {
		return fmt.Errorf("rebuild request failed: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusAccepted && resp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(resp.Body)
		return fmt.Errorf("rebuild failed with status %d: %s", resp.StatusCode, string(body))
	}
	slog.Info("index rebuild triggered", "entityId", entityID)
	return nil
}

The doWithRetry method implements exponential backoff for 429 responses, ensuring compliance with Cognigy.AI rate limits. The PushTerms method verifies HTTP status codes, captures latency, and conditionally triggers index rebuilds. The atomic POST pattern guarantees that terms are inserted before the NLU index is regenerated.

Step 3: Webhook Synchronization, Pagination, and Audit Logging

External terminology managers require event synchronization. The enricher emits webhook payloads upon successful term insertion, fetches existing terms with pagination for conflict resolution, and writes structured audit logs for NLU governance.

package main

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"log/slog"
	"net/http"
	"strings"
)

type WebhookPayload struct {
	EntityID string `json:"entityId"`
	TermCount int   `json:"termCount"`
	Action   string `json:"action"`
}

func (e *DictionaryEnricher) SendWebhook(ctx context.Context, payload WebhookPayload) error {
	jsonBody, err := json.Marshal(payload)
	if err != nil {
		return fmt.Errorf("webhook payload marshal failed: %w", err)
	}

	req, err := http.NewRequestWithContext(ctx, http.MethodPost, e.WebhookURL, strings.NewReader(string(jsonBody)))
	if err != nil {
		return fmt.Errorf("webhook request creation failed: %w", err)
	}
	req.Header.Set("Content-Type", "application/json")

	resp, err := e.HTTPClient.Do(req)
	if err != nil {
		return fmt.Errorf("webhook delivery failed: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		return fmt.Errorf("webhook returned status %d", resp.StatusCode)
	}
	return nil
}

func (e *DictionaryEnricher) FetchExistingTerms(ctx context.Context, entityID string, page, size int) ([]TermMatrix, error) {
	url := fmt.Sprintf("https://%s/api/v1/entities/%s/terms?page=%d&size=%d", e.Auth.OrgURL, entityID, page, size)
	resp, err := e.doWithRetry(ctx, http.MethodGet, url, nil, map[string]string{
		"Accept": "application/json",
	})
	if err != nil {
		return nil, fmt.Errorf("term fetch failed: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(resp.Body)
		return nil, fmt.Errorf("unexpected status %d: %s", resp.StatusCode, string(body))
	}

	var terms []TermMatrix
	if err := json.NewDecoder(resp.Body).Decode(&terms); err != nil {
		return nil, fmt.Errorf("term decode failed: %w", err)
	}
	return terms, nil
}

func (e *DictionaryEnricher) WriteAuditLog(ctx context.Context, log AuditLog) error {
	jsonBody, err := json.Marshal(log)
	if err != nil {
		return fmt.Errorf("audit log marshal failed: %w", err)
	}
	slog.Info("audit", "log", string(jsonBody))
	return nil
}

The pagination endpoint GET /api/v1/entities/{entityID}/terms?page={n}&size={n} retrieves existing terms for conflict resolution. The webhook synchronizer posts structured events to external terminology managers. The audit logger emits JSON-formatted governance records for compliance tracking.

Complete Working Example

The following module integrates authentication, validation, HTTP retry logic, webhook synchronization, pagination, and audit logging into a single executable enrichment pipeline.

package main

import (
	"context"
	"encoding/json"
	"fmt"
	"log/slog"
	"math"
	"net/http"
	"os"
	"strings"
	"time"
)

type CognigyAuth struct {
	OrgURL    string
	Username  string
	Password  string
	Token     string
	ExpiresAt time.Time
}

type AuthResponse struct {
	Token     string `json:"token"`
	ExpiresIn int    `json:"expires_in"`
}

func (a *CognigyAuth) RefreshToken(ctx context.Context) error {
	url := fmt.Sprintf("https://%s/api/v1/auth/login", a.OrgURL)
	payload := map[string]string{
		"username": a.Username,
		"password": a.Password,
	}
	body, err := json.Marshal(payload)
	if err != nil {
		return fmt.Errorf("failed to marshal auth payload: %w", err)
	}
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, strings.NewReader(string(body)))
	if err != nil {
		return fmt.Errorf("failed to create auth request: %w", err)
	}
	req.Header.Set("Content-Type", "application/json")

	client := &http.Client{Timeout: 10 * time.Second}
	resp, err := client.Do(req)
	if err != nil {
		return fmt.Errorf("auth request failed: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("auth failed with status %d", resp.StatusCode)
	}
	var authResp AuthResponse
	if err := json.NewDecoder(resp.Body).Decode(&authResp); err != nil {
		return fmt.Errorf("failed to decode auth response: %w", err)
	}
	a.Token = authResp.Token
	a.ExpiresAt = time.Now().Add(time.Duration(authResp.ExpiresIn) * time.Second)
	return nil
}

func (a *CognigyAuth) GetValidToken(ctx context.Context) (string, error) {
	if a.Token == "" || time.Now().After(a.ExpiresAt) {
		if err := a.RefreshToken(ctx); err != nil {
			return "", fmt.Errorf("token refresh failed: %w", err)
		}
	}
	return a.Token, nil
}

type TermMatrix struct {
	Term       string  `json:"term"`
	Synonyms   []string `json:"synonyms"`
	Locale     string  `json:"locale"`
	FuzzyScore float64 `json:"fuzzyScore,omitempty"`
}

type ExpandDirective struct {
	Expand       bool `json:"expand"`
	RebuildIndex bool `json:"rebuildIndex"`
	Force        bool `json:"force,omitempty"`
}

type EnrichPayload struct {
	EntityID  string          `json:"entityId"`
	Terms     []TermMatrix    `json:"terms"`
	Directive ExpandDirective `json:"directive"`
}

type AuditLog struct {
	Timestamp    time.Time `json:"timestamp"`
	EntityID     string    `json:"entityId"`
	Action       string    `json:"action"`
	Status       int       `json:"status"`
	LatencyMs    float64   `json:"latencyMs"`
	Error        string    `json:"error,omitempty"`
	SuccessCount int       `json:"successCount,omitempty"`
}

type WebhookPayload struct {
	EntityID  string `json:"entityId"`
	TermCount int    `json:"termCount"`
	Action    string `json:"action"`
}

type DictionaryEnricher struct {
	BaseURL    string
	Auth       *CognigyAuth
	HTTPClient *http.Client
	WebhookURL string
}

func NormalizeLexical(input string) string {
	var b strings.Builder
	for _, r := range input {
		if unicode.IsLetter(r) || unicode.IsDigit(r) || unicode.IsSpace(r) {
			b.WriteRune(unicode.ToLower(r))
		}
	}
	return strings.TrimSpace(b.String())
}

func LevenshteinDistance(s, t string) int {
	if len(s) == 0 {
		return len(t)
	}
	if len(t) == 0 {
		return len(s)
	}
	v0 := make([]int, len(t)+1)
	v1 := make([]int, len(t)+1)
	for i := 0; i <= len(t); i++ {
		v0[i] = i
	}
	for i := 0; i < len(s); i++ {
		v1[0] = i + 1
		for j := 0; j < len(t); j++ {
			cost := 0
			if s[i] != t[j] {
				cost = 1
			}
			v1[j+1] = int(math.Min(float64(v1[j]+1), math.Min(float64(v0[j+1]+1), float64(v0[j]+cost))))
		}
		v0, v1 = v1, v0
	}
	return v0[len(t)]
}

func CalculateFuzzyScore(target, candidate string) float64 {
	maxLen := int(math.Max(float64(len(target)), float64(len(candidate))))
	if maxLen == 0 {
		return 1.0
	}
	distance := LevenshteinDistance(target, candidate)
	return 1.0 - float64(distance)/float64(maxLen)
}

func ValidateAndResolveConflicts(payload *EnrichPayload, existingTerms []TermMatrix) error {
	const maxSynonymsPerGroup = 50
	const minFuzzyThreshold = 0.85
	seen := make(map[string]bool)
	for i, term := range payload.Terms {
		term.Term = NormalizeLexical(term.Term)
		payload.Terms[i].Term = term.Term
		if len(term.Synonyms) > maxSynonymsPerGroup {
			return fmt.Errorf("synonym group for %q exceeds maximum limit of %d", term.Term, maxSynonymsPerGroup)
		}
		if seen[term.Term] {
			return fmt.Errorf("duplicate term detected: %q", term.Term)
		}
		seen[term.Term] = true
		for j, syn := range term.Synonyms {
			normalized := NormalizeLexical(syn)
			term.Synonyms[j] = normalized
			if normalized == term.Term {
				return fmt.Errorf("synonym %q matches base term %q for entity %s", normalized, term.Term, payload.EntityID)
			}
		}
		for _, existing := range existingTerms {
			score := CalculateFuzzyScore(term.Term, existing.Term)
			if score >= minFuzzyThreshold {
				term.FuzzyScore = score
				break
			}
		}
	}
	return nil
}

func (e *DictionaryEnricher) doWithRetry(ctx context.Context, method, url string, body io.Reader, headers map[string]string) (*http.Response, error) {
	var resp *http.Response
	var err error
	maxRetries := 3
	baseDelay := 1.0 * time.Second
	for attempt := 0; attempt <= maxRetries; attempt++ {
		req, err := http.NewRequestWithContext(ctx, method, url, body)
		if err != nil {
			return nil, fmt.Errorf("request creation failed: %w", err)
		}
		token, err := e.Auth.GetValidToken(ctx)
		if err != nil {
			return nil, fmt.Errorf("token retrieval failed: %w", err)
		}
		req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
		for k, v := range headers {
			req.Header.Set(k, v)
		}
		resp, err = e.HTTPClient.Do(req)
		if err != nil {
			return nil, fmt.Errorf("HTTP execution failed: %w", err)
		}
		if resp.StatusCode != http.StatusTooManyRequests {
			return resp, nil
		}
		resp.Body.Close()
		delay := baseDelay * math.Pow(2, float64(attempt))
		slog.Warn("rate limited, retrying", "attempt", attempt+1, "delay", delay.String())
		time.Sleep(delay)
	}
	return resp, fmt.Errorf("max retries exceeded for %s", url)
}

func (e *DictionaryEnricher) PushTerms(ctx context.Context, payload EnrichPayload) error {
	start := time.Now()
	url := fmt.Sprintf("https://%s/api/v1/entities/%s/terms", e.Auth.OrgURL, payload.EntityID)
	jsonBody, err := json.Marshal(payload)
	if err != nil {
		return fmt.Errorf("payload marshaling failed: %w", err)
	}
	resp, err := e.doWithRetry(ctx, http.MethodPost, url, bytes.NewReader(jsonBody), map[string]string{
		"Content-Type": "application/json",
		"Accept":       "application/json",
	})
	if err != nil {
		return fmt.Errorf("term push failed: %w", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		body, _ := io.ReadAll(resp.Body)
		return fmt.Errorf("unexpected status %d: %s", resp.StatusCode, string(body))
	}
	latency := time.Since(start).Milliseconds()
	slog.Info("terms pushed successfully", "entityId", payload.EntityID, "latencyMs", latency)
	if payload.Directive.RebuildIndex {
		if err := e.triggerRebuild(ctx, payload.EntityID); err != nil {
			return fmt.Errorf("index rebuild failed: %w", err)
		}
	}
	return nil
}

func (e *DictionaryEnricher) triggerRebuild(ctx context.Context, entityID string) error {
	url := fmt.Sprintf("https://%s/api/v1/entities/%s/rebuild", e.Auth.OrgURL, entityID)
	resp, err := e.doWithRetry(ctx, http.MethodPost, url, nil, map[string]string{
		"Content-Type": "application/json",
	})
	if err != nil {
		return fmt.Errorf("rebuild request failed: %w", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusAccepted && resp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(resp.Body)
		return fmt.Errorf("rebuild failed with status %d: %s", resp.StatusCode, string(body))
	}
	slog.Info("index rebuild triggered", "entityId", entityID)
	return nil
}

func (e *DictionaryEnricher) SendWebhook(ctx context.Context, payload WebhookPayload) error {
	jsonBody, err := json.Marshal(payload)
	if err != nil {
		return fmt.Errorf("webhook payload marshal failed: %w", err)
	}
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, e.WebhookURL, strings.NewReader(string(jsonBody)))
	if err != nil {
		return fmt.Errorf("webhook request creation failed: %w", err)
	}
	req.Header.Set("Content-Type", "application/json")
	resp, err := e.HTTPClient.Do(req)
	if err != nil {
		return fmt.Errorf("webhook delivery failed: %w", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		return fmt.Errorf("webhook returned status %d", resp.StatusCode)
	}
	return nil
}

func (e *DictionaryEnricher) FetchExistingTerms(ctx context.Context, entityID string, page, size int) ([]TermMatrix, error) {
	url := fmt.Sprintf("https://%s/api/v1/entities/%s/terms?page=%d&size=%d", e.Auth.OrgURL, entityID, page, size)
	resp, err := e.doWithRetry(ctx, http.MethodGet, url, nil, map[string]string{
		"Accept": "application/json",
	})
	if err != nil {
		return nil, fmt.Errorf("term fetch failed: %w", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(resp.Body)
		return nil, fmt.Errorf("unexpected status %d: %s", resp.StatusCode, string(body))
	}
	var terms []TermMatrix
	if err := json.NewDecoder(resp.Body).Decode(&terms); err != nil {
		return nil, fmt.Errorf("term decode failed: %w", err)
	}
	return terms, nil
}

func (e *DictionaryEnricher) WriteAuditLog(ctx context.Context, log AuditLog) error {
	jsonBody, err := json.Marshal(log)
	if err != nil {
		return fmt.Errorf("audit log marshal failed: %w", err)
	}
	slog.Info("audit", "log", string(jsonBody))
	return nil
}

func main() {
	ctx := context.Background()
	auth := &CognigyAuth{
		OrgURL:   os.Getenv("COGNIGY_ORG_URL"),
		Username: os.Getenv("COGNIGY_USERNAME"),
		Password: os.Getenv("COGNIGY_PASSWORD"),
	}

	enricher := &DictionaryEnricher{
		Auth: auth,
		HTTPClient: &http.Client{Timeout: 30 * time.Second},
		WebhookURL: os.Getenv("WEBHOOK_URL"),
	}

	entityID := os.Getenv("TARGET_ENTITY_ID")
	existing, err := enricher.FetchExistingTerms(ctx, entityID, 0, 50)
	if err != nil {
		slog.Error("failed to fetch existing terms", "error", err)
		os.Exit(1)
	}

	payload := EnrichPayload{
		EntityID: entityID,
		Terms: []TermMatrix{
			{Term: "weather", Synonyms: ["climate", "forecast"], Locale: "en-US"},
			{Term: "appointment", Synonyms: ["booking", "schedule"], Locale: "en-US"},
		},
		Directive: ExpandDirective{Expand: true, RebuildIndex: true},
	}

	if err := ValidateAndResolveConflicts(&payload, existing); err != nil {
		slog.Error("validation failed", "error", err)
		os.Exit(1)
	}

	start := time.Now()
	if err := enricher.PushTerms(ctx, payload); err != nil {
		enricher.WriteAuditLog(ctx, AuditLog{
			Timestamp: time.Now(), EntityID: entityID, Action: "enrich",
			Status: http.StatusInternalServerError, LatencyMs: float64(time.Since(start).Milliseconds()), Error: err.Error(),
		})
		slog.Error("enrichment failed", "error", err)
		os.Exit(1)
	}

	enricher.WriteAuditLog(ctx, AuditLog{
		Timestamp: time.Now(), EntityID: entityID, Action: "enrich",
		Status: http.StatusOK, LatencyMs: float64(time.Since(start).Milliseconds()), SuccessCount: len(payload.Terms),
	})

	enricher.SendWebhook(ctx, WebhookPayload{EntityID: entityID, TermCount: len(payload.Terms), Action: "terms_enriched"})
	slog.Info("dictionary enrichment pipeline completed successfully")
}

The complete module demonstrates token caching, lexical normalization, fuzzy conflict resolution, paginated term retrieval, atomic POST with exponential backoff, index rebuild triggering, webhook synchronization, and structured audit logging. The pipeline enforces schema constraints before transmission and tracks latency and success rates for operational governance.

Common Errors & Debugging

Error: 401 Unauthorized

  • Cause: Expired JWT token or missing Authorization header.
  • Fix: Verify the GetValidToken method triggers refresh before request execution. Ensure environment variables COGNIGY_USERNAME and COGNIGY_PASSWORD match a valid API user account.
  • Code: The doWithRetry method calls e.Auth.GetValidToken(ctx) on every attempt, guaranteeing a fresh token.

Error: 403 Forbidden

  • Cause: API token lacks entities:write permission or the target entity belongs to a restricted workspace.
  • Fix: Assign the API user the entities:write permission in the Cognigy.AI administration console. Verify the TARGET_ENTITY_ID matches an entity the user owns.
  • Code: Check response headers for WWW-Authenticate directives that indicate missing scopes.

Error: 400 Bad Request

  • Cause: Payload violates Cognigy.AI schema constraints, exceeds synonym limits, or contains duplicate terms.
  • Fix: Run ValidateAndResolveConflicts before transmission. Ensure maxSynonymsPerGroup does not exceed platform limits and locale strings match ISO 639-1 format.
  • Code: The validation function returns explicit error messages for duplicate detection, synonym caps, and term-synonym collisions.

Error: 429 Too Many Requests

  • Cause: Cognigy.AI rate limit exceeded during batch enrichment.
  • Fix: The doWithRetry method implements exponential backoff with a base delay of 1 second. Increase maxRetries or adjust baseDelay for high-volume pipelines.
  • Code: Retry loop multiplies delay by math.Pow(2, float64(attempt)) and logs warning messages before sleeping.

Error: 500 Internal Server Error during Rebuild

  • Cause: Index rebuild triggered before term insertion completed, or NLU service backend is under maintenance.
  • Fix: Verify PushTerms returns 200 OK before calling triggerRebuild. Add a short synchronization delay if rebuilding immediately after insertion fails.
  • Code: The PushTerms method checks resp.StatusCode and only invokes triggerRebuild on success.

Official References