@noodleseed/one 0.177.0 → 0.178.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/commands/solutions-activity.d.ts.map +1 -1
- package/dist/commands/solutions-activity.js +9 -35
- package/dist/commands/solutions-activity.js.map +1 -1
- package/dist/commands/solutions-conversations.d.ts.map +1 -1
- package/dist/commands/solutions-conversations.js +60 -6
- package/dist/commands/solutions-conversations.js.map +1 -1
- package/dist/commands/solutions-history.d.ts +9 -0
- package/dist/commands/solutions-history.d.ts.map +1 -0
- package/dist/commands/solutions-history.js +166 -0
- package/dist/commands/solutions-history.js.map +1 -0
- package/dist/commands/solutions-ops.d.ts.map +1 -1
- package/dist/commands/solutions-ops.js +3 -0
- package/dist/commands/solutions-ops.js.map +1 -1
- package/dist/commands/whatsapp-ops.d.ts.map +1 -1
- package/dist/commands/whatsapp-ops.js +0 -7
- package/dist/commands/whatsapp-ops.js.map +1 -1
- package/node_modules/@noodle-borg/agent-kit/dist/generated/example-files.js +1 -1
- package/node_modules/@noodle-borg/agent-kit/dist/skill-embedded-assistant-ref.js +14 -0
- package/node_modules/@noodle-borg/agent-kit/package.json +1 -1
- package/node_modules/@noodle-borg/assistant-gateway/dist/public-surface.d.ts +6 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/public-surface.js +5 -2
- package/node_modules/@noodle-borg/assistant-gateway/package.json +1 -1
- package/node_modules/@noodle-borg/authoring/dist/assistant.d.ts +18 -0
- package/node_modules/@noodle-borg/authoring/dist/assistant.js +14 -1
- package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-channels.js +0 -3
- package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-solution-conversations.js +50 -5
- package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-solution-history.js +82 -0
- package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-solutions.js +3 -42
- package/node_modules/@noodle-borg/compiler/dist/manifest/schema.d.ts +8 -0
- package/node_modules/@noodle-borg/compiler/dist/manifest/schema.js +7 -0
- package/node_modules/@noodle-borg/service/dist/application-activity.js +76 -21
- package/node_modules/@noodle-borg/service/dist/application-history-settings.js +108 -0
- package/node_modules/@noodle-borg/service/dist/assistant-composition.js +11 -2
- package/node_modules/@noodle-borg/service/dist/channels/composition.js +10 -1
- package/node_modules/@noodle-borg/service/dist/channels/provider.js +13 -13
- package/node_modules/@noodle-borg/service/dist/channels/runtime.js +31 -5
- package/node_modules/@noodle-borg/service/dist/conversation-history/capture.js +63 -7
- package/node_modules/@noodle-borg/service/dist/conversation-history/contracts.js +13 -0
- package/node_modules/@noodle-borg/service/dist/conversation-history/customer.js +118 -0
- package/node_modules/@noodle-borg/service/dist/conversation-history/operator.js +133 -24
- package/node_modules/@noodle-borg/service/dist/routes/assistant-conversations.js +52 -0
- package/node_modules/@noodle-borg/service/dist/routes/assistant-dispatch.js +6 -0
- package/node_modules/@noodle-borg/service/dist/routes/assistant-elevation.js +3 -0
- package/node_modules/@noodle-borg/service/dist/routes/assistant-interaction-stream.js +2 -1
- package/node_modules/@noodle-borg/service/dist/routes/assistant-interactions.js +6 -11
- package/node_modules/@noodle-borg/service/dist/routes/assistant-public-session.js +6 -0
- package/node_modules/@noodle-borg/service/dist/routes/assistant-route-http.js +16 -0
- package/node_modules/@noodle-borg/service/dist/routes/assistant.js +5 -17
- package/node_modules/@noodle-borg/service/dist/routes/business-information-activity.js +6 -5
- package/node_modules/@noodle-borg/service/dist/routes/business-information-conversations.js +36 -11
- package/node_modules/@noodle-borg/service/dist/routes/business-information-dispatch.js +4 -2
- package/node_modules/@noodle-borg/service/dist/routes/business-information-paths.js +4 -2
- package/node_modules/@noodle-borg/service/dist/routes/business-information.js +4 -0
- package/node_modules/@noodle-borg/service/dist/routes/whatsapp.js +0 -10
- package/node_modules/@noodle-borg/service/dist/service-history-projections.js +32 -4
- package/node_modules/@noodle-borg/service/dist/service-resource-cleanup.js +32 -0
- package/node_modules/@noodle-borg/service/dist/service.js +4 -3
- package/node_modules/@noodle-borg/service/package.json +1 -1
- package/node_modules/@noodle-borg/wire-contracts/dist/application-activity.d.ts +0 -28
- package/node_modules/@noodle-borg/wire-contracts/dist/application-activity.js +0 -18
- package/node_modules/@noodle-borg/wire-contracts/dist/application-history-settings.d.ts +190 -0
- package/node_modules/@noodle-borg/wire-contracts/dist/application-history-settings.js +92 -0
- package/node_modules/@noodle-borg/wire-contracts/dist/assistant.d.ts +3 -0
- package/node_modules/@noodle-borg/wire-contracts/dist/assistant.js +6 -0
- package/node_modules/@noodle-borg/wire-contracts/dist/conversation-history.d.ts +176 -0
- package/node_modules/@noodle-borg/wire-contracts/dist/conversation-history.js +74 -2
- package/node_modules/@noodle-borg/wire-contracts/dist/index.d.ts +1 -0
- package/node_modules/@noodle-borg/wire-contracts/dist/index.js +1 -0
- package/node_modules/@noodleseed/assistant/README.md +19 -0
- package/node_modules/@noodleseed/assistant/package.json +1 -1
- package/package.json +2 -2
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { createHmac } from 'node:crypto';
|
|
2
2
|
import { canonicalJson } from '@noodle-borg/compiler';
|
|
3
|
-
import { ConversationExportResponseSchema, ConversationForgetRequestSchema, ConversationForgetResponseSchema, ConversationIdSchema, ConversationListResponseSchema, ConversationShowResponseSchema, } from '@noodle-borg/wire-contracts';
|
|
3
|
+
import { ConversationExportResponseSchema, ConversationForgetRequestSchema, ConversationForgetResponseSchema, ConversationIdSchema, ConversationListResponseSchema, ConversationReviewRequestSchema, ConversationReviewStatusSchema, ConversationShowResponseSchema, } from '@noodle-borg/wire-contracts';
|
|
4
|
+
import { maskPaymentCards } from '../payment-card.js';
|
|
5
|
+
import { CONVERSATION_DAY_MS, CONVERSATION_NOTE_LIMIT, } from './contracts.js';
|
|
4
6
|
const runLocal = (operation) => operation();
|
|
5
7
|
const MESSAGES = {
|
|
6
8
|
conversation_invalid: 'Invalid conversation request.',
|
|
@@ -17,7 +19,8 @@ export class ConversationHistoryError extends Error {
|
|
|
17
19
|
const PAGE = { list: { default: 50, maximum: 100 }, export: { default: 25, maximum: 25 } };
|
|
18
20
|
/**
|
|
19
21
|
* The staff projection of conversation history (ADR 0241). Callers authorize before and after, and every
|
|
20
|
-
* read, export and forget returns identifier-only audit details; message
|
|
22
|
+
* read, review, note, export and forget returns identifier-only audit details; message and note text
|
|
23
|
+
* never reach audit.
|
|
21
24
|
*/
|
|
22
25
|
export class ApplicationConversations {
|
|
23
26
|
options;
|
|
@@ -26,7 +29,7 @@ export class ApplicationConversations {
|
|
|
26
29
|
}
|
|
27
30
|
async project(scope, action, input, local = runLocal) {
|
|
28
31
|
const tenant = { org: scope.org, app: scope.app, env: scope.env };
|
|
29
|
-
const allowed = action === 'list' || action === 'export' ? ['limit', 'cursor', 'channel'] : [];
|
|
32
|
+
const allowed = action === 'list' || action === 'export' ? ['limit', 'cursor', 'channel', 'status'] : [];
|
|
30
33
|
const parameters = new Map();
|
|
31
34
|
for (const [key, value] of input.parameters) {
|
|
32
35
|
if (!allowed.includes(key) || parameters.has(key))
|
|
@@ -37,36 +40,94 @@ export class ApplicationConversations {
|
|
|
37
40
|
eventType,
|
|
38
41
|
details,
|
|
39
42
|
});
|
|
43
|
+
if (action === 'review') {
|
|
44
|
+
const id = ConversationIdSchema.safeParse(input.conversationId);
|
|
45
|
+
if (!id.success)
|
|
46
|
+
throw new ConversationHistoryError('conversation_not_found');
|
|
47
|
+
const parsed = ConversationReviewRequestSchema.safeParse(input.body);
|
|
48
|
+
if (!parsed.success || (parsed.data.note !== undefined && !input.actor))
|
|
49
|
+
throw invalid();
|
|
50
|
+
const { reviewStatus, note } = parsed.data;
|
|
51
|
+
const now = this.#now();
|
|
52
|
+
const notBefore = await this.#notBefore(tenant, now);
|
|
53
|
+
const conversation = await local(async () => {
|
|
54
|
+
const current = await this.options.store.read(tenant, id.data, now, notBefore);
|
|
55
|
+
if (!current)
|
|
56
|
+
throw new ConversationHistoryError('conversation_not_found');
|
|
57
|
+
if (note !== undefined && current.notes.length >= CONVERSATION_NOTE_LIMIT)
|
|
58
|
+
throw invalid();
|
|
59
|
+
if (reviewStatus !== undefined)
|
|
60
|
+
await this.options.store.setReviewStatus(tenant, id.data, reviewStatus);
|
|
61
|
+
if (note !== undefined)
|
|
62
|
+
await this.options.store.addNote(tenant, id.data, {
|
|
63
|
+
author: String(input.actor),
|
|
64
|
+
text: maskPaymentCards(note),
|
|
65
|
+
at: now,
|
|
66
|
+
});
|
|
67
|
+
return this.options.store.read(tenant, id.data, now, notBefore);
|
|
68
|
+
});
|
|
69
|
+
if (!conversation)
|
|
70
|
+
throw new ConversationHistoryError('conversation_not_found');
|
|
71
|
+
return {
|
|
72
|
+
response: ConversationShowResponseSchema.parse({
|
|
73
|
+
ok: true,
|
|
74
|
+
data: { conversation: shown(conversation, now - notBefore) },
|
|
75
|
+
}),
|
|
76
|
+
audits: [
|
|
77
|
+
...(reviewStatus === undefined
|
|
78
|
+
? []
|
|
79
|
+
: [audit('conversation.reviewed', { conversationId: id.data, reviewStatus })]),
|
|
80
|
+
...(note === undefined ? [] : [audit('conversation.noted', { conversationId: id.data })]),
|
|
81
|
+
],
|
|
82
|
+
};
|
|
83
|
+
}
|
|
40
84
|
if (action === 'forget') {
|
|
41
85
|
const parsed = ConversationForgetRequestSchema.safeParse(input.body);
|
|
42
86
|
if (!parsed.success)
|
|
43
87
|
throw invalid();
|
|
44
88
|
const request = parsed.data;
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
89
|
+
// A WhatsApp participant's working memory goes with their history (ADR 0241); a web session
|
|
90
|
+
// expires on its own, so a customer has none to erase.
|
|
91
|
+
const { forgotten, workingMemory } = await local(async () => {
|
|
92
|
+
if ('conversationId' in request)
|
|
93
|
+
return {
|
|
94
|
+
forgotten: await this.options.store.forget(tenant, request.conversationId),
|
|
95
|
+
workingMemory: false,
|
|
96
|
+
};
|
|
97
|
+
const erased = await this.options.store.forgetSubject(tenant, request.subject);
|
|
98
|
+
return {
|
|
99
|
+
forgotten: erased,
|
|
100
|
+
workingMemory: request.subject.kind === 'participant' &&
|
|
101
|
+
(await input.forgetWorkingMemory?.(tenant, request.subject.ref, String(input.actor))) === true,
|
|
102
|
+
};
|
|
103
|
+
});
|
|
48
104
|
return {
|
|
49
105
|
response: ConversationForgetResponseSchema.parse({ ok: true, data: { forgotten } }),
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
106
|
+
audits: [
|
|
107
|
+
audit('conversation.forgotten', {
|
|
108
|
+
conversations: forgotten.conversations,
|
|
109
|
+
items: forgotten.items,
|
|
110
|
+
kind: 'conversationId' in request ? 'conversation' : request.subject.kind,
|
|
111
|
+
workingMemory,
|
|
112
|
+
}),
|
|
113
|
+
],
|
|
55
114
|
};
|
|
56
115
|
}
|
|
57
116
|
if (action === 'show') {
|
|
58
117
|
const id = ConversationIdSchema.safeParse(input.conversationId);
|
|
59
118
|
if (!id.success)
|
|
60
119
|
throw new ConversationHistoryError('conversation_not_found');
|
|
61
|
-
const
|
|
120
|
+
const now = this.#now();
|
|
121
|
+
const notBefore = await this.#notBefore(tenant, now);
|
|
122
|
+
const conversation = await local(() => this.options.store.read(tenant, id.data, now, notBefore));
|
|
62
123
|
if (!conversation)
|
|
63
124
|
throw new ConversationHistoryError('conversation_not_found');
|
|
64
125
|
return {
|
|
65
126
|
response: ConversationShowResponseSchema.parse({
|
|
66
127
|
ok: true,
|
|
67
|
-
data: { conversation:
|
|
128
|
+
data: { conversation: shown(conversation, now - notBefore) },
|
|
68
129
|
}),
|
|
69
|
-
|
|
130
|
+
audits: [audit('conversation.read', { conversationId: id.data })],
|
|
70
131
|
};
|
|
71
132
|
}
|
|
72
133
|
const page = await this.#page(scope, tenant, action, parameters, local);
|
|
@@ -79,11 +140,13 @@ export class ApplicationConversations {
|
|
|
79
140
|
...(page.nextCursor ? { nextCursor: page.nextCursor } : {}),
|
|
80
141
|
},
|
|
81
142
|
}),
|
|
143
|
+
audits: [],
|
|
82
144
|
};
|
|
83
145
|
const conversations = await local(async () => {
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
146
|
+
const read = await Promise.all(page.summaries.map((row) => this.options.store.read(tenant, row.id, page.now, page.notBefore)));
|
|
147
|
+
return read
|
|
148
|
+
.filter((row) => row !== undefined)
|
|
149
|
+
.map((row) => detail(row, page.now - page.notBefore));
|
|
87
150
|
});
|
|
88
151
|
return {
|
|
89
152
|
response: ConversationExportResponseSchema.parse({
|
|
@@ -93,18 +156,20 @@ export class ApplicationConversations {
|
|
|
93
156
|
...(page.nextCursor ? { nextCursor: page.nextCursor } : {}),
|
|
94
157
|
},
|
|
95
158
|
}),
|
|
96
|
-
|
|
159
|
+
audits: [audit('conversation.exported', { count: conversations.length })],
|
|
97
160
|
};
|
|
98
161
|
}
|
|
99
162
|
async #page(scope, tenant, purpose, parameters, local) {
|
|
100
163
|
const bounds = PAGE[purpose];
|
|
101
164
|
const limit = parameters.has('limit') ? Number(parameters.get('limit')) : bounds.default;
|
|
102
165
|
const channel = parameters.get('channel');
|
|
166
|
+
const status = parameters.get('status');
|
|
103
167
|
const cursor = parameters.get('cursor');
|
|
104
168
|
if (!Number.isInteger(limit) ||
|
|
105
169
|
limit < 1 ||
|
|
106
170
|
limit > bounds.maximum ||
|
|
107
171
|
(channel !== undefined && channel !== 'website' && channel !== 'whatsapp') ||
|
|
172
|
+
(status !== undefined && !ConversationReviewStatusSchema.safeParse(status).success) ||
|
|
108
173
|
(cursor !== undefined && (!cursor || cursor.length > 2048)))
|
|
109
174
|
throw invalid();
|
|
110
175
|
const binding = this.#hash({
|
|
@@ -112,26 +177,42 @@ export class ApplicationConversations {
|
|
|
112
177
|
scope,
|
|
113
178
|
limit,
|
|
114
179
|
channel: channel ?? null,
|
|
180
|
+
status: status ?? null,
|
|
115
181
|
});
|
|
116
182
|
const after = decodeConversationCursor(cursor, binding);
|
|
183
|
+
const now = this.#now();
|
|
184
|
+
const notBefore = await this.#notBefore(tenant, now);
|
|
117
185
|
const rows = await local(() => this.options.store.list(tenant, {
|
|
118
|
-
now
|
|
186
|
+
now,
|
|
187
|
+
notBefore,
|
|
119
188
|
limit: limit + 1,
|
|
120
189
|
...(after ? { after } : {}),
|
|
121
190
|
...(channel ? { channel: channel } : {}),
|
|
191
|
+
...(status ? { reviewStatus: status } : {}),
|
|
122
192
|
}));
|
|
123
193
|
const summaries = rows.slice(0, limit);
|
|
124
194
|
const last = summaries.at(-1);
|
|
125
195
|
return {
|
|
196
|
+
now,
|
|
197
|
+
notBefore,
|
|
126
198
|
summaries,
|
|
127
|
-
nextCursor: rows.length > limit && last
|
|
128
|
-
? Buffer.from(JSON.stringify({ binding, lastMessageAt: last.lastMessageAt, id: last.id })).toString('base64url')
|
|
129
|
-
: undefined,
|
|
199
|
+
nextCursor: rows.length > limit && last ? encodeConversationCursor(binding, last) : undefined,
|
|
130
200
|
};
|
|
131
201
|
}
|
|
132
202
|
#now() {
|
|
133
203
|
return this.options.now?.() ?? Date.now();
|
|
134
204
|
}
|
|
205
|
+
/**
|
|
206
|
+
* Oldest visible item time under the live window. Items the window hides are not deleted: they purge
|
|
207
|
+
* at their stamped expiry, so restoring a longer plan before then shows them again. That recovers
|
|
208
|
+
* nothing deleted, which is all the business history contract forbids.
|
|
209
|
+
*/
|
|
210
|
+
async #notBefore(tenant, now) {
|
|
211
|
+
const policy = await this.options.policy(tenant).catch(() => undefined);
|
|
212
|
+
if (!policy || !Number.isInteger(policy.maximumDays))
|
|
213
|
+
throw new ConversationHistoryError('conversation_unavailable');
|
|
214
|
+
return conversationReadBound(policy, now);
|
|
215
|
+
}
|
|
135
216
|
#hash(value) {
|
|
136
217
|
return createHmac('sha256', this.options.identityKey)
|
|
137
218
|
.update(canonicalJson(value))
|
|
@@ -149,11 +230,30 @@ function summary(row) {
|
|
|
149
230
|
startedAt: new Date(row.startedAt).toISOString(),
|
|
150
231
|
lastMessageAt: new Date(row.lastMessageAt).toISOString(),
|
|
151
232
|
itemCount: row.itemCount,
|
|
233
|
+
reviewStatus: row.reviewStatus,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* The last visible item's effective removal: its stored expiry, or sooner where the live window of
|
|
238
|
+
* `windowMs` hides it first.
|
|
239
|
+
*/
|
|
240
|
+
function removalAt(conversation, windowMs) {
|
|
241
|
+
return Math.max(...conversation.items.map((item) => Math.min(item.expiresAt, item.at + windowMs)));
|
|
242
|
+
}
|
|
243
|
+
function shown(conversation, windowMs) {
|
|
244
|
+
return {
|
|
245
|
+
...detail(conversation, windowMs),
|
|
246
|
+
notes: conversation.notes.map((note) => ({
|
|
247
|
+
author: note.author,
|
|
248
|
+
text: note.text,
|
|
249
|
+
at: new Date(note.at).toISOString(),
|
|
250
|
+
})),
|
|
152
251
|
};
|
|
153
252
|
}
|
|
154
|
-
function detail(conversation) {
|
|
253
|
+
function detail(conversation, windowMs) {
|
|
155
254
|
return {
|
|
156
255
|
...summary({ ...conversation, itemCount: conversation.items.length }),
|
|
256
|
+
expiresAt: new Date(removalAt(conversation, windowMs)).toISOString(),
|
|
157
257
|
items: conversation.items.map((item) => item.kind === 'message'
|
|
158
258
|
? { kind: item.kind, role: item.role, text: item.text, at: new Date(item.at).toISOString() }
|
|
159
259
|
: {
|
|
@@ -165,7 +265,16 @@ function detail(conversation) {
|
|
|
165
265
|
}),
|
|
166
266
|
};
|
|
167
267
|
}
|
|
168
|
-
|
|
268
|
+
/** Oldest visible item time under a policy's live window; Off hides every item. */
|
|
269
|
+
export function conversationReadBound(policy, now) {
|
|
270
|
+
const days = Math.max(0, Math.min(policy.conversationDays ?? 0, policy.maximumDays));
|
|
271
|
+
return days === 0 ? now + 1 : now - days * CONVERSATION_DAY_MS;
|
|
272
|
+
}
|
|
273
|
+
/** An opaque keyset position bound to the purpose, scope and page shape that `binding` hashes. */
|
|
274
|
+
export function encodeConversationCursor(binding, last) {
|
|
275
|
+
return Buffer.from(JSON.stringify({ binding, lastMessageAt: last.lastMessageAt, id: last.id })).toString('base64url');
|
|
276
|
+
}
|
|
277
|
+
export function decodeConversationCursor(raw, binding) {
|
|
169
278
|
if (raw === undefined)
|
|
170
279
|
return undefined;
|
|
171
280
|
try {
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { readJsonBody, sendJson } from '@noodle-borg/transport-http';
|
|
2
|
+
import { ConversationHistoryError } from '../conversation-history/operator.js';
|
|
3
|
+
import { sendUnauthorized } from '../http-util.js';
|
|
4
|
+
import { basicCredentials } from './assistant-route-http.js';
|
|
5
|
+
/**
|
|
6
|
+
* Backend-only conversation routes for an embed client (ADR 0241 decision 12). They authenticate
|
|
7
|
+
* exactly like the session exchange and never read a tenant from the body; there is no browser CORS
|
|
8
|
+
* surface, because the client secret must never reach a browser.
|
|
9
|
+
*/
|
|
10
|
+
export async function handleAssistantConversations(req, res, action, deps) {
|
|
11
|
+
if (req.method !== 'POST') {
|
|
12
|
+
res.setHeader('allow', 'POST');
|
|
13
|
+
return sendJson(res, 405, { error: 'method not allowed' });
|
|
14
|
+
}
|
|
15
|
+
const credentials = basicCredentials(req);
|
|
16
|
+
if (!credentials)
|
|
17
|
+
return sendUnauthorized(res, 'invalid assistant client');
|
|
18
|
+
const client = await deps.store.authenticateClient(credentials.id, credentials.secret);
|
|
19
|
+
if (!client)
|
|
20
|
+
return sendUnauthorized(res, 'invalid assistant client');
|
|
21
|
+
res.setHeader('cache-control', 'private, no-store');
|
|
22
|
+
const body = await readJsonBody(req, deps.maxBody);
|
|
23
|
+
if (!body.ok)
|
|
24
|
+
return sendJson(res, body.status, { error: body.error });
|
|
25
|
+
try {
|
|
26
|
+
const conversations = deps.customerConversations;
|
|
27
|
+
if (!conversations)
|
|
28
|
+
throw new ConversationHistoryError('conversation_unavailable');
|
|
29
|
+
const owner = { id: client.id, tenant: client.tenant };
|
|
30
|
+
const result = action === 'list'
|
|
31
|
+
? await conversations.list(owner, body.value)
|
|
32
|
+
: await conversations.forgetUser(owner, body.value);
|
|
33
|
+
await deps.audit.emit({
|
|
34
|
+
...result.audit,
|
|
35
|
+
org: client.tenant.org,
|
|
36
|
+
app: client.tenant.app,
|
|
37
|
+
env: client.tenant.env,
|
|
38
|
+
decision: 'allow',
|
|
39
|
+
actorSubject: client.id,
|
|
40
|
+
});
|
|
41
|
+
sendJson(res, 200, result.response);
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
if (!(error instanceof ConversationHistoryError))
|
|
45
|
+
throw error;
|
|
46
|
+
sendJson(res, error.code === 'conversation_unavailable' ? 503 : 400, {
|
|
47
|
+
code: error.code,
|
|
48
|
+
error: error.message,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=assistant-conversations.js.map
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { handleAssistantAppRequest, handleAssistantPreflight, handleAssistantSession, handleAssistantTurn, handleConsoleApprovalNonce, } from './assistant.js';
|
|
2
2
|
import { handleAssistantAppearance } from './assistant-appearance.js';
|
|
3
3
|
import { handleAssistantClients } from './assistant-clients.js';
|
|
4
|
+
import { handleAssistantConversations } from './assistant-conversations.js';
|
|
4
5
|
import { handleAssistantDoctor } from './assistant-doctor.js';
|
|
5
6
|
import { handleAssistantEmbedScript } from './assistant-embed-script.js';
|
|
6
7
|
import { handleAssistantEmbeds } from './assistant-embeds.js';
|
|
@@ -60,6 +61,11 @@ export function dispatchAssistantRoutes(req, res, url, deps) {
|
|
|
60
61
|
if (url.pathname === '/v1/assistant/sessions' && req.method === 'POST') {
|
|
61
62
|
return run(req, res, deps, () => handleAssistantSession(req, res, deps));
|
|
62
63
|
}
|
|
64
|
+
const conversationsMatch = /^\/v1\/assistant\/conversations\/(list|forget-user)$/.exec(url.pathname);
|
|
65
|
+
if (conversationsMatch?.[1] === 'list' || conversationsMatch?.[1] === 'forget-user') {
|
|
66
|
+
const action = conversationsMatch[1];
|
|
67
|
+
return run(req, res, deps, () => handleAssistantConversations(req, res, action, deps));
|
|
68
|
+
}
|
|
63
69
|
if (url.pathname === '/v1/assistant/turns' && req.method === 'POST') {
|
|
64
70
|
return run(req, res, deps, () => handleAssistantTurn(req, res, deps));
|
|
65
71
|
}
|
|
@@ -3,6 +3,7 @@ import { anonymousBehavior } from '@noodle-borg/compiler';
|
|
|
3
3
|
import { claimableStateHandleNames } from '@noodle-borg/runtime';
|
|
4
4
|
import { sendJson } from '@noodle-borg/transport-http';
|
|
5
5
|
import { assistantSessionResponseSchema } from '@noodle-borg/wire-contracts';
|
|
6
|
+
import { sessionHistoryNotice, } from '../conversation-history/capture.js';
|
|
6
7
|
function isMixed(surface) {
|
|
7
8
|
return surface?.mode === 'mixed';
|
|
8
9
|
}
|
|
@@ -147,6 +148,8 @@ export async function elevateAssistantSession(res, deps, input) {
|
|
|
147
148
|
// The armed hint: the widget answers with one { resume: true } turn on the turns endpoint.
|
|
148
149
|
...(elevated.resumeArmed ? { resume: { tool: elevated.tool } } : {}),
|
|
149
150
|
continuedAfterAuthentication: true,
|
|
151
|
+
// The caller is now verified, so the notice states the signed-in customers' window.
|
|
152
|
+
...(await sessionHistoryNotice(deps.conversations, input.client.tenant, 'signed_in_customers', input.surfaceHistory)),
|
|
150
153
|
};
|
|
151
154
|
assistantSessionResponseSchema.parse(body);
|
|
152
155
|
return sendJson(res, 200, body);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { assistantConfirmationProposal } from '@noodle-borg/assistant-gateway/portable';
|
|
2
|
+
import { sessionSurfaceHistory } from '../conversation-history/capture.js';
|
|
2
3
|
import { narrateInteractionResolution } from './assistant-agent.js';
|
|
3
4
|
/** Stream the optional post-resolution narration without weakening the durable structured result. */
|
|
4
5
|
export async function narrateResolvedInteraction(res, deps, target, session, tool, action, result, context, suggestions = false) {
|
|
@@ -10,7 +11,7 @@ export async function narrateResolvedInteraction(res, deps, target, session, too
|
|
|
10
11
|
{ role: 'assistant', content: generated.narration, kind: 'visible' },
|
|
11
12
|
];
|
|
12
13
|
await deps.store.appendHistory(session.id, rows);
|
|
13
|
-
await deps.conversations?.recordSessionTurn(session, rows);
|
|
14
|
+
await deps.conversations?.recordSessionTurn(session, rows, sessionSurfaceHistory(target.served.artifact.server.assistant, session));
|
|
14
15
|
}
|
|
15
16
|
if (generated.suggestions.length > 0) {
|
|
16
17
|
await deps.store.replaceLatestSuggestions(session.id, {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { AssistantInteractionCapacityError, assistantConfirmationReview, assistantConfirmationTransition, assistantInputTransition, assistantInteractionTool, assistantPreparedArgumentReview, assistantPreparedToolContinuation, assistantSafeOutput, assistantToolContinuation, assistantViewAvailableData, auditAssistantInteraction, boundedAssistantExecution, recoverableAssistantView, withAssistantSessionExecutionAuthority, } from '@noodle-borg/assistant-gateway/portable';
|
|
2
2
|
import { executePreparedTool, executeToolInteractive, hasEphemeralEvidence, resumeTool, resumeToolPreparation, } from '@noodle-borg/runtime';
|
|
3
3
|
import { readJsonBody, sendJson } from '@noodle-borg/transport-http';
|
|
4
|
+
import { sessionSurfaceHistory } from '../conversation-history/capture.js';
|
|
4
5
|
import { applyBrowserCors, authenticateSession, now, } from './assistant.js';
|
|
5
6
|
import { replayOrConflict, writeUnknownOutcome } from './assistant-interaction-replay.js';
|
|
6
7
|
import { parseInteractionResponse, validateInputContent } from './assistant-interaction-request.js';
|
|
@@ -340,11 +341,7 @@ async function finishExecution(res, deps, session, interaction, context, result,
|
|
|
340
341
|
status: 'failed',
|
|
341
342
|
reasonCode: code,
|
|
342
343
|
});
|
|
343
|
-
await deps.conversations?.recordSessionOutcome(session, {
|
|
344
|
-
interactionId: interaction.id,
|
|
345
|
-
tool: toolName,
|
|
346
|
-
status: 'failed',
|
|
347
|
-
});
|
|
344
|
+
await deps.conversations?.recordSessionOutcome(session, { interactionId: interaction.id, tool: toolName, status: 'failed' }, sessionSurfaceHistory(target.served.artifact.server.assistant, session));
|
|
348
345
|
writeInteractionSseHeaders(res, session.origin);
|
|
349
346
|
writeInteractionResolved(res, interaction.id, 'accept');
|
|
350
347
|
writeInteractionError(res, code, false);
|
|
@@ -375,11 +372,7 @@ async function finishExecution(res, deps, session, interaction, context, result,
|
|
|
375
372
|
kind: 'narration',
|
|
376
373
|
},
|
|
377
374
|
]);
|
|
378
|
-
await deps.conversations?.recordSessionOutcome(session, {
|
|
379
|
-
interactionId: interaction.id,
|
|
380
|
-
tool: toolName,
|
|
381
|
-
status: 'succeeded',
|
|
382
|
-
});
|
|
375
|
+
await deps.conversations?.recordSessionOutcome(session, { interactionId: interaction.id, tool: toolName, status: 'succeeded' }, sessionSurfaceHistory(target.served.artifact.server.assistant, session));
|
|
383
376
|
writeInteractionSseHeaders(res, session.origin);
|
|
384
377
|
writeInteractionResolved(res, interaction.id, 'accept');
|
|
385
378
|
writeToolCompleted(res, interaction.id, toolName, safeOutput);
|
|
@@ -463,7 +456,9 @@ async function resolveStop(req, res, deps, session, interaction, action, suggest
|
|
|
463
456
|
interactionId: interaction.id,
|
|
464
457
|
tool: toolName,
|
|
465
458
|
status: action === 'decline' ? 'declined' : 'cancelled',
|
|
466
|
-
}
|
|
459
|
+
},
|
|
460
|
+
// A vanished target leaves the surface unreadable, which records nothing (fail closed).
|
|
461
|
+
sessionSurfaceHistory(target?.served.artifact.server.assistant, session));
|
|
467
462
|
writeInteractionSseHeaders(res, session.origin);
|
|
468
463
|
writeInteractionResolved(res, interaction.id, action);
|
|
469
464
|
if (target && context) {
|
|
@@ -4,6 +4,7 @@ import { effectiveAssistantBrowserConfiguration, mintPublicSession, publicEmbedT
|
|
|
4
4
|
import { assistantSessionUsageRequestEvent, captureAssistantUsage, captureRefusedAssistantSession, } from '@noodle-borg/observability';
|
|
5
5
|
import { readJsonBody, sendJson } from '@noodle-borg/transport-http';
|
|
6
6
|
import { assistantSessionResponseSchema } from '@noodle-borg/wire-contracts';
|
|
7
|
+
import { sessionHistoryNotice, websiteSurfaceHistory, } from '../conversation-history/capture.js';
|
|
7
8
|
import { applyBrowserCors, assistantSessionEndpoints, now } from './assistant-route-http.js';
|
|
8
9
|
import { activeAssistantTarget } from './assistant-session-target.js';
|
|
9
10
|
/**
|
|
@@ -35,6 +36,7 @@ export async function handlePublicAssistantSession(req, res, deps) {
|
|
|
35
36
|
let configuration;
|
|
36
37
|
let sponsoredBudget;
|
|
37
38
|
let usageSession;
|
|
39
|
+
let surfaceHistory = { historyDisabled: true };
|
|
38
40
|
const result = await mintPublicSession({ embedId, visitorId, origin, ...(addressBucket ? { addressBucket } : {}) }, deps.admissionEnvelope ?? ADMISSION_DEFAULTS, {
|
|
39
41
|
embeds,
|
|
40
42
|
counters,
|
|
@@ -59,6 +61,7 @@ export async function handlePublicAssistantSession(req, res, deps) {
|
|
|
59
61
|
!publicSurfaceOf(target.served.artifact.server.assistant)?.origins.includes(input.origin))
|
|
60
62
|
throw new Error('assistant surface changed mid-mint');
|
|
61
63
|
const current = now(deps);
|
|
64
|
+
surfaceHistory = websiteSurfaceHistory(target.served.artifact.server.assistant, 'public');
|
|
62
65
|
configuration = (await effectiveAssistantBrowserConfiguration(target.served.artifact.server, publicEmbedTenant(input.embed), deps.appearance, 'public', target.businessNotice)).effective;
|
|
63
66
|
const created = await deps.store.createSession({
|
|
64
67
|
// A public surface has no client credential, so the embed id stands in as the minting
|
|
@@ -99,6 +102,9 @@ export async function handlePublicAssistantSession(req, res, deps) {
|
|
|
99
102
|
expiresAt: result.expiresAt,
|
|
100
103
|
endpoints: assistantSessionEndpoints(deps.serviceBase(req)),
|
|
101
104
|
...(configuration ? { configuration } : {}),
|
|
105
|
+
...(usageSession
|
|
106
|
+
? await sessionHistoryNotice(deps.conversations, usageSession.tenant, 'website_visitors', surfaceHistory)
|
|
107
|
+
: {}),
|
|
102
108
|
};
|
|
103
109
|
// Parse rather than trust: every published @noodleseed/assistant widget consumes this shape, and the
|
|
104
110
|
// public mint must not be the path that quietly diverges from it (ADR 0151).
|
|
@@ -60,4 +60,20 @@ export function assistantSessionEndpoints(base) {
|
|
|
60
60
|
suggestions: `${base}/v1/assistant/suggestions`,
|
|
61
61
|
};
|
|
62
62
|
}
|
|
63
|
+
/** HTTP Basic embed-client credentials for the backend-only assistant routes. */
|
|
64
|
+
export function basicCredentials(req) {
|
|
65
|
+
const match = /^Basic\s+(.+)$/i.exec(req.headers.authorization ?? '');
|
|
66
|
+
if (!match?.[1])
|
|
67
|
+
return undefined;
|
|
68
|
+
try {
|
|
69
|
+
const decoded = Buffer.from(match[1], 'base64').toString('utf8');
|
|
70
|
+
const index = decoded.indexOf(':');
|
|
71
|
+
return index > 0
|
|
72
|
+
? { id: decoded.slice(0, index), secret: decoded.slice(index + 1) }
|
|
73
|
+
: undefined;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
63
79
|
//# sourceMappingURL=assistant-route-http.js.map
|
|
@@ -8,11 +8,12 @@ import { mapExecutionError, mapResourceContents, mapResourcesList, mapToolOutput
|
|
|
8
8
|
import { executeResource } from '@noodle-borg/runtime';
|
|
9
9
|
import { readJsonBody, sendJson } from '@noodle-borg/transport-http';
|
|
10
10
|
import { assistantMessageTurnRequestSchema, assistantResumeTurnRequestSchema, assistantSessionResponseSchema, } from '@noodle-borg/wire-contracts';
|
|
11
|
+
import { sessionHistoryNotice, sessionSurfaceHistory, websiteSurfaceHistory, } from '../conversation-history/capture.js';
|
|
11
12
|
import { sendForbidden, sendUnauthorized } from '../http-util.js';
|
|
12
13
|
import { createAssistantTurnStats, runAgentTurn } from './assistant-agent.js';
|
|
13
14
|
import { elevateAssistantSession } from './assistant-elevation.js';
|
|
14
15
|
import { executeAssistantKnowledgeSearch, findAssistantKnowledgeComponent, resolveAssistantKnowledge, } from './assistant-knowledge.js';
|
|
15
|
-
import { applyBrowserCors, assistantSessionEndpoints, authenticateSession, handleAssistantPreflight, now, } from './assistant-route-http.js';
|
|
16
|
+
import { applyBrowserCors, assistantSessionEndpoints, authenticateSession, basicCredentials, handleAssistantPreflight, now, } from './assistant-route-http.js';
|
|
16
17
|
import { activeAssistantTarget, sessionScopedTarget } from './assistant-session-target.js';
|
|
17
18
|
import { authorizeControlPlane } from './control-plane.js';
|
|
18
19
|
export { applyBrowserCors, authenticateSession, handleAssistantPreflight, now };
|
|
@@ -141,6 +142,7 @@ export async function handleAssistantSession(req, res, deps) {
|
|
|
141
142
|
caller,
|
|
142
143
|
configuration,
|
|
143
144
|
endpoints: assistantSessionEndpoints(deps.serviceBase(req)),
|
|
145
|
+
surfaceHistory: websiteSurfaceHistory(assistant, surfaceBinding.kind),
|
|
144
146
|
});
|
|
145
147
|
}
|
|
146
148
|
const session = await deps.store.createSession({
|
|
@@ -166,6 +168,7 @@ export async function handleAssistantSession(req, res, deps) {
|
|
|
166
168
|
expiresAt: session.session.expiresAt,
|
|
167
169
|
endpoints: assistantSessionEndpoints(deps.serviceBase(req)),
|
|
168
170
|
...(configuration ? { configuration } : {}),
|
|
171
|
+
...(await sessionHistoryNotice(deps.conversations, client.tenant, 'signed_in_customers', websiteSurfaceHistory(assistant, surfaceBinding.kind))),
|
|
169
172
|
};
|
|
170
173
|
// Every published @noodleseed/assistant widget consumes this shape; parse (don't just test)
|
|
171
174
|
// so a contract break fails loudly at mint time instead of stranding deployed widgets (ADR 0151).
|
|
@@ -344,7 +347,7 @@ export async function handleAssistantTurn(req, res, deps) {
|
|
|
344
347
|
];
|
|
345
348
|
try {
|
|
346
349
|
await deps.store.appendHistory(session.id, rows);
|
|
347
|
-
await deps.conversations?.recordSessionTurn(session, rows);
|
|
350
|
+
await deps.conversations?.recordSessionTurn(session, rows, sessionSurfaceHistory(target?.served.artifact.server.assistant, session));
|
|
348
351
|
}
|
|
349
352
|
catch {
|
|
350
353
|
emit({ event: 'error', data: { code: 'conversation_state_failed', retryable: false } });
|
|
@@ -502,19 +505,4 @@ export async function handleAssistantAppRequest(req, res, deps) {
|
|
|
502
505
|
function isRecord(value) {
|
|
503
506
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
504
507
|
}
|
|
505
|
-
function basicCredentials(req) {
|
|
506
|
-
const match = /^Basic\s+(.+)$/i.exec(req.headers.authorization ?? '');
|
|
507
|
-
if (!match?.[1])
|
|
508
|
-
return undefined;
|
|
509
|
-
try {
|
|
510
|
-
const decoded = Buffer.from(match[1], 'base64').toString('utf8');
|
|
511
|
-
const index = decoded.indexOf(':');
|
|
512
|
-
return index > 0
|
|
513
|
-
? { id: decoded.slice(0, index), secret: decoded.slice(index + 1) }
|
|
514
|
-
: undefined;
|
|
515
|
-
}
|
|
516
|
-
catch {
|
|
517
|
-
return undefined;
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
508
|
//# sourceMappingURL=assistant.js.map
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import { readJsonBody, sendJson } from '@noodle-borg/transport-http';
|
|
2
|
-
import { ActivityPolicyError
|
|
2
|
+
import { ActivityPolicyError } from '../application-activity.js';
|
|
3
3
|
import { businessGrantAllows } from '../business-information/model.js';
|
|
4
4
|
import { requireIdentity, requireInstallationPermission, } from './business-information.js';
|
|
5
5
|
export async function handleApplicationActivity(req, res, url, ref, deps) {
|
|
6
6
|
const coordination = ref.action === 'coordination';
|
|
7
|
+
const history = ref.action === 'history';
|
|
7
8
|
const suffix = url.pathname.split('/').at(-1);
|
|
8
9
|
const action = coordination
|
|
9
10
|
? suffix === 'resolve'
|
|
10
11
|
? 'coordination-resolve'
|
|
11
12
|
: 'coordination-list'
|
|
12
|
-
:
|
|
13
|
-
? req.method === '
|
|
13
|
+
: history
|
|
14
|
+
? req.method === 'PUT'
|
|
14
15
|
? 'save-settings'
|
|
15
16
|
: 'settings'
|
|
16
17
|
: suffix === 'preview' || suffix === 'export'
|
|
@@ -20,8 +21,8 @@ export async function handleApplicationActivity(req, res, url, ref, deps) {
|
|
|
20
21
|
? suffix === 'resolve'
|
|
21
22
|
? ['POST']
|
|
22
23
|
: ['GET']
|
|
23
|
-
:
|
|
24
|
-
? ['GET', '
|
|
24
|
+
: history
|
|
25
|
+
? ['GET', 'PUT']
|
|
25
26
|
: ['GET'];
|
|
26
27
|
if (!allowed.includes(req.method ?? '')) {
|
|
27
28
|
res.setHeader('allow', allowed.join(', '));
|