reveal-sdk-node-ai 1.2.2-rc.1 → 1.3.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/index.d.ts CHANGED
@@ -80,6 +80,14 @@ export interface AIRequestJson {
80
80
  /** The stable engine intent that produced this call (e.g. 'generation-build-report'). */
81
81
  intent: string;
82
82
  userContext: AIUserContextJson | null;
83
+ /**
84
+ * Present only when a message has a stable prefix: one entry per message (same order as `messages`), the number of
85
+ * leading characters of its `content` (UTF-16 code units, as JavaScript's `length` counts) that stay the same from one
86
+ * request to the next (a prompt's instructions and schema), or null. A hint for services with explicit prompt caching,
87
+ * which can mark that prefix as a cache point; ignore it otherwise. Kept out of the messages, which can be passed to an
88
+ * LLM client as they are.
89
+ */
90
+ stablePrefixLengths?: (number | null)[];
83
91
  }
84
92
 
85
93
  /** What a custom AI provider callback returns (v2 contract). */
@@ -113,11 +121,21 @@ export interface AIUsageRecordJson {
113
121
  inputTokens?: number | null;
114
122
  outputTokens?: number | null;
115
123
  totalTokens?: number | null;
124
+ /** Tokens spent on internal reasoning; null when the provider reported none. */
125
+ reasoningTokens?: number | null;
126
+ /** Input tokens served from the provider's prompt cache; null when the provider reported none. */
127
+ cachedInputTokens?: number | null;
116
128
  durationMs: number;
117
129
  finishReason?: 'Stop' | 'Length' | 'ContentFilter' | 'Other' | null;
118
130
  succeeded: boolean;
119
131
  /** 'provider_error' | 'timeout' | 'cancelled' | 'denied' when succeeded is false. */
120
132
  errorCode?: string | null;
133
+ /** The stage of a multi-step feature (e.g. 'DefineWidgets' in dashboard generation); null outside one. */
134
+ stage?: string | null;
135
+ /** Within `stage`: 1 for its first call, 2 and up for repair calls. */
136
+ attempt?: number | null;
137
+ /** The top-level operation this call ran under, when it differs from `operationId` (e.g. datasource ranking during a generation). */
138
+ parentOperationId?: string | null;
121
139
  }
122
140
 
123
141
  /** What `quotaGate` receives before every gated AI call. The user's identity and any
@@ -203,9 +221,32 @@ export interface MetadataManagerCallbacks {
203
221
  upsertFieldOverride?: (userContext: any, datasourceId: string, datasourceItemId: string | null, database: string, table: string, fieldName: string, fieldOverride: any) => Promise<void>;
204
222
  deleteFieldOverride?: (userContext: any, datasourceId: string, datasourceItemId: string | null, database: string, table: string, fieldName: string) => Promise<boolean>;
205
223
  upsertTableDescription?: (userContext: any, datasourceId: string, datasourceItemId: string | null, database: string, table: string, description: string | null, appliesTo?: string[] | null) => Promise<void>;
206
- getRuntimeOverrides?: (userContext: any, datasourceId?: string | null, datasourceItemId?: string | null) => Promise<any | null>;
224
+ /** The plugin passes the user context again as the first argument after `userContext` (`userContextArg`). */
225
+ getRuntimeOverrides?: (userContext: any, userContextArg: any, datasourceId?: string | null, datasourceItemId?: string | null) => Promise<any | null>;
207
226
  getFullMetadata?: (userContext: any, datasourceId: string, datasourceItemId?: string | null) => Promise<any | null>;
