vibefast-cli 0.7.12 → 0.7.14
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/commands/add.d.ts.map +1 -1
- package/dist/commands/add.js +28 -2
- package/dist/commands/add.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +5 -3
- package/dist/commands/init.js.map +1 -1
- package/package.json +1 -1
- package/recipes/audio-recorder/recipe.json +1 -1
- package/recipes/audio-recorder@latest.zip +0 -0
- package/recipes/charts/apps/native/src/app/{charts → (root)/(protected)/charts}/index.tsx +0 -3
- package/recipes/charts/apps/native/src/features/charts/app/preview.tsx +0 -3
- package/recipes/charts/apps/native/src/features/charts/components/area-chart.tsx +0 -3
- package/recipes/charts/apps/native/src/features/charts/components/bar-chart.tsx +0 -3
- package/recipes/charts/apps/native/src/features/charts/components/candlestick-chart.tsx +0 -3
- package/recipes/charts/apps/native/src/features/charts/components/chart-card.tsx +0 -3
- package/recipes/charts/apps/native/src/features/charts/components/column-chart.tsx +0 -3
- package/recipes/charts/apps/native/src/features/charts/components/doughnut-chart.tsx +0 -3
- package/recipes/charts/apps/native/src/features/charts/components/index.ts +0 -3
- package/recipes/charts/apps/native/src/features/charts/components/line-chart.tsx +0 -3
- package/recipes/charts/apps/native/src/features/charts/components/radar-chart.tsx +0 -3
- package/recipes/charts/apps/native/src/features/charts/components/radial-bar-chart.tsx +0 -3
- package/recipes/charts/apps/native/src/features/charts/components/stacked-area-chart.tsx +0 -3
- package/recipes/charts/apps/native/src/features/charts/components/stacked-bar-chart.tsx +0 -3
- package/recipes/charts/apps/native/src/features/charts/data/mock-data.ts +0 -3
- package/recipes/charts/apps/native/src/features/charts/types/index.ts +0 -3
- package/recipes/charts/recipe.json +1 -1
- package/recipes/charts@latest.zip +0 -0
- package/recipes/chatbot/apps/native/src/api-client/chatbot.ts +83 -0
- package/recipes/chatbot/apps/native/src/app/{chatbot → (root)/(protected)/chatbot}/index.tsx +0 -1
- package/recipes/chatbot/apps/native/src/features/chatbot/app/index.tsx +56 -60
- package/recipes/chatbot/apps/native/src/features/chatbot/components/chat-header-buttons.tsx +0 -1
- package/recipes/chatbot/apps/native/src/features/chatbot/components/chat-input-bar.tsx +0 -1
- package/recipes/chatbot/apps/native/src/features/chatbot/components/chat-markdown.tsx +0 -1
- package/recipes/chatbot/apps/native/src/features/chatbot/components/chat-message-bubble.tsx +3 -26
- package/recipes/chatbot/apps/native/src/features/chatbot/components/chat-settings-modal.tsx +0 -1
- package/recipes/chatbot/apps/native/src/features/chatbot/components/image-preview-list.tsx +0 -1
- package/recipes/chatbot/apps/native/src/features/chatbot/components/markdown/code-block.tsx +0 -1
- package/recipes/chatbot/apps/native/src/features/chatbot/components/markdown/index.ts +0 -1
- package/recipes/chatbot/apps/native/src/features/chatbot/components/markdown/table-renderer.tsx +0 -1
- package/recipes/chatbot/apps/native/src/features/chatbot/components/message-error-boundary.tsx +0 -1
- package/recipes/chatbot/apps/native/src/features/chatbot/components/message-list.tsx +10 -14
- package/recipes/chatbot/apps/native/src/features/chatbot/components/model-selector.tsx +0 -1
- package/recipes/chatbot/apps/native/src/features/chatbot/components/report-content-modal.tsx +0 -1
- package/recipes/chatbot/apps/native/src/features/chatbot/components/suggested-messages.tsx +0 -1
- package/recipes/chatbot/apps/native/src/features/chatbot/constants/models.ts +0 -1
- package/recipes/chatbot/apps/native/src/features/chatbot/constants/report-reasons.ts +0 -1
- package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-attachment-cache.ts +0 -1
- package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-chat-config.ts +0 -1
- package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-chat-handlers.ts +0 -1
- package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-chatbot-settings.ts +0 -1
- package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-conversation.ts +0 -1
- package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-image-picker.ts +0 -1
- package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-keyboard-coordinator.ts +0 -1
- package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-smart-scroll-manager.ts +0 -1
- package/recipes/chatbot/apps/native/src/features/chatbot/models/index.ts +0 -1
- package/recipes/chatbot/apps/native/src/features/chatbot/models/models.ts +0 -1
- package/recipes/chatbot/apps/native/src/features/chatbot/models/providers.ts +0 -1
- package/recipes/chatbot/apps/native/src/features/chatbot/models/types.ts +0 -1
- package/recipes/chatbot/apps/native/src/features/chatbot/services/file-uploader.ts +0 -1
- package/recipes/chatbot/apps/native/src/features/chatbot/services/message-handler-service.ts +0 -1
- package/recipes/chatbot/apps/native/src/features/chatbot/types/index.ts +5 -3
- package/recipes/chatbot/apps/native/src/features/chatbot/utils/chat-telemetry.ts +0 -1
- package/recipes/chatbot/packages/backend/convex/agents.ts +3 -4
- package/recipes/chatbot/packages/backend/convex/chatbot/content.ts +35 -0
- package/recipes/chatbot/packages/backend/convex/chatbot/sessions.ts +52 -0
- package/recipes/chatbot/packages/backend/convex/chatbot/streaming.ts +422 -0
- package/recipes/chatbot/packages/backend/convex/chatbot/telemetry.ts +56 -0
- package/recipes/chatbot/packages/backend/convex/chatbot/tools.ts +128 -0
- package/recipes/chatbot/packages/backend/convex/chatbotAgent.ts +6 -651
- package/recipes/chatbot/packages/backend/convex/ragKnowledge.ts +0 -714
- package/recipes/chatbot/packages/backend/convex/tools/knowledgeRetrieval.ts +12 -7
- package/recipes/chatbot/recipe.json +6 -1
- package/recipes/chatbot@latest.zip +0 -0
- package/recipes/image-generator/apps/native/src/api-client/image-generator.ts +34 -0
- package/recipes/image-generator/packages/backend/convex/{imageGeneratorFunctions.ts → imageGenerator.ts} +1 -1
- package/recipes/image-generator/recipe.json +5 -1
- package/recipes/image-generator@latest.zip +0 -0
- package/recipes/payments/apps/native/src/api-client/payments.ts +44 -0
- package/recipes/payments/packages/backend/convex/payments/index.ts +13 -0
- package/recipes/payments/packages/backend/convex/payments.ts +119 -0
- package/recipes/payments/recipe.json +15 -2
- package/recipes/payments@latest.zip +0 -0
- package/recipes/quiz/recipe.json +1 -1
- package/recipes/quiz@latest.zip +0 -0
- package/recipes/tracker-app/recipe.json +1 -1
- package/recipes/tracker-app@latest.zip +0 -0
- package/recipes/voice-bot/recipe.json +1 -1
- package/recipes/voice-bot@latest.zip +0 -0
- package/src/commands/add.ts +108 -70
- package/src/commands/init.ts +5 -3
- package/tmp-npm-cache/_update-notifier-last-checked +0 -0
- /package/recipes/audio-recorder/apps/native/src/app/{audio-recorder → (root)/(protected)/audio-recorder}/index.tsx +0 -0
- /package/recipes/image-generator/apps/native/src/app/{image-generator → (root)/(protected)/image-generator}/gallery.tsx +0 -0
- /package/recipes/image-generator/apps/native/src/app/{image-generator → (root)/(protected)/image-generator}/index.tsx +0 -0
- /package/recipes/quiz/apps/native/src/app/{quiz → (root)/(protected)/quiz}/index.tsx +0 -0
- /package/recipes/tracker-app/apps/native/src/app/{tracker-app → (root)/(protected)/tracker-app}/index.tsx +0 -0
- /package/recipes/voice-bot/apps/native/src/app/{voice-bot → (root)/(protected)/voice-bot}/index.tsx +0 -0
|
@@ -1,714 +0,0 @@
|
|
|
1
|
-
import { openai } from '@ai-sdk/openai';
|
|
2
|
-
import { getAuthUserId } from '@convex-dev/auth/server';
|
|
3
|
-
import {
|
|
4
|
-
contentHashFromArrayBuffer,
|
|
5
|
-
defaultChunker,
|
|
6
|
-
type Entry,
|
|
7
|
-
type EntryId,
|
|
8
|
-
guessMimeTypeFromContents,
|
|
9
|
-
guessMimeTypeFromExtension,
|
|
10
|
-
RAG,
|
|
11
|
-
type SearchEntry,
|
|
12
|
-
vEntryId,
|
|
13
|
-
} from '@convex-dev/rag';
|
|
14
|
-
import type { PaginationResult, StorageReader } from 'convex/server';
|
|
15
|
-
import { paginationOptsValidator } from 'convex/server';
|
|
16
|
-
import { v } from 'convex/values';
|
|
17
|
-
|
|
18
|
-
import { components, internal } from './_generated/api';
|
|
19
|
-
import type { DataModel, Doc, Id } from './_generated/dataModel';
|
|
20
|
-
import {
|
|
21
|
-
type ActionCtx,
|
|
22
|
-
action,
|
|
23
|
-
internalMutation,
|
|
24
|
-
internalQuery,
|
|
25
|
-
type MutationCtx,
|
|
26
|
-
mutation,
|
|
27
|
-
type QueryCtx,
|
|
28
|
-
query,
|
|
29
|
-
} from './_generated/server';
|
|
30
|
-
|
|
31
|
-
type Filters = {
|
|
32
|
-
filename: string;
|
|
33
|
-
category: string | null;
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
type Metadata = {
|
|
37
|
-
storageId: Id<'_storage'>;
|
|
38
|
-
uploadedBy: Id<'users'>;
|
|
39
|
-
namespace: string;
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
const EMBEDDING_DIMENSION = 1536;
|
|
43
|
-
const DEFAULT_CHUNK_CONTEXT = { before: 1, after: 1 };
|
|
44
|
-
const GLOBAL_NAMESPACE = 'global';
|
|
45
|
-
|
|
46
|
-
const rag = new RAG<Filters, Metadata>(components.rag, {
|
|
47
|
-
filterNames: ['filename', 'category'],
|
|
48
|
-
// @ts-expect-error: Convex RAG expects AI SDK v2 embedding interface.
|
|
49
|
-
textEmbeddingModel: openai.textEmbeddingModel('text-embedding-3-small'),
|
|
50
|
-
embeddingDimension: EMBEDDING_DIMENSION,
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
type RagSearchResult = Awaited<ReturnType<typeof rag.search>>;
|
|
54
|
-
type RagGenerateResult = Awaited<ReturnType<typeof rag.generateText>>;
|
|
55
|
-
type RagGenerationContext = RagGenerateResult['context'];
|
|
56
|
-
|
|
57
|
-
export const addKnowledgeFile = action({
|
|
58
|
-
args: {
|
|
59
|
-
globalNamespace: v.boolean(),
|
|
60
|
-
filename: v.string(),
|
|
61
|
-
bytes: v.bytes(),
|
|
62
|
-
mimeType: v.optional(v.string()),
|
|
63
|
-
category: v.optional(v.string()),
|
|
64
|
-
},
|
|
65
|
-
handler: async (
|
|
66
|
-
ctx,
|
|
67
|
-
args,
|
|
68
|
-
): Promise<{ entryId: EntryId; created: boolean; url: string | null }> => {
|
|
69
|
-
const userId = await getRequiredUserId(ctx);
|
|
70
|
-
const namespace = args.globalNamespace ? GLOBAL_NAMESPACE : userId;
|
|
71
|
-
const contentHash = await contentHashFromArrayBuffer(args.bytes);
|
|
72
|
-
|
|
73
|
-
const existing = await rag.findEntryByContentHash(ctx, {
|
|
74
|
-
contentHash,
|
|
75
|
-
key: args.filename,
|
|
76
|
-
namespace,
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
if (existing) {
|
|
80
|
-
const existingFile: Doc<'knowledgeFiles'> | null =
|
|
81
|
-
await findKnowledgeFileByEntryId(ctx, existing.entryId);
|
|
82
|
-
const existingUrl: string | null = existingFile
|
|
83
|
-
? await ctx.storage.getUrl(existingFile.storageId)
|
|
84
|
-
: null;
|
|
85
|
-
return {
|
|
86
|
-
entryId: existing.entryId,
|
|
87
|
-
created: false,
|
|
88
|
-
url: existingUrl,
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const mimeType =
|
|
93
|
-
args.mimeType ??
|
|
94
|
-
guessMimeType(args.filename, args.bytes) ??
|
|
95
|
-
'application/octet-stream';
|
|
96
|
-
|
|
97
|
-
const blob = new Blob([args.bytes], { type: mimeType });
|
|
98
|
-
const storageId = await ctx.storage.store(blob);
|
|
99
|
-
const text = await extractText(ctx, {
|
|
100
|
-
filename: args.filename,
|
|
101
|
-
bytes: args.bytes,
|
|
102
|
-
mimeType,
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
const { entryId, created } = await rag.add(ctx, {
|
|
106
|
-
namespace,
|
|
107
|
-
text,
|
|
108
|
-
key: args.filename,
|
|
109
|
-
title: args.filename,
|
|
110
|
-
filterValues: [
|
|
111
|
-
{ name: 'filename', value: args.filename },
|
|
112
|
-
{ name: 'category', value: args.category ?? null },
|
|
113
|
-
],
|
|
114
|
-
metadata: { storageId, uploadedBy: userId, namespace },
|
|
115
|
-
contentHash,
|
|
116
|
-
onComplete: internal.ragKnowledge.recordUploadMetadata,
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
if (!created) {
|
|
120
|
-
await ctx.storage.delete(storageId);
|
|
121
|
-
return {
|
|
122
|
-
entryId,
|
|
123
|
-
created: false,
|
|
124
|
-
url: await resolveUrlForEntry(ctx, entryId),
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
return {
|
|
129
|
-
entryId,
|
|
130
|
-
created: true,
|
|
131
|
-
url: await ctx.storage.getUrl(storageId),
|
|
132
|
-
};
|
|
133
|
-
},
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
export const addKnowledgeFileAsync = action({
|
|
137
|
-
args: {
|
|
138
|
-
globalNamespace: v.boolean(),
|
|
139
|
-
filename: v.string(),
|
|
140
|
-
storageId: v.id('_storage'),
|
|
141
|
-
mimeType: v.optional(v.string()),
|
|
142
|
-
category: v.optional(v.string()),
|
|
143
|
-
},
|
|
144
|
-
handler: async (
|
|
145
|
-
ctx,
|
|
146
|
-
args,
|
|
147
|
-
): Promise<{ entryId: EntryId; created: boolean; url: string | null }> => {
|
|
148
|
-
const userId = await getRequiredUserId(ctx);
|
|
149
|
-
const namespace = args.globalNamespace ? GLOBAL_NAMESPACE : userId;
|
|
150
|
-
const metadata = await ctx.storage.getMetadata(args.storageId);
|
|
151
|
-
|
|
152
|
-
const blob = await ctx.storage.get(args.storageId);
|
|
153
|
-
if (!blob) {
|
|
154
|
-
throw new Error('Unable to retrieve stored file');
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
const bytes = await blob.arrayBuffer();
|
|
158
|
-
const contentHash = await contentHashFromArrayBuffer(bytes);
|
|
159
|
-
|
|
160
|
-
const existing = await rag.findEntryByContentHash(ctx, {
|
|
161
|
-
contentHash,
|
|
162
|
-
key: args.filename,
|
|
163
|
-
namespace,
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
if (existing) {
|
|
167
|
-
return {
|
|
168
|
-
entryId: existing.entryId,
|
|
169
|
-
created: false,
|
|
170
|
-
url: await resolveUrlForEntry(ctx, existing.entryId),
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const mimeType =
|
|
175
|
-
args.mimeType ??
|
|
176
|
-
metadata?.contentType ??
|
|
177
|
-
guessMimeType(args.filename, bytes) ??
|
|
178
|
-
'application/octet-stream';
|
|
179
|
-
|
|
180
|
-
const { entryId } = await rag.addAsync(ctx, {
|
|
181
|
-
namespace,
|
|
182
|
-
key: args.filename,
|
|
183
|
-
title: args.filename,
|
|
184
|
-
filterValues: [
|
|
185
|
-
{ name: 'filename', value: args.filename },
|
|
186
|
-
{ name: 'category', value: args.category ?? null },
|
|
187
|
-
],
|
|
188
|
-
metadata: { storageId: args.storageId, uploadedBy: userId, namespace },
|
|
189
|
-
chunkerAction: internal.ragKnowledge.chunkerAction,
|
|
190
|
-
contentHash,
|
|
191
|
-
onComplete: internal.ragKnowledge.recordUploadMetadata,
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
return {
|
|
195
|
-
entryId,
|
|
196
|
-
created: true,
|
|
197
|
-
url: await ctx.storage.getUrl(args.storageId),
|
|
198
|
-
};
|
|
199
|
-
},
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
export const searchKnowledge = action({
|
|
203
|
-
args: {
|
|
204
|
-
query: v.string(),
|
|
205
|
-
globalNamespace: v.boolean(),
|
|
206
|
-
limit: v.optional(v.number()),
|
|
207
|
-
chunkContext: v.optional(
|
|
208
|
-
v.object({ before: v.number(), after: v.number() }),
|
|
209
|
-
),
|
|
210
|
-
filter: v.optional(
|
|
211
|
-
v.union(
|
|
212
|
-
v.object({
|
|
213
|
-
name: v.literal('category'),
|
|
214
|
-
value: v.union(v.null(), v.string()),
|
|
215
|
-
}),
|
|
216
|
-
v.object({
|
|
217
|
-
name: v.literal('filename'),
|
|
218
|
-
value: v.string(),
|
|
219
|
-
}),
|
|
220
|
-
),
|
|
221
|
-
),
|
|
222
|
-
},
|
|
223
|
-
handler: async (
|
|
224
|
-
ctx,
|
|
225
|
-
args,
|
|
226
|
-
): Promise<RagSearchResult & { files: PublicKnowledgeFile[] }> => {
|
|
227
|
-
const userId = await getRequiredUserId(ctx);
|
|
228
|
-
const namespace = args.globalNamespace ? GLOBAL_NAMESPACE : userId;
|
|
229
|
-
const results = await rag.search(ctx, {
|
|
230
|
-
namespace,
|
|
231
|
-
query: args.query,
|
|
232
|
-
limit: args.limit ?? 10,
|
|
233
|
-
filters: args.filter ? [args.filter] : undefined,
|
|
234
|
-
chunkContext: args.chunkContext ?? DEFAULT_CHUNK_CONTEXT,
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
return {
|
|
238
|
-
...results,
|
|
239
|
-
files: await entriesToPublicFiles(ctx, results.entries),
|
|
240
|
-
};
|
|
241
|
-
},
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
export const askKnowledge = action({
|
|
245
|
-
args: {
|
|
246
|
-
prompt: v.string(),
|
|
247
|
-
globalNamespace: v.boolean(),
|
|
248
|
-
limit: v.optional(v.number()),
|
|
249
|
-
chunkContext: v.optional(
|
|
250
|
-
v.object({ before: v.number(), after: v.number() }),
|
|
251
|
-
),
|
|
252
|
-
filter: v.optional(
|
|
253
|
-
v.union(
|
|
254
|
-
v.object({
|
|
255
|
-
name: v.literal('category'),
|
|
256
|
-
value: v.union(v.null(), v.string()),
|
|
257
|
-
}),
|
|
258
|
-
v.object({
|
|
259
|
-
name: v.literal('filename'),
|
|
260
|
-
value: v.string(),
|
|
261
|
-
}),
|
|
262
|
-
),
|
|
263
|
-
),
|
|
264
|
-
},
|
|
265
|
-
handler: async (
|
|
266
|
-
ctx,
|
|
267
|
-
args,
|
|
268
|
-
): Promise<
|
|
269
|
-
{ answer: string; files: PublicKnowledgeFile[] } & RagGenerationContext
|
|
270
|
-
> => {
|
|
271
|
-
const userId = await getRequiredUserId(ctx);
|
|
272
|
-
const namespace = args.globalNamespace ? GLOBAL_NAMESPACE : userId;
|
|
273
|
-
|
|
274
|
-
const { text, context } = await rag.generateText(ctx, {
|
|
275
|
-
search: {
|
|
276
|
-
namespace,
|
|
277
|
-
limit: args.limit ?? 10,
|
|
278
|
-
filters: args.filter ? [args.filter] : undefined,
|
|
279
|
-
chunkContext: args.chunkContext ?? DEFAULT_CHUNK_CONTEXT,
|
|
280
|
-
},
|
|
281
|
-
prompt: args.prompt,
|
|
282
|
-
// @ts-expect-error: Convex RAG expects AI SDK v2 language model interface.
|
|
283
|
-
model: openai.chat('gpt-4o-mini'),
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
return {
|
|
287
|
-
answer: text,
|
|
288
|
-
...context,
|
|
289
|
-
files: await entriesToPublicFiles(ctx, context.entries),
|
|
290
|
-
};
|
|
291
|
-
},
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
export const listKnowledgeFiles = query({
|
|
295
|
-
args: {
|
|
296
|
-
globalNamespace: v.boolean(),
|
|
297
|
-
category: v.optional(v.string()),
|
|
298
|
-
paginationOpts: paginationOptsValidator,
|
|
299
|
-
},
|
|
300
|
-
handler: async (
|
|
301
|
-
ctx,
|
|
302
|
-
args,
|
|
303
|
-
): Promise<PaginationResult<PublicKnowledgeFile>> => {
|
|
304
|
-
const userId = await getRequiredUserId(ctx);
|
|
305
|
-
const namespace = args.globalNamespace ? GLOBAL_NAMESPACE : userId;
|
|
306
|
-
const namespaceInfo = await rag.getNamespace(ctx, { namespace });
|
|
307
|
-
|
|
308
|
-
if (!namespaceInfo) {
|
|
309
|
-
return { isDone: true, page: [], continueCursor: '' };
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
const results = await rag.list(ctx, {
|
|
313
|
-
namespaceId: namespaceInfo.namespaceId,
|
|
314
|
-
paginationOpts: args.paginationOpts,
|
|
315
|
-
});
|
|
316
|
-
|
|
317
|
-
const entries = args.category
|
|
318
|
-
? results.page.filter((entry) =>
|
|
319
|
-
entry.filterValues.some(
|
|
320
|
-
(filter) =>
|
|
321
|
-
filter.name === 'category' && filter.value === args.category,
|
|
322
|
-
),
|
|
323
|
-
)
|
|
324
|
-
: results.page;
|
|
325
|
-
|
|
326
|
-
const pageFiles = (
|
|
327
|
-
await Promise.all(entries.map((entry) => entryToPublicFile(ctx, entry)))
|
|
328
|
-
).filter((file): file is PublicKnowledgeFile => file !== null);
|
|
329
|
-
|
|
330
|
-
return {
|
|
331
|
-
...results,
|
|
332
|
-
page: pageFiles,
|
|
333
|
-
};
|
|
334
|
-
},
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
export const listPendingKnowledgeFiles = query({
|
|
338
|
-
args: {},
|
|
339
|
-
handler: async (ctx) => {
|
|
340
|
-
const userId = await getRequiredUserId(ctx);
|
|
341
|
-
const namespace = await rag.getNamespace(ctx, {
|
|
342
|
-
namespace: userId,
|
|
343
|
-
});
|
|
344
|
-
const globalNamespaceInfo = await rag.getNamespace(ctx, {
|
|
345
|
-
namespace: GLOBAL_NAMESPACE,
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
const paginationOpts = { numItems: 20, cursor: null };
|
|
349
|
-
|
|
350
|
-
const [userResults, globalResults] = await Promise.all([
|
|
351
|
-
namespace
|
|
352
|
-
? rag.list(ctx, {
|
|
353
|
-
namespaceId: namespace.namespaceId,
|
|
354
|
-
status: 'pending',
|
|
355
|
-
paginationOpts,
|
|
356
|
-
})
|
|
357
|
-
: null,
|
|
358
|
-
globalNamespaceInfo
|
|
359
|
-
? rag.list(ctx, {
|
|
360
|
-
namespaceId: globalNamespaceInfo.namespaceId,
|
|
361
|
-
status: 'pending',
|
|
362
|
-
paginationOpts,
|
|
363
|
-
})
|
|
364
|
-
: null,
|
|
365
|
-
]);
|
|
366
|
-
|
|
367
|
-
const pendingEntries = [
|
|
368
|
-
...(userResults?.page ?? []),
|
|
369
|
-
...(globalResults?.page ?? []),
|
|
370
|
-
];
|
|
371
|
-
|
|
372
|
-
return (
|
|
373
|
-
await Promise.all(
|
|
374
|
-
pendingEntries.map((entry) => entryToPublicFile(ctx, entry)),
|
|
375
|
-
)
|
|
376
|
-
).filter((file): file is PublicKnowledgeFile => file !== null);
|
|
377
|
-
},
|
|
378
|
-
});
|
|
379
|
-
|
|
380
|
-
export const listKnowledgeChunks = query({
|
|
381
|
-
args: {
|
|
382
|
-
entryId: vEntryId,
|
|
383
|
-
paginationOpts: paginationOptsValidator,
|
|
384
|
-
order: v.union(v.literal('desc'), v.literal('asc')),
|
|
385
|
-
},
|
|
386
|
-
handler: async (ctx, args) => {
|
|
387
|
-
await getRequiredUserId(ctx);
|
|
388
|
-
return await rag.listChunks(ctx, {
|
|
389
|
-
entryId: args.entryId,
|
|
390
|
-
paginationOpts: args.paginationOpts,
|
|
391
|
-
order: args.order,
|
|
392
|
-
});
|
|
393
|
-
},
|
|
394
|
-
});
|
|
395
|
-
|
|
396
|
-
export const deleteKnowledgeFile = mutation({
|
|
397
|
-
args: { entryId: vEntryId },
|
|
398
|
-
handler: async (ctx, args) => {
|
|
399
|
-
const userId = await getRequiredUserId(ctx);
|
|
400
|
-
const fileDoc = await ctx.db
|
|
401
|
-
.query('knowledgeFiles')
|
|
402
|
-
.withIndex('by_entryId', (q) => q.eq('entryId', args.entryId))
|
|
403
|
-
.first();
|
|
404
|
-
|
|
405
|
-
if (!fileDoc) {
|
|
406
|
-
return;
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
if (!fileDoc.global && fileDoc.uploadedBy !== userId) {
|
|
410
|
-
throw new Error('Not authorized to delete this file');
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
await deleteFileRecord(ctx, args.entryId);
|
|
414
|
-
},
|
|
415
|
-
});
|
|
416
|
-
|
|
417
|
-
export const chunkerAction = rag.defineChunkerAction(async (ctx, { entry }) => {
|
|
418
|
-
if (!entry.metadata?.storageId) {
|
|
419
|
-
throw new Error('Missing storage metadata for chunking');
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
const fileBlob = await ctx.storage.get(entry.metadata.storageId);
|
|
423
|
-
if (!fileBlob) {
|
|
424
|
-
throw new Error('Stored file not found for chunking');
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
const bytes = await fileBlob.arrayBuffer();
|
|
428
|
-
const mimeType =
|
|
429
|
-
fileBlob.type ??
|
|
430
|
-
(await ctx.storage.getMetadata(entry.metadata.storageId))?.contentType ??
|
|
431
|
-
guessMimeType(entry.key ?? 'uploaded', bytes) ??
|
|
432
|
-
'application/octet-stream';
|
|
433
|
-
|
|
434
|
-
const text = await extractText(ctx, {
|
|
435
|
-
filename: entry.key ?? 'uploaded',
|
|
436
|
-
bytes,
|
|
437
|
-
mimeType,
|
|
438
|
-
});
|
|
439
|
-
|
|
440
|
-
return { chunks: defaultChunker(text) };
|
|
441
|
-
});
|
|
442
|
-
|
|
443
|
-
export const recordUploadMetadata = rag.defineOnComplete<DataModel>(
|
|
444
|
-
async (ctx, args) => {
|
|
445
|
-
const { entry, replacedEntry, namespace, error } = args;
|
|
446
|
-
|
|
447
|
-
if (replacedEntry) {
|
|
448
|
-
await deleteFileRecord(ctx, replacedEntry.entryId);
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
if (!entry.metadata?.storageId || !entry.metadata?.uploadedBy) {
|
|
452
|
-
return;
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
if (error) {
|
|
456
|
-
const errorMessage =
|
|
457
|
-
typeof error === 'string'
|
|
458
|
-
? error
|
|
459
|
-
: ((error as { message?: string })?.message ?? 'Unknown error');
|
|
460
|
-
await ctx.db.insert('knowledgeFiles', {
|
|
461
|
-
entryId: entry.entryId,
|
|
462
|
-
namespace: namespace.namespace,
|
|
463
|
-
filename: entry.key ?? 'unknown',
|
|
464
|
-
storageId: entry.metadata.storageId,
|
|
465
|
-
uploadedBy: entry.metadata.uploadedBy,
|
|
466
|
-
global: namespace.namespace === GLOBAL_NAMESPACE,
|
|
467
|
-
category:
|
|
468
|
-
entry.filterValues.find((f) => f.name === 'category')?.value ??
|
|
469
|
-
undefined,
|
|
470
|
-
title: entry.title ?? undefined,
|
|
471
|
-
status: 'error',
|
|
472
|
-
lastError: errorMessage,
|
|
473
|
-
createdAt: Date.now(),
|
|
474
|
-
updatedAt: Date.now(),
|
|
475
|
-
});
|
|
476
|
-
return;
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
if (entry.status !== 'ready') {
|
|
480
|
-
await upsertKnowledgeFile(ctx, {
|
|
481
|
-
entry,
|
|
482
|
-
namespace: namespace.namespace,
|
|
483
|
-
status: 'pending',
|
|
484
|
-
});
|
|
485
|
-
return;
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
await upsertKnowledgeFile(ctx, {
|
|
489
|
-
entry,
|
|
490
|
-
namespace: namespace.namespace,
|
|
491
|
-
status: 'ready',
|
|
492
|
-
});
|
|
493
|
-
},
|
|
494
|
-
);
|
|
495
|
-
|
|
496
|
-
export const deleteReplacedKnowledge = internalMutation({
|
|
497
|
-
args: { cursor: v.optional(v.string()) },
|
|
498
|
-
handler: async (ctx, args) => {
|
|
499
|
-
const entries = await rag.list(ctx, {
|
|
500
|
-
status: 'replaced',
|
|
501
|
-
paginationOpts: { cursor: args.cursor ?? null, numItems: 100 },
|
|
502
|
-
});
|
|
503
|
-
|
|
504
|
-
const cutoff = Date.now() - 1000 * 60 * 60 * 24 * 7;
|
|
505
|
-
|
|
506
|
-
for (const entry of entries.page) {
|
|
507
|
-
const replacedAt = (
|
|
508
|
-
entry as EntryWithMetadata & {
|
|
509
|
-
replacedAt?: number;
|
|
510
|
-
}
|
|
511
|
-
).replacedAt;
|
|
512
|
-
if (typeof replacedAt === 'number' && replacedAt >= cutoff) {
|
|
513
|
-
continue;
|
|
514
|
-
}
|
|
515
|
-
await deleteFileRecord(ctx, entry.entryId);
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
if (!entries.isDone) {
|
|
519
|
-
await ctx.scheduler.runAfter(
|
|
520
|
-
0,
|
|
521
|
-
internal.ragKnowledge.deleteReplacedKnowledge,
|
|
522
|
-
{
|
|
523
|
-
cursor: entries.continueCursor,
|
|
524
|
-
},
|
|
525
|
-
);
|
|
526
|
-
}
|
|
527
|
-
},
|
|
528
|
-
});
|
|
529
|
-
|
|
530
|
-
export const getKnowledgeFileMetadata = internalQuery({
|
|
531
|
-
args: { entryId: v.string() },
|
|
532
|
-
handler: async (ctx, args) => {
|
|
533
|
-
return await ctx.db
|
|
534
|
-
.query('knowledgeFiles')
|
|
535
|
-
.withIndex('by_entryId', (q) => q.eq('entryId', args.entryId))
|
|
536
|
-
.unique();
|
|
537
|
-
},
|
|
538
|
-
});
|
|
539
|
-
|
|
540
|
-
type EntryWithMetadata = Entry<Filters, Metadata>;
|
|
541
|
-
|
|
542
|
-
export type PublicKnowledgeFile = {
|
|
543
|
-
entryId: EntryId;
|
|
544
|
-
filename: string;
|
|
545
|
-
storageId: Id<'_storage'>;
|
|
546
|
-
global: boolean;
|
|
547
|
-
category?: string;
|
|
548
|
-
title?: string;
|
|
549
|
-
status: EntryWithMetadata['status'];
|
|
550
|
-
url: string | null;
|
|
551
|
-
};
|
|
552
|
-
|
|
553
|
-
async function entriesToPublicFiles(
|
|
554
|
-
ctx: ActionCtx | QueryCtx,
|
|
555
|
-
entries: SearchEntry<Filters, Metadata>[],
|
|
556
|
-
): Promise<PublicKnowledgeFile[]> {
|
|
557
|
-
const files = await Promise.all(
|
|
558
|
-
entries.map((entry) => entryToPublicFile(ctx, entry)),
|
|
559
|
-
);
|
|
560
|
-
return files.filter((file): file is PublicKnowledgeFile => file !== null);
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
async function entryToPublicFile(
|
|
564
|
-
ctx: { storage: StorageReader },
|
|
565
|
-
entry: EntryWithMetadata,
|
|
566
|
-
): Promise<PublicKnowledgeFile | null> {
|
|
567
|
-
if (!entry.metadata?.storageId) {
|
|
568
|
-
return null;
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
const category =
|
|
572
|
-
entry.filterValues.find((filter) => filter.name === 'category')?.value ??
|
|
573
|
-
undefined;
|
|
574
|
-
|
|
575
|
-
return {
|
|
576
|
-
entryId: entry.entryId,
|
|
577
|
-
filename: entry.key ?? 'unknown',
|
|
578
|
-
storageId: entry.metadata.storageId,
|
|
579
|
-
global: entry.metadata.namespace === GLOBAL_NAMESPACE,
|
|
580
|
-
category,
|
|
581
|
-
title: entry.title ?? undefined,
|
|
582
|
-
status: entry.status,
|
|
583
|
-
url: await ctx.storage.getUrl(entry.metadata.storageId),
|
|
584
|
-
};
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
async function getRequiredUserId(ctx: QueryCtx | MutationCtx | ActionCtx) {
|
|
588
|
-
const userId = await getAuthUserId(ctx);
|
|
589
|
-
if (!userId) {
|
|
590
|
-
throw new Error('Unauthorized');
|
|
591
|
-
}
|
|
592
|
-
return userId;
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
async function upsertKnowledgeFile(
|
|
596
|
-
ctx: MutationCtx,
|
|
597
|
-
params: {
|
|
598
|
-
entry: EntryWithMetadata;
|
|
599
|
-
namespace: string;
|
|
600
|
-
status: 'pending' | 'ready';
|
|
601
|
-
},
|
|
602
|
-
) {
|
|
603
|
-
const doc = await ctx.db
|
|
604
|
-
.query('knowledgeFiles')
|
|
605
|
-
.withIndex('by_entryId', (q) => q.eq('entryId', params.entry.entryId))
|
|
606
|
-
.unique();
|
|
607
|
-
|
|
608
|
-
const storageId = params.entry.metadata?.storageId;
|
|
609
|
-
const uploadedBy = params.entry.metadata?.uploadedBy;
|
|
610
|
-
|
|
611
|
-
if (!storageId || !uploadedBy) {
|
|
612
|
-
return;
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
const payload: Omit<Doc<'knowledgeFiles'>, '_id' | '_creationTime'> = {
|
|
616
|
-
entryId: params.entry.entryId,
|
|
617
|
-
namespace: params.namespace,
|
|
618
|
-
filename: params.entry.key ?? 'unknown',
|
|
619
|
-
storageId: storageId as Id<'_storage'>,
|
|
620
|
-
uploadedBy: uploadedBy as Id<'users'>,
|
|
621
|
-
global: params.namespace === GLOBAL_NAMESPACE,
|
|
622
|
-
category:
|
|
623
|
-
params.entry.filterValues.find((filter) => filter.name === 'category')
|
|
624
|
-
?.value ?? undefined,
|
|
625
|
-
title: params.entry.title ?? undefined,
|
|
626
|
-
status: params.status,
|
|
627
|
-
lastError: undefined,
|
|
628
|
-
createdAt: doc?.createdAt ?? Date.now(),
|
|
629
|
-
updatedAt: Date.now(),
|
|
630
|
-
};
|
|
631
|
-
|
|
632
|
-
if (doc) {
|
|
633
|
-
await ctx.db.replace(doc._id, payload);
|
|
634
|
-
} else {
|
|
635
|
-
await ctx.db.insert('knowledgeFiles', payload);
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
async function deleteFileRecord(ctx: MutationCtx, entryId: EntryId) {
|
|
640
|
-
const doc = await ctx.db
|
|
641
|
-
.query('knowledgeFiles')
|
|
642
|
-
.withIndex('by_entryId', (q) => q.eq('entryId', entryId))
|
|
643
|
-
.unique();
|
|
644
|
-
|
|
645
|
-
if (doc) {
|
|
646
|
-
await ctx.db.delete(doc._id);
|
|
647
|
-
await ctx.storage.delete(doc.storageId);
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
await rag.delete(ctx, { entryId });
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
async function extractText(
|
|
654
|
-
ctx: Pick<ActionCtx, 'storage'>,
|
|
655
|
-
{
|
|
656
|
-
filename,
|
|
657
|
-
bytes,
|
|
658
|
-
storageId,
|
|
659
|
-
mimeType,
|
|
660
|
-
}: {
|
|
661
|
-
filename: string;
|
|
662
|
-
bytes?: ArrayBuffer;
|
|
663
|
-
storageId?: Id<'_storage'>;
|
|
664
|
-
mimeType?: string;
|
|
665
|
-
},
|
|
666
|
-
) {
|
|
667
|
-
let buffer = bytes;
|
|
668
|
-
|
|
669
|
-
if (!buffer && storageId) {
|
|
670
|
-
const blob = await ctx.storage.get(storageId);
|
|
671
|
-
if (blob) {
|
|
672
|
-
buffer = await blob.arrayBuffer();
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
if (!buffer) {
|
|
677
|
-
throw new Error('Unable to retrieve file contents for ingestion');
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
const detectedMime =
|
|
681
|
-
mimeType ?? guessMimeType(filename, buffer) ?? 'application/octet-stream';
|
|
682
|
-
|
|
683
|
-
if (
|
|
684
|
-
detectedMime.startsWith('text/') ||
|
|
685
|
-
detectedMime === 'application/json' ||
|
|
686
|
-
detectedMime === 'application/xml'
|
|
687
|
-
) {
|
|
688
|
-
return new TextDecoder('utf-8').decode(buffer);
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
throw new Error(
|
|
692
|
-
`Unsupported file type for ingestion: ${detectedMime} (${filename})`,
|
|
693
|
-
);
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
async function resolveUrlForEntry(ctx: ActionCtx, entryId: EntryId) {
|
|
697
|
-
const fileDoc = await findKnowledgeFileByEntryId(ctx, entryId);
|
|
698
|
-
return fileDoc ? await ctx.storage.getUrl(fileDoc.storageId) : null;
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
async function findKnowledgeFileByEntryId(
|
|
702
|
-
ctx: ActionCtx,
|
|
703
|
-
entryId: EntryId,
|
|
704
|
-
): Promise<Doc<'knowledgeFiles'> | null> {
|
|
705
|
-
return await ctx.runQuery(internal.ragKnowledge.getKnowledgeFileMetadata, {
|
|
706
|
-
entryId,
|
|
707
|
-
});
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
function guessMimeType(filename: string, bytes: ArrayBuffer) {
|
|
711
|
-
return (
|
|
712
|
-
guessMimeTypeFromExtension(filename) || guessMimeTypeFromContents(bytes)
|
|
713
|
-
);
|
|
714
|
-
}
|