UNPKG

@juspay/neurolink

Version:

The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types — generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002

114 lines (113 loc) • 4.35 kB
/** * Redis-backed skill store. * * Reuses NeuroLink's pooled Redis client (`redis` v5 is already a core * dependency, shared with Redis conversation memory). One JSON value per * skill under `<keyPrefix><id>`; the index is derived with SCAN + MGET — * no separate index document to drift. Skills are persistent (no TTL). */ import { getNormalizedConfig, getPooledRedisClient, scanKeys, } from "../utils/redis.js"; import { logger } from "../utils/logger.js"; import { isSafeSkillResourcePath, toSkillIndexItem } from "./skillMatcher.js"; const DEFAULT_KEY_PREFIX = "neurolink:skills:"; export class RedisSkillStore { keyPrefix; /** * Resources live under `<keyPrefix>__resources__:` — a reserved segment * inside the skill prefix, explicitly excluded from the index SCAN so * resource values are never mistaken for skills, whatever the * configured keyPrefix looks like. */ resourcePrefix; normalizedConfig; clientPromise = null; constructor(config) { this.keyPrefix = config.keyPrefix ?? DEFAULT_KEY_PREFIX; this.resourcePrefix = `${this.keyPrefix}__resources__:`; this.normalizedConfig = getNormalizedConfig({ ...(config.url ? { url: config.url } : {}), ...(config.host ? { host: config.host } : {}), ...(config.port !== undefined ? { port: config.port } : {}), ...(config.username ? { username: config.username } : {}), ...(config.password ? { password: config.password } : {}), ...(config.db !== undefined ? { db: config.db } : {}), keyPrefix: this.keyPrefix, }); } getClient() { if (!this.clientPromise) { this.clientPromise = getPooledRedisClient(this.normalizedConfig).catch((error) => { // Reset so a later call can retry after a transient outage. this.clientPromise = null; throw error; }); } return this.clientPromise; } skillKey(id) { return `${this.keyPrefix}${id}`; } async getResource(id, resourcePath) { if (!isSafeSkillResourcePath(resourcePath)) { return null; } const client = await this.getClient(); const raw = await client.get(`${this.resourcePrefix}${id}:${resourcePath}`); return raw ? String(raw) : null; } async get(id) { const client = await this.getClient(); const raw = await client.get(this.skillKey(id)); return raw ? this.parseSkill(String(raw), this.skillKey(id)) : null; } async put(skill) { const client = await this.getClient(); // Persistent — deliberately no TTL: skills must not expire silently. await client.set(this.skillKey(skill.id), JSON.stringify(skill)); } async delete(id) { const client = await this.getClient(); await client.del(this.skillKey(id)); } async index() { const client = await this.getClient(); const keys = (await scanKeys(client, `${this.keyPrefix}*`)).filter((key) => !key.startsWith(this.resourcePrefix)); if (keys.length === 0) { return []; } const values = await client.mGet(keys); const items = []; values.forEach((value, i) => { if (!value) { return; } const skill = this.parseSkill(String(value), keys[i]); if (skill) { items.push(toSkillIndexItem(skill)); } }); return items; } parseSkill(raw, key) { try { const parsed = JSON.parse(raw); if (typeof parsed?.id === "string" && typeof parsed?.name === "string" && typeof parsed?.description === "string" && typeof parsed?.instructions === "string") { return parsed; } logger.warn("[SkillStoreRedis] Value is not a valid skill — skipping", { key, }); return null; } catch (error) { logger.warn("[SkillStoreRedis] Failed to parse skill value", { key, error: error instanceof Error ? error.message : String(error), }); return null; } } }