Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 9 additions & 40 deletions src/headings/shared-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ import { load as cheerioLoad } from 'cheerio';
import SeoChecks from '../metatags/seo-checks.js';
import { getTopPagesForSiteId } from '../utils/data-access.js';
import { getObjectKeysUsingPrefix, getObjectFromKey } from '../utils/s3-utils.js';
import {
extractBrandGuidelinesFromProfile,
formatBrandGuidelinesToMarkdown,
} from '../utils/brand-profile.js';
import {
getHeadingLevel,
getHeadingContext,
Expand Down Expand Up @@ -141,50 +145,14 @@ export async function loadScrapeJson(url, site, allKeys, s3Client, S3_SCRAPER_BU
return getObjectFromKey(s3Client, S3_SCRAPER_BUCKET_NAME, s3Key, log);
}

/**
* Extract brand guidelines from brand profile
* @param {Object} brandProfile - Brand profile from site config
* @returns {Object} Formatted brand guidelines
*/
function extractBrandGuidelinesFromProfile(brandProfile) {
const mainProfile = brandProfile.main_profile || {};
// Extract brand persona (short description)
const brandPersona = mainProfile.brand_personality?.description || '';

// Extract tone
const toneAttributes = mainProfile.tone_attributes || {};
const primaryTones = toneAttributes.primary || [];
const tone = primaryTones.join(', ');

// Extract editorial guidelines
const editorialGuidelines = mainProfile.editorial_guidelines || {};
const dos = editorialGuidelines.dos || [];
const donts = editorialGuidelines.donts || [];

// Extract forbidden items
const languagePatterns = mainProfile.language_patterns || {};
const avoidPatterns = languagePatterns.avoid || [];
const avoidTones = toneAttributes.avoid || [];
const forbidden = [...avoidPatterns, ...avoidTones];

return {
brand_persona: brandPersona,
tone,
editorial_guidelines: {
do: dos,
dont: donts,
},
forbidden,
};
}

/**
* Get brand guidelines from site config or generate from healthy tags using AI
* Returns formatted markdown string suitable for AI prompts
* @param {Object} healthyTagsObject - Object with healthy title, description, h1
* @param {Object} log - Logger instance
* @param {Object} context - Audit context
* @param {Object} site - Site object (optional, for accessing brand profile)
* @returns {Promise<Object>} Brand guidelines
* @returns {Promise<string>} Brand guidelines formatted as markdown string
*/
export async function getBrandGuidelines(healthyTagsObject, log, context, site = null) {
// First, try to get brand profile from site config
Expand All @@ -195,8 +163,9 @@ export async function getBrandGuidelines(healthyTagsObject, log, context, site =
if (brandProfile && typeof brandProfile === 'object' && Object.keys(brandProfile).length > 0) {
log.info('[Brand Guidelines] Using brand profile from site config');
const guidelines = extractBrandGuidelinesFromProfile(brandProfile);
log.debug(`[Brand Guidelines] Extracted guidelines: ${JSON.stringify(guidelines)}`);
return guidelines;
const formattedGuidelines = formatBrandGuidelinesToMarkdown(guidelines);
log.debug(`[Brand Guidelines] Extracted guidelines: ${formattedGuidelines}`);
return formattedGuidelines;
}
} catch (error) {
log.warn(`[Brand Guidelines] Error accessing brand profile from site config: ${error.message}`);
Expand Down
185 changes: 185 additions & 0 deletions src/utils/brand-profile.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
/*
* Copyright 2025 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/

/**
* Brand Profile Utilities
*
* Shared functions for extracting and formatting brand profile data.
* Matches the extraction used in Mystique BrandProfileTool.py for consistency
* across Readability, Summarization, Headings, and Content AI.
*/

/**
* Extract brand guidelines from brand profile
* Matches the extraction used in Mystique BrandProfileTool.py for Readability/Summarization
* @param {Object} brandProfile - Brand profile from site config
* @returns {Object} Formatted brand guidelines
*/
export function extractBrandGuidelinesFromProfile(brandProfile) {
const mainProfile = brandProfile.main_profile || {};

// Extract tone attributes (primary and avoid)
const toneAttributes = mainProfile.tone_attributes || {};
const tonePrimary = toneAttributes.primary || [];
const toneAvoid = toneAttributes.avoid || [];

// Extract vocabulary - signature phrases (top 5)
const vocabulary = mainProfile.vocabulary || {};
const signaturePhrases = (vocabulary.signature_phrases || []).slice(0, 5);

// Extract brand values - core values (top 5)
const brandValues = mainProfile.brand_values || {};
const coreValues = (brandValues.core_values || []).slice(0, 5).map((v) => ({
name: v.name || '',
evidence: v.evidence || '',
}));

// Extract language patterns (preferred and avoid) (top 5 each)
const languagePatterns = mainProfile.language_patterns || {};
const languagePreferred = (languagePatterns.preferred || []).slice(0, 5);
const languageAvoid = (languagePatterns.avoid || []).slice(0, 5);

// Extract communication style
const communicationStyle = mainProfile.communication_style || '';

// Extract editorial guidelines (dos and donts) (top 5 each)
const editorialGuidelines = mainProfile.editorial_guidelines || {};
const editorialDos = (editorialGuidelines.dos || []).slice(0, 5);
const editorialDonts = (editorialGuidelines.donts || []).slice(0, 5);

return {
tone_attributes: {
primary: tonePrimary,
avoid: toneAvoid,
},
signature_phrases: signaturePhrases,
brand_values: coreValues,
language_patterns: {
preferred: languagePreferred,
avoid: languageAvoid,
},
communication_style: communicationStyle,
editorial_guidelines: {
dos: editorialDos,
donts: editorialDonts,
},
};
}

/**
* Format brand guidelines to markdown string for AI prompts
* Matches the format used in Mystique BrandProfileTool.py
* @param {Object} guidelines - Extracted brand guidelines object
* @returns {string} Formatted markdown string
*/
export function formatBrandGuidelinesToMarkdown(guidelines) {
const parts = ['## Brand Guidelines (from Brand Profile)'];

// Tone attributes
if (guidelines.tone_attributes) {
parts.push('\n### TONE ATTRIBUTES');
if (guidelines.tone_attributes.primary?.length > 0) {
parts.push(` ✓ MUST USE: ${guidelines.tone_attributes.primary.join(', ')}`);
}
if (guidelines.tone_attributes.avoid?.length > 0) {
parts.push(` ✗ MUST AVOID: ${guidelines.tone_attributes.avoid.join(', ')}`);
}
}

// Signature phrases
if (guidelines.signature_phrases?.length > 0) {
parts.push('\n### SIGNATURE PHRASES');
parts.push(' ✓ USE these phrases when relevant:');
guidelines.signature_phrases.forEach((phrase) => {
parts.push(` • "${phrase}"`);
});
}

// Brand values
if (guidelines.brand_values?.length > 0) {
parts.push('\n### BRAND VALUES');
guidelines.brand_values.forEach((value) => {
parts.push(` • ${value.name}: ${value.evidence}`);
});
}

// Language patterns
if (guidelines.language_patterns) {
parts.push('\n### LANGUAGE PATTERNS');
if (guidelines.language_patterns.preferred?.length > 0) {
parts.push(' ✓ Preferred:');
guidelines.language_patterns.preferred.forEach((item) => {
parts.push(` • ${item}`);
});
}
if (guidelines.language_patterns.avoid?.length > 0) {
parts.push(' ✗ Avoid:');
guidelines.language_patterns.avoid.forEach((item) => {
parts.push(` • ${item}`);
});
}
}

// Communication style
if (guidelines.communication_style) {
parts.push('\n### COMMUNICATION STYLE');
parts.push(` ${guidelines.communication_style}`);
}

// Editorial guidelines
if (guidelines.editorial_guidelines) {
parts.push('\n### EDITORIAL GUIDELINES');
if (guidelines.editorial_guidelines.dos?.length > 0) {
parts.push(' ✓ DO:');
guidelines.editorial_guidelines.dos.forEach((item) => {
parts.push(` • ${item}`);
});
}
if (guidelines.editorial_guidelines.donts?.length > 0) {
parts.push(" ✗ DON'T:");
guidelines.editorial_guidelines.donts.forEach((item) => {
parts.push(` • ${item}`);
});
}
}

return parts.join('\n');
}

/**
* Get formatted brand guidelines from a site's brand profile
* @param {Object} site - Site object with getConfig method
* @param {Object} log - Logger instance (optional)
* @returns {string} Formatted brand guidelines markdown string, or empty string if not available
*/
export function getBrandGuidelinesFromSite(site, log = null) {
if (!site) {
return '';
}

try {
const config = site.getConfig();
const brandProfile = config?.getBrandProfile?.();

if (brandProfile && typeof brandProfile === 'object' && Object.keys(brandProfile).length > 0) {
log?.info('[Brand Guidelines] Using brand profile from site config');
const guidelines = extractBrandGuidelinesFromProfile(brandProfile);
const formattedGuidelines = formatBrandGuidelinesToMarkdown(guidelines);
log?.debug(`[Brand Guidelines] Extracted guidelines: ${formattedGuidelines}`);
return formattedGuidelines;
}
} catch (error) {
log?.error(`[Brand Guidelines] Error accessing brand profile from site config: ${error.message}`);
}

return '';
}
22 changes: 21 additions & 1 deletion src/utils/content-ai.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
*/

import { ImsClient } from '@adobe/spacecat-shared-ims-client';
import { getBrandGuidelinesFromSite } from './brand-profile.js';

/**
* Calculates a weekly cron schedule set to run one hour from now.
Expand Down Expand Up @@ -208,6 +209,25 @@ export class ContentAIClient {

this.log?.info(`Creating ContentAI configuration for site ${baseURL} with cron schedule ${cronSchedule} and name ${name}`);

// Get brand guidelines from site config
const brandGuidelines = getBrandGuidelinesFromSite(site, this.log);
const hasBrandGuidelines = brandGuidelines && brandGuidelines.length > 0;

if (hasBrandGuidelines) {
this.log?.info(`[ContentAI] Brand guidelines found for site ${baseURL}`);
} else {
this.log?.info(`[ContentAI] No brand guidelines found for site ${baseURL}`);
}

// Build system prompt with optional brand guidelines
let systemPrompt = 'You are a helpful AI Assistant powering the search experience.\nYou will answer questions using the provided context.\n';

if (hasBrandGuidelines) {
systemPrompt += `\n**Brand Guidelines**:\n${brandGuidelines}\n\nWhen generating responses, follow the brand guidelines to ensure answers match the brand's tone, vocabulary, and editorial standards.\n`;
}

systemPrompt += '\nContext: {context}\n';

const contentAiData = {
steps: [
{
Expand All @@ -232,7 +252,7 @@ export class ContentAIClient {
name: 'Comprehensive Q&A assitant',
description: 'AI assistant for answering any user question about a topic in the indexed knowledge',
prompts: {
system: 'You are a helpful AI Assistant powering the search experience.\nYou will answer questions using the provided context.\nContext: {context}\n',
system: systemPrompt,
user: 'Please answer the following question: {question}\n',
},
},
Expand Down
Loading