Nihal2000 commited on
Commit
4c296ca
·
1 Parent(s): 44aacee

Refactor to Unified MCP + Gradio Server

Browse files
Files changed (2) hide show
  1. app.py +187 -1323
  2. mcp_server.py +2 -3
app.py CHANGED
@@ -1,1374 +1,238 @@
1
  import gradio as gr
2
  import os
3
  import asyncio
4
- import json
5
  import logging
6
- import tempfile
7
- import uuid
8
- from datetime import datetime
9
- from pathlib import Path
10
- from typing import List, Dict, Any, Optional
11
  import nest_asyncio
 
 
12
 
13
- # Apply nest_asyncio to handle nested event loops in Gradio
14
  nest_asyncio.apply()
15
 
16
- # Import our custom modules
17
- from mcp_tools.ingestion_tool import IngestionTool
18
- from mcp_tools.search_tool import SearchTool
19
- from mcp_tools.generative_tool import GenerativeTool
20
- from services.vector_store_service import VectorStoreService
21
- from services.document_store_service import DocumentStoreService
22
- from services.embedding_service import EmbeddingService
23
- from services.llm_service import LLMService
24
- from services.ocr_service import OCRService
25
- from core.models import SearchResult, Document
26
- import config
 
 
 
 
 
 
 
27
 
28
  # Setup logging
29
  logging.basicConfig(level=logging.INFO)
30
  logger = logging.getLogger(__name__)
31
- # Import our custom modules
32
- from mcp_tools.ingestion_tool import IngestionTool
33
- from mcp_tools.search_tool import SearchTool
34
- from mcp_tools.generative_tool import GenerativeTool
35
- from services.vector_store_service import VectorStoreService
36
- from services.document_store_service import DocumentStoreService
37
- from services.embedding_service import EmbeddingService
38
- from services.llm_service import LLMService
39
- from services.ocr_service import OCRService
40
- from core.models import SearchResult, Document
41
- import config
42
- from services.llamaindex_service import LlamaIndexService
43
- from services.elevenlabs_service import ElevenLabsService
44
- from services.podcast_generator_service import PodcastGeneratorService
45
- from mcp_tools.voice_tool import VoiceTool
46
- from mcp_tools.podcast_tool import PodcastTool
47
 
