Skip to content

Commit 334018d

Browse files
committed
Merge branch 'fix/litellm-streaming-content-duplication' of https://github.com/thesynapses/adk-python into fix/litellm-streaming-content-duplication
2 parents 20b3c19 + b4f1c7f commit 334018d

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)