Skip to content

Commit a57f75a

Browse files
authored
Merge pull request #118 from IBM/text-detection-api
Add Text Detection APIs
2 parents 024e02e + cb813b8 commit a57f75a

30 files changed

+1306
-55
lines changed

modules/watsonx-ai/src/main/java/com/ibm/watsonx/ai/WatsonxParameters.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,9 @@ public String getTransactionId() {
5252
*/
5353
@SuppressWarnings("unchecked")
5454
public static abstract class Builder<T extends Builder<T>> {
55-
private String projectId;
56-
private String spaceId;
57-
private String transactionId;
55+
protected String projectId;
56+
protected String spaceId;
57+
protected String transactionId;
5858

5959
/**
6060
* Sets the project id.
@@ -114,7 +114,7 @@ public String getModelId() {
114114
*/
115115
@SuppressWarnings("unchecked")
116116
public static abstract class Builder<T extends Builder<T>> extends WatsonxParameters.Builder<T> {
117-
private String modelId;
117+
protected String modelId;
118118

119119
/**
120120
* Sets the model id.

modules/watsonx-ai/src/main/java/com/ibm/watsonx/ai/WatsonxService.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package com.ibm.watsonx.ai;
66

77
import static java.util.Objects.isNull;
8+
import static java.util.Objects.nonNull;
89
import static java.util.Objects.requireNonNull;
910
import static java.util.Objects.requireNonNullElse;
1011
import java.net.URI;
@@ -13,6 +14,7 @@
1314
import com.ibm.watsonx.ai.core.auth.AuthenticationProvider;
1415
import com.ibm.watsonx.ai.core.auth.iam.IAMAuthenticator;
1516
import com.ibm.watsonx.ai.deployment.DeploymentService;
17+
import com.ibm.watsonx.ai.detection.DetectionService;
1618
import com.ibm.watsonx.ai.embedding.EmbeddingService;
1719
import com.ibm.watsonx.ai.foundationmodel.FoundationModelService;
1820
import com.ibm.watsonx.ai.rerank.RerankService;
@@ -38,6 +40,7 @@
3840
* @see TimeSeriesService
3941
* @see FoundationModelService
4042
* @see ToolService
43+
* @see DetectionService
4144
*/
4245
public abstract class WatsonxService {
4346

@@ -176,6 +179,9 @@ public AuthenticationProvider getAuthenticationProvider() {
176179
* Abstract base class for watsonx services that require a project or space context.
177180
*/
178181
public static abstract class ProjectService extends WatsonxService {
182+
183+
protected record ProjectSpace(String projectId, String spaceId) {}
184+
179185
protected final String projectId;
180186
protected final String spaceId;
181187

@@ -188,6 +194,12 @@ protected ProjectService(Builder<?> builder) {
188194
throw new NullPointerException("Either projectId or spaceId must be provided");
189195
}
190196

197+
protected ProjectSpace resolveProjectSpace(WatsonxParameters parameters) {
198+
return nonNull(parameters.getProjectId()) || nonNull(parameters.getSpaceId())
199+
? new ProjectSpace(parameters.getProjectId(), parameters.getSpaceId())
200+
: new ProjectSpace(projectId, spaceId);
201+
}
202+
191203
@SuppressWarnings("unchecked")
192204
protected static abstract class Builder<T extends Builder<T>> extends WatsonxService.Builder<T> {
193205
private String projectId;
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright IBM Corp. 2025 - 2025
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
package com.ibm.watsonx.ai.detection;
6+
7+
import java.util.Map;
8+
9+
/**
10+
* Represents a generic detection request.
11+
*/
12+
abstract class BaseDetectionRequest {
13+
private final Map<String, Map<String, Object>> detectors;
14+
private final String projectId;
15+
private final String spaceId;
16+
17+
protected BaseDetectionRequest(Map<String, Map<String, Object>> detectors, String projectId, String spaceId) {
18+
this.detectors = detectors;
19+
this.projectId = projectId;
20+
this.spaceId = spaceId;
21+
}
22+
23+
public Map<String, Map<String, Object>> getDetectors() {
24+
return detectors;
25+
}
26+
27+
public String getProjectId() {
28+
return projectId;
29+
}
30+
31+
public String getSpaceId() {
32+
return spaceId;
33+
}
34+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* Copyright IBM Corp. 2025 - 2025
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
package com.ibm.watsonx.ai.detection;
6+
7+
/**
8+
* Represents the response for a generic detection request.
9+
*/
10+
abstract class BaseDetectionResponse {
11+
protected String text;
12+
protected String detectionType;
13+
protected String detection;
14+
protected float score;
15+
16+
public String getText() {
17+
return text;
18+
}
19+
20+
public void setText(String text) {
21+
this.text = text;
22+
}
23+
24+
public String getDetectionType() {
25+
return detectionType;
26+
}
27+
28+
public void setDetectionType(String detectionType) {
29+
this.detectionType = detectionType;
30+
}
31+
32+
public String getDetection() {
33+
return detection;
34+
}
35+
36+
public void setDetection(String detection) {
37+
this.detection = detection;
38+
}
39+
40+
public float getScore() {
41+
return score;
42+
}
43+
44+
public void setScore(float score) {
45+
this.score = score;
46+
}
47+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*
2+
* Copyright IBM Corp. 2025 - 2025
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
package com.ibm.watsonx.ai.detection;
6+
7+
import static com.ibm.watsonx.ai.core.Json.fromJson;
8+
import static com.ibm.watsonx.ai.core.Json.toJson;
9+
import static java.util.Objects.nonNull;
10+
import static java.util.Objects.requireNonNull;
11+
import java.io.IOException;
12+
import java.net.URI;
13+
import java.net.http.HttpRequest;
14+
import java.net.http.HttpRequest.BodyPublishers;
15+
import java.net.http.HttpResponse.BodyHandlers;
16+
import com.ibm.watsonx.ai.core.factory.HttpClientFactory;
17+
import com.ibm.watsonx.ai.core.http.SyncHttpClient;
18+
import com.ibm.watsonx.ai.core.http.interceptors.LoggerInterceptor.LogMode;
19+
import com.ibm.watsonx.ai.core.spi.json.TypeToken;
20+
21+
/**
22+
* Default implementation of the {@link DetectionRestClient} abstract class.
23+
*/
24+
final class DefaultRestClient extends DetectionRestClient {
25+
private final SyncHttpClient syncHttpClient;
26+
27+
DefaultRestClient(Builder builder) {
28+
super(builder);
29+
requireNonNull(authenticationProvider, "authenticationProvider is mandatory");
30+
syncHttpClient = HttpClientFactory.createSync(authenticationProvider, LogMode.of(logRequests, logResponses));
31+
}
32+
33+
@Override
34+
public DetectionResponse<DetectionTextResponse> detect(String transactionId, TextDetectionContentDetectors request) {
35+
36+
var httpRequest = HttpRequest
37+
.newBuilder(URI.create(baseUrl + "/ml/v1/text/detection?version=%s".formatted(version)))
38+
.header("Content-Type", "application/json")
39+
.header("Accept", "application/json")
40+
.POST(BodyPublishers.ofString(toJson(request)))
41+
.timeout(timeout);
42+
43+
if (nonNull(transactionId))
44+
httpRequest.header(TRANSACTION_ID_HEADER, transactionId);
45+
46+
try {
47+
48+
var httpReponse = syncHttpClient.send(httpRequest.build(), BodyHandlers.ofString());
49+
return fromJson(httpReponse.body(), new TypeToken<DetectionResponse<DetectionTextResponse>>() {});
50+
51+
} catch (IOException | InterruptedException e) {
52+
throw new RuntimeException(e);
53+
}
54+
}
55+
56+
/**
57+
* Returns a new {@link Builder} instance.
58+
*/
59+
static Builder builder() {
60+
return new Builder();
61+
}
62+
63+
/**
64+
* Builder class for constructing {@link DefaultRestClient} instances with configurable parameters.
65+
*/
66+
public final static class Builder extends DetectionRestClient.Builder {
67+
68+
private Builder() {}
69+
70+
/**
71+
* Builds a {@link DefaultRestClient} instance using the configured parameters.
72+
*
73+
* @return a new instance of {@link DefaultRestClient}
74+
*/
75+
public DefaultRestClient build() {
76+
return new DefaultRestClient(this);
77+
}
78+
}
79+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/*
2+
* Copyright IBM Corp. 2025 - 2025
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
package com.ibm.watsonx.ai.detection;
6+
7+
import java.util.List;
8+
9+
/**
10+
* Represents the response returned by the Text Detection API.
11+
*/
12+
public record DetectionResponse<T extends BaseDetectionResponse>(List<T> detections) {}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Copyright IBM Corp. 2025 - 2025
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
package com.ibm.watsonx.ai.detection;
6+
7+
import java.util.ServiceLoader;
8+
import java.util.function.Supplier;
9+
import com.ibm.watsonx.ai.WatsonxRestClient;
10+
11+
/**
12+
* Abstraction of a REST client for interacting with the IBM watsonx.ai Text Detection APIs.
13+
*/
14+
public abstract class DetectionRestClient extends WatsonxRestClient {
15+
16+
protected DetectionRestClient(Builder builder) {
17+
super(builder);
18+
}
19+
20+
public abstract DetectionResponse<DetectionTextResponse> detect(String transactionId, TextDetectionContentDetectors request);
21+
22+
/**
23+
* Creates a new {@link Builder} using the first available {@link DetectionRestClientBuilderFactory} discovered via {@link ServiceLoader}.
24+
* <p>
25+
* If no factory is found, falls back to the default {@link DefaultRestClient}.
26+
*/
27+
static DetectionRestClient.Builder builder() {
28+
return ServiceLoader.load(DetectionRestClientBuilderFactory.class).findFirst()
29+
.map(Supplier::get)
30+
.orElse(DefaultRestClient.builder());
31+
}
32+
33+
/**
34+
* Builder abstract class for constructing {@link DetectionRestClient} instances with configurable parameters.
35+
*/
36+
public abstract static class Builder extends WatsonxRestClient.Builder<DetectionRestClient, Builder> {}
37+
38+
/**
39+
* Service Provider Interface for supplying custom {@link Builder} implementations.
40+
* <p>
41+
* This allows frameworks to provide their own client implementations.
42+
*/
43+
public interface DetectionRestClientBuilderFactory extends Supplier<DetectionRestClient.Builder> {}
44+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/*
2+
* Copyright IBM Corp. 2025 - 2025
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
package com.ibm.watsonx.ai.detection;
6+
7+
import static java.util.Objects.requireNonNull;
8+
import com.ibm.watsonx.ai.WatsonxService.ProjectService;
9+
import com.ibm.watsonx.ai.core.auth.AuthenticationProvider;
10+
11+
/**
12+
* Service class to interact with IBM watsonx.ai Text Detection APIs.
13+
* <p>
14+
* <b>Example usage:</b>
15+
*
16+
* <pre>{@code
17+
* DetectionService detectionService = DetectionService.builder()
18+
* .baseUrl("https://...") // or use CloudRegion
19+
* .apiKey("my-api-key") // creates an IAM-based AuthenticationProvider
20+
* .projectId("my-project-id")
21+
* .build();
22+
*
23+
* DetectionResponse<DetectionTextResponse> response = detectionService.detect(
24+
* DetectionTextRequest.builder()
25+
* .input("...")
26+
* .detectors(Pii.create(), Hap.builder().threshold(0.3).build())
27+
* .build()
28+
* );
29+
* }</pre>
30+
*
31+
* To use a custom authentication mechanism, configure it explicitly with {@code authenticationProvider(AuthenticationProvider)}.
32+
*
33+
* @see AuthenticationProvider
34+
*/
35+
public final class DetectionService extends ProjectService {
36+
37+
private final DetectionRestClient client;
38+
39+
private DetectionService(Builder builder) {
40+
super(builder);
41+
requireNonNull(builder.getAuthenticationProvider(), "authenticationProvider cannot be null");
42+
client = DetectionRestClient.builder()
43+
.baseUrl(baseUrl)
44+
.version(version)
45+
.logRequests(logRequests)
46+
.logResponses(logResponses)
47+
.timeout(timeout)
48+
.authenticationProvider(builder.getAuthenticationProvider())
49+
.build();
50+
}
51+
52+
public DetectionResponse<DetectionTextResponse> detect(DetectionTextRequest request) {
53+
var projectSpace = resolveProjectSpace(request);
54+
var transactionId = request.getTransactionId();
55+
var textDetectionRequest =
56+
new TextDetectionContentDetectors(request.getInput(), request.getDetectors(), projectSpace.projectId(), projectSpace.spaceId());
57+
return client.detect(transactionId, textDetectionRequest);
58+
}
59+
60+
/**
61+
* Returns a new {@link Builder} instance.
62+
* <p>
63+
* <b>Example usage:</b>
64+
*
65+
* <pre>{@code
66+
* DetectionService detectionService = DetectionService.builder()
67+
* .baseUrl("https://...") // or use CloudRegion
68+
* .apiKey("my-api-key") // creates an IAM-based AuthenticationProvider
69+
* .projectId("my-project-id")
70+
* .build();
71+
*
72+
* DetectionResponse<DetectionTextResponse> response = detectionService.detect(
73+
* DetectionTextRequest.builder()
74+
* .input("...")
75+
* .detectors(Pii.create(), Hap.builder().threshold(0.3).build())
76+
* .build()
77+
* );
78+
* }</pre>
79+
*
80+
* @return {@link Builder} instance.
81+
*/
82+
public static Builder builder() {
83+
return new Builder();
84+
}
85+
86+
/**
87+
* Builder class for constructing {@link DetectionService} instances with configurable parameters.
88+
*/
89+
public final static class Builder extends ProjectService.Builder<Builder> {
90+
91+
private Builder() {}
92+
93+
/**
94+
* Builds a {@link DetectionService} instance using the configured parameters.
95+
*
96+
* @return a new instance of {@link DetectionService}
97+
*/
98+
public DetectionService build() {
99+
return new DetectionService(this);
100+
}
101+
}
102+
}

0 commit comments

Comments
 (0)