@jupyternaut/persona 0.0.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/chat-commands/mention.d.ts +9 -0
- package/lib/chat-commands/mention.js +30 -0
- package/lib/completion/completion-provider.d.ts +86 -0
- package/lib/completion/completion-provider.js +246 -0
- package/lib/completion/index.d.ts +2 -0
- package/lib/completion/index.js +1 -0
- package/lib/components/completion-status.d.ts +26 -0
- package/lib/components/completion-status.js +52 -0
- package/lib/components/index.d.ts +2 -0
- package/lib/components/index.js +1 -0
- package/lib/diff-manager.d.ts +25 -0
- package/lib/diff-manager.js +60 -0
- package/lib/index.d.ts +8 -0
- package/lib/index.js +522 -0
- package/lib/models/settings-model.d.ts +36 -0
- package/lib/models/settings-model.js +356 -0
- package/lib/persona-registry.d.ts +15 -0
- package/lib/persona-registry.js +29 -0
- package/lib/persona.d.ts +66 -0
- package/lib/persona.js +414 -0
- package/lib/process-attachments.d.ts +5 -0
- package/lib/process-attachments.js +287 -0
- package/lib/tokens.d.ts +101 -0
- package/lib/tokens.js +20 -0
- package/lib/widgets/ai-settings.d.ts +54 -0
- package/lib/widgets/ai-settings.js +572 -0
- package/lib/widgets/provider-config-dialog.d.ts +16 -0
- package/lib/widgets/provider-config-dialog.js +384 -0
- package/package.json +111 -7
- package/schema/settings-model.json +287 -0
- package/src/chat-commands/mention.tsx +46 -0
- package/src/completion/completion-provider.ts +350 -0
- package/src/completion/index.ts +1 -0
- package/src/components/completion-status.tsx +93 -0
- package/src/components/index.ts +1 -0
- package/src/diff-manager.ts +81 -0
- package/src/index.ts +710 -0
- package/src/models/settings-model.ts +415 -0
- package/src/persona-registry.ts +46 -0
- package/src/persona.ts +610 -0
- package/src/process-attachments.ts +369 -0
- package/src/tokens.ts +121 -0
- package/src/widgets/ai-settings.tsx +1308 -0
- package/src/widgets/provider-config-dialog.tsx +997 -0
- package/style/base.css +14 -0
- package/style/index.css +1 -0
- package/style/index.js +1 -0
- package/README.md +0 -3
- package/index.js +0 -1
package/lib/persona.js
ADDED
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
import * as nbformat from '@jupyterlab/nbformat';
|
|
2
|
+
import { modelSupportsAudio, modelSupportsImages, modelSupportsPdf } from '@jupyternaut/agent';
|
|
3
|
+
import { Signal } from '@lumino/signaling';
|
|
4
|
+
import { processAttachments } from './process-attachments';
|
|
5
|
+
function extractToolSummary(toolName, input) {
|
|
6
|
+
try {
|
|
7
|
+
const parsed = JSON.parse(input);
|
|
8
|
+
switch (toolName) {
|
|
9
|
+
case 'execute_command':
|
|
10
|
+
return parsed.commandId ?? '';
|
|
11
|
+
case 'discover_commands':
|
|
12
|
+
case 'discover_skills':
|
|
13
|
+
case 'web_search':
|
|
14
|
+
return parsed.query ? `query: "${parsed.query}"` : '';
|
|
15
|
+
case 'load_skill':
|
|
16
|
+
return parsed.name
|
|
17
|
+
? parsed.resource
|
|
18
|
+
? `${parsed.name} (${parsed.resource})`
|
|
19
|
+
: parsed.name
|
|
20
|
+
: '';
|
|
21
|
+
case 'browser_fetch':
|
|
22
|
+
case 'web_fetch':
|
|
23
|
+
return parsed.url ?? '';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// ignore malformed input
|
|
28
|
+
}
|
|
29
|
+
return '';
|
|
30
|
+
}
|
|
31
|
+
function formatToolOutput(outputData) {
|
|
32
|
+
if (typeof outputData === 'string') {
|
|
33
|
+
return outputData;
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
return JSON.stringify(outputData, null, 2);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return '[Complex object - cannot serialize]';
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function isPlainObject(value) {
|
|
43
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
44
|
+
}
|
|
45
|
+
function isDisplayOutput(value) {
|
|
46
|
+
if (!isPlainObject(value)) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
const output = value;
|
|
50
|
+
return (nbformat.isDisplayData(output) ||
|
|
51
|
+
nbformat.isDisplayUpdate(output) ||
|
|
52
|
+
nbformat.isExecuteResult(output));
|
|
53
|
+
}
|
|
54
|
+
function toDisplayOutputs(value) {
|
|
55
|
+
if (isDisplayOutput(value)) {
|
|
56
|
+
return [value];
|
|
57
|
+
}
|
|
58
|
+
if (Array.isArray(value)) {
|
|
59
|
+
return value.filter(isDisplayOutput);
|
|
60
|
+
}
|
|
61
|
+
if (!isPlainObject(value)) {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
if (Array.isArray(value.outputs)) {
|
|
65
|
+
return value.outputs.filter(isDisplayOutput);
|
|
66
|
+
}
|
|
67
|
+
if ('result' in value) {
|
|
68
|
+
return toDisplayOutputs(value.result);
|
|
69
|
+
}
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
function extractMimeBundles(content, trustedMimeTypes) {
|
|
73
|
+
return toDisplayOutputs(content)
|
|
74
|
+
.map((output) => {
|
|
75
|
+
const data = output.data;
|
|
76
|
+
if (!isPlainObject(data) || Object.keys(data).length === 0) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
data: data,
|
|
81
|
+
...(isPlainObject(output.metadata)
|
|
82
|
+
? {
|
|
83
|
+
metadata: output.metadata
|
|
84
|
+
}
|
|
85
|
+
: {}),
|
|
86
|
+
...(Object.keys(data).some(m => trustedMimeTypes.has(m))
|
|
87
|
+
? { trusted: true }
|
|
88
|
+
: {})
|
|
89
|
+
};
|
|
90
|
+
})
|
|
91
|
+
.filter((b) => b !== null);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Links an IAgentManager to an IChatModel for the Jupyternaut persona.
|
|
95
|
+
*
|
|
96
|
+
* Monitors new messages arriving on the chat model and responds when the
|
|
97
|
+
* persona trigger string is mentioned. The handler and its agent stay alive
|
|
98
|
+
* as long as the associated chat widget is open, so conversation history is
|
|
99
|
+
* preserved across multiple mentions.
|
|
100
|
+
*/
|
|
101
|
+
export class Persona {
|
|
102
|
+
constructor(options) {
|
|
103
|
+
this._model = options.model;
|
|
104
|
+
this._agent = options.agentManager;
|
|
105
|
+
this._persona = options.persona;
|
|
106
|
+
this._settingsModel = options.settingsModel;
|
|
107
|
+
this._providerRegistry = options.providerRegistry;
|
|
108
|
+
this._documentManager = options.documentManager;
|
|
109
|
+
for (const message of options.model.messages) {
|
|
110
|
+
this._respondedToIds.add(message.id);
|
|
111
|
+
}
|
|
112
|
+
this._agent.agentEvent.connect(this._onAgentEvent, this);
|
|
113
|
+
this._agent.activeProviderChanged.connect(this._onActiveProviderChanged, this);
|
|
114
|
+
this._model.messagesUpdated.connect(this._onMessagesUpdated, this);
|
|
115
|
+
this._model.disposed.connect(this.dispose, this);
|
|
116
|
+
}
|
|
117
|
+
dispose() {
|
|
118
|
+
this._agent.agentEvent.disconnect(this._onAgentEvent, this);
|
|
119
|
+
this._agent.activeProviderChanged.disconnect(this._onActiveProviderChanged, this);
|
|
120
|
+
this._model.messagesUpdated.disconnect(this._onMessagesUpdated, this);
|
|
121
|
+
}
|
|
122
|
+
get agentManager() {
|
|
123
|
+
return this._agent;
|
|
124
|
+
}
|
|
125
|
+
get model() {
|
|
126
|
+
return this._model;
|
|
127
|
+
}
|
|
128
|
+
get isBusy() {
|
|
129
|
+
return this._busy;
|
|
130
|
+
}
|
|
131
|
+
get busyChanged() {
|
|
132
|
+
return this._busyChanged;
|
|
133
|
+
}
|
|
134
|
+
async _onMessagesUpdated() {
|
|
135
|
+
const unhandled = this._model.messages.filter(m => !this._respondedToIds.has(m.id) &&
|
|
136
|
+
!m.sender.bot &&
|
|
137
|
+
(!this.requireMention || m.mentions?.includes(this._persona)));
|
|
138
|
+
for (const message of unhandled) {
|
|
139
|
+
this._respondedToIds.add(message.id);
|
|
140
|
+
}
|
|
141
|
+
for (const message of unhandled) {
|
|
142
|
+
const personaMention = `@${this._persona.mention_name}`;
|
|
143
|
+
const body = message.body.replace(personaMention, '').trim();
|
|
144
|
+
await this._respond(body || message.body, message.attachments);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
async _respond(body, attachments) {
|
|
148
|
+
this._busy = true;
|
|
149
|
+
this._busyChanged.emit(true);
|
|
150
|
+
this._model.updateWriters([{ user: this._persona }]);
|
|
151
|
+
try {
|
|
152
|
+
let content = body;
|
|
153
|
+
if (attachments && attachments.length > 0) {
|
|
154
|
+
const providerConfig = this._settingsModel.getProvider(this._agent.activeProvider);
|
|
155
|
+
content = await processAttachments(attachments, this._documentManager, body, modelSupportsImages(providerConfig, this._providerRegistry), modelSupportsPdf(providerConfig, this._providerRegistry), modelSupportsAudio(providerConfig, this._providerRegistry));
|
|
156
|
+
}
|
|
157
|
+
await this._agent.generateResponse(content);
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
console.error('Persona: error generating response', error);
|
|
161
|
+
}
|
|
162
|
+
finally {
|
|
163
|
+
this._busy = false;
|
|
164
|
+
this._busyChanged.emit(false);
|
|
165
|
+
this._model.updateWriters([]);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
rebuildHistory() {
|
|
169
|
+
return this._rebuildHistory();
|
|
170
|
+
}
|
|
171
|
+
_onActiveProviderChanged() {
|
|
172
|
+
const providerConfig = this._settingsModel.getProvider(this._agent.activeProvider);
|
|
173
|
+
const modelKey = providerConfig
|
|
174
|
+
? `${providerConfig.provider}:${providerConfig.model}`
|
|
175
|
+
: undefined;
|
|
176
|
+
if (modelKey && modelKey !== this._currentModelKey) {
|
|
177
|
+
this._currentModelKey = modelKey;
|
|
178
|
+
this._rebuildHistory().catch(e => console.warn('Failed to rebuild history on model change:', e));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
async _rebuildHistory() {
|
|
182
|
+
const providerConfig = this._settingsModel.getProvider(this._agent.activeProvider);
|
|
183
|
+
const supportsImages = modelSupportsImages(providerConfig, this._providerRegistry);
|
|
184
|
+
const supportsPdf = modelSupportsPdf(providerConfig, this._providerRegistry);
|
|
185
|
+
const supportsAudio = modelSupportsAudio(providerConfig, this._providerRegistry);
|
|
186
|
+
const modelMessages = [];
|
|
187
|
+
for (const msg of this._model.messages) {
|
|
188
|
+
const isAI = msg.sender.bot === true;
|
|
189
|
+
if (!isAI && msg.attachments?.length) {
|
|
190
|
+
const enhancedContent = await processAttachments(msg.attachments, this._documentManager, msg.body, supportsImages, supportsPdf, supportsAudio);
|
|
191
|
+
modelMessages.push({ role: 'user', content: enhancedContent });
|
|
192
|
+
}
|
|
193
|
+
else if (msg.body) {
|
|
194
|
+
modelMessages.push({
|
|
195
|
+
role: isAI ? 'assistant' : 'user',
|
|
196
|
+
content: msg.body
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
this._agent.setHistory(modelMessages);
|
|
201
|
+
}
|
|
202
|
+
_onAgentEvent(_, event) {
|
|
203
|
+
switch (event.type) {
|
|
204
|
+
case 'message_start':
|
|
205
|
+
this._handleMessageStart(event);
|
|
206
|
+
break;
|
|
207
|
+
case 'message_chunk':
|
|
208
|
+
this._handleMessageChunk(event);
|
|
209
|
+
break;
|
|
210
|
+
case 'message_complete':
|
|
211
|
+
this._handleMessageComplete(event);
|
|
212
|
+
break;
|
|
213
|
+
case 'tool_call_start':
|
|
214
|
+
this._handleToolCallStart(event);
|
|
215
|
+
break;
|
|
216
|
+
case 'tool_call_complete':
|
|
217
|
+
this._handleToolCallComplete(event);
|
|
218
|
+
break;
|
|
219
|
+
case 'tool_approval_request':
|
|
220
|
+
this._handleToolApprovalRequest(event);
|
|
221
|
+
break;
|
|
222
|
+
case 'tool_approval_resolved':
|
|
223
|
+
this._handleToolApprovalResolved(event);
|
|
224
|
+
break;
|
|
225
|
+
case 'error':
|
|
226
|
+
this._handleError(event);
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
async _handleMessageStart(event) {
|
|
231
|
+
const message = {
|
|
232
|
+
body: '',
|
|
233
|
+
sender: this._persona
|
|
234
|
+
};
|
|
235
|
+
const msgId = await this._model.sendMessage(message);
|
|
236
|
+
const streamingMessage = this._model.messages.find(m => m.id === msgId) ?? null;
|
|
237
|
+
if (streamingMessage) {
|
|
238
|
+
this._streamingMessage.set(event.data.messageId, streamingMessage);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
_handleMessageChunk(event) {
|
|
242
|
+
const streamingMessage = this._streamingMessage.get(event.data.messageId);
|
|
243
|
+
if (streamingMessage) {
|
|
244
|
+
streamingMessage.update({ body: event.data.fullContent });
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
_handleMessageComplete(event) {
|
|
248
|
+
const streamingMessage = this._streamingMessage.get(event.data.messageId);
|
|
249
|
+
if (streamingMessage) {
|
|
250
|
+
streamingMessage.update({ body: event.data.content });
|
|
251
|
+
this._streamingMessage.delete(event.data.messageId);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
async _handleToolCallStart(event) {
|
|
255
|
+
const summary = extractToolSummary(event.data.toolName, event.data.input);
|
|
256
|
+
const shouldAutoRenderMimeBundles = this._computeShouldAutoRenderMimeBundles(event.data.toolName, event.data.input);
|
|
257
|
+
const context = {
|
|
258
|
+
toolCallId: event.data.callId,
|
|
259
|
+
messageId: '',
|
|
260
|
+
toolName: event.data.toolName,
|
|
261
|
+
title: event.data.title,
|
|
262
|
+
input: event.data.input,
|
|
263
|
+
status: 'pending',
|
|
264
|
+
summary,
|
|
265
|
+
shouldAutoRenderMimeBundles
|
|
266
|
+
};
|
|
267
|
+
const displayName = context.title ?? context.toolName;
|
|
268
|
+
const messageId = await this._model.sendMessage({
|
|
269
|
+
body: '',
|
|
270
|
+
mime_model: {
|
|
271
|
+
data: {
|
|
272
|
+
'application/vnd.jupyter.chat.components': 'grouped-tool-calls'
|
|
273
|
+
},
|
|
274
|
+
metadata: {
|
|
275
|
+
toolCalls: [
|
|
276
|
+
{
|
|
277
|
+
toolCallId: context.toolCallId,
|
|
278
|
+
title: context.summary
|
|
279
|
+
? `${displayName} : ${context.summary}`
|
|
280
|
+
: displayName,
|
|
281
|
+
kind: context.toolName,
|
|
282
|
+
status: 'in_progress',
|
|
283
|
+
rawInput: context.input
|
|
284
|
+
}
|
|
285
|
+
]
|
|
286
|
+
}
|
|
287
|
+
},
|
|
288
|
+
sender: this._persona
|
|
289
|
+
});
|
|
290
|
+
if (messageId) {
|
|
291
|
+
context.messageId = messageId;
|
|
292
|
+
this._toolContexts.set(event.data.callId, context);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
_handleToolCallComplete(event) {
|
|
296
|
+
const context = this._toolContexts.get(event.data.callId);
|
|
297
|
+
const status = event.data.isError ? 'error' : 'completed';
|
|
298
|
+
this._updateToolCallUI(event.data.callId, status, formatToolOutput(event.data.outputData));
|
|
299
|
+
if (!event.data.isError && context?.shouldAutoRenderMimeBundles) {
|
|
300
|
+
const trustedMimeTypes = new Set(this._settingsModel.config.trustedMimeTypesForAutoRender);
|
|
301
|
+
for (const bundle of extractMimeBundles(event.data.outputData, trustedMimeTypes)) {
|
|
302
|
+
this._model.sendMessage({
|
|
303
|
+
body: '',
|
|
304
|
+
mime_model: bundle,
|
|
305
|
+
sender: this._persona
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
this._toolContexts.delete(event.data.callId);
|
|
310
|
+
}
|
|
311
|
+
_computeShouldAutoRenderMimeBundles(toolName, input) {
|
|
312
|
+
if (toolName !== 'execute_command') {
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
try {
|
|
316
|
+
const parsed = JSON.parse(input);
|
|
317
|
+
return (typeof parsed.commandId === 'string' &&
|
|
318
|
+
this._settingsModel.config.commandsAutoRenderMimeBundles.includes(parsed.commandId));
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
_handleToolApprovalRequest(event) {
|
|
325
|
+
const context = this._toolContexts.get(event.data.toolCallId);
|
|
326
|
+
if (!context) {
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
context.input = JSON.stringify(event.data.args, null, 2);
|
|
330
|
+
this._updateToolCallUI(event.data.toolCallId, 'awaiting_approval');
|
|
331
|
+
}
|
|
332
|
+
_handleToolApprovalResolved(event) {
|
|
333
|
+
const context = this._toolContexts.get(event.data.toolCallId);
|
|
334
|
+
if (!context) {
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
const status = event.data.approved ? 'approved' : 'rejected';
|
|
338
|
+
this._updateToolCallUI(event.data.toolCallId, status);
|
|
339
|
+
if (!event.data.approved) {
|
|
340
|
+
this._toolContexts.delete(event.data.toolCallId);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
_handleError(event) {
|
|
344
|
+
this._model.sendMessage({
|
|
345
|
+
body: '',
|
|
346
|
+
mime_model: {
|
|
347
|
+
data: { 'application/vnd.jupyter.chat.components': 'error' },
|
|
348
|
+
metadata: {
|
|
349
|
+
errorMessage: `Error generating response: ${event.data.error.message}`
|
|
350
|
+
}
|
|
351
|
+
},
|
|
352
|
+
sender: this._persona
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
_updateToolCallUI(toolCallId, status, output) {
|
|
356
|
+
const context = this._toolContexts.get(toolCallId);
|
|
357
|
+
if (!context) {
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
const message = this._model.messages.find(m => m.id === context.messageId);
|
|
361
|
+
if (!message) {
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
context.status = status;
|
|
365
|
+
const displayName = context.title ?? context.toolName;
|
|
366
|
+
message.update({
|
|
367
|
+
mime_model: {
|
|
368
|
+
data: {
|
|
369
|
+
'application/vnd.jupyter.chat.components': 'grouped-tool-calls'
|
|
370
|
+
},
|
|
371
|
+
metadata: {
|
|
372
|
+
toolCalls: [
|
|
373
|
+
{
|
|
374
|
+
toolCallId: context.toolCallId,
|
|
375
|
+
title: context.summary
|
|
376
|
+
? `${displayName} : ${context.summary}`
|
|
377
|
+
: displayName,
|
|
378
|
+
kind: context.toolName,
|
|
379
|
+
status: context.status,
|
|
380
|
+
rawInput: context.input,
|
|
381
|
+
rawOutput: output,
|
|
382
|
+
sessionId: this._model.name,
|
|
383
|
+
permissionStatus: status === 'awaiting_approval' ? 'pending' : 'resolved',
|
|
384
|
+
...(status === 'awaiting_approval' && {
|
|
385
|
+
permissionOptions: [
|
|
386
|
+
{ optionId: 'approve', name: 'Approve', kind: 'allow_once' },
|
|
387
|
+
{ optionId: 'reject', name: 'Reject', kind: 'reject_once' }
|
|
388
|
+
]
|
|
389
|
+
})
|
|
390
|
+
}
|
|
391
|
+
]
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Whether a mention is required to trigger a response.
|
|
398
|
+
* When false, the persona responds to all non-bot messages.
|
|
399
|
+
* Defaults to true.
|
|
400
|
+
*/
|
|
401
|
+
requireMention = true;
|
|
402
|
+
_model;
|
|
403
|
+
_agent;
|
|
404
|
+
_persona;
|
|
405
|
+
_settingsModel;
|
|
406
|
+
_providerRegistry;
|
|
407
|
+
_documentManager;
|
|
408
|
+
_respondedToIds = new Set();
|
|
409
|
+
_currentModelKey;
|
|
410
|
+
_busy = false;
|
|
411
|
+
_busyChanged = new Signal(this);
|
|
412
|
+
_streamingMessage = new Map();
|
|
413
|
+
_toolContexts = new Map();
|
|
414
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { IAttachment } from '@jupyter/chat';
|
|
2
|
+
import type { IDocumentManager } from '@jupyterlab/docmanager';
|
|
3
|
+
import type { UserContent } from 'ai';
|
|
4
|
+
export declare function processAttachments(attachments: IAttachment[], documentManager: IDocumentManager | null | undefined, body: string, supportsImages: boolean, supportsPdf: boolean, supportsAudio: boolean): Promise<UserContent>;
|
|
5
|
+
//# sourceMappingURL=process-attachments.d.ts.map
|