Tutorials / AutoGen Integration

AutoGen Content Pipeline: Generate, Humanize, Publish

Build a Microsoft AutoGen 0.4+ multi-agent pipeline where one agent generates content, a second humanizes it via the ToHuman API, and a third reviews and publishes the result.

· 20 min read

Microsoft AutoGen is one of the fastest-growing multi-agent frameworks in enterprise AI. Built by the Microsoft Research team, it powers production workloads at companies across the Azure ecosystem and sits at the foundation of Microsoft's own Copilot tooling. If your team generates written content at scale — blog posts, reports, marketing copy, internal documentation — AutoGen gives you a clean way to build the pipeline that produces it.

The problem is that every agent in the pipeline is powered by an LLM, and every LLM leaves a fingerprint. AI detectors like GPTZero, Originality.ai, and Turnitin flag content based on statistical patterns in sentence structure, word choice, and rhythm. Content that looks polished can still get flagged. The fix isn't to avoid AI — it's to add a humanization step that rewrites the output so those patterns disappear.

This tutorial shows you how to register the ToHuman API as a tool in AutoGen 0.4+, assign it to a dedicated Humanizer agent, and run a three-agent pipeline — Writer, Humanizer, Publisher — using RoundRobinGroupChat. The entire thing runs in about 80 lines of Python.

For background on why AI detectors flag content the way they do, see our post on AI detection false positives.

AutoGen 0.4: What Changed

AutoGen 0.4 is a ground-up redesign. The old 0.2 API had a monolithic ConversableAgent class that handled everything — not great for large teams or async workflows. Version 0.4 splits into two layers:

  • autogen-core — the low-level async, event-driven runtime. You build custom agents with RoutedAgent and wire up message handlers. Powerful but verbose.
  • autogen-agentchat — a high-level API with pre-built AssistantAgent, team orchestrators like RoundRobinGroupChat, and termination conditions. This is what you want for content pipelines.

We'll use autogen-agentchat throughout. It's the pragmatic choice for building something useful quickly, and it's what Microsoft's own documentation recommends for most use cases.

Prerequisites

  • Python 3.10+
  • An OpenAI API key (or an Azure OpenAI deployment) stored as OPENAI_API_KEY.
  • A ToHuman API keysign up free at tohuman.io. Store it as TOHUMAN_API_KEY.

Install the dependencies:

Terminal

pip install "autogen-agentchat>=0.4" "autogen-ext[openai]>=0.4" httpx

The autogen-ext[openai] package provides OpenAIChatCompletionClient. If you're on Azure OpenAI, install autogen-ext[azure] instead — the agent code is identical, only the model client changes.

Step 1: Define the ToHuman Tool

In AutoGen 0.4, a tool is a Python function. Pass it to an AssistantAgent and AutoGen automatically wraps it, reads the type annotations and docstring, and generates the tool schema the LLM uses to decide when to call it. The docstring is the tool's description — write it clearly.

Create tools.py:

tools.py

import os
import httpx

TOHUMAN_API_KEY = os.environ["TOHUMAN_API_KEY"]
TOHUMAN_API_URL = "https://tohuman.io/api/v1/humanize"


async def humanize_text(text: str) -> str:
    """Rewrite AI-generated text so it reads like a human wrote it.

    Sends the text to the ToHuman API, which rewrites sentence
    structure, word choice, and rhythm to remove detectable AI
    writing patterns. The original meaning is preserved.

    Use this tool on any written content — blog posts, emails,
    reports, documentation — before it is published or returned
    as a final result. Works best on complete paragraphs (50+
    words). Pass the full text; do not truncate or summarize.

    Args:
        text: The AI-generated text to humanize.

    Returns:
        The humanized version of the input text.
    """
    async with httpx.AsyncClient() as client:
        response = await client.post(
            TOHUMAN_API_URL,
            headers={
                "Authorization": f"Bearer {TOHUMAN_API_KEY}",
                "Content-Type": "application/json",
            },
            json={"text": text, "strength": "medium"},
            timeout=60,
        )
        response.raise_for_status()
        data = response.json()
        return data["humanized_text"]

A few things worth noting. The function is async — AutoGen's runtime is async throughout, so async tool functions run natively without blocking. The timeout=60 matters: humanizing longer text can take several seconds, and the default httpx timeout will cut the request off prematurely.

The strength parameter in the request body tells ToHuman how aggressively to rewrite the text. "medium" is the right default for most content — it produces natural-sounding output without changing the meaning in ways that require review. For full parameter documentation, see the API reference.

Verify the tool works before wiring it into any agent:

Python REPL — verify the tool

