File size: 16,027 Bytes
278dd0a
 
d8d147e
278dd0a
d8d147e
278dd0a
 
4cfe125
278dd0a
 
 
4cfe125
2a993af
 
 
d8d147e
 
 
4cfe125
 
d8d147e
4cfe125
 
 
 
 
 
 
 
2a993af
d8d147e
 
 
d09d051
d8d147e
278dd0a
 
 
 
036a914
278dd0a
 
 
 
 
 
d8d147e
 
278dd0a
 
 
 
 
 
 
 
d8d147e
278dd0a
 
5be49c1
 
 
 
 
278dd0a
5be49c1
278dd0a
 
 
 
 
d8d147e
 
278dd0a
 
 
 
d8d147e
278dd0a
 
d8d147e
 
278dd0a
2a993af
278dd0a
 
 
d8d147e
 
278dd0a
 
d8d147e
278dd0a
 
 
 
 
 
 
 
 
 
d8d147e
278dd0a
 
4cfe125
 
 
 
 
 
 
 
 
d8d147e
 
 
 
 
 
 
 
4cfe125
d8d147e
a8595ef
d8d147e
4cfe125
d8d147e
 
 
 
 
4cfe125
d8d147e
278dd0a
d8d147e
278dd0a
d8d147e
 
 
278dd0a
d8d147e
 
2329218
2a993af
d8d147e
 
2329218
4cfe125
278dd0a
2329218
 
278dd0a
d8d147e
 
278dd0a
 
 
d8d147e
 
 
 
 
 
 
 
 
 
 
 
 
e8a6d88
278dd0a
d8d147e
 
 
 
 
 
278dd0a
 
 
 
 
d09d051
278dd0a
 
d8d147e
278dd0a
 
 
 
4cfe125
 
d8d147e
d09d051
4cfe125
d8d147e
4cfe125
 
2329218
4cfe125
278dd0a
d8d147e
4cfe125
 
 
 
d8d147e
 
 
 
 
 
 
 
 
 
 
 
4cfe125
 
 
 
 
d09d051
d8d147e
4cfe125
 
 
 
 
d8d147e
4cfe125
 
d8d147e
4cfe125
 
 
 
 
 
278dd0a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2a993af
5be49c1
 
 
 
2a993af
5de3b29
278dd0a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d8d147e
278dd0a
 
 
 
 
 
 
 
d8d147e
 
 
 
 
 
 
 
 
 
 
a8595ef
d8d147e
 
 
 
 
 
 
 
 
 
2a993af
d8d147e
 
 
 
 
 
 
 
 
 
 
 
 
 
278dd0a
 
 
 
 
 
 
 
 
 
d8d147e
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
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
import streamlit as st
import streamlit_chat
import json
import os
from pymongo import MongoClient
from bson import ObjectId
from dotenv import load_dotenv
import pinecone
from langchain_google_genai import GoogleGenerativeAIEmbeddings
from langchain_google_genai import ChatGoogleGenerativeAI
from langchain_core.prompts import ChatPromptTemplate
import re

st.set_page_config(layout="wide", page_title="IOCL Chatbot", page_icon="πŸ“„")
load_dotenv()
import logging
from pytz import timezone, utc
from datetime import datetime

logging.basicConfig(
    level=logging.DEBUG,  # This is for your application logs
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    datefmt='%Y-%m-%d %H:%M:%S'
)

# Suppress pymongo debug logs by setting the pymongo logger to a higher level
pymongo_logger = logging.getLogger('pymongo')
pymongo_logger.setLevel(logging.WARNING)

PINECONE_API = os.getenv("PINECONE_API_KEY")
pc = pinecone.Pinecone(
    api_key=PINECONE_API
)
index_name = "iocl2"
index = pc.Index(index_name)
# MongoDB connection setup
MONGO_URI = os.getenv("MONGO_URI")
client = MongoClient(MONGO_URI)
db = client["chatbot_db"]
chat_sessions = db["chat_sessions3"]