48
- # Setup logging
49
- logging.basicConfig(level=logging.INFO)
50
- logger = logging.getLogger(__name__)
51
-
52
- class ContentOrganizerMCPServer:
53
- def __init__(self):
54
- # Initialize services
55
- logger.info("Initializing Content Organizer MCP Server...")
56
- self.vector_store = VectorStoreService()
57
- self.document_store = DocumentStoreService()
58
- self.embedding_service = EmbeddingService()
59
- self.llm_service = LLMService()
60
- self.ocr_service = OCRService()
61
- self.llamaindex_service = LlamaIndexService(self.document_store)
62
-
63
- # Initialize ElevenLabs voice service
64
- self.elevenlabs_service = ElevenLabsService(self.llamaindex_service)
65
-
66
- # Initialize Podcast Generator
67
- self.podcast_generator = PodcastGeneratorService(
68
- llamaindex_service=self.llamaindex_service,
69
- llm_service=self.llm_service
70
- )
71
-
72
- # Initialize tools
73
- self.ingestion_tool = IngestionTool(
74
- vector_store=self.vector_store,
75
- document_store=self.document_store,
76
- embedding_service=self.embedding_service,
77
- ocr_service=self.ocr_service
78
- )
79
- self.search_tool = SearchTool(
80
- vector_store=self.vector_store,
81
- embedding_service=self.embedding_service,
82
- document_store=self.document_store
83
- )
84
- self.generative_tool = GenerativeTool(
85
- llm_service=self.llm_service,
86
- search_tool=self.search_tool
87
- )
88
- self.voice_tool = VoiceTool(self.elevenlabs_service)
89
- self.podcast_tool = PodcastTool(self.podcast_generator)
90
-
91
-
92
- # Track processing status
93
- self.processing_status = {}
94
-
95
- # Document cache for quick access
96
- self.document_cache = {}
97
- logger.info("Content Organizer MCP Server initialized successfully!")
98
-
99
- def run_async(self, coro):
100
- """Helper to run async functions in Gradio"""
101
- try:
102
- loop = asyncio.get_event_loop()
103
- except RuntimeError:
104
- loop = asyncio.new_event_loop()
105
- asyncio.set_event_loop(loop)
106
- if loop.is_running():
107
- # If loop is already running, create a task
108
- import concurrent.futures
109
- with concurrent.futures.ThreadPoolExecutor() as executor:
110
- future = executor.submit(asyncio.run, coro)
111
- return future.result()
112
- else:
113
- return loop.run_until_complete(coro)
114
-
115
- async def ingest_document_async(self, file_path: str, file_type: str) -> Dict[str, Any]:
116
- """MCP Tool: Ingest and process a document"""
117
- try:
118
- task_id = str(uuid.uuid4())
119
- self.processing_status[task_id] = {"status": "processing", "progress": 0}
120
- result = await self.ingestion_tool.process_document(file_path, file_type, task_id)
121
- if result.get("success"):
122
- self.processing_status[task_id] = {"status": "completed", "progress": 100}
123
- doc_id = result.get("document_id")
124
- if doc_id:
125
- doc = await self.document_store.get_document(doc_id)
126
- if doc:
127
- self.document_cache[doc_id] = doc
128
- return result
129
- else:
130
- self.processing_status[task_id] = {"status": "failed", "error": result.get("error")}
131
- return result
132
- except Exception as e:
133
- logger.error(f"Document ingestion failed: {str(e)}")
134
- return {"success": False, "error": str(e), "message": "Failed to process document"}
135
-
136
- async def get_document_content_async(self, document_id: str) -> Optional[str]:
137
- """Get document content by ID"""
138
- try:
139
- # Check cache first
140
- if document_id in self.document_cache:
141
- return self.document_cache[document_id].content
142
-
143
- # Get from store
144
- doc = await self.document_store.get_document(document_id)
145
- if doc:
146
- self.document_cache[document_id] = doc
147
- return doc.content
148
- return None
149
- except Exception as e:
150
- logger.error(f"Error getting document content: {str(e)}")
151
- return None
152
-
153
- async def semantic_search_async(self, query: str, top_k: int = 5, filters: Optional[Dict] = None) -> Dict[str, Any]:
154
- """MCP Tool: Perform semantic search"""
155
- try:
156
- results = await self.search_tool.search(query, top_k, filters)
157
- return {"success": True, "query": query, "results": [result.to_dict() for result in results], "total_results": len(results)}
158
- except Exception as e:
159
- logger.error(f"Semantic search failed: {str(e)}")
160
- return {"success": False, "error": str(e), "query": query, "results": []}
161
-
162
- async def summarize_content_async(self, content: str = None, document_id: str = None, style: str = "concise") -> Dict[str, Any]:
163
- try:
164
- if document_id and document_id != "none":
165
- content = await self.get_document_content_async(document_id)
166
- if not content:
167
- return {"success": False, "error": f"Document {document_id} not found"}
168
- if not content or not content.strip():
169
- return {"success": False, "error": "No content provided for summarization"}
170
- max_content_length = 4000
171
- if len(content) > max_content_length:
172
- content = content[:max_content_length] + "..."
173
- summary = await self.generative_tool.summarize(content, style)
174
- return {"success": True, "summary": summary, "original_length": len(content), "summary_length": len(summary), "style": style, "document_id": document_id}
175
- except Exception as e:
176
- logger.error(f"Summarization failed: {str(e)}")
177
- return {"success": False, "error": str(e)}
178
-
179
- async def generate_tags_async(self, content: str = None, document_id: str = None, max_tags: int = 5) -> Dict[str, Any]:
180
- """MCP Tool: Generate tags for content"""
181
- try:
182
- if document_id and document_id != "none":
183
- content = await self.get_document_content_async(document_id)
184
- if not content:
185
- return {"success": False, "error": f"Document {document_id} not found"}
186
- if not content or not content.strip():
187
- return {"success": False, "error": "No content provided for tag generation"}
188
- tags = await self.generative_tool.generate_tags(content, max_tags)
189
- if document_id and document_id != "none" and tags:
190
- await self.document_store.update_document_metadata(document_id, {"tags": tags})
191
- return {"success": True, "tags": tags, "content_length": len(content), "document_id": document_id}
192
- except Exception as e:
193
- logger.error(f"Tag generation failed: {str(e)}")
194
- return {"success": False, "error": str(e)}
195
- async def generate_podcast_async(
196
- self,
197
- document_ids: List[str],
198
- style: str = "conversational",
199
- duration_minutes: int = 10,
200
- host1_voice: str = "Rachel",
201
- host2_voice: str = "Adam"
202
- ) -> Dict[str, Any]:
203
- """Generate podcast from documents"""
204
- try:
205
- result = await self.podcast_tool.generate_podcast(
206
- document_ids=document_ids,
207
- style=style,
208
- duration_minutes=duration_minutes,
209
- host1_voice=host1_voice,
210
- host2_voice=host2_voice
211
- )
212
- return result
213
- except Exception as e:
214
- logger.error(f"Podcast generation failed: {str(e)}")
215
- return {"success": False, "error": str(e)}
216
-
217
- async def answer_question_async(self, question: str, context_filter: Optional[Dict] = None) -> Dict[str, Any]:
218
- try:
219
- search_results = await self.search_tool.search(question, top_k=5, filters=context_filter)
220
- if not search_results:
221
- return {"success": False, "error": "No relevant context found in your documents. Please make sure you have uploaded relevant documents.", "question": question}
222
- answer = await self.generative_tool.answer_question(question, search_results)
223
- return {"success": True, "question": question, "answer": answer, "sources": [result.to_dict() for result in search_results], "confidence": "high" if len(search_results) >= 3 else "medium"}
224
- except Exception as e:
225
- logger.error(f"Question answering failed: {str(e)}")
226
- return {"success": False, "error": str(e), "question": question}
227
-
228
- async def generate_outline_async(self, topic: str, num_sections: int = 5, detail_level: str = "medium") -> Dict[str, Any]:
229
- try:
230
- outline = await self.generative_tool.generate_outline(topic, num_sections, detail_level)
231
- return {"success": True, "result": outline}
232
- except Exception as e:
233
- return {"success": False, "error": str(e)}
234
-
235
- async def explain_concept_async(self, concept: str, audience: str = "general", length: str = "medium") -> Dict[str, Any]:
236
- try:
237
- explanation = await self.generative_tool.explain_concept(concept, audience, length)
238
- return {"success": True, "result": explanation}
239
- except Exception as e:
240
- return {"success": False, "error": str(e)}
241
-
242
- async def paraphrase_text_async(self, text: str, style: str = "formal") -> Dict[str, Any]:
243
- try:
244
- paraphrase = await self.generative_tool.paraphrase_text(text, style)
245
- return {"success": True, "result": paraphrase}
246
- except Exception as e:
247
- return {"success": False, "error": str(e)}
248
-
249
- async def categorize_content_async(self, content: str, categories: List[str]) -> Dict[str, Any]:
250
- try:
251
- category = await self.generative_tool.categorize(content, categories)
252
- return {"success": True, "result": category}
253
- except Exception as e:
254
- return {"success": False, "error": str(e)}
255
-
256
- async def extract_key_insights_async(self, content: str, num_insights: int = 5) -> Dict[str, Any]:
257
- try:
258
- insights = await self.generative_tool.extract_key_insights(content, num_insights)
259
- return {"success": True, "result": "\n".join([f"- {insight}" for insight in insights])}
260
- except Exception as e:
261
- return {"success": False, "error": str(e)}
262
-
263
- async def generate_questions_async(self, content: str, question_type: str = "comprehension", num_questions: int = 5) -> Dict[str, Any]:
264
- try:
265
- questions = await self.generative_tool.generate_questions(content, question_type, num_questions)
266
- return {"success": True, "result": "\n".join([f"{i+1}. {q}" for i, q in enumerate(questions)])}
267
- except Exception as e:
268
- return {"success": False, "error": str(e)}
269
-
270
- async def extract_key_information_async(self, content: str) -> Dict[str, Any]:
271
- try:
272
- info = await self.llm_service.extract_key_information(content)
273
- return {"success": True, "result": json.dumps(info, indent=2)}
274
- except Exception as e:
275
- return {"success": False, "error": str(e)}
276
-
277
- def list_documents_sync(self, limit: int = 100, offset: int = 0) -> Dict[str, Any]:
278
- try:
279
- documents = self.run_async(self.document_store.list_documents(limit, offset))
280
- return {"success": True, "documents": [doc.to_dict() for doc in documents], "total": len(documents)}
281
- except Exception as e:
282
- return {"success": False, "error": str(e)}
283
-
284
- mcp_server = ContentOrganizerMCPServer()
285
-
286
- def get_document_list():
287
- try:
288
- result = mcp_server.list_documents_sync(limit=100)
289
- if result["success"]:
290
- if result["documents"]:
291
- doc_list_str = "📚 Documents in Library:\n\n"
292
- for i, doc_item in enumerate(result["documents"], 1):
293
- doc_list_str += f"{i}. {doc_item['filename']} (ID: {doc_item['id'][:8]}...)\n"
294
- doc_list_str += f" Type: {doc_item['doc_type']}, Size: {doc_item['file_size']} bytes\n"
295
- if doc_item.get('tags'):
296
- doc_list_str += f" Tags: {', '.join(doc_item['tags'])}\n"
297
- doc_list_str += f" Created: {doc_item['created_at'][:10]}\n\n"
298
- return doc_list_str
299
- else:
300
- return "No documents in library yet. Upload some documents to get started!"
301
- else:
302
- return f"Error loading documents: {result['error']}"
303
- except Exception as e:
304
- return f"Error: {str(e)}"
305
-
306
- def get_document_choices():
307
- try:
308
- result = mcp_server.list_documents_sync(limit=100)
309
- if result["success"] and result["documents"]:
310
- choices = [(f"{doc['filename']} ({doc['id'][:8]}...)", doc['id']) for doc in result["documents"]]
311
- logger.info(f"Generated {len(choices)} document choices")
312
- return choices
313
- return []
314
- except Exception as e:
315
- logger.error(f"Error getting document choices: {str(e)}")
316
- return []
317
-
318
- def refresh_library():
319
- doc_list_refreshed = get_document_list()
320
- doc_choices_refreshed = get_document_choices()
321
- logger.info(f"Refreshing library. Found {len(doc_choices_refreshed)} choices.")
322
- return (
323
- doc_list_refreshed,
324
- gr.update(choices=doc_choices_refreshed),
325
- gr.update(choices=doc_choices_refreshed),
326
- gr.update(choices=doc_choices_refreshed)
327
- )
328
-
329
- def upload_and_process_file(file):
330
- if file is None:
331
- doc_list_initial = get_document_list()
332
- doc_choices_initial = get_document_choices()
333
- return (
334
- "No file uploaded", "", doc_list_initial,
335
- gr.update(choices=doc_choices_initial),
336
- gr.update(choices=doc_choices_initial),
337
- gr.update(choices=doc_choices_initial)
338
- )
339
- try:
340
- file_path = file.name if hasattr(file, 'name') else str(file)
341
- file_type = Path(file_path).suffix.lower().strip('.') # Ensure suffix is clean
342
- logger.info(f"Processing file: {file_path}, type: {file_type}")
343
- result = mcp_server.run_async(mcp_server.ingest_document_async(file_path, file_type))
344
-
345
- doc_list_updated = get_document_list()
346
- doc_choices_updated = get_document_choices()
347
-
348
- if result["success"]:
349
- return (
350
- f"✅ Success: {result['message']}\nDocument ID: {result['document_id']}\nChunks created: {result['chunks_created']}",
351
- result["document_id"],
352
- doc_list_updated,
353
- gr.update(choices=doc_choices_updated),
354
- gr.update(choices=doc_choices_updated),
355
- gr.update(choices=doc_choices_updated)
356
- )
357
- else:
358
- return (
359
- f"❌ Error: {result.get('error', 'Unknown error')}", "",
360
- doc_list_updated,
361
- gr.update(choices=doc_choices_updated),
362
- gr.update(choices=doc_choices_updated),
363
- gr.update(choices=doc_choices_updated)
364
- )
365
- except Exception as e:
366
- logger.error(f"Error processing file: {str(e)}")
367
- doc_list_error = get_document_list()
368
- doc_choices_error = get_document_choices()
369
- return (
370
- f"❌ Error: {str(e)}", "",
371
- doc_list_error,
372
- gr.update(choices=doc_choices_error),
373
- gr.update(choices=doc_choices_error),
374
- gr.update(choices=doc_choices_error)
375
- )
376
-
377
- def perform_search(query, top_k):
378
- if not query.strip():
379
- return "Please enter a search query"
380
- try:
381
- result = mcp_server.run_async(mcp_server.semantic_search_async(query, int(top_k)))
382
- if result["success"]:
383
- if result["results"]:
384
- output_str = f"🔍 Found {result['total_results']} results for: '{query}'\n\n"
385
- for i, res_item in enumerate(result["results"], 1):
386
- output_str += f"Result {i}:\n"
387
- output_str += f"📊 Relevance Score: {res_item['score']:.3f}\n"
388
- output_str += f"📄 Content: {res_item['content'][:300]}...\n"
389
- if 'document_filename' in res_item.get('metadata', {}):
390
- output_str += f"📁 Source: {res_item['metadata']['document_filename']}\n"
391
- output_str += f"🔗 Document ID: {res_item.get('document_id', 'Unknown')}\n"
392
- output_str += "-" * 80 + "\n\n"
393
- return output_str
394
- else:
395
- return f"No results found for: '{query}'\n\nMake sure you have uploaded relevant documents first."
396
- else:
397
- return f"❌ Search failed: {result['error']}"
398
- except Exception as e:
399
- logger.error(f"Search error: {str(e)}")
400
- return f"❌ Error: {str(e)}"
401
-
402
- def update_options_visibility(task):
403
- """Update visibility of options based on selected task"""
404
- return (
405
- gr.update(visible=task == "Summarize"), # summary_style
406
- gr.update(visible=task == "Generate Outline"), # outline_sections
407
- gr.update(visible=task == "Generate Outline"), # outline_detail
408
- gr.update(visible=task == "Explain Concept"), # explain_audience
409
- gr.update(visible=task == "Explain Concept"), # explain_length
410
- gr.update(visible=task == "Paraphrase"), # paraphrase_style
411
- gr.update(visible=task == "Categorize"), # categories_input
412
- gr.update(visible=task in ["Key Insights", "Generate Questions"]), # num_items
413
- gr.update(visible=task == "Generate Questions") # question_type
414
- )
415
-
416
- def execute_content_task(task, doc_choice, custom_text,
417
- summary_style, outline_sections, outline_detail,
418
- explain_audience, explain_length,
419
- paraphrase_style, categories_input,
420
- num_items, question_type):
421
- try:
422
- # Get content
423
- content = ""
424
- if custom_text and custom_text.strip():
425
- content = custom_text
426
- elif doc_choice and doc_choice != "none":
427
- content = mcp_server.run_async(mcp_server.get_document_content_async(doc_choice))
428
- if not content:
429
- return "❌ Error: Document not found or empty"
430
- else:
431
- if task == "Generate Outline":
432
- content = custom_text # Topic is passed as text
433
- else:
434
- return "⚠️ Please select a document or enter text"
435
 
