Skip to content

Commit 83fcb2f

Browse files
authored
[Gemini] Add Java GeminiModelHandler class (#39245)
* [Gemini] Add Java GeminiModelHandler class * Remove model scratch files * address review comments * checkstyle issue * unit test fix * Fix weird coder UX * fix altered base class signatures
1 parent 414b48a commit 83fcb2f

15 files changed

Lines changed: 809 additions & 5 deletions

File tree

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
plugins {
19+
id 'org.apache.beam.module'
20+
}
21+
22+
applyJavaNature(
23+
automaticModuleName: 'org.apache.beam.sdk.ml.inference.gemini',
24+
)
25+
provideIntegrationTestingDependencies()
26+
enableJavaPerformanceTesting()
27+
28+
description = "Apache Beam :: SDKs :: Java :: ML :: Inference :: Gemini"
29+
ext.summary = "Gemini model handler for remote inference"
30+
31+
dependencies {
32+
implementation project(":sdks:java:ml:inference:remote")
33+
implementation "com.google.genai:google-genai:1.59.0"
34+
35+
36+
testRuntimeOnly project(path: ":runners:direct-java", configuration: "shadow")
37+
testImplementation project(path: ":sdks:java:core", configuration: "shadow")
38+
testImplementation library.java.slf4j_api
39+
testRuntimeOnly library.java.slf4j_simple
40+
testImplementation library.java.junit
41+
testImplementation library.java.mockito_core
42+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
package org.apache.beam.sdk.ml.inference.gemini;
19+
20+
import org.apache.beam.sdk.ml.inference.remote.BaseResponse;
21+
22+
/** Wrapper for image (byte array) responses from Gemini. */
23+
public class GeminiImageResponse implements BaseResponse {
24+
public final byte[] imageBytes;
25+
26+
public GeminiImageResponse(byte[] imageBytes) {
27+
this.imageBytes = imageBytes;
28+
}
29+
30+
public byte[] getImageBytes() {
31+
return imageBytes;
32+
}
33+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
package org.apache.beam.sdk.ml.inference.gemini;
19+
20+
import com.google.genai.types.GenerateContentConfig;
21+
import com.google.genai.types.GenerateContentResponse;
22+
import com.google.genai.types.GenerateImagesConfig;
23+
import com.google.genai.types.GenerateImagesResponse;
24+
import java.util.ArrayList;
25+
import java.util.List;
26+
27+
/** Common inference functions for Gemini. */
28+
public class GeminiInferenceFunctions {
29+
30+
/** Generates content from string prompts using the standard generateContent API. */
31+
public static GeminiRequestFunction<GeminiStringInput, GeminiStringResponse>
32+
generateFromString() {
33+
return (modelName, batch, client) -> {
34+
List<GeminiStringResponse> results = new ArrayList<>();
35+
for (GeminiStringInput input : batch) {
36+
GenerateContentResponse response =
37+
client.models.generateContent(
38+
modelName, input.getText(), GenerateContentConfig.builder().build());
39+
String text = response.text();
40+
results.add(new GeminiStringResponse(text != null ? text : ""));
41+
}
42+
return results;
43+
};
44+
}
45+
46+
/** Generates images from string prompts using the generateImages API. */
47+
public static GeminiRequestFunction<GeminiStringInput, GeminiImageResponse>
48+
generateImageFromString() {
49+
return (modelName, batch, client) -> {
50+
List<GeminiImageResponse> results = new ArrayList<>();
51+
for (GeminiStringInput input : batch) {
52+
GenerateImagesResponse response =
53+
client.models.generateImages(
54+
modelName, input.getText(), GenerateImagesConfig.builder().build());
55+
// Retrieve the base64 string or bytes from the first generated image
56+
List<com.google.genai.types.Image> images = response.images();
57+
if (images != null && !images.isEmpty()) {
58+
byte[] imageBytes = images.get(0).imageBytes().orElse(new byte[0]);
59+
results.add(new GeminiImageResponse(imageBytes));
60+
} else {
61+
results.add(new GeminiImageResponse(new byte[0]));
62+
}
63+
}
64+
return results;
65+
};
66+
}
67+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
package org.apache.beam.sdk.ml.inference.gemini;
19+
20+
import com.google.genai.Client;
21+
import java.util.ArrayList;
22+
import java.util.List;
23+
import org.apache.beam.sdk.ml.inference.remote.BaseInput;
24+
import org.apache.beam.sdk.ml.inference.remote.BaseModelHandler;
25+
import org.apache.beam.sdk.ml.inference.remote.BaseResponse;
26+
import org.apache.beam.sdk.ml.inference.remote.PredictionResult;
27+
28+
/**
29+
* Model handler for Google Gemini API inference requests.
30+
*
31+
* <p>This handler manages communication with Google's Gemini API, including client initialization,
32+
* request formatting, and response parsing. It allows executing a custom {@link
33+
* GeminiRequestFunction} against a batch of inputs.
34+
*/
35+
@SuppressWarnings("nullness")
36+
public class GeminiModelHandler<InputT extends BaseInput, OutputT extends BaseResponse>
37+
implements BaseModelHandler<GeminiModelParameters<InputT, OutputT>, InputT, OutputT> {
38+
39+
private transient Client client;
40+
private GeminiModelParameters<InputT, OutputT> modelParameters;
41+
42+
@Override
43+
public void createClient(GeminiModelParameters<InputT, OutputT> parameters) {
44+
if (parameters == null) {
45+
throw new NullPointerException("GeminiModelParameters must not be null");
46+
}
47+
this.modelParameters = parameters;
48+
49+
// Configure client based on vertex or API key
50+
if (parameters.getApiKey() != null) {
51+
if (parameters.getProject() != null || parameters.getLocation() != null) {
52+
throw new IllegalArgumentException("Project and location must be null if API key is set");
53+
}
54+
this.client = Client.builder().apiKey(parameters.getApiKey()).build();
55+
} else {
56+
Client.Builder builder = Client.builder();
57+
if (parameters.getProject() != null && parameters.getLocation() != null) {
58+
builder.vertexAI(true).project(parameters.getProject()).location(parameters.getLocation());
59+
} else if (parameters.getProject() != null || parameters.getLocation() != null) {
60+
throw new IllegalArgumentException(
61+
"Project and location must both be provided if one is provided");
62+
}
63+
this.client = builder.build();
64+
}
65+
}
66+
67+
@Override
68+
public Iterable<PredictionResult<InputT, OutputT>> request(List<InputT> input) {
69+
try {
70+
GeminiRequestFunction<InputT, OutputT> requestFn = modelParameters.getRequestFn();
71+
List<OutputT> responses = requestFn.apply(modelParameters.getModelName(), input, client);
72+
73+
if (responses.size() != input.size()) {
74+
throw new IllegalStateException("Number of responses must match number of inputs");
75+
}
76+
77+
List<PredictionResult<InputT, OutputT>> results = new ArrayList<>();
78+
for (int i = 0; i < input.size(); i++) {
79+
results.add(PredictionResult.create(input.get(i), responses.get(i)));
80+
}
81+
return results;
82+
} catch (Exception e) {
83+
throw new RuntimeException("Error during Gemini inference request", e);
84+
}
85+
}
86+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
package org.apache.beam.sdk.ml.inference.gemini;
19+
20+
import com.google.auto.value.AutoValue;
21+
import org.apache.beam.sdk.ml.inference.remote.BaseInput;
22+
import org.apache.beam.sdk.ml.inference.remote.BaseModelParameters;
23+
import org.apache.beam.sdk.ml.inference.remote.BaseResponse;
24+
import org.checkerframework.checker.nullness.qual.Nullable;
25+
26+
@AutoValue
27+
public abstract class GeminiModelParameters<InputT extends BaseInput, OutputT extends BaseResponse>
28+
implements BaseModelParameters {
29+
30+
public abstract @Nullable String getApiKey();
31+
32+
public abstract @Nullable String getProject();
33+
34+
public abstract @Nullable String getLocation();
35+
36+
public abstract String getModelName();
37+
38+
public abstract GeminiRequestFunction<InputT, OutputT> getRequestFn();
39+
40+
public static <InputT extends BaseInput, OutputT extends BaseResponse>
41+
Builder<InputT, OutputT> builder() {
42+
return new AutoValue_GeminiModelParameters.Builder<InputT, OutputT>();
43+
}
44+
45+
@AutoValue.Builder
46+
public abstract static class Builder<InputT extends BaseInput, OutputT extends BaseResponse> {
47+
public abstract Builder<InputT, OutputT> setApiKey(String apiKey);
48+
49+
public abstract Builder<InputT, OutputT> setProject(String project);
50+
51+
public abstract Builder<InputT, OutputT> setLocation(String location);
52+
53+
public abstract Builder<InputT, OutputT> setModelName(String modelName);
54+
55+
public abstract Builder<InputT, OutputT> setRequestFn(
56+
GeminiRequestFunction<InputT, OutputT> requestFn);
57+
58+
public abstract GeminiModelParameters<InputT, OutputT> build();
59+
}
60+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
package org.apache.beam.sdk.ml.inference.gemini;
19+
20+
import com.google.genai.Client;
21+
import java.io.Serializable;
22+
import java.util.List;
23+
import org.apache.beam.sdk.ml.inference.remote.BaseInput;
24+
import org.apache.beam.sdk.ml.inference.remote.BaseResponse;
25+
26+
/** Functional interface for custom request functions to the Gemini API. */
27+
@FunctionalInterface
28+
public interface GeminiRequestFunction<InputT extends BaseInput, OutputT extends BaseResponse>
29+
extends Serializable {
30+
List<OutputT> apply(String modelName, List<InputT> batch, Client client) throws Exception;
31+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
package org.apache.beam.sdk.ml.inference.gemini;
19+
20+
import org.apache.beam.sdk.ml.inference.remote.BaseInput;
21+
22+
/** Wrapper for string inputs to Gemini. */
23+
public class GeminiStringInput implements BaseInput {
24+
public final String text;
25+
26+
public GeminiStringInput(String text) {
27+
this.text = text;
28+
}
29+
30+
public String getText() {
31+
return text;
32+
}
33+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
package org.apache.beam.sdk.ml.inference.gemini;
19+
20+
import org.apache.beam.sdk.ml.inference.remote.BaseResponse;
21+
22+
/** Wrapper for string responses from Gemini. */
23+
public class GeminiStringResponse implements BaseResponse {
24+
public final String text;
25+
26+
public GeminiStringResponse(String text) {
27+
this.text = text;
28+
}
29+
30+
public String getText() {
31+
return text;
32+
}
33+
}

0 commit comments

Comments
 (0)