mirror of
https://github.com/qdrant/landing_page.git
synced 2026-09-25 14:08:30 +02:00
Blog: Building Intelligent Agentic RAG with CrewAI and Qdrant (#1423)
* Blog: Building Intelligent Agentic RAG with CrewAI and Qdrant * Adjust the publication date * Incorporate Maddie changes
This commit is contained in:
@@ -0,0 +1,393 @@
|
||||
---
|
||||
draft: false
|
||||
title: "How to Build Intelligent Agentic RAG with CrewAI and Qdrant"
|
||||
slug: webinar-crewai-qdrant-obsidian
|
||||
short_description: "Email automation with CrewAI, Qdrant, and Obsidian notes"
|
||||
description: "Learn how to build an agentic RAG system to semi-automate email communication with CrewAI, Qdrant, and Obsidian."
|
||||
preview_image: /blog/webinar-crewai-qdrant-obsidian/preview.jpg
|
||||
date: 2025-01-24T09:00:00.000Z
|
||||
author: Kacper Łukawski
|
||||
featured: false
|
||||
---
|
||||
|
||||
In a recent live session, we teamed up with [CrewAI](https://crewai.com/), a framework for building intelligent,
|
||||
multi-agent applications. If you missed it, [Kacper Łukawski](https://www.linkedin.com/in/kacperlukawski/) from Qdrant
|
||||
and [Tony Kipkemboi](https://www.linkedin.com/in/tonykipkemboi) from [CrewAI](https://crewai.com/) gave an insightful
|
||||
overview of CrewAI’s capabilities and demonstrated how to leverage Qdrant for creating an agentic RAG
|
||||
(Retrieval-Augmented Generation) system. The focus was on semi-automating email communication, using
|
||||
[Obsidian](https://obsidian.md/) as the knowledge base.
|
||||
|
||||
In this article, we’ll guide you through the process of setting up an AI-powered system that connects directly to your
|
||||
email inbox and knowledge base, enabling it to analyze incoming messages and existing content to generate contextually
|
||||
relevant response suggestions.
|
||||
|
||||
## Background agents
|
||||
|
||||
Although we got used to LLM-based apps that usually have a chat-like interface, even if it's not a real UI but a CLI
|
||||
tool, plenty of day-to-day tasks can be automated in the background without explicit human action firing the process.
|
||||
This concept is also known as **ambient agents**, where the agent is always there, waiting for a trigger to act.
|
||||
|
||||
### The basic concepts of CrewAI
|
||||
|
||||
Thanks for Tony's participation, we could learn more about CrewAI, and understand the basic concepts of the framework.
|
||||
He introduced the concepts of agents and crews, and how they can be used to build intelligent multi-agent applications.
|
||||
Moreover, Tony described different types of memory that CrewAI applications can use.
|
||||
|
||||
When it comes to Qdrant role in CrewAI applications, it can be used as short-term, or entity memory, as both components
|
||||
are based on RAG and vector embeddings. If you'd like to know more about memory in CrewAI, please visit the [CrewAI
|
||||
concepts](https://docs.crewai.com/concepts/memory).
|
||||
|
||||
Tony made an interesting analogy. He compared crews to different departments in a company, where each department has its
|
||||
own responsibilities, but they all work together to achieve the company's goals.
|
||||
|
||||
### Email automation with CrewAI, Qdrant, and Obsidian notes
|
||||
|
||||
Our webinar focused on building an agentic RAG system that would semi-automate email communication. RAG is an essential
|
||||
component of such a system, as you don't want to take responsibility for responses that cannot be grounded. The system
|
||||
would monitor your Gmail inbox, analyze the incoming emails, and prepare response drafts if it detects that the email is
|
||||
not spam, newsletter, or notification.
|
||||
|
||||
On the other hand, the system would also monitor the Obsidian notes, by watching any changes in the local file system.
|
||||
When a file is created, modified, or deleted, the system would automatically move these changes to the Qdrant
|
||||
collection, so the knowledge base is always up-to-date. Obsidian uses Markdown files to store notes, so complex parsing
|
||||
is not required.
|
||||
|
||||
Here is a simplified diagram presenting the target architecture of the system:
|
||||
|
||||

|
||||
|
||||
Qdrant acts as a knowledge base, storing the embeddings of the Obsidian notes.
|
||||
|
||||
## Implementing the system
|
||||
|
||||
Since our system integrates with two external APIs - Gmail and filesystem. **We won't go into details of how to work
|
||||
with these APIs**, as it's out of the scope of this webinar. Instead, we will focus on the CrewAI and Qdrant
|
||||
integration, and CrewAI agents' implementation.
|
||||
|
||||
### CrewAI <> Qdrant integration
|
||||
|
||||
Since there is no official integration between CrewAI and Qdrant yet, we created a custom implementation of the
|
||||
`RAGStorage` class, which has a pretty straightforward interface.
|
||||
|
||||
```python
|
||||
from typing import Optional
|
||||
from crewai.memory.storage.rag_storage import RAGStorage
|
||||
|
||||
class QdrantStorage(RAGStorage):
|
||||
"""
|
||||
Extends Storage to handle embeddings for memory entries
|
||||
using Qdrant.
|
||||
"""
|
||||
|
||||
...
|
||||
|
||||
def search(self,
|
||||
query: str,
|
||||
limit: int = 3,
|
||||
filter: Optional[dict] = None,
|
||||
score_threshold: float = 0,
|
||||
) -> list[dict]:
|
||||
...
|
||||
|
||||
def reset(self) -> None:
|
||||
...
|
||||
```
|
||||
|
||||
Full implementation might be found in the [GitHub
|
||||
repository](https://github.com/qdrant/webinar-crewai-qdrant-obsidian/blob/main/src/email_assistant/storage.py). You can
|
||||
use it for your own projects, or as a reference for your custom implementation. If you want to set up a crew that uses
|
||||
Qdrant as both entity and short memory layers, you can do it like this:
|
||||
|
||||
```python
|
||||
from crewai import Crew, Process
|
||||
from crewai.memory import EntityMemory, ShortTermMemory
|
||||
from email_assistant.storage import QdrantStorage
|
||||
|
||||
qdrant_location= "http://localhost:6333"
|
||||
qdrant_api_key = "your-secret-api-key"
|
||||
embedder_config = {...}
|
||||
|
||||
crew = Crew(
|
||||
agents=[...],
|
||||
tasks=[...], # Automatically created by the @task decorator
|
||||
process=Process.sequential,
|
||||
memory=True,
|
||||
entity_memory=EntityMemory(
|
||||
storage=QdrantStorage(
|
||||
type="entity-memory",
|
||||
embedder_config=embedder_config,
|
||||
qdrant_location=qdrant_location,
|
||||
qdrant_api_key=qdrant_api_key,
|
||||
),
|
||||
),
|
||||
short_term_memory=ShortTermMemory(
|
||||
storage=QdrantStorage(
|
||||
type="short-term-memory",
|
||||
embedder_config=embedder_config,
|
||||
qdrant_location=qdrant_location,
|
||||
qdrant_api_key=qdrant_api_key,
|
||||
),
|
||||
),
|
||||
embedder=embedder_config,
|
||||
verbose=True,
|
||||
)
|
||||
```
|
||||
|
||||
Both types of memory will use different collection names in Qdrant, so you can easily distinguish between them, and the
|
||||
data won't be mixed up.
|
||||
|
||||
**We are planning to release a CrewAI tool for Qdrant integration in the near future**, so stay tuned!
|
||||
|
||||
### Loading the Obsidian notes to Qdrant
|
||||
|
||||
For the sake of the demo, we decided to simply scrape the documentation of both CrewAI and Qdrant, and store it in the
|
||||
Obsidian notes. That's easy with Obsidian Web Clipper, as it allows you to save the web page as a Markdown file.
|
||||
|
||||

|
||||
|
||||
Assuming we detected a change in the Obsidian notes, such as new note creation or modification, we would like to load
|
||||
the changes to Qdrant. We could possibly use some chunking methods, starting from basic fixed-size chunks, or go
|
||||
straight to semantic chunking. However, LLMs are also well-known for their ability to divide the text into meaningful
|
||||
parts, so we decided to try them out. Moreover, standard chunking is enough in many cases, but we also wanted to test
|
||||
the [Contextual Retrieval concept introduced by Anthropic](https://www.anthropic.com/news/contextual-retrieval). In a
|
||||
nutshell, the idea is to use LLMs to generate a short context for each chunk, so it situates the chunk in the context of
|
||||
the whole document.
|
||||
|
||||
It turns out, implementing such a crew in CrewAI is quite straightforward. There are two actors in the crew - one
|
||||
chunking the text and the other one generating the context. Both might be defined in YAML files like this:
|
||||
|
||||
```yaml
|
||||
chunks_extractor:
|
||||
role: >
|
||||
Semantic chunks extractor
|
||||
goal: >
|
||||
Parse Markdown to extract digestible pieces of information which are
|
||||
semantically meaningful and can be easily understood by a human.
|
||||
backstory: >
|
||||
You are a search expert building a search engine for Markdown files.
|
||||
Once you receive a Markdown file, you divide it into meaningful semantic
|
||||
chunks, so each chunk is about a certain topic or concept. You're known
|
||||
for your ability to extract relevant information from large documents and
|
||||
present it in a structured and easy-to-understand format, that increases
|
||||
the searchability of the content and results quality.
|
||||
|
||||
contextualizer:
|
||||
role: >
|
||||
Bringing context to the extracted chunks
|
||||
goal: >
|
||||
Add context to the extracted chunks to make them more meaningful and
|
||||
understandable. This context should help the reader understand the
|
||||
significance of the information and how it relates to the broader topic.
|
||||
backstory: >
|
||||
You are a knowledge curator who specializes in making information more
|
||||
accessible and understandable. You take the extracted chunks and provide
|
||||
additional context to make them more meaningful by bringing in relevant
|
||||
information about the whole document or the topic at hand.
|
||||
```
|
||||
|
||||
CrewAI makes it very easy to define such agents, and even a non-tech person can understand and modify the YAML files.
|
||||
|
||||
Another YAML file defines the tasks that the agents should perform:
|
||||
|
||||
```yaml
|
||||
extract_chunks:
|
||||
description: >
|
||||
Review the document you got and extract the chunks from it. Each
|
||||
chunk should be a separate piece of information that can be easily understood
|
||||
by a human and is semantically meaningful. If there are two or more chunks that
|
||||
are closely related, but not put next to each other, you can merge them into
|
||||
a single chunk. It is important to cover all the important information in the
|
||||
document and make sure that the chunks are logically structured and coherent.
|
||||
|
||||
<document>{document}</document>
|
||||
expected_output: >
|
||||
A list of semantic chunks with succinct context of information extracted from
|
||||
the document.
|
||||
agent: chunks_extractor
|
||||
|
||||
contextualize_chunks:
|
||||
description: >
|
||||
You have the chunks we want to situate within the whole document.
|
||||
Please give a short succinct context to situate this chunk within the overall
|
||||
document for the purposes of improving search retrieval of the chunk. Answer
|
||||
only with the succinct context and nothing else.
|
||||
expected_output: >
|
||||
A short succinct context to situate the chunk within the overall document, along
|
||||
with the chunk itself.
|
||||
agent: contextualizer
|
||||
```
|
||||
|
||||
YAML is not enough to make the agents work, so we need to implement them in Python. The role, goal, and backstory
|
||||
of the agent, as well as the task description and expected output, are used to build a prompt sent to the LLM. However,
|
||||
the code defines which LLM to use, and some other parameters of the interaction, like structured output. We heavily rely
|
||||
on Pydantic models to define the output of the task, so the responses might be easily processed by the application,
|
||||
for example, to store them in Qdrant.
|
||||
|
||||
```python
|
||||
from crewai import Agent, Crew, Process, Task
|
||||
from crewai.project import CrewBase, agent, crew, task
|
||||
from email_assistant import models
|
||||
|
||||
...
|
||||
|
||||
@CrewBase
|
||||
class KnowledgeOrganizingCrew(BaseCrew):
|
||||
"""
|
||||
A crew responsible for processing raw text data and converting it into structured knowledge.
|
||||
"""
|
||||
|
||||
agents_config = "config/knowledge/agents.yaml"
|
||||
tasks_config = "config/knowledge/tasks.yaml"
|
||||
|
||||
@agent
|
||||
def chunks_extractor(self) -> Agent:
|
||||
return Agent(
|
||||
config=self.agents_config["chunks_extractor"],
|
||||
verbose=True,
|
||||
llm="anthropic/claude-3-5-sonnet-20241022",
|
||||
)
|
||||
|
||||
...
|
||||
|
||||
@task
|
||||
def contextualize_chunks(self) -> Task:
|
||||
# The task description is borrowed from the Anthropic Contextual Retrieval
|
||||
# See: https://www.anthropic.com/news/contextual-retrieval/
|
||||
return Task(
|
||||
config=self.tasks_config["contextualize_chunks"],
|
||||
output_pydantic=models.ContextualizedChunks,
|
||||
)
|
||||
|
||||
...
|
||||
|
||||
@crew
|
||||
def crew(self) -> Crew:
|
||||
"""Creates the KnowledgeOrganizingCrew crew"""
|
||||
return Crew(
|
||||
agents=self.agents, # Automatically created by the @agent decorator
|
||||
tasks=self.tasks, # Automatically created by the @task decorator
|
||||
process=Process.sequential,
|
||||
memory=True,
|
||||
entity_memory=self.entity_memory(),
|
||||
short_term_memory=self.short_term_memory(),
|
||||
embedder=self.embedder_config,
|
||||
verbose=True,
|
||||
)
|
||||
```
|
||||
|
||||
Full implementation might again be found in the [GitHub
|
||||
repository](https://github.com/qdrant/webinar-crewai-qdrant-obsidian/blob/main/src/email_assistant/crew.py).
|
||||
|
||||
### Drafting emails in Gmail Inbox
|
||||
|
||||
At this point we already have our notes stored in Qdrant, and we can write emails in Gmail Inbox using the notes as a
|
||||
ground truth. The system would monitor the Gmail inbox, and if it detects an email that is not spam, newsletter, or
|
||||
notification, it would draft a response based on the knowledge base stored in Qdrant. Again, that means we need to use
|
||||
two agents - one for detecting the kind of the incoming email, and the other one for drafting the response.
|
||||
|
||||
The YAML files for these agents might look like this:
|
||||
|
||||
```yaml
|
||||
categorizer:
|
||||
role: >
|
||||
Email threads categorizer
|
||||
goal: >
|
||||
Automatically categorize email threads based on their content.
|
||||
backstory: >
|
||||
You're a virtual assistant with a knack for organizing information.
|
||||
You're known for your ability to quickly and accurately categorize email
|
||||
threads, so that your clients know which ones are important to answer
|
||||
and which ones are spam, newsletters, or other types of messages that
|
||||
do not require attention.
|
||||
Available categories: QUESTION, NOTIFICATION, NEWSLETTER, SPAM. Do not make
|
||||
up new categories.
|
||||
|
||||
response_writer:
|
||||
role: >
|
||||
Email response writer
|
||||
goal: >
|
||||
Write clear and concise responses to an email thread. Try to help the
|
||||
sender. Use the external knowledge base to provide relevant information.
|
||||
backstory: >
|
||||
You are a professional writer with a talent for crafting concise and
|
||||
informative responses. You're known for your ability to quickly understand
|
||||
the context of an email thread and provide a helpful and relevant response
|
||||
that addresses the sender's needs. You always rely on your knowledge base
|
||||
to provide accurate and up-to-date information.
|
||||
```
|
||||
|
||||
The set of categories is predefined, so the categorizer should not invent new categories. The task definitions are as
|
||||
follows:
|
||||
|
||||
```yaml
|
||||
categorization_task:
|
||||
description: >
|
||||
Review the content of the following email thread and categorize it
|
||||
into the appropriate category. There might be multiple categories that
|
||||
apply to the email thread.
|
||||
|
||||
<messages>{messages}</messages>
|
||||
expected_output: >
|
||||
A list of all the categories that the email threads can be classified into.
|
||||
agent: categorizer
|
||||
|
||||
response_writing_task:
|
||||
description: >
|
||||
Write a response to the following email thread. The response should be
|
||||
clear, concise, and helpful to the sender. Always rely on the Qdrant search
|
||||
tool, so you can get the most relevant information to craft your response.
|
||||
Please try to include the source URLs of the information you provide.
|
||||
|
||||
Only focus on the real question asked by the sender and do not try to
|
||||
address any other issues that are not directly related to the sender's needs.
|
||||
Do not try to provide a response if the context is not clear enough.
|
||||
|
||||
<messages>{messages}</messages>
|
||||
expected_output: >
|
||||
A well-crafted response to the email thread that addresses the sender's needs.
|
||||
Please use simple HTML formatting to make the response more readable.
|
||||
Do not include greetings or signatures in your response, but provide the footnotes
|
||||
with the source URLs of the information you used, if possible.
|
||||
|
||||
If the provided context does not give you enough information to write a response,
|
||||
you must admit that you cannot provide a response and write "I cannot provide a response.".
|
||||
agent: response_writer
|
||||
```
|
||||
|
||||
We specifically asked the agents to include the source URLs of the information they provide, so both the sender and the
|
||||
recipient can verify the information.
|
||||
|
||||
### Working system
|
||||
|
||||
We have both crews defined, and the application is ready to run. The only thing left is to monitor the Gmail inbox and
|
||||
the Obsidian notes for changes. We use the `watchdog` library to monitor the filesystem, and the `google-api-python-client`
|
||||
to monitor the Gmail inbox, but we won't go into details of how to use these libraries, as the integration code would
|
||||
make this blog post too long.
|
||||
|
||||
If you open the [main file of the
|
||||
application](https://github.com/qdrant/webinar-crewai-qdrant-obsidian/blob/main/main.py), you will see that it is quite
|
||||
simple. It runs two separate threads, one for monitoring the Gmail inbox, and the other one for monitoring the Obsidian
|
||||
notes. If there is any event detected, the application will run the appropriate crew to process the data, and the
|
||||
resulting response will be sent back to the email thread, or Qdrant collection, respectively. No UI is required, as your
|
||||
ambient agents are working in the background.
|
||||
|
||||
## Results
|
||||
|
||||
The system is now ready to run, and it can semi-automate email communication, and keep the knowledge base up-to-date.
|
||||
If you set it up properly, you can expect the system to draft responses to emails that are not spam, newsletter, or
|
||||
notification, so your email inbox may look like this, even when you sleep:
|
||||
|
||||

|
||||
|
||||
## Materials
|
||||
|
||||
As usual, we prepared a video recording of the webinar, so you can watch it at your convenience:
|
||||
|
||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/soGB3UowTZ0" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
|
||||
|
||||
The source code of the demo is available on [GitHub](https://github.com/qdrant/webinar-crewai-qdrant-obsidian/), so if
|
||||
you would like to try it out yourself, feel free to clone or fork the repository and follow the instructions in the
|
||||
[README](https://github.com/qdrant/webinar-crewai-qdrant-obsidian/blob/main/README.md) file.
|
||||
|
||||
Are you building agentic RAG applications using CrewAI and Qdrant? Please join [our Discord
|
||||
community](https://github.com/qdrant/webinar-crewai-qdrant-obsidian/blob/main/README.md) and share your experience!
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 603 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 76 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 133 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
Reference in New Issue
Block a user