IW2025 commited on
Commit
00a2c28
Β·
verified Β·
1 Parent(s): d3a17e2

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +159 -302
app.py CHANGED
@@ -4,7 +4,6 @@ from pathlib import Path
4
  import fitz # PyMuPDF
5
  from langchain_community.embeddings import HuggingFaceEmbeddings
6
  from langchain_community.vectorstores import Chroma
7
- from langchain.text_splitter import RecursiveCharacterTextSplitter
8
  from langchain.prompts import PromptTemplate
9
  from langchain.chains import LLMChain
10
  import anthropic
@@ -12,12 +11,15 @@ import base64
12
  from PIL import Image
13
  import io
14
  import re
 
15
 
16
- # --- Minimal PDF Search & Display App ---
 
17
 
18
- # 1. Preprocess PDFs and build vector DB
19
- class CurriculumChatbot:
20
- def __init__(self, slides_dir="Slides", fast_mode=False):
 
21
  self.pdf_pages = {} # {filename: {page_num: text}}
22
  self.pdf_files = {} # {filename: path}
23
  self.chunks = []
@@ -25,32 +27,33 @@ class CurriculumChatbot:
25
  self.vector_db = None
26
  self.embeddings = None
27
  self.llm = None
28
- self.qa_chain = None
29
- self.slide_selection_chain = None
30
- self.focused_qa_chain = None
31
- self.response_cache = {} # Simple cache for responses
32
- self.fast_mode = fast_mode # Skip LLM for faster responses
33
  self._process_pdfs(slides_dir)
34
  self._build_vector_db()
35
- if not fast_mode:
36
- self._setup_llm()
37
- else:
38
- print("πŸš€ Fast mode enabled - LLM disabled for instant responses")
39
-
40
  def _process_pdfs(self, slides_dir):
 
41
  slides_path = Path(slides_dir)
42
  pdf_files = list(slides_path.glob("*.pdf"))
 
43
  for pdf_file in pdf_files:
44
  self.pdf_files[pdf_file.name] = str(pdf_file)
45
  doc = fitz.open(str(pdf_file))
46
  pages = {}
 
47
  for page_num in range(len(doc)):
48
  page = doc[page_num]
49
  text = page.get_text()
50
  if text.strip():
51
  pages[page_num + 1] = text.strip()
 
52
  self.pdf_pages[pdf_file.name] = pages
53
  doc.close()
 
54
  # Add each page as a chunk
55
  for page_num, text in pages.items():
56
  self.chunks.append(text)
@@ -58,8 +61,11 @@ class CurriculumChatbot:
58
  "filename": pdf_file.name,
59
  "page_number": page_num
60
  })
 
 
61
 
62
  def _build_vector_db(self):
 
63
  self.embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
64
  self.vector_db = Chroma.from_texts(
65
  texts=self.chunks,
@@ -67,22 +73,17 @@ class CurriculumChatbot:
67
  metadatas=self.chunk_metadata,
68
  persist_directory="./chroma_db"
69
  )
70
-
 
71
  def _setup_llm(self):
72
- """Setup LLM with Claude"""
73
  try:
74
- # Initialize LLM attributes
75
- self.llm = None
76
- self.qa_chain = None
77
- self.focused_qa_chain = None
78
- self.content_selection_chain = None
79
-
80
- # Load Claude
81
  self.anthropic_client = anthropic.Anthropic(
82
- api_key=os.environ.get("ANTHROPIC_API_KEY")
83
  )
84
 
85
- # Create a custom LLM wrapper that works with LangChain
86
  class ClaudeLLM:
87
  def __init__(self, client):
88
  self.client = client
@@ -102,91 +103,67 @@ class CurriculumChatbot:
102
 
103
  self.llm = ClaudeLLM(self.anthropic_client)
104
 
