Skip to content
Merged
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
202 changes: 4 additions & 198 deletions docs/agents/custom-agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,72 +80,7 @@ Let's illustrate the power of custom agents with an example pattern: a multi-sta
We define the `StoryFlowAgent` inheriting from `BaseAgent`. In `__init__`, we store the necessary sub-agents (passed in) as instance attributes and tell the `BaseAgent` framework about the top-level agents this custom agent will directly orchestrate.

```python
# agent.py (Initialization part)
import logging
from typing import AsyncGenerator
from typing_extensions import override

from google.adk.agents import Agent, LlmAgent, BaseAgent, LoopAgent, SequentialAgent
from google.adk.agents.invocation_context import InvocationContext
from google.adk.events import Event

logger = logging.getLogger(__name__)

class StoryFlowAgent(BaseAgent):
"""
custom agent demonstrating a conditional story generation workflow.
Illustrates using custom logic to orchestrate LLM, Loop, and Sequential agents.
"""
# --- Field Declarations for Clarity (Optional but good practice) ---
story_generator: LlmAgent
# loop_agent and sequential_agent are created internally but used directly
loop_agent: LoopAgent
sequential_agent: SequentialAgent

model_config = {"arbitrary_types_allowed": True} # Allow agent types

def __init__(
self,
name: str,
story_generator: LlmAgent, # Generates initial/regenerated story
critic: LlmAgent, # Critiques story (used by loop)
reviser: LlmAgent, # Revises story (used by loop)
grammar_check: LlmAgent, # Checks grammar (used by sequence)
tone_check: LlmAgent, # Checks tone (used by sequence)
):
# Store agents needed by _run_async_impl
self.story_generator = story_generator

# Create internal workflow agents used by our custom logic
self.loop_agent = LoopAgent(
name="CriticReviserLoop", sub_agents=[critic, reviser], max_iterations=2
)
self.sequential_agent = SequentialAgent(
name="PostProcessing", sub_agents=[grammar_check, tone_check]
)

# Define the list of agents directly orchestrated by _run_async_impl
# This list informs the ADK framework.
framework_sub_agents = [
self.story_generator,
self.loop_agent,
self.sequential_agent,
# critic, reviser, grammar_check, tone_check are managed *within*
# the loop_agent and sequential_agent, so not listed here directly.
]

# Initialize BaseAgent, telling it about the direct sub-agents
super().__init__(
name=name,
sub_agents=framework_sub_agents,
# Pydantic automatically assigns the passed-in agents to the declared fields
story_generator=story_generator,
critic=critic, # Still pass these if needed for Pydantic validation/typing
reviser=reviser,
grammar_check=grammar_check,
tone_check=tone_check,
)

--8<-- "examples/python/snippets/agents/custom-agent/storyflow_agent.py:init"
```

---
Expand All @@ -155,53 +90,7 @@ class StoryFlowAgent(BaseAgent):
This method orchestrates the sub-agents using standard Python async/await and control flow.

```python
# agent.py (_run_async_impl part, inside StoryFlowAgent class)

@override
async def _run_async_impl(
self, ctx: InvocationContext
) -> AsyncGenerator[Event, None]:
"""Implements the custom story workflow logic."""
logger.info(f"[{self.name}] Starting story generation workflow.")

# --- Step 1: Initial Story Generation ---
logger.info(f"[{self.name}] Running StoryGenerator...")
async for event in self.story_generator.run_async(ctx):
yield event # Pass events up

# Check state: Ensure story was generated before proceeding
if "current_story" not in ctx.session.state or not ctx.session.state["current_story"]:
logger.error(f"[{self.name}] Failed to generate initial story. Aborting.")
return

# --- Step 2: Critic-Reviser Loop ---
# Use the pre-configured LoopAgent instance
logger.info(f"[{self.name}] Running CriticReviserLoop...")
async for event in self.loop_agent.run_async(ctx):
yield event

# --- Step 3: Sequential Post-Processing ---
# Use the pre-configured SequentialAgent instance
logger.info(f"[{self.name}] Running PostProcessing (Grammar & Tone)...")
async for event in self.sequential_agent.run_async(ctx):
yield event

# --- Step 4: Conditional Logic based on Tone Check ---
# Read the result stored in state by the 'tone_check' agent
tone_check_result = ctx.session.state.get("tone_check_result")
logger.info(f"[{self.name}] Tone check result: {tone_check_result}")

if str(tone_check_result).strip().lower() == "negative": # Check tone
logger.warning(f"[{self.name}] Tone is negative. Regenerating story...")
# Re-run the story generator if tone is negative
async for event in self.story_generator.run_async(ctx):
yield event
else:
logger.info(f"[{self.name}] Tone is acceptable. Keeping current story.")
# Optionally yield a final status event here if needed

logger.info(f"[{self.name}] Workflow finished.")

--8<-- "examples/python/snippets/agents/custom-agent/storyflow_agent.py:executionlogic"
```