# Set LLM models
FLASH_API = os.getenv("FLASH_API")
embeddings = GoogleGenerativeAIEmbeddings(model="models/embedding-001", google_api_key=FLASH_API)
llm = ChatGoogleGenerativeAI(model="gemini-1.5-flash", temperature=0, max_tokens=None, google_api_key=FLASH_API)

# Load the extracted JSON data

# Initialize session state for current chat session
if 'current_chat_id' not in st.session_state:
    st.session_state['current_chat_id'] = None
if 'chat_history' not in st.session_state:
    st.session_state['chat_history'] = []
if 'regenerate' not in st.session_state:
    st.session_state['regenerate'] = False  # Track regenerate button state


# Function to create a new chat session in MongoDB
def create_new_chat_session():
    # Get the current time in IST
    ind_time = datetime.now(timezone("Asia/Kolkata"))
    # Convert IST time to UTC for storing in MongoDB
    utc_time = ind_time.astimezone(utc)

    new_session = {
        "created_at": utc_time,  # Store in UTC
        "messages": []  # Empty at first
    }
    session_id = chat_sessions.insert_one(new_session).inserted_id
    return str(session_id)


# Function to load a chat session by MongoDB ID
# Function to load the chat session by MongoDB ID (load full history for display)
def load_chat_session(session_id):
    session = chat_sessions.find_one({"_id": ObjectId(session_id)})
    if session:
        # Load the full chat history (no slicing here)
        st.session_state['chat_history'] = session['messages']


# Function to update chat session in MongoDB (store last 15 question-answer pairs)
# Function to update chat session in MongoDB (store entire chat history)
def update_chat_session(session_id, question, answer, improved_question):
    # Append the new question-answer pair to the full messages array
    chat_sessions.update_one(
        {"_id": ObjectId(session_id)},
        {"$push": {
            "messages": {"$each": [{"question": question, 'improved_question': improved_question, "answer": answer}]}}}
    )


# Function to replace the last response in MongoDB
def replace_last_response_in_mongo(session_id, new_answer):
    last_message_index = len(st.session_state['chat_history']) - 1
    if last_message_index >= 0:
        # Replace the last response in MongoDB
        chat_sessions.update_one(
            {"_id": ObjectId(session_id)},
            {"$set": {f"messages.{last_message_index}.answer": new_answer}}
        )


# Function to regenerate the response
def regenerate_response():
    try:
        if st.session_state['chat_history']:
            last_question = st.session_state['chat_history'][-1]["question"]  # Get the last question
            # Exclude the last response from the history when sending the question to LLM
            previous_history = st.session_state['chat_history'][:-1]  # Exclude the last Q&A pair

            with st.spinner("Please wait, regenerating the response!"):
                # Generate a new response for the last question using only the previous history

                query = get_context_from_messages(last_question, previous_history)
                if query:
                    logging.info(f"Extracted query is :{query}\n")
                    extracted_query = get_query_from_llm_answer(query)
                    if extracted_query:
                        query = extracted_query
                    else:
                        query = last_question

                    query_embedding = embeddings.embed_query(query)
                    search_results = index.query(vector=query_embedding, top_k=10, include_metadata=True)
                    matches = search_results['matches']

                    content = ""
                    for i, match in enumerate(matches):
                        chunk = match['metadata']['chunk']
                        url = match['metadata']['url']
                        content += f"chunk{i}: {chunk}\n" + f"url{i}: {url}\n"

                    new_reply = generate_summary(content, query, previous_history)

                    st.session_state['chat_history'][-1]["answer"] = new_reply

                    # Update MongoDB with the new response
                    if st.session_state['current_chat_id']:
                        replace_last_response_in_mongo(st.session_state['current_chat_id'], new_reply)

                    st.session_state['regenerate'] = False  # Reset regenerate flag
                    st.rerun()

    except Exception as e:
        st.error("Error occured in Regenerating response, please try again later.")