208
- rankDatasourcesForPrompt?: (userContext: any, userPrompt: string, maxResults?: number) => Promise<any>;
227
+ /**
228
+ * Ranks the catalog datasources against a prompt. The dashboard generator uses it to shortlist the
229
+ * datasources a chat request without `datasourceId` is built from (see `RevealAIPluginOptions` for which
230
+ * datasources the AI may then see). Optional: when it is missing, the
231
+ * plugin ranks with the same built-in ranker as the C# SDK, reading this manager's
232
+ * `getDatasourceList`, `getMetadataIndex` and `getMetadata`. It calls `getMetadata` with no
233
+ * `currentModel`, so it ranks on what that handler returns for the request's user context
234
+ * (overrides and any model filtering included), and leaves out a datasource whose index is
235
+ * missing or fails to load. Implement it only to rank with your own logic; when it fails, the AI
236
+ * is shown every datasource instead. A stub that returns no candidates also leaves the AI with
237
+ * every datasource: delete it to get the built-in ranker. The plugin passes the user context
238
+ * again after `userPrompt` (`userContextArg`).
239
+ *
240
+ * Resolve to `{ candidates: [{ datasourceId, datasourceItemId?, score, rationale?, relevantTables? }] }`,
241
+ * or `{ candidates: [], error }` on failure:
242
+ * - `datasourceId` / `datasourceItemId`: the catalog entry's `Id` / `ItemId` exactly (omit the
243
+ * item id when the entry has none); a candidate that matches no entry is ignored.
244
+ * - `score`: 0..1; the shortlist is ordered by it.
245
+ * - `relevantTables`: "database.table", with the real names as the metadata index lists them,
246
+ * not their aliases (e.g. `'NorthwindCloud.dbo.Orders'`). The AI is shown each table's alias;
247
+ * an entry without its database is shown as is.
248
+ */
249
+ rankDatasourcesForPrompt?: (userContext: any, userPrompt: string, userContextArg: any, maxResults: number) => Promise<any>;
209
250
  }
210
251
 
211
252
  /**
@@ -284,6 +325,46 @@ export type MetadataCatalogProviderCallback = (
284
325
  userContext?: any
285
326
  ) => MetadataCatalogJson | Promise<MetadataCatalogJson>;
286
327
 
328
+ /**
329
+ * A single custom date filter rule declared by the host — a relative window such as "Next 7 Days"
330
+ * or "Last 90 Days". Mirrors the C# `AIDateFilterRule`. Only `Last` and `Next` relations are
331
+ * honored (other relations overlap the built-in options and are ignored).
332
+ */
333
+ export interface AIDateFilterRuleJson {
334
+ /** The window's relation to today. Only `'Last'` and `'Next'` are honored. */
335
+ Relation: 'Last' | 'Next';
336
+ /** How many `Period`s the window spans (e.g. 7 for "Next 7 Days"). Must be positive. */
337
+ Count: number;
338
+ /** The unit of the window. */
339
+ Period: 'Day' | 'Week' | 'Month' | 'Quarter' | 'Semester' | 'Year';
340
+ /**
341
+ * Whether today is included in the window, or omitted for the default. Honored for `Last` rules,
342
+ * which include today by default, and for `Next` rules over `'Day'`, which start tomorrow by default
343
+ * (with true, "Next 7 Days" starts today instead). Ignored for other periods.
344
+ */
345
+ IncludeToday?: boolean | null;
346
+ /**
347
+ * Optional canonical name the AI uses to select this rule. When omitted, a stable name is
348
+ * derived from the relation, count and period (e.g. "Next7Days"). Names that collide with a
349
+ * built-in date option are ignored in favor of the built-in.
350
+ */
351
+ Name?: string | null;
352
+ /**
353
+ * Optional human-readable description surfaced to the AI (falls back to a generated phrase).
354
+ */
355
+ Description?: string | null;
356
+ }
357
+
358
+ /**
359
+ * Node.js-side implementation of IAIDateFilterProvider. Returns the host's custom date menu
360
+ * options so the AI can offer them alongside the built-ins when generating or editing dashboards.
361
+ * Invoked once per generation/edit with no arguments other than the request's user context (so the
362
+ * rules can vary per user/tenant). Return an empty array when there are none.
363
+ */
364
+ export type DateFilterProviderCallback = (
365
+ userContext?: any
366
+ ) => AIDateFilterRuleJson[] | Promise<AIDateFilterRuleJson[]>;
367
+
287
368
  /**
288
369
  * Node.js-side implementation of IMetadataStorageProvider — the persistence layer the built-in
289
370
  * file-based MetadataManager reads and writes through. Implement the operations your host supports
@@ -292,6 +373,16 @@ export type MetadataCatalogProviderCallback = (
292
373
  * unchanged from `getById` (deserialized back into the requesting C# type). The first argument of
293
374
  * every handler is the request's user context, for multi-tenant scoping.
294
375
  *
376
+ * A handler may filter what that user sees, but the answer of `getById` and `list` must depend only
377
+ * on the id (or pattern) and the user context: within one dashboard generation or edit, each item
378
+ * and listing is fetched once for the request's user and the request's later reads reuse a copy of
379
+ * that answer. Nothing is reused across requests or users; a metadata write Reveal makes, from any
380
+ * request, makes running requests fetch again, and a change your application makes to the storage
381
+ * directly is seen from the next request. Writes are made under the system
382
+ * user context (`reveal-ai-metadata-user` by default), and some write back a document just read, so
383
+ * return unfiltered documents to it. The index document carries every table's alias and
384
+ * description; filter it like the per-table documents.
385
+ *
295
386
  * When this object is provided, the C# plugin replaces its default JSONMetadataStorageProvider with
296
387
  * MetadataStorageProvider, which proxies every call to these handlers. Independent of
297
388
  * `metadataManager` (which replaces the whole IMetadataManager and so bypasses storage entirely).
@@ -330,7 +421,8 @@ export interface Callbacks {
330
421
  * Observability export: receives one {@link AIUsageRecordJson} per AI call (success or
331
422
  * failure, foreground or background) — "save the usage log anywhere". One-way — the return
332
423
  * value is ignored and failures never affect AI calls. Not a billing seam: plan checks,
333
- * budgets, and debiting tokens belong on `quotaGate`/`quotaSettle`.
424
+ * budgets, and debiting tokens belong on `quotaGate`/`quotaSettle`. The sink is not called
425
+ * within a request, so `userContext` is always null; the record's `userId` identifies the user.
334
426
  */
