@hed-hog/core 0.0.364 → 0.0.366

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 (189) hide show
  1. package/dist/ai/ai.controller.d.ts +4 -4
  2. package/dist/ai/ai.module.d.ts.map +1 -1
  3. package/dist/ai/ai.module.js +2 -0
  4. package/dist/ai/ai.module.js.map +1 -1
  5. package/dist/ai/ai.service.d.ts +7 -5
  6. package/dist/ai/ai.service.d.ts.map +1 -1
  7. package/dist/ai/ai.service.js +63 -31
  8. package/dist/ai/ai.service.js.map +1 -1
  9. package/dist/ai-instruction/ai-execution.service.d.ts +45 -0
  10. package/dist/ai-instruction/ai-execution.service.d.ts.map +1 -0
  11. package/dist/ai-instruction/ai-execution.service.js +203 -0
  12. package/dist/ai-instruction/ai-execution.service.js.map +1 -0
  13. package/dist/ai-instruction/ai-instruction.controller.d.ts +25 -0
  14. package/dist/ai-instruction/ai-instruction.controller.d.ts.map +1 -0
  15. package/dist/ai-instruction/ai-instruction.controller.js +117 -0
  16. package/dist/ai-instruction/ai-instruction.controller.js.map +1 -0
  17. package/dist/ai-instruction/ai-instruction.module.d.ts +3 -0
  18. package/dist/ai-instruction/ai-instruction.module.d.ts.map +1 -0
  19. package/dist/ai-instruction/ai-instruction.module.js +27 -0
  20. package/dist/ai-instruction/ai-instruction.module.js.map +1 -0
  21. package/dist/ai-instruction/ai-instruction.service.d.ts +38 -0
  22. package/dist/ai-instruction/ai-instruction.service.d.ts.map +1 -0
  23. package/dist/ai-instruction/ai-instruction.service.js +289 -0
  24. package/dist/ai-instruction/ai-instruction.service.js.map +1 -0
  25. package/dist/ai-instruction/ai-prompt-builder.service.d.ts +53 -0
  26. package/dist/ai-instruction/ai-prompt-builder.service.d.ts.map +1 -0
  27. package/dist/ai-instruction/ai-prompt-builder.service.js +132 -0
  28. package/dist/ai-instruction/ai-prompt-builder.service.js.map +1 -0
  29. package/dist/ai-instruction/dto/create-ai-instruction.dto.d.ts +12 -0
  30. package/dist/ai-instruction/dto/create-ai-instruction.dto.d.ts.map +1 -0
  31. package/dist/ai-instruction/dto/create-ai-instruction.dto.js +57 -0
  32. package/dist/ai-instruction/dto/create-ai-instruction.dto.js.map +1 -0
  33. package/dist/ai-instruction/dto/update-ai-instruction.dto.d.ts +10 -0
  34. package/dist/ai-instruction/dto/update-ai-instruction.dto.d.ts.map +1 -0
  35. package/dist/ai-instruction/dto/update-ai-instruction.dto.js +56 -0
  36. package/dist/ai-instruction/dto/update-ai-instruction.dto.js.map +1 -0
  37. package/dist/ai-instruction/types/ai-instruction.types.d.ts +78 -0
  38. package/dist/ai-instruction/types/ai-instruction.types.d.ts.map +1 -0
  39. package/dist/ai-instruction/types/ai-instruction.types.js +3 -0
  40. package/dist/ai-instruction/types/ai-instruction.types.js.map +1 -0
  41. package/dist/auth/auth.controller.d.ts +13 -5
  42. package/dist/auth/auth.controller.d.ts.map +1 -1
  43. package/dist/auth/auth.controller.js +39 -20
  44. package/dist/auth/auth.controller.js.map +1 -1
  45. package/dist/auth/auth.service.d.ts +18 -7
  46. package/dist/auth/auth.service.d.ts.map +1 -1
  47. package/dist/auth/auth.service.js +20 -16
  48. package/dist/auth/auth.service.js.map +1 -1
  49. package/dist/core.module.d.ts.map +1 -1
  50. package/dist/core.module.js +3 -0
  51. package/dist/core.module.js.map +1 -1
  52. package/dist/file/file.service.d.ts +37 -0
  53. package/dist/file/file.service.d.ts.map +1 -1
  54. package/dist/file/file.service.js +94 -4
  55. package/dist/file/file.service.js.map +1 -1
  56. package/dist/file/provider/abstract.provider.d.ts +6 -2
  57. package/dist/file/provider/abstract.provider.d.ts.map +1 -1
  58. package/dist/file/provider/abstract.provider.js.map +1 -1
  59. package/dist/file/provider/azure.provider.d.ts +4 -1
  60. package/dist/file/provider/azure.provider.d.ts.map +1 -1
  61. package/dist/file/provider/azure.provider.js +7 -2
  62. package/dist/file/provider/azure.provider.js.map +1 -1
  63. package/dist/file/provider/gcs.provider.d.ts +4 -1
  64. package/dist/file/provider/gcs.provider.d.ts.map +1 -1
  65. package/dist/file/provider/gcs.provider.js +7 -2
  66. package/dist/file/provider/gcs.provider.js.map +1 -1
  67. package/dist/file/provider/local.provider.d.ts +2 -2
  68. package/dist/file/provider/local.provider.d.ts.map +1 -1
  69. package/dist/file/provider/local.provider.js +7 -2
  70. package/dist/file/provider/local.provider.js.map +1 -1
  71. package/dist/file/provider/s3.provider.d.ts +4 -1
  72. package/dist/file/provider/s3.provider.d.ts.map +1 -1
  73. package/dist/file/provider/s3.provider.js +9 -6
  74. package/dist/file/provider/s3.provider.js.map +1 -1
  75. package/dist/file/provider/spaces.provider.d.ts +4 -1
  76. package/dist/file/provider/spaces.provider.d.ts.map +1 -1
  77. package/dist/file/provider/spaces.provider.js +9 -5
  78. package/dist/file/provider/spaces.provider.js.map +1 -1
  79. package/dist/integration-profile/integration-profile.controller.d.ts +3 -3
  80. package/dist/integration-profile/integration-profile.service.d.ts +3 -3
  81. package/dist/integration-profile/integration-profile.service.d.ts.map +1 -1
  82. package/dist/integration-profile/integration-profile.service.js +1 -10
  83. package/dist/integration-profile/integration-profile.service.js.map +1 -1
  84. package/dist/mcp-chat/mcp-chat.controller.d.ts +2 -2
  85. package/dist/mcp-chat/mcp-chat.module.d.ts.map +1 -1
  86. package/dist/mcp-chat/mcp-chat.module.js +2 -0
  87. package/dist/mcp-chat/mcp-chat.module.js.map +1 -1
  88. package/dist/mcp-chat/mcp-chat.service.d.ts +8 -3
  89. package/dist/mcp-chat/mcp-chat.service.d.ts.map +1 -1
  90. package/dist/mcp-chat/mcp-chat.service.js +100 -20
  91. package/dist/mcp-chat/mcp-chat.service.js.map +1 -1
  92. package/dist/notification/notification.service.d.ts +1 -0
  93. package/dist/notification/notification.service.d.ts.map +1 -1
  94. package/dist/notification/notification.service.js +29 -1
  95. package/dist/notification/notification.service.js.map +1 -1
  96. package/dist/oauth/oauth.service.d.ts +1 -0
  97. package/dist/oauth/oauth.service.d.ts.map +1 -1
  98. package/dist/oauth/oauth.service.js +55 -1
  99. package/dist/oauth/oauth.service.js.map +1 -1
  100. package/dist/profile/events/profile-avatar-updated.event.d.ts +7 -0
  101. package/dist/profile/events/profile-avatar-updated.event.d.ts.map +1 -0
  102. package/dist/profile/events/profile-avatar-updated.event.js +12 -0
  103. package/dist/profile/events/profile-avatar-updated.event.js.map +1 -0
  104. package/dist/profile/events/profile-email-changed.event.d.ts +7 -0
  105. package/dist/profile/events/profile-email-changed.event.d.ts.map +1 -0
  106. package/dist/profile/events/profile-email-changed.event.js +12 -0
  107. package/dist/profile/events/profile-email-changed.event.js.map +1 -0
  108. package/dist/profile/events/profile-updated.event.d.ts +7 -0
  109. package/dist/profile/events/profile-updated.event.d.ts.map +1 -0
  110. package/dist/profile/events/profile-updated.event.js +12 -0
  111. package/dist/profile/events/profile-updated.event.js.map +1 -0
  112. package/dist/profile/profile.service.d.ts +3 -1
  113. package/dist/profile/profile.service.d.ts.map +1 -1
  114. package/dist/profile/profile.service.js +28 -17
  115. package/dist/profile/profile.service.js.map +1 -1
  116. package/dist/session/session.controller.d.ts +1 -0
  117. package/dist/session/session.controller.d.ts.map +1 -1
  118. package/dist/session/session.service.d.ts +9 -4
  119. package/dist/session/session.service.d.ts.map +1 -1
  120. package/dist/session/session.service.js +61 -51
  121. package/dist/session/session.service.js.map +1 -1
  122. package/dist/setting/setting.controller.d.ts +5 -5
  123. package/dist/setting/setting.service.d.ts +5 -5
  124. package/dist/setting/setting.service.d.ts.map +1 -1
  125. package/dist/setting/setting.service.js +2 -0
  126. package/dist/setting/setting.service.js.map +1 -1
  127. package/dist/task/task.service.d.ts +8 -11
  128. package/dist/task/task.service.d.ts.map +1 -1
  129. package/dist/task/task.service.js +10 -16
  130. package/dist/task/task.service.js.map +1 -1
  131. package/dist/user/user.controller.d.ts +1 -0
  132. package/dist/user/user.controller.d.ts.map +1 -1
  133. package/dist/user/user.service.d.ts +3 -0
  134. package/dist/user/user.service.d.ts.map +1 -1
  135. package/dist/webhook-integration/webhook-command.registry.d.ts +9 -2
  136. package/dist/webhook-integration/webhook-command.registry.d.ts.map +1 -1
  137. package/dist/webhook-integration/webhook-command.registry.js.map +1 -1
  138. package/dist/webhook-integration/webhook-integration.service.d.ts.map +1 -1
  139. package/dist/webhook-integration/webhook-integration.service.js +15 -7
  140. package/dist/webhook-integration/webhook-integration.service.js.map +1 -1
  141. package/hedhog/data/integration_provider.yaml +17 -0
  142. package/hedhog/data/integration_type.yaml +6 -0
  143. package/hedhog/data/route.yaml +86 -2
  144. package/hedhog/frontend/app/configurations/[slug]/components/setting-field.tsx.ejs +109 -0
  145. package/hedhog/frontend/app/configurations/layout.tsx.ejs +6 -6
  146. package/hedhog/frontend/app/integration/profiles/page.tsx.ejs +169 -14
  147. package/hedhog/frontend/messages/en.json +11 -2
  148. package/hedhog/frontend/messages/pt.json +11 -2
  149. package/hedhog/table/ai_execution.yaml +79 -0
  150. package/hedhog/table/ai_execution_step.yaml +65 -0
  151. package/hedhog/table/ai_instruction.yaml +43 -0
  152. package/hedhog/table/ai_instruction_version.yaml +30 -0
  153. package/hedhog/table/ai_model_pricing.yaml +35 -0
  154. package/hedhog/table/user_session.yaml +2 -0
  155. package/package.json +3 -2
  156. package/src/ai/ai.module.ts +2 -0
  157. package/src/ai/ai.service.ts +64 -31
  158. package/src/ai-instruction/ai-execution.service.ts +244 -0
  159. package/src/ai-instruction/ai-instruction.controller.ts +73 -0
  160. package/src/ai-instruction/ai-instruction.module.ts +14 -0
  161. package/src/ai-instruction/ai-instruction.service.ts +343 -0
  162. package/src/ai-instruction/ai-prompt-builder.service.ts +142 -0
  163. package/src/ai-instruction/dto/create-ai-instruction.dto.ts +36 -0
  164. package/src/ai-instruction/dto/update-ai-instruction.dto.ts +35 -0
  165. package/src/ai-instruction/types/ai-instruction.types.ts +90 -0
  166. package/src/auth/auth.controller.ts +23 -5
  167. package/src/auth/auth.service.ts +26 -15
  168. package/src/core.module.ts +3 -0
  169. package/src/file/file.service.ts +136 -4
  170. package/src/file/provider/abstract.provider.ts +4 -2
  171. package/src/file/provider/azure.provider.ts +6 -3
  172. package/src/file/provider/gcs.provider.ts +5 -3
  173. package/src/file/provider/local.provider.ts +7 -3
  174. package/src/file/provider/s3.provider.ts +8 -6
  175. package/src/file/provider/spaces.provider.ts +8 -7
  176. package/src/integration-profile/integration-profile.service.ts +1 -10
  177. package/src/mcp-chat/mcp-chat.module.ts +19 -17
  178. package/src/mcp-chat/mcp-chat.service.ts +95 -9
  179. package/src/notification/notification.service.ts +32 -1
  180. package/src/oauth/oauth.service.ts +74 -1
  181. package/src/profile/events/profile-avatar-updated.event.ts +8 -0
  182. package/src/profile/events/profile-email-changed.event.ts +8 -0
  183. package/src/profile/events/profile-updated.event.ts +8 -0
  184. package/src/profile/profile.service.ts +25 -16
  185. package/src/session/session.service.ts +70 -52
  186. package/src/setting/setting.service.ts +10 -8
  187. package/src/task/task.service.ts +10 -16
  188. package/src/webhook-integration/webhook-command.registry.ts +10 -2
  189. package/src/webhook-integration/webhook-integration.service.ts +33 -6
@@ -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
- if (provider === 'openai') {
73
- const model = data.model || 'gpt-4o-mini';
74
- const content = await this.chatWithOpenAi({
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
- return {
82
- provider,
111
+ await this.aiExecution.recordStep({
112
+ executionId,
113
+ stepOrder: 0,
114
+ type: 'output',
115
+ provider: 'gemini',
83
116
  model,
84
- content,
85
- };
86
- }
87
-
88
- const model = data.model || 'gemini-1.5-flash';
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
- return {
97
- provider,
98
- model,
99
- content,
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
- return response.data?.choices?.[0]?.message?.content || '';
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
- return parts.map((part: any) => part.text || '').join('');
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 {}