Skip to content

Commit 3b424c3

Browse files
google-genai-botcopybara-github
authored andcommitted
feat: Introduced ConnectionClient and IntegrationClient to get OPENAPISPEC of connection
PiperOrigin-RevId: 775484780
1 parent e21807c commit 3b424c3

File tree

8 files changed

+2141
-196
lines changed

8 files changed

+2141
-196
lines changed

core/src/main/java/com/google/adk/tools/applicationintegrationtoolset/ApplicationIntegrationTool.java

Lines changed: 154 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@
22

33
import com.fasterxml.jackson.databind.JsonNode;
44
import com.fasterxml.jackson.databind.ObjectMapper;
5+
import com.fasterxml.jackson.databind.node.ArrayNode;
56
import com.fasterxml.jackson.databind.node.ObjectNode;
67
import com.google.adk.tools.BaseTool;
78
import com.google.adk.tools.ToolContext;
89
import com.google.auth.oauth2.GoogleCredentials;
10+
import com.google.common.collect.ImmutableList;
911
import com.google.common.collect.ImmutableMap;
12+
import com.google.common.collect.Streams;
1013
import com.google.genai.types.FunctionDeclaration;
1114
import com.google.genai.types.Schema;
1215
import io.reactivex.rxjava3.core.Single;
@@ -16,7 +19,9 @@
1619
import java.net.http.HttpRequest;
1720
import java.net.http.HttpResponse;
1821
import java.util.Iterator;
22+
import java.util.List;
1923
import java.util.Map;
24+
import java.util.Objects;
2025
import java.util.Optional;
2126
import org.jspecify.annotations.Nullable;
2227

