Redis langserve template (#12443)

Add Redis langserve template! Eventually will add semantic caching to
this too. But I was struggling to get that to work for some reason with
the LCEL implementation here.

- **Description:** Introduces the Redis LangServe template. A simple RAG
based app built on top of Redis that allows you to chat with company's
public financial data (Edgar 10k filings)
  - **Issue:** None
- **Dependencies:** The template contains the poetry project
requirements to run this template
  - **Tag maintainer:** @baskaryan @Spartee 
  - **Twitter handle:** @tchutch94

**Note**: this requires the commit here that deletes the
`_aget_relevant_documents()` method from the Redis retriever class that
wasn't implemented. That was breaking the langserve app.

---------

Co-authored-by: Sam Partee <sam.partee@redis.com>
pull/12527/head^2
Tyler Hutcherson 7 months ago committed by GitHub
parent 9adaa78c65
commit 4209457bdc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -24,10 +24,7 @@ import numpy as np
import yaml
from langchain._api import deprecated
from langchain.callbacks.manager import (
AsyncCallbackManagerForRetrieverRun,
CallbackManagerForRetrieverRun,
)
from langchain.callbacks.manager import CallbackManagerForRetrieverRun
from langchain.docstore.document import Document
from langchain.schema.embeddings import Embeddings
from langchain.schema.vectorstore import VectorStore, VectorStoreRetriever
@ -1450,11 +1447,6 @@ class RedisVectorStoreRetriever(VectorStoreRetriever):
raise ValueError(f"search_type of {self.search_type} not allowed.")
return docs
async def _aget_relevant_documents(
self, query: str, *, run_manager: AsyncCallbackManagerForRetrieverRun
) -> List[Document]:
raise NotImplementedError("RedisVectorStoreRetriever does not support async")
def add_documents(self, documents: List[Document], **kwargs: Any) -> List[str]:
"""Add documents to vectorstore."""
return self.vectorstore.add_documents(documents, **kwargs)

@ -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.

@ -0,0 +1,77 @@
# Redis RAG Example
Using Langserve and Redis to build a RAG search example for answering questions on financial 10k filings docs (for Nike).
Relies on the sentence transformer `all-MiniLM-L6-v2` for embedding chunks of the pdf and user questions.
## Running Redis
There are a number of ways to run Redis depending on your use case and scenario.
### Easiest? Redis Cloud
Create a free database on [Redis Cloud](https://redis.com/try-free). *No credit card information is required*. Simply fill out the info form and select the cloud vendor of your choice and region.
Once you have created an account and database, you can find the connection credentials by clicking on the database and finding the "Connect" button which will provide a few options. Below are the environment variables you need to configure to run this RAG app.
```bash
export REDIS_HOST = <YOUR REDIS HOST>
export REDIS_PORT = <YOUR REDIS PORT>
export REDIS_USER = <YOUR REDIS USER NAME>
export REDIS_PASSWORD = <YOUR REDIS PASSWORD>
```
For larger use cases (greater than 30mb of data), you can certainly created a Fixed or Flexible billing subscription which can scale with your dataset size.
### Redis Stack -- Local Docker
For local development, you can use Docker:
```bash
docker run -p 6397:6397 -p 8001:8001 redis/redis-stack:latest
```
This will run Redis on port 6379. You can then check that it is running by visiting the RedisInsight GUI at [http://localhost:8001](http://localhost:8001).
This is the connection that the application will try to use by default -- local dockerized Redis.
## Data
To load the financial 10k pdf (for Nike) into the vectorstore, run the following command from the root of this repository:
```bash
poetry shell
python ingest.py
```
## Supported Settings
We use a variety of environment variables to configure this application
| Environment Variable | Description | Default Value |
|----------------------|-----------------------------------|---------------|
| `DEBUG` | Enable or disable Langchain debugging logs | True |
| `REDIS_HOST` | Hostname for the Redis server | "localhost" |
| `REDIS_PORT` | Port for the Redis server | 6379 |
| `REDIS_USER` | User for the Redis server | "" |
| `REDIS_PASSWORD` | Password for the Redis server | "" |
| `REDIS_URL` | Full URL for connecting to Redis | `None`, Constructed from user, password, host, and port if not provided |
| `INDEX_NAME` | Name of the vector index | "rag-redis" |
## Installation
To create a langserve application using this template, run the following:
```bash
langchain serve new my-langserve-app
cd my-langserve-app
```
Add this template:
```bash
langchain serve add rag-redis
```
Start the server:
```bash
langchain start
```

@ -0,0 +1,49 @@
import os
from langchain.document_loaders import UnstructuredFileLoader
from langchain.embeddings import HuggingFaceEmbeddings
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.vectorstores import Redis
from rag_redis.config import EMBED_MODEL, INDEX_NAME, INDEX_SCHEMA, REDIS_URL
def ingest_documents():
"""
Ingest PDF to Redis from the data/ directory that
contains Edgar 10k filings data for Nike.
"""
# Load list of pdfs
company_name = "Nike"
data_path = "data/"
doc = [
os.path.join(data_path, file) for file in os.listdir(data_path)
][0]
print("Parsing 10k filing doc for NIKE", doc)
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1500, chunk_overlap=100, add_start_index=True
)
loader = UnstructuredFileLoader(doc, mode="single", strategy="fast")
chunks = loader.load_and_split(text_splitter)
print("Done preprocessing. Created", len(chunks), "chunks of the original pdf")
# Create vectorstore
embedder = HuggingFaceEmbeddings(
model_name=EMBED_MODEL
)
_ = Redis.from_texts(
# appending this little bit can sometimes help with semantic retrieval
# especially with multiple companies
texts=[f"Company: {company_name}. " + chunk.page_content for chunk in chunks],
metadatas=[chunk.metadata for chunk in chunks],
embedding=embedder,
index_name=INDEX_NAME,
index_schema=INDEX_SCHEMA,
redis_url=REDIS_URL
)
if __name__ == "__main__":
ingest_documents()

File diff suppressed because it is too large Load Diff

@ -0,0 +1,37 @@
[tool.poetry]
name = "rag-redis"
version = "0.0.1"
description = "Run a RAG app backed by OpenAI, HuggingFace, and Redis as a vector database"
authors = ["Tyler Hutcherson <tyler.hutcherson@redis.com>", "Sam Partee <sam.partee@redis.com>"]
readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
langchain = ">=0.0.313, <0.1"
fastapi = "^0.104.0"
sse-starlette = "^1.6.5"
openai = "^0.28.1"
sentence-transformers = "2.2.2"
redis = "5.0.1"
tiktoken = "0.5.1"
pdf2image = "1.16.3"
unstructured = {version = "^0.10.27", extras = ["pdf"]}
[tool.poetry.group.dev.dependencies]
langchain-cli = {git = "https://github.com/langchain-ai/langchain.git", rev = "erick/cli", subdirectory = "libs/cli"}
poethepoet = "^0.24.1"
[tool.langserve]
export_module = "rag_redis.chain"
export_attr = "chain"
[tool.poe.tasks.start]
cmd="uvicorn langchain_cli.dev_scripts:create_demo_server --reload --port $port --host $host"
args = [
{name = "port", help = "port to run on", default = "8000"},
{name = "host", help = "host to run on", default = "127.0.0.1"}
]
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

@ -0,0 +1,88 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "681a5d1e",
"metadata": {},
"source": [
"## Connect to RAG App\n",
"\n",
"Assuming you are already running this server:\n",
"```bash\n",
"langserve start\n",
"```"
]
},
{
"cell_type": "code",
"execution_count": 37,
"id": "d774be2a",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Nike's revenue in 2023 was $51.2 billion. \n",
"\n",
"Source: 'data/nke-10k-2023.pdf', Start Index: '146100'\n"
]
}
],
"source": [
"from langserve.client import RemoteRunnable\n",
"\n",
"rag_redis = RemoteRunnable('http://localhost:8000/rag-redis')\n",
"\n",
"print(rag_redis.invoke(\"What was Nike's revenue in 2023?\"))"
]
},
{
"cell_type": "code",
"execution_count": 43,
"id": "07ae0005",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"As of May 31, 2023, Nike had approximately 83,700 employees worldwide. This information can be found in the first piece of context provided. (source: data/nke-10k-2023.pdf, start_index: 32532)\n"
]
}
],
"source": [
"print(rag_redis.invoke(\"How many employees work at Nike?\"))"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "4a6b9f00",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.6"
}
},
"nbformat": 4,
"nbformat_minor": 5
}