436
- # Execute task
437
- result = {"success": False, "error": "Unknown task"}
438
-
439
- if task == "Summarize":
440
- result = mcp_server.run_async(mcp_server.summarize_content_async(content=content, style=summary_style))
441
- if result["success"]:
442
- return f"📝 Summary ({summary_style}):\n\n{result['summary']}"
443
-
444
- elif task == "Generate Outline":
445
- # For outline, content is the topic
446
- result = mcp_server.run_async(mcp_server.generate_outline_async(content, int(outline_sections), outline_detail))
447
- if result["success"]:
448
- return f"📝 Outline for '{content}':\n\n{result['result']}"
449
-
450
- elif task == "Explain Concept":
451
- # For explain, content is the concept
452
- result = mcp_server.run_async(mcp_server.explain_concept_async(content, explain_audience, explain_length))
453
- if result["success"]:
454
- return f"💡 Explanation ({explain_audience}):\n\n{result['result']}"
455
-
456
- elif task == "Paraphrase":
457
- result = mcp_server.run_async(mcp_server.paraphrase_text_async(content, paraphrase_style))
458
- if result["success"]:
459
- return f"🔄 Paraphrased Text ({paraphrase_style}):\n\n{result['result']}"
460
-
461
- elif task == "Categorize":
462
- categories = [c.strip() for c in categories_input.split(',')] if categories_input else []
463
- result = mcp_server.run_async(mcp_server.categorize_content_async(content, categories))
464
- if result["success"]:
465
- return f"🏷️ Category:\n\n{result['result']}"
466
-
467
- elif task == "Key Insights":
468
- result = mcp_server.run_async(mcp_server.extract_key_insights_async(content, int(num_items)))
469
- if result["success"]:
470
- return f"🔍 Key Insights:\n\n{result['result']}"
471
-
472
- elif task == "Generate Questions":
473
- result = mcp_server.run_async(mcp_server.generate_questions_async(content, question_type, int(num_items)))
474
- if result["success"]:
475
- return f"❓ Generated Questions ({question_type}):\n\n{result['result']}"
476
-
477
- elif task == "Extract Key Info":
478
- result = mcp_server.run_async(mcp_server.extract_key_information_async(content))
479
- if result["success"]:
480
- return f"📊 Key Information:\n\n{result['result']}"
481
-
482
- if not result["success"]:
483
- return f"❌ Error: {result.get('error', 'Unknown error')}"
484
-
485
- return "✅ Task completed"
486
-
487
- except Exception as e:
488
- logger.error(f"Task execution error: {str(e)}")
489
- return f"❌ Error: {str(e)}"
490
-
491
- def generate_tags_for_document(doc_choice, custom_text, max_tags):
492
- try:
493
- logger.info(f"Generate tags called with doc_choice: {doc_choice}, type: {type(doc_choice)}")
494
- document_id = doc_choice if doc_choice and doc_choice != "none" and doc_choice != "" else None
495
-
496
- if custom_text and custom_text.strip():
497
- logger.info("Using custom text for tag generation")
498
- result = mcp_server.run_async(mcp_server.generate_tags_async(content=custom_text, max_tags=int(max_tags)))
499
- elif document_id:
500
- logger.info(f"Generating tags for document: {document_id}")
501
- result = mcp_server.run_async(mcp_server.generate_tags_async(document_id=document_id, max_tags=int(max_tags)))
502
- else:
503
- return "Please select a document from the dropdown or enter text to generate tags"
504
-
505
- if result["success"]:
506
- tags_str = ", ".join(result["tags"])
507
- output_str = f"🏷️ Generated Tags:\n\n{tags_str}\n\n"
508
- output_str += f"📊 Statistics:\n"
509
- output_str += f"- Content length: {result['content_length']} characters\n"
510
- output_str += f"- Number of tags: {len(result['tags'])}\n"
511
- if result.get('document_id'):
512
- output_str += f"- Document ID: {result['document_id']}\n"
513
- output_str += f"\n✅ Tags have been saved to the document."
514
- return output_str
515
- else:
516
- return f"❌ Tag generation failed: {result['error']}"
517
- except Exception as e:
518
- logger.error(f"Tag generation error: {str(e)}")
519
- return f"❌ Error: {str(e)}"
520
-
521
- def ask_question(question):
522
- if not question.strip():
523
- return "Please enter a question"
524
- try:
525
- result = mcp_server.run_async(mcp_server.answer_question_async(question))
526
- if result["success"]:
527
- output_str = f"❓ Question: {result['question']}\n\n"
528
- output_str += f"💡 Answer:\n{result['answer']}\n\n"
529
- output_str += f"🎯 Confidence: {result['confidence']}\n\n"
530
- output_str += f"📚 Sources Used ({len(result['sources'])}):\n"
531
- for i, source_item in enumerate(result['sources'], 1):
532
- filename = source_item.get('metadata', {}).get('document_filename', 'Unknown')
533
- output_str += f"\n{i}. 📄 {filename}\n"
534
- output_str += f" 📝 Excerpt: {source_item['content'][:150]}...\n"
535
- output_str += f" 📊 Relevance: {source_item['score']:.3f}\n"
536
- return output_str
537
- else:
538
- return f"❌ {result.get('error', 'Failed to answer question')}"
539
- except Exception as e:
540
- return f"❌ Error: {str(e)}"
541
-
542
- def delete_document_from_library(document_id):
543
- if not document_id:
544
- doc_list_current = get_document_list()
545
- doc_choices_current = get_document_choices()
546
- return (
547
- "No document selected to delete.",
548
- doc_list_current,
549
- gr.update(choices=doc_choices_current),
550
- gr.update(choices=doc_choices_current),
551
- gr.update(choices=doc_choices_current)
552
- )
553
- try:
554
- delete_doc_store_result = mcp_server.run_async(mcp_server.document_store.delete_document(document_id))
555
- delete_vec_store_result = mcp_server.run_async(mcp_server.vector_store.delete_document(document_id))
556
-
557
- msg = ""
558
- if delete_doc_store_result:
559
- msg += f"🗑️ Document {document_id[:8]}... deleted from document store. "
560
- else:
561
- msg += f"❌ Failed to delete document {document_id[:8]}... from document store. "
562
-
563
- if delete_vec_store_result:
564
- msg += "Embeddings deleted from vector store."
565
- else:
566
- msg += "Failed to delete embeddings from vector store (or no embeddings existed)."
567
-
568
-
569
- doc_list_updated = get_document_list()
570
- doc_choices_updated = get_document_choices()
571
- return (
572
- msg,
573
- doc_list_updated,
574
- gr.update(choices=doc_choices_updated),
575
- gr.update(choices=doc_choices_updated),
576
- gr.update(choices=doc_choices_updated)
577
- )
578
- except Exception as e:
579
- logger.error(f"Error deleting document: {str(e)}")
580
- doc_list_error = get_document_list()
581
- doc_choices_error = get_document_choices()
582
- return (
583
- f"❌ Error deleting document: {str(e)}",
584
- doc_list_error,
585
- gr.update(choices=doc_choices_error),
586
- gr.update(choices=doc_choices_error),
587
- gr.update(choices=doc_choices_error)
588
- )
589
-
590
- # Voice conversation state - global scope
591
  voice_conversation_state = {
592
- "session_id": None,
593
  "active": False,
 
594
  "transcript": []
595
  }
