|
14 | 14 | from __future__ import annotations |
15 | 15 |
|
16 | 16 | from datetime import datetime |
| 17 | +import importlib |
17 | 18 | import json |
18 | 19 | import os |
19 | 20 | import shutil |
20 | 21 | import subprocess |
| 22 | +import sys |
| 23 | +import traceback |
21 | 24 | from typing import Final |
22 | 25 | from typing import Optional |
23 | 26 | import warnings |
@@ -465,6 +468,122 @@ def _validate_gcloud_extra_args( |
465 | 468 | ) |
466 | 469 |
|
467 | 470 |
|
| 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 | + |
468 | 587 | def _get_service_option_by_adk_version( |
469 | 588 | adk_version: str, |
470 | 589 | session_uri: Optional[str], |
@@ -702,6 +821,7 @@ def to_agent_engine( |
702 | 821 | requirements_file: Optional[str] = None, |
703 | 822 | env_file: Optional[str] = None, |
704 | 823 | agent_engine_config_file: Optional[str] = None, |
| 824 | + skip_agent_import_validation: bool = True, |
705 | 825 | ): |
706 | 826 | """Deploys an agent to Vertex AI Agent Engine. |
707 | 827 |
|
@@ -761,6 +881,11 @@ def to_agent_engine( |
761 | 881 | agent_engine_config_file (str): The filepath to the agent engine config file |
762 | 882 | to use. If not specified, the `.agent_engine_config.json` file in the |
763 | 883 | `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. |
764 | 889 | """ |
765 | 890 | app_name = os.path.basename(agent_folder) |
766 | 891 | display_name = display_name or app_name |
@@ -953,6 +1078,11 @@ def to_agent_engine( |
953 | 1078 | click.echo(f'Config agent detected: {config_root_agent_file}') |
954 | 1079 | is_config_agent = True |
955 | 1080 |
|
| 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 | + |
956 | 1086 | adk_app_file = os.path.join(temp_folder, f'{adk_app}.py') |
957 | 1087 | if adk_app_object == 'root_agent': |
958 | 1088 | adk_app_type = 'agent' |
|
0 commit comments