@carlonicora/nestjs-neo4jsonapi 2.0.0 → 2.2.0

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 (152) hide show
  1. package/dist/agents/graph/graph.module.d.ts.map +1 -1
  2. package/dist/agents/graph/graph.module.js +3 -0
  3. package/dist/agents/graph/graph.module.js.map +1 -1
  4. package/dist/agents/graph/interfaces/graph.catalog.interface.d.ts +24 -0
  5. package/dist/agents/graph/interfaces/graph.catalog.interface.d.ts.map +1 -1
  6. package/dist/agents/graph/services/graph.catalog.service.d.ts +9 -0
  7. package/dist/agents/graph/services/graph.catalog.service.d.ts.map +1 -1
  8. package/dist/agents/graph/services/graph.catalog.service.js +61 -1
  9. package/dist/agents/graph/services/graph.catalog.service.js.map +1 -1
  10. package/dist/agents/graph/services/graph.search.service.d.ts +16 -1
  11. package/dist/agents/graph/services/graph.search.service.d.ts.map +1 -1
  12. package/dist/agents/graph/services/graph.search.service.js +51 -4
  13. package/dist/agents/graph/services/graph.search.service.js.map +1 -1
  14. package/dist/agents/graph/services/humanize-tool.d.ts +17 -2
  15. package/dist/agents/graph/services/humanize-tool.d.ts.map +1 -1
  16. package/dist/agents/graph/services/humanize-tool.js +71 -6
  17. package/dist/agents/graph/services/humanize-tool.js.map +1 -1
  18. package/dist/agents/graph/services/materialise-bridge.d.ts +6 -0
  19. package/dist/agents/graph/services/materialise-bridge.d.ts.map +1 -1
  20. package/dist/agents/graph/services/materialise-bridge.js +17 -1
  21. package/dist/agents/graph/services/materialise-bridge.js.map +1 -1
  22. package/dist/agents/graph/services/scope.guard.d.ts +43 -0
  23. package/dist/agents/graph/services/scope.guard.d.ts.map +1 -0
  24. package/dist/agents/graph/services/scope.guard.js +91 -0
  25. package/dist/agents/graph/services/scope.guard.js.map +1 -0
  26. package/dist/agents/graph/tools/read-entity.tool.d.ts +9 -1
  27. package/dist/agents/graph/tools/read-entity.tool.d.ts.map +1 -1
  28. package/dist/agents/graph/tools/read-entity.tool.js +30 -5
  29. package/dist/agents/graph/tools/read-entity.tool.js.map +1 -1
  30. package/dist/agents/graph/tools/resolve-entity.tool.d.ts.map +1 -1
  31. package/dist/agents/graph/tools/resolve-entity.tool.js +8 -1
  32. package/dist/agents/graph/tools/resolve-entity.tool.js.map +1 -1
  33. package/dist/agents/graph/tools/search-entities.tool.d.ts +9 -1
  34. package/dist/agents/graph/tools/search-entities.tool.d.ts.map +1 -1
  35. package/dist/agents/graph/tools/search-entities.tool.js +37 -8
  36. package/dist/agents/graph/tools/search-entities.tool.js.map +1 -1
  37. package/dist/agents/graph/tools/tool.factory.d.ts +4 -0
  38. package/dist/agents/graph/tools/tool.factory.d.ts.map +1 -1
  39. package/dist/agents/graph/tools/tool.factory.js.map +1 -1
  40. package/dist/agents/graph/tools/traverse.tool.d.ts +9 -1
  41. package/dist/agents/graph/tools/traverse.tool.d.ts.map +1 -1
  42. package/dist/agents/graph/tools/traverse.tool.js +21 -5
  43. package/dist/agents/graph/tools/traverse.tool.js.map +1 -1
  44. package/dist/agents/operator/contexts/operator.context.d.ts +4 -0
  45. package/dist/agents/operator/contexts/operator.context.d.ts.map +1 -1
  46. package/dist/agents/operator/contexts/operator.context.js +4 -0
  47. package/dist/agents/operator/contexts/operator.context.js.map +1 -1
  48. package/dist/agents/operator/operator.module.d.ts.map +1 -1
  49. package/dist/agents/operator/operator.module.js +4 -0
  50. package/dist/agents/operator/operator.module.js.map +1 -1
  51. package/dist/agents/operator/services/operator.service.d.ts +8 -0
  52. package/dist/agents/operator/services/operator.service.d.ts.map +1 -1
  53. package/dist/agents/operator/services/operator.service.js +8 -0
  54. package/dist/agents/operator/services/operator.service.js.map +1 -1
  55. package/dist/agents/operator/tools/entity-write.tools.d.ts +124 -0
  56. package/dist/agents/operator/tools/entity-write.tools.d.ts.map +1 -0
  57. package/dist/agents/operator/tools/entity-write.tools.js +471 -0
  58. package/dist/agents/operator/tools/entity-write.tools.js.map +1 -0
  59. package/dist/agents/operator/tools/operator.tool.registry.d.ts +5 -1
  60. package/dist/agents/operator/tools/operator.tool.registry.d.ts.map +1 -1
  61. package/dist/agents/operator/tools/operator.tool.registry.js +19 -2
  62. package/dist/agents/operator/tools/operator.tool.registry.js.map +1 -1
  63. package/dist/agents/operator/tools/search-documents.tool.d.ts +16 -1
  64. package/dist/agents/operator/tools/search-documents.tool.d.ts.map +1 -1
  65. package/dist/agents/operator/tools/search-documents.tool.js +59 -3
  66. package/dist/agents/operator/tools/search-documents.tool.js.map +1 -1
  67. package/dist/agents/prompts/index.d.ts +1 -0
  68. package/dist/agents/prompts/index.d.ts.map +1 -1
  69. package/dist/agents/prompts/index.js +7 -1
  70. package/dist/agents/prompts/index.js.map +1 -1
  71. package/dist/agents/responder/contexts/responder.context.d.ts +12 -0
  72. package/dist/agents/responder/contexts/responder.context.d.ts.map +1 -1
  73. package/dist/agents/responder/contexts/responder.context.js +4 -0
  74. package/dist/agents/responder/contexts/responder.context.js.map +1 -1
  75. package/dist/agents/responder/factories/responder.context.factory.d.ts +4 -0
  76. package/dist/agents/responder/factories/responder.context.factory.d.ts.map +1 -1
  77. package/dist/agents/responder/factories/responder.context.factory.js +2 -0
  78. package/dist/agents/responder/factories/responder.context.factory.js.map +1 -1
  79. package/dist/agents/responder/nodes/graph.node.service.d.ts.map +1 -1
  80. package/dist/agents/responder/nodes/graph.node.service.js +18 -2
  81. package/dist/agents/responder/nodes/graph.node.service.js.map +1 -1
  82. package/dist/agents/responder/nodes/responder.answer.node.service.d.ts +32 -0
  83. package/dist/agents/responder/nodes/responder.answer.node.service.d.ts.map +1 -1
  84. package/dist/agents/responder/nodes/responder.answer.node.service.js +31 -19
  85. package/dist/agents/responder/nodes/responder.answer.node.service.js.map +1 -1
  86. package/dist/agents/responder/services/responder.service.d.ts +4 -0
  87. package/dist/agents/responder/services/responder.service.d.ts.map +1 -1
  88. package/dist/agents/responder/services/responder.service.js +2 -0
  89. package/dist/agents/responder/services/responder.service.js.map +1 -1
  90. package/dist/common/interfaces/entity.schema.interface.d.ts +16 -0
  91. package/dist/common/interfaces/entity.schema.interface.d.ts.map +1 -1
  92. package/dist/config/interfaces/config.prompts.interface.d.ts +12 -0
  93. package/dist/config/interfaces/config.prompts.interface.d.ts.map +1 -1
  94. package/dist/core/blocknote/services/blocknote.service.d.ts +23 -14
  95. package/dist/core/blocknote/services/blocknote.service.d.ts.map +1 -1
  96. package/dist/core/blocknote/services/blocknote.service.js +48 -32
  97. package/dist/core/blocknote/services/blocknote.service.js.map +1 -1
  98. package/dist/core/email/email.helpers.d.ts +18 -0
  99. package/dist/core/email/email.helpers.d.ts.map +1 -0
  100. package/dist/core/email/email.helpers.js +50 -0
  101. package/dist/core/email/email.helpers.js.map +1 -0
  102. package/dist/core/email/index.d.ts +1 -0
  103. package/dist/core/email/index.d.ts.map +1 -1
  104. package/dist/core/email/index.js +2 -0
  105. package/dist/core/email/index.js.map +1 -1
  106. package/dist/core/email/services/email.service.d.ts.map +1 -1
  107. package/dist/core/email/services/email.service.js +4 -4
  108. package/dist/core/email/services/email.service.js.map +1 -1
  109. package/dist/core/llm/services/llm.service.d.ts.map +1 -1
  110. package/dist/core/llm/services/llm.service.js +34 -2
  111. package/dist/core/llm/services/llm.service.js.map +1 -1
  112. package/dist/core/llm/services/tool-error-feedback.d.ts +40 -0
  113. package/dist/core/llm/services/tool-error-feedback.d.ts.map +1 -0
  114. package/dist/core/llm/services/tool-error-feedback.js +131 -0
  115. package/dist/core/llm/services/tool-error-feedback.js.map +1 -0
  116. package/dist/foundations/assistant/assistant.module.d.ts.map +1 -1
  117. package/dist/foundations/assistant/assistant.module.js +7 -1
  118. package/dist/foundations/assistant/assistant.module.js.map +1 -1
  119. package/dist/foundations/assistant/controllers/assistant.controller.d.ts +13 -1
  120. package/dist/foundations/assistant/controllers/assistant.controller.d.ts.map +1 -1
  121. package/dist/foundations/assistant/controllers/assistant.controller.js +37 -3
  122. package/dist/foundations/assistant/controllers/assistant.controller.js.map +1 -1
  123. package/dist/foundations/assistant/dtos/assistant-append.dto.d.ts +6 -0
  124. package/dist/foundations/assistant/dtos/assistant-append.dto.d.ts.map +1 -1
  125. package/dist/foundations/assistant/dtos/assistant-append.dto.js +1 -1
  126. package/dist/foundations/assistant/dtos/assistant-append.dto.js.map +1 -1
  127. package/dist/foundations/assistant/dtos/assistant-post.dto.d.ts +29 -0
  128. package/dist/foundations/assistant/dtos/assistant-post.dto.d.ts.map +1 -1
  129. package/dist/foundations/assistant/dtos/assistant-post.dto.js +47 -2
  130. package/dist/foundations/assistant/dtos/assistant-post.dto.js.map +1 -1
  131. package/dist/foundations/assistant/repositories/assistant.repository.d.ts +22 -0
  132. package/dist/foundations/assistant/repositories/assistant.repository.d.ts.map +1 -1
  133. package/dist/foundations/assistant/repositories/assistant.repository.js +28 -0
  134. package/dist/foundations/assistant/repositories/assistant.repository.js.map +1 -1
  135. package/dist/foundations/assistant/services/assistant-mention.extractor.d.ts +36 -0
  136. package/dist/foundations/assistant/services/assistant-mention.extractor.d.ts.map +1 -0
  137. package/dist/foundations/assistant/services/assistant-mention.extractor.js +99 -0
  138. package/dist/foundations/assistant/services/assistant-mention.extractor.js.map +1 -0
  139. package/dist/foundations/assistant/services/assistant.service.d.ts +84 -2
  140. package/dist/foundations/assistant/services/assistant.service.d.ts.map +1 -1
  141. package/dist/foundations/assistant/services/assistant.service.js +312 -60
  142. package/dist/foundations/assistant/services/assistant.service.js.map +1 -1
  143. package/dist/foundations/auth/services/auth.service.d.ts.map +1 -1
  144. package/dist/foundations/auth/services/auth.service.js +2 -0
  145. package/dist/foundations/auth/services/auth.service.js.map +1 -1
  146. package/dist/foundations/user/services/user.service.d.ts.map +1 -1
  147. package/dist/foundations/user/services/user.service.js +3 -0
  148. package/dist/foundations/user/services/user.service.js.map +1 -1
  149. package/dist/foundations/waitlist/services/waitlist.service.d.ts.map +1 -1
  150. package/dist/foundations/waitlist/services/waitlist.service.js +2 -0
  151. package/dist/foundations/waitlist/services/waitlist.service.js.map +1 -1
  152. package/package.json +1 -1
