@rudra-js/core 0.8.1 → 0.8.3
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 +457 -364
- package/dist/claim-screening.d.ts.map +1 -1
- package/dist/claim-screening.js +10 -1
- package/dist/claim-screening.js.map +1 -1
- package/dist/spec-cache.d.ts.map +1 -1
- package/dist/spec-cache.js +8 -1
- package/dist/spec-cache.js.map +1 -1
- package/package.json +3 -3
- package/src/claim-screening.ts +9 -1
- package/src/spec-cache.ts +9 -1
package/README.md
CHANGED
|
@@ -2,43 +2,67 @@
|
|
|
2
2
|
|
|
3
3
|
This package is part of [rudra-js](https://rudrajs.com).
|
|
4
4
|
|
|
5
|
-
rudra-js is an open-source TypeScript library (MIT) you install from npm. A
|
|
5
|
+
rudra-js is an open-source TypeScript library (MIT) you install from npm. A
|
|
6
|
+
language model designs one product-recommendation block for a page in your shop,
|
|
7
|
+
rudra-js checks it against your catalog, and React renders it.
|
|
6
8
|
|
|
7
|
-
It is not a hosted platform, a visual or low-code app builder, or a workspace
|
|
9
|
+
It is not a hosted platform, a visual or low-code app builder, or a workspace
|
|
10
|
+
you sign up for.
|
|
8
11
|
|
|
9
|
-
You give this package one page view:
|
|
10
|
-
the page they're on, and the products you're willing to show. It gives back a
|
|
11
|
-
specification for the recommendations on that page, written by your model or
|
|
12
|
-
built without one, and checked against those products. There's no React in it.
|
|
13
|
-
`@rudra-js/react` turns the specification into markup.
|
|
12
|
+
You give this package one page view:
|
|
14
13
|
|
|
15
|
-
|
|
14
|
+
- the shopper and what they've been doing
|
|
15
|
+
- the page they're on
|
|
16
|
+
- the products you're willing to show
|
|
17
|
+
|
|
18
|
+
It gives back a spec: data that describes the recommendations on that page. Your
|
|
19
|
+
model writes it, or core builds it without one. Either way, it's checked against
|
|
20
|
+
those products. There's no React in this package. `@rudra-js/react` turns the
|
|
21
|
+
spec into markup.
|
|
22
|
+
|
|
23
|
+
The guides and the API reference are at [rudrajs.com](https://rudrajs.com).
|
|
16
24
|
|
|
17
25
|
## Install
|
|
18
26
|
|
|
19
27
|
```sh
|
|
20
|
-
npm install @rudra-js/core @rudra-js/attested
|
|
28
|
+
npm install @rudra-js/core@latest @rudra-js/attested@latest
|
|
21
29
|
```
|
|
22
30
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
31
|
+
Run the same command in a new app, in an app that already uses rudra-js, and to
|
|
32
|
+
upgrade.
|
|
33
|
+
|
|
34
|
+
If your app also has `@rudra-js/react` or `@rudra-js/anthropic`, add them to the
|
|
35
|
+
command with `@latest`. Each one's peer range on core covers only its own minor
|
|
36
|
+
release. Leave one behind and npm only warns, but the next `npm install` or
|
|
37
|
+
`npm ci` fails with `ERESOLVE`.
|
|
38
|
+
|
|
39
|
+
If your app has `@rudra-js/a2ui`, use the install line in that package's README
|
|
40
|
+
instead. It also names the two A2UI packages.
|
|
41
|
+
|
|
42
|
+
Keep `@latest` on every name. For a bare name, npm stays inside the range your
|
|
43
|
+
`package.json` already has, so an app on an older core never leaves the minor
|
|
44
|
+
release it's on.
|
|
45
|
+
|
|
46
|
+
`zod` is a dependency, so you don't need to install it yourself. The public API
|
|
47
|
+
of this package _is_ zod schemas. An app on any zod 4 shares one copy of zod
|
|
48
|
+
with it.
|
|
49
|
+
|
|
50
|
+
An app on zod 3 gets its own copy next to ours, and the two don't mix. Build on
|
|
51
|
+
the exported schemas with their own methods (`productSchema.array()`), not with
|
|
52
|
+
your zod's (`z.array(productSchema)`). Read `error.issues` instead of checking
|
|
53
|
+
`instanceof ZodError`.
|
|
54
|
+
|
|
55
|
+
`@rudra-js/anthropic` uses its own zod to write the tool schema sent to the
|
|
56
|
+
model, so the schema's wording shifts a little between zod releases. Before zod
|
|
57
|
+
4.5, a nullable field is written as `anyOf` instead of a type array. Both mean
|
|
58
|
+
the same thing.
|
|
59
|
+
|
|
60
|
+
`@rudra-js/attested` is a peer dependency. None of its types appear in this
|
|
61
|
+
package's public API, and you never have to import it. It's a peer so your app
|
|
62
|
+
has exactly one copy of its phrase list. If your app pins its own version, npm
|
|
63
|
+
stops with an `ERESOLVE` you can see and fix. If you pin nothing, npm installs
|
|
64
|
+
one copy at the top of the tree. One tag publishes every `@rudra-js` package to
|
|
65
|
+
npm at the same version, so these ranges always move together.
|
|
42
66
|
|
|
43
67
|
## Running without a model
|
|
44
68
|
|
|
@@ -50,44 +74,46 @@ const generator = createComponentGenerator({ provider: null });
|
|
|
50
74
|
const spec = await generator.generate(input);
|
|
51
75
|
```
|
|
52
76
|
|
|
53
|
-
This is a supported setting
|
|
54
|
-
|
|
55
|
-
|
|
77
|
+
This is a supported setting. The benchmark compares the models against it. It's
|
|
78
|
+
the right setting until you've picked a provider. `generate` returns a promise
|
|
79
|
+
either way, so your code keeps its shape when you add one.
|
|
56
80
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
is
|
|
61
|
-
|
|
62
|
-
only ever comes from a model. So if you're wiring up bundles and none of them
|
|
63
|
-
appear, that's why. It isn't your catalog.
|
|
81
|
+
Without a model, core builds the spec itself. The rest of this README calls that
|
|
82
|
+
spec the fallback. It has exactly one **grid** block, with a headline from a
|
|
83
|
+
fixed set of four. It has no blocks at all when there's nothing left to show.
|
|
84
|
+
That happens when no candidate is in stock, and also when every candidate is
|
|
85
|
+
ruled out for this shopper.
|
|
64
86
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
87
|
+
The other block kinds (hero, carousel, banner, copy, bundle) only ever come from
|
|
88
|
+
a model. So if you're wiring up bundles and none of them appear, that's why. It
|
|
89
|
+
isn't your catalog.
|
|
90
|
+
|
|
91
|
+
To render a spec you wrote yourself, still without a model, pass
|
|
92
|
+
`createFixedSpecProvider(spec)` as the provider. It answers every request with
|
|
93
|
+
that spec. Your spec goes through the same checks as a model's, so it needs a
|
|
94
|
+
headline that passes the [claim check](#what-the-host-must-supply) and at least
|
|
95
|
+
one product on show. Otherwise you get the fallback.
|
|
70
96
|
|
|
71
97
|
## Options
|
|
72
98
|
|
|
73
99
|
Everything `createComponentGenerator` takes, and what you get if you leave it
|
|
74
100
|
out.
|
|
75
101
|
|
|
76
|
-
| Option | What it does
|
|
77
|
-
| ---------------- |
|
|
78
|
-
| `provider` | The model adapter. `null` runs without a model and bills nothing.
|
|
79
|
-
| `cache` | Where generated specs live between requests. Pass `createNullSpecCache()` to keep none.
|
|
80
|
-
| `generation` | `'cohort'` shares one
|
|
81
|
-
| `rank` | `'signals'` orders products by the shopper's signals
|
|
82
|
-
| `modelTimeoutMs` | How long the model gets.
|
|
83
|
-
| `cacheTimeoutMs` | How long a cache read gets.
|
|
84
|
-
| `onEvent` | Called once per `generate` with a `GenerationEvent`. If your
|
|
102
|
+
| Option | What it does | Default |
|
|
103
|
+
| ---------------- | ------------------------------------------------------------------------------------------------------- | ------------------------- |
|
|
104
|
+
| `provider` | The model adapter. `null` runs without a model and bills nothing. | `null` |
|
|
105
|
+
| `cache` | Where generated specs live between requests. Pass `createNullSpecCache()` to keep none. | `createMemorySpecCache()` |
|
|
106
|
+
| `generation` | `'cohort'` shares one spec between shoppers who look alike. `'per-shopper'` generates for each shopper. | `'cohort'` |
|
|
107
|
+
| `rank` | `'signals'` orders products by the shopper's signals. `'given'` keeps the order you sent. | `'signals'` |
|
|
108
|
+
| `modelTimeoutMs` | How long the model gets. After that, core aborts the request and renders the fallback. | `1500` |
|
|
109
|
+
| `cacheTimeoutMs` | How long a cache read gets. After that, the request generates as if the cache had nothing. | `50` |
|
|
110
|
+
| `onEvent` | Called once per `generate` with a `GenerationEvent`. If your function throws, core ignores the error. | none |
|
|
85
111
|
|
|
86
112
|
## `tracking-input`
|
|
87
113
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
114
|
+
This is the boundary between your app and rudra-js. You own your tracking
|
|
115
|
+
pipeline and pass one JSON object per render. rudra-js collects nothing and
|
|
116
|
+
stores nothing. It doesn't aggregate anything either.
|
|
91
117
|
|
|
92
118
|
```ts
|
|
93
119
|
import { parseTrackingInput } from '@rudra-js/core';
|
|
@@ -112,118 +138,128 @@ const input = parseTrackingInput({
|
|
|
112
138
|
```
|
|
113
139
|
|
|
114
140
|
`parseTrackingInput` throws a `ZodError`. If you'd rather not catch,
|
|
115
|
-
`safeParseTrackingInput`
|
|
141
|
+
`safeParseTrackingInput` returns a `TrackingInputResult` instead. Then you can
|
|
116
142
|
read `result.error.issues` without importing zod yourself.
|
|
117
143
|
|
|
118
144
|
### Cold start is not an error
|
|
119
145
|
|
|
120
|
-
A payload with no `signals` block is a first-time visitor
|
|
121
|
-
|
|
122
|
-
it.
|
|
146
|
+
A payload with no `signals` block is a first-time visitor. Every signal list
|
|
147
|
+
defaults to `[]`, so you don't need a special case for it.
|
|
123
148
|
|
|
124
149
|
### What the host must supply
|
|
125
150
|
|
|
126
|
-
`user.id`, `context.surface
|
|
127
|
-
be unique.
|
|
128
|
-
|
|
129
|
-
`candidates`
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
`bundles` is optional. These are the sets you sell together
|
|
134
|
-
price for the set
|
|
135
|
-
name for
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
151
|
+
Send `user.id`, `context.surface` and at least one entry in `candidates`. SKUs
|
|
152
|
+
must be unique.
|
|
153
|
+
|
|
154
|
+
`candidates` sets which products can reach the page. Core looks up every SKU the
|
|
155
|
+
model writes in that list. A SKU that isn't on it is dropped before anything
|
|
156
|
+
renders.
|
|
157
|
+
|
|
158
|
+
`bundles` is optional. These are the sets you sell together. Each one has your
|
|
159
|
+
own price for the set and the currency that price is in. It can also have your
|
|
160
|
+
own name for the set, its `label`. Every product in a set has to be a candidate
|
|
161
|
+
as well. Bundle ids must be unique, and one set must not name the same product
|
|
162
|
+
twice.
|
|
163
|
+
|
|
164
|
+
A set is placed whole, so one check works differently. A set may hold something
|
|
165
|
+
the shopper already bought or has in the basket. A grid or a carousel would drop
|
|
166
|
+
that product. The whole set is still blocked when any member is:
|
|
167
|
+
|
|
168
|
+
- disliked by the shopper
|
|
169
|
+
- out of stock
|
|
170
|
+
- already on the page
|
|
171
|
+
- the product being looked at right now
|
|
172
|
+
|
|
173
|
+
That last rule has a cost. A "frequently bought together, including this item"
|
|
174
|
+
set names the product whose page it sits on, so it's refused on exactly the page
|
|
175
|
+
you wrote it for. Leave that item out and sell its companions as the set. That
|
|
176
|
+
rejection goes away, and the other checks still apply.
|
|
177
|
+
|
|
178
|
+
When a bundle block is the only block the model asked for, refusing the set
|
|
179
|
+
leaves nothing to render. Then the fallback takes over.
|
|
180
|
+
|
|
181
|
+
The model _never_ picks a set and is never told a price. All it does is ask for
|
|
182
|
+
a bundle block and write the words around it. Core picks which set when the page
|
|
183
|
+
is served. It bases the choice on what the shopper has in their basket, has
|
|
184
|
+
looked at, or is browsing right now.
|
|
185
|
+
|
|
186
|
+
The prompt tells the model to write about the offer, not the products in it. It
|
|
187
|
+
also tells the model never to say the set saves money, or by how much. Your
|
|
188
|
+
`label`, when you pass one, renders ahead of the model's words.
|
|
189
|
+
|
|
190
|
+
Core reads every word the model writes for claims:
|
|
191
|
+
|
|
192
|
+
- the headline and the subheadline
|
|
193
|
+
- a hero
|
|
194
|
+
- a banner
|
|
195
|
+
- a block title
|
|
196
|
+
- the copy block
|
|
197
|
+
- the reason under a product
|
|
198
|
+
- the words around the set
|
|
199
|
+
|
|
200
|
+
Text you supplied is never read this way. A product title, a category and a
|
|
201
|
+
bundle `label` are your words.
|
|
202
|
+
|
|
203
|
+
Core drops text that makes a claim it can't check. This README calls that the
|
|
204
|
+
claim check. It runs in three passes:
|
|
205
|
+
|
|
206
|
+
1. The first looks for money, a customer score, a delivery date and a count of
|
|
207
|
+
what's left.
|
|
208
|
+
2. The second asks whether every numeral in the sentence is one you supplied.
|
|
209
|
+
3. The third is `@rudra-js/attested`'s phrase list. It catches claims with no
|
|
210
|
+
number in them to check, like "top pick" and "customer favourite".
|
|
211
|
+
|
|
212
|
+
For example, the second pass keeps "a comfort rating of -5C" only when a `5`
|
|
213
|
+
turns up in a `tag` or a `category` name you sent. The pass compares the number,
|
|
214
|
+
not the string, so a tag reading `5-pocket` keeps it as surely as one reading
|
|
215
|
+
`-5C comfort`.
|
|
216
|
+
|
|
217
|
+
Tags and category names count because they're the strings core hands the model
|
|
218
|
+
and lets it repeat. The model is also shown each product's `title` and `rating`.
|
|
219
|
+
Neither one backs a number, because the prompt tells it never to restate either.
|
|
220
|
+
Put your product's figures in `tags` and the model can quote them. Leave them
|
|
221
|
+
out, and any figure the model writes about them is made up, so it's dropped.
|
|
222
|
+
|
|
223
|
+
Which strings count, exactly:
|
|
224
|
+
|
|
225
|
+
- Only candidates the model was shown. A product that's out of stock, or past
|
|
226
|
+
the 60 core sends, is one the model never saw. Its tags back nothing.
|
|
227
|
+
- Not the `currentCategory` on the request. It's a string from this request, not
|
|
228
|
+
a row of your catalog, and plenty of sites pass a URL segment straight into
|
|
229
|
+
it.
|
|
186
230
|
- A `reason` or a `badge` sits under a named product, so it's read against that
|
|
187
|
-
product's own tags and category. Every other field
|
|
188
|
-
pooled.
|
|
189
|
-
|
|
190
|
-
Two
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
which it drops rather than waves through.
|
|
231
|
+
product's own tags and category. Every other field is read against all the
|
|
232
|
+
candidates' tags and categories, pooled.
|
|
233
|
+
|
|
234
|
+
Two things to know about the second pass. Pooled means pooled: one product's
|
|
235
|
+
`40 litre` tag backs "take 40 off" written in a headline about another. And
|
|
236
|
+
nothing in the pass knows which quantity a tag was about, so a tag holding a
|
|
237
|
+
weight backs a price with the same digits in it. The pass proves the digits came
|
|
238
|
+
from you. It doesn't prove the sentence is true.
|
|
239
|
+
|
|
240
|
+
The first and third passes are word lists. A word list can miss a claim, so it
|
|
241
|
+
isn't a guarantee the way checking a price against your catalog is. The second
|
|
242
|
+
pass proves something narrower than it sounds: every digit was one of yours. A
|
|
243
|
+
number written as a word isn't a number to it, so it can't see "four and a half
|
|
244
|
+
stars" as a rating. A ½, or the K in "10K", is a numeral it can't read. It drops
|
|
245
|
+
those instead of letting them through.
|
|
203
246
|
|
|
204
247
|
The model isn't told any of this. The prompt bans prices, ratings, discounts,
|
|
205
|
-
delivery dates and stock levels
|
|
206
|
-
`3-season
|
|
207
|
-
the
|
|
208
|
-
down
|
|
209
|
-
|
|
210
|
-
Some fields can't be
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
carries a 3
|
|
217
|
-
|
|
218
|
-
cohort mode the spec is cached before
|
|
219
|
-
|
|
220
|
-
`reason` under a card is exempt is the same idea from the other side: in cohort
|
|
221
|
-
mode we wrote that sentence, so reading it back would only ever cost us.
|
|
222
|
-
|
|
223
|
-
For the set, the prompt also tells the model to write about the offer rather than
|
|
224
|
-
the products in it, and never to say the set saves money or by how much. Pass a
|
|
225
|
-
`label` on the bundle to put your own words on the set. A label is text you
|
|
226
|
-
wrote, not text the model wrote, so it renders ahead of the model's words.
|
|
248
|
+
delivery dates and stock levels. It says nothing about digits. So the model
|
|
249
|
+
can't tell `3-season`, a tag you sent and fine to use, from `2 litres`, which
|
|
250
|
+
isn't. However much good text the check drops on your catalog, nothing in the
|
|
251
|
+
prompt brings that down.
|
|
252
|
+
|
|
253
|
+
Some fields can't be null, like a headline or a banner's text. When the check
|
|
254
|
+
drops text from one of those, it empties the field instead. The block then
|
|
255
|
+
drops, as any block with no text does. An empty page headline makes the whole
|
|
256
|
+
generation unusable.
|
|
257
|
+
|
|
258
|
+
That's the costly case. Say the model writes "Our 3 favourites for wet weather"
|
|
259
|
+
and no candidate carries a 3. The headline empties, so the generation is
|
|
260
|
+
unusable and the fallback renders. You paid for a model call and threw it away.
|
|
261
|
+
In cohort mode the spec is cached before it's checked, so every cache hit runs
|
|
262
|
+
the same check and gets the same fallback until the entry expires.
|
|
227
263
|
|
|
228
264
|
### Defaults
|
|
229
265
|
|
|
@@ -242,52 +278,70 @@ wrote, not text the model wrote, so it renders ahead of the model's words.
|
|
|
242
278
|
| `mostViewed[].views` | `1` |
|
|
243
279
|
| `lastPurchased[].quantity` | `1` |
|
|
244
280
|
|
|
245
|
-
`context.locale` has to be
|
|
246
|
-
|
|
281
|
+
`context.locale` has to be one language tag, such as `en-US`. Not a list, and
|
|
282
|
+
not an `Accept-Language` header.
|
|
247
283
|
|
|
248
284
|
### Cohorts
|
|
249
285
|
|
|
250
|
-
By default
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
286
|
+
By default, shoppers who look alike share one generated spec. Each shopper's own
|
|
287
|
+
products are filled in per request. A group of look-alike shoppers is a cohort.
|
|
288
|
+
A cohort is set by:
|
|
289
|
+
|
|
290
|
+
- the shopper's segment
|
|
291
|
+
- the surface and the slot
|
|
292
|
+
- the locale
|
|
293
|
+
- the item count
|
|
294
|
+
- whether they have any history
|
|
295
|
+
- the category being browsed
|
|
296
|
+
- the category they lean towards
|
|
297
|
+
|
|
298
|
+
Only two of these come from the shopper's own data: whether they have any
|
|
299
|
+
history, and the category they lean towards. Who they are, what they searched
|
|
300
|
+
for and the products they liked or viewed stay out. That's what lets many page
|
|
301
|
+
views reuse one model call.
|
|
258
302
|
|
|
259
|
-
The candidate list is part of the cohort too,
|
|
303
|
+
The candidate list is part of the cohort too, because the model is shown those
|
|
260
304
|
products and writes about them. In most shops candidates come from the page, so
|
|
261
|
-
everyone looking at it shares them. If you pick candidates per shopper you'll
|
|
262
|
-
smaller cohorts
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
305
|
+
everyone looking at it shares them. If you pick candidates per shopper, you'll
|
|
306
|
+
get smaller cohorts, because each prompt really is personal.
|
|
307
|
+
|
|
308
|
+
The cache key for a cohort holds the _set_ of candidate SKUs. It ignores the
|
|
309
|
+
order you sent them in, which ones were in stock, and where the 60-product cut
|
|
310
|
+
fell. So two requests with the same SKUs in a different order share one spec,
|
|
311
|
+
and both get the one generated first. If your ranking is personal and you order
|
|
312
|
+
your candidates per shopper, the products the copy talks about won't be the ones
|
|
313
|
+
you put at the top.
|
|
314
|
+
|
|
315
|
+
Prices, titles and stock are always read from your catalog at render time, so a
|
|
316
|
+
stale one never shows. A product that sold out is dropped as the page is served.
|
|
317
|
+
Any sentence the model wrote about it stays.
|
|
273
318
|
|
|
274
319
|
#### Who can create a cohort
|
|
275
320
|
|
|
276
|
-
Every field above is one you supply, and each distinct value
|
|
277
|
-
own
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
321
|
+
Every cohort field above is one you supply, and each distinct value makes a
|
|
322
|
+
cohort of its own. That's a fresh model call, and a slot in the cache. If a
|
|
323
|
+
visitor can choose one of these values, a visitor can choose how many calls you
|
|
324
|
+
pay for. These are the three that go wrong:
|
|
325
|
+
|
|
326
|
+
- a `locale` copied straight out of `Accept-Language`
|
|
327
|
+
- a `currentCategory` read off a URL slug
|
|
328
|
+
- a `segment` built from a query parameter
|
|
283
329
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
no shopper text at all — the cost is the bill and the model round trip on a page
|
|
287
|
-
that was being served from cache.
|
|
330
|
+
The built-in cache holds 10,000 entries, so once it's full, made-up values push
|
|
331
|
+
out the cohorts your real shoppers were being served from.
|
|
288
332
|
|
|
289
|
-
|
|
290
|
-
|
|
333
|
+
Draw those fields from sets you control:
|
|
334
|
+
|
|
335
|
+
- your own locale list
|
|
336
|
+
- your own category ids
|
|
337
|
+
- your own segment labels
|
|
338
|
+
|
|
339
|
+
Nothing leaks either way, because a cohort prompt carries no shopper text at
|
|
340
|
+
all. The cost is the bill, plus a model round trip on a page that was being
|
|
341
|
+
served from cache.
|
|
342
|
+
|
|
343
|
+
Pass `generation: 'per-shopper'` to generate for each shopper instead. The model
|
|
344
|
+
then picks the products too, and every shopper costs their own call.
|
|
291
345
|
|
|
292
346
|
```ts
|
|
293
347
|
createComponentGenerator({ provider, generation: 'per-shopper' });
|
|
@@ -295,23 +349,30 @@ createComponentGenerator({ provider, generation: 'per-shopper' });
|
|
|
295
349
|
|
|
296
350
|
#### What you put in `segment`
|
|
297
351
|
|
|
298
|
-
`segment`
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
352
|
+
`segment` goes to the model exactly as you wrote it, in both modes. The schema
|
|
353
|
+
takes any string up to 128 characters. Stick to plain merchandising labels like
|
|
354
|
+
`lapsed`, `high-value`, `trial` or `wholesale`. Keep out anything that says
|
|
355
|
+
something protected about a person:
|
|
356
|
+
|
|
357
|
+
- health
|
|
358
|
+
- race
|
|
359
|
+
- ethnic origin
|
|
360
|
+
- religion or belief
|
|
361
|
+
- sex life or sexual orientation
|
|
362
|
+
- politics
|
|
363
|
+
- union membership
|
|
364
|
+
- biometric or genetic data
|
|
304
365
|
|
|
305
366
|
The same goes for `recentSearches`, `context.searchQuery` and `interaction.type`
|
|
306
|
-
in per-shopper mode. Those three are shopper text and they're sent as written.
|
|
307
|
-
What a shopper types
|
|
367
|
+
in per-shopper mode. Those three are shopper text, and they're sent as written.
|
|
368
|
+
What a shopper types belongs to them. The label you give them is your choice.
|
|
308
369
|
|
|
309
370
|
### Limits
|
|
310
371
|
|
|
311
|
-
Every free-text field and every array
|
|
312
|
-
inside a model prompt and a model
|
|
313
|
-
`FIELD_LIMITS
|
|
314
|
-
|
|
372
|
+
Every free-text field and every array has a cap, because your strings end up
|
|
373
|
+
inside a model prompt and you pay for a model per token. The caps live in
|
|
374
|
+
`FIELD_LIMITS`, which is exported. Validate against the same numbers instead of
|
|
375
|
+
finding them out from a rejection.
|
|
315
376
|
|
|
316
377
|
| Limit | Value | Applies to |
|
|
317
378
|
| -------------------- | ----- | ------------------------------------------------------------------- |
|
|
@@ -329,46 +390,64 @@ instead of finding them out from a rejection.
|
|
|
329
390
|
| `maxItems` | 12 | `context.maxItems`, which also needs at least 1 |
|
|
330
391
|
| `reason` | 120 | `candidates[].reason`, your own phrase for a product |
|
|
331
392
|
|
|
332
|
-
|
|
333
|
-
Fitting a payload into a context window is
|
|
334
|
-
|
|
393
|
+
Each cap bounds one field on its own. Together they aren't a budget for the
|
|
394
|
+
whole prompt. Fitting a payload into a context window is the digest's job. The
|
|
395
|
+
digest is core's short summary of the shopper and the page, which goes into the
|
|
396
|
+
prompt. It trims instead of throwing.
|
|
335
397
|
|
|
336
398
|
### Unknown fields are rejected
|
|
337
399
|
|
|
338
|
-
Every fixed-shape object is a `strictObject`. Misspell `recentSearches` and
|
|
339
|
-
|
|
340
|
-
`interaction.meta` is the one
|
|
341
|
-
would
|
|
400
|
+
Every fixed-shape object is a `strictObject`. Misspell `recentSearches` and you
|
|
401
|
+
get an error, instead of a shopper who quietly looks like a first-time visitor.
|
|
402
|
+
`interaction.meta` is the one open shape. It takes any key, except the keys that
|
|
403
|
+
would change a prototype instead of the object.
|
|
342
404
|
|
|
343
405
|
## Bringing your own ranking
|
|
344
406
|
|
|
345
|
-
By default
|
|
346
|
-
shopper's signals.
|
|
347
|
-
|
|
348
|
-
|
|
407
|
+
By default core orders the products for you. It scores each candidate against
|
|
408
|
+
the shopper's signals. You might already have a recommender you trust:
|
|
409
|
+
|
|
410
|
+
- bought-together data
|
|
411
|
+
- an engine trained on your own orders
|
|
412
|
+
- a merchandiser's hand-picked row
|
|
413
|
+
|
|
414
|
+
Pass `rank: 'given'` and the order you sent is the order that renders.
|
|
349
415
|
|
|
350
416
|
```ts
|
|
351
417
|
const generator = createComponentGenerator({ provider, rank: 'given' });
|
|
352
418
|
```
|
|
353
419
|
|
|
354
|
-
You keep the
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
420
|
+
You keep the other checks either way. Core still drops anything the shopper
|
|
421
|
+
shouldn't be shown:
|
|
422
|
+
|
|
423
|
+
- out of stock
|
|
424
|
+
- already bought
|
|
425
|
+
- in the basket
|
|
426
|
+
- disliked
|
|
427
|
+
- the product they're looking at right now
|
|
428
|
+
|
|
429
|
+
Core reads these from the payload you sent, not from the shorter history the
|
|
430
|
+
model was shown. So a shopper with a long order history is covered too. Every
|
|
431
|
+
product still carries a basis, a code like `most_viewed` that says why it's
|
|
432
|
+
shown, and core checks it against the shopper's real signals. Everything the
|
|
433
|
+
model writes still goes through the claim check.
|
|
434
|
+
|
|
435
|
+
There are six bases, and two of them prove less than their names suggest:
|
|
436
|
+
|
|
437
|
+
- `most_viewed` is checked against the SKUs they viewed.
|
|
438
|
+
- `similar_to_current` is checked against the category being browsed.
|
|
439
|
+
- `liked_category` is checked against the categories they bought, liked, carted
|
|
440
|
+
or viewed in. Standing on a category page isn't one of those.
|
|
441
|
+
- `popular` claims nothing.
|
|
442
|
+
- `complements_cart` checks only that the basket isn't empty.
|
|
443
|
+
- `complements_purchase` checks only that the order history isn't empty.
|
|
444
|
+
|
|
445
|
+
The two `complements_` bases are the weak ones, because nothing here can know
|
|
446
|
+
that one product goes with another. A basis that fails takes the sentence and
|
|
447
|
+
the badge stating it with it.
|
|
369
448
|
|
|
370
449
|
Each candidate can carry its own `reason`, the phrase shown under the product.
|
|
371
|
-
|
|
450
|
+
Use it when your ranking knows something the signals don't:
|
|
372
451
|
|
|
373
452
|
```ts
|
|
374
453
|
candidates: [
|
|
@@ -382,18 +461,16 @@ candidates: [
|
|
|
382
461
|
];
|
|
383
462
|
```
|
|
384
463
|
|
|
385
|
-
A reason you supply is your own words, like the title, so it
|
|
386
|
-
|
|
387
|
-
which is the default `cohort` mode. In `per-shopper` mode the model writes
|
|
388
|
-
reasons itself
|
|
389
|
-
|
|
464
|
+
A reason you supply is your own words, like the title, so it renders as written
|
|
465
|
+
and skips the claim check. That only applies where this request actually used
|
|
466
|
+
it, which is the default `cohort` mode. In `per-shopper` mode the model writes
|
|
467
|
+
the reasons itself. Every one of them goes through the claim check, even one
|
|
468
|
+
that reads the same as yours.
|
|
390
469
|
|
|
391
|
-
Without a reason of your own, the basis
|
|
392
|
-
signals
|
|
393
|
-
words, not the model's, so in `cohort` mode they
|
|
394
|
-
|
|
395
|
-
have watched the phrase list delete the reason under every card while the
|
|
396
|
-
deterministic component printed the same sentence untouched.
|
|
470
|
+
Without a reason of your own, core states the basis for you from the shopper's
|
|
471
|
+
signals, like "More in Backpacks" or "Goes with what is in your cart". Those are
|
|
472
|
+
core's words, not the model's, so in `cohort` mode they skip the claim check
|
|
473
|
+
too.
|
|
397
474
|
|
|
398
475
|
## What the model sees
|
|
399
476
|
|
|
@@ -405,13 +482,12 @@ The two generation modes send different things. Cohort is the default.
|
|
|
405
482
|
- the locale
|
|
406
483
|
- the segment, when you set one
|
|
407
484
|
- the category being browsed (`context.currentCategory`)
|
|
408
|
-
- the name of the category the shopper leans towards most
|
|
409
|
-
score stays behind
|
|
485
|
+
- the name of the category the shopper leans towards most, without its score
|
|
410
486
|
- whether this shopper has no history at all, meaning no likes, dislikes,
|
|
411
487
|
purchases, basket items or views. Searches and other interactions don't count
|
|
412
|
-
- how many products the
|
|
413
|
-
- the candidate list: one line per product, with its SKU, title, category,
|
|
414
|
-
and tags
|
|
488
|
+
- how many products the spec may place (`context.maxItems`)
|
|
489
|
+
- the candidate list: one line per product, with its SKU, title, category,
|
|
490
|
+
rating and tags
|
|
415
491
|
|
|
416
492
|
### Per-shopper mode
|
|
417
493
|
|
|
@@ -430,25 +506,24 @@ Everything above, and:
|
|
|
430
506
|
### Left out of both
|
|
431
507
|
|
|
432
508
|
- `user.id`
|
|
433
|
-
- every timestamp (`at`)
|
|
434
|
-
- dwell time (`dwellMs`)
|
|
509
|
+
- every timestamp (`at`). Core uses them to sort signals, then drops them
|
|
510
|
+
- dwell time (`dwellMs`). It's added up in the digest and left out of the prompt
|
|
435
511
|
- every price, and every currency
|
|
436
512
|
- `imageUrl`
|
|
437
513
|
- `interaction.value` and `interaction.meta`. The model is told which kinds of
|
|
438
514
|
interaction happened and how often, and no more
|
|
439
515
|
|
|
440
|
-
The candidate list is trimmed
|
|
441
|
-
and at most 60 products go, in the order you supplied them.
|
|
516
|
+
The candidate list is trimmed before it's sent. Out-of-stock products are
|
|
517
|
+
dropped, and at most 60 products go, in the order you supplied them.
|
|
442
518
|
|
|
443
|
-
`spec-cache.test.ts` walks every field of the digest
|
|
444
|
-
either in the
|
|
445
|
-
leaves out that still changes the prompt fails that test.
|
|
446
|
-
digest fails it too, until someone says which side the field is on.
|
|
519
|
+
`spec-cache.test.ts` walks every field of the digest. It checks that each one is
|
|
520
|
+
either in the cache key or removed from the cohort prompt. A field the key
|
|
521
|
+
leaves out that still changes the prompt fails that test. A new field added to
|
|
522
|
+
the digest fails it too, until someone says which side the field is on.
|
|
447
523
|
|
|
448
524
|
## What the model decides, by mode
|
|
449
525
|
|
|
450
|
-
What the model wrote, and what
|
|
451
|
-
the default.
|
|
526
|
+
What the model wrote, and what core replaces before the page is served.
|
|
452
527
|
|
|
453
528
|
| Decision | Cohort, the default | Per-shopper |
|
|
454
529
|
| ---------------------------------- | --------------------------------------- | -------------------------------------- |
|
|
@@ -461,15 +536,16 @@ the default.
|
|
|
461
536
|
| Which bundle, of the ones you pass | Chosen per request, not by the model | Chosen per request, not by the model |
|
|
462
537
|
|
|
463
538
|
In cohort mode the grid and carousel items are filled in per request, best pick
|
|
464
|
-
first, so a
|
|
539
|
+
first, so a spec written for one shopper still fits the next.
|
|
465
540
|
|
|
466
541
|
The hero is the exception. It keeps the product the model named, because its
|
|
467
|
-
headline and body were written about that product
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
shopper can't see it
|
|
471
|
-
the basket, or the one being looked at
|
|
472
|
-
|
|
542
|
+
headline and body were written about that product. When that product can't be
|
|
543
|
+
placed, core drops the link and keeps the words. It can't be placed when:
|
|
544
|
+
|
|
545
|
+
- this shopper can't see it: it's out of stock, not a candidate, disliked,
|
|
546
|
+
already bought, in the basket, or the one being looked at
|
|
547
|
+
- an earlier block already placed it
|
|
548
|
+
- the item budget ran out before the hero was reached
|
|
473
549
|
|
|
474
550
|
## Any provider
|
|
475
551
|
|
|
@@ -485,33 +561,36 @@ export interface ComponentProvider {
|
|
|
485
561
|
}
|
|
486
562
|
```
|
|
487
563
|
|
|
488
|
-
`createComponentGenerator({ provider })` takes any object of that shape
|
|
489
|
-
API, a model you run yourself, a deployment inside your own
|
|
490
|
-
recorded fixture. `@rudra-js/core` depends on no vendor SDK.
|
|
491
|
-
`@rudra-js/anthropic` is one adapter,
|
|
492
|
-
the default that costs nothing.
|
|
564
|
+
`createComponentGenerator({ provider })` takes any object of that shape. That
|
|
565
|
+
can be a hosted API, a model you run yourself, a deployment inside your own
|
|
566
|
+
tenancy, or a recorded fixture. `@rudra-js/core` depends on no vendor SDK.
|
|
567
|
+
`@rudra-js/anthropic` is one adapter, and you don't need it.
|
|
493
568
|
|
|
494
569
|
An adapter takes its API key as an option, so you choose where the key comes
|
|
495
|
-
from. `ANTHROPIC_API_KEY` is just the name the example shop uses
|
|
496
|
-
|
|
570
|
+
from. `ANTHROPIC_API_KEY` is just the name the example shop uses. No package
|
|
571
|
+
here reads the environment.
|
|
497
572
|
|
|
498
573
|
## The cache
|
|
499
574
|
|
|
500
|
-
`cache`
|
|
501
|
-
for `ttlMs`, 60,000 milliseconds
|
|
502
|
-
`maxEntries`, 10,000
|
|
503
|
-
the first to go.
|
|
504
|
-
|
|
505
|
-
An entry holds the generated spec, as the model returned it before
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
575
|
+
By default `cache` is an in-process store. `createMemorySpecCache()` keeps an
|
|
576
|
+
entry for `ttlMs`, which defaults to 60,000 milliseconds, so one minute. It
|
|
577
|
+
holds up to `maxEntries`, which defaults to 10,000. Once it's full, the entry
|
|
578
|
+
read longest ago is the first to go.
|
|
579
|
+
|
|
580
|
+
An entry holds the generated spec, as the model returned it before any checks.
|
|
581
|
+
It also holds `generatedAt`, the epoch milliseconds when the model produced it.
|
|
582
|
+
It holds no payload and no prompt.
|
|
583
|
+
|
|
584
|
+
In cohort mode an entry holds no shopper id, and everyone in the cohort shares
|
|
585
|
+
it. The exception is an entry whose segment or candidates were picked for one
|
|
586
|
+
shopper. In per-shopper mode the model wrote the entry from that shopper's
|
|
587
|
+
signals. So its text can repeat them, a search term for instance, and the entry
|
|
588
|
+
is personal data until it's gone.
|
|
589
|
+
|
|
590
|
+
Expiry is checked when an entry is read, so an expired entry stays in memory
|
|
512
591
|
until it's read again or pushed out by a newer one.
|
|
513
592
|
|
|
514
|
-
The
|
|
593
|
+
The cache interface is two methods, and an optional third:
|
|
515
594
|
|
|
516
595
|
```ts
|
|
517
596
|
export interface SpecCache {
|
|
@@ -521,125 +600,139 @@ export interface SpecCache {
|
|
|
521
600
|
}
|
|
522
601
|
```
|
|
523
602
|
|
|
524
|
-
Pass your own store, whether that's Redis, Memcached or whatever you already
|
|
525
|
-
and it keeps entries on its own terms.
|
|
526
|
-
|
|
527
|
-
|
|
603
|
+
Pass your own store, whether that's Redis, Memcached or whatever you already
|
|
604
|
+
run, and it keeps entries on its own terms. What that store holds, and for how
|
|
605
|
+
long, is yours to declare to your users. This package doesn't set it. Pass
|
|
606
|
+
`createNullSpecCache()` to store nothing at all.
|
|
528
607
|
|
|
529
608
|
### When a generation is wrong
|
|
530
609
|
|
|
531
|
-
Pass `provider: null` and nothing new is generated, so every page renders the
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
610
|
+
- Pass `provider: null` and nothing new is generated, so every page renders the
|
|
611
|
+
fallback.
|
|
612
|
+
- Shorten `ttlMs` and a bad entry ends sooner.
|
|
613
|
+
- If your store has `delete`, drop one entry by the `key` on its
|
|
614
|
+
`GenerationEvent`. The next request generates again.
|
|
615
|
+
|
|
616
|
+
Per-shopper entries only end when they expire, because nothing maps a shopper to
|
|
617
|
+
their keys.
|
|
536
618
|
|
|
537
619
|
## Watching it in production
|
|
538
620
|
|
|
539
621
|
The generator never fails a render, so a provider that's been down for a week
|
|
540
|
-
only shows as plainer pages.
|
|
541
|
-
`generate` that gets past input validation reports exactly one
|
|
542
|
-
A payload that fails `parseTrackingInput` throws instead, and
|
|
622
|
+
only shows up as plainer pages. Watch `onEvent` to find out. Every call to
|
|
623
|
+
`generate` that gets past input validation reports exactly one
|
|
624
|
+
`GenerationEvent`. A payload that fails `parseTrackingInput` throws instead, and
|
|
625
|
+
reports nothing.
|
|
543
626
|
|
|
544
|
-
|
|
627
|
+
Keep these numbers:
|
|
545
628
|
|
|
546
|
-
- **Fallback share
|
|
547
|
-
it climbs. `degradedReason` tells you which way the call failed
|
|
548
|
-
|
|
549
|
-
- **Cache hit rate
|
|
629
|
+
- **Fallback share.** The share of events with `source: 'fallback'`. Alert when
|
|
630
|
+
it climbs. `degradedReason` tells you which way the call failed. When the
|
|
631
|
+
reason is `'provider-error'` or `'timeout'`, `error` carries what was thrown.
|
|
632
|
+
- **Cache hit rate.** `cache: 'hit'` over the events that have a `cache` field.
|
|
550
633
|
A store that's down shows as `cache: 'error'`, and a slow one as
|
|
551
|
-
`cache: 'timeout'
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
634
|
+
`cache: 'timeout'`. So a broken store shows up here, not only as a rising
|
|
635
|
+
bill.
|
|
636
|
+
- **Spend.** Sum `usage` over the events where `calledModel` is true. Requests
|
|
637
|
+
that joined a generation already in flight carry the same `usage`, so summing
|
|
638
|
+
over every event counts one call many times. Some calls the model answered are
|
|
639
|
+
turned down by the adapter: a `max_tokens` stop, a refusal, no tool call, or a
|
|
640
|
+
reply that doesn't fit the schema. Those were billed too. They arrive as a
|
|
557
641
|
`'provider-error'` with no `usage`, so this sum is lower than your bill.
|
|
558
642
|
|
|
559
643
|
## Running it in a regulated shop
|
|
560
644
|
|
|
561
|
-
This isn't legal advice, and which rules apply depends on where you sell. It
|
|
562
|
-
what the package does, so you can answer what a privacy or
|
|
563
|
-
will ask.
|
|
645
|
+
This isn't legal advice, and which rules apply depends on where you sell. It
|
|
646
|
+
describes what the package does, so you can answer what a privacy or
|
|
647
|
+
consumer-law review will ask.
|
|
564
648
|
|
|
565
649
|
**What leaves your server.** Nothing, unless you pass a `provider`, or a `cache`
|
|
566
650
|
whose store lives somewhere else. With a provider, cohort mode sends the page
|
|
567
651
|
and locale, the category, the segment you set, the shopper's strongest category,
|
|
568
|
-
whether they have any history, and the candidate products.
|
|
569
|
-
|
|
570
|
-
flag come from that shopper's data
|
|
571
|
-
|
|
572
|
-
Per-shopper mode adds the shopper's own signals. Both lists are under
|
|
573
|
-
model sees](#what-the-model-sees). `user.id` is never sent in either
|
|
574
|
-
provider should be your processor, under a contract that says so
|
|
575
|
-
see the `@rudra-js/anthropic` README.
|
|
652
|
+
whether they have any history, and the candidate products. That has no shopper
|
|
653
|
+
id, but it can still be personal data. The strongest category and the history
|
|
654
|
+
flag come from that shopper's data. The segment goes exactly as you wrote it.
|
|
655
|
+
The candidate list is about them too if you pick or order it per shopper.
|
|
656
|
+
Per-shopper mode adds the shopper's own signals. Both lists are under
|
|
657
|
+
[What the model sees](#what-the-model-sees). `user.id` is never sent in either
|
|
658
|
+
mode. The provider should be your processor, under a contract that says so. For
|
|
659
|
+
Anthropic, see the `@rudra-js/anthropic` README.
|
|
576
660
|
|
|
577
661
|
**A shopper who objects to profiling.** Leave `signals` and `user.segment` out
|
|
578
|
-
of their payload
|
|
579
|
-
`user.isReturning` too. They get
|
|
580
|
-
and the catalog, not from them. With those left out,
|
|
581
|
-
gives the same kind of
|
|
582
|
-
with signals, it still personalises.
|
|
662
|
+
of their payload. In per-shopper mode, leave out `context.searchQuery` and
|
|
663
|
+
`user.isReturning` too. They get what a first-time visitor gets, built from the
|
|
664
|
+
page and the catalog, not from them. With those left out,
|
|
665
|
+
`generateDeterministic(input)` gives the same kind of spec without calling the
|
|
666
|
+
model or the cache. Called with signals, it still personalises.
|
|
583
667
|
|
|
584
668
|
**Deleting a shopper's data.** The package keeps nothing about a shopper except
|
|
585
669
|
the cache. In cohort mode an entry holds no shopper id and everyone in the
|
|
586
670
|
cohort shares it, so usually there's no one shopper's entry to delete. The
|
|
587
671
|
exception is a cohort entry whose segment or candidate list was picked for one
|
|
588
|
-
shopper
|
|
672
|
+
shopper. Like every per-shopper entry, it's about that shopper. Nothing maps a
|
|
589
673
|
shopper to their entries, so keep `ttlMs` short or pass `createNullSpecCache()`.
|
|
590
|
-
`onEvent` sends no shopper id
|
|
591
|
-
one, and `violations` can name SKUs from a shopper's basket or purchases
|
|
674
|
+
`onEvent` sends no shopper id. But in per-shopper mode its `key` is derived from
|
|
675
|
+
one, and `violations` can name SKUs from a shopper's basket or purchases. So
|
|
592
676
|
treat logs of it as personal data.
|
|
593
677
|
|
|
594
|
-
**Caching pages.** A personalised page says things like "Goes with your cart"
|
|
595
|
-
"You looked at this recently" in its HTML. Serve those pages with
|
|
678
|
+
**Caching pages.** A personalised page says things like "Goes with your cart"
|
|
679
|
+
and "You looked at this recently" in its HTML. Serve those pages with
|
|
596
680
|
`Cache-Control: private` or `no-store`, so a shared cache never hands one
|
|
597
681
|
shopper's page to another.
|
|
598
682
|
|
|
599
683
|
**Telling shoppers a model wrote the copy.** The wrapper's `data-rudra-source`
|
|
600
|
-
says `llm` or `cache` when a model wrote the copy and `fallback` when it
|
|
601
|
-
|
|
602
|
-
Whether you must label AI-written copy, and how, depends on
|
|
603
|
-
the EU, Article 50 of the AI Act is the rule to ask about.
|
|
684
|
+
says `llm` or `cache` when a model wrote the copy, and `fallback` when it
|
|
685
|
+
didn't. That makes it a machine-readable marker you can build on, for example
|
|
686
|
+
to show a note. Whether you must label AI-written copy, and how, depends on
|
|
687
|
+
where you sell. In the EU, Article 50 of the AI Act is the rule to ask about.
|
|
604
688
|
|
|
605
689
|
**Claims.** The copy on your page is your own advertising, whoever wrote it. The
|
|
606
|
-
|
|
607
|
-
environmental claims
|
|
608
|
-
explains why, and the tests list the known ones
|
|
609
|
-
yours to stand behind.
|
|
690
|
+
claim check catches prices, discounts, stock, ratings, delivery, urgency and
|
|
691
|
+
generic environmental claims. But it's a list, and rewordings get through.
|
|
692
|
+
SECURITY.md explains why, and the tests list the known ones. Anything it misses
|
|
693
|
+
is still yours to stand behind.
|
|
610
694
|
|
|
611
695
|
**Children.** In the UK, the ICO's Children's Code covers any shop that
|
|
612
696
|
under-18s are more likely than not to use, not only one aimed at them. Its
|
|
613
697
|
standard 12 asks for profiling to be off by default unless you can show a
|
|
614
|
-
compelling reason, judged by the child's best interests
|
|
615
|
-
|
|
616
|
-
18, leave `signals` and `user.segment` out of their
|
|
617
|
-
switched personalisation on, as for a shopper who objects
|
|
618
|
-
can't tell a shopper's age with confidence that fits the
|
|
619
|
-
shopper as under 18. Cohort mode alone doesn't turn profiling
|
|
620
|
-
`rank: 'signals'` still orders products by the shopper's
|
|
621
|
-
and in the fallback
|
|
622
|
-
sends their strongest category, whether they have any history, and
|
|
623
|
-
to the model.
|
|
698
|
+
compelling reason, judged by the child's best interests. It also asks for
|
|
699
|
+
safeguards when profiling is on. For a shop like that, don't use per-shopper
|
|
700
|
+
mode. For a shopper under 18, leave `signals` and `user.segment` out of their
|
|
701
|
+
payload unless they've switched personalisation on, as for a shopper who objects
|
|
702
|
+
to profiling. If you can't tell a shopper's age with confidence that fits the
|
|
703
|
+
risk, treat every shopper as under 18. Cohort mode alone doesn't turn profiling
|
|
704
|
+
off. The default `rank: 'signals'` still orders products by the shopper's
|
|
705
|
+
signals in cohort mode and in the fallback, even with no provider. And cohort
|
|
706
|
+
mode still sends their strongest category, whether they have any history, and
|
|
707
|
+
any segment to the model.
|
|
624
708
|
|
|
625
709
|
**Marketplaces.** If other sellers list products on your site, EU platform rules
|
|
626
|
-
(Article 27 of the Digital Services Act) ask you
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
710
|
+
(Article 27 of the Digital Services Act) ask you to set out in your terms:
|
|
711
|
+
|
|
712
|
+
- your recommender's main parameters
|
|
713
|
+
- why they weigh as they do
|
|
714
|
+
- any options shoppers have to change them
|
|
715
|
+
|
|
716
|
+
This doesn't apply if you're a micro or small business. If shoppers can pick
|
|
717
|
+
between options, such as switching personalisation off, the control has to sit
|
|
718
|
+
where the products are shown.
|
|
719
|
+
|
|
720
|
+
Which parameters apply depends on your mode and your `rank` setting:
|
|
721
|
+
|
|
722
|
+
- `rank` sets the order in cohort mode and in the fallback. With `rank: 'given'`
|
|
723
|
+
that's the order you sent, so describe your own ranking.
|
|
724
|
+
- With `rank: 'signals'` the order comes from the shopper's category interest
|
|
725
|
+
(the category they're browsing counts too), how often they've viewed a
|
|
726
|
+
product, its rating, and tags it shares with products they've engaged with.
|
|
727
|
+
Category weighs most.
|
|
728
|
+
- In per-shopper mode the model picks which candidates to show and in what
|
|
729
|
+
order, from what's under [What the model sees](#what-the-model-sees).
|
|
730
|
+
- In cohort mode the model picks the hero's product, from the cohort list in
|
|
731
|
+
that section.
|
|
732
|
+
- In every mode, anything out of stock, disliked or being looked at right now is
|
|
733
|
+
left out. So is anything bought or in the basket, except as part of a bundle.
|
|
734
|
+
- A bundle is picked by what's in the basket, then what they viewed, then the
|
|
735
|
+
category being browsed.
|
|
643
736
|
|
|
644
737
|
**Accessibility.** Every card is one link with a readable name, and headings
|
|
645
738
|
only render when they have text. Contrast, focus styles and layout come from
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"claim-screening.d.ts","sourceRoot":"","sources":["../src/claim-screening.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AA4EzD,eAAO,MAAM,eAAe,UAK3B,CAAC;AAKF,eAAO,MAAM,QAAQ,EAAE,SAAS,MAAM,EAAO,CAAC;AAE9C,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;CAC9B;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,aAAa,GAAG,SAAS,CAWzD;
|
|
1
|
+
{"version":3,"file":"claim-screening.d.ts","sourceRoot":"","sources":["../src/claim-screening.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AA4EzD,eAAO,MAAM,eAAe,UAK3B,CAAC;AAKF,eAAO,MAAM,QAAQ,EAAE,SAAS,MAAM,EAAO,CAAC;AAE9C,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;CAC9B;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,aAAa,GAAG,SAAS,CAWzD;AAqBD,wBAAgB,aAAa,CAC3B,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,SAAS,MAAM,EAAE,GACvB,MAAM,GAAG,IAAI,CASf"}
|
package/dist/claim-screening.js
CHANGED
|
@@ -101,7 +101,16 @@ function kindIn(reading) {
|
|
|
101
101
|
return null;
|
|
102
102
|
}
|
|
103
103
|
function claimIn(text) {
|
|
104
|
-
|
|
104
|
+
const readings = new Set([foldLookalikes(text), foldLookalikes(text, true)]);
|
|
105
|
+
if (text.includes('\u04C0')) {
|
|
106
|
+
readings.add(foldLookalikes(text.replaceAll('\u04C0', '\u04CF'), true));
|
|
107
|
+
}
|
|
108
|
+
for (const reading of readings) {
|
|
109
|
+
const kind = kindIn(reading);
|
|
110
|
+
if (kind !== null)
|
|
111
|
+
return kind;
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
105
114
|
}
|
|
106
115
|
export function unbackedClaim(raw, clamped, facts) {
|
|
107
116
|
const kind = claimIn(raw) ?? claimIn(clamped);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"claim-screening.js","sourceRoot":"","sources":["../src/claim-screening.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAC5D,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAGtD,MAAM,cAAc,GAA2C;IAC7D;QACE,IAAI,EAAE,QAAQ;QACd,QAAQ,EAAE;YACR,2BAA2B;YAC3B,mDAAmD;YACnD,0BAA0B;YAC1B,kEAAkE;YAClE,+BAA+B;YAC/B,0CAA0C;YAC1C,yDAAyD;YACzD,uBAAuB;YACvB,2DAA2D;YAC3D,kCAAkC;YAClC,6DAA6D;YAC7D,+CAA+C;SAChD;KACF;IACD;QACE,IAAI,EAAE,OAAO;QACb,QAAQ,EAAE;YACR,SAAS;YACT,6DAA6D;YAC7D,yBAAyB;YACzB,wBAAwB;YACxB,qBAAqB;YACrB,yBAAyB;YACzB,6DAA6D;YAC7D,qBAAqB;YACrB,4BAA4B;YAC5B,2DAA2D;YAC3D,yBAAyB;YACzB,kDAAkD;SACnD;KACF;IACD;QACE,IAAI,EAAE,UAAU;QAChB,QAAQ,EAAE;YACR,qEAAqE;YACrE,iGAAiG;YACjG,uBAAuB;YACvB,sCAAsC;YACtC,6BAA6B;YAC7B,8BAA8B;YAC9B,4DAA4D;YAC5D,mBAAmB;YACnB,mBAAmB;YACnB,kCAAkC;YAClC,yCAAyC;SAC1C;KACF;IACD;QACE,IAAI,EAAE,UAAU;QAChB,QAAQ,EAAE;YACR,2BAA2B;YAC3B,iDAAiD;YACjD,kMAAkM;YAClM,qDAAqD;YACrD,iBAAiB;SAClB;KACF;IACD;QACE,IAAI,EAAE,OAAO;QACb,QAAQ,EAAE;YACR,gCAAgC;YAChC,mBAAmB;YACnB,6BAA6B;YAC7B,sFAAsF;YACtF,yFAAyF;YACzF,mEAAmE;SACpE;KACF;CACF,CAAC;AAEF,MAAM,CAAC,MAAM,eAAe,GAAG;IAC7B,qBAAqB;IACrB,oBAAoB;IACpB,gBAAgB;IAChB,kBAAkB;CACnB,CAAC;AAEF,MAAM,UAAU,GAAG,SAAS,CAAC;AAC7B,MAAM,OAAO,GAAG,uBAAuB,CAAC;AAExC,MAAM,CAAC,MAAM,QAAQ,GAAsB,EAAE,CAAC;AAO9C,MAAM,UAAU,SAAS,CAAC,KAAoB;IAC5C,MAAM,MAAM,GAAG,IAAI,GAAG,EAAU,CAAC;IACjC,MAAM,KAAK,GAAG,IAAI,GAAG,EAAoB,CAAC;IAE1C,KAAK,MAAM,OAAO,IAAI,iBAAiB,CAAC,KAAK,CAAC,EAAE,CAAC;QAC/C,MAAM,GAAG,GAAG,CAAC,OAAO,CAAC,QAAQ,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;QACxF,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;QAC5B,KAAK,MAAM,IAAI,IAAI,GAAG;YAAE,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC3C,CAAC;IAED,OAAO,EAAE,MAAM,EAAE,CAAC,GAAG,MAAM,CAAC,EAAE,KAAK,EAAE,CAAC;AACxC,CAAC;AAED,SAAS,MAAM,CAAC,OAAe;IAC7B,KAAK,MAAM,KAAK,IAAI,cAAc,EAAE,CAAC;QACnC,IAAI,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YAAE,OAAO,KAAK,CAAC,IAAI,CAAC;IACjF,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAS,OAAO,CAAC,IAAY;IAC3B,
|
|
1
|
+
{"version":3,"file":"claim-screening.js","sourceRoot":"","sources":["../src/claim-screening.ts"],"names":[],"mappings":"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"}
|
package/dist/spec-cache.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"spec-cache.d.ts","sourceRoot":"","sources":["../src/spec-cache.ts"],"names":[],"mappings":"AACA,OAAO,EAAgB,KAAK,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEvE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEvD,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,aAAa,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,SAAS;IACxB,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,SAAS,CAAC,CAAC;IAClD,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpD,MAAM,CAAC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACrC;AAED,MAAM,WAAW,sBAAsB;IACrC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAgBD,wBAAgB,qBAAqB,CAAC,OAAO,GAAE,sBAA2B,GAAG,SAAS,CAuCrF;AAED,wBAAgB,mBAAmB,IAAI,SAAS,CAE/C;
|
|
1
|
+
{"version":3,"file":"spec-cache.d.ts","sourceRoot":"","sources":["../src/spec-cache.ts"],"names":[],"mappings":"AACA,OAAO,EAAgB,KAAK,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEvE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEvD,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,aAAa,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,SAAS;IACxB,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,SAAS,CAAC,CAAC;IAClD,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpD,MAAM,CAAC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACrC;AAED,MAAM,WAAW,sBAAsB;IACrC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAgBD,wBAAgB,qBAAqB,CAAC,OAAO,GAAE,sBAA2B,GAAG,SAAS,CAuCrF;AAED,wBAAgB,mBAAmB,IAAI,SAAS,CAE/C;AA2BD,wBAAgB,YAAY,CAC1B,MAAM,EAAE,YAAY,EACpB,aAAa,EAAE,SAAS,MAAM,EAAE,EAChC,QAAQ,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACxC,MAAM,CAUR"}
|
package/dist/spec-cache.js
CHANGED
|
@@ -63,12 +63,19 @@ function canonicalise(value) {
|
|
|
63
63
|
return `{${fields.join(',')}}`;
|
|
64
64
|
}
|
|
65
65
|
const PROMPT_FINGERPRINT = createHash('sha256').update(SYSTEM_PROMPT).digest('hex').slice(0, 16);
|
|
66
|
+
function withoutDwellAndScores(digest) {
|
|
67
|
+
return {
|
|
68
|
+
...digest,
|
|
69
|
+
topViewed: digest.topViewed.map(({ dwellMs: _dwellMs, ...view }) => view),
|
|
70
|
+
categoryAffinity: digest.categoryAffinity.map(({ score: _score, ...affinity }) => affinity),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
66
73
|
export function specCacheKey(digest, candidateSkus, provider) {
|
|
67
74
|
const material = canonicalise({
|
|
68
75
|
specVersion: SPEC_VERSION,
|
|
69
76
|
prompt: PROMPT_FINGERPRINT,
|
|
70
77
|
provider,
|
|
71
|
-
digest,
|
|
78
|
+
digest: withoutDwellAndScores(digest),
|
|
72
79
|
candidates: candidateSkus.toSorted(),
|
|
73
80
|
});
|
|
74
81
|
return createHash('sha256').update(material).digest('hex').slice(0, 32);
|
package/dist/spec-cache.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"spec-cache.js","sourceRoot":"","sources":["../src/spec-cache.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"spec-cache.js","sourceRoot":"","sources":["../src/spec-cache.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rudra-js/core",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.3",
|
|
4
4
|
"description": "Core of rudra-js, a TypeScript library for LLM-designed product recommendations: turns one page view into a recommendation spec checked against your catalog",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"rudra-js",
|
|
@@ -60,10 +60,10 @@
|
|
|
60
60
|
"zod": "^4.6.7"
|
|
61
61
|
},
|
|
62
62
|
"peerDependencies": {
|
|
63
|
-
"@rudra-js/attested": "^0.8.
|
|
63
|
+
"@rudra-js/attested": "^0.8.3"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
|
-
"@rudra-js/attested": "0.8.
|
|
66
|
+
"@rudra-js/attested": "0.8.3",
|
|
67
67
|
"@types/node": "^26.4.1"
|
|
68
68
|
}
|
|
69
69
|
}
|
package/src/claim-screening.ts
CHANGED
|
@@ -114,7 +114,15 @@ function kindIn(reading: string): string | null {
|
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
function claimIn(text: string): string | null {
|
|
117
|
-
|
|
117
|
+
const readings = new Set([foldLookalikes(text), foldLookalikes(text, true)]);
|
|
118
|
+
if (text.includes('\u04C0')) {
|
|
119
|
+
readings.add(foldLookalikes(text.replaceAll('\u04C0', '\u04CF'), true));
|
|
120
|
+
}
|
|
121
|
+
for (const reading of readings) {
|
|
122
|
+
const kind = kindIn(reading);
|
|
123
|
+
if (kind !== null) return kind;
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
118
126
|
}
|
|
119
127
|
|
|
120
128
|
export function unbackedClaim(
|
package/src/spec-cache.ts
CHANGED
|
@@ -96,6 +96,14 @@ function canonicalise(value: unknown): string {
|
|
|
96
96
|
|
|
97
97
|
const PROMPT_FINGERPRINT = createHash('sha256').update(SYSTEM_PROMPT).digest('hex').slice(0, 16);
|
|
98
98
|
|
|
99
|
+
function withoutDwellAndScores(digest: SignalDigest) {
|
|
100
|
+
return {
|
|
101
|
+
...digest,
|
|
102
|
+
topViewed: digest.topViewed.map(({ dwellMs: _dwellMs, ...view }) => view),
|
|
103
|
+
categoryAffinity: digest.categoryAffinity.map(({ score: _score, ...affinity }) => affinity),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
99
107
|
export function specCacheKey(
|
|
100
108
|
digest: SignalDigest,
|
|
101
109
|
candidateSkus: readonly string[],
|
|
@@ -105,7 +113,7 @@ export function specCacheKey(
|
|
|
105
113
|
specVersion: SPEC_VERSION,
|
|
106
114
|
prompt: PROMPT_FINGERPRINT,
|
|
107
115
|
provider,
|
|
108
|
-
digest,
|
|
116
|
+
digest: withoutDwellAndScores(digest),
|
|
109
117
|
candidates: candidateSkus.toSorted(),
|
|
110
118
|
});
|
|
111
119
|
|