@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.
- package/README.md +62 -6
- package/dist/ai/{proposals.d.ts → proposalSigning.d.ts} +1 -1
- package/dist/ai/{proposals.js → proposalSigning.js} +6 -11
- package/dist/ai/providerOptions.d.ts +17 -1
- package/dist/ai/providerOptions.js +35 -7
- package/dist/ai/providerRuntime.d.ts +2 -2
- package/dist/ai/providerRuntime.js +50 -16
- package/dist/ai/sensitiveData.js +2 -6
- package/dist/components/AIInput.d.ts +2 -1
- package/dist/components/AIInput.js +327 -41
- package/dist/components/AIInput.module.css +41 -6
- package/dist/components/APIKeyField.d.ts +3 -0
- package/dist/components/{AIApiKeyField.js → APIKeyField.js} +3 -4
- package/dist/components/ActionToast.d.ts +30 -0
- package/dist/components/{AIActionProposalList.js → ActionToast.js} +78 -8
- package/dist/components/{AIActionProposalList.module.css → ActionToast.module.css} +49 -5
- package/dist/components/AuditLogList.d.ts +32 -0
- package/dist/components/AuditLogList.js +67 -0
- package/dist/components/AuditLogList.module.css +152 -0
- package/dist/components/DiffDialog.d.ts +19 -0
- package/dist/components/DiffDialog.js +396 -0
- package/dist/components/DiffDialog.module.css +333 -0
- package/dist/components/Icons.d.ts +9 -0
- package/dist/components/Icons.js +193 -0
- package/dist/components/MentionPopover.d.ts +19 -0
- package/dist/components/{CollectionMentionPopover.js → MentionPopover.js} +11 -4
- package/dist/components/{CollectionMentionPopover.module.css → MentionPopover.module.css} +0 -2
- package/dist/components/hooks/useAISettings.d.ts +6 -4
- package/dist/components/hooks/useAISettings.js +18 -3
- package/dist/components/hooks/useDocumentMentionSuggestions.d.ts +3 -3
- package/dist/components/hooks/useDocumentMentionSuggestions.js +2 -2
- package/dist/exports/client.d.ts +4 -2
- package/dist/exports/client.js +4 -2
- package/dist/handlers/applyActionHandler.d.ts +8 -0
- package/dist/handlers/applyActionHandler.js +563 -0
- package/dist/handlers/auditLogHandler.d.ts +6 -0
- package/dist/handlers/auditLogHandler.js +39 -0
- package/dist/handlers/chatHandler.d.ts +42 -0
- package/dist/handlers/chatHandler.js +634 -0
- package/dist/handlers/mentionSuggestionHandler.d.ts +7 -0
- package/dist/handlers/mentionSuggestionHandler.js +97 -0
- package/dist/handlers/proposalDiffHandler.d.ts +7 -0
- package/dist/handlers/proposalDiffHandler.js +208 -0
- package/dist/index.d.ts +6 -6
- package/dist/index.js +151 -8
- package/dist/payload/collectionPermissions.d.ts +14 -13
- package/dist/payload/collectionPermissions.js +3 -2
- package/dist/payload/normalizeData.d.ts +8 -0
- package/dist/payload/normalizeData.js +25 -3
- package/dist/payload/schemaContext.d.ts +79 -9
- package/dist/payload/schemaContext.js +231 -22
- package/dist/payload/shared.d.ts +33 -0
- package/dist/payload/shared.js +114 -0
- package/package.json +7 -2
- package/dist/components/AIActionProposalList.d.ts +0 -25
- package/dist/components/AIApiKeyField.d.ts +0 -2
- package/dist/components/CollectionMentionPopover.d.ts +0 -16
- package/dist/components/hooks/utils.d.ts +0 -1
- package/dist/components/hooks/utils.js +0 -3
- package/dist/endpoints/aiApplyActionEndpointHandler.d.ts +0 -7
- package/dist/endpoints/aiApplyActionEndpointHandler.js +0 -169
- package/dist/endpoints/aiChatEndpointHandler.d.ts +0 -35
- package/dist/endpoints/aiChatEndpointHandler.js +0 -321
- package/dist/endpoints/aiMentionSuggestionsEndpointHandler.d.ts +0 -7
- 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 {};
|