@hed-hog/core 0.0.400 → 0.0.402

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 (274) hide show
  1. package/dist/access-log/access-log.interceptor.d.ts +10 -0
  2. package/dist/access-log/access-log.interceptor.d.ts.map +1 -1
  3. package/dist/access-log/access-log.interceptor.js +15 -1
  4. package/dist/access-log/access-log.interceptor.js.map +1 -1
  5. package/dist/ai/ai.controller.d.ts +2 -0
  6. package/dist/ai/ai.controller.d.ts.map +1 -1
  7. package/dist/ai/ai.service.d.ts +86 -2
  8. package/dist/ai/ai.service.d.ts.map +1 -1
  9. package/dist/ai/ai.service.js +304 -23
  10. package/dist/ai/ai.service.js.map +1 -1
  11. package/dist/ai-instruction/ai-prompt-resolver.service.d.ts +12 -0
  12. package/dist/ai-instruction/ai-prompt-resolver.service.d.ts.map +1 -1
  13. package/dist/ai-instruction/ai-prompt-resolver.service.js +26 -0
  14. package/dist/ai-instruction/ai-prompt-resolver.service.js.map +1 -1
  15. package/dist/core.module.d.ts.map +1 -1
  16. package/dist/core.module.js +3 -0
  17. package/dist/core.module.js.map +1 -1
  18. package/dist/document-kit/document-kit.module.d.ts +3 -0
  19. package/dist/document-kit/document-kit.module.d.ts.map +1 -0
  20. package/dist/document-kit/document-kit.module.js +24 -0
  21. package/dist/document-kit/document-kit.module.js.map +1 -0
  22. package/dist/document-kit/document-kit.registry.d.ts +55 -0
  23. package/dist/document-kit/document-kit.registry.d.ts.map +1 -0
  24. package/dist/document-kit/document-kit.registry.js +91 -0
  25. package/dist/document-kit/document-kit.registry.js.map +1 -0
  26. package/dist/document-kit/document-kit.service.d.ts +49 -0
  27. package/dist/document-kit/document-kit.service.d.ts.map +1 -0
  28. package/dist/document-kit/document-kit.service.js +135 -0
  29. package/dist/document-kit/document-kit.service.js.map +1 -0
  30. package/dist/file/file-upload-ticket.service.d.ts +1 -1
  31. package/dist/file/file.controller.d.ts +3 -3
  32. package/dist/file/file.service.d.ts +136 -13
  33. package/dist/file/file.service.d.ts.map +1 -1
  34. package/dist/file/file.service.js +297 -27
  35. package/dist/file/file.service.js.map +1 -1
  36. package/dist/file/mcp-file-source.schema.d.ts.map +1 -1
  37. package/dist/file/mcp-file-source.schema.js +8 -5
  38. package/dist/file/mcp-file-source.schema.js.map +1 -1
  39. package/dist/file/provider/abstract.provider.d.ts +33 -0
  40. package/dist/file/provider/abstract.provider.d.ts.map +1 -1
  41. package/dist/file/provider/abstract.provider.js +9 -0
  42. package/dist/file/provider/abstract.provider.js.map +1 -1
  43. package/dist/file/provider/s3-multipart.util.d.ts +17 -0
  44. package/dist/file/provider/s3-multipart.util.d.ts.map +1 -0
  45. package/dist/file/provider/s3-multipart.util.js +88 -0
  46. package/dist/file/provider/s3-multipart.util.js.map +1 -0
  47. package/dist/file/provider/s3.provider.d.ts +9 -1
  48. package/dist/file/provider/s3.provider.d.ts.map +1 -1
  49. package/dist/file/provider/s3.provider.js +16 -0
  50. package/dist/file/provider/s3.provider.js.map +1 -1
  51. package/dist/file/provider/spaces.provider.d.ts +10 -1
  52. package/dist/file/provider/spaces.provider.d.ts.map +1 -1
  53. package/dist/file/provider/spaces.provider.js +17 -0
  54. package/dist/file/provider/spaces.provider.js.map +1 -1
  55. package/dist/index.d.ts +5 -0
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/index.js +5 -0
  58. package/dist/index.js.map +1 -1
  59. package/dist/integration-profile/integration-profile.controller.d.ts +1 -0
  60. package/dist/integration-profile/integration-profile.controller.d.ts.map +1 -1
  61. package/dist/integration-profile/integration-profile.secrets.d.ts.map +1 -1
  62. package/dist/integration-profile/integration-profile.secrets.js +26 -0
  63. package/dist/integration-profile/integration-profile.secrets.js.map +1 -1
  64. package/dist/integration-profile/integration-profile.service.d.ts +1 -0
  65. package/dist/integration-profile/integration-profile.service.d.ts.map +1 -1
  66. package/dist/integration-profile/integration-profile.service.js +3 -3
  67. package/dist/integration-profile/integration-profile.service.js.map +1 -1
  68. package/dist/integration-profile/testers/ai.testers.d.ts.map +1 -1
  69. package/dist/integration-profile/testers/ai.testers.js +46 -8
  70. package/dist/integration-profile/testers/ai.testers.js.map +1 -1
  71. package/dist/integration-profile/testers/http-probe.d.ts.map +1 -1
  72. package/dist/integration-profile/testers/http-probe.js +5 -3
  73. package/dist/integration-profile/testers/http-probe.js.map +1 -1
  74. package/dist/integration-profile/testers/integration-tester.types.d.ts +5 -0
  75. package/dist/integration-profile/testers/integration-tester.types.d.ts.map +1 -1
  76. package/dist/mail/mail.service.d.ts.map +1 -1
  77. package/dist/mail/mail.service.js +16 -1
  78. package/dist/mail/mail.service.js.map +1 -1
  79. package/dist/mcp/mcp-server.service.d.ts.map +1 -1
  80. package/dist/mcp/mcp-server.service.js +10 -1
  81. package/dist/mcp/mcp-server.service.js.map +1 -1
  82. package/dist/mcp/mcp.module.d.ts.map +1 -1
  83. package/dist/mcp/mcp.module.js +2 -0
  84. package/dist/mcp/mcp.module.js.map +1 -1
  85. package/dist/mcp/tools/core-access-log.mcp-tools.d.ts +17 -0
  86. package/dist/mcp/tools/core-access-log.mcp-tools.d.ts.map +1 -0
  87. package/dist/mcp/tools/core-access-log.mcp-tools.js +74 -0
  88. package/dist/mcp/tools/core-access-log.mcp-tools.js.map +1 -0
  89. package/dist/mcp/tools/core-files.mcp-tools.js +4 -4
  90. package/dist/mcp/tools/core-files.mcp-tools.js.map +1 -1
  91. package/dist/meeting/meeting-provider.registry.d.ts +72 -0
  92. package/dist/meeting/meeting-provider.registry.d.ts.map +1 -1
  93. package/dist/meeting/meeting-provider.registry.js.map +1 -1
  94. package/dist/oauth/oauth-token-broker.service.d.ts +50 -1
  95. package/dist/oauth/oauth-token-broker.service.d.ts.map +1 -1
  96. package/dist/oauth/oauth-token-broker.service.js +105 -3
  97. package/dist/oauth/oauth-token-broker.service.js.map +1 -1
  98. package/dist/oauth/providers/microsoft-entra-id.provider.d.ts +6 -0
  99. package/dist/oauth/providers/microsoft-entra-id.provider.d.ts.map +1 -1
  100. package/dist/oauth/providers/microsoft-entra-id.provider.js +30 -4
  101. package/dist/oauth/providers/microsoft-entra-id.provider.js.map +1 -1
  102. package/dist/pdf/document/charts/chart-bar.d.ts +24 -0
  103. package/dist/pdf/document/charts/chart-bar.d.ts.map +1 -0
  104. package/dist/pdf/document/charts/chart-bar.js +117 -0
  105. package/dist/pdf/document/charts/chart-bar.js.map +1 -0
  106. package/dist/pdf/document/charts/chart-donut.d.ts +16 -0
  107. package/dist/pdf/document/charts/chart-donut.d.ts.map +1 -0
  108. package/dist/pdf/document/charts/chart-donut.js +89 -0
  109. package/dist/pdf/document/charts/chart-donut.js.map +1 -0
  110. package/dist/pdf/document/charts/chart-gauge.d.ts +15 -0
  111. package/dist/pdf/document/charts/chart-gauge.d.ts.map +1 -0
  112. package/dist/pdf/document/charts/chart-gauge.js +60 -0
  113. package/dist/pdf/document/charts/chart-gauge.js.map +1 -0
  114. package/dist/pdf/document/charts/chart-line.d.ts +29 -0
  115. package/dist/pdf/document/charts/chart-line.d.ts.map +1 -0
  116. package/dist/pdf/document/charts/chart-line.js +89 -0
  117. package/dist/pdf/document/charts/chart-line.js.map +1 -0
  118. package/dist/pdf/document/charts/chart-primitives.d.ts +64 -0
  119. package/dist/pdf/document/charts/chart-primitives.d.ts.map +1 -0
  120. package/dist/pdf/document/charts/chart-primitives.js +110 -0
  121. package/dist/pdf/document/charts/chart-primitives.js.map +1 -0
  122. package/dist/pdf/document/charts/chart-radar.d.ts +13 -0
  123. package/dist/pdf/document/charts/chart-radar.d.ts.map +1 -0
  124. package/dist/pdf/document/charts/chart-radar.js +83 -0
  125. package/dist/pdf/document/charts/chart-radar.js.map +1 -0
  126. package/dist/pdf/document/charts/chart-scale.d.ts +39 -0
  127. package/dist/pdf/document/charts/chart-scale.d.ts.map +1 -0
  128. package/dist/pdf/document/charts/chart-scale.js +118 -0
  129. package/dist/pdf/document/charts/chart-scale.js.map +1 -0
  130. package/dist/pdf/document/charts/index.d.ts +8 -0
  131. package/dist/pdf/document/charts/index.d.ts.map +1 -0
  132. package/dist/pdf/document/charts/index.js +24 -0
  133. package/dist/pdf/document/charts/index.js.map +1 -0
  134. package/dist/pdf/document/index.d.ts +8 -0
  135. package/dist/pdf/document/index.d.ts.map +1 -0
  136. package/dist/pdf/document/index.js +24 -0
  137. package/dist/pdf/document/index.js.map +1 -0
  138. package/dist/pdf/document/pdf-document.chrome.d.ts +20 -0
  139. package/dist/pdf/document/pdf-document.chrome.d.ts.map +1 -0
  140. package/dist/pdf/document/pdf-document.chrome.js +36 -0
  141. package/dist/pdf/document/pdf-document.chrome.js.map +1 -0
  142. package/dist/pdf/document/pdf-document.css.d.ts +20 -0
  143. package/dist/pdf/document/pdf-document.css.d.ts.map +1 -0
  144. package/dist/pdf/document/pdf-document.css.js +327 -0
  145. package/dist/pdf/document/pdf-document.css.js.map +1 -0
  146. package/dist/pdf/document/pdf-document.renderer.d.ts +10 -0
  147. package/dist/pdf/document/pdf-document.renderer.d.ts.map +1 -0
  148. package/dist/pdf/document/pdf-document.renderer.js +226 -0
  149. package/dist/pdf/document/pdf-document.renderer.js.map +1 -0
  150. package/dist/pdf/document/pdf-document.service.d.ts +27 -0
  151. package/dist/pdf/document/pdf-document.service.d.ts.map +1 -0
  152. package/dist/pdf/document/pdf-document.service.js +81 -0
  153. package/dist/pdf/document/pdf-document.service.js.map +1 -0
  154. package/dist/pdf/document/pdf-document.theme.d.ts +41 -0
  155. package/dist/pdf/document/pdf-document.theme.d.ts.map +1 -0
  156. package/dist/pdf/document/pdf-document.theme.js +105 -0
  157. package/dist/pdf/document/pdf-document.theme.js.map +1 -0
  158. package/dist/pdf/document/pdf-document.types.d.ts +164 -0
  159. package/dist/pdf/document/pdf-document.types.d.ts.map +1 -0
  160. package/dist/pdf/document/pdf-document.types.js +18 -0
  161. package/dist/pdf/document/pdf-document.types.js.map +1 -0
  162. package/dist/pdf/pdf.module.d.ts.map +1 -1
  163. package/dist/pdf/pdf.module.js +3 -2
  164. package/dist/pdf/pdf.module.js.map +1 -1
  165. package/dist/pdf/pdf.service.d.ts +75 -1
  166. package/dist/pdf/pdf.service.d.ts.map +1 -1
  167. package/dist/pdf/pdf.service.js +167 -63
  168. package/dist/pdf/pdf.service.js.map +1 -1
  169. package/dist/pdf/templates/commercial-proposal.template.d.ts +96 -0
  170. package/dist/pdf/templates/commercial-proposal.template.d.ts.map +1 -0
  171. package/dist/pdf/templates/commercial-proposal.template.js +276 -0
  172. package/dist/pdf/templates/commercial-proposal.template.js.map +1 -0
  173. package/dist/setting/favicon.controller.d.ts +26 -0
  174. package/dist/setting/favicon.controller.d.ts.map +1 -0
  175. package/dist/setting/favicon.controller.js +130 -0
  176. package/dist/setting/favicon.controller.js.map +1 -0
  177. package/dist/setting/setting.module.d.ts.map +1 -1
  178. package/dist/setting/setting.module.js +2 -1
  179. package/dist/setting/setting.module.js.map +1 -1
  180. package/dist/setting/setting.service.d.ts.map +1 -1
  181. package/dist/setting/setting.service.js +3 -0
  182. package/dist/setting/setting.service.js.map +1 -1
  183. package/dist/task/task.service.d.ts +6 -0
  184. package/dist/task/task.service.d.ts.map +1 -1
  185. package/dist/task/task.service.js +44 -13
  186. package/dist/task/task.service.js.map +1 -1
  187. package/dist/whatsapp-sender/whatsapp-sender.service.d.ts +35 -2
  188. package/dist/whatsapp-sender/whatsapp-sender.service.d.ts.map +1 -1
  189. package/dist/whatsapp-sender/whatsapp-sender.service.js +107 -22
  190. package/dist/whatsapp-sender/whatsapp-sender.service.js.map +1 -1
  191. package/hedhog/data/integration_type.yaml +6 -0
  192. package/hedhog/data/route.yaml +8 -0
  193. package/hedhog/data/setting_group.yaml +52 -0
  194. package/hedhog/frontend/app/configurations/[slug]/components/setting-field.test.tsx.ejs +85 -2
  195. package/hedhog/frontend/app/configurations/[slug]/components/setting-field.tsx.ejs +576 -491
  196. package/hedhog/frontend/app/users/simulate-access-dialog.test.tsx.ejs +29 -50
  197. package/hedhog/frontend/app/users/simulate-access-dialog.tsx.ejs +146 -50
  198. package/hedhog/frontend/messages/en.json +81 -2
  199. package/hedhog/frontend/messages/pt.json +81 -2
  200. package/hedhog/table/file.yaml +3 -1
  201. package/package.json +5 -5
  202. package/src/access-log/access-log.interceptor.spec.ts +50 -0
  203. package/src/access-log/access-log.interceptor.ts +18 -0
  204. package/src/ai/ai-attachment.fragments.spec.ts +56 -3
  205. package/src/ai/ai.service.responses.spec.ts +429 -0
  206. package/src/ai/ai.service.ts +409 -27
  207. package/src/ai-instruction/ai-prompt-resolver.service.spec.ts +44 -0
  208. package/src/ai-instruction/ai-prompt-resolver.service.ts +29 -0
  209. package/src/core.module.ts +3 -0
  210. package/src/document-kit/document-kit.module.ts +12 -0
  211. package/src/document-kit/document-kit.registry.ts +125 -0
  212. package/src/document-kit/document-kit.service.spec.ts +156 -0
  213. package/src/document-kit/document-kit.service.ts +173 -0
  214. package/src/file/file-direct-upload.spec.ts +344 -0
  215. package/src/file/file.service.spec.ts +168 -1
  216. package/src/file/file.service.ts +435 -36
  217. package/src/file/mcp-file-source.schema.ts +8 -5
  218. package/src/file/provider/abstract.provider.ts +57 -0
  219. package/src/file/provider/s3-multipart.spec.ts +123 -0
  220. package/src/file/provider/s3-multipart.util.ts +126 -0
  221. package/src/file/provider/s3.provider.ts +65 -1
  222. package/src/file/provider/spaces.provider.ts +67 -1
  223. package/src/index.ts +5 -0
  224. package/src/integration-profile/integration-profile.secrets.ts +26 -0
  225. package/src/integration-profile/integration-profile.service.ts +1 -0
  226. package/src/integration-profile/testers/ai.testers.ts +70 -5
  227. package/src/integration-profile/testers/http-probe.ts +5 -3
  228. package/src/integration-profile/testers/integration-tester.types.ts +5 -0
  229. package/src/integration-profile/testers/oauth.testers.spec.ts +13 -0
  230. package/src/integration-profile/testers/remaining.testers.spec.ts +65 -1
  231. package/src/integration-profile/testers/tester-coverage.spec.ts +12 -0
  232. package/src/mail/mail.service.spec.ts +25 -0
  233. package/src/mail/mail.service.ts +18 -1
  234. package/src/mcp/mcp-server.service.spec.ts +16 -0
  235. package/src/mcp/mcp-server.service.ts +10 -1
  236. package/src/mcp/mcp.module.ts +2 -0
  237. package/src/mcp/tools/core-access-log.mcp-tools.ts +66 -0
  238. package/src/mcp/tools/core-files.mcp-tools.ts +4 -4
  239. package/src/meeting/meeting-provider.registry.ts +80 -0
  240. package/src/oauth/oauth-token-broker.service.spec.ts +108 -0
  241. package/src/oauth/oauth-token-broker.service.ts +129 -2
  242. package/src/oauth/providers/microsoft-entra-id.provider.spec.ts +133 -0
  243. package/src/oauth/providers/microsoft-entra-id.provider.ts +29 -2
  244. package/src/pdf/document/charts/chart-bar.ts +217 -0
  245. package/src/pdf/document/charts/chart-donut.ts +163 -0
  246. package/src/pdf/document/charts/chart-gauge.ts +94 -0
  247. package/src/pdf/document/charts/chart-line.ts +159 -0
  248. package/src/pdf/document/charts/chart-primitives.ts +188 -0
  249. package/src/pdf/document/charts/chart-radar.ts +136 -0
  250. package/src/pdf/document/charts/chart-scale.spec.ts +93 -0
  251. package/src/pdf/document/charts/chart-scale.ts +141 -0
  252. package/src/pdf/document/charts/charts.spec.ts +157 -0
  253. package/src/pdf/document/charts/index.ts +7 -0
  254. package/src/pdf/document/index.ts +7 -0
  255. package/src/pdf/document/pdf-document.chrome.ts +38 -0
  256. package/src/pdf/document/pdf-document.css.ts +326 -0
  257. package/src/pdf/document/pdf-document.renderer.spec.ts +172 -0
  258. package/src/pdf/document/pdf-document.renderer.ts +317 -0
  259. package/src/pdf/document/pdf-document.service.ts +77 -0
  260. package/src/pdf/document/pdf-document.theme.ts +146 -0
  261. package/src/pdf/document/pdf-document.types.ts +149 -0
  262. package/src/pdf/pdf.module.ts +3 -2
  263. package/src/pdf/pdf.service.spec.ts +131 -0
  264. package/src/pdf/pdf.service.ts +256 -82
  265. package/src/pdf/templates/commercial-proposal.template.spec.ts +177 -0
  266. package/src/pdf/templates/commercial-proposal.template.ts +400 -0
  267. package/src/setting/favicon.controller.spec.ts +92 -0
  268. package/src/setting/favicon.controller.ts +135 -0
  269. package/src/setting/setting.module.ts +2 -1
  270. package/src/setting/setting.service.ts +3 -0
  271. package/src/task/task.service.spec.ts +77 -1
  272. package/src/task/task.service.ts +58 -14
  273. package/src/whatsapp-sender/whatsapp-sender.service.spec.ts +263 -0
  274. package/src/whatsapp-sender/whatsapp-sender.service.ts +154 -16
