File size: 13,775 Bytes
e5b03ae
 
 
 
1ff95ec
 
be4a77c
eea9911
d3a17e2
7bcf298
f7c1f61
 
4a97432
00a2c28
e5b03ae
00a2c28
 
55a16ef
00a2c28
 
 
 
7bcf298
 
55a16ef
 
e5b03ae
 
be4a77c
00a2c28
 
 
 
55a16ef
 
00a2c28
 
55a16ef
00a2c28
55a16ef
 
00a2c28
55a16ef
 
 
e5b03ae
00a2c28
e5b03ae
 
 
55a16ef
e5b03ae
00a2c28
55a16ef
e5b03ae
00a2c28
55a16ef
 
 
 
 
 
 
00a2c28
 
55a16ef
 
00a2c28
55a16ef
 
 
 
 
 
 
00a2c28
 
be4a77c
00a2c28
be4a77c
00a2c28
d3a17e2
00a2c28
be4a77c
ce130ce
00a2c28
 
eea9911
00a2c28
eea9911
00a2c28
eea9911
 
00a2c28
 
 
 
ce130ce
00a2c28
7bcf298
00a2c28
7bcf298
32ddee3
 
 
00a2c28
4a97432
00a2c28
 
7bcf298
00a2c28
d92cf2c
00a2c28
 
b7840f2
00a2c28
 
 
 
 
 
 
7bcf298
00a2c28
 
32ddee3
 
 
00a2c28
7bcf298
00a2c28
7bcf298
be4a77c
00a2c28
32ddee3
 
 
55a16ef
 
00a2c28
f7c1f61
 
 
55a16ef
 
f7c1f61
 
 
 
 
 
a9f34af
f7c1f61
 
 
 
 
e5b03ae
4a97432
00a2c28
 
7caf6aa
00a2c28
 
eea9911
00a2c28
 
 
 
fc75dd1
00a2c28
 
 
 
32ddee3
be4a77c
00a2c28
 
 
 
 
 
 
eea9911
00a2c28
fc75dd1
00a2c28
eea9911
32ddee3
 
00a2c28
 
 
7bcf298
32ddee3
 
 
 
 
 
 
 
 
00a2c28
7bcf298
00a2c28
 
 
 
 
 
 
 
 
b7840f2
00a2c28
 
 
b7840f2
00a2c28
 
 
 
 
 
 
 
 
ce130ce
00a2c28
 
 
 
 
 
 
ce130ce
00a2c28
 
32ddee3
ce130ce
00a2c28
32ddee3
 
 
00a2c28
 
 
32ddee3
 
 
 
 
 
 
 
 
 
00a2c28
7bcf298
be4a77c
00a2c28
 
be4a77c
00a2c28
be4a77c
00a2c28
7bcf298
00a2c28
 
 
fc75dd1
ce130ce
 
 
 
 
 
00a2c28
 
 
ce130ce
 
 
 
00a2c28
ce130ce
 
 
 
 
 
 
 
 
 
 
 
 
799c93c
00a2c28
799c93c
7bcf298
00a2c28
e5b03ae
00a2c28
 
 
 
e5b03ae
00a2c28
 
4a97432
e5b03ae
7bcf298
4a97432
 
00a2c28
0fbf85b
4a97432
 
7bcf298
4a97432
 
 
00a2c28
7bcf298
 
 
00a2c28
7bcf298
 
 
 
 
 
 
 
4a97432
 
00a2c28
 
e5b03ae
 
00a2c28
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
338
339
340
341
342
343
344
345
import gradio as gr
import os
from pathlib import Path
import fitz  # PyMuPDF
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain_community.vectorstores import Chroma
from langchain.prompts import PromptTemplate
from langchain.chains import LLMChain
import anthropic
import base64
from PIL import Image
import io
import re
from dotenv import load_dotenv

# Load environment variables from .env file
load_dotenv()

# --- LLM-Powered Curriculum Assistant ---

