Skip to content

Commit 30593b6

Browse files
Merge pull request #51 from boldare/feat/assistant-streaming
feat(openai-assistant): assistants streaming events
2 parents 40d1910 + 209abbb commit 30593b6

27 files changed

+623
-233
lines changed

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { Component, Input, OnInit } from '@angular/core';
22
import { ChatClientService } from '../../../modules/+chat/shared/chat-client.service';
3-
import { ChatMessage, SpeechVoice } from '../../../modules/+chat/shared/chat.model';
3+
import {
4+
ChatMessage,
5+
SpeechVoice,
6+
} from '../../../modules/+chat/shared/chat.model';
47
import { environment } from '../../../../environments/environment';
58
import { MatIconModule } from '@angular/material/icon';
69
import { delay } from 'rxjs';

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import { Component, HostBinding, Input } from '@angular/core';
2-
import { ChatRole, ChatMessage } from '../../../modules/+chat/shared/chat.model';
2+
import {
3+
ChatRole,
4+
ChatMessage,
5+
} from '../../../modules/+chat/shared/chat.model';
36
import { MarkdownComponent } from 'ngx-markdown';
47
import { ChatAudioComponent } from '../chat-audio/chat-audio.component';
58
import { NgClass } from '@angular/common';

apps/spa/src/app/modules/+chat/containers/chat-iframe/chat-iframe.component.html

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,19 @@
88

99
<ai-spinner [isActive]="isLoading()" />
1010
@if (isConfigEnabled && !threadId()) {
11-
<ai-configuration-form class="chat__content" />
11+
<ai-configuration-form class="chat__content" />
1212
} @else {
13-
<ai-chat-messages
14-
[messages]="messages()"
15-
[isTyping]="isTyping()"
16-
[tips]="tips"
17-
(tipSelected$)="chatService.sendMessage($event)"
18-
class="chat__content" />
19-
<ai-chat-footer
20-
[isDisabled]="isTyping()"
21-
[isTranscriptionEnabled]="isTranscriptionEnabled"
22-
[isAttachmentEnabled]="isAttachmentEnabled"
23-
(sendMessage$)="chatService.sendMessage($event)"
24-
(sendAudio$)="chatService.sendAudio($event)" />
13+
<ai-chat-messages
14+
[messages]="messages()"
15+
[isTyping]="isTyping()"
16+
[tips]="tips"
17+
(tipSelected$)="chatService.sendMessage($event)"
18+
class="chat__content" />
19+
<ai-chat-footer
20+
[isDisabled]="isTyping()"
21+
[isTranscriptionEnabled]="isTranscriptionEnabled"
22+
[isAttachmentEnabled]="isAttachmentEnabled"
23+
(sendMessage$)="chatService.sendMessage($event)"
24+
(sendAudio$)="chatService.sendAudio($event)" />
2525
}
2626
</ai-card>
Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,43 @@
11
import { Injectable } from '@angular/core';
22
import { ChatEvents } from './chat.model';
33
import io from 'socket.io-client';
4-
import { ChatCallDto } from '@boldare/openai-assistant';
4+
import {
5+
ChatCallDto,
6+
TextCreatedPayload,
7+
TextDeltaPayload,
8+
TextDonePayload,
9+
} from '@boldare/openai-assistant';
510
import { Observable } from 'rxjs';
611
import { environment } from '../../../../environments/environment';
712