105
- # Create content selection prompt template
106
- content_selection_template = """You are an expert curriculum analyst. Your task is to find the most relevant slide for a student's question.
107
 
108
- Student Question: {question}
109
 
110
- Available Slide Contents:
111
  {slide_contents}
112
 
113
- Instructions:
114
- 1. Read each slide content carefully
115
- 2. Identify which slide best answers the student's specific question
116
- 3. Consider the exact terminology and concepts the student is asking about
117
- 4. If the student asks about "for loops", look for slides that specifically mention "for loops"
118
- 5. If the student asks about "loops" in general, look for slides that explain loops comprehensively
119
- 6. Respond with ONLY the slide number (1, 2, 3, etc.) that is most relevant
120
- 7. If no slide is relevant, respond with "0"
121
-
122
- Most relevant slide number:"""
123
-
124
- self.content_selection_chain = LLMChain(llm=self.llm, prompt=PromptTemplate(
125
- input_variables=["question", "slide_contents"],
126
- template=content_selection_template
127
- ))
128
-
129
- # Create QA prompt template for Claude
130
- qa_template = """You are an expert programming tutor. Your task is to provide a comprehensive, educational answer based on the curriculum content.
131
 
132
- Curriculum Content:
133
- {filled_context}
134
 
135
- Student Question: {question}
136
-
137
- Instructions:
138
- 1. Analyze the curriculum content carefully
139
- 2. Provide a detailed, educational explanation
140
- 3. Use examples if the content contains them
141
- 4. Explain the concept step-by-step
142
- 5. Make sure your answer directly addresses what the student is asking
143
- 6. If the content is limited, provide additional educational context
144
- 7. Structure your answer clearly with bullet points or numbered lists when appropriate
145
-
146
- Your detailed answer:"""
147
 
148
- self.qa_chain = LLMChain(llm=self.llm, prompt=PromptTemplate(
149
- input_variables=["question", "filled_context"],
150
- template=qa_template
151
- ))
 
 
 
152
 
153
- # Create focused answer prompt template
154
- focused_qa_template = """You are an expert programming tutor. Your task is to provide a comprehensive, educational answer based on the curriculum slide content.
155
 
156
- Slide Content:
157
- {slide_content}
158
 
159
- Student Question: {question}
 
160
 
161
- Instructions:
162
- 1. Analyze the slide content carefully
163
- 2. Provide a detailed, educational explanation
164
- 3. Use examples if the slide contains them
165
- 4. Explain the concept step-by-step
166
- 5. Make sure your answer directly addresses what the student is asking
167
- 6. If the slide content is limited, provide additional educational context
168
- 7. Structure your answer clearly with bullet points or numbered lists when appropriate
169
 
170
- Your detailed answer:"""
 
 
 
 
 
 
 
 
171
 
172
- self.focused_qa_chain = LLMChain(llm=self.llm, prompt=PromptTemplate(
173
- input_variables=["question", "slide_content"],
174
- template=focused_qa_template
175
- ))
176
 
177
- print("βœ… LLM loaded successfully!")
178
- print(f"πŸ” LLM object: {self.llm}")
179
- print(f"πŸ” Content selection chain: {self.content_selection_chain}")
180
- print(f"πŸ” Focused QA chain: {self.focused_qa_chain}")
181
  except Exception as e:
182
- print(f"Warning: Could not load LLM: {e}")
183
- print("Falling back to basic search mode...")
184
  self.llm = None
185
- self.qa_chain = None
186
- self.focused_qa_chain = None
187
  self.content_selection_chain = None
 
188
 
189
  def get_pdf_page_image(self, pdf_path, page_num):
 
190
  try:
191
  doc = fitz.open(pdf_path)
192
  if page_num <= len(doc):