@@ -255,6 +255,35 @@ export function applyOpenAiModelParams(
255
255
  return result;
256
256
  }
257
257
 
258
+ /**
259
+ * Same as `applyOpenAiModelParams`, but for `POST /v1/responses`.
260
+ *
261
+ * That endpoint renames three things, and this is the single place the rename
262
+ * lives: the messages array is `input`, `max_tokens` is `max_output_tokens`,
263
+ * and `response_format` moved under `text.format`. The system prompt is not a
264
+ * message either — it is the top-level `instructions`, handled by the caller.
265
+ *
266
+ * Same contract as the sibling: a key is only written when there is a value, so
267
+ * a body built with no params stays exactly `{ model, input }`.
268
+ */
269
+ export function applyOpenAiResponsesParams(
270
+ body: Record<string, unknown>,
271
+ params: ModelParams,
272
+ defaults: OpenAiParamDefaults,
273
+ ): Record<string, unknown> {
274
+ const model = params.model || defaults.model;
275
+ const temperature = params.temperature ?? defaults.temperature;
276
+ const maxTokens = params.maxTokens ?? defaults.maxTokens;
277
+
278
+ const result: Record<string, unknown> = { model, ...body };
279
+
280
+ if (temperature != null) result.temperature = temperature;
281
+ if (maxTokens != null) result.max_output_tokens = maxTokens;
282
+ if (defaults.jsonObject) result.text = { format: { type: 'json_object' } };
283
+
284
+ return result;
285
+ }
286
+
258
287
  export function applyGeminiModelParams(
259
288
  payload: Record<string, unknown>,
260
289
  params: ModelParams,
@@ -26,6 +26,7 @@ import { LeakedPasswordService } from './leaked-password/leaked-password.service
26
26
  import { MailLayoutModule } from './mail-layout/mail-layout.module';
27
27
  import { MailModule } from './mail/mail.module';
28
28
  import { MessageTemplateModule } from './message-template/message-template.module';
29
+ import { DocumentKitModule } from './document-kit/document-kit.module';
29
30
  import { McpChatModule } from './mcp-chat/mcp-chat.module';
30
31
  import { McpModule } from './mcp/mcp.module';
31
32
  import { MenuModule } from './menu/menu.module';
@@ -90,6 +91,7 @@ import { WhatsappSenderModule } from './whatsapp-sender/whatsapp-sender.module';
90
91
  forwardRef(() => LeakedPasswordModule),
91
92
  forwardRef(() => MailModule),
92
93
  forwardRef(() => MessageTemplateModule),
94
+ forwardRef(() => DocumentKitModule),
93
95
  forwardRef(() => MailLayoutModule),
94
96
  forwardRef(() => IntegrationProfileModule),
95
97
  forwardRef(() => WhatsappSenderModule),
@@ -125,6 +127,7 @@ import { WhatsappSenderModule } from './whatsapp-sender/whatsapp-sender.module';
125
127
  LeakedPasswordModule,
126
128
  MailModule,
127
129
  MessageTemplateModule,
130
+ DocumentKitModule,
128
131
  MailLayoutModule,
129
132
  IntegrationProfileModule,
130
133
  WhatsappSenderModule,
@@ -0,0 +1,12 @@
1
+ import { forwardRef, Module } from '@nestjs/common';
2
+
3
+ import { AiInstructionModule } from '../ai-instruction/ai-instruction.module';
4
+ import { AiModule } from '../ai/ai.module';
5
+ import { DocumentKitService } from './document-kit.service';
6
+
7
+ @Module({
8
+ imports: [forwardRef(() => AiModule), forwardRef(() => AiInstructionModule)],
9
+ providers: [DocumentKitService],
10
+ exports: [DocumentKitService],
11
+ })
12
+ export class DocumentKitModule {}
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Document kits: what a library needs to have a document drafted by AI and
3
+ * printed with a core template — the prompts, the reference texts the prompts
4
+ * lean on (catalog, price table, writing rules) and the sections the document
5
+ * is made of.
6
+ *
7
+ * Only code, like the message templates: the editable parts are the prompts and
8
+ * references, which are `ai_instruction` rows and already have their own screen
9
+ * (Core › AI Instructions), with versions and "restore default". A kit only ties
10
+ * them together.
11
+ *
12
+ * Libraries register at import time, from their `*.prompts.ts`:
13
+ *
14
+ * registerDocumentKits([{ slug: 'crm.commercial-proposal', ... }]);
15
+ */
16
+
17
+ export type DocumentKitTemplate = 'commercial-proposal';
18
+
19
+ export type DocumentKitSection = {
20
+ /** Stable id: the key the model answers with and the one stored with the document. */
21
+ key: string;
22
+ /** Short label next to the section number (`02 · ESCOPO`). */
23
+ eyebrow: Record<string, string>;
24
+ /** Default title, a short sentence; the model may propose a better one. */
25
+ title: Record<string, string>;
26
+ /** What goes in the section, as told to the model. */
27
+ guidance: string;
28
+ };
29
+
30
+ export type DocumentKitDefinition = {
31
+ /** `<module>.<name>`, e.g. `crm.commercial-proposal`. */
32
+ slug: string;
33
+ /** Must equal the first slug segment. */
34
+ moduleSlug: string;
35
+ /** 'pt' required. */
36
+ name: Record<string, string>;
37
+ template: DocumentKitTemplate;
38
+ /** `ai_instruction` slugs of the drafting call. */
39
+ prompts: { system: string; user: string };
40
+ /**
41
+ * Prompt variable → `ai_instruction` slug (`.raw`) whose content is handed to
42
+ * the prompts under that name. Lets the business content be edited apart from
43
+ * the instructions.
44
+ */
45
+ references: Record<string, string>;
46
+ sections: DocumentKitSection[];
47
+ };
48
+
49
+ const SLUG_RE = /^([a-z0-9-]+)\.([a-z0-9-]+)$/;
50
+ const KEY_RE = /^[a-z][a-z0-9_]*$/;
51
+
52
+ const registry = new Map<string, DocumentKitDefinition>();
53
+
54
+ function assertDefinition(definition: DocumentKitDefinition) {
55
+ const match = SLUG_RE.exec(definition.slug);
56
+ if (!match) {
57
+ throw new Error(`Invalid document kit slug "${definition.slug}". Expected "<module>.<name>".`);
58
+ }
59
+ if (match[1] !== definition.moduleSlug) {
60
+ throw new Error(
61
+ `Document kit "${definition.slug}" declares moduleSlug "${definition.moduleSlug}" but its slug says "${match[1]}".`,
62
+ );
63
+ }
64
+ if (!definition.name?.pt) {
65
+ throw new Error(`Document kit "${definition.slug}" needs a 'pt' name.`);
66
+ }
67
+ if (!definition.prompts?.system || !definition.prompts?.user) {
68
+ throw new Error(`Document kit "${definition.slug}" needs a system and a user prompt.`);
69
+ }
70
+ if (!definition.sections?.length) {
71
+ throw new Error(`Document kit "${definition.slug}" has no sections.`);
72
+ }
73
+
74
+ const keys = new Set<string>();
75
+ for (const section of definition.sections) {
76
+ if (!KEY_RE.test(section.key)) {
77
+ throw new Error(`Document kit "${definition.slug}" has an invalid section key "${section.key}".`);
78
+ }
79
+ if (keys.has(section.key)) {
80
+ throw new Error(`Document kit "${definition.slug}" repeats the section key "${section.key}".`);
81
+ }
82
+ if (!section.eyebrow?.pt || !section.title?.pt) {
83
+ throw new Error(`Section "${section.key}" of "${definition.slug}" needs a 'pt' eyebrow and title.`);
84
+ }
85
+ keys.add(section.key);
86
+ }
87
+
88
+ for (const variable of Object.keys(definition.references ?? {})) {
89
+ if (variable === 'sectionsSpec') {
90
+ throw new Error(`Document kit "${definition.slug}" cannot name a reference "sectionsSpec".`);
91
+ }
92
+ }
93
+ }
94
+
95
+ export function registerDocumentKits(definitions: DocumentKitDefinition[]): void {
96
+ for (const definition of definitions) {
97
+ assertDefinition(definition);
98
+ const existing = registry.get(definition.slug);
99
+ if (existing && existing !== definition) {
100
+ throw new Error(`Document kit "${definition.slug}" is already registered.`);
101
+ }
102
+ registry.set(definition.slug, definition);
103
+ }
104
+ }
105
+
106
+ export function getDocumentKit(slug: string): DocumentKitDefinition | undefined {
107
+ return registry.get(slug);
108
+ }
109
+
110
+ export function listDocumentKits(moduleSlug?: string): DocumentKitDefinition[] {
111
+ const all = [...registry.values()];
112
+ const filtered = moduleSlug ? all.filter((kit) => kit.moduleSlug === moduleSlug) : all;
113
+ return filtered.sort((a, b) => a.slug.localeCompare(b.slug));
114
+ }
115
+
116
+ /** Test helper. */
117
+ export function clearDocumentKitRegistry(): void {
118
+ registry.clear();
119
+ }
120
+
121
+ /** Text of a localized label, falling back to 'pt'. */
122
+ export function pickLocale(value: Record<string, string>, locale?: string): string {
123
+ const short = String(locale || 'pt').toLowerCase().split('-')[0];
124
+ return value[short] ?? value.pt ?? Object.values(value)[0] ?? '';
125
+ }
@@ -0,0 +1,156 @@
1
+ import { BadGatewayException, NotFoundException } from '@nestjs/common';
2
+ import { afterEach, beforeEach, describe, expect, it, jest } from '@jest/globals';
3
+
4
+ import {
5
+ DocumentKitDefinition,
6
+ clearDocumentKitRegistry,
7
+ getDocumentKit,
8
+ registerDocumentKits,
9
+ } from './document-kit.registry';
10
+ import { DocumentKitService, extractJsonObject } from './document-kit.service';
11
+
12
+ const kit = (overrides: Partial<DocumentKitDefinition> = {}): DocumentKitDefinition => ({
13
+ slug: 'crm.commercial-proposal',
14
+ moduleSlug: 'crm',
15
+ name: { pt: 'Proposta comercial' },
16
+ template: 'commercial-proposal',
17
+ prompts: { system: 'task.crm.proposal-draft.compose.system', user: 'task.crm.proposal-draft.compose.user' },
18
+ references: { catalog: 'task.crm.proposal-draft.catalog.raw' },
19
+ sections: [
20
+ { key: 'scope', eyebrow: { pt: 'Escopo', en: 'Scope' }, title: { pt: 'O que entra.', en: 'What is in.' }, guidance: 'O que está incluído.' },
21
+ { key: 'program', eyebrow: { pt: 'Programa' }, title: { pt: 'Como é o dia.' }, guidance: 'Blocos do programa.' },
22
+ ],
23
+ ...overrides,
24
+ });
25
+
26
+ describe('registerDocumentKits', () => {
27
+ afterEach(() => clearDocumentKitRegistry());
28
+
29
+ it('registra e devolve o kit pelo slug', () => {
30
+ registerDocumentKits([kit()]);
31
+ expect(getDocumentKit('crm.commercial-proposal')?.sections).toHaveLength(2);
32
+ });
33
+
34
+ it('recusa slug fora do formato e módulo divergente', () => {
35
+ expect(() => registerDocumentKits([kit({ slug: 'proposal' })])).toThrow(/Invalid document kit slug/);
36
+ expect(() => registerDocumentKits([kit({ moduleSlug: 'lms' })])).toThrow(/declares moduleSlug/);
37
+ });
38
+
39
+ it('recusa seção repetida, chave inválida e kit sem seção', () => {
40
+ const section = kit().sections[0];
41
+ expect(() => registerDocumentKits([kit({ sections: [section, section] })])).toThrow(/repeats/);
42
+ expect(() => registerDocumentKits([kit({ sections: [{ ...section, key: 'Escopo' }] })])).toThrow(/invalid section key/);
43
+ expect(() => registerDocumentKits([kit({ sections: [] })])).toThrow(/no sections/);
44
+ });
45
+
46
+ it('recusa outro kit com o mesmo slug', () => {
47
+ registerDocumentKits([kit()]);
48
+ expect(() => registerDocumentKits([kit()])).toThrow(/already registered/);
49
+ });
50
+ });
51
+
52
+ describe('extractJsonObject', () => {
53
+ it('lê JSON puro, com cerca de código e com texto em volta', () => {
54
+ expect(extractJsonObject('{"a":1}')).toEqual({ a: 1 });
55
+ expect(extractJsonObject('```json\n{"a":1}\n```')).toEqual({ a: 1 });
56
+ expect(extractJsonObject('Segue a proposta:\n{"a":{"b":2}}\nBoa sorte.')).toEqual({ a: { b: 2 } });
57
+ });
58
+
59
+ it('devolve null para texto sem objeto ou JSON quebrado', () => {
60
+ expect(extractJsonObject('não consegui')).toBeNull();
61
+ expect(extractJsonObject('{"a":')).toBeNull();
62
+ expect(extractJsonObject('[1,2]')).toBeNull();
63
+ });
64
+ });
65
+
66
+ describe('DocumentKitService.draft', () => {
67
+ let chatWithPrompt: jest.Mock<(...args: any[]) => Promise<any>>;
68
+ let resolveMany: jest.Mock<(...args: any[]) => Promise<any>>;
69
+ let service: DocumentKitService;
70
+
71
+ beforeEach(() => {
72
+ registerDocumentKits([kit()]);
73
+ chatWithPrompt = jest.fn();
74
+ resolveMany = jest.fn(async () => ({
75
+ 'task.crm.proposal-draft.catalog.raw': { content: ' Workshop 4h ' },
76
+ }));
77
+ service = new DocumentKitService({ chatWithPrompt } as any, { resolveMany } as any);
78
+ });
79
+
80
+ afterEach(() => clearDocumentKitRegistry());
81
+
82
+ const answer = (content: string) =>
83
+ chatWithPrompt.mockResolvedValue({ provider: 'claude', model: 'claude-sonnet-4-5', content, executionId: 9 });
84
+
85
+ it('passa referências, seções e variáveis do chamador aos prompts, pedindo JSON', async () => {
86
+ answer('{"summary_html":"<p>Oi</p>","sections":{}}');
87
+
88
+ await service.draft('crm.commercial-proposal', { variables: { clientName: 'ArcelorMittal' }, locale: 'pt-BR' });
89
+
90
+ const call = chatWithPrompt.mock.calls[0][0];
91
+ expect(call).toMatchObject({
92
+ systemPromptSlug: 'task.crm.proposal-draft.compose.system',
93
+ userPromptSlug: 'task.crm.proposal-draft.compose.user',
94
+ params: { responseFormat: 'json_object' },
95
+ contextSlug: 'document-kit.crm.commercial-proposal',
96
+ });
97
+ expect(call.variables.catalog).toBe('Workshop 4h');
98
+ expect(call.variables.clientName).toBe('ArcelorMittal');
99
+ expect(call.variables.sectionsSpec).toContain('- scope — "O que entra.": O que está incluído.');
100
+ });
101
+
102
+ it('devolve as seções na ordem do kit, sem chave desconhecida nem seção vazia', async () => {
103
+ answer(
104
+ '```json\n' +
105
+ JSON.stringify({
106
+ summary_html: '<p>Resumo</p>',
107
+ sections: {
108
+ program: { title: 'Um dia de prática.', html: '<p>Blocos</p>' },
109
+ invented: { html: '<p>fora do kit</p>' },
110
+ scope: { html: '<p>Tudo</p>' },
111
+ },
112
+ }) +
113
+ '\n```',
114
+ );
115
+
116
+ const draft = await service.draft('crm.commercial-proposal', { locale: 'en' });
117
+
118
+ expect(draft.summaryHtml).toBe('<p>Resumo</p>');
119
+ expect(draft.sections).toEqual([
120
+ { key: 'scope', eyebrow: 'Scope', title: 'What is in.', html: '<p>Tudo</p>' },
121
+ { key: 'program', eyebrow: 'Programa', title: 'Um dia de prática.', html: '<p>Blocos</p>' },
122
+ ]);
123
+ expect(draft.executionId).toBe(9);
124
+ });
125
+
126
+ it('aceita a seção como string e omite a que veio vazia', async () => {
127
+ answer('{"sections":{"scope":"<p>Tudo</p>","program":{"html":" "}}}');
128
+
129
+ const draft = await service.draft('crm.commercial-proposal', {});
130
+
131
+ expect(draft.summaryHtml).toBeNull();
132
+ expect(draft.sections.map((section) => section.key)).toEqual(['scope']);
133
+ });
134
+
135
+ it('responde 502 quando a IA não devolve JSON', async () => {
136
+ answer('Desculpe, não consigo ajudar.');
137
+
138
+ await expect(service.draft('crm.commercial-proposal', {})).rejects.toBeInstanceOf(BadGatewayException);
139
+ });
140
+
141
+ it('troca a falha do provedor por 502 e deixa passar erro HTTP com mensagem própria', async () => {
142
+ chatWithPrompt.mockRejectedValueOnce(new Error('Request failed with status code 401'));
143
+ await expect(service.draft('crm.commercial-proposal', {})).rejects.toThrow(
144
+ 'The AI provider did not answer',
145
+ );
146
+
147
+ chatWithPrompt.mockRejectedValueOnce(new NotFoundException('Integration profile not found'));
148
+ await expect(service.draft('crm.commercial-proposal', {})).rejects.toThrow(
149
+ 'Integration profile not found',
150
+ );
151
+ });
152
+
153
+ it('responde 404 para kit não registrado', async () => {
154
+ await expect(service.draft('crm.nao-existe', {})).rejects.toBeInstanceOf(NotFoundException);
155
+ });
156
+ });
@@ -0,0 +1,173 @@
1
+ import {
2
+ BadGatewayException,
3
+ HttpException,
4
+ Inject,
5
+ Injectable,
6
+ Logger,
7
+ NotFoundException,
8
+ forwardRef,
9
+ } from '@nestjs/common';
10
+
11
+ import { AiService } from '../ai/ai.service';
12
+ import { AiPromptResolverService } from '../ai-instruction/ai-prompt-resolver.service';
13
+ import { TemplateVars } from '../ai-instruction/ai-prompt-template.util';
14
+ import { DocumentKitDefinition, getDocumentKit, pickLocale } from './document-kit.registry';
15
+
16
+ export type DocumentKitDraftSection = {
17
+ key: string;
18
+ eyebrow: string;
19
+ title: string;
20
+ /** Straight from the model: the caller sanitizes before storing or printing. */
21
+ html: string;
22
+ };
23
+
24
+ export type DocumentKitDraft = {
25
+ kit: string;
26
+ /** Opening text of the document (executive summary), when the model wrote one. */
27
+ summaryHtml: string | null;
28
+ sections: DocumentKitDraftSection[];
29
+ provider: string;
30
+ model: string;
31
+ executionId: number | null;
32
+ };
33
+
34
+ /**
35
+ * Pulls the JSON object out of a model answer. Providers without a JSON mode
36
+ * (Claude) wrap it in a code fence or add a sentence before it.
37
+ */
38
+ export function extractJsonObject(content: string): Record<string, any> | null {
39
+ const text = String(content ?? '')
40
+ .replace(/```(?:json)?/gi, '')
41
+ .trim();
42
+ const start = text.indexOf('{');
43
+ const end = text.lastIndexOf('}');
44
+ if (start < 0 || end <= start) return null;
45
+
46
+ try {
47
+ const parsed = JSON.parse(text.slice(start, end + 1));
48
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
49
+ } catch {
50
+ return null;
51
+ }
52
+ }
53
+
54
+ function asText(value: unknown): string {
55
+ return typeof value === 'string' ? value.trim() : '';
56
+ }
57
+
58
+ /** Only keys the kit declares survive, in the kit's order, and empty ones drop. */
59
+ export function readDraftAnswer(
60
+ kit: DocumentKitDefinition,
61
+ answer: Record<string, any>,
62
+ locale?: string,
63
+ ): { summaryHtml: string | null; sections: DocumentKitDraftSection[] } {
64
+ const raw = answer.sections && typeof answer.sections === 'object' ? answer.sections : {};
65
+
66
+ const sections = kit.sections
67
+ .map((section) => {
68
+ const value = raw[section.key];
69
+ const html = typeof value === 'string' ? value.trim() : asText(value?.html);
70
+ if (!html) return null;
71
+ return {
72
+ key: section.key,
73
+ eyebrow: pickLocale(section.eyebrow, locale),
74
+ title: asText(value?.title) || pickLocale(section.title, locale),
75
+ html,
76
+ };
77
+ })
78
+ .filter((section): section is DocumentKitDraftSection => section !== null);
79
+
80
+ return { summaryHtml: asText(answer.summary_html) || null, sections };
81
+ }
82
+
83
+ /** The section list as the prompts receive it, in `{{sectionsSpec}}`. */
84
+ export function buildSectionsSpec(kit: DocumentKitDefinition, locale?: string): string {
85
+ return kit.sections
86
+ .map(
87
+ (section) =>
88
+ `- ${section.key} — "${pickLocale(section.title, locale)}": ${section.guidance}`,
89
+ )
90
+ .join('\n');
91
+ }
92
+
93
+ @Injectable()
94
+ export class DocumentKitService {
95
+ private readonly logger = new Logger(DocumentKitService.name);
96
+
97
+ constructor(
98
+ @Inject(forwardRef(() => AiService))
99
+ private readonly aiService: AiService,
100
+ @Inject(forwardRef(() => AiPromptResolverService))
101
+ private readonly prompts: AiPromptResolverService,
102
+ ) {}
103
+
104
+ /**
105
+ * Drafts a document with the kit's prompts. The references are resolved first
106
+ * and reach the prompts as variables, next to `sectionsSpec` and whatever the
107
+ * caller passes (client, items, values already computed by the system).
108
+ */
109
+ async draft(
110
+ slug: string,
111
+ input: { variables?: TemplateVars; locale?: string; contextSlug?: string },
112
+ ): Promise<DocumentKitDraft> {
113
+ const kit = getDocumentKit(slug);
114
+ if (!kit) {
115
+ throw new NotFoundException(`Document kit "${slug}" is not registered.`);
116
+ }
117
+
118
+ const referenceEntries = Object.entries(kit.references ?? {});
119
+ const resolvedReferences = referenceEntries.length
120
+ ? await this.prompts.resolveMany(referenceEntries.map(([, referenceSlug]) => ({ slug: referenceSlug })))
121
+ : {};
122
+ const references = Object.fromEntries(
123
+ referenceEntries.map(([variable, referenceSlug]) => [
124
+ variable,
125
+ resolvedReferences[referenceSlug]?.content?.trim() ?? '',
126
+ ]),
127
+ );
128
+
129
+ let result: Awaited<ReturnType<AiService['chatWithPrompt']>>;
130
+ try {
131
+ result = await this.aiService.chatWithPrompt({
132
+ systemPromptSlug: kit.prompts.system,
133
+ userPromptSlug: kit.prompts.user,
134
+ variables: {
135
+ ...references,
136
+ ...(input.variables ?? {}),
137
+ sectionsSpec: buildSectionsSpec(kit, input.locale),
138
+ },
139
+ params: { responseFormat: 'json_object' },
140
+ contextSlug: input.contextSlug ?? `document-kit.${kit.slug}`,
141
+ });
142
+ } catch (error) {
143
+ // Erro com mensagem propria (setting faltando, perfil invalido) passa como
144
+ // veio; o resto e falha do provedor (chave recusada, timeout) e chegaria
145
+ // na tela como "erro interno".
146
+ if (error instanceof HttpException) throw error;
147
+ this.logger.error(
148
+ `Document kit "${kit.slug}": the AI provider call failed. ${error instanceof Error ? error.message : String(error)}`,
149
+ );
150
+ throw new BadGatewayException(
151
+ 'The AI provider did not answer. Check the AI integration profile and try again.',
152
+ );
153
+ }
154
+
155
+ const answer = extractJsonObject(result.content);
156
+ if (!answer) {
157
+ this.logger.warn(
158
+ `Document kit "${kit.slug}": the model answer is not a JSON object (${result.provider}/${result.model}).`,
159
+ );
160
+ throw new BadGatewayException(
161
+ 'The AI answer could not be read. Try again, or review the prompts in AI Instructions.',
162
+ );
163
+ }
164
+
165
+ return {
166
+ kit: kit.slug,
167
+ ...readDraftAnswer(kit, answer, input.locale),
168
+ provider: result.provider,
169
+ model: result.model,
170
+ executionId: result.executionId,
171
+ };
172
+ }
173
+ }