Skip to content

Commit

Permalink
Merge pull request #6 from henriqueweiand/ai-module
Browse files Browse the repository at this point in the history
Ai module
  • Loading branch information
henriqueweiand authored Nov 3, 2023
2 parents f181d8a + ecca9cc commit cdaa805
Show file tree
Hide file tree
Showing 26 changed files with 605 additions and 111 deletions.
3 changes: 2 additions & 1 deletion .env → .env.example
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
DATABASE_URL="postgresql://docker:docker@localhost:5432/project?schema=public"
PORT=3000
PORT=3000
OPENAI_API_KEY=
2 changes: 2 additions & 0 deletions .github/workflows/run-e2e-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ on: [pull_request]
jobs:
run-e2e-tests:
name: Run E2E Tests
environment: production
runs-on: ubuntu-latest

services:
Expand Down Expand Up @@ -37,3 +38,4 @@ jobs:
- run: npm run test:e2e
env:
DATABASE_URL: 'postgresql://docker:docker@localhost:5432/project?schema=public'
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
/node_modules

# Logs
.env
logs
*.log
npm-debug.log*
Expand Down Expand Up @@ -32,4 +33,5 @@ lerna-debug.log*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/extensions.json

26 changes: 26 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Debug Nest Framework",
"args": [
"${workspaceFolder}/src/main.ts"
],
"runtimeArgs": [
"--nolazy",
"-r",
"ts-node/register",
"-r",
"tsconfig-paths/register"
],
"sourceMaps": true,
"restart": true,
"envFile": "${workspaceFolder}/.env",
"cwd": "${workspaceRoot}",
"console": "integratedTerminal",
"protocol": "inspector"
}
]
}
79 changes: 74 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,76 @@
# nestjs-generate-questions

- [x] Base library with database and repository
- [x] Module user with one endpoint
- [x] Apply unit-tests for all parts of the library
- [x] Apply unit-tests for user module
- [x] Apply e2e-tests for user module
This repository houses a question generation tool, enabling users to input a text, from which it generates multiple questions for selection, with one of them being the correct answer.

Common libraries

- [x] Base library with database and user repository
- [x] unit-tests for all parts of the library
- [x] Questions and answers repositories
- [x] Swagger (http://localhost:3000/api)

User module

- [x] Module
- [x] Use-case
- [x] unit-tests
- [x] e2e-tests

Question and answers module

- [x] Module
- [x] Use-case
- [x] unit-tests

To-do

- [ ] Review unit-test of questions create
- [ ] Review e2e-test of questions create

### How to use

1. Configure .env file
2. Create an user by using POST /user
3. Create an question using POST /question

Example of POST /question response

```
{
"id": "2c9218cd-99c7-49f7-990a-381983017112",
"content": "When Britney Spears' much-hyped autobiography, The Woman in Me, went on sale in October, it instantly became the bestselling book on Amazon, thanks to presales from eager fans. This week it debuted at number one on the New York Times Bestsellers List (memoirs from actors John Stamos and Jada Pinkett Smith also earned spots on the coveted list). In her book, Spears recounts, with a kind of fevered urgency, her rise from small town Louisiana to mega-stardom. Even before she gets to the subject of the infamous 2008 court-ordered conservatorship, which gave Spears's father power over her finances and daily life, the reader feels intimately familiar with the singer. Some of the more salacious details from the book made their way into the media in the weeks before the memoir's publication: the painful at-home abortion endured as her then-partner, the singer and actor Justin Timberlake strummed an acoustic guitar; the night she shaved her head in front of a throng of camera-toting paparazzi; even the menacing hiss of the giant snake she bore aloft at the 2001 MTV Video Music Awards. ",
"createdAt": "2023-11-03T21:17:35.455Z",
"updatedAt": "2023-11-03T21:17:35.455Z",
"authorId": "f82828f7-ba96-443c-b45a-90d4803f4111",
"answers": [
{
"id": "e4b227e9-0472-4417-b0a3-78bd7486a02c",
"content": "The Woman in Me debuted at number one on the New York Times Bestsellers List.",
"createdAt": "2023-11-03T21:17:35.465Z",
"updatedAt": "2023-11-03T21:17:35.465Z",
"questionId": "2c9218cd-99c7-49f7-990a-381983017112",
"correct": true
},
{
"id": "232f236d-688f-4a28-bc5c-ba942275dbb2",
"content": "The book instantly became the bestselling book on Amazon due to presales from eager fans.",
"createdAt": "2023-11-03T21:17:35.471Z",
"updatedAt": "2023-11-03T21:17:35.471Z",
"questionId": "2c9218cd-99c7-49f7-990a-381983017112",
"correct": false
},
{
"id": "06133b15-b6f7-471b-b502-50032772cb85",
"content": "John Stamos and Jada Pinkett Smith also earned spots on the New York Times Bestsellers List with their memoirs.",
"createdAt": "2023-11-03T21:17:35.475Z",
"updatedAt": "2023-11-03T21:17:35.475Z",
"questionId": "2c9218cd-99c7-49f7-990a-381983017112",
"correct": false
}
]
}
```

### swagger

![Preview](https://raw.githubusercontent.com/henriqueweiand/nestjs-generate-questions/main/assets/swagger.png)
Binary file added assets/example.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/swagger.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"@nestjs/platform-express": "^9.0.0",
"@nestjs/swagger": "^7.1.14",
"@prisma/client": "^5.5.2",
"chatgpt": "5.2.5",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"reflect-metadata": "^0.1.13",
Expand Down
33 changes: 33 additions & 0 deletions src/modules/common/AI/ai-chat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Injectable, InternalServerErrorException, OnModuleInit } from '@nestjs/common';
import { ChatGPTAPI as IChatGPTAPI, ChatMessage } from 'chatgpt';
import { EnvService } from '../env';
import { AIChatGenerator } from './interface/ai-chat-generator';

@Injectable()
export class AIChat implements AIChatGenerator, OnModuleInit {
private API: IChatGPTAPI;

constructor(
private envService: EnvService) {
}

async onModuleInit() {
const importDynamic = new Function('modulePath', 'return import(modulePath)')
const { ChatGPTAPI } = await importDynamic('chatgpt')

this.API = new ChatGPTAPI({
apiKey: this.envService.get('OPENAI_API_KEY'),
});
}

async ask(question: string): Promise<ChatMessage | null> {
try {
const response = await this.API.sendMessage(question);
return response;
} catch (e) {
throw new InternalServerErrorException('Was not possible to generate the answers');
}

return null;
}
}
16 changes: 16 additions & 0 deletions src/modules/common/AI/ai.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Module } from '@nestjs/common';
import { AIChatGenerator } from './interface/ai-chat-generator';
import { AIChat } from './ai-chat';
import { EnvModule } from '../env';

