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