File size: 13,506 Bytes
3784462
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""

Gradio UI for Context Thread Agent.

Interactive interface for uploading notebooks and asking questions.

"""

import gradio as gr
import json
import tempfile
from pathlib import Path
from typing import Tuple, List, Dict
from src.models import Cell

from src.parser import NotebookParser
from src.dependencies import ContextThreadBuilder
from src.indexing import FAISSIndexer
from src.retrieval import RetrievalEngine, ContextBuilder
from src.reasoning import ContextualAnsweringSystem
from src.intent import ContextThreadEnricher
from src.groq_integration import GroqReasoningEngine
import pandas as pd


class NotebookAgentUI:
    """Gradio UI for the Context Thread Agent."""
    
    def __init__(self):
        self.current_thread = None
        self.current_indexer = None
        self.current_engine = None
        self.answering_system = None
        self.conversation_history = []  # To maintain context across questions
    
    def load_notebook(self, notebook_file) -> str:
        """Load and index a notebook or Excel file."""
        try:
            if notebook_file is None:
                return "❌ No file provided"
            
            # Save uploaded file temporarily
            with tempfile.NamedTemporaryFile(suffix=Path(notebook_file).suffix if isinstance(notebook_file, str) else ".ipynb", delete=False) as f:
                if isinstance(notebook_file, str):
                    # File path
                    f.write(open(notebook_file, 'rb').read())
                else:
                    # Uploaded file
                    f.write(notebook_file.read())
                temp_path = f.name
            
            file_ext = Path(temp_path).suffix.lower()
            
            if file_ext == '.ipynb':
                # Parse notebook
                parser = NotebookParser()
                result = parser.parse_file(temp_path)
                cells = result['cells']
            elif file_ext in ['.xlsx', '.xls']:
                # Convert Excel to pseudo-cells
                cells = self._excel_to_cells(temp_path)
            else:
                return "❌ Unsupported file type. Please upload .ipynb or .xlsx/.xls"
            
            # Build context thread
            builder = ContextThreadBuilder(
                notebook_name=Path(temp_path).stem,
                thread_id=f"thread_{id(self)}"
            )
            builder.add_cells(cells)
            self.current_thread = builder.build()
            
            # Enrich with intents (heuristic, not LLM for speed)
            enricher = ContextThreadEnricher(infer_intents=True)
            self.current_thread = enricher.enrich(self.current_thread)
            
            # Index
            self.current_indexer = FAISSIndexer()
            self.current_indexer.add_multiple(self.current_thread.units)
            
            # Setup retrieval and reasoning
            self.current_engine = RetrievalEngine(self.current_thread, self.current_indexer)
            self.answering_system = ContextualAnsweringSystem(self.current_engine)
            
            # Cleanup
            Path(temp_path).unlink()
            
            return f"""

✅ **File Loaded Successfully!**



**Stats:**

- Total cells/sections: {len(cells)}

- Code cells: {sum(1 for c in cells if c.cell_type == 'code')}

- Markdown cells: {sum(1 for c in cells if c.cell_type == 'markdown')}

- Indexed: ✓



Ready to ask questions! 🎯