335
427
  usageSink?: (record: AIUsageRecordJson, userContext?: any) => void | Promise<void>;
336
428
  /**
@@ -346,8 +438,10 @@ export interface Callbacks {
346
438
  */
347
439
  quotaSettle?: (settlement: { context: AICallContextJson; usage: AIUsageRecordJson }, userContext?: any) => void | Promise<void>;
348
440
  /**
349
- * Plan gating: return the subset of `candidateProfiles` this user may use. Malformed results
350
- * or exceptions deny all profiles (fail closed); names outside the candidate set are ignored.
441
+ * Plan gating: return the subset of `candidateProfiles` this user may use. Called with every
442
+ * profile for the profile listing, and with just the requested profile to validate a request
443
+ * (leaving it out returns 403). Malformed results or exceptions deny all profiles (fail
444
+ * closed); names outside the candidate set are ignored.
351
445
  */
352
446
  profileAuthorizer?: (request: { user: AIUserContextJson | null; candidateProfiles: string[] }, userContext?: any) => string[] | Promise<string[]>;
353
447
  /**
@@ -368,6 +462,12 @@ export interface Callbacks {
368
462
  * — configure exactly one catalog source (the plugin throws at startup if both are set).
369
463
  */
370
464
  metadataCatalogProvider?: MetadataCatalogProviderCallback;
