@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.
Files changed (39) hide show
  1. package/CHANGELOG.md +3 -3
  2. package/dist/artifacts/artifactBanking.d.ts +6 -2
  3. package/dist/artifacts/artifactBanking.js +9 -14
  4. package/dist/artifacts/artifactReader.d.ts +69 -0
  5. package/dist/artifacts/artifactReader.js +157 -0
  6. package/dist/artifacts/artifactStore.d.ts +7 -3
  7. package/dist/artifacts/artifactStore.js +10 -21
  8. package/dist/artifacts/artifactStoreFactory.d.ts +39 -0
  9. package/dist/artifacts/artifactStoreFactory.js +101 -0
  10. package/dist/artifacts/redisArtifactStore.d.ts +84 -0
  11. package/dist/artifacts/redisArtifactStore.js +270 -0
  12. package/dist/browser/neurolink.min.js +381 -383
  13. package/dist/constants/enums.d.ts +77 -0
  14. package/dist/constants/enums.js +82 -0
  15. package/dist/core/modules/GenerationHandler.d.ts +15 -3
  16. package/dist/core/modules/GenerationHandler.js +172 -14
  17. package/dist/index.d.ts +3 -0
  18. package/dist/index.js +7 -0
  19. package/dist/memory/memoryRetrievalTools.js +104 -35
  20. package/dist/neurolink.d.ts +45 -1
  21. package/dist/neurolink.js +128 -18
  22. package/dist/providers/catalog/baseten.json +263 -0
  23. package/dist/providers/catalog/gmicloud.json +64 -0
  24. package/dist/providers/catalog/inception-labs.json +75 -0
  25. package/dist/providers/catalog/index.generated.d.ts +1 -1
  26. package/dist/providers/catalog/index.generated.js +15 -0
  27. package/dist/providers/catalog/io-intelligence.json +463 -0
  28. package/dist/providers/catalog/schema.d.ts +1 -1
  29. package/dist/providers/catalog/upstage.json +165 -0
  30. package/dist/providers/openaiChatCompletionsClient.js +18 -1
  31. package/dist/types/artifact.d.ts +124 -3
  32. package/dist/types/config.d.ts +7 -0
  33. package/dist/types/generate.d.ts +17 -0
  34. package/dist/types/openaiCompatible.d.ts +5 -1
  35. package/dist/types/providerCatalog.generated.d.ts +2 -2
  36. package/dist/types/providers.d.ts +20 -0
  37. package/dist/utils/redis.d.ts +15 -0
  38. package/dist/utils/redis.js +64 -6
  39. 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
+ }