"""
        
        except Exception as e:
            return f"❌ Error loading file: {str(e)}"
    
    def generate_keypoints(self) -> str:
        """Generate key points summary of the notebook."""
        if not self.answering_system:
            return "❌ No notebook loaded."
        
        try:
            # Use the reasoning engine to summarize keypoints
            query = "Summarize the key points and main insights from this notebook."
            response = self.answering_system.answer_question(query, top_k=10)  # More context for summary
            
            keypoints = f"**Key Points Summary:**\n\n{response.answer}"
            return keypoints
        except Exception as e:
            return f"❌ Error generating keypoints: {str(e)}"
    
    def get_notebook_display(self) -> str:
        """Get formatted notebook content for display."""
        if not self.current_thread:
            return "No notebook loaded."
        
        display = ""
        for i, unit in enumerate(self.current_thread.units, 1):
            display += f"### Cell {i}: {unit.cell.cell_id} [{unit.cell.cell_type}]\n"
            if unit.intent and unit.intent != "[Pending intent inference]":
                display += f"**Intent:** {unit.intent}\n\n"
            display += f"```\n{unit.cell.source}\n```\n\n"
            if unit.cell.outputs:
                display += "**Output:**\n"
                for output in unit.cell.outputs:
                    if 'text' in output:
                        display += f"```\n{output['text']}\n```\n"
                    elif 'data' in output and 'text/plain' in output['data']:
                        display += f"```\n{output['data']['text/plain']}\n```\n"
                display += "\n"
        
        return display
    
    def ask_question(self, query: str) -> Tuple[str, str, str]:
        """Answer a question about the notebook."""
        if not self.answering_system:
            return (
                "❌ No notebook loaded. Please upload a notebook first.",
                "",
                ""
            )
        
        try:
            # Add to conversation history
            self.conversation_history.append({"role": "user", "content": query})
            
            # Get answer
            response = self.answering_system.answer_question(query, top_k=5)
            
            # Add assistant response to history
            self.conversation_history.append({"role": "assistant", "content": response.answer})
            
            # Format answer
            answer_text = f"**Answer:**\n\n{response.answer}"
            
            # Format citations
            if response.citations:
                citations_text = "**Citations:**\n\n"
                for i, citation in enumerate(response.citations, 1):
                    citations_text += f"{i}. **{citation.cell_id}** [{citation.cell_type}]\n"
                    if citation.intent:
                        citations_text += f"   *Intent: {citation.intent}*\n"
                    citations_text += f"   ```\n{citation.content_snippet}\n   ```\n\n"
            else:
                citations_text = "*No specific cells cited*"
            
            # Format context
            context_text = "**Retrieved Context:**\n\n"
            for unit in response.retrieved_units:
                context_text += f"### {unit.cell.cell_id} [{unit.cell.cell_type}]\n"
                if unit.intent != "[Pending intent inference]":
                    context_text += f"**Intent:** {unit.intent}\n\n"
                if unit.dependencies:
                    context_text += f"**Depends on:** {', '.join(unit.dependencies)}\n\n"
                context_text += f"```python\n{unit.cell.source[:300]}\n```\n\n"
            
            context_text += f"\n**Confidence:** {response.confidence:.2%}\n"
            context_text += f"**Hallucination Risk:** {'⚠️ Yes' if response.has_hallucination_risk else '✅ No'}"
            
            return (answer_text, citations_text, context_text)
        
        except Exception as e:
            return (f"❌ Error: {str(e)}", "", "")
    
    def _excel_to_cells(self, excel_path: str) -> List[Cell]:
        """Convert Excel file to notebook-like cells."""
        from src.models import Cell, CellType
        
        cells = []
        
        # Read all sheets
        xl = pd.ExcelFile(excel_path)
        
        for sheet_name in xl.sheet_names:
            df = xl.parse(sheet_name)
            
            # Create a markdown cell for sheet name
            cells.append(Cell(
                cell_id=f"sheet_{sheet_name}",
                cell_type=CellType.MARKDOWN,
                source=f"# Sheet: {sheet_name}",
                outputs=[]
            ))
            
            # Create a code cell for data loading
            cells.append(Cell(
                cell_id=f"data_{sheet_name}",
                cell_type=CellType.CODE,
                source=f"# Data from {sheet_name}\ndf_{sheet_name} = pd.read_excel('{excel_path}', sheet_name='{sheet_name}')",
                outputs=[{"data": {"text/plain": str(df.head())}}]
            ))
            
            # Create cells for basic analysis
            cells.append(Cell(
                cell_id=f"shape_{sheet_name}",
                cell_type=CellType.CODE,
                source=f"print(f'Shape: {df.shape}')",
                outputs=[{"text": f"Shape: {df.shape}"}]
            ))
            
            cells.append(Cell(
                cell_id=f"info_{sheet_name}",
                cell_type=CellType.CODE,
                source=f"df_{sheet_name}.info()",
                outputs=[{"text": str(df.dtypes)}]
            ))
        
        return cells


def create_gradio_app():
    """Create and return the Gradio interface."""
    agent = NotebookAgentUI()
    
    with gr.Blocks(title="Context Thread Agent", theme=gr.themes.Soft()) as demo:
        gr.Markdown("# 🧵 Context Thread Agent")
        gr.Markdown("""

