@mvriu5/payload-ai 1.6.2 → 1.6.4
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/action-toast/ActionToast.d.ts +1 -14
- package/dist/components/action-toast/ActionToast.js +1 -1
- package/dist/components/ai-input/AIInput.js +1 -1
- package/dist/components/audit-log-list/AuditLogList.d.ts +2 -3
- package/dist/components/diff-dialog/DiffDialog.d.ts +2 -6
- package/dist/components/diff-dialog/DiffDialog.js +1 -1
- package/dist/components/generate-field/GenerateField.js +2 -0
- package/dist/components/hooks/useAIChatStream.d.ts +1 -1
- package/dist/components/hooks/useAISettings.d.ts +1 -1
- package/dist/components/hooks/useAISettings.js +1 -1
- package/dist/components/hooks/useDocumentMentionSuggestions.js +1 -1
- package/dist/components/hooks/useMentions.js +1 -1
- package/dist/components/hooks/usePluginConfig.d.ts +6 -6
- package/dist/components/hooks/usePluginConfig.js +1 -1
- package/dist/{payload → features}/collectionPermissions.js +1 -1
- package/dist/{payload/textFieldGeneration.d.ts → features/content/fieldGeneration.d.ts} +4 -0
- package/dist/{payload/textFieldGeneration.js → features/content/fieldGeneration.js} +19 -3
- package/dist/{ai → features}/promptCaching.d.ts +2 -2
- package/dist/features/proposals/containerNormalization.d.ts +8 -0
- package/dist/features/proposals/containerNormalization.js +112 -0
- package/dist/{payload/proposalData.d.ts → features/proposals/data.d.ts} +5 -10
- package/dist/features/proposals/data.js +113 -0
- package/dist/features/proposals/fieldNormalization.d.ts +2 -0
- package/dist/features/proposals/fieldNormalization.js +69 -0
- package/dist/features/proposals/normalizationTypes.d.ts +31 -0
- package/dist/features/proposals/normalizationTypes.js +1 -0
- package/dist/features/proposals/optionInference.d.ts +13 -0
- package/dist/features/proposals/optionInference.js +46 -0
- package/dist/features/proposals/recordNormalization.d.ts +6 -0
- package/dist/features/proposals/recordNormalization.js +78 -0
- package/dist/features/proposals/relationshipNormalization.d.ts +8 -0
- package/dist/features/proposals/relationshipNormalization.js +71 -0
- package/dist/{ai/proposalRepair.d.ts → features/proposals/repair.d.ts} +1 -1
- package/dist/{ai/proposalSigning.d.ts → features/proposals/signing.d.ts} +2 -5
- package/dist/{ai/proposalSigning.js → features/proposals/signing.js} +1 -1
- package/dist/features/proposals/target.d.ts +52 -0
- package/dist/features/proposals/target.js +125 -0
- package/dist/features/proposals/types.d.ts +49 -0
- package/dist/features/proposals/types.js +1 -0
- package/dist/features/proposals/validation.d.ts +12 -0
- package/dist/features/proposals/validation.js +26 -0
- package/dist/features/providers/requestContext.d.ts +53 -0
- package/dist/features/providers/requestContext.js +90 -0
- package/dist/{ai/providerRuntime.d.ts → features/providers/runtime.d.ts} +1 -1
- package/dist/{ai/providerRuntime.js → features/providers/runtime.js} +1 -1
- package/dist/features/schema/context.d.ts +7 -0
- package/dist/features/schema/context.js +11 -0
- package/dist/{payload/schemaContext.d.ts → features/schema/description.d.ts} +6 -33
- package/dist/{payload/schemaContext.js → features/schema/description.js} +3 -207
- package/dist/{payload/toolFieldSelection.d.ts → features/schema/fieldSelection.d.ts} +1 -1
- package/dist/{payload/toolFieldSelection.js → features/schema/fieldSelection.js} +2 -2
- package/dist/features/schema/mentionDocuments.d.ts +15 -0
- package/dist/features/schema/mentionDocuments.js +50 -0
- package/dist/features/schema/mentions.d.ts +18 -0
- package/dist/features/schema/mentions.js +100 -0
- package/dist/{payload/normalizeData.js → features/schema/normalize.js} +1 -1
- package/dist/features/schema/registry.d.ts +30 -0
- package/dist/features/schema/registry.js +73 -0
- package/dist/{payload → features/schema}/toolSchemas.d.ts +1 -1
- package/dist/{payload → features/schema}/toolSchemas.js +1 -1
- package/dist/{ai → features}/sensitiveData.js +1 -1
- package/dist/{ai → features}/tokenUsage.d.ts +1 -2
- package/dist/handlers/apply/audit.d.ts +42 -0
- package/dist/handlers/apply/audit.js +150 -0
- package/dist/handlers/apply/operations.d.ts +18 -0
- package/dist/handlers/apply/operations.js +377 -0
- package/dist/handlers/apply/types.d.ts +33 -0
- package/dist/handlers/apply/types.js +1 -0
- package/dist/handlers/apply/validation.d.ts +45 -0
- package/dist/handlers/apply/validation.js +125 -0
- package/dist/handlers/applyActionHandler.d.ts +1 -6
- package/dist/handlers/applyActionHandler.js +13 -757
- package/dist/handlers/auditLogHandler.js +4 -8
- package/dist/handlers/chat/intent.d.ts +32 -0
- package/dist/handlers/chat/intent.js +155 -0
- package/dist/handlers/chat/prompt.d.ts +20 -0
- package/dist/handlers/chat/prompt.js +55 -0
- package/dist/handlers/chat/proposalTools.d.ts +96 -0
- package/dist/handlers/chat/proposalTools.js +451 -0
- package/dist/handlers/chat/streaming.d.ts +38 -0
- package/dist/handlers/chat/streaming.js +136 -0
- package/dist/handlers/chat/toolFactory.d.ts +79 -0
- package/dist/handlers/chat/toolFactory.js +156 -0
- package/dist/handlers/chat/types.d.ts +57 -0
- package/dist/handlers/chat/types.js +1 -0
- package/dist/handlers/chatHandler.d.ts +1 -44
- package/dist/handlers/chatHandler.js +186 -1162
- package/dist/handlers/generateFieldHandler.d.ts +3 -8
- package/dist/handlers/generateFieldHandler.js +71 -99
- package/dist/handlers/http.d.ts +17 -0
- package/dist/handlers/http.js +21 -0
- package/dist/handlers/mediaUploadHandler.js +12 -34
- package/dist/handlers/mentionSuggestionHandler.d.ts +1 -1
- package/dist/handlers/mentionSuggestionHandler.js +6 -10
- package/dist/handlers/proposalDiffHandler.d.ts +1 -1
- package/dist/handlers/proposalDiffHandler.js +72 -195
- package/dist/handlers/translateDocumentHandler.d.ts +3 -8
- package/dist/handlers/translateDocumentHandler.js +26 -82
- package/dist/index.d.ts +4 -30
- package/dist/index.js +31 -421
- package/dist/plugin/admin.d.ts +30 -0
- package/dist/plugin/admin.js +155 -0
- package/dist/plugin/collections.d.ts +3 -0
- package/dist/plugin/collections.js +169 -0
- package/dist/plugin/endpoints.d.ts +23 -0
- package/dist/plugin/endpoints.js +84 -0
- package/dist/plugin/options.d.ts +4 -0
- package/dist/plugin/options.js +13 -0
- package/dist/plugin/types.d.ts +28 -0
- package/dist/plugin/types.js +1 -0
- package/dist/{payload/shared.d.ts → utils/data.d.ts} +2 -12
- package/package.json +1 -1
- package/dist/payload/proposalData.js +0 -578
- /package/dist/{payload → features}/collectionPermissions.d.ts +0 -0
- /package/dist/{payload → features/content}/documentTranslation.d.ts +0 -0
- /package/dist/{payload → features/content}/documentTranslation.js +0 -0
- /package/dist/{ai → features}/promptCaching.js +0 -0
- /package/dist/{ai/proposalRepair.js → features/proposals/repair.js} +0 -0
- /package/dist/{ai/providerOptions.d.ts → features/providers/options.d.ts} +0 -0
- /package/dist/{ai/providerOptions.js → features/providers/options.js} +0 -0
- /package/dist/{payload/normalizeData.d.ts → features/schema/normalize.d.ts} +0 -0
- /package/dist/{ai → features}/sensitiveData.d.ts +0 -0
- /package/dist/{ai → features}/tokenUsage.js +0 -0
- /package/dist/{payload/shared.js → utils/data.js} +0 -0
- /package/dist/{payload → utils}/logging.d.ts +0 -0
- /package/dist/{payload → utils}/logging.js +0 -0
|
@@ -1,568 +1,21 @@
|
|
|
1
1
|
import { stepCountIs, streamText } from "ai";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
3
|
+
import { compactProposalRepairIssues } from "../features/proposals/repair.js";
|
|
4
|
+
import { isSensitiveKey, redactSensitiveData } from "../features/sensitiveData.js";
|
|
5
|
+
import { resolveAIRequestContext } from "../features/providers/requestContext.js";
|
|
6
|
+
import { prepareProposalWriteData } from "../features/proposals/data.js";
|
|
7
|
+
import { buildPromptWithMentionContext, createSchemaContextRegistry, describeCollectionLikeConfig, describeCollectionLikeSummary, getAllowedCollectionSlugs, getMentionContext } from "../features/schema/context.js";
|
|
8
|
+
import { getLogPreview, logHandlerEvent } from "../utils/logging.js";
|
|
9
|
+
import { getSafeProposalLabel, isRecord } from "../utils/data.js";
|
|
10
|
+
import { createLocalizedPayloadDataSchema, createPayloadDataSchema, genericPayloadDataSchema } from "../features/schema/toolSchemas.js";
|
|
11
|
+
import { createToolFieldNamesSchema, resolveToolFieldSelection } from "../features/schema/fieldSelection.js";
|
|
12
|
+
import { createCollectionAliasMap, getChatIntent, getIntentToolChoice, getLikelyCollectionMatches, getToolNamesForIntent } from "./chat/intent.js";
|
|
13
|
+
import { createChatPromptContext } from "./chat/prompt.js";
|
|
14
|
+
import { createChatToolFactory } from "./chat/toolFactory.js";
|
|
15
|
+
import { jsonError, logHandlerError, readJSONBody, withAuthenticatedHandler } from "./http.js";
|
|
16
|
+
import { collectProposalBlockTypes, getCollectionBlockTypes, getProposalSummary, getRequestedBlockTypes, getRequiredFieldInfos, validateProposalReferences } from "./chat/proposalTools.js";
|
|
17
|
+
import { createDebugPayload, createE2EChatResponse, createInitialChatDebug, getChatCompletionReason } from "./chat/streaming.js";
|
|
17
18
|
const e2eModeEnabled = ()=>process.env.PAYLOAD_AI_E2E_MODE === "true";
|
|
18
|
-
const createSSEEventStream = (events)=>{
|
|
19
|
-
const encoder = new TextEncoder();
|
|
20
|
-
return new ReadableStream({
|
|
21
|
-
start (controller) {
|
|
22
|
-
for (const { data, event } of events){
|
|
23
|
-
controller.enqueue(encoder.encode(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`));
|
|
24
|
-
}
|
|
25
|
-
controller.close();
|
|
26
|
-
}
|
|
27
|
-
});
|
|
28
|
-
};
|
|
29
|
-
const getChatCompletionReason = ({ proposalCount, toolFailures, writeIntent })=>{
|
|
30
|
-
if (proposalCount > 0) return "proposal_created";
|
|
31
|
-
if (toolFailures.length > 0) return "tool_validation_failed";
|
|
32
|
-
if (writeIntent) return "write_intent_without_tool_call";
|
|
33
|
-
return "model_did_not_call_tool";
|
|
34
|
-
};
|
|
35
|
-
const createDebugPayload = ({ activeLocale, debug, proposalCount, selectedLocales, toolFailures, usage, writeIntent })=>({
|
|
36
|
-
activeLocale,
|
|
37
|
-
model: debug.model,
|
|
38
|
-
proposalCount,
|
|
39
|
-
provider: debug.provider,
|
|
40
|
-
reason: getChatCompletionReason({
|
|
41
|
-
proposalCount,
|
|
42
|
-
toolFailures,
|
|
43
|
-
writeIntent
|
|
44
|
-
}),
|
|
45
|
-
selectedLocales,
|
|
46
|
-
toolFailures,
|
|
47
|
-
usage
|
|
48
|
-
});
|
|
49
|
-
const createE2EChatResponse = ({ prompt, selectedLocales })=>{
|
|
50
|
-
const normalizedPrompt = prompt.toLowerCase();
|
|
51
|
-
const wantsCreatePost = normalizedPrompt.includes("post") && (normalizedPrompt.includes("create") || normalizedPrompt.includes("erstell") || normalizedPrompt.includes("apply flow") || normalizedPrompt.includes("locale review") || normalizedPrompt.includes("proposal review"));
|
|
52
|
-
const mentionsMars = normalizedPrompt.includes("mars");
|
|
53
|
-
const multipleLocales = selectedLocales.length > 1;
|
|
54
|
-
const activeLocale = selectedLocales.at(-1);
|
|
55
|
-
const proposalLabel = multipleLocales ? normalizedPrompt.includes("locale review") ? "Create localized locale review draft post about Mars" : "Create localized draft post about Mars" : normalizedPrompt.includes("apply flow") ? "Create apply flow draft post about Mars" : normalizedPrompt.includes("proposal review") ? "Create proposal review draft post about Mars" : "Create draft post about Mars";
|
|
56
|
-
const proposal = wantsCreatePost && mentionsMars ? signAIActionProposal(multipleLocales ? {
|
|
57
|
-
action: "create",
|
|
58
|
-
collection: "posts",
|
|
59
|
-
label: proposalLabel,
|
|
60
|
-
localizedData: Object.fromEntries(selectedLocales.map((locale)=>[
|
|
61
|
-
locale,
|
|
62
|
-
{
|
|
63
|
-
content: locale === "de" ? "Mars ist der vierte Planet von der Sonne." : "Mars is the fourth planet from the Sun.",
|
|
64
|
-
excerpt: locale === "de" ? "Ein kurzer Entwurf ueber Mars." : "A short draft about Mars.",
|
|
65
|
-
title: locale === "de" ? "Mars im Ueberblick" : "Mars Overview"
|
|
66
|
-
}
|
|
67
|
-
])),
|
|
68
|
-
...activeLocale ? {
|
|
69
|
-
locale: activeLocale
|
|
70
|
-
} : {}
|
|
71
|
-
} : {
|
|
72
|
-
action: "create",
|
|
73
|
-
collection: "posts",
|
|
74
|
-
data: {
|
|
75
|
-
content: "Mars is the fourth planet from the Sun.",
|
|
76
|
-
excerpt: "A short draft about Mars.",
|
|
77
|
-
status: "draft",
|
|
78
|
-
title: "Mars Overview"
|
|
79
|
-
},
|
|
80
|
-
label: proposalLabel,
|
|
81
|
-
...activeLocale ? {
|
|
82
|
-
locale: activeLocale
|
|
83
|
-
} : {}
|
|
84
|
-
}) : null;
|
|
85
|
-
const responseText = proposal ? "Prepared one draft post proposal." : "No content change was proposed.";
|
|
86
|
-
const debugPayload = createDebugPayload({
|
|
87
|
-
debug: {
|
|
88
|
-
model: "e2e-model",
|
|
89
|
-
provider: "openai",
|
|
90
|
-
tools: []
|
|
91
|
-
},
|
|
92
|
-
proposalCount: proposal ? 1 : 0,
|
|
93
|
-
selectedLocales,
|
|
94
|
-
toolFailures: [],
|
|
95
|
-
usage: {
|
|
96
|
-
inputTokens: 42,
|
|
97
|
-
outputTokens: 27,
|
|
98
|
-
totalTokens: 69
|
|
99
|
-
},
|
|
100
|
-
writeIntent: wantsCreatePost
|
|
101
|
-
});
|
|
102
|
-
return new Response(createSSEEventStream([
|
|
103
|
-
{
|
|
104
|
-
data: {
|
|
105
|
-
delta: responseText
|
|
106
|
-
},
|
|
107
|
-
event: "text"
|
|
108
|
-
},
|
|
109
|
-
{
|
|
110
|
-
data: {
|
|
111
|
-
proposals: proposal ? [
|
|
112
|
-
proposal
|
|
113
|
-
] : [],
|
|
114
|
-
usage: {
|
|
115
|
-
inputTokens: 42,
|
|
116
|
-
outputTokens: 27,
|
|
117
|
-
totalTokens: 69
|
|
118
|
-
}
|
|
119
|
-
},
|
|
120
|
-
event: "proposals"
|
|
121
|
-
},
|
|
122
|
-
{
|
|
123
|
-
data: debugPayload,
|
|
124
|
-
event: "debug"
|
|
125
|
-
},
|
|
126
|
-
{
|
|
127
|
-
data: {},
|
|
128
|
-
event: "done"
|
|
129
|
-
}
|
|
130
|
-
]), {
|
|
131
|
-
headers: {
|
|
132
|
-
"Cache-Control": "no-cache, no-transform",
|
|
133
|
-
Connection: "keep-alive",
|
|
134
|
-
"Content-Type": "text/event-stream; charset=utf-8"
|
|
135
|
-
}
|
|
136
|
-
});
|
|
137
|
-
};
|
|
138
|
-
const getRequiredFieldInfos = (fields, titleFieldName, path = "")=>{
|
|
139
|
-
return fields.flatMap((field)=>{
|
|
140
|
-
const fieldPath = field.name ? path ? `${path}.${field.name}` : field.name : path;
|
|
141
|
-
const requiredField = field.required && fieldPath ? [
|
|
142
|
-
{
|
|
143
|
-
defaultValue: field.defaultValue,
|
|
144
|
-
isTitleField: field.name === titleFieldName,
|
|
145
|
-
localized: Boolean(field.localized),
|
|
146
|
-
options: field.options,
|
|
147
|
-
path: fieldPath,
|
|
148
|
-
type: field.type
|
|
149
|
-
}
|
|
150
|
-
] : [];
|
|
151
|
-
const nestedFields = field.fields?.length ? getRequiredFieldInfos(field.fields, titleFieldName, fieldPath) : [];
|
|
152
|
-
const nestedBlocks = field.blocks?.length ? field.blocks.flatMap((block)=>getRequiredFieldInfos(block.fields || [], titleFieldName, `${fieldPath}.${block.slug}`)) : [];
|
|
153
|
-
return [
|
|
154
|
-
...requiredField,
|
|
155
|
-
...nestedFields,
|
|
156
|
-
...nestedBlocks
|
|
157
|
-
];
|
|
158
|
-
});
|
|
159
|
-
};
|
|
160
|
-
const getCreateFallbackValue = ({ field, label })=>{
|
|
161
|
-
if (field.defaultValue !== undefined) return field.defaultValue;
|
|
162
|
-
if (field.path === "_status") return "draft";
|
|
163
|
-
if (field.type === "checkbox") return false;
|
|
164
|
-
if (field.type === "select" || field.type === "radio") {
|
|
165
|
-
return getOptionValue(field.options?.[0]);
|
|
166
|
-
}
|
|
167
|
-
const terminalSegment = field.path.split(".").at(-1)?.toLowerCase();
|
|
168
|
-
if ((field.isTitleField || [
|
|
169
|
-
"title",
|
|
170
|
-
"name",
|
|
171
|
-
"label",
|
|
172
|
-
"headline"
|
|
173
|
-
].includes(terminalSegment || "")) && [
|
|
174
|
-
"text",
|
|
175
|
-
"textarea"
|
|
176
|
-
].includes(field.type || "")) {
|
|
177
|
-
return getSafeProposalLabel(label);
|
|
178
|
-
}
|
|
179
|
-
return undefined;
|
|
180
|
-
};
|
|
181
|
-
const fillMissingCreateFields = ({ data, label, localizedData, requiredFields })=>{
|
|
182
|
-
if (localizedData) {
|
|
183
|
-
const completedLocalizedData = Object.fromEntries(Object.entries(localizedData).map(([locale, localeData])=>[
|
|
184
|
-
locale,
|
|
185
|
-
{
|
|
186
|
-
...localeData
|
|
187
|
-
}
|
|
188
|
-
]));
|
|
189
|
-
const localeEntries = Object.entries(completedLocalizedData);
|
|
190
|
-
const [firstLocale, firstLocaleData] = localeEntries[0] || [];
|
|
191
|
-
for (const [locale, localeData] of localeEntries){
|
|
192
|
-
for (const field of requiredFields.filter((item)=>item.localized)){
|
|
193
|
-
if (hasValueAtPath(localeData, field.path)) continue;
|
|
194
|
-
const fallbackValue = getCreateFallbackValue({
|
|
195
|
-
field,
|
|
196
|
-
label
|
|
197
|
-
});
|
|
198
|
-
if (fallbackValue !== undefined) {
|
|
199
|
-
setValueAtPath(localeData, field.path, fallbackValue);
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
if (firstLocale && firstLocaleData) {
|
|
204
|
-
for (const field of requiredFields.filter((item)=>!item.localized)){
|
|
205
|
-
if (hasValueAtPath(firstLocaleData, field.path)) continue;
|
|
206
|
-
const fallbackValue = getCreateFallbackValue({
|
|
207
|
-
field,
|
|
208
|
-
label
|
|
209
|
-
});
|
|
210
|
-
if (fallbackValue !== undefined) {
|
|
211
|
-
setValueAtPath(firstLocaleData, field.path, fallbackValue);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
return {
|
|
216
|
-
localizedData: completedLocalizedData
|
|
217
|
-
};
|
|
218
|
-
}
|
|
219
|
-
const completedData = {
|
|
220
|
-
...data || {}
|
|
221
|
-
};
|
|
222
|
-
for (const field of requiredFields){
|
|
223
|
-
if (hasValueAtPath(completedData, field.path)) continue;
|
|
224
|
-
const fallbackValue = getCreateFallbackValue({
|
|
225
|
-
field,
|
|
226
|
-
label
|
|
227
|
-
});
|
|
228
|
-
if (fallbackValue !== undefined) {
|
|
229
|
-
setValueAtPath(completedData, field.path, fallbackValue);
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
return {
|
|
233
|
-
data: completedData
|
|
234
|
-
};
|
|
235
|
-
};
|
|
236
|
-
const getMissingCreateFields = ({ data, localizedData, requiredFields })=>{
|
|
237
|
-
if (localizedData) {
|
|
238
|
-
const locales = Object.entries(localizedData);
|
|
239
|
-
const firstLocale = locales[0];
|
|
240
|
-
if (!firstLocale) {
|
|
241
|
-
return [
|
|
242
|
-
"localizedData must include at least one locale entry"
|
|
243
|
-
];
|
|
244
|
-
}
|
|
245
|
-
const missing = [];
|
|
246
|
-
const localizedRequiredFields = [];
|
|
247
|
-
const sharedRequiredFields = [];
|
|
248
|
-
for (const field of requiredFields){
|
|
249
|
-
if (field.localized) {
|
|
250
|
-
localizedRequiredFields.push(field);
|
|
251
|
-
} else {
|
|
252
|
-
sharedRequiredFields.push(field);
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
for (const [locale, localeData] of locales){
|
|
256
|
-
for (const field of localizedRequiredFields){
|
|
257
|
-
if (!hasValueAtPath(localeData, field.path)) {
|
|
258
|
-
missing.push(`${locale}:${field.path}`);
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
for (const field of sharedRequiredFields){
|
|
263
|
-
if (!hasValueAtPath(firstLocale[1], field.path)) {
|
|
264
|
-
missing.push(`${firstLocale[0]}:${field.path}`);
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
return missing;
|
|
268
|
-
}
|
|
269
|
-
if (!data) return [
|
|
270
|
-
"data is required"
|
|
271
|
-
];
|
|
272
|
-
return requiredFields.flatMap((field)=>hasValueAtPath(data, field.path) ? [] : [
|
|
273
|
-
field.path
|
|
274
|
-
]);
|
|
275
|
-
};
|
|
276
|
-
const getProposalSummary = (proposal)=>({
|
|
277
|
-
action: proposal.action,
|
|
278
|
-
collection: "collection" in proposal ? proposal.collection : undefined,
|
|
279
|
-
hasData: "data" in proposal && Boolean(proposal.data),
|
|
280
|
-
hasLocalizedData: "localizedData" in proposal && Boolean(proposal.localizedData),
|
|
281
|
-
id: "id" in proposal ? proposal.id : undefined,
|
|
282
|
-
label: proposal.label,
|
|
283
|
-
locale: proposal.locale,
|
|
284
|
-
locales: "localizedData" in proposal && proposal.localizedData ? Object.keys(proposal.localizedData) : undefined,
|
|
285
|
-
slug: "slug" in proposal ? proposal.slug : undefined
|
|
286
|
-
});
|
|
287
|
-
const getCollectionBlockTypes = (fields)=>{
|
|
288
|
-
const blockTypes = new Set();
|
|
289
|
-
const visitFields = (items)=>{
|
|
290
|
-
for (const field of items){
|
|
291
|
-
if (field.type === "blocks" && field.blocks) {
|
|
292
|
-
for (const block of field.blocks){
|
|
293
|
-
blockTypes.add(block.slug);
|
|
294
|
-
if (block.fields?.length) {
|
|
295
|
-
visitFields(block.fields);
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
if (field.fields?.length) {
|
|
300
|
-
visitFields(field.fields);
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
};
|
|
304
|
-
visitFields(fields);
|
|
305
|
-
return [
|
|
306
|
-
...blockTypes
|
|
307
|
-
];
|
|
308
|
-
};
|
|
309
|
-
const regexSpecialCharactersPattern = /[.*+?^${}()|[\]\\]/g;
|
|
310
|
-
const getRequestedBlockTypes = ({ availableBlockTypes, mentions, prompt })=>{
|
|
311
|
-
const requestedBlockTypes = new Set();
|
|
312
|
-
const normalizedPrompt = prompt.toLowerCase();
|
|
313
|
-
const availableBlockTypeSet = new Set(availableBlockTypes);
|
|
314
|
-
const blockTypesByNormalizedSlug = new Map(availableBlockTypes.map((blockType)=>[
|
|
315
|
-
blockType.toLowerCase(),
|
|
316
|
-
blockType
|
|
317
|
-
]));
|
|
318
|
-
const escapedBlockTypes = availableBlockTypes.map((blockType)=>blockType.replace(regexSpecialCharactersPattern, "\\$&")).join("|");
|
|
319
|
-
const blockPattern = escapedBlockTypes ? new RegExp(`\\b(${escapedBlockTypes})\\b(?:\\s+block)?`, "gi") : null;
|
|
320
|
-
for (const mention of mentions || []){
|
|
321
|
-
if (mention.type === "block" && mention.slug && availableBlockTypeSet.has(mention.slug)) {
|
|
322
|
-
requestedBlockTypes.add(mention.slug);
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
if (blockPattern) {
|
|
326
|
-
for (const match of normalizedPrompt.matchAll(blockPattern)){
|
|
327
|
-
const blockType = match[1] ? blockTypesByNormalizedSlug.get(match[1].toLowerCase()) : null;
|
|
328
|
-
if (blockType) {
|
|
329
|
-
requestedBlockTypes.add(blockType);
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
return [
|
|
334
|
-
...requestedBlockTypes
|
|
335
|
-
];
|
|
336
|
-
};
|
|
337
|
-
const collectProposalBlockTypes = ({ data, fields })=>{
|
|
338
|
-
const foundBlockTypes = new Set();
|
|
339
|
-
const visitFields = (items, value)=>{
|
|
340
|
-
for (const field of items){
|
|
341
|
-
if (!field.name) continue;
|
|
342
|
-
const fieldValue = value[field.name];
|
|
343
|
-
if (fieldValue === undefined || fieldValue === null) continue;
|
|
344
|
-
if (field.type === "blocks" && Array.isArray(fieldValue)) {
|
|
345
|
-
const blocksBySlug = new Map(field.blocks?.map((block)=>[
|
|
346
|
-
block.slug,
|
|
347
|
-
block
|
|
348
|
-
]));
|
|
349
|
-
for (const blockItem of fieldValue){
|
|
350
|
-
if (!isRecord(blockItem)) continue;
|
|
351
|
-
const blockType = typeof blockItem.blockType === "string" ? blockItem.blockType : typeof blockItem.type === "string" ? blockItem.type : typeof blockItem.slug === "string" ? blockItem.slug : null;
|
|
352
|
-
if (blockType) {
|
|
353
|
-
foundBlockTypes.add(blockType);
|
|
354
|
-
}
|
|
355
|
-
if (blockType) {
|
|
356
|
-
const blockConfig = blocksBySlug.get(blockType);
|
|
357
|
-
if (blockConfig?.fields?.length) {
|
|
358
|
-
visitFields(blockConfig.fields, blockItem);
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
continue;
|
|
363
|
-
}
|
|
364
|
-
if (field.type === "group" && isRecord(fieldValue) && field.fields?.length) {
|
|
365
|
-
visitFields(field.fields, fieldValue);
|
|
366
|
-
continue;
|
|
367
|
-
}
|
|
368
|
-
if (field.type === "array" && Array.isArray(fieldValue) && field.fields?.length) {
|
|
369
|
-
for (const item of fieldValue){
|
|
370
|
-
if (isRecord(item)) {
|
|
371
|
-
visitFields(field.fields, item);
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
};
|
|
377
|
-
visitFields(fields, data);
|
|
378
|
-
return foundBlockTypes;
|
|
379
|
-
};
|
|
380
|
-
const collectRelationshipTargets = ({ data, fields, path = "" })=>{
|
|
381
|
-
const targets = [];
|
|
382
|
-
for (const field of fields){
|
|
383
|
-
if (!field.name) continue;
|
|
384
|
-
const fieldPath = path ? `${path}.${field.name}` : field.name;
|
|
385
|
-
const fieldValue = data[field.name];
|
|
386
|
-
if (fieldValue === undefined || fieldValue === null) continue;
|
|
387
|
-
if (field.type === "relationship" || field.type === "upload") {
|
|
388
|
-
const relationTargets = Array.isArray(field.relationTo) ? field.relationTo.filter((item)=>typeof item === "string") : typeof field.relationTo === "string" ? [
|
|
389
|
-
field.relationTo
|
|
390
|
-
] : [];
|
|
391
|
-
const collectSingle = (value, itemPath)=>{
|
|
392
|
-
if (typeof value === "string" || typeof value === "number") {
|
|
393
|
-
if (relationTargets.length === 1) {
|
|
394
|
-
targets.push({
|
|
395
|
-
collection: relationTargets[0],
|
|
396
|
-
id: value,
|
|
397
|
-
path: itemPath
|
|
398
|
-
});
|
|
399
|
-
}
|
|
400
|
-
return;
|
|
401
|
-
}
|
|
402
|
-
if (!isRecord(value)) return;
|
|
403
|
-
const relationTo = typeof value.relationTo === "string" ? value.relationTo : relationTargets[0];
|
|
404
|
-
const id = typeof value.id === "string" || typeof value.id === "number" ? value.id : typeof value.value === "string" || typeof value.value === "number" ? value.value : undefined;
|
|
405
|
-
if (!relationTo || id === undefined) return;
|
|
406
|
-
targets.push({
|
|
407
|
-
collection: relationTo,
|
|
408
|
-
id,
|
|
409
|
-
path: itemPath
|
|
410
|
-
});
|
|
411
|
-
};
|
|
412
|
-
if (field.hasMany && Array.isArray(fieldValue)) {
|
|
413
|
-
fieldValue.forEach((item, index)=>collectSingle(item, `${fieldPath}.${index}`));
|
|
414
|
-
} else {
|
|
415
|
-
collectSingle(fieldValue, fieldPath);
|
|
416
|
-
}
|
|
417
|
-
continue;
|
|
418
|
-
}
|
|
419
|
-
if (field.type === "group" && isRecord(fieldValue) && field.fields?.length) {
|
|
420
|
-
targets.push(...collectRelationshipTargets({
|
|
421
|
-
data: fieldValue,
|
|
422
|
-
fields: field.fields,
|
|
423
|
-
path: fieldPath
|
|
424
|
-
}));
|
|
425
|
-
continue;
|
|
426
|
-
}
|
|
427
|
-
if (field.type === "array" && Array.isArray(fieldValue) && field.fields?.length) {
|
|
428
|
-
fieldValue.forEach((item, index)=>{
|
|
429
|
-
if (!isRecord(item)) return;
|
|
430
|
-
targets.push(...collectRelationshipTargets({
|
|
431
|
-
data: item,
|
|
432
|
-
fields: field.fields,
|
|
433
|
-
path: `${fieldPath}.${index}`
|
|
434
|
-
}));
|
|
435
|
-
});
|
|
436
|
-
continue;
|
|
437
|
-
}
|
|
438
|
-
if (field.type === "blocks" && Array.isArray(fieldValue) && field.blocks?.length) {
|
|
439
|
-
const blocksBySlug = new Map(field.blocks.map((block)=>[
|
|
440
|
-
block.slug,
|
|
441
|
-
block
|
|
442
|
-
]));
|
|
443
|
-
fieldValue.forEach((item, index)=>{
|
|
444
|
-
if (!isRecord(item)) return;
|
|
445
|
-
const blockType = typeof item.blockType === "string" ? item.blockType : typeof item.type === "string" ? item.type : typeof item.slug === "string" ? item.slug : null;
|
|
446
|
-
if (!blockType) return;
|
|
447
|
-
const blockConfig = blocksBySlug.get(blockType);
|
|
448
|
-
if (!blockConfig?.fields?.length) return;
|
|
449
|
-
targets.push(...collectRelationshipTargets({
|
|
450
|
-
data: item,
|
|
451
|
-
fields: blockConfig.fields,
|
|
452
|
-
path: `${fieldPath}.${index}`
|
|
453
|
-
}));
|
|
454
|
-
});
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
return targets;
|
|
458
|
-
};
|
|
459
|
-
const collectUploadTargets = ({ data, fields, path = "" })=>{
|
|
460
|
-
const targets = [];
|
|
461
|
-
for (const field of fields){
|
|
462
|
-
if (!field.name) continue;
|
|
463
|
-
const fieldPath = path ? `${path}.${field.name}` : field.name;
|
|
464
|
-
const fieldValue = data[field.name];
|
|
465
|
-
if (fieldValue === undefined || fieldValue === null) continue;
|
|
466
|
-
if (field.type === "upload") {
|
|
467
|
-
const relationTargets = Array.isArray(field.relationTo) ? field.relationTo.filter((item)=>typeof item === "string") : typeof field.relationTo === "string" ? [
|
|
468
|
-
field.relationTo
|
|
469
|
-
] : [];
|
|
470
|
-
const collectSingle = (value, itemPath)=>{
|
|
471
|
-
if (typeof value === "string" || typeof value === "number") {
|
|
472
|
-
if (relationTargets.length === 1) {
|
|
473
|
-
targets.push({
|
|
474
|
-
collection: relationTargets[0],
|
|
475
|
-
id: value,
|
|
476
|
-
path: itemPath
|
|
477
|
-
});
|
|
478
|
-
}
|
|
479
|
-
return;
|
|
480
|
-
}
|
|
481
|
-
if (!isRecord(value)) return;
|
|
482
|
-
const relationTo = typeof value.relationTo === "string" ? value.relationTo : relationTargets[0];
|
|
483
|
-
const id = typeof value.id === "string" || typeof value.id === "number" ? value.id : typeof value.value === "string" || typeof value.value === "number" ? value.value : undefined;
|
|
484
|
-
if (!relationTo || id === undefined) return;
|
|
485
|
-
targets.push({
|
|
486
|
-
collection: relationTo,
|
|
487
|
-
id,
|
|
488
|
-
path: itemPath
|
|
489
|
-
});
|
|
490
|
-
};
|
|
491
|
-
if (field.hasMany && Array.isArray(fieldValue)) {
|
|
492
|
-
fieldValue.forEach((item, index)=>collectSingle(item, `${fieldPath}.${index}`));
|
|
493
|
-
} else {
|
|
494
|
-
collectSingle(fieldValue, fieldPath);
|
|
495
|
-
}
|
|
496
|
-
continue;
|
|
497
|
-
}
|
|
498
|
-
if (field.type === "group" && isRecord(fieldValue) && field.fields?.length) {
|
|
499
|
-
targets.push(...collectUploadTargets({
|
|
500
|
-
data: fieldValue,
|
|
501
|
-
fields: field.fields,
|
|
502
|
-
path: fieldPath
|
|
503
|
-
}));
|
|
504
|
-
continue;
|
|
505
|
-
}
|
|
506
|
-
if (field.type === "array" && Array.isArray(fieldValue) && field.fields?.length) {
|
|
507
|
-
fieldValue.forEach((item, index)=>{
|
|
508
|
-
if (!isRecord(item)) return;
|
|
509
|
-
targets.push(...collectUploadTargets({
|
|
510
|
-
data: item,
|
|
511
|
-
fields: field.fields,
|
|
512
|
-
path: `${fieldPath}.${index}`
|
|
513
|
-
}));
|
|
514
|
-
});
|
|
515
|
-
continue;
|
|
516
|
-
}
|
|
517
|
-
if (field.type === "blocks" && Array.isArray(fieldValue) && field.blocks?.length) {
|
|
518
|
-
const blocksBySlug = new Map(field.blocks.map((block)=>[
|
|
519
|
-
block.slug,
|
|
520
|
-
block
|
|
521
|
-
]));
|
|
522
|
-
fieldValue.forEach((item, index)=>{
|
|
523
|
-
if (!isRecord(item)) return;
|
|
524
|
-
const blockType = typeof item.blockType === "string" ? item.blockType : typeof item.type === "string" ? item.type : typeof item.slug === "string" ? item.slug : null;
|
|
525
|
-
if (!blockType) return;
|
|
526
|
-
const blockConfig = blocksBySlug.get(blockType);
|
|
527
|
-
if (!blockConfig?.fields?.length) return;
|
|
528
|
-
targets.push(...collectUploadTargets({
|
|
529
|
-
data: item,
|
|
530
|
-
fields: blockConfig.fields,
|
|
531
|
-
path: `${fieldPath}.${index}`
|
|
532
|
-
}));
|
|
533
|
-
});
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
return targets;
|
|
537
|
-
};
|
|
538
|
-
const validateRelationshipTargetsExist = async ({ data, fields, req })=>{
|
|
539
|
-
const targets = collectRelationshipTargets({
|
|
540
|
-
data,
|
|
541
|
-
fields
|
|
542
|
-
});
|
|
543
|
-
const targetResults = await Promise.all(targets.map(async (target)=>{
|
|
544
|
-
try {
|
|
545
|
-
await req.payload.findByID({
|
|
546
|
-
collection: target.collection,
|
|
547
|
-
depth: 0,
|
|
548
|
-
id: String(target.id),
|
|
549
|
-
overrideAccess: false,
|
|
550
|
-
req
|
|
551
|
-
});
|
|
552
|
-
return null;
|
|
553
|
-
} catch {
|
|
554
|
-
return target;
|
|
555
|
-
}
|
|
556
|
-
}));
|
|
557
|
-
return targetResults.filter((target)=>Boolean(target));
|
|
558
|
-
};
|
|
559
|
-
const getUploadTargetsOutsideAttachments = ({ allowedAttachmentKeys, data, fields })=>{
|
|
560
|
-
if (allowedAttachmentKeys.size === 0) return [];
|
|
561
|
-
return collectUploadTargets({
|
|
562
|
-
data,
|
|
563
|
-
fields
|
|
564
|
-
}).filter((target)=>!allowedAttachmentKeys.has(`${target.collection}:${String(target.id)}`));
|
|
565
|
-
};
|
|
566
19
|
const getMentionSummary = (mentions)=>mentions?.map((mention)=>({
|
|
567
20
|
collection: "collection" in mention ? mention.collection : undefined,
|
|
568
21
|
id: "id" in mention ? mention.id : undefined,
|
|
@@ -606,194 +59,10 @@ const getMediaAttachmentContext = async ({ allowedCollectionsBySlug, attachments
|
|
|
606
59
|
}
|
|
607
60
|
return contexts;
|
|
608
61
|
};
|
|
609
|
-
const
|
|
610
|
-
|
|
611
|
-
const addAlias = (alias, slug)=>{
|
|
612
|
-
const normalizedAlias = alias?.trim().toLowerCase();
|
|
613
|
-
if (!normalizedAlias) return;
|
|
614
|
-
if (!aliasMap.has(normalizedAlias)) {
|
|
615
|
-
aliasMap.set(normalizedAlias, slug);
|
|
616
|
-
}
|
|
617
|
-
};
|
|
618
|
-
for (const collection of collections){
|
|
619
|
-
addAlias(collection.slug, collection.slug);
|
|
620
|
-
addAlias(collection.slug.replace(/-/g, " "), collection.slug);
|
|
621
|
-
const singular = typeof collection.labels?.singular === "string" ? collection.labels.singular : undefined;
|
|
622
|
-
const plural = typeof collection.labels?.plural === "string" ? collection.labels.plural : undefined;
|
|
623
|
-
addAlias(singular, collection.slug);
|
|
624
|
-
addAlias(plural, collection.slug);
|
|
625
|
-
if (singular?.endsWith("s")) {
|
|
626
|
-
addAlias(singular.slice(0, -1), collection.slug);
|
|
627
|
-
}
|
|
628
|
-
if (plural?.endsWith("s")) {
|
|
629
|
-
addAlias(plural.slice(0, -1), collection.slug);
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
return Object.fromEntries(aliasMap.entries());
|
|
633
|
-
};
|
|
634
|
-
const getLikelyCollectionMatches = ({ aliasMap, prompt })=>{
|
|
635
|
-
const normalizedPrompt = prompt.toLowerCase();
|
|
636
|
-
const matches = new Set();
|
|
637
|
-
const aliases = Object.keys(aliasMap).sort((a, b)=>b.length - a.length);
|
|
638
|
-
const aliasPattern = aliases.length > 0 ? new RegExp(aliases.map((alias)=>alias.replace(regexSpecialCharactersPattern, "\\$&")).join("|"), "g") : null;
|
|
639
|
-
if (!aliasPattern) return [];
|
|
640
|
-
for (const match of normalizedPrompt.matchAll(aliasPattern)){
|
|
641
|
-
const alias = match[0];
|
|
642
|
-
const slug = aliasMap[alias];
|
|
643
|
-
if (slug) {
|
|
644
|
-
matches.add(slug);
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
return [
|
|
648
|
-
...matches
|
|
649
|
-
];
|
|
650
|
-
};
|
|
651
|
-
const normalizeIntentText = (prompt)=>prompt.toLowerCase().replace(/ä/g, "ae").replace(/ö/g, "oe").replace(/ü/g, "ue").replace(/ß/g, "ss").normalize("NFKD").replace(/[\u0300-\u036f]/g, "");
|
|
652
|
-
const deleteIntentPattern = /\b(?:delete|remove|erase|destroy|discard|purge|loesch\w*|losch\w*|entfern\w*|rausnehm\w*|wegnehm\w*|verwerf\w*)\b/;
|
|
653
|
-
const createIntentPattern = /\b(?:create|build|generate|draft|compose|author|write|make|new|erstell\w*|anleg\w*|erzeug\w*|generier\w*|entwerf\w*|verfass\w*|schreib\w*|neu)\b/;
|
|
654
|
-
const updateIntentPattern = /\b(?:update|edit|change|modify|revise|refine|rewrite|translate|replace|rename|correct|fix|set|publish|unpublish|aktualisier\w*|aender\w*|bearbeit\w*|anpass\w*|ueberarbeit\w*|uberarbeit\w*|umschreib\w*|uebersetz\w*|ubersetz\w*|ersetz\w*|umbenenn\w*|korrigier\w*|reparier\w*|setz\w*|veroeffentlich\w*|veroffentlich\w*|zurueckzieh\w*|zuruckzieh\w*)\b/;
|
|
655
|
-
const additiveIntentPattern = /\b(?:add|insert|append|attach|assign|link|include|extend|ergaenz\w*|erganz\w*|hinzufueg\w*|hinzufug\w*|einfueg\w*|einfug\w*|anhaeng\w*|anhang\w*|zuweis\w*|verknuepf\w*|verknupf\w*)\b/;
|
|
656
|
-
const searchIntentPattern = /\b(?:search|find|lookup|locate|list|show|fetch|browse|query|such\w*|find\w*|nachschlag\w*|auflist\w*|anzeig\w*|zeig\w*|hol\w*|durchsuch\w*)\b/;
|
|
657
|
-
const separableDeleteIntentPattern = /\b(?:nimm|nehm\w*)\b(?:\s+\w+){0,8}\s+(?:raus|weg)\b/;
|
|
658
|
-
const separableUpdateIntentPattern = /\b(?:pass(?:e|t|en|st)?\b(?:\s+\w+){0,8}\s+an|fueg\w*\b(?:\s+\w+){0,8}\s+(?:ein|hinzu)|haeng\w*\b(?:\s+\w+){0,8}\s+an|weis\w*\b(?:\s+\w+){0,8}\s+zu)\b/;
|
|
659
|
-
const getChatIntent = ({ hasCurrentDocument, hasCurrentGlobal, prompt })=>{
|
|
660
|
-
const normalizedPrompt = normalizeIntentText(prompt);
|
|
661
|
-
if (deleteIntentPattern.test(normalizedPrompt) || separableDeleteIntentPattern.test(normalizedPrompt)) {
|
|
662
|
-
return hasCurrentGlobal ? "read" : "delete";
|
|
663
|
-
}
|
|
664
|
-
if (createIntentPattern.test(normalizedPrompt)) {
|
|
665
|
-
if (hasCurrentGlobal) return "updateGlobal";
|
|
666
|
-
if (hasCurrentDocument) return "update";
|
|
667
|
-
return "create";
|
|
668
|
-
}
|
|
669
|
-
if (updateIntentPattern.test(normalizedPrompt) || additiveIntentPattern.test(normalizedPrompt) || separableUpdateIntentPattern.test(normalizedPrompt)) {
|
|
670
|
-
return hasCurrentGlobal ? "updateGlobal" : "update";
|
|
671
|
-
}
|
|
672
|
-
if (searchIntentPattern.test(normalizedPrompt)) {
|
|
673
|
-
return "search";
|
|
674
|
-
}
|
|
675
|
-
return "read";
|
|
676
|
-
};
|
|
677
|
-
const getIntentToolChoice = ({ hasKnownCollection, hasKnownDocument, hasKnownGlobal, intent })=>{
|
|
678
|
-
if (intent === "create" && hasKnownCollection) {
|
|
679
|
-
return {
|
|
680
|
-
toolName: "proposeCreateDoc",
|
|
681
|
-
type: "tool"
|
|
682
|
-
};
|
|
683
|
-
}
|
|
684
|
-
if (intent === "update" && hasKnownDocument) {
|
|
685
|
-
return {
|
|
686
|
-
toolName: "proposeUpdateDoc",
|
|
687
|
-
type: "tool"
|
|
688
|
-
};
|
|
689
|
-
}
|
|
690
|
-
if (intent === "delete" && hasKnownDocument) {
|
|
691
|
-
return {
|
|
692
|
-
toolName: "proposeDeleteDoc",
|
|
693
|
-
type: "tool"
|
|
694
|
-
};
|
|
695
|
-
}
|
|
696
|
-
if (intent === "updateGlobal" && hasKnownGlobal) {
|
|
697
|
-
return {
|
|
698
|
-
toolName: "proposeUpdateGlobal",
|
|
699
|
-
type: "tool"
|
|
700
|
-
};
|
|
701
|
-
}
|
|
702
|
-
return undefined;
|
|
703
|
-
};
|
|
704
|
-
const getToolNamesForIntent = ({ hasAttachments, hasKnownCollection, hasCurrentDocument, hasCurrentGlobal, intent })=>{
|
|
705
|
-
if (hasCurrentGlobal) {
|
|
706
|
-
return new Set(intent === "updateGlobal" ? [
|
|
707
|
-
"getGlobal",
|
|
708
|
-
"listGlobals",
|
|
709
|
-
"proposeUpdateGlobal"
|
|
710
|
-
] : [
|
|
711
|
-
"getGlobal",
|
|
712
|
-
"listGlobals"
|
|
713
|
-
]);
|
|
714
|
-
}
|
|
715
|
-
if (hasCurrentDocument) {
|
|
716
|
-
if (intent === "delete") return new Set([
|
|
717
|
-
"getDoc",
|
|
718
|
-
"proposeDeleteDoc"
|
|
719
|
-
]);
|
|
720
|
-
if (intent === "update" || intent === "create") return new Set([
|
|
721
|
-
"getDoc",
|
|
722
|
-
"listCollections",
|
|
723
|
-
"proposeUpdateDoc"
|
|
724
|
-
]);
|
|
725
|
-
return new Set([
|
|
726
|
-
"getDoc",
|
|
727
|
-
"listCollections"
|
|
728
|
-
]);
|
|
729
|
-
}
|
|
730
|
-
if (hasKnownCollection) {
|
|
731
|
-
if (intent === "create") return new Set([
|
|
732
|
-
"getDoc",
|
|
733
|
-
"listCollections",
|
|
734
|
-
"proposeCreateDoc",
|
|
735
|
-
"searchDocs"
|
|
736
|
-
]);
|
|
737
|
-
if (intent === "update") return new Set([
|
|
738
|
-
"getDoc",
|
|
739
|
-
"listCollections",
|
|
740
|
-
"proposeUpdateDoc",
|
|
741
|
-
"searchDocs"
|
|
742
|
-
]);
|
|
743
|
-
if (intent === "delete") return new Set([
|
|
744
|
-
"getDoc",
|
|
745
|
-
"proposeDeleteDoc",
|
|
746
|
-
"searchDocs"
|
|
747
|
-
]);
|
|
748
|
-
return new Set([
|
|
749
|
-
"getDoc",
|
|
750
|
-
"listCollections",
|
|
751
|
-
"searchDocs"
|
|
752
|
-
]);
|
|
753
|
-
}
|
|
754
|
-
const toolNames = intent === "create" ? new Set([
|
|
755
|
-
"getDoc",
|
|
756
|
-
"listCollections",
|
|
757
|
-
"proposeCreateDoc",
|
|
758
|
-
"searchDocs"
|
|
759
|
-
]) : intent === "update" ? new Set([
|
|
760
|
-
"getDoc",
|
|
761
|
-
"getGlobal",
|
|
762
|
-
"listCollections",
|
|
763
|
-
"listGlobals",
|
|
764
|
-
"proposeUpdateDoc",
|
|
765
|
-
"proposeUpdateGlobal",
|
|
766
|
-
"searchDocs"
|
|
767
|
-
]) : intent === "delete" ? new Set([
|
|
768
|
-
"getDoc",
|
|
769
|
-
"listCollections",
|
|
770
|
-
"proposeDeleteDoc",
|
|
771
|
-
"searchDocs"
|
|
772
|
-
]) : new Set([
|
|
773
|
-
"getDoc",
|
|
774
|
-
"getGlobal",
|
|
775
|
-
"listCollections",
|
|
776
|
-
"listGlobals",
|
|
777
|
-
"searchDocs"
|
|
778
|
-
]);
|
|
779
|
-
if (hasAttachments && (intent === "create" || intent === "update")) {
|
|
780
|
-
toolNames.add("proposeUpdateDoc");
|
|
781
|
-
}
|
|
782
|
-
return toolNames;
|
|
783
|
-
};
|
|
784
|
-
export const createChatHandler = (options = {})=>async (req)=>{
|
|
785
|
-
if (!req.user) return Response.json({
|
|
786
|
-
error: "Unauthorized"
|
|
787
|
-
}, {
|
|
788
|
-
status: 401
|
|
789
|
-
});
|
|
790
|
-
const body = req.json ? await req.json().catch(()=>null) : null;
|
|
62
|
+
export const createChatHandler = (options = {})=>withAuthenticatedHandler(async (req)=>{
|
|
63
|
+
const body = await readJSONBody(req);
|
|
791
64
|
const prompt = body?.prompt?.trim();
|
|
792
|
-
if (!prompt) return
|
|
793
|
-
error: "Prompt is required"
|
|
794
|
-
}, {
|
|
795
|
-
status: 400
|
|
796
|
-
});
|
|
65
|
+
if (!prompt) return jsonError("Prompt is required");
|
|
797
66
|
const selectedLocales = [];
|
|
798
67
|
const selectedLocaleSet = new Set();
|
|
799
68
|
for (const mention of body?.mentions || []){
|
|
@@ -810,79 +79,68 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
810
79
|
});
|
|
811
80
|
}
|
|
812
81
|
const user = req.user;
|
|
813
|
-
const
|
|
814
|
-
|
|
82
|
+
const aiRequestResolution = await resolveAIRequestContext({
|
|
83
|
+
options,
|
|
815
84
|
req,
|
|
816
|
-
|
|
85
|
+
requestedModel: body?.model,
|
|
86
|
+
requestedProvider: body?.provider,
|
|
87
|
+
user
|
|
817
88
|
});
|
|
818
|
-
if (
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
89
|
+
if (!aiRequestResolution.ok) {
|
|
90
|
+
if (aiRequestResolution.error.code === "token_limit") {
|
|
91
|
+
const exceededTokenUsageLimit = aiRequestResolution.error.tokenLimit;
|
|
92
|
+
const scope = options.maxTokenUsage?.type === "site" ? "site" : "user";
|
|
93
|
+
const periodLabel = exceededTokenUsageLimit.period === "day" ? "Daily" : "Weekly";
|
|
94
|
+
logHandlerEvent(req, "warn", {
|
|
95
|
+
limit: exceededTokenUsageLimit.limit,
|
|
96
|
+
msg: "AI chat blocked: token usage limit reached",
|
|
97
|
+
period: exceededTokenUsageLimit.period,
|
|
98
|
+
scope,
|
|
99
|
+
used: exceededTokenUsageLimit.used,
|
|
100
|
+
userID: String(user.id)
|
|
101
|
+
});
|
|
102
|
+
return Response.json({
|
|
103
|
+
error: `${periodLabel} AI token limit reached for this ${scope}.`,
|
|
104
|
+
limit: exceededTokenUsageLimit.limit,
|
|
105
|
+
period: exceededTokenUsageLimit.period,
|
|
106
|
+
used: exceededTokenUsageLimit.used
|
|
107
|
+
}, {
|
|
108
|
+
status: 429
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
if (aiRequestResolution.error.code !== "missing_api_key") {
|
|
112
|
+
return jsonError(aiRequestResolution.error.message);
|
|
113
|
+
}
|
|
114
|
+
const missingKeyError = aiRequestResolution.error;
|
|
115
|
+
const debug = createInitialChatDebug({
|
|
116
|
+
model: missingKeyError.modelID,
|
|
117
|
+
provider: missingKeyError.providerID
|
|
846
118
|
});
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
119
|
+
logHandlerEvent(req, "info", {
|
|
120
|
+
activeLocale,
|
|
121
|
+
debug,
|
|
122
|
+
mentionCount: mentionSummary.length,
|
|
123
|
+
mentions: mentionSummary,
|
|
124
|
+
msg: "AI chat started",
|
|
125
|
+
promptPreview: getLogPreview(prompt),
|
|
126
|
+
selectedLocales
|
|
853
127
|
});
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
}, {
|
|
861
|
-
status: 400
|
|
128
|
+
logHandlerEvent(req, "warn", {
|
|
129
|
+
activeLocale,
|
|
130
|
+
debug,
|
|
131
|
+
msg: "AI chat blocked: missing provider API key",
|
|
132
|
+
promptPreview: getLogPreview(prompt),
|
|
133
|
+
selectedLocales
|
|
862
134
|
});
|
|
135
|
+
return jsonError(missingKeyError.managedProvider ? `Configure a ${missingKeyError.providerID} API key in the plugin config or server environment first.` : options.allowUserApiKeys === false ? `Configure a ${missingKeyError.provider} API key in the server environment first.` : `Add a ${missingKeyError.provider} API key to your account settings or configure it in the server environment first.`);
|
|
863
136
|
}
|
|
864
|
-
const
|
|
865
|
-
const
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
model:
|
|
869
|
-
provider
|
|
137
|
+
const resolvedAIRequest = aiRequestResolution.context;
|
|
138
|
+
const provider = resolvedAIRequest.provider;
|
|
139
|
+
const managedProvider = resolvedAIRequest.managedProvider;
|
|
140
|
+
const debug = createInitialChatDebug({
|
|
141
|
+
model: resolvedAIRequest.modelID,
|
|
142
|
+
provider: resolvedAIRequest.providerID
|
|
870
143
|
});
|
|
871
|
-
const debug = {
|
|
872
|
-
model: providerConfig.modelID,
|
|
873
|
-
provider: managedProvider?.id || provider,
|
|
874
|
-
tools: [
|
|
875
|
-
"getDoc",
|
|
876
|
-
"getGlobal",
|
|
877
|
-
"listCollections",
|
|
878
|
-
"listGlobals",
|
|
879
|
-
"proposeCreateDoc",
|
|
880
|
-
"proposeDeleteDoc",
|
|
881
|
-
"proposeUpdateDoc",
|
|
882
|
-
"proposeUpdateGlobal",
|
|
883
|
-
"searchDocs"
|
|
884
|
-
]
|
|
885
|
-
};
|
|
886
144
|
logHandlerEvent(req, "info", {
|
|
887
145
|
activeLocale,
|
|
888
146
|
debug,
|
|
@@ -892,117 +150,7 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
892
150
|
promptPreview: getLogPreview(prompt),
|
|
893
151
|
selectedLocales
|
|
894
152
|
});
|
|
895
|
-
if (!providerConfig.apiKey) {
|
|
896
|
-
logHandlerEvent(req, "warn", {
|
|
897
|
-
activeLocale,
|
|
898
|
-
debug,
|
|
899
|
-
msg: "AI chat blocked: missing provider API key",
|
|
900
|
-
promptPreview: getLogPreview(prompt),
|
|
901
|
-
selectedLocales
|
|
902
|
-
});
|
|
903
|
-
return Response.json({
|
|
904
|
-
error: managedProvider ? `Configure a ${managedProvider?.id || provider} API key in the plugin config or server environment first.` : 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.`
|
|
905
|
-
}, {
|
|
906
|
-
status: 400
|
|
907
|
-
});
|
|
908
|
-
}
|
|
909
153
|
try {
|
|
910
|
-
const proposals = [];
|
|
911
|
-
const toolFailures = [];
|
|
912
|
-
const proposalRepairTracker = createProposalRepairTracker();
|
|
913
|
-
const registerToolFailure = (failure)=>{
|
|
914
|
-
toolFailures.push(failure);
|
|
915
|
-
logHandlerEvent(req, "warn", {
|
|
916
|
-
debug,
|
|
917
|
-
...failure,
|
|
918
|
-
msg: "AI tool validation failed",
|
|
919
|
-
promptPreview: getLogPreview(prompt)
|
|
920
|
-
});
|
|
921
|
-
};
|
|
922
|
-
const createToolError = ({ collection, details, errorCode = "NON_RETRYABLE_TOOL_ERROR", message, slug, tool })=>{
|
|
923
|
-
registerToolFailure({
|
|
924
|
-
collection,
|
|
925
|
-
details,
|
|
926
|
-
errorCode,
|
|
927
|
-
message,
|
|
928
|
-
retryable: false,
|
|
929
|
-
slug,
|
|
930
|
-
tool
|
|
931
|
-
});
|
|
932
|
-
return {
|
|
933
|
-
error: message,
|
|
934
|
-
errorCode,
|
|
935
|
-
retryable: false
|
|
936
|
-
};
|
|
937
|
-
};
|
|
938
|
-
const createRepairableToolError = ({ collection, details, id, issues, message, slug, tool })=>{
|
|
939
|
-
const repair = proposalRepairTracker.registerFailure({
|
|
940
|
-
collection,
|
|
941
|
-
id,
|
|
942
|
-
slug,
|
|
943
|
-
tool
|
|
944
|
-
});
|
|
945
|
-
const responseMessage = repair.retryable ? message : "The single proposal repair attempt failed. Do not call the same proposal tool for this target again.";
|
|
946
|
-
registerToolFailure({
|
|
947
|
-
collection,
|
|
948
|
-
details,
|
|
949
|
-
errorCode: repair.errorCode,
|
|
950
|
-
message: responseMessage,
|
|
951
|
-
retryable: repair.retryable,
|
|
952
|
-
slug,
|
|
953
|
-
tool
|
|
954
|
-
});
|
|
955
|
-
return {
|
|
956
|
-
error: responseMessage,
|
|
957
|
-
errorCode: repair.errorCode,
|
|
958
|
-
repair: {
|
|
959
|
-
attempt: repair.attempt,
|
|
960
|
-
issues,
|
|
961
|
-
maxAttempts: repair.maxAttempts
|
|
962
|
-
},
|
|
963
|
-
retryable: repair.retryable
|
|
964
|
-
};
|
|
965
|
-
};
|
|
966
|
-
const getProposalRepairLimitError = ({ collection, id, slug, tool })=>{
|
|
967
|
-
const callState = proposalRepairTracker.beginCall({
|
|
968
|
-
collection,
|
|
969
|
-
id,
|
|
970
|
-
slug,
|
|
971
|
-
tool
|
|
972
|
-
});
|
|
973
|
-
if (callState !== "blocked") return null;
|
|
974
|
-
return createToolError({
|
|
975
|
-
collection,
|
|
976
|
-
errorCode: "REPAIR_EXHAUSTED",
|
|
977
|
-
message: "The single proposal repair attempt was already used. Do not call this proposal tool for the same target again.",
|
|
978
|
-
slug,
|
|
979
|
-
tool
|
|
980
|
-
});
|
|
981
|
-
};
|
|
982
|
-
const addSignedProposal = (proposal)=>{
|
|
983
|
-
if ("data" in proposal && proposal.data && containsSensitiveData(proposal.data)) {
|
|
984
|
-
return createToolError({
|
|
985
|
-
details: getProposalSummary(proposal),
|
|
986
|
-
message: "Proposal contains sensitive fields and cannot be created.",
|
|
987
|
-
tool: `propose${proposal.action[0]?.toUpperCase()}${proposal.action.slice(1)}`
|
|
988
|
-
});
|
|
989
|
-
}
|
|
990
|
-
if ("localizedData" in proposal && hasLocalizedData(proposal.localizedData) && Object.values(proposal.localizedData).some((value)=>containsSensitiveData(value))) {
|
|
991
|
-
return createToolError({
|
|
992
|
-
details: getProposalSummary(proposal),
|
|
993
|
-
message: "Proposal contains sensitive fields and cannot be created.",
|
|
994
|
-
tool: `propose${proposal.action[0]?.toUpperCase()}${proposal.action.slice(1)}`
|
|
995
|
-
});
|
|
996
|
-
}
|
|
997
|
-
const signedProposal = signAIActionProposal(proposal);
|
|
998
|
-
proposals.push(signedProposal);
|
|
999
|
-
logHandlerEvent(req, "info", {
|
|
1000
|
-
debug,
|
|
1001
|
-
msg: "AI proposal created",
|
|
1002
|
-
proposal: getProposalSummary(signedProposal)
|
|
1003
|
-
});
|
|
1004
|
-
return signedProposal;
|
|
1005
|
-
};
|
|
1006
154
|
const requestedDocumentScope = body?.documentScope;
|
|
1007
155
|
const configuredCollectionSlugs = getAllowedCollectionSlugs(req, options.collections);
|
|
1008
156
|
const configuredCollectionSlugSet = new Set(configuredCollectionSlugs);
|
|
@@ -1011,18 +159,10 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
1011
159
|
const requestedGlobalSlug = requestedDocumentScope?.type === "global" && typeof requestedDocumentScope.slug === "string" ? requestedDocumentScope.slug.trim() : undefined;
|
|
1012
160
|
const requestedDocumentID = requestedDocumentScope?.type === "collection" && typeof requestedDocumentScope.id === "string" ? requestedDocumentScope.id.trim() : undefined;
|
|
1013
161
|
if (requestedDocumentScope?.type === "collection" && (!requestedCollectionSlug || !configuredCollectionSlugSet.has(requestedCollectionSlug))) {
|
|
1014
|
-
return
|
|
1015
|
-
error: "The current collection is not available to the AI assistant."
|
|
1016
|
-
}, {
|
|
1017
|
-
status: 400
|
|
1018
|
-
});
|
|
162
|
+
return jsonError("The current collection is not available to the AI assistant.");
|
|
1019
163
|
}
|
|
1020
164
|
if (requestedDocumentScope?.type === "global" && (!requestedGlobalSlug || !allGlobalConfigs.some((global)=>global.slug === requestedGlobalSlug))) {
|
|
1021
|
-
return
|
|
1022
|
-
error: "The current global is not available to the AI assistant."
|
|
1023
|
-
}, {
|
|
1024
|
-
status: 400
|
|
1025
|
-
});
|
|
165
|
+
return jsonError("The current global is not available to the AI assistant.");
|
|
1026
166
|
}
|
|
1027
167
|
const collectionSlugs = requestedDocumentScope ? requestedCollectionSlug ? [
|
|
1028
168
|
requestedCollectionSlug
|
|
@@ -1030,36 +170,20 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
1030
170
|
const collectionSlugSet = new Set(collectionSlugs);
|
|
1031
171
|
const globalConfigs = requestedDocumentScope ? requestedGlobalSlug ? allGlobalConfigs.filter((global)=>global.slug === requestedGlobalSlug) : [] : allGlobalConfigs;
|
|
1032
172
|
const globalSlugs = globalConfigs.map((global)=>global.slug);
|
|
1033
|
-
const globalConfigsBySlug = new Map(globalConfigs.map((global)=>[
|
|
1034
|
-
global.slug,
|
|
1035
|
-
global
|
|
1036
|
-
]));
|
|
1037
173
|
const allowedCollections = req.payload.config.collections.filter((collection)=>collectionSlugSet.has(collection.slug));
|
|
1038
|
-
const allowedCollectionsBySlug = new Map(allowedCollections.map((collection)=>[
|
|
1039
|
-
collection.slug,
|
|
1040
|
-
collection
|
|
1041
|
-
]));
|
|
1042
174
|
if (collectionSlugs.length === 0 && globalConfigs.length === 0) {
|
|
1043
175
|
logHandlerEvent(req, "warn", {
|
|
1044
176
|
debug,
|
|
1045
177
|
msg: "AI chat blocked: no AI-enabled collections configured"
|
|
1046
178
|
});
|
|
1047
|
-
return
|
|
1048
|
-
error: "No AI-enabled collections are configured."
|
|
1049
|
-
}, {
|
|
1050
|
-
status: 400
|
|
1051
|
-
});
|
|
179
|
+
return jsonError("No AI-enabled collections are configured.");
|
|
1052
180
|
}
|
|
1053
|
-
const
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
fields: global.fields,
|
|
1060
|
-
parent: global.slug
|
|
1061
|
-
}))
|
|
1062
|
-
];
|
|
181
|
+
const schemaRegistry = createSchemaContextRegistry({
|
|
182
|
+
collections: allowedCollections,
|
|
183
|
+
globals: globalConfigs,
|
|
184
|
+
req
|
|
185
|
+
});
|
|
186
|
+
const { collectionConfigsBySlug: allowedCollectionsBySlug, globalConfigsBySlug } = schemaRegistry;
|
|
1063
187
|
const explicitlyMentionedDocumentKeys = new Set((body?.mentions || []).flatMap((mention)=>mention.type === "doc" && mention.collection && mention.id ? [
|
|
1064
188
|
`${mention.collection}:${mention.id}`
|
|
1065
189
|
] : []));
|
|
@@ -1067,11 +191,9 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
1067
191
|
mention.slug
|
|
1068
192
|
] : []));
|
|
1069
193
|
const mentionContext = await getMentionContext({
|
|
1070
|
-
blockContexts,
|
|
1071
|
-
collectionSlugs,
|
|
1072
194
|
collections: options.collections,
|
|
1073
|
-
globalSlugs,
|
|
1074
195
|
mentions: body?.mentions,
|
|
196
|
+
registry: schemaRegistry,
|
|
1075
197
|
req
|
|
1076
198
|
});
|
|
1077
199
|
if (requestedCollectionSlug && requestedDocumentID) {
|
|
@@ -1247,35 +369,29 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
1247
369
|
const updateLocalizedDataSchema = createLocalizedPayloadDataSchema(updateDataSchema);
|
|
1248
370
|
const globalDataSchema = focusedGlobalConfig ? focusedGlobalDataSchema : genericPayloadDataSchema;
|
|
1249
371
|
const globalLocalizedDataSchema = createLocalizedPayloadDataSchema(globalDataSchema);
|
|
1250
|
-
const
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
getDoc: {
|
|
372
|
+
const { addProposal: addSignedProposal, collectionTool, error: createToolError, proposalCollectionTool, proposalTool, proposals, repairableError: createRepairableToolError, toolFailures } = createChatToolFactory({
|
|
373
|
+
collections: options.collections,
|
|
374
|
+
currentDocument: requestedCollectionSlug && requestedDocumentID ? {
|
|
375
|
+
collection: requestedCollectionSlug,
|
|
376
|
+
id: requestedDocumentID
|
|
377
|
+
} : undefined,
|
|
378
|
+
debug,
|
|
379
|
+
prompt,
|
|
380
|
+
req
|
|
381
|
+
});
|
|
382
|
+
const toolRegistry = {
|
|
383
|
+
getDoc: collectionTool({
|
|
384
|
+
action: "read",
|
|
385
|
+
currentDocumentOnly: Boolean(requestedDocumentScope),
|
|
1265
386
|
description: "Read a compact document by collection and id. Optionally request up to 12 exact top-level schema fields. Explicitly @-mentioned documents remain complete.",
|
|
387
|
+
getDocumentID: ({ id })=>id,
|
|
1266
388
|
inputSchema: z.object({
|
|
1267
389
|
collection: collectionSlugSchema,
|
|
1268
390
|
fields: collectionReadFieldsSchema,
|
|
1269
391
|
id: z.string().min(1)
|
|
1270
392
|
}),
|
|
393
|
+
name: "getDoc",
|
|
1271
394
|
execute: async ({ collection, fields, id })=>{
|
|
1272
|
-
if (requestedDocumentScope && (collection !== requestedCollectionSlug || id !== requestedDocumentID)) {
|
|
1273
|
-
return createToolError({
|
|
1274
|
-
collection,
|
|
1275
|
-
message: "Only the current document can be read in this context.",
|
|
1276
|
-
tool: "getDoc"
|
|
1277
|
-
});
|
|
1278
|
-
}
|
|
1279
395
|
const explicitlyMentioned = explicitlyMentionedDocumentKeys.has(`${collection}:${id}`);
|
|
1280
396
|
const collectionConfig = allowedCollectionsBySlug.get(collection);
|
|
1281
397
|
const fieldSelection = resolveToolFieldSelection({
|
|
@@ -1308,7 +424,7 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
1308
424
|
});
|
|
1309
425
|
return redactSensitiveData(document);
|
|
1310
426
|
}
|
|
1311
|
-
},
|
|
427
|
+
}),
|
|
1312
428
|
listCollections: {
|
|
1313
429
|
description: "List collections; pass slug for one full schema.",
|
|
1314
430
|
inputSchema: z.object({
|
|
@@ -1416,8 +532,13 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
1416
532
|
}));
|
|
1417
533
|
}
|
|
1418
534
|
},
|
|
1419
|
-
proposeCreateDoc: {
|
|
535
|
+
proposeCreateDoc: proposalCollectionTool({
|
|
536
|
+
action: "create",
|
|
1420
537
|
description: "Propose document creation. Use exact schema fields; include required fields. Use localizedData for multi-locale writes.",
|
|
538
|
+
getRepairTarget: ({ collection, label })=>({
|
|
539
|
+
collection,
|
|
540
|
+
id: getSafeProposalLabel(label)
|
|
541
|
+
}),
|
|
1421
542
|
inputSchema: z.object({
|
|
1422
543
|
collection: focusedCollectionSlugSchema,
|
|
1423
544
|
data: createDataSchema.optional(),
|
|
@@ -1426,16 +547,9 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
1426
547
|
}).refine((value)=>Boolean(value.data || value.localizedData), {
|
|
1427
548
|
message: "Either data or localizedData is required."
|
|
1428
549
|
}),
|
|
550
|
+
name: "proposeCreateDoc",
|
|
1429
551
|
execute: async ({ collection, data, label, localizedData })=>{
|
|
1430
|
-
const permissionError = getDisallowedCollectionActionError(collection, "create");
|
|
1431
|
-
if (permissionError) return permissionError;
|
|
1432
552
|
const repairTargetID = getSafeProposalLabel(label);
|
|
1433
|
-
const repairLimitError = getProposalRepairLimitError({
|
|
1434
|
-
collection,
|
|
1435
|
-
id: repairTargetID,
|
|
1436
|
-
tool: "proposeCreateDoc"
|
|
1437
|
-
});
|
|
1438
|
-
if (repairLimitError) return repairLimitError;
|
|
1439
553
|
const collectionConfig = allowedCollectionsBySlug.get(collection);
|
|
1440
554
|
const collectionFields = collectionConfig?.fields || [];
|
|
1441
555
|
const preparedData = prepareProposalWriteData({
|
|
@@ -1505,18 +619,14 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
1505
619
|
});
|
|
1506
620
|
}
|
|
1507
621
|
}
|
|
1508
|
-
const uploadTargetsOutsideAttachments =
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
data: localeData,
|
|
1517
|
-
fields: collectionFields
|
|
1518
|
-
})) : []
|
|
1519
|
-
];
|
|
622
|
+
const { invalidRelationshipTargets, uploadTargetsOutsideAttachments } = await validateProposalReferences({
|
|
623
|
+
allowedAttachmentKeys,
|
|
624
|
+
data: preparedData.data,
|
|
625
|
+
fields: collectionFields,
|
|
626
|
+
localizedData: preparedData.localizedData,
|
|
627
|
+
priority: "attachments",
|
|
628
|
+
req
|
|
629
|
+
});
|
|
1520
630
|
if (uploadTargetsOutsideAttachments.length > 0) {
|
|
1521
631
|
return createToolError({
|
|
1522
632
|
collection,
|
|
@@ -1530,18 +640,6 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
1530
640
|
tool: "proposeCreateDoc"
|
|
1531
641
|
});
|
|
1532
642
|
}
|
|
1533
|
-
const invalidRelationshipTargets = [
|
|
1534
|
-
...preparedData.data ? await validateRelationshipTargetsExist({
|
|
1535
|
-
data: preparedData.data,
|
|
1536
|
-
fields: collectionFields,
|
|
1537
|
-
req
|
|
1538
|
-
}) : [],
|
|
1539
|
-
...preparedData.localizedData ? (await Promise.all(Object.values(preparedData.localizedData).map((localeData)=>validateRelationshipTargetsExist({
|
|
1540
|
-
data: localeData,
|
|
1541
|
-
fields: collectionFields,
|
|
1542
|
-
req
|
|
1543
|
-
})))).flat() : []
|
|
1544
|
-
];
|
|
1545
643
|
if (invalidRelationshipTargets.length > 0) {
|
|
1546
644
|
return createToolError({
|
|
1547
645
|
collection,
|
|
@@ -1571,24 +669,19 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
1571
669
|
};
|
|
1572
670
|
return addSignedProposal(proposal);
|
|
1573
671
|
}
|
|
1574
|
-
},
|
|
1575
|
-
proposeDeleteDoc: {
|
|
672
|
+
}),
|
|
673
|
+
proposeDeleteDoc: collectionTool({
|
|
674
|
+
action: "delete",
|
|
675
|
+
currentDocumentOnly: Boolean(requestedDocumentScope),
|
|
1576
676
|
description: "Propose document deletion.",
|
|
677
|
+
getDocumentID: ({ id })=>id,
|
|
1577
678
|
inputSchema: z.object({
|
|
1578
679
|
collection: collectionSlugSchema,
|
|
1579
680
|
id: z.string().min(1),
|
|
1580
681
|
label: z.string().min(1)
|
|
1581
682
|
}),
|
|
683
|
+
name: "proposeDeleteDoc",
|
|
1582
684
|
execute: async ({ collection, id, label })=>{
|
|
1583
|
-
if (requestedDocumentScope && (collection !== requestedCollectionSlug || id !== requestedDocumentID)) {
|
|
1584
|
-
return createToolError({
|
|
1585
|
-
collection,
|
|
1586
|
-
message: "Only the current document can be deleted in this context.",
|
|
1587
|
-
tool: "proposeDeleteDoc"
|
|
1588
|
-
});
|
|
1589
|
-
}
|
|
1590
|
-
const permissionError = getDisallowedCollectionActionError(collection, "delete");
|
|
1591
|
-
if (permissionError) return permissionError;
|
|
1592
685
|
const proposal = {
|
|
1593
686
|
action: "delete",
|
|
1594
687
|
collection,
|
|
@@ -1600,9 +693,16 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
1600
693
|
};
|
|
1601
694
|
return addSignedProposal(proposal);
|
|
1602
695
|
}
|
|
1603
|
-
},
|
|
1604
|
-
proposeUpdateDoc: {
|
|
696
|
+
}),
|
|
697
|
+
proposeUpdateDoc: proposalCollectionTool({
|
|
698
|
+
action: "update",
|
|
699
|
+
currentDocumentOnly: Boolean(requestedDocumentScope),
|
|
1605
700
|
description: "Propose document update. Use exact schema fields. Use localizedData for multi-locale writes.",
|
|
701
|
+
getDocumentID: ({ id })=>id,
|
|
702
|
+
getRepairTarget: ({ collection, id })=>({
|
|
703
|
+
collection,
|
|
704
|
+
id
|
|
705
|
+
}),
|
|
1606
706
|
inputSchema: z.object({
|
|
1607
707
|
collection: updateHasMultipleTargets ? collectionSlugSchema : focusedCollectionSlugSchema,
|
|
1608
708
|
data: updateDataSchema.optional(),
|
|
@@ -1612,22 +712,8 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
1612
712
|
}).refine((value)=>Boolean(value.data || value.localizedData), {
|
|
1613
713
|
message: "Either data or localizedData is required."
|
|
1614
714
|
}),
|
|
715
|
+
name: "proposeUpdateDoc",
|
|
1615
716
|
execute: async ({ collection, data, id, label, localizedData })=>{
|
|
1616
|
-
if (requestedDocumentScope && (collection !== requestedCollectionSlug || id !== requestedDocumentID)) {
|
|
1617
|
-
return createToolError({
|
|
1618
|
-
collection,
|
|
1619
|
-
message: "Only the current document can be updated in this context.",
|
|
1620
|
-
tool: "proposeUpdateDoc"
|
|
1621
|
-
});
|
|
1622
|
-
}
|
|
1623
|
-
const permissionError = getDisallowedCollectionActionError(collection, "update");
|
|
1624
|
-
if (permissionError) return permissionError;
|
|
1625
|
-
const repairLimitError = getProposalRepairLimitError({
|
|
1626
|
-
collection,
|
|
1627
|
-
id,
|
|
1628
|
-
tool: "proposeUpdateDoc"
|
|
1629
|
-
});
|
|
1630
|
-
if (repairLimitError) return repairLimitError;
|
|
1631
717
|
const collectionConfig = allowedCollectionsBySlug.get(collection);
|
|
1632
718
|
const collectionFields = collectionConfig?.fields || [];
|
|
1633
719
|
const preparedData = prepareProposalWriteData({
|
|
@@ -1650,18 +736,14 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
1650
736
|
tool: "proposeUpdateDoc"
|
|
1651
737
|
});
|
|
1652
738
|
}
|
|
1653
|
-
const invalidRelationshipTargets =
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
fields: collectionFields,
|
|
1662
|
-
req
|
|
1663
|
-
})))).flat() : []
|
|
1664
|
-
];
|
|
739
|
+
const { invalidRelationshipTargets, uploadTargetsOutsideAttachments } = await validateProposalReferences({
|
|
740
|
+
allowedAttachmentKeys,
|
|
741
|
+
data: preparedData.data,
|
|
742
|
+
fields: collectionFields,
|
|
743
|
+
localizedData: preparedData.localizedData,
|
|
744
|
+
priority: "relationships",
|
|
745
|
+
req
|
|
746
|
+
});
|
|
1665
747
|
if (invalidRelationshipTargets.length > 0) {
|
|
1666
748
|
return createToolError({
|
|
1667
749
|
collection,
|
|
@@ -1672,18 +754,6 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
1672
754
|
tool: "proposeUpdateDoc"
|
|
1673
755
|
});
|
|
1674
756
|
}
|
|
1675
|
-
const uploadTargetsOutsideAttachments = [
|
|
1676
|
-
...preparedData.data ? getUploadTargetsOutsideAttachments({
|
|
1677
|
-
allowedAttachmentKeys,
|
|
1678
|
-
data: preparedData.data,
|
|
1679
|
-
fields: collectionFields
|
|
1680
|
-
}) : [],
|
|
1681
|
-
...preparedData.localizedData ? Object.values(preparedData.localizedData).flatMap((localeData)=>getUploadTargetsOutsideAttachments({
|
|
1682
|
-
allowedAttachmentKeys,
|
|
1683
|
-
data: localeData,
|
|
1684
|
-
fields: collectionFields
|
|
1685
|
-
})) : []
|
|
1686
|
-
];
|
|
1687
757
|
if (uploadTargetsOutsideAttachments.length > 0) {
|
|
1688
758
|
return createToolError({
|
|
1689
759
|
collection,
|
|
@@ -1713,18 +783,9 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
1713
783
|
};
|
|
1714
784
|
return addSignedProposal(proposal);
|
|
1715
785
|
}
|
|
1716
|
-
},
|
|
1717
|
-
proposeUpdateGlobal: {
|
|
1718
|
-
|
|
1719
|
-
inputSchema: z.object({
|
|
1720
|
-
data: globalDataSchema.optional(),
|
|
1721
|
-
label: z.string().min(1),
|
|
1722
|
-
localizedData: globalLocalizedDataSchema.optional(),
|
|
1723
|
-
slug: requestedGlobalSlug ? z.literal(requestedGlobalSlug) : z.string().min(1)
|
|
1724
|
-
}).refine((value)=>Boolean(value.data || value.localizedData), {
|
|
1725
|
-
message: "Either data or localizedData is required."
|
|
1726
|
-
}),
|
|
1727
|
-
execute: async ({ data, label, localizedData, slug })=>{
|
|
786
|
+
}),
|
|
787
|
+
proposeUpdateGlobal: proposalTool({
|
|
788
|
+
beforeRepair: ({ slug })=>{
|
|
1728
789
|
if (requestedDocumentScope && slug !== requestedGlobalSlug) {
|
|
1729
790
|
return createToolError({
|
|
1730
791
|
message: "Only the current global can be updated in this context.",
|
|
@@ -1732,19 +793,31 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
1732
793
|
tool: "proposeUpdateGlobal"
|
|
1733
794
|
});
|
|
1734
795
|
}
|
|
1735
|
-
|
|
1736
|
-
if (!globalConfig) {
|
|
796
|
+
if (!globalConfigsBySlug.has(slug)) {
|
|
1737
797
|
return createToolError({
|
|
1738
798
|
message: `Unknown global: ${slug}`,
|
|
1739
799
|
slug,
|
|
1740
800
|
tool: "proposeUpdateGlobal"
|
|
1741
801
|
});
|
|
1742
802
|
}
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
803
|
+
return null;
|
|
804
|
+
},
|
|
805
|
+
description: "Propose global update. Use localizedData for multi-locale writes.",
|
|
806
|
+
getRepairTarget: ({ slug })=>({
|
|
807
|
+
slug
|
|
808
|
+
}),
|
|
809
|
+
inputSchema: z.object({
|
|
810
|
+
data: globalDataSchema.optional(),
|
|
811
|
+
label: z.string().min(1),
|
|
812
|
+
localizedData: globalLocalizedDataSchema.optional(),
|
|
813
|
+
slug: requestedGlobalSlug ? z.literal(requestedGlobalSlug) : z.string().min(1)
|
|
814
|
+
}).refine((value)=>Boolean(value.data || value.localizedData), {
|
|
815
|
+
message: "Either data or localizedData is required."
|
|
816
|
+
}),
|
|
817
|
+
name: "proposeUpdateGlobal",
|
|
818
|
+
execute: async ({ data, label, localizedData, slug })=>{
|
|
819
|
+
const globalConfig = globalConfigsBySlug.get(slug);
|
|
820
|
+
if (!globalConfig) return null;
|
|
1748
821
|
const preparedData = prepareProposalWriteData({
|
|
1749
822
|
collectionConfig: {
|
|
1750
823
|
fields: globalConfig.fields || [],
|
|
@@ -1782,7 +855,7 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
1782
855
|
};
|
|
1783
856
|
return addSignedProposal(proposal);
|
|
1784
857
|
}
|
|
1785
|
-
},
|
|
858
|
+
}),
|
|
1786
859
|
searchDocs: {
|
|
1787
860
|
description: "Search compact documents in one collection. Returns default identity fields or up to 12 requested exact top-level schema fields.",
|
|
1788
861
|
inputSchema: z.object({
|
|
@@ -1852,7 +925,7 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
1852
925
|
hasCurrentGlobal: Boolean(requestedGlobalSlug),
|
|
1853
926
|
intent: chatIntent
|
|
1854
927
|
});
|
|
1855
|
-
const tools = Object.fromEntries(Object.entries(
|
|
928
|
+
const tools = Object.fromEntries(Object.entries(toolRegistry).filter(([name])=>scopedToolNames.has(name)));
|
|
1856
929
|
debug.tools = Object.keys(tools);
|
|
1857
930
|
if (intentToolChoice && !scopedToolNames.has(intentToolChoice.toolName)) {
|
|
1858
931
|
intentToolChoice = undefined;
|
|
@@ -1861,63 +934,21 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
1861
934
|
const sendEvent = (controller, event, data)=>{
|
|
1862
935
|
controller.enqueue(encoder.encode(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`));
|
|
1863
936
|
};
|
|
1864
|
-
const model = await
|
|
1865
|
-
apiKey: providerConfig.apiKey,
|
|
1866
|
-
...managedProvider?.baseURL ? {
|
|
1867
|
-
baseURL: managedProvider.baseURL
|
|
1868
|
-
} : {},
|
|
1869
|
-
model: providerConfig.modelID,
|
|
1870
|
-
provider
|
|
1871
|
-
});
|
|
937
|
+
const model = await resolvedAIRequest.loadModel();
|
|
1872
938
|
const promptCaching = options.promptCaching !== false;
|
|
1873
939
|
const explicitPromptCaching = promptCaching && !managedProvider?.baseURL;
|
|
1874
|
-
const
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
"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.",
|
|
1885
|
-
"Put concrete field values only in tool data, not visible text.",
|
|
1886
|
-
"For blocks fields: use exact blockType values from schema, exact field names, and complete objects for required block fields.",
|
|
1887
|
-
"For arrays: every item must be an object matching the child field schema, not free text.",
|
|
1888
|
-
"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.",
|
|
1889
|
-
"Read tools return compact identity fields by default. Request only the additional fields needed for the current task; full schemas remain available separately.",
|
|
1890
|
-
"If a proposal tool returns retryable=true, correct only repair.issues and call the same proposal tool once more. If retryable=false, do not retry that tool target."
|
|
1891
|
-
];
|
|
1892
|
-
const cacheableSystemInstructions = [
|
|
1893
|
-
`Collection aliases: ${JSON.stringify(collectionAliasMap)}.`,
|
|
1894
|
-
`Focused required create fields: ${JSON.stringify(focusedRequiredFieldsByCollection)}.`,
|
|
1895
|
-
`Focused title fields: ${JSON.stringify(focusedTitleFieldByCollection)}. Infer concise titles when needed.`
|
|
1896
|
-
];
|
|
1897
|
-
const dynamicSystemInstructions = [
|
|
1898
|
-
`Likely collection matches for this prompt: ${JSON.stringify(likelyCollectionMatches)}.`,
|
|
1899
|
-
`Request intent: ${chatIntent}. Preferred proposal tool: ${intentToolChoice?.toolName || "none"}.`,
|
|
1900
|
-
"Visible response: plain text, under 40 words, no Markdown, no proposed content."
|
|
1901
|
-
];
|
|
1902
|
-
const system = createCachedSystemMessages({
|
|
1903
|
-
cacheableContext: cacheableMentionContext,
|
|
1904
|
-
cacheableInstructions: cacheableSystemInstructions,
|
|
1905
|
-
dynamicInstructions: dynamicSystemInstructions,
|
|
1906
|
-
enabled: explicitPromptCaching,
|
|
940
|
+
const { dynamicMentionContext, promptCacheProviderOptions, system } = createChatPromptContext({
|
|
941
|
+
chatIntent,
|
|
942
|
+
collectionAliasMap,
|
|
943
|
+
focusedRequiredFieldsByCollection,
|
|
944
|
+
focusedTitleFieldByCollection,
|
|
945
|
+
intentToolName: intentToolChoice?.toolName,
|
|
946
|
+
likelyCollectionMatches,
|
|
947
|
+
mentionContext,
|
|
948
|
+
modelID: resolvedAIRequest.modelID,
|
|
949
|
+
promptCaching: explicitPromptCaching,
|
|
1907
950
|
provider,
|
|
1908
|
-
|
|
1909
|
-
});
|
|
1910
|
-
const promptCacheProviderOptions = createPromptCacheProviderOptions({
|
|
1911
|
-
cacheKeyParts: [
|
|
1912
|
-
cacheableMentionContext,
|
|
1913
|
-
cacheableSystemInstructions,
|
|
1914
|
-
[
|
|
1915
|
-
...scopedToolNames
|
|
1916
|
-
].sort()
|
|
1917
|
-
],
|
|
1918
|
-
enabled: explicitPromptCaching,
|
|
1919
|
-
model: providerConfig.modelID,
|
|
1920
|
-
provider
|
|
951
|
+
scopedToolNames
|
|
1921
952
|
});
|
|
1922
953
|
const result = streamText({
|
|
1923
954
|
maxOutputTokens: options.maxOutputTokens || 700,
|
|
@@ -1958,15 +989,9 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
1958
989
|
if (part.type === "finish") {
|
|
1959
990
|
const finishPart = part;
|
|
1960
991
|
usage = finishPart.totalUsage || finishPart.usage || null;
|
|
1961
|
-
if (usage
|
|
992
|
+
if (usage) {
|
|
1962
993
|
try {
|
|
1963
|
-
await
|
|
1964
|
-
model: providerConfig.modelID,
|
|
1965
|
-
provider: managedProvider?.id || provider,
|
|
1966
|
-
req,
|
|
1967
|
-
usage,
|
|
1968
|
-
userID: user.id
|
|
1969
|
-
});
|
|
994
|
+
await resolvedAIRequest.recordUsage(usage);
|
|
1970
995
|
} catch (err) {
|
|
1971
996
|
req.payload.logger.error({
|
|
1972
997
|
err,
|
|
@@ -2070,15 +1095,14 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
2070
1095
|
}
|
|
2071
1096
|
});
|
|
2072
1097
|
} catch (err) {
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
status: 500
|
|
1098
|
+
return logHandlerError({
|
|
1099
|
+
details: {
|
|
1100
|
+
debug
|
|
1101
|
+
},
|
|
1102
|
+
error: err,
|
|
1103
|
+
logMessage: "AI chat request failed",
|
|
1104
|
+
publicMessage: "AI request failed.",
|
|
1105
|
+
req
|
|
2082
1106
|
});
|
|
2083
1107
|
}
|
|
2084
|
-
};
|
|
1108
|
+
});
|