@memberjunction/ai-recommendations 4.0.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/README.md +376 -0
- package/package.json +5 -5
- package/readme.md +315 -176
package/README.md
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
# @memberjunction/ai-recommendations
|
|
2
|
+
|
|
3
|
+
A provider-based recommendation engine for MemberJunction. Manages recommendation runs, delegates to pluggable providers via the class factory, and tracks results through Recommendation, Recommendation Run, and Recommendation Item entities.
|
|
4
|
+
|
|
5
|
+
## Architecture
|
|
6
|
+
|
|
7
|
+
```mermaid
|
|
8
|
+
graph TD
|
|
9
|
+
subgraph Engine["@memberjunction/ai-recommendations"]
|
|
10
|
+
REB["RecommendationEngineBase<br/>(singleton BaseEngine)"]
|
|
11
|
+
RPB["RecommendationProviderBase<br/>(abstract)"]
|
|
12
|
+
RR["RecommendationRequest<T>"]
|
|
13
|
+
RRES["RecommendationResult"]
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
subgraph Providers["Registered Providers"]
|
|
17
|
+
P1["Provider A"]
|
|
18
|
+
P2["Provider B"]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
subgraph MJEntities["MemberJunction Entities"]
|
|
22
|
+
RP["Recommendation Providers"]
|
|
23
|
+
RUN["Recommendation Runs"]
|
|
24
|
+
REC["Recommendations"]
|
|
25
|
+
RI["Recommendation Items"]
|
|
26
|
+
LIST["Lists / List Details"]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
subgraph MJCore["MemberJunction Core"]
|
|
30
|
+
BE["BaseEngine"]
|
|
31
|
+
CF["ClassFactory"]
|
|
32
|
+
MD["Metadata"]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
REB -->|extends| BE
|
|
36
|
+
REB -->|discovers| CF
|
|
37
|
+
CF -->|creates| P1
|
|
38
|
+
CF -->|creates| P2
|
|
39
|
+
P1 -->|extends| RPB
|
|
40
|
+
P2 -->|extends| RPB
|
|
41
|
+
REB --> RP
|
|
42
|
+
REB --> RUN
|
|
43
|
+
RPB --> REC
|
|
44
|
+
RPB --> RI
|
|
45
|
+
REB --> LIST
|
|
46
|
+
|
|
47
|
+
style Engine fill:#2d6a9f,stroke:#1a4971,color:#fff
|
|
48
|
+
style Providers fill:#2d8659,stroke:#1a5c3a,color:#fff
|
|
49
|
+
style MJEntities fill:#b8762f,stroke:#8a5722,color:#fff
|
|
50
|
+
style MJCore fill:#7c5295,stroke:#563a6b,color:#fff
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Installation
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
npm install @memberjunction/ai-recommendations
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Overview
|
|
60
|
+
|
|
61
|
+
This package provides the framework for running recommendations in MemberJunction. It follows the engine/provider pattern used throughout the platform:
|
|
62
|
+
|
|
63
|
+
1. **RecommendationEngineBase** -- a singleton engine (extending `BaseEngine`) that loads provider metadata, selects a provider, creates Recommendation Run tracking records, and delegates the actual recommendation logic
|
|
64
|
+
2. **RecommendationProviderBase** -- an abstract class that concrete providers implement to generate recommendations for each source record
|
|
65
|
+
3. **RecommendationRequest/RecommendationResult** -- typed request and response objects that flow through the pipeline
|
|
66
|
+
|
|
67
|
+
Providers are discovered at runtime through MemberJunction's `ClassFactory` using `@RegisterClass(RecommendationProviderBase, 'ProviderName')`.
|
|
68
|
+
|
|
69
|
+
## Recommendation Flow
|
|
70
|
+
|
|
71
|
+
```mermaid
|
|
72
|
+
sequenceDiagram
|
|
73
|
+
participant Caller
|
|
74
|
+
participant Engine as RecommendationEngineBase
|
|
75
|
+
participant CF as ClassFactory
|
|
76
|
+
participant Provider as RecommendationProvider
|
|
77
|
+
participant DB as MJ Database
|
|
78
|
+
|
|
79
|
+
Caller->>Engine: Recommend(request)
|
|
80
|
+
Engine->>Engine: TryThrowIfNotLoaded()
|
|
81
|
+
|
|
82
|
+
alt Provider specified
|
|
83
|
+
Engine->>Engine: Use request.Provider
|
|
84
|
+
else No provider
|
|
85
|
+
Engine->>Engine: Use first available
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
Engine->>Engine: GetRecommendationEntities(request)
|
|
89
|
+
|
|
90
|
+
alt From List
|
|
91
|
+
Engine->>DB: Load List + List Details
|
|
92
|
+
Engine->>DB: Load entity records by IDs
|
|
93
|
+
else From EntityAndRecordsInfo
|
|
94
|
+
Engine->>DB: Load records by entity name + IDs
|
|
95
|
+
else Pre-built
|
|
96
|
+
Engine->>Engine: Validate Recommendations array
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
Engine->>DB: Create Recommendation Run (Status: In Progress)
|
|
100
|
+
|
|
101
|
+
opt CreateErrorList = true
|
|
102
|
+
Engine->>DB: Create error tracking List
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
Engine->>CF: CreateInstance(provider.Name)
|
|
106
|
+
CF-->>Engine: Provider instance
|
|
107
|
+
|
|
108
|
+
Engine->>Provider: Recommend(request)
|
|
109
|
+
|
|
110
|
+
loop For each recommendation
|
|
111
|
+
Provider->>Provider: Call external API
|
|
112
|
+
Provider->>DB: SaveRecommendation + Items
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
Provider-->>Engine: RecommendationResult
|
|
116
|
+
|
|
117
|
+
Engine->>DB: Update Run (Completed/Error)
|
|
118
|
+
Engine-->>Caller: RecommendationResult
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Core Components
|
|
122
|
+
|
|
123
|
+
### RecommendationEngineBase
|
|
124
|
+
|
|
125
|
+
A singleton engine that manages the recommendation lifecycle.
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
import { RecommendationEngineBase } from '@memberjunction/ai-recommendations';
|
|
129
|
+
|
|
130
|
+
// Access the singleton
|
|
131
|
+
const engine = RecommendationEngineBase.Instance;
|
|
132
|
+
|
|
133
|
+
// Initialize (loads Recommendation Providers metadata)
|
|
134
|
+
await engine.Config(false, contextUser);
|
|
135
|
+
|
|
136
|
+
// Run recommendations
|
|
137
|
+
const result = await engine.Recommend(request);
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
**Key properties and methods:**
|
|
141
|
+
|
|
142
|
+
| Member | Description |
|
|
143
|
+
|---|---|
|
|
144
|
+
| `Instance` | Static getter for the singleton instance |
|
|
145
|
+
| `RecommendationProviders` | Array of `RecommendationProviderEntity` loaded from metadata |
|
|
146
|
+
| `Config(forceRefresh?, contextUser?, provider?)` | Loads provider metadata into cache |
|
|
147
|
+
| `Recommend<T>(request)` | Runs the full recommendation pipeline |
|
|
148
|
+
|
|
149
|
+
### RecommendationProviderBase
|
|
150
|
+
|
|
151
|
+
Abstract base class for implementing recommendation providers.
|
|
152
|
+
|
|
153
|
+
```mermaid
|
|
154
|
+
classDiagram
|
|
155
|
+
class RecommendationProviderBase {
|
|
156
|
+
<<abstract>>
|
|
157
|
+
-_md : Metadata
|
|
158
|
+
-_ContextUser : UserInfo
|
|
159
|
+
+ContextUser : UserInfo
|
|
160
|
+
+Recommend(request)* RecommendationResult
|
|
161
|
+
#SaveRecommendation(rec, runID, items) boolean
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
class ConcreteProvider {
|
|
165
|
+
+Recommend(request) RecommendationResult
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
RecommendationProviderBase <|-- ConcreteProvider
|
|
169
|
+
|
|
170
|
+
style RecommendationProviderBase fill:#2d6a9f,stroke:#1a4971,color:#fff
|
|
171
|
+
style ConcreteProvider fill:#2d8659,stroke:#1a5c3a,color:#fff
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
The `SaveRecommendation` helper method handles:
|
|
175
|
+
1. Setting the `RecommendationRunID` on the recommendation entity
|
|
176
|
+
2. Saving the recommendation record
|
|
177
|
+
3. Linking and saving all `RecommendationItemEntity` records
|
|
178
|
+
|
|
179
|
+
### RecommendationRequest\<T\>
|
|
180
|
+
|
|
181
|
+
The request object supports three ways to specify source records:
|
|
182
|
+
|
|
183
|
+
```mermaid
|
|
184
|
+
graph TD
|
|
185
|
+
RR["RecommendationRequest"]
|
|
186
|
+
OPT1["Recommendations[]<br/>Pre-built entities"]
|
|
187
|
+
OPT2["EntityAndRecordsInfo<br/>Entity name + Record IDs"]
|
|
188
|
+
OPT3["ListID<br/>MJ List reference"]
|
|
189
|
+
|
|
190
|
+
RR --> OPT1
|
|
191
|
+
RR --> OPT2
|
|
192
|
+
RR --> OPT3
|
|
193
|
+
|
|
194
|
+
style RR fill:#2d6a9f,stroke:#1a4971,color:#fff
|
|
195
|
+
style OPT1 fill:#2d8659,stroke:#1a5c3a,color:#fff
|
|
196
|
+
style OPT2 fill:#2d8659,stroke:#1a5c3a,color:#fff
|
|
197
|
+
style OPT3 fill:#2d8659,stroke:#1a5c3a,color:#fff
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
| Field | Type | Description |
|
|
201
|
+
|---|---|---|
|
|
202
|
+
| `Recommendations` | `RecommendationEntity[]` | Pre-built unsaved recommendation entities |
|
|
203
|
+
| `EntityAndRecordsInfo` | `{ EntityName, RecordIDs }` | Entity name and array of record IDs to process |
|
|
204
|
+
| `ListID` | `string` | ID of a MJ List whose details become the source records |
|
|
205
|
+
| `Provider` | `RecommendationProviderEntity` | Specific provider to use (defaults to first available) |
|
|
206
|
+
| `CurrentUser` | `UserInfo` | User context |
|
|
207
|
+
| `Options` | `T` | Generic additional options passed to the provider |
|
|
208
|
+
| `CreateErrorList` | `boolean` | Whether to create an error tracking list |
|
|
209
|
+
| `RunID` | `string` | Set automatically by the engine |
|
|
210
|
+
| `ErrorListID` | `string` | Set automatically if error list is created |
|
|
211
|
+
|
|
212
|
+
### RecommendationResult
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
class RecommendationResult {
|
|
216
|
+
Request: RecommendationRequest;
|
|
217
|
+
RecommendationRun?: RecommendationRunEntity;
|
|
218
|
+
RecommendationItems?: RecommendationItemEntity[];
|
|
219
|
+
Success: boolean;
|
|
220
|
+
ErrorMessage: string;
|
|
221
|
+
|
|
222
|
+
AppendWarning(message: string): void; // Adds warning without setting Success=false
|
|
223
|
+
AppendError(message: string): void; // Adds error and sets Success=false
|
|
224
|
+
GetErrorMessages(): string[]; // Splits ErrorMessage into array
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## Usage
|
|
229
|
+
|
|
230
|
+
### Running Recommendations from a List
|
|
231
|
+
|
|
232
|
+
```typescript
|
|
233
|
+
import { RecommendationEngineBase } from '@memberjunction/ai-recommendations';
|
|
234
|
+
import { RecommendationRequest } from '@memberjunction/ai-recommendations';
|
|
235
|
+
|
|
236
|
+
const engine = RecommendationEngineBase.Instance;
|
|
237
|
+
await engine.Config(false, contextUser);
|
|
238
|
+
|
|
239
|
+
const request = new RecommendationRequest();
|
|
240
|
+
request.ListID = 'list-uuid';
|
|
241
|
+
request.CurrentUser = contextUser;
|
|
242
|
+
request.CreateErrorList = true;
|
|
243
|
+
|
|
244
|
+
const result = await engine.Recommend(request);
|
|
245
|
+
|
|
246
|
+
if (result.Success) {
|
|
247
|
+
console.log(`Generated ${result.RecommendationItems?.length ?? 0} items`);
|
|
248
|
+
} else {
|
|
249
|
+
console.error(result.ErrorMessage);
|
|
250
|
+
}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### Running Recommendations by Entity and Record IDs
|
|
254
|
+
|
|
255
|
+
```typescript
|
|
256
|
+
const request = new RecommendationRequest();
|
|
257
|
+
request.EntityAndRecordsInfo = {
|
|
258
|
+
EntityName: 'Products',
|
|
259
|
+
RecordIDs: ['id-1', 'id-2', 'id-3']
|
|
260
|
+
};
|
|
261
|
+
request.CurrentUser = contextUser;
|
|
262
|
+
|
|
263
|
+
const result = await engine.Recommend(request);
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### Implementing a Provider
|
|
267
|
+
|
|
268
|
+
```typescript
|
|
269
|
+
import { RecommendationProviderBase } from '@memberjunction/ai-recommendations';
|
|
270
|
+
import { RecommendationRequest, RecommendationResult } from '@memberjunction/ai-recommendations';
|
|
271
|
+
import { RegisterClass } from '@memberjunction/global';
|
|
272
|
+
import { Metadata } from '@memberjunction/core';
|
|
273
|
+
import { RecommendationItemEntity } from '@memberjunction/core-entities';
|
|
274
|
+
|
|
275
|
+
@RegisterClass(RecommendationProviderBase, 'My Recommendation Provider')
|
|
276
|
+
export class MyProvider extends RecommendationProviderBase {
|
|
277
|
+
async Recommend(request: RecommendationRequest): Promise<RecommendationResult> {
|
|
278
|
+
const result = new RecommendationResult(request);
|
|
279
|
+
const md = new Metadata();
|
|
280
|
+
|
|
281
|
+
for (const rec of request.Recommendations) {
|
|
282
|
+
// Call your recommendation API/algorithm
|
|
283
|
+
const suggestions = await this.getSuggestions(rec.SourceEntityRecordID);
|
|
284
|
+
|
|
285
|
+
const items: RecommendationItemEntity[] = [];
|
|
286
|
+
for (const suggestion of suggestions) {
|
|
287
|
+
const item = await md.GetEntityObject<RecommendationItemEntity>(
|
|
288
|
+
'Recommendation Items', request.CurrentUser
|
|
289
|
+
);
|
|
290
|
+
item.NewRecord();
|
|
291
|
+
item.DestinationEntityID = suggestion.entityID;
|
|
292
|
+
item.DestinationEntityRecordID = suggestion.recordID;
|
|
293
|
+
item.MatchProbability = suggestion.score;
|
|
294
|
+
items.push(item);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
await this.SaveRecommendation(rec, request.RunID, items);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return result;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
private async getSuggestions(recordID: string): Promise<Suggestion[]> {
|
|
304
|
+
// Your recommendation logic here
|
|
305
|
+
return [];
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
## Database Entities
|
|
311
|
+
|
|
312
|
+
```mermaid
|
|
313
|
+
erDiagram
|
|
314
|
+
RECOMMENDATION_PROVIDERS {
|
|
315
|
+
string ID PK
|
|
316
|
+
string Name
|
|
317
|
+
string Description
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
RECOMMENDATION_RUNS {
|
|
321
|
+
string ID PK
|
|
322
|
+
string RecommendationProviderID FK
|
|
323
|
+
string RunByUserID FK
|
|
324
|
+
datetime StartDate
|
|
325
|
+
string Status
|
|
326
|
+
string Description
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
RECOMMENDATIONS {
|
|
330
|
+
string ID PK
|
|
331
|
+
string RecommendationRunID FK
|
|
332
|
+
string SourceEntityID FK
|
|
333
|
+
string SourceEntityRecordID
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
RECOMMENDATION_ITEMS {
|
|
337
|
+
string ID PK
|
|
338
|
+
string RecommendationID FK
|
|
339
|
+
string DestinationEntityID FK
|
|
340
|
+
string DestinationEntityRecordID
|
|
341
|
+
float MatchProbability
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
LISTS {
|
|
345
|
+
string ID PK
|
|
346
|
+
string Name
|
|
347
|
+
string EntityID FK
|
|
348
|
+
string UserID FK
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
RECOMMENDATION_PROVIDERS ||--o{ RECOMMENDATION_RUNS : has
|
|
352
|
+
RECOMMENDATION_RUNS ||--o{ RECOMMENDATIONS : contains
|
|
353
|
+
RECOMMENDATIONS ||--o{ RECOMMENDATION_ITEMS : produces
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
## Dependencies
|
|
357
|
+
|
|
358
|
+
| Package | Purpose |
|
|
359
|
+
|---|---|
|
|
360
|
+
| `@memberjunction/core` | `BaseEngine`, `Metadata`, `RunView`, `UserInfo`, `LogStatus` |
|
|
361
|
+
| `@memberjunction/core-entities` | `RecommendationEntity`, `RecommendationRunEntity`, `RecommendationItemEntity`, `RecommendationProviderEntity`, `ListEntity` |
|
|
362
|
+
| `@memberjunction/global` | `MJGlobal` class factory for provider discovery |
|
|
363
|
+
|
|
364
|
+
## Development
|
|
365
|
+
|
|
366
|
+
```bash
|
|
367
|
+
# Build
|
|
368
|
+
npm run build
|
|
369
|
+
|
|
370
|
+
# Development mode
|
|
371
|
+
npm run start
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
## License
|
|
375
|
+
|
|
376
|
+
ISC
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@memberjunction/ai-recommendations",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "4.
|
|
4
|
+
"version": "4.1.0",
|
|
5
5
|
"description": "MemberJunction Recommendations Engine",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
@@ -20,10 +20,10 @@
|
|
|
20
20
|
"typescript": "^5.9.3"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@memberjunction/global": "4.
|
|
24
|
-
"@memberjunction/core": "4.
|
|
25
|
-
"@memberjunction/core-entities": "4.
|
|
26
|
-
"@memberjunction/ai": "4.
|
|
23
|
+
"@memberjunction/global": "4.1.0",
|
|
24
|
+
"@memberjunction/core": "4.1.0",
|
|
25
|
+
"@memberjunction/core-entities": "4.1.0",
|
|
26
|
+
"@memberjunction/ai": "4.1.0"
|
|
27
27
|
},
|
|
28
28
|
"repository": {
|
|
29
29
|
"type": "git",
|
package/readme.md
CHANGED
|
@@ -1,16 +1,54 @@
|
|
|
1
1
|
# @memberjunction/ai-recommendations
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
##
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
3
|
+
A provider-based recommendation engine for MemberJunction. Manages recommendation runs, delegates to pluggable providers via the class factory, and tracks results through Recommendation, Recommendation Run, and Recommendation Item entities.
|
|
4
|
+
|
|
5
|
+
## Architecture
|
|
6
|
+
|
|
7
|
+
```mermaid
|
|
8
|
+
graph TD
|
|
9
|
+
subgraph Engine["@memberjunction/ai-recommendations"]
|
|
10
|
+
REB["RecommendationEngineBase<br/>(singleton BaseEngine)"]
|
|
11
|
+
RPB["RecommendationProviderBase<br/>(abstract)"]
|
|
12
|
+
RR["RecommendationRequest<T>"]
|
|
13
|
+
RRES["RecommendationResult"]
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
subgraph Providers["Registered Providers"]
|
|
17
|
+
P1["Provider A"]
|
|
18
|
+
P2["Provider B"]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
subgraph MJEntities["MemberJunction Entities"]
|
|
22
|
+
RP["Recommendation Providers"]
|
|
23
|
+
RUN["Recommendation Runs"]
|
|
24
|
+
REC["Recommendations"]
|
|
25
|
+
RI["Recommendation Items"]
|
|
26
|
+
LIST["Lists / List Details"]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
subgraph MJCore["MemberJunction Core"]
|
|
30
|
+
BE["BaseEngine"]
|
|
31
|
+
CF["ClassFactory"]
|
|
32
|
+
MD["Metadata"]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
REB -->|extends| BE
|
|
36
|
+
REB -->|discovers| CF
|
|
37
|
+
CF -->|creates| P1
|
|
38
|
+
CF -->|creates| P2
|
|
39
|
+
P1 -->|extends| RPB
|
|
40
|
+
P2 -->|extends| RPB
|
|
41
|
+
REB --> RP
|
|
42
|
+
REB --> RUN
|
|
43
|
+
RPB --> REC
|
|
44
|
+
RPB --> RI
|
|
45
|
+
REB --> LIST
|
|
46
|
+
|
|
47
|
+
style Engine fill:#2d6a9f,stroke:#1a4971,color:#fff
|
|
48
|
+
style Providers fill:#2d8659,stroke:#1a5c3a,color:#fff
|
|
49
|
+
style MJEntities fill:#b8762f,stroke:#8a5722,color:#fff
|
|
50
|
+
style MJCore fill:#7c5295,stroke:#563a6b,color:#fff
|
|
51
|
+
```
|
|
14
52
|
|
|
15
53
|
## Installation
|
|
16
54
|
|
|
@@ -18,220 +56,321 @@ The MemberJunction Recommendations Engine provides a flexible and extensible fra
|
|
|
18
56
|
npm install @memberjunction/ai-recommendations
|
|
19
57
|
```
|
|
20
58
|
|
|
59
|
+
## Overview
|
|
60
|
+
|
|
61
|
+
This package provides the framework for running recommendations in MemberJunction. It follows the engine/provider pattern used throughout the platform:
|
|
62
|
+
|
|
63
|
+
1. **RecommendationEngineBase** -- a singleton engine (extending `BaseEngine`) that loads provider metadata, selects a provider, creates Recommendation Run tracking records, and delegates the actual recommendation logic
|
|
64
|
+
2. **RecommendationProviderBase** -- an abstract class that concrete providers implement to generate recommendations for each source record
|
|
65
|
+
3. **RecommendationRequest/RecommendationResult** -- typed request and response objects that flow through the pipeline
|
|
66
|
+
|
|
67
|
+
Providers are discovered at runtime through MemberJunction's `ClassFactory` using `@RegisterClass(RecommendationProviderBase, 'ProviderName')`.
|
|
68
|
+
|
|
69
|
+
## Recommendation Flow
|
|
70
|
+
|
|
71
|
+
```mermaid
|
|
72
|
+
sequenceDiagram
|
|
73
|
+
participant Caller
|
|
74
|
+
participant Engine as RecommendationEngineBase
|
|
75
|
+
participant CF as ClassFactory
|
|
76
|
+
participant Provider as RecommendationProvider
|
|
77
|
+
participant DB as MJ Database
|
|
78
|
+
|
|
79
|
+
Caller->>Engine: Recommend(request)
|
|
80
|
+
Engine->>Engine: TryThrowIfNotLoaded()
|
|
81
|
+
|
|
82
|
+
alt Provider specified
|
|
83
|
+
Engine->>Engine: Use request.Provider
|
|
84
|
+
else No provider
|
|
85
|
+
Engine->>Engine: Use first available
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
Engine->>Engine: GetRecommendationEntities(request)
|
|
89
|
+
|
|
90
|
+
alt From List
|
|
91
|
+
Engine->>DB: Load List + List Details
|
|
92
|
+
Engine->>DB: Load entity records by IDs
|
|
93
|
+
else From EntityAndRecordsInfo
|
|
94
|
+
Engine->>DB: Load records by entity name + IDs
|
|
95
|
+
else Pre-built
|
|
96
|
+
Engine->>Engine: Validate Recommendations array
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
Engine->>DB: Create Recommendation Run (Status: In Progress)
|
|
100
|
+
|
|
101
|
+
opt CreateErrorList = true
|
|
102
|
+
Engine->>DB: Create error tracking List
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
Engine->>CF: CreateInstance(provider.Name)
|
|
106
|
+
CF-->>Engine: Provider instance
|
|
107
|
+
|
|
108
|
+
Engine->>Provider: Recommend(request)
|
|
109
|
+
|
|
110
|
+
loop For each recommendation
|
|
111
|
+
Provider->>Provider: Call external API
|
|
112
|
+
Provider->>DB: SaveRecommendation + Items
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
Provider-->>Engine: RecommendationResult
|
|
116
|
+
|
|
117
|
+
Engine->>DB: Update Run (Completed/Error)
|
|
118
|
+
Engine-->>Caller: RecommendationResult
|
|
119
|
+
```
|
|
120
|
+
|
|
21
121
|
## Core Components
|
|
22
122
|
|
|
23
123
|
### RecommendationEngineBase
|
|
24
124
|
|
|
25
|
-
|
|
125
|
+
A singleton engine that manages the recommendation lifecycle.
|
|
26
126
|
|
|
27
127
|
```typescript
|
|
28
|
-
import { RecommendationEngineBase
|
|
128
|
+
import { RecommendationEngineBase } from '@memberjunction/ai-recommendations';
|
|
29
129
|
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
// Create a recommendation request
|
|
34
|
-
const request = new RecommendationRequest();
|
|
130
|
+
// Access the singleton
|
|
131
|
+
const engine = RecommendationEngineBase.Instance;
|
|
35
132
|
|
|
36
|
-
//
|
|
37
|
-
|
|
38
|
-
EntityName: 'Customers',
|
|
39
|
-
RecordIDs: ['CUST001', 'CUST002']
|
|
40
|
-
};
|
|
133
|
+
// Initialize (loads Recommendation Providers metadata)
|
|
134
|
+
await engine.Config(false, contextUser);
|
|
41
135
|
|
|
42
|
-
//
|
|
43
|
-
const result = await
|
|
136
|
+
// Run recommendations
|
|
137
|
+
const result = await engine.Recommend(request);
|
|
44
138
|
```
|
|
45
139
|
|
|
46
|
-
|
|
140
|
+
**Key properties and methods:**
|
|
47
141
|
|
|
48
|
-
|
|
142
|
+
| Member | Description |
|
|
143
|
+
|---|---|
|
|
144
|
+
| `Instance` | Static getter for the singleton instance |
|
|
145
|
+
| `RecommendationProviders` | Array of `RecommendationProviderEntity` loaded from metadata |
|
|
146
|
+
| `Config(forceRefresh?, contextUser?, provider?)` | Loads provider metadata into cache |
|
|
147
|
+
| `Recommend<T>(request)` | Runs the full recommendation pipeline |
|
|
49
148
|
|
|
50
|
-
|
|
51
|
-
import { RecommendationProviderBase, RecommendationRequest, RecommendationResult } from '@memberjunction/ai-recommendations';
|
|
52
|
-
import { UserInfo } from '@memberjunction/core';
|
|
53
|
-
import { RecommendationItemEntity } from '@memberjunction/core-entities';
|
|
149
|
+
### RecommendationProviderBase
|
|
54
150
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
// Generate items for this recommendation
|
|
67
|
-
const items: RecommendationItemEntity[] = [];
|
|
68
|
-
|
|
69
|
-
// Your recommendation logic here
|
|
70
|
-
// ...
|
|
71
|
-
|
|
72
|
-
// Save the recommendation and its items
|
|
73
|
-
await this.SaveRecommendation(recommendation, request.RunID, items);
|
|
74
|
-
}
|
|
75
|
-
} catch (error) {
|
|
76
|
-
result.AppendError(error.message);
|
|
151
|
+
Abstract base class for implementing recommendation providers.
|
|
152
|
+
|
|
153
|
+
```mermaid
|
|
154
|
+
classDiagram
|
|
155
|
+
class RecommendationProviderBase {
|
|
156
|
+
<<abstract>>
|
|
157
|
+
-_md : Metadata
|
|
158
|
+
-_ContextUser : UserInfo
|
|
159
|
+
+ContextUser : UserInfo
|
|
160
|
+
+Recommend(request)* RecommendationResult
|
|
161
|
+
#SaveRecommendation(rec, runID, items) boolean
|
|
77
162
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
163
|
+
|
|
164
|
+
class ConcreteProvider {
|
|
165
|
+
+Recommend(request) RecommendationResult
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
RecommendationProviderBase <|-- ConcreteProvider
|
|
169
|
+
|
|
170
|
+
style RecommendationProviderBase fill:#2d6a9f,stroke:#1a4971,color:#fff
|
|
171
|
+
style ConcreteProvider fill:#2d8659,stroke:#1a5c3a,color:#fff
|
|
82
172
|
```
|
|
83
173
|
|
|
84
|
-
|
|
174
|
+
The `SaveRecommendation` helper method handles:
|
|
175
|
+
1. Setting the `RecommendationRunID` on the recommendation entity
|
|
176
|
+
2. Saving the recommendation record
|
|
177
|
+
3. Linking and saving all `RecommendationItemEntity` records
|
|
85
178
|
|
|
86
|
-
###
|
|
179
|
+
### RecommendationRequest\<T\>
|
|
87
180
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
// Execute the recommendation
|
|
106
|
-
const result = await RecommendationEngineBase.Instance.Recommend(request);
|
|
107
|
-
|
|
108
|
-
if (result.Success) {
|
|
109
|
-
console.log('Recommendations generated successfully!');
|
|
110
|
-
return result.RecommendationItems;
|
|
111
|
-
} else {
|
|
112
|
-
console.error('Error generating recommendations:', result.ErrorMessage);
|
|
113
|
-
return null;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
181
|
+
The request object supports three ways to specify source records:
|
|
182
|
+
|
|
183
|
+
```mermaid
|
|
184
|
+
graph TD
|
|
185
|
+
RR["RecommendationRequest"]
|
|
186
|
+
OPT1["Recommendations[]<br/>Pre-built entities"]
|
|
187
|
+
OPT2["EntityAndRecordsInfo<br/>Entity name + Record IDs"]
|
|
188
|
+
OPT3["ListID<br/>MJ List reference"]
|
|
189
|
+
|
|
190
|
+
RR --> OPT1
|
|
191
|
+
RR --> OPT2
|
|
192
|
+
RR --> OPT3
|
|
193
|
+
|
|
194
|
+
style RR fill:#2d6a9f,stroke:#1a4971,color:#fff
|
|
195
|
+
style OPT1 fill:#2d8659,stroke:#1a5c3a,color:#fff
|
|
196
|
+
style OPT2 fill:#2d8659,stroke:#1a5c3a,color:#fff
|
|
197
|
+
style OPT3 fill:#2d8659,stroke:#1a5c3a,color:#fff
|
|
116
198
|
```
|
|
117
199
|
|
|
118
|
-
|
|
200
|
+
| Field | Type | Description |
|
|
201
|
+
|---|---|---|
|
|
202
|
+
| `Recommendations` | `RecommendationEntity[]` | Pre-built unsaved recommendation entities |
|
|
203
|
+
| `EntityAndRecordsInfo` | `{ EntityName, RecordIDs }` | Entity name and array of record IDs to process |
|
|
204
|
+
| `ListID` | `string` | ID of a MJ List whose details become the source records |
|
|
205
|
+
| `Provider` | `RecommendationProviderEntity` | Specific provider to use (defaults to first available) |
|
|
206
|
+
| `CurrentUser` | `UserInfo` | User context |
|
|
207
|
+
| `Options` | `T` | Generic additional options passed to the provider |
|
|
208
|
+
| `CreateErrorList` | `boolean` | Whether to create an error tracking list |
|
|
209
|
+
| `RunID` | `string` | Set automatically by the engine |
|
|
210
|
+
| `ErrorListID` | `string` | Set automatically if error list is created |
|
|
211
|
+
|
|
212
|
+
### RecommendationResult
|
|
119
213
|
|
|
120
214
|
```typescript
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
// Execute the recommendation
|
|
132
|
-
const result = await RecommendationEngineBase.Instance.Recommend(request);
|
|
133
|
-
|
|
134
|
-
if (result.Success) {
|
|
135
|
-
console.log('Recommendations generated successfully!');
|
|
136
|
-
console.log('Error list ID (if needed):', result.Request.ErrorListID);
|
|
137
|
-
return result.RecommendationItems;
|
|
138
|
-
} else {
|
|
139
|
-
console.error('Error generating recommendations:', result.ErrorMessage);
|
|
140
|
-
return null;
|
|
141
|
-
}
|
|
215
|
+
class RecommendationResult {
|
|
216
|
+
Request: RecommendationRequest;
|
|
217
|
+
RecommendationRun?: RecommendationRunEntity;
|
|
218
|
+
RecommendationItems?: RecommendationItemEntity[];
|
|
219
|
+
Success: boolean;
|
|
220
|
+
ErrorMessage: string;
|
|
221
|
+
|
|
222
|
+
AppendWarning(message: string): void; // Adds warning without setting Success=false
|
|
223
|
+
AppendError(message: string): void; // Adds error and sets Success=false
|
|
224
|
+
GetErrorMessages(): string[]; // Splits ErrorMessage into array
|
|
142
225
|
}
|
|
143
226
|
```
|
|
144
227
|
|
|
145
|
-
|
|
228
|
+
## Usage
|
|
229
|
+
|
|
230
|
+
### Running Recommendations from a List
|
|
146
231
|
|
|
147
232
|
```typescript
|
|
148
|
-
import { RecommendationEngineBase
|
|
233
|
+
import { RecommendationEngineBase } from '@memberjunction/ai-recommendations';
|
|
234
|
+
import { RecommendationRequest } from '@memberjunction/ai-recommendations';
|
|
149
235
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
236
|
+
const engine = RecommendationEngineBase.Instance;
|
|
237
|
+
await engine.Config(false, contextUser);
|
|
238
|
+
|
|
239
|
+
const request = new RecommendationRequest();
|
|
240
|
+
request.ListID = 'list-uuid';
|
|
241
|
+
request.CurrentUser = contextUser;
|
|
242
|
+
request.CreateErrorList = true;
|
|
156
243
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
EntityName: 'Customers',
|
|
164
|
-
RecordIDs: [customerId]
|
|
165
|
-
};
|
|
166
|
-
|
|
167
|
-
// Add provider-specific options
|
|
168
|
-
request.Options = {
|
|
169
|
-
similarityThreshold: 0.75,
|
|
170
|
-
maxRecommendations: 5,
|
|
171
|
-
includeRatings: true
|
|
172
|
-
};
|
|
173
|
-
|
|
174
|
-
// Execute the recommendation
|
|
175
|
-
return await RecommendationEngineBase.Instance.Recommend(request);
|
|
244
|
+
const result = await engine.Recommend(request);
|
|
245
|
+
|
|
246
|
+
if (result.Success) {
|
|
247
|
+
console.log(`Generated ${result.RecommendationItems?.length ?? 0} items`);
|
|
248
|
+
} else {
|
|
249
|
+
console.error(result.ErrorMessage);
|
|
176
250
|
}
|
|
177
251
|
```
|
|
178
252
|
|
|
179
|
-
|
|
253
|
+
### Running Recommendations by Entity and Record IDs
|
|
180
254
|
|
|
181
|
-
|
|
255
|
+
```typescript
|
|
256
|
+
const request = new RecommendationRequest();
|
|
257
|
+
request.EntityAndRecordsInfo = {
|
|
258
|
+
EntityName: 'Products',
|
|
259
|
+
RecordIDs: ['id-1', 'id-2', 'id-3']
|
|
260
|
+
};
|
|
261
|
+
request.CurrentUser = contextUser;
|
|
182
262
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
3. **Provider Selection**: The appropriate recommendation provider is selected
|
|
186
|
-
4. **Recommendation Generation**: The provider generates recommendations for each requested record
|
|
187
|
-
5. **Result Storage**: Recommendations and items are saved to the database
|
|
188
|
-
6. **Status Update**: The run status is updated (completed or error)
|
|
189
|
-
7. **Result Return**: The `RecommendationResult` is returned to the caller
|
|
263
|
+
const result = await engine.Recommend(request);
|
|
264
|
+
```
|
|
190
265
|
|
|
191
|
-
|
|
266
|
+
### Implementing a Provider
|
|
192
267
|
|
|
193
|
-
|
|
268
|
+
```typescript
|
|
269
|
+
import { RecommendationProviderBase } from '@memberjunction/ai-recommendations';
|
|
270
|
+
import { RecommendationRequest, RecommendationResult } from '@memberjunction/ai-recommendations';
|
|
271
|
+
import { RegisterClass } from '@memberjunction/global';
|
|
272
|
+
import { Metadata } from '@memberjunction/core';
|
|
273
|
+
import { RecommendationItemEntity } from '@memberjunction/core-entities';
|
|
194
274
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
275
|
+
@RegisterClass(RecommendationProviderBase, 'My Recommendation Provider')
|
|
276
|
+
export class MyProvider extends RecommendationProviderBase {
|
|
277
|
+
async Recommend(request: RecommendationRequest): Promise<RecommendationResult> {
|
|
278
|
+
const result = new RecommendationResult(request);
|
|
279
|
+
const md = new Metadata();
|
|
280
|
+
|
|
281
|
+
for (const rec of request.Recommendations) {
|
|
282
|
+
// Call your recommendation API/algorithm
|
|
283
|
+
const suggestions = await this.getSuggestions(rec.SourceEntityRecordID);
|
|
284
|
+
|
|
285
|
+
const items: RecommendationItemEntity[] = [];
|
|
286
|
+
for (const suggestion of suggestions) {
|
|
287
|
+
const item = await md.GetEntityObject<RecommendationItemEntity>(
|
|
288
|
+
'Recommendation Items', request.CurrentUser
|
|
289
|
+
);
|
|
290
|
+
item.NewRecord();
|
|
291
|
+
item.DestinationEntityID = suggestion.entityID;
|
|
292
|
+
item.DestinationEntityRecordID = suggestion.recordID;
|
|
293
|
+
item.MatchProbability = suggestion.score;
|
|
294
|
+
items.push(item);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
await this.SaveRecommendation(rec, request.RunID, items);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return result;
|
|
301
|
+
}
|
|
199
302
|
|
|
200
|
-
|
|
303
|
+
private async getSuggestions(recordID: string): Promise<Suggestion[]> {
|
|
304
|
+
// Your recommendation logic here
|
|
305
|
+
return [];
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
```
|
|
201
309
|
|
|
202
|
-
|
|
310
|
+
## Database Entities
|
|
203
311
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
312
|
+
```mermaid
|
|
313
|
+
erDiagram
|
|
314
|
+
RECOMMENDATION_PROVIDERS {
|
|
315
|
+
string ID PK
|
|
316
|
+
string Name
|
|
317
|
+
string Description
|
|
318
|
+
}
|
|
207
319
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
320
|
+
RECOMMENDATION_RUNS {
|
|
321
|
+
string ID PK
|
|
322
|
+
string RecommendationProviderID FK
|
|
323
|
+
string RunByUserID FK
|
|
324
|
+
datetime StartDate
|
|
325
|
+
string Status
|
|
326
|
+
string Description
|
|
327
|
+
}
|
|
212
328
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
329
|
+
RECOMMENDATIONS {
|
|
330
|
+
string ID PK
|
|
331
|
+
string RecommendationRunID FK
|
|
332
|
+
string SourceEntityID FK
|
|
333
|
+
string SourceEntityRecordID
|
|
334
|
+
}
|
|
219
335
|
|
|
220
|
-
|
|
336
|
+
RECOMMENDATION_ITEMS {
|
|
337
|
+
string ID PK
|
|
338
|
+
string RecommendationID FK
|
|
339
|
+
string DestinationEntityID FK
|
|
340
|
+
string DestinationEntityRecordID
|
|
341
|
+
float MatchProbability
|
|
342
|
+
}
|
|
221
343
|
|
|
222
|
-
|
|
344
|
+
LISTS {
|
|
345
|
+
string ID PK
|
|
346
|
+
string Name
|
|
347
|
+
string EntityID FK
|
|
348
|
+
string UserID FK
|
|
349
|
+
}
|
|
223
350
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
351
|
+
RECOMMENDATION_PROVIDERS ||--o{ RECOMMENDATION_RUNS : has
|
|
352
|
+
RECOMMENDATION_RUNS ||--o{ RECOMMENDATIONS : contains
|
|
353
|
+
RECOMMENDATIONS ||--o{ RECOMMENDATION_ITEMS : produces
|
|
354
|
+
```
|
|
227
355
|
|
|
228
356
|
## Dependencies
|
|
229
357
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
358
|
+
| Package | Purpose |
|
|
359
|
+
|---|---|
|
|
360
|
+
| `@memberjunction/core` | `BaseEngine`, `Metadata`, `RunView`, `UserInfo`, `LogStatus` |
|
|
361
|
+
| `@memberjunction/core-entities` | `RecommendationEntity`, `RecommendationRunEntity`, `RecommendationItemEntity`, `RecommendationProviderEntity`, `ListEntity` |
|
|
362
|
+
| `@memberjunction/global` | `MJGlobal` class factory for provider discovery |
|
|
363
|
+
|
|
364
|
+
## Development
|
|
365
|
+
|
|
366
|
+
```bash
|
|
367
|
+
# Build
|
|
368
|
+
npm run build
|
|
369
|
+
|
|
370
|
+
# Development mode
|
|
371
|
+
npm run start
|
|
372
|
+
```
|
|
234
373
|
|
|
235
374
|
## License
|
|
236
375
|
|
|
237
|
-
ISC
|
|
376
|
+
ISC
|