File size: 6,921 Bytes
8f07f94
a08d304
a546f7a
f62f25d
3b468ed
8f2cddf
 
3b468ed
8f2cddf
 
3b468ed
 
50356ed
8f07f94
8f2cddf
3b468ed
8f07f94
50356ed
 
 
 
 
 
8f2cddf
50356ed
 
 
 
f62f25d
50356ed
f62f25d
50356ed
3c8c9ec
50356ed
 
f62f25d
50356ed
f62f25d
 
 
 
50356ed
 
b4f4511
50356ed
8f2cddf
 
 
8cf9ff5
50356ed
8cf9ff5
8f2cddf
8cf9ff5
8f2cddf
 
8cf9ff5
8f2cddf
 
3c8c9ec
50356ed
 
8f2cddf
 
50356ed
8f2cddf
 
8f07f94
f62f25d
 
50356ed
 
 
8f07f94
50356ed
 
f62f25d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8f07f94
 
a294dd2
f62f25d
a294dd2
50356ed
f62f25d
50356ed
 
fc1ab54
50356ed
8f07f94
50356ed
 
 
8f07f94
 
552deeb
fc1ab54
50356ed
552deeb
 
 
f62f25d
552deeb
50356ed
f62f25d
 
 
 
50356ed
 
 
 
 
 
 
552deeb
50356ed
 
 
 
 
 
 
 
fc1ab54
50356ed
 
 
 
 
 
b1805d6
8f07f94
50356ed
 
 
 
8f07f94
 
3b468ed
50356ed
7fabd5f
50356ed
7fabd5f
 
3b468ed
7fabd5f
50356ed
 
 
f62f25d
 
50356ed
7fabd5f
50356ed
 
 
 
 
7fabd5f
 
 
d4f700d
50356ed
 
 
 
 
 
 
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
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
import os
import tempfile
import logging
from typing import List, Dict
from langchain.embeddings import OpenAIEmbeddings
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.vectorstores import Chroma
from langchain.chains import ConversationalRetrievalChain
from langchain.document_loaders import PyPDFLoader
from langchain.chat_models import ChatOpenAI
from langchain.prompts import ChatPromptTemplate, SystemMessagePromptTemplate, HumanMessagePromptTemplate
from langchain.schema import Document
from langchain.memory import ConversationBufferMemory

import chainlit as cl
from chainlit.types import AskFileResponse

# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

# Set Chainlit config directory
os.environ["CHAINLIT_CONFIG_DIR"] = tempfile.gettempdir()

# App configuration
APP_CONFIG = {
    "model_name": "gpt-4o",
    "temperature": 0,
    "chunk_size": 2000,  # Increased for book-length content
    "chunk_overlap": 200,
    "max_file_size_mb": 100,  # Increased for larger books
    "max_output_tokens": 4096,
    "embedding_model": "text-embedding-3-large"
}

# Initialize text splitter with more sophisticated settings
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=APP_CONFIG["chunk_size"],
    chunk_overlap=APP_CONFIG["chunk_overlap"],
    length_function=len,
    separators=["\n\n", "\n", " ", ""]  # Prioritize splitting on paragraph breaks
)

# System template
system_template = """You are GPT-4o, an advanced AI model with multimodal capabilities and high intelligence. Use the following pieces of context to answer the user's question.
If you don't know the answer, just say that you don't know, don't try to make up an answer.
ALWAYS return a "SOURCES" part in your answer.
The "SOURCES" part should be a reference to the source of the document from which you got your answer.

If the user greets with phrases like Hi, hello, How are you, etc., reply accordingly as well.

Example of your response should be:

The answer is foo
SOURCES: xyz

Begin!
----------------
{context}"""

# Create prompt template
messages = [
    SystemMessagePromptTemplate.from_template(system_template),
    HumanMessagePromptTemplate.from_template("{question}")
]
prompt = ChatPromptTemplate.from_messages(messages)

def process_file(file: AskFileResponse) -> List[Dict[str, str]]:
    """Process uploaded PDF file and return list of text chunks with metadata."""
    with tempfile.NamedTemporaryFile(mode="wb", delete=False, suffix=".pdf") as temp_file:
        temp_file.write(file.content)
        temp_file_path = temp_file.name

    loader = PyPDFLoader(temp_file_path)
    pages = loader.load_and_split()
    
    chunks_with_metadata = []
    for i, page in enumerate(pages):
        chunks = text_splitter.split_text(page.page_content)
        for j, chunk in enumerate(chunks):
            chunks_with_metadata.append({
                "content": chunk,
                "metadata": {
                    "page": i + 1,
                    "chunk": j + 1,
                    "source": f"page_{i+1}_chunk_{j+1}"
                }
            })
    
    return chunks_with_metadata

@cl.on_chat_start
async def start():
    await cl.Message(f"Welcome! Chat session started using {APP_CONFIG['model_name']} for text generation and {APP_CONFIG['embedding_model']} for embeddings. This combination is optimized for processing and querying book-length content. You can now upload a PDF of a book to begin!").send()
    
    files = await cl.AskFileMessage(
        content="Please upload a PDF file of a book to begin!",
        accept=["application/pdf"],
        max_size_mb=APP_CONFIG["max_file_size_mb"],
        timeout=3600,
    ).send()

    if not files:
        await cl.Message("No file uploaded. Please try again.").send()
        return

    file = files[0]
    await cl.Message(f"Starting to process `{file.name}`. This may take a few minutes...").send()

    try:
        # Process the file
        await cl.Message("Reading and splitting the document...").send()
        chunks_with_metadata = process_file(file)
        
        await cl.Message("Creating embeddings and setting up the retrieval system...").send()
        embeddings = OpenAIEmbeddings(model=APP_CONFIG["embedding_model"])
        docsearch = await cl.make_async(Chroma.from_documents)(
            [Document(page_content=chunk["content"], metadata=chunk["metadata"]) for chunk in chunks_with_metadata],
            embeddings
        )

        memory = ConversationBufferMemory(
            memory_key="chat_history",
            output_key="answer",
            return_messages=True,
        )

        await cl.Message("Finalizing the chat model...").send()
        chain = ConversationalRetrievalChain.from_llm(
            ChatOpenAI(
                model_name=APP_CONFIG["model_name"],
                temperature=APP_CONFIG["temperature"],
                max_tokens=APP_CONFIG["max_output_tokens"],
                streaming=True
            ),
            chain_type="stuff",
            retriever=docsearch.as_retriever(search_kwargs={"k": 3}),
            memory=memory,
            return_source_documents=True,
            combine_docs_chain_kwargs={"prompt": prompt}
        )

        cl.user_session.set("chain", chain)
        await cl.Message(content=f"Processing `{file.name}` complete. You can now ask questions about the book!").send()

    except Exception as e:
        error_msg = f"An error occurred while processing the file: {str(e)}"
        logger.error(error_msg)
        await cl.Message(content=error_msg).send()

@cl.on_message
async def main(message: cl.Message):
    """Handle user messages and generate responses."""
    try:
        chain = cl.user_session.get("chain")
        cb = cl.AsyncLangchainCallbackHandler()
        
        res = await chain.acall(message.content, callbacks=[cb])
        answer = res["answer"]
        source_documents = res["source_documents"]
        
        text_elements = []
        for source_doc in source_documents:
            source_name = f"Page {source_doc.metadata['page']}, Chunk {source_doc.metadata['chunk']}"
            text_elements.append(cl.Text(content=source_doc.page_content, name=source_name))
        
        source_names = [text_el.name for text_el in text_elements]
        if source_names:
            answer += f"\nSources: {', '.join(source_names)}"
        else:
            answer += "\nNo sources found"
        
        await cl.Message(content=answer, elements=text_elements).send()
    
    except Exception as e:
        error_msg = f"An error occurred: {str(e)}"
        logger.error(error_msg)
        await cl.Message(content=error_msg).send()

if __name__ == "__main__":
    logger.info(f"Starting Chainlit application with model: {APP_CONFIG['model_name']} and embedding model: {APP_CONFIG['embedding_model']}")
    cl.run()