@paroicms/site-generator-plugin
Version:
ParoiCMS Site Generator Plugin
241 lines (240 loc) • 9.31 kB
JavaScript
import { loadStep, readStepSchema } from "../../db/db-read.queries.js";
import { insertStep, saveCompletedSchemaStep, updateStep, } from "../../db/db-write.queries.js";
import { invokeClaude } from "../lib/calling-llm-anthropic.js";
import { createPromptTemplate, getPredefinedFields, getSiteSchemaTsDefs, } from "../lib/create-prompt.js";
import { debugLlmOutput } from "../lib/debug-utils.js";
import { parseLlmResponseAsProperties } from "../lib/parse-llm-response.js";
import { safeCallStep } from "../lib/session-utils.js";
const prompt1Tpl = await createPromptTemplate({
filename: "update-site-schema-1-write-details.md",
});
const prompt2Tpl = await createPromptTemplate({
filename: "update-site-schema-2-execute.md",
});
export async function startUpdateSiteSchema(ctx, input) {
const fromStepSchema = await readStepSchema(ctx, input.fromStepNumber);
const stepHandle = await insertStep(ctx, {
kind: "updateSchema",
status: "pending",
currentActivity: "updating1",
});
safeCallStep(ctx, stepHandle, () => invokeUpdateSiteSchema(ctx, stepHandle, { prompt: input.prompt, fromStepSchema }));
return await loadStep(ctx, stepHandle.stepNumber);
}
export async function invokeUpdateSiteSchema(ctx, stepHandle, input, { asRemainingOf } = {}) {
const { properties, llmReport: llmReport1 } = await invokeUpdateSiteSchemaStep1(ctx, input, stepHandle);
if (!properties.taskDetailsMd) {
await updateStep(ctx, stepHandle, {
status: "noEffect",
currentActivity: null,
explanation: properties.explanation,
});
return;
}
await updateStep(ctx, stepHandle, {
currentActivity: "updating2",
explanation: properties.explanation,
});
const { stepSchema, llmReport: llmReport2 } = await invokeUpdateSiteSchemaStep2(ctx, {
taskDetailsMd: properties.taskDetailsMd,
fromStepSchema: input.fromStepSchema,
}, stepHandle);
const completedValues = {
...stepSchema,
status: "completed",
inputTokenCount: llmReport1.inputTokenCount + llmReport2.inputTokenCount,
outputTokenCount: (llmReport1.outputTokenCount ?? 0) + (llmReport2.outputTokenCount ?? 0),
promptTitle: undefined, // TODO: implement prompt title
};
if (asRemainingOf) {
completedValues.inputTokenCount += asRemainingOf.inputTokenCount;
completedValues.outputTokenCount += asRemainingOf.outputTokenCount;
completedValues.promptTitle = undefined;
if (asRemainingOf.explanation) {
completedValues.explanation = properties.explanation
? `${asRemainingOf.explanation}\n\n${properties.explanation}`
: asRemainingOf.explanation;
}
}
await saveCompletedSchemaStep(ctx, stepHandle, completedValues);
}
async function invokeUpdateSiteSchemaStep1(ctx, input, stepHandle) {
const llmTaskName = "update-step1";
const llmInput = {
siteSchemaTsDefs: getSiteSchemaTsDefs(),
predefinedFields: JSON.stringify(getPredefinedFields(), undefined, 2),
siteSchemaJson: JSON.stringify(input.fromStepSchema.siteSchema, undefined, 2),
l10nJson: JSON.stringify(input.fromStepSchema.l10n, undefined, 2),
updateMessage: input.prompt,
};
const debug = await debugLlmOutput(ctx, llmTaskName, ctx.anthropicModelName, stepHandle, {
updateMessage: llmInput.updateMessage,
siteSchemaJson: llmInput.siteSchemaJson,
l10nJson: llmInput.l10nJson,
});
let llmOutput = debug.stored;
if (!llmOutput) {
// Create formatted messages using the template
const message = prompt1Tpl(llmInput);
// Call the model
const { messageContent, report } = await invokeClaude(ctx, {
llmTaskName,
prompt: message,
maxTokens: 1_500,
temperature: 0.1,
systemInstruction: "beSmart",
});
llmOutput = await debug.getMessageContent(messageContent, report);
}
const properties = parseLlmResponseAsProperties(llmOutput.output, [
{
tagName: "task_details_md",
key: "taskDetailsMd",
format: "markdown",
optional: true,
},
{
tagName: "explanation_md",
key: "explanation",
format: "markdown",
},
]);
return {
properties,
llmReport: llmOutput.llmReport,
};
}
async function invokeUpdateSiteSchemaStep2(ctx, input, stepHandle) {
const llmTaskName = "update-step2";
const llmInput = {
siteSchemaTsDefs: getSiteSchemaTsDefs(),
predefinedFields: JSON.stringify(getPredefinedFields(), undefined, 2),
siteSchemaJson: JSON.stringify(input.fromStepSchema.siteSchema, undefined, 2),
l10nJson: JSON.stringify(input.fromStepSchema.l10n, undefined, 2),
taskDetailsMd: input.taskDetailsMd,
};
const debug = await debugLlmOutput(ctx, llmTaskName, ctx.anthropicModelName, stepHandle, {
taskDetailsMd: llmInput.taskDetailsMd,
siteSchemaJson: llmInput.siteSchemaJson,
l10nJson: llmInput.l10nJson,
});
let llmOutput = debug.stored;
if (!llmOutput) {
// Create formatted messages using the template
const message = prompt2Tpl(llmInput);
// Call the model
const { messageContent, report } = await invokeClaude(ctx, {
llmTaskName,
prompt: message,
maxTokens: 7_000,
temperature: 0.1,
systemInstruction: "beSmart",
});
llmOutput = await debug.getMessageContent(messageContent, report);
}
const parsed = parseLlmResponseAsProperties(llmOutput.output, [
{
tagName: "updated_site_schema_json",
key: "siteSchema",
format: "json",
optional: true,
},
{
tagName: "updated_l10n_json",
key: "l10n",
format: "json",
optional: true,
},
]);
const result = {
...input.fromStepSchema,
};
if (parsed.siteSchema) {
result.siteSchema = fixSiteSchema(parsed.siteSchema);
}
if (parsed.l10n) {
result.l10n = parsed.l10n;
}
return {
stepSchema: result,
llmReport: llmOutput.llmReport,
};
}
function fixSiteSchema(siteSchema) {
const nodeTypes = siteSchema.nodeTypes ?? [];
for (const nodeType of nodeTypes) {
if (!nodeType.fields)
continue;
// Add Tiptap plugin to fields with renderAs "html"
for (const field of nodeType.fields) {
if (typeof field !== "string" &&
field.renderAs === "html" &&
field.storedAs !== "labeling" &&
field.storedAs !== "partField") {
field.dataType = "json";
field.plugin = "@paroicms/tiptap-editor-plugin";
}
}
// Move labeling fields to the beginning of the fields array
const labelingFields = nodeType.fields.filter((f) => typeof f !== "string" && f.storedAs === "labeling");
if (labelingFields.length > 0) {
nodeType.fields = [
...labelingFields,
...nodeType.fields.filter((f) => typeof f === "string" || f.storedAs !== "labeling"),
];
}
}
validatePartFieldReferences(nodeTypes);
setMenuPlacementOnTaxonomies(nodeTypes);
return siteSchema;
}
function validatePartFieldReferences(nodeTypes) {
if (!nodeTypes)
return;
const partTypeNames = new Set(nodeTypes.filter((nt) => nt.kind === "part").map((nt) => nt.typeName));
for (const nodeType of nodeTypes) {
if (!nodeType.fields)
continue;
nodeType.fields = nodeType.fields.filter((field) => {
if (typeof field === "string")
return true;
if (field.storedAs !== "partField")
return true;
if (typeof field.partType !== "string" || !partTypeNames.has(field.partType)) {
return false;
}
return true;
});
}
}
function setMenuPlacementOnTaxonomies(nodeTypes) {
if (!nodeTypes)
return;
const taxonomyTypeNames = new Set();
for (const nodeType of nodeTypes) {
if (!nodeType.fields)
continue;
for (const field of nodeType.fields) {
if (typeof field !== "string" && field.storedAs === "labeling") {
taxonomyTypeNames.add(field.taxonomy);
}
}
}
const homeNode = nodeTypes.find((nt) => nt.kind === "document" && nt.typeName === "home");
if (homeNode?.kind !== "document")
return;
const homeRoutingChildren = new Set(homeNode.routingChildren ?? []);
for (const nodeType of nodeTypes) {
if (nodeType.kind !== "document")
continue;
if (nodeType.documentKind !== "routing")
continue;
if (nodeType.adminUi)
continue;
if (!taxonomyTypeNames.has(nodeType.typeName))
continue;
if (homeRoutingChildren.has(nodeType.typeName))
continue;
nodeType.adminUi = { menuPlacement: "popup" };
}
}