class LLMCurriculumAssistant:
    def __init__(self, slides_dir="Slides"):
        self.pdf_pages = {}  # {filename: {page_num: text}}
        self.pdf_files = {}  # {filename: path}
        self.chunks = []
        self.chunk_metadata = []
        self.vector_db = None
        self.embeddings = None
        self.llm = None
        self.content_selection_chain = None
        self.answer_chain = None
        
        # Setup
        self._process_pdfs(slides_dir)
        self._build_vector_db()
        self._setup_llm()
        
    def _process_pdfs(self, slides_dir):
        """Process PDFs and extract text"""
        slides_path = Path(slides_dir)
        pdf_files = list(slides_path.glob("*.pdf"))
        
        for pdf_file in pdf_files:
            self.pdf_files[pdf_file.name] = str(pdf_file)
            doc = fitz.open(str(pdf_file))
            pages = {}
            
            for page_num in range(len(doc)):
                page = doc[page_num]
                text = page.get_text()
                if text.strip():
                    pages[page_num + 1] = text.strip()
            
            self.pdf_pages[pdf_file.name] = pages
            doc.close()
            
            # Add each page as a chunk
            for page_num, text in pages.items():
                self.chunks.append(text)
                self.chunk_metadata.append({
                    "filename": pdf_file.name,
                    "page_number": page_num
                })
        
        print(f"βœ… Processed {len(pdf_files)} PDF files with {len(self.chunks)} total pages")

    def _build_vector_db(self):
        """Build vector database for semantic search"""
        self.embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
        self.vector_db = Chroma.from_texts(
            texts=self.chunks,
            embedding=self.embeddings,
            metadatas=self.chunk_metadata,
            persist_directory="./chroma_db"
        )
        print("βœ… Vector database built successfully")

    def _setup_llm(self):
        """Setup Claude LLM"""
        try:
            # Initialize Claude client
            self.anthropic_client = anthropic.Anthropic(
                api_key=os.environ.get("ANTHROPIC_KEY")
            )
            
            # Create content selection prompt
            content_selection_template = """Hi! I'm helping a student find the best curriculum slide for their question. 

The student asked: "{question}"

Here are some slides that might be relevant:
{slide_contents}

Could you help me pick the slide that best answers their specific question? Look for:
- Slides that specifically mention what they're asking about
- Slides with clear explanations and examples
- Slides that match the exact terms they used (like "for loops" vs just "loops")

Just respond with the slide number (1, 2, 3, etc.) that you think is most helpful. If none really fit, say "0".

Thanks! Slide number:"""
            
            self.content_selection_prompt = PromptTemplate(
                input_variables=["question", "slide_contents"],
                template=content_selection_template
            )
            
            # Create answer generation prompt
            answer_template = """Hey there! I'm helping a student understand a programming concept. They asked:

"{question}"

Here's what the curriculum slide says about it:
{slide_content}

Could you help me explain this to them in a friendly, educational way? I'd like you to:
- Break it down in simple terms
- Use examples if the slide has them
- Make it step-by-step and easy to follow
- Add some helpful context if the slide is brief
- Use bullet points or lists to make it clear
- Make sure your answer directly addresses what they asked

Thanks for your help! Here's what I'd tell the student:"""
            
            self.answer_prompt = PromptTemplate(
                input_variables=["question", "slide_content"],
                template=answer_template
            )
            
            print("βœ… LLM setup successful!")
            
        except Exception as e:
            print(f"❌ Error setting up LLM: {e}")
            self.anthropic_client = None
            self.content_selection_prompt = None
            self.answer_prompt = None

    def get_pdf_page_image(self, pdf_path, page_num):
        """Get PDF page as image"""
        try:
            doc = fitz.open(pdf_path)
            if page_num <= len(doc):
                page = doc[page_num - 1]
                mat = fitz.Matrix(1.5, 1.5)
                pix = page.get_pixmap(matrix=mat)
                img_data = pix.tobytes("png")
                img = Image.open(io.BytesIO(img_data))
                if img.mode != 'RGB':
                    img = img.convert('RGB')
                doc.close()
                return img
            doc.close()
            return None
        except Exception as e:
            print(f"Error rendering PDF page: {str(e)}")
            return None

    def chat(self, query):
        """Main chat function with LLM-powered content selection and answer generation"""
        print(f"\nπŸ” Processing query: {query}")
        
        # Step 1: Vector search to find relevant content
        results = self.vector_db.similarity_search(query, k=5)
        
        if not results:
            return "I couldn't find any relevant content in the curriculum for your question.", [], None, None
        
        print(f"πŸ“š Found {len(results)} relevant slides from vector search")
        
        # Step 2: LLM content selection
        selected_content = None
        selected_result = None
        
        if self.anthropic_client and self.content_selection_prompt:
            try:
                # Prepare slide contents for LLM analysis
                slide_contents = []
                for i, result in enumerate(results):
                    filename = result.metadata['filename']
                    page_num = result.metadata['page_number']
                    content = result.page_content[:800]
                    slide_contents.append(f"Slide {i+1} ({filename} - Page {page_num}):\n{content}")
                
                slide_contents_text = "\n\n".join(slide_contents)
                
                print("πŸ€– Using LLM to select most relevant content...")
                
                # Format the prompt
                prompt = self.content_selection_prompt.format(
                    question=query, 
                    slide_contents=slide_contents_text
                )
                
                # Get LLM's selection
                response = self.anthropic_client.messages.create(
                    model="claude-3-5-haiku-20241022",
                    max_tokens=1500,
                    temperature=0.7,
                    messages=[{"role": "user", "content": prompt}]
                )
                
                selection_response = response.content[0].text
                print(f"LLM Selection Response: {selection_response}")
                
                # Parse the selection
                try:
                    numbers = re.findall(r'\d+', selection_response)
                    if numbers:
                        selected_index = int(numbers[0]) - 1
                        if 0 <= selected_index < len(results):
                            selected_result = results[selected_index]
                            selected_content = selected_result.page_content
                            print(f"βœ… LLM selected slide {selected_index + 1}")
                        else:
                            print(f"⚠️ LLM selection out of range: {selected_index + 1}")
                            selected_result = results[0]
                            selected_content = selected_result.page_content
                    else:
                        print("⚠️ No number found in LLM response, using first result")
                        selected_result = results[0]
                        selected_content = selected_result.page_content
                        
                except Exception as e:
                    print(f"Error parsing LLM selection: {e}")
                    selected_result = results[0]
                    selected_content = selected_result.page_content
                    
            except Exception as e:
                print(f"Error in LLM content selection: {e}")
                selected_result = results[0]
                selected_content = selected_result.page_content
        else:
            # Fallback to first result
            selected_result = results[0]
            selected_content = selected_result.page_content
        
        # Step 3: LLM answer generation
        answer = ""
        if self.anthropic_client and self.answer_prompt and selected_content:
            try:
                print("πŸ€– Generating LLM answer...")
                
                # Format the prompt
                prompt = self.answer_prompt.format(
                    question=query, 
                    slide_content=selected_content
                )
                
                # Get LLM's answer
                response = self.anthropic_client.messages.create(
                    model="claude-3-5-haiku-20241022",
                    max_tokens=1500,
                    temperature=0.7,
                    messages=[{"role": "user", "content": prompt}]
                )
                
                answer = response.content[0].text.strip()
                print(f"βœ… LLM answer generated: {answer[:100]}...")
                
            except Exception as e:
                print(f"Error generating LLM answer: {e}")
                answer = f"Based on the curriculum slide:\n\n{selected_content}\n\nThis slide contains relevant information about your question."
        else:
            answer = f"Based on the curriculum slide:\n\n{selected_content}\n\nThis slide contains relevant information about your question."
        
        # Step 4: Get relevant slides for display
        relevant_slides = []
        if selected_result:
            filename = selected_result.metadata["filename"]
            page_number = selected_result.metadata["page_number"]
            
            if filename in self.pdf_files:
                pdf_path = self.pdf_files[filename]
                doc = fitz.open(pdf_path)
                total_pages = len(doc)
                doc.close()
                
                # Get the selected page and neighboring pages
                start_page = max(1, page_number - 2)
                end_page = min(total_pages, page_number + 2)
                
                for page_num in range(start_page, end_page + 1):
                    img = self.get_pdf_page_image(pdf_path, page_num)
                    if img:
                        if page_num == page_number:
                            label = f"πŸ“Œ {filename} - Page {page_num} (Most Relevant)"
                        else:
                            label = f"{filename} - Page {page_num}"
                        relevant_slides.append((img, label))
                
                recommended_slide = relevant_slides[0][0] if relevant_slides else None
                recommended_label = relevant_slides[0][1] if relevant_slides else None
            else:
                recommended_slide = None
                recommended_label = None
        else:
            recommended_slide = None
            recommended_label = None
        
        return answer, relevant_slides, recommended_slide, recommended_label