@@ -204,213 +181,115 @@ Your detailed answer:"""
204
  except Exception as e:
205
  print(f"Error rendering PDF page: {str(e)}")
206
  return None
207
-
208
- def get_all_slides(self):
209
- """Get all available slides for display"""
210
- all_slides = []
211
- for filename, pages in self.pdf_pages.items():
212
- for page_num in pages.keys():
213
- img = self.get_pdf_page_image(self.pdf_files[filename], page_num)
214
- if img:
215
- all_slides.append((img, f"{filename} - Page {page_num}"))
216
- return all_slides
217
-
218
- def get_available_slides_text(self):
219
- """Get text representation of available slides for LLM"""
220
- slides_text = []
221
- for filename, pages in self.pdf_pages.items():
222
- for page_num in pages.keys():
223
- slides_text.append(f"{filename} - Page {page_num}")
224
- return "\n".join(slides_text)
225
 
226
  def chat(self, query):
227
- """Comprehensive chat function with LLM-powered content selection and answers"""
228
- # First, try to find relevant curriculum content using vector search
229
- results = self.vector_db.similarity_search(query, k=5) # Get top 5 results for LLM analysis
230
 
231
- curriculum_relevance_score = 0
232
- best_slide_content = ""
233
- best_result = None
234
 
235
- if results:
236
- curriculum_relevance_score = len(results)
237
-
238
- # Debug: Print what we found
239
- print(f"Query: {query}")
240
- print(f"Found {len(results)} relevant results for LLM analysis:")
241
- for i, result in enumerate(results):
242
- print(f" {i+1}. {result.metadata['filename']} - Page {result.metadata['page_number']}")
243
- print(f" Content: {result.page_content[:100]}...")
244
-
245
- # Use LLM to select the most relevant content
246
- if self.content_selection_chain and curriculum_relevance_score > 0:
247
- try:
248
- # Prepare slide contents for LLM analysis
249
- slide_contents = []
250
- for i, result in enumerate(results):
251
- filename = result.metadata['filename']
252
- page_num = result.metadata['page_number']
253
- content = result.page_content[:800] # More content for better analysis
254
- slide_contents.append(f"Slide {i+1} ({filename} - Page {page_num}):\n{content}")
255
-
256
- slide_contents_text = "\n\n".join(slide_contents)
257
-
258
- print(f"πŸ” Using LLM to select most relevant content...")
259
-
260
- # Get LLM's selection
261
- selection_response = self.content_selection_chain.run(
262
- question=query,
263
- slide_contents=slide_contents_text
264
- )
265
-
266
- print(f"LLM Selection Response: {selection_response}")
267
-
268
- # Parse the selection (expecting a number)
269
- try:
270
- # Extract number from response
271
- import re
272
- numbers = re.findall(r'\d+', selection_response)
273
- if numbers:
274
- selected_index = int(numbers[0]) - 1 # Convert to 0-based index
275
- if 0 <= selected_index < len(results):
276
- best_result = results[selected_index]
277
- best_slide_content = best_result.page_content
278
- print(f"βœ… LLM selected slide {selected_index + 1}")
279
- else:
280
- print(f"⚠️ LLM selection out of range: {selected_index + 1}")
281
- # Fallback to first result
282
- best_result = results[0]
283
- best_slide_content = best_result.page_content
284
- else:
285
- print("⚠️ No number found in LLM response, using first result")
286
- best_result = results[0]
287
- best_slide_content = best_result.page_content
288
- except Exception as e:
289
- print(f"Error parsing LLM selection: {e}")
290
- # Fallback to first result
291
- best_result = results[0]
292
- best_slide_content = best_result.page_content
293
-
294
- except Exception as e:
295
- print(f"Error in LLM content selection: {e}")
296
- # Fallback to simple selection
297
- best_result = results[0]
298
- best_slide_content = best_result.page_content
299
- else:
300
- # Fallback to simple selection if no LLM
301
- best_result = results[0]
302
- best_slide_content = best_result.page_content
303
 
304
- # Generate focused LLM answer using the most relevant slide
305
- if self.focused_qa_chain and curriculum_relevance_score > 0:
 
 
 
306
  try:
307
- print(f"πŸ” Calling LLM with question: {query}")
308
- print(f"πŸ” LLM available: {self.focused_qa_chain is not None}")
 
 
 
 
 
309
 
310
- answer = self.focused_qa_chain.run(question=query, slide_content=best_slide_content)
311
 
312
- print(f"LLM Response: {answer[:200]}...")
313
 
314
- # Clean up the answer (Claude is cleaner, but just in case)
315
- answer = answer.strip()
 
 
 
316
 
317
- # Remove any prompt artifacts
318
- if answer.startswith("Answer:"):
319
- answer = answer[7:].strip()
320
- if answer.startswith("Your detailed answer:"):
321
- answer = answer[20:].strip()
322
 
323
- # Check if the answer is too short, generic, or poor quality
324
- if (len(answer.strip()) < 100 or
325
- answer.lower().startswith("how does that work") or
326
- "loops" in query.lower() and "loop" not in answer.lower() or
327
- answer.strip() == query.strip()):
328
-
329
- # Generate a comprehensive educational answer
330
- if "loop" in query.lower():
331
- if "for loop" in query.lower():
332
- answer = f"Based on the curriculum slide:\n\n{best_slide_content}\n\n**For Loops** are a specific type of loop in programming that allow you to iterate over a sequence (like a range of numbers) a predetermined number of times. They are different from while loops and are particularly useful when you know exactly how many times you want to repeat an action.\n\nKey characteristics of for loops:\n- They use a counter variable\n- They have a defined start, end, and increment\n- They are perfect for iterating through lists, ranges, or any sequence\n- They are more structured than while loops"
333
  else:
334
- answer = f"Based on the curriculum slide:\n\n{best_slide_content}\n\n**Loops** are fundamental programming constructs that allow you to repeat code multiple times without having to write the same code repeatedly. They are essential for:\n\n- Processing large amounts of data\n- Repeating actions a specific number of times\n- Iterating through collections like lists and arrays\n- Automating repetitive tasks\n\nThere are different types of loops including for loops, while loops, and do-while loops, each with their own use cases."
 
 
335
  else:
336
- answer = f"Based on the curriculum slide:\n\n{best_slide_content}\n\nThis slide explains the concept clearly. The curriculum content provides the foundation for understanding this programming concept."
337
-
 
 
 
 
 
 
 
338
  except Exception as e:
339
- print(f"Error generating focused answer: {e}")
340
- # Fallback to slide content with explanation
341
- answer = f"Based on the curriculum slide:\n\n{best_slide_content}\n\nThis slide contains the relevant information about your question."
 
 
 
 
342
 
343
- elif self.qa_chain:
344
- # Fallback to general LLM if focused chain fails
 
345
  try:
346
- if curriculum_relevance_score > 0:
347
-
348
- context = "\n\n".join([result.page_content for result in results])
349
- filled_context = f"Curriculum Context:\n{context}\n\nPlease answer based on this curriculum content."
350
- else:
351
-
352
- filled_context = "Note: This question is not covered in the current curriculum. Please provide a general programming answer."
353
-
354
- answer = self.qa_chain.run(question=query, filled_context=filled_context)
355
  answer = answer.strip()
356
-
357
- # Remove any prompt artifacts (Claude is cleaner, but just in case)
358
- if answer.startswith("Answer:"):
359
- answer = answer[7:].strip()
360
- if answer.startswith("Provide a clear, educational answer explaining the concept:"):
361
- answer = answer[58:].strip()
362
-
363
- # Check if the answer is too short
364
- if len(answer.strip()) < 50:
365
- if curriculum_relevance_score > 0:
366
- answer = f"Based on the curriculum content:\n\n{best_slide_content}\n\nThis slide explains the concept clearly."
367
- else:
368
- answer = "I'm sorry, I couldn't generate a proper answer. Please try rephrasing your question."
369
-
370
- # Add warning if not in curriculum
371
- if curriculum_relevance_score == 0:
372
- answer = "πŸ’‘ **Note: This topic isn't covered in your current curriculum, but here's a helpful answer:**\n\n" + answer
373
 
374
  except Exception as e:
375
- print(f"Error generating answer: {e}")
376
-
377
- if curriculum_relevance_score > 0:
378
- answer = f"Based on the curriculum slide:\n\n{best_slide_content}\n\nThis slide contains the relevant information about your question."
379
- else:
380
- answer = "I'm sorry, I couldn't generate an answer at the moment. Please try rephrasing your question."
381
  else:
382
- # If no LLM available
383
- if curriculum_relevance_score > 0:
384
- answer = f"Based on the curriculum slide:\n\n{best_slide_content}\n\n*Note: AI generation is not available, but here's the relevant curriculum content.*"
385
- else:
386
- answer = "I couldn't find relevant content in the curriculum for this question. Please try rephrasing or ask about a different programming topic."
387
 
388
- # Get the most relevant slide and its neighboring pages
389
  relevant_slides = []
390
- if curriculum_relevance_score > 0 and best_result:
391
- # Use the LLM-selected result
392
- filename = best_result.metadata["filename"]
393
- page_number = best_result.metadata["page_number"]
394
 
395
- # Get the specific PDF and its pages
396
  if filename in self.pdf_files:
397
  pdf_path = self.pdf_files[filename]
398
  doc = fitz.open(pdf_path)
399
  total_pages = len(doc)
400
  doc.close()
401
 
402
- # Use the LLM-selected page as the target
403
- target_page = page_number
404
-
405
- # Get the target page and neighboring pages (2 before, 2 after)
406
- start_page = max(1, target_page - 2)
407
- end_page = min(total_pages, target_page + 2)
408
 
409
  for page_num in range(start_page, end_page + 1):
410
  img = self.get_pdf_page_image(pdf_path, page_num)
411
  if img:
412
- if page_num == target_page:
413
- # Highlight the most relevant page
414
  label = f"πŸ“Œ {filename} - Page {page_num} (Most Relevant)"
415
  else:
416
  label = f"{filename} - Page {page_num}"
@@ -419,52 +298,30 @@ Your detailed answer:"""
419
  recommended_slide = relevant_slides[0][0] if relevant_slides else None
