-
Notifications
You must be signed in to change notification settings - Fork 81
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
1 parent
6f14d33
commit 9e68149
Showing
5 changed files
with
393 additions
and
0 deletions.
There are no files selected for viewing
393 changes: 393 additions & 0 deletions
393
qdrant-landing/content/blog/webinar-crewai-qdrant-obsidian.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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: | ||
|
||
![Project architecture](/blog/webinar-crewai-qdrant-obsidian/project-architecture.png) | ||
|
||
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. | ||
|
||
![Obsidian notes](/blog/webinar-crewai-qdrant-obsidian/obsidian.png) | ||
|
||
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: | ||
|
||
![Drafted emails](/blog/webinar-crewai-qdrant-obsidian/gmail-inbox.png) | ||
|
||
## 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 added
BIN
+603 KB
qdrant-landing/static/blog/webinar-crewai-qdrant-obsidian/gmail-inbox.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+75.7 KB
qdrant-landing/static/blog/webinar-crewai-qdrant-obsidian/obsidian.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+36 KB
qdrant-landing/static/blog/webinar-crewai-qdrant-obsidian/project-architecture.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.