Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
182 changes: 142 additions & 40 deletions src/google/adk/cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,17 @@
from __future__ import annotations

from datetime import datetime
import os
import threading
from typing import Optional
from typing import Union

import click
from google.genai import types
from pydantic import BaseModel
from watchdog.events import FileSystemEvent
from watchdog.events import FileSystemEventHandler
from watchdog.observers import Observer

from ..agents.base_agent import BaseAgent
from ..agents.llm_agent import LlmAgent
Expand All @@ -44,6 +49,35 @@ class InputFile(BaseModel):
queries: list[str]


class DevModeChangeHandler(FileSystemEventHandler):
"""Handles file system events for development mode auto-reload."""

def __init__(self):
self.reload_needed = threading.Event()
self.last_modified_file = None

def _handle_event(self, event: FileSystemEvent):
"""Handle file system events for .py and .yaml files."""
if event.is_directory:
return
if event.src_path.endswith(('.py', '.yaml')):
self.last_modified_file = event.src_path
self.reload_needed.set()

def on_modified(self, event: FileSystemEvent):
self._handle_event(event)

def on_created(self, event: FileSystemEvent):
self._handle_event(event)

def check_and_reset(self) -> tuple[bool, Optional[str]]:
"""Check if reload is needed and reset the flag."""
if self.reload_needed.is_set():
self.reload_needed.clear()
return True, self.last_modified_file
return False, None


