Lab 20: Building an Essay Refinement System
Goal
In this lab, you will build a self-improving agent system that uses a Dynamic Workflow to iteratively refine an essay. You will implement the powerful "Critic -> Refiner" pattern using a standard Python loop.
The Architecture
- Initial Writer Node: An agent that creates the first draft.
- Refinement Loop Node (
@node): A function that orchestrates the iteration:- Critic Node: Evaluates the draft and provides feedback.
- Refiner Node: Applies feedback to improve the draft.
- Termination: The loop breaks if the Critic returns "APPROVED" or after 3 iterations.
Step 1: Create the Project Structure
- Create the project:
uv run adk create essay_refiner
Step 2: Define the Nodes and Orchestrator
Exercise: Open agent.py. Your task is to define the specialist agents and the @node function that runs the loop.
# In agent.py (Starter Code)
from google.adk import Agent, Context, Workflow
from google.adk.workflow import node
# 1. Define the Specialist Agents
# Note: write these instructions in plain language describing the input,
# not with {template} placeholders -- ctx.run_node()'s second argument
# becomes the node's input content directly, it does NOT populate {key}
# placeholders in the instruction (those only come from session state).
# TODO: Define the initial writer agent.
writer = ...
# TODO: Define the critic agent.
# It must return 'APPROVED' if the work is good, or feedback otherwise.
critic = ...
# TODO: Define the refiner agent.
# It must rewrite the story based on feedback.
refiner = ...
# 2. Define the Iterative Orchestrator
# rerun_on_resume=True is required on every @node used with ctx.run_node().
@node(rerun_on_resume=True)
async def refinement_orchestrator(ctx: Context, node_input: str):
# Step A: Get the initial draft from the 'writer'.
# Note: ctx.run_node()'s second argument is positional, not a keyword
# (there's no `input=` parameter).
current_story = await ctx.run_node(writer, f"Topic: {node_input}")
# Step B: Run the loop (max 3 times)
# 1. Call the 'critic' with ctx.run_node(critic, current_story)
# 2. Check if 'APPROVED' is in the feedback (break if so)
# 3. Call the 'refiner' to improve 'current_story' based on feedback
# [STUDENT TODO: Implement the loop here]
return current_story
root_agent = Workflow(
name="EssayRefiner",
edges=[("START", refinement_orchestrator)]
)
Step 3: Run and Test
- Start the Dev UI:
uv run adk web . - Observe the Trace:
Notice how each call to
ctx.run_node()appears in the trace. You can see the story evolving iteration by iteration.
Lab Summary
You have successfully built an iterative system!
- You used a Dynamic Workflow (
@node) to manage execution logic. - You used a standard Python loop to implement
max_iterations. - You learned how to pass data between nodes manually by passing the node's input as the positional second argument to
ctx.run_node()(there is noinputkeyword).
Self-Reflection Questions
- Why is the
max_iterationslimit a crucial safety feature for an iterative workflow? What could go wrong without it? - In our pattern, the
refinerreturns a new version of the story. How would you modify the loop to keep track of all versions in the session state? - Can you think of another problem, besides writing an essay, that could be solved effectively using a Dynamic Workflow loop?
🕵️ Hidden Solution 🕵️
Looking for the solution? Here's a hint (Base64 decode me):
L2RvYy1hZGstdHJhaW5pbmcvbW9kdWxlMjAtY3ljbGljLXdvcmtmbG93cy9sYWItc29sdXRpb24=
The direct link is: Lab Solution