import asyncio
from tools import humanize_text

result = asyncio.run(
    humanize_text(
        "Artificial intelligence has demonstrated remarkable capabilities "
        "across numerous domains, enabling the automation of complex tasks "
        "that previously required significant human expertise and judgment."
    )
)
print(result)

You should get back a naturally rewritten version. A 401 means your TOHUMAN_API_KEY is missing or invalid — check it on your ToHuman dashboard. A 422 means the request payload is malformed.

Step 2: Define the Three Agents

The pipeline has three agents with distinct roles. Writer generates a first draft. Humanizer receives that draft and runs it through the humanize_text tool. Publisher reviews the humanized output and produces a final version ready to send.

Create agents.py:

agents.py

import os
from autogen_agentchat.agents import AssistantAgent
from autogen_ext.models.openai import OpenAIChatCompletionClient
from tools import humanize_text

model_client = OpenAIChatCompletionClient(
    model="gpt-4o-mini",
    api_key=os.environ["OPENAI_API_KEY"],
)

# Agent 1: Generates the raw draft
writer_agent = AssistantAgent(
    name="Writer",
    model_client=model_client,
    system_message=(
        "You are a professional content writer. When given a topic, "
        "write a clear, engaging 250-300 word article in flowing "
        "paragraphs. No bullet points. Use a conversational tone "
        "with concrete examples. End your response with DRAFT_READY."
    ),
)

# Agent 2: Humanizes the draft via the ToHuman API
humanizer_agent = AssistantAgent(
    name="Humanizer",
    model_client=model_client,
    tools=[humanize_text],
    system_message=(
        "You are a content polisher. When you receive a draft, call "
        "the humanize_text tool with the full text — do not summarize "
        "or truncate it. Return only the humanized result from the "
        "tool. End your response with HUMANIZED_READY."
    ),
)

# Agent 3: Reviews and finalizes the humanized content
publisher_agent = AssistantAgent(
    name="Publisher",
    model_client=model_client,
    system_message=(
        "You are a content editor. Review the humanized content you "
        "receive. Check that it reads naturally, is coherent, and "
        "is ready to publish. Make minor edits only if necessary. "
        "Output the final publish-ready version and end with "
        "TERMINATE."
    ),
)

Each agent gets its own system_message that describes its role and, critically, tells it what to do next. The handoff signals (DRAFT_READY, HUMANIZED_READY) are optional — RoundRobinGroupChat handles agent sequencing automatically — but they make the conversation trace easier to read during debugging.

Only the Humanizer agent gets the humanize_text tool. Giving tools to agents that don't need them wastes tokens and causes the LLM to call tools inappropriately. The Writer and Publisher agents have no tools — they just reason and generate text.

Step 3: Orchestrate with RoundRobinGroupChat

RoundRobinGroupChat is the simplest team orchestrator in AutoGen. Each agent speaks once per round in order: Writer first, then Humanizer, then Publisher. The team stops when the termination condition fires — in this case, when any agent's response includes the word TERMINATE.

Create pipeline.py:

pipeline.py

import asyncio
from autogen_agentchat.teams import RoundRobinGroupChat
from autogen_agentchat.conditions import TextMentionTermination
from autogen_agentchat.ui import Console
from agents import writer_agent, humanizer_agent, publisher_agent


async def run_content_pipeline(topic: str) -> str:
    """Run the three-agent content pipeline for a given topic."""

    # Stop when any agent says TERMINATE
    termination = TextMentionTermination("TERMINATE")

    team = RoundRobinGroupChat(
        participants=[writer_agent, humanizer_agent, publisher_agent],
        termination_condition=termination,
        max_turns=6,  # Safety cap: 2 full rounds
    )

    result = await Console(team.run_stream(task=topic))
    return result.messages[-1].content


if __name__ == "__main__":
    topic = (
        "Write an article about why enterprise teams are adopting "
        "AI coding assistants in 2026 and what the risks are."
    )
    final_output = asyncio.run(run_content_pipeline(topic))
    print("\n--- FINAL OUTPUT ---")
    print(final_output)

The Console wrapper streams each agent's messages to your terminal in real time as the team runs. You'll see the Writer's draft, then the Humanizer calling the tool and returning the rewritten text, then the Publisher's final version. The max_turns=6 cap prevents runaway loops if something goes wrong — three agents, two turns each.

Run it:

Terminal

python pipeline.py

In the streamed output you'll see each agent's turn labeled by name. Look for the Humanizer's tool invocation — AutoGen logs the function call and return value so you can confirm the ToHuman API is being called with the correct text. If the Humanizer skips the tool call and just paraphrases the draft instead, make the system message more explicit: "you MUST call the humanize_text tool — do not rewrite the text yourself."