465
+ /**
466
+ * Node.js-side IAIDateFilterProvider implementation. Declares the host's custom date menu
467
+ * options ("Next 7 Days", "Last 90 Days") server-side so the AI recognizes them when generating
468
+ * or editing dashboards. Mirror the same rules into the client's `onDateFilterMenuOpening`.
469
+ */
470
+ dateFilterProvider?: DateFilterProviderCallback;
371
471
  /**
372
472
  * Node.js-side IMetadataStorageProvider implementation. Swaps the built-in file-based metadata
373
473
  * persistence for a custom backend (database, cloud storage, ...) while leaving the rest of the
@@ -376,6 +476,17 @@ export interface Callbacks {
376
476
  metadataStorageProvider?: MetadataStorageProviderCallbacks;
377
477
  }
378
478
 
479
+ /**
480
+ * Options for the Reveal AI plugin.
481
+ *
482
+ * A chat request may omit `datasourceId`: the AI then picks one or more datasources from the catalog
483
+ * (`metadataCatalogFile`, or `getDatasourceList` of a `metadataManager` callback) and may mix and join them in
484
+ * one dashboard. The request comes from the browser client, so the server can't rely on it naming a datasource.
485
+ * The AI only considers the datasources whose metadata index the request's user can read: every metadata read
486
+ * gets the user context. A host that restricts datasources per user must therefore also filter its metadata
487
+ * per user (or tenant) in its metadata storage or manager callbacks, returning no index for the datasources a
488
+ * user can't use; then nothing about them reaches the AI.
489
+ */
379
490
  export interface RevealAIPluginOptions {
380
491
  /** Absolute path to AIPlugin.dll (defaults to the bundled native binaries). */
381
492
  assemblyPath?: string;
package/index.js CHANGED
@@ -17,6 +17,27 @@ function wrapJsonCallback(handler) {
17
17
  };
18
18
  }
19
19
 
20
+ /**
21
+ * The engine only receives the user's id from the SDK, so the hook payloads it sends carry
22
+ * `user.properties: null`. Fill them in from the user context the SDK resolved for the request
23
+ * (passed to every callback), so per-request claims are on `user.properties` as documented.
24
+ * Only done when that context is the payload's user: system calls carry the plugin's own user.
25
+ */
26
+ function withUserProperties(payload, userContext) {
27
+ const user = payload?.user ?? payload?.context?.user;
28
+ const properties = userContext?.properties;
29
+ if (!user || user.properties != null || properties == null || user.userId !== userContext.userId) {
30
+ return payload;
31
+ }
32
+ user.properties = properties instanceof Map ? Object.fromEntries(properties) : properties;
33
+ return payload;
34
+ }
35
+
36
+ /** wrapJsonCallback for the hooks whose payload carries the user (gate, authorizer, resolver). */
37
+ function wrapUserJsonCallback(handler) {
38
+ return wrapJsonCallback((payload, userContext) => handler(withUserProperties(payload, userContext), userContext));
39
+ }
40
+
20
41
  /**
21
42
  * Same as wrapJsonCallback for one-way callbacks: the return value is ignored by the plugin.
22
43
  */
