JasonKishore commited on
Commit
f765487
Β·
verified Β·
1 Parent(s): f438a6e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +147 -175
app.py CHANGED
@@ -5,12 +5,10 @@ import os
5
  from pypdf import PdfReader
6
 
7
  # ── CONFIG ───────────────────────────────────────────────────────────────────
8
- OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") # ← fixed: use environment variable
9
- # If testing locally, you can temporarily do:
10
- # OPENAI_API_KEY = "sk-......................."
11
 
12
  if not OPENAI_API_KEY:
13
- raise ValueError("OPENAI_API_KEY environment variable not set")
14
 
15
  client = openai.OpenAI(api_key=OPENAI_API_KEY)
16
 
@@ -27,67 +25,90 @@ doc_name = ""
27
 
28
  # ── CORE LOGIC ───────────────────────────────────────────────────────────────
29
  def split_chunks(text, size=CHUNK_SIZE, overlap=OVERLAP):
30
- words, chunks, start = text.split(), [], 0
 
 
31
  while start < len(words):
32
  chunks.append(" ".join(words[start:start + size]))
33
  start += size - overlap
34
  return chunks
35
 
36
  def embed(text):
37
- return client.embeddings.create(model=EMBED_MODEL, input=text).data[0].embedding
 
 
 
38
 
39
  def cosine_similarity(a, b):
40
- dot = sum(x * y for x, y in zip(a, b))
41
- normA = math.sqrt(sum(x * x for x in a))
42
- normB = math.sqrt(sum(x * x for x in b))
43
- return dot / (normA * normB) if normA * normB != 0 else 0
44
 
45
  def get_top_chunks(question, k=TOP_K):
46
- q_vec = embed(question)
47
- scores = [(cosine_similarity(q_vec, cv), c)
48
- for cv, c in zip(stored_embeddings, stored_chunks)]
49
- scores.sort(reverse=True)
 
 
 
 
50
  return [c for _, c in scores[:k]]
51
 
52
  def upload_pdf(file):
53
  global stored_chunks, stored_embeddings, doc_name
54
  if file is None:
55
- return ("β€” No file selected.", gr.update(interactive=False))
56
 
57
- reader = PdfReader(file.name)
58
- full_text = "".join(page.extract_text() or "" for page in reader.pages)
59
- doc_name = os.path.basename(file.name)
 
60
 
61
- stored_chunks = split_chunks(full_text)
62
- stored_embeddings = [embed(c) for c in stored_chunks]
63
 
64
- return (
65
- f"✦ {doc_name}\nβ†’ {len(stored_chunks)} chunks indexed and ready.",
66
- gr.update(interactive=True)
67
- )
 
 
68
 
69
  def chat(question, history):
70
  if not stored_chunks:
71
- return history + [(question, "Please index a document in the Upload tab first.")]
72
-
73
- context = "\n\n".join([f"[{i+1}] {c}" for i, c in enumerate(get_top_chunks(question))])
74
- prompt = (
75
- "Answer strictly from the document text below.\n"
76
- "If not found, reply: \"This information is not in the document.\"\n\n"
77
- f"TEXT:\n{context}\n\nQUESTION: {question}\n\nANSWER:"
 
 
 
 
 
 
 
 
78
  )
 
79
  response = client.chat.completions.create(
80
  model=CHAT_MODEL,
81
  messages=[
82
- {"role": "system", "content":
83
- "Strict document assistant. Only use provided text. "
84
- "If answer absent, say: This information is not in the document."},
85
  {"role": "user", "content": prompt}
86
- ]
 
 
87
  )
88
- return history + [(question, response.choices[0].message.content)]
89
 
90
- # ── CSS (cleaner, higher contrast, Apple-like feel) ──────────────────────────
 
 
 
91
  css = """
92
  :root {
93
  --bg: #fdfdfd;
@@ -95,39 +116,33 @@ css = """
95
  --surface-2: #f5f5f7;
96
  --border: #d2d2d7;
97
  --border-2: #c7c7cc;
98
- --accent: #0071e3; /* Apple blue */
99
  --accent-light: #4090ff;
100
- --accent-glow: rgba(0, 113, 227, 0.12);
101
- --green: #34c759;
102
  --text-1: #1d1d1f;
