Skip to content

Generation Pipeline Revamp#115

Open
rasulkireev wants to merge 4 commits intomainfrom
generation-pipeline-revamp
Open

Generation Pipeline Revamp#115
rasulkireev wants to merge 4 commits intomainfrom
generation-pipeline-revamp

Conversation

@rasulkireev
Copy link
Owner

@rasulkireev rasulkireev commented Nov 8, 2025

Summary by CodeRabbit

  • New Features

    • Multi-step blog generation pipeline with per-step progress, retries, retry UI, and a visible progress dialog; "Continue" resumes in-progress posts and shows a final "View Post" link when ready.
  • Removed Features

    • Content type tabs and selection removed — flows now default to SEO.
    • Content validation warning UI and Posted Suggestions panel removed; suggestion cards now show "Continue" or "View Post" based on readiness.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 8, 2025

Walkthrough

A modular rewrite replaces monolithic agents with per-responsibility agent modules and a stateful, multi-step blog-generation pipeline (structure → content → validation → internal links → finalization), updates schemas, migrations and models to track pipeline state/metadata, and adds frontend controllers/templates to drive the pipeline UI.

Changes

Cohort / File(s) Summary
Agent modularization
core/agents.py (deleted), core/agents/...
core/agents/analyze_project_agent.py, core/agents/blog_structure_agent.py, core/agents/competitor_finder_agent.py, core/agents/competitor_vs_blog_post_agent.py, core/agents/content_editor_agent.py, core/agents/content_validation_agent.py, core/agents/fix_validation_issue_agent.py, core/agents/internal_links_agent.py, core/agents/seo_content_generator_agent.py, core/agents/summarize_page_agent.py, core/agents/title_suggestions_agent.py, core/agents/system_prompts.py
Removed monolithic core/agents.py; added many per-step agent modules with configured models, prompts, and helper prompt functions (including new inline_link_formatting() helper).
Schemas & API
core/schemas.py, core/api/schemas.py
Added pipeline and content-structure schemas (BlogPostSection, BlogPostStructure, InternalLinkContext, ContentValidationContext/Result, ContentFixContext), added is_on_free_plan to ProjectDetails, removed competitor-vs title context, narrowed generation context to SEO-only; API schemas added pipeline response types and removed legacy In/Out schemas.
Models, migrations & pipeline state
core/models.py, core/migrations/0042_*.py, core/migrations/0043_*.py
Added GeneratedBlogPost fields (generation_structure, pipeline_metadata, pipeline_state, raw_content) and pipeline lifecycle methods (initialize/update/status/retry/execute); removed legacy validation fields; migrations updated accordingly.
Endpoint refactor
core/api/views.py
Replaced legacy content endpoints with pipeline endpoints (start, step, status, retry, complete); removed legacy generate/fix/competitor endpoints and ContentType-driven flows.
Enums, constants & prompts
core/choices.py, core/constants.py, core/prompts.py
Removed SHARING from ContentType; removed PLACEHOLDER_PATTERNS and PLACEHOLDER_BRACKET_PATTERNS; deleted large prompt constants (TITLE_SUGGESTION_SYSTEM_PROMPTS, GENERATE_CONTENT_SYSTEM_PROMPTS, PRICING_PAGE_STRATEGY_SYSTEM_PROMPT).
Backend tasks & utils
core/tasks.py, core/utils.py
Switched generation flows to pipeline (removed content_type usage), removed placeholder/ending/header validators, and added find_internal_link_opportunities() and insert_link_at_position().
Frontend: pipeline controllers
frontend/src/controllers/blog_generation_pipeline_controller.js, frontend/src/controllers/generate-content-controller.js, frontend/src/controllers/content_idea_controller.js, frontend/src/controllers/scan_progress_controller.js, frontend/src/controllers/title_suggestions_controller.js, (deleted) frontend/src/controllers/fix-blog-post-controller.js
Added a Stimulus controller for multi-step pipeline UI (start, per-step execute, retry, progress, complete); updated other controllers to default to SEO and to support pipeline resume/continue; removed fix-blog-post controller.
Frontend templates & components
frontend/templates/blog/generated_blog_post_detail.html, frontend/templates/components/blog_post_suggestion_card.html, frontend/templates/components/blog_post_validation_warning.html (deleted), frontend/templates/project/project_detail.html
Removed content validation warning fragment and Content Type tabs; suggestion card now uses is_ready_to_view and shows "Continue" for in-progress posts; UI simplifications to match pipeline.
Changelog
CHANGELOG.md
Added v0.0.8 entry: "Completely redesign of how content gets generated."

Sequence Diagram(s)

sequenceDiagram
    participant UI as Frontend UI
    participant API as Pipeline API
    participant Agents as Agent Modules
    participant DB as Database

    UI->>API: POST /pipeline/start (suggestion_id)
    API->>DB: create pipeline_state (initialized)
    API-->>UI: PipelineStartOut

    loop steps: structure → content → preliminary_validation → internal_links → final_validation
        UI->>API: POST /pipeline/step (pipeline_id, step_name)
        API->>Agents: invoke agent for step_name
        Agents-->>API: step_result (output, needs_fix?)
        API->>DB: update pipeline_state & pipeline_metadata
        API-->>UI: PipelineStepOut (status, result)
        alt needs_fix
            UI->>API: POST /pipeline/retry (pipeline_id, step_name)
            API->>Agents: invoke fix agent / re-run step
            Agents-->>API: fixed_result
            API->>DB: update retry count/status
            API-->>UI: PipelineRetryOut
        end
    end

    UI->>API: POST /pipeline/complete (pipeline_id)
    API->>DB: finalize pipeline_state, persist raw_content
    API-->>UI: PipelineCompleteOut (post_id, view_url)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60–90 minutes

Areas to focus on:

  • core/models.py: pipeline state machine, DB writes, defaults, and removal of legacy fields referenced elsewhere.
  • Agent modules: correct deps_type/output_type, prompt wiring, and provider/model settings (Perplexity/OpenAI integrations).
  • API views and frontend controllers: step-name mapping, CSRF handling, retry semantics, and error flows.
  • Migrations 0042/0043: defaults, nullability, and ensuring no stale references to removed fields/constants.

Possibly related PRs

Poem

🐰 I hopped through prompts and tidy queues,

Carrots of structure, content, fixes, news,
I nudge each step until the post is right,
Retries like tiny paws in the night,
Hop!—the pipeline blooms, a post in sight.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 63.16% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Generation Pipeline Revamp' accurately reflects the main objective of the PR, which is a comprehensive redesign of how content gets generated through a multi-step pipeline architecture replacing the legacy system.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch generation-pipeline-revamp

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@code-review-doctor code-review-doctor bot left a comment

Choose a reason for hiding this comment

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

Looks good. Worth considering though. View full project report here.

@@ -1,3 +1,4 @@
import json

Choose a reason for hiding this comment

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

There's a lot of models here. Consider splitting into multiple files. More info.

Pro model prefix is used quite a lot. Maybe split the models into multiple files. Explained here.

Copy link
Contributor

@greptile-apps greptile-apps bot left a 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

This PR implements a comprehensive revamp of the blog post generation system, transforming it from a single-step process into a sophisticated multi-step pipeline with validation and retry capabilities.

Key Changes

Architecture Improvements:

  • Refactored monolithic core/agents.py (233 lines) into focused, single-responsibility agent modules in core/agents/ directory
  • Extracted system prompts into reusable functions in core/agents/system_prompts.py
  • Removed deprecated files: core/prompts.py, core/constants.py

Pipeline Implementation:

  • Added 5-step generation pipeline: structure → content → preliminary validation → internal links → final validation
  • Implemented pipeline state tracking with JSON-based status, retry counts, and error messages in GeneratedBlogPost model
  • Added pipeline_state, generation_structure, raw_content, and pipeline_metadata fields via migrations

New AI Agents:

  • blog_structure_agent.py: Generates comprehensive outline before content creation
  • content_validation_agent.py: AI-powered validation replacing rule-based checks
  • fix_validation_issue_agent.py: Automatically fixes validation issues
  • internal_links_agent.py: Intelligently inserts internal links to project pages

API & Frontend:

  • Added 5 new pipeline endpoints: /start, /step/{step_name}, /status, /retry/{step_name}, /complete
  • New Stimulus controller blog_generation_pipeline_controller.js for real-time pipeline progress UI
  • Removed old single-step endpoints: /generate-blog-content, /fix-generated-blog-post, /generate-competitor-vs-title

Simplified Content Type:

  • Removed ContentType.SHARING option, keeping only ContentType.SEO
  • Updated all title generation to use SEO-focused approach

Benefits

This refactoring significantly improves:

  • Code maintainability: Modular agents easier to test and modify
  • Content quality: Structure-first approach and AI validation ensure better output
  • User experience: Progressive UI with retry capability on failures
  • Flexibility: Each pipeline step can be executed independently or as a complete flow

Confidence Score: 4/5

  • This PR is safe to merge with minor spelling corrections needed
  • Score reflects excellent architectural improvements and comprehensive implementation, but two spelling errors need correction before merge: 'gnerate' in system_prompts.py:119 and 'NICH_AUDIENCE' in choices.py:10. The refactoring follows Django best practices with proper error handling, logging, and database migrations. The multi-step pipeline design is well-structured with retry logic and state management. No logical errors or security issues detected.
  • core/agents/system_prompts.py and core/choices.py require spelling corrections

Important Files Changed

File Analysis

Filename Score Overview
core/agents/blog_structure_agent.py 5/5 New agent for generating blog post structure/outline, well-documented system prompt with clear guidelines
core/agents/seo_content_generator_agent.py 5/5 Comprehensive SEO content generation agent with structure-based guidance, properly configured with high token limits
core/agents/system_prompts.py 5/5 Centralized reusable system prompt functions, promotes DRY principle and consistency across agents
core/models.py 4/5 Major refactoring to implement multi-step pipeline: extracted agent logic to separate modules, added pipeline state tracking with retry logic, new methods for structure generation and validation steps
core/api/views.py 4/5 Removed old generate-blog-content endpoint, added new pipeline endpoints (start, step execution, status, retry, complete) with comprehensive error handling and logging
frontend/src/controllers/blog_generation_pipeline_controller.js 4/5 New Stimulus controller for managing UI during multi-step pipeline execution with progress tracking, retry logic, and visual step status updates
core/tasks.py 5/5 Updated to use new pipeline method (execute_complete_pipeline) instead of old generate_content, removed ContentType randomization
core/schemas.py 5/5 Added new schemas for pipeline (BlogPostStructure, BlogPostSection, InternalLinkContext, ContentValidationResult, ContentFixContext), updated ProjectDetails with is_on_free_plan field
core/choices.py 4/5 Removed ContentType.SHARING option, keeping only SEO type; contains spelling error in NICH_AUDIENCE constant

