Skip to content

Commit 5027d5e

Browse files
authored
Merge pull request #76 from import-ai/feature/chat_title
Support generate conversation title
2 parents f1278c2 + 3c8fc5b commit 5027d5e

File tree

4 files changed

+87
-33
lines changed

4 files changed

+87
-33
lines changed

src/conversations/conversations.controller.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,11 @@ export class ConversationsController {
6161
return await this.conversationsService.getDetail(id, req.user);
6262
}
6363

64+
@Post(':id/title')
65+
async createTitle(@Param('id') id: string, @Req() req) {
66+
return await this.conversationsService.createTitle(id, req.user.id);
67+
}
68+
6469
@Delete(':id')
6570
async remove(@Param('id') id: string) {
6671
return await this.conversationsService.remove(id);

src/conversations/conversations.service.ts

Lines changed: 58 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ import { InjectRepository } from '@nestjs/typeorm';
33
import { Repository } from 'typeorm';
44
import { Conversation } from 'src/conversations/entities/conversation.entity';
55
import { User } from 'src/user/user.entity';
6+
import { ConfigService } from '@nestjs/config';
67
import { MessagesService } from 'src/messages/messages.service';
8+
import { WizardAPIService } from 'src/wizard/api.wizard.service';
79
import {
810
ConversationDetailDto,
911
ConversationMessageMappingDto,
@@ -16,11 +18,20 @@ import {
1618

1719
@Injectable()
1820
export class ConversationsService {
21+
private readonly wizardApiService: WizardAPIService;
22+
1923
constructor(
2024
@InjectRepository(Conversation)
2125
private readonly conversationRepository: Repository<Conversation>,
2226
private readonly messagesService: MessagesService,
23-
) {}
27+
private readonly configService: ConfigService,
28+
) {
29+
const baseUrl = this.configService.get<string>('OBB_WIZARD_BASE_URL');
30+
if (!baseUrl) {
31+
throw new Error('Environment variable OBB_WIZARD_BASE_URL is required');
32+
}
33+
this.wizardApiService = new WizardAPIService(baseUrl);
34+
}
2435

2536
async create(namespaceId: string, user: User) {
2637
const conversation = this.conversationRepository.create({
@@ -87,18 +98,51 @@ export class ConversationsService {
8798
return composed;
8899
}
89100

90-
async getFirstContent(
91-
userId: string,
92-
conversationId: string,
93-
targetRole: OpenAIMessageRole = OpenAIMessageRole.ASSISTANT,
94-
): Promise<string | undefined> {
95-
const messages: Message[] = await this.compose(userId, conversationId);
96-
for (const m of messages) {
97-
if (m.message.role === targetRole && m.message.content) {
98-
return m.message.content;
101+
async createTitle(id: string, userId: string): Promise<{ title: string }> {
102+
const conversation = await this.conversationRepository.findOneOrFail({
103+
where: { id, user: { id: userId } },
104+
});
105+
if (conversation.title) {
106+
return { title: conversation.title };
107+
}
108+
const summary = await this.getSummary(userId, conversation);
109+
if (summary.user_content) {
110+
const content = summary.user_content.trim();
111+
if (content.length > 0) {
112+
const titleCreateResponse = await this.wizardApiService.request(
113+
'POST',
114+
'/internal/api/v1/wizard/title',
115+
{
116+
text: content,
117+
},
118+
);
119+
conversation.title = titleCreateResponse.title!;
120+
await this.conversationRepository.save(conversation);
121+
return titleCreateResponse as { title: string };
99122
}
100123
}
101-
return undefined;
124+
throw new Error('No query content found to create title');
125+
}
126+
127+
async getSummary(
128+
userId: string,
129+
c: Conversation,
130+
): Promise<ConversationSummaryDto> {
131+
const messages: Message[] = await this.compose(userId, c.id);
132+
const check = (m: Message, role: OpenAIMessageRole) => {
133+
return m.message.role === role && m.message.content?.trim();
134+
};
135+
return {
136+
id: c.id,
137+
title: c.title,
138+
created_at: c.createdAt.toISOString(),
139+
updated_at: c.updatedAt?.toISOString(),
140+
user_content: messages.find((m) => check(m, OpenAIMessageRole.USER))
141+
?.message?.content,
142+
assistant_content: messages.find((m) =>
143+
check(m, OpenAIMessageRole.ASSISTANT),
144+
)?.message?.content,
145+
};
102146
}
103147

104148
async listSummary(
@@ -110,26 +154,9 @@ export class ConversationsService {
110154
data: ConversationSummaryDto[];
111155
}> {
112156
const conversations = await this.findAll(namespaceId, userId, options);
113-
const summaries: ConversationSummaryDto[] = [];
114-
115-
const check = (m: Message, role: OpenAIMessageRole) => {
116-
return m.message.role === role && m.message.content?.trim();
117-
};
118-
119-
for (const c of conversations) {
120-
const messages: Message[] = await this.compose(userId, c.id);
121-
summaries.push({
122-
id: c.id,
123-
title: c.title,
124-
created_at: c.createdAt.toISOString(),
125-
updated_at: c.updatedAt?.toISOString(),
126-
user_content: messages.find((m) => check(m, OpenAIMessageRole.USER))
127-
?.message?.content,
128-
assistant_content: messages.find((m) =>
129-
check(m, OpenAIMessageRole.ASSISTANT),
130-
)?.message?.content,
131-
} as ConversationSummaryDto);
132-
}
157+
const summaries: ConversationSummaryDto[] = await Promise.all(
158+
conversations.map((c) => this.getSummary(userId, c)),
159+
);
133160
const summariesTotal = await this.countAll(namespaceId, userId);
134161
return {
135162
data: summaries,

src/wizard/api.wizard.service.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,29 @@
11
export class WizardAPIService {
22
constructor(private readonly wizardBaseUrl: string) {}
33

4-
async request(req: Request): Promise<Record<string, any>> {
4+
async request(
5+
method: string,
6+
url: string,
7+
body: Record<string, any>,
8+
headers: Record<string, string> = {},
9+
): Promise<Record<string, any>> {
10+
const response = await fetch(`${this.wizardBaseUrl}${url}`, {
11+
method,
12+
headers: {
13+
'Content-Type': 'application/json',
14+
...headers,
15+
},
16+
body: JSON.stringify(body),
17+
});
18+
19+
if (!response.ok) {
20+
throw new Error(`Request failed with status ${response.status}`);
21+
}
22+
23+
return response.json();
24+
}
25+
26+
async proxy(req: Request): Promise<Record<string, any>> {
527
const url = `${this.wizardBaseUrl}${req.url}`;
628
const response = await fetch(url, {
729
method: req.method,

src/wizard/wizard.controller.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,6 @@ export class WizardController {
3333

3434
@Post('*')
3535
async proxy(@Req() req: Request): Promise<Record<string, any>> {
36-
return await this.wizardService.wizardApiService.request(req);
36+
return await this.wizardService.wizardApiService.proxy(req);
3737
}
3838
}

0 commit comments

Comments
 (0)