graphile-llm 0.11.5 → 0.12.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/__tests__/graphile-llm.test.js +318 -37
- package/__tests__/unified-search-embedding.test.d.ts +10 -0
- package/__tests__/unified-search-embedding.test.js +176 -0
- package/embedder.d.ts +24 -4
- package/embedder.js +36 -7
- package/env.d.ts +6 -28
- package/env.js +10 -47
- package/esm/__tests__/graphile-llm.test.js +319 -38
- package/esm/__tests__/unified-search-embedding.test.d.ts +10 -0
- package/esm/__tests__/unified-search-embedding.test.js +174 -0
- package/esm/embedder.d.ts +24 -4
- package/esm/embedder.js +35 -7
- package/esm/env.d.ts +6 -28
- package/esm/env.js +5 -46
- package/esm/index.d.ts +5 -4
- package/esm/index.js +3 -4
- package/esm/metering.d.ts +1 -3
- package/esm/metering.js +48 -108
- package/esm/plugins/llm-module-plugin.d.ts +9 -4
- package/esm/plugins/llm-module-plugin.js +9 -4
- package/esm/plugins/rag-plugin.js +30 -20
- package/esm/plugins/text-mutation-plugin.js +15 -2
- package/esm/plugins/text-search-plugin.d.ts +15 -3
- package/esm/plugins/text-search-plugin.js +94 -23
- package/esm/preset.d.ts +13 -2
- package/esm/preset.js +15 -4
- package/esm/types.d.ts +13 -0
- package/index.d.ts +5 -4
- package/index.js +6 -2
- package/metering.d.ts +1 -3
- package/metering.js +48 -108
- package/package.json +6 -4
- package/plugins/llm-module-plugin.d.ts +9 -4
- package/plugins/llm-module-plugin.js +9 -4
- package/plugins/rag-plugin.js +30 -20
- package/plugins/text-mutation-plugin.js +15 -2
- package/plugins/text-search-plugin.d.ts +15 -3
- package/plugins/text-search-plugin.js +95 -23
- package/preset.d.ts +13 -2
- package/preset.js +15 -4
- package/types.d.ts +13 -0
|
@@ -11,10 +11,15 @@
|
|
|
11
11
|
* to consume.
|
|
12
12
|
*
|
|
13
13
|
* Resolution order for the embedder:
|
|
14
|
-
* 1. `
|
|
15
|
-
* 2.
|
|
16
|
-
* 3.
|
|
17
|
-
*
|
|
14
|
+
* 1. `defaultEmbedder` from preset options (build-time)
|
|
15
|
+
* 2. Environment variables (EMBEDDER_PROVIDER, EMBEDDER_MODEL, EMBEDDER_BASE_URL)
|
|
16
|
+
* 3. null — LLM features are disabled
|
|
17
|
+
*
|
|
18
|
+
* Per-database model/baseUrl overrides (from `llm_module` via `ctx.useLlm()`)
|
|
19
|
+
* are applied at request time via `llmConfigStore` (AsyncLocalStorage) in
|
|
20
|
+
* the text-search-plugin resolver wrapper. The same embedder function — and
|
|
21
|
+
* its metering wrapper — handles every request; only the model name and
|
|
22
|
+
* base URL parameters change per-tenant.
|
|
18
23
|
*
|
|
19
24
|
* This plugin is intentionally pure — no billing or metering logic.
|
|
20
25
|
* The optional LlmMeteringPlugin wraps the embedder with billing integration
|
|
@@ -70,18 +70,18 @@ function parseHasChunksTag(raw, codec) {
|
|
|
70
70
|
*/
|
|
71
71
|
function discoverChunkTables(build) {
|
|
72
72
|
const chunkTables = [];
|
|
73
|
-
const pgRegistry = build.pgRegistry;
|
|
73
|
+
const pgRegistry = build.input?.pgRegistry ?? build.pgRegistry;
|
|
74
74
|
if (!pgRegistry)
|
|
75
75
|
return chunkTables;
|
|
76
76
|
// Scan all codecs for @hasChunks smart tag
|
|
77
|
-
for (const
|
|
78
|
-
const
|
|
79
|
-
if (!
|
|
77
|
+
for (const codec of Object.values(pgRegistry.pgCodecs || {})) {
|
|
78
|
+
const c = codec;
|
|
79
|
+
if (!c?.attributes)
|
|
80
80
|
continue;
|
|
81
|
-
const tags =
|
|
81
|
+
const tags = c.extensions?.tags;
|
|
82
82
|
if (!tags?.hasChunks)
|
|
83
83
|
continue;
|
|
84
|
-
const info = parseHasChunksTag(tags.hasChunks,
|
|
84
|
+
const info = parseHasChunksTag(tags.hasChunks, c);
|
|
85
85
|
if (info) {
|
|
86
86
|
chunkTables.push(info);
|
|
87
87
|
}
|
|
@@ -135,18 +135,6 @@ export function createLlmRagPlugin(ragDefaults = {}) {
|
|
|
135
135
|
let embedder = null;
|
|
136
136
|
let chatCompleter = null;
|
|
137
137
|
const schemaExtension = extendSchema((build) => {
|
|
138
|
-
// Discover chunk-aware tables from pgRegistry
|
|
139
|
-
chunkTables = discoverChunkTables(build);
|
|
140
|
-
embedder = build.llmEmbedder || null;
|
|
141
|
-
chatCompleter = build.llmChatCompleter || null;
|
|
142
|
-
if (chunkTables.length > 0) {
|
|
143
|
-
console.log(`[graphile-llm] RAG plugin discovered ${chunkTables.length} chunk-aware table(s): ` +
|
|
144
|
-
chunkTables.map((t) => t.parentCodecName).join(', '));
|
|
145
|
-
}
|
|
146
|
-
else {
|
|
147
|
-
console.log('[graphile-llm] RAG plugin found no @hasChunks tables. ' +
|
|
148
|
-
'ragQuery will still work if chunks tables are queried directly.');
|
|
149
|
-
}
|
|
150
138
|
return {
|
|
151
139
|
typeDefs: gql `
|
|
152
140
|
"""A source chunk retrieved during RAG context assembly."""
|
|
@@ -325,8 +313,8 @@ export function createLlmRagPlugin(ragDefaults = {}) {
|
|
|
325
313
|
}
|
|
326
314
|
}
|
|
327
315
|
};
|
|
328
|
-
});
|
|
329
|
-
|
|
316
|
+
}, 'LlmRagPlugin');
|
|
317
|
+
const plugin = {
|
|
330
318
|
...schemaExtension,
|
|
331
319
|
name: 'LlmRagPlugin',
|
|
332
320
|
version: '0.1.0',
|
|
@@ -338,4 +326,26 @@ export function createLlmRagPlugin(ragDefaults = {}) {
|
|
|
338
326
|
'VectorCodecPlugin'
|
|
339
327
|
]
|
|
340
328
|
};
|
|
329
|
+
// Wrap the build hook to also discover chunk tables.
|
|
330
|
+
// The build hook runs after all init hooks (including smart tag injection),
|
|
331
|
+
// so @hasChunks tags are guaranteed to be visible.
|
|
332
|
+
const existingBuildHook = plugin.schema.hooks.build;
|
|
333
|
+
plugin.schema.hooks.build = (build) => {
|
|
334
|
+
// Run extendSchema's build hook first (sets up graphql ref)
|
|
335
|
+
build = existingBuildHook(build) || build;
|
|
336
|
+
// Discover chunk tables — runs during build phase when all smart tags are applied
|
|
337
|
+
chunkTables = discoverChunkTables(build);
|
|
338
|
+
embedder = build.llmEmbedder || null;
|
|
339
|
+
chatCompleter = build.llmChatCompleter || null;
|
|
340
|
+
if (chunkTables.length > 0) {
|
|
341
|
+
console.log(`[graphile-llm] RAG plugin discovered ${chunkTables.length} chunk-aware table(s): ` +
|
|
342
|
+
chunkTables.map((t) => t.parentCodecName).join(', '));
|
|
343
|
+
}
|
|
344
|
+
else {
|
|
345
|
+
console.log('[graphile-llm] RAG plugin found no @hasChunks tables. ' +
|
|
346
|
+
'ragQuery will still work if chunks tables are queried directly.');
|
|
347
|
+
}
|
|
348
|
+
return build;
|
|
349
|
+
};
|
|
350
|
+
return plugin;
|
|
341
351
|
}
|
|
@@ -83,8 +83,21 @@ export function createLlmTextMutationPlugin() {
|
|
|
83
83
|
*/
|
|
84
84
|
GraphQLInputObjectType_fields(fields, build, context) {
|
|
85
85
|
const { scope: { isPgPatch, isPgBaseInput, isMutationInput, pgCodec } } = context;
|
|
86
|
-
// Only intercept
|
|
87
|
-
|
|
86
|
+
// Only intercept mutation-related input types for table rows.
|
|
87
|
+
// PostGraphile v5 sets isPgPatch on update types, isPgBaseInput on
|
|
88
|
+
// create base types. For tables with `serial PRIMARY KEY`, the create
|
|
89
|
+
// input type (e.g. ArticleInput) has pgCodec set but none of the
|
|
90
|
+
// explicit mutation scope flags — detect via type name convention.
|
|
91
|
+
if (!pgCodec?.attributes)
|
|
92
|
+
return fields;
|
|
93
|
+
const typeName = context.Self.name;
|
|
94
|
+
const hasExplicitMutationScope = isPgPatch || isPgBaseInput || isMutationInput;
|
|
95
|
+
const looksLikeMutationInput = typeName.endsWith('Input') || typeName.endsWith('Patch');
|
|
96
|
+
const isFilterOrCondition = typeName.endsWith('FilterInput')
|
|
97
|
+
|| typeName.endsWith('Filter')
|
|
98
|
+
|| typeName.endsWith('Condition')
|
|
99
|
+
|| typeName === 'VectorNearbyInput';
|
|
100
|
+
if (!hasExplicitMutationScope && (!looksLikeMutationInput || isFilterOrCondition)) {
|
|
88
101
|
return fields;
|
|
89
102
|
}
|
|
90
103
|
const { graphql: { GraphQLString } } = build;
|
|
@@ -24,8 +24,10 @@
|
|
|
24
24
|
* (so the schema is stable) but will return a clear error at execution time.
|
|
25
25
|
*
|
|
26
26
|
* If the embedder returns null (e.g. quota exceeded when the metering
|
|
27
|
-
* plugin is loaded),
|
|
28
|
-
*
|
|
27
|
+
* plugin is loaded), behavior depends on `onQuotaExceeded`:
|
|
28
|
+
* - `'degrade'` (default): silently removes the text field and continues
|
|
29
|
+
* with text-only search as a graceful fallback.
|
|
30
|
+
* - `'throw'`: always throws an error, even if text adapters could handle it.
|
|
29
31
|
*/
|
|
30
32
|
import type { GraphileConfig } from 'graphile-config';
|
|
31
33
|
declare global {
|
|
@@ -35,6 +37,14 @@ declare global {
|
|
|
35
37
|
}
|
|
36
38
|
}
|
|
37
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* Recursively walk a `where` argument object and embed any VectorNearbyInput
|
|
42
|
+
* values that have `text` instead of `vector`.
|
|
43
|
+
*
|
|
44
|
+
* If the embedder returns null (e.g. quota exceeded), the text field is
|
|
45
|
+
* removed so the pgvector filter is skipped — graceful text-only fallback.
|
|
46
|
+
*/
|
|
47
|
+
export declare function embedTextInWhere(obj: any, embedder: (text: string) => Promise<number[] | null>, hasTextAdapters: boolean, onQuotaExceeded?: 'degrade' | 'throw'): Promise<void>;
|
|
38
48
|
/**
|
|
39
49
|
* Creates the LlmTextSearchPlugin.
|
|
40
50
|
*
|
|
@@ -42,4 +52,6 @@ declare global {
|
|
|
42
52
|
* existing `vector` field. When a user provides `text`, the plugin's
|
|
43
53
|
* resolver wrapper embeds it before passing to pgvector.
|
|
44
54
|
*/
|
|
45
|
-
export declare function createLlmTextSearchPlugin(
|
|
55
|
+
export declare function createLlmTextSearchPlugin(options?: {
|
|
56
|
+
onQuotaExceeded?: 'degrade' | 'throw';
|
|
57
|
+
}): GraphileConfig.Plugin;
|
|
@@ -24,9 +24,12 @@
|
|
|
24
24
|
* (so the schema is stable) but will return a clear error at execution time.
|
|
25
25
|
*
|
|
26
26
|
* If the embedder returns null (e.g. quota exceeded when the metering
|
|
27
|
-
* plugin is loaded),
|
|
28
|
-
*
|
|
27
|
+
* plugin is loaded), behavior depends on `onQuotaExceeded`:
|
|
28
|
+
* - `'degrade'` (default): silently removes the text field and continues
|
|
29
|
+
* with text-only search as a graceful fallback.
|
|
30
|
+
* - `'throw'`: always throws an error, even if text adapters could handle it.
|
|
29
31
|
*/
|
|
32
|
+
import { llmConfigStore } from '../embedder';
|
|
30
33
|
/**
|
|
31
34
|
* Check if a codec has any pgvector `vector` columns.
|
|
32
35
|
*/
|
|
@@ -46,12 +49,34 @@ function hasVectorColumns(pgCodec) {
|
|
|
46
49
|
* If the embedder returns null (e.g. quota exceeded), the text field is
|
|
47
50
|
* removed so the pgvector filter is skipped — graceful text-only fallback.
|
|
48
51
|
*/
|
|
49
|
-
async function embedTextInWhere(obj, embedder) {
|
|
52
|
+
export async function embedTextInWhere(obj, embedder, hasTextAdapters, onQuotaExceeded = 'degrade') {
|
|
50
53
|
if (!obj || typeof obj !== 'object')
|
|
51
54
|
return;
|
|
52
55
|
const pending = [];
|
|
53
56
|
for (const key of Object.keys(obj)) {
|
|
54
57
|
const value = obj[key];
|
|
58
|
+
// Handle unifiedSearch: embed text and transform to { __text, __vector }
|
|
59
|
+
if (key === 'unifiedSearch' && typeof value === 'string' && value.trim().length > 0) {
|
|
60
|
+
pending.push((async () => {
|
|
61
|
+
const startTime = Date.now();
|
|
62
|
+
const vector = await embedder(value);
|
|
63
|
+
const latencyMs = Date.now() - startTime;
|
|
64
|
+
if (vector === null) {
|
|
65
|
+
if (onQuotaExceeded === 'throw' || !hasTextAdapters) {
|
|
66
|
+
throw new Error('unifiedSearch: embedding failed (quota exceeded or provider unavailable). ' +
|
|
67
|
+
(!hasTextAdapters
|
|
68
|
+
? 'No text search adapters available for fallback. '
|
|
69
|
+
: 'onQuotaExceeded is set to \'throw\'. ') +
|
|
70
|
+
'Upgrade your plan or adjust onQuotaExceeded to \'degrade\' for text-only fallback.');
|
|
71
|
+
}
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
console.log(`[graphile-llm] unifiedSearch embed: dims=${vector.length}, latency=${latencyMs}ms`);
|
|
75
|
+
// Transform to object shape that graphile-search understands
|
|
76
|
+
obj[key] = { __text: value, __vector: vector };
|
|
77
|
+
})());
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
55
80
|
if (!value || typeof value !== 'object')
|
|
56
81
|
continue;
|
|
57
82
|
// Detect VectorNearbyInput shape: has `text` and no `vector`
|
|
@@ -61,7 +86,10 @@ async function embedTextInWhere(obj, embedder) {
|
|
|
61
86
|
const vector = await embedder(value.text);
|
|
62
87
|
const latencyMs = Date.now() - startTime;
|
|
63
88
|
if (vector === null) {
|
|
64
|
-
|
|
89
|
+
if (onQuotaExceeded === 'throw') {
|
|
90
|
+
throw new Error('VectorNearbyInput: embedding failed (quota exceeded or provider unavailable). ' +
|
|
91
|
+
'Upgrade your plan or adjust onQuotaExceeded to \'degrade\' for graceful fallback.');
|
|
92
|
+
}
|
|
65
93
|
delete value.text;
|
|
66
94
|
return;
|
|
67
95
|
}
|
|
@@ -74,12 +102,12 @@ async function embedTextInWhere(obj, embedder) {
|
|
|
74
102
|
}
|
|
75
103
|
// Recurse into nested filter objects (AND, OR, etc.)
|
|
76
104
|
if (!Array.isArray(value)) {
|
|
77
|
-
pending.push(embedTextInWhere(value, embedder));
|
|
105
|
+
pending.push(embedTextInWhere(value, embedder, hasTextAdapters, onQuotaExceeded));
|
|
78
106
|
}
|
|
79
107
|
else {
|
|
80
108
|
// Handle arrays (e.g. AND: [...], OR: [...])
|
|
81
109
|
for (const item of value) {
|
|
82
|
-
pending.push(embedTextInWhere(item, embedder));
|
|
110
|
+
pending.push(embedTextInWhere(item, embedder, hasTextAdapters, onQuotaExceeded));
|
|
83
111
|
}
|
|
84
112
|
}
|
|
85
113
|
}
|
|
@@ -87,6 +115,32 @@ async function embedTextInWhere(obj, embedder) {
|
|
|
87
115
|
await Promise.all(pending);
|
|
88
116
|
}
|
|
89
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* Resolve per-DB LLM config overrides from express-context.
|
|
120
|
+
* Returns an LlmConfigOverrides object if per-DB config is available,
|
|
121
|
+
* or null to use defaults. These overrides are string parameters only
|
|
122
|
+
* (model name, base URL) — the embedder function and its metering
|
|
123
|
+
* wrapper stay the same.
|
|
124
|
+
*/
|
|
125
|
+
async function resolveConfigOverrides(graphqlContext) {
|
|
126
|
+
const ctx = graphqlContext?.constructive;
|
|
127
|
+
if (!ctx?.useLlm)
|
|
128
|
+
return null;
|
|
129
|
+
try {
|
|
130
|
+
const llm = await ctx.useLlm();
|
|
131
|
+
if (!llm?.embeddingModel)
|
|
132
|
+
return null;
|
|
133
|
+
return {
|
|
134
|
+
embeddingModel: llm.embeddingModel,
|
|
135
|
+
embeddingBaseUrl: llm.embeddingBaseUrl,
|
|
136
|
+
chatModel: llm.chatModel ?? undefined,
|
|
137
|
+
chatBaseUrl: llm.chatBaseUrl ?? undefined,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
90
144
|
/**
|
|
91
145
|
* Creates the LlmTextSearchPlugin.
|
|
92
146
|
*
|
|
@@ -94,7 +148,8 @@ async function embedTextInWhere(obj, embedder) {
|
|
|
94
148
|
* existing `vector` field. When a user provides `text`, the plugin's
|
|
95
149
|
* resolver wrapper embeds it before passing to pgvector.
|
|
96
150
|
*/
|
|
97
|
-
export function createLlmTextSearchPlugin() {
|
|
151
|
+
export function createLlmTextSearchPlugin(options = {}) {
|
|
152
|
+
const { onQuotaExceeded = 'degrade' } = options;
|
|
98
153
|
return {
|
|
99
154
|
name: 'LlmTextSearchPlugin',
|
|
100
155
|
version: '0.2.0',
|
|
@@ -113,8 +168,8 @@ export function createLlmTextSearchPlugin() {
|
|
|
113
168
|
* The field is optional — clients provide either `text` or `vector`.
|
|
114
169
|
*/
|
|
115
170
|
GraphQLInputObjectType_fields(fields, build, context) {
|
|
116
|
-
const
|
|
117
|
-
if (
|
|
171
|
+
const typeName = context.Self.name;
|
|
172
|
+
if (typeName !== 'VectorNearbyInput') {
|
|
118
173
|
return fields;
|
|
119
174
|
}
|
|
120
175
|
const { graphql: { GraphQLString } } = build;
|
|
@@ -129,35 +184,51 @@ export function createLlmTextSearchPlugin() {
|
|
|
129
184
|
},
|
|
130
185
|
/**
|
|
131
186
|
* Wrap connection query resolvers to intercept `where` arguments that
|
|
132
|
-
* contain VectorNearbyInput with `text
|
|
133
|
-
*
|
|
187
|
+
* contain VectorNearbyInput with `text` or `unifiedSearch` with text,
|
|
188
|
+
* embed the text, and inject the resulting vector before the plan executes.
|
|
189
|
+
*
|
|
190
|
+
* For tables with vector columns: embeds VectorNearbyInput.text → vector
|
|
191
|
+
* For ALL tables with unifiedSearch: embeds unifiedSearch text → { __text, __vector }
|
|
134
192
|
*
|
|
135
193
|
* Uses the same v4-style resolver wrapping pattern as graphile-upload-plugin
|
|
136
194
|
* and graphile-bucket-provisioner-plugin.
|
|
137
195
|
*/
|
|
138
196
|
GraphQLObjectType_fields_field(field, build, context) {
|
|
139
197
|
const { scope: { isRootQuery, pgCodec } } = context;
|
|
140
|
-
|
|
141
|
-
|
|
198
|
+
if (!isRootQuery || !pgCodec)
|
|
199
|
+
return field;
|
|
200
|
+
// Wrap if the table has vector columns OR has any searchable columns
|
|
201
|
+
// (for unifiedSearch embedding support)
|
|
202
|
+
const hasVector = hasVectorColumns(pgCodec);
|
|
203
|
+
const hasSearchableColumns = pgCodec.attributes && Object.values(pgCodec.attributes).some((attr) => attr.codec?.name === 'tsvector' || attr.codec?.name === 'vector');
|
|
204
|
+
if (!hasVector && !hasSearchableColumns)
|
|
142
205
|
return field;
|
|
143
|
-
}
|
|
144
206
|
const embedder = build.llmEmbedder;
|
|
145
207
|
if (!embedder)
|
|
146
208
|
return field;
|
|
209
|
+
// Determine if this table has text-based search adapters for fallback logic
|
|
210
|
+
const hasTextAdapters = pgCodec.attributes && Object.values(pgCodec.attributes).some((attr) => attr.codec?.name === 'tsvector');
|
|
147
211
|
const defaultResolver = (obj) => obj[context.scope.fieldName];
|
|
148
212
|
const { resolve: oldResolve = defaultResolver, ...rest } = field;
|
|
149
213
|
return {
|
|
150
214
|
...rest,
|
|
151
215
|
async resolve(source, args, graphqlContext, info) {
|
|
152
|
-
//
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
//
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
216
|
+
// Resolve per-DB model/baseUrl overrides from express-context.
|
|
217
|
+
// These are just string parameters — the embedder function
|
|
218
|
+
// (and its metering wrapper) stays the same.
|
|
219
|
+
const overrides = await resolveConfigOverrides(graphqlContext);
|
|
220
|
+
// Run within llmConfigStore so the embedder picks up per-DB
|
|
221
|
+
// model/baseUrl at call time. Metering wraps the same function
|
|
222
|
+
// and is unaffected.
|
|
223
|
+
return llmConfigStore.run(overrides, async () => {
|
|
224
|
+
if (args?.where) {
|
|
225
|
+
await embedTextInWhere(args.where, embedder, !!hasTextAdapters, onQuotaExceeded);
|
|
226
|
+
}
|
|
227
|
+
if (args?.filter) {
|
|
228
|
+
await embedTextInWhere(args.filter, embedder, !!hasTextAdapters, onQuotaExceeded);
|
|
229
|
+
}
|
|
230
|
+
return oldResolve(source, args, graphqlContext, info);
|
|
231
|
+
});
|
|
161
232
|
}
|
|
162
233
|
};
|
|
163
234
|
},
|
package/esm/preset.d.ts
CHANGED
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
* - Adds `{column}Text: String` companion fields on mutation inputs for vector columns
|
|
11
11
|
* - Optionally enables billing/metering via the LlmMeteringPlugin
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
13
|
+
* Included in ConstructivePreset when `enableLlm` is true (the default).
|
|
14
|
+
* Can also be used standalone by adding it directly to your preset's `extends` array.
|
|
15
15
|
*
|
|
16
16
|
* @example
|
|
17
17
|
* ```typescript
|
|
@@ -53,6 +53,17 @@
|
|
|
53
53
|
* })
|
|
54
54
|
* ```
|
|
55
55
|
*
|
|
56
|
+
* @example With strict quota enforcement (fail if embedding unavailable):
|
|
57
|
+
* ```typescript
|
|
58
|
+
* GraphileLlmPreset({
|
|
59
|
+
* defaultEmbedder: { provider: 'openai', model: 'text-embedding-3-small' },
|
|
60
|
+
* metering: true,
|
|
61
|
+
* onQuotaExceeded: 'throw',
|
|
62
|
+
* // → if billing quota is exceeded, queries with unifiedSearch throw an error
|
|
63
|
+
* // → without this, queries silently fall back to text-only search
|
|
64
|
+
* })
|
|
65
|
+
* ```
|
|
66
|
+
*
|
|
56
67
|
* @example With custom entity_id resolution (bill per-database):
|
|
57
68
|
* ```typescript
|
|
58
69
|
* GraphileLlmPreset({
|
package/esm/preset.js
CHANGED
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
* - Adds `{column}Text: String` companion fields on mutation inputs for vector columns
|
|
11
11
|
* - Optionally enables billing/metering via the LlmMeteringPlugin
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
13
|
+
* Included in ConstructivePreset when `enableLlm` is true (the default).
|
|
14
|
+
* Can also be used standalone by adding it directly to your preset's `extends` array.
|
|
15
15
|
*
|
|
16
16
|
* @example
|
|
17
17
|
* ```typescript
|
|
@@ -53,6 +53,17 @@
|
|
|
53
53
|
* })
|
|
54
54
|
* ```
|
|
55
55
|
*
|
|
56
|
+
* @example With strict quota enforcement (fail if embedding unavailable):
|
|
57
|
+
* ```typescript
|
|
58
|
+
* GraphileLlmPreset({
|
|
59
|
+
* defaultEmbedder: { provider: 'openai', model: 'text-embedding-3-small' },
|
|
60
|
+
* metering: true,
|
|
61
|
+
* onQuotaExceeded: 'throw',
|
|
62
|
+
* // → if billing quota is exceeded, queries with unifiedSearch throw an error
|
|
63
|
+
* // → without this, queries silently fall back to text-only search
|
|
64
|
+
* })
|
|
65
|
+
* ```
|
|
66
|
+
*
|
|
56
67
|
* @example With custom entity_id resolution (bill per-database):
|
|
57
68
|
* ```typescript
|
|
58
69
|
* GraphileLlmPreset({
|
|
@@ -75,7 +86,7 @@ import { createLlmTextSearchPlugin } from './plugins/text-search-plugin';
|
|
|
75
86
|
* @returns A GraphileConfig.Preset to add to your extends array
|
|
76
87
|
*/
|
|
77
88
|
export function GraphileLlmPreset(options = {}) {
|
|
78
|
-
const { enableTextSearch = true, enableTextMutations = true, enableRag = false, ragDefaults, metering } = options;
|
|
89
|
+
const { enableTextSearch = true, enableTextMutations = true, enableRag = false, onQuotaExceeded, ragDefaults, metering } = options;
|
|
79
90
|
const plugins = [
|
|
80
91
|
createLlmModulePlugin(options)
|
|
81
92
|
];
|
|
@@ -86,7 +97,7 @@ export function GraphileLlmPreset(options = {}) {
|
|
|
86
97
|
plugins.push(createLlmMeteringPlugin(meteringConfig));
|
|
87
98
|
}
|
|
88
99
|
if (enableTextSearch) {
|
|
89
|
-
plugins.push(createLlmTextSearchPlugin());
|
|
100
|
+
plugins.push(createLlmTextSearchPlugin({ onQuotaExceeded }));
|
|
90
101
|
}
|
|
91
102
|
if (enableTextMutations) {
|
|
92
103
|
plugins.push(createLlmTextMutationPlugin());
|
package/esm/types.d.ts
CHANGED
|
@@ -216,6 +216,19 @@ export interface GraphileLlmOptions {
|
|
|
216
216
|
* @default true
|
|
217
217
|
*/
|
|
218
218
|
enableTextSearch?: boolean;
|
|
219
|
+
/**
|
|
220
|
+
* Controls behavior when the embedder returns null (e.g. billing quota exceeded).
|
|
221
|
+
*
|
|
222
|
+
* - `'degrade'` (default): Skip the vector search silently and fall back to
|
|
223
|
+
* text-only adapters (tsvector, BM25, trgm). If no text adapters are
|
|
224
|
+
* available, throws an error regardless of this setting.
|
|
225
|
+
* - `'throw'`: Always throw an error when the embedder fails, even if
|
|
226
|
+
* text adapters could handle the query. Use this when semantic search
|
|
227
|
+
* is critical and keyword-only results are unacceptable.
|
|
228
|
+
*
|
|
229
|
+
* @default 'degrade'
|
|
230
|
+
*/
|
|
231
|
+
onQuotaExceeded?: 'degrade' | 'throw';
|
|
219
232
|
/**
|
|
220
233
|
* Whether to add `*Text` companion fields on mutation inputs for vector columns.
|
|
221
234
|
* @default true
|
package/index.d.ts
CHANGED
|
@@ -29,17 +29,18 @@
|
|
|
29
29
|
* };
|
|
30
30
|
* ```
|
|
31
31
|
*/
|
|
32
|
-
export type { LlmEnvOptions, LlmProviderConfig } from './env';
|
|
33
|
-
export { getLlmEnvOptions } from './env';
|
|
32
|
+
export type { LlmEnvOptions, LlmProviderConfig, ResolvedLlmEnvOptions } from './env';
|
|
33
|
+
export { getEnvVars, getEnvOptions, getLlmEnvOptions, llmDefaults } from './env';
|
|
34
34
|
export { GraphileLlmPreset } from './preset';
|
|
35
35
|
export { createLlmModulePlugin } from './plugins/llm-module-plugin';
|
|
36
36
|
export { createLlmRagPlugin } from './plugins/rag-plugin';
|
|
37
37
|
export { createLlmTextMutationPlugin } from './plugins/text-mutation-plugin';
|
|
38
|
-
export { createLlmTextSearchPlugin } from './plugins/text-search-plugin';
|
|
38
|
+
export { createLlmTextSearchPlugin, embedTextInWhere } from './plugins/text-search-plugin';
|
|
39
39
|
export { createLlmMeteringPlugin } from './plugins/metering-plugin';
|
|
40
40
|
export type { AgentDiscovery, AgentTableInfo } from './plugins/agent-discovery-plugin';
|
|
41
41
|
export { clearAgentDiscoveryCache, getAgentDiscovery } from './plugins/agent-discovery-plugin';
|
|
42
|
-
export {
|
|
42
|
+
export type { LlmConfigOverrides } from './embedder';
|
|
43
|
+
export { buildEmbedder, buildEmbedderFromEnv, buildEmbedderFromModule, llmConfigStore } from './embedder';
|
|
43
44
|
export { buildChatCompleter, buildChatCompleterFromEnv, buildChatCompleterFromModule } from './chat';
|
|
44
45
|
export type { InferenceLogEntry, MeteringContext, MeteringOptions, MeterResult, WithPgClient } from './metering';
|
|
45
46
|
export { logInferenceUsage, meteredChat, meteredEmbed, QuotaExceededError } from './metering';
|
package/index.js
CHANGED
|
@@ -31,9 +31,12 @@
|
|
|
31
31
|
* ```
|
|
32
32
|
*/
|
|
33
33
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
34
|
-
exports.invalidateLlmBillingConfig = exports.getLlmBillingConfig = exports.getLlmBillingCacheStats = exports.QuotaExceededError = exports.meteredEmbed = exports.meteredChat = exports.logInferenceUsage = exports.buildChatCompleterFromModule = exports.buildChatCompleterFromEnv = exports.buildChatCompleter = exports.buildEmbedderFromModule = exports.buildEmbedderFromEnv = exports.buildEmbedder = exports.getAgentDiscovery = exports.clearAgentDiscoveryCache = exports.createLlmMeteringPlugin = exports.createLlmTextSearchPlugin = exports.createLlmTextMutationPlugin = exports.createLlmRagPlugin = exports.createLlmModulePlugin = exports.GraphileLlmPreset = exports.getLlmEnvOptions = void 0;
|
|
34
|
+
exports.invalidateLlmBillingConfig = exports.getLlmBillingConfig = exports.getLlmBillingCacheStats = exports.QuotaExceededError = exports.meteredEmbed = exports.meteredChat = exports.logInferenceUsage = exports.buildChatCompleterFromModule = exports.buildChatCompleterFromEnv = exports.buildChatCompleter = exports.llmConfigStore = exports.buildEmbedderFromModule = exports.buildEmbedderFromEnv = exports.buildEmbedder = exports.getAgentDiscovery = exports.clearAgentDiscoveryCache = exports.createLlmMeteringPlugin = exports.embedTextInWhere = exports.createLlmTextSearchPlugin = exports.createLlmTextMutationPlugin = exports.createLlmRagPlugin = exports.createLlmModulePlugin = exports.GraphileLlmPreset = exports.llmDefaults = exports.getLlmEnvOptions = exports.getEnvOptions = exports.getEnvVars = void 0;
|
|
35
35
|
var env_1 = require("./env");
|
|
36
|
+
Object.defineProperty(exports, "getEnvVars", { enumerable: true, get: function () { return env_1.getEnvVars; } });
|
|
37
|
+
Object.defineProperty(exports, "getEnvOptions", { enumerable: true, get: function () { return env_1.getEnvOptions; } });
|
|
36
38
|
Object.defineProperty(exports, "getLlmEnvOptions", { enumerable: true, get: function () { return env_1.getLlmEnvOptions; } });
|
|
39
|
+
Object.defineProperty(exports, "llmDefaults", { enumerable: true, get: function () { return env_1.llmDefaults; } });
|
|
37
40
|
// Preset (recommended entry point)
|
|
38
41
|
var preset_1 = require("./preset");
|
|
39
42
|
Object.defineProperty(exports, "GraphileLlmPreset", { enumerable: true, get: function () { return preset_1.GraphileLlmPreset; } });
|
|
@@ -46,17 +49,18 @@ var text_mutation_plugin_1 = require("./plugins/text-mutation-plugin");
|
|
|
46
49
|
Object.defineProperty(exports, "createLlmTextMutationPlugin", { enumerable: true, get: function () { return text_mutation_plugin_1.createLlmTextMutationPlugin; } });
|
|
47
50
|
var text_search_plugin_1 = require("./plugins/text-search-plugin");
|
|
48
51
|
Object.defineProperty(exports, "createLlmTextSearchPlugin", { enumerable: true, get: function () { return text_search_plugin_1.createLlmTextSearchPlugin; } });
|
|
52
|
+
Object.defineProperty(exports, "embedTextInWhere", { enumerable: true, get: function () { return text_search_plugin_1.embedTextInWhere; } });
|
|
49
53
|
// Metering plugin (opt-in billing integration)
|
|
50
54
|
var metering_plugin_1 = require("./plugins/metering-plugin");
|
|
51
55
|
Object.defineProperty(exports, "createLlmMeteringPlugin", { enumerable: true, get: function () { return metering_plugin_1.createLlmMeteringPlugin; } });
|
|
52
56
|
var agent_discovery_plugin_1 = require("./plugins/agent-discovery-plugin");
|
|
53
57
|
Object.defineProperty(exports, "clearAgentDiscoveryCache", { enumerable: true, get: function () { return agent_discovery_plugin_1.clearAgentDiscoveryCache; } });
|
|
54
58
|
Object.defineProperty(exports, "getAgentDiscovery", { enumerable: true, get: function () { return agent_discovery_plugin_1.getAgentDiscovery; } });
|
|
55
|
-
// Embedder utilities
|
|
56
59
|
var embedder_1 = require("./embedder");
|
|
57
60
|
Object.defineProperty(exports, "buildEmbedder", { enumerable: true, get: function () { return embedder_1.buildEmbedder; } });
|
|
58
61
|
Object.defineProperty(exports, "buildEmbedderFromEnv", { enumerable: true, get: function () { return embedder_1.buildEmbedderFromEnv; } });
|
|
59
62
|
Object.defineProperty(exports, "buildEmbedderFromModule", { enumerable: true, get: function () { return embedder_1.buildEmbedderFromModule; } });
|
|
63
|
+
Object.defineProperty(exports, "llmConfigStore", { enumerable: true, get: function () { return embedder_1.llmConfigStore; } });
|
|
60
64
|
// Chat completion utilities
|
|
61
65
|
var chat_1 = require("./chat");
|
|
62
66
|
Object.defineProperty(exports, "buildChatCompleter", { enumerable: true, get: function () { return chat_1.buildChatCompleter; } });
|
package/metering.d.ts
CHANGED
|
@@ -90,9 +90,7 @@ export interface InferenceLogEntry {
|
|
|
90
90
|
}
|
|
91
91
|
/**
|
|
92
92
|
* Write a row to the usage_log_inference table.
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
* TODO: Also write to child (generated) database when dual-write is needed.
|
|
93
|
+
* Delegates to the shared BillingClient from express-context.
|
|
96
94
|
*/
|
|
97
95
|
export declare function logInferenceUsage(ctx: MeteringContext, entry: InferenceLogEntry): Promise<void>;
|
|
98
96
|
/**
|