@mvriu5/payload-ai 0.5.8 → 0.5.10

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.
Files changed (65) hide show
  1. package/README.md +62 -6
  2. package/dist/ai/{proposals.d.ts → proposalSigning.d.ts} +1 -1
  3. package/dist/ai/{proposals.js → proposalSigning.js} +6 -11
  4. package/dist/ai/providerOptions.d.ts +17 -1
  5. package/dist/ai/providerOptions.js +35 -7
  6. package/dist/ai/providerRuntime.d.ts +2 -2
  7. package/dist/ai/providerRuntime.js +50 -16
  8. package/dist/ai/sensitiveData.js +2 -6
  9. package/dist/components/AIInput.d.ts +2 -1
  10. package/dist/components/AIInput.js +327 -41
  11. package/dist/components/AIInput.module.css +41 -6
  12. package/dist/components/APIKeyField.d.ts +3 -0
  13. package/dist/components/{AIApiKeyField.js → APIKeyField.js} +3 -4
  14. package/dist/components/ActionToast.d.ts +30 -0
  15. package/dist/components/{AIActionProposalList.js → ActionToast.js} +78 -8
  16. package/dist/components/{AIActionProposalList.module.css → ActionToast.module.css} +49 -5
  17. package/dist/components/AuditLogList.d.ts +32 -0
  18. package/dist/components/AuditLogList.js +67 -0
  19. package/dist/components/AuditLogList.module.css +152 -0
  20. package/dist/components/DiffDialog.d.ts +19 -0
  21. package/dist/components/DiffDialog.js +396 -0
  22. package/dist/components/DiffDialog.module.css +333 -0
  23. package/dist/components/Icons.d.ts +9 -0
  24. package/dist/components/Icons.js +193 -0
  25. package/dist/components/MentionPopover.d.ts +19 -0
  26. package/dist/components/{CollectionMentionPopover.js → MentionPopover.js} +11 -4
  27. package/dist/components/{CollectionMentionPopover.module.css → MentionPopover.module.css} +0 -2
  28. package/dist/components/hooks/useAISettings.d.ts +6 -4
  29. package/dist/components/hooks/useAISettings.js +18 -3
  30. package/dist/components/hooks/useDocumentMentionSuggestions.d.ts +3 -3
  31. package/dist/components/hooks/useDocumentMentionSuggestions.js +2 -2
  32. package/dist/exports/client.d.ts +4 -2
  33. package/dist/exports/client.js +4 -2
  34. package/dist/handlers/applyActionHandler.d.ts +8 -0
  35. package/dist/handlers/applyActionHandler.js +563 -0
  36. package/dist/handlers/auditLogHandler.d.ts +6 -0
  37. package/dist/handlers/auditLogHandler.js +39 -0
  38. package/dist/handlers/chatHandler.d.ts +42 -0
  39. package/dist/handlers/chatHandler.js +634 -0
  40. package/dist/handlers/mentionSuggestionHandler.d.ts +7 -0
  41. package/dist/handlers/mentionSuggestionHandler.js +97 -0
  42. package/dist/handlers/proposalDiffHandler.d.ts +7 -0
  43. package/dist/handlers/proposalDiffHandler.js +208 -0
  44. package/dist/index.d.ts +6 -6
  45. package/dist/index.js +151 -8
  46. package/dist/payload/collectionPermissions.d.ts +14 -13
  47. package/dist/payload/collectionPermissions.js +3 -2
  48. package/dist/payload/normalizeData.d.ts +8 -0
  49. package/dist/payload/normalizeData.js +25 -3
  50. package/dist/payload/schemaContext.d.ts +79 -9
  51. package/dist/payload/schemaContext.js +231 -22
  52. package/dist/payload/shared.d.ts +33 -0
  53. package/dist/payload/shared.js +114 -0
  54. package/package.json +7 -2
  55. package/dist/components/AIActionProposalList.d.ts +0 -25
  56. package/dist/components/AIApiKeyField.d.ts +0 -2
  57. package/dist/components/CollectionMentionPopover.d.ts +0 -16
  58. package/dist/components/hooks/utils.d.ts +0 -1
  59. package/dist/components/hooks/utils.js +0 -3
  60. package/dist/endpoints/aiApplyActionEndpointHandler.d.ts +0 -7
  61. package/dist/endpoints/aiApplyActionEndpointHandler.js +0 -169
  62. package/dist/endpoints/aiChatEndpointHandler.d.ts +0 -35
  63. package/dist/endpoints/aiChatEndpointHandler.js +0 -321
  64. package/dist/endpoints/aiMentionSuggestionsEndpointHandler.d.ts +0 -7
  65. package/dist/endpoints/aiMentionSuggestionsEndpointHandler.js +0 -63
