@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.
- package/LICENSE +201 -0
- package/dist/agent/agent-context.d.ts +19 -0
- package/dist/agent/agent-context.js +67 -0
- package/dist/agent/agent-logger.d.ts +5 -0
- package/dist/agent/agent-logger.js +22 -0
- package/dist/agent/agent-loop-openai.d.ts +8 -0
- package/dist/agent/agent-loop-openai.js +172 -0
- package/dist/agent/agent-loop.d.ts +56 -0
- package/dist/agent/agent-loop.js +167 -0
- package/dist/agent/agent-provider.d.ts +28 -0
- package/dist/agent/agent-provider.js +63 -0
- package/dist/agent/agent-tools.d.ts +28 -0
- package/dist/agent/agent-tools.js +899 -0
- package/dist/agent/context/editing-guidelines.md +46 -0
- package/dist/agent/context/role.md +39 -0
- package/dist/agent/integration-prompt.d.ts +9 -0
- package/dist/agent/integration-prompt.js +154 -0
- package/dist/agent/sites-agent-context.d.ts +12 -0
- package/dist/agent/sites-agent-context.js +316 -0
- package/dist/agent/sites-agent-shared.d.ts +161 -0
- package/dist/agent/sites-agent-shared.js +1101 -0
- package/dist/agent/sites-agent-tools.d.ts +18 -0
- package/dist/agent/sites-agent-tools.js +1227 -0
- package/dist/chat/anthropic-cache.d.ts +20 -0
- package/dist/chat/anthropic-cache.js +54 -0
- package/dist/chat/anthropic-planner.d.ts +98 -0
- package/dist/chat/anthropic-planner.js +1012 -0
- package/dist/chat/changelog-coverage-validator.d.ts +37 -0
- package/dist/chat/changelog-coverage-validator.js +215 -0
- package/dist/chat/chat-pipeline-context.d.ts +211 -0
- package/dist/chat/chat-pipeline-context.js +249 -0
- package/dist/chat/chat-pipeline-deterministic.d.ts +61 -0
- package/dist/chat/chat-pipeline-deterministic.js +407 -0
- package/dist/chat/chat-pipeline-image.d.ts +86 -0
- package/dist/chat/chat-pipeline-image.js +897 -0
- package/dist/chat/chat-pipeline-shared.d.ts +69 -0
- package/dist/chat/chat-pipeline-shared.js +212 -0
- package/dist/chat/chat-pipeline-translation.d.ts +27 -0
- package/dist/chat/chat-pipeline-translation.js +417 -0
- package/dist/chat/chat-pipeline-ui.d.ts +14 -0
- package/dist/chat/chat-pipeline-ui.js +244 -0
- package/dist/chat/chat-pipeline.d.ts +99 -0
- package/dist/chat/chat-pipeline.js +3999 -0
- package/dist/chat/decomposer.d.ts +21 -0
- package/dist/chat/decomposer.js +65 -0
- package/dist/chat/gemini-planner.d.ts +70 -0
- package/dist/chat/gemini-planner.js +541 -0
- package/dist/chat/hallucination-validator.d.ts +36 -0
- package/dist/chat/hallucination-validator.js +110 -0
- package/dist/chat/locale-strings.d.ts +47 -0
- package/dist/chat/locale-strings.js +100 -0
- package/dist/chat/plan-json-schema.d.ts +133 -0
- package/dist/chat/plan-json-schema.js +112 -0
- package/dist/chat/planner-types.d.ts +120 -0
- package/dist/chat/planner-types.js +66 -0
- package/dist/chat/planner.d.ts +148 -0
- package/dist/chat/planner.js +1361 -0
- package/dist/chat/prompts.d.ts +67 -0
- package/dist/chat/prompts.js +356 -0
- package/dist/chat/provider-routing.d.ts +14 -0
- package/dist/chat/provider-routing.js +27 -0
- package/dist/chat/variation-pipeline.d.ts +135 -0
- package/dist/chat/variation-pipeline.js +837 -0
- package/dist/chat/vision-alt-generator.d.ts +35 -0
- package/dist/chat/vision-alt-generator.js +152 -0
- package/dist/cms/adapter.d.ts +62 -0
- package/dist/cms/adapter.js +1 -0
- package/dist/cms/bootstrap.d.ts +17 -0
- package/dist/cms/bootstrap.js +85 -0
- package/dist/cms/editor-api-adapter.d.ts +23 -0
- package/dist/cms/editor-api-adapter.js +71 -0
- package/dist/cms/index.d.ts +4 -0
- package/dist/cms/index.js +3 -0
- package/dist/cms/json-file-adapter.d.ts +11 -0
- package/dist/cms/json-file-adapter.js +62 -0
- package/dist/demo-mode.d.ts +59 -0
- package/dist/demo-mode.js +201 -0
- package/dist/errors.d.ts +67 -0
- package/dist/errors.js +129 -0
- package/dist/http/chat-stream-resumable.d.ts +108 -0
- package/dist/http/chat-stream-resumable.js +290 -0
- package/dist/http/chat-stream.d.ts +99 -0
- package/dist/http/chat-stream.js +92 -0
- package/dist/image/gdrive-client.d.ts +22 -0
- package/dist/image/gdrive-client.js +215 -0
- package/dist/image/image-helpers.d.ts +95 -0
- package/dist/image/image-helpers.js +488 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/jira/jira-approval.d.ts +22 -0
- package/dist/jira/jira-approval.js +51 -0
- package/dist/jira/jira-client.d.ts +44 -0
- package/dist/jira/jira-client.js +313 -0
- package/dist/jira/jira-poller.d.ts +46 -0
- package/dist/jira/jira-poller.js +184 -0
- package/dist/jira/jira-processor.d.ts +103 -0
- package/dist/jira/jira-processor.js +1085 -0
- package/dist/jira/jira-types.d.ts +117 -0
- package/dist/jira/jira-types.js +38 -0
- package/dist/logger.d.ts +12 -0
- package/dist/logger.js +28 -0
- package/dist/migration/mcp-server-stdio.d.ts +8 -0
- package/dist/migration/mcp-server-stdio.js +672 -0
- package/dist/migration/migration-prompt.d.ts +7 -0
- package/dist/migration/migration-prompt.js +197 -0
- package/dist/migration/migration-tools.d.ts +17 -0
- package/dist/migration/migration-tools.js +159 -0
- package/dist/migration/scrape-cache.d.ts +9 -0
- package/dist/migration/scrape-cache.js +19 -0
- package/dist/nlp/deterministic-planner-context.d.ts +141 -0
- package/dist/nlp/deterministic-planner-context.js +362 -0
- package/dist/nlp/deterministic-planner-pages.d.ts +26 -0
- package/dist/nlp/deterministic-planner-pages.js +170 -0
- package/dist/nlp/deterministic-planner-patches.d.ts +80 -0
- package/dist/nlp/deterministic-planner-patches.js +508 -0
- package/dist/nlp/deterministic-planner-refs.d.ts +33 -0
- package/dist/nlp/deterministic-planner-refs.js +164 -0
- package/dist/nlp/deterministic-planner-suggestions.d.ts +49 -0
- package/dist/nlp/deterministic-planner-suggestions.js +579 -0
- package/dist/nlp/deterministic-planner.d.ts +85 -0
- package/dist/nlp/deterministic-planner.js +1631 -0
- package/dist/nlp/intent-detection.d.ts +309 -0
- package/dist/nlp/intent-detection.js +730 -0
- package/dist/nlp/intent-helpers.d.ts +15 -0
- package/dist/nlp/intent-helpers.js +243 -0
- package/dist/nlp/intent-patterns.d.ts +40 -0
- package/dist/nlp/intent-patterns.js +223 -0
- package/dist/nlp/plan-normalizer.d.ts +41 -0
- package/dist/nlp/plan-normalizer.js +1537 -0
- package/dist/ops/destructive-action-gate.d.ts +44 -0
- package/dist/ops/destructive-action-gate.js +90 -0
- package/dist/ops/ops-engine.d.ts +151 -0
- package/dist/ops/ops-engine.js +1394 -0
- package/dist/publish/diff-engine.d.ts +18 -0
- package/dist/publish/diff-engine.js +305 -0
- package/dist/publish/publish-helpers.d.ts +87 -0
- package/dist/publish/publish-helpers.js +521 -0
- package/dist/publish/publish-target-registry.d.ts +7 -0
- package/dist/publish/publish-target-registry.js +61 -0
- package/dist/publish/publish-target.d.ts +81 -0
- package/dist/publish/publish-target.js +1 -0
- package/dist/publish/targets/deploy-hook.d.ts +13 -0
- package/dist/publish/targets/deploy-hook.js +123 -0
- package/dist/publish/targets/git.d.ts +13 -0
- package/dist/publish/targets/git.js +55 -0
- package/dist/publish/targets/site-contract.d.ts +19 -0
- package/dist/publish/targets/site-contract.js +124 -0
- package/dist/state/content-source.d.ts +17 -0
- package/dist/state/content-source.js +1 -0
- package/dist/state/in-memory-content-source.d.ts +27 -0
- package/dist/state/in-memory-content-source.js +51 -0
- package/dist/state/session-lock.d.ts +13 -0
- package/dist/state/session-lock.js +29 -0
- package/dist/state/session-state.d.ts +310 -0
- package/dist/state/session-state.js +1083 -0
- package/dist/state/sqlite-store-singleton.d.ts +31 -0
- package/dist/state/sqlite-store-singleton.js +170 -0
- package/dist/state/sqlite-store.d.ts +135 -0
- package/dist/state/sqlite-store.js +421 -0
- package/dist/telemetry/chat-telemetry.d.ts +105 -0
- package/dist/telemetry/chat-telemetry.js +247 -0
- package/dist/telemetry/eval-candidate-store.d.ts +50 -0
- package/dist/telemetry/eval-candidate-store.js +120 -0
- package/dist/telemetry/feedback-store.d.ts +34 -0
- package/dist/telemetry/feedback-store.js +76 -0
- package/dist/telemetry/jira-telemetry.d.ts +57 -0
- package/dist/telemetry/jira-telemetry.js +68 -0
- package/dist/telemetry/migration-telemetry.d.ts +35 -0
- package/dist/telemetry/migration-telemetry.js +40 -0
- package/dist/telemetry/usage.d.ts +24 -0
- package/dist/telemetry/usage.js +80 -0
- package/dist/tools/builtin-registrations.d.ts +12 -0
- package/dist/tools/builtin-registrations.js +33 -0
- package/dist/tools/builtins/gdrive-browse.d.ts +3 -0
- package/dist/tools/builtins/gdrive-browse.js +68 -0
- package/dist/tools/builtins/image-generate.d.ts +3 -0
- package/dist/tools/builtins/image-generate.js +211 -0
- package/dist/tools/builtins/unsplash-get-by-id.d.ts +23 -0
- package/dist/tools/builtins/unsplash-get-by-id.js +119 -0
- package/dist/tools/builtins/unsplash-search.d.ts +3 -0
- package/dist/tools/builtins/unsplash-search.js +74 -0
- package/dist/tools/executor.d.ts +23 -0
- package/dist/tools/executor.js +169 -0
- package/dist/tools/index.d.ts +5 -0
- package/dist/tools/index.js +5 -0
- package/dist/tools/registry.d.ts +21 -0
- package/dist/tools/registry.js +75 -0
- package/dist/tools/runtime.d.ts +27 -0
- package/dist/tools/runtime.js +48 -0
- package/dist/tools/schema-validator.d.ts +24 -0
- package/dist/tools/schema-validator.js +88 -0
- package/dist/tools/types.d.ts +86 -0
- package/dist/tools/types.js +1 -0
- package/dist/variation-images.d.ts +19 -0
- package/dist/variation-images.js +12 -0
- 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
|
+
}
|