mackenzietechdocs commited on
Commit
6822668
·
1 Parent(s): f639a6f

adding app content and source files

Browse files
app.py CHANGED
@@ -1,7 +1,768 @@
 
 
 
 
 
 
 
 
1
  import gradio as gr
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
- def greet(name):
4
- return "Hello " + name + "!!"
5
 
6
- demo = gr.Interface(fn=greet, inputs="text", outputs="text")
7
- demo.launch()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Docs Navigator MCP - Modern Gradio UI
4
+
5
+ An elegant, AI-powered documentation assistant with advanced features.
6
+ """
7
+
8
+ import os
9
  import gradio as gr
10
+ from pathlib import Path
11
+ from typing import List, Dict, Optional
12
+ from anthropic import Anthropic
13
+ from dotenv import load_dotenv
14
+ from document_intelligence import DocumentIntelligence
15
+ import time
16
+
17
+ # Load environment variables
18
+ load_dotenv()
19
+
20
+ # Configuration
21
+ DOCS_DIR = Path(__file__).parent / "docs"
22
+ ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY")
23
+
24
+ # Initialize services
25
+ client = Anthropic(api_key=ANTHROPIC_API_KEY)
26
+ doc_intelligence = DocumentIntelligence(DOCS_DIR)
27
+
28
+ # Supported file extensions
29
+ SUPPORTED_EXTENSIONS = {'.md', '.txt', '.rst', '.pdf'}
30
+
31
+
32
+ def load_documentation() -> str:
33
+ """Load all documentation files into a single context."""
34
+ all_docs = []
35
+
36
+ if not DOCS_DIR.exists():
37
+ return "Documentation directory not found."
38
+
39
+ for file_path in DOCS_DIR.rglob('*'):
40
+ if file_path.is_file() and file_path.suffix in SUPPORTED_EXTENSIONS:
41
+ try:
42
+ if file_path.suffix == '.pdf':
43
+ content = extract_pdf_content(file_path)
44
+ else:
45
+ content = file_path.read_text(encoding='utf-8', errors='ignore')
46
+
47
+ relative_path = file_path.relative_to(DOCS_DIR)
48
+ all_docs.append(f"=== {relative_path} ===\n{content}\n")
49
+ except Exception as e:
50
+ all_docs.append(f"=== {file_path.name} ===\nError reading file: {str(e)}\n")
51
+
52
+ return "\n\n".join(all_docs) if all_docs else "No documentation files found."
53
+
54
+
55
+ def extract_pdf_content(pdf_path: Path) -> str:
56
+ """Extract text content from PDF files."""
57
+ try:
58
+ from PyPDF2 import PdfReader
59
+ reader = PdfReader(pdf_path)
60
+ content = []
61
+
62
+ for i, page in enumerate(reader.pages, 1):
63
+ try:
64
+ text = page.extract_text()
65
+ content.append(f"--- Page {i} ---\n{text}")
66
+ except Exception as e:
67
+ content.append(f"--- Page {i} (Error reading: {str(e)}) ---")
68
+
69
+ return "\n\n".join(content)
70
+ except Exception as e:
71
+ return f"Error extracting PDF: {str(e)}"
72
+
73
+
74
+ def get_available_files() -> List[str]:
75
+ """Get list of available documentation files."""
76
+ files = []
77
+ if DOCS_DIR.exists():
78
+ for file_path in DOCS_DIR.rglob('*'):
79
+ if file_path.is_file() and file_path.suffix in SUPPORTED_EXTENSIONS:
80
+ files.append(str(file_path.relative_to(DOCS_DIR)))
81
+ return sorted(files)
82
+
83
+
84
+ def chat_with_docs(message: str, history: List[dict], system_prompt: str = None) -> str:
85
+ """Process user message and generate AI response."""
86
+ if not ANTHROPIC_API_KEY:
87
+ return "⚠️ Please set your ANTHROPIC_API_KEY in the .env file."
88
+
89
+ # Load documentation context
90
+ docs_context = load_documentation()
91
+
92
+ # Build conversation history from messages format
93
+ messages = []
94
+ for msg in history:
95
+ if msg.get("role") == "user":
96
+ messages.append({"role": "user", "content": msg["content"]})
97
+ elif msg.get("role") == "assistant":
98
+ messages.append({"role": "assistant", "content": msg["content"]})
99
+
100
+ messages.append({"role": "user", "content": message})
101
+
102
+ # Default system prompt
103
+ default_system = f"""You are an expert documentation assistant. You have access to the following documentation:
104
+
105
+ {docs_context}
106
+
107
+ Your role is to:
108
+ - Answer questions accurately based on the documentation
109
+ - Provide clear, concise, and helpful responses
110
+ - Reference specific sections when relevant
111
+ - Admit when information is not in the documentation
112
+ - Use markdown formatting for better readability
113
+ - Be friendly and professional
114
+
115
+ Always base your answers on the provided documentation."""
116
+
117
+ system = system_prompt if system_prompt else default_system
118
+
119
+ try:
120
+ # Call Claude API with streaming
121
+ response_text = ""
122
+ with client.messages.stream(
123
+ model="claude-3-haiku-20240307",
124
+ max_tokens=4096,
125
+ system=system,
126
+ messages=messages
127
+ ) as stream:
128
+ for text in stream.text_stream:
129
+ response_text += text
130
+
131
+ return response_text
132
+
133
+ except Exception as e:
134
+ return f"❌ Error: {str(e)}"
135
+
136
+
137
+ def get_document_stats() -> str:
138
+ """Get statistics about the documentation."""
139
+ if not DOCS_DIR.exists():
140
+ return "📁 No documentation directory found"
141
+
142
+ files = list(DOCS_DIR.rglob('*'))
143
+ doc_files = [f for f in files if f.is_file() and f.suffix in SUPPORTED_EXTENSIONS]
144
+
145
+ total_size = sum(f.stat().st_size for f in doc_files) / 1024 # KB
146
+
147
+ stats = f"""
148
+ 📊 **Documentation Statistics**
149
+
150
+ - 📄 Total Files: {len(doc_files)}
151
+ - 💾 Total Size: {total_size:.1f} KB
152
+ - 📁 Directory: `{DOCS_DIR.name}/`
153
+
154
+ **File Types:**
155
+ """
156
+
157
+ by_type = {}
158
+ for f in doc_files:
159
+ ext = f.suffix
160
+ by_type[ext] = by_type.get(ext, 0) + 1
161
+
162
+ for ext, count in sorted(by_type.items()):
163
+ stats += f"\n- {ext}: {count} files"
164
+
165
+ return stats
166
+
167
+
168
+ def analyze_document(file_name: str, analysis_type: str) -> str:
169
+ """Analyze a specific document."""
170
+ if not file_name:
171
+ return "⚠️ Please select a file to analyze"
172
+
173
+ file_path = DOCS_DIR / file_name
174
+
175
+ if not file_path.exists():
176
+ return f"❌ File not found: {file_name}"
177
+
178
+ try:
179
+ if file_path.suffix == '.pdf':
180
+ content = extract_pdf_content(file_path)
181
+ else:
182
+ content = file_path.read_text(encoding='utf-8', errors='ignore')
183
+
184
+ if analysis_type == "Summary":
185
+ summary = doc_intelligence.generate_smart_summary(content, "medium")
186
+ return f"📝 **Summary of {file_name}**\n\n{summary}"
187
+
188
+ elif analysis_type == "Key Concepts":
189
+ concepts = doc_intelligence.extract_key_concepts(content)
190
+ result = f"🔑 **Key Concepts in {file_name}**\n\n"
191
+ for i, concept in enumerate(concepts[:10], 1):
192
+ result += f"{i}. **{concept['concept']}** ({concept['type']}) - appears {concept['frequency']} times\n"
193
+ return result
194
+
195
+ elif analysis_type == "Readability":
196
+ analysis = doc_intelligence.analyze_readability(content)
197
+ return f"""
198
+ 📊 **Readability Analysis of {file_name}**
199
+
200
+ - **Flesch Reading Ease**: {analysis['flesch_score']} ({analysis['complexity']})
201
+ - **Grade Level**: {analysis['grade_level']}
202
+ - **Avg Sentence Length**: {analysis['avg_sentence_length']} words
203
+ - **Total Words**: {analysis['total_words']}
204
+ - **Total Sentences**: {analysis['total_sentences']}
205
+ """
206
+
207
+ elif analysis_type == "Q&A Extraction":
208
+ qa_pairs = doc_intelligence.extract_questions_and_answers(content)
209
+ if not qa_pairs:
210
+ return f"❓ No Q&A pairs found in {file_name}"
211
+
212
+ result = f"❓ **Questions & Answers from {file_name}**\n\n"
213
+ for i, qa in enumerate(qa_pairs[:5], 1):
214
+ result += f"**Q{i}:** {qa['question']}\n**A:** {qa['answer']}\n\n"
215
+ return result
216
+
217
+ except Exception as e:
218
+ return f"❌ Error analyzing file: {str(e)}"
219
+
220
+
221
+ # Custom CSS for sleek dark mode design with messenger-style chat
222
+ custom_css = """
223
+ /* Dark mode color scheme */
224
+ :root {
225
+ --bg-primary: #0d1117;
226
+ --bg-secondary: #161b22;
227
+ --bg-tertiary: #21262d;
228
+ --accent-primary: #8b5cf6;
229
+ --accent-secondary: #a78bfa;
230
+ --accent-glow: rgba(139, 92, 246, 0.3);
231
+ --text-primary: #e6edf3;
232
+ --text-secondary: #8b949e;
233
+ --border-color: #30363d;
234
+ --message-user: #8b5cf6;
235
+ --message-bot: #1f2937;
236
+ }
237
+
238
+ /* Main container - full dark mode */
239
+ .gradio-container {
240
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif !important;
241
+ background: var(--bg-primary) !important;
242
+ }
243
+
244
+ body, .gradio-container, .main, .contain {
245
+ background: var(--bg-primary) !important;
246
+ }
247
+
248
+ /* Header styling - sleek gradient */
249
+ .header-container {
250
+ background: linear-gradient(135deg, #8b5cf6 0%, #6366f1 50%, #ec4899 100%);
251
+ padding: 2.5rem 2rem;
252
+ border-radius: 16px;
253
+ margin-bottom: 2rem;
254
+ box-shadow: 0 20px 60px rgba(139, 92, 246, 0.4);
255
+ border: 1px solid rgba(139, 92, 246, 0.2);
256
+ }
257
+
258
+ .header-title {
259
+ color: white;
260
+ font-size: 2.8rem;
261
+ font-weight: 900;
262
+ margin: 0;
263
+ text-align: center;
264
+ text-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
265
+ letter-spacing: -0.02em;
266
+ }
267
+
268
+ .header-subtitle {
269
+ color: rgba(255, 255, 255, 0.95);
270
+ font-size: 1.15rem;
271
+ text-align: center;
272
+ margin-top: 0.75rem;
273
+ font-weight: 500;
274
+ letter-spacing: 0.01em;
275
+ }
276
+
277
+ /* Messenger-style chat window */
278
+ .chatbot.svelte-1w8gs39 {
279
+ background: var(--bg-secondary) !important;
280
+ border: 1px solid var(--border-color) !important;
281
+ border-radius: 16px !important;
282
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4) !important;
283
+ }
284
+
285
+ /* Chat messages - messenger style */
286
+ .message-wrap {
287
+ padding: 0.5rem 1rem !important;
288
+ }
289
+
290
+ .user-message {
291
+ background: linear-gradient(135deg, var(--message-user) 0%, #a78bfa 100%) !important;
292
+ color: white !important;
293
+ border-radius: 18px 18px 4px 18px !important;
294
+ padding: 0.875rem 1.25rem !important;
295
+ margin: 0.5rem 0 !important;
296
+ max-width: 75% !important;
297
+ margin-left: auto !important;
298
+ box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3) !important;
299
+ font-size: 0.95rem !important;
300
+ line-height: 1.5 !important;
301
+ }
302
+
303
+ .bot-message {
304
+ background: var(--message-bot) !important;
305
+ color: var(--text-primary) !important;
306
+ border-radius: 18px 18px 18px 4px !important;
307
+ padding: 0.875rem 1.25rem !important;
308
+ margin: 0.5rem 0 !important;
309
+ max-width: 85% !important;
310
+ border: 1px solid var(--border-color) !important;
311
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2) !important;
312
+ font-size: 0.95rem !important;
313
+ line-height: 1.6 !important;
314
+ }
315
+
316
+ /* Button styling - modern purple gradient */
317
+ button, .primary-button {
318
+ background: linear-gradient(135deg, #8b5cf6 0%, #6366f1 100%) !important;
319
+ border: none !important;
320
+ border-radius: 12px !important;
321
+ font-weight: 600 !important;
322
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
323
+ box-shadow: 0 4px 16px var(--accent-glow) !important;
324
+ color: white !important;
325
+ padding: 0.75rem 1.5rem !important;
326
+ }
327
+
328
+ button:hover, .primary-button:hover {
329
+ transform: translateY(-2px) !important;
330
+ box-shadow: 0 8px 24px rgba(139, 92, 246, 0.5) !important;
331
+ background: linear-gradient(135deg, #9d72ff 0%, #7c7eff 100%) !important;
332
+ }
333
+
334
+ /* Tabs - dark mode */
335
+ .tabs {
336
+ background: var(--bg-secondary) !important;
337
+ border-radius: 16px !important;
338
+ border: 1px solid var(--border-color) !important;
339
+ }
340
+
341
+ .tab-nav {
342
+ background: var(--bg-tertiary) !important;
343
+ border-radius: 12px 12px 0 0 !important;
344
+ border-bottom: 1px solid var(--border-color) !important;
345
+ padding: 0.5rem !important;
346
+ }
347
+
348
+ .tab-nav button {
349
+ font-weight: 600 !important;
350
+ border-radius: 10px !important;
351
+ color: var(--text-secondary) !important;
352
+ border: none !important;
353
+ transition: all 0.3s ease !important;
354
+ padding: 0.75rem 1.5rem !important;
355
+ margin: 0 0.25rem !important;
356
+ }
357
+
358
+ .tab-nav button:hover {
359
+ background: rgba(139, 92, 246, 0.1) !important;
360
+ color: var(--accent-secondary) !important;
361
+ }
362
+
363
+ .tab-nav button.selected {
364
+ background: linear-gradient(135deg, #8b5cf6 0%, #6366f1 100%) !important;
365
+ color: white !important;
366
+ box-shadow: 0 4px 12px var(--accent-glow) !important;
367
+ }
368
+
369
+ /* Input fields - dark mode */
370
+ textarea, input, .input-box {
371
+ background: var(--bg-tertiary) !important;
372
+ border: 2px solid var(--border-color) !important;
373
+ border-radius: 12px !important;
374
+ color: var(--text-primary) !important;
375
+ transition: all 0.3s ease !important;
376
+ padding: 0.875rem 1rem !important;
377
+ }
378
+
379
+ textarea:focus, input:focus, .input-box:focus {
380
+ border-color: var(--accent-primary) !important;
381
+ box-shadow: 0 0 0 3px var(--accent-glow) !important;
382
+ background: var(--bg-secondary) !important;
383
+ outline: none !important;
384
+ }
385
+
386
+ textarea::placeholder, input::placeholder {
387
+ color: var(--text-secondary) !important;
388
+ opacity: 0.8 !important;
389
+ }
390
+
391
+ /* Dropdown styling */
392
+ .dropdown, select {
393
+ background: var(--bg-tertiary) !important;
394
+ border: 2px solid var(--border-color) !important;
395
+ border-radius: 12px !important;
396
+ color: var(--text-primary) !important;
397
+ padding: 0.75rem 1rem !important;
398
+ }
399
+
400
+ /* Stats and info cards - dark mode */
401
+ .stats-box, .info-card {
402
+ background: var(--bg-secondary) !important;
403
+ padding: 1.75rem !important;
404
+ border-radius: 16px !important;
405
+ border: 1px solid var(--border-color) !important;
406
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3) !important;
407
+ color: var(--text-primary) !important;
408
+ transition: transform 0.3s ease, box-shadow 0.3s ease !important;
409
+ }
410
+
411
+ .info-card:hover, .stats-box:hover {
412
+ transform: translateY(-4px) !important;
413
+ box-shadow: 0 12px 32px rgba(139, 92, 246, 0.2) !important;
414
+ border-color: var(--accent-primary) !important;
415
+ }
416
+
417
+ /* Markdown content styling - dark mode */
418
+ .markdown-text, .prose {
419
+ color: var(--text-primary) !important;
420
+ }
421
+
422
+ .markdown-text h1, .markdown-text h2, .markdown-text h3,
423
+ .prose h1, .prose h2, .prose h3 {
424
+ color: var(--accent-secondary) !important;
425
+ font-weight: 700 !important;
426
+ }
427
+
428
+ .markdown-text strong, .prose strong {
429
+ color: var(--text-primary) !important;
430
+ font-weight: 700 !important;
431
+ }
432
+
433
+ .markdown-text code, .prose code {
434
+ background: var(--bg-tertiary) !important;
435
+ color: var(--accent-secondary) !important;
436
+ padding: 0.2rem 0.5rem !important;
437
+ border-radius: 6px !important;
438
+ font-size: 0.9em !important;
439
+ border: 1px solid var(--border-color) !important;
440
+ }
441
+
442
+ .markdown-text pre, .prose pre {
443
+ background: #1a1b26 !important;
444
+ color: #a9b1d6 !important;
445
+ padding: 1.25rem !important;
446
+ border-radius: 12px !important;
447
+ overflow-x: auto !important;
448
+ border: 1px solid var(--border-color) !important;
449
+ }
450
+
451
+ /* Labels and text */
452
+ label, .label {
453
+ color: var(--text-primary) !important;
454
+ font-weight: 600 !important;
455
+ font-size: 0.95rem !important;
456
+ margin-bottom: 0.5rem !important;
457
+ }
458
+
459
+ .secondary-text {
460
+ color: var(--text-secondary) !important;
461
+ }
462
+
463
+ /* Remove white backgrounds globally */
464
+ .block, .form, .panel {
465
+ background: var(--bg-secondary) !important;
466
+ border: 1px solid var(--border-color) !important;
467
+ border-radius: 12px !important;
468
+ }
469
+
470
+ /* Scrollbar styling for dark mode */
471
+ ::-webkit-scrollbar {
472
+ width: 10px;
473
+ height: 10px;
474
+ }
475
+
476
+ ::-webkit-scrollbar-track {
477
+ background: var(--bg-secondary);
478
+ border-radius: 8px;
479
+ }
480
+
481
+ ::-webkit-scrollbar-thumb {
482
+ background: var(--border-color);
483
+ border-radius: 8px;
484
+ }
485
+
486
+ ::-webkit-scrollbar-thumb:hover {
487
+ background: var(--accent-primary);
488
+ }
489
+
490
+ /* Loading animation */
491
+ .pending {
492
+ background: linear-gradient(90deg, var(--bg-tertiary) 25%, var(--border-color) 50%, var(--bg-tertiary) 75%);
493
+ background-size: 200% 100%;
494
+ animation: loading 1.5s ease-in-out infinite;
495
+ }
496
+
497
+ @keyframes loading {
498
+ 0% { background-position: 200% 0; }
499
+ 100% { background-position: -200% 0; }
500
+ }
501
+
502
+ /* Examples section */
503
+ .examples {
504
+ background: var(--bg-tertiary) !important;
505
+ border-radius: 12px !important;
506
+ border: 1px solid var(--border-color) !important;
507
+ padding: 1rem !important;
508
+ }
509
+
510
+ .example-item {
511
+ background: var(--bg-secondary) !important;
512
+ border: 1px solid var(--border-color) !important;
513
+ border-radius: 8px !important;
514
+ color: var(--text-primary) !important;
515
+ transition: all 0.2s ease !important;
516
+ }
517
+
518
+ .example-item:hover {
519
+ background: var(--bg-tertiary) !important;
520
+ border-color: var(--accent-primary) !important;
521
+ transform: translateX(4px) !important;
522
+ }
523
+
524
+ /* Radio buttons */
525
+ .radio-group {
526
+ background: var(--bg-tertiary) !important;
527
+ border-radius: 12px !important;
528
+ padding: 0.5rem !important;
529
+ }
530
+ """
531
+
532
+ # Create the Gradio interface with dark theme
533
+ with gr.Blocks(
534
+ theme=gr.themes.Base(
535
+ primary_hue="purple",
536
+ secondary_hue="indigo",
537
+ neutral_hue="slate",
538
+ font=[gr.themes.GoogleFont("Inter"), "system-ui", "sans-serif"]
539
+ ).set(
540
+ body_background_fill="#0d1117",
541
+ body_background_fill_dark="#0d1117",
542
+ block_background_fill="#161b22",
543
+ block_background_fill_dark="#161b22",
544
+ input_background_fill="#21262d",
545
+ input_background_fill_dark="#21262d",
546
+ button_primary_background_fill="linear-gradient(135deg, #8b5cf6 0%, #6366f1 100%)",
547
+ button_primary_background_fill_dark="linear-gradient(135deg, #8b5cf6 0%, #6366f1 100%)",
548
+ ),
549
+ css=custom_css,
550
+ title="Docs Navigator MCP - AI Documentation Assistant",
551
+ ) as demo:
552
+
553
+ # Header with modern dark gradient
554
+ gr.HTML("""
555
+ <div class="header-container">
556
+ <h1 class="header-title">🚀 Docs Navigator MCP</h1>
557
+ <p class="header-subtitle">Intelligent Documentation Assistant Powered by Claude AI</p>
558
+ </div>
559
+ """)
560
+
561
+ with gr.Tabs() as tabs:
562
+ # Chat Tab - Messenger Style
563
+ with gr.Tab("💬 Chat", id=0):
564
+ gr.HTML("""
565
+ <div style='padding: 1rem 0 0.5rem 0; color: var(--text-secondary); font-size: 0.95rem;'>
566
+ <strong style='color: var(--accent-secondary);'>💬 AI Chat Assistant</strong><br>
567
+ Ask anything about your documentation - I have full context of all your files.
568
+ </div>
569
+ """)
570
+
571
+ chatbot = gr.Chatbot(
572
+ height=550,
573
+ placeholder="<div style='text-align: center; padding: 3rem; color: #8b949e;'><div style='font-size: 3rem; margin-bottom: 1rem;'>💬</div><div style='font-size: 1.2rem; font-weight: 600; color: #e6edf3; margin-bottom: 0.5rem;'>Start a Conversation</div><div>Ask me anything about your documentation!</div></div>",
574
+ show_label=False,
575
+ avatar_images=("👤", "🤖"),
576
+ type="messages",
577
+ layout="bubble",
578
+ show_copy_button=True
579
+ )
580
+
581
+ with gr.Row():
582
+ msg = gr.Textbox(
583
+ placeholder="💭 Message the docs assistant...",
584
+ show_label=False,
585
+ scale=5,
586
+ container=False,
587
+ lines=1,
588
+ max_lines=3
589
+ )
590
+ submit_btn = gr.Button("Send", scale=1, variant="primary", elem_classes="primary-button", size="lg")
591
+
592
+ with gr.Row():
593
+ clear_btn = gr.Button("🗑️ Clear", size="sm", variant="secondary")
594
+ gr.HTML("<div style='flex-grow: 1;'></div>")
595
+
596
+ with gr.Accordion("💡 Example Questions", open=False):
597
+ gr.Examples(
598
+ examples=[
599
+ "What is this documentation about?",
600
+ "How do I get started with setup?",
601
+ "What are the main features?",
602
+ "Show me troubleshooting steps",
603
+ "What configuration options are available?",
604
+ "Explain the architecture"
605
+ ],
606
+ inputs=msg,
607
+ label=None
608
+ )
609
+
610
+ # Document Analysis Tab
611
+ with gr.Tab("🔍 Analysis", id=1):
612
+ gr.HTML("""
613
+ <div style='padding: 1rem 0 0.5rem 0; color: var(--text-secondary); font-size: 0.95rem;'>
614
+ <strong style='color: var(--accent-secondary);'>🔍 Document Intelligence</strong><br>
615
+ Deep analysis of individual documentation files with AI-powered insights.
616
+ </div>
617
+ """)
618
+
619
+ with gr.Row():
620
+ with gr.Column(scale=1):
621
+ file_dropdown = gr.Dropdown(
622
+ choices=get_available_files(),
623
+ label="📄 Select Document",
624
+ interactive=True,
625
+ container=True
626
+ )
627
+
628
+ analysis_type = gr.Radio(
629
+ choices=["Summary", "Key Concepts", "Readability", "Q&A Extraction"],
630
+ value="Summary",
631
+ label="🎯 Analysis Type",
632
+ container=True
633
+ )
634
+
635
+ analyze_btn = gr.Button("🔎 Analyze Document", variant="primary", elem_classes="primary-button", size="lg")
636
+ refresh_btn = gr.Button("🔄 Refresh Files", size="sm", variant="secondary")
637
+
638
+ with gr.Column(scale=2):
639
+ analysis_output = gr.Markdown(
640
+ value="<div style='text-align: center; padding: 3rem; color: #8b949e;'><div style='font-size: 2.5rem; margin-bottom: 1rem;'>📊</div><div style='font-size: 1.1rem;'>Select a document and analysis type to begin</div></div>",
641
+ elem_classes="info-card"
642
+ )
643
+
644
+ # Statistics Tab
645
+ with gr.Tab("📊 Stats", id=2):
646
+ gr.HTML("""
647
+ <div style='padding: 1rem 0 0.5rem 0; color: var(--text-secondary); font-size: 0.95rem;'>
648
+ <strong style='color: var(--accent-secondary);'>📊 Documentation Overview</strong><br>
649
+ Real-time statistics and insights about your documentation collection.
650
+ </div>
651
+ """)
652
+
653
+ stats_display = gr.Markdown(
654
+ value=get_document_stats(),
655
+ elem_classes="stats-box"
656
+ )
657
+
658
+ refresh_stats_btn = gr.Button("🔄 Refresh Statistics", variant="secondary", size="sm")
659
+
660
+ # Settings Tab
661
+ with gr.Tab("⚙️ Settings", id=3):
662
+ gr.HTML("""
663
+ <div style='padding: 1rem 0 0.5rem 0; color: var(--text-secondary); font-size: 0.95rem;'>
664
+ <strong style='color: var(--accent-secondary);'>⚙️ Configuration</strong><br>
665
+ Customize the AI assistant behavior and view system information.
666
+ </div>
667
+ """)
668
+
669
+ with gr.Group():
670
+ custom_system_prompt = gr.Textbox(
671
+ label="🎨 Custom System Prompt (Optional)",
672
+ placeholder="Enter a custom system prompt to modify the AI's personality and behavior...",
673
+ lines=6,
674
+ info="💡 Leave empty to use the default documentation assistant prompt",
675
+ container=True
676
+ )
677
+
678
+ gr.HTML("""
679
+ <div style='margin-top: 2rem; padding: 2rem; background: linear-gradient(135deg, rgba(139, 92, 246, 0.1) 0%, rgba(99, 102, 241, 0.1) 100%); border-radius: 16px; border: 1px solid var(--border-color);'>
680
+ <h3 style='color: var(--accent-secondary); margin: 0 0 1rem 0; font-size: 1.5rem;'>📖 About Docs Navigator MCP</h3>
681
+ <p style='color: var(--text-primary); line-height: 1.8; margin-bottom: 1.5rem;'>
682
+ An intelligent documentation assistant combining <strong>Claude AI</strong> with
683
+ <strong>Model Context Protocol (MCP)</strong> for powerful document analysis and Q&A.
684
+ </p>
685
+ <div style='display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; margin-top: 1.5rem;'>
686
+ <div style='padding: 1rem; background: rgba(139, 92, 246, 0.1); border-radius: 12px; border: 1px solid rgba(139, 92, 246, 0.2);'>
687
+ <div style='font-size: 1.5rem; margin-bottom: 0.5rem;'>🤖</div>
688
+ <div style='color: var(--text-primary); font-weight: 600;'>Claude 3.5 Sonnet</div>
689
+ <div style='color: var(--text-secondary); font-size: 0.85rem;'>Latest AI Model</div>
690
+ </div>
691
+ <div style='padding: 1rem; background: rgba(139, 92, 246, 0.1); border-radius: 12px; border: 1px solid rgba(139, 92, 246, 0.2);'>
692
+ <div style='font-size: 1.5rem; margin-bottom: 0.5rem;'>🔍</div>
693
+ <div style='color: var(--text-primary); font-weight: 600;'>Smart Analysis</div>
694
+ <div style='color: var(--text-secondary); font-size: 0.85rem;'>Document Intelligence</div>
695
+ </div>
696
+ <div style='padding: 1rem; background: rgba(139, 92, 246, 0.1); border-radius: 12px; border: 1px solid rgba(139, 92, 246, 0.2);'>
697
+ <div style='font-size: 1.5rem; margin-bottom: 0.5rem;'>📄</div>
698
+ <div style='color: var(--text-primary); font-weight: 600;'>Multi-Format</div>
699
+ <div style='color: var(--text-secondary); font-size: 0.85rem;'>MD, TXT, RST, PDF</div>
700
+ </div>
701
+ <div style='padding: 1rem; background: rgba(139, 92, 246, 0.1); border-radius: 12px; border: 1px solid rgba(139, 92, 246, 0.2);'>
702
+ <div style='font-size: 1.5rem; margin-bottom: 0.5rem;'>⚡</div>
703
+ <div style='color: var(--text-primary); font-weight: 600;'>Fast & Responsive</div>
704
+ <div style='color: var(--text-secondary); font-size: 0.85rem;'>Streaming Responses</div>
705
+ </div>
706
+ </div>
707
+ </div>
708
+ """)
709
+
710
+ gr.HTML("<div style='margin: 2rem 0 1rem 0; padding-top: 1.5rem; border-top: 1px solid var(--border-color);'></div>")
711
+
712
+ api_status = gr.Markdown(
713
+ value=f"""
714
+ ### 🔑 API Status
715
+
716
+ {"✅ **Anthropic API Key Configured** - Ready to use!" if ANTHROPIC_API_KEY else "⚠️ **API Key Missing** - Set `ANTHROPIC_API_KEY` in your `.env` file"}
717
+ """,
718
+ elem_classes="info-card"
719
+ )
720
+
721
+ # Event handlers
722
+ def respond(message, chat_history, system_prompt):
723
+ if not message.strip():
724
+ return "", chat_history
725
+
726
+ response = chat_with_docs(message, chat_history, system_prompt if system_prompt.strip() else None)
727
+ chat_history.append({"role": "user", "content": message})
728
+ chat_history.append({"role": "assistant", "content": response})
729
+ return "", chat_history
730
+
731
+ msg.submit(respond, [msg, chatbot, custom_system_prompt], [msg, chatbot])
732
+ submit_btn.click(respond, [msg, chatbot, custom_system_prompt], [msg, chatbot])
733
+ clear_btn.click(lambda: [], None, chatbot)
734
+
735
+ analyze_btn.click(
736
+ analyze_document,
737
+ [file_dropdown, analysis_type],
738
+ analysis_output
739
+ )
740
+
741
+ refresh_btn.click(
742
+ lambda: gr.update(choices=get_available_files()),
743
+ None,
744
+ file_dropdown
745
+ )
746
+
747
+ refresh_stats_btn.click(
748
+ get_document_stats,
749
+ None,
750
+ stats_display
751
+ )
752
 
 
 
753
 
754
+ if __name__ == "__main__":
755
+ print("🚀 Starting Docs Navigator MCP...")
756
+ print("📚 AI-Powered Documentation Assistant")
757
+ print("💡 Ask questions about your documentation!")
758
+ print("-" * 50)
759
+
760
+ # Detect if running on Hugging Face Spaces
761
+ is_spaces = os.getenv("SPACE_ID") is not None
762
+
763
+ demo.launch(
764
+ server_name="0.0.0.0" if is_spaces else "127.0.0.1",
765
+ server_port=7860,
766
+ show_error=True,
767
+ share=False
768
+ )
requirements.txt CHANGED
@@ -1,5 +1,5 @@
1
  mcp[cli]>=0.1.0
2
  anthropic>=0.36.0
3
  python-dotenv>=1.0.1
4
- gradio>=5.0.0
5
  PyPDF2>=3.0.0
 
1
  mcp[cli]>=0.1.0
2
  anthropic>=0.36.0
3
  python-dotenv>=1.0.1
4
+ gradio>=6.0.2
5
  PyPDF2>=3.0.0
server.py DELETED
File without changes
src/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # Docs Navigator MCP package
src/agent/client.py ADDED
@@ -0,0 +1,215 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # client_agent.py
2
+ import asyncio
3
+ from contextlib import AsyncExitStack
4
+ from typing import Optional
5
+
6
+ from dotenv import load_dotenv
7
+ from anthropic import Anthropic
8
+
9
+ from mcp import ClientSession, StdioServerParameters
10
+ from mcp.client.stdio import stdio_client
11
+
12
+ load_dotenv()
13
+
14
+
15
+ class DocsNavigatorClient:
16
+ def __init__(self):
17
+ self.session: Optional[ClientSession] = None
18
+ self.exit_stack = AsyncExitStack()
19
+ self.anthropic = Anthropic()
20
+ self._tools_cache = None
21
+
22
+ async def connect(self, server_script_path: str = "src/server/server.py"):
23
+ """
24
+ Start the docs MCP server (via stdio) and initialize a session.
25
+ """
26
+ import os
27
+ import sys
28
+
29
+ # Try to use uv run first, then fall back to the virtual environment python
30
+ if os.path.exists(".venv/Scripts/python.exe"):
31
+ # Windows virtual environment
32
+ python_path = ".venv/Scripts/python.exe"
33
+ params = StdioServerParameters(
34
+ command=python_path,
35
+ args=[server_script_path],
36
+ env=None,
37
+ )
38
+ elif os.path.exists(".venv/bin/python"):
39
+ # Unix virtual environment
40
+ python_path = ".venv/bin/python"
41
+ params = StdioServerParameters(
42
+ command=python_path,
43
+ args=[server_script_path],
44
+ env=None,
45
+ )
46
+ else:
47
+ # Fallback to system python
48
+ params = StdioServerParameters(
49
+ command="python",
50
+ args=[server_script_path],
51
+ env=None,
52
+ )
53
+ stdio_transport = await self.exit_stack.enter_async_context(
54
+ stdio_client(params)
55
+ )
56
+ self.stdio, self.write = stdio_transport
57
+ self.session = await self.exit_stack.enter_async_context(
58
+ ClientSession(self.stdio, self.write)
59
+ )
60
+ await self.session.initialize()
61
+
62
+ tools_response = await self.session.list_tools()
63
+ self._tools_cache = [
64
+ {
65
+ "name": t.name,
66
+ "description": t.description,
67
+ "input_schema": t.inputSchema,
68
+ }
69
+ for t in tools_response.tools
70
+ ]
71
+
72
+ async def close(self):
73
+ await self.exit_stack.aclose()
74
+
75
+ async def answer(self, user_query: str) -> str:
76
+ """
77
+ Ask the LLM to answer a question, using docs tools when needed.
78
+ Supports multi-turn conversations with multiple tool calls.
79
+ """
80
+ if not self.session:
81
+ raise RuntimeError("MCP session not initialized. Call connect() first.")
82
+
83
+ if self._tools_cache is None:
84
+ tools_response = await self.session.list_tools()
85
+ self._tools_cache = [
86
+ {
87
+ "name": t.name,
88
+ "description": t.description,
89
+ "input_schema": t.inputSchema,
90
+ }
91
+ for t in tools_response.tools
92
+ ]
93
+
94
+ messages = [
95
+ {
96
+ "role": "user",
97
+ "content": (
98
+ "You are a documentation assistant. "
99
+ "Use the available MCP tools to search and read docs in order "
100
+ "to answer the question. You can use multiple tools and think through "
101
+ "your response step by step. Always reference the files you used.\n\n"
102
+ f"User question: {user_query}"
103
+ ),
104
+ }
105
+ ]
106
+
107
+ tools = self._tools_cache
108
+ max_iterations = 10 # Prevent infinite loops
109
+ iteration = 0
110
+
111
+ while iteration < max_iterations:
112
+ iteration += 1
113
+
114
+ # Call the LLM
115
+ response = self.anthropic.messages.create(
116
+ model="claude-3-haiku-20240307",
117
+ max_tokens=2500, # Increased token limit for longer responses
118
+ messages=messages,
119
+ tools=tools,
120
+ )
121
+
122
+ # Add assistant's response to conversation
123
+ messages.append({
124
+ "role": "assistant",
125
+ "content": response.content,
126
+ })
127
+
128
+ # Check if there are any tool calls to execute
129
+ tool_calls = [content for content in response.content if content.type == "tool_use"]
130
+
131
+ if not tool_calls:
132
+ # No more tool calls - we're done
133
+ text_content = [content.text for content in response.content if content.type == "text"]
134
+ return "\n".join(text_content) if text_content else "[no text response from model]"
135
+
136
+ # Execute all tool calls in this round
137
+ tool_results = []
138
+ for tool_call in tool_calls:
139
+ try:
140
+ tool_name = tool_call.name
141
+ tool_args = tool_call.input
142
+
143
+ # Call the MCP tool
144
+ result = await self.session.call_tool(tool_name, tool_args)
145
+
146
+ tool_results.append({
147
+ "type": "tool_result",
148
+ "tool_use_id": tool_call.id,
149
+ "content": result.content,
150
+ })
151
+ except Exception as e:
152
+ # Handle tool errors gracefully
153
+ tool_results.append({
154
+ "type": "tool_result",
155
+ "tool_use_id": tool_call.id,
156
+ "content": f"Error calling tool {tool_call.name}: {str(e)}",
157
+ "is_error": True,
158
+ })
159
+
160
+ # Add tool results to conversation
161
+ messages.append({
162
+ "role": "user",
163
+ "content": tool_results,
164
+ })
165
+
166
+ # If we hit max iterations, return what we have so far
167
+ text_content = []
168
+ for message in messages:
169
+ if message["role"] == "assistant":
170
+ for content in message["content"]:
171
+ if hasattr(content, 'type') and content.type == "text":
172
+ text_content.append(content.text)
173
+ elif isinstance(content, dict) and content.get("type") == "text":
174
+ text_content.append(content.get("text", ""))
175
+
176
+ return "\n".join(text_content) if text_content else "[reached max iterations without final response]"
177
+
178
+
179
+ # Thread-local storage for client instances
180
+ import threading
181
+ _thread_local = threading.local()
182
+
183
+
184
+ def answer_sync(user_query: str) -> str:
185
+ """
186
+ Synchronous wrapper so Gradio can call into our async flow easily.
187
+ Creates a new client for each request to avoid event loop conflicts.
188
+ """
189
+ import concurrent.futures
190
+
191
+ def run_in_new_loop():
192
+ # Create a new event loop in this thread
193
+ loop = asyncio.new_event_loop()
194
+ asyncio.set_event_loop(loop)
195
+ try:
196
+ return loop.run_until_complete(_answer_async(user_query))
197
+ finally:
198
+ loop.close()
199
+
200
+ # Run in a separate thread to avoid conflicts with Gradio's event loop
201
+ with concurrent.futures.ThreadPoolExecutor() as executor:
202
+ future = executor.submit(run_in_new_loop)
203
+ return future.result()
204
+
205
+
206
+ async def _answer_async(user_query: str) -> str:
207
+ """
208
+ Create a fresh client for each request to avoid event loop issues.
209
+ """
210
+ client = DocsNavigatorClient()
211
+ try:
212
+ await client.connect("src/server/server.py")
213
+ return await client.answer(user_query)
214
+ finally:
215
+ await client.close()
src/server/__init__.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ # MCP Module
2
+ from .server import mcp
3
+
4
+ __all__ = ["mcp"]
src/server/server.py ADDED
@@ -0,0 +1,1241 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # server_docs.py
2
+ from __future__ import annotations
3
+ from pathlib import Path
4
+ from typing import List, Dict, Any
5
+
6
+ from mcp.server.fastmcp import FastMCP
7
+ import sys
8
+ import os
9
+ sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..'))
10
+ from document_intelligence import DocumentIntelligence
11
+
12
+ # Import PDF processing library
13
+ try:
14
+ import PyPDF2
15
+ PDF_SUPPORT = True
16
+ except ImportError:
17
+ PDF_SUPPORT = False
18
+ print("Warning: PyPDF2 not installed. PDF support disabled.")
19
+
20
+ # Name your server – this is what clients see
21
+ mcp = FastMCP("DocsNavigator")
22
+
23
+ DOCS_ROOT = Path(__file__).parent.parent.parent / "docs"
24
+ doc_intel = DocumentIntelligence(DOCS_ROOT)
25
+
26
+
27
+ def _iter_docs() -> list[Path]:
28
+ exts = {".md", ".txt", ".rst"}
29
+ if PDF_SUPPORT:
30
+ exts.add(".pdf")
31
+ return [
32
+ p for p in DOCS_ROOT.rglob("*")
33
+ if p.is_file() and p.suffix.lower() in exts
34
+ ]
35
+
36
+
37
+ def _read_file(path: Path) -> str:
38
+ if path.suffix.lower() == ".pdf":
39
+ return _read_pdf_file(path)
40
+ else:
41
+ return path.read_text(encoding="utf-8", errors="ignore")
42
+
43
+
44
+ def _read_pdf_file(path: Path) -> str:
45
+ """Extract text from PDF file."""
46
+ if not PDF_SUPPORT:
47
+ return f"PDF support not available. Install PyPDF2 to read {path.name}"
48
+
49
+ try:
50
+ text = ""
51
+ with open(path, 'rb') as file:
52
+ pdf_reader = PyPDF2.PdfReader(file)
53
+
54
+ for page_num, page in enumerate(pdf_reader.pages):
55
+ try:
56
+ page_text = page.extract_text()
57
+ if page_text:
58
+ text += f"\n--- Page {page_num + 1} ---\n{page_text}\n"
59
+ except Exception as e:
60
+ text += f"\n--- Page {page_num + 1} (Error reading: {str(e)}) ---\n"
61
+
62
+ return text if text.strip() else f"No text could be extracted from {path.name}"
63
+
64
+ except Exception as e:
65
+ return f"Error reading PDF {path.name}: {str(e)}"
66
+
67
+
68
+ def _extract_hierarchical_sections(content: str) -> List[Dict[str, str]]:
69
+ """Extract sections including their subsections for better content access."""
70
+ lines = content.split('\n')
71
+ headers = []
72
+
73
+ # Identify all headers
74
+ for i, line in enumerate(lines):
75
+ stripped = line.strip()
76
+ if stripped.startswith('#'):
77
+ level = len(stripped) - len(stripped.lstrip('#'))
78
+ title = stripped.lstrip('#').strip()
79
+ headers.append({
80
+ 'title': stripped,
81
+ 'clean_title': title,
82
+ 'level': level,
83
+ 'line_index': i
84
+ })
85
+
86
+ if not headers:
87
+ return [{'title': 'Document Content', 'content': content.strip()}]
88
+
89
+ hierarchical_sections = []
90
+
91
+ # Extract content for each header including subsections
92
+ for i, header in enumerate(headers):
93
+ start_line = header['line_index']
94
+
95
+ # Find content that belongs to this section (including subsections)
96
+ end_line = len(lines)
97
+ for j in range(i + 1, len(headers)):
98
+ next_header = headers[j]
99
+ # Only stop at headers of the same or higher level (lower number)
100
+ if next_header['level'] <= header['level']:
101
+ end_line = next_header['line_index']
102
+ break
103
+
104
+ # Extract all content for this section (header + content + subsections)
105
+ section_lines = lines[start_line:end_line]
106
+ section_content = '\n'.join(section_lines).strip()
107
+
108
+ # Remove the header line itself from content for cleaner output
109
+ if section_content.startswith('#'):
110
+ content_lines = section_content.split('\n')[1:]
111
+ clean_content = '\n'.join(content_lines).strip()
112
+ else:
113
+ clean_content = section_content
114
+
115
+ hierarchical_sections.append({
116
+ 'title': header['title'],
117
+ 'content': clean_content,
118
+ 'level': header['level'],
119
+ 'includes_subsections': any(h['level'] > header['level'] for h in headers[i+1:] if h['line_index'] < end_line)
120
+ })
121
+
122
+ return hierarchical_sections
123
+
124
+
125
+ def _extract_sections(content: str) -> List[Dict[str, str]]:
126
+ """Extract sections from markdown content based on headers with proper hierarchy."""
127
+ lines = content.split('\n')
128
+ headers = []
129
+
130
+ # First pass: identify all headers with their positions
131
+ for i, line in enumerate(lines):
132
+ stripped = line.strip()
133
+ if stripped.startswith('#'):
134
+ level = len(stripped) - len(stripped.lstrip('#'))
135
+ title = stripped.lstrip('#').strip()
136
+ headers.append({
137
+ 'title': stripped,
138
+ 'clean_title': title,
139
+ 'level': level,
140
+ 'line_index': i
141
+ })
142
+
143
+ if not headers:
144
+ return [{'title': 'Document Content', 'content': content.strip()}]
145
+
146
+ sections = []
147
+
148
+ # Second pass: extract content for each header
149
+ for i, header in enumerate(headers):
150
+ start_line = header['line_index'] + 1
151
+
152
+ # Find the end of this section (next header of same or higher level)
153
+ end_line = len(lines)
154
+ for j in range(i + 1, len(headers)):
155
+ next_header = headers[j]
156
+ if next_header['level'] <= header['level']:
157
+ end_line = next_header['line_index']
158
+ break
159
+
160
+ # Extract content for this section
161
+ section_lines = lines[start_line:end_line]
162
+ section_content = '\n'.join(section_lines).strip()
163
+
164
+ sections.append({
165
+ 'title': header['title'],
166
+ 'content': section_content,
167
+ 'level': header['level']
168
+ })
169
+
170
+ return sections
171
+
172
+
173
+ def _extract_headers(content: str) -> List[Dict[str, Any]]:
174
+ """Extract header hierarchy from markdown content."""
175
+ headers = []
176
+ lines = content.split('\n')
177
+
178
+ for line_num, line in enumerate(lines, 1):
179
+ stripped = line.strip()
180
+ if stripped.startswith('#'):
181
+ level = len(stripped) - len(stripped.lstrip('#'))
182
+ title = stripped.lstrip('#').strip()
183
+ headers.append({
184
+ 'level': level,
185
+ 'title': title,
186
+ 'line': line_num
187
+ })
188
+
189
+ return headers
190
+
191
+
192
+ def _create_outline(headers: List[Dict[str, Any]]) -> List[str]:
193
+ """Create a hierarchical outline from headers."""
194
+ outline = []
195
+ for header in headers:
196
+ indent = " " * (header['level'] - 1)
197
+ outline.append(f"{indent}- {header['title']}")
198
+ return outline
199
+
200
+
201
+ def _count_code_blocks(content: str) -> int:
202
+ """Count code blocks in markdown content."""
203
+ return content.count('```')
204
+
205
+
206
+ def _extract_links(content: str) -> List[str]:
207
+ """Extract links from markdown content."""
208
+ import re
209
+ # Match markdown links [text](url) and bare URLs
210
+ link_pattern = r'\[([^\]]+)\]\(([^)]+)\)|https?://[^\s\])]+'
211
+ matches = re.findall(link_pattern, content)
212
+ links = []
213
+ for match in matches:
214
+ if isinstance(match, tuple) and match[1]:
215
+ links.append(match[1]) # URL from [text](url)
216
+ elif isinstance(match, str):
217
+ links.append(match) # Bare URL
218
+ return links
219
+
220
+
221
+ def _generate_overview_summary(content: str, sections: List[Dict[str, str]]) -> str:
222
+ """Generate a concise overview summary."""
223
+ if not sections:
224
+ # If no sections, summarize the whole content
225
+ words = content.split()[:100] # First 100 words
226
+ return ' '.join(words) + "..." if len(content.split()) > 100 else ' '.join(words)
227
+
228
+ summary_parts = []
229
+
230
+ # Process all meaningful sections (skip empty ones)
231
+ for section in sections:
232
+ title = section['title'].lstrip('#').strip()
233
+ section_content = section['content'].strip()
234
+
235
+ # Skip empty sections
236
+ if not section_content:
237
+ continue
238
+
239
+ # For overview, take first 50 words of each section
240
+ content_words = section_content.split()[:50]
241
+ section_summary = ' '.join(content_words)
242
+ if len(section['content'].split()) > 50:
243
+ section_summary += "..."
244
+
245
+ summary_parts.append(f"**{title}**: {section_summary}")
246
+
247
+ # Limit to 5 sections for overview to avoid too much text
248
+ if len(summary_parts) >= 5:
249
+ break
250
+
251
+ # If we still have no content, fall back to first 100 words
252
+ if not summary_parts:
253
+ words = content.split()[:100]
254
+ return ' '.join(words) + "..." if len(content.split()) > 100 else ' '.join(words)
255
+
256
+ return '\n\n'.join(summary_parts)
257
+
258
+
259
+ def _extract_key_points(content: str, sections: List[Dict[str, str]]) -> str:
260
+ """Extract key points from content."""
261
+ key_points = []
262
+
263
+ # Look for bullet points and numbered lists in sections
264
+ for section in sections:
265
+ section_content = section['content']
266
+ lines = section_content.split('\n')
267
+
268
+ for line in lines:
269
+ stripped = line.strip()
270
+ if (stripped.startswith('- ') or
271
+ stripped.startswith('* ') or
272
+ stripped.startswith('+ ') or
273
+ (stripped and len(stripped) > 0 and stripped[0].isdigit() and '. ' in stripped)):
274
+ # Clean up the bullet point
275
+ clean_point = stripped.lstrip('- *+0123456789. ').strip()
276
+ if clean_point:
277
+ key_points.append(f"• {clean_point}")
278
+
279
+ if key_points:
280
+ return '\n'.join(key_points[:15]) # Top 15 points
281
+
282
+ # Fallback: extract sentences that contain key indicators from all content
283
+ sentences = content.replace('\n', ' ').split('.')
284
+ important_sentences = []
285
+ keywords = ['important', 'note', 'warning', 'key', 'must', 'should', 'required', 'avoid', 'best', 'practice']
286
+
287
+ for sentence in sentences:
288
+ sentence = sentence.strip()
289
+ if sentence and any(keyword in sentence.lower() for keyword in keywords):
290
+ important_sentences.append(f"• {sentence}.")
291
+
292
+ return '\n'.join(important_sentences[:8]) if important_sentences else "No specific key points identified."
293
+
294
+
295
+ def _generate_detailed_summary(content: str, sections: List[Dict[str, str]]) -> str:
296
+ """Generate a detailed summary with all sections."""
297
+ if not sections:
298
+ return content[:1500] + "..." if len(content) > 1500 else content
299
+
300
+ detailed_parts = []
301
+
302
+ for section in sections:
303
+ title = section['title'].lstrip('#').strip()
304
+ section_content = section['content'].strip()
305
+
306
+ # Skip empty sections
307
+ if not section_content:
308
+ continue
309
+
310
+ # For detailed summary, include more content
311
+ content_preview = section_content[:400]
312
+ if len(section_content) > 400:
313
+ content_preview += "..."
314
+
315
+ detailed_parts.append(f"## {title}\n{content_preview}")
316
+
317
+ # If no sections with content, return truncated full content
318
+ if not detailed_parts:
319
+ return content[:1500] + "..." if len(content) > 1500 else content
320
+
321
+ return '\n\n'.join(detailed_parts)
322
+
323
+
324
+ def _extract_technical_details(content: str, sections: List[Dict[str, str]]) -> str:
325
+ """Extract technical details like code, configurations, and specifications."""
326
+ technical_parts = []
327
+
328
+ # Extract code blocks
329
+ import re
330
+ code_blocks = re.findall(r'```[\s\S]*?```', content)
331
+ if code_blocks:
332
+ technical_parts.append("**Code Examples:**")
333
+ for i, block in enumerate(code_blocks[:3], 1):
334
+ technical_parts.append(f"Block {i}: {block[:100]}..." if len(block) > 100 else block)
335
+
336
+ # Extract technical terms (words in backticks)
337
+ tech_terms = re.findall(r'`([^`]+)`', content)
338
+ if tech_terms:
339
+ unique_terms = list(set(tech_terms))[:10]
340
+ technical_parts.append(f"**Technical Terms:** {', '.join(unique_terms)}")
341
+
342
+ # Look for configuration or specification patterns
343
+ config_lines = []
344
+ lines = content.split('\n')
345
+ for line in lines:
346
+ if ('config' in line.lower() or
347
+ 'setting' in line.lower() or
348
+ '=' in line or
349
+ ':' in line and not line.strip().startswith('#')):
350
+ config_lines.append(line.strip())
351
+
352
+ if config_lines:
353
+ technical_parts.append("**Configurations/Settings:**")
354
+ technical_parts.extend(config_lines[:5])
355
+
356
+ return '\n\n'.join(technical_parts) if technical_parts else "No specific technical details identified."
357
+
358
+
359
+ def _generate_brief_summary(content: str) -> str:
360
+ """Generate a very brief summary (1-2 sentences)."""
361
+ words = content.split()
362
+ if len(words) <= 30:
363
+ return content
364
+
365
+ # Take first sentence or first 30 words
366
+ sentences = content.split('.')
367
+ first_sentence = sentences[0].strip() + '.' if sentences else ''
368
+
369
+ if len(first_sentence.split()) <= 30:
370
+ return first_sentence
371
+ else:
372
+ return ' '.join(words[:30]) + "..."
373
+
374
+
375
+ @mcp.resource("docs://list")
376
+ def list_docs_resource() -> list[str]:
377
+ """
378
+ Resource that returns a simple list of available doc paths.
379
+ """
380
+ return [str(p.relative_to(DOCS_ROOT)) for p in _iter_docs()]
381
+
382
+
383
+ @mcp.resource("docs://{relative_path}")
384
+ def read_doc(relative_path: str) -> str:
385
+ """
386
+ Read a specific doc by relative path (e.g. 'getting-started.md').
387
+ """
388
+ path = (DOCS_ROOT / relative_path).resolve()
389
+ if not path.exists() or not path.is_file():
390
+ return f"Document not found: {relative_path}"
391
+ if DOCS_ROOT not in path.parents and DOCS_ROOT != path.parent:
392
+ return "Access denied: path escapes docs root."
393
+ return _read_file(path)
394
+
395
+
396
+ @mcp.tool()
397
+ def list_docs() -> List[str]:
398
+ """
399
+ List available documentation files relative to the docs/ folder.
400
+ """
401
+ return [str(p.relative_to(DOCS_ROOT)) for p in _iter_docs()]
402
+
403
+
404
+ @mcp.tool()
405
+ def search_docs(query: str, max_results: int = 10) -> List[Dict[str, str]]:
406
+ """
407
+ Improved full-text search over docs with better matching.
408
+
409
+ Args:
410
+ query: Search query string.
411
+ max_results: Max number of matches to return.
412
+ Returns:
413
+ List of {path, snippet} matches.
414
+ """
415
+ import re
416
+
417
+ query_lower = query.lower()
418
+ query_words = query_lower.split()
419
+ results: list[dict[str, str]] = []
420
+
421
+ for path in _iter_docs():
422
+ text = _read_file(path)
423
+ text_lower = text.lower()
424
+
425
+ # Score based on how many query words are found
426
+ matches = []
427
+
428
+ # First, try exact phrase match (highest score)
429
+ if query_lower in text_lower:
430
+ idx = text_lower.find(query_lower)
431
+ start = max(0, idx - 80)
432
+ end = min(len(text), idx + 80)
433
+ snippet = text[start:end].replace("\n", " ")
434
+ matches.append({
435
+ "score": 100,
436
+ "snippet": snippet,
437
+ "match_type": "exact_phrase"
438
+ })
439
+
440
+ # Then try to find sentences containing most query words
441
+ sentences = re.split(r'[.!?]+|\n\n+', text)
442
+ for sentence in sentences:
443
+ sentence_lower = sentence.lower()
444
+ word_matches = sum(1 for word in query_words if word in sentence_lower)
445
+
446
+ if word_matches >= max(1, len(query_words) * 0.6): # At least 60% of words
447
+ # Calculate score based on word matches and total words
448
+ score = (word_matches / len(query_words)) * 80
449
+ if len(sentence.strip()) > 20: # Prefer longer, more informative sentences
450
+ snippet = sentence.strip()[:160] + "..." if len(sentence.strip()) > 160 else sentence.strip()
451
+ matches.append({
452
+ "score": score,
453
+ "snippet": snippet,
454
+ "match_type": f"words_{word_matches}/{len(query_words)}"
455
+ })
456
+
457
+ # Add the best matches for this document
458
+ if matches:
459
+ # Sort by score and take the best match
460
+ best_match = max(matches, key=lambda x: x["score"])
461
+ results.append({
462
+ "path": str(path.relative_to(DOCS_ROOT)),
463
+ "snippet": best_match["snippet"],
464
+ "score": str(best_match["score"]),
465
+ "match_type": best_match["match_type"]
466
+ })
467
+
468
+ # Sort results by score (highest first) and limit
469
+ results.sort(key=lambda x: x["score"], reverse=True)
470
+ return results[:max_results]
471
+
472
+
473
+ @mcp.tool()
474
+ def extract_section(relative_path: str, section_title: str, include_subsections: bool = True) -> Dict[str, Any]:
475
+ """
476
+ Extract a specific section from a document.
477
+
478
+ Args:
479
+ relative_path: Path to the document relative to docs/ folder
480
+ section_title: Title of the section to extract (case-insensitive, partial matches allowed)
481
+ include_subsections: Whether to include subsections in the extracted content
482
+ Returns:
483
+ Dictionary with section content and metadata
484
+ """
485
+ path = (DOCS_ROOT / relative_path).resolve()
486
+ if not path.exists() or not path.is_file():
487
+ return {"error": f"Document not found: {relative_path}"}
488
+ if DOCS_ROOT not in path.parents and DOCS_ROOT != path.parent:
489
+ return {"error": "Access denied: path escapes docs root."}
490
+
491
+ content = _read_file(path)
492
+
493
+ # Use hierarchical extraction if including subsections, otherwise flat extraction
494
+ if include_subsections:
495
+ sections = _extract_hierarchical_sections(content)
496
+ else:
497
+ sections = _extract_sections(content)
498
+
499
+ # Find matching section (case-insensitive, partial match)
500
+ section_title_lower = section_title.lower()
501
+ matching_sections = []
502
+
503
+ for section in sections:
504
+ section_title_clean = section['title'].lstrip('#').strip().lower()
505
+ if section_title_lower in section_title_clean or section_title_clean in section_title_lower:
506
+ matching_sections.append(section)
507
+
508
+ if not matching_sections:
509
+ # List available sections for user reference
510
+ available_sections = [s['title'].lstrip('#').strip() for s in sections if s['content'].strip()]
511
+ return {
512
+ "error": f"Section '{section_title}' not found",
513
+ "available_sections": available_sections[:10], # Limit to first 10 for readability
514
+ "total_sections": str(len(available_sections))
515
+ }
516
+
517
+ if len(matching_sections) == 1:
518
+ section = matching_sections[0]
519
+ result = {
520
+ "document": relative_path,
521
+ "section_title": section['title'].lstrip('#').strip(),
522
+ "content": section['content'].strip(),
523
+ "word_count": str(len(section['content'].split())),
524
+ "match_type": "single",
525
+ "extraction_mode": "hierarchical" if include_subsections else "flat"
526
+ }
527
+
528
+ # Add metadata about subsections if available
529
+ if 'includes_subsections' in section:
530
+ result["includes_subsections"] = section['includes_subsections']
531
+ if 'level' in section:
532
+ result["header_level"] = section['level']
533
+
534
+ return result
535
+ else:
536
+ # Multiple matches - return all
537
+ results = []
538
+ for section in matching_sections:
539
+ section_info = {
540
+ "section_title": section['title'].lstrip('#').strip(),
541
+ "content": section['content'].strip(),
542
+ "word_count": str(len(section['content'].split()))
543
+ }
544
+ if 'level' in section:
545
+ section_info["header_level"] = section['level']
546
+ if 'includes_subsections' in section:
547
+ section_info["includes_subsections"] = section['includes_subsections']
548
+ results.append(section_info)
549
+
550
+ return {
551
+ "document": relative_path,
552
+ "match_type": "multiple",
553
+ "matching_sections": results,
554
+ "total_matches": str(len(results)),
555
+ "extraction_mode": "hierarchical" if include_subsections else "flat"
556
+ }
557
+
558
+
559
+ @mcp.tool()
560
+ def summarize_document(relative_path: str, summary_type: str = "overview") -> Dict[str, str]:
561
+ """
562
+ Generate a smart summary of a specific document.
563
+
564
+ Args:
565
+ relative_path: Path to the document relative to docs/ folder
566
+ summary_type: Type of summary - 'overview', 'key_points', 'detailed', or 'technical'
567
+ Returns:
568
+ Dictionary with document info and structured summary
569
+ """
570
+ path = (DOCS_ROOT / relative_path).resolve()
571
+ if not path.exists() or not path.is_file():
572
+ return {"error": f"Document not found: {relative_path}"}
573
+ if DOCS_ROOT not in path.parents and DOCS_ROOT != path.parent:
574
+ return {"error": "Access denied: path escapes docs root."}
575
+
576
+ content = _read_file(path)
577
+ word_count = len(content.split())
578
+
579
+ # Extract key sections based on markdown headers
580
+ sections = _extract_sections(content)
581
+
582
+ # Generate summary based on type
583
+ if summary_type == "key_points":
584
+ summary = _extract_key_points(content, sections)
585
+ elif summary_type == "detailed":
586
+ summary = _generate_detailed_summary(content, sections)
587
+ elif summary_type == "technical":
588
+ summary = _extract_technical_details(content, sections)
589
+ else: # overview
590
+ summary = _generate_overview_summary(content, sections)
591
+
592
+ return {
593
+ "document": relative_path,
594
+ "word_count": str(word_count),
595
+ "sections": str(len(sections)),
596
+ "summary_type": summary_type,
597
+ "summary": summary
598
+ }
599
+
600
+
601
+ @mcp.tool()
602
+ def analyze_document_structure(relative_path: str) -> Dict[str, Any]:
603
+ """
604
+ Analyze the structure and metadata of a document.
605
+
606
+ Args:
607
+ relative_path: Path to the document relative to docs/ folder
608
+ Returns:
609
+ Dictionary with structural analysis
610
+ """
611
+ path = (DOCS_ROOT / relative_path).resolve()
612
+ if not path.exists() or not path.is_file():
613
+ return {"error": f"Document not found: {relative_path}"}
614
+
615
+ content = _read_file(path)
616
+
617
+ # Extract headers and create outline
618
+ headers = _extract_headers(content)
619
+ sections = _extract_sections(content)
620
+
621
+ # Basic statistics
622
+ lines = content.split('\n')
623
+ words = content.split()
624
+
625
+ # Find code blocks and links
626
+ code_blocks = _count_code_blocks(content)
627
+ links = _extract_links(content)
628
+
629
+ return {
630
+ "document": relative_path,
631
+ "statistics": {
632
+ "lines": len(lines),
633
+ "words": len(words),
634
+ "characters": len(content),
635
+ "sections": str(len(sections)),
636
+ "code_blocks": code_blocks,
637
+ "links": len(links)
638
+ },
639
+ "structure": {
640
+ "headers": headers,
641
+ "outline": _create_outline(headers)
642
+ },
643
+ "content_analysis": {
644
+ "has_tables": "| " in content,
645
+ "has_images": "![" in content,
646
+ "has_code": "```" in content or " " in content,
647
+ "external_links": [link for link in links if link.startswith(('http', 'https'))]
648
+ }
649
+ }
650
+
651
+
652
+ @mcp.tool()
653
+ def generate_doc_overview() -> Dict[str, Any]:
654
+ """
655
+ Generate a comprehensive overview of the entire documentation set.
656
+
657
+ Returns:
658
+ Dictionary with overall documentation analysis
659
+ """
660
+ docs = _iter_docs()
661
+ overview = {
662
+ "total_documents": str(len(docs)),
663
+ "documents_by_type": {},
664
+ "total_content": {"words": 0, "lines": 0, "characters": 0},
665
+ "structure_analysis": {"sections": 0, "code_blocks": 0},
666
+ "document_summaries": []
667
+ }
668
+
669
+ for path in docs:
670
+ content = _read_file(path)
671
+ ext = path.suffix.lower()
672
+ rel_path = str(path.relative_to(DOCS_ROOT))
673
+
674
+ # Count by type
675
+ overview["documents_by_type"][ext] = overview["documents_by_type"].get(ext, 0) + 1
676
+
677
+ # Aggregate statistics
678
+ words = len(content.split())
679
+ lines = len(content.split('\n'))
680
+ chars = len(content)
681
+
682
+ overview["total_content"]["words"] += words
683
+ overview["total_content"]["lines"] += lines
684
+ overview["total_content"]["characters"] += chars
685
+
686
+ # Structure analysis
687
+ sections = len(_extract_sections(content))
688
+ code_blocks = _count_code_blocks(content)
689
+
690
+ overview["structure_analysis"]["sections"] += sections
691
+ overview["structure_analysis"]["code_blocks"] += code_blocks
692
+
693
+ # Brief summary for each doc
694
+ brief_summary = _generate_brief_summary(content)
695
+ overview["document_summaries"].append({
696
+ "path": rel_path,
697
+ "words": words,
698
+ "sections": sections,
699
+ "brief_summary": brief_summary
700
+ })
701
+
702
+ return overview
703
+
704
+
705
+ @mcp.tool()
706
+ def semantic_search(query: str, max_results: int = 5) -> List[Dict[str, Any]]:
707
+ """
708
+ Perform semantic search across documents using keyword matching and relevance scoring.
709
+
710
+ Args:
711
+ query: Search query
712
+ max_results: Maximum number of results to return
713
+ Returns:
714
+ List of documents with relevance scores and context
715
+ """
716
+ query_words = set(query.lower().split())
717
+ results = []
718
+
719
+ for path in _iter_docs():
720
+ content = _read_file(path)
721
+ content_lower = content.lower()
722
+
723
+ # Calculate relevance score
724
+ score = 0
725
+ context_snippets = []
726
+
727
+ for word in query_words:
728
+ word_count = content_lower.count(word)
729
+ score += word_count * len(word) # Longer words get higher weight
730
+
731
+ # Find context for each query word
732
+ word_positions = []
733
+ start = 0
734
+ while True:
735
+ pos = content_lower.find(word, start)
736
+ if pos == -1:
737
+ break
738
+ word_positions.append(pos)
739
+ start = pos + 1
740
+
741
+ # Get context snippets around found words
742
+ for pos in word_positions[:2]: # Max 2 snippets per word
743
+ snippet_start = max(0, pos - 60)
744
+ snippet_end = min(len(content), pos + 60)
745
+ snippet = content[snippet_start:snippet_end].replace('\n', ' ')
746
+ context_snippets.append(snippet)
747
+
748
+ if score > 0:
749
+ # Normalize score by document length
750
+ normalized_score = score / len(content.split())
751
+
752
+ results.append({
753
+ 'path': str(path.relative_to(DOCS_ROOT)),
754
+ 'relevance_score': normalized_score,
755
+ 'context_snippets': context_snippets[:3], # Max 3 snippets
756
+ 'word_count': len(content.split())
757
+ })
758
+
759
+ # Sort by relevance score
760
+ results.sort(key=lambda x: x['relevance_score'], reverse=True)
761
+ return results[:max_results]
762
+
763
+
764
+ @mcp.tool()
765
+ def compare_documents(doc1_path: str, doc2_path: str) -> Dict[str, Any]:
766
+ """
767
+ Compare two documents and identify similarities and differences.
768
+
769
+ Args:
770
+ doc1_path: Path to first document
771
+ doc2_path: Path to second document
772
+ Returns:
773
+ Comparison analysis
774
+ """
775
+ path1 = (DOCS_ROOT / doc1_path).resolve()
776
+ path2 = (DOCS_ROOT / doc2_path).resolve()
777
+
778
+ if not path1.exists() or not path2.exists():
779
+ return {"error": "One or both documents not found"}
780
+
781
+ content1 = _read_file(path1)
782
+ content2 = _read_file(path2)
783
+
784
+ # Basic statistics comparison
785
+ stats1 = {
786
+ "words": len(content1.split()),
787
+ "lines": len(content1.split('\n')),
788
+ "characters": len(content1)
789
+ }
790
+ stats2 = {
791
+ "words": len(content2.split()),
792
+ "lines": len(content2.split('\n')),
793
+ "characters": len(content2)
794
+ }
795
+
796
+ # Find common and unique words
797
+ words1 = set(word.lower().strip('.,!?;:') for word in content1.split())
798
+ words2 = set(word.lower().strip('.,!?;:') for word in content2.split())
799
+
800
+ common_words = words1.intersection(words2)
801
+ unique_to_doc1 = words1 - words2
802
+ unique_to_doc2 = words2 - words1
803
+
804
+ # Extract headers for structure comparison
805
+ headers1 = [h['title'] for h in _extract_headers(content1)]
806
+ headers2 = [h['title'] for h in _extract_headers(content2)]
807
+
808
+ return {
809
+ "document1": doc1_path,
810
+ "document2": doc2_path,
811
+ "statistics": {
812
+ "doc1": stats1,
813
+ "doc2": stats2,
814
+ "size_ratio": stats1["words"] / stats2["words"] if stats2["words"] > 0 else float('inf')
815
+ },
816
+ "content_similarity": {
817
+ "common_words_count": len(common_words),
818
+ "unique_to_doc1_count": len(unique_to_doc1),
819
+ "unique_to_doc2_count": len(unique_to_doc2),
820
+ "similarity_ratio": len(common_words) / len(words1.union(words2)) if len(words1.union(words2)) > 0 else 0
821
+ },
822
+ "structure_comparison": {
823
+ "doc1_headers": headers1,
824
+ "doc2_headers": headers2,
825
+ "common_headers": list(set(headers1).intersection(set(headers2))),
826
+ "unique_headers_doc1": list(set(headers1) - set(headers2)),
827
+ "unique_headers_doc2": list(set(headers2) - set(headers1))
828
+ },
829
+ "sample_unique_words": {
830
+ "doc1": list(unique_to_doc1)[:10],
831
+ "doc2": list(unique_to_doc2)[:10]
832
+ }
833
+ }
834
+
835
+
836
+ @mcp.tool()
837
+ def extract_definitions(relative_path: str) -> Dict[str, Any]:
838
+ """
839
+ Extract definitions, terms, and explanations from a document.
840
+
841
+ Args:
842
+ relative_path: Path to the document
843
+ Returns:
844
+ Extracted definitions and terms
845
+ """
846
+ path = (DOCS_ROOT / relative_path).resolve()
847
+ if not path.exists():
848
+ return {"error": f"Document not found: {relative_path}"}
849
+
850
+ content = _read_file(path)
851
+ definitions = []
852
+
853
+ # Look for definition patterns
854
+ import re
855
+
856
+ # Pattern 1: "Term: Definition" or "Term - Definition"
857
+ definition_patterns = [
858
+ r'^([A-Z][^:\-\n]+):\s*(.+)$', # Term: Definition
859
+ r'^([A-Z][^:\-\n]+)\s*-\s*(.+)$', # Term - Definition
860
+ r'\*\*([^*]+)\*\*:\s*([^\n]+)', # **Term**: Definition
861
+ r'`([^`]+)`:\s*([^\n]+)' # `Term`: Definition
862
+ ]
863
+
864
+ for pattern in definition_patterns:
865
+ matches = re.findall(pattern, content, re.MULTILINE)
866
+ for match in matches:
867
+ term, definition = match
868
+ definitions.append({
869
+ "term": term.strip(),
870
+ "definition": definition.strip(),
871
+ "type": "explicit"
872
+ })
873
+
874
+ # Look for glossary sections
875
+ sections = _extract_sections(content)
876
+ glossary_terms = []
877
+
878
+ for section in sections:
879
+ if any(keyword in section['title'].lower() for keyword in ['glossary', 'definition', 'terminology', 'terms']):
880
+ lines = section['content'].split('\n')
881
+ for line in lines:
882
+ if ':' in line or '-' in line:
883
+ parts = line.split(':') if ':' in line else line.split('-')
884
+ if len(parts) == 2:
885
+ glossary_terms.append({
886
+ "term": parts[0].strip(),
887
+ "definition": parts[1].strip(),
888
+ "type": "glossary"
889
+ })
890
+
891
+ # Extract technical terms (words in backticks)
892
+ tech_terms = re.findall(r'`([^`]+)`', content)
893
+ tech_terms_unique = list(set(tech_terms))
894
+
895
+ return {
896
+ "document": relative_path,
897
+ "definitions": definitions,
898
+ "glossary_terms": glossary_terms,
899
+ "technical_terms": tech_terms_unique,
900
+ "total_definitions": str(len(definitions) + len(glossary_terms)),
901
+ "definition_density": (len(definitions) + len(glossary_terms)) / len(content.split()) if content.split() else 0
902
+ }
903
+
904
+
905
+ @mcp.tool()
906
+ def generate_table_of_contents(relative_path: str = None) -> Dict[str, Any]:
907
+ """
908
+ Generate a table of contents for a specific document or all documents.
909
+
910
+ Args:
911
+ relative_path: Path to specific document, or None for all documents
912
+ Returns:
913
+ Table of contents structure
914
+ """
915
+ if relative_path:
916
+ # Single document TOC
917
+ path = (DOCS_ROOT / relative_path).resolve()
918
+ if not path.exists():
919
+ return {"error": f"Document not found: {relative_path}"}
920
+
921
+ content = _read_file(path)
922
+ headers = _extract_headers(content)
923
+
924
+ return {
925
+ "document": relative_path,
926
+ "table_of_contents": _create_outline(headers),
927
+ "header_count": len(headers),
928
+ "max_depth": max([h['level'] for h in headers]) if headers else 0
929
+ }
930
+ else:
931
+ # All documents TOC
932
+ all_toc = {}
933
+ for path in _iter_docs():
934
+ content = _read_file(path)
935
+ headers = _extract_headers(content)
936
+ rel_path = str(path.relative_to(DOCS_ROOT))
937
+
938
+ all_toc[rel_path] = {
939
+ "outline": _create_outline(headers),
940
+ "header_count": len(headers),
941
+ "max_depth": max([h['level'] for h in headers]) if headers else 0
942
+ }
943
+
944
+ return {
945
+ "type": "complete_documentation_toc",
946
+ "documents": all_toc,
947
+ "total_documents": str(len(all_toc))
948
+ }
949
+
950
+
951
+ @mcp.tool()
952
+ def intelligent_summarize(relative_path: str, summary_type: str = "medium", focus_keywords: str = None) -> Dict[str, Any]:
953
+ """
954
+ Generate an intelligent summary using advanced text analysis.
955
+
956
+ Args:
957
+ relative_path: Path to the document
958
+ summary_type: "short", "medium", or "long"
959
+ focus_keywords: Optional comma-separated keywords to focus on
960
+ Returns:
961
+ Intelligent summary with analysis
962
+ """
963
+ path = (DOCS_ROOT / relative_path).resolve()
964
+ if not path.exists():
965
+ return {"error": f"Document not found: {relative_path}"}
966
+
967
+ try:
968
+ content = _read_file(path)
969
+
970
+ # Use document intelligence for smart summary
971
+ summary_result = doc_intel.generate_smart_summary(content, summary_type)
972
+
973
+ # Add key concepts
974
+ key_concepts = doc_intel.extract_key_concepts(content)
975
+
976
+ # Add readability analysis
977
+ readability = doc_intel.analyze_readability(content)
978
+
979
+ # If focus keywords provided, highlight relevant sections
980
+ focused_content = None
981
+ if focus_keywords:
982
+ keywords = [k.strip() for k in focus_keywords.split(',')]
983
+ # Find sections that contain the keywords
984
+ sections = _extract_sections(content)
985
+ relevant_sections = []
986
+ for section in sections:
987
+ if section['content'].strip() and any(keyword.lower() in section['content'].lower() for keyword in keywords):
988
+ relevant_sections.append(section['title'].lstrip('#').strip())
989
+ focused_content = relevant_sections
990
+
991
+ return {
992
+ "document": relative_path,
993
+ "summary": summary_result,
994
+ "key_concepts": key_concepts[:10],
995
+ "readability": readability,
996
+ "focused_sections": focused_content,
997
+ "analysis_method": "advanced_intelligence"
998
+ }
999
+ except Exception as e:
1000
+ return {
1001
+ "error": f"Failed to analyze document: {str(e)}",
1002
+ "document": relative_path,
1003
+ "fallback_available": True
1004
+ }
1005
+
1006
+
1007
+ @mcp.tool()
1008
+ def extract_qa_pairs(relative_path: str = None) -> Dict[str, Any]:
1009
+ """
1010
+ Extract question-answer pairs from documents for FAQ generation.
1011
+
1012
+ Args:
1013
+ relative_path: Specific document path, or None for all documents
1014
+ Returns:
1015
+ Extracted Q&A pairs
1016
+ """
1017
+ if relative_path:
1018
+ path = (DOCS_ROOT / relative_path).resolve()
1019
+ if not path.exists():
1020
+ return {"error": f"Document not found: {relative_path}"}
1021
+
1022
+ content = _read_file(path)
1023
+ qa_pairs = doc_intel.extract_questions_and_answers(content)
1024
+
1025
+ return {
1026
+ "document": relative_path,
1027
+ "qa_pairs": qa_pairs,
1028
+ "total_pairs": str(len(qa_pairs))
1029
+ }
1030
+ else:
1031
+ # Extract from all documents
1032
+ all_qa_pairs = {}
1033
+ total_pairs = 0
1034
+
1035
+ for path in _iter_docs():
1036
+ content = _read_file(path)
1037
+ qa_pairs = doc_intel.extract_questions_and_answers(content)
1038
+ if qa_pairs:
1039
+ rel_path = str(path.relative_to(DOCS_ROOT))
1040
+ all_qa_pairs[rel_path] = qa_pairs
1041
+ total_pairs += len(qa_pairs)
1042
+
1043
+ return {
1044
+ "type": "complete_documentation_qa",
1045
+ "qa_by_document": all_qa_pairs,
1046
+ "total_pairs": str(total_pairs)
1047
+ }
1048
+
1049
+
1050
+ @mcp.tool()
1051
+ def find_related_documents(query: str, max_results: int = 3) -> List[Dict[str, Any]]:
1052
+ """
1053
+ Find documents most related to a query using advanced similarity scoring.
1054
+
1055
+ Args:
1056
+ query: Search query or topic
1057
+ max_results: Maximum number of related documents to return
1058
+ Returns:
1059
+ List of related documents with scores and explanations
1060
+ """
1061
+ all_docs = list(_iter_docs())
1062
+ related = doc_intel.find_related_content(query, all_docs, max_results)
1063
+
1064
+ return {
1065
+ "query": query,
1066
+ "related_documents": related,
1067
+ "total_analyzed": len(all_docs),
1068
+ "method": "tf-idf_similarity"
1069
+ }
1070
+
1071
+
1072
+ @mcp.tool()
1073
+ def analyze_document_gaps() -> Dict[str, Any]:
1074
+ """
1075
+ Analyze the documentation set to identify potential gaps or areas needing improvement.
1076
+
1077
+ Returns:
1078
+ Analysis of documentation completeness and suggestions
1079
+ """
1080
+ all_docs = list(_iter_docs())
1081
+ analysis = {
1082
+ "total_documents": len(all_docs),
1083
+ "coverage_analysis": {},
1084
+ "recommendations": [],
1085
+ "content_quality": {},
1086
+ "structure_issues": []
1087
+ }
1088
+
1089
+ # Analyze each document
1090
+ total_words = 0
1091
+ short_docs = []
1092
+ long_docs = []
1093
+ low_readability_docs = []
1094
+ missing_sections = []
1095
+
1096
+ common_sections = ['introduction', 'overview', 'getting started', 'configuration', 'examples', 'troubleshooting']
1097
+ section_coverage = {section: 0 for section in common_sections}
1098
+
1099
+ for path in all_docs:
1100
+ content = _read_file(path)
1101
+ rel_path = str(path.relative_to(DOCS_ROOT))
1102
+
1103
+ # Word count analysis
1104
+ word_count = len(content.split())
1105
+ total_words += word_count
1106
+
1107
+ if word_count < 100:
1108
+ short_docs.append(rel_path)
1109
+ elif word_count > 3000:
1110
+ long_docs.append(rel_path)
1111
+
1112
+ # Readability analysis
1113
+ readability = doc_intel.analyze_readability(content)
1114
+ if readability.get('flesch_score', 50) < 30:
1115
+ low_readability_docs.append(rel_path)
1116
+
1117
+ # Section coverage analysis
1118
+ headers = [h['title'].lower() for h in _extract_headers(content)]
1119
+ doc_sections = []
1120
+ for section in common_sections:
1121
+ if any(section in header for header in headers):
1122
+ section_coverage[section] += 1
1123
+ doc_sections.append(section)
1124
+
1125
+ missing = [s for s in common_sections if s not in doc_sections]
1126
+ if missing:
1127
+ missing_sections.append({"document": rel_path, "missing": missing})
1128
+
1129
+ # Generate recommendations
1130
+ if short_docs:
1131
+ analysis["recommendations"].append(f"Consider expanding these short documents: {', '.join(short_docs[:3])}")
1132
+
1133
+ if low_readability_docs:
1134
+ analysis["recommendations"].append(f"Improve readability of: {', '.join(low_readability_docs[:3])}")
1135
+
1136
+ # Find least covered sections
1137
+ least_covered = min(section_coverage.values())
1138
+ missing_section_types = [section for section, count in section_coverage.items() if count <= least_covered]
1139
+ if missing_section_types:
1140
+ analysis["recommendations"].append(f"Consider adding {', '.join(missing_section_types)} sections to more documents")
1141
+
1142
+ analysis["coverage_analysis"] = {
1143
+ "average_words_per_doc": total_words / len(all_docs) if all_docs else 0,
1144
+ "short_documents": short_docs,
1145
+ "long_documents": long_docs,
1146
+ "section_coverage": section_coverage
1147
+ }
1148
+
1149
+ analysis["content_quality"] = {
1150
+ "low_readability": low_readability_docs,
1151
+ "missing_common_sections": missing_sections
1152
+ }
1153
+
1154
+ return analysis
1155
+
1156
+
1157
+ @mcp.tool()
1158
+ def generate_documentation_index() -> Dict[str, Any]:
1159
+ """
1160
+ Generate a comprehensive searchable index of all documentation content.
1161
+
1162
+ Returns:
1163
+ Searchable index with topics, concepts, and cross-references
1164
+ """
1165
+ index = {
1166
+ "concepts": {}, # concept -> [documents]
1167
+ "topics": {}, # topic -> documents
1168
+ "cross_references": {}, # document -> related documents
1169
+ "metadata": {}
1170
+ }
1171
+
1172
+ all_docs = list(_iter_docs())
1173
+
1174
+ # Build concept index
1175
+ all_concepts = {}
1176
+
1177
+ for path in all_docs:
1178
+ content = _read_file(path)
1179
+ rel_path = str(path.relative_to(DOCS_ROOT))
1180
+
1181
+ # Extract concepts from this document
1182
+ concepts = doc_intel.extract_key_concepts(content, min_frequency=1)
1183
+
1184
+ # Add to global concept index
1185
+ for concept_info in concepts:
1186
+ concept = concept_info['concept']
1187
+ if concept not in all_concepts:
1188
+ all_concepts[concept] = []
1189
+ all_concepts[concept].append({
1190
+ "document": rel_path,
1191
+ "frequency": concept_info['frequency'],
1192
+ "type": concept_info['type']
1193
+ })
1194
+
1195
+ # Find cross-references (documents with similar concepts)
1196
+ related_docs = doc_intel.find_related_content(
1197
+ ' '.join([c['concept'] for c in concepts[:5]]),
1198
+ all_docs,
1199
+ max_results=3
1200
+ )
1201
+ index["cross_references"][rel_path] = [doc['path'] for doc in related_docs if doc['path'] != rel_path]
1202
+
1203
+ # Document metadata
1204
+ headers = _extract_headers(content)
1205
+ readability = doc_intel.analyze_readability(content)
1206
+
1207
+ index["metadata"][rel_path] = {
1208
+ "word_count": len(content.split()),
1209
+ "sections": len(headers),
1210
+ "readability_score": readability.get('flesch_score', 0),
1211
+ "main_topics": [c['concept'] for c in concepts[:5]]
1212
+ }
1213
+
1214
+ # Filter concepts that appear in multiple documents (more valuable for index)
1215
+ index["concepts"] = {
1216
+ concept: docs for concept, docs in all_concepts.items()
1217
+ if len(docs) > 1 or any(d['frequency'] > 2 for d in docs)
1218
+ }
1219
+
1220
+ # Create topic clusters
1221
+ topic_clusters = {}
1222
+ for concept, docs in index["concepts"].items():
1223
+ if len(docs) >= 2: # Concept appears in multiple docs
1224
+ topic_clusters[concept] = [doc['document'] for doc in docs]
1225
+
1226
+ index["topics"] = topic_clusters
1227
+
1228
+ return {
1229
+ "index": index,
1230
+ "statistics": {
1231
+ "total_concepts": len(index["concepts"]),
1232
+ "total_topics": len(index["topics"]),
1233
+ "total_documents": len(all_docs),
1234
+ "avg_cross_references": sum(len(refs) for refs in index["cross_references"].values()) / len(index["cross_references"]) if index["cross_references"] else 0
1235
+ }
1236
+ }
1237
+
1238
+
1239
+ if __name__ == "__main__":
1240
+ # stdio transport keeps it compatible with the official client pattern
1241
+ mcp.run(transport="stdio")
src/ui/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # UI module
src/ui/app.py ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app_gradio.py
2
+ import gradio as gr
3
+ import sys
4
+ import os
5
+ sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..'))
6
+ from src.agent.client import answer_sync
7
+
8
+
9
+ def chat_fn(message: str, history: list[dict]):
10
+ """Enhanced chat function with better error handling and user feedback"""
11
+ try:
12
+ if not message.strip():
13
+ return "Please enter a question about the documentation."
14
+
15
+ reply = answer_sync(message)
16
+ return reply
17
+ except Exception as e:
18
+ return f"⚠️ I encountered an error while processing your question: {str(e)}\n\nPlease try again or rephrase your question."
19
+
20
+
21
+ def main():
22
+ """Main entry point for the application when run via uv."""
23
+ print("🚀 Starting Docs Navigator MCP...")
24
+ print("📚 AI-Powered Documentation Assistant")
25
+ print("🌐 The app will be available at: http://127.0.0.1:7862")
26
+ print("💡 Ask questions about your documentation!")
27
+ print("-" * 50)
28
+
29
+ demo.launch(
30
+ server_name="127.0.0.1",
31
+ server_port=7862,
32
+ show_error=True,
33
+ share=False
34
+ )
35
+
36
+
37
+ # Professional theme configuration
38
+ professional_theme = gr.themes.Soft(
39
+ primary_hue="blue",
40
+ secondary_hue="slate",
41
+ neutral_hue="gray",
42
+ font=[
43
+ gr.themes.GoogleFont("Inter"),
44
+ "ui-sans-serif",
45
+ "system-ui",
46
+ "sans-serif"
47
+ ]
48
+ ).set(
49
+ body_background_fill="*neutral_50",
50
+ panel_background_fill="white",
51
+ button_primary_background_fill="*primary_600",
52
+ button_primary_background_fill_hover="*primary_700",
53
+ input_background_fill="white"
54
+ )
55
+
56
+ # Custom CSS for enhanced styling
57
+ custom_css = """
58
+ .gradio-container {
59
+ max-width: 1000px !important;
60
+ margin: 0 auto !important;
61
+ }
62
+
63
+ .chat-interface {
64
+ border-radius: 12px !important;
65
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1) !important;
66
+ }
67
+
68
+ .message {
69
+ border-radius: 8px !important;
70
+ margin: 6px 0 !important;
71
+ }
72
+
73
+ /* Enhanced input styling */
74
+ .input-container textarea {
75
+ border-radius: 8px !important;
76
+ border: 2px solid #e5e7eb !important;
77
+ transition: all 0.2s ease !important;
78
+ }
79
+
80
+ .input-container textarea:focus {
81
+ border-color: #3b82f6 !important;
82
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1) !important;
83
+ }
84
+ """
85
+
86
+ demo = gr.ChatInterface(
87
+ fn=chat_fn,
88
+ type="messages",
89
+ title="📚 Docs Navigator MCP",
90
+ description="🤖 **AI-Powered Documentation Assistant**\n\nAsk questions about your documentation and get intelligent, contextual answers. Powered by Claude AI and Model Context Protocol.",
91
+ theme=professional_theme,
92
+ css=custom_css,
93
+ chatbot=gr.Chatbot(
94
+ height=500,
95
+ show_label=False,
96
+ type="messages",
97
+ avatar_images=(
98
+ "https://api.dicebear.com/7.x/thumbs/svg?seed=user&backgroundColor=3b82f6",
99
+ "https://api.dicebear.com/7.x/bottts/svg?seed=docs&backgroundColor=1e40af"
100
+ )
101
+ ),
102
+ textbox=gr.Textbox(
103
+ placeholder="💭 Ask me anything about your documentation...",
104
+ container=False,
105
+ scale=7
106
+ ),
107
+ examples=[
108
+ "🚀 How do I get started with this project?",
109
+ "⚙️ What configuration options are available?",
110
+ "🔧 How do I troubleshoot connection issues?",
111
+ "📖 Tell me about the setup process",
112
+ "💡 What does the overview documentation explain?",
113
+ "📄 What information is in the PDF documents?"
114
+ ]
115
+ )
116
+
117
+ if __name__ == "__main__":
118
+ demo.launch(
119
+ server_name="127.0.0.1",
120
+ server_port=7860,
121
+ show_error=True
122
+ )
src/ui/enhanced.py ADDED
@@ -0,0 +1,304 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app_gradio_enhanced.py
2
+ import gradio as gr
3
+ from client_agent import answer_sync
4
+
5
+
6
+ def chat_fn(message: str, history: list[dict]):
7
+ """Enhanced chat function with better error handling"""
8
+ try:
9
+ reply = answer_sync(message)
10
+ return reply
11
+ except Exception as e:
12
+ return f"⚠️ Error: {str(e)}"
13
+
14
+
15
+ # Custom CSS for professional styling
16
+ custom_css = """
17
+ /* Main container styling */
18
+ .gradio-container {
19
+ max-width: 1200px !important;
20
+ margin: 0 auto !important;
21
+ }
22
+
23
+ /* Header styling */
24
+ .header-text {
25
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
26
+ -webkit-background-clip: text;
27
+ -webkit-text-fill-color: transparent;
28
+ background-clip: text;
29
+ text-align: center;
30
+ font-weight: bold;
31
+ margin-bottom: 1rem;
32
+ }
33
+
34
+ /* Chat message styling */
35
+ .message-wrap {
36
+ border-radius: 12px !important;
37
+ margin: 8px 0 !important;
38
+ padding: 12px 16px !important;
39
+ }
40
+
41
+ .user-message {
42
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
43
+ color: white !important;
44
+ }
45
+
46
+ .bot-message {
47
+ background: #f8f9fa !important;
48
+ border-left: 4px solid #667eea !important;
49
+ }
50
+
51
+ /* Input styling */
52
+ .input-container {
53
+ border-radius: 25px !important;
54
+ border: 2px solid #e9ecef !important;
55
+ transition: all 0.3s ease !important;
56
+ }
57
+
58
+ .input-container:focus-within {
59
+ border-color: #667eea !important;
60
+ box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1) !important;
61
+ }
62
+
63
+ /* Button styling */
64
+ .submit-btn {
65
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
66
+ border: none !important;
67
+ border-radius: 20px !important;
68
+ padding: 10px 20px !important;
69
+ font-weight: 600 !important;
70
+ transition: all 0.3s ease !important;
71
+ }
72
+
73
+ .submit-btn:hover {
74
+ transform: translateY(-2px) !important;
75
+ box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3) !important;
76
+ }
77
+
78
+ /* Status indicators */
79
+ .status-indicator {
80
+ display: inline-flex;
81
+ align-items: center;
82
+ gap: 8px;
83
+ padding: 6px 12px;
84
+ border-radius: 20px;
85
+ font-size: 14px;
86
+ font-weight: 500;
87
+ }
88
+
89
+ .status-online {
90
+ background: #d4edda;
91
+ color: #155724;
92
+ border: 1px solid #c3e6cb;
93
+ }
94
+
95
+ /* Loading animation */
96
+ @keyframes pulse {
97
+ 0% { opacity: 1; }
98
+ 50% { opacity: 0.5; }
99
+ 100% { opacity: 1; }
100
+ }
101
+
102
+ .loading {
103
+ animation: pulse 1.5s infinite;
104
+ }
105
+
106
+ /* Responsive design */
107
+ @media (max-width: 768px) {
108
+ .gradio-container {
109
+ padding: 10px !important;
110
+ }
111
+
112
+ .message-wrap {
113
+ margin: 4px 0 !important;
114
+ padding: 8px 12px !important;
115
+ }
116
+ }
117
+
118
+ /* Dark mode support */
119
+ .dark .bot-message {
120
+ background: #2d3748 !important;
121
+ color: #e2e8f0 !important;
122
+ border-left-color: #667eea !important;
123
+ }
124
+
125
+ .dark .input-container {
126
+ background: #2d3748 !important;
127
+ border-color: #4a5568 !important;
128
+ color: #e2e8f0 !important;
129
+ }
130
+ """
131
+
132
+
133
+ def create_enhanced_interface():
134
+ """Create an enhanced Gradio interface with professional styling"""
135
+
136
+ with gr.Blocks(
137
+ css=custom_css,
138
+ theme=gr.themes.Soft(
139
+ primary_hue="blue",
140
+ secondary_hue="purple",
141
+ neutral_hue="slate",
142
+ font=[
143
+ gr.themes.GoogleFont("Inter"),
144
+ "ui-sans-serif",
145
+ "system-ui",
146
+ "sans-serif"
147
+ ]
148
+ ),
149
+ title="📚 Docs Navigator - AI-Powered Documentation Assistant",
150
+ analytics_enabled=False
151
+ ) as interface:
152
+
153
+ # Header section
154
+ with gr.Row():
155
+ gr.Markdown(
156
+ """
157
+ # 📚 Docs Navigator MCP
158
+ ### AI-Powered Documentation Assistant
159
+
160
+ Ask questions about your documentation and get intelligent, contextual answers powered by Claude and MCP.
161
+ """,
162
+ elem_classes=["header-text"]
163
+ )
164
+
165
+ # Status indicator
166
+ with gr.Row():
167
+ gr.HTML(
168
+ """
169
+ <div class="status-indicator status-online">
170
+ <span>🟢</span>
171
+ <span>MCP Server Connected</span>
172
+ </div>
173
+ """,
174
+ visible=True
175
+ )
176
+
177
+ # Main chat interface
178
+ chat = gr.ChatInterface(
179
+ fn=chat_fn,
180
+ type="messages",
181
+ chatbot=gr.Chatbot(
182
+ height=500,
183
+ show_label=False,
184
+ container=True,
185
+ bubble_full_width=False,
186
+ avatar_images=(
187
+ "https://api.dicebear.com/7.x/thumbs/svg?seed=user&backgroundColor=667eea",
188
+ "https://api.dicebear.com/7.x/bottts/svg?seed=bot&backgroundColor=764ba2"
189
+ )
190
+ ),
191
+ textbox=gr.Textbox(
192
+ placeholder="Ask me anything about your documentation... 💭",
193
+ container=False,
194
+ scale=7,
195
+ elem_classes=["input-container"]
196
+ ),
197
+ submit_btn=gr.Button(
198
+ "Send 🚀",
199
+ variant="primary",
200
+ elem_classes=["submit-btn"]
201
+ ),
202
+ retry_btn=gr.Button("🔄 Retry", variant="secondary"),
203
+ undo_btn=gr.Button("↩️ Undo", variant="secondary"),
204
+ clear_btn=gr.Button("🗑️ Clear", variant="secondary"),
205
+ examples=[
206
+ "How do I set up AuroraAI?",
207
+ "What are the troubleshooting steps for connection issues?",
208
+ "Tell me about the configuration options",
209
+ "What does the overview documentation say?",
210
+ "How do I get started with this project?"
211
+ ]
212
+ )
213
+
214
+ # Footer section
215
+ with gr.Row():
216
+ gr.Markdown(
217
+ """
218
+ ---
219
+ <div style="text-align: center; color: #6c757d; font-size: 14px; margin-top: 20px;">
220
+ <p>
221
+ 🔧 Powered by <strong>Model Context Protocol (MCP)</strong> |
222
+ 🤖 <strong>Claude AI</strong> |
223
+ 🎨 <strong>Gradio</strong>
224
+ </p>
225
+ <p style="font-size: 12px; margin-top: 10px;">
226
+ 💡 Tip: Ask specific questions about your documentation for the best results!
227
+ </p>
228
+ </div>
229
+ """,
230
+ elem_classes=["footer"]
231
+ )
232
+
233
+ return interface
234
+
235
+
236
+ # Create the demo with different styling options
237
+ def create_minimal_interface():
238
+ """Create a minimal, clean interface"""
239
+ return gr.ChatInterface(
240
+ fn=chat_fn,
241
+ type="messages",
242
+ title="📚 Docs Navigator",
243
+ description="Clean, minimal documentation assistant",
244
+ theme=gr.themes.Monochrome(),
245
+ chatbot=gr.Chatbot(height=400, show_label=False),
246
+ textbox=gr.Textbox(placeholder="Ask about your docs...", container=False),
247
+ examples=["Setup guide", "Troubleshooting", "Configuration"]
248
+ )
249
+
250
+
251
+ def create_corporate_interface():
252
+ """Create a corporate/professional interface"""
253
+ corporate_theme = gr.themes.Default(
254
+ primary_hue="slate",
255
+ secondary_hue="blue",
256
+ neutral_hue="gray"
257
+ ).set(
258
+ body_background_fill="white",
259
+ panel_background_fill="*neutral_50",
260
+ button_primary_background_fill="*primary_600"
261
+ )
262
+
263
+ return gr.ChatInterface(
264
+ fn=chat_fn,
265
+ type="messages",
266
+ title="Documentation Assistant",
267
+ description="Enterprise AI Documentation System",
268
+ theme=corporate_theme,
269
+ chatbot=gr.Chatbot(
270
+ height=450,
271
+ show_label=False,
272
+ bubble_full_width=False
273
+ ),
274
+ textbox=gr.Textbox(
275
+ placeholder="Enter your documentation question...",
276
+ container=False
277
+ )
278
+ )
279
+
280
+
281
+ if __name__ == "__main__":
282
+ import sys
283
+
284
+ # Choose interface style based on command line argument
285
+ style = sys.argv[1] if len(sys.argv) > 1 else "enhanced"
286
+
287
+ if style == "minimal":
288
+ demo = create_minimal_interface()
289
+ elif style == "corporate":
290
+ demo = create_corporate_interface()
291
+ else: # enhanced (default)
292
+ demo = create_enhanced_interface()
293
+
294
+ # Launch with professional settings
295
+ demo.launch(
296
+ server_name="127.0.0.1",
297
+ server_port=7860,
298
+ share=False,
299
+ show_error=True,
300
+ quiet=False,
301
+ favicon_path=None, # You can add a custom favicon here
302
+ ssl_verify=False,
303
+ show_tips=True
304
+ )
src/ui/showcase.py ADDED
@@ -0,0 +1,402 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # gradio_ui_showcase.py
2
+ """
3
+ Showcase different Gradio UI/UX options for the Docs Navigator
4
+ Run with: python gradio_ui_showcase.py [style_name]
5
+
6
+ Available styles:
7
+ - modern: Modern, clean design with animations
8
+ - dark: Dark theme professional interface
9
+ - minimal: Minimal, distraction-free design
10
+ - corporate: Enterprise/business-focused styling
11
+ - glassmorphism: Modern glass-effect design
12
+ """
13
+
14
+ import gradio as gr
15
+ from client_agent import answer_sync
16
+ import sys
17
+
18
+
19
+ def chat_fn(message: str, history: list[dict]):
20
+ """Enhanced chat function"""
21
+ try:
22
+ if not message.strip():
23
+ return "Please enter a question about the documentation."
24
+ reply = answer_sync(message)
25
+ return reply
26
+ except Exception as e:
27
+ return f"⚠️ Error: {str(e)}"
28
+
29
+
30
+ def create_modern_interface():
31
+ """Modern, animated interface with gradient backgrounds"""
32
+ modern_css = """
33
+ @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap');
34
+
35
+ .gradio-container {
36
+ font-family: 'Poppins', sans-serif !important;
37
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
38
+ min-height: 100vh !important;
39
+ }
40
+
41
+ .main-wrap {
42
+ background: rgba(255, 255, 255, 0.95) !important;
43
+ backdrop-filter: blur(10px) !important;
44
+ border-radius: 20px !important;
45
+ margin: 20px !important;
46
+ padding: 30px !important;
47
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25) !important;
48
+ }
49
+
50
+ .title {
51
+ background: linear-gradient(135deg, #667eea, #764ba2) !important;
52
+ -webkit-background-clip: text !important;
53
+ -webkit-text-fill-color: transparent !important;
54
+ text-align: center !important;
55
+ font-size: 2.5rem !important;
56
+ font-weight: 700 !important;
57
+ margin-bottom: 1rem !important;
58
+ }
59
+
60
+ .chat-message {
61
+ animation: slideIn 0.5s ease-out !important;
62
+ margin: 10px 0 !important;
63
+ }
64
+
65
+ @keyframes slideIn {
66
+ from { opacity: 0; transform: translateY(20px); }
67
+ to { opacity: 1; transform: translateY(0); }
68
+ }
69
+
70
+ .input-wrap {
71
+ border-radius: 25px !important;
72
+ background: white !important;
73
+ box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1) !important;
74
+ border: none !important;
75
+ }
76
+
77
+ .send-button {
78
+ background: linear-gradient(135deg, #667eea, #764ba2) !important;
79
+ border: none !important;
80
+ border-radius: 50% !important;
81
+ width: 50px !important;
82
+ height: 50px !important;
83
+ transition: all 0.3s ease !important;
84
+ }
85
+
86
+ .send-button:hover {
87
+ transform: scale(1.1) !important;
88
+ box-shadow: 0 10px 20px rgba(102, 126, 234, 0.4) !important;
89
+ }
90
+ """
91
+
92
+ return gr.ChatInterface(
93
+ fn=chat_fn,
94
+ type="messages",
95
+ title="✨ Docs Navigator AI",
96
+ description="**Modern AI Documentation Assistant** - Powered by cutting-edge AI technology",
97
+ css=modern_css,
98
+ theme=gr.themes.Soft(),
99
+ chatbot=gr.Chatbot(height=450, show_label=False),
100
+ examples=["🚀 Quick Start Guide", "⚙️ Configuration", "🔍 Advanced Features"]
101
+ )
102
+
103
+
104
+ def create_dark_interface():
105
+ """Professional dark theme interface"""
106
+ dark_css = """
107
+ .gradio-container {
108
+ background: #0f172a !important;
109
+ color: #e2e8f0 !important;
110
+ }
111
+
112
+ .main-wrap, .panel {
113
+ background: #1e293b !important;
114
+ border: 1px solid #334155 !important;
115
+ border-radius: 12px !important;
116
+ }
117
+
118
+ .chatbot {
119
+ background: #1e293b !important;
120
+ border: 1px solid #334155 !important;
121
+ }
122
+
123
+ .message-wrap {
124
+ background: #334155 !important;
125
+ border-radius: 8px !important;
126
+ margin: 8px !important;
127
+ padding: 12px !important;
128
+ }
129
+
130
+ .user-message {
131
+ background: linear-gradient(135deg, #3b82f6, #1d4ed8) !important;
132
+ color: white !important;
133
+ }
134
+
135
+ .input-container textarea {
136
+ background: #334155 !important;
137
+ border: 1px solid #475569 !important;
138
+ color: #e2e8f0 !important;
139
+ border-radius: 8px !important;
140
+ }
141
+
142
+ .input-container textarea:focus {
143
+ border-color: #3b82f6 !important;
144
+ box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2) !important;
145
+ }
146
+ """
147
+
148
+ return gr.ChatInterface(
149
+ fn=chat_fn,
150
+ type="messages",
151
+ title="🌙 Docs Navigator - Dark Mode",
152
+ description="**Professional Dark Theme** - Easy on the eyes, powerful AI assistance",
153
+ css=dark_css,
154
+ theme=gr.themes.Monochrome().set(
155
+ body_background_fill="#0f172a",
156
+ panel_background_fill="#1e293b"
157
+ ),
158
+ chatbot=gr.Chatbot(height=450, show_label=False),
159
+ examples=["📚 Documentation Overview", "🛠️ Setup Instructions", "❓ FAQ"]
160
+ )
161
+
162
+
163
+ def create_minimal_interface():
164
+ """Ultra-minimal, distraction-free interface"""
165
+ minimal_css = """
166
+ .gradio-container {
167
+ max-width: 800px !important;
168
+ margin: 0 auto !important;
169
+ padding: 20px !important;
170
+ }
171
+
172
+ * {
173
+ border-radius: 4px !important;
174
+ }
175
+
176
+ .title {
177
+ font-size: 1.5rem !important;
178
+ font-weight: 400 !important;
179
+ color: #374151 !important;
180
+ text-align: center !important;
181
+ margin-bottom: 2rem !important;
182
+ }
183
+
184
+ .chatbot {
185
+ border: 1px solid #e5e7eb !important;
186
+ box-shadow: none !important;
187
+ }
188
+
189
+ .input-container {
190
+ border: 1px solid #d1d5db !important;
191
+ background: white !important;
192
+ }
193
+ """
194
+
195
+ return gr.ChatInterface(
196
+ fn=chat_fn,
197
+ type="messages",
198
+ title="Docs Navigator",
199
+ description="Simple documentation assistant",
200
+ css=minimal_css,
201
+ theme=gr.themes.Base(),
202
+ chatbot=gr.Chatbot(height=400, show_label=False),
203
+ textbox=gr.Textbox(placeholder="Ask about docs...", container=False),
204
+ examples=["Setup", "Config", "Help"]
205
+ )
206
+
207
+
208
+ def create_corporate_interface():
209
+ """Enterprise/business-focused styling"""
210
+ corporate_css = """
211
+ .gradio-container {
212
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif !important;
213
+ background: #f8fafc !important;
214
+ }
215
+
216
+ .main-wrap {
217
+ background: white !important;
218
+ border: 1px solid #e2e8f0 !important;
219
+ border-radius: 8px !important;
220
+ padding: 24px !important;
221
+ margin: 16px !important;
222
+ box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1) !important;
223
+ }
224
+
225
+ .title {
226
+ color: #1a202c !important;
227
+ font-size: 1.875rem !important;
228
+ font-weight: 600 !important;
229
+ text-align: center !important;
230
+ margin-bottom: 0.5rem !important;
231
+ }
232
+
233
+ .description {
234
+ color: #4a5568 !important;
235
+ text-align: center !important;
236
+ margin-bottom: 2rem !important;
237
+ }
238
+
239
+ .chatbot {
240
+ border: 1px solid #e2e8f0 !important;
241
+ border-radius: 6px !important;
242
+ background: #ffffff !important;
243
+ }
244
+
245
+ .input-container {
246
+ border: 1px solid #cbd5e0 !important;
247
+ border-radius: 6px !important;
248
+ background: white !important;
249
+ }
250
+
251
+ .submit-button {
252
+ background: #3182ce !important;
253
+ color: white !important;
254
+ border: none !important;
255
+ border-radius: 6px !important;
256
+ font-weight: 500 !important;
257
+ padding: 8px 16px !important;
258
+ }
259
+
260
+ .submit-button:hover {
261
+ background: #2c5aa0 !important;
262
+ }
263
+ """
264
+
265
+ return gr.ChatInterface(
266
+ fn=chat_fn,
267
+ type="messages",
268
+ title="Enterprise Documentation Assistant",
269
+ description="Professional AI-powered documentation system for enterprise environments",
270
+ css=corporate_css,
271
+ theme=gr.themes.Default().set(
272
+ primary_hue="blue",
273
+ secondary_hue="slate",
274
+ neutral_hue="gray"
275
+ ),
276
+ chatbot=gr.Chatbot(height=450, show_label=False),
277
+ examples=[
278
+ "System Documentation",
279
+ "API Reference",
280
+ "Implementation Guide",
281
+ "Security Protocols"
282
+ ]
283
+ )
284
+
285
+
286
+ def create_glassmorphism_interface():
287
+ """Modern glass-effect design"""
288
+ glass_css = """
289
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
290
+
291
+ body {
292
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
293
+ min-height: 100vh !important;
294
+ }
295
+
296
+ .gradio-container {
297
+ font-family: 'Inter', sans-serif !important;
298
+ background: transparent !important;
299
+ }
300
+
301
+ .main-wrap {
302
+ background: rgba(255, 255, 255, 0.1) !important;
303
+ backdrop-filter: blur(20px) !important;
304
+ border: 1px solid rgba(255, 255, 255, 0.2) !important;
305
+ border-radius: 20px !important;
306
+ margin: 20px !important;
307
+ padding: 30px !important;
308
+ box-shadow:
309
+ 0 8px 32px 0 rgba(31, 38, 135, 0.37),
310
+ inset 0 1px 1px 0 rgba(255, 255, 255, 0.3) !important;
311
+ }
312
+
313
+ .title {
314
+ color: white !important;
315
+ text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1) !important;
316
+ font-size: 2.25rem !important;
317
+ font-weight: 600 !important;
318
+ text-align: center !important;
319
+ margin-bottom: 1rem !important;
320
+ }
321
+
322
+ .description {
323
+ color: rgba(255, 255, 255, 0.9) !important;
324
+ text-align: center !important;
325
+ font-size: 1.1rem !important;
326
+ margin-bottom: 2rem !important;
327
+ }
328
+
329
+ .chatbot, .panel {
330
+ background: rgba(255, 255, 255, 0.15) !important;
331
+ backdrop-filter: blur(10px) !important;
332
+ border: 1px solid rgba(255, 255, 255, 0.2) !important;
333
+ border-radius: 15px !important;
334
+ }
335
+
336
+ .input-container {
337
+ background: rgba(255, 255, 255, 0.2) !important;
338
+ backdrop-filter: blur(10px) !important;
339
+ border: 1px solid rgba(255, 255, 255, 0.3) !important;
340
+ border-radius: 12px !important;
341
+ }
342
+
343
+ .input-container textarea {
344
+ background: transparent !important;
345
+ color: white !important;
346
+ border: none !important;
347
+ }
348
+
349
+ .input-container textarea::placeholder {
350
+ color: rgba(255, 255, 255, 0.7) !important;
351
+ }
352
+
353
+ .message-wrap {
354
+ background: rgba(255, 255, 255, 0.1) !important;
355
+ backdrop-filter: blur(5px) !important;
356
+ border-radius: 10px !important;
357
+ margin: 8px !important;
358
+ padding: 12px !important;
359
+ border: 1px solid rgba(255, 255, 255, 0.1) !important;
360
+ }
361
+
362
+ .user-message {
363
+ background: rgba(59, 130, 246, 0.3) !important;
364
+ color: white !important;
365
+ }
366
+ """
367
+
368
+ return gr.ChatInterface(
369
+ fn=chat_fn,
370
+ type="messages",
371
+ title="🔮 Docs Navigator Glass",
372
+ description="Experience the future of documentation with glassmorphism design",
373
+ css=glass_css,
374
+ theme=gr.themes.Glass(),
375
+ chatbot=gr.Chatbot(height=450, show_label=False),
376
+ examples=["✨ Modern Features", "🎨 Design System", "🔧 Advanced Config"]
377
+ )
378
+
379
+
380
+ if __name__ == "__main__":
381
+ # Get style from command line or default to modern
382
+ style = sys.argv[1] if len(sys.argv) > 1 else "modern"
383
+
384
+ interfaces = {
385
+ "modern": create_modern_interface,
386
+ "dark": create_dark_interface,
387
+ "minimal": create_minimal_interface,
388
+ "corporate": create_corporate_interface,
389
+ "glassmorphism": create_glassmorphism_interface
390
+ }
391
+
392
+ if style not in interfaces:
393
+ print(f"Available styles: {', '.join(interfaces.keys())}")
394
+ style = "modern"
395
+
396
+ print(f"🎨 Launching {style} interface...")
397
+ demo = interfaces[style]()
398
+ demo.launch(
399
+ server_name="127.0.0.1",
400
+ server_port=7860,
401
+ show_error=True
402
+ )