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
198 changes: 150 additions & 48 deletions src/google/adk/cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,18 @@
from __future__ import annotations

from datetime import datetime
import os
from pathlib import Path
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.llm_agent import LlmAgent
from ..apps.app import App
Expand All @@ -47,6 +52,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 @@ -95,6 +129,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 @@ -107,7 +144,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 @@ -137,6 +211,7 @@ async def run_cli(
saved_session_file: Optional[str] = None,
save_session: bool,
session_id: Optional[str] = None,
dev_mode: bool = False,
session_service_uri: Optional[str] = None,
artifact_service_uri: Optional[str] = None,
) -> None:
Expand All @@ -153,6 +228,7 @@ 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.
session_service_uri: Optional[str], custom session service URI.
artifact_service_uri: Optional[str], custom artifact service URI.
"""
Expand All @@ -174,9 +250,9 @@ async def run_cli(

credential_service = InMemoryCredentialService()
agents_dir = str(agent_parent_path)
agent_or_app = AgentLoader(agents_dir=agents_dir).load_agent(
agent_folder_name
)
agent_loader = AgentLoader(agents_dir=agents_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 @@ -194,53 +270,79 @@ def _print_event(event) -> None:
author = event.author or 'system'
click.echo(f'[{author}]: {"".join(text_parts)}')

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:
# Load the saved session from file
with open(saved_session_file, 'r', encoding='utf-8') as f:
loaded_session = Session.model_validate_json(f.read())

# Create a new session in the service, copying state from the file
session = await session_service.create_session(
app_name=session_app_name,
user_id=user_id,
state=loaded_session.state if loaded_session else None,
# 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',
)

# Append events from the file to the new session and display them
if loaded_session:
for event in loaded_session.events:
await session_service.append_event(session, event)
_print_event(event)

await run_interactively(
agent_or_app,
artifact_service,
session,
session_service,
credential_service,
)
else:
session = await session_service.create_session(
app_name=session_app_name, user_id=user_id
)
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,
)
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:
# Load the saved session from file
with open(saved_session_file, 'r', encoding='utf-8') as f:
loaded_session = Session.model_validate_json(f.read())

# Create a new session in the service, copying state from the file
session = await session_service.create_session(
app_name=session_app_name,
user_id=user_id,
state=loaded_session.state if loaded_session else None,
)

# Append events from the file to the new session and display them
if loaded_session:
for event in loaded_session.events:
await session_service.append_event(session, event)
_print_event(event)

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:
session = await session_service.create_session(
app_name=session_app_name, user_id=user_id
)
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: ')
Expand Down
10 changes: 9 additions & 1 deletion src/google/adk/cli/cli_eval.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,15 @@ def parse_and_get_evals_to_run(
eval_set_to_evals = {}
for input_eval_set in evals_to_run_info:
evals = []
if ":" not in input_eval_set:
# Check if the input is a file path that exists (e.g. C:\path\to\file.json)
if os.path.exists(input_eval_set):
eval_set = input_eval_set
# Check if it's a file path with cases (e.g. C:\path\to\file.json:case1,case2)
elif ":" in input_eval_set and os.path.exists(input_eval_set.rsplit(":", 1)[0]):
eval_set = input_eval_set.rsplit(":", 1)[0]
evals = input_eval_set.rsplit(":", 1)[1].split(",")
evals = [s for s in evals if s.strip()]
elif ":" not in input_eval_set:
# We don't have any eval cases specified. This would be the case where the
# the user wants to run all eval cases in the eval set.
eval_set = input_eval_set
Expand Down
Loading