Skip to content
Snippets Groups Projects
Commit ef6fc4a5 authored by Embruch, Gerd's avatar Embruch, Gerd
Browse files

finished testing route ai/models

parent d2a506ea
Branches
No related tags found
No related merge requests found
// 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,
},
]
`;
// 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,
}
`;
// 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
......@@ -14,7 +14,6 @@ let response;
const mockedVals = vi.hoisted(() => {
return {
foundUser: {
_doc: {
_id: '66a29da2942b3eb',
username: 'snoopy',
name: 'My User',
......@@ -27,7 +26,6 @@ const mockedVals = vi.hoisted(() => {
fullname: '',
id: '66a29da2942b3ebcaf047f07'
}
}
};
});
......
......
......@@ -15,7 +15,6 @@ let response;
const mockedVals = vi.hoisted(() => {
return {
foundUser: {
_doc: {
_id: '66a29da2942b3eb',
username: 'snoopy',
name: 'My User',
......@@ -26,7 +25,6 @@ const mockedVals = vi.hoisted(() => {
updatedAt: '2024-07 - 25T18: 46: 58.982Z',
__v: 0,
id: '66a29da2942b3ebcaf047f07'
}
},
validInput: {
email: 'user@mail.local',
......
......
......@@ -13,7 +13,6 @@ let response;
const mockedVals = vi.hoisted(() => {
return {
foundUser: {
_doc: {
_id: '66a29da2942b3eb',
username: 'snoopy',
name: 'My User',
......@@ -24,7 +23,6 @@ const mockedVals = vi.hoisted(() => {
updatedAt: '2024-07 - 25T18: 46: 58.982Z',
__v: 0,
id: '66a29da2942b3ebcaf047f07'
}
},
validInput: {
email: 'user@mail.local'
......
......
......@@ -48,7 +48,7 @@ Accept: application/json
Content-Type: application/json
{
"filter": "lla"
"filter": "bai"
}
### show info of a specific model
......
......
......@@ -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',
......
......
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;
}
};
......@@ -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.' });
......
......
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";
......
......
// 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
......@@ -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;
}
};
......
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please to comment