Skip to content

Commit 02a371d

Browse files
authored
Merge pull request #167 from import-ai/chore/lang
Chore/lang
2 parents fa528db + 46f37da commit 02a371d

15 files changed

+388
-257
lines changed

src/conversations/conversations.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@ import { Conversation } from 'omniboxd/conversations/entities/conversation.entit
44
import { ConversationsService } from 'omniboxd/conversations/conversations.service';
55
import { ConversationsController } from 'omniboxd/conversations/conversations.controller';
66
import { MessagesModule } from '../messages/messages.module';
7+
import { UserModule } from '../user/user.module';
78
import { TasksModule } from 'omniboxd/tasks/tasks.module';
89

910
@Module({
1011
imports: [
1112
MessagesModule,
13+
UserModule,
1214
TasksModule,
1315
TypeOrmModule.forFeature([Conversation]),
1416
],

src/conversations/conversations.service.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm';
33
import { DataSource, Repository } from 'typeorm';
44
import { Conversation } from 'omniboxd/conversations/entities/conversation.entity';
55
import { User } from 'omniboxd/user/entities/user.entity';
6+
import { UserService } from 'omniboxd/user/user.service';
67
import { ConfigService } from '@nestjs/config';
78
import { MessagesService } from 'omniboxd/messages/messages.service';
89
import { WizardAPIService } from 'omniboxd/wizard/api.wizard.service';
@@ -29,6 +30,7 @@ export class ConversationsService {
2930
private readonly conversationRepository: Repository<Conversation>,
3031
private readonly dataSource: DataSource,
3132
private readonly messagesService: MessagesService,
33+
private readonly userService: UserService,
3234
private readonly configService: ConfigService,
3335
private readonly wizardTaskService: WizardTaskService,
3436
) {
@@ -116,11 +118,30 @@ export class ConversationsService {
116118
if (summary.user_content) {
117119
const content = summary.user_content.trim();
118120
if (content.length > 0) {
121+
// Get user's language preference
122+
let lang: '简体中文' | 'English' | undefined;
123+
try {
124+
const languageOption = await this.userService.getOption(
125+
userId,
126+
'language',
127+
);
128+
if (languageOption?.value) {
129+
if (languageOption.value === 'zh-CN') {
130+
lang = '简体中文';
131+
} else if (languageOption.value === 'en-US') {
132+
lang = 'English';
133+
}
134+
}
135+
} catch (error) {
136+
// Ignore language preference errors, continue without lang
137+
}
138+
119139
const titleCreateResponse = await this.wizardApiService.request(
120140
'POST',
121141
'/internal/api/v1/wizard/title',
122142
{
123143
text: content,
144+
lang,
124145
},
125146
);
126147
conversation.title = titleCreateResponse.title!;

src/tasks/tasks.module.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,16 @@ import { Module } from '@nestjs/common';
22
import { Task } from 'omniboxd/tasks/tasks.entity';
33
import { TypeOrmModule } from '@nestjs/typeorm';
44
import { TasksService } from 'omniboxd/tasks/tasks.service';
5+
import { UserModule } from 'omniboxd/user/user.module';
56
import {
6-
TasksController,
77
ResourceTasksController,
8+
TasksController,
89
} from 'omniboxd/tasks/tasks.controller';
910
import { WizardTaskService } from 'omniboxd/tasks/wizard-task.service';
1011

1112
@Module({
1213
providers: [TasksService, WizardTaskService],
13-
imports: [TypeOrmModule.forFeature([Task])],
14+
imports: [TypeOrmModule.forFeature([Task]), UserModule],
1415
controllers: [TasksController, ResourceTasksController],
1516
exports: [TasksService, WizardTaskService],
1617
})

src/tasks/wizard-task.service.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Injectable } from '@nestjs/common';
22
import { InjectRepository } from '@nestjs/typeorm';
33
import { Repository } from 'typeorm';
4+
import { UserService } from 'omniboxd/user/user.service';
45
import { Task } from 'omniboxd/tasks/tasks.entity';
56
import {
67
Resource,
@@ -16,6 +17,7 @@ import { context, propagation } from '@opentelemetry/api';
1617
export class WizardTaskService {
1718
constructor(
1819
@InjectRepository(Task) public taskRepository: Repository<Task>,
20+
private readonly userService: UserService,
1921
) {}
2022

2123
injectTraceHeaders(task: Partial<Task>) {
@@ -25,6 +27,20 @@ export class WizardTaskService {
2527
return task;
2628
}
2729

30+
private async getUserLanguage(
31+
userId: string,
32+
): Promise<'简体中文' | 'English' | undefined> {
33+
const languageOption = await this.userService.getOption(userId, 'language');
34+
if (languageOption?.value) {
35+
if (languageOption.value === 'zh-CN') {
36+
return '简体中文';
37+
} else if (languageOption.value === 'en-US') {
38+
return 'English';
39+
}
40+
}
41+
return undefined;
42+
}
43+
2844
async create(data: Partial<Task>, repo?: Repository<Task>) {
2945
const repository = repo || this.taskRepository;
3046
const task = repository.create(this.injectTraceHeaders(data));
@@ -51,10 +67,14 @@ export class WizardTaskService {
5167
}
5268

5369
async createExtractTagsTask(parentTask: Task, repo?: Repository<Task>) {
70+
const lang = await this.getUserLanguage(parentTask.userId);
5471
return this.create(
5572
{
5673
function: 'extract_tags',
57-
input: { text: parentTask.output?.markdown },
74+
input: {
75+
text: parentTask.output?.markdown,
76+
lang: parentTask.input?.lang || lang,
77+
},
5878
namespaceId: parentTask.namespaceId,
5979
payload: {
6080
resource_id:
@@ -74,10 +94,11 @@ export class WizardTaskService {
7494
input: { text: string },
7595
repo?: Repository<Task>,
7696
) {
97+
const lang = await this.getUserLanguage(userId);
7798
return this.create(
7899
{
79100
function: 'generate_title',
80-
input,
101+
input: { lang, ...input },
81102
namespaceId,
82103
payload,
83104
userId,

src/wizard/dto/agent-request.dto.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export interface BaseAgentRequestDto {
2929
conversation_id: string;
3030
tools: Array<PrivateSearchToolDto | WebSearchToolDto>;
3131
enable_thinking: boolean;
32+
lang?: '简体中文' | 'English';
3233
}
3334

3435
export interface AgentRequestDto extends BaseAgentRequestDto {

src/wizard/processors/collect.processor.spec.ts

Lines changed: 99 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@ import { Test, TestingModule } from '@nestjs/testing';
33
import { BadRequestException } from '@nestjs/common';
44
import { CollectProcessor } from './collect.processor';
55
import { NamespaceResourcesService } from 'omniboxd/namespace-resources/namespace-resources.service';
6+
import { TagService } from 'omniboxd/tag/tag.service';
67
import { Task } from 'omniboxd/tasks/tasks.entity';
78
import { Resource } from 'omniboxd/resources/entities/resource.entity';
89

910
describe('CollectProcessor', () => {
1011
let processor: CollectProcessor;
1112
let namespaceResourcesService: jest.Mocked<NamespaceResourcesService>;
13+
let tagService: jest.Mocked<TagService>;
1214

1315
const mockResource: Partial<Resource> = {
1416
id: 'test-resource-id',
@@ -24,17 +26,26 @@ describe('CollectProcessor', () => {
2426
update: jest.fn(),
2527
};
2628

29+
const mockTagService = {
30+
getOrCreateTagsByNames: jest.fn(),
31+
};
32+
2733
const module: TestingModule = await Test.createTestingModule({
2834
providers: [
2935
{
3036
provide: NamespaceResourcesService,
3137
useValue: mockResourcesService,
3238
},
39+
{
40+
provide: TagService,
41+
useValue: mockTagService,
42+
},
3343
],
3444
}).compile();
3545

3646
namespaceResourcesService = module.get(NamespaceResourcesService);
37-
processor = new CollectProcessor(namespaceResourcesService);
47+
tagService = module.get(TagService);
48+
processor = new CollectProcessor(namespaceResourcesService, tagService);
3849
});
3950

4051
afterEach(() => {
@@ -291,6 +302,90 @@ describe('CollectProcessor', () => {
291302
},
292303
);
293304
});
305+
306+
it('should handle tags in metadata and return tagIds', async () => {
307+
const task = createMockTask({
308+
payload: { resource_id: 'test-resource-id' },
309+
output: {
310+
markdown: 'content',
311+
title: 'title',
312+
metadata: {
313+
tags: ['tag1', 'tag2', 'tag3'],
314+
otherMeta: 'value',
315+
},
316+
},
317+
});
318+
319+
namespaceResourcesService.get.mockResolvedValue(
320+
mockResource as Resource,
321+
);
322+
namespaceResourcesService.update.mockResolvedValue(undefined);
323+
tagService.getOrCreateTagsByNames.mockResolvedValue(['id1', 'id2', 'id3']);
324+
325+
const result = await processor.process(task);
326+
327+
expect(tagService.getOrCreateTagsByNames).toHaveBeenCalledWith(
328+
'test-namespace',
329+
['tag1', 'tag2', 'tag3'],
330+
);
331+
expect(namespaceResourcesService.update).toHaveBeenCalledWith(
332+
'test-user',
333+
'test-resource-id',
334+
{
335+
namespaceId: 'test-namespace',
336+
name: 'title',
337+
content: 'content',
338+
attrs: {
339+
url: 'https://example.com',
340+
metadata: {
341+
otherMeta: 'value',
342+
},
343+
},
344+
tag_ids: ['id1', 'id2', 'id3'],
345+
},
346+
);
347+
expect(result).toEqual({ resourceId: 'test-resource-id', tagIds: ['id1', 'id2', 'id3'] });
348+
});
349+
350+
it('should handle images in output and replace links', async () => {
351+
const task = createMockTask({
352+
payload: { resource_id: 'test-resource-id' },
353+
output: {
354+
markdown: 'content with ![image](http://example.com/image.png)',
355+
title: 'title',
356+
images: [{
357+
originalLink: 'http://example.com/image.png',
358+
attachmentId: 'attachment-123',
359+
}],
360+
},
361+
});
362+
363+
namespaceResourcesService.get.mockResolvedValue(
364+
mockResource as Resource,
365+
);
366+
namespaceResourcesService.update.mockResolvedValue(undefined);
367+
368+
const result = await processor.process(task);
369+
370+
expect(namespaceResourcesService.update).toHaveBeenCalledWith(
371+
'test-user',
372+
'test-resource-id',
373+
{
374+
namespaceId: 'test-namespace',
375+
name: 'title',
376+
content: 'content with ![image](attachments/attachment-123)',
377+
attrs: {
378+
url: 'https://example.com',
379+
images: [{
380+
originalLink: 'http://example.com/image.png',
381+
attachmentId: 'attachment-123',
382+
}],
383+
},
384+
tag_ids: undefined,
385+
},
386+
);
387+
expect(result).toEqual({ resourceId: 'test-resource-id', tagIds: undefined });
388+
});
294389
});
295390

296391
describe('edge cases', () => {
@@ -327,13 +422,14 @@ describe('CollectProcessor', () => {
327422
{
328423
namespaceId: 'test-namespace',
329424
name: undefined,
330-
content: undefined,
425+
content: '',
331426
attrs: {
332427
url: 'https://example.com',
333428
},
429+
tag_ids: undefined,
334430
},
335431
);
336-
expect(result).toEqual({ resourceId: 'test-resource-id' });
432+
expect(result).toEqual({ resourceId: 'test-resource-id', tagIds: undefined });
337433
});
338434
});
339435
});

src/wizard/processors/collect.processor.ts

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,13 @@ import { Task } from 'omniboxd/tasks/tasks.entity';
33
import { BadRequestException } from '@nestjs/common';
44
import { Processor } from 'omniboxd/wizard/processors/processor.abstract';
55
import { isEmpty } from 'omniboxd/utils/is-empty';
6+
import { TagService } from 'omniboxd/tag/tag.service';
7+
import { ProcessedImage } from 'omniboxd/wizard/types/wizard.types';
68

79
export class CollectProcessor extends Processor {
810
constructor(
911
protected readonly namespaceResourcesService: NamespaceResourcesService,
12+
private readonly tagService: TagService,
1013
) {
1114
super();
1215
}
@@ -24,15 +27,38 @@ export class CollectProcessor extends Processor {
2427
return {};
2528
} else if (task.output) {
2629
const { markdown, title, ...attrs } = task.output || {};
30+
31+
let tagIds: string[] | undefined = undefined;
32+
const tags: string[] | undefined = attrs?.metadata?.tags;
33+
if (Array.isArray(tags) && tags.length > 0) {
34+
attrs.metadata.tags = undefined;
35+
tagIds = await this.tagService.getOrCreateTagsByNames(
36+
task.namespaceId,
37+
tags,
38+
);
39+
}
40+
41+
let processedMarkdown: string = markdown || '';
42+
const images: ProcessedImage[] | undefined = task.output.images;
43+
if (Array.isArray(images) && images.length > 0) {
44+
for (const image of images) {
45+
processedMarkdown = processedMarkdown.replaceAll(
46+
image.originalLink,
47+
`attachments/${image.attachmentId}`,
48+
);
49+
}
50+
}
51+
2752
const resource = await this.namespaceResourcesService.get(resourceId);
2853
const mergedAttrs = { ...(resource?.attrs || {}), ...attrs };
2954
await this.namespaceResourcesService.update(task.userId, resourceId, {
3055
namespaceId: task.namespaceId,
3156
name: title,
32-
content: markdown,
57+
content: processedMarkdown,
3358
attrs: mergedAttrs,
59+
tag_ids: tagIds,
3460
});
35-
return { resourceId };
61+
return { resourceId, tagIds };
3662
}
3763
return {};
3864
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export interface ExtractTagsOutputDto {
2+
tags?: string[];
3+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { ProcessedImage } from 'omniboxd/wizard/types/wizard.types';
2+
import { Record } from 'openai/core';
3+
4+
export interface ReaderOutputDto {
5+
title: string;
6+
markdown: string;
7+
images?: ProcessedImage[];
8+
metadata?: Record<string, any>;
9+
}

0 commit comments

Comments
 (0)