diff --git a/src/prerender/guidance-handler.js b/src/prerender/guidance-handler.js index 1603b4bde9..70f215cfea 100644 --- a/src/prerender/guidance-handler.js +++ b/src/prerender/guidance-handler.js @@ -138,11 +138,13 @@ export default async function handler(message, context) { // Track valuable suggestion metrics for quality logging let valuableCount = 0; let validAiSummaryCount = 0; + let suggestionsWithPrompts = 0; + let totalPromptCount = 0; suggestions.forEach((incoming) => { // Handle potential null/undefined elements in suggestions array const { - url, aiSummary, valuable, + url, aiSummary, valuable, prompts, } = incoming || {}; if (!url) { @@ -171,12 +173,20 @@ export default async function handler(message, context) { } } + const hasNewPrompts = Array.isArray(prompts) && prompts.length > 0; + if (hasNewPrompts) { + suggestionsWithPrompts += 1; + totalPromptCount += prompts.length; + } + const updatedData = { ...currentData, // Use new summary if valid; otherwise preserve existing (don't overwrite with empty) aiSummary: hasValidAiSummary ? aiSummary : (currentData.aiSummary ?? ''), // Default to true if not provided, but respect explicit boolean from Mystique valuable: isValuable, + // Use new prompts if provided; otherwise preserve existing + prompts: hasNewPrompts ? prompts : (currentData.prompts ?? []), }; warnOnInvalidSuggestionData(updatedData, opportunity.getType(), log); @@ -200,7 +210,9 @@ export default async function handler(message, context) { isPaidLLMOCustomer=${isPaid}, totalSuggestions=${suggestionsToSave.length}, valuableSuggestions=${valuableCount}, - validAiSummaryCount=${validAiSummaryCount},`); + validAiSummaryCount=${validAiSummaryCount}, + suggestionsWithPrompts=${suggestionsWithPrompts}, + totalPromptCount=${totalPromptCount},`); } catch (error) { log.error(`${LOG_PREFIX} Error batch saving suggestions: ${error.message}`); throw error; diff --git a/src/prerender/handler.js b/src/prerender/handler.js index 7548501dc9..f21e428a00 100644 --- a/src/prerender/handler.js +++ b/src/prerender/handler.js @@ -451,7 +451,13 @@ async function fetchLatestScrapeJobId(siteId, context) { * @param {Object} context - Processing context * @returns {Promise} - Number of suggestions sent to Mystique */ -async function sendPrerenderGuidanceRequestToMystique(auditUrl, auditData, opportunity, context) { +async function sendPrerenderGuidanceRequestToMystique( + auditUrl, + auditData, + opportunity, + context, + generatePrompts = false, +) { const { log, sqs, env, site, } = context; @@ -540,6 +546,8 @@ async function sendPrerenderGuidanceRequestToMystique(auditUrl, auditData, oppor url: data.url, originalHtmlMarkdownKey, markdownDiffKey, + // Signal whether this suggestion already has prompts so Mystique can skip re-generation + hasPrompts: Array.isArray(data.prompts) && data.prompts.length > 0, }); }); @@ -548,6 +556,33 @@ async function sendPrerenderGuidanceRequestToMystique(auditUrl, auditData, oppor return 0; } + // Send LLMO site config category/topic/region names to Mystique for prompt classification. + // Mirrors how the rcv-prompts utility injects llmoCategoryNames/llmoTopicNames/llmoRegionNames. + // Gracefully degrades to empty arrays if config is unavailable. + let llmoCategories = []; + let llmoTopics = []; + let llmoRegions = []; + /* c8 ignore start - LLMO config read is best-effort; tested separately */ + try { + const llmoCfg = site?.getConfig?.()?.getLlmoConfig?.(); + if (llmoCfg) { + llmoCategories = Object.values(llmoCfg.categories || {}) + .map((c) => c.name).filter(Boolean); + const allTopics = { ...llmoCfg.topics, ...llmoCfg.aiTopics }; + llmoTopics = Object.values(allTopics).map((t) => t.name).filter(Boolean); + const regionSet = new Set(); + Object.values(llmoCfg.categories || {}).forEach((cat) => { + if (Array.isArray(cat.region)) cat.region.forEach((r) => regionSet.add(r)); + else if (cat.region) regionSet.add(cat.region); + }); + llmoRegions = [...regionSet]; + log.debug(`${LOG_PREFIX} Loaded LLMO config: ${llmoCategories.length} categories, ${llmoTopics.length} topics, ${llmoRegions.length} regions. baseUrl=${baseUrl}`); + } + } catch (llmoErr) { + log.warn(`${LOG_PREFIX} Failed to read LLMO config for prompt classification (non-fatal): ${llmoErr.message}. baseUrl=${baseUrl}`); + } + /* c8 ignore stop */ + const deliveryType = site?.getDeliveryType?.() || 'unknown'; const message = { @@ -559,6 +594,10 @@ async function sendPrerenderGuidanceRequestToMystique(auditUrl, auditData, oppor data: { opportunityId, suggestions: suggestionsPayload, + generatePrompts, + llmoCategories, + llmoTopics, + llmoRegions, }, }; @@ -589,14 +628,16 @@ export async function handleAiOnlyMode(context) { const siteId = site.getId(); const baseUrl = site.getBaseURL(); - // Parse optional params from data field (opportunityId, scrapeJobId) + // Parse optional params from data field (opportunityId, scrapeJobId, generatePrompts) let opportunityId = null; let scrapeJobId = null; + let generatePrompts = false; if (data) { try { const parsedData = typeof data === 'string' ? JSON.parse(data) : data; opportunityId = parsedData.opportunityId; scrapeJobId = parsedData.scrapeJobId; + generatePrompts = !!parsedData.generatePrompts; } catch (e) { // Ignore parse errors - graceful degradation for malformed JSON } @@ -679,6 +720,7 @@ export async function handleAiOnlyMode(context) { auditData, opportunity, context, + generatePrompts, ); log.info(`${LOG_PREFIX} ai-only: Successfully queued AI summary request for ${suggestionCount} suggestion(s). baseUrl=${baseUrl}, siteId=${siteId}, opportunityId=${opportunity.getId()}`);