Skip to content

Commit 3e76ce0

Browse files
google-genai-botcopybara-github
authored andcommitted
Introduced ApplicationIntegrationToolset in JavaADK
PiperOrigin-RevId: 769592012
1 parent c8fed2d commit 3e76ce0

File tree

5 files changed

+674
-0
lines changed

5 files changed

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

0 commit comments

Comments
 (0)