@lacneu/openclaw-knowledge 3.1.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/CHANGELOG.md +159 -0
- package/LICENSE +21 -0
- package/README.md +424 -0
- package/dist/config.d.ts +14 -0
- package/dist/config.js +51 -0
- package/dist/config.js.map +1 -0
- package/dist/embeddings.d.ts +9 -0
- package/dist/embeddings.js +33 -0
- package/dist/embeddings.js.map +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +239 -0
- package/dist/index.js.map +1 -0
- package/dist/lightrag.d.ts +28 -0
- package/dist/lightrag.js +69 -0
- package/dist/lightrag.js.map +1 -0
- package/dist/pgvector.d.ts +21 -0
- package/dist/pgvector.js +81 -0
- package/dist/pgvector.js.map +1 -0
- package/dist/types.d.ts +107 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/openclaw.plugin.json +149 -0
- package/package.json +70 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Gemini embedding client for query vectors.
|
|
2
|
+
//
|
|
3
|
+
// We deliberately hit the *native* `embedContent` endpoint rather than the
|
|
4
|
+
// OpenAI-compatible one, because the OpenAI endpoint does not support
|
|
5
|
+
// multimodal inputs and we want to stay in the same embedding space as the
|
|
6
|
+
// n8n ingestion pipeline (which uses the native endpoint with images/audio).
|
|
7
|
+
const GEMINI_EMBED_URL = "https://generativelanguage.googleapis.com/v1beta/" +
|
|
8
|
+
"models/gemini-embedding-2-preview:embedContent";
|
|
9
|
+
/**
|
|
10
|
+
* Embed a text query via Gemini Embedding 2 Preview.
|
|
11
|
+
* Uses the same model as n8n document ingestion so that query vectors and
|
|
12
|
+
* stored chunks live in the same 3072-dimensional space.
|
|
13
|
+
*
|
|
14
|
+
* @throws Error on any non-OK HTTP response, with the first 200 chars of the
|
|
15
|
+
* error body for debugging.
|
|
16
|
+
*/
|
|
17
|
+
export async function embedQuery(text, geminiApiKey) {
|
|
18
|
+
const url = `${GEMINI_EMBED_URL}?key=${geminiApiKey}`;
|
|
19
|
+
const resp = await fetch(url, {
|
|
20
|
+
method: "POST",
|
|
21
|
+
headers: { "Content-Type": "application/json" },
|
|
22
|
+
body: JSON.stringify({
|
|
23
|
+
content: { parts: [{ text }] },
|
|
24
|
+
}),
|
|
25
|
+
});
|
|
26
|
+
if (!resp.ok) {
|
|
27
|
+
const body = await resp.text();
|
|
28
|
+
throw new Error(`Gemini embedding failed (${resp.status}): ${body.slice(0, 200)}`);
|
|
29
|
+
}
|
|
30
|
+
const data = (await resp.json());
|
|
31
|
+
return data.embedding.values;
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=embeddings.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"embeddings.js","sourceRoot":"","sources":["../src/embeddings.ts"],"names":[],"mappings":"AAAA,6CAA6C;AAC7C,EAAE;AACF,2EAA2E;AAC3E,sEAAsE;AACtE,2EAA2E;AAC3E,6EAA6E;AAE7E,MAAM,gBAAgB,GACpB,mDAAmD;IACnD,gDAAgD,CAAC;AAMnD;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,IAAY,EACZ,YAAoB;IAEpB,MAAM,GAAG,GAAG,GAAG,gBAAgB,QAAQ,YAAY,EAAE,CAAC;IAEtD,MAAM,IAAI,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;QAC5B,MAAM,EAAE,MAAM;QACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;QAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;YACnB,OAAO,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE;SAC/B,CAAC;KACH,CAAC,CAAC;IAEH,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC;QACb,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;QAC/B,MAAM,IAAI,KAAK,CACb,4BAA4B,IAAI,CAAC,MAAM,MAAM,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAClE,CAAC;IACJ,CAAC;IAED,MAAM,IAAI,GAAG,CAAC,MAAM,IAAI,CAAC,IAAI,EAAE,CAA4B,CAAC;IAC5D,OAAO,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC;AAC/B,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { OpenClawPluginApi, PluginLogger } from "openclaw/plugin-sdk/plugin-entry";
|
|
2
|
+
import type { BeforePromptBuildEvent, BeforePromptBuildResult, PgPoolLike, ResolvedKnowledgeConfig } from "./types.js";
|
|
3
|
+
export { resolveEnv, resolveConfig } from "./config.js";
|
|
4
|
+
export { embedQuery } from "./embeddings.js";
|
|
5
|
+
export { searchCollection, formatPgvectorResults } from "./pgvector.js";
|
|
6
|
+
export { queryLightRAG, truncateLightRAG, formatLightRAGResults } from "./lightrag.js";
|
|
7
|
+
export type { BeforePromptBuildEvent, BeforePromptBuildResult, KnowledgePluginConfig, LightRAGQueryMode, PgPoolLike, PgvectorResult, PgvectorRow, PromptContentPart, PromptMessage, ResolvedKnowledgeConfig, } from "./types.js";
|
|
8
|
+
interface HookHandlerDeps {
|
|
9
|
+
config: ResolvedKnowledgeConfig;
|
|
10
|
+
pool: PgPoolLike | null;
|
|
11
|
+
logger: PluginLogger;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Build the `before_prompt_build` handler bound to a specific plugin state.
|
|
15
|
+
* Kept as a pure factory so the handler can be unit-tested with fake deps.
|
|
16
|
+
*/
|
|
17
|
+
export declare function createBeforePromptBuildHandler(deps: HookHandlerDeps): (event: BeforePromptBuildEvent) => Promise<BeforePromptBuildResult | undefined>;
|
|
18
|
+
/**
|
|
19
|
+
* Register the plugin against a minimal shape-compatible subset of the
|
|
20
|
+
* OpenClaw plugin API. Returns nothing; side effects are setting a hook and
|
|
21
|
+
* logging the initial status.
|
|
22
|
+
*/
|
|
23
|
+
export declare function registerKnowledgePlugin(api: OpenClawPluginApi): void;
|
|
24
|
+
declare const _default: {
|
|
25
|
+
id: string;
|
|
26
|
+
name: string;
|
|
27
|
+
description: string;
|
|
28
|
+
configSchema: import("openclaw/plugin-sdk/plugin-entry").OpenClawPluginConfigSchema;
|
|
29
|
+
register: NonNullable<import("openclaw/plugin-sdk/plugin-entry").OpenClawPluginDefinition["register"]>;
|
|
30
|
+
} & Pick<import("openclaw/plugin-sdk/plugin-entry").OpenClawPluginDefinition, "kind" | "reload" | "nodeHostCommands" | "securityAuditCollectors">;
|
|
31
|
+
export default _default;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
// openclaw-knowledge — Multi-source knowledge plugin for OpenClaw
|
|
2
|
+
//
|
|
3
|
+
// Queries two knowledge sources in parallel and injects relevant context
|
|
4
|
+
// into the agent's system prompt via `appendSystemContext`:
|
|
5
|
+
// 1. PostgreSQL pgvector — semantic vector search on document embeddings
|
|
6
|
+
// 2. LightRAG — knowledge graph with entity/relation multi-hop search
|
|
7
|
+
//
|
|
8
|
+
// Hook: before_prompt_build (requires OpenClaw >= v2026.3.7)
|
|
9
|
+
// Depends on: pg (node-postgres)
|
|
10
|
+
//
|
|
11
|
+
// This is the canonical entry point for the plugin. Helpers live in sibling
|
|
12
|
+
// modules (`config.ts`, `embeddings.ts`, `pgvector.ts`, `lightrag.ts`) so the
|
|
13
|
+
// business logic can be unit-tested without instantiating the full SDK.
|
|
14
|
+
import pg from "pg";
|
|
15
|
+
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
|
16
|
+
import { resolveConfig } from "./config.js";
|
|
17
|
+
import { embedQuery } from "./embeddings.js";
|
|
18
|
+
import { searchCollection, formatPgvectorResults } from "./pgvector.js";
|
|
19
|
+
import { queryLightRAG, formatLightRAGResults } from "./lightrag.js";
|
|
20
|
+
// Re-export helpers so the test suite can import them directly without
|
|
21
|
+
// duplicating imports from every submodule.
|
|
22
|
+
export { resolveEnv, resolveConfig } from "./config.js";
|
|
23
|
+
export { embedQuery } from "./embeddings.js";
|
|
24
|
+
export { searchCollection, formatPgvectorResults } from "./pgvector.js";
|
|
25
|
+
export { queryLightRAG, truncateLightRAG, formatLightRAGResults } from "./lightrag.js";
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Hook handler factory
|
|
28
|
+
//
|
|
29
|
+
// Extracted from `register` so tests can exercise the handler directly
|
|
30
|
+
// without mocking the full plugin API surface.
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
const MAX_CONSECUTIVE_ERRORS = 3;
|
|
33
|
+
const COOLDOWN_MS = 5 * 60 * 1000;
|
|
34
|
+
const MIN_QUERY_LENGTH = 3;
|
|
35
|
+
/**
|
|
36
|
+
* Build the `before_prompt_build` handler bound to a specific plugin state.
|
|
37
|
+
* Kept as a pure factory so the handler can be unit-tested with fake deps.
|
|
38
|
+
*/
|
|
39
|
+
export function createBeforePromptBuildHandler(deps) {
|
|
40
|
+
const { config, pool, logger } = deps;
|
|
41
|
+
// Per-instance state: consecutive failure counter and cooldown deadline.
|
|
42
|
+
// Closed-over so two registrations of the hook never share state.
|
|
43
|
+
let consecutiveErrors = 0;
|
|
44
|
+
let cooldownUntil = 0;
|
|
45
|
+
return async function beforePromptBuild(event) {
|
|
46
|
+
if (!config.enabled)
|
|
47
|
+
return undefined;
|
|
48
|
+
// Cooldown after repeated failures: skip silently until the deadline
|
|
49
|
+
// passes, then reset the counter and resume normal operation.
|
|
50
|
+
if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
|
|
51
|
+
if (Date.now() < cooldownUntil)
|
|
52
|
+
return undefined;
|
|
53
|
+
consecutiveErrors = 0;
|
|
54
|
+
logger.info("openclaw-knowledge: resuming after cooldown");
|
|
55
|
+
}
|
|
56
|
+
const query = extractQueryFromMessages(event.messages);
|
|
57
|
+
if (!query || query.trim().length < MIN_QUERY_LENGTH)
|
|
58
|
+
return undefined;
|
|
59
|
+
try {
|
|
60
|
+
const tasks = [];
|
|
61
|
+
if (config.pgvectorEnabled && pool) {
|
|
62
|
+
tasks.push(runPgvectorSource(pool, query, config));
|
|
63
|
+
}
|
|
64
|
+
if (config.lightragEnabled) {
|
|
65
|
+
tasks.push(runLightRAGSource(query, config));
|
|
66
|
+
}
|
|
67
|
+
const settled = await Promise.allSettled(tasks);
|
|
68
|
+
const sections = [];
|
|
69
|
+
let failedSources = 0;
|
|
70
|
+
for (const result of settled) {
|
|
71
|
+
if (result.status === "rejected") {
|
|
72
|
+
failedSources++;
|
|
73
|
+
const reason = result.reason;
|
|
74
|
+
logger.error(`openclaw-knowledge: source failed — ${reason?.message ?? String(result.reason)}`);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const section = renderSection(result.value, config, logger);
|
|
78
|
+
if (section)
|
|
79
|
+
sections.push(section);
|
|
80
|
+
}
|
|
81
|
+
// If every source we launched failed, treat the turn as a failure for
|
|
82
|
+
// cooldown tracking. A partial failure is fine — the other source's
|
|
83
|
+
// context is better than nothing.
|
|
84
|
+
if (failedSources > 0 && failedSources === tasks.length) {
|
|
85
|
+
consecutiveErrors++;
|
|
86
|
+
if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
|
|
87
|
+
cooldownUntil = Date.now() + COOLDOWN_MS;
|
|
88
|
+
logger.error(`openclaw-knowledge: ${consecutiveErrors} consecutive errors — cooling down 5 min`);
|
|
89
|
+
}
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
consecutiveErrors = 0;
|
|
93
|
+
if (sections.length === 0)
|
|
94
|
+
return undefined;
|
|
95
|
+
return {
|
|
96
|
+
appendSystemContext: [
|
|
97
|
+
"",
|
|
98
|
+
"## Relevant Knowledge Base",
|
|
99
|
+
"Use this information to answer the user's question accurately.",
|
|
100
|
+
"Always cite the source document name when using this information.",
|
|
101
|
+
"",
|
|
102
|
+
...sections,
|
|
103
|
+
].join("\n"),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
// Catch-all: an unexpected crash must never propagate to the agent.
|
|
108
|
+
consecutiveErrors++;
|
|
109
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
110
|
+
if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
|
|
111
|
+
cooldownUntil = Date.now() + COOLDOWN_MS;
|
|
112
|
+
logger.error(`openclaw-knowledge: ${consecutiveErrors} consecutive errors — cooling down 5 min: ${message}`);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
logger.error(`openclaw-knowledge: ${message}`);
|
|
116
|
+
}
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Extract the most recent user message text. OpenClaw surfaces two content
|
|
123
|
+
* shapes: a plain string, or an array of typed content parts (multi-modal).
|
|
124
|
+
*/
|
|
125
|
+
function extractQueryFromMessages(messages) {
|
|
126
|
+
if (!Array.isArray(messages) || messages.length === 0)
|
|
127
|
+
return "";
|
|
128
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
129
|
+
const msg = messages[i];
|
|
130
|
+
if (!msg || msg.role !== "user")
|
|
131
|
+
continue;
|
|
132
|
+
if (typeof msg.content === "string") {
|
|
133
|
+
return msg.content;
|
|
134
|
+
}
|
|
135
|
+
if (Array.isArray(msg.content)) {
|
|
136
|
+
return msg.content
|
|
137
|
+
.filter((p) => p.type === "text" && typeof p.text === "string")
|
|
138
|
+
.map((p) => p.text)
|
|
139
|
+
.join(" ");
|
|
140
|
+
}
|
|
141
|
+
return "";
|
|
142
|
+
}
|
|
143
|
+
return "";
|
|
144
|
+
}
|
|
145
|
+
async function runPgvectorSource(pool, query, config) {
|
|
146
|
+
const vector = await embedQuery(query, config.geminiApiKey);
|
|
147
|
+
const searches = config.collections.map((col) => searchCollection(pool, col, vector, config.topK, config.scoreThreshold));
|
|
148
|
+
const allResults = (await Promise.all(searches)).flat();
|
|
149
|
+
allResults.sort((a, b) => b.score - a.score);
|
|
150
|
+
return { source: "pgvector", data: allResults };
|
|
151
|
+
}
|
|
152
|
+
async function runLightRAGSource(query, config) {
|
|
153
|
+
const context = await queryLightRAG(config.lightragUrl, config.lightragApiKey, query, config.lightragQueryMode);
|
|
154
|
+
return { source: "lightrag", data: context };
|
|
155
|
+
}
|
|
156
|
+
function renderSection(result, config, logger) {
|
|
157
|
+
if (result.source === "pgvector") {
|
|
158
|
+
const formatted = formatPgvectorResults(result.data, config.maxInjectChars);
|
|
159
|
+
if (!formatted)
|
|
160
|
+
return null;
|
|
161
|
+
const topScore = result.data[0]?.score?.toFixed(2) ?? "n/a";
|
|
162
|
+
logger.info(`openclaw-knowledge: pgvector — ${result.data.length} result(s) (top: ${topScore})`);
|
|
163
|
+
return "### Document Search Results (pgvector)\n" + formatted;
|
|
164
|
+
}
|
|
165
|
+
if (result.source === "lightrag") {
|
|
166
|
+
const formatted = formatLightRAGResults(result.data, config.lightragMaxChars);
|
|
167
|
+
if (!formatted)
|
|
168
|
+
return null;
|
|
169
|
+
logger.info(`openclaw-knowledge: LightRAG — ${formatted.truncated.length}/${formatted.originalLength} chars (truncated from ${formatted.originalLength})`);
|
|
170
|
+
return "### Knowledge Graph Context (LightRAG)\n" + formatted.truncated;
|
|
171
|
+
}
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
// Plugin registration helper
|
|
176
|
+
//
|
|
177
|
+
// Exposed so tests can exercise the full wiring (including api.on) without
|
|
178
|
+
// going through `definePluginEntry`, which is tied to the SDK runtime.
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
/**
|
|
181
|
+
* Register the plugin against a minimal shape-compatible subset of the
|
|
182
|
+
* OpenClaw plugin API. Returns nothing; side effects are setting a hook and
|
|
183
|
+
* logging the initial status.
|
|
184
|
+
*/
|
|
185
|
+
export function registerKnowledgePlugin(api) {
|
|
186
|
+
const rawConfig = (api.pluginConfig ?? {});
|
|
187
|
+
const config = resolveConfig(rawConfig);
|
|
188
|
+
if (!config.pgvectorEnabled && !config.lightragEnabled) {
|
|
189
|
+
api.logger.warn("openclaw-knowledge: neither pgvector nor LightRAG configured — plugin disabled");
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
// Only instantiate the pg pool when pgvector is actually in play. Booting
|
|
193
|
+
// a pool with no valid connection string would keep the plugin disabled
|
|
194
|
+
// anyway and leak sockets on hot-reload.
|
|
195
|
+
let pool = null;
|
|
196
|
+
if (config.pgvectorEnabled) {
|
|
197
|
+
const realPool = new pg.Pool({
|
|
198
|
+
connectionString: config.postgresUrl,
|
|
199
|
+
max: 3,
|
|
200
|
+
idleTimeoutMillis: 30000,
|
|
201
|
+
});
|
|
202
|
+
realPool.on("error", (err) => {
|
|
203
|
+
api.logger.error(`openclaw-knowledge: pool error — ${err.message}`);
|
|
204
|
+
});
|
|
205
|
+
pool = realPool;
|
|
206
|
+
}
|
|
207
|
+
const sources = [];
|
|
208
|
+
if (config.pgvectorEnabled) {
|
|
209
|
+
sources.push(`pgvector (${config.collections.join(", ")})`);
|
|
210
|
+
}
|
|
211
|
+
if (config.lightragEnabled) {
|
|
212
|
+
sources.push(`LightRAG (${config.lightragQueryMode})`);
|
|
213
|
+
}
|
|
214
|
+
api.logger.info(`openclaw-knowledge: ready — sources: ${sources.join(" + ")}`);
|
|
215
|
+
const handler = createBeforePromptBuildHandler({
|
|
216
|
+
config,
|
|
217
|
+
pool,
|
|
218
|
+
logger: api.logger,
|
|
219
|
+
});
|
|
220
|
+
// The SDK's `api.on<K>` signature is strongly typed per hook name, so we
|
|
221
|
+
// use a cast here to bridge our structural handler type with the precise
|
|
222
|
+
// `PluginHookHandlerMap["before_prompt_build"]` expected signature.
|
|
223
|
+
// The handler itself is fully type-safe on its own contract (see
|
|
224
|
+
// {@link createBeforePromptBuildHandler}).
|
|
225
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
226
|
+
api.on("before_prompt_build", handler);
|
|
227
|
+
}
|
|
228
|
+
// ---------------------------------------------------------------------------
|
|
229
|
+
// Canonical plugin entry
|
|
230
|
+
// ---------------------------------------------------------------------------
|
|
231
|
+
export default definePluginEntry({
|
|
232
|
+
id: "openclaw-knowledge",
|
|
233
|
+
name: "Knowledge Base",
|
|
234
|
+
description: "Multi-source knowledge search for OpenClaw (pgvector + LightRAG)",
|
|
235
|
+
register(api) {
|
|
236
|
+
registerKnowledgePlugin(api);
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { LightRAGQueryMode } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Query a LightRAG server for context relevant to `query`.
|
|
4
|
+
*
|
|
5
|
+
* Modes:
|
|
6
|
+
* - `naive` — simple vector similarity on chunks
|
|
7
|
+
* - `local` — entity-neighbourhood traversal
|
|
8
|
+
* - `global` — community summaries
|
|
9
|
+
* - `hybrid` — local + global (recommended default)
|
|
10
|
+
*
|
|
11
|
+
* @throws Error on any non-OK HTTP response, with the first 200 chars of the
|
|
12
|
+
* error body for debugging.
|
|
13
|
+
*/
|
|
14
|
+
export declare function queryLightRAG(url: string, apiKey: string, query: string, mode?: LightRAGQueryMode): Promise<string>;
|
|
15
|
+
/**
|
|
16
|
+
* Truncate text to `maxChars` without cutting mid-sentence when possible.
|
|
17
|
+
* Falls back to a raw character cut if no sentence boundary is found in the
|
|
18
|
+
* second half of the allowed window.
|
|
19
|
+
*/
|
|
20
|
+
export declare function truncateLightRAG(text: string, maxChars: number): string;
|
|
21
|
+
/**
|
|
22
|
+
* Convenience wrapper used by the hook handler: trim the context, truncate
|
|
23
|
+
* it to the configured budget, and return `null` if nothing remains.
|
|
24
|
+
*/
|
|
25
|
+
export declare function formatLightRAGResults(rawContext: string, maxChars: number): {
|
|
26
|
+
truncated: string;
|
|
27
|
+
originalLength: number;
|
|
28
|
+
} | null;
|
package/dist/lightrag.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// LightRAG query client.
|
|
2
|
+
//
|
|
3
|
+
// LightRAG is a knowledge graph server built on Neo4j + a vector store. We
|
|
4
|
+
// call its `/query` endpoint with `only_need_context=true` so it returns the
|
|
5
|
+
// assembled context text WITHOUT running its own LLM synthesis — we only need
|
|
6
|
+
// the raw context to feed back into OpenClaw's agent.
|
|
7
|
+
/**
|
|
8
|
+
* Query a LightRAG server for context relevant to `query`.
|
|
9
|
+
*
|
|
10
|
+
* Modes:
|
|
11
|
+
* - `naive` — simple vector similarity on chunks
|
|
12
|
+
* - `local` — entity-neighbourhood traversal
|
|
13
|
+
* - `global` — community summaries
|
|
14
|
+
* - `hybrid` — local + global (recommended default)
|
|
15
|
+
*
|
|
16
|
+
* @throws Error on any non-OK HTTP response, with the first 200 chars of the
|
|
17
|
+
* error body for debugging.
|
|
18
|
+
*/
|
|
19
|
+
export async function queryLightRAG(url, apiKey, query, mode = "hybrid") {
|
|
20
|
+
const headers = {
|
|
21
|
+
"Content-Type": "application/json",
|
|
22
|
+
};
|
|
23
|
+
if (apiKey)
|
|
24
|
+
headers["X-API-Key"] = apiKey;
|
|
25
|
+
const resp = await fetch(`${url}/query`, {
|
|
26
|
+
method: "POST",
|
|
27
|
+
headers,
|
|
28
|
+
body: JSON.stringify({
|
|
29
|
+
query,
|
|
30
|
+
mode,
|
|
31
|
+
only_need_context: true,
|
|
32
|
+
stream: false,
|
|
33
|
+
}),
|
|
34
|
+
});
|
|
35
|
+
if (!resp.ok) {
|
|
36
|
+
const body = await resp.text();
|
|
37
|
+
throw new Error(`LightRAG query failed (${resp.status}): ${body.slice(0, 200)}`);
|
|
38
|
+
}
|
|
39
|
+
const data = (await resp.json());
|
|
40
|
+
return data.response ?? "";
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Truncate text to `maxChars` without cutting mid-sentence when possible.
|
|
44
|
+
* Falls back to a raw character cut if no sentence boundary is found in the
|
|
45
|
+
* second half of the allowed window.
|
|
46
|
+
*/
|
|
47
|
+
export function truncateLightRAG(text, maxChars) {
|
|
48
|
+
if (text.length <= maxChars)
|
|
49
|
+
return text;
|
|
50
|
+
const truncated = text.slice(0, maxChars);
|
|
51
|
+
const lastPeriod = truncated.lastIndexOf(".");
|
|
52
|
+
return lastPeriod > maxChars * 0.5
|
|
53
|
+
? truncated.slice(0, lastPeriod + 1)
|
|
54
|
+
: truncated;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Convenience wrapper used by the hook handler: trim the context, truncate
|
|
58
|
+
* it to the configured budget, and return `null` if nothing remains.
|
|
59
|
+
*/
|
|
60
|
+
export function formatLightRAGResults(rawContext, maxChars) {
|
|
61
|
+
const context = rawContext.trim();
|
|
62
|
+
if (context.length === 0)
|
|
63
|
+
return null;
|
|
64
|
+
return {
|
|
65
|
+
truncated: truncateLightRAG(context, maxChars),
|
|
66
|
+
originalLength: context.length,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=lightrag.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lightrag.js","sourceRoot":"","sources":["../src/lightrag.ts"],"names":[],"mappings":"AAAA,yBAAyB;AACzB,EAAE;AACF,2EAA2E;AAC3E,6EAA6E;AAC7E,8EAA8E;AAC9E,sDAAsD;AAQtD;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,GAAW,EACX,MAAc,EACd,KAAa,EACb,OAA0B,QAAQ;IAElC,MAAM,OAAO,GAA2B;QACtC,cAAc,EAAE,kBAAkB;KACnC,CAAC;IACF,IAAI,MAAM;QAAE,OAAO,CAAC,WAAW,CAAC,GAAG,MAAM,CAAC;IAE1C,MAAM,IAAI,GAAG,MAAM,KAAK,CAAC,GAAG,GAAG,QAAQ,EAAE;QACvC,MAAM,EAAE,MAAM;QACd,OAAO;QACP,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;YACnB,KAAK;YACL,IAAI;YACJ,iBAAiB,EAAE,IAAI;YACvB,MAAM,EAAE,KAAK;SACd,CAAC;KACH,CAAC,CAAC;IAEH,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC;QACb,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;QAC/B,MAAM,IAAI,KAAK,CACb,0BAA0B,IAAI,CAAC,MAAM,MAAM,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAChE,CAAC;IACJ,CAAC;IAED,MAAM,IAAI,GAAG,CAAC,MAAM,IAAI,CAAC,IAAI,EAAE,CAA4B,CAAC;IAC5D,OAAO,IAAI,CAAC,QAAQ,IAAI,EAAE,CAAC;AAC7B,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAAY,EAAE,QAAgB;IAC7D,IAAI,IAAI,CAAC,MAAM,IAAI,QAAQ;QAAE,OAAO,IAAI,CAAC;IAEzC,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;IAC1C,MAAM,UAAU,GAAG,SAAS,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IAC9C,OAAO,UAAU,GAAG,QAAQ,GAAG,GAAG;QAChC,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,UAAU,GAAG,CAAC,CAAC;QACpC,CAAC,CAAC,SAAS,CAAC;AAChB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,qBAAqB,CACnC,UAAkB,EAClB,QAAgB;IAEhB,MAAM,OAAO,GAAG,UAAU,CAAC,IAAI,EAAE,CAAC;IAClC,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAEtC,OAAO;QACL,SAAS,EAAE,gBAAgB,CAAC,OAAO,EAAE,QAAQ,CAAC;QAC9C,cAAc,EAAE,OAAO,CAAC,MAAM;KAC/B,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { PgPoolLike, PgvectorResult } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Search a single collection in `knowledge_vectors` using cosine similarity.
|
|
4
|
+
*
|
|
5
|
+
* Score filtering is performed in JS after the query rather than in SQL so
|
|
6
|
+
* the HNSW index can handle `ORDER BY ... <=> ... LIMIT $3` efficiently
|
|
7
|
+
* (a WHERE clause on the computed score would defeat the index).
|
|
8
|
+
*
|
|
9
|
+
* On any database error we swallow the exception and return an empty array —
|
|
10
|
+
* the plugin must never block the agent, so a DB hiccup degrades gracefully.
|
|
11
|
+
*/
|
|
12
|
+
export declare function searchCollection(pool: PgPoolLike, collection: string, vector: number[], topK: number, scoreThreshold: number): Promise<PgvectorResult[]>;
|
|
13
|
+
/**
|
|
14
|
+
* Format pgvector results for injection into the system prompt.
|
|
15
|
+
* Respects a character budget so we never blow the context window with a
|
|
16
|
+
* single oversized chunk — entries are appended whole until the budget is hit.
|
|
17
|
+
*
|
|
18
|
+
* Returns `null` when there is nothing useful to inject so the caller can
|
|
19
|
+
* easily skip empty sections.
|
|
20
|
+
*/
|
|
21
|
+
export declare function formatPgvectorResults(results: PgvectorResult[], maxChars: number): string | null;
|
package/dist/pgvector.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// PostgreSQL / pgvector search helpers.
|
|
2
|
+
//
|
|
3
|
+
// The HNSW index for our 3072-dimensional embeddings is built on
|
|
4
|
+
// `halfvec(3072)` because pgvector's HNSW implementation caps at 2000 dims
|
|
5
|
+
// for the native `vector` type. Both the column cast and the query parameter
|
|
6
|
+
// cast must match, otherwise the planner falls back to a sequential scan.
|
|
7
|
+
const SEARCH_SQL = `SELECT file_name, mime_type, text, file_id, source, owner,
|
|
8
|
+
chunk_index, total_chunks, timestamp_start, timestamp_end,
|
|
9
|
+
embedded_at,
|
|
10
|
+
1 - (embedding::halfvec(3072) <=> $1::halfvec(3072)) AS score
|
|
11
|
+
FROM knowledge_vectors
|
|
12
|
+
WHERE collection = $2
|
|
13
|
+
ORDER BY embedding::halfvec(3072) <=> $1::halfvec(3072)
|
|
14
|
+
LIMIT $3`;
|
|
15
|
+
/**
|
|
16
|
+
* Search a single collection in `knowledge_vectors` using cosine similarity.
|
|
17
|
+
*
|
|
18
|
+
* Score filtering is performed in JS after the query rather than in SQL so
|
|
19
|
+
* the HNSW index can handle `ORDER BY ... <=> ... LIMIT $3` efficiently
|
|
20
|
+
* (a WHERE clause on the computed score would defeat the index).
|
|
21
|
+
*
|
|
22
|
+
* On any database error we swallow the exception and return an empty array —
|
|
23
|
+
* the plugin must never block the agent, so a DB hiccup degrades gracefully.
|
|
24
|
+
*/
|
|
25
|
+
export async function searchCollection(pool, collection, vector, topK, scoreThreshold) {
|
|
26
|
+
const vectorStr = `[${vector.join(",")}]`;
|
|
27
|
+
try {
|
|
28
|
+
const result = await pool.query(SEARCH_SQL, [vectorStr, collection, topK]);
|
|
29
|
+
// pg returns numeric columns as strings by default, so parse the score.
|
|
30
|
+
return result.rows
|
|
31
|
+
.map((row) => ({
|
|
32
|
+
collection,
|
|
33
|
+
score: parseFloat(row.score),
|
|
34
|
+
file_name: row.file_name ?? null,
|
|
35
|
+
mime_type: row.mime_type ?? null,
|
|
36
|
+
text: row.text ?? null,
|
|
37
|
+
file_id: row.file_id ?? null,
|
|
38
|
+
source: row.source ?? null,
|
|
39
|
+
owner: row.owner ?? null,
|
|
40
|
+
chunk_index: row.chunk_index ?? null,
|
|
41
|
+
total_chunks: row.total_chunks ?? null,
|
|
42
|
+
timestamp_start: row.timestamp_start ?? null,
|
|
43
|
+
timestamp_end: row.timestamp_end ?? null,
|
|
44
|
+
}))
|
|
45
|
+
.filter((row) => row.score >= scoreThreshold);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Format pgvector results for injection into the system prompt.
|
|
53
|
+
* Respects a character budget so we never blow the context window with a
|
|
54
|
+
* single oversized chunk — entries are appended whole until the budget is hit.
|
|
55
|
+
*
|
|
56
|
+
* Returns `null` when there is nothing useful to inject so the caller can
|
|
57
|
+
* easily skip empty sections.
|
|
58
|
+
*/
|
|
59
|
+
export function formatPgvectorResults(results, maxChars) {
|
|
60
|
+
if (results.length === 0)
|
|
61
|
+
return null;
|
|
62
|
+
let output = "";
|
|
63
|
+
for (const r of results) {
|
|
64
|
+
const lines = [
|
|
65
|
+
`[${r.collection}] ${r.file_name ?? "unknown"} (score: ${r.score.toFixed(2)})`,
|
|
66
|
+
];
|
|
67
|
+
if (r.timestamp_start) {
|
|
68
|
+
lines.push(`Segment: ${r.timestamp_start} - ${r.timestamp_end ?? ""}`);
|
|
69
|
+
}
|
|
70
|
+
if (r.text) {
|
|
71
|
+
lines.push(`Content: ${r.text}`);
|
|
72
|
+
}
|
|
73
|
+
lines.push(""); // blank line separator between entries
|
|
74
|
+
const entry = lines.join("\n");
|
|
75
|
+
if (output.length + entry.length > maxChars)
|
|
76
|
+
break;
|
|
77
|
+
output += entry;
|
|
78
|
+
}
|
|
79
|
+
return output;
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=pgvector.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pgvector.js","sourceRoot":"","sources":["../src/pgvector.ts"],"names":[],"mappings":"AAAA,wCAAwC;AACxC,EAAE;AACF,iEAAiE;AACjE,2EAA2E;AAC3E,6EAA6E;AAC7E,0EAA0E;AAI1E,MAAM,UAAU,GAAG;;;;;;;gBAOH,CAAC;AAEjB;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,IAAgB,EAChB,UAAkB,EAClB,MAAgB,EAChB,IAAY,EACZ,cAAsB;IAEtB,MAAM,SAAS,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;IAE1C,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,UAAU,EAAE,CAAC,SAAS,EAAE,UAAU,EAAE,IAAI,CAAC,CAAC,CAAC;QAE3E,wEAAwE;QACxE,OAAO,MAAM,CAAC,IAAI;aACf,GAAG,CAAC,CAAC,GAAgB,EAAkB,EAAE,CAAC,CAAC;YAC1C,UAAU;YACV,KAAK,EAAE,UAAU,CAAC,GAAG,CAAC,KAAK,CAAC;YAC5B,SAAS,EAAE,GAAG,CAAC,SAAS,IAAI,IAAI;YAChC,SAAS,EAAE,GAAG,CAAC,SAAS,IAAI,IAAI;YAChC,IAAI,EAAE,GAAG,CAAC,IAAI,IAAI,IAAI;YACtB,OAAO,EAAE,GAAG,CAAC,OAAO,IAAI,IAAI;YAC5B,MAAM,EAAE,GAAG,CAAC,MAAM,IAAI,IAAI;YAC1B,KAAK,EAAE,GAAG,CAAC,KAAK,IAAI,IAAI;YACxB,WAAW,EAAE,GAAG,CAAC,WAAW,IAAI,IAAI;YACpC,YAAY,EAAE,GAAG,CAAC,YAAY,IAAI,IAAI;YACtC,eAAe,EAAE,GAAG,CAAC,eAAe,IAAI,IAAI;YAC5C,aAAa,EAAE,GAAG,CAAC,aAAa,IAAI,IAAI;SACzC,CAAC,CAAC;aACF,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,KAAK,IAAI,cAAc,CAAC,CAAC;IAClD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,qBAAqB,CACnC,OAAyB,EACzB,QAAgB;IAEhB,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAEtC,IAAI,MAAM,GAAG,EAAE,CAAC;IAChB,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;QACxB,MAAM,KAAK,GAAa;YACtB,IAAI,CAAC,CAAC,UAAU,KAAK,CAAC,CAAC,SAAS,IAAI,SAAS,YAAY,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG;SAC/E,CAAC;QAEF,IAAI,CAAC,CAAC,eAAe,EAAE,CAAC;YACtB,KAAK,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,eAAe,MAAM,CAAC,CAAC,aAAa,IAAI,EAAE,EAAE,CAAC,CAAC;QACzE,CAAC;QAED,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC;YACX,KAAK,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;QACnC,CAAC;QAED,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,uCAAuC;QACvD,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAE/B,IAAI,MAAM,CAAC,MAAM,GAAG,KAAK,CAAC,MAAM,GAAG,QAAQ;YAAE,MAAM;QACnD,MAAM,IAAI,KAAK,CAAC;IAClB,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime configuration as it appears in `plugins.entries.openclaw-knowledge.config`.
|
|
3
|
+
* All fields are optional — defaults are applied in {@link resolveConfig}.
|
|
4
|
+
*/
|
|
5
|
+
export interface KnowledgePluginConfig {
|
|
6
|
+
enabled?: boolean;
|
|
7
|
+
geminiApiKey?: string;
|
|
8
|
+
postgresUrl?: string;
|
|
9
|
+
collections?: string[];
|
|
10
|
+
topK?: number;
|
|
11
|
+
scoreThreshold?: number;
|
|
12
|
+
maxInjectChars?: number;
|
|
13
|
+
pgvectorEnabled?: boolean;
|
|
14
|
+
lightragUrl?: string;
|
|
15
|
+
lightragApiKey?: string;
|
|
16
|
+
lightragQueryMode?: LightRAGQueryMode;
|
|
17
|
+
lightragMaxChars?: number;
|
|
18
|
+
lightragEnabled?: boolean;
|
|
19
|
+
}
|
|
20
|
+
export type LightRAGQueryMode = "naive" | "local" | "global" | "hybrid";
|
|
21
|
+
/**
|
|
22
|
+
* Fully resolved plugin configuration after defaults, env substitution, and
|
|
23
|
+
* derivation of the pgvector/lightrag enabled flags from presence of secrets.
|
|
24
|
+
*/
|
|
25
|
+
export interface ResolvedKnowledgeConfig {
|
|
26
|
+
enabled: boolean;
|
|
27
|
+
geminiApiKey: string;
|
|
28
|
+
postgresUrl: string;
|
|
29
|
+
collections: string[];
|
|
30
|
+
topK: number;
|
|
31
|
+
scoreThreshold: number;
|
|
32
|
+
maxInjectChars: number;
|
|
33
|
+
pgvectorEnabled: boolean;
|
|
34
|
+
lightragUrl: string;
|
|
35
|
+
lightragApiKey: string;
|
|
36
|
+
lightragQueryMode: LightRAGQueryMode;
|
|
37
|
+
lightragMaxChars: number;
|
|
38
|
+
lightragEnabled: boolean;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* One search hit from the PostgreSQL `knowledge_vectors` table, after score
|
|
42
|
+
* parsing and filtering.
|
|
43
|
+
*/
|
|
44
|
+
export interface PgvectorResult {
|
|
45
|
+
collection: string;
|
|
46
|
+
score: number;
|
|
47
|
+
file_name: string | null;
|
|
48
|
+
mime_type: string | null;
|
|
49
|
+
text: string | null;
|
|
50
|
+
file_id: string | null;
|
|
51
|
+
source: string | null;
|
|
52
|
+
owner: string | null;
|
|
53
|
+
chunk_index: number | null;
|
|
54
|
+
total_chunks: number | null;
|
|
55
|
+
timestamp_start: string | null;
|
|
56
|
+
timestamp_end: string | null;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Minimal `pg.Pool` surface that {@link searchCollection} actually uses.
|
|
60
|
+
* Declared locally so helpers can be unit-tested without a real database
|
|
61
|
+
* and without pulling `@types/pg` into the test graph.
|
|
62
|
+
*/
|
|
63
|
+
export interface PgPoolLike {
|
|
64
|
+
query(sql: string, params: unknown[]): Promise<{
|
|
65
|
+
rows: PgvectorRow[];
|
|
66
|
+
}>;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Raw row shape returned by the pgvector SQL query. `score` comes back as a
|
|
70
|
+
* string because pg returns numeric values as strings by default.
|
|
71
|
+
*/
|
|
72
|
+
export interface PgvectorRow {
|
|
73
|
+
file_name?: string | null;
|
|
74
|
+
mime_type?: string | null;
|
|
75
|
+
text?: string | null;
|
|
76
|
+
file_id?: string | null;
|
|
77
|
+
source?: string | null;
|
|
78
|
+
owner?: string | null;
|
|
79
|
+
chunk_index?: number | null;
|
|
80
|
+
total_chunks?: number | null;
|
|
81
|
+
timestamp_start?: string | null;
|
|
82
|
+
timestamp_end?: string | null;
|
|
83
|
+
embedded_at?: string | null;
|
|
84
|
+
score: string;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Shape of the `before_prompt_build` event payload as consumed by this plugin.
|
|
88
|
+
* We only rely on `messages`; the SDK may add other fields that we ignore.
|
|
89
|
+
*/
|
|
90
|
+
export interface BeforePromptBuildEvent {
|
|
91
|
+
messages?: PromptMessage[];
|
|
92
|
+
}
|
|
93
|
+
export interface PromptMessage {
|
|
94
|
+
role?: string;
|
|
95
|
+
content?: string | PromptContentPart[];
|
|
96
|
+
}
|
|
97
|
+
export interface PromptContentPart {
|
|
98
|
+
type?: string;
|
|
99
|
+
text?: string;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Return value honoured by OpenClaw when a `before_prompt_build` handler wants
|
|
103
|
+
* to append extra text to the agent's system prompt.
|
|
104
|
+
*/
|
|
105
|
+
export interface BeforePromptBuildResult {
|
|
106
|
+
appendSystemContext: string;
|
|
107
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,sDAAsD;AACtD,EAAE;AACF,0EAA0E;AAC1E,oEAAoE"}
|