Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions core/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
BlogPostTitleSuggestion,
EmailSent,
GeneratedBlogPost,
Inspiration,
Profile,
Project,
ProjectPage,
Expand Down Expand Up @@ -80,3 +81,4 @@ class ReferrerBannerAdmin(admin.ModelAdmin):
admin.site.register(AutoSubmissionSetting)
admin.site.register(ProjectPage)
admin.site.register(EmailSent)
admin.site.register(Inspiration)
35 changes: 35 additions & 0 deletions core/agent_system_prompts.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,41 @@ def add_target_keywords(ctx: RunContext[BlogPostGenerationContext]) -> str:
return ""


def add_inspirations(ctx: RunContext[BlogPostGenerationContext]) -> str:
if not ctx.deps.inspirations:
return ""

inspiration_sections = []
inspiration_sections.append(
"""
INSPIRATION SOURCES:
The user has shared the following blog posts and blogs as inspiration sources
they would like to emulate in style, tone, and structure.
Use these as reference points when generating content:
"""
)

for inspiration in ctx.deps.inspirations:
inspiration_sections.append(
f"""
- Title: {inspiration.title}
URL: {inspiration.url}
Description: {inspiration.description or 'No description provided'}
"""
)

inspiration_sections.append(
"""
IMPORTANT: Draw inspiration from these sources' writing style, tone, structure,
and approach while creating content that is unique and appropriate for the current project.
Consider how these inspirations structure their content, engage their audience,
and present information.
"""
)

return "\n".join(inspiration_sections)
Comment on lines +156 to +188
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Code duplication: Similar function exists in core/models.py.

This add_inspirations function is nearly identical to the one in core/models.py (lines 634-663), with only minor differences in the prompt text. Both functions:

  • Accept a RunContext with inspirations
  • Return empty string if no inspirations
  • Build the same markdown structure
  • Use the same format string for each inspiration

Consider extracting a shared utility function to avoid duplication:

def format_inspirations_prompt(inspirations: list[InspirationContext], context_type: str = "content") -> str:
    """Format inspirations into a prompt section.
    
    Args:
        inspirations: List of inspiration contexts
        context_type: Either "content" or "titles" to customize the prompt
    """
    if not inspirations:
        return ""

    sections = [
        """
        INSPIRATION SOURCES:
        The user has shared the following blog posts and blogs as inspiration sources
        they would like to emulate in style, tone, and structure.
        Use these as reference points when generating """ + context_type + """:
        """
    ]

    for inspiration in inspirations:
        sections.append(
            f"""
            - Title: {inspiration.title}
              URL: {inspiration.url}
              Description: {inspiration.description or 'No description provided'}
            """
        )

    sections.append(
        """
        IMPORTANT: Draw inspiration from these sources' writing style, tone, structure,
        and approach while creating """ + context_type + """ that are unique and appropriate for the current project.
        """
    )

    return "\n".join(sections)


def add_inspirations(ctx: RunContext[BlogPostGenerationContext]) -> str:
    return format_inspirations_prompt(ctx.deps.inspirations, "content")

Then update the similar function in core/models.py to use the same utility.

🤖 Prompt for AI Agents
In core/agent_system_prompts.py around lines 156-188, the add_inspirations
function duplicates logic present in core/models.py (lines ~634-663); extract a
single shared helper (e.g., format_inspirations_prompt) that accepts the
inspirations list and a context_type ("content" vs "titles") and returns the
formatted string, replace the local add_inspirations to call that helper, and
update the similar function in core/models.py to call the same helper so both
modules reuse the shared implementation.