Step 4: Using Azure OpenAI

If your team is on Azure OpenAI, swap the model client. Everything else stays the same:

agents.py — Azure OpenAI variant

import os
from autogen_ext.models.openai import AzureOpenAIChatCompletionClient

model_client = AzureOpenAIChatCompletionClient(
    azure_deployment=os.environ["AZURE_OPENAI_DEPLOYMENT"],
    model="gpt-4o-mini",
    api_version="2024-08-01-preview",
    azure_endpoint=os.environ["AZURE_OPENAI_ENDPOINT"],
    api_key=os.environ["AZURE_OPENAI_API_KEY"],
)

# The rest of the agent definitions are identical

This is one of AutoGen's practical advantages for enterprise teams. The agent logic, tool definitions, and team orchestration are completely decoupled from the model provider. You can switch between OpenAI and Azure OpenAI — or any other AutoGen-compatible model client — without touching your business logic.

Step 5: Production Error Handling

The tool as written will raise an exception on API errors, which causes the AutoGen runtime to surface a tool error to the Humanizer agent. The agent will typically retry, which can lead to a confusing loop. Return structured error messages instead so the agent can report the problem clearly and the pipeline can continue:

tools.py — with production error handling

import os
import logging
import httpx

logger = logging.getLogger(__name__)

TOHUMAN_API_KEY = os.environ["TOHUMAN_API_KEY"]
TOHUMAN_API_URL = "https://tohuman.io/api/v1/humanize"


async def humanize_text(text: str) -> str:
    """Rewrite AI-generated text so it reads like a human wrote it.

    Sends the text to the ToHuman API to remove AI writing patterns.
    Preserves the original meaning. Use on any written content before
    publishing. Pass the full text — do not truncate or summarize.

    Args:
        text: The AI-generated text to humanize.

    Returns:
        The humanized text, or an error message prefixed with [ERROR]
        if the API call fails.
    """
    try:
        async with httpx.AsyncClient() as client:
            response = await client.post(
                TOHUMAN_API_URL,
                headers={
                    "Authorization": f"Bearer {TOHUMAN_API_KEY}",
                    "Content-Type": "application/json",
                },
                json={"text": text, "strength": "medium"},
                timeout=60,
            )
            response.raise_for_status()
            data = response.json()
            return data["humanized_text"]
    except httpx.HTTPStatusError as e:
        logger.error(
            "ToHuman API returned %s: %s",
            e.response.status_code,
            e.response.text[:200],
        )
        return (
            f"[ERROR: ToHuman API returned {e.response.status_code}. "
            f"Check your API key and request. Returning original text.]\n\n"
            f"{text}"
        )
    except httpx.TimeoutException:
        logger.error("ToHuman API timed out")
        return (
            f"[ERROR: ToHuman API timed out after 60s. "
            f"Returning original text.]\n\n{text}"
        )

Returning errors as strings keeps the pipeline alive. The Humanizer agent receives the error message and passes it to the Publisher, which can flag the item for manual review. This is the right behavior for a production batch pipeline — one failed API call shouldn't kill the entire run.

Step 6: Batch Processing Multiple Topics

For content teams generating at scale, run the pipeline over a list of topics. Each topic produces a separate humanized article. Reset the team state between runs by creating a fresh team instance per topic:

batch.py — process multiple topics

import asyncio
from autogen_agentchat.teams import RoundRobinGroupChat
from autogen_agentchat.conditions import TextMentionTermination
from agents import writer_agent, humanizer_agent, publisher_agent

TOPICS = [
    "Why enterprise AI adoption is accelerating in financial services",
    "The case for human review in AI-generated compliance reports",
    "How SaaS teams are cutting documentation time with AI pipelines",
]


async def process_topic(topic: str) -> str:
    termination = TextMentionTermination("TERMINATE")
    team = RoundRobinGroupChat(
        participants=[writer_agent, humanizer_agent, publisher_agent],
        termination_condition=termination,
        max_turns=6,
    )
    result = await team.run(task=topic)
    return result.messages[-1].content


async def main():
    for topic in TOPICS:
        print(f"\n{'='*60}")
        print(f"Topic: {topic}")
        print("="*60)
        output = await process_topic(topic)
        print(output)


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

Note that team.run() is used here instead of Console(team.run_stream()). In batch mode you usually don't want to stream every agent's turn to stdout — you just want the final output. Use run_stream() with Console during development for visibility, switch to run() in production pipelines.

