Skip to content

Commit e21807c

Browse files
google-genai-botcopybara-github
authored andcommitted
feat: Introduced ApplicationIntegrationToolset in JavaADK
PiperOrigin-RevId: 779788941
1 parent faf2913 commit e21807c

5 files changed

Lines changed: 682 additions & 0 deletions

File tree

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

0 commit comments

Comments
 (0)