Sequence Diagram

sequenceDiagram
    participant User
    participant UI as Stimulus Controller
    participant API as Django API
    participant Model as BlogPostTitleSuggestion
    participant Agent as AI Agents
    participant DB as Database

    User->>UI: Click Generate Blog Post
    UI->>API: POST /pipeline/{id}/start
    API->>Model: start_generation_pipeline()
    Model->>DB: Create GeneratedBlogPost
    Model->>DB: initialize_pipeline()
    DB-->>Model: Pipeline state initialized
    Model-->>API: Return blog_post_id
    API-->>UI: Return success + blog_post_id
    
    loop For each pipeline step
        UI->>API: POST /pipeline/{id}/step/{step_name}
        
        alt Step: generate_structure
            API->>Model: generate_structure(blog_post)
            Model->>Agent: blog_structure_agent.run()
            Agent-->>Model: BlogPostStructure
            Model->>DB: Save structure as JSON
            Model->>DB: update_pipeline_step("completed")
        else Step: generate_content
            API->>Model: generate_content_from_structure(blog_post)
            Model->>DB: Load structure
            Model->>Agent: seo_content_generator_agent.run()
            Agent-->>Model: Generated content
            Model->>DB: Save content
            Model->>DB: update_pipeline_step("completed")
        else Step: preliminary_validation
            API->>Model: run_preliminary_validation(blog_post)
            Model->>Agent: content_validation_agent.run()
            Agent-->>Model: ValidationResult
            alt Validation fails
                Model->>DB: update_pipeline_step("failed", issues)
                Model-->>API: Return needs_fix=true
                UI->>API: POST /pipeline/{id}/step/fix_preliminary_validation
                API->>Model: fix_preliminary_validation(blog_post, issues)
                Model->>Agent: fix_validation_issue_agent.run()
                Agent-->>Model: Fixed content
                Model->>DB: Save fixed content
            else Validation passes
                Model->>DB: update_pipeline_step("completed")
            end
        else Step: insert_internal_links
            API->>Model: insert_internal_links(blog_post)
            Model->>Agent: internal_links_agent.run()
            Agent-->>Model: Content with links
            Model->>DB: Save updated content
            Model->>DB: update_pipeline_step("completed")
        else Step: final_validation
            API->>Model: run_final_validation(blog_post)
            Model->>Agent: content_validation_agent.run()
            Agent-->>Model: ValidationResult
            alt Validation fails
                Model->>DB: update_pipeline_step("failed", issues)
                Model-->>API: Return needs_fix=true
                UI->>API: POST /pipeline/{id}/step/fix_final_validation
                API->>Model: fix_final_validation(blog_post, issues)
                Model->>Agent: fix_validation_issue_agent.run()
                Agent-->>Model: Fixed content
                Model->>DB: Save fixed content
                Model->>DB: Re-run validation
            else Validation passes
                Model->>DB: update_pipeline_step("completed")
            end
        end
        
        API-->>UI: Return step result + pipeline state
        UI->>UI: Update progress UI
    end
    
    UI->>User: Show completion + link to blog post
Loading

Additional Comments (2)

  1. core/agents/system_prompts.py, line 119 (link)

    syntax: gnerate is mis-spelled

  2. core/choices.py, line 10 (link)

    syntax: NICH_AUDIENCE is mis-spelled

35 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 16

🧹 Nitpick comments (6)
frontend/src/controllers/content_idea_controller.js (1)

14-17: Consider simplifying the redundant method.

Since getCurrentTab() now always returns "SEO", the method is redundant. Consider one of these options:

  1. Replace with a constant and use it directly:
-  getCurrentTab() {
-    // Always return SEO
-    return "SEO";
-  }
+  static get CONTENT_TYPE() {
+    return "SEO";
+  }

Then update line 24:

-    const contentType = this.getCurrentTab();
+    const contentType = this.constructor.CONTENT_TYPE;
  1. Or inline the value directly at line 24 since it's now a constant:
-    const contentType = this.getCurrentTab();
+    const contentType = "SEO";

And remove the getCurrentTab() method entirely.

core/agents/blog_structure_agent.py (1)

39-39: Remove unused noqa directive.

The noqa: E501 directive on line 39 is unused since E501 is not enabled in your Ruff configuration.

Apply this diff:

-    """,  # noqa: E501
+    """,
core/agents/fix_validation_issue_agent.py (1)

27-27: Remove unused noqa directive.

The noqa: E501 directive on line 27 is unused since E501 is not enabled in your Ruff configuration.

Apply this diff:

-    """,  # noqa: E501
+    """,
core/api/views.py (1)

1077-1077: Consider using f-string conversion for explicit str() calls.

Static analysis suggests using explicit conversion flags in f-strings (e.g., f"{error}" instead of str(error)) for clarity.

Example for line 1077:

-            "message": f"Failed to start pipeline: {str(error)}",
+            "message": f"Failed to start pipeline: {error}",

Apply similar changes to lines 1206 and 1349.

Also applies to: 1206-1206, 1349-1349

core/models.py (2)

819-819: Consider handling JSON decode errors explicitly.

If blog_post.generation_structure contains invalid JSON, json.loads() will raise JSONDecodeError. This would be caught by the general exception handler, but explicitly catching and logging JSON decode errors would provide clearer diagnostics.

         try:
             # Load the structure
-            structure_dict = json.loads(blog_post.generation_structure)
+            try:
+                structure_dict = json.loads(blog_post.generation_structure)
+            except json.JSONDecodeError as json_error:
+                logger.error(
+                    "[Pipeline] Invalid JSON in generation_structure",
+                    blog_post_id=blog_post.id,
+                    error=str(json_error),
+                )
+                raise ValueError(f"Invalid structure JSON: {json_error}") from json_error

1504-1504: Add explicit type hint for optional parameter.

Static analysis suggests making the Optional type explicit per PEP 484.

-    def update_pipeline_step(self, step_name: str, status: str, error: str = None):
+    def update_pipeline_step(self, step_name: str, status: str, error: str | None = None):
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 06ffc29 and d522819.

📒 Files selected for processing (35)
  • CHANGELOG.md (1 hunks)
  • core/agents.py (0 hunks)
  • core/agents/analyze_project_agent.py (1 hunks)
  • core/agents/blog_structure_agent.py (1 hunks)
  • core/agents/competitor_finder_agent.py (1 hunks)
  • core/agents/competitor_vs_blog_post_agent.py (1 hunks)
  • core/agents/content_editor_agent.py (1 hunks)
  • core/agents/content_validation_agent.py (1 hunks)
  • core/agents/fix_validation_issue_agent.py (1 hunks)
  • core/agents/internal_links_agent.py (1 hunks)
  • core/agents/seo_content_generator_agent.py (1 hunks)
  • core/agents/summarize_page_agent.py (1 hunks)
  • core/agents/system_prompts.py (1 hunks)
  • core/agents/title_suggestions_agent.py (1 hunks)
  • core/api/schemas.py (2 hunks)
  • core/api/views.py (5 hunks)
  • core/choices.py (0 hunks)
  • core/constants.py (0 hunks)
  • core/migrations/0042_generatedblogpost_generation_structure_and_more.py (1 hunks)
  • core/migrations/0043_remove_generatedblogpost_content_too_short_and_more.py (1 hunks)
  • core/models.py (17 hunks)
  • core/prompts.py (0 hunks)
  • core/schemas.py (3 hunks)
  • core/tasks.py (4 hunks)
  • core/utils.py (2 hunks)
  • frontend/src/controllers/blog_generation_pipeline_controller.js (1 hunks)
  • frontend/src/controllers/content_idea_controller.js (1 hunks)
  • frontend/src/controllers/fix-blog-post-controller.js (0 hunks)
  • frontend/src/controllers/generate-content-controller.js (4 hunks)
  • frontend/src/controllers/scan_progress_controller.js (1 hunks)
  • frontend/src/controllers/title_suggestions_controller.js (1 hunks)
  • frontend/templates/blog/generated_blog_post_detail.html (0 hunks)
  • frontend/templates/components/blog_post_suggestion_card.html (3 hunks)
  • frontend/templates/components/blog_post_validation_warning.html (0 hunks)
  • frontend/templates/project/project_detail.html (0 hunks)
💤 Files with no reviewable changes (8)
  • frontend/src/controllers/fix-blog-post-controller.js
  • frontend/templates/components/blog_post_validation_warning.html
  • core/choices.py
  • frontend/templates/project/project_detail.html
  • frontend/templates/blog/generated_blog_post_detail.html
  • core/prompts.py
  • core/constants.py
  • core/agents.py
🧰 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 underscores

In 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/agents/system_prompts.py
  • core/agents/internal_links_agent.py
  • core/agents/fix_validation_issue_agent.py
  • core/agents/analyze_project_agent.py
  • core/agents/content_validation_agent.py
  • core/agents/blog_structure_agent.py
  • core/agents/content_editor_agent.py
  • core/migrations/0043_remove_generatedblogpost_content_too_short_and_more.py
  • core/agents/seo_content_generator_agent.py
  • core/agents/competitor_vs_blog_post_agent.py
  • core/agents/summarize_page_agent.py
  • core/api/schemas.py
  • core/migrations/0042_generatedblogpost_generation_structure_and_more.py
  • core/api/views.py
  • core/agents/title_suggestions_agent.py
  • core/tasks.py
  • core/schemas.py
  • core/agents/competitor_finder_agent.py
  • core/utils.py
  • core/models.py
core/**/*.py

📄 CodeRabbit inference engine (.cursor/rules/backend.mdc)

