Skip to content

Commit

Permalink
Merge pull request #1 from rasulkireev/analyze-project
Browse files Browse the repository at this point in the history
Analyze project
  • Loading branch information
rasulkireev authored Nov 19, 2024
2 parents 412791a + eec9ca4 commit b100492
Show file tree
Hide file tree
Showing 47 changed files with 2,657 additions and 1,372 deletions.
2 changes: 1 addition & 1 deletion .flake8
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
[flake8]
max-line-length=120
max-line-length=120
2 changes: 1 addition & 1 deletion .github/workflows/deploy-workers.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ on:
- main

env:
PROJECT_NAME: seo_blog_bot
PROJECT_NAME: seo-blog-bot

jobs:
build-and-deploy:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ on:
- main

env:
PROJECT_NAME: seo_blog_bot
PROJECT_NAME: seo-blog-bot

jobs:
build-and-deploy:
Expand Down
2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,5 @@ test-webhook:
stripe-sync:
docker compose run --rm backend python ./manage.py djstripe_sync_models Product Price

restart-worker:
docker compose up -d workers --force-recreate
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,14 @@ The following notes are applicable only after you got the app running locally vi
## Deployment

1. Create 4 apps on CapRover.
- `seo_blog_bot`
- `seo_blog_bot-workers`
- `seo_blog_bot-postgres`
- `seo_blog_bot-redis`
- `seo-blog-bot`
- `seo-blog-bot-workers`
- `seo-blog-bot-postgres`
- `seo-blog-bot-redis`

2. Create a new CapRover app token for:
- `seo_blog_bot`
- `seo_blog_bot-workers`
- `seo-blog-bot`
- `seo-blog-bot-workers`

3. Add Environment Variables to those same apps from `.env`.

Expand Down
5 changes: 2 additions & 3 deletions core/admin.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@

from django.contrib import admin

from core.models import BlogPost
from core.models import BlogPost, Project

admin.site.register(BlogPost)

admin.site.register(Project)
32 changes: 32 additions & 0 deletions core/api/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from typing import Optional

from django.http import HttpRequest
from ninja.security import HttpBearer

from core.models import Profile


class MultipleAuthSchema(HttpBearer):
def authenticate(self, request: HttpRequest, token: Optional[str] = None) -> Optional[Profile]:
# For session-based authentication (when using the web interface)
if hasattr(request, "user") and request.user.is_authenticated:
try:
return request.user.profile
except Profile.DoesNotExist:
return None

# For API token authentication (when using the API directly)
if token:
try:
return Profile.objects.get(api_key=token)
except Profile.DoesNotExist:
return None

return None

def __call__(self, request):
# Override to make authentication optional for session-based requests
if hasattr(request, "user") and request.user.is_authenticated:
return self.authenticate(request)

return super().__call__(request)
35 changes: 35 additions & 0 deletions core/api/schemas.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from ninja import Schema


class ProjectScanIn(Schema):
url: str


class ProjectScanOut(Schema):
project_id: int
name: str = ""
type: str = ""
url: str
summary: str = ""


class GenerateTitleSuggestionsIn(Schema):
project_id: int


class TitleSuggestionOut(Schema):
category: str
title: str
description: str


class GenerateTitleSuggestionsOut(Schema):
suggestions: list[TitleSuggestionOut]


class GeneratedContentOut(Schema):
status: str
content: str
slug: str
tags: str
description: str
230 changes: 230 additions & 0 deletions core/api/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
import json
import re

import anthropic
from django.conf import settings
from django.http import HttpRequest
from django.shortcuts import get_object_or_404
from django.utils.text import slugify
from ninja import NinjaAPI

from core.api.auth import MultipleAuthSchema
from core.api.schemas import (
GeneratedContentOut,
GenerateTitleSuggestionsIn,
GenerateTitleSuggestionsOut,
ProjectScanIn,
ProjectScanOut,
)
from core.models import BlogPostTitleSuggestion, GeneratedBlogPost, Project
from core.utils import generate_blog_titles_with_claude, process_project_url, save_blog_titles
from seo_blog_bot.utils import get_seo_blog_bot_logger

logger = get_seo_blog_bot_logger(__name__)

api = NinjaAPI(auth=MultipleAuthSchema(), csrf=True) # Enable CSRF protection


@api.post("/scan", response=ProjectScanOut)
def scan_project(request: HttpRequest, data: ProjectScanIn):
profile = request.auth

# Check if project already exists for this user
project = Project.objects.filter(profile=profile, url=data.url).first()

if project:
return {
"project_id": project.id,
"has_details": bool(project.name),
"has_suggestions": project.blog_post_title_suggestions.exists(),
}

# Create new project
project = Project.objects.create(profile=profile, url=data.url)

try:
# Process the URL synchronously
info = process_project_url(data.url)

type_mapping = {choice[1]: choice[0] for choice in Project.Type.choices}
project_type = type_mapping.get(info.get("type", ""), Project.Type.SAAS)

# Update project with processed information
project.name = info.get("name", "")
project.type = project_type
project.summary = info.get("summary", "")
project.blog_theme = info.get("blog_theme", "")
project.founders = info.get("founders", "")
project.key_features = info.get("key_features", "")
project.target_audience_summary = info.get("target_audience_summary", "")
project.pain_points = info.get("pain_points", "")
project.product_usage = info.get("product_usage", "")
project.save()

except Exception as e:
logger.error("Error processing project", error=str(e), project_id=project.id)
project.delete()
raise ValueError(f"Error processing URL: {str(e)}")

return {
"project_id": project.id,
"name": project.name,
"type": project.get_type_display(),
"url": project.url,
"summary": project.summary,
}


