@mvriu5/payload-ai 1.1.1 → 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 +155 -6
- package/dist/components/AIInput.module.css +11 -2
- package/dist/components/ActionToast.d.ts +2 -1
- package/dist/components/ActionToast.js +3 -2
- package/dist/handlers/applyActionHandler.js +316 -107
- package/dist/handlers/chatHandler.js +660 -48
- package/dist/handlers/proposalDiffHandler.js +81 -26
- 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 +1 -1
|
@@ -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,6 +24,26 @@ 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
49
|
const wantsCreatePost = normalizedPrompt.includes("post") && (normalizedPrompt.includes("create") || normalizedPrompt.includes("erstell"));
|
|
@@ -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"
|
|
@@ -215,6 +260,277 @@ const getMissingCreateFields = ({ data, localizedData, requiredFields })=>{
|
|
|
215
260
|
];
|
|
216
261
|
return requiredFields.filter((field)=>!hasValueAtPath(data, field.path)).map((field)=>field.path);
|
|
217
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
|
+
};
|
|
218
534
|
export const createChatHandler = (options = {})=>async (req)=>{
|
|
219
535
|
if (!req.user) return Response.json({
|
|
220
536
|
error: "Unauthorized"
|
|
@@ -230,6 +546,7 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
230
546
|
});
|
|
231
547
|
const selectedLocales = (body?.mentions?.filter((mention)=>mention.type === "locale" && mention.slug).map((mention)=>mention.slug) || []).filter((locale, index, array)=>array.indexOf(locale) === index);
|
|
232
548
|
const activeLocale = selectedLocales.at(-1);
|
|
549
|
+
const mentionSummary = getMentionSummary(body?.mentions);
|
|
233
550
|
if (e2eModeEnabled()) {
|
|
234
551
|
return createE2EChatResponse({
|
|
235
552
|
prompt,
|
|
@@ -266,7 +583,23 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
266
583
|
"searchDocs"
|
|
267
584
|
]
|
|
268
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
|
+
});
|
|
269
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
|
+
});
|
|
270
603
|
return Response.json({
|
|
271
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.`
|
|
272
605
|
}, {
|
|
@@ -275,25 +608,60 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
275
608
|
}
|
|
276
609
|
try {
|
|
277
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
|
+
};
|
|
278
633
|
const addSignedProposal = (proposal)=>{
|
|
279
634
|
if ("data" in proposal && proposal.data && containsSensitiveData(proposal.data)) {
|
|
280
|
-
return {
|
|
281
|
-
|
|
282
|
-
|
|
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
|
+
});
|
|
283
640
|
}
|
|
284
641
|
if ("localizedData" in proposal && hasLocalizedData(proposal.localizedData) && Object.values(proposal.localizedData).some((value)=>containsSensitiveData(value))) {
|
|
285
|
-
return {
|
|
286
|
-
|
|
287
|
-
|
|
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
|
+
});
|
|
288
647
|
}
|
|
289
648
|
const signedProposal = signAIActionProposal(proposal);
|
|
290
649
|
proposals.push(signedProposal);
|
|
650
|
+
logHandlerEvent(req, "info", {
|
|
651
|
+
debug,
|
|
652
|
+
msg: "AI proposal created",
|
|
653
|
+
proposal: getProposalSummary(signedProposal)
|
|
654
|
+
});
|
|
291
655
|
return signedProposal;
|
|
292
656
|
};
|
|
293
657
|
const collectionSlugs = getAllowedCollectionSlugs(req, options.collections);
|
|
294
658
|
const globalSlugs = req.payload.config.globals?.map((global)=>global.slug) || [];
|
|
295
659
|
const allowedCollections = req.payload.config.collections.filter((collection)=>collectionSlugs.includes(collection.slug));
|
|
296
660
|
if (collectionSlugs.length === 0) {
|
|
661
|
+
logHandlerEvent(req, "warn", {
|
|
662
|
+
debug,
|
|
663
|
+
msg: "AI chat blocked: no AI-enabled collections configured"
|
|
664
|
+
});
|
|
297
665
|
return Response.json({
|
|
298
666
|
error: "No AI-enabled collections are configured."
|
|
299
667
|
}, {
|
|
@@ -347,6 +715,38 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
347
715
|
titleFieldByCollection[slug]
|
|
348
716
|
]
|
|
349
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
|
+
});
|
|
350
750
|
const collectionSlugSchema = z.enum(collectionSlugs);
|
|
351
751
|
const getDisallowedCollectionActionError = (collection, action)=>{
|
|
352
752
|
if (isCollectionActionAllowed({
|
|
@@ -355,9 +755,11 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
355
755
|
req,
|
|
356
756
|
slug: collection
|
|
357
757
|
})) return null;
|
|
358
|
-
return {
|
|
359
|
-
|
|
360
|
-
|
|
758
|
+
return createToolError({
|
|
759
|
+
collection,
|
|
760
|
+
message: `${action} is not enabled for collection: ${collection}`,
|
|
761
|
+
tool: "collectionPermissionCheck"
|
|
762
|
+
});
|
|
361
763
|
};
|
|
362
764
|
const tools = {
|
|
363
765
|
getDoc: {
|
|
@@ -387,9 +789,13 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
387
789
|
execute: async ({ slug })=>{
|
|
388
790
|
if (slug) {
|
|
389
791
|
const collection = allowedCollections.find((item)=>item.slug === slug);
|
|
390
|
-
if (!collection)
|
|
391
|
-
|
|
392
|
-
|
|
792
|
+
if (!collection) {
|
|
793
|
+
return createToolError({
|
|
794
|
+
message: `Unknown collection: ${slug}`,
|
|
795
|
+
slug,
|
|
796
|
+
tool: "listCollections"
|
|
797
|
+
});
|
|
798
|
+
}
|
|
393
799
|
return describeCollectionLikeConfig({
|
|
394
800
|
config: collection,
|
|
395
801
|
permissions: options.collections,
|
|
@@ -410,9 +816,13 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
410
816
|
}),
|
|
411
817
|
execute: async ({ slug })=>{
|
|
412
818
|
const globalConfig = req.payload.config.globals?.find((global)=>global.slug === slug);
|
|
413
|
-
if (!globalConfig)
|
|
414
|
-
|
|
415
|
-
|
|
819
|
+
if (!globalConfig) {
|
|
820
|
+
return createToolError({
|
|
821
|
+
message: `Unknown global: ${slug}`,
|
|
822
|
+
slug,
|
|
823
|
+
tool: "getGlobal"
|
|
824
|
+
});
|
|
825
|
+
}
|
|
416
826
|
return req.payload.findGlobal({
|
|
417
827
|
depth: 2,
|
|
418
828
|
...activeLocale ? {
|
|
@@ -433,9 +843,13 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
433
843
|
const globals = req.payload.config.globals || [];
|
|
434
844
|
if (slug) {
|
|
435
845
|
const global = globals.find((item)=>item.slug === slug);
|
|
436
|
-
if (!global)
|
|
437
|
-
|
|
438
|
-
|
|
846
|
+
if (!global) {
|
|
847
|
+
return createToolError({
|
|
848
|
+
message: `Unknown global: ${slug}`,
|
|
849
|
+
slug,
|
|
850
|
+
tool: "listGlobals"
|
|
851
|
+
});
|
|
852
|
+
}
|
|
439
853
|
return describeCollectionLikeConfig({
|
|
440
854
|
config: global,
|
|
441
855
|
type: "global"
|
|
@@ -453,43 +867,108 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
453
867
|
collection: collectionSlugSchema,
|
|
454
868
|
data: z.record(z.string(), z.unknown()).optional(),
|
|
455
869
|
label: z.string().min(1),
|
|
456
|
-
localizedData:
|
|
870
|
+
localizedData: nonEmptyLocalizedDataSchema.optional()
|
|
457
871
|
}).refine((value)=>Boolean(value.data || value.localizedData), {
|
|
458
872
|
message: "Either data or localizedData is required."
|
|
459
873
|
}),
|
|
460
874
|
execute: async ({ collection, data, label, localizedData })=>{
|
|
461
875
|
const permissionError = getDisallowedCollectionActionError(collection, "create");
|
|
462
876
|
if (permissionError) return permissionError;
|
|
463
|
-
const
|
|
877
|
+
const collectionConfig = allowedCollections.find((item)=>item.slug === collection);
|
|
878
|
+
const collectionFields = collectionConfig?.fields || [];
|
|
879
|
+
const preparedData = prepareProposalWriteData({
|
|
880
|
+
collectionConfig: collectionConfig,
|
|
464
881
|
data,
|
|
882
|
+
inferenceText: prompt,
|
|
465
883
|
label,
|
|
466
884
|
localizedData,
|
|
467
|
-
|
|
468
|
-
});
|
|
469
|
-
const missingFields = getMissingCreateFields({
|
|
470
|
-
data: completedCreatePayload.data,
|
|
471
|
-
localizedData: completedCreatePayload.localizedData,
|
|
472
|
-
requiredFields: createRequiredFieldsByCollection[collection] || []
|
|
885
|
+
mode: "create"
|
|
473
886
|
});
|
|
474
|
-
if (
|
|
887
|
+
if (preparedData.issues.length > 0) {
|
|
475
888
|
const titleFieldName = titleFieldByCollection[collection];
|
|
476
|
-
const missingTitleField = titleFieldName ?
|
|
477
|
-
return {
|
|
478
|
-
|
|
479
|
-
|
|
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
|
+
}
|
|
937
|
+
}
|
|
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
|
+
});
|
|
480
959
|
}
|
|
481
|
-
const proposal =
|
|
960
|
+
const proposal = preparedData.localizedData ? {
|
|
482
961
|
action: "create",
|
|
483
962
|
collection,
|
|
484
963
|
label: getSafeProposalLabel(label),
|
|
485
|
-
localizedData:
|
|
964
|
+
localizedData: preparedData.localizedData,
|
|
486
965
|
...activeLocale ? {
|
|
487
966
|
locale: activeLocale
|
|
488
967
|
} : {}
|
|
489
968
|
} : {
|
|
490
969
|
action: "create",
|
|
491
970
|
collection,
|
|
492
|
-
data:
|
|
971
|
+
data: preparedData.data || {},
|
|
493
972
|
label: getSafeProposalLabel(label),
|
|
494
973
|
...activeLocale ? {
|
|
495
974
|
locale: activeLocale
|
|
@@ -527,20 +1006,62 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
527
1006
|
data: z.record(z.string(), z.unknown()).optional(),
|
|
528
1007
|
id: z.string().min(1),
|
|
529
1008
|
label: z.string().min(1),
|
|
530
|
-
localizedData:
|
|
1009
|
+
localizedData: nonEmptyLocalizedDataSchema.optional()
|
|
531
1010
|
}).refine((value)=>Boolean(value.data || value.localizedData), {
|
|
532
1011
|
message: "Either data or localizedData is required."
|
|
533
1012
|
}),
|
|
534
1013
|
execute: async ({ collection, data, id, label, localizedData })=>{
|
|
535
1014
|
const permissionError = getDisallowedCollectionActionError(collection, "update");
|
|
536
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
|
+
}
|
|
537
1058
|
const proposal = {
|
|
538
1059
|
action: "update",
|
|
539
1060
|
collection,
|
|
540
|
-
...localizedData ? {
|
|
541
|
-
localizedData
|
|
1061
|
+
...preparedData.localizedData ? {
|
|
1062
|
+
localizedData: preparedData.localizedData
|
|
542
1063
|
} : {
|
|
543
|
-
data: data || {}
|
|
1064
|
+
data: preparedData.data || {}
|
|
544
1065
|
},
|
|
545
1066
|
id,
|
|
546
1067
|
label: getSafeProposalLabel(label),
|
|
@@ -556,22 +1077,47 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
556
1077
|
inputSchema: z.object({
|
|
557
1078
|
data: z.record(z.string(), z.unknown()).optional(),
|
|
558
1079
|
label: z.string().min(1),
|
|
559
|
-
localizedData:
|
|
1080
|
+
localizedData: nonEmptyLocalizedDataSchema.optional(),
|
|
560
1081
|
slug: z.string().min(1)
|
|
561
1082
|
}).refine((value)=>Boolean(value.data || value.localizedData), {
|
|
562
1083
|
message: "Either data or localizedData is required."
|
|
563
1084
|
}),
|
|
564
1085
|
execute: async ({ data, label, localizedData, slug })=>{
|
|
565
1086
|
const globalConfig = req.payload.config.globals?.find((global)=>global.slug === slug);
|
|
566
|
-
if (!globalConfig)
|
|
567
|
-
|
|
568
|
-
|
|
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
|
+
}
|
|
569
1115
|
const proposal = {
|
|
570
1116
|
action: "updateGlobal",
|
|
571
|
-
...localizedData ? {
|
|
572
|
-
localizedData
|
|
1117
|
+
...preparedData.localizedData ? {
|
|
1118
|
+
localizedData: preparedData.localizedData
|
|
573
1119
|
} : {
|
|
574
|
-
data: data || {}
|
|
1120
|
+
data: preparedData.data || {}
|
|
575
1121
|
},
|
|
576
1122
|
label: getSafeProposalLabel(label),
|
|
577
1123
|
...activeLocale ? {
|
|
@@ -638,12 +1184,22 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
638
1184
|
"You are a Payload CMS assistant. Inspect schema/content with tools before proposing writes.",
|
|
639
1185
|
"Mentions define the active CMS scope. Locale mentions define active locale; multiple locales require localizedData keyed by locale.",
|
|
640
1186
|
"Writes are proposals only. Never claim changes were applied before user confirmation.",
|
|
641
|
-
"For create/update/delete use proposal tools.
|
|
642
|
-
"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)}.`,
|
|
643
1195
|
`Focused required create fields: ${JSON.stringify(focusedRequiredFieldsByCollection)}.`,
|
|
644
1196
|
`Focused title fields: ${JSON.stringify(focusedTitleFieldByCollection)}. Infer concise titles when needed.`,
|
|
1197
|
+
`Preferred proposal tool for this prompt: ${intentToolChoice?.toolName || "none"}.`,
|
|
645
1198
|
"Visible response: plain text, under 40 words, no Markdown, no proposed content."
|
|
646
1199
|
].join("\n"),
|
|
1200
|
+
...intentToolChoice ? {
|
|
1201
|
+
toolChoice: intentToolChoice
|
|
1202
|
+
} : {},
|
|
647
1203
|
tools
|
|
648
1204
|
});
|
|
649
1205
|
const stream = new ReadableStream({
|
|
@@ -668,19 +1224,75 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
668
1224
|
if (part.type === "finish") {
|
|
669
1225
|
const finishPart = part;
|
|
670
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
|
+
});
|
|
671
1254
|
sendEvent(controller, "proposals", {
|
|
672
1255
|
proposals,
|
|
673
1256
|
usage
|
|
674
1257
|
});
|
|
1258
|
+
sendEvent(controller, "debug", debugPayload);
|
|
675
1259
|
sendEvent(controller, "done", {});
|
|
676
1260
|
didSendTerminalEvent = true;
|
|
677
1261
|
}
|
|
678
1262
|
}
|
|
679
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
|
+
});
|
|
680
1291
|
sendEvent(controller, "proposals", {
|
|
681
1292
|
proposals,
|
|
682
1293
|
usage
|
|
683
1294
|
});
|
|
1295
|
+
sendEvent(controller, "debug", debugPayload);
|
|
684
1296
|
sendEvent(controller, "done", {});
|
|
685
1297
|
}
|
|
686
1298
|
} catch (err) {
|