Skip to content

Commit 82f2cee

Browse files
committed
Allow mentioning in comments
1 parent 21621e9 commit 82f2cee

20 files changed

Lines changed: 829 additions & 379 deletions

backend/src/entities/comment.ts

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,31 +19,37 @@ export class Comment extends BaseEntity {
1919
user: User;
2020

2121
@Property({ columnType: 'text' })
22-
text: string;
22+
contentText: string;
23+
@Property({ columnType: 'text' })
24+
contentHtml: string;
2325
@Property()
2426
editedAt?: Date;
2527

2628
constructor({
2729
boardCard,
2830
user,
29-
text,
31+
contentText,
32+
contentHtml,
3033
createdAt,
3134
}: {
3235
boardCard: BoardCard;
3336
user: User;
34-
text: string;
37+
contentText: string;
38+
contentHtml: string;
3539
createdAt?: Date;
3640
}) {
3741
super();
3842
this.boardCard = boardCard;
3943
this.user = user;
40-
this.text = text;
44+
this.contentText = contentText;
45+
this.contentHtml = contentHtml;
4146
this.createdAt = createdAt ?? new Date();
4247
this.validate();
4348
}
4449

45-
update({ text }: { text: string }) {
46-
this.text = text;
50+
update({ contentText, contentHtml }: { contentText: string; contentHtml: string }) {
51+
this.contentText = contentText;
52+
this.contentHtml = contentHtml;
4753
this.editedAt = new Date();
4854
this.validate();
4955
}
@@ -53,7 +59,8 @@ export class Comment extends BaseEntity {
5359
id: comment.id,
5460
boardCardId: comment.boardCard.id,
5561
user: User.toJson(comment.loadedUser),
56-
text: comment.text,
62+
contentHtml: comment.contentHtml,
63+
contentText: comment.contentText,
5764
createdAt: comment.createdAt,
5865
editedAt: comment.editedAt,
5966
};
@@ -62,10 +69,10 @@ export class Comment extends BaseEntity {
6269
static toText(comment: Loaded<Comment, 'user'>) {
6370
const user = comment.loadedUser;
6471
const items = [
65-
`- Comment ID: ${comment.id}`,
72+
`- ID: ${comment.id}`,
6673
`- Created At: ${comment.createdAt.toISOString()}`,
6774
`- User: ${User.toStr(user)}`,
68-
`- Text: ${comment.text}`,
75+
`- Content: ${comment.contentText}`,
6976
];
7077

7178
return `Comment:
@@ -75,6 +82,7 @@ ${items.join('\n')}`;
7582
private validate() {
7683
if (!this.boardCard) throw new Error('BoardCard is required');
7784
if (!this.user) throw new Error('User is required');
78-
if (!this.text) throw new Error('Text is required');
85+
if (!this.contentHtml) throw new Error('Content html is required');
86+
if (!this.contentText) throw new Error('Content text is required');
7987
}
8088
}

backend/src/entities/email-message.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { BaseEntity } from '@/entities/base-entity';
33
import { Domain } from '@/entities/domain';
44
import type { GmailAccount } from '@/entities/gmail-account';
55
import { GmailAttachment } from '@/entities/gmail-attachment';
6-
import { htmlToText, parseHtmlBody, parseTextBody } from '@/utils/email';
6+
import { parseHtmlBody, parseTextBody } from '@/utils/email';
77
import { type Participant, participantToString } from '@/utils/shared';
88

99
export interface EmailMessage {

backend/src/migrations/.snapshot-bordly_dev.json

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1889,8 +1889,17 @@
18891889
"nullable": false,
18901890
"mappedType": "uuid"
18911891
},
1892-
"text": {
1893-
"name": "text",
1892+
"content_text": {
1893+
"name": "content_text",
1894+
"type": "text",
1895+
"unsigned": false,
1896+
"autoincrement": false,
1897+
"primary": false,
1898+
"nullable": false,
1899+
"mappedType": "text"
1900+
},
1901+
"content_html": {
1902+
"name": "content_html",
18941903
"type": "text",
18951904
"unsigned": false,
18961905
"autoincrement": false,
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { Migration } from '@mikro-orm/migrations';
2+
3+
export class Migration20260224153054_add_content_html_in_comments extends Migration {
4+
override async up(): Promise<void> {
5+
this.addSql(`alter table "comments" add column "content_html" text not null;`);
6+
this.addSql(`alter table "comments" rename column "text" to "content_text";`);
7+
}
8+
9+
override async down(): Promise<void> {
10+
this.addSql(`alter table "comments" drop column "content_html";`);
11+
12+
this.addSql(`alter table "comments" rename column "content_text" to "text";`);
13+
}
14+
}

backend/src/services/agent.service.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { RequestContext } from '@mastra/core/request-context';
44
import type { ToolAction } from '@mastra/core/tools';
55
import type { Loaded } from '@mikro-orm/postgresql';
66
import type { Board } from '@/entities/board';
7+
import type { BoardCard } from '@/entities/board-card';
78
import { BoardMember } from '@/entities/board-member';
89
import { BORDLY_USER_ID } from '@/entities/user';
910
import { BoardMemberService } from '@/services/board-member.service';
@@ -17,6 +18,7 @@ export interface Context {
1718
userBoardMember?: Loaded<BoardMember>;
1819
userTimeZone?: string;
1920
bordlyBoardMember: Loaded<BoardMember>;
21+
boardCard: Loaded<BoardCard>;
2022
}
2123

2224
const BORDLY_AGENT = {
@@ -58,13 +60,13 @@ export class AgentService {
5860

5961
static async runBordlyAgent({
6062
board,
61-
boardCardId,
63+
boardCard,
6264
prompt,
6365
userBoardMember,
6466
userTimeZone,
6567
}: {
6668
board: Loaded<Board>;
67-
boardCardId: string;
69+
boardCard: Loaded<BoardCard>;
6870
prompt: string;
6971
userBoardMember: Loaded<BoardMember, 'user' | 'memory'>;
7072
userTimeZone?: string;
@@ -78,19 +80,24 @@ export class AgentService {
7880
requestContext.set('bordlyBoardMember', bordlyBoardMember);
7981
requestContext.set('userBoardMember', userBoardMember);
8082
requestContext.set('userTimeZone', userTimeZone);
83+
requestContext.set('boardCard', boardCard);
8184

8285
const agent = AgentService.createAgent(BORDLY_AGENT);
8386
const messages: MessageListInput = [
84-
{ role: 'system', content: `You are assisting with email management for a board card with ID ${boardCardId}.` },
87+
{ role: 'system', content: `You are assisting with email management for a board card with ID ${boardCard.id}.` },
88+
{
89+
role: 'system',
90+
content: `Current date and time: ${new Date().toLocaleString('en-US', { timeZone: userTimeZone })} (${userTimeZone})`,
91+
},
8592
{ role: 'system', content: `The user who sent the prompt: ${BoardMember.toText(userBoardMember)}` },
8693
{ role: 'user', content: prompt },
8794
];
8895

89-
Logger.info(`[AGENT] Running Bordly for board card ${boardCardId}`);
96+
Logger.info(`[AGENT] Running Bordly for board card ${boardCard.id}`);
9097
Logger.logObjects(messages);
9198
const response = await agent.generate(messages, { requestContext });
92-
Logger.info(`[AGENT] Completed Bordly for board card ${boardCardId}:\n${response.text}`);
99+
Logger.info(`[AGENT] Completed Bordly for board card ${boardCard.id}:\n${response.text}`);
93100

94-
return response;
101+
return requestContext.get('boardCard') as typeof boardCard;
95102
}
96103
}

backend/src/services/board-card.service.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Loaded, Populate } from '@mikro-orm/postgresql';
1+
import type { AutoPath, Loaded, Populate, PopulatePath } from '@mikro-orm/postgresql';
22
import type { Board } from '@/entities/board';
33
import type { BoardAccount } from '@/entities/board-account';
44
import { BoardCard, State } from '@/entities/board-card';
@@ -81,6 +81,14 @@ export class BoardCardService {
8181
return orm.em.find(BoardCard, { boardColumn: { board } }, { populate });
8282
}
8383

84+
static async populate<Hint extends string = never>(
85+
boardCard: Loaded<BoardCard>,
86+
populate: readonly AutoPath<BoardCard, Hint, PopulatePath.ALL>[],
87+
) {
88+
await orm.em.populate(boardCard, populate);
89+
return boardCard as Loaded<BoardCard, Hint>;
90+
}
91+
8492
static async createWithEmailDraft(
8593
board: Loaded<Board>,
8694
{ user, boardAccountId }: { user: Loaded<User, 'boardMembers'>; boardAccountId: string },

backend/src/services/board-member.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export class BoardMemberService {
3030
populate: readonly AutoPath<BoardMember, Hint, PopulatePath.ALL>[],
3131
) {
3232
await orm.em.populate(boardMember, populate);
33-
return boardMember;
33+
return boardMember as Loaded<BoardMember, Hint>;
3434
}
3535

3636
static async findMembers<Hint extends string = never>(

backend/src/services/board.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export class BoardService {
3232
populate: readonly AutoPath<Board, Hint, PopulatePath.ALL>[],
3333
) {
3434
await orm.em.populate(board, populate);
35-
return board;
35+
return board as Loaded<Board, Hint>;
3636
}
3737

3838
static async setName(board: Board, { name }: { name: string }) {

backend/src/services/comment.service.ts

Lines changed: 41 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import type { Loaded, OrderDefinition, Populate } from '@mikro-orm/postgresql';
22
import type { Board } from '@/entities/board';
33
import type { BoardCard } from '@/entities/board-card';
4-
import type { BoardMember } from '@/entities/board-member';
54
import { Comment } from '@/entities/comment';
65
import type { User } from '@/entities/user';
76
import { AgentService } from '@/services/agent.service';
@@ -16,16 +15,23 @@ export class CommentService {
1615
{
1716
board,
1817
user,
19-
text,
18+
contentHtml,
19+
contentText,
2020
userTimeZone,
21-
}: { board: Loaded<Board>; user: Loaded<User, 'boardMembers'>; text: string; userTimeZone?: string },
21+
}: {
22+
board: Loaded<Board>;
23+
user: Loaded<User, 'boardMembers'>;
24+
contentHtml: string;
25+
contentText: string;
26+
userTimeZone?: string;
27+
},
2228
) {
23-
const comment = new Comment({ boardCard, user, text });
29+
const comment = new Comment({ boardCard, user, contentHtml, contentText });
2430
const boardMember = user.boardMembers.find((bm) => bm.board.id === board.id)!;
2531

2632
if (!boardCard.assignedBoardMember) boardCard.assignToBoardMember(boardMember);
2733
boardCard.addParticipantUserId(user.id);
28-
boardCard.setSnippet(`${user.firstName}: ${text}`);
34+
boardCard.setSnippet(`${user.firstName}: ${contentText}`);
2935
boardCard.setLastEventAt(comment.createdAt);
3036

3137
const userBoardCardReadPosition = boardCard.boardCardReadPositions.find((pos) => pos.user.id === user.id)!;
@@ -35,16 +41,23 @@ export class CommentService {
3541

3642
await orm.em.flush();
3743

38-
if (isBordlyComment(text)) {
44+
if (isBordlyComment(contentText)) {
3945
const userBoardMember = await BoardMemberService.findById(board, {
4046
boardMemberId: boardMember.id,
4147
populate: ['user', 'memory'],
4248
});
43-
const prompt = CommentService.bordlyPrompt(text);
44-
await AgentService.runBordlyAgent({ board, boardCardId: boardCard.id, prompt, userBoardMember, userTimeZone });
49+
const prompt = CommentService.bordlyPrompt(contentText);
50+
const updatedBoardCard = await AgentService.runBordlyAgent({
51+
board,
52+
boardCard,
53+
prompt,
54+
userBoardMember,
55+
userTimeZone,
56+
});
57+
return { boardCard: updatedBoardCard, comment };
4558
}
4659

47-
return comment;
60+
return { boardCard, comment };
4861
}
4962

5063
static async edit<Hint extends string = never>(
@@ -53,14 +66,16 @@ export class CommentService {
5366
board,
5467
user,
5568
commentId,
56-
text,
69+
contentHtml,
70+
contentText,
5771
userTimeZone,
5872
populate,
5973
}: {
6074
board: Loaded<Board>;
6175
user: Loaded<User, 'boardMembers'>;
6276
commentId: string;
63-
text: string;
77+
contentHtml: string;
78+
contentText: string;
6479
userTimeZone?: string;
6580
populate?: Populate<Comment, Hint>;
6681
},
@@ -69,26 +84,33 @@ export class CommentService {
6984
const boardMember = user.boardMembers.find((bm) => bm.board.id === board.id)!;
7085
const wasLastBoardCardEvent = comment.createdAt.getTime() === boardCard.lastEventAt.getTime();
7186

72-
comment.update({ text });
87+
comment.update({ contentHtml, contentText });
7388
orm.em.persist(comment);
7489
if (wasLastBoardCardEvent) {
75-
boardCard.setSnippet(`${comment.user.firstName}: ${text}`);
90+
boardCard.setSnippet(`${comment.user.firstName}: ${contentText}`);
7691
orm.em.persist(boardCard);
7792
}
7893

7994
await orm.em.flush();
8095

81-
if (isBordlyComment(text)) {
96+
if (isBordlyComment(contentText)) {
8297
const userBoardMember = await BoardMemberService.findById(board, {
8398
boardMemberId: boardMember.id,
8499
populate: ['user', 'memory'],
85100
});
86101

87-
const prompt = CommentService.bordlyPrompt(text);
88-
await AgentService.runBordlyAgent({ board, boardCardId: boardCard.id, prompt, userBoardMember, userTimeZone });
102+
const prompt = CommentService.bordlyPrompt(contentText);
103+
const updatedBoardCard = await AgentService.runBordlyAgent({
104+
board,
105+
boardCard,
106+
prompt,
107+
userBoardMember,
108+
userTimeZone,
109+
});
110+
return { boardCard: updatedBoardCard, comment };
89111
}
90112

91-
return comment;
113+
return { boardCard, comment };
92114
}
93115

94116
static async delete(boardCard: BoardCard, { commentId }: { commentId: string }) {
@@ -115,14 +137,14 @@ export class CommentService {
115137

116138
if (lastComment && lastEmailMessage) {
117139
if (lastComment.createdAt.getTime() > lastEmailMessage.externalCreatedAt.getTime()) {
118-
boardCard.setSnippet(`${lastComment.user.firstName}: ${lastComment.text}`);
140+
boardCard.setSnippet(`${lastComment.user.firstName}: ${lastComment.contentText}`);
119141
boardCard.setLastEventAt(lastComment.createdAt);
120142
} else {
121143
boardCard.setSnippet(lastEmailMessage.snippet);
122144
boardCard.setLastEventAt(lastEmailMessage.externalCreatedAt);
123145
}
124146
} else if (lastComment) {
125-
boardCard.setSnippet(`${lastComment.user.firstName}: ${lastComment.text}`);
147+
boardCard.setSnippet(`${lastComment.user.firstName}: ${lastComment.contentText}`);
126148
boardCard.setLastEventAt(lastComment.createdAt);
127149
} else if (lastEmailMessage) {
128150
boardCard.setSnippet(lastEmailMessage.snippet);

backend/src/tools/board-card-read.tool.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,17 @@ import { Logger } from '@/utils/logger';
1414
export const boardCardReadTool = createTool({
1515
id: 'board-card-read',
1616
description: 'Read board card with email messages, an email draft, and comments',
17-
inputSchema: z.object({
18-
boardCardId: z.uuid().describe('The ID of the board card to read'),
19-
}),
20-
execute: async ({ boardCardId }, context) => {
21-
Logger.info(`[AGENT] Tool board-card-read for board card ${boardCardId}`);
17+
inputSchema: z.object({}),
18+
execute: async (_, context) => {
2219
const { requestContext } = context as { requestContext: RequestContext<Context> };
23-
const bordlyBoardMember = requestContext.get('bordlyBoardMember');
20+
const initialBoardCard = requestContext.get('boardCard');
21+
Logger.info(`[AGENT] Tool board-card-read for board card ${initialBoardCard.id}`);
2422

25-
const boardCard = await BoardCardService.findById(bordlyBoardMember.board, {
26-
boardCardId,
27-
populate: ['assignedBoardMember.user', 'boardColumn', 'emailDraft.fileAttachments'],
28-
});
23+
const boardCard = await BoardCardService.populate(initialBoardCard, [
24+
'assignedBoardMember.user',
25+
'boardColumn',
26+
'emailDraft.fileAttachments',
27+
]);
2928

3029
const [lastEmailMessage] = await EmailMessageService.findEmailMessagesByBoardCard(boardCard, {
3130
populate: ['domain', 'gmailAttachments'],
@@ -50,6 +49,7 @@ export const boardCardReadTool = createTool({
5049
if (result.lastEmailMessage) Logger.log(result.lastEmailMessage);
5150
Logger.logObjects(result.commentsAsc);
5251

52+
requestContext.set('boardCard', boardCard);
5353
return result;
5454
},
5555
});

0 commit comments

Comments
 (0)