Quarkus extension for integrating IBM watsonx.ai foundation models with LangChain4j. Provides chat models, generation models, streaming models, embedding models, and scoring models for IBM watsonx.ai. Includes comprehensive configuration options, support for tool/function calling, text extraction from documents in Cloud Object Storage, and experimental built-in services for Google search, weather, and web crawling. Designed for enterprise Java applications using the Quarkus framework with built-in dependency injection and native compilation support.
Comprehensive exception hierarchy for different error sources including Watsonx API errors, IBM Cloud IAM authentication errors, Cloud Object Storage errors, text extraction errors, and built-in service errors. All exceptions provide detailed error information for debugging and error handling.
RuntimeException
├── WatsonxException
├── TextExtractionException
├── COSException
└── BuiltinServiceException
Exception
└── WeatherServiceException
└── NoCityFoundExceptionMain exception for Watsonx API errors including model invocation, chat, generation, embedding, and scoring errors.
public class WatsonxException extends RuntimeException {
public WatsonxException(String message, Integer statusCode, WatsonxError details);
public WatsonxException(String message, Integer statusCode);
public WatsonxException(Integer statusCode, WatsonxError details);
public WatsonxException(Throwable cause, Integer statusCode);
public Integer statusCode();
public WatsonxError details();
}Common Status Codes:
Example Usage:
import io.quarkiverse.langchain4j.watsonx.exception.WatsonxException;
import io.quarkiverse.langchain4j.watsonx.bean.WatsonxError;
try {
ChatResponse response = chatModel.chat(UserMessage.from("Hello"));
} catch (WatsonxException e) {
System.err.println("Status code: " + e.statusCode());
System.err.println("Message: " + e.getMessage());
WatsonxError details = e.details();
if (details != null) {
System.err.println("Error code: " + details.code());
System.err.println("Error message: " + details.message());
}
// Handle specific status codes
switch (e.statusCode()) {
case 401:
// Re-authenticate or refresh token
break;
case 429:
// Implement backoff and retry
break;
case 500:
case 503:
// Retry with exponential backoff
break;
default:
// Log and handle as appropriate
break;
}
}Token Expiration Handling:
import io.quarkiverse.langchain4j.watsonx.WatsonxUtils;
try {
ChatResponse response = chatModel.chat(UserMessage.from("Hello"));
} catch (WatsonxException e) {
if (WatsonxUtils.isTokenExpired(e)) {
// Token expired, automatic retry will happen
System.err.println("Token expired, retrying...");
}
}
// Or use built-in retry helper
ChatResponse response = WatsonxUtils.retryOn(() ->
chatModel.chat(UserMessage.from("Hello"))
);Detailed error information from Watsonx API responses.
public record WatsonxError(Integer statusCode, String trace, List<Error> errors) {
public static record Error(String code, String message) {
public Optional<Code> codeToEnum();
}
public static enum Code {
AUTHORIZATION_REJECTED,
JSON_TYPE_ERROR,
MODEL_NOT_SUPPORTED,
MODEL_NO_SUPPORT_FOR_FUNCTION,
USER_AUTHORIZATION_FAILED,
JSON_VALIDATION_ERROR,
INVALID_REQUEST_ENTITY,
INVALID_INPUT_ARGUMENT,
TOKEN_QUOTA_REACHED,
AUTHENTICATION_TOKEN_EXPIRED,
TEXT_EXTRACTION_EVENT_DOES_NOT_EXIST
}
}Error Code Descriptions:
Example:
try {
ChatResponse response = chatModel.chat(request);
} catch (WatsonxException e) {
WatsonxError details = e.details();
if (details != null && details.errors() != null) {
for (WatsonxError.Error error : details.errors()) {
System.err.println("Error code: " + error.code());
System.err.println("Error message: " + error.message());
// Check for specific error codes
Optional<WatsonxError.Code> codeEnum = error.codeToEnum();
if (codeEnum.isPresent()) {
switch (codeEnum.get()) {
case AUTHENTICATION_TOKEN_EXPIRED:
System.err.println("Token expired - will retry");
break;
case MODEL_NOT_SUPPORTED:
System.err.println("Invalid model ID");
break;
case TOKEN_QUOTA_REACHED:
System.err.println("Quota exceeded - try later");
break;
default:
System.err.println("Other error: " + codeEnum.get());
}
}
}
}
}Exception for text extraction operations from Cloud Object Storage documents.
public class TextExtractionException extends RuntimeException {
public TextExtractionException(String code, String message);
public TextExtractionException(String code, String message, Throwable cause);
public String getCode();
public String getMessage();
}Common Error Codes:
Example Usage:
import io.quarkiverse.langchain4j.watsonx.exception.TextExtractionException;
import io.quarkiverse.langchain4j.watsonx.runtime.TextExtraction;
import java.io.File;
try {
String extractedText = textExtraction.uploadExtractAndFetch(file);
} catch (TextExtractionException e) {
System.err.println("Error code: " + e.getCode());
System.err.println("Message: " + e.getMessage());
switch (e.getCode()) {
case "invalid_document_format":
System.err.println("File format not supported");
break;
case "document_not_found":
System.err.println("Document not found in storage");
break;
case "timeout":
System.err.println("Extraction took too long");
break;
default:
System.err.println("Extraction failed: " + e.getMessage());
break;
}
}Async Extraction Error Handling:
String jobId = textExtraction.uploadAndStartExtraction(file);
// Check status
TextExtractionResponse status = textExtraction.checkExtractionStatus(jobId);
if (status.metadata().status() == TextExtractionResponse.Status.FAILED) {
TextExtractionResponse.ServiceError error = status.entity().serviceError();
throw new TextExtractionException(
error.code(),
error.message()
);
}Exception for Cloud Object Storage operations including file upload, download, and deletion.
public class COSException extends RuntimeException {
public COSException(Integer statusCode, CosError details);
public COSException(String message, Integer statusCode);
public Integer statusCode();
public CosError details();
}Common Status Codes:
Example Usage:
import io.quarkiverse.langchain4j.watsonx.exception.COSException;
import io.quarkiverse.langchain4j.watsonx.bean.CosError;
try {
textExtraction.deleteFile("bucket-name", "/path/to/file.pdf");
} catch (COSException e) {
System.err.println("Status code: " + e.statusCode());
System.err.println("Message: " + e.getMessage());
CosError details = e.details();
if (details != null) {
System.err.println("Error code: " + details.code());
System.err.println("Error message: " + details.message());
}
// Handle specific status codes
if (e.statusCode() == 404) {
System.err.println("File not found in COS");
} else if (e.statusCode() == 403) {
System.err.println("Permission denied");
}
}Detailed error information from Cloud Object Storage API.
public class CosError {
public Code getCode();
public String getMessage();
public String getResource();
public String getRequestId();
public int getHttpStatusCode();
public static enum Code {
ACCESS_DENIED,
BAD_DIGEST,
BUCKET_ALREADY_EXISTS,
BUCKET_ALREADY_OWNED_BY_YOU,
BUCKET_NOT_EMPTY,
CREDENTIALS_NOT_SUPPORTED,
ENTITY_TOO_SMALL,
ENTITY_TOO_LARGE,
INCOMPLETE_BODY,
INCORRECT_NUMBER_OF_FILES_IN_POST_REQUEST,
INLINE_DATA_TOO_LARGE,
INTERNAL_ERROR,
INVALID_ACCESS_KEY_ID,
INVALID_ARGUMENT,
INVALID_BUCKET_NAME,
INVALID_BUCKET_STATE,
INVALID_DIGEST,
INVALID_LOCATION_CONSTRAINT,
INVALID_OBJECT_STATE,
INVALID_PART,
INVALID_PART_ORDER,
INVALID_RANGE,
INVALID_REQUEST,
INVALID_SECURITY,
INVALID_URI,
KEY_TOO_LONG,
MALFORMED_POST_REQUEST,
MALFORMED_XML,
MAX_MESSAGE_LENGTH_EXCEEDED,
MAX_POST_PRE_DATA_LENGTH_EXCEEDED_ERROR,
METADATA_TOO_LARGE,
METHOD_NOT_ALLOWED,
MISSING_CONTENT_LENGTH,
MISSING_REQUEST_BODY_ERROR,
NO_SUCH_BUCKET,
NO_SUCH_KEY,
NO_SUCH_UPLOAD,
NOT_IMPLEMENTED,
OPERATION_ABORTED,
PRECONDITION_FAILED,
REDIRECT,
REQUEST_IS_NOT_MULTIPART_CONTENT,
REQUEST_TIMEOUT,
REQUEST_TIME_TOO_SKEWED,
SERVICE_UNAVAILABLE,
SLOW_DOWN,
TEMPORARY_REDIRECT,
TOO_MANY_BUCKETS,
UNEXPECTED_CONTENT,
USER_KEY_MUST_BE_SPECIFIED
}
}Common COS Error Codes:
Example:
try {
textExtraction.deleteFile(bucket, path);
} catch (COSException e) {
CosError error = e.details();
if (error != null) {
System.err.println("Error code: " + error.getCode());
System.err.println("Message: " + error.getMessage());
System.err.println("Resource: " + error.getResource());
System.err.println("Request ID: " + error.getRequestId());
System.err.println("HTTP status: " + error.getHttpStatusCode());
// Handle specific error codes
switch (error.getCode()) {
case ACCESS_DENIED:
System.err.println("Check COS permissions");
break;
case NO_SUCH_BUCKET:
case NO_SUCH_KEY:
System.err.println("Resource not found");
break;
case SERVICE_UNAVAILABLE:
System.err.println("Retry later");
break;
default:
System.err.println("COS error: " + error.getCode());
}
}
}Exception for built-in service operations (Google search, weather, web crawler).
public class BuiltinServiceException extends RuntimeException {
public BuiltinServiceException(String message, Integer statusCode);
public Integer statusCode();
public String details();
}Common Status Codes:
Example Usage:
import io.quarkiverse.langchain4j.watsonx.exception.BuiltinServiceException;
import io.quarkiverse.langchain4j.watsonx.services.WebCrawlerService;
try {
WebCrawlerResult result = webCrawler.process(url);
} catch (BuiltinServiceException e) {
System.err.println("Status code: " + e.statusCode());
System.err.println("Details: " + e.details());
if (e.statusCode() == 404) {
System.err.println("Web page not found");
} else if (e.statusCode() == 429) {
System.err.println("Rate limit exceeded, retry later");
} else if (e.statusCode() >= 500) {
System.err.println("Service error, retry with backoff");
}
}Exception for weather service operations.
public class WeatherServiceException extends Exception {
public WeatherServiceException(String message);
public WeatherServiceException(String message, Throwable cause);
}Example Usage:
import io.quarkiverse.langchain4j.watsonx.services.WeatherService.WeatherServiceException;
try {
String weather = weatherService.find(city, country);
} catch (WeatherServiceException e) {
System.err.println("Weather service error: " + e.getMessage());
}Exception thrown when a city is not found in the weather service.
public class NoCityFoundException extends WeatherServiceException {
public NoCityFoundException(String message);
}Example Usage:
import io.quarkiverse.langchain4j.watsonx.services.WeatherService.NoCityFoundException;
try {
String weather = weatherService.find("InvalidCity", "XX");
} catch (NoCityFoundException e) {
System.err.println("City not found: " + e.getMessage());
} catch (WeatherServiceException e) {
System.err.println("Weather service error: " + e.getMessage());
}@ApplicationScoped
public class SafeChatService {
@Inject
ChatModel chatModel;
public Optional<String> chatSafely(String message) {
try {
ChatResponse response = chatModel.chat(UserMessage.from(message));
return Optional.of(response.aiMessage().text());
} catch (WatsonxException e) {
logger.error("Chat failed: " + e.getMessage(), e);
return Optional.empty();
}
}
}@ApplicationScoped
public class RobustService {
@Inject
ChatModel chatModel;
@Inject
TextExtraction textExtraction;
@Inject
WebCrawlerService webCrawler;
public String processWithFallback(String input) {
try {
return chatModel.chat(UserMessage.from(input)).aiMessage().text();
} catch (WatsonxException e) {
if (e.statusCode() == 401) {
// Re-authenticate and retry
return retryAfterAuth(input);
} else if (e.statusCode() == 429) {
// Wait and retry
return retryWithBackoff(input);
} else {
throw e;
}
}
}
public String extractWithFallback(File file) {
try {
return textExtraction.uploadExtractAndFetch(file);
} catch (TextExtractionException e) {
if ("invalid_document_format".equals(e.getCode())) {
// Try alternative extraction method
return extractAlternative(file);
}
throw e;
} catch (COSException e) {
if (e.statusCode() == 403) {
// Permission issue, use different bucket
return extractFromAlternativeBucket(file);
}
throw e;
}
}
public WebCrawlerResult fetchWithRetry(String url) {
try {
return webCrawler.process(url);
} catch (BuiltinServiceException e) {
if (e.statusCode() == 429) {
// Rate limited, wait and retry
Thread.sleep(5000);
return webCrawler.process(url);
} else if (e.statusCode() >= 500) {
// Server error, retry with backoff
return retryWebCrawl(url);
}
throw e;
}
}
}import java.time.Duration;
import java.util.concurrent.TimeUnit;
@ApplicationScoped
public class RetryService {
@Inject
ChatModel chatModel;
public ChatResponse chatWithRetry(String message, int maxAttempts) {
int attempt = 0;
Duration backoff = Duration.ofSeconds(1);
while (attempt < maxAttempts) {
try {
return chatModel.chat(UserMessage.from(message));
} catch (WatsonxException e) {
attempt++;
// Only retry on transient errors
if (isTransientError(e) && attempt < maxAttempts) {
logger.warn("Attempt {} failed, retrying in {}",
attempt, backoff);
try {
Thread.sleep(backoff.toMillis());
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new RuntimeException("Interrupted", ie);
}
// Exponential backoff
backoff = backoff.multipliedBy(2);
} else {
throw e;
}
}
}
throw new RuntimeException("Max retry attempts exceeded");
}
private boolean isTransientError(WatsonxException e) {
return e.statusCode() == 429 || // Rate limit
e.statusCode() == 500 || // Server error
e.statusCode() == 503; // Service unavailable
}
}import io.quarkiverse.langchain4j.watsonx.WatsonxUtils;
@ApplicationScoped
public class AutoRefreshService {
@Inject
ChatModel chatModel;
public ChatResponse chatWithAutoRefresh(String message) {
// WatsonxUtils.retryOn automatically handles token expiration
return WatsonxUtils.retryOn(() ->
chatModel.chat(UserMessage.from(message))
);
}
}import io.github.resilience4j.circuitbreaker.CircuitBreaker;
import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig;
@ApplicationScoped
public class CircuitBreakerService {
private final CircuitBreaker circuitBreaker;
@Inject
ChatModel chatModel;
public CircuitBreakerService() {
CircuitBreakerConfig config = CircuitBreakerConfig.custom()
.failureRateThreshold(50)
.waitDurationInOpenState(Duration.ofSeconds(30))
.slidingWindowSize(10)
.build();
this.circuitBreaker = CircuitBreaker.of("watsonx", config);
}
public Optional<String> chatWithCircuitBreaker(String message) {
try {
return Optional.of(circuitBreaker.executeSupplier(() -> {
ChatResponse response = chatModel.chat(UserMessage.from(message));
return response.aiMessage().text();
}));
} catch (Exception e) {
logger.error("Chat failed with circuit breaker", e);
return Optional.empty();
}
}
}@ApplicationScoped
public class BulkOperationService {
@Inject
TextExtraction textExtraction;
public record ExtractionResult(File file, String text, String error) {}
public List<ExtractionResult> extractMultiple(List<File> files) {
return files.stream()
.map(file -> {
try {
String text = textExtraction.uploadExtractAndFetch(file);
return new ExtractionResult(file, text, null);
} catch (TextExtractionException e) {
return new ExtractionResult(file, null,
"Extraction error: " + e.getMessage());
} catch (COSException e) {
return new ExtractionResult(file, null,
"Storage error: " + e.getMessage());
} catch (Exception e) {
return new ExtractionResult(file, null,
"Unexpected error: " + e.getMessage());
}
})
.toList();
}
public void printResults(List<ExtractionResult> results) {
long successful = results.stream()
.filter(r -> r.error() == null)
.count();
long failed = results.size() - successful;
System.out.println("Successful: " + successful);
System.out.println("Failed: " + failed);
results.stream()
.filter(r -> r.error() != null)
.forEach(r -> System.err.println(
r.file().getName() + ": " + r.error()));
}
}import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ApplicationScoped
public class LoggingService {
private static final Logger logger = LoggerFactory.getLogger(LoggingService.class);
@Inject
ChatModel chatModel;
public String chatWithLogging(String message) {
try {
logger.debug("Sending chat request: {}", message);
ChatResponse response = chatModel.chat(UserMessage.from(message));
logger.debug("Chat response received successfully");
return response.aiMessage().text();
} catch (WatsonxException e) {
logger.error("Watsonx error - Status: {}, Message: {}",
e.statusCode(), e.getMessage());
if (e.details() != null) {
logger.error("Error details - Code: {}, Message: {}",
e.details().code(), e.details().message());
}
// Log full stack trace at debug level
logger.debug("Full exception:", e);
throw e;
} catch (Exception e) {
logger.error("Unexpected error in chat operation", e);
throw e;
}
}
}// Custom exception wrapper
public class ChatServiceException extends RuntimeException {
private final ErrorType errorType;
private final Integer statusCode;
public enum ErrorType {
AUTHENTICATION_ERROR,
RATE_LIMIT_ERROR,
SERVICE_ERROR,
CLIENT_ERROR,
UNKNOWN_ERROR
}
public ChatServiceException(WatsonxException e) {
super(e.getMessage(), e);
this.statusCode = e.statusCode();
this.errorType = determineErrorType(e);
}
private ErrorType determineErrorType(WatsonxException e) {
return switch (e.statusCode()) {
case 401, 403 -> ErrorType.AUTHENTICATION_ERROR;
case 429 -> ErrorType.RATE_LIMIT_ERROR;
case 500, 503 -> ErrorType.SERVICE_ERROR;
case 400 -> ErrorType.CLIENT_ERROR;
default -> ErrorType.UNKNOWN_ERROR;
};
}
public ErrorType getErrorType() {
return errorType;
}
public Integer getStatusCode() {
return statusCode;
}
}
// Usage
@ApplicationScoped
public class CustomExceptionService {
@Inject
ChatModel chatModel;
public String chat(String message) {
try {
ChatResponse response = chatModel.chat(UserMessage.from(message));
return response.aiMessage().text();
} catch (WatsonxException e) {
throw new ChatServiceException(e);
}
}
}Detailed error information from IAM authentication.
public class IAMError {
public String errorCode();
public String errorMessage();
public Map<String, Object> additionalProperties();
}IAM errors are typically wrapped in WatsonxException during token generation.
Install with Tessl CLI
npx tessl i tessl/maven-io-quarkiverse-langchain4j--quarkus-langchain4j-watsonx@1.7.0