@paroicms/site-generator-plugin 0.8.0 → 0.9.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/gen-backend/dist/commands/actions.js +49 -0
- package/gen-backend/dist/db/db.queries.js +3 -3
- package/gen-backend/dist/errors.js +20 -0
- package/gen-backend/dist/generator/actions.js +45 -0
- package/gen-backend/dist/generator/fake-content-generator.ts/augment-fields.js +51 -0
- package/gen-backend/dist/generator/fake-content-generator.ts/content-helpers.js +17 -0
- package/gen-backend/dist/generator/fake-content-generator.ts/create-database-with-fake-content.js +7 -4
- package/gen-backend/dist/generator/generator-session.js +33 -0
- package/gen-backend/dist/generator/generator-types.js +1 -0
- package/gen-backend/dist/generator/lib/parse-llm-response.js +10 -16
- package/gen-backend/dist/generator/lib/token-tracking.js +118 -0
- package/gen-backend/dist/generator/llm-queries/invoke-new-site-analysis.js +24 -5
- package/gen-backend/dist/generator/session/generator-session.js +33 -0
- package/gen-backend/dist/generator/session/session-command.js +17 -0
- package/gen-backend/dist/generator/site-generator/theme-scss.js +262 -0
- package/gen-backend/dist/generator/site-schema-generator/create-site-schema.js +1 -0
- package/gen-backend/dist/lib/generator-context.js +14 -0
- package/gen-backend/dist/plugin.js +2 -2
- package/gen-backend/prompts/0-context.md +4 -2
- package/gen-backend/prompts/new-site-1-analysis.md +23 -7
- 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.mjs +64 -64
- package/package.json +3 -3
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { invokeMessageGuard } from "../generator/llm-queries/invoke-message-guard.js";
|
|
2
|
+
import { invokeNewSiteAnalysis } from "../generator/llm-queries/invoke-new-site-analysis.js";
|
|
3
|
+
import { invokeUpdateSiteSchema } from "../generator/llm-queries/invoke-update-site-schema.js";
|
|
4
|
+
import { generateSite } from "../generator/site-generator/site-generator.js";
|
|
5
|
+
import { newSession, verifySessionToken } from "./generator-session.js";
|
|
6
|
+
export async function executeCommand(commandCtx, input) {
|
|
7
|
+
if (input.command === "newSession") {
|
|
8
|
+
return {
|
|
9
|
+
success: true,
|
|
10
|
+
result: await newSession(commandCtx, input),
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
const { sessionId } = verifySessionToken(commandCtx, input.sessionToken);
|
|
14
|
+
const ctx = {
|
|
15
|
+
...commandCtx,
|
|
16
|
+
sessionId,
|
|
17
|
+
};
|
|
18
|
+
if (input.command === "createSiteSchema") {
|
|
19
|
+
const errorResponse = await invokeMessageGuard(ctx, input);
|
|
20
|
+
if (errorResponse)
|
|
21
|
+
return errorResponse;
|
|
22
|
+
return {
|
|
23
|
+
success: true,
|
|
24
|
+
result: await invokeNewSiteAnalysis(ctx, input),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
if (input.command === "updateSiteSchema") {
|
|
28
|
+
const errorResponse = await invokeMessageGuard(ctx, input, {
|
|
29
|
+
skipOutOfScopeCheck: true,
|
|
30
|
+
});
|
|
31
|
+
if (errorResponse)
|
|
32
|
+
return errorResponse;
|
|
33
|
+
return {
|
|
34
|
+
success: true,
|
|
35
|
+
result: await invokeUpdateSiteSchema(ctx, input),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
if (input.command === "generateSite") {
|
|
39
|
+
return {
|
|
40
|
+
success: true,
|
|
41
|
+
result: await generateSite(ctx, input),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
ctx.logger.error(`Invalid command: ${input.command}`);
|
|
45
|
+
return {
|
|
46
|
+
success: false,
|
|
47
|
+
userMessage: "Invalid command",
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { strVal } from "@paroi/data-formatters-lib";
|
|
1
|
+
import { strVal, strValOrUndef } from "@paroi/data-formatters-lib";
|
|
2
2
|
export async function readSession(ctx, sessionId) {
|
|
3
3
|
const { cn } = ctx;
|
|
4
4
|
const row = await cn("PaGenSession")
|
|
5
|
-
.select("id", "status", "guardCount", "promptCount", "nodeTypeCount", "contentCount", "errorMessage")
|
|
5
|
+
.select("id", "createdAt", "status", "guardCount", "promptCount", "nodeTypeCount", "contentCount", "errorMessage")
|
|
6
6
|
.where({ id: sessionId })
|
|
7
7
|
.first();
|
|
8
8
|
if (!row)
|
|
@@ -15,7 +15,7 @@ export async function readSession(ctx, sessionId) {
|
|
|
15
15
|
promptCount: Number(row.promptCount),
|
|
16
16
|
nodeTypeCount: Number(row.nodeTypeCount),
|
|
17
17
|
contentCount: Number(row.contentCount),
|
|
18
|
-
errorMessage:
|
|
18
|
+
errorMessage: strValOrUndef(row.errorMessage),
|
|
19
19
|
};
|
|
20
20
|
}
|
|
21
21
|
export async function insertSession(ctx, { sessionId }) {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom API error class for handling API-specific errors
|
|
3
|
+
*/
|
|
4
|
+
export class ApiError extends Error {
|
|
5
|
+
statusCode;
|
|
6
|
+
constructor(message, statusCode = 400) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = "ApiError";
|
|
9
|
+
this.statusCode = statusCode;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Create a new API error
|
|
14
|
+
* @param message Error message
|
|
15
|
+
* @param statusCode HTTP status code (default: 400)
|
|
16
|
+
* @returns ApiError instance
|
|
17
|
+
*/
|
|
18
|
+
export function createApiError(message, statusCode = 400) {
|
|
19
|
+
return new ApiError(message, statusCode);
|
|
20
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { invokeMessageGuard } from "./llm-queries/invoke-message-guard.js";
|
|
2
|
+
import { invokeNewSiteAnalysis } from "./llm-queries/invoke-new-site-analysis.js";
|
|
3
|
+
import { invokeUpdateSiteSchema } from "./llm-queries/invoke-update-site-schema.js";
|
|
4
|
+
import { newSession, verifySessionToken } from "./session/generator-session.js";
|
|
5
|
+
import { generateSite } from "./site-generator/site-generator.js";
|
|
6
|
+
export async function executeCommand(ctx, input) {
|
|
7
|
+
if (input.command === "newSession") {
|
|
8
|
+
return {
|
|
9
|
+
success: true,
|
|
10
|
+
result: await newSession(ctx, input),
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
verifySessionToken(input.sessionToken);
|
|
14
|
+
if (input.command === "createSiteSchema") {
|
|
15
|
+
const errorResponse = await invokeMessageGuard(ctx, input);
|
|
16
|
+
if (errorResponse)
|
|
17
|
+
return errorResponse;
|
|
18
|
+
return {
|
|
19
|
+
success: true,
|
|
20
|
+
result: await invokeNewSiteAnalysis(ctx, input),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
if (input.command === "updateSiteSchema") {
|
|
24
|
+
const errorResponse = await invokeMessageGuard(ctx, input, {
|
|
25
|
+
skipOutOfScopeCheck: true,
|
|
26
|
+
});
|
|
27
|
+
if (errorResponse)
|
|
28
|
+
return errorResponse;
|
|
29
|
+
return {
|
|
30
|
+
success: true,
|
|
31
|
+
result: await invokeUpdateSiteSchema(ctx, input),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
if (input.command === "generateSite") {
|
|
35
|
+
return {
|
|
36
|
+
success: true,
|
|
37
|
+
result: await generateSite(ctx, input),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
ctx.logger.error(`Invalid command: ${input.command}`);
|
|
41
|
+
return {
|
|
42
|
+
success: false,
|
|
43
|
+
userMessage: "Invalid command",
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { getRandomImagePath } from "../lib/images-lib.js";
|
|
2
|
+
export function augmentWithComputedValues(list, nodeType, language) {
|
|
3
|
+
if (!nodeType.fields)
|
|
4
|
+
return;
|
|
5
|
+
for (const item of list) {
|
|
6
|
+
for (const f of nodeType.fields) {
|
|
7
|
+
const languageKey = f.storedOn === "node" ? "." : language;
|
|
8
|
+
if (f.storedAs === "mediaHandle") {
|
|
9
|
+
const value = getMediaGeneratedFieldContent(f, language);
|
|
10
|
+
if (value) {
|
|
11
|
+
item[f.name] = value;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
const value = getDefaultValueForField(f.name);
|
|
16
|
+
if (value !== undefined) {
|
|
17
|
+
item[f.name] = { [languageKey]: value };
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function getMediaGeneratedFieldContent(f, language) {
|
|
24
|
+
const languageKey = f.storedOn === "node" ? "." : language;
|
|
25
|
+
if (f.dataType === "media") {
|
|
26
|
+
return { [languageKey]: { file: getRandomImagePath() } };
|
|
27
|
+
}
|
|
28
|
+
if (f.dataType === "gallery") {
|
|
29
|
+
return {
|
|
30
|
+
[languageKey]: {
|
|
31
|
+
files: [
|
|
32
|
+
getRandomImagePath(),
|
|
33
|
+
getRandomImagePath(),
|
|
34
|
+
getRandomImagePath(),
|
|
35
|
+
getRandomImagePath(),
|
|
36
|
+
getRandomImagePath(),
|
|
37
|
+
getRandomImagePath(),
|
|
38
|
+
],
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function getDefaultValueForField(fieldName) {
|
|
44
|
+
if (fieldName === "phone" || fieldName === "phone2")
|
|
45
|
+
return "0123456789";
|
|
46
|
+
if (fieldName === "phones")
|
|
47
|
+
return JSON.stringify(["0123456789", "9876543210"]);
|
|
48
|
+
if (fieldName === "updateDateTime")
|
|
49
|
+
return new Date().toISOString();
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
@@ -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,6 +2,7 @@ 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";
|
|
5
6
|
import { createGeneratedContentReport } from "./content-report.js";
|
|
6
7
|
import { generateLocalizedFooterMention } from "./create-node-contents.js";
|
|
7
8
|
import { generateFieldSetContent, generateMultipleFieldSetContents, } from "./generate-fake-content.js";
|
|
@@ -161,8 +162,9 @@ async function addRegularDocuments(ctx, report, siteOptions, nodeOptions) {
|
|
|
161
162
|
tolerateErrors,
|
|
162
163
|
debugName: nodeType.kebabName,
|
|
163
164
|
});
|
|
164
|
-
|
|
165
|
-
|
|
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 - ")}`);
|
|
166
168
|
}
|
|
167
169
|
await ctx.service.connector.addMultipleDocumentContents(fqdn, {
|
|
168
170
|
parentNodeId,
|
|
@@ -187,8 +189,9 @@ async function addParts(ctx, report, siteOptions, nodeOptions) {
|
|
|
187
189
|
tolerateErrors,
|
|
188
190
|
debugName: nodeType.kebabName,
|
|
189
191
|
});
|
|
190
|
-
|
|
191
|
-
|
|
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 - ")}`);
|
|
192
195
|
}
|
|
193
196
|
await ctx.service.connector.addMultiplePartContents(fqdn, {
|
|
194
197
|
parentNodeId,
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import jwt from "jsonwebtoken";
|
|
2
|
+
import { createApiError } from "../errors.js";
|
|
3
|
+
const JWT_SECRET = "init"; // Hardcoded JWT secret as specified
|
|
4
|
+
/**
|
|
5
|
+
* Verifies a session token and returns the session ID if valid
|
|
6
|
+
* @param token The JWT token to verify
|
|
7
|
+
* @returns The session ID contained in the token
|
|
8
|
+
* @throws ApiError if the token is invalid or expired
|
|
9
|
+
*/
|
|
10
|
+
export function verifySessionToken(token) {
|
|
11
|
+
try {
|
|
12
|
+
// Verify and decode the token
|
|
13
|
+
const payload = jwt.verify(token, JWT_SECRET);
|
|
14
|
+
if (!payload || !payload.sessionId) {
|
|
15
|
+
throw createApiError("Invalid session token", 401);
|
|
16
|
+
}
|
|
17
|
+
return payload.sessionId;
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
if (error.name === "TokenExpiredError") {
|
|
21
|
+
throw createApiError("Session token expired", 401);
|
|
22
|
+
}
|
|
23
|
+
else if (error.name === "JsonWebTokenError") {
|
|
24
|
+
throw createApiError("Invalid session token", 401);
|
|
25
|
+
}
|
|
26
|
+
// Re-throw our custom error
|
|
27
|
+
if (error.name === "ApiError") {
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
// Unexpected error
|
|
31
|
+
throw createApiError("Session verification failed", 500);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -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) {
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// Default limits if not specified in configuration
|
|
2
|
+
const DEFAULT_REQUEST_TOKENS = 8000;
|
|
3
|
+
const DEFAULT_SESSION_TOKENS = 32000;
|
|
4
|
+
const DEFAULT_DAILY_TOKENS = 100000;
|
|
5
|
+
/**
|
|
6
|
+
* Initialize token tracking for a session
|
|
7
|
+
*/
|
|
8
|
+
export function initTokenTracking(pluginConf) {
|
|
9
|
+
const requestLimit = pluginConf.maxRequestTokens ?? DEFAULT_REQUEST_TOKENS;
|
|
10
|
+
const sessionLimit = pluginConf.maxSessionTokens ?? DEFAULT_SESSION_TOKENS;
|
|
11
|
+
return {
|
|
12
|
+
requestLimit,
|
|
13
|
+
sessionLimit,
|
|
14
|
+
sessionUsage: 0,
|
|
15
|
+
userLimits: new Map(),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Roughly estimate token count from text length
|
|
20
|
+
* This is a simple approximation - for production, use a proper tokenizer
|
|
21
|
+
*/
|
|
22
|
+
export function estimateTokenCount(text) {
|
|
23
|
+
// Rough estimate: 1 token ≈ 4 characters for English text
|
|
24
|
+
return Math.ceil(text.length / 4);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Track token usage for the current user
|
|
28
|
+
*/
|
|
29
|
+
export function trackUserTokens(ctx, tokenCount) {
|
|
30
|
+
const { userKey, tokenUsage, logger } = ctx;
|
|
31
|
+
const dailyLimit = ctx.pluginConf.maxDailyTokens ?? DEFAULT_DAILY_TOKENS;
|
|
32
|
+
// Update session usage
|
|
33
|
+
tokenUsage.sessionUsage += tokenCount;
|
|
34
|
+
// Get or create user usage entry
|
|
35
|
+
let userUsage = tokenUsage.userLimits.get(userKey);
|
|
36
|
+
if (!userUsage) {
|
|
37
|
+
userUsage = {
|
|
38
|
+
dailyUsage: 0,
|
|
39
|
+
lastReset: new Date(),
|
|
40
|
+
totalRequests: 0,
|
|
41
|
+
};
|
|
42
|
+
tokenUsage.userLimits.set(userKey, userUsage);
|
|
43
|
+
}
|
|
44
|
+
// Reset daily usage if it's a new day
|
|
45
|
+
const today = new Date().toDateString();
|
|
46
|
+
if (userUsage.lastReset.toDateString() !== today) {
|
|
47
|
+
logger.info(`Resetting daily token usage for user ${userKey}`);
|
|
48
|
+
userUsage.dailyUsage = 0;
|
|
49
|
+
userUsage.lastReset = new Date();
|
|
50
|
+
}
|
|
51
|
+
// Update daily usage
|
|
52
|
+
userUsage.dailyUsage += tokenCount;
|
|
53
|
+
userUsage.totalRequests++;
|
|
54
|
+
// Log usage
|
|
55
|
+
logger.debug(`Token usage - User: ${userKey}, Request: ${tokenCount}, ` +
|
|
56
|
+
`Session: ${tokenUsage.sessionUsage}/${tokenUsage.sessionLimit}, ` +
|
|
57
|
+
`Daily: ${userUsage.dailyUsage}/${dailyLimit}`);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Check if token limits have been exceeded
|
|
61
|
+
* @throws Error if any limit is exceeded
|
|
62
|
+
*/
|
|
63
|
+
export function checkTokenLimits(ctx, estimatedTokens) {
|
|
64
|
+
const { tokenUsage, userKey, logger } = ctx;
|
|
65
|
+
const dailyLimit = ctx.pluginConf.maxDailyTokens ?? DEFAULT_DAILY_TOKENS;
|
|
66
|
+
// Check single request limit
|
|
67
|
+
if (estimatedTokens > tokenUsage.requestLimit) {
|
|
68
|
+
logger.warn(`User ${userKey} exceeded request token limit: ${estimatedTokens}/${tokenUsage.requestLimit}`);
|
|
69
|
+
throw new Error(`Prompt exceeds maximum token limit of ${tokenUsage.requestLimit}`);
|
|
70
|
+
}
|
|
71
|
+
// Check session limit
|
|
72
|
+
if (tokenUsage.sessionUsage + estimatedTokens > tokenUsage.sessionLimit) {
|
|
73
|
+
logger.warn(`User ${userKey} exceeded session token limit: ${tokenUsage.sessionUsage}/${tokenUsage.sessionLimit}`);
|
|
74
|
+
throw new Error("Session token limit exceeded. Please start a new session.");
|
|
75
|
+
}
|
|
76
|
+
// Check daily limit
|
|
77
|
+
const userUsage = tokenUsage.userLimits.get(userKey);
|
|
78
|
+
if (userUsage && userUsage.dailyUsage + estimatedTokens > dailyLimit) {
|
|
79
|
+
logger.warn(`User ${userKey} exceeded daily token limit: ${userUsage.dailyUsage}/${dailyLimit}`);
|
|
80
|
+
throw new Error("Daily token limit exceeded. Please try again tomorrow.");
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Safe wrapper for LLM invocation with token tracking and limits
|
|
85
|
+
*/
|
|
86
|
+
export async function invokeWithTokenLimit(ctx, model, prompt, processor) {
|
|
87
|
+
// For text prompts, estimate tokens
|
|
88
|
+
let promptTokens = 0;
|
|
89
|
+
if (typeof prompt === "string") {
|
|
90
|
+
promptTokens = estimateTokenCount(prompt);
|
|
91
|
+
}
|
|
92
|
+
else if (prompt.message && typeof prompt.message === "string") {
|
|
93
|
+
promptTokens = estimateTokenCount(prompt.message);
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
// JSON objects are harder to estimate; use a conservative approach
|
|
97
|
+
promptTokens = estimateTokenCount(JSON.stringify(prompt)) * 1.5;
|
|
98
|
+
}
|
|
99
|
+
// Check limits before making the call
|
|
100
|
+
checkTokenLimits(ctx, promptTokens);
|
|
101
|
+
// Make the LLM call
|
|
102
|
+
const response = await model.invoke(prompt);
|
|
103
|
+
// Estimate response tokens and track total
|
|
104
|
+
let responseContent = "";
|
|
105
|
+
if (typeof response.content === "string") {
|
|
106
|
+
responseContent = response.content;
|
|
107
|
+
}
|
|
108
|
+
else if (response.content && response.content.length > 0) {
|
|
109
|
+
// Handle array response
|
|
110
|
+
responseContent = response.content.join("\n");
|
|
111
|
+
}
|
|
112
|
+
const responseTokens = estimateTokenCount(responseContent);
|
|
113
|
+
const totalTokens = promptTokens + responseTokens;
|
|
114
|
+
// Track the usage
|
|
115
|
+
trackUserTokens(ctx, totalTokens);
|
|
116
|
+
// Process and return the response
|
|
117
|
+
return processor(response);
|
|
118
|
+
}
|
|
@@ -18,13 +18,13 @@ const fieldsPrompt = await createPromptTemplate({
|
|
|
18
18
|
export async function invokeNewSiteAnalysis(ctx, input) {
|
|
19
19
|
const { analysis, explanation, unusedInformation } = await invokeAnalysisStep1(ctx, input);
|
|
20
20
|
const siteSchema = createSiteSchemaFromAnalysis(analysis);
|
|
21
|
-
await invokeAnalysisStep2(ctx, { prompt: unusedInformation ?? "" }, siteSchema);
|
|
21
|
+
const { unusedInformation: unusedInformation2 } = await invokeAnalysisStep2(ctx, { prompt: createUnusedInformationPrompt(unusedInformation, analysis) ?? "" }, siteSchema);
|
|
22
22
|
reorderSiteSchemaNodeTypes(siteSchema);
|
|
23
23
|
const l10n = createL10n(analysis, siteSchema);
|
|
24
24
|
const siteTitle = {
|
|
25
25
|
[analysis.siteProperties.language]: analysis.siteProperties.title,
|
|
26
26
|
};
|
|
27
|
-
if (!
|
|
27
|
+
if (!unusedInformation2) {
|
|
28
28
|
await updateSession(ctx, { status: "analyzed", promptCountInc: 1 });
|
|
29
29
|
return {
|
|
30
30
|
siteTitle,
|
|
@@ -34,9 +34,9 @@ export async function invokeNewSiteAnalysis(ctx, input) {
|
|
|
34
34
|
explanation,
|
|
35
35
|
};
|
|
36
36
|
}
|
|
37
|
-
ctx.logger.debug("Unused information:",
|
|
37
|
+
ctx.logger.debug("Unused information:", unusedInformation2);
|
|
38
38
|
const updated = await invokeUpdateSiteSchema(ctx, {
|
|
39
|
-
prompt:
|
|
39
|
+
prompt: unusedInformation2,
|
|
40
40
|
generatedSchema: {
|
|
41
41
|
siteTitle,
|
|
42
42
|
siteSchema,
|
|
@@ -125,16 +125,23 @@ siteSchema) {
|
|
|
125
125
|
const llmMessage = await fieldsPrompt.pipe(ctx.goodModel).invoke(llmInput);
|
|
126
126
|
llmMessageContent = await debug.getMessageContent(llmMessage);
|
|
127
127
|
}
|
|
128
|
-
const { assignedFields } = parseLlmResponseAsProperties(llmMessageContent, [
|
|
128
|
+
const { assignedFields, unusedInformation } = parseLlmResponseAsProperties(llmMessageContent, [
|
|
129
129
|
{
|
|
130
130
|
tagName: "yaml_result",
|
|
131
131
|
key: "assignedFields",
|
|
132
132
|
format: "yaml",
|
|
133
133
|
},
|
|
134
|
+
{
|
|
135
|
+
tagName: "unused_information_md",
|
|
136
|
+
key: "unusedInformation",
|
|
137
|
+
format: "markdown",
|
|
138
|
+
optional: true,
|
|
139
|
+
},
|
|
134
140
|
]);
|
|
135
141
|
if (siteSchema.nodeTypes) {
|
|
136
142
|
assignFieldsToNodeTypes(ctx, assignedFields, siteSchema.nodeTypes);
|
|
137
143
|
}
|
|
144
|
+
return { unusedInformation };
|
|
138
145
|
}
|
|
139
146
|
function assignFieldsToNodeTypes(ctx, assignedFields, nodeTypes) {
|
|
140
147
|
const remainingTypeNames = new Set(Object.keys(assignedFields));
|
|
@@ -172,3 +179,15 @@ function reorderSiteSchemaNodeTypes(siteSchema) {
|
|
|
172
179
|
"orderChildrenBy",
|
|
173
180
|
]));
|
|
174
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
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { ApiError } from "@paroicms/public-server-lib";
|
|
2
|
+
import { nanoid } from "nanoid";
|
|
3
|
+
const { sign, verify } = (await import("jsonwebtoken")).default;
|
|
4
|
+
const JWT_SECRET = "init"; // FIXME: Hardcoded JWT secret as specified
|
|
5
|
+
export async function newSession(ctx, command) {
|
|
6
|
+
const { service } = ctx;
|
|
7
|
+
const { recaptchaToken } = command;
|
|
8
|
+
const isValid = await service.validateRecaptchaResponse(recaptchaToken);
|
|
9
|
+
if (!isValid)
|
|
10
|
+
throw new ApiError("Invalid reCAPTCHA token", 400);
|
|
11
|
+
const sessionId = nanoid();
|
|
12
|
+
const token = sign({ sessionId }, JWT_SECRET, { expiresIn: "24h" });
|
|
13
|
+
return { token };
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Verifies a session token and returns the session ID if valid
|
|
17
|
+
*/
|
|
18
|
+
export function verifySessionToken(token) {
|
|
19
|
+
let payload;
|
|
20
|
+
try {
|
|
21
|
+
payload = verify(token, JWT_SECRET);
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
if (error.name === "TokenExpiredError")
|
|
25
|
+
throw new ApiError("Session token expired", 401);
|
|
26
|
+
if (error.name === "JsonWebTokenError")
|
|
27
|
+
throw new ApiError("Invalid session token", 401);
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
if (!payload || !payload.sessionId)
|
|
31
|
+
throw new ApiError("Invalid session token", 401);
|
|
32
|
+
return payload.sessionId;
|
|
33
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ApiError } from "@paroicms/public-server-lib";
|
|
2
|
+
import { nanoid } from "nanoid";
|
|
3
|
+
const { sign, verify } = (await import("jsonwebtoken")).default;
|
|
4
|
+
export async function newSession(ctx, command) {
|
|
5
|
+
const { service } = ctx;
|
|
6
|
+
const { recaptchaToken } = command;
|
|
7
|
+
const isValid = await service.validateRecaptchaResponse(recaptchaToken);
|
|
8
|
+
if (!isValid) {
|
|
9
|
+
throw new ApiError("Invalid reCAPTCHA token", {
|
|
10
|
+
status: 400,
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
const sessionId = nanoid();
|
|
14
|
+
const token = sign({ sessionId }, "init", // Hardcoded JWT secret as specified
|
|
15
|
+
{ expiresIn: "1h" });
|
|
16
|
+
return { token };
|
|
17
|
+
}
|