Agent Development Kit(ADK)

An easy-to-use and powerful framework to build AI agents.
This commit is contained in:
hangfei
2025-04-08 17:22:09 +00:00
parent f92478bd5c
commit 9827820143
299 changed files with 44398 additions and 2 deletions

View File

@@ -0,0 +1,19 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from .application_integration_toolset import ApplicationIntegrationToolset
__all__ = [
'ApplicationIntegrationToolset',
]

View File

@@ -0,0 +1,230 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from typing import Dict
from typing import List
from typing import Optional
from fastapi.openapi.models import HTTPBearer
from google.adk.tools.application_integration_tool.clients.connections_client import ConnectionsClient
from google.adk.tools.application_integration_tool.clients.integration_client import IntegrationClient
from google.adk.tools.openapi_tool.auth.auth_helpers import service_account_scheme_credential
from google.adk.tools.openapi_tool.openapi_spec_parser.openapi_toolset import OpenAPIToolset
from google.adk.tools.openapi_tool.openapi_spec_parser.rest_api_tool import RestApiTool
from ...auth.auth_credential import AuthCredential
from ...auth.auth_credential import AuthCredentialTypes
from ...auth.auth_credential import ServiceAccount
from ...auth.auth_credential import ServiceAccountCredential
# TODO(cheliu): Apply a common toolset interface
class ApplicationIntegrationToolset:
"""ApplicationIntegrationToolset generates tools from a given Application
Integration or Integration Connector resource.
Example Usage:
```
# Get all available tools for an integration with api trigger
application_integration_toolset = ApplicationIntegrationToolset(
project="test-project",
location="us-central1"
integration="test-integration",
trigger="api_trigger/test_trigger",
service_account_credentials={...},
)
# Get all available tools for a connection using entity operations and
# actions
# Note: Find the list of supported entity operations and actions for a
connection
# using integration connector apis:
#
https://cloud.google.com/integration-connectors/docs/reference/rest/v1/projects.locations.connections.connectionSchemaMetadata
application_integration_toolset = ApplicationIntegrationToolset(
project="test-project",
location="us-central1"
connection="test-connection",
entity_operations=["EntityId1": ["LIST","CREATE"], "EntityId2": []],
#empty list for actions means all operations on the entity are supported
actions=["action1"],
service_account_credentials={...},
)
# Get all available tools
agent = LlmAgent(tools=[
...
*application_integration_toolset.get_tools(),
])
```
"""
def __init__(
self,
project: str,
location: str,
integration: Optional[str] = None,
trigger: Optional[str] = None,
connection: Optional[str] = None,
entity_operations: Optional[str] = None,
actions: Optional[str] = None,
# Optional parameter for the toolset. This is prepended to the generated
# tool/python function name.
tool_name: Optional[str] = "",
# Optional parameter for the toolset. This is appended to the generated
# tool/python function description.
tool_instructions: Optional[str] = "",
service_account_json: Optional[str] = None,
):
"""Initializes the ApplicationIntegrationToolset.
Example Usage:
```
# Get all available tools for an integration with api trigger
application_integration_toolset = ApplicationIntegrationToolset(
project="test-project",
location="us-central1"
integration="test-integration",
trigger="api_trigger/test_trigger",
service_account_credentials={...},
)
# Get all available tools for a connection using entity operations and
# actions
# Note: Find the list of supported entity operations and actions for a
connection
# using integration connector apis:
#
https://cloud.google.com/integration-connectors/docs/reference/rest/v1/projects.locations.connections.connectionSchemaMetadata
application_integration_toolset = ApplicationIntegrationToolset(
project="test-project",
location="us-central1"
connection="test-connection",
entity_operations=["EntityId1": ["LIST","CREATE"], "EntityId2": []],
#empty list for actions means all operations on the entity are supported
actions=["action1"],
service_account_credentials={...},
)
# Get all available tools
agent = LlmAgent(tools=[
...
*application_integration_toolset.get_tools(),
])
```
Args:
project: The GCP project ID.
location: The GCP location.
integration: The integration name.
trigger: The trigger name.
connection: The connection name.
entity_operations: The entity operations supported by the connection.
actions: The actions supported by the connection.
tool_name: The name of the tool.
tool_instructions: The instructions for the tool.
service_account_json: The service account configuration as a dictionary.
Required if not using default service credential. Used for fetching
the Application Integration or Integration Connector resource.
Raises:
ValueError: If neither integration and trigger nor connection and
(entity_operations or actions) is provided.
Exception: If there is an error during the initialization of the
integration or connection client.
"""
self.project = project
self.location = location
self.integration = integration
self.trigger = trigger
self.connection = connection
self.entity_operations = entity_operations
self.actions = actions
self.tool_name = tool_name
self.tool_instructions = tool_instructions
self.service_account_json = service_account_json
self.generated_tools: Dict[str, RestApiTool] = {}
integration_client = IntegrationClient(
project,
location,
integration,
trigger,
connection,
entity_operations,
actions,
service_account_json,
)
if integration and trigger:
spec = integration_client.get_openapi_spec_for_integration()
elif connection and (entity_operations or actions):
connections_client = ConnectionsClient(
project, location, connection, service_account_json
)
connection_details = connections_client.get_connection_details()
tool_instructions += (
"ALWAYS use serviceName = "
+ connection_details["serviceName"]
+ ", host = "
+ connection_details["host"]
+ " and the connection name = "
+ f"projects/{project}/locations/{location}/connections/{connection} when"
" using this tool"
+ ". DONOT ask the user for these values as you already have those."
)
spec = integration_client.get_openapi_spec_for_connection(
tool_name,
tool_instructions,
)
else:
raise ValueError(
"Either (integration and trigger) or (connection and"
" (entity_operations or actions)) should be provided."
)
self._parse_spec_to_tools(spec)
def _parse_spec_to_tools(self, spec_dict):
"""Parses the spec dict to a list of RestApiTool."""
if self.service_account_json:
sa_credential = ServiceAccountCredential.model_validate_json(
self.service_account_json
)
service_account = ServiceAccount(
service_account_credential=sa_credential,
scopes=["https://www.googleapis.com/auth/cloud-platform"],
)
auth_scheme, auth_credential = service_account_scheme_credential(
config=service_account
)
else:
auth_credential = AuthCredential(
auth_type=AuthCredentialTypes.SERVICE_ACCOUNT,
service_account=ServiceAccount(
use_default_credential=True,
scopes=["https://www.googleapis.com/auth/cloud-platform"],
),
)
auth_scheme = HTTPBearer(bearerFormat="JWT")
tools = OpenAPIToolset(
spec_dict=spec_dict,
auth_credential=auth_credential,
auth_scheme=auth_scheme,
).get_tools()
for tool in tools:
self.generated_tools[tool.name] = tool
def get_tools(self) -> List[RestApiTool]:
return list(self.generated_tools.values())

