Skip to content

Commit 2ac468e

Browse files
GWealecopybara-github
authored andcommitted
fix: Add pre-deployment validation for agent module imports
This change introduces a new function, _validate_agent_import, which attempts to import the user's agent.py file before the deployment process begins. This helps catch common issues such as missing dependencies, incorrect relative imports, or syntax errors in the agent code early, providing more informative error messages to the user. Co-authored-by: George Weale <gweale@google.com> PiperOrigin-RevId: 863310033
1 parent 00aba2d commit 2ac468e

File tree

3 files changed

+406
-2
lines changed

3 files changed

+406
-2
lines changed

src/google/adk/cli/cli_deploy.py

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,13 @@
1414
from __future__ import annotations
1515

1616
from datetime import datetime
17+
import importlib
1718
import json
1819
import os
1920
import shutil
2021
import subprocess
22+
import sys
23+
import traceback
2124
from typing import Final
2225
from typing import Optional
2326
import warnings
@@ -465,6 +468,122 @@ def _validate_gcloud_extra_args(
465468
)
466469

467470

471+
def _validate_agent_import(
472+
agent_src_path: str,
473+
adk_app_object: str,
474+
is_config_agent: bool,
475+
) -> None:
476+
"""Validates that the agent module can be imported successfully.
477+
478+
This pre-deployment validation catches common issues like missing
479+
dependencies or import errors in custom BaseLlm implementations before
480+
the agent is deployed to Agent Engine. This provides clearer error
481+
messages and prevents deployments that would fail at runtime.
482+
483+
Args:
484+
agent_src_path: Path to the staged agent source code.
485+
adk_app_object: The Python object name to import ('root_agent' or 'app').
486+
is_config_agent: Whether this is a config-based agent.
487+
488+
Raises:
489+
click.ClickException: If the agent module cannot be imported.
490+
"""
491+
if is_config_agent:
492+
# Config agents are loaded from YAML, skip Python import validation
493+
return
494+
495+
agent_module_path = os.path.join(agent_src_path, 'agent.py')
496+
if not os.path.exists(agent_module_path):
497+
raise click.ClickException(
498+
f'Agent module not found at {agent_module_path}. '
499+
'Please ensure your agent folder contains an agent.py file.'
500+
)
501+
502+
# Add the parent directory to sys.path temporarily for import resolution
503+
parent_dir = os.path.dirname(agent_src_path)
504+
module_name = os.path.basename(agent_src_path)
505+
506+
original_sys_path = sys.path.copy()
507+
original_sys_modules_keys = set(sys.modules.keys())
508+
try:
509+
# Add parent directory to path so imports work correctly
510+
if parent_dir not in sys.path:
511+
sys.path.insert(0, parent_dir)
512+
try:
513+
module = importlib.import_module(f'{module_name}.agent')
514+
except ImportError as e:
515+
error_msg = str(e)
516+
tb = traceback.format_exc()
517+
518+
# Check for common issues
519+
if 'BaseLlm' in tb or 'base_llm' in tb.lower():
520+
raise click.ClickException(
521+
'Failed to import agent module due to a BaseLlm-related error:\n'
522+
f'{error_msg}\n\n'
523+
'This error often occurs when deploying agents with custom LLM '
524+
'implementations. Please ensure:\n'
525+
'1. All custom LLM classes are defined in files within your agent '
526+
'folder\n'
527+
'2. All required dependencies are listed in requirements.txt\n'
528+
'3. Import paths use relative imports (e.g., "from .my_llm import '
529+
'MyLlm")\n'
530+
'4. Your custom BaseLlm class and its dependencies are installed\n'
531+
'\n'
532+
'If this failure is expected (e.g., missing local dependencies), '
533+
'disable agent import validation by omitting '
534+
'--validate-agent-import (default) or passing '
535+
'--skip-agent-import-validation (or --no-validate-agent-import).'
536+
) from e
537+
else:
538+
raise click.ClickException(
539+
f'Failed to import agent module:\n{error_msg}\n\n'
540+
'Please ensure all dependencies are listed in requirements.txt '
541+
'and all imports are resolvable.\n\n'
542+
f'Full traceback:\n{tb}\n\n'
543+
'If this failure is expected (e.g., missing local dependencies), '
544+
'disable agent import validation by omitting '
545+
'--validate-agent-import (default) or passing '
546+
'--skip-agent-import-validation (or --no-validate-agent-import).'
547+
) from e
548+
except Exception as e:
549+
tb = traceback.format_exc()
550+
raise click.ClickException(
551+
f'Error while loading agent module:\n{e}\n\n'
552+
'Please check your agent code for errors.\n\n'
553+
f'Full traceback:\n{tb}\n\n'
554+
'If this failure is expected (e.g., missing local dependencies), '
555+
'disable agent import validation by omitting '
556+
'--validate-agent-import (default) or passing '
557+
'--skip-agent-import-validation (or --no-validate-agent-import).'
558+
) from e
559+
560+
# Check that the expected object exists
561+
if not hasattr(module, adk_app_object):
562+
available_attrs = [
563+
attr for attr in dir(module) if not attr.startswith('_')
564+
]
565+
raise click.ClickException(
566+
f"Agent module does not export '{adk_app_object}'. "
567+
f'Available exports: {available_attrs}\n\n'
568+
'Please ensure your agent.py exports either "root_agent" or "app".'
569+
)
570+
571+
click.echo(
572+
'Agent module validation successful: '
573+
f'found "{adk_app_object}" in agent.py'
574+
)
575+
576+
finally:
577+
# Restore original sys.path
578+
sys.path[:] = original_sys_path
579+
# Clean up modules introduced by validation.
580+
for key in list(sys.modules.keys()):
581+
if key in original_sys_modules_keys:
582+
continue
583+
if key == module_name or key.startswith(f'{module_name}.'):
584+
sys.modules.pop(key, None)
585+
586+
468587
def _get_service_option_by_adk_version(
469588
adk_version: str,
470589
session_uri: Optional[str],
@@ -702,6 +821,7 @@ def to_agent_engine(
702821
requirements_file: Optional[str] = None,
703822
env_file: Optional[str] = None,
704823
agent_engine_config_file: Optional[str] = None,
824+
skip_agent_import_validation: bool = True,
705825
):
706826
"""Deploys an agent to Vertex AI Agent Engine.
707827
@@ -761,6 +881,11 @@ def to_agent_engine(
761881
agent_engine_config_file (str): The filepath to the agent engine config file
762882
to use. If not specified, the `.agent_engine_config.json` file in the
763883
`agent_folder` will be used.
884+
skip_agent_import_validation (bool): Optional. Default is True. If True,
885+
skip the
886+
pre-deployment import validation of `agent.py`. This can be useful when
887+
the local environment does not have the same dependencies as the
888+
deployment environment.
764889
"""
765890
app_name = os.path.basename(agent_folder)
766891
display_name = display_name or app_name
@@ -953,6 +1078,11 @@ def to_agent_engine(
9531078
click.echo(f'Config agent detected: {config_root_agent_file}')
9541079
is_config_agent = True
9551080

1081+
# Validate that the agent module can be imported before deployment.
1082+
if not skip_agent_import_validation:
1083+
click.echo('Validating agent module...')
1084+
_validate_agent_import(agent_src_path, adk_app_object, is_config_agent)
1085+
9561086
adk_app_file = os.path.join(temp_folder, f'{adk_app}.py')
9571087
if adk_app_object == 'root_agent':
9581088
adk_app_type = 'agent'

src/google/adk/cli/cli_tools_click.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1867,6 +1867,25 @@ def cli_migrate_session(
18671867
" directory, if any.)"
18681868
),
18691869
)
1870+
@click.option(
1871+
"--validate-agent-import/--no-validate-agent-import",
1872+
default=False,
1873+
help=(
1874+
"Optional. Validate that the agent module can be imported before"
1875+
" deployment. This requires your local environment to have the same"
1876+
" dependencies as the deployment environment. (default: disabled)"
1877+
),
1878+
)
1879+
@click.option(
1880+
"--skip-agent-import-validation",
1881+
"skip_agent_import_validation_alias",
1882+
is_flag=True,
1883+
default=False,
1884+
help=(
1885+
"Optional. Skip pre-deployment import validation of `agent.py`. This is"
1886+
" the default; use --validate-agent-import to enable validation."
1887+
),
1888+
)
18701889
@click.argument(
18711890
"agent",
18721891
type=click.Path(
@@ -1891,6 +1910,8 @@ def cli_deploy_agent_engine(
18911910
requirements_file: str,
18921911
absolutize_imports: bool,
18931912
agent_engine_config_file: str,
1913+
validate_agent_import: bool = False,
1914+
skip_agent_import_validation_alias: bool = False,
18941915
):
18951916
"""Deploys an agent to Agent Engine.
18961917
@@ -1905,6 +1926,11 @@ def cli_deploy_agent_engine(
19051926
"""
19061927
logging.getLogger("vertexai_genai.agentengines").setLevel(logging.INFO)
19071928
try:
1929+
if validate_agent_import and skip_agent_import_validation_alias:
1930+
raise click.UsageError(
1931+
"Do not pass both --validate-agent-import and"
1932+
" --skip-agent-import-validation."
1933+
)
19081934
cli_deploy.to_agent_engine(
19091935
agent_folder=agent,
19101936
project=project,
@@ -1922,6 +1948,7 @@ def cli_deploy_agent_engine(
19221948
requirements_file=requirements_file,
19231949
absolutize_imports=absolutize_imports,
19241950
agent_engine_config_file=agent_engine_config_file,
1951+
skip_agent_import_validation=not validate_agent_import,
19251952
)
19261953
except Exception as e:
19271954
click.secho(f"Deploy failed: {e}", fg="red", err=True)

0 commit comments

Comments
 (0)