@mvriu5/payload-ai 1.1.1 → 1.3.2
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/README.md +42 -0
- package/dist/components/Icons.d.ts +0 -8
- package/dist/components/Icons.js +4 -216
- package/dist/components/{ActionToast.d.ts → action-toast/ActionToast.d.ts} +2 -2
- package/dist/components/{ActionToast.js → action-toast/ActionToast.js} +27 -35
- package/dist/components/{ActionToast.module.css → action-toast/ActionToast.module.css} +0 -79
- package/dist/components/ai-input/AIInput.js +433 -0
- package/dist/components/{AIInput.module.css → ai-input/AIInput.module.css} +119 -42
- package/dist/components/ai-input/badge.d.ts +14 -0
- package/dist/components/ai-input/badge.js +85 -0
- package/dist/components/{AuditLogList.d.ts → audit-log-list/AuditLogList.d.ts} +4 -8
- package/dist/components/{AuditLogList.js → audit-log-list/AuditLogList.js} +49 -21
- package/dist/components/{AuditLogList.module.css → audit-log-list/AuditLogList.module.css} +11 -65
- package/dist/components/dashboard/Dashboard.d.ts +2 -0
- package/dist/components/dashboard/Dashboard.js +15 -0
- package/dist/components/dashboard/Dashboard.module.css +13 -0
- package/dist/components/{DiffDialog.d.ts → diff-dialog/DiffDialog.d.ts} +2 -2
- package/dist/components/{DiffDialog.js → diff-dialog/DiffDialog.js} +200 -189
- package/dist/components/{DiffDialog.module.css → diff-dialog/DiffDialog.module.css} +17 -35
- package/dist/components/hooks/useAIChatStream.d.ts +39 -0
- package/dist/components/hooks/useAIChatStream.js +217 -0
- package/dist/components/hooks/useAISettings.js +26 -25
- package/dist/components/hooks/useAuditLog.d.ts +11 -0
- package/dist/components/hooks/useAuditLog.js +41 -0
- package/dist/components/hooks/useDocumentMentionSuggestions.d.ts +1 -1
- package/dist/components/hooks/useDocumentMentionSuggestions.js +31 -27
- package/dist/components/hooks/useMentions.d.ts +58 -0
- package/dist/components/hooks/useMentions.js +276 -0
- package/dist/components/hooks/usePluginConfig.d.ts +52 -0
- package/dist/components/hooks/usePluginConfig.js +20 -0
- package/dist/components/{MentionPopover.d.ts → mention-popover/MentionPopover.d.ts} +2 -4
- package/dist/components/{MentionPopover.js → mention-popover/MentionPopover.js} +1 -8
- package/dist/components/{MentionPopover.module.css → mention-popover/MentionPopover.module.css} +1 -1
- package/dist/exports/client.d.ts +2 -2
- package/dist/exports/client.js +2 -2
- package/dist/handlers/applyActionHandler.js +341 -112
- package/dist/handlers/chatHandler.js +940 -84
- package/dist/handlers/mediaUploadHandler.d.ts +7 -0
- package/dist/handlers/mediaUploadHandler.js +99 -0
- package/dist/handlers/mentionSuggestionHandler.js +16 -14
- package/dist/handlers/proposalDiffHandler.js +117 -50
- package/dist/index.d.ts +6 -0
- package/dist/index.js +39 -7
- package/dist/payload/collectionPermissions.js +3 -1
- package/dist/payload/logging.d.ts +9 -0
- package/dist/payload/logging.js +14 -0
- package/dist/payload/normalizeData.d.ts +16 -12
- package/dist/payload/normalizeData.js +47 -14
- package/dist/payload/proposalData.d.ts +31 -0
- package/dist/payload/proposalData.js +578 -0
- package/dist/payload/schemaContext.d.ts +3 -7
- package/dist/payload/schemaContext.js +147 -66
- package/package.json +6 -6
- package/dist/components/AIInput.js +0 -747
- /package/dist/components/{AIInput.d.ts → ai-input/AIInput.d.ts} +0 -0
|
@@ -5,8 +5,13 @@ 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
|
+
});
|
|
10
15
|
const e2eModeEnabled = ()=>process.env.PAYLOAD_AI_E2E_MODE === "true";
|
|
11
16
|
const createSSEEventStream = (events)=>{
|
|
12
17
|
const encoder = new TextEncoder();
|
|
@@ -19,9 +24,29 @@ const createSSEEventStream = (events)=>{
|
|
|
19
24
|
}
|
|
20
25
|
});
|
|
21
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
|
+
});
|
|
22
47
|
const createE2EChatResponse = ({ prompt, selectedLocales })=>{
|
|
23
48
|
const normalizedPrompt = prompt.toLowerCase();
|
|
24
|
-
const wantsCreatePost = normalizedPrompt.includes("post") && (normalizedPrompt.includes("create") || normalizedPrompt.includes("erstell"));
|
|
49
|
+
const wantsCreatePost = normalizedPrompt.includes("post") && (normalizedPrompt.includes("create") || normalizedPrompt.includes("erstell") || normalizedPrompt.includes("apply flow") || normalizedPrompt.includes("locale review") || normalizedPrompt.includes("proposal review"));
|
|
25
50
|
const mentionsMars = normalizedPrompt.includes("mars");
|
|
26
51
|
const multipleLocales = selectedLocales.length > 1;
|
|
27
52
|
const activeLocale = selectedLocales.at(-1);
|
|
@@ -56,6 +81,22 @@ const createE2EChatResponse = ({ prompt, selectedLocales })=>{
|
|
|
56
81
|
} : {}
|
|
57
82
|
}) : null;
|
|
58
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
|
+
});
|
|
59
100
|
return new Response(createSSEEventStream([
|
|
60
101
|
{
|
|
61
102
|
data: {
|
|
@@ -76,6 +117,10 @@ const createE2EChatResponse = ({ prompt, selectedLocales })=>{
|
|
|
76
117
|
},
|
|
77
118
|
event: "proposals"
|
|
78
119
|
},
|
|
120
|
+
{
|
|
121
|
+
data: debugPayload,
|
|
122
|
+
event: "debug"
|
|
123
|
+
},
|
|
79
124
|
{
|
|
80
125
|
data: {},
|
|
81
126
|
event: "done"
|
|
@@ -196,14 +241,23 @@ const getMissingCreateFields = ({ data, localizedData, requiredFields })=>{
|
|
|
196
241
|
];
|
|
197
242
|
}
|
|
198
243
|
const missing = [];
|
|
244
|
+
const localizedRequiredFields = [];
|
|
245
|
+
const sharedRequiredFields = [];
|
|
246
|
+
for (const field of requiredFields){
|
|
247
|
+
if (field.localized) {
|
|
248
|
+
localizedRequiredFields.push(field);
|
|
249
|
+
} else {
|
|
250
|
+
sharedRequiredFields.push(field);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
199
253
|
for (const [locale, localeData] of locales){
|
|
200
|
-
for (const field of
|
|
254
|
+
for (const field of localizedRequiredFields){
|
|
201
255
|
if (!hasValueAtPath(localeData, field.path)) {
|
|
202
256
|
missing.push(`${locale}:${field.path}`);
|
|
203
257
|
}
|
|
204
258
|
}
|
|
205
259
|
}
|
|
206
|
-
for (const field of
|
|
260
|
+
for (const field of sharedRequiredFields){
|
|
207
261
|
if (!hasValueAtPath(firstLocale[1], field.path)) {
|
|
208
262
|
missing.push(`${firstLocale[0]}:${field.path}`);
|
|
209
263
|
}
|
|
@@ -213,7 +267,428 @@ const getMissingCreateFields = ({ data, localizedData, requiredFields })=>{
|
|
|
213
267
|
if (!data) return [
|
|
214
268
|
"data is required"
|
|
215
269
|
];
|
|
216
|
-
return requiredFields.
|
|
270
|
+
return requiredFields.flatMap((field)=>hasValueAtPath(data, field.path) ? [] : [
|
|
271
|
+
field.path
|
|
272
|
+
]);
|
|
273
|
+
};
|
|
274
|
+
const getProposalSummary = (proposal)=>({
|
|
275
|
+
action: proposal.action,
|
|
276
|
+
collection: "collection" in proposal ? proposal.collection : undefined,
|
|
277
|
+
hasData: "data" in proposal && Boolean(proposal.data),
|
|
278
|
+
hasLocalizedData: "localizedData" in proposal && Boolean(proposal.localizedData),
|
|
279
|
+
id: "id" in proposal ? proposal.id : undefined,
|
|
280
|
+
label: proposal.label,
|
|
281
|
+
locale: proposal.locale,
|
|
282
|
+
locales: "localizedData" in proposal && proposal.localizedData ? Object.keys(proposal.localizedData) : undefined,
|
|
283
|
+
slug: "slug" in proposal ? proposal.slug : undefined
|
|
284
|
+
});
|
|
285
|
+
const getCollectionBlockTypes = (fields)=>{
|
|
286
|
+
const blockTypes = new Set();
|
|
287
|
+
const visitFields = (items)=>{
|
|
288
|
+
for (const field of items){
|
|
289
|
+
if (field.type === "blocks" && field.blocks) {
|
|
290
|
+
for (const block of field.blocks){
|
|
291
|
+
blockTypes.add(block.slug);
|
|
292
|
+
if (block.fields?.length) {
|
|
293
|
+
visitFields(block.fields);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
if (field.fields?.length) {
|
|
298
|
+
visitFields(field.fields);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
visitFields(fields);
|
|
303
|
+
return [
|
|
304
|
+
...blockTypes
|
|
305
|
+
];
|
|
306
|
+
};
|
|
307
|
+
const regexSpecialCharactersPattern = /[.*+?^${}()|[\]\\]/g;
|
|
308
|
+
const getRequestedBlockTypes = ({ availableBlockTypes, mentions, prompt })=>{
|
|
309
|
+
const requestedBlockTypes = new Set();
|
|
310
|
+
const normalizedPrompt = prompt.toLowerCase();
|
|
311
|
+
const availableBlockTypeSet = new Set(availableBlockTypes);
|
|
312
|
+
const blockTypesByNormalizedSlug = new Map(availableBlockTypes.map((blockType)=>[
|
|
313
|
+
blockType.toLowerCase(),
|
|
314
|
+
blockType
|
|
315
|
+
]));
|
|
316
|
+
const escapedBlockTypes = availableBlockTypes.map((blockType)=>blockType.replace(regexSpecialCharactersPattern, "\\$&")).join("|");
|
|
317
|
+
const blockPattern = escapedBlockTypes ? new RegExp(`\\b(${escapedBlockTypes})\\b(?:\\s+block)?`, "gi") : null;
|
|
318
|
+
for (const mention of mentions || []){
|
|
319
|
+
if (mention.type === "block" && mention.slug && availableBlockTypeSet.has(mention.slug)) {
|
|
320
|
+
requestedBlockTypes.add(mention.slug);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
if (blockPattern) {
|
|
324
|
+
for (const match of normalizedPrompt.matchAll(blockPattern)){
|
|
325
|
+
const blockType = match[1] ? blockTypesByNormalizedSlug.get(match[1].toLowerCase()) : null;
|
|
326
|
+
if (blockType) {
|
|
327
|
+
requestedBlockTypes.add(blockType);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return [
|
|
332
|
+
...requestedBlockTypes
|
|
333
|
+
];
|
|
334
|
+
};
|
|
335
|
+
const collectProposalBlockTypes = ({ data, fields })=>{
|
|
336
|
+
const foundBlockTypes = new Set();
|
|
337
|
+
const visitFields = (items, value)=>{
|
|
338
|
+
for (const field of items){
|
|
339
|
+
if (!field.name) continue;
|
|
340
|
+
const fieldValue = value[field.name];
|
|
341
|
+
if (fieldValue === undefined || fieldValue === null) continue;
|
|
342
|
+
if (field.type === "blocks" && Array.isArray(fieldValue)) {
|
|
343
|
+
const blocksBySlug = new Map(field.blocks?.map((block)=>[
|
|
344
|
+
block.slug,
|
|
345
|
+
block
|
|
346
|
+
]));
|
|
347
|
+
for (const blockItem of fieldValue){
|
|
348
|
+
if (!isRecord(blockItem)) continue;
|
|
349
|
+
const blockType = typeof blockItem.blockType === "string" ? blockItem.blockType : typeof blockItem.type === "string" ? blockItem.type : typeof blockItem.slug === "string" ? blockItem.slug : null;
|
|
350
|
+
if (blockType) {
|
|
351
|
+
foundBlockTypes.add(blockType);
|
|
352
|
+
}
|
|
353
|
+
if (blockType) {
|
|
354
|
+
const blockConfig = blocksBySlug.get(blockType);
|
|
355
|
+
if (blockConfig?.fields?.length) {
|
|
356
|
+
visitFields(blockConfig.fields, blockItem);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
if (field.type === "group" && isRecord(fieldValue) && field.fields?.length) {
|
|
363
|
+
visitFields(field.fields, fieldValue);
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
if (field.type === "array" && Array.isArray(fieldValue) && field.fields?.length) {
|
|
367
|
+
for (const item of fieldValue){
|
|
368
|
+
if (isRecord(item)) {
|
|
369
|
+
visitFields(field.fields, item);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
visitFields(fields, data);
|
|
376
|
+
return foundBlockTypes;
|
|
377
|
+
};
|
|
378
|
+
const collectRelationshipTargets = ({ data, fields, path = "" })=>{
|
|
379
|
+
const targets = [];
|
|
380
|
+
for (const field of fields){
|
|
381
|
+
if (!field.name) continue;
|
|
382
|
+
const fieldPath = path ? `${path}.${field.name}` : field.name;
|
|
383
|
+
const fieldValue = data[field.name];
|
|
384
|
+
if (fieldValue === undefined || fieldValue === null) continue;
|
|
385
|
+
if (field.type === "relationship" || field.type === "upload") {
|
|
386
|
+
const relationTargets = Array.isArray(field.relationTo) ? field.relationTo.filter((item)=>typeof item === "string") : typeof field.relationTo === "string" ? [
|
|
387
|
+
field.relationTo
|
|
388
|
+
] : [];
|
|
389
|
+
const collectSingle = (value, itemPath)=>{
|
|
390
|
+
if (typeof value === "string" || typeof value === "number") {
|
|
391
|
+
if (relationTargets.length === 1) {
|
|
392
|
+
targets.push({
|
|
393
|
+
collection: relationTargets[0],
|
|
394
|
+
id: value,
|
|
395
|
+
path: itemPath
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
if (!isRecord(value)) return;
|
|
401
|
+
const relationTo = typeof value.relationTo === "string" ? value.relationTo : relationTargets[0];
|
|
402
|
+
const id = typeof value.id === "string" || typeof value.id === "number" ? value.id : typeof value.value === "string" || typeof value.value === "number" ? value.value : undefined;
|
|
403
|
+
if (!relationTo || id === undefined) return;
|
|
404
|
+
targets.push({
|
|
405
|
+
collection: relationTo,
|
|
406
|
+
id,
|
|
407
|
+
path: itemPath
|
|
408
|
+
});
|
|
409
|
+
};
|
|
410
|
+
if (field.hasMany && Array.isArray(fieldValue)) {
|
|
411
|
+
fieldValue.forEach((item, index)=>collectSingle(item, `${fieldPath}.${index}`));
|
|
412
|
+
} else {
|
|
413
|
+
collectSingle(fieldValue, fieldPath);
|
|
414
|
+
}
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
if (field.type === "group" && isRecord(fieldValue) && field.fields?.length) {
|
|
418
|
+
targets.push(...collectRelationshipTargets({
|
|
419
|
+
data: fieldValue,
|
|
420
|
+
fields: field.fields,
|
|
421
|
+
path: fieldPath
|
|
422
|
+
}));
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
if (field.type === "array" && Array.isArray(fieldValue) && field.fields?.length) {
|
|
426
|
+
fieldValue.forEach((item, index)=>{
|
|
427
|
+
if (!isRecord(item)) return;
|
|
428
|
+
targets.push(...collectRelationshipTargets({
|
|
429
|
+
data: item,
|
|
430
|
+
fields: field.fields,
|
|
431
|
+
path: `${fieldPath}.${index}`
|
|
432
|
+
}));
|
|
433
|
+
});
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
if (field.type === "blocks" && Array.isArray(fieldValue) && field.blocks?.length) {
|
|
437
|
+
const blocksBySlug = new Map(field.blocks.map((block)=>[
|
|
438
|
+
block.slug,
|
|
439
|
+
block
|
|
440
|
+
]));
|
|
441
|
+
fieldValue.forEach((item, index)=>{
|
|
442
|
+
if (!isRecord(item)) return;
|
|
443
|
+
const blockType = typeof item.blockType === "string" ? item.blockType : typeof item.type === "string" ? item.type : typeof item.slug === "string" ? item.slug : null;
|
|
444
|
+
if (!blockType) return;
|
|
445
|
+
const blockConfig = blocksBySlug.get(blockType);
|
|
446
|
+
if (!blockConfig?.fields?.length) return;
|
|
447
|
+
targets.push(...collectRelationshipTargets({
|
|
448
|
+
data: item,
|
|
449
|
+
fields: blockConfig.fields,
|
|
450
|
+
path: `${fieldPath}.${index}`
|
|
451
|
+
}));
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
return targets;
|
|
456
|
+
};
|
|
457
|
+
const collectUploadTargets = ({ data, fields, path = "" })=>{
|
|
458
|
+
const targets = [];
|
|
459
|
+
for (const field of fields){
|
|
460
|
+
if (!field.name) continue;
|
|
461
|
+
const fieldPath = path ? `${path}.${field.name}` : field.name;
|
|
462
|
+
const fieldValue = data[field.name];
|
|
463
|
+
if (fieldValue === undefined || fieldValue === null) continue;
|
|
464
|
+
if (field.type === "upload") {
|
|
465
|
+
const relationTargets = Array.isArray(field.relationTo) ? field.relationTo.filter((item)=>typeof item === "string") : typeof field.relationTo === "string" ? [
|
|
466
|
+
field.relationTo
|
|
467
|
+
] : [];
|
|
468
|
+
const collectSingle = (value, itemPath)=>{
|
|
469
|
+
if (typeof value === "string" || typeof value === "number") {
|
|
470
|
+
if (relationTargets.length === 1) {
|
|
471
|
+
targets.push({
|
|
472
|
+
collection: relationTargets[0],
|
|
473
|
+
id: value,
|
|
474
|
+
path: itemPath
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
if (!isRecord(value)) return;
|
|
480
|
+
const relationTo = typeof value.relationTo === "string" ? value.relationTo : relationTargets[0];
|
|
481
|
+
const id = typeof value.id === "string" || typeof value.id === "number" ? value.id : typeof value.value === "string" || typeof value.value === "number" ? value.value : undefined;
|
|
482
|
+
if (!relationTo || id === undefined) return;
|
|
483
|
+
targets.push({
|
|
484
|
+
collection: relationTo,
|
|
485
|
+
id,
|
|
486
|
+
path: itemPath
|
|
487
|
+
});
|
|
488
|
+
};
|
|
489
|
+
if (field.hasMany && Array.isArray(fieldValue)) {
|
|
490
|
+
fieldValue.forEach((item, index)=>collectSingle(item, `${fieldPath}.${index}`));
|
|
491
|
+
} else {
|
|
492
|
+
collectSingle(fieldValue, fieldPath);
|
|
493
|
+
}
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
if (field.type === "group" && isRecord(fieldValue) && field.fields?.length) {
|
|
497
|
+
targets.push(...collectUploadTargets({
|
|
498
|
+
data: fieldValue,
|
|
499
|
+
fields: field.fields,
|
|
500
|
+
path: fieldPath
|
|
501
|
+
}));
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
if (field.type === "array" && Array.isArray(fieldValue) && field.fields?.length) {
|
|
505
|
+
fieldValue.forEach((item, index)=>{
|
|
506
|
+
if (!isRecord(item)) return;
|
|
507
|
+
targets.push(...collectUploadTargets({
|
|
508
|
+
data: item,
|
|
509
|
+
fields: field.fields,
|
|
510
|
+
path: `${fieldPath}.${index}`
|
|
511
|
+
}));
|
|
512
|
+
});
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
515
|
+
if (field.type === "blocks" && Array.isArray(fieldValue) && field.blocks?.length) {
|
|
516
|
+
const blocksBySlug = new Map(field.blocks.map((block)=>[
|
|
517
|
+
block.slug,
|
|
518
|
+
block
|
|
519
|
+
]));
|
|
520
|
+
fieldValue.forEach((item, index)=>{
|
|
521
|
+
if (!isRecord(item)) return;
|
|
522
|
+
const blockType = typeof item.blockType === "string" ? item.blockType : typeof item.type === "string" ? item.type : typeof item.slug === "string" ? item.slug : null;
|
|
523
|
+
if (!blockType) return;
|
|
524
|
+
const blockConfig = blocksBySlug.get(blockType);
|
|
525
|
+
if (!blockConfig?.fields?.length) return;
|
|
526
|
+
targets.push(...collectUploadTargets({
|
|
527
|
+
data: item,
|
|
528
|
+
fields: blockConfig.fields,
|
|
529
|
+
path: `${fieldPath}.${index}`
|
|
530
|
+
}));
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
return targets;
|
|
535
|
+
};
|
|
536
|
+
const validateRelationshipTargetsExist = async ({ data, fields, req })=>{
|
|
537
|
+
const targets = collectRelationshipTargets({
|
|
538
|
+
data,
|
|
539
|
+
fields
|
|
540
|
+
});
|
|
541
|
+
const targetResults = await Promise.all(targets.map(async (target)=>{
|
|
542
|
+
try {
|
|
543
|
+
await req.payload.findByID({
|
|
544
|
+
collection: target.collection,
|
|
545
|
+
depth: 0,
|
|
546
|
+
id: String(target.id),
|
|
547
|
+
overrideAccess: false,
|
|
548
|
+
req
|
|
549
|
+
});
|
|
550
|
+
return null;
|
|
551
|
+
} catch {
|
|
552
|
+
return target;
|
|
553
|
+
}
|
|
554
|
+
}));
|
|
555
|
+
return targetResults.filter((target)=>Boolean(target));
|
|
556
|
+
};
|
|
557
|
+
const getUploadTargetsOutsideAttachments = ({ allowedAttachmentKeys, data, fields })=>{
|
|
558
|
+
if (allowedAttachmentKeys.size === 0) return [];
|
|
559
|
+
return collectUploadTargets({
|
|
560
|
+
data,
|
|
561
|
+
fields
|
|
562
|
+
}).filter((target)=>!allowedAttachmentKeys.has(`${target.collection}:${String(target.id)}`));
|
|
563
|
+
};
|
|
564
|
+
const getMentionSummary = (mentions)=>mentions?.map((mention)=>({
|
|
565
|
+
collection: "collection" in mention ? mention.collection : undefined,
|
|
566
|
+
id: "id" in mention ? mention.id : undefined,
|
|
567
|
+
slug: mention.slug,
|
|
568
|
+
type: mention.type
|
|
569
|
+
})) || [];
|
|
570
|
+
const getMediaAttachmentContext = async ({ allowedCollectionsBySlug, attachments, collections, req })=>{
|
|
571
|
+
if (!attachments?.length) return [];
|
|
572
|
+
const contexts = [];
|
|
573
|
+
const seen = new Set();
|
|
574
|
+
for (const attachment of attachments.slice(0, 8)){
|
|
575
|
+
if (attachment.type !== "media" || !attachment.collection || !attachment.id) continue;
|
|
576
|
+
const key = `${attachment.collection}:${attachment.id}`;
|
|
577
|
+
if (seen.has(key)) continue;
|
|
578
|
+
const collectionConfig = allowedCollectionsBySlug.get(attachment.collection);
|
|
579
|
+
if (!collectionConfig?.upload) continue;
|
|
580
|
+
const doc = await req.payload.findByID({
|
|
581
|
+
collection: attachment.collection,
|
|
582
|
+
depth: 1,
|
|
583
|
+
id: attachment.id,
|
|
584
|
+
overrideAccess: false,
|
|
585
|
+
req
|
|
586
|
+
}).catch(()=>null);
|
|
587
|
+
if (!doc) continue;
|
|
588
|
+
seen.add(key);
|
|
589
|
+
contexts.push({
|
|
590
|
+
attachment,
|
|
591
|
+
collection: attachment.collection,
|
|
592
|
+
doc,
|
|
593
|
+
schema: describeCollectionLikeConfig({
|
|
594
|
+
config: collectionConfig,
|
|
595
|
+
permissions: collections,
|
|
596
|
+
type: "collection"
|
|
597
|
+
}),
|
|
598
|
+
type: "mediaAttachment"
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
return contexts;
|
|
602
|
+
};
|
|
603
|
+
const formatProposalIssuesForRetry = (issues)=>{
|
|
604
|
+
return issues.slice(0, 6).map((issue)=>{
|
|
605
|
+
switch(issue.code){
|
|
606
|
+
case "invalid_block_type":
|
|
607
|
+
return `${issue.path}: use an exact blockType from the schema`;
|
|
608
|
+
case "invalid_blocks":
|
|
609
|
+
return `${issue.path}: blocks fields must be arrays of objects with blockType and exact field names`;
|
|
610
|
+
case "invalid_array":
|
|
611
|
+
return `${issue.path}: array fields must be arrays of complete objects`;
|
|
612
|
+
case "missing_required_field":
|
|
613
|
+
return `${issue.path}: required field missing`;
|
|
614
|
+
case "unknown_field":
|
|
615
|
+
return `${issue.path}: unknown field, use exact schema field names only`;
|
|
616
|
+
case "invalid_relationship":
|
|
617
|
+
return `${issue.path}: use relationship IDs or { relationTo, value }, not free text`;
|
|
618
|
+
default:
|
|
619
|
+
return `${issue.path}: ${issue.message}`;
|
|
620
|
+
}
|
|
621
|
+
}).join("; ");
|
|
622
|
+
};
|
|
623
|
+
const createCollectionAliasMap = (collections)=>{
|
|
624
|
+
const aliasMap = new Map();
|
|
625
|
+
const addAlias = (alias, slug)=>{
|
|
626
|
+
const normalizedAlias = alias?.trim().toLowerCase();
|
|
627
|
+
if (!normalizedAlias) return;
|
|
628
|
+
if (!aliasMap.has(normalizedAlias)) {
|
|
629
|
+
aliasMap.set(normalizedAlias, slug);
|
|
630
|
+
}
|
|
631
|
+
};
|
|
632
|
+
for (const collection of collections){
|
|
633
|
+
addAlias(collection.slug, collection.slug);
|
|
634
|
+
addAlias(collection.slug.replace(/-/g, " "), collection.slug);
|
|
635
|
+
const singular = typeof collection.labels?.singular === "string" ? collection.labels.singular : undefined;
|
|
636
|
+
const plural = typeof collection.labels?.plural === "string" ? collection.labels.plural : undefined;
|
|
637
|
+
addAlias(singular, collection.slug);
|
|
638
|
+
addAlias(plural, collection.slug);
|
|
639
|
+
if (singular?.endsWith("s")) {
|
|
640
|
+
addAlias(singular.slice(0, -1), collection.slug);
|
|
641
|
+
}
|
|
642
|
+
if (plural?.endsWith("s")) {
|
|
643
|
+
addAlias(plural.slice(0, -1), collection.slug);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
return Object.fromEntries(aliasMap.entries());
|
|
647
|
+
};
|
|
648
|
+
const getLikelyCollectionMatches = ({ aliasMap, prompt })=>{
|
|
649
|
+
const normalizedPrompt = prompt.toLowerCase();
|
|
650
|
+
const matches = new Set();
|
|
651
|
+
const aliases = Object.keys(aliasMap).sort((a, b)=>b.length - a.length);
|
|
652
|
+
const aliasPattern = aliases.length > 0 ? new RegExp(aliases.map((alias)=>alias.replace(regexSpecialCharactersPattern, "\\$&")).join("|"), "g") : null;
|
|
653
|
+
if (!aliasPattern) return [];
|
|
654
|
+
for (const match of normalizedPrompt.matchAll(aliasPattern)){
|
|
655
|
+
const alias = match[0];
|
|
656
|
+
const slug = aliasMap[alias];
|
|
657
|
+
if (slug) {
|
|
658
|
+
matches.add(slug);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
return [
|
|
662
|
+
...matches
|
|
663
|
+
];
|
|
664
|
+
};
|
|
665
|
+
const hasWriteIntent = (prompt)=>{
|
|
666
|
+
const normalizedPrompt = prompt.toLowerCase();
|
|
667
|
+
return /\b(create|build|generate|write|draft|make|add|update|edit|change|revise|refine|rewrite|translate|delete|remove)\b/.test(normalizedPrompt);
|
|
668
|
+
};
|
|
669
|
+
const getIntentToolChoice = (prompt)=>{
|
|
670
|
+
const normalizedPrompt = prompt.toLowerCase();
|
|
671
|
+
if (/\b(delete|remove)\b/.test(normalizedPrompt)) {
|
|
672
|
+
return {
|
|
673
|
+
toolName: "proposeDeleteDoc",
|
|
674
|
+
type: "tool"
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
// Update existing document (add block, modify content, etc.)
|
|
678
|
+
if (/\b(update|edit|change|modify|einfügen|hinzufügen|addieren)\b/.test(normalizedPrompt)) {
|
|
679
|
+
return {
|
|
680
|
+
toolName: "proposeUpdateDoc",
|
|
681
|
+
type: "tool"
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
// Create a new document
|
|
685
|
+
if (/\b(create|build|generate|write|draft|make|add|füge|einfügen|hinzufügen|addieren)\b/.test(normalizedPrompt)) {
|
|
686
|
+
return {
|
|
687
|
+
toolName: "proposeCreateDoc",
|
|
688
|
+
type: "tool"
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
return undefined;
|
|
217
692
|
};
|
|
218
693
|
export const createChatHandler = (options = {})=>async (req)=>{
|
|
219
694
|
if (!req.user) return Response.json({
|
|
@@ -228,8 +703,15 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
228
703
|
}, {
|
|
229
704
|
status: 400
|
|
230
705
|
});
|
|
231
|
-
const selectedLocales =
|
|
706
|
+
const selectedLocales = [];
|
|
707
|
+
const selectedLocaleSet = new Set();
|
|
708
|
+
for (const mention of body?.mentions || []){
|
|
709
|
+
if (mention.type !== "locale" || !mention.slug || selectedLocaleSet.has(mention.slug)) continue;
|
|
710
|
+
selectedLocaleSet.add(mention.slug);
|
|
711
|
+
selectedLocales.push(mention.slug);
|
|
712
|
+
}
|
|
232
713
|
const activeLocale = selectedLocales.at(-1);
|
|
714
|
+
const mentionSummary = getMentionSummary(body?.mentions);
|
|
233
715
|
if (e2eModeEnabled()) {
|
|
234
716
|
return createE2EChatResponse({
|
|
235
717
|
prompt,
|
|
@@ -266,7 +748,23 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
266
748
|
"searchDocs"
|
|
267
749
|
]
|
|
268
750
|
};
|
|
751
|
+
logHandlerEvent(req, "info", {
|
|
752
|
+
activeLocale,
|
|
753
|
+
debug,
|
|
754
|
+
mentionCount: mentionSummary.length,
|
|
755
|
+
mentions: mentionSummary,
|
|
756
|
+
msg: "AI chat started",
|
|
757
|
+
promptPreview: getLogPreview(prompt),
|
|
758
|
+
selectedLocales
|
|
759
|
+
});
|
|
269
760
|
if (!providerConfig.apiKey) {
|
|
761
|
+
logHandlerEvent(req, "warn", {
|
|
762
|
+
activeLocale,
|
|
763
|
+
debug,
|
|
764
|
+
msg: "AI chat blocked: missing provider API key",
|
|
765
|
+
promptPreview: getLogPreview(prompt),
|
|
766
|
+
selectedLocales
|
|
767
|
+
});
|
|
270
768
|
return Response.json({
|
|
271
769
|
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.`
|
|
272
770
|
}, {
|
|
@@ -275,25 +773,70 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
275
773
|
}
|
|
276
774
|
try {
|
|
277
775
|
const proposals = [];
|
|
776
|
+
const toolFailures = [];
|
|
777
|
+
const registerToolFailure = (failure)=>{
|
|
778
|
+
toolFailures.push(failure);
|
|
779
|
+
logHandlerEvent(req, "warn", {
|
|
780
|
+
debug,
|
|
781
|
+
...failure,
|
|
782
|
+
msg: "AI tool validation failed",
|
|
783
|
+
promptPreview: getLogPreview(prompt)
|
|
784
|
+
});
|
|
785
|
+
};
|
|
786
|
+
const createToolError = ({ collection, details, message, slug, tool })=>{
|
|
787
|
+
registerToolFailure({
|
|
788
|
+
collection,
|
|
789
|
+
details,
|
|
790
|
+
message,
|
|
791
|
+
slug,
|
|
792
|
+
tool
|
|
793
|
+
});
|
|
794
|
+
return {
|
|
795
|
+
error: message
|
|
796
|
+
};
|
|
797
|
+
};
|
|
278
798
|
const addSignedProposal = (proposal)=>{
|
|
279
799
|
if ("data" in proposal && proposal.data && containsSensitiveData(proposal.data)) {
|
|
280
|
-
return {
|
|
281
|
-
|
|
282
|
-
|
|
800
|
+
return createToolError({
|
|
801
|
+
details: getProposalSummary(proposal),
|
|
802
|
+
message: "Proposal contains sensitive fields and cannot be created.",
|
|
803
|
+
tool: `propose${proposal.action[0]?.toUpperCase()}${proposal.action.slice(1)}`
|
|
804
|
+
});
|
|
283
805
|
}
|
|
284
806
|
if ("localizedData" in proposal && hasLocalizedData(proposal.localizedData) && Object.values(proposal.localizedData).some((value)=>containsSensitiveData(value))) {
|
|
285
|
-
return {
|
|
286
|
-
|
|
287
|
-
|
|
807
|
+
return createToolError({
|
|
808
|
+
details: getProposalSummary(proposal),
|
|
809
|
+
message: "Proposal contains sensitive fields and cannot be created.",
|
|
810
|
+
tool: `propose${proposal.action[0]?.toUpperCase()}${proposal.action.slice(1)}`
|
|
811
|
+
});
|
|
288
812
|
}
|
|
289
813
|
const signedProposal = signAIActionProposal(proposal);
|
|
290
814
|
proposals.push(signedProposal);
|
|
815
|
+
logHandlerEvent(req, "info", {
|
|
816
|
+
debug,
|
|
817
|
+
msg: "AI proposal created",
|
|
818
|
+
proposal: getProposalSummary(signedProposal)
|
|
819
|
+
});
|
|
291
820
|
return signedProposal;
|
|
292
821
|
};
|
|
293
822
|
const collectionSlugs = getAllowedCollectionSlugs(req, options.collections);
|
|
294
|
-
const
|
|
295
|
-
const
|
|
823
|
+
const collectionSlugSet = new Set(collectionSlugs);
|
|
824
|
+
const globalConfigs = req.payload.config.globals || [];
|
|
825
|
+
const globalSlugs = globalConfigs.map((global)=>global.slug);
|
|
826
|
+
const globalConfigsBySlug = new Map(globalConfigs.map((global)=>[
|
|
827
|
+
global.slug,
|
|
828
|
+
global
|
|
829
|
+
]));
|
|
830
|
+
const allowedCollections = req.payload.config.collections.filter((collection)=>collectionSlugSet.has(collection.slug));
|
|
831
|
+
const allowedCollectionsBySlug = new Map(allowedCollections.map((collection)=>[
|
|
832
|
+
collection.slug,
|
|
833
|
+
collection
|
|
834
|
+
]));
|
|
296
835
|
if (collectionSlugs.length === 0) {
|
|
836
|
+
logHandlerEvent(req, "warn", {
|
|
837
|
+
debug,
|
|
838
|
+
msg: "AI chat blocked: no AI-enabled collections configured"
|
|
839
|
+
});
|
|
297
840
|
return Response.json({
|
|
298
841
|
error: "No AI-enabled collections are configured."
|
|
299
842
|
}, {
|
|
@@ -305,10 +848,10 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
305
848
|
fields: collection.fields,
|
|
306
849
|
parent: collection.slug
|
|
307
850
|
})),
|
|
308
|
-
...
|
|
851
|
+
...globalConfigs.flatMap((global)=>collectBlocks({
|
|
309
852
|
fields: global.fields,
|
|
310
853
|
parent: global.slug
|
|
311
|
-
}))
|
|
854
|
+
}))
|
|
312
855
|
];
|
|
313
856
|
const mentionContext = await getMentionContext({
|
|
314
857
|
blockContexts,
|
|
@@ -318,15 +861,27 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
318
861
|
mentions: body?.mentions,
|
|
319
862
|
req
|
|
320
863
|
});
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
864
|
+
const mediaAttachmentContext = await getMediaAttachmentContext({
|
|
865
|
+
allowedCollectionsBySlug,
|
|
866
|
+
attachments: body?.attachments,
|
|
867
|
+
collections: options.collections,
|
|
868
|
+
req
|
|
869
|
+
});
|
|
870
|
+
const allowedAttachmentKeys = new Set(mediaAttachmentContext.flatMap((context)=>{
|
|
871
|
+
const attachment = context.attachment;
|
|
872
|
+
return isRecord(attachment) && typeof attachment.collection === "string" && typeof attachment.id === "string" ? [
|
|
873
|
+
`${attachment.collection}:${attachment.id}`
|
|
874
|
+
] : [];
|
|
875
|
+
}));
|
|
876
|
+
mentionContext.push(...mediaAttachmentContext);
|
|
877
|
+
const mentionedCollectionSlugs = [];
|
|
878
|
+
const mentionedCollectionSlugSet = new Set();
|
|
879
|
+
for (const mention of body?.mentions || []){
|
|
880
|
+
const slug = mention.type === "collection" && mention.slug ? mention.slug : mention.type === "doc" && mention.collection ? mention.collection : null;
|
|
881
|
+
if (!slug || !collectionSlugSet.has(slug) || mentionedCollectionSlugSet.has(slug)) continue;
|
|
882
|
+
mentionedCollectionSlugSet.add(slug);
|
|
883
|
+
mentionedCollectionSlugs.push(slug);
|
|
884
|
+
}
|
|
330
885
|
const createRequiredFieldsByCollection = Object.fromEntries(allowedCollections.map((collection)=>[
|
|
331
886
|
collection.slug,
|
|
332
887
|
getRequiredFieldInfos(collection.fields, collection.admin?.useAsTitle)
|
|
@@ -347,6 +902,38 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
347
902
|
titleFieldByCollection[slug]
|
|
348
903
|
]
|
|
349
904
|
] : []));
|
|
905
|
+
const collectionAliasMap = createCollectionAliasMap(allowedCollections);
|
|
906
|
+
const likelyCollectionMatches = getLikelyCollectionMatches({
|
|
907
|
+
aliasMap: collectionAliasMap,
|
|
908
|
+
prompt
|
|
909
|
+
});
|
|
910
|
+
const writeIntent = hasWriteIntent(prompt);
|
|
911
|
+
const inferredCollectionSlug = mentionedCollectionSlugs.length === 1 ? mentionedCollectionSlugs[0] : mentionedCollectionSlugs.length === 0 && likelyCollectionMatches.length === 1 ? likelyCollectionMatches[0] : undefined;
|
|
912
|
+
const inferredCollectionConfig = inferredCollectionSlug ? allowedCollectionsBySlug.get(inferredCollectionSlug) : undefined;
|
|
913
|
+
if (inferredCollectionConfig && !mentionContext.some((item)=>item.type === "collection" && item.slug === inferredCollectionConfig.slug)) {
|
|
914
|
+
mentionContext.push({
|
|
915
|
+
...describeCollectionLikeConfig({
|
|
916
|
+
config: inferredCollectionConfig,
|
|
917
|
+
permissions: options.collections,
|
|
918
|
+
type: "collection"
|
|
919
|
+
}),
|
|
920
|
+
inferredFromPrompt: true
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
const intentToolChoice = inferredCollectionConfig ? getIntentToolChoice(prompt) : undefined;
|
|
924
|
+
logHandlerEvent(req, "info", {
|
|
925
|
+
activeLocale,
|
|
926
|
+
allowedCollectionCount: allowedCollections.length,
|
|
927
|
+
collectionSlugs,
|
|
928
|
+
focusedCollections: mentionedCollectionSlugs,
|
|
929
|
+
globalSlugs,
|
|
930
|
+
inferredCollectionSlug,
|
|
931
|
+
intentToolChoice,
|
|
932
|
+
likelyCollectionMatches,
|
|
933
|
+
msg: "AI chat context prepared",
|
|
934
|
+
selectedLocales,
|
|
935
|
+
writeIntent
|
|
936
|
+
});
|
|
350
937
|
const collectionSlugSchema = z.enum(collectionSlugs);
|
|
351
938
|
const getDisallowedCollectionActionError = (collection, action)=>{
|
|
352
939
|
if (isCollectionActionAllowed({
|
|
@@ -355,13 +942,15 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
355
942
|
req,
|
|
356
943
|
slug: collection
|
|
357
944
|
})) return null;
|
|
358
|
-
return {
|
|
359
|
-
|
|
360
|
-
|
|
945
|
+
return createToolError({
|
|
946
|
+
collection,
|
|
947
|
+
message: `${action} is not enabled for collection: ${collection}`,
|
|
948
|
+
tool: "collectionPermissionCheck"
|
|
949
|
+
});
|
|
361
950
|
};
|
|
362
951
|
const tools = {
|
|
363
952
|
getDoc: {
|
|
364
|
-
description: "Read
|
|
953
|
+
description: "Read a document by collection and id.",
|
|
365
954
|
inputSchema: z.object({
|
|
366
955
|
collection: collectionSlugSchema,
|
|
367
956
|
id: z.string().min(1)
|
|
@@ -380,16 +969,20 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
380
969
|
}
|
|
381
970
|
},
|
|
382
971
|
listCollections: {
|
|
383
|
-
description: "List
|
|
972
|
+
description: "List collections; pass slug for one full schema.",
|
|
384
973
|
inputSchema: z.object({
|
|
385
974
|
slug: collectionSlugSchema.optional()
|
|
386
975
|
}),
|
|
387
976
|
execute: async ({ slug })=>{
|
|
388
977
|
if (slug) {
|
|
389
|
-
const collection =
|
|
390
|
-
if (!collection)
|
|
391
|
-
|
|
392
|
-
|
|
978
|
+
const collection = allowedCollectionsBySlug.get(slug);
|
|
979
|
+
if (!collection) {
|
|
980
|
+
return createToolError({
|
|
981
|
+
message: `Unknown collection: ${slug}`,
|
|
982
|
+
slug,
|
|
983
|
+
tool: "listCollections"
|
|
984
|
+
});
|
|
985
|
+
}
|
|
393
986
|
return describeCollectionLikeConfig({
|
|
394
987
|
config: collection,
|
|
395
988
|
permissions: options.collections,
|
|
@@ -404,15 +997,19 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
404
997
|
}
|
|
405
998
|
},
|
|
406
999
|
getGlobal: {
|
|
407
|
-
description: "Read
|
|
1000
|
+
description: "Read a global by slug.",
|
|
408
1001
|
inputSchema: z.object({
|
|
409
1002
|
slug: z.string().min(1)
|
|
410
1003
|
}),
|
|
411
1004
|
execute: async ({ slug })=>{
|
|
412
|
-
const globalConfig =
|
|
413
|
-
if (!globalConfig)
|
|
414
|
-
|
|
415
|
-
|
|
1005
|
+
const globalConfig = globalConfigsBySlug.get(slug);
|
|
1006
|
+
if (!globalConfig) {
|
|
1007
|
+
return createToolError({
|
|
1008
|
+
message: `Unknown global: ${slug}`,
|
|
1009
|
+
slug,
|
|
1010
|
+
tool: "getGlobal"
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
416
1013
|
return req.payload.findGlobal({
|
|
417
1014
|
depth: 2,
|
|
418
1015
|
...activeLocale ? {
|
|
@@ -425,71 +1022,164 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
425
1022
|
}
|
|
426
1023
|
},
|
|
427
1024
|
listGlobals: {
|
|
428
|
-
description: "List
|
|
1025
|
+
description: "List globals; pass slug for one full schema.",
|
|
429
1026
|
inputSchema: z.object({
|
|
430
1027
|
slug: z.string().optional()
|
|
431
1028
|
}),
|
|
432
1029
|
execute: async ({ slug })=>{
|
|
433
|
-
const globals = req.payload.config.globals || [];
|
|
434
1030
|
if (slug) {
|
|
435
|
-
const global =
|
|
436
|
-
if (!global)
|
|
437
|
-
|
|
438
|
-
|
|
1031
|
+
const global = globalConfigsBySlug.get(slug);
|
|
1032
|
+
if (!global) {
|
|
1033
|
+
return createToolError({
|
|
1034
|
+
message: `Unknown global: ${slug}`,
|
|
1035
|
+
slug,
|
|
1036
|
+
tool: "listGlobals"
|
|
1037
|
+
});
|
|
1038
|
+
}
|
|
439
1039
|
return describeCollectionLikeConfig({
|
|
440
1040
|
config: global,
|
|
441
1041
|
type: "global"
|
|
442
1042
|
});
|
|
443
1043
|
}
|
|
444
|
-
return
|
|
1044
|
+
return globalConfigs.map((global)=>describeCollectionLikeSummary({
|
|
445
1045
|
config: global,
|
|
446
1046
|
type: "global"
|
|
447
1047
|
}));
|
|
448
1048
|
}
|
|
449
1049
|
},
|
|
450
1050
|
proposeCreateDoc: {
|
|
451
|
-
description: "
|
|
1051
|
+
description: "Propose document creation. Use exact schema fields; include required fields. Use localizedData for multi-locale writes.",
|
|
452
1052
|
inputSchema: z.object({
|
|
453
1053
|
collection: collectionSlugSchema,
|
|
454
1054
|
data: z.record(z.string(), z.unknown()).optional(),
|
|
455
1055
|
label: z.string().min(1),
|
|
456
|
-
localizedData:
|
|
1056
|
+
localizedData: nonEmptyLocalizedDataSchema.optional()
|
|
457
1057
|
}).refine((value)=>Boolean(value.data || value.localizedData), {
|
|
458
1058
|
message: "Either data or localizedData is required."
|
|
459
1059
|
}),
|
|
460
1060
|
execute: async ({ collection, data, label, localizedData })=>{
|
|
461
1061
|
const permissionError = getDisallowedCollectionActionError(collection, "create");
|
|
462
1062
|
if (permissionError) return permissionError;
|
|
463
|
-
const
|
|
1063
|
+
const collectionConfig = allowedCollectionsBySlug.get(collection);
|
|
1064
|
+
const collectionFields = collectionConfig?.fields || [];
|
|
1065
|
+
const preparedData = prepareProposalWriteData({
|
|
1066
|
+
collectionConfig: collectionConfig,
|
|
464
1067
|
data,
|
|
1068
|
+
inferenceText: prompt,
|
|
465
1069
|
label,
|
|
466
1070
|
localizedData,
|
|
467
|
-
|
|
468
|
-
});
|
|
469
|
-
const missingFields = getMissingCreateFields({
|
|
470
|
-
data: completedCreatePayload.data,
|
|
471
|
-
localizedData: completedCreatePayload.localizedData,
|
|
472
|
-
requiredFields: createRequiredFieldsByCollection[collection] || []
|
|
1071
|
+
mode: "create"
|
|
473
1072
|
});
|
|
474
|
-
if (
|
|
1073
|
+
if (preparedData.issues.length > 0) {
|
|
475
1074
|
const titleFieldName = titleFieldByCollection[collection];
|
|
476
|
-
const missingTitleField = titleFieldName ?
|
|
477
|
-
return {
|
|
478
|
-
|
|
479
|
-
|
|
1075
|
+
const missingTitleField = titleFieldName ? preparedData.issues.some((issue)=>issue.path === titleFieldName || issue.path.endsWith(`.${titleFieldName}`)) : false;
|
|
1076
|
+
return createToolError({
|
|
1077
|
+
collection,
|
|
1078
|
+
details: {
|
|
1079
|
+
issues: preparedData.issues,
|
|
1080
|
+
titleFieldName
|
|
1081
|
+
},
|
|
1082
|
+
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)}`,
|
|
1083
|
+
tool: "proposeCreateDoc"
|
|
1084
|
+
});
|
|
480
1085
|
}
|
|
481
|
-
const
|
|
1086
|
+
const requestedBlockTypes = getRequestedBlockTypes({
|
|
1087
|
+
availableBlockTypes: getCollectionBlockTypes(collectionFields),
|
|
1088
|
+
mentions: body?.mentions,
|
|
1089
|
+
prompt
|
|
1090
|
+
});
|
|
1091
|
+
if (requestedBlockTypes.length > 0) {
|
|
1092
|
+
const proposalBlockTypes = new Set();
|
|
1093
|
+
if (preparedData.data) {
|
|
1094
|
+
for (const blockType of collectProposalBlockTypes({
|
|
1095
|
+
data: preparedData.data,
|
|
1096
|
+
fields: collectionFields
|
|
1097
|
+
})){
|
|
1098
|
+
proposalBlockTypes.add(blockType);
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
if (preparedData.localizedData) {
|
|
1102
|
+
for (const localeData of Object.values(preparedData.localizedData)){
|
|
1103
|
+
for (const blockType of collectProposalBlockTypes({
|
|
1104
|
+
data: localeData,
|
|
1105
|
+
fields: collectionFields
|
|
1106
|
+
})){
|
|
1107
|
+
proposalBlockTypes.add(blockType);
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
const missingBlockTypes = requestedBlockTypes.filter((blockType)=>!proposalBlockTypes.has(blockType));
|
|
1112
|
+
if (missingBlockTypes.length > 0) {
|
|
1113
|
+
return createToolError({
|
|
1114
|
+
collection,
|
|
1115
|
+
details: {
|
|
1116
|
+
missingBlockTypes,
|
|
1117
|
+
requestedBlockTypes
|
|
1118
|
+
},
|
|
1119
|
+
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.`,
|
|
1120
|
+
tool: "proposeCreateDoc"
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
const uploadTargetsOutsideAttachments = [
|
|
1125
|
+
...preparedData.data ? getUploadTargetsOutsideAttachments({
|
|
1126
|
+
allowedAttachmentKeys,
|
|
1127
|
+
data: preparedData.data,
|
|
1128
|
+
fields: collectionFields
|
|
1129
|
+
}) : [],
|
|
1130
|
+
...preparedData.localizedData ? Object.values(preparedData.localizedData).flatMap((localeData)=>getUploadTargetsOutsideAttachments({
|
|
1131
|
+
allowedAttachmentKeys,
|
|
1132
|
+
data: localeData,
|
|
1133
|
+
fields: collectionFields
|
|
1134
|
+
})) : []
|
|
1135
|
+
];
|
|
1136
|
+
if (uploadTargetsOutsideAttachments.length > 0) {
|
|
1137
|
+
return createToolError({
|
|
1138
|
+
collection,
|
|
1139
|
+
details: {
|
|
1140
|
+
allowedAttachments: [
|
|
1141
|
+
...allowedAttachmentKeys
|
|
1142
|
+
],
|
|
1143
|
+
uploadTargetsOutsideAttachments
|
|
1144
|
+
},
|
|
1145
|
+
message: `Create proposal for ${collection} uses upload references that are not in the uploaded attachments: ${uploadTargetsOutsideAttachments.map((target)=>`${target.path} -> ${target.collection}:${target.id}`).join(", ")}. Use only uploaded media attachment IDs for upload fields.`,
|
|
1146
|
+
tool: "proposeCreateDoc"
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
const invalidRelationshipTargets = [
|
|
1150
|
+
...preparedData.data ? await validateRelationshipTargetsExist({
|
|
1151
|
+
data: preparedData.data,
|
|
1152
|
+
fields: collectionFields,
|
|
1153
|
+
req
|
|
1154
|
+
}) : [],
|
|
1155
|
+
...preparedData.localizedData ? (await Promise.all(Object.values(preparedData.localizedData).map((localeData)=>validateRelationshipTargetsExist({
|
|
1156
|
+
data: localeData,
|
|
1157
|
+
fields: collectionFields,
|
|
1158
|
+
req
|
|
1159
|
+
})))).flat() : []
|
|
1160
|
+
];
|
|
1161
|
+
if (invalidRelationshipTargets.length > 0) {
|
|
1162
|
+
return createToolError({
|
|
1163
|
+
collection,
|
|
1164
|
+
details: {
|
|
1165
|
+
invalidRelationshipTargets
|
|
1166
|
+
},
|
|
1167
|
+
message: `Create proposal for ${collection} contains relationship or upload references that do not exist: ${invalidRelationshipTargets.map((target)=>`${target.path} -> ${target.collection}:${target.id}`).join(", ")}.`,
|
|
1168
|
+
tool: "proposeCreateDoc"
|
|
1169
|
+
});
|
|
1170
|
+
}
|
|
1171
|
+
const proposal = preparedData.localizedData ? {
|
|
482
1172
|
action: "create",
|
|
483
1173
|
collection,
|
|
484
1174
|
label: getSafeProposalLabel(label),
|
|
485
|
-
localizedData:
|
|
1175
|
+
localizedData: preparedData.localizedData,
|
|
486
1176
|
...activeLocale ? {
|
|
487
1177
|
locale: activeLocale
|
|
488
1178
|
} : {}
|
|
489
1179
|
} : {
|
|
490
1180
|
action: "create",
|
|
491
1181
|
collection,
|
|
492
|
-
data:
|
|
1182
|
+
data: preparedData.data || {},
|
|
493
1183
|
label: getSafeProposalLabel(label),
|
|
494
1184
|
...activeLocale ? {
|
|
495
1185
|
locale: activeLocale
|
|
@@ -499,7 +1189,7 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
499
1189
|
}
|
|
500
1190
|
},
|
|
501
1191
|
proposeDeleteDoc: {
|
|
502
|
-
description: "
|
|
1192
|
+
description: "Propose document deletion.",
|
|
503
1193
|
inputSchema: z.object({
|
|
504
1194
|
collection: collectionSlugSchema,
|
|
505
1195
|
id: z.string().min(1),
|
|
@@ -521,26 +1211,93 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
521
1211
|
}
|
|
522
1212
|
},
|
|
523
1213
|
proposeUpdateDoc: {
|
|
524
|
-
description: "
|
|
1214
|
+
description: "Propose document update. Use exact schema fields. Use localizedData for multi-locale writes.",
|
|
525
1215
|
inputSchema: z.object({
|
|
526
1216
|
collection: collectionSlugSchema,
|
|
527
1217
|
data: z.record(z.string(), z.unknown()).optional(),
|
|
528
1218
|
id: z.string().min(1),
|
|
529
1219
|
label: z.string().min(1),
|
|
530
|
-
localizedData:
|
|
1220
|
+
localizedData: nonEmptyLocalizedDataSchema.optional()
|
|
531
1221
|
}).refine((value)=>Boolean(value.data || value.localizedData), {
|
|
532
1222
|
message: "Either data or localizedData is required."
|
|
533
1223
|
}),
|
|
534
1224
|
execute: async ({ collection, data, id, label, localizedData })=>{
|
|
535
1225
|
const permissionError = getDisallowedCollectionActionError(collection, "update");
|
|
536
1226
|
if (permissionError) return permissionError;
|
|
1227
|
+
const collectionConfig = allowedCollectionsBySlug.get(collection);
|
|
1228
|
+
const collectionFields = collectionConfig?.fields || [];
|
|
1229
|
+
const preparedData = prepareProposalWriteData({
|
|
1230
|
+
collectionConfig: collectionConfig,
|
|
1231
|
+
data,
|
|
1232
|
+
inferenceText: prompt,
|
|
1233
|
+
label,
|
|
1234
|
+
localizedData,
|
|
1235
|
+
mode: "update"
|
|
1236
|
+
});
|
|
1237
|
+
if (preparedData.issues.length > 0) {
|
|
1238
|
+
return createToolError({
|
|
1239
|
+
collection,
|
|
1240
|
+
details: {
|
|
1241
|
+
issues: preparedData.issues
|
|
1242
|
+
},
|
|
1243
|
+
message: `Update proposal for ${collection} is invalid. Retry with exact schema fields and complete array/block objects: ${formatProposalIssuesForRetry(preparedData.issues)}`,
|
|
1244
|
+
tool: "proposeUpdateDoc"
|
|
1245
|
+
});
|
|
1246
|
+
}
|
|
1247
|
+
const invalidRelationshipTargets = [
|
|
1248
|
+
...preparedData.data ? await validateRelationshipTargetsExist({
|
|
1249
|
+
data: preparedData.data,
|
|
1250
|
+
fields: collectionFields,
|
|
1251
|
+
req
|
|
1252
|
+
}) : [],
|
|
1253
|
+
...preparedData.localizedData ? (await Promise.all(Object.values(preparedData.localizedData).map((localeData)=>validateRelationshipTargetsExist({
|
|
1254
|
+
data: localeData,
|
|
1255
|
+
fields: collectionFields,
|
|
1256
|
+
req
|
|
1257
|
+
})))).flat() : []
|
|
1258
|
+
];
|
|
1259
|
+
if (invalidRelationshipTargets.length > 0) {
|
|
1260
|
+
return createToolError({
|
|
1261
|
+
collection,
|
|
1262
|
+
details: {
|
|
1263
|
+
invalidRelationshipTargets
|
|
1264
|
+
},
|
|
1265
|
+
message: `Update proposal for ${collection} contains relationship or upload references that do not exist: ${invalidRelationshipTargets.map((target)=>`${target.path} -> ${target.collection}:${target.id}`).join(", ")}.`,
|
|
1266
|
+
tool: "proposeUpdateDoc"
|
|
1267
|
+
});
|
|
1268
|
+
}
|
|
1269
|
+
const uploadTargetsOutsideAttachments = [
|
|
1270
|
+
...preparedData.data ? getUploadTargetsOutsideAttachments({
|
|
1271
|
+
allowedAttachmentKeys,
|
|
1272
|
+
data: preparedData.data,
|
|
1273
|
+
fields: collectionFields
|
|
1274
|
+
}) : [],
|
|
1275
|
+
...preparedData.localizedData ? Object.values(preparedData.localizedData).flatMap((localeData)=>getUploadTargetsOutsideAttachments({
|
|
1276
|
+
allowedAttachmentKeys,
|
|
1277
|
+
data: localeData,
|
|
1278
|
+
fields: collectionFields
|
|
1279
|
+
})) : []
|
|
1280
|
+
];
|
|
1281
|
+
if (uploadTargetsOutsideAttachments.length > 0) {
|
|
1282
|
+
return createToolError({
|
|
1283
|
+
collection,
|
|
1284
|
+
details: {
|
|
1285
|
+
allowedAttachments: [
|
|
1286
|
+
...allowedAttachmentKeys
|
|
1287
|
+
],
|
|
1288
|
+
uploadTargetsOutsideAttachments
|
|
1289
|
+
},
|
|
1290
|
+
message: `Update proposal for ${collection} uses upload references that are not in the uploaded attachments: ${uploadTargetsOutsideAttachments.map((target)=>`${target.path} -> ${target.collection}:${target.id}`).join(", ")}. Use only uploaded media attachment IDs for upload fields.`,
|
|
1291
|
+
tool: "proposeUpdateDoc"
|
|
1292
|
+
});
|
|
1293
|
+
}
|
|
537
1294
|
const proposal = {
|
|
538
1295
|
action: "update",
|
|
539
1296
|
collection,
|
|
540
|
-
...localizedData ? {
|
|
541
|
-
localizedData
|
|
1297
|
+
...preparedData.localizedData ? {
|
|
1298
|
+
localizedData: preparedData.localizedData
|
|
542
1299
|
} : {
|
|
543
|
-
data: data || {}
|
|
1300
|
+
data: preparedData.data || {}
|
|
544
1301
|
},
|
|
545
1302
|
id,
|
|
546
1303
|
label: getSafeProposalLabel(label),
|
|
@@ -552,26 +1309,51 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
552
1309
|
}
|
|
553
1310
|
},
|
|
554
1311
|
proposeUpdateGlobal: {
|
|
555
|
-
description: "
|
|
1312
|
+
description: "Propose global update. Use localizedData for multi-locale writes.",
|
|
556
1313
|
inputSchema: z.object({
|
|
557
1314
|
data: z.record(z.string(), z.unknown()).optional(),
|
|
558
1315
|
label: z.string().min(1),
|
|
559
|
-
localizedData:
|
|
1316
|
+
localizedData: nonEmptyLocalizedDataSchema.optional(),
|
|
560
1317
|
slug: z.string().min(1)
|
|
561
1318
|
}).refine((value)=>Boolean(value.data || value.localizedData), {
|
|
562
1319
|
message: "Either data or localizedData is required."
|
|
563
1320
|
}),
|
|
564
1321
|
execute: async ({ data, label, localizedData, slug })=>{
|
|
565
|
-
const globalConfig =
|
|
566
|
-
if (!globalConfig)
|
|
567
|
-
|
|
568
|
-
|
|
1322
|
+
const globalConfig = globalConfigsBySlug.get(slug);
|
|
1323
|
+
if (!globalConfig) {
|
|
1324
|
+
return createToolError({
|
|
1325
|
+
message: `Unknown global: ${slug}`,
|
|
1326
|
+
slug,
|
|
1327
|
+
tool: "proposeUpdateGlobal"
|
|
1328
|
+
});
|
|
1329
|
+
}
|
|
1330
|
+
const preparedData = prepareProposalWriteData({
|
|
1331
|
+
collectionConfig: {
|
|
1332
|
+
fields: globalConfig.fields || [],
|
|
1333
|
+
slug: globalConfig.slug
|
|
1334
|
+
},
|
|
1335
|
+
data,
|
|
1336
|
+
inferenceText: prompt,
|
|
1337
|
+
label,
|
|
1338
|
+
localizedData,
|
|
1339
|
+
mode: "update"
|
|
1340
|
+
});
|
|
1341
|
+
if (preparedData.issues.length > 0) {
|
|
1342
|
+
return createToolError({
|
|
1343
|
+
details: {
|
|
1344
|
+
issues: preparedData.issues
|
|
1345
|
+
},
|
|
1346
|
+
message: `Update proposal for global ${slug} is invalid. Retry with exact schema fields and complete array/block objects: ${formatProposalIssuesForRetry(preparedData.issues)}`,
|
|
1347
|
+
slug,
|
|
1348
|
+
tool: "proposeUpdateGlobal"
|
|
1349
|
+
});
|
|
1350
|
+
}
|
|
569
1351
|
const proposal = {
|
|
570
1352
|
action: "updateGlobal",
|
|
571
|
-
...localizedData ? {
|
|
572
|
-
localizedData
|
|
1353
|
+
...preparedData.localizedData ? {
|
|
1354
|
+
localizedData: preparedData.localizedData
|
|
573
1355
|
} : {
|
|
574
|
-
data: data || {}
|
|
1356
|
+
data: preparedData.data || {}
|
|
575
1357
|
},
|
|
576
1358
|
label: getSafeProposalLabel(label),
|
|
577
1359
|
...activeLocale ? {
|
|
@@ -583,19 +1365,24 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
583
1365
|
}
|
|
584
1366
|
},
|
|
585
1367
|
searchDocs: {
|
|
586
|
-
description: "Search documents in one collection.
|
|
1368
|
+
description: "Search documents in one collection.",
|
|
587
1369
|
inputSchema: z.object({
|
|
588
1370
|
collection: collectionSlugSchema,
|
|
589
1371
|
limit: z.number().int().min(1).max(10).default(5),
|
|
590
1372
|
query: z.string().optional()
|
|
591
1373
|
}),
|
|
592
1374
|
execute: async ({ collection, limit, query })=>{
|
|
593
|
-
const collectionConfig =
|
|
594
|
-
const searchableFields = collectionConfig?.fields.
|
|
1375
|
+
const collectionConfig = allowedCollectionsBySlug.get(collection);
|
|
1376
|
+
const searchableFields = collectionConfig?.fields.flatMap((field)=>{
|
|
1377
|
+
if (!("name" in field) || ![
|
|
595
1378
|
"email",
|
|
596
1379
|
"text",
|
|
597
1380
|
"textarea"
|
|
598
|
-
].includes(field.type)
|
|
1381
|
+
].includes(field.type) || !field.name) return [];
|
|
1382
|
+
return [
|
|
1383
|
+
field.name
|
|
1384
|
+
];
|
|
1385
|
+
}) || [];
|
|
599
1386
|
const where = query && searchableFields.length > 0 ? {
|
|
600
1387
|
or: searchableFields.map((field)=>({
|
|
601
1388
|
[field]: {
|
|
@@ -638,12 +1425,25 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
638
1425
|
"You are a Payload CMS assistant. Inspect schema/content with tools before proposing writes.",
|
|
639
1426
|
"Mentions define the active CMS scope. Locale mentions define active locale; multiple locales require localizedData keyed by locale.",
|
|
640
1427
|
"Writes are proposals only. Never claim changes were applied before user confirmation.",
|
|
641
|
-
"
|
|
642
|
-
"If
|
|
1428
|
+
"Uploaded media attachments appear in context as mediaAttachment entries. Use their exact IDs for upload fields.",
|
|
1429
|
+
"If an uploaded media document has editable descriptive fields, propose an update to that media document with suitable values.",
|
|
1430
|
+
"If a collection has a required `slug` field and the user does not provide it, generate a URL‑friendly slug from the title or from the first meaningful word of the prompt.",
|
|
1431
|
+
"For create/update/delete requests, you must use proposal tools. Do not end with plain text if the user asked for a content change.",
|
|
1432
|
+
"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.",
|
|
1433
|
+
"Put concrete field values only in tool data, not visible text.",
|
|
1434
|
+
"For blocks fields: use exact blockType values from schema, exact field names, and complete objects for required block fields.",
|
|
1435
|
+
"For arrays: every item must be an object matching the child field schema, not free text.",
|
|
1436
|
+
"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.",
|
|
1437
|
+
`Collection aliases: ${JSON.stringify(collectionAliasMap)}.`,
|
|
1438
|
+
`Likely collection matches for this prompt: ${JSON.stringify(likelyCollectionMatches)}.`,
|
|
643
1439
|
`Focused required create fields: ${JSON.stringify(focusedRequiredFieldsByCollection)}.`,
|
|
644
1440
|
`Focused title fields: ${JSON.stringify(focusedTitleFieldByCollection)}. Infer concise titles when needed.`,
|
|
1441
|
+
`Preferred proposal tool for this prompt: ${intentToolChoice?.toolName || "none"}.`,
|
|
645
1442
|
"Visible response: plain text, under 40 words, no Markdown, no proposed content."
|
|
646
1443
|
].join("\n"),
|
|
1444
|
+
...intentToolChoice ? {
|
|
1445
|
+
toolChoice: intentToolChoice
|
|
1446
|
+
} : {},
|
|
647
1447
|
tools
|
|
648
1448
|
});
|
|
649
1449
|
const stream = new ReadableStream({
|
|
@@ -668,19 +1468,75 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
668
1468
|
if (part.type === "finish") {
|
|
669
1469
|
const finishPart = part;
|
|
670
1470
|
usage = finishPart.totalUsage || finishPart.usage || null;
|
|
1471
|
+
const reason = getChatCompletionReason({
|
|
1472
|
+
proposalCount: proposals.length,
|
|
1473
|
+
toolFailures,
|
|
1474
|
+
writeIntent
|
|
1475
|
+
});
|
|
1476
|
+
const debugPayload = createDebugPayload({
|
|
1477
|
+
activeLocale,
|
|
1478
|
+
debug,
|
|
1479
|
+
proposalCount: proposals.length,
|
|
1480
|
+
selectedLocales,
|
|
1481
|
+
toolFailures,
|
|
1482
|
+
usage,
|
|
1483
|
+
writeIntent
|
|
1484
|
+
});
|
|
1485
|
+
logHandlerEvent(req, proposals.length > 0 ? "info" : "warn", {
|
|
1486
|
+
activeLocale,
|
|
1487
|
+
debug,
|
|
1488
|
+
msg: proposals.length > 0 ? "AI chat completed with proposals" : "AI chat completed without proposals",
|
|
1489
|
+
proposalCount: proposals.length,
|
|
1490
|
+
proposals: proposals.map((proposal)=>getProposalSummary(proposal)),
|
|
1491
|
+
promptPreview: getLogPreview(prompt),
|
|
1492
|
+
reason,
|
|
1493
|
+
selectedLocales,
|
|
1494
|
+
toolFailureCount: toolFailures.length,
|
|
1495
|
+
toolFailures,
|
|
1496
|
+
usage
|
|
1497
|
+
});
|
|
671
1498
|
sendEvent(controller, "proposals", {
|
|
672
1499
|
proposals,
|
|
673
1500
|
usage
|
|
674
1501
|
});
|
|
1502
|
+
sendEvent(controller, "debug", debugPayload);
|
|
675
1503
|
sendEvent(controller, "done", {});
|
|
676
1504
|
didSendTerminalEvent = true;
|
|
677
1505
|
}
|
|
678
1506
|
}
|
|
679
1507
|
if (!didSendTerminalEvent) {
|
|
1508
|
+
const reason = getChatCompletionReason({
|
|
1509
|
+
proposalCount: proposals.length,
|
|
1510
|
+
toolFailures,
|
|
1511
|
+
writeIntent
|
|
1512
|
+
});
|
|
1513
|
+
const debugPayload = createDebugPayload({
|
|
1514
|
+
activeLocale,
|
|
1515
|
+
debug,
|
|
1516
|
+
proposalCount: proposals.length,
|
|
1517
|
+
selectedLocales,
|
|
1518
|
+
toolFailures,
|
|
1519
|
+
usage,
|
|
1520
|
+
writeIntent
|
|
1521
|
+
});
|
|
1522
|
+
logHandlerEvent(req, proposals.length > 0 ? "info" : "warn", {
|
|
1523
|
+
activeLocale,
|
|
1524
|
+
debug,
|
|
1525
|
+
msg: proposals.length > 0 ? "AI chat completed with proposals" : "AI chat completed without proposals",
|
|
1526
|
+
proposalCount: proposals.length,
|
|
1527
|
+
proposals: proposals.map((proposal)=>getProposalSummary(proposal)),
|
|
1528
|
+
promptPreview: getLogPreview(prompt),
|
|
1529
|
+
reason,
|
|
1530
|
+
selectedLocales,
|
|
1531
|
+
toolFailureCount: toolFailures.length,
|
|
1532
|
+
toolFailures,
|
|
1533
|
+
usage
|
|
1534
|
+
});
|
|
680
1535
|
sendEvent(controller, "proposals", {
|
|
681
1536
|
proposals,
|
|
682
1537
|
usage
|
|
683
1538
|
});
|
|
1539
|
+
sendEvent(controller, "debug", debugPayload);
|
|
684
1540
|
sendEvent(controller, "done", {});
|
|
685
1541
|
}
|
|
686
1542
|
} catch (err) {
|