@avocadostudio-ai/orchestrator-core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (196) hide show
  1. package/LICENSE +201 -0
  2. package/dist/agent/agent-context.d.ts +19 -0
  3. package/dist/agent/agent-context.js +67 -0
  4. package/dist/agent/agent-logger.d.ts +5 -0
  5. package/dist/agent/agent-logger.js +22 -0
  6. package/dist/agent/agent-loop-openai.d.ts +8 -0
  7. package/dist/agent/agent-loop-openai.js +172 -0
  8. package/dist/agent/agent-loop.d.ts +56 -0
  9. package/dist/agent/agent-loop.js +167 -0
  10. package/dist/agent/agent-provider.d.ts +28 -0
  11. package/dist/agent/agent-provider.js +63 -0
  12. package/dist/agent/agent-tools.d.ts +28 -0
  13. package/dist/agent/agent-tools.js +899 -0
  14. package/dist/agent/context/editing-guidelines.md +46 -0
  15. package/dist/agent/context/role.md +39 -0
  16. package/dist/agent/integration-prompt.d.ts +9 -0
  17. package/dist/agent/integration-prompt.js +154 -0
  18. package/dist/agent/sites-agent-context.d.ts +12 -0
  19. package/dist/agent/sites-agent-context.js +316 -0
  20. package/dist/agent/sites-agent-shared.d.ts +161 -0
  21. package/dist/agent/sites-agent-shared.js +1101 -0
  22. package/dist/agent/sites-agent-tools.d.ts +18 -0
  23. package/dist/agent/sites-agent-tools.js +1227 -0
  24. package/dist/chat/anthropic-cache.d.ts +20 -0
  25. package/dist/chat/anthropic-cache.js +54 -0
  26. package/dist/chat/anthropic-planner.d.ts +98 -0
  27. package/dist/chat/anthropic-planner.js +1012 -0
  28. package/dist/chat/changelog-coverage-validator.d.ts +37 -0
  29. package/dist/chat/changelog-coverage-validator.js +215 -0
  30. package/dist/chat/chat-pipeline-context.d.ts +211 -0
  31. package/dist/chat/chat-pipeline-context.js +249 -0
  32. package/dist/chat/chat-pipeline-deterministic.d.ts +61 -0
  33. package/dist/chat/chat-pipeline-deterministic.js +407 -0
  34. package/dist/chat/chat-pipeline-image.d.ts +86 -0
  35. package/dist/chat/chat-pipeline-image.js +897 -0
  36. package/dist/chat/chat-pipeline-shared.d.ts +69 -0
  37. package/dist/chat/chat-pipeline-shared.js +212 -0
  38. package/dist/chat/chat-pipeline-translation.d.ts +27 -0
  39. package/dist/chat/chat-pipeline-translation.js +417 -0
  40. package/dist/chat/chat-pipeline-ui.d.ts +14 -0
  41. package/dist/chat/chat-pipeline-ui.js +244 -0
  42. package/dist/chat/chat-pipeline.d.ts +99 -0
  43. package/dist/chat/chat-pipeline.js +3999 -0
  44. package/dist/chat/decomposer.d.ts +21 -0
  45. package/dist/chat/decomposer.js +65 -0
  46. package/dist/chat/gemini-planner.d.ts +70 -0
  47. package/dist/chat/gemini-planner.js +541 -0
  48. package/dist/chat/hallucination-validator.d.ts +36 -0
  49. package/dist/chat/hallucination-validator.js +110 -0
  50. package/dist/chat/locale-strings.d.ts +47 -0
  51. package/dist/chat/locale-strings.js +100 -0
  52. package/dist/chat/plan-json-schema.d.ts +133 -0
  53. package/dist/chat/plan-json-schema.js +112 -0
  54. package/dist/chat/planner-types.d.ts +120 -0
  55. package/dist/chat/planner-types.js +66 -0
  56. package/dist/chat/planner.d.ts +148 -0
  57. package/dist/chat/planner.js +1361 -0
  58. package/dist/chat/prompts.d.ts +67 -0
  59. package/dist/chat/prompts.js +356 -0
  60. package/dist/chat/provider-routing.d.ts +14 -0
  61. package/dist/chat/provider-routing.js +27 -0
  62. package/dist/chat/variation-pipeline.d.ts +135 -0
  63. package/dist/chat/variation-pipeline.js +837 -0
  64. package/dist/chat/vision-alt-generator.d.ts +35 -0
  65. package/dist/chat/vision-alt-generator.js +152 -0
  66. package/dist/cms/adapter.d.ts +62 -0
  67. package/dist/cms/adapter.js +1 -0
  68. package/dist/cms/bootstrap.d.ts +17 -0
  69. package/dist/cms/bootstrap.js +85 -0
  70. package/dist/cms/editor-api-adapter.d.ts +23 -0
  71. package/dist/cms/editor-api-adapter.js +71 -0
  72. package/dist/cms/index.d.ts +4 -0
  73. package/dist/cms/index.js +3 -0
  74. package/dist/cms/json-file-adapter.d.ts +11 -0
  75. package/dist/cms/json-file-adapter.js +62 -0
  76. package/dist/demo-mode.d.ts +59 -0
  77. package/dist/demo-mode.js +201 -0
  78. package/dist/errors.d.ts +67 -0
  79. package/dist/errors.js +129 -0
  80. package/dist/http/chat-stream-resumable.d.ts +108 -0
  81. package/dist/http/chat-stream-resumable.js +290 -0
  82. package/dist/http/chat-stream.d.ts +99 -0
  83. package/dist/http/chat-stream.js +92 -0
  84. package/dist/image/gdrive-client.d.ts +22 -0
  85. package/dist/image/gdrive-client.js +215 -0
  86. package/dist/image/image-helpers.d.ts +95 -0
  87. package/dist/image/image-helpers.js +488 -0
  88. package/dist/index.d.ts +1 -0
  89. package/dist/index.js +1 -0
  90. package/dist/jira/jira-approval.d.ts +22 -0
  91. package/dist/jira/jira-approval.js +51 -0
  92. package/dist/jira/jira-client.d.ts +44 -0
  93. package/dist/jira/jira-client.js +313 -0
  94. package/dist/jira/jira-poller.d.ts +46 -0
  95. package/dist/jira/jira-poller.js +184 -0
  96. package/dist/jira/jira-processor.d.ts +103 -0
  97. package/dist/jira/jira-processor.js +1085 -0
  98. package/dist/jira/jira-types.d.ts +117 -0
  99. package/dist/jira/jira-types.js +38 -0
  100. package/dist/logger.d.ts +12 -0
  101. package/dist/logger.js +28 -0
  102. package/dist/migration/mcp-server-stdio.d.ts +8 -0
  103. package/dist/migration/mcp-server-stdio.js +672 -0
  104. package/dist/migration/migration-prompt.d.ts +7 -0
  105. package/dist/migration/migration-prompt.js +197 -0
  106. package/dist/migration/migration-tools.d.ts +17 -0
  107. package/dist/migration/migration-tools.js +159 -0
  108. package/dist/migration/scrape-cache.d.ts +9 -0
  109. package/dist/migration/scrape-cache.js +19 -0
  110. package/dist/nlp/deterministic-planner-context.d.ts +141 -0
  111. package/dist/nlp/deterministic-planner-context.js +362 -0
  112. package/dist/nlp/deterministic-planner-pages.d.ts +26 -0
  113. package/dist/nlp/deterministic-planner-pages.js +170 -0
  114. package/dist/nlp/deterministic-planner-patches.d.ts +80 -0
  115. package/dist/nlp/deterministic-planner-patches.js +508 -0
  116. package/dist/nlp/deterministic-planner-refs.d.ts +33 -0
  117. package/dist/nlp/deterministic-planner-refs.js +164 -0
  118. package/dist/nlp/deterministic-planner-suggestions.d.ts +49 -0
  119. package/dist/nlp/deterministic-planner-suggestions.js +579 -0
  120. package/dist/nlp/deterministic-planner.d.ts +85 -0
  121. package/dist/nlp/deterministic-planner.js +1631 -0
  122. package/dist/nlp/intent-detection.d.ts +309 -0
  123. package/dist/nlp/intent-detection.js +730 -0
  124. package/dist/nlp/intent-helpers.d.ts +15 -0
  125. package/dist/nlp/intent-helpers.js +243 -0
  126. package/dist/nlp/intent-patterns.d.ts +40 -0
  127. package/dist/nlp/intent-patterns.js +223 -0
  128. package/dist/nlp/plan-normalizer.d.ts +41 -0
  129. package/dist/nlp/plan-normalizer.js +1537 -0
  130. package/dist/ops/destructive-action-gate.d.ts +44 -0
  131. package/dist/ops/destructive-action-gate.js +90 -0
  132. package/dist/ops/ops-engine.d.ts +151 -0
  133. package/dist/ops/ops-engine.js +1394 -0
  134. package/dist/publish/diff-engine.d.ts +18 -0
  135. package/dist/publish/diff-engine.js +305 -0
  136. package/dist/publish/publish-helpers.d.ts +87 -0
  137. package/dist/publish/publish-helpers.js +521 -0
  138. package/dist/publish/publish-target-registry.d.ts +7 -0
  139. package/dist/publish/publish-target-registry.js +61 -0
  140. package/dist/publish/publish-target.d.ts +81 -0
  141. package/dist/publish/publish-target.js +1 -0
  142. package/dist/publish/targets/deploy-hook.d.ts +13 -0
  143. package/dist/publish/targets/deploy-hook.js +123 -0
  144. package/dist/publish/targets/git.d.ts +13 -0
  145. package/dist/publish/targets/git.js +55 -0
  146. package/dist/publish/targets/site-contract.d.ts +19 -0
  147. package/dist/publish/targets/site-contract.js +124 -0
  148. package/dist/state/content-source.d.ts +17 -0
  149. package/dist/state/content-source.js +1 -0
  150. package/dist/state/in-memory-content-source.d.ts +27 -0
  151. package/dist/state/in-memory-content-source.js +51 -0
  152. package/dist/state/session-lock.d.ts +13 -0
  153. package/dist/state/session-lock.js +29 -0
  154. package/dist/state/session-state.d.ts +310 -0
  155. package/dist/state/session-state.js +1083 -0
  156. package/dist/state/sqlite-store-singleton.d.ts +31 -0
  157. package/dist/state/sqlite-store-singleton.js +170 -0
  158. package/dist/state/sqlite-store.d.ts +135 -0
  159. package/dist/state/sqlite-store.js +421 -0
  160. package/dist/telemetry/chat-telemetry.d.ts +105 -0
  161. package/dist/telemetry/chat-telemetry.js +247 -0
  162. package/dist/telemetry/eval-candidate-store.d.ts +50 -0
  163. package/dist/telemetry/eval-candidate-store.js +120 -0
  164. package/dist/telemetry/feedback-store.d.ts +34 -0
  165. package/dist/telemetry/feedback-store.js +76 -0
  166. package/dist/telemetry/jira-telemetry.d.ts +57 -0
  167. package/dist/telemetry/jira-telemetry.js +68 -0
  168. package/dist/telemetry/migration-telemetry.d.ts +35 -0
  169. package/dist/telemetry/migration-telemetry.js +40 -0
  170. package/dist/telemetry/usage.d.ts +24 -0
  171. package/dist/telemetry/usage.js +80 -0
  172. package/dist/tools/builtin-registrations.d.ts +12 -0
  173. package/dist/tools/builtin-registrations.js +33 -0
  174. package/dist/tools/builtins/gdrive-browse.d.ts +3 -0
  175. package/dist/tools/builtins/gdrive-browse.js +68 -0
  176. package/dist/tools/builtins/image-generate.d.ts +3 -0
  177. package/dist/tools/builtins/image-generate.js +211 -0
  178. package/dist/tools/builtins/unsplash-get-by-id.d.ts +23 -0
  179. package/dist/tools/builtins/unsplash-get-by-id.js +119 -0
  180. package/dist/tools/builtins/unsplash-search.d.ts +3 -0
  181. package/dist/tools/builtins/unsplash-search.js +74 -0
  182. package/dist/tools/executor.d.ts +23 -0
  183. package/dist/tools/executor.js +169 -0
  184. package/dist/tools/index.d.ts +5 -0
  185. package/dist/tools/index.js +5 -0
  186. package/dist/tools/registry.d.ts +21 -0
  187. package/dist/tools/registry.js +75 -0
  188. package/dist/tools/runtime.d.ts +27 -0
  189. package/dist/tools/runtime.js +48 -0
  190. package/dist/tools/schema-validator.d.ts +24 -0
  191. package/dist/tools/schema-validator.js +88 -0
  192. package/dist/tools/types.d.ts +86 -0
  193. package/dist/tools/types.js +1 -0
  194. package/dist/variation-images.d.ts +19 -0
  195. package/dist/variation-images.js +12 -0
  196. package/package.json +78 -0
