Spaces:
Sleeping
Sleeping
File size: 7,238 Bytes
ebc1af9 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 | # chat_logic.py
import os
import re
import warnings
from pathlib import Path
from typing import Any, Tuple, Optional, Dict
# Langchain/OpenAI imports
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain_core.prompts import PromptTemplate, ChatPromptTemplate
from langchain_classic.chains import ConversationalRetrievalChain
from langchain_classic.memory import ConversationBufferMemory, ConversationSummaryBufferMemory
from langchain_community.document_loaders import PyPDFLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter, CharacterTextSplitter
from langchain_community.vectorstores import Chroma
from langchain_community.document_transformers import EmbeddingsRedundantFilter, LongContextReorder
from langchain_classic.retrievers.document_compressors import DocumentCompressorPipeline
from langchain_classic.retrievers.document_compressors import EmbeddingsFilter
from langchain_classic.retrievers import ContextualCompressionRetriever
from langchain_text_splitters import TextSplitter
from langchain_core.retrievers import BaseRetriever
from langchain_core.language_models import BaseChatModel
# --- Constants & Helpers ---
LOCAL_VECTOR_STORE_DIR = Path(__file__).resolve().parent.joinpath("data", "vector_stores")
# !!! SET YOUR DEFAULT PDF PATH HERE !!!
# Assuming the default PDF is in the same directory as this script.
DEFAULT_PDF_PATH = Path(__file__).resolve().parent.joinpath("S:\\ano_dec_pro\\AnomalyDetectionCVPR2018-Pytorch\\ring_chat_bot\\Ring_App_Documentation.pdf")
DEFAULT_VECTORSTORE_NAME = "default_pdf_db"
OPENAI_KEY = os.getenv("OPENAI_API_KEY")
def ensure_dir(p: Path) -> None:
p.mkdir(parents=True, exist_ok=True)
def load_default_pdf():
# Attempt to find the default PDF in the script directory
if not DEFAULT_PDF_PATH.exists():
raise FileNotFoundError(
f"Default PDF not found: {DEFAULT_PDF_PATH}. Please place your PDF here or update the path in chat_logic.py"
)
loader = PyPDFLoader(DEFAULT_PDF_PATH.as_posix())
return loader.load()
def split_documents(docs, chunk_size: int = 1600, chunk_overlap: int = 200):
splitter = RecursiveCharacterTextSplitter(chunk_size=chunk_size, chunk_overlap=chunk_overlap)
return splitter.split_documents(docs)
def select_embeddings(openai_key: str | None) -> OpenAIEmbeddings:
if not openai_key:
raise ValueError("OPENAI_API_KEY is required.")
return OpenAIEmbeddings(api_key=openai_key)
# --- Core RAG Components ---
def vectorstore_backed_retriever(vs: Chroma, search_type: str = "similarity", k: int = 16, score_threshold: float | None = None) -> BaseRetriever:
kwargs = {}
if k is not None:
kwargs["k"] = k
if score_threshold is not None:
kwargs["score_threshold"] = score_threshold
return vs.as_retriever(search_type=search_type, search_kwargs=kwargs)
def make_compression_retriever(embeddings: OpenAIEmbeddings, base_retriever: BaseRetriever, chunk_size: int = 500, k: int = 16, similarity_threshold: float | None = None) -> ContextualCompressionRetriever:
splitter: TextSplitter = CharacterTextSplitter(chunk_size=chunk_size, chunk_overlap=0, separator=". ")
redundant_filter = EmbeddingsRedundantFilter(embeddings=embeddings)
relevant_filter = EmbeddingsFilter(embeddings=embeddings, k=k, similarity_threshold=similarity_threshold)
reordering = LongContextReorder()
pipeline = DocumentCompressorPipeline(transformers=[splitter, redundant_filter, relevant_filter, reordering])
return ContextualCompressionRetriever(base_compressor=pipeline, base_retriever=base_retriever)
def make_memory(model_name: str, openai_key: str | None):
# Simplified memory logic for Streamlit
return ConversationSummaryBufferMemory(
max_token_limit=1024,
llm=ChatOpenAI(model_name="gpt-3.5-turbo", openai_api_key=openai_key, temperature=0.1),
return_messages=True,
memory_key="chat_history",
output_key="answer",
input_key="question",
)
def answer_template(language: str = "english") -> str:
return f"""Answer the question at the end, using only the following context (delimited by <context></context>).
Your answer must be in the language at the end.
<context>
{{chat_history}}
{{context}}
</context>
Question: {{question}}
Language: {language}.
"""
def build_chain(model: str, retriever: BaseRetriever, openai_key: str | None) -> Tuple[ConversationalRetrievalChain, Any]:
condense_question_prompt = PromptTemplate(
input_variables=["chat_history", "question"],
template=(
"Given the following conversation and a follow up question, rephrase the follow up question to be a standalone question, in its original language.\n\nChat History:\n{chat_history}\n\nFollow Up Input: {question}\n\nStandalone question:"
),
)
answer_prompt = ChatPromptTemplate.from_template(answer_template(language="english"))
memory = make_memory(model, openai_key)
standalone_llm = ChatOpenAI(api_key=openai_key, model=model, temperature=0.1)
response_llm = ChatOpenAI(api_key=openai_key, model=model, temperature=0.5, top_p=0.95)
chain = ConversationalRetrievalChain.from_llm(
condense_question_prompt=condense_question_prompt,
combine_docs_chain_kwargs={"prompt": answer_prompt},
condense_question_llm=standalone_llm,
llm=response_llm,
memory=memory,
retriever=retriever,
chain_type="stuff",
verbose=False,
return_source_documents=True,
)
return chain, memory
def setup_default_rag(openai_key: str, model_name: str = "gpt-4-turbo") -> Tuple[ConversationalRetrievalChain, Any]:
"""
Sets up the RAG chain using the default hardcoded PDF file.
This replaces the file upload functionality for the initial setup.
"""
vectorstore_path = LOCAL_VECTOR_STORE_DIR.joinpath(DEFAULT_VECTORSTORE_NAME)
ensure_dir(vectorstore_path)
embeddings = select_embeddings(openai_key)
# Check if the vector store already exists locally (persistence logic)
if not any(vectorstore_path.iterdir()):
# 1. Load and split the default PDF
docs = load_default_pdf()
chunks = split_documents(docs)
# 2. Create and persist the Vector Store (Chroma)
vs = Chroma.from_documents(
documents=chunks,
embedding=embeddings,
persist_directory=vectorstore_path.as_posix()
)
vs.persist()
else:
# 3. Load the existing Vector Store
vs = Chroma(embedding_function=embeddings, persist_directory=vectorstore_path.as_posix())
# 4. Create Retriever
base_retriever = vectorstore_backed_retriever(vs)
retriever = make_compression_retriever(embeddings=embeddings, base_retriever=base_retriever)
# 5. Build and return chain
chain, memory = build_chain(model_name, retriever, openai_key)
return chain, memory
# The process_uploaded_file function is removed as we are hardcoding the default file setup. |