420
  recommended_label = relevant_slides[0][1] if relevant_slides else None
421
  else:
422
- # Fallback if filename not found
423
  recommended_slide = None
424
  recommended_label = None
425
  else:
426
- # If no curriculum content, provide a helpful response
427
- relevant_slides = []
428
  recommended_slide = None
429
  recommended_label = None
430
 
431
- return answer, recommended_slide, recommended_label, relevant_slides
432
 
433
  # --- Gradio UI ---
434
- chatbot = CurriculumChatbot(fast_mode=False) # Enable AI mode by default
435
 
436
- def gradio_chat(query, use_ai=True):
437
- # Temporarily switch modes based on user preference
438
- original_fast_mode = chatbot.fast_mode
439
- chatbot.fast_mode = not use_ai
440
-
441
- try:
442
- answer, recommended_slide, recommended_label, relevant_slides = chatbot.chat(query)
443
- finally:
444
- # Restore original mode
445
- chatbot.fast_mode = original_fast_mode
446
-
447
- # Use the relevant slides (specific PDF with neighboring pages)
448
- gallery_items = relevant_slides if relevant_slides else []
449
-
450
- return answer, gallery_items
451
 
452
- with gr.Blocks(title="Inclusive World Curriculum Assistant", theme=gr.themes.Soft()) as demo:
453
- gr.Markdown("# πŸ€– Inclusive World Curriculum Assistant\nYour AI programming tutor with curriculum-based answers and slide navigation!")
454
 