@@ -93,6 +114,7 @@ function withOptions(options = {}) {
93
114
  aiProvider,
94
115
  metadataManager: metadataManagerCallbacks,
95
116
  metadataCatalogProvider,
117
+ dateFilterProvider,
96
118
  metadataStorageProvider: metadataStorageProviderCallbacks,
97
119
  usageSink, quotaGate, quotaSettle, profileAuthorizer, connectionResolver,
98
120
  ...otherCallbacks
@@ -130,10 +152,12 @@ function withOptions(options = {}) {
130
152
  // v2 hook callbacks: always object-friendly (parsed payload in, plain value out). Their
131
153
  // presence is signaled to the plugin via has* flags.
132
154
  if (typeof usageSink === 'function') callbacks.usageSink = wrapOneWayJsonCallback(usageSink);
133
- if (typeof quotaGate === 'function') callbacks.quotaGate = wrapJsonCallback(quotaGate);
134
- if (typeof quotaSettle === 'function') callbacks.quotaSettle = wrapOneWayJsonCallback(quotaSettle);
135
- if (typeof profileAuthorizer === 'function') callbacks.profileAuthorizer = wrapJsonCallback(profileAuthorizer);
136
- if (typeof connectionResolver === 'function') callbacks.connectionResolver = wrapJsonCallback(connectionResolver);
155
+ if (typeof quotaGate === 'function') callbacks.quotaGate = wrapUserJsonCallback(quotaGate);
156
+ if (typeof quotaSettle === 'function') {
157
+ callbacks.quotaSettle = wrapOneWayJsonCallback((payload, userContext) => quotaSettle(withUserProperties(payload, userContext), userContext));
158
+ }
159
+ if (typeof profileAuthorizer === 'function') callbacks.profileAuthorizer = wrapUserJsonCallback(profileAuthorizer);
160
+ if (typeof connectionResolver === 'function') callbacks.connectionResolver = wrapUserJsonCallback(connectionResolver);
137
161
  parameters.hasUsageSink = typeof usageSink === 'function';
138
162
  parameters.hasQuotaGate = typeof quotaGate === 'function';
139
163
  parameters.hasQuotaSettle = typeof quotaSettle === 'function';
@@ -159,6 +183,22 @@ function withOptions(options = {}) {
159
183
  parameters.hasMetadataCatalogProvider = true;
160
184
  }
161
185
 
186
+ // IAIDateFilterProvider bridge — a single getFilterRules() method. When
187
+ // provided, the C# plugin registers DateFilterProvider, which proxies
188
+ // IAIDateFilterProvider.GetFilterRulesAsync() to this handler so the AI can
189
+ // offer the host's custom date menu options ("Next 7 Days", "Last 90 Days"). The payload
190
+ // carries no arguments, so only the userContext is forwarded; return the array of rules.
191
+ if (dateFilterProvider) {
192
+ if (typeof dateFilterProvider !== 'function') {
193
+ throw new Error('callbacks.dateFilterProvider must be a function returning the custom date filter rules (it may be sync or async)');
194
+ }
195
+ callbacks.dateFilterProvider = async (userContext, _message) => {
196
+ const rules = await dateFilterProvider(userContext);
197
+ return JSON.stringify(rules ?? []);
198
+ };
199
+ parameters.hasDateFilterProvider = true;
200
+ }
201
+
162
202
  // IMetadataManager bridge — dispatches by methodName from EndpointMethodCallModel.
163
203
  if (metadataManagerCallbacks) {
164
204
  if (typeof metadataManagerCallbacks !== 'object' || typeof metadataManagerCallbacks === 'function') {
@@ -166,7 +206,12 @@ function withOptions(options = {}) {
166
206
  }
167
207
 
168
208
  // Tell the C# side to use MetadataManagerProvider instead of the file-based MetadataManager.
169
- parameters.metadataManager = Object.assign({}, parameters.metadataManager, { useNodeProvider: true });
209
+ // Without a rankDatasourcesForPrompt handler, the C# side ranks datasources with the
210
+ // built-in ranker over this manager's getDatasourceList / getMetadataIndex / getMetadata.
211
+ parameters.metadataManager = Object.assign({}, parameters.metadataManager, {
212
+ useNodeProvider: true,
213
+ hostRanksDatasources: typeof metadataManagerCallbacks.rankDatasourcesForPrompt === 'function',
214
+ });
170
215
 
171
216
  callbacks.metadataManager = async (userContext, message) => {
172
217
  let methodName, args;
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reveal-sdk-node-ai",
3
- "version": "1.2.2-rc.1",
3
+ "version": "1.3.0",
4
4
  "description": "Reveal SDK AI plugin for Node.js — adds AI capabilities to Reveal BI dashboards",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -23,6 +23,6 @@
23
23
  "node": ">=16.0.0"
24
24
  },
25
25
  "dependencies": {
26
- "reveal-sdk-node": ">=2.2.1"
26
+ "reveal-sdk-node": ">=2.3.0"
27
27
  }
28
28
  }