diff --git a/src/google/adk/cli/service_registry.py b/src/google/adk/cli/service_registry.py index 2ea286ef5b..d0a55362b0 100644 --- a/src/google/adk/cli/service_registry.py +++ b/src/google/adk/cli/service_registry.py @@ -357,15 +357,12 @@ def _parse_agent_engine_kwargs( "Agent engine resource name or resource id cannot be empty." ) - # If uri_part is just an ID, load project/location from env + # If uri_part is just an ID, defer project/location loading to runtime if "/" not in uri_part: - project, location = _load_gcp_config( - agents_dir, "short-form agent engine IDs" - ) + # Return with agents_dir for lazy resolution at runtime return { - "project": project, - "location": location, "agent_engine_id": uri_part, + "agents_dir": agents_dir, } # If uri_part is a full resource name, parse it @@ -385,6 +382,7 @@ def _parse_agent_engine_kwargs( "project": parts[1], "location": parts[3], "agent_engine_id": parts[5], + "agents_dir": agents_dir, } diff --git a/src/google/adk/memory/vertex_ai_memory_bank_service.py b/src/google/adk/memory/vertex_ai_memory_bank_service.py index a33095e168..95bd6c4f91 100644 --- a/src/google/adk/memory/vertex_ai_memory_bank_service.py +++ b/src/google/adk/memory/vertex_ai_memory_bank_service.py @@ -72,12 +72,15 @@ def __init__( agent_engine_id: Optional[str] = None, *, express_mode_api_key: Optional[str] = None, + agents_dir: Optional[str] = None, ): """Initializes a VertexAiMemoryBankService. Args: - project: The project ID of the Memory Bank to use. - location: The location of the Memory Bank to use. + project: The project ID of the Memory Bank to use. If not provided, will + be resolved lazily at runtime from environment variables or .env files. + location: The location of the Memory Bank to use. If not provided, will + be resolved lazily at runtime from environment variables or .env files. agent_engine_id: The ID of the agent engine to use for the Memory Bank, e.g. '456' in 'projects/my-project/locations/us-central1/reasoningEngines/456'. To @@ -88,13 +91,15 @@ def __init__( be used. It will only be used if GOOGLE_GENAI_USE_VERTEXAI is true. Do not use Google AI Studio API key for this field. For more details, visit https://cloud.google.com/vertex-ai/generative-ai/docs/start/express-mode/overview + agents_dir: The directory containing agent configurations and .env files. + Used for lazy resolution of project/location when not explicitly provided. """ self._project = project self._location = location self._agent_engine_id = agent_engine_id - self._express_mode_api_key = get_express_mode_api_key( - project, location, express_mode_api_key - ) + self._agents_dir = agents_dir + self._config_resolved = False + self._express_mode_api_key = express_mode_api_key if agent_engine_id and '/' in agent_engine_id: logger.warning( @@ -168,6 +173,9 @@ async def _add_events_to_memory_from_events( @override async def search_memory(self, *, app_name: str, user_id: str, query: str): + # Lazily resolve project/location on first use + self._resolve_config() + if not self._agent_engine_id: raise ValueError('Agent Engine ID is required for Memory Bank.') @@ -203,7 +211,56 @@ async def search_memory(self, *, app_name: str, user_id: str, query: str): ) return SearchMemoryResponse(memories=memory_events) - def _get_api_client(self) -> vertexai.AsyncClient: + def _resolve_config(self) -> None: + """Lazily resolves project and location if not provided at initialization. + + This method is called on first use to resolve GCP configuration from: + 1. Explicit environment variables (highest priority) + 2. agents_dir root .env file + 3. Parent directory .env files (walking upward) + + Raises: + ValueError: If project or location cannot be resolved. + """ + if self._config_resolved: + return + + import os + + # If both are already set (either at init or via environment), we're done + if self._project and self._location: + self._config_resolved = True + self._express_mode_api_key = get_express_mode_api_key( + self._project, self._location, self._express_mode_api_key + ) + return + + # Try to load from environment and .env files + if self._agents_dir: + # Load from agents_dir root + from ..cli.utils import envs + envs.load_dotenv_for_agent("", self._agents_dir) + + # Resolve from environment after loading .env + self._project = self._project or os.environ.get("GOOGLE_CLOUD_PROJECT") + self._location = self._location or os.environ.get("GOOGLE_CLOUD_LOCATION") + + if not self._project or not self._location: + error_msg = ( + "GOOGLE_CLOUD_PROJECT and GOOGLE_CLOUD_LOCATION must be set. " + "You can set them via:\n" + " 1. Environment variables: GOOGLE_CLOUD_PROJECT and GOOGLE_CLOUD_LOCATION\n" + " 2. .env file in your agents_dir\n" + " 3. Use full resource name: agentengine://projects/{project}/locations/{location}/reasoningEngines/{id}" + ) + raise ValueError(error_msg) + + self._config_resolved = True + self._express_mode_api_key = get_express_mode_api_key( + self._project, self._location, self._express_mode_api_key + ) + + def _get_api_client(self): """Instantiates an API client for the given project and location. It needs to be instantiated inside each request so that the event loop diff --git a/src/google/adk/sessions/vertex_ai_session_service.py b/src/google/adk/sessions/vertex_ai_session_service.py index 1837a907eb..af0b0a7272 100644 --- a/src/google/adk/sessions/vertex_ai_session_service.py +++ b/src/google/adk/sessions/vertex_ai_session_service.py @@ -55,12 +55,15 @@ def __init__( agent_engine_id: Optional[str] = None, *, express_mode_api_key: Optional[str] = None, + agents_dir: Optional[str] = None, ): """Initializes the VertexAiSessionService. Args: - project: The project id of the project to use. - location: The location of the project to use. + project: The project id of the project to use. If not provided, will be + resolved lazily at runtime from environment variables or .env files. + location: The location of the project to use. If not provided, will be + resolved lazily at runtime from environment variables or .env files. agent_engine_id: The resource ID of the agent engine to use. express_mode_api_key: The API key to use for Express Mode. If not provided, the API key from the GOOGLE_API_KEY environment variable will @@ -68,13 +71,15 @@ def __init__( Do not use Google AI Studio API key for this field. For more details, visit https://cloud.google.com/vertex-ai/generative-ai/docs/start/express-mode/overview + agents_dir: The directory containing agent configurations and .env files. + Used for lazy resolution of project/location when not explicitly provided. """ self._project = project self._location = location self._agent_engine_id = agent_engine_id - self._express_mode_api_key = get_express_mode_api_key( - project, location, express_mode_api_key - ) + self._agents_dir = agents_dir + self._config_resolved = False + self._express_mode_api_key = express_mode_api_key @override async def create_session( @@ -100,6 +105,8 @@ async def create_session( Returns: The created session. """ + # Lazily resolve project/location on first use + self._resolve_config(app_name) if session_id: raise ValueError( @@ -139,6 +146,9 @@ async def get_session( session_id: str, config: Optional[GetSessionConfig] = None, ) -> Optional[Session]: + # Lazily resolve project/location on first use + self._resolve_config(app_name) + reasoning_engine_id = self._get_reasoning_engine_id(app_name) session_resource_name = ( f'reasoningEngines/{reasoning_engine_id}/sessions/{session_id}' @@ -203,6 +213,9 @@ async def get_session( async def list_sessions( self, *, app_name: str, user_id: Optional[str] = None ) -> ListSessionsResponse: + # Lazily resolve project/location on first use + self._resolve_config(app_name) + reasoning_engine_id = self._get_reasoning_engine_id(app_name) async with self._get_api_client() as api_client: @@ -231,6 +244,9 @@ async def list_sessions( async def delete_session( self, *, app_name: str, user_id: str, session_id: str ) -> None: + # Lazily resolve project/location on first use + self._resolve_config(app_name) + reasoning_engine_id = self._get_reasoning_engine_id(app_name) async with self._get_api_client() as api_client: @@ -249,6 +265,9 @@ async def append_event(self, session: Session, event: Event) -> Event: # Update the in-memory session. await super().append_event(session=session, event=event) + # Lazily resolve project/location on first use + self._resolve_config(session.app_name) + reasoning_engine_id = self._get_reasoning_engine_id(session.app_name) config = {} @@ -323,6 +342,64 @@ def _get_reasoning_engine_id(self, app_name: str): return match.groups()[-1] + def _resolve_config(self, app_name: Optional[str] = None) -> None: + """Lazily resolves project and location if not provided at initialization. + + This method is called on first use to resolve GCP configuration from: + 1. Explicit environment variables (highest priority) + 2. Agent-specific .env file (if app_name and agents_dir provided) + 3. agents_dir root .env file + 4. Parent directory .env files (walking upward) + + Args: + app_name: Optional app name to load agent-specific .env files. + + Raises: + ValueError: If project or location cannot be resolved. + """ + if self._config_resolved: + return + + import os + + # If both are already set (either at init or via environment), we're done + if self._project and self._location: + self._config_resolved = True + self._express_mode_api_key = get_express_mode_api_key( + self._project, self._location, self._express_mode_api_key + ) + return + + # Try to load from environment and .env files + if self._agents_dir and app_name: + # Load agent-specific .env + from ..cli.utils import envs + envs.load_dotenv_for_agent(app_name, self._agents_dir) + elif self._agents_dir: + # Load from agents_dir root + from ..cli.utils import envs + envs.load_dotenv_for_agent("", self._agents_dir) + + # Resolve from environment after loading .env + self._project = self._project or os.environ.get("GOOGLE_CLOUD_PROJECT") + self._location = self._location or os.environ.get("GOOGLE_CLOUD_LOCATION") + + if not self._project or not self._location: + error_msg = ( + "GOOGLE_CLOUD_PROJECT and GOOGLE_CLOUD_LOCATION must be set. " + "You can set them via:\n" + " 1. Environment variables: GOOGLE_CLOUD_PROJECT and GOOGLE_CLOUD_LOCATION\n" + " 2. .env file in your agent directory\n" + " 3. .env file in your agents_dir\n" + " 4. Use full resource name: agentengine://projects/{project}/locations/{location}/reasoningEngines/{id}" + ) + raise ValueError(error_msg) + + self._config_resolved = True + self._express_mode_api_key = get_express_mode_api_key( + self._project, self._location, self._express_mode_api_key + ) + def _api_client_http_options_override( self, ) -> Optional[Union[types.HttpOptions, types.HttpOptionsDict]]: