File size: 9,368 Bytes
0a81317
 
e764d84
 
 
 
0a81317
 
 
 
 
 
 
 
 
033375f
 
c2198d6
 
033375f
 
 
 
 
 
 
c2198d6
033375f
 
 
c2198d6
0a81317
c2198d6
 
 
 
 
033375f
 
 
 
c2198d6
033375f
 
 
c2198d6
 
033375f
 
 
0a81317
 
033375f
0a81317
033375f
 
c2198d6
0a81317
8907b38
033375f
 
c2198d6
 
 
033375f
0a81317
 
033375f
0a81317
 
033375f
0a81317
c2198d6
 
e367093
0a81317
 
c2198d6
 
033375f
 
 
0a81317
033375f
 
0a81317
033375f
 
 
0a81317
 
c2198d6
 
033375f
 
c2198d6
 
033375f
 
 
 
 
 
 
0a81317
 
033375f
c2198d6
0a81317
c2198d6
 
033375f
 
0a81317
c2198d6
033375f
 
 
 
c2198d6
 
0a81317
c2198d6
 
033375f
 
2a28b9c
033375f
 
c2198d6
033375f
 
 
 
0a81317
c2198d6
033375f
e764d84
033375f
0a81317
c2198d6
 
 
033375f
 
c2198d6
033375f
 
0a81317
c2198d6
033375f
 
0a81317
c2198d6
e764d84
033375f
0a81317
c2198d6
033375f
 
c2198d6
033375f
 
c2198d6
033375f
 
0a81317
 
c2198d6
033375f
 
 
0a81317
c2198d6
0a81317
 
 
c2198d6
0a81317
 
 
 
 
 
 
 
 
 
033375f
c2198d6
033375f
 
 
 
 
 
 
 
8907b38
033375f
 
 
e764d84
033375f
0a81317
 
 
 
 
 
e764d84
033375f
e764d84
 
033375f
0a81317
e764d84
033375f
0a81317
428a54e
0a81317
 
428a54e
c2198d6
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
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
import os
import logging
import gradio as gr
from transformers import pipeline
from sentence_transformers import SentenceTransformer, util
import PyPDF2

# Set up logging with immediate writing
logging.basicConfig(
    filename='support_bot_log.txt',
    level=logging.INFO,
    format='%(asctime)s - %(message)s',
    force=True  # Ensures any existing handlers are replaced and logging starts fresh
)
logger = logging.getLogger()

# Load models
qa_model = pipeline("question-answering", model="distilbert-base-uncased-distilled-squad") # Load the Hugging Face QA model for extracting answers from retrieved context.
embedder = SentenceTransformer('all-MiniLM-L6-v2') # Loading SentenceTransformer to convert text into vector embeddings for cosine similarity search.

# Helper function to extract text from PDF
def extract_text_from_pdf(file_path):
    text = ""
    with open(file_path, "rb") as file:
        pdf_reader = PyPDF2.PdfReader(file)
        for page in pdf_reader.pages:
            text += page.extract_text() + "\n" # Extract text from each page and concatenate.
    return text

# Find the most relevant section in the document

def find_relevant_section(query, sections, section_embeddings):
    """
    1. First, it performs a semantic search using cosine similarity.
    2. If the similarity score is below a threshold, it falls back to a keyword-based search.
    """
    
    stopwords = {"and", "the", "is", "for", "to", "a", "an", "of", "in", "on", "at", "with", "by", "it", "as", "so", "what"}
    
    # Semantic search
    query_embedding = embedder.encode(query, convert_to_tensor=True)
    similarities = util.cos_sim(query_embedding, section_embeddings)[0] # Compute cosine similarity between the query embedding and all section embeddings. 
    best_idx = similarities.argmax().item()
    best_section = sections[best_idx]
    similarity_score = similarities[best_idx].item()

    # Defining a threshold to determine if semantic search is confident enough.
    
    SIMILARITY_THRESHOLD = 0.4
    if similarity_score >= SIMILARITY_THRESHOLD:
        logger.info(f"Found relevant section using embeddings for query: {query}")
        return best_section
    
    logger.info(f"Low similarity ({similarity_score}). Falling back to keyword search.")
    
    # Keyword-based fallback search with stopword filtering
    
    query_words = {word for word in query.lower().split() if word not in stopwords}
    for section in sections:
        section_words = {word for word in section.lower().split() if word not in stopwords}
        common_words = query_words.intersection(section_words)

        # If at least two words match, return this section.
        
        if len(common_words) >= 2:
            logger.info(f"Keyword match found for query: {query} with common words: {common_words}")
            return section
    
    logger.info(f"No good keyword match found. Returning default fallback response.")
    return "I don’t have enough information to answer that."

def process_file(file, state):
    """Handles the uploaded file, processes its text, and prepares it for querying."""
    
    if file is None:
        logger.info("No file uploaded.")
        return [("Bot", "Please upload a file.")], state

    # Determine file type and extract text accordingly.
    
    file_path = file.name
    if file_path.lower().endswith(".pdf"):
        logger.info(f"Uploaded PDF file: {file_path}")
        text = extract_text_from_pdf(file_path)
    elif file_path.lower().endswith(".txt"):
        logger.info(f"Uploaded TXT file: {file_path}")
        with open(file_path, 'r', encoding='utf-8') as f:
            text = f.read()
    else:
        logger.error(f"Unsupported file format: {file_path}")
        return [("Bot", "Unsupported file format. Please upload a PDF or TXT file.")], state

    # Split document into sections and encode them into embeddings.
    sections = text.split('\n\n')
    section_embeddings = embedder.encode(sections, convert_to_tensor=True)

    # Store extracted text and embeddings in the chatbot's state dictionary.  
    state['document_text'] = text
    state['sections'] = sections
    state['section_embeddings'] = section_embeddings
    state['current_query'] = None
    state['feedback_count'] = 0
    state['mode'] = 'waiting_for_query'
    state['chat_history'] = [("Bot", "File processed. You can now ask questions.")]
    logger.info(f"Processed file: {file_path}")
    return state['chat_history'], state


def handle_input(user_input, state):
    """Processes user queries, fetches answers, and handles feedback loops."""

    if state['mode'] == 'waiting_for_upload':
        state['chat_history'].append(("Bot", "Please upload a file first."))
        logger.info("User attempted to interact without uploading a file.")
        
    elif state['mode'] == 'waiting_for_query':
        query = user_input
        state['current_query'] = query
        state['feedback_count'] = 0

        # Finding the best matching section.
        context = find_relevant_section(query, state['sections'], state['section_embeddings'])

        # Generating an answer using the QA model.
        if context == "I don’t have enough information to answer that.":
            answer = context
        else:
            result = qa_model(question=query, context=context)
            answer = result["answer"]
            
        state['last_answer'] = answer
        state['mode'] = 'waiting_for_feedback'
        state['chat_history'].append(("User", query))
        state['chat_history'].append(("Bot", f"Answer: {answer}\nPlease provide feedback: good, too vague, not helpful."))
        logger.info(f"Query: {query}, Answer: {answer}")
        
    elif state['mode'] == 'waiting_for_feedback':
        feedback = user_input.lower()
        state['chat_history'].append(("User", feedback))
        logger.info(f"Feedback: {feedback}")

        # Handling feedback responses.
        
        if feedback == "good" or state['feedback_count'] >= 2:
            state['mode'] = 'waiting_for_query'
            
            if feedback == "good":
                state['chat_history'].append(("Bot", "Thank you for your feedback. You can ask another question."))
                logger.info("Feedback accepted as 'good'. Waiting for next query.")
                
            else:
                state['chat_history'].append(("Bot", "Maximum feedback iterations reached. You can ask another question."))
                logger.info("Max feedback iterations reached. Waiting for next query.")
                
        else:
            query = state['current_query']
            context = find_relevant_section(query, state['sections'], state['section_embeddings'])
            
            if feedback == "too vague":
                adjusted_answer = f"{state['last_answer']}\n\n(More details:\n{context[:500]}...)"
                
            elif feedback == "not helpful":
                adjusted_answer = qa_model(question=query + " Please provide more detailed information with examples.", context=context)['answer']
                
            else:
                state['chat_history'].append(("Bot", "Please provide valid feedback: good, too vague, not helpful."))
                logger.info(f"Invalid feedback received: {feedback}")
                return state['chat_history'], state
                
            state['last_answer'] = adjusted_answer
            state['feedback_count'] += 1
            state['chat_history'].append(("Bot", f"Updated answer: {adjusted_answer}\nPlease provide feedback: good, too vague, not helpful."))
            logger.info(f"Adjusted answer: {adjusted_answer}")
            
    return state['chat_history'], state

# Function to return the up-to-date log file for download

def get_log_file():
    # Flush all log handlers to ensure log file is current
    for handler in logger.handlers:
        handler.flush()
    # Ensure the log file exists; if not, create an empty one.
    if not os.path.exists("support_bot_log.txt"):
        with open("support_bot_log.txt", "w", encoding="utf-8") as f:
            f.write("")
    logger.info("Log file downloaded by user.")
    return "support_bot_log.txt"

# Initial state setup
initial_state = {
    'document_text': None,
    'sections': None,
    'section_embeddings': None,
    'current_query': None,
    'feedback_count': 0,
    'mode': 'waiting_for_upload',
    'chat_history': [("Bot", "Please upload a PDF or TXT file to start.")],
    'last_answer': None
}

# Gradio interface
with gr.Blocks() as demo:
    state = gr.State(initial_state)
    
    with gr.Row():
        file_upload = gr.File(label="Upload PDF or TXT file")
        download_btn = gr.Button("Download Log")
        download_file = gr.File(label="Log File", interactive=False)
    
    chat = gr.Chatbot()
    user_input = gr.Textbox(label="Your query or feedback")
    submit_btn = gr.Button("Submit")

    # Process file upload
    file_upload.upload(process_file, inputs=[file_upload, state], outputs=[chat, state])

    # Handle user input and clear the textbox
    submit_btn.click(handle_input, inputs=[user_input, state], outputs=[chat, state]).then(lambda: "", None, user_input)
    
    # Set up download log button
    download_btn.click(fn=get_log_file, inputs=[], outputs=download_file)

demo.launch(share=True)