596
 
597
- def start_voice_conversation():
598
- """Start a new voice conversation session"""
599
- try:
600
- if not mcp_server.elevenlabs_service.is_available():
601
- return (
602
- "⚠️ Voice assistant not configured. Please set ELEVENLABS_API_KEY and ELEVENLABS_AGENT_ID in .env",
603
- gr.update(interactive=False),
604
- gr.update(interactive=True),
605
- ""
606
- )
607
-
608
- session_id = str(uuid.uuid4())
609
- result = mcp_server.run_async(mcp_server.elevenlabs_service.start_conversation(session_id))
610
-
611
- if result.get("success"):
612
- voice_conversation_state["session_id"] = session_id
613
- voice_conversation_state["active"] = True
614
- voice_conversation_state["transcript"] = []
615
-
616
- return (
617
- "🎙️ Voice assistant is ready. Type your question below.",
618
- gr.update(interactive=False),
619
- gr.update(interactive=True),
620
- []
621
- )
622
- else:
623
- return (
624
- f"❌ Failed to start conversation: {result.get('error')}",
625
- gr.update(interactive=True),
626
- gr.update(interactive=False),
627
- []
628
- )
629
- except Exception as e:
630
- logger.error(f"Error starting voice conversation: {str(e)}")
631
- return (
632
- f"❌ Error: {str(e)}",
633
- gr.update(interactive=True),
634
- gr.update(interactive=False),
635
- []
636
- )
637
-
638
-
639
- def stop_voice_conversation():
640
- """Stop active voice conversation"""
641
- try:
642
- if not voice_conversation_state["active"]:
643
- return (
644
- "No active conversation",
645
- gr.update(interactive=True),
646
- gr.update(interactive=False),
647
- voice_conversation_state["transcript"]
648
- )
649
-
650
- session_id = voice_conversation_state["session_id"]
651
- if session_id:
652
- mcp_server.run_async(mcp_server.elevenlabs_service.end_conversation(session_id))
653
-
654
- voice_conversation_state["active"] = False
655
- voice_conversation_state["session_id"] = None
656
-
657
- return (
658
- "✅ Conversation ended",
659
- gr.update(interactive=True),
660
- gr.update(interactive=False),
661
- voice_conversation_state["transcript"]
662
- )
663
- except Exception as e:
664
- logger.error(f"Error stopping conversation: {str(e)}")
665
- return (
666
- f"❌ Error: {str(e)}",
667
- gr.update(interactive=True),
668
- gr.update(interactive=False),
669
- voice_conversation_state["transcript"]
670
- )
671
-
672
-
673
- def send_voice_message(message):
674
- """Send a text message in voice conversation"""
675
- try:
676
- if not voice_conversation_state["active"]:
677
- return ("Please start a conversation first", "", format_transcript(voice_conversation_state["transcript"]))
678
-
679
- if not message or not message.strip():
680
- return ("Please enter a message", message, format_transcript(voice_conversation_state["transcript"]))
681
-
682
- session_id = voice_conversation_state["session_id"]
683
- voice_conversation_state["transcript"].append({"role": "user", "content": message})
684
-
685
- result = mcp_server.run_async(mcp_server.voice_tool.voice_qa(message, session_id))
686
-
687
- if result.get("success"):
688
- answer = result.get("answer", "No response")
689
- voice_conversation_state["transcript"].append({"role": "assistant", "content": answer})
690
- return ("✅ Response received", "", format_transcript(voice_conversation_state["transcript"]))
691
- else:
692
- return (f"❌ Error: {result.get('error')}", message, format_transcript(voice_conversation_state["transcript"]))
693
- except Exception as e:
694
- logger.error(f"Error sending message: {str(e)}")
695
- return (f"❌ Error: {str(e)}", message, format_transcript(voice_conversation_state["transcript"]))
696
-
697
- def format_transcript(transcript):
698
- """Format conversation transcript for display"""
699
- if not transcript:
700
- return "No conversation yet. Start talking to the AI librarian!"
701
-
702
- formatted = ""
703
- for msg in transcript:
704
- role = msg["role"]
705
- content = msg["content"]
706
- if role == "user":
707
- formatted += f"👤 **You:** {content}\n\n"
708
- else:
709
- formatted += f"🤖 **AI Librarian:** {content}\n\n"
710
- formatted += "---\n\n"
711
- return formatted
712
-
713
- def clear_voice_transcript():
714
- """Clear conversation transcript"""
715
- voice_conversation_state["transcript"] = []
716
- return ""
717
-
718
- def send_voice_message_v6(message, chat_history):
719
- """Send message in voice conversation - Gradio 6 format"""
720
- try:
721
- if not voice_conversation_state["active"]:
722
- return chat_history, ""
723
-
724
- if not message or not message.strip():
725
- return chat_history, message
726
-
727
- session_id = voice_conversation_state["session_id"]
728
-
729
- # Add user message in Gradio 6 format
730
- chat_history.append({"role": "user", "content": message})
731
-
732
- # Get AI response
733
- result = mcp_server.run_async(mcp_server.voice_tool.voice_qa(message, session_id))
734
-
735
- if result.get("success"):
736
- answer = result.get("answer", "No response")
737
- chat_history.append({"role": "assistant", "content": answer})
738
- else:
739
- chat_history.append({
740
- "role": "assistant",
741
- "content": f"❌ Error: {result.get('error')}"
742
- })
743
-
744
- return chat_history, ""
745
- except Exception as e:
746
- logger.error(f"Error in voice message: {str(e)}")
747
- chat_history.append({
748
- "role": "assistant",
749
- "content": f"❌ Error: {str(e)}"
750
- })
751
- return chat_history, ""
752
-
753
- def generate_podcast_ui(doc_ids, style, duration, voice1, voice2):
754
- """UI wrapper for podcast generation"""
755
- try:
756
- if not doc_ids or len(doc_ids) == 0:
757
- return ("⚠️ Please select at least one document", None, "No documents selected", "")
758
-
759
- logger.info(f"Generating podcast: {len(doc_ids)} docs, {style}, {duration}min")
760
-
761
- result = mcp_server.run_async(
762
- mcp_server.generate_podcast_async(
763
- document_ids=doc_ids,
764
- style=style,
765
- duration_minutes=int(duration),
766
- host1_voice=voice1,
767
- host2_voice=voice2
768
- )
769
- )
770
-
771
- if result.get("success"):
772
- audio_file = result.get("audio_file")
773
- transcript = result.get("transcript", "Transcript not available")
774
- message = result.get("message", "Podcast generated!")
775
- formatted_transcript = f"## Podcast Transcript\n\n{transcript}"
776
-
777
- return (
778
- f"✅ {message}",
779
- audio_file,
780
- formatted_transcript,
781
- result.get("podcast_id", "")
782
- )
783
- else:
784
- error = result.get("error", "Unknown error")
785
- return (f"❌ Error: {error}", None, "Generation failed", "")
786
- except Exception as e:
787
- logger.error(f"Podcast UI error: {str(e)}")
788
- return (f"❌ Error: {str(e)}", None, "An error occurred", "")
789
-
790
- def load_dashboard_stats():
791
- """Load dashboard statistics for the UI"""
792
- try:
793
- # Get document list
794
- docs_result = mcp_server.list_documents_sync(limit=1000)
795
- doc_count = 0
796
- total_chunks = 0
797
- total_size = 0
798
- recent_data = []
799
-
800
- if docs_result.get("success"):
801
- documents = docs_result.get("documents", [])
802
- doc_count = len(documents)
803
- total_chunks = sum(doc.get("metadata", {}).get("chunk_count", 0) for doc in documents)
804
- total_size = sum(doc.get("file_size", 0) for doc in documents)
805
- storage_mb = round(total_size / (1024 * 1024), 2) if total_size > 0 else 0.0
806
-
807
- # Get recent 5 documents
808
- recent = documents[:5]
809
- recent_data = [
810
- [
811
- doc.get("filename", "Unknown"),
812
- doc.get("doc_type", "unknown"),
813
- doc.get("created_at", "")[:10] if doc.get("created_at") else "N/A",
814
- f"{doc.get('file_size', 0)} bytes"
815
- ]
816
- for doc in recent
817
- ]
818
- else:
819
- storage_mb = 0.0
820
-
821
- # Service status indicators
822
- vector_stat = "✅ Online" if getattr(mcp_server, "vector_store", None) else "❌ Offline"
823
- llm_stat = "✅ Ready" if getattr(mcp_server, "llm_service", None) else "❌ Offline"
824
- voice_stat = "✅ Ready" if (getattr(mcp_server, "elevenlabs_service", None) and mcp_server.elevenlabs_service.is_available()) else "⚠️ Configure API Key"
825
-
826
- return (
827
- doc_count,
828
- total_chunks,
829
- storage_mb,
830
- recent_data,
831
- vector_stat,
832
- llm_stat,
833
- voice_stat,
834
- )
835
- except Exception as e:
836
- logger.error(f"Error loading dashboard stats: {str(e)}")
837
- return (0, 0, 0.0, [], "❌ Error", "❌ Error", "❌ Error")
838
-
839
  def create_gradio_interface():
