ysharma HF Staff commited on
Commit
331a316
Β·
verified Β·
1 Parent(s): 9b08b09

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +369 -0
app.py ADDED
@@ -0,0 +1,369 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import requests
3
+ import json
4
+ import os
5
+ import re
6
+ from typing import List, Dict, Optional
7
+
8
+ # Initialize OpenAI client if available
9
+ openai_client = None
10
+ if os.environ.get("OPENAI_API_KEY"):
11
+ try:
12
+ from openai import OpenAI
13
+ openai_client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY"))
14
+ except ImportError:
15
+ print("OpenAI library not installed. Install with: pip install openai")
16
+ openai_client = None
17
+
18
+ class GradioOutreachAgent:
19
+ def __init__(self):
20
+ self.github_token = os.environ.get("GITHUB_TOKEN")
21
+
22
+ def extract_pr_info(self, pr_data: Dict) -> Dict:
23
+ """Extract key info from PR"""
24
+ return {
25
+ "title": pr_data["title"],
26
+ "description": pr_data.get("body", "") or "",
27
+ "author": pr_data["user"]["login"],
28
+ "author_name": pr_data["user"].get("name") or pr_data["user"]["login"],
29
+ "html_url": pr_data["html_url"],
30
+ "number": pr_data["number"],
31
+ "labels": [label["name"] for label in pr_data.get("labels", [])],
32
+ "merged_at": pr_data.get("merged_at"),
33
+ "files_changed": self.get_pr_files(pr_data.get("url", "")),
34
+ "repo_name": pr_data.get("base", {}).get("repo", {}).get("name", "gradio"),
35
+ "diff_url": pr_data.get("diff_url", ""),
36
+ "patch_url": pr_data.get("patch_url", ""),
37
+ "commits": pr_data.get("commits", 0),
38
+ "additions": pr_data.get("additions", 0),
39
+ "deletions": pr_data.get("deletions", 0),
40
+ "changed_files": pr_data.get("changed_files", 0)
41
+ }
42
+
43
+ def get_pr_files(self, pr_url: str) -> List[Dict]:
44
+ """Get files changed in PR"""
45
+ if not pr_url or not self.github_token:
46
+ return []
47
+
48
+ files_url = f"{pr_url}/files"
49
+ headers = {"Authorization": f"token {self.github_token}"}
50
+
51
+ try:
52
+ response = requests.get(files_url, headers=headers)
53
+ if response.status_code == 200:
54
+ return response.json()
55
+ except Exception as e:
56
+ print(f"Error fetching PR files: {e}")
57
+
58
+ return []
59
+
60
+ def get_pr_diff(self, pr_info: Dict) -> str:
61
+ """Get PR diff content"""
62
+ if not pr_info.get("diff_url") or not self.github_token:
63
+ return ""
64
+
65
+ headers = {"Authorization": f"token {self.github_token}"}
66
+
67
+ try:
68
+ response = requests.get(pr_info["diff_url"], headers=headers)
69
+ if response.status_code == 200:
70
+ # Limit diff size to avoid token limits
71
+ diff_content = response.text
72
+ if len(diff_content) > 8000: # Limit to ~8k characters
73
+ diff_content = diff_content[:8000] + "\n... (truncated)"
74
+ return diff_content
75
+ except Exception as e:
76
+ print(f"Error fetching PR diff: {e}")
77
+
78
+ return ""
79
+
80
+ def extract_images_from_pr(self, pr_info: Dict) -> List[str]:
81
+ """Extract image URLs from PR description and files"""
82
+ images = []
83
+
84
+ # Extract from PR description with multiple patterns
85
+ description = pr_info["description"]
86
+
87
+ # Pattern 1: Standard markdown images ![alt](url)
88
+ img_pattern1 = r'!\[.*?\]\((.*?)\)'
89
+ img_matches1 = re.findall(img_pattern1, description)
90
+
91
+ # Pattern 2: HTML img tags
92
+ img_pattern2 = r'<img[^>]+src=["\']([^"\']+)["\']'
93
+ img_matches2 = re.findall(img_pattern2, description)
94
+
95
+ # Pattern 3: Direct image URLs
96
+ img_pattern3 = r'https?://[^\s]+\.(?:png|jpg|jpeg|gif|webp|svg)'
97
+ img_matches3 = re.findall(img_pattern3, description, re.IGNORECASE)
98
+
99
+ # Combine all matches
100
+ all_matches = img_matches1 + img_matches2 + img_matches3
101
+
102
+ for img_url in all_matches:
103
+ if img_url.startswith("http"):
104
+ images.append(img_url)
105
+
106
+ # Extract from changed files
107
+ for file in pr_info.get("files_changed", []):
108
+ filename = file["filename"]
109
+ if any(filename.lower().endswith(ext) for ext in ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg']):
110
+ if file["status"] in ["added", "modified"]:
111
+ # Try to construct the raw URL for the image
112
+ raw_url = file.get("raw_url")
113
+ if raw_url:
114
+ images.append(raw_url)
115
+
116
+ # Remove duplicates while preserving order
117
+ unique_images = []
118
+ for img in images:
119
+ if img not in unique_images:
120
+ unique_images.append(img)
121
+
122
+ return unique_images
123
+
124
+ def generate_tweet_from_pr_analysis(self, pr_info: Dict) -> str:
125
+ """Generate tweet by analyzing the entire PR content"""
126
+ if not openai_client:
127
+ return self.generate_fallback_tweet(pr_info)
128
+
129
+ # Get PR diff for better understanding
130
+ diff_content = self.get_pr_diff(pr_info)
131
+
132
+ # Prepare comprehensive PR context
133
+ files_summary = self.summarize_files_changed(pr_info.get("files_changed", []))
134
+
135
+ prompt = f"""Analyze this GitHub PR and create a viral tweet explaining what was accomplished:
136
+
137
+ PR DETAILS:
138
+ Title: {pr_info['title']}
139
+ Description: {pr_info['description']}
140
+ Author: {pr_info['author_name']}
141
+ Files Changed: {pr_info['changed_files']} files
142
+ Lines Added: {pr_info['additions']}
143
+ Lines Deleted: {pr_info['deletions']}
144
+ Commits: {pr_info['commits']}
145
+ Labels: {', '.join(pr_info['labels'])}
146
+ Changed Files Summary: {files_summary}
147
+
148
+ TECHNICAL CHANGES:
149
+ {diff_content}
150
+
151
+ INSTRUCTIONS:
152
+ 1. First understand what this PR actually does - what problem it solves or what feature it adds
153
+ 2. Focus on the USER BENEFIT - what can developers/users now do that they couldn't before?
154
+ 3. Create a viral tweet that explains the work done in simple, exciting terms
155
+ 4. Don't mention the author name
156
+ 5. Use emojis and engaging language
157
+ 6. Keep it under 280 characters
158
+ 7. Make it sound like a breakthrough or useful improvement
159
+
160
+ Examples of good explanations:
161
+ - "πŸ”₯ Gradio now supports [specific feature] - making [task] 10x easier for developers!"
162
+ - "✨ New: [feature] in Gradio! No more [old pain point] - just [new simple way]"
163
+ - "πŸš€ Game changer: Gradio can now [capability] - this opens up [new possibilities]"
164
+
165
+ Return only the tweet text, no additional formatting, and no hashtag."""
166
+
167
+ try:
168
+ response = openai_client.chat.completions.create(
169
+ model="gpt-4.1-mini",
170
+ messages=[
171
+ {"role": "system", "content": "You are a technical social media expert who analyzes code changes and explains them in engaging, accessible terms. Focus on practical benefits and make complex technical work sound exciting and useful."},
172
+ {"role": "user", "content": prompt}
173
+ ],
174
+ max_tokens=200,
175
+ temperature=0.7
176
+ )
177
+ return response.choices[0].message.content.strip()
178
+ except Exception as e:
179
+ print(f"OpenAI error: {e}")
180
+ return self.generate_fallback_tweet(pr_info)
181
+
182
+ def generate_fallback_tweet(self, pr_info: Dict) -> str:
183
+ """Generate fallback tweet when OpenAI is not available"""
184
+ title = pr_info['title'].lower()
185
+ pr_title = pr_info['title']
186
+
187
+ # Extract key features/benefits from title
188
+ if 'api' in title and 'description' in title:
189
+ return f"πŸ”₯ Gradio now auto-generates API documentation! No more manual docs - just add one parameter and get beautiful API descriptions instantly ✨ #gradio #ai #developer"
190
+ elif 'add' in title and 'parameter' in title:
191
+ return f"✨ New Gradio parameter unlocked! You can now {pr_title.lower().replace('add ', '').replace('parameter', 'customize')} - making your apps even more powerful πŸš€ #gradio #nocode"
192
+ elif 'fix' in title or 'bug' in title:
193
+ return f"πŸ”§ Gradio just got more reliable! Fixed the issue where {pr_title.lower().replace('fix ', '').replace('bug', 'things')} - smoother experience for everyone πŸ’ͺ #gradio #ai"
194
+ elif 'support' in title or 'enable' in title:
195
+ return f"πŸš€ Breaking: Gradio now supports {pr_title.lower().replace('add support for ', '').replace('enable ', '')}! This opens up so many new possibilities 🌟 #gradio #ai"
196
+ elif 'improve' in title or 'enhance' in title:
197
+ return f"⚑ Gradio upgrade: {pr_title.lower().replace('improve ', '').replace('enhance ', '')} just got way better! Your apps will feel snappier than ever πŸ”₯ #gradio #performance"
198
+ elif 'allow' in title or 'let' in title:
199
+ return f"✨ You can now {pr_title.lower().replace('allow ', '').replace('let ', '')} in Gradio! This is exactly what the community asked for πŸŽ‰ #gradio #ai"
200
+ else:
201
+ # Generic viral template
202
+ benefit = self.extract_benefit_from_title(pr_title)
203
+ return f"πŸ”₯ Gradio just leveled up! {benefit} - this changes everything for AI app builders πŸš€ #gradio #ai #machinelearning"
204
+
205
+ def extract_benefit_from_title(self, title: str) -> str:
206
+ """Extract user benefit from PR title"""
207
+ # Simple benefit extraction
208
+ if 'add' in title.lower():
209
+ return f"New feature: {title.replace('Add ', '').replace('add ', '')}"
210
+ elif 'fix' in title.lower():
211
+ return f"No more issues with {title.replace('Fix ', '').replace('fix ', '')}"
212
+ elif 'support' in title.lower():
213
+ return f"Full support for {title.replace('Add support for ', '').replace('support for ', '')}"
214
+ else:
215
+ return title
216
+
217
+ def summarize_files_changed(self, files: List[Dict]) -> str:
218
+ """Summarize what files were changed"""
219
+ if not files:
220
+ return "No files changed"
221
+
222
+ categories = {
223
+ "Python backend": [],
224
+ "Frontend (JS/CSS)": [],
225
+ "Documentation": [],
226
+ "Tests": [],
227
+ "Examples": [],
228
+ "Config": []
229
+ }
230
+
231
+ for file in files:
232
+ filename = file["filename"]
233
+ if filename.endswith('.py'):
234
+ categories["Python backend"].append(filename)
235
+ elif any(filename.endswith(ext) for ext in ['.js', '.jsx', '.ts', '.tsx', '.css', '.scss']):
236
+ categories["Frontend (JS/CSS)"].append(filename)
237
+ elif filename.endswith('.md') or 'readme' in filename.lower():
238
+ categories["Documentation"].append(filename)
239
+ elif 'test' in filename.lower():
240
+ categories["Tests"].append(filename)
241
+ elif 'example' in filename.lower() or 'demo' in filename.lower():
242
+ categories["Examples"].append(filename)
243
+ else:
244
+ categories["Config"].append(filename)
245
+
246
+ summary_parts = []
247
+ for category, file_list in categories.items():
248
+ if file_list:
249
+ summary_parts.append(f"{category}: {len(file_list)} files")
250
+
251
+ return ", ".join(summary_parts) if summary_parts else "misc files"
252
+
253
+ # Initialize agent
254
+ agent = GradioOutreachAgent()
255
+
256
+ def generate_tweet_from_pr(pr_url: str) -> tuple:
257
+ """Generate tweet from PR URL by analyzing the entire PR"""
258
+ try:
259
+ # Validate PR URL
260
+ if not pr_url or 'github.com' not in pr_url or '/pull/' not in pr_url:
261
+ return "❌ Please provide a valid GitHub PR URL", []
262
+
263
+ # Extract PR number from URL
264
+ pr_match = re.search(r'/pull/(\d+)', pr_url)
265
+ if not pr_match:
266
+ return "❌ Invalid PR URL format", []
267
+
268
+ pr_number = pr_match.group(1)
269
+
270
+ # Convert to API URL
271
+ api_url = pr_url.replace('github.com', 'api.github.com/repos').replace('/pull/', '/pulls/')
272
+
273
+ # Set up headers
274
+ headers = {}
275
+ if agent.github_token:
276
+ headers["Authorization"] = f"token {agent.github_token}"
277
+
278
+ # Fetch PR data
279
+ response = requests.get(api_url, headers=headers)
280
+ if response.status_code != 200:
281
+ return f"❌ Failed to fetch PR data (Status: {response.status_code})", []
282
+
283
+ pr_data = response.json()
284
+
285
+ # Check if PR is merged
286
+ if not pr_data.get("merged", False):
287
+ return "⚠️ This PR is not merged yet", []
288
+
289
+ # Extract PR info
290
+ pr_info = agent.extract_pr_info(pr_data)
291
+
292
+ # Generate tweet by analyzing the entire PR
293
+ tweet = agent.generate_tweet_from_pr_analysis(pr_info)
294
+
295
+ # Extract images
296
+ images = agent.extract_images_from_pr(pr_info)
297
+
298
+ # Ensure images is a proper list for gallery
299
+ if not images:
300
+ images = []
301
+
302
+ return tweet, images
303
+
304
+ except Exception as e:
305
+ return f"❌ Error: {str(e)}", []
306
+
307
+ # Gradio UI
308
+ with gr.Blocks() as demo:
309
+ gr.Markdown("# 🐦 Gradio PR Outreach : Tweet Generator")
310
+ gr.Markdown("Generate viral format tweets for merged Gradio PRs! This tool will analyze the entire PR content and try to create an engaging tweet explaining what was accomplished.")
311
+
312
+ with gr.Row():
313
+ with gr.Column(scale=2):
314
+ pr_url_input = gr.Textbox(
315
+ label="πŸ”— Merged PR URL",
316
+ placeholder="https://github.com/gradio-app/gradio/pull/11578",
317
+ value="https://github.com/gradio-app/gradio/pull/11578",
318
+ lines=1
319
+ )
320
+
321
+ generate_button = gr.Button("✨ Analyze PR & Generate Tweet", variant="primary")
322
+
323
+ with gr.Column(scale=1):
324
+ if not openai_client:
325
+ gr.Markdown("⚠️ **OpenAI API key not configured**\nAdd `OPENAI_API_KEY` to environment variables for better tweet generation.")
326
+ else:
327
+ gr.Markdown("βœ… **Using OpenAI gpt-4.1-mini to analyze PRs!**")
328
+
329
+ with gr.Row():
330
+ with gr.Column(scale=3):
331
+ tweet_output = gr.Textbox(
332
+ label="🐦 Generated Tweet",
333
+ lines=6,
334
+ interactive=True,
335
+ placeholder="AI-generated tweet will appear here..."
336
+ )
337
+
338
+ gr.Markdown("*πŸ’‘ Tip: You can edit the tweet above before posting and also use the extracted images for your post!*")
339
+
340
+ with gr.Column(scale=2):
341
+ images_output = gr.Gallery(
342
+ label="πŸ–ΌοΈ Extracted Images",
343
+ height=400,
344
+ columns=2,
345
+ object_fit="cover"
346
+ )
347
+
348
+ generate_button.click(
349
+ generate_tweet_from_pr,
350
+ inputs=[pr_url_input],
351
+ outputs=[tweet_output, images_output]
352
+ )
353
+
354
+ # Adding examples
355
+ gr.Examples(
356
+ examples=[
357
+ ["https://github.com/gradio-app/gradio/pull/11578"],
358
+ ["https://github.com/gradio-app/gradio/pull/11567"],
359
+ ["https://github.com/gradio-app/gradio/pull/11532"]
360
+ ],
361
+ inputs=[pr_url_input],
362
+ outputs=[tweet_output, images_output],
363
+ fn=generate_tweet_from_pr,
364
+ cache_examples=False,
365
+ label="🎯 Example PRs to Try:"
366
+ )
367
+
368
+ if __name__ == "__main__":
369
+ demo.launch(mcp_server=True)