@@ -0,0 +1,42 @@
1
+ import type { PayloadHandler } from "payload";
2
+ import { type AIActionSignature } from "../ai/proposalSigning.js";
3
+ import { type AIModelConfig } from "../ai/providerOptions.js";
4
+ import { type ResolvedCollectionPermissionMap } from "../payload/collectionPermissions.js";
5
+ type LocalizedDataInput = Record<string, Record<string, unknown>>;
6
+ type ProposalWritePayload = {
7
+ data: Record<string, unknown>;
8
+ localizedData?: never;
9
+ } | {
10
+ data?: never;
11
+ localizedData: LocalizedDataInput;
12
+ };
13
+ export type ActionProposal = (({
14
+ action: "create";
15
+ collection: string;
16
+ label: string;
17
+ } & ProposalWritePayload) | {
18
+ action: "delete";
19
+ collection: string;
20
+ id: string;
21
+ label: string;
22
+ } | ({
23
+ action: "update";
24
+ collection: string;
25
+ id: string;
26
+ label: string;
27
+ } & ProposalWritePayload) | ({
28
+ action: "updateGlobal";
29
+ label: string;
30
+ slug: string;
31
+ } & ProposalWritePayload)) & {
32
+ _aiSignature?: AIActionSignature;
33
+ locale?: string;
34
+ };
35
+ type ChatOptions = {
36
+ allowUserApiKeys?: boolean;
37
+ collections?: ResolvedCollectionPermissionMap;
38
+ maxOutputTokens?: number;
39
+ models?: AIModelConfig;
40
+ };
41
+ export declare const createChatHandler: (options?: ChatOptions) => PayloadHandler;
42
+ export {};
@@ -0,0 +1,634 @@
1
+ import { stepCountIs, streamText } from "ai";
2
+ import { z } from "zod";
3
+ import { signAIActionProposal } from "../ai/proposalSigning.js";
4
+ import { isAIProvider } from "../ai/providerOptions.js";
5
+ import { getModel, getProviderConfig } from "../ai/providerRuntime.js";
6
+ import { containsSensitiveData } from "../ai/sensitiveData.js";
7
+ import { isCollectionActionAllowed } from "../payload/collectionPermissions.js";
8
+ import { buildPromptWithMentionContext, collectBlocks, describeCollectionLikeConfig, describeCollectionLikeSummary, getAllowedCollectionSlugs, getMentionContext } from "../payload/schemaContext.js";
9
+ import { getOptionValue, getSafeProposalLabel, hasLocalizedData, hasValueAtPath, setValueAtPath } from "../payload/shared.js";
10
+ const getRequiredFieldInfos = (fields, titleFieldName, path = "")=>{
11
+ return fields.flatMap((field)=>{
12
+ const fieldPath = field.name ? path ? `${path}.${field.name}` : field.name : path;
13
+ const requiredField = field.required && fieldPath ? [
14
+ {
15
+ defaultValue: field.defaultValue,
16
+ isTitleField: field.name === titleFieldName,
17
+ localized: Boolean(field.localized),
18
+ options: field.options,
19
+ path: fieldPath,
20
+ type: field.type
21
+ }
22
+ ] : [];
23
+ const nestedFields = field.fields?.length ? getRequiredFieldInfos(field.fields, titleFieldName, fieldPath) : [];
24
+ const nestedBlocks = field.blocks?.length ? field.blocks.flatMap((block)=>getRequiredFieldInfos(block.fields || [], titleFieldName, `${fieldPath}.${block.slug}`)) : [];
25
+ return [
26
+ ...requiredField,
27
+ ...nestedFields,
28
+ ...nestedBlocks
29
+ ];
30
+ });
31
+ };
32
+ const getCreateFallbackValue = ({ field, label })=>{
33
+ if (field.defaultValue !== undefined) return field.defaultValue;
34
+ if (field.path === "_status") return "draft";
35
+ if (field.type === "checkbox") return false;
36
+ if (field.type === "select" || field.type === "radio") {
37
+ return getOptionValue(field.options?.[0]);
38
+ }
39
+ const terminalSegment = field.path.split(".").at(-1)?.toLowerCase();
40
+ if ((field.isTitleField || [
41
+ "title",
42
+ "name",
43
+ "label",
44
+ "headline"
45
+ ].includes(terminalSegment || "")) && [
46
+ "text",
47
+ "textarea"
48
+ ].includes(field.type || "")) {
49
+ return getSafeProposalLabel(label);
50
+ }
51
+ return undefined;
52
+ };
53
+ const fillMissingCreateFields = ({ data, label, localizedData, requiredFields })=>{
54
+ if (localizedData) {
55
+ const completedLocalizedData = Object.fromEntries(Object.entries(localizedData).map(([locale, localeData])=>[
56
+ locale,
57
+ {
58
+ ...localeData
59
+ }
60
+ ]));
61
+ const localeEntries = Object.entries(completedLocalizedData);
62
+ const [firstLocale, firstLocaleData] = localeEntries[0] || [];
63
+ for (const [locale, localeData] of localeEntries){
64
+ for (const field of requiredFields.filter((item)=>item.localized)){
65
+ if (hasValueAtPath(localeData, field.path)) continue;
66
+ const fallbackValue = getCreateFallbackValue({
67
+ field,
68
+ label
69
+ });
70
+ if (fallbackValue !== undefined) {
71
+ setValueAtPath(localeData, field.path, fallbackValue);
72
+ }
73
+ }
74
+ }
75
+ if (firstLocale && firstLocaleData) {
76
+ for (const field of requiredFields.filter((item)=>!item.localized)){
77
+ if (hasValueAtPath(firstLocaleData, field.path)) continue;
78
+ const fallbackValue = getCreateFallbackValue({
79
+ field,
80
+ label
81
+ });
82
+ if (fallbackValue !== undefined) {
83
+ setValueAtPath(firstLocaleData, field.path, fallbackValue);
84
+ }
85
+ }
86
+ }
87
+ return {
88
+ localizedData: completedLocalizedData
89
+ };
90
+ }
91
+ const completedData = {
92
+ ...data || {}
93
+ };
94
+ for (const field of requiredFields){
95
+ if (hasValueAtPath(completedData, field.path)) continue;
96
+ const fallbackValue = getCreateFallbackValue({
97
+ field,
98
+ label
99
+ });
100
+ if (fallbackValue !== undefined) {
101
+ setValueAtPath(completedData, field.path, fallbackValue);
102
+ }
103
+ }
104
+ return {
105
+ data: completedData
106
+ };
107
+ };
108
+ const getMissingCreateFields = ({ data, localizedData, requiredFields })=>{
109
+ if (localizedData) {
110
+ const locales = Object.entries(localizedData);
111
+ const firstLocale = locales[0];
112
+ if (!firstLocale) {
113
+ return [
114
+ "localizedData must include at least one locale entry"
115
+ ];
116
+ }
117
+ const missing = [];
118
+ for (const [locale, localeData] of locales){
119
+ for (const field of requiredFields.filter((item)=>item.localized)){
120
+ if (!hasValueAtPath(localeData, field.path)) {
121
+ missing.push(`${locale}:${field.path}`);
122
+ }
123
+ }
124
+ }
125
+ for (const field of requiredFields.filter((item)=>!item.localized)){
126
+ if (!hasValueAtPath(firstLocale[1], field.path)) {
127
+ missing.push(`${firstLocale[0]}:${field.path}`);
128
+ }
129
+ }
130
+ return missing;
131
+ }
132
+ if (!data) return [
133
+ "data is required"
134
+ ];
135
+ return requiredFields.filter((field)=>!hasValueAtPath(data, field.path)).map((field)=>field.path);
136
+ };
137
+ export const createChatHandler = (options = {})=>async (req)=>{
138
+ if (!req.user) return Response.json({
139
+ error: "Unauthorized"
140
+ }, {
141
+ status: 401
142
+ });
143
+ const body = req.json ? await req.json().catch(()=>null) : null;
144
+ const prompt = body?.prompt?.trim();
145
+ if (!prompt) return Response.json({
146
+ error: "Prompt is required"
147
+ }, {
148
+ status: 400
149
+ });
150
+ const user = req.user;
151
+ const requestedProvider = body?.provider || user.aiProvider || "openai";
152
+ if (!isAIProvider(requestedProvider)) return Response.json({
153
+ error: `Unsupported AI provider: ${requestedProvider}`
154
+ }, {
155
+ status: 400
156
+ });
157
+ const provider = requestedProvider;
158
+ const userApiKey = options.allowUserApiKeys === false ? null : user.aiApiKey;
159
+ const providerConfig = getProviderConfig({
160
+ apiKey: userApiKey,
161
+ defaultModels: options.models?.defaults,
162
+ model: body?.model,
163
+ provider
164
+ });
165
+ const debug = {
166
+ model: providerConfig.modelID,
167
+ provider,
168
+ tools: [
169
+ "getDoc",
170
+ "getGlobal",
171
+ "listCollections",
172
+ "listGlobals",
173
+ "proposeCreateDoc",
174
+ "proposeDeleteDoc",
175
+ "proposeUpdateDoc",
176
+ "proposeUpdateGlobal",
177
+ "searchDocs"
178
+ ]
179
+ };
180
+ if (!providerConfig.apiKey) {
181
+ return Response.json({
182
+ error: options.allowUserApiKeys === false ? `Configure a ${provider} API key in the server environment first.` : `Add a ${provider} API key to your account settings or configure it in the server environment first.`
183
+ }, {
184
+ status: 400
185
+ });
186
+ }
187
+ try {
188
+ const proposals = [];
189
+ const addSignedProposal = (proposal)=>{
190
+ if ("data" in proposal && proposal.data && containsSensitiveData(proposal.data)) {
191
+ return {
192
+ error: "Proposal contains sensitive fields and cannot be created."
193
+ };
194
+ }
195
+ if ("localizedData" in proposal && hasLocalizedData(proposal.localizedData) && Object.values(proposal.localizedData).some((value)=>containsSensitiveData(value))) {
196
+ return {
197
+ error: "Proposal contains sensitive fields and cannot be created."
198
+ };
199
+ }
200
+ const signedProposal = signAIActionProposal(proposal);
201
+ proposals.push(signedProposal);
202
+ return signedProposal;
203
+ };
204
+ const collectionSlugs = getAllowedCollectionSlugs(req, options.collections);
205
+ const globalSlugs = req.payload.config.globals?.map((global)=>global.slug) || [];
206
+ const allowedCollections = req.payload.config.collections.filter((collection)=>collectionSlugs.includes(collection.slug));
207
+ if (collectionSlugs.length === 0) {
208
+ return Response.json({
209
+ error: "No AI-enabled collections are configured."
210
+ }, {
211
+ status: 400
212
+ });
213
+ }
214
+ const blockContexts = [
215
+ ...allowedCollections.flatMap((collection)=>collectBlocks({
216
+ fields: collection.fields,
217
+ parent: collection.slug
218
+ })),
219
+ ...req.payload.config.globals?.flatMap((global)=>collectBlocks({
220
+ fields: global.fields,
221
+ parent: global.slug
222
+ })) || []
223
+ ];
224
+ const mentionContext = await getMentionContext({
225
+ blockContexts,
226
+ collectionSlugs,
227
+ collections: options.collections,
228
+ globalSlugs,
229
+ mentions: body?.mentions,
230
+ req
231
+ });
232
+ const selectedLocales = (body?.mentions?.filter((mention)=>mention.type === "locale" && mention.slug).map((mention)=>mention.slug) || []).filter((locale, index, array)=>array.indexOf(locale) === index);
233
+ const activeLocale = selectedLocales.at(-1);
234
+ const mentionedCollectionSlugs = (body?.mentions?.flatMap((mention)=>{
235
+ if (mention.type === "collection" && mention.slug) return [
236
+ mention.slug
237
+ ];
238
+ if (mention.type === "doc" && mention.collection) return [
239
+ mention.collection
240
+ ];
241
+ return [];
242
+ }).filter((slug)=>collectionSlugs.includes(slug)) || []).filter((slug, index, array)=>array.indexOf(slug) === index);
243
+ const createRequiredFieldsByCollection = Object.fromEntries(allowedCollections.map((collection)=>[
244
+ collection.slug,
245
+ getRequiredFieldInfos(collection.fields, collection.admin?.useAsTitle)
246
+ ]));
247
+ const titleFieldByCollection = Object.fromEntries(allowedCollections.flatMap((collection)=>collection.admin?.useAsTitle ? [
248
+ [
249
+ collection.slug,
250
+ collection.admin.useAsTitle
251
+ ]
252
+ ] : []));
253
+ const focusedRequiredFieldsByCollection = Object.fromEntries(mentionedCollectionSlugs.map((slug)=>[
254
+ slug,
255
+ createRequiredFieldsByCollection[slug] || []
256
+ ]));
257
+ const focusedTitleFieldByCollection = Object.fromEntries(mentionedCollectionSlugs.flatMap((slug)=>titleFieldByCollection[slug] ? [
258
+ [
259
+ slug,
260
+ titleFieldByCollection[slug]
261
+ ]
262
+ ] : []));
263
+ const collectionSlugSchema = z.enum(collectionSlugs);
264
+ const getDisallowedCollectionActionError = (collection, action)=>{
265
+ if (isCollectionActionAllowed({
266
+ action,
267
+ permissions: options.collections,
268
+ req,
269
+ slug: collection
270
+ })) return null;
271
+ return {
272
+ error: `${action} is not enabled for collection: ${collection}`
273
+ };
274
+ };
275
+ const tools = {
276
+ getDoc: {
277
+ description: "Read one document by collection slug and document id.",
278
+ inputSchema: z.object({
279
+ collection: collectionSlugSchema,
280
+ id: z.string().min(1)
281
+ }),
282
+ execute: async ({ collection, id })=>{
283
+ return req.payload.findByID({
284
+ collection: collection,
285
+ depth: 2,
286
+ id,
287
+ ...activeLocale ? {
288
+ locale: activeLocale
289
+ } : {},
290
+ overrideAccess: false,
291
+ req
292
+ });
293
+ }
294
+ },
295
+ listCollections: {
296
+ description: "List AI-enabled Payload collections. Omit slug for compact summaries; pass slug to get full field schema for one collection.",
297
+ inputSchema: z.object({
298
+ slug: collectionSlugSchema.optional()
299
+ }),
300
+ execute: async ({ slug })=>{
301
+ if (slug) {
302
+ const collection = allowedCollections.find((item)=>item.slug === slug);
303
+ if (!collection) return {
304
+ error: `Unknown collection: ${slug}`
305
+ };
306
+ return describeCollectionLikeConfig({
307
+ config: collection,
308
+ permissions: options.collections,
309
+ type: "collection"
310
+ });
311
+ }
312
+ return allowedCollections.map((collection)=>describeCollectionLikeSummary({
313
+ config: collection,
314
+ permissions: options.collections,
315
+ type: "collection"
316
+ }));
317
+ }
318
+ },
319
+ getGlobal: {
320
+ description: "Read one Payload CMS global by slug.",
321
+ inputSchema: z.object({
322
+ slug: z.string().min(1)
323
+ }),
324
+ execute: async ({ slug })=>{
325
+ const globalConfig = req.payload.config.globals?.find((global)=>global.slug === slug);
326
+ if (!globalConfig) return {
327
+ error: `Unknown global: ${slug}`
328
+ };
329
+ return req.payload.findGlobal({
330
+ depth: 2,
331
+ ...activeLocale ? {
332
+ locale: activeLocale
333
+ } : {},
334
+ overrideAccess: false,
335
+ req,
336
+ slug: slug
337
+ });
338
+ }
339
+ },
340
+ listGlobals: {
341
+ description: "List Payload globals. Omit slug for compact summaries; pass slug to get full field schema for one global.",
342
+ inputSchema: z.object({
343
+ slug: z.string().optional()
344
+ }),
345
+ execute: async ({ slug })=>{
346
+ const globals = req.payload.config.globals || [];
347
+ if (slug) {
348
+ const global = globals.find((item)=>item.slug === slug);
349
+ if (!global) return {
350
+ error: `Unknown global: ${slug}`
351
+ };
352
+ return describeCollectionLikeConfig({
353
+ config: global,
354
+ type: "global"
355
+ });
356
+ }
357
+ return globals.map((global)=>describeCollectionLikeSummary({
358
+ config: global,
359
+ type: "global"
360
+ }));
361
+ }
362
+ },
363
+ proposeCreateDoc: {
364
+ description: "Prepare a CMS document creation proposal. This does not write to the database. Use exact field names from listCollections. Include every required field for the target collection. For localizedData, include every localized required field in every locale entry, and include non-localized required fields in the first locale entry. For array fields, provide arrays of objects matching their child fields. For richText fields, prefer plain text or omit if unsure. Use localizedData when writing multiple locales in one proposal.",
365
+ inputSchema: z.object({
366
+ collection: collectionSlugSchema,
367
+ data: z.record(z.string(), z.unknown()).optional(),
368
+ label: z.string().min(1),
369
+ localizedData: z.record(z.string(), z.record(z.string(), z.unknown())).optional()
370
+ }).refine((value)=>Boolean(value.data || value.localizedData), {
371
+ message: "Either data or localizedData is required."
372
+ }),
373
+ execute: async ({ collection, data, label, localizedData })=>{
374
+ const permissionError = getDisallowedCollectionActionError(collection, "create");
375
+ if (permissionError) return permissionError;
376
+ const completedCreatePayload = fillMissingCreateFields({
377
+ data,
378
+ label,
379
+ localizedData,
380
+ requiredFields: createRequiredFieldsByCollection[collection] || []
381
+ });
382
+ const missingFields = getMissingCreateFields({
383
+ data: completedCreatePayload.data,
384
+ localizedData: completedCreatePayload.localizedData,
385
+ requiredFields: createRequiredFieldsByCollection[collection] || []
386
+ });
387
+ if (missingFields.length > 0) {
388
+ const titleFieldName = titleFieldByCollection[collection];
389
+ const missingTitleField = titleFieldName ? missingFields.some((field)=>field === titleFieldName || field.endsWith(`:${titleFieldName}`)) : false;
390
+ return {
391
+ error: missingTitleField ? `Create proposal is missing the required title field "${titleFieldName}" for ${collection}. Infer a concise title from the user request and retry.` : `Create proposal is missing required fields for ${collection}: ${missingFields.join(", ")}`
392
+ };
393
+ }
394
+ const proposal = completedCreatePayload.localizedData ? {
395
+ action: "create",
396
+ collection,
397
+ label: getSafeProposalLabel(label),
398
+ localizedData: completedCreatePayload.localizedData,
399
+ ...activeLocale ? {
400
+ locale: activeLocale
401
+ } : {}
402
+ } : {
403
+ action: "create",
404
+ collection,
405
+ data: completedCreatePayload.data || {},
406
+ label: getSafeProposalLabel(label),
407
+ ...activeLocale ? {
408
+ locale: activeLocale
409
+ } : {}
410
+ };
411
+ return addSignedProposal(proposal);
412
+ }
413
+ },
414
+ proposeDeleteDoc: {
415
+ description: "Prepare a CMS document deletion proposal. This does not write to the database.",
416
+ inputSchema: z.object({
417
+ collection: collectionSlugSchema,
418
+ id: z.string().min(1),
419
+ label: z.string().min(1)
420
+ }),
421
+ execute: async ({ collection, id, label })=>{
422
+ const permissionError = getDisallowedCollectionActionError(collection, "delete");
423
+ if (permissionError) return permissionError;
424
+ const proposal = {
425
+ action: "delete",
426
+ collection,
427
+ id,
428
+ label: getSafeProposalLabel(label),
429
+ ...activeLocale ? {
430
+ locale: activeLocale
431
+ } : {}
432
+ };
433
+ return addSignedProposal(proposal);
434
+ }
435
+ },
436
+ proposeUpdateDoc: {
437
+ description: "Prepare a CMS document update proposal. This does not write to the database. Use exact field names from listCollections. For array fields, provide arrays of objects matching their child fields. For richText fields, prefer plain text or omit if unsure. Use localizedData when writing multiple locales in one proposal.",
438
+ inputSchema: z.object({
439
+ collection: collectionSlugSchema,
440
+ data: z.record(z.string(), z.unknown()).optional(),
441
+ id: z.string().min(1),
442
+ label: z.string().min(1),
443
+ localizedData: z.record(z.string(), z.record(z.string(), z.unknown())).optional()
444
+ }).refine((value)=>Boolean(value.data || value.localizedData), {
445
+ message: "Either data or localizedData is required."
446
+ }),
447
+ execute: async ({ collection, data, id, label, localizedData })=>{
448
+ const permissionError = getDisallowedCollectionActionError(collection, "update");
449
+ if (permissionError) return permissionError;
450
+ const proposal = {
451
+ action: "update",
452
+ collection,
453
+ ...localizedData ? {
454
+ localizedData
455
+ } : {
456
+ data: data || {}
457
+ },
458
+ id,
459
+ label: getSafeProposalLabel(label),
460
+ ...activeLocale ? {
461
+ locale: activeLocale
462
+ } : {}
463
+ };
464
+ return addSignedProposal(proposal);
465
+ }
466
+ },
467
+ proposeUpdateGlobal: {
468
+ description: "Prepare a Payload global update proposal. This does not write to the database. Use localizedData when writing multiple locales in one proposal.",
469
+ inputSchema: z.object({
470
+ data: z.record(z.string(), z.unknown()).optional(),
471
+ label: z.string().min(1),
472
+ localizedData: z.record(z.string(), z.record(z.string(), z.unknown())).optional(),
473
+ slug: z.string().min(1)
474
+ }).refine((value)=>Boolean(value.data || value.localizedData), {
475
+ message: "Either data or localizedData is required."
476
+ }),
477
+ execute: async ({ data, label, localizedData, slug })=>{
478
+ const globalConfig = req.payload.config.globals?.find((global)=>global.slug === slug);
479
+ if (!globalConfig) return {
480
+ error: `Unknown global: ${slug}`
481
+ };
482
+ const proposal = {
483
+ action: "updateGlobal",
484
+ ...localizedData ? {
485
+ localizedData
486
+ } : {
487
+ data: data || {}
488
+ },
489
+ label: getSafeProposalLabel(label),
490
+ ...activeLocale ? {
491
+ locale: activeLocale
492
+ } : {},
493
+ slug
494
+ };
495
+ return addSignedProposal(proposal);
496
+ }
497
+ },
498
+ searchDocs: {
499
+ description: "Search documents in one collection. Use query for a loose text search where possible.",
500
+ inputSchema: z.object({
501
+ collection: collectionSlugSchema,
502
+ limit: z.number().int().min(1).max(10).default(5),
503
+ query: z.string().optional()
504
+ }),
505
+ execute: async ({ collection, limit, query })=>{
506
+ const collectionConfig = allowedCollections.find((item)=>item.slug === collection);
507
+ const searchableFields = collectionConfig?.fields.filter((field)=>"name" in field && [
508
+ "email",
509
+ "text",
510
+ "textarea"
511
+ ].includes(field.type)).map((field)=>"name" in field ? field.name : null).filter(Boolean) || [];
512
+ const where = query && searchableFields.length > 0 ? {
513
+ or: searchableFields.map((field)=>({
514
+ [field]: {
515
+ contains: query
516
+ }
517
+ }))
518
+ } : undefined;
519
+ return req.payload.find({
520
+ collection: collection,
521
+ depth: 1,
522
+ limit,
523
+ ...activeLocale ? {
524
+ locale: activeLocale
525
+ } : {},
526
+ overrideAccess: false,
527
+ req,
528
+ where
529
+ });
530
+ }
531
+ }
532
+ };
533
+ const encoder = new TextEncoder();
534
+ const sendEvent = (controller, event, data)=>{
535
+ controller.enqueue(encoder.encode(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`));
536
+ };
537
+ const model = await getModel({
538
+ apiKey: providerConfig.apiKey,
539
+ model: providerConfig.modelID,
540
+ provider
541
+ });
542
+ const result = streamText({
543
+ maxOutputTokens: options.maxOutputTokens || 700,
544
+ model,
545
+ prompt: buildPromptWithMentionContext({
546
+ mentionContext,
547
+ prompt
548
+ }),
549
+ stopWhen: stepCountIs(6),
550
+ system: [
551
+ "You are a Payload CMS assistant. Inspect schema/content with tools before proposing writes.",
552
+ "Mentions define the active CMS scope. Locale mentions define active locale; multiple locales require localizedData keyed by locale.",
553
+ "Writes are proposals only. Never claim changes were applied before user confirmation.",
554
+ "For create/update/delete use proposal tools. Put concrete field values only in tool data, not visible text.",
555
+ "If schema details are missing, call listCollections/listGlobals with a slug before proposing.",
556
+ `Focused required create fields: ${JSON.stringify(focusedRequiredFieldsByCollection)}.`,
557
+ `Focused title fields: ${JSON.stringify(focusedTitleFieldByCollection)}. Infer concise titles when needed.`,
558
+ "Visible response: plain text, under 40 words, no Markdown, no proposed content."
559
+ ].join("\n"),
560
+ tools
561
+ });
562
+ const stream = new ReadableStream({
563
+ start: async (controller)=>{
564
+ let didSendTerminalEvent = false;
565
+ let usage = null;
566
+ try {
567
+ for await (const part of result.fullStream){
568
+ if (part.type === "text-delta") {
569
+ sendEvent(controller, "text", {
570
+ delta: part.text
571
+ });
572
+ continue;
573
+ }
574
+ if (part.type === "error") {
575
+ didSendTerminalEvent = true;
576
+ sendEvent(controller, "error", {
577
+ error: "AI request failed."
578
+ });
579
+ break;
580
+ }
581
+ if (part.type === "finish") {
582
+ const finishPart = part;
583
+ usage = finishPart.totalUsage || finishPart.usage || null;
584
+ sendEvent(controller, "proposals", {
585
+ proposals,
586
+ usage
587
+ });
588
+ sendEvent(controller, "done", {});
589
+ didSendTerminalEvent = true;
590
+ }
591
+ }
592
+ if (!didSendTerminalEvent) {
593
+ sendEvent(controller, "proposals", {
594
+ proposals,
595
+ usage
596
+ });
597
+ sendEvent(controller, "done", {});
598
+ }
599
+ } catch (err) {
600
+ req.payload.logger.error({
601
+ debug,
602
+ err,
603
+ msg: "AI chat stream failed"
604
+ });
605
+ if (!didSendTerminalEvent) {
606
+ sendEvent(controller, "error", {
607
+ error: "AI request failed."
608
+ });
609
+ }
610
+ } finally{
611
+ controller.close();
612
+ }
613
+ }
614
+ });
615
+ return new Response(stream, {
616
+ headers: {
617
+ "Cache-Control": "no-cache, no-transform",
618
+ Connection: "keep-alive",
619
+ "Content-Type": "text/event-stream; charset=utf-8"
620
+ }
621
+ });
622
+ } catch (err) {
623
+ req.payload.logger.error({
624
+ debug,
625
+ err,
626
+ msg: "AI chat request failed"
627
+ });
628
+ return Response.json({
629
+ error: "AI request failed."
630
+ }, {
631
+ status: 500
632
+ });
633
+ }
634
+ };
@@ -0,0 +1,7 @@
1
+ import type { PayloadHandler } from "payload";
2
+ import { type ResolvedCollectionPermissionMap } from "../payload/collectionPermissions.js";
3
+ type MentionSuggestionsOptions = {
4
+ collections?: ResolvedCollectionPermissionMap;
5
+ };
6
+ export declare const createMentionSuggestionHandler: (options?: MentionSuggestionsOptions) => PayloadHandler;
7
+ export {};