# 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 ).
Your answer must be in the language at the end.
{{chat_history}}
{{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.