mbd-studio-sdk 3.8.0 → 4.1.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/StudioConfig.js +1 -1
- package/V1/Studio.js +13 -2
- package/V1/features/Features.js +2 -1
- package/V1/ranking/Ranking.js +2 -0
- package/V1/search/Search.js +36 -0
- package/V1/search/filters/ConsoleAccountFilter.js +2 -0
- package/V1/search/filters/GroupBoostFilter.js +2 -0
- package/V1/search/filters/TermsLookupFilter.js +2 -0
- package/V1/utils/indexUtils.js +1 -0
- package/index.d.ts +218 -0
- package/index.js +7 -0
- package/llms.txt +484 -0
- package/package.json +10 -3
package/StudioConfig.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const defaultBaseUrl = 'https://api.mbd.xyz/v3/studio';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
/** API config: apiKey required; commonUrl (single base) or servicesUrl (per-service) for endpoints. */
|
|
4
4
|
export class StudioConfig {
|
|
5
5
|
constructor(options) {
|
|
6
6
|
if (!options || typeof options !== 'object') {
|
package/V1/Studio.js
CHANGED
|
@@ -5,6 +5,7 @@ import { Scoring } from './scoring/Scoring.js';
|
|
|
5
5
|
import { Ranking } from './ranking/Ranking.js';
|
|
6
6
|
import { findIndex } from './utils/indexUtils.js';
|
|
7
7
|
|
|
8
|
+
/** Main client: search, features, scoring, ranking. Use addCandidates() then addFeatures/addScores/addRanking to enrich. */
|
|
8
9
|
export class Studio {
|
|
9
10
|
constructor(options) {
|
|
10
11
|
if (!options || typeof options !== 'object') {
|
|
@@ -56,12 +57,14 @@ export class Studio {
|
|
|
56
57
|
origin: this._origin,
|
|
57
58
|
});
|
|
58
59
|
}
|
|
60
|
+
/** Merges featuresResult (features, scores, info) into hits. Keys use canonical index names (findIndex). */
|
|
59
61
|
addFeatures(featuresResult) {
|
|
60
62
|
const hits = this._candidates || [];
|
|
61
63
|
const features = featuresResult.features;
|
|
62
64
|
const scores = featuresResult.scores;
|
|
63
|
-
|
|
64
|
-
|
|
65
|
+
const info = featuresResult.info;
|
|
66
|
+
if (!features && !scores && !info) {
|
|
67
|
+
this._log('No features, scores, or info found in featuresResult');
|
|
65
68
|
return;
|
|
66
69
|
}
|
|
67
70
|
let availableFeatures = {};
|
|
@@ -70,6 +73,7 @@ export class Studio {
|
|
|
70
73
|
const hitIndex = findIndex(hit._index);
|
|
71
74
|
const hitFeatures = features?.[hitIndex]?.[hit._id];
|
|
72
75
|
const hitScores = scores?.[hitIndex]?.[hit._id];
|
|
76
|
+
const hitInfo = info?.[hitIndex]?.[hit._id];
|
|
73
77
|
if (hit._features) {
|
|
74
78
|
hit._features = { ...hit._features, ...hitFeatures };
|
|
75
79
|
} else {
|
|
@@ -90,6 +94,11 @@ export class Studio {
|
|
|
90
94
|
if (typeof value === 'number' && !Number.isNaN(value)) availableScores[key] = true;
|
|
91
95
|
}
|
|
92
96
|
}
|
|
97
|
+
if (hit._info) {
|
|
98
|
+
hit._info = { ...hit._info, ...hitInfo };
|
|
99
|
+
} else {
|
|
100
|
+
hit._info = hitInfo;
|
|
101
|
+
}
|
|
93
102
|
}
|
|
94
103
|
availableFeatures = sortAvailableFeatures(Object.keys(availableFeatures));
|
|
95
104
|
availableScores = sortAvailableFeatures(Object.keys(availableScores));
|
|
@@ -111,6 +120,7 @@ export class Studio {
|
|
|
111
120
|
origin: this._origin,
|
|
112
121
|
});
|
|
113
122
|
}
|
|
123
|
+
/** Maps ranked item IDs to scores (1.0 for rank 0, decreasing by rank). Merges into hit._scores[scoringKey]. */
|
|
114
124
|
addScores(scoringResult, scoringKey) {
|
|
115
125
|
const rankedItemIds = scoringResult;
|
|
116
126
|
if (!this._candidates || !rankedItemIds || !Array.isArray(rankedItemIds)) return;
|
|
@@ -137,6 +147,7 @@ export class Studio {
|
|
|
137
147
|
origin: this._origin,
|
|
138
148
|
});
|
|
139
149
|
}
|
|
150
|
+
/** Sets hit._ranking_score from ranking items, then sorts candidates by score descending. */
|
|
140
151
|
addRanking(rankingResult) {
|
|
141
152
|
const rankedItems = rankingResult?.items;
|
|
142
153
|
if (!this._candidates || !rankedItems || !Array.isArray(rankedItems)) return;
|
package/V1/features/Features.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/** Feature columns shown first when listing available features (relevance, affinity, clusters, semantic). */
|
|
1
2
|
export const PREFERRED_FEATURE_COLUMNS = [
|
|
2
3
|
'found', 'original_rank', 'sem_sim_fuzzy', 'sem_sim_closest',
|
|
3
4
|
'usr_primary_labels', 'usr_secondary_labels', 'usr_primary_tags', 'usr_secondary_tags',
|
|
@@ -110,7 +111,7 @@ export class Features {
|
|
|
110
111
|
item_embed_rate_pct: ((res.item_embed_rate ?? 0) * 100).toFixed(1),
|
|
111
112
|
};
|
|
112
113
|
this._log('Features result:');
|
|
113
|
-
this._log(`
|
|
114
|
+
this._log(` took_sdk_ms: ${infos.took_sdk_ms}`);
|
|
114
115
|
this._log(` took_backend_ms: ${infos.took_backend_ms}`);
|
|
115
116
|
this._log(` took_dynamo_user_ms: ${infos.took_dynamo_user_ms}`);
|
|
116
117
|
this._log(` took_dynamo_items_ms: ${infos.took_dynamo_items_ms}`);
|
package/V1/ranking/Ranking.js
CHANGED
|
@@ -30,6 +30,7 @@ export class Ranking {
|
|
|
30
30
|
getEndpoint() {
|
|
31
31
|
return '/ranking/feed';
|
|
32
32
|
}
|
|
33
|
+
/** Collects fields referenced by sort/diversity/limits; sets needEmbedding for semantic diversity. */
|
|
33
34
|
_getUsefulFieldsAndNeedEmbedding() {
|
|
34
35
|
const useful = new Set();
|
|
35
36
|
let needEmbedding = false;
|
|
@@ -53,6 +54,7 @@ export class Ranking {
|
|
|
53
54
|
}
|
|
54
55
|
return { usefulFields: useful, needEmbedding };
|
|
55
56
|
}
|
|
57
|
+
/** Builds items from hit._features, hit._scores; adds embed (item_sem_embed2 or text_vector) if semantic diversity. */
|
|
56
58
|
getPayload() {
|
|
57
59
|
const { usefulFields, needEmbedding } = this._getUsefulFieldsAndNeedEmbedding();
|
|
58
60
|
const hits = this._candidates || [];
|
package/V1/search/Search.js
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
IsNullFilter, NotNullFilter, CustomFilter, GroupBoostFilter, TermsLookupFilter, ConsoleAccountFilter,
|
|
4
4
|
} from './filters/index.js';
|
|
5
5
|
|
|
6
|
+
/** Search builder: index(), include()/exclude()/boost(), filters, execute(). Call include/exclude/boost before adding filters. */
|
|
6
7
|
export class Search {
|
|
7
8
|
_index = null;
|
|
8
9
|
_es_query = null;
|
|
@@ -16,6 +17,7 @@ export class Search {
|
|
|
16
17
|
_include = [];
|
|
17
18
|
_exclude = [];
|
|
18
19
|
_boost = [];
|
|
20
|
+
/** Set by include()/exclude()/boost(); filters are pushed to whichever array is active. */
|
|
19
21
|
_active_array = null;
|
|
20
22
|
lastCall = null;
|
|
21
23
|
lastResult = null;
|
|
@@ -36,6 +38,7 @@ export class Search {
|
|
|
36
38
|
this._log = typeof log === 'function' ? log : console.log.bind(console);
|
|
37
39
|
this._show = typeof show === 'function' ? show : console.log.bind(console);
|
|
38
40
|
}
|
|
41
|
+
/** Endpoint selection: es_query > semantic (text/vector) > boost > filter_and_sort. */
|
|
39
42
|
getEndpoint() {
|
|
40
43
|
if (this._es_query != null) return '/search/es_query';
|
|
41
44
|
const hasTextOrVector = (typeof this._text === 'string' && this._text.length > 0) || (Array.isArray(this._vector) && this._vector.length > 0);
|
|
@@ -76,7 +79,40 @@ export class Search {
|
|
|
76
79
|
if (this._es_query != null && (typeof this._es_query !== 'object' || Array.isArray(this._es_query))) {
|
|
77
80
|
throw new Error('Search.execute: esQuery() must be called with a plain object (e.g. { query: {...}, sort: [...] })');
|
|
78
81
|
}
|
|
82
|
+
const hasOnlyIds = this._only_ids === true;
|
|
83
|
+
const hasSelectFields = Array.isArray(this._select_fields) && this._select_fields.length > 0;
|
|
84
|
+
const hasIncludeVector = this._include_vector === true;
|
|
85
|
+
const exclusiveCount = (hasOnlyIds ? 1 : 0) + (hasSelectFields ? 1 : 0) + (hasIncludeVector ? 1 : 0);
|
|
86
|
+
if (exclusiveCount > 1) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
'Search: onlyIds, selectFields, and includeVectors are mutually exclusive; only one may be set at a time.'
|
|
89
|
+
);
|
|
90
|
+
}
|
|
79
91
|
const endpoint = this.getEndpoint();
|
|
92
|
+
if (endpoint === '/search/es_query') {
|
|
93
|
+
if (this._include.length > 0 || this._exclude.length > 0 || this._boost.length > 0) {
|
|
94
|
+
throw new Error(
|
|
95
|
+
'Search: esQuery() does not support include(), exclude(), or boost() filters. Add filters directly in your Elasticsearch query.'
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
} else if (endpoint === '/search/semantic') {
|
|
99
|
+
if (this._include.length > 0 || this._exclude.length > 0 || this._boost.length > 0) {
|
|
100
|
+
throw new Error(
|
|
101
|
+
'Search: semantic search does not support include(), exclude(), or boost() filters. Use filter_and_sort or boost endpoints for filtering.'
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
if (this._sort_by != null) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
'Search: semantic search does not support sortBy(). Results are ranked by similarity.'
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
} else if (endpoint === '/search/boost') {
|
|
110
|
+
if (this._sort_by != null) {
|
|
111
|
+
throw new Error(
|
|
112
|
+
'Search: boost endpoint does not support sortBy(). Use filter_and_sort for sorting.'
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
80
116
|
const payload = this.getPayload();
|
|
81
117
|
const url = `${this._url}${endpoint}`;
|
|
82
118
|
this.log(`Sending request to ${url}`);
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { Filter } from './Filter.js';
|
|
2
|
+
|
|
3
|
+
/** Filters by console account data. path: dot-notation field in the account doc. */
|
|
2
4
|
export class ConsoleAccountFilter extends Filter {
|
|
3
5
|
constructor(field, value, path, boost = null) {
|
|
4
6
|
super('console_account', field, boost);
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { Filter } from './Filter.js';
|
|
2
|
+
|
|
3
|
+
/** Boosts items by group membership. lookup_index/field/value identify the group; min_boost/max_boost/n control boost range. */
|
|
2
4
|
export class GroupBoostFilter extends Filter {
|
|
3
5
|
constructor(lookup_index, field, value, group, min_boost = null, max_boost = null, n = null) {
|
|
4
6
|
super('group_boost', field, null);
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { Filter } from './Filter.js';
|
|
2
|
+
|
|
3
|
+
/** Filters by terms fetched from another index. path: dot-notation field in lookup doc (e.g. "followers.ids"). */
|
|
2
4
|
export class TermsLookupFilter extends Filter {
|
|
3
5
|
constructor(lookup_index, field, value, path, boost = null) {
|
|
4
6
|
super('terms_lookup', field, boost);
|
package/V1/utils/indexUtils.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/** Maps index names to canonical base (e.g. farcaster-items-v2 → farcaster-items). Used for feature/score keys. */
|
|
1
2
|
export function findIndex(index) {
|
|
2
3
|
const indexOptions = ['farcaster-items', 'zora-coins', 'polymarket-items', 'polymarket-wallets'];
|
|
3
4
|
for (const option of indexOptions) {
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MBD Studio SDK – search, features, scoring, and ranking for personalized feeds.
|
|
3
|
+
* @example
|
|
4
|
+
* const config = new StudioConfig({ apiKey, commonUrl });
|
|
5
|
+
* const studio = new StudioV1({ config });
|
|
6
|
+
* const hits = await studio.search().index('farcaster-items').include().term('type', 'cast').execute();
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// --- StudioConfig ---
|
|
10
|
+
|
|
11
|
+
export interface StudioConfigOptions {
|
|
12
|
+
apiKey: string;
|
|
13
|
+
commonUrl?: string;
|
|
14
|
+
servicesUrl?: {
|
|
15
|
+
searchService: string;
|
|
16
|
+
storiesService: string;
|
|
17
|
+
featuresService: string;
|
|
18
|
+
scoringService: string;
|
|
19
|
+
rankingService: string;
|
|
20
|
+
};
|
|
21
|
+
log?: (msg: string) => void;
|
|
22
|
+
show?: (results?: unknown) => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class StudioConfig {
|
|
26
|
+
constructor(options: StudioConfigOptions);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// --- Filter (abstract base, used by Search.filter()) ---
|
|
30
|
+
|
|
31
|
+
export class Filter {
|
|
32
|
+
constructor(filterType: string, field: string, boost?: number | null);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// --- Search ---
|
|
36
|
+
|
|
37
|
+
export interface SearchHit {
|
|
38
|
+
_index?: string;
|
|
39
|
+
_id?: string;
|
|
40
|
+
_source?: Record<string, unknown>;
|
|
41
|
+
_features?: Record<string, number>;
|
|
42
|
+
_scores?: Record<string, number>;
|
|
43
|
+
_info?: Record<string, unknown>;
|
|
44
|
+
_ranking_score?: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface SearchResult {
|
|
48
|
+
total_hits?: number;
|
|
49
|
+
hits?: SearchHit[];
|
|
50
|
+
took_es?: number;
|
|
51
|
+
took_backend?: number;
|
|
52
|
+
max_score?: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface FrequentValuesResult {
|
|
56
|
+
[key: string]: unknown;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export class Search {
|
|
60
|
+
lastCall: { endpoint: string; payload: unknown } | null;
|
|
61
|
+
lastResult: unknown;
|
|
62
|
+
|
|
63
|
+
index(selected_index: string): this;
|
|
64
|
+
size(size: number): this;
|
|
65
|
+
onlyIds(value?: boolean): this;
|
|
66
|
+
includeVectors(value?: boolean): this;
|
|
67
|
+
selectFields(fields: string[] | null): this;
|
|
68
|
+
text(text: string): this;
|
|
69
|
+
vector(vector: number[]): this;
|
|
70
|
+
esQuery(rawQuery: Record<string, unknown>): this;
|
|
71
|
+
sortBy(field: string, direction?: 'asc' | 'desc', field2?: string, direction2?: 'asc' | 'desc'): this;
|
|
72
|
+
include(): this;
|
|
73
|
+
exclude(): this;
|
|
74
|
+
boost(): this;
|
|
75
|
+
filter(filterInstance: Filter): this;
|
|
76
|
+
term(field: string, value: string | number | boolean, boost?: number | null): this;
|
|
77
|
+
terms(field: string, values: (string | number | boolean)[], boost?: number | null): this;
|
|
78
|
+
numeric(field: string, operator: string, value: number, boost?: number | null): this;
|
|
79
|
+
date(field: string, dateFrom?: string | null, dateTo?: string | null, boost?: number | null): this;
|
|
80
|
+
geo(field: string, value: unknown, boost?: number | null): this;
|
|
81
|
+
match(field: string, value: string, boost?: number | null): this;
|
|
82
|
+
isNull(field: string, boost?: number | null): this;
|
|
83
|
+
notNull(field: string, boost?: number | null): this;
|
|
84
|
+
custom(field: string, value: unknown, boost?: number | null): this;
|
|
85
|
+
groupBoost(
|
|
86
|
+
lookup_index: string,
|
|
87
|
+
field: string,
|
|
88
|
+
value: unknown,
|
|
89
|
+
group: string,
|
|
90
|
+
min_boost?: number | null,
|
|
91
|
+
max_boost?: number | null,
|
|
92
|
+
n?: number | null
|
|
93
|
+
): this;
|
|
94
|
+
termsLookup(
|
|
95
|
+
lookup_index: string,
|
|
96
|
+
field: string,
|
|
97
|
+
value: unknown,
|
|
98
|
+
path: string,
|
|
99
|
+
boost?: number | null
|
|
100
|
+
): this;
|
|
101
|
+
consoleAccount(field: string, value: unknown, path: string, boost?: number | null): this;
|
|
102
|
+
execute(): Promise<SearchHit[]>;
|
|
103
|
+
frequentValues(field: string, size?: number): Promise<FrequentValuesResult>;
|
|
104
|
+
lookup(docId: string): Promise<unknown>;
|
|
105
|
+
log(string: string): void;
|
|
106
|
+
show(results?: unknown): void;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// --- Features ---
|
|
110
|
+
|
|
111
|
+
export interface FeaturesResult {
|
|
112
|
+
features?: Record<string, Record<string, Record<string, number>>>;
|
|
113
|
+
scores?: Record<string, Record<string, Record<string, number>>>;
|
|
114
|
+
info?: Record<string, Record<string, Record<string, unknown>>>;
|
|
115
|
+
took_backend?: number;
|
|
116
|
+
hit_rate?: number;
|
|
117
|
+
item_embed_rate?: number;
|
|
118
|
+
[key: string]: unknown;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export class Features {
|
|
122
|
+
lastCall: { endpoint: string; payload: unknown } | null;
|
|
123
|
+
lastResult: unknown;
|
|
124
|
+
|
|
125
|
+
version(v: string): this;
|
|
126
|
+
items(items: Array<{ index: string; id: string }>): this;
|
|
127
|
+
user(index: string, userId: string): this;
|
|
128
|
+
execute(): Promise<FeaturesResult>;
|
|
129
|
+
log(string: string): void;
|
|
130
|
+
show(results?: unknown): void;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// --- Scoring ---
|
|
134
|
+
|
|
135
|
+
export class Scoring {
|
|
136
|
+
lastCall: { endpoint: string; payload: unknown } | null;
|
|
137
|
+
lastResult: unknown;
|
|
138
|
+
|
|
139
|
+
model(endpoint: string): this;
|
|
140
|
+
userId(userId: string): this;
|
|
141
|
+
itemIds(itemIds: string[]): this;
|
|
142
|
+
execute(): Promise<string[]>;
|
|
143
|
+
log(string: string): void;
|
|
144
|
+
show(results?: unknown): void;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// --- Ranking ---
|
|
148
|
+
|
|
149
|
+
export interface RankingItem {
|
|
150
|
+
item_id: string;
|
|
151
|
+
score: number;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export interface RankingResult {
|
|
155
|
+
items: RankingItem[];
|
|
156
|
+
[key: string]: unknown;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export class Ranking {
|
|
160
|
+
lastCall: { endpoint: string; payload: unknown } | null;
|
|
161
|
+
lastResult: unknown;
|
|
162
|
+
|
|
163
|
+
sortingMethod(x: 'sort' | 'linear' | 'mix'): this;
|
|
164
|
+
sortBy(
|
|
165
|
+
field: string,
|
|
166
|
+
direction?: 'asc' | 'desc',
|
|
167
|
+
field2?: string,
|
|
168
|
+
direction2?: 'asc' | 'desc'
|
|
169
|
+
): this;
|
|
170
|
+
weight(field: string, w: number): this;
|
|
171
|
+
mix(field: string, direction: 'asc' | 'desc', percentage: number): this;
|
|
172
|
+
diversity(method: 'fields' | 'semantic'): this;
|
|
173
|
+
fields(arrayOrItem: string | string[]): this;
|
|
174
|
+
horizon(n: number): this;
|
|
175
|
+
lambda(value: number): this;
|
|
176
|
+
limitByField(): this;
|
|
177
|
+
every(n: number): this;
|
|
178
|
+
limit(field: string, max: number): this;
|
|
179
|
+
candidates(candidates: SearchHit[]): this;
|
|
180
|
+
execute(): Promise<RankingResult>;
|
|
181
|
+
log(string: string): void;
|
|
182
|
+
show(results?: unknown): void;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// --- Studio (main client) ---
|
|
186
|
+
|
|
187
|
+
export interface StudioOptions {
|
|
188
|
+
config?: StudioConfig;
|
|
189
|
+
apiKey?: string;
|
|
190
|
+
commonUrl?: string;
|
|
191
|
+
servicesUrl?: StudioConfigOptions['servicesUrl'];
|
|
192
|
+
log?: (msg: string) => void;
|
|
193
|
+
show?: (results?: unknown) => void;
|
|
194
|
+
origin?: string;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export class Studio {
|
|
198
|
+
constructor(options: StudioOptions);
|
|
199
|
+
|
|
200
|
+
version(): string;
|
|
201
|
+
forUser(index: string, userId: string): void;
|
|
202
|
+
search(): Search;
|
|
203
|
+
frequentValues(index: string, field: string, size?: number): Promise<FrequentValuesResult>;
|
|
204
|
+
addCandidates(array: SearchHit[]): void;
|
|
205
|
+
features(version?: string): Features;
|
|
206
|
+
addFeatures(featuresResult: FeaturesResult): void;
|
|
207
|
+
scoring(): Scoring;
|
|
208
|
+
addScores(scoringResult: string[], scoringKey: string): void;
|
|
209
|
+
ranking(): Ranking;
|
|
210
|
+
addRanking(rankingResult: { items?: RankingItem[] }): void;
|
|
211
|
+
log(string: string): void;
|
|
212
|
+
show(results?: SearchHit[]): void;
|
|
213
|
+
getFeed(): SearchHit[];
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// --- Main export ---
|
|
217
|
+
|
|
218
|
+
export const StudioV1: typeof Studio;
|
package/index.js
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MBD Studio SDK – search, features, scoring, and ranking for personalized feeds.
|
|
3
|
+
* @example
|
|
4
|
+
* const config = new StudioConfig({ apiKey, commonUrl });
|
|
5
|
+
* const studio = new StudioV1({ config });
|
|
6
|
+
* const hits = await studio.search().index('farcaster-items').include().term('type', 'cast').execute();
|
|
7
|
+
*/
|
|
1
8
|
import * as V1 from './V1/index.js';
|
|
2
9
|
export { StudioConfig } from './StudioConfig.js';
|
|
3
10
|
export const StudioV1 = V1.Studio;
|
package/llms.txt
ADDED
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
# MBD Studio SDK – AI Agent Reference
|
|
2
|
+
|
|
3
|
+
## Introduction
|
|
4
|
+
|
|
5
|
+
**What it is:** SDK for MBD Studio backend services — search, features, scoring, and ranking for personalized feeds (Polymarket, Farcaster, Zora, etc.).
|
|
6
|
+
|
|
7
|
+
**Entry points:**
|
|
8
|
+
- `import { StudioConfig, StudioV1 } from 'mbd-studio-sdk'`
|
|
9
|
+
- Main client: `new StudioV1({ config })` where `config = new StudioConfig({ apiKey })`
|
|
10
|
+
|
|
11
|
+
**Typical flow:**
|
|
12
|
+
1. `forUser(index, userId)` — set user for personalization
|
|
13
|
+
2. `search().index(name).include()/exclude()/boost().term()|numeric()|...execute()` — get candidates
|
|
14
|
+
3. `addCandidates(hits)` — attach to context
|
|
15
|
+
4. `features("v1").execute()` → `addFeatures(result)` — enrich with signals
|
|
16
|
+
5. `scoring().model("/scoring/...").execute()` → `addScores(result, key)` — ML reranking
|
|
17
|
+
6. `ranking().sortingMethod().mix()|sortBy().diversity().execute()` → `addRanking(result)` — final ranked list
|
|
18
|
+
7. `getFeed()` — get sorted candidates
|
|
19
|
+
|
|
20
|
+
**Search endpoint selection (auto):** es_query > semantic (text/vector) > boost > filter_and_sort. Call `include()`/`exclude()`/`boost()` before adding filters.
|
|
21
|
+
|
|
22
|
+
**Debug tips:** All services have `lastCall` and `lastResult`. Use `config.log`/`config.show` or pass `log`/`show` in options. Features require `forUser()` and `addCandidates()` first. Ranking needs `addFeatures()` or `addScores()` for sort fields. Semantic diversity needs `includeVectors(true)` in search.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## index.js
|
|
27
|
+
import * as V1 from './V1/index.js';
|
|
28
|
+
export { StudioConfig } from './StudioConfig.js';
|
|
29
|
+
export const StudioV1 = V1.Studio;
|
|
30
|
+
|
|
31
|
+
## V1/index.js
|
|
32
|
+
export { Studio } from './Studio.js';
|
|
33
|
+
|
|
34
|
+
## StudioConfig.js
|
|
35
|
+
const defaultBaseUrl = 'https://api.mbd.xyz/v3/studio';
|
|
36
|
+
export class StudioConfig {
|
|
37
|
+
constructor(options) {
|
|
38
|
+
if (!options || typeof options !== 'object') throw new Error('StudioConfig: options object is required');
|
|
39
|
+
const { apiKey, commonUrl, servicesUrl, log, show } = options;
|
|
40
|
+
if (typeof apiKey !== 'string' || !apiKey.trim()) throw new Error('StudioConfig: apiKey is required and must be a non-empty string');
|
|
41
|
+
const hasCommonUrl = typeof commonUrl === 'string' && commonUrl.trim().length > 0;
|
|
42
|
+
if (hasCommonUrl) {
|
|
43
|
+
const url = commonUrl.trim().replace(/\/$/, '');
|
|
44
|
+
this.searchService = this.storiesService = this.featuresService = this.scoringService = this.rankingService = url;
|
|
45
|
+
} else if (servicesUrl && typeof servicesUrl === 'object') {
|
|
46
|
+
const { searchService, storiesService, featuresService, scoringService, rankingService } = servicesUrl;
|
|
47
|
+
const services = { searchService, storiesService, featuresService, scoringService, rankingService };
|
|
48
|
+
const missing = Object.entries(services).filter(([, v]) => typeof v !== 'string' || !v.trim()).map(([k]) => k);
|
|
49
|
+
if (missing.length > 0) throw new Error(`StudioConfig: when using servicesUrl, all service URLs are required. Missing: ${missing.join(', ')}`);
|
|
50
|
+
this.searchService = searchService.trim().replace(/\/$/, '');
|
|
51
|
+
this.storiesService = storiesService.trim().replace(/\/$/, '');
|
|
52
|
+
this.featuresService = featuresService.trim().replace(/\/$/, '');
|
|
53
|
+
this.scoringService = scoringService.trim().replace(/\/$/, '');
|
|
54
|
+
this.rankingService = rankingService.trim().replace(/\/$/, '');
|
|
55
|
+
} else {
|
|
56
|
+
this.searchService = this.storiesService = this.featuresService = this.scoringService = this.rankingService = defaultBaseUrl;
|
|
57
|
+
}
|
|
58
|
+
this.apiKey = apiKey.trim();
|
|
59
|
+
this.log = typeof log === 'function' ? log : console.log.bind(console);
|
|
60
|
+
this.show = typeof show === 'function' ? show : console.log.bind(console);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
## V1/Studio.js
|
|
65
|
+
import { StudioConfig } from '../StudioConfig.js';
|
|
66
|
+
import { Search } from './search/Search.js';
|
|
67
|
+
import { Features, sortAvailableFeatures } from './features/Features.js';
|
|
68
|
+
import { Scoring } from './scoring/Scoring.js';
|
|
69
|
+
import { Ranking } from './ranking/Ranking.js';
|
|
70
|
+
import { findIndex } from './utils/indexUtils.js';
|
|
71
|
+
|
|
72
|
+
export class Studio {
|
|
73
|
+
constructor(options) {
|
|
74
|
+
if (!options || typeof options !== 'object') throw new Error('Studio: options object is required');
|
|
75
|
+
const { config, apiKey, commonUrl, servicesUrl, log, show, origin } = options;
|
|
76
|
+
this._config = config instanceof StudioConfig ? config : new StudioConfig({ commonUrl, servicesUrl, apiKey });
|
|
77
|
+
this._log = typeof log === 'function' ? log : this._config.log;
|
|
78
|
+
this._show = typeof show === 'function' ? show : this._config.show;
|
|
79
|
+
this._origin = typeof origin === 'string' && origin.trim() ? origin.trim() : 'sdk';
|
|
80
|
+
this._forUser = null;
|
|
81
|
+
this._candidates = [];
|
|
82
|
+
}
|
|
83
|
+
version() { return 'V1'; }
|
|
84
|
+
forUser(index, userId) { this._forUser = { index, id: userId }; }
|
|
85
|
+
search() { return new Search({ url: this._config.searchService, apiKey: this._config.apiKey, origin: this._origin, log: this._log, show: this._show }); }
|
|
86
|
+
async frequentValues(index, field, size = 25) { return this.search().index(index).frequentValues(field, size); }
|
|
87
|
+
addCandidates(array) { this._candidates.push(...array); }
|
|
88
|
+
features(version = 'v1') {
|
|
89
|
+
const items = (this._candidates && this._candidates.length > 0) ? this._candidates.map((hit) => ({ index: hit._index, id: hit._id })) : [];
|
|
90
|
+
return new Features({ url: this._config.featuresService, apiKey: this._config.apiKey, log: this._log, show: this._show, version, items, userIndex: this._forUser?.index, userId: this._forUser?.id, origin: this._origin });
|
|
91
|
+
}
|
|
92
|
+
addFeatures(featuresResult) {
|
|
93
|
+
const hits = this._candidates || [];
|
|
94
|
+
const { features, scores, info } = featuresResult;
|
|
95
|
+
if (!features && !scores && !info) { this._log('No features, scores, or info found'); return; }
|
|
96
|
+
let availableFeatures = {}, availableScores = {};
|
|
97
|
+
for (const hit of hits) {
|
|
98
|
+
const hitIndex = findIndex(hit._index);
|
|
99
|
+
const hitFeatures = features?.[hitIndex]?.[hit._id], hitScores = scores?.[hitIndex]?.[hit._id], hitInfo = info?.[hitIndex]?.[hit._id];
|
|
100
|
+
hit._features = hit._features ? { ...hit._features, ...hitFeatures } : hitFeatures;
|
|
101
|
+
hit._scores = hit._scores ? { ...hit._scores, ...hitScores } : hitScores;
|
|
102
|
+
hit._info = hit._info ? { ...hit._info, ...hitInfo } : hitInfo;
|
|
103
|
+
if (hit._features) for (const [k, v] of Object.entries(hit._features)) if (typeof v === 'number' && !Number.isNaN(v)) availableFeatures[k] = true;
|
|
104
|
+
if (hit._scores) for (const [k, v] of Object.entries(hit._scores)) if (typeof v === 'number' && !Number.isNaN(v)) availableScores[k] = true;
|
|
105
|
+
}
|
|
106
|
+
this._log(`Available features: ${sortAvailableFeatures(Object.keys(availableFeatures))}`);
|
|
107
|
+
this._log(`Available scores: ${sortAvailableFeatures(Object.keys(availableScores))}`);
|
|
108
|
+
}
|
|
109
|
+
scoring() {
|
|
110
|
+
const userId = this._forUser?.id ?? null;
|
|
111
|
+
const itemIds = Array.isArray(this._candidates) && this._candidates.length > 0 ? this._candidates.map((c) => c && c._id != null ? String(c._id) : null).filter(Boolean) : [];
|
|
112
|
+
return new Scoring({ url: this._config.scoringService, apiKey: this._config.apiKey, log: this._log, show: this._show, userId, itemIds, origin: this._origin });
|
|
113
|
+
}
|
|
114
|
+
addScores(scoringResult, scoringKey) {
|
|
115
|
+
const rankedItemIds = scoringResult;
|
|
116
|
+
if (!this._candidates || !rankedItemIds || !Array.isArray(rankedItemIds)) return;
|
|
117
|
+
const rankToScore = {};
|
|
118
|
+
rankedItemIds.forEach((itemId, index) => { rankToScore[itemId] = 1.0 - (index / rankedItemIds.length); });
|
|
119
|
+
for (const hit of this._candidates) {
|
|
120
|
+
const hitScore = rankToScore[hit._id];
|
|
121
|
+
if (hitScore) { if (!hit._scores) hit._scores = {}; hit._scores[scoringKey] = hitScore; }
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
ranking() { return new Ranking({ url: this._config.rankingService, apiKey: this._config.apiKey, log: this._log, show: this._show, candidates: this._candidates, origin: this._origin }); }
|
|
125
|
+
addRanking(rankingResult) {
|
|
126
|
+
const rankedItems = rankingResult?.items;
|
|
127
|
+
if (!this._candidates || !rankedItems || !Array.isArray(rankedItems)) return;
|
|
128
|
+
const scoreByItemId = {};
|
|
129
|
+
rankedItems.forEach(({ item_id, score }) => { scoreByItemId[item_id] = score; });
|
|
130
|
+
for (const hit of this._candidates) hit._ranking_score = scoreByItemId[hit._id];
|
|
131
|
+
this._candidates.sort((a, b) => (b._ranking_score ?? -Infinity) - (a._ranking_score ?? -Infinity));
|
|
132
|
+
}
|
|
133
|
+
log(string) { this._log(string); }
|
|
134
|
+
show(results) { this._show(results === undefined ? this._candidates : results); }
|
|
135
|
+
getFeed() { return this._candidates; }
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
## index.d.ts (types summary)
|
|
139
|
+
// StudioConfig: apiKey, commonUrl?, servicesUrl?, log?, show?
|
|
140
|
+
// SearchHit: _index, _id, _source, _features, _scores, _info, _ranking_score
|
|
141
|
+
// Search: index, size, onlyIds, includeVectors, selectFields, text, vector, esQuery, sortBy, include/exclude/boost, term/terms/numeric/date/geo/match/isNull/notNull/custom, groupBoost, termsLookup, consoleAccount, execute, frequentValues, lookup
|
|
142
|
+
// Features: version, items, user, execute
|
|
143
|
+
// Scoring: model, userId, itemIds, execute
|
|
144
|
+
// Ranking: sortingMethod(sort|linear|mix), sortBy, weight, mix, diversity(fields|semantic), fields, horizon, lambda, limitByField, every, limit, candidates, execute
|
|
145
|
+
|
|
146
|
+
## V1/search/Search.js
|
|
147
|
+
import { Filter, TermFilter, TermsFilter, NumericFilter, MatchFilter, GeoFilter, DateFilter, IsNullFilter, NotNullFilter, CustomFilter, GroupBoostFilter, TermsLookupFilter, ConsoleAccountFilter } from './filters/index.js';
|
|
148
|
+
|
|
149
|
+
export class Search {
|
|
150
|
+
_index = null; _es_query = null; _size = 100; _only_ids = false; _include_vector = false; _select_fields = null; _text = null; _vector = null; _sort_by = null;
|
|
151
|
+
_include = []; _exclude = []; _boost = []; _active_array = null; lastCall = null; lastResult = null;
|
|
152
|
+
constructor(options) {
|
|
153
|
+
if (!options || typeof options !== 'object') throw new Error('Search: options object is required');
|
|
154
|
+
const { url, apiKey, origin = 'sdk', log, show } = options;
|
|
155
|
+
if (typeof url !== 'string' || !url.trim()) throw new Error('Search: options.url is required');
|
|
156
|
+
if (typeof apiKey !== 'string' || !apiKey.trim()) throw new Error('Search: options.apiKey is required');
|
|
157
|
+
this._url = url.trim().replace(/\/$/, ''); this._apiKey = apiKey.trim();
|
|
158
|
+
this._origin = typeof origin === 'string' && origin.trim() ? origin.trim() : 'sdk';
|
|
159
|
+
this._log = typeof log === 'function' ? log : console.log.bind(console);
|
|
160
|
+
this._show = typeof show === 'function' ? show : console.log.bind(console);
|
|
161
|
+
}
|
|
162
|
+
getEndpoint() {
|
|
163
|
+
if (this._es_query != null) return '/search/es_query';
|
|
164
|
+
const hasTextOrVector = (typeof this._text === 'string' && this._text.length > 0) || (Array.isArray(this._vector) && this._vector.length > 0);
|
|
165
|
+
if (hasTextOrVector) return '/search/semantic';
|
|
166
|
+
if (this._boost.length > 0) return '/search/boost';
|
|
167
|
+
return '/search/filter_and_sort';
|
|
168
|
+
}
|
|
169
|
+
getPayload() {
|
|
170
|
+
const endpoint = this.getEndpoint();
|
|
171
|
+
if (endpoint === '/search/es_query') return { index: this._index, origin: this._origin, feed_type: 'es_query', query: this._es_query };
|
|
172
|
+
const feedType = endpoint === '/search/semantic' ? 'semantic' : endpoint === '/search/boost' ? 'boost' : 'filter_and_sort';
|
|
173
|
+
const serializeFilters = (arr) => arr.map((f) => ({ ...f }));
|
|
174
|
+
const payload = { index: this._index, origin: this._origin, feed_type: feedType, include_vector: this._include_vector, size: this._size, include: serializeFilters(this._include), exclude: serializeFilters(this._exclude) };
|
|
175
|
+
if (feedType === 'boost') payload.boost = serializeFilters(this._boost);
|
|
176
|
+
if (feedType === 'filter_and_sort' && this._sort_by) payload.sort_by = this._sort_by;
|
|
177
|
+
if (feedType === 'semantic') { if (typeof this._text === 'string' && this._text.length > 0) payload.text = this._text; if (Array.isArray(this._vector) && this._vector.length > 0) payload.vector = this._vector; }
|
|
178
|
+
if (this._only_ids) payload.only_ids = true;
|
|
179
|
+
if (Array.isArray(this._select_fields) && this._select_fields.length > 0) payload.select_fields = this._select_fields;
|
|
180
|
+
return payload;
|
|
181
|
+
}
|
|
182
|
+
async execute() {
|
|
183
|
+
if (!this._index || typeof this._index !== 'string' || !this._index.trim()) throw new Error('Search.execute: index must be set');
|
|
184
|
+
if (this._es_query != null && (typeof this._es_query !== 'object' || Array.isArray(this._es_query))) throw new Error('Search.execute: esQuery() must be called with a plain object');
|
|
185
|
+
const hasOnlyIds = this._only_ids === true, hasSelectFields = Array.isArray(this._select_fields) && this._select_fields.length > 0, hasIncludeVector = this._include_vector === true;
|
|
186
|
+
if ((hasOnlyIds ? 1 : 0) + (hasSelectFields ? 1 : 0) + (hasIncludeVector ? 1 : 0) > 1) throw new Error('Search: onlyIds, selectFields, includeVectors are mutually exclusive');
|
|
187
|
+
if (this.getEndpoint() === '/search/es_query' && (this._include.length || this._exclude.length || this._boost.length)) throw new Error('Search: esQuery does not support include/exclude/boost');
|
|
188
|
+
if (this.getEndpoint() === '/search/semantic' && (this._include.length || this._exclude.length || this._boost.length)) throw new Error('Search: semantic does not support include/exclude/boost');
|
|
189
|
+
if (this.getEndpoint() === '/search/semantic' && this._sort_by) throw new Error('Search: semantic does not support sortBy');
|
|
190
|
+
if (this.getEndpoint() === '/search/boost' && this._sort_by) throw new Error('Search: boost does not support sortBy');
|
|
191
|
+
const endpoint = this.getEndpoint(), payload = this.getPayload(), url = `${this._url}${endpoint}`;
|
|
192
|
+
this.log(`Sending request to ${url}`);
|
|
193
|
+
const startTime = performance.now();
|
|
194
|
+
const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${this._apiKey}` }, body: JSON.stringify(payload) });
|
|
195
|
+
if (!response.ok) { const text = await response.text(); throw new Error(`Search API error: ${response.status} — ${text}`); }
|
|
196
|
+
const result = await response.json();
|
|
197
|
+
if (result && typeof result.error !== 'undefined' && result.error !== null) throw new Error(typeof result.error === 'string' ? result.error : String(result.error));
|
|
198
|
+
result.took_sdk = Math.round(performance.now() - startTime);
|
|
199
|
+
this.lastCall = { endpoint, payload }; this.lastResult = result;
|
|
200
|
+
const res = result.result; if (!res) throw new Error('Search.execute: result.result is undefined');
|
|
201
|
+
return res.hits;
|
|
202
|
+
}
|
|
203
|
+
async frequentValues(field, size = 25) {
|
|
204
|
+
if (!this._index || typeof this._index !== 'string' || !this._index.trim()) throw new Error('Search.frequentValues: index must be set');
|
|
205
|
+
const n = Number(size); if (!Number.isInteger(n) || n <= 0) throw new Error('Search.frequentValues: size must be positive integer');
|
|
206
|
+
const endpoint = `/search/frequent_values/${encodeURIComponent(this._index)}/${encodeURIComponent(field.trim())}?size=${n}`;
|
|
207
|
+
const response = await fetch(`${this._url}${endpoint}`, { method: 'GET', headers: { Authorization: `Bearer ${this._apiKey}` } });
|
|
208
|
+
if (!response.ok) throw new Error(`Search frequentValues error: ${response.status}`);
|
|
209
|
+
const result = await response.json();
|
|
210
|
+
if (result && typeof result.error !== 'undefined' && result.error !== null) throw new Error(typeof result.error === 'string' ? result.error : String(result.error));
|
|
211
|
+
return result;
|
|
212
|
+
}
|
|
213
|
+
async lookup(docId) {
|
|
214
|
+
if (!this._index || typeof this._index !== 'string' || !this._index.trim()) throw new Error('Search.lookup: index must be set');
|
|
215
|
+
const endpoint = `/search/document/${encodeURIComponent(this._index)}/${encodeURIComponent(docId.trim())}`;
|
|
216
|
+
const response = await fetch(`${this._url}${endpoint}`, { method: 'GET', headers: { Authorization: `Bearer ${this._apiKey}` } });
|
|
217
|
+
if (!response.ok) throw new Error(`Search lookup error: ${response.status}`);
|
|
218
|
+
const result = await response.json();
|
|
219
|
+
if (result && typeof result.error !== 'undefined' && result.error !== null) throw new Error(typeof result.error === 'string' ? result.error : String(result.error));
|
|
220
|
+
return result;
|
|
221
|
+
}
|
|
222
|
+
index(selected_index) { if (typeof selected_index !== 'string' || !selected_index.trim()) throw new Error('Search.index: selected_index required'); this._index = selected_index.trim(); return this; }
|
|
223
|
+
size(size) { const n = Number(size); if (!Number.isInteger(n) || n <= 0 || n >= 2000) throw new Error('Search.size: 0 < size < 2000'); this._size = n; return this; }
|
|
224
|
+
onlyIds(value) { this._only_ids = value == null ? true : Boolean(value); return this; }
|
|
225
|
+
includeVectors(value) { this._include_vector = value == null ? true : Boolean(value); return this; }
|
|
226
|
+
selectFields(fields) { if (fields === null) { this._select_fields = null; return this; } if (!Array.isArray(fields)) throw new Error('Search.selectFields: array or null'); this._select_fields = fields.map((f) => (typeof f === 'string' ? f.trim() : String(f))); return this; }
|
|
227
|
+
text(text) { if (typeof text !== 'string' || !text.trim()) throw new Error('Search.text: non-empty string'); this._text = text.trim(); return this; }
|
|
228
|
+
vector(vector) { if (!Array.isArray(vector)) throw new Error('Search.vector: array'); this._vector = vector; return this; }
|
|
229
|
+
esQuery(rawQuery) { if (rawQuery == null || typeof rawQuery !== 'object' || Array.isArray(rawQuery)) throw new Error('Search.esQuery: plain object'); this._es_query = rawQuery; return this; }
|
|
230
|
+
sortBy(field, direction = 'desc') { if (typeof field !== 'string' || !field.trim()) throw new Error('Search.sortBy: field required'); if (direction !== 'asc' && direction !== 'desc') throw new Error('Search.sortBy: asc|desc'); this._sort_by = { field: field.trim(), order: direction }; return this; }
|
|
231
|
+
include() { this._active_array = this._include; return this; }
|
|
232
|
+
exclude() { this._active_array = this._exclude; return this; }
|
|
233
|
+
boost() { this._active_array = this._boost; return this; }
|
|
234
|
+
_requireActiveArray() { if (this._active_array === null) throw new Error('Search: call include(), exclude(), or boost() before filters'); }
|
|
235
|
+
_requireBoostForBoostArray(boost) { if (this._active_array === this._boost && boost == null) throw new Error('Search: boost array requires non-null boost'); }
|
|
236
|
+
filter(filterInstance) {
|
|
237
|
+
this._requireActiveArray();
|
|
238
|
+
if (filterInstance == null || !(filterInstance instanceof Filter)) throw new Error('Search.filter: Filter instance required');
|
|
239
|
+
if (this._active_array === this._boost && filterInstance.filter !== 'group_boost' && filterInstance.boost == null) throw new Error('Search: boost array filter needs boost');
|
|
240
|
+
this._active_array.push(filterInstance); return this;
|
|
241
|
+
}
|
|
242
|
+
term(field, value, boost = null) { this._requireActiveArray(); this._requireBoostForBoostArray(boost); this._active_array.push(new TermFilter(field, value, boost)); return this; }
|
|
243
|
+
terms(field, values, boost = null) { this._requireActiveArray(); this._requireBoostForBoostArray(boost); this._active_array.push(new TermsFilter(field, values, boost)); return this; }
|
|
244
|
+
numeric(field, operator, value, boost = null) { this._requireActiveArray(); this._requireBoostForBoostArray(boost); this._active_array.push(new NumericFilter(field, operator, value, boost)); return this; }
|
|
245
|
+
date(field, dateFrom = null, dateTo = null, boost = null) { this._requireActiveArray(); this._requireBoostForBoostArray(boost); this._active_array.push(new DateFilter(field, dateFrom, dateTo, boost)); return this; }
|
|
246
|
+
geo(field, value, boost = null) { this._requireActiveArray(); this._requireBoostForBoostArray(boost); this._active_array.push(new GeoFilter(field, value, boost)); return this; }
|
|
247
|
+
match(field, value, boost = null) { this._requireActiveArray(); this._requireBoostForBoostArray(boost); this._active_array.push(new MatchFilter(field, value, boost)); return this; }
|
|
248
|
+
isNull(field, boost = null) { this._requireActiveArray(); this._requireBoostForBoostArray(boost); this._active_array.push(new IsNullFilter(field, boost)); return this; }
|
|
249
|
+
notNull(field, boost = null) { this._requireActiveArray(); this._requireBoostForBoostArray(boost); this._active_array.push(new NotNullFilter(field, boost)); return this; }
|
|
250
|
+
custom(field, value, boost = null) { this._requireActiveArray(); this._requireBoostForBoostArray(boost); this._active_array.push(new CustomFilter(field, value, boost)); return this; }
|
|
251
|
+
groupBoost(lookup_index, field, value, group, min_boost = null, max_boost = null, n = null) { this._requireActiveArray(); this._active_array.push(new GroupBoostFilter(lookup_index, field, value, group, min_boost, max_boost, n)); return this; }
|
|
252
|
+
termsLookup(lookup_index, field, value, path, boost = null) { this._requireActiveArray(); this._requireBoostForBoostArray(boost); this._active_array.push(new TermsLookupFilter(lookup_index, field, value, path, boost)); return this; }
|
|
253
|
+
consoleAccount(field, value, path, boost = null) { this._requireActiveArray(); this._requireBoostForBoostArray(boost); this._active_array.push(new ConsoleAccountFilter(field, value, path, boost)); return this; }
|
|
254
|
+
log(string) { this._log(string); }
|
|
255
|
+
show(results) { this._show(results); }
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
## V1/search/filters/Filter.js
|
|
259
|
+
export class Filter {
|
|
260
|
+
constructor(filterType, field, boost = null) {
|
|
261
|
+
if (new.target === Filter) throw new Error('Filter is abstract');
|
|
262
|
+
this.filter = filterType; this.field = field; this.boost = boost;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
## V1/search/filters/*Filter.js (extend Filter)
|
|
267
|
+
// TermFilter(field, value, boost): super('term', field, boost); this.value = value
|
|
268
|
+
// TermsFilter(field, value, boost): super('terms', field, boost); this.value = value
|
|
269
|
+
// NumericFilter(field, operator, value, boost): super('numeric', field, boost); this.operator = operator; this.value = value
|
|
270
|
+
// MatchFilter, GeoFilter, CustomFilter: same pattern with this.value
|
|
271
|
+
// DateFilter(field, dateFrom, dateTo, boost): value = { date_from?, date_to? }; at least one required
|
|
272
|
+
// IsNullFilter, NotNullFilter: no value
|
|
273
|
+
// GroupBoostFilter(lookup_index, field, value, group, min_boost, max_boost, n): super('group_boost', field, null)
|
|
274
|
+
// TermsLookupFilter(lookup_index, field, value, path): path = dot-notation in lookup doc (e.g. "followers.ids")
|
|
275
|
+
// ConsoleAccountFilter(field, value, path): path = dot-notation in account doc
|
|
276
|
+
|
|
277
|
+
## V1/features/Features.js
|
|
278
|
+
export const PREFERRED_FEATURE_COLUMNS = ['found','original_rank','sem_sim_fuzzy','sem_sim_closest','usr_primary_labels','usr_secondary_labels','usr_primary_tags','usr_secondary_tags','user_affinity_avg','user_affinity_usdc','user_affinity_count','cluster_1'..'cluster_10','sem_sim_cluster1'..'sem_sim_cluster5'];
|
|
279
|
+
export function sortAvailableFeatures(available) {
|
|
280
|
+
const preferred = PREFERRED_FEATURE_COLUMNS.filter((col) => available.includes(col));
|
|
281
|
+
const nonPreferred = available.filter((col) => !PREFERRED_FEATURE_COLUMNS.includes(col));
|
|
282
|
+
const regular = nonPreferred.filter((c) => !c.startsWith('AI:') && !c.startsWith('TAG:')).sort();
|
|
283
|
+
const aiColumns = nonPreferred.filter((c) => c.startsWith('AI:')).sort();
|
|
284
|
+
const tagColumns = nonPreferred.filter((c) => c.startsWith('TAG:')).sort();
|
|
285
|
+
return [...preferred, ...regular, ...aiColumns, ...tagColumns];
|
|
286
|
+
}
|
|
287
|
+
export class Features {
|
|
288
|
+
_version = 'v1'; _user = null; _items = []; lastCall = null; lastResult = null;
|
|
289
|
+
constructor(options) {
|
|
290
|
+
if (!options || typeof options !== 'object') throw new Error('Features: options required');
|
|
291
|
+
const { url, apiKey, version = 'v1', items = [], userIndex, userId, origin = 'sdk', log, show } = options;
|
|
292
|
+
if (typeof url !== 'string' || !url.trim()) throw new Error('Features: url required');
|
|
293
|
+
if (typeof apiKey !== 'string' || !apiKey.trim()) throw new Error('Features: apiKey required');
|
|
294
|
+
this._url = url.trim().replace(/\/$/, ''); this._apiKey = apiKey.trim(); this._version = version;
|
|
295
|
+
this._origin = typeof origin === 'string' && origin.trim() ? origin.trim() : 'sdk';
|
|
296
|
+
this._log = typeof log === 'function' ? log : console.log.bind(console);
|
|
297
|
+
this._show = typeof show === 'function' ? show : console.log.bind(console);
|
|
298
|
+
if (Array.isArray(items) && items.length > 0) this._items = items;
|
|
299
|
+
if (userIndex != null && userId != null) this._user = { index: userIndex, id: userId };
|
|
300
|
+
}
|
|
301
|
+
getEndpoint() { return `/features/${this._version}`; }
|
|
302
|
+
getPayload() { return { origin: this._origin, user: this._user, items: this._items.map((item) => ({ ...item })) }; }
|
|
303
|
+
version(v) { this._version = v; return this; }
|
|
304
|
+
items(items) { this._items = [...items]; return this; }
|
|
305
|
+
user(index, userId) { this._user = { index, id: userId }; return this; }
|
|
306
|
+
async execute() {
|
|
307
|
+
if (!this._user || !this._user.index || !this._user.id) throw new Error('Features.execute: user must be set');
|
|
308
|
+
if (!Array.isArray(this._items) || this._items.length === 0) throw new Error('Features.execute: items must be set');
|
|
309
|
+
const url = `${this._url}${this.getEndpoint()}`;
|
|
310
|
+
const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${this._apiKey}` }, body: JSON.stringify(this.getPayload()) });
|
|
311
|
+
if (!response.ok) throw new Error(`Features API error: ${response.status}`);
|
|
312
|
+
const result = await response.json();
|
|
313
|
+
if (result && typeof result.error !== 'undefined' && result.error !== null) throw new Error(typeof result.error === 'string' ? result.error : String(result.error));
|
|
314
|
+
this.lastCall = { endpoint: this.getEndpoint(), payload: this.getPayload() }; this.lastResult = result;
|
|
315
|
+
const res = result.result; if (!res) throw new Error('Features.execute: result.result undefined');
|
|
316
|
+
return res;
|
|
317
|
+
}
|
|
318
|
+
log(string) { this._log(string); }
|
|
319
|
+
show(results) { this._show(results); }
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
## V1/scoring/Scoring.js
|
|
323
|
+
export class Scoring {
|
|
324
|
+
_userId = null; _itemIds = []; _modelEndpoint = null; lastCall = null; lastResult = null;
|
|
325
|
+
constructor(options) {
|
|
326
|
+
if (!options || typeof options !== 'object') throw new Error('Scoring: options required');
|
|
327
|
+
const { url, apiKey, userId = null, itemIds = [], origin = 'sdk', log, show } = options;
|
|
328
|
+
if (typeof url !== 'string' || !url.trim()) throw new Error('Scoring: url required');
|
|
329
|
+
if (typeof apiKey !== 'string' || !apiKey.trim()) throw new Error('Scoring: apiKey required');
|
|
330
|
+
this._url = url.trim().replace(/\/$/, ''); this._apiKey = apiKey.trim();
|
|
331
|
+
this._origin = typeof origin === 'string' && origin.trim() ? origin.trim() : 'sdk';
|
|
332
|
+
this._log = typeof log === 'function' ? log : console.log.bind(console);
|
|
333
|
+
this._show = typeof show === 'function' ? show : console.log.bind(console);
|
|
334
|
+
if (userId != null && typeof userId === 'string' && userId.trim()) this._userId = userId.trim();
|
|
335
|
+
if (Array.isArray(itemIds) && itemIds.length > 0) this._itemIds = itemIds.map((id) => (typeof id === 'string' ? id : String(id)));
|
|
336
|
+
}
|
|
337
|
+
getEndpoint() { if (!this._modelEndpoint || !this._modelEndpoint.trim()) throw new Error('Scoring: model(endpoint) required'); return this._modelEndpoint.startsWith('/') ? this._modelEndpoint : `/${this._modelEndpoint}`; }
|
|
338
|
+
getPayload() { return { origin: this._origin, user_id: this._userId, item_ids: [...this._itemIds] }; }
|
|
339
|
+
model(endpoint) { this._modelEndpoint = endpoint; return this; }
|
|
340
|
+
userId(userId) { this._userId = userId; return this; }
|
|
341
|
+
itemIds(itemIds) { this._itemIds = itemIds; return this; }
|
|
342
|
+
async execute() {
|
|
343
|
+
if (!this._modelEndpoint || !this._modelEndpoint.trim()) throw new Error('Scoring: model required');
|
|
344
|
+
if (!this._userId || typeof this._userId !== 'string' || !this._userId.trim()) throw new Error('Scoring: user_id required');
|
|
345
|
+
if (!Array.isArray(this._itemIds) || this._itemIds.length === 0) throw new Error('Scoring: item_ids required');
|
|
346
|
+
const url = `${this._url}${this.getEndpoint()}`;
|
|
347
|
+
const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${this._apiKey}` }, body: JSON.stringify(this.getPayload()) });
|
|
348
|
+
if (!response.ok) throw new Error(`Scoring API error: ${response.status}`);
|
|
349
|
+
const result = await response.json();
|
|
350
|
+
if (result && typeof result.error !== 'undefined' && result.error !== null) throw new Error(typeof result.error === 'string' ? result.error : String(result.error));
|
|
351
|
+
this.lastCall = { endpoint: this.getEndpoint(), payload: this.getPayload() }; this.lastResult = result;
|
|
352
|
+
const res = result.result; if (res === undefined) throw new Error('Scoring: result.result undefined');
|
|
353
|
+
return res;
|
|
354
|
+
}
|
|
355
|
+
log(string) { this._log(string); }
|
|
356
|
+
show(results) { this._show(results); }
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
## V1/ranking/Ranking.js
|
|
360
|
+
export class Ranking {
|
|
361
|
+
_candidates = []; _sortMethod = 'sort'; _sortParams = null; _diversityMethod = null; _diversityParams = null; _limitsByFieldEnabled = false; _everyN = 10; _limitRules = []; lastCall = null; lastResult = null;
|
|
362
|
+
constructor(options) {
|
|
363
|
+
if (!options || typeof options !== 'object') throw new Error('Ranking: options required');
|
|
364
|
+
const { url, apiKey, candidates = [], origin = 'sdk', log, show } = options;
|
|
365
|
+
if (typeof url !== 'string' || !url.trim()) throw new Error('Ranking: url required');
|
|
366
|
+
if (typeof apiKey !== 'string' || !apiKey.trim()) throw new Error('Ranking: apiKey required');
|
|
367
|
+
this._url = url.trim().replace(/\/$/, ''); this._apiKey = apiKey.trim();
|
|
368
|
+
this._origin = typeof origin === 'string' && origin.trim() ? origin.trim() : 'sdk';
|
|
369
|
+
this._log = typeof log === 'function' ? log : console.log.bind(console);
|
|
370
|
+
this._show = typeof show === 'function' ? show : console.log.bind(console);
|
|
371
|
+
this._candidates = candidates;
|
|
372
|
+
}
|
|
373
|
+
getEndpoint() { return '/ranking/feed'; }
|
|
374
|
+
_getUsefulFieldsAndNeedEmbedding() {
|
|
375
|
+
const useful = new Set(); let needEmbedding = false;
|
|
376
|
+
if (this._sortParams) {
|
|
377
|
+
if (this._sortMethod === 'sort' && Array.isArray(this._sortParams.fields)) this._sortParams.fields.forEach((f) => useful.add(f));
|
|
378
|
+
if ((this._sortMethod === 'linear' || this._sortMethod === 'mix') && Array.isArray(this._sortParams)) this._sortParams.forEach((p) => p.field && useful.add(p.field));
|
|
379
|
+
}
|
|
380
|
+
if (this._diversityMethod === 'fields' && this._diversityParams?.fields) this._diversityParams.fields.forEach((f) => useful.add(f));
|
|
381
|
+
if (this._diversityMethod === 'semantic') needEmbedding = true;
|
|
382
|
+
if (this._limitsByFieldEnabled && this._limitRules.length > 0) this._limitRules.forEach((r) => r.field && useful.add(r.field));
|
|
383
|
+
return { usefulFields: useful, needEmbedding };
|
|
384
|
+
}
|
|
385
|
+
getPayload() {
|
|
386
|
+
const { usefulFields, needEmbedding } = this._getUsefulFieldsAndNeedEmbedding();
|
|
387
|
+
const hits = this._candidates || [];
|
|
388
|
+
const items = hits.map((hit) => {
|
|
389
|
+
const item = { item_id: hit._id };
|
|
390
|
+
for (const key of usefulFields) { const v = hit._features?.[key] ?? hit._scores?.[key]; if (v !== undefined) item[key] = v; }
|
|
391
|
+
if (needEmbedding) { let embed = hit._source?.item_sem_embed2; if (!embed || !Array.isArray(embed)) embed = hit._source?.text_vector; if (embed) item.embed = embed; }
|
|
392
|
+
return item;
|
|
393
|
+
});
|
|
394
|
+
const payload = { origin: this._origin, items };
|
|
395
|
+
const sortConfig = this._buildSortConfig(); if (sortConfig) payload.sort = sortConfig;
|
|
396
|
+
const diversityConfig = this._buildDiversityConfig(); if (diversityConfig) payload.diversity = diversityConfig;
|
|
397
|
+
const limitsByFieldConfig = this._buildLimitsByFieldConfig(); if (limitsByFieldConfig) payload.limits_by_field = limitsByFieldConfig;
|
|
398
|
+
return payload;
|
|
399
|
+
}
|
|
400
|
+
_buildSortConfig() {
|
|
401
|
+
if (!this._sortParams) return undefined;
|
|
402
|
+
if (this._sortMethod === 'sort' && this._sortParams.fields?.length > 0) return { method: 'sort', params: { ...this._sortParams } };
|
|
403
|
+
if (this._sortMethod === 'linear' && Array.isArray(this._sortParams) && this._sortParams.length > 0) return { method: 'linear', params: this._sortParams.map((p) => ({ field: p.field, weight: p.weight })) };
|
|
404
|
+
if (this._sortMethod === 'mix' && Array.isArray(this._sortParams) && this._sortParams.length > 0) return { method: 'mix', params: this._sortParams.map((p) => ({ field: p.field, direction: p.direction, percentage: p.percentage })) };
|
|
405
|
+
return undefined;
|
|
406
|
+
}
|
|
407
|
+
_buildDiversityConfig() {
|
|
408
|
+
if (!this._diversityMethod) return undefined;
|
|
409
|
+
if (this._diversityMethod === 'fields' && this._diversityParams?.fields?.length > 0) return { method: 'fields', params: { fields: [...this._diversityParams.fields] } };
|
|
410
|
+
if (this._diversityMethod === 'semantic') return { method: 'semantic', params: { lambda: Number(this._diversityParams?.lambda ?? 0.5), horizon: Number(this._diversityParams?.horizon ?? 20) } };
|
|
411
|
+
return undefined;
|
|
412
|
+
}
|
|
413
|
+
_buildLimitsByFieldConfig() {
|
|
414
|
+
if (!this._limitsByFieldEnabled || !this._limitRules.length) return undefined;
|
|
415
|
+
const everyN = Number(this._everyN); if (!Number.isInteger(everyN) || everyN < 2) return undefined;
|
|
416
|
+
return { every_n: everyN, rules: this._limitRules.map((r) => ({ field: r.field, limit: Number(r.limit) || 0 })) };
|
|
417
|
+
}
|
|
418
|
+
sortingMethod(x) {
|
|
419
|
+
if (x !== 'sort' && x !== 'linear' && x !== 'mix') throw new Error('Ranking.sortingMethod: sort|linear|mix');
|
|
420
|
+
this._sortMethod = x;
|
|
421
|
+
if (x === 'sort') this._sortParams = { fields: [], direction: [] };
|
|
422
|
+
if (x === 'linear' || x === 'mix') this._sortParams = [];
|
|
423
|
+
return this;
|
|
424
|
+
}
|
|
425
|
+
sortBy(field, direction = 'desc', field2, direction2 = 'desc') {
|
|
426
|
+
if (this._sortMethod === 'linear' || this._sortMethod === 'mix') throw new Error('Ranking.sortBy: only for sortingMethod("sort")');
|
|
427
|
+
this._sortMethod = 'sort'; if (!this._sortParams || !this._sortParams.fields) this._sortParams = { fields: [], direction: [] };
|
|
428
|
+
const f = typeof field === 'string' && field.trim() ? field.trim() : null; if (!f) throw new Error('Ranking.sortBy: field required');
|
|
429
|
+
if (direction !== 'asc' && direction !== 'desc') throw new Error('Ranking.sortBy: asc|desc');
|
|
430
|
+
this._sortParams = { fields: [f], direction: [direction] };
|
|
431
|
+
if (typeof field2 === 'string' && field2.trim()) { this._sortParams.fields.push(field2.trim()); this._sortParams.direction.push(direction2 === 'asc' ? 'asc' : 'desc'); }
|
|
432
|
+
return this;
|
|
433
|
+
}
|
|
434
|
+
weight(field, w) { if (this._sortMethod !== 'linear') throw new Error('Ranking.weight: only for linear'); const f = typeof field === 'string' && field.trim() ? field.trim() : null; if (!f) throw new Error('Ranking.weight: field required'); if (!Array.isArray(this._sortParams)) this._sortParams = []; this._sortParams.push({ field: f, weight: Number(w) }); return this; }
|
|
435
|
+
mix(field, direction, percentage) {
|
|
436
|
+
if (this._sortMethod === 'linear') throw new Error('Ranking.mix: only for mix');
|
|
437
|
+
this._sortMethod = 'mix'; if (!Array.isArray(this._sortParams)) this._sortParams = [];
|
|
438
|
+
const f = typeof field === 'string' && field.trim() ? field.trim() : null; if (!f) throw new Error('Ranking.mix: field required');
|
|
439
|
+
if (direction !== 'asc' && direction !== 'desc') throw new Error('Ranking.mix: asc|desc');
|
|
440
|
+
this._sortParams.push({ field: f, direction, percentage: Number(percentage) || 0 }); return this;
|
|
441
|
+
}
|
|
442
|
+
diversity(method) {
|
|
443
|
+
if (method !== 'fields' && method !== 'semantic') throw new Error('Ranking.diversity: fields|semantic');
|
|
444
|
+
this._diversityMethod = method;
|
|
445
|
+
if (method === 'fields') this._diversityParams = { fields: [] };
|
|
446
|
+
if (method === 'semantic') this._diversityParams = { lambda: 0.5, horizon: 20 };
|
|
447
|
+
return this;
|
|
448
|
+
}
|
|
449
|
+
fields(arrayOrItem) {
|
|
450
|
+
if (this._diversityMethod !== 'fields') throw new Error('Ranking.fields: only for diversity("fields")');
|
|
451
|
+
if (!this._diversityParams || !Array.isArray(this._diversityParams.fields)) this._diversityParams = { fields: [] };
|
|
452
|
+
const add = (name) => { const s = typeof name === 'string' && name.trim() ? name.trim() : null; if (s && !this._diversityParams.fields.includes(s)) this._diversityParams.fields.push(s); };
|
|
453
|
+
if (Array.isArray(arrayOrItem)) arrayOrItem.forEach(add); else add(arrayOrItem);
|
|
454
|
+
return this;
|
|
455
|
+
}
|
|
456
|
+
horizon(n) { if (this._diversityMethod !== 'semantic') throw new Error('Ranking.horizon: only for semantic'); if (!this._diversityParams) this._diversityParams = { lambda: 0.5, horizon: 20 }; this._diversityParams.horizon = Number(n); return this; }
|
|
457
|
+
lambda(value) { if (this._diversityMethod !== 'semantic') throw new Error('Ranking.lambda: only for semantic'); if (!this._diversityParams) this._diversityParams = { lambda: 0.5, horizon: 20 }; this._diversityParams.lambda = Number(value); return this; }
|
|
458
|
+
limitByField() { this._limitsByFieldEnabled = true; return this; }
|
|
459
|
+
every(n) { this._everyN = Number(n); return this; }
|
|
460
|
+
limit(field, max) { const f = typeof field === 'string' && field.trim() ? field.trim() : null; if (!f) throw new Error('Ranking.limit: field required'); const existing = this._limitRules.find((r) => r.field === f); if (existing) existing.limit = Number(max) || 0; else this._limitRules.push({ field: f, limit: Number(max) || 0 }); return this; }
|
|
461
|
+
candidates(candidates) { this._candidates = candidates; return this; }
|
|
462
|
+
async execute() {
|
|
463
|
+
if (!Array.isArray(this._candidates) || this._candidates.length === 0) throw new Error('Ranking.execute: candidates required');
|
|
464
|
+
if (!this._buildSortConfig()) throw new Error('Ranking.execute: sort config required');
|
|
465
|
+
const url = `${this._url}${this.getEndpoint()}`;
|
|
466
|
+
const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${this._apiKey}` }, body: JSON.stringify(this.getPayload()) });
|
|
467
|
+
if (!response.ok) throw new Error(`Ranking API error: ${response.status}`);
|
|
468
|
+
const result = await response.json();
|
|
469
|
+
if (result && typeof result.error !== 'undefined' && result.error !== null) throw new Error(typeof result.error === 'string' ? result.error : String(result.error));
|
|
470
|
+
this.lastCall = { endpoint: this.getEndpoint(), payload: this.getPayload() }; this.lastResult = result;
|
|
471
|
+
const res = result.result; if (!res) throw new Error('Ranking.execute: result undefined');
|
|
472
|
+
return res;
|
|
473
|
+
}
|
|
474
|
+
log(string) { this._log(string); }
|
|
475
|
+
show(results) { this._show(results); }
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
## V1/utils/indexUtils.js
|
|
479
|
+
/** Maps index names to canonical base (farcaster-items-v2 → farcaster-items). Used for feature/score keys. */
|
|
480
|
+
export function findIndex(index) {
|
|
481
|
+
const indexOptions = ['farcaster-items', 'zora-coins', 'polymarket-items', 'polymarket-wallets'];
|
|
482
|
+
for (const option of indexOptions) if (index.startsWith(option)) return option;
|
|
483
|
+
return null;
|
|
484
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mbd-studio-sdk",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.1.0",
|
|
4
4
|
"description": "SDK for Embed Recommendation Engine APIs",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"web3",
|
|
@@ -12,12 +12,19 @@
|
|
|
12
12
|
],
|
|
13
13
|
"type": "module",
|
|
14
14
|
"main": "index.js",
|
|
15
|
+
"types": "index.d.ts",
|
|
15
16
|
"exports": {
|
|
16
|
-
".":
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./index.d.ts",
|
|
19
|
+
"import": "./index.js",
|
|
20
|
+
"default": "./index.js"
|
|
21
|
+
}
|
|
17
22
|
},
|
|
18
23
|
"files": [
|
|
19
24
|
"index.js",
|
|
25
|
+
"index.d.ts",
|
|
20
26
|
"StudioConfig.js",
|
|
21
|
-
"V1"
|
|
27
|
+
"V1",
|
|
28
|
+
"llms.txt"
|
|
22
29
|
]
|
|
23
30
|
}
|