-
Notifications
You must be signed in to change notification settings - Fork 8
Add competitor vs. post title generation #105
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add competitor vs. post title generation #105
Conversation
Co-authored-by: me <me@rasulkireev.com>
|
Cursor Agent can help with this pull request. Just |
|
Caution Review failedThe pull request is closed. Note Other AI code review bot(s) detectedCodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review. WalkthroughAdds competitor-focused content generation: new Perplexity chat agents and prompts, Competitor.blog_post storage and generation, profile quota fields and checks, API endpoint to generate VS titles/posts, frontend UI/controllers and templates for competitor management, a DB migration, and dependency upgrades for pydantic-ai. Changes
Sequence Diagram(s)sequenceDiagram
participant User as Frontend (User)
participant API as API Server
participant DB as Database
participant Agent as Agent Layer
participant Perplexity as Perplexity Sonar
User->>API: POST /generate-competitor-vs-title { competitor_id }
API->>DB: Validate competitor & project ownership
API->>API: Check profile limits (competitor_posts_limit, blog_posts_limit)
API->>Agent: generate_vs_blog_post(context via add_competitor_vs_context)
Agent->>Perplexity: Invoke chat-model (markdown_lists + project pages)
Perplexity-->>Agent: Returns title and markdown blog post
Agent->>DB: Persist Competitor.blog_post (and any title suggestion)
API-->>User: 200 OK { status: "success", competitor_id }
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes
Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (2 warnings)
✅ Passed checks (1 passed)
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (2)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…r/add-competitor-vs-post-title-generation-63a8
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Greptile Overview
Greptile Summary
Adds competitor comparison blog post title generation feature using Perplexity Sonar AI model.
Key Changes:
- Created new
competitor_vs_title_agentincore/agents.pythat uses Perplexity Sonar API for web-search enabled title generation - Added
generate_vs_title()method toCompetitormodel for generating comparison titles - Implemented
/generate-competitor-vs-titleAPI endpoint with authentication, permission checks, and quota limits - Added
VS_COMPETITORcontent type to categorize comparison blog posts - Created comprehensive system prompts for competitive comparison content generation and writing
- Added
CompetitorVsTitleContextschema to structure agent input data
Implementation Quality:
- Follows existing patterns in the codebase (agent structure, API endpoint design, error handling)
- Proper authentication and authorization checks in place
- Uses async task queue for keyword processing
- Comprehensive error logging with structured logging
- Clean separation of concerns across layers (agents, models, views, schemas)
Confidence Score: 5/5
- This PR is safe to merge with minimal risk
- The implementation follows established patterns in the codebase, includes proper authentication/authorization, error handling, and logging. Only minor style improvement suggested (unused import). The changes are well-structured and consistent with the existing architecture.
- No files require special attention
Important Files Changed
File Analysis
| Filename | Score | Overview |
|---|---|---|
| core/agents.py | 4/5 | Added new competitor_vs_title_agent using Perplexity Sonar model for generating competitor comparison titles with custom system prompts |
| core/models.py | 5/5 | Added generate_vs_title() method to Competitor model that generates comparison blog post titles using the new agent |
| core/api/views.py | 5/5 | Added /generate-competitor-vs-title API endpoint with proper authentication, permissions, and error handling |
Sequence Diagram
sequenceDiagram
participant User
participant API as API View<br/>(generate_competitor_vs_title)
participant Competitor as Competitor Model
participant Agent as competitor_vs_title_agent
participant Perplexity as Perplexity Sonar API
participant DB as Database
User->>API: POST /generate-competitor-vs-title<br/>{competitor_id}
API->>API: Authenticate user<br/>(session_auth)
API->>DB: Get Competitor by ID
DB-->>API: Return Competitor
API->>API: Check permissions<br/>(project.profile == profile)
API->>API: Check generation limits<br/>(can_generate_title_suggestions)
API->>Competitor: generate_vs_title()
Competitor->>Competitor: Create CompetitorVsTitleContext<br/>(project_details, competitor_details)
Competitor->>Agent: run_agent_synchronously()<br/>with context
Agent->>Perplexity: Request title generation<br/>with web search capability
Perplexity-->>Agent: Return TitleSuggestion
Agent-->>Competitor: Return result
Competitor->>DB: Create BlogPostTitleSuggestion<br/>(VS_COMPETITOR type)
DB-->>Competitor: Return title_suggestion
Competitor->>DB: async_task: save_title_suggestion_keywords
Competitor-->>API: Return title_suggestion
API->>API: render_to_string()<br/>(title_suggestion_card.html)
API-->>User: Return success response<br/>with suggestion + HTML
7 files reviewed, 1 comment
core/agents.py
Outdated
|
|
||
| @competitor_vs_title_agent.system_prompt | ||
| def add_competitor_vs_context(ctx): | ||
| from pydantic_ai import RunContext |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
style: RunContext is imported but never used in the function
| from pydantic_ai import RunContext | |
| def add_competitor_vs_context(ctx): |
Prompt To Fix With AI
This is a comment left during a code review.
Path: core/agents.py
Line: 121:121
Comment:
**style:** `RunContext` is imported but never used in the function
```suggestion
def add_competitor_vs_context(ctx):
```
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
core/agents.py (1)
1-115: Prevent import-time crash when Perplexity API key is absentAccessing
settings.PERPLEXITY_API_KEYat import time will raiseAttributeError(and break the module) whenever the key isn’t configured. Add a strict check and fail fast with a clear error before constructing the provider.+from django.core.exceptions import ImproperlyConfigured @@ -competitor_vs_title_agent = Agent( - OpenAIModel( - AIModel.PERPLEXITY_SONAR, - provider=OpenAIProvider( - base_url="https://api.perplexity.ai", - api_key=settings.PERPLEXITY_API_KEY, - ), - ), +perplexity_api_key = getattr(settings, "PERPLEXITY_API_KEY", "") +if not perplexity_api_key: + raise ImproperlyConfigured( + "PERPLEXITY_API_KEY must be configured to use competitor vs. title generation." + ) + +competitor_vs_title_agent = Agent( + OpenAIModel( + AIModel.PERPLEXITY_SONAR, + provider=OpenAIProvider( + base_url="https://api.perplexity.ai", + api_key=perplexity_api_key, + ), + ),
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (7)
core/agents.py(3 hunks)core/api/schemas.py(1 hunks)core/api/views.py(2 hunks)core/choices.py(1 hunks)core/models.py(1 hunks)core/prompts.py(2 hunks)core/schemas.py(1 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
**/*.py
📄 CodeRabbit inference engine (.cursor/rules/backend.mdc)
**/*.py: Prioritize readability and maintainability; follow PEP 8
Use descriptive variable/function names with underscoresIn Python, use try-except blocks that catch specific exception types; do not catch broad Exception.
**/*.py: Follow PEP 8 for Python code style
Use descriptive variable names with underscores (snake_case) in Python
Prefer Django's built-in features over external libraries
**/*.py: Use structlog for logging in Python code (avoid print and the standard logging module)
Include contextual key-value fields in log messages (e.g., error=str(e), exc_info=True, profile_id=profile.id)
**/*.py: Use descriptive, full-word variable names; avoid abbreviations and single-letter variables in Python
Provide context in variable names when format or type matters (e.g., include_iso_format,date)
Extract unchanging values into UPPER_CASE constants
Use intermediary variables to name parsed groups instead of using index access directly
Naming conventions: use is_/has_/can_ for booleans; include 'date' for dates; snake_case for variables/functions; PascalCase for classes
Define variables close to where they are used to keep lifespan short
Name things after what they do, not how they're used; ensure names make sense without extra context
Avoid generic names like data, info, manager; use specific, intention-revealing names
Function names should include necessary context without being verbose
If naming is hard, split functions into smaller focused parts
Maintain consistency: reuse the same verbs and nouns for the same concepts; name variables after the functions that create them
Use more descriptive names for longer-lived variables
Avoid else statements by using guard clauses for early returns
Replace simple conditionals with direct assignment when both branches call the same function with different values
Use dictionaries as dispatch tables instead of multiple equal-probability elif chains
Validate input before processing to prevent errors propagating in...
Files:
core/schemas.pycore/choices.pycore/api/schemas.pycore/agents.pycore/prompts.pycore/api/views.pycore/models.py
core/**/*.py
📄 CodeRabbit inference engine (.cursor/rules/backend.mdc)
Leverage Django's ORM; avoid raw SQL when possible
Files:
core/schemas.pycore/choices.pycore/api/schemas.pycore/agents.pycore/prompts.pycore/api/views.pycore/models.py
core/api/**/*.py
📄 CodeRabbit inference engine (.cursor/rules/backend.mdc)
core/api/**/*.py: django-ninja generate openapi documentation, so make sure to populate views with relevant data in order for it to show up in the OpenAPI docs.
Use Pydantic models for schema validation
Use django-ninja's authentication classesImplement REST APIs with django-ninja using Pydantic schemas under core/api/
Files:
core/api/schemas.pycore/api/views.py
core/models.py
📄 CodeRabbit inference engine (CLAUDE.md)
core/models.py: Place business logic in Django models (fat models pattern)
Validate simple constraints in the database and place complex domain logic in Django models
Files:
core/models.py
🧠 Learnings (2)
📚 Learning: 2025-10-04T08:52:37.437Z
Learnt from: CR
Repo: rasulkireev/TuxSEO PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-10-04T08:52:37.437Z
Learning: Applies to core/agents/**/*.py : Implement AI agents using pydantic-ai under core/agents/
Applied to files:
core/agents.py
📚 Learning: 2025-10-04T08:52:37.437Z
Learnt from: CR
Repo: rasulkireev/TuxSEO PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-10-04T08:52:37.437Z
Learning: Always add AGENTS.md into AI context
Applied to files:
core/agents.py
🧬 Code graph analysis (4)
core/schemas.py (1)
core/models.py (3)
project_details(349-364)competitor_details(1376-1381)competitor_details(1486-1494)
core/agents.py (4)
core/choices.py (2)
AIModel(112-114)get_default_ai_model(120-122)core/schemas.py (4)
BlogPostGenerationContext(180-187)CompetitorVsTitleContext(255-259)ProjectDetails(15-97)TitleSuggestion(149-160)core/models.py (4)
project_details(349-364)competitor_details(1376-1381)competitor_details(1486-1494)add_language_specification(508-514)core/agent_system_prompts.py (1)
add_language_specification(118-123)
core/api/views.py (2)
core/api/schemas.py (2)
GenerateCompetitorVsTitleIn(134-135)GenerateCompetitorVsTitleOut(138-142)core/models.py (4)
Competitor(1345-1559)can_generate_title_suggestions(266-267)product_name(171-176)generate_vs_title(1525-1559)
core/models.py (3)
core/schemas.py (1)
CompetitorVsTitleContext(255-259)core/model_utils.py (1)
run_agent_synchronously(18-75)core/choices.py (1)
ContentType(4-7)
🪛 Flake8 (7.3.0)
core/agents.py
[error] 121-121: 'pydantic_ai.RunContext' imported but unused
(F401)
🪛 Ruff (0.14.2)
core/api/views.py
660-672: Consider moving this statement to an else block
(TRY300)
685-685: Use explicit conversion flag
Replace with conversion flag
(RUF010)
|
@coderabbitai full review |
✅ Actions performedFull review triggered. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Greptile Overview
Greptile Summary
Adds AI-powered competitor comparison blog post generation using Perplexity Sonar with web search capabilities.
Key Changes:
- Added
Competitor.blog_postfield to store generated comparison content - Created two new AI agents using Perplexity Sonar for title suggestions and content generation
- Added
/api/generate-competitor-vs-titleendpoint with proper authorization and limit checks - Implemented profile limits for competitor features (
competitor_limit,competitor_posts_limit) - Optimized competitor creation flow by removing expensive analysis step (cost savings)
- Added new Stimulus controller and template for displaying comparison blog posts
- Included proper Pydantic schemas for context management
Architecture:
The feature uses Perplexity's Sonar model which can search the web in real-time to gather current information about both products before generating the comparison content. This ensures up-to-date and factually accurate comparisons.
Cost Optimization:
The PR includes smart cost optimization by making the initial competitor analysis lightweight (only fetches homepage content) while deferring the expensive VS blog post generation to a manual user action.
Confidence Score: 4/5
- This PR is safe to merge with minor considerations
- The implementation follows Django and project conventions well, includes proper error handling and authorization checks, and demonstrates good architectural decisions (cost optimization, proper separation of concerns). One minor issue was already flagged (unused import in
core/agents.py:121). The code quality is high with proper logging, limit checks, and user-friendly error messages. - No files require special attention - the implementation is solid across all changed files
Important Files Changed
File Analysis
| Filename | Score | Overview |
|---|---|---|
| core/models.py | 4/5 | Added competitor VS blog post generation capability with new blog_post field and generate_vs_blog_post() method. Also added profile limit properties for competitor features. |
| core/agents.py | 4/5 | Added two new AI agents (competitor_vs_title_agent and competitor_vs_blog_post_agent) using Perplexity Sonar for generating comparison content. Clean implementation with proper system prompts. |
| core/schemas.py | 5/5 | Added Pydantic schemas for competitor VS post context (CompetitorVsTitleContext, CompetitorVsPostContext). Schemas are well-structured with proper field descriptions. |
| core/api/views.py | 4/5 | Modified add_competitor endpoint to be more lightweight (removed expensive analysis call), added new /generate-competitor-vs-title endpoint with proper limit checks and error handling. |
| core/tasks.py | 5/5 | Updated analyze_project_competitor to skip expensive analysis step (cost optimization). Task now only fetches homepage content and populates basic details. |
| frontend/src/controllers/competitor_vs_post_controller.js | 5/5 | New Stimulus controller for generating competitor VS blog posts. Clean async implementation with proper loading states and error handling. |
Sequence Diagram
sequenceDiagram
participant User
participant Frontend
participant API
participant Competitor
participant Agent
participant Perplexity
User->>Frontend: Click "Generate Post"
Frontend->>API: POST /api/generate-competitor-vs-title
API->>API: Check profile limits
alt Limit reached
API-->>Frontend: Error: Limit reached
Frontend-->>User: Show error message
else Can generate
API->>Competitor: generate_vs_blog_post()
Competitor->>Agent: Call competitor_vs_blog_post_agent
Agent->>Perplexity: Research both products
Perplexity-->>Agent: Web search results
Agent->>Agent: Generate comparison content
Agent-->>Competitor: Return markdown blog post
Competitor->>Competitor: Save to blog_post field
Competitor-->>API: Success
API-->>Frontend: Success response
Frontend->>Frontend: Replace button with "View Post"
Frontend-->>User: Show success message
User->>Frontend: Click "View Post"
Frontend->>Frontend: Navigate to detail page
end
22 files reviewed, no comments
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 9
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
core/api/views.py (1)
575-659: Drop the# noqa: E501directivesRuff reports every
# noqa: E501here as unused (RUF100), so the lint job fails. Reflow the messages instead of suppressing the warning.if not profile.can_add_competitors: return { "status": "error", - "message": f"You have reached the competitor limit for your {profile.product_name} plan. Please upgrade to add more competitors.", # noqa: E501 + "message": ( + "You have reached the competitor limit for your " + f"{profile.product_name} plan. Please upgrade to add more competitors." + ), } ... if not profile.can_generate_competitor_posts: return { "status": "error", - "message": f"You have reached the competitor post generation limit for your {profile.product_name} plan. Please upgrade to generate more competitor comparison posts.", # noqa: E501 + "message": ( + "You have reached the competitor post generation limit for your " + f"{profile.product_name} plan. Please upgrade to generate more competitor comparison posts." + ), } if not profile.can_generate_blog_posts: return { "status": "error", - "message": f"You have reached the content generation limit for your {profile.product_name} plan", # noqa: E501 + "message": ( + "You have reached the content generation limit for your " + f"{profile.product_name} plan" + ), }
🧹 Nitpick comments (2)
requirements.txt (1)
1-201: Consider documenting dependency purpose and audit strategy.This update introduces ~50+ new or upgraded dependencies to support agent and competitor generation workflows. While the additions are reasonable for the feature scope, there is no inline documentation or companion changelog explaining:
- Why each new dependency category was added (e.g., temporalio for workflows, keyring for secrets, openapi-pydantic for schema generation).
- The supply chain impact and any security vetting performed.
- A strategy for managing the growing dependency footprint (lock file, vulnerability scanning, etc.).
Recommendation: Consider adding comments to group related dependencies or creating a
DEPENDENCIES.mddocumenting the purpose of new packages. Additionally, ensure a dependency audit/lock strategy is in place.frontend/src/controllers/add_competitor_controller.js (1)
66-74: Emit a namespaced CustomEvent after successful addPer the Stimulus controller guidelines we follow, this controller should broadcast a bubbled, namespaced CustomEvent when the action succeeds so other controllers can react without a full page reload. Dispatching the event (with the relevant payload in
event.detail) keeps behaviors decoupled and avoids violating the “controllers talk via events” rule. Please fire the event right before showing the toast/reload.As per coding guidelines
if (data.status === "success") { + const competitorAddedEvent = new CustomEvent("competitor:added", { + detail: { + competitorId: data.competitor_id, + projectId: this.projectIdValue, + url, + }, + bubbles: true, + }); + this.element.dispatchEvent(competitorAddedEvent); + showMessage("Competitor added! Analysis will continue in the background. The page will reload.", "success"); setTimeout(() => { window.location.reload();
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
poetry.lockis excluded by!**/*.lock
📒 Files selected for processing (22)
CHANGELOG.md(1 hunks)core/agent_system_prompts.py(2 hunks)core/agents.py(3 hunks)core/api/schemas.py(1 hunks)core/api/views.py(3 hunks)core/migrations/0038_competitor_blog_post.py(1 hunks)core/models.py(20 hunks)core/prompts.py(5 hunks)core/schemas.py(1 hunks)core/tasks.py(3 hunks)core/urls.py(2 hunks)core/utils.py(1 hunks)core/views.py(3 hunks)frontend/src/controllers/add_competitor_controller.js(1 hunks)frontend/src/controllers/competitor_analysis_controller.js(0 hunks)frontend/src/controllers/competitor_vs_post_controller.js(1 hunks)frontend/src/controllers/copy_markdown_controller.js(1 hunks)frontend/templates/base_project.html(1 hunks)frontend/templates/project/competitor_blog_post_detail.html(1 hunks)frontend/templates/project/project_competitors.html(1 hunks)pyproject.toml(1 hunks)requirements.txt(7 hunks)
💤 Files with no reviewable changes (1)
- frontend/src/controllers/competitor_analysis_controller.js
🚧 Files skipped from review as they are similar to previous changes (1)
- core/schemas.py
🧰 Additional context used
📓 Path-based instructions (15)
**/*.html
📄 CodeRabbit inference engine (.cursor/rules/frontend.mdc)
**/*.html: Prefer Stimulus JS for adding interactivity to Django templates instead of raw script elements
Leverage Stimulus data attributes to connect HTML elements with JavaScript functionality
Utilize Stimulus targets to reference specific elements within a controller
Employ Stimulus actions to handle user interactions and events
Files:
frontend/templates/project/competitor_blog_post_detail.htmlfrontend/templates/base_project.htmlfrontend/templates/project/project_competitors.html
**/*.{html,htm}
📄 CodeRabbit inference engine (.cursor/rules/ui-ux-design-guidelines.mdc)
Always generate semantic HTML
Files:
frontend/templates/project/competitor_blog_post_detail.htmlfrontend/templates/base_project.htmlfrontend/templates/project/project_competitors.html
{**/*.{html,htm,css,scss},**/*_controller.@(js|ts)}
📄 CodeRabbit inference engine (.cursor/rules/ui-ux-design-guidelines.mdc)
Always favor the utility-first Tailwind approach; avoid creating reusable style classes and prefer reuse via template components
Files:
frontend/templates/project/competitor_blog_post_detail.htmlfrontend/src/controllers/competitor_vs_post_controller.jsfrontend/src/controllers/add_competitor_controller.jsfrontend/src/controllers/copy_markdown_controller.jsfrontend/templates/base_project.htmlfrontend/templates/project/project_competitors.html
frontend/templates/**/*.html
📄 CodeRabbit inference engine (.cursor/rules/stimulus-events.mdc)
Avoid data-*-outlet links for sibling controller communication when using the event-based approach; keep controllers self-contained
Use semantic HTML elements (e.g., dialog, details/summary) in templates
Files:
frontend/templates/project/competitor_blog_post_detail.htmlfrontend/templates/base_project.htmlfrontend/templates/project/project_competitors.html
frontend/src/controllers/**/*.js
📄 CodeRabbit inference engine (.cursor/rules/frontend.mdc)
frontend/src/controllers/**/*.js: Use Stimulus controllers to encapsulate JavaScript behavior and keep it separate from HTML structure
New controllers should be created in thefrontend/src/controllersdirectoryUse Stimulus.js controllers for interactive frontend behavior
Files:
frontend/src/controllers/competitor_vs_post_controller.jsfrontend/src/controllers/add_competitor_controller.jsfrontend/src/controllers/copy_markdown_controller.js
**/*.js
📄 CodeRabbit inference engine (.cursor/rules/stimulus-general.mdc)
**/*.js: Add semicolons at the end of statements in JavaScript files
Use double quotes instead of single quotes for string literals in JavaScript files
Files:
frontend/src/controllers/competitor_vs_post_controller.jsfrontend/src/controllers/add_competitor_controller.jsfrontend/src/controllers/copy_markdown_controller.js
{**/*.{css,scss},**/*_controller.@(js|ts)}
📄 CodeRabbit inference engine (.cursor/rules/ui-ux-design-guidelines.mdc)
Never create new styles without explicitly receiving permission to do so
Files:
frontend/src/controllers/competitor_vs_post_controller.jsfrontend/src/controllers/add_competitor_controller.jsfrontend/src/controllers/copy_markdown_controller.js
frontend/src/controllers/**/*_controller.js
📄 CodeRabbit inference engine (.cursor/rules/stimulus-events.mdc)
frontend/src/controllers/**/*_controller.js: Dispatch a CustomEvent from the actor Stimulus controller to signal actions to other controllers
Name custom events descriptively and namespaced (e.g., "suggestion:move")
Set bubbles: true on CustomEvent so ancestor listeners (window/document) can receive it
Include all necessary payload in event.detail (e.g., element and destination)
Dispatch the event from the controller element (this.element.dispatchEvent(event))
In listener controllers, register global event listeners in connect() and remove them in disconnect()
Bind handler methods and keep a reference (e.g., this.boundMove = this.move.bind(this)) to correctly remove listeners
Attach listeners to window or document to catch bubbled custom events from anywhere on the page
Inspect event.detail in the handler and act conditionally based on its contents (e.g., match destination)
Files:
frontend/src/controllers/competitor_vs_post_controller.jsfrontend/src/controllers/add_competitor_controller.jsfrontend/src/controllers/copy_markdown_controller.js
**/*.py
📄 CodeRabbit inference engine (.cursor/rules/backend.mdc)
**/*.py: Prioritize readability and maintainability; follow PEP 8
Use descriptive variable/function names with underscoresIn Python, use try-except blocks that catch specific exception types; do not catch broad Exception.
**/*.py: Follow PEP 8 for Python code style
Use descriptive variable names with underscores (snake_case) in Python
Prefer Django's built-in features over external libraries
**/*.py: Use structlog for logging in Python code (avoid print and the standard logging module)
Include contextual key-value fields in log messages (e.g., error=str(e), exc_info=True, profile_id=profile.id)
**/*.py: Use descriptive, full-word variable names; avoid abbreviations and single-letter variables in Python
Provide context in variable names when format or type matters (e.g., include_iso_format,date)
Extract unchanging values into UPPER_CASE constants
Use intermediary variables to name parsed groups instead of using index access directly
Naming conventions: use is_/has_/can_ for booleans; include 'date' for dates; snake_case for variables/functions; PascalCase for classes
Define variables close to where they are used to keep lifespan short
Name things after what they do, not how they're used; ensure names make sense without extra context
Avoid generic names like data, info, manager; use specific, intention-revealing names
Function names should include necessary context without being verbose
If naming is hard, split functions into smaller focused parts
Maintain consistency: reuse the same verbs and nouns for the same concepts; name variables after the functions that create them
Use more descriptive names for longer-lived variables
Avoid else statements by using guard clauses for early returns
Replace simple conditionals with direct assignment when both branches call the same function with different values
Use dictionaries as dispatch tables instead of multiple equal-probability elif chains
Validate input before processing to prevent errors propagating in...
Files:
core/utils.pycore/tasks.pycore/api/views.pycore/agents.pycore/api/schemas.pycore/urls.pycore/migrations/0038_competitor_blog_post.pycore/agent_system_prompts.pycore/models.pycore/views.pycore/prompts.py
core/**/*.py
📄 CodeRabbit inference engine (.cursor/rules/backend.mdc)
Leverage Django's ORM; avoid raw SQL when possible
Files:
core/utils.pycore/tasks.pycore/api/views.pycore/agents.pycore/api/schemas.pycore/urls.pycore/migrations/0038_competitor_blog_post.pycore/agent_system_prompts.pycore/models.pycore/views.pycore/prompts.py
**/{utils,repository}.py
📄 CodeRabbit inference engine (AGENTS.md)
Avoid generic filenames like utils.py and repository.py; use specific names instead
Files:
core/utils.py
core/tasks.py
📄 CodeRabbit inference engine (CLAUDE.md)
Define background tasks using django-q2 in core/tasks.py
Files:
core/tasks.py
core/api/**/*.py
📄 CodeRabbit inference engine (.cursor/rules/backend.mdc)
core/api/**/*.py: django-ninja generate openapi documentation, so make sure to populate views with relevant data in order for it to show up in the OpenAPI docs.
Use Pydantic models for schema validation
Use django-ninja's authentication classesImplement REST APIs with django-ninja using Pydantic schemas under core/api/
Files:
core/api/views.pycore/api/schemas.py
core/models.py
📄 CodeRabbit inference engine (CLAUDE.md)
core/models.py: Place business logic in Django models (fat models pattern)
Validate simple constraints in the database and place complex domain logic in Django models
Files:
core/models.py
core/views.py
📄 CodeRabbit inference engine (CLAUDE.md)
Keep Django views skinny: handle request/response only
Files:
core/views.py
🧠 Learnings (5)
📚 Learning: 2025-10-04T08:52:37.437Z
Learnt from: CR
Repo: rasulkireev/TuxSEO PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-10-04T08:52:37.437Z
Learning: Applies to frontend/src/controllers/**/*.js : Use Stimulus.js controllers for interactive frontend behavior
Applied to files:
frontend/src/controllers/competitor_vs_post_controller.jsfrontend/src/controllers/add_competitor_controller.jsfrontend/src/controllers/copy_markdown_controller.js
📚 Learning: 2025-10-04T08:52:37.437Z
Learnt from: CR
Repo: rasulkireev/TuxSEO PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-10-04T08:52:37.437Z
Learning: Always add AGENTS.md into AI context
Applied to files:
core/agents.py
📚 Learning: 2025-10-04T08:52:37.437Z
Learnt from: CR
Repo: rasulkireev/TuxSEO PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-10-04T08:52:37.437Z
Learning: Applies to core/agents/**/*.py : Implement AI agents using pydantic-ai under core/agents/
Applied to files:
core/agents.pycore/models.py
📚 Learning: 2025-10-04T08:52:58.590Z
Learnt from: CR
Repo: rasulkireev/TuxSEO PR: 0
File: .cursor/rules/agent-rules.mdc:0-0
Timestamp: 2025-10-04T08:52:58.590Z
Learning: Always add AGENTS.md into context
Applied to files:
core/agents.py
📚 Learning: 2025-10-04T08:52:37.437Z
Learnt from: CR
Repo: rasulkireev/TuxSEO PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-10-04T08:52:37.437Z
Learning: Applies to core/tasks.py : Define background tasks using django-q2 in core/tasks.py
Applied to files:
core/models.py
🧬 Code graph analysis (8)
frontend/src/controllers/competitor_vs_post_controller.js (1)
frontend/src/utils/messages.js (1)
showMessage(2-39)
frontend/src/controllers/add_competitor_controller.js (2)
frontend/src/utils/messages.js (1)
showMessage(2-39)frontend/src/controllers/competitor_vs_post_controller.js (3)
response(25-34)data(41-41)error(37-37)
core/tasks.py (1)
core/models.py (4)
Project(361-827)find_competitors(705-789)get_and_save_list_of_competitors(791-827)populate_name_description(1472-1509)
core/api/views.py (2)
core/api/schemas.py (2)
GenerateCompetitorVsTitleIn(134-135)GenerateCompetitorVsTitleOut(138-141)core/models.py (10)
can_add_competitors(321-322)product_name(173-178)Competitor(1405-1635)get_page_content(456-484)get_page_content(1339-1363)get_page_content(1446-1470)populate_name_description(1472-1509)can_generate_competitor_posts(325-326)can_generate_blog_posts(272-273)generate_vs_blog_post(1588-1635)
core/agents.py (4)
core/agent_system_prompts.py (3)
markdown_lists(30-36)add_language_specification(132-137)add_project_pages(65-113)core/choices.py (2)
AIModel(117-119)get_default_ai_model(125-127)core/schemas.py (6)
BlogPostGenerationContext(180-187)CompetitorVsPostContext(262-275)CompetitorVsTitleContext(255-259)ProjectDetails(15-97)ProjectPageDetails(100-127)TitleSuggestion(149-160)core/models.py (4)
project_details(404-419)competitor_details(1439-1444)competitor_details(1549-1557)add_language_specification(563-569)
core/urls.py (1)
core/views.py (2)
ProjectCompetitorsView(824-862)CompetitorBlogPostDetailView(912-930)
core/models.py (3)
core/agent_system_prompts.py (1)
markdown_lists(30-36)core/schemas.py (3)
ProjectDetails(15-97)CompetitorVsPostContext(262-275)ProjectPageContext(169-177)core/model_utils.py (1)
run_agent_synchronously(18-75)
core/views.py (1)
core/models.py (10)
Competitor(1405-1635)Project(361-827)competitor_limit(276-280)number_of_competitors(290-293)number_of_competitors(750-755)can_add_competitors(321-322)competitor_posts_limit(283-287)number_of_competitor_posts_generated(296-304)can_generate_competitor_posts(325-326)is_on_free_plan(181-182)
🪛 Ruff (0.14.2)
core/tasks.py
249-249: Unused noqa directive (non-enabled: E501)
Remove unused noqa directive
(RUF100)
266-266: Use explicit conversion flag
Replace with conversion flag
(RUF010)
core/api/views.py
576-576: Unused noqa directive (non-enabled: E501)
Remove unused noqa directive
(RUF100)
652-652: Unused noqa directive (non-enabled: E501)
Remove unused noqa directive
(RUF100)
658-658: Unused noqa directive (non-enabled: E501)
Remove unused noqa directive
(RUF100)
681-685: Consider moving this statement to an else block
(TRY300)
698-698: Use explicit conversion flag
Replace with conversion flag
(RUF010)
core/agents.py
208-208: Unused noqa directive (non-enabled: E501)
Remove unused noqa directive
(RUF100)
core/migrations/0038_competitor_blog_post.py
8-10: Mutable class attributes should be annotated with typing.ClassVar
(RUF012)
12-18: Mutable class attributes should be annotated with typing.ClassVar
(RUF012)
core/agent_system_prompts.py
36-36: Unused noqa directive (non-enabled: E501)
Remove unused noqa directive
(RUF100)
core/models.py
750-750: Unused function argument: ctx
(ARG001)
1622-1622: Unused noqa directive (non-enabled: E501)
Remove unused noqa directive
(RUF100)
core/prompts.py
21-21: Unused noqa directive (non-enabled: E501)
Remove unused noqa directive
(RUF100)
61-61: Unused noqa directive (non-enabled: E501)
Remove unused noqa directive
(RUF100)
148-148: Unused noqa directive (non-enabled: E501)
Remove unused noqa directive
(RUF100)
195-195: Unused noqa directive (non-enabled: E501)
Remove unused noqa directive
(RUF100)
227-227: Unused noqa directive (non-enabled: E501)
Remove unused noqa directive
(RUF100)
🔇 Additional comments (5)
CHANGELOG.md (1)
20-21: CHANGELOG entries are clear and well-formatted.The new entries accurately document the PydanticAI library upgrade, agent refactoring for centralization, and the new Competitors page feature, all in line with the PR objectives.
Also applies to: 28-28
pyproject.toml (1)
38-38: Pydantic-ai v1.9.1 upgrade verified—no issues found.The codebase has been successfully updated for v1.9.1 compatibility. All 20+ agent executions use the correct async pattern via
run_agent_synchronously, all 40+ result accesses follow the newresult.outputpattern, and all Agent instantiations use the correct v1.9.1 API with proper parameters (output_type, deps_type, system_prompt). No deprecated patterns remain, and all imports are compatible with the new version.requirements.txt (3)
70-70: Clarify hardware constraint logic for hf-xet package.Line 70 specifies hf-xet with a complex platform constraint:
platform_machine == "x86_64" or platform_machine == "amd64" or platform_machine == "AMD64" or platform_machine == "arm64" or platform_machine == "aarch64". This is unusual because:
- The condition essentially includes most modern architectures (essentially all except older/embedded systems).
- The original constraint (if any) is unclear — was this broadened intentionally?
- Consider whether this should be inverted (exclude unsupported platforms) or simplified.
Clarify the intent or simplify the constraint for maintainability.
109-109: Outdated pre-release OpenTelemetry versions in production dependencies.Lines 109 and 114 pin outdated beta versions of OpenTelemetry instrumentation packages:
opentelemetry-instrumentation-httpx==0.56b0(latest available: 0.58b0)opentelemetry-util-http==0.56b0(latest available: 0.59b0)No stable releases currently exist for these packages—only beta versions are available. However, the pinned versions are outdated. Consider updating to the latest available versions (0.58b0 and 0.59b0 respectively) for bug fixes and improvements, while acknowledging that production use still carries the stability risk inherent to pre-release software.
134-138: Correct invalid pydantic-ai version: 1.9.1 does not exist (latest is v1.7.0).pydantic-ai v1.9.1 does not appear to exist on GitHub releases, with v1.7.0 (Oct 27, 2025) being the latest available. The requirements.txt specifies
pydantic-ai==1.9.1which would causepip installto fail.Update line 135 to use a valid version (e.g.,
pydantic-ai==1.7.0or the intended version if this was a typo). The codebase actively uses pydantic_ai (Agent, RunContext, OpenAIChatModel, OpenAIProvider, capture_run_messages), so ensure the specified version is compatible with the imports in core/agents.py, core/models.py, and core/utils.py.⛔ Skipped due to learnings
Learnt from: CR Repo: rasulkireev/TuxSEO PR: 0 File: CLAUDE.md:0-0 Timestamp: 2025-10-04T08:52:37.437Z Learning: Applies to core/agents/**/*.py : Implement AI agents using pydantic-ai under core/agents/
| ## [0.0.7] - 2025-11-02 | ||
| ### Changed | ||
| - Project UI updated to be more intuitive. | ||
| - PydanticAI library upgrade. | ||
| - Competitor blog post generation agent moved to centralized agents.py file for easier maintenance. | ||
|
|
||
| ### Added | ||
| - Copy as HTML on Generated Blog Posts. | ||
| - PDF Generation for blog posts. | ||
| - Centralized location of all AI models used in the app. | ||
| - Referrer model to display banners for expected referrer like producthunt | ||
|
|
||
| - Competitors page to view all competitors for any given project |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Multiple entries for the same version (0.0.7) with different dates.
The CHANGELOG contains several entries for version 0.0.7 dated 2025-11-02, 2025-11-01, 2025-10-29, etc. This makes it unclear which release date is authoritative. Typically, all changes for a given version are consolidated under a single, final release date. Consider consolidating all v0.0.7 entries into a single dated entry or clarifying the versioning strategy if this pattern is intentional.
🤖 Prompt for AI Agents
In CHANGELOG.md around lines 17 to 28, there are multiple separate entries for
version 0.0.7 with different dates; consolidate these into a single v0.0.7 entry
using the final/authoritative release date (pick the intended date), merge and
deduplicate all bullet points under the appropriate sections
(Added/Changed/Fixed), remove the extra v0.0.7 headings, and ensure the
consolidated entry follows the project's changelog format and ordering.
| logger.warning( | ||
| "[Add Competitor] Failed to get page content for competitor", | ||
| competitor_id=competitor.id, | ||
| url=data.url, | ||
| project_id=project.id, | ||
| ) | ||
| return { | ||
| "status": "error", | ||
| "message": "Failed to get page content for this competitor URL", | ||
| "message": "Failed to get page content for competitor", | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Clean up failed competitor creations
If get_page_content() fails we return an error but leave the freshly created Competitor row in place. That phantom record still counts against plan limits and blocks the user from retrying (duplicate URL constraint). Please delete the object before returning the error so we don’t strand unusable competitors.
else:
logger.warning(
"[Add Competitor] Failed to get page content for competitor",
competitor_id=competitor.id,
url=data.url,
project_id=project.id,
)
+ competitor.delete()
return {
"status": "error",
"message": "Failed to get page content for competitor",
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| logger.warning( | |
| "[Add Competitor] Failed to get page content for competitor", | |
| competitor_id=competitor.id, | |
| url=data.url, | |
| project_id=project.id, | |
| ) | |
| return { | |
| "status": "error", | |
| "message": "Failed to get page content for this competitor URL", | |
| "message": "Failed to get page content for competitor", | |
| } | |
| logger.warning( | |
| "[Add Competitor] Failed to get page content for competitor", | |
| competitor_id=competitor.id, | |
| url=data.url, | |
| project_id=project.id, | |
| ) | |
| competitor.delete() | |
| return { | |
| "status": "error", | |
| "message": "Failed to get page content for competitor", | |
| } |
🤖 Prompt for AI Agents
In core/api/views.py around lines 603 to 612, when get_page_content() fails you
currently log and return an error but leave the newly created Competitor row in
the DB; delete the created competitor before returning the error (e.g., call
competitor.delete() or the appropriate ORM cleanup) and ensure this deletion
happens inside the failure branch (or a try/except/finally) so no phantom
competitor remains to count against plan limits or block retries.
| showMessage("VS blog post generated successfully!", "success"); | ||
|
|
||
| const viewPostUrl = `/project/${this.projectIdValue}/competitor/${this.competitorIdValue}/post/`; | ||
|
|
||
| button.outerHTML = ` | ||
| <a href="${viewPostUrl}" | ||
| class="inline-flex items-center px-3 py-1.5 text-xs font-medium text-white bg-gray-900 rounded-md border border-gray-900 hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-gray-500"> | ||
| <svg class="mr-1.5 w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | ||
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path> | ||
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path> | ||
| </svg> | ||
| View Post | ||
| </a> | ||
| `; | ||
|
|
||
| } catch (error) { | ||
| button.disabled = false; | ||
| button.innerHTML = ` | ||
| <svg class="mr-1.5 w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | ||
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path> | ||
| </svg> | ||
| Generate Post | ||
| `; | ||
|
|
||
| showMessage(error.message, "error"); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
Broadcast generation results via CustomEvent.
Per our Stimulus conventions, this controller should notify the rest of the UI through a bubbling CustomEvent both on success and on failure. Suggest patch:
showMessage("VS blog post generated successfully!", "success");
+ const generatedEvent = new CustomEvent("competitor-vs-post:generated", {
+ bubbles: true,
+ detail: {
+ competitorId: this.competitorIdValue,
+ projectId: this.projectIdValue
+ }
+ });
+ this.element.dispatchEvent(generatedEvent);
+
const viewPostUrl = `/project/${this.projectIdValue}/competitor/${this.competitorIdValue}/post/`;
@@
showMessage(error.message, "error");
+
+ const errorEvent = new CustomEvent("competitor-vs-post:error", {
+ bubbles: true,
+ detail: {
+ competitorId: this.competitorIdValue,
+ projectId: this.projectIdValue,
+ errorMessage: error.message
+ }
+ });
+ this.element.dispatchEvent(errorEvent);As per coding guidelines.
| async copy() { | ||
| await navigator.clipboard.writeText(this.contentValue); | ||
|
|
||
| const button = this.buttonTarget; | ||
| const originalText = button.innerHTML; | ||
|
|
||
| button.innerHTML = ` | ||
| <svg class="mr-2 w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | ||
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" /> | ||
| </svg> | ||
| Copied! | ||
| `; | ||
| button.classList.remove("hover:bg-gray-50"); | ||
| button.classList.add("bg-green-50", "text-green-700", "border-green-300"); | ||
|
|
||
| setTimeout(() => { | ||
| button.innerHTML = originalText; | ||
| button.classList.remove("bg-green-50", "text-green-700", "border-green-300"); | ||
| button.classList.add("hover:bg-gray-50"); | ||
| }, 2000); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
Dispatch a bubbling copy event from the controller.
Our Stimulus controllers are expected to signal actions via CustomEvent so other controllers can react. Please emit a namespaced, bubbling event with the copied payload right after the clipboard write succeeds. One possible patch:
async copy() {
await navigator.clipboard.writeText(this.contentValue);
+ const copiedEvent = new CustomEvent("copy-markdown:copied", {
+ bubbles: true,
+ detail: {
+ content: this.contentValue,
+ sourceElement: this.element
+ }
+ });
+ this.element.dispatchEvent(copiedEvent);
+
const button = this.buttonTarget;
const originalText = button.innerHTML;As per coding guidelines.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| async copy() { | |
| await navigator.clipboard.writeText(this.contentValue); | |
| const button = this.buttonTarget; | |
| const originalText = button.innerHTML; | |
| button.innerHTML = ` | |
| <svg class="mr-2 w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" /> | |
| </svg> | |
| Copied! | |
| `; | |
| button.classList.remove("hover:bg-gray-50"); | |
| button.classList.add("bg-green-50", "text-green-700", "border-green-300"); | |
| setTimeout(() => { | |
| button.innerHTML = originalText; | |
| button.classList.remove("bg-green-50", "text-green-700", "border-green-300"); | |
| button.classList.add("hover:bg-gray-50"); | |
| }, 2000); | |
| } | |
| async copy() { | |
| await navigator.clipboard.writeText(this.contentValue); | |
| const copiedEvent = new CustomEvent("copy-markdown:copied", { | |
| bubbles: true, | |
| detail: { | |
| content: this.contentValue, | |
| sourceElement: this.element | |
| } | |
| }); | |
| this.element.dispatchEvent(copiedEvent); | |
| const button = this.buttonTarget; | |
| const originalText = button.innerHTML; | |
| button.innerHTML = ` | |
| <svg class="mr-2 w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" /> | |
| </svg> | |
| Copied! | |
| `; | |
| button.classList.remove("hover:bg-gray-50"); | |
| button.classList.add("bg-green-50", "text-green-700", "border-green-300"); | |
| setTimeout(() => { | |
| button.innerHTML = originalText; | |
| button.classList.remove("bg-green-50", "text-green-700", "border-green-300"); | |
| button.classList.add("hover:bg-gray-50"); | |
| }, 2000); | |
| } |
🤖 Prompt for AI Agents
In frontend/src/controllers/copy_markdown_controller.js around lines 9 to 29,
after the await navigator.clipboard.writeText(this.contentValue) call dispatch a
namespaced, bubbling CustomEvent (e.g. "copy:markdown:copied") with the copied
text in its detail so other Stimulus controllers can react; construct the event
with detail: { text: this.contentValue } and options { bubbles: true, composed:
true } and dispatch it from the controller root (this.element) immediately after
the successful write before updating the button UI.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
♻️ Duplicate comments (9)
CHANGELOG.md (1)
17-28: Multiple 0.0.7 version entries remain unresolved.As previously noted, this CHANGELOG contains multiple separate entries for version 0.0.7 with different dates. The new additions (PydanticAI library upgrade and Competitors page) continue this pattern. Consider consolidating all v0.0.7 entries into a single dated entry with all changes grouped by type.
core/tasks.py (2)
240-249: LGTM! Correctly implements the cost-saving workflow.The logic change from
analyze_competitor()topopulate_name_description()aligns with the documented goal of reducing costs. The inline comments clearly explain why the expensive analysis path is intentionally skipped.However, the unused
# noqa: E501directive on line 249 should be removed as previously noted, since E501 is not enabled in the Ruff configuration.
260-266: Error handling updated appropriately.The error message update from "Error analyzing competitor" to "Error processing competitor" correctly reflects the simplified workflow.
However, as previously noted, line 266 should use an f-string conversion flag (
{e!s}or just{e}) instead of{str(e)}to satisfy Ruff rule RUF010.core/prompts.py (1)
21-21: Remove unused# noqa: E501directives.As previously noted, all five
# noqa: E501directives added in this file (lines 21, 61, 148, 195, and 227) are flagged by Ruff as unused (RUF100) because the E501 rule is not enabled in the project's configuration. Please remove these directives to resolve the linting warnings.Also applies to: 61-61, 148-148, 195-195, 227-227
core/agent_system_prompts.py (1)
30-36: Remove the unused# noqa: E501directive.The existing review comment already flags this issue. Ruff reports RUF100 because E501 is not enabled in the linter configuration. Remove the directive to clean up the linting output.
frontend/src/controllers/copy_markdown_controller.js (1)
9-29: Dispatch a bubbling CustomEvent after clipboard write.The existing review comment already covers this requirement comprehensively. Per coding guidelines, dispatch a namespaced, bubbling CustomEvent (e.g.,
"copy-markdown:copied") with the copied content inevent.detailimmediately after the successful clipboard write so other Stimulus controllers can react to the copy action.As per coding guidelines.
frontend/src/controllers/competitor_vs_post_controller.js (1)
12-73: Dispatch bubbling CustomEvents for success and error outcomes.The existing review comment already covers this requirement comprehensively. Per coding guidelines, dispatch namespaced, bubbling CustomEvents (e.g.,
"competitor-vs-post:generated"and"competitor-vs-post:error") with relevant payload inevent.detailso other Stimulus controllers can react to generation outcomes.As per coding guidelines.
core/agents.py (1)
203-208: Remove unused noqa directive.The
# noqa: E501on line 208 is unnecessary since the multi-line string doesn't trigger line-length warnings.Apply this diff:
For example, if the text is 'sample text[1]', the link should be inserted like this: '[sample text](https://www.example.com)'. - """ # noqa: E501 + """core/api/views.py (1)
592-612: Delete competitor on content fetch failure.If
get_page_content()fails, the newly createdCompetitorobject remains in the database, counting against plan limits and preventing retries due to the duplicate URL constraint.Apply this diff:
got_content = competitor.get_page_content() if got_content: competitor.populate_name_description() logger.info( "[Add Competitor] Successfully populated competitor details", competitor_id=competitor.id, competitor_name=competitor.name, project_id=project.id, ) else: logger.warning( "[Add Competitor] Failed to get page content for competitor", competitor_id=competitor.id, url=data.url, project_id=project.id, ) + competitor.delete() return { "status": "error", "message": "Failed to get page content for competitor", }
🧹 Nitpick comments (3)
requirements.txt (1)
1-201: Large dependency surface expansion.The upgrade to pydantic-ai 1.9.1 introduces numerous new dependencies including AI tooling (ag-ui-protocol, fastmcp, genai-prices), platform-specific packages (jeepney, secretstorage for Linux; pywin32-ctypes for Windows), OpenTelemetry instrumentation, and various utility libraries. While this expansion is expected given the major version upgrade and new competitor analysis features, consider reviewing whether all transitive dependencies are necessary for your use case.
core/views.py (1)
912-931: Move title formatting to the Competitor model.The blog post title format is duplicated here and in
core/models.py:1596withinCompetitor.generate_vs_blog_post(). Consider adding a property to theCompetitormodel to centralize this logic.Add a property to the
Competitormodel incore/models.py:@property def vs_blog_post_title(self): return f"{self.project.name} vs. {self.name}: Which is Better?"Then update this view:
- context["blog_post_title"] = f"{project.name} vs. {competitor.name}: Which is Better?" + context["blog_post_title"] = competitor.vs_blog_post_titleAnd update
core/models.pyline 1596 to use the same property.core/api/views.py (1)
633-700: LGTM with optional style improvement.The endpoint properly checks permissions and limits, handles errors appropriately, and returns a clean response structure.
Optional: Consider moving the success return to an
elseblock for clarity (suggested by static analysis):try: logger.info( "Generating VS competitor blog post", competitor_id=competitor.id, competitor_name=competitor.name, project_id=project.id, profile_id=profile.id, ) blog_post_content = competitor.generate_vs_blog_post() logger.info( "VS competitor blog post generated successfully", competitor_id=competitor.id, competitor_name=competitor.name, content_length=len(blog_post_content), project_id=project.id, profile_id=profile.id, ) - return { - "status": "success", - "message": "VS blog post generated successfully!", - "competitor_id": competitor.id, - } - except Exception as e: logger.error( "Failed to generate competitor vs. blog post", error=str(e), exc_info=True, competitor_id=data.competitor_id, project_id=project.id, profile_id=profile.id, ) return { "status": "error", "message": f"Failed to generate competitor comparison blog post: {str(e)}", } + else: + return { + "status": "success", + "message": "VS blog post generated successfully!", + "competitor_id": competitor.id, + }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
poetry.lockis excluded by!**/*.lock
📒 Files selected for processing (22)
CHANGELOG.md(1 hunks)core/agent_system_prompts.py(2 hunks)core/agents.py(3 hunks)core/api/schemas.py(1 hunks)core/api/views.py(3 hunks)core/migrations/0038_competitor_blog_post.py(1 hunks)core/models.py(20 hunks)core/prompts.py(5 hunks)core/schemas.py(1 hunks)core/tasks.py(3 hunks)core/urls.py(2 hunks)core/utils.py(1 hunks)core/views.py(3 hunks)frontend/src/controllers/add_competitor_controller.js(1 hunks)frontend/src/controllers/competitor_analysis_controller.js(0 hunks)frontend/src/controllers/competitor_vs_post_controller.js(1 hunks)frontend/src/controllers/copy_markdown_controller.js(1 hunks)frontend/templates/base_project.html(1 hunks)frontend/templates/project/competitor_blog_post_detail.html(1 hunks)frontend/templates/project/project_competitors.html(1 hunks)pyproject.toml(1 hunks)requirements.txt(7 hunks)
💤 Files with no reviewable changes (1)
- frontend/src/controllers/competitor_analysis_controller.js
🧰 Additional context used
📓 Path-based instructions (15)
**/*.py
📄 CodeRabbit inference engine (.cursor/rules/backend.mdc)
**/*.py: Prioritize readability and maintainability; follow PEP 8
Use descriptive variable/function names with underscoresIn Python, use try-except blocks that catch specific exception types; do not catch broad Exception.
**/*.py: Follow PEP 8 for Python code style
Use descriptive variable names with underscores (snake_case) in Python
Prefer Django's built-in features over external libraries
**/*.py: Use structlog for logging in Python code (avoid print and the standard logging module)
Include contextual key-value fields in log messages (e.g., error=str(e), exc_info=True, profile_id=profile.id)
**/*.py: Use descriptive, full-word variable names; avoid abbreviations and single-letter variables in Python
Provide context in variable names when format or type matters (e.g., include_iso_format,date)
Extract unchanging values into UPPER_CASE constants
Use intermediary variables to name parsed groups instead of using index access directly
Naming conventions: use is_/has_/can_ for booleans; include 'date' for dates; snake_case for variables/functions; PascalCase for classes
Define variables close to where they are used to keep lifespan short
Name things after what they do, not how they're used; ensure names make sense without extra context
Avoid generic names like data, info, manager; use specific, intention-revealing names
Function names should include necessary context without being verbose
If naming is hard, split functions into smaller focused parts
Maintain consistency: reuse the same verbs and nouns for the same concepts; name variables after the functions that create them
Use more descriptive names for longer-lived variables
Avoid else statements by using guard clauses for early returns
Replace simple conditionals with direct assignment when both branches call the same function with different values
Use dictionaries as dispatch tables instead of multiple equal-probability elif chains
Validate input before processing to prevent errors propagating in...
Files:
core/agent_system_prompts.pycore/api/schemas.pycore/prompts.pycore/schemas.pycore/utils.pycore/views.pycore/api/views.pycore/migrations/0038_competitor_blog_post.pycore/urls.pycore/tasks.pycore/agents.pycore/models.py
core/**/*.py
📄 CodeRabbit inference engine (.cursor/rules/backend.mdc)
Leverage Django's ORM; avoid raw SQL when possible
Files:
core/agent_system_prompts.pycore/api/schemas.pycore/prompts.pycore/schemas.pycore/utils.pycore/views.pycore/api/views.pycore/migrations/0038_competitor_blog_post.pycore/urls.pycore/tasks.pycore/agents.pycore/models.py
frontend/src/controllers/**/*.js
📄 CodeRabbit inference engine (.cursor/rules/frontend.mdc)
frontend/src/controllers/**/*.js: Use Stimulus controllers to encapsulate JavaScript behavior and keep it separate from HTML structure
New controllers should be created in thefrontend/src/controllersdirectoryUse Stimulus.js controllers for interactive frontend behavior
Files:
frontend/src/controllers/add_competitor_controller.jsfrontend/src/controllers/competitor_vs_post_controller.jsfrontend/src/controllers/copy_markdown_controller.js
**/*.js
📄 CodeRabbit inference engine (.cursor/rules/stimulus-general.mdc)
**/*.js: Add semicolons at the end of statements in JavaScript files
Use double quotes instead of single quotes for string literals in JavaScript files
Files:
frontend/src/controllers/add_competitor_controller.jsfrontend/src/controllers/competitor_vs_post_controller.jsfrontend/src/controllers/copy_markdown_controller.js
{**/*.{css,scss},**/*_controller.@(js|ts)}
📄 CodeRabbit inference engine (.cursor/rules/ui-ux-design-guidelines.mdc)
Never create new styles without explicitly receiving permission to do so
Files:
frontend/src/controllers/add_competitor_controller.jsfrontend/src/controllers/competitor_vs_post_controller.jsfrontend/src/controllers/copy_markdown_controller.js
{**/*.{html,htm,css,scss},**/*_controller.@(js|ts)}
📄 CodeRabbit inference engine (.cursor/rules/ui-ux-design-guidelines.mdc)
Always favor the utility-first Tailwind approach; avoid creating reusable style classes and prefer reuse via template components
Files:
frontend/src/controllers/add_competitor_controller.jsfrontend/templates/project/competitor_blog_post_detail.htmlfrontend/src/controllers/competitor_vs_post_controller.jsfrontend/templates/base_project.htmlfrontend/src/controllers/copy_markdown_controller.jsfrontend/templates/project/project_competitors.html
frontend/src/controllers/**/*_controller.js
📄 CodeRabbit inference engine (.cursor/rules/stimulus-events.mdc)
frontend/src/controllers/**/*_controller.js: Dispatch a CustomEvent from the actor Stimulus controller to signal actions to other controllers
Name custom events descriptively and namespaced (e.g., "suggestion:move")
Set bubbles: true on CustomEvent so ancestor listeners (window/document) can receive it
Include all necessary payload in event.detail (e.g., element and destination)
Dispatch the event from the controller element (this.element.dispatchEvent(event))
In listener controllers, register global event listeners in connect() and remove them in disconnect()
Bind handler methods and keep a reference (e.g., this.boundMove = this.move.bind(this)) to correctly remove listeners
Attach listeners to window or document to catch bubbled custom events from anywhere on the page
Inspect event.detail in the handler and act conditionally based on its contents (e.g., match destination)
Files:
frontend/src/controllers/add_competitor_controller.jsfrontend/src/controllers/competitor_vs_post_controller.jsfrontend/src/controllers/copy_markdown_controller.js
**/*.html
📄 CodeRabbit inference engine (.cursor/rules/frontend.mdc)
**/*.html: Prefer Stimulus JS for adding interactivity to Django templates instead of raw script elements
Leverage Stimulus data attributes to connect HTML elements with JavaScript functionality
Utilize Stimulus targets to reference specific elements within a controller
Employ Stimulus actions to handle user interactions and events
Files:
frontend/templates/project/competitor_blog_post_detail.htmlfrontend/templates/base_project.htmlfrontend/templates/project/project_competitors.html
**/*.{html,htm}
📄 CodeRabbit inference engine (.cursor/rules/ui-ux-design-guidelines.mdc)
Always generate semantic HTML
Files:
frontend/templates/project/competitor_blog_post_detail.htmlfrontend/templates/base_project.htmlfrontend/templates/project/project_competitors.html
frontend/templates/**/*.html
📄 CodeRabbit inference engine (.cursor/rules/stimulus-events.mdc)
Avoid data-*-outlet links for sibling controller communication when using the event-based approach; keep controllers self-contained
Use semantic HTML elements (e.g., dialog, details/summary) in templates
Files:
frontend/templates/project/competitor_blog_post_detail.htmlfrontend/templates/base_project.htmlfrontend/templates/project/project_competitors.html
core/api/**/*.py
📄 CodeRabbit inference engine (.cursor/rules/backend.mdc)
core/api/**/*.py: django-ninja generate openapi documentation, so make sure to populate views with relevant data in order for it to show up in the OpenAPI docs.
Use Pydantic models for schema validation
Use django-ninja's authentication classesImplement REST APIs with django-ninja using Pydantic schemas under core/api/
Files:
core/api/schemas.pycore/api/views.py
**/{utils,repository}.py
📄 CodeRabbit inference engine (AGENTS.md)
Avoid generic filenames like utils.py and repository.py; use specific names instead
Files:
core/utils.py
core/views.py
📄 CodeRabbit inference engine (CLAUDE.md)
Keep Django views skinny: handle request/response only
Files:
core/views.py
core/tasks.py
📄 CodeRabbit inference engine (CLAUDE.md)
Define background tasks using django-q2 in core/tasks.py
Files:
core/tasks.py
core/models.py
📄 CodeRabbit inference engine (CLAUDE.md)
core/models.py: Place business logic in Django models (fat models pattern)
Validate simple constraints in the database and place complex domain logic in Django models
Files:
core/models.py
🧠 Learnings (5)
📚 Learning: 2025-10-04T08:52:37.437Z
Learnt from: CR
Repo: rasulkireev/TuxSEO PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-10-04T08:52:37.437Z
Learning: Applies to frontend/src/controllers/**/*.js : Use Stimulus.js controllers for interactive frontend behavior
Applied to files:
frontend/src/controllers/add_competitor_controller.jsfrontend/src/controllers/competitor_vs_post_controller.jsfrontend/src/controllers/copy_markdown_controller.js
📚 Learning: 2025-10-04T08:52:37.437Z
Learnt from: CR
Repo: rasulkireev/TuxSEO PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-10-04T08:52:37.437Z
Learning: Applies to core/agents/**/*.py : Implement AI agents using pydantic-ai under core/agents/
Applied to files:
CHANGELOG.mdcore/agents.pycore/models.py
📚 Learning: 2025-10-04T08:52:58.590Z
Learnt from: CR
Repo: rasulkireev/TuxSEO PR: 0
File: .cursor/rules/agent-rules.mdc:0-0
Timestamp: 2025-10-04T08:52:58.590Z
Learning: Always add AGENTS.md into AI context
Applied to files:
core/agents.py
📚 Learning: 2025-10-04T08:52:58.590Z
Learnt from: CR
Repo: rasulkireev/TuxSEO PR: 0
File: .cursor/rules/agent-rules.mdc:0-0
Timestamp: 2025-10-04T08:52:58.590Z
Learning: Always add AGENTS.md into context
Applied to files:
core/agents.py
📚 Learning: 2025-10-04T08:52:37.437Z
Learnt from: CR
Repo: rasulkireev/TuxSEO PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-10-04T08:52:37.437Z
Learning: Applies to core/tasks.py : Define background tasks using django-q2 in core/tasks.py
Applied to files:
core/models.py
🪛 Ruff (0.14.2)
core/agent_system_prompts.py
36-36: Unused noqa directive (non-enabled: E501)
Remove unused noqa directive
(RUF100)
core/prompts.py
21-21: Unused noqa directive (non-enabled: E501)
Remove unused noqa directive
(RUF100)
61-61: Unused noqa directive (non-enabled: E501)
Remove unused noqa directive
(RUF100)
148-148: Unused noqa directive (non-enabled: E501)
Remove unused noqa directive
(RUF100)
195-195: Unused noqa directive (non-enabled: E501)
Remove unused noqa directive
(RUF100)
227-227: Unused noqa directive (non-enabled: E501)
Remove unused noqa directive
(RUF100)
core/api/views.py
576-576: Unused noqa directive (non-enabled: E501)
Remove unused noqa directive
(RUF100)
652-652: Unused noqa directive (non-enabled: E501)
Remove unused noqa directive
(RUF100)
658-658: Unused noqa directive (non-enabled: E501)
Remove unused noqa directive
(RUF100)
681-685: Consider moving this statement to an else block
(TRY300)
698-698: Use explicit conversion flag
Replace with conversion flag
(RUF010)
core/migrations/0038_competitor_blog_post.py
8-10: Mutable class attributes should be annotated with typing.ClassVar
(RUF012)
12-18: Mutable class attributes should be annotated with typing.ClassVar
(RUF012)
core/tasks.py
249-249: Unused noqa directive (non-enabled: E501)
Remove unused noqa directive
(RUF100)
266-266: Use explicit conversion flag
Replace with conversion flag
(RUF010)
core/agents.py
208-208: Unused noqa directive (non-enabled: E501)
Remove unused noqa directive
(RUF100)
core/models.py
750-750: Unused function argument: ctx
(ARG001)
1622-1622: Unused noqa directive (non-enabled: E501)
Remove unused noqa directive
(RUF100)
🔇 Additional comments (12)
core/utils.py (1)
207-207: LGTM! Correct API update for pydantic-ai 1.9.1.The change from
result.datatoresult.outputcorrectly reflects the API changes in pydantic-ai's upgrade from 0.2.9 to 1.9.1. This aligns with the OpenAIChatModel migration mentioned in the AI summary.pyproject.toml (1)
38-38: LGTM! Major version upgrade from 0.2.9 to 1.9.1.This major version upgrade of pydantic-ai is intentional and necessary for the new competitor-focused content generation features. The codebase has been updated accordingly (e.g.,
result.outputinstead ofresult.datain core/utils.py).core/tasks.py (1)
192-212: LGTM! Clear documentation of the cost-saving workflow.The updated docstring accurately describes the new workflow: finding competitors via Perplexity, fetching homepage content, and populating basic details, while explicitly noting that full competitor analysis is skipped to save costs and VS blog post generation is manual.
frontend/templates/base_project.html (1)
107-117: LGTM! Consistent navigation item addition.The new "Competitors" navigation item follows the established pattern for sidebar menu items, including proper active state styling, appropriate SVG icon, and correct URL routing to
project_competitors. The placement between "Your Pages" and "Publish History" is logical.core/api/schemas.py (1)
134-142: LGTM! Well-structured API schemas.The new
GenerateCompetitorVsTitleInandGenerateCompetitorVsTitleOutschemas follow the established naming conventions and patterns in this file. The field types are appropriate, and the optionalcompetitor_idin the output schema allows for flexible error handling.core/agent_system_prompts.py (1)
65-70: LGTM: Generic RunContext improves reusability.The signature change from
RunContext[BlogPostGenerationContext]to genericRunContextwith a clear docstring is an excellent improvement that allows this function to work with multiple context types (BlogPostGenerationContext, CompetitorVsPostContext, etc.) while maintaining type safety.core/views.py (1)
824-862: LGTM!The implementation properly isolates competitors by project and user profile, and exposes the necessary context for free-plan limits and analytics in the UI.
frontend/templates/project/project_competitors.html (2)
85-159: LGTM!The modal form is properly structured with CSRF protection, accessible ARIA attributes, semantic HTML, and appropriate input validation (required + type="url").
201-290: LGTM!The competitors table properly handles conditional states (blog post exists vs. not, free plan limits), includes accessible markup, and integrates cleanly with Stimulus controllers for interactivity.
core/agents.py (1)
91-151: LGTM!The competitor VS title agent is properly configured with Perplexity Sonar for web-search capabilities, clear system prompts, and appropriate context building. The previously flagged unused import issue has been resolved.
core/models.py (2)
275-327: LGTM!The competitor limit properties follow the established pattern for other plan-based limits and correctly count competitors and generated posts across all user projects.
1432-1434: LGTM!The
blog_postfield is appropriately defined as a TextField with sensible defaults for storing generated markdown content.
| <!-- Blog Post Content --> | ||
| {% if competitor.blog_post %} | ||
| <!-- Copy Action --> | ||
| <div class="flex justify-end mb-4" data-controller="copy-markdown" data-copy-markdown-content-value="{{ competitor.blog_post }}"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Escape or JSON-encode the blog_post content for the data attribute.
Embedding competitor.blog_post directly into a data attribute can break HTML if the content contains double quotes, newlines, or other special characters. While Django auto-escapes by default, for complex text content in data attributes, use the escapejs filter or Django's json_script tag to ensure proper encoding.
Apply this diff to properly escape the content:
- <div class="flex justify-end mb-4" data-controller="copy-markdown" data-copy-markdown-content-value="{{ competitor.blog_post }}">
+ <div class="flex justify-end mb-4" data-controller="copy-markdown" data-copy-markdown-content-value="{{ competitor.blog_post|escapejs }}">📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <div class="flex justify-end mb-4" data-controller="copy-markdown" data-copy-markdown-content-value="{{ competitor.blog_post }}"> | |
| <div class="flex justify-end mb-4" data-controller="copy-markdown" data-copy-markdown-content-value="{{ competitor.blog_post|escapejs }}"> |
This pull request contains changes generated by a Cursor Cloud Agent
Summary by CodeRabbit
New Features
UI
Chores