@bliggy/chat-runtime 0.0.0-stage → 0.3.1
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/README.md +314 -2
- package/index.js +35 -0
- package/package.json +29 -4
- package/src/channels.js +72 -0
- package/src/conversation.js +294 -0
- package/src/memoryTurnStore.js +95 -0
- package/src/pendingActions.js +37 -0
- package/src/runtime.js +525 -0
- package/src/twilio.js +176 -0
package/README.md
CHANGED
|
@@ -1,3 +1,315 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Chat Runtime
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
`@bliggy/chat-runtime` runs conversations and tools using `@bliggy/llm-client`.
|
|
4
|
+
Applications supply their prompts, authorized tools, storage and business rules.
|
|
5
|
+
The package also retains the channel orchestration API from
|
|
6
|
+
`@davejohnson/agent-channel-runtime` for Chat, Email, Twilio SMS and turn-based Voice.
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
npm install @bliggy/chat-runtime
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
There are two application flows:
|
|
13
|
+
|
|
14
|
+
- `runConversation` runs a bounded conversation and tool loop; `streamConversation`
|
|
15
|
+
exposes that same loop as an async iterator. Both delegate tool calls to the
|
|
16
|
+
host's authorization/execution callback.
|
|
17
|
+
- `ChannelRuntime` authenticates channel messages, claims durable turns, checks
|
|
18
|
+
action policy and handles confirmation of pending turns. Its host supplies
|
|
19
|
+
planning, execution and storage callbacks.
|
|
20
|
+
|
|
21
|
+
`runConversation` does not implicitly create a `ChannelRuntime` or provide a
|
|
22
|
+
database. Hosts can use their existing confirmation store or compose the two
|
|
23
|
+
APIs. Model requests, transport retries and provider formats belong to
|
|
24
|
+
`@bliggy/llm-client`; this package does not introduce another provider retry layer.
|
|
25
|
+
|
|
26
|
+
## Streaming conversations
|
|
27
|
+
|
|
28
|
+
```js
|
|
29
|
+
const { runConversation } = require('@bliggy/chat-runtime');
|
|
30
|
+
const { createOpenAIAdapter } = require('@bliggy/llm-client');
|
|
31
|
+
|
|
32
|
+
const outcome = await runConversation({
|
|
33
|
+
messages: [{ role: 'user', content: [{ type: 'text', text: userText }] }],
|
|
34
|
+
tools: authorizedTools,
|
|
35
|
+
maxIterations: 16,
|
|
36
|
+
requestOptions: {
|
|
37
|
+
adapter: createOpenAIAdapter({ apiKey: process.env.OPENAI_API_KEY }),
|
|
38
|
+
provider: 'openai',
|
|
39
|
+
model: configuredModel,
|
|
40
|
+
maxOutputTokens: configuredOutputTokens,
|
|
41
|
+
promptCacheKey: conversationKey
|
|
42
|
+
},
|
|
43
|
+
getSystem: async () => buildCurrentSystemContext(),
|
|
44
|
+
onEvent: async event => sendToBrowser(event),
|
|
45
|
+
onAssistantMessage: async message => saveMessage(message),
|
|
46
|
+
executeTool: async toolCall => {
|
|
47
|
+
// Recheck current permissions and validate input here. Queue actions that
|
|
48
|
+
// need approval rather than executing them; return the actual outcome.
|
|
49
|
+
const result = await authorizeAndRunOrQueue(toolCall);
|
|
50
|
+
return { output: result, isError: false };
|
|
51
|
+
},
|
|
52
|
+
onToolResult: async message => saveMessage(message)
|
|
53
|
+
});
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The returned `status` is `completed`, `max_tokens`, or `limit`. Hosts render
|
|
57
|
+
their own output-budget and iteration-limit notices. Errors propagate to the
|
|
58
|
+
host. The final allowed request is text-only; a provider that disregards it
|
|
59
|
+
cannot execute another tool.
|
|
60
|
+
|
|
61
|
+
Completed replies must have `end_turn`, `stop_sequence`, or `refusal` as their
|
|
62
|
+
normalized stop reason. A `tool_call` stop reason must have matching tool-call
|
|
63
|
+
blocks; terminal text replies must not contain tool calls. Paused, failed,
|
|
64
|
+
incomplete, missing or unknown stop reasons throw `CHAT_RESPONSE_NOT_COMPLETE`
|
|
65
|
+
before response policy, persistence or tool execution. Contradictory tool state
|
|
66
|
+
throws `INVALID_CHAT_TOOL_STATE`. Both errors include `stopReason`. The loop does
|
|
67
|
+
not automatically resume provider-side paused work.
|
|
68
|
+
|
|
69
|
+
The loop forwards stream events but persists and executes only after a complete
|
|
70
|
+
final result and successful stream completion. Every tool call in that result
|
|
71
|
+
gets a sequential execution callback and matching result message. A tool outcome
|
|
72
|
+
may return `forceText: true` to make the next request text-only. Execution errors
|
|
73
|
+
must be converted by the host to safe `{ output, isError: true }` results when
|
|
74
|
+
the conversation should continue; uncaught errors stop the loop and are not retried.
|
|
75
|
+
|
|
76
|
+
When the host authorizes or queues a whole proposal together, supply
|
|
77
|
+
`executeTools(toolCalls, state)` instead of `executeTool`. The callbacks are
|
|
78
|
+
mutually exclusive. The batch callback runs once per completed assistant response
|
|
79
|
+
and returns one `{ output, isError?, forceText? }` outcome per call, in the original
|
|
80
|
+
order. The loop appends one user message containing all tool results, then calls
|
|
81
|
+
the provider again. `onToolResult(message, state)` receives that combined message
|
|
82
|
+
once, with `state.toolCalls` and `state.outcomes`. An omitted `isError` remains
|
|
83
|
+
omitted on this batch path. The host owns batch authorization, transaction scope,
|
|
84
|
+
durable claims and confirmation; this callback does not make a batch atomic.
|
|
85
|
+
|
|
86
|
+
For async-iterator consumers, pass the same options to `streamConversation`:
|
|
87
|
+
|
|
88
|
+
```js
|
|
89
|
+
const { streamConversation } = require('@bliggy/chat-runtime');
|
|
90
|
+
|
|
91
|
+
for await (const event of streamConversation(options)) {
|
|
92
|
+
if (event.type === 'done') handleOutcome(event.result);
|
|
93
|
+
else sendToBrowser(event);
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The iterator yields normalized provider events and application events sent with
|
|
98
|
+
`await state.emit(event)`, followed by one `{ type: 'done', result: outcome }`.
|
|
99
|
+
It starts when first consumed, and waits for the consumer to advance after each
|
|
100
|
+
event. An interrupted provider stream throws without a successful `done` event.
|
|
101
|
+
Both APIs accept an optional `AbortSignal` as `signal`. Closing or throwing into
|
|
102
|
+
the iterator also cancels immediately, including while `next()` is pending.
|
|
103
|
+
Cancellation prevents later model calls, tool callbacks and persistence callbacks;
|
|
104
|
+
it cannot undo an already-started host action or abort an in-flight provider
|
|
105
|
+
request. Host callbacks that emit progress should await `state.emit` before
|
|
106
|
+
starting their next action.
|
|
107
|
+
|
|
108
|
+
Opaque reasoning and assistant phase survive in-memory tool continuation.
|
|
109
|
+
`onAssistantMessage` receives content without reasoning/thinking blocks. The
|
|
110
|
+
response state and returned `messages` still contain the full provider context:
|
|
111
|
+
do not persist or expose those as a transcript. Hosts remain responsible for
|
|
112
|
+
safe historical context, attachments, compaction and user-visible receipts.
|
|
113
|
+
|
|
114
|
+
Additional hooks:
|
|
115
|
+
|
|
116
|
+
- `streamTurn(request)` replaces the default `llm-client.streamChatTurn` boundary
|
|
117
|
+
when the application already wraps model configuration.
|
|
118
|
+
- `trackTurn(run, state)` wraps consumption of one provider stream for usage
|
|
119
|
+
accounting. Call and return `run()` once; replaying it creates another request.
|
|
120
|
+
- `getSystem(state)` refreshes context each iteration, including newly queued
|
|
121
|
+
actions. `state.iteration` starts at 1 and `state.textOnly` describes the request.
|
|
122
|
+
- `beforeResponse(state)` can return `{ retryMessages: [...] }` to correct an
|
|
123
|
+
unpersisted response. The rejected response and correction remain in memory,
|
|
124
|
+
and the retry consumes the same bounded iteration budget. Otherwise return
|
|
125
|
+
`null` to persist and process the response.
|
|
126
|
+
|
|
127
|
+
## Partial and flagged confirmations
|
|
128
|
+
|
|
129
|
+
```js
|
|
130
|
+
const { selectPendingActions } = require('@bliggy/chat-runtime');
|
|
131
|
+
const { selected, remaining, reason } = selectPendingActions(pendingActions, {
|
|
132
|
+
decision: 'confirm',
|
|
133
|
+
actionId: requestedActionId // omit for a general confirmation
|
|
134
|
+
});
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
This pure helper preserves action IDs, order and objects. General `confirm`
|
|
138
|
+
selects actions without a truthy `warning`; flagged actions remain queued.
|
|
139
|
+
An explicit individual ID can select a flagged action. `cancel` selects one
|
|
140
|
+
action or the whole queue, including flagged actions. A host may use
|
|
141
|
+
`confirm_including_flagged` only when it has independently established explicit
|
|
142
|
+
approval of the warnings. Other decisions leave the queue unchanged.
|
|
143
|
+
|
|
144
|
+
`reason` is `empty`, `not_found`, `all_flagged`, `unconfirmed`, or `null` for a
|
|
145
|
+
successful selection. Selection is not authorization or an atomic claim. The
|
|
146
|
+
host must claim the same persisted queue, recheck current permissions, retain
|
|
147
|
+
unselected actions and record the actual results. Never accept a model-supplied
|
|
148
|
+
decision as proof of user approval.
|
|
149
|
+
|
|
150
|
+
## Upgrade from agent-channel-runtime
|
|
151
|
+
|
|
152
|
+
The existing exports remain available under the new npm name. Upgrading from
|
|
153
|
+
0.1.x also requires the 0.2 store contract below: add `getPending` and make
|
|
154
|
+
`claimPending` match the exact turn ID and pending-action snapshot. A package-name
|
|
155
|
+
replacement alone is not sufficient for a host using the older store contract.
|
|
156
|
+
|
|
157
|
+
## Runtime flow
|
|
158
|
+
|
|
159
|
+
1. Normalize the provider payload into an envelope.
|
|
160
|
+
2. Authenticate the transport before resolving an actor.
|
|
161
|
+
3. Resolve the tenant and actor through a host callback.
|
|
162
|
+
4. Apply rate limits and idempotently claim the source message.
|
|
163
|
+
5. Plan, authorize, and execute actions.
|
|
164
|
+
6. Persist completion or hold risky actions for confirmation.
|
|
165
|
+
7. Format the response for the originating channel.
|
|
166
|
+
|
|
167
|
+
The runtime returns formatted responses; the host owns durable delivery. Twilio
|
|
168
|
+
outbound SMS and call helpers are provided separately, so a webhook retry never
|
|
169
|
+
silently sends an email or text twice.
|
|
170
|
+
|
|
171
|
+
Turn statuses are:
|
|
172
|
+
|
|
173
|
+
```text
|
|
174
|
+
received -> authenticated -> processing -> completed
|
|
175
|
+
\-> awaiting_confirmation -> executing -> completed
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Host contracts
|
|
179
|
+
|
|
180
|
+
Create `ChannelRuntime` with:
|
|
181
|
+
|
|
182
|
+
- `adapters`: channel adapters created by this package or the host.
|
|
183
|
+
- `turnStore`: `claim`, `transition`, `getPending`, and atomic `claimPending` methods.
|
|
184
|
+
- `resolvePrincipal`: returns trusted `{ tenantKey, actorKey, principal }`.
|
|
185
|
+
- `plan`: returns `{ content, summary, actions }`.
|
|
186
|
+
- `authorize`: explicitly classifies actions into `approved`,
|
|
187
|
+
`requiresConfirmation`, and `rejected` arrays. Unclassified actions fail
|
|
188
|
+
closed.
|
|
189
|
+
- `execute`: executes one action with the supplied idempotency key.
|
|
190
|
+
- Optional `render`, `rateLimit`, `onSecurityEvent`, and `onError` callbacks.
|
|
191
|
+
- Optional async `parseConfirmation(text, { pendingTurn, principal, envelope,
|
|
192
|
+
channel, context })`: returns `confirm`, `cancel`, `clarify`, or `null` (normal
|
|
193
|
+
planning). It runs only after transport authentication and principal resolution.
|
|
194
|
+
The default retains the legacy short commands; applications that support natural
|
|
195
|
+
conversation should supply their semantic interpreter. Never fabricate a new
|
|
196
|
+
message containing a command word. Unrecognized non-null results ask for
|
|
197
|
+
clarification without claiming the pending action.
|
|
198
|
+
|
|
199
|
+
The default confirmation lifetime is 24 hours. `confirmationTtlMs`,
|
|
200
|
+
`maxTextLength`, and `maxActions` are configurable constructor limits.
|
|
201
|
+
|
|
202
|
+
`MemoryTurnStore` is suitable for tests. Production hosts should implement the
|
|
203
|
+
same contract with durable storage and make `claimPending` an atomic row claim.
|
|
204
|
+
`getPending(scope, metadata)` returns the current unexpired pending turn for the
|
|
205
|
+
exact tenant, actor, channel and thread, without claiming it. The interpreter gets
|
|
206
|
+
a copy of that snapshot. `claimPending({ ...scope, turnId, pendingActions }, metadata)`
|
|
207
|
+
must atomically claim only that exact ID with the unchanged action set, still
|
|
208
|
+
pending and unexpired. A different/newer turn must never be substituted. This
|
|
209
|
+
two-phase store contract is required starting in 0.2.0; update durable stores when
|
|
210
|
+
upgrading. No database lock may be held during a model/provider call.
|
|
211
|
+
Confirmed execution receives a separate runtime-created `confirmation` value
|
|
212
|
+
(`decision`, `turnId`, `sourceKey`); ordinary execution receives `null`. The host
|
|
213
|
+
must not accept equivalent claims from model arguments. Policy is rechecked after
|
|
214
|
+
the claim. Interpretation is not an authorization decision.
|
|
215
|
+
Store methods receive a second `{ principal, context }` argument so a host can
|
|
216
|
+
persist through already-resolved tenant associations without querying global
|
|
217
|
+
identifiers again. Runtime callbacks also receive the original trusted
|
|
218
|
+
`context`; it is never copied into the persisted turn automatically.
|
|
219
|
+
|
|
220
|
+
```js
|
|
221
|
+
const {
|
|
222
|
+
ChannelRuntime,
|
|
223
|
+
MemoryTurnStore,
|
|
224
|
+
createChatAdapter
|
|
225
|
+
} = require('@bliggy/chat-runtime');
|
|
226
|
+
|
|
227
|
+
const runtime = new ChannelRuntime({
|
|
228
|
+
adapters: {
|
|
229
|
+
chat: createChatAdapter({
|
|
230
|
+
authenticate: async (_envelope, context) => ({
|
|
231
|
+
allowed: context.sessionAuthenticated,
|
|
232
|
+
method: 'session'
|
|
233
|
+
})
|
|
234
|
+
})
|
|
235
|
+
},
|
|
236
|
+
turnStore: new MemoryTurnStore(),
|
|
237
|
+
resolvePrincipal: async ({ context }) => context.principalResolution,
|
|
238
|
+
plan: async ({ envelope }) => planner.plan(envelope.text),
|
|
239
|
+
authorize: async input => policy.authorize(input),
|
|
240
|
+
execute: async input => tools.execute(input)
|
|
241
|
+
});
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
The host must generate a unique source key for each Chat message and reuse it
|
|
245
|
+
when retrying that same message.
|
|
246
|
+
Never accept tenant or actor identifiers from the untrusted channel payload.
|
|
247
|
+
|
|
248
|
+
## Twilio
|
|
249
|
+
|
|
250
|
+
`createTwilioChannels` returns `sms`, `voice`, `sendSms`, and `startCall`.
|
|
251
|
+
Inbound adapters validate `X-Twilio-Signature` with the official Twilio SDK.
|
|
252
|
+
The host must pass the exact public request URL and every form parameter:
|
|
253
|
+
|
|
254
|
+
```js
|
|
255
|
+
const { ChannelRuntime, createTwilioChannels } = require('@bliggy/chat-runtime');
|
|
256
|
+
|
|
257
|
+
const channels = createTwilioChannels({
|
|
258
|
+
accountSid: process.env.TWILIO_ACCOUNT_SID,
|
|
259
|
+
authToken: process.env.TWILIO_AUTH_TOKEN,
|
|
260
|
+
messagingServiceSid: process.env.TWILIO_MESSAGING_SERVICE_SID,
|
|
261
|
+
voiceFrom: process.env.TWILIO_VOICE_NUMBER,
|
|
262
|
+
voiceActionUrl: 'https://app.example.com/webhooks/twilio/voice'
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// Supply the application's durable store and callbacks described above.
|
|
266
|
+
const twilioRuntime = new ChannelRuntime({
|
|
267
|
+
adapters: { sms: channels.sms, voice: channels.voice },
|
|
268
|
+
turnStore,
|
|
269
|
+
resolvePrincipal,
|
|
270
|
+
plan,
|
|
271
|
+
authorize,
|
|
272
|
+
execute
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
await twilioRuntime.handle({
|
|
276
|
+
channel: 'sms',
|
|
277
|
+
payload: req.body,
|
|
278
|
+
context: {
|
|
279
|
+
url: publicRequestUrl,
|
|
280
|
+
params: req.body,
|
|
281
|
+
headers: req.headers
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Voice uses Programmable Voice speech/DTMF `Gather`. The generated TwiML adds a
|
|
287
|
+
fresh `turnKey` query parameter to every Gather action URL. The host must pass
|
|
288
|
+
that query value back as `context.turnKey`; this distinguishes successive turns
|
|
289
|
+
inside one `CallSid` and makes webhook retries idempotent. Twilio's optional
|
|
290
|
+
speech `Confidence` value is recorded as metadata but is never trusted as an
|
|
291
|
+
authentication signal.
|
|
292
|
+
|
|
293
|
+
Real-time bidirectional Media Streams are intentionally outside this package
|
|
294
|
+
version and should be added as a separate streaming adapter.
|
|
295
|
+
|
|
296
|
+
## Security rules
|
|
297
|
+
|
|
298
|
+
- Authentication happens before tenant or actor resolution and before storage.
|
|
299
|
+
- Provider payloads never choose their own tenant or actor.
|
|
300
|
+
- Duplicate source messages return the stored result without replanning.
|
|
301
|
+
- Confirmations are scoped to tenant, actor, channel, and thread.
|
|
302
|
+
- Asynchronous interpretation binds to an exact pending-action snapshot; a stale,
|
|
303
|
+
changed, expired, cancelled or concurrently claimed snapshot cannot execute.
|
|
304
|
+
- Hosts own semantic accuracy: keep classifier inputs limited to the authenticated
|
|
305
|
+
current reply and application-owned action descriptions, excluding document,
|
|
306
|
+
assistant and tool text. Partial/conditional/uncertain acknowledgment must not
|
|
307
|
+
become approval of every action in a batch.
|
|
308
|
+
- Policy is fail-closed and is checked again when confirmation is received.
|
|
309
|
+
- Authentication records stored on a turn exclude secrets and signatures.
|
|
310
|
+
- Security-event callbacks receive identifiers and verdicts, not message text.
|
|
311
|
+
|
|
312
|
+
Twilio webhook validation follows:
|
|
313
|
+
|
|
314
|
+
- https://www.twilio.com/docs/usage/webhooks/webhooks-security
|
|
315
|
+
- https://www.twilio.com/docs/voice/twiml/gather
|
package/index.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
ChannelRuntime,
|
|
5
|
+
ChannelRuntimeError,
|
|
6
|
+
EnvelopeValidationError,
|
|
7
|
+
SecurityError,
|
|
8
|
+
RateLimitError,
|
|
9
|
+
TURN_STATUSES,
|
|
10
|
+
TURN_TRANSITIONS,
|
|
11
|
+
parseDecision
|
|
12
|
+
} = require('./src/runtime');
|
|
13
|
+
const { MemoryTurnStore } = require('./src/memoryTurnStore');
|
|
14
|
+
const { createChatAdapter, createEmailAdapter } = require('./src/channels');
|
|
15
|
+
const { createTwilioChannels } = require('./src/twilio');
|
|
16
|
+
const { runConversation, streamConversation } = require('./src/conversation');
|
|
17
|
+
const { selectPendingActions } = require('./src/pendingActions');
|
|
18
|
+
|
|
19
|
+
module.exports = {
|
|
20
|
+
ChannelRuntime,
|
|
21
|
+
ChannelRuntimeError,
|
|
22
|
+
EnvelopeValidationError,
|
|
23
|
+
SecurityError,
|
|
24
|
+
RateLimitError,
|
|
25
|
+
MemoryTurnStore,
|
|
26
|
+
TURN_STATUSES,
|
|
27
|
+
TURN_TRANSITIONS,
|
|
28
|
+
parseDecision,
|
|
29
|
+
createChatAdapter,
|
|
30
|
+
createEmailAdapter,
|
|
31
|
+
createTwilioChannels,
|
|
32
|
+
runConversation,
|
|
33
|
+
streamConversation,
|
|
34
|
+
selectPendingActions
|
|
35
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,31 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bliggy/chat-runtime",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.3.1",
|
|
4
|
+
"description": "Shared chat conversations, tool execution, and action confirmations powered by @bliggy/llm-client",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/davejohnson/agent-channel-runtime.git"
|
|
8
|
+
},
|
|
9
|
+
"main": "index.js",
|
|
10
|
+
"exports": "./index.js",
|
|
11
|
+
"files": [
|
|
12
|
+
"index.js",
|
|
13
|
+
"src",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"test": "node --test test/*.node.js"
|
|
18
|
+
},
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=24 <25"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@bliggy/llm-client": "0.1.0",
|
|
24
|
+
"twilio": "^6.0.2"
|
|
25
|
+
},
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"registry": "https://registry.npmjs.org",
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"license": "ISC"
|
|
31
|
+
}
|
package/src/channels.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function header(headers, name) {
|
|
4
|
+
if (!headers || typeof headers !== 'object') return null;
|
|
5
|
+
const key = Object.keys(headers).find(candidate => candidate.toLowerCase() === name.toLowerCase());
|
|
6
|
+
return key ? headers[key] : null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function requireAuthenticator(authenticate, channel) {
|
|
10
|
+
if (typeof authenticate !== 'function') {
|
|
11
|
+
throw new TypeError(`${channel} authenticate callback is required`);
|
|
12
|
+
}
|
|
13
|
+
return authenticate;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function createChatAdapter({ authenticate, format = null } = {}) {
|
|
17
|
+
const verify = requireAuthenticator(authenticate, 'chat');
|
|
18
|
+
return {
|
|
19
|
+
async normalize(payload, context) {
|
|
20
|
+
return {
|
|
21
|
+
channel: 'chat',
|
|
22
|
+
sourceKey: payload?.sourceKey || context.sourceKey || '',
|
|
23
|
+
threadKey: payload?.threadKey || payload?.conversationId || context.threadKey || '',
|
|
24
|
+
sender: null,
|
|
25
|
+
recipient: 'agent',
|
|
26
|
+
text: payload?.text ?? payload?.message ?? '',
|
|
27
|
+
attachments: payload?.attachments || [],
|
|
28
|
+
metadata: payload?.metadata || null
|
|
29
|
+
};
|
|
30
|
+
},
|
|
31
|
+
authenticate: verify,
|
|
32
|
+
async format(output, details) {
|
|
33
|
+
if (format) return await format(output, details);
|
|
34
|
+
return {
|
|
35
|
+
content: output.content || '',
|
|
36
|
+
results: output.results || [],
|
|
37
|
+
pendingActions: output.pendingActions || []
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function createEmailAdapter({ authenticate, format = null } = {}) {
|
|
44
|
+
const verify = requireAuthenticator(authenticate, 'email');
|
|
45
|
+
return {
|
|
46
|
+
async normalize(payload) {
|
|
47
|
+
const sourceKey = payload?.messageId || header(payload?.headers, 'message-id') || '';
|
|
48
|
+
const inReplyTo = payload?.inReplyTo || header(payload?.headers, 'in-reply-to');
|
|
49
|
+
return {
|
|
50
|
+
channel: 'email',
|
|
51
|
+
sourceKey,
|
|
52
|
+
threadKey: payload?.threadKey || inReplyTo || sourceKey,
|
|
53
|
+
sender: payload?.from || null,
|
|
54
|
+
recipient: payload?.to || null,
|
|
55
|
+
subject: payload?.subject || '(no subject)',
|
|
56
|
+
text: payload?.text || '',
|
|
57
|
+
attachments: payload?.attachments || [],
|
|
58
|
+
metadata: payload?.metadata || null
|
|
59
|
+
};
|
|
60
|
+
},
|
|
61
|
+
authenticate: verify,
|
|
62
|
+
async format(output, details) {
|
|
63
|
+
if (format) return await format(output, details);
|
|
64
|
+
const subject = details.envelope.subject.startsWith('Re:')
|
|
65
|
+
? details.envelope.subject
|
|
66
|
+
: `Re: ${details.envelope.subject}`;
|
|
67
|
+
return { subject, text: output.content || '', html: null };
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
module.exports = { createChatAdapter, createEmailAdapter };
|