Saturday, July 19, 2025

What are Common Multi-Agent Patterns using ADK Primitives

Coordinator/Dispatcher Pattern

Structure: A central LlmAgent (Coordinator) manages several specialized sub_agents.

Goal: Route incoming requests to the appropriate specialist agent.

ADK Primitives Used:

Hierarchy: Coordinator has specialists listed in sub_agents.

Interaction: Primarily uses LLM-Driven Delegation (requires clear descriptions on sub-agents and appropriate instruction on Coordinator) or Explicit Invocation (AgentTool) (Coordinator includes AgentTool-wrapped specialists in its tools).


# Conceptual Code: Coordinator using LLM Transfer

from google.adk.agents import LlmAgent


billing_agent = LlmAgent(name="Billing", description="Handles billing inquiries.")

support_agent = LlmAgent(name="Support", description="Handles technical support requests.")


coordinator = LlmAgent(

    name="HelpDeskCoordinator",

    model="gemini-2.0-flash",

    instruction="Route user requests: Use Billing agent for payment issues, Support agent for technical problems.",

    description="Main help desk router.",

    # allow_transfer=True is often implicit with sub_agents in AutoFlow

    sub_agents=[billing_agent, support_agent]

)

# User asks "My payment failed" -> Coordinator's LLM should call transfer_to_agent(agent_name='Billing')

# User asks "I can't log in" -> Coordinator's LLM should call transfer_to_agent(agent_name='Support')



Sequential Pipeline Pattern


Structure: A SequentialAgent contains sub_agents executed in a fixed order.

Goal: Implement a multi-step process where the output of one step feeds into the next.

ADK Primitives Used:

Workflow: SequentialAgent defines the order.

Communication: Primarily uses Shared Session State. Earlier agents write results (often via output_key), later agents read those results from context.state.


# Conceptual Code: Sequential Data Pipeline

from google.adk.agents import SequentialAgent, LlmAgent


validator = LlmAgent(name="ValidateInput", instruction="Validate the input.", output_key="validation_status")

processor = LlmAgent(name="ProcessData", instruction="Process data if {validation_status} is 'valid'.", output_key="result")

reporter = LlmAgent(name="ReportResult", instruction="Report the result from {result}.")


data_pipeline = SequentialAgent(

    name="DataPipeline",

    sub_agents=[validator, processor, reporter]

)

# validator runs -> saves to state['validation_status']

# processor runs -> reads state['validation_status'], saves to state['result']

# reporter runs -> reads state['result']




Parallel Fan-Out/Gather Pattern

Structure: A ParallelAgent runs multiple sub_agents concurrently, often followed by a later agent (in a SequentialAgent) that aggregates results.

Goal: Execute independent tasks simultaneously to reduce latency, then combine their outputs.

ADK Primitives Used:

Workflow: ParallelAgent for concurrent execution (Fan-Out). Often nested within a SequentialAgent to handle the subsequent aggregation step (Gather).

Communication: Sub-agents write results to distinct keys in Shared Session State. The subsequent "Gather" agent reads multiple state keys.


# Conceptual Code: Parallel Information Gathering

from google.adk.agents import SequentialAgent, ParallelAgent, LlmAgent


fetch_api1 = LlmAgent(name="API1Fetcher", instruction="Fetch data from API 1.", output_key="api1_data")

fetch_api2 = LlmAgent(name="API2Fetcher", instruction="Fetch data from API 2.", output_key="api2_data")


gather_concurrently = ParallelAgent(

    name="ConcurrentFetch",

    sub_agents=[fetch_api1, fetch_api2]

)


synthesizer = LlmAgent(

    name="Synthesizer",

    instruction="Combine results from {api1_data} and {api2_data}."

)


overall_workflow = SequentialAgent(

    name="FetchAndSynthesize",

    sub_agents=[gather_concurrently, synthesizer] # Run parallel fetch, then synthesize

)

# fetch_api1 and fetch_api2 run concurrently, saving to state.

# synthesizer runs afterwards, reading state['api1_data'] and state['api2_data'].




Hierarchical Task Decomposition¶

Structure: A multi-level tree of agents where higher-level agents break down complex goals and delegate sub-tasks to lower-level agents.

Goal: Solve complex problems by recursively breaking them down into simpler, executable steps.

ADK Primitives Used:

Hierarchy: Multi-level parent_agent/sub_agents structure.

Interaction: Primarily LLM-Driven Delegation or Explicit Invocation (AgentTool) used by parent agents to assign tasks to subagents. Results are returned up the hierarchy (via tool responses or state).


# Conceptual Code: Hierarchical Research Task

from google.adk.agents import LlmAgent

from google.adk.tools import agent_tool


# Low-level tool-like agents

web_searcher = LlmAgent(name="WebSearch", description="Performs web searches for facts.")

summarizer = LlmAgent(name="Summarizer", description="Summarizes text.")


# Mid-level agent combining tools

research_assistant = LlmAgent(

    name="ResearchAssistant",

    model="gemini-2.0-flash",

    description="Finds and summarizes information on a topic.",

    tools=[agent_tool.AgentTool(agent=web_searcher), agent_tool.AgentTool(agent=summarizer)]

)


# High-level agent delegating research

report_writer = LlmAgent(

    name="ReportWriter",

    model="gemini-2.0-flash",

    instruction="Write a report on topic X. Use the ResearchAssistant to gather information.",

    tools=[agent_tool.AgentTool(agent=research_assistant)]

    # Alternatively, could use LLM Transfer if research_assistant is a sub_agent

)

# User interacts with ReportWriter.

# ReportWriter calls ResearchAssistant tool.

# ResearchAssistant calls WebSearch and Summarizer tools.

