This project applies the core knowledge from the LLMOps module, including the design and implementation of the API Layer, Inference Layer, Observability Layer, Cache Layer, Guardrails Layer, Routing Layer, and the Data Ingestion Pipeline.
73
stars
9
commits
Python
primary language
Dec 27, 2025
updated
This project implements a complete Retrieval-Augmented Generation (RAG) chatbot system using Langchain and modern LLMOps best practices. It covers the full lifecycle of a RAG-based application — from ingesting documents and managing embeddings, to optimizing inference and ensuring observability, safety, and scalability. The system is designed in a modular and production-ready architecture, consisting of key layers such as embedding ingestion, inference, caching, observability, routing, and gateway. It supports both streaming and blocking APIs, integrates with tools like Langfuse, Airflow, vLLM, and FastAPI, and follows best practices in error handling, fallback strategies, and guardrail implementation. This project is ideal for developers, MLOps engineers, or researchers looking to understand, build, or deploy scalable and secure RAG systems in production.
|
This RAG system follows a modular, microservices-oriented architecture with clear separation of concerns.
git clone https://github.com/your-user/your-repo.git
cd your-repo
Copy the example environment file to create your own configuration:
cp example.env .env
Next, edit the .env file and add the required values:
LANGFUSE_SECRET_KEY=your-langfuse-secret-key
LANGFUSE_PUBLIC_KEY=your-langfuse-public-key
LANGFUSE_HOST=http://localhost:3000
GROQ_API_KEY=your-groq-api-key
# If you plan to use OpenAI, add your key here
# OPENAI_API_KEY=your-openai-api-key
Create and activate a Conda environment for the project:
conda create --name rag-ops python=3.10
conda activate rag-ops
Install the required Python packages using pip:
pip install -r requirements.txt
The system is composed of multiple services that need to be started in order.
Start the Redis cache service:
cd infrastructure/cache/
docker compose up -d
Start the Langfuse observability stack (from the parent directory):
cd ../observability/
docker compose up -d
Start the data ingestion services (Airflow, Minio):
cd ../../ingest_data/
docker compose up -d
airflow/airflow).ingest_data and trigger it manually.infrastructure/storage/data_source and store the embeddings in ChromaDB.After all infrastructure is running and the data has been ingested, start the FastAPI application from the project's root directory:
# Use the default dataset (environment_battery)
python -m src.main --provider groq
# Or specify a different dataset that you've ingested
python -m src.main --provider groq --dataset llm_papers
The server will be accessible at http://localhost:8000 by default. You can access the API documentation at http://localhost:8000/docs.
The API layer is built with FastAPI and provides a modern, robust interface for interacting with the RAG system. It supports both standard and real-time communication patterns. To offer a flexible API that supports both blocking and streaming responses, ensuring a good user experience for various applications.
/v1/rest-retrieve/) for simple request-response interactions./v1/sse-retrieve/) that sends responses token-by-token, ideal for real-time applications like chatbots.This section details the automated pipeline for ingesting documents, processing them, and storing them as vector embeddings for retrieval. The entire workflow is orchestrated using Apache Airflow.
The ingestion pipeline, defined in ingest_data/dags/ingesting_data.py, follows a standard flow:
infrastructure/storage/data_source directory.infrastructure/storage/chromadb directory, making them available for the RAG service.This entire process is managed by an Airflow DAG that you can trigger and monitor through the Airflow UI.
The ingest_data DAG is displayed on the Airflow UI after a successful run. This visualizes the pipeline's flow from loading and chunking data to embedding and storing it.
This layer is designed to deliver fast, cost-efficient, and scalable language model inference, regardless of the underlying provider. The goal is to support a flexible and modular architecture that allows seamless switching between different LLM serving options—whether you're running local models for development or deploying high-throughput inference at scale. By abstracting the provider logic, the system ensures portability and performance across environments.
The system is designed to be provider-agnostic, allowing you to switch between different LLMs with ease. This is managed in run.py, where you can specify a provider at startup.
Supported providers and serving options include:
You can select the provider when starting the API server:
# Example using Groq
python -m src.main --provider groq
# Example using a local model served with LM Studio
python -m src.main --provider lm-studio
This layer brings end-to-end visibility into the RAG system, leveraging Langfuse to trace prompts, monitor LLM interactions, track token usage, and evaluate system performance. By integrating observability directly into core services, the system enables systematic debugging, performance tuning, and cost optimization — making it easier to operate and maintain RAG applications in production.
The system integrates Langfuse across multiple components:
rag.py): Employs multiple Langfuse features for comprehensive tracing. In addition to decorators, it uses with langfuse.start_as_current_span(...) to create custom spans for granular monitoring of specific logic, such as guardrail checks.generator.py): Utilize Langfuse's CallbackHandler to automatically trace the generation process and LLM interactions.The project's observability setup adapts to different environments:
LANGFUSE_SECRET_KEY, LANGFUSE_PUBLIC_KEY, LANGFUSE_HOST) in your CI/CD secrets to point to your cloud project.This layer focuses on reducing latency and API overhead by implementing both standard and semantic caching strategies tailored for RAG systems. It intelligently avoids redundant computations—especially costly LLM calls—by caching deterministic operations and semantically similar responses. The goal is to improve system responsiveness, optimize cost, and ensure consistent user experience, even under high load.
The system employs two distinct caching strategies, both implemented as easy-to-use decorators @:
standard_cache.py)A traditional key-value cache built on Redis for deterministic outputs. It features a well-designed, hierarchical key structure to ensure cache entries are unique, organized, and easy to debug.
Cache Key Anatomy
A cache key is automatically generated with the following structure:
┌─────────────────────────────────────────────────────────────────────────────────────────┐
│ mlops : development : src.services.rag : Rag.get_response : [...] : {} │
│ ↑ ↑ ↑ ↑ ↑ ↑ │
│ │ │ │ │ │ │ │
│ │ │ │ │ │ └── Kwargs (JSON)
│ │ │ │ │ └───────── Args (JSON)
│ │ │ │ └───────────────────── Function Name
│ │ │ └─────────────────────────────────────────────── Module Path
│ │ └────────────────────────────────────────────────────────────── Environment
│ └──────────────────────────────────────────────────────────────────────── Project Namespace
└─────────────────────────────────────────────────────────────────────────────────────────┘
Key Components Breakdown:
mlops): A static prefix to prevent key collisions with other projects that might be sharing the same Redis instance.development): Automatically captures the current environment (e.g., development, staging, production), isolating caches so that development data does not interfere with production.src.services.rag): The full path to the module containing the cached function, making it easy to trace the origin of a cache entry.Rag.get_response): The specific name of the function being cached. The implementation is smart enough to include the class name for methods, distinguishing them from standalone functions.[...] and {}): The positional and keyword arguments passed to the function—such as the user's query, session_id, and user_id—are serialized into the key. This is the most critical part, as it ensures that calls to the same function with different arguments produce unique cache entries.Benefits of this Structure:
session_id in the key is a deliberate design choice that enables personalized responses. The answer to the same question can differ between sessions based on the conversation history. For example, for the query "What is ML?", a user who previously discussed Python might get a Python-centric answer, while another who discussed Java would receive a Java-related one. Caching by session is therefore essential for correctness.semantic_cache.py)An advanced caching mechanism specifically for LLM responses. Instead of relying on exact matches of input strings, this cache uses vector embeddings to store and retrieve responses based on the semantic similarity of user queries. When a new query is received, it is converted into an embedding and compared against the cached entries. If a sufficiently similar query is found, the cached response is served, avoiding a costly LLM call. This is particularly effective for handling frequently asked questions or paraphrased versions of the same query.
You can interact with the RAG API using the following endpoints.
curl -X 'POST' \
'http://localhost:8000/v1/rest-retrieve/' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"user_input": "What is attention mechanism?"
}'
curl -X 'POST' \
'http://localhost:8000/v1/sse-retrieve/' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"user_input": "What is attention mechanism?"
}'
If you encounter connection errors when using LM Studio with Langfuse:
http://host.docker.internal:1234/v1 for Docker environmentsMake sure to start services in the correct order:
9 commits
Python
97.8%
Shell
1.4%
This project applies the core knowledge from the LLMOps module, including the design and implementation of the API Layer, Inference Layer, Observability Layer, Cache Layer, Guardrails Layer, Routing Layer, and the Data Ingestion Pipeline.
73
stars
9
commits
Python
primary language
Dec 27, 2025
updated
This project implements a complete Retrieval-Augmented Generation (RAG) chatbot system using Langchain and modern LLMOps best practices. It covers the full lifecycle of a RAG-based application — from ingesting documents and managing embeddings, to optimizing inference and ensuring observability, safety, and scalability. The system is designed in a modular and production-ready architecture, consisting of key layers such as embedding ingestion, inference, caching, observability, routing, and gateway. It supports both streaming and blocking APIs, integrates with tools like Langfuse, Airflow, vLLM, and FastAPI, and follows best practices in error handling, fallback strategies, and guardrail implementation. This project is ideal for developers, MLOps engineers, or researchers looking to understand, build, or deploy scalable and secure RAG systems in production.
|
This RAG system follows a modular, microservices-oriented architecture with clear separation of concerns.
git clone https://github.com/your-user/your-repo.git
cd your-repo
Copy the example environment file to create your own configuration:
cp example.env .env
Next, edit the .env file and add the required values:
LANGFUSE_SECRET_KEY=your-langfuse-secret-key
LANGFUSE_PUBLIC_KEY=your-langfuse-public-key
LANGFUSE_HOST=http://localhost:3000
GROQ_API_KEY=your-groq-api-key
# If you plan to use OpenAI, add your key here
# OPENAI_API_KEY=your-openai-api-key
Create and activate a Conda environment for the project:
conda create --name rag-ops python=3.10
conda activate rag-ops
Install the required Python packages using pip:
pip install -r requirements.txt
The system is composed of multiple services that need to be started in order.
Start the Redis cache service:
cd infrastructure/cache/
docker compose up -d
Start the Langfuse observability stack (from the parent directory):
cd ../observability/
docker compose up -d
Start the data ingestion services (Airflow, Minio):
cd ../../ingest_data/
docker compose up -d
airflow/airflow).ingest_data and trigger it manually.infrastructure/storage/data_source and store the embeddings in ChromaDB.After all infrastructure is running and the data has been ingested, start the FastAPI application from the project's root directory:
# Use the default dataset (environment_battery)
python -m src.main --provider groq
# Or specify a different dataset that you've ingested
python -m src.main --provider groq --dataset llm_papers
The server will be accessible at http://localhost:8000 by default. You can access the API documentation at http://localhost:8000/docs.
The API layer is built with FastAPI and provides a modern, robust interface for interacting with the RAG system. It supports both standard and real-time communication patterns. To offer a flexible API that supports both blocking and streaming responses, ensuring a good user experience for various applications.
/v1/rest-retrieve/) for simple request-response interactions./v1/sse-retrieve/) that sends responses token-by-token, ideal for real-time applications like chatbots.This section details the automated pipeline for ingesting documents, processing them, and storing them as vector embeddings for retrieval. The entire workflow is orchestrated using Apache Airflow.
The ingestion pipeline, defined in ingest_data/dags/ingesting_data.py, follows a standard flow:
infrastructure/storage/data_source directory.infrastructure/storage/chromadb directory, making them available for the RAG service.This entire process is managed by an Airflow DAG that you can trigger and monitor through the Airflow UI.
The ingest_data DAG is displayed on the Airflow UI after a successful run. This visualizes the pipeline's flow from loading and chunking data to embedding and storing it.
This layer is designed to deliver fast, cost-efficient, and scalable language model inference, regardless of the underlying provider. The goal is to support a flexible and modular architecture that allows seamless switching between different LLM serving options—whether you're running local models for development or deploying high-throughput inference at scale. By abstracting the provider logic, the system ensures portability and performance across environments.
The system is designed to be provider-agnostic, allowing you to switch between different LLMs with ease. This is managed in run.py, where you can specify a provider at startup.
Supported providers and serving options include:
You can select the provider when starting the API server:
# Example using Groq
python -m src.main --provider groq
# Example using a local model served with LM Studio
python -m src.main --provider lm-studio
This layer brings end-to-end visibility into the RAG system, leveraging Langfuse to trace prompts, monitor LLM interactions, track token usage, and evaluate system performance. By integrating observability directly into core services, the system enables systematic debugging, performance tuning, and cost optimization — making it easier to operate and maintain RAG applications in production.
The system integrates Langfuse across multiple components:
rag.py): Employs multiple Langfuse features for comprehensive tracing. In addition to decorators, it uses with langfuse.start_as_current_span(...) to create custom spans for granular monitoring of specific logic, such as guardrail checks.generator.py): Utilize Langfuse's CallbackHandler to automatically trace the generation process and LLM interactions.The project's observability setup adapts to different environments:
LANGFUSE_SECRET_KEY, LANGFUSE_PUBLIC_KEY, LANGFUSE_HOST) in your CI/CD secrets to point to your cloud project.This layer focuses on reducing latency and API overhead by implementing both standard and semantic caching strategies tailored for RAG systems. It intelligently avoids redundant computations—especially costly LLM calls—by caching deterministic operations and semantically similar responses. The goal is to improve system responsiveness, optimize cost, and ensure consistent user experience, even under high load.
The system employs two distinct caching strategies, both implemented as easy-to-use decorators @:
standard_cache.py)A traditional key-value cache built on Redis for deterministic outputs. It features a well-designed, hierarchical key structure to ensure cache entries are unique, organized, and easy to debug.
Cache Key Anatomy
A cache key is automatically generated with the following structure:
┌─────────────────────────────────────────────────────────────────────────────────────────┐
│ mlops : development : src.services.rag : Rag.get_response : [...] : {} │
│ ↑ ↑ ↑ ↑ ↑ ↑ │
│ │ │ │ │ │ │ │
│ │ │ │ │ │ └── Kwargs (JSON)
│ │ │ │ │ └───────── Args (JSON)
│ │ │ │ └───────────────────── Function Name
│ │ │ └─────────────────────────────────────────────── Module Path
│ │ └────────────────────────────────────────────────────────────── Environment
│ └──────────────────────────────────────────────────────────────────────── Project Namespace
└─────────────────────────────────────────────────────────────────────────────────────────┘
Key Components Breakdown:
mlops): A static prefix to prevent key collisions with other projects that might be sharing the same Redis instance.development): Automatically captures the current environment (e.g., development, staging, production), isolating caches so that development data does not interfere with production.src.services.rag): The full path to the module containing the cached function, making it easy to trace the origin of a cache entry.Rag.get_response): The specific name of the function being cached. The implementation is smart enough to include the class name for methods, distinguishing them from standalone functions.[...] and {}): The positional and keyword arguments passed to the function—such as the user's query, session_id, and user_id—are serialized into the key. This is the most critical part, as it ensures that calls to the same function with different arguments produce unique cache entries.Benefits of this Structure:
session_id in the key is a deliberate design choice that enables personalized responses. The answer to the same question can differ between sessions based on the conversation history. For example, for the query "What is ML?", a user who previously discussed Python might get a Python-centric answer, while another who discussed Java would receive a Java-related one. Caching by session is therefore essential for correctness.semantic_cache.py)An advanced caching mechanism specifically for LLM responses. Instead of relying on exact matches of input strings, this cache uses vector embeddings to store and retrieve responses based on the semantic similarity of user queries. When a new query is received, it is converted into an embedding and compared against the cached entries. If a sufficiently similar query is found, the cached response is served, avoiding a costly LLM call. This is particularly effective for handling frequently asked questions or paraphrased versions of the same query.
You can interact with the RAG API using the following endpoints.
curl -X 'POST' \
'http://localhost:8000/v1/rest-retrieve/' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"user_input": "What is attention mechanism?"
}'
curl -X 'POST' \
'http://localhost:8000/v1/sse-retrieve/' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"user_input": "What is attention mechanism?"
}'
If you encounter connection errors when using LM Studio with Langfuse:
http://host.docker.internal:1234/v1 for Docker environmentsMake sure to start services in the correct order:
9 commits
Python
97.8%
Shell
1.4%