455
  with gr.Row():
456
  # Left Column - Chatbot Interface
457
  with gr.Column(scale=1):
458
  gr.Markdown("### πŸ’¬ Chatbot")
459
- gr.Markdown("**What questions do you have?**")
460
-
461
- # AI Mode Toggle
462
- with gr.Row():
463
- use_ai = gr.Checkbox(
464
- label="πŸ€– Enable AI Responses",
465
- value=True,
466
- info="Toggle AI-generated answers on/off"
467
- )
468
 
469
  question = gr.Textbox(
470
  label="Question Input",
@@ -472,11 +329,11 @@ with gr.Blocks(title="Inclusive World Curriculum Assistant", theme=gr.themes.Sof
472
  lines=3
473
  )
474
  submit = gr.Button("πŸ€– Ask AI", variant="primary", size="lg")
475
- answer = gr.Markdown(label="LLM Generated Output")
476
 
477
  # Right Column - Slides Display
478
  with gr.Column(scale=1):
479
- gr.Markdown("### πŸ“„ Most Similar Slides")
480
  gallery = gr.Gallery(
481
  label="Curriculum Slides",
482
  columns=1,
@@ -487,8 +344,8 @@ with gr.Blocks(title="Inclusive World Curriculum Assistant", theme=gr.themes.Sof
487
  )
488
 
489
  # Event handlers
490
- submit.click(fn=gradio_chat, inputs=[question, use_ai], outputs=[answer, gallery])
491
- question.submit(fn=gradio_chat, inputs=[question, use_ai], outputs=[answer, gallery])
492
 
493
  if __name__ == "__main__":
494
- demo.launch()
 
4
  import fitz # PyMuPDF
5
  from langchain_community.embeddings import HuggingFaceEmbeddings
6
  from langchain_community.vectorstores import Chroma
 
7
  from langchain.prompts import PromptTemplate
8
  from langchain.chains import LLMChain
9
  import anthropic
 
11
  from PIL import Image
12
  import io
13
  import re
14
+ from dotenv import load_dotenv
15
 
16
+ # Load environment variables from .env file
17
+ load_dotenv()
18
 
19
+ # --- LLM-Powered Curriculum Assistant ---
20
+
21
+ class LLMCurriculumAssistant:
22
+ def __init__(self, slides_dir="Slides"):
23
  self.pdf_pages = {} # {filename: {page_num: text}}
24
  self.pdf_files = {} # {filename: path}
25
  self.chunks = []
 
27
  self.vector_db = None
28
  self.embeddings = None
29
  self.llm = None
30
+ self.content_selection_chain = None
31
+ self.answer_chain = None
32
+
33
+ # Setup
 
34
  self._process_pdfs(slides_dir)
35
  self._build_vector_db()
36
+ self._setup_llm()
37
+
 
 
 
38
  def _process_pdfs(self, slides_dir):
39
+ """Process PDFs and extract text"""
40
  slides_path = Path(slides_dir)
41
  pdf_files = list(slides_path.glob("*.pdf"))
42
+
43
  for pdf_file in pdf_files:
44
  self.pdf_files[pdf_file.name] = str(pdf_file)
45
  doc = fitz.open(str(pdf_file))
46
  pages = {}
47
+
48
  for page_num in range(len(doc)):
49
  page = doc[page_num]
50
  text = page.get_text()
51
  if text.strip():
52
  pages[page_num + 1] = text.strip()
53
+
54
  self.pdf_pages[pdf_file.name] = pages
55
  doc.close()
56
+
57
  # Add each page as a chunk
58
  for page_num, text in pages.items():
59
  self.chunks.append(text)
 
61
  "filename": pdf_file.name,
62
  "page_number": page_num
63
  })
