@convex-dev/rag 0.7.1 → 0.7.2

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.
@@ -1,11 +1,18 @@
1
+ import {
2
+ vResultValidator,
3
+ vWorkIdValidator,
4
+ Workpool,
5
+ } from "@convex-dev/workpool";
1
6
  import { assert, omit } from "convex-helpers";
7
+ import { mergedStream, stream } from "convex-helpers/server/stream";
8
+ import { doc } from "convex-helpers/validators";
2
9
  import {
3
10
  createFunctionHandle,
4
11
  paginationOptsValidator,
5
12
  PaginationResult,
6
13
  } from "convex/server";
7
- import { v, type Value } from "convex/values";
8
- import type { ChunkerAction, EntryFilter, EntryId } from "../shared.js";
14
+ import { v } from "convex/values";
15
+ import type { ChunkerAction, OnComplete } from "../shared.js";
9
16
  import {
10
17
  statuses,
11
18
  vActiveStatus,
@@ -15,7 +22,7 @@ import {
15
22
  vStatus,
16
23
  type Entry,
17
24
  } from "../shared.js";
18
- import { api, internal } from "./_generated/api.js";
25
+ import { api, components, internal } from "./_generated/api.js";
19
26
  import type { Doc, Id } from "./_generated/dataModel.js";
20
27
  import {
21
28
  action,
@@ -23,26 +30,19 @@ import {
23
30
  internalQuery,
24
31
  mutation,
25
32
  query,
33
+ type ActionCtx,
26
34
  type MutationCtx,
27
35
  type QueryCtx,
28
36
  } from "./_generated/server.js";
29
37
  import { deleteChunksPageHandler, insertChunks } from "./chunks.js";
30
- import schema, { type StatusWithOnComplete } from "./schema.js";
31
- import { mergedStream } from "convex-helpers/server/stream";
32
- import { stream } from "convex-helpers/server/stream";
33
38
  import {
34
39
  getCompatibleNamespaceHandler,
40
+ getPreviousEntry,
41
+ publicEntry,
35
42
  publicNamespace,
36
43
  vNamespaceLookupArgs,
37
- } from "./namespaces.js";
38
- import type { OnComplete } from "../shared.js";
39
- import {
40
- vResultValidator,
41
- vWorkIdValidator,
42
- Workpool,
43
- } from "@convex-dev/workpool";
44
- import { components } from "./_generated/api.js";
45
- import { doc } from "convex-helpers/validators";
44
+ } from "./helpers.js";
45
+ import schema, { type StatusWithOnComplete } from "./schema.js";
46
46
 
47
47
  const workpool = new Workpool(components.workpool, {
48
48
  retryActionsByDefault: true,
@@ -479,58 +479,6 @@ async function promoteToReadyHandler(
479
479
  };
480
480
  }
481
481
 
482
- export async function getPreviousEntry(ctx: QueryCtx, entry: Doc<"entries">) {
483
- if (!entry.key) {
484
- return null;
485
- }
486
- const previousEntry = await ctx.db
487
- .query("entries")
488
- .withIndex("namespaceId_status_key_version", (q) =>
489
- q
490
- .eq("namespaceId", entry.namespaceId)
491
- .eq("status.kind", "ready")
492
- .eq("key", entry.key),
493
- )
494
- .unique();
495
- if (previousEntry?._id === entry._id) return null;
496
- return previousEntry;
497
- }
498
-
499
- export function publicEntry(entry: {
500
- _id: Id<"entries">;
501
- key?: string | undefined;
502
- importance: number;
503
- filterValues: EntryFilter[];
504
- contentHash?: string | undefined;
505
- title?: string | undefined;
506
- metadata?: Record<string, Value> | undefined;
507
- status: StatusWithOnComplete;
508
- }): Entry {
509
- const { key, importance, filterValues, contentHash, title, metadata } = entry;
510
-
511
- const fields = {
512
- entryId: entry._id as unknown as EntryId,
513
- key,
514
- title,
515
- metadata,
516
- importance,
517
- filterValues,
518
- contentHash,
519
- };
520
- if (entry.status.kind === "replaced") {
521
- return {
522
- ...fields,
523
- status: "replaced" as const,
524
- replacedAt: entry.status.replacedAt,
525
- };
526
- } else {
527
- return {
528
- ...fields,
529
- status: entry.status.kind,
530
- };
531
- }
532
- }
533
-
534
482
  export const deleteAsync = mutation({
535
483
  args: v.object({
536
484
  entryId: v.id("entries"),
@@ -563,22 +511,24 @@ async function deleteAsyncHandler(
563
511
  export const deleteSync = action({
564
512
  args: { entryId: v.id("entries") },
565
513
  returns: v.null(),
566
- handler: async (ctx, { entryId }) => {
567
- let startOrder = 0;
568
- while (true) {
569
- const status = await ctx.runMutation(internal.chunks.deleteChunksPage, {
570
- entryId,
571
- startOrder,
572
- });
573
- if (status.isDone) {
574
- await ctx.runMutation(internal.entries._del, { entryId });
575
- break;
576
- }
577
- startOrder = status.nextStartOrder;
578
- }
579
- },
514
+ handler: async (ctx, { entryId }) => deleteEntrySync(ctx, entryId),
580
515
  });
581
516
 
517
+ export async function deleteEntrySync(ctx: ActionCtx, entryId: Id<"entries">) {
518
+ let startOrder = 0;
519
+ while (true) {
520
+ const status = await ctx.runMutation(internal.chunks.deleteChunksPage, {
521
+ entryId,
522
+ startOrder,
523
+ });
524
+ if (status.isDone) {
525
+ await ctx.runMutation(internal.entries._del, { entryId });
526
+ break;
527
+ }
528
+ startOrder = status.nextStartOrder;
529
+ }
530
+ }
531
+
582
532
  export const _del = internalMutation({
583
533
  args: { entryId: v.id("entries") },
584
534
  returns: v.null(),
@@ -656,9 +606,7 @@ export const deleteByKeySync = action({
656
606
  { namespaceId: args.namespaceId, key: args.key },
657
607
  );
658
608
  for await (const entry of entries) {
659
- await ctx.runAction(api.entries.deleteSync, {
660
- entryId: entry._id,
661
- });
609
+ await deleteEntrySync(ctx, entry._id);
662
610
  }
663
611
  if (entries.length <= 100) {
664
612
  break;
@@ -0,0 +1,124 @@
1
+ import type { ObjectType, Value } from "convex/values";
2
+ import {
3
+ Entry,
4
+ EntryFilter,
5
+ EntryId,
6
+ filterNamesContain,
7
+ type Namespace,
8
+ type NamespaceId,
9
+ } from "../shared.js";
10
+ import type { Doc, Id } from "./_generated/dataModel.js";
11
+ import { type QueryCtx } from "./_generated/server.js";
12
+ import { StatusWithOnComplete, v } from "./schema.js";
13
+
14
+ export function publicNamespace(namespace: Doc<"namespaces">): Namespace {
15
+ const { _id, _creationTime, status, ...rest } = namespace;
16
+ return {
17
+ namespaceId: _id as unknown as NamespaceId,
18
+ createdAt: _creationTime,
19
+ ...rest,
20
+ status: status.kind,
21
+ };
22
+ }
23
+
24
+ export function publicEntry(entry: {
25
+ _id: Id<"entries">;
26
+ key?: string | undefined;
27
+ importance: number;
28
+ filterValues: EntryFilter[];
29
+ contentHash?: string | undefined;
30
+ title?: string | undefined;
31
+ metadata?: Record<string, Value> | undefined;
32
+ status: StatusWithOnComplete;
33
+ }): Entry {
34
+ const { key, importance, filterValues, contentHash, title, metadata } = entry;
35
+
36
+ const fields = {
37
+ entryId: entry._id as unknown as EntryId,
38
+ key,
39
+ title,
40
+ metadata,
41
+ importance,
42
+ filterValues,
43
+ contentHash,
44
+ };
45
+ if (entry.status.kind === "replaced") {
46
+ return {
47
+ ...fields,
48
+ status: "replaced" as const,
49
+ replacedAt: entry.status.replacedAt,
50
+ };
51
+ } else {
52
+ return {
53
+ ...fields,
54
+ status: entry.status.kind,
55
+ };
56
+ }
57
+ }
58
+
59
+ export const vNamespaceLookupArgs = {
60
+ namespace: v.string(),
61
+ modelId: v.string(),
62
+ dimension: v.number(),
63
+ filterNames: v.array(v.string()),
64
+ };
65
+
66
+ export async function getCompatibleNamespaceHandler(
67
+ ctx: QueryCtx,
68
+ args: ObjectType<typeof vNamespaceLookupArgs>,
69
+ ) {
70
+ const iter = ctx.db
71
+ .query("namespaces")
72
+ .withIndex("status_namespace_version", (q) =>
73
+ q.eq("status.kind", "ready").eq("namespace", args.namespace),
74
+ )
75
+ .order("desc");
76
+ for await (const existing of iter) {
77
+ if (namespaceIsCompatible(existing, args)) {
78
+ return existing;
79
+ }
80
+ }
81
+ return null;
82
+ }
83
+
84
+ export function namespaceIsCompatible(
85
+ existing: Doc<"namespaces">,
86
+ args: {
87
+ modelId: string;
88
+ dimension: number;
89
+ filterNames: string[];
90
+ },
91
+ ) {
92
+ // Check basic compatibility
93
+ if (
94
+ existing.modelId !== args.modelId ||
95
+ existing.dimension !== args.dimension
96
+ ) {
97
+ return false;
98
+ }
99
+
100
+ // For filter names, the namespace must support all requested filters
101
+ // but can support additional filters (superset is OK)
102
+ if (!filterNamesContain(existing.filterNames, args.filterNames)) {
103
+ return false;
104
+ }
105
+
106
+ return true;
107
+ }
108
+
109
+ export async function getPreviousEntry(ctx: QueryCtx, entry: Doc<"entries">) {
110
+ if (!entry.key) {
111
+ return null;
112
+ }
113
+ const previousEntry = await ctx.db
114
+ .query("entries")
115
+ .withIndex("namespaceId_status_key_version", (q) =>
116
+ q
117
+ .eq("namespaceId", entry.namespaceId)
118
+ .eq("status.kind", "ready")
119
+ .eq("key", entry.key),
120
+ )
121
+ .unique();
122
+ if (previousEntry?._id === entry._id) return null;
123
+ return previousEntry;
124
+ }
@@ -0,0 +1,97 @@
1
+ /// <reference types="vite/client" />
2
+
3
+ import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
4
+ import { type TestConvex } from "convex-test";
5
+ import schema from "./schema.js";
6
+ import { api } from "./_generated/api.js";
7
+ import { initConvexTest } from "./setup.test.js";
8
+ import type { Id } from "./_generated/dataModel.js";
9
+
10
+ type ConvexTest = TestConvex<typeof schema>;
11
+
12
+ describe("namespaces", () => {
13
+ async function setupTestNamespace(t: ConvexTest) {
14
+ const namespace = await t.mutation(api.namespaces.getOrCreate, {
15
+ namespace: "test-namespace",
16
+ status: "ready",
17
+ modelId: "test-model",
18
+ dimension: 128,
19
+ filterNames: [],
20
+ });
21
+ return namespace.namespaceId;
22
+ }
23
+
24
+ function testEntryArgs(namespaceId: Id<"namespaces">, key: string) {
25
+ return {
26
+ namespaceId,
27
+ key,
28
+ importance: 0.5,
29
+ filterValues: [],
30
+ contentHash: `hash-${key}`,
31
+ title: `Entry ${key}`,
32
+ };
33
+ }
34
+
35
+ function testChunk() {
36
+ return {
37
+ content: { text: "test chunk" },
38
+ embedding: Array.from({ length: 128 }, () => Math.random()),
39
+ searchableText: "test chunk",
40
+ };
41
+ }
42
+
43
+ beforeEach(async () => {
44
+ vi.useFakeTimers();
45
+ });
46
+ afterEach(() => {
47
+ vi.useRealTimers();
48
+ });
49
+
50
+ test("deleteNamespaceSync deletes all entries then the namespace", async () => {
51
+ const t = initConvexTest();
52
+ const namespaceId = await setupTestNamespace(t);
53
+
54
+ // Create multiple entries so the pagination loop must handle them all
55
+ const entryIds: Id<"entries">[] = [];
56
+ for (let i = 0; i < 3; i++) {
57
+ const result = await t.mutation(api.entries.add, {
58
+ entry: testEntryArgs(namespaceId, `key-${i}`),
59
+ allChunks: [testChunk()],
60
+ });
61
+ expect(result.status).toBe("ready");
62
+ entryIds.push(result.entryId);
63
+ }
64
+
65
+ // Verify entries exist
66
+ const entriesBefore = await t.run(async (ctx) => {
67
+ return ctx.db
68
+ .query("entries")
69
+ .filter((q) => q.eq(q.field("namespaceId"), namespaceId))
70
+ .collect();
71
+ });
72
+ expect(entriesBefore).toHaveLength(3);
73
+
74
+ // This should delete all entries and then the namespace.
75
+ // Before the fix, this threw "cannot delete, has entries" because
76
+ // the pagination loop checked isDone before processing the page,
77
+ // skipping deletion when entries fit in a single page.
78
+ await t.action(api.namespaces.deleteNamespaceSync, {
79
+ namespaceId,
80
+ });
81
+
82
+ // Verify all entries are deleted
83
+ const entriesAfter = await t.run(async (ctx) => {
84
+ return ctx.db
85
+ .query("entries")
86
+ .filter((q) => q.eq(q.field("namespaceId"), namespaceId))
87
+ .collect();
88
+ });
89
+ expect(entriesAfter).toHaveLength(0);
90
+
91
+ // Verify namespace is deleted
92
+ const namespaceAfter = await t.run(async (ctx) => {
93
+ return ctx.db.get(namespaceId);
94
+ });
95
+ expect(namespaceAfter).toBeNull();
96
+ });
97
+ });
@@ -1,3 +1,18 @@
1
+ import { assert } from "convex-helpers";
2
+ import { paginator } from "convex-helpers/server/pagination";
3
+ import { mergedStream, stream } from "convex-helpers/server/stream";
4
+ import { paginationOptsValidator, PaginationResult } from "convex/server";
5
+ import type { Infer } from "convex/values";
6
+ import {
7
+ statuses,
8
+ vActiveStatus,
9
+ vEntry,
10
+ vNamespace,
11
+ vPaginationResult,
12
+ vStatus,
13
+ type OnCompleteNamespace,
14
+ } from "../shared.js";
15
+ import { api } from "./_generated/api.js";
1
16
  import type { Doc, Id } from "./_generated/dataModel.js";
2
17
  import {
3
18
  action,
@@ -5,59 +20,15 @@ import {
5
20
  mutation,
6
21
  query,
7
22
  type MutationCtx,
8
- type QueryCtx,
9
23
  } from "./_generated/server.js";
10
- import { schema, v } from "./schema.js";
24
+ import { deleteEntrySync } from "./entries.js";
11
25
  import {
12
- vNamespace,
13
- vPaginationResult,
14
- vActiveStatus,
15
- type Namespace,
16
- type NamespaceId,
17
- type OnCompleteNamespace,
18
- vStatus,
19
- statuses,
20
- filterNamesContain,
21
- vEntry,
22
- } from "../shared.js";
23
- import { paginationOptsValidator, PaginationResult } from "convex/server";
24
- import { paginator } from "convex-helpers/server/pagination";
25
- import type { Infer, ObjectType } from "convex/values";
26
- import { mergedStream, stream } from "convex-helpers/server/stream";
27
- import { assert } from "convex-helpers";
28
- import { api } from "./_generated/api.js";
29
-
30
- function namespaceIsCompatible(
31
- existing: Doc<"namespaces">,
32
- args: {
33
- modelId: string;
34
- dimension: number;
35
- filterNames: string[];
36
- },
37
- ) {
38
- // Check basic compatibility
39
- if (
40
- existing.modelId !== args.modelId ||
41
- existing.dimension !== args.dimension
42
- ) {
43
- return false;
44
- }
45
-
46
- // For filter names, the namespace must support all requested filters
47
- // but can support additional filters (superset is OK)
48
- if (!filterNamesContain(existing.filterNames, args.filterNames)) {
49
- return false;
50
- }
51
-
52
- return true;
53
- }
54
-
55
- export const vNamespaceLookupArgs = {
56
- namespace: v.string(),
57
- modelId: v.string(),
58
- dimension: v.number(),
59
- filterNames: v.array(v.string()),
60
- };
26
+ getCompatibleNamespaceHandler,
27
+ namespaceIsCompatible,
28
+ publicNamespace,
29
+ vNamespaceLookupArgs,
30
+ } from "./helpers.js";
31
+ import { schema, v } from "./schema.js";
61
32
 
62
33
  export const get = query({
63
34
  args: vNamespaceLookupArgs,
@@ -77,24 +48,6 @@ export const getCompatibleNamespace = internalQuery({
77
48
  handler: getCompatibleNamespaceHandler,
78
49
  });
79
50
 
80
- export async function getCompatibleNamespaceHandler(
81
- ctx: QueryCtx,
82
- args: ObjectType<typeof vNamespaceLookupArgs>,
83
- ) {
84
- const iter = ctx.db
85
- .query("namespaces")
86
- .withIndex("status_namespace_version", (q) =>
87
- q.eq("status.kind", "ready").eq("namespace", args.namespace),
88
- )
89
- .order("desc");
90
- for await (const existing of iter) {
91
- if (namespaceIsCompatible(existing, args)) {
92
- return existing;
93
- }
94
- }
95
- return null;
96
- }
97
-
98
51
  export const lookup = query({
99
52
  args: {
100
53
  namespace: v.string(),
@@ -312,16 +265,6 @@ export const listNamespaceVersions = query({
312
265
  },
313
266
  });
314
267
 
315
- export function publicNamespace(namespace: Doc<"namespaces">): Namespace {
316
- const { _id, _creationTime, status, ...rest } = namespace;
317
- return {
318
- namespaceId: _id as unknown as NamespaceId,
319
- createdAt: _creationTime,
320
- ...rest,
321
- status: status.kind,
322
- };
323
- }
324
-
325
268
  export const deleteNamespace = mutation({
326
269
  args: { namespaceId: v.id("namespaces") },
327
270
  returns: v.object({
@@ -368,13 +311,13 @@ export const deleteNamespaceSync = action({
368
311
  cursor,
369
312
  },
370
313
  })) as PaginationResult<Infer<typeof vEntry>>;
314
+ for (const entry of entries.page) {
315
+ await deleteEntrySync(ctx, entry.entryId as unknown as Id<"entries">);
316
+ }
371
317
  if (entries.isDone) {
372
318
  break;
373
319
  }
374
320
  cursor = entries.continueCursor;
375
- await ctx.runAction(api.entries.deleteSync, {
376
- entryId: entries.page[0].entryId as unknown as Id<"entries">,
377
- });
378
321
  }
379
322
  }
380
323
  await ctx.runMutation(api.namespaces.deleteNamespace, {