840
- # Create custom theme with modern aesthetics
841
- custom_theme = gr.themes.Soft(
842
- primary_hue=gr.themes.colors.indigo,
843
- secondary_hue=gr.themes.colors.blue,
844
- neutral_hue=gr.themes.colors.slate,
845
- font=[gr.themes.GoogleFont("Inter"), "system-ui", "sans-serif"],
846
- font_mono=[gr.themes.GoogleFont("Fira Code"), "monospace"],
847
- ).set(
848
- button_primary_background_fill="*primary_500",
849
- button_primary_background_fill_hover="*primary_600",
850
- block_title_text_weight="600",
851
- block_label_text_size="sm",
852
- block_label_text_weight="500",
853
- )
854
 
855
- with gr.Blocks(title="🧠 AI Digital Library Assistant", theme=custom_theme) as interface:
 
 
856
  with gr.Tabs():
857
- # Dashboard Tab - New Landing Page
858
- with gr.Tab("🏠 Dashboard"):
859
- gr.Markdown("# Welcome to Your AI Library Assistant")
860
- gr.Markdown("*Your intelligent document management and analysis platform powered by AI*")
861
-
862
- # Quick Stats Section
863
- gr.Markdown("## 📊 Quick Stats")
864
  with gr.Row():
865
- total_docs = gr.Number(
866
- label="📚 Total Documents",
867
- value=0,
868
- interactive=False,
869
- container=True
870
- )
871
- total_chunks = gr.Number(
872
- label="🧩 Vector Chunks",
873
- value=0,
874
- interactive=False,
875
- container=True
876
- )
877
- storage_size = gr.Number(
878
- label="💾 Storage (MB)",
879
- value=0,
880
- interactive=False,
881
- container=True
882
- )
883
-
884
- # Recent Activity Section
885
- gr.Markdown("## 📊 Recent Activity")
886
- with gr.Group():
887
- recent_docs = gr.Dataframe(
888
- headers=["Document", "Type", "Date", "Size"],
889
- datatype=["str", "str", "str", "str"],
890
- row_count=(5, "fixed"),
891
- col_count=(4, "fixed"),
892
- interactive=False,
893
- label="Recently Added Documents"
894
- )
895
 
896
- # System Status Section
897
- gr.Markdown("## � System Status")
898
  with gr.Row():
899
- vector_status = gr.Textbox(
900
- label="Vector Store",
901
- value=" Online",
902
- interactive=False,
903
- container=True
904
- )
905
- llm_status = gr.Textbox(
906
- label="LLM Service",
907
- value="✅ Ready",
908
- interactive=False,
909
- container=True
910
- )
911
- voice_status = gr.Textbox(
912
- label="Voice Service",
913
- value="⚠️ Configure API Key",
914
- interactive=False,
915
- container=True
916
- )
917
-
918
- with gr.Tab("📚 Document Library"):
919
- with gr.Row():
920
- with gr.Column():
921
- gr.Markdown("### Your Document Collection")
922
- document_list_display = gr.Textbox(label="Documents in Library", value=get_document_list(), lines=20, interactive=False)
923
- refresh_btn_library = gr.Button("🔄 Refresh Library", variant="secondary")
924
- delete_doc_dropdown_visible = gr.Dropdown(label="Select Document to Delete", choices=get_document_choices(), value=None, interactive=True, allow_custom_value=False)
925
- delete_btn = gr.Button("🗑️ Delete Selected Document", variant="stop")
926
- delete_output_display = gr.Textbox(label="Delete Status", visible=True)
927
-
928
- with gr.Tab("📄 Upload Documents"):
929
- gr.Markdown("""
930
- ### 📥 Add Documents to Library
931
- Upload PDFs, Word documents, text files, or images. OCR will extract text from images automatically.
932
- """)
933
 
934
- with gr.Row():
935
- with gr.Column():
936
- with gr.Group():
937
- gr.Markdown("**Supported formats:** PDF, DOCX, TXT, Images (JPG, PNG)")
938
- file_input_upload = gr.File(
939
- label="Select File",
940
- file_types=[".pdf", ".txt", ".docx", ".png", ".jpg", ".jpeg"],
941
- type="filepath",
942
- file_count="single"
943
- )
944
-
945
- upload_btn_process = gr.Button("🚀 Upload & Process", variant="primary", size="lg")
946
-
947
-
948
- with gr.Group():
949
- upload_output_display = gr.Textbox(
950
- label="Status",
951
- lines=6,
952
- interactive=False,
953
- show_copy_button=False
954
- )
955
-
956
- doc_id_output_display = gr.Textbox(
957
- label="Document ID",
958
- interactive=False,
959
- visible=False
960
- )
961
-
962
 
963
- with gr.Tab("🔍 Search Documents"):
964
- gr.Markdown("""
965
- ### 🔎 Semantic Search
966
- Find relevant content across your entire document library using AI-powered semantic search.
967
- """)
968
-
969
  with gr.Row():
970
  with gr.Column(scale=1):
971
- with gr.Group():
972
- search_query_input = gr.Textbox(
973
- label="Search Query",
974
- placeholder="What are you looking for?",
975
- lines=2,
976
- info="Use natural language to describe what you need"
977
- )
978
-
979
- with gr.Accordion("🎛️ Search Options", open=False):
980
- search_top_k_slider = gr.Slider(
981
- label="Number of Results",
982
- minimum=1, maximum=20, value=5, step=1,
983
- info="More results = broader search"
984
- )
985
-
986
- search_btn_action = gr.Button("🔍 Search", variant="primary", size="lg")
987
 
988
  with gr.Column(scale=2):
989
- with gr.Group():
990
- search_output_display = gr.Textbox(
991
- label="Results",
992
- lines=20,
993
- placeholder="Search results will appear here...",
994
- show_copy_button=True
995
- )
996
 
997
-
998
- with gr.Tab("📝 Content Studio"):
999
- gr.Markdown("""
1000
- ### 🎨 Create & Analyze Content
1001
- Transform documents with AI-powered tools: summarize, outline, explain, and more.
1002
- """)
1003
-
1004
  with gr.Row():
