Build a Multi-Agent Research Pipeline

Orchestrate specialist AI agents to produce research reports using Groq.

Introduction

30 Second Summary

Think about how a research team works in real life. One person gathers information, another analyzes the data for patterns, and a third writes up the findings into a polished report.

In this project, you will build a multi-agent AI pipeline where an orchestrator agent coordinates three specialist agents to produce structured research reports from any topic you give it.

What You'll Build

A Python multi-agent research pipeline using the OpenAI Agents SDK and Groq's free LLM inference to orchestrate specialist agents that research, analyze, and write reports.

By the end of this project, you'll have:

  • A modular agent architecture with three specialists (Researcher, Analyst, Writer) coordinated by an Orchestrator using the agents-as-tools pattern.
  • Custom function tools that demonstrate how agents interact with external data sources through tool calling.
  • Production-aware error handling with rate-limit retries, max-turn limits, and structured logging for debugging the orchestration flow.
  • Secret Mission: An LLM-as-a-Judge evaluator agent that scores report quality and triggers a revision loop if the output falls below a quality threshold.

Are there any prerequisites?

You should have basic Python knowledge (variables, functions, imports) and some familiarity with AI or LLM concepts. No prior experience with agent frameworks is needed.

Before We Start

Before We Start...

Set Up Your Environment

Building a multi-agent system requires a few foundational pieces: a project folder to keep your code organized, a virtual environment to manage dependencies, and an API connection to the LLM that powers your agents.

In this step, you will create the project structure, install the OpenAI Agents SDK, connect to Groq's free inference API, and confirm everything works with a quick test.

In this step, get ready to:
  • Create your project folder, virtual environment, and install dependencies.
  • Sign up for Groq and configure your API key.
  • Verify the setup by running a test agent that connects to Groq.
Create your project folder and install dependencies

Every Python project benefits from its own folder and virtual environment. A virtual environment isolates your project's packages from the rest of your system so nothing conflicts.

  • Open your terminal by pressing Cmd+Space (macOS) or the Windows key (Windows), typing Terminal (macOS) or PowerShell (Windows), and pressing Enter.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir research-pipeline
cd research-pipeline
  • Create a Python virtual environment by running:
python3 -m venv venv
  • Activate the virtual environment. The command differs by operating system:

macOS

source venv/bin/activate

Windows

venv\Scripts\activate

You should see (venv) appear at the beginning of your terminal prompt. This confirms the virtual environment is active.

Don't see (venv) in your prompt?

On Windows PowerShell, you may need to allow script execution first. Run Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser and then try the activation command again.

On macOS, make sure you used source before the path. Without it, the script won't modify your current shell session.

Help me activate my virtual environment.

Now you will define the exact package versions this project uses, then install them.

  • Open VS Code by pressing Cmd+Space (macOS) or the Windows key (Windows), typing VS Code, and pressing Enter.
  • In VS Code, click File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop, select the research-pipeline folder, and click Open.
  • Create a new file by clicking File then New File.
  • Name the file requirements.txt and press Enter.
  • Paste the following into requirements.txt:
openai-agents==0.17.7
python-dotenv==1.1.0

What does this file do?