@@ -26,11 +31,19 @@ public class ApplicationIntegrationTool extends BaseTool {
2631
private final String openApiSpec;
2732
private final String pathUrl;
2833
private final HttpExecutor httpExecutor;
34+
private final String connectionName;
35+
private final String serviceName;
36+
private final String host;
37+
private String entity;
38+
private String operation;
39+
private String action;
2940
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
3041

3142
interface HttpExecutor {
3243
<T> HttpResponse<T> send(HttpRequest request, HttpResponse.BodyHandler<T> responseBodyHandler)
3344
throws IOException, InterruptedException;
45+
46+
String getToken() throws IOException;
3447
}
3548

3649
static class DefaultHttpExecutor implements HttpExecutor {
@@ -42,23 +55,75 @@ public <T> HttpResponse<T> send(
4255
throws IOException, InterruptedException {
4356
return client.send(request, responseBodyHandler);
4457
}
58+
59+
@Override
60+
public String getToken() throws IOException {
61+
GoogleCredentials credentials =
62+
GoogleCredentials.getApplicationDefault()
63+
.createScoped("https://www.googleapis.com/auth/cloud-platform");
64+
credentials.refreshIfExpired();
65+
return credentials.getAccessToken().getTokenValue();
66+
}
4567
}
4668

47-
public ApplicationIntegrationTool(
69+
private static final ImmutableList<String> EXCLUDE_FIELDS =
70+
ImmutableList.of("connectionName", "serviceName", "host", "entity", "operation", "action");
71+
72+
private static final ImmutableList<String> OPTIONAL_FIELDS =
73+
ImmutableList.of("pageSize", "pageToken", "filter", "sortByColumns");
74+
75+
/** Constructor for Application Integration Tool for integration */
76+
ApplicationIntegrationTool(
4877
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());
78+
this(
79+
openApiSpec,
80+
pathUrl,
81+
toolName,
82+
toolDescription,
83+
null,
84+
null,
85+
null,
86+
new DefaultHttpExecutor());
87+
}
88+
89+
/**
90+
* Constructor for Application Integration Tool with connection name, service name, host, entity,
91+
* operation, and action
92+
*/
93+
ApplicationIntegrationTool(
94+
String openApiSpec,
95+
String pathUrl,
96+
String toolName,
97+
String toolDescription,
98+
String connectionName,
99+
String serviceName,
100+
String host) {
101+
this(
102+
openApiSpec,
103+
pathUrl,
104+
toolName,
105+
toolDescription,
106+
connectionName,
107+
serviceName,
108+
host,
109+
new DefaultHttpExecutor());
51110
}
52111

53112
ApplicationIntegrationTool(
54113
String openApiSpec,
55114
String pathUrl,
56115
String toolName,
57116
String toolDescription,
117+
@Nullable String connectionName,
118+
@Nullable String serviceName,
119+
@Nullable String host,
58120
HttpExecutor httpExecutor) {
59121
super(toolName, toolDescription);
60122
this.openApiSpec = openApiSpec;
61123
this.pathUrl = pathUrl;
124+
this.connectionName = connectionName;
125+
this.serviceName = serviceName;
126+
this.host = host;
62127
this.httpExecutor = httpExecutor;
63128
}
64129

@@ -67,19 +132,10 @@ Schema toGeminiSchema(String openApiSchema, String operationId) throws Exception
67132
return Schema.fromJson(resolvedSchemaString);
68133
}
69134

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-
79135
@Override
80136
public Optional<FunctionDeclaration> declaration() {
81137
try {
82-
String operationId = extractTriggerIdFromPath(pathUrl);
138+
String operationId = getOperationIdFromPathUrl(openApiSpec, pathUrl);
83139
Schema parametersSchema = toGeminiSchema(openApiSpec, operationId);
84140
String operationDescription = getOperationDescription(openApiSpec, operationId);
85141

@@ -98,6 +154,18 @@ public Optional<FunctionDeclaration> declaration() {
98154

99155
@Override
100156
public Single<Map<String, Object>> runAsync(Map<String, Object> args, ToolContext toolContext) {
157+
if (this.connectionName != null) {
158+
args.put("connectionName", this.connectionName);
159+
args.put("serviceName", this.serviceName);
160+
args.put("host", this.host);
161+
if (!Objects.equals(this.entity, "")) {
162+
args.put("entity", this.entity);
163+
args.put("operation", this.operation);
164+
} else if (!Objects.equals(this.action, "")) {
165+
args.put("action", this.action);
166+
}
167+
}
168+
101169
return Single.fromCallable(
102170
() -> {
103171
try {
@@ -121,7 +189,7 @@ private String executeIntegration(Map<String, Object> args) throws Exception {
121189
HttpRequest request =
122190
HttpRequest.newBuilder()
123191
.uri(URI.create(url))
124-
.header("Authorization", "Bearer " + getAccessToken())
192+
.header("Authorization", "Bearer " + httpExecutor.getToken())
125193
.header("Content-Type", "application/json")
126194
.POST(HttpRequest.BodyPublishers.ofString(jsonRequestBody))
127195
.build();
@@ -138,12 +206,47 @@ private String executeIntegration(Map<String, Object> args) throws Exception {
138206
return response.body();
139207
}
140208

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();
209+
String getOperationIdFromPathUrl(String openApiSchemaString, String pathUrl) throws Exception {
210+
JsonNode topLevelNode = OBJECT_MAPPER.readTree(openApiSchemaString);
211+
JsonNode specNode = topLevelNode.path("openApiSpec");
212+
if (specNode.isMissingNode() || !specNode.isTextual()) {
213+
throw new IllegalArgumentException(
214+
"Failed to get OpenApiSpec, please check the project and region for the integration.");
215+
}
216+
JsonNode rootNode = OBJECT_MAPPER.readTree(specNode.asText());
217+
JsonNode paths = rootNode.path("paths");
218+
219+
// Iterate through each path in the OpenAPI spec.
220+
Iterator<Map.Entry<String, JsonNode>> pathsFields = paths.fields();
221+
while (pathsFields.hasNext()) {
222+
Map.Entry<String, JsonNode> pathEntry = pathsFields.next();
223+
String currentPath = pathEntry.getKey();
224+
if (!currentPath.equals(pathUrl)) {
225+
continue;
226+
}
227+
JsonNode pathItem = pathEntry.getValue();
228+
229+
Iterator<Map.Entry<String, JsonNode>> methods = pathItem.fields();
230+
while (methods.hasNext()) {
231+
Map.Entry<String, JsonNode> methodEntry = methods.next();
232+
JsonNode operationNode = methodEntry.getValue();
233+
// Set values for entity, operation, and action
234+
this.entity = "";
235+
this.operation = "";
236+
this.action = "";
237+
if (operationNode.has("x-entity")) {
238+
this.entity = operationNode.path("x-entity").asText();
239+
this.operation = operationNode.path("x-operation").asText();
240+
} else if (operationNode.has("x-action")) {
241+
this.action = operationNode.path("x-action").asText();
242+
}
243+
// Get the operationId from the operationNode
244+
if (operationNode.has("operationId")) {
245+
return operationNode.path("operationId").asText();
246+
}
247+
}
248+
}
249+
throw new Exception("Could not find operationId for pathUrl: " + pathUrl);
147250
}
148251

149252
private String getResolvedRequestSchemaByOperationId(
@@ -155,7 +258,6 @@ private String getResolvedRequestSchemaByOperationId(
155258
"Failed to get OpenApiSpec, please check the project and region for the integration.");
156259
}
157260
JsonNode rootNode = OBJECT_MAPPER.readTree(specNode.asText());
158-
159261
JsonNode operationNode = findOperationNodeById(rootNode, operationId);
160262
if (operationNode == null) {
161263
throw new Exception("Could not find operation with operationId: " + operationId);
@@ -169,19 +271,47 @@ private String getResolvedRequestSchemaByOperationId(
169271

170272
JsonNode resolvedSchema = resolveRefs(requestSchemaNode, rootNode);
171273

274+
if (resolvedSchema.isObject()) {
275+
ObjectNode schemaObject = (ObjectNode) resolvedSchema;
276+
277+
// 1. Remove excluded fields from the 'properties' object.
278+
JsonNode propertiesNode = schemaObject.path("properties");
279+
if (propertiesNode.isObject()) {
280+
ObjectNode propertiesObject = (ObjectNode) propertiesNode;
281+
for (String field : EXCLUDE_FIELDS) {
282+
propertiesObject.remove(field);
283+
}
284+
}
285+
286+
// 2. Remove optional and excluded fields from the 'required' array.
287+
JsonNode requiredNode = schemaObject.path("required");
288+
if (requiredNode.isArray()) {
289+
// Combine the lists of fields to remove
290+
List<String> fieldsToRemove =
291+
Streams.concat(OPTIONAL_FIELDS.stream(), EXCLUDE_FIELDS.stream()).toList();
292+
293+
// To safely remove items from a list while iterating, we must use an Iterator.
294+
ArrayNode requiredArray = (ArrayNode) requiredNode;
295+
Iterator<JsonNode> elements = requiredArray.elements();
296+
while (elements.hasNext()) {
297+
JsonNode element = elements.next();
298+
if (element.isTextual() && fieldsToRemove.contains(element.asText())) {
299+
// This removes the current element from the underlying array.
300+
elements.remove();
301+
}
302+
}
303+
}
304+
}
172305
return OBJECT_MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(resolvedSchema);
173306
}
174307

175308
private @Nullable JsonNode findOperationNodeById(JsonNode rootNode, String operationId) {
176309
JsonNode paths = rootNode.path("paths");
177-
// Iterate through each path in the OpenAPI spec.
178310
for (JsonNode pathItem : paths) {
179-
// Iterate through each HTTP method (e.g., GET, POST) for the current path.
180311
Iterator<Map.Entry<String, JsonNode>> methods = pathItem.fields();
181312
while (methods.hasNext()) {
182313
Map.Entry<String, JsonNode> methodEntry = methods.next();
183314
JsonNode operationNode = methodEntry.getValue();
184-
// Check if the operationId matches the target operationId.
185315
if (operationNode.path("operationId").asText().equals(operationId)) {
186316
return operationNode;
187317
}

0 commit comments

Comments
 (0)