Skip to content

Commit

Permalink
Merge pull request #63 from BinaryStudioAcademy/task/OV-52-add-chat-h…
Browse files Browse the repository at this point in the history
…istory-saving

OV-52: add chat controller
  • Loading branch information
nikita-remeslov authored Aug 30, 2024
2 parents 5ac5142 + dcb791f commit 03e2a6b
Show file tree
Hide file tree
Showing 41 changed files with 459 additions and 22 deletions.
5 changes: 4 additions & 1 deletion backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
"@aws-sdk/client-s3": "3.635.0",
"@aws-sdk/s3-request-presigner": "3.635.0",
"@fastify/multipart": "8.3.0",
"@fastify/cookie": "9.4.0",
"@fastify/session": "10.9.0",
"@fastify/static": "7.0.4",
"@fastify/swagger": "8.15.0",
"@fastify/swagger-ui": "4.0.1",
Expand All @@ -48,6 +50,7 @@
"pino": "9.3.2",
"pino-pretty": "10.3.1",
"shared": "*",
"swagger-jsdoc": "6.2.8"
"swagger-jsdoc": "6.2.8",
"tiktoken": "1.0.16"
}
}
176 changes: 176 additions & 0 deletions backend/src/bundles/chat/chat.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import { type FastifySessionObject } from '@fastify/session';

import {
type ApiHandlerOptions,
type ApiHandlerResponse,
BaseController,
} from '~/common/controller/controller.js';
import { ApiPath } from '~/common/enums/enums.js';
import { HttpCode, HTTPMethod } from '~/common/http/http.js';
import { type Logger } from '~/common/logger/logger.js';
import { MAX_TOKEN } from '~/common/services/open-ai/libs/constants/constants.js';
import {
ChatPath,
OpenAIRole,
} from '~/common/services/open-ai/libs/enums/enums.js';
import { type OpenAIService } from '~/common/services/open-ai/open-ai.service.js';

import { type ChatService } from './chat.service.js';
import { type GenerateTextRequestDto } from './libs/types/types.js';
import { textGenerationValidationSchema } from './libs/validation-schemas/validation-schemas.js';

class ChatController extends BaseController {
private openAIService: OpenAIService;
private chatService: ChatService;

public constructor(
logger: Logger,
openAIService: OpenAIService,
chatService: ChatService,
) {
super(logger, ApiPath.CHAT);

this.openAIService = openAIService;
this.chatService = chatService;

this.addRoute({
path: ChatPath.ROOT,
method: HTTPMethod.POST,
validation: {
body: textGenerationValidationSchema,
},
handler: (options) =>
this.generateChatAnswer(
options as ApiHandlerOptions<{
body: GenerateTextRequestDto;
session: FastifySessionObject;
}>,
),
});

this.addRoute({
path: ChatPath.ROOT,
method: HTTPMethod.DELETE,
handler: (options) =>
this.deleteSession(
options as ApiHandlerOptions<{
session: FastifySessionObject;
}>,
),
});
}

/**
* @swagger
* /chat/:
* post:
* description: Returns generated text by Open AI
* requestBody:
* description: User message
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* message:
* type: string
* responses:
* 200:
* description: Successful operation
* content:
* application/json:
* schema:
* type: object
* properties:
* generatedText:
* type: string
*/

private async generateChatAnswer(
options: ApiHandlerOptions<{
body: GenerateTextRequestDto;
session: FastifySessionObject;
}>,
): Promise<ApiHandlerResponse> {
const { body, session } = options;

session.chatHistory = this.chatService.addMessageToHistory(
session.chatHistory,
body.message,
OpenAIRole.USER,
);

session.chatHistory = this.chatService.deleteOldMessages(
session.chatHistory,
MAX_TOKEN,
);

const generatedText = await this.openAIService.generateText(
session.chatHistory,
);

session.chatHistory = this.chatService.addMessageToHistory(
session.chatHistory,
generatedText,
OpenAIRole.ASSISTANT,
);

return {
payload: { generatedText },
status: HttpCode.OK,
};
}

/**
* @swagger
* /chat/:
* delete:
* description: Clears chat history
* requestBody:
* description: User message
* required: false
* responses:
* 200:
* description: Successful operation
* content:
* application/json:
* schema:
* type: object
* properties:
* isDeleted:
* type: boolean
* 500:
* description: Failed operation
* content:
* application/json:
* schema:
* type: object
* properties:
* isDeleted:
* type: boolean
*/
private deleteSession(
options: ApiHandlerOptions<{
session: FastifySessionObject;
}>,
): ApiHandlerResponse {
const { session } = options;

session.destroy((error) => {
if (error) {
return {
payload: { isDeleted: false },
status: HttpCode.INTERNAL_SERVER_ERROR,
};
}
});

return {
payload: { isDeleted: true },
status: HttpCode.OK,
};
}
}

export { ChatController };
64 changes: 64 additions & 0 deletions backend/src/bundles/chat/chat.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { type ValueOf } from 'shared';
import { type Tiktoken, encoding_for_model } from 'tiktoken';

import { CHAT_MODEL } from '~/common/services/open-ai/libs/constants/constants.js';
import { type OpenAIRole } from '~/common/services/open-ai/libs/enums/enums.js';

