如何像ChatGPT一样通过Langchain导航到之前的聊天记录?
我正在开发一个基于RAG的ChatPDF应用。用户可以上传PDF文件来生成嵌入(也就是把信息转化为计算机可以理解的形式),这些嵌入会存储在一个向量数据库里(我用的是Chromadb)。现在基本的聊天功能都正常,用户可以提问,模型也能记住上下文,直到用户结束当前会话。一旦会话关闭,模型就不记得任何东西了。我用MongoDB来存储聊天记录,聊天信息都能正常存入数据库。但不知怎么的,一旦会话关闭并重新开始,模型就忘记之前的上下文了。
我想要的是一种方法,让用户能够回到之前的聊天记录(就像ChatGPT那样),并且模型应该能记住在那个窗口里讨论的内容。
编辑:我会分享整个脚本(根据要求提供的最小可运行版本)。这实际上是一个API,所以我会相应地分享代码。
import os
import shutil
from datetime import datetime
from dotenv import dotenv_values
from collections import namedtuple
import uuid
from fastapi import File, UploadFile, APIRouter, Form, status
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_community.document_loaders import PyPDFLoader
from langchain.text_splitter import CharacterTextSplitter
from langchain_community.vectorstores import Chroma
from langchain.chains import ConversationalRetrievalChain, RetrievalQA, LLMChain
from langchain.chains.question_answering import load_qa_chain
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain.prompts import PromptTemplate
from langchain_mongodb import MongoDBChatMessageHistory
import chromadb
from chromadb.utils import embedding_functions
config = dotenv_values(".env")
chat_router = APIRouter()
Constants = namedtuple('Constants', ['OPEN_API_KEY', 'EMBEDDINGS_MODEL', 'CHAT_MODEL', 'MONGO_CONNECTION_STRING'])
configs = Constants(config["OPENAI_API_KEY"], config["EMBEDDINGS_MODEL"], config["CHAT_MODEL"], config['MONGO_CONNECTION_STRING'])
@chat_router.post("/trainpdf/", status_code=status.HTTP_201_CREATED)
async def create_upload_file(user_id: str = Form(...), pdf_file: UploadFile = File(...)):
if not pdf_file.filename.endswith(".pdf"):
return {"code": "400", "answer": "Only PDF files are allowed."}
client = chromadb.PersistentClient(path="./trained_db")
collection = client.get_or_create_collection("PDF_Embeddings")
vectordb = Chroma(persist_directory="./trained_db", collection_name = collection.name, client = client)
if check_for_existing_embeddings(pdf_file.filename, vectordb):
return {"code": "400", "answer": "PDF EMBEDDINGS HAVE ALREADY BEEN GENERATED FOR THIS FILE. PLEASE PROVIDE A NEW FILE."}
pdf_folder_path = f"Training_Data"
os.makedirs(pdf_folder_path, exist_ok=True)
file_path = os.path.join(pdf_folder_path, pdf_file.filename)
with open(file_path, "wb") as temp_dest_file:
temp_dest_file.write(await pdf_file.read())
docs = read_docs(file_path, user_id)
vectordb = generate_and_store_embeddings(docs, pdf_file, user_id)
shutil.rmtree(pdf_folder_path, ignore_errors=True)
if vectordb is None:
return {"code": "400", "answer": "Error Occurred during Data Extraction from Pdf."}
return {"code": "201", "answer": "PDF EMBEDDINGS GENERATED SUCCESSFULLY"}
@chat_router.post("/chatpdf/", status_code=status.HTTP_200_OK)
async def pdf_chat(query_params: dict):
user_id: str = query_params.get('user_id')
query: str = query_params.get('query')
session_id: str = user_id + "-" + datetime.now().strftime("%d/%m/%Y")
embeddings = OpenAIEmbeddings(openai_api_key=configs.OPEN_API_KEY)
client = chromadb.PersistentClient(path="./trained_db")
collection = client.get_or_create_collection("PDF_Embeddings", embedding_function=embedding_functions.OpenAIEmbeddingFunction(api_key=config["OPENAI_API_KEY"], model_name=configs.EMBEDDINGS_MODEL))
vectordb = Chroma(persist_directory="./trained_db", embedding_function=embeddings, collection_name = collection.name)
"""Retrieve the documents relevant to the query and generate the response."""
retriever = vectordb.as_retriever(search_type="mmr")
relevant_docs = retriever.get_relevant_documents(query)
user_specific_chat_memory = get_message_history(session_id)
"""Now I am going about adding chat history into two ways. Both have their share of problems.
1. Adding chat history to the prompt template. This method takes in chat history as context. But it returns the error:
ValueError: Missing some input keys: {'context'}
Note that the error is returned once the user asks a second question after the chat model responds to the first one.
"""
prompt_template = f"""You are engaged in conversation with a human,
your responses will be generated using a comprehensive long document as a contextual reference.
You can summarize long documents and also provide comprehensive answers, depending on what the user has asked.
You also take context in consideration and answer based on chat history.
Chat History: {{context}}
Question: {{question}}
Answer :
"""
PROMPT = PromptTemplate(template=prompt_template, input_variables=["context", "question"])
model = configs.CHAT_MODEL
streaming_llm = ChatOpenAI(openai_api_key=configs.OPEN_API_KEY, model = model, temperature = 0.1, streaming=True)
# use the streaming LLM to create a question answering chain
qa_chain = load_qa_chain(
llm=streaming_llm,
chain_type="stuff",
prompt=PROMPT
)
question_generator_chain = LLMChain(llm=streaming_llm, prompt=PROMPT)
qa_chain_with_history = ConversationalRetrievalChain(
retriever = vectordb.as_retriever(search_kwargs={'k': 3}, search_type='mmr'),
combine_docs_chain=qa_chain,
question_generator=question_generator_chain
)
response = qa_chain_with_history(
{"question": query, "chat_history": user_specific_chat_memory.messages}
)
user_specific_chat_memory.add_user_message(response["question"])
user_specific_chat_memory.add_ai_message(response["answer"])
#return {"code": "200", "answer": response["answer"]}
"""2. Adding chat history to the memory. This saves the memory in a buffer which is passed to the retrieval chain.
But it forgets the entire context of the conversation once the session restarts (even though messages are being added to MongoDB).
"""
memory = ConversationBufferMemory(
memory_key="chat_history",
chat_memory=user_specific_chat_memory,
output_key="answer",
return_messages=True
)
qa_chain_with_history = ConversationalRetrievalChain.from_llm(
ChatOpenAI(openai_api_key = config["OPENAI_API_KEY"], model_name = model, temperature = 0.1),
retriever = vectordb.as_retriever(search_kwargs={'k': 3}, search_type='mmr'),
memory = memory,
chain_type="stuff"
)
result = qa_chain_with_history.invoke({'question': query})
user_specific_chat_memory.add_user_message(result["question"])
user_specific_chat_memory.add_ai_message(result["answer"])
return {"code": "200", "answer": result["answer"]}
def read_docs(pdf_file, user_id: str):
pdf_loader = PyPDFLoader(pdf_file)
pdf_documents = pdf_loader.load()
text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
documents = text_splitter.split_documents(pdf_documents)
now = datetime.now()
for doc in documents:
doc.metadata = {
"user": user_id,
"id": str(uuid.uuid4()),
"source": pdf_file.split("\\")[-1],
'created_at': now.strftime("%d/%m/%Y %H:%M:%S")
}
return documents
def generate_and_store_embeddings(documents, pdf_file, user_id):
client = chromadb.PersistentClient(path="./trained_db")
collection = client.get_or_create_collection("PDF_Embeddings",
embedding_function=embedding_functions.OpenAIEmbeddingFunction(api_key=config["OPENAI_API_KEY"],
model_name=configs.EMBEDDINGS_MODEL))
try:
vectordb = Chroma.from_documents(
documents,
embedding=OpenAIEmbeddings(openai_api_key=config["OPENAI_API_KEY"], model=configs.EMBEDDINGS_MODEL),
persist_directory='./trained_db',
collection_name = collection.name,
client = client
)
vectordb.persist()
data_associated_with_ids = vectordb.get(where={"source":
pdf_file.filename})
except Exception as err:
return None
return vectordb
def check_for_existing_embeddings(pdf_filename, vectordb):
doc_metadatas: list = vectordb.get(include=['metadatas'])['metadatas']
results = [doc['source'].split("\\")[-1] for doc in doc_metadatas]
if pdf_filename in list(set(results)):
return True
def get_message_history(session_id: str) -> MongoDBChatMessageHistory:
return MongoDBChatMessageHistory(connection_string=configs.MONGO_CONNECTION_STRING,
session_id=session_id,
collection_name="Chat_History")
在pdf_chat函数中,我尝试了两种方法来加入聊天历史。第二种方法在会话重启后会忘记上下文(即使是同一个用户),而第一种方法在用户对模型的第一次回答进行后续提问时会返回错误(而不是用户第一次问模型的问题)。如果其中任何一种方法能正常工作,记住会话结束前的聊天内容,以及特定对话的日期信息(也就是有点像ChatGPT),我会非常满意。
ValueError: 缺少一些输入键:{'context'}
0 个回答
暂无回答