Harrisun commited on
Commit
0d1b42d
Β·
verified Β·
1 Parent(s): 30cf27f

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +609 -0
app.py ADDED
@@ -0,0 +1,609 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import os
3
+ import json
4
+ import pickle
5
+ from datetime import datetime
6
+ import requests
7
+ from bs4 import BeautifulSoup
8
+ import fitz # PyMuPDF for PDF processing
9
+ import numpy as np
10
+ from sentence_transformers import SentenceTransformer
11
+ from sklearn.metrics.pairwise import cosine_similarity
12
+ import sqlite3
13
+ import hashlib
14
+ from typing import List, Dict, Any, Tuple
15
+ import logging
16
+ import tempfile
17
+ import shutil
18
+ from urllib.parse import urlparse, urljoin
19
+ import re
20
+
21
+ # Setup logging
22
+ logging.basicConfig(level=logging.INFO)
23
+ logger = logging.getLogger(__name__)
24
+
25
+ class MedicalRAGSystem:
26
+ def __init__(self):
27
+ self.embedding_model = None
28
+ self.db_path = "medical_rag.db"
29
+ self.embeddings_cache = {}
30
+ self.init_database()
31
+ self.load_embedding_model()
32
+
33
+ def load_embedding_model(self):
34
+ """Load a free sentence transformer model"""
35
+ try:
36
+ # Using a lightweight, free model suitable for regulatory text
37
+ self.embedding_model = SentenceTransformer('all-MiniLM-L6-v2')
38
+ logger.info("Embedding model loaded successfully")
39
+ except Exception as e:
40
+ logger.error(f"Error loading embedding model: {e}")
41
+ return None
42
+
43
+ def init_database(self):
44
+ """Initialize SQLite database for persistent storage"""
45
+ conn = sqlite3.connect(self.db_path)
46
+ cursor = conn.cursor()
47
+
48
+ # Create tables for different source types
49
+ cursor.execute('''
50
+ CREATE TABLE IF NOT EXISTS documents (
51
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
52
+ filename TEXT NOT NULL,
53
+ content TEXT NOT NULL,
54
+ content_hash TEXT UNIQUE,
55
+ category TEXT NOT NULL,
56
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
57
+ metadata TEXT
58
+ )
59
+ ''')
60
+
61
+ cursor.execute('''
62
+ CREATE TABLE IF NOT EXISTS websites (
63
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
64
+ url TEXT NOT NULL,
65
+ content TEXT NOT NULL,
66
+ content_hash TEXT UNIQUE,
67
+ title TEXT,
68
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
69
+ metadata TEXT
70
+ )
71
+ ''')
72
+
73
+ cursor.execute('''
74
+ CREATE TABLE IF NOT EXISTS standards (
75
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
76
+ standard_name TEXT NOT NULL,
77
+ content TEXT NOT NULL,
78
+ content_hash TEXT UNIQUE,
79
+ version TEXT,
80
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
81
+ metadata TEXT
82
+ )
83
+ ''')
84
+
85
+ cursor.execute('''
86
+ CREATE TABLE IF NOT EXISTS embeddings (
87
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
88
+ source_type TEXT NOT NULL,
89
+ source_id INTEGER NOT NULL,
90
+ chunk_index INTEGER NOT NULL,
91
+ embedding BLOB NOT NULL,
92
+ text_chunk TEXT NOT NULL
93
+ )
94
+ ''')
95
+
96
+ conn.commit()
97
+ conn.close()
98
+ logger.info("Database initialized successfully")
99
+
100
+ def get_content_hash(self, content: str) -> str:
101
+ """Generate hash for content to avoid duplicates"""
102
+ return hashlib.md5(content.encode()).hexdigest()
103
+
104
+ def chunk_text(self, text: str, chunk_size: int = 500, overlap: int = 50) -> List[str]:
105
+ """Split text into overlapping chunks for better retrieval"""
106
+ words = text.split()
107
+ chunks = []
108
+
109
+ for i in range(0, len(words), chunk_size - overlap):
110
+ chunk = ' '.join(words[i:i + chunk_size])
111
+ if chunk.strip():
112
+ chunks.append(chunk)
113
+
114
+ return chunks
115
+
116
+ def process_pdf_document(self, file_path: str) -> Tuple[str, Dict]:
117
+ """Extract text content from PDF documents"""
118
+ try:
119
+ doc = fitz.open(file_path)
120
+ text_content = ""
121
+ metadata = {"pages": doc.page_count, "format": "PDF"}
122
+
123
+ for page_num in range(doc.page_count):
124
+ page = doc[page_num]
125
+ text_content += page.get_text()
126
+
127
+ doc.close()
128
+ return text_content, metadata
129
+ except Exception as e:
130
+ logger.error(f"Error processing PDF: {e}")
131
+ return "", {}
132
+
133
+ def process_text_document(self, file_path: str) -> Tuple[str, Dict]:
134
+ """Process text documents"""
135
+ try:
136
+ with open(file_path, 'r', encoding='utf-8') as f:
137
+ content = f.read()
138
+ return content, {"format": "TEXT"}
139
+ except Exception as e:
140
+ logger.error(f"Error processing text document: {e}")
141
+ return "", {}
142
+
143
+ def scrape_website(self, url: str) -> Tuple[str, str, Dict]:
144
+ """Scrape content from regulatory websites"""
145
+ try:
146
+ headers = {
147
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
148
+ }
149
+ response = requests.get(url, headers=headers, timeout=30)
150
+ response.raise_for_status()
151
+
152
+ soup = BeautifulSoup(response.content, 'html.parser')
153
+
154
+ # Remove script and style elements
155
+ for script in soup(["script", "style"]):
156
+ script.decompose()
157
+
158
+ # Get title
159
+ title = soup.title.string if soup.title else url
160
+
161
+ # Extract main content
162
+ content = soup.get_text()
163
+ content = re.sub(r'\s+', ' ', content).strip()
164
+
165
+ metadata = {
166
+ "title": title,
167
+ "url": url,
168
+ "scraped_at": datetime.now().isoformat()
169
+ }
170
+
171
+ return content, title, metadata
172
+
173
+ except Exception as e:
174
+ logger.error(f"Error scraping website {url}: {e}")
175
+ return "", "", {}
176
+
177
+ def add_document(self, file_path: str, filename: str, category: str) -> str:
178
+ """Add document to the knowledge base"""
179
+ try:
180
+ # Determine file type and process accordingly
181
+ if filename.lower().endswith('.pdf'):
182
+ content, metadata = self.process_pdf_document(file_path)
183
+ else:
184
+ content, metadata = self.process_text_document(file_path)
185
+
186
+ if not content:
187
+ return "Error: Could not extract content from document"
188
+
189
+ content_hash = self.get_content_hash(content)
190
+
191
+ # Store in database
192
+ conn = sqlite3.connect(self.db_path)
193
+ cursor = conn.cursor()
194
+
195
+ try:
196
+ cursor.execute('''
197
+ INSERT INTO documents (filename, content, content_hash, category, metadata)
198
+ VALUES (?, ?, ?, ?, ?)
199
+ ''', (filename, content, content_hash, category, json.dumps(metadata)))
200
+
201
+ doc_id = cursor.lastrowid
202
+ conn.commit()
203
+
204
+ # Generate embeddings
205
+ self.generate_embeddings_for_content(content, 'document', doc_id)
206
+
207
+ conn.close()
208
+ return f"Document '{filename}' added successfully to category '{category}'"
209
+
210
+ except sqlite3.IntegrityError:
211
+ conn.close()
212
+ return "Document already exists in the knowledge base"
213
+
214
+ except Exception as e:
215
+ logger.error(f"Error adding document: {e}")
216
+ return f"Error adding document: {str(e)}"
217
+
218
+ def add_website(self, url: str) -> str:
219
+ """Add website content to the knowledge base"""
220
+ try:
221
+ content, title, metadata = self.scrape_website(url)
222
+
223
+ if not content:
224
+ return "Error: Could not scrape website content"
225
+
226
+ content_hash = self.get_content_hash(content)
227
+
228
+ conn = sqlite3.connect(self.db_path)
229
+ cursor = conn.cursor()
230
+
231
+ try:
232
+ cursor.execute('''
233
+ INSERT INTO websites (url, content, content_hash, title, metadata)
234
+ VALUES (?, ?, ?, ?, ?)
235
+ ''', (url, content, content_hash, title, json.dumps(metadata)))
236
+
237
+ website_id = cursor.lastrowid
238
+ conn.commit()
239
+
240
+ # Generate embeddings
241
+ self.generate_embeddings_for_content(content, 'website', website_id)
242
+
243
+ conn.close()
244
+ return f"Website '{title}' added successfully"
245
+
246
+ except sqlite3.IntegrityError:
247
+ conn.close()
248
+ return "Website already exists in the knowledge base"
249
+
250
+ except Exception as e:
251
+ logger.error(f"Error adding website: {e}")
252
+ return f"Error adding website: {str(e)}"
253
+
254
+ def add_standard(self, standard_name: str, content: str, version: str = "") -> str:
255
+ """Add standard content to the knowledge base"""
256
+ try:
257
+ if not content.strip():
258
+ return "Error: Standard content cannot be empty"
259
+
260
+ content_hash = self.get_content_hash(content)
261
+
262
+ conn = sqlite3.connect(self.db_path)
263
+ cursor = conn.cursor()
264
+
265
+ metadata = {"version": version, "added_at": datetime.now().isoformat()}
266
+
267
+ try:
268
+ cursor.execute('''
269
+ INSERT INTO standards (standard_name, content, content_hash, version, metadata)
270
+ VALUES (?, ?, ?, ?, ?)
271
+ ''', (standard_name, content, content_hash, version, json.dumps(metadata)))
272
+
273
+ standard_id = cursor.lastrowid
274
+ conn.commit()
275
+
276
+ # Generate embeddings
277
+ self.generate_embeddings_for_content(content, 'standard', standard_id)
278
+
279
+ conn.close()
280
+ return f"Standard '{standard_name}' added successfully"
281
+
282
+ except sqlite3.IntegrityError:
283
+ conn.close()
284
+ return "Standard already exists in the knowledge base"
285
+
286
+ except Exception as e:
287
+ logger.error(f"Error adding standard: {e}")
288
+ return f"Error adding standard: {str(e)}"
289
+
290
+ def generate_embeddings_for_content(self, content: str, source_type: str, source_id: int):
291
+ """Generate embeddings for content chunks"""
292
+ if not self.embedding_model:
293
+ logger.error("Embedding model not available")
294
+ return
295
+
296
+ chunks = self.chunk_text(content)
297
+
298
+ conn = sqlite3.connect(self.db_path)
299
+ cursor = conn.cursor()
300
+
301
+ for i, chunk in enumerate(chunks):
302
+ try:
303
+ embedding = self.embedding_model.encode(chunk)
304
+ embedding_blob = pickle.dumps(embedding)
305
+
306
+ cursor.execute('''
307
+ INSERT INTO embeddings (source_type, source_id, chunk_index, embedding, text_chunk)
308
+ VALUES (?, ?, ?, ?, ?)
309
+ ''', (source_type, source_id, i, embedding_blob, chunk))
310
+
311
+ except Exception as e:
312
+ logger.error(f"Error generating embedding for chunk {i}: {e}")
313
+
314
+ conn.commit()
315
+ conn.close()
316
+
317
+ def search_knowledge_base(self, query: str, top_k: int = 5) -> List[Dict]:
318
+ """Search the knowledge base using semantic similarity"""
319
+ if not self.embedding_model:
320
+ return []
321
+
322
+ try:
323
+ query_embedding = self.embedding_model.encode(query)
324
+
325
+ conn = sqlite3.connect(self.db_path)
326
+ cursor = conn.cursor()
327
+
328
+ # Get all embeddings
329
+ cursor.execute('''
330
+ SELECT e.source_type, e.source_id, e.text_chunk, e.embedding,
331
+ CASE
332
+ WHEN e.source_type = 'document' THEN d.filename
333
+ WHEN e.source_type = 'website' THEN w.title
334
+ WHEN e.source_type = 'standard' THEN s.standard_name
335
+ END as source_name
336
+ FROM embeddings e
337
+ LEFT JOIN documents d ON e.source_type = 'document' AND e.source_id = d.id
338
+ LEFT JOIN websites w ON e.source_type = 'website' AND e.source_id = w.id
339
+ LEFT JOIN standards s ON e.source_type = 'standard' AND e.source_id = s.id
340
+ ''')
341
+
342
+ results = []
343
+ for row in cursor.fetchall():
344
+ try:
345
+ stored_embedding = pickle.loads(row[3])
346
+ similarity = cosine_similarity([query_embedding], [stored_embedding])[0][0]
347
+
348
+ results.append({
349
+ 'source_type': row[0],
350
+ 'source_id': row[1],
351
+ 'text_chunk': row[2],
352
+ 'source_name': row[4],
353
+ 'similarity': similarity
354
+ })
355
+ except Exception as e:
356
+ logger.error(f"Error processing embedding: {e}")
357
+
358
+ conn.close()
359
+
360
+ # Sort by similarity and return top k
361
+ results.sort(key=lambda x: x['similarity'], reverse=True)
362
+ return results[:top_k]
363
+
364
+ except Exception as e:
365
+ logger.error(f"Error searching knowledge base: {e}")
366
+ return []
367
+
368
+ def get_knowledge_base_stats(self) -> Dict:
369
+ """Get statistics about the knowledge base"""
370
+ conn = sqlite3.connect(self.db_path)
371
+ cursor = conn.cursor()
372
+
373
+ stats = {}
374
+
375
+ # Count documents
376
+ cursor.execute("SELECT COUNT(*) FROM documents")
377
+ stats['documents'] = cursor.fetchone()[0]
378
+
379
+ # Count websites
380
+ cursor.execute("SELECT COUNT(*) FROM websites")
381
+ stats['websites'] = cursor.fetchone()[0]
382
+
383
+ # Count standards
384
+ cursor.execute("SELECT COUNT(*) FROM standards")
385
+ stats['standards'] = cursor.fetchone()[0]
386
+
387
+ # Count total embeddings
388
+ cursor.execute("SELECT COUNT(*) FROM embeddings")
389
+ stats['embeddings'] = cursor.fetchone()[0]
390
+
391
+ conn.close()
392
+ return stats
393
+
394
+ # Initialize the RAG system
395
+ rag_system = MedicalRAGSystem()
396
+
397
+ def handle_document_upload(files, category):
398
+ """Handle document upload"""
399
+ if not files:
400
+ return "No files selected"
401
+
402
+ results = []
403
+ for file in files:
404
+ filename = os.path.basename(file.name)
405
+ result = rag_system.add_document(file.name, filename, category)
406
+ results.append(result)
407
+
408
+ return "\n".join(results)
409
+
410
+ def handle_website_addition(url):
411
+ """Handle website addition"""
412
+ if not url.strip():
413
+ return "Please enter a valid URL"
414
+
415
+ return rag_system.add_website(url.strip())
416
+
417
+ def handle_standard_addition(standard_name, content, version):
418
+ """Handle standard addition"""
419
+ if not standard_name.strip() or not content.strip():
420
+ return "Please provide both standard name and content"
421
+
422
+ return rag_system.add_standard(standard_name.strip(), content.strip(), version.strip())
423
+
424
+ def handle_search(query):
425
+ """Handle search queries"""
426
+ if not query.strip():
427
+ return "Please enter a search query", ""
428
+
429
+ results = rag_system.search_knowledge_base(query.strip())
430
+
431
+ if not results:
432
+ return "No relevant results found", ""
433
+
434
+ # Format results for display
435
+ formatted_results = []
436
+ context = []
437
+
438
+ for i, result in enumerate(results, 1):
439
+ similarity_pct = result['similarity'] * 100
440
+ formatted_results.append(f"""
441
+ **Result {i}** (Similarity: {similarity_pct:.1f}%)
442
+ **Source:** {result['source_name']} ({result['source_type']})
443
+ **Content:** {result['text_chunk'][:300]}{'...' if len(result['text_chunk']) > 300 else ''}
444
+ ---
445
+ """)
446
+ context.append(result['text_chunk'])
447
+
448
+ # Generate a comprehensive answer based on the context
449
+ answer = generate_answer(query, context)
450
+
451
+ return "\n".join(formatted_results), answer
452
+
453
+ def generate_answer(query: str, context: List[str]) -> str:
454
+ """Generate an answer based on the retrieved context"""
455
+ # Simple extractive approach - in a production system, you might use a generative model
456
+ relevant_info = []
457
+
458
+ query_lower = query.lower()
459
+ for chunk in context:
460
+ # Find sentences that contain query terms
461
+ sentences = chunk.split('.')
462
+ for sentence in sentences:
463
+ if any(term in sentence.lower() for term in query_lower.split()):
464
+ relevant_info.append(sentence.strip())
465
+
466
+ if relevant_info:
467
+ # Remove duplicates and combine
468
+ unique_info = list(dict.fromkeys(relevant_info))
469
+ return "Based on the regulatory documents:\n\n" + "\n\n".join(unique_info[:3])
470
+ else:
471
+ return "The retrieved content may contain relevant information, but I couldn't extract a specific answer. Please review the search results above."
472
+
473
+ def get_stats():
474
+ """Get knowledge base statistics"""
475
+ stats = rag_system.get_knowledge_base_stats()
476
+ return f"""
477
+ Knowledge Base Statistics:
478
+ - Documents: {stats['documents']}
479
+ - Websites: {stats['websites']}
480
+ - Standards: {stats['standards']}
481
+ - Total Text Chunks: {stats['embeddings']}
482
+ """
483
+
484
+ # Create Gradio interface
485
+ with gr.Blocks(title="Medical Devices RAG System", theme=gr.themes.Soft()) as demo:
486
+ gr.Markdown("""
487
+ # πŸ₯ Medical Devices Regulatory RAG System
488
+
489
+ A comprehensive knowledge base system for medical device regulatory analysts.
490
+ Add documents, websites, and standards to build your regulatory knowledge base.
491
+ """)
492
+
493
+ with gr.Tabs():
494
+ # Search Tab
495
+ with gr.Tab("πŸ” Search Knowledge Base"):
496
+ gr.Markdown("### Search your regulatory knowledge base")
497
+
498
+ search_input = gr.Textbox(
499
+ placeholder="Enter your regulatory question (e.g., 'What are the requirements for Class II medical devices?')",
500
+ label="Search Query",
501
+ lines=2
502
+ )
503
+ search_button = gr.Button("Search", variant="primary")
504
+
505
+ with gr.Row():
506
+ with gr.Column():
507
+ search_results = gr.Markdown(label="Search Results")
508
+ with gr.Column():
509
+ answer_output = gr.Markdown(label="Generated Answer")
510
+
511
+ search_button.click(
512
+ handle_search,
513
+ inputs=[search_input],
514
+ outputs=[search_results, answer_output]
515
+ )
516
+
517
+ # Add Documents Tab
518
+ with gr.Tab("πŸ“„ Add Documents"):
519
+ gr.Markdown("### Add regulatory documents (PDF, TXT)")
520
+
521
+ document_files = gr.File(
522
+ label="Upload Documents",
523
+ file_count="multiple",
524
+ file_types=[".pdf", ".txt", ".docx"]
525
+ )
526
+ document_category = gr.Dropdown(
527
+ choices=["EU MDR 2017/745", "CMDR SOR/98-282", "MDCG", "MDSAP Audit Approach", "UK MDR", "Other"],
528
+ label="Document Category",
529
+ value="Other"
530
+ )
531
+ add_doc_button = gr.Button("Add Documents", variant="primary")
532
+ doc_output = gr.Textbox(label="Result", lines=3)
533
+
534
+ add_doc_button.click(
535
+ handle_document_upload,
536
+ inputs=[document_files, document_category],
537
+ outputs=[doc_output]
538
+ )
539
+
540
+ # Add Websites Tab
541
+ with gr.Tab("🌐 Add Websites"):
542
+ gr.Markdown("### Add regulatory websites")
543
+
544
+ website_url = gr.Textbox(
545
+ placeholder="https://www.fda.gov/medical-devices/...",
546
+ label="Website URL",
547
+ lines=1
548
+ )
549
+ add_website_button = gr.Button("Add Website", variant="primary")
550
+ website_output = gr.Textbox(label="Result", lines=3)
551
+
552
+ gr.Markdown("**Suggested regulatory websites:**")
553
+ gr.Markdown("""
554
+ - US FDA 21CFR: https://www.accessdata.fda.gov/scripts/cdrh/cfdocs/cfcfr/cfrsearch.cfm
555
+ - EU Medical Devices: https://ec.europa.eu/health/medical-devices-sector_en
556
+ - Health Canada Medical Devices: https://www.canada.ca/en/health-canada/services/drugs-health-products/medical-devices.html
557
+ """)
558
+
559
+ add_website_button.click(
560
+ handle_website_addition,
561
+ inputs=[website_url],
562
+ outputs=[website_output]
563
+ )
564
+
565
+ # Add Standards Tab
566
+ with gr.Tab("πŸ“‹ Add Standards"):
567
+ gr.Markdown("### Add regulatory standards")
568
+
569
+ standard_name = gr.Textbox(
570
+ placeholder="ISO 13485:2016",
571
+ label="Standard Name",
572
+ lines=1
573
+ )
574
+ standard_version = gr.Textbox(
575
+ placeholder="2016 (optional)",
576
+ label="Version",
577
+ lines=1
578
+ )
579
+ standard_content = gr.Textbox(
580
+ placeholder="Enter or paste the standard content here...",
581
+ label="Standard Content",
582
+ lines=10
583
+ )
584
+ add_standard_button = gr.Button("Add Standard", variant="primary")
585
+ standard_output = gr.Textbox(label="Result", lines=3)
586
+
587
+ add_standard_button.click(
588
+ handle_standard_addition,
589
+ inputs=[standard_name, standard_content, standard_version],
590
+ outputs=[standard_output]
591
+ )
592
+
593
+ # Statistics Tab
594
+ with gr.Tab("πŸ“Š Knowledge Base Stats"):
595
+ gr.Markdown("### Knowledge Base Statistics")
596
+
597
+ stats_button = gr.Button("Refresh Statistics", variant="secondary")
598
+ stats_output = gr.Textbox(label="Statistics", lines=8)
599
+
600
+ stats_button.click(
601
+ get_stats,
602
+ outputs=[stats_output]
603
+ )
604
+
605
+ # Load initial stats
606
+ demo.load(get_stats, outputs=[stats_output])
607
+
608
+ if __name__ == "__main__":
609
+ demo.launch(share=True)