UNPKG

@aquiles-ai/aquiles-rag-js

Version:

Aquiles-RAG-JS variant made in Fastify to continue the high-performance philosophy of the Python version with FastAPI

445 lines (386 loc) 10.4 kB
import Fastify from 'fastify'; import RedisConPl from './plugins/redis.js'; import swagger from '@fastify/swagger'; import swaggerUI from '@fastify/swagger-ui'; import cors from '@fastify/middie'; import { loadAquilesConfig } from './configs/configs.js'; import { SCHEMA_FIELD_TYPE, SCHEMA_VECTOR_FIELD_ALGORITHM, createClient } from 'redis'; const fastify = Fastify({ logger: true, ajv: { customOptions: { allErrors: true} } }) await fastify.register(swagger, { openapi: { openapi: '3.0.0', info: {title: 'Aquiles-RAG-js', version: '0.1.0'} } }) fastify.setErrorHandler((error, request, reply) => { if (error.validation) { fastify.log.error({ validation: error.validation }, 'AJV validation failed') } reply.send(error) }) await fastify.register(RedisConPl) await fastify.register(cors, { origin: '*', methods: ['GET','POST'], }) await fastify.register(swaggerUI, { routePrefix: '/docs', uiConfig: { docExpansion: 'list' } }) fastify.decorate('verifyApiKey', async (request, reply) => { const configs = loadAquilesConfig() const validKeys = (configs.allows_api_keys || []) .filter(k => k && k.trim()) if (validKeys.length === 0) { return } const apiKey = request.headers['x-api-key'] if (!apiKey) { reply .code(403) .send({ detail: 'API key missing' }) return reply } if (!validKeys.includes(apiKey)) { reply .code(403) .send({ detail: 'Invalid API key' }) return reply } return }) fastify.get('/', async function(request, reply) { reply.send({hello: 'Hey this is the JS version of Aquiles-RAG'}) }) fastify.post('/create/index', { schema: { body: { type: 'object', required: ['indexname', 'embeddings_dim', 'dtype', 'delete_the_index_if_it_exists'], properties: { indexname: { type: 'string' }, embeddings_dim: { type: 'number' }, dtype: { type: 'string', enum: ['FLOAT32','FLOAT16','FLOAT64'] }, delete_the_index_if_it_exists: { type: 'boolean' }, } }, response: { 200: { type: 'object', properties: { status: { type: 'string' }, index: { type: 'string' }, fields: { type: 'array', items: { type: 'string' } } } } } }, preHandler: fastify.verifyApiKey, }, async (request, reply) => { const { indexname, embeddings_dim, dtype, delete_the_index_if_it_exists } = request.body; const client = fastify.redis; let info = null; try { info = await client.ft.info(indexname); } catch (_) { /* no existe */ } if (info && !delete_the_index_if_it_exists) { return reply .code(400) .send({ error: `Index '${indexname}' already exists. Set delete_the_index_if_it_exists=true to overwrite.` }); } if (info && delete_the_index_if_it_exists) { await client.ft.dropIndex(indexname, { DD: false }); } const schema = { name_chunk: { type: SCHEMA_FIELD_TYPE.TEXT, SORTABLE: true }, chunk_id: { type: SCHEMA_FIELD_TYPE.NUMERIC, SORTABLE: true }, chunk_size: { type: SCHEMA_FIELD_TYPE.NUMERIC, SORTABLE: true }, raw_text: { type: SCHEMA_FIELD_TYPE.TEXT }, embedding: { type: SCHEMA_FIELD_TYPE.VECTOR, TYPE: dtype, ALGORITHM: SCHEMA_VECTOR_FIELD_ALGORITHM.HNSW, DISTANCE_METRIC: 'COSINE', DIM: embeddings_dim, INITIAL_CAP: 400, M: 16, EF_CONSTRUCTION: 200, EF_RUNTIME: 100 } }; await client.ft.create( indexname, schema, { ON: 'HASH', PREFIX: `${indexname}:` } ); return { status: 'success', index: indexname, fields: Object.keys(schema) }; }); fastify.post('/rag/create', { schema:{ body: { type: 'object', required: ['index', 'name_chunk', 'dtype', 'chunk_size', 'raw_text', 'embeddings'], properties: { index: { type: 'string' }, name_chunk: { type: 'string' }, dtype: { type: 'string', enum: ['FLOAT32','FLOAT16','FLOAT64'] }, chunk_size: { type: 'number' }, raw_text: { type: 'string' }, embeddings: { type: 'array', items: { type: 'number' } } } }, response: { 200: { type: 'object', properties: { status: { type: 'string' }, key: { type: 'string' } } } } }, preHandler: fastify.verifyApiKey, }, async (request, reply) => { const { index, name_chunk, dtype, chunk_size, raw_text, embeddings } = request.body; const client = fastify.redis; let typedArray; switch (dtype){ case 'FLOAT32': typedArray = new Float32Array(embeddings); break; case 'FLOAT16': typedArray = new Uint16Array(embeddings); break; case 'FLOAT64': typedArray = new Float64Array(embeddings); break; default: throw new Error(`dtype ${dtype} not supported`); } const embBytes = Buffer.from(typedArray.buffer); const new_id = await client.incr(`${index}:next_id`); const key = `${index}:${new_id}`; const mapping = { name_chunk: name_chunk, chunk_id: new_id, chunk_size: chunk_size, raw_text: raw_text, embedding: embBytes }; try{ await client.hSet(key, mapping); return {status: 'ok', key: key}; } catch(error){ return reply .code(500) .send({ error: `Error saving chunk: '${error}'` }); } }); fastify.post('/rag/query-rag', { schema:{ body: { type: 'object', required: ['index', 'embeddings', 'dtype', 'top_k', 'cosine_distance_threshold'], properties: { index: { type: 'string' }, embeddings: { type: 'array', items: { type: 'number' } }, dtype: { type: 'string', enum: ['FLOAT32','FLOAT16','FLOAT64'] }, top_k: { type: 'number' }, cosine_distance_threshold: { type: 'number' } } }, response: { 200: { type: 'object', properties: { status: { type: 'string' }, total: { type: 'number' }, results: { type: 'array', items: { type: 'object', properties: { name_chunk: { type: 'string' }, chunk_id: { type: 'number' }, chunk_size: { type: 'number' }, raw_text: { type: 'string' }, score: { type: 'number' } }, required: ['name_chunk','chunk_id','chunk_size','raw_text','score'] } } }, required: ['status','total','results'] } } }, preHandler: fastify.verifyApiKey, }, async (request, reply) => { const { index, embeddings, dtype, top_k, cosine_distance_threshold } = request.body; const client = fastify.redis; let typedArray; switch (dtype){ case 'FLOAT32': typedArray = new Float32Array(embeddings); break; case 'FLOAT16': typedArray = new Uint16Array(embeddings); break; case 'FLOAT64': typedArray = new Float64Array(embeddings); break; default: throw new Error(`dtype ${dtype} not supported`); } const embBytes = Buffer.from(typedArray.buffer); let similar; try { similar = await client.ft.search( index, `*=>[KNN ${top_k} @embedding $vec AS score]`, { PARAMS: { vec: embBytes }, RETURN: [ 'name_chunk', 'chunk_id', 'chunk_size', 'raw_text', 'score' ], DIALECT: '2' }); } catch(error){ return reply .code(500) .send({ error: `Search error: '${error}'` }); } let docs = similar.documents.map(d => ({ name_chunk: d.value.name_chunk, chunk_id: Number(d.value.chunk_id), chunk_size: Number(d.value.chunk_size), raw_text: d.value.raw_text, score: Number(d.value.score) })); if (cosine_distance_threshold != null) { docs = docs.filter(d => d.score <= cosine_distance_threshold); } docs = docs.slice(0, top_k); return reply.code(200).send({ status: 'ok', total: docs.length, results: docs }); }); fastify.post('/rag/drop_index', { schema:{ body: { type: 'object', required: ['index_name', 'delete_docs'], properties: { index_name: { type: 'string' }, delete_docs: { type: 'boolean' } } }, response: { 200: { type: 'object', properties: { status: { type: 'string' }, drop_index: { type: 'string' }, }, } } }, preHandler: fastify.verifyApiKey, }, async (request, reply) => { const { index_name, delete_docs } = request.body; const client = fastify.redis; let res; try{ if (delete_docs){ res = await client.ft.dropIndex(index_name, { DD: true }); } else{ res = await client.ft.dropIndex(index_name); } return reply.code(200).send({ status: res, drop_index: index_name }); } catch(error){ return reply .code(500) .send({ error: `Delete error: '${error}'` }); } }); fastify.get('/health/live', { schema:{ response: { 200: { type: 'object', properties: { status: { type: 'string' }, }, } } }, }, async (request, reply) => { return reply.code(200).send({ status: 'alive', }); }) fastify.get('/health/ready', { schema:{ response: { 200: { type: 'object', properties: { status: { type: 'string' }, }, } } }, }, async (request, reply) => { const client = fastify.redis; try{ await client.ping(); return reply.code(200).send({ status: 'ready', }); } catch(error){ return reply.code(503).send({status: "Redis unavailable"}); } }); fastify.listen({ port: 3000, host: '0.0.0.0' }, function (err, address) { if (err) { fastify.log.error(err) process.exit(1) } }); export default fastify;