@paroicms/site-generator-plugin 0.7.0 → 0.8.1
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/gen-backend/ddl/site-generator.ddl.sql +22 -0
- package/gen-backend/dist/commands/execute-command.js +60 -0
- package/gen-backend/dist/{generator/session → commands}/generator-session.js +16 -8
- package/gen-backend/dist/context.js +6 -0
- package/gen-backend/dist/db/db-init.js +35 -0
- package/gen-backend/dist/db/db.queries.js +60 -0
- package/gen-backend/dist/db/ddl-migration.js +15 -0
- package/gen-backend/dist/generator/fake-content-generator.ts/content-helpers.js +17 -0
- package/gen-backend/dist/generator/fake-content-generator.ts/content-report.js +11 -0
- package/gen-backend/dist/generator/fake-content-generator.ts/create-database-with-fake-content.js +27 -18
- package/gen-backend/dist/generator/lib/parse-llm-response.js +10 -16
- package/gen-backend/dist/generator/llm-queries/invoke-message-guard.js +2 -0
- package/gen-backend/dist/generator/llm-queries/invoke-new-site-analysis.js +28 -6
- package/gen-backend/dist/generator/llm-queries/invoke-update-site-schema.js +5 -1
- package/gen-backend/dist/generator/site-generator/site-generator.js +14 -11
- package/gen-backend/dist/generator/site-schema-generator/create-site-schema.js +1 -0
- package/gen-backend/dist/lib/site-remover.js +39 -0
- package/gen-backend/dist/plugin.js +43 -25
- package/gen-backend/prompts/0-context.md +2 -2
- package/gen-backend/prompts/new-site-1-analysis.md +20 -5
- package/gen-backend/prompts/new-site-2-fields.md +1 -0
- package/gen-backend/prompts/update-site-schema-2-execute.md +20 -4
- package/gen-front/dist/gen-front.css +1 -1
- package/gen-front/dist/gen-front.woff2 +0 -0
- package/gen-front/dist/gen-front2.woff2 +0 -0
- package/package.json +7 -5
- package/gen-backend/dist/generator/actions.js +0 -45
- /package/gen-backend/dist/{generator/generator-types.js → lib/internal-types.js} +0 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
PRAGMA journal_mode = TRUNCATE;
|
|
2
|
+
PRAGMA foreign_keys = 1;
|
|
3
|
+
|
|
4
|
+
create table PaMetadata (
|
|
5
|
+
dbSchema varchar(100) not null,
|
|
6
|
+
k varchar(100) not null,
|
|
7
|
+
val varchar(250) not null,
|
|
8
|
+
primary key (dbSchema, k)
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
insert into PaMetadata (dbSchema, k, val) values ('site-generator', 'dbSchemaVersion', '1');
|
|
12
|
+
|
|
13
|
+
create table PaGenSession (
|
|
14
|
+
id char(36) not null primary key,
|
|
15
|
+
createdAt timestamp not null default current_timestamp,
|
|
16
|
+
status varchar(100) not null,
|
|
17
|
+
guardCount integer not null default 0,
|
|
18
|
+
promptCount integer not null default 0,
|
|
19
|
+
nodeTypeCount integer not null default 0,
|
|
20
|
+
contentCount integer not null default 0,
|
|
21
|
+
errorMessage varchar(500)
|
|
22
|
+
);
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { messageOf } from "@paroi/data-formatters-lib";
|
|
2
|
+
import { updateSession } from "../db/db.queries.js";
|
|
3
|
+
import { invokeMessageGuard } from "../generator/llm-queries/invoke-message-guard.js";
|
|
4
|
+
import { invokeNewSiteAnalysis } from "../generator/llm-queries/invoke-new-site-analysis.js";
|
|
5
|
+
import { invokeUpdateSiteSchema } from "../generator/llm-queries/invoke-update-site-schema.js";
|
|
6
|
+
import { generateSite } from "../generator/site-generator/site-generator.js";
|
|
7
|
+
import { newSession, verifySessionToken } from "./generator-session.js";
|
|
8
|
+
export async function executeCommand(commandCtx, input) {
|
|
9
|
+
if (input.command === "newSession") {
|
|
10
|
+
return {
|
|
11
|
+
success: true,
|
|
12
|
+
result: await newSession(commandCtx, input),
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
const { sessionId } = await verifySessionToken(commandCtx, input.sessionToken);
|
|
16
|
+
const ctx = {
|
|
17
|
+
...commandCtx,
|
|
18
|
+
sessionId,
|
|
19
|
+
};
|
|
20
|
+
try {
|
|
21
|
+
if (input.command === "createSiteSchema") {
|
|
22
|
+
const errorResponse = await invokeMessageGuard(ctx, input);
|
|
23
|
+
if (errorResponse)
|
|
24
|
+
return errorResponse;
|
|
25
|
+
return {
|
|
26
|
+
success: true,
|
|
27
|
+
result: await invokeNewSiteAnalysis(ctx, input),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
if (input.command === "updateSiteSchema") {
|
|
31
|
+
const errorResponse = await invokeMessageGuard(ctx, input, { skipOutOfScopeCheck: true });
|
|
32
|
+
if (errorResponse)
|
|
33
|
+
return errorResponse;
|
|
34
|
+
return {
|
|
35
|
+
success: true,
|
|
36
|
+
result: await invokeUpdateSiteSchema(ctx, input),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
if (input.command === "generateSite") {
|
|
40
|
+
return {
|
|
41
|
+
success: true,
|
|
42
|
+
result: await generateSite(ctx, input),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
ctx.logger.error(`Invalid command: ${input.command}`);
|
|
46
|
+
return {
|
|
47
|
+
success: false,
|
|
48
|
+
userMessage: "Invalid command",
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
try {
|
|
53
|
+
await updateSession(ctx, { status: "error", errorMessage: messageOf(error) });
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
ctx.logger.error("Failed to update session error message", error);
|
|
57
|
+
}
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -1,24 +1,25 @@
|
|
|
1
1
|
import { ApiError } from "@paroicms/public-server-lib";
|
|
2
|
-
import {
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { insertSession, readSession } from "../db/db.queries.js";
|
|
3
4
|
const { sign, verify } = (await import("jsonwebtoken")).default;
|
|
4
|
-
const JWT_SECRET = "init"; // FIXME: Hardcoded JWT secret as specified
|
|
5
5
|
export async function newSession(ctx, command) {
|
|
6
6
|
const { service } = ctx;
|
|
7
7
|
const { recaptchaToken } = command;
|
|
8
8
|
const isValid = await service.validateRecaptchaResponse(recaptchaToken);
|
|
9
9
|
if (!isValid)
|
|
10
10
|
throw new ApiError("Invalid reCAPTCHA token", 400);
|
|
11
|
-
const sessionId =
|
|
12
|
-
const token = sign({ sessionId },
|
|
11
|
+
const sessionId = randomUUID();
|
|
12
|
+
const token = sign({ sessionId }, ctx.jwtSecret, { expiresIn: "48h" });
|
|
13
|
+
await insertSession(ctx, { sessionId });
|
|
13
14
|
return { token };
|
|
14
15
|
}
|
|
15
16
|
/**
|
|
16
17
|
* Verifies a session token and returns the session ID if valid
|
|
17
18
|
*/
|
|
18
|
-
export function verifySessionToken(token) {
|
|
19
|
+
export async function verifySessionToken(ctx, token) {
|
|
19
20
|
let payload;
|
|
20
21
|
try {
|
|
21
|
-
payload = verify(token,
|
|
22
|
+
payload = verify(token, ctx.jwtSecret);
|
|
22
23
|
}
|
|
23
24
|
catch (error) {
|
|
24
25
|
if (error.name === "TokenExpiredError")
|
|
@@ -27,7 +28,14 @@ export function verifySessionToken(token) {
|
|
|
27
28
|
throw new ApiError("Invalid session token", 401);
|
|
28
29
|
throw error;
|
|
29
30
|
}
|
|
30
|
-
if (!payload || !payload.sessionId)
|
|
31
|
+
if (!payload || !("sessionId" in payload) || typeof payload.sessionId !== "string") {
|
|
31
32
|
throw new ApiError("Invalid session token", 401);
|
|
32
|
-
|
|
33
|
+
}
|
|
34
|
+
const sessionId = payload.sessionId;
|
|
35
|
+
const session = await readSession(ctx, sessionId);
|
|
36
|
+
if (!session)
|
|
37
|
+
throw new ApiError(`Missing session "${sessionId}"`, 500);
|
|
38
|
+
if (session.promptCount >= 20)
|
|
39
|
+
throw new ApiError("Session consumed", 401);
|
|
40
|
+
return { sessionId };
|
|
33
41
|
}
|
|
@@ -1,2 +1,8 @@
|
|
|
1
|
+
import { strVal } from "@paroi/data-formatters-lib";
|
|
1
2
|
import { resolveModuleDirectory } from "@paroicms/public-server-lib";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
2
5
|
export const projectDir = resolveModuleDirectory(import.meta.url, { parent: true });
|
|
6
|
+
export const packageDir = dirname(projectDir);
|
|
7
|
+
export const pluginVersion = strVal(JSON.parse(readFileSync(join(packageDir, "package.json"), "utf-8")).version);
|
|
8
|
+
export const SLUG = "site-generator";
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { createOrOpenSqliteConnection, createSqlLogger, getMetadataDbSchemaVersion, } from "@paroicms/internal-server-lib";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { projectDir } from "../context.js";
|
|
4
|
+
import { currentDbSchemaVersion, dbSchemaName, migrateSiteGeneratorDb } from "./ddl-migration.js";
|
|
5
|
+
export async function createOrOpenSiteGeneratorConnection({ sqliteFile, canCreate, logger, }) {
|
|
6
|
+
const { logNextQuery, knexLogger } = createSqlLogger({ logger, dbSchemaName });
|
|
7
|
+
const { cn } = await createOrOpenSqliteConnection({
|
|
8
|
+
canCreate,
|
|
9
|
+
dbSchemaName,
|
|
10
|
+
sqliteFile,
|
|
11
|
+
ddlFile: join(projectDir, "ddl", "site-generator.ddl.sql"),
|
|
12
|
+
migrateDb,
|
|
13
|
+
knexLogger,
|
|
14
|
+
logger,
|
|
15
|
+
});
|
|
16
|
+
async function migrateDb(cn) {
|
|
17
|
+
const dbVersion = await getMetadataDbSchemaVersion(cn, { dbSchemaName });
|
|
18
|
+
if (dbVersion === currentDbSchemaVersion) {
|
|
19
|
+
return {
|
|
20
|
+
migrated: false,
|
|
21
|
+
schemaVersion: dbVersion,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
await migrateSiteGeneratorDb(cn, {
|
|
25
|
+
fromVersion: dbVersion,
|
|
26
|
+
logger,
|
|
27
|
+
});
|
|
28
|
+
return {
|
|
29
|
+
migrated: true,
|
|
30
|
+
fromVersion: dbVersion,
|
|
31
|
+
schemaVersion: currentDbSchemaVersion,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
return { cn, logNextQuery };
|
|
35
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { strVal, strValOrUndef } from "@paroi/data-formatters-lib";
|
|
2
|
+
export async function readSession(ctx, sessionId) {
|
|
3
|
+
const { cn } = ctx;
|
|
4
|
+
const row = await cn("PaGenSession")
|
|
5
|
+
.select("id", "createdAt", "status", "guardCount", "promptCount", "nodeTypeCount", "contentCount", "errorMessage")
|
|
6
|
+
.where({ id: sessionId })
|
|
7
|
+
.first();
|
|
8
|
+
if (!row)
|
|
9
|
+
return;
|
|
10
|
+
return {
|
|
11
|
+
id: strVal(row.id),
|
|
12
|
+
createdAt: strVal(row.createdAt),
|
|
13
|
+
status: formatSessionStatus(row.status),
|
|
14
|
+
guardCount: Number(row.guardCount),
|
|
15
|
+
promptCount: Number(row.promptCount),
|
|
16
|
+
nodeTypeCount: Number(row.nodeTypeCount),
|
|
17
|
+
contentCount: Number(row.contentCount),
|
|
18
|
+
errorMessage: strValOrUndef(row.errorMessage),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export async function insertSession(ctx, { sessionId }) {
|
|
22
|
+
const { cn } = ctx;
|
|
23
|
+
await cn("PaGenSession").insert({
|
|
24
|
+
id: sessionId,
|
|
25
|
+
status: "started",
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
export async function updateSession(ctx, values) {
|
|
29
|
+
const { sessionId, cn } = ctx;
|
|
30
|
+
const { status, guardCountInc, promptCountInc, nodeTypeCount, contentCountInc, errorMessage } = values;
|
|
31
|
+
const affected = await cn("PaGenSession")
|
|
32
|
+
.where({
|
|
33
|
+
id: sessionId,
|
|
34
|
+
})
|
|
35
|
+
.update({
|
|
36
|
+
status,
|
|
37
|
+
guardCountInc: guardCountInc ? cn.raw(`guardCount + ${guardCountInc}`) : undefined,
|
|
38
|
+
promptCount: promptCountInc ? cn.raw(`promptCount + ${promptCountInc}`) : undefined,
|
|
39
|
+
nodeTypeCount,
|
|
40
|
+
contentCount: contentCountInc ? cn.raw(`contentCount + ${contentCountInc}`) : undefined,
|
|
41
|
+
errorMessage,
|
|
42
|
+
});
|
|
43
|
+
if (affected !== 1) {
|
|
44
|
+
throw new Error(`Session "${sessionId}" not found`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function formatSessionStatus(val) {
|
|
48
|
+
const v = strVal(val);
|
|
49
|
+
if (v === "started")
|
|
50
|
+
return "started";
|
|
51
|
+
if (v === "analyzed")
|
|
52
|
+
return "analyzed";
|
|
53
|
+
if (v === "updated")
|
|
54
|
+
return "updated";
|
|
55
|
+
if (v === "generated")
|
|
56
|
+
return "generated";
|
|
57
|
+
if (v === "error")
|
|
58
|
+
return "error";
|
|
59
|
+
throw new Error(`Invalid session status: "${val}"`);
|
|
60
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export const dbSchemaName = "site-generator";
|
|
2
|
+
export const currentDbSchemaVersion = 1;
|
|
3
|
+
export async function migrateSiteGeneratorDb(_cn, { fromVersion, logger }) {
|
|
4
|
+
const toVersion = currentDbSchemaVersion;
|
|
5
|
+
const currentVersion = fromVersion;
|
|
6
|
+
if (currentVersion === 1) {
|
|
7
|
+
// await cn.raw("...");
|
|
8
|
+
// await setMetadataDbSchemaVersion(cn, { dbSchemaName, value: 2 });
|
|
9
|
+
// currentVersion = 2;
|
|
10
|
+
}
|
|
11
|
+
if (currentVersion !== toVersion) {
|
|
12
|
+
throw new Error(`version of ${dbSchemaName} database should be '${toVersion}', but is '${currentVersion}'`);
|
|
13
|
+
}
|
|
14
|
+
logger.info(`${dbSchemaName} database was migrated from ${fromVersion} to ${currentVersion}`);
|
|
15
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function dedupMessages(messages) {
|
|
2
|
+
const counters = new Map();
|
|
3
|
+
const result = [];
|
|
4
|
+
for (const m of messages) {
|
|
5
|
+
const counter = counters.get(m);
|
|
6
|
+
if (counter) {
|
|
7
|
+
counters.set(m, counter + 1);
|
|
8
|
+
continue;
|
|
9
|
+
}
|
|
10
|
+
counters.set(m, 1);
|
|
11
|
+
result.push(m);
|
|
12
|
+
}
|
|
13
|
+
return result.map((m) => {
|
|
14
|
+
const counter = counters.get(m);
|
|
15
|
+
return counter && counter > 1 ? `${m} (×${counter})` : m;
|
|
16
|
+
});
|
|
17
|
+
}
|
package/gen-backend/dist/generator/fake-content-generator.ts/create-database-with-fake-content.js
CHANGED
|
@@ -2,19 +2,22 @@ import { getPartTypeByName, getRegularDocumentTypeByName, getRoutingDocumentType
|
|
|
2
2
|
import { createSimpleTranslator, } from "@paroicms/public-server-lib";
|
|
3
3
|
import { getRandomImagePath } from "../lib/images-lib.js";
|
|
4
4
|
import { createTaskCollector } from "../lib/tasks.js";
|
|
5
|
+
import { dedupMessages } from "./content-helpers.js";
|
|
6
|
+
import { createGeneratedContentReport } from "./content-report.js";
|
|
5
7
|
import { generateLocalizedFooterMention } from "./create-node-contents.js";
|
|
6
8
|
import { generateFieldSetContent, generateMultipleFieldSetContents, } from "./generate-fake-content.js";
|
|
7
|
-
export async function fillSiteWithFakeContent(ctx, {
|
|
9
|
+
export async function fillSiteWithFakeContent(ctx, { regSite, siteTitle }) {
|
|
8
10
|
const { service } = ctx;
|
|
9
|
-
const { fqdn } =
|
|
11
|
+
const { fqdn } = regSite;
|
|
12
|
+
const report = createGeneratedContentReport();
|
|
10
13
|
const { siteSchema, siteIds } = await service.connector.loadSiteSchemaAndIds(fqdn);
|
|
11
14
|
const schemaI18n = createSimpleTranslator({
|
|
12
15
|
labels: siteSchema.l10n,
|
|
13
16
|
logger: ctx.logger,
|
|
14
17
|
});
|
|
15
|
-
await updateSiteFields(ctx, { fqdn, siteSchema,
|
|
18
|
+
await updateSiteFields(ctx, report, { fqdn, siteSchema, siteTitle });
|
|
16
19
|
const tasks = createTaskCollector(ctx);
|
|
17
|
-
fillRoutingDocumentAndAddChildren(ctx, tasks, {
|
|
20
|
+
fillRoutingDocumentAndAddChildren(ctx, tasks, report, {
|
|
18
21
|
fqdn,
|
|
19
22
|
siteSchema,
|
|
20
23
|
schemaI18n,
|
|
@@ -28,15 +31,16 @@ export async function fillSiteWithFakeContent(ctx, { siteConf, siteId, siteTitle
|
|
|
28
31
|
ctx.logger.warn(`Failed to generate ${errorMessages.length} documents:\n - ${errorMessages.join("\n - ")}`);
|
|
29
32
|
}
|
|
30
33
|
ctx.logger.debug(`… Executed ${doneCount} generating tasks`);
|
|
34
|
+
return report;
|
|
31
35
|
}
|
|
32
|
-
function fillRoutingDocumentAndAddChildren(ctx, tasks, siteOptions, nodeOptions) {
|
|
36
|
+
function fillRoutingDocumentAndAddChildren(ctx, tasks, report, siteOptions, nodeOptions) {
|
|
33
37
|
const { routingIds, nodeType } = nodeOptions;
|
|
34
38
|
const { siteSchema } = siteOptions;
|
|
35
|
-
tasks.add(() => updateRoutingDocument(ctx, siteOptions, nodeOptions));
|
|
39
|
+
tasks.add(() => updateRoutingDocument(ctx, report, siteOptions, nodeOptions));
|
|
36
40
|
for (const listType of nodeType.lists ?? []) {
|
|
37
41
|
for (const typeName of listType.parts) {
|
|
38
42
|
const partType = getPartTypeByName(siteSchema, typeName);
|
|
39
|
-
tasks.add(() => addParts(ctx, siteOptions, {
|
|
43
|
+
tasks.add(() => addParts(ctx, report, siteOptions, {
|
|
40
44
|
parentNodeId: routingIds.nodeId,
|
|
41
45
|
nodeType: partType,
|
|
42
46
|
documentType: nodeType,
|
|
@@ -48,24 +52,24 @@ function fillRoutingDocumentAndAddChildren(ctx, tasks, siteOptions, nodeOptions)
|
|
|
48
52
|
const childIds = routingIds.children?.[typeName];
|
|
49
53
|
if (!childIds)
|
|
50
54
|
throw new Error(`Missing childIds for ${typeName}`);
|
|
51
|
-
fillRoutingDocumentAndAddChildren(ctx, tasks, siteOptions, {
|
|
55
|
+
fillRoutingDocumentAndAddChildren(ctx, tasks, report, siteOptions, {
|
|
52
56
|
routingIds: childIds,
|
|
53
57
|
nodeType: childType,
|
|
54
58
|
});
|
|
55
59
|
}
|
|
56
60
|
for (const typeName of nodeType.regularChildren ?? []) {
|
|
57
61
|
const childType = getRegularDocumentTypeByName(siteSchema, typeName);
|
|
58
|
-
tasks.add(() => addRegularDocuments(ctx, siteOptions, {
|
|
62
|
+
tasks.add(() => addRegularDocuments(ctx, report, siteOptions, {
|
|
59
63
|
parentNodeId: routingIds.nodeId,
|
|
60
64
|
nodeType: childType,
|
|
61
65
|
documentType: childType,
|
|
62
66
|
}));
|
|
63
67
|
}
|
|
64
68
|
}
|
|
65
|
-
async function updateSiteFields(ctx, options) {
|
|
69
|
+
async function updateSiteFields(ctx, _report, options) {
|
|
66
70
|
const { service, logger } = ctx;
|
|
67
71
|
logger.debug("Updating site fields…");
|
|
68
|
-
const { fqdn, siteSchema,
|
|
72
|
+
const { fqdn, siteSchema, siteTitle } = options;
|
|
69
73
|
const siteType = siteSchema.nodeTypes._site;
|
|
70
74
|
const logoImageFile = getRandomImagePath();
|
|
71
75
|
const fields = [
|
|
@@ -123,7 +127,7 @@ async function updateSiteFields(ctx, options) {
|
|
|
123
127
|
await service.connector.updateSiteFields(fqdn, Object.fromEntries(fields));
|
|
124
128
|
logger.debug("… Site fields updated");
|
|
125
129
|
}
|
|
126
|
-
async function updateRoutingDocument(ctx, siteOptions, nodeOptions) {
|
|
130
|
+
async function updateRoutingDocument(ctx, report, siteOptions, nodeOptions) {
|
|
127
131
|
ctx.logger.debug(`[TASK] Updating routing document "${nodeOptions.nodeType.typeName}"…`);
|
|
128
132
|
const { routingIds, nodeType } = nodeOptions;
|
|
129
133
|
const { fqdn, siteSchema, schemaI18n } = siteOptions;
|
|
@@ -139,8 +143,9 @@ async function updateRoutingDocument(ctx, siteOptions, nodeOptions) {
|
|
|
139
143
|
nodeId: routingIds.nodeId,
|
|
140
144
|
content: toRiDocumentContent(content, nodeType),
|
|
141
145
|
});
|
|
146
|
+
report.addContentCount(1);
|
|
142
147
|
}
|
|
143
|
-
async function addRegularDocuments(ctx, siteOptions, nodeOptions) {
|
|
148
|
+
async function addRegularDocuments(ctx, report, siteOptions, nodeOptions) {
|
|
144
149
|
ctx.logger.debug(`[TASK] Adding regular documents "${nodeOptions.nodeType.typeName}"…`);
|
|
145
150
|
const { parentNodeId, nodeType, documentType } = nodeOptions;
|
|
146
151
|
const { fqdn, siteSchema, schemaI18n } = siteOptions;
|
|
@@ -157,15 +162,17 @@ async function addRegularDocuments(ctx, siteOptions, nodeOptions) {
|
|
|
157
162
|
tolerateErrors,
|
|
158
163
|
debugName: nodeType.kebabName,
|
|
159
164
|
});
|
|
160
|
-
|
|
161
|
-
|
|
165
|
+
const errorMessages = dedupMessages(tolerateErrors.errorMessages);
|
|
166
|
+
if (errorMessages.length > 0) {
|
|
167
|
+
ctx.logger.warn(`Error generating content for ${nodeType.typeName}:\n - ${errorMessages.join("\n - ")}`);
|
|
162
168
|
}
|
|
163
169
|
await ctx.service.connector.addMultipleDocumentContents(fqdn, {
|
|
164
170
|
parentNodeId,
|
|
165
171
|
contents: list.map((content) => toRiDocumentContent(content, nodeType)),
|
|
166
172
|
});
|
|
173
|
+
report.addContentCount(list.length);
|
|
167
174
|
}
|
|
168
|
-
async function addParts(ctx, siteOptions, nodeOptions) {
|
|
175
|
+
async function addParts(ctx, report, siteOptions, nodeOptions) {
|
|
169
176
|
ctx.logger.debug(`[TASK] Adding parts "${nodeOptions.nodeType.typeName}"…`);
|
|
170
177
|
const { parentNodeId, nodeType, documentType } = nodeOptions;
|
|
171
178
|
const { fqdn, siteSchema, schemaI18n } = siteOptions;
|
|
@@ -182,13 +189,15 @@ async function addParts(ctx, siteOptions, nodeOptions) {
|
|
|
182
189
|
tolerateErrors,
|
|
183
190
|
debugName: nodeType.kebabName,
|
|
184
191
|
});
|
|
185
|
-
|
|
186
|
-
|
|
192
|
+
const errorMessages = dedupMessages(tolerateErrors.errorMessages);
|
|
193
|
+
if (errorMessages.length > 0) {
|
|
194
|
+
ctx.logger.warn(`Error generating content for ${nodeType.typeName}:\n - ${errorMessages.join("\n - ")}`);
|
|
187
195
|
}
|
|
188
196
|
await ctx.service.connector.addMultiplePartContents(fqdn, {
|
|
189
197
|
parentNodeId,
|
|
190
198
|
contents: list.map((content) => toRiPartContent(content, nodeType)),
|
|
191
199
|
});
|
|
200
|
+
report.addContentCount(list.length);
|
|
192
201
|
}
|
|
193
202
|
function toRiDocumentContent(content, nodeType) {
|
|
194
203
|
const { title, fields, featuredImage } = content;
|
|
@@ -106,9 +106,9 @@ export function parseLlmRawTags(llmResponse, tagNames, options = {}) {
|
|
|
106
106
|
const current = matches[i];
|
|
107
107
|
if (current.isOpening) {
|
|
108
108
|
// Find the next corresponding closing tag
|
|
109
|
-
|
|
109
|
+
const j = i + 1;
|
|
110
110
|
let foundClosing = false;
|
|
111
|
-
|
|
111
|
+
if (j < matches.length) {
|
|
112
112
|
const next = matches[j];
|
|
113
113
|
// If we encounter another opening tag of any type before finding our closing tag,
|
|
114
114
|
// it's an error if not tolerating errors
|
|
@@ -119,9 +119,15 @@ export function parseLlmRawTags(llmResponse, tagNames, options = {}) {
|
|
|
119
119
|
tolerateErrors.errorMessages.push(message);
|
|
120
120
|
foundClosing = undefined;
|
|
121
121
|
// If we are tolerating errors, we skip this opening tag entirely
|
|
122
|
-
break;
|
|
123
122
|
}
|
|
124
|
-
|
|
123
|
+
else {
|
|
124
|
+
if (next.tagName !== current.tagName) {
|
|
125
|
+
// Found a non-matching closing tag
|
|
126
|
+
const message = `Mismatched tags: opening <${current.tagName}>, closing </${next.tagName}>`;
|
|
127
|
+
if (!tolerateErrors)
|
|
128
|
+
throw new Error(message);
|
|
129
|
+
tolerateErrors.errorMessages.push(message);
|
|
130
|
+
}
|
|
125
131
|
// Found a matching closing tag
|
|
126
132
|
const contentStart = current.position + `<${current.tagName}>`.length;
|
|
127
133
|
const contentEnd = next.position;
|
|
@@ -133,19 +139,7 @@ export function parseLlmRawTags(llmResponse, tagNames, options = {}) {
|
|
|
133
139
|
// Skip to after this closing tag
|
|
134
140
|
i = j;
|
|
135
141
|
foundClosing = true;
|
|
136
|
-
break;
|
|
137
142
|
}
|
|
138
|
-
if (!next.isOpening && next.tagName !== current.tagName) {
|
|
139
|
-
// Found a non-matching closing tag
|
|
140
|
-
const message = `Mismatched tags: opening <${current.tagName}>, closing </${next.tagName}>`;
|
|
141
|
-
if (!tolerateErrors)
|
|
142
|
-
throw new Error(message);
|
|
143
|
-
tolerateErrors.errorMessages.push(message);
|
|
144
|
-
foundClosing = undefined;
|
|
145
|
-
// If we are tolerating errors, we skip this current opening tag entirely
|
|
146
|
-
break;
|
|
147
|
-
}
|
|
148
|
-
++j;
|
|
149
143
|
}
|
|
150
144
|
// Handle case where no matching closing tag was found
|
|
151
145
|
if (foundClosing === false) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { boolVal, listValOrUndef, strVal, strValOrUndef, } from "@paroi/data-formatters-lib";
|
|
2
|
+
import { updateSession } from "../../db/db.queries.js";
|
|
2
3
|
import { createPromptTemplate } from "../lib/create-prompt.js";
|
|
3
4
|
import { debugLlmOutput } from "../lib/debug-utils.js";
|
|
4
5
|
import { parseLlmResponseAsProperties } from "../lib/parse-llm-response.js";
|
|
@@ -35,6 +36,7 @@ export async function invokeMessageGuard(ctx, input, { skipOutOfScopeCheck = fal
|
|
|
35
36
|
}
|
|
36
37
|
if (report.valid)
|
|
37
38
|
return;
|
|
39
|
+
await updateSession(ctx, { guardCountInc: 1 });
|
|
38
40
|
return {
|
|
39
41
|
success: false,
|
|
40
42
|
language: report.language,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { updateSession } from "../../db/db.queries.js";
|
|
1
2
|
import { reorderObjectKeys } from "../helpers/js-utils.js";
|
|
2
3
|
import { createPromptTemplate, getPredefinedFields, getSiteSchemaTsDefs, } from "../lib/create-prompt.js";
|
|
3
4
|
import { debugLlmOutput } from "../lib/debug-utils.js";
|
|
@@ -17,13 +18,14 @@ const fieldsPrompt = await createPromptTemplate({
|
|
|
17
18
|
export async function invokeNewSiteAnalysis(ctx, input) {
|
|
18
19
|
const { analysis, explanation, unusedInformation } = await invokeAnalysisStep1(ctx, input);
|
|
19
20
|
const siteSchema = createSiteSchemaFromAnalysis(analysis);
|
|
20
|
-
await invokeAnalysisStep2(ctx, { prompt: unusedInformation ?? "" }, siteSchema);
|
|
21
|
+
const { unusedInformation: unusedInformation2 } = await invokeAnalysisStep2(ctx, { prompt: createUnusedInformationPrompt(unusedInformation, analysis) ?? "" }, siteSchema);
|
|
21
22
|
reorderSiteSchemaNodeTypes(siteSchema);
|
|
22
23
|
const l10n = createL10n(analysis, siteSchema);
|
|
23
24
|
const siteTitle = {
|
|
24
25
|
[analysis.siteProperties.language]: analysis.siteProperties.title,
|
|
25
26
|
};
|
|
26
|
-
if (!
|
|
27
|
+
if (!unusedInformation2) {
|
|
28
|
+
await updateSession(ctx, { status: "analyzed", promptCountInc: 1 });
|
|
27
29
|
return {
|
|
28
30
|
siteTitle,
|
|
29
31
|
siteSchema,
|
|
@@ -32,15 +34,16 @@ export async function invokeNewSiteAnalysis(ctx, input) {
|
|
|
32
34
|
explanation,
|
|
33
35
|
};
|
|
34
36
|
}
|
|
35
|
-
ctx.logger.debug("Unused information:",
|
|
37
|
+
ctx.logger.debug("Unused information:", unusedInformation2);
|
|
36
38
|
const updated = await invokeUpdateSiteSchema(ctx, {
|
|
37
|
-
prompt:
|
|
39
|
+
prompt: unusedInformation2,
|
|
38
40
|
generatedSchema: {
|
|
39
41
|
siteTitle,
|
|
40
42
|
siteSchema,
|
|
41
43
|
l10n,
|
|
42
44
|
},
|
|
43
|
-
});
|
|
45
|
+
}, { asRemainingPrompt: true });
|
|
46
|
+
await updateSession(ctx, { status: "analyzed", promptCountInc: 1 });
|
|
44
47
|
return {
|
|
45
48
|
siteTitle,
|
|
46
49
|
siteSchema: updated.siteSchema,
|
|
@@ -122,16 +125,23 @@ siteSchema) {
|
|
|
122
125
|
const llmMessage = await fieldsPrompt.pipe(ctx.goodModel).invoke(llmInput);
|
|
123
126
|
llmMessageContent = await debug.getMessageContent(llmMessage);
|
|
124
127
|
}
|
|
125
|
-
const { assignedFields } = parseLlmResponseAsProperties(llmMessageContent, [
|
|
128
|
+
const { assignedFields, unusedInformation } = parseLlmResponseAsProperties(llmMessageContent, [
|
|
126
129
|
{
|
|
127
130
|
tagName: "yaml_result",
|
|
128
131
|
key: "assignedFields",
|
|
129
132
|
format: "yaml",
|
|
130
133
|
},
|
|
134
|
+
{
|
|
135
|
+
tagName: "unused_information_md",
|
|
136
|
+
key: "unusedInformation",
|
|
137
|
+
format: "markdown",
|
|
138
|
+
optional: true,
|
|
139
|
+
},
|
|
131
140
|
]);
|
|
132
141
|
if (siteSchema.nodeTypes) {
|
|
133
142
|
assignFieldsToNodeTypes(ctx, assignedFields, siteSchema.nodeTypes);
|
|
134
143
|
}
|
|
144
|
+
return { unusedInformation };
|
|
135
145
|
}
|
|
136
146
|
function assignFieldsToNodeTypes(ctx, assignedFields, nodeTypes) {
|
|
137
147
|
const remainingTypeNames = new Set(Object.keys(assignedFields));
|
|
@@ -169,3 +179,15 @@ function reorderSiteSchemaNodeTypes(siteSchema) {
|
|
|
169
179
|
"orderChildrenBy",
|
|
170
180
|
]));
|
|
171
181
|
}
|
|
182
|
+
function createUnusedInformationPrompt(unusedInformation, analysis) {
|
|
183
|
+
const prompts = Object.entries(analysis.dictionary)
|
|
184
|
+
.map(([typeName, entry]) => {
|
|
185
|
+
return entry.prompt ? `${typeName}: ${entry.prompt}` : undefined;
|
|
186
|
+
})
|
|
187
|
+
.filter(Boolean);
|
|
188
|
+
if (prompts.length > 0) {
|
|
189
|
+
const nodeTypePrompts = `To do:\n\n- ${prompts.join("- \n")}`;
|
|
190
|
+
return unusedInformation ? `${nodeTypePrompts}\n\n${unusedInformation}` : nodeTypePrompts;
|
|
191
|
+
}
|
|
192
|
+
return unusedInformation;
|
|
193
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { updateSession } from "../../db/db.queries.js";
|
|
1
2
|
import { createPromptTemplate, getPredefinedFields, getSiteSchemaTsDefs, } from "../lib/create-prompt.js";
|
|
2
3
|
import { debugLlmOutput } from "../lib/debug-utils.js";
|
|
3
4
|
import { parseLlmResponseAsProperties } from "../lib/parse-llm-response.js";
|
|
@@ -7,7 +8,7 @@ const prompt1Tpl = await createPromptTemplate({
|
|
|
7
8
|
const prompt2Tpl = await createPromptTemplate({
|
|
8
9
|
fileName: "update-site-schema-2-execute.md",
|
|
9
10
|
});
|
|
10
|
-
export async function invokeUpdateSiteSchema(ctx, input) {
|
|
11
|
+
export async function invokeUpdateSiteSchema(ctx, input, { asRemainingPrompt = false } = {}) {
|
|
11
12
|
const task = await invokeUpdateSiteSchemaStep1(ctx, input);
|
|
12
13
|
if (!task.taskDetailsMd) {
|
|
13
14
|
// no changes
|
|
@@ -21,6 +22,9 @@ export async function invokeUpdateSiteSchema(ctx, input) {
|
|
|
21
22
|
taskDetailsMd: task.taskDetailsMd,
|
|
22
23
|
generatedSchema: input.generatedSchema,
|
|
23
24
|
});
|
|
25
|
+
if (!asRemainingPrompt) {
|
|
26
|
+
await updateSession(ctx, { status: "updated", promptCountInc: 1 });
|
|
27
|
+
}
|
|
24
28
|
return {
|
|
25
29
|
...genSchema,
|
|
26
30
|
changed: true,
|
|
@@ -1,17 +1,13 @@
|
|
|
1
1
|
import { generateSlug } from "@paroicms/public-anywhere-lib";
|
|
2
|
-
import { randomUUID } from "node:crypto";
|
|
3
2
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
4
3
|
import { join } from "node:path";
|
|
4
|
+
import { updateSession } from "../../db/db.queries.js";
|
|
5
5
|
import { fillSiteWithFakeContent } from "../fake-content-generator.ts/create-database-with-fake-content.js";
|
|
6
6
|
import { createTheme } from "./theme-creator.js";
|
|
7
7
|
export async function generateSite(ctx, input) {
|
|
8
|
-
const { service, logger, packConf } = ctx;
|
|
9
|
-
const { sitesDir, packName } = packConf;
|
|
8
|
+
const { service, logger, sitesDir, packConf: { packName }, } = ctx;
|
|
10
9
|
const { generatedSchema: { l10n, siteSchema, siteTitle }, withFakeContent, } = input;
|
|
11
|
-
|
|
12
|
-
throw new Error(`Site-generator plugin can generate sites only for pack with "sitesDir", but pack "${packName}" doesn't have it`);
|
|
13
|
-
}
|
|
14
|
-
const siteId = randomUUID();
|
|
10
|
+
const siteId = ctx.sessionId;
|
|
15
11
|
logger.info(`Generating site: ${siteId}…`);
|
|
16
12
|
const siteDir = join(sitesDir, siteId);
|
|
17
13
|
await mkdir(siteDir);
|
|
@@ -23,14 +19,17 @@ export async function generateSite(ctx, input) {
|
|
|
23
19
|
siteTitle: siteTitle.en ?? Object.values(siteTitle)[0] ?? "new-website",
|
|
24
20
|
}), null, 2), "utf-8");
|
|
25
21
|
await createTheme(ctx, siteDir, siteSchema);
|
|
26
|
-
const
|
|
22
|
+
const regSite = await service.connector.registerNewSite({
|
|
27
23
|
packName,
|
|
28
24
|
siteDir,
|
|
29
25
|
domain: siteId,
|
|
30
26
|
version: "0.0.0",
|
|
31
27
|
});
|
|
32
28
|
if (withFakeContent) {
|
|
33
|
-
await fillSiteWithFakeContent(ctx, {
|
|
29
|
+
const report = await fillSiteWithFakeContent(ctx, { regSite, siteTitle });
|
|
30
|
+
await updateSession(ctx, {
|
|
31
|
+
contentCountInc: report.getContentCount(),
|
|
32
|
+
});
|
|
34
33
|
}
|
|
35
34
|
const account = {
|
|
36
35
|
kind: "local",
|
|
@@ -38,8 +37,12 @@ export async function generateSite(ctx, input) {
|
|
|
38
37
|
name: "Admin",
|
|
39
38
|
password: Math.random().toString(36).substring(2, 6), // 4 random lowercase characters,
|
|
40
39
|
};
|
|
41
|
-
await ctx.service.connector.createAccount(
|
|
42
|
-
const { siteUrl } =
|
|
40
|
+
await ctx.service.connector.createAccount(regSite.fqdn, account, { asContactEmail: true });
|
|
41
|
+
const { siteUrl } = regSite;
|
|
42
|
+
await updateSession(ctx, {
|
|
43
|
+
status: "generated",
|
|
44
|
+
nodeTypeCount: siteSchema.nodeTypes?.length ?? 0,
|
|
45
|
+
});
|
|
43
46
|
return {
|
|
44
47
|
siteId,
|
|
45
48
|
url: siteUrl,
|