Leverage Django's ORM; avoid raw SQL when possible

Files:

  • core/agents/system_prompts.py
  • core/agents/internal_links_agent.py
  • core/agents/fix_validation_issue_agent.py
  • core/agents/analyze_project_agent.py
  • core/agents/content_validation_agent.py
  • core/agents/blog_structure_agent.py
  • core/agents/content_editor_agent.py
  • core/migrations/0043_remove_generatedblogpost_content_too_short_and_more.py
  • core/agents/seo_content_generator_agent.py
  • core/agents/competitor_vs_blog_post_agent.py
  • core/agents/summarize_page_agent.py
  • core/api/schemas.py
  • core/migrations/0042_generatedblogpost_generation_structure_and_more.py
  • core/api/views.py
  • core/agents/title_suggestions_agent.py
  • core/tasks.py
  • core/schemas.py
  • core/agents/competitor_finder_agent.py
  • core/utils.py
  • core/models.py
core/agents/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

Implement AI agents using pydantic-ai under core/agents/

Files:

  • core/agents/system_prompts.py
  • core/agents/internal_links_agent.py
  • core/agents/fix_validation_issue_agent.py
  • core/agents/analyze_project_agent.py
  • core/agents/content_validation_agent.py
  • core/agents/blog_structure_agent.py
  • core/agents/content_editor_agent.py
  • core/agents/seo_content_generator_agent.py
  • core/agents/competitor_vs_blog_post_agent.py
  • core/agents/summarize_page_agent.py
  • core/agents/title_suggestions_agent.py
  • core/agents/competitor_finder_agent.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 the frontend/src/controllers directory

Use Stimulus.js controllers for interactive frontend behavior

Files:

  • frontend/src/controllers/scan_progress_controller.js
  • frontend/src/controllers/title_suggestions_controller.js
  • frontend/src/controllers/generate-content-controller.js
  • frontend/src/controllers/blog_generation_pipeline_controller.js
  • frontend/src/controllers/content_idea_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/scan_progress_controller.js
  • frontend/src/controllers/title_suggestions_controller.js
  • frontend/src/controllers/generate-content-controller.js
  • frontend/src/controllers/blog_generation_pipeline_controller.js
  • frontend/src/controllers/content_idea_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/scan_progress_controller.js
  • frontend/src/controllers/title_suggestions_controller.js
  • frontend/src/controllers/blog_generation_pipeline_controller.js
  • frontend/src/controllers/content_idea_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/scan_progress_controller.js
  • frontend/src/controllers/title_suggestions_controller.js
  • frontend/templates/components/blog_post_suggestion_card.html
  • frontend/src/controllers/blog_generation_pipeline_controller.js
  • frontend/src/controllers/content_idea_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/scan_progress_controller.js
  • frontend/src/controllers/title_suggestions_controller.js
  • frontend/src/controllers/blog_generation_pipeline_controller.js
  • frontend/src/controllers/content_idea_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/components/blog_post_suggestion_card.html
**/*.{html,htm}

📄 CodeRabbit inference engine (.cursor/rules/ui-ux-design-guidelines.mdc)

Always generate semantic HTML

Files:

  • frontend/templates/components/blog_post_suggestion_card.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/components/blog_post_suggestion_card.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 classes

Implement REST APIs with django-ninja using Pydantic schemas under core/api/

Files:

  • core/api/schemas.py
  • core/api/views.py
core/tasks.py

📄 CodeRabbit inference engine (CLAUDE.md)

Define background tasks using django-q2 in core/tasks.py

Files:

  • core/tasks.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/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: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/internal_links_agent.py
  • core/agents/fix_validation_issue_agent.py
  • core/agents/analyze_project_agent.py
  • core/agents/content_validation_agent.py
  • core/agents/blog_structure_agent.py
  • core/agents/content_editor_agent.py
  • core/agents/seo_content_generator_agent.py
  • core/agents/competitor_vs_blog_post_agent.py
  • core/agents/summarize_page_agent.py
  • core/agents/title_suggestions_agent.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/internal_links_agent.py
  • core/agents/fix_validation_issue_agent.py
  • core/agents/competitor_vs_blog_post_agent.py
  • core/agents/summarize_page_agent.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/analyze_project_agent.py
  • core/agents/content_validation_agent.py
  • core/agents/blog_structure_agent.py
  • core/agents/competitor_vs_blog_post_agent.py
  • core/agents/summarize_page_agent.py
  • core/agents/competitor_finder_agent.py
  • core/utils.py
  • core/models.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 frontend/src/controllers/**/*.js : Use Stimulus.js controllers for interactive frontend behavior

Applied to files:

  • frontend/src/controllers/blog_generation_pipeline_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/tasks.py : Define background tasks using django-q2 in core/tasks.py

Applied to files:

  • core/tasks.py
  • core/models.py
🧬 Code graph analysis (20)
frontend/src/controllers/scan_progress_controller.js (1)
frontend/src/controllers/content_idea_controller.js (1)
  • contentType (24-24)
core/agents/internal_links_agent.py (3)
core/agents/system_prompts.py (1)
  • inline_link_formatting (165-179)
core/choices.py (1)
  • get_default_ai_model (135-137)
core/schemas.py (1)
  • InternalLinkContext (312-318)
core/agents/fix_validation_issue_agent.py (2)
core/choices.py (1)
  • get_default_ai_model (135-137)
core/schemas.py (1)
  • ContentFixContext (331-337)
core/agents/analyze_project_agent.py (3)
core/agents/system_prompts.py (1)
  • add_webpage_content (156-162)
core/choices.py (1)
  • get_default_ai_model (135-137)
core/schemas.py (2)
  • ProjectDetails (17-102)
  • WebPageContent (11-14)
core/agents/content_validation_agent.py (2)
core/choices.py (1)
  • get_default_ai_model (135-137)
core/schemas.py (1)
  • ContentValidationResult (321-328)
core/agents/blog_structure_agent.py (3)
core/agents/system_prompts.py (5)
  • add_language_specification (132-137)
  • add_project_details (49-62)
  • add_project_pages (65-113)
  • add_target_keywords (140-153)
  • add_title_details (116-129)
core/choices.py (1)
  • get_default_ai_model (135-137)
core/schemas.py (2)
  • BlogPostGenerationContext (185-192)
  • BlogPostStructure (292-309)
frontend/src/controllers/generate-content-controller.js (2)
frontend/src/controllers/blog_generation_pipeline_controller.js (6)
  • step (65-65)
  • step (133-133)
  • response (48-53)
  • response (89-97)
  • data (55-55)
  • data (99-99)
frontend/src/utils/messages.js (1)
  • showMessage (2-39)
core/agents/content_editor_agent.py (3)
core/agents/system_prompts.py (6)
  • add_language_specification (132-137)
  • add_project_details (49-62)
  • add_project_pages (65-113)
  • add_target_keywords (140-153)
  • add_title_details (116-129)
  • inline_link_formatting (165-179)
core/choices.py (1)
  • get_default_ai_model (135-137)
core/schemas.py (1)
  • BlogPostGenerationContext (185-192)
core/agents/seo_content_generator_agent.py (3)
core/agents/system_prompts.py (9)
  • add_language_specification (132-137)
  • add_project_details (49-62)
  • add_target_keywords (140-153)
  • add_title_details (116-129)
  • add_todays_date (7-8)
  • filler_content (39-46)
  • inline_link_formatting (165-179)
  • markdown_lists (30-36)
  • post_structure (22-27)
core/choices.py (1)
  • get_default_ai_model (135-137)
core/schemas.py (2)
  • BlogPostGenerationContext (185-192)
  • GeneratedBlogPostSchema (195-205)
frontend/src/controllers/blog_generation_pipeline_controller.js (3)
frontend/src/controllers/content_idea_controller.js (2)
  • response (36-47)
  • data (53-53)
frontend/src/controllers/generate-content-controller.js (4)
  • response (311-316)
  • data (323-323)
  • error (69-69)
  • error (319-319)
frontend/src/utils/messages.js (1)
  • showMessage (2-39)
core/agents/competitor_vs_blog_post_agent.py (3)
core/agents/system_prompts.py (3)
  • add_project_pages (65-113)
  • inline_link_formatting (165-179)
  • markdown_lists (30-36)
core/choices.py (1)
  • AIModel (127-129)
core/schemas.py (1)
  • CompetitorVsPostContext (260-273)
core/agents/summarize_page_agent.py (3)
core/agents/system_prompts.py (1)
  • add_webpage_content (156-162)
core/choices.py (1)
  • get_default_ai_model (135-137)
core/schemas.py (2)
  • ProjectPageDetails (105-132)
  • WebPageContent (11-14)
core/api/schemas.py (1)
core/choices.py (1)
  • ContentType (4-5)
core/api/views.py (2)
core/api/schemas.py (5)
  • PipelineCompleteOut (286-290)
  • PipelineRetryOut (280-283)
  • PipelineStartOut (257-261)
  • PipelineStatusOut (272-277)
  • PipelineStepOut (264-269)
core/models.py (7)
  • BlogPostTitleSuggestion (648-1275)
  • start_generation_pipeline (694-720)
  • get_pipeline_status (1564-1584)
  • GeneratedBlogPost (1316-1592)
  • can_retry_step (1586-1592)
  • update_pipeline_step (1504-1562)
  • execute_complete_pipeline (1200-1275)
core/agents/title_suggestions_agent.py (3)
core/agents/system_prompts.py (3)
  • add_todays_date (7-8)
  • add_project_details (49-62)
  • add_language_specification (132-137)
core/choices.py (1)
  • get_default_ai_model (135-137)
core/schemas.py (2)
  • TitleSuggestionContext (135-151)
  • TitleSuggestions (168-171)
core/tasks.py (2)
core/api/views.py (1)
  • generate_title_suggestions (214-254)
core/models.py (2)
  • generate_title_suggestions (516-563)
  • execute_complete_pipeline (1200-1275)
core/schemas.py (1)
core/models.py (1)
  • is_on_free_plan (161-162)
core/agents/competitor_finder_agent.py (2)
core/choices.py (1)
  • AIModel (127-129)
core/schemas.py (1)
  • ProjectDetails (17-102)
