google-adk-extras 0.1.1__py3-none-any.whl → 0.2.3__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 (35) hide show
  1. google_adk_extras/__init__.py +31 -1
  2. google_adk_extras/adk_builder.py +1030 -0
  3. google_adk_extras/artifacts/__init__.py +25 -12
  4. google_adk_extras/artifacts/base_custom_artifact_service.py +148 -11
  5. google_adk_extras/artifacts/local_folder_artifact_service.py +133 -13
  6. google_adk_extras/artifacts/s3_artifact_service.py +135 -19
  7. google_adk_extras/artifacts/sql_artifact_service.py +109 -10
  8. google_adk_extras/credentials/__init__.py +34 -0
  9. google_adk_extras/credentials/base_custom_credential_service.py +113 -0
  10. google_adk_extras/credentials/github_oauth2_credential_service.py +213 -0
  11. google_adk_extras/credentials/google_oauth2_credential_service.py +216 -0
  12. google_adk_extras/credentials/http_basic_auth_credential_service.py +388 -0
  13. google_adk_extras/credentials/jwt_credential_service.py +345 -0
  14. google_adk_extras/credentials/microsoft_oauth2_credential_service.py +250 -0
  15. google_adk_extras/credentials/x_oauth2_credential_service.py +240 -0
  16. google_adk_extras/custom_agent_loader.py +156 -0
  17. google_adk_extras/enhanced_adk_web_server.py +137 -0
  18. google_adk_extras/enhanced_fastapi.py +470 -0
  19. google_adk_extras/enhanced_runner.py +38 -0
  20. google_adk_extras/memory/__init__.py +30 -13
  21. google_adk_extras/memory/base_custom_memory_service.py +37 -5
  22. google_adk_extras/memory/sql_memory_service.py +105 -19
  23. google_adk_extras/memory/yaml_file_memory_service.py +115 -22
  24. google_adk_extras/sessions/__init__.py +29 -13
  25. google_adk_extras/sessions/base_custom_session_service.py +133 -11
  26. google_adk_extras/sessions/sql_session_service.py +127 -16
  27. google_adk_extras/sessions/yaml_file_session_service.py +122 -14
  28. google_adk_extras-0.2.3.dist-info/METADATA +302 -0
  29. google_adk_extras-0.2.3.dist-info/RECORD +37 -0
  30. google_adk_extras/py.typed +0 -0
  31. google_adk_extras-0.1.1.dist-info/METADATA +0 -175
  32. google_adk_extras-0.1.1.dist-info/RECORD +0 -25
  33. {google_adk_extras-0.1.1.dist-info → google_adk_extras-0.2.3.dist-info}/WHEEL +0 -0
  34. {google_adk_extras-0.1.1.dist-info → google_adk_extras-0.2.3.dist-info}/licenses/LICENSE +0 -0
  35. {google_adk_extras-0.1.1.dist-info → google_adk_extras-0.2.3.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
+ )