Skip to content

Commit 8806dc2

Browse files
GWealecopybara-github
authored andcommitted
perf: improve adk import loading
Importing google.adk eagerly pulled in Agent, Runner, Workflow, and the server and CLI runtimes even for callers that used none of them, and google-genai imported the MCP client and FastMCP server stack whenever MCP happened to be installed. The package, agents, workflow, cli, and cli.utils namespaces now resolve their exports lazily on first use (PEP 562) through a shared google.adk.utils._lazy helper. Importing google.adk drops from roughly 2.1s to a few ms. Public APIs and object identities are unchanged, with two things to note when upgrading: * The google-genai floor moves from 2.9 to 2.12.1, the release that defers MCP itself. Environments pinned below 2.12.1 will fail to resolve. * google.adk.cli.utils no longer re-exports BaseAgent and LlmAgent. They were unused eager imports, never part of that module's __all__; import them from google.adk.agents instead. Lazy resolution moves failures from import time to first use, so a missing or broken optional dependency now surfaces on the first request rather than at process start. Long-running servers pay the one-time resolution cost on their first request; a warmup hook is deliberately left to a follow-up so this change adds no public API. Co-authored-by: George Weale <gweale@google.com> PiperOrigin-RevId: 956721154
1 parent b0f52f0 commit 8806dc2

13 files changed

