diff --git a/cortex-js/src/app.module.ts b/cortex-js/src/app.module.ts index d97c018d2..2fd0cbee6 100644 --- a/cortex-js/src/app.module.ts +++ b/cortex-js/src/app.module.ts @@ -17,6 +17,14 @@ import { AppLoggerMiddleware } from './infrastructure/middlewares/app.logger.mid import { EventEmitterModule } from '@nestjs/event-emitter'; import { DownloadManagerModule } from './download-manager/download-manager.module'; import { EventsController } from './infrastructure/controllers/events.controller'; +import { AppController } from './infrastructure/controllers/app.controller'; +import { AssistantsController } from './infrastructure/controllers/assistants.controller'; +import { ChatController } from './infrastructure/controllers/chat.controller'; +import { EmbeddingsController } from './infrastructure/controllers/embeddings.controller'; +import { ModelsController } from './infrastructure/controllers/models.controller'; +import { ThreadsController } from './infrastructure/controllers/threads.controller'; +import { StatusController } from './infrastructure/controllers/status.controller'; +import { ProcessController } from './infrastructure/controllers/process.controller'; @Module({ imports: [ @@ -40,7 +48,17 @@ import { EventsController } from './infrastructure/controllers/events.controller ModelRepositoryModule, DownloadManagerModule, ], - controllers: [EventsController], + controllers: [ + AppController, + AssistantsController, + ChatController, + EmbeddingsController, + ModelsController, + ThreadsController, + StatusController, + ProcessController, + EventsController, + ], providers: [SeedService], }) export class AppModule implements NestModule { diff --git a/cortex-js/src/infrastructure/commanders/ps.command.ts b/cortex-js/src/infrastructure/commanders/ps.command.ts index 9aa4ace3a..944aeb12b 100644 --- a/cortex-js/src/infrastructure/commanders/ps.command.ts +++ b/cortex-js/src/infrastructure/commanders/ps.command.ts @@ -10,6 +10,12 @@ export class PSCommand extends CommandRunner { super(); } async run(): Promise { - return this.usecases.getModels().then(console.table); + return this.usecases + .getModels() + .then(console.table) + .then(() => this.usecases.isAPIServerOnline()) + .then((isOnline) => { + if (isOnline) console.log('API server is online'); + }); } } diff --git a/cortex-js/src/infrastructure/commanders/serve.command.ts b/cortex-js/src/infrastructure/commanders/serve.command.ts index 99575bcb2..181333e2d 100644 --- a/cortex-js/src/infrastructure/commanders/serve.command.ts +++ b/cortex-js/src/infrastructure/commanders/serve.command.ts @@ -1,5 +1,6 @@ import { spawn } from 'child_process'; import { + CORTEX_JS_STOP_API_SERVER_URL, defaultCortexJsHost, defaultCortexJsPort, } from '@/infrastructure/constants/cortex'; @@ -9,6 +10,7 @@ import { join } from 'path'; type ServeOptions = { host?: string; port?: number; + attach: boolean; }; @SubCommand({ @@ -20,7 +22,25 @@ export class ServeCommand extends CommandRunner { const host = options?.host || defaultCortexJsHost; const port = options?.port || defaultCortexJsPort; - spawn( + if (_input[0] === 'stop') { + return this.stopServer().then(() => console.log('API server stopped')); + } else { + return this.startServer(host, port, options); + } + } + + private async stopServer() { + return fetch(CORTEX_JS_STOP_API_SERVER_URL(), { + method: 'DELETE', + }).catch(() => {}); + } + + private async startServer( + host: string, + port: number, + options: ServeOptions = { attach: true }, + ) { + const serveProcess = spawn( 'node', process.env.TEST ? [join(__dirname, '../../../dist/src/main.js')] @@ -32,10 +52,14 @@ export class ServeCommand extends CommandRunner { CORTEX_JS_PORT: port.toString(), NODE_ENV: 'production', }, - stdio: 'inherit', - detached: false, + stdio: options?.attach ? 'inherit' : 'ignore', + detached: true, }, ); + if (!options?.attach) { + serveProcess.unref(); + console.log('Started server at http://%s:%d', host, port); + } } @Option({ @@ -53,4 +77,14 @@ export class ServeCommand extends CommandRunner { parsePort(value: string) { return parseInt(value, 10); } + + @Option({ + flags: '-a, --attach', + description: 'Attach to interactive chat session', + defaultValue: false, + name: 'attach', + }) + parseAttach() { + return true; + } } diff --git a/cortex-js/src/infrastructure/commanders/usecases/ps.cli.usecases.ts b/cortex-js/src/infrastructure/commanders/usecases/ps.cli.usecases.ts index 9223cc5cb..0ad57b2b9 100644 --- a/cortex-js/src/infrastructure/commanders/usecases/ps.cli.usecases.ts +++ b/cortex-js/src/infrastructure/commanders/usecases/ps.cli.usecases.ts @@ -1,8 +1,11 @@ import { HttpStatus, Injectable } from '@nestjs/common'; import { CORTEX_CPP_MODELS_URL, + CORTEX_JS_HEALTH_URL, defaultCortexCppHost, defaultCortexCppPort, + defaultCortexJsHost, + defaultCortexJsPort, } from '@/infrastructure/constants/cortex'; import { HttpService } from '@nestjs/axios'; import { firstValueFrom } from 'rxjs'; @@ -56,6 +59,23 @@ export class PSCliUsecases { ).catch(() => []); } + /** + * Check if the Cortex API server is online + * @param host Cortex host address + * @param port Cortex port address + * @returns + */ + async isAPIServerOnline( + host: string = defaultCortexJsHost, + port: number = defaultCortexJsPort, + ): Promise { + return firstValueFrom( + this.httpService.get(CORTEX_JS_HEALTH_URL(host, port)), + ) + .then((res) => res.status === HttpStatus.OK) + .catch(() => false); + } + private formatDuration(milliseconds: number): string { const days = Math.floor(milliseconds / (1000 * 60 * 60 * 24)); const hours = Math.floor( diff --git a/cortex-js/src/infrastructure/constants/cortex.ts b/cortex-js/src/infrastructure/constants/cortex.ts index 09707dd1a..0f6cd9778 100644 --- a/cortex-js/src/infrastructure/constants/cortex.ts +++ b/cortex-js/src/infrastructure/constants/cortex.ts @@ -28,6 +28,16 @@ export const CORTEX_CPP_MODELS_URL = ( port: number = defaultCortexCppPort, ) => `http://${host}:${port}/inferences/server/models`; +export const CORTEX_JS_HEALTH_URL = ( + host: string = defaultCortexJsHost, + port: number = defaultCortexJsPort, +) => `http://${host}:${port}/health`; + +export const CORTEX_JS_STOP_API_SERVER_URL = ( + host: string = defaultCortexJsHost, + port: number = defaultCortexJsPort, +) => `http://${host}:${port}/process`; + // INITIALIZATION export const CORTEX_RELEASES_URL = 'https://api.github.com/repos/janhq/cortex/releases'; diff --git a/cortex-js/src/infrastructure/controllers/process.controller.ts b/cortex-js/src/infrastructure/controllers/process.controller.ts new file mode 100644 index 000000000..7c8a932a7 --- /dev/null +++ b/cortex-js/src/infrastructure/controllers/process.controller.ts @@ -0,0 +1,17 @@ +import { Controller, Delete } from '@nestjs/common'; +import { ApiOperation, ApiTags } from '@nestjs/swagger'; + +@ApiTags('Processes') +@Controller('process') +export class ProcessController { + constructor() {} + + @ApiOperation({ + summary: 'Terminate service', + description: 'Terminate service endpoint', + }) + @Delete() + async delete() { + process.exit(0); + } +} diff --git a/cortex-js/src/infrastructure/controllers/status.controller.ts b/cortex-js/src/infrastructure/controllers/status.controller.ts new file mode 100644 index 000000000..80116d9ea --- /dev/null +++ b/cortex-js/src/infrastructure/controllers/status.controller.ts @@ -0,0 +1,22 @@ +import { Controller, HttpCode, Get } from '@nestjs/common'; +import { ApiOperation, ApiTags, ApiResponse } from '@nestjs/swagger'; + +@ApiTags('Status') +@Controller('health') +export class StatusController { + constructor() {} + + @ApiOperation({ + summary: 'Health check', + description: 'Health check endpoint.', + }) + @HttpCode(200) + @ApiResponse({ + status: 200, + description: 'Ok', + }) + @Get() + async get() { + return 'OK'; + } +} diff --git a/cortex-js/src/main.ts b/cortex-js/src/main.ts index 8209b8963..734ab9b07 100644 --- a/cortex-js/src/main.ts +++ b/cortex-js/src/main.ts @@ -70,7 +70,7 @@ async function bootstrap() { const port = process.env.CORTEX_JS_PORT || defaultCortexJsPort; await app.listen(port, host); - console.log(`Server running on http://${host}:${port}`); + console.log(`Started server at http://${host}:${port}`); } const buildSwagger = (app: INestApplication) => { diff --git a/cortex-js/src/usecases/assistants/assistants.module.ts b/cortex-js/src/usecases/assistants/assistants.module.ts index 1af8a9624..1fec26c19 100644 --- a/cortex-js/src/usecases/assistants/assistants.module.ts +++ b/cortex-js/src/usecases/assistants/assistants.module.ts @@ -1,11 +1,10 @@ import { Module } from '@nestjs/common'; -import { AssistantsController } from '@/infrastructure/controllers/assistants.controller'; import { AssistantsUsecases } from './assistants.usecases'; import { DatabaseModule } from '@/infrastructure/database/database.module'; @Module({ imports: [DatabaseModule], - controllers: [AssistantsController], + controllers: [], providers: [AssistantsUsecases], exports: [AssistantsUsecases], }) diff --git a/cortex-js/src/usecases/chat/chat.module.ts b/cortex-js/src/usecases/chat/chat.module.ts index 39395c657..1cfd7e533 100644 --- a/cortex-js/src/usecases/chat/chat.module.ts +++ b/cortex-js/src/usecases/chat/chat.module.ts @@ -1,15 +1,13 @@ import { Module } from '@nestjs/common'; -import { ChatController } from '@/infrastructure/controllers/chat.controller'; import { ChatUsecases } from './chat.usecases'; import { DatabaseModule } from '@/infrastructure/database/database.module'; import { ExtensionModule } from '@/infrastructure/repositories/extensions/extension.module'; import { ModelRepositoryModule } from '@/infrastructure/repositories/models/model.module'; import { HttpModule } from '@nestjs/axios'; -import { EmbeddingsController } from '@/infrastructure/controllers/embeddings.controller'; @Module({ imports: [DatabaseModule, ExtensionModule, ModelRepositoryModule, HttpModule], - controllers: [ChatController, EmbeddingsController], + controllers: [], providers: [ChatUsecases], exports: [ChatUsecases], }) diff --git a/cortex-js/src/usecases/models/models.module.ts b/cortex-js/src/usecases/models/models.module.ts index 53d2a69d8..197d6c05d 100644 --- a/cortex-js/src/usecases/models/models.module.ts +++ b/cortex-js/src/usecases/models/models.module.ts @@ -1,6 +1,5 @@ import { Module } from '@nestjs/common'; import { ModelsUsecases } from './models.usecases'; -import { ModelsController } from '@/infrastructure/controllers/models.controller'; import { DatabaseModule } from '@/infrastructure/database/database.module'; import { CortexModule } from '@/usecases/cortex/cortex.module'; import { ExtensionModule } from '@/infrastructure/repositories/extensions/extension.module'; @@ -19,7 +18,7 @@ import { DownloadManagerModule } from '@/download-manager/download-manager.modul ModelRepositoryModule, DownloadManagerModule, ], - controllers: [ModelsController], + controllers: [], providers: [ModelsUsecases], exports: [ModelsUsecases], }) diff --git a/cortex-js/src/usecases/threads/threads.module.ts b/cortex-js/src/usecases/threads/threads.module.ts index f9c828f00..b11756f27 100644 --- a/cortex-js/src/usecases/threads/threads.module.ts +++ b/cortex-js/src/usecases/threads/threads.module.ts @@ -1,11 +1,10 @@ import { Module } from '@nestjs/common'; import { ThreadsUsecases } from './threads.usecases'; -import { ThreadsController } from '@/infrastructure/controllers/threads.controller'; import { DatabaseModule } from '@/infrastructure/database/database.module'; @Module({ imports: [DatabaseModule], - controllers: [ThreadsController], + controllers: [], providers: [ThreadsUsecases], exports: [ThreadsUsecases], })