# --- Gradio UI ---
assistant = LLMCurriculumAssistant()

def gradio_chat(query):
    """Gradio chat interface"""
    answer, relevant_slides, recommended_slide, recommended_label = assistant.chat(query)
    return answer, relevant_slides

with gr.Blocks(title="LLM Curriculum Assistant", theme=gr.themes.Soft()) as demo:
    gr.Markdown("# πŸ€– LLM Curriculum Assistant\nYour AI programming tutor with LLM-powered content selection and answers!")
    
    with gr.Row():
        # Left Column - Chatbot Interface
        with gr.Column(scale=1):
            gr.Markdown("### πŸ’¬ Chatbot")
            gr.Markdown("**Ask questions about programming concepts:**")
            
            question = gr.Textbox(
                label="Question Input", 
                placeholder="e.g., What are for loops? How do variables work? Explain functions...", 
                lines=3
            )
            submit = gr.Button("πŸ€– Ask AI", variant="primary", size="lg")
            answer = gr.Markdown(label="LLM Generated Answer")
        
        # Right Column - Slides Display
        with gr.Column(scale=1):
            gr.Markdown("### πŸ“„ Most Relevant Slides")
            gallery = gr.Gallery(
                label="Curriculum Slides", 
                columns=1, 
                rows=3, 
                height="600px", 
                object_fit="contain",
                show_label=False
            )
    
    # Event handlers
    submit.click(fn=gradio_chat, inputs=[question], outputs=[answer, gallery])
    question.submit(fn=gradio_chat, inputs=[question], outputs=[answer, gallery])

if __name__ == "__main__":
    demo.launch()