diff --git a/hacks/adk-intro/README.md b/hacks/adk-intro/README.md index a32d7e05..e3f23461 100644 --- a/hacks/adk-intro/README.md +++ b/hacks/adk-intro/README.md @@ -28,9 +28,10 @@ This hack will help you explore the following tasks: - Challenge 1: First Scan - Challenge 2: Equipping the Scanner - Challenge 3: Sticky Notes -- Challenge 4: Agent Symphony -- Challenge 5: MCP: Universal Tooling -- Challenge 6: A2A: Remote Agent Power +- Challenge 4: The Gatekeeper +- Challenge 5: Agent Symphony +- Challenge 6: MCP: Universal Tooling +- Challenge 7: A2A: Remote Agent Power ## Prerequisites @@ -144,7 +145,41 @@ Modify the `resource_scanner_agent` to save the list of all virtual machines in - You can use `adk web` UI to inspect the session state (and to verify that everything works as expected). -## Challenge 4: Agent Symphony +## Challenge 4: The Gatekeeper + +### Introduction + +In the previous challenge, we saved the scanned resources to the session state under the `resources` key. + +While our LLM is intelligent enough to inspect the conversation history and recognize that we already scanned the resources, asking the agent about them still triggers a model invocation. This wastes Gemini tokens and adds unnecessary latency. To prevent this, we'll introduce a *callback*. Callbacks are specialized lifecycle hooks that intercept execution of agents, models and tools. + +Callbacks can be used for many different purposes, such as to modify data flowing through the agent, or even to bypass certain steps entirely based on your logic. You can use them to log detailed information at every step, enforce guardrails, cache things and manage state. In this challenge we'll skip the agent execution altogether if the state indicates that a certain step has already run. + +### Description + +Create a new callback function named `gatekeeper` that returns `Already scanned!` *if and only if* the session state contains the `resources` key. + +Make sure that this function is called every time *before* the agent runs. + +### Success Criteria + +- There's a new Python function called `gatekeeper` with the correct function signature. +- The `resource_scanner_agent` is configured to use the `gatekeeper` every time before the agent runs. +- Asking the agent to scan resources a second time returns `"Already scanned!"` instantly, bypassing model and tool calls. +- The changes have been pushed to the remote Git repository. + +### Learning Resources + +- [ADK Callbacks](https://adk.dev/callbacks/) +- [Before agent callback](https://adk.dev/callbacks/types-of-callbacks/#before-agent-callback) + +### Tips + +- Returning `None` from the callback causes the agent to proceed with the normal execution flow. +- You cannot return a string from a callback function, it has to be wrapped into an appropriate object. +- Callback function parameter names must match the [documented names exactly](https://adk.dev/callbacks/types-of-callbacks/#agent-lifecycle-callbacks) because ADK passes callback arguments by keyword. + +## Challenge 5: Agent Symphony ### Introduction @@ -179,7 +214,7 @@ Then create a new *Workflow* `orchestrator_agent` that calls the `resource_scann - You can use `adk web` UI to view the agents involved. - If idle resource list is not generated correctly, make your instructions more specific. -## Challenge 5: MCP: Universal Tooling +## Challenge 6: MCP: Universal Tooling ### Introduction @@ -215,7 +250,7 @@ Then add the `resource_labeler_agent` to the `orchestrator_agent` sequence. - LLMs have no understanding of the current date and can struggle with date arithmetic, you might want to use additional tools. - If anything goes wrong with the labels, you can use the provided `reset-labels.sh` script to reset the labels to their original state. -## Challenge 6: A2A: Remote Agent Power +## Challenge 7: A2A: Remote Agent Power ### Introduction diff --git a/hacks/adk-intro/artifacts/setup.tftpl b/hacks/adk-intro/artifacts/setup.tftpl index 963bb4fb..97ea3e47 100644 --- a/hacks/adk-intro/artifacts/setup.tftpl +++ b/hacks/adk-intro/artifacts/setup.tftpl @@ -65,5 +65,24 @@ gcloud run deploy a2a-server \ echo "Setup done!" echo "Starting CPU hogging..." -# Hog the cpu after 2 minutes (120 seconds = 120,000,000 microseconds) -stress -c 1 --backoff 120000000 & +cat << EOF > /etc/systemd/system/cpu-hog.service +[Unit] +Description=My CPU Hogging Service +After=network.target + +[Service] +ExecStart=/usr/bin/stress -c 1 +Restart=on-failure + +[Install] +WantedBy=multi-user.target +EOF + +sleep 120 + +# Reload systemd, enable and start the service +systemctl daemon-reload +systemctl enable cpu-hog.service +systemctl start cpu-hog.service + +echo "CPU hogging started!" diff --git a/hacks/adk-intro/solutions.md b/hacks/adk-intro/solutions.md index f96f4587..af498c7d 100644 --- a/hacks/adk-intro/solutions.md +++ b/hacks/adk-intro/solutions.md @@ -17,9 +17,10 @@ Welcome to the coach's guide for Introduction to Agents with ADK gHack. Here you - Challenge 1: First Scan - Challenge 2: Equipping the Scanner - Challenge 3: Sticky Notes -- Challenge 4: Agent Symphony -- Challenge 5: MCP: Universal Tooling -- Challenge 6: A2A: Remote Agent Power +- Challenge 4: The Gatekeeper +- Challenge 5: Agent Symphony +- Challenge 6: MCP: Universal Tooling +- Challenge 7: A2A: Remote Agent Power ## Challenge 1: First Scan @@ -123,6 +124,13 @@ Format of the response is not relevant for this challenge as long as all of the Make sure that the changes are pushed to the repository so the next driver can pick up the changes. +Note that Git requires users to set up their identity before anything can be committed. So users need do the following before they can commit their changes: + +```shell +git config --global user.name "$USER" # or participant's real name +git config --global user.email "$USER_EMAIL" # or participant's real email address +``` + ## Challenge 3: Sticky Notes ### Notes & Guidance @@ -147,7 +155,45 @@ resource_scanner_agent = Agent( Make sure that the changes are pushed to the repository so the next driver can pick up the changes. -## Challenge 4: Agent Symphony +## Challenge 4: The Gatekeeper + +### Notes & Guidance + +Again the new driver should follow the same steps for the first challenge to clone the repository (or pull the latest changes if they have already cloned it) and set up their environment (if they haven't done that already). + +Then edit the `janitor/agent.py` to define the callback and register it on the scanner agent. + +```python +# Keep other imports +from google.adk.agents.callback_context import CallbackContext +from google.genai.types import Content, Part + + +def gatekeeper(callback_context: CallbackContext) -> Content | None: + if "resources" in callback_context.state: + return Content(parts=[Part.from_text(text="Already scanned!")]) + return None + + +resource_scanner_agent = Agent( + name="resource_scanner_agent", + model=settings.GEMINI_MODEL, + instruction=""" + You are a Cloud Resource Scanner. + Return *all* resources. + """, + tools=[tools.get_compute_instances_list], + output_key="resources", + output_schema=schemas.VMInstanceList, + before_agent_callback=gatekeeper +) + +root_agent = resource_scanner_agent +``` + +Make sure that the changes are pushed to the repository so the next driver can pick up the changes. + +## Challenge 5: Agent Symphony ### Notes & Guidance @@ -184,11 +230,11 @@ root_agent = orchestrator_agent ``` > [!NOTE] -> Sometimes after running both agents, the `resources` state variable seems to be empty, but as long as `idle_resources` provides the correct set of instances, it should be fine. +> Sometimes after running both agents, the `resources` state variable seems to be empty, but as long as `idle_resources` provides the correct set of instances, it should be fine. If the callback causees issues, because it will skip the agent execution if the `resources` is set, it can be updated to check for the length of the list and override execution only if the list exists and it is not empty. Make sure that the changes are pushed to the repository so the next driver can pick up the changes. -## Challenge 5: MCP: Universal Tooling +## Challenge 6: MCP: Universal Tooling ### Notes & Guidance @@ -262,7 +308,7 @@ mcp_tool_set = McpToolset( ``` > [!NOTE] -> At the time of this writing using the `auth_scheme` and `auth_credentials` for bearer tokens doesn't work well with MCP servers, as those credentials are not utilized for listing the tools, tracked [here](https://github.com/google/adk-python/issues/2168). +> When we originally designed this challenge, the `auth_scheme` and `auth_credentials` for bearer tokens didn't work well with MCP servers, as those credentials were not utilized for listing the tools, tracked [here](https://github.com/google/adk-python/issues/2168). This issue has been resolved, so participants are free to use that approach too. As our tool is simple, this approach works fine, but in real world, you might need to use OAuth flows, API keys etc. And there will be cases where the currently authenticated user's credentials need to be forwarded to remote agents/tools so that they can perform actions on behalf of the user (see the official [docs](https://adk.dev/safety/#identity-and-authorization) for more details). @@ -276,7 +322,7 @@ gcloud compute instances list \ Make sure that the changes are pushed to the repository so the next driver can pick up the changes. -## Challenge 6: A2A: Remote Agent Power +## Challenge 7: A2A: Remote Agent Power ### Notes & Guidance