@mvriu5/payload-ai 0.5.10 → 1.2.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/dist/components/AIInput.js +318 -112
- package/dist/components/AIInput.module.css +11 -2
- package/dist/components/APIKeyField.js +41 -24
- package/dist/components/ActionToast.d.ts +2 -1
- package/dist/components/ActionToast.js +203 -103
- package/dist/components/AuditLogList.js +90 -53
- package/dist/components/DiffDialog.js +222 -121
- package/dist/components/Icons.js +390 -274
- package/dist/components/MentionPopover.js +33 -22
- package/dist/handlers/applyActionHandler.js +316 -107
- package/dist/handlers/chatHandler.js +749 -50
- package/dist/handlers/proposalDiffHandler.js +81 -26
- package/dist/index.js +2 -2
- package/dist/payload/logging.d.ts +9 -0
- package/dist/payload/logging.js +14 -0
- package/dist/payload/normalizeData.d.ts +17 -7
- package/dist/payload/normalizeData.js +22 -1
- package/dist/payload/proposalData.d.ts +31 -0
- package/dist/payload/proposalData.js +575 -0
- package/dist/payload/schemaContext.d.ts +3 -7
- package/dist/payload/schemaContext.js +49 -23
- package/package.json +40 -9
|
@@ -5,8 +5,134 @@ import { isAIProvider } from "../ai/providerOptions.js";
|
|
|
5
5
|
import { getModel, getProviderConfig } from "../ai/providerRuntime.js";
|
|
6
6
|
import { containsSensitiveData } from "../ai/sensitiveData.js";
|
|
7
7
|
import { isCollectionActionAllowed } from "../payload/collectionPermissions.js";
|
|
8
|
+
import { prepareProposalWriteData } from "../payload/proposalData.js";
|
|
8
9
|
import { buildPromptWithMentionContext, collectBlocks, describeCollectionLikeConfig, describeCollectionLikeSummary, getAllowedCollectionSlugs, getMentionContext } from "../payload/schemaContext.js";
|
|
9
|
-
import {
|
|
10
|
+
import { getLogPreview, logHandlerEvent } from "../payload/logging.js";
|
|
11
|
+
import { getOptionValue, getSafeProposalLabel, hasLocalizedData, hasValueAtPath, isRecord, setValueAtPath } from "../payload/shared.js";
|
|
12
|
+
const nonEmptyLocalizedDataSchema = z.record(z.string(), z.record(z.string(), z.unknown())).refine((value)=>Object.keys(value).length > 0, {
|
|
13
|
+
message: "localizedData must include at least one locale entry."
|
|
14
|
+
});
|
|
15
|
+
const e2eModeEnabled = ()=>process.env.PAYLOAD_AI_E2E_MODE === "true";
|
|
16
|
+
const createSSEEventStream = (events)=>{
|
|
17
|
+
const encoder = new TextEncoder();
|
|
18
|
+
return new ReadableStream({
|
|
19
|
+
start (controller) {
|
|
20
|
+
for (const { data, event } of events){
|
|
21
|
+
controller.enqueue(encoder.encode(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`));
|
|
22
|
+
}
|
|
23
|
+
controller.close();
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
const getChatCompletionReason = ({ proposalCount, toolFailures, writeIntent })=>{
|
|
28
|
+
if (proposalCount > 0) return "proposal_created";
|
|
29
|
+
if (toolFailures.length > 0) return "tool_validation_failed";
|
|
30
|
+
if (writeIntent) return "write_intent_without_tool_call";
|
|
31
|
+
return "model_did_not_call_tool";
|
|
32
|
+
};
|
|
33
|
+
const createDebugPayload = ({ activeLocale, debug, proposalCount, selectedLocales, toolFailures, usage, writeIntent })=>({
|
|
34
|
+
activeLocale,
|
|
35
|
+
model: debug.model,
|
|
36
|
+
proposalCount,
|
|
37
|
+
provider: debug.provider,
|
|
38
|
+
reason: getChatCompletionReason({
|
|
39
|
+
proposalCount,
|
|
40
|
+
toolFailures,
|
|
41
|
+
writeIntent
|
|
42
|
+
}),
|
|
43
|
+
selectedLocales,
|
|
44
|
+
toolFailures,
|
|
45
|
+
usage
|
|
46
|
+
});
|
|
47
|
+
const createE2EChatResponse = ({ prompt, selectedLocales })=>{
|
|
48
|
+
const normalizedPrompt = prompt.toLowerCase();
|
|
49
|
+
const wantsCreatePost = normalizedPrompt.includes("post") && (normalizedPrompt.includes("create") || normalizedPrompt.includes("erstell"));
|
|
50
|
+
const mentionsMars = normalizedPrompt.includes("mars");
|
|
51
|
+
const multipleLocales = selectedLocales.length > 1;
|
|
52
|
+
const activeLocale = selectedLocales.at(-1);
|
|
53
|
+
const proposalLabel = multipleLocales ? normalizedPrompt.includes("locale review") ? "Create localized locale review draft post about Mars" : "Create localized draft post about Mars" : normalizedPrompt.includes("apply flow") ? "Create apply flow draft post about Mars" : normalizedPrompt.includes("proposal review") ? "Create proposal review draft post about Mars" : "Create draft post about Mars";
|
|
54
|
+
const proposal = wantsCreatePost && mentionsMars ? signAIActionProposal(multipleLocales ? {
|
|
55
|
+
action: "create",
|
|
56
|
+
collection: "posts",
|
|
57
|
+
label: proposalLabel,
|
|
58
|
+
localizedData: Object.fromEntries(selectedLocales.map((locale)=>[
|
|
59
|
+
locale,
|
|
60
|
+
{
|
|
61
|
+
content: locale === "de" ? "Mars ist der vierte Planet von der Sonne." : "Mars is the fourth planet from the Sun.",
|
|
62
|
+
excerpt: locale === "de" ? "Ein kurzer Entwurf ueber Mars." : "A short draft about Mars.",
|
|
63
|
+
title: locale === "de" ? "Mars im Ueberblick" : "Mars Overview"
|
|
64
|
+
}
|
|
65
|
+
])),
|
|
66
|
+
...activeLocale ? {
|
|
67
|
+
locale: activeLocale
|
|
68
|
+
} : {}
|
|
69
|
+
} : {
|
|
70
|
+
action: "create",
|
|
71
|
+
collection: "posts",
|
|
72
|
+
data: {
|
|
73
|
+
content: "Mars is the fourth planet from the Sun.",
|
|
74
|
+
excerpt: "A short draft about Mars.",
|
|
75
|
+
status: "draft",
|
|
76
|
+
title: "Mars Overview"
|
|
77
|
+
},
|
|
78
|
+
label: proposalLabel,
|
|
79
|
+
...activeLocale ? {
|
|
80
|
+
locale: activeLocale
|
|
81
|
+
} : {}
|
|
82
|
+
}) : null;
|
|
83
|
+
const responseText = proposal ? "Prepared one draft post proposal." : "No content change was proposed.";
|
|
84
|
+
const debugPayload = createDebugPayload({
|
|
85
|
+
debug: {
|
|
86
|
+
model: "e2e-model",
|
|
87
|
+
provider: "openai",
|
|
88
|
+
tools: []
|
|
89
|
+
},
|
|
90
|
+
proposalCount: proposal ? 1 : 0,
|
|
91
|
+
selectedLocales,
|
|
92
|
+
toolFailures: [],
|
|
93
|
+
usage: {
|
|
94
|
+
inputTokens: 42,
|
|
95
|
+
outputTokens: 27,
|
|
96
|
+
totalTokens: 69
|
|
97
|
+
},
|
|
98
|
+
writeIntent: wantsCreatePost
|
|
99
|
+
});
|
|
100
|
+
return new Response(createSSEEventStream([
|
|
101
|
+
{
|
|
102
|
+
data: {
|
|
103
|
+
delta: responseText
|
|
104
|
+
},
|
|
105
|
+
event: "text"
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
data: {
|
|
109
|
+
proposals: proposal ? [
|
|
110
|
+
proposal
|
|
111
|
+
] : [],
|
|
112
|
+
usage: {
|
|
113
|
+
inputTokens: 42,
|
|
114
|
+
outputTokens: 27,
|
|
115
|
+
totalTokens: 69
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
event: "proposals"
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
data: debugPayload,
|
|
122
|
+
event: "debug"
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
data: {},
|
|
126
|
+
event: "done"
|
|
127
|
+
}
|
|
128
|
+
]), {
|
|
129
|
+
headers: {
|
|
130
|
+
"Cache-Control": "no-cache, no-transform",
|
|
131
|
+
Connection: "keep-alive",
|
|
132
|
+
"Content-Type": "text/event-stream; charset=utf-8"
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
};
|
|
10
136
|
const getRequiredFieldInfos = (fields, titleFieldName, path = "")=>{
|
|
11
137
|
return fields.flatMap((field)=>{
|
|
12
138
|
const fieldPath = field.name ? path ? `${path}.${field.name}` : field.name : path;
|
|
@@ -134,6 +260,277 @@ const getMissingCreateFields = ({ data, localizedData, requiredFields })=>{
|
|
|
134
260
|
];
|
|
135
261
|
return requiredFields.filter((field)=>!hasValueAtPath(data, field.path)).map((field)=>field.path);
|
|
136
262
|
};
|
|
263
|
+
const getProposalSummary = (proposal)=>({
|
|
264
|
+
action: proposal.action,
|
|
265
|
+
collection: "collection" in proposal ? proposal.collection : undefined,
|
|
266
|
+
hasData: "data" in proposal && Boolean(proposal.data),
|
|
267
|
+
hasLocalizedData: "localizedData" in proposal && Boolean(proposal.localizedData),
|
|
268
|
+
id: "id" in proposal ? proposal.id : undefined,
|
|
269
|
+
label: proposal.label,
|
|
270
|
+
locale: proposal.locale,
|
|
271
|
+
locales: "localizedData" in proposal && proposal.localizedData ? Object.keys(proposal.localizedData) : undefined,
|
|
272
|
+
slug: "slug" in proposal ? proposal.slug : undefined
|
|
273
|
+
});
|
|
274
|
+
const getCollectionBlockTypes = (fields)=>{
|
|
275
|
+
const blockTypes = new Set();
|
|
276
|
+
const visitFields = (items)=>{
|
|
277
|
+
for (const field of items){
|
|
278
|
+
if (field.type === "blocks" && field.blocks) {
|
|
279
|
+
for (const block of field.blocks){
|
|
280
|
+
blockTypes.add(block.slug);
|
|
281
|
+
if (block.fields?.length) {
|
|
282
|
+
visitFields(block.fields);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
if (field.fields?.length) {
|
|
287
|
+
visitFields(field.fields);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
visitFields(fields);
|
|
292
|
+
return [
|
|
293
|
+
...blockTypes
|
|
294
|
+
];
|
|
295
|
+
};
|
|
296
|
+
const getRequestedBlockTypes = ({ availableBlockTypes, mentions, prompt })=>{
|
|
297
|
+
const requestedBlockTypes = new Set();
|
|
298
|
+
const normalizedPrompt = prompt.toLowerCase();
|
|
299
|
+
for (const mention of mentions || []){
|
|
300
|
+
if (mention.type === "block" && mention.slug && availableBlockTypes.includes(mention.slug)) {
|
|
301
|
+
requestedBlockTypes.add(mention.slug);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
for (const blockType of availableBlockTypes){
|
|
305
|
+
const escapedBlockType = blockType.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
306
|
+
const blockPattern = new RegExp(`\\b${escapedBlockType}\\b(?:\\s+block)?`, "i");
|
|
307
|
+
if (blockPattern.test(normalizedPrompt)) {
|
|
308
|
+
requestedBlockTypes.add(blockType);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return [
|
|
312
|
+
...requestedBlockTypes
|
|
313
|
+
];
|
|
314
|
+
};
|
|
315
|
+
const collectProposalBlockTypes = ({ data, fields })=>{
|
|
316
|
+
const foundBlockTypes = new Set();
|
|
317
|
+
const visitFields = (items, value)=>{
|
|
318
|
+
for (const field of items){
|
|
319
|
+
if (!field.name) continue;
|
|
320
|
+
const fieldValue = value[field.name];
|
|
321
|
+
if (fieldValue === undefined || fieldValue === null) continue;
|
|
322
|
+
if (field.type === "blocks" && Array.isArray(fieldValue)) {
|
|
323
|
+
for (const blockItem of fieldValue){
|
|
324
|
+
if (!isRecord(blockItem)) continue;
|
|
325
|
+
const blockType = typeof blockItem.blockType === "string" ? blockItem.blockType : typeof blockItem.type === "string" ? blockItem.type : typeof blockItem.slug === "string" ? blockItem.slug : null;
|
|
326
|
+
if (blockType) {
|
|
327
|
+
foundBlockTypes.add(blockType);
|
|
328
|
+
}
|
|
329
|
+
if (blockType) {
|
|
330
|
+
const blockConfig = field.blocks?.find((candidate)=>candidate.slug === blockType);
|
|
331
|
+
if (blockConfig?.fields?.length) {
|
|
332
|
+
visitFields(blockConfig.fields, blockItem);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
if (field.type === "group" && isRecord(fieldValue) && field.fields?.length) {
|
|
339
|
+
visitFields(field.fields, fieldValue);
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
if (field.type === "array" && Array.isArray(fieldValue) && field.fields?.length) {
|
|
343
|
+
for (const item of fieldValue){
|
|
344
|
+
if (isRecord(item)) {
|
|
345
|
+
visitFields(field.fields, item);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
visitFields(fields, data);
|
|
352
|
+
return foundBlockTypes;
|
|
353
|
+
};
|
|
354
|
+
const collectRelationshipTargets = ({ data, fields, path = "" })=>{
|
|
355
|
+
const targets = [];
|
|
356
|
+
for (const field of fields){
|
|
357
|
+
if (!field.name) continue;
|
|
358
|
+
const fieldPath = path ? `${path}.${field.name}` : field.name;
|
|
359
|
+
const fieldValue = data[field.name];
|
|
360
|
+
if (fieldValue === undefined || fieldValue === null) continue;
|
|
361
|
+
if (field.type === "relationship" || field.type === "upload") {
|
|
362
|
+
const relationTargets = Array.isArray(field.relationTo) ? field.relationTo.filter((item)=>typeof item === "string") : typeof field.relationTo === "string" ? [
|
|
363
|
+
field.relationTo
|
|
364
|
+
] : [];
|
|
365
|
+
const collectSingle = (value, itemPath)=>{
|
|
366
|
+
if (typeof value === "string" || typeof value === "number") {
|
|
367
|
+
if (relationTargets.length === 1) {
|
|
368
|
+
targets.push({
|
|
369
|
+
collection: relationTargets[0],
|
|
370
|
+
id: value,
|
|
371
|
+
path: itemPath
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
if (!isRecord(value)) return;
|
|
377
|
+
const relationTo = typeof value.relationTo === "string" ? value.relationTo : relationTargets[0];
|
|
378
|
+
const id = typeof value.id === "string" || typeof value.id === "number" ? value.id : typeof value.value === "string" || typeof value.value === "number" ? value.value : undefined;
|
|
379
|
+
if (!relationTo || id === undefined) return;
|
|
380
|
+
targets.push({
|
|
381
|
+
collection: relationTo,
|
|
382
|
+
id,
|
|
383
|
+
path: itemPath
|
|
384
|
+
});
|
|
385
|
+
};
|
|
386
|
+
if (field.hasMany && Array.isArray(fieldValue)) {
|
|
387
|
+
fieldValue.forEach((item, index)=>collectSingle(item, `${fieldPath}.${index}`));
|
|
388
|
+
} else {
|
|
389
|
+
collectSingle(fieldValue, fieldPath);
|
|
390
|
+
}
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
if (field.type === "group" && isRecord(fieldValue) && field.fields?.length) {
|
|
394
|
+
targets.push(...collectRelationshipTargets({
|
|
395
|
+
data: fieldValue,
|
|
396
|
+
fields: field.fields,
|
|
397
|
+
path: fieldPath
|
|
398
|
+
}));
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
if (field.type === "array" && Array.isArray(fieldValue) && field.fields?.length) {
|
|
402
|
+
fieldValue.forEach((item, index)=>{
|
|
403
|
+
if (!isRecord(item)) return;
|
|
404
|
+
targets.push(...collectRelationshipTargets({
|
|
405
|
+
data: item,
|
|
406
|
+
fields: field.fields,
|
|
407
|
+
path: `${fieldPath}.${index}`
|
|
408
|
+
}));
|
|
409
|
+
});
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
if (field.type === "blocks" && Array.isArray(fieldValue) && field.blocks?.length) {
|
|
413
|
+
fieldValue.forEach((item, index)=>{
|
|
414
|
+
if (!isRecord(item)) return;
|
|
415
|
+
const blockType = typeof item.blockType === "string" ? item.blockType : typeof item.type === "string" ? item.type : typeof item.slug === "string" ? item.slug : null;
|
|
416
|
+
if (!blockType) return;
|
|
417
|
+
const blockConfig = field.blocks?.find((candidate)=>candidate.slug === blockType);
|
|
418
|
+
if (!blockConfig?.fields?.length) return;
|
|
419
|
+
targets.push(...collectRelationshipTargets({
|
|
420
|
+
data: item,
|
|
421
|
+
fields: blockConfig.fields,
|
|
422
|
+
path: `${fieldPath}.${index}`
|
|
423
|
+
}));
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return targets;
|
|
428
|
+
};
|
|
429
|
+
const validateRelationshipTargetsExist = async ({ data, fields, req })=>{
|
|
430
|
+
const targets = collectRelationshipTargets({
|
|
431
|
+
data,
|
|
432
|
+
fields
|
|
433
|
+
});
|
|
434
|
+
const invalidTargets = [];
|
|
435
|
+
for (const target of targets){
|
|
436
|
+
try {
|
|
437
|
+
await req.payload.findByID({
|
|
438
|
+
collection: target.collection,
|
|
439
|
+
depth: 0,
|
|
440
|
+
id: String(target.id),
|
|
441
|
+
overrideAccess: false,
|
|
442
|
+
req
|
|
443
|
+
});
|
|
444
|
+
} catch {
|
|
445
|
+
invalidTargets.push(target);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
return invalidTargets;
|
|
449
|
+
};
|
|
450
|
+
const getMentionSummary = (mentions)=>mentions?.map((mention)=>({
|
|
451
|
+
collection: "collection" in mention ? mention.collection : undefined,
|
|
452
|
+
id: "id" in mention ? mention.id : undefined,
|
|
453
|
+
slug: mention.slug,
|
|
454
|
+
type: mention.type
|
|
455
|
+
})) || [];
|
|
456
|
+
const formatProposalIssuesForRetry = (issues)=>{
|
|
457
|
+
return issues.slice(0, 6).map((issue)=>{
|
|
458
|
+
switch(issue.code){
|
|
459
|
+
case "invalid_block_type":
|
|
460
|
+
return `${issue.path}: use an exact blockType from the schema`;
|
|
461
|
+
case "invalid_blocks":
|
|
462
|
+
return `${issue.path}: blocks fields must be arrays of objects with blockType and exact field names`;
|
|
463
|
+
case "invalid_array":
|
|
464
|
+
return `${issue.path}: array fields must be arrays of complete objects`;
|
|
465
|
+
case "missing_required_field":
|
|
466
|
+
return `${issue.path}: required field missing`;
|
|
467
|
+
case "unknown_field":
|
|
468
|
+
return `${issue.path}: unknown field, use exact schema field names only`;
|
|
469
|
+
case "invalid_relationship":
|
|
470
|
+
return `${issue.path}: use relationship IDs or { relationTo, value }, not free text`;
|
|
471
|
+
default:
|
|
472
|
+
return `${issue.path}: ${issue.message}`;
|
|
473
|
+
}
|
|
474
|
+
}).join("; ");
|
|
475
|
+
};
|
|
476
|
+
const createCollectionAliasMap = (collections)=>{
|
|
477
|
+
const aliasMap = new Map();
|
|
478
|
+
const addAlias = (alias, slug)=>{
|
|
479
|
+
const normalizedAlias = alias?.trim().toLowerCase();
|
|
480
|
+
if (!normalizedAlias) return;
|
|
481
|
+
if (!aliasMap.has(normalizedAlias)) {
|
|
482
|
+
aliasMap.set(normalizedAlias, slug);
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
for (const collection of collections){
|
|
486
|
+
addAlias(collection.slug, collection.slug);
|
|
487
|
+
addAlias(collection.slug.replace(/-/g, " "), collection.slug);
|
|
488
|
+
const singular = typeof collection.labels?.singular === "string" ? collection.labels.singular : undefined;
|
|
489
|
+
const plural = typeof collection.labels?.plural === "string" ? collection.labels.plural : undefined;
|
|
490
|
+
addAlias(singular, collection.slug);
|
|
491
|
+
addAlias(plural, collection.slug);
|
|
492
|
+
if (singular?.endsWith("s")) {
|
|
493
|
+
addAlias(singular.slice(0, -1), collection.slug);
|
|
494
|
+
}
|
|
495
|
+
if (plural?.endsWith("s")) {
|
|
496
|
+
addAlias(plural.slice(0, -1), collection.slug);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
return Object.fromEntries(aliasMap.entries());
|
|
500
|
+
};
|
|
501
|
+
const getLikelyCollectionMatches = ({ aliasMap, prompt })=>{
|
|
502
|
+
const normalizedPrompt = prompt.toLowerCase();
|
|
503
|
+
const matches = Object.entries(aliasMap).filter(([alias])=>normalizedPrompt.includes(alias)).map(([, slug])=>slug);
|
|
504
|
+
return [
|
|
505
|
+
...new Set(matches)
|
|
506
|
+
];
|
|
507
|
+
};
|
|
508
|
+
const hasWriteIntent = (prompt)=>{
|
|
509
|
+
const normalizedPrompt = prompt.toLowerCase();
|
|
510
|
+
return /\b(create|build|generate|write|draft|make|add|update|edit|change|revise|refine|rewrite|translate|delete|remove)\b/.test(normalizedPrompt);
|
|
511
|
+
};
|
|
512
|
+
const getIntentToolChoice = (prompt)=>{
|
|
513
|
+
const normalizedPrompt = prompt.toLowerCase();
|
|
514
|
+
if (/\b(delete|remove)\b/.test(normalizedPrompt)) {
|
|
515
|
+
return {
|
|
516
|
+
toolName: "proposeDeleteDoc",
|
|
517
|
+
type: "tool"
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
if (/\b(create|build|generate|write|draft|make|add)\b/.test(normalizedPrompt)) {
|
|
521
|
+
return {
|
|
522
|
+
toolName: "proposeCreateDoc",
|
|
523
|
+
type: "tool"
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
if (/\b(update|edit|change|revise|refine|rewrite|translate)\b/.test(normalizedPrompt)) {
|
|
527
|
+
return {
|
|
528
|
+
toolName: "proposeUpdateDoc",
|
|
529
|
+
type: "tool"
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
return undefined;
|
|
533
|
+
};
|
|
137
534
|
export const createChatHandler = (options = {})=>async (req)=>{
|
|
138
535
|
if (!req.user) return Response.json({
|
|
139
536
|
error: "Unauthorized"
|
|
@@ -147,6 +544,15 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
147
544
|
}, {
|
|
148
545
|
status: 400
|
|
149
546
|
});
|
|
547
|
+
const selectedLocales = (body?.mentions?.filter((mention)=>mention.type === "locale" && mention.slug).map((mention)=>mention.slug) || []).filter((locale, index, array)=>array.indexOf(locale) === index);
|
|
548
|
+
const activeLocale = selectedLocales.at(-1);
|
|
549
|
+
const mentionSummary = getMentionSummary(body?.mentions);
|
|
550
|
+
if (e2eModeEnabled()) {
|
|
551
|
+
return createE2EChatResponse({
|
|
552
|
+
prompt,
|
|
553
|
+
selectedLocales
|
|
554
|
+
});
|
|
555
|
+
}
|
|
150
556
|
const user = req.user;
|
|
151
557
|
const requestedProvider = body?.provider || user.aiProvider || "openai";
|
|
152
558
|
if (!isAIProvider(requestedProvider)) return Response.json({
|
|
@@ -177,7 +583,23 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
177
583
|
"searchDocs"
|
|
178
584
|
]
|
|
179
585
|
};
|
|
586
|
+
logHandlerEvent(req, "info", {
|
|
587
|
+
activeLocale,
|
|
588
|
+
debug,
|
|
589
|
+
mentionCount: mentionSummary.length,
|
|
590
|
+
mentions: mentionSummary,
|
|
591
|
+
msg: "AI chat started",
|
|
592
|
+
promptPreview: getLogPreview(prompt),
|
|
593
|
+
selectedLocales
|
|
594
|
+
});
|
|
180
595
|
if (!providerConfig.apiKey) {
|
|
596
|
+
logHandlerEvent(req, "warn", {
|
|
597
|
+
activeLocale,
|
|
598
|
+
debug,
|
|
599
|
+
msg: "AI chat blocked: missing provider API key",
|
|
600
|
+
promptPreview: getLogPreview(prompt),
|
|
601
|
+
selectedLocales
|
|
602
|
+
});
|
|
181
603
|
return Response.json({
|
|
182
604
|
error: options.allowUserApiKeys === false ? `Configure a ${provider} API key in the server environment first.` : `Add a ${provider} API key to your account settings or configure it in the server environment first.`
|
|
183
605
|
}, {
|
|
@@ -186,25 +608,60 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
186
608
|
}
|
|
187
609
|
try {
|
|
188
610
|
const proposals = [];
|
|
611
|
+
const toolFailures = [];
|
|
612
|
+
const registerToolFailure = (failure)=>{
|
|
613
|
+
toolFailures.push(failure);
|
|
614
|
+
logHandlerEvent(req, "warn", {
|
|
615
|
+
debug,
|
|
616
|
+
...failure,
|
|
617
|
+
msg: "AI tool validation failed",
|
|
618
|
+
promptPreview: getLogPreview(prompt)
|
|
619
|
+
});
|
|
620
|
+
};
|
|
621
|
+
const createToolError = ({ collection, details, message, slug, tool })=>{
|
|
622
|
+
registerToolFailure({
|
|
623
|
+
collection,
|
|
624
|
+
details,
|
|
625
|
+
message,
|
|
626
|
+
slug,
|
|
627
|
+
tool
|
|
628
|
+
});
|
|
629
|
+
return {
|
|
630
|
+
error: message
|
|
631
|
+
};
|
|
632
|
+
};
|
|
189
633
|
const addSignedProposal = (proposal)=>{
|
|
190
634
|
if ("data" in proposal && proposal.data && containsSensitiveData(proposal.data)) {
|
|
191
|
-
return {
|
|
192
|
-
|
|
193
|
-
|
|
635
|
+
return createToolError({
|
|
636
|
+
details: getProposalSummary(proposal),
|
|
637
|
+
message: "Proposal contains sensitive fields and cannot be created.",
|
|
638
|
+
tool: `propose${proposal.action[0]?.toUpperCase()}${proposal.action.slice(1)}`
|
|
639
|
+
});
|
|
194
640
|
}
|
|
195
641
|
if ("localizedData" in proposal && hasLocalizedData(proposal.localizedData) && Object.values(proposal.localizedData).some((value)=>containsSensitiveData(value))) {
|
|
196
|
-
return {
|
|
197
|
-
|
|
198
|
-
|
|
642
|
+
return createToolError({
|
|
643
|
+
details: getProposalSummary(proposal),
|
|
644
|
+
message: "Proposal contains sensitive fields and cannot be created.",
|
|
645
|
+
tool: `propose${proposal.action[0]?.toUpperCase()}${proposal.action.slice(1)}`
|
|
646
|
+
});
|
|
199
647
|
}
|
|
200
648
|
const signedProposal = signAIActionProposal(proposal);
|
|
201
649
|
proposals.push(signedProposal);
|
|
650
|
+
logHandlerEvent(req, "info", {
|
|
651
|
+
debug,
|
|
652
|
+
msg: "AI proposal created",
|
|
653
|
+
proposal: getProposalSummary(signedProposal)
|
|
654
|
+
});
|
|
202
655
|
return signedProposal;
|
|
203
656
|
};
|
|
204
657
|
const collectionSlugs = getAllowedCollectionSlugs(req, options.collections);
|
|
205
658
|
const globalSlugs = req.payload.config.globals?.map((global)=>global.slug) || [];
|
|
206
659
|
const allowedCollections = req.payload.config.collections.filter((collection)=>collectionSlugs.includes(collection.slug));
|
|
207
660
|
if (collectionSlugs.length === 0) {
|
|
661
|
+
logHandlerEvent(req, "warn", {
|
|
662
|
+
debug,
|
|
663
|
+
msg: "AI chat blocked: no AI-enabled collections configured"
|
|
664
|
+
});
|
|
208
665
|
return Response.json({
|
|
209
666
|
error: "No AI-enabled collections are configured."
|
|
210
667
|
}, {
|
|
@@ -229,8 +686,6 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
229
686
|
mentions: body?.mentions,
|
|
230
687
|
req
|
|
231
688
|
});
|
|
232
|
-
const selectedLocales = (body?.mentions?.filter((mention)=>mention.type === "locale" && mention.slug).map((mention)=>mention.slug) || []).filter((locale, index, array)=>array.indexOf(locale) === index);
|
|
233
|
-
const activeLocale = selectedLocales.at(-1);
|
|
234
689
|
const mentionedCollectionSlugs = (body?.mentions?.flatMap((mention)=>{
|
|
235
690
|
if (mention.type === "collection" && mention.slug) return [
|
|
236
691
|
mention.slug
|
|
@@ -260,6 +715,38 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
260
715
|
titleFieldByCollection[slug]
|
|
261
716
|
]
|
|
262
717
|
] : []));
|
|
718
|
+
const collectionAliasMap = createCollectionAliasMap(allowedCollections);
|
|
719
|
+
const likelyCollectionMatches = getLikelyCollectionMatches({
|
|
720
|
+
aliasMap: collectionAliasMap,
|
|
721
|
+
prompt
|
|
722
|
+
});
|
|
723
|
+
const writeIntent = hasWriteIntent(prompt);
|
|
724
|
+
const inferredCollectionSlug = mentionedCollectionSlugs.length === 1 ? mentionedCollectionSlugs[0] : mentionedCollectionSlugs.length === 0 && likelyCollectionMatches.length === 1 ? likelyCollectionMatches[0] : undefined;
|
|
725
|
+
const inferredCollectionConfig = inferredCollectionSlug ? allowedCollections.find((collection)=>collection.slug === inferredCollectionSlug) : undefined;
|
|
726
|
+
if (inferredCollectionConfig && !mentionContext.some((item)=>item.type === "collection" && item.slug === inferredCollectionConfig.slug)) {
|
|
727
|
+
mentionContext.push({
|
|
728
|
+
...describeCollectionLikeConfig({
|
|
729
|
+
config: inferredCollectionConfig,
|
|
730
|
+
permissions: options.collections,
|
|
731
|
+
type: "collection"
|
|
732
|
+
}),
|
|
733
|
+
inferredFromPrompt: true
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
const intentToolChoice = inferredCollectionConfig ? getIntentToolChoice(prompt) : undefined;
|
|
737
|
+
logHandlerEvent(req, "info", {
|
|
738
|
+
activeLocale,
|
|
739
|
+
allowedCollectionCount: allowedCollections.length,
|
|
740
|
+
collectionSlugs,
|
|
741
|
+
focusedCollections: mentionedCollectionSlugs,
|
|
742
|
+
globalSlugs,
|
|
743
|
+
inferredCollectionSlug,
|
|
744
|
+
intentToolChoice,
|
|
745
|
+
likelyCollectionMatches,
|
|
746
|
+
msg: "AI chat context prepared",
|
|
747
|
+
selectedLocales,
|
|
748
|
+
writeIntent
|
|
749
|
+
});
|
|
263
750
|
const collectionSlugSchema = z.enum(collectionSlugs);
|
|
264
751
|
const getDisallowedCollectionActionError = (collection, action)=>{
|
|
265
752
|
if (isCollectionActionAllowed({
|
|
@@ -268,9 +755,11 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
268
755
|
req,
|
|
269
756
|
slug: collection
|
|
270
757
|
})) return null;
|
|
271
|
-
return {
|
|
272
|
-
|
|
273
|
-
|
|
758
|
+
return createToolError({
|
|
759
|
+
collection,
|
|
760
|
+
message: `${action} is not enabled for collection: ${collection}`,
|
|
761
|
+
tool: "collectionPermissionCheck"
|
|
762
|
+
});
|
|
274
763
|
};
|
|
275
764
|
const tools = {
|
|
276
765
|
getDoc: {
|
|
@@ -300,9 +789,13 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
300
789
|
execute: async ({ slug })=>{
|
|
301
790
|
if (slug) {
|
|
302
791
|
const collection = allowedCollections.find((item)=>item.slug === slug);
|
|
303
|
-
if (!collection)
|
|
304
|
-
|
|
305
|
-
|
|
792
|
+
if (!collection) {
|
|
793
|
+
return createToolError({
|
|
794
|
+
message: `Unknown collection: ${slug}`,
|
|
795
|
+
slug,
|
|
796
|
+
tool: "listCollections"
|
|
797
|
+
});
|
|
798
|
+
}
|
|
306
799
|
return describeCollectionLikeConfig({
|
|
307
800
|
config: collection,
|
|
308
801
|
permissions: options.collections,
|
|
@@ -323,9 +816,13 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
323
816
|
}),
|
|
324
817
|
execute: async ({ slug })=>{
|
|
325
818
|
const globalConfig = req.payload.config.globals?.find((global)=>global.slug === slug);
|
|
326
|
-
if (!globalConfig)
|
|
327
|
-
|
|
328
|
-
|
|
819
|
+
if (!globalConfig) {
|
|
820
|
+
return createToolError({
|
|
821
|
+
message: `Unknown global: ${slug}`,
|
|
822
|
+
slug,
|
|
823
|
+
tool: "getGlobal"
|
|
824
|
+
});
|
|
825
|
+
}
|
|
329
826
|
return req.payload.findGlobal({
|
|
330
827
|
depth: 2,
|
|
331
828
|
...activeLocale ? {
|
|
@@ -346,9 +843,13 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
346
843
|
const globals = req.payload.config.globals || [];
|
|
347
844
|
if (slug) {
|
|
348
845
|
const global = globals.find((item)=>item.slug === slug);
|
|
349
|
-
if (!global)
|
|
350
|
-
|
|
351
|
-
|
|
846
|
+
if (!global) {
|
|
847
|
+
return createToolError({
|
|
848
|
+
message: `Unknown global: ${slug}`,
|
|
849
|
+
slug,
|
|
850
|
+
tool: "listGlobals"
|
|
851
|
+
});
|
|
852
|
+
}
|
|
352
853
|
return describeCollectionLikeConfig({
|
|
353
854
|
config: global,
|
|
354
855
|
type: "global"
|
|
@@ -366,43 +867,108 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
366
867
|
collection: collectionSlugSchema,
|
|
367
868
|
data: z.record(z.string(), z.unknown()).optional(),
|
|
368
869
|
label: z.string().min(1),
|
|
369
|
-
localizedData:
|
|
870
|
+
localizedData: nonEmptyLocalizedDataSchema.optional()
|
|
370
871
|
}).refine((value)=>Boolean(value.data || value.localizedData), {
|
|
371
872
|
message: "Either data or localizedData is required."
|
|
372
873
|
}),
|
|
373
874
|
execute: async ({ collection, data, label, localizedData })=>{
|
|
374
875
|
const permissionError = getDisallowedCollectionActionError(collection, "create");
|
|
375
876
|
if (permissionError) return permissionError;
|
|
376
|
-
const
|
|
877
|
+
const collectionConfig = allowedCollections.find((item)=>item.slug === collection);
|
|
878
|
+
const collectionFields = collectionConfig?.fields || [];
|
|
879
|
+
const preparedData = prepareProposalWriteData({
|
|
880
|
+
collectionConfig: collectionConfig,
|
|
377
881
|
data,
|
|
882
|
+
inferenceText: prompt,
|
|
378
883
|
label,
|
|
379
884
|
localizedData,
|
|
380
|
-
|
|
381
|
-
});
|
|
382
|
-
const missingFields = getMissingCreateFields({
|
|
383
|
-
data: completedCreatePayload.data,
|
|
384
|
-
localizedData: completedCreatePayload.localizedData,
|
|
385
|
-
requiredFields: createRequiredFieldsByCollection[collection] || []
|
|
885
|
+
mode: "create"
|
|
386
886
|
});
|
|
387
|
-
if (
|
|
887
|
+
if (preparedData.issues.length > 0) {
|
|
388
888
|
const titleFieldName = titleFieldByCollection[collection];
|
|
389
|
-
const missingTitleField = titleFieldName ?
|
|
390
|
-
return {
|
|
391
|
-
|
|
392
|
-
|
|
889
|
+
const missingTitleField = titleFieldName ? preparedData.issues.some((issue)=>issue.path === titleFieldName || issue.path.endsWith(`.${titleFieldName}`)) : false;
|
|
890
|
+
return createToolError({
|
|
891
|
+
collection,
|
|
892
|
+
details: {
|
|
893
|
+
issues: preparedData.issues,
|
|
894
|
+
titleFieldName
|
|
895
|
+
},
|
|
896
|
+
message: missingTitleField ? `Create proposal is missing the required title field "${titleFieldName}" for ${collection}. Infer a concise title from the user request and retry.` : `Create proposal for ${collection} is invalid. Retry with exact schema fields and complete array/block objects: ${formatProposalIssuesForRetry(preparedData.issues)}`,
|
|
897
|
+
tool: "proposeCreateDoc"
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
const requestedBlockTypes = getRequestedBlockTypes({
|
|
901
|
+
availableBlockTypes: getCollectionBlockTypes(collectionFields),
|
|
902
|
+
mentions: body?.mentions,
|
|
903
|
+
prompt
|
|
904
|
+
});
|
|
905
|
+
if (requestedBlockTypes.length > 0) {
|
|
906
|
+
const proposalBlockTypes = new Set();
|
|
907
|
+
if (preparedData.data) {
|
|
908
|
+
for (const blockType of collectProposalBlockTypes({
|
|
909
|
+
data: preparedData.data,
|
|
910
|
+
fields: collectionFields
|
|
911
|
+
})){
|
|
912
|
+
proposalBlockTypes.add(blockType);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
if (preparedData.localizedData) {
|
|
916
|
+
for (const localeData of Object.values(preparedData.localizedData)){
|
|
917
|
+
for (const blockType of collectProposalBlockTypes({
|
|
918
|
+
data: localeData,
|
|
919
|
+
fields: collectionFields
|
|
920
|
+
})){
|
|
921
|
+
proposalBlockTypes.add(blockType);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
const missingBlockTypes = requestedBlockTypes.filter((blockType)=>!proposalBlockTypes.has(blockType));
|
|
926
|
+
if (missingBlockTypes.length > 0) {
|
|
927
|
+
return createToolError({
|
|
928
|
+
collection,
|
|
929
|
+
details: {
|
|
930
|
+
missingBlockTypes,
|
|
931
|
+
requestedBlockTypes
|
|
932
|
+
},
|
|
933
|
+
message: `Create proposal for ${collection} is missing required block types from the request: ${missingBlockTypes.join(", ")}. Add them to the appropriate blocks field using exact blockType values and complete required fields.`,
|
|
934
|
+
tool: "proposeCreateDoc"
|
|
935
|
+
});
|
|
936
|
+
}
|
|
393
937
|
}
|
|
394
|
-
const
|
|
938
|
+
const invalidRelationshipTargets = [
|
|
939
|
+
...preparedData.data ? await validateRelationshipTargetsExist({
|
|
940
|
+
data: preparedData.data,
|
|
941
|
+
fields: collectionFields,
|
|
942
|
+
req
|
|
943
|
+
}) : [],
|
|
944
|
+
...preparedData.localizedData ? (await Promise.all(Object.values(preparedData.localizedData).map((localeData)=>validateRelationshipTargetsExist({
|
|
945
|
+
data: localeData,
|
|
946
|
+
fields: collectionFields,
|
|
947
|
+
req
|
|
948
|
+
})))).flat() : []
|
|
949
|
+
];
|
|
950
|
+
if (invalidRelationshipTargets.length > 0) {
|
|
951
|
+
return createToolError({
|
|
952
|
+
collection,
|
|
953
|
+
details: {
|
|
954
|
+
invalidRelationshipTargets
|
|
955
|
+
},
|
|
956
|
+
message: `Create proposal for ${collection} contains relationship or upload references that do not exist: ${invalidRelationshipTargets.map((target)=>`${target.path} -> ${target.collection}:${target.id}`).join(", ")}.`,
|
|
957
|
+
tool: "proposeCreateDoc"
|
|
958
|
+
});
|
|
959
|
+
}
|
|
960
|
+
const proposal = preparedData.localizedData ? {
|
|
395
961
|
action: "create",
|
|
396
962
|
collection,
|
|
397
963
|
label: getSafeProposalLabel(label),
|
|
398
|
-
localizedData:
|
|
964
|
+
localizedData: preparedData.localizedData,
|
|
399
965
|
...activeLocale ? {
|
|
400
966
|
locale: activeLocale
|
|
401
967
|
} : {}
|
|
402
968
|
} : {
|
|
403
969
|
action: "create",
|
|
404
970
|
collection,
|
|
405
|
-
data:
|
|
971
|
+
data: preparedData.data || {},
|
|
406
972
|
label: getSafeProposalLabel(label),
|
|
407
973
|
...activeLocale ? {
|
|
408
974
|
locale: activeLocale
|
|
@@ -440,20 +1006,62 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
440
1006
|
data: z.record(z.string(), z.unknown()).optional(),
|
|
441
1007
|
id: z.string().min(1),
|
|
442
1008
|
label: z.string().min(1),
|
|
443
|
-
localizedData:
|
|
1009
|
+
localizedData: nonEmptyLocalizedDataSchema.optional()
|
|
444
1010
|
}).refine((value)=>Boolean(value.data || value.localizedData), {
|
|
445
1011
|
message: "Either data or localizedData is required."
|
|
446
1012
|
}),
|
|
447
1013
|
execute: async ({ collection, data, id, label, localizedData })=>{
|
|
448
1014
|
const permissionError = getDisallowedCollectionActionError(collection, "update");
|
|
449
1015
|
if (permissionError) return permissionError;
|
|
1016
|
+
const collectionConfig = allowedCollections.find((item)=>item.slug === collection);
|
|
1017
|
+
const collectionFields = collectionConfig?.fields || [];
|
|
1018
|
+
const preparedData = prepareProposalWriteData({
|
|
1019
|
+
collectionConfig: collectionConfig,
|
|
1020
|
+
data,
|
|
1021
|
+
inferenceText: prompt,
|
|
1022
|
+
label,
|
|
1023
|
+
localizedData,
|
|
1024
|
+
mode: "update"
|
|
1025
|
+
});
|
|
1026
|
+
if (preparedData.issues.length > 0) {
|
|
1027
|
+
return createToolError({
|
|
1028
|
+
collection,
|
|
1029
|
+
details: {
|
|
1030
|
+
issues: preparedData.issues
|
|
1031
|
+
},
|
|
1032
|
+
message: `Update proposal for ${collection} is invalid. Retry with exact schema fields and complete array/block objects: ${formatProposalIssuesForRetry(preparedData.issues)}`,
|
|
1033
|
+
tool: "proposeUpdateDoc"
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
const invalidRelationshipTargets = [
|
|
1037
|
+
...preparedData.data ? await validateRelationshipTargetsExist({
|
|
1038
|
+
data: preparedData.data,
|
|
1039
|
+
fields: collectionFields,
|
|
1040
|
+
req
|
|
1041
|
+
}) : [],
|
|
1042
|
+
...preparedData.localizedData ? (await Promise.all(Object.values(preparedData.localizedData).map((localeData)=>validateRelationshipTargetsExist({
|
|
1043
|
+
data: localeData,
|
|
1044
|
+
fields: collectionFields,
|
|
1045
|
+
req
|
|
1046
|
+
})))).flat() : []
|
|
1047
|
+
];
|
|
1048
|
+
if (invalidRelationshipTargets.length > 0) {
|
|
1049
|
+
return createToolError({
|
|
1050
|
+
collection,
|
|
1051
|
+
details: {
|
|
1052
|
+
invalidRelationshipTargets
|
|
1053
|
+
},
|
|
1054
|
+
message: `Update proposal for ${collection} contains relationship or upload references that do not exist: ${invalidRelationshipTargets.map((target)=>`${target.path} -> ${target.collection}:${target.id}`).join(", ")}.`,
|
|
1055
|
+
tool: "proposeUpdateDoc"
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
450
1058
|
const proposal = {
|
|
451
1059
|
action: "update",
|
|
452
1060
|
collection,
|
|
453
|
-
...localizedData ? {
|
|
454
|
-
localizedData
|
|
1061
|
+
...preparedData.localizedData ? {
|
|
1062
|
+
localizedData: preparedData.localizedData
|
|
455
1063
|
} : {
|
|
456
|
-
data: data || {}
|
|
1064
|
+
data: preparedData.data || {}
|
|
457
1065
|
},
|
|
458
1066
|
id,
|
|
459
1067
|
label: getSafeProposalLabel(label),
|
|
@@ -469,22 +1077,47 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
469
1077
|
inputSchema: z.object({
|
|
470
1078
|
data: z.record(z.string(), z.unknown()).optional(),
|
|
471
1079
|
label: z.string().min(1),
|
|
472
|
-
localizedData:
|
|
1080
|
+
localizedData: nonEmptyLocalizedDataSchema.optional(),
|
|
473
1081
|
slug: z.string().min(1)
|
|
474
1082
|
}).refine((value)=>Boolean(value.data || value.localizedData), {
|
|
475
1083
|
message: "Either data or localizedData is required."
|
|
476
1084
|
}),
|
|
477
1085
|
execute: async ({ data, label, localizedData, slug })=>{
|
|
478
1086
|
const globalConfig = req.payload.config.globals?.find((global)=>global.slug === slug);
|
|
479
|
-
if (!globalConfig)
|
|
480
|
-
|
|
481
|
-
|
|
1087
|
+
if (!globalConfig) {
|
|
1088
|
+
return createToolError({
|
|
1089
|
+
message: `Unknown global: ${slug}`,
|
|
1090
|
+
slug,
|
|
1091
|
+
tool: "proposeUpdateGlobal"
|
|
1092
|
+
});
|
|
1093
|
+
}
|
|
1094
|
+
const preparedData = prepareProposalWriteData({
|
|
1095
|
+
collectionConfig: {
|
|
1096
|
+
fields: globalConfig.fields || [],
|
|
1097
|
+
slug: globalConfig.slug
|
|
1098
|
+
},
|
|
1099
|
+
data,
|
|
1100
|
+
inferenceText: prompt,
|
|
1101
|
+
label,
|
|
1102
|
+
localizedData,
|
|
1103
|
+
mode: "update"
|
|
1104
|
+
});
|
|
1105
|
+
if (preparedData.issues.length > 0) {
|
|
1106
|
+
return createToolError({
|
|
1107
|
+
details: {
|
|
1108
|
+
issues: preparedData.issues
|
|
1109
|
+
},
|
|
1110
|
+
message: `Update proposal for global ${slug} is invalid. Retry with exact schema fields and complete array/block objects: ${formatProposalIssuesForRetry(preparedData.issues)}`,
|
|
1111
|
+
slug,
|
|
1112
|
+
tool: "proposeUpdateGlobal"
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
482
1115
|
const proposal = {
|
|
483
1116
|
action: "updateGlobal",
|
|
484
|
-
...localizedData ? {
|
|
485
|
-
localizedData
|
|
1117
|
+
...preparedData.localizedData ? {
|
|
1118
|
+
localizedData: preparedData.localizedData
|
|
486
1119
|
} : {
|
|
487
|
-
data: data || {}
|
|
1120
|
+
data: preparedData.data || {}
|
|
488
1121
|
},
|
|
489
1122
|
label: getSafeProposalLabel(label),
|
|
490
1123
|
...activeLocale ? {
|
|
@@ -551,12 +1184,22 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
551
1184
|
"You are a Payload CMS assistant. Inspect schema/content with tools before proposing writes.",
|
|
552
1185
|
"Mentions define the active CMS scope. Locale mentions define active locale; multiple locales require localizedData keyed by locale.",
|
|
553
1186
|
"Writes are proposals only. Never claim changes were applied before user confirmation.",
|
|
554
|
-
"For create/update/delete use proposal tools.
|
|
555
|
-
"If
|
|
1187
|
+
"For create/update/delete requests, you must use proposal tools. Do not end with plain text if the user asked for a content change.",
|
|
1188
|
+
"If the user asks to create, update, refine, translate, remove, or delete content, produce at least one proposal tool call unless blocked by missing schema information or permissions.",
|
|
1189
|
+
"Put concrete field values only in tool data, not visible text.",
|
|
1190
|
+
"For blocks fields: use exact blockType values from schema, exact field names, and complete objects for required block fields.",
|
|
1191
|
+
"For arrays: every item must be an object matching the child field schema, not free text.",
|
|
1192
|
+
"If schema details are missing, call listCollections/listGlobals with a slug before proposing. If an inferred collection schema is already present in context, use it directly.",
|
|
1193
|
+
`Collection aliases: ${JSON.stringify(collectionAliasMap)}.`,
|
|
1194
|
+
`Likely collection matches for this prompt: ${JSON.stringify(likelyCollectionMatches)}.`,
|
|
556
1195
|
`Focused required create fields: ${JSON.stringify(focusedRequiredFieldsByCollection)}.`,
|
|
557
1196
|
`Focused title fields: ${JSON.stringify(focusedTitleFieldByCollection)}. Infer concise titles when needed.`,
|
|
1197
|
+
`Preferred proposal tool for this prompt: ${intentToolChoice?.toolName || "none"}.`,
|
|
558
1198
|
"Visible response: plain text, under 40 words, no Markdown, no proposed content."
|
|
559
1199
|
].join("\n"),
|
|
1200
|
+
...intentToolChoice ? {
|
|
1201
|
+
toolChoice: intentToolChoice
|
|
1202
|
+
} : {},
|
|
560
1203
|
tools
|
|
561
1204
|
});
|
|
562
1205
|
const stream = new ReadableStream({
|
|
@@ -581,19 +1224,75 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
581
1224
|
if (part.type === "finish") {
|
|
582
1225
|
const finishPart = part;
|
|
583
1226
|
usage = finishPart.totalUsage || finishPart.usage || null;
|
|
1227
|
+
const reason = getChatCompletionReason({
|
|
1228
|
+
proposalCount: proposals.length,
|
|
1229
|
+
toolFailures,
|
|
1230
|
+
writeIntent
|
|
1231
|
+
});
|
|
1232
|
+
const debugPayload = createDebugPayload({
|
|
1233
|
+
activeLocale,
|
|
1234
|
+
debug,
|
|
1235
|
+
proposalCount: proposals.length,
|
|
1236
|
+
selectedLocales,
|
|
1237
|
+
toolFailures,
|
|
1238
|
+
usage,
|
|
1239
|
+
writeIntent
|
|
1240
|
+
});
|
|
1241
|
+
logHandlerEvent(req, proposals.length > 0 ? "info" : "warn", {
|
|
1242
|
+
activeLocale,
|
|
1243
|
+
debug,
|
|
1244
|
+
msg: proposals.length > 0 ? "AI chat completed with proposals" : "AI chat completed without proposals",
|
|
1245
|
+
proposalCount: proposals.length,
|
|
1246
|
+
proposals: proposals.map((proposal)=>getProposalSummary(proposal)),
|
|
1247
|
+
promptPreview: getLogPreview(prompt),
|
|
1248
|
+
reason,
|
|
1249
|
+
selectedLocales,
|
|
1250
|
+
toolFailureCount: toolFailures.length,
|
|
1251
|
+
toolFailures,
|
|
1252
|
+
usage
|
|
1253
|
+
});
|
|
584
1254
|
sendEvent(controller, "proposals", {
|
|
585
1255
|
proposals,
|
|
586
1256
|
usage
|
|
587
1257
|
});
|
|
1258
|
+
sendEvent(controller, "debug", debugPayload);
|
|
588
1259
|
sendEvent(controller, "done", {});
|
|
589
1260
|
didSendTerminalEvent = true;
|
|
590
1261
|
}
|
|
591
1262
|
}
|
|
592
1263
|
if (!didSendTerminalEvent) {
|
|
1264
|
+
const reason = getChatCompletionReason({
|
|
1265
|
+
proposalCount: proposals.length,
|
|
1266
|
+
toolFailures,
|
|
1267
|
+
writeIntent
|
|
1268
|
+
});
|
|
1269
|
+
const debugPayload = createDebugPayload({
|
|
1270
|
+
activeLocale,
|
|
1271
|
+
debug,
|
|
1272
|
+
proposalCount: proposals.length,
|
|
1273
|
+
selectedLocales,
|
|
1274
|
+
toolFailures,
|
|
1275
|
+
usage,
|
|
1276
|
+
writeIntent
|
|
1277
|
+
});
|
|
1278
|
+
logHandlerEvent(req, proposals.length > 0 ? "info" : "warn", {
|
|
1279
|
+
activeLocale,
|
|
1280
|
+
debug,
|
|
1281
|
+
msg: proposals.length > 0 ? "AI chat completed with proposals" : "AI chat completed without proposals",
|
|
1282
|
+
proposalCount: proposals.length,
|
|
1283
|
+
proposals: proposals.map((proposal)=>getProposalSummary(proposal)),
|
|
1284
|
+
promptPreview: getLogPreview(prompt),
|
|
1285
|
+
reason,
|
|
1286
|
+
selectedLocales,
|
|
1287
|
+
toolFailureCount: toolFailures.length,
|
|
1288
|
+
toolFailures,
|
|
1289
|
+
usage
|
|
1290
|
+
});
|
|
593
1291
|
sendEvent(controller, "proposals", {
|
|
594
1292
|
proposals,
|
|
595
1293
|
usage
|
|
596
1294
|
});
|
|
1295
|
+
sendEvent(controller, "debug", debugPayload);
|
|
597
1296
|
sendEvent(controller, "done", {});
|
|
598
1297
|
}
|
|
599
1298
|
} catch (err) {
|