def add_webpage_content(ctx: RunContext[WebPageContent]) -> str:
return (
"Web page content:"
Expand Down
2 changes: 2 additions & 0 deletions core/agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from pydantic_ai.providers.openai import OpenAIProvider

from core.agent_system_prompts import (
add_inspirations,
add_language_specification,
add_project_details,
add_project_pages,
Expand Down Expand Up @@ -51,6 +52,7 @@ def only_return_the_edited_content() -> str:
content_editor_agent.system_prompt(add_title_details)
content_editor_agent.system_prompt(add_language_specification)
content_editor_agent.system_prompt(add_target_keywords)
content_editor_agent.system_prompt(add_inspirations)


########################################################
Expand Down
22 changes: 22 additions & 0 deletions core/api/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,3 +262,25 @@ class ToggleProjectPageAlwaysUseOut(Schema):
status: str
always_use: bool
message: str | None = None


class AddInspirationIn(Schema):
project_id: int
url: str


class AddInspirationOut(Schema):
status: str
message: str | None = None
inspiration_id: int | None = None
title: str | None = None
url: str | None = None


class DeleteInspirationIn(Schema):
inspiration_id: int


class DeleteInspirationOut(Schema):
status: str
message: str | None = None
86 changes: 86 additions & 0 deletions core/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,16 @@
from core.api.auth import session_auth, superuser_api_auth
from core.api.schemas import (
AddCompetitorIn,
AddInspirationIn,
AddInspirationOut,
AddKeywordIn,
AddKeywordOut,
AddPricingPageIn,
BlogPostIn,
BlogPostOut,
CompetitorAnalysisOut,
DeleteInspirationIn,
DeleteInspirationOut,
DeleteProjectKeywordIn,
DeleteProjectKeywordOut,
FixGeneratedBlogPostIn,
Expand Down Expand Up @@ -52,6 +56,7 @@
Competitor,
Feedback,
GeneratedBlogPost,
Inspiration,
Keyword,
Project,
ProjectKeyword,
Expand Down Expand Up @@ -1056,3 +1061,84 @@ def toggle_project_page_always_use(request: HttpRequest, data: ToggleProjectPage
"always_use": False,
"message": f"Failed to toggle always use: {str(error)}",
}


@api.post("/inspirations/add", response=AddInspirationOut, auth=[session_auth])
def add_inspiration(request: HttpRequest, data: AddInspirationIn):
"""Add a new inspiration link to a project."""
profile = request.auth
project = get_object_or_404(Project, id=data.project_id, profile=profile)

url_to_add = data.url.strip()

if not url_to_add:
return {"status": "error", "message": "URL cannot be empty"}

if not url_to_add.startswith(("http://", "https://")):
return {"status": "error", "message": "URL must start with http:// or https://"}

try:
if Inspiration.objects.filter(project=project, url=url_to_add).exists():
return {"status": "error", "message": "This inspiration already exists for your project"}

inspiration = Inspiration.objects.create(project=project, url=url_to_add)

inspiration.get_page_content()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: calling get_page_content() in the request-response cycle can cause timeout issues as it makes external HTTP requests. Consider moving this to a background task using django-q2

Suggested change
inspiration.get_page_content()
async_task("core.tasks.fetch_inspiration_content", inspiration.id)
Prompt To Fix With AI
This is a comment left during a code review.
Path: core/api/views.py
Line: 1086:1086

Comment:
**logic:** calling `get_page_content()` in the request-response cycle can cause timeout issues as it makes external HTTP requests. Consider moving this to a background task using django-q2

```suggestion
        async_task("core.tasks.fetch_inspiration_content", inspiration.id)
```

How can I resolve this? If you propose a fix, please make it concise.


logger.info(
"[Add Inspiration] Successfully added inspiration",
inspiration_id=inspiration.id,
project_id=project.id,
url=url_to_add,
)

return {
"status": "success",
"inspiration_id": inspiration.id,
"title": inspiration.title,
"url": inspiration.url,
"message": "Inspiration added successfully!",
}

except Exception as e:
logger.error(
"Failed to add inspiration",
error=str(e),
exc_info=True,
project_id=project.id,
url=url_to_add,
)
return {"status": "error", "message": f"An unexpected error occurred: {str(e)}"}


@api.post("/inspirations/delete", response=DeleteInspirationOut, auth=[session_auth])
def delete_inspiration(request: HttpRequest, data: DeleteInspirationIn):
"""Delete an inspiration from a project."""
profile = request.auth

try:
inspiration = get_object_or_404(
Inspiration, id=data.inspiration_id, project__profile=profile
)

inspiration_url = inspiration.url
inspiration.delete()

logger.info(
"[Delete Inspiration] Successfully deleted inspiration",
inspiration_id=data.inspiration_id,
url=inspiration_url,
profile_id=profile.id,
)

return {"status": "success", "message": f"Inspiration deleted successfully"}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return {"status": "success", "message": f"Inspiration deleted successfully"}
return {"status": "success", "message": "Inspiration deleted successfully"}

f-string is unnecessary here. This can just be a string. Read more.


except Exception as e:
logger.error(
"Failed to delete inspiration",
error=str(e),
exc_info=True,
inspiration_id=data.inspiration_id,
profile_id=profile.id,
)
return {"status": "error", "message": f"Failed to delete inspiration: {str(e)}"}
Loading