Build a multi-agent AI system using CrewAI, Gemini, and CircleCI
Software Engineer and Data Scientist
Multi-agent AI systems are trending in the software development industry right now. These systems consist of a group of individual agents that collaborate to achieve a desired goal. They mimic real world teams and departments in how they are organized. In multi-agent AI systems, each agent is assigned a task that is required to achieve a final output. Consider the example of a newsroom editorial team at a media company like the BBC; there are news researchers, content writers, editors, and the publishing cordinator. All these members collaborate to ensure quality news for BBC news viewers and listeners. In the CrewAI framework, multi-agent teams are referred to as crews.
In this tutorial, you will build a multi-agent AI system that gathers and summarizes trending news articles. You will use CrewAI to organize and coordinate the agents, Google Gemini for processing content intelligently, and CircleCI for automating testing.
By the end of the tutorial, you will have a smart and collaborative system that mimicks a team of workers, and can be tested and deployed using CI/CD principles.
Prerequisites
Before you begin, make sure you have the following:
- Python 3.8 and above installed on your system.
- Basic understanding of generative AI and agent-based systems.
- GitHub and CircleCI accounts.
- Access to Gemini API Key for Gemini large language model (LLM).
Setting up the project
To set up your CrewAI project, you need to install uv which CrewAI uses as its dependecy management and package handling tool. To install uv, go to your terminal run this command:
curl -LsSf https://astral.sh/uv/install.sh | sh #on macOS/Linux
# or wget -qO- https://astral.sh/uv/install.sh | sh ---> for macOS/Linux systems that don't support curl
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" #on Windows
Refer to the uv installation guide if you have questions or need help.
With uv installed, you can install CrewAI. Run:
uv tool install crewai
You may get a PATH warning. If you do, update your shell by running:
uv tool update-shell
Note: There are instances of Windows users encountering the chroma-hnswlib==0.7.6build error (fatal error C1083: Cannot open include file: 'float.h') while trying to install CrewAI. install Visual Studio Build Tools with Desktop development with C++. To verify that CrewAI is installed run this command:
uv tool list
Your output should be:
crewai v0.130.0
- crewai
Now set up your project. The CrewAI documentation recommends using the YAML approach, which is provided as a template for you to use. You’ll need to generate a project scaffolding to use with the template. Name your project multi_agent_ai and run:
crewai create crew multi_agent_ai
You will be prompted to choose a LLM provider; choose gemini. You will then be prompted to choose a model. Choose the most recent one, gemini-1.5-flash. Next, insert your GEMINI_API_KEY (you can add it later if you need to). Press Enter. Open your multi_agent_ai folder to review the structure of the project folder:
multi_agent_ai/
├── .gitignore
├── knowledge/
├── pyproject.toml
├── README.md
├── .env
└── src/
└── multi_agent_ai/
├── __init__.py
├── main.py
├── crew.py
├── tools/
│ ├── custom_tool.py
│ └── __init__.py
└── config/
├── agents.yaml
└── tasks.yaml
This project structure promotes maintainability and organization of files and workflows in the CrewAI framework. Go to your project directory by running this command:
cd multi_agent_ai
If you didn’t add your GEMINI_API_KEY earlier, you can add it now. Add it to the .env file in the project directory.
Installing dependencies
In the CrewAI framework, project dependecies are handled by pyproject.toml, a modern alternative to requirements.txt. Most of the dependencies required for creating CrewAI multi-agent systems come already listed in pyproject.toml. You do need to add a new package, pytest to pyproject.toml for writing our tests in the future. To add it, run this command:
uv add pytest
This command also installs the dependecies.
After successfully installing the dependencies, you can start building.
Building the multi-agent system
Before you start building, review the architecture of our multi-agent system below.
A topic has been passed to your news summarizer crew.
- This topic is first assigned to the
researcheragent, which uses theSerperDevToolto search for the daily trending news on Google for that topic. - This agent retrieves highlights of the news and the source links with support of the tool.
- The output is the passed to the
scraperagent, which gets the URL source links for each news article returned by theresearcheragent. - With the help of
WebsiteScrapeTool, it scrapes all the news content from that page. - This output is then passed to the
writeragent which summarizes it for readers. - It then passes the output to the
editoragent to refine the content and output amarkdownfile. The file,final-post.md, is ready for publishing.
All these agents are given intelligence by the gemini-1.5-flash model.
Now that you understand the system architecture, start building by configuring your agents, defining their tasks, and configuring the crew. For agent and task definition, use the YAML approach that’s provided in the project scaffolding.
Configuring the agents
In your project directory, open src folder then open config. Click the agents.yaml file. You notice this file has a template for defining agents. You are going to replace the content there with right one for your use case. Remove the existing content, then copy and paste this code:
researcher:
role: >
{topic} Senior Data Researcher
goal: >
Uncover cutting-edge developments in {topic}
backstory: >
You're a seasoned researcher with a knack for uncovering the latest
developments in {topic}. Known for your ability to find the most relevant
information and present it in a clear and concise manner.
scraper:
role: >
{topic} Web Data Extractor
goal: >
Extract full and accurate content from online articles about {topic}
backstory: >
You are a focused and efficient web scraper with experience navigating
online content and retrieving full article details. Your strength lies
in pulling raw, complete information from source pages without missing key facts.
writer:
role: >
{topic} Technical Content Writer
goal: >
Create digestible and engaging summaries of complex articles about {topic}
backstory: >
You specialize in converting long, complex articles into shorter, more
digestible summaries. You understand how to write in a plain and friendly tone
while retaining all critical insights and structure from the source.
editor:
role: >
{topic} Content Editor & SEO Refiner
goal: >
Refine content for clarity, grammar, and structure, and prepare it for publishing
backstory: >
You're a sharp-eyed editor known for turning drafts into publish-ready pieces.
You focus on correcting grammar, improving readability, and organizing content clearly,
while formatting it cleanly in Markdown for web/blog/newsletter use.
In this YAML code, each agent is defined by name (researcher) and is given a role, goal, and backstory. The role describes what an agent is with a short and professional title. The goal describes what an agent is meant to accomplish. The backstory gives an agent personality that mimicks human professionals while executing its task. The topic is a variable that dynamically changes based on user input.
Defining agent tasks
Now that you are done with defining your agents, you can define the task of each agent. Click the tasks.yaml file in config. It contains the template for defining agent tasks. Remove that content and add this content instead:
research_task:
description: >
Find what is trending and interesting in this domain **{topic}** in the current day: **{current_date}**.
Gather relevant news articles and include their source links.
expected_output: >
A list of bullet points summarizing the most relevant news stories
about **{topic}**, each accompanied by the original article or blog URL.
agent: researcher
scraping_task:
description: >
Take the list of links from the researcher and scrape the full content from each.
Use the scraping tool to visit each article page and extract the article text.
expected_output: >
A collection of fully detailed news articles or blog contents,
each matched to its original link.
agent: scraper
writing_task:
description: >
Read the full articles scraped by the scraper agent. For each, write a short summary (100–200 words)
capturing all the important information in plain language.
expected_output: >
A friendly, concise summary of each article, between 100–200 words.
agent: writer
editing_task:
description: >
Take the summaries written and refine the grammar, clarity, and structure.
Format the final result as a clean Markdown document, suitable for publishing.
expected_output: >
A polished, Markdown-formatted post for each article, ready for blog or newsletter use.
agent: editor
Each task is defined by name (research_task) and has a description, an expected_output, and an agent to execute it. The description explains what this task is, giving instructions to the agent. The expected_output tells the agent what kind of final answer is expected from it. The agent helps tell the system which agent will perform this particular task. The topic variable is the same placeholder described in agent definition. current_date is a placeholder for the actual date when a task is run.
Confinguring the agent crew
Your next task is to orchestrate your crew. Open the crew.py file and delete the content that’s there. Add this content instead:
from crewai import Agent, Crew, Process, Task
from crewai.project import CrewBase, agent, crew, task
from crewai.agents.agent_builder.base_agent import BaseAgent
from typing import List
from crewai_tools import SerperDevTool, ScrapeWebsiteTool
@CrewBase
class MultiAgentAi():
"""MultiAgentAi crew"""
agents: List[BaseAgent]
tasks: List[Task]
@agent
def researcher(self) -> Agent:
return Agent(
config=self.agents_config['researcher'], # type: ignore[index]
verbose=True,
tools=[SerperDevTool(
search_url="https://google.serper.dev/news",
n_results=2,
)]
)
@agent
def scraper(self) -> Agent:
return Agent(
config=self.agents_config['scraper'], # type: ignore[index]
verbose=True,
tools=[ScrapeWebsiteTool()]
)
@agent
def writer(self) -> Agent:
return Agent(
config=self.agents_config['writer'], # type: ignore[index]
verbose=True
)
@agent
def editor(self) -> Agent:
return Agent(
config=self.agents_config['editor'], # type: ignore[index]
verbose=True
)
@task
def research_task(self) -> Task:
return Task(
config=self.tasks_config['research_task'], # type: ignore[index]
)
@task
def scraping_task(self) -> Task:
return Task(
config=self.tasks_config['scraping_task'], # type: ignore[index]
)
@task
def writing_task(self) -> Task:
return Task(
config=self.tasks_config['writing_task'], # type: ignore[index]
)
@task
def editing_task(self) -> Task:
return Task(
config=self.tasks_config['editing_task'], # type: ignore[index]
output_file='final_post.md'
)
@crew
def crew(self) -> Crew:
"""Creates the MultiAgentAi crew"""
return Crew(
agents=self.agents,
tasks=self.tasks,
process=Process.sequential,
verbose=True,
)
This code sets up four agents, assigns each of them a task, and organizes them in a sequential flow. The flow streamlines and automates the process of news creation from research to summarizing and publishing.
These are the components that are imported:
Agent,Task,Crew, andProcessfrom CrewAI core.- Decorators like
@CrewBase,@agent,@task, and@crewto define and register elements cleanly. - External tools like
SerperDevTool(for news search) andScrapeWebsiteTool(for web scraping). BaseAgentwhich is the base class that all your agents inherit from CrewAI.List, a type hint from Python’s built-intypingmodule. It is used to showPythonthat tasks and agents are lists of certain objects.
The agent crew class MultiAgentCrew is defined using @CrewBase.
In the crew class, agents and tasks are also defined using the @agent and @task decorators respectively. For example, here is the research agent:
@agent
def researcher(self) -> Agent:
return Agent(
config=self.agents_config['researcher'],
verbose=True,
tools=[SerperDevTool(search_url="https://google.serper.dev/news", n_results=2)]
)
This agent method pulls from the YAML config for its behavior (self.agents_config['researcher']) and uses the SerperDevTool to search Google News. verbose=True enables detailed logs for debugging. Agents without need for tools are not assigned the tools parameter; it’s optional.
The research_task snippet:
@task
def research_task(self) -> Task:
return Task(config=self.tasks_config['research_task'])
This method loads the task config like description, expected_output, and assigned agent. The output_file='final_post.md' in editing_task saves the final article to a markdown file.
After defining agents and their tasks, crew orchestration uses the @crew decorator:
@crew
def crew(self) -> Crew:
return Crew(
agents=self.agents,
tasks=self.tasks,
process=Process.sequential,
verbose=True,
)
This method combines all the defined agents and tasks into a crew. process=Process.sequential makes agents perform their tasks one after the other, in the order they are listed. verbose=True helps with understanding what each agent is doing at runtime.
Running the crew
Before running the crew, it is helpful to review the main.py file in src:
#!/usr/bin/env python
import sys
import warnings
from datetime import datetime
from multi_agent_ai.crew import MultiAgentAi
warnings.filterwarnings("ignore", category=SyntaxWarning, module="pysbd")
# This main file is intended to be a way for you to run your
# crew locally, so refrain from adding unnecessary logic into this file.
# Replace with inputs you want to test with, it will automatically
# interpolate any tasks and agents information
def run():
"""
Run the crew.
"""
inputs = {
'topic': 'AI LLMs',
#'current_year': str(datetime.now().year),
"current_date": datetime.now().strftime('%Y-%m-%d'),
}
try:
MultiAgentAi().crew().kickoff(inputs=inputs)
except Exception as e:
raise Exception(f"An error occurred while running the crew: {e}")
def train():
"""
Train the crew for a given number of iterations.
"""
inputs = {
"topic": "AI LLMs",
#'current_year': str(datetime.now().year)
"current_date": datetime.now().strftime('%Y-%m-%d'),
}
try:
MultiAgentAi().crew().train(n_iterations=int(sys.argv[1]), filename=sys.argv[2], inputs=inputs)
except Exception as e:
raise Exception(f"An error occurred while training the crew: {e}")
def replay():
"""
Replay the crew execution from a specific task.
"""
try:
MultiAgentAi().crew().replay(task_id=sys.argv[1])
except Exception as e:
raise Exception(f"An error occurred while replaying the crew: {e}")
def test():
"""
Test the crew execution and returns the results.
"""
inputs = {
"topic": "AI LLMs",
#"current_year": str(datetime.now().year)
"current_date": datetime.now().strftime('%Y-%m-%d'),
}
try:
MultiAgentAi().crew().test(n_iterations=int(sys.argv[1]), eval_llm=sys.argv[2], inputs=inputs)
except Exception as e:
raise Exception(f"An error occurred while testing the crew: {e}")
In most cases, the only thing to adjust in this script is the inputs found in these defined methods: run(), train(), and test():
inputs = {
'topic': 'AI LLMs',
"current_date": datetime.now().strftime('%Y-%m-%d'),
}
In the previous YAML configuration you came across placeholders like {topic} and {current_date} used to make agent roles, goals, and tasks. This snippet shows how to pass in those values:
topicsets the specific subject or domain the agents will focus on. In this case, we are using"AI LLMs"but you can choose your own e.g.CybersecurityorClimate technoloy, and so on.current_dateautomatically gets today’s date usingdatetime.now().strftime('%Y-%m-%d'). This returns the date in the format like 2025-06-28.- These values are dynamically passed to the YAML wherever you used the placeholders
{topic}and{current_date}.
To run the multi-agent crew, go to your terminal and run:
crewai run
This command starts by creating a .venv for your project, (if it wasn’t created during installation) then runs the crew. You can observe the crew executing in your terminal from the beginning to the end.
At the end, you will have a file named final_post.md in your project folder.
Now you have successfully implemented a multi-agent AI crew that researches trending news content and provides output of the summarized content.
But what if something isn’t right? For example an agent is missing or there’s a typo in its name. A task could be missing or an agent is missing a necessary tool. I have experienced these challenges personally. The next section shows you how to solve this problem. It is crucial to ensure the quality of research and output of your agentic crew before pushing to production.
Writing tests for the multi-agent system
Now that your agentic crew is running, you need to write tests to ensure that everything works as expected. This is crucial for producing high quality agentic systems because it helps catch bugs before pushing the crew to production. For this tutorial, you will focus on ensuring that:
- Your crew has the right agents
- Agents with tools have been assigned the right tools
- Each agent has been assigned the right task
You will also check that your crew has the right number of agents and tasks, and also that it is being successfully created.
Start by creating a test_multi_agent_ai.py file in the tests folder in your project directory. Add this code to it:
from multi_agent_ai.crew import MultiAgentAi
def test_all_agent_initialization():
crew=MultiAgentAi()
assert crew.researcher() is not None
assert crew.scraper() is not None
def test_all_tasks_initializable():
crew=MultiAgentAi()
assert crew.research_task() is not None
assert crew.scraping_task() is not None
def test_scraper_has_tool():
crew =MultiAgentAi()
scraper = crew.scraper()
assert scraper.tools, "Scraper agent should have atleast one tool"
assert any("ScrapeWebsiteTool" in str(tool.__class__) for tool in scraper.tools)
def test_crew_creation():
crew_instance = MultiAgentAi()
crew=crew_instance.crew()
assert crew is not None
assert len(crew.tasks)==4, "Crew should have 4 tasks"
assert len(crew.agents)==4, "Crew should have 4 agents"
Run the tests locally from your terminal using this command:
pytest tests/
If all is well, your tests will run successfully with no bugs.
Automate testing using CircleCI
To automate testing when pushing to production, you need to push your project to GitHub. Then link it to CircleCI for automating your tests. This ensures that, if there is any failure in any test, the bug isn’t pushed to production. There are also real time notifications.
But before you push the project to GitHub, create a .circleci/config.yml file in your project directory. Add this code to it:
version: 2.1
executors:
python-executor:
docker:
- image: cimg/python:3.11
jobs:
test:
executor: python-executor
steps:
- checkout
- run:
name: Install uv
command: |
curl -LsSf https://astral.sh/uv/install.sh | sh
export PATH="$HOME/.cargo/bin:$PATH"
- run:
name: Install project dependencies
command: |
uv pip install --system .
- run:
name: Run pytest
command: |
uv venv
uv pip install -e .
uv pip install pytest
.venv/bin/pytest tests/ --junitxml=test-results/results.xml
- store_test_results:
path: test-results
workflows:
test_and_deploy:
jobs:
- test
This configuration helps set up your testing environment using the cimg/python:3.11 Docker image. It also helps install uv to use for dependency management. This config helps run the test suite with pytest, and stores the test results.
Initialize a GitHub repository for the project and push all the code to it.
Now set up your project on CircleCI. Log in and go to your CircleCI dashboard. Click Projects on the side panel. Click Set up for the project you want.
CircleCI will request a configuration file. In this case you have already added it to your project’s folder. Select the option Fastest , choose or enter your brach name (for example main). Then click Set Up Project to trigger the pipeline.
After a succeful run, you should have a green success tick as below.
CircleCI automatically tests your crew every time you add new code to your project repository. If there are any failed tests, you will be notified so you can prevent pushing bugs to production.
Conclusion
In this tutorial, you have built a multi-agent AI system using CrewAI and Gemini LLM. Each agent plays a role, from researching to summarizing, then to editing. You have also set up a CircleCI pipeline to automatically test your code, ensuring reliable and iterative development.
This tutorial shows how CI/CD can be implemented in multi-agent CrewAI systems, as well as its significance in building AI projects.