def generate_summary(chunks, query, chat_history):
    try:
        # Limit the history sent to the LLM to the latest 3 question-answer pairs
        limited_history = chat_history[-3:] if len(chat_history) > 3 else chat_history

        # Create conversation history for the LLM, only using the last 15 entries
        history_text = "\n".join([f"User: {q['improved_question']}\nLLM: {q['answer']}" for q in limited_history])

        # Define the system and user prompts including the limited history
        prompt = ChatPromptTemplate.from_messages([
            ("system", """You are a chatbot specializing in answering queries related to Indian Oil Corporation Limited (IOCL). You will be provided with chunks of data from the IOCL website to answer user queries. Each chunk will include associated URLs, You must give the url of the chunks which you are using to answer the query.
                            Key Guidelines:
                            1.If the user query is not clear, or you think multiple answers are possbile, you should ask for clarification with proper reasoning.
                            2.Do not mention chunk name in any of your replies.
                            2.Detailed and Clear: Provide thorough, clear, and concise responses without omitting relevant information from the data chunks.
                            3.Natural Summarization: When answering, you must not  directly quote chunk names,formats. Instead, summarize or interpret the data naturally and conversationally.
                            4.Use Conversation History: Refer back to the conversation history to maintain consistency and build on prior responses, if applicable.
                            5.Ignore Unanswered Queries: If the conversation history contains previous responses like "The answer is not available in the context," disregard them when formulating your current response.
                            6.Graceful Handling of General Queries: If a user sends greetings, introduction, salutations, or unrelated questions, respond appropriately and conversationally.
                            7.Include Source URLs: Always include the URLs from the relevant chunks of data that you're using to answer the query.
                            8.Thoroughly looks for answer to the query in the provided chunks before replying, if you feel the query is irrelevant or answer is not present then you can ask user to clarify or tell that it cannot be answered.
                            9.Sometimes chunks might contain very less data still use it if its relevant.
                            """),

            ("human", f'''
                            "Query":\n {query}\n
                            Below are the pinecone chunks that should be used to answer the user query:
                            "Extracted Data": \n{chunks}\n
                            Below is the previous conversation history:
                            "Previous Conversation History": \n{history_text}\n
                            '''
             )
        ])

        # Chain the prompt with LLM for response generation
        chain = prompt | llm
        result = chain.invoke({"Query": query, "Extracted Data": chunks, "Previous Conversation History": history_text})

        # Return the generated response
        logging.info(f"LLM answer is :{result}")
        return result.content

    except Exception as e:
        st.error(f"Error answering your question: {e}")
        return None


def get_context_from_messages(query, chat_history):
    try:

        logging.info(f"Getting context from original query: {query}")

        # Limit the history sent to the LLM to the latest 3 question-answer pairs
        limited_history = chat_history[-3:] if len(chat_history) > 3 else chat_history

        # Create conversation history for the LLM, only using the last 15 entries
        history_text = "\n".join([f"User: {q['question']}\nLLM: {q['answer']}" for q in limited_history])

        # Define the system and user prompts including the limited history
        prompt = ChatPromptTemplate.from_messages([
            ("system", """"I will provide you with a user query and up to the last 3 messages from the chat history which includes both questions and answers.Your task is to understand  the user query nicely and restructure it if required such that  it  makes complete sense and is completely self contained.
                           The provided queries are related to Indian Oil Corporation limited (IOCL).
                           1. If the query is a follow-up, use the provided chat history to reconstruct a well-defined, contextually complete query that can stand alone."
                           2. if the query is self contained, if applicable try to improve it to make is coherent.
                           3. if the user query is salutations, greetings or not relevant in that case give the query back as it is.
                           I have provided an output format below, stricly follow it. Do not give anything else other than just the output.
                           expected_output_format: "query: String or None"
                           """),
            ("human", f'''  
                            "Query":\n {query}\n
                            "Previous Conversation History": \n{history_text}\n
                            '''
             )
        ])

        # Chain the prompt with LLM for response generation
        chain = prompt | llm
        result = chain.invoke({"Query": query, "Previous Conversation History": history_text})
        logging.info(f"llm answer for query extraction is :{result}")

        # Return the generated response
        return result.content

    except Exception as e:
        logging.error(f"exception occured in getting query from original query :{e}")
        return None


