@adzenai/ai 1.1.1 → 1.2.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 +58 -3
- package/dist/copilotkit/index.cjs +481 -10
- package/dist/copilotkit/index.cjs.map +1 -1
- package/dist/copilotkit/index.d.cts +66 -1
- package/dist/copilotkit/index.d.ts +66 -1
- package/dist/copilotkit/index.js +479 -9
- package/dist/copilotkit/index.js.map +1 -1
- package/dist/copilotkit/react/index.cjs +204 -3
- package/dist/copilotkit/react/index.cjs.map +1 -1
- package/dist/copilotkit/react/index.d.cts +63 -7
- package/dist/copilotkit/react/index.d.ts +63 -7
- package/dist/copilotkit/react/index.js +200 -3
- package/dist/copilotkit/react/index.js.map +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/package.json +12 -10
package/README.md
CHANGED
|
@@ -51,8 +51,8 @@ dispatchPlacementEvents(events);
|
|
|
51
51
|
| Path | Environment | Provides |
|
|
52
52
|
| --- | --- | --- |
|
|
53
53
|
| `@adzenai/ai` | Any | Core type re-exports (`AdzenPlacement`, `ProcessResponse`, `ProcessResponseAd`) |
|
|
54
|
-
| `@adzenai/ai/copilotkit` |
|
|
55
|
-
| `@adzenai/ai/copilotkit/react` | Browser (React 18+) | `AdzenCard`, `useAdzenPlacement`, `dispatchPlacementEvent(s)` |
|
|
54
|
+
| `@adzenai/ai/copilotkit` | Async: server or client. Stream: server only. | `AdzenAsyncMiddleware`, `AdzenAsyncConfig`, `AdzenStreamMiddleware`, `AdzenStreamConfig` |
|
|
55
|
+
| `@adzenai/ai/copilotkit/react` | Browser (React 18+) | `AdzenCard`, `InlineAd`, `useAdzenPlacement`, `useAdzenInlineAds`, `buildInlineAdSegments`, `dispatchPlacementEvent(s)` |
|
|
56
56
|
|
|
57
57
|
## How It Works
|
|
58
58
|
|
|
@@ -66,19 +66,74 @@ Impression beacons are fired as client-side GET requests directly to the deliver
|
|
|
66
66
|
|
|
67
67
|
The middleware is **additive only** — it never modifies, blocks, or delays the original event stream. All failures degrade silently.
|
|
68
68
|
|
|
69
|
+
## Streaming Enrichment
|
|
70
|
+
|
|
71
|
+
`AdzenStreamMiddleware` proxies the LLM response stream through Adzen's `/stream` endpoint for in-message ads, instead of waiting for the completed message. Tee the upstream response so one branch feeds AG-UI parsing and the other is piped to Adzen:
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
const [parseBranch, proxyBranch] = res.body!.tee();
|
|
75
|
+
|
|
76
|
+
for await (const evt of adzen.processStream(parseIntoAgUiEvents(parseBranch), {
|
|
77
|
+
prompt,
|
|
78
|
+
upstreamBody: proxyBranch,
|
|
79
|
+
})) {
|
|
80
|
+
sendToClient(evt);
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
**Server-side only (Node 18+).** It passes a `ReadableStream` as the fetch request body; browsers other than Chromium do not support this and coerce the stream to the string `"[object ReadableStream]"`. The SDK feature-detects and emits an `adzen_error` rather than sending a corrupted body.
|
|
85
|
+
|
|
86
|
+
### Rendering inline ads
|
|
87
|
+
|
|
88
|
+
Inline ads carry a `content_offset` — the number of message characters streamed before the ad arrived — so they render at that point in the text rather than at the end of the message. `getInlineAdSegments()` does the splitting:
|
|
89
|
+
|
|
90
|
+
```tsx
|
|
91
|
+
const { getInlineAdSegments } = useAdzenInlineAds();
|
|
92
|
+
|
|
93
|
+
<div style={{ whiteSpace: "pre-wrap" }}>
|
|
94
|
+
{getInlineAdSegments(msg.id, msg.content).map((segment, i) =>
|
|
95
|
+
segment.kind === "text" ? (
|
|
96
|
+
<span key={i}>{segment.text}</span>
|
|
97
|
+
) : (
|
|
98
|
+
<InlineAd key={segment.ad.ad_id} ad={segment.ad} />
|
|
99
|
+
),
|
|
100
|
+
)}
|
|
101
|
+
</div>
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
`InlineAd` renders as a plain text snippet — an "Ad" pill, the advertiser name, and the CTA as the only link (`[AD] Brooks Brothers clothing sale`) — and is block-level, so it appears on its own line directly below the text it follows. The advertiser text uses `creative.advertiser_name`, falling back to `creative.headline`.
|
|
105
|
+
|
|
106
|
+
Anchors are snapped forward to the next paragraph break, so segments are always whole paragraphs and each one can be passed to a Markdown renderer (`<Markdown>{segment.text}</Markdown>`) without a split breaking `**bold text**`, a list, or a code fence.
|
|
107
|
+
|
|
108
|
+
At most one ad is placed per paragraph break. Ads that would land on an already-used break move to the next free one (the end of the message counts as a slot), and any that still have nowhere to go are held back until the message grows, rather than stacking in one spot.
|
|
109
|
+
|
|
69
110
|
## Configuration
|
|
70
111
|
|
|
71
112
|
```typescript
|
|
72
113
|
new AdzenAsyncMiddleware({
|
|
73
114
|
apiKey: string; // required — Adzen API key
|
|
74
|
-
endpointUrl?: string; // default: "https://api.adzen.ai/v1
|
|
115
|
+
endpointUrl?: string; // default: "https://api.adzen.ai/v1"
|
|
75
116
|
timeoutMs?: number; // default: 3000
|
|
76
117
|
adUnitPosition?: string; // default: "chin"
|
|
77
118
|
location?: string; // optional — DMA location code for geo-targeting
|
|
78
119
|
conversationId?: string; // optional — links impressions to conversation context
|
|
120
|
+
profileId?: string; // optional — sent as X-Profile-Id where APIM does not inject it
|
|
79
121
|
});
|
|
80
122
|
```
|
|
81
123
|
|
|
124
|
+
`endpointUrl` is a base URL; the SDK appends `/process` and `/stream`. Include any environment-specific path prefix (such as `/ai` on stage and prod) in the value you pass.
|
|
125
|
+
|
|
126
|
+
`AdzenStreamMiddleware` accepts the same options plus `prefetch` (POST `/process` with the prompt before opening the stream), `placementPollIntervalMs`, `sidecar`, and the `onInlineAd` / `onPlacement` / `onError` / `onStreamStart` / `onStreamEnd` callbacks.
|
|
127
|
+
|
|
128
|
+
<!-- INTERNAL / DEBUGGING-MODE ONLY — do not surface in the public shared-vision/developer-docs.
|
|
129
|
+
There is also an optional `headers` (`Record<string, string>`) option, merged into every outbound
|
|
130
|
+
Adzen request (`/stream`, `/process`, placement polling) for internal debugging (e.g. `X-Adzen-Debug-*`).
|
|
131
|
+
SDK-managed headers (auth, content-type, correlation IDs) always take precedence. -->
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
The middleware never makes the assistant message wait on the ad pipeline: the prefetch is dispatched without being awaited, and content events are forwarded as they arrive whether or not an ad has been matched yet.
|
|
136
|
+
|
|
82
137
|
## License
|
|
83
138
|
|
|
84
139
|
MIT
|
|
@@ -20,7 +20,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/copilotkit/index.ts
|
|
21
21
|
var copilotkit_exports = {};
|
|
22
22
|
__export(copilotkit_exports, {
|
|
23
|
-
AdzenAsyncMiddleware: () => AdzenAsyncMiddleware
|
|
23
|
+
AdzenAsyncMiddleware: () => AdzenAsyncMiddleware,
|
|
24
|
+
AdzenStreamMiddleware: () => AdzenStreamMiddleware
|
|
24
25
|
});
|
|
25
26
|
module.exports = __toCommonJS(copilotkit_exports);
|
|
26
27
|
|
|
@@ -63,7 +64,7 @@ var AdzenAsyncMiddleware = class {
|
|
|
63
64
|
constructor(config) {
|
|
64
65
|
this.config = {
|
|
65
66
|
...config,
|
|
66
|
-
endpointUrl: config.endpointUrl ?? "https://api.adzen.ai/v1
|
|
67
|
+
endpointUrl: config.endpointUrl ?? "https://api.adzen.ai/v1",
|
|
67
68
|
timeoutMs: config.timeoutMs ?? 3e3,
|
|
68
69
|
adUnitPosition: config.adUnitPosition ?? "chin"
|
|
69
70
|
};
|
|
@@ -176,15 +177,17 @@ var AdzenAsyncMiddleware = class {
|
|
|
176
177
|
};
|
|
177
178
|
if (this.config.location) body.location = this.config.location;
|
|
178
179
|
if (this.conversationId) body.conversation_id = this.conversationId;
|
|
180
|
+
const headers = {
|
|
181
|
+
"Content-Type": "application/json",
|
|
182
|
+
"X-API-Key": this.config.apiKey,
|
|
183
|
+
// AI-API requires an Idempotency-Key on /process (400 otherwise).
|
|
184
|
+
// One per ad fetch — each completed message is a distinct request.
|
|
185
|
+
"Idempotency-Key": generateIdempotencyKey()
|
|
186
|
+
};
|
|
187
|
+
if (this.config.profileId) headers["X-Profile-Id"] = this.config.profileId;
|
|
179
188
|
const res = await fetch(`${this.config.endpointUrl}/process`, {
|
|
180
189
|
method: "POST",
|
|
181
|
-
headers
|
|
182
|
-
"Content-Type": "application/json",
|
|
183
|
-
"X-API-Key": this.config.apiKey,
|
|
184
|
-
// AI-API requires an Idempotency-Key on /process (400 otherwise).
|
|
185
|
-
// One per ad fetch — each completed message is a distinct request.
|
|
186
|
-
"Idempotency-Key": generateIdempotencyKey()
|
|
187
|
-
},
|
|
190
|
+
headers,
|
|
188
191
|
body: JSON.stringify(body),
|
|
189
192
|
signal: controller.signal
|
|
190
193
|
});
|
|
@@ -229,8 +232,476 @@ var AdzenAsyncMiddleware = class {
|
|
|
229
232
|
};
|
|
230
233
|
}
|
|
231
234
|
};
|
|
235
|
+
|
|
236
|
+
// src/copilotkit/AdzenStreamClient.ts
|
|
237
|
+
var import_core = require("@adzenai/core");
|
|
238
|
+
|
|
239
|
+
// src/copilotkit/parseSSE.ts
|
|
240
|
+
async function* parseSSE(stream) {
|
|
241
|
+
const reader = stream.getReader();
|
|
242
|
+
const decoder = new TextDecoder();
|
|
243
|
+
let buffer = "";
|
|
244
|
+
let currentEvent;
|
|
245
|
+
let dataLines = [];
|
|
246
|
+
try {
|
|
247
|
+
while (true) {
|
|
248
|
+
const { done, value } = await reader.read();
|
|
249
|
+
if (done) break;
|
|
250
|
+
buffer += decoder.decode(value, { stream: true });
|
|
251
|
+
const lines = buffer.split("\n");
|
|
252
|
+
buffer = lines.pop();
|
|
253
|
+
for (const raw of lines) {
|
|
254
|
+
const line = raw.endsWith("\r") ? raw.slice(0, -1) : raw;
|
|
255
|
+
if (line === "") {
|
|
256
|
+
if (dataLines.length > 0) {
|
|
257
|
+
yield { event: currentEvent, data: dataLines.join("\n") };
|
|
258
|
+
dataLines = [];
|
|
259
|
+
currentEvent = void 0;
|
|
260
|
+
}
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
if (line.startsWith(":")) continue;
|
|
264
|
+
const colonIdx = line.indexOf(":");
|
|
265
|
+
if (colonIdx === -1) continue;
|
|
266
|
+
const field = line.slice(0, colonIdx);
|
|
267
|
+
const value2 = line.slice(colonIdx + 1).replace(/^ /, "");
|
|
268
|
+
switch (field) {
|
|
269
|
+
case "data":
|
|
270
|
+
dataLines.push(value2);
|
|
271
|
+
break;
|
|
272
|
+
case "event":
|
|
273
|
+
currentEvent = value2;
|
|
274
|
+
break;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
if (dataLines.length > 0) {
|
|
279
|
+
yield { event: currentEvent, data: dataLines.join("\n") };
|
|
280
|
+
}
|
|
281
|
+
} finally {
|
|
282
|
+
reader.releaseLock();
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// src/copilotkit/AdzenStreamClient.ts
|
|
287
|
+
var DEFAULT_ENDPOINT = "https://api.adzen.ai/v1";
|
|
288
|
+
var DEFAULT_TIMEOUT_MS = 3e3;
|
|
289
|
+
var DEFAULT_POLL_INTERVAL_MS = 2e3;
|
|
290
|
+
var SUPPORTS_REQUEST_STREAMS = (() => {
|
|
291
|
+
try {
|
|
292
|
+
let duplexAccessed = false;
|
|
293
|
+
const hasContentType = new Request("http://localhost", {
|
|
294
|
+
method: "POST",
|
|
295
|
+
body: new ReadableStream(),
|
|
296
|
+
get duplex() {
|
|
297
|
+
duplexAccessed = true;
|
|
298
|
+
return "half";
|
|
299
|
+
}
|
|
300
|
+
}).headers.has("Content-Type");
|
|
301
|
+
return duplexAccessed && !hasContentType;
|
|
302
|
+
} catch {
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
305
|
+
})();
|
|
306
|
+
var AdzenStreamClient = class {
|
|
307
|
+
endpointUrl;
|
|
308
|
+
apiKey;
|
|
309
|
+
timeoutMs;
|
|
310
|
+
location;
|
|
311
|
+
profileId;
|
|
312
|
+
placementPollIntervalMs;
|
|
313
|
+
prefetchEnabled;
|
|
314
|
+
sidecarEnabled;
|
|
315
|
+
customHeaders;
|
|
316
|
+
conversationId;
|
|
317
|
+
abortController = null;
|
|
318
|
+
placementEndpoint = null;
|
|
319
|
+
pollTimer = null;
|
|
320
|
+
streamEnded = false;
|
|
321
|
+
inlineAdQueue = [];
|
|
322
|
+
streamPromise = null;
|
|
323
|
+
prefetchPromise = null;
|
|
324
|
+
onInlineAd;
|
|
325
|
+
onPlacement;
|
|
326
|
+
onError;
|
|
327
|
+
onStreamStart;
|
|
328
|
+
onStreamEnd;
|
|
329
|
+
constructor(config) {
|
|
330
|
+
this.endpointUrl = config.endpointUrl ?? DEFAULT_ENDPOINT;
|
|
331
|
+
this.apiKey = config.apiKey;
|
|
332
|
+
this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
333
|
+
this.location = config.location;
|
|
334
|
+
this.profileId = config.profileId;
|
|
335
|
+
this.placementPollIntervalMs = config.placementPollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
336
|
+
this.prefetchEnabled = config.prefetch ?? false;
|
|
337
|
+
this.sidecarEnabled = config.sidecar ?? true;
|
|
338
|
+
this.customHeaders = config.headers ?? {};
|
|
339
|
+
this.conversationId = config.conversationId ?? (0, import_core.uuidv4)();
|
|
340
|
+
this.onInlineAd = config.onInlineAd;
|
|
341
|
+
this.onPlacement = config.onPlacement;
|
|
342
|
+
this.onError = config.onError;
|
|
343
|
+
this.onStreamStart = config.onStreamStart;
|
|
344
|
+
this.onStreamEnd = config.onStreamEnd;
|
|
345
|
+
}
|
|
346
|
+
getConversationId() {
|
|
347
|
+
return this.conversationId;
|
|
348
|
+
}
|
|
349
|
+
setConversationId(id) {
|
|
350
|
+
this.conversationId = id;
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Dispatches the prefetch without waiting for it.
|
|
354
|
+
*
|
|
355
|
+
* The request leaves before `connect()` opens `/stream`, which is all the
|
|
356
|
+
* 2-step chained flow needs — the two calls are correlated server-side by
|
|
357
|
+
* `conversation_id`. Awaiting the response here would stall the assistant
|
|
358
|
+
* message behind a full ad-matching round trip while the LLM keeps
|
|
359
|
+
* streaming, so the buffered tokens would then arrive in one burst.
|
|
360
|
+
*/
|
|
361
|
+
startPrefetch(prompt) {
|
|
362
|
+
if (!this.prefetchEnabled) return;
|
|
363
|
+
this.prefetchPromise = this.prefetchProcess(prompt);
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* 2-step chained flow: POST /process with the user prompt and
|
|
367
|
+
* conversationId. Awaits the response (or timeout) but discards the
|
|
368
|
+
* result. If it fails, logs and continues — the stream will proceed
|
|
369
|
+
* in degraded mode.
|
|
370
|
+
*/
|
|
371
|
+
async prefetchProcess(prompt) {
|
|
372
|
+
if (!this.prefetchEnabled) return;
|
|
373
|
+
const controller = new AbortController();
|
|
374
|
+
const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
375
|
+
try {
|
|
376
|
+
const body = {
|
|
377
|
+
content: prompt,
|
|
378
|
+
message_id: (0, import_core.uuidv4)(),
|
|
379
|
+
conversation_id: this.conversationId
|
|
380
|
+
};
|
|
381
|
+
if (this.location) body.location = this.location;
|
|
382
|
+
const headers = {
|
|
383
|
+
"Content-Type": "application/json",
|
|
384
|
+
"X-API-Key": this.apiKey,
|
|
385
|
+
"Idempotency-Key": (0, import_core.uuidv4)()
|
|
386
|
+
};
|
|
387
|
+
if (this.profileId) headers["X-Profile-Id"] = this.profileId;
|
|
388
|
+
await fetch(`${this.endpointUrl}/process`, {
|
|
389
|
+
method: "POST",
|
|
390
|
+
headers: this.mergeHeaders(headers),
|
|
391
|
+
body: JSON.stringify(body),
|
|
392
|
+
signal: controller.signal
|
|
393
|
+
});
|
|
394
|
+
} catch {
|
|
395
|
+
} finally {
|
|
396
|
+
clearTimeout(timeout);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Opens a streaming POST to /stream, piping the upstream LLM response
|
|
401
|
+
* body directly. The API reads the raw LLM stream in real time and
|
|
402
|
+
* sends SSE ad events back on the response side.
|
|
403
|
+
*
|
|
404
|
+
* @param messageId Unique ID for this assistant message
|
|
405
|
+
* @param body The upstream LLM response body (one branch of a tee)
|
|
406
|
+
*/
|
|
407
|
+
connect(messageId, body) {
|
|
408
|
+
this.streamEnded = false;
|
|
409
|
+
this.inlineAdQueue = [];
|
|
410
|
+
this.placementEndpoint = null;
|
|
411
|
+
this.streamPromise = null;
|
|
412
|
+
this.abortController = new AbortController();
|
|
413
|
+
if (!SUPPORTS_REQUEST_STREAMS) {
|
|
414
|
+
this.streamEnded = true;
|
|
415
|
+
this.onError?.({
|
|
416
|
+
type: "adzen_error",
|
|
417
|
+
code: "request_streams_unsupported",
|
|
418
|
+
message: "This runtime cannot send a ReadableStream as a request body, so the LLM stream cannot be proxied. Run AdzenStreamMiddleware server-side (Node 18+) \u2014 browsers other than Chromium do not support fetch upload streams."
|
|
419
|
+
});
|
|
420
|
+
void body.cancel().catch(() => {
|
|
421
|
+
});
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
const headers = {
|
|
425
|
+
"Content-Type": "application/octet-stream",
|
|
426
|
+
"X-API-Key": this.apiKey,
|
|
427
|
+
"Idempotency-Key": (0, import_core.uuidv4)(),
|
|
428
|
+
"X-Message-Id": messageId,
|
|
429
|
+
"X-Conversation-Id": this.conversationId
|
|
430
|
+
};
|
|
431
|
+
if (this.profileId) headers["X-Profile-Id"] = this.profileId;
|
|
432
|
+
if (this.location) headers["X-Location"] = this.location;
|
|
433
|
+
this.streamPromise = this.executeStreamFetch(body, this.mergeHeaders(headers));
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Waits for the SSE response stream from the API to finish, along with any
|
|
437
|
+
* prefetch still in flight. Neither can reject — both swallow their errors.
|
|
438
|
+
*/
|
|
439
|
+
async finalize() {
|
|
440
|
+
await Promise.all([this.prefetchPromise, this.streamPromise]);
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Merges caller-provided passthrough headers with the SDK's fixed headers.
|
|
444
|
+
* Fixed headers are spread last so SDK-managed keys (auth, content-type,
|
|
445
|
+
* correlation IDs) always win over any passthrough value.
|
|
446
|
+
*/
|
|
447
|
+
mergeHeaders(fixed) {
|
|
448
|
+
return { ...this.customHeaders, ...fixed };
|
|
449
|
+
}
|
|
450
|
+
async executeStreamFetch(body, headers) {
|
|
451
|
+
const timeout = setTimeout(
|
|
452
|
+
() => this.abortController?.abort(),
|
|
453
|
+
this.timeoutMs
|
|
454
|
+
);
|
|
455
|
+
try {
|
|
456
|
+
const res = await fetch(`${this.endpointUrl}/stream`, {
|
|
457
|
+
method: "POST",
|
|
458
|
+
headers,
|
|
459
|
+
body,
|
|
460
|
+
signal: this.abortController?.signal,
|
|
461
|
+
// @ts-expect-error -- required for streaming request bodies (Node/undici)
|
|
462
|
+
duplex: "half"
|
|
463
|
+
});
|
|
464
|
+
clearTimeout(timeout);
|
|
465
|
+
if (!res.ok || !res.body) return;
|
|
466
|
+
await this.readStream(res.body);
|
|
467
|
+
} catch {
|
|
468
|
+
clearTimeout(timeout);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
/** Drains and returns any queued inline ad events. */
|
|
472
|
+
drainInlineAds() {
|
|
473
|
+
const ads = this.inlineAdQueue.splice(0);
|
|
474
|
+
return ads;
|
|
475
|
+
}
|
|
476
|
+
/** Cancels pending requests and cleans up state. */
|
|
477
|
+
abort() {
|
|
478
|
+
this.abortController?.abort();
|
|
479
|
+
this.stopPolling();
|
|
480
|
+
this.streamEnded = true;
|
|
481
|
+
}
|
|
482
|
+
/** Polls the placement endpoint once. Called on an interval internally. */
|
|
483
|
+
async pollPlacements() {
|
|
484
|
+
if (!this.sidecarEnabled || !this.placementEndpoint) return;
|
|
485
|
+
try {
|
|
486
|
+
const res = await fetch(this.placementEndpoint, {
|
|
487
|
+
method: "GET",
|
|
488
|
+
headers: this.mergeHeaders({ "X-API-Key": this.apiKey })
|
|
489
|
+
});
|
|
490
|
+
if (!res.ok) return;
|
|
491
|
+
const data = await res.json();
|
|
492
|
+
for (const placement of data.placements) {
|
|
493
|
+
this.onPlacement?.(placement);
|
|
494
|
+
}
|
|
495
|
+
} catch {
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
async readStream(body) {
|
|
499
|
+
try {
|
|
500
|
+
for await (const frame of parseSSE(body)) {
|
|
501
|
+
if (this.abortController?.signal.aborted) break;
|
|
502
|
+
this.handleFrame(frame.event, frame.data);
|
|
503
|
+
}
|
|
504
|
+
} catch {
|
|
505
|
+
} finally {
|
|
506
|
+
this.streamEnded = true;
|
|
507
|
+
this.stopPolling();
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
handleFrame(event, data) {
|
|
511
|
+
switch (event) {
|
|
512
|
+
case "adzen_stream_start": {
|
|
513
|
+
try {
|
|
514
|
+
const parsed = JSON.parse(data);
|
|
515
|
+
const evt = { type: "adzen_stream_start", ...parsed };
|
|
516
|
+
this.placementEndpoint = parsed.placement_endpoint;
|
|
517
|
+
this.onStreamStart?.(evt);
|
|
518
|
+
this.startPolling();
|
|
519
|
+
} catch {
|
|
520
|
+
}
|
|
521
|
+
break;
|
|
522
|
+
}
|
|
523
|
+
case "adzen_inline": {
|
|
524
|
+
try {
|
|
525
|
+
const ad = JSON.parse(data);
|
|
526
|
+
this.inlineAdQueue.push(ad);
|
|
527
|
+
this.onInlineAd?.(ad);
|
|
528
|
+
} catch {
|
|
529
|
+
}
|
|
530
|
+
break;
|
|
531
|
+
}
|
|
532
|
+
case "adzen_error": {
|
|
533
|
+
try {
|
|
534
|
+
const parsed = JSON.parse(data);
|
|
535
|
+
this.onError?.({ type: "adzen_error", ...parsed });
|
|
536
|
+
} catch {
|
|
537
|
+
}
|
|
538
|
+
break;
|
|
539
|
+
}
|
|
540
|
+
case "adzen_stream_end": {
|
|
541
|
+
try {
|
|
542
|
+
const parsed = JSON.parse(data);
|
|
543
|
+
const evt = { type: "adzen_stream_end", ...parsed };
|
|
544
|
+
this.onStreamEnd?.(evt);
|
|
545
|
+
} catch {
|
|
546
|
+
}
|
|
547
|
+
this.streamEnded = true;
|
|
548
|
+
this.stopPolling();
|
|
549
|
+
break;
|
|
550
|
+
}
|
|
551
|
+
default: {
|
|
552
|
+
if (event?.startsWith("adzen_debug_")) break;
|
|
553
|
+
break;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
startPolling() {
|
|
558
|
+
if (!this.sidecarEnabled) return;
|
|
559
|
+
if (this.pollTimer || !this.placementEndpoint) return;
|
|
560
|
+
this.pollTimer = setInterval(
|
|
561
|
+
() => this.pollPlacements(),
|
|
562
|
+
this.placementPollIntervalMs
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
stopPolling() {
|
|
566
|
+
if (this.pollTimer) {
|
|
567
|
+
clearInterval(this.pollTimer);
|
|
568
|
+
this.pollTimer = null;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
// src/copilotkit/streamMiddleware.ts
|
|
574
|
+
var AdzenStreamMiddleware = class {
|
|
575
|
+
config;
|
|
576
|
+
adUnitPosition;
|
|
577
|
+
constructor(config) {
|
|
578
|
+
this.config = config;
|
|
579
|
+
this.adUnitPosition = config.adUnitPosition ?? "chin";
|
|
580
|
+
}
|
|
581
|
+
async *processStream(events, options) {
|
|
582
|
+
const sidecarPlacements = [];
|
|
583
|
+
let client = null;
|
|
584
|
+
let currentMessageId = "";
|
|
585
|
+
let contentOffset = 0;
|
|
586
|
+
const onPlacement = (record) => {
|
|
587
|
+
sidecarPlacements.push(this.mapPlacementRecord(record, currentMessageId));
|
|
588
|
+
};
|
|
589
|
+
const onError = (error) => {
|
|
590
|
+
console.warn(`[adzen-stream] ${error.code}: ${error.message}`);
|
|
591
|
+
};
|
|
592
|
+
for await (const event of events) {
|
|
593
|
+
const type = String(event.type ?? "");
|
|
594
|
+
switch (type) {
|
|
595
|
+
case "RUN_STARTED":
|
|
596
|
+
case "RunStarted": {
|
|
597
|
+
const threadId = String(event.threadId ?? "");
|
|
598
|
+
client = new AdzenStreamClient({
|
|
599
|
+
...this.config,
|
|
600
|
+
conversationId: threadId || this.config.conversationId,
|
|
601
|
+
onPlacement,
|
|
602
|
+
onError
|
|
603
|
+
});
|
|
604
|
+
if (this.config.prefetch && options?.prompt) {
|
|
605
|
+
client.startPrefetch(options.prompt);
|
|
606
|
+
}
|
|
607
|
+
yield event;
|
|
608
|
+
break;
|
|
609
|
+
}
|
|
610
|
+
case "TEXT_MESSAGE_START":
|
|
611
|
+
case "TextMessageStart": {
|
|
612
|
+
currentMessageId = String(event.messageId ?? "");
|
|
613
|
+
contentOffset = 0;
|
|
614
|
+
if (client && options?.upstreamBody) {
|
|
615
|
+
client.connect(currentMessageId, options.upstreamBody);
|
|
616
|
+
}
|
|
617
|
+
yield event;
|
|
618
|
+
break;
|
|
619
|
+
}
|
|
620
|
+
case "TEXT_MESSAGE_CONTENT":
|
|
621
|
+
case "TextMessageContent": {
|
|
622
|
+
yield event;
|
|
623
|
+
contentOffset += String(event.delta ?? "").length;
|
|
624
|
+
if (client) {
|
|
625
|
+
for (const ad of client.drainInlineAds()) {
|
|
626
|
+
yield this.makeInlineAdEvent(ad, currentMessageId, contentOffset);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
break;
|
|
630
|
+
}
|
|
631
|
+
case "TEXT_MESSAGE_END":
|
|
632
|
+
case "TextMessageEnd": {
|
|
633
|
+
if (client) {
|
|
634
|
+
await client.finalize();
|
|
635
|
+
for (const ad of client.drainInlineAds()) {
|
|
636
|
+
yield this.makeInlineAdEvent(ad, currentMessageId, contentOffset);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
yield event;
|
|
640
|
+
break;
|
|
641
|
+
}
|
|
642
|
+
case "RUN_FINISHED":
|
|
643
|
+
case "RunFinished": {
|
|
644
|
+
if (client) {
|
|
645
|
+
await client.pollPlacements();
|
|
646
|
+
client.abort();
|
|
647
|
+
}
|
|
648
|
+
for (const placement of sidecarPlacements) {
|
|
649
|
+
yield this.makePlacementEvent(placement, placement.ad_id);
|
|
650
|
+
}
|
|
651
|
+
yield event;
|
|
652
|
+
break;
|
|
653
|
+
}
|
|
654
|
+
default:
|
|
655
|
+
yield event;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* Wraps an inline ad as an AG-UI custom event anchored to `contentOffset`,
|
|
661
|
+
* the number of message characters emitted before the ad arrived. Clients
|
|
662
|
+
* use it to render the ad directly after that content instead of at the
|
|
663
|
+
* end of the message.
|
|
664
|
+
*/
|
|
665
|
+
makeInlineAdEvent(ad, messageId, contentOffset) {
|
|
666
|
+
const value = {
|
|
667
|
+
...ad,
|
|
668
|
+
message_id: messageId,
|
|
669
|
+
content_offset: contentOffset
|
|
670
|
+
};
|
|
671
|
+
return {
|
|
672
|
+
type: "CUSTOM_EVENT",
|
|
673
|
+
name: "adzen_inline_ad",
|
|
674
|
+
messageId,
|
|
675
|
+
value
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
makePlacementEvent(placement, adId) {
|
|
679
|
+
return {
|
|
680
|
+
type: "CUSTOM_EVENT",
|
|
681
|
+
name: "adzen_placement",
|
|
682
|
+
messageId: adId,
|
|
683
|
+
value: {
|
|
684
|
+
...placement,
|
|
685
|
+
message_id: placement.ad_id
|
|
686
|
+
}
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
mapPlacementRecord(record, messageId) {
|
|
690
|
+
return {
|
|
691
|
+
ad_id: String(record.ad_id),
|
|
692
|
+
advertiser_name: "",
|
|
693
|
+
headline: record.headline,
|
|
694
|
+
cta_text: record.cta_text,
|
|
695
|
+
destination_url: record.destination_url,
|
|
696
|
+
adUnitPosition: record.placement || this.adUnitPosition,
|
|
697
|
+
render_impression_url: record.tracking.impression_url,
|
|
698
|
+
view_impression_url: record.tracking.impression_url
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
};
|
|
232
702
|
// Annotate the CommonJS export names for ESM import in node:
|
|
233
703
|
0 && (module.exports = {
|
|
234
|
-
AdzenAsyncMiddleware
|
|
704
|
+
AdzenAsyncMiddleware,
|
|
705
|
+
AdzenStreamMiddleware
|
|
235
706
|
});
|
|
236
707
|
//# sourceMappingURL=index.cjs.map
|