Advanced: SelectorGroupChat for Dynamic Routing

For more complex pipelines where not every piece of content needs the same path — some content might need a researcher, others a fact-checker — use SelectorGroupChat instead of RoundRobinGroupChat. The selector uses an LLM to decide which agent speaks next based on the conversation context:

pipeline_selector.py — dynamic agent routing

import asyncio
import os
from autogen_agentchat.teams import SelectorGroupChat
from autogen_agentchat.conditions import TextMentionTermination
from autogen_ext.models.openai import OpenAIChatCompletionClient
from agents import writer_agent, humanizer_agent, publisher_agent

selector_model = OpenAIChatCompletionClient(
    model="gpt-4o-mini",
    api_key=os.environ["OPENAI_API_KEY"],
)

SELECTOR_PROMPT = """You are managing a content pipeline team.
Available agents:
- Writer: drafts content on a given topic
- Humanizer: humanizes AI-generated text using the ToHuman API
- Publisher: reviews and finalizes content for publishing

Based on the conversation history, select the next agent.
Always follow the order: Writer -> Humanizer -> Publisher.
Return only the agent name.
"""

team = SelectorGroupChat(
    participants=[writer_agent, humanizer_agent, publisher_agent],
    model_client=selector_model,
    termination_condition=TextMentionTermination("TERMINATE"),
    selector_prompt=SELECTOR_PROMPT,
)


async def main():
    result = await team.run(
        task="Write an article about AI adoption risks in healthcare."
    )
    print(result.messages[-1].content)


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

SelectorGroupChat costs more per run (an extra LLM call to select the next agent) but gives you dynamic routing without hard-coding the agent order. For simple linear pipelines like Writer → Humanizer → Publisher, RoundRobinGroupChat is the better default — faster, cheaper, and deterministic.

How This Compares to Other Integrations

AutoGen is the right choice if your team is already on Azure or the Microsoft stack, if you're building async pipelines that need to run concurrently, or if enterprise support and .NET interoperability matter. For other frameworks and platforms, the ToHuman integration looks almost identical:

  • CrewAI — role-based orchestration with a slightly simpler setup for multi-agent crews. See the CrewAI humanization tutorial.
  • LangChain — general-purpose LLM framework with agent support via LangGraph. See the LangChain tutorial.
  • Direct API — if you don't need a multi-agent framework, the API guide covers Python, Node.js, and curl with batch processing patterns.

The ToHuman API call is the same in every case — a single POST request. The difference is only in how you wire the tool into the surrounding framework.

What You've Built

You have a three-agent AutoGen pipeline that generates written content, humanizes it via the ToHuman API, and produces a publish-ready version — all in a single asyncio.run() call. The pipeline runs on OpenAI or Azure OpenAI with a one-line model client swap, handles API errors gracefully, and scales to batch processing with minimal changes.

From here, you can add more agents (a Researcher that pulls sources, a SEO Optimizer that checks keyword density), switch to SelectorGroupChat for dynamic routing, or deploy the pipeline as a FastAPI endpoint that accepts topics and returns humanized articles. The ToHuman API docs cover all parameters and response fields in detail, and signing up gets you a free API key in 30 seconds.

Frequently Asked Questions

What is Microsoft AutoGen 0.4?

AutoGen 0.4 is a complete redesign of Microsoft's multi-agent AI framework. It splits into autogen-core (the async runtime) and autogen-agentchat (high-level agents and teams). The agentchat layer — with pre-built AssistantAgent, RoundRobinGroupChat, and termination conditions — is the right starting point for most content pipelines.

How do I register a custom tool in AutoGen 0.4?

Pass a Python function in the tools list when constructing an AssistantAgent. AutoGen reads the type annotations and docstring to generate the tool schema automatically. Async functions work natively. For more control over the description, wrap the function manually with FunctionTool from autogen_core.tools.

How does AutoGen differ from CrewAI or LangChain?

AutoGen integrates naturally with Azure OpenAI and Microsoft's enterprise ecosystem. Its async-first architecture suits concurrent pipelines. CrewAI's role-based abstractions are easier to read for simple sequential crews. LangChain via LangGraph offers the most control flow flexibility for custom graph workflows. The ToHuman API integration is nearly identical across all three — a single POST request in a tool function.

Can I use AutoGen with Azure OpenAI?

Yes. Replace OpenAIChatCompletionClient with AzureOpenAIChatCompletionClient from autogen_ext.models.openai and pass your Azure endpoint, deployment name, and API key. The agent and team setup is identical — only the model client changes.

Published April 17, 2026 by the ToHuman team.

Back to tutorials