Add site builder service to Docker Compose and remove obsolete scripts
- Introduced a new service `igny8_site_builder` in `docker-compose.app.yml` for site building functionality, including environment variables and volume mappings. - Deleted several outdated scripts: `create_test_users.py`, `test_image_write_access.py`, `update_free_plan.py`, and the database file `db.sqlite3` to clean up the backend. - Updated Django settings and URL configurations to integrate the new site builder module.
This commit is contained in:
@@ -3,4 +3,4 @@ Site Building Business Logic
|
||||
Phase 3: Site Builder
|
||||
"""
|
||||
|
||||
|
||||
default_app_config = 'igny8_core.business.site_building.apps.SiteBuildingConfig'
|
||||
|
||||
9
backend/igny8_core/business/site_building/apps.py
Normal file
9
backend/igny8_core/business/site_building/apps.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class SiteBuildingConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'igny8_core.business.site_building'
|
||||
verbose_name = 'Site Building'
|
||||
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django.core.validators
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('igny8_core_auth', '0008_passwordresettoken_alter_industry_options_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='SiteBlueprint',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('name', models.CharField(help_text='Site name', max_length=255)),
|
||||
('description', models.TextField(blank=True, help_text='Site description', null=True)),
|
||||
('config_json', models.JSONField(default=dict, help_text='Wizard configuration: business_type, style, objectives, etc.')),
|
||||
('structure_json', models.JSONField(default=dict, help_text='AI-generated structure: pages, layout, theme, etc.')),
|
||||
('status', models.CharField(choices=[('draft', 'Draft'), ('generating', 'Generating'), ('ready', 'Ready'), ('deployed', 'Deployed')], db_index=True, default='draft', help_text='Blueprint status', max_length=20)),
|
||||
('hosting_type', models.CharField(choices=[('igny8_sites', 'IGNY8 Sites'), ('wordpress', 'WordPress'), ('shopify', 'Shopify'), ('multi', 'Multiple Destinations')], default='igny8_sites', help_text='Target hosting platform', max_length=50)),
|
||||
('version', models.IntegerField(default=1, help_text='Blueprint version', validators=[django.core.validators.MinValueValidator(1)])),
|
||||
('deployed_version', models.IntegerField(blank=True, help_text='Currently deployed version', null=True)),
|
||||
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='siteblueprint_set', to='igny8_core_auth.account')),
|
||||
('sector', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='siteblueprint_set', to='igny8_core_auth.sector')),
|
||||
('site', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='siteblueprint_set', to='igny8_core_auth.site')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Site Blueprint',
|
||||
'verbose_name_plural': 'Site Blueprints',
|
||||
'db_table': 'igny8_site_blueprints',
|
||||
'ordering': ['-created_at'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PageBlueprint',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('slug', models.SlugField(help_text='Page URL slug', max_length=255)),
|
||||
('title', models.CharField(help_text='Page title', max_length=255)),
|
||||
('type', models.CharField(choices=[('home', 'Home'), ('about', 'About'), ('services', 'Services'), ('products', 'Products'), ('blog', 'Blog'), ('contact', 'Contact'), ('custom', 'Custom')], default='custom', help_text='Page type', max_length=50)),
|
||||
('blocks_json', models.JSONField(default=list, help_text="Page content blocks: [{'type': 'hero', 'data': {...}}, ...]")),
|
||||
('status', models.CharField(choices=[('draft', 'Draft'), ('generating', 'Generating'), ('ready', 'Ready')], db_index=True, default='draft', help_text='Page status', max_length=20)),
|
||||
('order', models.IntegerField(default=0, help_text='Page order in navigation')),
|
||||
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='pageblueprint_set', to='igny8_core_auth.account')),
|
||||
('sector', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='pageblueprint_set', to='igny8_core_auth.sector')),
|
||||
('site', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='pageblueprint_set', to='igny8_core_auth.site')),
|
||||
('site_blueprint', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='pages', to='site_building.siteblueprint')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Page Blueprint',
|
||||
'verbose_name_plural': 'Page Blueprints',
|
||||
'db_table': 'igny8_page_blueprints',
|
||||
'ordering': ['order', 'created_at'],
|
||||
'unique_together': {('site_blueprint', 'slug')},
|
||||
},
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='siteblueprint',
|
||||
index=models.Index(fields=['status'], name='igny8_site__status_247ddc_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='siteblueprint',
|
||||
index=models.Index(fields=['hosting_type'], name='igny8_site__hosting_c4bb41_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='siteblueprint',
|
||||
index=models.Index(fields=['site', 'sector'], name='igny8_site__site_id__5f0a4e_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='siteblueprint',
|
||||
index=models.Index(fields=['account', 'status'], name='igny8_site__account__38f18a_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='pageblueprint',
|
||||
index=models.Index(fields=['site_blueprint', 'status'], name='igny8_page__site_bl_1b5d8b_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='pageblueprint',
|
||||
index=models.Index(fields=['type'], name='igny8_page__type_b11552_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='pageblueprint',
|
||||
index=models.Index(fields=['site_blueprint', 'order'], name='igny8_page__site_bl_7a77d7_idx'),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
|
||||
|
||||
@@ -2,4 +2,12 @@
|
||||
Site Building Services
|
||||
"""
|
||||
|
||||
from igny8_core.business.site_building.services.file_management_service import SiteBuilderFileService
|
||||
from igny8_core.business.site_building.services.structure_generation_service import StructureGenerationService
|
||||
from igny8_core.business.site_building.services.page_generation_service import PageGenerationService
|
||||
|
||||
__all__ = [
|
||||
'SiteBuilderFileService',
|
||||
'StructureGenerationService',
|
||||
'PageGenerationService',
|
||||
]
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
"""
|
||||
Page Generation Service
|
||||
Leverages the Writer ContentGenerationService to draft page copy for Site Builder blueprints.
|
||||
"""
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from django.db import transaction
|
||||
|
||||
from igny8_core.business.content.models import Tasks
|
||||
from igny8_core.business.content.services.content_generation_service import ContentGenerationService
|
||||
from igny8_core.business.site_building.models import PageBlueprint
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PageGenerationService:
|
||||
"""
|
||||
Thin wrapper that converts Site Builder pages into writer tasks and reuses the
|
||||
existing content generation pipeline. This keeps content authoring logic
|
||||
inside the Writer module while Site Builder focuses on structure.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.content_service = ContentGenerationService()
|
||||
|
||||
def generate_page_content(self, page_blueprint: PageBlueprint, force_regenerate: bool = False) -> dict:
|
||||
"""
|
||||
Generate (or regenerate) content for a single Site Builder page.
|
||||
|
||||
Args:
|
||||
page_blueprint: Target PageBlueprint instance.
|
||||
force_regenerate: If True, resets any temporary task data.
|
||||
"""
|
||||
if not page_blueprint:
|
||||
raise ValueError("Page blueprint is required")
|
||||
|
||||
task = self._ensure_task(page_blueprint, force_regenerate=force_regenerate)
|
||||
|
||||
# Mark page as generating before handing off to Writer pipeline
|
||||
page_blueprint.status = 'generating'
|
||||
page_blueprint.save(update_fields=['status', 'updated_at'])
|
||||
|
||||
account = page_blueprint.account
|
||||
logger.info(
|
||||
"[PageGenerationService] Triggering content generation for page %s (task %s)",
|
||||
page_blueprint.id,
|
||||
task.id,
|
||||
)
|
||||
return self.content_service.generate_content([task.id], account)
|
||||
|
||||
def regenerate_page(self, page_blueprint: PageBlueprint) -> dict:
|
||||
"""Force regeneration by dropping the cached task metadata."""
|
||||
return self.generate_page_content(page_blueprint, force_regenerate=True)
|
||||
|
||||
# Internal helpers --------------------------------------------------------
|
||||
|
||||
def _ensure_task(self, page_blueprint: PageBlueprint, force_regenerate: bool = False) -> Tasks:
|
||||
"""
|
||||
Create or reuse a Writer task that mirrors the given page blueprint.
|
||||
We rely on a deterministic title pattern to keep the mapping lightweight
|
||||
without introducing new relations/migrations.
|
||||
"""
|
||||
title = self._build_task_title(page_blueprint)
|
||||
task_qs = Tasks.objects.filter(
|
||||
account=page_blueprint.account,
|
||||
site=page_blueprint.site,
|
||||
sector=page_blueprint.sector,
|
||||
title=title,
|
||||
)
|
||||
|
||||
if force_regenerate:
|
||||
task_qs.delete()
|
||||
else:
|
||||
existing = task_qs.first()
|
||||
if existing:
|
||||
return existing
|
||||
|
||||
return self._create_task_from_page(page_blueprint, title)
|
||||
|
||||
@transaction.atomic
|
||||
def _create_task_from_page(self, page_blueprint: PageBlueprint, title: str) -> Tasks:
|
||||
"""Translate blueprint metadata into a Writer task."""
|
||||
description_parts = [
|
||||
f"Site Blueprint: {page_blueprint.site_blueprint.name}",
|
||||
f"Page Type: {page_blueprint.type}",
|
||||
]
|
||||
hero_block = self._first_block_heading(page_blueprint)
|
||||
if hero_block:
|
||||
description_parts.append(f"Hero/Primary Heading: {hero_block}")
|
||||
|
||||
keywords = self._build_keywords_hint(page_blueprint)
|
||||
|
||||
task = Tasks.objects.create(
|
||||
account=page_blueprint.account,
|
||||
site=page_blueprint.site,
|
||||
sector=page_blueprint.sector,
|
||||
title=title,
|
||||
description="\n".join(filter(None, description_parts)),
|
||||
keywords=keywords,
|
||||
content_structure=self._map_content_structure(page_blueprint.type),
|
||||
content_type='article',
|
||||
status='queued',
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"[PageGenerationService] Created writer task %s for page blueprint %s",
|
||||
task.id,
|
||||
page_blueprint.id,
|
||||
)
|
||||
return task
|
||||
|
||||
def _build_task_title(self, page_blueprint: PageBlueprint) -> str:
|
||||
base = page_blueprint.title or page_blueprint.slug.replace('-', ' ').title()
|
||||
return f"[Site Builder] {base}"
|
||||
|
||||
def _build_keywords_hint(self, page_blueprint: PageBlueprint) -> str:
|
||||
keywords = []
|
||||
if page_blueprint.blocks_json:
|
||||
for block in page_blueprint.blocks_json:
|
||||
heading = block.get('heading') if isinstance(block, dict) else None
|
||||
if heading:
|
||||
keywords.append(heading)
|
||||
keywords.append(page_blueprint.slug.replace('-', ' '))
|
||||
return ", ".join(dict.fromkeys(filter(None, keywords)))
|
||||
|
||||
def _map_content_structure(self, page_type: Optional[str]) -> str:
|
||||
if not page_type:
|
||||
return 'landing_page'
|
||||
mapping = {
|
||||
'home': 'landing_page',
|
||||
'about': 'supporting_page',
|
||||
'services': 'pillar_page',
|
||||
'products': 'pillar_page',
|
||||
'blog': 'cluster_hub',
|
||||
'contact': 'supporting_page',
|
||||
}
|
||||
return mapping.get(page_type.lower(), 'landing_page')
|
||||
|
||||
def _first_block_heading(self, page_blueprint: PageBlueprint) -> Optional[str]:
|
||||
if not page_blueprint.blocks_json:
|
||||
return None
|
||||
for block in page_blueprint.blocks_json:
|
||||
if isinstance(block, dict):
|
||||
heading = block.get('heading') or block.get('title')
|
||||
if heading:
|
||||
return heading
|
||||
return None
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
"""
|
||||
Structure Generation Service
|
||||
Triggers the AI workflow that maps business briefs to page blueprints.
|
||||
"""
|
||||
import logging
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from django.utils import timezone
|
||||
|
||||
from igny8_core.business.billing.exceptions import InsufficientCreditsError
|
||||
from igny8_core.business.billing.services.credit_service import CreditService
|
||||
from igny8_core.business.site_building.models import SiteBlueprint
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class StructureGenerationService:
|
||||
"""Orchestrates AI-powered site structure generation."""
|
||||
|
||||
def __init__(self):
|
||||
self.credit_service = CreditService()
|
||||
|
||||
def generate_structure(
|
||||
self,
|
||||
site_blueprint: SiteBlueprint,
|
||||
business_brief: str,
|
||||
objectives: Optional[List[str]] = None,
|
||||
style_preferences: Optional[Dict[str, Any]] = None,
|
||||
metadata: Optional[Dict[str, Any]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Kick off AI structure generation for a single blueprint.
|
||||
|
||||
Args:
|
||||
site_blueprint: Target blueprint instance.
|
||||
business_brief: Business description / positioning statement.
|
||||
objectives: Optional list of goals for the new site.
|
||||
style_preferences: Optional design/style hints.
|
||||
metadata: Additional free-form context.
|
||||
"""
|
||||
if not site_blueprint:
|
||||
raise ValueError("Site blueprint is required")
|
||||
|
||||
account = site_blueprint.account
|
||||
objectives = objectives or []
|
||||
style_preferences = style_preferences or {}
|
||||
metadata = metadata or {}
|
||||
|
||||
logger.info(
|
||||
"[StructureGenerationService] Starting generation for blueprint %s (account %s)",
|
||||
site_blueprint.id,
|
||||
getattr(account, 'id', None),
|
||||
)
|
||||
|
||||
# Ensure the account can afford the request
|
||||
try:
|
||||
self.credit_service.check_credits(account, 'site_structure_generation')
|
||||
except InsufficientCreditsError:
|
||||
site_blueprint.status = 'draft'
|
||||
site_blueprint.save(update_fields=['status', 'updated_at'])
|
||||
raise
|
||||
|
||||
# Persist the latest inputs for future regenerations
|
||||
config = site_blueprint.config_json or {}
|
||||
config.update({
|
||||
'business_brief': business_brief,
|
||||
'objectives': objectives,
|
||||
'style': style_preferences,
|
||||
'last_requested_at': timezone.now().isoformat(),
|
||||
'metadata': metadata,
|
||||
})
|
||||
site_blueprint.config_json = config
|
||||
site_blueprint.status = 'generating'
|
||||
site_blueprint.save(update_fields=['config_json', 'status', 'updated_at'])
|
||||
|
||||
payload = {
|
||||
'ids': [site_blueprint.id],
|
||||
'business_brief': business_brief,
|
||||
'objectives': objectives,
|
||||
'style': style_preferences,
|
||||
'metadata': metadata,
|
||||
}
|
||||
|
||||
return self._dispatch_ai_task(payload, account_id=account.id)
|
||||
|
||||
# Internal helpers --------------------------------------------------------
|
||||
|
||||
def _dispatch_ai_task(self, payload: Dict[str, Any], account_id: int) -> Dict[str, Any]:
|
||||
from igny8_core.ai.tasks import run_ai_task
|
||||
|
||||
try:
|
||||
if hasattr(run_ai_task, 'delay'):
|
||||
async_result = run_ai_task.delay(
|
||||
function_name='generate_site_structure',
|
||||
payload=payload,
|
||||
account_id=account_id
|
||||
)
|
||||
logger.info(
|
||||
"[StructureGenerationService] Queued AI task %s for account %s",
|
||||
async_result.id,
|
||||
account_id,
|
||||
)
|
||||
return {
|
||||
'success': True,
|
||||
'task_id': str(async_result.id),
|
||||
'message': 'Site structure generation queued',
|
||||
}
|
||||
|
||||
# Celery not available – run synchronously
|
||||
logger.warning("[StructureGenerationService] Celery unavailable, running synchronously")
|
||||
return run_ai_task(
|
||||
function_name='generate_site_structure',
|
||||
payload=payload,
|
||||
account_id=account_id
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.error("Failed to dispatch structure generation: %s", exc, exc_info=True)
|
||||
return {
|
||||
'success': False,
|
||||
'error': str(exc),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user