@grackle-ai/plugin-knowledge 0.99.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/README.md ADDED
@@ -0,0 +1,40 @@
1
+ # @grackle-ai/plugin-knowledge
2
+
3
+ Knowledge graph plugin for Grackle. Provides Neo4j-backed semantic search, knowledge node management, and entity sync via the `GracklePlugin` interface.
4
+
5
+ ## Overview
6
+
7
+ Loading this plugin opts the server into knowledge graph functionality. When enabled (`GRACKLE_KNOWLEDGE_ENABLED=true`), the plugin:
8
+
9
+ - Connects to Neo4j and initializes the schema
10
+ - Creates a local embedding model for semantic search
11
+ - Registers gRPC handlers for knowledge operations
12
+ - Syncs task and finding entities to the knowledge graph
13
+ - Exposes `knowledge_search`, `knowledge_get_node`, and `knowledge_create_node` MCP tools
14
+ - Monitors Neo4j health via a reconciliation phase
15
+
16
+ ## Usage
17
+
18
+ ```ts
19
+ import { createKnowledgePlugin } from "@grackle-ai/plugin-knowledge";
20
+
21
+ const plugins = [createCorePlugin()];
22
+ if (config.knowledgeEnabled) {
23
+ plugins.push(createKnowledgePlugin());
24
+ }
25
+ const loaded = await loadPlugins(plugins, ctx);
26
+ ```
27
+
28
+ ## gRPC Handlers
29
+
30
+ - `searchKnowledge` — Semantic similarity search
31
+ - `getKnowledgeNode` — Retrieve a node by ID
32
+ - `expandKnowledgeNode` — Expand a node's neighbors
33
+ - `listRecentKnowledgeNodes` — List recently created nodes
34
+ - `createKnowledgeNode` — Create a native node with embedding
35
+
36
+ ## MCP Tools
37
+
38
+ - `knowledge_search` — Natural language search
39
+ - `knowledge_get_node` — Retrieve a node by ID
40
+ - `knowledge_create_node` — Create a knowledge entry
@@ -0,0 +1,4 @@
1
+ export { createKnowledgePlugin } from "./knowledge-plugin.js";
2
+ export { getKnowledgeReadinessCheck } from "./knowledge-health.js";
3
+ export type { KnowledgeReadinessCheck } from "./knowledge-health.js";
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAC9D,OAAO,EAAE,0BAA0B,EAAE,MAAM,uBAAuB,CAAC;AACnE,YAAY,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { createKnowledgePlugin } from "./knowledge-plugin.js";
2
+ export { getKnowledgeReadinessCheck } from "./knowledge-health.js";
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAC9D,OAAO,EAAE,0BAA0B,EAAE,MAAM,uBAAuB,CAAC"}
@@ -0,0 +1,12 @@
1
+ import { grackle } from "@grackle-ai/common";
2
+ /** Search the knowledge graph using semantic similarity. */
3
+ export declare function searchKnowledge(req: grackle.SearchKnowledgeRequest): Promise<grackle.SearchKnowledgeResponse>;
4
+ /** Get a knowledge node by ID. */
5
+ export declare function getKnowledgeNode(req: grackle.GetKnowledgeNodeRequest): Promise<grackle.GetKnowledgeNodeResponse>;
6
+ /** Expand a knowledge node to retrieve its neighbors. */
7
+ export declare function expandKnowledgeNode(req: grackle.ExpandKnowledgeNodeRequest): Promise<grackle.ExpandKnowledgeNodeResponse>;
8
+ /** List recently created knowledge nodes. */
9
+ export declare function listRecentKnowledgeNodes(req: grackle.ListRecentKnowledgeNodesRequest): Promise<grackle.ListRecentKnowledgeNodesResponse>;
10
+ /** Create a new native knowledge node with embedding. */
11
+ export declare function createKnowledgeNode(req: grackle.CreateKnowledgeNodeRequest): Promise<grackle.CreateKnowledgeNodeResponse>;
12
+ //# sourceMappingURL=knowledge-handlers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"knowledge-handlers.d.ts","sourceRoot":"","sources":["../src/knowledge-handlers.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AA+D7C,4DAA4D;AAC5D,wBAAsB,eAAe,CAAC,GAAG,EAAE,OAAO,CAAC,sBAAsB,GAAG,OAAO,CAAC,OAAO,CAAC,uBAAuB,CAAC,CAqBnH;AAED,kCAAkC;AAClC,wBAAsB,gBAAgB,CAAC,GAAG,EAAE,OAAO,CAAC,uBAAuB,GAAG,OAAO,CAAC,OAAO,CAAC,wBAAwB,CAAC,CAgBtH;AAED,yDAAyD;AACzD,wBAAsB,mBAAmB,CAAC,GAAG,EAAE,OAAO,CAAC,0BAA0B,GAAG,OAAO,CAAC,OAAO,CAAC,2BAA2B,CAAC,CAgB/H;AAED,6CAA6C;AAC7C,wBAAsB,wBAAwB,CAAC,GAAG,EAAE,OAAO,CAAC,+BAA+B,GAAG,OAAO,CAAC,OAAO,CAAC,gCAAgC,CAAC,CAgB9I;AAED,yDAAyD;AACzD,wBAAsB,mBAAmB,CAAC,GAAG,EAAE,OAAO,CAAC,0BAA0B,GAAG,OAAO,CAAC,OAAO,CAAC,2BAA2B,CAAC,CAuB/H"}
@@ -0,0 +1,140 @@
1
+ import { ConnectError, Code } from "@connectrpc/connect";
2
+ import { create } from "@bufbuild/protobuf";
3
+ import { grackle } from "@grackle-ai/common";
4
+ import { knowledgeSearch, getNode as getKnowledgeNodeById, expandNode, createNativeNode, ingest, createPassThroughChunker, listRecentNodes, } from "@grackle-ai/knowledge";
5
+ import { getKnowledgeEmbedder } from "./knowledge-init.js";
6
+ import { isNeo4jHealthy } from "./knowledge-health.js";
7
+ import { knowledgeNodeToProto, knowledgeEdgeToProto } from "./proto-converters.js";
8
+ import { logger } from "./logger.js";
9
+ /** Error message returned when Neo4j is unreachable. */
10
+ const NEO4J_UNAVAILABLE_MESSAGE = "Knowledge graph temporarily unavailable — Neo4j unreachable";
11
+ /**
12
+ * Guard that checks Neo4j health status.
13
+ *
14
+ * @throws ConnectError with Code.Unavailable if Neo4j is unreachable.
15
+ */
16
+ function requireKnowledgeReady() {
17
+ if (!isNeo4jHealthy()) {
18
+ throw new ConnectError(NEO4J_UNAVAILABLE_MESSAGE, Code.Unavailable);
19
+ }
20
+ }
21
+ /**
22
+ * Guard that checks embedder availability and Neo4j health, returning the embedder.
23
+ *
24
+ * @throws ConnectError with Code.Unavailable if knowledge is not ready.
25
+ */
26
+ function requireEmbedder() {
27
+ const embedder = getKnowledgeEmbedder();
28
+ if (!embedder) {
29
+ throw new ConnectError("Knowledge graph not available", Code.Unavailable);
30
+ }
31
+ requireKnowledgeReady();
32
+ return embedder;
33
+ }
34
+ /**
35
+ * Wrap non-ConnectError exceptions as Code.Unavailable.
36
+ *
37
+ * ConnectErrors (e.g. NotFound, InvalidArgument) are re-thrown as-is so
38
+ * the handler's own error semantics are preserved.
39
+ */
40
+ function wrapNeo4jError(err) {
41
+ if (err instanceof ConnectError) {
42
+ throw err;
43
+ }
44
+ // Log the full error server-side for debugging; return a generic message
45
+ // to clients to avoid leaking internal details (hostnames, ports, etc.)
46
+ logger.error({ err }, "Knowledge graph operation failed");
47
+ throw new ConnectError(NEO4J_UNAVAILABLE_MESSAGE, Code.Unavailable);
48
+ }
49
+ /** Search the knowledge graph using semantic similarity. */
50
+ export async function searchKnowledge(req) {
51
+ const embedder = requireEmbedder();
52
+ try {
53
+ const results = await knowledgeSearch(req.query, embedder, {
54
+ limit: req.limit || 10,
55
+ workspaceId: req.workspaceId || undefined,
56
+ });
57
+ return create(grackle.SearchKnowledgeResponseSchema, {
58
+ results: results.map((r) => create(grackle.SearchKnowledgeResultSchema, {
59
+ score: r.score,
60
+ node: knowledgeNodeToProto(r.node),
61
+ edges: r.edges.map(knowledgeEdgeToProto),
62
+ })),
63
+ });
64
+ }
65
+ catch (err) {
66
+ wrapNeo4jError(err);
67
+ }
68
+ }
69
+ /** Get a knowledge node by ID. */
70
+ export async function getKnowledgeNode(req) {
71
+ requireKnowledgeReady();
72
+ try {
73
+ const result = await getKnowledgeNodeById(req.id);
74
+ if (!result) {
75
+ throw new ConnectError(`Knowledge node not found: ${req.id}`, Code.NotFound);
76
+ }
77
+ return create(grackle.GetKnowledgeNodeResponseSchema, {
78
+ node: knowledgeNodeToProto(result.node),
79
+ edges: result.edges.map(knowledgeEdgeToProto),
80
+ });
81
+ }
82
+ catch (err) {
83
+ wrapNeo4jError(err);
84
+ }
85
+ }
86
+ /** Expand a knowledge node to retrieve its neighbors. */
87
+ export async function expandKnowledgeNode(req) {
88
+ requireKnowledgeReady();
89
+ try {
90
+ const result = await expandNode(req.id, {
91
+ depth: req.depth || 1,
92
+ edgeTypes: req.edgeTypes.length > 0 ? req.edgeTypes : undefined,
93
+ });
94
+ return create(grackle.ExpandKnowledgeNodeResponseSchema, {
95
+ nodes: result.nodes.map(knowledgeNodeToProto),
96
+ edges: result.edges.map(knowledgeEdgeToProto),
97
+ });
98
+ }
99
+ catch (err) {
100
+ wrapNeo4jError(err);
101
+ }
102
+ }
103
+ /** List recently created knowledge nodes. */
104
+ export async function listRecentKnowledgeNodes(req) {
105
+ requireKnowledgeReady();
106
+ try {
107
+ const result = await listRecentNodes(req.limit || 20, req.workspaceId || undefined);
108
+ return create(grackle.ListRecentKnowledgeNodesResponseSchema, {
109
+ nodes: result.nodes.map(knowledgeNodeToProto),
110
+ edges: result.edges.map(knowledgeEdgeToProto),
111
+ });
112
+ }
113
+ catch (err) {
114
+ wrapNeo4jError(err);
115
+ }
116
+ }
117
+ /** Create a new native knowledge node with embedding. */
118
+ export async function createKnowledgeNode(req) {
119
+ const embedder = requireEmbedder();
120
+ try {
121
+ const chunker = createPassThroughChunker();
122
+ const embedded = await ingest(req.content, chunker, embedder);
123
+ if (embedded.length === 0) {
124
+ throw new ConnectError("Content produced no embeddings", Code.InvalidArgument);
125
+ }
126
+ const id = await createNativeNode({
127
+ category: (req.category || "insight"),
128
+ title: req.title,
129
+ content: req.content,
130
+ tags: [...req.tags],
131
+ embedding: embedded[0].vector,
132
+ workspaceId: req.workspaceId || "",
133
+ });
134
+ return create(grackle.CreateKnowledgeNodeResponseSchema, { id });
135
+ }
136
+ catch (err) {
137
+ wrapNeo4jError(err);
138
+ }
139
+ }
140
+ //# sourceMappingURL=knowledge-handlers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"knowledge-handlers.js","sourceRoot":"","sources":["../src/knowledge-handlers.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Knowledge graph health monitoring.
3
+ *
4
+ * Provides a {@link ReconciliationPhase} that periodically checks Neo4j
5
+ * connectivity and tracks state transitions. Exposes the current health
6
+ * state for use by gRPC handlers, the event sync circuit breaker, and
7
+ * the `/readyz` endpoint.
8
+ *
9
+ * @module
10
+ */
11
+ import type { ReconciliationPhase } from "@grackle-ai/plugin-sdk";
12
+ /** Dependencies injected into the knowledge health phase for testability. */
13
+ export interface KnowledgeHealthPhaseDeps {
14
+ /** Check Neo4j connectivity. Returns `true` if reachable. */
15
+ healthCheck: () => Promise<boolean>;
16
+ }
17
+ /** Readiness check result compatible with the web-server ReadinessCheck type. */
18
+ export interface KnowledgeReadinessCheck {
19
+ /** Whether Neo4j is reachable. */
20
+ ok: boolean;
21
+ /** Human-readable detail when unhealthy. */
22
+ message?: string;
23
+ }
24
+ /**
25
+ * Create a reconciliation phase that monitors Neo4j health.
26
+ *
27
+ * Calls `healthCheck()` on every tick and updates the module-level state.
28
+ * Logs only on state transitions (healthy to unhealthy or vice versa) to
29
+ * avoid log flooding during sustained outages.
30
+ */
31
+ export declare function createKnowledgeHealthPhase(deps: KnowledgeHealthPhaseDeps): ReconciliationPhase;
32
+ /**
33
+ * Whether Neo4j is currently considered healthy.
34
+ *
35
+ * Returns `true` before the first health check has completed (optimistic
36
+ * default — `initKnowledge()` verifies connectivity at startup).
37
+ */
38
+ export declare function isNeo4jHealthy(): boolean;
39
+ /**
40
+ * Get a readiness check result for the `/readyz` endpoint.
41
+ *
42
+ * Returns `{ ok: true }` when Neo4j is reachable (or before the first check
43
+ * completes, using the optimistic default), or `{ ok: false, message }` when
44
+ * Neo4j has been observed unreachable.
45
+ */
46
+ export declare function getKnowledgeReadinessCheck(): KnowledgeReadinessCheck;
47
+ /**
48
+ * Mark the knowledge subsystem as unhealthy immediately.
49
+ *
50
+ * Called when plugin initialization fails (e.g. Neo4j unreachable at startup)
51
+ * so that `/readyz` returns an accurate status instead of the optimistic default.
52
+ */
53
+ export declare function markKnowledgeInitFailed(): void;
54
+ /**
55
+ * Reset health state for testing.
56
+ *
57
+ * @internal
58
+ */
59
+ export declare function resetKnowledgeHealthState(): void;
60
+ //# sourceMappingURL=knowledge-health.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"knowledge-health.d.ts","sourceRoot":"","sources":["../src/knowledge-health.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAuBlE,6EAA6E;AAC7E,MAAM,WAAW,wBAAwB;IACvC,6DAA6D;IAC7D,WAAW,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;CACrC;AAED,iFAAiF;AACjF,MAAM,WAAW,uBAAuB;IACtC,kCAAkC;IAClC,EAAE,EAAE,OAAO,CAAC;IACZ,4CAA4C;IAC5C,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CACxC,IAAI,EAAE,wBAAwB,GAC7B,mBAAmB,CAwBrB;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,IAAI,OAAO,CAExC;AAED;;;;;;GAMG;AACH,wBAAgB,0BAA0B,IAAI,uBAAuB,CAQpE;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,IAAI,IAAI,CAG9C;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,IAAI,IAAI,CAGhD"}
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Knowledge graph health monitoring.
3
+ *
4
+ * Provides a {@link ReconciliationPhase} that periodically checks Neo4j
5
+ * connectivity and tracks state transitions. Exposes the current health
6
+ * state for use by gRPC handlers, the event sync circuit breaker, and
7
+ * the `/readyz` endpoint.
8
+ *
9
+ * @module
10
+ */
11
+ import { logger } from "./logger.js";
12
+ // ---------------------------------------------------------------------------
13
+ // Module-level state
14
+ // ---------------------------------------------------------------------------
15
+ /**
16
+ * Whether Neo4j was healthy on the last check.
17
+ *
18
+ * Defaults to `true` (optimistic) because `initKnowledge()` verifies
19
+ * connectivity via `openNeo4j()` before subscribing to events. This
20
+ * avoids a startup gap where events would be dropped before the first
21
+ * reconciliation tick (~10s).
22
+ */
23
+ let healthy = true;
24
+ /** Whether at least one health check has completed. */
25
+ let initialized = false;
26
+ /**
27
+ * Create a reconciliation phase that monitors Neo4j health.
28
+ *
29
+ * Calls `healthCheck()` on every tick and updates the module-level state.
30
+ * Logs only on state transitions (healthy to unhealthy or vice versa) to
31
+ * avoid log flooding during sustained outages.
32
+ */
33
+ export function createKnowledgeHealthPhase(deps) {
34
+ return {
35
+ name: "knowledge-health",
36
+ execute: async () => {
37
+ let result;
38
+ try {
39
+ result = await deps.healthCheck();
40
+ }
41
+ catch {
42
+ result = false;
43
+ }
44
+ const previous = healthy;
45
+ const wasInitialized = initialized;
46
+ healthy = result;
47
+ initialized = true;
48
+ // Log on state transitions and on first-check failures
49
+ if (previous && !result) {
50
+ logger.warn("Neo4j became unreachable — knowledge graph operations will be skipped");
51
+ }
52
+ else if (wasInitialized && !previous && result) {
53
+ logger.info("Neo4j recovered — knowledge graph operations resumed");
54
+ }
55
+ },
56
+ };
57
+ }
58
+ /**
59
+ * Whether Neo4j is currently considered healthy.
60
+ *
61
+ * Returns `true` before the first health check has completed (optimistic
62
+ * default — `initKnowledge()` verifies connectivity at startup).
63
+ */
64
+ export function isNeo4jHealthy() {
65
+ return healthy;
66
+ }
67
+ /**
68
+ * Get a readiness check result for the `/readyz` endpoint.
69
+ *
70
+ * Returns `{ ok: true }` when Neo4j is reachable (or before the first check
71
+ * completes, using the optimistic default), or `{ ok: false, message }` when
72
+ * Neo4j has been observed unreachable.
73
+ */
74
+ export function getKnowledgeReadinessCheck() {
75
+ if (!initialized) {
76
+ return { ok: true, message: "Neo4j health check has not run yet" };
77
+ }
78
+ if (!healthy) {
79
+ return { ok: false, message: "Neo4j is unreachable" };
80
+ }
81
+ return { ok: true };
82
+ }
83
+ /**
84
+ * Mark the knowledge subsystem as unhealthy immediately.
85
+ *
86
+ * Called when plugin initialization fails (e.g. Neo4j unreachable at startup)
87
+ * so that `/readyz` returns an accurate status instead of the optimistic default.
88
+ */
89
+ export function markKnowledgeInitFailed() {
90
+ healthy = false;
91
+ initialized = true;
92
+ }
93
+ /**
94
+ * Reset health state for testing.
95
+ *
96
+ * @internal
97
+ */
98
+ export function resetKnowledgeHealthState() {
99
+ healthy = true;
100
+ initialized = false;
101
+ }
102
+ //# sourceMappingURL=knowledge-health.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"knowledge-health.js","sourceRoot":"","sources":["../src/knowledge-health.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAGrC,8EAA8E;AAC9E,qBAAqB;AACrB,8EAA8E;AAE9E;;;;;;;GAOG;AACH,IAAI,OAAO,GAAY,IAAI,CAAC;AAE5B,uDAAuD;AACvD,IAAI,WAAW,GAAY,KAAK,CAAC;AAoBjC;;;;;;GAMG;AACH,MAAM,UAAU,0BAA0B,CACxC,IAA8B;IAE9B,OAAO;QACL,IAAI,EAAE,kBAAkB;QACxB,OAAO,EAAE,KAAK,IAAmB,EAAE;YACjC,IAAI,MAAe,CAAC;YACpB,IAAI,CAAC;gBACH,MAAM,GAAG,MAAM,IAAI,CAAC,WAAW,EAAE,CAAC;YACpC,CAAC;YAAC,MAAM,CAAC;gBACP,MAAM,GAAG,KAAK,CAAC;YACjB,CAAC;YAED,MAAM,QAAQ,GAAY,OAAO,CAAC;YAClC,MAAM,cAAc,GAAY,WAAW,CAAC;YAC5C,OAAO,GAAG,MAAM,CAAC;YACjB,WAAW,GAAG,IAAI,CAAC;YAEnB,uDAAuD;YACvD,IAAI,QAAQ,IAAI,CAAC,MAAM,EAAE,CAAC;gBACxB,MAAM,CAAC,IAAI,CAAC,uEAAuE,CAAC,CAAC;YACvF,CAAC;iBAAM,IAAI,cAAc,IAAI,CAAC,QAAQ,IAAI,MAAM,EAAE,CAAC;gBACjD,MAAM,CAAC,IAAI,CAAC,sDAAsD,CAAC,CAAC;YACtE,CAAC;QACH,CAAC;KACF,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,cAAc;IAC5B,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,0BAA0B;IACxC,IAAI,CAAC,WAAW,EAAE,CAAC;QACjB,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,oCAAoC,EAAE,CAAC;IACrE,CAAC;IACD,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,sBAAsB,EAAE,CAAC;IACxD,CAAC;IACD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;AACtB,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,uBAAuB;IACrC,OAAO,GAAG,KAAK,CAAC;IAChB,WAAW,GAAG,IAAI,CAAC;AACrB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,yBAAyB;IACvC,OAAO,GAAG,IAAI,CAAC;IACf,WAAW,GAAG,KAAK,CAAC;AACtB,CAAC"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Knowledge graph subsystem initialization and lifecycle management.
3
+ *
4
+ * Wires Neo4j, the local embedder, and event-driven reference node sync
5
+ * into the Grackle server. Opt-in by loading the plugin.
6
+ *
7
+ * @module
8
+ */
9
+ import { healthCheck as neo4jHealthCheck, type Embedder } from "@grackle-ai/knowledge";
10
+ import type { PluginContext, Disposable } from "@grackle-ai/plugin-sdk";
11
+ /** Re-export Neo4j health check for use by the reconciliation health phase. */
12
+ export { neo4jHealthCheck };
13
+ /** Get the knowledge embedder. Returns undefined if knowledge is not initialized. */
14
+ export declare function getKnowledgeEmbedder(): Embedder | undefined;
15
+ /**
16
+ * Initialize the knowledge graph subsystem.
17
+ *
18
+ * Opens Neo4j, initializes the schema, and creates the local embedder.
19
+ * If any step after Neo4j connection fails, cleans up the connection
20
+ * before re-throwing.
21
+ *
22
+ * @returns A cleanup function that closes Neo4j.
23
+ */
24
+ export declare function initKnowledge(ctx: PluginContext): Promise<() => Promise<void>>;
25
+ /**
26
+ * Create an entity sync subscriber that listens for task/finding events
27
+ * and keeps the knowledge graph in sync.
28
+ *
29
+ * @returns A Disposable that unsubscribes when disposed.
30
+ */
31
+ export declare function createEntitySyncSubscriber(ctx: PluginContext): Disposable;
32
+ //# sourceMappingURL=knowledge-init.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"knowledge-init.d.ts","sourceRoot":"","sources":["../src/knowledge-init.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAIL,WAAW,IAAI,gBAAgB,EAS/B,KAAK,QAAQ,EACd,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EAAgB,aAAa,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAKtF,+EAA+E;AAC/E,OAAO,EAAE,gBAAgB,EAAE,CAAC;AAK5B,qFAAqF;AACrF,wBAAgB,oBAAoB,IAAI,QAAQ,GAAG,SAAS,CAE3D;AAsLD;;;;;;;;GAQG;AACH,wBAAsB,aAAa,CAAC,GAAG,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,CA0BpF;AAED;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,GAAG,EAAE,aAAa,GAAG,UAAU,CAazE"}
@@ -0,0 +1,225 @@
1
+ /**
2
+ * Knowledge graph subsystem initialization and lifecycle management.
3
+ *
4
+ * Wires Neo4j, the local embedder, and event-driven reference node sync
5
+ * into the Grackle server. Opt-in by loading the plugin.
6
+ *
7
+ * @module
8
+ */
9
+ import { openNeo4j, initSchema, closeNeo4j, healthCheck as neo4jHealthCheck, createLocalEmbedder, syncReferenceNode, deleteReferenceNodeBySource, findReferenceNodeBySource, createEdge, deriveTaskText, deriveFindingText, EDGE_TYPE, } from "@grackle-ai/knowledge";
10
+ import { taskStore, findingStore, safeParseJsonArray } from "@grackle-ai/database";
11
+ import { logger } from "./logger.js";
12
+ import { isNeo4jHealthy } from "./knowledge-health.js";
13
+ /** Re-export Neo4j health check for use by the reconciliation health phase. */
14
+ export { neo4jHealthCheck };
15
+ /** Module-level embedder, available after initKnowledge() completes. */
16
+ let knowledgeEmbedder;
17
+ /** Get the knowledge embedder. Returns undefined if knowledge is not initialized. */
18
+ export function getKnowledgeEmbedder() {
19
+ return knowledgeEmbedder;
20
+ }
21
+ // ---------------------------------------------------------------------------
22
+ // Event handler
23
+ // ---------------------------------------------------------------------------
24
+ /**
25
+ * Create an event bus subscriber that syncs reference nodes.
26
+ *
27
+ * Sync is fire-and-forget — errors are logged but never propagated.
28
+ */
29
+ function createEntitySyncHandler(embedder) {
30
+ return (event) => {
31
+ // Fire-and-forget async — errors handled inside handleEvent
32
+ handleEvent(embedder, event).catch(() => { });
33
+ };
34
+ }
35
+ /**
36
+ * Ensure a task's reference node exists in the knowledge graph.
37
+ *
38
+ * If the node already exists, returns its ID. Otherwise syncs it from
39
+ * the task store and returns the new ID. Returns `undefined` if the
40
+ * task is not found in the store.
41
+ */
42
+ async function ensureTaskReferenceNode(embedder, taskId) {
43
+ const existing = await findReferenceNodeBySource("task", taskId);
44
+ if (existing) {
45
+ return existing.id;
46
+ }
47
+ const task = taskStore.getTask(taskId);
48
+ if (!task) {
49
+ return undefined;
50
+ }
51
+ return syncReferenceNode(embedder, {
52
+ sourceType: "task",
53
+ sourceId: taskId,
54
+ label: task.title,
55
+ text: deriveTaskText(task.title, task.description),
56
+ workspaceId: task.workspaceId ?? "",
57
+ });
58
+ }
59
+ /**
60
+ * Create edges from a task reference node to its parent and dependencies.
61
+ *
62
+ * Best-effort — edge creation failures are logged but don't block sync.
63
+ */
64
+ async function syncTaskEdges(embedder, taskNodeId, task) {
65
+ // Parent task → PART_OF edge
66
+ if (task.parentTaskId) {
67
+ try {
68
+ const parentNodeId = await ensureTaskReferenceNode(embedder, task.parentTaskId);
69
+ if (parentNodeId) {
70
+ await createEdge(taskNodeId, parentNodeId, EDGE_TYPE.PART_OF);
71
+ }
72
+ }
73
+ catch (err) {
74
+ logger.warn({ taskNodeId, parentTaskId: task.parentTaskId, err }, "Failed to create PART_OF edge");
75
+ }
76
+ }
77
+ // Dependencies → DEPENDS_ON edges
78
+ const deps = safeParseJsonArray(task.dependsOn);
79
+ for (const depId of deps) {
80
+ try {
81
+ const depNodeId = await ensureTaskReferenceNode(embedder, depId);
82
+ if (depNodeId) {
83
+ await createEdge(taskNodeId, depNodeId, EDGE_TYPE.DEPENDS_ON);
84
+ }
85
+ }
86
+ catch (err) {
87
+ logger.warn({ taskNodeId, depId, err }, "Failed to create DEPENDS_ON edge");
88
+ }
89
+ }
90
+ }
91
+ /** Handle a single entity event. */
92
+ async function handleEvent(embedder, event) {
93
+ // Circuit breaker: skip sync when Neo4j is known-unreachable
94
+ if (!isNeo4jHealthy()) {
95
+ logger.debug({ eventType: event.type }, "Knowledge sync skipped — Neo4j unhealthy");
96
+ return;
97
+ }
98
+ try {
99
+ const payload = event.payload;
100
+ switch (event.type) {
101
+ case "task.created":
102
+ case "task.updated": {
103
+ const taskId = payload.taskId;
104
+ if (typeof taskId !== "string" || !taskId) {
105
+ return;
106
+ }
107
+ const task = taskStore.getTask(taskId);
108
+ if (!task) {
109
+ logger.warn({ taskId }, "Knowledge sync: task not found, skipping");
110
+ return;
111
+ }
112
+ const taskNodeId = await syncReferenceNode(embedder, {
113
+ sourceType: "task",
114
+ sourceId: taskId,
115
+ label: task.title,
116
+ text: deriveTaskText(task.title, task.description),
117
+ workspaceId: task.workspaceId ?? "",
118
+ });
119
+ await syncTaskEdges(embedder, taskNodeId, task);
120
+ break;
121
+ }
122
+ case "task.deleted": {
123
+ const taskId = payload.taskId;
124
+ if (typeof taskId !== "string" || !taskId) {
125
+ return;
126
+ }
127
+ await deleteReferenceNodeBySource("task", taskId);
128
+ break;
129
+ }
130
+ case "finding.posted": {
131
+ const findingId = payload.findingId;
132
+ if (typeof findingId !== "string" || !findingId) {
133
+ return;
134
+ }
135
+ const workspaceId = typeof payload.workspaceId === "string" ? payload.workspaceId : "";
136
+ const findings = findingStore.queryFindings(workspaceId);
137
+ const finding = findings.find((f) => f.id === findingId);
138
+ if (!finding) {
139
+ logger.warn({ findingId }, "Knowledge sync: finding not found, skipping");
140
+ return;
141
+ }
142
+ const tags = safeParseJsonArray(typeof finding.tags === "string" ? finding.tags : null);
143
+ const findingNodeId = await syncReferenceNode(embedder, {
144
+ sourceType: "finding",
145
+ sourceId: findingId,
146
+ label: finding.title,
147
+ text: deriveFindingText(finding.title, finding.content, tags),
148
+ workspaceId,
149
+ });
150
+ // Link finding to its task
151
+ if (finding.taskId) {
152
+ try {
153
+ const taskNodeId = await ensureTaskReferenceNode(embedder, finding.taskId);
154
+ if (taskNodeId) {
155
+ await createEdge(findingNodeId, taskNodeId, EDGE_TYPE.DERIVED_FROM);
156
+ }
157
+ }
158
+ catch (err) {
159
+ logger.warn({ findingNodeId, taskId: finding.taskId, err }, "Failed to create DERIVED_FROM edge");
160
+ }
161
+ }
162
+ break;
163
+ }
164
+ default:
165
+ // Ignore events we don't handle
166
+ break;
167
+ }
168
+ }
169
+ catch (err) {
170
+ logger.error({ err, eventType: event.type, eventId: event.id }, "Knowledge sync failed for entity event");
171
+ }
172
+ }
173
+ // ---------------------------------------------------------------------------
174
+ // Lifecycle
175
+ // ---------------------------------------------------------------------------
176
+ /**
177
+ * Initialize the knowledge graph subsystem.
178
+ *
179
+ * Opens Neo4j, initializes the schema, and creates the local embedder.
180
+ * If any step after Neo4j connection fails, cleans up the connection
181
+ * before re-throwing.
182
+ *
183
+ * @returns A cleanup function that closes Neo4j.
184
+ */
185
+ export async function initKnowledge(ctx) {
186
+ ctx.logger.info("Initializing knowledge graph subsystem");
187
+ const embedder = createLocalEmbedder();
188
+ await openNeo4j();
189
+ try {
190
+ await initSchema(embedder.dimensions);
191
+ knowledgeEmbedder = embedder;
192
+ ctx.logger.info("Knowledge graph subsystem ready");
193
+ return async () => {
194
+ ctx.logger.info("Shutting down knowledge graph subsystem");
195
+ knowledgeEmbedder = undefined;
196
+ await closeNeo4j();
197
+ ctx.logger.info("Knowledge graph subsystem stopped");
198
+ };
199
+ }
200
+ catch (err) {
201
+ // Clean up Neo4j if a later step fails
202
+ knowledgeEmbedder = undefined;
203
+ await closeNeo4j().catch(() => { });
204
+ throw err;
205
+ }
206
+ }
207
+ /**
208
+ * Create an entity sync subscriber that listens for task/finding events
209
+ * and keeps the knowledge graph in sync.
210
+ *
211
+ * @returns A Disposable that unsubscribes when disposed.
212
+ */
213
+ export function createEntitySyncSubscriber(ctx) {
214
+ const embedder = knowledgeEmbedder;
215
+ if (!embedder) {
216
+ return { dispose: () => { } };
217
+ }
218
+ const unsubscribe = ctx.subscribe(createEntitySyncHandler(embedder));
219
+ return {
220
+ dispose: () => {
221
+ unsubscribe();
222
+ },
223
+ };
224
+ }
225
+ //# sourceMappingURL=knowledge-init.js.map