64
+
65
+ print(f"βœ… Processed {len(pdf_files)} PDF files with {len(self.chunks)} total pages")
66
 
67
  def _build_vector_db(self):
68
+ """Build vector database for semantic search"""
69
  self.embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
70
  self.vector_db = Chroma.from_texts(
71
  texts=self.chunks,
 
73
  metadatas=self.chunk_metadata,
74
  persist_directory="./chroma_db"
75
  )
76
+ print("βœ… Vector database built successfully")
77
+
78
  def _setup_llm(self):
79
+ """Setup Claude LLM"""
80
  try:
81
+ # Initialize Claude client
 
 
 
 
 
 
82
  self.anthropic_client = anthropic.Anthropic(
83
+ api_key=os.environ.get("ANTHROPIC_KEY")
84
  )
85
 
86
+ # Create LLM wrapper for LangChain compatibility
87
  class ClaudeLLM:
88
  def __init__(self, client):
89
  self.client = client
 
103
 
104
  self.llm = ClaudeLLM(self.anthropic_client)
105
 
106
+ # Create content selection prompt
107
+ content_selection_template = """Hi! I'm helping a student find the best curriculum slide for their question.
108
 
109
+ The student asked: "{question}"
110
 
111
+ Here are some slides that might be relevant:
112
  {slide_contents}
113
 
114
+ Could you help me pick the slide that best answers their specific question? Look for:
115
+ - Slides that specifically mention what they're asking about
116
+ - Slides with clear explanations and examples
117
+ - Slides that match the exact terms they used (like "for loops" vs just "loops")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
 
119
+ Just respond with the slide number (1, 2, 3, etc.) that you think is most helpful. If none really fit, say "0".
 
120
 
121
+ Thanks! Slide number:"""
 
 
 
 
 
 
 
 
 
 
 
122
 
123
+ self.content_selection_chain = LLMChain(
124
+ llm=self.llm,
125
+ prompt=PromptTemplate(
126
+ input_variables=["question", "slide_contents"],
127
+ template=content_selection_template
128
+ )
129
+ )
130
 