103
  --text-2: #3c3c43;
104
  --text-3: #6e6e73;
105
- --font-sans: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
106
- --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
107
- --r: 12px;
108
- --r-lg: 20px;
109
- --ease: cubic-bezier(0.25, 0.1, 0.25, 1);
110
- --shadow: 0 1px 3px rgba(0,0,0,0.08), 0 4px 12px rgba(0,0,0,0.05);
111
  }
112
 
113
- html, body, .gradio-container {
114
- font-family: var(--font-sans) !important;
115
- background: var(--bg) !important;
116
- color: var(--text-1) !important;
117
  }
118
 
119
  .gradio-container {
120
- max-width: 1100px !important;
121
- margin: 0 auto !important;
122
- padding: 32px 24px 80px !important;
123
  }
124
 
125
  ::-webkit-scrollbar {
126
  width: 6px;
127
  }
128
- ::-webkit-scrollbar-track {
129
- background: transparent;
130
- }
131
  ::-webkit-scrollbar-thumb {
132
  background: #c0c0c5;
133
  border-radius: 3px;
@@ -136,168 +151,125 @@ html, body, .gradio-container {
136
  background: #a0a0a8;
137
  }
138
 
139
- /* Masthead */
140
- .mast-title {
141
- font-size: 2.4rem;
142
- font-weight: 600;
143
- letter-spacing: -0.02em;
144
- color: var(--text-1);
145
- }
146
- .mast-title em {
147
- color: var(--accent);
148
- font-style: normal;
149
- font-weight: 700;
150
- }
151
- .mast-sub {
152
- font-family: var(--font-mono);
153
- font-size: 0.68rem;
154
- color: var(--text-3);
155
- letter-spacing: 0.06em;
156
- text-transform: uppercase;
157
- margin-top: 6px;
158
- }
159
-
160
- /* Chatbot fixes – most important for readability */
161
- .chatbot-wrap > div {
162
- background: var(--bg) !important;
163
  border: 1px solid var(--border) !important;
164
- border-radius: var(--r-lg) !important;
 
 
165
  }
166
- .message-wrap .user-row .message {
 
167
  background: var(--accent) !important;
168
  color: white !important;
169
  border-radius: 20px 20px 4px 20px !important;
 
 
170
  padding: 12px 16px !important;
171
- font-size: 0.94rem !important;
172
- line-height: 1.5 !important;
173
  }
174
- .message-wrap .bot-row .message {
 
175
  background: var(--surface) !important;
176
  color: var(--text-1) !important;
177
  border: 1px solid var(--border-2) !important;
178
  border-radius: 20px 20px 20px 4px !important;
 
 
179
  padding: 12px 16px !important;
180
- font-size: 0.94rem !important;
181
- line-height: 1.5 !important;
182
  }
183
 
184
- /* Inputs */
185
- textarea, input[type="text"] {
186
- background: white !important;
187
  border: 1px solid var(--border-2) !important;
188
- border-radius: var(--r) !important;
189
- color: var(--text-1) !important;
190
- font-size: 0.96rem !important;
191
- padding: 12px 16px !important;
192
- }
193
- textarea:focus, input[type="text"]:focus {
194
- border-color: var(--accent) !important;
195
- box-shadow: 0 0 0 3px var(--accent-glow) !important;
196
- }
197
- textarea::placeholder {
198
- color: var(--text-3) !important;
199
  }
200
-
201
- /* Buttons */
202
- button.primary {
203
  background: var(--accent) !important;
204
  color: white !important;
205
  border: none !important;
206
- border-radius: 10px !important;
207
- font-weight: 500 !important;
208
  }
209
- button.primary:hover {
210
  background: var(--accent-light) !important;
211
  }
212
-
213
- /* Tabs & labels */
214
- label > span, .gr-form label span {
215
- font-family: var(--font-mono) !important;
216
- font-size: 0.64rem !important;
217
- color: var(--text-3) !important;
218
- letter-spacing: 0.04em;
219
- text-transform: uppercase;
220
- }
221
  """
222
 
223
- # ── UI ────────────────────────────────────────────────────────────────────────
224
- with gr.Blocks(css=css, title="Document Intelligence") as demo:
225
-
226
- gr.HTML("""
227
- <div style="display:flex; justify-content:space-between; align-items:flex-start; margin-bottom:32px; padding-bottom:20px; border-bottom:1px solid #d2d2d7;">
228
- <div>
229
- <div style="font-size:2.4rem; font-weight:600; letter-spacing:-0.02em;">Document <span style="color:#0071e3;">Intelligence</span></div>
230
- <div style="font-family:ui-monospace,monospace; font-size:0.68rem; color:#6e6e73; letter-spacing:0.06em; text-transform:uppercase; margin-top:6px;">
231
- Retrieval-Augmented Generation Β· Semantic Search Β· GPT-4o mini
232
- </div>
233
- </div>
234
- <div style="text-align:right;">
235
- <div style="font-size:0.9rem; font-weight:500; color:#1d1d1f;">Your Name / Portfolio</div>
236
- <div style="font-size:0.7rem; color:#6e6e73;">Gradio + OpenAI</div>
237
- </div>
238
- </div>
239
- """)
240
 
241
  with gr.Tabs():
242
- with gr.TabItem("Upload & Index"):
243
- with gr.Row():
244
- with gr.Column(scale=1):
245
- pdf_input = gr.File(label="Upload PDF document", file_types=[".pdf"])
246
- upload_btn = gr.Button("Index Document", variant="primary")
247
- upload_status = gr.Textbox(
248
- label="Status",
249
- lines=3,
250
- interactive=False,
251
- value="β€” Waiting for PDF…",
252
- )
253
-
254
- with gr.Column(scale=1):
255
- gr.Markdown("### Settings & Pipeline")
256
- gr.Markdown("""
257
- - Embedding model: **text-embedding-3-small**
258
- - Chat model: **gpt-4o-mini**
259
- - Chunk size: **100** words
260
- - Overlap: **40** words
261
- - Retrieval: **top-2** chunks
262
- """)
263
-
264
- with gr.TabItem("Chat"):
265
  chatbot = gr.Chatbot(
266
  height=520,
267
  show_label=False,
268
- bubble_full_width=False,
269
- elem_classes=["chatbot-wrap"],
270
  )
271
  with gr.Row():
272
- msg_box = gr.Textbox(
273
- placeholder="Ask anything about the document…",
274
  container=False,
275
  scale=5,
 
276
  )
277
- send_btn = gr.Button("Send", variant="primary", scale=1, min_width=80)
278
 
279
  gr.Markdown(
280
- "*Only answers grounded in the uploaded document. Clear chat to start over.*",
281
- elem_classes=["small-footer"]
282
  )
283
 
284
  # Events
285
  upload_btn.click(
286
  fn=upload_pdf,
287
  inputs=pdf_input,
288
- outputs=[upload_status, send_btn]
289
  )
290
 
291
- send_btn.click(
292
- fn=chat,
293
- inputs=[msg_box, chatbot],
294
- outputs=chatbot
295
- ).then(lambda: "", None, msg_box)
296
 
297
- msg_box.submit(
298
- fn=chat,
299
- inputs=[msg_box, chatbot],
300
- outputs=chatbot
301
- ).then(lambda: "", None, msg_box)
 
 
 
 
 
302
 
303
- demo.launch()
 
 
 
 
 
 
 
5
  from pypdf import PdfReader
6
 
7
  # ── CONFIG ───────────────────────────────────────────────────────────────────
8
+ OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
 
 
9
 
10
  if not OPENAI_API_KEY:
11
+ raise ValueError("Please set the OPENAI_API_KEY environment variable")
12
 
13
  client = openai.OpenAI(api_key=OPENAI_API_KEY)
14
 
 
25
 
26
  # ── CORE LOGIC ───────────────────────────────────────────────────────────────
27
  def split_chunks(text, size=CHUNK_SIZE, overlap=OVERLAP):
28
+ words = text.split()
29
+ chunks = []
30
+ start = 0
31
  while start < len(words):
32
  chunks.append(" ".join(words[start:start + size]))
33
  start += size - overlap
34
  return chunks
35
 
36
  def embed(text):
37
+ return client.embeddings.create(
38
+ model=EMBED_MODEL,
39
+ input=text
40
+ ).data[0].embedding
41
 
42
  def cosine_similarity(a, b):
43
+ dot = sum(x * y for x, y in zip(a, b))
44
+ norm_a = math.sqrt(sum(x * x for x in a))
45
+ norm_b = math.sqrt(sum(x * x for x in b))
46
+ return dot / (norm_a * norm_b) if norm_a * norm_b != 0 else 0.0
47
 
48
  def get_top_chunks(question, k=TOP_K):
49
+ if not stored_embeddings:
50
+ return []
51
+ q_vec = embed(question)
52
+ scores = [
53
+ (cosine_similarity(q_vec, cv), c)
54
+ for cv, c in zip(stored_embeddings, stored_chunks)
55
+ ]
56
+ scores.sort(reverse=True, key=lambda x: x[0])
57
  return [c for _, c in scores[:k]]
58
 
59
  def upload_pdf(file):
60
  global stored_chunks, stored_embeddings, doc_name
61
  if file is None:
62
+ return "β€” No file selected.", gr.update(interactive=False)
63
 
64
+ try:
65
+ reader = PdfReader(file.name)
66
+ full_text = "".join(page.extract_text() or "" for page in reader.pages)
67
+ doc_name = os.path.basename(file.name)
68
 
69
+ stored_chunks = split_chunks(full_text)
70
+ stored_embeddings = [embed(c) for c in stored_chunks]
71
 
72
+ return (
73
+ f"✦ {doc_name}\nβ†’ {len(stored_chunks)} chunks indexed and ready.",
74
+ gr.update(interactive=True)
75
+ )
76
+ except Exception as e:
77
+ return f"Error processing PDF: {str(e)}", gr.update(interactive=False)
78
 
79
  def chat(question, history):
80
  if not stored_chunks:
81
+ return history + [(question, "Please upload and index a document first in the Upload tab.")]
82
+
83
+ context_chunks = get_top_chunks(question)
84
+ if not context_chunks:
85
+ return history + [(question, "No relevant context found.")]
86
+
87
+ context = "\n\n".join([f"[{i+1}] {c}" for i, c in enumerate(context_chunks)])
88
+ prompt = (
89
+ "You are a strict document Q&A assistant. "
90
+ "Answer only using the provided document text below. "
91
+ "If the information is not present, reply exactly: "
92
+ "\"This information is not in the document.\"\n\n"
93
+ f"TEXT:\n{context}\n\n"
94
+ f"QUESTION: {question}\n\n"
95
+ "ANSWER:"
96
  )
97
+
98
  response = client.chat.completions.create(
99
  model=CHAT_MODEL,
100
  messages=[
101
+ {"role": "system", "content": "Follow the instructions precisely."},
 
 
102
  {"role": "user", "content": prompt}
103
+ ],
104
+ temperature=0.1,
105
+ max_tokens=800
106
  )
 
107
 
108
+ answer = response.choices[0].message.content.strip()
109
+ return history + [(question, answer)]
110
+
111
+ # ── CSS (clean, high-contrast, Apple-like) ──────────────────────────────────
112
  css = """
113
  :root {
114
  --bg: #fdfdfd;
 
116
  --surface-2: #f5f5f7;
117
  --border: #d2d2d7;
118
  --border-2: #c7c7cc;
119
+ --accent: #0071e3;
120
  --accent-light: #4090ff;
121
+ --accent-glow: rgba(0, 113, 227, 0.14);
 
122
  --text-1: #1d1d1f;
123
  --text-2: #3c3c43;
124
  --text-3: #6e6e73;
125
+ --radius: 12px;
126
+ --radius-lg: 20px;
127
+ --shadow-sm: 0 1px 3px rgba(0,0,0,0.08);
128
+ --shadow: 0 4px 12px rgba(0,0,0,0.06);
 
 
129
  }
130
 
131
+ body, .gradio-container {
132
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
133
+ background: var(--bg);
134
+ color: var(--text-1);
135
  }
136
 
137
  .gradio-container {
138
+ max-width: 1100px;
139
+ margin: 0 auto;
140
+ padding: 32px 24px 80px;
141
  }
142
 
143
  ::-webkit-scrollbar {
144
  width: 6px;
145
  }
 
 
 
146
  ::-webkit-scrollbar-thumb {
147
  background: #c0c0c5;
148
  border-radius: 3px;
 
151
  background: #a0a0a8;
152
  }
153
 
154
+ /* Chat */
155
+ .chatbot {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
156
  border: 1px solid var(--border) !important;
157
+ border-radius: var(--radius-lg) !important;
158
+ background: var(--bg) !important;
159
+ overflow: hidden;
160
  }
161
+
162
+ .message.user {
163
  background: var(--accent) !important;
164
  color: white !important;
165
  border-radius: 20px 20px 4px 20px !important;
166
+ margin: 8px 12px 8px auto !important;
167
+ max-width: 78% !important;
168
  padding: 12px 16px !important;
169
+ line-height: 1.48;
 
170
  }
171
+
172
+ .message.bot, .message {
173
  background: var(--surface) !important;
174
  color: var(--text-1) !important;
175
  border: 1px solid var(--border-2) !important;
176
  border-radius: 20px 20px 20px 4px !important;
177
+ margin: 8px auto 8px 12px !important;
178
+ max-width: 78% !important;
179
  padding: 12px 16px !important;
180
+ line-height: 1.48;
 
181
  }
182
 
183
+ /* Input & Button */
184
+ .gr-text-input, textarea {
 
185
  border: 1px solid var(--border-2) !important;
186
+ border-radius: var(--radius) !important;
187
+ background: white !important;
 
 
 
 
 
 
 
 
 
188
  }
189
+ .gr-button-primary {
 
 
190
  background: var(--accent) !important;
191
  color: white !important;
192
  border: none !important;
 
 
193
  }
194
+ .gr-button-primary:hover {
195
  background: var(--accent-light) !important;
196
  }
 
 
 
 
 
 
 
 
 
197
  """
198
 
199
+ # ── INTERFACE ────────────────────────────────────────────────────────────────
200
+ with gr.Blocks(title="Document Q&A") as demo:
201
+
202
+ gr.Markdown(
203
+ """
204
+ # Document Intelligence
205
+
206
+ Upload a PDF β†’ ask questions about its content.
207
+ Answers are grounded **only** in the uploaded document.
208
+ """
209
+ )
 
 
 
 
 
 
210
 
211
  with gr.Tabs():
212
+ with gr.Tab("Upload & Index"):
213
+ pdf_input = gr.File(
214
+ label="Upload PDF",
215
+ file_types=[".pdf"],
216
+ type="filepath"
217
+ )
218
+ upload_btn = gr.Button("Index Document", variant="primary")
219
+ status = gr.Textbox(
220
+ label="Status",
221
+ interactive=False,
222
+ lines=3,
223
+ value="β€” Waiting for PDF…"
224
+ )
225
+
226
+ with gr.Tab("Ask Questions"):
 
 
 
 
 
 
 
 
227
  chatbot = gr.Chatbot(
228
  height=520,
229
  show_label=False,
230
+ elem_classes=["chatbot"],
231
+ avatar_images=(None, None)
232
  )
233
  with gr.Row():
234
+ msg = gr.Textbox(
235
+ placeholder="Ask a question about the document…",
236
  container=False,
237
  scale=5,
238
+ autofocus=True
239
  )
240
+ send = gr.Button("Send", variant="primary", scale=1, min_width=80)
241
 
242
  gr.Markdown(
243
+ "*Answers are strictly based on the indexed document content.*",
244
+ elem_classes=["small muted"]
245
  )
246
 
247
  # Events
248
  upload_btn.click(
249
  fn=upload_pdf,
250
  inputs=pdf_input,
251
+ outputs=[status, send]
252
  )
253
 
254
+ def respond(message, chat_history):
255
+ new_history = chat(message, chat_history)
256
+ return "", new_history
 
 
257
 
258
+ send.click(
259
+ fn=respond,
260
+ inputs=[msg, chatbot],
261
+ outputs=[msg, chatbot]
262
+ )
263
+ msg.submit(
264
+ fn=respond,
265
+ inputs=[msg, chatbot],
266
+ outputs=[msg, chatbot]
267
+ )
268
 
269
+ if __name__ == "__main__":
270
+ demo.launch(
271
+ css=css,
272
+ share=False, # change to True if you want public link
273
+ server_name="0.0.0.0",
274
+ server_port=7860
275
+ )