Skip to content

Commit

Permalink
Added QNA chat using Qdrant (#100)
Browse files Browse the repository at this point in the history
Signed-off-by: Anush008 <[email protected]>
Co-authored-by: lvliang-intel <[email protected]>
  • Loading branch information
Anush008 and lvliang-intel authored Apr 30, 2024
1 parent 04c5e64 commit f1b4aef
Show file tree
Hide file tree
Showing 15 changed files with 524 additions and 45 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
**/node_modules
**/.svelte-kit
**/package-lock.json
**/package-lock.json

__pycache__/
12 changes: 9 additions & 3 deletions ChatQnA/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,25 +113,31 @@ curl 127.0.0.1:9090/embed \

Note: If you want to integrate the TEI service into the LangChain application, you'll need to restart the LangChain backend service after launching the TEI service.

## Launch Redis and LangChain Backend Service
## Launch Vector Database and LangChain Backend Service

Update the `HUGGINGFACEHUB_API_TOKEN` environment variable with your huggingface token in the `docker-compose.yml`

By default, Redis is used as the vector store. To use Qdrant, use the `docker-compose-qdrant.yml` file instead.

```bash
cd langchain/docker
docker compose -f docker-compose.yml up -d
# To use Qdrant, run
# docker compose -f docker-compose-qdrant.yml up -d
cd ../../
```

> [!NOTE]
> If you modified any files and want that change introduced in this step, add `--build` to the end of the command to build the container image instead of pulling it from dockerhub.
## Ingest data into Redis
## Ingest Data Into Vector Database

Each time the Redis container is launched, data should be ingested into the container using the commands:
Each time the vector database container is launched, data should be ingested into the container using the commands:

```bash
docker exec -it qna-rag-redis-server bash
# To use Qdrant, run
# docker exec -it qna-rag-qdrant-server bash
cd /ws
python ingest.py
```
Expand Down
45 changes: 45 additions & 0 deletions ChatQnA/langchain/docker/docker-compose-qdrant.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Copyright (c) 2024 Intel Corporation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

services:
qdrant-vector-db:
image: qdrant/qdrant:v1.9.0
container_name: qdrant-vector-db
ports:
- "6333:6333"
- "6334:6334"
qna-rag-qdrant-server:
build:
args:
https_proxy: ${https_proxy}
http_proxy: ${http_proxy}
dockerfile: Dockerfile
context: .
image: intel/gen-ai-examples:qna-rag-qdrant-server
container_name: qna-rag-qdrant-server
environment:
- https_proxy=${https_proxy}
- HUGGINGFACEHUB_API_TOKEN=${HUGGINGFACEHUB_API_TOKEN}
- "EMBED_MODEL=BAAI/bge-base-en-v1.5"
- "VECTOR_DATABASE=QDRANT"
- "TGI_LLM_ENDPOINT=http://localhost:8080"
# "TEI_ENDPOINT="http://xxx.xxx.xxx.xxx:9090" - To use a custom TEI endpoint
ulimits:
memlock:
soft: -1 # Set memlock to unlimited (no soft or hard limit)
hard: -1
volumes:
- ../qdrant:/ws
- ../test:/test
network_mode: "host"
1 change: 1 addition & 0 deletions ChatQnA/langchain/docker/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ services:
- "REDIS_PORT=6379"
- "EMBED_MODEL=BAAI/bge-base-en-v1.5"
- "REDIS_SCHEMA=schema_dim_768.yml"
- "VECTOR_DATABASE=REDIS"
ulimits:
memlock:
soft: -1 # Set memlock to unlimited (no soft or hard limit)
Expand Down
39 changes: 29 additions & 10 deletions ChatQnA/langchain/docker/qna-app/app/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,14 @@
from guardrails import moderation_prompt_for_chat, unsafe_dict
from langchain_community.embeddings import HuggingFaceBgeEmbeddings, HuggingFaceHubEmbeddings
from langchain_community.llms import HuggingFaceEndpoint
from langchain_community.vectorstores import Redis
from langchain_core.messages import HumanMessage
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
from langserve import add_routes
from prompts import contextualize_q_prompt, prompt, qa_prompt
from rag_redis.config import EMBED_MODEL, INDEX_NAME, INDEX_SCHEMA, REDIS_URL
from starlette.middleware.cors import CORSMiddleware
from utils import (
VECTOR_DATABASE,
create_kb_folder,
create_retriever_from_files,
create_retriever_from_links,
Expand All @@ -40,6 +39,11 @@
reload_retriever,
)

if VECTOR_DATABASE == "REDIS":
from rag_redis.config import INDEX_NAME
elif VECTOR_DATABASE == "QDRANT":
from rag_qdrant.config import COLLECTION_NAME as INDEX_NAME

parser = argparse.ArgumentParser(description="Server Configuration")
parser.add_argument("--chathistory", action="store_true", help="Enable debug mode")
args = parser.parse_args()
Expand All @@ -52,7 +56,6 @@


class RAGAPIRouter(APIRouter):

def __init__(self, upload_dir, entrypoint, safety_guard_endpoint, tei_endpoint=None) -> None:
super().__init__()
self.upload_dir = upload_dir
Expand Down Expand Up @@ -93,15 +96,31 @@ def __init__(self, upload_dir, entrypoint, safety_guard_endpoint, tei_endpoint=N
self.embeddings = HuggingFaceHubEmbeddings(model=tei_endpoint)
else:
# create embeddings using local embedding model
EMBED_MODEL = os.getenv("EMBED_MODEL", "sentence-transformers/all-MiniLM-L6-v2")
self.embeddings = HuggingFaceBgeEmbeddings(model_name=EMBED_MODEL)

rds = Redis.from_existing_index(
self.embeddings,
index_name=INDEX_NAME,
redis_url=REDIS_URL,
schema=INDEX_SCHEMA,
)
retriever = rds.as_retriever(search_type="mmr")
if VECTOR_DATABASE == "REDIS":
from langchain_community.vectorstores import Redis
from rag_redis.config import INDEX_SCHEMA, REDIS_URL

vdb = Redis.from_existing_index(
self.embeddings,
index_name=INDEX_NAME,
redis_url=REDIS_URL,
schema=INDEX_SCHEMA,
)
elif VECTOR_DATABASE == "QDRANT":
from langchain_community.vectorstores import Qdrant
from qdrant_client import QdrantClient
from rag_qdrant.config import QDRANT_HOST, QDRANT_PORT

client = QdrantClient(host=QDRANT_HOST, port=QDRANT_PORT)
vdb = Qdrant(
embeddings=self.embeddings,
collection_name=INDEX_NAME,
client=client,
)
retriever = vdb.as_retriever(search_type="mmr")

# Define contextualize chain
self.contextualize_q_chain = contextualize_q_prompt | self.llm | StrOutputParser()
Expand Down
116 changes: 85 additions & 31 deletions ChatQnA/langchain/docker/qna-app/app/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,13 @@
from bs4 import BeautifulSoup
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.document_loaders import UnstructuredFileLoader
from langchain_community.vectorstores import Redis
from langchain_core.documents import Document
from rag_redis.config import INDEX_SCHEMA, REDIS_URL

SUPPORTED_VECTOR_DATABASES = ["REDIS", "QDRANT"]

VECTOR_DATABASE = str(os.getenv("VECTOR_DATABASE", "redis")).upper()

assert VECTOR_DATABASE in SUPPORTED_VECTOR_DATABASES, f"Invalid VECTOR_DATABASE: {VECTOR_DATABASE}"


def get_current_beijing_time():
Expand All @@ -57,7 +61,6 @@ def create_kb_folder(upload_dir):


class Crawler:

def __init__(self, pool=None):
if pool:
assert isinstance(pool, (str, list, tuple)), "url pool should be str, list or tuple"
Expand Down Expand Up @@ -292,16 +295,33 @@ def create_retriever_from_files(doc, embeddings, index_name: str):
loader = UnstructuredFileLoader(doc, mode="single", strategy="fast")
chunks = loader.load_and_split(text_splitter)

rds = Redis.from_texts(
texts=[chunk.page_content for chunk in chunks],
metadatas=[chunk.metadata for chunk in chunks],
embedding=embeddings,
index_name=index_name,
redis_url=REDIS_URL,
index_schema=INDEX_SCHEMA,
)

retriever = rds.as_retriever(search_type="mmr")
if VECTOR_DATABASE == "REDIS":
from langchain_community.vectorstores import Redis
from rag_redis.config import INDEX_SCHEMA, REDIS_URL

vdb = Redis.from_texts(
texts=[chunk.page_content for chunk in chunks],
metadatas=[chunk.metadata for chunk in chunks],
embedding=embeddings,
index_name=index_name,
redis_url=REDIS_URL,
index_schema=INDEX_SCHEMA,
)

elif VECTOR_DATABASE == "QDRANT":
from langchain_community.vectorstores import Qdrant
from rag_qdrant.config import COLLECTION_NAME, QDRANT_HOST, QDRANT_PORT

vdb = Qdrant.from_texts(
texts=[chunk.page_content for chunk in chunks],
metadatas=[chunk.metadata for chunk in chunks],
embedding=embeddings,
collection_name=COLLECTION_NAME,
host=QDRANT_HOST,
port=QDRANT_PORT,
)

retriever = vdb.as_retriever(search_type="mmr")
return retriever


Expand All @@ -315,29 +335,63 @@ def create_retriever_from_links(embeddings, link_list: list, index_name):
texts.append(data)
metadatas.append(metadata)

rds = Redis.from_texts(
texts=texts,
metadatas=metadatas,
embedding=embeddings,
index_name=index_name,
redis_url=REDIS_URL,
index_schema=INDEX_SCHEMA,
)

retriever = rds.as_retriever(search_type="mmr")
if VECTOR_DATABASE == "REDIS":
from langchain_community.vectorstores import Redis
from rag_redis.config import INDEX_SCHEMA, REDIS_URL

vdb = Redis.from_texts(
texts=texts,
metadatas=metadatas,
embedding=embeddings,
index_name=index_name,
redis_url=REDIS_URL,
index_schema=INDEX_SCHEMA,
)

elif VECTOR_DATABASE == "QDRANT":
from langchain_community.vectorstores import Qdrant
from rag_qdrant.config import COLLECTION_NAME, QDRANT_HOST, QDRANT_PORT

vdb = Qdrant.from_texts(
texts=texts,
metadatas=metadatas,
embedding=embeddings,
collection_name=COLLECTION_NAME,
host=QDRANT_HOST,
port=QDRANT_PORT,
)

retriever = vdb.as_retriever(search_type="mmr")
return retriever


def reload_retriever(embeddings, index_name):
print(f"[rag - reload retriever] reload with index: {index_name}")
rds = Redis.from_existing_index(
embeddings,
index_name=index_name,
redis_url=REDIS_URL,
schema=INDEX_SCHEMA,
)

retriever = rds.as_retriever(search_type="mmr")

if VECTOR_DATABASE == "REDIS":
from langchain_community.vectorstores import Redis
from rag_redis.config import INDEX_SCHEMA, REDIS_URL

vdb = Redis.from_existing_index(
embeddings,
index_name=index_name,
redis_url=REDIS_URL,
schema=INDEX_SCHEMA,
)

elif VECTOR_DATABASE == "QDRANT":
from langchain_community.vectorstores import Qdrant
from qdrant_client import QdrantClient
from rag_qdrant.config import COLLECTION_NAME, QDRANT_HOST, QDRANT_PORT

client = QdrantClient(host=QDRANT_HOST, port=QDRANT_PORT)
vdb = Qdrant(
embeddings=embeddings,
collection_name=COLLECTION_NAME,
client=client,
)

retriever = vdb.as_retriever(search_type="mmr")
return retriever


Expand Down
1 change: 1 addition & 0 deletions ChatQnA/langchain/docker/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ poetry
pyarrow
pydantic==1.10.13
pymupdf
qdrant-client==1.9.0
redis
sentence-transformers
unstructured
Expand Down
21 changes: 21 additions & 0 deletions ChatQnA/langchain/qdrant/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2023 LangChain, Inc.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Binary file added ChatQnA/langchain/qdrant/data/nke-10k-2023.pdf
Binary file not shown.
Loading

0 comments on commit f1b4aef

Please sign in to comment.