diff --git a/.env.dist b/.env.dist index 4c5f6b1..5d84fea 100644 --- a/.env.dist +++ b/.env.dist @@ -1,2 +1,5 @@ +# OpenAI API Key OPENAI_API_KEY= + +# Assistant ID - leave it empty if you don't have an assistant yet ASSISTANT_ID= diff --git a/README.md b/README.md index a70ab57..48483fb 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,119 @@ +

+ + Boldare + +

+ # AI Assistant -Install the dependencies: +Introducing the NestJS library, designed to harness the power of OpenAI's Assistant, enabling developers to create highly efficient, scalable, and rapid AI assistants and chatbots. This library is tailored for seamless integration into the NestJS ecosystem, offering an intuitive API, WebSockets, and tools that streamline the development of AI-driven interactions. Whether you're building a customer service bot, a virtual assistant, or an interactive chatbot for engaging user experiences, our library empowers you to leverage cutting-edge AI capabilities with minimal effort. + +## First steps + +### Prerequiring + +Before you start, you will need to have an account on the OpenAI platform and an API key. You can create an account [here](https://platform.openai.com/). + +Open or create your NestJS application where you would like to integrate the AI Assistant. If you don't have a NestJS application yet, you can create one using the following command: + +```bash +$ nest new project-name +``` + +### Step 1: Installation + +Install the library using npm: + +```bash +$ npm i @boldare/ai-assistant --save +``` + +### Step 2: Env variables + +Set up your environment variables, create environment variables in the `.env` file in the root directory of the project, and populate it with the necessary secrets. You will need to add the OpenAI API Key and the Assistant ID. The Assistant ID is optional, and you can leave it empty if you don't have an assistant yet. + +Create a `.env` file in the root directory of your project and populate it with the necessary secrets: + +```bash +$ touch .env +``` + +Add the following content to the `.env` file: + +```dotenv +# OpenAI API Key +OPENAI_API_KEY= + +# Assistant ID - leave it empty if you don't have an assistant yet +ASSISTANT_ID= +``` + +### Step 3: Configuration + +Configure the settings for your assistant. For more information about assistant parameters, you can refer to the [OpenAI documentation](https://platform.openai.com/docs/assistants/how-it-works/creating-assistants). A sample configuration can be found in ([chat.config.ts](apps%2Fapi%2Fsrc%2Fapp%2Fchat%2Fchat.config.ts)). + +```js +// chat.config.ts file + +// Default OpenAI configuration +export const assistantParams: AssistantCreateParams = { + name: 'Your assistant name', + instructions: `You are a chatbot assistant. Speak briefly and clearly.`, + tools: [ + { type: 'code_interpreter' }, + { type: 'retrieval' }, + // (...) function calling - functions are configured by extended services + ], + model: 'gpt-4-1106-preview', + metadata: {}, +}; + +// Additional configuration for our assistant +export const assistantConfig: AssistantConfigParams = { + id: process.env['ASSISTANT_ID'], // OpenAI API Key + params: assistantParams, // AssistantCreateParams + filesDir: './apps/api/src/app/knowledge', // Path to the directory with files (the final path is "fileDir" + "single file") + files: [], // List of file names (or paths if you didn't fill in the above parameter) +}; +``` + +Import the AI Assistant module with your configuration into the module file where you intend to use it: + +```js +@Module({ + imports: [AssistantModule.forRoot(assistantConfig)], +}) +export class ChatbotModule {} +``` + +# Repository + +The repository includes a library with an AI assistant as well as other useful parts: + +| Name | Type | Description | +| ----------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------- | +| `@boldare/ai-assistant` | `library` | A NestJS library based on the OpenAI Assistant for building efficient, scalable, and quick solutions for AI assistants/chatbots | +| `@boldare/ai-embedded` | `library` | The code enables embedding the chatbot on various websites through JavaScript scripts. | +| `api` | `application` | Example usage of the `@boldare/ai-assistant` library. | +| `spa` | `application` | Example client application (SPA) with a chatbot. | + +## Getting started + +### Step 1: Install dependencies ```bash $ npm install ``` -Project takes advantage of the `envfile` dependency to manage secrets. Assuming so, you need to copy `.env.dist` to `.env` file in the root directory of the project and fill it with the relevant secrets. +### Step 2: Env variables + +Set up your environment variables, copy the `.env.dist` file to `.env` file in the root directory of the project, and populate it with the necessary secrets. ```bash $ cp .env.dist .env ``` ---- +### Step 3: Run applications ```bash # Start the app (api and spa) @@ -25,27 +126,12 @@ $ npm run start:api $ npm run start:spa ``` -Open your browser and navigate to: - -- http://localhost:4200/ - spa -- http://localhost:3000/api/ - api - -Happy coding! - -## Ready to deploy? - -Just merge your code to the `main` branch. - -## Set up CI! - -Nx comes with local caching already built-in (check your `nx.json`). On CI you might want to go a step further. - -- [Set up remote caching](https://nx.dev/core-features/share-your-cache) -- [Set up task distribution across multiple machines](https://nx.dev/nx-cloud/features/distribute-task-execution) -- [Learn more how to setup CI](https://nx.dev/recipes/ci) +Now you can open your browser and navigate to: -## Connect with us! +| URL | Description | +| ------------------------------ | --------------------------- | +| http://localhost:4200/ | Client application (SPA) | +| http://localhost:3000/api/ | API application | +| http://localhost:3000/api/docs | API documentation (swagger) | -- [Join the community](https://nx.dev/community) -- [Subscribe to the Nx Youtube Channel](https://www.youtube.com/@nxdevtools) -- [Follow us on Twitter](https://twitter.com/nxdevtools) +### 🎉 Happy coding 🎉 diff --git a/apps/api-e2e/.eslintrc.json b/apps/api-e2e/.eslintrc.json deleted file mode 100644 index 9d9c0db..0000000 --- a/apps/api-e2e/.eslintrc.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "extends": ["../../.eslintrc.json"], - "ignorePatterns": ["!**/*"], - "overrides": [ - { - "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], - "rules": {} - }, - { - "files": ["*.ts", "*.tsx"], - "rules": {} - }, - { - "files": ["*.js", "*.jsx"], - "rules": {} - } - ] -} diff --git a/apps/api-e2e/jest.config.ts b/apps/api-e2e/jest.config.ts deleted file mode 100644 index 4e7fd4c..0000000 --- a/apps/api-e2e/jest.config.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* eslint-disable */ -export default { - displayName: 'api-e2e', - preset: '../../jest.preset.js', - globalSetup: '/src/support/global-setup.ts', - globalTeardown: '/src/support/global-teardown.ts', - setupFiles: ['/src/support/test-setup.ts'], - testEnvironment: 'node', - transform: { - '^.+\\.[tj]s$': [ - 'ts-jest', - { - tsconfig: '/tsconfig.spec.json', - }, - ], - }, - moduleFileExtensions: ['ts', 'js', 'html'], - coverageDirectory: '../../coverage/api-e2e', -}; diff --git a/apps/api-e2e/project.json b/apps/api-e2e/project.json deleted file mode 100644 index b3ac047..0000000 --- a/apps/api-e2e/project.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "api-e2e", - "$schema": "../../node_modules/nx/schemas/project-schema.json", - "implicitDependencies": ["api"], - "projectType": "application", - "targets": { - "e2e": { - "executor": "@nx/jest:jest", - "outputs": ["{workspaceRoot}/coverage/{e2eProjectRoot}"], - "options": { - "jestConfig": "apps/api-e2e/jest.config.ts", - "passWithNoTests": true - } - }, - "lint": { - "executor": "@nx/eslint:lint", - "outputs": ["{options.outputFile}"] - } - } -} diff --git a/apps/api-e2e/src/ai-restaurants-api/ai-restaurants-api.spec.ts b/apps/api-e2e/src/ai-restaurants-api/ai-restaurants-api.spec.ts deleted file mode 100644 index e8ac2a6..0000000 --- a/apps/api-e2e/src/ai-restaurants-api/ai-restaurants-api.spec.ts +++ /dev/null @@ -1,10 +0,0 @@ -import axios from 'axios'; - -describe('GET /api', () => { - it('should return a message', async () => { - const res = await axios.get(`/api`); - - expect(res.status).toBe(200); - expect(res.data).toEqual({ message: 'Hello API' }); - }); -}); diff --git a/apps/api-e2e/src/support/global-setup.ts b/apps/api-e2e/src/support/global-setup.ts deleted file mode 100644 index c1f5144..0000000 --- a/apps/api-e2e/src/support/global-setup.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var __TEARDOWN_MESSAGE__: string; - -module.exports = async function () { - // Start services that that the app needs to run (e.g. database, docker-compose, etc.). - console.log('\nSetting up...\n'); - - // Hint: Use `globalThis` to pass variables to global teardown. - globalThis.__TEARDOWN_MESSAGE__ = '\nTearing down...\n'; -}; diff --git a/apps/api-e2e/src/support/global-teardown.ts b/apps/api-e2e/src/support/global-teardown.ts deleted file mode 100644 index 32ea345..0000000 --- a/apps/api-e2e/src/support/global-teardown.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* eslint-disable */ - -module.exports = async function () { - // Put clean up logic here (e.g. stopping services, docker-compose, etc.). - // Hint: `globalThis` is shared between setup and teardown. - console.log(globalThis.__TEARDOWN_MESSAGE__); -}; diff --git a/apps/api-e2e/src/support/test-setup.ts b/apps/api-e2e/src/support/test-setup.ts deleted file mode 100644 index 07f2870..0000000 --- a/apps/api-e2e/src/support/test-setup.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ - -import axios from 'axios'; - -module.exports = async function () { - // Configure axios for tests to use. - const host = process.env.HOST ?? 'localhost'; - const port = process.env.PORT ?? '3000'; - axios.defaults.baseURL = `http://${host}:${port}`; -}; diff --git a/apps/api-e2e/tsconfig.json b/apps/api-e2e/tsconfig.json deleted file mode 100644 index ed633e1..0000000 --- a/apps/api-e2e/tsconfig.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "files": [], - "include": [], - "references": [ - { - "path": "./tsconfig.spec.json" - } - ], - "compilerOptions": { - "esModuleInterop": true - } -} diff --git a/apps/api-e2e/tsconfig.spec.json b/apps/api-e2e/tsconfig.spec.json deleted file mode 100644 index d7f9cf2..0000000 --- a/apps/api-e2e/tsconfig.spec.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "../../dist/out-tsc", - "module": "commonjs", - "types": ["jest", "node"] - }, - "include": ["jest.config.ts", "src/**/*.ts"] -} diff --git a/apps/api/src/app/chat/agents/agents.module.ts b/apps/api/src/app/chat/agents/agents.module.ts index 0cd555d..fd20cb5 100644 --- a/apps/api/src/app/chat/agents/agents.module.ts +++ b/apps/api/src/app/chat/agents/agents.module.ts @@ -1,6 +1,9 @@ import { Module } from '@nestjs/common'; +import { GetAnimalAgent } from './get-animal.agent'; +import { AgentModule } from '@boldare/ai-assistant'; @Module({ - imports: [], + imports: [AgentModule], + providers: [GetAnimalAgent], }) export class AgentsModule {} diff --git a/apps/api/src/app/chat/agents/get-animal.agent.ts b/apps/api/src/app/chat/agents/get-animal.agent.ts new file mode 100644 index 0000000..abb2a4c --- /dev/null +++ b/apps/api/src/app/chat/agents/get-animal.agent.ts @@ -0,0 +1,50 @@ +import { Injectable } from '@nestjs/common'; +import { AssistantCreateParams } from 'openai/resources/beta'; +import { AgentBase, AgentData, AgentService } from '@boldare/ai-assistant'; + +@Injectable() +export class GetAnimalAgent extends AgentBase { + definition: AssistantCreateParams.AssistantToolsFunction = { + type: 'function', + function: { + name: this.constructor.name, + description: `Display name od the animal`, + parameters: { + type: 'object', + properties: { + animal: { + type: 'string', + description: `Type of the animal`, + }, + }, + required: ['animal'], + }, + }, + }; + + constructor(protected readonly agentService: AgentService) { + super(agentService); + } + + async output(data: AgentData): Promise { + try { + const params = JSON.parse(data.params); + const animal = params?.animal; + const animals = [ + { id: 1, animal: 'dog', name: 'Rex' }, + { id: 2, animal: 'cat', name: 'Mittens' }, + { id: 3, animal: 'bird', name: 'Tweety' }, + { id: 4, animal: 'fish', name: 'Goldie' }, + { id: 5, animal: 'rabbit', name: 'Bugs' }, + ]; + + if (!animal) { + return 'No animal provided'; + } + + return animals.find(a => a.animal === animal)?.name || 'No such animal'; + } catch (errors) { + return `Invalid data: ${JSON.stringify(errors)}`; + } + } +} diff --git a/apps/api/src/app/chat/chat.module.ts b/apps/api/src/app/chat/chat.module.ts index ac24731..89d637f 100644 --- a/apps/api/src/app/chat/chat.module.ts +++ b/apps/api/src/app/chat/chat.module.ts @@ -1,8 +1,9 @@ import { Module } from '@nestjs/common'; import { AssistantModule } from '@boldare/ai-assistant'; import { assistantConfig } from './chat.config'; +import { AgentsModule } from './agents/agents.module'; @Module({ - imports: [AssistantModule.forRoot(assistantConfig)], + imports: [AssistantModule.forRoot(assistantConfig), AgentsModule], }) export class ChatModule {} diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts index 8f87552..a31c76d 100644 --- a/apps/api/src/main.ts +++ b/apps/api/src/main.ts @@ -1,20 +1,30 @@ import { Logger } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; +import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import { AppModule } from './app/app.module'; async function bootstrap() { const app = await NestFactory.create(AppModule); const globalPrefix = 'api'; + const config = new DocumentBuilder() + .setTitle('@boldare/ai-assistant') + .setVersion('0.1.0') + .build(); + const document = SwaggerModule.createDocument(app, config); + SwaggerModule.setup('api/docs', app, document); + app.setGlobalPrefix(globalPrefix); app.enableCors({ origin: '*', credentials: true, }); + const port = process.env.PORT || 3000; await app.listen(port); + Logger.log( - `🚀 Application is running on: http://localhost:${port}/${globalPrefix}` + `🚀 Application is running on: http://localhost:${port}/${globalPrefix}`, ); } diff --git a/apps/spa/src/app/components/chat/chat-audio/chat-audio.component.ts b/apps/spa/src/app/components/chat/chat-audio/chat-audio.component.ts index 9e0166a..cf783a3 100644 --- a/apps/spa/src/app/components/chat/chat-audio/chat-audio.component.ts +++ b/apps/spa/src/app/components/chat/chat-audio/chat-audio.component.ts @@ -1,14 +1,10 @@ import { Component, Input, OnInit } from '@angular/core'; import { ChatClientService } from '../../../modules/+chat/shared/chat-client.service'; -import { Message } from '../../../modules/+chat/shared/chat.model'; +import { Message, SpeechVoice } from '../../../modules/+chat/shared/chat.model'; import { environment } from '../../../../environments/environment'; import { MatIconModule } from '@angular/material/icon'; import { delay } from 'rxjs'; -import { - ChatAudioResponse, - SpeechPayload, - SpeechVoice, -} from '@boldare/ai-assistant'; +import { ChatAudioResponse, PostSpeechDto } from '@boldare/ai-assistant'; import { NgClass } from '@angular/common'; @Component({ @@ -50,7 +46,7 @@ export class ChatAudioComponent implements OnInit { return; } - const payload: SpeechPayload = { + const payload: PostSpeechDto = { content: this.message.content, voice: SpeechVoice.Onyx, }; diff --git a/apps/spa/src/app/modules/+chat/shared/chat-client.service.ts b/apps/spa/src/app/modules/+chat/shared/chat-client.service.ts index 976dd66..b8328b2 100644 --- a/apps/spa/src/app/modules/+chat/shared/chat-client.service.ts +++ b/apps/spa/src/app/modules/+chat/shared/chat-client.service.ts @@ -6,9 +6,9 @@ import { AudioResponse } from './chat.model'; import { ChatAudio, ChatAudioResponse, - SpeechPayload, - UploadFileResponse, + PostSpeechDto, UploadFilesPayload, + UploadFilesResponseDto, } from '@boldare/ai-assistant'; @Injectable({ providedIn: 'root' }) @@ -21,7 +21,6 @@ export class ChatClientService { const formData = new FormData(); formData.append('file', payload.file); - formData.append('threadId', payload.threadId); return this.httpClient.post( `${this.apiUrl}/ai/transcription`, @@ -29,20 +28,22 @@ export class ChatClientService { ); } - speech(payload: SpeechPayload): Observable { + speech(payload: PostSpeechDto): Observable { return this.httpClient.post( `${this.apiUrl}/ai/speech`, payload, ); } - async uploadFiles(payload: UploadFilesPayload): Promise { + async uploadFiles( + payload: UploadFilesPayload, + ): Promise { const formData = new FormData(); payload.files.forEach(file => formData.append('files', file)); return await lastValueFrom( - this.httpClient.post( + this.httpClient.post( `${this.apiUrl}/files`, formData, ), diff --git a/apps/spa/src/app/modules/+chat/shared/chat-files.service.ts b/apps/spa/src/app/modules/+chat/shared/chat-files.service.ts index 0f5708e..79c847b 100644 --- a/apps/spa/src/app/modules/+chat/shared/chat-files.service.ts +++ b/apps/spa/src/app/modules/+chat/shared/chat-files.service.ts @@ -17,9 +17,11 @@ export class ChatFilesService { return []; } - const uploadedFiles = await this.chatClientService.uploadFiles({ files }); + const uploadedFilesResponse = await this.chatClientService.uploadFiles({ + files, + }); this.filesService.clear(); - return uploadedFiles || []; + return uploadedFilesResponse.files || []; } } diff --git a/apps/spa/src/app/modules/+chat/shared/chat-gateway.service.ts b/apps/spa/src/app/modules/+chat/shared/chat-gateway.service.ts index 6080a2f..fd9d6b1 100644 --- a/apps/spa/src/app/modules/+chat/shared/chat-gateway.service.ts +++ b/apps/spa/src/app/modules/+chat/shared/chat-gateway.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@angular/core'; import { ChatEvents } from './chat.model'; import io from 'socket.io-client'; -import { ChatCall } from '@boldare/ai-assistant'; +import { ChatCallDto } from '@boldare/ai-assistant'; import { Observable } from 'rxjs'; import { environment } from '../../../../environments/environment'; @@ -9,12 +9,12 @@ import { environment } from '../../../../environments/environment'; export class ChatGatewayService { private socket = io(environment.websocketUrl); - sendMessage(payload: ChatCall): void { + sendMessage(payload: ChatCallDto): void { this.socket.emit(ChatEvents.SendMessage, payload); } - getMessages(): Observable { - return new Observable(observer => { + getMessages(): Observable { + return new Observable(observer => { this.socket.on(ChatEvents.MessageReceived, data => observer.next(data)); return () => this.socket.disconnect(); }); diff --git a/apps/spa/src/app/modules/+chat/shared/chat.model.ts b/apps/spa/src/app/modules/+chat/shared/chat.model.ts index 2fed212..7d48983 100644 --- a/apps/spa/src/app/modules/+chat/shared/chat.model.ts +++ b/apps/spa/src/app/modules/+chat/shared/chat.model.ts @@ -22,3 +22,12 @@ export enum ChatEvents { export enum MessageStatus { Invisible = 'invisible', } + +export enum SpeechVoice { + Alloy = 'alloy', + Echo = 'echo', + Fable = 'fable', + Onyx = 'onyx', + Nova = 'nova', + Shimmer = 'shimmer', +} diff --git a/apps/spa/src/app/modules/+chat/shared/chat.service.ts b/apps/spa/src/app/modules/+chat/shared/chat.service.ts index 774b642..1c72ec4 100644 --- a/apps/spa/src/app/modules/+chat/shared/chat.service.ts +++ b/apps/spa/src/app/modules/+chat/shared/chat.service.ts @@ -15,7 +15,7 @@ import { ChatClientService } from './chat-client.service'; import { ThreadService } from './thread.service'; import { ChatFilesService } from './chat-files.service'; import { environment } from '../../../../environments/environment'; -import { OpenAiFile, ThreadResponse } from '@boldare/ai-assistant'; +import { OpenAiFile, GetThreadResponseDto } from '@boldare/ai-assistant'; import { Threads } from 'openai/resources/beta'; import MessageContentText = Threads.MessageContentText; import { ThreadMessage } from 'openai/resources/beta/threads'; @@ -49,7 +49,7 @@ export class ChatService { return message.content[0].type === 'text'; } - parseMessages(thread: ThreadResponse): Message[] { + parseMessages(thread: GetThreadResponseDto): Message[] { return thread.messages .reverse() .filter( @@ -69,7 +69,7 @@ export class ChatService { filter(threadId => !!threadId), tap(() => this.isLoading$.next(true)), mergeMap(threadId => this.threadService.getThread(threadId)), - map((response: ThreadResponse) => this.parseMessages(response)), + map((response: GetThreadResponseDto) => this.parseMessages(response)), ) .subscribe(data => { this.messages$.next(data); @@ -137,10 +137,7 @@ export class ChatService { this.isTyping$.next(true); this.chatClientService - .transcription({ - threadId: this.threadService.threadId$.value, - file: file as File, - }) + .transcription({ file: file as File }) .pipe(take(1)) .subscribe(response => this.sendMessage(response.content)); } diff --git a/apps/spa/src/app/modules/+chat/shared/thread-client.service.ts b/apps/spa/src/app/modules/+chat/shared/thread-client.service.ts index a4bf148..0a7c04f 100644 --- a/apps/spa/src/app/modules/+chat/shared/thread-client.service.ts +++ b/apps/spa/src/app/modules/+chat/shared/thread-client.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; -import { ThreadConfig, ThreadResponse } from '@boldare/ai-assistant'; +import { CreateThreadDto, GetThreadResponseDto } from '@boldare/ai-assistant'; import { Observable } from 'rxjs'; import { environment } from '../../../../environments/environment'; @@ -8,15 +8,15 @@ import { environment } from '../../../../environments/environment'; export class ThreadClientService { constructor(private readonly httpClient: HttpClient) {} - postThread(payload: ThreadConfig = {}): Observable { - return this.httpClient.post( + postThread(payload: CreateThreadDto = {}): Observable { + return this.httpClient.post( `${environment.apiUrl}/assistant/threads`, payload, ); } - getThread(id: string): Observable { - return this.httpClient.get( + getThread(id: string): Observable { + return this.httpClient.get( `${environment.apiUrl}/assistant/threads/${id}`, ); } diff --git a/apps/spa/src/app/modules/+chat/shared/thread.service.ts b/apps/spa/src/app/modules/+chat/shared/thread.service.ts index 4be7d60..89d1b34 100644 --- a/apps/spa/src/app/modules/+chat/shared/thread.service.ts +++ b/apps/spa/src/app/modules/+chat/shared/thread.service.ts @@ -3,7 +3,7 @@ import { BehaviorSubject, Observable, Subject, take, tap } from 'rxjs'; import { environment } from '../../../../environments/environment'; import { ThreadClientService } from './thread-client.service'; import { ConfigurationFormService } from '../../+configuration/shared/configuration-form.service'; -import { ThreadResponse } from '@boldare/ai-assistant'; +import { GetThreadResponseDto } from '@boldare/ai-assistant'; @Injectable({ providedIn: 'root' }) export class ThreadService { @@ -19,7 +19,7 @@ export class ThreadService { private readonly configurationFormService: ConfigurationFormService, ) {} - start(): Observable { + start(): Observable { const messages = this.configurationFormService.getInitialThreadMessages(); return this.threadClientService @@ -42,7 +42,7 @@ export class ThreadService { localStorage.removeItem(this.key); } - getThread(id: string): Observable { + getThread(id: string): Observable { return this.threadClientService.getThread(id).pipe(take(1)); } } diff --git a/apps/spa/src/app/modules/+configuration/shared/configuration-form.service.ts b/apps/spa/src/app/modules/+configuration/shared/configuration-form.service.ts index 41313a1..797df27 100644 --- a/apps/spa/src/app/modules/+configuration/shared/configuration-form.service.ts +++ b/apps/spa/src/app/modules/+configuration/shared/configuration-form.service.ts @@ -1,8 +1,8 @@ import { Injectable } from '@angular/core'; import { FormControl, FormGroup } from '@angular/forms'; -import { SpeechVoice, ThreadConfig } from '@boldare/ai-assistant'; +import { CreateThreadDto } from '@boldare/ai-assistant'; import { ConfigurationForm } from './configuration.model'; -import { MessageStatus } from '../../+chat/shared/chat.model'; +import { MessageStatus, SpeechVoice } from '../../+chat/shared/chat.model'; @Injectable({ providedIn: 'root' }) export class ConfigurationFormService { @@ -11,7 +11,7 @@ export class ConfigurationFormService { voice: new FormControl(SpeechVoice.Alloy, { nonNullable: true }), }); - getInitialThreadMessages(): ThreadConfig { + getInitialThreadMessages(): CreateThreadDto { return { messages: [ { diff --git a/apps/spa/src/app/modules/+configuration/shared/configuration.helpers.ts b/apps/spa/src/app/modules/+configuration/shared/configuration.helpers.ts index 8704aef..ac8b6ee 100644 --- a/apps/spa/src/app/modules/+configuration/shared/configuration.helpers.ts +++ b/apps/spa/src/app/modules/+configuration/shared/configuration.helpers.ts @@ -1,4 +1,4 @@ -import { SpeechVoice } from '@boldare/ai-assistant'; +import { SpeechVoice } from '../../+chat/shared/chat.model'; export const voices: SpeechVoice[] = [ SpeechVoice.Alloy, diff --git a/apps/spa/src/app/modules/+configuration/shared/configuration.model.ts b/apps/spa/src/app/modules/+configuration/shared/configuration.model.ts index 1a7d7ad..07931dc 100644 --- a/apps/spa/src/app/modules/+configuration/shared/configuration.model.ts +++ b/apps/spa/src/app/modules/+configuration/shared/configuration.model.ts @@ -1,5 +1,5 @@ import { FormControl } from '@angular/forms'; -import { SpeechVoice } from '@boldare/ai-assistant'; +import { SpeechVoice } from '../../+chat/shared/chat.model'; export interface ConfigurationForm { firstName: FormControl; diff --git a/apps/spa/src/test-setup.ts b/apps/spa/src/test-setup.ts index ab1eeeb..ef358fb 100644 --- a/apps/spa/src/test-setup.ts +++ b/apps/spa/src/test-setup.ts @@ -1,4 +1,3 @@ -// @ts-expect-error https://thymikee.github.io/jest-preset-angular/docs/getting-started/test-environment globalThis.ngJest = { testEnvironmentOptions: { errorOnUnknownElements: true, diff --git a/libs/ai-assistant/src/lib/ai/ai.controller.ts b/libs/ai-assistant/src/lib/ai/ai.controller.ts index 43c958d..0e451fb 100644 --- a/libs/ai-assistant/src/lib/ai/ai.controller.ts +++ b/libs/ai-assistant/src/lib/ai/ai.controller.ts @@ -6,20 +6,38 @@ import { UseInterceptors, } from '@nestjs/common'; import { AiService } from './ai.service'; -import { SpeechPayload } from './ai.model'; +import { + PostSpeechDto, + PostSpeechResponseDto, + PostTranscriptionDto, + PostTranscriptionResponseDto, +} from './ai.model'; import { FileInterceptor } from '@nestjs/platform-express'; import { toFile } from 'openai/uploads'; // @ts-expect-error multer is necessary // eslint-disable-next-line import { multer } from 'multer'; +import { ApiBody, ApiConsumes, ApiResponse, ApiTags } from '@nestjs/swagger'; +@ApiTags('AI') @Controller('assistant/ai') export class AiController { constructor(public readonly aiService: AiService) {} + @ApiConsumes('multipart/form-data') + @ApiResponse({ + status: 200, + type: PostTranscriptionResponseDto, + }) + @ApiBody({ + description: 'Audio file to be transcribed.', + type: PostTranscriptionDto, + }) @Post('transcription') @UseInterceptors(FileInterceptor('file')) - async postTranscription(@UploadedFile() fileData: Express.Multer.File) { + async postTranscription( + @UploadedFile() fileData: Express.Multer.File, + ): Promise { try { const file = await toFile(fileData.buffer, 'audio.wav', { type: 'wav' }); const transcription = await this.aiService.transcription(file); @@ -30,8 +48,13 @@ export class AiController { } } + @ApiResponse({ + status: 200, + type: PostSpeechResponseDto, + description: 'TTS - response as the audio buffer', + }) @Post('speech') - async postSpeech(@Body() payload: SpeechPayload): Promise { + async postSpeech(@Body() payload: PostSpeechDto): Promise { try { return await this.aiService.speech(payload); } catch (error) { diff --git a/libs/ai-assistant/src/lib/ai/ai.model.ts b/libs/ai-assistant/src/lib/ai/ai.model.ts index f49b351..3c44727 100644 --- a/libs/ai-assistant/src/lib/ai/ai.model.ts +++ b/libs/ai-assistant/src/lib/ai/ai.model.ts @@ -1,6 +1,7 @@ import { Transcription } from 'openai/resources/audio'; +import { ApiProperty } from '@nestjs/swagger'; -export const enum SpeechVoice { +export enum SpeechVoice { Alloy = 'alloy', Echo = 'echo', Fable = 'fable', @@ -9,9 +10,34 @@ export const enum SpeechVoice { Shimmer = 'shimmer', } -export interface SpeechPayload { - content: string; +export class PostSpeechDto { + @ApiProperty({ description: 'Content of the message' }) + content!: string; + + @ApiProperty({ + description: 'Voice of the message author.', + enum: SpeechVoice, + required: false, + }) voice?: SpeechVoice; } +export class PostSpeechResponseDto { + @ApiProperty() + type!: 'buffer'; + + @ApiProperty({ type: 'number', isArray: true }) + data!: number[]; +} + +export class PostTranscriptionDto { + @ApiProperty({type: 'string', format: 'binary'}) + file!: File; +} + +export class PostTranscriptionResponseDto { + @ApiProperty({ description: 'Transcription of the audio file' }) + content!: string; +} + export type AiTranscription = Transcription; diff --git a/libs/ai-assistant/src/lib/ai/ai.service.ts b/libs/ai-assistant/src/lib/ai/ai.service.ts index 739d007..f454616 100644 --- a/libs/ai-assistant/src/lib/ai/ai.service.ts +++ b/libs/ai-assistant/src/lib/ai/ai.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; import OpenAI from 'openai'; import { Uploadable } from 'openai/uploads'; -import { AiTranscription, SpeechPayload, SpeechVoice } from './ai.model'; +import { AiTranscription, PostSpeechDto, SpeechVoice } from './ai.model'; import 'dotenv/config'; @Injectable() @@ -15,7 +15,7 @@ export class AiService { }); } - async speech(data: SpeechPayload): Promise { + async speech(data: PostSpeechDto): Promise { const response = await this.provider.audio.speech.create({ model: 'tts-1', voice: data.voice || SpeechVoice.Alloy, diff --git a/libs/ai-assistant/src/lib/chat/chat.controller.spec.ts b/libs/ai-assistant/src/lib/chat/chat.controller.spec.ts index b80a5f4..25466bc 100644 --- a/libs/ai-assistant/src/lib/chat/chat.controller.spec.ts +++ b/libs/ai-assistant/src/lib/chat/chat.controller.spec.ts @@ -3,7 +3,8 @@ import { ChatController } from './chat.controller'; import { AiModule } from './../ai/ai.module'; import { ChatModule } from './chat.module'; import { ChatService } from './chat.service'; -import { ChatCall, ChatCallResponse } from './chat.model'; +import { ChatCallDto, ChatCallResponseDto } from './chat.model'; + describe('ChatController', () => { let chatController: ChatController; let chatService: ChatService; @@ -24,8 +25,10 @@ describe('ChatController', () => { describe('call', () => { it('should call chatService.call', async () => { - jest.spyOn(chatService, 'call').mockResolvedValue({} as ChatCallResponse); - const payload = { content: 'Hello' } as ChatCall; + jest + .spyOn(chatService, 'call') + .mockResolvedValue({} as ChatCallResponseDto); + const payload = { content: 'Hello' } as ChatCallDto; await chatController.call(payload); diff --git a/libs/ai-assistant/src/lib/chat/chat.controller.ts b/libs/ai-assistant/src/lib/chat/chat.controller.ts index 1deb4bf..724fc9b 100644 --- a/libs/ai-assistant/src/lib/chat/chat.controller.ts +++ b/libs/ai-assistant/src/lib/chat/chat.controller.ts @@ -1,13 +1,21 @@ import { Body, Controller, Post } from '@nestjs/common'; -import { ChatCall, ChatCallResponse } from './chat.model'; +import { ApiBody, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { ChatCallDto, ChatCallResponseDto } from './chat.model'; import { ChatService } from './chat.service'; +@ApiTags('Chat') @Controller('assistant/chat') export class ChatController { constructor(public readonly chatsService: ChatService) {} + @ApiResponse({ + status: 200, + type: ChatCallResponseDto, + description: 'Default action for conversation between user and bot', + }) + @ApiBody({ type: ChatCallDto }) @Post() - async call(@Body() payload: ChatCall): Promise { + async call(@Body() payload: ChatCallDto): Promise { return await this.chatsService.call(payload); } } diff --git a/libs/ai-assistant/src/lib/chat/chat.gateway.spec.ts b/libs/ai-assistant/src/lib/chat/chat.gateway.spec.ts index 271d851..c74c181 100644 --- a/libs/ai-assistant/src/lib/chat/chat.gateway.spec.ts +++ b/libs/ai-assistant/src/lib/chat/chat.gateway.spec.ts @@ -4,7 +4,6 @@ import { ChatGateway } from './chat.gateway'; import { AiModule } from './../ai/ai.module'; import { ChatModule } from './chat.module'; import { ChatService } from './chat.service'; -import { ChatAudio } from './chat.model'; describe('ChatGateway', () => { let chatGateway: ChatGateway; @@ -34,17 +33,6 @@ describe('ChatGateway', () => { }); }); - describe('listenForAudio', () => { - it('should call chatService.transcription', async () => { - jest.spyOn(chatService, 'transcription').mockReturnThis(); - const request = { threadId: '123' } as ChatAudio; - - await chatGateway.listenForAudio(request, {} as Socket); - - expect(chatService.transcription).toHaveBeenCalledWith(request); - }); - }); - afterEach(() => { jest.clearAllMocks(); }); diff --git a/libs/ai-assistant/src/lib/chat/chat.gateway.ts b/libs/ai-assistant/src/lib/chat/chat.gateway.ts index 83df11b..d56b109 100644 --- a/libs/ai-assistant/src/lib/chat/chat.gateway.ts +++ b/libs/ai-assistant/src/lib/chat/chat.gateway.ts @@ -8,7 +8,7 @@ import { WebSocketServer, } from '@nestjs/websockets'; import { Server, Socket } from 'socket.io'; -import { ChatEvents, ChatAudio, ChatCall } from './chat.model'; +import { ChatEvents, ChatCallDto } from './chat.model'; import { ChatService } from './chat.service'; @WebSocketGateway({ @@ -30,7 +30,7 @@ export class ChatGateway implements OnGatewayConnection { @SubscribeMessage(ChatEvents.SendMessage) async listenForMessages( - @MessageBody() request: ChatCall, + @MessageBody() request: ChatCallDto, @ConnectedSocket() socket: Socket, ) { this.logger.log(`Socket "${ChatEvents.SendMessage}" (${socket.id}): @@ -45,21 +45,4 @@ export class ChatGateway implements OnGatewayConnection { * thread: ${message.threadId} * content: ${message.content}`); } - - @SubscribeMessage(ChatEvents.SendAudio) - async listenForAudio( - @MessageBody() request: ChatAudio, - @ConnectedSocket() socket: Socket, - ) { - this.logger.log(`Socket "${ChatEvents.SendAudio}" (${socket.id}): - * thread: ${request.threadId} - * file: ${request.file}`); - - const message = await this.chatsService.transcription(request); - - this.server?.to(socket.id).emit(ChatEvents.MessageReceived, message); - this.logger.log(`Socket "${ChatEvents.AudioReceived}" (${socket.id}): - * thread: ${message.threadId} - * file: ${message.content}`); - } } diff --git a/libs/ai-assistant/src/lib/chat/chat.model.ts b/libs/ai-assistant/src/lib/chat/chat.model.ts index 2b4150b..d12b62d 100644 --- a/libs/ai-assistant/src/lib/chat/chat.model.ts +++ b/libs/ai-assistant/src/lib/chat/chat.model.ts @@ -1,18 +1,7 @@ -export interface ChatCall { - content: string; - threadId: string; - file_ids?: string[]; - metadata?: Record; -} +import { ApiProperty } from '@nestjs/swagger'; export interface ChatAudio { file: File; - threadId: string; -} - -export interface ChatCallResponse { - content: string; - threadId: string; } export interface ChatAudioResponse { @@ -23,10 +12,30 @@ export interface ChatAudioResponse { export enum ChatEvents { SendMessage = 'send_message', MessageReceived = 'message_received', - SendAudio = 'send_audio', - AudioReceived = 'audio_received', } export enum MessageStatus { Invisible = 'invisible', } + +export class ChatCallResponseDto { + @ApiProperty() + threadId!: string; + + @ApiProperty() + content!: string; +} + +export class ChatCallDto { + @ApiProperty() + threadId!: string; + + @ApiProperty() + content!: string; + + @ApiProperty({ required: false }) + file_ids?: string[]; + + @ApiProperty({ required: false }) + metadata?: unknown | null; +} diff --git a/libs/ai-assistant/src/lib/chat/chat.module.ts b/libs/ai-assistant/src/lib/chat/chat.module.ts index a7c8ae3..9367eb2 100644 --- a/libs/ai-assistant/src/lib/chat/chat.module.ts +++ b/libs/ai-assistant/src/lib/chat/chat.module.ts @@ -5,13 +5,14 @@ import { ChatHelpers } from './chat.helpers'; import { ChatService } from './chat.service'; import { SocketModule } from '@nestjs/websockets/socket-module'; import { ChatGateway } from './chat.gateway'; +import { ChatController } from './chat.controller'; export const sharedServices = [ChatHelpers, ChatService]; @Module({ imports: [SocketModule, AiModule, RunModule], providers: [ChatGateway, ...sharedServices], - controllers: [], + controllers: [ChatController], exports: [...sharedServices], }) export class ChatModule {} diff --git a/libs/ai-assistant/src/lib/chat/chat.service.spec.ts b/libs/ai-assistant/src/lib/chat/chat.service.spec.ts index 220d798..952e85e 100644 --- a/libs/ai-assistant/src/lib/chat/chat.service.spec.ts +++ b/libs/ai-assistant/src/lib/chat/chat.service.spec.ts @@ -4,16 +4,14 @@ import { Run, ThreadMessage } from 'openai/resources/beta/threads'; import { AiModule } from './../ai/ai.module'; import { ChatModule } from './chat.module'; import { ChatService } from './chat.service'; -import { ChatAudio, ChatCall } from './chat.model'; import { ChatHelpers } from './chat.helpers'; import { RunService } from '../run'; -import { AiService } from '../ai'; +import { ChatCallDto } from './chat.model'; describe('ChatService', () => { let chatService: ChatService; let chatbotHelpers: ChatHelpers; let runService: RunService; - let aiService: AiService; beforeEach(async () => { const moduleRef = await Test.createTestingModule({ @@ -23,7 +21,6 @@ describe('ChatService', () => { chatService = moduleRef.get(ChatService); chatbotHelpers = moduleRef.get(ChatHelpers); runService = moduleRef.get(RunService); - aiService = moduleRef.get(AiService); jest .spyOn(chatbotHelpers, 'getAnswer') @@ -42,7 +39,7 @@ describe('ChatService', () => { describe('call', () => { it('should create "thread run"', async () => { - const payload = { content: 'Hello', threadId: '1' } as ChatCall; + const payload = { content: 'Hello', threadId: '1' } as ChatCallDto; const spyOnThreadRunsCreate = jest .spyOn(chatService.threads.runs, 'create') .mockResolvedValue({} as Run); @@ -53,7 +50,7 @@ describe('ChatService', () => { }); it('should return ChatCallResponse', async () => { - const payload = { content: 'Hello', threadId: '1' } as ChatCall; + const payload = { content: 'Hello', threadId: '1' } as ChatCallDto; jest .spyOn(chatService.threads.runs, 'create') .mockResolvedValue({} as Run); @@ -64,24 +61,6 @@ describe('ChatService', () => { }); }); - describe('transcription', () => { - it('should trigger call and transcription', async () => { - const payload = { file: 'file', threadId: '1' } as unknown as ChatAudio; - const spyOnTranscription = jest - .spyOn(aiService, 'transcription') - .mockResolvedValue({ text: 'Hello' }); - const spyOnCall = jest.spyOn(chatService, 'call').mockReturnThis(); - - await chatService.transcription(payload); - - expect(spyOnTranscription).toHaveBeenCalledWith(payload.file); - expect(spyOnCall).toHaveBeenCalledWith({ - threadId: payload.threadId, - content: 'Hello', - }); - }); - }); - afterEach(() => { jest.clearAllMocks(); }); diff --git a/libs/ai-assistant/src/lib/chat/chat.service.ts b/libs/ai-assistant/src/lib/chat/chat.service.ts index d9168b7..711f1be 100644 --- a/libs/ai-assistant/src/lib/chat/chat.service.ts +++ b/libs/ai-assistant/src/lib/chat/chat.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common'; import { MessageCreateParams } from 'openai/resources/beta/threads'; import { AiService } from '../ai'; import { RunService } from '../run'; -import { ChatAudio, ChatCall, ChatCallResponse } from './chat.model'; +import { ChatCallDto, ChatCallResponseDto } from './chat.model'; import { ChatHelpers } from './chat.helpers'; @Injectable() @@ -16,7 +16,7 @@ export class ChatService { private readonly chatbotHelpers: ChatHelpers, ) {} - async call(payload: ChatCall): Promise { + async call(payload: ChatCallDto): Promise { const { threadId, content, file_ids, metadata } = payload; const message: MessageCreateParams = { role: 'user', @@ -38,12 +38,4 @@ export class ChatService { threadId, }; } - - async transcription(payload: ChatAudio): Promise { - const transcription = await this.aiService.transcription(payload.file); - return await this.call({ - threadId: payload.threadId, - content: transcription.text, - }); - } } diff --git a/libs/ai-assistant/src/lib/files/files.controller.ts b/libs/ai-assistant/src/lib/files/files.controller.ts index 3f8f367..b899215 100644 --- a/libs/ai-assistant/src/lib/files/files.controller.ts +++ b/libs/ai-assistant/src/lib/files/files.controller.ts @@ -6,17 +6,24 @@ import { } from '@nestjs/common'; import { FileFieldsInterceptor } from '@nestjs/platform-express'; import { FilesService } from './files.service'; -import { UploadFileResponse } from './files.model'; +import { UploadFilesDto, UploadFilesResponseDto } from './files.model'; +import { ApiBody, ApiConsumes, ApiResponse, ApiTags } from '@nestjs/swagger'; +@ApiTags('Files') @Controller('assistant/files') export class FilesController { constructor(public readonly filesService: FilesService) {} + @ApiConsumes('multipart/form-data') + @ApiResponse({ status: 200, type: UploadFilesResponseDto }) + @ApiBody({ type: UploadFilesDto }) @Post('/') @UseInterceptors(FileFieldsInterceptor([{ name: 'files', maxCount: 10 }])) async updateFiles( @UploadedFiles() uploadedData: { files: Express.Multer.File[] }, - ): Promise { - return this.filesService.files(uploadedData.files); + ): Promise { + return { + files: await this.filesService.files(uploadedData.files), + }; } } diff --git a/libs/ai-assistant/src/lib/files/files.model.ts b/libs/ai-assistant/src/lib/files/files.model.ts index 069d596..1194de0 100644 --- a/libs/ai-assistant/src/lib/files/files.model.ts +++ b/libs/ai-assistant/src/lib/files/files.model.ts @@ -1,9 +1,51 @@ import { FileObject } from 'openai/resources'; +import { ApiProperty } from '@nestjs/swagger'; export type OpenAiFile = FileObject; -export type UploadFileResponse = OpenAiFile[]; - export interface UploadFilesPayload { files: File[]; } + +export class UploadFile { + @ApiProperty({ description: 'Unique identifier of the file' }) + id!: string; + + @ApiProperty() + bytes!: number; + + @ApiProperty({ description: 'Datetime the file was created.' }) + created_at!: number; + + @ApiProperty({ description: 'Name of the file' }) + filename!: string; + + @ApiProperty() + object!: 'file'; + + @ApiProperty() + purpose!: + | 'fine-tune' + | 'fine-tune-results' + | 'assistants' + | 'assistants_output'; + + @ApiProperty() + status!: 'uploaded' | 'processed' | 'error'; + + @ApiProperty() + status_details?: string; +} + +export class UploadFilesResponseDto { + @ApiProperty({ + isArray: true, + type: UploadFile, + }) + files!: UploadFile[]; +} + +export class UploadFilesDto { + @ApiProperty({ type: 'string', isArray: true, format: 'binary' }) + files!: File[]; +} diff --git a/libs/ai-assistant/src/lib/threads/threads.controller.spec.ts b/libs/ai-assistant/src/lib/threads/threads.controller.spec.ts index b2810a0..6fa86be 100644 --- a/libs/ai-assistant/src/lib/threads/threads.controller.spec.ts +++ b/libs/ai-assistant/src/lib/threads/threads.controller.spec.ts @@ -3,7 +3,7 @@ import { ThreadsController } from './threads.controller'; import { Thread } from 'openai/resources/beta'; import { ThreadsModule } from './threads.module'; import { ThreadsService } from './threads.service'; -import { ThreadResponse } from './threads.model'; +import { GetThreadResponseDto } from './threads.model'; describe('ThreadsController', () => { let threadsController: ThreadsController; @@ -27,7 +27,7 @@ describe('ThreadsController', () => { it('should call threadsService.getThread', async () => { const spyOnGetThread = jest .spyOn(threadsService, 'getThread') - .mockResolvedValue({} as ThreadResponse); + .mockResolvedValue({} as GetThreadResponseDto); await threadsController.getThread({ id: '1' }); diff --git a/libs/ai-assistant/src/lib/threads/threads.controller.ts b/libs/ai-assistant/src/lib/threads/threads.controller.ts index 3b03904..a292461 100644 --- a/libs/ai-assistant/src/lib/threads/threads.controller.ts +++ b/libs/ai-assistant/src/lib/threads/threads.controller.ts @@ -1,19 +1,32 @@ import { Body, Controller, Get, Param, Post } from '@nestjs/common'; -import { Thread } from 'openai/resources/beta'; -import { GetThreadParams, ThreadConfig, ThreadResponse } from './threads.model'; +import { + GetThreadDto, + CreateThreadDto, + CreateThreadResponseDto, + GetThreadResponseDto, +} from './threads.model'; import { ThreadsService } from './threads.service'; +import { ApiBody, ApiResponse, ApiTags } from '@nestjs/swagger'; +@ApiTags('Threads') @Controller('assistant/threads') export class ThreadsController { constructor(private readonly threadsService: ThreadsService) {} + @ApiResponse({ status: 200, type: GetThreadResponseDto }) @Get(':id') - async getThread(@Param() params: GetThreadParams): Promise { + async getThread( + @Param() params: GetThreadDto, + ): Promise { return await this.threadsService.getThread(params.id); } + @ApiResponse({ status: 200, type: CreateThreadResponseDto }) + @ApiBody({ type: CreateThreadDto, required: false }) @Post('') - async createThread(@Body() payload: ThreadConfig): Promise { + async createThread( + @Body() payload?: CreateThreadDto, + ): Promise { return await this.threadsService.createThread(payload); } } diff --git a/libs/ai-assistant/src/lib/threads/threads.model.ts b/libs/ai-assistant/src/lib/threads/threads.model.ts index 28a7d9a..8e19133 100644 --- a/libs/ai-assistant/src/lib/threads/threads.model.ts +++ b/libs/ai-assistant/src/lib/threads/threads.model.ts @@ -1,15 +1,101 @@ -import { ThreadCreateParams } from 'openai/resources/beta'; -import { ThreadMessage } from 'openai/resources/beta/threads'; +import { ApiProperty } from '@nestjs/swagger'; +import { + MessageContentImageFile, + MessageContentText, +} from 'openai/resources/beta/threads/messages/messages'; +import { IsOptional } from 'class-validator'; -export interface GetThreadParams { - id: string; +export class GetThreadDto { + @ApiProperty({ description: 'Unique identifier of the thread.' }) + @IsOptional() + id!: string; } -export interface ThreadConfig { - messages?: ThreadCreateParams.Message[]; +export class CreateThreadMessage { + @ApiProperty({ description: 'Content of the message' }) + content!: string; + + @ApiProperty({ description: 'Role of the message author.', enum: ['user'] }) + role!: 'user'; + + @ApiProperty({ description: 'File IDs', required: false }) + file_ids?: Array; + + @ApiProperty({ description: 'Metadata', required: false }) + metadata: unknown | null; +} + +export class CreateThreadDto { + @ApiProperty({ + description: 'Messages in the thread.', + type: CreateThreadMessage, + isArray: true, + required: false, + }) + messages?: CreateThreadMessage[]; +} + +export class ThreadMessage { + @ApiProperty({ description: 'Unique identifier of the thread.' }) + id!: string; + + @ApiProperty({ description: 'Identifier of the assistant' }) + assistant_id!: string | null; + + @ApiProperty({ + description: 'Content of the message in array of text and/or images.', + }) + content!: Array; + + @ApiProperty({ + description: 'Role of the message author.', + enum: ['user', 'assistant'], + }) + role!: 'user' | 'assistant'; + + @ApiProperty({ description: 'File IDs' }) + file_ids!: Array; + + @ApiProperty({ description: 'Metadata' }) + metadata: unknown | null; + + @ApiProperty({ description: 'Datetime the message was created.' }) + created_at!: number; + + @ApiProperty() + object!: 'thread.message'; + + @ApiProperty({ description: 'Run ID' }) + run_id!: string | null; + + @ApiProperty({ description: 'Thread ID' }) + thread_id!: string; } -export interface ThreadResponse { - id: string; - messages: ThreadMessage[]; +export class GetThreadResponseDto { + @ApiProperty({ + description: 'Unique identifier of the thread.', + }) + id!: string; + + @ApiProperty({ + description: 'Messages in the thread.', + type: ThreadMessage, + isArray: true, + }) + messages!: ThreadMessage[]; +} + +export class CreateThreadResponseDto { + @ApiProperty({ description: 'Unique identifier of the thread.' }) + id!: string; + + @ApiProperty({ description: 'Datetime the message was created.' }) + created_at!: number; + + @ApiProperty({ description: 'Metadata' }) + metadata: unknown | null; + + @ApiProperty() + object!: 'thread'; } diff --git a/libs/ai-assistant/src/lib/threads/threads.service.ts b/libs/ai-assistant/src/lib/threads/threads.service.ts index fb6539e..3cd4104 100644 --- a/libs/ai-assistant/src/lib/threads/threads.service.ts +++ b/libs/ai-assistant/src/lib/threads/threads.service.ts @@ -1,13 +1,13 @@ import { Injectable } from '@nestjs/common'; import { Thread } from 'openai/resources/beta'; import { AiService } from '../ai'; -import { ThreadConfig, ThreadResponse } from './threads.model'; +import { CreateThreadDto, GetThreadResponseDto } from './threads.model'; @Injectable() export class ThreadsService { constructor(private readonly aiService: AiService) {} - async getThread(id: string): Promise { + async getThread(id: string): Promise { const messages = await this.aiService.provider.beta.threads.messages.list( id, ); @@ -17,7 +17,7 @@ export class ThreadsService { }; } - async createThread({ messages }: ThreadConfig = {}): Promise { + async createThread({ messages }: CreateThreadDto = {}): Promise { return this.aiService.provider.beta.threads.create({ messages }); } } diff --git a/libs/ai-assistant/tsconfig.lib.json b/libs/ai-assistant/tsconfig.lib.json index c297a24..8926162 100644 --- a/libs/ai-assistant/tsconfig.lib.json +++ b/libs/ai-assistant/tsconfig.lib.json @@ -6,7 +6,6 @@ "types": ["node"], "target": "es2021", "strictNullChecks": true, - "noImplicitAny": true, "strictBindCallApply": true, "forceConsistentCasingInFileNames": true, "noFallthroughCasesInSwitch": true diff --git a/package-lock.json b/package-lock.json index b9dc59f..211ab9a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,8 +26,10 @@ "@nestjs/platform-express": "^10.0.2", "@nestjs/platform-socket.io": "^10.3.0", "@nestjs/serve-static": "^4.0.0", + "@nestjs/swagger": "^7.3.0", "@nestjs/websockets": "^10.3.0", "axios": "^1.0.0", + "class-validator": "^0.14.1", "envfile": "^7.1.0", "marked": "^9.1.6", "ngx-markdown": "^17.1.1", @@ -42,7 +44,7 @@ "zone.js": "~0.14.0" }, "devDependencies": { - "@angular-devkit/build-angular": "^17.1.0", + "@angular-devkit/build-angular": "^17.2.3", "@angular-devkit/core": "~17.0.0", "@angular-devkit/schematics": "~17.0.0", "@angular-eslint/eslint-plugin": "~17.0.0", @@ -124,12 +126,12 @@ } }, "node_modules/@angular-devkit/architect": { - "version": "0.1702.2", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1702.2.tgz", - "integrity": "sha512-qBvif8/NquFUqVQgs4U+8wXh/rQZv+YlYwg6eDZly1bIaTd/k9spko/seTtNT1OpK/Be+GLo5IbiQ7i2SON3iQ==", + "version": "0.1702.3", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1702.3.tgz", + "integrity": "sha512-4jeBgtBIZxAeJyiwSdbRE4+rWu34j0UMCKia8s7473rKj0Tn4+dXlHmA/kuFYIp6K/9pE/hBoeUFxLNA/DZuRQ==", "dev": true, "dependencies": { - "@angular-devkit/core": "17.2.2", + "@angular-devkit/core": "17.2.3", "rxjs": "7.8.1" }, "engines": { @@ -139,9 +141,9 @@ } }, "node_modules/@angular-devkit/architect/node_modules/@angular-devkit/core": { - "version": "17.2.2", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.2.2.tgz", - "integrity": "sha512-bKMi6bBkEeN4a3qTxCykhrAvE0ESHhKO38Qh1bN/8QSyvKVAEyVAVls5W9IN5GKRHvXgEn9aw+DSzRnPpy9nyw==", + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.2.3.tgz", + "integrity": "sha512-A7WWl1/VsZw6utFFPBib1wSbAB5OeBgAgQmVpVe9wW8u9UZa6CLc7b3InWtRRyBXTo9Sa5GNZDFfwlXhy3iW3w==", "dev": true, "dependencies": { "ajv": "8.12.0", @@ -166,15 +168,15 @@ } }, "node_modules/@angular-devkit/build-angular": { - "version": "17.2.2", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-17.2.2.tgz", - "integrity": "sha512-K55xBiWBfxD4wmxLR2viOPbBryOk6YaZeNr72IMkp1yIrIy1BES6LDJi7R9fDW7+TprqZdM4B91Tkc+BCwYQzQ==", + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-17.2.3.tgz", + "integrity": "sha512-AZsEHZj+k2Lxb7uQUwfEpSE6TvQhCoIgP6XLKgKxZHUOiTUVXDj84WhNcbup5SsSG1cafmoVN7APxxuSwHcoeg==", "dev": true, "dependencies": { "@ampproject/remapping": "2.2.1", - "@angular-devkit/architect": "0.1702.2", - "@angular-devkit/build-webpack": "0.1702.2", - "@angular-devkit/core": "17.2.2", + "@angular-devkit/architect": "0.1702.3", + "@angular-devkit/build-webpack": "0.1702.3", + "@angular-devkit/core": "17.2.3", "@babel/core": "7.23.9", "@babel/generator": "7.23.6", "@babel/helper-annotate-as-pure": "7.22.5", @@ -185,7 +187,7 @@ "@babel/preset-env": "7.23.9", "@babel/runtime": "7.23.9", "@discoveryjs/json-ext": "0.5.7", - "@ngtools/webpack": "17.2.2", + "@ngtools/webpack": "17.2.3", "@vitejs/plugin-basic-ssl": "1.1.0", "ansi-colors": "4.1.3", "autoprefixer": "10.4.17", @@ -295,9 +297,9 @@ } }, "node_modules/@angular-devkit/build-angular/node_modules/@angular-devkit/core": { - "version": "17.2.2", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.2.2.tgz", - "integrity": "sha512-bKMi6bBkEeN4a3qTxCykhrAvE0ESHhKO38Qh1bN/8QSyvKVAEyVAVls5W9IN5GKRHvXgEn9aw+DSzRnPpy9nyw==", + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.2.3.tgz", + "integrity": "sha512-A7WWl1/VsZw6utFFPBib1wSbAB5OeBgAgQmVpVe9wW8u9UZa6CLc7b3InWtRRyBXTo9Sa5GNZDFfwlXhy3iW3w==", "dev": true, "dependencies": { "ajv": "8.12.0", @@ -804,12 +806,12 @@ } }, "node_modules/@angular-devkit/build-webpack": { - "version": "0.1702.2", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1702.2.tgz", - "integrity": "sha512-+c7rHD2Se1VD9i9uPEYHqhq8hTnsUAn5LfeJCLS8g7FU8T42tDSC/k1qWxHp7d99kf7ecg2BvYcZDlYaBUnl3A==", + "version": "0.1702.3", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1702.3.tgz", + "integrity": "sha512-G9F2Ori8WxJtMvOQGxTdg7d+5aAO1IPeEtMiZwFPrw65Ey6Gvfm0h2+3FnQdzeKrZmGaTk5E6gffHXJJQfCnmQ==", "dev": true, "dependencies": { - "@angular-devkit/architect": "0.1702.2", + "@angular-devkit/architect": "0.1702.3", "rxjs": "7.8.1" }, "engines": { @@ -3851,12 +3853,6 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/@eslint/eslintrc/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -3882,18 +3878,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@eslint/eslintrc/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -4109,6 +4093,15 @@ "node": ">=8" } }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, "node_modules/@istanbuljs/load-nyc-config/node_modules/camelcase": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", @@ -4131,6 +4124,19 @@ "node": ">=8" } }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", @@ -5756,6 +5762,11 @@ "tslib": "^2.1.0" } }, + "node_modules/@microsoft/tsdoc": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.14.2.tgz", + "integrity": "sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug==" + }, "node_modules/@mole-inc/bin-wrapper": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/@mole-inc/bin-wrapper/-/bin-wrapper-8.0.1.tgz", @@ -5865,6 +5876,25 @@ } } }, + "node_modules/@nestjs/mapped-types": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.0.5.tgz", + "integrity": "sha512-bSJv4pd6EY99NX9CjBIyn4TVDoSit82DUZlL4I3bqNfy5Gt+gXTa86i3I/i0iIV9P4hntcGM5GyO+FhZAhxtyg==", + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "class-transformer": "^0.4.0 || ^0.5.0", + "class-validator": "^0.13.0 || ^0.14.0", + "reflect-metadata": "^0.1.12 || ^0.2.0" + }, + "peerDependenciesMeta": { + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, "node_modules/@nestjs/platform-express": { "version": "10.3.3", "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.3.3.tgz", @@ -6031,6 +6061,38 @@ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.2.5.tgz", "integrity": "sha512-l6qtdDPIkmAmzEO6egquYDfqQGPMRNGjYtrU13HAXb3YSRrt7HSb1sJY0pKp6o2bAa86tSB6iwaW2JbthPKr7Q==" }, + "node_modules/@nestjs/swagger": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-7.3.0.tgz", + "integrity": "sha512-zLkfKZ+ioYsIZ3dfv7Bj8YHnZMNAGWFUmx2ZDuLp/fBE4P8BSjB7hldzDueFXsmwaPL90v7lgyd82P+s7KME1Q==", + "dependencies": { + "@microsoft/tsdoc": "^0.14.2", + "@nestjs/mapped-types": "2.0.5", + "js-yaml": "4.1.0", + "lodash": "4.17.21", + "path-to-regexp": "3.2.0", + "swagger-ui-dist": "5.11.2" + }, + "peerDependencies": { + "@fastify/static": "^6.0.0 || ^7.0.0", + "@nestjs/common": "^9.0.0 || ^10.0.0", + "@nestjs/core": "^9.0.0 || ^10.0.0", + "class-transformer": "*", + "class-validator": "*", + "reflect-metadata": "^0.1.12 || ^0.2.0" + }, + "peerDependenciesMeta": { + "@fastify/static": { + "optional": true + }, + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, "node_modules/@nestjs/testing": { "version": "10.3.3", "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.3.3.tgz", @@ -6081,9 +6143,9 @@ } }, "node_modules/@ngtools/webpack": { - "version": "17.2.2", - "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-17.2.2.tgz", - "integrity": "sha512-HgvClGO6WVq4VA5d0ZvlDG5hrj8lQzRH99Gt87URm7G8E5XkatysdOsMqUQsJz+OwFWhP4PvTRWVblpBDiDl/A==", + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-17.2.3.tgz", + "integrity": "sha512-+d5Q7/ctDHePYZXcg0GFwL/AbyEkPMHoCiT7pmLI0B0n87D/mYKK/qmVN1VANBrFLTuIe8RtcL0aJ9pw8HAxWA==", "dev": true, "engines": { "node": "^18.13.0 || >=20.9.0", @@ -6946,12 +7008,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/@nx/angular/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, "node_modules/@nx/angular/node_modules/array-union": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/array-union/-/array-union-3.0.1.tgz", @@ -7103,18 +7159,6 @@ "node": ">=0.10.0" } }, - "node_modules/@nx/angular/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, "node_modules/@nx/angular/node_modules/jsonc-parser": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", @@ -7515,28 +7559,6 @@ "@nx/eslint": "17.3.2" } }, - "node_modules/@nx/cypress/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "optional": true, - "peer": true - }, - "node_modules/@nx/cypress/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, "node_modules/@nx/cypress/node_modules/typescript": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", @@ -8980,12 +9002,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/@nx/js/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, "node_modules/@nx/js/node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -9057,18 +9073,6 @@ "node": ">=8" } }, - "node_modules/@nx/js/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, "node_modules/@nx/js/node_modules/jsonc-parser": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", @@ -12318,6 +12322,11 @@ "integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==", "optional": true }, + "node_modules/@types/validator": { + "version": "13.11.9", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.11.9.tgz", + "integrity": "sha512-FCTsikRozryfayPuiI46QzH3fnrOoctTjvOYZkho9BTFLCOZ2rgZJHMOVgCOfttjPJcgOx52EpkY0CMfy87MIw==" + }, "node_modules/@types/ws": { "version": "8.5.10", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", @@ -12904,6 +12913,28 @@ "node": ">=14.15.0" } }, + "node_modules/@yarnpkg/parsers/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@yarnpkg/parsers/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/@zkochan/js-yaml": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/@zkochan/js-yaml/-/js-yaml-0.0.6.tgz", @@ -12916,12 +12947,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/@zkochan/js-yaml/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, "node_modules/abab": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", @@ -13241,13 +13266,9 @@ "dev": true }, "node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "dependencies": { - "sprintf-js": "~1.0.2" - } + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, "node_modules/aria-query": { "version": "5.3.0", @@ -14437,6 +14458,16 @@ "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==", "dev": true }, + "node_modules/class-validator": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.1.tgz", + "integrity": "sha512-2VEG9JICxIqTpoK1eMzZqaV+u/EiwEJkMGzTrZf6sU/fwsnOITVgYJ8yojSy6CaXtO9V0Cc6ZQZ8h8m4UBuLwQ==", + "dependencies": { + "@types/validator": "^13.11.8", + "libphonenumber-js": "^1.10.53", + "validator": "^13.9.0" + } + }, "node_modules/clean-stack": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", @@ -15014,24 +15045,6 @@ } } }, - "node_modules/cosmiconfig/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "node_modules/cosmiconfig/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, "node_modules/create-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", @@ -17019,12 +17032,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/eslint/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, "node_modules/eslint/node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -17117,18 +17124,6 @@ "node": ">=8" } }, - "node_modules/eslint/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, "node_modules/eslint/node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -21245,13 +21240,11 @@ "dev": true }, "node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "dev": true, + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" + "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" @@ -21633,6 +21626,11 @@ "node": ">= 0.8.0" } }, + "node_modules/libphonenumber-js": { + "version": "1.10.57", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.10.57.tgz", + "integrity": "sha512-OjsEd9y4LgcX+Ig09SbxWqcGESxliDDFNVepFhB9KEsQZTrnk3UdEU+cO0sW1APvLprHstQpS23OQpZ3bwxy6Q==" + }, "node_modules/license-webpack-plugin": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/license-webpack-plugin/-/license-webpack-plugin-4.0.2.tgz", @@ -23476,12 +23474,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/nx/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, "node_modules/nx/node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -23564,18 +23556,6 @@ "node": ">=8" } }, - "node_modules/nx/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, "node_modules/nx/node_modules/jsonc-parser": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", @@ -24521,12 +24501,6 @@ } } }, - "node_modules/postcss-loader/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, "node_modules/postcss-loader/node_modules/cosmiconfig": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", @@ -24553,18 +24527,6 @@ } } }, - "node_modules/postcss-loader/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, "node_modules/postcss-merge-longhand": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-6.0.3.tgz", @@ -26927,6 +26889,11 @@ "url": "https://opencollective.com/svgo" } }, + "node_modules/swagger-ui-dist": { + "version": "5.11.2", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.11.2.tgz", + "integrity": "sha512-jQG0cRgJNMZ7aCoiFofnoojeSaa/+KgWaDlfgs8QN+BXoGMpxeMVY5OEnjq4OlNvF3yjftO8c9GRAgcHlO+u7A==" + }, "node_modules/swc-loader": { "version": "0.1.15", "resolved": "https://registry.npmjs.org/swc-loader/-/swc-loader-0.1.15.tgz", @@ -28093,6 +28060,14 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/validator": { + "version": "13.11.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.11.0.tgz", + "integrity": "sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/package.json b/package.json index 675668d..746ad84 100644 --- a/package.json +++ b/package.json @@ -37,8 +37,10 @@ "@nestjs/platform-express": "^10.0.2", "@nestjs/platform-socket.io": "^10.3.0", "@nestjs/serve-static": "^4.0.0", + "@nestjs/swagger": "^7.3.0", "@nestjs/websockets": "^10.3.0", "axios": "^1.0.0", + "class-validator": "^0.14.1", "envfile": "^7.1.0", "marked": "^9.1.6", "ngx-markdown": "^17.1.1", @@ -53,7 +55,7 @@ "zone.js": "~0.14.0" }, "devDependencies": { - "@angular-devkit/build-angular": "^17.1.0", + "@angular-devkit/build-angular": "^17.2.3", "@angular-devkit/core": "~17.0.0", "@angular-devkit/schematics": "~17.0.0", "@angular-eslint/eslint-plugin": "~17.0.0",