@brinkcommerce/agentic-shopping-sdk 0.1.0-alpha.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/LICENSE +21 -0
- package/README.md +360 -0
- package/dist/chunk-2MMTZSYZ.js +26 -0
- package/dist/errors-CYbkUYHr.d.cts +126 -0
- package/dist/errors-CYbkUYHr.d.ts +126 -0
- package/dist/index.cjs +168 -0
- package/dist/index.d.cts +102 -0
- package/dist/index.d.ts +102 -0
- package/dist/index.js +127 -0
- package/dist/server.cjs +244 -0
- package/dist/server.d.cts +103 -0
- package/dist/server.d.ts +103 -0
- package/dist/server.js +204 -0
- package/package.json +54 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
AgentError: () => AgentError,
|
|
24
|
+
PRODUCT_MARKER: () => PRODUCT_MARKER,
|
|
25
|
+
collectReply: () => collectReply,
|
|
26
|
+
createSessionId: () => createSessionId,
|
|
27
|
+
formatPrice: () => formatPrice,
|
|
28
|
+
interleaveProducts: () => interleaveProducts,
|
|
29
|
+
isWireEvent: () => isWireEvent,
|
|
30
|
+
parseWireEvents: () => parseWireEvents,
|
|
31
|
+
stripMarkers: () => stripMarkers
|
|
32
|
+
});
|
|
33
|
+
module.exports = __toCommonJS(index_exports);
|
|
34
|
+
|
|
35
|
+
// src/ndjson.ts
|
|
36
|
+
function parseLine(line) {
|
|
37
|
+
const trimmed = line.trim();
|
|
38
|
+
if (trimmed === "") return void 0;
|
|
39
|
+
let parsed;
|
|
40
|
+
try {
|
|
41
|
+
parsed = JSON.parse(trimmed);
|
|
42
|
+
} catch {
|
|
43
|
+
return void 0;
|
|
44
|
+
}
|
|
45
|
+
if (typeof parsed !== "object" || parsed === null) return void 0;
|
|
46
|
+
const event = parsed;
|
|
47
|
+
return typeof event.type === "string" ? event : void 0;
|
|
48
|
+
}
|
|
49
|
+
function isWireEvent(event, type) {
|
|
50
|
+
return event.type === type;
|
|
51
|
+
}
|
|
52
|
+
async function* parseWireEvents(body) {
|
|
53
|
+
const reader = body.getReader();
|
|
54
|
+
const decoder = new TextDecoder();
|
|
55
|
+
let buffer = "";
|
|
56
|
+
try {
|
|
57
|
+
for (; ; ) {
|
|
58
|
+
const { done, value } = await reader.read();
|
|
59
|
+
if (done) break;
|
|
60
|
+
buffer += decoder.decode(value, { stream: true });
|
|
61
|
+
let newline = buffer.indexOf("\n");
|
|
62
|
+
while (newline !== -1) {
|
|
63
|
+
const event = parseLine(buffer.slice(0, newline));
|
|
64
|
+
buffer = buffer.slice(newline + 1);
|
|
65
|
+
if (event !== void 0) yield event;
|
|
66
|
+
newline = buffer.indexOf("\n");
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
buffer += decoder.decode();
|
|
70
|
+
const last = parseLine(buffer);
|
|
71
|
+
if (last !== void 0) yield last;
|
|
72
|
+
} finally {
|
|
73
|
+
reader.releaseLock();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async function collectReply(events) {
|
|
77
|
+
let text = "";
|
|
78
|
+
let products = [];
|
|
79
|
+
let suggestions = [];
|
|
80
|
+
for await (const event of events) {
|
|
81
|
+
if (event.type === "text" && typeof event.content === "string") {
|
|
82
|
+
text += event.content;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (event.type === "products" && Array.isArray(event.products)) {
|
|
86
|
+
products = event.products;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (event.type === "suggestions" && Array.isArray(event.suggestions)) {
|
|
90
|
+
suggestions = event.suggestions;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return { text, products, suggestions };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// src/markers.ts
|
|
97
|
+
var PRODUCT_MARKER = /\[product:([\w-]+)\]/g;
|
|
98
|
+
var MARKER_OPEN = "[product:";
|
|
99
|
+
function hidePartialMarker(text) {
|
|
100
|
+
const open = text.lastIndexOf("[");
|
|
101
|
+
if (open === -1 || text.includes("]", open)) return text;
|
|
102
|
+
const tail = text.slice(open);
|
|
103
|
+
const partial = tail.startsWith(MARKER_OPEN) ? /^\[product:[\w-]*$/.test(tail) : MARKER_OPEN.startsWith(tail);
|
|
104
|
+
return partial ? text.slice(0, open) : text;
|
|
105
|
+
}
|
|
106
|
+
var PARAGRAPH_BREAK = /\n\s*\n/;
|
|
107
|
+
var MARKER_WITH_PADDING = /[^\S\r\n]*\[product:[\w-]+\][^\S\r\n]*/g;
|
|
108
|
+
function stripMarkersFrom(paragraph) {
|
|
109
|
+
return paragraph.replace(MARKER_WITH_PADDING, (gap) => /\s/.test(gap) ? " " : "").trim();
|
|
110
|
+
}
|
|
111
|
+
function interleaveProducts(text, products) {
|
|
112
|
+
const byId = new Map(products.map((product) => [product.productId, product]));
|
|
113
|
+
const segments = [];
|
|
114
|
+
const matched = [];
|
|
115
|
+
const placed = /* @__PURE__ */ new Set();
|
|
116
|
+
const visible = hidePartialMarker(text);
|
|
117
|
+
for (const paragraph of visible.split(PARAGRAPH_BREAK)) {
|
|
118
|
+
const pattern = new RegExp(PRODUCT_MARKER.source, PRODUCT_MARKER.flags);
|
|
119
|
+
const cards = [];
|
|
120
|
+
let match;
|
|
121
|
+
while ((match = pattern.exec(paragraph)) !== null) {
|
|
122
|
+
const product = byId.get(match[1]);
|
|
123
|
+
if (product === void 0 || placed.has(product.productId)) continue;
|
|
124
|
+
placed.add(product.productId);
|
|
125
|
+
matched.push(product);
|
|
126
|
+
cards.push(product);
|
|
127
|
+
}
|
|
128
|
+
const prose = stripMarkersFrom(paragraph);
|
|
129
|
+
if (prose !== "") segments.push({ type: "text", content: prose });
|
|
130
|
+
for (const product of cards) segments.push({ type: "product", product });
|
|
131
|
+
}
|
|
132
|
+
return { segments, matched, unmatched: products.filter((product) => !placed.has(product.productId)) };
|
|
133
|
+
}
|
|
134
|
+
function stripMarkers(text) {
|
|
135
|
+
return hidePartialMarker(text).replace(PRODUCT_MARKER, "");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// src/session.ts
|
|
139
|
+
function createSessionId() {
|
|
140
|
+
return `session-${crypto.randomUUID()}`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// src/price.ts
|
|
144
|
+
function formatPrice(amount, currencyCode, locale) {
|
|
145
|
+
return new Intl.NumberFormat(locale, { style: "currency", currency: currencyCode }).format(amount / 100);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// src/errors.ts
|
|
149
|
+
var AgentError = class extends Error {
|
|
150
|
+
constructor(statusCode, body) {
|
|
151
|
+
super(`Agent error: ${statusCode}`);
|
|
152
|
+
this.statusCode = statusCode;
|
|
153
|
+
this.body = body;
|
|
154
|
+
this.name = "AgentError";
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
158
|
+
0 && (module.exports = {
|
|
159
|
+
AgentError,
|
|
160
|
+
PRODUCT_MARKER,
|
|
161
|
+
collectReply,
|
|
162
|
+
createSessionId,
|
|
163
|
+
formatPrice,
|
|
164
|
+
interleaveProducts,
|
|
165
|
+
isWireEvent,
|
|
166
|
+
parseWireEvents,
|
|
167
|
+
stripMarkers
|
|
168
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { W as WireEvent, U as UnknownWireEvent, P as ProductCard } from './errors-CYbkUYHr.cjs';
|
|
2
|
+
export { A as AgentError, C as CartContext, a as CartLine, b as ProductCardVariant, S as StoreContext } from './errors-CYbkUYHr.cjs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Narrows an event to one of the five this SDK knows.
|
|
6
|
+
*
|
|
7
|
+
* `switch (event.type)` cannot do it on its own: `UnknownWireEvent` carries an index signature, so it stays
|
|
8
|
+
* in the union on every branch and each field comes out as `unknown`. This is what makes a handler read the
|
|
9
|
+
* fields without casting.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* if (isWireEvent(event, 'text')) append(event.content)
|
|
13
|
+
*/
|
|
14
|
+
declare function isWireEvent<T extends WireEvent['type']>(event: WireEvent | UnknownWireEvent, type: T): event is Extract<WireEvent, {
|
|
15
|
+
type: T;
|
|
16
|
+
}>;
|
|
17
|
+
/**
|
|
18
|
+
* The events in an NDJSON response body, one JSON object per line. Lines arrive split across chunk
|
|
19
|
+
* boundaries, so partial lines are buffered until their newline.
|
|
20
|
+
*
|
|
21
|
+
* An event type this SDK does not know is yielded through untouched — the backend adds types without a major
|
|
22
|
+
* bump, and dropping them here would hide the new one from a caller that does know it.
|
|
23
|
+
*
|
|
24
|
+
* The wire is bare, single-encoded NDJSON: no `data: ` prefix and no double encoding. Stripping a prefix or
|
|
25
|
+
* parsing twice "just in case" only ever masks a real framing change.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* for await (const event of parseWireEvents(response.body)) {
|
|
29
|
+
* if (event.type === 'text') append(event.content)
|
|
30
|
+
* }
|
|
31
|
+
*/
|
|
32
|
+
declare function parseWireEvents(body: ReadableStream<Uint8Array>): AsyncGenerator<WireEvent | UnknownWireEvent>;
|
|
33
|
+
/**
|
|
34
|
+
* A whole reply, for callers that render nothing until the agent is done. Streaming is what the protocol is
|
|
35
|
+
* for — reach for this only when there is no progressive UI to feed.
|
|
36
|
+
*/
|
|
37
|
+
declare function collectReply(events: AsyncIterable<WireEvent | UnknownWireEvent>): Promise<{
|
|
38
|
+
text: string;
|
|
39
|
+
products: ProductCard[];
|
|
40
|
+
suggestions: string[];
|
|
41
|
+
}>;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The marker the agent writes after each product name: `[product:<productId>]`. The card's own id, never
|
|
45
|
+
* `productParentId` — several cards can share a parent, so a parent id would resolve to an arbitrary one.
|
|
46
|
+
*
|
|
47
|
+
* The hyphen in the character class is load-bearing. A product id is a merchant-chosen string over
|
|
48
|
+
* `[A-Za-z0-9_-]`, and hyphens are common in one — so a `\w+` pattern misses every id that carries one, and
|
|
49
|
+
* a reply whose ids all carry one loses every card.
|
|
50
|
+
*/
|
|
51
|
+
declare const PRODUCT_MARKER: RegExp;
|
|
52
|
+
type ContentSegment = {
|
|
53
|
+
type: 'text';
|
|
54
|
+
content: string;
|
|
55
|
+
} | {
|
|
56
|
+
type: 'product';
|
|
57
|
+
product: ProductCard;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* A reply split into the pieces to render in order: prose, with each product card below the paragraph whose
|
|
61
|
+
* marker named it. Markers are consumed, so no `[product:…]` reaches the shopper — including the half of one
|
|
62
|
+
* that the tail of a streaming reply holds.
|
|
63
|
+
*
|
|
64
|
+
* The cut is at the paragraph boundary rather than at the marker, because the agent writes the marker inside
|
|
65
|
+
* a sentence — straight after the product name. Cutting at the marker itself puts the card between "The
|
|
66
|
+
* Gore-Tex Offshore Jacket" and "is the shell", breaking the sentence across it. So each text segment is one
|
|
67
|
+
* paragraph, trimmed, and the cards it named follow it in the order they were written.
|
|
68
|
+
*
|
|
69
|
+
* A marker whose id has no card is stripped silently, which is what the backend expects — the prose already
|
|
70
|
+
* names the product. Cards no marker referenced come back as `unmatched`, to render as a shelf below the
|
|
71
|
+
* reply. A product referenced twice gets one card, under the paragraph that mentions it first.
|
|
72
|
+
*
|
|
73
|
+
* There is deliberately no fuzzy name matching: a fallback that guesses degrades silently instead of failing,
|
|
74
|
+
* and any word list it needs is tenant- and language-specific.
|
|
75
|
+
*/
|
|
76
|
+
declare function interleaveProducts(text: string, products: ProductCard[]): {
|
|
77
|
+
segments: ContentSegment[];
|
|
78
|
+
matched: ProductCard[];
|
|
79
|
+
unmatched: ProductCard[];
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* The reply with every marker removed, for a caller that renders prose and cards separately. A partial
|
|
83
|
+
* marker at the end of a streaming reply goes too, for the same reason it does in `interleaveProducts`.
|
|
84
|
+
*/
|
|
85
|
+
declare function stripMarkers(text: string): string;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* A session id for one conversation. The agent requires a session id of at least 33 characters; a bare
|
|
89
|
+
* UUID is 36 and passes, but the prefix makes that constraint visible and the value greppable in logs.
|
|
90
|
+
*
|
|
91
|
+
* The first turn pins the session to its store group and market. A later turn naming a different store is
|
|
92
|
+
* refused, so switching store means a new id.
|
|
93
|
+
*/
|
|
94
|
+
declare function createSessionId(): string;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* A card's price, formatted for display. Every amount on the wire is in minor units, so `59900` in `SEK` is
|
|
98
|
+
* 599,00 kr. Pass the card's own `currencyCode` — it is per product, not per store.
|
|
99
|
+
*/
|
|
100
|
+
declare function formatPrice(amount: number, currencyCode: string, locale?: string): string;
|
|
101
|
+
|
|
102
|
+
export { type ContentSegment, PRODUCT_MARKER, ProductCard, UnknownWireEvent, WireEvent, collectReply, createSessionId, formatPrice, interleaveProducts, isWireEvent, parseWireEvents, stripMarkers };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { W as WireEvent, U as UnknownWireEvent, P as ProductCard } from './errors-CYbkUYHr.js';
|
|
2
|
+
export { A as AgentError, C as CartContext, a as CartLine, b as ProductCardVariant, S as StoreContext } from './errors-CYbkUYHr.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Narrows an event to one of the five this SDK knows.
|
|
6
|
+
*
|
|
7
|
+
* `switch (event.type)` cannot do it on its own: `UnknownWireEvent` carries an index signature, so it stays
|
|
8
|
+
* in the union on every branch and each field comes out as `unknown`. This is what makes a handler read the
|
|
9
|
+
* fields without casting.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* if (isWireEvent(event, 'text')) append(event.content)
|
|
13
|
+
*/
|
|
14
|
+
declare function isWireEvent<T extends WireEvent['type']>(event: WireEvent | UnknownWireEvent, type: T): event is Extract<WireEvent, {
|
|
15
|
+
type: T;
|
|
16
|
+
}>;
|
|
17
|
+
/**
|
|
18
|
+
* The events in an NDJSON response body, one JSON object per line. Lines arrive split across chunk
|
|
19
|
+
* boundaries, so partial lines are buffered until their newline.
|
|
20
|
+
*
|
|
21
|
+
* An event type this SDK does not know is yielded through untouched — the backend adds types without a major
|
|
22
|
+
* bump, and dropping them here would hide the new one from a caller that does know it.
|
|
23
|
+
*
|
|
24
|
+
* The wire is bare, single-encoded NDJSON: no `data: ` prefix and no double encoding. Stripping a prefix or
|
|
25
|
+
* parsing twice "just in case" only ever masks a real framing change.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* for await (const event of parseWireEvents(response.body)) {
|
|
29
|
+
* if (event.type === 'text') append(event.content)
|
|
30
|
+
* }
|
|
31
|
+
*/
|
|
32
|
+
declare function parseWireEvents(body: ReadableStream<Uint8Array>): AsyncGenerator<WireEvent | UnknownWireEvent>;
|
|
33
|
+
/**
|
|
34
|
+
* A whole reply, for callers that render nothing until the agent is done. Streaming is what the protocol is
|
|
35
|
+
* for — reach for this only when there is no progressive UI to feed.
|
|
36
|
+
*/
|
|
37
|
+
declare function collectReply(events: AsyncIterable<WireEvent | UnknownWireEvent>): Promise<{
|
|
38
|
+
text: string;
|
|
39
|
+
products: ProductCard[];
|
|
40
|
+
suggestions: string[];
|
|
41
|
+
}>;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The marker the agent writes after each product name: `[product:<productId>]`. The card's own id, never
|
|
45
|
+
* `productParentId` — several cards can share a parent, so a parent id would resolve to an arbitrary one.
|
|
46
|
+
*
|
|
47
|
+
* The hyphen in the character class is load-bearing. A product id is a merchant-chosen string over
|
|
48
|
+
* `[A-Za-z0-9_-]`, and hyphens are common in one — so a `\w+` pattern misses every id that carries one, and
|
|
49
|
+
* a reply whose ids all carry one loses every card.
|
|
50
|
+
*/
|
|
51
|
+
declare const PRODUCT_MARKER: RegExp;
|
|
52
|
+
type ContentSegment = {
|
|
53
|
+
type: 'text';
|
|
54
|
+
content: string;
|
|
55
|
+
} | {
|
|
56
|
+
type: 'product';
|
|
57
|
+
product: ProductCard;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* A reply split into the pieces to render in order: prose, with each product card below the paragraph whose
|
|
61
|
+
* marker named it. Markers are consumed, so no `[product:…]` reaches the shopper — including the half of one
|
|
62
|
+
* that the tail of a streaming reply holds.
|
|
63
|
+
*
|
|
64
|
+
* The cut is at the paragraph boundary rather than at the marker, because the agent writes the marker inside
|
|
65
|
+
* a sentence — straight after the product name. Cutting at the marker itself puts the card between "The
|
|
66
|
+
* Gore-Tex Offshore Jacket" and "is the shell", breaking the sentence across it. So each text segment is one
|
|
67
|
+
* paragraph, trimmed, and the cards it named follow it in the order they were written.
|
|
68
|
+
*
|
|
69
|
+
* A marker whose id has no card is stripped silently, which is what the backend expects — the prose already
|
|
70
|
+
* names the product. Cards no marker referenced come back as `unmatched`, to render as a shelf below the
|
|
71
|
+
* reply. A product referenced twice gets one card, under the paragraph that mentions it first.
|
|
72
|
+
*
|
|
73
|
+
* There is deliberately no fuzzy name matching: a fallback that guesses degrades silently instead of failing,
|
|
74
|
+
* and any word list it needs is tenant- and language-specific.
|
|
75
|
+
*/
|
|
76
|
+
declare function interleaveProducts(text: string, products: ProductCard[]): {
|
|
77
|
+
segments: ContentSegment[];
|
|
78
|
+
matched: ProductCard[];
|
|
79
|
+
unmatched: ProductCard[];
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* The reply with every marker removed, for a caller that renders prose and cards separately. A partial
|
|
83
|
+
* marker at the end of a streaming reply goes too, for the same reason it does in `interleaveProducts`.
|
|
84
|
+
*/
|
|
85
|
+
declare function stripMarkers(text: string): string;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* A session id for one conversation. The agent requires a session id of at least 33 characters; a bare
|
|
89
|
+
* UUID is 36 and passes, but the prefix makes that constraint visible and the value greppable in logs.
|
|
90
|
+
*
|
|
91
|
+
* The first turn pins the session to its store group and market. A later turn naming a different store is
|
|
92
|
+
* refused, so switching store means a new id.
|
|
93
|
+
*/
|
|
94
|
+
declare function createSessionId(): string;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* A card's price, formatted for display. Every amount on the wire is in minor units, so `59900` in `SEK` is
|
|
98
|
+
* 599,00 kr. Pass the card's own `currencyCode` — it is per product, not per store.
|
|
99
|
+
*/
|
|
100
|
+
declare function formatPrice(amount: number, currencyCode: string, locale?: string): string;
|
|
101
|
+
|
|
102
|
+
export { type ContentSegment, PRODUCT_MARKER, ProductCard, UnknownWireEvent, WireEvent, collectReply, createSessionId, formatPrice, interleaveProducts, isWireEvent, parseWireEvents, stripMarkers };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AgentError
|
|
3
|
+
} from "./chunk-2MMTZSYZ.js";
|
|
4
|
+
|
|
5
|
+
// src/ndjson.ts
|
|
6
|
+
function parseLine(line) {
|
|
7
|
+
const trimmed = line.trim();
|
|
8
|
+
if (trimmed === "") return void 0;
|
|
9
|
+
let parsed;
|
|
10
|
+
try {
|
|
11
|
+
parsed = JSON.parse(trimmed);
|
|
12
|
+
} catch {
|
|
13
|
+
return void 0;
|
|
14
|
+
}
|
|
15
|
+
if (typeof parsed !== "object" || parsed === null) return void 0;
|
|
16
|
+
const event = parsed;
|
|
17
|
+
return typeof event.type === "string" ? event : void 0;
|
|
18
|
+
}
|
|
19
|
+
function isWireEvent(event, type) {
|
|
20
|
+
return event.type === type;
|
|
21
|
+
}
|
|
22
|
+
async function* parseWireEvents(body) {
|
|
23
|
+
const reader = body.getReader();
|
|
24
|
+
const decoder = new TextDecoder();
|
|
25
|
+
let buffer = "";
|
|
26
|
+
try {
|
|
27
|
+
for (; ; ) {
|
|
28
|
+
const { done, value } = await reader.read();
|
|
29
|
+
if (done) break;
|
|
30
|
+
buffer += decoder.decode(value, { stream: true });
|
|
31
|
+
let newline = buffer.indexOf("\n");
|
|
32
|
+
while (newline !== -1) {
|
|
33
|
+
const event = parseLine(buffer.slice(0, newline));
|
|
34
|
+
buffer = buffer.slice(newline + 1);
|
|
35
|
+
if (event !== void 0) yield event;
|
|
36
|
+
newline = buffer.indexOf("\n");
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
buffer += decoder.decode();
|
|
40
|
+
const last = parseLine(buffer);
|
|
41
|
+
if (last !== void 0) yield last;
|
|
42
|
+
} finally {
|
|
43
|
+
reader.releaseLock();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async function collectReply(events) {
|
|
47
|
+
let text = "";
|
|
48
|
+
let products = [];
|
|
49
|
+
let suggestions = [];
|
|
50
|
+
for await (const event of events) {
|
|
51
|
+
if (event.type === "text" && typeof event.content === "string") {
|
|
52
|
+
text += event.content;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (event.type === "products" && Array.isArray(event.products)) {
|
|
56
|
+
products = event.products;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (event.type === "suggestions" && Array.isArray(event.suggestions)) {
|
|
60
|
+
suggestions = event.suggestions;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return { text, products, suggestions };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// src/markers.ts
|
|
67
|
+
var PRODUCT_MARKER = /\[product:([\w-]+)\]/g;
|
|
68
|
+
var MARKER_OPEN = "[product:";
|
|
69
|
+
function hidePartialMarker(text) {
|
|
70
|
+
const open = text.lastIndexOf("[");
|
|
71
|
+
if (open === -1 || text.includes("]", open)) return text;
|
|
72
|
+
const tail = text.slice(open);
|
|
73
|
+
const partial = tail.startsWith(MARKER_OPEN) ? /^\[product:[\w-]*$/.test(tail) : MARKER_OPEN.startsWith(tail);
|
|
74
|
+
return partial ? text.slice(0, open) : text;
|
|
75
|
+
}
|
|
76
|
+
var PARAGRAPH_BREAK = /\n\s*\n/;
|
|
77
|
+
var MARKER_WITH_PADDING = /[^\S\r\n]*\[product:[\w-]+\][^\S\r\n]*/g;
|
|
78
|
+
function stripMarkersFrom(paragraph) {
|
|
79
|
+
return paragraph.replace(MARKER_WITH_PADDING, (gap) => /\s/.test(gap) ? " " : "").trim();
|
|
80
|
+
}
|
|
81
|
+
function interleaveProducts(text, products) {
|
|
82
|
+
const byId = new Map(products.map((product) => [product.productId, product]));
|
|
83
|
+
const segments = [];
|
|
84
|
+
const matched = [];
|
|
85
|
+
const placed = /* @__PURE__ */ new Set();
|
|
86
|
+
const visible = hidePartialMarker(text);
|
|
87
|
+
for (const paragraph of visible.split(PARAGRAPH_BREAK)) {
|
|
88
|
+
const pattern = new RegExp(PRODUCT_MARKER.source, PRODUCT_MARKER.flags);
|
|
89
|
+
const cards = [];
|
|
90
|
+
let match;
|
|
91
|
+
while ((match = pattern.exec(paragraph)) !== null) {
|
|
92
|
+
const product = byId.get(match[1]);
|
|
93
|
+
if (product === void 0 || placed.has(product.productId)) continue;
|
|
94
|
+
placed.add(product.productId);
|
|
95
|
+
matched.push(product);
|
|
96
|
+
cards.push(product);
|
|
97
|
+
}
|
|
98
|
+
const prose = stripMarkersFrom(paragraph);
|
|
99
|
+
if (prose !== "") segments.push({ type: "text", content: prose });
|
|
100
|
+
for (const product of cards) segments.push({ type: "product", product });
|
|
101
|
+
}
|
|
102
|
+
return { segments, matched, unmatched: products.filter((product) => !placed.has(product.productId)) };
|
|
103
|
+
}
|
|
104
|
+
function stripMarkers(text) {
|
|
105
|
+
return hidePartialMarker(text).replace(PRODUCT_MARKER, "");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// src/session.ts
|
|
109
|
+
function createSessionId() {
|
|
110
|
+
return `session-${crypto.randomUUID()}`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// src/price.ts
|
|
114
|
+
function formatPrice(amount, currencyCode, locale) {
|
|
115
|
+
return new Intl.NumberFormat(locale, { style: "currency", currency: currencyCode }).format(amount / 100);
|
|
116
|
+
}
|
|
117
|
+
export {
|
|
118
|
+
AgentError,
|
|
119
|
+
PRODUCT_MARKER,
|
|
120
|
+
collectReply,
|
|
121
|
+
createSessionId,
|
|
122
|
+
formatPrice,
|
|
123
|
+
interleaveProducts,
|
|
124
|
+
isWireEvent,
|
|
125
|
+
parseWireEvents,
|
|
126
|
+
stripMarkers
|
|
127
|
+
};
|