View File

@@ -0,0 +1,903 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import json
import time
from typing import Any, Dict, List, Optional, Tuple
import google.auth
from google.auth import default as default_service_credential
from google.auth.transport.requests import Request
from google.oauth2 import service_account
import requests
class ConnectionsClient:
"""Utility class for interacting with Google Cloud Connectors API."""
def __init__(
self,
project: str,
location: str,
connection: str,
service_account_json: Optional[str] = None,
):
"""Initializes the ConnectionsClient.
Args:
project: The Google Cloud project ID.
location: The Google Cloud location (e.g., us-central1).
connection: The connection name.
service_account_json: The service account configuration as a dictionary.
Required if not using default service credential. Used for fetching
connection details.
"""
self.project = project
self.location = location
self.connection = connection
self.connector_url = "https://connectors.googleapis.com"
self.service_account_json = service_account_json
self.credential_cache = None
def get_connection_details(self) -> Dict[str, Any]:
"""Retrieves service details (service name and host) for a given connection.
Also returns if auth override is enabled for the connection.
Returns:
tuple: A tuple containing (service_name, host).
Raises:
PermissionError: If there are credential issues.
ValueError: If there's a request error.
Exception: For any other unexpected errors.
"""
url = f"{self.connector_url}/v1/projects/{self.project}/locations/{self.location}/connections/{self.connection}?view=BASIC"
response = self._execute_api_call(url)
connection_data = response.json()
service_name = connection_data.get("serviceDirectory", "")
host = connection_data.get("host", "")
if host:
service_name = connection_data.get("tlsServiceDirectory", "")
auth_override_enabled = connection_data.get("authOverrideEnabled", False)
return {
"serviceName": service_name,
"host": host,
"authOverrideEnabled": auth_override_enabled,
}
def get_entity_schema_and_operations(
self, entity: str
) -> Tuple[Dict[str, Any], List[str]]:
"""Retrieves the JSON schema for a given entity in a connection.
Args:
entity (str): The entity name.
Returns:
tuple: A tuple containing (schema, operations).
Raises:
PermissionError: If there are credential issues.
ValueError: If there's a request or processing error.
Exception: For any other unexpected errors.
"""
url = f"{self.connector_url}/v1/projects/{self.project}/locations/{self.location}/connections/{self.connection}/connectionSchemaMetadata:getEntityType?entityId={entity}"
response = self._execute_api_call(url)
operation_id = response.json().get("name")
if not operation_id:
raise ValueError(
f"Failed to get entity schema and operations for entity: {entity}"
)
operation_response = self._poll_operation(operation_id)
schema = operation_response.get("response", {}).get("jsonSchema", {})
operations = operation_response.get("response", {}).get("operations", [])
return schema, operations
def get_action_schema(self, action: str) -> Dict[str, Any]:
"""Retrieves the input and output JSON schema for a given action in a connection.
Args:
action (str): The action name.
Returns:
tuple: A tuple containing (input_schema, output_schema).
Raises:
PermissionError: If there are credential issues.
ValueError: If there's a request or processing error.
Exception: For any other unexpected errors.
"""
url = f"{self.connector_url}/v1/projects/{self.project}/locations/{self.location}/connections/{self.connection}/connectionSchemaMetadata:getAction?actionId={action}"
response = self._execute_api_call(url)
operation_id = response.json().get("name")
if not operation_id:
raise ValueError(f"Failed to get action schema for action: {action}")
operation_response = self._poll_operation(operation_id)
input_schema = operation_response.get("response", {}).get(
"inputJsonSchema", {}
)
output_schema = operation_response.get("response", {}).get(
"outputJsonSchema", {}
)
description = operation_response.get("response", {}).get("description", "")
display_name = operation_response.get("response", {}).get("displayName", "")
return {
"inputSchema": input_schema,
"outputSchema": output_schema,
"description": description,
"displayName": display_name,
}
@staticmethod
def get_connector_base_spec() -> Dict[str, Any]:
return {
"openapi": "3.0.1",
"info": {
"title": "ExecuteConnection",
"description": "This tool can execute a query on connection",
"version": "4",
},
"servers": [{"url": "https://integrations.googleapis.com"}],
"security": [
{"google_auth": ["https://www.googleapis.com/auth/cloud-platform"]}
],
"paths": {},
"components": {
"schemas": {
"operation": {
"type": "string",
"default": "LIST_ENTITIES",
"description": (
"Operation to execute. Possible values are"
" LIST_ENTITIES, GET_ENTITY, CREATE_ENTITY,"
" UPDATE_ENTITY, DELETE_ENTITY in case of entities."
" EXECUTE_ACTION in case of actions. and EXECUTE_QUERY"
" in case of custom queries."
),
},
"entityId": {
"type": "string",
"description": "Name of the entity",
},
"connectorInputPayload": {"type": "object"},
"filterClause": {
"type": "string",
"default": "",
"description": "WHERE clause in SQL query",
},
"pageSize": {
"type": "integer",
"default": 50,
"description": (
"Number of entities to return in the response"
),
},
"pageToken": {
"type": "string",
"default": "",
"description": (
"Page token to return the next page of entities"
),
},
"connectionName": {
"type": "string",
"default": "",
"description": (
"Connection resource name to run the query for"
),
},
"serviceName": {
"type": "string",
"default": "",
"description": "Service directory for the connection",
},
"host": {
"type": "string",
"default": "",
"description": "Host name incase of tls service directory",
},
"entity": {
"type": "string",
"default": "Issues",
"description": "Entity to run the query for",
},
"action": {
"type": "string",
"default": "ExecuteCustomQuery",
"description": "Action to run the query for",
},
"query": {
"type": "string",
"default": "",
"description": "Custom Query to execute on the connection",
},
"dynamicAuthConfig": {
"type": "object",
"default": {},
"description": "Dynamic auth config for the connection",
},
"timeout": {
"type": "integer",
"default": 120,
"description": (
"Timeout in seconds for execution of custom query"
),
},
"connectorOutputPayload": {"type": "object"},
"nextPageToken": {"type": "string"},
"execute-connector_Response": {
"required": ["connectorOutputPayload"],
"type": "object",
"properties": {
"connectorOutputPayload": {
"$ref": (
"#/components/schemas/connectorOutputPayload"
)
},
"nextPageToken": {
"$ref": "#/components/schemas/nextPageToken"
},
},
},
},
"securitySchemes": {
"google_auth": {
"type": "oauth2",
"flows": {
"implicit": {
"authorizationUrl": (
"https://accounts.google.com/o/oauth2/auth"
),
"scopes": {
"https://www.googleapis.com/auth/cloud-platform": (
"Auth for google cloud services"
)
},
}
},
}
},
},
}
@staticmethod
def get_action_operation(
action: str,
operation: str,
action_display_name: str,
tool_name: str = "",
tool_instructions: str = "",
) -> Dict[str, Any]:
description = (
f"Use this tool with" f' action = "{action}" and'
) + f' operation = "{operation}" only. Dont ask these values from user.'
if operation == "EXECUTE_QUERY":
description = (
(f"Use this tool with" f' action = "{action}" and')
+ f' operation = "{operation}" only. Dont ask these values from user.'
" Use pageSize = 50 and timeout = 120 until user specifies a"
" different value otherwise. If user provides a query in natural"
" language, convert it to SQL query and then execute it using the"
" tool."
)
return {
"post": {
"summary": f"{action_display_name}",
"description": f"{description} {tool_instructions}",
"operationId": f"{tool_name}_{action_display_name}",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": (
f"#/components/schemas/{action_display_name}_Request"
)
}
}
}
},
"responses": {
"200": {
"description": "Success response",
"content": {
"application/json": {
"schema": {
"$ref": (
f"#/components/schemas/{action_display_name}_Response"
),
}
}
},
}
},
}
}
@staticmethod
def list_operation(
entity: str,
schema_as_string: str = "",
tool_name: str = "",
tool_instructions: str = "",
) -> Dict[str, Any]:
return {
"post": {
"summary": f"List {entity}",
"description": (
f"Returns all entities of type {entity}. Use this tool with"
+ f' entity = "{entity}" and'
+ ' operation = "LIST_ENTITIES" only. Dont ask these values'
" from"
+ ' user. Always use ""'
+ ' as filter clause and ""'
+ " as page token and 50 as page size until user specifies a"
" different value otherwise. Use single quotes for strings in"
f" filter clause. {tool_instructions}"
),
"operationId": f"{tool_name}_list_{entity}",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": (
f"#/components/schemas/list_{entity}_Request"
)
}
}
}
},
"responses": {
"200": {
"description": "Success response",
"content": {
"application/json": {
"schema": {
"description": (
f"Returns a list of {entity} of json"
f" schema: {schema_as_string}"
),
"$ref": (
"#/components/schemas/execute-connector_Response"
),
}
}
},
}
},
}
}
@staticmethod
def get_operation(
entity: str,
schema_as_string: str = "",
tool_name: str = "",
tool_instructions: str = "",
) -> Dict[str, Any]:
return {
"post": {
"summary": f"Get {entity}",
"description": (
(
f"Returns the details of the {entity}. Use this tool with"
f' entity = "{entity}" and'
)
+ ' operation = "GET_ENTITY" only. Dont ask these values from'
f" user. {tool_instructions}"
),
"operationId": f"{tool_name}_get_{entity}",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": f"#/components/schemas/get_{entity}_Request"
}
}
}
},
"responses": {
"200": {
"description": "Success response",
"content": {
"application/json": {
"schema": {
"description": (
f"Returns {entity} of json schema:"
f" {schema_as_string}"
),
"$ref": (
"#/components/schemas/execute-connector_Response"
),
}
}
},
}
},
}
}
@staticmethod
def create_operation(
entity: str, tool_name: str = "", tool_instructions: str = ""
) -> Dict[str, Any]:
return {
"post": {
"summary": f"Create {entity}",
"description": (
(
f"Creates a new entity of type {entity}. Use this tool with"
f' entity = "{entity}" and'
)
+ ' operation = "CREATE_ENTITY" only. Dont ask these values'
" from"
+ " user. Follow the schema of the entity provided in the"
f" instructions to create {entity}. {tool_instructions}"
),
"operationId": f"{tool_name}_create_{entity}",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": (
f"#/components/schemas/create_{entity}_Request"
)
}
}
}
},
"responses": {
"200": {
"description": "Success response",
"content": {
"application/json": {
"schema": {
"$ref": (
"#/components/schemas/execute-connector_Response"
)
}
}
},
}
},
}
}
@staticmethod
def update_operation(
entity: str, tool_name: str = "", tool_instructions: str = ""
) -> Dict[str, Any]:
return {
"post": {
"summary": f"Update {entity}",
"description": (
(
f"Updates an entity of type {entity}. Use this tool with"
f' entity = "{entity}" and'
)
+ ' operation = "UPDATE_ENTITY" only. Dont ask these values'
" from"
+ " user. Use entityId to uniquely identify the entity to"
" update. Follow the schema of the entity provided in the"
f" instructions to update {entity}. {tool_instructions}"
),
"operationId": f"{tool_name}_update_{entity}",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": (
f"#/components/schemas/update_{entity}_Request"
)
}
}
}
},
"responses": {
"200": {
"description": "Success response",
"content": {
"application/json": {
"schema": {
"$ref": (
"#/components/schemas/execute-connector_Response"
)
}
}
},
}
},
}
}
@staticmethod
def delete_operation(
entity: str, tool_name: str = "", tool_instructions: str = ""
) -> Dict[str, Any]:
return {
"post": {
"summary": f"Delete {entity}",
"description": (
(
f"Deletes an entity of type {entity}. Use this tool with"
f' entity = "{entity}" and'
)
+ ' operation = "DELETE_ENTITY" only. Dont ask these values'
" from"
f" user. {tool_instructions}"
),
"operationId": f"{tool_name}_delete_{entity}",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": (
f"#/components/schemas/delete_{entity}_Request"
)
}
}
}
},
"responses": {
"200": {
"description": "Success response",
"content": {
"application/json": {
"schema": {
"$ref": (
"#/components/schemas/execute-connector_Response"
)
}
}
},
}
},
}
}
@staticmethod
def create_operation_request(entity: str) -> Dict[str, Any]:
return {
"type": "object",
"required": [
"connectorInputPayload",
"operation",
"connectionName",
"serviceName",
"host",
"entity",
],
"properties": {
"connectorInputPayload": {
"$ref": f"#/components/schemas/connectorInputPayload_{entity}"
},
"operation": {"$ref": "#/components/schemas/operation"},
"connectionName": {"$ref": "#/components/schemas/connectionName"},
"serviceName": {"$ref": "#/components/schemas/serviceName"},
"host": {"$ref": "#/components/schemas/host"},
"entity": {"$ref": "#/components/schemas/entity"},
},
}
@staticmethod
def update_operation_request(entity: str) -> Dict[str, Any]:
return {
"type": "object",
"required": [
"connectorInputPayload",
"entityId",
"operation",
"connectionName",
"serviceName",
"host",
"entity",
],
"properties": {
"connectorInputPayload": {
"$ref": f"#/components/schemas/connectorInputPayload_{entity}"
},
"entityId": {"$ref": "#/components/schemas/entityId"},
"operation": {"$ref": "#/components/schemas/operation"},
"connectionName": {"$ref": "#/components/schemas/connectionName"},
"serviceName": {"$ref": "#/components/schemas/serviceName"},
"host": {"$ref": "#/components/schemas/host"},
"entity": {"$ref": "#/components/schemas/entity"},
},
}
@staticmethod
def get_operation_request() -> Dict[str, Any]:
return {
"type": "object",
"required": [
"entityId",
"operation",
"connectionName",
"serviceName",
"host",
"entity",
],
"properties": {
"entityId": {"$ref": "#/components/schemas/entityId"},
"operation": {"$ref": "#/components/schemas/operation"},
"connectionName": {"$ref": "#/components/schemas/connectionName"},
"serviceName": {"$ref": "#/components/schemas/serviceName"},
"host": {"$ref": "#/components/schemas/host"},
"entity": {"$ref": "#/components/schemas/entity"},
},
}
@staticmethod
def delete_operation_request() -> Dict[str, Any]:
return {
"type": "object",
"required": [
"entityId",
"operation",
"connectionName",
"serviceName",
"host",
"entity",
],
"properties": {
"entityId": {"$ref": "#/components/schemas/entityId"},
"operation": {"$ref": "#/components/schemas/operation"},
"connectionName": {"$ref": "#/components/schemas/connectionName"},
"serviceName": {"$ref": "#/components/schemas/serviceName"},
"host": {"$ref": "#/components/schemas/host"},
"entity": {"$ref": "#/components/schemas/entity"},
},
}
@staticmethod
def list_operation_request() -> Dict[str, Any]:
return {
"type": "object",
"required": [
"operation",
"connectionName",
"serviceName",
"host",
"entity",
],
"properties": {
"filterClause": {"$ref": "#/components/schemas/filterClause"},
"pageSize": {"$ref": "#/components/schemas/pageSize"},
"pageToken": {"$ref": "#/components/schemas/pageToken"},
"operation": {"$ref": "#/components/schemas/operation"},
"connectionName": {"$ref": "#/components/schemas/connectionName"},
"serviceName": {"$ref": "#/components/schemas/serviceName"},
"host": {"$ref": "#/components/schemas/host"},
"entity": {"$ref": "#/components/schemas/entity"},
},
}
@staticmethod
def action_request(action: str) -> Dict[str, Any]:
return {
"type": "object",
"required": [
"operation",
"connectionName",
"serviceName",
"host",
"action",
"connectorInputPayload",
],
"properties": {
"operation": {"$ref": "#/components/schemas/operation"},
"connectionName": {"$ref": "#/components/schemas/connectionName"},
"serviceName": {"$ref": "#/components/schemas/serviceName"},
"host": {"$ref": "#/components/schemas/host"},
"action": {"$ref": "#/components/schemas/action"},
"connectorInputPayload": {
"$ref": f"#/components/schemas/connectorInputPayload_{action}"
},
},
}
@staticmethod
def action_response(action: str) -> Dict[str, Any]:
return {
"type": "object",
"properties": {
"connectorOutputPayload": {
"$ref": f"#/components/schemas/connectorOutputPayload_{action}"
},
},
}
@staticmethod
def execute_custom_query_request() -> Dict[str, Any]:
return {
"type": "object",
"required": [
"operation",
"connectionName",
"serviceName",
"host",
"action",
"query",
"timeout",
"pageSize",
],
"properties": {
"operation": {"$ref": "#/components/schemas/operation"},
"connectionName": {"$ref": "#/components/schemas/connectionName"},
"serviceName": {"$ref": "#/components/schemas/serviceName"},
"host": {"$ref": "#/components/schemas/host"},
"action": {"$ref": "#/components/schemas/action"},
"query": {"$ref": "#/components/schemas/query"},
"timeout": {"$ref": "#/components/schemas/timeout"},
"pageSize": {"$ref": "#/components/schemas/pageSize"},
},
}
def connector_payload(self, json_schema: Dict[str, Any]) -> Dict[str, Any]:
return self._convert_json_schema_to_openapi_schema(json_schema)
def _convert_json_schema_to_openapi_schema(self, json_schema):
"""Converts a JSON schema dictionary to an OpenAPI schema dictionary, handling variable types, properties, items, nullable, and description.
Args:
json_schema (dict): The input JSON schema dictionary.
Returns:
dict: The converted OpenAPI schema dictionary.
"""
openapi_schema = {}
if "description" in json_schema:
openapi_schema["description"] = json_schema["description"]
if "type" in json_schema:
if isinstance(json_schema["type"], list):
if "null" in json_schema["type"]:
openapi_schema["nullable"] = True
other_types = [t for t in json_schema["type"] if t != "null"]
if other_types:
openapi_schema["type"] = other_types[0]
else:
openapi_schema["type"] = json_schema["type"][0]
else:
openapi_schema["type"] = json_schema["type"]
if openapi_schema.get("type") == "object" and "properties" in json_schema:
openapi_schema["properties"] = {}
for prop_name, prop_schema in json_schema["properties"].items():
openapi_schema["properties"][prop_name] = (
self._convert_json_schema_to_openapi_schema(prop_schema)
)
elif openapi_schema.get("type") == "array" and "items" in json_schema:
if isinstance(json_schema["items"], list):
openapi_schema["items"] = [
self._convert_json_schema_to_openapi_schema(item)
for item in json_schema["items"]
]
else:
openapi_schema["items"] = self._convert_json_schema_to_openapi_schema(
json_schema["items"]
)
return openapi_schema
def _get_access_token(self) -> str:
"""Gets the access token for the service account.
Returns:
The access token.
"""
if self.credential_cache and not self.credential_cache.expired:
return self.credential_cache.token
if self.service_account_json:
credentials = service_account.Credentials.from_service_account_info(
json.loads(self.service_account_json),
scopes=["https://www.googleapis.com/auth/cloud-platform"],
)
else:
try:
credentials, _ = default_service_credential()
except:
credentials = None
if not credentials:
raise ValueError(
"Please provide a service account that has the required permissions"
" to access the connection."
)
credentials.refresh(Request())
self.credential_cache = credentials
return credentials.token
def _execute_api_call(self, url):
"""Executes an API call to the given URL.
Args:
url (str): The URL to call.
Returns:
requests.Response: The response object from the API call.
Raises:
PermissionError: If there are credential issues.
ValueError: If there's a request error.
Exception: For any other unexpected errors.
"""
try:
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {self._get_access_token()}",
}
response = requests.get(url, headers=headers)
response.raise_for_status()
return response
except google.auth.exceptions.DefaultCredentialsError as e:
raise PermissionError(f"Credentials error: {e}") from e
except requests.exceptions.RequestException as e:
if (
"404" in str(e)
or "Not found" in str(e)
or "400" in str(e)
or "Bad request" in str(e)
):
raise ValueError(
"Invalid request. Please check the provided"
f" values of project({self.project}), location({self.location}),"
f" connection({self.connection})."
) from e
raise ValueError(f"Request error: {e}") from e
except Exception as e:
raise Exception(f"An unexpected error occurred: {e}") from e
def _poll_operation(self, operation_id: str) -> Dict[str, Any]:
"""Polls an operation until it is done.
Args:
operation_id: The ID of the operation to poll.
Returns:
The final response of the operation.
Raises:
PermissionError: If there are credential issues.
ValueError: If there's a request error.
Exception: For any other unexpected errors.
"""
operation_done: bool = False
operation_response: Dict[str, Any] = {}
while not operation_done:
get_operation_url = f"{self.connector_url}/v1/{operation_id}"
response = self._execute_api_call(get_operation_url)
operation_response = response.json()
operation_done = operation_response.get("done", False)
time.sleep(1)
return operation_response

