Skip to content

Commit ca3ed31

Browse files
google-genai-botcopybara-github
authored andcommitted
feat: Introduced ApplicationIntegrationToolset in JavaADK
PiperOrigin-RevId: 769592012
1 parent 4983747 commit ca3ed31

File tree

5 files changed

+673
-0
lines changed

5 files changed

+673
-0
lines changed
Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
package com.google.adk.tools.applicationintegrationtoolset;
2+
3+
import com.fasterxml.jackson.databind.JsonNode;
4+
import com.fasterxml.jackson.databind.ObjectMapper;
5+
import com.fasterxml.jackson.databind.node.ObjectNode;
6+
import com.google.adk.tools.BaseTool;
7+
import com.google.adk.tools.ToolContext;
8+
import com.google.auth.oauth2.GoogleCredentials;
9+
import com.google.common.collect.ImmutableMap;
10+
import com.google.genai.types.FunctionDeclaration;
11+
import com.google.genai.types.Schema;
12+
import io.reactivex.rxjava3.core.Single;
13+
import java.io.IOException;
14+
import java.net.URI;
15+
import java.net.http.HttpClient;
16+
import java.net.http.HttpRequest;
17+
import java.net.http.HttpResponse;
18+
import java.util.Iterator;
19+
import java.util.Map;
20+
import java.util.Optional;
21+
import org.jspecify.annotations.Nullable;
22+
23+
/** Application Integration Toolset */
24+
public class ApplicationIntegrationTool extends BaseTool {
25+
26+
private final String openApiSpec;
27+
private final String pathUrl;
28+
private final HttpExecutor httpExecutor;
29+
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
30+
31+
/**
32+
* An interface that describes the contract for sending HTTP requests. It can be public or
33+
* package-private. Public is fine if it might be useful elsewhere.
34+
*/
35+
interface HttpExecutor {
36+
<T> HttpResponse<T> send(HttpRequest request, HttpResponse.BodyHandler<T> responseBodyHandler)
37+
throws IOException, InterruptedException;
38+
}
39+
40+
static class DefaultHttpExecutor implements HttpExecutor {
41+
private final HttpClient client = HttpClient.newHttpClient();
42+
43+
@Override
44+
public <T> HttpResponse<T> send(
45+
HttpRequest request, HttpResponse.BodyHandler<T> responseBodyHandler)
46+
throws IOException, InterruptedException {
47+
return client.send(request, responseBodyHandler);
48+
}
49+
}
50+
51+
/** Public constructor for users. */
52+
public ApplicationIntegrationTool(
53+
String openApiSpec, String pathUrl, String toolName, String toolDescription) {
54+
// Chain to the internal constructor, providing real dependencies.
55+
this(openApiSpec, pathUrl, toolName, toolDescription, new DefaultHttpExecutor());
56+
}
57+
58+
ApplicationIntegrationTool(
59+
String openApiSpec,
60+
String pathUrl,
61+
String toolName,
62+
String toolDescription,
63+
HttpExecutor httpExecutor) {
64+
super(toolName, toolDescription);
65+
this.openApiSpec = openApiSpec;
66+
this.pathUrl = pathUrl;
67+
this.httpExecutor = httpExecutor;
68+
}
69+
70+
Schema toGeminiSchema(String openApiSchema, String operationId) throws Exception {
71+
String resolvedSchemaString = getResolvedRequestSchemaByOperationId(openApiSchema, operationId);
72+
return Schema.fromJson(resolvedSchemaString);
73+
}
74+
75+
@Nullable String extractTriggerIdFromPath(String path) {
76+
String prefix = "triggerId=api_trigger/";
77+
int startIndex = path.indexOf(prefix);
78+
if (startIndex == -1) {
79+
return null;
80+
}
81+
return path.substring(startIndex + prefix.length());
82+
}
83+
84+
@Override
85+
public Optional<FunctionDeclaration> declaration() {
86+
try {
87+
String operationId = extractTriggerIdFromPath(pathUrl);
88+
Schema parametersSchema = toGeminiSchema(openApiSpec, operationId);
89+
String operationDescription = getOperationDescription(openApiSpec, operationId);
90+
91+
FunctionDeclaration declaration =
92+
FunctionDeclaration.builder()
93+
.name(operationId)
94+
.description(operationDescription)
95+
.parameters(parametersSchema)
96+
.build();
97+
return Optional.of(declaration);
98+
} catch (Exception e) {
99+
System.err.println("Failed to get OpenAPI spec: " + e.getMessage());
100+
return Optional.empty();
101+
}
102+
}
103+
104+
@Override
105+
public Single<Map<String, Object>> runAsync(Map<String, Object> args, ToolContext toolContext) {
106+
return Single.fromCallable(
107+
() -> {
108+
try {
109+
String response = executeIntegration(args);
110+
return ImmutableMap.of("result", response);
111+
} catch (Exception e) {
112+
System.err.println("Failed to execute integration: " + e.getMessage());
113+
return ImmutableMap.of("error", e.getMessage());
114+
}
115+
});
116+
}
117+
118+
private String executeIntegration(Map<String, Object> args) throws Exception {
119+
String url = String.format("https://integrations.googleapis.com%s", pathUrl);
120+
String jsonRequestBody;
121+
try {
122+
jsonRequestBody = OBJECT_MAPPER.writeValueAsString(args);
123+
} catch (IOException e) {
124+
throw new Exception("Error converting args to JSON: " + e.getMessage(), e);
125+
}
126+
HttpRequest request =
127+
HttpRequest.newBuilder()
128+
.uri(URI.create(url))
129+
.header("Authorization", "Bearer " + getAccessToken())
130+
.header("Content-Type", "application/json")
131+
.POST(HttpRequest.BodyPublishers.ofString(jsonRequestBody))
132+
.build();
133+
HttpResponse<String> response =
134+
httpExecutor.send(request, HttpResponse.BodyHandlers.ofString());
135+
136+
if (response.statusCode() < 200 || response.statusCode() >= 300) {
137+
throw new Exception(
138+
"Error executing integration. Status: "
139+
+ response.statusCode()
140+
+ " , Response: "
141+
+ response.body());
142+
}
143+
return response.body();
144+
}
145+
146+
String getAccessToken() throws IOException {
147+
GoogleCredentials credentials =
148+
GoogleCredentials.getApplicationDefault()
149+
.createScoped("https://www.googleapis.com/auth/cloud-platform");
150+
credentials.refreshIfExpired();
151+
return credentials.getAccessToken().getTokenValue();
152+
}
153+
154+
private String getResolvedRequestSchemaByOperationId(
155+
String openApiSchemaString, String operationId) throws Exception {
156+
JsonNode topLevelNode = OBJECT_MAPPER.readTree(openApiSchemaString);
157+
JsonNode specNode = topLevelNode.path("openApiSpec");
158+
if (specNode.isMissingNode() || !specNode.isTextual()) {
159+
throw new IllegalArgumentException(
160+
"Failed to get OpenApiSpec, please check the project and region for the integration.");
161+
}
162+
JsonNode rootNode = OBJECT_MAPPER.readTree(specNode.asText());
163+
164+
JsonNode operationNode = findOperationNodeById(rootNode, operationId);
165+
if (operationNode == null) {
166+
throw new Exception("Could not find operation with operationId: " + operationId);
167+
}
168+
JsonNode requestSchemaNode =
169+
operationNode.path("requestBody").path("content").path("application/json").path("schema");
170+
171+
if (requestSchemaNode.isMissingNode()) {
172+
throw new Exception("Could not find request body schema for operationId: " + operationId);
173+
}
174+
175+
JsonNode resolvedSchema = resolveRefs(requestSchemaNode, rootNode);
176+
177+
return OBJECT_MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(resolvedSchema);
178+
}
179+
180+
private @Nullable JsonNode findOperationNodeById(JsonNode rootNode, String operationId) {
181+
JsonNode paths = rootNode.path("paths");
182+
// Iterate through each path in the OpenAPI spec.
183+
for (JsonNode pathItem : paths) {
184+
// Iterate through each HTTP method (e.g., GET, POST) for the current path.
185+
Iterator<Map.Entry<String, JsonNode>> methods = pathItem.fields();
186+
while (methods.hasNext()) {
187+
Map.Entry<String, JsonNode> methodEntry = methods.next();
188+
JsonNode operationNode = methodEntry.getValue();
189+
// Check if the operationId matches the target operationId.
190+
if (operationNode.path("operationId").asText().equals(operationId)) {
191+
return operationNode;
192+
}
193+
}
194+
}
195+
return null;
196+
}
197+
198+
private JsonNode resolveRefs(JsonNode currentNode, JsonNode rootNode) {
199+
if (currentNode.isObject()) {
200+
ObjectNode objectNode = (ObjectNode) currentNode;
201+
if (objectNode.has("$ref")) {
202+
String refPath = objectNode.get("$ref").asText();
203+
if (refPath.isEmpty() || !refPath.startsWith("#/")) {
204+
return objectNode;
205+
}
206+
JsonNode referencedNode = rootNode.at(refPath.substring(1));
207+
if (referencedNode.isMissingNode()) {
208+
return objectNode;
209+
}
210+
return resolveRefs(referencedNode, rootNode);
211+
} else {
212+
ObjectNode newObjectNode = OBJECT_MAPPER.createObjectNode();
213+
Iterator<Map.Entry<String, JsonNode>> fields = currentNode.fields();
214+
while (fields.hasNext()) {
215+
Map.Entry<String, JsonNode> field = fields.next();
216+
newObjectNode.set(field.getKey(), resolveRefs(field.getValue(), rootNode));
217+
}
218+
return newObjectNode;
219+
}
220+
}
221+
return currentNode;
222+
}
223+
224+
private String getOperationDescription(String openApiSchemaString, String operationId)
225+
throws Exception {
226+
JsonNode topLevelNode = OBJECT_MAPPER.readTree(openApiSchemaString);
227+
JsonNode specNode = topLevelNode.path("openApiSpec");
228+
if (specNode.isMissingNode() || !specNode.isTextual()) {
229+
return "";
230+
}
231+
JsonNode rootNode = OBJECT_MAPPER.readTree(specNode.asText());
232+
JsonNode operationNode = findOperationNodeById(rootNode, operationId);
233+
if (operationNode == null) {
234+
return "";
235+
}
236+
return operationNode.path("summary").asText();
237+
}
238+
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
package com.google.adk.tools.applicationintegrationtoolset;
2+
3+
import com.fasterxml.jackson.databind.JsonNode;
4+
import com.fasterxml.jackson.databind.ObjectMapper;
5+
import com.google.adk.tools.BaseTool;
6+
import com.google.adk.tools.applicationintegrationtoolset.ApplicationIntegrationTool.DefaultHttpExecutor;
7+
import com.google.adk.tools.applicationintegrationtoolset.ApplicationIntegrationTool.HttpExecutor;
8+
import com.google.auth.oauth2.GoogleCredentials;
9+
import com.google.common.collect.ImmutableList;
10+
import com.google.common.collect.ImmutableMap;
11+
import java.io.IOException;
12+
import java.net.URI;
13+
import java.net.http.HttpRequest;
14+
import java.net.http.HttpResponse;
15+
import java.util.ArrayList;
16+
import java.util.Arrays;
17+
import java.util.Iterator;
18+
import java.util.List;
19+
import java.util.Map;
20+
import org.jspecify.annotations.Nullable;
21+
22+
/** Application Integration Toolset */
23+
public class ApplicationIntegrationToolset {
24+
String project;
25+
String location;
26+
String integration;
27+
List<String> triggers;
28+
private final HttpExecutor httpExecutor;
29+
public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
30+
31+
public ApplicationIntegrationToolset(
32+
String project, String location, String integration, List<String> triggers) {
33+
this(project, location, integration, triggers, new DefaultHttpExecutor());
34+
}
35+
36+
ApplicationIntegrationToolset(
37+
String project,
38+
String location,
39+
String integration,
40+
List<String> triggers,
41+
HttpExecutor httpExecutor) {
42+
this.project = project;
43+
this.location = location;
44+
this.integration = integration;
45+
this.triggers = triggers;
46+
this.httpExecutor = httpExecutor;
47+
}
48+
49+
String generateOpenApiSpec() throws Exception {
50+
String url =
51+
String.format(
52+
"https://%s-integrations.googleapis.com/v1/projects/%s/locations/%s:generateOpenApiSpec",
53+
this.location, this.project, this.location);
54+
55+
String jsonRequestBody =
56+
OBJECT_MAPPER.writeValueAsString(
57+
ImmutableMap.of(
58+
"apiTriggerResources",
59+
ImmutableList.of(
60+
ImmutableMap.of(
61+
"integrationResource",
62+
this.integration,
63+
"triggerId",
64+
Arrays.asList(this.triggers))),
65+
"fileFormat",
66+
"JSON"));
67+
HttpRequest request =
68+
HttpRequest.newBuilder()
69+
.uri(URI.create(url))
70+
.header("Authorization", "Bearer " + getAccessToken())
71+
.header("Content-Type", "application/json")
72+
.POST(HttpRequest.BodyPublishers.ofString(jsonRequestBody))
73+
.build();
74+
HttpResponse<String> response =
75+
httpExecutor.send(request, HttpResponse.BodyHandlers.ofString());
76+
77+
if (response.statusCode() < 200 || response.statusCode() >= 300) {
78+
throw new Exception("Error fetching OpenAPI spec. Status: " + response.statusCode());
79+
}
80+
return response.body();
81+
}
82+
83+
String getAccessToken() throws IOException {
84+
GoogleCredentials credentials =
85+
GoogleCredentials.getApplicationDefault()
86+
.createScoped(ImmutableList.of("https://www.googleapis.com/auth/cloud-platform"));
87+
credentials.refreshIfExpired();
88+
return credentials.getAccessToken().getTokenValue();
89+
}
90+
91+
List<String> getPathUrl(String openApiSchemaString) throws Exception {
92+
List<String> pathUrls = new ArrayList<>();
93+
JsonNode topLevelNode = OBJECT_MAPPER.readTree(openApiSchemaString);
94+
JsonNode specNode = topLevelNode.path("openApiSpec");
95+
if (specNode.isMissingNode() || !specNode.isTextual()) {
96+
throw new IllegalArgumentException(
97+
"API response must contain an 'openApiSpec' key with a string value.");
98+
}
99+
JsonNode rootNode = OBJECT_MAPPER.readTree(specNode.asText());
100+
JsonNode pathsNode = rootNode.path("paths");
101+
Iterator<Map.Entry<String, JsonNode>> paths = pathsNode.fields();
102+
while (paths.hasNext()) {
103+
Map.Entry<String, JsonNode> pathEntry = paths.next();
104+
String pathUrl = pathEntry.getKey();
105+
pathUrls.add(pathUrl);
106+
}
107+
return pathUrls;
108+
}
109+
110+
@Nullable String extractTriggerIdFromPath(String path) {
111+
String prefix = "triggerId=api_trigger/";
112+
int startIndex = path.indexOf(prefix);
113+
if (startIndex == -1) {
114+
return null;
115+
}
116+
return path.substring(startIndex + prefix.length());
117+
}
118+
119+
public List<BaseTool> getTools() throws Exception {
120+
String openApiSchemaString = generateOpenApiSpec();
121+
List<String> pathUrls = getPathUrl(openApiSchemaString);
122+
123+
List<BaseTool> tools = new ArrayList<>();
124+
for (String pathUrl : pathUrls) {
125+
String toolName = extractTriggerIdFromPath(pathUrl);
126+
if (toolName != null) {
127+
tools.add(new ApplicationIntegrationTool(openApiSchemaString, pathUrl, toolName, ""));
128+
} else {
129+
System.err.println(
130+
"Failed to get tool name , Please check the integration name , trigger id and location"
131+
+ " and project id.");
132+
}
133+
}
134+
135+
return tools;
136+
}
137+
}

0 commit comments

Comments
 (0)