@@ -0,0 +1,837 @@
1
+ import OpenAI from "openai";
2
+ import Anthropic from "@anthropic-ai/sdk";
3
+ import { z } from "zod";
4
+ import { validateBlockProps } from "@avocadostudio-ai/shared";
5
+ import { buildVariationSystemPrompt } from "./prompts.js";
6
+ import { getPage } from "../state/session-state.js";
7
+ import { withSiteContext } from "../nlp/intent-detection.js";
8
+ import { coercePatchForBlock } from "../nlp/deterministic-planner.js";
9
+ import { openAIChatOptionsForModel } from "./planner.js";
10
+ import { extractJsonObject } from "../nlp/plan-normalizer.js";
11
+ import { extractUsage, estimateUsd } from "../telemetry/usage.js";
12
+ import { anthropicSystemPromptWithCache } from "./anthropic-cache.js";
13
+ import { resolveEffectiveProvider, resolveModelKeyForProvider, resolvePlannerSource } from "./provider-routing.js";
14
+ import { deriveVariationImageIntent, buildVariationImageQuery, buildVariationImagePrompt, generateVariationImageWithOpenAI, generateVariationImageWithGemini, resolveUnsplashImage } from "../image/image-helpers.js";
15
+ import { firstUrlFromText, resolveEffectiveSlug } from "./chat-pipeline.js";
16
+ import { resolveDistinctUnsplashImage } from "../variation-images.js";
17
+ // ---------------------------------------------------------------------------
18
+ // Types
19
+ // ---------------------------------------------------------------------------
20
+ export const variationRequestBodySchema = z.object({
21
+ session: z.string().optional(),
22
+ siteId: z.string().optional(),
23
+ sitePurpose: z.string().optional(),
24
+ siteHosting: z.string().optional(),
25
+ businessContext: z.union([
26
+ z.object({ purpose: z.string().optional(), tone: z.string().optional(), constraints: z.array(z.string()).optional() }),
27
+ z.string()
28
+ ]).optional(),
29
+ siteContext: z.union([
30
+ z.object({ siteId: z.string().optional(), siteName: z.string().optional(), purpose: z.string().optional(), hosting: z.string().optional(), tone: z.string().optional(), constraints: z.array(z.string()).optional() }),
31
+ z.string()
32
+ ]).optional(),
33
+ slug: z.string().optional(),
34
+ message: z.string().optional(),
35
+ modelKey: z.enum(["fast", "balanced", "reasoning", "codex"]).optional(),
36
+ provider: z.enum(["openai", "anthropic", "gemini"]).optional(),
37
+ activeBlockId: z.string().optional(),
38
+ activeBlockType: z.string().optional(),
39
+ activeEditablePath: z.string().optional(),
40
+ locale: z.string().optional(),
41
+ });
42
+ const VARIATION_CACHE_TTL_MS = 5 * 60_000;
43
+ const lastVariationBySessionBlock = new Map();
44
+ const variationCacheKey = (session, blockId) => `${session}::${blockId}`;
45
+ export function getCachedVariations(session, blockId) {
46
+ const key = variationCacheKey(session, blockId);
47
+ const entry = lastVariationBySessionBlock.get(key);
48
+ if (!entry)
49
+ return null;
50
+ if (Date.now() - entry.storedAt > VARIATION_CACHE_TTL_MS) {
51
+ lastVariationBySessionBlock.delete(key);
52
+ return null;
53
+ }
54
+ return entry.result;
55
+ }
56
+ function rememberVariations(session, result) {
57
+ lastVariationBySessionBlock.set(variationCacheKey(session, result.blockId), {
58
+ result,
59
+ storedAt: Date.now()
60
+ });
61
+ }
62
+ let resolveUnsplashImageImpl = resolveUnsplashImage;
63
+ export function setResolveUnsplashImageForTests(fn) {
64
+ resolveUnsplashImageImpl = fn ?? resolveUnsplashImage;
65
+ }
66
+ let generateVariationImageForTests = null;
67
+ export function setGenerateVariationImageForTests(fn) {
68
+ generateVariationImageForTests = fn ?? null;
69
+ }
70
+ function resolveAiImageProvider() {
71
+ const raw = process.env.IMAGE_GEN_PROVIDER?.trim().toLowerCase();
72
+ if (raw === "openai")
73
+ return "openai";
74
+ // Default: gemini. Falls back to openai at call time if Gemini key is missing.
75
+ return "gemini";
76
+ }
77
+ async function generateVariationImage(args) {
78
+ if (generateVariationImageForTests) {
79
+ return generateVariationImageForTests({ prompt: args.prompt, altText: args.altText });
80
+ }
81
+ const provider = resolveAiImageProvider();
82
+ const hasGemini = !!process.env.GOOGLE_GENAI_API_KEY?.trim();
83
+ const hasOpenAI = !!process.env.OPENAI_API_KEY?.trim();
84
+ if (provider === "gemini" && hasGemini) {
85
+ return generateVariationImageWithGemini({
86
+ prompt: args.prompt,
87
+ altText: args.altText,
88
+ aspectRatio: "landscape",
89
+ quality: "draft",
90
+ log: args.log
91
+ });
92
+ }
93
+ if (hasOpenAI) {
94
+ return generateVariationImageWithOpenAI({
95
+ prompt: args.prompt,
96
+ altText: args.altText,
97
+ model: process.env.OPENAI_IMAGE_MODEL?.trim() || undefined,
98
+ log: args.log
99
+ });
100
+ }
101
+ if (hasGemini) {
102
+ return generateVariationImageWithGemini({
103
+ prompt: args.prompt,
104
+ altText: args.altText,
105
+ aspectRatio: "landscape",
106
+ quality: "draft",
107
+ log: args.log
108
+ });
109
+ }
110
+ return null;
111
+ }
112
+ // ---------------------------------------------------------------------------
113
+ // Utility
114
+ // ---------------------------------------------------------------------------
115
+ export function parseJsonMaybe(text) {
116
+ try {
117
+ return JSON.parse(text);
118
+ }
119
+ catch {
120
+ return null;
121
+ }
122
+ }
123
+ // ---------------------------------------------------------------------------
124
+ // Variation count
125
+ // ---------------------------------------------------------------------------
126
+ const DEFAULT_VARIATION_COUNT = 3;
127
+ const MAX_VARIATION_COUNT = 12;
128
+ const VARIATION_NOUN_PATTERN = "(?:variations?|variants?|alternatives?|options)";
129
+ export function requestedVariationCount(message) {
130
+ const normalized = message.toLowerCase().replace(/-/g, " ");
131
+ const numberMatch = normalized.match(new RegExp(`\\b(\\d{1,2})\\s+${VARIATION_NOUN_PATTERN}\\b`));
132
+ if (numberMatch) {
133
+ const parsed = Number.parseInt(numberMatch[1], 10);
134
+ if (Number.isFinite(parsed) && parsed > 0)
135
+ return Math.min(parsed, MAX_VARIATION_COUNT);
136
+ }
137
+ const wordsToNumbers = {
138
+ one: 1,
139
+ two: 2,
140
+ three: 3,
141
+ four: 4,
142
+ five: 5,
143
+ six: 6,
144
+ seven: 7,
145
+ eight: 8,
146
+ nine: 9,
147
+ ten: 10,
148
+ eleven: 11,
149
+ twelve: 12
150
+ };
151
+ for (const [word, value] of Object.entries(wordsToNumbers)) {
152
+ const re = new RegExp(`\\b${word}\\s+${VARIATION_NOUN_PATTERN}\\b`, "i");
153
+ if (re.test(normalized))
154
+ return value;
155
+ }
156
+ return DEFAULT_VARIATION_COUNT;
157
+ }
158
+ // ---------------------------------------------------------------------------
159
+ // Patch sanitization
160
+ // ---------------------------------------------------------------------------
161
+ function sanitizeVariationPatch(block, patch) {
162
+ const safePatch = coercePatchForBlock(block, patch);
163
+ if (Object.keys(safePatch).length === 0)
164
+ return null;
165
+ const nextProps = { ...block.props, ...safePatch };
166
+ const validated = validateBlockProps(block.type, nextProps);
167
+ if (!validated.success)
168
+ return null;
169
+ if (JSON.stringify(block.props) === JSON.stringify(validated.data))
170
+ return null;
171
+ return safePatch;
172
+ }
173
+ // ---------------------------------------------------------------------------
174
+ // Image variation support
175
+ // ---------------------------------------------------------------------------
176
+ function supportsImageVariation(block) {
177
+ return Object.prototype.hasOwnProperty.call(block.props, "imageUrl");
178
+ }
179
+ function prefersSameImageAcrossVariations(message) {
180
+ const lower = message.toLowerCase();
181
+ return (/\bsame\s+image\b/.test(lower) ||
182
+ /\bsame\s+photo\b/.test(lower) ||
183
+ /\bkeep\s+(the\s+)?same\s+image\b/.test(lower) ||
184
+ /\buse\s+one\s+image\b/.test(lower) ||
185
+ /\bsingle\s+image\b/.test(lower));
186
+ }
187
+ function buildVariationImageContext(args) {
188
+ const explicitUrl = firstUrlFromText(args.message);
189
+ const imageIntent = deriveVariationImageIntent({ message: args.message, block: args.block });
190
+ const noDuplicateRequested = /\bno\s+duplicates?\b/i.test(args.message) ||
191
+ /\bdo\s+not\s+reuse\b/i.test(args.message) ||
192
+ /\bunique\s+images?\b/i.test(args.message);
193
+ const enforceUniqueImages = !explicitUrl && !prefersSameImageAcrossVariations(args.message);
194
+ return { explicitUrl: explicitUrl ?? null, imageIntent, enforceUniqueImages, noDuplicateRequested };
195
+ }
196
+ async function resolveVariationImage(args) {
197
+ const { imageCtx, variationIndex } = args;
198
+ if (imageCtx.explicitUrl) {
199
+ return { url: imageCtx.explicitUrl, alt: `Image for ${args.block.type} variation`, query: "" };
200
+ }
201
+ if (imageCtx.imageIntent.provider === "llm") {
202
+ const blockProps = args.block.props;
203
+ const heading = typeof blockProps.heading === "string" ? blockProps.heading : "";
204
+ const subheading = typeof blockProps.subheading === "string" ? blockProps.subheading : "";
205
+ const sectionContext = [args.block.type, heading, subheading].filter(Boolean).join(" — ");
206
+ const pageContext = args.page ? `${args.page.title} (${args.page.slug})` : undefined;
207
+ const prompt = buildVariationImagePrompt({
208
+ intent: imageCtx.imageIntent,
209
+ blockType: args.block.type,
210
+ variationIndex,
211
+ sectionContext,
212
+ pageContext
213
+ });
214
+ return generateVariationImage({
215
+ prompt,
216
+ altText: `AI-generated ${args.block.type} image variation ${variationIndex + 1}`,
217
+ log: args.log
218
+ });
219
+ }
220
+ const query = buildVariationImageQuery(imageCtx.imageIntent, variationIndex);
221
+ if (imageCtx.enforceUniqueImages) {
222
+ return resolveDistinctUnsplashImage({
223
+ query,
224
+ variationIndex,
225
+ usedImageUrls: args.usedImageUrls,
226
+ resolveImage: async (queryValue, options) => resolveUnsplashImageImpl(queryValue, {
227
+ variationIndex: options?.variationIndex,
228
+ subjectKeywords: imageCtx.imageIntent.subjectKeywords,
229
+ usedImageUrls: args.usedImageUrls
230
+ }, { logger: args.log }),
231
+ maxAttempts: imageCtx.noDuplicateRequested ? 8 : 5
232
+ });
233
+ }
234
+ return resolveUnsplashImageImpl(query, {
235
+ variationIndex,
236
+ subjectKeywords: imageCtx.imageIntent.subjectKeywords,
237
+ usedImageUrls: imageCtx.noDuplicateRequested ? args.usedImageUrls : undefined
238
+ }, { logger: args.log });
239
+ }
240
+ function applyResolvedImageToPatch(args) {
241
+ const patch = { ...args.patch };
242
+ if (args.resolved) {
243
+ patch.imageUrl = args.resolved.url;
244
+ if (!Object.prototype.hasOwnProperty.call(patch, "imageAlt")) {
245
+ patch.imageAlt = args.resolved.alt;
246
+ }
247
+ }
248
+ else if (args.llmAuthoredImageUrl &&
249
+ !Object.prototype.hasOwnProperty.call(patch, "imageUrl") &&
250
+ Object.prototype.hasOwnProperty.call(patch, "imageAlt")) {
251
+ // LLM authored a fake imageUrl + matching alt; real resolution failed, so
252
+ // drop the orphan alt too to avoid describing the wrong (existing) image.
253
+ delete patch.imageAlt;
254
+ }
255
+ return patch;
256
+ }
257
+ export async function withDefaultImageVariations(args) {
258
+ if (!supportsImageVariation(args.block))
259
+ return args.variations;
260
+ const imageCtx = buildVariationImageContext({ block: args.block, message: args.message });
261
+ const usedImageUrls = new Set();
262
+ const out = [];
263
+ for (const [variationIndex, variation] of args.variations.entries()) {
264
+ const patchWithoutLlmImage = { ...variation.patch };
265
+ const llmAuthoredImageUrl = Object.prototype.hasOwnProperty.call(patchWithoutLlmImage, "imageUrl");
266
+ if (llmAuthoredImageUrl && !imageCtx.explicitUrl) {
267
+ delete patchWithoutLlmImage.imageUrl;
268
+ }
269
+ const resolved = await resolveVariationImage({
270
+ block: args.block,
271
+ imageCtx,
272
+ variationIndex,
273
+ usedImageUrls,
274
+ page: args.page,
275
+ log: args.log
276
+ });
277
+ if (resolved)
278
+ usedImageUrls.add(resolved.url);
279
+ const patch = applyResolvedImageToPatch({
280
+ patch: patchWithoutLlmImage,
281
+ resolved,
282
+ llmAuthoredImageUrl
283
+ });
284
+ const sanitized = sanitizeVariationPatch(args.block, patch);
285
+ if (!sanitized)
286
+ continue;
287
+ out.push({
288
+ ...variation,
289
+ patch: sanitized,
290
+ changedKeys: Object.keys(sanitized)
291
+ });
292
+ }
293
+ return out;
294
+ }
295
+ export function planDefaultImageVariations(args) {
296
+ if (!supportsImageVariation(args.block)) {
297
+ return {
298
+ initial: args.variations,
299
+ run: async () => args.variations
300
+ };
301
+ }
302
+ const imageCtx = buildVariationImageContext({ block: args.block, message: args.message });
303
+ const prepared = args.variations.map((variation, variationIndex) => {
304
+ const basePatch = { ...variation.patch };
305
+ const llmAuthoredImageUrl = Object.prototype.hasOwnProperty.call(basePatch, "imageUrl");
306
+ if (llmAuthoredImageUrl && !imageCtx.explicitUrl) {
307
+ delete basePatch.imageUrl;
308
+ }
309
+ const initialSanitized = sanitizeVariationPatch(args.block, basePatch);
310
+ const initial = initialSanitized
311
+ ? { ...variation, patch: initialSanitized, changedKeys: Object.keys(initialSanitized) }
312
+ : { ...variation, patch: basePatch, changedKeys: Object.keys(basePatch) };
313
+ // Note: when the text-only patch fails sanitization (e.g. it becomes empty
314
+ // after stripping imageUrl), we still keep the variation so the modal can
315
+ // show it while the image resolves. Final sanitization happens in run().
316
+ return { source: variation, variationIndex, basePatch, llmAuthoredImageUrl, initial };
317
+ });
318
+ const initial = prepared.map((p) => p.initial).filter(Boolean);
319
+ const run = async (onProgress) => {
320
+ const usedImageUrls = new Set();
321
+ const results = await Promise.all(prepared.map(async (p) => {
322
+ const resolved = await resolveVariationImage({
323
+ block: args.block,
324
+ imageCtx,
325
+ variationIndex: p.variationIndex,
326
+ usedImageUrls,
327
+ page: args.page,
328
+ log: args.log
329
+ }).catch((err) => {
330
+ args.log?.warn({ err, variationIndex: p.variationIndex }, "variation image resolution failed");
331
+ return null;
332
+ });
333
+ if (resolved)
334
+ usedImageUrls.add(resolved.url);
335
+ const patch = applyResolvedImageToPatch({
336
+ patch: p.basePatch,
337
+ resolved,
338
+ llmAuthoredImageUrl: p.llmAuthoredImageUrl
339
+ });
340
+ const sanitized = sanitizeVariationPatch(args.block, patch);
341
+ const finalOption = sanitized
342
+ ? { ...p.source, patch: sanitized, changedKeys: Object.keys(sanitized) }
343
+ : null;
344
+ if (finalOption) {
345
+ onProgress({
346
+ variationId: p.source.id,
347
+ index: p.variationIndex,
348
+ patch: finalOption.patch,
349
+ changedKeys: finalOption.changedKeys,
350
+ ...(resolved ? { imageUrl: resolved.url, imageAlt: resolved.alt } : {})
351
+ });
352
+ }
353
+ return finalOption;
354
+ }));
355
+ return results.filter((r) => r !== null);
356
+ };
357
+ return { initial, run };
358
+ }
359
+ // ---------------------------------------------------------------------------
360
+ // Text key inference
361
+ // ---------------------------------------------------------------------------
362
+ function inferVariationTextKey(block) {
363
+ const preferred = ["heading", "title", "subheading", "description", "body", "ctaText", "imageAlt"];
364
+ const props = block.props;
365
+ for (const key of preferred) {
366
+ if (typeof props[key] === "string" && props[key].trim().length > 0)
367
+ return key;
368
+ }
369
+ const firstString = Object.entries(props).find(([, value]) => typeof value === "string" && value.trim().length > 0);
370
+ return firstString?.[0] ?? null;
371
+ }
372
+ // ---------------------------------------------------------------------------
373
+ // Variation constraints
374
+ // ---------------------------------------------------------------------------
375
+ function variationConstraints(message, block) {
376
+ const lower = message.toLowerCase();
377
+ const keepTitle = /\bsame\s+title\b/.test(lower) ||
378
+ /\bkeep\s+(the\s+)?title\b/.test(lower) ||
379
+ /\btitle\s+(unchanged|same)\b/.test(lower);
380
+ const cardsOnly = block.type === "CardGrid" && (/\bcards?\s+only\b/.test(lower) || /\bonly\s+cards?\b/.test(lower));
381
+ return { keepTitle, cardsOnly };
382
+ }
383
+ function applyVariationConstraints(args) {
384
+ const constraints = variationConstraints(args.message, args.block);
385
+ let nextPatch = { ...args.patch };
386
+ if (constraints.keepTitle) {
387
+ delete nextPatch.title;
388
+ }
389
+ if (constraints.cardsOnly && args.block.type === "CardGrid") {
390
+ nextPatch = Object.prototype.hasOwnProperty.call(nextPatch, "cards") ? { cards: nextPatch.cards } : {};
391
+ }
392
+ return nextPatch;
393
+ }
394
+ // ---------------------------------------------------------------------------
395
+ // Deterministic variations
396
+ // ---------------------------------------------------------------------------
397
+ function deterministicCardGridVariations(args) {
398
+ if (args.block.type !== "CardGrid")
399
+ return [];
400
+ const cards = Array.isArray(args.block.props.cards) ? args.block.props.cards : [];
401
+ if (cards.length === 0)
402
+ return [];
403
+ const tones = [
404
+ {
405
+ title: "Crisp",
406
+ summary: "Shorter and more direct card copy.",
407
+ description: (cardTitle) => `${cardTitle} essentials in one quick guide.`,
408
+ ctaText: "Explore"
409
+ },
410
+ {
411
+ title: "Benefit-led",
412
+ summary: "Highlights outcomes in every card.",
413
+ description: (cardTitle) => `Get practical ${cardTitle.toLowerCase()} tips you can use right away.`,
414
+ ctaText: "See Benefits"
415
+ },
416
+ {
417
+ title: "Action-driven",
418
+ summary: "Pushes a stronger next step.",
419
+ description: (cardTitle) => `Start ${cardTitle.toLowerCase()} today with a clear step-by-step plan.`,
420
+ ctaText: "Start Now"
421
+ }
422
+ ];
423
+ const seen = new Set(args.existing.map((item) => JSON.stringify(item.patch)));
424
+ const out = [];
425
+ for (const tone of tones) {
426
+ if (args.existing.length + out.length >= args.count)
427
+ break;
428
+ const nextCards = cards.map((card) => {
429
+ const title = typeof card.title === "string" && card.title.trim().length > 0 ? card.title.trim() : "Card";
430
+ return {
431
+ ...card,
432
+ description: tone.description(title),
433
+ ctaText: tone.ctaText
434
+ };
435
+ });
436
+ const patch = sanitizeVariationPatch(args.block, { cards: nextCards });
437
+ if (!patch)
438
+ continue;
439
+ const key = JSON.stringify(patch);
440
+ if (seen.has(key))
441
+ continue;
442
+ seen.add(key);
443
+ out.push({
444
+ id: `var_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`,
445
+ title: tone.title,
446
+ summary: tone.summary,
447
+ patch,
448
+ changedKeys: Object.keys(patch)
449
+ });
450
+ }
451
+ return out;
452
+ }
453
+ function deterministicVariations(args) {
454
+ const { block, count } = args;
455
+ if (block.type === "CardGrid") {
456
+ return deterministicCardGridVariations({
457
+ block,
458
+ count,
459
+ existing: args.existing
460
+ });
461
+ }
462
+ const textKey = inferVariationTextKey(block);
463
+ if (!textKey)
464
+ return [];
465
+ const currentValue = String(block.props[textKey] ?? "").trim();
466
+ if (!currentValue)
467
+ return [];
468
+ const tones = [
469
+ { title: "Crisp", suffix: " Keep it concise and direct.", summary: "Shorter and more direct copy." },
470
+ { title: "Benefit-led", suffix: " Emphasize the user benefit first.", summary: "Highlights user outcomes." },
471
+ { title: "Action-driven", suffix: " Use a stronger action-oriented tone.", summary: "Adds stronger CTA energy." }
472
+ ];
473
+ const seen = new Set(args.existing.map((item) => JSON.stringify(item.patch)));
474
+ const out = [];
475
+ for (const tone of tones) {
476
+ if (args.existing.length + out.length >= count)
477
+ break;
478
+ const nextText = `${currentValue.replace(/\s+/g, " ").trim()}${tone.suffix}`;
479
+ const patch = sanitizeVariationPatch(block, { [textKey]: nextText });
480
+ if (!patch)
481
+ continue;
482
+ const key = JSON.stringify(patch);
483
+ if (seen.has(key))
484
+ continue;
485
+ seen.add(key);
486
+ out.push({
487
+ id: `var_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`,
488
+ title: tone.title,
489
+ summary: tone.summary,
490
+ patch,
491
+ changedKeys: Object.keys(patch)
492
+ });
493
+ }
494
+ return out;
495
+ }
496
+ // ---------------------------------------------------------------------------
497
+ // OpenAI variation generation
498
+ // ---------------------------------------------------------------------------
499
+ async function generateVariationsWithOpenAI(args) {
500
+ const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
501
+ const props = args.block.props;
502
+ const allowedKeys = Object.keys(props);
503
+ const constraints = variationConstraints(args.message, args.block);
504
+ const system = buildVariationSystemPrompt({
505
+ count: args.count,
506
+ keepTitle: constraints.keepTitle,
507
+ cardsOnly: constraints.cardsOnly,
508
+ blockType: args.block.type,
509
+ locale: args.locale,
510
+ });
511
+ const user = {
512
+ request: args.message,
513
+ blockId: args.block.id,
514
+ blockType: args.block.type,
515
+ currentProps: props,
516
+ allowedPatchKeys: allowedKeys
517
+ };
518
+ const completion = await client.chat.completions.create({
519
+ model: args.model,
520
+ ...openAIChatOptionsForModel(args.model),
521
+ response_format: { type: "json_object" },
522
+ messages: [
523
+ { role: "system", content: system },
524
+ { role: "user", content: JSON.stringify(user) }
525
+ ]
526
+ });
527
+ const usage = extractUsage(completion);
528
+ const raw = completion.choices[0]?.message?.content ?? "";
529
+ const parsed = parseJsonMaybe(raw);
530
+ const list = Array.isArray(parsed?.variations) ? parsed.variations : [];
531
+ const seen = new Set();
532
+ const out = [];
533
+ for (const item of list) {
534
+ if (out.length >= args.count)
535
+ break;
536
+ const constrainedPatch = applyVariationConstraints({
537
+ block: args.block,
538
+ message: args.message,
539
+ patch: coercePatchForBlock(args.block, item.patch)
540
+ });
541
+ const patch = sanitizeVariationPatch(args.block, constrainedPatch);
542
+ if (!patch)
543
+ continue;
544
+ const patchKey = JSON.stringify(patch);
545
+ if (seen.has(patchKey))
546
+ continue;
547
+ seen.add(patchKey);
548
+ out.push({
549
+ id: `var_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`,
550
+ title: typeof item.title === "string" && item.title.trim().length > 0 ? item.title.trim() : `Variation ${out.length + 1}`,
551
+ summary: typeof item.summary === "string" && item.summary.trim().length > 0 ? item.summary.trim() : "Alternative copy direction.",
552
+ patch,
553
+ changedKeys: Object.keys(patch)
554
+ });
555
+ }
556
+ return { variations: out, usage };
557
+ }
558
+ // ---------------------------------------------------------------------------
559
+ // Anthropic variation generation
560
+ // ---------------------------------------------------------------------------
561
+ async function generateVariationsWithAnthropic(args) {
562
+ const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
563
+ const props = args.block.props;
564
+ const allowedKeys = Object.keys(props);
565
+ const constraints = variationConstraints(args.message, args.block);
566
+ const system = buildVariationSystemPrompt({
567
+ count: args.count,
568
+ keepTitle: constraints.keepTitle,
569
+ cardsOnly: constraints.cardsOnly,
570
+ blockType: args.block.type,
571
+ locale: args.locale,
572
+ });
573
+ const user = {
574
+ request: args.message,
575
+ blockId: args.block.id,
576
+ blockType: args.block.type,
577
+ currentProps: props,
578
+ allowedPatchKeys: allowedKeys
579
+ };
580
+ const response = await client.messages.create({
581
+ model: args.model,
582
+ max_tokens: 4096,
583
+ system: anthropicSystemPromptWithCache(system),
584
+ messages: [
585
+ { role: "user", content: JSON.stringify(user) }
586
+ ],
587
+ });
588
+ const usage = extractUsage(response);
589
+ const textBlock = response.content.find((b) => b.type === "text");
590
+ const rawText = textBlock && "text" in textBlock ? textBlock.text : "";
591
+ const jsonText = extractJsonObject(rawText);
592
+ const parsed = jsonText ? parseJsonMaybe(jsonText) : null;
593
+ const list = Array.isArray(parsed?.variations) ? parsed.variations : [];
594
+ const seen = new Set();
595
+ const out = [];
596
+ for (const item of list) {
597
+ if (out.length >= args.count)
598
+ break;
599
+ const constrainedPatch = applyVariationConstraints({
600
+ block: args.block,
601
+ message: args.message,
602
+ patch: coercePatchForBlock(args.block, item.patch)
603
+ });
604
+ const patch = sanitizeVariationPatch(args.block, constrainedPatch);
605
+ if (!patch)
606
+ continue;
607
+ const patchKey = JSON.stringify(patch);
608
+ if (seen.has(patchKey))
609
+ continue;
610
+ seen.add(patchKey);
611
+ out.push({
612
+ id: `var_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`,
613
+ title: typeof item.title === "string" && item.title.trim().length > 0 ? item.title.trim() : `Variation ${out.length + 1}`,
614
+ summary: typeof item.summary === "string" && item.summary.trim().length > 0 ? item.summary.trim() : "Alternative copy direction.",
615
+ patch,
616
+ changedKeys: Object.keys(patch)
617
+ });
618
+ }
619
+ return { variations: out, usage };
620
+ }
621
+ async function prepareTextVariations(ctx, body) {
622
+ if (!body.session || !body.slug || !body.message) {
623
+ return { code: 400, payload: { error: "session, slug, and message are required" } };
624
+ }
625
+ const contextualMessage = withSiteContext(body.message, {
626
+ sitePurpose: body.sitePurpose,
627
+ siteHosting: body.siteHosting,
628
+ businessContext: body.businessContext,
629
+ siteContext: body.siteContext
630
+ });
631
+ const requestedSlug = body.slug;
632
+ const effectiveSlug = resolveEffectiveSlug({
633
+ session: body.session,
634
+ requestedSlug,
635
+ activeBlockId: body.activeBlockId
636
+ });
637
+ if (!body.activeBlockId) {
638
+ return { code: 400, payload: { error: "Select a block first before generating variations." } };
639
+ }
640
+ const page = getPage(body.session, effectiveSlug);
641
+ if (!page)
642
+ return { code: 404, payload: { error: "page not found" } };
643
+ const selected = page.blocks.find((block) => block.id === body.activeBlockId);
644
+ if (!selected) {
645
+ return { code: 404, payload: { error: "selected block not found on current page" } };
646
+ }
647
+ const requestedProvider = body.provider ?? ctx.availableProviders[0];
648
+ const provider = resolveEffectiveProvider({
649
+ requestedProvider,
650
+ availableProviders: ctx.availableProviders,
651
+ fallbackProvider: "openai"
652
+ });
653
+ const modelKey = resolveModelKeyForProvider({
654
+ requestedModelKey: body.modelKey,
655
+ provider,
656
+ modelLookup: ctx.modelLookup,
657
+ defaultModelKey: process.env.OPENAI_MODEL_KEY ?? "balanced"
658
+ });
659
+ const modelUsed = ctx.modelLookup[provider][modelKey];
660
+ const count = requestedVariationCount(contextualMessage);
661
+ const plannerSource = resolvePlannerSource(provider);
662
+ let variations = [];
663
+ let generatorUsage;
664
+ if (plannerSource === "anthropic") {
665
+ try {
666
+ const result = await generateVariationsWithAnthropic({
667
+ block: selected,
668
+ message: contextualMessage,
669
+ model: modelUsed,
670
+ modelKey,
671
+ count,
672
+ locale: body.locale
673
+ });
674
+ variations = result.variations;
675
+ generatorUsage = result.usage;
676
+ }
677
+ catch (err) {
678
+ ctx.log.warn({ err, provider: "anthropic", model: modelUsed, blockType: selected.type }, "variation pipeline: anthropic generation failed; falling back to deterministic");
679
+ variations = [];
680
+ }
681
+ }
682
+ else if (plannerSource === "openai") {
683
+ try {
684
+ const result = await generateVariationsWithOpenAI({
685
+ block: selected,
686
+ message: contextualMessage,
687
+ model: modelUsed,
688
+ modelKey,
689
+ count,
690
+ locale: body.locale
691
+ });
692
+ variations = result.variations;
693
+ generatorUsage = result.usage;
694
+ }
695
+ catch (err) {
696
+ ctx.log.warn({ err, provider: "openai", model: modelUsed, blockType: selected.type }, "variation pipeline: openai generation failed; falling back to deterministic");
697
+ variations = [];
698
+ }
699
+ }
700
+ if (variations.length < count) {
701
+ const fallback = deterministicVariations({
702
+ block: selected,
703
+ message: contextualMessage,
704
+ count,
705
+ existing: variations
706
+ });
707
+ variations = [...variations, ...fallback].slice(0, count);
708
+ }
709
+ return {
710
+ code: 200,
711
+ prepared: {
712
+ contextualMessage,
713
+ effectiveSlug,
714
+ page,
715
+ selected,
716
+ plannerSource,
717
+ modelUsed,
718
+ modelKey,
719
+ variations,
720
+ generatorUsage,
721
+ count
722
+ }
723
+ };
724
+ }
725
+ function summaryForIntent(intent, count, blockType) {
726
+ if (intent === "regenerate")
727
+ return `Generated ${count} fresh variations for ${blockType}.`;
728
+ if (intent === "show")
729
+ return `Showing your ${count} variations for ${blockType}.`;
730
+ return `Generated ${count} variations for ${blockType}.`;
731
+ }
732
+ const POST_VARIATION_SUGGESTIONS = [
733
+ "Generate different variations",
734
+ "Try a more playful tone",
735
+ "Try a more formal tone"
736
+ ];
737
+ function buildVariationResult(args) {
738
+ const { prepared, variations } = args;
739
+ const intent = args.intent ?? "generate";
740
+ const { selected, effectiveSlug, plannerSource, modelUsed, modelKey, generatorUsage } = prepared;
741
+ return {
742
+ status: "ok",
743
+ summary: summaryForIntent(intent, variations.length, selected.type),
744
+ blockId: selected.id,
745
+ blockType: selected.type,
746
+ pageSlug: effectiveSlug,
747
+ baseProps: structuredClone(selected.props),
748
+ variations,
749
+ suggestions: POST_VARIATION_SUGGESTIONS,
750
+ plannerSource,
751
+ modelUsed,
752
+ modelKey,
753
+ ...(generatorUsage ? {
754
+ usage: {
755
+ inputTokens: generatorUsage.inputTokens,
756
+ outputTokens: generatorUsage.outputTokens,
757
+ totalTokens: generatorUsage.totalTokens,
758
+ ...(typeof generatorUsage.cacheCreationInputTokens === "number"
759
+ ? { cacheCreationInputTokens: generatorUsage.cacheCreationInputTokens }
760
+ : {}),
761
+ ...(typeof generatorUsage.cacheReadInputTokens === "number"
762
+ ? { cacheReadInputTokens: generatorUsage.cacheReadInputTokens }
763
+ : {}),
764
+ estimatedUsd: estimateUsd(modelUsed, generatorUsage)
765
+ }
766
+ } : {})
767
+ };
768
+ }
769
+ function resolveLlmIntent(session, blockId, explicit) {
770
+ if (explicit && explicit !== "show")
771
+ return explicit;
772
+ if (session && getCachedVariations(session, blockId))
773
+ return "regenerate";
774
+ return "generate";
775
+ }
776
+ export async function runVariationPipeline(ctx, body, options) {
777
+ const prep = await prepareTextVariations(ctx, body);
778
+ if (prep.code !== 200 || !prep.prepared) {
779
+ return { code: prep.code, payload: prep.payload ?? { error: "unknown error" } };
780
+ }
781
+ const { prepared } = prep;
782
+ const variations = await withDefaultImageVariations({
783
+ block: prepared.selected,
784
+ message: prepared.contextualMessage,
785
+ variations: prepared.variations,
786
+ page: prepared.page,
787
+ log: ctx.log
788
+ });
789
+ if (variations.length === 0) {
790
+ return {
791
+ code: 400,
792
+ payload: { error: "Could not generate valid variations for this block. Try a more specific instruction." }
793
+ };
794
+ }
795
+ const intent = resolveLlmIntent(body.session, prepared.selected.id, options?.intent);
796
+ const result = buildVariationResult({ prepared, variations, intent });
797
+ if (body.session)
798
+ rememberVariations(body.session, result);
799
+ return { code: 200, payload: result };
800
+ }
801
+ export async function runVariationPipelineStreaming(ctx, body, hooks, options) {
802
+ const prep = await prepareTextVariations(ctx, body);
803
+ if (prep.code !== 200 || !prep.prepared) {
804
+ return { code: prep.code, payload: prep.payload ?? { error: "unknown error" } };
805
+ }
806
+ const { prepared } = prep;
807
+ const plan = planDefaultImageVariations({
808
+ block: prepared.selected,
809
+ message: prepared.contextualMessage,
810
+ variations: prepared.variations,
811
+ page: prepared.page,
812
+ log: ctx.log
813
+ });
814
+ if (plan.initial.length === 0) {
815
+ return {
816
+ code: 400,
817
+ payload: { error: "Could not generate valid variations for this block. Try a more specific instruction." }
818
+ };
819
+ }
820
+ const intent = resolveLlmIntent(body.session, prepared.selected.id, options?.intent);
821
+ const imagesPending = supportsImageVariation(prepared.selected);
822
+ hooks.onInitial({
823
+ ...buildVariationResult({ prepared, variations: plan.initial, intent }),
824
+ imagesPending
825
+ });
826
+ const finalVariations = await plan.run((update) => hooks.onImageResolved(update));
827
+ if (finalVariations.length === 0) {
828
+ return {
829
+ code: 400,
830
+ payload: { error: "Could not generate valid variations for this block. Try a more specific instruction." }
831
+ };
832
+ }
833
+ const result = buildVariationResult({ prepared, variations: finalVariations, intent });
834
+ if (body.session)
835
+ rememberVariations(body.session, result);
836
+ return { code: 200, payload: result };
837
+ }