Lines changed: 515 additions & 118 deletions

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ dependencies = [
3737
"click>=8.1.8,<9",
3838
"fastapi>=0.133,<1",
3939
"google-auth[pyopenssl]>=2.47",
40-
"google-genai>=2.9,<3",
40+
"google-genai>=2.12.1,<3",
4141
"graphviz>=0.20.2,<1",
4242
"httpx>=0.27,<1",
4343
"jsonschema>=4.23,<5",

src/google/adk/__init__.py

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,26 @@
1414

1515
from __future__ import annotations
1616

17+
from typing import TYPE_CHECKING
18+
1719
from . import version
18-
from .agents.context import Context
19-
from .agents.llm_agent import Agent
20-
from .events.event import Event
21-
from .runners import Runner
22-
from .workflow import Workflow
20+
from .utils import _lazy
21+
22+
if TYPE_CHECKING:
23+
from .agents.context import Context
24+
from .agents.llm_agent import Agent
25+
from .events.event import Event
26+
from .runners import Runner
27+
from .workflow import Workflow
2328

2429
__version__ = version.__version__
25-
__all__ = ["Agent", "Context", "Event", "Runner", "Workflow"]
30+
_LAZY_MEMBERS: dict[str, str] = {
31+
'Agent': '.agents.llm_agent',
32+
'Context': '.agents.context',
33+
'Event': '.events.event',
34+
'Runner': '.runners',
35+
'Workflow': '.workflow',
36+
}
37+
__all__ = ['Agent', 'Context', 'Event', 'Runner', 'Workflow']
38+
39+
__getattr__, __dir__ = _lazy.accessors(globals(), _LAZY_MEMBERS)

src/google/adk/agents/__init__.py

Lines changed: 40 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -12,31 +12,52 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
import importlib
16-
from typing import Any
15+
from __future__ import annotations
16+
1717
from typing import TYPE_CHECKING
1818

19-
from .base_agent import BaseAgent
20-
from .base_agent_config import BaseAgentConfig
21-
from .context import Context
22-
from .invocation_context import InvocationContext
23-
from .live_request_queue import LiveRequest
24-
from .live_request_queue import LiveRequestQueue
25-
from .llm_agent import Agent
26-
from .llm_agent import LlmAgent
27-
from .llm_agent_config import LlmAgentConfig
28-
from .loop_agent import LoopAgent
29-
from .loop_agent_config import LoopAgentConfig
30-
from .parallel_agent import ParallelAgent
31-
from .parallel_agent_config import ParallelAgentConfig
32-
from .run_config import RunConfig
33-
from .sequential_agent import SequentialAgent
34-
from .sequential_agent_config import SequentialAgentConfig
19+
from ..utils import _lazy
3520

3621
if TYPE_CHECKING:
3722
from ._managed_agent import ManagedAgent
23+
from .base_agent import BaseAgent
24+
from .base_agent_config import BaseAgentConfig
25+
from .context import Context
26+
from .invocation_context import InvocationContext
27+
from .live_request_queue import LiveRequest
28+
from .live_request_queue import LiveRequestQueue
29+
from .llm_agent import Agent
30+
from .llm_agent import LlmAgent
31+
from .llm_agent_config import LlmAgentConfig
32+
from .loop_agent import LoopAgent
33+
from .loop_agent_config import LoopAgentConfig
3834
from .mcp_instruction_provider import McpInstructionProvider
35+
from .parallel_agent import ParallelAgent
36+
from .parallel_agent_config import ParallelAgentConfig
37+
from .run_config import RunConfig
38+
from .sequential_agent import SequentialAgent
39+
from .sequential_agent_config import SequentialAgentConfig
3940

41+
_LAZY_MEMBERS: dict[str, str] = {
42+
'Agent': '.llm_agent',
43+
'BaseAgent': '.base_agent',
44+
'BaseAgentConfig': '.base_agent_config',
45+
'Context': '.context',
46+
'InvocationContext': '.invocation_context',
47+
'LiveRequest': '.live_request_queue',
48+
'LiveRequestQueue': '.live_request_queue',
49+
'LlmAgent': '.llm_agent',
50+
'LlmAgentConfig': '.llm_agent_config',
51+
'LoopAgent': '.loop_agent',
52+
'LoopAgentConfig': '.loop_agent_config',
53+
'ManagedAgent': '._managed_agent',
54+
'McpInstructionProvider': '.mcp_instruction_provider',
55+
'ParallelAgent': '.parallel_agent',
56+
'ParallelAgentConfig': '.parallel_agent_config',
57+
'RunConfig': '.run_config',
58+
'SequentialAgent': '.sequential_agent',
59+
'SequentialAgentConfig': '.sequential_agent_config',
60+
}
4061
__all__ = [
4162
'Agent',
4263
'BaseAgent',
@@ -58,21 +79,4 @@
5879
'SequentialAgentConfig',
5980
]
6081

61-
62-
_LAZY_ATTRS = {
63-
'ManagedAgent': '._managed_agent',
64-
'McpInstructionProvider': '.mcp_instruction_provider',
65-
}
66-
67-
68-
def __getattr__(name: str) -> Any:
69-
if name in _LAZY_ATTRS:
70-
module = importlib.import_module(_LAZY_ATTRS[name], __name__)
71-
attr = getattr(module, name)
72-
globals()[name] = attr
73-
return attr
74-
raise AttributeError(f'module {__name__!r} has no attribute {name!r}')
75-
76-
77-
def __dir__() -> list[str]:
78-
return list(globals().keys()) + __all__
82+
__getattr__, __dir__ = _lazy.accessors(globals(), _LAZY_MEMBERS)

src/google/adk/cli/__init__.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,18 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
from .cli_tools_click import main
15+
from __future__ import annotations
16+
17+
from typing import TYPE_CHECKING
18+
19+
from ..utils import _lazy
20+
21+
if TYPE_CHECKING:
22+
from .cli_tools_click import main
23+
24+
_LAZY_MEMBERS: dict[str, str] = {
25+
'main': '.cli_tools_click',
26+
}
27+
__all__ = ['main']
28+
29+
__getattr__, __dir__ = _lazy.accessors(globals(), _LAZY_MEMBERS)

src/google/adk/cli/cli_tools_click.py

Lines changed: 47 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -35,29 +35,56 @@
3535

3636
import click
3737
from click.core import ParameterSource
38-
from fastapi import FastAPI
39-
import uvicorn
4038

4139
from .. import version
42-
from ..agents.run_config import StreamingMode
43-
from ..evaluation.constants import MISSING_EVAL_DEPENDENCIES_MESSAGE
4440
from ..features import FeatureName
4541
from ..features import override_feature_enabled
4642
from ..utils._telemetry_config import read_telemetry_consent
4743
from ..utils._telemetry_config import write_telemetry_consent
4844
from ._telemetry._metrics_collector import MetricsCollector
49-
from .cli import run_cli
5045
from .utils import envs
5146
from .utils import logs
5247

5348
if TYPE_CHECKING:
49+
from fastapi import FastAPI
50+
5451
from ..agents.llm_agent import LlmAgent
52+
from ..agents.run_config import StreamingMode
53+
5554

5655
LOG_LEVELS = click.Choice(
5756
["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
5857
case_sensitive=False,
5958
)
6059

60+
_STREAMING_MODE_CHOICES = ("None", "sse", "bidi")
61+
62+
63+
def _missing_eval_dependencies_message() -> str:
64+
# Imported lazily so loading the CLI does not pull in the evaluation stack.
65+
from ..evaluation.constants import MISSING_EVAL_DEPENDENCIES_MESSAGE
66+
67+
return MISSING_EVAL_DEPENDENCIES_MESSAGE
68+
69+
70+
def _parse_streaming_mode(
71+
_ctx: click.Context,
72+
param: click.Parameter,
73+
value: str | None,
74+
) -> StreamingMode | None:
75+
"""Converts a validated CLI value without importing the runtime for help."""
76+
if value is None:
77+
return None
78+
79+
from ..agents.run_config import StreamingMode
80+
81+
mode = next(
82+
(m for m in StreamingMode if str(m.value).lower() == value.lower()), None
83+
)
84+
if mode is None:
85+
raise click.BadParameter(f"unknown streaming mode {value!r}", param=param)
86+
return mode
87+
6188

6289
def _logging_options():
6390
"""Decorator to add logging options to click commands."""
@@ -426,13 +453,8 @@ def conformance():
426453
)
427454
@click.argument(
428455
"streaming-mode",
429-
type=click.Choice(
430-
[str(m.value) for m in StreamingMode], case_sensitive=False
431-
),
432-
callback=lambda ctx, param, value: next(
433-
(m for m in StreamingMode if str(m.value).lower() == value.lower()),
434-
value,
435-
),
456+
type=click.Choice(_STREAMING_MODE_CHOICES, case_sensitive=False),
457+
callback=_parse_streaming_mode,
436458
)
437459
@click.pass_context
438460
def cli_conformance_record(
@@ -516,15 +538,8 @@ def cli_conformance_record(
516538
)
517539
@click.option(
518540
"--streaming-mode",
519-
type=click.Choice(
520-
[str(m.value) for m in StreamingMode], case_sensitive=False
521-
),
522-
callback=lambda ctx, param, value: next(
523-
(m for m in StreamingMode if str(m.value).lower() == value.lower()),
524-
value,
525-
)
526-
if value is not None
527-
else None,
541+
type=click.Choice(_STREAMING_MODE_CHOICES, case_sensitive=False),
542+
callback=_parse_streaming_mode,
528543
required=False,
529544
default=None,
530545
)
@@ -940,6 +955,8 @@ def cli_run(
940955
sys.exit(exit_code)
941956
else:
942957
# Legacy interactive mode
958+
from .cli import run_cli
959+
943960
asyncio.run(
944961
run_cli(
945962
agent_parent_dir=agent_parent_folder,
@@ -1182,7 +1199,7 @@ def cli_eval(
11821199
from .cli_eval import parse_and_get_evals_to_run
11831200
from .cli_eval import pretty_print_eval_result
11841201
except ModuleNotFoundError as mnf:
1185-
raise click.ClickException(MISSING_EVAL_DEPENDENCIES_MESSAGE) from mnf
1202+
raise click.ClickException(_missing_eval_dependencies_message()) from mnf
11861203

11871204
eval_config = get_evaluation_criteria_or_default(config_file_path)
11881205
print(f"Using evaluation criteria: {eval_config}")
@@ -1305,7 +1322,7 @@ def cli_eval(
13051322
)
13061323
)
13071324
except ModuleNotFoundError as mnf:
1308-
raise click.ClickException(MISSING_EVAL_DEPENDENCIES_MESSAGE) from mnf
1325+
raise click.ClickException(_missing_eval_dependencies_message()) from mnf
13091326

13101327
click.echo(
13111328
"*********************************************************************"
@@ -1413,7 +1430,7 @@ def cli_optimize(
14131430
from .cli_eval import get_root_agent
14141431

14151432
except ModuleNotFoundError as mnf:
1416-
raise click.ClickException(MISSING_EVAL_DEPENDENCIES_MESSAGE) from mnf
1433+
raise click.ClickException(_missing_eval_dependencies_message()) from mnf
14171434

14181435
with open(sampler_config_file_path, "r", encoding="utf-8") as f:
14191436
content = f.read()
@@ -1551,7 +1568,7 @@ def cli_add_eval_case(
15511568
from .cli_eval import get_eval_sets_manager
15521569

15531570
except ModuleNotFoundError as mnf:
1554-
raise click.ClickException(MISSING_EVAL_DEPENDENCIES_MESSAGE) from mnf
1571+
raise click.ClickException(_missing_eval_dependencies_message()) from mnf
15551572

15561573
app_name = os.path.basename(agent_module_file_path)
15571574
agents_dir = os.path.dirname(agent_module_file_path)
@@ -1649,7 +1666,7 @@ def cli_generate_eval_cases(
16491666
from .utils.state import create_empty_state
16501667

16511668
except ModuleNotFoundError as mnf:
1652-
raise click.ClickException(MISSING_EVAL_DEPENDENCIES_MESSAGE) from mnf
1669+
raise click.ClickException(_missing_eval_dependencies_message()) from mnf
16531670

16541671
app_name = os.path.basename(agent_module_file_path)
16551672
agents_dir = os.path.dirname(agent_module_file_path)
@@ -1994,6 +2011,8 @@ async def _lifespan(app: FastAPI):
19942011
fg="green",
19952012
)
19962013

2014+
import uvicorn
2015+
19972016
from .fast_api import get_fast_api_app
19982017

19992018
app = get_fast_api_app(
@@ -2123,6 +2142,8 @@ def cli_api_server(
21232142

21242143
logs.setup_adk_logger(getattr(logging, log_level.upper()))
21252144

2145+
import uvicorn
2146+
21262147
from .fast_api import get_fast_api_app
21272148

21282149
config = uvicorn.Config(

src/google/adk/cli/utils/__init__.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,23 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
import re
16-
from typing import Any
17-
from typing import Optional
15+
from __future__ import annotations
1816

19-
from ...agents.base_agent import BaseAgent
20-
from ...agents.llm_agent import LlmAgent
21-
from .dot_adk_folder import DotAdkFolder
22-
from .state import create_empty_state
17+
from typing import TYPE_CHECKING
2318

19+
from ...utils import _lazy
20+
21+
if TYPE_CHECKING:
22+
from .dot_adk_folder import DotAdkFolder
23+
from .state import create_empty_state
24+
25+
_LAZY_MEMBERS: dict[str, str] = {
26+
'create_empty_state': '.state',
27+
'DotAdkFolder': '.dot_adk_folder',
28+
}
2429
__all__ = [
2530
'create_empty_state',
2631
'DotAdkFolder',
2732
]
33+
34+
__getattr__, __dir__ = _lazy.accessors(globals(), _LAZY_MEMBERS)

0 commit comments

Comments
 (0)