**Explanation of Logic:**
Expand All @@ -218,39 +107,8 @@ This method orchestrates the sub-agents using standard Python async/await and co
These are standard `LlmAgent` definitions, responsible for specific tasks. Their `output_key` parameter is crucial for placing results into the `session.state` where other agents or the custom orchestrator can access them.

```python
# agent.py (LLM Agent Definitions part)

GEMINI_FLASH = "gemini-2.0-flash-exp" # Define model constant

story_generator = LlmAgent(
name="StoryGenerator", model=GEMINI_FLASH,
instruction="Write a short story (~100 words) about the topic in state['topic'].",
output_key="current_story",
)

critic = LlmAgent(
name="Critic", model=GEMINI_FLASH,
instruction="Review the story in state['current_story']. Provide 1-2 sentences of constructive criticism.",
output_key="criticism",
)

reviser = LlmAgent(
name="Reviser", model=GEMINI_FLASH,
instruction="Revise the story in state['current_story'] based on criticism in state['criticism']. Output only the revised story.",
output_key="current_story", # Overwrites previous story
)

grammar_check = LlmAgent(
name="GrammarCheck", model=GEMINI_FLASH,
instruction="Check grammar of story in state['current_story']. Output corrections or 'Grammar is good!'.",
output_key="grammar_suggestions",
)

tone_check = LlmAgent(
name="ToneCheck", model=GEMINI_FLASH,
instruction="Analyze tone of story in state['current_story']. Output one word: 'positive', 'negative', or 'neutral'.",
output_key="tone_check_result", # Crucial for conditional logic
)
--8<-- "examples/python/snippets/agents/custom-agent/storyflow_agent.py:llmagents"
```