@api.post("/generate-title-suggestions", response=GenerateTitleSuggestionsOut)
def generate_title_suggestions(request: HttpRequest, data: GenerateTitleSuggestionsIn):
profile = request.auth

# Get project and verify ownership
project = get_object_or_404(Project, id=data.project_id, profile=profile)

# Prepare project data
project_data = {
"name": project.name,
"type": project.type,
"summary": project.summary,
"blog_theme": project.blog_theme,
"key_features": project.key_features,
"target_audience_summary": project.target_audience_summary,
"pain_points": project.pain_points,
"product_usage": project.product_usage,
}

try:
# Generate titles
titles = generate_blog_titles_with_claude(project_data)

# Save titles to database
save_blog_titles(project.id, titles)

return {"suggestions": titles}

except Exception as e:
logger.error("Error generating title suggestions", error=str(e), project_id=project.id)
raise ValueError(f"Error generating title suggestions: {str(e)}")


@api.post("/generate-blog-content/{suggestion_id}", response=GeneratedContentOut)
def generate_blog_content(request: HttpRequest, suggestion_id: int):
suggestion = get_object_or_404(BlogPostTitleSuggestion, id=suggestion_id, project__profile=request.auth)

# Create a placeholder GeneratedBlogPost
generated_post = GeneratedBlogPost.objects.create(
project=suggestion.project,
title=suggestion,
slug=slugify(suggestion.title),
description=suggestion.description,
tags="",
content="",
)

try:
claude = anthropic.Client(api_key=settings.ANTHROPIC_API_KEY)

prompt = f"""You are an experienced online writer for {suggestion.project.name}, a {suggestion.project.type} platform. You understand both the art of capturing attention and the specific needs of our target audience: {suggestion.project.target_audience_summary}
Your task is to generate a blog post and return it in the following JSON format. Ensure the JSON is properly escaped and contains no control characters or line breaks within field values:
{{
"description": "A single-line meta description (150-160 characters)",
"slug": "url-friendly-version-of-title",
"tags": "Tag 1, Tag 2, Tag 3, Tag 4, Tag 5",
"content": "The full blog post content in Markdown"
}}
Context for content generation:
- Platform: {suggestion.project.name} ({suggestion.project.type})
- Key features: {suggestion.project.key_features}
- Pain points addressed: {suggestion.project.pain_points}
- Target audience: {suggestion.project.target_audience_summary}
- Usage patterns: {suggestion.project.product_usage}
- Blog theme: {suggestion.project.blog_theme}
For the given title '{suggestion.title}', please create:
1. Description:
- Write a compelling 150-160 character meta description
- Focus on value proposition and SEO
- Single line, no line breaks
2. Slug:
- Convert title to URL-friendly format
- Use lowercase letters, numbers, and hyphens only
- Remove special characters and spaces
3. Tags:
- Generate 5-8 relevant keywords
- Comma-separated, no spaces
- Relevant to {suggestion.project.type} industry
- Include general and specific terms
4. Content:
- Full blog post in Markdown format
- Follow the structure:
* Strong opening hook
* 3-5 main points with examples
* Clear conclusion with call-to-action
- Maintain professional tone for {suggestion.project.type} sector
- Address target audience pain points
- Reference key features where relevant
- Optimize for online readability
IMPORTANT: Ensure the response is a valid JSON object. All string values must be properly escaped. Do not include line breaks within JSON field values except in the "content" field where Markdown formatting is used."""

message = claude.messages.create(
model="claude-3-5-sonnet-latest",
max_tokens=8000,
temperature=0.7,
messages=[{"role": "user", "content": prompt}],
)

# Clean and parse the response
response_text = message.content[0].text.strip()
response_text = "".join(char for char in response_text if ord(char) >= 32 or char in "\n\r\t")

# Extract JSON content
json_match = re.search(r"\{[\s\S]*\}", response_text)
if json_match:
response_text = json_match.group(0)

# Parse the cleaned JSON
try:
response_json = json.loads(response_text)
except json.JSONDecodeError:
response_text = response_text.replace("\n", "\\n").replace("\r", "\\r")
response_json = json.loads(response_text)

required_fields = ["description", "slug", "tags", "content"]
missing_fields = [field for field in required_fields if field not in response_json]
if missing_fields:
raise ValueError(f"Missing required fields: {', '.join(missing_fields)}")

# Update the generated post
generated_post.description = response_json["description"]
generated_post.slug = response_json["slug"]
generated_post.tags = response_json["tags"]
generated_post.content = response_json["content"]
generated_post.save()

return {
"status": "success",
"content": response_json["content"],
"slug": response_json["slug"],
"tags": response_json["tags"],
"description": response_json["description"],
}

except Exception as e:
logger.error(
"Failed to generate blog content",
error=str(e),
post_id=generated_post.id,
title=suggestion.title,
project_id=suggestion.project.id,
)
generated_post.delete()
raise ValueError(f"Failed to generate content: {str(e)}")
8 changes: 3 additions & 5 deletions core/apps.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import posthog

from django.conf import settings
from django.apps import AppConfig
from django.conf import settings

from seo_blog_bot.utils import get_seo_blog_bot_logger

logger = get_seo_blog_bot_logger(__name__)
Expand All @@ -13,10 +13,8 @@ class CoreConfig(AppConfig):

def ready(self):
import core.signals # noqa
import core.webhooks # noqa

import core.webhooks # noqa

if settings.ENVIRONMENT == "prod":
posthog.api_key = settings.POSTHOG_API_KEY
posthog.host = "https://us.i.posthog.com"

2 changes: 1 addition & 1 deletion core/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,4 @@ def save(self, commit=True):
class ProjectScanForm(forms.ModelForm):
class Meta:
model = Project
fields = ['url']
fields = ["url"]
Loading

0 comments on commit b100492

Please sign in to comment.