@hed-hog/core 0.0.223 → 0.0.233

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