import {
type ChatService as ChatServiceT,
type Message,
} from './libs/types/types.js';

class ChatService implements ChatServiceT {
private modelEncoding: Tiktoken;

public constructor() {
this.modelEncoding = encoding_for_model(CHAT_MODEL);
}

public addMessageToHistory(
chatHistory: Message[],
userMessage: string,
role: ValueOf<typeof OpenAIRole>,
): Message[] {
const newUserMessage = {
content: userMessage,
role,
};

return [...chatHistory, newUserMessage];
}

private countTokens(messages: Message[]): number {
return messages.reduce(
(sum, message) =>
sum + this.modelEncoding.encode(message.content).length,
0,
);
}

public deleteOldMessages(
messages: Message[],
maxTokens: number,
): Message[] {
let totalTokens = this.countTokens(messages);
let updatedMessages = [...messages];

while (totalTokens > maxTokens && updatedMessages.length > 0) {
const [removedMessage, ...rest] = updatedMessages;
updatedMessages = rest;

if (!removedMessage) {
break;
}

totalTokens -= this.modelEncoding.encode(
removedMessage.content,
).length;
}

return updatedMessages;
}
}

export { ChatService };
10 changes: 10 additions & 0 deletions backend/src/bundles/chat/chat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { logger } from '~/common/logger/logger.js';
import { openAIService } from '~/common/services/services.js';

import { ChatController } from './chat.controller.js';
import { ChatService } from './chat.service.js';

const chatService = new ChatService();
const chatController = new ChatController(logger, openAIService, chatService);

export { chatController };
16 changes: 16 additions & 0 deletions backend/src/bundles/chat/libs/types/chat-service.type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { type ValueOf } from 'shared';

import { type OpenAIRole } from '~/common/services/open-ai/libs/enums/enums.js';

import { type Message } from './message.type.js';

type ChatService = {
addMessageToHistory(
chatHistory: Message[],
userMessage: string,
role: ValueOf<typeof OpenAIRole>,
): Message[];
deleteOldMessages(messages: Message[], maxTokens: number): void;
};

export { type ChatService };
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { type ValueOf } from 'shared';

import { type OpenAIRole } from '../enums/enums.js';
import { type OpenAIRole } from '~/common/services/open-ai/libs/enums/enums.js';

type Message = {
role: ValueOf<typeof OpenAIRole>;
Expand Down
3 changes: 3 additions & 0 deletions backend/src/bundles/chat/libs/types/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { type ChatService } from './chat-service.type.js';
export { type Message } from './message.type.js';
export { type GenerateTextRequestDto } from 'shared';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { textGenerationValidationSchema } from 'shared';
6 changes: 6 additions & 0 deletions backend/src/common/config/base-config.package.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ class BaseConfig implements Config {
env: 'OPEN_AI_KEY',
default: null,
},
SESSION_KEY: {
doc: 'Key for sessions',
format: String,
env: 'SESSION_KEY',
default: null,
},
},
DB: {
CONNECTION_STRING: {
Expand Down
1 change: 1 addition & 0 deletions backend/src/common/config/types/environment-schema.type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ type EnvironmentSchema = {
PORT: number;
ENVIRONMENT: ValueOf<typeof AppEnvironment>;
OPEN_AI_KEY: string;
SESSION_KEY: string;
};
DB: {
CONNECTION_STRING: string;
Expand Down
3 changes: 2 additions & 1 deletion backend/src/common/controller/base-controller.package.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,13 @@ class BaseController implements Controller {
private mapRequest(
request: Parameters<ServerAppRouteParameters['handler']>[0],
): ApiHandlerOptions {
const { body, query, params } = request;
const { body, query, params, session } = request;

return {
body,
query,
params,
session,
};
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ type DefaultApiHandlerOptions = {
body?: unknown;
query?: unknown;
params?: unknown;
session?: unknown;
};

type ApiHandlerOptions<
Expand All @@ -10,6 +11,7 @@ type ApiHandlerOptions<
body: T['body'];
query: T['query'];
params: T['params'];
session: T['session'];
};

export { type ApiHandlerOptions };
2 changes: 1 addition & 1 deletion backend/src/common/http/enums/enums.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export { HttpCode } from 'shared';
export { HttpCode, HTTPMethod } from 'shared';
2 changes: 1 addition & 1 deletion backend/src/common/http/http.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export { HttpCode } from './enums/enums.js';
export { HttpCode, HTTPMethod } from './enums/enums.js';
export { HttpError } from './exceptions/exceptions.js';
export { type HttpMethod } from './types/types.js';
5 changes: 5 additions & 0 deletions backend/src/common/plugins/libs/enums/controller-hook.enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const ControllerHook = {
ON_REQUEST: 'onRequest',
} as const;

export { ControllerHook };
1 change: 1 addition & 0 deletions backend/src/common/plugins/libs/enums/enums.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { ControllerHook } from './controller-hook.enum.js';
1 change: 1 addition & 0 deletions backend/src/common/plugins/plugins.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { authenticateJWT } from './auth/auth-jwt.plugin.js';
export { session } from './session/session.plugin.js';
Loading

0 comments on commit 03e2a6b

Please sign in to comment.