A requirements.txt file pins the exact package versions your project depends on. This ensures anyone running your code gets the same behavior.

  • openai-agents is the OpenAI Agents SDK, which provides the framework for building multi-agent systems.
  • python-dotenv loads environment variables from a .env file so your API keys stay out of source code.
  • Save requirements.txt by pressing Cmd+S (macOS) or Ctrl+S (Windows).
  • Open the VS Code integrated terminal by pressing Ctrl+` (backtick key, same on both platforms).
  • Make sure your virtual environment is active in this terminal. You should see (venv) in the prompt. If not, activate it again using the platform-appropriate command from above.
  • Install the dependencies by running:
pip install -r requirements.txt

You should see output showing both packages downloading and installing successfully. The final line should say something like "Successfully installed".

Installation failed?

Make sure (venv) appears in your prompt before running pip install. Installing without an active virtual environment can cause permission errors.

If you see a network error, check your internet connection and try running the command again.

Help me fix my pip install error.

Sign up for Groq and create your API key

Groq provides free LLM inference with no credit card required. Your agents will send prompts to Groq's API and receive completions back.

  • Open your browser and go to:
https://console.groq.com
  • Click Sign Up and create an account using your email or GitHub.
  • Once signed in, navigate to the API Keys page at:
https://console.groq.com/keys
  • Click Create API Key.
  • Give your key a name (for example, research-pipeline) and click Submit.
  • Copy the API key immediately. You will not be able to see it again after closing this dialog.

Why Groq?

Groq's free tier gives you 30 requests per minute and 14,400 requests per day. That is more than enough for developing and testing a multi-agent pipeline.

Groq also supports the standard OpenAI tools[] schema for function calling, which is exactly what the OpenAI Agents SDK uses under the hood.

Now store this key securely in an environment file that your code can read.

  • In VS Code, create a new file called .env in the root of your research-pipeline folder.
  • Paste the following into .env, replacing your-groq-api-key-here with the API key you just copied:
GROQ_API_KEY=your-groq-api-key-here
OPENAI_AGENTS_DISABLE_TRACING=1

What is OPENAI_AGENTS_DISABLE_TRACING?

The OpenAI Agents SDK includes a tracing feature designed for OpenAI's platform. Since you are using Groq instead, this setting suppresses warning messages that would otherwise clutter your terminal output.

  • Save .env by pressing Cmd+S (macOS) or Ctrl+S (Windows).

Can't create an API key?

Some users report that signing up with Google OAuth can cause a permissions issue where the Create API Key button is disabled. If this happens, try signing up with a direct email address (a standard Gmail account works well).

Help me get my Groq API key working.

Create a test agent and verify the connection

Before building the full pipeline, you need to confirm that the Agents SDK can successfully reach Groq and get a response. This requires a small configuration module and a test script.

  • Create a folder called agents inside your research-pipeline project. In VS Code, right-click in the Explorer sidebar and select New Folder.
  • Name the folder agents.
  • Inside the agents folder, create a file called __init__.py and leave it empty. This tells Python to treat the folder as a package.
  • Inside the agents folder, create another file called config.py and paste the following:
import os
from dotenv import load_dotenv
from openai import AsyncOpenAI
from agents.models.openai_chatcompletions import OpenAIChatCompletionsModel

load_dotenv()


def get_groq_model():
    """Create a Groq-backed model for the OpenAI Agents SDK."""
    groq_client = AsyncOpenAI(
        api_key=os.environ.get("GROQ_API_KEY"),
        base_url="https://api.groq.com/openai/v1",
    )
    return OpenAIChatCompletionsModel(
        model="llama-3.3-70b-versatile",
        openai_client=groq_client,
    )

What does this config do?

  • load_dotenv() reads your .env file and loads GROQ_API_KEY into the environment.
  • AsyncOpenAI creates an async HTTP client pointed at Groq's API endpoint instead of OpenAI's.
  • OpenAIChatCompletionsModel wraps this client so the Agents SDK can use Groq's llama-3.3-70b-versatile model as if it were an OpenAI model.
  • The get_groq_model() function returns a reusable model object that every agent in your pipeline will share.
  • Save agents/config.py.
  • Now create a test script. In the root of your research-pipeline folder (not inside agents/), create a new file called test_setup.py and paste the following:
import asyncio
from agents import Agent, Runner
from agents.config import get_groq_model


async def main():
    # Create a simple test agent using Groq
    agent = Agent(
        name="Test Agent",
        instructions="You are a helpful assistant. Respond in one sentence.",
        model=get_groq_model(),
    )
    # Run the agent with a simple prompt
    result = await Runner.run(agent, input="Say hello and confirm you are working!")
    print(result.final_output)


if __name__ == "__main__":
    asyncio.run(main())

What does this test do?

  • It creates a minimal Agent with a name, simple instructions, and the Groq model from your config.
  • Runner.run() sends the prompt to Groq through the Agents SDK and returns the model's response.
  • If this script prints a response, your entire stack (SDK, Groq connection, API key, virtual environment) is working.
  • Save test_setup.py.
  • Run the test script in your VS Code terminal:
python test_setup.py

You should see a short greeting or confirmation message printed in the terminal. The exact wording will vary, but any response from the model confirms your setup is working.

Seeing an error instead of a response?

If you see an authentication error, double-check that your .env file is in the root of your research-pipeline folder (not inside agents/) and that GROQ_API_KEY has no extra spaces around the = sign.

If you see a module import error mentioning agents, confirm you ran pip install -r requirements.txt with the virtual environment active and that the agents/__init__.py file exists (even if empty).

If you see a rate limit error (429), wait about 60 seconds and try again.

Help me debug my test_setup.py error.

✔️ Awesome, I've got everything!

Great. Double check you have saved all your files.

ⓧ I'd like to double check the full code

Here is what each file should look like at this point:

.env

GROQ_API_KEY=your-groq-api-key-here
OPENAI_AGENTS_DISABLE_TRACING=1

requirements.txt

openai-agents==0.17.7
python-dotenv==1.1.0

agents/__init__.py (empty file, no content needed).

agents/config.py

import os
from dotenv import load_dotenv
from openai import AsyncOpenAI
from agents.models.openai_chatcompletions import OpenAIChatCompletionsModel

load_dotenv()


def get_groq_model():
    """Create a Groq-backed model for the OpenAI Agents SDK."""
    groq_client = AsyncOpenAI(
        api_key=os.environ.get("GROQ_API_KEY"),
        base_url="https://api.groq.com/openai/v1",
    )
    return OpenAIChatCompletionsModel(
        model="llama-3.3-70b-versatile",
        openai_client=groq_client,
    )

Your environment is ready and your Groq connection is confirmed. Next up, you will build the three specialist agents that form the core of your research pipeline.

Build the Specialist Agents

Your environment is set up and verified. Now it's time to build the agents that will do the actual work in your research pipeline.

In a multi-agent system, each specialist agent has a focused job and its own tools. You'll create three: a Researcher that searches for information, an Analyst that extracts insights from raw data, and a Writer that produces polished reports.

In this step, get ready to:
  • Create a Researcher agent with a simulated web search tool.
  • Create an Analyst agent with a data extraction tool.
  • Create a Writer agent and test all three specialists individually.
Create the Researcher agent with a search tool

The Researcher agent needs a way to gather information. In the OpenAI Agents SDK, you give agents capabilities by attaching function tools. The @function_tool decorator turns any Python function into a tool the LLM can call.

For this project, you'll simulate a web search tool that returns structured results. In production, you'd swap this for a real API call. The pattern stays the same.

  • Create a new file called researcher.py inside your agents/ folder by running this command:
touch agents/researcher.py
  • Open agents/researcher.py in VS Code and paste in the following code:
from agents import Agent, function_tool
from agents.config import get_groq_model


@function_tool
def search_web(query: str) -> str:
    """Search the web for information on a topic.

    Args:
        query: The search query to look up.
    """
    results = [
        {
            "title": f"Research findings on: {query}",
            "snippet": f"Recent studies show significant developments in {query}. "
            f"Experts highlight key trends including automation, scalability, "
            f"and cross-domain applications. Market growth is estimated at 15-25% annually.",
            "source": "Academic Research Database",
        },
        {
            "title": f"Industry report: {query}",
            "snippet": f"Industry leaders report that {query} is transforming workflows. "
            f"Key challenges include integration complexity, cost management, "
            f"and skill gaps. Adoption rates have doubled in the past 18 months.",
            "source": "Industry Analysis Report",
        },
        {
            "title": f"Technical overview: {query}",
            "snippet": f"Technical deep-dive into {query} reveals core components: "
            f"distributed architectures, event-driven processing, and intelligent routing. "
            f"Performance benchmarks show 3-5x improvements over traditional approaches.",
            "source": "Technical Review Journal",
        },
    ]

    output = ""
    for i, result in enumerate(results, 1):
        output += f"\n[{i}] {result['title']}\n"
        output += f"    Source: {result['source']}\n"
        output += f"    {result['snippet']}\n"
    return output


def create_researcher():
    """Create the Researcher specialist agent."""
    return Agent(
        name="Researcher",
        instructions=(
            "You are a research specialist. When given a topic, use the search_web tool "
            "to gather information. Make multiple searches with different angles to get "
            "comprehensive coverage. Compile all findings into a structured summary with "
            "sources cited."
        ),
        tools=[search_web],
        model=get_groq_model(),
    )

What does this code do?

This file defines two things: a tool and an agent that uses it.

  • The @function_tool decorator on search_web turns it into a callable tool. The SDK automatically reads the function's type annotations and docstring to build the tool's schema for the LLM.
  • The search_web function returns three simulated search results formatted as numbered entries with titles, sources, and snippets.
  • The create_researcher() function builds an Agent with a name, instructions (its system prompt), the search tool, and the Groq model from your config.
  • Save agents/researcher.py.
  • Confirm the file exists by checking the agents/ folder in the VS Code **Explorer** sidebar. You should see researcher.py listed alongside __init__.py and config.py.

File not showing up?

  • Make sure you created it inside the agents/ folder, not in the project root.
  • Check for typos in the filename. It must be researcher.py (all lowercase).

Why isn't my researcher.py file appearing in the agents folder?

✔️ Awesome, I've got everything!

Great. Make sure the file is saved before moving on.

ⓧ I'd like to double check the full code

from agents import Agent, function_tool
from agents.config import get_groq_model


@function_tool
def search_web(query: str) -> str:
    """Search the web for information on a topic.

    Args:
        query: The search query to look up.
    """
    results = [
        {
            "title": f"Research findings on: {query}",
            "snippet": f"Recent studies show significant developments in {query}. "
            f"Experts highlight key trends including automation, scalability, "
            f"and cross-domain applications. Market growth is estimated at 15-25% annually.",
            "source": "Academic Research Database",
        },
        {
            "title": f"Industry report: {query}",
            "snippet": f"Industry leaders report that {query} is transforming workflows. "
            f"Key challenges include integration complexity, cost management, "
            f"and skill gaps. Adoption rates have doubled in the past 18 months.",
            "source": "Industry Analysis Report",
        },
        {
            "title": f"Technical overview: {query}",
            "snippet": f"Technical deep-dive into {query} reveals core components: "
            f"distributed architectures, event-driven processing, and intelligent routing. "
            f"Performance benchmarks show 3-5x improvements over traditional approaches.",
            "source": "Technical Review Journal",
        },
    ]

    output = ""
    for i, result in enumerate(results, 1):
        output += f"\n[{i}] {result['title']}\n"
        output += f"    Source: {result['source']}\n"
        output += f"    {result['snippet']}\n"
    return output


def create_researcher():
    """Create the Researcher specialist agent."""
    return Agent(
        name="Researcher",
        instructions=(
            "You are a research specialist. When given a topic, use the search_web tool "
            "to gather information. Make multiple searches with different angles to get "
            "comprehensive coverage. Compile all findings into a structured summary with "
            "sources cited."
        ),
        tools=[search_web],
        model=get_groq_model(),
    )
Create the Analyst agent with a data extraction tool

The Analyst takes raw research output and structures it into actionable insights. Its tool simulates the kind of data processing that a real analytics pipeline would handle.

  • Create a new file called analyst.py inside your agents/ folder by running:
touch agents/analyst.py
  • Open agents/analyst.py and paste in the following code:
from agents import Agent, function_tool
from agents.config import get_groq_model


@function_tool
def extract_insights(raw_text: str) -> str:
    """Analyze raw research text and extract structured insights.

    Args:
        raw_text: The raw research text to analyze.
    """
    analysis = (
        "EXTRACTED INSIGHTS:\n"
        "---\n"
        "Key Findings:\n"
        "- Strong growth trajectory identified (15-25% annual growth)\n"
        "- Adoption rates doubling every 18 months\n"
        "- 3-5x performance improvements over traditional methods\n"
        "\n"
        "Themes:\n"
        "1. Automation and scalability are primary drivers\n"
        "2. Integration complexity remains top challenge\n"
        "3. Cross-domain applications expanding rapidly\n"
        "\n"
        "Statistics:\n"
        "- Growth: 15-25% annually\n"
        "- Performance: 3-5x improvement\n"
        "- Adoption: 2x increase in 18 months\n"
        "\n"
        "Challenges:\n"
        "- Integration complexity\n"
        "- Cost management\n"
        "- Skill gaps in workforce\n"
        "---"
    )
    return analysis


def create_analyst():
    """Create the Analyst specialist agent."""
    return Agent(
        name="Analyst",
        instructions=(
            "You are a data analyst specialist. When given raw research findings, "
            "use the extract_insights tool to process the text and identify key patterns. "
            "Then provide your own analytical commentary on the significance of the findings, "
            "potential implications, and areas that need further investigation."
        ),
        tools=[extract_insights],
        model=get_groq_model(),
    )

What does this code do?

The Analyst follows the same pattern as the Researcher: a @function_tool-decorated function paired with an agent creation function.

  • The extract_insights tool takes raw text and returns a structured breakdown of key findings, themes, statistics, and challenges.
  • The Analyst's instructions tell it to call the tool first, then add its own commentary. This separation of tool output and agent reasoning is a common production pattern.
  • Save agents/analyst.py.
  • Check the VS Code **Explorer** sidebar. You should now see analyst.py in your agents/ folder.

Seeing a syntax error?

The multi-line string uses parentheses for implicit string concatenation. Make sure each line inside the parentheses ends with \n" and not a stray comma or missing quote.

Help me fix a syntax error in my analyst.py file

✔️ Awesome, I've got everything!

Great. Make sure the file is saved before moving on.

ⓧ I'd like to double check the full code

from agents import Agent, function_tool
from agents.config import get_groq_model


@function_tool
def extract_insights(raw_text: str) -> str:
    """Analyze raw research text and extract structured insights.

    Args:
        raw_text: The raw research text to analyze.
    """
    analysis = (
        "EXTRACTED INSIGHTS:\n"
        "---\n"
        "Key Findings:\n"
        "- Strong growth trajectory identified (15-25% annual growth)\n"
        "- Adoption rates doubling every 18 months\n"
        "- 3-5x performance improvements over traditional methods\n"
        "\n"
        "Themes:\n"
        "1. Automation and scalability are primary drivers\n"
        "2. Integration complexity remains top challenge\n"
        "3. Cross-domain applications expanding rapidly\n"
        "\n"
        "Statistics:\n"
        "- Growth: 15-25% annually\n"
        "- Performance: 3-5x improvement\n"
        "- Adoption: 2x increase in 18 months\n"
        "\n"
        "Challenges:\n"
        "- Integration complexity\n"
        "- Cost management\n"
        "- Skill gaps in workforce\n"
        "---"
    )
    return analysis


def create_analyst():
    """Create the Analyst specialist agent."""
    return Agent(
        name="Analyst",
        instructions=(
            "You are a data analyst specialist. When given raw research findings, "
            "use the extract_insights tool to process the text and identify key patterns. "
            "Then provide your own analytical commentary on the significance of the findings, "
            "potential implications, and areas that need further investigation."
        ),
        tools=[extract_insights],
        model=get_groq_model(),
    )
Create the Writer agent and test each specialist

The Writer is the simplest agent. It has no tools because its job is purely generative: take research and analysis as input and produce a polished markdown report.

  • Create a new file called writer.py inside your agents/ folder by running:
touch agents/writer.py
  • Open agents/writer.py and paste in the following code:
from agents import Agent
from agents.config import get_groq_model


def create_writer():
    """Create the Writer specialist agent."""
    return Agent(
        name="Writer",
        instructions=(
            "You are a professional technical writer. Given research findings and analysis, "
            "produce a polished markdown research report with the following structure:\n\n"
            "# [Topic] Research Report\n\n"
            "## Executive Summary\n"
            "A 2-3 sentence overview of the key findings.\n\n"
            "## Key Findings\n"
            "Bullet points of the most important discoveries.\n\n"
            "## Analysis\n"
            "A paragraph discussing implications and significance.\n\n"
            "## Challenges and Risks\n"
            "Known obstacles and potential issues.\n\n"
            "## Conclusion\n"
            "A brief forward-looking conclusion.\n\n"
            "Write clearly and concisely. Use data from the research where available."
        ),
        model=get_groq_model(),
    )

Why doesn't the Writer have tools?

Not every agent needs tools. The Writer's job is to synthesize information it receives as input. Its instructions contain a detailed report template that guides the LLM's output format.

This is a deliberate design choice. Agents with tools are best for tasks that require external data. Agents without tools are best for transformation and generation tasks.

  • Save agents/writer.py.

✔️ Awesome, I've got everything!

Great. Now let's test these agents to make sure they work.

ⓧ I'd like to double check the full code

from agents import Agent
from agents.config import get_groq_model


def create_writer():
    """Create the Writer specialist agent."""
    return Agent(
        name="Writer",
        instructions=(
            "You are a professional technical writer. Given research findings and analysis, "
            "produce a polished markdown research report with the following structure:\n\n"
            "# [Topic] Research Report\n\n"
            "## Executive Summary\n"
            "A 2-3 sentence overview of the key findings.\n\n"
            "## Key Findings\n"
            "Bullet points of the most important discoveries.\n\n"
            "## Analysis\n"
            "A paragraph discussing implications and significance.\n\n"
            "## Challenges and Risks\n"
            "Known obstacles and potential issues.\n\n"
            "## Conclusion\n"
            "A brief forward-looking conclusion.\n\n"
            "Write clearly and concisely. Use data from the research where available."
        ),
        model=get_groq_model(),
    )

Now let's verify the agents actually run. You'll test the Researcher since it exercises tool calling, the most interesting part of the system.

  • Create a temporary test file by running:
touch test_agents.py
  • Open test_agents.py and paste in the following code:
import asyncio
from dotenv import load_dotenv
from agents import Runner
from agents.researcher import create_researcher

# Load environment variables
load_dotenv()


async def test():
    # Create and run the Researcher agent
    researcher = create_researcher()
    result = await Runner.run(researcher, input="Research the topic: cloud computing")
    print("=== Researcher Output ===")
    print(result.final_output[:500])


asyncio.run(test())
  • Save test_agents.py, then run it with:
python test_agents.py

You should see the Researcher's output printed to your terminal. It will include compiled findings from the simulated search results, showing that the agent successfully called the search_web tool.

Getting a rate limit or connection error?

  • If you see a 429 error, wait 60 seconds and try again. Groq's free tier allows 30 requests per minute.
  • If you see an authentication error, double-check that your .env file contains GROQ_API_KEY with a valid key from console.groq.com/keys.
  • If you see an import error mentioning agents, make sure your virtual environment is active (you should see (venv) in your terminal prompt).

Help me debug an error when running test_agents.py

Test the other agents too

You can modify test_agents.py to test the Analyst or Writer by importing create_analyst or create_writer and passing them different input. Each agent should return a response without errors.

  • Once you've confirmed the test works, delete the temporary test file by running:
rm test_agents.py

All three specialist agents are built and ready. Next up, you'll wire them together with an Orchestrator that calls each one in sequence to produce a full research report.

Wire Up the Orchestrator

Your three specialist agents can each do their job independently. Now you need a manager that knows when to call each one and how to pass results between them.

In this step, you'll build an Orchestrator agent that exposes each specialist as a callable tool. The orchestrator decides the order of operations: research first, then analysis, then writing. This is the agents-as-tools pattern, and it's how production AI systems coordinate multiple specialists without hardcoding the flow.

In this step, get ready to:
  • Create an orchestrator agent that uses each specialist as a tool.
  • Implement a pipeline runner with rate-limit retry logic.
  • Build a CLI entry point that runs the full pipeline end-to-end.
Create the orchestrator agent

The agents-as-tools pattern works by calling agent.as_tool() on each specialist. This wraps the entire agent (its instructions, model, and tools) into a single callable tool that the orchestrator can invoke just like any other function tool.

  • Create a new file called orchestrator.py inside your agents/ folder.
  • Add the imports and the create_orchestrator() function by pasting this code:
import asyncio
import re
from agents import Agent, Runner
from agents.config import get_groq_model
from agents.researcher import create_researcher
from agents.analyst import create_analyst
from agents.writer import create_writer


def create_orchestrator():
    """Create the Orchestrator agent with specialists as tools."""
    researcher = create_researcher()
    analyst = create_analyst()
    writer = create_writer()

    orchestrator = Agent(
        name="Research Pipeline Orchestrator",
        instructions=(
            "You manage a research pipeline. For any given topic:\n"
            "1. Call research_topic to gather comprehensive information\n"
            "2. Call analyze_findings with the research results to extract insights\n"
            "3. Call write_report with both the research and analysis to produce the final report\n\n"
            "Always follow this sequence. Pass the full output from each step to the next. "
            "Return the final report from the writer as your output."
        ),
        tools=[
            researcher.as_tool(
                tool_name="research_topic",
                tool_description="Research a topic by searching for information from multiple sources. Pass the topic as input.",
            ),
            analyst.as_tool(
                tool_name="analyze_findings",
                tool_description="Analyze raw research findings to extract key insights, themes, and statistics. Pass the research text as input.",
            ),
            writer.as_tool(
                tool_name="write_report",
                tool_description="Write a polished markdown research report from research findings and analysis. Pass the combined research and analysis as input.",
            ),
        ],
        model=get_groq_model(),
    )
    return orchestrator

What does this code do?

The orchestrator creates all three specialist agents, then exposes each one as a tool using .as_tool(). Each tool gets a tool_name and tool_description so the orchestrator's LLM knows what each one does and when to call it.

The instructions tell the orchestrator the exact sequence: research first, then analyze, then write. The LLM reads these instructions and decides which tool to call at each turn.

Why agents-as-tools instead of hardcoded function calls?

You could call each specialist in a fixed sequence with plain Python. The agents-as-tools pattern lets the orchestrator's LLM decide the order dynamically based on its instructions.

This means the orchestrator can adapt. If a research query returns thin results, it could call the researcher again with a different angle before moving to analysis. This flexibility is what makes agent orchestration more powerful than a simple script.

  • You won't see a change yet. The next chunk adds the pipeline runner that actually executes this orchestrator.
Add the pipeline runner with retry logic

Groq's free tier has a 30 RPM rate limit. A multi-agent pipeline can make 8-15 LLM calls in a single run, so you need retry logic that waits and tries again if you hit the limit.

  • In agents/orchestrator.py, add the run_pipeline() function below the create_orchestrator() function:

async def run_pipeline(topic: str, max_retries: int = 3) -> str:
    """Run the full research pipeline for a given topic with retry logic."""
    orchestrator = create_orchestrator()

    for attempt in range(max_retries):
        try:
            result = await Runner.run(
                orchestrator,
                input=f"Research the following topic and produce a comprehensive report: {topic}",
                max_turns=10,
            )
            return result.final_output
        except Exception as e:
            if "429" in str(e) or "rate_limit" in str(e).lower():
                wait_time = 2 ** (attempt + 1)
                print(f"[Rate Limit] Attempt {attempt + 1}/{max_retries}. Waiting {wait_time}s...")
                await asyncio.sleep(wait_time)
            else:
                raise

    raise RuntimeError("Max retries exceeded due to rate limiting.")

What does this code do?

  • Runner.run() executes the orchestrator agent. The max_turns=10 parameter prevents infinite loops by capping how many tool calls the orchestrator can make.
  • The retry loop catches HTTP 429 (rate limit) errors. It uses exponential backoff: waiting 2 seconds on the first retry, 4 seconds on the second, and 8 on the third.
  • result.final_output contains the orchestrator's last message, which is the finished report from the Writer.
  • Save agents/orchestrator.py.
  • You won't see a change yet. The next file (main.py) is the entry point that calls this pipeline.

✔️ Awesome, I've got everything!

Great. Double check you've saved agents/orchestrator.py before moving on.

ⓧ I'd like to double check the full code

import asyncio
import re
from agents import Agent, Runner
from agents.config import get_groq_model
from agents.researcher import create_researcher
from agents.analyst import create_analyst
from agents.writer import create_writer


def create_orchestrator():
    """Create the Orchestrator agent with specialists as tools."""
    researcher = create_researcher()
    analyst = create_analyst()
    writer = create_writer()

    orchestrator = Agent(
        name="Research Pipeline Orchestrator",
        instructions=(
            "You manage a research pipeline. For any given topic:\n"
            "1. Call research_topic to gather comprehensive information\n"
            "2. Call analyze_findings with the research results to extract insights\n"
            "3. Call write_report with both the research and analysis to produce the final report\n\n"
            "Always follow this sequence. Pass the full output from each step to the next. "
            "Return the final report from the writer as your output."
        ),
        tools=[
            researcher.as_tool(
                tool_name="research_topic",
                tool_description="Research a topic by searching for information from multiple sources. Pass the topic as input.",
            ),
            analyst.as_tool(
                tool_name="analyze_findings",
                tool_description="Analyze raw research findings to extract key insights, themes, and statistics. Pass the research text as input.",
            ),
            writer.as_tool(
                tool_name="write_report",
                tool_description="Write a polished markdown research report from research findings and analysis. Pass the combined research and analysis as input.",
            ),
        ],
        model=get_groq_model(),
    )
    return orchestrator


async def run_pipeline(topic: str, max_retries: int = 3) -> str:
    """Run the full research pipeline for a given topic with retry logic."""
    orchestrator = create_orchestrator()

    for attempt in range(max_retries):
        try:
            result = await Runner.run(
                orchestrator,
                input=f"Research the following topic and produce a comprehensive report: {topic}",
                max_turns=10,
            )
            return result.final_output
        except Exception as e:
            if "429" in str(e) or "rate_limit" in str(e).lower():
                wait_time = 2 ** (attempt + 1)
                print(f"[Rate Limit] Attempt {attempt + 1}/{max_retries}. Waiting {wait_time}s...")
                await asyncio.sleep(wait_time)
            else:
                raise

    raise RuntimeError("Max retries exceeded due to rate limiting.")
Create the CLI entry point

The orchestrator and pipeline runner are ready. Now you need an entry point that accepts a topic from the user, kicks off the pipeline, and prints the finished report.

  • Create a new file called main.py in your project root (the same level as your agents/ folder).
  • Paste the following code into main.py:
import asyncio
import time
import sys
from dotenv import load_dotenv
from agents.orchestrator import run_pipeline

# Load environment variables from .env
load_dotenv()


async def main():
    # Accept topic from command line args or prompt the user
    if len(sys.argv) > 1:
        topic = " ".join(sys.argv[1:])
    else:
        topic = input("Enter a research topic: ")

    print(f"\nStarting research pipeline for: '{topic}'")
    print("=" * 50)
    print("Orchestrating agents: Researcher -> Analyst -> Writer")
    print("=" * 50)

    start_time = time.time()

    try:
        report = await run_pipeline(topic)
        elapsed = time.time() - start_time

        print(f"\n{'=' * 50}")
        print("RESEARCH REPORT")
        print(f"{'=' * 50}\n")
        print(report)
        print(f"\n{'=' * 50}")
        print(f"Pipeline completed in {elapsed:.1f} seconds")
        print(f"{'=' * 50}")

    except Exception as e:
        print(f"\nError running pipeline: {e}")
        print("If this is a rate limit error, wait a moment and try again.")
        sys.exit(1)


if __name__ == "__main__":
    asyncio.run(main())

What does this code do?

  • The script accepts a topic either from command-line arguments (python main.py AI agents) or from an interactive prompt if no arguments are given.
  • asyncio.run(main()) starts the async event loop. The OpenAI Agents SDK is built on asyncio, so the entry point must be async.
  • The timer tracks how long the full pipeline takes, giving you a sense of the orchestration overhead.
  • Save main.py.
  • Run the full pipeline by entering this command in your terminal:
python main.py multi-agent orchestration

You should see status messages followed by a full markdown research report printed to your terminal. The report includes an Executive Summary, Key Findings, Analysis, Challenges, and a Conclusion.

Seeing a rate limit error?

  • The retry logic should handle this automatically. If you still see the error after 3 retries, wait 60 seconds and run the command again. Groq's free tier allows 30 requests per minute.
  • If you tested your specialist agents rapidly in the previous step, you may have already used up some of your per-minute quota. Give it a minute to reset.

Help me debug a rate limit error in my multi-agent pipeline

Getting an import error or module not found?

  • Make sure your virtual environment is activated. You should see the environment name in your terminal prompt.
  • Check that agents/__init__.py exists (even if it's empty). Python needs this file to treat the folder as a package.
  • Confirm you're running the command from the project root (the folder that contains both main.py and the agents/ folder).

Help me fix a module import error in my Python agent project

✔️ Awesome, I've got everything!

Great. Make sure you've saved both agents/orchestrator.py and main.py.

ⓧ I'd like to double check the full code

Here's the complete main.py:

import asyncio
import time
import sys
from dotenv import load_dotenv
from agents.orchestrator import run_pipeline

load_dotenv()


async def main():
    if len(sys.argv) > 1:
        topic = " ".join(sys.argv[1:])
    else:
        topic = input("Enter a research topic: ")

    print(f"\nStarting research pipeline for: '{topic}'")
    print("=" * 50)
    print("Orchestrating agents: Researcher -> Analyst -> Writer")
    print("=" * 50)

    start_time = time.time()

    try:
        report = await run_pipeline(topic)
        elapsed = time.time() - start_time

        print(f"\n{'=' * 50}")
        print("RESEARCH REPORT")
        print(f"{'=' * 50}\n")
        print(report)
        print(f"\n{'=' * 50}")
        print(f"Pipeline completed in {elapsed:.1f} seconds")
        print(f"{'=' * 50}")

    except Exception as e:
        print(f"\nError running pipeline: {e}")
        print("If this is a rate limit error, wait a moment and try again.")
        sys.exit(1)


if __name__ == "__main__":
    asyncio.run(main())

Your multi-agent pipeline is working end-to-end. Next up, you'll add production-aware features like structured logging and timeout handling so you can see exactly what's happening inside the orchestration flow.

Add Rate Limiting and Error Handling

Your multi-agent pipeline works, but it is running on a free tier with hard limits. Groq's free tier allows 30 requests per minute, and your pipeline makes multiple LLM calls in a single run.

Without safeguards, rapid development or repeated runs will hit rate limits and crash. In this step, you will review and test the production-awareness features already built into your orchestrator: retry logic, loop guards, and structured logging.

In this step, get ready to:
  • Understand exponential backoff retry logic for Groq's 30 RPM rate limit.
  • Review the max_turns guard that prevents runaway agent loops.
  • Run the full pipeline and observe structured logging output.
Review the retry logic with exponential backoff

Groq's free tier caps you at 30 requests per minute. A multi-agent pipeline with three specialists can easily make 8-15 LLM calls in a single run, so rapid iteration during development can trigger a 429 rate limit error.

  • In VS Code, open agents/orchestrator.py and find the run_pipeline function.
  • Locate the for attempt in range(max_retries): loop and the except block inside it.
async def run_pipeline(topic: str, max_retries: int = 3) -> str:
    """Run the full research pipeline for a given topic with retry logic."""
    orchestrator = create_orchestrator()

    for attempt in range(max_retries):
        try:
            result = await Runner.run(
                orchestrator,
                input=f"Research the following topic and produce a comprehensive report: {topic}",
                max_turns=10,
            )
            return result.final_output
        except Exception as e:
            if "429" in str(e) or "rate_limit" in str(e).lower():
                wait_time = 2 ** (attempt + 1)
                print(f"[Rate Limit] Attempt {attempt + 1}/{max_retries}. Waiting {wait_time}s...")
                await asyncio.sleep(wait_time)
            else:
                raise

    raise RuntimeError("Max retries exceeded due to rate limiting.")

What does this retry logic do?

This function wraps the entire pipeline run in an exponential backoff loop.

  • max_retries: int = 3 gives the pipeline three attempts before giving up.
  • The except block catches any exception, then checks if it contains 429 or rate_limit in the error message.
  • wait_time = 2 ** (attempt + 1) doubles the delay each time: 2 seconds, then 4, then 8. This is exponential backoff.
  • If the error is NOT a rate limit, it re-raises immediately so you see the real problem.

Why exponential backoff instead of a fixed delay?

A fixed retry (always wait 2 seconds) can hammer the API repeatedly at the same pace. Exponential backoff spaces retries further apart each time, giving the rate limit window time to reset.

This is the same pattern used by AWS SDKs, Google Cloud libraries, and most production API clients.

Review the max_turns guard against runaway loops

LLM agents can sometimes get stuck in a loop, calling tools repeatedly without making progress. Without a hard limit, a confused agent could burn through your entire rate limit in one run.

  • In the same run_pipeline function, find the Runner.run() call.
  • Notice the max_turns=10 parameter:
result = await Runner.run(
    orchestrator,
    input=f"Research the following topic and produce a comprehensive report: {topic}",
    max_turns=10,
)

What does max_turns do?

Each "turn" is one cycle of the agent calling a tool and processing the result. Your pipeline has three specialist calls (Researcher, Analyst, Writer) plus the orchestrator's own reasoning steps.

Setting max_turns=10 gives the orchestrator enough room to complete the full sequence while preventing infinite loops. If the agent exceeds 10 turns, the SDK stops execution and returns what it has.

How does max_turns relate to rate limiting?

Each turn makes at least one LLM call. Without max_turns, a stuck agent could burn 30+ calls in seconds, consuming your entire free-tier allowance of 30 RPM. The guard keeps each run predictable and budget-friendly.

Run the pipeline and observe the logging

Your main.py already prints structured output showing the pipeline sequence and total execution time. Time to run it and watch the orchestration flow in action.

  • Run the pipeline with a test topic by running this command:
python main.py "multi-agent AI systems"

What should I see?

Your terminal output shows the full orchestration flow:

  • Starting research pipeline for: 'multi-agent AI systems' confirms your topic was received.
  • Orchestrating agents: Researcher -> Analyst -> Writer shows the planned sequence.
  • The RESEARCH REPORT section displays the Writer's final markdown output.
  • Pipeline completed in X.X seconds tells you total execution time across all agents.
  • Check the terminal output. You should see a complete research report with sections for Executive Summary, Key Findings, Analysis, Challenges, and Conclusion.

Seeing a rate limit error?

If you ran the pipeline multiple times quickly during Step 3, you may have hit Groq's 30 RPM limit. The retry logic will print [Rate Limit] Attempt 1/3. Waiting 2s... and retry automatically.

If all three retries fail, wait 60 seconds for the rate limit window to reset, then run the command again.

help me debug a rate limit error in my multi-agent pipeline

  • Try running the pipeline again immediately to test rate limit resilience:
python main.py "cloud computing trends"

If you run twice in quick succession, you may see the rate limit retry messages before the pipeline completes. That is the exponential backoff working as designed.

Check your pipeline timing

A typical run completes in 10-30 seconds depending on Groq's response time. If your pipeline consistently takes longer than 60 seconds, the rate limiter is likely kicking in between agent calls. Space out your test runs to stay within the 30 RPM budget.

✔️ Awesome, I've got everything!

Great. Double check you have saved agents/orchestrator.py and main.py.

ⓧ I'd like to double check the full code

import asyncio
import re
from agents import Agent, Runner
from agents.config import get_groq_model
from agents.researcher import create_researcher
from agents.analyst import create_analyst
from agents.writer import create_writer


def create_orchestrator():
    """Create the Orchestrator agent with specialists as tools."""
    researcher = create_researcher()
    analyst = create_analyst()
    writer = create_writer()

    orchestrator = Agent(
        name="Research Pipeline Orchestrator",
        instructions=(
            "You manage a research pipeline. For any given topic:\n"
            "1. Call research_topic to gather comprehensive information\n"
            "2. Call analyze_findings with the research results to extract insights\n"
            "3. Call write_report with both the research and analysis to produce the final report\n\n"
            "Always follow this sequence. Pass the full output from each step to the next. "
            "Return the final report from the writer as your output."
        ),
        tools=[
            researcher.as_tool(
                tool_name="research_topic",
                tool_description="Research a topic by searching for information from multiple sources. Pass the topic as input.",
            ),
            analyst.as_tool(
                tool_name="analyze_findings",
                tool_description="Analyze raw research findings to extract key insights, themes, and statistics. Pass the research text as input.",
            ),
            writer.as_tool(
                tool_name="write_report",
                tool_description="Write a polished markdown research report from research findings and analysis. Pass the combined research and analysis as input.",
            ),
        ],
        model=get_groq_model(),
    )
    return orchestrator


async def run_pipeline(topic: str, max_retries: int = 3) -> str:
    """Run the full research pipeline for a given topic with retry logic."""
    orchestrator = create_orchestrator()

    for attempt in range(max_retries):
        try:
            result = await Runner.run(
                orchestrator,
                input=f"Research the following topic and produce a comprehensive report: {topic}",
                max_turns=10,
            )
            return result.final_output
        except Exception as e:
            if "429" in str(e) or "rate_limit" in str(e).lower():
                wait_time = 2 ** (attempt + 1)
                print(f"[Rate Limit] Attempt {attempt + 1}/{max_retries}. Waiting {wait_time}s...")
                await asyncio.sleep(wait_time)
            else:
                raise

    raise RuntimeError("Max retries exceeded due to rate limiting.")
import asyncio
import time
import sys
from dotenv import load_dotenv
from agents.orchestrator import run_pipeline

load_dotenv()


async def main():
    if len(sys.argv) > 1:
        topic = " ".join(sys.argv[1:])
    else:
        topic = input("Enter a research topic: ")

    print(f"\nStarting research pipeline for: '{topic}'")
    print("=" * 50)
    print("Orchestrating agents: Researcher -> Analyst -> Writer")
    print("=" * 50)

    start_time = time.time()

    try:
        report = await run_pipeline(topic)
        elapsed = time.time() - start_time

        print(f"\n{'=' * 50}")
        print("RESEARCH REPORT")
        print(f"{'=' * 50}\n")
        print(report)
        print(f"\n{'=' * 50}")
        print(f"Pipeline completed in {elapsed:.1f} seconds")
        print(f"{'=' * 50}")

    except Exception as e:
        print(f"\nError running pipeline: {e}")
        print("If this is a rate limit error, wait a moment and try again.")
        sys.exit(1)


if __name__ == "__main__":
    asyncio.run(main())

Your pipeline now has the resilience features that separate a prototype from a production system. Next up, you will tackle a Secret Mission to add an LLM-as-a-Judge quality loop that scores reports and triggers revisions.

Secret mission

Add an LLM-as-a-Judge Quality Loop

Your pipeline produces a report, but how do you know if it's any good? In this secret mission, you'll add a Judge agent that scores every report on completeness, clarity, and accuracy. If the score is too low, the Judge sends feedback to the Writer for a revision, creating an iterative quality loop that mirrors how production AI systems self-improve.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

This project runs entirely locally and uses Groq's free tier, so there are no ongoing costs.

Resources you used:

  • Project folder on your Desktop (includes virtual environment and all Python files).
  • Groq API key (free tier, no charges).

Keep everything running

No action needed. Choose this if you're still actively building or plan to extend the pipeline with a web UI.

  • Your project folder stays on your Desktop, ready to run whenever you want.
  • Your Groq API key remains active. The free tier has no charges, so leaving it active costs nothing.
  • Your virtual environment stays inside the project folder, keeping packages isolated from your system Python.

Pause - I'll come back to this later

Shut down any running processes to free up memory. Your files and API key remain intact for when you return.

  • Close VS Code if you have the project open.
  • Close any terminal sessions running the pipeline.
  • Your project folder and Groq API key will be waiting when you come back.

Delete - I don't want to use this again

Remove all project resources and revoke your API key to start fresh.

  • Delete your project folder by running this command in your terminal:

macOS

rm -rf ~/Desktop/research-pipeline

Windows

rmdir /s /q %USERPROFILE%\Desktop\research-pipeline

This removes the entire project folder, including the virtual environment and all installed packages.

  • Revoke your Groq API key by visiting console.groq.com/keys.
  • Find the key you created for this project and click the delete button next to it.

Nice Work!

Nice Work!

Nice work! You just built a multi-agent research pipeline using the OpenAI Agents SDK and Groq's free tier.

You've learned how to:

  • Build a multi-agent orchestration system using the agents-as-tools pattern where a manager agent coordinates specialist agents (Researcher, Analyst, Writer).
  • Create custom function tools with the @function_tool decorator, demonstrating how agents interact with external data sources through tool calling.
  • Add production-aware features including rate-limit handling with exponential backoff, max_turns loop protection, and structured logging to trace the orchestration flow.
  • Secret Mission: Added an LLM-as-a-Judge evaluator agent with an iterative revision loop for automated quality control.

Ready to quiz yourself?