**An AI-powered notebook copilot for analytical workflows**



Upload your Jupyter notebook (.ipynb) or Excel file (.xlsx/.xls) and ask questions about your data analysis. 

The agent understands your notebook's context, dependencies, and reasoning—providing grounded answers with citations.



### Key Features:

- ✅ **Context-Aware**: Answers based only on your notebook content

- ✅ **Citation-Based**: Every claim references specific cells

- ✅ **Dependency-Aware**: Understands how cells relate

- ✅ **No Hallucinations**: Grounded in your actual analysis

- ✅ **Fast & Free**: Powered by Groq AI



### Major Uses:

- **Audit Analysis**: Verify assumptions and decisions in complex notebooks

- **Code Review**: Understand data transformations and logic flows

- **Documentation**: Generate insights summaries with evidence

- **Debugging**: Trace errors through dependent cells

- **Collaboration**: Share verifiable insights from your work

""")
        
        # Upload section
        with gr.Row():
            with gr.Column(scale=1):
                file_input = gr.File(
                    label="Upload Notebook or Excel",
                    file_types=[".ipynb", ".xlsx", ".xls"],
                    type="filepath"
                )
                upload_btn = gr.Button("📤 Upload & Analyze", variant="primary", size="lg")
            with gr.Column(scale=1):
                upload_status = gr.Markdown("### Status\n\nReady to upload...")
        
        # After upload, show the main interface
        with gr.Row(visible=False) as main_interface:
            # Left side: Notebook viewer
            with gr.Column(scale=1):
                gr.Markdown("### 📓 Notebook Viewer")
                notebook_display = gr.Markdown("")
                
                keypoints_btn = gr.Button("🔑 Generate Key Points", variant="secondary")
                keypoints_display = gr.Markdown("")
                
            # Right side: Question answering
            with gr.Column(scale=1):
                gr.Markdown("### ❓ Ask Questions")
                query_input = gr.Textbox(
                    label="Your Question",
                    placeholder="e.g., 'Why did we remove Q4 data?' or 'What are the key findings?'",
                    lines=3
                )
                ask_btn = gr.Button("🤖 Get Answer", variant="primary")
                
                answer_display = gr.Markdown("")
                citations_display = gr.Markdown("")
                context_display = gr.Markdown("")
        
        # Event handlers
        def on_upload(file):
            status = agent.load_notebook(file)
            if "✅" in status:
                return status, gr.update(visible=True), agent.get_notebook_display(), ""
            else:
                return status, gr.update(visible=False), "", ""
        
        upload_btn.click(
            fn=on_upload,
            inputs=[file_input],
            outputs=[upload_status, main_interface, notebook_display, keypoints_display]
        )
        
        keypoints_btn.click(
            fn=lambda: agent.generate_keypoints(),
            inputs=[],
            outputs=[keypoints_display]
        ).then(
            fn=lambda: gr.update(visible=True),
            inputs=[],
            outputs=[keypoints_display]
        )
        
        ask_btn.click(
            fn=agent.ask_question,
            inputs=[query_input],
            outputs=[answer_display, citations_display, context_display]
        )
    
    return demo


if __name__ == "__main__":
    demo = create_gradio_app()
    demo.launch(
        server_name="0.0.0.0",
        server_port=7860,
        share=True
    )