Skip to content

Commit d213440

Browse files
callback-docs-fix (#132)
1 parent a33e8d6 commit d213440

File tree

5 files changed

+242
-80
lines changed

5 files changed

+242
-80
lines changed

docs/assets/callback_flow.png

-54.1 KB
Loading

docs/callbacks/index.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,15 @@
44

55
Callbacks are a cornerstone feature of ADK, providing a powerful mechanism to hook into an agent's execution process. They allow you to observe, customize, and even control the agent's behavior at specific, predefined points without modifying the core ADK framework code.
66

7-
**What are they?** In essence, callbacks are standard Python functions that you define. You then associate these functions with an agent when you create it. The ADK framework automatically calls your functions at key stages in the agent's lifecycle, such as:
7+
**What are they?** In essence, callbacks are standard Python functions that you define. You then associate these functions with an agent when you create it. The ADK framework automatically calls your functions at key stages, letting you observe or intervene. Think of it like checkpoints during the agent's process:
8+
9+
* **Before the agent starts its main work on a request, and after it finishes:** When you ask an agent to do something (e.g., answer a question), it runs its internal logic to figure out the response.
10+
* The `before_agent` callback executes *right before* this main work begins for that specific request.
11+
* The `after_agent` callback executes *right after* the agent has finished all its steps for that request and has prepared the final result, but just before the result is returned.
12+
* This "main work" encompasses the agent's *entire* process for handling that single request. This might involve deciding to call an LLM, actually calling the LLM, deciding to use a tool, using the tool, processing the results, and finally putting together the answer. These callbacks essentially wrap the whole sequence from receiving the input to producing the final output for that one interaction.
13+
* **Before sending a request to, or after receiving a response from, the Large Language Model (LLM):** These callbacks (`before_model`, `after_model`) allow you to inspect or modify the data going to and coming from the LLM specifically.
14+
* **Before executing a tool (like a Python function or another agent) or after it finishes:** Similarly, `before_tool` and `after_tool` callbacks give you control points specifically around the execution of tools invoked by the agent.
815

9-
* Before or after the agent's main processing logic runs.
10-
* Before sending a request to, or after receiving a response from, the Large Language Model (LLM).
11-
* Before executing a tool (like a Python function or another agent) or after it finishes.
1216

1317
![intro_components.png](../assets/callback_flow.png)
1418

docs/callbacks/types-of-callbacks.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,18 @@ These callbacks are available on *any* agent that inherits from `BaseAgent` (inc
1818
--8<-- "examples/python/snippets/callbacks/before_agent_callback.py"
1919
```
2020

21+
**Note on the `before_agent_callback` Example:**
22+
23+
* **What it Shows:** This example demonstrates the `before_agent_callback`. This callback runs *right before* the agent's main processing logic starts for a given request.
24+
* **How it Works:** The callback function (`check_if_agent_should_run`) looks at a flag (`skip_llm_agent`) in the session's state.
25+
* If the flag is `True`, the callback returns a `types.Content` object. This tells the ADK framework to **skip** the agent's main execution entirely and use the callback's returned content as the final response.
26+
* If the flag is `False` (or not set), the callback returns `None`. This tells the ADK framework to **proceed** with the agent's normal execution (calling the LLM in this case).
27+
* **Expected Outcome:** You'll see two scenarios:
28+
1. In the session *with* the `skip_llm_agent: True` state, the agent's LLM call is bypassed, and the output comes directly from the callback ("Agent... skipped...").
29+
2. In the session *without* that state flag, the callback allows the agent to run, and you see the actual response from the LLM (e.g., "Hello!").
30+
* **Understanding Callbacks:** This highlights how `before_` callbacks act as **gatekeepers**, allowing you to intercept execution *before* a major step and potentially prevent it based on checks (like state, input validation, permissions).
31+
32+
2133
### After Agent Callback
2234

2335
**When:** Called *immediately after* the agent's `_run_async_impl` (or `_run_live_impl`) method successfully completes. It does *not* run if the agent was skipped due to `before_agent_callback` returning content or if `end_invocation` was set during the agent's run.
@@ -30,6 +42,17 @@ These callbacks are available on *any* agent that inherits from `BaseAgent` (inc
3042
--8<-- "examples/python/snippets/callbacks/after_agent_callback.py"
3143
```
3244

45+
**Note on the `after_agent_callback` Example:**
46+
47+
* **What it Shows:** This example demonstrates the `after_agent_callback`. This callback runs *right after* the agent's main processing logic has finished and produced its result, but *before* that result is finalized and returned.
48+
* **How it Works:** The callback function (`modify_output_after_agent`) checks a flag (`add_concluding_note`) in the session's state.
49+
* If the flag is `True`, the callback returns a *new* `types.Content` object. This tells the ADK framework to **replace** the agent's original output with the content returned by the callback.
50+
* If the flag is `False` (or not set), the callback returns `None`. This tells the ADK framework to **use** the original output generated by the agent.
51+
* **Expected Outcome:** You'll see two scenarios:
52+
1. In the session *without* the `add_concluding_note: True` state, the callback allows the agent's original output ("Processing complete!") to be used.
53+
2. In the session *with* that state flag, the callback intercepts the agent's original output and replaces it with its own message ("Concluding note added...").
54+
* **Understanding Callbacks:** This highlights how `after_` callbacks allow **post-processing** or **modification**. You can inspect the result of a step (the agent's run) and decide whether to let it pass through, change it, or completely replace it based on your logic.
55+
3356
## LLM Interaction Callbacks
3457

3558
These callbacks are specific to `LlmAgent` and provide hooks around the interaction with the Large Language Model.
Lines changed: 107 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,127 @@
1+
# # --- Setup Instructions ---
2+
# # 1. Install the ADK package:
3+
# !pip install google-adk
4+
# # Make sure to restart kernel if using colab/jupyter notebooks
5+
6+
# # 2. Set up your Gemini API Key:
7+
# # - Get a key from Google AI Studio: https://aistudio.google.com/app/apikey
8+
# # - Set it as an environment variable:
9+
# import os
10+
# os.environ["GOOGLE_API_KEY"] = "YOUR_API_KEY_HERE" # <--- REPLACE with your actual key
11+
# # Or learn about other authentication methods (like Vertex AI):
12+
# # https://google.github.io/adk-docs/agents/models/
13+
14+
15+
# ADK Imports
116
from google.adk.agents import LlmAgent
217
from google.adk.agents.callback_context import CallbackContext
3-
from google.adk.runners import Runner
18+
from google.adk.runners import InMemoryRunner # Use InMemoryRunner
19+
from google.genai import types # For types.Content
420
from typing import Optional
5-
from google.genai import types
6-
from google.adk.sessions import InMemorySessionService
721

22+
# Define the model - Use the specific model name requested
823
GEMINI_2_FLASH="gemini-2.0-flash"
924

10-
# --- Define the Callback Function ---
11-
def simple_after_agent_logger(callback_context: CallbackContext) -> Optional[types.Content]:
12-
"""Logs exit from an agent and optionally appends a message."""
25+
# --- 1. Define the Callback Function ---
26+
def modify_output_after_agent(callback_context: CallbackContext) -> Optional[types.Content]:
27+
"""
28+
Logs exit from an agent and checks 'add_concluding_note' in session state.
29+
If True, returns new Content to *replace* the agent's original output.
30+
If False or not present, returns None, allowing the agent's original output to be used.
31+
"""
1332
agent_name = callback_context.agent_name
1433
invocation_id = callback_context.invocation_id
15-
print(f"[Callback] Exiting agent: {agent_name} (Invocation: {invocation_id})")
34+
current_state = callback_context.state.to_dict()
1635

17-
# Example: Check state potentially modified during the agent's run
18-
final_status = callback_context.state.get("agent_run_status", "Completed Normally")
19-
print(f"[Callback] Agent run status from state: {final_status}")
36+
print(f"\n[Callback] Exiting agent: {agent_name} (Inv: {invocation_id})")
37+
print(f"[Callback] Current State: {current_state}")
2038

21-
# Example: Optionally return Content to append a message
22-
if callback_context.state.get("add_concluding_note", False):
23-
print(f"[Callback] Adding concluding note for agent {agent_name}.")
24-
# Return Content to append after the agent's own output
25-
return types.Content(parts=[types.Part(text=f"Concluding note added by after_agent_callback.")])
39+
# Example: Check state to decide whether to modify the final output
40+
if current_state.get("add_concluding_note", False):
41+
print(f"[Callback] State condition 'add_concluding_note=True' met: Replacing agent {agent_name}'s output.")
42+
# Return Content to *replace* the agent's own output
43+
return types.Content(
44+
parts=[types.Part(text=f"Concluding note added by after_agent_callback, replacing original output.")],
45+
role="model" # Assign model role to the overriding response
46+
)
2647
else:
27-
print(f"[Callback] No concluding note added for agent {agent_name}.")
28-
# Return None - no additional message appended
48+
print(f"[Callback] State condition not met: Using agent {agent_name}'s original output.")
49+
# Return None - the agent's output produced just before this callback will be used.
2950
return None
3051

31-
my_llm_agent = LlmAgent(
32-
name="SimpleLlmAgentWithAfter",
33-
model=GEMINI_2_FLASH,
34-
instruction="You are a simple agent. Just say 'Processing complete!'",
35-
description="An LLM agent demonstrating after_agent_callback",
36-
after_agent_callback=simple_after_agent_logger # Assign the function here
52+
# --- 2. Setup Agent with Callback ---
53+
llm_agent_with_after_cb = LlmAgent(
54+
name="MySimpleAgentWithAfter",
55+
model=GEMINI_2_FLASH,
56+
instruction="You are a simple agent. Just say 'Processing complete!'",
57+
description="An LLM agent demonstrating after_agent_callback for output modification",
58+
after_agent_callback=modify_output_after_agent # Assign the callback here
59+
)
60+
61+
# --- 3. Setup Runner and Sessions using InMemoryRunner ---
62+
async def main():
63+
app_name = "after_agent_demo"
64+
user_id = "test_user_after"
65+
session_id_normal = "session_run_normally"
66+
session_id_modify = "session_modify_output"
67+
68+
# Use InMemoryRunner - it includes InMemorySessionService
69+
runner = InMemoryRunner(agent=llm_agent_with_after_cb, app_name=app_name)
70+
# Get the bundled session service to create sessions
71+
session_service = runner.session_service
72+
73+
# Create session 1: Agent output will be used as is (default empty state)
74+
session_service.create_session(
75+
app_name=app_name,
76+
user_id=user_id,
77+
session_id=session_id_normal
78+
# No initial state means 'add_concluding_note' will be False in the callback check
3779
)
80+
# print(f"Session '{session_id_normal}' created with default state.")
3881

39-
APP_NAME = "guardrail_app"
40-
USER_ID = "user_1"
41-
SESSION_ID = "session_001"
82+
# Create session 2: Agent output will be replaced by the callback
83+
session_service.create_session(
84+
app_name=app_name,
85+
user_id=user_id,
86+
session_id=session_id_modify,
87+
state={"add_concluding_note": True} # Set the state flag here
88+
)
89+
# print(f"Session '{session_id_modify}' created with state={{'add_concluding_note': True}}.")
4290

43-
# Session and Runner
44-
session_service = InMemorySessionService()
45-
session = session_service.create_session(app_name=APP_NAME, user_id=USER_ID, session_id=SESSION_ID)
46-
runner = Runner(agent=my_llm_agent, app_name=APP_NAME, session_service=session_service)
4791

92+
# --- Scenario 1: Run where callback allows agent's original output ---
93+
print("\n" + "="*20 + f" SCENARIO 1: Running Agent on Session '{session_id_normal}' (Should Use Original Output) " + "="*20)
94+
async for event in runner.run_async(
95+
user_id=user_id,
96+
session_id=session_id_normal,
97+
new_message=types.Content(role="user", parts=[types.Part(text="Process this please.")])
98+
):
99+
# Print final output (either from LLM or callback override)
100+
if event.is_final_response() and event.content:
101+
print(f"Final Output: [{event.author}] {event.content.parts[0].text.strip()}")
102+
elif event.is_error():
103+
print(f"Error Event: {event.error_details}")
48104

49-
# Agent Interaction
50-
def call_agent(query):
51-
content = types.Content(role='user', parts=[types.Part(text=query)])
52-
events = runner.run(user_id=USER_ID, session_id=SESSION_ID, new_message=content)
105+
# --- Scenario 2: Run where callback replaces the agent's output ---
106+
print("\n" + "="*20 + f" SCENARIO 2: Running Agent on Session '{session_id_modify}' (Should Replace Output) " + "="*20)
107+
async for event in runner.run_async(
108+
user_id=user_id,
109+
session_id=session_id_modify,
110+
new_message=types.Content(role="user", parts=[types.Part(text="Process this and add note.")])
111+
):
112+
# Print final output (either from LLM or callback override)
113+
if event.is_final_response() and event.content:
114+
print(f"Final Output: [{event.author}] {event.content.parts[0].text.strip()}")
115+
elif event.is_error():
116+
print(f"Error Event: {event.error_details}")
53117

54-
for event in events:
55-
if event.is_final_response():
56-
final_response = event.content.parts[0].text
57-
print("Agent Response: ", final_response)
118+
# --- 4. Execute ---
119+
# In a Python script:
120+
# import asyncio
121+
# if __name__ == "__main__":
122+
# # Make sure GOOGLE_API_KEY environment variable is set if not using Vertex AI auth
123+
# # Or ensure Application Default Credentials (ADC) are configured for Vertex AI
124+
# asyncio.run(main())
58125

59-
call_agent("callback example")
126+
# In a Jupyter Notebook or similar environment:
127+
await main()

0 commit comments

Comments
 (0)