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.
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 +170 -0
  17. google_adk_extras/enhanced_adk_web_server.py +137 -0
  18. google_adk_extras/enhanced_fastapi.py +507 -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.5.dist-info/METADATA +302 -0
  29. google_adk_extras-0.2.5.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.5.dist-info}/WHEEL +0 -0
  34. {google_adk_extras-0.1.1.dist-info → google_adk_extras-0.2.5.dist-info}/licenses/LICENSE +0 -0
  35. {google_adk_extras-0.1.1.dist-info → google_adk_extras-0.2.5.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,240 @@
1
+ """X (Twitter) OAuth2 credential service implementation."""
2
+
3
+ from typing import Optional, List
4
+ import logging
5
+
6
+ from google.adk.auth.credential_service.session_state_credential_service import SessionStateCredentialService
7
+ from google.adk.auth.credential_service.base_credential_service import CallbackContext
8
+ from google.adk.auth import AuthConfig, AuthCredential, AuthCredentialTypes
9
+ from google.adk.auth.auth_credential import OAuth2Auth
10
+ from fastapi.openapi.models import OAuth2
11
+ from fastapi.openapi.models import OAuthFlowAuthorizationCode, OAuthFlows
12
+
13
+ from .base_custom_credential_service import BaseCustomCredentialService
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class XOAuth2CredentialService(BaseCustomCredentialService):
19
+ """X (Twitter) OAuth2 credential service for handling X API authentication flows.
20
+
21
+ This service provides pre-configured OAuth2 flows for X API v2 including
22
+ reading tweets, posting content, and managing user data.
23
+
24
+ Args:
25
+ client_id: The X OAuth2 client ID from X Developer Portal.
26
+ client_secret: The X OAuth2 client secret from X Developer Portal.
27
+ scopes: List of OAuth2 scopes to request. Common scopes include:
28
+ - "tweet.read" - Read tweets
29
+ - "tweet.write" - Write tweets
30
+ - "tweet.moderate.write" - Moderate tweets
31
+ - "users.read" - Read user information
32
+ - "follows.read" - Read follows information
33
+ - "follows.write" - Manage follows
34
+ - "offline.access" - Maintain access when user is offline
35
+ - "space.read" - Read Spaces information
36
+ - "mute.read" - Read muted accounts
37
+ - "mute.write" - Manage muted accounts
38
+ - "like.read" - Read likes information
39
+ - "like.write" - Manage likes
40
+ - "list.read" - Read list information
41
+ - "list.write" - Manage lists
42
+ - "block.read" - Read blocked accounts
43
+ - "block.write" - Manage blocked accounts
44
+ use_session_state: If True, stores credentials in session state. If False,
45
+ uses in-memory storage. Default is True for persistence.
46
+
47
+ Example:
48
+ ```python
49
+ credential_service = XOAuth2CredentialService(
50
+ client_id="your-x-client-id",
51
+ client_secret="your-x-client-secret",
52
+ scopes=["tweet.read", "tweet.write", "users.read", "offline.access"]
53
+ )
54
+ await credential_service.initialize()
55
+
56
+ # Use with Runner
57
+ runner = Runner(
58
+ agent=agent,
59
+ session_service=session_service,
60
+ credential_service=credential_service,
61
+ app_name="my_app"
62
+ )
63
+ ```
64
+ """
65
+
66
+ # X OAuth2 endpoints
67
+ X_AUTH_URL = "https://twitter.com/i/oauth2/authorize"
68
+ X_TOKEN_URL = "https://api.twitter.com/2/oauth2/token"
69
+
70
+ # Common X OAuth2 scopes
71
+ COMMON_SCOPES = {
72
+ # Tweet scopes
73
+ "tweet.read": "Read tweets",
74
+ "tweet.write": "Write tweets",
75
+ "tweet.moderate.write": "Hide and unhide replies to your tweets",
76
+
77
+ # User scopes
78
+ "users.read": "Read user profile information",
79
+
80
+ # Follows scopes
81
+ "follows.read": "Read who a user is following or who is following a user",
82
+ "follows.write": "Follow and unfollow other users",
83
+
84
+ # Offline access
85
+ "offline.access": "Maintain access to accounts when users are offline",
86
+
87
+ # Space scopes
88
+ "space.read": "Read Spaces information",
89
+
90
+ # Mute scopes
91
+ "mute.read": "Read muted accounts",
92
+ "mute.write": "Mute and unmute accounts",
93
+
94
+ # Like scopes
95
+ "like.read": "Read liked tweets",
96
+ "like.write": "Like and unlike tweets",
97
+
98
+ # List scopes
99
+ "list.read": "Read list information",
100
+ "list.write": "Create and manage lists",
101
+
102
+ # Block scopes
103
+ "block.read": "Read blocked accounts",
104
+ "block.write": "Block and unblock accounts",
105
+
106
+ # Bookmark scopes
107
+ "bookmark.read": "Read bookmarked tweets",
108
+ "bookmark.write": "Bookmark and unbookmark tweets",
109
+
110
+ # Direct message scopes
111
+ "dm.read": "Read direct messages",
112
+ "dm.write": "Send direct messages",
113
+ }
114
+
115
+ def __init__(
116
+ self,
117
+ client_id: str,
118
+ client_secret: str,
119
+ scopes: Optional[List[str]] = None,
120
+ use_session_state: bool = True
121
+ ):
122
+ """Initialize the X OAuth2 credential service.
123
+
124
+ Args:
125
+ client_id: X OAuth2 client ID.
126
+ client_secret: X OAuth2 client secret.
127
+ scopes: List of OAuth2 scopes to request.
128
+ use_session_state: Whether to use session state for credential storage.
129
+ """
130
+ super().__init__()
131
+ self.client_id = client_id
132
+ self.client_secret = client_secret
133
+ self.scopes = scopes or ["tweet.read", "users.read", "offline.access"]
134
+ self.use_session_state = use_session_state
135
+
136
+ # Underlying credential service for storage
137
+ if use_session_state:
138
+ self._storage_service = SessionStateCredentialService()
139
+ else:
140
+ from google.adk.auth.credential_service.in_memory_credential_service import InMemoryCredentialService
141
+ self._storage_service = InMemoryCredentialService()
142
+
143
+ async def _initialize_impl(self) -> None:
144
+ """Initialize the X OAuth2 credential service.
145
+
146
+ Validates the client credentials and sets up the OAuth2 auth scheme.
147
+
148
+ Raises:
149
+ ValueError: If client_id or client_secret is missing.
150
+ """
151
+ if not self.client_id:
152
+ raise ValueError("X OAuth2 client_id is required")
153
+ if not self.client_secret:
154
+ raise ValueError("X OAuth2 client_secret is required")
155
+ if not self.scopes:
156
+ raise ValueError("At least one OAuth2 scope is required")
157
+
158
+ # Validate scopes against known X scopes
159
+ unknown_scopes = set(self.scopes) - set(self.COMMON_SCOPES.keys())
160
+ if unknown_scopes:
161
+ logger.warning(f"Unknown X OAuth2 scopes: {unknown_scopes}")
162
+
163
+ logger.info(f"Initialized X OAuth2 credential service with scopes: {self.scopes}")
164
+
165
+ def create_auth_config(self) -> AuthConfig:
166
+ """Create an AuthConfig for X OAuth2 authentication.
167
+
168
+ Returns:
169
+ AuthConfig: Configured auth config for X OAuth2 flow.
170
+ """
171
+ self._check_initialized()
172
+
173
+ # Create OAuth2 auth scheme
174
+ auth_scheme = OAuth2(
175
+ flows=OAuthFlows(
176
+ authorizationCode=OAuthFlowAuthorizationCode(
177
+ authorizationUrl=self.X_AUTH_URL,
178
+ tokenUrl=self.X_TOKEN_URL,
179
+ scopes={
180
+ scope: self.COMMON_SCOPES.get(scope, f"X API scope: {scope}")
181
+ for scope in self.scopes
182
+ }
183
+ )
184
+ )
185
+ )
186
+
187
+ # Create OAuth2 credential
188
+ auth_credential = AuthCredential(
189
+ auth_type=AuthCredentialTypes.OAUTH2,
190
+ oauth2=OAuth2Auth(
191
+ client_id=self.client_id,
192
+ client_secret=self.client_secret
193
+ )
194
+ )
195
+
196
+ return AuthConfig(
197
+ auth_scheme=auth_scheme,
198
+ raw_auth_credential=auth_credential
199
+ )
200
+
201
+ async def load_credential(
202
+ self,
203
+ auth_config: AuthConfig,
204
+ callback_context: CallbackContext,
205
+ ) -> Optional[AuthCredential]:
206
+ """Load X OAuth2 credential from storage.
207
+
208
+ Args:
209
+ auth_config: The auth config containing credential key information.
210
+ callback_context: The current callback context.
211
+
212
+ Returns:
213
+ Optional[AuthCredential]: The stored credential or None if not found.
214
+ """
215
+ self._check_initialized()
216
+ return await self._storage_service.load_credential(auth_config, callback_context)
217
+
218
+ async def save_credential(
219
+ self,
220
+ auth_config: AuthConfig,
221
+ callback_context: CallbackContext,
222
+ ) -> None:
223
+ """Save X OAuth2 credential to storage.
224
+
225
+ Args:
226
+ auth_config: The auth config containing the credential to save.
227
+ callback_context: The current callback context.
228
+ """
229
+ self._check_initialized()
230
+ await self._storage_service.save_credential(auth_config, callback_context)
231
+
232
+ logger.info(f"Saved X OAuth2 credential for user {callback_context._invocation_context.user_id}")
233
+
234
+ def get_supported_scopes(self) -> dict:
235
+ """Get dictionary of supported X OAuth2 scopes and their descriptions.
236
+
237
+ Returns:
238
+ dict: Mapping of scope names to descriptions.
239
+ """
240
+ return self.COMMON_SCOPES.copy()
@@ -0,0 +1,170 @@
1
+ """CustomAgentLoader - Enhanced agent loader for programmatic agent management.
2
+
3
+ This module provides CustomAgentLoader which extends Google ADK's agent loading
4
+ capabilities to support programmatically registered agent instances with
5
+ thread-safe registry management.
6
+ """
7
+
8
+ import logging
9
+ import threading
10
+ from typing import Dict, List, Optional
11
+
12
+ from google.adk.agents.base_agent import BaseAgent
13
+ from google.adk.cli.utils.base_agent_loader import BaseAgentLoader
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class CustomAgentLoader(BaseAgentLoader):
19
+ """Enhanced agent loader for programmatic agent management.
20
+
21
+ This loader allows you to:
22
+ 1. Register agent instances directly for programmatic control
23
+ 2. Dynamically add/remove agents at runtime
24
+ 3. Thread-safe access to agent registry
25
+
26
+ Examples:
27
+ # Register and use agents
28
+ loader = CustomAgentLoader()
29
+ loader.register_agent("my_agent", my_agent_instance)
30
+ agent = loader.load_agent("my_agent")
31
+
32
+ # List all registered agents
33
+ agents = loader.list_agents() # ['my_agent']
34
+ """
35
+
36
+ def __init__(self):
37
+ """Initialize CustomAgentLoader."""
38
+ self._registered_agents: Dict[str, BaseAgent] = {}
39
+ self._lock = threading.RLock() # Thread-safe access to registry
40
+
41
+ logger.debug("CustomAgentLoader initialized")
42
+
43
+ def register_agent(self, name: str, agent: BaseAgent) -> None:
44
+ """Register an agent instance by name.
45
+
46
+ Args:
47
+ name: Agent name for discovery and loading.
48
+ agent: BaseAgent instance to register.
49
+
50
+ Raises:
51
+ ValueError: If name is empty or agent is not a BaseAgent instance.
52
+ """
53
+ if not name or not name.strip():
54
+ raise ValueError("Agent name cannot be empty")
55
+
56
+ if not isinstance(agent, BaseAgent):
57
+ raise ValueError(f"Agent must be BaseAgent instance, got {type(agent)}")
58
+
59
+ with self._lock:
60
+ if name in self._registered_agents:
61
+ logger.info("Replacing existing registered agent: %s", name)
62
+ else:
63
+ logger.info("Registering new agent instance: %s", name)
64
+
65
+ self._registered_agents[name] = agent
66
+
67
+ def unregister_agent(self, name: str) -> bool:
68
+ """Unregister an agent instance by name.
69
+
70
+ Args:
71
+ name: Name of agent to unregister.
72
+
73
+ Returns:
74
+ bool: True if agent was found and removed, False otherwise.
75
+ """
76
+ with self._lock:
77
+ if name in self._registered_agents:
78
+ del self._registered_agents[name]
79
+ logger.info("Unregistered agent instance: %s", name)
80
+ return True
81
+ else:
82
+ logger.debug("Agent not found in registry: %s", name)
83
+ return False
84
+
85
+ def is_registered(self, name: str) -> bool:
86
+ """Check if an agent is registered by name.
87
+
88
+ Args:
89
+ name: Agent name to check.
90
+
91
+ Returns:
92
+ bool: True if agent is registered, False otherwise.
93
+ """
94
+ with self._lock:
95
+ return name in self._registered_agents
96
+
97
+ def get_registered_agents(self) -> Dict[str, BaseAgent]:
98
+ """Get a copy of all registered agents.
99
+
100
+ Returns:
101
+ Dict[str, BaseAgent]: Copy of registered agents mapping.
102
+ """
103
+ with self._lock:
104
+ return self._registered_agents.copy()
105
+
106
+ def clear_registry(self) -> None:
107
+ """Clear all registered agents from the registry."""
108
+ with self._lock:
109
+ count = len(self._registered_agents)
110
+ self._registered_agents.clear()
111
+ logger.info("Cleared %d registered agents", count)
112
+
113
+ def load_agent(self, name: str) -> BaseAgent:
114
+ """Load an agent by name.
115
+
116
+ Args:
117
+ name: Name of agent to load.
118
+
119
+ Returns:
120
+ BaseAgent: The loaded agent instance.
121
+
122
+ Raises:
123
+ ValueError: If agent is not found in registry.
124
+ """
125
+ with self._lock:
126
+ if name in self._registered_agents:
127
+ logger.debug("Loading registered agent: %s", name)
128
+ return self._registered_agents[name]
129
+
130
+ # Agent not found
131
+ available_agents = self.list_agents()
132
+ raise ValueError(
133
+ f"Agent '{name}' not found. "
134
+ f"Available agents: {available_agents if available_agents else 'None'}"
135
+ )
136
+
137
+ def list_agents(self) -> List[str]:
138
+ """List all available agents from registry.
139
+
140
+ Returns:
141
+ List[str]: Sorted list of all registered agent names.
142
+ """
143
+ with self._lock:
144
+ agent_names = list(self._registered_agents.keys())
145
+
146
+ sorted_agents = sorted(agent_names)
147
+ logger.debug("Total registered agents: %d", len(sorted_agents))
148
+ return sorted_agents
149
+
150
+ # Compatibility with ADK's AgentLoader API used by AgentChangeEventHandler
151
+ def remove_agent_from_cache(self, name: str) -> None:
152
+ """No-op cache invalidation for compatibility with ADK hot reload.
153
+
154
+ ADK's file-watcher calls `agent_loader.remove_agent_from_cache(current_app)`
155
+ when files change. Our loader does not cache filesystem-loaded agents,
156
+ but we provide this method to satisfy the expected interface.
157
+
158
+ Args:
159
+ name: Agent name to invalidate (ignored here).
160
+ """
161
+ # Nothing to do; present for interface compatibility.
162
+ logger.debug("CustomAgentLoader.remove_agent_from_cache(%s) - no-op", name)
163
+
164
+
165
+ def __repr__(self) -> str:
166
+ """String representation of the loader."""
167
+ with self._lock:
168
+ registered_count = len(self._registered_agents)
169
+
170
+ return f"CustomAgentLoader(registered={registered_count})"
@@ -0,0 +1,137 @@
1
+ """Enhanced ADK Web Server that uses EnhancedRunner.
2
+
3
+ This module provides the EnhancedAdkWebServer class which extends Google ADK's
4
+ AdkWebServer to use our EnhancedRunner with advanced features.
5
+ """
6
+
7
+ import os
8
+ from typing import Optional
9
+
10
+ from google.adk.cli.adk_web_server import AdkWebServer
11
+ from google.adk.auth.credential_service.in_memory_credential_service import InMemoryCredentialService
12
+ from google.adk.cli.utils import cleanup
13
+ from google.adk.cli.utils import envs
14
+ from google.adk.runners import Runner
15
+
16
+ from .enhanced_runner import EnhancedRunner
17
+
18
+
19
+ class EnhancedAdkWebServer(AdkWebServer):
20
+ """Enhanced ADK Web Server that creates EnhancedRunner instances.
21
+
22
+ This class extends Google's AdkWebServer to use our EnhancedRunner with:
23
+ - Advanced tool execution strategies (MCP, OpenAPI, Function tools)
24
+ - Circuit breakers and retry policies for resilience
25
+ - YAML system context and enhanced configuration
26
+ - Performance monitoring and debugging capabilities
27
+ - Credential service integration (inherited)
28
+
29
+ The EnhancedAdkWebServer is a drop-in replacement for AdkWebServer that
30
+ provides significantly enhanced capabilities while maintaining full
31
+ backward compatibility with all existing APIs.
32
+
33
+ Examples:
34
+ Basic usage (drop-in replacement):
35
+ ```python
36
+ enhanced_server = EnhancedAdkWebServer(
37
+ agent_loader=agent_loader,
38
+ session_service=session_service,
39
+ artifact_service=artifact_service,
40
+ memory_service=memory_service,
41
+ credential_service=credential_service,
42
+ eval_sets_manager=eval_sets_manager,
43
+ eval_set_results_manager=eval_set_results_manager,
44
+ agents_dir="./agents"
45
+ )
46
+ ```
47
+
48
+ With enhanced features:
49
+ ```python
50
+ enhanced_config = EnhancedRunConfig.from_yaml_dict({
51
+ 'max_llm_calls': 200,
52
+ 'tool_timeouts': {'mcp_tools': 30.0},
53
+ 'circuit_breaker': {'failure_threshold': 3},
54
+ 'debug': {'enabled': True}
55
+ })
56
+
57
+ strategy_manager = ToolExecutionStrategyManager()
58
+ strategy_manager.register_strategy('mcp', McpToolExecutionStrategy(timeout=45.0))
59
+
60
+ enhanced_server = EnhancedAdkWebServer(
61
+ agent_loader=agent_loader,
62
+ session_service=session_service,
63
+ # ... other services ...
64
+ enhanced_config=enhanced_config,
65
+ yaml_context=YamlSystemContext(
66
+ system_name="my-agent-system",
67
+ config_path="/path/to/config.yaml"
68
+ ),
69
+ tool_strategy_manager=strategy_manager
70
+ )
71
+ ```
72
+ """
73
+
74
+ def __init__(self, **kwargs):
75
+ """Initialize EnhancedAdkWebServer.
76
+
77
+ Args:
78
+ enhanced_config: Enhanced configuration for runners (optional)
79
+ yaml_context: YAML system context for error handling (optional)
80
+ tool_strategy_manager: Tool execution strategy manager (optional)
81
+ **kwargs: All other parameters passed to AdkWebServer
82
+ """
83
+ # Ensure a credential service exists; default to InMemory if not provided
84
+ if 'credential_service' not in kwargs or kwargs.get('credential_service') is None:
85
+ kwargs['credential_service'] = InMemoryCredentialService()
86
+
87
+ # Initialize base AdkWebServer with all standard parameters
88
+ super().__init__(**kwargs)
89
+
90
+ # No enhanced configuration retained in simplified scope
91
+
92
+ async def get_runner_async(self, app_name: str) -> EnhancedRunner:
93
+ """Returns the enhanced runner for the given app.
94
+
95
+ This method overrides AdkWebServer.get_runner_async to create
96
+ EnhancedRunner instances instead of standard Runner instances.
97
+
98
+ The logic is identical to the parent class except:
99
+ 1. Creates EnhancedRunner instead of Runner
100
+ 2. Passes enhanced configuration parameters
101
+ 3. Maintains full compatibility with cleanup and caching
102
+
103
+ Args:
104
+ app_name: The name of the application/agent to get runner for
105
+
106
+ Returns:
107
+ An EnhancedRunner instance for the specified app
108
+ """
109
+ # EXACT copy of parent logic for cleanup and caching
110
+ if app_name in self.runners_to_clean:
111
+ self.runners_to_clean.remove(app_name)
112
+ runner = self.runner_dict.pop(app_name, None)
113
+ await cleanup.close_runners(list([runner]))
114
+
115
+ # Load environment for the agent
116
+ envs.load_dotenv_for_agent(os.path.basename(app_name), self.agents_dir)
117
+
118
+ # Return cached runner if available
119
+ if app_name in self.runner_dict:
120
+ return self.runner_dict[app_name]
121
+
122
+ # Load agent and create new EnhancedRunner
123
+ root_agent = self.agent_loader.load_agent(app_name)
124
+
125
+ # Create EnhancedRunner (thin wrapper over ADK Runner)
126
+ runner = EnhancedRunner(
127
+ app_name=app_name,
128
+ agent=root_agent,
129
+ artifact_service=self.artifact_service,
130
+ session_service=self.session_service,
131
+ memory_service=self.memory_service,
132
+ credential_service=self.credential_service,
133
+ )
134
+
135
+ # Cache and return runner (same as parent)
136
+ self.runner_dict[app_name] = runner
137
+ return runner