@ -0,0 +1,68 @@
from langchain.chat_models import ChatOpenAI
from langchain.embeddings import HuggingFaceEmbeddings
from langchain.prompts import ChatPromptTemplate
from langchain.pydantic_v1 import BaseModel
from langchain.schema.output_parser import StrOutputParser
from langchain.schema.runnable import RunnableParallel, RunnablePassthrough
from langchain.vectorstores import Redis
from rag_redis.config import (
EMBED_MODEL,
INDEX_NAME,
INDEX_SCHEMA,
REDIS_URL,
)
# Make this look better in the docs.
class Question(BaseModel):
__root__: str
# Init Embeddings
embedder = HuggingFaceEmbeddings(model_name=EMBED_MODEL)
# Connect to pre-loaded vectorstore
# run the ingest.py script to populate this
vectorstore = Redis.from_existing_index(
embedding=embedder,
index_name=INDEX_NAME,
schema=INDEX_SCHEMA,
redis_url=REDIS_URL
)
# TODO allow user to change parameters
retriever = vectorstore.as_retriever(search_type="mmr")
# Define our prompt
template = """
Use the following pieces of context from Nike's financial 10k filings
dataset to answer the question. Do not make up an answer if there is no
context provided to help answer it. Include the 'source' and 'start_index'
from the metadata included in the context you used to answer the question
Context:
---------
{context}
---------
Question: {question}
---------
Answer:
"""
prompt = ChatPromptTemplate.from_template(template)
# RAG Chain
model = ChatOpenAI(model_name="gpt-3.5-turbo-16k")
chain = (
RunnableParallel({"context": retriever,
"question": RunnablePassthrough()})
| prompt
| model
| StrOutputParser()
).with_types(input_type=Question)

