convex-agent-knowledge 0.0.1

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.
Files changed (97) hide show
  1. package/LICENSE +73 -0
  2. package/README.md +156 -0
  3. package/dist/client/chunking.d.ts +7 -0
  4. package/dist/client/chunking.d.ts.map +1 -0
  5. package/dist/client/chunking.js +36 -0
  6. package/dist/client/chunking.js.map +1 -0
  7. package/dist/client/extraction.d.ts +9 -0
  8. package/dist/client/extraction.d.ts.map +1 -0
  9. package/dist/client/extraction.js +88 -0
  10. package/dist/client/extraction.js.map +1 -0
  11. package/dist/client/hash.d.ts +3 -0
  12. package/dist/client/hash.d.ts.map +1 -0
  13. package/dist/client/hash.js +17 -0
  14. package/dist/client/hash.js.map +1 -0
  15. package/dist/client/index.d.ts +99 -0
  16. package/dist/client/index.d.ts.map +1 -0
  17. package/dist/client/index.js +194 -0
  18. package/dist/client/index.js.map +1 -0
  19. package/dist/client/types.d.ts +126 -0
  20. package/dist/client/types.d.ts.map +1 -0
  21. package/dist/client/types.js +2 -0
  22. package/dist/client/types.js.map +1 -0
  23. package/dist/component/_generated/api.d.ts +40 -0
  24. package/dist/component/_generated/api.d.ts.map +1 -0
  25. package/dist/component/_generated/api.js +31 -0
  26. package/dist/component/_generated/api.js.map +1 -0
  27. package/dist/component/_generated/component.d.ts +277 -0
  28. package/dist/component/_generated/component.d.ts.map +1 -0
  29. package/dist/component/_generated/component.js +11 -0
  30. package/dist/component/_generated/component.js.map +1 -0
  31. package/dist/component/_generated/dataModel.d.ts +46 -0
  32. package/dist/component/_generated/dataModel.d.ts.map +1 -0
  33. package/dist/component/_generated/dataModel.js +11 -0
  34. package/dist/component/_generated/dataModel.js.map +1 -0
  35. package/dist/component/_generated/server.d.ts +121 -0
  36. package/dist/component/_generated/server.d.ts.map +1 -0
  37. package/dist/component/_generated/server.js +78 -0
  38. package/dist/component/_generated/server.js.map +1 -0
  39. package/dist/component/actions.d.ts +12 -0
  40. package/dist/component/actions.d.ts.map +1 -0
  41. package/dist/component/actions.js +42 -0
  42. package/dist/component/actions.js.map +1 -0
  43. package/dist/component/convex.config.d.ts +3 -0
  44. package/dist/component/convex.config.d.ts.map +1 -0
  45. package/dist/component/convex.config.js +4 -0
  46. package/dist/component/convex.config.js.map +1 -0
  47. package/dist/component/mutations.d.ts +85 -0
  48. package/dist/component/mutations.d.ts.map +1 -0
  49. package/dist/component/mutations.js +407 -0
  50. package/dist/component/mutations.js.map +1 -0
  51. package/dist/component/queries.d.ts +95 -0
  52. package/dist/component/queries.d.ts.map +1 -0
  53. package/dist/component/queries.js +181 -0
  54. package/dist/component/queries.js.map +1 -0
  55. package/dist/component/schema.d.ts +442 -0
  56. package/dist/component/schema.d.ts.map +1 -0
  57. package/dist/component/schema.js +140 -0
  58. package/dist/component/schema.js.map +1 -0
  59. package/dist/component/validators.d.ts +158 -0
  60. package/dist/component/validators.d.ts.map +1 -0
  61. package/dist/component/validators.js +90 -0
  62. package/dist/component/validators.js.map +1 -0
  63. package/dist/node/index.d.ts +3 -0
  64. package/dist/node/index.d.ts.map +1 -0
  65. package/dist/node/index.js +2 -0
  66. package/dist/node/index.js.map +1 -0
  67. package/dist/node/neo4j.d.ts +5 -0
  68. package/dist/node/neo4j.d.ts.map +1 -0
  69. package/dist/node/neo4j.js +147 -0
  70. package/dist/node/neo4j.js.map +1 -0
  71. package/dist/shared/ranking.d.ts +17 -0
  72. package/dist/shared/ranking.d.ts.map +1 -0
  73. package/dist/shared/ranking.js +44 -0
  74. package/dist/shared/ranking.js.map +1 -0
  75. package/package.json +84 -0
  76. package/src/client/chunking.test.ts +25 -0
  77. package/src/client/chunking.ts +45 -0
  78. package/src/client/extraction.test.ts +15 -0
  79. package/src/client/extraction.ts +108 -0
  80. package/src/client/hash.test.ts +17 -0
  81. package/src/client/hash.ts +17 -0
  82. package/src/client/index.ts +307 -0
  83. package/src/client/types.ts +141 -0
  84. package/src/component/_generated/api.ts +56 -0
  85. package/src/component/_generated/component.ts +310 -0
  86. package/src/component/_generated/dataModel.ts +60 -0
  87. package/src/component/_generated/server.ts +156 -0
  88. package/src/component/actions.ts +47 -0
  89. package/src/component/convex.config.ts +5 -0
  90. package/src/component/mutations.ts +445 -0
  91. package/src/component/queries.ts +202 -0
  92. package/src/component/schema.ts +161 -0
  93. package/src/component/validators.ts +104 -0
  94. package/src/node/index.ts +2 -0
  95. package/src/node/neo4j.ts +210 -0
  96. package/src/shared/ranking.test.ts +30 -0
  97. package/src/shared/ranking.ts +64 -0
