diff --git a/src/google/adk/cli/cli_deploy.py b/src/google/adk/cli/cli_deploy.py index bda96497b0..a0846dabc5 100644 --- a/src/google/adk/cli/cli_deploy.py +++ b/src/google/adk/cli/cli_deploy.py @@ -99,33 +99,7 @@ def _ensure_agent_engine_dependency(requirements_txt_path: str) -> None: EXPOSE {port} -CMD adk {command} --port={port} {host_option} {service_option} {trace_to_cloud_option} {otel_to_cloud_option} {allow_origins_option} {a2a_option} {trigger_sources_option} "/app/agents" -""" - -_AGENT_ENGINE_APP_TEMPLATE: Final[str] = """ -import os -import vertexai -from vertexai.agent_engines import AdkApp - -if {is_config_agent}: - from google.adk.agents import config_agent_utils - config_path = os.path.join(os.path.dirname(__file__), "root_agent.yaml") - root_agent = config_agent_utils.from_config(config_path) -else: - from .agent import {adk_app_object} - -if {express_mode}: # Whether or not to use Express Mode - vertexai.init(api_key=os.environ.get("GOOGLE_API_KEY")) -else: - vertexai.init( - project=os.environ.get("GOOGLE_CLOUD_PROJECT"), - location=os.environ.get("GOOGLE_CLOUD_LOCATION"), - ) - -adk_app = AdkApp( - {adk_app_type}={adk_app_object}, - enable_tracing={trace_to_cloud_option}, -) +CMD adk {command} --port={port} {host_option} {service_option} {trace_to_cloud_option} {otel_to_cloud_option} {allow_origins_option} {a2a_option} {trigger_sources_option} {gemini_enterprise_option} "/app/agents" """ _AGENT_ENGINE_CLASS_METHODS = [ @@ -830,7 +804,7 @@ def to_agent_engine( *, agent_folder: str, temp_folder: Optional[str] = None, - adk_app: str, + adk_app: Optional[str] = None, staging_bucket: Optional[str] = None, trace_to_cloud: Optional[bool] = None, otel_to_cloud: Optional[bool] = None, @@ -846,6 +820,9 @@ def to_agent_engine( env_file: Optional[str] = None, agent_engine_config_file: Optional[str] = None, skip_agent_import_validation: bool = True, + trigger_sources: Optional[str] = None, + artifact_service_uri: Optional[str] = None, + adk_version: Optional[str] = None, ): """Deploys an agent to Vertex AI Agent Engine. @@ -853,29 +830,16 @@ def to_agent_engine( - __init__.py - agent.py - - .py (optional, for customization; will be autogenerated otherwise) - requirements.txt (optional, for additional dependencies) - .env (optional, for environment variables) - ... (other required source files) - The contents of `adk_app` should look something like: - - ``` - from agent import - from vertexai.agent_engines import AdkApp - - adk_app = AdkApp( - agent=, # or `app=` - ) - ``` - Args: agent_folder (str): The folder (absolute path) containing the agent source code. temp_folder (str): The temp folder for the generated Agent Engine source files. It will be replaced with the generated files if it already exists. - adk_app (str): The name of the file (without .py) containing the AdkApp - instance. + adk_app (str): Deprecated. This argument is no longer required or used. staging_bucket (str): Deprecated. This argument is no longer required or used. trace_to_cloud (bool): Whether to enable Cloud Trace. @@ -884,13 +848,12 @@ def to_agent_engine( api_key (str): Optional. The API key to use for Express Mode. If not provided, the API key from the GOOGLE_API_KEY environment variable will be used. It will only be used if GOOGLE_GENAI_USE_VERTEXAI is true. - adk_app_object (str): Optional. The Python object corresponding to the root - ADK agent or app. Defaults to `root_agent` if not specified. + adk_app_object (str): Deprecated. This argument is no longer required or + used. agent_engine_id (str): Optional. The ID of the Agent Engine instance to update. If not specified, a new Agent Engine instance will be created. - absolutize_imports (bool): Optional. Default is True. Whether to absolutize - imports. If True, all relative imports will be converted to absolute - import statements. + absolutize_imports (bool): Deprecated. This argument is no longer required + or used. project (str): Optional. Google Cloud project id for the deployed agent. If not specified, the project from the `GOOGLE_CLOUD_PROJECT` environment variable will be used. It will be ignored if `api_key` is specified. @@ -899,9 +862,8 @@ def to_agent_engine( variable will be used. It will be ignored if `api_key` is specified. display_name (str): Optional. The display name of the Agent Engine. description (str): Optional. The description of the Agent Engine. - requirements_file (str): Optional. The filepath to the `requirements.txt` - file to use. If not specified, the `requirements.txt` file in the - `agent_folder` will be used. + requirements_file (str): Deprecated. This argument is no longer required or + used. env_file (str): Optional. The filepath to the `.env` file for environment variables. If not specified, the `.env` file in the `agent_folder` will be used. The values of `GOOGLE_CLOUD_PROJECT` and `GOOGLE_CLOUD_LOCATION` @@ -909,21 +871,32 @@ def to_agent_engine( agent_engine_config_file (str): The filepath to the agent engine config file to use. If not specified, the `.agent_engine_config.json` file in the `agent_folder` will be used. - skip_agent_import_validation (bool): Optional. Default is True. If True, - skip the pre-deployment import validation of `agent.py`. This can be - useful when the local environment does not have the same dependencies as - the deployment environment. + skip_agent_import_validation (bool): Deprecated. This argument is no longer + required or used. + trigger_sources (str): Optional. Comma-separated list of trigger sources to + enable (e.g., 'pubsub,eventarc'). Registers /trigger/* endpoints for + batch and event-driven agent invocations. + artifact_service_uri (str): Optional. The URI of the artifact service. + adk_version (str): Optional. The ADK version to use in Agent Engine + deployment. """ app_name = os.path.basename(agent_folder) display_name = display_name or app_name parent_folder = os.path.dirname(agent_folder) - adk_app_object = adk_app_object or 'root_agent' - if adk_app_object not in ['root_agent', 'app']: - click.echo( - f'Invalid adk_app_object: {adk_app_object}. Please use "root_agent"' - ' or "app".' + if adk_app_object: + warnings.warn( + 'WARNING: `--adk_app_object` is deprecated and will be removed in the' + ' future. Please drop it from the list of arguments.', + DeprecationWarning, + stacklevel=2, + ) + if adk_app: + warnings.warn( + 'WARNING: `adk_app` is deprecated and will be removed in a future' + ' release. Please drop it from the list of arguments.', + DeprecationWarning, + stacklevel=2, ) - return if staging_bucket: warnings.warn( 'WARNING: `staging_bucket` is deprecated and will be removed in a' @@ -966,6 +939,7 @@ def to_agent_engine( ignore=ignore_patterns, dirs_exist_ok=True, ) + os.chdir(agent_src_path) click.echo('Copying agent source code complete.') project = _resolve_project(project) @@ -1002,30 +976,13 @@ def to_agent_engine( ) agent_config['description'] = description - requirements_txt_path = os.path.join(agent_src_path, 'requirements.txt') if requirements_file: - if os.path.exists(requirements_txt_path): - click.echo( - f'Overwriting {requirements_txt_path} with {requirements_file}' - ) - shutil.copyfile(requirements_file, requirements_txt_path) - elif 'requirements_file' in agent_config: - if os.path.exists(requirements_txt_path): - click.echo( - f'Overwriting {requirements_txt_path} with' - f' {agent_config["requirements_file"]}' - ) - shutil.copyfile(agent_config['requirements_file'], requirements_txt_path) - else: - # Attempt to read requirements from requirements.txt in the dir (if any). - if not os.path.exists(requirements_txt_path): - click.echo(f'Creating {requirements_txt_path}...') - with open(requirements_txt_path, 'w', encoding='utf-8') as f: - f.write(_AGENT_ENGINE_REQUIREMENT + '\n') - click.echo(f'Created {requirements_txt_path}') - _ensure_agent_engine_dependency(requirements_txt_path) - agent_config['requirements_file'] = f'{temp_folder}/requirements.txt' - + warnings.warn( + 'WARNING: `--requirements_file` is deprecated and will be removed in the' + ' future. Please drop it from the list of arguments.', + DeprecationWarning, + stacklevel=2, + ) env_vars = {} if not env_file: # Attempt to read the env variables from .env in the dir (if any). @@ -1094,87 +1051,98 @@ def to_agent_engine( from ..utils._google_client_headers import get_tracking_headers + http_options = {'headers': get_tracking_headers()} if project and region: - click.echo('Initializing Vertex AI...') + click.echo('Initializing Client with project and region...') client = vertexai.Client( project=project, location=region, - http_options={'headers': get_tracking_headers()}, + http_options=http_options, ) elif api_key: - click.echo('Initializing Vertex AI in Express Mode with API key...') - client = vertexai.Client( - api_key=api_key, http_options={'headers': get_tracking_headers()} - ) + click.echo('Initializing Client with Express Mode API key...') + client = vertexai.Client(api_key=api_key, http_options=http_options) else: click.echo( 'No project/region or api_key provided. ' 'Please specify either project/region or api_key.' ) return - click.echo('Vertex AI initialized.') - - is_config_agent = False - config_root_agent_file = os.path.join(agent_src_path, 'root_agent.yaml') - if os.path.exists(config_root_agent_file): - click.echo(f'Config agent detected: {config_root_agent_file}') - is_config_agent = True - - # Validate that the agent module can be imported before deployment. - if not skip_agent_import_validation: - click.echo('Validating agent module...') - _validate_agent_import(agent_src_path, adk_app_object, is_config_agent) - - adk_app_file = os.path.join(temp_folder, f'{adk_app}.py') - if adk_app_object == 'root_agent': - adk_app_type = 'agent' - elif adk_app_object == 'app': - adk_app_type = 'app' - else: - click.echo( - f'Invalid adk_app_object: {adk_app_object}. Please use "root_agent"' - ' or "app".' + + if skip_agent_import_validation: + warnings.warn( + 'WARNING: `--skip-agent-import-validation` is deprecated and will be' + ' removed in the future. Please drop it from the list of arguments.', + DeprecationWarning, + stacklevel=2, ) - return - with open(adk_app_file, 'w', encoding='utf-8') as f: - f.write( - _AGENT_ENGINE_APP_TEMPLATE.format( - app_name=app_name, - trace_to_cloud_option=trace_to_cloud, - is_config_agent=is_config_agent, - agent_folder=f'./{temp_folder}', - adk_app_object=adk_app_object, - adk_app_type=adk_app_type, - express_mode=api_key is not None, - ) + click.echo('Creating Dockerfile...') + requirements_txt_path = os.path.join(agent_src_path, 'requirements.txt') + install_agent_deps = ( + f'RUN pip install -r "/app/agents/{app_name}/requirements.txt"' + if os.path.exists(requirements_txt_path) + else '# No requirements.txt found.' + ) + trigger_sources_option = ( + f'--trigger_sources={trigger_sources}' if trigger_sources else '' + ) + def create_dockerfile_for_agent_engine(resource_name: str): + agent_engine_uri = f'agentengine://{resource_name}' + dockerfile_content = _DOCKERFILE_TEMPLATE.format( + gcp_project_id=project, + gcp_region=region, + app_name=app_name, + port=8080, + command='api_server', + install_agent_deps=install_agent_deps, + service_option=_get_service_option_by_adk_version( + adk_version, + agent_engine_uri, # session_service_uri + artifact_service_uri, + agent_engine_uri, # memory_service_uri + False, # use_local_storage + ), + trace_to_cloud_option='--trace_to_cloud' if trace_to_cloud else '', + otel_to_cloud_option='--otel_to_cloud' if otel_to_cloud else '', + allow_origins_option='', # Not supported for now. + adk_version=adk_version, + host_option='--host=0.0.0.0', + a2a_option='--a2a', + trigger_sources_option=trigger_sources_option, + gemini_enterprise_option=f'--gemini_enterprise_app_name={app_name}', ) - click.echo(f'Created {adk_app_file}') - click.echo('Files and dependencies resolved') - if absolutize_imports: + dockerfile_path = os.path.join(temp_folder, 'Dockerfile') + os.makedirs(temp_folder, exist_ok=True) + with open(dockerfile_path, 'w', encoding='utf-8') as f: + f.write(dockerfile_content) click.echo( - 'Agent Engine deployments have switched to source-based deployment, ' - 'so it is no longer necessary to absolutize imports.' + f'Creating Dockerfile complete. {os.path.abspath(dockerfile_path)}' + ) + + if absolutize_imports: + warnings.warn( + 'WARNING: `--absolutize_imports` is deprecated and will be removed' + ' in the future. Please drop it from the list of arguments.', + DeprecationWarning, + stacklevel=2, ) click.echo('Deploying to agent engine...') - agent_config['entrypoint_module'] = f'{temp_folder}.{adk_app}' - agent_config['entrypoint_object'] = 'adk_app' - agent_config['source_packages'] = [temp_folder] + agent_config['source_packages'] = ['.'] + agent_config['image_spec'] = {} # Use the Dockerfile agent_config['class_methods'] = _AGENT_ENGINE_CLASS_METHODS agent_config['agent_framework'] = 'google-adk' if not agent_engine_id: - agent_engine = client.agent_engines.create(config=agent_config) - click.secho( - f'✅ Created agent engine: {agent_engine.api_resource.name}', - fg='green', - ) - _print_agent_engine_url(agent_engine.api_resource.name) - else: - if project and region and not agent_engine_id.startswith('projects/'): - agent_engine_id = f'projects/{project}/locations/{region}/reasoningEngines/{agent_engine_id}' - client.agent_engines.update(name=agent_engine_id, config=agent_config) - click.secho(f'✅ Updated agent engine: {agent_engine_id}', fg='green') + agent_engine = client.agent_engines.create() + agent_engine_id = agent_engine.api_resource.name + click.secho(f'✅ Created agent engine: {agent_engine_id}', fg='green') _print_agent_engine_url(agent_engine_id) + elif project and region and not agent_engine_id.startswith('projects/'): + agent_engine_id = f'projects/{project}/locations/{region}/reasoningEngines/{agent_engine_id}' + create_dockerfile_for_agent_engine(agent_engine_id) + client.agent_engines.update(name=agent_engine_id, config=agent_config) + click.secho(f'✅ Updated agent engine: {agent_engine_id}', fg='green') + _print_agent_engine_url(agent_engine_id) finally: click.echo(f'Cleaning up the temp folder: {temp_folder}') shutil.rmtree(agent_src_path) diff --git a/src/google/adk/cli/cli_tools_click.py b/src/google/adk/cli/cli_tools_click.py index 07ccc15892..041a920a56 100644 --- a/src/google/adk/cli/cli_tools_click.py +++ b/src/google/adk/cli/cli_tools_click.py @@ -1720,6 +1720,15 @@ async def _lifespan(app: FastAPI): "Automatically create a session if it doesn't exist when calling /run." ), ) +@click.option( + "--gemini_enterprise_app_name", + type=str, + default=None, + help=( + "The app_name to register with Gemini Enterprise via" + " https://docs.cloud.google.com/gemini/enterprise/docs/register-and-manage-an-adk-agent" + ), +) def cli_api_server( agents_dir: str, eval_storage_uri: Optional[str] = None, @@ -1742,6 +1751,7 @@ def cli_api_server( extra_plugins: Optional[list[str]] = None, auto_create_session: bool = False, trigger_sources: Optional[list[str]] = None, + gemini_enterprise_app_name: Optional[str] = None, ): """Starts a FastAPI server for agents. @@ -1776,6 +1786,7 @@ def cli_api_server( extra_plugins=extra_plugins, auto_create_session=auto_create_session, trigger_sources=trigger_sources, + gemini_enterprise_app_name=gemini_enterprise_app_name, ), host=host, port=port, @@ -2173,10 +2184,7 @@ def cli_migrate_session( "--adk_app", type=str, default="agent_engine_app", - help=( - "Optional. Python file for defining the ADK application" - " (default: a file named agent_engine_app.py)" - ), + help=" NOTE: This flag is deprecated and will be removed in the future.", ) @click.option( "--temp_folder", @@ -2192,29 +2200,19 @@ def cli_migrate_session( "--adk_app_object", type=str, default=None, - help=( - "Optional. Python object corresponding to the root ADK agent or app." - " It can only be `root_agent` or `app`. (default: `root_agent`)" - ), + help=" NOTE: This flag is deprecated and will be removed in the future.", ) @click.option( "--env_file", type=str, default="", - help=( - "Optional. The filepath to the `.env` file for environment variables." - " (default: the `.env` file in the `agent` directory, if any.)" - ), + help=" NOTE: This flag is deprecated and will be removed in the future.", ) @click.option( "--requirements_file", type=str, default="", - help=( - "Optional. The filepath to the `requirements.txt` file to use." - " (default: the `requirements.txt` file in the `agent` directory, if" - " any.)" - ), + help=" NOTE: This flag is deprecated and will be removed in the future.", ) @click.option( "--absolutize_imports", @@ -2236,22 +2234,51 @@ def cli_migrate_session( @click.option( "--validate-agent-import/--no-validate-agent-import", default=False, - help=( - "Optional. Validate that the agent module can be imported before" - " deployment. This requires your local environment to have the same" - " dependencies as the deployment environment. (default: disabled)" - ), + help=" NOTE: This flag is deprecated and will be removed in the future.", ) @click.option( "--skip-agent-import-validation", "skip_agent_import_validation_alias", is_flag=True, default=False, + help=" NOTE: This flag is deprecated and will be removed in the future.", +) +# Kept as raw str (not parsed to list) — interpolated directly into Dockerfile CMD. +@click.option( + "--trigger_sources", + type=str, + help=( + "Optional. Comma-separated list of trigger sources to enable" + " (e.g., 'pubsub,eventarc'). Registers /trigger/* endpoints" + " for batch and event-driven agent invocations." + ), + default=None, +) +@click.option( + "--adk_version", + type=str, + default=version.__version__, + show_default=True, help=( - "Optional. Skip pre-deployment import validation of `agent.py`. This is" - " the default; use --validate-agent-import to enable validation." + "Optional. The ADK version used in Agent Engine deployment. (default: " + " the version in the dev environment)" ), ) +@click.option( + "--artifact_service_uri", + type=str, + help=textwrap.dedent( + """\ + Optional. The URI of the artifact service. If set, ADK uses this service. + + \b + If unset, ADK chooses a default artifact service. + - Use 'gs://' to connect to the GCS artifact service. + - Use 'memory://' to force the in-memory artifact service. + - Use 'file://' to store artifacts in a custom local directory.""" + ), + default=None, +) @click.argument( "agent", type=click.Path( @@ -2276,8 +2303,11 @@ def cli_deploy_agent_engine( requirements_file: str, absolutize_imports: bool, agent_engine_config_file: str, + adk_version: str, validate_agent_import: bool = False, skip_agent_import_validation_alias: bool = False, + trigger_sources: Optional[str] = None, + artifact_service_uri: Optional[str] = None, ): """Deploys an agent to Agent Engine. @@ -2317,6 +2347,9 @@ def cli_deploy_agent_engine( absolutize_imports=absolutize_imports, agent_engine_config_file=agent_engine_config_file, skip_agent_import_validation=not validate_agent_import, + trigger_sources=trigger_sources, + artifact_service_uri=artifact_service_uri, + adk_version=adk_version, ) except Exception as e: click.secho(f"Deploy failed: {e}", fg="red", err=True) diff --git a/src/google/adk/cli/fast_api.py b/src/google/adk/cli/fast_api.py index 199791f7da..4747e9ad20 100644 --- a/src/google/adk/cli/fast_api.py +++ b/src/google/adk/cli/fast_api.py @@ -97,6 +97,7 @@ def get_fast_api_app( logo_image_url: Optional[str] = None, auto_create_session: bool = False, trigger_sources: Optional[list[Literal["pubsub", "eventarc"]]] = None, + gemini_enterprise_app_name: Optional[str] = None, ) -> FastAPI: """Constructs and returns a FastAPI application for serving ADK agents. @@ -143,6 +144,8 @@ def get_fast_api_app( trigger_sources: List of trigger sources to enable (e.g. ["pubsub", "eventarc"]). When set, registers /trigger/* endpoints for batch and event-driven agent invocations. None disables all trigger endpoints. + gemini_enterprise_app_name: The app_name to register with Gemini Enterprise + via https://docs.cloud.google.com/gemini/enterprise/docs/register-and-manage-an-adk-agent Returns: The configured FastAPI application instance. @@ -650,5 +653,102 @@ async def _get_a2a_runner_async() -> Runner: except Exception as e: logger.error("Failed to setup A2A agent %s: %s", app_name, e) # Continue with other agents even if one fails + if gemini_enterprise_app_name: + if gemini_enterprise_app_name not in agent_loader.list_agents(): + raise ValueError( + f"App {gemini_enterprise_app_name} not found in dir: {agents_dir}" + ) + + import inspect + import json + import vertexai + from fastapi import encoders, responses + from pydantic import BaseModel + from vertexai import agent_engines + from google.adk.agents import Agent + from google.adk.utils._google_client_headers import get_tracking_headers + + class QueryRequest(BaseModel): + input: dict | None = None + class_method: str | None = None + + project = os.environ.get("GOOGLE_CLOUD_PROJECT", None) + location = os.environ.get( + "GOOGLE_CLOUD_AGENT_ENGINE_LOCATION", + os.environ.get("GOOGLE_CLOUD_LOCATION", None), + ) + api_key = os.environ.get("GOOGLE_API_KEY", None) + if project: + vertexai.init( + project=project, + location=location, + http_options={"headers": get_tracking_headers()}, + ) + elif api_key: + vertexai.init( + api_key=api_key, + http_options={"headers": get_tracking_headers()}, + ) + else: + raise ValueError( + "No GOOGLE_CLOUD_PROJECT or GOOGLE_API_KEY found in environment" + " variables." + ) + # The tmp agent will be replaced by the adk server's runner and services. + adk_app = agent_engines.AdkApp(agent=Agent(name="tmp")) + adk_app._tmpl_attrs["runner"] = ( + adk_web_server.get_runner_async(app_name=gemini_enterprise_app_name) + ) + adk_app._tmpl_attrs["app_name"] = gemini_enterprise_app_name + adk_app._tmpl_attrs["session_service"] = session_service + adk_app._tmpl_attrs["memory_service"] = memory_service + adk_app._tmpl_attrs["artifact_service"] = artifact_service + + def _encode_chunk_to_json(chunk): + """Encodes a chunk to a JSON string with a newline.""" + try: + json_chunk = encoders.jsonable_encoder(chunk) + return json.dumps(json_chunk) + "\n" + except Exception: + logging.exception("Failed to encode chunk") + return None + + async def json_generator(output): + async for chunk in output: + encoded_chunk = _encode_chunk_to_json(chunk) + if encoded_chunk is None: + break + yield encoded_chunk + + async def _invoke_callable_or_raise(invocation_callable, invocation_payload): + if inspect.iscoroutinefunction(invocation_callable): + return await invocation_callable(**invocation_payload) + else: + return invocation_callable(**invocation_payload) + + @app.post("/api/reasoning_engine") + async def query(request: QueryRequest) -> responses.JSONResponse: + method = getattr(adk_app, request.class_method) + output = await _invoke_callable_or_raise(method, request.input or {}) + + try: + json_serialized_content = encoders.jsonable_encoder({"output": output}) + except ValueError as encoding_error: + logging.exception( + "FastAPI could not JSON-encode the response from invocation method" + " %s. Error: %s. Invocation method's original response: %r", + request.class_method, encoding_error, output, + ) + raise encoding_error + return responses.JSONResponse(content=json_serialized_content) + + @app.post("/api/stream_reasoning_engine") + async def stream_query(request: QueryRequest) -> responses.StreamingResponse: + method = getattr(adk_app, request.class_method) + output = await _invoke_callable_or_raise(method, request.input or {}) + return responses.StreamingResponse( + content=json_generator(output), + media_type="application/json", + ) return app