google-adk 1.2.1__py3-none-any.whl → 1.4.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. google/adk/a2a/__init__.py +13 -0
  2. google/adk/a2a/converters/__init__.py +13 -0
  3. google/adk/a2a/converters/part_converter.py +177 -0
  4. google/adk/agents/invocation_context.py +2 -0
  5. google/adk/agents/llm_agent.py +1 -6
  6. google/adk/agents/run_config.py +11 -0
  7. google/adk/auth/auth_credential.py +4 -0
  8. google/adk/auth/auth_handler.py +22 -96
  9. google/adk/auth/auth_preprocessor.py +3 -3
  10. google/adk/auth/auth_tool.py +46 -0
  11. google/adk/auth/credential_manager.py +261 -0
  12. google/adk/auth/credential_service/__init__.py +13 -0
  13. google/adk/auth/credential_service/base_credential_service.py +75 -0
  14. google/adk/auth/credential_service/in_memory_credential_service.py +64 -0
  15. google/adk/auth/exchanger/__init__.py +21 -0
  16. google/adk/auth/exchanger/base_credential_exchanger.py +57 -0
  17. google/adk/auth/exchanger/credential_exchanger_registry.py +58 -0
  18. google/adk/auth/exchanger/oauth2_credential_exchanger.py +104 -0
  19. google/adk/auth/oauth2_credential_util.py +107 -0
  20. google/adk/auth/refresher/__init__.py +21 -0
  21. google/adk/auth/refresher/base_credential_refresher.py +74 -0
  22. google/adk/auth/refresher/credential_refresher_registry.py +59 -0
  23. google/adk/auth/refresher/oauth2_credential_refresher.py +126 -0
  24. google/adk/cli/agent_graph.py +34 -32
  25. google/adk/cli/browser/index.html +2 -2
  26. google/adk/cli/browser/main-JAAWEV7F.js +92 -0
  27. google/adk/cli/browser/polyfills-B6TNHZQ6.js +17 -0
  28. google/adk/cli/cli.py +10 -0
  29. google/adk/cli/cli_deploy.py +80 -21
  30. google/adk/cli/cli_tools_click.py +132 -61
  31. google/adk/cli/fast_api.py +46 -41
  32. google/adk/cli/utils/agent_loader.py +15 -2
  33. google/adk/code_executors/container_code_executor.py +10 -6
  34. google/adk/code_executors/vertex_ai_code_executor.py +8 -2
  35. google/adk/evaluation/_eval_set_results_manager_utils.py +44 -0
  36. google/adk/evaluation/_eval_sets_manager_utils.py +108 -0
  37. google/adk/evaluation/eval_metrics.py +0 -5
  38. google/adk/evaluation/eval_result.py +12 -7
  39. google/adk/evaluation/eval_set_results_manager.py +6 -1
  40. google/adk/evaluation/gcs_eval_set_results_manager.py +121 -0
  41. google/adk/evaluation/gcs_eval_sets_manager.py +196 -0
  42. google/adk/evaluation/local_eval_set_results_manager.py +6 -18
  43. google/adk/evaluation/local_eval_sets_manager.py +27 -78
  44. google/adk/flows/llm_flows/basic.py +9 -0
  45. google/adk/flows/llm_flows/functions.py +1 -2
  46. google/adk/models/anthropic_llm.py +1 -1
  47. google/adk/models/gemini_llm_connection.py +2 -0
  48. google/adk/models/google_llm.py +57 -16
  49. google/adk/models/lite_llm.py +2 -1
  50. google/adk/platform/__init__.py +13 -0
  51. google/adk/platform/internal/__init__.py +15 -0
  52. google/adk/platform/internal/thread.py +30 -0
  53. google/adk/platform/thread.py +31 -0
  54. google/adk/runners.py +8 -2
  55. google/adk/sessions/in_memory_session_service.py +12 -1
  56. google/adk/sessions/vertex_ai_session_service.py +71 -50
  57. google/adk/tools/__init__.py +2 -0
  58. google/adk/tools/_automatic_function_calling_util.py +1 -0
  59. google/adk/tools/_forwarding_artifact_service.py +96 -0
  60. google/adk/tools/_function_parameter_parse_util.py +1 -0
  61. google/adk/tools/agent_tool.py +5 -39
  62. google/adk/tools/application_integration_tool/integration_connector_tool.py +2 -2
  63. google/adk/tools/authenticated_function_tool.py +107 -0
  64. google/adk/tools/base_authenticated_tool.py +107 -0
  65. google/adk/tools/bigquery/bigquery_credentials.py +6 -4
  66. google/adk/tools/bigquery/bigquery_tool.py +22 -9
  67. google/adk/tools/bigquery/bigquery_toolset.py +9 -3
  68. google/adk/tools/bigquery/client.py +7 -3
  69. google/adk/tools/bigquery/config.py +46 -0
  70. google/adk/tools/bigquery/metadata_tool.py +114 -91
  71. google/adk/tools/bigquery/query_tool.py +141 -23
  72. google/adk/tools/google_api_tool/googleapi_to_openapi_converter.py +7 -4
  73. google/adk/tools/google_search_tool.py +0 -1
  74. google/adk/tools/mcp_tool/__init__.py +6 -0
  75. google/adk/tools/mcp_tool/mcp_session_manager.py +271 -149
  76. google/adk/tools/mcp_tool/mcp_tool.py +73 -22
  77. google/adk/tools/mcp_tool/mcp_toolset.py +32 -29
  78. google/adk/tools/openapi_tool/openapi_spec_parser/rest_api_tool.py +3 -3
  79. google/adk/tools/openapi_tool/openapi_spec_parser/tool_auth_handler.py +55 -33
  80. google/adk/tools/retrieval/files_retrieval.py +7 -1
  81. google/adk/tools/url_context_tool.py +61 -0
  82. google/adk/tools/vertex_ai_search_tool.py +13 -2
  83. google/adk/utils/feature_decorator.py +175 -0
  84. google/adk/version.py +1 -1
  85. {google_adk-1.2.1.dist-info → google_adk-1.4.1.dist-info}/METADATA +10 -2
  86. {google_adk-1.2.1.dist-info → google_adk-1.4.1.dist-info}/RECORD +89 -59
  87. google/adk/cli/browser/main-CS5OLUMF.js +0 -91
  88. google/adk/cli/browser/polyfills-FFHMD2TL.js +0 -17
  89. {google_adk-1.2.1.dist-info → google_adk-1.4.1.dist-info}/WHEEL +0 -0
  90. {google_adk-1.2.1.dist-info → google_adk-1.4.1.dist-info}/entry_points.txt +0 -0
  91. {google_adk-1.2.1.dist-info → google_adk-1.4.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,261 @@
1
+ # Copyright 2025 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from __future__ import annotations
16
+
17
+ from typing import Optional
18
+
19
+ from ..tools.tool_context import ToolContext
20
+ from ..utils.feature_decorator import experimental
21
+ from .auth_credential import AuthCredential
22
+ from .auth_credential import AuthCredentialTypes
23
+ from .auth_schemes import AuthSchemeType
24
+ from .auth_tool import AuthConfig
25
+ from .exchanger.base_credential_exchanger import BaseCredentialExchanger
26
+ from .exchanger.credential_exchanger_registry import CredentialExchangerRegistry
27
+ from .refresher.base_credential_refresher import BaseCredentialRefresher
28
+ from .refresher.credential_refresher_registry import CredentialRefresherRegistry
29
+
30
+
31
+ @experimental
32
+ class CredentialManager:
33
+ """Manages authentication credentials through a structured workflow.
34
+
35
+ The CredentialManager orchestrates the complete lifecycle of authentication
36
+ credentials, from initial loading to final preparation for use. It provides
37
+ a centralized interface for handling various credential types and authentication
38
+ schemes while maintaining proper credential hygiene (refresh, exchange, caching).
39
+
40
+ This class is only for use by Agent Development Kit.
41
+
42
+ Args:
43
+ auth_config: Configuration containing authentication scheme and credentials
44
+
45
+ Example:
46
+ ```python
47
+ auth_config = AuthConfig(
48
+ auth_scheme=oauth2_scheme,
49
+ raw_auth_credential=service_account_credential
50
+ )
51
+ manager = CredentialManager(auth_config)
52
+
53
+ # Register custom exchanger if needed
54
+ manager.register_credential_exchanger(
55
+ AuthCredentialTypes.CUSTOM_TYPE,
56
+ CustomCredentialExchanger()
57
+ )
58
+
59
+ # Register custom refresher if needed
60
+ manager.register_credential_refresher(
61
+ AuthCredentialTypes.CUSTOM_TYPE,
62
+ CustomCredentialRefresher()
63
+ )
64
+
65
+ # Load and prepare credential
66
+ credential = await manager.load_auth_credential(tool_context)
67
+ ```
68
+ """
69
+
70
+ def __init__(
71
+ self,
72
+ auth_config: AuthConfig,
73
+ ):
74
+ self._auth_config = auth_config
75
+ self._exchanger_registry = CredentialExchangerRegistry()
76
+ self._refresher_registry = CredentialRefresherRegistry()
77
+
78
+ # Register default exchangers and refreshers
79
+ # TODO: support service account credential exchanger
80
+ from .refresher.oauth2_credential_refresher import OAuth2CredentialRefresher
81
+
82
+ oauth2_refresher = OAuth2CredentialRefresher()
83
+ self._refresher_registry.register(
84
+ AuthCredentialTypes.OAUTH2, oauth2_refresher
85
+ )
86
+ self._refresher_registry.register(
87
+ AuthCredentialTypes.OPEN_ID_CONNECT, oauth2_refresher
88
+ )
89
+
90
+ def register_credential_exchanger(
91
+ self,
92
+ credential_type: AuthCredentialTypes,
93
+ exchanger_instance: BaseCredentialExchanger,
94
+ ) -> None:
95
+ """Register a credential exchanger for a credential type.
96
+
97
+ Args:
98
+ credential_type: The credential type to register for.
99
+ exchanger_instance: The exchanger instance to register.
100
+ """
101
+ self._exchanger_registry.register(credential_type, exchanger_instance)
102
+
103
+ async def request_credential(self, tool_context: ToolContext) -> None:
104
+ tool_context.request_credential(self._auth_config)
105
+
106
+ async def get_auth_credential(
107
+ self, tool_context: ToolContext
108
+ ) -> Optional[AuthCredential]:
109
+ """Load and prepare authentication credential through a structured workflow."""
110
+
111
+ # Step 1: Validate credential configuration
112
+ await self._validate_credential()
113
+
114
+ # Step 2: Check if credential is already ready (no processing needed)
115
+ if self._is_credential_ready():
116
+ return self._auth_config.raw_auth_credential
117
+
118
+ # Step 3: Try to load existing processed credential
119
+ credential = await self._load_existing_credential(tool_context)
120
+
121
+ # Step 4: If no existing credential, load from auth response
122
+ # TODO instead of load from auth response, we can store auth response in
123
+ # credential service.
124
+ was_from_auth_response = False
125
+ if not credential:
126
+ credential = await self._load_from_auth_response(tool_context)
127
+ was_from_auth_response = True
128
+
129
+ # Step 5: If still no credential available, return None
130
+ if not credential:
131
+ return None
132
+
133
+ # Step 6: Exchange credential if needed (e.g., service account to access token)
134
+ credential, was_exchanged = await self._exchange_credential(credential)
135
+
136
+ # Step 7: Refresh credential if expired
137
+ if not was_exchanged:
138
+ credential, was_refreshed = await self._refresh_credential(credential)
139
+
140
+ # Step 8: Save credential if it was modified
141
+ if was_from_auth_response or was_exchanged or was_refreshed:
142
+ await self._save_credential(tool_context, credential)
143
+
144
+ return credential
145
+
146
+ async def _load_existing_credential(
147
+ self, tool_context: ToolContext
148
+ ) -> Optional[AuthCredential]:
149
+ """Load existing credential from credential service or cached exchanged credential."""
150
+
151
+ # Try loading from credential service first
152
+ credential = await self._load_from_credential_service(tool_context)
153
+ if credential:
154
+ return credential
155
+
156
+ # Check if we have a cached exchanged credential
157
+ if self._auth_config.exchanged_auth_credential:
158
+ return self._auth_config.exchanged_auth_credential
159
+
160
+ return None
161
+
162
+ async def _load_from_credential_service(
163
+ self, tool_context: ToolContext
164
+ ) -> Optional[AuthCredential]:
165
+ """Load credential from credential service if available."""
166
+ credential_service = tool_context._invocation_context.credential_service
167
+ if credential_service:
168
+ # Note: This should be made async in a future refactor
169
+ # For now, assuming synchronous operation
170
+ return await credential_service.load_credential(
171
+ self._auth_config, tool_context
172
+ )
173
+ return None
174
+
175
+ async def _load_from_auth_response(
176
+ self, tool_context: ToolContext
177
+ ) -> Optional[AuthCredential]:
178
+ """Load credential from auth response in tool context."""
179
+ return tool_context.get_auth_response(self._auth_config)
180
+
181
+ async def _exchange_credential(
182
+ self, credential: AuthCredential
183
+ ) -> tuple[AuthCredential, bool]:
184
+ """Exchange credential if needed and return the credential and whether it was exchanged."""
185
+ exchanger = self._exchanger_registry.get_exchanger(credential.auth_type)
186
+ if not exchanger:
187
+ return credential, False
188
+
189
+ exchanged_credential = await exchanger.exchange(
190
+ credential, self._auth_config.auth_scheme
191
+ )
192
+ return exchanged_credential, True
193
+
194
+ async def _refresh_credential(
195
+ self, credential: AuthCredential
196
+ ) -> tuple[AuthCredential, bool]:
197
+ """Refresh credential if expired and return the credential and whether it was refreshed."""
198
+ refresher = self._refresher_registry.get_refresher(credential.auth_type)
199
+ if not refresher:
200
+ return credential, False
201
+
202
+ if await refresher.is_refresh_needed(
203
+ credential, self._auth_config.auth_scheme
204
+ ):
205
+ refreshed_credential = await refresher.refresh(
206
+ credential, self._auth_config.auth_scheme
207
+ )
208
+ return refreshed_credential, True
209
+
210
+ return credential, False
211
+
212
+ def _is_credential_ready(self) -> bool:
213
+ """Check if credential is ready to use without further processing."""
214
+ raw_credential = self._auth_config.raw_auth_credential
215
+ if not raw_credential:
216
+ return False
217
+
218
+ # Simple credentials that don't need exchange or refresh
219
+ return raw_credential.auth_type in (
220
+ AuthCredentialTypes.API_KEY,
221
+ AuthCredentialTypes.HTTP,
222
+ # Add other simple auth types as needed
223
+ )
224
+
225
+ async def _validate_credential(self) -> None:
226
+ """Validate credential configuration and raise errors if invalid."""
227
+ if not self._auth_config.raw_auth_credential:
228
+ if self._auth_config.auth_scheme.type_ in (
229
+ AuthSchemeType.oauth2,
230
+ AuthSchemeType.openIdConnect,
231
+ ):
232
+ raise ValueError(
233
+ "raw_auth_credential is required for auth_scheme type "
234
+ f"{self._auth_config.auth_scheme.type_}"
235
+ )
236
+
237
+ raw_credential = self._auth_config.raw_auth_credential
238
+ if raw_credential:
239
+ if (
240
+ raw_credential.auth_type
241
+ in (
242
+ AuthCredentialTypes.OAUTH2,
243
+ AuthCredentialTypes.OPEN_ID_CONNECT,
244
+ )
245
+ and not raw_credential.oauth2
246
+ ):
247
+ raise ValueError(
248
+ "auth_config.raw_credential.oauth2 required for credential type "
249
+ f"{raw_credential.auth_type}"
250
+ )
251
+ # Additional validation can be added here
252
+
253
+ async def _save_credential(
254
+ self, tool_context: ToolContext, credential: AuthCredential
255
+ ) -> None:
256
+ """Save credential to credential service if available."""
257
+ credential_service = tool_context._invocation_context.credential_service
258
+ if credential_service:
259
+ # Update the exchanged credential in config
260
+ self._auth_config.exchanged_auth_credential = credential
261
+ await credential_service.save_credential(self._auth_config, tool_context)
@@ -0,0 +1,13 @@
1
+ # Copyright 2025 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
@@ -0,0 +1,75 @@
1
+ # Copyright 2025 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from __future__ import annotations
16
+
17
+ from abc import ABC
18
+ from abc import abstractmethod
19
+ from typing import Optional
20
+
21
+ from ...tools.tool_context import ToolContext
22
+ from ...utils.feature_decorator import experimental
23
+ from ..auth_credential import AuthCredential
24
+ from ..auth_tool import AuthConfig
25
+
26
+
27
+ @experimental
28
+ class BaseCredentialService(ABC):
29
+ """Abstract class for Service that loads / saves tool credentials from / to
30
+ the backend credential store."""
31
+
32
+ @abstractmethod
33
+ async def load_credential(
34
+ self,
35
+ auth_config: AuthConfig,
36
+ tool_context: ToolContext,
37
+ ) -> Optional[AuthCredential]:
38
+ """
39
+ Loads the credential by auth config and current tool context from the
40
+ backend credential store.
41
+
42
+ Args:
43
+ auth_config: The auth config which contains the auth scheme and auth
44
+ credential information. auth_config.get_credential_key will be used to
45
+ build the key to load the credential.
46
+
47
+ tool_context: The context of the current invocation when the tool is
48
+ trying to load the credential.
49
+
50
+ Returns:
51
+ Optional[AuthCredential]: the credential saved in the store.
52
+
53
+ """
54
+
55
+ @abstractmethod
56
+ async def save_credential(
57
+ self,
58
+ auth_config: AuthConfig,
59
+ tool_context: ToolContext,
60
+ ) -> None:
61
+ """
62
+ Saves the exchanged_auth_credential in auth config to the backend credential
63
+ store.
64
+
65
+ Args:
66
+ auth_config: The auth config which contains the auth scheme and auth
67
+ credential information. auth_config.get_credential_key will be used to
68
+ build the key to save the credential.
69
+
70
+ tool_context: The context of the current invocation when the tool is
71
+ trying to save the credential.
72
+
73
+ Returns:
74
+ None
75
+ """
@@ -0,0 +1,64 @@
1
+ # Copyright 2025 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from __future__ import annotations
16
+
17
+ from typing import Optional
18
+
19
+ from typing_extensions import override
20
+
21
+ from ...tools.tool_context import ToolContext
22
+ from ...utils.feature_decorator import experimental
23
+ from ..auth_credential import AuthCredential
24
+ from ..auth_tool import AuthConfig
25
+ from .base_credential_service import BaseCredentialService
26
+
27
+
28
+ @experimental
29
+ class InMemoryCredentialService(BaseCredentialService):
30
+ """Class for in memory implementation of credential service(Experimental)"""
31
+
32
+ def __init__(self):
33
+ super().__init__()
34
+ self._credentials = {}
35
+
36
+ @override
37
+ async def load_credential(
38
+ self,
39
+ auth_config: AuthConfig,
40
+ tool_context: ToolContext,
41
+ ) -> Optional[AuthCredential]:
42
+ credential_bucket = self._get_bucket_for_current_context(tool_context)
43
+ return credential_bucket.get(auth_config.credential_key)
44
+
45
+ @override
46
+ async def save_credential(
47
+ self,
48
+ auth_config: AuthConfig,
49
+ tool_context: ToolContext,
50
+ ) -> None:
51
+ credential_bucket = self._get_bucket_for_current_context(tool_context)
52
+ credential_bucket[auth_config.credential_key] = (
53
+ auth_config.exchanged_auth_credential
54
+ )
55
+
56
+ def _get_bucket_for_current_context(self, tool_context: ToolContext) -> str:
57
+ app_name = tool_context._invocation_context.app_name
58
+ user_id = tool_context._invocation_context.user_id
59
+
60
+ if app_name not in self._credentials:
61
+ self._credentials[app_name] = {}
62
+ if user_id not in self._credentials[app_name]:
63
+ self._credentials[app_name][user_id] = {}
64
+ return self._credentials[app_name][user_id]
@@ -0,0 +1,21 @@
1
+ # Copyright 2025 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Credential exchanger module."""
16
+
17
+ from .base_credential_exchanger import BaseCredentialExchanger
18
+
19
+ __all__ = [
20
+ "BaseCredentialExchanger",
21
+ ]
@@ -0,0 +1,57 @@
1
+ # Copyright 2025 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Base credential exchanger interface."""
16
+
17
+ from __future__ import annotations
18
+
19
+ import abc
20
+ from typing import Optional
21
+
22
+ from ...utils.feature_decorator import experimental
23
+ from ..auth_credential import AuthCredential
24
+ from ..auth_schemes import AuthScheme
25
+
26
+
27
+ class CredentialExchangError(Exception):
28
+ """Base exception for credential exchange errors."""
29
+
30
+
31
+ @experimental
32
+ class BaseCredentialExchanger(abc.ABC):
33
+ """Base interface for credential exchangers.
34
+
35
+ Credential exchangers are responsible for exchanging credentials from
36
+ one format or scheme to another.
37
+ """
38
+
39
+ @abc.abstractmethod
40
+ async def exchange(
41
+ self,
42
+ auth_credential: AuthCredential,
43
+ auth_scheme: Optional[AuthScheme] = None,
44
+ ) -> AuthCredential:
45
+ """Exchange credential if needed.
46
+
47
+ Args:
48
+ auth_credential: The credential to exchange.
49
+ auth_scheme: The authentication scheme (optional, some exchangers don't need it).
50
+
51
+ Returns:
52
+ The exchanged credential.
53
+
54
+ Raises:
55
+ CredentialExchangError: If credential exchange fails.
56
+ """
57
+ pass
@@ -0,0 +1,58 @@
1
+ # Copyright 2025 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Credential exchanger registry."""
16
+
17
+ from __future__ import annotations
18
+
19
+ from typing import Dict
20
+ from typing import Optional
21
+
22
+ from ...utils.feature_decorator import experimental
23
+ from ..auth_credential import AuthCredentialTypes
24
+ from .base_credential_exchanger import BaseCredentialExchanger
25
+
26
+
27
+ @experimental
28
+ class CredentialExchangerRegistry:
29
+ """Registry for credential exchanger instances."""
30
+
31
+ def __init__(self):
32
+ self._exchangers: Dict[AuthCredentialTypes, BaseCredentialExchanger] = {}
33
+
34
+ def register(
35
+ self,
36
+ credential_type: AuthCredentialTypes,
37
+ exchanger_instance: BaseCredentialExchanger,
38
+ ) -> None:
39
+ """Register an exchanger instance for a credential type.
40
+
41
+ Args:
42
+ credential_type: The credential type to register for.
43
+ exchanger_instance: The exchanger instance to register.
44
+ """
45
+ self._exchangers[credential_type] = exchanger_instance
46
+
47
+ def get_exchanger(
48
+ self, credential_type: AuthCredentialTypes
49
+ ) -> Optional[BaseCredentialExchanger]:
50
+ """Get the exchanger instance for a credential type.
51
+
52
+ Args:
53
+ credential_type: The credential type to get exchanger for.
54
+
55
+ Returns:
56
+ The exchanger instance if registered, None otherwise.
57
+ """
58
+ return self._exchangers.get(credential_type)
@@ -0,0 +1,104 @@
1
+ # Copyright 2025 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """OAuth2 credential exchanger implementation."""
16
+
17
+ from __future__ import annotations
18
+
19
+ import logging
20
+ from typing import Optional
21
+
22
+ from google.adk.auth.auth_credential import AuthCredential
23
+ from google.adk.auth.auth_schemes import AuthScheme
24
+ from google.adk.auth.auth_schemes import OAuthGrantType
25
+ from google.adk.auth.oauth2_credential_util import create_oauth2_session
26
+ from google.adk.auth.oauth2_credential_util import update_credential_with_tokens
27
+ from google.adk.utils.feature_decorator import experimental
28
+ from typing_extensions import override
29
+
30
+ from .base_credential_exchanger import BaseCredentialExchanger
31
+ from .base_credential_exchanger import CredentialExchangError
32
+
33
+ try:
34
+ from authlib.integrations.requests_client import OAuth2Session
35
+
36
+ AUTHLIB_AVIALABLE = True
37
+ except ImportError:
38
+ AUTHLIB_AVIALABLE = False
39
+
40
+ logger = logging.getLogger("google_adk." + __name__)
41
+
42
+
43
+ @experimental
44
+ class OAuth2CredentialExchanger(BaseCredentialExchanger):
45
+ """Exchanges OAuth2 credentials from authorization responses."""
46
+
47
+ @override
48
+ async def exchange(
49
+ self,
50
+ auth_credential: AuthCredential,
51
+ auth_scheme: Optional[AuthScheme] = None,
52
+ ) -> AuthCredential:
53
+ """Exchange OAuth2 credential from authorization response.
54
+ if credential exchange failed, the original credential will be returned.
55
+
56
+ Args:
57
+ auth_credential: The OAuth2 credential to exchange.
58
+ auth_scheme: The OAuth2 authentication scheme.
59
+
60
+ Returns:
61
+ The exchanged credential with access token.
62
+
63
+ Raises:
64
+ CredentialExchangError: If auth_scheme is missing.
65
+ """
66
+ if not auth_scheme:
67
+ raise CredentialExchangError(
68
+ "auth_scheme is required for OAuth2 credential exchange"
69
+ )
70
+
71
+ if not AUTHLIB_AVIALABLE:
72
+ # If authlib is not available, we cannot exchange the credential.
73
+ # We return the original credential without exchange.
74
+ # The client using this tool can decide to exchange the credential
75
+ # themselves using other lib.
76
+ logger.warning(
77
+ "authlib is not available, skipping OAuth2 credential exchange."
78
+ )
79
+ return auth_credential
80
+
81
+ if auth_credential.oauth2 and auth_credential.oauth2.access_token:
82
+ return auth_credential
83
+
84
+ client, token_endpoint = create_oauth2_session(auth_scheme, auth_credential)
85
+ if not client:
86
+ logger.warning("Could not create OAuth2 session for token exchange")
87
+ return auth_credential
88
+
89
+ try:
90
+ tokens = client.fetch_token(
91
+ token_endpoint,
92
+ authorization_response=auth_credential.oauth2.auth_response_uri,
93
+ code=auth_credential.oauth2.auth_code,
94
+ grant_type=OAuthGrantType.AUTHORIZATION_CODE,
95
+ )
96
+ update_credential_with_tokens(auth_credential, tokens)
97
+ logger.debug("Successfully exchanged OAuth2 tokens")
98
+ except Exception as e:
99
+ # TODO reconsider whether we should raise errors in this case
100
+ logger.error("Failed to exchange OAuth2 tokens: %s", e)
101
+ # Return original credential on failure
102
+ return auth_credential
103
+
104
+ return auth_credential