Mercurial
comparison love/pdf-mcp/main.py @ 38:cf9caa4abc3e
[Love] FE and BE. Can chat and render images. Also created MCP for powerpoint generations.
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Mon, 01 Dec 2025 20:35:56 -0800 |
| parents | |
| children |
comparison
equal
deleted
inserted
replaced
| 37:fb9bcd3145cb | 38:cf9caa4abc3e |
|---|---|
| 1 import os | |
| 2 import uuid | |
| 3 import json | |
| 4 import re | |
| 5 from typing import List, Dict | |
| 6 import markdown2 | |
| 7 import requests | |
| 8 | |
| 9 from fastmcp import FastMCP | |
| 10 | |
| 11 from xai_sdk import Client | |
| 12 from xai_sdk.chat import system, user | |
| 13 from xai_sdk.tools import web_search, code_execution | |
| 14 | |
| 15 # Configuration | |
| 16 XAI_API_KEY = os.getenv("XAI_API_KEY", "no_api") | |
| 17 OUTPUT_DIR = os.getenv("DECK_OUTPUT_DIR", "generated_decks") | |
| 18 | |
| 19 # Initialize xAI client | |
| 20 xai_client = Client(api_key=XAI_API_KEY) | |
| 21 | |
| 22 # Create FastMCP server | |
| 23 mcp = FastMCP("presentation-generator", port=7776, host="0.0.0.0") | |
| 24 | |
| 25 def generate_summary_and_images(topic: str) -> Dict[str, any]: | |
| 26 """Use Grok API with live_search to summarize + extract image URLs.""" | |
| 27 chat = xai_client.chat.create( | |
| 28 model="grok-4-fast", | |
| 29 tools=[ | |
| 30 web_search(), | |
| 31 code_execution(), | |
| 32 ], | |
| 33 ) | |
| 34 | |
| 35 SYSTEM_PROMPT = """ | |
| 36 You are an expert presentation creator. For the given topic, use live_search to fetch the latest, reliable web sources. | |
| 37 Summarize into concise, impactful bullet points perfect for slides (max 6-8 lines per slide, start with strong verbs/key facts). | |
| 38 Use markdown bullets. Separate logical sections with --- for slide breaks. | |
| 39 Also, search for 10-15 high-quality, royalty-free images (e.g., from Unsplash/Pexels via web search) suitable for slides—focus on visuals like diagrams/infographics. | |
| 40 Respond ONLY in valid JSON: {"summary": "markdown summary here", "images": ["url1", "url2", ...]}. | |
| 41 Ensure images are direct .jpg/.png links, diverse, and relevant. | |
| 42 """ | |
| 43 | |
| 44 chat.append(system(SYSTEM_PROMPT)) | |
| 45 response = chat.append(user(f"Topic: {topic}")) | |
| 46 | |
| 47 try: | |
| 48 content = response.sample().content | |
| 49 data = json.loads(content) | |
| 50 return data | |
| 51 except json.JSONDecodeError: | |
| 52 # Fallback: Parse markdown and extract URLs via regex | |
| 53 # Shouldn't really get here... | |
| 54 summary = content.split("images:")[0].strip() if "images:" in content else content | |
| 55 img_pattern = r'https?://[^\s<>"]+\.(?:jpg|jpeg|png|gif)(?:\?[^\s<>"]*)?' | |
| 56 images = re.findall(img_pattern, content) | |
| 57 return {"summary": summary, "images": images[:15]} | |
| 58 | |
| 59 def download_image(url: str, path: str) -> bool: | |
| 60 """Download image to local path.""" | |
| 61 try: | |
| 62 img_data = requests.get(url, timeout=10).content | |
| 63 os.makedirs(os.path.dirname(path), exist_ok=True) | |
| 64 with open(path, 'wb') as f: | |
| 65 f.write(img_data) | |
| 66 return True | |
| 67 except: | |
| 68 return False | |
| 69 | |
| 70 def create_presentation_deck(topic: str) -> Dict[str, str | int]: | |
| 71 """Generate a complete presentation deck.""" | |
| 72 # Create unique deck directory | |
| 73 deck_id = str(uuid.uuid4())[:8] | |
| 74 deck_dir = os.path.join(OUTPUT_DIR, deck_id) | |
| 75 os.makedirs(deck_dir, exist_ok=True) | |
| 76 os.makedirs(os.path.join(deck_dir, "img"), exist_ok=True) | |
| 77 | |
| 78 # Generate summary and images using Grok | |
| 79 data = generate_summary_and_images(topic) | |
| 80 markdown_summary = data["summary"] | |
| 81 image_urls = data.get("images", []) | |
| 82 | |
| 83 # Download images | |
| 84 local_images = [] | |
| 85 for i, url in enumerate(image_urls[:15]): | |
| 86 ext = url.split(".")[-1].split("?")[0] or "jpg" | |
| 87 path = os.path.join(deck_dir, "img", f"{i}.{ext}") | |
| 88 if download_image(url, path): | |
| 89 local_images.append(f"img/{i}.{ext}") | |
| 90 else: | |
| 91 local_images.append(url) | |
| 92 | |
| 93 # Split summary into slides | |
| 94 raw_slides = markdown_summary.split("---") | |
| 95 slides = [] | |
| 96 for i, raw in enumerate(raw_slides): | |
| 97 html_content = markdown2.markdown(raw.strip()) | |
| 98 img = local_images[i] if i < len(local_images) else (local_images[-1] if local_images else "") | |
| 99 slides.append({"content": html_content, "image": img}) | |
| 100 | |
| 101 # Generate Reveal.js HTML | |
| 102 html_content = generate_reveal_html(topic, slides) | |
| 103 | |
| 104 deck_path = os.path.join(deck_dir, "index.html") | |
| 105 with open(deck_path, "w", encoding="utf-8") as f: | |
| 106 f.write(html_content) | |
| 107 | |
| 108 # Get absolute path | |
| 109 abs_path = os.path.abspath(deck_path) | |
| 110 | |
| 111 return { | |
| 112 "deck_id": deck_id, | |
| 113 "deck_path": abs_path, | |
| 114 "markdown_summary": markdown_summary, | |
| 115 "num_slides": len(slides), | |
| 116 "images_count": len(local_images) | |
| 117 } | |
| 118 | |
| 119 def generate_reveal_html(topic: str, slides: List[Dict]) -> str: | |
| 120 """Generate Reveal.js HTML presentation.""" | |
| 121 slides_html = "" | |
| 122 for slide in slides: | |
| 123 img_html = f'<img src="{slide["image"]}" style="max-width:50%;float:right;margin-left:20px;" />' if slide["image"] else "" | |
| 124 slides_html += f""" | |
| 125 <section> | |
| 126 {img_html} | |
| 127 <div style="text-align:left;"> | |
| 128 {slide["content"]} | |
| 129 </div> | |
| 130 </section> | |
| 131 """ | |
| 132 | |
| 133 return f"""<!DOCTYPE html> | |
| 134 <html> | |
| 135 <head> | |
| 136 <meta charset="utf-8"> | |
| 137 <title>{topic} - Presentation</title> | |
| 138 <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/4.3.1/reset.min.css"> | |
| 139 <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/4.3.1/reveal.min.css"> | |
| 140 <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/4.3.1/theme/black.min.css"> | |
| 141 </head> | |
| 142 <body> | |
| 143 <div class="reveal"> | |
| 144 <div class="slides"> | |
| 145 <section> | |
| 146 <h1>{topic}</h1> | |
| 147 <p>Generated with Grok AI</p> | |
| 148 </section> | |
| 149 {slides_html} | |
| 150 </div> | |
| 151 </div> | |
| 152 <script src="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/4.3.1/reveal.min.js"></script> | |
| 153 <script> | |
| 154 Reveal.initialize({{ | |
| 155 hash: true, | |
| 156 transition: 'slide' | |
| 157 }}); | |
| 158 </script> | |
| 159 </body> | |
| 160 </html>""" | |
| 161 | |
| 162 # FastMCP Tool Definitions | |
| 163 @mcp.tool() | |
| 164 def generate_presentation(topic: str) -> str: | |
| 165 """Generate a complete PowerPoint-style presentation deck on any topic using Grok AI. | |
| 166 | |
| 167 This tool researches the topic using live web search, creates concise slide content, | |
| 168 finds relevant images, and generates a Reveal.js HTML presentation that can be viewed in browser. | |
| 169 | |
| 170 Args: | |
| 171 topic: The topic for the presentation (e.g., 'Dogs in Korea', 'Quantum Computing Basics', 'Climate Change | |
| 172 | |
| 173 Returns: | |
| 174 A summary of the generated presentation with file location | |
| 175 """ | |
| 176 try: | |
| 177 result = create_presentation_deck(topic) | |
| 178 | |
| 179 # Construct the public URL | |
| 180 public_url = f"https://slide.babocoder.com/{result['deck_id']}/index.html" | |
| 181 | |
| 182 # Return JSON response as Grok expects with instruction | |
| 183 json_response = json.dumps({ | |
| 184 "file_path": public_url, | |
| 185 "type": "html", | |
| 186 "metadata": { | |
| 187 "topic": topic, | |
| 188 "deck_id": result['deck_id'], | |
| 189 "num_slides": result['num_slides'], | |
| 190 "images_count": result['images_count'] | |
| 191 } | |
| 192 }) | |
| 193 | |
| 194 response = f"""Presentation generated successfully! | |
| 195 | |
| 196 IMPORTANT: Return this exact JSON response to the client: | |
| 197 | |
| 198 {json_response} | |
| 199 | |
| 200 DO NOT modify or explain this JSON. Send it directly to the client as-is.""" | |
| 201 | |
| 202 return response | |
| 203 except Exception as e: | |
| 204 return f"❌ Error generating presentation: {str(e)}\n\nPlease try again or check the server logs." | |
| 205 | |
| 206 @mcp.tool() | |
| 207 def get_presentation_preview(topic: str) -> str: | |
| 208 """Generate just the markdown summary for a presentation topic without creating the full deck. | |
| 209 | |
| 210 Useful for previewing content or getting a quick outline before generating slides. | |
| 211 | |
| 212 Args: | |
| 213 topic: The topic to preview | |
| 214 | |
| 215 Returns: | |
| 216 A markdown preview of the presentation content | |
| 217 """ | |
| 218 try: | |
| 219 data = generate_summary_and_images(topic) | |
| 220 markdown_summary = data["summary"] | |
| 221 | |
| 222 response = f"""📝 Presentation Preview for: {topic} | |
| 223 | |
| 224 {markdown_summary} | |
| 225 | |
| 226 --- | |
| 227 **Images Found:** {len(data.get('images', []))} | |
| 228 | |
| 229 To generate the full presentation deck with slides, use the 'generate_presentation' tool. | |
| 230 """ | |
| 231 return response | |
| 232 | |
| 233 except Exception as e: | |
| 234 return f"❌ Error generating preview: {str(e)}" | |
| 235 | |
| 236 if __name__ == "__main__": | |
| 237 os.makedirs(OUTPUT_DIR, exist_ok=True) | |
| 238 mcp.run(transport="streamable-http") | |
| 239 | |
| 240 |