# Results flow back up.


Review/Critique Pattern (Generator-Critic)


Structure: Typically involves two agents within a SequentialAgent: a Generator and a Critic/Reviewer.

Goal: Improve the quality or validity of generated output by having a dedicated agent review it.

ADK Primitives Used:

Workflow: SequentialAgent ensures generation happens before review.

Communication: Shared Session State (Generator uses output_key to save output; Reviewer reads that state key). The Reviewer might save its feedback to another state key for subsequent steps.



# Conceptual Code: Generator-Critic

from google.adk.agents import SequentialAgent, LlmAgent


generator = LlmAgent(

    name="DraftWriter",

    instruction="Write a short paragraph about subject X.",

    output_key="draft_text"

)


reviewer = LlmAgent(

    name="FactChecker",

    instruction="Review the text in {draft_text} for factual accuracy. Output 'valid' or 'invalid' with reasons.",

    output_key="review_status"

)


# Optional: Further steps based on review_status


review_pipeline = SequentialAgent(

    name="WriteAndReview",

    sub_agents=[generator, reviewer]

)

# generator runs -> saves draft to state['draft_text']

# reviewer runs -> reads state['draft_text'], saves status to state['review_status']




Iterative Refinement Pattern

Structure: Uses a LoopAgent containing one or more agents that work on a task over multiple iterations.

Goal: Progressively improve a result (e.g., code, text, plan) stored in the session state until a quality threshold is met or a maximum number of iterations is reached.

ADK Primitives Used:

Workflow: LoopAgent manages the repetition.

Communication: Shared Session State is essential for agents to read the previous iteration's output and save the refined version.

Termination: The loop typically ends based on max_iterations or a dedicated checking agent setting escalate=True in the Event Actions when the result is satisfactory.


# Conceptual Code: Iterative Code Refinement

from google.adk.agents import LoopAgent, LlmAgent, BaseAgent

from google.adk.events import Event, EventActions

from google.adk.agents.invocation_context import InvocationContext

from typing import AsyncGenerator


# Agent to generate/refine code based on state['current_code'] and state['requirements']

code_refiner = LlmAgent(

    name="CodeRefiner",

    instruction="Read state['current_code'] (if exists) and state['requirements']. Generate/refine Python code to meet requirements. Save to state['current_code'].",

    output_key="current_code" # Overwrites previous code in state

)


# Agent to check if the code meets quality standards

quality_checker = LlmAgent(

    name="QualityChecker",

    instruction="Evaluate the code in state['current_code'] against state['requirements']. Output 'pass' or 'fail'.",

    output_key="quality_status"

)


# Custom agent to check the status and escalate if 'pass'

class CheckStatusAndEscalate(BaseAgent):

    async def _run_async_impl(self, ctx: InvocationContext) -> AsyncGenerator[Event, None]:

        status = ctx.session.state.get("quality_status", "fail")

        should_stop = (status == "pass")

        yield Event(author=self.name, actions=EventActions(escalate=should_stop))


refinement_loop = LoopAgent(

    name="CodeRefinementLoop",

    max_iterations=5,

    sub_agents=[code_refiner, quality_checker, CheckStatusAndEscalate(name="StopChecker")]

)

# Loop runs: Refiner -> Checker -> StopChecker

# State['current_code'] is updated each iteration.

# Loop stops if QualityChecker outputs 'pass' (leading to StopChecker escalating) or after 5 iterations.



Human-in-the-Loop Pattern


Structure: Integrates human intervention points within an agent workflow.

Goal: Allow for human oversight, approval, correction, or tasks that AI cannot perform.

ADK Primitives Used (Conceptual):

Interaction: Can be implemented using a custom Tool that pauses execution and sends a request to an external system (e.g., a UI, ticketing system) waiting for human input. The tool then returns the human's response to the agent.

Workflow: Could use LLM-Driven Delegation (transfer_to_agent) targeting a conceptual "Human Agent" that triggers the external workflow, or use the custom tool within an LlmAgent.

State/Callbacks: State can hold task details for the human; callbacks can manage the interaction flow.

Note: ADK doesn't have a built-in "Human Agent" type, so this requires custom integration.


# Conceptual Code: Using a Tool for Human Approval

from google.adk.agents import LlmAgent, SequentialAgent

from google.adk.tools import FunctionTool


# --- Assume external_approval_tool exists ---

# This tool would:

# 1. Take details (e.g., request_id, amount, reason).

# 2. Send these details to a human review system (e.g., via API).

# 3. Poll or wait for the human response (approved/rejected).

# 4. Return the human's decision.

# async def external_approval_tool(amount: float, reason: str) -> str: ...

approval_tool = FunctionTool(func=external_approval_tool)


# Agent that prepares the request

prepare_request = LlmAgent(

    name="PrepareApproval",

    instruction="Prepare the approval request details based on user input. Store amount and reason in state.",

    # ... likely sets state['approval_amount'] and state['approval_reason'] ...

)


# Agent that calls the human approval tool

request_approval = LlmAgent(

    name="RequestHumanApproval",

    instruction="Use the external_approval_tool with amount from state['approval_amount'] and reason from state['approval_reason'].",

    tools=[approval_tool],

    output_key="human_decision"

)


# Agent that proceeds based on human decision

process_decision = LlmAgent(

    name="ProcessDecision",

    instruction="Check {human_decision}. If 'approved', proceed. If 'rejected', inform user."

)


approval_workflow = SequentialAgent(

    name="HumanApprovalWorkflow",

    sub_agents=[prepare_request, request_approval, process_decision]

)





references:

https://google.github.io/adk-docs/agents/multi-agents/#c-explicit-invocation-agenttool


No comments:

Post a Comment