---
Expand All @@ -260,59 +118,7 @@ tone_check = LlmAgent(
Finally, you instantiate your `StoryFlowAgent` and use the `Runner` as usual.

```python
# agent.py (Instantiation and Runner Setup part)
from google.adk.sessions import InMemorySessionService
from google.adk.runners import Runner
from google.genai import types
import asyncio # Import asyncio

# Create the custom agent instance, passing in the LLM agents
story_flow_agent = StoryFlowAgent(
name="StoryFlowOrchestrator", # Give the orchestrator a distinct name
story_generator=story_generator,
critic=critic,
reviser=reviser,
grammar_check=grammar_check,
tone_check=tone_check,
)

# --- Setup Runner and Session (Example) ---
async def run_story_agent():
session_service = InMemorySessionService()
initial_state = {"topic": "a curious squirrel discovering sunglasses"}
session_id = "story_session_001"
user_id = "user_abc"
app_name = "story_creator_app"

session = session_service.create_session(
app_name=app_name, user_id=user_id, session_id=session_id, state=initial_state
)
logger.info(f"Initial session state: {session.state}")

runner = Runner(
agent=story_flow_agent, # Use the custom orchestrator agent
app_name=app_name,
session_service=session_service
)

# Trigger the agent (content here is just a trigger, actual topic is in state)
start_content = types.Content(role='user', parts=[types.Part(text="Start story flow.")])
events = []
async for event in runner.run_async(user_id=user_id, session_id=session_id, new_message=start_content):
events.append(event)
# You can inspect events here as they are yielded

print("\n--- Agent Workflow Complete ---")
final_session = session_service.get_session(app_name, user_id, session_id)
print("Final Session State:")
import json
print(json.dumps(final_session.state, indent=2))
print("-----------------------------\n")

if __name__ == "__main__":
logging.basicConfig(level=logging.INFO) # Ensure logging is configured
asyncio.run(run_story_agent())

--8<-- "examples/python/snippets/agents/custom-agent/storyflow_agent.py:story_flow_agent"
```

*(Note: The full runnable code, including imports and execution logic, can be found linked below.)*
Expand Down
10 changes: 7 additions & 3 deletions docs/tools/google-cloud-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ When running the agent, make sure to run adk web in project\_root\_folder

---

## 3. **GenAI Toolbox**
## 3. **Toolbox Tools for Databases**

[MCP Toolbox for Databases](https://github.com/googleapis/genai-toolbox) is an open source MCP server for databases. It was designed with enterprise-grade and production-quality in mind. It enables you to develop tools easier, faster, and more securely by handling the complexities such as connection pooling, authentication, and more.

Expand Down Expand Up @@ -272,14 +272,18 @@ pip install toolbox-langchain
Once you’ve Toolbox server is configured and up and running, you can load tools from your server using the ADK:

```py
from google.adk.tools import ToolboxTool toolbox = ToolboxTool("https://127.0.0.1:5000")
from google.adk.tools import ToolboxTool

toolbox = ToolboxTool("https://127.0.0.1:5000")

# Load all tools
tools = toolbox.get_toolset(),
# Load a specific set of tools
tools = toolbox.get_toolset(toolset_name='my-toolset-name'),
# Load single tool
tools = toolbox.get_toolset(tool_name='my-tool-name'), root_agent = Agent(
tools = toolbox.get_toolset(tool_name='my-tool-name'),

root_agent = Agent(
...,
tools=tools # Provide the list of tools to the Agent

Expand Down
31 changes: 26 additions & 5 deletions docs/tools/mcp-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,7 @@ You will create a standard Python MCP server application using the model-context
Install the MCP server library in the same environment as ADK:

```shell
pip install model-context-protocol
pip install mcp
```

### Step 1: Create the MCP Server Script
Expand Down Expand Up @@ -453,15 +453,36 @@ if __name__ == "__main__":

```

### Step 3: Run the MCP Server
### Step 3: Test your MCP Server with ADK

Execute the script from your terminal (ensure necessary libraries like model-context-protocol and google-adk are installed in your environment):
Follow the same instructions in “Example 1: File System MCP Server” and create a MCP client. This time use your MCP Server file created above as input command:

```py
# ./adk_agent_samples/mcp_agent/agent.py

# ...

async def get_tools_async():
"""Gets tools from the File System MCP Server."""
print("Attempting to connect to MCP Filesystem server...")
tools, exit_stack = await MCPToolset.from_server(
# Use StdioServerParameters for local process communication
connection_params=StdioServerParameters(
command='python3', # Command to run the server
args=[
"/absolute/path/to/adk_mcp_server.py"],
)
)
```

Execute the agent script from your terminal similar to above (ensure necessary libraries like model-context-protocol and google-adk are installed in your environment):

```shell
python adk_mcp_server.py
cd ./adk_agent_samples
python3 ./mcp_agent/agent.py
```

The script will print startup messages and then wait for an MCP client to connect via its standard input/output. Any MCP-compliant client (like Claude Desktop, or a custom client using the MCP libraries) can now connect to this process, discover the load\_web\_page tool, and invoke it. The server will print log messages indicating received requests and ADK tool execution. Refer to the [documentation](https://modelcontextprotocol.io/quickstart/server#core-mcp-concepts), to try it out with Claude Desktop.
The script will print startup messages and then wait for an MCP client to connect via its standard input/output to your MCP Server in adk\_mcp\_server.py. Any MCP-compliant client (like Claude Desktop, or a custom client using the MCP libraries) can now connect to this process, discover the load\_web\_page tool, and invoke it. The server will print log messages indicating received requests and ADK tool execution. Refer to the [documentation](https://modelcontextprotocol.io/quickstart/server#core-mcp-concepts), to try it out with Claude Desktop.

## Key considerations

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ def __init__(
sequential_agent=sequential_agent,
sub_agents=sub_agents_list, # Pass the sub_agents list directly
)
# --8<-- [end:init]
# --8<-- [end:init]

# --8<-- [start:executionlogic]
@override
Expand Down Expand Up @@ -215,7 +215,6 @@ async def _run_async_impl(
grammar_check=grammar_check,
tone_check=tone_check,
)
# --8<-- [end:story_flow_agent]

# --- Setup Runner and Session ---
session_service = InMemorySessionService()
Expand Down Expand Up @@ -271,4 +270,5 @@ def call_agent(user_input_topic: str):
print("-------------------------------\n")

# --- Run the Agent ---
call_agent("a lonely robot finding a friend in a junkyard")
call_agent("a lonely robot finding a friend in a junkyard")
# --8<-- [end:story_flow_agent]
1 change: 0 additions & 1 deletion mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,6 @@ nav:
- Local testing: deploy/local-testing.md
- Agent Engine: deploy/agent-engine.md
- Cloud Run: deploy/cloud-run.md
- Docker: deploy/docker.md
- Guides:
- Evaluate agents: guides/evaluate-agents.md
- Responsible Agents: guides/responsible-agents.md
Expand Down