@@ -0,0 +1,44 @@
1
+ export function clamp(value, min = 0, max = 1) {
2
+ return Math.min(max, Math.max(min, value));
3
+ }
4
+ export function fuseMemoryScores(semanticCards, graphCards, options) {
5
+ const semanticWeight = options?.semanticWeight ?? 0.65;
6
+ const graphWeight = options?.graphWeight ?? 0.25;
7
+ const importanceWeight = options?.importanceWeight ?? 0.1;
8
+ const byMemoryId = new Map();
9
+ for (const card of semanticCards) {
10
+ byMemoryId.set(card.memoryId, {
11
+ ...card,
12
+ semanticScore: card.semanticScore ?? card.score,
13
+ });
14
+ }
15
+ for (const graphCard of graphCards) {
16
+ const current = byMemoryId.get(graphCard.memoryId);
17
+ if (current) {
18
+ byMemoryId.set(graphCard.memoryId, {
19
+ ...current,
20
+ graphScore: graphCard.graphScore ?? graphCard.score,
21
+ });
22
+ }
23
+ else {
24
+ byMemoryId.set(graphCard.memoryId, {
25
+ ...graphCard,
26
+ graphScore: graphCard.graphScore ?? graphCard.score,
27
+ semanticScore: graphCard.semanticScore ?? 0,
28
+ });
29
+ }
30
+ }
31
+ return [...byMemoryId.values()]
32
+ .map((card) => {
33
+ const semanticScore = card.semanticScore ?? 0;
34
+ const graphScore = card.graphScore ?? 0;
35
+ const importance = card.importance ?? 0;
36
+ return {
37
+ ...card,
38
+ score: semanticScore * semanticWeight + graphScore * graphWeight + importance * importanceWeight,
39
+ };
40
+ })
41
+ .sort((a, b) => b.score - a.score)
42
+ .slice(0, options?.limit ?? 10);
43
+ }
44
+ //# sourceMappingURL=ranking.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ranking.js","sourceRoot":"","sources":["../../src/shared/ranking.ts"],"names":[],"mappings":"AAQA,MAAM,UAAU,KAAK,CAAC,KAAa,EAAE,GAAG,GAAG,CAAC,EAAE,GAAG,GAAG,CAAC;IACnD,OAAO,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC;AAC7C,CAAC;AAED,MAAM,UAAU,gBAAgB,CAC9B,aAAkB,EAClB,UAAe,EACf,OAKC;IAED,MAAM,cAAc,GAAG,OAAO,EAAE,cAAc,IAAI,IAAI,CAAC;IACvD,MAAM,WAAW,GAAG,OAAO,EAAE,WAAW,IAAI,IAAI,CAAC;IACjD,MAAM,gBAAgB,GAAG,OAAO,EAAE,gBAAgB,IAAI,GAAG,CAAC;IAC1D,MAAM,UAAU,GAAG,IAAI,GAAG,EAAa,CAAC;IAExC,KAAK,MAAM,IAAI,IAAI,aAAa,EAAE,CAAC;QACjC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,EAAE;YAC5B,GAAG,IAAI;YACP,aAAa,EAAE,IAAI,CAAC,aAAa,IAAI,IAAI,CAAC,KAAK;SAChD,CAAC,CAAC;IACL,CAAC;IAED,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;QACnC,MAAM,OAAO,GAAG,UAAU,CAAC,GAAG,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;QACnD,IAAI,OAAO,EAAE,CAAC;YACZ,UAAU,CAAC,GAAG,CAAC,SAAS,CAAC,QAAQ,EAAE;gBACjC,GAAG,OAAO;gBACV,UAAU,EAAE,SAAS,CAAC,UAAU,IAAI,SAAS,CAAC,KAAK;aACpD,CAAC,CAAC;QACL,CAAC;aAAM,CAAC;YACN,UAAU,CAAC,GAAG,CAAC,SAAS,CAAC,QAAQ,EAAE;gBACjC,GAAG,SAAS;gBACZ,UAAU,EAAE,SAAS,CAAC,UAAU,IAAI,SAAS,CAAC,KAAK;gBACnD,aAAa,EAAE,SAAS,CAAC,aAAa,IAAI,CAAC;aAC5C,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,OAAO,CAAC,GAAG,UAAU,CAAC,MAAM,EAAE,CAAC;SAC5B,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE;QACZ,MAAM,aAAa,GAAG,IAAI,CAAC,aAAa,IAAI,CAAC,CAAC;QAC9C,MAAM,UAAU,GAAG,IAAI,CAAC,UAAU,IAAI,CAAC,CAAC;QACxC,MAAM,UAAU,GAAG,IAAI,CAAC,UAAU,IAAI,CAAC,CAAC;QACxC,OAAO;YACL,GAAG,IAAI;YACP,KAAK,EACH,aAAa,GAAG,cAAc,GAAG,UAAU,GAAG,WAAW,GAAG,UAAU,GAAG,gBAAgB;SAC5F,CAAC;IACJ,CAAC,CAAC;SACD,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC;SACjC,KAAK,CAAC,CAAC,EAAE,OAAO,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC;AACpC,CAAC"}
package/package.json ADDED
@@ -0,0 +1,84 @@
1
+ {
2
+ "name": "convex-agent-knowledge",
3
+ "version": "0.0.1",
4
+ "description": "Convex component for agent memory backed by Convex vector search and Neo4j graph traversal.",
5
+ "keywords": [
6
+ "agent",
7
+ "ai",
8
+ "convex",
9
+ "convex-component",
10
+ "knowledge-graph",
11
+ "memory",
12
+ "neo4j",
13
+ "vector-search"
14
+ ],
15
+ "homepage": "https://github.com/phl28/agent-knowledge#readme",
16
+ "bugs": {
17
+ "url": "https://github.com/phl28/agent-knowledge/issues"
18
+ },
19
+ "license": "Apache-2.0",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/phl28/agent-knowledge.git"
23
+ },
24
+ "files": [
25
+ "dist",
26
+ "src",
27
+ "README.md"
28
+ ],
29
+ "type": "module",
30
+ "main": "./dist/client/index.js",
31
+ "types": "./dist/client/index.d.ts",
32
+ "exports": {
33
+ ".": {
34
+ "types": "./dist/client/index.d.ts",
35
+ "default": "./dist/client/index.js"
36
+ },
37
+ "./convex.config": "./dist/component/convex.config.js",
38
+ "./convex.config.js": {
39
+ "types": "./dist/component/convex.config.d.ts",
40
+ "default": "./dist/component/convex.config.js"
41
+ },
42
+ "./chunking": {
43
+ "types": "./dist/client/chunking.d.ts",
44
+ "default": "./dist/client/chunking.js"
45
+ },
46
+ "./node": {
47
+ "types": "./dist/node/index.d.ts",
48
+ "default": "./dist/node/index.js"
49
+ },
50
+ "./_generated/component.js": {
51
+ "types": "./dist/component/_generated/component.d.ts"
52
+ },
53
+ "./package.json": "./package.json"
54
+ },
55
+ "dependencies": {
56
+ "ai": "^6.0.168",
57
+ "neo4j-driver": "^6.0.1",
58
+ "zod": "^4.3.6"
59
+ },
60
+ "devDependencies": {
61
+ "@ai-sdk/openai": "^3.0.53",
62
+ "@types/node": "^25.6.0",
63
+ "convex": "^1.35.1",
64
+ "oxfmt": "^0.45.0",
65
+ "oxlint": "^1.60.0",
66
+ "typescript": "^6.0.3",
67
+ "vitest": "^4.1.4"
68
+ },
69
+ "peerDependencies": {
70
+ "convex": ">=1.29.0"
71
+ },
72
+ "scripts": {
73
+ "build": "tsc -p tsconfig.json",
74
+ "check": "pnpm lint && pnpm format:check && pnpm typecheck",
75
+ "format": "oxfmt README.md package.json convex.json pnpm-workspace.yaml src example/convex '!src/component/_generated/**' '!example/convex/_generated/**'",
76
+ "format:check": "oxfmt --check README.md package.json convex.json pnpm-workspace.yaml src example/convex '!src/component/_generated/**' '!example/convex/_generated/**'",
77
+ "lint": "oxlint --deny correctness --vitest-plugin --ignore-pattern src/component/_generated --ignore-pattern example/convex/_generated .",
78
+ "lint:fix": "oxlint --deny correctness --vitest-plugin --fix --ignore-pattern src/component/_generated --ignore-pattern example/convex/_generated .",
79
+ "publish:alpha": "pnpm publish --tag alpha",
80
+ "test": "vitest run",
81
+ "typecheck": "tsc -p tsconfig.json --noEmit",
82
+ "build:codegen": "convex codegen --component-dir ./src/component && pnpm build"
83
+ }
84
+ }
@@ -0,0 +1,25 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { chunkText } from "./chunking.js";
3
+
4
+ describe("chunkText", () => {
5
+ it("returns no chunks for empty input", () => {
6
+ expect(chunkText(" ")).toEqual([]);
7
+ });
8
+
9
+ it("keeps short text as one chunk", () => {
10
+ expect(chunkText("Alice works on Convex components.")).toEqual([
11
+ {
12
+ text: "Alice works on Convex components.",
13
+ tokenCount: 9,
14
+ },
15
+ ]);
16
+ });
17
+
18
+ it("splits long text with overlap", () => {
19
+ const text = Array.from({ length: 60 }, (_, index) => `Sentence ${index}.`).join(" ");
20
+ const chunks = chunkText(text, { maxChars: 220, overlapChars: 30 });
21
+ expect(chunks.length).toBeGreaterThan(1);
22
+ expect(chunks[0]!.text.length).toBeLessThanOrEqual(220);
23
+ expect(chunks.at(-1)!.text).toContain("Sentence 59");
24
+ });
25
+ });
@@ -0,0 +1,45 @@
1
+ import type { ChunkInput } from "./types.js";
2
+
3
+ export type ChunkTextOptions = {
4
+ maxChars?: number;
5
+ overlapChars?: number;
6
+ };
7
+
8
+ export function chunkText(text: string, options?: ChunkTextOptions): ChunkInput[] {
9
+ const trimmed = text.trim();
10
+ if (!trimmed) {
11
+ return [];
12
+ }
13
+ const maxChars = Math.max(options?.maxChars ?? 1_500, 200);
14
+ const overlapChars = Math.min(options?.overlapChars ?? 150, Math.floor(maxChars / 3));
15
+ const chunks: ChunkInput[] = [];
16
+ let cursor = 0;
17
+
18
+ while (cursor < trimmed.length) {
19
+ const hardEnd = Math.min(cursor + maxChars, trimmed.length);
20
+ let end = hardEnd;
21
+ if (hardEnd < trimmed.length) {
22
+ const paragraphBreak = trimmed.lastIndexOf("\n\n", hardEnd);
23
+ const sentenceBreak = trimmed.lastIndexOf(". ", hardEnd);
24
+ const whitespaceBreak = trimmed.lastIndexOf(" ", hardEnd);
25
+ const bestBreak = Math.max(paragraphBreak, sentenceBreak, whitespaceBreak);
26
+ if (bestBreak > cursor + Math.floor(maxChars * 0.55)) {
27
+ end = bestBreak + (bestBreak === sentenceBreak ? 1 : 0);
28
+ }
29
+ }
30
+
31
+ const chunk = trimmed.slice(cursor, end).trim();
32
+ if (chunk) {
33
+ chunks.push({
34
+ text: chunk,
35
+ tokenCount: Math.ceil(chunk.length / 4),
36
+ });
37
+ }
38
+ if (end >= trimmed.length) {
39
+ break;
40
+ }
41
+ cursor = Math.max(end - overlapChars, cursor + 1);
42
+ }
43
+
44
+ return chunks;
45
+ }
@@ -0,0 +1,15 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { heuristicExtractKnowledge } from "./extraction.js";
3
+
4
+ describe("heuristicExtractKnowledge", () => {
5
+ it("extracts capitalized entity candidates", () => {
6
+ const result = heuristicExtractKnowledge(
7
+ "agent:alice",
8
+ "Alice is building Agent Knowledge with Convex and Neo4j.",
9
+ );
10
+ expect(result.entities.map((entity) => entity.name)).toEqual(
11
+ expect.arrayContaining(["Alice", "Agent Knowledge", "Convex", "Neo4j"]),
12
+ );
13
+ expect(result.relationships).toEqual([]);
14
+ });
15
+ });
@@ -0,0 +1,108 @@
1
+ import { generateObject } from "ai";
2
+ import { z } from "zod";
3
+ import type { ExtractedKnowledge } from "./types.js";
4
+ import { normalizeEntityId } from "./hash.js";
5
+
6
+ const extractionSchema = z.object({
7
+ entities: z.array(
8
+ z.object({
9
+ name: z.string(),
10
+ type: z.string().default("entity"),
11
+ description: z.string().optional(),
12
+ aliases: z.array(z.string()).optional(),
13
+ confidence: z.number().min(0).max(1).optional(),
14
+ }),
15
+ ),
16
+ relationships: z.array(
17
+ z.object({
18
+ from: z.string(),
19
+ to: z.string(),
20
+ type: z.string(),
21
+ description: z.string().optional(),
22
+ confidence: z.number().min(0).max(1).optional(),
23
+ weight: z.number().min(0).max(1).optional(),
24
+ }),
25
+ ),
26
+ });
27
+
28
+ export type ExtractKnowledgeOptions = {
29
+ namespace: string;
30
+ text: string;
31
+ model?: unknown;
32
+ };
33
+
34
+ export async function extractKnowledge(
35
+ options: ExtractKnowledgeOptions,
36
+ ): Promise<ExtractedKnowledge> {
37
+ if (options.model) {
38
+ const result = await generateObject({
39
+ model: options.model as never,
40
+ schema: extractionSchema,
41
+ prompt: [
42
+ "Extract entities and relationships useful for long-lived agent memory.",
43
+ "Use concise entity names. Relationship types should be uppercase snake case.",
44
+ "Only include facts supported by the text.",
45
+ "",
46
+ options.text,
47
+ ].join("\n"),
48
+ });
49
+ const byName = new Map<string, string>();
50
+ const entities = result.object.entities.map((entity) => {
51
+ const externalId = normalizeEntityId(options.namespace, entity.name, entity.type);
52
+ byName.set(entity.name.toLowerCase(), externalId);
53
+ return {
54
+ externalId,
55
+ name: entity.name,
56
+ type: entity.type,
57
+ ...(entity.description === undefined ? {} : { description: entity.description }),
58
+ ...(entity.aliases === undefined ? {} : { aliases: entity.aliases }),
59
+ confidence: entity.confidence ?? 0.75,
60
+ };
61
+ });
62
+ return {
63
+ entities,
64
+ relationships: result.object.relationships
65
+ .map((relationship) => {
66
+ const fromEntityExternalId = byName.get(relationship.from.toLowerCase());
67
+ const toEntityExternalId = byName.get(relationship.to.toLowerCase());
68
+ if (!fromEntityExternalId || !toEntityExternalId) {
69
+ return null;
70
+ }
71
+ return {
72
+ fromEntityExternalId,
73
+ toEntityExternalId,
74
+ type: relationship.type,
75
+ ...(relationship.description === undefined
76
+ ? {}
77
+ : { description: relationship.description }),
78
+ confidence: relationship.confidence ?? 0.75,
79
+ weight: relationship.weight ?? 0.5,
80
+ };
81
+ })
82
+ .filter((relationship): relationship is NonNullable<typeof relationship> =>
83
+ Boolean(relationship),
84
+ ),
85
+ };
86
+ }
87
+
88
+ return heuristicExtractKnowledge(options.namespace, options.text);
89
+ }
90
+
91
+ export function heuristicExtractKnowledge(namespace: string, text: string): ExtractedKnowledge {
92
+ const candidates = new Set<string>();
93
+ for (const match of text.matchAll(
94
+ /\b[A-Z][a-zA-Z0-9_-]{2,}(?:\s+[A-Z][a-zA-Z0-9_-]{2,}){0,3}\b/g,
95
+ )) {
96
+ candidates.add(match[0]);
97
+ }
98
+ const entities = [...candidates].slice(0, 24).map((name) => ({
99
+ externalId: normalizeEntityId(namespace, name),
100
+ name,
101
+ type: "entity",
102
+ confidence: 0.45,
103
+ }));
104
+ return {
105
+ entities,
106
+ relationships: [],
107
+ };
108
+ }
@@ -0,0 +1,17 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { normalizeEntityId, stableHash } from "./hash.js";
3
+
4
+ describe("stableHash", () => {
5
+ it("is deterministic", () => {
6
+ expect(stableHash("agent memory")).toBe(stableHash("agent memory"));
7
+ expect(stableHash("agent memory")).not.toBe(stableHash("other memory"));
8
+ });
9
+ });
10
+
11
+ describe("normalizeEntityId", () => {
12
+ it("builds namespace-scoped entity IDs", () => {
13
+ expect(normalizeEntityId("agent:alice", "Convex Components", "tool")).toBe(
14
+ "agent:alice:tool:convex-components",
15
+ );
16
+ });
17
+ });
@@ -0,0 +1,17 @@
1
+ export function stableHash(value: string) {
2
+ let hash = 0x811c9dc5;
3
+ for (let index = 0; index < value.length; index += 1) {
4
+ hash ^= value.charCodeAt(index);
5
+ hash = Math.imul(hash, 0x01000193);
6
+ }
7
+ return (hash >>> 0).toString(16).padStart(8, "0");
8
+ }
9
+
10
+ export function normalizeEntityId(namespace: string, name: string, type = "entity") {
11
+ const normalized = name
12
+ .trim()
13
+ .toLowerCase()
14
+ .replace(/[^a-z0-9]+/g, "-")
15
+ .replace(/^-+|-+$/g, "");
16
+ return `${namespace}:${type}:${normalized || stableHash(name)}`;
17
+ }
@@ -0,0 +1,307 @@
1
+ import { embed, embedMany } from "ai";
2
+ import { chunkText, type ChunkTextOptions } from "./chunking.js";
3
+ import { extractKnowledge } from "./extraction.js";
4
+ import { stableHash } from "./hash.js";
5
+ import { fuseMemoryScores } from "../shared/ranking.js";
6
+ import type {
7
+ AgentKnowledgeComponent,
8
+ ChunkInput,
9
+ ConvexActionCtx,
10
+ ConvexMutationCtx,
11
+ ConvexQueryCtx,
12
+ EmbeddedChunkInput,
13
+ ExtractedKnowledge,
14
+ GraphStore,
15
+ GraphSyncJob,
16
+ MemoryCard,
17
+ MemorySource,
18
+ SearchType,
19
+ } from "./types.js";
20
+
21
+ export type AgentKnowledgeOptions = {
22
+ textEmbeddingModel?: unknown;
23
+ embeddingDimension: number;
24
+ extractionModel?: unknown;
25
+ graph?: GraphStore;
26
+ chunking?: ChunkTextOptions;
27
+ extract?: (input: {
28
+ namespace: string;
29
+ text: string;
30
+ chunks: ChunkInput[];
31
+ }) => Promise<ExtractedKnowledge>;
32
+ };
33
+
34
+ export type RememberInput = {
35
+ namespace: string;
36
+ key?: string;
37
+ agentId?: string;
38
+ text: string;
39
+ source?: MemorySource;
40
+ metadata?: unknown;
41
+ importance?: number;
42
+ chunks?: ChunkInput[];
43
+ extracted?: ExtractedKnowledge;
44
+ };
45
+
46
+ export type RecallInput = {
47
+ namespace: string;
48
+ query: string;
49
+ searchType?: SearchType;
50
+ limit?: number;
51
+ agentId?: string;
52
+ entityHints?: string[];
53
+ queryEmbedding?: number[];
54
+ graph?: GraphStore;
55
+ };
56
+
57
+ export type ObserveInput = {
58
+ namespace: string;
59
+ memoryId: string;
60
+ query: string;
61
+ outcome: "helpful" | "not_helpful" | "neutral";
62
+ feedback?: string;
63
+ metadata?: unknown;
64
+ };
65
+
66
+ export class AgentKnowledge {
67
+ constructor(
68
+ private readonly component: AgentKnowledgeComponent,
69
+ private readonly options: AgentKnowledgeOptions,
70
+ ) {}
71
+
72
+ async remember(ctx: ConvexActionCtx, input: RememberInput) {
73
+ const chunks = input.chunks ?? chunkText(input.text, this.options.chunking);
74
+ if (chunks.length === 0) {
75
+ throw new Error("Cannot remember empty text");
76
+ }
77
+ const embeddedChunks = await this.embedChunks(chunks);
78
+ const extracted =
79
+ input.extracted ??
80
+ (this.options.extract
81
+ ? await this.options.extract({
82
+ namespace: input.namespace,
83
+ text: input.text,
84
+ chunks,
85
+ })
86
+ : await extractKnowledge({
87
+ namespace: input.namespace,
88
+ text: input.text,
89
+ model: this.options.extractionModel,
90
+ }));
91
+
92
+ const mutationArgs = {
93
+ namespace: input.namespace,
94
+ ...(input.key === undefined ? {} : { key: input.key }),
95
+ ...(input.agentId === undefined ? {} : { agentId: input.agentId }),
96
+ text: input.text,
97
+ contentHash: stableHash(`${input.namespace}:${input.text}`),
98
+ ...(input.source === undefined ? {} : { source: input.source }),
99
+ ...(input.metadata === undefined ? {} : { metadata: input.metadata }),
100
+ ...(input.importance === undefined ? {} : { importance: input.importance }),
101
+ embeddingDimension: this.options.embeddingDimension,
102
+ chunks: embeddedChunks,
103
+ entities: extracted.entities,
104
+ relationships: extracted.relationships,
105
+ };
106
+
107
+ const result = await ctx.runMutation(this.component.mutations.remember, mutationArgs);
108
+ if (this.options.graph) {
109
+ await this.syncGraph(ctx, { limit: 10 });
110
+ }
111
+ return result as {
112
+ memoryId: string;
113
+ replacedMemoryId?: string;
114
+ chunkCount: number;
115
+ entityCount: number;
116
+ relationshipCount: number;
117
+ graphSyncJobId: string;
118
+ };
119
+ }
120
+
121
+ async recall(ctx: ConvexActionCtx, input: RecallInput) {
122
+ const searchType = input.searchType ?? "hybrid";
123
+ const limit = input.limit ?? 10;
124
+ const queryEmbedding =
125
+ input.queryEmbedding ??
126
+ (searchType === "graph" ? undefined : await this.embedQuery(input.query));
127
+ const semanticResult =
128
+ searchType === "graph"
129
+ ? { results: [] as MemoryCard[] }
130
+ : ((await ctx.runAction(this.component.actions.recall, {
131
+ namespace: input.namespace,
132
+ query: input.query,
133
+ ...(queryEmbedding === undefined ? {} : { queryEmbedding }),
134
+ embeddingDimension: this.options.embeddingDimension,
135
+ searchType,
136
+ limit,
137
+ ...(input.agentId === undefined ? {} : { agentId: input.agentId }),
138
+ })) as { results: MemoryCard[] });
139
+
140
+ const graph = input.graph ?? this.options.graph;
141
+ if (!graph || searchType === "semantic") {
142
+ return semanticResult;
143
+ }
144
+
145
+ const graphScores = await graph.expand({
146
+ namespace: input.namespace,
147
+ seedMemoryIds: semanticResult.results.map((card) => card.memoryId),
148
+ hops: 2,
149
+ limit: Math.max(limit * 4, 16),
150
+ ...(input.entityHints === undefined ? {} : { entityHints: input.entityHints }),
151
+ });
152
+ const graphCards =
153
+ graphScores.length === 0
154
+ ? []
155
+ : ((await ctx.runQuery(this.component.queries.fetchMemoryCards, {
156
+ matches: graphScores.map((score) => ({
157
+ memoryId: score.memoryId,
158
+ score: score.graphScore,
159
+ graphScore: score.graphScore,
160
+ })),
161
+ })) as MemoryCard[]);
162
+
163
+ return {
164
+ results: fuseMemoryScores(semanticResult.results, graphCards, { limit }),
165
+ };
166
+ }
167
+
168
+ async observe(ctx: ConvexMutationCtx, input: ObserveInput) {
169
+ return await ctx.runMutation(this.component.mutations.observe, {
170
+ namespace: input.namespace,
171
+ memoryId: input.memoryId,
172
+ query: input.query,
173
+ outcome: input.outcome,
174
+ ...(input.feedback === undefined ? {} : { feedback: input.feedback }),
175
+ ...(input.metadata === undefined ? {} : { metadata: input.metadata }),
176
+ });
177
+ }
178
+
179
+ async promote(ctx: ConvexMutationCtx, input: { namespace: string; limit?: number }) {
180
+ return (await ctx.runMutation(this.component.mutations.promote, {
181
+ namespace: input.namespace,
182
+ ...(input.limit === undefined ? {} : { limit: input.limit }),
183
+ })) as { promoted: number };
184
+ }
185
+
186
+ async deleteByKey(ctx: ConvexMutationCtx, input: { namespace: string; key: string }) {
187
+ return (await ctx.runMutation(this.component.mutations.deleteByKey, {
188
+ namespace: input.namespace,
189
+ key: input.key,
190
+ })) as {
191
+ deleted: boolean;
192
+ memoryId?: string;
193
+ graphSyncJobId?: string;
194
+ };
195
+ }
196
+
197
+ async getMemory(ctx: ConvexQueryCtx, input: { memoryId: string }) {
198
+ return (await ctx.runQuery(this.component.queries.getMemory, {
199
+ memoryId: input.memoryId,
200
+ })) as MemoryCard | null;
201
+ }
202
+
203
+ async listMemories(
204
+ ctx: ConvexQueryCtx,
205
+ input: { namespace: string; limit?: number; cursor?: string },
206
+ ) {
207
+ return (await ctx.runQuery(this.component.queries.listMemories, {
208
+ namespace: input.namespace,
209
+ ...(input.limit === undefined ? {} : { limit: input.limit }),
210
+ ...(input.cursor === undefined ? {} : { cursor: input.cursor }),
211
+ })) as {
212
+ page: MemoryCard[];
213
+ continueCursor: string | null;
214
+ isDone: boolean;
215
+ };
216
+ }
217
+
218
+ async syncGraph(ctx: ConvexActionCtx, input?: { graph?: GraphStore; limit?: number }) {
219
+ const graph = input?.graph ?? this.options.graph;
220
+ if (!graph) {
221
+ return { succeeded: 0, failed: 0 };
222
+ }
223
+ const jobs = (await ctx.runQuery(this.component.queries.listPendingGraphSyncJobs, {
224
+ limit: input?.limit ?? 10,
225
+ })) as GraphSyncJob[];
226
+ let succeeded = 0;
227
+ let failed = 0;
228
+ for (const job of jobs) {
229
+ await ctx.runMutation(this.component.mutations.markGraphSyncJobRunning, {
230
+ jobId: job.jobId,
231
+ });
232
+ try {
233
+ await graph.syncJob(job);
234
+ await ctx.runMutation(this.component.mutations.markGraphSyncJobSucceeded, {
235
+ jobId: job.jobId,
236
+ });
237
+ succeeded += 1;
238
+ } catch (error) {
239
+ await ctx.runMutation(this.component.mutations.markGraphSyncJobFailed, {
240
+ jobId: job.jobId,
241
+ error: error instanceof Error ? error.message : String(error),
242
+ });
243
+ failed += 1;
244
+ }
245
+ }
246
+ return { succeeded, failed };
247
+ }
248
+
249
+ private async embedChunks(chunks: ChunkInput[]): Promise<EmbeddedChunkInput[]> {
250
+ if (!this.options.textEmbeddingModel) {
251
+ throw new Error("AgentKnowledge requires textEmbeddingModel to remember text");
252
+ }
253
+ const result = await embedMany({
254
+ model: this.options.textEmbeddingModel as never,
255
+ values: chunks.map((chunk) => chunk.text),
256
+ });
257
+ return chunks.map((chunk, index) => {
258
+ const embedding = result.embeddings[index];
259
+ if (!embedding) {
260
+ throw new Error(`Missing embedding for chunk ${index}`);
261
+ }
262
+ if (embedding.length !== this.options.embeddingDimension) {
263
+ throw new Error(
264
+ `Embedding dimension ${embedding.length} does not match configured dimension ${this.options.embeddingDimension}`,
265
+ );
266
+ }
267
+ return {
268
+ ...chunk,
269
+ embedding,
270
+ };
271
+ });
272
+ }
273
+
274
+ private async embedQuery(query: string) {
275
+ if (!this.options.textEmbeddingModel) {
276
+ throw new Error("AgentKnowledge requires textEmbeddingModel for semantic or hybrid recall");
277
+ }
278
+ const result = await embed({
279
+ model: this.options.textEmbeddingModel as never,
280
+ value: query,
281
+ });
282
+ if (result.embedding.length !== this.options.embeddingDimension) {
283
+ throw new Error(
284
+ `Embedding dimension ${result.embedding.length} does not match configured dimension ${this.options.embeddingDimension}`,
285
+ );
286
+ }
287
+ return result.embedding;
288
+ }
289
+ }
290
+
291
+ export { chunkText } from "./chunking.js";
292
+ export { extractKnowledge, heuristicExtractKnowledge } from "./extraction.js";
293
+ export type {
294
+ ChunkInput,
295
+ EmbeddedChunkInput,
296
+ ExtractedEntity,
297
+ ExtractedKnowledge,
298
+ ExtractedRelationship,
299
+ GraphExpandInput,
300
+ GraphMemoryScore,
301
+ GraphStore,
302
+ GraphSyncJob,
303
+ MemoryCard,
304
+ MemorySource,
305
+ Neo4jConfig,
306
+ SearchType,
307
+ } from "./types.js";