@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.
- package/dist/agents/graph/graph.module.d.ts.map +1 -1
- package/dist/agents/graph/graph.module.js +3 -0
- package/dist/agents/graph/graph.module.js.map +1 -1
- package/dist/agents/graph/interfaces/graph.catalog.interface.d.ts +24 -0
- package/dist/agents/graph/interfaces/graph.catalog.interface.d.ts.map +1 -1
- package/dist/agents/graph/services/graph.catalog.service.d.ts +9 -0
- package/dist/agents/graph/services/graph.catalog.service.d.ts.map +1 -1
- package/dist/agents/graph/services/graph.catalog.service.js +61 -1
- package/dist/agents/graph/services/graph.catalog.service.js.map +1 -1
- package/dist/agents/graph/services/graph.search.service.d.ts +16 -1
- package/dist/agents/graph/services/graph.search.service.d.ts.map +1 -1
- package/dist/agents/graph/services/graph.search.service.js +51 -4
- package/dist/agents/graph/services/graph.search.service.js.map +1 -1
- package/dist/agents/graph/services/humanize-tool.d.ts +17 -2
- package/dist/agents/graph/services/humanize-tool.d.ts.map +1 -1
- package/dist/agents/graph/services/humanize-tool.js +71 -6
- package/dist/agents/graph/services/humanize-tool.js.map +1 -1
- package/dist/agents/graph/services/materialise-bridge.d.ts +6 -0
- package/dist/agents/graph/services/materialise-bridge.d.ts.map +1 -1
- package/dist/agents/graph/services/materialise-bridge.js +17 -1
- package/dist/agents/graph/services/materialise-bridge.js.map +1 -1
- package/dist/agents/graph/services/scope.guard.d.ts +43 -0
- package/dist/agents/graph/services/scope.guard.d.ts.map +1 -0
- package/dist/agents/graph/services/scope.guard.js +91 -0
- package/dist/agents/graph/services/scope.guard.js.map +1 -0
- package/dist/agents/graph/tools/read-entity.tool.d.ts +9 -1
- package/dist/agents/graph/tools/read-entity.tool.d.ts.map +1 -1
- package/dist/agents/graph/tools/read-entity.tool.js +30 -5
- package/dist/agents/graph/tools/read-entity.tool.js.map +1 -1
- package/dist/agents/graph/tools/resolve-entity.tool.d.ts.map +1 -1
- package/dist/agents/graph/tools/resolve-entity.tool.js +8 -1
- package/dist/agents/graph/tools/resolve-entity.tool.js.map +1 -1
- package/dist/agents/graph/tools/search-entities.tool.d.ts +9 -1
- package/dist/agents/graph/tools/search-entities.tool.d.ts.map +1 -1
- package/dist/agents/graph/tools/search-entities.tool.js +37 -8
- package/dist/agents/graph/tools/search-entities.tool.js.map +1 -1
- package/dist/agents/graph/tools/tool.factory.d.ts +4 -0
- package/dist/agents/graph/tools/tool.factory.d.ts.map +1 -1
- package/dist/agents/graph/tools/tool.factory.js.map +1 -1
- package/dist/agents/graph/tools/traverse.tool.d.ts +9 -1
- package/dist/agents/graph/tools/traverse.tool.d.ts.map +1 -1
- package/dist/agents/graph/tools/traverse.tool.js +21 -5
- package/dist/agents/graph/tools/traverse.tool.js.map +1 -1
- package/dist/agents/operator/contexts/operator.context.d.ts +4 -0
- package/dist/agents/operator/contexts/operator.context.d.ts.map +1 -1
- package/dist/agents/operator/contexts/operator.context.js +4 -0
- package/dist/agents/operator/contexts/operator.context.js.map +1 -1
- package/dist/agents/operator/operator.module.d.ts.map +1 -1
- package/dist/agents/operator/operator.module.js +4 -0
- package/dist/agents/operator/operator.module.js.map +1 -1
- package/dist/agents/operator/services/operator.service.d.ts +8 -0
- package/dist/agents/operator/services/operator.service.d.ts.map +1 -1
- package/dist/agents/operator/services/operator.service.js +8 -0
- package/dist/agents/operator/services/operator.service.js.map +1 -1
- package/dist/agents/operator/tools/entity-write.tools.d.ts +124 -0
- package/dist/agents/operator/tools/entity-write.tools.d.ts.map +1 -0
- package/dist/agents/operator/tools/entity-write.tools.js +471 -0
- package/dist/agents/operator/tools/entity-write.tools.js.map +1 -0
- package/dist/agents/operator/tools/operator.tool.registry.d.ts +5 -1
- package/dist/agents/operator/tools/operator.tool.registry.d.ts.map +1 -1
- package/dist/agents/operator/tools/operator.tool.registry.js +19 -2
- package/dist/agents/operator/tools/operator.tool.registry.js.map +1 -1
- package/dist/agents/operator/tools/search-documents.tool.d.ts +16 -1
- package/dist/agents/operator/tools/search-documents.tool.d.ts.map +1 -1
- package/dist/agents/operator/tools/search-documents.tool.js +59 -3
- package/dist/agents/operator/tools/search-documents.tool.js.map +1 -1
- package/dist/agents/prompts/index.d.ts +1 -0
- package/dist/agents/prompts/index.d.ts.map +1 -1
- package/dist/agents/prompts/index.js +7 -1
- package/dist/agents/prompts/index.js.map +1 -1
- package/dist/agents/responder/contexts/responder.context.d.ts +12 -0
- package/dist/agents/responder/contexts/responder.context.d.ts.map +1 -1
- package/dist/agents/responder/contexts/responder.context.js +4 -0
- package/dist/agents/responder/contexts/responder.context.js.map +1 -1
- package/dist/agents/responder/factories/responder.context.factory.d.ts +4 -0
- package/dist/agents/responder/factories/responder.context.factory.d.ts.map +1 -1
- package/dist/agents/responder/factories/responder.context.factory.js +2 -0
- package/dist/agents/responder/factories/responder.context.factory.js.map +1 -1
- package/dist/agents/responder/nodes/graph.node.service.d.ts.map +1 -1
- package/dist/agents/responder/nodes/graph.node.service.js +18 -2
- package/dist/agents/responder/nodes/graph.node.service.js.map +1 -1
- package/dist/agents/responder/nodes/responder.answer.node.service.d.ts +32 -0
- package/dist/agents/responder/nodes/responder.answer.node.service.d.ts.map +1 -1
- package/dist/agents/responder/nodes/responder.answer.node.service.js +31 -19
- package/dist/agents/responder/nodes/responder.answer.node.service.js.map +1 -1
- package/dist/agents/responder/services/responder.service.d.ts +4 -0
- package/dist/agents/responder/services/responder.service.d.ts.map +1 -1
- package/dist/agents/responder/services/responder.service.js +2 -0
- package/dist/agents/responder/services/responder.service.js.map +1 -1
- package/dist/common/interfaces/entity.schema.interface.d.ts +16 -0
- package/dist/common/interfaces/entity.schema.interface.d.ts.map +1 -1
- package/dist/config/interfaces/config.prompts.interface.d.ts +12 -0
- package/dist/config/interfaces/config.prompts.interface.d.ts.map +1 -1
- package/dist/core/blocknote/services/blocknote.service.d.ts +23 -14
- package/dist/core/blocknote/services/blocknote.service.d.ts.map +1 -1
- package/dist/core/blocknote/services/blocknote.service.js +48 -32
- package/dist/core/blocknote/services/blocknote.service.js.map +1 -1
- package/dist/core/email/email.helpers.d.ts +18 -0
- package/dist/core/email/email.helpers.d.ts.map +1 -0
- package/dist/core/email/email.helpers.js +50 -0
- package/dist/core/email/email.helpers.js.map +1 -0
- package/dist/core/email/index.d.ts +1 -0
- package/dist/core/email/index.d.ts.map +1 -1
- package/dist/core/email/index.js +2 -0
- package/dist/core/email/index.js.map +1 -1
- package/dist/core/email/services/email.service.d.ts.map +1 -1
- package/dist/core/email/services/email.service.js +4 -4
- package/dist/core/email/services/email.service.js.map +1 -1
- package/dist/core/llm/services/llm.service.d.ts.map +1 -1
- package/dist/core/llm/services/llm.service.js +34 -2
- package/dist/core/llm/services/llm.service.js.map +1 -1
- package/dist/core/llm/services/tool-error-feedback.d.ts +40 -0
- package/dist/core/llm/services/tool-error-feedback.d.ts.map +1 -0
- package/dist/core/llm/services/tool-error-feedback.js +131 -0
- package/dist/core/llm/services/tool-error-feedback.js.map +1 -0
- package/dist/foundations/assistant/assistant.module.d.ts.map +1 -1
- package/dist/foundations/assistant/assistant.module.js +7 -1
- package/dist/foundations/assistant/assistant.module.js.map +1 -1
- package/dist/foundations/assistant/controllers/assistant.controller.d.ts +13 -1
- package/dist/foundations/assistant/controllers/assistant.controller.d.ts.map +1 -1
- package/dist/foundations/assistant/controllers/assistant.controller.js +37 -3
- package/dist/foundations/assistant/controllers/assistant.controller.js.map +1 -1
- package/dist/foundations/assistant/dtos/assistant-append.dto.d.ts +6 -0
- package/dist/foundations/assistant/dtos/assistant-append.dto.d.ts.map +1 -1
- package/dist/foundations/assistant/dtos/assistant-append.dto.js +1 -1
- package/dist/foundations/assistant/dtos/assistant-append.dto.js.map +1 -1
- package/dist/foundations/assistant/dtos/assistant-post.dto.d.ts +29 -0
- package/dist/foundations/assistant/dtos/assistant-post.dto.d.ts.map +1 -1
- package/dist/foundations/assistant/dtos/assistant-post.dto.js +47 -2
- package/dist/foundations/assistant/dtos/assistant-post.dto.js.map +1 -1
- package/dist/foundations/assistant/repositories/assistant.repository.d.ts +22 -0
- package/dist/foundations/assistant/repositories/assistant.repository.d.ts.map +1 -1
- package/dist/foundations/assistant/repositories/assistant.repository.js +28 -0
- package/dist/foundations/assistant/repositories/assistant.repository.js.map +1 -1
- package/dist/foundations/assistant/services/assistant-mention.extractor.d.ts +36 -0
- package/dist/foundations/assistant/services/assistant-mention.extractor.d.ts.map +1 -0
- package/dist/foundations/assistant/services/assistant-mention.extractor.js +99 -0
- package/dist/foundations/assistant/services/assistant-mention.extractor.js.map +1 -0
- package/dist/foundations/assistant/services/assistant.service.d.ts +84 -2
- package/dist/foundations/assistant/services/assistant.service.d.ts.map +1 -1
- package/dist/foundations/assistant/services/assistant.service.js +312 -60
- package/dist/foundations/assistant/services/assistant.service.js.map +1 -1
- package/dist/foundations/auth/services/auth.service.d.ts.map +1 -1
- package/dist/foundations/auth/services/auth.service.js +2 -0
- package/dist/foundations/auth/services/auth.service.js.map +1 -1
- package/dist/foundations/user/services/user.service.d.ts.map +1 -1
- package/dist/foundations/user/services/user.service.js +3 -0
- package/dist/foundations/user/services/user.service.js.map +1 -1
- package/dist/foundations/waitlist/services/waitlist.service.d.ts.map +1 -1
- package/dist/foundations/waitlist/services/waitlist.service.js +2 -0
- package/dist/foundations/waitlist/services/waitlist.service.js.map +1 -1
- 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
|
|
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:
|
|
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
|
-
|
|
106
|
-
userId: params.userId,
|
|
107
|
-
userModuleIds,
|
|
127
|
+
ctx,
|
|
108
128
|
priorMessages: [],
|
|
109
|
-
newUserMessage: { role: "user", content:
|
|
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:
|
|
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
|
-
|
|
185
|
-
userId: params.userId,
|
|
186
|
-
userModuleIds,
|
|
219
|
+
ctx,
|
|
187
220
|
priorMessages,
|
|
188
|
-
newUserMessage: { role: "user", content:
|
|
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
|
|
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:
|
|
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 =
|
|
321
|
+
const contentScope = this.toContentScope(params.boundContent);
|
|
269
322
|
const result = await this.runOperatorTurn({
|
|
270
|
-
|
|
271
|
-
userId: params.userId,
|
|
272
|
-
userModuleIds,
|
|
323
|
+
ctx,
|
|
273
324
|
priorMessages: [],
|
|
274
|
-
question:
|
|
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:
|
|
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
|
-
|
|
326
|
-
userId: params.userId,
|
|
327
|
-
userModuleIds,
|
|
388
|
+
ctx,
|
|
328
389
|
priorMessages,
|
|
329
|
-
question:
|
|
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.
|
|
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.
|
|
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.
|
|
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,
|
|
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
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
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
|
-
|
|
721
|
-
|
|
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
|