Skip to content

Commit

Permalink
Langchain integration (redis-developer#3)
Browse files Browse the repository at this point in the history
* use langchain
* finish integration
  • Loading branch information
tylerhutcherson authored Apr 28, 2023
1 parent b747e11 commit cdcb494
Show file tree
Hide file tree
Showing 8 changed files with 158 additions and 344 deletions.
18 changes: 13 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,30 @@
<img src="./app/assets/RedisOpenAI.png" alt="Drawing" style="width: 100%;"/> </td>


# Question & Answering using Redis & OpenAI
# Question & Answering using LangChain, Redis & OpenAI

**Redis** plays a crucial role in the LLM & GenAI wave with it's ability to store, retrieve, and search with vector spaces in a low-latency, high-availability setting. With its heritage in enterprise caching, Redis has both the developer community and enterprise-readiness required to deploy quality AI-enabled applications in this demanding marketplace.
**LangChain** simplifies the development of LLM applications through modular components and "chains". It acts as a wrapper around several complex tools and makes us more efficient in our development workflow.

**Redis** plays a crucial role with large language models (LLMs) for a few resons. It can store and retrieve data in near realtime (for caching) and can also index vector embeddings for semantic search. Semantic search enables the LLM to attach to external memory or "knowledge" to help augment the LLM prompts and ensure greater quality in results. Redis has both the developer community and enterprise-readiness required to deploy quality AI-enabled applications in this demanding marketplace.

**OpenAI** is shaping the future of next-gen apps through it's release of powerful natural language and computer vision models that are used in a variety of downstream tasks.

This example Streamlit app gives you the tools to get up and running with **Redis** as a vector database and **OpenAI** as a LLM provider for embedding creation and text generation. *The combination of the two is where the magic lies.*
This example Streamlit app gives you the tools to get up and running with **Redis** as a vector database, **OpenAI** as a LLM provider for embedding creation and text generation, and **LangChain** for application dev. *The combination of these is what makes things happen.*

![ref arch](app/assets/RedisOpenAI-QnA-Architecture.drawio.png)

____

## Create dev env (*optional*)
Use the provided conda env for development:
```bash
conda env create -f environment.yml
```

## Run the App
The example QnA application uses a dataset from wikipedia of articles about the 2020 summer olympics. The **first time you run the app** -- all docs will be downloaded, processed, and stored in Redis. This will take a few minutes to spin up initially. From that point forward, the app should be quicker to load.

### Use Docker Compose
Create your env file:
```bash
$ cp .env.template .env
Expand All @@ -35,6 +44,5 @@ Navigate to:
http://localhost:8080/
```

The **first time you run the app** -- all documents will be downloaded, processed, and stored in Redis. This will take a few minutes to spin up initially. From that point forward, the app should be quicker to load.

**Ask the app anything about the 2020 Summer Olympics...**
**NOW: Ask the app anything about the 2020 Summer Olympics!**
44 changes: 25 additions & 19 deletions app/app.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,32 @@
import os
import streamlit as st
import langchain

from urllib.error import URLError
from qna import answer_question_with_context
from dotenv import load_dotenv
load_dotenv()

from langchain.cache import RedisCache
from urllib.error import URLError
from qna import make_qna_chain


@st.cache_resource
def startup_qna_backend():
return make_qna_chain()


try:

default_prompt = ""
qna_chain = startup_qna_backend()
client = qna_chain.retriever.vectorstore.client
langchain.llm_cache = RedisCache(client)


default_question = ""
default_answer = ""

if 'question' not in st.session_state:
st.session_state['question'] = default_question
if 'prompt' not in st.session_state:
st.session_state['prompt'] = default_prompt
if 'response' not in st.session_state:
st.session_state['response'] = {
"choices" :[{
Expand All @@ -40,21 +51,16 @@
if question != st.session_state['question']:
st.session_state['question'] = question
with st.spinner("OpenAI and Redis are working to answer your question..."):
st.session_state['prompt'], st.session_state['response'] = answer_question_with_context(
question,
tokens_response=st.tokens_response,
temperature=st.temperature
)
result = qna_chain({"query": question})
print(result, flush=True)
# return result['source_documents'], result['result']
st.session_state['context'], st.session_state['response'] = result['source_documents'], result['result']
st.write("### Response")
st.write(f"Q: {question}")
st.write(f"A: {st.session_state['response']['choices'][0]['text']}")
with st.expander("Show Question and Answer Context"):
st.text(st.session_state['prompt'])
else:
st.write(f"Q: {st.session_state['question']}")
st.write(f"{st.session_state['response']['choices'][0]['text']}")
with st.expander("Question and Answer Context"):
st.text(st.session_state['prompt'].encode().decode())
st.write(f"{st.session_state['response']}")
with st.expander("Show Q&A Context Documents"):
if st.session_state['context']:
docs = "\n".join([doc.page_content for doc in st.session_state['context']])
st.text(docs)

st.markdown("____")
st.markdown("")
Expand Down
100 changes: 1 addition & 99 deletions app/qna/__init__.py
Original file line number Diff line number Diff line change
@@ -1,99 +1 @@

import typing as t

from . import db
from .models import (
MAX_SECTION_LEN,
SEPARATOR,
SEPARATOR_LEN,
get_embedding,
get_completion
)

redis_conn = db.init()
PROMPT_HEADER = """Answer the question as truthfully as possible using the provided context, and if the answer is not contained within the text below, say "I don't know."\n\nContext:\n"""

def search_semantic_redis(search_query: str, n: int) -> t.List[dict]:
"""
Search Redis using computed embeddings from OpenAI.
Args:
search_query (str): Text query to embed and use in document retrieval.
n (int): Number of documents to consider.
Returns:
list<dict>: List of relevant documents ordered by similarity score.
"""
embedding = get_embedding(text=search_query)
return db.search_redis(
redis_conn,
query_vector=embedding,
k=n,
return_fields=["title", "content", "tokens"]
)

def construct_prompt(question: str) -> str:
"""
Construct full prompt based on the input question using
the document sections indexed in Redis.
Args:
question (str): User input question.
pre_filter (str, optional): Pre filter to constrain the KNN search with conditions.
Returns:
str: Full prompt string to pass along to a generative language model.
"""
chosen_sections = []
chosen_sections_len = 0
chosen_sections_indexes = []

# Search for relevant document sections based on the question
most_relevant_document_sections = search_semantic_redis(question, n = 5)

# Iterate through results
for document_section in most_relevant_document_sections:
# Add contexts until we run out of token space
chosen_sections_len += int(document_section['tokens']) + SEPARATOR_LEN
if chosen_sections_len > MAX_SECTION_LEN:
break

chosen_sections.append(SEPARATOR + document_section['content'].replace("\n", " "))
chosen_sections_indexes.append(document_section['id'])

# Useful diagnostic information
print(f"Selected {len(chosen_sections)} document sections:")
print("\n".join(chosen_sections_indexes))

return PROMPT_HEADER + "".join(chosen_sections) + "\n\n Q: " + question + "\n A:"

def answer_question_with_context(
question: str,
show_prompt: bool = False,
explicit_prompt: str = "",
tokens_response=100,
temperature=0.0
) -> str:
"""
Answer the question.
Args:
question (str): Input question from the user.
show_prompt (bool, optional): Print out the prompt? Defaults to False.
explicit_prompt (str, optional): Use an explicit prompt provided by user? Defaults to "".
tokens_response (int, optional): Max number of tokens in the response. Defaults to 100.
temperature (float, optional): Model temperature. Defaults to 0.0.
Returns:
str: _description_
"""
if explicit_prompt == "":
# Construct prompt with Redis Vector Search
prompt = construct_prompt(question)
else:
prompt = f"{explicit_prompt}\n\n{question}"

if show_prompt:
print(prompt)

return get_completion(prompt, max_tokens=tokens_response, temperature=temperature)
from .llm import make_qna_chain
155 changes: 0 additions & 155 deletions app/qna/db.py

This file was deleted.

Loading

0 comments on commit cdcb494

Please sign in to comment.