@juspay/neurolink 12.9.6 → 12.11.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 +3 -3
- package/dist/artifacts/artifactBanking.d.ts +6 -2
- package/dist/artifacts/artifactBanking.js +9 -14
- package/dist/artifacts/artifactReader.d.ts +69 -0
- package/dist/artifacts/artifactReader.js +157 -0
- package/dist/artifacts/artifactStore.d.ts +7 -3
- package/dist/artifacts/artifactStore.js +10 -21
- package/dist/artifacts/artifactStoreFactory.d.ts +39 -0
- package/dist/artifacts/artifactStoreFactory.js +101 -0
- package/dist/artifacts/redisArtifactStore.d.ts +84 -0
- package/dist/artifacts/redisArtifactStore.js +270 -0
- package/dist/browser/neurolink.min.js +381 -383
- package/dist/constants/enums.d.ts +77 -0
- package/dist/constants/enums.js +82 -0
- package/dist/core/modules/GenerationHandler.d.ts +15 -3
- package/dist/core/modules/GenerationHandler.js +172 -14
- package/dist/index.d.ts +3 -0
- package/dist/index.js +7 -0
- package/dist/memory/memoryRetrievalTools.js +104 -35
- package/dist/neurolink.d.ts +45 -1
- package/dist/neurolink.js +128 -18
- package/dist/providers/catalog/baseten.json +263 -0
- package/dist/providers/catalog/gmicloud.json +64 -0
- package/dist/providers/catalog/inception-labs.json +75 -0
- package/dist/providers/catalog/index.generated.d.ts +1 -1
- package/dist/providers/catalog/index.generated.js +15 -0
- package/dist/providers/catalog/io-intelligence.json +463 -0
- package/dist/providers/catalog/schema.d.ts +1 -1
- package/dist/providers/catalog/upstage.json +165 -0
- package/dist/providers/openaiChatCompletionsClient.js +18 -1
- package/dist/types/artifact.d.ts +124 -3
- package/dist/types/config.d.ts +7 -0
- package/dist/types/generate.d.ts +17 -0
- package/dist/types/openaiCompatible.d.ts +5 -1
- package/dist/types/providerCatalog.generated.d.ts +2 -2
- package/dist/types/providers.d.ts +20 -0
- package/dist/utils/redis.d.ts +15 -0
- package/dist/utils/redis.js +64 -6
- package/package.json +10 -6
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redis Artifact Store
|
|
3
|
+
*
|
|
4
|
+
* The artifact backend for more than one machine. `LocalTempArtifactStore`
|
|
5
|
+
* writes to a pod's own `/tmp`: a replica that did not store an artifact
|
|
6
|
+
* cannot read it, a redeploy loses all of them, and `cleanup()` can only
|
|
7
|
+
* expire what its own process wrote. Here every replica sees every artifact,
|
|
8
|
+
* Redis expires them by TTL, and a paged read moves only the window.
|
|
9
|
+
*
|
|
10
|
+
* Layout, under `keyPrefix` (default `neurolink:artifact:`):
|
|
11
|
+
*
|
|
12
|
+
* <prefix><id> STRING the payload, verbatim
|
|
13
|
+
* <prefix><id>:meta STRING JSON `RedisArtifactRecord`
|
|
14
|
+
*
|
|
15
|
+
* Both keys carry the same TTL and are written in one MULTI, so an id either
|
|
16
|
+
* resolves completely or not at all.
|
|
17
|
+
*
|
|
18
|
+
* Range reads are honest about units. `retrieve_context` addresses characters;
|
|
19
|
+
* `GETRANGE` addresses bytes. The record stores the payload's character length
|
|
20
|
+
* next to its byte length, and only when the two are equal — pure ASCII, which
|
|
21
|
+
* is what JSON tool output and logs almost always are — is a byte range used
|
|
22
|
+
* as a character range. Anything else falls back to a whole read and a slice,
|
|
23
|
+
* which is slower and still correct. A window never starts on the wrong
|
|
24
|
+
* character.
|
|
25
|
+
*
|
|
26
|
+
* The connection is the same pool Redis conversation memory uses, keyed by
|
|
27
|
+
* host, port and database, so a deployment that already keeps sessions in
|
|
28
|
+
* Redis adds no connection by keeping artifacts there too.
|
|
29
|
+
*
|
|
30
|
+
* @module artifacts/redisArtifactStore
|
|
31
|
+
*/
|
|
32
|
+
import { randomUUID } from "node:crypto";
|
|
33
|
+
import { logger } from "../utils/logger.js";
|
|
34
|
+
import { getNormalizedConfig, getPooledRedisClient, releasePooledRedisClient, } from "../utils/redis.js";
|
|
35
|
+
import { generateArtifactPreview, isSafeArtifactId, sliceArtifactWindow, } from "./artifactReader.js";
|
|
36
|
+
/** Key prefix when the caller does not choose one. Never the conversation prefix. */
|
|
37
|
+
export const DEFAULT_ARTIFACT_KEY_PREFIX = "neurolink:artifact:";
|
|
38
|
+
/** Suffix of the metadata key beside each payload. */
|
|
39
|
+
const META_SUFFIX = ":meta";
|
|
40
|
+
/** Expiry when the caller does not choose one, or chooses an unusable one. */
|
|
41
|
+
export const DEFAULT_ARTIFACT_TTL_SECONDS = 86_400;
|
|
42
|
+
/** A TTL Redis can apply: a finite, positive number of seconds. */
|
|
43
|
+
function isUsableTtl(ttl) {
|
|
44
|
+
return ttl !== undefined && Number.isFinite(ttl) && ttl > 0;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Runtime shape check for a record read back from Redis. It is untrusted
|
|
48
|
+
* input — another version wrote it, or something else owns the key — so it is
|
|
49
|
+
* validated rather than asserted.
|
|
50
|
+
*/
|
|
51
|
+
function parseRecord(value) {
|
|
52
|
+
if (!value || typeof value !== "object") {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
const row = { ...value };
|
|
56
|
+
const { toolName, serverId, sizeBytes, contentType, createdAt, charLength } = row;
|
|
57
|
+
if (typeof toolName !== "string" ||
|
|
58
|
+
typeof serverId !== "string" ||
|
|
59
|
+
typeof sizeBytes !== "number" ||
|
|
60
|
+
(contentType !== "json" && contentType !== "text") ||
|
|
61
|
+
typeof createdAt !== "number" ||
|
|
62
|
+
typeof charLength !== "number") {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
const record = {
|
|
66
|
+
toolName,
|
|
67
|
+
serverId,
|
|
68
|
+
sizeBytes,
|
|
69
|
+
contentType,
|
|
70
|
+
createdAt,
|
|
71
|
+
charLength,
|
|
72
|
+
};
|
|
73
|
+
if (typeof row.sessionId === "string") {
|
|
74
|
+
record.sessionId = row.sessionId;
|
|
75
|
+
}
|
|
76
|
+
if (typeof row.label === "string") {
|
|
77
|
+
record.label = row.label;
|
|
78
|
+
}
|
|
79
|
+
const kind = row.kind;
|
|
80
|
+
if (kind === "worker-report" ||
|
|
81
|
+
kind === "command-output" ||
|
|
82
|
+
kind === "stage-output" ||
|
|
83
|
+
kind === "other") {
|
|
84
|
+
record.kind = kind;
|
|
85
|
+
}
|
|
86
|
+
return record;
|
|
87
|
+
}
|
|
88
|
+
function parseJson(raw) {
|
|
89
|
+
try {
|
|
90
|
+
return JSON.parse(raw);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* node-redis may hand back a Buffer for a string command; payloads are text.
|
|
98
|
+
* `String(buffer)` is `buffer.toString()`, which decodes UTF-8.
|
|
99
|
+
*/
|
|
100
|
+
function asText(value) {
|
|
101
|
+
return value === null ? null : String(value);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Redis-backed artifact store: shared across replicas, expired by TTL,
|
|
105
|
+
* range reads for ASCII payloads.
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* ```typescript
|
|
109
|
+
* const store = new RedisArtifactStore({ url: process.env.REDIS_URL });
|
|
110
|
+
* const neurolink = new NeuroLink({ artifacts: { store } });
|
|
111
|
+
* // or let NeuroLink build it: { artifacts: { storage: "redis" } }
|
|
112
|
+
* ```
|
|
113
|
+
*/
|
|
114
|
+
export class RedisArtifactStore {
|
|
115
|
+
config;
|
|
116
|
+
client;
|
|
117
|
+
connecting;
|
|
118
|
+
/**
|
|
119
|
+
* @param config - Connection and key settings. `keyPrefix` defaults to
|
|
120
|
+
* `neurolink:artifact:`. `ttl` is seconds and must be positive; it
|
|
121
|
+
* defaults to 86400 (24 hours), and zero, negative or non-finite values
|
|
122
|
+
* are replaced by that default with a warning — artifacts in Redis always
|
|
123
|
+
* expire, there is no "keep forever". `userSessionsKeyPrefix` is
|
|
124
|
+
* meaningless here and ignored.
|
|
125
|
+
*/
|
|
126
|
+
constructor(config = {}) {
|
|
127
|
+
if (config.ttl !== undefined && !isUsableTtl(config.ttl)) {
|
|
128
|
+
logger.warn(`[RedisArtifactStore] Ignoring ttl ${String(config.ttl)}: it must be ` +
|
|
129
|
+
`a positive number of seconds — using ${DEFAULT_ARTIFACT_TTL_SECONDS}s`);
|
|
130
|
+
}
|
|
131
|
+
this.config = getNormalizedConfig({
|
|
132
|
+
...config,
|
|
133
|
+
keyPrefix: config.keyPrefix ?? DEFAULT_ARTIFACT_KEY_PREFIX,
|
|
134
|
+
ttl: isUsableTtl(config.ttl) ? config.ttl : DEFAULT_ARTIFACT_TTL_SECONDS,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
generatePreview(payload) {
|
|
138
|
+
return generateArtifactPreview(payload);
|
|
139
|
+
}
|
|
140
|
+
async store(payload, meta) {
|
|
141
|
+
const client = await this.getClient();
|
|
142
|
+
const id = randomUUID();
|
|
143
|
+
const createdAt = Date.now();
|
|
144
|
+
// Measured here rather than trusted from `meta`: the ASCII fast path in
|
|
145
|
+
// `retrieveRange` compares these two numbers, and a caller's estimate is
|
|
146
|
+
// not evidence.
|
|
147
|
+
const record = {
|
|
148
|
+
...meta,
|
|
149
|
+
sizeBytes: Buffer.byteLength(payload, "utf-8"),
|
|
150
|
+
createdAt,
|
|
151
|
+
charLength: payload.length,
|
|
152
|
+
};
|
|
153
|
+
const ttl = this.config.ttl;
|
|
154
|
+
const options = { EX: ttl };
|
|
155
|
+
await client
|
|
156
|
+
.multi()
|
|
157
|
+
.set(this.payloadKey(id), payload, options)
|
|
158
|
+
.set(this.metaKey(id), JSON.stringify(record), options)
|
|
159
|
+
.exec();
|
|
160
|
+
logger.debug(`[RedisArtifactStore] Stored artifact ${id} for tool "${meta.toolName}" ` +
|
|
161
|
+
`(${record.sizeBytes} B, ttl ${ttl}s)`);
|
|
162
|
+
return {
|
|
163
|
+
id,
|
|
164
|
+
preview: this.generatePreview(payload),
|
|
165
|
+
sizeBytes: meta.sizeBytes,
|
|
166
|
+
meta: { ...meta, createdAt },
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
async retrieve(id) {
|
|
170
|
+
if (!isSafeArtifactId(id)) {
|
|
171
|
+
logger.debug(`[RedisArtifactStore] Rejected unsafe artifact id "${id}"`);
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
const client = await this.getClient();
|
|
175
|
+
return asText(await client.get(this.payloadKey(id)));
|
|
176
|
+
}
|
|
177
|
+
async retrieveRange(id, range) {
|
|
178
|
+
if (!isSafeArtifactId(id)) {
|
|
179
|
+
logger.debug(`[RedisArtifactStore] Rejected unsafe artifact id "${id}"`);
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
const client = await this.getClient();
|
|
183
|
+
const offset = Math.max(0, range.offset ?? 0);
|
|
184
|
+
const limit = range.limit === undefined ? undefined : Math.max(0, range.limit);
|
|
185
|
+
const rawRecord = asText(await client.get(this.metaKey(id)));
|
|
186
|
+
const record = rawRecord === null ? undefined : parseRecord(parseJson(rawRecord));
|
|
187
|
+
if (!record || record.charLength !== record.sizeBytes) {
|
|
188
|
+
// No usable record, or a multi-byte payload where a byte offset is not
|
|
189
|
+
// a character offset: read whole and cut. Still correct, just not cheap.
|
|
190
|
+
const content = asText(await client.get(this.payloadKey(id)));
|
|
191
|
+
return content === null
|
|
192
|
+
? null
|
|
193
|
+
: sliceArtifactWindow(content, { offset, limit });
|
|
194
|
+
}
|
|
195
|
+
const totalLength = record.charLength;
|
|
196
|
+
if (offset >= totalLength || limit === 0) {
|
|
197
|
+
return { content: "", offset, totalLength };
|
|
198
|
+
}
|
|
199
|
+
const end = limit === undefined ? -1 : offset + limit - 1;
|
|
200
|
+
const content = asText(await client.getRange(this.payloadKey(id), offset, end));
|
|
201
|
+
if (content === null || content.length === 0) {
|
|
202
|
+
// GETRANGE on a missing key is "", not nil — the payload expired between
|
|
203
|
+
// the record read and this one.
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
return { content, offset, totalLength };
|
|
207
|
+
}
|
|
208
|
+
async delete(id) {
|
|
209
|
+
if (!isSafeArtifactId(id)) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const client = await this.getClient();
|
|
213
|
+
await client.del([this.payloadKey(id), this.metaKey(id)]);
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Nothing to sweep: Redis expires every artifact `ttl` seconds after it was
|
|
217
|
+
* written, on every replica at once, which is what `cleanup()` on the local
|
|
218
|
+
* store could never do.
|
|
219
|
+
*/
|
|
220
|
+
async cleanup(olderThanMs) {
|
|
221
|
+
logger.debug(`[RedisArtifactStore] cleanup(${olderThanMs}) is a no-op — artifacts ` +
|
|
222
|
+
`expire by TTL (${this.config.ttl}s)`);
|
|
223
|
+
return 0;
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Release this store's reference on the pooled connection.
|
|
227
|
+
*
|
|
228
|
+
* Waits for a connect that is still in flight: otherwise the reference it
|
|
229
|
+
* is about to acquire would be assigned after this returned, and nothing
|
|
230
|
+
* would ever release it.
|
|
231
|
+
*/
|
|
232
|
+
async close() {
|
|
233
|
+
if (this.connecting) {
|
|
234
|
+
try {
|
|
235
|
+
await this.connecting;
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
// The connect failed, so nothing was acquired.
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (!this.client) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
this.client = undefined;
|
|
245
|
+
await releasePooledRedisClient(this.config);
|
|
246
|
+
}
|
|
247
|
+
payloadKey(id) {
|
|
248
|
+
return `${this.config.keyPrefix}${id}`;
|
|
249
|
+
}
|
|
250
|
+
metaKey(id) {
|
|
251
|
+
return `${this.config.keyPrefix}${id}${META_SUFFIX}`;
|
|
252
|
+
}
|
|
253
|
+
/** Connect on first use, once, and share the pooled client afterwards. */
|
|
254
|
+
async getClient() {
|
|
255
|
+
if (this.client?.isOpen) {
|
|
256
|
+
return this.client;
|
|
257
|
+
}
|
|
258
|
+
if (!this.connecting) {
|
|
259
|
+
this.connecting = getPooledRedisClient(this.config)
|
|
260
|
+
.then((client) => {
|
|
261
|
+
this.client = client;
|
|
262
|
+
return client;
|
|
263
|
+
})
|
|
264
|
+
.finally(() => {
|
|
265
|
+
this.connecting = undefined;
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
return this.connecting;
|
|
269
|
+
}
|
|
270
|
+
}
|