Skip to content

Commit cc8f0d0

Browse files
feat(assistant): type improvements for messages
1 parent 7bc9544 commit cc8f0d0

23 files changed

+254
-150
lines changed

apps/spa/src/app/components/chat/chat-message/chat-message.component.html

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,12 @@
88
<span markdown [data]="message | messageText"></span>
99
}
1010

11-
@if (message.role !== chatRole.System) {
12-
<ai-chat-audio [message]="message"></ai-chat-audio>
13-
}
14-
1511
@if ((message | messageImageFile).length) {
1612
<div class="chat-message__file">
17-
@for (image of message | messageImageFile; track image.image_file.file_id) {
13+
@for (
14+
image of message | messageImageFile;
15+
track image.image_file.file_id
16+
) {
1817
<div>File ID: {{ image.image_file.file_id }}</div>
1918
}
2019
</div>

apps/spa/src/app/components/chat/chat-message/chat-message.component.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import { NgClass } from '@angular/common';
99
import { ChatAvatarComponent } from '../chat-avatar/chat-avatar.component';
1010
import { MessageTextPipe } from '../../../pipes/message-text.pipe';
1111
import { MessageImageFilePipe } from '../../../pipes/message-file.pipe';
12-
import { AnnotationPipe } from '../../../pipes/annotation.pipe';
1312

1413
@Component({
1514
selector: 'ai-chat-message',
@@ -26,7 +25,7 @@ import { AnnotationPipe } from '../../../pipes/annotation.pipe';
2625
],
2726
})
2827
export class ChatMessageComponent {
29-
@Input() message!: ChatMessage;
28+
@Input() message!: Partial<ChatMessage>;
3029
@Input() class = '';
3130
chatRole = ChatRole;
3231

apps/spa/src/app/components/chat/chat-messages/chat-messages.component.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ import { ChatTipsComponent } from '../chat-tips/chat-tips.component';
2929
],
3030
})
3131
export class ChatMessagesComponent implements AfterViewInit, OnChanges {
32-
@Input() initialMessages: ChatMessage[] = [];
33-
@Input() messages: ChatMessage[] = [];
32+
@Input() initialMessages: Partial<ChatMessage>[] = [];
33+
@Input() messages: Partial<ChatMessage>[] = [];
3434
@Input() isTyping = false;
3535
@Input() tips: string[] = [];
3636
@Output() tipSelected$ = new EventEmitter<string>();
Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import { TextContentBlock } from 'openai/resources/beta/threads/messages';
22
import { ImageFileContentBlock } from 'openai/src/resources/beta/threads/messages';
33

4-
export function isTextContentBlock(item: {
5-
type: string;
4+
export function isTextContentBlock(item?: {
5+
type?: string;
66
}): item is TextContentBlock {
7-
return item.type === 'text';
7+
return item?.type === 'text';
88
}
99

10-
export function isImageFileContentBlock(item: {
11-
type: string;
10+
export function isImageFileContentBlock(item?: {
11+
type?: string;
1212
}): item is ImageFileContentBlock {
13-
return item.type === 'image_file';
13+
return item?.type === 'image_file';
1414
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import {
2+
ImageFileContentBlock,
3+
MessageContent,
4+
MessageCreateParams,
5+
} from 'openai/resources/beta/threads';
6+
import { TextContentBlock } from 'openai/src/resources/beta/threads/messages';
7+
import { ChatMessage, ChatRole } from './chat.model';
8+
import { CodeInterpreterTool, FileSearchTool } from 'openai/resources/beta';
9+
10+
export const textContentBlock = (content: string): TextContentBlock => ({
11+
type: 'text',
12+
text: {
13+
value: content,
14+
annotations: [],
15+
},
16+
});
17+
18+
export const imageFileContentBlock = (
19+
fileId: string,
20+
): ImageFileContentBlock => ({
21+
type: 'image_file',
22+
image_file: {
23+
file_id: fileId,
24+
},
25+
});
26+
27+
export const messageAttachment = (
28+
fileId: string,
29+
tools: Array<CodeInterpreterTool | FileSearchTool> = [
30+
{ type: 'code_interpreter' },
31+
],
32+
): MessageCreateParams.Attachment => ({
33+
file_id: fileId,
34+
tools,
35+
});
36+
37+
export const messageContentBlock = (
38+
content: MessageContent[],
39+
role: ChatRole,
40+
): Partial<ChatMessage> => ({
41+
content,
42+
role,
43+
});

apps/spa/src/app/modules/+chat/shared/chat.model.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { MessageContent } from 'openai/resources/beta/threads';
1+
import { AnnotationData } from '@boldare/openai-assistant';
2+
import { Message } from 'openai/resources/beta/threads';
23

34
export interface AudioResponse {
45
content: string;
@@ -7,12 +8,10 @@ export interface AudioResponse {
78
export enum ChatRole {
89
User = 'user',
910
Assistant = 'assistant',
10-
System = 'system',
1111
}
1212

13-
export interface ChatMessage {
14-
metadata?: Record<string, unknown>;
15-
content: string | Array<MessageContent>;
13+
export interface ChatMessage extends Message {
14+
annotations?: AnnotationData[];
1615
role: ChatRole;
1716
}
1817

apps/spa/src/app/modules/+chat/shared/chat.service.ts

Lines changed: 39 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Injectable, Injector } from '@angular/core';
1+
import { Injectable } from '@angular/core';
22
import {
33
BehaviorSubject,
44
distinctUntilChanged,
@@ -10,10 +10,10 @@ import {
1010
tap,
1111
} from 'rxjs';
1212
import {
13-
ImageFileContentBlock,
1413
Message,
1514
MessageContent,
1615
Text,
16+
TextContentBlock,
1717
} from 'openai/resources/beta/threads/messages';
1818
import { OpenAiFile, GetThreadResponseDto } from '@boldare/openai-assistant';
1919
import { ChatRole, ChatMessage, ChatMessageStatus } from './chat.model';
@@ -24,14 +24,21 @@ import { ChatFilesService } from './chat-files.service';
2424
import { MessageContentService } from '../../../components/controls/message-content/message-content.service';
2525
import { environment } from '../../../../environments/environment';
2626
import { AnnotationPipe } from '../../../pipes/annotation.pipe';
27+
import {
28+
imageFileContentBlock,
29+
messageAttachment,
30+
messageContentBlock,
31+
textContentBlock,
32+
} from './chat.helpers';
33+
import { isTextContentBlock } from '../../../components/controls/message-content/message-content.helpers';
2734

2835
@Injectable({ providedIn: 'root' })
2936
export class ChatService {
3037
isLoading$ = new BehaviorSubject<boolean>(false);
3138
isVisible$ = new BehaviorSubject<boolean>(environment.isAutoOpen);
3239
isTyping$ = new BehaviorSubject<boolean>(false);
3340
isResponding$ = new BehaviorSubject<boolean>(false);
34-
messages$ = new BehaviorSubject<ChatMessage[]>([]);
41+
messages$ = new BehaviorSubject<Partial<ChatMessage>[]>([]);
3542

3643
constructor(
3744
private readonly chatGatewayService: ChatGatewayService,
@@ -121,72 +128,41 @@ export class ChatService {
121128
window?.top?.postMessage('changeView', '*');
122129
}
123130

124-
addMessage(message: ChatMessage): void {
131+
addMessage(message: Partial<ChatMessage>): void {
125132
this.messages$.next([...this.messages$.value, message]);
126133
}
127134

128135
addFileMessage(files: OpenAiFile[]): void {
129136
if (!files?.length) {
130137
return;
131138
}
132-
133-
this.addMessage({
134-
content: `The user has attached files to the message: ${files
135-
.map(file => file.filename)
136-
.join(', ')}`,
137-
role: ChatRole.System,
138-
});
139139
}
140140

141141
async sendMessage(content: string, role = ChatRole.User): Promise<void> {
142142
this.isTyping$.next(true);
143143
this.isResponding$.next(true);
144-
this.addMessage({ content, role });
144+
145+
const message = messageContentBlock([textContentBlock(content)], role);
146+
this.addMessage(message);
145147

146148
const files = await this.chatFilesService.sendFiles();
147149
this.addFileMessage(files);
148150

149151
this.chatGatewayService.callStart({
150152
content: await this.getMessageContent(content),
151153
threadId: this.threadService.threadId$.value,
152-
attachments: files.map(
153-
file =>
154-
({
155-
file_id: file.id,
156-
tools: [{ type: 'code_interpreter' }],
157-
}) || [],
158-
),
154+
attachments: files.map(file => messageAttachment(file.id) || []),
159155
});
160156
}
161157

162158
async getMessageContent(content: string): Promise<MessageContent[]> {
163159
const images = (await this.messageContentService.sendFiles()) || [];
164160
const imageFileContentList =
165-
images?.map(
166-
file =>
167-
({
168-
type: 'image_file',
169-
image_file: {
170-
file_id: file.id,
171-
},
172-
}) as ImageFileContentBlock,
173-
) || [];
161+
images?.map(file => imageFileContentBlock(file.id)) || [];
174162

175163
this.messages$.next([
176164
...this.messages$.value.slice(0, -1),
177-
{
178-
content: [
179-
{
180-
type: 'text',
181-
text: {
182-
value: content,
183-
annotations: [],
184-
},
185-
},
186-
...imageFileContentList,
187-
],
188-
role: ChatRole.User,
189-
},
165+
messageContentBlock([textContentBlock(content)], ChatRole.User),
190166
]);
191167

192168
return [
@@ -202,29 +178,42 @@ export class ChatService {
202178
return this.chatGatewayService.textCreated().subscribe(data => {
203179
this.isTyping$.next(false);
204180
this.isResponding$.next(true);
205-
this.addMessage({ content: data.text.value, role: ChatRole.Assistant });
181+
182+
const message = messageContentBlock(
183+
[textContentBlock(data.text.value)],
184+
ChatRole.Assistant,
185+
);
186+
this.addMessage(message);
206187
});
207188
}
208189

209190
watchTextDelta(): Subscription {
210191
return this.chatGatewayService.textDelta().subscribe(data => {
211192
const length = this.messages$.value.length;
212193
this.isResponding$.next(true);
213-
this.messages$.value[length - 1].content = data.text.value;
194+
195+
const lastMessageContent = this.messages$.value[length - 1]?.content?.[0];
196+
197+
if (isTextContentBlock(lastMessageContent)) {
198+
(
199+
this.messages$.value[length - 1].content?.[0] as TextContentBlock
200+
).text = data.text;
201+
}
214202
});
215203
}
216204

217205
watchTextDone(): Subscription {
218206
return this.chatGatewayService.textDone().subscribe(event => {
219207
this.isTyping$.next(false);
220208
this.isResponding$.next(false);
221-
this.messages$.next([
222-
...this.messages$.value.slice(0, -1),
223-
{
224-
content: this.annotationPipe.transform(event),
225-
role: ChatRole.Assistant,
226-
},
227-
]);
209+
210+
const annotationContent = this.annotationPipe.transform(event);
211+
const message = messageContentBlock(
212+
[textContentBlock(annotationContent)],
213+
ChatRole.Assistant,
214+
);
215+
216+
this.messages$.next([...this.messages$.value.slice(0, -1), message]);
228217
});
229218
}
230219

apps/spa/src/app/pipes/annotation.pipe.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { isTextContentBlock } from '../components/controls/message-content/messa
55

66
@Pipe({
77
standalone: true,
8-
name: 'annotation'
8+
name: 'annotation',
99
})
1010
export class AnnotationPipe implements PipeTransform {
1111
transform(event: MessageWithAnnotations<{ message: Message }>): string {
@@ -14,7 +14,7 @@ export class AnnotationPipe implements PipeTransform {
1414
}
1515

1616
const { text } = event.data.message.content[0];
17-
17+
1818
if (!event.annotations?.length) {
1919
return text.value;
2020
}
@@ -26,4 +26,4 @@ export class AnnotationPipe implements PipeTransform {
2626

2727
return text.value;
2828
}
29-
}
29+
}

apps/spa/src/app/pipes/message-file.pipe.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@ import { ImageFileContentBlock } from 'openai/resources/beta/threads';
55

66
@Pipe({
77
standalone: true,
8-
name: 'messageImageFile'
8+
name: 'messageImageFile',
99
})
1010
export class MessageImageFilePipe implements PipeTransform {
11-
transform(message: ChatMessage): ImageFileContentBlock[] {
11+
transform(message: Partial<ChatMessage>): ImageFileContentBlock[] {
1212
if (typeof message.content === 'string') {
1313
return [];
1414
}
15-
16-
return message.content.filter(isImageFileContentBlock);
15+
16+
return message?.content?.filter(isImageFileContentBlock) || [];
1717
}
18-
}
18+
}

apps/spa/src/app/pipes/message-text.pipe.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,21 @@ import { ChatMessage } from '../modules/+chat/shared/chat.model';
44

55
@Pipe({
66
standalone: true,
7-
name: 'messageText'
7+
name: 'messageText',
8+
pure: false,
89
})
910
export class MessageTextPipe implements PipeTransform {
10-
transform(message: ChatMessage): string {
11+
transform(message: Partial<ChatMessage>): string {
1112
if (typeof message.content === 'string') {
1213
return message.content;
1314
}
14-
15+
1516
// @TODO: handle all types of message content
16-
return message.content
17-
.filter(isTextContentBlock)
18-
.map(block => block.text.value)
19-
.join(' ');
17+
return (
18+
message.content
19+
?.filter(isTextContentBlock)
20+
?.map(block => block.text.value)
21+
?.join(' ') || ''
22+
);
2023
}
21-
}
24+
}

0 commit comments

Comments
 (0)