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