more fixes
This commit is contained in:
@@ -45,8 +45,6 @@ class Igny8AdminSite(admin.AdminSite):
|
||||
('igny8_core_auth', 'User'),
|
||||
('igny8_core_auth', 'SiteUserAccess'),
|
||||
('igny8_core_auth', 'PasswordResetToken'),
|
||||
('site_building', 'SiteBlueprint'),
|
||||
('site_building', 'PageBlueprint'),
|
||||
],
|
||||
},
|
||||
'Global Reference Data': {
|
||||
|
||||
@@ -6,7 +6,6 @@ from igny8_core.ai.functions.generate_ideas import GenerateIdeasFunction
|
||||
from igny8_core.ai.functions.generate_content import GenerateContentFunction
|
||||
from igny8_core.ai.functions.generate_images import GenerateImagesFunction, generate_images_core
|
||||
from igny8_core.ai.functions.generate_image_prompts import GenerateImagePromptsFunction
|
||||
from igny8_core.ai.functions.generate_site_structure import GenerateSiteStructureFunction
|
||||
from igny8_core.ai.functions.generate_page_content import GeneratePageContentFunction
|
||||
|
||||
__all__ = [
|
||||
@@ -16,6 +15,5 @@ __all__ = [
|
||||
'GenerateImagesFunction',
|
||||
'generate_images_core',
|
||||
'GenerateImagePromptsFunction',
|
||||
'GenerateSiteStructureFunction',
|
||||
'GeneratePageContentFunction',
|
||||
]
|
||||
|
||||
@@ -1,214 +0,0 @@
|
||||
"""
|
||||
Generate Site Structure AI Function
|
||||
Phase 3 – Site Builder
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
from typing import Any, Dict, List, Tuple
|
||||
|
||||
from django.utils.text import slugify
|
||||
|
||||
from igny8_core.ai.base import BaseAIFunction
|
||||
from igny8_core.ai.prompts import PromptRegistry
|
||||
from igny8_core.business.site_building.models import SiteBlueprint, PageBlueprint
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GenerateSiteStructureFunction(BaseAIFunction):
|
||||
"""AI function that turns a business brief into a full site blueprint."""
|
||||
|
||||
def get_name(self) -> str:
|
||||
return 'generate_site_structure'
|
||||
|
||||
def get_metadata(self) -> Dict:
|
||||
metadata = super().get_metadata()
|
||||
metadata.update({
|
||||
'display_name': 'Generate Site Structure',
|
||||
'description': 'Create site/page architecture from business brief, objectives, and style guides.',
|
||||
'phases': {
|
||||
'INIT': 'Validating blueprint data…',
|
||||
'PREP': 'Preparing site context…',
|
||||
'AI_CALL': 'Generating site structure with AI…',
|
||||
'PARSE': 'Parsing generated blueprint…',
|
||||
'SAVE': 'Saving pages and blocks…',
|
||||
'DONE': 'Site structure ready!'
|
||||
}
|
||||
})
|
||||
return metadata
|
||||
|
||||
def validate(self, payload: dict, account=None) -> Dict[str, Any]:
|
||||
if not payload.get('ids'):
|
||||
return {'valid': False, 'error': 'Site blueprint ID is required'}
|
||||
return {'valid': True}
|
||||
|
||||
def prepare(self, payload: dict, account=None) -> Dict[str, Any]:
|
||||
blueprint_ids = payload.get('ids', [])
|
||||
queryset = SiteBlueprint.objects.filter(id__in=blueprint_ids)
|
||||
if account:
|
||||
queryset = queryset.filter(account=account)
|
||||
blueprint = queryset.select_related('account', 'site').prefetch_related('pages').first()
|
||||
if not blueprint:
|
||||
raise ValueError("Site blueprint not found")
|
||||
|
||||
config = blueprint.config_json or {}
|
||||
business_brief = payload.get('business_brief') or config.get('business_brief') or ''
|
||||
objectives = payload.get('objectives') or config.get('objectives') or []
|
||||
style = payload.get('style') or config.get('style') or {}
|
||||
|
||||
return {
|
||||
'blueprint': blueprint,
|
||||
'business_brief': business_brief,
|
||||
'objectives': objectives,
|
||||
'style': style,
|
||||
}
|
||||
|
||||
def build_prompt(self, data: Dict[str, Any], account=None) -> str:
|
||||
blueprint: SiteBlueprint = data['blueprint']
|
||||
objectives = data.get('objectives') or []
|
||||
objectives_text = '\n'.join(f"- {obj}" for obj in objectives) if isinstance(objectives, list) else objectives
|
||||
style = data.get('style') or {}
|
||||
style_text = json.dumps(style, indent=2) if isinstance(style, dict) and style else str(style)
|
||||
|
||||
existing_pages = [
|
||||
{
|
||||
'title': page.title,
|
||||
'slug': page.slug,
|
||||
'type': page.type,
|
||||
'status': page.status,
|
||||
}
|
||||
for page in blueprint.pages.all()
|
||||
]
|
||||
|
||||
context = {
|
||||
'BUSINESS_BRIEF': data.get('business_brief', ''),
|
||||
'OBJECTIVES': objectives_text or 'Create a full marketing site with clear navigation.',
|
||||
'STYLE': style_text or 'Modern, responsive, accessible web design.',
|
||||
'SITE_INFO': json.dumps({
|
||||
'site_name': blueprint.name,
|
||||
'site_description': blueprint.description,
|
||||
'hosting_type': blueprint.hosting_type,
|
||||
'existing_pages': existing_pages,
|
||||
'existing_structure': blueprint.structure_json or {},
|
||||
}, indent=2)
|
||||
}
|
||||
|
||||
return PromptRegistry.get_prompt(
|
||||
'generate_site_structure',
|
||||
account=account or blueprint.account,
|
||||
context=context
|
||||
)
|
||||
|
||||
def parse_response(self, response: str, step_tracker=None) -> Dict[str, Any]:
|
||||
if not response:
|
||||
raise ValueError("AI response is empty")
|
||||
|
||||
response = response.strip()
|
||||
try:
|
||||
return self._ensure_dict(json.loads(response))
|
||||
except json.JSONDecodeError:
|
||||
logger.warning("Response not valid JSON, attempting to extract JSON object")
|
||||
cleaned = self._extract_json_object(response)
|
||||
if cleaned:
|
||||
return self._ensure_dict(json.loads(cleaned))
|
||||
raise ValueError("Unable to parse AI response into JSON")
|
||||
|
||||
def save_output(
|
||||
self,
|
||||
parsed: Dict[str, Any],
|
||||
original_data: Dict[str, Any],
|
||||
account=None,
|
||||
progress_tracker=None,
|
||||
step_tracker=None
|
||||
) -> Dict[str, Any]:
|
||||
blueprint: SiteBlueprint = original_data['blueprint']
|
||||
structure = self._ensure_dict(parsed)
|
||||
pages = structure.get('pages', [])
|
||||
|
||||
blueprint.structure_json = structure
|
||||
blueprint.status = 'ready'
|
||||
blueprint.save(update_fields=['structure_json', 'status', 'updated_at'])
|
||||
|
||||
created, updated, deleted = self._sync_page_blueprints(blueprint, pages)
|
||||
|
||||
message = f"Pages synced (created: {created}, updated: {updated}, deleted: {deleted})"
|
||||
logger.info("[GenerateSiteStructure] %s for blueprint %s", message, blueprint.id)
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'count': created + updated,
|
||||
'site_blueprint_id': blueprint.id,
|
||||
'pages_created': created,
|
||||
'pages_updated': updated,
|
||||
'pages_deleted': deleted,
|
||||
}
|
||||
|
||||
# Helpers -----------------------------------------------------------------
|
||||
|
||||
def _ensure_dict(self, data: Any) -> Dict[str, Any]:
|
||||
if isinstance(data, dict):
|
||||
return data
|
||||
raise ValueError("AI response must be a JSON object with site metadata")
|
||||
|
||||
def _extract_json_object(self, text: str) -> str:
|
||||
start = text.find('{')
|
||||
end = text.rfind('}')
|
||||
if start != -1 and end != -1 and end > start:
|
||||
return text[start:end + 1]
|
||||
return ''
|
||||
|
||||
def _sync_page_blueprints(self, blueprint: SiteBlueprint, pages: List[Dict[str, Any]]) -> Tuple[int, int, int]:
|
||||
existing = {page.slug: page for page in blueprint.pages.all()}
|
||||
seen_slugs = set()
|
||||
created = updated = 0
|
||||
|
||||
for order, page_data in enumerate(pages or []):
|
||||
slug = page_data.get('slug') or page_data.get('id') or page_data.get('title') or f"page-{order + 1}"
|
||||
slug = slugify(slug) or f"page-{order + 1}"
|
||||
seen_slugs.add(slug)
|
||||
|
||||
defaults = {
|
||||
'title': page_data.get('title') or page_data.get('name') or slug.replace('-', ' ').title(),
|
||||
'type': self._map_page_type(page_data.get('type')),
|
||||
'blocks_json': page_data.get('blocks') or page_data.get('sections') or [],
|
||||
'status': page_data.get('status') or 'draft',
|
||||
'order': order,
|
||||
}
|
||||
|
||||
page_obj, created_flag = PageBlueprint.objects.update_or_create(
|
||||
site_blueprint=blueprint,
|
||||
slug=slug,
|
||||
defaults=defaults
|
||||
)
|
||||
if created_flag:
|
||||
created += 1
|
||||
else:
|
||||
updated += 1
|
||||
|
||||
# Delete pages not present in new structure
|
||||
deleted = 0
|
||||
for slug, page in existing.items():
|
||||
if slug not in seen_slugs:
|
||||
page.delete()
|
||||
deleted += 1
|
||||
|
||||
return created, updated, deleted
|
||||
|
||||
def _map_page_type(self, page_type: Any) -> str:
|
||||
allowed = {choice[0] for choice in PageBlueprint._meta.get_field('type').choices}
|
||||
if isinstance(page_type, str):
|
||||
normalized = page_type.lower()
|
||||
if normalized in allowed:
|
||||
return normalized
|
||||
# Map friendly names
|
||||
mapping = {
|
||||
'homepage': 'home',
|
||||
'landing': 'home',
|
||||
'service': 'services',
|
||||
'product': 'products',
|
||||
}
|
||||
mapped = mapping.get(normalized)
|
||||
if mapped in allowed:
|
||||
return mapped
|
||||
return 'custom'
|
||||
|
||||
@@ -94,11 +94,6 @@ def _load_generate_image_prompts():
|
||||
from igny8_core.ai.functions.generate_image_prompts import GenerateImagePromptsFunction
|
||||
return GenerateImagePromptsFunction
|
||||
|
||||
def _load_generate_site_structure():
|
||||
"""Lazy loader for generate_site_structure function"""
|
||||
from igny8_core.ai.functions.generate_site_structure import GenerateSiteStructureFunction
|
||||
return GenerateSiteStructureFunction
|
||||
|
||||
def _load_optimize_content():
|
||||
"""Lazy loader for optimize_content function"""
|
||||
from igny8_core.ai.functions.optimize_content import OptimizeContentFunction
|
||||
@@ -109,6 +104,5 @@ register_lazy_function('generate_ideas', _load_generate_ideas)
|
||||
register_lazy_function('generate_content', _load_generate_content)
|
||||
register_lazy_function('generate_images', _load_generate_images)
|
||||
register_lazy_function('generate_image_prompts', _load_generate_image_prompts)
|
||||
register_lazy_function('generate_site_structure', _load_generate_site_structure)
|
||||
register_lazy_function('optimize_content', _load_optimize_content)
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ class Migration(migrations.Migration):
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('system', '__latest__'),
|
||||
('igny8_core_auth', '__latest__'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
@@ -35,11 +35,11 @@ class Migration(migrations.Migration):
|
||||
('next_run_at', models.DateTimeField(blank=True, null=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='system.account')),
|
||||
('site', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='automation_config', to='system.site')),
|
||||
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='igny8_core_auth.account')),
|
||||
('site', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='automation_config', to='igny8_core_auth.site')),
|
||||
],
|
||||
options={
|
||||
'db_table': 'automation_config',
|
||||
'db_table': 'igny8_automation_configs',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
@@ -74,11 +74,11 @@ class Migration(migrations.Migration):
|
||||
('error_message', models.TextField(blank=True, null=True)),
|
||||
('started_at', models.DateTimeField(auto_now_add=True)),
|
||||
('completed_at', models.DateTimeField(blank=True, null=True)),
|
||||
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='system.account')),
|
||||
('site', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='automation_runs', to='system.site')),
|
||||
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='igny8_core_auth.account')),
|
||||
('site', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='automation_runs', to='igny8_core_auth.site')),
|
||||
],
|
||||
options={
|
||||
'db_table': 'automation_run',
|
||||
'db_table': 'igny8_automation_runs',
|
||||
'ordering': ['-started_at'],
|
||||
'indexes': [
|
||||
models.Index(fields=['site', 'status'], name='automation_site_status_idx'),
|
||||
|
||||
@@ -760,14 +760,14 @@ class AutomationService:
|
||||
def estimate_credits(self) -> int:
|
||||
"""Estimate total credits needed for automation"""
|
||||
# Count items
|
||||
keywords_count = Keywords.objects.filter(site=self.site, status='new', cluster__isnull=True).count()
|
||||
keywords_count = Keywords.objects.filter(site=self.site, status='new', cluster__isnull=True, disabled=False).count()
|
||||
clusters_count = Clusters.objects.filter(site=self.site, status='new').exclude(ideas__isnull=False).count()
|
||||
ideas_count = ContentIdeas.objects.filter(site=self.site, status='new').count()
|
||||
tasks_count = Tasks.objects.filter(site=self.site, status='queued', content__isnull=True).count()
|
||||
tasks_count = Tasks.objects.filter(site=self.site, status='queued').count()
|
||||
content_count = Content.objects.filter(site=self.site, status='draft').annotate(images_count=Count('images')).filter(images_count=0).count()
|
||||
|
||||
# Estimate credits
|
||||
clustering_credits = (keywords_count // 5) + 1 # 1 credit per 5 keywords
|
||||
clustering_credits = (keywords_count // 5) + 1 if keywords_count > 0 else 0 # 1 credit per 5 keywords
|
||||
ideas_credits = clusters_count * 2 # 2 credits per cluster
|
||||
content_credits = tasks_count * 5 # Assume 2500 words avg = 5 credits
|
||||
prompts_credits = content_count * 2 # Assume 4 prompts per content = 2 credits
|
||||
|
||||
@@ -27,7 +27,7 @@ class AutomationViewSet(viewsets.ViewSet):
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
site = get_object_or_404(Site, id=site_id, account__user=request.user)
|
||||
site = get_object_or_404(Site, id=site_id, account=request.user.account)
|
||||
return site, None
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
@@ -308,6 +308,6 @@ class AutomationViewSet(viewsets.ViewSet):
|
||||
|
||||
return Response({
|
||||
'estimated_credits': estimated_credits,
|
||||
'current_balance': site.account.credits_balance,
|
||||
'sufficient': site.account.credits_balance >= (estimated_credits * 1.2)
|
||||
'current_balance': site.account.credits,
|
||||
'sufficient': site.account.credits >= (estimated_credits * 1.2)
|
||||
})
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
"""
|
||||
Tests for DeploymentService
|
||||
DEPRECATED: Tests for DeploymentService - SiteBlueprint models removed
|
||||
Phase 5: Sites Renderer & Publishing
|
||||
"""
|
||||
from django.test import TestCase
|
||||
from django.utils import timezone
|
||||
|
||||
from igny8_core.auth.models import Account, Site, Sector, User, Plan, Industry, IndustrySector
|
||||
from igny8_core.business.site_building.models import SiteBlueprint
|
||||
from igny8_core.business.publishing.models import DeploymentRecord
|
||||
from igny8_core.business.publishing.services.deployment_service import DeploymentService
|
||||
|
||||
|
||||
class DeploymentServiceTestCase(TestCase):
|
||||
"""Test cases for DeploymentService"""
|
||||
"""DEPRECATED: Test cases for DeploymentService"""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test data"""
|
||||
@@ -69,14 +68,8 @@ class DeploymentServiceTestCase(TestCase):
|
||||
name="Test Sector",
|
||||
slug="test-sector"
|
||||
)
|
||||
self.blueprint = SiteBlueprint.objects.create(
|
||||
account=self.account,
|
||||
site=self.site,
|
||||
sector=self.sector,
|
||||
name="Test Blueprint",
|
||||
status='ready',
|
||||
version=1
|
||||
)
|
||||
# DEPRECATED: SiteBlueprint model removed
|
||||
self.blueprint = None
|
||||
self.service = DeploymentService()
|
||||
|
||||
def test_get_status_returns_deployed_record(self):
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
"""
|
||||
Site Building Models
|
||||
Legacy SiteBuilder module has been removed.
|
||||
This file is kept for backwards compatibility with migrations.
|
||||
This file is kept for backwards compatibility with migrations and legacy code.
|
||||
"""
|
||||
from django.db import models
|
||||
from igny8_core.auth.models import AccountBaseModel
|
||||
|
||||
# All SiteBuilder models have been removed:
|
||||
# - SiteBlueprint
|
||||
@@ -13,3 +14,32 @@ from django.db import models
|
||||
# - BusinessType, AudienceProfile, BrandPersonality, HeroImageryDirection
|
||||
#
|
||||
# Taxonomy functionality moved to ContentTaxonomy model in business/content/models.py
|
||||
|
||||
# Stub classes for backwards compatibility with legacy imports
|
||||
class SiteBlueprint(AccountBaseModel):
|
||||
"""Legacy stub - SiteBuilder has been removed"""
|
||||
class Meta:
|
||||
app_label = 'site_building'
|
||||
db_table = 'legacy_site_blueprint_stub'
|
||||
managed = False # Don't create table
|
||||
|
||||
class PageBlueprint(AccountBaseModel):
|
||||
"""Legacy stub - SiteBuilder has been removed"""
|
||||
class Meta:
|
||||
app_label = 'site_building'
|
||||
db_table = 'legacy_page_blueprint_stub'
|
||||
managed = False # Don't create table
|
||||
|
||||
class SiteBlueprintCluster(AccountBaseModel):
|
||||
"""Legacy stub - SiteBuilder has been removed"""
|
||||
class Meta:
|
||||
app_label = 'site_building'
|
||||
db_table = 'legacy_site_blueprint_cluster_stub'
|
||||
managed = False # Don't create table
|
||||
|
||||
class SiteBlueprintTaxonomy(AccountBaseModel):
|
||||
"""Legacy stub - SiteBuilder has been removed"""
|
||||
class Meta:
|
||||
app_label = 'site_building'
|
||||
db_table = 'legacy_site_blueprint_taxonomy_stub'
|
||||
managed = False # Don't create table
|
||||
|
||||
@@ -13,13 +13,12 @@ from igny8_core.auth.models import (
|
||||
Site,
|
||||
User,
|
||||
)
|
||||
from igny8_core.business.site_building.models import PageBlueprint, SiteBlueprint
|
||||
|
||||
|
||||
class SiteBuilderTestBase(TestCase):
|
||||
"""
|
||||
Provides a lightweight set of fixtures (account/site/sector/blueprint)
|
||||
so Site Builder tests can focus on service logic instead of boilerplate.
|
||||
DEPRECATED: Provides a lightweight set of fixtures (account/site/sector/blueprint)
|
||||
SiteBlueprint models have been removed.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
@@ -65,20 +64,9 @@ class SiteBuilderTestBase(TestCase):
|
||||
account=self.account,
|
||||
)
|
||||
|
||||
self.blueprint = SiteBlueprint.objects.create(
|
||||
site=self.site,
|
||||
sector=self.sector,
|
||||
name='Core Blueprint',
|
||||
description='Initial blueprint used for tests',
|
||||
hosting_type='igny8_sites',
|
||||
config_json={
|
||||
'business_brief': 'Default brief',
|
||||
'objectives': ['Drive demos'],
|
||||
'style': {'palette': 'bold'},
|
||||
},
|
||||
)
|
||||
self.page_blueprint = PageBlueprint.objects.create(
|
||||
site_blueprint=self.blueprint,
|
||||
# DEPRECATED: SiteBlueprint and PageBlueprint models removed
|
||||
self.blueprint = None
|
||||
self.page_blueprint = None
|
||||
slug='home',
|
||||
title='Home',
|
||||
type='home',
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
"""
|
||||
Tests for Bulk Page Generation
|
||||
DEPRECATED: Tests for Bulk Page Generation - SiteBlueprint models removed
|
||||
Phase 5: Sites Renderer & Bulk Generation
|
||||
"""
|
||||
from django.test import TestCase
|
||||
from unittest.mock import patch, Mock
|
||||
|
||||
from igny8_core.auth.models import Account, Site, Sector
|
||||
from igny8_core.business.site_building.models import SiteBlueprint, PageBlueprint
|
||||
from igny8_core.business.site_building.services.page_generation_service import PageGenerationService
|
||||
from igny8_core.business.content.models import Tasks
|
||||
|
||||
from .base import SiteBuilderTestBase
|
||||
|
||||
|
||||
class BulkGenerationTestCase(SiteBuilderTestBase):
|
||||
"""Test cases for bulk page generation"""
|
||||
"""DEPRECATED: Test cases for bulk page generation"""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test data"""
|
||||
|
||||
@@ -52,3 +52,9 @@ urlpatterns = [
|
||||
path('api/docs/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
|
||||
path('api/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'),
|
||||
]
|
||||
|
||||
# Error handlers
|
||||
handler400 = 'django.views.defaults.bad_request'
|
||||
handler403 = 'django.views.defaults.permission_denied'
|
||||
handler404 = 'django.views.defaults.page_not_found'
|
||||
handler500 = 'django.views.defaults.server_error'
|
||||
|
||||
@@ -59,6 +59,3 @@ const ActivityLog: React.FC<ActivityLogProps> = ({ runId }) => {
|
||||
};
|
||||
|
||||
export default ActivityLog;
|
||||
};
|
||||
|
||||
export default ActivityLog;
|
||||
|
||||
@@ -118,6 +118,3 @@ const RunHistory: React.FC<RunHistoryProps> = ({ siteId }) => {
|
||||
};
|
||||
|
||||
export default RunHistory;
|
||||
};
|
||||
|
||||
export default RunHistory;
|
||||
|
||||
Reference in New Issue
Block a user