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:
IGNY8 VPS (Salman)
2025-11-17 16:08:51 +00:00
parent e3d4ba2c02
commit 5a36686844
74 changed files with 7217 additions and 374 deletions

View File

@@ -3,4 +3,4 @@ Site Building Business Logic
Phase 3: Site Builder
"""
default_app_config = 'igny8_core.business.site_building.apps.SiteBuildingConfig'

View 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'

View File

@@ -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'),
),
]

View File

@@ -0,0 +1,2 @@

View File

@@ -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',
]

View File

@@ -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

View File

@@ -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),
}