813
@Injectable({ providedIn: 'root' })
914
export class ChatGatewayService {
1015
private socket = io(environment.websocketUrl);
1116

12-
sendMessage(payload: ChatCallDto): void {
13-
this.socket.emit(ChatEvents.SendMessage, payload);
14-
}
15-
16-
getMessages(): Observable<ChatCallDto> {
17-
return new Observable<ChatCallDto>(observer => {
18-
this.socket.on(ChatEvents.MessageReceived, data => observer.next(data));
17+
watchEvent<T>(event: ChatEvents): Observable<T> {
18+
return new Observable<T>(observer => {
19+
this.socket.on(event, data => observer.next(data));
1920
return () => this.socket.disconnect();
2021
});
2122
}
23+
24+
callStart(payload: ChatCallDto): void {
25+
this.socket.emit(ChatEvents.CallStart, payload);
26+
}
27+
28+
callDone(): Observable<ChatCallDto> {
29+
return this.watchEvent(ChatEvents.CallDone);
30+
}
31+
32+
textCreated(): Observable<TextCreatedPayload> {
33+
return this.watchEvent(ChatEvents.TextCreated);
34+
}
35+
36+
textDelta(): Observable<TextDeltaPayload> {
37+
return this.watchEvent(ChatEvents.TextDelta);
38+
}
39+
40+
textDone(): Observable<TextDonePayload> {
41+
return this.watchEvent(ChatEvents.TextDone);
42+
}
2243
}

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

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,21 @@ export interface ChatMessage {
1515
}
1616

1717
export enum ChatEvents {
18-
SendMessage = 'send_message',
19-
MessageReceived = 'message_received',
18+
CallStart = 'callStart',
19+
CallDone = 'callDone',
20+
MessageCreated = 'messageCreated',
21+
MessageDelta = 'messageDelta',
22+
MessageDone = 'messageDone',
23+
TextCreated = 'textCreated',
24+
TextDelta = 'textDelta',
25+
TextDone = 'textDone',
26+
ImageFileDone = 'imageFileDone',
27+
ToolCallCreated = 'toolCallCreated',
28+
ToolCallDelta = 'toolCallDelta',
29+
ToolCallDone = 'toolCallDone',
30+
RunStepCreated = 'runStepCreated',
31+
RunStepDelta = 'runStepDelta',
32+
RunStepDone = 'runStepDone',
2033
}
2134

2235
export enum ChatMessageStatus {

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

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import { OpenAiFile, GetThreadResponseDto } from '@boldare/openai-assistant';
1919
import { Message } from 'openai/resources/beta/threads/messages';
2020
import { TextContentBlock } from 'openai/resources/beta/threads/messages/messages';
2121

22-
2322
@Injectable({ providedIn: 'root' })
2423
export class ChatService {
2524
isLoading$ = new BehaviorSubject<boolean>(false);
@@ -35,11 +34,21 @@ export class ChatService {
3534
) {
3635
document.body.classList.add('ai-chat');
3736

37+
this.subscribeMessages();
3838
this.setInitialValues();
39-
this.watchMessages();
4039
this.watchVisibility();
4140
}
4241

42+
subscribeMessages(): void {
43+
if (!environment.isStreamingEnabled) {
44+
this.watchMessages();
45+
} else {
46+
this.watchTextCreated();
47+
this.watchTextDelta();
48+
this.watchTextDone();
49+
}
50+
}
51+
4352
isMessageInvisible(message: Message): boolean {
4453
const metadata = message.metadata as Record<string, unknown>;
4554
return metadata?.['status'] === ChatMessageStatus.Invisible;
@@ -87,11 +96,13 @@ export class ChatService {
8796

8897
refresh(): void {
8998
this.isLoading$.next(true);
99+
this.isTyping$.next(false);
90100
this.messages$.next([]);
91101
this.threadService.start().subscribe();
92102
}
93103

94104
clear(): void {
105+
this.isTyping$.next(false);
95106
this.threadService.clear();
96107
this.messages$.next([]);
97108
}
@@ -120,15 +131,42 @@ export class ChatService {
120131
const files = await this.chatFilesService.sendFiles();
121132
this.addFileMessage(files);
122133

123-
this.chatGatewayService.sendMessage({
134+
this.chatGatewayService.callStart({
124135
content,
125136
threadId: this.threadService.threadId$.value,
126137
file_ids: files.map(file => file.id) || [],
127138
});
128139
}
129140

141+
watchTextCreated(): Subscription {
142+
return this.chatGatewayService.textCreated().subscribe(data => {
143+
this.isTyping$.next(false);
144+
this.addMessage({ content: data.text.value, role: ChatRole.Assistant });
145+
});
146+
}
147+
148+
watchTextDelta(): Subscription {
149+
return this.chatGatewayService.textDelta().subscribe(data => {
150+
const length = this.messages$.value.length;
151+
this.messages$.value[length - 1].content = data.text.value;
152+
});
153+
}
154+
155+
watchTextDone(): Subscription {
156+
return this.chatGatewayService.textDone().subscribe(data => {
157+
this.isTyping$.next(false);
158+
this.messages$.next([
159+
...this.messages$.value.slice(0, -1),
160+
{
161+
content: data.text.value,
162+
role: ChatRole.Assistant,
163+
},
164+
]);
165+
});
166+
}
167+
130168
watchMessages(): Subscription {
131-
return this.chatGatewayService.getMessages().subscribe(data => {
169+
return this.chatGatewayService.callDone().subscribe(data => {
132170
this.addMessage({
133171
content: data.content,
134172
role: ChatRole.Assistant,

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

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
import { Injectable } from '@angular/core';
2-
import { BehaviorSubject, catchError, Observable, Subject, take, tap } from 'rxjs';
2+
import {
3+
BehaviorSubject,
4+
catchError,
5+
Observable,
6+
Subject,
7+
take,
8+
tap,
9+
} from 'rxjs';
310
import { environment } from '../../../../environments/environment';
411
import { ThreadClientService } from './thread-client.service';
512
import { ConfigurationFormService } from '../../+configuration/shared/configuration-form.service';
@@ -43,11 +50,9 @@ export class ThreadService {
4350
}
4451

4552
getThread(id: string): Observable<GetThreadResponseDto> {
46-
return this.threadClientService
47-
.getThread(id)
48-
.pipe(
49-
take(1),
50-
catchError(() => this.start()),
51-
);
53+
return this.threadClientService.getThread(id).pipe(
54+
take(1),
55+
catchError(() => this.start()),
56+
);
5257
}
5358
}

apps/spa/src/environments/environment.development.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ export const environment = {
1010
isRefreshEnabled: true,
1111
isConfigEnabled: true,
1212
isAutoOpen: true,
13+
isStreamingEnabled: true,
1314
};

apps/spa/src/environments/environment.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ export const environment = {
1010
isRefreshEnabled: true,
1111
isConfigEnabled: true,
1212
isAutoOpen: true,
13+
isStreamingEnabled: true,
1314
};
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import { AssistantCreateParams } from 'openai/resources/beta';
1+
import { FunctionTool } from 'openai/resources/beta';
22

33
export const agentNameMock = 'agent-name';
44

55
export const agentMock = async () => 'agent-result';
66

7-
export const definitionMock: AssistantCreateParams.AssistantToolsFunction = {
7+
export const definitionMock: FunctionTool = {
88
type: 'function',
99
function: { name: agentNameMock },
1010
};

0 commit comments

Comments
 (0)