@ -0,0 +1,76 @@
import os
def get_boolean_env_var(var_name, default_value=False):
"""Retrieve the boolean value of an environment variable.
Args:
var_name (str): The name of the environment variable to retrieve.
default_value (bool): The default value to return if the variable
is not found.
Returns:
bool: The value of the environment variable, interpreted as a boolean.
"""
true_values = {'true', '1', 't', 'y', 'yes'}
false_values = {'false', '0', 'f', 'n', 'no'}
# Retrieve the environment variable's value
value = os.getenv(var_name, '').lower()
# Decide the boolean value based on the content of the string
if value in true_values:
return True
elif value in false_values:
return False
else:
return default_value
# Check for openai API key
if "OPENAI_API_KEY" not in os.environ:
raise Exception("Must provide an OPENAI_API_KEY as an env var.")
# Whether or not to enable langchain debugging
DEBUG = get_boolean_env_var("DEBUG", False)
# Set DEBUG env var to "true" if you wish to enable LC debugging module
if DEBUG:
import langchain
langchain.debug=True
# Embedding model
EMBED_MODEL = os.getenv("EMBED_MODEL",
"sentence-transformers/all-MiniLM-L6-v2")
# Redis Connection Information
REDIS_HOST = os.getenv("REDIS_HOST", "localhost")
REDIS_PORT = int(os.getenv("REDIS_PORT", 6379))
def format_redis_conn_from_env():
redis_url = os.getenv("REDIS_URL", None)
if redis_url:
return redis_url
else:
using_ssl = get_boolean_env_var("REDIS_SSL", False)
start = "rediss://" if using_ssl else "redis://"
# if using RBAC
password = os.getenv("REDIS_PASSWORD", None)
username = os.getenv("REDIS_USERNAME", "default")
if password is not None:
start += f"{username}:{password}@"
return start + f"{REDIS_HOST}:{REDIS_PORT}"
REDIS_URL = format_redis_conn_from_env()
# Vector Index Configuration
INDEX_NAME = os.getenv("INDEX_NAME", "rag-redis")
current_file_path = os.path.abspath(__file__)
parent_dir = os.path.dirname(current_file_path)
schema_path = os.path.join(parent_dir, 'schema.yml')
INDEX_SCHEMA = schema_path

@ -0,0 +1,11 @@
text:
- name: content
- name: source
numeric:
- name: start_index
vector:
- name: content_vector
algorithm: HNSW
datatype: FLOAT32
dims: 384
distance_metric: COSINE
Loading…
Cancel
Save