core/utils.py (1)
core/choices.py (3)
  • EmailType (121-124)
  • KeywordDataSource (106-108)
  • OGImageStyle (86-94)
core/models.py (5)
core/model_utils.py (1)
  • run_agent_synchronously (18-75)
core/api/views.py (2)
  • generate_title_suggestions (214-254)
  • get_pipeline_status (1218-1248)
core/choices.py (1)
  • ContentType (4-5)
core/schemas.py (5)
  • ProjectPageContext (174-182)
  • BlogPostGenerationContext (185-192)
  • ContentFixContext (331-337)
  • InternalLinkContext (312-318)
  • CompetitorVsPostContext (260-273)
core/agents/seo_content_generator_agent.py (1)
  • create_structure_guidance_prompt (90-115)
🪛 Ruff (0.14.3)
core/agents/fix_validation_issue_agent.py

27-27: Unused noqa directive (non-enabled: E501)

Remove unused noqa directive

(RUF100)

core/agents/content_validation_agent.py

32-32: Unused noqa directive (non-enabled: E501)

Remove unused noqa directive

(RUF100)

core/agents/blog_structure_agent.py

39-39: Unused noqa directive (non-enabled: E501)

Remove unused noqa directive

(RUF100)

core/migrations/0043_remove_generatedblogpost_content_too_short_and_more.py

8-10: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


12-34: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)

core/agents/seo_content_generator_agent.py

66-66: Unused noqa directive (non-enabled: E501)

Remove unused noqa directive

(RUF100)


101-101: Unused function argument: ctx

(ARG001)

core/migrations/0042_generatedblogpost_generation_structure_and_more.py

8-10: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


12-33: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)

core/api/views.py

1041-1041: Unused noqa directive (non-enabled: E501)

Remove unused noqa directive

(RUF100)


1060-1065: Consider moving this statement to an else block

(TRY300)


1077-1077: Use explicit conversion flag

Replace with conversion flag

(RUF010)


1206-1206: Use explicit conversion flag

Replace with conversion flag

(RUF010)


1306-1306: Unused noqa directive (non-enabled: E501)

Remove unused noqa directive

(RUF100)


1349-1349: Use explicit conversion flag

Replace with conversion flag

(RUF010)

core/agents/title_suggestions_agent.py

46-46: Unused noqa directive (non-enabled: E501)

Remove unused noqa directive

(RUF100)

core/utils.py

393-393: Unused function argument: max_links

(ARG001)

core/models.py

784-784: Consider moving this statement to an else block

(TRY300)


851-851: Unused noqa directive (non-enabled: E501)

Remove unused noqa directive

(RUF100)


873-873: Consider moving this statement to an else block

(TRY300)


933-936: Consider moving this statement to an else block

(TRY300)


991-991: Consider moving this statement to an else block

(TRY300)


1066-1066: Consider moving this statement to an else block

(TRY300)


1126-1129: Consider moving this statement to an else block

(TRY300)


1184-1184: Consider moving this statement to an else block

(TRY300)


1265-1265: Consider moving this statement to an else block

(TRY300)


1504-1504: PEP 484 prohibits implicit Optional

Convert to T | None

(RUF013)

🔇 Additional comments (15)
frontend/src/controllers/content_idea_controller.js (1)

24-24: No backend compatibility issues exist—backend already enforces SEO-only content type.

The backend has already been refactored to support only the SEO content type. The ContentType enum in core/choices.py defines only SEO, and the BlogPostTitleSuggestion model restricts the content_type field to ContentType.choices (SEO-only) with a default of ContentType.SEO. The frontend change is fully compatible with the current backend implementation.

core/agents/competitor_finder_agent.py (1)

1-86: LGTM! Well-structured agent implementation.

The competitor finder agent is cleanly implemented with focused prompt helpers, proper use of pydantic-ai patterns, and intelligent differentiation between free and paid plans. The Perplexity integration is correctly configured, and the system prompts compose well to provide comprehensive context.

core/agents/system_prompts.py (1)

165-179: LGTM! Clear formatting guidance.

The inline_link_formatting() helper provides clear, actionable guidance with both correct and incorrect examples for link formatting.

core/agents/analyze_project_agent.py (1)

1-23: LGTM! Clean agent configuration.

The analyze project agent is properly configured with appropriate system prompts and follows the modular agent pattern introduced in this PR.

frontend/src/controllers/title_suggestions_controller.js (1)

13-14: LGTM! Simplified to SEO-only generation.

Hardcoding to "SEO" aligns with the PR's shift toward a pipeline-driven, SEO-focused content generation flow. The value is still appropriately sent to the backend API.

frontend/templates/components/blog_post_suggestion_card.html (1)

47-100: LGTM! Improved state handling for pipeline-driven generation.

The updated UI logic correctly leverages is_ready_to_view to differentiate between ready, in-progress, and not-yet-generated states. The "Continue" button provides a clear path to resume incomplete generation, aligning well with the multi-step pipeline architecture.

core/agents/blog_structure_agent.py (1)

13-48: LGTM! Comprehensive blog structure agent.

The agent is well-configured with detailed guidelines for creating blog post outlines. The temperature setting (0.7) provides a good balance for creative structure generation while maintaining consistency.

core/agents/summarize_page_agent.py (1)

1-19: LGTM! Well-configured summarization agent.

The agent is properly configured with an appropriate temperature (0.5) for summarization tasks and clear instructions for concise summaries.

core/agents/fix_validation_issue_agent.py (1)

6-42: LGTM! Effective content fixing agent.

The agent is well-configured for fixing validation issues with clear guidelines to preserve style while addressing problems. The add_content_and_issues system prompt provider cleanly formats the context. Temperature (0.5) is appropriate for editing tasks.

core/models.py (6)

694-720: LGTM: Clean pipeline initialization.

The method properly creates a blog post and initializes the pipeline state with appropriate logging.


722-798: LGTM: Well-structured pipeline step with proper error handling.

The method correctly updates pipeline state, builds context, runs the agent, and handles errors by updating the pipeline state before re-raising. This allows the pipeline to track failures while still propagating exceptions.


1034-1040: Good use of early return for edge case.

The early return when no project pages are available avoids unnecessary agent calls and clearly logs the skip reason.


1200-1275: LGTM: Complete pipeline orchestration is well-structured.

The method properly orchestrates all pipeline steps with appropriate validation and fixing logic. The one-retry approach for validation failures is reasonable, and async OG image generation is a good choice.


1382-1392: Verify is_ready_to_view logic matches intended behavior.

The is_ready_to_view property returns True for posts without a pipeline_state (line 1388). This means legacy posts or posts created outside the pipeline are considered ready to view. Confirm this is the intended behavior.

If legacy posts without pipeline tracking should not be automatically marked as ready, consider:

if not self.pipeline_state:
    return False  # Or check other fields for legacy validation

1156-1158: Remove redundant imports.

All three imports here are redundant. Remove them.

-        from core.agents.fix_validation_issue_agent import agent
-        from core.model_utils import run_agent_synchronously
-        from core.schemas import ContentFixContext
+        from core.agents.fix_validation_issue_agent import agent
+        from core.schemas import ContentFixContext

Likely an incorrect or invalid review comment.

Comment on lines +1067 to +1080
except Exception as error:
logger.error(
"[Pipeline API] Failed to start pipeline",
error=str(error),
exc_info=True,
suggestion_id=suggestion_id,
profile_id=profile.id,
)
return {
"status": "error",
"message": f"Failed to start pipeline: {str(error)}",
"blog_post_id": None,
"pipeline_state": None,
}
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 | 🟡 Minor

Catch specific exception types instead of broad Exception.

The coding guidelines specify avoiding broad Exception catches. Consider catching specific exceptions (e.g., DatabaseError, ValidationError) to handle different failure scenarios appropriately.

Apply this diff to improve exception handling:

-    except Exception as error:
+    except (DatabaseError, ValidationError, IntegrityError) as error:
         logger.error(
             "[Pipeline API] Failed to start pipeline",
             error=str(error),
             exc_info=True,
             suggestion_id=suggestion_id,
             profile_id=profile.id,
         )
         return {
             "status": "error",
             "message": f"Failed to start pipeline: {str(error)}",
             "blog_post_id": None,
             "pipeline_state": None,
         }
+    except Exception as error:
+        logger.error(
+            "[Pipeline API] Unexpected error starting pipeline",
+            error=str(error),
+            exc_info=True,
+            suggestion_id=suggestion_id,
+            profile_id=profile.id,
+        )
+        raise

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 Ruff (0.14.3)

1077-1077: Use explicit conversion flag

Replace with conversion flag

(RUF010)

Comment on lines +1190 to +1210
except Exception as error:
logger.error(
"[Pipeline API] Step execution failed",
error=str(error),
exc_info=True,
blog_post_id=blog_post_id,
step_name=step_name,
profile_id=profile.id,
)

# Update pipeline step to failed
blog_post.refresh_from_db()
pipeline_state = blog_post.get_pipeline_status()

return {
"status": "error",
"message": f"Step failed: {str(error)}",
"step_name": step_name,
"pipeline_state": pipeline_state,
"result": None,
}
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 | 🟡 Minor

Catch specific exception types instead of broad Exception.

Similar to the start_pipeline endpoint, catching broad Exception violates the coding guidelines.

Apply the same pattern as suggested for start_pipeline to catch specific Django/database exceptions first, then re-raise unexpected exceptions.

🧰 Tools
🪛 Ruff (0.14.3)

1206-1206: Use explicit conversion flag

Replace with conversion flag

(RUF010)

🤖 Prompt for AI Agents
In core/api/views.py around lines 1190 to 1210, replace the broad "except
Exception" handler with specific exception handlers like Django/DB-related
exceptions (e.g., ObjectDoesNotExist, ValidationError, DatabaseError,
OperationalError) that you expect from the pipeline step, log each specific
error with the same logger fields and update the blog_post/pipeline_state as
currently done, and then add a final generic except Exception as e: raise to
re-raise unexpected errors so they bubble up (do not swallow unknown
exceptions).

