@hed-hog/core 0.0.232 → 0.0.234
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 +2 -2
- package/dist/ai/ai.controller.d.ts.map +1 -1
- package/dist/ai/ai.controller.js +11 -6
- package/dist/ai/ai.controller.js.map +1 -1
- package/dist/ai/ai.module.d.ts.map +1 -1
- package/dist/ai/ai.module.js +6 -1
- package/dist/ai/ai.module.js.map +1 -1
- package/dist/ai/ai.service.d.ts +27 -3
- package/dist/ai/ai.service.d.ts.map +1 -1
- package/dist/ai/ai.service.js +213 -13
- package/dist/ai/ai.service.js.map +1 -1
- package/dist/ai/dto/chat-agent.dto.d.ts +1 -0
- package/dist/ai/dto/chat-agent.dto.d.ts.map +1 -1
- package/dist/ai/dto/chat-agent.dto.js +7 -0
- package/dist/ai/dto/chat-agent.dto.js.map +1 -1
- package/dist/ai/dto/chat.dto.d.ts +1 -0
- package/dist/ai/dto/chat.dto.d.ts.map +1 -1
- package/dist/ai/dto/chat.dto.js +7 -0
- package/dist/ai/dto/chat.dto.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/hedhog/frontend/app/ai_agent/page.tsx.ejs +27 -30
- package/package.json +3 -2
- package/src/ai/ai.controller.ts +12 -5
- package/src/ai/ai.module.ts +6 -1
- package/src/ai/ai.service.ts +257 -13
- package/src/ai/dto/chat-agent.dto.ts +7 -1
- package/src/ai/dto/chat.dto.ts +7 -1
- package/src/index.ts +2 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chat.dto.d.ts","sourceRoot":"","sources":["../../../src/ai/dto/chat.dto.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"chat.dto.d.ts","sourceRoot":"","sources":["../../../src/ai/dto/chat.dto.ts"],"names":[],"mappings":"AAGA,qBAAa,OAAO;IAGlB,OAAO,EAAE,MAAM,CAAC;IAKhB,QAAQ,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAI/B,KAAK,CAAC,EAAE,MAAM,CAAC;IAIf,YAAY,CAAC,EAAE,MAAM,CAAC;IAKtB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB"}
|
package/dist/ai/dto/chat.dto.js
CHANGED
|
@@ -10,6 +10,7 @@ var __metadata = (this && this.__metadata) || function (k, v) {
|
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
exports.ChatDTO = void 0;
|
|
13
|
+
const class_transformer_1 = require("class-transformer");
|
|
13
14
|
const class_validator_1 = require("class-validator");
|
|
14
15
|
class ChatDTO {
|
|
15
16
|
}
|
|
@@ -35,4 +36,10 @@ __decorate([
|
|
|
35
36
|
(0, class_validator_1.IsString)(),
|
|
36
37
|
__metadata("design:type", String)
|
|
37
38
|
], ChatDTO.prototype, "systemPrompt", void 0);
|
|
39
|
+
__decorate([
|
|
40
|
+
(0, class_validator_1.IsOptional)(),
|
|
41
|
+
(0, class_transformer_1.Type)(() => Number),
|
|
42
|
+
(0, class_validator_1.IsInt)(),
|
|
43
|
+
__metadata("design:type", Number)
|
|
44
|
+
], ChatDTO.prototype, "file_id", void 0);
|
|
38
45
|
//# sourceMappingURL=chat.dto.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chat.dto.js","sourceRoot":"","sources":["../../../src/ai/dto/chat.dto.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,
|
|
1
|
+
{"version":3,"file":"chat.dto.js","sourceRoot":"","sources":["../../../src/ai/dto/chat.dto.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,yDAAyC;AACzC,qDAAgF;AAEhF,MAAa,OAAO;CAsBnB;AAtBD,0BAsBC;AAnBC;IAFC,IAAA,0BAAQ,GAAE;IACV,IAAA,4BAAU,GAAE;;wCACG;AAKhB;IAHC,IAAA,4BAAU,GAAE;IACZ,IAAA,0BAAQ,GAAE;IACV,IAAA,sBAAI,EAAC,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;;yCACI;AAI/B;IAFC,IAAA,4BAAU,GAAE;IACZ,IAAA,0BAAQ,GAAE;;sCACI;AAIf;IAFC,IAAA,4BAAU,GAAE;IACZ,IAAA,0BAAQ,GAAE;;6CACW;AAKtB;IAHC,IAAA,4BAAU,GAAE;IACZ,IAAA,wBAAI,EAAC,GAAG,EAAE,CAAC,MAAM,CAAC;IAClB,IAAA,uBAAK,GAAE;;wCACS"}
|
package/dist/index.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ export * from './user/user.service';
|
|
|
10
10
|
export * from './mail/mail.module';
|
|
11
11
|
export * from './mail/mail.service';
|
|
12
12
|
export * from './setting/setting.service';
|
|
13
|
+
export * from './ai/ai.module';
|
|
13
14
|
export * from './ai/ai.service';
|
|
14
15
|
export * from './validators/is-email-with-settings.validator';
|
|
15
16
|
export * from './validators/is-pin-code-with-setting.validator';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,eAAe,CAAC;AAG9B,cAAc,qBAAqB,CAAC;AACpC,cAAc,0BAA0B,CAAC;AAGzC,cAAc,qBAAqB,CAAC;AAGpC,cAAc,0BAA0B,CAAC;AACzC,cAAc,qBAAqB,CAAC;AAGpC,cAAc,yBAAyB,CAAC;AAGxC,cAAc,iCAAiC,CAAC;AAChD,cAAc,qBAAqB,CAAC;AAEpC,cAAc,oBAAoB,CAAC;AACnC,cAAc,qBAAqB,CAAC;AAEpC,cAAc,2BAA2B,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,eAAe,CAAC;AAG9B,cAAc,qBAAqB,CAAC;AACpC,cAAc,0BAA0B,CAAC;AAGzC,cAAc,qBAAqB,CAAC;AAGpC,cAAc,0BAA0B,CAAC;AACzC,cAAc,qBAAqB,CAAC;AAGpC,cAAc,yBAAyB,CAAC;AAGxC,cAAc,iCAAiC,CAAC;AAChD,cAAc,qBAAqB,CAAC;AAEpC,cAAc,oBAAoB,CAAC;AACnC,cAAc,qBAAqB,CAAC;AAEpC,cAAc,2BAA2B,CAAC;AAE1C,cAAc,gBAAgB,CAAC;AAC/B,cAAc,iBAAiB,CAAC;AAGhC,cAAc,+CAA+C,CAAC;AAC9D,cAAc,iDAAiD,CAAC;AAChE,cAAc,yDAAyD,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -32,6 +32,7 @@ __exportStar(require("./user/user.service"), exports);
|
|
|
32
32
|
__exportStar(require("./mail/mail.module"), exports);
|
|
33
33
|
__exportStar(require("./mail/mail.service"), exports);
|
|
34
34
|
__exportStar(require("./setting/setting.service"), exports);
|
|
35
|
+
__exportStar(require("./ai/ai.module"), exports);
|
|
35
36
|
__exportStar(require("./ai/ai.service"), exports);
|
|
36
37
|
// Validators
|
|
37
38
|
__exportStar(require("./validators/is-email-with-settings.validator"), exports);
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,cAAc;AACd,gDAA8B;AAE9B,cAAc;AACd,sDAAoC;AACpC,2DAAyC;AAEzC,cAAc;AACd,sDAAoC;AAEpC,oBAAoB;AACpB,2DAAyC;AACzC,sDAAoC;AAEpC,gBAAgB;AAChB,0DAAwC;AAExC,cAAc;AACd,kEAAgD;AAChD,sDAAoC;AAEpC,qDAAmC;AACnC,sDAAoC;AAEpC,4DAA0C;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,cAAc;AACd,gDAA8B;AAE9B,cAAc;AACd,sDAAoC;AACpC,2DAAyC;AAEzC,cAAc;AACd,sDAAoC;AAEpC,oBAAoB;AACpB,2DAAyC;AACzC,sDAAoC;AAEpC,gBAAgB;AAChB,0DAAwC;AAExC,cAAc;AACd,kEAAgD;AAChD,sDAAoC;AAEpC,qDAAmC;AACnC,sDAAoC;AAEpC,4DAA0C;AAE1C,iDAA+B;AAC/B,kDAAgC;AAEhC,aAAa;AACb,gFAA8D;AAC9D,kFAAgE;AAChE,0FAAwE"}
|
|
@@ -18,14 +18,6 @@ import {
|
|
|
18
18
|
} from '@/components/ui/alert-dialog';
|
|
19
19
|
import { Button } from '@/components/ui/button';
|
|
20
20
|
import { Card, CardContent } from '@/components/ui/card';
|
|
21
|
-
import {
|
|
22
|
-
Dialog,
|
|
23
|
-
DialogContent,
|
|
24
|
-
DialogDescription,
|
|
25
|
-
DialogFooter,
|
|
26
|
-
DialogHeader,
|
|
27
|
-
DialogTitle,
|
|
28
|
-
} from '@/components/ui/dialog';
|
|
29
21
|
import {
|
|
30
22
|
Form,
|
|
31
23
|
FormControl,
|
|
@@ -42,6 +34,14 @@ import {
|
|
|
42
34
|
SelectTrigger,
|
|
43
35
|
SelectValue,
|
|
44
36
|
} from '@/components/ui/select';
|
|
37
|
+
import {
|
|
38
|
+
Sheet,
|
|
39
|
+
SheetContent,
|
|
40
|
+
SheetDescription,
|
|
41
|
+
SheetFooter,
|
|
42
|
+
SheetHeader,
|
|
43
|
+
SheetTitle,
|
|
44
|
+
} from '@/components/ui/sheet';
|
|
45
45
|
import {
|
|
46
46
|
Table,
|
|
47
47
|
TableBody,
|
|
@@ -330,17 +330,20 @@ export default function AiAgentPage() {
|
|
|
330
330
|
}}
|
|
331
331
|
/>
|
|
332
332
|
|
|
333
|
-
<
|
|
334
|
-
<
|
|
335
|
-
<
|
|
336
|
-
<
|
|
333
|
+
<Sheet open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
|
334
|
+
<SheetContent side="right" className="sm:max-w-xl overflow-y-auto">
|
|
335
|
+
<SheetHeader>
|
|
336
|
+
<SheetTitle>
|
|
337
337
|
{editingAgent ? t('editTitle') : t('createTitle')}
|
|
338
|
-
</
|
|
339
|
-
<
|
|
340
|
-
</
|
|
338
|
+
</SheetTitle>
|
|
339
|
+
<SheetDescription>{t('dialogDescription')}</SheetDescription>
|
|
340
|
+
</SheetHeader>
|
|
341
341
|
|
|
342
342
|
<Form {...form}>
|
|
343
|
-
<form
|
|
343
|
+
<form
|
|
344
|
+
className="flex flex-col px-4 gap-4"
|
|
345
|
+
onSubmit={form.handleSubmit(onSubmit)}
|
|
346
|
+
>
|
|
344
347
|
<FormField
|
|
345
348
|
control={form.control}
|
|
346
349
|
name="slug"
|
|
@@ -369,7 +372,7 @@ export default function AiAgentPage() {
|
|
|
369
372
|
value={field.value}
|
|
370
373
|
onValueChange={field.onChange}
|
|
371
374
|
>
|
|
372
|
-
<SelectTrigger>
|
|
375
|
+
<SelectTrigger className="w-full">
|
|
373
376
|
<SelectValue
|
|
374
377
|
placeholder={t('fieldProviderPlaceholder')}
|
|
375
378
|
/>
|
|
@@ -415,7 +418,8 @@ export default function AiAgentPage() {
|
|
|
415
418
|
<FormControl>
|
|
416
419
|
<Textarea
|
|
417
420
|
placeholder={t('fieldInstructionsPlaceholder')}
|
|
418
|
-
rows={
|
|
421
|
+
rows={10}
|
|
422
|
+
className="min-h-24"
|
|
419
423
|
{...field}
|
|
420
424
|
/>
|
|
421
425
|
</FormControl>
|
|
@@ -424,23 +428,16 @@ export default function AiAgentPage() {
|
|
|
424
428
|
)}
|
|
425
429
|
/>
|
|
426
430
|
|
|
427
|
-
<
|
|
428
|
-
<Button
|
|
429
|
-
type="button"
|
|
430
|
-
variant="outline"
|
|
431
|
-
onClick={() => setIsDialogOpen(false)}
|
|
432
|
-
>
|
|
433
|
-
{t('cancel')}
|
|
434
|
-
</Button>
|
|
431
|
+
<SheetFooter className="p-0">
|
|
435
432
|
<Button type="submit">
|
|
436
|
-
<Bot className="h-4 w-4
|
|
433
|
+
<Bot className="h-4 w-4" />
|
|
437
434
|
{editingAgent ? t('saveChanges') : t('createAgent')}
|
|
438
435
|
</Button>
|
|
439
|
-
</
|
|
436
|
+
</SheetFooter>
|
|
440
437
|
</form>
|
|
441
438
|
</Form>
|
|
442
|
-
</
|
|
443
|
-
</
|
|
439
|
+
</SheetContent>
|
|
440
|
+
</Sheet>
|
|
444
441
|
|
|
445
442
|
<AlertDialog
|
|
446
443
|
open={Boolean(agentToDelete)}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hed-hog/core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.234",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"dependencies": {
|
|
@@ -23,15 +23,16 @@
|
|
|
23
23
|
"jsonwebtoken": "^9.0.2",
|
|
24
24
|
"multer": "^2.0.1",
|
|
25
25
|
"pako": "^2.1.0",
|
|
26
|
+
"pdf-parse": "^1.1.1",
|
|
26
27
|
"png-to-ico": "^2.1.8",
|
|
27
28
|
"qrcode": "^1.5.4",
|
|
28
29
|
"rxjs": "^7.8.2",
|
|
29
30
|
"sharp": "^0.34.2",
|
|
30
31
|
"speakeasy": "^2.0.0",
|
|
31
32
|
"uuid": "^11.1.0",
|
|
32
|
-
"@hed-hog/api": "0.0.3",
|
|
33
33
|
"@hed-hog/api-prisma": "0.0.4",
|
|
34
34
|
"@hed-hog/api-types": "0.0.1",
|
|
35
|
+
"@hed-hog/api": "0.0.3",
|
|
35
36
|
"@hed-hog/api-locale": "0.0.11",
|
|
36
37
|
"@hed-hog/api-pagination": "0.0.5",
|
|
37
38
|
"@hed-hog/api-mail": "0.0.7"
|
package/src/ai/ai.controller.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Role } from '@hed-hog/api';
|
|
2
2
|
import { Pagination } from '@hed-hog/api-pagination';
|
|
3
|
-
import { Body, Controller, Delete, Get, Inject, Param, ParseIntPipe, Patch, Post, forwardRef } from '@nestjs/common';
|
|
3
|
+
import { Body, Controller, Delete, Get, Inject, Param, ParseIntPipe, Patch, Post, UploadedFile, UseInterceptors, forwardRef } from '@nestjs/common';
|
|
4
|
+
import { FileInterceptor } from '@nestjs/platform-express';
|
|
4
5
|
import { DeleteDTO } from '../dto/delete.dto';
|
|
5
6
|
import { AiService } from './ai.service';
|
|
6
7
|
import { ChatAgentDTO } from './dto/chat-agent.dto';
|
|
@@ -17,8 +18,9 @@ export class AiController {
|
|
|
17
18
|
) {}
|
|
18
19
|
|
|
19
20
|
@Post('chat')
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
@UseInterceptors(FileInterceptor('file'))
|
|
22
|
+
async chat(@Body() data: ChatDTO, @UploadedFile() file?: MulterFile) {
|
|
23
|
+
return this.aiService.chat(data, file);
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
@Post('agent')
|
|
@@ -55,7 +57,12 @@ export class AiController {
|
|
|
55
57
|
}
|
|
56
58
|
|
|
57
59
|
@Post('agent/:slug/chat')
|
|
58
|
-
|
|
59
|
-
|
|
60
|
+
@UseInterceptors(FileInterceptor('file'))
|
|
61
|
+
async chatWithAgent(
|
|
62
|
+
@Param('slug') slug: string,
|
|
63
|
+
@Body() data: ChatAgentDTO,
|
|
64
|
+
@UploadedFile() file?: MulterFile,
|
|
65
|
+
) {
|
|
66
|
+
return this.aiService.chatWithAgent(slug, data, file);
|
|
60
67
|
}
|
|
61
68
|
}
|
package/src/ai/ai.module.ts
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import { PrismaModule } from '@hed-hog/api-prisma';
|
|
2
2
|
import { Module, forwardRef } from '@nestjs/common';
|
|
3
|
+
import { FileModule } from '../file/file.module';
|
|
3
4
|
import { SettingModule } from '../setting/setting.module';
|
|
4
5
|
import { AiController } from './ai.controller';
|
|
5
6
|
import { AiService } from './ai.service';
|
|
6
7
|
|
|
7
8
|
@Module({
|
|
8
|
-
imports: [
|
|
9
|
+
imports: [
|
|
10
|
+
forwardRef(() => PrismaModule),
|
|
11
|
+
forwardRef(() => SettingModule),
|
|
12
|
+
forwardRef(() => FileModule),
|
|
13
|
+
],
|
|
9
14
|
controllers: [AiController],
|
|
10
15
|
providers: [AiService],
|
|
11
16
|
exports: [AiService],
|
package/src/ai/ai.service.ts
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import { PaginationDTO } from '@hed-hog/api-pagination';
|
|
2
2
|
import { PrismaService } from '@hed-hog/api-prisma';
|
|
3
3
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
BadRequestException,
|
|
5
|
+
forwardRef,
|
|
6
|
+
Inject,
|
|
7
|
+
Injectable,
|
|
8
|
+
Logger,
|
|
9
|
+
NotFoundException,
|
|
9
10
|
} from '@nestjs/common';
|
|
10
11
|
import { Prisma } from '@prisma/client';
|
|
11
12
|
import axios from 'axios';
|
|
13
|
+
import { createHash } from 'crypto';
|
|
14
|
+
import pdfParse from 'pdf-parse';
|
|
12
15
|
import { DeleteDTO } from '../dto/delete.dto';
|
|
16
|
+
import { FileService } from '../file/file.service';
|
|
13
17
|
import { SettingService } from '../setting/setting.service';
|
|
14
18
|
import { ChatAgentDTO } from './dto/chat-agent.dto';
|
|
15
19
|
import { ChatDTO } from './dto/chat.dto';
|
|
@@ -29,17 +33,28 @@ type AiAgentRecord = {
|
|
|
29
33
|
updated_at: Date;
|
|
30
34
|
};
|
|
31
35
|
|
|
36
|
+
type AiAttachment = {
|
|
37
|
+
filename: string;
|
|
38
|
+
mimeType: string;
|
|
39
|
+
buffer: Buffer;
|
|
40
|
+
};
|
|
41
|
+
|
|
32
42
|
@Injectable()
|
|
33
43
|
export class AiService {
|
|
44
|
+
private readonly logger = new Logger(AiService.name);
|
|
45
|
+
|
|
34
46
|
constructor(
|
|
35
47
|
@Inject(forwardRef(() => PrismaService))
|
|
36
48
|
private readonly prismaService: PrismaService,
|
|
37
49
|
@Inject(forwardRef(() => SettingService))
|
|
38
50
|
private readonly settingService: SettingService,
|
|
51
|
+
@Inject(forwardRef(() => FileService))
|
|
52
|
+
private readonly fileService: FileService,
|
|
39
53
|
) {}
|
|
40
54
|
|
|
41
|
-
async chat(data: ChatDTO) {
|
|
55
|
+
async chat(data: ChatDTO, file?: MulterFile) {
|
|
42
56
|
const provider = data.provider || 'openai';
|
|
57
|
+
const attachment = await this.resolveAttachment(file, data.file_id);
|
|
43
58
|
|
|
44
59
|
if (provider === 'openai') {
|
|
45
60
|
const model = data.model || 'gpt-4o-mini';
|
|
@@ -47,6 +62,7 @@ export class AiService {
|
|
|
47
62
|
message: data.message,
|
|
48
63
|
model,
|
|
49
64
|
systemPrompt: data.systemPrompt,
|
|
65
|
+
attachment,
|
|
50
66
|
});
|
|
51
67
|
|
|
52
68
|
return {
|
|
@@ -61,6 +77,7 @@ export class AiService {
|
|
|
61
77
|
message: data.message,
|
|
62
78
|
model,
|
|
63
79
|
systemPrompt: data.systemPrompt,
|
|
80
|
+
attachment,
|
|
64
81
|
});
|
|
65
82
|
|
|
66
83
|
return {
|
|
@@ -93,7 +110,13 @@ export class AiService {
|
|
|
93
110
|
|
|
94
111
|
await this.prismaService.$executeRaw`
|
|
95
112
|
INSERT INTO ai_agent (slug, provider, model, instructions, external_agent_id)
|
|
96
|
-
VALUES (
|
|
113
|
+
VALUES (
|
|
114
|
+
${slug},
|
|
115
|
+
CAST(${provider} AS ai_agent_provider_enum),
|
|
116
|
+
${model},
|
|
117
|
+
${instructions},
|
|
118
|
+
${externalAgentId}
|
|
119
|
+
)
|
|
97
120
|
`;
|
|
98
121
|
|
|
99
122
|
return this.findAgentBySlug(slug);
|
|
@@ -182,7 +205,7 @@ export class AiService {
|
|
|
182
205
|
await this.prismaService.$executeRaw`
|
|
183
206
|
UPDATE ai_agent
|
|
184
207
|
SET slug = ${slug},
|
|
185
|
-
provider = ${provider},
|
|
208
|
+
provider = CAST(${provider} AS ai_agent_provider_enum),
|
|
186
209
|
model = ${model},
|
|
187
210
|
instructions = ${instructions},
|
|
188
211
|
external_agent_id = ${externalAgentId},
|
|
@@ -218,15 +241,16 @@ export class AiService {
|
|
|
218
241
|
};
|
|
219
242
|
}
|
|
220
243
|
|
|
221
|
-
async chatWithAgent(slug: string, data: ChatAgentDTO) {
|
|
244
|
+
async chatWithAgent(slug: string, data: ChatAgentDTO, file?: MulterFile) {
|
|
222
245
|
const agent = await this.findAgentBySlug(slug);
|
|
246
|
+
const attachment = await this.resolveAttachment(file, data.file_id);
|
|
223
247
|
|
|
224
248
|
if (!agent) {
|
|
225
249
|
throw new NotFoundException(`AI agent with slug "${slug}" was not found.`);
|
|
226
250
|
}
|
|
227
251
|
|
|
228
252
|
if (agent.provider === 'openai') {
|
|
229
|
-
if (agent.external_agent_id) {
|
|
253
|
+
if (agent.external_agent_id && !attachment) {
|
|
230
254
|
const content = await this.chatWithOpenAiAssistant(
|
|
231
255
|
agent.external_agent_id,
|
|
232
256
|
data.message,
|
|
@@ -244,6 +268,7 @@ export class AiService {
|
|
|
244
268
|
message: data.message,
|
|
245
269
|
model: agent.model || this.getDefaultModel('openai'),
|
|
246
270
|
systemPrompt: agent.instructions || undefined,
|
|
271
|
+
attachment,
|
|
247
272
|
});
|
|
248
273
|
|
|
249
274
|
return {
|
|
@@ -258,6 +283,7 @@ export class AiService {
|
|
|
258
283
|
message: data.message,
|
|
259
284
|
model: agent.model || this.getDefaultModel('gemini'),
|
|
260
285
|
systemPrompt: agent.instructions || undefined,
|
|
286
|
+
attachment,
|
|
261
287
|
});
|
|
262
288
|
|
|
263
289
|
return {
|
|
@@ -278,6 +304,64 @@ export class AiService {
|
|
|
278
304
|
return agent;
|
|
279
305
|
}
|
|
280
306
|
|
|
307
|
+
async extractAttachmentText(file?: MulterFile, fileId?: number): Promise<string> {
|
|
308
|
+
const attachment = await this.resolveAttachment(file, fileId);
|
|
309
|
+
|
|
310
|
+
if (!attachment) {
|
|
311
|
+
return '';
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (this.isPdfMime(attachment.mimeType)) {
|
|
315
|
+
return this.extractPdfText(attachment.buffer);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (this.isTextMime(attachment.mimeType)) {
|
|
319
|
+
return this.toUtf8Text(attachment.buffer);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return '';
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async debugAttachmentForLlm(file?: MulterFile, fileId?: number) {
|
|
326
|
+
const attachment = await this.resolveAttachment(file, fileId);
|
|
327
|
+
|
|
328
|
+
if (!attachment) {
|
|
329
|
+
return {
|
|
330
|
+
hasAttachment: false,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
let mode: 'image' | 'pdf-text' | 'text' | 'binary' = 'binary';
|
|
335
|
+
let extractedText = '';
|
|
336
|
+
|
|
337
|
+
if (this.isImageMime(attachment.mimeType)) {
|
|
338
|
+
mode = 'image';
|
|
339
|
+
} else if (this.isPdfMime(attachment.mimeType)) {
|
|
340
|
+
mode = 'pdf-text';
|
|
341
|
+
extractedText = await this.extractPdfText(attachment.buffer);
|
|
342
|
+
} else if (this.isTextMime(attachment.mimeType)) {
|
|
343
|
+
mode = 'text';
|
|
344
|
+
extractedText = this.toUtf8Text(attachment.buffer);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const result = {
|
|
348
|
+
hasAttachment: true,
|
|
349
|
+
filename: attachment.filename,
|
|
350
|
+
mimeType: attachment.mimeType,
|
|
351
|
+
bytes: attachment.buffer.length,
|
|
352
|
+
mode,
|
|
353
|
+
textLength: extractedText.length,
|
|
354
|
+
textSample: extractedText.slice(0, 600),
|
|
355
|
+
sha256: createHash('sha256').update(attachment.buffer).digest('hex'),
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
this.logger.warn(
|
|
359
|
+
`[AI-ATTACHMENT-DEBUG] fileId=${fileId || 'upload'} filename="${result.filename}" mime=${result.mimeType} mode=${result.mode} bytes=${result.bytes} textLength=${result.textLength}`,
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
return result;
|
|
363
|
+
}
|
|
364
|
+
|
|
281
365
|
private async findAgentBySlug(slug: string): Promise<AiAgentRecord | null> {
|
|
282
366
|
const result = await this.prismaService.$queryRaw<AiAgentRecord[]>`
|
|
283
367
|
SELECT id, slug, provider, model, instructions, external_agent_id, created_at, updated_at
|
|
@@ -324,10 +408,12 @@ export class AiService {
|
|
|
324
408
|
message,
|
|
325
409
|
model,
|
|
326
410
|
systemPrompt,
|
|
411
|
+
attachment,
|
|
327
412
|
}: {
|
|
328
413
|
message: string;
|
|
329
414
|
model: string;
|
|
330
415
|
systemPrompt?: string;
|
|
416
|
+
attachment?: AiAttachment | null;
|
|
331
417
|
}) {
|
|
332
418
|
const { openai } = await this.getApiKeys();
|
|
333
419
|
|
|
@@ -337,13 +423,14 @@ export class AiService {
|
|
|
337
423
|
);
|
|
338
424
|
}
|
|
339
425
|
|
|
340
|
-
const messages: Array<
|
|
426
|
+
const messages: Array<any> = [];
|
|
341
427
|
|
|
342
428
|
if (systemPrompt) {
|
|
343
429
|
messages.push({ role: 'system', content: systemPrompt });
|
|
344
430
|
}
|
|
345
431
|
|
|
346
|
-
|
|
432
|
+
const userContent = await this.buildOpenAiUserContent(message, attachment);
|
|
433
|
+
messages.push({ role: 'user', content: userContent });
|
|
347
434
|
|
|
348
435
|
const response = await axios.post(
|
|
349
436
|
'https://api.openai.com/v1/chat/completions',
|
|
@@ -505,10 +592,12 @@ export class AiService {
|
|
|
505
592
|
message,
|
|
506
593
|
model,
|
|
507
594
|
systemPrompt,
|
|
595
|
+
attachment,
|
|
508
596
|
}: {
|
|
509
597
|
message: string;
|
|
510
598
|
model: string;
|
|
511
599
|
systemPrompt?: string;
|
|
600
|
+
attachment?: AiAttachment | null;
|
|
512
601
|
}) {
|
|
513
602
|
const { gemini } = await this.getApiKeys();
|
|
514
603
|
|
|
@@ -520,8 +609,39 @@ export class AiService {
|
|
|
520
609
|
|
|
521
610
|
const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${gemini}`;
|
|
522
611
|
|
|
612
|
+
const userParts: any[] = [{ text: message }];
|
|
613
|
+
|
|
614
|
+
if (attachment) {
|
|
615
|
+
if (this.isTextMime(attachment.mimeType)) {
|
|
616
|
+
userParts.push({
|
|
617
|
+
text: `\n\n[Attached file: ${attachment.filename}]\n${this.toUtf8Text(attachment.buffer)}`,
|
|
618
|
+
});
|
|
619
|
+
} else if (this.isPdfMime(attachment.mimeType)) {
|
|
620
|
+
const pdfText = await this.extractPdfText(attachment.buffer);
|
|
621
|
+
if (pdfText) {
|
|
622
|
+
userParts.push({
|
|
623
|
+
text: `\n\n[Attached file: ${attachment.filename}]\n${pdfText}`,
|
|
624
|
+
});
|
|
625
|
+
} else {
|
|
626
|
+
userParts.push({
|
|
627
|
+
inline_data: {
|
|
628
|
+
mime_type: attachment.mimeType,
|
|
629
|
+
data: attachment.buffer.toString('base64'),
|
|
630
|
+
},
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
} else {
|
|
634
|
+
userParts.push({
|
|
635
|
+
inline_data: {
|
|
636
|
+
mime_type: attachment.mimeType,
|
|
637
|
+
data: attachment.buffer.toString('base64'),
|
|
638
|
+
},
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
523
643
|
const payload: any = {
|
|
524
|
-
contents: [{ parts:
|
|
644
|
+
contents: [{ parts: userParts }],
|
|
525
645
|
};
|
|
526
646
|
|
|
527
647
|
if (systemPrompt) {
|
|
@@ -543,4 +663,128 @@ export class AiService {
|
|
|
543
663
|
private async sleep(ms: number) {
|
|
544
664
|
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
545
665
|
}
|
|
666
|
+
|
|
667
|
+
private async resolveAttachment(
|
|
668
|
+
uploadFile?: MulterFile,
|
|
669
|
+
fileId?: number,
|
|
670
|
+
): Promise<AiAttachment | null> {
|
|
671
|
+
if (uploadFile) {
|
|
672
|
+
const mimeType = this.normalizeAttachmentMimeType(
|
|
673
|
+
uploadFile.mimetype,
|
|
674
|
+
uploadFile.originalname,
|
|
675
|
+
);
|
|
676
|
+
return {
|
|
677
|
+
filename: uploadFile.originalname,
|
|
678
|
+
mimeType,
|
|
679
|
+
buffer: uploadFile.buffer,
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
if (fileId) {
|
|
684
|
+
const { file, buffer } = await this.fileService.getBuffer(fileId);
|
|
685
|
+
const mimeType = this.normalizeAttachmentMimeType(
|
|
686
|
+
file.file_mimetype?.name || 'application/octet-stream',
|
|
687
|
+
file.filename,
|
|
688
|
+
);
|
|
689
|
+
return {
|
|
690
|
+
filename: file.filename,
|
|
691
|
+
mimeType,
|
|
692
|
+
buffer,
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
return null;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
private async buildOpenAiUserContent(
|
|
700
|
+
message: string,
|
|
701
|
+
attachment?: AiAttachment | null,
|
|
702
|
+
) {
|
|
703
|
+
if (!attachment) {
|
|
704
|
+
return message;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
if (this.isImageMime(attachment.mimeType)) {
|
|
708
|
+
const dataUri = `data:${attachment.mimeType};base64,${attachment.buffer.toString('base64')}`;
|
|
709
|
+
return [
|
|
710
|
+
{ type: 'text', text: message },
|
|
711
|
+
{ type: 'image_url', image_url: { url: dataUri } },
|
|
712
|
+
];
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
let text: string;
|
|
716
|
+
|
|
717
|
+
if (this.isTextMime(attachment.mimeType)) {
|
|
718
|
+
text = this.toUtf8Text(attachment.buffer);
|
|
719
|
+
} else if (this.isPdfMime(attachment.mimeType)) {
|
|
720
|
+
text =
|
|
721
|
+
(await this.extractPdfText(attachment.buffer)) ||
|
|
722
|
+
`[PDF attachment with unreadable text: ${attachment.filename}]`;
|
|
723
|
+
} else {
|
|
724
|
+
text = `[Binary attachment: ${attachment.filename} | ${attachment.mimeType} | ${attachment.buffer.length} bytes]`;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
return `${message}\n\n[Attached file: ${attachment.filename}]\n${text}`;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
private isImageMime(mimeType: string) {
|
|
731
|
+
return mimeType.startsWith('image/');
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
private isTextMime(mimeType: string) {
|
|
735
|
+
return (
|
|
736
|
+
mimeType.startsWith('text/') ||
|
|
737
|
+
mimeType === 'application/json' ||
|
|
738
|
+
mimeType === 'application/xml'
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
private isPdfMime(mimeType: string) {
|
|
743
|
+
return (
|
|
744
|
+
mimeType === 'application/pdf' ||
|
|
745
|
+
mimeType === 'application/x-pdf' ||
|
|
746
|
+
mimeType.endsWith('/pdf')
|
|
747
|
+
);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
private normalizeAttachmentMimeType(mimeType: string, filename?: string) {
|
|
751
|
+
const current = (mimeType || '').toLowerCase();
|
|
752
|
+
if (current && current !== 'application/octet-stream') {
|
|
753
|
+
return current;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
const name = (filename || '').toLowerCase();
|
|
757
|
+
|
|
758
|
+
if (name.endsWith('.pdf')) return 'application/pdf';
|
|
759
|
+
if (name.endsWith('.json')) return 'application/json';
|
|
760
|
+
if (name.endsWith('.xml')) return 'application/xml';
|
|
761
|
+
if (name.endsWith('.txt') || name.endsWith('.csv')) return 'text/plain';
|
|
762
|
+
if (name.endsWith('.png')) return 'image/png';
|
|
763
|
+
if (name.endsWith('.jpg') || name.endsWith('.jpeg')) return 'image/jpeg';
|
|
764
|
+
if (name.endsWith('.webp')) return 'image/webp';
|
|
765
|
+
|
|
766
|
+
return current || 'application/octet-stream';
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
private async extractPdfText(buffer: Buffer) {
|
|
770
|
+
try {
|
|
771
|
+
const result = await pdfParse(buffer);
|
|
772
|
+
const text = (result?.text || '')
|
|
773
|
+
.replace(/\r/g, '')
|
|
774
|
+
.replace(/[ \t]+\n/g, '\n')
|
|
775
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
776
|
+
.trim();
|
|
777
|
+
if (!text) {
|
|
778
|
+
return '';
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
return text.slice(0, 60000);
|
|
782
|
+
} catch {
|
|
783
|
+
return '';
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
private toUtf8Text(buffer: Buffer) {
|
|
788
|
+
return buffer.toString('utf8');
|
|
789
|
+
}
|
|
546
790
|
}
|
|
@@ -1,7 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Type } from 'class-transformer';
|
|
2
|
+
import { IsInt, IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
|
2
3
|
|
|
3
4
|
export class ChatAgentDTO {
|
|
4
5
|
@IsString()
|
|
5
6
|
@IsNotEmpty()
|
|
6
7
|
message: string;
|
|
8
|
+
|
|
9
|
+
@IsOptional()
|
|
10
|
+
@Type(() => Number)
|
|
11
|
+
@IsInt()
|
|
12
|
+
file_id?: number;
|
|
7
13
|
}
|