@hed-hog/core 0.0.349 → 0.0.350
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/dist/auth/auth.controller.d.ts +3 -3
- package/dist/auth/auth.service.d.ts +8 -8
- package/dist/core.module.d.ts.map +1 -1
- package/dist/core.module.js +3 -0
- package/dist/core.module.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp/guards/mcp-auth.guard.d.ts +6 -1
- package/dist/mcp/guards/mcp-auth.guard.d.ts.map +1 -1
- package/dist/mcp/guards/mcp-auth.guard.js +29 -2
- package/dist/mcp/guards/mcp-auth.guard.js.map +1 -1
- package/dist/mcp/mcp-api-key.controller.d.ts +32 -0
- package/dist/mcp/mcp-api-key.controller.d.ts.map +1 -0
- package/dist/mcp/mcp-api-key.controller.js +64 -0
- package/dist/mcp/mcp-api-key.controller.js.map +1 -0
- package/dist/mcp/mcp-api-key.service.d.ts +27 -0
- package/dist/mcp/mcp-api-key.service.d.ts.map +1 -0
- package/dist/mcp/mcp-api-key.service.js +74 -0
- package/dist/mcp/mcp-api-key.service.js.map +1 -0
- package/dist/mcp/mcp-registry.service.d.ts +1 -2
- package/dist/mcp/mcp-registry.service.d.ts.map +1 -1
- package/dist/mcp/mcp-registry.service.js +6 -2
- package/dist/mcp/mcp-registry.service.js.map +1 -1
- package/dist/mcp/mcp.module.d.ts.map +1 -1
- package/dist/mcp/mcp.module.js +8 -1
- package/dist/mcp/mcp.module.js.map +1 -1
- package/dist/mcp-chat/mcp-chat.controller.d.ts +4 -0
- package/dist/mcp-chat/mcp-chat.controller.d.ts.map +1 -1
- package/dist/mcp-chat/mcp-chat.controller.js +46 -0
- package/dist/mcp-chat/mcp-chat.controller.js.map +1 -1
- package/dist/mcp-chat/mcp-chat.service.d.ts +6 -1
- package/dist/mcp-chat/mcp-chat.service.d.ts.map +1 -1
- package/dist/mcp-chat/mcp-chat.service.js +125 -17
- package/dist/mcp-chat/mcp-chat.service.js.map +1 -1
- package/dist/notification/dto/create-notification.dto.d.ts +26 -0
- package/dist/notification/dto/create-notification.dto.d.ts.map +1 -0
- package/dist/notification/dto/create-notification.dto.js +86 -0
- package/dist/notification/dto/create-notification.dto.js.map +1 -0
- package/dist/notification/dto/update-progress.dto.d.ts +6 -0
- package/dist/notification/dto/update-progress.dto.d.ts.map +1 -0
- package/dist/notification/dto/update-progress.dto.js +33 -0
- package/dist/notification/dto/update-progress.dto.js.map +1 -0
- package/dist/notification/notification.controller.d.ts +108 -0
- package/dist/notification/notification.controller.d.ts.map +1 -0
- package/dist/notification/notification.controller.js +118 -0
- package/dist/notification/notification.controller.js.map +1 -0
- package/dist/notification/notification.module.d.ts +3 -0
- package/dist/notification/notification.module.d.ts.map +1 -0
- package/dist/notification/notification.module.js +29 -0
- package/dist/notification/notification.module.js.map +1 -0
- package/dist/notification/notification.service.d.ts +98 -0
- package/dist/notification/notification.service.d.ts.map +1 -0
- package/dist/notification/notification.service.js +123 -0
- package/dist/notification/notification.service.js.map +1 -0
- package/dist/profile/profile.controller.d.ts +1 -1
- package/dist/profile/profile.service.d.ts +1 -1
- package/dist/session/session.controller.d.ts +1 -1
- package/dist/session/session.service.d.ts +3 -3
- package/dist/setting/dto/update-setting-list.dto.d.ts +9 -0
- package/dist/setting/dto/update-setting-list.dto.d.ts.map +1 -0
- package/dist/setting/dto/update-setting-list.dto.js +44 -0
- package/dist/setting/dto/update-setting-list.dto.js.map +1 -0
- package/dist/setting/setting.controller.d.ts +19 -0
- package/dist/setting/setting.controller.d.ts.map +1 -1
- package/dist/setting/setting.controller.js +12 -0
- package/dist/setting/setting.controller.js.map +1 -1
- package/dist/setting/setting.service.d.ts +19 -0
- package/dist/setting/setting.service.d.ts.map +1 -1
- package/dist/setting/setting.service.js +79 -0
- package/dist/setting/setting.service.js.map +1 -1
- package/dist/task/task.service.d.ts +12 -0
- package/dist/task/task.service.d.ts.map +1 -1
- package/dist/task/task.service.js +60 -0
- package/dist/task/task.service.js.map +1 -1
- package/dist/user/user.controller.d.ts +2 -2
- package/dist/user/user.service.d.ts +6 -6
- package/dist/webhook-integration/webhook-integration.service.d.ts +4 -0
- package/dist/webhook-integration/webhook-integration.service.d.ts.map +1 -1
- package/dist/webhook-integration/webhook-integration.service.js +184 -34
- package/dist/webhook-integration/webhook-integration.service.js.map +1 -1
- package/dist/webhook-integration/webhook-utils.d.ts +1 -0
- package/dist/webhook-integration/webhook-utils.d.ts.map +1 -1
- package/dist/webhook-integration/webhook-utils.js +38 -0
- package/dist/webhook-integration/webhook-utils.js.map +1 -1
- package/hedhog/data/menu.yaml +48 -4
- package/hedhog/data/route.yaml +133 -0
- package/hedhog/data/setting_group.yaml +45 -0
- package/hedhog/data/setting_subgroup.yaml +16 -0
- package/hedhog/frontend/app/account/components/mcp-tokens.tsx.ejs +285 -0
- package/hedhog/frontend/app/account/layout.tsx.ejs +32 -55
- package/hedhog/frontend/app/account/mcp-tokens/page.tsx.ejs +5 -0
- package/hedhog/frontend/app/integrations-webhooks/page.tsx.ejs +2098 -1835
- package/hedhog/frontend/app/mcp_chat/chat-components.tsx.ejs +595 -0
- package/hedhog/frontend/app/mcp_chat/chat-types.ts.ejs +16 -0
- package/hedhog/frontend/app/mcp_chat/page.tsx.ejs +554 -383
- package/hedhog/frontend/app/setting-list/page.tsx.ejs +649 -0
- package/hedhog/frontend/app/users/page.tsx.ejs +445 -203
- package/hedhog/frontend/app/users/user-sheet.tsx.ejs +16 -37
- package/hedhog/frontend/messages/en.json +241 -2
- package/hedhog/frontend/messages/pt.json +241 -2
- package/hedhog/table/mcp_api_key.yaml +30 -0
- package/hedhog/table/notification.yaml +72 -0
- package/hedhog/table/webhook_action_log.yaml +49 -0
- package/package.json +4 -4
- package/src/core.module.ts +3 -0
- package/src/index.ts +5 -0
- package/src/mcp/guards/mcp-auth.guard.ts +64 -32
- package/src/mcp/mcp-api-key.controller.ts +42 -0
- package/src/mcp/mcp-api-key.service.ts +66 -0
- package/src/mcp/mcp-registry.service.ts +75 -69
- package/src/mcp/mcp.module.ts +43 -36
- package/src/mcp-chat/mcp-chat.controller.ts +113 -60
- package/src/mcp-chat/mcp-chat.service.ts +494 -332
- package/src/notification/dto/create-notification.dto.ts +71 -0
- package/src/notification/dto/update-progress.dto.ts +16 -0
- package/src/notification/notification.controller.ts +71 -0
- package/src/notification/notification.module.ts +16 -0
- package/src/notification/notification.service.ts +124 -0
- package/src/setting/dto/update-setting-list.dto.ts +32 -0
- package/src/setting/setting.controller.ts +9 -0
- package/src/setting/setting.service.ts +91 -0
- package/src/task/task.service.ts +72 -0
- package/src/webhook-integration/webhook-integration.service.ts +934 -735
- package/src/webhook-integration/webhook-utils.ts +136 -79
|
@@ -1,332 +1,494 @@
|
|
|
1
|
-
import { PrismaService } from '@hed-hog/api-prisma';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
import
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
| { role: '
|
|
21
|
-
| { role: '
|
|
22
|
-
| { role: '
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
content:
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
private async
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
);
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
}
|
|
1
|
+
import { PrismaService } from '@hed-hog/api-prisma';
|
|
2
|
+
import {
|
|
3
|
+
BadRequestException,
|
|
4
|
+
ForbiddenException,
|
|
5
|
+
forwardRef,
|
|
6
|
+
HttpException,
|
|
7
|
+
HttpStatus,
|
|
8
|
+
Inject,
|
|
9
|
+
Injectable,
|
|
10
|
+
Logger,
|
|
11
|
+
NotFoundException,
|
|
12
|
+
} from '@nestjs/common';
|
|
13
|
+
import axios from 'axios';
|
|
14
|
+
import { McpRegistryService } from '../mcp/mcp-registry.service';
|
|
15
|
+
import { McpContext } from '../mcp/types/mcp-context.type';
|
|
16
|
+
import { SettingService } from '../setting/setting.service';
|
|
17
|
+
import { CreateConversationDTO } from './dto/create-conversation.dto';
|
|
18
|
+
|
|
19
|
+
type OpenAiMessage =
|
|
20
|
+
| { role: 'system'; content: string }
|
|
21
|
+
| { role: 'user'; content: string }
|
|
22
|
+
| { role: 'assistant'; content: string | null; tool_calls?: OpenAiToolCall[] }
|
|
23
|
+
| { role: 'tool'; tool_call_id: string; content: string };
|
|
24
|
+
|
|
25
|
+
interface OpenAiToolCall {
|
|
26
|
+
id: string;
|
|
27
|
+
type: 'function';
|
|
28
|
+
function: { name: string; arguments: string };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface OpenAiToolDefinition {
|
|
32
|
+
type: 'function';
|
|
33
|
+
function: {
|
|
34
|
+
name: string;
|
|
35
|
+
description: string;
|
|
36
|
+
parameters: Record<string, any>;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface ScoredTool {
|
|
41
|
+
name: string;
|
|
42
|
+
readOnly: boolean;
|
|
43
|
+
priorityBucket: number;
|
|
44
|
+
keywordBoost: number;
|
|
45
|
+
index: number;
|
|
46
|
+
tool: OpenAiToolDefinition;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const MAX_OPENAI_TOOLS = 128;
|
|
50
|
+
|
|
51
|
+
const SYSTEM_PROMPT =
|
|
52
|
+
'You are a helpful assistant with access to tools from the HedHog system. ' +
|
|
53
|
+
'Use the available tools to answer the user\'s questions accurately. ' +
|
|
54
|
+
'Always respond in the same language the user writes in.';
|
|
55
|
+
|
|
56
|
+
const sanitize = (n: string) => n.replace(/\./g, '__');
|
|
57
|
+
const restore = (n: string) => n.replace(/__/g, '.');
|
|
58
|
+
|
|
59
|
+
@Injectable()
|
|
60
|
+
export class McpChatService {
|
|
61
|
+
private readonly logger = new Logger(McpChatService.name);
|
|
62
|
+
|
|
63
|
+
constructor(
|
|
64
|
+
@Inject(forwardRef(() => PrismaService))
|
|
65
|
+
private readonly prisma: PrismaService,
|
|
66
|
+
@Inject(forwardRef(() => SettingService))
|
|
67
|
+
private readonly setting: SettingService,
|
|
68
|
+
@Inject(forwardRef(() => McpRegistryService))
|
|
69
|
+
private readonly registry: McpRegistryService,
|
|
70
|
+
) {}
|
|
71
|
+
|
|
72
|
+
async listConversations(userId: number) {
|
|
73
|
+
return this.prisma.mcp_conversation.findMany({
|
|
74
|
+
where: { user_id: userId },
|
|
75
|
+
orderBy: { created_at: 'desc' },
|
|
76
|
+
select: { id: true, title: true, created_at: true, updated_at: true },
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async createConversation(userId: number, dto: CreateConversationDTO) {
|
|
81
|
+
return this.prisma.mcp_conversation.create({
|
|
82
|
+
data: {
|
|
83
|
+
user_id: userId,
|
|
84
|
+
title: dto.title ?? null,
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async deleteConversations(userId: number, ids: number[]) {
|
|
90
|
+
await this.prisma.mcp_conversation.deleteMany({
|
|
91
|
+
where: { id: { in: ids }, user_id: userId },
|
|
92
|
+
});
|
|
93
|
+
return { count: ids.length };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async getMessages(conversationId: number, userId: number) {
|
|
97
|
+
await this.assertOwnership(conversationId, userId);
|
|
98
|
+
return this.prisma.mcp_conversation_message.findMany({
|
|
99
|
+
where: { conversation_id: conversationId },
|
|
100
|
+
orderBy: { created_at: 'asc' },
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async sendMessage(
|
|
105
|
+
conversationId: number,
|
|
106
|
+
userId: number,
|
|
107
|
+
locale: string,
|
|
108
|
+
message: string,
|
|
109
|
+
) {
|
|
110
|
+
await this.assertOwnership(conversationId, userId);
|
|
111
|
+
|
|
112
|
+
const apiKey = await this.getOpenAiKey();
|
|
113
|
+
|
|
114
|
+
// Save user message
|
|
115
|
+
await this.prisma.mcp_conversation_message.create({
|
|
116
|
+
data: {
|
|
117
|
+
conversation_id: conversationId,
|
|
118
|
+
role: 'user',
|
|
119
|
+
content: message,
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Auto-set title on first message
|
|
124
|
+
const conv = await this.prisma.mcp_conversation.findUnique({
|
|
125
|
+
where: { id: conversationId },
|
|
126
|
+
});
|
|
127
|
+
if (!conv!.title) {
|
|
128
|
+
await this.prisma.mcp_conversation.update({
|
|
129
|
+
where: { id: conversationId },
|
|
130
|
+
data: { title: message.slice(0, 60) },
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Build OpenAI messages from history
|
|
135
|
+
const history = await this.prisma.mcp_conversation_message.findMany({
|
|
136
|
+
where: { conversation_id: conversationId },
|
|
137
|
+
orderBy: { created_at: 'asc' },
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const openAiMessages: OpenAiMessage[] = [
|
|
141
|
+
{ role: 'system', content: SYSTEM_PROMPT },
|
|
142
|
+
...this.toOpenAiMessages(history),
|
|
143
|
+
];
|
|
144
|
+
|
|
145
|
+
// Build tool definitions
|
|
146
|
+
const tools = this.buildOpenAiTools(message, conversationId, userId);
|
|
147
|
+
|
|
148
|
+
// Tool-calling loop
|
|
149
|
+
let maxIterations = 10;
|
|
150
|
+
while (maxIterations-- > 0) {
|
|
151
|
+
let response: any;
|
|
152
|
+
try {
|
|
153
|
+
response = await axios.post(
|
|
154
|
+
'https://api.openai.com/v1/chat/completions',
|
|
155
|
+
{
|
|
156
|
+
model: 'gpt-4o-mini',
|
|
157
|
+
messages: openAiMessages,
|
|
158
|
+
tools: tools.length > 0 ? tools : undefined,
|
|
159
|
+
tool_choice: tools.length > 0 ? 'auto' : undefined,
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
headers: {
|
|
163
|
+
Authorization: `Bearer ${apiKey}`,
|
|
164
|
+
'Content-Type': 'application/json',
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
);
|
|
168
|
+
} catch (err: any) {
|
|
169
|
+
const status = err?.response?.status;
|
|
170
|
+
const errBody = err?.response?.data?.error;
|
|
171
|
+
if (status === 429) {
|
|
172
|
+
throw new HttpException(
|
|
173
|
+
errBody?.message ?? 'OpenAI rate limit exceeded. Please try again later.',
|
|
174
|
+
HttpStatus.TOO_MANY_REQUESTS,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
if (status === 401) {
|
|
178
|
+
throw new BadRequestException('Invalid OpenAI API key.');
|
|
179
|
+
}
|
|
180
|
+
throw new HttpException(
|
|
181
|
+
errBody?.message ?? 'OpenAI API error.',
|
|
182
|
+
status ?? HttpStatus.BAD_GATEWAY,
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const choice = response.data?.choices?.[0];
|
|
187
|
+
const assistantMsg = choice?.message;
|
|
188
|
+
|
|
189
|
+
if (assistantMsg?.tool_calls?.length) {
|
|
190
|
+
// Add assistant tool-call message to context
|
|
191
|
+
openAiMessages.push({
|
|
192
|
+
role: 'assistant',
|
|
193
|
+
content: null,
|
|
194
|
+
tool_calls: assistantMsg.tool_calls,
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// Execute each tool call
|
|
198
|
+
for (const toolCall of assistantMsg.tool_calls as OpenAiToolCall[]) {
|
|
199
|
+
const originalName = restore(toolCall.function.name);
|
|
200
|
+
const argsJson = toolCall.function.arguments;
|
|
201
|
+
let args: Record<string, any> = {};
|
|
202
|
+
try {
|
|
203
|
+
args = JSON.parse(argsJson);
|
|
204
|
+
} catch {
|
|
205
|
+
args = {};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Persist tool_call message
|
|
209
|
+
await this.prisma.mcp_conversation_message.create({
|
|
210
|
+
data: {
|
|
211
|
+
conversation_id: conversationId,
|
|
212
|
+
role: 'tool_call',
|
|
213
|
+
content: argsJson,
|
|
214
|
+
tool_name: originalName,
|
|
215
|
+
tool_call_id: toolCall.id,
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// Execute the tool
|
|
220
|
+
let resultContent: string;
|
|
221
|
+
const entry = this.registry.get(originalName);
|
|
222
|
+
if (entry) {
|
|
223
|
+
const context: McpContext = {
|
|
224
|
+
userId,
|
|
225
|
+
sessionId: '',
|
|
226
|
+
locale,
|
|
227
|
+
toolName: originalName,
|
|
228
|
+
};
|
|
229
|
+
try {
|
|
230
|
+
const result = await entry.handler(args, context);
|
|
231
|
+
resultContent = JSON.stringify(result);
|
|
232
|
+
} catch (e: any) {
|
|
233
|
+
resultContent = JSON.stringify({ error: e.message });
|
|
234
|
+
}
|
|
235
|
+
} else {
|
|
236
|
+
resultContent = JSON.stringify({ error: `Tool '${originalName}' not found` });
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Persist tool_result message
|
|
240
|
+
await this.prisma.mcp_conversation_message.create({
|
|
241
|
+
data: {
|
|
242
|
+
conversation_id: conversationId,
|
|
243
|
+
role: 'tool_result',
|
|
244
|
+
content: resultContent,
|
|
245
|
+
tool_name: originalName,
|
|
246
|
+
tool_call_id: toolCall.id,
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// Add tool result to context for next iteration
|
|
251
|
+
openAiMessages.push({
|
|
252
|
+
role: 'tool',
|
|
253
|
+
tool_call_id: toolCall.id,
|
|
254
|
+
content: resultContent,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
continue; // next iteration with tool results
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Final assistant response
|
|
262
|
+
const finalContent: string = assistantMsg?.content || '';
|
|
263
|
+
await this.prisma.mcp_conversation_message.create({
|
|
264
|
+
data: {
|
|
265
|
+
conversation_id: conversationId,
|
|
266
|
+
role: 'assistant',
|
|
267
|
+
content: finalContent,
|
|
268
|
+
},
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
return this.prisma.mcp_conversation_message.findMany({
|
|
272
|
+
where: { conversation_id: conversationId },
|
|
273
|
+
orderBy: { created_at: 'asc' },
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
throw new BadRequestException('Maximum tool call iterations exceeded.');
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
private toOpenAiMessages(
|
|
281
|
+
history: Array<{
|
|
282
|
+
role: string;
|
|
283
|
+
content: string;
|
|
284
|
+
tool_name: string | null;
|
|
285
|
+
tool_call_id: string | null;
|
|
286
|
+
}>,
|
|
287
|
+
): OpenAiMessage[] {
|
|
288
|
+
const messages: OpenAiMessage[] = [];
|
|
289
|
+
const toolCallMap = new Map<string, string>(); // tool_call_id → sanitized name
|
|
290
|
+
|
|
291
|
+
for (const msg of history) {
|
|
292
|
+
if (msg.role === 'user') {
|
|
293
|
+
messages.push({ role: 'user', content: msg.content });
|
|
294
|
+
} else if (msg.role === 'assistant') {
|
|
295
|
+
messages.push({ role: 'assistant', content: msg.content });
|
|
296
|
+
} else if (msg.role === 'tool_call' && msg.tool_call_id && msg.tool_name) {
|
|
297
|
+
const sanitizedName = sanitize(msg.tool_name);
|
|
298
|
+
toolCallMap.set(msg.tool_call_id, sanitizedName);
|
|
299
|
+
messages.push({
|
|
300
|
+
role: 'assistant',
|
|
301
|
+
content: null,
|
|
302
|
+
tool_calls: [
|
|
303
|
+
{
|
|
304
|
+
id: msg.tool_call_id,
|
|
305
|
+
type: 'function',
|
|
306
|
+
function: { name: sanitizedName, arguments: msg.content },
|
|
307
|
+
},
|
|
308
|
+
],
|
|
309
|
+
});
|
|
310
|
+
} else if (msg.role === 'tool_result' && msg.tool_call_id) {
|
|
311
|
+
messages.push({
|
|
312
|
+
role: 'tool',
|
|
313
|
+
tool_call_id: msg.tool_call_id,
|
|
314
|
+
content: msg.content,
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return messages;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
private async assertOwnership(conversationId: number, userId: number) {
|
|
323
|
+
const conv = await this.prisma.mcp_conversation.findUnique({
|
|
324
|
+
where: { id: conversationId },
|
|
325
|
+
select: { user_id: true },
|
|
326
|
+
});
|
|
327
|
+
if (!conv) {
|
|
328
|
+
throw new NotFoundException(`Conversation ${conversationId} not found.`);
|
|
329
|
+
}
|
|
330
|
+
if (conv.user_id !== userId) {
|
|
331
|
+
throw new ForbiddenException('Access denied to this conversation.');
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
private async getOpenAiKey(): Promise<string> {
|
|
336
|
+
const settings = await this.setting.getSettingValues(['ai-openai-api-key']);
|
|
337
|
+
const key = settings['ai-openai-api-key'];
|
|
338
|
+
if (!key) {
|
|
339
|
+
throw new BadRequestException(
|
|
340
|
+
'OpenAI API key is not configured (setting "ai-openai-api-key").',
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
return key;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
private buildOpenAiTools(
|
|
347
|
+
message: string,
|
|
348
|
+
conversationId: number,
|
|
349
|
+
userId: number,
|
|
350
|
+
): OpenAiToolDefinition[] {
|
|
351
|
+
const allEntries = this.registry.getAll();
|
|
352
|
+
|
|
353
|
+
const dedupedBySanitized = new Map<string, ScoredTool>();
|
|
354
|
+
const excludedDuplicates: string[] = [];
|
|
355
|
+
const normalizedMessage = message.toLowerCase();
|
|
356
|
+
|
|
357
|
+
for (const [index, entry] of allEntries.entries()) {
|
|
358
|
+
const originalName = entry.definition.name;
|
|
359
|
+
const sanitizedName = sanitize(originalName);
|
|
360
|
+
const readOnly = Boolean(entry.definition.readOnly);
|
|
361
|
+
|
|
362
|
+
if (dedupedBySanitized.has(sanitizedName)) {
|
|
363
|
+
excludedDuplicates.push(originalName);
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const scored: ScoredTool = {
|
|
368
|
+
name: originalName,
|
|
369
|
+
readOnly,
|
|
370
|
+
priorityBucket: this.getDomainPriorityBucket(originalName),
|
|
371
|
+
keywordBoost: this.getKeywordBoost(normalizedMessage, originalName),
|
|
372
|
+
index,
|
|
373
|
+
tool: {
|
|
374
|
+
type: 'function',
|
|
375
|
+
function: {
|
|
376
|
+
name: sanitizedName,
|
|
377
|
+
description: entry.definition.description,
|
|
378
|
+
parameters: entry.definition.inputSchema ?? {
|
|
379
|
+
type: 'object',
|
|
380
|
+
properties: {},
|
|
381
|
+
},
|
|
382
|
+
},
|
|
383
|
+
},
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
dedupedBySanitized.set(sanitizedName, scored);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const scoredTools = Array.from(dedupedBySanitized.values());
|
|
390
|
+
scoredTools.sort((a, b) => {
|
|
391
|
+
if (a.priorityBucket !== b.priorityBucket) {
|
|
392
|
+
return a.priorityBucket - b.priorityBucket;
|
|
393
|
+
}
|
|
394
|
+
if (a.keywordBoost !== b.keywordBoost) {
|
|
395
|
+
return b.keywordBoost - a.keywordBoost;
|
|
396
|
+
}
|
|
397
|
+
if (a.readOnly !== b.readOnly) {
|
|
398
|
+
return a.readOnly ? -1 : 1;
|
|
399
|
+
}
|
|
400
|
+
return a.index - b.index;
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
const selected = scoredTools.slice(0, MAX_OPENAI_TOOLS);
|
|
404
|
+
const excludedByLimit = scoredTools
|
|
405
|
+
.slice(MAX_OPENAI_TOOLS)
|
|
406
|
+
.map((tool) => tool.name);
|
|
407
|
+
|
|
408
|
+
if (excludedDuplicates.length > 0) {
|
|
409
|
+
this.logger.warn(
|
|
410
|
+
`MCP tools duplicate sanitize names ignored: ${excludedDuplicates.length}. ` +
|
|
411
|
+
`Examples: ${excludedDuplicates.slice(0, 10).join(', ')}`,
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (excludedByLimit.length > 0) {
|
|
416
|
+
this.logger.warn(
|
|
417
|
+
`MCP tools truncated for OpenAI limit (${MAX_OPENAI_TOOLS}). ` +
|
|
418
|
+
`conversationId=${conversationId} userId=${userId} ` +
|
|
419
|
+
`available=${scoredTools.length} selected=${selected.length} excluded=${excludedByLimit.length}. ` +
|
|
420
|
+
`Top excluded: ${excludedByLimit.slice(0, 12).join(', ')}`,
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
this.logger.log(
|
|
425
|
+
`MCP tools prepared: conversationId=${conversationId} userId=${userId} ` +
|
|
426
|
+
`raw=${allEntries.length} deduped=${scoredTools.length} selected=${selected.length}`,
|
|
427
|
+
);
|
|
428
|
+
|
|
429
|
+
return selected.map((entry) => entry.tool);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
private getDomainPriorityBucket(toolName: string): number {
|
|
433
|
+
if (toolName.startsWith('operations.')) return 0;
|
|
434
|
+
if (toolName.startsWith('lms.')) return 1;
|
|
435
|
+
if (toolName.startsWith('core.')) return 2;
|
|
436
|
+
return 3;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
private getKeywordBoost(message: string, toolName: string): number {
|
|
440
|
+
const terms = this.getMessageKeywordSet(message);
|
|
441
|
+
if (terms.size === 0) {
|
|
442
|
+
return 0;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
let boost = 0;
|
|
446
|
+
if (
|
|
447
|
+
terms.has('task') &&
|
|
448
|
+
(toolName.includes('.task') || toolName.includes('.tasks.'))
|
|
449
|
+
) {
|
|
450
|
+
boost += 4;
|
|
451
|
+
}
|
|
452
|
+
if (
|
|
453
|
+
terms.has('project') &&
|
|
454
|
+
(toolName.includes('.project') || toolName.includes('.projects.'))
|
|
455
|
+
) {
|
|
456
|
+
boost += 3;
|
|
457
|
+
}
|
|
458
|
+
if (
|
|
459
|
+
terms.has('class') &&
|
|
460
|
+
(toolName.includes('.class') ||
|
|
461
|
+
toolName.includes('.course') ||
|
|
462
|
+
toolName.includes('.training'))
|
|
463
|
+
) {
|
|
464
|
+
boost += 3;
|
|
465
|
+
}
|
|
466
|
+
if (
|
|
467
|
+
terms.has('pending') &&
|
|
468
|
+
(toolName.includes('.task') ||
|
|
469
|
+
toolName.includes('.approval') ||
|
|
470
|
+
toolName.includes('.time-off'))
|
|
471
|
+
) {
|
|
472
|
+
boost += 2;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
return boost;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
private getMessageKeywordSet(message: string): Set<string> {
|
|
479
|
+
const keywords = new Set<string>();
|
|
480
|
+
if (/(tarefa|tarefas|task|tasks)/i.test(message)) {
|
|
481
|
+
keywords.add('task');
|
|
482
|
+
}
|
|
483
|
+
if (/(projeto|projetos|project|projects)/i.test(message)) {
|
|
484
|
+
keywords.add('project');
|
|
485
|
+
}
|
|
486
|
+
if (/(turma|class|curso|cursos|course|training)/i.test(message)) {
|
|
487
|
+
keywords.add('class');
|
|
488
|
+
}
|
|
489
|
+
if (/(pendente|pendentes|pending|open|aberta|abertas)/i.test(message)) {
|
|
490
|
+
keywords.add('pending');
|
|
491
|
+
}
|
|
492
|
+
return keywords;
|
|
493
|
+
}
|
|
494
|
+
}
|