mirror of https://github.com/hwchase17/langchain
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
parent
9adaa78c65
commit
4209457bdc
@ -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
|
||||
```
|
Binary file not shown.
@ -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…
Reference in New Issue