@hed-hog/core 0.0.362 → 0.0.365
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 +4 -4
- package/dist/ai/ai.module.d.ts.map +1 -1
- package/dist/ai/ai.module.js +2 -0
- package/dist/ai/ai.module.js.map +1 -1
- package/dist/ai/ai.service.d.ts +7 -5
- package/dist/ai/ai.service.d.ts.map +1 -1
- package/dist/ai/ai.service.js +63 -31
- package/dist/ai/ai.service.js.map +1 -1
- package/dist/ai-instruction/ai-execution.service.d.ts +45 -0
- package/dist/ai-instruction/ai-execution.service.d.ts.map +1 -0
- package/dist/ai-instruction/ai-execution.service.js +203 -0
- package/dist/ai-instruction/ai-execution.service.js.map +1 -0
- package/dist/ai-instruction/ai-instruction.controller.d.ts +25 -0
- package/dist/ai-instruction/ai-instruction.controller.d.ts.map +1 -0
- package/dist/ai-instruction/ai-instruction.controller.js +117 -0
- package/dist/ai-instruction/ai-instruction.controller.js.map +1 -0
- package/dist/ai-instruction/ai-instruction.module.d.ts +3 -0
- package/dist/ai-instruction/ai-instruction.module.d.ts.map +1 -0
- package/dist/ai-instruction/ai-instruction.module.js +27 -0
- package/dist/ai-instruction/ai-instruction.module.js.map +1 -0
- package/dist/ai-instruction/ai-instruction.service.d.ts +38 -0
- package/dist/ai-instruction/ai-instruction.service.d.ts.map +1 -0
- package/dist/ai-instruction/ai-instruction.service.js +289 -0
- package/dist/ai-instruction/ai-instruction.service.js.map +1 -0
- package/dist/ai-instruction/ai-prompt-builder.service.d.ts +53 -0
- package/dist/ai-instruction/ai-prompt-builder.service.d.ts.map +1 -0
- package/dist/ai-instruction/ai-prompt-builder.service.js +132 -0
- package/dist/ai-instruction/ai-prompt-builder.service.js.map +1 -0
- package/dist/ai-instruction/dto/create-ai-instruction.dto.d.ts +12 -0
- package/dist/ai-instruction/dto/create-ai-instruction.dto.d.ts.map +1 -0
- package/dist/ai-instruction/dto/create-ai-instruction.dto.js +57 -0
- package/dist/ai-instruction/dto/create-ai-instruction.dto.js.map +1 -0
- package/dist/ai-instruction/dto/update-ai-instruction.dto.d.ts +10 -0
- package/dist/ai-instruction/dto/update-ai-instruction.dto.d.ts.map +1 -0
- package/dist/ai-instruction/dto/update-ai-instruction.dto.js +56 -0
- package/dist/ai-instruction/dto/update-ai-instruction.dto.js.map +1 -0
- package/dist/ai-instruction/types/ai-instruction.types.d.ts +78 -0
- package/dist/ai-instruction/types/ai-instruction.types.d.ts.map +1 -0
- package/dist/ai-instruction/types/ai-instruction.types.js +3 -0
- package/dist/ai-instruction/types/ai-instruction.types.js.map +1 -0
- package/dist/auth/auth.controller.d.ts +13 -5
- package/dist/auth/auth.controller.d.ts.map +1 -1
- package/dist/auth/auth.controller.js +39 -20
- package/dist/auth/auth.controller.js.map +1 -1
- package/dist/auth/auth.service.d.ts +18 -7
- package/dist/auth/auth.service.d.ts.map +1 -1
- package/dist/auth/auth.service.js +20 -16
- package/dist/auth/auth.service.js.map +1 -1
- 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 +37 -0
- package/dist/file/file.service.d.ts.map +1 -1
- package/dist/file/file.service.js +94 -4
- package/dist/file/file.service.js.map +1 -1
- package/dist/file/provider/abstract.provider.d.ts +6 -2
- package/dist/file/provider/abstract.provider.d.ts.map +1 -1
- package/dist/file/provider/abstract.provider.js.map +1 -1
- package/dist/file/provider/azure.provider.d.ts +4 -1
- package/dist/file/provider/azure.provider.d.ts.map +1 -1
- package/dist/file/provider/azure.provider.js +7 -2
- package/dist/file/provider/azure.provider.js.map +1 -1
- package/dist/file/provider/gcs.provider.d.ts +4 -1
- package/dist/file/provider/gcs.provider.d.ts.map +1 -1
- package/dist/file/provider/gcs.provider.js +7 -2
- package/dist/file/provider/gcs.provider.js.map +1 -1
- package/dist/file/provider/local.provider.d.ts +2 -2
- package/dist/file/provider/local.provider.d.ts.map +1 -1
- package/dist/file/provider/local.provider.js +7 -2
- package/dist/file/provider/local.provider.js.map +1 -1
- package/dist/file/provider/s3.provider.d.ts +4 -1
- package/dist/file/provider/s3.provider.d.ts.map +1 -1
- package/dist/file/provider/s3.provider.js +9 -6
- package/dist/file/provider/s3.provider.js.map +1 -1
- package/dist/file/provider/spaces.provider.d.ts +4 -1
- package/dist/file/provider/spaces.provider.d.ts.map +1 -1
- package/dist/file/provider/spaces.provider.js +9 -5
- package/dist/file/provider/spaces.provider.js.map +1 -1
- package/dist/integration-profile/integration-profile.controller.d.ts +3 -3
- package/dist/integration-profile/integration-profile.service.d.ts +3 -3
- package/dist/mcp-chat/mcp-chat.controller.d.ts +2 -2
- package/dist/mcp-chat/mcp-chat.module.d.ts.map +1 -1
- package/dist/mcp-chat/mcp-chat.module.js +2 -0
- package/dist/mcp-chat/mcp-chat.module.js.map +1 -1
- package/dist/mcp-chat/mcp-chat.service.d.ts +8 -3
- package/dist/mcp-chat/mcp-chat.service.d.ts.map +1 -1
- package/dist/mcp-chat/mcp-chat.service.js +100 -20
- package/dist/mcp-chat/mcp-chat.service.js.map +1 -1
- package/dist/notification/notification.service.d.ts +1 -0
- package/dist/notification/notification.service.d.ts.map +1 -1
- package/dist/notification/notification.service.js +29 -1
- package/dist/notification/notification.service.js.map +1 -1
- package/dist/oauth/oauth.service.d.ts +1 -0
- package/dist/oauth/oauth.service.d.ts.map +1 -1
- package/dist/oauth/oauth.service.js +55 -1
- package/dist/oauth/oauth.service.js.map +1 -1
- package/dist/profile/events/profile-avatar-updated.event.d.ts +7 -0
- package/dist/profile/events/profile-avatar-updated.event.d.ts.map +1 -0
- package/dist/profile/events/profile-avatar-updated.event.js +12 -0
- package/dist/profile/events/profile-avatar-updated.event.js.map +1 -0
- package/dist/profile/events/profile-email-changed.event.d.ts +7 -0
- package/dist/profile/events/profile-email-changed.event.d.ts.map +1 -0
- package/dist/profile/events/profile-email-changed.event.js +12 -0
- package/dist/profile/events/profile-email-changed.event.js.map +1 -0
- package/dist/profile/events/profile-updated.event.d.ts +7 -0
- package/dist/profile/events/profile-updated.event.d.ts.map +1 -0
- package/dist/profile/events/profile-updated.event.js +12 -0
- package/dist/profile/events/profile-updated.event.js.map +1 -0
- package/dist/profile/profile.service.d.ts +3 -1
- package/dist/profile/profile.service.d.ts.map +1 -1
- package/dist/profile/profile.service.js +28 -17
- package/dist/profile/profile.service.js.map +1 -1
- package/dist/session/session.controller.d.ts +1 -0
- package/dist/session/session.controller.d.ts.map +1 -1
- package/dist/session/session.service.d.ts +9 -4
- package/dist/session/session.service.d.ts.map +1 -1
- package/dist/session/session.service.js +61 -51
- package/dist/session/session.service.js.map +1 -1
- package/dist/setting/setting.controller.d.ts +5 -5
- package/dist/setting/setting.service.d.ts +5 -5
- package/dist/setting/setting.service.d.ts.map +1 -1
- package/dist/setting/setting.service.js +2 -0
- package/dist/setting/setting.service.js.map +1 -1
- package/dist/task/task.service.d.ts +8 -11
- package/dist/task/task.service.d.ts.map +1 -1
- package/dist/task/task.service.js +10 -16
- package/dist/task/task.service.js.map +1 -1
- package/dist/user/user.controller.d.ts +1 -0
- package/dist/user/user.controller.d.ts.map +1 -1
- package/dist/user/user.service.d.ts +3 -0
- package/dist/user/user.service.d.ts.map +1 -1
- package/dist/webhook-integration/webhook-command.registry.d.ts +9 -2
- package/dist/webhook-integration/webhook-command.registry.d.ts.map +1 -1
- package/dist/webhook-integration/webhook-command.registry.js.map +1 -1
- package/dist/webhook-integration/webhook-integration.service.d.ts.map +1 -1
- package/dist/webhook-integration/webhook-integration.service.js +13 -4
- package/dist/webhook-integration/webhook-integration.service.js.map +1 -1
- package/hedhog/data/integration_provider.yaml +17 -0
- package/hedhog/data/integration_type.yaml +6 -0
- package/hedhog/data/route.yaml +86 -2
- package/hedhog/frontend/app/configurations/[slug]/components/setting-field.tsx.ejs +109 -0
- package/hedhog/frontend/app/configurations/layout.tsx.ejs +6 -6
- package/hedhog/frontend/app/integration/profiles/page.tsx.ejs +169 -14
- package/hedhog/frontend/messages/en.json +11 -2
- package/hedhog/frontend/messages/pt.json +11 -2
- package/hedhog/table/ai_execution.yaml +79 -0
- package/hedhog/table/ai_execution_step.yaml +65 -0
- package/hedhog/table/ai_instruction.yaml +43 -0
- package/hedhog/table/ai_instruction_version.yaml +30 -0
- package/hedhog/table/ai_model_pricing.yaml +35 -0
- package/hedhog/table/user_session.yaml +2 -0
- package/package.json +5 -4
- package/src/ai/ai.module.ts +2 -0
- package/src/ai/ai.service.ts +64 -31
- package/src/ai-instruction/ai-execution.service.ts +244 -0
- package/src/ai-instruction/ai-instruction.controller.ts +73 -0
- package/src/ai-instruction/ai-instruction.module.ts +14 -0
- package/src/ai-instruction/ai-instruction.service.ts +343 -0
- package/src/ai-instruction/ai-prompt-builder.service.ts +142 -0
- package/src/ai-instruction/dto/create-ai-instruction.dto.ts +36 -0
- package/src/ai-instruction/dto/update-ai-instruction.dto.ts +35 -0
- package/src/ai-instruction/types/ai-instruction.types.ts +90 -0
- package/src/auth/auth.controller.ts +23 -5
- package/src/auth/auth.service.ts +26 -15
- package/src/core.module.ts +3 -0
- package/src/file/file.service.ts +136 -4
- package/src/file/provider/abstract.provider.ts +4 -2
- package/src/file/provider/azure.provider.ts +6 -3
- package/src/file/provider/gcs.provider.ts +5 -3
- package/src/file/provider/local.provider.ts +7 -3
- package/src/file/provider/s3.provider.ts +8 -6
- package/src/file/provider/spaces.provider.ts +8 -7
- package/src/mcp-chat/mcp-chat.module.ts +19 -17
- package/src/mcp-chat/mcp-chat.service.ts +95 -9
- package/src/notification/notification.service.ts +32 -1
- package/src/oauth/oauth.service.ts +74 -1
- package/src/profile/events/profile-avatar-updated.event.ts +8 -0
- package/src/profile/events/profile-email-changed.event.ts +8 -0
- package/src/profile/events/profile-updated.event.ts +8 -0
- package/src/profile/profile.service.ts +25 -16
- package/src/session/session.service.ts +70 -52
- package/src/setting/setting.service.ts +10 -8
- package/src/task/task.service.ts +10 -16
- package/src/webhook-integration/webhook-command.registry.ts +10 -2
- package/src/webhook-integration/webhook-integration.service.ts +31 -4
package/src/ai/ai.service.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
import axios from 'axios';
|
|
13
13
|
import { createHash } from 'crypto';
|
|
14
14
|
import pdfParse from 'pdf-parse';
|
|
15
|
+
import { AiExecutionService } from '../ai-instruction/ai-execution.service';
|
|
15
16
|
import { DeleteDTO } from '../dto/delete.dto';
|
|
16
17
|
import { FileService } from '../file/file.service';
|
|
17
18
|
import { buildAiConfigFromIntegration } from '../integration-profile/integration-profile.utils';
|
|
@@ -51,6 +52,8 @@ export class AiService implements OnModuleInit {
|
|
|
51
52
|
private readonly settingService: SettingService,
|
|
52
53
|
@Inject(forwardRef(() => FileService))
|
|
53
54
|
private readonly fileService: FileService,
|
|
55
|
+
@Inject(forwardRef(() => AiExecutionService))
|
|
56
|
+
private readonly aiExecution: AiExecutionService,
|
|
54
57
|
) {}
|
|
55
58
|
|
|
56
59
|
async onModuleInit() {
|
|
@@ -69,35 +72,59 @@ export class AiService implements OnModuleInit {
|
|
|
69
72
|
const provider = data.provider || 'openai';
|
|
70
73
|
const attachment = await this.resolveAttachment(file, data.file_id);
|
|
71
74
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
+
const executionId = await this.aiExecution.start({ trigger: 'api', contextSlug: 'ai-chat' });
|
|
76
|
+
const stepStart = Date.now();
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
if (provider === 'openai') {
|
|
80
|
+
const model = data.model || 'gpt-4o-mini';
|
|
81
|
+
const { content, usage } = await this.chatWithOpenAi({
|
|
82
|
+
message: data.message,
|
|
83
|
+
model,
|
|
84
|
+
systemPrompt: data.systemPrompt,
|
|
85
|
+
attachment,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
await this.aiExecution.recordStep({
|
|
89
|
+
executionId,
|
|
90
|
+
stepOrder: 0,
|
|
91
|
+
type: 'output',
|
|
92
|
+
provider: 'openai',
|
|
93
|
+
model,
|
|
94
|
+
durationMs: Date.now() - stepStart,
|
|
95
|
+
usage,
|
|
96
|
+
outputSummary: content.slice(0, 200),
|
|
97
|
+
});
|
|
98
|
+
await this.aiExecution.finish(executionId, 'completed');
|
|
99
|
+
|
|
100
|
+
return { provider, model, content };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const model = data.model || 'gemini-1.5-flash';
|
|
104
|
+
const { content, usage } = await this.chatWithGemini({
|
|
75
105
|
message: data.message,
|
|
76
106
|
model,
|
|
77
107
|
systemPrompt: data.systemPrompt,
|
|
78
108
|
attachment,
|
|
79
109
|
});
|
|
80
110
|
|
|
81
|
-
|
|
82
|
-
|
|
111
|
+
await this.aiExecution.recordStep({
|
|
112
|
+
executionId,
|
|
113
|
+
stepOrder: 0,
|
|
114
|
+
type: 'output',
|
|
115
|
+
provider: 'gemini',
|
|
83
116
|
model,
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const content = await this.chatWithGemini({
|
|
90
|
-
message: data.message,
|
|
91
|
-
model,
|
|
92
|
-
systemPrompt: data.systemPrompt,
|
|
93
|
-
attachment,
|
|
94
|
-
});
|
|
117
|
+
durationMs: Date.now() - stepStart,
|
|
118
|
+
usage,
|
|
119
|
+
outputSummary: content.slice(0, 200),
|
|
120
|
+
});
|
|
121
|
+
await this.aiExecution.finish(executionId, 'completed');
|
|
95
122
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
123
|
+
return { provider, model, content };
|
|
124
|
+
} catch (err) {
|
|
125
|
+
await this.aiExecution.finish(executionId, 'failed', String(err));
|
|
126
|
+
throw err;
|
|
127
|
+
}
|
|
101
128
|
}
|
|
102
129
|
|
|
103
130
|
async createAgent(data: CreateAgentDTO) {
|
|
@@ -277,7 +304,7 @@ export class AiService implements OnModuleInit {
|
|
|
277
304
|
};
|
|
278
305
|
}
|
|
279
306
|
|
|
280
|
-
const content = await this.chatWithOpenAi({
|
|
307
|
+
const { content } = await this.chatWithOpenAi({
|
|
281
308
|
message: data.message,
|
|
282
309
|
model: agent.model || this.getDefaultModel('openai'),
|
|
283
310
|
systemPrompt: agent.instructions || undefined,
|
|
@@ -292,7 +319,7 @@ export class AiService implements OnModuleInit {
|
|
|
292
319
|
};
|
|
293
320
|
}
|
|
294
321
|
|
|
295
|
-
const content = await this.chatWithGemini({
|
|
322
|
+
const { content } = await this.chatWithGemini({
|
|
296
323
|
message: data.message,
|
|
297
324
|
model: agent.model || this.getDefaultModel('gemini'),
|
|
298
325
|
systemPrompt: agent.instructions || undefined,
|
|
@@ -454,7 +481,7 @@ export class AiService implements OnModuleInit {
|
|
|
454
481
|
model: string;
|
|
455
482
|
systemPrompt?: string;
|
|
456
483
|
attachment?: AiAttachment | null;
|
|
457
|
-
}) {
|
|
484
|
+
}): Promise<{ content: string; usage?: { input: number; output: number; total: number } }> {
|
|
458
485
|
const openai = await this.getAiApiKey('openai');
|
|
459
486
|
|
|
460
487
|
const messages: Array<any> = [];
|
|
@@ -468,10 +495,7 @@ export class AiService implements OnModuleInit {
|
|
|
468
495
|
|
|
469
496
|
const response = await axios.post(
|
|
470
497
|
'https://api.openai.com/v1/chat/completions',
|
|
471
|
-
{
|
|
472
|
-
model,
|
|
473
|
-
messages,
|
|
474
|
-
},
|
|
498
|
+
{ model, messages },
|
|
475
499
|
{
|
|
476
500
|
headers: {
|
|
477
501
|
Authorization: `Bearer ${openai}`,
|
|
@@ -480,7 +504,11 @@ export class AiService implements OnModuleInit {
|
|
|
480
504
|
},
|
|
481
505
|
);
|
|
482
506
|
|
|
483
|
-
|
|
507
|
+
const u = response.data?.usage;
|
|
508
|
+
return {
|
|
509
|
+
content: response.data?.choices?.[0]?.message?.content || '',
|
|
510
|
+
usage: u ? { input: u.prompt_tokens ?? 0, output: u.completion_tokens ?? 0, total: u.total_tokens ?? 0 } : undefined,
|
|
511
|
+
};
|
|
484
512
|
}
|
|
485
513
|
|
|
486
514
|
private async createOpenAiAssistant({
|
|
@@ -620,7 +648,7 @@ export class AiService implements OnModuleInit {
|
|
|
620
648
|
model: string;
|
|
621
649
|
systemPrompt?: string;
|
|
622
650
|
attachment?: AiAttachment | null;
|
|
623
|
-
}) {
|
|
651
|
+
}): Promise<{ content: string; usage?: { input: number; output: number; total: number } }> {
|
|
624
652
|
const gemini = await this.getAiApiKey('gemini');
|
|
625
653
|
|
|
626
654
|
const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${gemini}`;
|
|
@@ -673,7 +701,12 @@ export class AiService implements OnModuleInit {
|
|
|
673
701
|
});
|
|
674
702
|
|
|
675
703
|
const parts = response.data?.candidates?.[0]?.content?.parts || [];
|
|
676
|
-
|
|
704
|
+
const content = parts.map((part: any) => part.text || '').join('');
|
|
705
|
+
const u = response.data?.usageMetadata;
|
|
706
|
+
return {
|
|
707
|
+
content,
|
|
708
|
+
usage: u ? { input: u.promptTokenCount ?? 0, output: u.candidatesTokenCount ?? 0, total: u.totalTokenCount ?? 0 } : undefined,
|
|
709
|
+
};
|
|
677
710
|
}
|
|
678
711
|
|
|
679
712
|
private async sleep(ms: number) {
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { PrismaService } from '@hed-hog/api-prisma';
|
|
2
|
+
import { forwardRef, Inject, Injectable, Logger } from '@nestjs/common';
|
|
3
|
+
import type {
|
|
4
|
+
AiExecutionRecord,
|
|
5
|
+
AiExecutionStepType,
|
|
6
|
+
AiExecutionTrigger,
|
|
7
|
+
AiModelPricingRecord,
|
|
8
|
+
AiTokenUsage,
|
|
9
|
+
} from './types/ai-instruction.types';
|
|
10
|
+
|
|
11
|
+
export type StartExecutionParams = {
|
|
12
|
+
userId?: number;
|
|
13
|
+
conversationId?: number;
|
|
14
|
+
agentId?: number;
|
|
15
|
+
trigger?: AiExecutionTrigger;
|
|
16
|
+
contextSlug?: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type RecordStepParams = {
|
|
20
|
+
executionId: number;
|
|
21
|
+
stepOrder: number;
|
|
22
|
+
type: AiExecutionStepType;
|
|
23
|
+
provider?: string;
|
|
24
|
+
model?: string;
|
|
25
|
+
toolName?: string;
|
|
26
|
+
usage?: AiTokenUsage;
|
|
27
|
+
durationMs?: number;
|
|
28
|
+
inputSummary?: string;
|
|
29
|
+
outputSummary?: string;
|
|
30
|
+
success?: boolean;
|
|
31
|
+
error?: string;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
@Injectable()
|
|
35
|
+
export class AiExecutionService {
|
|
36
|
+
private readonly logger = new Logger(AiExecutionService.name);
|
|
37
|
+
private readonly pricingCache = new Map<string, AiModelPricingRecord>();
|
|
38
|
+
private pricingLoadedAt = 0;
|
|
39
|
+
private readonly PRICING_TTL_MS = 60 * 60 * 1000; // 1h
|
|
40
|
+
|
|
41
|
+
constructor(
|
|
42
|
+
@Inject(forwardRef(() => PrismaService))
|
|
43
|
+
private readonly prisma: PrismaService,
|
|
44
|
+
) {}
|
|
45
|
+
|
|
46
|
+
async start(params: StartExecutionParams): Promise<number> {
|
|
47
|
+
try {
|
|
48
|
+
const result = await this.prisma.$queryRaw<Array<{ id: number }>>`
|
|
49
|
+
INSERT INTO ai_execution (user_id, conversation_id, agent_id, trigger, status, context_slug, started_at)
|
|
50
|
+
VALUES (
|
|
51
|
+
${params.userId ?? null},
|
|
52
|
+
${params.conversationId ?? null},
|
|
53
|
+
${params.agentId ?? null},
|
|
54
|
+
CAST(${params.trigger ?? 'manual'} AS ai_execution_trigger_enum),
|
|
55
|
+
'running',
|
|
56
|
+
${params.contextSlug ?? null},
|
|
57
|
+
NOW()
|
|
58
|
+
)
|
|
59
|
+
RETURNING id
|
|
60
|
+
`;
|
|
61
|
+
return result[0].id;
|
|
62
|
+
} catch (e) {
|
|
63
|
+
this.logger.warn(`Failed to start execution record: ${e}`);
|
|
64
|
+
return 0;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async recordStep(params: RecordStepParams): Promise<void> {
|
|
69
|
+
if (!params.executionId) return;
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
const costUsd = await this.estimateCost(
|
|
73
|
+
params.provider,
|
|
74
|
+
params.model,
|
|
75
|
+
params.usage,
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
await this.prisma.$executeRaw`
|
|
79
|
+
INSERT INTO ai_execution_step (
|
|
80
|
+
execution_id, step_order, type, provider, model, tool_name,
|
|
81
|
+
tokens_input, tokens_output, cost_usd, duration_ms,
|
|
82
|
+
input_summary, output_summary, success, error
|
|
83
|
+
) VALUES (
|
|
84
|
+
${params.executionId},
|
|
85
|
+
${params.stepOrder},
|
|
86
|
+
CAST(${params.type} AS ai_execution_step_type_enum),
|
|
87
|
+
${params.provider ?? null},
|
|
88
|
+
${params.model ?? null},
|
|
89
|
+
${params.toolName ?? null},
|
|
90
|
+
${params.usage?.input ?? 0},
|
|
91
|
+
${params.usage?.output ?? 0},
|
|
92
|
+
${costUsd},
|
|
93
|
+
${params.durationMs ?? 0},
|
|
94
|
+
${params.inputSummary ? params.inputSummary.slice(0, 500) : null},
|
|
95
|
+
${params.outputSummary ? params.outputSummary.slice(0, 500) : null},
|
|
96
|
+
${params.success ?? true},
|
|
97
|
+
${params.error ?? null}
|
|
98
|
+
)
|
|
99
|
+
`;
|
|
100
|
+
} catch (e) {
|
|
101
|
+
this.logger.warn(`Failed to record execution step: ${e}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async finish(
|
|
106
|
+
executionId: number,
|
|
107
|
+
status: 'completed' | 'failed' | 'cancelled',
|
|
108
|
+
error?: string,
|
|
109
|
+
): Promise<void> {
|
|
110
|
+
if (!executionId) return;
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
// Aggregate totals from steps
|
|
114
|
+
const totals = await this.prisma.$queryRaw<
|
|
115
|
+
Array<{ tokens_input: bigint; tokens_output: bigint; cost_usd: string }>
|
|
116
|
+
>`
|
|
117
|
+
SELECT
|
|
118
|
+
SUM(tokens_input) AS tokens_input,
|
|
119
|
+
SUM(tokens_output) AS tokens_output,
|
|
120
|
+
SUM(cost_usd) AS cost_usd
|
|
121
|
+
FROM ai_execution_step
|
|
122
|
+
WHERE execution_id = ${executionId}
|
|
123
|
+
`;
|
|
124
|
+
|
|
125
|
+
const t = totals[0];
|
|
126
|
+
const tokensInput = Number(t?.tokens_input || 0);
|
|
127
|
+
const tokensOutput = Number(t?.tokens_output || 0);
|
|
128
|
+
const costUsd = parseFloat(String(t?.cost_usd || 0));
|
|
129
|
+
|
|
130
|
+
await this.prisma.$executeRaw`
|
|
131
|
+
UPDATE ai_execution
|
|
132
|
+
SET
|
|
133
|
+
status = CAST(${status} AS ai_execution_status_enum),
|
|
134
|
+
tokens_input = ${tokensInput},
|
|
135
|
+
tokens_output = ${tokensOutput},
|
|
136
|
+
tokens_total = ${tokensInput + tokensOutput},
|
|
137
|
+
cost_usd = ${costUsd},
|
|
138
|
+
finished_at = NOW(),
|
|
139
|
+
error = ${error ?? null},
|
|
140
|
+
updated_at = NOW()
|
|
141
|
+
WHERE id = ${executionId}
|
|
142
|
+
`;
|
|
143
|
+
} catch (e) {
|
|
144
|
+
this.logger.warn(`Failed to finish execution record ${executionId}: ${e}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async getExecution(executionId: number): Promise<AiExecutionRecord | null> {
|
|
149
|
+
const rows = await this.prisma.$queryRaw<AiExecutionRecord[]>`
|
|
150
|
+
SELECT * FROM ai_execution WHERE id = ${executionId} LIMIT 1
|
|
151
|
+
`;
|
|
152
|
+
return rows?.[0] || null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async listExecutions(params: {
|
|
156
|
+
userId?: number;
|
|
157
|
+
contextSlug?: string;
|
|
158
|
+
limit?: number;
|
|
159
|
+
offset?: number;
|
|
160
|
+
}) {
|
|
161
|
+
const limit = Math.min(params.limit ?? 20, 100);
|
|
162
|
+
const offset = params.offset ?? 0;
|
|
163
|
+
|
|
164
|
+
// Use parameterized queries to prevent SQL injection
|
|
165
|
+
if (params.userId !== undefined && params.contextSlug !== undefined) {
|
|
166
|
+
return this.prisma.$queryRaw<AiExecutionRecord[]>`
|
|
167
|
+
SELECT * FROM ai_execution
|
|
168
|
+
WHERE user_id = ${params.userId} AND context_slug = ${params.contextSlug}
|
|
169
|
+
ORDER BY started_at DESC LIMIT ${limit} OFFSET ${offset}
|
|
170
|
+
`;
|
|
171
|
+
}
|
|
172
|
+
if (params.userId !== undefined) {
|
|
173
|
+
return this.prisma.$queryRaw<AiExecutionRecord[]>`
|
|
174
|
+
SELECT * FROM ai_execution
|
|
175
|
+
WHERE user_id = ${params.userId}
|
|
176
|
+
ORDER BY started_at DESC LIMIT ${limit} OFFSET ${offset}
|
|
177
|
+
`;
|
|
178
|
+
}
|
|
179
|
+
if (params.contextSlug !== undefined) {
|
|
180
|
+
return this.prisma.$queryRaw<AiExecutionRecord[]>`
|
|
181
|
+
SELECT * FROM ai_execution
|
|
182
|
+
WHERE context_slug = ${params.contextSlug}
|
|
183
|
+
ORDER BY started_at DESC LIMIT ${limit} OFFSET ${offset}
|
|
184
|
+
`;
|
|
185
|
+
}
|
|
186
|
+
return this.prisma.$queryRaw<AiExecutionRecord[]>`
|
|
187
|
+
SELECT * FROM ai_execution
|
|
188
|
+
ORDER BY started_at DESC LIMIT ${limit} OFFSET ${offset}
|
|
189
|
+
`;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ── Cost estimation ───────────────────────────────────────────────────────
|
|
193
|
+
|
|
194
|
+
private async estimateCost(
|
|
195
|
+
provider?: string,
|
|
196
|
+
model?: string,
|
|
197
|
+
usage?: AiTokenUsage,
|
|
198
|
+
): Promise<number> {
|
|
199
|
+
if (!provider || !model || !usage || (!usage.input && !usage.output)) {
|
|
200
|
+
return 0;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const pricing = await this.getPricing(provider, model);
|
|
204
|
+
if (!pricing) return 0;
|
|
205
|
+
|
|
206
|
+
const inputCost = (usage.input / 1_000_000) * Number(pricing.price_input_per_million);
|
|
207
|
+
const outputCost = (usage.output / 1_000_000) * Number(pricing.price_output_per_million);
|
|
208
|
+
const cachedCost =
|
|
209
|
+
usage.cached && pricing.price_cached_input_per_million
|
|
210
|
+
? (usage.cached / 1_000_000) * Number(pricing.price_cached_input_per_million)
|
|
211
|
+
: 0;
|
|
212
|
+
|
|
213
|
+
return parseFloat((inputCost + outputCost + cachedCost).toFixed(8));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
private async getPricing(provider: string, model: string): Promise<AiModelPricingRecord | null> {
|
|
217
|
+
const key = `${provider}:${model}`;
|
|
218
|
+
|
|
219
|
+
if (Date.now() - this.pricingLoadedAt > this.PRICING_TTL_MS) {
|
|
220
|
+
await this.loadPricingCache();
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return this.pricingCache.get(key) || null;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
private async loadPricingCache() {
|
|
227
|
+
try {
|
|
228
|
+
const rows = await this.prisma.$queryRaw<AiModelPricingRecord[]>`
|
|
229
|
+
SELECT * FROM ai_model_pricing WHERE is_active = TRUE ORDER BY valid_from DESC
|
|
230
|
+
`;
|
|
231
|
+
this.pricingCache.clear();
|
|
232
|
+
for (const row of rows) {
|
|
233
|
+
const key = `${row.provider}:${row.model}`;
|
|
234
|
+
// Only keep the most recent pricing entry per provider+model
|
|
235
|
+
if (!this.pricingCache.has(key)) {
|
|
236
|
+
this.pricingCache.set(key, row);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
this.pricingLoadedAt = Date.now();
|
|
240
|
+
} catch (e) {
|
|
241
|
+
this.logger.warn(`Failed to load pricing cache: ${e}`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Role } from '@hed-hog/api';
|
|
2
|
+
import { Pagination } from '@hed-hog/api-pagination';
|
|
3
|
+
import {
|
|
4
|
+
Body,
|
|
5
|
+
Controller,
|
|
6
|
+
Delete,
|
|
7
|
+
Get,
|
|
8
|
+
Inject,
|
|
9
|
+
Param,
|
|
10
|
+
ParseIntPipe,
|
|
11
|
+
Patch,
|
|
12
|
+
Post,
|
|
13
|
+
forwardRef,
|
|
14
|
+
} from '@nestjs/common';
|
|
15
|
+
import { DeleteDTO } from '../dto/delete.dto';
|
|
16
|
+
import { AiInstructionService } from './ai-instruction.service';
|
|
17
|
+
import { CreateAiInstructionDTO } from './dto/create-ai-instruction.dto';
|
|
18
|
+
import { UpdateAiInstructionDTO } from './dto/update-ai-instruction.dto';
|
|
19
|
+
|
|
20
|
+
@Role()
|
|
21
|
+
@Controller('ai-instruction')
|
|
22
|
+
export class AiInstructionController {
|
|
23
|
+
constructor(
|
|
24
|
+
@Inject(forwardRef(() => AiInstructionService))
|
|
25
|
+
private readonly aiInstructionService: AiInstructionService,
|
|
26
|
+
) {}
|
|
27
|
+
|
|
28
|
+
@Get()
|
|
29
|
+
async list(@Pagination() paginationParams) {
|
|
30
|
+
return this.aiInstructionService.list(paginationParams);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@Get(':id')
|
|
34
|
+
async getById(@Param('id', ParseIntPipe) id: number) {
|
|
35
|
+
return this.aiInstructionService.getById(id);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@Get('slug/:slug')
|
|
39
|
+
async getBySlug(@Param('slug') slug: string) {
|
|
40
|
+
return this.aiInstructionService.getBySlug(slug);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@Post()
|
|
44
|
+
async create(@Body() dto: CreateAiInstructionDTO) {
|
|
45
|
+
return this.aiInstructionService.create(dto);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@Patch(':id')
|
|
49
|
+
async update(
|
|
50
|
+
@Param('id', ParseIntPipe) id: number,
|
|
51
|
+
@Body() dto: UpdateAiInstructionDTO,
|
|
52
|
+
) {
|
|
53
|
+
return this.aiInstructionService.update(id, dto);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@Delete()
|
|
57
|
+
async delete(@Body() data: DeleteDTO) {
|
|
58
|
+
return this.aiInstructionService.delete(data);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
@Get(':id/versions')
|
|
62
|
+
async listVersions(@Param('id', ParseIntPipe) id: number) {
|
|
63
|
+
return this.aiInstructionService.listVersions(id);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@Post(':id/versions/:version/restore')
|
|
67
|
+
async restoreVersion(
|
|
68
|
+
@Param('id', ParseIntPipe) id: number,
|
|
69
|
+
@Param('version', ParseIntPipe) version: number,
|
|
70
|
+
) {
|
|
71
|
+
return this.aiInstructionService.restoreVersion(id, version);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { PrismaModule } from '@hed-hog/api-prisma';
|
|
2
|
+
import { Module, forwardRef } from '@nestjs/common';
|
|
3
|
+
import { AiExecutionService } from './ai-execution.service';
|
|
4
|
+
import { AiInstructionController } from './ai-instruction.controller';
|
|
5
|
+
import { AiInstructionService } from './ai-instruction.service';
|
|
6
|
+
import { AiPromptBuilderService } from './ai-prompt-builder.service';
|
|
7
|
+
|
|
8
|
+
@Module({
|
|
9
|
+
imports: [forwardRef(() => PrismaModule)],
|
|
10
|
+
controllers: [AiInstructionController],
|
|
11
|
+
providers: [AiInstructionService, AiExecutionService, AiPromptBuilderService],
|
|
12
|
+
exports: [AiInstructionService, AiExecutionService, AiPromptBuilderService],
|
|
13
|
+
})
|
|
14
|
+
export class AiInstructionModule {}
|