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.