1005
- with gr.Column(scale=2):
1006
- # Source Selection with Group
1007
- with gr.Group():
1008
- gr.Markdown("#### 📄 Content Source")
1009
- doc_dropdown_content = gr.Dropdown(
1010
- label="Select Document",
1011
- choices=get_document_choices(),
1012
- value=None,
1013
- interactive=True,
1014
- info="Choose a document from your library"
1015
- )
1016
-
1017
- gr.Markdown("**OR**")
1018
-
1019
- content_text_input = gr.Textbox(
1020
- label="Enter Text or Topic",
1021
- placeholder="Paste content or enter a topic...",
1022
- lines=4,
1023
- info="For outlines, enter a topic. For other tasks, paste text to analyze."
1024
- )
1025
-
1026
- # Task Configuration with Group
1027
- with gr.Group():
1028
- gr.Markdown("#### 🛠️ Task Configuration")
1029
- task_dropdown = gr.Dropdown(
1030
- label="Select Task",
1031
- choices=[
1032
- "Summarize", "Generate Outline", "Explain Concept",
1033
- "Paraphrase", "Categorize", "Key Insights",
1034
- "Generate Questions", "Extract Key Info"
1035
- ],
1036
- value="Summarize",
1037
- interactive=True,
1038
- info="Choose the type of analysis to perform"
1039
- )
1040
-
1041
- # Dynamic Options with Accordion
1042
- with gr.Accordion("⚙️ Advanced Options", open=False):
1043
- summary_style_opt = gr.Dropdown(
1044
- label="Summary Style",
1045
- choices=["concise", "detailed", "bullet_points", "executive"],
1046
- value="concise",
1047
- visible=True,
1048
- info="How detailed should the summary be?"
1049
- )
1050
-
1051
- outline_sections_opt = gr.Slider(
1052
- label="Number of Sections",
1053
- minimum=3, maximum=10, value=5, step=1,
1054
- visible=False,
1055
- info="How many main sections?"
1056
- )
1057
- outline_detail_opt = gr.Dropdown(
1058
- label="Detail Level",
1059
- choices=["brief", "medium", "detailed"],
1060
- value="medium",
1061
- visible=False
1062
- )
1063
-
1064
- explain_audience_opt = gr.Dropdown(
1065
- label="Target Audience",
1066
- choices=["general", "technical", "beginner", "expert"],
1067
- value="general",
1068
- visible=False,
1069
- info="Who is this explanation for?"
1070
- )
1071
- explain_length_opt = gr.Dropdown(
1072
- label="Length",
1073
- choices=["brief", "medium", "detailed"],
1074
- value="medium",
1075
- visible=False
1076
- )
1077
-
1078
- paraphrase_style_opt = gr.Dropdown(
1079
- label="Style",
1080
- choices=["formal", "casual", "academic", "simple", "technical"],
1081
- value="formal",
1082
- visible=False,
1083
- info="Writing style for paraphrasing"
1084
- )
1085
-
1086
- categories_input_opt = gr.Textbox(
1087
- label="Categories (comma separated)",
1088
- placeholder="Technology, Business, Science...",
1089
- visible=False
1090
- )
1091
-
1092
- num_items_opt = gr.Slider(
1093
- label="Number of Items",
1094
- minimum=1, maximum=10, value=5, step=1,
1095
- visible=False
1096
- )
1097
- question_type_opt = gr.Dropdown(
1098
- label="Question Type",
1099
- choices=["comprehension", "analysis", "application", "creative", "factual"],
1100
- value="comprehension",
1101
- visible=False
1102
- )
1103
-
1104
- run_task_btn = gr.Button("🚀 Run Task", variant="primary", size="lg")
1105
 
1106
- with gr.Column(scale=3):
1107
- # Results with copy button and Group
1108
- with gr.Group():
1109
- gr.Markdown("#### 📊 Result")
1110
- content_output_display = gr.Textbox(
1111
- label="",
1112
- lines=25,
1113
- placeholder="Results will appear here...",
1114
- show_copy_button=True,
1115
- container=False
1116
- )
1117
-
1118
- # Event Handlers
1119
- task_dropdown.change(
1120
- fn=update_options_visibility,
1121
- inputs=[task_dropdown],
1122
- outputs=[
1123
- summary_style_opt, outline_sections_opt, outline_detail_opt,
1124
- explain_audience_opt, explain_length_opt, paraphrase_style_opt,
1125
- categories_input_opt, num_items_opt, question_type_opt
1126
- ]
1127
- )
1128
 
1129
- run_task_btn.click(
1130
- fn=execute_content_task,
1131
- inputs=[
1132
- task_dropdown, doc_dropdown_content, content_text_input,
1133
- summary_style_opt, outline_sections_opt, outline_detail_opt,
1134
- explain_audience_opt, explain_length_opt, paraphrase_style_opt,
1135
- categories_input_opt, num_items_opt, question_type_opt
1136
- ],
1137
- outputs=[content_output_display]
1138
- )
1139
-
1140
- with gr.Tab("🏷️ Generate Tags"):
1141
  with gr.Row():
1142
- with gr.Column():
1143
- gr.Markdown("### Generate Document Tags")
1144
- doc_dropdown_tag_visible = gr.Dropdown(label="Select Document to Tag", choices=get_document_choices(), value=None, interactive=True, allow_custom_value=False)
1145
- tag_text_input = gr.Textbox(label="Or Paste Text to Generate Tags", placeholder="Paste any text here to generate tags...", lines=8)
1146
- max_tags_slider = gr.Slider(label="Number of Tags", minimum=3, maximum=15, value=5, step=1)
1147
- tag_btn_action = gr.Button("🏷️ Generate Tags", variant="primary", size="lg")
1148
- with gr.Column():
1149
- tag_output_display = gr.Textbox(label="Generated Tags", lines=10, placeholder="Tags will appear here...")
1150
 
 
1151
  with gr.Tab("🎙️ Voice Assistant"):
1152
- gr.Markdown("""
1153
- ### 🗣️ Talk to Your AI Librarian
1154
-
1155
- Have a natural conversation about your documents. Ask questions, request summaries,
1156
- or explore your content library through voice-powered interaction.
1157
-
1158
- **Note:** Requires ElevenLabs API configuration.
1159
- """)
1160
-
1161
  with gr.Row():
1162
- with gr.Column(scale=2):
1163
- # Status and Controls
1164
- with gr.Group():
1165
- voice_status_display = gr.Textbox(
1166
- label="Status",
1167
- value="Ready to start",
1168
- interactive=False,
1169
- lines=2
1170
- )
1171
-
1172
- with gr.Row():
1173
- start_voice_btn = gr.Button("🎤 Start Conversation", variant="primary", size="lg")
1174
- stop_voice_btn = gr.Button("⏹️ Stop", variant="stop", size="lg", interactive=False)
1175
-
1176
- # Message Input
1177
- with gr.Group():
1178
- gr.Markdown("#### 💬 Send Message")
1179
- voice_input_text = gr.Textbox(
1180
- label="",
1181
- placeholder="Type your question...",
1182
- lines=3,
1183
- container=False,
1184
- info="Press Enter or click Send"
1185
- )
1186
- send_voice_btn = gr.Button("📤 Send", variant="secondary")
1187
-
1188
- with gr.Column(scale=3):
1189
- # Chat Interface with Gradio 6 Chatbot
1190
- with gr.Group():
1191
- voice_chatbot = gr.Chatbot(
1192
- label="Conversation",
1193
- type="messages",
1194
- height=500,
1195
- show_copy_button=True
1196
- )
1197
-
1198
- clear_chat_btn = gr.Button("🗑️ Clear Chat", variant="secondary")
1199
 
1200
- # Voice Assistant event handlers
1201
- start_voice_btn.click(
1202
- fn=start_voice_conversation,
1203
- outputs=[voice_status_display, start_voice_btn, stop_voice_btn, voice_chatbot]
1204
- )
1205
-
1206
- stop_voice_btn.click(
1207
- fn=stop_voice_conversation,
1208
- outputs=[voice_status_display, start_voice_btn, stop_voice_btn, voice_chatbot]
1209
- )
1210
-
1211
- send_voice_btn.click(
1212
- fn=send_voice_message_v6,
1213
- inputs=[voice_input_text, voice_chatbot],
1214
- outputs=[voice_chatbot, voice_input_text]
1215
- )
1216
-
1217
- voice_input_text.submit(
1218
- fn=send_voice_message_v6,
1219
- inputs=[voice_input_text, voice_chatbot],
1220
- outputs=[voice_chatbot, voice_input_text]
1221
- )
1222
-
1223
- clear_chat_btn.click(
1224
- fn=lambda: [],
1225
- outputs=[voice_chatbot]
1226
- )
1227
 
 
1228
  with gr.Tab("🎧 Podcast Studio"):