async def run_input_file(
app_name: str,
user_id: str,
Expand Down Expand Up @@ -92,6 +126,9 @@ async def run_interactively(
session: Session,
session_service: BaseSessionService,
credential_service: BaseCredentialService,
agent_loader: Optional[AgentLoader] = None,
agent_folder_name: Optional[str] = None,
change_handler: Optional[DevModeChangeHandler] = None,
) -> None:
app = (
root_agent_or_app
Expand All @@ -104,7 +141,44 @@ async def run_interactively(
session_service=session_service,
credential_service=credential_service,
)

current_agent_or_app = root_agent_or_app

while True:
# Check if we need to reload the agent in dev mode
if change_handler and agent_loader and agent_folder_name:
needs_reload, changed_file = change_handler.check_and_reset()
if needs_reload:
try:
click.secho(
f'\nDetected change in {changed_file}',
fg='yellow',
)
click.secho('Reloading agent...', fg='yellow')
# Remove from cache and reload
agent_loader.remove_agent_from_cache(agent_folder_name)
current_agent_or_app = agent_loader.load_agent(agent_folder_name)

# Update the app and runner with the new agent
app = (
current_agent_or_app
if isinstance(current_agent_or_app, App)
else App(name=session.app_name, root_agent=current_agent_or_app)
)
await runner.close()
runner = Runner(
app=app,
artifact_service=artifact_service,
session_service=session_service,
credential_service=credential_service,
)
click.secho('Agent reloaded successfully!\n', fg='green')
except Exception as e:
click.secho(
f'Error reloading agent: {e}\n',
fg='red',
)

query = input('[user]: ')
if not query or not query.strip():
continue
Expand Down Expand Up @@ -134,6 +208,7 @@ async def run_cli(
saved_session_file: Optional[str] = None,
save_session: bool,
session_id: Optional[str] = None,
dev_mode: bool = False,
) -> None:
"""Runs an interactive CLI for a certain agent.

Expand All @@ -148,16 +223,16 @@ async def run_cli(
contains a previously saved session, exclusive with input_file.
save_session: bool, whether to save the session on exit.
session_id: Optional[str], the session ID to save the session to on exit.
dev_mode: bool, whether to enable development mode with auto-reload.
"""

artifact_service = InMemoryArtifactService()
session_service = InMemorySessionService()
credential_service = InMemoryCredentialService()

user_id = 'test_user'
agent_or_app = AgentLoader(agents_dir=agent_parent_dir).load_agent(
agent_folder_name
)
agent_loader = AgentLoader(agents_dir=agent_parent_dir)
agent_or_app = agent_loader.load_agent(agent_folder_name)
session_app_name = (
agent_or_app.name if isinstance(agent_or_app, App) else agent_folder_name
)
Expand All @@ -166,45 +241,72 @@ async def run_cli(
)
if not is_env_enabled('ADK_DISABLE_LOAD_DOTENV'):
envs.load_dotenv_for_agent(agent_folder_name, agent_parent_dir)
if input_file:
session = await run_input_file(
app_name=session_app_name,
user_id=user_id,
agent_or_app=agent_or_app,
artifact_service=artifact_service,
session_service=session_service,
credential_service=credential_service,
input_path=input_file,
)
elif saved_session_file:
with open(saved_session_file, 'r', encoding='utf-8') as f:
loaded_session = Session.model_validate_json(f.read())

if loaded_session:
for event in loaded_session.events:
await session_service.append_event(session, event)
content = event.content
if not content or not content.parts or not content.parts[0].text:
continue
click.echo(f'[{event.author}]: {content.parts[0].text}')

await run_interactively(
agent_or_app,
artifact_service,
session,
session_service,
credential_service,
)
else:
click.echo(f'Running agent {agent_or_app.name}, type exit to exit.')
await run_interactively(
agent_or_app,
artifact_service,
session,
session_service,
credential_service,

# Set up watchdog observer for dev mode
observer: Optional[Observer] = None
change_handler: Optional[DevModeChangeHandler] = None
if dev_mode:
agent_path = os.path.join(agent_parent_dir, agent_folder_name)
change_handler = DevModeChangeHandler()
observer = Observer()
observer.schedule(change_handler, agent_path, recursive=True)
observer.start()
click.secho(
'Auto-reload enabled - watching for file changes...',
fg='green',
)

try:
if input_file:
session = await run_input_file(
app_name=session_app_name,
user_id=user_id,
agent_or_app=agent_or_app,
artifact_service=artifact_service,
session_service=session_service,
credential_service=credential_service,
input_path=input_file,
)
elif saved_session_file:
with open(saved_session_file, 'r', encoding='utf-8') as f:
loaded_session = Session.model_validate_json(f.read())

if loaded_session:
for event in loaded_session.events:
await session_service.append_event(session, event)
content = event.content
if not content or not content.parts or not content.parts[0].text:
continue
click.echo(f'[{event.author}]: {content.parts[0].text}')

await run_interactively(
agent_or_app,
artifact_service,
session,
session_service,
credential_service,
agent_loader=agent_loader if dev_mode else None,
agent_folder_name=agent_folder_name if dev_mode else None,
change_handler=change_handler,
)
else:
click.echo(f'Running agent {agent_or_app.name}, type exit to exit.')
await run_interactively(
agent_or_app,
artifact_service,
session,
session_service,
credential_service,
agent_loader=agent_loader if dev_mode else None,
agent_folder_name=agent_folder_name if dev_mode else None,
change_handler=change_handler,
)
finally:
# Clean up observer
if observer:
observer.stop()
observer.join()

if save_session:
session_id = session_id or input('Session ID to save: ')
session_path = (
Expand Down
14 changes: 14 additions & 0 deletions src/google/adk/cli/cli_tools_click.py
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,16 @@ def validate_exclusive(ctx, param, value):
),
callback=validate_exclusive,
)
@click.option(
"--dev",
is_flag=True,
show_default=True,
default=False,
help=(
"Optional. Enable development mode with automatic agent reloading when"
" source files change. Watches for changes in .py and .yaml files."
),
)
@click.argument(
"agent",
type=click.Path(
Expand All @@ -409,6 +419,7 @@ def cli_run(
session_id: Optional[str],
replay: Optional[str],
resume: Optional[str],
dev: bool,
):
"""Runs an interactive CLI for a certain agent.

Expand All @@ -417,6 +428,8 @@ def cli_run(
Example:

adk run path/to/my_agent

adk run --dev path/to/my_agent
"""
logs.log_to_tmp_folder()

Expand All @@ -431,6 +444,7 @@ def cli_run(
saved_session_file=resume,
save_session=save_session,
session_id=session_id,
dev_mode=dev,
)
)

Expand Down
Loading