Implementing a Java Webhook for Cognigy.AI Entity Extraction and State Management
What You Will Build
- A Java Spring Boot webhook service that intercepts Cognigy.AI user utterances, delegates named entity recognition to a spaCy pipeline, resolves ambiguous references using historical context, maps results to Cognigy slot variables, and updates the session state via the Cognigy REST API.
- This implementation uses the Cognigy.AI REST API for state synchronization and standard Java HTTP clients for external service communication.
- The tutorial covers Java 17 with Spring Boot 3, utilizing
java.net.http.HttpClientfor synchronous and asynchronous API calls.
Prerequisites
- Cognigy.AI instance with a generated API key possessing
session:state:writeandsession:state:readscopes - Java 17 runtime and Maven 3.8+
- A running spaCy FastAPI server exposing
POST /parseendpoint - Spring Boot 3.2.0+ dependencies (
spring-boot-starter-web) - Internet access for Maven dependency resolution and Cognigy API communication
Authentication Setup
Cognigy.AI authenticates REST API requests using Bearer tokens. You generate an API key in the Cognigy.AI instance settings. The key functions as a long-lived bearer token for webhook and state management operations. Store the key in environment variables to avoid hardcoding secrets.
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class CognigyAuthConfig {
private static final Map<String, String> TOKEN_CACHE = new ConcurrentHashMap<>();
public static String getApiToken(String instanceUrl) {
String envKey = System.getenv("COGNIGY_API_KEY");
if (envKey == null || envKey.isBlank()) {
throw new IllegalStateException("COGNIGY_API_KEY environment variable is not set");
}
TOKEN_CACHE.put(instanceUrl, envKey);
return envKey;
}
public static String getToken(String instanceUrl) {
String token = TOKEN_CACHE.get(instanceUrl);
if (token == null) {
throw new IllegalStateException("API token not initialized for " + instanceUrl);
}
return token;
}
}
The token caching mechanism prevents repeated environment variable lookups. Cognigy API keys do not expire unless explicitly rotated in the console. If your instance uses OAuth2 client credentials flow, replace the environment variable lookup with a standard POST /oauth/token request and implement a refresh callback before token expiration.
Implementation
Step 1: Webhook Endpoint and Payload Parsing
Cognigy.AI triggers webhooks with a POST request containing the current session data. The endpoint must parse the JSON payload, extract the utterance and session identifier, and prepare the request for NLP processing.
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
@RequestMapping("/webhook")
public class CognigyWebhookController {
private final EntityExtractionService extractionService;
public CognigyWebhookController(EntityExtractionService extractionService) {
this.extractionService = extractionService;
}
@PostMapping("/ingest")
public ResponseEntity<Map<String, Object>> handleIngestion(@RequestBody Map<String, Object> payload) {
String sessionId = (String) payload.get("sessionId");
String utterance = (String) payload.get("utterance");
@SuppressWarnings("unchecked")
Map<String, Object> previousState = (Map<String, Object>) payload.get("previousState");
if (sessionId == null || utterance == null) {
return ResponseEntity.badRequest().body(Map.of("error", "Missing sessionId or utterance"));
}
Map<String, Object> extractedState = extractionService.processUtterance(utterance, previousState);
return ResponseEntity.ok(extractedState);
}
}
Expected request payload from Cognigy:
{
"sessionId": "s-1234567890abcdef",
"conversationId": "c-0987654321fedcba",
"utterance": "Book a flight to Paris for tomorrow",
"previousState": {
"destination": "London",
"travelDate": "2023-10-15"
},
"language": "en"
}
The endpoint returns immediately with the extracted state. Cognigy expects a 200 OK response to continue flow execution. Error handling at this layer ensures malformed payloads do not crash the webhook service.
Step 2: spaCy NLP Pipeline Integration
Java does not natively run spaCy. The standard production pattern delegates NLP processing to a local Python FastAPI server. This step shows how to invoke the spaCy endpoint, parse the response, and handle network failures.
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.List;
import java.util.Map;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
public class SpacyClient {
private final HttpClient client;
private final String spacyEndpoint;
private final Gson gson;
public SpacyClient(String spacyEndpoint) {
this.spacyEndpoint = spacyEndpoint;
this.client = HttpClient.newBuilder()
.connectTimeout(java.time.Duration.ofSeconds(5))
.readTimeout(java.time.Duration.ofSeconds(10))
.build();
this.gson = new Gson();
}
public List<Map<String, Object>> extractEntities(String text) {
String requestBody = gson.toJson(Map.of("text", text));
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(spacyEndpoint + "/parse"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(requestBody))
.build();
try {
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) {
throw new RuntimeException("spaCy pipeline returned status " + response.statusCode());
}
Map<String, Object> responseBody = gson.fromJson(response.body(), Map.class);
@SuppressWarnings("unchecked")
List<Map<String, Object>> entities = (List<Map<String, Object>>) responseBody.get("ents");
return entities != null ? entities : List.of();
} catch (Exception e) {
throw new RuntimeException("Failed to communicate with spaCy pipeline", e);
}
}
}
The spaCy FastAPI server must expose /parse and return entities in the format:
{
"ents": [
{"label": "GPE", "text": "Paris", "start": 17, "end": 22},
{"label": "DATE", "text": "tomorrow", "start": 27, "end": 35}
]
}
Network timeouts and non-200 responses throw exceptions that propagate to the controller. The connectTimeout and readTimeout prevent thread starvation during spaCy pipeline degradation.
Step 3: Context-Aware Disambiguation Algorithm
Raw NER output lacks conversational context. Ambiguous references such as relative dates or pronouns require resolution against previous session state. This algorithm compares current entities against historical slots and applies recency and frequency weighting.
import java.util.*;
import java.util.stream.Collectors;
public class ContextDisambiguator {
public Map<String, Object> resolveEntities(List<Map<String, Object>> rawEntities, Map<String, Object> previousState) {
Map<String, Object> resolvedSlots = new LinkedHashMap<>();
Map<String, Object> history = previousState != null ? previousState : Map.of();
for (Map<String, Object> entity : rawEntities) {
String label = (String) entity.get("label");
String text = (String) entity.get("text");
String slotName = mapLabelToSlot(label);
String resolvedValue = disambiguate(text, label, history);
resolvedSlots.put(slotName, Map.of(
"value", resolvedValue,
"confidence", 0.95,
"source", "spacy"
));
}
return resolvedSlots;
}
private String mapLabelToSlot(String label) {
return switch (label) {
case "GPE", "LOC" -> "destination";
case "DATE" -> "travelDate";
case "PERSON" -> "passengerName";
default -> "entity_" + label.toLowerCase();
};
}
private String disambiguate(String text, String label, Map<String, Object> history) {
if (label.equals("DATE") && isRelativeDate(text)) {
String baseDate = (String) history.get("travelDate");
if (baseDate != null) {
return resolveRelativeToDate(text, baseDate);
}
}
if (text.equalsIgnoreCase("it") || text.equalsIgnoreCase("that")) {
return findMostRecentRelevantEntity(history);
}
return text;
}
private boolean isRelativeDate(String text) {
return text.matches("(?i)^(tomorrow|today|next\\s+week|yesterday)$");
}
private String resolveRelativeToDate(String relative, String base) {
return switch (relative.toLowerCase()) {
case "tomorrow" -> calculateNextDay(base);
case "yesterday" -> calculatePreviousDay(base);
default -> base;
};
}
private String calculateNextDay(String isoDate) {
return java.time.LocalDate.parse(isoDate).plusDays(1).toString();
}
private String calculatePreviousDay(String isoDate) {
return java.time.LocalDate.parse(isoDate).minusDays(1).toString();
}
private String findMostRecentRelevantEntity(Map<String, Object> history) {
for (Object value : history.values()) {
if (value instanceof Map) {
Map<String, Object> slot = (Map<String, Object>) value;
Object val = slot.get("value");
if (val != null) return val.toString();
}
}
return "unknown";
}
}
The disambiguation logic maintains conversation continuity by anchoring relative references to previously established absolute values. The mapLabelToSlot method enforces Cognigy slot naming conventions.
Step 4: Slot Mapping and Cognigy State Update
After disambiguation, the service must push the resolved entities back to Cognigy.AI. The REST API requires a specific JSON structure and the session:state:write scope. This step handles the HTTP request, pagination of state updates if necessary, and conflict resolution.
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import com.google.gson.Gson;
import java.util.Map;
public class CognigyStateClient {
private final HttpClient client;
private final String cognigyInstanceUrl;
private final Gson gson;
public CognigyStateClient(String cognigyInstanceUrl) {
this.cognigyInstanceUrl = cognigyInstanceUrl.endsWith("/") ?
cognigyInstanceUrl.substring(0, cognigyInstanceUrl.length() - 1) : cognigyInstanceUrl;
this.client = HttpClient.newBuilder().build();
this.gson = new Gson();
}
public void updateSessionState(String sessionId, Map<String, Object> resolvedSlots) {
String apiToken = CognigyAuthConfig.getToken(cognigyInstanceUrl);
String endpoint = String.format("%s/api/v1/sessions/%s/state", cognigyInstanceUrl, sessionId);
Map<String, Object> requestBody = Map.of("state", resolvedSlots);
String jsonPayload = gson.toJson(requestBody);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(endpoint))
.header("Authorization", "Bearer " + apiToken)
.header("Content-Type", "application/json")
.PUT(HttpRequest.BodyPublishers.ofString(jsonPayload))
.build();
try {
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() == 401) {
throw new SecurityException("Invalid or expired Cognigy API token");
}
if (response.statusCode() == 403) {
throw new SecurityException("API key lacks session:state:write scope");
}
if (response.statusCode() == 429) {
String retryAfter = response.headers().firstValue("Retry-After").orElse("5");
Thread.sleep(Long.parseLong(retryAfter) * 1000);
updateSessionState(sessionId, resolvedSlots);
return;
}
if (response.statusCode() >= 500) {
throw new RuntimeException("Cognigy API internal error: " + response.statusCode());
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("State update interrupted", e);
} catch (Exception e) {
throw new RuntimeException("Failed to update Cognigy session state", e);
}
}
}
The Cognigy REST API expects a PUT request to /api/v1/sessions/{sessionId}/state. The request body wraps the slot map under the state key. The implementation handles 429 rate limits by reading the Retry-After header and executing a single retry. 401 and 403 responses indicate authentication or scope misconfiguration.
Complete Working Example
The following module combines the controller, service, and clients into a single runnable Spring Boot application. Place the code in src/main/java/com/example/cognigy/CognigyNlpApplication.java.
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.http.ResponseEntity;
import java.util.Map;
@SpringBootApplication
@RestController
@RequestMapping("/webhook")
public class CognigyNlpApplication {
private final EntityExtractionService extractionService;
private final CognigyStateClient stateClient;
public CognigyNlpApplication(SpacyClient spacyClient, ContextDisambiguator disambiguator, String cognigyInstance) {
this.extractionService = new EntityExtractionService(spacyClient, disambiguator);
this.stateClient = new CognigyStateClient(cognigyInstance);
}
@Bean
public SpacyClient spacyClient() {
String endpoint = System.getenv("SPACY_ENDPOINT");
return new SpacyClient(endpoint != null ? endpoint : "http://localhost:8000");
}
@Bean
public ContextDisambiguator disambiguator() {
return new ContextDisambiguator();
}
@Bean
public CognigyStateClient cognigyStateClient() {
return new CognigyStateClient(System.getenv("COGNIGY_INSTANCE_URL"));
}
@PostMapping("/ingest")
public ResponseEntity<Map<String, Object>> handleIngestion(@RequestBody Map<String, Object> payload) {
String sessionId = (String) payload.get("sessionId");
String utterance = (String) payload.get("utterance");
@SuppressWarnings("unchecked")
Map<String, Object> previousState = (Map<String, Object>) payload.get("previousState");
if (sessionId == null || utterance == null) {
return ResponseEntity.badRequest().body(Map.of("error", "Missing sessionId or utterance"));
}
Map<String, Object> extractedState = extractionService.processUtterance(utterance, previousState);
stateClient.updateSessionState(sessionId, extractedState);
return ResponseEntity.ok(extractedState);
}
public static void main(String[] args) {
SpringApplication.run(CognigyNlpApplication.class, args);
}
}
class EntityExtractionService {
private final SpacyClient spacyClient;
private final ContextDisambiguator disambiguator;
public EntityExtractionService(SpacyClient spacyClient, ContextDisambiguator disambiguator) {
this.spacyClient = spacyClient;
this.disambiguator = disambiguator;
}
public Map<String, Object> processUtterance(String utterance, Map<String, Object> previousState) {
var rawEntities = spacyClient.extractEntities(utterance);
return disambiguator.resolveEntities(rawEntities, previousState);
}
}
Add the following to pom.xml for compilation:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>3.2.0</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.10.1</version>
</dependency>
</dependencies>
Run the application with environment variables:
export COGNIGY_API_KEY="your_api_key_here"
export COGNIGY_INSTANCE_URL="https://your-instance.api.cognigy.ai"
export SPACY_ENDPOINT="http://localhost:8000"
java -jar target/cognigy-nlp-webhook-0.0.1-SNAPSHOT.jar
Common Errors & Debugging
Error: 401 Unauthorized
- Cause: The API key is missing, malformed, or revoked in the Cognigy console.
- Fix: Verify the
COGNIGY_API_KEYenvironment variable matches the key generated in Cognigy.AI instance settings. Regenerate the key if rotation occurred. - Code showing the fix: The
CognigyAuthConfigthrowsIllegalStateExceptionif the key is absent. Catch this exception in production and trigger an alert or fallback mechanism.
Error: 403 Forbidden
- Cause: The API key lacks the
session:state:writescope. - Fix: Navigate to the Cognigy.AI instance API key settings and assign the
session:state:writepermission. Save and reinitialize the webhook service. - Code showing the fix: The
CognigyStateClientexplicitly checks for 403 and throwsSecurityException. Log this event and verify scope assignment.
Error: 429 Too Many Requests
- Cause: The Cognigy REST API enforces rate limits per instance. Rapid webhook triggers exceed the threshold.
- Fix: Implement exponential backoff. The provided code reads the
Retry-Afterheader and sleeps before a single retry. For high-volume deployments, queue state updates and batch them. - Code showing the fix: The 429 handler in
updateSessionStatedemonstrates header parsing and thread suspension.
Error: spaCy Pipeline Timeout
- Cause: The local FastAPI server is overloaded or the NLP model requires cold start.
- Fix: Increase
connectTimeoutandreadTimeoutinHttpClient.newBuilder(). Preload the spaCy model by sending a dummy request during application startup. - Code showing the fix: Modify
SpacyClientconstructor:.connectTimeout(java.time.Duration.ofSeconds(10)).readTimeout(java.time.Duration.ofSeconds(15)).