@hed-hog/core 0.0.222 → 0.0.232
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/ai/ai.controller.d.ts +90 -0
- package/dist/ai/ai.controller.d.ts.map +1 -0
- package/dist/ai/ai.controller.js +119 -0
- package/dist/ai/ai.controller.js.map +1 -0
- package/dist/ai/ai.module.d.ts +3 -0
- package/dist/ai/ai.module.d.ts.map +1 -0
- package/dist/ai/ai.module.js +26 -0
- package/dist/ai/ai.module.js.map +1 -0
- package/dist/ai/ai.service.d.ts +69 -0
- package/dist/ai/ai.service.d.ts.map +1 -0
- package/dist/ai/ai.service.js +394 -0
- package/dist/ai/ai.service.js.map +1 -0
- package/dist/ai/dto/chat-agent.dto.d.ts +4 -0
- package/dist/ai/dto/chat-agent.dto.d.ts.map +1 -0
- package/dist/ai/dto/chat-agent.dto.js +22 -0
- package/dist/ai/dto/chat-agent.dto.js.map +1 -0
- package/dist/ai/dto/chat.dto.d.ts +7 -0
- package/dist/ai/dto/chat.dto.d.ts.map +1 -0
- package/dist/ai/dto/chat.dto.js +38 -0
- package/dist/ai/dto/chat.dto.js.map +1 -0
- package/dist/ai/dto/create-agent.dto.d.ts +7 -0
- package/dist/ai/dto/create-agent.dto.d.ts.map +1 -0
- package/dist/ai/dto/create-agent.dto.js +38 -0
- package/dist/ai/dto/create-agent.dto.js.map +1 -0
- package/dist/ai/dto/update-agent.dto.d.ts +7 -0
- package/dist/ai/dto/update-agent.dto.d.ts.map +1 -0
- package/dist/ai/dto/update-agent.dto.js +38 -0
- package/dist/ai/dto/update-agent.dto.js.map +1 -0
- package/dist/auth/auth.controller.d.ts +2 -2
- package/dist/auth/auth.service.d.ts +7 -7
- 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/file/file.service.d.ts +2 -0
- package/dist/file/file.service.d.ts.map +1 -1
- package/dist/file/file.service.js +17 -2
- package/dist/file/file.service.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/session/session.controller.d.ts +1 -1
- package/dist/session/session.service.d.ts +3 -3
- package/dist/user/user.controller.d.ts +1 -1
- package/dist/user/user.service.d.ts +3 -3
- package/hedhog/data/menu.yaml +17 -3
- package/hedhog/data/route.yaml +64 -0
- package/hedhog/data/setting_group.yaml +33 -1
- package/hedhog/frontend/app/ai_agent/page.tsx.ejs +468 -0
- package/hedhog/table/ai_agent.yaml +24 -0
- package/package.json +5 -5
- package/src/ai/ai.controller.ts +61 -0
- package/src/ai/ai.module.ts +13 -0
- package/src/ai/ai.service.ts +546 -0
- package/src/ai/dto/chat-agent.dto.ts +7 -0
- package/src/ai/dto/chat.dto.ts +20 -0
- package/src/ai/dto/create-agent.dto.ts +20 -0
- package/src/ai/dto/update-agent.dto.ts +20 -0
- package/src/core.module.ts +3 -0
- package/src/file/file.service.ts +34 -8
- package/src/index.ts +1 -0
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
import { PaginationDTO } from '@hed-hog/api-pagination';
|
|
2
|
+
import { PrismaService } from '@hed-hog/api-prisma';
|
|
3
|
+
import {
|
|
4
|
+
BadRequestException,
|
|
5
|
+
forwardRef,
|
|
6
|
+
Inject,
|
|
7
|
+
Injectable,
|
|
8
|
+
NotFoundException,
|
|
9
|
+
} from '@nestjs/common';
|
|
10
|
+
import { Prisma } from '@prisma/client';
|
|
11
|
+
import axios from 'axios';
|
|
12
|
+
import { DeleteDTO } from '../dto/delete.dto';
|
|
13
|
+
import { SettingService } from '../setting/setting.service';
|
|
14
|
+
import { ChatAgentDTO } from './dto/chat-agent.dto';
|
|
15
|
+
import { ChatDTO } from './dto/chat.dto';
|
|
16
|
+
import { CreateAgentDTO } from './dto/create-agent.dto';
|
|
17
|
+
import { UpdateAgentDTO } from './dto/update-agent.dto';
|
|
18
|
+
|
|
19
|
+
type AiProvider = 'openai' | 'gemini';
|
|
20
|
+
|
|
21
|
+
type AiAgentRecord = {
|
|
22
|
+
id: number;
|
|
23
|
+
slug: string;
|
|
24
|
+
provider: AiProvider;
|
|
25
|
+
model: string | null;
|
|
26
|
+
instructions: string | null;
|
|
27
|
+
external_agent_id: string | null;
|
|
28
|
+
created_at: Date;
|
|
29
|
+
updated_at: Date;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
@Injectable()
|
|
33
|
+
export class AiService {
|
|
34
|
+
constructor(
|
|
35
|
+
@Inject(forwardRef(() => PrismaService))
|
|
36
|
+
private readonly prismaService: PrismaService,
|
|
37
|
+
@Inject(forwardRef(() => SettingService))
|
|
38
|
+
private readonly settingService: SettingService,
|
|
39
|
+
) {}
|
|
40
|
+
|
|
41
|
+
async chat(data: ChatDTO) {
|
|
42
|
+
const provider = data.provider || 'openai';
|
|
43
|
+
|
|
44
|
+
if (provider === 'openai') {
|
|
45
|
+
const model = data.model || 'gpt-4o-mini';
|
|
46
|
+
const content = await this.chatWithOpenAi({
|
|
47
|
+
message: data.message,
|
|
48
|
+
model,
|
|
49
|
+
systemPrompt: data.systemPrompt,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
provider,
|
|
54
|
+
model,
|
|
55
|
+
content,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const model = data.model || 'gemini-1.5-flash';
|
|
60
|
+
const content = await this.chatWithGemini({
|
|
61
|
+
message: data.message,
|
|
62
|
+
model,
|
|
63
|
+
systemPrompt: data.systemPrompt,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
provider,
|
|
68
|
+
model,
|
|
69
|
+
content,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async createAgent(data: CreateAgentDTO) {
|
|
74
|
+
const slug = data.slug.trim();
|
|
75
|
+
const provider = data.provider || 'openai';
|
|
76
|
+
const model = data.model || this.getDefaultModel(provider);
|
|
77
|
+
const instructions = data.instructions?.trim() || null;
|
|
78
|
+
|
|
79
|
+
const existing = await this.findAgentBySlug(slug);
|
|
80
|
+
if (existing) {
|
|
81
|
+
return existing;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let externalAgentId: string | null = null;
|
|
85
|
+
|
|
86
|
+
if (provider === 'openai') {
|
|
87
|
+
externalAgentId = await this.createOpenAiAssistant({
|
|
88
|
+
slug,
|
|
89
|
+
model,
|
|
90
|
+
instructions,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
await this.prismaService.$executeRaw`
|
|
95
|
+
INSERT INTO ai_agent (slug, provider, model, instructions, external_agent_id)
|
|
96
|
+
VALUES (${slug}, ${provider}, ${model}, ${instructions}, ${externalAgentId})
|
|
97
|
+
`;
|
|
98
|
+
|
|
99
|
+
return this.findAgentBySlug(slug);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async listAgents(paginationParams: PaginationDTO) {
|
|
103
|
+
const page = Math.max(Number(paginationParams?.page || 1), 1);
|
|
104
|
+
const pageSize = Math.min(
|
|
105
|
+
Math.max(Number(paginationParams?.pageSize || 10), 1),
|
|
106
|
+
100,
|
|
107
|
+
);
|
|
108
|
+
const offset = (page - 1) * pageSize;
|
|
109
|
+
const search = String(paginationParams?.search || '').trim();
|
|
110
|
+
|
|
111
|
+
const whereClause = search
|
|
112
|
+
? Prisma.sql`WHERE LOWER(slug) LIKE LOWER(${`%${search}%`})`
|
|
113
|
+
: Prisma.empty;
|
|
114
|
+
|
|
115
|
+
const data = await this.prismaService.$queryRaw<AiAgentRecord[]>(Prisma.sql`
|
|
116
|
+
SELECT id, slug, provider, model, instructions, external_agent_id, created_at, updated_at
|
|
117
|
+
FROM ai_agent
|
|
118
|
+
${whereClause}
|
|
119
|
+
ORDER BY id DESC
|
|
120
|
+
LIMIT ${pageSize}
|
|
121
|
+
OFFSET ${offset}
|
|
122
|
+
`);
|
|
123
|
+
|
|
124
|
+
const totalResult = await this.prismaService.$queryRaw<Array<{ total: bigint | number | string }>>(Prisma.sql`
|
|
125
|
+
SELECT COUNT(*) AS total
|
|
126
|
+
FROM ai_agent
|
|
127
|
+
${whereClause}
|
|
128
|
+
`);
|
|
129
|
+
|
|
130
|
+
const total = Number(totalResult?.[0]?.total || 0);
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
data,
|
|
134
|
+
total,
|
|
135
|
+
page,
|
|
136
|
+
pageSize,
|
|
137
|
+
totalPages: Math.ceil(total / pageSize),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async getAgentById(agentId: number) {
|
|
142
|
+
const agent = await this.findAgentById(agentId);
|
|
143
|
+
|
|
144
|
+
if (!agent) {
|
|
145
|
+
throw new NotFoundException(`AI agent with id "${agentId}" was not found.`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return agent;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async updateAgent(agentId: number, data: UpdateAgentDTO) {
|
|
152
|
+
const currentAgent = await this.findAgentById(agentId);
|
|
153
|
+
|
|
154
|
+
if (!currentAgent) {
|
|
155
|
+
throw new NotFoundException(`AI agent with id "${agentId}" was not found.`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const slug = data.slug?.trim() ?? currentAgent.slug;
|
|
159
|
+
const provider = data.provider ?? currentAgent.provider;
|
|
160
|
+
const model = data.model ?? currentAgent.model ?? this.getDefaultModel(provider);
|
|
161
|
+
const instructions = data.instructions ?? currentAgent.instructions;
|
|
162
|
+
|
|
163
|
+
if (slug !== currentAgent.slug) {
|
|
164
|
+
const existingSlug = await this.findAgentBySlug(slug);
|
|
165
|
+
if (existingSlug && existingSlug.id !== agentId) {
|
|
166
|
+
throw new BadRequestException(`AI agent with slug "${slug}" already exists.`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
let externalAgentId = currentAgent.external_agent_id;
|
|
171
|
+
|
|
172
|
+
if (provider !== 'openai') {
|
|
173
|
+
externalAgentId = null;
|
|
174
|
+
} else if (!externalAgentId) {
|
|
175
|
+
externalAgentId = await this.createOpenAiAssistant({
|
|
176
|
+
slug,
|
|
177
|
+
model,
|
|
178
|
+
instructions,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
await this.prismaService.$executeRaw`
|
|
183
|
+
UPDATE ai_agent
|
|
184
|
+
SET slug = ${slug},
|
|
185
|
+
provider = ${provider},
|
|
186
|
+
model = ${model},
|
|
187
|
+
instructions = ${instructions},
|
|
188
|
+
external_agent_id = ${externalAgentId},
|
|
189
|
+
updated_at = NOW()
|
|
190
|
+
WHERE id = ${agentId}
|
|
191
|
+
`;
|
|
192
|
+
|
|
193
|
+
return this.getAgentById(agentId);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async deleteAgents({ ids }: DeleteDTO) {
|
|
197
|
+
const existing = await this.prismaService.$queryRaw<Array<{ id: number }>>(Prisma.sql`
|
|
198
|
+
SELECT id
|
|
199
|
+
FROM ai_agent
|
|
200
|
+
WHERE id IN (${Prisma.join(ids)})
|
|
201
|
+
`);
|
|
202
|
+
|
|
203
|
+
if (existing.length !== ids.length) {
|
|
204
|
+
const existingIds = existing.map((item) => item.id);
|
|
205
|
+
const missingIds = ids.filter((id) => !existingIds.includes(id));
|
|
206
|
+
throw new NotFoundException(
|
|
207
|
+
`AI agents with ids "${missingIds.join(', ')}" were not found.`,
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const deletedCount = await this.prismaService.$executeRaw`
|
|
212
|
+
DELETE FROM ai_agent
|
|
213
|
+
WHERE id IN (${Prisma.join(ids)})
|
|
214
|
+
`;
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
count: Number(deletedCount || 0),
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async chatWithAgent(slug: string, data: ChatAgentDTO) {
|
|
222
|
+
const agent = await this.findAgentBySlug(slug);
|
|
223
|
+
|
|
224
|
+
if (!agent) {
|
|
225
|
+
throw new NotFoundException(`AI agent with slug "${slug}" was not found.`);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (agent.provider === 'openai') {
|
|
229
|
+
if (agent.external_agent_id) {
|
|
230
|
+
const content = await this.chatWithOpenAiAssistant(
|
|
231
|
+
agent.external_agent_id,
|
|
232
|
+
data.message,
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
slug: agent.slug,
|
|
237
|
+
provider: agent.provider,
|
|
238
|
+
model: agent.model,
|
|
239
|
+
content,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const content = await this.chatWithOpenAi({
|
|
244
|
+
message: data.message,
|
|
245
|
+
model: agent.model || this.getDefaultModel('openai'),
|
|
246
|
+
systemPrompt: agent.instructions || undefined,
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
slug: agent.slug,
|
|
251
|
+
provider: agent.provider,
|
|
252
|
+
model: agent.model,
|
|
253
|
+
content,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const content = await this.chatWithGemini({
|
|
258
|
+
message: data.message,
|
|
259
|
+
model: agent.model || this.getDefaultModel('gemini'),
|
|
260
|
+
systemPrompt: agent.instructions || undefined,
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
slug: agent.slug,
|
|
265
|
+
provider: agent.provider,
|
|
266
|
+
model: agent.model,
|
|
267
|
+
content,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async getAgentBySlug(slug: string) {
|
|
272
|
+
const agent = await this.findAgentBySlug(slug);
|
|
273
|
+
|
|
274
|
+
if (!agent) {
|
|
275
|
+
throw new NotFoundException(`AI agent with slug "${slug}" was not found.`);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return agent;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
private async findAgentBySlug(slug: string): Promise<AiAgentRecord | null> {
|
|
282
|
+
const result = await this.prismaService.$queryRaw<AiAgentRecord[]>`
|
|
283
|
+
SELECT id, slug, provider, model, instructions, external_agent_id, created_at, updated_at
|
|
284
|
+
FROM ai_agent
|
|
285
|
+
WHERE slug = ${slug}
|
|
286
|
+
LIMIT 1
|
|
287
|
+
`;
|
|
288
|
+
|
|
289
|
+
return result?.[0] || null;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
private async findAgentById(id: number): Promise<AiAgentRecord | null> {
|
|
293
|
+
const result = await this.prismaService.$queryRaw<AiAgentRecord[]>`
|
|
294
|
+
SELECT id, slug, provider, model, instructions, external_agent_id, created_at, updated_at
|
|
295
|
+
FROM ai_agent
|
|
296
|
+
WHERE id = ${id}
|
|
297
|
+
LIMIT 1
|
|
298
|
+
`;
|
|
299
|
+
|
|
300
|
+
return result?.[0] || null;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
private getDefaultModel(provider: AiProvider): string {
|
|
304
|
+
if (provider === 'openai') {
|
|
305
|
+
return 'gpt-4o-mini';
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return 'gemini-1.5-flash';
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
private async getApiKeys() {
|
|
312
|
+
const settings = await this.settingService.getSettingValues([
|
|
313
|
+
'ai-openai-api-key',
|
|
314
|
+
'ai-gemini-api-key',
|
|
315
|
+
]);
|
|
316
|
+
|
|
317
|
+
return {
|
|
318
|
+
openai: settings['ai-openai-api-key'],
|
|
319
|
+
gemini: settings['ai-gemini-api-key'],
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
private async chatWithOpenAi({
|
|
324
|
+
message,
|
|
325
|
+
model,
|
|
326
|
+
systemPrompt,
|
|
327
|
+
}: {
|
|
328
|
+
message: string;
|
|
329
|
+
model: string;
|
|
330
|
+
systemPrompt?: string;
|
|
331
|
+
}) {
|
|
332
|
+
const { openai } = await this.getApiKeys();
|
|
333
|
+
|
|
334
|
+
if (!openai) {
|
|
335
|
+
throw new BadRequestException(
|
|
336
|
+
'OpenAI API key is not configured (setting "ai-openai-api-key").',
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const messages: Array<{ role: 'system' | 'user'; content: string }> = [];
|
|
341
|
+
|
|
342
|
+
if (systemPrompt) {
|
|
343
|
+
messages.push({ role: 'system', content: systemPrompt });
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
messages.push({ role: 'user', content: message });
|
|
347
|
+
|
|
348
|
+
const response = await axios.post(
|
|
349
|
+
'https://api.openai.com/v1/chat/completions',
|
|
350
|
+
{
|
|
351
|
+
model,
|
|
352
|
+
messages,
|
|
353
|
+
},
|
|
354
|
+
{
|
|
355
|
+
headers: {
|
|
356
|
+
Authorization: `Bearer ${openai}`,
|
|
357
|
+
'Content-Type': 'application/json',
|
|
358
|
+
},
|
|
359
|
+
},
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
return response.data?.choices?.[0]?.message?.content || '';
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
private async createOpenAiAssistant({
|
|
366
|
+
slug,
|
|
367
|
+
model,
|
|
368
|
+
instructions,
|
|
369
|
+
}: {
|
|
370
|
+
slug: string;
|
|
371
|
+
model: string;
|
|
372
|
+
instructions: string | null;
|
|
373
|
+
}): Promise<string> {
|
|
374
|
+
const { openai } = await this.getApiKeys();
|
|
375
|
+
|
|
376
|
+
if (!openai) {
|
|
377
|
+
throw new BadRequestException(
|
|
378
|
+
'OpenAI API key is not configured (setting "ai-openai-api-key").',
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const response = await axios.post(
|
|
383
|
+
'https://api.openai.com/v1/assistants',
|
|
384
|
+
{
|
|
385
|
+
name: slug,
|
|
386
|
+
model,
|
|
387
|
+
instructions: instructions || `You are the assistant identified by slug ${slug}.`,
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
headers: {
|
|
391
|
+
Authorization: `Bearer ${openai}`,
|
|
392
|
+
'Content-Type': 'application/json',
|
|
393
|
+
'OpenAI-Beta': 'assistants=v2',
|
|
394
|
+
},
|
|
395
|
+
},
|
|
396
|
+
);
|
|
397
|
+
|
|
398
|
+
const assistantId = response.data?.id;
|
|
399
|
+
|
|
400
|
+
if (!assistantId) {
|
|
401
|
+
throw new BadRequestException('OpenAI assistant creation failed.');
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return assistantId;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
private async chatWithOpenAiAssistant(
|
|
408
|
+
assistantId: string,
|
|
409
|
+
message: string,
|
|
410
|
+
): Promise<string> {
|
|
411
|
+
const { openai } = await this.getApiKeys();
|
|
412
|
+
|
|
413
|
+
if (!openai) {
|
|
414
|
+
throw new BadRequestException(
|
|
415
|
+
'OpenAI API key is not configured (setting "ai-openai-api-key").',
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const threadResponse = await axios.post(
|
|
420
|
+
'https://api.openai.com/v1/threads',
|
|
421
|
+
{
|
|
422
|
+
messages: [{ role: 'user', content: message }],
|
|
423
|
+
},
|
|
424
|
+
{
|
|
425
|
+
headers: {
|
|
426
|
+
Authorization: `Bearer ${openai}`,
|
|
427
|
+
'Content-Type': 'application/json',
|
|
428
|
+
'OpenAI-Beta': 'assistants=v2',
|
|
429
|
+
},
|
|
430
|
+
},
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
const threadId = threadResponse.data?.id;
|
|
434
|
+
|
|
435
|
+
if (!threadId) {
|
|
436
|
+
throw new BadRequestException('OpenAI thread creation failed.');
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const runResponse = await axios.post(
|
|
440
|
+
`https://api.openai.com/v1/threads/${threadId}/runs`,
|
|
441
|
+
{
|
|
442
|
+
assistant_id: assistantId,
|
|
443
|
+
},
|
|
444
|
+
{
|
|
445
|
+
headers: {
|
|
446
|
+
Authorization: `Bearer ${openai}`,
|
|
447
|
+
'Content-Type': 'application/json',
|
|
448
|
+
'OpenAI-Beta': 'assistants=v2',
|
|
449
|
+
},
|
|
450
|
+
},
|
|
451
|
+
);
|
|
452
|
+
|
|
453
|
+
const runId = runResponse.data?.id;
|
|
454
|
+
|
|
455
|
+
if (!runId) {
|
|
456
|
+
throw new BadRequestException('OpenAI assistant run failed to start.');
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
for (let i = 0; i < 30; i++) {
|
|
460
|
+
const runStatusResponse = await axios.get(
|
|
461
|
+
`https://api.openai.com/v1/threads/${threadId}/runs/${runId}`,
|
|
462
|
+
{
|
|
463
|
+
headers: {
|
|
464
|
+
Authorization: `Bearer ${openai}`,
|
|
465
|
+
'OpenAI-Beta': 'assistants=v2',
|
|
466
|
+
},
|
|
467
|
+
},
|
|
468
|
+
);
|
|
469
|
+
|
|
470
|
+
const status = runStatusResponse.data?.status;
|
|
471
|
+
|
|
472
|
+
if (status === 'completed') {
|
|
473
|
+
break;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (['cancelled', 'failed', 'expired'].includes(status)) {
|
|
477
|
+
throw new BadRequestException(
|
|
478
|
+
`OpenAI assistant run did not complete successfully (status: ${status}).`,
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
await this.sleep(1000);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const messagesResponse = await axios.get(
|
|
486
|
+
`https://api.openai.com/v1/threads/${threadId}/messages`,
|
|
487
|
+
{
|
|
488
|
+
headers: {
|
|
489
|
+
Authorization: `Bearer ${openai}`,
|
|
490
|
+
'OpenAI-Beta': 'assistants=v2',
|
|
491
|
+
},
|
|
492
|
+
},
|
|
493
|
+
);
|
|
494
|
+
|
|
495
|
+
const assistantMessage = messagesResponse.data?.data?.find(
|
|
496
|
+
(item: any) => item.role === 'assistant',
|
|
497
|
+
);
|
|
498
|
+
|
|
499
|
+
const output = assistantMessage?.content?.[0]?.text?.value;
|
|
500
|
+
|
|
501
|
+
return output || '';
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
private async chatWithGemini({
|
|
505
|
+
message,
|
|
506
|
+
model,
|
|
507
|
+
systemPrompt,
|
|
508
|
+
}: {
|
|
509
|
+
message: string;
|
|
510
|
+
model: string;
|
|
511
|
+
systemPrompt?: string;
|
|
512
|
+
}) {
|
|
513
|
+
const { gemini } = await this.getApiKeys();
|
|
514
|
+
|
|
515
|
+
if (!gemini) {
|
|
516
|
+
throw new BadRequestException(
|
|
517
|
+
'Gemini API key is not configured (setting "ai-gemini-api-key").',
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${gemini}`;
|
|
522
|
+
|
|
523
|
+
const payload: any = {
|
|
524
|
+
contents: [{ parts: [{ text: message }] }],
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
if (systemPrompt) {
|
|
528
|
+
payload.systemInstruction = {
|
|
529
|
+
parts: [{ text: systemPrompt }],
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const response = await axios.post(url, payload, {
|
|
534
|
+
headers: {
|
|
535
|
+
'Content-Type': 'application/json',
|
|
536
|
+
},
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
const parts = response.data?.candidates?.[0]?.content?.parts || [];
|
|
540
|
+
return parts.map((part: any) => part.text || '').join('');
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
private async sleep(ms: number) {
|
|
544
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
545
|
+
}
|
|
546
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { IsIn, IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
|
2
|
+
|
|
3
|
+
export class ChatDTO {
|
|
4
|
+
@IsString()
|
|
5
|
+
@IsNotEmpty()
|
|
6
|
+
message: string;
|
|
7
|
+
|
|
8
|
+
@IsOptional()
|
|
9
|
+
@IsString()
|
|
10
|
+
@IsIn(['openai', 'gemini'])
|
|
11
|
+
provider?: 'openai' | 'gemini';
|
|
12
|
+
|
|
13
|
+
@IsOptional()
|
|
14
|
+
@IsString()
|
|
15
|
+
model?: string;
|
|
16
|
+
|
|
17
|
+
@IsOptional()
|
|
18
|
+
@IsString()
|
|
19
|
+
systemPrompt?: string;
|
|
20
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { IsIn, IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
|
2
|
+
|
|
3
|
+
export class CreateAgentDTO {
|
|
4
|
+
@IsString()
|
|
5
|
+
@IsNotEmpty()
|
|
6
|
+
slug: string;
|
|
7
|
+
|
|
8
|
+
@IsOptional()
|
|
9
|
+
@IsString()
|
|
10
|
+
@IsIn(['openai', 'gemini'])
|
|
11
|
+
provider?: 'openai' | 'gemini';
|
|
12
|
+
|
|
13
|
+
@IsOptional()
|
|
14
|
+
@IsString()
|
|
15
|
+
model?: string;
|
|
16
|
+
|
|
17
|
+
@IsOptional()
|
|
18
|
+
@IsString()
|
|
19
|
+
instructions?: string;
|
|
20
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { IsIn, IsOptional, IsString } from 'class-validator';
|
|
2
|
+
|
|
3
|
+
export class UpdateAgentDTO {
|
|
4
|
+
@IsOptional()
|
|
5
|
+
@IsString()
|
|
6
|
+
slug?: string;
|
|
7
|
+
|
|
8
|
+
@IsOptional()
|
|
9
|
+
@IsString()
|
|
10
|
+
@IsIn(['openai', 'gemini'])
|
|
11
|
+
provider?: 'openai' | 'gemini';
|
|
12
|
+
|
|
13
|
+
@IsOptional()
|
|
14
|
+
@IsString()
|
|
15
|
+
model?: string;
|
|
16
|
+
|
|
17
|
+
@IsOptional()
|
|
18
|
+
@IsString()
|
|
19
|
+
instructions?: string;
|
|
20
|
+
}
|
package/src/core.module.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { HttpModule } from '@nestjs/axios';
|
|
|
7
7
|
import { forwardRef, Global, Inject, MiddlewareConsumer, Module, NestModule, OnModuleInit } from '@nestjs/common';
|
|
8
8
|
import { ConfigModule } from '@nestjs/config';
|
|
9
9
|
import { APP_INTERCEPTOR, Reflector } from '@nestjs/core';
|
|
10
|
+
import { AiModule } from './ai/ai.module';
|
|
10
11
|
import { AuthModule } from './auth/auth.module';
|
|
11
12
|
import { SystemModule } from './core/system.module';
|
|
12
13
|
import { DashboardModule } from './dashboard/dashboard.module';
|
|
@@ -37,6 +38,7 @@ import { ValidatorServiceLocator } from './validators/service-locator';
|
|
|
37
38
|
imports: [
|
|
38
39
|
HttpModule,
|
|
39
40
|
ConfigModule.forRoot(),
|
|
41
|
+
forwardRef(() => AiModule),
|
|
40
42
|
forwardRef(() => InstallModule),
|
|
41
43
|
forwardRef(() => AuthModule),
|
|
42
44
|
forwardRef(() => DashboardModule),
|
|
@@ -72,6 +74,7 @@ import { ValidatorServiceLocator } from './validators/service-locator';
|
|
|
72
74
|
SettingModule,
|
|
73
75
|
FileModule,
|
|
74
76
|
SessionModule,
|
|
77
|
+
AiModule,
|
|
75
78
|
IsStrongPasswordWithSettingsConstraint,
|
|
76
79
|
IsEmailWithSettingsConstraint,
|
|
77
80
|
IsPinCodeWithSettingConstraint,
|
package/src/file/file.service.ts
CHANGED
|
@@ -2,12 +2,12 @@ import { getLocaleText } from '@hed-hog/api-locale';
|
|
|
2
2
|
import { PaginationDTO, PaginationService } from '@hed-hog/api-pagination';
|
|
3
3
|
import { PrismaService } from '@hed-hog/api-prisma';
|
|
4
4
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
BadRequestException,
|
|
6
|
+
forwardRef,
|
|
7
|
+
Inject,
|
|
8
|
+
Injectable,
|
|
9
|
+
NotFoundException,
|
|
10
|
+
OnModuleInit
|
|
11
11
|
} from '@nestjs/common';
|
|
12
12
|
import { JwtService } from '@nestjs/jwt';
|
|
13
13
|
import { SettingService } from '../setting/setting.service';
|
|
@@ -21,6 +21,23 @@ export class FileService implements OnModuleInit {
|
|
|
21
21
|
private providerId: number;
|
|
22
22
|
private mimetypes: Record<string, number> = {};
|
|
23
23
|
|
|
24
|
+
private hasMojibake(value: string): boolean {
|
|
25
|
+
return /Ã.|Â.|â[\u0080-\u00BF]/.test(value);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
private normalizeUploadedFilename(filename: string): string {
|
|
29
|
+
if (!filename) {
|
|
30
|
+
return filename;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!this.hasMojibake(filename)) {
|
|
34
|
+
return filename;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const decoded = Buffer.from(filename, 'latin1').toString('utf8');
|
|
38
|
+
return this.hasMojibake(decoded) ? filename : decoded;
|
|
39
|
+
}
|
|
40
|
+
|
|
24
41
|
constructor(
|
|
25
42
|
@Inject(forwardRef(() => PrismaService))
|
|
26
43
|
private readonly prismaService: PrismaService,
|
|
@@ -284,11 +301,20 @@ export class FileService implements OnModuleInit {
|
|
|
284
301
|
throw new BadRequestException(`File too large: ${fileBuffer.size} bytes`);
|
|
285
302
|
}
|
|
286
303
|
|
|
287
|
-
const
|
|
304
|
+
const normalizedFilename = this.normalizeUploadedFilename(
|
|
305
|
+
fileBuffer.originalname,
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
const normalizedFile = {
|
|
309
|
+
...fileBuffer,
|
|
310
|
+
originalname: normalizedFilename,
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
const url = await provider.upload(destination, normalizedFile);
|
|
288
314
|
|
|
289
315
|
const file = await this.prismaService.file.create({
|
|
290
316
|
data: {
|
|
291
|
-
filename:
|
|
317
|
+
filename: normalizedFilename,
|
|
292
318
|
path: url,
|
|
293
319
|
provider_id: this.providerId,
|
|
294
320
|
location: destination,
|
package/src/index.ts
CHANGED