diff --git a/__tests__/ai/__snapshots__/models.test.js.snap b/__tests__/ai/__snapshots__/models.test.js.snap new file mode 100644 index 0000000000000000000000000000000000000000..24a010bd22e93652dea491a55ea1ba3ae9378d8d --- /dev/null +++ b/__tests__/ai/__snapshots__/models.test.js.snap @@ -0,0 +1,78 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`ai models > given no jwt sended > should respond with a proper body 1`] = ` +{ + "message": "No access token found. Access denied.", +} +`; + +exports[`ai models > given no valid jwt sended > should respond with a proper body 1`] = ` +{ + "message": "Access token is no longer valid. Access denied.", +} +`; + +exports[`ai models > given required fields are missing > should respond with a proper body 1`] = ` +{ + "message": "Validation errors. Please check the error messages.", + "validationErrors": { + "filter": "Required", + }, +} +`; + +exports[`ai models > given the inputs are valid > should respond with a proper body 1`] = ` +[ + { + "details": { + "families": [ + "llama", + ], + "family": "llama", + "format": "gguf", + "parameter_size": "8.0B", + "parent_model": "", + "quantization_level": "Q4_0", + }, + "digest": "365c0bd3c000a25d28ddbf732fe1c6add414de7275464c4e4d1c3b5fcb5d8ad1", + "model": "llama3:latest", + "modified_at": "2024-07-25T20:25:26.869024101+02:00", + "name": "llama3:latest", + "size": 4661224676, + }, + { + "details": { + "families": [ + "llama", + ], + "family": "llama", + "format": "gguf", + "parameter_size": "13B", + "parent_model": "", + "quantization_level": "Q4_0", + }, + "digest": "d475bf4c50bc4d29f333023e38cd56535039eec11052204e5304c8773cc8416c", + "model": "llama2:13b", + "modified_at": "2024-06-15T16:39:07.956494263+02:00", + "name": "llama2:13b", + "size": 7366821294, + }, + { + "details": { + "families": [ + "llama", + ], + "family": "llama", + "format": "gguf", + "parameter_size": "7B", + "parent_model": "", + "quantization_level": "Q4_0", + }, + "digest": "78e26419b4469263f75331927a00a0284ef6544c1975b826b15abdaef17bb962", + "model": "llama2:latest", + "modified_at": "2024-06-13T11:00:05.975159345+02:00", + "name": "llama2:latest", + "size": 3826793677, + }, +] +`; diff --git a/__tests__/ai/__snapshots__/status.test.js.snap b/__tests__/ai/__snapshots__/status.test.js.snap new file mode 100644 index 0000000000000000000000000000000000000000..e3c06f7f72ed09becc4b5331f6f2512544f9f5b5 --- /dev/null +++ b/__tests__/ai/__snapshots__/status.test.js.snap @@ -0,0 +1,13 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`ai status > given ai is not running > should respond with a proper body 1`] = ` +{ + "running": false, +} +`; + +exports[`ai status > given ai is running > should respond with a proper body 1`] = ` +{ + "running": true, +} +`; diff --git a/__tests__/ai/models.test.js b/__tests__/ai/models.test.js new file mode 100644 index 0000000000000000000000000000000000000000..204b7f6a09587a80840733b0ba8c9a0a3b8865ef --- /dev/null +++ b/__tests__/ai/models.test.js @@ -0,0 +1,259 @@ +// import vitest, supertest & app +import { vi, beforeAll, beforeEach, describe, expect, expectTypeOf, test, it, afterEach } from 'vitest'; +import supertest from "supertest"; +import app from "../../app.js"; +import jwt from 'jsonwebtoken'; + +// set route +const ROUTE = '/ai/models'; +// prepare response of each test +let response; + +// ############################ +// OBJECTS +// ############################ +const mockedVals = vi.hoisted(() => { + return { + foundUser: { + _id: '66a29da2942b3eb', + username: 'snoopy', + name: 'My User', + email: 'user@mail.local', + verified: true, + role: 0, + createdAt: '2024-07 - 25T18: 46: 58.982Z', + updatedAt: '2024-07 - 25T18: 46: 58.982Z', + __v: 0, + fullname: '', + id: '66a29da2942b3ebcaf047f07' + }, + foundModels: { + models: [ + { + "name": "llama3:latest", + "model": "llama3:latest", + "modified_at": "2024-07-25T20:25:26.869024101+02:00", + "size": 4661224676, + "digest": "365c0bd3c000a25d28ddbf732fe1c6add414de7275464c4e4d1c3b5fcb5d8ad1", + "details": { + "parent_model": "", + "format": "gguf", + "family": "llama", + "families": [ + "llama" + ], + "parameter_size": "8.0B", + "quantization_level": "Q4_0" + } + }, + { + "name": "orca2:13b", + "model": "orca2:13b", + "modified_at": "2024-06-15T16:53:37.368220025+02:00", + "size": 7365868139, + "digest": "a8dcfac3ac32d06f6241896d56928ac7b1d7a6e7f5dcc6b2aec69f2194a9f091", + "details": { + "parent_model": "", + "format": "gguf", + "family": "llama", + "families": null, + "parameter_size": "13B", + "quantization_level": "Q4_0" + } + }, + { + "name": "llama2:13b", + "model": "llama2:13b", + "modified_at": "2024-06-15T16:39:07.956494263+02:00", + "size": 7366821294, + "digest": "d475bf4c50bc4d29f333023e38cd56535039eec11052204e5304c8773cc8416c", + "details": { + "parent_model": "", + "format": "gguf", + "family": "llama", + "families": [ + "llama" + ], + "parameter_size": "13B", + "quantization_level": "Q4_0" + } + }, + { + "name": "starling-lm:latest", + "model": "starling-lm:latest", + "modified_at": "2024-06-15T16:12:29.439449821+02:00", + "size": 4108940286, + "digest": "39153f619be614bf1b8b91cf31afe53ec107d70b6b7bb4118aa52bccc107ca7e", + "details": { + "parent_model": "", + "format": "gguf", + "family": "llama", + "families": [ + "llama" + ], + "parameter_size": "7B", + "quantization_level": "Q4_0" + } + }, + { + "name": "llama2:latest", + "model": "llama2:latest", + "modified_at": "2024-06-13T11:00:05.975159345+02:00", + "size": 3826793677, + "digest": "78e26419b4469263f75331927a00a0284ef6544c1975b826b15abdaef17bb962", + "details": { + "parent_model": "", + "format": "gguf", + "family": "llama", + "families": [ + "llama" + ], + "parameter_size": "7B", + "quantization_level": "Q4_0" + } + }, + { + "name": "mistral:latest", + "model": "mistral:latest", + "modified_at": "2024-06-13T11:00:05.455160458+02:00", + "size": 4113301090, + "digest": "2ae6f6dd7a3dd734790bbbf58b8909a606e0e7e97e94b7604e0aa7ae4490e6d8", + "details": { + "parent_model": "", + "format": "gguf", + "family": "llama", + "families": [ + "llama" + ], + "parameter_size": "7.2B", + "quantization_level": "Q4_0" + } + }, + { + "name": "mxbai-embed-large:latest", + "model": "mxbai-embed-large:latest", + "modified_at": "2024-05-17T20:38:00.241769083+02:00", + "size": 669615493, + "digest": "468836162de7f81e041c43663fedbbba921dcea9b9fefea135685a39b2d83dd8", + "details": { + "parent_model": "", + "format": "gguf", + "family": "bert", + "families": [ + "bert" + ], + "parameter_size": "334M", + "quantization_level": "F16" + } + } + ] + }, + validInput: { + filter: 'llama' + } + }; +}); + +// ############################ +// MOCKS +// ############################ +// import Database Service +import * as dbService from '../../utils/handleDB.js'; +// mock dbService +vi.mock('../../utils/handleDB.js', async (importOriginal) => { + return { + ...await importOriginal(), + dbConnection: vi.fn(() => 'mocked'), + findOneRecord: vi.fn(() => mockedVals.foundUser), + }; +}); + +// mock aiService +vi.mock('../../utils/handleAI.js', async (importOriginal) => { + return { + ...await importOriginal(), + aiGetModels: vi.fn(() => mockedVals.foundModels) + }; +}); + +// ############################ +// TESTS +// ############################ + +describe('ai models', () => { + const _jwt = () => { + return jwt.sign({ id: mockedVals.foundUser.id, role: mockedVals.foundUser.role }, process.env.JWT_SECRET_KEY, { expiresIn: process.env.JWT_TTL }); + }; + + describe('given the inputs are valid', () => { + beforeAll(async () => { + response = await supertest(app) + .post(ROUTE) + .set('Authorization', `Bearer ${_jwt()}`) + .send(mockedVals.validInput); + }); + + it('should return a proper status code status', () => { + expect(response.status).toBe(200); + }); + it('should respond with a proper body', () => { + expect(response.body).toMatchSnapshot(); + }); + }); + + // ############################ + + describe('given required fields are missing', () => { + beforeAll(async () => { + const { filter, ...input } = mockedVals.validInput; + + response = await supertest(app) + .post(ROUTE) + .set('Authorization', `Bearer ${_jwt()}`) + .send(input); + }); + + it('should return a proper status code status', () => { + expect(response.status).toBe(400); + }); + it('should respond with a proper body', () => { + expect(response.body).toMatchSnapshot(); + }); + }); + + // ############################ + + describe('given no valid jwt sended', () => { + beforeAll(async () => { + response = await supertest(app) + .post(ROUTE) + .set('Authorization', `Bearer invalid`) + .send(mockedVals.validInput); + }); + + it('should return a proper status code status', () => { + expect(response.status).toBe(403); + }); + it('should respond with a proper body', () => { + expect(response.body).toMatchSnapshot(); + }); + }); + + // ############################ + + describe('given no jwt sended', () => { + beforeAll(async () => { + response = await supertest(app) + .post(ROUTE) + .send(mockedVals.validInput); + }); + + it('should return a proper status code status', () => { + expect(response.status).toBe(401); + }); + it('should respond with a proper body', () => { + expect(response.body).toMatchSnapshot(); + }); + }); + +}); \ No newline at end of file diff --git a/__tests__/ai/status.test.js b/__tests__/ai/status.test.js index c840eaf89b029ffc55be76b1f2e6683ee9f26c50..94a9a75387c8bd35e67b4f716dfa93da991d85ba 100644 --- a/__tests__/ai/status.test.js +++ b/__tests__/ai/status.test.js @@ -14,19 +14,17 @@ let response; const mockedVals = vi.hoisted(() => { return { foundUser: { - _doc: { - _id: '66a29da2942b3eb', - username: 'snoopy', - name: 'My User', - email: 'user@mail.local', - verified: true, - role: 0, - createdAt: '2024-07 - 25T18: 46: 58.982Z', - updatedAt: '2024-07 - 25T18: 46: 58.982Z', - __v: 0, - fullname: '', - id: '66a29da2942b3ebcaf047f07' - } + _id: '66a29da2942b3eb', + username: 'snoopy', + name: 'My User', + email: 'user@mail.local', + verified: true, + role: 0, + createdAt: '2024-07 - 25T18: 46: 58.982Z', + updatedAt: '2024-07 - 25T18: 46: 58.982Z', + __v: 0, + fullname: '', + id: '66a29da2942b3ebcaf047f07' } }; }); diff --git a/__tests__/auth/confirmverification.test.js b/__tests__/auth/confirmverification.test.js index 43041e1e997e7d5d9e8628ce1c68df5936272c8d..c97f68577b17b3476f8c1798721d23f2e8ba546e 100644 --- a/__tests__/auth/confirmverification.test.js +++ b/__tests__/auth/confirmverification.test.js @@ -15,18 +15,16 @@ let response; const mockedVals = vi.hoisted(() => { return { foundUser: { - _doc: { - _id: '66a29da2942b3eb', - username: 'snoopy', - name: 'My User', - email: 'user@mail.local', - verified: true, - role: 0, - createdAt: '2024-07 - 25T18: 46: 58.982Z', - updatedAt: '2024-07 - 25T18: 46: 58.982Z', - __v: 0, - id: '66a29da2942b3ebcaf047f07' - } + _id: '66a29da2942b3eb', + username: 'snoopy', + name: 'My User', + email: 'user@mail.local', + verified: true, + role: 0, + createdAt: '2024-07 - 25T18: 46: 58.982Z', + updatedAt: '2024-07 - 25T18: 46: 58.982Z', + __v: 0, + id: '66a29da2942b3ebcaf047f07' }, validInput: { email: 'user@mail.local', diff --git a/__tests__/auth/requestverification.test.js b/__tests__/auth/requestverification.test.js index 6e294411dd95cafe77486d8aa47ca6bfe308f246..a113df2a49e21fb8c35ba90b459dedd993b0039b 100644 --- a/__tests__/auth/requestverification.test.js +++ b/__tests__/auth/requestverification.test.js @@ -13,18 +13,16 @@ let response; const mockedVals = vi.hoisted(() => { return { foundUser: { - _doc: { - _id: '66a29da2942b3eb', - username: 'snoopy', - name: 'My User', - email: 'user@mail.local', - verified: true, - role: 0, - createdAt: '2024-07 - 25T18: 46: 58.982Z', - updatedAt: '2024-07 - 25T18: 46: 58.982Z', - __v: 0, - id: '66a29da2942b3ebcaf047f07' - } + _id: '66a29da2942b3eb', + username: 'snoopy', + name: 'My User', + email: 'user@mail.local', + verified: true, + role: 0, + createdAt: '2024-07 - 25T18: 46: 58.982Z', + updatedAt: '2024-07 - 25T18: 46: 58.982Z', + __v: 0, + id: '66a29da2942b3ebcaf047f07' }, validInput: { email: 'user@mail.local' diff --git a/__tests__/manualREST/ollama.rest b/__tests__/manualREST/ollama.rest index 502bf9eb366ba50d6a02ca8f33aba8feeb445381..c3484fb5713daa15a99b1d134cfe36af51fb9f7e 100644 --- a/__tests__/manualREST/ollama.rest +++ b/__tests__/manualREST/ollama.rest @@ -48,7 +48,7 @@ Accept: application/json Content-Type: application/json { - "filter": "lla" + "filter": "bai" } ### show info of a specific model diff --git a/app.js b/app.js index 30a8cd13d7828bf4c2eace5c979c8db37327e5e2..cc9b40deaadda231f38d8bf40235be5a16d5ee49 100644 --- a/app.js +++ b/app.js @@ -11,6 +11,7 @@ const db = dbConnection(); // create superadmin on first run const isSuperAdminAvailable = await findOneRecord(User, { email: process.env.SUPERADMIN_EMAIL }); + if (!isSuperAdminAvailable) { const newSuperAdmin = await createRecord(User, { name: 'RAGChat Admin', diff --git a/controllers/AI.js b/controllers/AI.js index 822b066d9e048f634e8525e6fc7e07b5cbd0ce50..660101399cd7a644f5a0c43a91048c6aee46b38e 100644 --- a/controllers/AI.js +++ b/controllers/AI.js @@ -1,6 +1,6 @@ import { Ollama } from 'ollama'; import Chat from "../models/Chat.js"; -import { aiDeleteModel, aiFilterModelsByName, aiGetModel, aiInstallModel, aiIsRunning, summarizeText } from "../utils/handleAI.js"; +import { aiDeleteModel, aiGetModels, aiGetModel, aiInstallModel, aiIsRunning, summarizeText } from "../utils/handleAI.js"; import { mapStoredMessagesToChatMessages } from "@langchain/core/messages"; import { createRecord, findOneRecord, findRecords } from '../utils/handleDB.js'; import { prefillDocumentObject } from '../utils/handleSchemes.js'; @@ -29,6 +29,10 @@ export const getStatus = async (req, res, next) => { export const getModels = async (req, res, next) => { try { const foundModels = await aiFilterModelsByName(req.body.filter); + + console.log("🚀 ~ getModels ~ foundModels:", foundModels); + + return res.json(foundModels); } catch (error) { next(error); @@ -154,6 +158,24 @@ export const checkRequestedModel = async (req, res, next) => { next(); }; +/** ******************************************************* + * FILTER AVAILABLE MODELS BY NAME + */ +export const aiFilterModelsByName = async (strFilter = '') => { + try { + // fetch all available models + const avail = await aiGetModels(); + + // return all if no regex query provided + if (strFilter === '') return avail; + // set regex query + const regex = new RegExp(strFilter, 'i'); + // filter models by regex query + return avail.models.filter((model) => regex.test(model.name)); + } catch (error) { + throw error; + } +}; diff --git a/controllers/Auth.js b/controllers/Auth.js index 4c098abd0c945ca593bd087ef53f5df7f800966d..d184d4e915895cb05ef78e21bc3f3b76a8ffc2a8 100644 --- a/controllers/Auth.js +++ b/controllers/Auth.js @@ -59,8 +59,6 @@ export const login = async (req, res, next) => { // console.log("🚀 ~ login ~ passwords:", req.body.password, foundUser.password); - - // wrong login name if (!foundUser) { return res.status(401).json({ message: 'Unknown combination of login credentials.' }); diff --git a/controllers/Embeddings.js b/controllers/Embeddings.js index 01e701bbef55afd67dbef5bc5e081bc228d7ec81..9e058b436c55c6a78978b1d756304971f4bd119d 100644 --- a/controllers/Embeddings.js +++ b/controllers/Embeddings.js @@ -1,6 +1,6 @@ import { Ollama } from 'ollama'; -import { aiFilterModelsByName } from "../utils/handleAI.js"; +import { aiFilterModelsByName } from "../controllers/AI.js"; import { ChromaClient } from "chromadb"; // embeddings import { Chroma } from "@langchain/community/vectorstores/chroma"; diff --git a/logs/__tests__/ai/models.test.js b/logs/__tests__/ai/models.test.js deleted file mode 100644 index c863f785ed1347945240bd588a60e55f031bc4a3..0000000000000000000000000000000000000000 --- a/logs/__tests__/ai/models.test.js +++ /dev/null @@ -1,135 +0,0 @@ -// import vitest, supertest & app -import { vi, beforeAll, beforeEach, describe, expect, expectTypeOf, test, it, afterEach } from 'vitest'; -import supertest from "supertest"; -import app from "../../app.js"; -// ignore expiration of the (self-signed) certificate -process.env.NODE_TLS_REJECT_UNAUTHORIZED = 0; -// set timeout -const BEFORE_ALL_TIMEOUT = 30000; // 30 sec -// set route -const ROUTE = '/ai/models'; -// prepare response of each test -let response; - -// ############################ -// OBJECTS -// ############################ -const modelsFoundResponse = [ - { - "name": "llama3:latest", - "model": "llama3:latest", - "modified_at": "2024-06-23T16:55:46.525847141+02:00", - "size": 4661224676, - "digest": "365c0bd3c000a25d28ddbf732fe1c6add414de7275464c4e4d1c3b5fcb5d8ad1", - "details": { - "parent_model": "", - "format": "gguf", - "family": "llama", - "families": [ - "llama" - ], - "parameter_size": "8.0B", - "quantization_level": "Q4_0" - } - }, - { - "name": "orca2:13b", - "model": "orca2:13b", - "modified_at": "2024-06-15T16:53:37.368220025+02:00", - "size": 7365868139, - "digest": "a8dcfac3ac32d06f6241896d56928ac7b1d7a6e7f5dcc6b2aec69f2194a9f091", - "details": { - "parent_model": "", - "format": "gguf", - "family": "llama", - "families": null, - "parameter_size": "13B", - "quantization_level": "Q4_0" - } - }]; - -// ############################ -// MOCKS -// ############################ -// import PocketBase Service -import * as pbService from '../../utils/pocketbase/handlePocketBase.js'; -// mock pbService -vi.mock('../../utils/pocketbase/handlePocketBase.js', async (importOriginal) => { - return { - ...await importOriginal(), - pbVerifyAccessToken: vi.fn().mockImplementation((req, res, next) => { - next(); - }), - aiFilterModelsByName: vi.fn(() => modelsFoundResponse) - }; -}); - -// import AI Service -import * as aiService from '../../utils/handleAI.js'; -// const spyIsRunning = vi.spyOn(aiService, 'isRunning'); -// mock aiService -vi.mock('../../utils/handleAI.js', async (importOriginal) => { - return { - ...await importOriginal(), - aiFilterModelsByName: vi.fn(() => modelsFoundResponse) - }; -}); - -// ############################ -// TESTS -// ############################ - -describe('ai models', () => { - describe('given the inputs are valid', () => { - beforeAll(async () => { - response = await supertest(app) - .post(ROUTE) - .send({ filter: 'llama' }); - }, BEFORE_ALL_TIMEOUT); - it('should call required mocks', () => { - expect(aiService.aiFilterModelsByName()).toEqual(modelsFoundResponse); - }); - it('should return a proper status code', () => { - expect(response.status).toBe(200); - }); - it('should respond with matching models', () => { - expect(response.body).toEqual(modelsFoundResponse); - }); - }); - - // ############################ - - describe('given no valid JWT sent', () => { - beforeAll(async () => { - pbService.pbVerifyAccessToken.mockImplementationOnce((req, res, next) => { - return res.status(403).json({ message: 'You are not logged in.' }); - }); - - response = await supertest(app) - .post(ROUTE) - .send({ filter: 'validRegex' }); - }, BEFORE_ALL_TIMEOUT); - it('should return a proper status code', () => { - expect(response.status).toBe(403); - }); - it('should respond with a proper message', () => { - expect(response.body.message).toEqual('You are not logged in.'); - }); - }); - - // ############################ - - describe('given no filter sent', () => { - beforeAll(async () => { - response = await supertest(app) - .post(ROUTE); - }, BEFORE_ALL_TIMEOUT); - it('should return a proper status code', () => { - expect(response.status).toBe(400); - }); - it('should respond with a proper message', () => { - expect(response.body.validationErrors.filter).toEqual('Required'); - }); - }); - -}); \ No newline at end of file diff --git a/utils/handleAI.js b/utils/handleAI.js index fbedfbc0ecf6c9bdd0ebabc62053052709375701..1771ae7d19d52a870c16471fda743ef2dd8ab089 100644 --- a/utils/handleAI.js +++ b/utils/handleAI.js @@ -28,20 +28,11 @@ export const aiIsRunning = async () => { /** ******************************************************* * FILTER INSTALLED MODELS BY NAME VIA REGEX */ -export const aiFilterModelsByName = async (strFilter = '') => { - // fetch all available models - const avail = await ollama.list(); - // return all if no regex query provided - if (strFilter === '') return avail.models; - +export const aiGetModels = async () => { try { - // set regex query - const regex = new RegExp(strFilter, 'i'); - // filter models by regex query - return avail.models.filter((model) => regex.test(model.name)); + return await ollama.list(); } catch (error) { - // on error return all models - return avail.models; + throw error; } };