@spark-agents/engram 0.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/LICENSE +21 -0
- package/README.md +337 -0
- package/dist/chunker.d.ts +3 -0
- package/dist/chunker.js +337 -0
- package/dist/config.d.ts +19 -0
- package/dist/config.js +38 -0
- package/dist/embedding.d.ts +8 -0
- package/dist/embedding.js +186 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +524 -0
- package/dist/manager.d.ts +76 -0
- package/dist/manager.js +103 -0
- package/dist/reranker.d.ts +15 -0
- package/dist/reranker.js +104 -0
- package/dist/search.d.ts +33 -0
- package/dist/search.js +203 -0
- package/dist/store.d.ts +6 -0
- package/dist/store.js +272 -0
- package/dist/sync.d.ts +31 -0
- package/dist/sync.js +516 -0
- package/dist/types.d.ts +111 -0
- package/dist/types.js +28 -0
- package/openclaw.plugin.json +58 -0
- package/package.json +39 -0
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type EngramConfig, type MediaModality } from "./types.js";
|
|
2
|
+
export interface ResolvedConfig {
|
|
3
|
+
geminiApiKey?: string;
|
|
4
|
+
dimensions: 768 | 1536 | 3072;
|
|
5
|
+
chunkTokens: number;
|
|
6
|
+
chunkOverlap: number;
|
|
7
|
+
reranking: boolean;
|
|
8
|
+
timeDecay: {
|
|
9
|
+
enabled: boolean;
|
|
10
|
+
halfLifeDays: number;
|
|
11
|
+
};
|
|
12
|
+
maxSessionShare: number;
|
|
13
|
+
multimodal: {
|
|
14
|
+
enabled: boolean;
|
|
15
|
+
modalities: MediaModality[];
|
|
16
|
+
maxFileBytes: number;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export declare function resolveConfig(raw?: EngramConfig | Record<string, unknown>): ResolvedConfig;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { DEFAULT_CONFIG } from "./types.js";
|
|
2
|
+
const DEFAULT_MAX_MEDIA_FILE_BYTES = 10 * 1024 * 1024;
|
|
3
|
+
const DEFAULT_MEDIA_MODALITIES = ["image", "audio"];
|
|
4
|
+
function normalizeMediaModalities(modalities) {
|
|
5
|
+
if (!Array.isArray(modalities) || modalities.length === 0) {
|
|
6
|
+
return [...DEFAULT_MEDIA_MODALITIES];
|
|
7
|
+
}
|
|
8
|
+
const unique = new Set();
|
|
9
|
+
for (const modality of modalities) {
|
|
10
|
+
if (modality === "image" || modality === "audio") {
|
|
11
|
+
unique.add(modality);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
if (unique.size === 0) {
|
|
15
|
+
return [...DEFAULT_MEDIA_MODALITIES];
|
|
16
|
+
}
|
|
17
|
+
return Array.from(unique);
|
|
18
|
+
}
|
|
19
|
+
export function resolveConfig(raw) {
|
|
20
|
+
const cfg = (raw ?? {});
|
|
21
|
+
return {
|
|
22
|
+
geminiApiKey: cfg.geminiApiKey,
|
|
23
|
+
dimensions: cfg.dimensions ?? DEFAULT_CONFIG.dimensions,
|
|
24
|
+
chunkTokens: cfg.chunkTokens ?? DEFAULT_CONFIG.chunkTokens,
|
|
25
|
+
chunkOverlap: cfg.chunkOverlap ?? DEFAULT_CONFIG.chunkOverlap,
|
|
26
|
+
reranking: cfg.reranking ?? true,
|
|
27
|
+
timeDecay: {
|
|
28
|
+
enabled: cfg.timeDecay?.enabled ?? DEFAULT_CONFIG.timeDecay.enabled,
|
|
29
|
+
halfLifeDays: cfg.timeDecay?.halfLifeDays ?? DEFAULT_CONFIG.timeDecay.halfLifeDays,
|
|
30
|
+
},
|
|
31
|
+
maxSessionShare: cfg.maxSessionShare ?? DEFAULT_CONFIG.maxSessionShare,
|
|
32
|
+
multimodal: {
|
|
33
|
+
enabled: cfg.multimodal?.enabled ?? true,
|
|
34
|
+
modalities: normalizeMediaModalities(cfg.multimodal?.modalities),
|
|
35
|
+
maxFileBytes: Math.max(1, Math.trunc(cfg.multimodal?.maxFileBytes ?? DEFAULT_MAX_MEDIA_FILE_BYTES)),
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { EmbeddingClient } from "./types.js";
|
|
2
|
+
export declare function l2Normalize(vec: Float32Array): Float32Array;
|
|
3
|
+
export declare function createEmbeddingClient(params: {
|
|
4
|
+
apiKey: string;
|
|
5
|
+
dimensions?: number;
|
|
6
|
+
model?: string;
|
|
7
|
+
baseUrl?: string;
|
|
8
|
+
}): Promise<EmbeddingClient>;
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
const GEMINI_DEFAULT_BASE_URL = "https://generativelanguage.googleapis.com";
|
|
2
|
+
const GEMINI_DEFAULT_MODEL = "gemini-embedding-2-preview";
|
|
3
|
+
const DEFAULT_DIMENSIONS = 768;
|
|
4
|
+
const MAX_BATCH_SIZE = 100;
|
|
5
|
+
function isRecord(value) {
|
|
6
|
+
return typeof value === "object" && value !== null;
|
|
7
|
+
}
|
|
8
|
+
function normalizeModelName(model) {
|
|
9
|
+
return model.startsWith("models/") ? model.slice("models/".length) : model;
|
|
10
|
+
}
|
|
11
|
+
function modelResourceName(model) {
|
|
12
|
+
return model.startsWith("models/") ? model : `models/${model}`;
|
|
13
|
+
}
|
|
14
|
+
function getNumberArray(value, context) {
|
|
15
|
+
if (!Array.isArray(value)) {
|
|
16
|
+
throw new Error(`${context} must be an array`);
|
|
17
|
+
}
|
|
18
|
+
const out = new Array(value.length);
|
|
19
|
+
for (let i = 0; i < value.length; i++) {
|
|
20
|
+
const item = value[i];
|
|
21
|
+
if (typeof item !== "number") {
|
|
22
|
+
throw new Error(`${context} contains a non-number value at index ${i}`);
|
|
23
|
+
}
|
|
24
|
+
out[i] = item;
|
|
25
|
+
}
|
|
26
|
+
return out;
|
|
27
|
+
}
|
|
28
|
+
function toGeminiVector(values, dimensions) {
|
|
29
|
+
const vec = Float32Array.from(values);
|
|
30
|
+
if (dimensions < 3072) {
|
|
31
|
+
return l2Normalize(vec);
|
|
32
|
+
}
|
|
33
|
+
return vec;
|
|
34
|
+
}
|
|
35
|
+
function buildGeminiEndpoint(params) {
|
|
36
|
+
const root = params.baseUrl.replace(/\/+$/, "");
|
|
37
|
+
return `${root}/v1beta/models/${params.model}:${params.method}?key=${encodeURIComponent(params.apiKey)}`;
|
|
38
|
+
}
|
|
39
|
+
function sleep(ms) {
|
|
40
|
+
return new Promise((resolve) => {
|
|
41
|
+
setTimeout(resolve, ms);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
async function postJsonWithRetry(params) {
|
|
45
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
46
|
+
const response = await fetch(params.url, {
|
|
47
|
+
method: "POST",
|
|
48
|
+
headers: {
|
|
49
|
+
"Content-Type": "application/json",
|
|
50
|
+
...(params.headers ?? {}),
|
|
51
|
+
},
|
|
52
|
+
body: JSON.stringify(params.body),
|
|
53
|
+
});
|
|
54
|
+
if (response.status === 429 && attempt === 0) {
|
|
55
|
+
await sleep(1000);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (!response.ok) {
|
|
59
|
+
const responseBody = await response.text();
|
|
60
|
+
throw new Error(`${params.providerName} embedding request failed (${response.status}): ${responseBody || "<empty response body>"}`);
|
|
61
|
+
}
|
|
62
|
+
return (await response.json());
|
|
63
|
+
}
|
|
64
|
+
throw new Error(`${params.providerName} embedding request failed after retry`);
|
|
65
|
+
}
|
|
66
|
+
function parseGeminiEmbeddingResponse(response, dimensions) {
|
|
67
|
+
if (!isRecord(response)) {
|
|
68
|
+
throw new Error("Gemini embedding response must be an object");
|
|
69
|
+
}
|
|
70
|
+
const embedding = response.embedding;
|
|
71
|
+
if (!isRecord(embedding)) {
|
|
72
|
+
throw new Error("Gemini embedding response missing embedding object");
|
|
73
|
+
}
|
|
74
|
+
return toGeminiVector(getNumberArray(embedding.values, "embedding.values"), dimensions);
|
|
75
|
+
}
|
|
76
|
+
export function l2Normalize(vec) {
|
|
77
|
+
let norm = 0;
|
|
78
|
+
for (let i = 0; i < vec.length; i++)
|
|
79
|
+
norm += vec[i] * vec[i];
|
|
80
|
+
norm = Math.sqrt(norm);
|
|
81
|
+
if (norm === 0)
|
|
82
|
+
return vec;
|
|
83
|
+
const out = new Float32Array(vec.length);
|
|
84
|
+
for (let i = 0; i < vec.length; i++)
|
|
85
|
+
out[i] = vec[i] / norm;
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
export async function createEmbeddingClient(params) {
|
|
89
|
+
const model = normalizeModelName(params.model ?? GEMINI_DEFAULT_MODEL);
|
|
90
|
+
const dimensions = params.dimensions ?? DEFAULT_DIMENSIONS;
|
|
91
|
+
const baseUrl = params.baseUrl ?? GEMINI_DEFAULT_BASE_URL;
|
|
92
|
+
const embedText = async (text, taskType) => {
|
|
93
|
+
const endpoint = buildGeminiEndpoint({
|
|
94
|
+
baseUrl,
|
|
95
|
+
model,
|
|
96
|
+
method: "embedContent",
|
|
97
|
+
apiKey: params.apiKey,
|
|
98
|
+
});
|
|
99
|
+
const body = {
|
|
100
|
+
content: {
|
|
101
|
+
parts: [{ text }],
|
|
102
|
+
},
|
|
103
|
+
taskType,
|
|
104
|
+
outputDimensionality: dimensions,
|
|
105
|
+
};
|
|
106
|
+
const response = await postJsonWithRetry({
|
|
107
|
+
url: endpoint,
|
|
108
|
+
body,
|
|
109
|
+
providerName: "Gemini",
|
|
110
|
+
});
|
|
111
|
+
return parseGeminiEmbeddingResponse(response, dimensions);
|
|
112
|
+
};
|
|
113
|
+
const embedMedia = async (data, mimeType) => {
|
|
114
|
+
const endpoint = buildGeminiEndpoint({
|
|
115
|
+
baseUrl,
|
|
116
|
+
model,
|
|
117
|
+
method: "embedContent",
|
|
118
|
+
apiKey: params.apiKey,
|
|
119
|
+
});
|
|
120
|
+
const body = {
|
|
121
|
+
content: {
|
|
122
|
+
parts: [{ inlineData: { data: data.toString("base64"), mimeType } }],
|
|
123
|
+
},
|
|
124
|
+
taskType: "RETRIEVAL_DOCUMENT",
|
|
125
|
+
outputDimensionality: dimensions,
|
|
126
|
+
};
|
|
127
|
+
const response = await postJsonWithRetry({
|
|
128
|
+
url: endpoint,
|
|
129
|
+
body,
|
|
130
|
+
providerName: "Gemini",
|
|
131
|
+
});
|
|
132
|
+
return parseGeminiEmbeddingResponse(response, dimensions);
|
|
133
|
+
};
|
|
134
|
+
const embedBatch = async (texts, taskType) => {
|
|
135
|
+
if (texts.length === 0) {
|
|
136
|
+
return [];
|
|
137
|
+
}
|
|
138
|
+
const endpoint = buildGeminiEndpoint({
|
|
139
|
+
baseUrl,
|
|
140
|
+
model,
|
|
141
|
+
method: "batchEmbedContents",
|
|
142
|
+
apiKey: params.apiKey,
|
|
143
|
+
});
|
|
144
|
+
const vectors = [];
|
|
145
|
+
for (let i = 0; i < texts.length; i += MAX_BATCH_SIZE) {
|
|
146
|
+
const chunk = texts.slice(i, i + MAX_BATCH_SIZE);
|
|
147
|
+
const body = {
|
|
148
|
+
requests: chunk.map((text) => ({
|
|
149
|
+
model: modelResourceName(model),
|
|
150
|
+
content: {
|
|
151
|
+
parts: [{ text }],
|
|
152
|
+
},
|
|
153
|
+
taskType,
|
|
154
|
+
outputDimensionality: dimensions,
|
|
155
|
+
})),
|
|
156
|
+
};
|
|
157
|
+
const response = await postJsonWithRetry({
|
|
158
|
+
url: endpoint,
|
|
159
|
+
body,
|
|
160
|
+
providerName: "Gemini",
|
|
161
|
+
});
|
|
162
|
+
if (!isRecord(response) || !Array.isArray(response.embeddings)) {
|
|
163
|
+
throw new Error("Gemini batch embedding response missing embeddings array");
|
|
164
|
+
}
|
|
165
|
+
if (response.embeddings.length !== chunk.length) {
|
|
166
|
+
throw new Error(`Gemini batch embedding response count mismatch: expected ${chunk.length}, got ${response.embeddings.length}`);
|
|
167
|
+
}
|
|
168
|
+
for (let j = 0; j < response.embeddings.length; j++) {
|
|
169
|
+
const embedding = response.embeddings[j];
|
|
170
|
+
if (!isRecord(embedding)) {
|
|
171
|
+
throw new Error(`Gemini batch embedding at index ${j} is not an object`);
|
|
172
|
+
}
|
|
173
|
+
vectors.push(toGeminiVector(getNumberArray(embedding.values, `embeddings[${j}].values`), dimensions));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return vectors;
|
|
177
|
+
};
|
|
178
|
+
return {
|
|
179
|
+
embedText,
|
|
180
|
+
embedBatch,
|
|
181
|
+
embedMedia,
|
|
182
|
+
supportsMultimodal: true,
|
|
183
|
+
dimensions,
|
|
184
|
+
model,
|
|
185
|
+
};
|
|
186
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type ResolvedConfig } from "./config.js";
|
|
2
|
+
type OpenClawApi = any;
|
|
3
|
+
interface CliAgentConfig {
|
|
4
|
+
id: string;
|
|
5
|
+
workspace: string;
|
|
6
|
+
agentDir: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function resolveAgentsFromConfig(clawConfig: any, agentFilter?: string): CliAgentConfig[];
|
|
9
|
+
export declare function registerEngramCli(program: any, api: OpenClawApi, config: ResolvedConfig, clawConfig: any): void;
|
|
10
|
+
declare const engramPlugin: {
|
|
11
|
+
id: string;
|
|
12
|
+
name: string;
|
|
13
|
+
description: string;
|
|
14
|
+
kind: "memory";
|
|
15
|
+
register(api: OpenClawApi): void;
|
|
16
|
+
};
|
|
17
|
+
export default engramPlugin;
|