more fixes
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user