131
+ # Create answer generation prompt
132
+ answer_template = """Hey there! I'm helping a student understand a programming concept. They asked:
133
 
134
+ "{question}"
 
135
 
136
+ Here's what the curriculum slide says about it:
137
+ {slide_content}
138
 
139
+ Could you help me explain this to them in a friendly, educational way? I'd like you to:
140
+ - Break it down in simple terms
141
+ - Use examples if the slide has them
142
+ - Make it step-by-step and easy to follow
143
+ - Add some helpful context if the slide is brief
144
+ - Use bullet points or lists to make it clear
145
+ - Make sure your answer directly addresses what they asked
 
146
 
147
+ Thanks for your help! Here's what I'd tell the student:"""
148
+
149
+ self.answer_chain = LLMChain(
150
+ llm=self.llm,
151
+ prompt=PromptTemplate(
152
+ input_variables=["question", "slide_content"],
153
+ template=answer_template
154
+ )
155
+ )
156
 
157
+ print("βœ… LLM setup successful!")
 
 
 
158
 
 
 
 
 
159
  except Exception as e:
160
+ print(f"❌ Error setting up LLM: {e}")
 
161
  self.llm = None
 
 
162
  self.content_selection_chain = None
163
+ self.answer_chain = None
164
 
165
  def get_pdf_page_image(self, pdf_path, page_num):
166
+ """Get PDF page as image"""
167
  try:
168
  doc = fitz.open(pdf_path)
169
  if page_num <= len(doc):
 
181
  except Exception as e:
182
  print(f"Error rendering PDF page: {str(e)}")
183
  return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184
 
185
  def chat(self, query):
186
+ """Main chat function with LLM-powered content selection and answer generation"""
187
+ print(f"\nπŸ” Processing query: {query}")
 
188
 
189
+ # Step 1: Vector search to find relevant content
190
+ results = self.vector_db.similarity_search(query, k=5)
 
191
 
192
+ if not results:
193
+ return "I couldn't find any relevant content in the curriculum for your question.", [], None, None
194
+
195
+ print(f"πŸ“š Found {len(results)} relevant slides from vector search")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
196
 
197
+ # Step 2: LLM content selection
198
+ selected_content = None
199
+ selected_result = None
200
+
201
+ if self.content_selection_chain:
202
  try:
203
+ # Prepare slide contents for LLM analysis
204
+ slide_contents = []
205
+ for i, result in enumerate(results):
206
+ filename = result.metadata['filename']
207
+ page_num = result.metadata['page_number']
208
+ content = result.page_content[:800]
209
+ slide_contents.append(f"Slide {i+1} ({filename} - Page {page_num}):\n{content}")
210
 
211
+ slide_contents_text = "\n\n".join(slide_contents)
212
 
213
+ print("πŸ€– Using LLM to select most relevant content...")
214
 
215
+ # Get LLM's selection
216
+ selection_response = self.content_selection_chain.run(
217
+ question=query,
218
+ slide_contents=slide_contents_text
219
+ )
220
 
221
+ print(f"LLM Selection Response: {selection_response}")
 
 
 
 
222
 
223
+ # Parse the selection
224
+ try:
225
+ numbers = re.findall(r'\d+', selection_response)
226
+ if numbers:
227
+ selected_index = int(numbers[0]) - 1
228
+ if 0 <= selected_index < len(results):
229
+ selected_result = results[selected_index]
230
+ selected_content = selected_result.page_content
231
+ print(f"βœ… LLM selected slide {selected_index + 1}")
 
232
  else:
233
+ print(f"⚠️ LLM selection out of range: {selected_index + 1}")
234
+ selected_result = results[0]
235
+ selected_content = selected_result.page_content
236
  else:
237
+ print("⚠️ No number found in LLM response, using first result")
238
+ selected_result = results[0]
239
+ selected_content = selected_result.page_content
240
+
241
+ except Exception as e:
242
+ print(f"Error parsing LLM selection: {e}")
243
+ selected_result = results[0]
244
+ selected_content = selected_result.page_content
245
+
246
  except Exception as e:
