devrel-agent-gradio / services.py
chenglu's picture
Upload 4 files
7c4bfcc verified
"""
Service adapters for Claude AI content generation
"""
import os
from typing import Optional
import anthropic
# Type definitions
class RepoProfile:
def __init__(
self,
url: str,
owner: str,
name: str,
description: str,
languages: dict,
primary_language: str,
stars: int,
forks: int,
structure: dict,
readme: str,
main_features: list[str],
technical_stack: list[str],
quickstart_path: Optional[str] = None,
commits: Optional[list] = None,
commits_with_diffs: Optional[list] = None,
breaking_changes: Optional[list] = None,
initial_readme: Optional[str] = None,
readme_diff: Optional[str] = None,
has_readme_changes: bool = False,
):
self.url = url
self.owner = owner
self.name = name
self.description = description
self.languages = languages
self.primary_language = primary_language
self.stars = stars
self.forks = forks
self.structure = structure
self.readme = readme
self.main_features = main_features
self.technical_stack = technical_stack
self.quickstart_path = quickstart_path
self.commits = commits or []
self.commits_with_diffs = commits_with_diffs or []
self.breaking_changes = breaking_changes or []
self.initial_readme = initial_readme or ""
self.readme_diff = readme_diff or ""
self.has_readme_changes = has_readme_changes
class ContentVariant:
def __init__(
self,
id: str,
format: str,
content: str,
score: Optional[float] = None,
evaluation_metrics: Optional[dict] = None,
):
self.id = id
self.format = format
self.content = content
self.score = score
self.evaluation_metrics = evaluation_metrics
# Claude Service
class ClaudeService:
def __init__(self, api_key: Optional[str] = None):
"""Initialize Claude service.
Priority for API key:
1. Environment variable ANTHROPIC_API_KEY (for Hugging Face Spaces secrets)
2. User-provided api_key parameter (for manual override)
"""
# Environment variable takes priority (for HF Spaces secrets)
self.api_key = os.environ.get("ANTHROPIC_API_KEY") or api_key
if not self.api_key:
raise ValueError("ANTHROPIC_API_KEY not configured. Please provide your own API key in Settings.")
self.client = anthropic.Anthropic(api_key=self.api_key)
def extract_repo_profile(self, repo_data: dict) -> dict:
prompt = f"""Analyze this GitHub repository and extract a structured profile:
Repository: {repo_data.get('full_name', '')}
Description: {repo_data.get('description', '')}
Languages: {repo_data.get('languages', {})}
README: {repo_data.get('readme', '')[:3000]}
Extract:
1. Main features (3-5 key capabilities)
2. Technical stack (frameworks, tools, dependencies)
3. Primary use cases
4. Key directories and their purposes
5. Quickstart path if available
Return as JSON with this structure:
{{
"mainFeatures": string[],
"technicalStack": string[],
"useCases": string[],
"keyDirectories": string[],
"quickstartPath": string | null
}}"""
try:
response = self.client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=2048,
messages=[{"role": "user", "content": prompt}],
)
import json
text = response.content[0].text
# Try to extract JSON from the response
try:
return json.loads(text)
except json.JSONDecodeError:
# Try to find JSON in the response
import re
json_match = re.search(r"\{[\s\S]*\}", text)
if json_match:
return json.loads(json_match.group())
return self._extract_profile_fallback(repo_data)
except Exception as e:
print(f"Claude API error: {e}")
return self._extract_profile_fallback(repo_data)
def _extract_profile_fallback(self, repo_data: dict) -> dict:
return {
"mainFeatures": [
repo_data.get("description", "No description available"),
"See README for details",
],
"technicalStack": list(repo_data.get("languages", {}).keys())[:5],
"useCases": ["General development"],
"keyDirectories": repo_data.get("structure", {}).get("keyDirectories", []),
"quickstartPath": None,
}
def generate_content_variants(
self, profile: RepoProfile, format: str, variant_count: int = 2
) -> list[ContentVariant]:
prompt = self._get_prompt_for_format(format, profile)
variants = []
try:
for i in range(variant_count):
response = self.client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=4096,
temperature=0.7 + (i * 0.1),
messages=[{"role": "user", "content": prompt}],
)
content = response.content[0].text
variants.append(
ContentVariant(
id=f"{format}-variant-{i + 1}",
format=format,
content=content,
)
)
except Exception as e:
print(f"Claude generation error: {e}")
variants.append(
ContentVariant(
id=f"{format}-variant-1",
format=format,
content=self._generate_fallback_content(format, profile),
)
)
return variants
def _generate_fallback_content(self, format: str, profile: RepoProfile) -> str:
repo_name = profile.name
description = profile.description
if format == "tutorial":
return f"""# Getting Started with {repo_name}
## Overview
{description}
## Prerequisites
- Git installed
- Basic knowledge of {profile.primary_language}
## Installation
```bash
git clone {profile.url}
cd {repo_name}
# Follow project-specific setup instructions
```
## Next Steps
Check the README for detailed documentation.
*Note: This is a fallback tutorial. For AI-generated content, please add valid ANTHROPIC_API_KEY.*"""
elif format == "blog":
features = "\n".join([f"- {f}" for f in profile.main_features])
return f"""# Introducing {repo_name}
{description}
## Key Features
{features}
## Tech Stack
Built with {', '.join(profile.technical_stack)}
## Get Started
Visit the repository: {profile.url}
*Note: This is a fallback blog post. For AI-generated content, please add valid ANTHROPIC_API_KEY.*"""
elif format == "talk":
return f"""# {repo_name}: Conference Talk Outline
## Title
"Introduction to {repo_name}"
## Abstract
{description}
## Slides (30-45 minutes)
1. Introduction & Background
2. Problem Statement
3. Solution Overview
4. Key Features
5. Live Demo
6. Q&A
*Note: This is a fallback talk outline. For AI-generated content, please add valid ANTHROPIC_API_KEY.*"""
elif format == "twitter":
return f"""πŸš€ Check out {repo_name}!
{description}
Built with {profile.primary_language}
⭐ {profile.stars} stars
Learn more: {profile.url}
#OpenSource #{profile.primary_language}
*Note: This is a fallback tweet. For AI-generated content, please add valid ANTHROPIC_API_KEY.*"""
elif format == "linkedin":
features = "\n".join([f"β€’ {f}" for f in profile.main_features])
return f"""I'm excited to share {repo_name} with the community.
{description}
Key highlights:
{features}
Check it out: {profile.url}
*Note: This is a fallback LinkedIn post. For AI-generated content, please add valid ANTHROPIC_API_KEY.*"""
elif format == "hackathon":
return f"""# Hackathon Challenge: Build with {repo_name}
## Challenge Description
Create an innovative project using {repo_name}
## Requirements
- Use {profile.primary_language}
- Implement at least one key feature
- Document your approach
## Judging Criteria
- Innovation
- Technical implementation
- Code quality
- Presentation
*Note: This is a fallback challenge. For AI-generated content, please add valid ANTHROPIC_API_KEY.*"""
return f"Content for {format} format.\n\n*Note: Add ANTHROPIC_API_KEY for AI-generated content.*"
def _get_prompt_for_format(self, format: str, profile: RepoProfile) -> str:
commits_list = profile.commits[:5] if profile.commits else []
recent_commits = ""
if commits_list:
commits_text = "\n".join(
[
f"- {c.get('date', '')[:10]}: {c.get('message', '').split(chr(10))[0]}"
for c in commits_list
]
)
recent_commits = f"\nRecent Commits (most recent first):\n{commits_text}"
# Add code diffs context
code_diffs = ""
if profile.commits_with_diffs:
diff_summaries = []
for commit in profile.commits_with_diffs[:3]:
diff = commit.get("diff", "")
if diff:
# Truncate each diff to keep context manageable
diff_preview = diff[:2000] + "..." if len(diff) > 2000 else diff
diff_summaries.append(f"### {commit.get('message', 'No message')}\n```diff\n{diff_preview}\n```")
if diff_summaries:
code_diffs = f"\n\nCode Changes (diffs from recent commits):\n" + "\n\n".join(diff_summaries)
# Add breaking changes context
breaking_changes_ctx = ""
if profile.breaking_changes:
bc_items = []
for bc in profile.breaking_changes[:5]:
changes_desc = ", ".join([f"{c['type']} ({c['count']})" for c in bc.get("changes", [])])
bc_items.append(f"- [{bc.get('sha', '')}] {bc.get('message', '')}: {changes_desc}")
if bc_items:
breaking_changes_ctx = f"\n\n⚠️ POTENTIAL BREAKING CHANGES DETECTED:\n" + "\n".join(bc_items)
breaking_changes_ctx += "\nIMPORTANT: If breaking changes exist, you MUST include migration guidance in the content."
# Add README changes context (highest priority)
readme_changes_ctx = ""
if profile.has_readme_changes and profile.readme_diff:
readme_changes_ctx = f"\n\nπŸ“ README CHANGES DETECTED (HIGHEST PRIORITY):\nThe README documentation has been updated during the analysis period. These changes likely reflect important API changes, new features, or usage updates that MUST be prominently featured:\n\n{profile.readme_diff[:3000]}"
# Add initial README context for project understanding
initial_readme_ctx = ""
if profile.initial_readme:
initial_readme_ctx = f"\n\nProject Background (from initial README):\n{profile.initial_readme[:2000]}"
base_context = f"""Repository: {profile.name}
Description: {profile.description}
Main Features: {', '.join(profile.main_features)}
Tech Stack: {', '.join(profile.technical_stack)}
Primary Language: {profile.primary_language}{recent_commits}{code_diffs}{breaking_changes_ctx}{readme_changes_ctx}{initial_readme_ctx}
Note: Focus content on changes within the selected analysis period when relevant."""
prompts = {
"tutorial": f"""{base_context}
Create a step-by-step tutorial showcasing the LATEST updates and new features from recent commits.
IMPORTANT:
- Start with a brief 2-3 sentence project introduction
- Focus 80% of content on recent updates, new features, and changes from the commit history
- Highlight what's NEW and DIFFERENT compared to older versions
- Include working code examples demonstrating recent features
- If no recent commits, focus on the most advanced/latest capabilities
Include:
- Brief project overview (2-3 sentences)
- What's new in recent updates (primary focus)
- Step-by-step guide to using new features
- Code examples showcasing latest additions
- Migration tips if APIs changed
- Next steps with new features
Make it executable and accurate. Use markdown format.""",
"blog": f"""{base_context}
Write a technical blog post announcing the LATEST updates and new features.
IMPORTANT:
- Focus on recent changes and what's NEW (based on recent commits)
- Brief context about the project (1 paragraph max)
- Deep dive into latest features and improvements (70-80% of content)
- Show before/after comparisons if APIs changed
- Highlight breaking changes or major improvements
Include:
- Hook: What's new and why it matters NOW
- Brief project context (1 paragraph)
- Deep dive: Latest features and updates (primary focus)
- Code examples showcasing new capabilities
- Migration guide if needed
- What's coming next
- Call to action
Target audience: Technical developers. Use markdown format.""",
"talk": f"""{base_context}
Create a 30-45 minute meetup/conference talk outline focused on LATEST updates and new features.
IMPORTANT:
- This is for a meetup/conference - focus on what's NEW and exciting
- Brief intro (2-3 slides max), then dive into recent updates
- 70-80% of talk should cover latest features, improvements, and changes
- Include live demos of new capabilities
- Show real code from recent commits
Include:
- Talk title emphasizing "What's New" or "Latest Updates"
- Abstract highlighting recent changes (150 words)
- Slide structure (15-20 slides):
* Quick intro (2-3 slides)
* Recent updates deep dive (10-12 slides)
* Live demos of new features (3-4 slides)
* Roadmap and what's next (1-2 slides)
- Speaker notes focusing on recent changes
- Demo points showcasing latest features
- Q&A prep about new capabilities
Format as markdown with clear sections.""",
"twitter": f"""{base_context}
Write an engaging Twitter/X thread (5-7 tweets) announcing the LATEST updates.
IMPORTANT:
- Focus on what's NEW (recent commits/features)
- First tweet: Brief intro + "Here's what's new 🧡"
- Next 3-5 tweets: Deep dive into specific new features
- Last tweet: Call to action
Make it:
- Start with "What's new in [project]" hook
- Highlight recent features/changes (use commit history)
- Technical but accessible
- Add relevant emojis (πŸš€ ⚑ πŸŽ‰ ✨)
- Include code snippets for new features
- End with link and CTA
- Each tweet under 280 chars
Format as numbered thread.""",
"linkedin": f"""{base_context}
Write a LinkedIn post announcing LATEST updates for a technical leadership audience.
IMPORTANT:
- Focus on recent updates and what's new
- Frame updates in terms of business value and impact
- Brief context, then dive into latest improvements
Include:
- Hook: Major update/new release announcement
- Brief project context (1-2 sentences)
- What's new: Recent features and improvements (primary focus)
- Business value of new updates
- Technical highlights of latest additions
- Team/community impact
- Call to action
Tone: Professional, insightful, forward-looking. 200-300 words.""",
"hackathon": f"""{base_context}
Design a hackathon challenge focused on exploring and using the LATEST features.
IMPORTANT:
- Challenge should require using recent updates/new features
- Encourage participants to push boundaries of new capabilities
- Provide recent code examples as starting points
Include:
- Challenge title emphasizing "Latest Features" or "New Capabilities"
- Brief project intro (2-3 sentences)
- Problem statement leveraging new features
- Required deliverables (must use recent updates)
- Judging criteria (4-5 points, bonus for creative use of new features)
- Starter resources with recent code examples
- Sample tasks exploring latest additions (3-5)
- Difficulty level
Make it engaging and achievable in 24-48 hours.""",
}
return prompts.get(
format, f"{base_context}\n\nCreate content for {format} format."
)