Skip to content
This repository was archived by the owner on Jul 4, 2025. It is now read-only.

Commit 90136a4

Browse files
authored
Merge pull request #697 from janhq/feat/add-model-event
feat: add model event
2 parents 06b2e40 + 3b2b882 commit 90136a4

File tree

10 files changed

+209
-15
lines changed

10 files changed

+209
-15
lines changed
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
export type ModelId = string;
2+
3+
const ModelLoadingEvents = [
4+
'starting',
5+
'stopping',
6+
'started',
7+
'stopped',
8+
'starting-failed',
9+
'stopping-failed',
10+
] as const;
11+
export type ModelLoadingEvent = (typeof ModelLoadingEvents)[number];
12+
13+
const AllModelStates = ['starting', 'stopping', 'started'] as const;
14+
export type ModelState = (typeof AllModelStates)[number];
15+
16+
export interface ModelStatus {
17+
model: ModelId;
18+
status: ModelState;
19+
metadata: Record<string, unknown>;
20+
}
21+
22+
export interface ModelEvent {
23+
model: ModelId;
24+
event: ModelLoadingEvent;
25+
metadata: Record<string, unknown>;
26+
}
27+
28+
export const EmptyModelEvent = {};
29+
30+
export interface ModelStatusAndEvent {
31+
data: {
32+
status: Record<ModelId, ModelStatus>;
33+
event: ModelEvent | typeof EmptyModelEvent;
34+
};
35+
}

cortex-js/src/infrastructure/commanders/test/helpers.command.spec.ts

Lines changed: 46 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@ import { spy, Stub, stubMethod } from 'hanbi';
33
import { CommandTestFactory } from 'nest-commander-testing';
44
import { CommandModule } from '@/command.module';
55
import { LogService } from '@/infrastructure/commanders/test/log.service';
6+
import { FileManagerService } from '@/infrastructure/services/file-manager/file-manager.service';
7+
68
import axios from 'axios';
9+
import { join } from 'path';
10+
import { rmSync } from 'fs';
711

812
let commandInstance: TestingModule,
913
exitSpy: Stub<typeof process.exit>,
@@ -24,9 +28,29 @@ beforeEach(
2428
.overrideProvider(LogService)
2529
.useValue({ log: spy().handler })
2630
.compile();
27-
res();
2831
stdoutSpy.reset();
2932
stderrSpy.reset();
33+
34+
const fileService =
35+
await commandInstance.resolve<FileManagerService>(FileManagerService);
36+
37+
// Attempt to create test folder
38+
await fileService.writeConfigFile({
39+
dataFolderPath: join(__dirname, 'test_data'),
40+
});
41+
res();
42+
}),
43+
);
44+
45+
afterEach(
46+
() =>
47+
new Promise<void>(async (res) => {
48+
// Attempt to clean test folder
49+
rmSync(join(__dirname, 'test_data'), {
50+
recursive: true,
51+
force: true,
52+
});
53+
res();
3054
}),
3155
);
3256

