Skip to main contentSkip to footer
PatternscriptbeginnerRunnableguided-flow

Reflection: draft, critique, revise

One pass drafts, a separate adversarial pass critiques it against a stated rubric, and a third rewrites using that critique. The critic is a distinct call with its own instruction, because a model asked to check its own work inside one completion tends to agree with itself.

Key Facts

Level
beginner
Runtime
Python • OpenAI API
Pattern
Inspectable flow with visible system boundaries
Interaction
Live sandbox • Script
Updated
27 July 2026

Navigate this example

High-level flow

How this example moves from input to execution and reviewable output

Reflection: draft, critique, revise -> Load environment keys -> Initialize OpenAI client -> Send chat request -> Constrain output schema -> Send message payload

Trigger

Reflection: draft, critique, revise

Runtime

Load environment keys

Outcome

Initialize OpenAI client

Why this page exists

This example is shown as both real source code and a product-facing interaction pattern so learners can connect implementation, UX, and doctrine without leaving the library.

Visual flowReal sourceSandbox or walkthroughMCP access

Doctrine relations

These are reasoning shapes: how an agent decides, when it acts, what it remembers. For how to compose the interface around it, see the AI Pattern Blueprint. For triggers, scheduling, context loading and recovery, see Agent runtime architecture.

Structural

This pattern expresses these principles directly in code.

  • P7 · Establish trust through inspectability

    The critique is a typed artifact rather than a hidden step: failing_rules and what_to_change are surfaced on every round, so a reader can see why a draft was rejected and judge whether the critic was right. A reflection loop that only prints the final text has thrown away the part worth inspecting.

    reflection.py:Critique

Default gap

The textbook form of this pattern violates these by default. Each one carries the fix.

  • P2 · Ensure that background work remains perceptible

    Each round is two or three model calls, so a three-round loop can run for a long time behind a single silent call. Fix, applied here: emit the round number and phase before each call, so a loop that is still working is distinguishable from one that has stalled.

    reflection.py:run

  • P10 · Optimise for steering, not only initiating

    The rubric is fixed at the start and the loop runs to acceptance or exhaustion. An operator watching a round fail for a reason they disagree with cannot amend the rubric or accept the draft as-is without restarting. Fix: check for an amended rubric or an operator override between rounds, and treat exhaustion as a decision point rather than a return.

    reflection.py:run

Depends on the wrapper

Your implementation decides these, not the pattern. No verdict is asserted.

  • P5 · Replace implied magic with clear mental models

    Whether the reader understands that the answer was revised, and how many times, is a wrapper decision. This implementation prints the arc; a version that returns only the accepted text presents a revised answer as if it were a first draft. The pattern does not decide which you get.

    reflection.py:run

Doctrine relations are first-party analysis, not the output of a validation run. Run the validator on your own code to get a scored verdict.

How should this example be used in the platform?

Use the sandbox to understand the experience pattern first, then inspect the source to see how the product boundary, model boundary, and doctrine boundary are actually implemented.

UX pattern: Inspectable flow with visible system boundaries
Ensure that background work remains perceptible
Replace implied magic with clear mental models
Establish trust through inspectability

Source references

Library entry
pattern-reflection
Source path
content/patterns/reasoning-reflection/reflection/reflection.py
Libraries
openai, pydantic, python-dotenv
Runtime requirements
OPENAI_API_KEY
Related principles
Ensure that background work remains perceptible, Replace implied magic with clear mental models, Establish trust through inspectability, Optimise for steering, not only initiating

reflection.py

python
"""
Reflection: draft, critique, revise

The cheapest reliability win available. One model drafts, a second pass
critiques that draft against a stated rubric, and a third rewrites using the
critique. Loop until the score clears the bar or the budget runs out.

    Draft -> Critique (scored) -> Revise -> ... -> Accept or exhaust

The critic is a separate call with a separate instruction, not the same call
asked to "check your work". A model grading its own output inside one
completion tends to agree with itself.

Two things worth knowing before you reach for this:

  - It costs you a multiple of the latency and tokens of a single call. Spend
    it where quality matters more than speed, not everywhere.
  - Scores from a model cluster hard around the middle of whatever range you
    give it. A 1 to 10 scale mostly returns 6, 7 and 8. Ask for specific
    boolean checks instead when you need a real gate.

Run: python reflection.py
"""

from dotenv import load_dotenv
from openai import OpenAI
from pydantic import BaseModel, Field

load_dotenv()

client = OpenAI()

MODEL = "gpt-5.4-mini"
MAX_ROUNDS = 3

#: What "good" means here, stated once so the critic and the reviser cannot
#: drift apart. Editing this changes the gate for both.
RUBRIC = """
1. Every claim is supported by something in the brief. No invented specifics.
2. States what is NOT covered, rather than implying full coverage.
3. Concrete nouns and verbs. No filler adjectives.
4. Under 120 words.
""".strip()


