From 222e579a3f2c37a34d9aa2abc5cc3ec35e23f962 Mon Sep 17 00:00:00 2001 From: ClaudioMartinH <160405164+ClaudioMartinH@users.noreply.github.com> Date: Sun, 24 Nov 2024 22:44:12 +0100 Subject: [PATCH] Controler and testing for new endpoint --- services/wiki/.env | 2 + .../__tests__/resources/description.test.ts | 82 +++++++++++++++++++ .../resources/generateDescription.ts | 59 +++++++++++++ .../wiki/src/helpers/wiki/getLanguageInput.ts | 17 ++++ .../routes/resources/generateDescription.ts | 76 +++++++++++++++++ services/wiki/src/routes/resourcesRouter.ts | 8 ++ .../resource/generateDescriptionSchema.ts | 13 +++ 7 files changed, 257 insertions(+) create mode 100644 services/wiki/src/__tests__/resources/description.test.ts create mode 100644 services/wiki/src/controllers/resources/generateDescription.ts create mode 100644 services/wiki/src/helpers/wiki/getLanguageInput.ts create mode 100644 services/wiki/src/openapi/routes/resources/generateDescription.ts create mode 100644 services/wiki/src/schemas/resource/generateDescriptionSchema.ts diff --git a/services/wiki/.env b/services/wiki/.env index 257b1657a..bc6721898 100644 --- a/services/wiki/.env +++ b/services/wiki/.env @@ -9,3 +9,5 @@ DB_PASS=123456 DB_PORT=10911 DB_USER=postgres DB_SCHEMA=public + +HUGGINGFACE_API_KEY=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 diff --git a/services/wiki/src/__tests__/resources/description.test.ts b/services/wiki/src/__tests__/resources/description.test.ts new file mode 100644 index 000000000..f30b5d227 --- /dev/null +++ b/services/wiki/src/__tests__/resources/description.test.ts @@ -0,0 +1,82 @@ +import supertest from 'supertest' +import { describe, it, expect } from 'vitest' +import { server } from '../globalSetup' +import { pathRoot } from '../../routes/routes' +import { authToken } from '../mocks/ssoHandlers/authToken' +import { checkInvalidToken } from '../helpers/checkInvalidToken' + +const url: string = `${pathRoot.v1.resources}/generate-description` +const urlToTest: string = 'http://www.example.com' +const topic: string = 'Testing topic' +const title: string = 'Test title' + +describe('Resources Generate Description', () => { + it('responds with a 200 status code and a description', async () => { + const response = await supertest(server) + .post(`${url}?language=en`) + .set('Cookie', [`authToken=${authToken.admin}`]) + .send({ url: urlToTest, topic, title }) + expect(response.status).toBe(200) + expect(response.body).toHaveProperty('description') + }) + it('should fail is language param is missing', async () => { + const response = await supertest(server) + .post(`${url}`) + .set('Cookie', [`authToken=${authToken.admin}`]) + .send({ url: urlToTest, title, topic }) + + expect(response.status).toBe(400) + expect(response.body.error).toBe('Language parameter is required') + }) + it('should fail with missing params (title, url, topic)', async () => { + const response = await supertest(server) + .post(`${url}?language=en`) + .set('Cookie', [`authToken=${authToken.admin}`]) + .send({}) + + expect(response.status).toBe(400) + // expect(response.body.error).toBe('All parameters are required') + }) + + it('should fail if missing title', async () => { + const response = await supertest(server) + .post(`${url}?language=en`) + .set('Cookie', [`authToken=${authToken.admin}`]) + .send({ url: 'http://example.com', topic: 'Testing topic' }) + + expect(response.status).toBe(400) + // expect(response.body.error).toBe('All parameters are required') + }) + + it('should fail if missing url', async () => { + const response = await supertest(server) + .post(`${url}?language=en`) + .set('Cookie', [`authToken=${authToken.admin}`]) + .send({ title: 'Test Title', topic: 'Testing topic' }) + + expect(response.status).toBe(400) + // expect(response.body.error).toBe('All parameters are required') + }) + + it('should fail if missing topic', async () => { + const response = await supertest(server) + .post(`${url}?language=en`) + .set('Cookie', [`authToken=${authToken.admin}`]) + .send({ title: 'Test Title', url: 'http://example.com' }) + + expect(response.status).toBe(400) + expect(response.body.error).toBe('All parameters are required') + }) + it('Should return error 401 if no token is provided', async () => { + const response = await supertest(server) + .post(`${pathRoot.v1.resources}`) + .send({ url: urlToTest, title, topic }) + expect(response.status).toBe(401) + expect(response.body.message).toBe('Missing token') + }) + checkInvalidToken(`${pathRoot.v1.resources}`, 'post', { + url: urlToTest, + title, + topic, + }) +}) diff --git a/services/wiki/src/controllers/resources/generateDescription.ts b/services/wiki/src/controllers/resources/generateDescription.ts new file mode 100644 index 000000000..4412cfee0 --- /dev/null +++ b/services/wiki/src/controllers/resources/generateDescription.ts @@ -0,0 +1,59 @@ +import Koa, { Middleware } from 'koa' +import { getLanguageInput } from '../../helpers/wiki/getLanguageInput' + +export const generateDescription: Middleware = async (ctx: Koa.Context) => { + const { title, url, topic } = ctx.request.body + const { language } = ctx.query + + try { + if (!language) { + ctx.status = 400 + ctx.body = { + error: 'Language parameter is required', + } + return + } + + if (!title || !url || !topic) { + ctx.status = 400 + ctx.body = { + error: 'All parameters are required', + } + return + } + const input = getLanguageInput(language as string, title, url, topic) + + const response = await fetch( + 'https://api-inference.huggingface.co/models/Qwen/Qwen2.5-Coder-32B-Instruct', + { + method: 'POST', + headers: { + Authorization: `Bearer ${process.env.HUGGINGFACE_API_KEY}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + inputs: input, + parameters: { + max_length: 450, + temperature: 0.7, + top_p: 0.95, + }, + }), + } + ) + if (!response.ok) { + ctx.status = 400 + ctx.body = { + error: 'Error fetching data from external API', + } + return + } + + const description = await response.json() + ctx.status = 200 + ctx.body = { description: description[0]?.generated_text.trim() } + } catch (error) { + ctx.status = 500 + ctx.body = { message: 'An error occured while getting the description' } + } +} diff --git a/services/wiki/src/helpers/wiki/getLanguageInput.ts b/services/wiki/src/helpers/wiki/getLanguageInput.ts new file mode 100644 index 000000000..0ef4ffa1c --- /dev/null +++ b/services/wiki/src/helpers/wiki/getLanguageInput.ts @@ -0,0 +1,17 @@ +export const getLanguageInput = ( + language: string, + title: string, + url: string, + topic: string +): string => { + switch (language) { + case 'en': + return `Please provide a detailed summary of the following resource ${title}, including the key points, the main purpose, and the most relevant concepts. Use a clear and accessible tone. The resource can be found at ${url}, and its topic is ${topic}. The summary should be between 200 and 300 words. Return just the summary.` + case 'es': + return `Por favor, proporciona una resumen detallado de la siguiente fuente ${title}, incluyendo los puntos clave, el propósito principal y los conceptos relevantes. Usa un tono claro y accesible. La fuente puede ser encontrada en ${url}, y su tema es ${topic}. El resumen debe estar entre 200 y 300 palabras.` + case 'ca': + return `Si us plau, porporciona un resum detallat de la següent font ${title}, incloent els punts clau, el propòsit principal i els conceptes més rellevants. Empra un to clar i accesible. La font es pot trobar a ${url}, i el seu tema és ${topic}. El resum ha de tenir entre 200 a 300 paraules.` + default: + throw new Error('Unsupported language') + } +} diff --git a/services/wiki/src/openapi/routes/resources/generateDescription.ts b/services/wiki/src/openapi/routes/resources/generateDescription.ts new file mode 100644 index 000000000..77bedb703 --- /dev/null +++ b/services/wiki/src/openapi/routes/resources/generateDescription.ts @@ -0,0 +1,76 @@ +import z from 'zod' +import { pathRoot } from '../../../routes/routes' +import { cookieAuth } from '../../components/cookieAuth' +import { generateDescriptionSchema } from '../../../schemas/resource/generateDescriptionSchema' +import { registry } from '../../registry' +import { ZodValidationError } from '../../components/errorSchemas' +import { invalidTokenResponse } from '../../components/responses/authMiddleware' + +registry.registerPath({ + method: 'post', + tags: ['resources'], + path: `${pathRoot.v1.resources}/generate-description`, + operationId: 'postResourcesGenerateDescription', + description: + 'Allows an authenticated user to generate a description for a resource. Requires language as a query parameter and title, url, and topic in the request body.', + security: [{ [cookieAuth.name]: [] }], + request: { + query: z.object({ + language: z.string().min(2).max(2).optional().openapi({ + example: 'es', + }), + }), + body: { + content: { + 'application/json': { + schema: generateDescriptionSchema, + }, + }, + }, + }, + responses: { + 200: { + description: 'A description for the resource is generated.', + content: { + 'application/json': { + schema: z.object({ + description: z.string(), + }), + }, + }, + }, + 400: { + description: 'Error fetching data from external API', + content: { + 'application/json': { + schema: z.object({ + error: z.string().openapi({ + example: 'Error fetching data from external API', + }), + }), + }, + }, + }, + 401: invalidTokenResponse, + 422: { + description: 'Missing required parameters', + content: { + 'application/json': { + schema: ZodValidationError, + }, + }, + }, + 500: { + description: 'An error occured while getting the description', + content: { + 'application/json': { + schema: z.object({ + message: z.string().openapi({ + example: 'An error occured while getting the description', + }), + }), + }, + }, + }, + }, +}) diff --git a/services/wiki/src/routes/resourcesRouter.ts b/services/wiki/src/routes/resourcesRouter.ts index 1f4612501..bee4f5b94 100644 --- a/services/wiki/src/routes/resourcesRouter.ts +++ b/services/wiki/src/routes/resourcesRouter.ts @@ -12,6 +12,8 @@ import { resourceCreateSchema, resourcesListParamsSchema } from '../schemas' import { pathRoot } from './routes' import { patchResource } from '../controllers/resources/patchResource' import { resourcePatchSchema } from '../schemas/resource/resourcePatchSchema' +import { generateDescriptionSchema } from '../schemas/resource/generateDescriptionSchema' +import { generateDescription } from '../controllers/resources/generateDescription' const resourcesRouter = new Router() @@ -23,6 +25,12 @@ resourcesRouter.post( validate(z.object({ body: resourceCreateSchema })), postResource ) +resourcesRouter.post( + '/generate-description', + authenticate, + validate(z.object({ body: generateDescriptionSchema })), + generateDescription +) resourcesRouter.get( '/', diff --git a/services/wiki/src/schemas/resource/generateDescriptionSchema.ts b/services/wiki/src/schemas/resource/generateDescriptionSchema.ts new file mode 100644 index 000000000..10d045e13 --- /dev/null +++ b/services/wiki/src/schemas/resource/generateDescriptionSchema.ts @@ -0,0 +1,13 @@ +import { knexResourceSchema } from './resourceSchema' + +export const generateDescriptionSchema = knexResourceSchema.omit({ + id: true, + slug: true, + resource_type: true, + category_id: true, + description: true, + created_at: true, + updated_at: true, + user_id: true, + topics: true, +})