1229
- gr.Markdown("""
1230
- ### 🎙️ AI-Powered Podcast Generation
1231
-
1232
- Transform your documents into engaging audio conversations. Select documents,
1233
- customize the style and voices, and let AI create a professional podcast.
1234
-
1235
- **Powered by:** ElevenLabs AI Voice Technology
1236
- """)
1237
-
1238
  with gr.Row():
1239
- with gr.Column(scale=2):
1240
- # Configuration Panel
1241
- with gr.Group():
1242
- gr.Markdown("#### 📚 Select Content")
1243
-
1244
- podcast_doc_selector = gr.CheckboxGroup(
1245
- choices=get_document_choices(),
1246
- label="Documents to Include",
1247
- info="Choose 1-5 documents for best results",
1248
- interactive=True
1249
- )
1250
-
1251
- with gr.Accordion("🎨 Podcast Settings", open=True):
1252
- with gr.Row():
1253
- podcast_style = gr.Dropdown(
1254
- label="Style",
1255
- choices=["conversational", "educational", "technical", "casual"],
1256
- value="conversational",
1257
- info="Sets the tone and format"
1258
- )
1259
-
1260
- podcast_duration = gr.Slider(
1261
- label="Duration (minutes)",
1262
- minimum=5,
1263
- maximum=30,
1264
- value=10,
1265
- step=5,
1266
- info="Approximate length"
1267
- )
1268
-
1269
- gr.Markdown("#### 🗣️ Voice Selection")
1270
- with gr.Row():
1271
- host1_voice_selector = gr.Dropdown(
1272
- label="Host 1",
1273
- choices=["Rachel", "Adam", "Domi", "Bella", "Antoni", "Elli", "Josh"],
1274
- value="Rachel"
1275
- )
1276
- host2_voice_selector = gr.Dropdown(
1277
- label="Host 2",
1278
- choices=["Adam", "Rachel", "Josh", "Sam", "Emily", "Antoni", "Arnold"],
1279
- value="Adam"
1280
- )
1281
-
1282
- generate_podcast_btn = gr.Button(
1283
- "🎙️ Generate Podcast",
1284
- variant="primary",
1285
- size="lg"
1286
- )
1287
-
1288
- podcast_status = gr.Textbox(
1289
- label="Status",
1290
- interactive=False,
1291
- lines=2
1292
- )
1293
-
1294
- podcast_id_display = gr.Textbox(
1295
- label="Podcast ID",
1296
- interactive=False,
1297
- visible=False
1298
  )
 
 
 
 
1299
 
1300
- with gr.Column(scale=3):
1301
- # Output Panel
1302
- with gr.Group():
1303
- gr.Markdown("#### 🎵 Generated Podcast")
1304
-
1305
- podcast_audio_player = gr.Audio(
1306
- label="",
1307
- type="filepath",
1308
- interactive=False,
1309
- autoplay=True,
1310
- container=False
1311
- )
1312
-
1313
- with gr.Accordion("📝 Transcript", open=False):
1314
- podcast_transcript_display = gr.Markdown(
1315
- value="*Transcript will appear after generation...*"
1316
- )
1317
-
1318
- # Event handlers
1319
- generate_podcast_btn.click(
1320
- fn=generate_podcast_ui,
1321
- inputs=[
1322
- podcast_doc_selector,
1323
- podcast_style,
1324
- podcast_duration,
1325
- host1_voice_selector,
1326
- host2_voice_selector
1327
- ],
1328
- outputs=[
1329
- podcast_status,
1330
- podcast_audio_player,
1331
- podcast_transcript_display,
1332
- podcast_id_display
1333
- ]
1334
- )
1335
-
1336
- with gr.Tab("❓ Ask Questions"):
1337
- with gr.Row():
1338
- with gr.Column():
1339
- gr.Markdown("""### Ask Questions About Your Documents
1340
- The AI will search through all your uploaded documents to find relevant information
1341
- and provide comprehensive answers with sources.""")
1342
- qa_question_input = gr.Textbox(label="Your Question", placeholder="Ask anything about your documents...", lines=3)
1343
- qa_btn_action = gr.Button("❓ Get Answer", variant="primary", size="lg")
1344
  with gr.Column():
1345
- qa_output_display = gr.Textbox(label="AI Answer", lines=20, placeholder="Answer will appear here with sources...")
1346
-
1347
- all_dropdowns_to_update = [delete_doc_dropdown_visible, doc_dropdown_content, doc_dropdown_tag_visible]
1348
-
1349
- refresh_outputs = [document_list_display] + [dd for dd in all_dropdowns_to_update]
1350
- refresh_btn_library.click(fn=refresh_library, outputs=refresh_outputs)
1351
-
1352
- upload_outputs = [upload_output_display, doc_id_output_display, document_list_display] + [dd for dd in all_dropdowns_to_update]
1353
- upload_btn_process.click(upload_and_process_file, inputs=[file_input_upload], outputs=upload_outputs)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1354
 
1355
- delete_outputs = [delete_output_display, document_list_display] + [dd for dd in all_dropdowns_to_update]
1356
- delete_btn.click(delete_document_from_library, inputs=[delete_doc_dropdown_visible], outputs=delete_outputs)
1357
-
1358
- search_btn_action.click(perform_search, inputs=[search_query_input, search_top_k_slider], outputs=[search_output_display])
1359
- tag_btn_action.click(generate_tags_for_document, inputs=[doc_dropdown_tag_visible, tag_text_input, max_tags_slider], outputs=[tag_output_display])
1360
- qa_btn_action.click(ask_question, inputs=[qa_question_input], outputs=[qa_output_display])
1361
 
 
 
1362
 
1363
- # Load dashboard stats on interface load
1364
- interface.load(
1365
- fn=load_dashboard_stats,
1366
- outputs=[total_docs, total_chunks, storage_size, recent_docs, vector_status, llm_status, voice_status]
1367
- )
1368
-
1369
- interface.load(fn=refresh_library, outputs=refresh_outputs)
1370
- return interface
1371
 
1372
  if __name__ == "__main__":
1373
- gradio_interface = create_gradio_interface()
1374
- gradio_interface.launch()
 
1
  import gradio as gr
2
  import os
3
  import asyncio
 
4
  import logging
 
 
 
 
 
5
  import nest_asyncio
6
+ from fastapi import FastAPI
7
+ import uvicorn
8
 
9
+ # Apply nest_asyncio to handle nested event loops
10
  nest_asyncio.apply()
11
 
12
+ # Import shared services and app from mcp_server
13
+ # This ensures we use the SAME instances for both MCP and UI
14
+ from mcp_server import (
15
+ app, # The FastAPI app with MCP transport mounted
16
+ vector_store_service,
17
+ document_store_service,
18
+ embedding_service_instance,
19
+ llm_service_instance,
20
+ ocr_service_instance,
21
+ ingestion_tool_instance,
22
+ search_tool_instance,
23
+ generative_tool_instance,
24
+ voice_tool_instance,
25
+ podcast_tool_instance,
26
+ elevenlabs_service_instance,
27
+ llamaindex_service_instance,
28
+ podcast_generator_instance
29
+ )
30
 
31
  # Setup logging
32
  logging.basicConfig(level=logging.INFO)
33
  logger = logging.getLogger(__name__)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
 
35
+ # --- Gradio UI Logic ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
 
37
+ # Global state for voice conversation (kept for UI compatibility)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  voice_conversation_state = {
 
39
  "active": False,
40
+ "session_id": None,
41
  "transcript": []
42
  }
43
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  def create_gradio_interface():
45
+ """Create the Gradio interface using imported services"""
 
 
 
 
 
 
 
 
 
 
 
 
 
46
 
47
+ with gr.Blocks(title="AI Digital Library Assistant", theme=gr.themes.Soft()) as demo:
48
+ gr.Markdown("# 📚 AI Digital Library Assistant")
49
+
50
  with gr.Tabs():
51
+ # Tab 1: Dashboard
52
+ with gr.Tab("📊 Dashboard"):
53
+ gr.Markdown("### Library Statistics")
 
 
 
 
54
  with gr.Row():
55
+ total_docs = gr.Number(label="Total Documents", value=0)
56
+ total_chunks = gr.Number(label="Total Chunks", value=0)
57
+ storage_size = gr.Textbox(label="Storage Usage", value="0 MB")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
 
59
+ gr.Markdown("### System Status")
 
60
  with gr.Row():
61
+ vector_status = gr.Textbox(label="Vector Store", value="Checking...")
62
+ llm_status = gr.Textbox(label="LLM Service", value="Checking...")
63
+ voice_status = gr.Textbox(label="Voice Service", value="Checking...")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
 
