google-adk-extras 0.1.1__py3-none-any.whl → 0.2.5__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.
- google_adk_extras/__init__.py +31 -1
- google_adk_extras/adk_builder.py +1030 -0
- google_adk_extras/artifacts/__init__.py +25 -12
- google_adk_extras/artifacts/base_custom_artifact_service.py +148 -11
- google_adk_extras/artifacts/local_folder_artifact_service.py +133 -13
- google_adk_extras/artifacts/s3_artifact_service.py +135 -19
- google_adk_extras/artifacts/sql_artifact_service.py +109 -10
- google_adk_extras/credentials/__init__.py +34 -0
- google_adk_extras/credentials/base_custom_credential_service.py +113 -0
- google_adk_extras/credentials/github_oauth2_credential_service.py +213 -0
- google_adk_extras/credentials/google_oauth2_credential_service.py +216 -0
- google_adk_extras/credentials/http_basic_auth_credential_service.py +388 -0
- google_adk_extras/credentials/jwt_credential_service.py +345 -0
- google_adk_extras/credentials/microsoft_oauth2_credential_service.py +250 -0
- google_adk_extras/credentials/x_oauth2_credential_service.py +240 -0
- google_adk_extras/custom_agent_loader.py +170 -0
- google_adk_extras/enhanced_adk_web_server.py +137 -0
- google_adk_extras/enhanced_fastapi.py +507 -0
- google_adk_extras/enhanced_runner.py +38 -0
- google_adk_extras/memory/__init__.py +30 -13
- google_adk_extras/memory/base_custom_memory_service.py +37 -5
- google_adk_extras/memory/sql_memory_service.py +105 -19
- google_adk_extras/memory/yaml_file_memory_service.py +115 -22
- google_adk_extras/sessions/__init__.py +29 -13
- google_adk_extras/sessions/base_custom_session_service.py +133 -11
- google_adk_extras/sessions/sql_session_service.py +127 -16
- google_adk_extras/sessions/yaml_file_session_service.py +122 -14
- google_adk_extras-0.2.5.dist-info/METADATA +302 -0
- google_adk_extras-0.2.5.dist-info/RECORD +37 -0
- google_adk_extras/py.typed +0 -0
- google_adk_extras-0.1.1.dist-info/METADATA +0 -175
- google_adk_extras-0.1.1.dist-info/RECORD +0 -25
- {google_adk_extras-0.1.1.dist-info → google_adk_extras-0.2.5.dist-info}/WHEEL +0 -0
- {google_adk_extras-0.1.1.dist-info → google_adk_extras-0.2.5.dist-info}/licenses/LICENSE +0 -0
- {google_adk_extras-0.1.1.dist-info → google_adk_extras-0.2.5.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1030 @@
|
|
1
|
+
"""AdkBuilder - Enhanced builder for Google ADK with credential service support.
|
2
|
+
|
3
|
+
This module provides the AdkBuilder class that extends Google ADK's FastAPI integration
|
4
|
+
with support for custom credential services and enhanced configuration options.
|
5
|
+
"""
|
6
|
+
|
7
|
+
import os
|
8
|
+
import logging
|
9
|
+
from pathlib import Path
|
10
|
+
from typing import Any, Dict, List, Mapping, Optional, Union, Callable
|
11
|
+
from starlette.types import Lifespan
|
12
|
+
|
13
|
+
from fastapi import FastAPI
|
14
|
+
from google.adk.runners import Runner
|
15
|
+
from google.adk.agents.base_agent import BaseAgent
|
16
|
+
from google.adk.sessions.base_session_service import BaseSessionService
|
17
|
+
from google.adk.sessions.in_memory_session_service import InMemorySessionService
|
18
|
+
from google.adk.sessions.database_session_service import DatabaseSessionService
|
19
|
+
from google.adk.artifacts.base_artifact_service import BaseArtifactService
|
20
|
+
from google.adk.artifacts.in_memory_artifact_service import InMemoryArtifactService
|
21
|
+
# GCS removed - vendor specific
|
22
|
+
from google.adk.memory.base_memory_service import BaseMemoryService
|
23
|
+
from google.adk.memory.in_memory_memory_service import InMemoryMemoryService
|
24
|
+
from google.adk.auth.credential_service.base_credential_service import BaseCredentialService
|
25
|
+
from google.adk.auth.credential_service.in_memory_credential_service import InMemoryCredentialService
|
26
|
+
from google.adk.cli.utils.agent_loader import AgentLoader
|
27
|
+
from google.adk.cli.utils.base_agent_loader import BaseAgentLoader
|
28
|
+
from google.adk.cli.adk_web_server import AdkWebServer
|
29
|
+
|
30
|
+
from .custom_agent_loader import CustomAgentLoader
|
31
|
+
from .credentials.base_custom_credential_service import BaseCustomCredentialService
|
32
|
+
|
33
|
+
logger = logging.getLogger(__name__)
|
34
|
+
|
35
|
+
|
36
|
+
class AdkBuilder:
|
37
|
+
"""Builder for creating enhanced Google ADK applications with custom credential services.
|
38
|
+
|
39
|
+
This builder extends Google ADK's capabilities by adding support for custom credential
|
40
|
+
services while maintaining full compatibility with all ADK features including web UI,
|
41
|
+
hot reloading, A2A protocol, and cloud deployment.
|
42
|
+
|
43
|
+
Example:
|
44
|
+
```python
|
45
|
+
from google_adk_extras import AdkBuilder
|
46
|
+
from google_adk_extras.credentials import GoogleOAuth2CredentialService
|
47
|
+
|
48
|
+
# Build FastAPI app with Google OAuth2 credentials
|
49
|
+
app = (AdkBuilder()
|
50
|
+
.with_agents_dir("./agents")
|
51
|
+
.with_session_service("sqlite:///sessions.db")
|
52
|
+
.with_credential_service(GoogleOAuth2CredentialService(
|
53
|
+
client_id="your-client-id",
|
54
|
+
client_secret="your-secret",
|
55
|
+
scopes=["calendar", "gmail.readonly"]
|
56
|
+
))
|
57
|
+
.with_web_ui()
|
58
|
+
.build_fastapi_app())
|
59
|
+
|
60
|
+
# Or build a Runner directly
|
61
|
+
runner = (AdkBuilder()
|
62
|
+
.with_agents_dir("./agents")
|
63
|
+
.with_credential_service_uri("oauth2-google://client-id:secret@scopes=calendar,gmail.readonly")
|
64
|
+
.build_runner("my_agent"))
|
65
|
+
```
|
66
|
+
"""
|
67
|
+
|
68
|
+
def __init__(self):
|
69
|
+
"""Initialize the AdkBuilder with default configuration."""
|
70
|
+
# Core configuration
|
71
|
+
self._agents_dir: Optional[str] = None
|
72
|
+
self._app_name: Optional[str] = None
|
73
|
+
|
74
|
+
# Service URIs (following ADK patterns)
|
75
|
+
self._session_service_uri: Optional[str] = None
|
76
|
+
self._artifact_service_uri: Optional[str] = None
|
77
|
+
self._memory_service_uri: Optional[str] = None
|
78
|
+
self._credential_service_uri: Optional[str] = None
|
79
|
+
self._eval_storage_uri: Optional[str] = None
|
80
|
+
|
81
|
+
# Service instances (alternative to URIs)
|
82
|
+
self._session_service: Optional[BaseSessionService] = None
|
83
|
+
self._artifact_service: Optional[BaseArtifactService] = None
|
84
|
+
self._memory_service: Optional[BaseMemoryService] = None
|
85
|
+
self._credential_service: Optional[BaseCredentialService] = None
|
86
|
+
|
87
|
+
# Agent loading configuration
|
88
|
+
self._agent_loader: Optional[BaseAgentLoader] = None
|
89
|
+
self._registered_agents: Dict[str, BaseAgent] = {}
|
90
|
+
|
91
|
+
# Database configuration
|
92
|
+
self._session_db_kwargs: Optional[Mapping[str, Any]] = None
|
93
|
+
|
94
|
+
# Web/FastAPI configuration
|
95
|
+
self._allow_origins: Optional[List[str]] = None
|
96
|
+
self._web_ui: bool = False
|
97
|
+
self._a2a: bool = False
|
98
|
+
# Programmatic A2A exposure (for registered/programmatic agents)
|
99
|
+
self._a2a_expose_programmatic: bool = False
|
100
|
+
self._a2a_programmatic_mount_base: str = "/a2a"
|
101
|
+
self._a2a_card_factory: Optional[Callable[[str, BaseAgent], Dict[str, Any]]] = None
|
102
|
+
self._host: str = "127.0.0.1"
|
103
|
+
self._port: int = 8000
|
104
|
+
self._trace_to_cloud: bool = False
|
105
|
+
self._reload_agents: bool = False
|
106
|
+
self._lifespan: Optional[Lifespan[FastAPI]] = None
|
107
|
+
|
108
|
+
# Staging list for remote A2A agents to register (if import is deferred)
|
109
|
+
self._pending_remote_a2a: List[Dict[str, str]] = []
|
110
|
+
|
111
|
+
# Core configuration methods
|
112
|
+
def with_agents_dir(self, agents_dir: str) -> "AdkBuilder":
|
113
|
+
"""Set the directory containing agent definitions.
|
114
|
+
|
115
|
+
Args:
|
116
|
+
agents_dir: Path to directory containing agent subdirectories.
|
117
|
+
|
118
|
+
Returns:
|
119
|
+
AdkBuilder: Self for method chaining.
|
120
|
+
"""
|
121
|
+
self._agents_dir = agents_dir
|
122
|
+
return self
|
123
|
+
|
124
|
+
def with_app_name(self, app_name: str) -> "AdkBuilder":
|
125
|
+
"""Set the application name.
|
126
|
+
|
127
|
+
Args:
|
128
|
+
app_name: Name of the application.
|
129
|
+
|
130
|
+
Returns:
|
131
|
+
AdkBuilder: Self for method chaining.
|
132
|
+
"""
|
133
|
+
self._app_name = app_name
|
134
|
+
return self
|
135
|
+
|
136
|
+
# Service URI methods (following ADK patterns)
|
137
|
+
def with_session_service(self, uri: str, **db_kwargs) -> "AdkBuilder":
|
138
|
+
"""Configure session service using URI.
|
139
|
+
|
140
|
+
Supported URIs:
|
141
|
+
- "sqlite:///./sessions.db" - SQLite database
|
142
|
+
- "postgresql://user:pass@host/db" - PostgreSQL database
|
143
|
+
- "yaml://path/to/sessions.yaml" - YAML file session storage
|
144
|
+
- "redis://localhost:6379" - Redis session storage
|
145
|
+
- "mongodb://localhost:27017/sessions" - MongoDB session storage
|
146
|
+
|
147
|
+
Args:
|
148
|
+
uri: Session service URI.
|
149
|
+
**db_kwargs: Additional database configuration options.
|
150
|
+
|
151
|
+
Returns:
|
152
|
+
AdkBuilder: Self for method chaining.
|
153
|
+
"""
|
154
|
+
self._session_service_uri = uri
|
155
|
+
if db_kwargs:
|
156
|
+
self._session_db_kwargs = db_kwargs
|
157
|
+
return self
|
158
|
+
|
159
|
+
def with_artifact_service(self, uri: str) -> "AdkBuilder":
|
160
|
+
"""Configure artifact service using URI.
|
161
|
+
|
162
|
+
Supported URIs:
|
163
|
+
- "gs://bucket-name" - Google Cloud Storage
|
164
|
+
|
165
|
+
Args:
|
166
|
+
uri: Artifact service URI.
|
167
|
+
|
168
|
+
Returns:
|
169
|
+
AdkBuilder: Self for method chaining.
|
170
|
+
"""
|
171
|
+
self._artifact_service_uri = uri
|
172
|
+
return self
|
173
|
+
|
174
|
+
def with_memory_service(self, uri: str) -> "AdkBuilder":
|
175
|
+
"""Configure memory service using URI.
|
176
|
+
|
177
|
+
Supported URIs:
|
178
|
+
- "yaml://path/to/memory.yaml" - YAML file memory storage
|
179
|
+
- "redis://localhost:6379" - Redis memory storage
|
180
|
+
- "sqlite:///./memory.db" - SQLite database
|
181
|
+
- "postgresql://user:pass@host/db" - PostgreSQL database
|
182
|
+
- "mongodb://localhost:27017/memory" - MongoDB memory storage
|
183
|
+
|
184
|
+
Args:
|
185
|
+
uri: Memory service URI.
|
186
|
+
|
187
|
+
Returns:
|
188
|
+
AdkBuilder: Self for method chaining.
|
189
|
+
"""
|
190
|
+
self._memory_service_uri = uri
|
191
|
+
return self
|
192
|
+
|
193
|
+
def with_credential_service_uri(self, uri: str) -> "AdkBuilder":
|
194
|
+
"""Configure credential service using URI.
|
195
|
+
|
196
|
+
Supported URIs:
|
197
|
+
- "oauth2-google://client-id:secret@scopes=scope1,scope2"
|
198
|
+
- "oauth2-github://client-id:secret@scopes=user,repo"
|
199
|
+
- "oauth2-microsoft://tenant-id/client-id:secret@scopes=User.Read"
|
200
|
+
- "oauth2-x://client-id:secret@scopes=tweet.read,users.read"
|
201
|
+
- "jwt://secret@algorithm=HS256&issuer=my-app&audience=api.example.com&expiration_minutes=60"
|
202
|
+
- "basic-auth://username:password@realm=My API"
|
203
|
+
|
204
|
+
Args:
|
205
|
+
uri: Credential service URI.
|
206
|
+
|
207
|
+
Returns:
|
208
|
+
AdkBuilder: Self for method chaining.
|
209
|
+
"""
|
210
|
+
self._credential_service_uri = uri
|
211
|
+
return self
|
212
|
+
|
213
|
+
def with_eval_storage(self, uri: str) -> "AdkBuilder":
|
214
|
+
"""Configure evaluation storage using URI.
|
215
|
+
|
216
|
+
Args:
|
217
|
+
uri: Evaluation storage URI.
|
218
|
+
|
219
|
+
Returns:
|
220
|
+
AdkBuilder: Self for method chaining.
|
221
|
+
"""
|
222
|
+
self._eval_storage_uri = uri
|
223
|
+
return self
|
224
|
+
|
225
|
+
# Service instance methods (alternative to URIs)
|
226
|
+
def with_session_service_instance(self, service: BaseSessionService) -> "AdkBuilder":
|
227
|
+
"""Configure session service using service instance.
|
228
|
+
|
229
|
+
Args:
|
230
|
+
service: Session service instance.
|
231
|
+
|
232
|
+
Returns:
|
233
|
+
AdkBuilder: Self for method chaining.
|
234
|
+
"""
|
235
|
+
self._session_service = service
|
236
|
+
return self
|
237
|
+
|
238
|
+
def with_artifact_service_instance(self, service: BaseArtifactService) -> "AdkBuilder":
|
239
|
+
"""Configure artifact service using service instance.
|
240
|
+
|
241
|
+
Args:
|
242
|
+
service: Artifact service instance.
|
243
|
+
|
244
|
+
Returns:
|
245
|
+
AdkBuilder: Self for method chaining.
|
246
|
+
"""
|
247
|
+
self._artifact_service = service
|
248
|
+
return self
|
249
|
+
|
250
|
+
def with_memory_service_instance(self, service: BaseMemoryService) -> "AdkBuilder":
|
251
|
+
"""Configure memory service using service instance.
|
252
|
+
|
253
|
+
Args:
|
254
|
+
service: Memory service instance.
|
255
|
+
|
256
|
+
Returns:
|
257
|
+
AdkBuilder: Self for method chaining.
|
258
|
+
"""
|
259
|
+
self._memory_service = service
|
260
|
+
return self
|
261
|
+
|
262
|
+
def with_credential_service(self, service: BaseCredentialService) -> "AdkBuilder":
|
263
|
+
"""Configure credential service using service instance.
|
264
|
+
|
265
|
+
Args:
|
266
|
+
service: Credential service instance (our custom services or ADK services).
|
267
|
+
|
268
|
+
Returns:
|
269
|
+
AdkBuilder: Self for method chaining.
|
270
|
+
"""
|
271
|
+
self._credential_service = service
|
272
|
+
return self
|
273
|
+
|
274
|
+
# Web/FastAPI configuration methods
|
275
|
+
def with_web_ui(self, enabled: bool = True) -> "AdkBuilder":
|
276
|
+
"""Enable or disable the web development UI.
|
277
|
+
|
278
|
+
Args:
|
279
|
+
enabled: Whether to enable web UI. Defaults to True.
|
280
|
+
|
281
|
+
Returns:
|
282
|
+
AdkBuilder: Self for method chaining.
|
283
|
+
"""
|
284
|
+
self._web_ui = enabled
|
285
|
+
return self
|
286
|
+
|
287
|
+
def with_cors(self, allow_origins: List[str]) -> "AdkBuilder":
|
288
|
+
"""Configure CORS allowed origins.
|
289
|
+
|
290
|
+
Args:
|
291
|
+
allow_origins: List of allowed origins for CORS.
|
292
|
+
|
293
|
+
Returns:
|
294
|
+
AdkBuilder: Self for method chaining.
|
295
|
+
"""
|
296
|
+
self._allow_origins = allow_origins
|
297
|
+
return self
|
298
|
+
|
299
|
+
def with_a2a_protocol(self, enabled: bool = True) -> "AdkBuilder":
|
300
|
+
"""Enable or disable Agent-to-Agent protocol support.
|
301
|
+
|
302
|
+
Args:
|
303
|
+
enabled: Whether to enable A2A protocol. Defaults to True.
|
304
|
+
|
305
|
+
Returns:
|
306
|
+
AdkBuilder: Self for method chaining.
|
307
|
+
"""
|
308
|
+
self._a2a = enabled
|
309
|
+
return self
|
310
|
+
|
311
|
+
def enable_a2a_for_registered_agents(
|
312
|
+
self,
|
313
|
+
*,
|
314
|
+
enabled: bool = True,
|
315
|
+
mount_base: str = "/a2a",
|
316
|
+
card_factory: Optional[Callable[[str, BaseAgent], Dict[str, Any]]] = None,
|
317
|
+
) -> "AdkBuilder":
|
318
|
+
"""Expose programmatically registered agents over A2A.
|
319
|
+
|
320
|
+
This enables A2A for agents added via with_agent_instance()/with_agents()
|
321
|
+
without requiring an `agents_dir`. Optionally provide a `card_factory`
|
322
|
+
to generate Agent Card dictionaries for each agent.
|
323
|
+
|
324
|
+
Args:
|
325
|
+
enabled: Toggle exposure.
|
326
|
+
mount_base: Base path to mount A2A routes, default "/a2a".
|
327
|
+
card_factory: Optional callable (name, agent) -> dict for AgentCard.
|
328
|
+
|
329
|
+
Returns:
|
330
|
+
AdkBuilder: Self for chaining.
|
331
|
+
"""
|
332
|
+
self._a2a_expose_programmatic = enabled
|
333
|
+
self._a2a_programmatic_mount_base = mount_base
|
334
|
+
self._a2a_card_factory = card_factory
|
335
|
+
return self
|
336
|
+
|
337
|
+
def with_remote_a2a_agent(
|
338
|
+
self, name: str, agent_card_url: str, description: Optional[str] = None
|
339
|
+
) -> "AdkBuilder":
|
340
|
+
"""Register a remote A2A agent (client proxy) by agent card URL.
|
341
|
+
|
342
|
+
Attempts to instantiate ADK's RemoteA2aAgent and register it by name.
|
343
|
+
Requires ADK installed with A2A extras: `pip install google-adk[a2a]`.
|
344
|
+
|
345
|
+
Args:
|
346
|
+
name: Logical name to register.
|
347
|
+
agent_card_url: Full URL to the remote agent card (well-known path).
|
348
|
+
description: Optional description.
|
349
|
+
|
350
|
+
Returns:
|
351
|
+
AdkBuilder: Self for chaining.
|
352
|
+
"""
|
353
|
+
# Try multiple likely import paths to be robust across ADK versions
|
354
|
+
RemoteA2aAgent = None # type: ignore
|
355
|
+
import_error: Optional[Exception] = None
|
356
|
+
for path in (
|
357
|
+
"google.adk.a2a.remote_a2a_agent",
|
358
|
+
"google.adk.a2a.remote_agent",
|
359
|
+
"google.adk.a2a.client.remote_a2a_agent",
|
360
|
+
):
|
361
|
+
try:
|
362
|
+
module = __import__(path, fromlist=["RemoteA2aAgent"]) # type: ignore
|
363
|
+
RemoteA2aAgent = getattr(module, "RemoteA2aAgent") # type: ignore
|
364
|
+
break
|
365
|
+
except Exception as e: # ImportError or AttributeError
|
366
|
+
import_error = e
|
367
|
+
continue
|
368
|
+
|
369
|
+
if RemoteA2aAgent is None:
|
370
|
+
raise ImportError(
|
371
|
+
"Could not import RemoteA2aAgent from ADK. Ensure A2A extras are installed: "
|
372
|
+
"pip install google-adk[a2a]. Last error: %r" % (import_error,)
|
373
|
+
)
|
374
|
+
|
375
|
+
# Instantiate and register
|
376
|
+
remote = RemoteA2aAgent(
|
377
|
+
name=name,
|
378
|
+
description=description or name,
|
379
|
+
agent_card=agent_card_url,
|
380
|
+
)
|
381
|
+
# Do not enforce BaseAgent type here; RemoteA2aAgent should be compatible
|
382
|
+
self._registered_agents[name] = remote
|
383
|
+
logger.info("Registered remote A2A agent: %s", name)
|
384
|
+
return self
|
385
|
+
|
386
|
+
def with_host_port(self, host: str = "127.0.0.1", port: int = 8000) -> "AdkBuilder":
|
387
|
+
"""Configure host and port for the server.
|
388
|
+
|
389
|
+
Args:
|
390
|
+
host: Host address. Defaults to "127.0.0.1".
|
391
|
+
port: Port number. Defaults to 8000.
|
392
|
+
|
393
|
+
Returns:
|
394
|
+
AdkBuilder: Self for method chaining.
|
395
|
+
"""
|
396
|
+
self._host = host
|
397
|
+
self._port = port
|
398
|
+
return self
|
399
|
+
|
400
|
+
def with_cloud_tracing(self, enabled: bool = True) -> "AdkBuilder":
|
401
|
+
"""Enable or disable cloud tracing.
|
402
|
+
|
403
|
+
Args:
|
404
|
+
enabled: Whether to enable cloud tracing. Defaults to True.
|
405
|
+
|
406
|
+
Returns:
|
407
|
+
AdkBuilder: Self for method chaining.
|
408
|
+
"""
|
409
|
+
self._trace_to_cloud = enabled
|
410
|
+
return self
|
411
|
+
|
412
|
+
def with_agent_reload(self, enabled: bool = True) -> "AdkBuilder":
|
413
|
+
"""Enable or disable hot reloading of agents during development.
|
414
|
+
|
415
|
+
Args:
|
416
|
+
enabled: Whether to enable agent hot reloading. Defaults to True.
|
417
|
+
|
418
|
+
Returns:
|
419
|
+
AdkBuilder: Self for method chaining.
|
420
|
+
"""
|
421
|
+
self._reload_agents = enabled
|
422
|
+
return self
|
423
|
+
|
424
|
+
def with_lifespan(self, lifespan: Lifespan[FastAPI]) -> "AdkBuilder":
|
425
|
+
"""Configure FastAPI lifespan events.
|
426
|
+
|
427
|
+
Args:
|
428
|
+
lifespan: FastAPI lifespan callable.
|
429
|
+
|
430
|
+
Returns:
|
431
|
+
AdkBuilder: Self for method chaining.
|
432
|
+
"""
|
433
|
+
self._lifespan = lifespan
|
434
|
+
return self
|
435
|
+
|
436
|
+
# Agent configuration methods
|
437
|
+
def with_agent_instance(self, name: str, agent: BaseAgent) -> "AdkBuilder":
|
438
|
+
"""Register an agent instance by name for programmatic agent control.
|
439
|
+
|
440
|
+
This allows you to define agents purely in code without requiring
|
441
|
+
directory structures or file-based definitions.
|
442
|
+
|
443
|
+
Args:
|
444
|
+
name: Agent name for discovery and loading.
|
445
|
+
agent: BaseAgent instance to register.
|
446
|
+
|
447
|
+
Returns:
|
448
|
+
AdkBuilder: Self for method chaining.
|
449
|
+
|
450
|
+
Example:
|
451
|
+
```python
|
452
|
+
from google.adk.agents import Agent
|
453
|
+
|
454
|
+
my_agent = Agent(
|
455
|
+
name="dynamic_agent",
|
456
|
+
model="gemini-2.0-flash",
|
457
|
+
instructions="You are a helpful assistant."
|
458
|
+
)
|
459
|
+
|
460
|
+
app = (AdkBuilder()
|
461
|
+
.with_agent_instance("my_agent", my_agent)
|
462
|
+
.build_fastapi_app())
|
463
|
+
```
|
464
|
+
"""
|
465
|
+
if not name or not name.strip():
|
466
|
+
raise ValueError("Agent name cannot be empty")
|
467
|
+
|
468
|
+
if not isinstance(agent, BaseAgent):
|
469
|
+
raise ValueError(f"Agent must be BaseAgent instance, got {type(agent)}")
|
470
|
+
|
471
|
+
self._registered_agents[name] = agent
|
472
|
+
logger.info("Registered agent instance: %s", name)
|
473
|
+
return self
|
474
|
+
|
475
|
+
def with_agents(self, agents_dict: Dict[str, BaseAgent]) -> "AdkBuilder":
|
476
|
+
"""Register multiple agent instances at once.
|
477
|
+
|
478
|
+
Args:
|
479
|
+
agents_dict: Dictionary mapping agent names to BaseAgent instances.
|
480
|
+
|
481
|
+
Returns:
|
482
|
+
AdkBuilder: Self for method chaining.
|
483
|
+
|
484
|
+
Example:
|
485
|
+
```python
|
486
|
+
agents = {
|
487
|
+
"agent1": Agent(...),
|
488
|
+
"agent2": Agent(...),
|
489
|
+
}
|
490
|
+
|
491
|
+
app = (AdkBuilder()
|
492
|
+
.with_agents(agents)
|
493
|
+
.build_fastapi_app())
|
494
|
+
```
|
495
|
+
"""
|
496
|
+
if not isinstance(agents_dict, dict):
|
497
|
+
raise ValueError("Agents must be a dictionary mapping names to BaseAgent instances")
|
498
|
+
|
499
|
+
for name, agent in agents_dict.items():
|
500
|
+
self.with_agent_instance(name, agent)
|
501
|
+
return self
|
502
|
+
|
503
|
+
def with_agent_loader(self, loader: BaseAgentLoader) -> "AdkBuilder":
|
504
|
+
"""Use a custom agent loader instead of directory-based loading.
|
505
|
+
|
506
|
+
This provides full control over agent discovery and loading logic.
|
507
|
+
The custom loader will be used instead of creating a default AgentLoader.
|
508
|
+
|
509
|
+
Args:
|
510
|
+
loader: BaseAgentLoader instance to use for agent loading.
|
511
|
+
|
512
|
+
Returns:
|
513
|
+
AdkBuilder: Self for method chaining.
|
514
|
+
|
515
|
+
Example:
|
516
|
+
```python
|
517
|
+
custom_loader = CustomAgentLoader()
|
518
|
+
custom_loader.register_agent("agent1", my_agent)
|
519
|
+
|
520
|
+
app = (AdkBuilder()
|
521
|
+
.with_agent_loader(custom_loader)
|
522
|
+
.build_fastapi_app())
|
523
|
+
```
|
524
|
+
"""
|
525
|
+
if not isinstance(loader, BaseAgentLoader):
|
526
|
+
raise ValueError(f"Agent loader must be BaseAgentLoader instance, got {type(loader)}")
|
527
|
+
|
528
|
+
self._agent_loader = loader
|
529
|
+
logger.info("Set custom agent loader: %s", type(loader).__name__)
|
530
|
+
return self
|
531
|
+
|
532
|
+
# Service creation methods
|
533
|
+
def _create_session_service(self) -> BaseSessionService:
|
534
|
+
"""Create session service from configuration."""
|
535
|
+
if self._session_service is not None:
|
536
|
+
return self._session_service
|
537
|
+
|
538
|
+
if self._session_service_uri:
|
539
|
+
db_kwargs = self._session_db_kwargs or {}
|
540
|
+
|
541
|
+
if self._session_service_uri.startswith("yaml://"):
|
542
|
+
from .sessions.yaml_file_session_service import YamlFileSessionService
|
543
|
+
base_directory = self._session_service_uri.split("://")[1]
|
544
|
+
return YamlFileSessionService(base_directory=base_directory)
|
545
|
+
elif self._session_service_uri.startswith("redis://"):
|
546
|
+
from .sessions.redis_session_service import RedisSessionService
|
547
|
+
return RedisSessionService(connection_string=self._session_service_uri)
|
548
|
+
elif self._session_service_uri.startswith("mongodb://"):
|
549
|
+
from .sessions.mongo_session_service import MongoSessionService
|
550
|
+
return MongoSessionService(connection_string=self._session_service_uri)
|
551
|
+
elif self._session_service_uri.startswith(("sqlite://", "postgresql://", "mysql://")):
|
552
|
+
from .sessions.sql_session_service import SQLSessionService
|
553
|
+
return SQLSessionService(database_url=self._session_service_uri)
|
554
|
+
else:
|
555
|
+
raise ValueError(f"Unsupported session service URI format: {self._session_service_uri}")
|
556
|
+
|
557
|
+
return InMemorySessionService()
|
558
|
+
|
559
|
+
def _create_artifact_service(self) -> BaseArtifactService:
|
560
|
+
"""Create artifact service from configuration."""
|
561
|
+
if self._artifact_service is not None:
|
562
|
+
return self._artifact_service
|
563
|
+
|
564
|
+
if self._artifact_service_uri:
|
565
|
+
if self._artifact_service_uri.startswith("local://"):
|
566
|
+
from .artifacts.local_folder_artifact_service import LocalFolderArtifactService
|
567
|
+
base_directory = self._artifact_service_uri.split("://")[1]
|
568
|
+
return LocalFolderArtifactService(base_directory=base_directory)
|
569
|
+
elif self._artifact_service_uri.startswith("s3://"):
|
570
|
+
from .artifacts.s3_artifact_service import S3ArtifactService
|
571
|
+
bucket_name = self._artifact_service_uri.split("://")[1]
|
572
|
+
return S3ArtifactService(bucket_name=bucket_name)
|
573
|
+
elif self._artifact_service_uri.startswith(("sqlite://", "postgresql://", "mysql://")):
|
574
|
+
from .artifacts.sql_artifact_service import SQLArtifactService
|
575
|
+
return SQLArtifactService(database_url=self._artifact_service_uri)
|
576
|
+
elif self._artifact_service_uri.startswith("mongodb://"):
|
577
|
+
from .artifacts.mongo_artifact_service import MongoArtifactService
|
578
|
+
return MongoArtifactService(connection_string=self._artifact_service_uri)
|
579
|
+
else:
|
580
|
+
raise ValueError(f"Unsupported artifact service URI: {self._artifact_service_uri}")
|
581
|
+
|
582
|
+
return InMemoryArtifactService()
|
583
|
+
|
584
|
+
def _create_memory_service(self) -> BaseMemoryService:
|
585
|
+
"""Create memory service from configuration."""
|
586
|
+
if self._memory_service is not None:
|
587
|
+
return self._memory_service
|
588
|
+
|
589
|
+
if self._memory_service_uri:
|
590
|
+
if self._memory_service_uri.startswith("yaml://"):
|
591
|
+
from .memory.yaml_file_memory_service import YamlFileMemoryService
|
592
|
+
base_directory = self._memory_service_uri.split("://")[1]
|
593
|
+
return YamlFileMemoryService(base_directory=base_directory)
|
594
|
+
elif self._memory_service_uri.startswith("redis://"):
|
595
|
+
from .memory.redis_memory_service import RedisMemoryService
|
596
|
+
return RedisMemoryService(connection_string=self._memory_service_uri)
|
597
|
+
elif self._memory_service_uri.startswith(("sqlite://", "postgresql://", "mysql://")):
|
598
|
+
from .memory.sql_memory_service import SQLMemoryService
|
599
|
+
return SQLMemoryService(database_url=self._memory_service_uri)
|
600
|
+
elif self._memory_service_uri.startswith("mongodb://"):
|
601
|
+
from .memory.mongo_memory_service import MongoMemoryService
|
602
|
+
return MongoMemoryService(connection_string=self._memory_service_uri)
|
603
|
+
else:
|
604
|
+
raise ValueError(f"Unsupported memory service URI: {self._memory_service_uri}")
|
605
|
+
|
606
|
+
return InMemoryMemoryService()
|
607
|
+
|
608
|
+
def _create_credential_service(self) -> Optional[BaseCredentialService]:
|
609
|
+
"""Create credential service from configuration (optional)."""
|
610
|
+
if self._credential_service is not None:
|
611
|
+
return self._credential_service
|
612
|
+
|
613
|
+
if self._credential_service_uri:
|
614
|
+
return self._parse_credential_service_uri(self._credential_service_uri)
|
615
|
+
|
616
|
+
# No credential service configured; allow server to default
|
617
|
+
return None
|
618
|
+
|
619
|
+
def _create_agent_loader(self) -> BaseAgentLoader:
|
620
|
+
"""Create agent loader from configuration.
|
621
|
+
|
622
|
+
Returns:
|
623
|
+
BaseAgentLoader: Configured agent loader instance.
|
624
|
+
|
625
|
+
Raises:
|
626
|
+
ValueError: If no agent configuration is provided.
|
627
|
+
"""
|
628
|
+
# If custom loader is provided, use it directly
|
629
|
+
if self._agent_loader is not None:
|
630
|
+
# If we also have registered agents, we need to register them
|
631
|
+
if self._registered_agents:
|
632
|
+
if isinstance(self._agent_loader, CustomAgentLoader):
|
633
|
+
# Register agents into the existing CustomAgentLoader
|
634
|
+
for name, agent in self._registered_agents.items():
|
635
|
+
self._agent_loader.register_agent(name, agent)
|
636
|
+
logger.info("Registered %d agents into existing CustomAgentLoader",
|
637
|
+
len(self._registered_agents))
|
638
|
+
else:
|
639
|
+
logger.warning(
|
640
|
+
"Custom agent loader is not CustomAgentLoader, but registered agents exist. "
|
641
|
+
"Registered agents will be ignored. Consider using CustomAgentLoader."
|
642
|
+
)
|
643
|
+
return self._agent_loader
|
644
|
+
|
645
|
+
# If we have registered agents, create CustomAgentLoader
|
646
|
+
if self._registered_agents:
|
647
|
+
# Create CustomAgentLoader (no fallback support)
|
648
|
+
if self._agents_dir:
|
649
|
+
raise ValueError("Cannot use agents_dir with registered agents - use either directory-based OR instance-based loading, not both")
|
650
|
+
|
651
|
+
logger.info("Creating CustomAgentLoader with registered agents")
|
652
|
+
custom_loader = CustomAgentLoader()
|
653
|
+
|
654
|
+
# Register all agents
|
655
|
+
for name, agent in self._registered_agents.items():
|
656
|
+
custom_loader.register_agent(name, agent)
|
657
|
+
|
658
|
+
logger.info("Registered %d agents into CustomAgentLoader", len(self._registered_agents))
|
659
|
+
return custom_loader
|
660
|
+
|
661
|
+
# If we only have agents_dir, create default AgentLoader
|
662
|
+
if self._agents_dir:
|
663
|
+
logger.info("Creating default AgentLoader for directory: %s", self._agents_dir)
|
664
|
+
return AgentLoader(self._agents_dir)
|
665
|
+
|
666
|
+
# No agent configuration provided
|
667
|
+
raise ValueError(
|
668
|
+
"No agent configuration provided. Use with_agents_dir(), with_agent_instance(), "
|
669
|
+
"or with_agent_loader() to configure agents."
|
670
|
+
)
|
671
|
+
|
672
|
+
def _parse_credential_service_uri(self, uri: str) -> BaseCredentialService:
|
673
|
+
"""Parse credential service URI and create appropriate service.
|
674
|
+
|
675
|
+
Args:
|
676
|
+
uri: Credential service URI.
|
677
|
+
|
678
|
+
Returns:
|
679
|
+
BaseCredentialService: Configured credential service.
|
680
|
+
|
681
|
+
Raises:
|
682
|
+
ValueError: If URI format is invalid or unsupported.
|
683
|
+
"""
|
684
|
+
try:
|
685
|
+
if uri.startswith("oauth2-google://"):
|
686
|
+
return self._parse_google_oauth2_uri(uri)
|
687
|
+
elif uri.startswith("oauth2-github://"):
|
688
|
+
return self._parse_github_oauth2_uri(uri)
|
689
|
+
elif uri.startswith("oauth2-microsoft://"):
|
690
|
+
return self._parse_microsoft_oauth2_uri(uri)
|
691
|
+
elif uri.startswith("oauth2-x://"):
|
692
|
+
return self._parse_x_oauth2_uri(uri)
|
693
|
+
elif uri.startswith("jwt://"):
|
694
|
+
return self._parse_jwt_uri(uri)
|
695
|
+
elif uri.startswith("basic-auth://"):
|
696
|
+
return self._parse_basic_auth_uri(uri)
|
697
|
+
else:
|
698
|
+
raise ValueError(f"Unsupported credential service URI scheme: {uri}")
|
699
|
+
except Exception as e:
|
700
|
+
raise ValueError(f"Failed to parse credential service URI '{uri}': {e}")
|
701
|
+
|
702
|
+
def _parse_google_oauth2_uri(self, uri: str) -> BaseCredentialService:
|
703
|
+
"""Parse Google OAuth2 URI: oauth2-google://client-id:secret@scopes=scope1,scope2"""
|
704
|
+
from .credentials.google_oauth2_credential_service import (
|
705
|
+
GoogleOAuth2CredentialService,
|
706
|
+
)
|
707
|
+
# Remove scheme
|
708
|
+
uri_part = uri[len("oauth2-google://"):]
|
709
|
+
|
710
|
+
# Split at @
|
711
|
+
if "@" in uri_part:
|
712
|
+
credentials_part, params_part = uri_part.split("@", 1)
|
713
|
+
else:
|
714
|
+
credentials_part = uri_part
|
715
|
+
params_part = ""
|
716
|
+
|
717
|
+
# Parse credentials
|
718
|
+
if ":" in credentials_part:
|
719
|
+
client_id, client_secret = credentials_part.split(":", 1)
|
720
|
+
else:
|
721
|
+
raise ValueError("Google OAuth2 URI must include client_id:client_secret")
|
722
|
+
|
723
|
+
# Parse parameters
|
724
|
+
scopes = []
|
725
|
+
if params_part:
|
726
|
+
for param in params_part.split("&"):
|
727
|
+
if param.startswith("scopes="):
|
728
|
+
scopes = param[7:].split(",")
|
729
|
+
|
730
|
+
return GoogleOAuth2CredentialService(
|
731
|
+
client_id=client_id,
|
732
|
+
client_secret=client_secret,
|
733
|
+
scopes=scopes or ["openid", "email", "profile"]
|
734
|
+
)
|
735
|
+
|
736
|
+
def _parse_github_oauth2_uri(self, uri: str) -> BaseCredentialService:
|
737
|
+
"""Parse GitHub OAuth2 URI: oauth2-github://client-id:secret@scopes=user,repo"""
|
738
|
+
from .credentials.github_oauth2_credential_service import (
|
739
|
+
GitHubOAuth2CredentialService,
|
740
|
+
)
|
741
|
+
uri_part = uri[len("oauth2-github://"):]
|
742
|
+
|
743
|
+
if "@" in uri_part:
|
744
|
+
credentials_part, params_part = uri_part.split("@", 1)
|
745
|
+
else:
|
746
|
+
credentials_part = uri_part
|
747
|
+
params_part = ""
|
748
|
+
|
749
|
+
if ":" in credentials_part:
|
750
|
+
client_id, client_secret = credentials_part.split(":", 1)
|
751
|
+
else:
|
752
|
+
raise ValueError("GitHub OAuth2 URI must include client_id:client_secret")
|
753
|
+
|
754
|
+
scopes = []
|
755
|
+
if params_part:
|
756
|
+
for param in params_part.split("&"):
|
757
|
+
if param.startswith("scopes="):
|
758
|
+
scopes = param[7:].split(",")
|
759
|
+
|
760
|
+
return GitHubOAuth2CredentialService(
|
761
|
+
client_id=client_id,
|
762
|
+
client_secret=client_secret,
|
763
|
+
scopes=scopes or ["user"]
|
764
|
+
)
|
765
|
+
|
766
|
+
def _parse_microsoft_oauth2_uri(self, uri: str) -> BaseCredentialService:
|
767
|
+
"""Parse Microsoft OAuth2 URI: oauth2-microsoft://tenant-id/client-id:secret@scopes=User.Read"""
|
768
|
+
from .credentials.microsoft_oauth2_credential_service import (
|
769
|
+
MicrosoftOAuth2CredentialService,
|
770
|
+
)
|
771
|
+
uri_part = uri[len("oauth2-microsoft://"):]
|
772
|
+
|
773
|
+
if "@" in uri_part:
|
774
|
+
credentials_part, params_part = uri_part.split("@", 1)
|
775
|
+
else:
|
776
|
+
credentials_part = uri_part
|
777
|
+
params_part = ""
|
778
|
+
|
779
|
+
# Parse tenant_id/client_id:secret
|
780
|
+
if "/" in credentials_part:
|
781
|
+
tenant_part, client_part = credentials_part.split("/", 1)
|
782
|
+
else:
|
783
|
+
raise ValueError("Microsoft OAuth2 URI must include tenant-id/client-id:secret")
|
784
|
+
|
785
|
+
if ":" in client_part:
|
786
|
+
client_id, client_secret = client_part.split(":", 1)
|
787
|
+
else:
|
788
|
+
raise ValueError("Microsoft OAuth2 URI must include client_id:client_secret")
|
789
|
+
|
790
|
+
scopes = []
|
791
|
+
if params_part:
|
792
|
+
for param in params_part.split("&"):
|
793
|
+
if param.startswith("scopes="):
|
794
|
+
scopes = param[7:].split(",")
|
795
|
+
|
796
|
+
return MicrosoftOAuth2CredentialService(
|
797
|
+
tenant_id=tenant_part,
|
798
|
+
client_id=client_id,
|
799
|
+
client_secret=client_secret,
|
800
|
+
scopes=scopes or ["User.Read"]
|
801
|
+
)
|
802
|
+
|
803
|
+
def _parse_x_oauth2_uri(self, uri: str) -> BaseCredentialService:
|
804
|
+
"""Parse X OAuth2 URI: oauth2-x://client-id:secret@scopes=tweet.read,users.read"""
|
805
|
+
from .credentials.x_oauth2_credential_service import XOAuth2CredentialService
|
806
|
+
uri_part = uri[len("oauth2-x://"):]
|
807
|
+
|
808
|
+
if "@" in uri_part:
|
809
|
+
credentials_part, params_part = uri_part.split("@", 1)
|
810
|
+
else:
|
811
|
+
credentials_part = uri_part
|
812
|
+
params_part = ""
|
813
|
+
|
814
|
+
if ":" in credentials_part:
|
815
|
+
client_id, client_secret = credentials_part.split(":", 1)
|
816
|
+
else:
|
817
|
+
raise ValueError("X OAuth2 URI must include client_id:client_secret")
|
818
|
+
|
819
|
+
scopes = []
|
820
|
+
if params_part:
|
821
|
+
for param in params_part.split("&"):
|
822
|
+
if param.startswith("scopes="):
|
823
|
+
scopes = param[7:].split(",")
|
824
|
+
|
825
|
+
return XOAuth2CredentialService(
|
826
|
+
client_id=client_id,
|
827
|
+
client_secret=client_secret,
|
828
|
+
scopes=scopes or ["tweet.read", "users.read", "offline.access"]
|
829
|
+
)
|
830
|
+
|
831
|
+
def _parse_jwt_uri(self, uri: str) -> BaseCredentialService:
|
832
|
+
"""Parse JWT URI: jwt://secret@algorithm=HS256&issuer=my-app&audience=api.example.com&expiration_minutes=60"""
|
833
|
+
from .credentials.jwt_credential_service import JWTCredentialService
|
834
|
+
uri_part = uri[len("jwt://"):]
|
835
|
+
|
836
|
+
if "@" in uri_part:
|
837
|
+
secret, params_part = uri_part.split("@", 1)
|
838
|
+
else:
|
839
|
+
secret = uri_part
|
840
|
+
params_part = ""
|
841
|
+
|
842
|
+
# Parse parameters
|
843
|
+
algorithm = "HS256"
|
844
|
+
issuer = None
|
845
|
+
audience = None
|
846
|
+
expiration_minutes = 60
|
847
|
+
custom_claims = {}
|
848
|
+
|
849
|
+
if params_part:
|
850
|
+
for param in params_part.split("&"):
|
851
|
+
if "=" in param:
|
852
|
+
key, value = param.split("=", 1)
|
853
|
+
if key == "algorithm":
|
854
|
+
algorithm = value
|
855
|
+
elif key == "issuer":
|
856
|
+
issuer = value
|
857
|
+
elif key == "audience":
|
858
|
+
audience = value
|
859
|
+
elif key == "expiration_minutes":
|
860
|
+
expiration_minutes = int(value)
|
861
|
+
else:
|
862
|
+
# Custom claim
|
863
|
+
custom_claims[key] = value
|
864
|
+
|
865
|
+
return JWTCredentialService(
|
866
|
+
secret=secret,
|
867
|
+
algorithm=algorithm,
|
868
|
+
issuer=issuer,
|
869
|
+
audience=audience,
|
870
|
+
expiration_minutes=expiration_minutes,
|
871
|
+
custom_claims=custom_claims
|
872
|
+
)
|
873
|
+
|
874
|
+
def _parse_basic_auth_uri(self, uri: str) -> BaseCredentialService:
|
875
|
+
"""Parse Basic Auth URI: basic-auth://username:password@realm=My API"""
|
876
|
+
from .credentials.http_basic_auth_credential_service import (
|
877
|
+
HTTPBasicAuthCredentialService,
|
878
|
+
)
|
879
|
+
uri_part = uri[len("basic-auth://"):]
|
880
|
+
|
881
|
+
if "@" in uri_part:
|
882
|
+
credentials_part, params_part = uri_part.split("@", 1)
|
883
|
+
else:
|
884
|
+
credentials_part = uri_part
|
885
|
+
params_part = ""
|
886
|
+
|
887
|
+
if ":" in credentials_part:
|
888
|
+
username, password = credentials_part.split(":", 1)
|
889
|
+
else:
|
890
|
+
raise ValueError("Basic Auth URI must include username:password")
|
891
|
+
|
892
|
+
realm = None
|
893
|
+
if params_part:
|
894
|
+
for param in params_part.split("&"):
|
895
|
+
if param.startswith("realm="):
|
896
|
+
realm = param[6:]
|
897
|
+
|
898
|
+
return HTTPBasicAuthCredentialService(
|
899
|
+
username=username,
|
900
|
+
password=password,
|
901
|
+
realm=realm
|
902
|
+
)
|
903
|
+
|
904
|
+
# Build methods
|
905
|
+
def build_fastapi_app(self) -> FastAPI:
|
906
|
+
"""Build and return configured FastAPI application.
|
907
|
+
|
908
|
+
Returns:
|
909
|
+
FastAPI: Configured FastAPI application with all ADK features.
|
910
|
+
|
911
|
+
Raises:
|
912
|
+
ValueError: If required configuration is missing.
|
913
|
+
"""
|
914
|
+
# Create services (agent loader validates agent configuration)
|
915
|
+
agent_loader = self._create_agent_loader()
|
916
|
+
session_service = self._create_session_service()
|
917
|
+
artifact_service = self._create_artifact_service()
|
918
|
+
memory_service = self._create_memory_service()
|
919
|
+
credential_service = self._create_credential_service()
|
920
|
+
|
921
|
+
# Initialize credential service if it's one of ours
|
922
|
+
if isinstance(credential_service, BaseCustomCredentialService):
|
923
|
+
import asyncio
|
924
|
+
try:
|
925
|
+
# Try to initialize in current event loop
|
926
|
+
loop = asyncio.get_event_loop()
|
927
|
+
if loop.is_running():
|
928
|
+
# Create a task for initialization
|
929
|
+
asyncio.create_task(credential_service.initialize())
|
930
|
+
else:
|
931
|
+
loop.run_until_complete(credential_service.initialize())
|
932
|
+
except RuntimeError:
|
933
|
+
# No event loop, create one
|
934
|
+
asyncio.run(credential_service.initialize())
|
935
|
+
|
936
|
+
# Use our enhanced FastAPI function that properly supports credential services
|
937
|
+
logger.info("Building FastAPI app with enhanced credential service support")
|
938
|
+
|
939
|
+
# Import our enhanced function
|
940
|
+
from .enhanced_fastapi import get_enhanced_fast_api_app
|
941
|
+
|
942
|
+
app = get_enhanced_fast_api_app(
|
943
|
+
agents_dir=self._agents_dir,
|
944
|
+
agent_loader=agent_loader,
|
945
|
+
session_service_uri=self._session_service_uri,
|
946
|
+
session_db_kwargs=self._session_db_kwargs,
|
947
|
+
artifact_service_uri=self._artifact_service_uri,
|
948
|
+
memory_service_uri=self._memory_service_uri,
|
949
|
+
credential_service=credential_service, # May be None; server will default
|
950
|
+
eval_storage_uri=self._eval_storage_uri,
|
951
|
+
allow_origins=self._allow_origins,
|
952
|
+
web=self._web_ui,
|
953
|
+
a2a=self._a2a,
|
954
|
+
programmatic_a2a=self._a2a_expose_programmatic,
|
955
|
+
programmatic_a2a_mount_base=self._a2a_programmatic_mount_base,
|
956
|
+
programmatic_a2a_card_factory=self._a2a_card_factory,
|
957
|
+
host=self._host,
|
958
|
+
port=self._port,
|
959
|
+
trace_to_cloud=self._trace_to_cloud,
|
960
|
+
reload_agents=self._reload_agents,
|
961
|
+
lifespan=self._lifespan,
|
962
|
+
)
|
963
|
+
|
964
|
+
return app
|
965
|
+
|
966
|
+
def build_runner(self, agent_or_agent_name: Union[BaseAgent, str]) -> Runner:
|
967
|
+
"""Build and return configured Runner.
|
968
|
+
|
969
|
+
Args:
|
970
|
+
agent_or_agent_name: Agent instance or agent name to load.
|
971
|
+
|
972
|
+
Returns:
|
973
|
+
Runner: Configured Runner instance.
|
974
|
+
|
975
|
+
Raises:
|
976
|
+
ValueError: If required configuration is missing.
|
977
|
+
"""
|
978
|
+
# Resolve agent instance
|
979
|
+
if isinstance(agent_or_agent_name, str):
|
980
|
+
name = agent_or_agent_name
|
981
|
+
agent = None
|
982
|
+
# 1) Prefer explicitly provided custom loader (supports programmatic agents)
|
983
|
+
if self._agent_loader is not None:
|
984
|
+
try:
|
985
|
+
agent = self._agent_loader.load_agent(name)
|
986
|
+
except Exception:
|
987
|
+
agent = None
|
988
|
+
# 2) Try registered agents collected via with_agent_instance()/with_agents()
|
989
|
+
if agent is None and self._registered_agents:
|
990
|
+
agent = self._registered_agents.get(name)
|
991
|
+
# 3) Fallback to directory-based AgentLoader if agents_dir is set
|
992
|
+
if agent is None and self._agents_dir:
|
993
|
+
agent = AgentLoader(self._agents_dir).load_agent(name)
|
994
|
+
if agent is None:
|
995
|
+
raise ValueError(
|
996
|
+
"Agent not found. Provide an instance via with_agent_instance()/with_agents(), "
|
997
|
+
"or set a custom loader with with_agent_loader(), or set with_agents_dir() for directory-based loading."
|
998
|
+
)
|
999
|
+
else:
|
1000
|
+
agent = agent_or_agent_name
|
1001
|
+
|
1002
|
+
# Create services
|
1003
|
+
session_service = self._create_session_service()
|
1004
|
+
artifact_service = self._create_artifact_service()
|
1005
|
+
memory_service = self._create_memory_service()
|
1006
|
+
credential_service = self._create_credential_service()
|
1007
|
+
|
1008
|
+
# Initialize credential service if it's one of ours
|
1009
|
+
if isinstance(credential_service, BaseCustomCredentialService):
|
1010
|
+
import asyncio
|
1011
|
+
try:
|
1012
|
+
loop = asyncio.get_event_loop()
|
1013
|
+
if loop.is_running():
|
1014
|
+
asyncio.create_task(credential_service.initialize())
|
1015
|
+
else:
|
1016
|
+
loop.run_until_complete(credential_service.initialize())
|
1017
|
+
except RuntimeError:
|
1018
|
+
asyncio.run(credential_service.initialize())
|
1019
|
+
|
1020
|
+
# Create Runner with all services
|
1021
|
+
app_name = self._app_name or (agent_or_agent_name if isinstance(agent_or_agent_name, str) else "default_app")
|
1022
|
+
|
1023
|
+
return Runner(
|
1024
|
+
app_name=app_name,
|
1025
|
+
agent=agent,
|
1026
|
+
session_service=session_service,
|
1027
|
+
artifact_service=artifact_service,
|
1028
|
+
memory_service=memory_service,
|
1029
|
+
credential_service=credential_service,
|
1030
|
+
)
|