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

Commit 9550b03

Browse files
committed
feat: add query messages of thread api
Signed-off-by: James <namnh0122@gmail.com>
1 parent bf665bf commit 9550b03

File tree

4 files changed

+156
-2
lines changed

4 files changed

+156
-2
lines changed

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

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,24 @@ import {
77
Param,
88
Delete,
99
UseInterceptors,
10+
HttpCode,
11+
Query,
12+
DefaultValuePipe,
1013
} from '@nestjs/common';
1114
import { ThreadsUsecases } from '@/usecases/threads/threads.usecases';
1215
import { CreateThreadDto } from '@/infrastructure/dtos/threads/create-thread.dto';
1316
import { UpdateThreadDto } from '@/infrastructure/dtos/threads/update-thread.dto';
1417
import { DeleteThreadResponseDto } from '@/infrastructure/dtos/threads/delete-thread.dto';
1518
import { GetThreadResponseDto } from '@/infrastructure/dtos/threads/get-thread.dto';
16-
import { ApiOperation, ApiParam, ApiTags, ApiResponse } from '@nestjs/swagger';
19+
import {
20+
ApiOperation,
21+
ApiParam,
22+
ApiTags,
23+
ApiResponse,
24+
ApiQuery,
25+
} from '@nestjs/swagger';
1726
import { TransformInterceptor } from '../interceptors/transform.interceptor';
27+
import { ListMessagesResponseDto } from '../dtos/messages/list-message.dto';
1828

1929
@ApiTags('Threads')
2030
@Controller('threads')
@@ -41,6 +51,74 @@ export class ThreadsController {
4151
return this.threadsService.findAll();
4252
}
4353