65
+ refresh_btn = gr.Button("Refresh Stats")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
 
67
+ # Tab 2: Document Management
68
+ with gr.Tab("📂 Documents"):
 
 
 
 
69
  with gr.Row():
70
  with gr.Column(scale=1):
71
+ file_input = gr.File(label="Upload Document", file_count="multiple")
72
+ upload_btn = gr.Button("Process & Ingest", variant="primary")
73
+ upload_status = gr.Textbox(label="Status")
 
 
 
 
 
 
 
 
 
 
 
 
 
74
 
75
  with gr.Column(scale=2):
76
+ doc_list = gr.DataFrame(
77
+ headers=["ID", "Name", "Type", "Size", "Date"],
78
+ label="Library Content"
79
+ )
80
+ refresh_library_btn = gr.Button("Refresh Library")
 
 
81
 
82
+ # Tab 3: Search & Chat
83
+ with gr.Tab("🔍 Search & Chat"):
 
 
 
 
 
84
  with gr.Row():
85
+ with gr.Column(scale=1):
86
+ search_query = gr.Textbox(label="Search Query", placeholder="Enter your search query...")
87
+ top_k_slider = gr.Slider(minimum=1, maximum=10, value=5, step=1, label="Top K Results")
88
+ search_btn = gr.Button("Search", variant="primary")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
 
90
+ with gr.Column(scale=1):
91
+ chat_input = gr.Textbox(label="Ask a Question (RAG)", placeholder="Ask about your documents...")
92
+ chat_btn = gr.Button("Ask", variant="primary")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
 
 
 
 
 
 
 
 
 
 
 
 
 
94
  with gr.Row():
95
+ search_results = gr.JSON(label="Search Results")
96
+ chat_output = gr.Markdown(label="Answer")
 
 
 
 
 
 
97
 
98
+ # Tab 4: Voice Assistant
99
  with gr.Tab("🎙️ Voice Assistant"):
100
+ gr.Markdown("### Talk to your Library")
 
 
 
 
 
 
 
 
101
  with gr.Row():
102
+ voice_status_display = gr.Textbox(label="Status", value="Ready")
103
+ start_voice_btn = gr.Button("Start Voice Session", variant="primary")
104
+ stop_voice_btn = gr.Button("End Session", variant="stop")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
 
106
+ voice_transcript = gr.Chatbot(label="Conversation Transcript")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
 
108
+ # Tab 5: Podcast Studio
109
  with gr.Tab("🎧 Podcast Studio"):
110
+ gr.Markdown("### Generate Podcasts from Documents")
 
 
 
 
 
 
 
 
111
  with gr.Row():
112
+ with gr.Column():
113
+ podcast_docs = gr.Dropdown(label="Select Documents", multiselect=True)
114
+ podcast_style = gr.Dropdown(
115
+ choices=["conversational", "educational", "technical", "casual"],
116
+ value="conversational",
117
+ label="Podcast Style"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
  )
119
+ podcast_duration = gr.Slider(minimum=1, maximum=30, value=5, step=1, label="Duration (minutes)")
120
+ host1 = gr.Dropdown(choices=["Rachel", "Domi", "Bella"], value="Rachel", label="Host 1 Voice")
121
+ host2 = gr.Dropdown(choices=["Adam", "Antoni", "Josh"], value="Adam", label="Host 2 Voice")
122
+ generate_podcast_btn = gr.Button("Generate Podcast", variant="primary")
123
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
  with gr.Column():
125
+ podcast_status = gr.Textbox(label="Generation Status")
126
+ podcast_audio = gr.Audio(label="Generated Podcast", type="filepath", autoplay=True)
127
+
128
+ # --- Event Handlers ---
129
+
130
+ # Dashboard
131
+ async def update_stats():
132
+ try:
133
+ docs = await document_store_service.list_documents(limit=1000)
134
+ # Simple stats logic
135
+ return len(docs), 0, "Unknown", "Ready", "Ready", "Ready"
136
+ except Exception as e:
137
+ return 0, 0, "Error", "Error", "Error", "Error"
138
+
139
+ refresh_btn.click(update_stats, outputs=[total_docs, total_chunks, storage_size, vector_status, llm_status, voice_status])
140
+
141
+ # Ingestion
142
+ async def process_files(files):
143
+ if not files:
144
+ return "No files selected"
145
+ results = []
146
+ for file in files:
147
+ try:
148
+ res = await ingestion_tool_instance.process_document(file.name)
149
+ results.append(f"{file.name}: {res.get('status', 'Success')}")
150
+ except Exception as e:
151
+ results.append(f"{file.name}: Error - {str(e)}")
152
+ return "\n".join(results)
153
+
154
+ upload_btn.click(process_files, inputs=[file_input], outputs=[upload_status])
155
+
156
+ # Library
157
+ async def list_docs():
158
+ try:
159
+ docs = await document_store_service.list_documents()
160
+ data = [[d.id, d.filename, d.file_type, d.file_size, d.upload_date] for d in docs]
161
+ return data
162
+ except:
163
+ return []
164
+
165
+ refresh_library_btn.click(list_docs, outputs=[doc_list])
166
+
167
+ # Podcast Doc List Update
168
+ async def update_podcast_docs():
169
+ try:
170
+ docs = await document_store_service.list_documents()
171
+ choices = [f"{d.filename} ({d.id})" for d in docs]
172
+ return gr.update(choices=choices)
173
+ except:
174
+ return gr.update(choices=[])
175
+
176
+ demo.load(update_podcast_docs, outputs=[podcast_docs])
177
+ refresh_library_btn.click(update_podcast_docs, outputs=[podcast_docs])
178
+
179
+ # Search
180
+ async def do_search(query, k):
181
+ if not query: return {}
182
+ return await search_tool_instance.search(query, int(k))
183
+
184
+ search_btn.click(do_search, inputs=[search_query, top_k_slider], outputs=[search_results])
185
+
186
+ # Chat
187
+ async def do_chat(question):
188
+ if not question: return ""
189
+ # Simple RAG implementation using search + LLM
190
+ results = await search_tool_instance.search(question, top_k=3)
191
+ context = "\n".join([r.content for r in results])
192
+ prompt = f"Context:\n{context}\n\nQuestion: {question}\nAnswer:"
193
+ return await llm_service_instance.generate_text(prompt)
194
+
195
+ chat_btn.click(do_chat, inputs=[chat_input], outputs=[chat_output])
196
+
197
+ # Podcast
198
+ async def generate_pod(doc_selection, style, duration, h1, h2):
199
+ if not doc_selection:
200
+ return "Please select documents", None
201
+
202
+ # Extract IDs from selection string "filename (id)"
203
+ doc_ids = [d.split('(')[-1].strip(')') for d in doc_selection]
204
+
205
+ try:
206
+ result = await podcast_tool_instance.generate_podcast(
207
+ document_ids=doc_ids,
208
+ style=style,
209
+ duration_minutes=duration,
210
+ host1_voice=h1,
211
+ host2_voice=h2
212
+ )
213
+ if result.get("success"):
214
+ return "Podcast generated successfully!", result.get("audio_file")
215
+ else:
216
+ return f"Error: {result.get('error')}", None
217
+ except Exception as e:
218
+ return f"Error: {str(e)}", None
219
+
220
+ generate_podcast_btn.click(
221
+ generate_pod,
222
+ inputs=[podcast_docs, podcast_style, podcast_duration, host1, host2],
223
+ outputs=[podcast_status, podcast_audio]
224
+ )
225
 
226
+ return demo
 
 
 
 
 
227
 
228
+ # Create the Gradio app
229
+ demo = create_gradio_interface()
230
 
231
+ # Mount Gradio app to FastAPI
232
+ # path="/" means Gradio will be at root
233
+ # MCP server is already mounted at /sse and /messages by mcp_server.py
234
+ app = gr.mount_gradio_app(app, demo, path="/")
 
 
 
 
235
 
236
  if __name__ == "__main__":
237
+ # Use port 7860 for HuggingFace Spaces
238
+ uvicorn.run(app, host="0.0.0.0", port=7860)
mcp_server.py CHANGED
@@ -285,6 +285,5 @@ async def health_check():
285
  """Health check endpoint for Modal"""
286
  return {"status": "healthy", "service": "mcp-server"}
287
 
288
- if __name__ == "__main__":
289
- import uvicorn
290
- uvicorn.run(app, host=host, port=port)
 
285
  """Health check endpoint for Modal"""
286
  return {"status": "healthy", "service": "mcp-server"}
287
 
288
+ # Main execution is now handled by app.py
289
+