import gradio as gr import requests import json import os import re from typing import List, Dict, Optional # Initialize OpenAI client if available openai_client = None if os.environ.get("OPENAI_API_KEY"): try: from openai import OpenAI openai_client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY")) except ImportError: print("OpenAI library not installed. Install with: pip install openai") openai_client = None class GradioOutreachAgent: def __init__(self): self.github_token = os.environ.get("GITHUB_TOKEN") def extract_pr_info(self, pr_data: Dict) -> Dict: """Extract key info from PR""" return { "title": pr_data["title"], "description": pr_data.get("body", "") or "", "author": pr_data["user"]["login"], "author_name": pr_data["user"].get("name") or pr_data["user"]["login"], "html_url": pr_data["html_url"], "number": pr_data["number"], "labels": [label["name"] for label in pr_data.get("labels", [])], "merged_at": pr_data.get("merged_at"), "files_changed": self.get_pr_files(pr_data.get("url", "")), "repo_name": pr_data.get("base", {}).get("repo", {}).get("name", "gradio"), "diff_url": pr_data.get("diff_url", ""), "patch_url": pr_data.get("patch_url", ""), "commits": pr_data.get("commits", 0), "additions": pr_data.get("additions", 0), "deletions": pr_data.get("deletions", 0), "changed_files": pr_data.get("changed_files", 0) } def get_pr_files(self, pr_url: str) -> List[Dict]: """Get files changed in PR""" if not pr_url or not self.github_token: return [] files_url = f"{pr_url}/files" headers = {"Authorization": f"token {self.github_token}"} try: response = requests.get(files_url, headers=headers) if response.status_code == 200: return response.json() except Exception as e: print(f"Error fetching PR files: {e}") return [] def get_pr_diff(self, pr_info: Dict) -> str: """Get PR diff content""" if not pr_info.get("diff_url") or not self.github_token: return "" headers = {"Authorization": f"token {self.github_token}"} try: response = requests.get(pr_info["diff_url"], headers=headers) if response.status_code == 200: # Limit diff size to avoid token limits diff_content = response.text if len(diff_content) > 8000: # Limit to ~8k characters diff_content = diff_content[:8000] + "\n... (truncated)" return diff_content except Exception as e: print(f"Error fetching PR diff: {e}") return "" def extract_images_from_pr(self, pr_info: Dict) -> List[str]: """Extract image URLs from PR description and files""" images = [] # Extract from PR description with multiple patterns description = pr_info["description"] # Pattern 1: Standard markdown images ![alt](url) img_pattern1 = r'!\[.*?\]\((.*?)\)' img_matches1 = re.findall(img_pattern1, description) # Pattern 2: HTML img tags img_pattern2 = r']+src=["\']([^"\']+)["\']' img_matches2 = re.findall(img_pattern2, description) # Pattern 3: Direct image URLs img_pattern3 = r'https?://[^\s]+\.(?:png|jpg|jpeg|gif|webp|svg)' img_matches3 = re.findall(img_pattern3, description, re.IGNORECASE) # Combine all matches all_matches = img_matches1 + img_matches2 + img_matches3 for img_url in all_matches: if img_url.startswith("http"): images.append(img_url) # Extract from changed files for file in pr_info.get("files_changed", []): filename = file["filename"] if any(filename.lower().endswith(ext) for ext in ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg']): if file["status"] in ["added", "modified"]: # Try to construct the raw URL for the image raw_url = file.get("raw_url") if raw_url: images.append(raw_url) # Remove duplicates while preserving order unique_images = [] for img in images: if img not in unique_images: unique_images.append(img) return unique_images def generate_tweet_from_pr_analysis(self, pr_info: Dict) -> str: """Generate tweet by analyzing the entire PR content""" if not openai_client: return self.generate_fallback_tweet(pr_info) # Get PR diff for better understanding diff_content = self.get_pr_diff(pr_info) # Prepare comprehensive PR context files_summary = self.summarize_files_changed(pr_info.get("files_changed", [])) prompt = f"""Analyze this GitHub PR and create a viral tweet explaining what was accomplished: PR DETAILS: Title: {pr_info['title']} Description: {pr_info['description']} Author: {pr_info['author_name']} Files Changed: {pr_info['changed_files']} files Lines Added: {pr_info['additions']} Lines Deleted: {pr_info['deletions']} Commits: {pr_info['commits']} Labels: {', '.join(pr_info['labels'])} Changed Files Summary: {files_summary} TECHNICAL CHANGES: {diff_content} INSTRUCTIONS: 1. First understand what this PR actually does - what problem it solves or what feature it adds 2. Focus on the USER BENEFIT - what can developers/users now do that they couldn't before? 3. Create a viral tweet that explains the work done in simple, exciting terms 4. Don't mention the author name 5. Use emojis and engaging language 6. Keep it under 280 characters 7. Make it sound like a breakthrough or useful improvement Examples of good explanations: - "🔥 Gradio now supports [specific feature] - making [task] 10x easier for developers!" - "✨ New: [feature] in Gradio! No more [old pain point] - just [new simple way]" - "🚀 Game changer: Gradio can now [capability] - this opens up [new possibilities]" Return only the tweet text, no additional formatting, and no hashtag.""" try: response = openai_client.chat.completions.create( model="gpt-4.1-mini", messages=[ {"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."}, {"role": "user", "content": prompt} ], max_tokens=200, temperature=0.7 ) return response.choices[0].message.content.strip() except Exception as e: print(f"OpenAI error: {e}") return self.generate_fallback_tweet(pr_info) def generate_fallback_tweet(self, pr_info: Dict) -> str: """Generate fallback tweet when OpenAI is not available""" title = pr_info['title'].lower() pr_title = pr_info['title'] # Extract key features/benefits from title if 'api' in title and 'description' in title: 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" elif 'add' in title and 'parameter' in title: 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" elif 'fix' in title or 'bug' in title: 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" elif 'support' in title or 'enable' in title: return f"🚀 Breaking: Gradio now supports {pr_title.lower().replace('add support for ', '').replace('enable ', '')}! This opens up so many new possibilities 🌟 #gradio #ai" elif 'improve' in title or 'enhance' in title: return f"⚡ Gradio upgrade: {pr_title.lower().replace('improve ', '').replace('enhance ', '')} just got way better! Your apps will feel snappier than ever 🔥 #gradio #performance" elif 'allow' in title or 'let' in title: return f"✨ You can now {pr_title.lower().replace('allow ', '').replace('let ', '')} in Gradio! This is exactly what the community asked for 🎉 #gradio #ai" else: # Generic viral template benefit = self.extract_benefit_from_title(pr_title) return f"🔥 Gradio just leveled up! {benefit} - this changes everything for AI app builders 🚀 #gradio #ai #machinelearning" def extract_benefit_from_title(self, title: str) -> str: """Extract user benefit from PR title""" # Simple benefit extraction if 'add' in title.lower(): return f"New feature: {title.replace('Add ', '').replace('add ', '')}" elif 'fix' in title.lower(): return f"No more issues with {title.replace('Fix ', '').replace('fix ', '')}" elif 'support' in title.lower(): return f"Full support for {title.replace('Add support for ', '').replace('support for ', '')}" else: return title def summarize_files_changed(self, files: List[Dict]) -> str: """Summarize what files were changed""" if not files: return "No files changed" categories = { "Python backend": [], "Frontend (JS/CSS)": [], "Documentation": [], "Tests": [], "Examples": [], "Config": [] } for file in files: filename = file["filename"] if filename.endswith('.py'): categories["Python backend"].append(filename) elif any(filename.endswith(ext) for ext in ['.js', '.jsx', '.ts', '.tsx', '.css', '.scss']): categories["Frontend (JS/CSS)"].append(filename) elif filename.endswith('.md') or 'readme' in filename.lower(): categories["Documentation"].append(filename) elif 'test' in filename.lower(): categories["Tests"].append(filename) elif 'example' in filename.lower() or 'demo' in filename.lower(): categories["Examples"].append(filename) else: categories["Config"].append(filename) summary_parts = [] for category, file_list in categories.items(): if file_list: summary_parts.append(f"{category}: {len(file_list)} files") return ", ".join(summary_parts) if summary_parts else "misc files" # Initialize agent agent = GradioOutreachAgent() def generate_tweet_from_pr(pr_url: str) -> tuple: """Generate tweet from PR URL by analyzing the entire PR""" try: # Validate PR URL if not pr_url or 'github.com' not in pr_url or '/pull/' not in pr_url: return "❌ Please provide a valid GitHub PR URL", [] # Extract PR number from URL pr_match = re.search(r'/pull/(\d+)', pr_url) if not pr_match: return "❌ Invalid PR URL format", [] pr_number = pr_match.group(1) # Convert to API URL api_url = pr_url.replace('github.com', 'api.github.com/repos').replace('/pull/', '/pulls/') # Set up headers headers = {} if agent.github_token: headers["Authorization"] = f"token {agent.github_token}" # Fetch PR data response = requests.get(api_url, headers=headers) if response.status_code != 200: return f"❌ Failed to fetch PR data (Status: {response.status_code})", [] pr_data = response.json() # Check if PR is merged if not pr_data.get("merged", False): return "⚠️ This PR is not merged yet", [] # Extract PR info pr_info = agent.extract_pr_info(pr_data) # Generate tweet by analyzing the entire PR tweet = agent.generate_tweet_from_pr_analysis(pr_info) # Extract images images = agent.extract_images_from_pr(pr_info) # Ensure images is a proper list for gallery if not images: images = [] return tweet, images except Exception as e: return f"❌ Error: {str(e)}", [] # Gradio UI with gr.Blocks() as demo: gr.Markdown("# 🐦 Gradio PR Outreach : Tweet Generator") 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.") with gr.Row(): with gr.Column(scale=2): pr_url_input = gr.Textbox( label="🔗 Merged PR URL", placeholder="https://github.com/gradio-app/gradio/pull/11578", value="https://github.com/gradio-app/gradio/pull/11578", lines=1 ) generate_button = gr.Button("✨ Analyze PR & Generate Tweet", variant="primary") with gr.Column(scale=1): if not openai_client: gr.Markdown("⚠️ **OpenAI API key not configured**\nAdd `OPENAI_API_KEY` to environment variables for better tweet generation.") else: gr.Markdown("✅ **Using OpenAI gpt-4.1-mini to analyze PRs!**") with gr.Row(): with gr.Column(scale=3): tweet_output = gr.Textbox( label="🐦 Generated Tweet", lines=6, interactive=True, placeholder="AI-generated tweet will appear here..." ) gr.Markdown("*💡 Tip: You can edit the tweet above before posting and also use the extracted images for your post!*") with gr.Column(scale=2): images_output = gr.Gallery( label="🖼️ Extracted Images", height=400, columns=2, object_fit="cover" ) generate_button.click( generate_tweet_from_pr, inputs=[pr_url_input], outputs=[tweet_output, images_output] ) # Adding examples gr.Examples( examples=[ ["https://github.com/gradio-app/gradio/pull/11578"], ["https://github.com/gradio-app/gradio/pull/11567"], ["https://github.com/gradio-app/gradio/pull/11532"] ], inputs=[pr_url_input], outputs=[tweet_output, images_output], fn=generate_tweet_from_pr, cache_examples=False, label="🎯 Example PRs to Try:" ) if __name__ == "__main__": demo.launch(mcp_server=True)