View File

@@ -0,0 +1,253 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import json
from typing import Optional
from google.adk.tools.application_integration_tool.clients.connections_client import ConnectionsClient
import google.auth
from google.auth import default as default_service_credential
import google.auth.transport.requests
from google.auth.transport.requests import Request
from google.oauth2 import service_account
import requests
class IntegrationClient:
"""A client for interacting with Google Cloud Application Integration.
This class provides methods for retrieving OpenAPI spec for an integration or
a connection.
"""
def __init__(
self,
project: str,
location: str,
integration: Optional[str] = None,
trigger: Optional[str] = None,
connection: Optional[str] = None,
entity_operations: Optional[dict[str, list[str]]] = None,
actions: Optional[list[str]] = None,
service_account_json: Optional[str] = None,
):
"""Initializes the ApplicationIntegrationClient.
Args:
project: The Google Cloud project ID.
location: The Google Cloud location (e.g., us-central1).
integration: The integration name.
trigger: The trigger ID for the integration.
connection: The connection name.
entity_operations: A dictionary mapping entity names to a list of
operations (e.g., LIST, CREATE, UPDATE, DELETE, GET).
actions: List of actions.
service_account_json: The service account configuration as a dictionary.
Required if not using default service credential. Used for fetching
connection details.
"""
self.project = project
self.location = location
self.integration = integration
self.trigger = trigger
self.connection = connection
self.entity_operations = (
entity_operations if entity_operations is not None else {}
)
self.actions = actions if actions is not None else []
self.service_account_json = service_account_json
self.credential_cache = None
def get_openapi_spec_for_integration(self):
"""Gets the OpenAPI spec for the integration.
Returns:
dict: The OpenAPI spec as a dictionary.
Raises:
PermissionError: If there are credential issues.
ValueError: If there's a request error or processing error.
Exception: For any other unexpected errors.
"""
try:
url = f"https://{self.location}-integrations.googleapis.com/v1/projects/{self.project}/locations/{self.location}:generateOpenApiSpec"
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {self._get_access_token()}",
}
data = {
"apiTriggerResources": [
{
"integrationResource": self.integration,
"triggerId": [self.trigger],
},
],
"fileFormat": "JSON",
}
response = requests.post(url, headers=headers, json=data)
response.raise_for_status()
spec = response.json().get("openApiSpec", {})
return json.loads(spec)
except google.auth.exceptions.DefaultCredentialsError as e:
raise PermissionError(f"Credentials error: {e}") from e
except requests.exceptions.RequestException as e:
if (
"404" in str(e)
or "Not found" in str(e)
or "400" in str(e)
or "Bad request" in str(e)
):
raise ValueError(
"Invalid request. Please check the provided values of"
f" project({self.project}), location({self.location}),"
f" integration({self.integration}) and trigger({self.trigger})."
) from e
raise ValueError(f"Request error: {e}") from e
except Exception as e:
raise Exception(f"An unexpected error occurred: {e}") from e
def get_openapi_spec_for_connection(self, tool_name="", tool_instructions=""):
"""Gets the OpenAPI spec for the connection.
Returns:
dict: The OpenAPI spec as a dictionary.
Raises:
ValueError: If there's an error retrieving the OpenAPI spec.
PermissionError: If there are credential issues.
Exception: For any other unexpected errors.
"""
# Application Integration needs to be provisioned in the same region as connection and an integration with name "ExecuteConnection" and trigger "api_trigger/ExecuteConnection" should be created as per the documentation.
integration_name = "ExecuteConnection"
connections_client = ConnectionsClient(
self.project,
self.location,
self.connection,
self.service_account_json,
)
if not self.entity_operations and not self.actions:
raise ValueError(
"No entity operations or actions provided. Please provide at least"
" one of them."
)
connector_spec = connections_client.get_connector_base_spec()
for entity, operations in self.entity_operations.items():
schema, supported_operations = (
connections_client.get_entity_schema_and_operations(entity)
)
if not operations:
operations = supported_operations
json_schema_as_string = json.dumps(schema)
entity_lower = entity
connector_spec["components"]["schemas"][
f"connectorInputPayload_{entity_lower}"
] = connections_client.connector_payload(schema)
for operation in operations:
operation_lower = operation.lower()
path = f"/v2/projects/{self.project}/locations/{self.location}/integrations/{integration_name}:execute?triggerId=api_trigger/{integration_name}#{operation_lower}_{entity_lower}"
if operation_lower == "create":
connector_spec["paths"][path] = connections_client.create_operation(
entity_lower, tool_name, tool_instructions
)
connector_spec["components"]["schemas"][
f"create_{entity_lower}_Request"
] = connections_client.create_operation_request(entity_lower)
elif operation_lower == "update":
connector_spec["paths"][path] = connections_client.update_operation(
entity_lower, tool_name, tool_instructions
)
connector_spec["components"]["schemas"][
f"update_{entity_lower}_Request"
] = connections_client.update_operation_request(entity_lower)
elif operation_lower == "delete":
connector_spec["paths"][path] = connections_client.delete_operation(
entity_lower, tool_name, tool_instructions
)
connector_spec["components"]["schemas"][
f"delete_{entity_lower}_Request"
] = connections_client.delete_operation_request()
elif operation_lower == "list":
connector_spec["paths"][path] = connections_client.list_operation(
entity_lower, json_schema_as_string, tool_name, tool_instructions
)
connector_spec["components"]["schemas"][
f"list_{entity_lower}_Request"
] = connections_client.list_operation_request()
elif operation_lower == "get":
connector_spec["paths"][path] = connections_client.get_operation(
entity_lower, json_schema_as_string, tool_name, tool_instructions
)
connector_spec["components"]["schemas"][
f"get_{entity_lower}_Request"
] = connections_client.get_operation_request()
else:
raise ValueError(
f"Invalid operation: {operation} for entity: {entity}"
)
for action in self.actions:
action_details = connections_client.get_action_schema(action)
input_schema = action_details["inputSchema"]
output_schema = action_details["outputSchema"]
action_display_name = action_details["displayName"]
operation = "EXECUTE_ACTION"
if action == "ExecuteCustomQuery":
connector_spec["components"]["schemas"][
f"{action}_Request"
] = connections_client.execute_custom_query_request()
operation = "EXECUTE_QUERY"
else:
connector_spec["components"]["schemas"][
f"{action_display_name}_Request"
] = connections_client.action_request(action_display_name)
connector_spec["components"]["schemas"][
f"connectorInputPayload_{action_display_name}"
] = connections_client.connector_payload(input_schema)
connector_spec["components"]["schemas"][
f"connectorOutputPayload_{action_display_name}"
] = connections_client.connector_payload(output_schema)
connector_spec["components"]["schemas"][
f"{action_display_name}_Response"
] = connections_client.action_response(action_display_name)
path = f"/v2/projects/{self.project}/locations/{self.location}/integrations/{integration_name}:execute?triggerId=api_trigger/{integration_name}#{action}"
connector_spec["paths"][path] = connections_client.get_action_operation(
action, operation, action_display_name, tool_name, tool_instructions
)
return connector_spec
def _get_access_token(self) -> str:
"""Gets the access token for the service account or using default credentials.
Returns:
The access token.
"""
if self.credential_cache and not self.credential_cache.expired:
return self.credential_cache.token
if self.service_account_json:
credentials = service_account.Credentials.from_service_account_info(
json.loads(self.service_account_json),
scopes=["https://www.googleapis.com/auth/cloud-platform"],
)
else:
try:
credentials, _ = default_service_credential()
except:
credentials = None
if not credentials:
raise ValueError(
"Please provide a service account that has the required permissions"
" to access the connection."
)
credentials.refresh(Request())
self.credential_cache = credentials
return credentials.token