@@ -44,19 +68,28 @@ describe('Helper commands', () => {
4468
timeout,
4569
);
4670

47-
test('Chat with option -m', async () => {
48-
const logMock = stubMethod(console, 'log');
71+
test(
72+
'Chat with option -m',
73+
async () => {
74+
const logMock = stubMethod(console, 'log');
4975

50-
await CommandTestFactory.run(commandInstance, [
51-
'chat',
52-
// '-m',
53-
// 'hello',
54-
// '>output.txt',
55-
]);
56-
expect(logMock.firstCall?.args[0]).toBe("Inorder to exit, type 'exit()'.");
57-
// expect(exitSpy.callCount).toBe(1);
58-
// expect(exitSpy.firstCall?.args[0]).toBe(1);
59-
});
76+
await CommandTestFactory.run(commandInstance, [
77+
'run',
78+
'tinyllama',
79+
// '-m',
80+
// 'hello',
81+
// '>output.txt',
82+
]);
83+
expect(
84+
logMock.firstCall?.args[0] === "Inorder to exit, type 'exit()'." ||
85+
logMock.firstCall?.args[0] ===
86+
'Model tinyllama not found. Try pulling model...',
87+
).toBeTruthy();
88+
// expect(exitSpy.callCount).toBe(1);
89+
// expect(exitSpy.firstCall?.args[0]).toBe(1);
90+
},
91+
timeout,
92+
);
6093

6194
test('Show / kill running models', async () => {
6295
const tableMock = stubMethod(console, 'table');

cortex-js/src/infrastructure/commanders/test/models.command.spec.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { CommandModule } from '@/command.module';
55
import { join } from 'path';
66
import { rmSync } from 'fs';
77
import { timeout } from '@/infrastructure/commanders/test/helpers.command.spec';
8+
import { FileManagerService } from '@/infrastructure/services/file-manager/file-manager.service';
89

910
let commandInstance: TestingModule;
1011

@@ -17,6 +18,14 @@ beforeEach(
1718
// .overrideProvider(LogService)
1819
// .useValue({})
1920
.compile();
21+
const fileService =
22+
await commandInstance.resolve<FileManagerService>(FileManagerService);
23+
24+
// Attempt to create test folder
25+
await fileService.writeConfigFile({
26+
dataFolderPath: join(__dirname, 'test_data'),
27+
});
28+
2029
res();
2130
}),
2231
);

cortex-js/src/infrastructure/controllers/chat.controller.spec.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,22 @@ import { ModelRepositoryModule } from '../repositories/models/model.module';
77
import { HttpModule } from '@nestjs/axios';
88
import { DownloadManagerModule } from '@/download-manager/download-manager.module';
99
import { EventEmitterModule } from '@nestjs/event-emitter';
10+
import { TelemetryModule } from '@/usecases/telemetry/telemetry.module';
1011

1112
describe('ChatController', () => {
1213
let controller: ChatController;
1314

1415
beforeEach(async () => {
1516
const module: TestingModule = await Test.createTestingModule({
1617
imports: [
18+
EventEmitterModule.forRoot(),
1719
DatabaseModule,
1820
ExtensionModule,
1921
ModelRepositoryModule,
2022
HttpModule,
2123
DownloadManagerModule,
2224
EventEmitterModule.forRoot(),
25+
TelemetryModule,
2326
],
2427
controllers: [ChatController],
2528
providers: [ChatUsecases],

cortex-js/src/infrastructure/controllers/embeddings.controller.spec.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,22 @@ import { ExtensionModule } from '../repositories/extensions/extension.module';
77
import { HttpModule } from '@nestjs/axios';
88
import { DownloadManagerModule } from '@/download-manager/download-manager.module';
99
import { EventEmitterModule } from '@nestjs/event-emitter';
10+
import { TelemetryModule } from '@/usecases/telemetry/telemetry.module';
1011

1112
describe('EmbeddingsController', () => {
1213
let controller: EmbeddingsController;
1314

1415
beforeEach(async () => {
1516
const module: TestingModule = await Test.createTestingModule({
1617
imports: [
18+
EventEmitterModule.forRoot(),
1719
DatabaseModule,
1820
ModelRepositoryModule,
1921
ExtensionModule,
2022
HttpModule,
2123
DownloadManagerModule,
2224
EventEmitterModule.forRoot(),
25+
TelemetryModule,
2326
],
2427
controllers: [EmbeddingsController],
2528
providers: [ChatUsecases],

cortex-js/src/infrastructure/controllers/events.controller.ts

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,40 @@ import {
22
DownloadState,
33
DownloadStateEvent,
44
} from '@/domain/models/download.interface';
5+
import {
6+
EmptyModelEvent,
7+
ModelEvent,
8+
ModelId,
9+
ModelStatus,
10+
ModelStatusAndEvent,
11+
} from '@/domain/models/model.event';
512
import { DownloadManagerService } from '@/download-manager/download-manager.service';
13+
import { ModelsUsecases } from '@/usecases/models/models.usecases';
614
import { Controller, Sse } from '@nestjs/common';
715
import { EventEmitter2 } from '@nestjs/event-emitter';
8-
import { Observable, fromEvent, map, merge, of, throttleTime } from 'rxjs';
16+
import { ApiTags } from '@nestjs/swagger';
17+
import {
18+
Observable,
19+
combineLatest,
20+
fromEvent,
21+
map,
22+
merge,
23+
of,
24+
startWith,
25+
throttleTime,
26+
} from 'rxjs';
927

28+
@ApiTags('Events')
1029
@Controller('events')
1130
export class EventsController {
1231
constructor(
1332
private readonly downloadManagerService: DownloadManagerService,
33+
private readonly modelsUsecases: ModelsUsecases,
1434
private readonly eventEmitter: EventEmitter2,
1535
) {}
1636

1737
@Sse('download')
1838
downloadEvent(): Observable<DownloadStateEvent> {
19-
// Welcome message Observable
2039
const latestDownloadState$: Observable<DownloadStateEvent> = of({
2140
data: this.downloadManagerService.getDownloadStates(),
2241
});
@@ -40,4 +59,20 @@ export class EventsController {
4059
downloadAbortEvent$,
4160
).pipe();
4261
}
62+
63+
@Sse('model')
64+
modelEvent(): Observable<ModelStatusAndEvent> {
65+
const latestModelStatus$: Observable<Record<ModelId, ModelStatus>> = of(
66+
this.modelsUsecases.getModelStatuses(),
67+
);
68+
69+
const modelEvent$ = fromEvent<ModelEvent>(
70+
this.eventEmitter,
71+
'model.event',
72+
).pipe(startWith(EmptyModelEvent));
73+
74+
return combineLatest([latestModelStatus$, modelEvent$]).pipe(
75+
map(([status, event]) => ({ data: { status, event } })),
76+
);
77+
}
4378
}

cortex-js/src/infrastructure/controllers/models.controller.spec.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,26 @@ import { CortexUsecases } from '@/usecases/cortex/cortex.usecases';
99
import { ModelRepositoryModule } from '../repositories/models/model.module';
1010
import { DownloadManagerModule } from '@/download-manager/download-manager.module';
1111
import { EventEmitterModule } from '@nestjs/event-emitter';
12+
import { TelemetryModule } from '@/usecases/telemetry/telemetry.module';
13+
import { UtilModule } from '@/util/util.module';
1214

1315
describe('ModelsController', () => {
1416
let controller: ModelsController;
1517

1618
beforeEach(async () => {
1719
const module: TestingModule = await Test.createTestingModule({
1820
imports: [
21+
EventEmitterModule.forRoot(),
1922
DatabaseModule,
2023
ExtensionModule,
2124
FileManagerModule,
2225
HttpModule,
26+
DownloadManagerModule,
2327
ModelRepositoryModule,
2428
DownloadManagerModule,
2529
EventEmitterModule.forRoot(),
30+
TelemetryModule,
31+
UtilModule,
2632
],
2733
controllers: [ModelsController],
2834
providers: [ModelsUsecases, CortexUsecases],

cortex-js/src/usecases/chat/chat.usecases.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ describe('ChatService', () => {
1414
beforeEach(async () => {
1515
const module: TestingModule = await Test.createTestingModule({
1616
imports: [
17+
EventEmitterModule.forRoot(),
1718
DatabaseModule,
1819
ExtensionModule,
1920
ModelRepositoryModule,

cortex-js/src/usecases/models/models.usecases.spec.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,28 @@ import { HttpModule } from '@nestjs/axios';
88
import { ModelRepositoryModule } from '@/infrastructure/repositories/models/model.module';
99
import { DownloadManagerModule } from '@/download-manager/download-manager.module';
1010
import { EventEmitterModule } from '@nestjs/event-emitter';
11+
import { TelemetryModule } from '../telemetry/telemetry.module';
12+
import { UtilModule } from '@/util/util.module';
1113

1214
describe('ModelsService', () => {
1315
let service: ModelsUsecases;
1416

1517
beforeEach(async () => {
1618
const module: TestingModule = await Test.createTestingModule({
1719
imports: [
20+
EventEmitterModule.forRoot(),
1821
DatabaseModule,
1922
ModelsModule,
2023
ExtensionModule,
2124
FileManagerModule,
25+
DownloadManagerModule,
2226
HttpModule,
2327
ModelRepositoryModule,
2428
DownloadManagerModule,
2529
EventEmitterModule.forRoot(),
30+
TelemetryModule,
31+
TelemetryModule,
32+
UtilModule,
2633
],
2734
providers: [ModelsUsecases],
2835
exports: [ModelsUsecases],

0 commit comments

Comments
 (0)