247
+ print(f"Error in LLM content selection: {e}")
248
+ selected_result = results[0]
249
+ selected_content = selected_result.page_content
250
+ else:
251
+ # Fallback to first result
252
+ selected_result = results[0]
253
+ selected_content = selected_result.page_content
254
 
255
+ # Step 3: LLM answer generation
256
+ answer = ""
257
+ if self.answer_chain and selected_content:
258
  try:
259
+ print("πŸ€– Generating LLM answer...")
260
+ answer = self.answer_chain.run(
261
+ question=query,
262
+ slide_content=selected_content
263
+ )
 
 
 
 
264
  answer = answer.strip()
265
+ print(f"βœ… LLM answer generated: {answer[:100]}...")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
266
 
267
  except Exception as e:
268
+ print(f"Error generating LLM answer: {e}")
269
+ answer = f"Based on the curriculum slide:\n\n{selected_content}\n\nThis slide contains relevant information about your question."
 
 
 
 
270
  else:
271
+ answer = f"Based on the curriculum slide:\n\n{selected_content}\n\nThis slide contains relevant information about your question."
 
 
 
 
272
 
273
+ # Step 4: Get relevant slides for display
274
  relevant_slides = []
275
+ if selected_result:
276
+ filename = selected_result.metadata["filename"]
277
+ page_number = selected_result.metadata["page_number"]
 
278
 
 
279
  if filename in self.pdf_files:
280
  pdf_path = self.pdf_files[filename]
281
  doc = fitz.open(pdf_path)
282
  total_pages = len(doc)
283
  doc.close()
284
 
285
+ # Get the selected page and neighboring pages
286
+ start_page = max(1, page_number - 2)
287
+ end_page = min(total_pages, page_number + 2)
 
 
 
288
 
289
  for page_num in range(start_page, end_page + 1):
290
  img = self.get_pdf_page_image(pdf_path, page_num)
291
  if img:
292
+ if page_num == page_number:
 
293
  label = f"πŸ“Œ {filename} - Page {page_num} (Most Relevant)"
294
  else:
295
  label = f"{filename} - Page {page_num}"
 
298
  recommended_slide = relevant_slides[0][0] if relevant_slides else None
299
  recommended_label = relevant_slides[0][1] if relevant_slides else None
300
  else:
 
301
  recommended_slide = None
302
  recommended_label = None
303
  else:
 
 
304
  recommended_slide = None
305
  recommended_label = None
306
 
307
+ return answer, relevant_slides, recommended_slide, recommended_label
308
 
309
  # --- Gradio UI ---
310
+ assistant = LLMCurriculumAssistant()
311
 
312
+ def gradio_chat(query):
313
+ """Gradio chat interface"""
314
+ answer, relevant_slides, recommended_slide, recommended_label = assistant.chat(query)
315
+ return answer, relevant_slides
 
 
 
 
 
 
 
 
 
 
 
316
 
317
+ with gr.Blocks(title="LLM Curriculum Assistant", theme=gr.themes.Soft()) as demo:
318
+ gr.Markdown("# πŸ€– LLM Curriculum Assistant\nYour AI programming tutor with LLM-powered content selection and answers!")
319
 
320
  with gr.Row():
321
  # Left Column - Chatbot Interface
322
  with gr.Column(scale=1):
323
  gr.Markdown("### πŸ’¬ Chatbot")
324
+ gr.Markdown("**Ask questions about programming concepts:**")
 
 
 
 
 
 
 
 
325
 
326
  question = gr.Textbox(
327
  label="Question Input",
 
329
  lines=3
330
  )
331
  submit = gr.Button("πŸ€– Ask AI", variant="primary", size="lg")
332
+ answer = gr.Markdown(label="LLM Generated Answer")
333
 
334
  # Right Column - Slides Display
335
  with gr.Column(scale=1):
336
+ gr.Markdown("### πŸ“„ Most Relevant Slides")
337
  gallery = gr.Gallery(
338
  label="Curriculum Slides",
339
  columns=1,
 
344
  )
345
 
346
  # Event handlers
347
+ submit.click(fn=gradio_chat, inputs=[question], outputs=[answer, gallery])
348
+ question.submit(fn=gradio_chat, inputs=[question], outputs=[answer, gallery])
349
 
350
  if __name__ == "__main__":
351
+ demo.launch()