@multiplayer-app/ai-agent-node 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +45 -0
- package/README.md +611 -0
- package/config.example.json +73 -0
- package/dist/config.d.ts +35 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +44 -0
- package/dist/config.js.map +1 -0
- package/dist/helpers/AIHelper.d.ts +23 -0
- package/dist/helpers/AIHelper.d.ts.map +1 -0
- package/dist/helpers/AIHelper.js +326 -0
- package/dist/helpers/AIHelper.js.map +1 -0
- package/dist/helpers/AIHelper.test.d.ts +2 -0
- package/dist/helpers/AIHelper.test.d.ts.map +1 -0
- package/dist/helpers/AIHelper.test.js +332 -0
- package/dist/helpers/AIHelper.test.js.map +1 -0
- package/dist/helpers/ConfigHelper.d.ts +20 -0
- package/dist/helpers/ConfigHelper.d.ts.map +1 -0
- package/dist/helpers/ConfigHelper.js +118 -0
- package/dist/helpers/ConfigHelper.js.map +1 -0
- package/dist/helpers/ContextLimiter.d.ts +82 -0
- package/dist/helpers/ContextLimiter.d.ts.map +1 -0
- package/dist/helpers/ContextLimiter.js +165 -0
- package/dist/helpers/ContextLimiter.js.map +1 -0
- package/dist/helpers/FileHelper.d.ts +31 -0
- package/dist/helpers/FileHelper.d.ts.map +1 -0
- package/dist/helpers/FileHelper.js +175 -0
- package/dist/helpers/FileHelper.js.map +1 -0
- package/dist/helpers/SetupHelper.d.ts +5 -0
- package/dist/helpers/SetupHelper.d.ts.map +1 -0
- package/dist/helpers/SetupHelper.js +32 -0
- package/dist/helpers/SetupHelper.js.map +1 -0
- package/dist/helpers/index.d.ts +6 -0
- package/dist/helpers/index.d.ts.map +1 -0
- package/dist/helpers/index.js +6 -0
- package/dist/helpers/index.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/libs/index.d.ts +4 -0
- package/dist/libs/index.d.ts.map +1 -0
- package/dist/libs/index.js +4 -0
- package/dist/libs/index.js.map +1 -0
- package/dist/libs/kafka/config.d.ts +5 -0
- package/dist/libs/kafka/config.d.ts.map +1 -0
- package/dist/libs/kafka/config.js +5 -0
- package/dist/libs/kafka/config.js.map +1 -0
- package/dist/libs/kafka/consumer.d.ts +16 -0
- package/dist/libs/kafka/consumer.d.ts.map +1 -0
- package/dist/libs/kafka/consumer.js +126 -0
- package/dist/libs/kafka/consumer.js.map +1 -0
- package/dist/libs/kafka/index.d.ts +3 -0
- package/dist/libs/kafka/index.d.ts.map +1 -0
- package/dist/libs/kafka/index.js +3 -0
- package/dist/libs/kafka/index.js.map +1 -0
- package/dist/libs/kafka/kafka.d.ts +3 -0
- package/dist/libs/kafka/kafka.d.ts.map +1 -0
- package/dist/libs/kafka/kafka.js +24 -0
- package/dist/libs/kafka/kafka.js.map +1 -0
- package/dist/libs/kafka/producer.d.ts +11 -0
- package/dist/libs/kafka/producer.d.ts.map +1 -0
- package/dist/libs/kafka/producer.js +44 -0
- package/dist/libs/kafka/producer.js.map +1 -0
- package/dist/libs/logger/config.d.ts +5 -0
- package/dist/libs/logger/config.d.ts.map +1 -0
- package/dist/libs/logger/config.js +6 -0
- package/dist/libs/logger/config.js.map +1 -0
- package/dist/libs/logger/index.d.ts +10 -0
- package/dist/libs/logger/index.d.ts.map +1 -0
- package/dist/libs/logger/index.js +20 -0
- package/dist/libs/logger/index.js.map +1 -0
- package/dist/libs/logger/kafkajs-logger-creator.d.ts +12 -0
- package/dist/libs/logger/kafkajs-logger-creator.d.ts.map +1 -0
- package/dist/libs/logger/kafkajs-logger-creator.js +29 -0
- package/dist/libs/logger/kafkajs-logger-creator.js.map +1 -0
- package/dist/libs/logger/logger.d.ts +42 -0
- package/dist/libs/logger/logger.d.ts.map +1 -0
- package/dist/libs/logger/logger.js +44 -0
- package/dist/libs/logger/logger.js.map +1 -0
- package/dist/libs/s3/config.d.ts +7 -0
- package/dist/libs/s3/config.d.ts.map +1 -0
- package/dist/libs/s3/config.js +7 -0
- package/dist/libs/s3/config.js.map +1 -0
- package/dist/libs/s3/index.d.ts +4 -0
- package/dist/libs/s3/index.d.ts.map +1 -0
- package/dist/libs/s3/index.js +4 -0
- package/dist/libs/s3/index.js.map +1 -0
- package/dist/libs/s3/s3.lib.d.ts +25 -0
- package/dist/libs/s3/s3.lib.d.ts.map +1 -0
- package/dist/libs/s3/s3.lib.js +202 -0
- package/dist/libs/s3/s3.lib.js.map +1 -0
- package/dist/processors/ChatProcessor.d.ts +66 -0
- package/dist/processors/ChatProcessor.d.ts.map +1 -0
- package/dist/processors/ChatProcessor.js +610 -0
- package/dist/processors/ChatProcessor.js.map +1 -0
- package/dist/processors/ModelsProcessor.d.ts +11 -0
- package/dist/processors/ModelsProcessor.d.ts.map +1 -0
- package/dist/processors/ModelsProcessor.js +30 -0
- package/dist/processors/ModelsProcessor.js.map +1 -0
- package/dist/processors/index.d.ts +3 -0
- package/dist/processors/index.d.ts.map +1 -0
- package/dist/processors/index.js +3 -0
- package/dist/processors/index.js.map +1 -0
- package/dist/services/AIService.d.ts +48 -0
- package/dist/services/AIService.d.ts.map +1 -0
- package/dist/services/AIService.js +196 -0
- package/dist/services/AIService.js.map +1 -0
- package/dist/services/InternalEventsHandler.d.ts +21 -0
- package/dist/services/InternalEventsHandler.d.ts.map +1 -0
- package/dist/services/InternalEventsHandler.js +56 -0
- package/dist/services/InternalEventsHandler.js.map +1 -0
- package/dist/services/KafkaService.d.ts +35 -0
- package/dist/services/KafkaService.d.ts.map +1 -0
- package/dist/services/KafkaService.js +120 -0
- package/dist/services/KafkaService.js.map +1 -0
- package/dist/services/ModelFetcher.d.ts +54 -0
- package/dist/services/ModelFetcher.d.ts.map +1 -0
- package/dist/services/ModelFetcher.js +247 -0
- package/dist/services/ModelFetcher.js.map +1 -0
- package/dist/services/RedisService.d.ts +90 -0
- package/dist/services/RedisService.d.ts.map +1 -0
- package/dist/services/RedisService.js +236 -0
- package/dist/services/RedisService.js.map +1 -0
- package/dist/services/SocketService.d.ts +39 -0
- package/dist/services/SocketService.d.ts.map +1 -0
- package/dist/services/SocketService.js +128 -0
- package/dist/services/SocketService.js.map +1 -0
- package/dist/services/index.d.ts +7 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/index.js +7 -0
- package/dist/services/index.js.map +1 -0
- package/dist/store/AgentStore.d.ts +48 -0
- package/dist/store/AgentStore.d.ts.map +1 -0
- package/dist/store/AgentStore.js +98 -0
- package/dist/store/AgentStore.js.map +1 -0
- package/dist/store/ArtifactStore.d.ts +13 -0
- package/dist/store/ArtifactStore.d.ts.map +1 -0
- package/dist/store/ArtifactStore.js +27 -0
- package/dist/store/ArtifactStore.js.map +1 -0
- package/dist/store/ConfigStore.d.ts +89 -0
- package/dist/store/ConfigStore.d.ts.map +1 -0
- package/dist/store/ConfigStore.js +214 -0
- package/dist/store/ConfigStore.js.map +1 -0
- package/dist/store/ConfigStore.test.d.ts +2 -0
- package/dist/store/ConfigStore.test.d.ts.map +1 -0
- package/dist/store/ConfigStore.test.js +259 -0
- package/dist/store/ConfigStore.test.js.map +1 -0
- package/dist/store/ModelStore.d.ts +44 -0
- package/dist/store/ModelStore.d.ts.map +1 -0
- package/dist/store/ModelStore.js +81 -0
- package/dist/store/ModelStore.js.map +1 -0
- package/dist/store/ModelStore.test.d.ts +2 -0
- package/dist/store/ModelStore.test.d.ts.map +1 -0
- package/dist/store/ModelStore.test.js +390 -0
- package/dist/store/ModelStore.test.js.map +1 -0
- package/dist/store/index.d.ts +5 -0
- package/dist/store/index.d.ts.map +1 -0
- package/dist/store/index.js +5 -0
- package/dist/store/index.js.map +1 -0
- package/dist/tools/generateChartTool.d.ts +24 -0
- package/dist/tools/generateChartTool.d.ts.map +1 -0
- package/dist/tools/generateChartTool.js +124 -0
- package/dist/tools/generateChartTool.js.map +1 -0
- package/dist/tools/proposeFormValuesTool.d.ts +35 -0
- package/dist/tools/proposeFormValuesTool.d.ts.map +1 -0
- package/dist/tools/proposeFormValuesTool.js +56 -0
- package/dist/tools/proposeFormValuesTool.js.map +1 -0
- package/package.json +71 -0
- package/src/config.ts +46 -0
- package/src/helpers/AIHelper.test.ts +375 -0
- package/src/helpers/AIHelper.ts +353 -0
- package/src/helpers/ConfigHelper.ts +130 -0
- package/src/helpers/ContextLimiter.ts +228 -0
- package/src/helpers/FileHelper.ts +197 -0
- package/src/helpers/SetupHelper.ts +35 -0
- package/src/helpers/index.ts +5 -0
- package/src/index.ts +18 -0
- package/src/libs/index.ts +3 -0
- package/src/libs/kafka/config.ts +4 -0
- package/src/libs/kafka/consumer.ts +161 -0
- package/src/libs/kafka/index.ts +2 -0
- package/src/libs/kafka/kafka.ts +27 -0
- package/src/libs/kafka/producer.ts +48 -0
- package/src/libs/logger/config.ts +4 -0
- package/src/libs/logger/index.ts +21 -0
- package/src/libs/logger/kafkajs-logger-creator.ts +28 -0
- package/src/libs/logger/logger.ts +60 -0
- package/src/libs/s3/config.ts +7 -0
- package/src/libs/s3/index.ts +3 -0
- package/src/libs/s3/s3.lib.ts +284 -0
- package/src/processors/ChatProcessor.ts +713 -0
- package/src/processors/ModelsProcessor.ts +34 -0
- package/src/processors/index.ts +2 -0
- package/src/services/AIService.ts +241 -0
- package/src/services/InternalEventsHandler.ts +61 -0
- package/src/services/KafkaService.ts +142 -0
- package/src/services/ModelFetcher.ts +286 -0
- package/src/services/RedisService.ts +285 -0
- package/src/services/SocketService.ts +153 -0
- package/src/services/index.ts +6 -0
- package/src/store/AgentStore.ts +138 -0
- package/src/store/ArtifactStore.ts +29 -0
- package/src/store/ConfigStore.test.ts +314 -0
- package/src/store/ConfigStore.ts +239 -0
- package/src/store/ModelStore.test.ts +473 -0
- package/src/store/ModelStore.ts +93 -0
- package/src/store/index.ts +4 -0
- package/src/tools/generateChartTool.ts +131 -0
- package/src/tools/proposeFormValuesTool.ts +67 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,713 @@
|
|
|
1
|
+
import type {AgentChatRepository, AgentMessageRepository} from '@multiplayer-app/ai-agent-db';
|
|
2
|
+
import {
|
|
3
|
+
AgentArtifact,
|
|
4
|
+
AgentChat,
|
|
5
|
+
AgentChatResponse,
|
|
6
|
+
AgentAttachment,
|
|
7
|
+
AgentMessage,
|
|
8
|
+
AgentStatus,
|
|
9
|
+
AgentToolCallStatus,
|
|
10
|
+
ChatType,
|
|
11
|
+
MessageRole,
|
|
12
|
+
SendMessagePayload,
|
|
13
|
+
SortOrder,
|
|
14
|
+
StreamChunk,
|
|
15
|
+
StreamChunkType,
|
|
16
|
+
SendMessagePayloadWithApproval,
|
|
17
|
+
SendMessagePayloadWithContent,
|
|
18
|
+
AdditionalContext
|
|
19
|
+
} from '@multiplayer-app/ai-agent-types';
|
|
20
|
+
import { ArtifactStore } from '../store';
|
|
21
|
+
import { socketService } from '../services';
|
|
22
|
+
import { kafkaService } from '../services';
|
|
23
|
+
import { AIHelper } from '../helpers';
|
|
24
|
+
import { AgentProcessEventType, AgentProcessEvent, agentStore } from '../store';
|
|
25
|
+
import { GenerateTextOptions } from '../services';
|
|
26
|
+
import { ContextLimiter } from '../helpers';
|
|
27
|
+
import { config } from '../config';
|
|
28
|
+
import { PassThrough, pipeline } from 'stream';
|
|
29
|
+
import { promisify } from 'util';
|
|
30
|
+
import { logger } from '../libs/logger';
|
|
31
|
+
|
|
32
|
+
const pipelineAsync = promisify(pipeline);
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
export class ChatProcessor {
|
|
36
|
+
constructor(
|
|
37
|
+
private chatRepository: AgentChatRepository,
|
|
38
|
+
private messageRepository: AgentMessageRepository,
|
|
39
|
+
private artifactStore: ArtifactStore
|
|
40
|
+
) { }
|
|
41
|
+
|
|
42
|
+
private getTemporaryTitle(contextKey: string): string {
|
|
43
|
+
return `${contextKey} session ${new Date().toISOString()}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async listChats(params?: {
|
|
47
|
+
contextKey?: string;
|
|
48
|
+
limit?: number;
|
|
49
|
+
userId?: string;
|
|
50
|
+
sortField?: string;
|
|
51
|
+
sortOrder?: SortOrder;
|
|
52
|
+
}): Promise<AgentChatResponse[]> {
|
|
53
|
+
// Build filter object from params
|
|
54
|
+
const filter: Partial<AgentChat> = {};
|
|
55
|
+
if (params?.userId) {
|
|
56
|
+
filter.userId = params.userId;
|
|
57
|
+
}
|
|
58
|
+
if (params?.contextKey) {
|
|
59
|
+
filter.contextKey = params.contextKey;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Build query options for sort and limit with defaults
|
|
63
|
+
const options = {
|
|
64
|
+
sort: {
|
|
65
|
+
field: params?.sortField ?? 'updatedAt',
|
|
66
|
+
order: params?.sortOrder ?? SortOrder.Desc
|
|
67
|
+
},
|
|
68
|
+
limit: params?.limit
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// Use aggregation to fetch chats with related messages
|
|
72
|
+
return this.chatRepository.findWithMessages(
|
|
73
|
+
filter,
|
|
74
|
+
options
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async getChat(chatId: string): Promise<AgentChatResponse> {
|
|
79
|
+
const chat = await this.chatRepository.findById(chatId);
|
|
80
|
+
if (!chat) {
|
|
81
|
+
throw new Error('Chat not found');
|
|
82
|
+
}
|
|
83
|
+
const messages = await this.messageRepository.findByChatId(chatId);
|
|
84
|
+
return {
|
|
85
|
+
...chat,
|
|
86
|
+
messages
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async deleteChat(chatId: string): Promise<void> {
|
|
91
|
+
const deleted = await this.chatRepository.delete(chatId);
|
|
92
|
+
if (!deleted) {
|
|
93
|
+
throw new Error('Chat not found');
|
|
94
|
+
}
|
|
95
|
+
this.artifactStore.deleteArtifacts(chatId);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async upsertAndGetChat(payload: SendMessagePayload, excludeSocketId?: string) {
|
|
99
|
+
const targetUserId = payload.userId ?? 'guest';
|
|
100
|
+
let chat: AgentChat | null = null;
|
|
101
|
+
|
|
102
|
+
if (payload.chatId) {
|
|
103
|
+
chat = await this.chatRepository.findById(payload.chatId);
|
|
104
|
+
if (!chat) {
|
|
105
|
+
throw new Error('Chat not found');
|
|
106
|
+
}
|
|
107
|
+
// Ensure chat is associated with the caller's userId
|
|
108
|
+
if (chat.userId && chat.userId !== targetUserId) {
|
|
109
|
+
throw new Error('Chat does not belong to this user');
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (!chat) {
|
|
114
|
+
chat = await this.createChat(payload as SendMessagePayloadWithContent, excludeSocketId);
|
|
115
|
+
} else if ('model' in payload) {
|
|
116
|
+
chat.model = payload.model || undefined;
|
|
117
|
+
await this.chatRepository.update(chat.id, { model: chat.model });
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return chat;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async getChatTitle(payload: {
|
|
124
|
+
chatId?: string;
|
|
125
|
+
content?: string;
|
|
126
|
+
contextKey: string;
|
|
127
|
+
userId?: string;
|
|
128
|
+
}): Promise<string> {
|
|
129
|
+
if (payload.chatId) {
|
|
130
|
+
const chat = await this.chatRepository.findById(payload.chatId);
|
|
131
|
+
if (chat && chat.title) {
|
|
132
|
+
return chat.title;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const content = payload.content;
|
|
137
|
+
if (!content) {
|
|
138
|
+
return this.getTemporaryTitle(payload.contextKey)
|
|
139
|
+
}
|
|
140
|
+
return AIHelper.generateTitleForMessage(payload.contextKey, [{
|
|
141
|
+
role: MessageRole.User,
|
|
142
|
+
content: content
|
|
143
|
+
}]);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async createMessage(chat: AgentChat, role: MessageRole, messageData: Pick<AgentMessage, 'content' | 'agentName'>, attachments?: AgentAttachment[], excludeSocketId?: string): Promise<AgentMessage> {
|
|
147
|
+
const message = await this.messageRepository.create({
|
|
148
|
+
chat: chat.id,
|
|
149
|
+
role,
|
|
150
|
+
content: messageData.content,
|
|
151
|
+
agentName: messageData.agentName,
|
|
152
|
+
attachments: attachments ?? [],
|
|
153
|
+
reasoning: "",
|
|
154
|
+
toolCalls: []
|
|
155
|
+
});
|
|
156
|
+
if (chat.userId) {
|
|
157
|
+
socketService.emitMessageUpdate(chat.userId, message, excludeSocketId);
|
|
158
|
+
}
|
|
159
|
+
return message;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async createChat(payload: SendMessagePayload, excludeSocketId?: string): Promise<AgentChat> {
|
|
163
|
+
const targetUserId = payload.userId ?? 'guest';
|
|
164
|
+
const contextKey = 'contextKey' in payload ? payload.contextKey : 'default';
|
|
165
|
+
const metadata = 'metadata' in payload ? payload.metadata : {};
|
|
166
|
+
const chat = await this.chatRepository.create({
|
|
167
|
+
title: this.getTemporaryTitle(contextKey),
|
|
168
|
+
type: ChatType.Chat,
|
|
169
|
+
status: AgentStatus.Streaming,
|
|
170
|
+
contextKey,
|
|
171
|
+
userId: targetUserId,
|
|
172
|
+
metadata,
|
|
173
|
+
...(payload.model ? { model: payload.model } : {})
|
|
174
|
+
});
|
|
175
|
+
await kafkaService.sendChatTitleGenerationEvent(chat.id);
|
|
176
|
+
const initialChatResponse: AgentChatResponse = {
|
|
177
|
+
...chat,
|
|
178
|
+
messages: []
|
|
179
|
+
};
|
|
180
|
+
socketService.emitChatUpdate(targetUserId, initialChatResponse, excludeSocketId);
|
|
181
|
+
|
|
182
|
+
return chat;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Persist a client-side action against a tool call (generic mechanism).
|
|
187
|
+
* This updates the message containing the tool call and optionally appends a System message.
|
|
188
|
+
*/
|
|
189
|
+
async recordToolCallAction(params: {
|
|
190
|
+
chatId: string;
|
|
191
|
+
toolCallId: string;
|
|
192
|
+
action: string;
|
|
193
|
+
data: Record<string, unknown>;
|
|
194
|
+
systemMessage?: string;
|
|
195
|
+
excludeSocketId?: string;
|
|
196
|
+
}): Promise<{ ok: true }> {
|
|
197
|
+
const chat = await this.chatRepository.findById(params.chatId);
|
|
198
|
+
if (!chat) {
|
|
199
|
+
throw new Error('Chat not found');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const messages = await this.messageRepository.findByChatId(params.chatId);
|
|
203
|
+
const message = messages.find((m) => (m.toolCalls ?? []).some((c) => c.id === params.toolCallId));
|
|
204
|
+
if (!message) {
|
|
205
|
+
throw new Error('Tool call not found');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const toolCalls = message.toolCalls ?? [];
|
|
209
|
+
const target = toolCalls.find((c) => c.id === params.toolCallId);
|
|
210
|
+
if (!target) {
|
|
211
|
+
throw new Error('Tool call not found');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const now = new Date().toISOString();
|
|
215
|
+
const output = (target.output ?? {}) as Record<string, unknown>;
|
|
216
|
+
const mp = (output['_mp'] && typeof output['_mp'] === 'object' && !Array.isArray(output['_mp']))
|
|
217
|
+
? (output['_mp'] as Record<string, unknown>)
|
|
218
|
+
: {};
|
|
219
|
+
const actions = Array.isArray(mp['actions']) ? (mp['actions'] as unknown[]) : [];
|
|
220
|
+
|
|
221
|
+
const nextMp = {
|
|
222
|
+
...mp,
|
|
223
|
+
lastAction: {
|
|
224
|
+
action: params.action,
|
|
225
|
+
at: now,
|
|
226
|
+
data: params.data ?? {},
|
|
227
|
+
},
|
|
228
|
+
actions: [
|
|
229
|
+
...actions,
|
|
230
|
+
{
|
|
231
|
+
action: params.action,
|
|
232
|
+
at: now,
|
|
233
|
+
data: params.data ?? {},
|
|
234
|
+
}
|
|
235
|
+
]
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
target.output = {
|
|
239
|
+
...output,
|
|
240
|
+
_mp: nextMp,
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const updatedMessage: AgentMessage = {
|
|
244
|
+
...message,
|
|
245
|
+
toolCalls,
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
await this.messageRepository.update(message.id, { toolCalls });
|
|
249
|
+
// Bump chat updatedAt so listChats ordering reflects the action.
|
|
250
|
+
await this.chatRepository.update(chat.id, { updatedAt: now } as any);
|
|
251
|
+
socketService.emitMessageUpdate(chat.userId, updatedMessage, params.excludeSocketId);
|
|
252
|
+
|
|
253
|
+
if (params.systemMessage) {
|
|
254
|
+
//todo discuss support for system messages
|
|
255
|
+
//await this.createMessage(chat, MessageRole.System, params.systemMessage, undefined, params.excludeSocketId);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return { ok: true };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private async streamMessageStep(params: { chat: AgentChat, assistantMessage: AgentMessage, existingMessages: AgentMessage[], signal: AbortSignal, excludeSocketId?: string, agentOptions: Omit<GenerateTextOptions, 'messages'> }): Promise<void> {
|
|
262
|
+
const { chat, assistantMessage, existingMessages, signal, excludeSocketId, agentOptions } = params;
|
|
263
|
+
if (signal.aborted) {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
const result = await AIHelper.streamAssistantResponse(existingMessages, signal, agentOptions);
|
|
267
|
+
|
|
268
|
+
try {
|
|
269
|
+
for await (const chunk of result.fullStream) {
|
|
270
|
+
if (chunk.type === 'error') {
|
|
271
|
+
await agentStore.shareAgentProcessEvent(chat.id, { type: AgentProcessEventType.Error, data: chunk.error as Error });
|
|
272
|
+
assistantMessage.content = (chunk.error as Record<string, string>).message || 'Sorry, I cannot process you request due to the error';
|
|
273
|
+
await this.messageRepository.update(assistantMessage.id, assistantMessage);
|
|
274
|
+
await this.chatRepository.update(chat.id, { status: AgentStatus.Error });
|
|
275
|
+
continue
|
|
276
|
+
}
|
|
277
|
+
if (chunk.type === 'abort') {
|
|
278
|
+
await agentStore.shareAgentProcessEvent(chat.id, { type: AgentProcessEventType.Aborted, data: undefined });
|
|
279
|
+
await this.chatRepository.update(chat.id, { status: AgentStatus.Aborted });
|
|
280
|
+
await this.messageRepository.update(assistantMessage.id, assistantMessage);
|
|
281
|
+
socketService.emitMessageUpdate(chat.userId, assistantMessage, excludeSocketId);
|
|
282
|
+
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
if (chunk.type === 'finish') {
|
|
286
|
+
const index = existingMessages.findIndex(({ id }) => id === assistantMessage.id);
|
|
287
|
+
|
|
288
|
+
if (index === -1){
|
|
289
|
+
existingMessages.push(assistantMessage)
|
|
290
|
+
} else {
|
|
291
|
+
existingMessages[index] = assistantMessage;
|
|
292
|
+
}
|
|
293
|
+
if (chunk.totalUsage && typeof chunk.totalUsage === 'object') {
|
|
294
|
+
const totalUsage = chunk.totalUsage as { promptTokens?: number; completionTokens?: number; totalTokens?: number };
|
|
295
|
+
assistantMessage.tokens = totalUsage.totalTokens ??
|
|
296
|
+
((totalUsage.promptTokens ?? 0) + (totalUsage.completionTokens ?? 0));
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
await this.messageRepository.update(assistantMessage.id, { ...assistantMessage });
|
|
300
|
+
if (chunk.finishReason === 'stop') {
|
|
301
|
+
if (assistantMessage.toolCalls?.some(toolCall => toolCall.requiresConfirmation && toolCall.approvalId && !toolCall.output)) {
|
|
302
|
+
await this.chatRepository.update(chat.id, { status: AgentStatus.WaitingForUserAction });
|
|
303
|
+
} else if (!assistantMessage.content && assistantMessage.toolCalls?.length && assistantMessage.toolCalls[assistantMessage.toolCalls?.length - 1].output) {
|
|
304
|
+
// handle tool-calls stop, openrouter provider does not return valid type
|
|
305
|
+
await agentStore.shareAgentProcessEvent(chat.id, { type: AgentProcessEventType.Update, data: assistantMessage });
|
|
306
|
+
await this.streamMessageStep(params);
|
|
307
|
+
continue;
|
|
308
|
+
} else {
|
|
309
|
+
await this.chatRepository.update(chat.id, {status: AgentStatus.Finished});
|
|
310
|
+
}
|
|
311
|
+
await agentStore.shareAgentProcessEvent(chat.id, { type: AgentProcessEventType.Finished, data: assistantMessage });
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
if (chunk.finishReason === 'tool-calls') {
|
|
315
|
+
if (assistantMessage.toolCalls?.some(toolCall => toolCall.requiresConfirmation && toolCall.approvalId && !toolCall.output)) {
|
|
316
|
+
await this.chatRepository.update(chat.id, { status: AgentStatus.WaitingForUserAction });
|
|
317
|
+
await agentStore.shareAgentProcessEvent(chat.id, { type: AgentProcessEventType.Finished, data: assistantMessage });
|
|
318
|
+
} else {
|
|
319
|
+
await agentStore.shareAgentProcessEvent(chat.id, {
|
|
320
|
+
type: AgentProcessEventType.Update,
|
|
321
|
+
data: assistantMessage
|
|
322
|
+
});
|
|
323
|
+
await this.streamMessageStep(params);
|
|
324
|
+
}
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
//todo: Handle other finish reasons // length, content, error, other, undefined
|
|
328
|
+
await agentStore.shareAgentProcessEvent(chat.id, { type: AgentProcessEventType.Finished, data: assistantMessage });
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
if (chunk.type === 'text-delta') {
|
|
332
|
+
assistantMessage.content += chunk.text;
|
|
333
|
+
const updatedMessage: AgentMessage = {
|
|
334
|
+
...assistantMessage,
|
|
335
|
+
content: assistantMessage.content
|
|
336
|
+
};
|
|
337
|
+
socketService.emitMessageUpdate(chat.userId, updatedMessage, excludeSocketId);
|
|
338
|
+
await agentStore.shareAgentProcessEvent(chat.id, { type: AgentProcessEventType.Update, data: updatedMessage });
|
|
339
|
+
await this.messageRepository.update(assistantMessage.id, { content: assistantMessage.content });
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
if (chunk.type === 'tool-input-start') {
|
|
343
|
+
if (!assistantMessage.toolCalls) assistantMessage.toolCalls = [];
|
|
344
|
+
assistantMessage.toolCalls.push({
|
|
345
|
+
id: chunk.id,
|
|
346
|
+
name: chunk.toolName,
|
|
347
|
+
status: AgentToolCallStatus.Pending,
|
|
348
|
+
input: {},
|
|
349
|
+
});
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
if (chunk.type === 'tool-call') {//Todo possible place to handle user-approval
|
|
353
|
+
const toolCall = assistantMessage.toolCalls?.find(toolCall => toolCall.id === chunk.toolCallId);
|
|
354
|
+
if (toolCall) {
|
|
355
|
+
toolCall.status = AgentToolCallStatus.Running;
|
|
356
|
+
toolCall.input = chunk.input;
|
|
357
|
+
}
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
if (chunk.type === 'tool-error') {
|
|
361
|
+
const toolCall = assistantMessage.toolCalls?.find(toolCall => toolCall.id === chunk.toolCallId);
|
|
362
|
+
if (toolCall) {
|
|
363
|
+
toolCall.status = AgentToolCallStatus.Failed;
|
|
364
|
+
toolCall.error = JSON.stringify(chunk.error);
|
|
365
|
+
}
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
if (chunk.type === 'tool-approval-request') {
|
|
369
|
+
const toolCall = assistantMessage.toolCalls?.find(toolCall => toolCall.id === chunk.toolCall.toolCallId);
|
|
370
|
+
|
|
371
|
+
if (toolCall) {
|
|
372
|
+
toolCall.requiresConfirmation = true;
|
|
373
|
+
toolCall.approvalId = chunk.approvalId;
|
|
374
|
+
}
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
if (chunk.type === 'tool-result') {
|
|
378
|
+
const toolCall = assistantMessage.toolCalls?.find(toolCall => toolCall.id === chunk.toolCallId);
|
|
379
|
+
|
|
380
|
+
if (toolCall) {
|
|
381
|
+
if (chunk.output.requiresApproval) {
|
|
382
|
+
toolCall.requiresConfirmation = true;
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
toolCall.status = AgentToolCallStatus.Succeeded;
|
|
386
|
+
toolCall.output = chunk.output;
|
|
387
|
+
}
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
if (chunk.type === 'reasoning-delta') {
|
|
391
|
+
if (!assistantMessage.reasoning) assistantMessage.reasoning = ''
|
|
392
|
+
assistantMessage.reasoning += chunk.text;
|
|
393
|
+
socketService.emitMessageUpdate(chat.userId, assistantMessage, excludeSocketId);
|
|
394
|
+
await this.messageRepository.update(assistantMessage.id, { reasoning: assistantMessage.reasoning });
|
|
395
|
+
await agentStore.shareAgentProcessEvent(chat.id, { type: AgentProcessEventType.Update, data: assistantMessage });
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
} catch (streamError) {
|
|
400
|
+
if (streamError instanceof Error && streamError.name === 'AbortError') {
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
await agentStore.shareAgentProcessEvent(chat.id, { type: AgentProcessEventType.Error, data: streamError as Error });
|
|
404
|
+
throw streamError;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
private isApprovalPayload(payload: SendMessagePayload): payload is SendMessagePayloadWithApproval {
|
|
409
|
+
return 'approvalId' in payload;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
private async processApprovalPayload(chat: AgentChat, payload: SendMessagePayloadWithApproval & { context?: AdditionalContext }) {
|
|
413
|
+
const assistantMessage = await this.messageRepository.findById(payload.messageId);
|
|
414
|
+
if (!assistantMessage || assistantMessage.chat !== chat.id) {
|
|
415
|
+
throw new Error(`Assistant message with id ${payload.messageId} not found`);
|
|
416
|
+
}
|
|
417
|
+
const agentOptions = await AIHelper.getAgentOptions({
|
|
418
|
+
agentName: assistantMessage.agentName,
|
|
419
|
+
context: payload.context
|
|
420
|
+
});
|
|
421
|
+
const toolCall = assistantMessage.toolCalls?.find(tc => tc.approvalId === payload.approvalId);
|
|
422
|
+
if (!toolCall) {
|
|
423
|
+
// continue, tool is not found
|
|
424
|
+
return {
|
|
425
|
+
assistantMessage,
|
|
426
|
+
agentOptions,
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
toolCall.approved = payload.approved;
|
|
431
|
+
toolCall.reason = payload.reason;
|
|
432
|
+
|
|
433
|
+
if (!toolCall.approved) {
|
|
434
|
+
toolCall.output = {
|
|
435
|
+
type: 'execution-denied',
|
|
436
|
+
reason: toolCall.reason
|
|
437
|
+
}
|
|
438
|
+
toolCall.status = AgentToolCallStatus.Failed;
|
|
439
|
+
} else {
|
|
440
|
+
const executeFunction = agentOptions.tools?.[toolCall.name]?.execute;
|
|
441
|
+
if (executeFunction) {
|
|
442
|
+
try {
|
|
443
|
+
toolCall.output = await executeFunction(toolCall.input, {toolCallId: toolCall.id, messages: []})
|
|
444
|
+
toolCall.status = AgentToolCallStatus.Succeeded;
|
|
445
|
+
} catch(error) {
|
|
446
|
+
logger.error(error);
|
|
447
|
+
toolCall.status = AgentToolCallStatus.Failed;
|
|
448
|
+
toolCall.output = {
|
|
449
|
+
type: 'execution-denied',
|
|
450
|
+
reason: 'Tool failed with error'
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
} else {
|
|
454
|
+
toolCall.output = {
|
|
455
|
+
type: 'execution-denied',
|
|
456
|
+
reason: 'Tool was not found'
|
|
457
|
+
}
|
|
458
|
+
toolCall.status = AgentToolCallStatus.Failed;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
await this.messageRepository.update(assistantMessage.id, assistantMessage);
|
|
463
|
+
return {
|
|
464
|
+
assistantMessage,
|
|
465
|
+
agentOptions,
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
async processNewUserMessage(chat: AgentChat, payload: SendMessagePayloadWithContent & { context?: AdditionalContext }, prevMessages: AgentMessage[], excludeSocketId?: string) {
|
|
470
|
+
const userMessage = await this.createMessage(chat, MessageRole.User, {content: payload.content, agentName: undefined}, payload.attachments);
|
|
471
|
+
await agentStore.shareAgentProcessEvent(chat.id, { type: AgentProcessEventType.Update, data: userMessage });
|
|
472
|
+
|
|
473
|
+
const agentOptions = await AIHelper.getAgentOptions({
|
|
474
|
+
contextKey: chat.contextKey,
|
|
475
|
+
messages: await AIHelper.convertMessages([...prevMessages, userMessage], true),
|
|
476
|
+
context: payload.context,
|
|
477
|
+
agentName: undefined,
|
|
478
|
+
modelId: chat.model,
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
const assistantMessage = await this.createMessage(chat, MessageRole.Assistant, {content: ' ', agentName: agentOptions.name}, undefined, excludeSocketId);
|
|
482
|
+
assistantMessage.content = ''
|
|
483
|
+
|
|
484
|
+
return {
|
|
485
|
+
userMessage,
|
|
486
|
+
assistantMessage,
|
|
487
|
+
agentOptions,
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
async streamMessage(chat: AgentChat, payload: SendMessagePayload & { context?: AdditionalContext }, excludeSocketId?: string, onAgentStart?: () => void): Promise<void> {
|
|
492
|
+
try {
|
|
493
|
+
const abortController = agentStore.registerAgentProcess(chat.id);
|
|
494
|
+
onAgentStart?.()
|
|
495
|
+
let assistantMessage: AgentMessage;
|
|
496
|
+
let agentOptions: Omit<GenerateTextOptions, 'messages'>;
|
|
497
|
+
|
|
498
|
+
const existingMessages = await this.messageRepository.findByChatId(chat.id);
|
|
499
|
+
|
|
500
|
+
// Limit context to prevent exceeding token limits
|
|
501
|
+
const limitedMessages = ContextLimiter.limitContext(
|
|
502
|
+
existingMessages,
|
|
503
|
+
{
|
|
504
|
+
maxMessages: config.ai.maxContextMessages,
|
|
505
|
+
keepFirstUserMessage: true,
|
|
506
|
+
keepSystemMessages: true,
|
|
507
|
+
}
|
|
508
|
+
);
|
|
509
|
+
|
|
510
|
+
if (this.isApprovalPayload(payload)) {
|
|
511
|
+
const result = await this.processApprovalPayload(chat, payload);
|
|
512
|
+
assistantMessage = result.assistantMessage;
|
|
513
|
+
agentOptions = result.agentOptions;
|
|
514
|
+
const foundIndex = limitedMessages.findIndex(m => m.id === result.assistantMessage.id);
|
|
515
|
+
if(foundIndex === -1) {
|
|
516
|
+
limitedMessages.push(result.assistantMessage);
|
|
517
|
+
} else {
|
|
518
|
+
limitedMessages[foundIndex] = result.assistantMessage;
|
|
519
|
+
}
|
|
520
|
+
} else {
|
|
521
|
+
const result = await this.processNewUserMessage(chat, payload, limitedMessages, excludeSocketId);
|
|
522
|
+
assistantMessage = result.assistantMessage;
|
|
523
|
+
agentOptions = result.agentOptions;
|
|
524
|
+
limitedMessages.push(result.userMessage);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
await this.streamMessageStep({
|
|
528
|
+
chat,
|
|
529
|
+
existingMessages: limitedMessages,
|
|
530
|
+
assistantMessage,
|
|
531
|
+
agentOptions,
|
|
532
|
+
excludeSocketId: excludeSocketId,
|
|
533
|
+
signal: abortController.signal,
|
|
534
|
+
})
|
|
535
|
+
} catch (error) {
|
|
536
|
+
throw error;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
listArtifacts(chatId: string): AgentArtifact[] {
|
|
541
|
+
return this.artifactStore.listArtifacts(chatId);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
private eventToChunk(event: AgentProcessEvent): StreamChunk | null {
|
|
545
|
+
switch (event.type) {
|
|
546
|
+
case AgentProcessEventType.Update:
|
|
547
|
+
return {
|
|
548
|
+
type: StreamChunkType.Message,
|
|
549
|
+
message: event.data
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
case AgentProcessEventType.Error:
|
|
553
|
+
return {
|
|
554
|
+
type: StreamChunkType.Error,
|
|
555
|
+
error: event.data.message || 'Unknown error occurred'
|
|
556
|
+
};
|
|
557
|
+
case AgentProcessEventType.Finished:
|
|
558
|
+
return {
|
|
559
|
+
type: StreamChunkType.Message,
|
|
560
|
+
message: event.data
|
|
561
|
+
};
|
|
562
|
+
case AgentProcessEventType.Aborted:
|
|
563
|
+
// Aborted events don't produce chunks, they just signal stream end
|
|
564
|
+
return null;
|
|
565
|
+
case AgentProcessEventType.Stop:
|
|
566
|
+
// Stop events don't produce chunks
|
|
567
|
+
return null;
|
|
568
|
+
default:
|
|
569
|
+
return null;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
getMessageStream(chatId: string): PassThrough {
|
|
574
|
+
const stream = new PassThrough();
|
|
575
|
+
let ended = false;
|
|
576
|
+
|
|
577
|
+
const endStream = (error?: unknown) => {
|
|
578
|
+
if (ended) return;
|
|
579
|
+
ended = true;
|
|
580
|
+
|
|
581
|
+
if (error) {
|
|
582
|
+
logger.error({ error }, 'Message stream error');
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
if (!stream.destroyed && !stream.writableEnded) {
|
|
586
|
+
try {
|
|
587
|
+
stream.write('data: [DONE]\n\n');
|
|
588
|
+
stream.end();
|
|
589
|
+
} catch (e) {
|
|
590
|
+
logger.error({ e }, 'Failed to end SSE stream');
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
};
|
|
594
|
+
|
|
595
|
+
const pushChunk = (chunk: StreamChunk) => {
|
|
596
|
+
if (ended || stream.destroyed || stream.writableEnded) return;
|
|
597
|
+
stream.write(`data: ${JSON.stringify(chunk)}\n\n`);
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
const listener = (event: AgentProcessEvent) => {
|
|
601
|
+
if (ended) return;
|
|
602
|
+
|
|
603
|
+
try {
|
|
604
|
+
const chunk = this.eventToChunk(event);
|
|
605
|
+
if (chunk) {
|
|
606
|
+
pushChunk(chunk);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
if (
|
|
610
|
+
event.type === AgentProcessEventType.Finished ||
|
|
611
|
+
event.type === AgentProcessEventType.Error ||
|
|
612
|
+
event.type === AgentProcessEventType.Aborted
|
|
613
|
+
) {
|
|
614
|
+
endStream();
|
|
615
|
+
agentStore.removeListener(chatId, listener);
|
|
616
|
+
}
|
|
617
|
+
} catch (err) {
|
|
618
|
+
pushChunk({
|
|
619
|
+
type: StreamChunkType.Error,
|
|
620
|
+
error: err instanceof Error ? err.message : 'Unknown error occurred',
|
|
621
|
+
});
|
|
622
|
+
endStream(err);
|
|
623
|
+
agentStore.removeListener(chatId, listener);
|
|
624
|
+
}
|
|
625
|
+
};
|
|
626
|
+
|
|
627
|
+
// Cleanup on consumer side closing
|
|
628
|
+
const closeHandler = () => {
|
|
629
|
+
agentStore.removeListener(chatId, listener);
|
|
630
|
+
};
|
|
631
|
+
|
|
632
|
+
const errorHandler = (err: Error) => {
|
|
633
|
+
logger.error({ err }, 'Readable stream error');
|
|
634
|
+
agentStore.removeListener(chatId, listener);
|
|
635
|
+
endStream(err);
|
|
636
|
+
};
|
|
637
|
+
|
|
638
|
+
stream.on('close', closeHandler);
|
|
639
|
+
stream.on('error', errorHandler);
|
|
640
|
+
|
|
641
|
+
const listenerAttached = agentStore.addListener(chatId, listener);
|
|
642
|
+
if (!listenerAttached) {
|
|
643
|
+
// Remove stream listeners if listener attachment failed to prevent memory leak
|
|
644
|
+
stream.removeListener('close', closeHandler);
|
|
645
|
+
stream.removeListener('error', errorHandler);
|
|
646
|
+
endStream(new Error('Listener could not be attached'));
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
return stream;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
async createMessageStream(
|
|
654
|
+
payload: SendMessagePayload & { context?: AdditionalContext },
|
|
655
|
+
excludeSocketId?: string
|
|
656
|
+
): Promise<PassThrough> {
|
|
657
|
+
const stream = new PassThrough();
|
|
658
|
+
|
|
659
|
+
(async () => {
|
|
660
|
+
try {
|
|
661
|
+
const chat = await this.upsertAndGetChat(payload, excludeSocketId);
|
|
662
|
+
|
|
663
|
+
// Send chat metadata first
|
|
664
|
+
if (!('chatId' in payload) && !('messageId' in payload) && chat) {
|
|
665
|
+
stream.write(
|
|
666
|
+
`data: ${JSON.stringify({
|
|
667
|
+
type: StreamChunkType.Chat,
|
|
668
|
+
chatId: chat.id,
|
|
669
|
+
chat,
|
|
670
|
+
})}\n\n`
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
await this.streamMessage(chat, payload, excludeSocketId, async () => {
|
|
675
|
+
if (stream.destroyed || stream.writableEnded){
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
const messageStream = this.getMessageStream(chat.id);
|
|
679
|
+
|
|
680
|
+
// If client disconnects, destroy inner stream
|
|
681
|
+
stream.on('close', () => {
|
|
682
|
+
if (!messageStream.destroyed) {
|
|
683
|
+
messageStream.destroy();
|
|
684
|
+
}
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
await pipelineAsync(
|
|
688
|
+
messageStream,
|
|
689
|
+
stream
|
|
690
|
+
);
|
|
691
|
+
});
|
|
692
|
+
} catch (error) {
|
|
693
|
+
logger.error({ error }, 'createMessageStream failed');
|
|
694
|
+
|
|
695
|
+
if (!stream.destroyed && !stream.writableEnded) {
|
|
696
|
+
stream.write(
|
|
697
|
+
`data: ${JSON.stringify({
|
|
698
|
+
type: StreamChunkType.Error,
|
|
699
|
+
error: error instanceof Error ? error.message : 'Unknown error occurred',
|
|
700
|
+
})}\n\n`
|
|
701
|
+
);
|
|
702
|
+
stream.write('data: [DONE]\n\n');
|
|
703
|
+
stream.end();
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
})().catch((err) => {
|
|
707
|
+
logger.error({ err }, 'Unhandled stream task error');
|
|
708
|
+
if (!stream.destroyed) stream.destroy(err);
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
return stream;
|
|
712
|
+
}
|
|
713
|
+
}
|