mbd-studio-sdk 2.1.2 → 2.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/StudioConfig.js CHANGED
@@ -1,127 +1,40 @@
1
- /**
2
- * @typedef {Object} ServicesUrl
3
- * @property {string} searchService - URL of the search service
4
- * @property {string} storiesService - URL of the stories service
5
- * @property {string} featuresService - URL of the features service
6
- * @property {string} scoringService - URL of the scoring service
7
- * @property {string} rankingService - URL of the ranking service
8
- */
9
-
10
- /**
11
- * @typedef {Object} StudioConfigOptions
12
- * @property {string} apiKey - API key for authentication (required)
13
- * @property {string} [commonUrl] - Base URL used for all services when provided
14
- * @property {ServicesUrl} [servicesUrl] - Per-service URLs when commonUrl is not provided
15
- * @property {function(string): void} [log] - Optional log function, defaults to console.log
16
- * @property {function(*): void} [show] - Optional show function, defaults to console.log
17
- */
18
-
19
- /**
20
- * Configuration for MBD Studio SDK.
21
- * Use commonUrl for a single URL for all services, or servicesUrl for one URL per service.
22
- */
23
1
  export class StudioConfig {
24
- /**
25
- * @param {StudioConfigOptions} options - Configuration options
26
- * @throws {Error} When parameters are invalid or missing required fields
27
- */
28
2
  constructor(options) {
29
3
  if (!options || typeof options !== 'object') {
30
4
  throw new Error('StudioConfig: options object is required');
31
5
  }
32
6
  const { apiKey, commonUrl, servicesUrl, log, show } = options;
33
-
34
7
  if (typeof apiKey !== 'string' || !apiKey.trim()) {
35
8
  throw new Error('StudioConfig: apiKey is required and must be a non-empty string');
36
9
  }
37
-
38
- const hasCommonUrl =
39
- typeof commonUrl === 'string' && commonUrl.trim().length > 0;
40
-
10
+ const hasCommonUrl = typeof commonUrl === 'string' && commonUrl.trim().length > 0;
41
11
  if (hasCommonUrl) {
42
12
  const url = commonUrl.trim().replace(/\/$/, '');
43
- this._searchService = url;
44
- this._storiesService = url;
45
- this._featuresService = url;
46
- this._scoringService = url;
47
- this._rankingService = url;
13
+ this.searchService = url;
14
+ this.storiesService = url;
15
+ this.featuresService = url;
16
+ this.scoringService = url;
17
+ this.rankingService = url;
48
18
  } else {
49
19
  if (!servicesUrl || typeof servicesUrl !== 'object') {
50
- throw new Error(
51
- 'StudioConfig: when commonUrl is not provided, servicesUrl must be an object with searchService, storiesService, featuresService, scoringService, rankingService'
52
- );
20
+ throw new Error('StudioConfig: when commonUrl is not provided, servicesUrl must be an object with searchService, storiesService, featuresService, scoringService, rankingService');
53
21
  }
54
- const {
55
- searchService,
56
- storiesService,
57
- featuresService,
58
- scoringService,
59
- rankingService,
60
- } = servicesUrl;
61
- const services = {
62
- searchService,
63
- storiesService,
64
- featuresService,
65
- scoringService,
66
- rankingService,
67
- };
22
+ const { searchService, storiesService, featuresService, scoringService, rankingService } = servicesUrl;
23
+ const services = { searchService, storiesService, featuresService, scoringService, rankingService };
68
24
  const missing = Object.entries(services)
69
25
  .filter(([, v]) => typeof v !== 'string' || !v.trim())
70
26
  .map(([k]) => k);
71
27
  if (missing.length > 0) {
72
- throw new Error(
73
- `StudioConfig: when using servicesUrl, all service URLs are required. Missing or invalid: ${missing.join(', ')}`
74
- );
28
+ throw new Error(`StudioConfig: when using servicesUrl, all service URLs are required. Missing or invalid: ${missing.join(', ')}`);
75
29
  }
76
- this._searchService = searchService.trim().replace(/\/$/, '');
77
- this._storiesService = storiesService.trim().replace(/\/$/, '');
78
- this._featuresService = featuresService.trim().replace(/\/$/, '');
79
- this._scoringService = scoringService.trim().replace(/\/$/, '');
80
- this._rankingService = rankingService.trim().replace(/\/$/, '');
30
+ this.searchService = searchService.trim().replace(/\/$/, '');
31
+ this.storiesService = storiesService.trim().replace(/\/$/, '');
32
+ this.featuresService = featuresService.trim().replace(/\/$/, '');
33
+ this.scoringService = scoringService.trim().replace(/\/$/, '');
34
+ this.rankingService = rankingService.trim().replace(/\/$/, '');
81
35
  }
82
-
83
- this._apiKey = apiKey.trim();
84
- this._log = typeof log === 'function' ? log : console.log.bind(console);
85
- this._show = typeof show === 'function' ? show : console.log.bind(console);
86
- }
87
-
88
- /** @returns {function(string): void} */
89
- get log() {
90
- return this._log;
91
- }
92
-
93
- /** @returns {function(*): void} */
94
- get show() {
95
- return this._show;
96
- }
97
-
98
- /** @returns {string} */
99
- get searchService() {
100
- return this._searchService;
101
- }
102
-
103
- /** @returns {string} */
104
- get storiesService() {
105
- return this._storiesService;
106
- }
107
-
108
- /** @returns {string} */
109
- get featuresService() {
110
- return this._featuresService;
111
- }
112
-
113
- /** @returns {string} */
114
- get scoringService() {
115
- return this._scoringService;
116
- }
117
-
118
- /** @returns {string} */
119
- get rankingService() {
120
- return this._rankingService;
121
- }
122
-
123
- /** @returns {string} */
124
- get apiKey() {
125
- return this._apiKey;
36
+ this.apiKey = apiKey.trim();
37
+ this.log = typeof log === 'function' ? log : console.log.bind(console);
38
+ this.show = typeof show === 'function' ? show : console.log.bind(console);
126
39
  }
127
40
  }
