feat(seo-copilot): caching + freshness UI; glassomorphic styling; CopilotKit HITL modular actions; provider fixes; DB sessions & action types; seed 17 actions
This commit is contained in:
1
backend/middleware/__init__.py
Normal file
1
backend/middleware/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Makes the middleware directory a Python package
|
||||
@@ -2,7 +2,7 @@
|
||||
Database models for SEO analysis data storage
|
||||
"""
|
||||
|
||||
from sqlalchemy import Column, Integer, String, DateTime, Text, JSON, Float, Boolean, ForeignKey
|
||||
from sqlalchemy import Column, Integer, String, DateTime, Text, JSON, Float, Boolean, ForeignKey, func
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import relationship
|
||||
from datetime import datetime
|
||||
@@ -10,6 +10,82 @@ from typing import Dict, Any, List
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
class SEOActionType(Base):
|
||||
"""Catalog of supported SEO action types (17 actions)."""
|
||||
__tablename__ = 'seo_action_types'
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
code = Column(String(100), unique=True, nullable=False) # e.g., analyze_page_speed
|
||||
name = Column(String(200), nullable=False)
|
||||
category = Column(String(50), nullable=True) # content, technical, performance, etc.
|
||||
description = Column(Text, nullable=True)
|
||||
created_at = Column(DateTime, default=func.now())
|
||||
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
||||
|
||||
def __repr__(self):
|
||||
return f"<SEOActionType(code='{self.code}', category='{self.category}')>"
|
||||
|
||||
class SEOAnalysisSession(Base):
|
||||
"""Anchor session for a set of SEO actions and summary."""
|
||||
__tablename__ = 'seo_analysis_sessions'
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
url = Column(String(500), nullable=False, index=True)
|
||||
triggered_by_user_id = Column(String(64), nullable=True)
|
||||
trigger_source = Column(String(32), nullable=True) # manual, schedule, action_followup, system
|
||||
input_context = Column(JSON, nullable=True)
|
||||
status = Column(String(20), default='success') # queued, running, success, failed, cancelled
|
||||
started_at = Column(DateTime, default=func.now(), nullable=False)
|
||||
completed_at = Column(DateTime, nullable=True)
|
||||
summary = Column(Text, nullable=True)
|
||||
overall_score = Column(Integer, nullable=True)
|
||||
health_label = Column(String(50), nullable=True)
|
||||
metrics = Column(JSON, nullable=True)
|
||||
issues_overview = Column(JSON, nullable=True)
|
||||
|
||||
# Relationships
|
||||
action_runs = relationship("SEOActionRun", back_populates="session", cascade="all, delete-orphan")
|
||||
analyses = relationship("SEOAnalysis", back_populates="session", cascade="all, delete-orphan")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<SEOAnalysisSession(url='{self.url}', status='{self.status}')>"
|
||||
|
||||
class SEOActionRun(Base):
|
||||
"""Each execution of a specific action (one of the 17)."""
|
||||
__tablename__ = 'seo_action_runs'
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
session_id = Column(Integer, ForeignKey('seo_analysis_sessions.id'), nullable=False)
|
||||
action_type_id = Column(Integer, ForeignKey('seo_action_types.id'), nullable=False)
|
||||
triggered_by_user_id = Column(String(64), nullable=True)
|
||||
input_params = Column(JSON, nullable=True)
|
||||
status = Column(String(20), default='success')
|
||||
started_at = Column(DateTime, default=func.now(), nullable=False)
|
||||
completed_at = Column(DateTime, nullable=True)
|
||||
result_summary = Column(Text, nullable=True)
|
||||
result = Column(JSON, nullable=True)
|
||||
diagnostics = Column(JSON, nullable=True)
|
||||
|
||||
# Relationships
|
||||
session = relationship("SEOAnalysisSession", back_populates="action_runs")
|
||||
action_type = relationship("SEOActionType")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<SEOActionRun(action_type_id={self.action_type_id}, status='{self.status}')>"
|
||||
|
||||
class SEOActionRunLink(Base):
|
||||
"""Graph relations between action runs for narrative linkage."""
|
||||
__tablename__ = 'seo_action_run_links'
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
from_action_run_id = Column(Integer, ForeignKey('seo_action_runs.id'), nullable=False)
|
||||
to_action_run_id = Column(Integer, ForeignKey('seo_action_runs.id'), nullable=False)
|
||||
relation = Column(String(50), nullable=False) # followup_of, supports, caused_by
|
||||
created_at = Column(DateTime, default=func.now())
|
||||
|
||||
def __repr__(self):
|
||||
return f"<SEOActionRunLink(relation='{self.relation}')>"
|
||||
|
||||
class SEOAnalysis(Base):
|
||||
"""Main SEO analysis record"""
|
||||
__tablename__ = 'seo_analyses'
|
||||
@@ -20,12 +96,14 @@ class SEOAnalysis(Base):
|
||||
health_status = Column(String(50), nullable=False) # excellent, good, needs_improvement, poor, error
|
||||
timestamp = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||
analysis_data = Column(JSON, nullable=True) # Store complete analysis data
|
||||
session_id = Column(Integer, ForeignKey('seo_analysis_sessions.id'), nullable=True)
|
||||
|
||||
# Relationships
|
||||
critical_issues = relationship("SEOIssue", back_populates="analysis", cascade="all, delete-orphan")
|
||||
warnings = relationship("SEOWarning", back_populates="analysis", cascade="all, delete-orphan")
|
||||
recommendations = relationship("SEORecommendation", back_populates="analysis", cascade="all, delete-orphan")
|
||||
category_scores = relationship("SEOCategoryScore", back_populates="analysis", cascade="all, delete-orphan")
|
||||
session = relationship("SEOAnalysisSession", back_populates="analyses")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<SEOAnalysis(url='{self.url}', score={self.overall_score}, status='{self.health_status}')>"
|
||||
@@ -36,6 +114,8 @@ class SEOIssue(Base):
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
analysis_id = Column(Integer, ForeignKey('seo_analyses.id'), nullable=False)
|
||||
session_id = Column(Integer, ForeignKey('seo_analysis_sessions.id'), nullable=True)
|
||||
action_run_id = Column(Integer, ForeignKey('seo_action_runs.id'), nullable=True)
|
||||
issue_text = Column(Text, nullable=False)
|
||||
category = Column(String(100), nullable=True) # url_structure, meta_data, content, etc.
|
||||
priority = Column(String(20), default='critical') # critical, high, medium, low
|
||||
@@ -53,6 +133,8 @@ class SEOWarning(Base):
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
analysis_id = Column(Integer, ForeignKey('seo_analyses.id'), nullable=False)
|
||||
session_id = Column(Integer, ForeignKey('seo_analysis_sessions.id'), nullable=True)
|
||||
action_run_id = Column(Integer, ForeignKey('seo_action_runs.id'), nullable=True)
|
||||
warning_text = Column(Text, nullable=False)
|
||||
category = Column(String(100), nullable=True)
|
||||
priority = Column(String(20), default='medium')
|
||||
@@ -70,6 +152,8 @@ class SEORecommendation(Base):
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
analysis_id = Column(Integer, ForeignKey('seo_analyses.id'), nullable=False)
|
||||
session_id = Column(Integer, ForeignKey('seo_analysis_sessions.id'), nullable=True)
|
||||
action_run_id = Column(Integer, ForeignKey('seo_action_runs.id'), nullable=True)
|
||||
recommendation_text = Column(Text, nullable=False)
|
||||
category = Column(String(100), nullable=True)
|
||||
difficulty = Column(String(20), default='medium') # easy, medium, hard
|
||||
|
||||
165
backend/scripts/seed_seo_action_types.py
Normal file
165
backend/scripts/seed_seo_action_types.py
Normal file
@@ -0,0 +1,165 @@
|
||||
"""
|
||||
Seed the seo_action_types table with the canonical set of SEO actions.
|
||||
|
||||
Run (from backend/):
|
||||
python scripts/seed_seo_action_types.py
|
||||
"""
|
||||
|
||||
from typing import List, Dict
|
||||
from loguru import logger
|
||||
import sys, os
|
||||
|
||||
# Ensure backend/ is on sys.path when running as a script
|
||||
CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
BACKEND_ROOT = os.path.abspath(os.path.join(CURRENT_DIR, os.pardir))
|
||||
if BACKEND_ROOT not in sys.path:
|
||||
sys.path.insert(0, BACKEND_ROOT)
|
||||
|
||||
from services.database import init_database, get_db_session
|
||||
from models.seo_analysis import SEOActionType
|
||||
|
||||
|
||||
def get_actions() -> List[Dict]:
|
||||
return [
|
||||
{
|
||||
"code": "analyze_seo_comprehensive",
|
||||
"name": "Analyze SEO (Comprehensive)",
|
||||
"category": "analysis",
|
||||
"description": "Perform a comprehensive SEO analysis across technical, on-page, and performance.",
|
||||
},
|
||||
{
|
||||
"code": "generate_meta_descriptions",
|
||||
"name": "Generate Meta Descriptions",
|
||||
"category": "content",
|
||||
"description": "Generate optimized meta description suggestions for pages.",
|
||||
},
|
||||
{
|
||||
"code": "analyze_page_speed",
|
||||
"name": "Analyze Page Speed",
|
||||
"category": "performance",
|
||||
"description": "Run page speed and Core Web Vitals checks for mobile/desktop.",
|
||||
},
|
||||
{
|
||||
"code": "analyze_sitemap",
|
||||
"name": "Analyze Sitemap",
|
||||
"category": "discovery",
|
||||
"description": "Analyze sitemap structure, coverage, and publishing patterns.",
|
||||
},
|
||||
{
|
||||
"code": "generate_image_alt_text",
|
||||
"name": "Generate Image Alt Text",
|
||||
"category": "content",
|
||||
"description": "Propose SEO-friendly alt text for images.",
|
||||
},
|
||||
{
|
||||
"code": "generate_opengraph_tags",
|
||||
"name": "Generate OpenGraph Tags",
|
||||
"category": "content",
|
||||
"description": "Create OpenGraph/Twitter meta tags for better social previews.",
|
||||
},
|
||||
{
|
||||
"code": "analyze_on_page_seo",
|
||||
"name": "Analyze On-Page SEO",
|
||||
"category": "on_page",
|
||||
"description": "Audit titles, headings, keyword usage, and internal links.",
|
||||
},
|
||||
{
|
||||
"code": "analyze_technical_seo",
|
||||
"name": "Analyze Technical SEO",
|
||||
"category": "technical",
|
||||
"description": "Audit crawlability, canonicals, schema, security, and redirects.",
|
||||
},
|
||||
{
|
||||
"code": "analyze_enterprise_seo",
|
||||
"name": "Analyze Enterprise SEO",
|
||||
"category": "enterprise",
|
||||
"description": "Advanced enterprise-level audits and recommendations.",
|
||||
},
|
||||
{
|
||||
"code": "analyze_content_strategy",
|
||||
"name": "Analyze Content Strategy",
|
||||
"category": "content",
|
||||
"description": "Analyze content themes, gaps, and strategy effectiveness.",
|
||||
},
|
||||
{
|
||||
"code": "perform_website_audit",
|
||||
"name": "Perform Website Audit",
|
||||
"category": "analysis",
|
||||
"description": "Holistic website audit with prioritized issues and actions.",
|
||||
},
|
||||
{
|
||||
"code": "analyze_content_comprehensive",
|
||||
"name": "Analyze Content (Comprehensive)",
|
||||
"category": "content",
|
||||
"description": "Deep content analysis including readability and structure.",
|
||||
},
|
||||
{
|
||||
"code": "check_seo_health",
|
||||
"name": "Check SEO Health",
|
||||
"category": "analysis",
|
||||
"description": "Quick health check and score snapshot.",
|
||||
},
|
||||
{
|
||||
"code": "explain_seo_concept",
|
||||
"name": "Explain SEO Concept",
|
||||
"category": "education",
|
||||
"description": "Explain SEO concepts in simple terms with examples.",
|
||||
},
|
||||
{
|
||||
"code": "update_seo_charts",
|
||||
"name": "Update SEO Charts",
|
||||
"category": "visualization",
|
||||
"description": "Update dashboard charts and visualizations per user request.",
|
||||
},
|
||||
{
|
||||
"code": "customize_seo_dashboard",
|
||||
"name": "Customize SEO Dashboard",
|
||||
"category": "visualization",
|
||||
"description": "Modify dashboard layout, widgets, and focus areas.",
|
||||
},
|
||||
{
|
||||
"code": "analyze_seo_full",
|
||||
"name": "Analyze SEO (Full)",
|
||||
"category": "analysis",
|
||||
"description": "Full analysis variant (alternate flow or endpoint).",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def seed_action_types():
|
||||
init_database()
|
||||
db = get_db_session()
|
||||
if db is None:
|
||||
raise RuntimeError("Could not get DB session")
|
||||
|
||||
try:
|
||||
actions = get_actions()
|
||||
created, updated, skipped = 0, 0, 0
|
||||
for action in actions:
|
||||
existing = db.query(SEOActionType).filter(SEOActionType.code == action["code"]).one_or_none()
|
||||
if existing:
|
||||
# Update name/category/description if changed
|
||||
changed = False
|
||||
if existing.name != action["name"]:
|
||||
existing.name = action["name"]; changed = True
|
||||
if existing.category != action["category"]:
|
||||
existing.category = action["category"]; changed = True
|
||||
if existing.description != action["description"]:
|
||||
existing.description = action["description"]; changed = True
|
||||
if changed:
|
||||
updated += 1
|
||||
else:
|
||||
skipped += 1
|
||||
else:
|
||||
db.add(SEOActionType(**action))
|
||||
created += 1
|
||||
db.commit()
|
||||
logger.info(f"SEO action types seeding done. created={created}, updated={updated}, unchanged={skipped}")
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
seed_action_types()
|
||||
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
"""
|
||||
AI SEO Tools Services Package
|
||||
|
||||
This package contains all migrated SEO tools as FastAPI services.
|
||||
Each service provides structured, AI-enhanced SEO analysis capabilities.
|
||||
"""
|
||||
# SEO tools package initializer
|
||||
|
||||
from .meta_description_service import MetaDescriptionService
|
||||
from .pagespeed_service import PageSpeedService
|
||||
from .pagespeed_service import PageSpeedService
|
||||
from .sitemap_service import SitemapService
|
||||
from .image_alt_service import ImageAltService
|
||||
from .opengraph_service import OpenGraphService
|
||||
@@ -16,13 +11,13 @@ from .enterprise_seo_service import EnterpriseSEOService
|
||||
from .content_strategy_service import ContentStrategyService
|
||||
|
||||
__all__ = [
|
||||
"MetaDescriptionService",
|
||||
"PageSpeedService",
|
||||
"SitemapService",
|
||||
"ImageAltService",
|
||||
"OpenGraphService",
|
||||
"OnPageSEOService",
|
||||
"TechnicalSEOService",
|
||||
"EnterpriseSEOService",
|
||||
"ContentStrategyService"
|
||||
'MetaDescriptionService',
|
||||
'PageSpeedService',
|
||||
'SitemapService',
|
||||
'ImageAltService',
|
||||
'OpenGraphService',
|
||||
'OnPageSEOService',
|
||||
'TechnicalSEOService',
|
||||
'EnterpriseSEOService',
|
||||
'ContentStrategyService',
|
||||
]
|
||||
@@ -10,7 +10,7 @@ from datetime import datetime
|
||||
from loguru import logger
|
||||
|
||||
from ..llm_providers.main_text_generation import llm_text_gen
|
||||
from ...middleware.logging_middleware import seo_logger
|
||||
from middleware.logging_middleware import seo_logger
|
||||
|
||||
|
||||
class MetaDescriptionService:
|
||||
|
||||
@@ -13,7 +13,7 @@ from loguru import logger
|
||||
import os
|
||||
|
||||
from ..llm_providers.main_text_generation import llm_text_gen
|
||||
from ...middleware.logging_middleware import seo_logger
|
||||
from middleware.logging_middleware import seo_logger
|
||||
|
||||
|
||||
class PageSpeedService:
|
||||
|
||||
@@ -15,7 +15,7 @@ from urllib.parse import urlparse, urljoin
|
||||
import pandas as pd
|
||||
|
||||
from ..llm_providers.main_text_generation import llm_text_gen
|
||||
from ...middleware.logging_middleware import seo_logger
|
||||
from middleware.logging_middleware import seo_logger
|
||||
|
||||
|
||||
class SitemapService:
|
||||
|
||||
Reference in New Issue
Block a user