@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 +40 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/knowledge-handlers.d.ts +12 -0
- package/dist/knowledge-handlers.d.ts.map +1 -0
- package/dist/knowledge-handlers.js +140 -0
- package/dist/knowledge-handlers.js.map +1 -0
- package/dist/knowledge-health.d.ts +60 -0
- package/dist/knowledge-health.d.ts.map +1 -0
- package/dist/knowledge-health.js +102 -0
- package/dist/knowledge-health.js.map +1 -0
- package/dist/knowledge-init.d.ts +32 -0
- package/dist/knowledge-init.d.ts.map +1 -0
- package/dist/knowledge-init.js +225 -0
- package/dist/knowledge-init.js.map +1 -0
- package/dist/knowledge-plugin.d.ts +25 -0
- package/dist/knowledge-plugin.d.ts.map +1 -0
- package/dist/knowledge-plugin.js +66 -0
- package/dist/knowledge-plugin.js.map +1 -0
- package/dist/logger.d.ts +4 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +4 -0
- package/dist/logger.js.map +1 -0
- package/dist/mcp-tools.d.ts +12 -0
- package/dist/mcp-tools.d.ts.map +1 -0
- package/dist/mcp-tools.js +342 -0
- package/dist/mcp-tools.js.map +1 -0
- package/dist/proto-converters.d.ts +12 -0
- package/dist/proto-converters.d.ts.map +1 -0
- package/dist/proto-converters.js +35 -0
- package/dist/proto-converters.js.map +1 -0
- package/dist/tsdoc-metadata.json +11 -0
- package/package.json +55 -0
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
|
package/dist/index.d.ts
ADDED
|
@@ -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 @@
|
|
|
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
|