@Module({
imports: [EnvModule],
providers: [
{
provide: AIChatGenerator,
useClass: AIChat
},
],
exports: [AIChatGenerator],
})
export class AIModule { }
3 changes: 3 additions & 0 deletions src/modules/common/AI/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './ai.module';
export * from './ai-chat';
export * from './interface/ai-chat-generator';
5 changes: 5 additions & 0 deletions src/modules/common/AI/interface/ai-chat-generator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { ChatMessage } from "chatgpt";

export abstract class AIChatGenerator {
abstract ask(question: string): Promise<ChatMessage | null>
}
8 changes: 7 additions & 1 deletion src/modules/common/database/database.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { PrismaService } from './prisma/prisma.service';
import { PrismaUserRepository } from './prisma/repository/prisma-user.repository';
import { PrismaQuestionRepository } from './prisma/repository/prisma-question.repository';
import { QuestionRepository } from './repository/question.repositoy';
import { PrismaAnswerRepository } from './prisma/repository/prisma-answer.repository';
import { AnswerRepository } from './repository/answer.repositoy';

@Module({
providers: [
Expand All @@ -17,8 +19,12 @@ import { QuestionRepository } from './repository/question.repositoy';
{
provide: QuestionRepository,
useClass: PrismaQuestionRepository
},
{
provide: AnswerRepository,
useClass: PrismaAnswerRepository
}
],
exports: [PrismaService, UserRepository, QuestionRepository],
exports: [PrismaService, UserRepository, QuestionRepository, AnswerRepository],
})
export class DatabaseModule { }
1 change: 1 addition & 0 deletions src/modules/common/database/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './database.module';
export * from './repository/question.repositoy';
export * from './repository/answer.repositoy';
export * from './repository/user.repositoy';
export * from './prisma/prisma.service';
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,17 @@ describe('PrismaQuestionRepository', () => {

it('should find a question by id', async () => {
const id = '1';
const expectedQuestion = { id: '1', title: 'Question title', content: 'Question content' };
const expectedQuestion = { id: '1', title: 'Question title', content: 'Question content', answers: [] };
prismaService.question.findUnique = jest.fn().mockResolvedValue(expectedQuestion);

const question = await prismaQuestionRepository.findById(id);

expect(question).toEqual(expectedQuestion);
expect(prismaService.question.findUnique).toHaveBeenCalledWith({ where: { id } });
expect(prismaService.question.findUnique).toHaveBeenCalledWith({
where: { id }, include: {
answers: true,
}
});
});

it('should call prismaService.findMany when findMany is called', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ export class PrismaQuestionRepository implements QuestionRepository {
where: {
id,
},
include: {
answers: true,
}
})

if (!question) {
Expand All @@ -29,7 +32,7 @@ export class PrismaQuestionRepository implements QuestionRepository {

async create(data: Prisma.QuestionCreateInput) {
const question = await this.prisma.question.create({
data,
data
});

return question;
Expand Down
1 change: 1 addition & 0 deletions src/modules/common/env/env.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { ConfigModule } from '@nestjs/config'
@Module({
imports: [
ConfigModule.forRoot({
envFilePath: process.env.NODE_ENV === 'test' ? '.env.example' : '.env',
validate: (env) => envSchema.parse(env),
isGlobal: true,
}),
Expand Down
1 change: 1 addition & 0 deletions src/modules/common/env/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { z } from 'zod'

export const envSchema = z.object({
DATABASE_URL: z.string().url(),
OPENAI_API_KEY: z.string(),
PORT: z.coerce.number().optional().default(3000),
})

Expand Down
1 change: 1 addition & 0 deletions src/modules/common/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './database';
export * from './env';
export * from './AI';
4 changes: 2 additions & 2 deletions src/modules/question/question.module.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { DatabaseModule } from '@app/common';
import { AIModule, DatabaseModule } from '@app/common';
import { Module } from '@nestjs/common';
import { CreateQuestionController } from './controllers/create-question.controller';
import { CreateQuestionUseCase } from './use-case/create-question';
Expand All @@ -9,7 +9,7 @@ import { GetManyQuestionsUseCase } from './use-case/get-many-questions';

@Module({
controllers: [CreateQuestionController, GetQuestionByIdController, GetManyQuestionsController],
imports: [DatabaseModule],
imports: [DatabaseModule, AIModule],
providers: [CreateQuestionUseCase, GetQuestionByIdUseCase, GetManyQuestionsUseCase],
exports: [],
})
Expand Down
Loading

0 comments on commit cdaa805

Please sign in to comment.