54+
@HttpCode(200)
55+
@ApiResponse({
56+
status: 200,
57+
description: 'A list of message objects.',
58+
type: ListMessagesResponseDto,
59+
})
60+
@ApiOperation({
61+
summary: 'List messages',
62+
description: 'Returns a list of messages for a given thread.',
63+
})
64+
@ApiParam({
65+
name: 'id',
66+
required: true,
67+
description: 'The ID of the thread the messages belong to.',
68+
})
69+
@ApiQuery({
70+
name: 'limit',
71+
type: Number,
72+
required: false,
73+
description:
74+
'A limit on the number of objects to be returned. Limit can range between 1 and 100, and the default is 20.',
75+
})
76+
@ApiQuery({
77+
name: 'order',
78+
type: String,
79+
required: false,
80+
description:
81+
'Sort order by the created_at timestamp of the objects. asc for ascending order and desc for descending order.',
82+
})
83+
@ApiQuery({
84+
name: 'after',
85+
type: String,
86+
required: false,
87+
description:
88+
'A cursor for use in pagination. after is an object ID that defines your place in the list. For instance, if you make a list request and receive 100 objects, ending with obj_foo, your subsequent call can include after=obj_foo in order to fetch the next page of the list.',
89+
})
90+
@ApiQuery({
91+
name: 'before',
92+
type: String,
93+
required: false,
94+
description:
95+
'A cursor for use in pagination. before is an object ID that defines your place in the list. For instance, if you make a list request and receive 100 objects, ending with obj_foo, your subsequent call can include before=obj_foo in order to fetch the previous page of the list.',
96+
})
97+
@ApiQuery({
98+
name: 'run_id',
99+
type: String,
100+
required: false,
101+
description: 'Filter messages by the run ID that generated them.',
102+
})
103+
@Get(':id/messages')
104+
getMessagesOfThread(
105+
@Param('id') id: string,
106+
@Query('limit', new DefaultValuePipe(20)) limit: number,
107+
@Query('order', new DefaultValuePipe('desc')) order: 'asc' | 'desc',
108+
@Query('after') after?: string,
109+
@Query('before') before?: string,
110+
@Query('run_id') runId?: string,
111+
) {
112+
return this.threadsService.getMessagesOfThread(
113+
id,
114+
limit,
115+
order,
116+
after,
117+
before,
118+
runId,
119+
);
120+
}
121+
44122
@ApiResponse({
45123
status: 200,
46124
description: 'Ok',
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { ApiProperty } from '@nestjs/swagger';
2+
import { IsArray } from 'class-validator';
3+
4+
export class PageDto<T> {
5+
@ApiProperty()
6+
readonly object: string;
7+
8+
@IsArray()
9+
@ApiProperty({ isArray: true })
10+
readonly data: T[];
11+
12+
@ApiProperty()
13+
readonly first_id: string | undefined;
14+
15+
@ApiProperty()
16+
readonly last_id: string | undefined;
17+
18+
@ApiProperty()
19+
readonly has_more: boolean;
20+
21+
constructor(data: T[], hasMore: boolean, firstId?: string, lastId?: string) {
22+
this.object = 'list';
23+
this.data = data;
24+
this.first_id = firstId;
25+
this.last_id = lastId;
26+
this.has_more = hasMore;
27+
}
28+
}

cortex-js/src/main.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ async function bootstrap() {
1919

2020
app.useGlobalPipes(
2121
new ValidationPipe({
22+
transform: true,
2223
enableDebugMessages: true,
2324
}),
2425
);

cortex-js/src/usecases/threads/threads.usecases.ts

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
1-
import { Inject, Injectable } from '@nestjs/common';
1+
import { Inject, Injectable, NotFoundException } from '@nestjs/common';
22
import { CreateThreadDto } from '@/infrastructure/dtos/threads/create-thread.dto';
33
import { UpdateThreadDto } from '@/infrastructure/dtos/threads/update-thread.dto';
44
import { ThreadEntity } from '@/infrastructure/entities/thread.entity';
55
import { Repository } from 'typeorm';
66
import { v4 as uuidv4 } from 'uuid';
7+
import { MessageEntity } from '@/infrastructure/entities/message.entity';
8+
import { PageDto } from '@/infrastructure/dtos/page.dto';
79

810
@Injectable()
911
export class ThreadsUsecases {
1012
constructor(
1113
@Inject('THREAD_REPOSITORY')
1214
private threadRepository: Repository<ThreadEntity>,
15+
@Inject('MESSAGE_REPOSITORY')
16+
private messageRepository: Repository<MessageEntity>,
1317
) {}
1418

1519
async create(createThreadDto: CreateThreadDto): Promise<ThreadEntity> {
@@ -29,6 +33,49 @@ export class ThreadsUsecases {
2933
return this.threadRepository.find();
3034
}
3135

36+
async getMessagesOfThread(
37+
id: string,
38+
limit: number,
39+
order: 'asc' | 'desc',
40+
after?: string,
41+
before?: string,
42+
runId?: string,
43+
) {
44+
const thread = await this.findOne(id);
45+
if (!thread) {
46+
throw new NotFoundException(`Thread with id ${id} not found`);
47+
}
48+
49+
const queryBuilder = this.messageRepository.createQueryBuilder();
50+
const normalizedOrder = order === 'asc' ? 'ASC' : 'DESC';
51+
52+
queryBuilder
53+
.where('thread_id = :id', { id })
54+
.orderBy('created', normalizedOrder)
55+
.take(limit + 1); // Fetch one more record than the limit
56+
57+
if (after) {
58+
queryBuilder.andWhere('id > :after', { after });
59+
}
60+
61+
if (before) {
62+
queryBuilder.andWhere('id < :before', { before });
63+
}
64+
65+
const { entities: messages } = await queryBuilder.getRawAndEntities();
66+
67+
let hasMore = false;
68+
if (messages.length > limit) {
69+
hasMore = true;
70+
messages.pop(); // Remove the extra record
71+
}
72+
73+
const firstId = messages[0]?.id ?? undefined;
74+
const lastId = messages[messages.length - 1]?.id ?? undefined;
75+
76+
return new PageDto(messages, hasMore, firstId, lastId);
77+
}
78+
3279
findOne(id: string) {
3380
return this.threadRepository.findOne({ where: { id } });
3481
}

0 commit comments

Comments
 (0)