(At the time of writing this article, the OpenAI Swarm Agentic Framework is in the experimental stage. The API I show in the examples may change in the future, or the project may even be discontinued.)
Table of Contents
- The building blocks of the Swarm Framework
- Building a database retrieval workflow with Swarm Framework
- Defining the agents
- Running the workflow
- Debugging the workflow
- Other ways to run a similar workflow
Regardless of the tool you choose, implementing agentic workflows is not straightforward. Agents tend to make mistakes, task handover is error-prone, debugging is challenging, and anything beyond the simplest use cases requires extensive work.
While Swarm Framework cannot solve all these problems, Swarm simplifies implementation and debugging.
The building blocks of the Swarm Framework
The two main elements of the framework are Agents and functions.
Agents are implemented as system messages for an LLM model, and the instructions we write for the agent will be added at the beginning of the conversation with AI. Additionally, we can define a set of functions the agent can call.
Functions are regular Python functions that the agents call. The functions can perform any task and return a text response, but a function can also return another agent. In such a case, the returned agent replaces the calling agent by swapping the system message.
Building a database retrieval workflow with Swarm Framework
Let’s build a database retrieval workflow where the user can ask any question about the data, and AI will generate a SQL query to retrieve the data. We want the workflow to either answer the question or explain to the user why we cannot help them.
The workflow will consist of three agents:
- The Plan agent decides whether the question can be answered with the database we have.
- The Query agent generates the SQL query, accesses the database, and returns the answer.
- The Apologize agent generates an apology if the question cannot be answered.
Data preparation
First, let’s prepare some data. We will use the Titanic dataset and split its content into three database tables:
import sqlite3
from sklearn.datasets import fetch_openml
import pandas as pd
DB_NAME = "local_database.db"
titanic_dataset = fetch_openml("Titanic", version=1)
df = titanic_dataset.frame
df = df.reset_index(drop=False)
df = df.rename(columns={"index": "passengerId"})
passenger_df = df[["passengerId", "name", "sex", "sibsp", "parch"]]
ticket_df = df[["passengerId", "ticket", "pclass", "fare", "cabin", "embarked"]]
survived_df = df[["passengerId", "survived"]]
conn = sqlite3.connect(DB_NAME)
passenger_df.to_sql("passengers", conn, if_exists="replace", index=False)
ticket_df.to_sql("tickets", conn, if_exists="replace", index=False)
survived_df.to_sql("survived", conn, if_exists="replace", index=False)
conn.commit()
conn.close()
Data retrieval function
We need to query the database, so let’s write a function that executes an SQL query and returns the result as text. In our case, we will get the values formatted as a Markdown table:
def query_db(query):
conn = sqlite3.connect(DB_NAME)
try:
return pd.read_sql_query(query, conn).to_markdown()
finally:
conn.close()
We can add a description to the function as a docstring if we want. The description will be used as a part of the prompt passed to the LLM model. But even if we don’t describe the function, the Swarm Framework will build a prompt using the function’s name, the argument names and types, and their default values. So, it’s often sufficient to write a descriptive function signature, and we don’t need to add a docstring.
Data description
Nothing will work if we don’t describe the tables and columns we want to use. Because we use the Titanic dataset, which is well known, we don’t need a detailed description. When you describe your own data, you should be more specific.
data_description = """
The database contains the Titanic dataset split into three tables:
1. Table: passengers:
This table contains details about passengers, including their ID, name, sex, number of siblings/spouses aboard, and number of parents/children aboard.
Columns: passengerId, name, sex, sibsp, parch.
2. Table: tickets:
This table provides ticket details, including ticket number, passenger ID, class of travel, fare amount, cabin number, and port of embarkation.
Columns: passengerId, ticket, pclass, fare, cabin, embarked.
3. Table: survived:
This table indicates whether the passenger survived (1) or not (0), along with the passenger ID.
Columns: passengerId, survived.
"""
Want to build AI systems that actually work?
Download my expert-crafted GenAI Transformation Guide for Data Teams and discover how to properly measure AI performance, set up guardrails, and continuously improve your AI solutions like the pros.
Defining the agents
Before we start, we have to instantiate the Swarm framework and configure the OpenAI API key.
import os
from swarm import Swarm, Agent
os.environ["OPENAI_API_KEY"] = "sk-proj-..."
client = Swarm()
We can finally define the agents. An agent consists of a name and the system message (passed as the “instructions” argument). Additionally, we can pass a set of functions the agent can call.
Database querying agent
Our database querying agent is responsible for creating the SQL query and generating the response after receiving the data from the database. The instructions refer to the chat history and mention a plan. We do it because the agent deciding if we can answer the question prepares the plan, so the agent defined below will already have access to the plan.
We also need a pass_to_query
function to return the query agent. The function will be one of the tools available for the agent who decides whether we can answer the question.
query = Agent(
name="Query",
instructions=f"""You are a database reading bot that can answer users' questions using information from a database. \n
{data_description} \n\n
In the previous step, a plan has been prepared. Use the plan to create a SQL query and call the database.""",
functions=[query_db]
)
def pass_to_query():
"Hands the task over to the Query agent"
return query
The apology agent
We want the user to receive an apology and explain why we cannot answer the question. I decided to use a separate agent for this task because it allows me to keep the instructions short and focused on a single task.
apologize = Agent(
name="Apologize",
instructions="Apologize and explain to the user why you cannot complete the task and retrieve the data."
)
def pass_to_apologize():
"Hands the task over to the Apologize agent"
return apologize
The plan agent
In the Plan agent, we must decide whether we can answer the question and delegate the task to the right agent. I want the agent to have the space to think and reason about the question, so I expect to get a response in a specific format. The format forces the agent to reason through the question and then decide what to do.
I explain that reasoning format in the agent prompt by providing a few examples.
The plan agent has access to two functions because the agent may delegate the task to either the query agent or the apology agent.
plan = Agent(
instructions=f"""You are a database reading bot that can answer users' questions using information from a database. \n
{data_description} \n\n
Given the user's question, decide whether the question can be answered using the information in the database. \n\n
Return a JSON with two keys, 'reasoning' and 'can_answer', and no preamble or explanation.
Return one of the following JSON:
{{"reasoning": "I can find the average revenue of customers with tenure over 24 months by averaging the Total Revenue column in the Billing table filtered by Tenure in Months > 24", "can_answer":true}}
{{"reasoning": "I can find customers who signed up during the last 12 month using the Tenure in Months column in the Billing table", "can_answer":true}}
{{"reasoning": "I can't answer how many customers churned last year because the Churn table doesn't contain a year", "can_answer":false}}
If the question can be answered, hand it over to the Query agent.
If it's not possible, pass the task to the Apologize agent.
You must always pass the task to another agent.""",
functions=[pass_to_query, pass_to_apologize]
)
Running the workflow
Finally, we can run the swarm of agents. The run
function requires the message from the user and the first agent to be called. In the response, we get the entire chat history. Therefore, we read the last message to get the final answer.
def ask_ai(question):
answers = client.run(
agent=plan,
messages=[{"role":"user", "content": question}],
debug=True
)
return answers.messages[-1]["content"]
ask_ai("How many passengers survived?")
# A total of 500 passengers survived.
Is the answer correct? Well, for this version of the Titanic dataset, it is. I don’t know, though, if the dataset contains valid values.
Debugging the workflow
If the debug
argument is set to True
, we will see log messages in the console. The messages tell us what the agent’s input was, what response it produced, and what function it was called. If the tool accepts parameters (as query_db
does), we will see the parameters passed to the function (but not the function’s output).
If we don’t want to run the debug mode, the collection of messages returned by the run
function may be sufficient to track the workflow execution. The returned value contains the messages passed to the last agent in the chain.
Other ways to run a similar workflow
In the past, I wrote two articles about the data retrieval workflow with AI decisions. The first one uses Langchain and it’s LangGraph module. The other one was implemented with the Marvin library.
Marvin allows us to write the simplest code but lacks debugging capabilities. LangGraph is verbose and complicated, but let’s use the Langsmith monitoring tool so we know exactly what happens in the workflow. Swarm Framework is somewhere in the middle. It’s simple to use, yet we still have a way to see the internal API calls.
Also, Marvin and LangGraph aren’t agentic frameworks. When you use them, you don’t give AI full control over the workflow.
Do you need help building AI-powered applications for your business?
You can hire me!