@@ -19,9 +19,12 @@ const operator_checkpointer_service_1 = require("../../../agents/operator/servic
19
19
  const operator_service_1 = require("../../../agents/operator/services/operator.service");
20
20
  const responder_service_1 = require("../../../agents/responder/services/responder.service");
21
21
  const graph_catalog_service_1 = require("../../../agents/graph/services/graph.catalog.service");
22
+ const scope_guard_1 = require("../../../agents/graph/services/scope.guard");
22
23
  const user_modules_repository_1 = require("../../../agents/graph/repositories/user-modules.repository");
23
24
  const agentmessage_type_1 = require("../../../common/enums/agentmessage.type");
24
25
  const entity_service_registry_1 = require("../../../common/registries/entity.service.registry");
26
+ const blocknote_service_1 = require("../../../core/blocknote/services/blocknote.service");
27
+ const jsonapi_paginator_1 = require("../../../core/jsonapi/serialisers/jsonapi.paginator");
25
28
  const jsonapi_service_1 = require("../../../core/jsonapi/services/jsonapi.service");
26
29
  const abstract_service_1 = require("../../../core/neo4j/abstracts/abstract.service");
27
30
  const websocket_service_1 = require("../../../core/websocket/services/websocket.service");
@@ -33,7 +36,9 @@ const assistant_message_repository_1 = require("../../assistant-message/reposito
33
36
  const assistant_message_service_1 = require("../../assistant-message/services/assistant-message.service");
34
37
  const assistant_1 = require("../entities/assistant");
35
38
  const assistant_meta_1 = require("../entities/assistant.meta");
39
+ const registry_1 = require("../../../common/registries/registry");
36
40
  const assistant_repository_1 = require("../repositories/assistant.repository");
41
+ const assistant_mention_extractor_1 = require("./assistant-mention.extractor");
37
42
  /**
38
43
  * Maximum number of prior messages (turns) passed to the LLM on each turn.
39
44
  * Keeps prompt size and cost bounded for long conversations.
@@ -55,7 +60,7 @@ exports.MAX_MESSAGES_TO_LLM = 20;
55
60
  * AssistantMessageRepository.linkReferences).
56
61
  */
57
62
  let AssistantService = AssistantService_1 = class AssistantService extends abstract_service_1.AbstractService {
58
- constructor(jsonApiService, assistantRepository, clsService, userModuleIdsRepository, responder, assistantMessages, assistantMessageRepo, graphCatalog, entityServices, operator, assistantActions, assistantActionRepo, webSocketService, configService) {
63
+ constructor(jsonApiService, assistantRepository, clsService, userModuleIdsRepository, responder, assistantMessages, assistantMessageRepo, graphCatalog, entityServices, operator, assistantActions, assistantActionRepo, webSocketService, configService, mentions, blockNote, scopeGuard) {
59
64
  super(jsonApiService, assistantRepository, clsService, assistant_1.AssistantDescriptor.model);
60
65
  this.userModuleIdsRepository = userModuleIdsRepository;
61
66
  this.responder = responder;
@@ -68,15 +73,30 @@ let AssistantService = AssistantService_1 = class AssistantService extends abstr
68
73
  this.assistantActionRepo = assistantActionRepo;
69
74
  this.webSocketService = webSocketService;
70
75
  this.configService = configService;
76
+ this.mentions = mentions;
77
+ this.blockNote = blockNote;
78
+ this.scopeGuard = scopeGuard;
71
79
  this.descriptor = assistant_1.AssistantDescriptor;
72
80
  this.assistantLogger = new common_1.Logger(AssistantService_1.name);
73
81
  }
74
82
  async createWithFirstMessage(params) {
75
83
  const userModuleIds = await this.userModuleIdsRepository.findModuleIdsForUser(params.userId);
76
- const title = params.title?.trim() || this.autoTitle(params.firstMessage);
84
+ const ctx = this.buildTurnContext({
85
+ companyId: params.companyId,
86
+ userId: params.userId,
87
+ userModuleIds,
88
+ boundContent: params.boundContent,
89
+ });
90
+ const { content: firstMessage, plainText, pinned, } = await this.resolveUserTurnInput({
91
+ rawContent: params.firstMessage,
92
+ ctx,
93
+ });
94
+ const title = params.title?.trim() || this.autoTitle(plainText);
77
95
  const assistantId = (0, crypto_1.randomUUID)();
78
96
  const userMessageId = (0, crypto_1.randomUUID)();
79
97
  // 1. Create the Assistant (owner edge attached from CLS via contextKey).
98
+ // A bound thread also gets the BOUND_TO edge to the resource it is
99
+ // confined to — the relationship is immutable, so it is set on create.
80
100
  await this.createFromDTO({
81
101
  data: {
82
102
  type: assistant_meta_1.assistantMeta.type,
@@ -84,6 +104,7 @@ let AssistantService = AssistantService_1 = class AssistantService extends abstr
84
104
  attributes: { title },
85
105
  },
86
106
  });
107
+ await this.attachBoundContent(assistantId, params.boundContent);
87
108
  // 2. Create the first user message at position 0.
88
109
  await this.assistantMessages.createFromDTO({
89
110
  data: {
@@ -91,7 +112,7 @@ let AssistantService = AssistantService_1 = class AssistantService extends abstr
91
112
  id: userMessageId,
92
113
  attributes: {
93
114
  role: "user",
94
- content: params.firstMessage,
115
+ content: firstMessage,
95
116
  position: 0,
96
117
  },
97
118
  relationships: {
@@ -99,15 +120,16 @@ let AssistantService = AssistantService_1 = class AssistantService extends abstr
99
120
  },
100
121
  },
101
122
  });
123
+ await this.linkPinnedMentions({ messageId: userMessageId, pinned });
102
124
  const userMessage = await this.assistantMessageRepo.findById({ id: userMessageId });
103
125
  // 3. Run the agent turn using the just-created user message as context.
104
126
  const turn = await this.runAgentTurn({
105
- companyId: params.companyId,
106
- userId: params.userId,
107
- userModuleIds,
127
+ ctx,
108
128
  priorMessages: [],
109
- newUserMessage: { role: "user", content: params.firstMessage },
129
+ newUserMessage: { role: "user", content: firstMessage },
130
+ pinned,
110
131
  assistantId,
132
+ contentScope: this.toContentScope(params.boundContent),
111
133
  howToMode: params.howToMode,
112
134
  limitToHowToId: params.limitToHowToId,
113
135
  });
@@ -156,6 +178,18 @@ let AssistantService = AssistantService_1 = class AssistantService extends abstr
156
178
  // Verify ownership via the owner-RBAC-enforcing findById.
157
179
  await this.repository.findById({ id: params.assistantId });
158
180
  const userModuleIds = await this.userModuleIdsRepository.findModuleIdsForUser(params.userId);
181
+ // An append inherits the thread's binding — never a client-supplied one.
182
+ const contentScope = await this.resolveContentScope(params.assistantId);
183
+ const ctx = this.buildTurnContext({
184
+ companyId: params.companyId,
185
+ userId: params.userId,
186
+ userModuleIds,
187
+ boundContent: this.toBoundContent(contentScope),
188
+ });
189
+ const { content: newMessage, pinned } = await this.resolveUserTurnInput({
190
+ rawContent: params.newMessage,
191
+ ctx,
192
+ });
159
193
  // Load prior messages for agent context.
160
194
  const priorMessages = await this.loadRecentMessages({
161
195
  assistantId: params.assistantId,
@@ -171,7 +205,7 @@ let AssistantService = AssistantService_1 = class AssistantService extends abstr
171
205
  id: userMessageId,
172
206
  attributes: {
173
207
  role: "user",
174
- content: params.newMessage,
208
+ content: newMessage,
175
209
  position: nextPosition,
176
210
  },
177
211
  relationships: {
@@ -179,14 +213,15 @@ let AssistantService = AssistantService_1 = class AssistantService extends abstr
179
213
  },
180
214
  },
181
215
  });
216
+ await this.linkPinnedMentions({ messageId: userMessageId, pinned });
182
217
  const userMessage = await this.assistantMessageRepo.findById({ id: userMessageId });
183
218
  const turn = await this.runAgentTurn({
184
- companyId: params.companyId,
185
- userId: params.userId,
186
- userModuleIds,
219
+ ctx,
187
220
  priorMessages,
188
- newUserMessage: { role: "user", content: params.newMessage },
221
+ newUserMessage: { role: "user", content: newMessage },
222
+ pinned,
189
223
  assistantId: params.assistantId,
224
+ contentScope,
190
225
  howToMode: params.howToMode,
191
226
  limitToHowToId: params.limitToHowToId,
192
227
  });
@@ -237,7 +272,17 @@ let AssistantService = AssistantService_1 = class AssistantService extends abstr
237
272
  */
238
273
  async createWithFirstMessageOperator(params) {
239
274
  const userModuleIds = await this.userModuleIdsRepository.findModuleIdsForUser(params.userId);
240
- const title = params.title?.trim() || this.autoTitle(params.firstMessage);
275
+ const ctx = this.buildTurnContext({
276
+ companyId: params.companyId,
277
+ userId: params.userId,
278
+ userModuleIds,
279
+ boundContent: params.boundContent,
280
+ });
281
+ const { content: firstMessage, plainText, pinned, } = await this.resolveUserTurnInput({
282
+ rawContent: params.firstMessage,
283
+ ctx,
284
+ });
285
+ const title = params.title?.trim() || this.autoTitle(plainText);
241
286
  const assistantId = (0, crypto_1.randomUUID)();
242
287
  const userMessageId = (0, crypto_1.randomUUID)();
243
288
  // Persist the engine marker so clients can route follow-up turns to the
@@ -247,6 +292,13 @@ let AssistantService = AssistantService_1 = class AssistantService extends abstr
247
292
  type: assistant_meta_1.assistantMeta.type,
248
293
  id: assistantId,
249
294
  attributes: { title, engine: "operator" },
295
+ ...(params.boundContent
296
+ ? {
297
+ relationships: {
298
+ content: { data: { type: params.boundContent.type, id: params.boundContent.id } },
299
+ },
300
+ }
301
+ : {}),
250
302
  },
251
303
  });
252
304
  await this.assistantMessages.createFromDTO({
@@ -255,7 +307,7 @@ let AssistantService = AssistantService_1 = class AssistantService extends abstr
255
307
  id: userMessageId,
256
308
  attributes: {
257
309
  role: "user",
258
- content: params.firstMessage,
310
+ content: firstMessage,
259
311
  position: 0,
260
312
  },
261
313
  relationships: {
@@ -263,15 +315,15 @@ let AssistantService = AssistantService_1 = class AssistantService extends abstr
263
315
  },
264
316
  },
265
317
  });
318
+ await this.linkPinnedMentions({ messageId: userMessageId, pinned });
266
319
  const userMessage = await this.assistantMessageRepo.findById({ id: userMessageId });
267
320
  const threadId = `${assistantId}:${userMessageId}`;
268
- const contentScope = await this.resolveContentScope(assistantId);
321
+ const contentScope = this.toContentScope(params.boundContent);
269
322
  const result = await this.runOperatorTurn({
270
- companyId: params.companyId,
271
- userId: params.userId,
272
- userModuleIds,
323
+ ctx,
273
324
  priorMessages: [],
274
- question: params.firstMessage,
325
+ question: firstMessage,
326
+ pinned,
275
327
  assistantId,
276
328
  threadId,
277
329
  contentScope,
@@ -296,6 +348,17 @@ let AssistantService = AssistantService_1 = class AssistantService extends abstr
296
348
  // Verify ownership via the owner-RBAC-enforcing findById.
297
349
  await this.repository.findById({ id: params.assistantId });
298
350
  const userModuleIds = await this.userModuleIdsRepository.findModuleIdsForUser(params.userId);
351
+ const contentScope = await this.resolveContentScope(params.assistantId);
352
+ const ctx = this.buildTurnContext({
353
+ companyId: params.companyId,
354
+ userId: params.userId,
355
+ userModuleIds,
356
+ boundContent: this.toBoundContent(contentScope),
357
+ });
358
+ const { content: newMessage, pinned } = await this.resolveUserTurnInput({
359
+ rawContent: params.newMessage,
360
+ ctx,
361
+ });
299
362
  const priorMessages = await this.loadRecentMessages({
300
363
  assistantId: params.assistantId,
301
364
  limit: exports.MAX_MESSAGES_TO_LLM,
@@ -310,7 +373,7 @@ let AssistantService = AssistantService_1 = class AssistantService extends abstr
310
373
  id: userMessageId,
311
374
  attributes: {
312
375
  role: "user",
313
- content: params.newMessage,
376
+ content: newMessage,
314
377
  position: nextPosition,
315
378
  },
316
379
  relationships: {
@@ -318,15 +381,14 @@ let AssistantService = AssistantService_1 = class AssistantService extends abstr
318
381
  },
319
382
  },
320
383
  });
384
+ await this.linkPinnedMentions({ messageId: userMessageId, pinned });
321
385
  const userMessage = await this.assistantMessageRepo.findById({ id: userMessageId });
322
386
  const threadId = `${params.assistantId}:${userMessageId}`;
323
- const contentScope = await this.resolveContentScope(params.assistantId);
324
387
  const result = await this.runOperatorTurn({
325
- companyId: params.companyId,
326
- userId: params.userId,
327
- userModuleIds,
388
+ ctx,
328
389
  priorMessages,
329
- question: params.newMessage,
390
+ question: newMessage,
391
+ pinned,
330
392
  assistantId: params.assistantId,
331
393
  threadId,
332
394
  contentScope,
@@ -447,9 +509,187 @@ let AssistantService = AssistantService_1 = class AssistantService extends abstr
447
509
  // pending action supersedes the (now executed) original in the response.
448
510
  return { assistantMessage: outcome.assistantMessage, action: outcome.action ?? updatedAction };
449
511
  }
512
+ /**
513
+ * List the threads bound to one resource (e.g. every assistant thread for a
514
+ * campaign). Delegates to the inherited `findByRelated` over the `content`
515
+ * relationship — the BOUND_TO traversal already exists, so there is no new
516
+ * Cypher here and company/owner RBAC still comes from the repository.
517
+ */
518
+ async findByBoundContent(params) {
519
+ const model = registry_1.modelRegistry.getByType(params.boundType);
520
+ if (!model) {
521
+ throw new common_1.BadRequestException(`Unknown resource type "${params.boundType}" for boundType.`);
522
+ }
523
+ const paginator = new jsonapi_paginator_1.JsonApiPaginator(params.query);
524
+ // `findByRelatedEdge`, not `findByRelated`: the latter derives the target
525
+ // label from `relationships.content.model`, which for this polymorphic edge
526
+ // is the `assistantMeta` placeholder, so it would look for
527
+ // `(:Assistant { id: <campaign-uuid> })` and always return nothing. This
528
+ // variant takes the edge spec explicitly, with the label resolved from the
529
+ // model registry.
530
+ const data = await this.repository.findByRelatedEdge({
531
+ cypherLabel: "BOUND_TO",
532
+ cypherDirection: "out",
533
+ relatedLabel: model.labelName,
534
+ relatedId: params.boundId,
535
+ cursor: paginator.generateCursor(),
536
+ });
537
+ return this.jsonApiService.buildList(assistant_1.AssistantDescriptor.model, data, paginator);
538
+ }
539
+ /**
540
+ * Build the run's UserContext once per turn. `scopeId`/`scopeType` are set
541
+ * only when the thread is bound to a catalogued scope ROOT (a descriptor
542
+ * whose compiled scope chain is empty, e.g. a campaign). A thread bound to
543
+ * something further down a chain still gets content scope, but not a
544
+ * transitive run scope — the guard has no root to confine the run to.
545
+ */
546
+ buildTurnContext(params) {
547
+ const ctx = {
548
+ companyId: params.companyId,
549
+ userId: params.userId,
550
+ userModuleIds: params.userModuleIds,
551
+ };
552
+ const bound = params.boundContent;
553
+ if (!bound?.type || !bound?.id)
554
+ return ctx;
555
+ const scope = this.graphCatalog.getEntityDetail(bound.type, params.userModuleIds)?.scope;
556
+ if (scope && scope.path.length === 0 && scope.rootType === bound.type) {
557
+ ctx.scopeId = bound.id;
558
+ ctx.scopeType = bound.type;
559
+ }
560
+ else {
561
+ this.assistantLogger.warn(`buildTurnContext: bound type "${bound.type}" is not a catalogued scope root — the turn runs unscoped.`);
562
+ }
563
+ return ctx;
564
+ }
565
+ /**
566
+ * Write the `BOUND_TO` edge that scopes a thread to a resource.
567
+ *
568
+ * Deliberately NOT part of the `createFromDTO` call above: `BOUND_TO` is
569
+ * polymorphic, so its descriptor entry carries `assistantMeta` as a
570
+ * placeholder model. The generic create path reads `rel.model.labelName` to
571
+ * validate the target and to build the edge, so a Campaign id sent that way
572
+ * is looked up as `(:Assistant { id: … })` and 400s with "One or more
573
+ * related nodes do not exist."
574
+ *
575
+ * The label is resolved from the model registry — the same registry the
576
+ * descriptor's own polymorphic discriminator uses on the read path — so an
577
+ * unregistered type fails loudly here rather than writing a dangling edge.
578
+ */
579
+ async attachBoundContent(assistantId, boundContent) {
580
+ if (!boundContent)
581
+ return;
582
+ const model = registry_1.modelRegistry.getByType(boundContent.type);
583
+ if (!model) {
584
+ throw new common_1.BadRequestException(`Unknown resource type "${boundContent.type}" for the assistant's bound content.`);
585
+ }
586
+ await this.repository.bindContent({
587
+ assistantId,
588
+ targetLabel: model.labelName,
589
+ targetId: boundContent.id,
590
+ });
591
+ }
592
+ toContentScope(boundContent) {
593
+ return boundContent ? { contentId: boundContent.id, contentType: boundContent.type } : {};
594
+ }
595
+ toBoundContent(contentScope) {
596
+ return contentScope.contentId && contentScope.contentType
597
+ ? { type: contentScope.contentType, id: contentScope.contentId }
598
+ : undefined;
599
+ }
600
+ /**
601
+ * A BlockNote-backed field arrives as a JSON string in its own string
602
+ * attribute — the convention every host-app entity already uses (e.g.
603
+ * `Npc.description`: `JSON.stringify` out, `JSON.parse` back). Detect that
604
+ * shape without trusting it: a plain-text message must never be mistaken for
605
+ * a document, so this requires an array whose first entry is an object with
606
+ * a string `type`, which is the minimal shape of a BlockNote block.
607
+ */
608
+ parseBlockNoteDocument(raw) {
609
+ const trimmed = raw.trimStart();
610
+ if (!trimmed.startsWith("["))
611
+ return null;
612
+ try {
613
+ const parsed = JSON.parse(trimmed);
614
+ if (!Array.isArray(parsed) || parsed.length === 0)
615
+ return null;
616
+ const first = parsed[0];
617
+ if (!first || typeof first !== "object" || typeof first.type !== "string")
618
+ return null;
619
+ return parsed;
620
+ }
621
+ catch {
622
+ return null;
623
+ }
624
+ }
625
+ /**
626
+ * Derive from a user message: the markdown to persist (mention links kept, so
627
+ * the thread can re-render them), a plain-text form for the thread title, and
628
+ * the validated entity mentions to pin into the turn's focus context.
629
+ *
630
+ * Plain-text input passes straight through, so apps without a rich composer
631
+ * are unaffected.
632
+ */
633
+ async resolveUserTurnInput(params) {
634
+ const nodes = this.parseBlockNoteDocument(params.rawContent);
635
+ if (!nodes)
636
+ return { content: params.rawContent, plainText: params.rawContent, pinned: [] };
637
+ const content = this.blockNote.convertToMarkdown({ nodes, preserveMentions: true });
638
+ // Alias-only rendering: what a human reads. Used for the thread title so it
639
+ // does not show raw `[Name](mention://npcs/<uuid>)` markup.
640
+ const plainText = this.blockNote.convertToMarkdown({ nodes });
641
+ const pinned = await this.mentions.validate({
642
+ mentions: this.mentions.extract(nodes),
643
+ ctx: params.ctx,
644
+ });
645
+ return { content, plainText, pinned };
646
+ }
647
+ /**
648
+ * Materialise REFERENCES edges from the USER message to the entities that
649
+ * message named. The reference metadata is fixed: a mention is an explicit,
650
+ * maximally relevant pointer, not a ranked retrieval hit.
651
+ */
652
+ async linkPinnedMentions(params) {
653
+ if (!params.pinned.length)
654
+ return;
655
+ const references = params.pinned.map((mention) => ({
656
+ type: mention.type,
657
+ id: mention.id,
658
+ relevance: 100,
659
+ reason: "Named explicitly by the user in this message.",
660
+ }));
661
+ await this.assistantMessageRepo.linkReferences({ messageId: params.messageId, references });
662
+ }
663
+ /**
664
+ * Drop records that fall outside the run's scope root, grouping by JSON:API
665
+ * type so `ScopeGuard` can build one predicate per type. A no-op for an
666
+ * unscoped run — `ScopeGuard.filter` returns everything in that case, so the
667
+ * short-circuit only avoids a pointless round trip.
668
+ */
669
+ async filterByScope(records, ctx) {
670
+ if (!ctx.scopeId || !ctx.scopeType || records.length === 0)
671
+ return records;
672
+ const byType = new Map();
673
+ for (const record of records) {
674
+ const type = typeof record.type === "string" ? record.type : undefined;
675
+ const id = typeof record.id === "string" ? record.id : undefined;
676
+ if (!type || !id)
677
+ continue;
678
+ const bucket = byType.get(type) ?? [];
679
+ bucket.push(record);
680
+ byType.set(type, bucket);
681
+ }
682
+ const kept = new Set();
683
+ for (const [type, bucket] of byType) {
684
+ const allowed = await this.scopeGuard.filter({ type, records: bucket, ctx });
685
+ for (const record of allowed)
686
+ kept.add(record);
687
+ }
688
+ return records.filter((record) => kept.has(record));
689
+ }
450
690
  /**
451
691
  * Resolve the content scope from the assistant's BOUND_TO relationship if
452
- * loaded. Mirrors the inline logic in `runAgentTurn`.
692
+ * loaded.
453
693
  */
454
694
  async resolveContentScope(assistantId) {
455
695
  try {
@@ -473,7 +713,7 @@ let AssistantService = AssistantService_1 = class AssistantService extends abstr
473
713
  assistantId: params.assistantId,
474
714
  turnStartedAt: now.toISOString().slice(0, 19).replace(/[T:]/g, "-"),
475
715
  });
476
- const hydrationContent = await this.buildHydrationMessage(params.priorMessages, params.userModuleIds);
716
+ const hydrationContent = await this.buildHydrationMessage(params.priorMessages, params.ctx, params.pinned);
477
717
  const trimmed = params.priorMessages.slice(-exports.MAX_MESSAGES_TO_LLM).map((m) => ({
478
718
  type: this.roleToType(m.role),
479
719
  content: m.content,
@@ -484,11 +724,13 @@ let AssistantService = AssistantService_1 = class AssistantService extends abstr
484
724
  { type: agentmessage_type_1.AgentMessageType.User, content: params.question },
485
725
  ];
486
726
  return await this.operator.run({
487
- companyId: params.companyId,
488
- userId: params.userId,
489
- userModuleIds: params.userModuleIds,
727
+ companyId: params.ctx.companyId,
728
+ userId: params.ctx.userId,
729
+ userModuleIds: params.ctx.userModuleIds,
490
730
  contentId: params.contentScope.contentId,
491
731
  contentType: params.contentScope.contentType,
732
+ scopeId: params.ctx.scopeId,
733
+ scopeType: params.ctx.scopeType,
492
734
  messages,
493
735
  question: params.question,
494
736
  threadId: params.threadId,
@@ -604,7 +846,7 @@ let AssistantService = AssistantService_1 = class AssistantService extends abstr
604
846
  turnStartedAt: now.toISOString().slice(0, 19).replace(/[T:]/g, "-"),
605
847
  });
606
848
  }
607
- const hydrationContent = await this.buildHydrationMessage(params.priorMessages, params.userModuleIds);
849
+ const hydrationContent = await this.buildHydrationMessage(params.priorMessages, params.ctx, params.pinned);
608
850
  const trimmed = params.priorMessages.slice(-exports.MAX_MESSAGES_TO_LLM).map((m) => ({
609
851
  type: this.roleToType(m.role),
610
852
  content: m.content,
@@ -614,26 +856,14 @@ let AssistantService = AssistantService_1 = class AssistantService extends abstr
614
856
  ...trimmed,
615
857
  { type: agentmessage_type_1.AgentMessageType.User, content: params.newUserMessage.content },
616
858
  ];
617
- // Resolve content scope from the assistant's BOUND_TO relationship if loaded.
618
- let contentId;
619
- let contentType;
620
- if (params.assistantId) {
621
- try {
622
- const assistant = await this.repository.findById({ id: params.assistantId });
623
- const c = assistant.content;
624
- contentId = c?.id;
625
- contentType = c?.type;
626
- }
627
- catch {
628
- // If load fails, proceed without content scope.
629
- }
630
- }
631
859
  const response = await this.responder.run({
632
- companyId: params.companyId,
633
- userId: params.userId,
634
- userModuleIds: params.userModuleIds,
635
- contentId,
636
- contentType,
860
+ companyId: params.ctx.companyId,
861
+ userId: params.ctx.userId,
862
+ userModuleIds: params.ctx.userModuleIds,
863
+ contentId: params.contentScope.contentId,
864
+ contentType: params.contentScope.contentType,
865
+ scopeId: params.ctx.scopeId,
866
+ scopeType: params.ctx.scopeType,
637
867
  dataLimits: {
638
868
  howToMode: params.howToMode,
639
869
  limitToHowToId: params.limitToHowToId,
@@ -673,14 +903,17 @@ let AssistantService = AssistantService_1 = class AssistantService extends abstr
673
903
  * the whole builder returns null so a single bad Neo4j hiccup does not
674
904
  * fail the chat turn.
675
905
  */
676
- async buildHydrationMessage(messages, userModuleIds) {
677
- if (messages.length === 0)
906
+ async buildHydrationMessage(messages, ctx, pinned = []) {
907
+ if (messages.length === 0 && pinned.length === 0)
678
908
  return null;
679
909
  try {
680
- const pairs = await this.assistantMessageRepo.findReferencedTypeIdPairs({
681
- messageIds: messages.map((m) => m.id),
682
- });
683
- if (pairs.length === 0)
910
+ const userModuleIds = ctx.userModuleIds;
911
+ const pairs = messages.length
912
+ ? await this.assistantMessageRepo.findReferencedTypeIdPairs({
913
+ messageIds: messages.map((m) => m.id),
914
+ })
915
+ : [];
916
+ if (pairs.length === 0 && pinned.length === 0)
684
917
  return null;
685
918
  // The most recent assistant message is the focus anchor.
686
919
  const prevAssistantId = [...messages].reverse().find((m) => m.role === "assistant")?.id;
@@ -690,6 +923,16 @@ let AssistantService = AssistantService_1 = class AssistantService extends abstr
690
923
  const backgroundKeys = new Set();
691
924
  const focusRefs = [];
692
925
  const backgroundRefs = [];
926
+ // Entities the user named explicitly lead the focus tier: they are the
927
+ // subject of the new question, so they must survive FOCUS_CAP and must
928
+ // never be demoted to a background stub by an earlier mention.
929
+ for (const mention of pinned) {
930
+ const key = `${mention.type}/${mention.id}`;
931
+ if (focusKeys.has(key))
932
+ continue;
933
+ focusKeys.add(key);
934
+ focusRefs.push({ type: mention.type, id: mention.id });
935
+ }
693
936
  for (const p of pairs) {
694
937
  const key = `${p.type}/${p.id}`;
695
938
  if (prevAssistantId && p.messageId === prevAssistantId) {
@@ -716,9 +959,12 @@ let AssistantService = AssistantService_1 = class AssistantService extends abstr
716
959
  if (backgroundOnly.length > BACKGROUND_CAP) {
717
960
  this.assistantLogger.warn(`buildHydrationMessage: background set capped at ${BACKGROUND_CAP} (had ${backgroundOnly.length})`);
718
961
  }
719
- // Load focus records (full) and background records (project to label).
720
- const focusRecords = await this.loadFocusRecords(focusCapped, userModuleIds);
721
- const backgroundStubs = await this.loadBackgroundStubs(backgroundCapped, userModuleIds);
962
+ // Load focus records (full) and background records (project to label),
963
+ // then drop anything outside the run's scope. Hydration is a data-access
964
+ // point like any tool: a record from another campaign must never reach
965
+ // the LLM just because an earlier turn referenced it.
966
+ const focusRecords = await this.filterByScope(await this.loadFocusRecords(focusCapped, userModuleIds), ctx);
967
+ const backgroundStubs = await this.filterByScope(await this.loadBackgroundStubs(backgroundCapped, userModuleIds), ctx);
722
968
  // Diagnostic trace: in the dump it lets us see exactly which entities were
723
969
  // handed to the LLM as focus context (and whether any were bridges, since
724
970
  // bridge ids in focus are addressable directly without resolve_entity).
@@ -737,6 +983,9 @@ let AssistantService = AssistantService_1 = class AssistantService extends abstr
737
983
  sections.push('These are the entities your previous answer was about. When the user\'s new question refers to any of them — by name or implicitly ("these", "them", "other orders", "their invoices") — use their id directly. Do not call resolve_entity for a name that matches one of these.');
738
984
  sections.push("");
739
985
  sections.push(JSON.stringify(focusRecords, null, 2));
986
+ if (pinned.length > 0) {
987
+ sections.push("The user named some of these entities explicitly in the new question. Treat every entity listed above as already identified: use its id directly and do not call resolve_entity for its name.");
988
+ }
740
989
  sections.push("");
741
990
  }
742
991
  if (backgroundStubs.length > 0) {
@@ -875,6 +1124,9 @@ exports.AssistantService = AssistantService = AssistantService_1 = __decorate([
875
1124
  assistant_action_service_1.AssistantActionService,
876
1125
  assistant_action_repository_1.AssistantActionRepository,
877
1126
  websocket_service_1.WebSocketService,
878
- config_1.ConfigService])
1127
+ config_1.ConfigService,
1128
+ assistant_mention_extractor_1.AssistantMentionExtractor,
1129
+ blocknote_service_1.BlockNoteService,
1130
+ scope_guard_1.ScopeGuard])
879
1131
  ], AssistantService);
880
1132
  //# sourceMappingURL=assistant.service.js.map