Comment on lines +392 to +423
def find_internal_link_opportunities(
content: str, project_pages: list, max_links: int = 10
) -> list[dict]:
"""
Use embeddings to find relevant spots for internal links.

Args:
content: The blog post content
project_pages: List of ProjectPage objects with embeddings
max_links: Maximum number of link opportunities to find

Returns:
List of link opportunities with position and relevance info
"""
# This is a simplified version - in production, you would:
# 1. Split content into paragraphs
# 2. Get embeddings for each paragraph
# 3. Use cosine similarity to find relevant pages
# 4. Return top matches with position information

opportunities = []

# For now, return empty list - actual implementation would use vector similarity
# This would be filled out when embeddings are ready
logger.info(
"[FindInternalLinkOpportunities] Finding link opportunities",
content_length=len(content),
available_pages=len(project_pages),
)

return opportunities

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

Implement the internal link opportunity finder.

This helper always returns an empty list and never honors max_links, so the new internal-link pipeline step will never surface any links. Please add at least a minimal implementation (paragraph split + embedding similarity capped by max_links) or gate the pipeline until this logic exists.

🧰 Tools
🪛 Ruff (0.14.3)

393-393: Unused function argument: max_links

(ARG001)

Comment on lines +425 to +448
def insert_link_at_position(content: str, link_url: str, link_text: str, position: int) -> str:
"""
Smart link insertion at a specific position in the content.

Args:
content: The original content
link_url: URL to link to
link_text: Anchor text for the link
position: Character position where to insert the link

Returns:
Content with link inserted
"""
# Find the text to replace with a link
start_pos = max(0, position - len(link_text) // 2)
end_pos = min(len(content), position + len(link_text) // 2)

# Create the markdown link
markdown_link = f"[{link_text}]({link_url})"

# Insert the link
new_content = content[:start_pos] + markdown_link + content[end_pos:]

return new_content
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

Fix link insertion to avoid deleting surrounding text.

Replacing a symmetric slice around position drops part of the paragraph and collapses spacing (e.g., "The product is affordable..." becomes "The[affordable solution](...)able..."). Insert the anchor cleanly without discarding adjacent characters.

-    start_pos = max(0, position - len(link_text) // 2)
-    end_pos = min(len(content), position + len(link_text) // 2)
-
-    markdown_link = f"[{link_text}]({link_url})"
-
-    new_content = content[:start_pos] + markdown_link + content[end_pos:]
-
-    return new_content
+    markdown_link = f"[{link_text}]({link_url})"
+
+    before = content[:position]
+    after = content[position:]
+
+    needs_space_before = bool(before) and not before[-1].isspace()
+    needs_space_after = bool(after) and not after[0].isspace()
+
+    insertion = markdown_link
+    if needs_space_before:
+        insertion = f" {insertion}"
+    if needs_space_after:
+        insertion = f"{insertion} "
+
+    return before + insertion + after
🤖 Prompt for AI Agents
In core/utils.py around lines 425 to 448, the current implementation replaces a
symmetric slice around the position which deletes surrounding text; instead
compute a safe insert_pos = clamp(position, 0, len(content)) and insert
markdown_link at that index (content[:insert_pos] + markdown_link +
content[insert_pos:]) rather than replacing characters; also normalize spacing
so the link is separated from adjacent alphanumeric characters (add a space
before or after the link if the neighboring characters are alphanumeric) and
return the resulting string.

Comment on lines +64 to +78
for (let i = 0; i < this.stepNames.length; i++) {
const step = this.stepNames[i];
this.currentStep = step.key;

const success = await this.executeStep(step, i);

if (!success) {
// Step failed, show retry option if available
break;
}
}

// All steps completed successfully
this.onPipelineComplete();

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 | 🔴 Critical

Prevent false “pipeline complete” states

If executeStep returns false and we break out of the loop, we still call onPipelineComplete(), so the dialog shows a success message even though a step failed. Guard the completion call with a flag.

-      for (let i = 0; i < this.stepNames.length; i++) {
+      let allStepsCompleted = true;
+      for (let i = 0; i < this.stepNames.length; i++) {
         const step = this.stepNames[i];
         this.currentStep = step.key;

         const success = await this.executeStep(step, i);

         if (!success) {
-          // Step failed, show retry option if available
-          break;
+          allStepsCompleted = false;
+          break;
         }
       }

-      // All steps completed successfully
-      this.onPipelineComplete();
+      if (allStepsCompleted) {
+        this.onPipelineComplete();
+      }
🤖 Prompt for AI Agents
In frontend/src/controllers/blog_generation_pipeline_controller.js around lines
64 to 78, the loop breaks when executeStep returns false but
onPipelineComplete() is still called, causing a false success state; add a
success flag (e.g., allSucceeded = true) before the loop, set it to false when a
step fails (when executeStep returns false) and break, then only call
this.onPipelineComplete() if allSucceeded is true (or alternatively return early
on failure) so the completion dialog is shown only when every step succeeds.

Comment on lines +156 to +170
// Continue with remaining steps
for (let i = parseInt(stepIndex) + 1; i < this.stepNames.length; i++) {
const nextStep = this.stepNames[i];
const nextSuccess = await this.executeStep(nextStep, i);

if (!nextSuccess) {
break;
}
}

// Check if all steps completed
if (parseInt(stepIndex) === this.stepNames.length - 1) {
this.onPipelineComplete();
}
}
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

Signal completion after a successful retry

When a mid-pipeline retry succeeds, we run the remaining steps but never reach onPipelineComplete(), so the dialog stays stuck. Track whether the remaining steps all succeed and only then call onPipelineComplete().

-    const success = await this.executeStep(step, parseInt(stepIndex));
-
-    if (success) {
-      // Continue with remaining steps
-      for (let i = parseInt(stepIndex) + 1; i < this.stepNames.length; i++) {
+    const success = await this.executeStep(step, parseInt(stepIndex));
+
+    if (success) {
+      let allRemainingStepsCompleted = true;
+      for (let i = parseInt(stepIndex) + 1; i < this.stepNames.length; i++) {
         const nextStep = this.stepNames[i];
         const nextSuccess = await this.executeStep(nextStep, i);

         if (!nextSuccess) {
-          break;
+          allRemainingStepsCompleted = false;
+          break;
         }
       }

-      // Check if all steps completed
-      if (parseInt(stepIndex) === this.stepNames.length - 1) {
-        this.onPipelineComplete();
-      }
+      if (allRemainingStepsCompleted) {
+        this.onPipelineComplete();
+      }
     }
📝 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.

Suggested change
// Continue with remaining steps
for (let i = parseInt(stepIndex) + 1; i < this.stepNames.length; i++) {
const nextStep = this.stepNames[i];
const nextSuccess = await this.executeStep(nextStep, i);
if (!nextSuccess) {
break;
}
}
// Check if all steps completed
if (parseInt(stepIndex) === this.stepNames.length - 1) {
this.onPipelineComplete();
}
}
let allRemainingStepsCompleted = true;
for (let i = parseInt(stepIndex) + 1; i < this.stepNames.length; i++) {
const nextStep = this.stepNames[i];
const nextSuccess = await this.executeStep(nextStep, i);
if (!nextSuccess) {
allRemainingStepsCompleted = false;
break;
}
}
if (allRemainingStepsCompleted) {
this.onPipelineComplete();
}
}

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

♻️ Duplicate comments (3)
core/models.py (1)

900-900: Remove redundant imports of run_agent_synchronously.

The function run_agent_synchronously is already imported at line 33 at the module level. These local imports within methods (lines 900, 986, 1121) are redundant and should be removed to keep imports organized.

Also applies to: 986-986, 1121-1121

core/api/views.py (2)

1136-1140: Fragile validation issue extraction from error message.

This code extracts validation issues by parsing the error message string with error_message.replace("Content validation failed - ", ""). This tight coupling between error message format and parsing logic is brittle—if the error format changes in the model layer, this breaks silently.

Consider storing validation issues in a structured field within the blog_post's pipeline_state (e.g., pipeline_state["steps"][step_name]["validation_issues"]) rather than embedding them in error messages. Then read directly from that structured field instead of parsing strings.


1067-1080: Catch specific exception types instead of broad Exception.

All four new pipeline endpoints catch broad Exception, which violates the coding guidelines. Consider catching specific Django/database exceptions (e.g., DatabaseError, ValidationError, IntegrityError, ObjectDoesNotExist) to handle different failure scenarios appropriately, then re-raise unexpected exceptions so they bubble up rather than being swallowed.

As per coding guidelines

Also applies to: 1198-1218, 1242-1256, 1347-1360

🧹 Nitpick comments (4)
frontend/src/controllers/generate-content-controller.js (3)

75-75: Unnecessary assignment to instance property.

pipelineSteps is only used within the generate() method and doesn't need to be stored on this. Consider keeping it as a local variable.

Apply this diff:

-      this.pipelineSteps = ["structure", "content", "preliminary_validation", "internal_links", "final_validation"];
+      const pipelineSteps = ["structure", "content", "preliminary_validation", "internal_links", "final_validation"];

And update line 78:

-      for (const step of this.pipelineSteps) {
+      for (const step of pipelineSteps) {

208-208: Unnecessary assignment to instance property.

Same issue as in the generate() method: pipelineSteps is only used locally and doesn't need to be stored on this.

Apply this diff:

-      this.pipelineSteps = ["structure", "content", "preliminary_validation", "internal_links", "final_validation"];
+      const pipelineSteps = ["structure", "content", "preliminary_validation", "internal_links", "final_validation"];

And update line 211:

-      for (const frontend_step of this.pipelineSteps) {
+      for (const frontend_step of pipelineSteps) {

368-400: Consider using helper method for step display names.

The progress dialog has hardcoded step display text (lines 378, 382, 386, 390, 394) that should ideally match the mappings in _getStepDisplayName(). While not critical, using the helper method would ensure consistency if display names change.

core/models.py (1)

1560-1631: Fix type hint for optional parameter.

Line 1560: The error parameter has type hint error: str = None which uses implicit Optional. PEP 484 requires explicit union syntax.

Apply this diff:

-    def update_pipeline_step(self, step_name: str, status: str, error: str = None):
+    def update_pipeline_step(self, step_name: str, status: str, error: str | None = None):

The method implementation is excellent—it handles all step transitions correctly:

  • Tracks retry counts for failed steps
  • Records timestamps (started_at, completed_at, failed_at, needs_fix_at)
  • Advances current_step through the sequence
  • Increments fix_attempt_count for validation steps needing fixes
  • Distinguishes between main steps and fix steps for progress tracking
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d522819 and d8449e9.

📒 Files selected for processing (3)
  • core/api/views.py (5 hunks)
  • core/models.py (17 hunks)
  • frontend/src/controllers/generate-content-controller.js (4 hunks)
🧰 Additional context used
📓 Path-based instructions (6)
**/*.py

📄 CodeRabbit inference engine (.cursor/rules/backend.mdc)

**/*.py: Prioritize readability and maintainability; follow PEP 8
Use descriptive variable/function names with underscores

In 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/api/views.py
  • core/models.py
core/**/*.py

📄 CodeRabbit inference engine (.cursor/rules/backend.mdc)

Leverage Django's ORM; avoid raw SQL when possible

Files:

  • core/api/views.py
  • core/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 classes

Implement REST APIs with django-ninja using Pydantic schemas under core/api/

Files:

  • core/api/views.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 the frontend/src/controllers directory

Use Stimulus.js controllers for interactive frontend behavior

Files:

  • frontend/src/controllers/generate-content-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/generate-content-controller.js
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/tasks.py : Define background tasks using django-q2 in core/tasks.py

Applied to files:

  • core/models.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/models.py
🧬 Code graph analysis (3)
core/api/views.py (2)
core/api/schemas.py (5)
  • PipelineCompleteOut (286-290)
  • PipelineRetryOut (280-283)
  • PipelineStartOut (257-261)
  • PipelineStatusOut (272-277)
  • PipelineStepOut (264-269)
core/models.py (7)
  • BlogPostTitleSuggestion (648-1331)
  • start_generation_pipeline (694-720)
  • get_pipeline_status (1633-1653)
  • GeneratedBlogPost (1372-1661)
  • can_retry_step (1655-1661)
  • update_pipeline_step (1560-1631)
  • execute_complete_pipeline (1256-1331)
frontend/src/controllers/generate-content-controller.js (2)
frontend/src/controllers/blog_generation_pipeline_controller.js (6)
  • step (65-65)
  • step (133-133)
  • response (48-53)
  • response (89-97)
  • data (55-55)
  • data (99-99)
frontend/src/utils/messages.js (1)
  • showMessage (2-39)
core/models.py (5)
core/model_utils.py (1)
  • run_agent_synchronously (18-75)
core/api/views.py (2)
  • generate_title_suggestions (214-254)
  • get_pipeline_status (1226-1256)
core/choices.py (1)
  • ContentType (4-5)
core/schemas.py (5)
  • ProjectPageContext (174-182)
  • BlogPostGenerationContext (185-192)
  • ContentFixContext (331-337)
  • InternalLinkContext (312-318)
  • CompetitorVsPostContext (260-273)
core/agents/seo_content_generator_agent.py (1)
  • create_structure_guidance_prompt (90-115)
🪛 Ruff (0.14.3)
core/api/views.py

1041-1041: Unused noqa directive (non-enabled: E501)

Remove unused noqa directive

(RUF100)


1060-1065: Consider moving this statement to an else block

(TRY300)


1077-1077: Use explicit conversion flag

Replace with conversion flag

(RUF010)


1214-1214: Use explicit conversion flag

Replace with conversion flag

(RUF010)


1314-1314: Unused noqa directive (non-enabled: E501)

Remove unused noqa directive

(RUF100)


1357-1357: Use explicit conversion flag

Replace with conversion flag

(RUF010)

core/models.py

784-784: Consider moving this statement to an else block

(TRY300)


851-851: Unused noqa directive (non-enabled: E501)

Remove unused noqa directive

(RUF100)


873-873: Consider moving this statement to an else block

(TRY300)


935-935: Unused noqa directive (non-enabled: E501)

Remove unused noqa directive

(RUF100)


955-958: Consider moving this statement to an else block

(TRY300)


1019-1019: Consider moving this statement to an else block

(TRY300)


1094-1094: Consider moving this statement to an else block

(TRY300)


1156-1156: Unused noqa directive (non-enabled: E501)

Remove unused noqa directive

(RUF100)


1176-1179: Consider moving this statement to an else block

(TRY300)


1240-1240: Consider moving this statement to an else block

(TRY300)


1321-1321: Consider moving this statement to an else block

(TRY300)


1560-1560: PEP 484 prohibits implicit Optional

Convert to T | None

(RUF013)

🔇 Additional comments (13)
frontend/src/controllers/generate-content-controller.js (2)

8-8: LGTM!

The blogPostId value is properly declared and follows Stimulus conventions.


473-497: LGTM!

The helper methods provide a clean centralized mapping for step names with sensible fallbacks. Good practice to have these extracted as reusable methods.

core/api/views.py (2)

28-32: LGTM! Pipeline schema integration and ContentType removal are consistent.

The new Pipeline schema imports and the removal of content_type parameters from title suggestion calls align well with the pipeline revamp described in the PR objectives.

Also applies to: 236-236, 272-272


1264-1291: LGTM! Clean implementation with appropriate error handling.

The retry endpoint has a straightforward implementation with proper defensive checks and logging. The absence of broad exception handling here is actually preferable since Django's get_object_or_404 and the can_retry_step method provide adequate error boundaries.

core/models.py (9)

516-563: LGTM! Simplified title generation aligns with ContentType removal.

The removal of the content_type parameter and hardcoding ContentType.SEO (line 549) is consistent with the broader architectural change to streamline the content generation flow. Since only one content type exists, this simplification is appropriate.


694-720: LGTM! Clean pipeline initialization.

The start_generation_pipeline method provides a clear entry point for the pipeline with proper logging and initialization. Creating the GeneratedBlogPost first and then calling initialize_pipeline() ensures the state is ready for subsequent steps.


722-798: LGTM! Well-structured structure and content generation steps.

Both generate_structure and generate_content_from_structure follow a consistent pattern:

  • Update step to in_progress
  • Build context with project pages and keywords
  • Run agent synchronously
  • Save results
  • Update step to completed
  • Comprehensive error handling with logging

The structure-guided content generation (lines 819-823) is a nice touch for ensuring consistency.

Also applies to: 800-887


889-972: LGTM! Validation methods include good defensive logic.

Both run_preliminary_validation and run_final_validation implement smart fix-attempt limiting (lines 924-936, 1145-1163). Capping at 2 fix attempts prevents infinite validation-fix loops while still allowing reasonable retry attempts. The distinction between needs_fix (retriable) and failed (max attempts exceeded) is well-designed.

Also applies to: 1110-1193


1256-1331: LGTM! Complete pipeline orchestration is thorough.

The execute_complete_pipeline method properly sequences all steps and handles validation failures gracefully. The conditional OG image generation (lines 1308-1313) respects project settings. The logging at key points provides good observability.


1398-1418: LGTM! Well-designed pipeline state management.

The new pipeline fields provide comprehensive state tracking:

  • pipeline_state: Current step and per-step status/errors
  • generation_structure: Persists the outline for content generation
  • raw_content: Preserves content before link insertion
  • pipeline_metadata: Timestamps and progress metrics

The initialize_pipeline method sets up a clear initial state with all 7 steps (5 main + 2 fix steps) properly initialized. The total_steps count of 5 correctly excludes fix steps from progress calculation.

Also applies to: 1527-1558


1633-1661: LGTM! Clean status and retry logic.

The get_pipeline_status method returns a well-structured API response with progress percentage calculation, and can_retry_step provides a simple retry limit check (max 3 attempts). Both methods handle missing pipeline_state gracefully.


479-479: LGTM! Consistent local agent imports.

The pattern of importing agents within methods (rather than at module level) is applied consistently across the file. This approach avoids potential circular import issues and keeps dependencies localized to where they're actually used.

Also applies to: 594-594, 1779-1779, 2106-2108


1438-1448: LGTM! Clean migration to pipeline-based validation.

The is_ready_to_view property replaces multiple legacy validation flags with a single, clear check: is final_validation completed? The backward compatibility fallback (line 1444) ensures existing blog posts without pipeline_state still work correctly.

Comment on lines +68 to 71
if (!startResponse.ok) {
const error = await startResponse.json();
throw new Error(error.message || "Failed to start generation");
}
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 | 🟡 Minor

Add error handling for JSON parsing.

If the response body is not valid JSON, await startResponse.json() will throw an error. This should be wrapped in a try-catch block.

Apply this diff:

-      if (!startResponse.ok) {
-        const error = await startResponse.json();
-        throw new Error(error.message || "Failed to start generation");
-      }
+      if (!startResponse.ok) {
+        let errorMessage = "Failed to start generation";
+        try {
+          const error = await startResponse.json();
+          errorMessage = error.message || errorMessage;
+        } catch (e) {
+          // Response body is not JSON, use default message
+        }
+        throw new Error(errorMessage);
+      }
🤖 Prompt for AI Agents
In frontend/src/controllers/generate-content-controller.js around lines 68 to
71, the code calls await startResponse.json() without guarding against invalid
JSON; wrap the JSON parsing in a try-catch so parsing failures are handled: try
to parse JSON, and if it fails fall back to await startResponse.text() (or a
default message) and include startResponse.status in the thrown Error; rethrow a
new Error containing either parsed error.message or the fallback text plus
status so callers get a clear, non-crashing error when the response body is not
valid JSON.

Comment on lines +82 to 89
if (stepResult && stepResult.needs_fix) {
const fixStepName = `fix_${step}`;
await this._executeStep(fixStepName);

// Re-run validation after fixing
await this._executeStep(step);
}
}
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

Handle persistent validation failures after fix.

After fixing validation issues and re-running validation, the code doesn't check if validation still fails. This could result in incomplete or invalid content being marked as complete. Consider adding a check similar to the one in the continue() method (lines 242-244).

Apply this diff:

       // Check if validation failed and needs fixing
       if (stepResult && stepResult.needs_fix) {
         const fixStepName = `fix_${step}`;
         await this._executeStep(fixStepName);

         // Re-run validation after fixing
-        await this._executeStep(step);
+        const revalidationResult = await this._executeStep(step);
+
+        // If validation still needs fixing after one fix attempt, stop here
+        if (revalidationResult && revalidationResult.needs_fix) {
+          throw new Error("Validation still has issues after fix. Please review the content and try again.");
+        }
       }
📝 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.

Suggested change
if (stepResult && stepResult.needs_fix) {
const fixStepName = `fix_${step}`;
await this._executeStep(fixStepName);
// Re-run validation after fixing
await this._executeStep(step);
}
}
if (stepResult && stepResult.needs_fix) {
const fixStepName = `fix_${step}`;
await this._executeStep(fixStepName);
// Re-run validation after fixing
const revalidationResult = await this._executeStep(step);
// If validation still needs fixing after one fix attempt, stop here
if (revalidationResult && revalidationResult.needs_fix) {
throw new Error("Validation still has issues after fix. Please review the content and try again.");
}
}
🤖 Prompt for AI Agents
In frontend/src/controllers/generate-content-controller.js around lines 82 to
89, after executing the fix step and re-running the original validation step,
re-check the validation result and handle persistent failures the same way as
the continue() method (see lines 242-244): capture the second stepResult from
the re-run, and if it still indicates needs_fix or failure, stop progression by
returning/throwing or marking the content as invalid (matching the existing
continue() behavior) so incomplete/invalid content is not treated as complete.

Comment on lines +189 to +195
const step_name_map = {
"generate_structure": "structure",
"generate_content": "content",
"preliminary_validation": "preliminary_validation",
"insert_internal_links": "internal_links",
"final_validation": "final_validation"
};
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Extract duplicated step name mapping.

This step name mapping is duplicated in the _getBackendStepName() method (lines 486-497). Consider using the helper method instead to maintain consistency and reduce duplication.

Apply this diff:

-      // Map backend step names to frontend step names
-      const step_name_map = {
-        "generate_structure": "structure",
-        "generate_content": "content",
-        "preliminary_validation": "preliminary_validation",
-        "insert_internal_links": "internal_links",
-        "final_validation": "final_validation"
-      };
+      // Map backend step names to frontend step names
+      const backend_to_frontend_map = {};
+      const frontend_steps = ["structure", "content", "preliminary_validation", "internal_links", "final_validation"];
+      frontend_steps.forEach(step => {
+        backend_to_frontend_map[this._getBackendStepName(step)] = step;
+      });

And update line 200:

-          const frontend_step_name = step_name_map[backend_step_name];
+          const frontend_step_name = backend_to_frontend_map[backend_step_name];

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In frontend/src/controllers/generate-content-controller.js around lines 189 to
195 and update at line 200, the step_name_map object is duplicated (same mapping
exists in _getBackendStepName at lines 486-497); remove this local step_name_map
and replace its usage on line 200 with a call to the existing helper
_getBackendStepName(step) (or _getBackendStepName(this, step) if it’s an
instance method), ensuring you pass the same step value and handle undefined
returns the same way as before so behavior is preserved.

Comment on lines +299 to +319
async _executeStep(step_name) {
const step_display_names = {
"structure": "Generating Structure",
"content": "Generating Content",
"preliminary_validation": "Validating Content",
"fix_preliminary_validation": "Fixing Validation Issues",
"internal_links": "Adding Internal Links",
"final_validation": "Final Validation",
"fix_final_validation": "Fixing Validation Issues"
};

// Map frontend step names to backend internal step names
const backend_step_names = {
"structure": "generate_structure",
"content": "generate_content",
"preliminary_validation": "preliminary_validation",
"fix_preliminary_validation": "fix_preliminary_validation",
"internal_links": "insert_internal_links",
"final_validation": "final_validation",
"fix_final_validation": "fix_final_validation"
};
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Extract duplicated mappings to use existing helper methods.

The step display names (lines 300-308) and backend step names (lines 311-319) are duplicated in _getStepDisplayName() and _getBackendStepName() helper methods. Use the helper methods instead to maintain a single source of truth.

Apply this diff:

   async _executeStep(step_name) {
-    const step_display_names = {
-      "structure": "Generating Structure",
-      "content": "Generating Content",
-      "preliminary_validation": "Validating Content",
-      "fix_preliminary_validation": "Fixing Validation Issues",
-      "internal_links": "Adding Internal Links",
-      "final_validation": "Final Validation",
-      "fix_final_validation": "Fixing Validation Issues"
-    };
-
-    // Map frontend step names to backend internal step names
-    const backend_step_names = {
-      "structure": "generate_structure",
-      "content": "generate_content",
-      "preliminary_validation": "preliminary_validation",
-      "fix_preliminary_validation": "fix_preliminary_validation",
-      "internal_links": "insert_internal_links",
-      "final_validation": "final_validation",
-      "fix_final_validation": "fix_final_validation"
-    };
-
     // Update progress dialog to show current step
-    this._updateProgressStep(step_name, "in_progress", step_display_names[step_name]);
+    this._updateProgressStep(step_name, "in_progress", this._getStepDisplayName(step_name));

     try {
       const response = await fetch(`/api/generate-blog-content-pipeline/${this.blogPostId}/step/${step_name}`, {
@@ -344,7 +332,7 @@
       }

       // Get the backend step name to check status
-      const backend_step_name = backend_step_names[step_name];
+      const backend_step_name = this._getBackendStepName(step_name);
       const step_status = data.pipeline_state?.steps?.[backend_step_name];

       // Check if step failed and needs retry
       if (step_status && step_status.status === "failed") {
-        this._updateProgressStep(step_name, "failed", `${step_display_names[step_name]} Failed`);
+        this._updateProgressStep(step_name, "failed", `${this._getStepDisplayName(step_name)} Failed`);
         throw new Error(step_status.error || `Step ${step_name} failed`);
       }

       // Update progress dialog to show step completed
-      this._updateProgressStep(step_name, "completed", step_display_names[step_name]);
+      this._updateProgressStep(step_name, "completed", this._getStepDisplayName(step_name));

       // Return the result data for validation checks
       return data;

     } catch (error) {
-      this._updateProgressStep(step_name, "failed", step_display_names[step_name]);
+      this._updateProgressStep(step_name, "failed", this._getStepDisplayName(step_name));
       throw error;
     }
   }
🤖 Prompt for AI Agents
In frontend/src/controllers/generate-content-controller.js around lines 299 to
319, remove the duplicated step_display_names and backend_step_names maps and
replace their usage by calling the existing helper methods
_getStepDisplayName(step_name) and _getBackendStepName(step_name) respectively;
update any references in this function to call those helpers (pass step_name)
before making API calls or updating UI so the mappings come from the
single-source helper methods and eliminate the duplicated objects.

Comment on lines +469 to +471
_getCsrfToken() {
return document.querySelector("[name=csrfmiddlewaretoken]").value;
}
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 | 🟡 Minor

Add error handling for missing CSRF token.

If the CSRF token element is not present in the DOM, this method will throw an error. Add a null check to provide a clearer error message.

Apply this diff:

   _getCsrfToken() {
-    return document.querySelector("[name=csrfmiddlewaretoken]").value;
+    const tokenElement = document.querySelector("[name=csrfmiddlewaretoken]");
+    if (!tokenElement) {
+      throw new Error("CSRF token not found in page");
+    }
+    return tokenElement.value;
   }
📝 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.

Suggested change
_getCsrfToken() {
return document.querySelector("[name=csrfmiddlewaretoken]").value;
}
_getCsrfToken() {
const tokenElement = document.querySelector("[name=csrfmiddlewaretoken]");
if (!tokenElement) {
throw new Error("CSRF token not found in page");
}
return tokenElement.value;
}
🤖 Prompt for AI Agents
In frontend/src/controllers/generate-content-controller.js around lines 469 to
471, _getCsrfToken() directly accesses document.querySelector(...).value which
will throw if the CSRF element is missing; update the method to check for a null
result, and if missing throw or return a clear error (e.g., throw new
Error("CSRF token not found in DOM") or return null), otherwise return the
element.value — keep the check simple and ensure callers handle the
error/nullable return.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

♻️ Duplicate comments (5)
core/agents/content_validation_agent.py (1)

48-48: Remove unused # noqa: E501.

The linter no longer requires this directive as the prompt string satisfies the line length limit. Remove the trailing comment to resolve the Ruff warning.

Apply this diff:

-    """,  # noqa: E501
+    """,
core/models.py (4)

899-901: Remove redundant imports.

Both run_agent_synchronously (already imported at line 33) and the agent import pattern duplicate earlier imports.

Apply this diff:

         from core.agents.content_validation_agent import agent
-        from core.model_utils import run_agent_synchronously
-        from core.schemas import ContentValidationContext

993-995: Remove redundant imports.

run_agent_synchronously is already imported at line 33. Remove the redundant import.

Apply this diff:

         from core.agents.fix_validation_issue_agent import agent
-        from core.model_utils import run_agent_synchronously
         from core.schemas import ContentFixContext

1128-1130: Remove redundant imports.

Both run_agent_synchronously and the schemas are already imported earlier in the file.

Apply this diff:

         from core.agents.content_validation_agent import agent
-        from core.model_utils import run_agent_synchronously
-        from core.schemas import ContentValidationContext

1222-1224: Remove redundant import.

run_agent_synchronously is already imported at line 33.

Apply this diff:

         from core.agents.fix_validation_issue_agent import agent
-        from core.model_utils import run_agent_synchronously
         from core.schemas import ContentFixContext
🧹 Nitpick comments (1)
core/models.py (1)

1576-1576: Use explicit type annotation for optional parameter.

PEP 484 prohibits implicit Optional. Use str | None for the error parameter.

Apply this diff:

-    def update_pipeline_step(self, step_name: str, status: str, error: str = None):
+    def update_pipeline_step(self, step_name: str, status: str, error: str | None = None):
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d8449e9 and a6b1304.

📒 Files selected for processing (3)
  • core/agents/content_validation_agent.py (1 hunks)
  • core/models.py (17 hunks)
  • core/schemas.py (3 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 underscores

In 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.py
  • core/models.py
  • core/agents/content_validation_agent.py
core/**/*.py

📄 CodeRabbit inference engine (.cursor/rules/backend.mdc)

Leverage Django's ORM; avoid raw SQL when possible

Files:

  • core/schemas.py
  • core/models.py
  • core/agents/content_validation_agent.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/agents/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

Implement AI agents using pydantic-ai under core/agents/

Files:

  • core/agents/content_validation_agent.py
🧠 Learnings (3)
📚 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
📚 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/models.py
  • core/agents/content_validation_agent.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/content_validation_agent.py
🧬 Code graph analysis (3)
core/schemas.py (1)
core/models.py (1)
  • is_on_free_plan (161-162)
core/models.py (5)
core/model_utils.py (1)
  • run_agent_synchronously (18-75)
core/api/views.py (2)
  • generate_title_suggestions (214-254)
  • get_pipeline_status (1226-1256)
core/choices.py (1)
  • ContentType (4-5)
core/schemas.py (6)
  • ProjectPageContext (174-182)
  • BlogPostGenerationContext (185-192)
  • ContentValidationContext (321-330)
  • ContentFixContext (343-349)
  • InternalLinkContext (312-318)
  • CompetitorVsPostContext (260-273)
core/agents/seo_content_generator_agent.py (1)
  • create_structure_guidance_prompt (90-115)
core/agents/content_validation_agent.py (2)
core/choices.py (1)
  • get_default_ai_model (135-137)
core/schemas.py (2)
  • ContentValidationContext (321-330)
  • ContentValidationResult (333-340)
🪛 Ruff (0.14.3)
core/models.py

784-784: Consider moving this statement to an else block

(TRY300)


851-851: Unused noqa directive (non-enabled: E501)

Remove unused noqa directive

(RUF100)


873-873: Consider moving this statement to an else block

(TRY300)


943-943: Unused noqa directive (non-enabled: E501)

Remove unused noqa directive

(RUF100)


963-966: Consider moving this statement to an else block

(TRY300)


1027-1027: Consider moving this statement to an else block

(TRY300)


1102-1102: Consider moving this statement to an else block

(TRY300)


1172-1172: Unused noqa directive (non-enabled: E501)

Remove unused noqa directive

(RUF100)


1192-1195: Consider moving this statement to an else block

(TRY300)


1256-1256: Consider moving this statement to an else block

(TRY300)


1337-1337: Consider moving this statement to an else block

(TRY300)


1576-1576: PEP 484 prohibits implicit Optional

Convert to T | None

(RUF013)

core/agents/content_validation_agent.py

48-48: Unused noqa directive (non-enabled: E501)

Remove unused noqa directive

(RUF100)

🔇 Additional comments (14)
core/schemas.py (7)

62-64: LGTM: Free plan flag properly integrated.

The is_on_free_plan field is correctly typed with an appropriate default and integrates well with the Profile model's property at line 161.


192-192: LGTM: Description accurately reflects SEO-only content type.

The updated description correctly indicates that SEO is the only content type option, consistent with the ContentType.SEO default at models.py line 549.


276-310: LGTM: Well-designed schemas for structured content generation.

The BlogPostSection and BlogPostStructure schemas provide a clear, hierarchical structure for the pipeline's outline phase, with appropriate field types and comprehensive descriptions.


312-319: LGTM: Clean context schema for internal linking.

The InternalLinkContext provides the necessary content and available pages for the internal links agent, with clear field descriptions.


321-330: LGTM: Comprehensive validation context.

The ContentValidationContext schema includes all necessary fields for content quality validation, with clear descriptions and appropriate defaults.


333-340: LGTM: Clear validation result structure.

The ContentValidationResult schema effectively communicates validation status with both a boolean flag and detailed issue descriptions.


343-349: LGTM: Appropriate context for fix operations.

The ContentFixContext schema provides the original content and specific issues to address, enabling targeted content corrections.

core/models.py (7)

1-1: LGTM: JSON import necessary for pipeline structure handling.

The json import is correctly used at lines 773 and 819 for structure serialization and deserialization.


406-406: LGTM: Free plan status correctly propagated.

The is_on_free_plan field properly passes the profile's subscription status to agents, enabling tier-based generation decisions.


479-491: LGTM: Agent import and call pattern correct.

The analyze_content method properly imports the agent locally and uses the correct calling pattern with the deps parameter.


516-563: LGTM: Title generation method well-implemented.

The method correctly imports the agent, uses proper calling conventions with deps, and wraps bulk creation in a transaction. The ContentType.SEO default aligns with the updated schema.


694-720: LGTM: Pipeline initialization clean and well-logged.

The start_generation_pipeline method properly creates the blog post, initializes the pipeline state, and includes comprehensive logging for tracking.


1414-1434: LGTM: Pipeline fields well-designed.

The new pipeline-tracking fields (pipeline_state, generation_structure, raw_content, pipeline_metadata) have appropriate types, defaults, and clear help text for tracking multi-step generation.


1454-1464: LGTM: Ready-to-view logic handles both legacy and pipeline posts.

The property correctly handles legacy posts (no pipeline_state) and checks final validation completion for new pipeline-generated posts.


result = run_agent_synchronously(
agent,
"Please generate the full blog post content following the provided structure outline.", # noqa: E501
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 | 🟡 Minor

Remove unused # noqa: E501.

The directive is unnecessary as the line satisfies length requirements.

Apply this diff:

-                "Please generate the full blog post content following the provided structure outline.",  # noqa: E501
+                "Please generate the full blog post content following the provided structure outline.",
📝 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.

Suggested change
"Please generate the full blog post content following the provided structure outline.", # noqa: E501
"Please generate the full blog post content following the provided structure outline.",
🧰 Tools
🪛 Ruff (0.14.3)

851-851: Unused noqa directive (non-enabled: E501)

Remove unused noqa directive

(RUF100)

🤖 Prompt for AI Agents
In core/models.py around line 851, remove the unnecessary inline comment " #
noqa: E501" appended to the string literal "Please generate the full blog post
content following the provided structure outline.", since the line length is
acceptable; simply delete the " # noqa: E501" portion so the line becomes the
string only and save the file.

Comment on lines +913 to +917
result = run_agent_synchronously(
agent,
validation_context,
function_name="run_preliminary_validation",
)
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 | 🔴 Critical

Fix critical agent calling pattern bug.

The validation context is passed as a positional argument instead of using the deps parameter. Since content_validation_agent is defined with deps_type=ContentValidationContext, the call must use the deps keyword argument. The current code will fail at runtime.

Apply this diff:

             result = run_agent_synchronously(
                 agent,
-                validation_context,
+                "Please validate this blog post content for completeness and quality.",
+                deps=validation_context,
                 function_name="run_preliminary_validation",
             )
🤖 Prompt for AI Agents
In core/models.py around lines 913 to 917, the call to run_agent_synchronously
incorrectly passes validation_context as a positional argument; change the call
to pass the context using the deps keyword (e.g., deps=validation_context) so
that the agent defined with deps_type=ContentValidationContext receives its
dependencies correctly and the runtime failure is avoided.

blog_post.update_pipeline_step(
"preliminary_validation",
"failed",
error=f"Content validation failed after {fix_attempt_count} fix attempts - {issues_text}", # noqa: E501
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 | 🟡 Minor

Remove unused # noqa: E501.

Apply this diff:

-                        error=f"Content validation failed after {fix_attempt_count} fix attempts - {issues_text}",  # noqa: E501
+                        error=f"Content validation failed after {fix_attempt_count} fix attempts - {issues_text}",
📝 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.

Suggested change
error=f"Content validation failed after {fix_attempt_count} fix attempts - {issues_text}", # noqa: E501
error=f"Content validation failed after {fix_attempt_count} fix attempts - {issues_text}",
🧰 Tools
🪛 Ruff (0.14.3)

943-943: Unused noqa directive (non-enabled: E501)

Remove unused noqa directive

(RUF100)

🤖 Prompt for AI Agents
In core/models.py around line 943, the trailing comment "# noqa: E501" is
unnecessary; remove the "# noqa: E501" from the end of the error= f-string line
so the line no longer contains the supressed lint directive and relies on normal
line-length checks (or wrap the string earlier if you want to satisfy
line-length rules instead of suppressing them).

Comment on lines +1142 to +1146
result = run_agent_synchronously(
agent,
validation_context,
function_name="run_final_validation",
)
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 | 🔴 Critical

Fix critical agent calling pattern bug.

Same issue as in run_preliminary_validation: the validation context must be passed via the deps parameter, not as a positional argument.

Apply this diff:

             result = run_agent_synchronously(
                 agent,
-                validation_context,
+                "Please validate this blog post content for completeness and quality.",
+                deps=validation_context,
                 function_name="run_final_validation",
             )
🤖 Prompt for AI Agents
In core/models.py around lines 1142 to 1146, the call to run_agent_synchronously
currently passes validation_context as a positional argument; change it to pass
the context via the deps keyword instead (remove the positional argument and add
deps={"validation_context": validation_context}) so the agent receives the
validation context the same way run_preliminary_validation does; keep
function_name="run_final_validation" and any other kwargs unchanged.

blog_post.update_pipeline_step(
"final_validation",
"failed",
error=f"Content validation failed after {fix_attempt_count} fix attempts - {issues_text}", # noqa: E501
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 | 🟡 Minor

Remove unused # noqa: E501.

Apply this diff:

-                        error=f"Content validation failed after {fix_attempt_count} fix attempts - {issues_text}",  # noqa: E501
+                        error=f"Content validation failed after {fix_attempt_count} fix attempts - {issues_text}",
📝 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.

Suggested change
error=f"Content validation failed after {fix_attempt_count} fix attempts - {issues_text}", # noqa: E501
error=f"Content validation failed after {fix_attempt_count} fix attempts - {issues_text}",
🧰 Tools
🪛 Ruff (0.14.3)

1172-1172: Unused noqa directive (non-enabled: E501)

Remove unused noqa directive

(RUF100)

🤖 Prompt for AI Agents
In core/models.py around line 1172, the trailing inline comment "# noqa: E501"
on the error= f-string is unused and should be removed; edit the line to delete
the "# noqa: E501" suffix so the file no longer contains that unnecessary flake8
disable comment and save the file.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant