Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ ipython_config.py
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
#uv.lock
requirements.uv.txt

# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ In order to run own copy of the project one must fulfill the following requireme

- [Python 3.12](https://www.python.org/downloads/release/python-3120/)
- [Git](https://git-scm.com/)
- [uv](https://github.com/astral-sh/uv)

### Virtual environments

Expand All @@ -20,7 +21,8 @@ The following sequence of commands creates an environment, activates the environ
```bash
python3 -m venv ~/path-to-venv; \
source ~/path-to-venv/bin/activate; \
pip3 install -r ./requirements.txt
uv pip compile requirements.txt --output-file requirements.uv.txt; \
uv pip sync ./requirements.uv.txt
```

## Committing changes to the repo
Expand Down
6 changes: 3 additions & 3 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
BeautifulSoup4
black
pre-commit

# RAG
python-dotenv
tiktoken
langchain
langchain_community
langchain_text_splitters
langchain-ollama
langchain-qdrant
qdrant-client
scikit-learn

# api
fastapi
uvicorn
14 changes: 11 additions & 3 deletions src/api.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
from contextlib import asynccontextmanager
from fastapi import FastAPI
import uvicorn

from app import rag_application
import app as rag_module

app = FastAPI()

@asynccontextmanager
async def lifespan(app: FastAPI):
rag_module.initialize()
yield


app = FastAPI(lifespan=lifespan)


@app.get("/")
async def root(question: str | None = None):
if question:
answer = rag_application.run(question)
answer = rag_module.rag_application.run(question)
return {"question": question, "answer": answer}
return {
"message": "RAG application is online. Pass a `question` as a query parameter to this endpoint."
Expand Down
98 changes: 70 additions & 28 deletions src/app.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import os
from dotenv import load_dotenv

import subprocess
import sys

from langchain_qdrant import QdrantVectorStore
from qdrant_client import QdrantClient
from qdrant_client.http.exceptions import UnexpectedResponse
from langchain_ollama import OllamaEmbeddings

from langchain_ollama import ChatOllama
Expand All @@ -10,33 +15,7 @@

load_dotenv()

embedding = OllamaEmbeddings(base_url="http://localhost:11434", model="llama3.2:latest")

vector_store = QdrantVectorStore.from_existing_collection(
embedding=embedding,
collection_name="documents",
url=os.getenv("QDRANT_URL", "http://localhost:6333"),
api_key=os.getenv("QDRANT__SERVICE__API_KEY", None),
)
retriever = vector_store.as_retriever(k=4)

prompt = PromptTemplate(
template="""You are an assistant for question-answering tasks.
Use the following documents to answer the question.
If you don't know the answer, just say that you don't know.
Use three sentences maximum and keep the answer concise:
Question: {question}
Documents: {documents}
Answer:
""",
input_variables=["question", "documents"],
)

llm = ChatOllama(
base_url="http://localhost:11434", model="llama3.2:latest", temperature=0
)

rag_chain = prompt | llm | StrOutputParser()
rag_application = None


class RAGApplication:
Expand All @@ -51,4 +30,67 @@ def run(self, question):
return answer


rag_application = RAGApplication(retriever, rag_chain)
def initialize():
"""The initialization function must be called once during the application startup."""

global rag_application

qdrant_client = QdrantClient(
url=os.getenv("QDRANT_URL", "https://localhost:6333"),
api_key=os.getenv("QDRANT_API_KEY", None),
)

try:
qdrant_client.get_collection(collection_name="rag-app")
except Exception as e:
if isinstance(e, UnexpectedResponse) and getattr(e, "status_code", None) == 404:
print("[i] Qdrant collection `rag-app` not found. Creating embeddings...")
script_dir = os.path.dirname(os.path.abspath(__file__))
result = subprocess.run(
[sys.executable, os.path.join(script_dir, "embeddings.py")],
capture_output=True,
text=True,
cwd=script_dir,
)
print(result.stdout)
if result.returncode != 0:
print("[err] embeddings.py failed:")
print(result.stderr)
raise RuntimeError("embeddings.py failed during startup.")
print("[i] Collection `rag-app` created. Continuing with startup...")
else:
raise

embedding = OllamaEmbeddings(
base_url="http://localhost:11434", model="embeddinggemma:latest"
)

vector_store = QdrantVectorStore(
client=qdrant_client, collection_name="rag-app", embedding=embedding
)
retriever = vector_store.as_retriever(k=4)

prompt = PromptTemplate(
template="""You are an assistant for question-answering tasks.
Use the following documents to answer the question.
If you don't know the answer, just say that you don't know.
Use three sentences maximum and keep the answer concise:
Question: {question}
Documents: {documents}
Answer:
""",
input_variables=["question", "documents"],
)

llm = ChatOllama(
base_url="http://localhost:11434", model="gemma3n:latest", temperature=0
)

rag_chain = prompt | llm | StrOutputParser()

rag_application = RAGApplication(retriever, rag_chain)

question = "What operating systems does nx-ng-starter support?"
answer = rag_application.run(question)
print("Question:", question)
print("Answer:", answer)
8 changes: 5 additions & 3 deletions src/embeddings.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@

doc_splits = text_splitter.split_documents(docs_list)

embedding = OllamaEmbeddings(base_url="http://localhost:11434", model="llama3.2:latest")
embedding = OllamaEmbeddings(
base_url="http://localhost:11434", model="embeddinggemma:latest"
)

vector_store = QdrantVectorStore.from_documents(
documents=doc_splits,
embedding=embedding,
collection_name="documents",
collection_name="rag-app",
url=os.getenv("QDRANT_URL", "http://localhost:6333"),
api_key=os.getenv("QDRANT__SERVICE__API_KEY", None),
api_key=os.getenv("QDRANT_API_KEY", None),
)
72 changes: 0 additions & 72 deletions src/init.py

This file was deleted.

91 changes: 91 additions & 0 deletions src/start.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
#!/bin/bash

# Install Qdrant vector database
# https://qdrant.tech/documentation/quick-start/

docker pull qdrant/qdrant

RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'

if docker ps | grep -q qdrant; then
echo "Qdrant is already running. Skipping docker run."
else
echo "Starting Qdrant container..."

if [ -f .env ]; then
# shellcheck source=/dev/null
source .env
else
echo "Warning: .env file not found."
fi

if [ -z "$QDRANT_API_KEY" ]; then
echo "Error: QDRANT_API_KEY not found in .env and is not set globally"
exit 1
fi

if [ -z "$QDRANT_API_KEY_READ_ONLY" ]; then
echo "Error: QDRANT_API_KEY_READ_ONLY not found in .env and is not set globally"
exit 1
fi

DATA_DIR="$HOME/qdrant"
mkdir -p "$DATA_DIR"

# Run Qdrant container
# This starts Qdrant on port 6333 with persistent storage in /var/lib/qdrant/storage
# API key authentication documentation: https://github.com/hdt12a1/qdrant-tutorial/blob/main/api_key_authentication.md
docker run -d \
--name qdrant-rag-app \
--restart always \
-p 6333:6333 \
-p 6334:6334 \
-v "$DATA_DIR:/qdrant/storage" \
-e QDRANT_API_KEY="$QDRANT_API_KEY" \
-e QDRANT_API_KEY_READ_ONLY="$QDRANT_API_KEY_READ_ONLY" \
qdrant/qdrant:latest
fi

cleanup() {
echo -e "${YELLOW}[i]${NC} Qdrant docker container cleanup..."
docker stop qdrant-rag-app >/dev/null 2>&1 || true
docker rm qdrant-rag-app >/dev/null 2>&1 || true

echo -e "${GREEN}[ok]{$NC} Done."
}
trap cleanup EXIT

echo -e "${YELLOW}[i]${NC} Qdrant is running with data dir $DATA_DIR. Waiting for Qdrant..."

MAX_WAIT_SEC=60
SLEEP_SEC=1
ELAPSED=0
STARTUP_STATUS=""

while [ "$ELAPSED" -lt "$MAX_WAIT_SEC" ]; do
STARTUP_STATUS="$(curl -s -o /dev/null -w "%{http_code}" -H "api-key: $QDRANT_API_KEY" http://localhost:6333/collections || echo "curl_error")"
if [ "$STARTUP_STATUS" = "200" ]; then
break
fi
sleep "$SLEEP_SEC"
ELAPSED=$((ELAPSED + SLEEP_SEC))
done
if [ "$STARTUP_STATUS" != "200" ]; then
echo -e "${RED}[err]{$NC} Qdrant startup timeout. Last HTTP status: $STARTUP_STATUS"
echo -e "${YELLOW}[i]${NC} Qdrant container logs:"
docker logs --tail 50 qdrant-rag-app || echo -e "${RED}[warn]${NC} Could not fetch qdrant-rag-app Docker logs."
exit 1
fi

echo "Qdrant status:"
docker ps | grep qdrant

echo "Qdrant version check:"
curl -H "api-key: $QDRANT_API_KEY_READ_ONLY" http://localhost:6333 | grep version

cd "$(dirname "$0")" || exit 1

uvicorn api:app --reload
Loading
Loading