package/V1/Studio.js CHANGED
@@ -1,8 +1,3 @@
1
- /**
2
- * Root entry point for the MBD Studio SDK.
3
- * Holds configuration and provides factory methods for service clients.
4
- */
5
-
6
1
  import { StudioConfig } from '../StudioConfig.js';
7
2
  import { Search } from './search/Search.js';
8
3
  import { Features, sortAvailableFeatures } from './features/Features.js';
@@ -10,62 +5,25 @@ import { Scoring } from './scoring/Scoring.js';
10
5
  import { Ranking } from './ranking/Ranking.js';
11
6
  import { findIndex } from './utils/indexUtils.js';
12
7
 
13
- /**
14
- * @typedef {Object} StudioOptions
15
- * @property {StudioConfig} [config] - Shared config instance (preferred). When provided, apiKey/commonUrl/servicesUrl are ignored.
16
- * @property {string} [apiKey] - API key for authentication (required when config is not provided)
17
- * @property {string} [commonUrl] - Base URL used for all services when provided
18
- * @property {{ searchService: string, storiesService: string, featuresService: string, scoringService: string, rankingService: string }} [servicesUrl] - Per-service URLs when commonUrl is not provided
19
- * @property {function(string): void} [log] - log function, defaults to console.log
20
- * @property {function(*): void} [show] - show function, defaults to console.log
21
- * @property {string} [origin='sdk'] - origin sent in backend call payloads
22
- */
23
-
24
- /**
25
- * Studio SDK client. Initialize with options; use search() to get a Search instance.
26
- * Pass a StudioConfig instance (config) for version-agnostic initialization, or apiKey/commonUrl/servicesUrl for legacy.
27
- */
28
8
  export class Studio {
29
- /**
30
- * @param {StudioOptions} options - Configuration options (config or apiKey+commonUrl/servicesUrl)
31
- */
32
9
  constructor(options) {
33
10
  if (!options || typeof options !== 'object') {
34
11
  throw new Error('Studio: options object is required');
35
12
  }
36
13
  const { config, apiKey, commonUrl, servicesUrl, log, show, origin } = options;
37
- this._config =
38
- config instanceof StudioConfig
39
- ? config
40
- : new StudioConfig({ commonUrl, servicesUrl, apiKey });
41
- this._log =
42
- typeof log === 'function' ? log : this._config.log;
43
- this._show =
44
- typeof show === 'function' ? show : this._config.show;
14
+ this._config = config instanceof StudioConfig ? config : new StudioConfig({ commonUrl, servicesUrl, apiKey });
15
+ this._log = typeof log === 'function' ? log : this._config.log;
16
+ this._show = typeof show === 'function' ? show : this._config.show;
45
17
  this._origin = typeof origin === 'string' && origin.trim() ? origin.trim() : 'sdk';
46
18
  this._forUser = null;
47
19
  this._candidates = [];
48
20
  }
49
-
50
- /* Return the SDK version */
51
21
  version() {
52
22
  return 'V1';
53
23
  }
54
-
55
- /**
56
- * Set the user for context (e.g. features). Puts { index, id } in this.forUser.
57
- * (Stored internally as this._forUser; read studio._forUser or call studio.forUser() to set.)
58
- * @param {number} index - User index
59
- * @param {string} userId - User id
60
- */
61
24
  forUser(index, userId) {
62
25
  this._forUser = { index, id: userId };
63
26
  }
64
-
65
- /**
66
- * Return a new Search instance configured with the studio's search service URL, API key, and origin.
67
- * @returns {Search}
68
- */
69
27
  search() {
70
28
  return new Search({
71
29
  url: this._config.searchService,
@@ -75,22 +33,11 @@ export class Studio {
75
33
  show: this._show,
76
34
  });
77
35
  }
78
-
79
- /**
80
- * Extend the candidates array with the given items.
81
- * @param {Array} array - Items to add to candidates
82
- */
83
36
  addCandidates(array) {
84
37
  this._candidates.push(...array);
85
38
  }
86
-
87
- /**
88
- * Return a new Features instance configured with the studio's features service URL and API key.
89
- * If candidates are set, they are passed to the constructor as items by default
90
- * @returns {Features}
91
- */
92
- features(version='v1') {
93
- let items = []
39
+ features(version = 'v1') {
40
+ let items = [];
94
41
  if (this._candidates && this._candidates.length > 0) {
95
42
  items = this._candidates.map((hit) => ({ index: hit._index, id: hit._id }));
96
43
  }
@@ -106,18 +53,13 @@ export class Studio {
106
53
  origin: this._origin,
107
54
  });
108
55
  }
109
-
110
- /**
111
- * Merge feature service response into candidates (hits). Mutates each candidate with _features and _scores.
112
- * @param {Object} featureServiceResponse
113
- */
114
56
  addFeatures(featuresResult) {
115
57
  const hits = this._candidates || [];
116
58
  const features = featuresResult.features;
117
59
  const scores = featuresResult.scores;
118
60
  if (!features && !scores) {
119
61
  this._log('No features or scores found in featuresResult');
120
- return ;
62
+ return;
121
63
  }
122
64
  let availableFeatures = {};
123
65
  let availableScores = {};
@@ -132,9 +74,7 @@ export class Studio {
132
74
  }
133
75
  if (hit._features) {
134
76
  for (const [key, value] of Object.entries(hit._features)) {
135
- if (typeof value === 'number' && !Number.isNaN(value)) {
136
- availableFeatures[key] = true;
137
- }
77
+ if (typeof value === 'number' && !Number.isNaN(value)) availableFeatures[key] = true;
138
78
  }
139
79
  }
140
80
  if (hit._scores) {
@@ -144,9 +84,7 @@ export class Studio {
144
84
  }
145
85
  if (hit._scores) {
146
86
  for (const [key, value] of Object.entries(hit._scores)) {
147
- if (typeof value === 'number' && !Number.isNaN(value)) {
148
- availableScores[key] = true;
149
- }
87
+ if (typeof value === 'number' && !Number.isNaN(value)) availableScores[key] = true;
150
88
  }
151
89
  }
152
90
  }
@@ -155,19 +93,11 @@ export class Studio {
155
93
  this._log(`Available features: ${availableFeatures}`);
156
94
  this._log(`Available scores: ${availableScores}`);
157
95
  }
158
-
159
-
160
- /**
161
- * Return a new Scoring instance configured with the studio's scoring service URL and API key.
162
- * If forUser is set, userId is passed; if candidates are set, item ids are extracted and passed.
163
- * @returns {Scoring}
164
- */
165
96
  scoring() {
166
97
  const userId = this._forUser?.id ?? null;
167
- const itemIds =
168
- Array.isArray(this._candidates) && this._candidates.length > 0
169
- ? this._candidates.map((c) => (c && (c._id != null) ? String(c._id) : null)).filter(Boolean)
170
- : [];
98
+ const itemIds = Array.isArray(this._candidates) && this._candidates.length > 0
99
+ ? this._candidates.map((c) => (c && (c._id != null) ? String(c._id) : null)).filter(Boolean)
100
+ : [];
171
101
  return new Scoring({
172
102
  url: this._config.scoringService,
173
103
  apiKey: this._config.apiKey,
@@ -178,39 +108,22 @@ export class Studio {
178
108
  origin: this._origin,
179
109
  });
180
110
  }
181
-
182
- /**
183
- * Add scores to candidates from a scoring/rerank result (ranking order).
184
- * @param {string} endpoint - Model endpoint used (key for hit._scores)
185
- * @param {Array} scoringResult - Array of item ids in ranking order
186
- */
187
111
  addScores(scoringResult, scoringKey) {
188
112
  const rankedItemIds = scoringResult;
189
- if (!this._candidates || !rankedItemIds || !Array.isArray(rankedItemIds)) {
190
- return;
191
- }
113
+ if (!this._candidates || !rankedItemIds || !Array.isArray(rankedItemIds)) return;
192
114
  const rankToScore = {};
193
115
  rankedItemIds.forEach((itemId, index) => {
194
- const score = 1.0 - (index / rankedItemIds.length);
195
- rankToScore[itemId] = score;
116
+ rankToScore[itemId] = 1.0 - (index / rankedItemIds.length);
196
117
  });
197
118
  for (const hit of this._candidates) {
198
119
  const hitId = hit._id;
199
120
  const hitScore = rankToScore[hitId];
200
121
  if (hitScore) {
201
- if (!hit._scores) {
202
- hit._scores = {};
203
- }
122
+ if (!hit._scores) hit._scores = {};
204
123
  hit._scores[scoringKey] = rankToScore[hitId];
205
124
  }
206
125
  }
207
126
  }
208
-
209
- /**
210
- * Return a new Ranking instance configured with the studio's ranking service URL and API key.
211
- * If candidates are set, they are passed to the constructor.
212
- * @returns {Ranking}
213
- */
214
127
  ranking() {
215
128
  return new Ranking({
216
129
  url: this._config.rankingService,
@@ -221,42 +134,20 @@ export class Studio {
221
134
  origin: this._origin,
222
135
  });
223
136
  }
224
-
225
- /**
226
- * Merge ranking result scores into candidates (hits). Mutates each candidate with _ranking_score.
227
- * @param {Object} rankingResult - Result from Ranking.execute() (result.result with items array of { item_id, score })
228
- */
229
137
  addRanking(rankingResult) {
230
138
  const rankedItems = rankingResult?.items;
231
- if (!this._candidates || !rankedItems || !Array.isArray(rankedItems)) {
232
- return;
233
- }
139
+ if (!this._candidates || !rankedItems || !Array.isArray(rankedItems)) return;
234
140
  const scoreByItemId = {};
235
- rankedItems.forEach(({ item_id, score }) => {
236
- scoreByItemId[item_id] = score;
237
- });
141
+ rankedItems.forEach(({ item_id, score }) => { scoreByItemId[item_id] = score; });
238
142
  for (const hit of this._candidates) {
239
143
  hit._ranking_score = scoreByItemId[hit._id];
240
144
  }
241
145
  }
242
-
243
- /**
244
- * Log a string. Uses custom implementation if provided to constructor, otherwise console.log.
245
- * @param {string} string
246
- */
247
146
  log(string) {
248
147
  this._log(string);
249
148
  }
250
-
251
- /**
252
- * Show results. Uses custom implementation if provided to constructor, otherwise console.log.
253
- * @param {*} results
254
- */
255
149
  show(results) {
256
- if (results === undefined) {
257
- results = this._candidates;
258
- }
150
+ if (results === undefined) results = this._candidates;
259
151
  this._show(results);
260
152
  }
261
-
262
153
  }
@@ -1,36 +1,11 @@
1
- /**
2
- * Features client for MBD Studio SDK.
3
- * Fluent API: methods return the instance for chaining.
4
- * Enrich items with ML features (embeddings, affinity, etc.) for a given user.
5
- */
6
-
7
- /**
8
- * @typedef {{ index: string, id: string }} UserRef
9
- * @typedef {{ index: string, id: string }} ItemRef
10
- */
11
-
12
1
  export const PREFERRED_FEATURE_COLUMNS = [
13
- 'found',
14
- 'original_rank',
15
- 'sem_sim_fuzzy',
16
- 'sem_sim_closest',
17
- 'usr_primary_labels',
18
- 'usr_secondary_labels',
19
- 'usr_primary_tags',
20
- 'usr_secondary_tags',
21
- 'user_affinity_avg',
22
- 'user_affinity_usdc',
23
- 'user_affinity_count',
2
+ 'found', 'original_rank', 'sem_sim_fuzzy', 'sem_sim_closest',
3
+ 'usr_primary_labels', 'usr_secondary_labels', 'usr_primary_tags', 'usr_secondary_tags',
4
+ 'user_affinity_avg', 'user_affinity_usdc', 'user_affinity_count',
24
5
  'cluster_1', 'cluster_2', 'cluster_3', 'cluster_4', 'cluster_5',
25
6
  'cluster_6', 'cluster_7', 'cluster_8', 'cluster_9', 'cluster_10',
26
7
  'sem_sim_cluster1', 'sem_sim_cluster2', 'sem_sim_cluster3', 'sem_sim_cluster4', 'sem_sim_cluster5',
27
8
  ];
28
-
29
- /**
30
- * Sort feature column names: preferred first, then regular, AI:, TAG:.
31
- * @param {string[]} available - Array of feature column names
32
- * @returns {string[]}
33
- */
34
9
  export function sortAvailableFeatures(available) {
35
10
  const preferred = PREFERRED_FEATURE_COLUMNS.filter((col) => available.includes(col));
36
11
  const nonPreferred = available.filter((col) => !PREFERRED_FEATURE_COLUMNS.includes(col));
@@ -39,41 +14,12 @@ export function sortAvailableFeatures(available) {
39
14
  const tagColumns = nonPreferred.filter((col) => col.startsWith('TAG:')).sort();
40
15
  return [...preferred, ...regular, ...aiColumns, ...tagColumns];
41
16
  }
42
-
43
17
  export class Features {
44
- /** @type {string} */
45
- _url;
46
-
47
- /** @type {string} */
48
- _apiKey;
49
-
50
- /** @type {string} */
51
18
  _version = 'v1';
52
-
53
- /** @type {UserRef | null} */
54
19
  _user = null;
55
-
56
- /** @type {ItemRef[]} */
57
20
  _items = [];
58
-
59
- /** @type {{ endpoint: string, payload: object } | null} */
60
21
  lastCall = null;
61
-
62
- /** @type {object | null} */
63
22
  lastResult = null;
64
-
65
- /**
66
- * @param {Object} options
67
- * @param {string} options.url - Features service base URL
68
- * @param {string} options.apiKey - API key for authentication
69
- * @param {string} [options.version='v1'] - API version
70
- * @param {{ index: string, id: string }[]} [options.items] - Items to enrich
71
- * @param {string} [options.userIndex] - User index for feature context
72
- * @param {string} [options.userId] - User id for feature context
73
- * @param {string} [options.origin='sdk'] - origin sent in backend call payloads
74
- * @param {function(string): void} [options.log] - Optional override for log()
75
- * @param {function(*): void} [options.show] - Optional override for show()
76
- */
77
23
  constructor(options) {
78
24
  if (!options || typeof options !== 'object') {
79
25
  throw new Error('Features: options object is required');
@@ -91,69 +37,27 @@ export class Features {
91
37
  this._origin = typeof origin === 'string' && origin.trim() ? origin.trim() : 'sdk';
92
38
  this._log = typeof log === 'function' ? log : console.log.bind(console);
93
39
  this._show = typeof show === 'function' ? show : console.log.bind(console);
94
- if (Array.isArray(items) && items.length > 0) {
95
- this._items = items;
96
- }
97
- if (userIndex != null && userId != null) {
98
- this._user = { index: userIndex, id: userId };
99
- }
40
+ if (Array.isArray(items) && items.length > 0) this._items = items;
41
+ if (userIndex != null && userId != null) this._user = { index: userIndex, id: userId };
100
42
  }
101
-
102
- /**
103
- * Return the endpoint path for the current version.
104
- * @returns {string}
105
- */
106
43
  getEndpoint() {
107
44
  return `/features/${this._version}`;
108
45
  }
109
-
110
- /**
111
- * Build the request payload (origin + user + items).
112
- * @returns {object}
113
- */
114
46
  getPayload() {
115
- return {
116
- origin: this._origin,
117
- user: this._user,
118
- items: this._items.map((item) => ({ ...item })),
119
- };
47
+ return { origin: this._origin, user: this._user, items: this._items.map((item) => ({ ...item })) };
120
48
  }
121
-
122
- /**
123
- * Set the API version (e.g. 'v1'). Default is 'v1'.
124
- * @param {string} v - Version string
125
- * @returns {this}
126
- */
127
49
  version(v) {
128
50
  this._version = v;
129
51
  return this;
130
52
  }
131
-
132
- /**
133
- * Set the items to enrich (array of { index, id }).
134
- * @param {ItemRef[]} items - Array of { index: string, id: string }
135
- * @returns {this}
136
- */
137
53
  items(items) {
138
54
  this._items = [...items];
139
55
  return this;
140
56
  }
141
-
142
- /**
143
- * Set the user for feature computation (index + userId).
144
- * @param {string} index - User index/table (e.g. 'polymarket-wallets')
145
- * @param {string} userId - User identifier
146
- * @returns {this}
147
- */
148
57
  user(index, userId) {
149
58
  this._user = { index, id: userId };
150
59
  return this;
151
60
  }
152
-
153
- /**
154
- * Execute the features request. Sets lastCall and lastResult on success; throws on error.
155
- * @returns {Promise<object>} The result object (result.items, result.user, result.feature_names, etc.)
156
- */
157
61
  async execute() {
158
62
  if (!this._user || !this._user.index || !this._user.id) {
159
63
  throw new Error('Features.execute: user must be set (call user(index, userId) first)');
@@ -168,10 +72,7 @@ export class Features {
168
72
  const startTime = performance.now();
169
73
  const response = await fetch(url, {
170
74
  method: 'POST',
171
- headers: {
172
- 'Content-Type': 'application/json',
173
- Authorization: `Bearer ${this._apiKey}`,
174
- },
75
+ headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${this._apiKey}` },
175
76
  body: JSON.stringify(payload),
176
77
  });
177
78
  const frontendTime = Math.round(performance.now() - startTime);
@@ -191,13 +92,8 @@ export class Features {
191
92
  result.took_frontend = frontendTime;
192
93
  this.lastCall = { endpoint, payload };
193
94
  this.lastResult = result;
194
-
195
95
  const res = result.result;
196
- if (!res) {
197
- throw new Error('Features.execute: result.result is undefined');
198
- }
199
-
200
- // Log infos (aligned with FeaturesInfoColumn.js)
96
+ if (!res) throw new Error('Features.execute: result.result is undefined');
201
97
  const infos = {
202
98
  took_frontend_ms: result.took_frontend,
203
99
  took_backend_ms: res.took_backend ?? 0,
@@ -227,22 +123,11 @@ export class Features {
227
123
  this._log(` took_clustering_ms: ${infos.took_clustering_ms}`);
228
124
  this._log(` hit_rate: ${infos.hit_rate_pct}%`);
229
125
  this._log(` item_embed_rate: ${infos.item_embed_rate_pct}%`);
230
-
231
126
  return res;
232
127
  }
233
-
234
- /**
235
- * Log a string.
236
- * @param {string} string
237
- */
238
128
  log(string) {
239
129
  this._log(string);
240
130
  }
241
-
242
- /**
243
- * Show results.
244
- * @param {*} results
245
- */
246
131
  show(results) {
247
132
  this._show(results);
248
133
  }