class Critique(BaseModel):
    """Boolean checks, not a 1-to-10 score.

    A model asked for a score returns the middle of the range almost
    regardless of quality. Specific checks force it to commit to something
    falsifiable, and they give the reviser an actionable list.
    """

    passes_rubric: bool = Field(description="True only if every numbered rule holds.")
    failing_rules: list[int] = Field(
        description="Rule numbers that fail. Empty when passes_rubric is true."
    )
    what_to_change: str = Field(
        description="Concrete instruction for the rewrite. Empty if nothing to change."
    )


def draft(brief: str) -> str:
    completion = client.chat.completions.create(
        model=MODEL,
        messages=[
            {
                "role": "system",
                "content": "Write a short internal summary for the given brief.",
            },
            {"role": "user", "content": brief},
        ],
    )
    return completion.choices[0].message.content or ""


def critique(brief: str, text: str) -> Critique:
    """A separate call with an adversarial instruction, not self-review."""
    completion = client.beta.chat.completions.parse(
        model=MODEL,
        messages=[
            {
                "role": "system",
                "content": (
                    "You are reviewing someone else's draft against a rubric. "
                    "You did not write it and you gain nothing by approving it. "
                    "Mark a rule as failing whenever you are unsure it holds.\n\n"
                    "Rubric:\n" + RUBRIC
                ),
            },
            {"role": "user", "content": f"Brief:\n{brief}\n\nDraft:\n{text}"},
        ],
        response_format=Critique,
    )
    return completion.choices[0].message.parsed


def revise(brief: str, text: str, verdict: Critique) -> str:
    completion = client.chat.completions.create(
        model=MODEL,
        messages=[
            {
                "role": "system",
                "content": (
                    "Rewrite the draft to fix exactly the listed problems. Change "
                    "nothing else. Do not add new claims.\n\nRubric:\n" + RUBRIC
                ),
            },
            {
                "role": "user",
                "content": (
                    f"Brief:\n{brief}\n\nDraft:\n{text}\n\n"
                    f"Failing rules: {verdict.failing_rules}\n"
                    f"Required change: {verdict.what_to_change}"
                ),
            },
        ],
    )
    return completion.choices[0].message.content or ""


def run(brief: str) -> str:
    print("  drafting...")
    text = draft(brief)
    print(f"      draft: {text[:90]}...")

    for round_number in range(1, MAX_ROUNDS + 1):
        print(f"  [{round_number}/{MAX_ROUNDS}] critiquing...")
        verdict = critique(brief, text)

        if verdict.passes_rubric:
            print(f"      ACCEPTED after {round_number} round(s)")
            print(f"      final: {text}")
            return text

        print(f"      failing rules: {verdict.failing_rules}")
        print(f"      change       : {verdict.what_to_change}")
        text = revise(brief, text, verdict)
        print(f"      revised: {text[:90]}...")

    # Budget exhausted is a real outcome. Return the best text with the
    # honest caveat rather than presenting it as if it passed.
    print(f"      NOT ACCEPTED after {MAX_ROUNDS} rounds, returning last revision")
    return text


if __name__ == "__main__":
    brief = (
        "Our nightly sync job failed twice this week. Both times it was the "
        "same downstream timeout. We have not yet found why the timeout "
        "started happening. Write the update for the team channel."
    )
    print(f"\n> {brief[:70]}...")
    run(brief)

What should the learner inspect in the code?

Look for the exact place where system scope is bounded: schema definitions, prompt framing, runtime configuration, and the call site that turns user intent into a concrete model or workflow action.

Look for output contracts and validation
Look for the exact execution call
Look for what the product could expose to the user

How does the sandbox relate to the source?

The sandbox should make the UX legible: what the user sees, what the system is deciding, and how the result becomes reviewable. The source then shows how that behavior is actually implemented.

Read the implementation summary.
Step through the user and system states.
Inspect the source code with the highlighted doctrine decisions in mind.
SandboxInspectable flow with visible system boundaries

Interaction walkthrough

Use the sandbox to step through the user-visible experience, the system work behind it, and the doctrine choice the example is making.

UX explanation

The sandbox explains what the user should see, what the system is doing, and where control or inspectability must remain explicit.

AI design explanation

The page turns raw source into a product-facing pattern: what the model is allowed to decide, what the product should expose, and where deterministic code or review should take over.

Interaction walkthrough

  1. 1Read the implementation summary.
  2. 2Step through the user and system states.
  3. 3Inspect the source code with the highlighted doctrine decisions in mind.

Visible to the user

One pass drafts, a separate adversarial pass critiques it against a stated rubric, and a third rewrites using that critique. The critic is a distinct call with its own instruction, because a model asked to check its own work inside one completion tends to agree with itself.

System work

The product prepares a bounded model or workflow task.

Why it matters

The interface should make the delegated task legible before automation happens.

Used in courses and paths

This example currently stands on its own in the library, but it still connects to the principle system and the broader example family.

Related principles

Runtime architecture

Use this example in your agents

This example is also available through the blueprint’s agent-ready layer. Use the For agents page for the public MCP, deterministic exports, and Claude/Cursor setup.

Define triggers, context, and boundaries before increasing autonomy
Make control, observability, and recovery explicit in the runtime
Choose the right operational patterns before delegating to workflows