shukdev3 commited on
Commit
8e6e7c2
Β·
verified Β·
1 Parent(s): 452e0d2

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +293 -0
app.py ADDED
@@ -0,0 +1,293 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ from llama_index.core import VectorStoreIndex, Document # Imports VectorStoreIndex for semantic search and Document class for text storage
3
+ from llama_index.llms.openai import OpenAI # Imports OpenAI language model integration from LlamaIndex
4
+ from llama_index.core import Settings # Imports Settings to configure global LlamaIndex parameters
5
+ import os # Imports OS module for interacting with the operating system (file paths, environment variables)
6
+ import pdfplumber # Imports pdfplumber library for extracting text from PDF files
7
+ from docx import Document as DocxDocument # Imports Document class from python-docx, renamed to avoid conflict with LlamaIndex's Document
8
+ import json # Imports JSON module for parsing and creating JSON data
9
+ from datetime import datetime # Imports datetime class for working with dates and times
10
+ import hashlib # Imports hashlib for creating hash functions (MD5, SHA, etc.) for data integrity/unique identifiers
11
+
12
+ # Global variables
13
+ chat_engine = None # Stores the LlamaIndex chat engine instance; None until initialized with documents
14
+ conversation_history = [] # Empty list to store all chat messages (user questions and AI responses)
15
+ current_user_id = None # Stores hashed identifier for current user based on their API key
16
+
17
+ # Function to generate user ID from API key
18
+ def get_user_id(api_key): # Defines function that takes API key string as input
19
+ if not api_key: # Checks if api_key is None, empty string, or falsy value
20
+ return None # Returns None if no API key provided
21
+ return hashlib.sha256(api_key.encode()).hexdigest()[:16] # Encodes key to bytes, creates SHA-256 hash, converts to hex string, returns first 16 characters as unique user ID
22
+
23
+ # Function to get user-specific filename
24
+ def get_user_file(api_key): # Defines function that generates unique filename for each user
25
+ user_id = get_user_id(api_key) # Calls get_user_id to generate unique identifier from API key
26
+ if not user_id: # Checks if user_id is None (happens when api_key is invalid/empty)
27
+ return None # Returns None if no valid user ID could be generated
28
+ return f"conversations_{user_id}.json" # Returns formatted string with user-specific filename for storing conversation history
29
+
30
+ # Function to read PDF files
31
+ def read_pdf(file_path): # Defines function that takes a file path string as parameter
32
+ with pdfplumber.open(file_path) as pdf: # Opens PDF file using context manager (auto-closes after use)
33
+ text = '' # Initializes empty string to accumulate extracted text
34
+ for page in pdf.pages: # Loops through each page object in the PDF
35
+ text += page.extract_text() + '\n' # Extracts text from current page and appends it with newline character
36
+ return text # Returns the complete concatenated text from all pages
37
+
38
+ # Function to read DOCX files
39
+ def read_docx(file_path): # Defines function that takes a file path string as parameter
40
+ doc = DocxDocument(file_path) # Creates a Document object by loading the .docx file
41
+ text = '' # Initializes empty string to store extracted text
42
+ for paragraph in doc.paragraphs: # Iterates through each paragraph object in the document
43
+ text += paragraph.text + '\n' # Extracts text from current paragraph and appends with newline
44
+ return text # Returns the complete text from all paragraphs
45
+
46
+ # Function to load and index documents
47
+ def load_data(files, api_key): # Defines function that accepts uploaded files list and API key string
48
+ global chat_engine, current_user_id # Declares these as global so changes persist outside function scope
49
+
50
+ if not api_key: # Checks if API key is missing, empty, or None
51
+ return "Please provide your OpenAI API key first." # Returns error message prompting for API key
52
+
53
+ if not files: # Checks if files list is empty, None, or falsy
54
+ return "Please upload files to proceed." # Returns error message prompting for file upload
55
+
56
+ try: # Begins try block to catch any errors during document processing
57
+ # Set current user
58
+ current_user_id = get_user_id(api_key) # Generates and stores unique user ID from API key in global variable
59
+
60
+ docs = [] # Initializes empty list to store Document objects
61
+ for file in files: # Loops through each uploaded file object in the files list
62
+ if file.name.endswith('.pdf'): # Checks if filename ends with .pdf extension
63
+ text = read_pdf(file.name) # Extracts all text from PDF using previously defined function
64
+ docs.append(Document(text=text)) # Creates LlamaIndex Document object from text and adds to list
65
+ elif file.name.endswith('.docx'): # Checks if filename ends with .docx extension
66
+ text = read_docx(file.name) # Extracts all text from Word document using previously defined function
67
+ docs.append(Document(text=text)) # Creates Document object from extracted text and appends to list
68
+
69
+ # Set OpenAI API key
70
+ os.environ["OPENAI_API_KEY"] = api_key # Sets environment variable so OpenAI library can automatically access the API key
71
+
72
+ Settings.llm = OpenAI( # Configures the global LLM (Large Language Model) settings for LlamaIndex
73
+ model="gpt-5-nano", # Specifies which OpenAI model to use (GPT-4 optimized mini version)
74
+ temperature=0.5, # Sets randomness level (0=deterministic/focused, 1=creative/random); 0.5 is balanced
75
+ api_key=api_key, # Passes API key directly to OpenAI client for authentication
76
+ system_prompt="You are a helpful AI assistant that answers questions based on the provided documents. Always base your answers on the content of the uploaded documents. If the answer cannot be found in the documents, clearly state that. Be accurate, concise, and cite specific information from the documents when possible." # Instructions that guide the AI's behavior, response style, and constrain it to document content
77
+ )
78
+
79
+ index = VectorStoreIndex.from_documents(docs) # Creates vector embeddings of all documents for semantic similarity search
80
+ chat_engine = index.as_chat_engine(chat_mode="condense_question", verbose=True) # Converts index to conversational chat interface; condense_question mode reformulates follow-up questions using conversation context; verbose=True prints debug info
81
+
82
+ return "Documents loaded and indexed successfully! You can now start chatting." # Returns success message to display to user
83
+ except Exception as e: # Catches any error that occurred anywhere in the try block
84
+ return f"Error loading documents: {str(e)}" # Returns formatted error message with details of what went wrong
85
+
86
+ # Function to handle chat
87
+ def chat_with_docs(message, history, api_key): # Defines function that takes user's message, chat history list, and API key as parameters
88
+ global chat_engine, conversation_history, current_user_id # Declares global variables so function can read/modify them
89
+
90
+ if not api_key: # Checks if API key is missing, empty, or None
91
+ return history + [{"role": "assistant", "content": "Please enter your OpenAI API key first."}] # Returns existing history plus error message as assistant response
92
+
93
+ # Update current user
94
+ current_user_id = get_user_id(api_key) # Generates unique user ID from API key and stores in global variable
95
+
96
+ if chat_engine is None: # Checks if chat_engine hasn't been initialized (no documents loaded yet)
97
+ return history + [ # Returns history with two new messages added to the list
98
+ {"role": "user", "content": message}, # Adds user's question to history as dictionary
99
+ {"role": "assistant", "content": "Please upload and load documents first before asking questions."} # Adds assistant's error response
100
+ ]
101
+
102
+ try: # Begins try block to catch errors during chat interaction
103
+ response = chat_engine.chat(message) # Sends user message to chat engine, which searches documents and generates response
104
+ conversation_history.append({"role": "user", "content": message}) # Adds user message to global conversation history list
105
+ conversation_history.append({"role": "assistant", "content": response.response}) # Adds AI response to global conversation history (response.response extracts text from response object)
106
+
107
+ return history + [ # Returns updated history by concatenating existing history with new messages
108
+ {"role": "user", "content": message}, # Adds current user message
109
+ {"role": "assistant", "content": response.response} # Adds AI's response text
110
+ ]
111
+ except Exception as e: # Catches any error that occurred during chat processing
112
+ return history + [ # Returns history with error message instead of crashing
113
+ {"role": "user", "content": message}, # Still adds user's message to show what they asked
114
+ {"role": "assistant", "content": f"Error: {str(e)}"} # Adds error details as assistant response for debugging
115
+ ]
116
+
117
+ # Function to save conversation (user-specific)
118
+ def save_conversation(api_key): # Defines function that saves conversation to user-specific file
119
+ global conversation_history # Accesses global conversation_history variable
120
+
121
+ if not api_key: # Checks if API key is missing or empty
122
+ return "Please enter your OpenAI API key first." # Returns error message and exits function
123
+
124
+ if not conversation_history: # Checks if conversation_history list is empty (no messages to save)
125
+ return "No conversation to save." # Returns message indicating nothing to save
126
+
127
+ try: # Begins try block to handle file writing errors
128
+ user_file = get_user_file(api_key) # Generates unique filename based on user's API key (e.g., "conversations_abc123.json")
129
+ timestamp = datetime.now().strftime(%Y-%m-%d_%H-%M-%S") # Gets current date/time and formats as string (e.g., "2026-01-14_15-30-45")
130
+
131
+ with open(user_file, "a") as f: # Opens user's file in append mode ("a" means add to end without overwriting); auto-closes when done
132
+ conv_data = { # Creates dictionary to structure the conversation data
133
+ "timestamp": timestamp, # Stores when conversation was saved
134
+ "messages": conversation_history # Stores all messages from current conversation
135
+ }
136
+ json.dump(conv_data, f) # Converts dictionary to JSON format and writes to file
137
+ f.write("\n") # Adds newline character so each saved conversation is on separate line in file
138
+ return "Conversation saved successfully!" # Returns success message to display to user
139
+ except Exception as e: # Catches any errors during file operations (permission issues, disk full, etc.)
140
+ return f"Error saving conversation: {str(e)}" # Returns formatted error message with details
141
+
142
+ # Function to delete all conversations (user-specific)
143
+ def delete_all_conversations(api_key): # Defines function to permanently delete user's conversation file
144
+ if not api_key: # Checks if API key is missing or empty
145
+ return "Please enter your OpenAI API key first." # Returns error message requiring API key
146
+
147
+ try: # Begins try block to handle file deletion errors
148
+ user_file = get_user_file(api_key) # Generates filename for this user's conversations
149
+ if os.path.exists(user_file): # Checks if file actually exists before attempting deletion
150
+ os.remove(user_file) # Deletes the file from disk permanently
151
+ return "All your conversations deleted successfully!" # Returns success confirmation message
152
+ return "No conversations to delete." # Returns message if file doesn't exist (nothing to delete)
153
+ except Exception as e: # Catches errors like permission denied, file in use, etc.
154
+ return f"Error deleting conversations: {str(e)}" # Returns error message with details for debugging
155
+
156
+ # Function to load previous conversations (user-specific)
157
+ def load_conversations(api_key): # Defines function that retrieves and displays user's saved conversations
158
+ if not api_key: # Checks if API key is missing, empty, or None
159
+ return "Please enter your OpenAI API key first to view your conversations." # Returns error message prompting for API key
160
+
161
+ user_file = get_user_file(api_key) # Generates unique filename for this user based on their API key (e.g., "conversations_abc123.json")
162
+
163
+ if os.path.exists(user_file): # Checks if the user's conversation file actually exists on disk
164
+ try: # Begins try block to handle file reading and parsing errors
165
+ with open(user_file, "r") as f: # Opens user's file in read mode; auto-closes when done
166
+ conversations = [json.loads(line) for line in f] # List comprehension: reads each line, parses JSON, creates list of conversation dictionaries
167
+
168
+ conv_text = "" # Initializes empty string to build formatted conversation display
169
+ for i, conv in enumerate(conversations): # Loops through conversations with index (i) and conversation data (conv)
170
+ conv_text += f"\n{'='*50}\nConversation {i + 1}\n{'='*50}\n" # Adds separator line (50 equals signs), conversation number header, and another separator
171
+ timestamp = conv.get("timestamp", "Unknown time") # Retrieves timestamp from conversation dict; defaults to "Unknown time" if key doesn't exist
172
+ conv_text += f"Timestamp: {timestamp}\n\n" # Adds timestamp to output with two newlines for spacing
173
+
174
+ messages = conv.get("messages", conv) # Gets messages list from conversation; if "messages" key doesn't exist, uses entire conv dict as fallback
175
+ for message in messages: # Loops through each message dictionary in the messages list
176
+ role = message.get('role', 'unknown') # Extracts role (user/assistant); defaults to 'unknown' if not found
177
+ content = message.get('content', '') # Extracts message content; defaults to empty string if not found
178
+ conv_text += f"{role.upper()}: {content}\n\n" # Adds formatted message with role in uppercase, content, and spacing
179
+
180
+ return conv_text if conv_text else "No previous conversations found." # Returns formatted text if any exists; otherwise returns "not found" message (ternary operator)
181
+ except Exception as e: # Catches any errors during file reading or JSON parsing
182
+ return f"Error loading conversations: {str(e)}" # Returns error message with exception details
183
+ return "No previous conversations found for your account." # Returns message if file doesn't exist (user has no saved conversations)
184
+
185
+ # Function to clear current conversation
186
+ def clear_conversation(): # Defines function to reset the current chat session
187
+ global conversation_history # Accesses global conversation_history variable to modify it
188
+ conversation_history = [] # Resets conversation_history to empty list, clearing all messages
189
+ return [] # Returns empty list to clear the Gradio chat interface display
190
+
191
+ # Create Gradio interface
192
+ with gr.Blocks(title="Chat with Documents πŸ’¬ πŸ“š", theme=gr.themes.Ocean()) as demo: # Creates Gradio app using Blocks API (custom layout); sets browser tab title and applies Ocean color theme; assigns to 'demo' variable
193
+ gr.Markdown("# Chat with Documents πŸ’¬ πŸ“š") # Displays large heading text using Markdown syntax (# = h1)
194
+ gr.Markdown("Upload PDF or DOCX files and chat with them using AI!") # Displays instruction text as second line
195
+ gr.Markdown("**Privacy Notice:** Your conversations are private and tied to your API key. Only you can see your saved conversations.") # Displays privacy notice in bold (**text** = bold in Markdown)
196
+
197
+ with gr.Row(): # Creates horizontal row container to arrange elements side-by-side
198
+ with gr.Column(scale=2): # Creates column inside row with scale=2 (takes 2/3 of width when combined with scale=1 column later)
199
+ api_key_input = gr.Textbox( # Creates text input box for API key
200
+ label="OpenAI API Key", # Sets label displayed above the textbox
201
+ type="password", # Masks input characters with dots/asterisks for security
202
+ placeholder="Enter your OpenAI API key here..." # Shows gray hint text when box is empty
203
+ )
204
+
205
+ file_upload = gr.File( # Creates file upload widget
206
+ label="Upload PDF or DOCX files", # Sets label above file upload area
207
+ file_count="multiple", # Allows user to select multiple files at once
208
+ file_types=[".pdf", ".docx"] # Restricts file picker to only show PDF and DOCX files
209
+ )
210
+
211
+ load_btn = gr.Button("Load Documents", variant="primary") # Creates button with text "Load Documents"; variant="primary" makes it blue/highlighted
212
+ load_status = gr.Textbox(label="Status", interactive=False) # Creates read-only textbox to display status messages; interactive=False prevents user editing
213
+
214
+ load_btn.click( # Defines what happens when load_btn is clicked
215
+ fn=load_data, # Calls load_data function when button clicked
216
+ inputs=[file_upload, api_key_input], # Passes file_upload and api_key_input values as arguments to load_data
217
+ outputs=load_status # Displays return value from load_data in load_status textbox
218
+ )
219
+
220
+ with gr.Row(): # Creates another horizontal row below the first one
221
+ with gr.Column(scale=3): # Creates column with scale=3 (takes 3/4 width; main chat area)
222
+ chatbot = gr.Chatbot( # Creates chatbot interface component for displaying conversation
223
+ label="Chat", # Sets label above chat window
224
+ height=400 # Sets chat window height to 400 pixels
225
+ )
226
+ msg = gr.Textbox( # Creates text input for user to type questions
227
+ label="Your Question", # Label displayed above input box
228
+ placeholder="Ask a question about your documents..." # Hint text shown when empty
229
+ )
230
+
231
+ with gr.Row(): # Creates row inside column for button group
232
+ submit_btn = gr.Button("Send", variant="primary") # Creates primary (highlighted) Send button
233
+ clear_btn = gr.Button("Clear Chat") # Creates Clear Chat button with default styling
234
+
235
+ with gr.Row(): # Creates another row for save functionality
236
+ save_btn = gr.Button("Save Conversation") # Creates button to save chat history
237
+ save_status = gr.Textbox(label="Save Status", interactive=False) # Read-only textbox for save confirmation messages
238
+
239
+ with gr.Column(scale=1): # Creates sidebar column with scale=1 (takes 1/4 width; for conversation history)
240
+ gr.Markdown("### Your Previous Conversations") # Displays h3 heading (### = h3 in Markdown)
241
+ load_convs_btn = gr.Button("Load Your Conversations") # Creates button to retrieve saved conversations
242
+ convs_display = gr.Textbox( # Creates large textbox for displaying conversation history
243
+ label="Conversation History", # Label above the textbox
244
+ lines=20, # Sets textbox height to 20 lines of text
245
+ interactive=False # Makes textbox read-only (user cannot edit)
246
+ )
247
+ delete_all_btn = gr.Button("Delete All Your Conversations", variant="stop") # Creates red warning-style button for deletion; variant="stop" makes it red
248
+ delete_status = gr.Textbox(label="Delete Status", interactive=False) # Read-only textbox for deletion confirmation messages
249
+
250
+ # Event handlers
251
+ submit_btn.click( # Defines behavior when Send button is clicked
252
+ fn=chat_with_docs, # Calls chat_with_docs function
253
+ inputs=[msg, chatbot, api_key_input], # Passes message text, current chat history, and API key as arguments
254
+ outputs=chatbot # Updates chatbot display with return value (new conversation history)
255
+ ).then( # Chains another action after the first completes
256
+ lambda: "", # Anonymous function that returns empty string
257
+ outputs=msg # Clears the message input box after sending
258
+ )
259
+
260
+ msg.submit( # Defines behavior when user presses Enter key in message textbox
261
+ fn=chat_with_docs, # Calls same chat function as submit button
262
+ inputs=[msg, chatbot, api_key_input], # Same inputs as submit button
263
+ outputs=chatbot # Updates chat display
264
+ ).then( # Chains follow-up action
265
+ lambda: "", # Returns empty string
266
+ outputs=msg # Clears message box after Enter is pressed
267
+ )
268
+
269
+ clear_btn.click( # Defines behavior when Clear Chat button clicked
270
+ fn=clear_conversation, # Calls clear_conversation function (resets conversation_history to [])
271
+ outputs=chatbot # Updates chatbot display with empty list (clears visible chat)
272
+ )
273
+
274
+ save_btn.click( # Defines behavior when Save Conversation button clicked
275
+ fn=save_conversation, # Calls save_conversation function to write to JSON file
276
+ inputs=[api_key_input], # Passes API key to identify which user's file to save to
277
+ outputs=save_status # Displays success/error message in save_status textbox
278
+ )
279
+
280
+ load_convs_btn.click( # Defines behavior when Load Your Conversations button clicked
281
+ fn=load_conversations, # Calls load_conversations function to read from user's JSON file
282
+ inputs=[api_key_input], # Passes API key to identify which user's file to load
283
+ outputs=convs_display # Displays formatted conversation history in convs_display textbox
284
+ )
285
+
286
+ delete_all_btn.click( # Defines behavior when Delete All button clicked
287
+ fn=delete_all_conversations, # Calls delete_all_conversations function to remove user's file
288
+ inputs=[api_key_input], # Passes API key to identify which file to delete
289
+ outputs=delete_status # Displays confirmation/error message in delete_status textbox
290
+ )
291
+
292
+ if __name__ == "__main__": # Python idiom: only runs code below if script is executed directly (not imported as module)
293
+ demo.launch() # Starts Gradio web server and opens app in browser; makes app accessible at local URL (e.g., http://127.0.0.1:7860)