Spaces:
Sleeping
Sleeping
| """Explainor - AI Agent that explains any topic in fun persona voices. | |
| MCP's 1st Birthday Hackathon Submission | |
| Track: MCP in Action (Creative) | |
| Team: kaiser-data | |
| """ | |
| import os | |
| import tempfile | |
| import gradio as gr | |
| from dotenv import load_dotenv | |
| from src.personas import PERSONAS, get_persona_names, get_persona | |
| from src.agent import run_agent | |
| from src.tts import generate_speech | |
| # Load environment variables | |
| load_dotenv() | |
| def format_sources(sources: list[dict]) -> str: | |
| """Format sources as markdown.""" | |
| if not sources: | |
| return "*No external sources used*" | |
| md = "" | |
| for i, src in enumerate(sources, 1): | |
| if src.get("url"): | |
| md += f"{i}. [{src['title']}]({src['url']})\n" | |
| else: | |
| md += f"{i}. {src['title']} ({src.get('source', 'General')})\n" | |
| return md | |
| def format_mcp_tools(tools: list[dict]) -> str: | |
| """Format tools used as markdown.""" | |
| if not tools: | |
| return "*No tools used*" | |
| md = "**🔌 Agent Tool Calls:**\n\n" | |
| for tool in tools: | |
| md += f"| {tool['icon']} | `{tool['name']}` | {tool['desc']} |\n" | |
| return md | |
| def explain_topic(topic: str, persona_name: str, audience: str = "", generate_audio: bool = False, progress=gr.Progress()): | |
| """Main function to explain a topic in a persona's voice. | |
| Returns: (explanation_text, audio_path, sources_md, steps_md, mcp_md) | |
| """ | |
| if not topic.strip(): | |
| return ( | |
| "Please enter a topic to explain!", | |
| None, | |
| "", | |
| "❌ No topic provided", | |
| "", | |
| ) | |
| if not persona_name: | |
| persona_name = "5-Year-Old" | |
| steps_log = [] | |
| explanation = "" | |
| sources = [] | |
| voice_id = None | |
| voice_settings = None | |
| mcp_tools = [] | |
| # Run the agent pipeline | |
| progress(0, desc="Starting...") | |
| for update in run_agent(topic, persona_name, audience): | |
| if update["type"] == "step": | |
| step_text = f"**{update['title']}**\n{update['content']}" | |
| steps_log.append(step_text) | |
| if update["step"] == "research": | |
| progress(0.2, desc="Researching...") | |
| elif update["step"] == "research_done": | |
| progress(0.4, desc="Research complete") | |
| if "sources" in update: | |
| sources = update["sources"] | |
| elif update["step"] == "generating": | |
| progress(0.6, desc="Generating explanation...") | |
| elif update["type"] == "result": | |
| explanation = update["explanation"] | |
| sources = update.get("sources", sources) | |
| voice_id = update["voice_id"] | |
| voice_settings = update.get("voice_settings") | |
| mcp_tools = update.get("mcp_tools", []) | |
| progress(0.8, desc="Explanation ready!") | |
| # Format the steps log | |
| steps_md = "\n\n---\n\n".join(steps_log) | |
| # Generate audio only if checkbox is checked | |
| audio_path = None | |
| if generate_audio and explanation and voice_id: | |
| progress(0.9, desc="Generating audio...") | |
| try: | |
| audio_bytes = generate_speech(explanation, voice_id, voice_settings) | |
| # Save to temp file for Gradio | |
| with tempfile.NamedTemporaryFile(suffix=".mp3", delete=False) as f: | |
| f.write(audio_bytes) | |
| audio_path = f.name | |
| # Add text_to_speech tool | |
| mcp_tools.append({"name": "text_to_speech", "icon": "🔊", "desc": "Audio generation via ElevenLabs API"}) | |
| progress(1.0, desc="Done!") | |
| except Exception as e: | |
| steps_log.append(f"**⚠️ Audio generation failed**\n{str(e)}") | |
| steps_md = "\n\n---\n\n".join(steps_log) | |
| progress(1.0, desc="Done (no audio)") | |
| else: | |
| progress(1.0, desc="Done!") | |
| # Format sources | |
| sources_md = format_sources(sources) | |
| # Format MCP tools | |
| mcp_md = format_mcp_tools(mcp_tools) | |
| return explanation, audio_path, sources_md, steps_md, mcp_md | |
| # Build the Gradio interface | |
| def create_app(): | |
| """Create and configure the Gradio app.""" | |
| with gr.Blocks(title="Explainor - AI Persona Explanations") as app: | |
| # Header | |
| gr.Markdown( | |
| """ | |
| # 🎭 Explainor | |
| ### *AI Agent with Tool Orchestration* | |
| **Learn anything through the voice of your favorite characters!** | |
| This agent orchestrates multiple tools to: research your topic, | |
| extract key facts, transform explanations into character voices, and generate audio. | |
| """ | |
| ) | |
| with gr.Row(): | |
| with gr.Column(scale=2): | |
| topic_input = gr.Textbox( | |
| label="📝 What do you want to learn about?", | |
| placeholder="e.g., Blockchain, Photosynthesis, Black Holes...", | |
| lines=1, | |
| max_lines=1, | |
| ) | |
| with gr.Column(scale=1): | |
| # Build persona choices with emojis | |
| persona_choices = [ | |
| f"{PERSONAS[name]['emoji']} {name}" | |
| for name in get_persona_names() | |
| ] | |
| persona_dropdown = gr.Dropdown( | |
| choices=persona_choices, | |
| value=persona_choices[0], | |
| label="🎭 Choose your explainer", | |
| ) | |
| with gr.Row(): | |
| # Funny listener options (don't overlap with personas) | |
| listener_choices = [ | |
| "👤 Just me", | |
| "👵 My confused grandmother", | |
| "🤖 A skeptical robot", | |
| "👽 An alien visiting Earth", | |
| "🧟 A zombie (short attention span)", | |
| "🦊 A very smart fox", | |
| "👔 A stressed CEO", | |
| "🎮 A distracted gamer", | |
| ] | |
| audience_dropdown = gr.Dropdown( | |
| choices=listener_choices, | |
| value=listener_choices[0], | |
| label="👤 Who's listening?", | |
| ) | |
| explain_btn = gr.Button( | |
| "✨ Explain it to me!", | |
| variant="primary", | |
| size="lg", | |
| ) | |
| # Output section | |
| with gr.Row(): | |
| with gr.Column(): | |
| explanation_output = gr.Textbox( | |
| label="📖 Explanation", | |
| lines=8, | |
| max_lines=15, | |
| ) | |
| audio_checkbox = gr.Checkbox( | |
| label="🔊 Generate audio", | |
| value=False, | |
| ) | |
| audio_output = gr.Audio( | |
| label="🔊 Listen to the explanation", | |
| type="filepath", | |
| autoplay=False, | |
| ) | |
| with gr.Row(): | |
| with gr.Column(): | |
| with gr.Accordion("🔌 Agent Tool Calls", open=True): | |
| mcp_output = gr.Markdown("") | |
| with gr.Row(): | |
| with gr.Column(): | |
| with gr.Accordion("🔍 Sources", open=False): | |
| sources_output = gr.Markdown("") | |
| with gr.Column(): | |
| with gr.Accordion("🧠 Execution Trace", open=False): | |
| steps_output = gr.Markdown("") | |
| # Example topics | |
| gr.Examples( | |
| examples=[ | |
| ["Quantum Computing", f"{PERSONAS['5-Year-Old']['emoji']} 5-Year-Old"], | |
| ["Blockchain", f"{PERSONAS['Gordon Ramsay']['emoji']} Gordon Ramsay"], | |
| ["Black Holes", f"{PERSONAS['Pirate']['emoji']} Pirate"], | |
| ["Machine Learning", f"{PERSONAS['Shakespeare']['emoji']} Shakespeare"], | |
| ["Climate Change", f"{PERSONAS['Surfer Dude']['emoji']} Surfer Dude"], | |
| ["The Force", f"{PERSONAS['Yoda']['emoji']} Yoda"], | |
| ], | |
| inputs=[topic_input, persona_dropdown], | |
| label="Try these examples:", | |
| ) | |
| # Footer | |
| gr.Markdown( | |
| """ | |
| --- | |
| **Built for MCP's 1st Birthday Hackathon** | Track: MCP in Action (Creative) | |
| Powered by: [Nebius AI](https://nebius.com) (LLM) + [ElevenLabs](https://elevenlabs.io) (TTS) | |
| Made with ❤️ by **kaiser-data** | |
| """ | |
| ) | |
| # Event handler | |
| def process_and_explain(topic, persona_with_emoji, gen_audio, audience_with_emoji): | |
| # Extract persona name (remove emoji prefix) | |
| persona_name = persona_with_emoji.split(" ", 1)[1] if " " in persona_with_emoji else persona_with_emoji | |
| # Extract audience (remove emoji prefix), skip if "Just me" | |
| audience = "" | |
| if audience_with_emoji and "Just me" not in audience_with_emoji: | |
| audience = audience_with_emoji.split(" ", 1)[1] if " " in audience_with_emoji else audience_with_emoji | |
| return explain_topic(topic, persona_name, audience, gen_audio) | |
| explain_btn.click( | |
| fn=process_and_explain, | |
| inputs=[topic_input, persona_dropdown, audio_checkbox, audience_dropdown], | |
| outputs=[explanation_output, audio_output, sources_output, steps_output, mcp_output], | |
| ) | |
| # Also trigger on Enter key in topic input | |
| topic_input.submit( | |
| fn=process_and_explain, | |
| inputs=[topic_input, persona_dropdown, audio_checkbox, audience_dropdown], | |
| outputs=[explanation_output, audio_output, sources_output, steps_output, mcp_output], | |
| ) | |
| return app | |
| # Create the app | |
| app = create_app() | |
| CUSTOM_CSS = """ | |
| /* Fix dark mode input visibility */ | |
| input, textarea, select { | |
| color: var(--body-text-color) !important; | |
| background-color: var(--input-background-fill) !important; | |
| } | |
| input:hover, textarea:hover, select:hover, | |
| input:focus, textarea:focus, select:focus { | |
| color: var(--body-text-color) !important; | |
| background-color: var(--input-background-fill) !important; | |
| } | |
| /* Ensure placeholder is visible */ | |
| input::placeholder, textarea::placeholder { | |
| color: var(--body-text-color-subdued) !important; | |
| opacity: 0.7; | |
| } | |
| """ | |
| if __name__ == "__main__": | |
| app.launch( | |
| server_name="0.0.0.0", | |
| server_port=7860, | |
| share=False, | |
| css=CUSTOM_CSS, | |
| mcp_server=True, # Enable MCP server - exposes this app as an MCP tool! | |
| ) | |