def get_query_from_llm_answer(llm_output):
    match = re.search(r'query:\s*(.*)', llm_output)
    if match:
        query = match.group(1).strip().strip('"')  # Remove leading/trailing spaces and quotes
        return None if query.lower() == "none" else query
    return None


# Sidebar for showing chat sessions and creating new sessions
st.sidebar.header("Chat Sessions")

# Button for creating a new chat
if st.sidebar.button("New Chat"):
    new_chat_id = create_new_chat_session()
    st.session_state['current_chat_id'] = new_chat_id
    st.session_state['chat_history'] = []

# List existing chat sessions with delete button (dustbin icon)
existing_sessions = chat_sessions.find().sort("created_at", -1)
for session in existing_sessions:
    session_id = str(session['_id'])

    # Retrieve stored UTC time and convert it to IST for display
    utc_time = session['created_at']
    ist_time = utc_time.replace(tzinfo=utc).astimezone(timezone("Asia/Kolkata"))
    session_date = ist_time.strftime("%Y-%m-%d %H:%M:%S")  # Format for display

    col1, col2 = st.sidebar.columns([8, 1])
    with col1:
        if st.button(f"Session {session_date}", key=session_id):
            st.session_state['current_chat_id'] = session_id
            load_chat_session(session_id)

    # Display delete icon (dustbin)
    with col2:
        if st.button("πŸ—‘οΈ", key=f"delete_{session_id}"):
            chat_sessions.delete_one({"_id": ObjectId(session_id)})
            st.rerun()  # Refresh the app to remove the deleted session from the sidebar

# Main chat interface
st.markdown('<div class="fixed-header"><h1>Welcome To IOCL Chatbot</h1></div>', unsafe_allow_html=True)
st.markdown("<hr>", unsafe_allow_html=True)

# Input box for the question
user_question = st.chat_input(f"Ask a Question related to IOCL Website")

if user_question:
    # Automatically create a new session if none exists
    if not st.session_state['current_chat_id']:
        new_chat_id = create_new_chat_session()
        st.session_state['current_chat_id'] = new_chat_id

    with st.spinner("Please wait, I am thinking!!"):
        # Store the user's question and get the assistant's response
        query = get_context_from_messages(user_question, st.session_state['chat_history'])
        if query:
            logging.info(f"Extracted query is :{query}\n")
            extracted_query = get_query_from_llm_answer(query)
            if extracted_query:
                query = extracted_query
            else:
                query = user_question

            query_embedding = embeddings.embed_query(query)
            search_results = index.query(vector=query_embedding, top_k=10, include_metadata=True)
            matches = search_results['matches']

            content = ""
            for i, match in enumerate(matches):
                chunk = match['metadata']['chunk']
                url = match['metadata']['url']
                content += f"chunk{i}: {chunk}\n" + f"url{i}: {url}\n"

            print(f"content being passed is {content}")
            reply = generate_summary(content, query, st.session_state['chat_history'])

            if reply:
                # Append the new question-answer pair to chat history
                st.session_state['chat_history'].append(
                    {"question": user_question, "answer": reply, "improved_question": query})

                # Update the current chat session in MongoDB
                if st.session_state['current_chat_id']:
                    update_chat_session(st.session_state['current_chat_id'], user_question, reply, query)

            else:
                st.error("Error processing your request, Please try again later.")
        else:
            st.error("Error processing your request, Please try again later.")
# Display the updated chat history (show last 15 question-answer pairs)
for i, pair in enumerate(st.session_state['chat_history']):
    question = pair["question"]
    answer = pair["answer"]
    streamlit_chat.message(question, is_user=True, key=f"chat_message_user_{i}")
    streamlit_chat.message(answer, is_user=False, key=f"chat_message_assistant_{i}")

# Display regenerate button under the last response
if st.session_state['chat_history'] and not st.session_state['regenerate']:
    if st.button("πŸ”„ Regenerate", key="regenerate_button"):
        st.session_state['regenerate'] = True
        regenerate_response()