@nice-tools/fake-llm 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/dist/browser.d.mts +349 -0
- package/dist/browser.d.ts +349 -0
- package/dist/browser.js +607 -0
- package/dist/browser.js.map +1 -0
- package/dist/browser.mjs +591 -0
- package/dist/browser.mjs.map +1 -0
- package/dist/index.d.mts +120 -0
- package/dist/index.d.ts +120 -0
- package/dist/index.js +941 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +899 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +59 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,899 @@
|
|
|
1
|
+
import { CosmosClient } from '@azure/cosmos';
|
|
2
|
+
import { BlobServiceClient } from '@azure/storage-blob';
|
|
3
|
+
import { Storage } from '@google-cloud/storage';
|
|
4
|
+
import * as fs from 'fs';
|
|
5
|
+
import * as path3 from 'path';
|
|
6
|
+
import nlp from 'compromise';
|
|
7
|
+
|
|
8
|
+
// src/adapters/base.adapter.ts
|
|
9
|
+
var BaseAdapter = class {
|
|
10
|
+
buildWhereClause(filters) {
|
|
11
|
+
if (!filters || Object.keys(filters).length === 0) {
|
|
12
|
+
return "";
|
|
13
|
+
}
|
|
14
|
+
const conditions = Object.entries(filters).map(([key, value]) => {
|
|
15
|
+
if (typeof value === "string") {
|
|
16
|
+
return `c.${key} = '${value}'`;
|
|
17
|
+
}
|
|
18
|
+
return `c.${key} = ${value}`;
|
|
19
|
+
});
|
|
20
|
+
return "WHERE " + conditions.join(" AND ");
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
var CosmosAdapter = class extends BaseAdapter {
|
|
24
|
+
constructor(options) {
|
|
25
|
+
super();
|
|
26
|
+
this.options = options;
|
|
27
|
+
this.client = new CosmosClient({
|
|
28
|
+
endpoint: options.endpoint,
|
|
29
|
+
key: options.key
|
|
30
|
+
});
|
|
31
|
+
this.database = this.client.database(options.databaseId);
|
|
32
|
+
}
|
|
33
|
+
options;
|
|
34
|
+
client;
|
|
35
|
+
database;
|
|
36
|
+
async query(params) {
|
|
37
|
+
const container = this.database.container(params.source);
|
|
38
|
+
let sql = `SELECT * FROM c`;
|
|
39
|
+
const whereClause = this.buildWhereClause(params.filters || {});
|
|
40
|
+
if (whereClause) {
|
|
41
|
+
sql += ` ${whereClause}`;
|
|
42
|
+
}
|
|
43
|
+
if (params.orderBy) {
|
|
44
|
+
sql += ` ORDER BY c.${params.orderBy}`;
|
|
45
|
+
}
|
|
46
|
+
if (params.limit) {
|
|
47
|
+
sql += ` OFFSET 0 LIMIT ${params.limit}`;
|
|
48
|
+
}
|
|
49
|
+
const { resources } = await container.items.query(sql).fetchAll();
|
|
50
|
+
return resources;
|
|
51
|
+
}
|
|
52
|
+
async getById(id) {
|
|
53
|
+
throw new Error("getById requires container specification");
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
var BlobAdapter = class extends BaseAdapter {
|
|
57
|
+
constructor(options) {
|
|
58
|
+
super();
|
|
59
|
+
this.options = options;
|
|
60
|
+
if (options.connectionString) {
|
|
61
|
+
this.blobServiceClient = BlobServiceClient.fromConnectionString(options.connectionString);
|
|
62
|
+
} else if (options.sasToken && options.accountName) {
|
|
63
|
+
const blobSasUrl = `https://${options.accountName}.blob.core.windows.net${options.sasToken}`;
|
|
64
|
+
this.blobServiceClient = new BlobServiceClient(blobSasUrl);
|
|
65
|
+
} else {
|
|
66
|
+
throw new Error("BlobAdapter requires connectionString or sasToken + accountName");
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
options;
|
|
70
|
+
blobServiceClient;
|
|
71
|
+
async query(params) {
|
|
72
|
+
const containerClient = this.blobServiceClient.getContainerClient(params.source);
|
|
73
|
+
const results = [];
|
|
74
|
+
for await (const blob of containerClient.listBlobsFlat()) {
|
|
75
|
+
if (blob.name.endsWith(".json")) {
|
|
76
|
+
const blobClient = containerClient.getBlobClient(blob.name);
|
|
77
|
+
const downloadResponse = await blobClient.download();
|
|
78
|
+
const content = await this.readDownloadResponse(downloadResponse);
|
|
79
|
+
const data = JSON.parse(content);
|
|
80
|
+
if (this.matchesFilters(data, params.filters || {})) {
|
|
81
|
+
results.push(data);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return results;
|
|
86
|
+
}
|
|
87
|
+
async getById(id) {
|
|
88
|
+
throw new Error("getById not implemented for BlobAdapter");
|
|
89
|
+
}
|
|
90
|
+
matchesFilters(data, filters) {
|
|
91
|
+
return Object.entries(filters).every(([key, value]) => data[key] === value);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Reads blob download response content as a UTF-8 string.
|
|
95
|
+
*
|
|
96
|
+
* The Azure Blob SDK exposes different properties depending on the runtime:
|
|
97
|
+
* - Browser: `blobBody` (`Promise<Blob>`) — uses `Blob.text()`.
|
|
98
|
+
* - Node.js: `readableStreamBody` (`NodeJS.ReadableStream`) — uses stream events.
|
|
99
|
+
*/
|
|
100
|
+
async readDownloadResponse(response) {
|
|
101
|
+
if (response.blobBody) {
|
|
102
|
+
const blob = await response.blobBody;
|
|
103
|
+
return blob.text();
|
|
104
|
+
}
|
|
105
|
+
const stream = response.readableStreamBody;
|
|
106
|
+
const chunks = [];
|
|
107
|
+
return new Promise((resolve, reject) => {
|
|
108
|
+
stream.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
|
|
109
|
+
stream.on("error", reject);
|
|
110
|
+
stream.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
var GCSAdapter = class extends BaseAdapter {
|
|
115
|
+
constructor(options) {
|
|
116
|
+
super();
|
|
117
|
+
this.options = options;
|
|
118
|
+
this.storage = new Storage({
|
|
119
|
+
projectId: options.projectId,
|
|
120
|
+
keyFilename: options.keyFilePath
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
options;
|
|
124
|
+
storage;
|
|
125
|
+
async query(params) {
|
|
126
|
+
const bucket = this.storage.bucket(params.source);
|
|
127
|
+
const [files] = await bucket.getFiles();
|
|
128
|
+
const results = [];
|
|
129
|
+
for (const file of files) {
|
|
130
|
+
if (file.name.endsWith(".json")) {
|
|
131
|
+
const [content] = await file.download();
|
|
132
|
+
const data = JSON.parse(content.toString("utf8"));
|
|
133
|
+
if (this.matchesFilters(data, params.filters || {})) {
|
|
134
|
+
results.push(data);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return results;
|
|
139
|
+
}
|
|
140
|
+
async getById(id) {
|
|
141
|
+
throw new Error("getById not implemented for GCSAdapter");
|
|
142
|
+
}
|
|
143
|
+
matchesFilters(data, filters) {
|
|
144
|
+
return Object.entries(filters).every(([key, value]) => data[key] === value);
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
var MockCosmosAdapter = class extends BaseAdapter {
|
|
148
|
+
constructor(options) {
|
|
149
|
+
super();
|
|
150
|
+
this.options = options;
|
|
151
|
+
}
|
|
152
|
+
options;
|
|
153
|
+
async query(params) {
|
|
154
|
+
const parts = params.source.split("/");
|
|
155
|
+
const containerPath = path3.join(this.options.basePath, ...parts);
|
|
156
|
+
let items = [];
|
|
157
|
+
if (fs.existsSync(containerPath) && fs.statSync(containerPath).isDirectory()) {
|
|
158
|
+
const files = fs.readdirSync(containerPath).filter((f) => f.endsWith(".json"));
|
|
159
|
+
items = files.map((file) => {
|
|
160
|
+
const content = fs.readFileSync(path3.join(containerPath, file), "utf8");
|
|
161
|
+
return JSON.parse(content);
|
|
162
|
+
});
|
|
163
|
+
} else {
|
|
164
|
+
const filePath = containerPath + ".json";
|
|
165
|
+
if (fs.existsSync(filePath)) {
|
|
166
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
167
|
+
const parsed = JSON.parse(content);
|
|
168
|
+
items = Array.isArray(parsed) ? parsed : [parsed];
|
|
169
|
+
} else {
|
|
170
|
+
console.warn(`MockCosmosAdapter: Path not found: ${containerPath}`);
|
|
171
|
+
return [];
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (params.filters && Object.keys(params.filters).length > 0) {
|
|
175
|
+
items = items.filter((item) => this.matchesFilters(item, params.filters));
|
|
176
|
+
}
|
|
177
|
+
if (params.limit) {
|
|
178
|
+
items = items.slice(0, params.limit);
|
|
179
|
+
}
|
|
180
|
+
return items;
|
|
181
|
+
}
|
|
182
|
+
async getById(id) {
|
|
183
|
+
throw new Error("getById requires source specification in MockCosmosAdapter");
|
|
184
|
+
}
|
|
185
|
+
matchesFilters(item, filters) {
|
|
186
|
+
return Object.entries(filters).every(([key, value]) => {
|
|
187
|
+
const itemValue = this.getNestedValue(item, key);
|
|
188
|
+
return itemValue === value;
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
getNestedValue(obj, path4) {
|
|
192
|
+
return path4.split(".").reduce((current, key) => current?.[key], obj);
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
var ImageAdapter = class {
|
|
196
|
+
async readImage(imagePath) {
|
|
197
|
+
try {
|
|
198
|
+
if (!fs.existsSync(imagePath)) {
|
|
199
|
+
return {
|
|
200
|
+
format: "unknown",
|
|
201
|
+
size: 0,
|
|
202
|
+
error: "File not found"
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
const buffer = fs.readFileSync(imagePath);
|
|
206
|
+
const ext = path3.extname(imagePath).toLowerCase();
|
|
207
|
+
return {
|
|
208
|
+
format: ext.substring(1),
|
|
209
|
+
size: buffer.length,
|
|
210
|
+
base64: buffer.toString("base64")
|
|
211
|
+
};
|
|
212
|
+
} catch (error) {
|
|
213
|
+
return {
|
|
214
|
+
format: "unknown",
|
|
215
|
+
size: 0,
|
|
216
|
+
error: error.message
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
async analyzeImage(imagePath) {
|
|
221
|
+
const result = await this.readImage(imagePath);
|
|
222
|
+
if (result.error) {
|
|
223
|
+
return `Cannot analyze image: ${result.error}`;
|
|
224
|
+
}
|
|
225
|
+
return `Image format: ${result.format}, Size: ${Math.round(result.size / 1024)}KB`;
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
// src/adapters/http-mock.adapter.ts
|
|
230
|
+
var HttpMockAdapter = class extends BaseAdapter {
|
|
231
|
+
constructor(options) {
|
|
232
|
+
super();
|
|
233
|
+
this.options = options;
|
|
234
|
+
}
|
|
235
|
+
options;
|
|
236
|
+
async query(params) {
|
|
237
|
+
if (params.source.includes("..") || params.source.startsWith("/") || params.source.includes("://")) {
|
|
238
|
+
throw new Error(`HttpMockAdapter: Invalid source path '${params.source}'`);
|
|
239
|
+
}
|
|
240
|
+
const base = this.options.baseUrl.replace(/\/$/, "");
|
|
241
|
+
const url = `${base}/${params.source}.json`;
|
|
242
|
+
const response = await fetch(url);
|
|
243
|
+
if (!response.ok) {
|
|
244
|
+
if (response.status === 404) return [];
|
|
245
|
+
throw new Error(`HttpMockAdapter: Failed to fetch ${url} (${response.status})`);
|
|
246
|
+
}
|
|
247
|
+
const data = await response.json();
|
|
248
|
+
let items = Array.isArray(data) ? data : [data];
|
|
249
|
+
if (params.filters && Object.keys(params.filters).length > 0) {
|
|
250
|
+
items = items.filter((item) => this.matchesFilters(item, params.filters));
|
|
251
|
+
}
|
|
252
|
+
if (params.limit) {
|
|
253
|
+
items = items.slice(0, params.limit);
|
|
254
|
+
}
|
|
255
|
+
return items;
|
|
256
|
+
}
|
|
257
|
+
async getById(_id) {
|
|
258
|
+
throw new Error("HttpMockAdapter.getById is not supported \u2014 use query() with a filters object");
|
|
259
|
+
}
|
|
260
|
+
matchesFilters(item, filters) {
|
|
261
|
+
return Object.entries(filters).every(([key, value]) => {
|
|
262
|
+
const itemValue = this.getNestedValue(item, key);
|
|
263
|
+
return itemValue === value;
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
getNestedValue(obj, path4) {
|
|
267
|
+
return path4.split(".").reduce((current, key) => current?.[key], obj);
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
var STOP_WORDS = /* @__PURE__ */ new Set([
|
|
271
|
+
"the",
|
|
272
|
+
"a",
|
|
273
|
+
"an",
|
|
274
|
+
"is",
|
|
275
|
+
"are",
|
|
276
|
+
"was",
|
|
277
|
+
"were",
|
|
278
|
+
"be",
|
|
279
|
+
"been",
|
|
280
|
+
"being",
|
|
281
|
+
"have",
|
|
282
|
+
"has",
|
|
283
|
+
"had",
|
|
284
|
+
"do",
|
|
285
|
+
"does",
|
|
286
|
+
"did",
|
|
287
|
+
"will",
|
|
288
|
+
"would",
|
|
289
|
+
"shall",
|
|
290
|
+
"should",
|
|
291
|
+
"may",
|
|
292
|
+
"might",
|
|
293
|
+
"must",
|
|
294
|
+
"can",
|
|
295
|
+
"could",
|
|
296
|
+
"let",
|
|
297
|
+
"make",
|
|
298
|
+
"why",
|
|
299
|
+
"what",
|
|
300
|
+
"how",
|
|
301
|
+
"when",
|
|
302
|
+
"where",
|
|
303
|
+
"who",
|
|
304
|
+
"which",
|
|
305
|
+
"that",
|
|
306
|
+
"this",
|
|
307
|
+
"these",
|
|
308
|
+
"those",
|
|
309
|
+
"it",
|
|
310
|
+
"its",
|
|
311
|
+
"my",
|
|
312
|
+
"your",
|
|
313
|
+
"his",
|
|
314
|
+
"her",
|
|
315
|
+
"our",
|
|
316
|
+
"their",
|
|
317
|
+
"me",
|
|
318
|
+
"him",
|
|
319
|
+
"us",
|
|
320
|
+
"them",
|
|
321
|
+
"and",
|
|
322
|
+
"or",
|
|
323
|
+
"but",
|
|
324
|
+
"if",
|
|
325
|
+
"in",
|
|
326
|
+
"on",
|
|
327
|
+
"at",
|
|
328
|
+
"to",
|
|
329
|
+
"for",
|
|
330
|
+
"of",
|
|
331
|
+
"with",
|
|
332
|
+
"by",
|
|
333
|
+
"from",
|
|
334
|
+
"about",
|
|
335
|
+
"into",
|
|
336
|
+
"through",
|
|
337
|
+
"during",
|
|
338
|
+
"before",
|
|
339
|
+
"after",
|
|
340
|
+
"above",
|
|
341
|
+
"below",
|
|
342
|
+
"between",
|
|
343
|
+
"out",
|
|
344
|
+
"off",
|
|
345
|
+
"over",
|
|
346
|
+
"under",
|
|
347
|
+
"again",
|
|
348
|
+
"further",
|
|
349
|
+
"just",
|
|
350
|
+
"also",
|
|
351
|
+
"not",
|
|
352
|
+
"no",
|
|
353
|
+
"nor",
|
|
354
|
+
"so",
|
|
355
|
+
"yet",
|
|
356
|
+
"both",
|
|
357
|
+
"either"
|
|
358
|
+
]);
|
|
359
|
+
var NLPMatcher = class {
|
|
360
|
+
parseQuery(query) {
|
|
361
|
+
const cleanQuery = query.replace(/[?!.,;:'"]/g, " ").trim();
|
|
362
|
+
const doc = nlp(cleanQuery);
|
|
363
|
+
const action = this.extractAction(query);
|
|
364
|
+
const keywords = this.extractKeywords(doc, cleanQuery);
|
|
365
|
+
const filters = this.extractFilters(query);
|
|
366
|
+
const confidence = keywords.length > 0 ? 0.8 : 0.3;
|
|
367
|
+
return { action, keywords, filters, confidence };
|
|
368
|
+
}
|
|
369
|
+
extractAction(query) {
|
|
370
|
+
const q = query.toLowerCase();
|
|
371
|
+
if (q.includes("compare") || q.includes("difference")) return "compare";
|
|
372
|
+
if (q.includes("diff") || q.includes("what changed")) return "diff";
|
|
373
|
+
if (q.includes("find") || q.includes("search") || q.includes("which")) return "find";
|
|
374
|
+
if (q.includes("explain") || q.includes("what is") || q.includes("tell me about") || q.startsWith("why")) return "explain";
|
|
375
|
+
return "list";
|
|
376
|
+
}
|
|
377
|
+
extractKeywords(doc, cleanQuery) {
|
|
378
|
+
const nouns = doc.nouns().out("array");
|
|
379
|
+
const adjectives = doc.adjectives().out("array");
|
|
380
|
+
const allTerms = [];
|
|
381
|
+
for (const term of [...nouns, ...adjectives]) {
|
|
382
|
+
const parts = term.toLowerCase().split(/\s+/);
|
|
383
|
+
allTerms.push(...parts);
|
|
384
|
+
}
|
|
385
|
+
const queryWords = cleanQuery.toLowerCase().split(/\s+/);
|
|
386
|
+
allTerms.push(...queryWords);
|
|
387
|
+
return [...new Set(allTerms)].map((w) => w.toLowerCase().replace(/[^a-z0-9-]/g, "")).filter((w) => w.length > 2 && !STOP_WORDS.has(w));
|
|
388
|
+
}
|
|
389
|
+
extractFilters(query) {
|
|
390
|
+
const filters = {};
|
|
391
|
+
const questionWords = /* @__PURE__ */ new Set(["why", "what", "how", "when", "where", "who", "which"]);
|
|
392
|
+
const pattern = /(?:where|with)\s+(\w+)\s+(?:is|=|equals?)\s+['"]?([^'"\s]+)['"]?/gi;
|
|
393
|
+
let match;
|
|
394
|
+
while ((match = pattern.exec(query)) !== null) {
|
|
395
|
+
const [, key, value] = match;
|
|
396
|
+
if (key && value && !questionWords.has(key.toLowerCase())) {
|
|
397
|
+
filters[key] = value;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
return filters;
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
// src/engine/keyword-resolver.ts
|
|
405
|
+
var KeywordResolver = class {
|
|
406
|
+
constructor(keywords) {
|
|
407
|
+
this.keywords = keywords;
|
|
408
|
+
}
|
|
409
|
+
keywords;
|
|
410
|
+
resolve(term) {
|
|
411
|
+
const lowerTerm = term.toLowerCase();
|
|
412
|
+
let match = this.keywords.find((kw) => kw.keyword.toLowerCase() === lowerTerm);
|
|
413
|
+
if (!match) {
|
|
414
|
+
match = this.keywords.find(
|
|
415
|
+
(kw) => kw.aliases.some((alias) => alias.toLowerCase() === lowerTerm)
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
if (!match) {
|
|
419
|
+
match = this.keywords.find(
|
|
420
|
+
(kw) => kw.keyword.length > 2 && lowerTerm.includes(kw.keyword.toLowerCase())
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
return match;
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Resolves a list of NLP-extracted terms AND also checks the full original query
|
|
427
|
+
* against all keyword aliases (to handle multi-word alias phrases).
|
|
428
|
+
*/
|
|
429
|
+
resolveAll(terms, fullQuery) {
|
|
430
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
431
|
+
for (const term of terms) {
|
|
432
|
+
const kw = this.resolve(term);
|
|
433
|
+
if (kw) resolved.set(kw.keyword, kw);
|
|
434
|
+
}
|
|
435
|
+
if (fullQuery) {
|
|
436
|
+
const lowerQuery = fullQuery.toLowerCase().replace(/[?!.,;:'"]/g, " ");
|
|
437
|
+
for (const kw of this.keywords) {
|
|
438
|
+
if (!resolved.has(kw.keyword)) {
|
|
439
|
+
const aliasMatch = kw.aliases.some((alias) => {
|
|
440
|
+
const lowerAlias = alias.toLowerCase();
|
|
441
|
+
return lowerAlias.length > 3 && lowerQuery.includes(lowerAlias);
|
|
442
|
+
});
|
|
443
|
+
if (aliasMatch) resolved.set(kw.keyword, kw);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
return Array.from(resolved.values());
|
|
448
|
+
}
|
|
449
|
+
getRelatedKeywords(keyword) {
|
|
450
|
+
return this.keywords.filter(
|
|
451
|
+
(kw) => kw.category === keyword.category && kw.keyword !== keyword.keyword
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
// src/engine/story-resolver.ts
|
|
457
|
+
var STORY_THRESHOLD = 0.1;
|
|
458
|
+
var StoryResolver = class {
|
|
459
|
+
constructor(stories) {
|
|
460
|
+
this.stories = stories;
|
|
461
|
+
}
|
|
462
|
+
stories;
|
|
463
|
+
scoreAll(intent, resolvedKeywords) {
|
|
464
|
+
return this.stories.map((story) => {
|
|
465
|
+
const score = this.scoreStory(story, intent, resolvedKeywords);
|
|
466
|
+
const matchedKeywords = resolvedKeywords.filter((kw) => story.keywords.includes(kw.keyword)).map((kw) => kw.keyword);
|
|
467
|
+
const storyId = story.story_id || story.id || "";
|
|
468
|
+
return { storyId, score, matchedKeywords, storyKeywords: story.keywords };
|
|
469
|
+
}).sort((a, b) => b.score - a.score);
|
|
470
|
+
}
|
|
471
|
+
findBestStory(intent, resolvedKeywords) {
|
|
472
|
+
const matches = this.stories.map((story) => {
|
|
473
|
+
const score = this.scoreStory(story, intent, resolvedKeywords);
|
|
474
|
+
const matchedKeywords = resolvedKeywords.filter((kw) => story.keywords.includes(kw.keyword)).map((kw) => kw.keyword);
|
|
475
|
+
return { story, score, matchedKeywords };
|
|
476
|
+
});
|
|
477
|
+
matches.sort((a, b) => b.score - a.score);
|
|
478
|
+
const best = matches[0];
|
|
479
|
+
return best && best.score > STORY_THRESHOLD ? best : void 0;
|
|
480
|
+
}
|
|
481
|
+
scoreStory(story, intent, resolvedKeywords) {
|
|
482
|
+
let score = 0;
|
|
483
|
+
const keywordNames = resolvedKeywords.map((kw) => kw.keyword);
|
|
484
|
+
const overlap = story.keywords.filter((kw) => keywordNames.includes(kw)).length;
|
|
485
|
+
const keywordScore = overlap / Math.max(story.keywords.length, keywordNames.length, 1);
|
|
486
|
+
score += keywordScore * 0.7;
|
|
487
|
+
if (intent.action === "compare" && story.resolution_steps.some((s) => s.action === "compare")) score += 0.2;
|
|
488
|
+
if (intent.action === "diff" && story.resolution_steps.some((s) => s.action === "diff")) score += 0.2;
|
|
489
|
+
if (overlap === story.keywords.length) score += 0.1;
|
|
490
|
+
return Math.min(score, 1);
|
|
491
|
+
}
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
// src/engine/query-builder.ts
|
|
495
|
+
var QueryBuilder = class {
|
|
496
|
+
buildQuery(step, intent) {
|
|
497
|
+
return {
|
|
498
|
+
source: step.from_source || "",
|
|
499
|
+
filters: { ...intent.filters }
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
buildQueryPreview(step, intent, resolvedKeywords = []) {
|
|
503
|
+
const filters = intent.filters || {};
|
|
504
|
+
const where = Object.entries(filters).map(
|
|
505
|
+
([key, value]) => typeof value === "string" ? `c.${key} = '${value}'` : `c.${key} = ${value}`
|
|
506
|
+
).join(" AND ");
|
|
507
|
+
const generatedQuery = where ? `SELECT * FROM c WHERE ${where}` : `SELECT * FROM c /* ${step.from_source || "source"} */`;
|
|
508
|
+
const terms = [.../* @__PURE__ */ new Set([...step.keyword ? [step.keyword] : [], ...resolvedKeywords])];
|
|
509
|
+
const searchPattern = terms.length > 0 ? `MATCH ${terms.map((term) => `"${term}"`).join(" OR ")}` : `MATCH ${step.from_source || "source"}*`;
|
|
510
|
+
return { generatedQuery, searchPattern };
|
|
511
|
+
}
|
|
512
|
+
buildSQLWhere(filters) {
|
|
513
|
+
if (!filters || Object.keys(filters).length === 0) return "";
|
|
514
|
+
const conditions = Object.entries(filters).map(
|
|
515
|
+
([key, value]) => typeof value === "string" ? `c.${key} = '${value}'` : `c.${key} = ${value}`
|
|
516
|
+
);
|
|
517
|
+
return "WHERE " + conditions.join(" AND ");
|
|
518
|
+
}
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
// src/engine/response-builder.ts
|
|
522
|
+
var ResponseBuilder = class {
|
|
523
|
+
buildAnswer(intent, story, results, executionTime, source) {
|
|
524
|
+
return {
|
|
525
|
+
intent,
|
|
526
|
+
story,
|
|
527
|
+
results,
|
|
528
|
+
summary: this.buildSummary(intent, results),
|
|
529
|
+
metadata: {
|
|
530
|
+
execution_time_ms: executionTime,
|
|
531
|
+
source
|
|
532
|
+
}
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
buildSummary(intent, results) {
|
|
536
|
+
if (results.length === 0) return "I couldn't find any results for your question.";
|
|
537
|
+
if (results.length === 1 && results[0]?.answer) {
|
|
538
|
+
return results[0].answer;
|
|
539
|
+
}
|
|
540
|
+
if (intent.action === "explain") {
|
|
541
|
+
const withAnswer = results.find((r) => r.answer);
|
|
542
|
+
if (withAnswer) return withAnswer.answer;
|
|
543
|
+
return Object.entries(results[0]).filter(([key]) => !key.startsWith("_") && key !== "id").map(([key, value]) => `${key}: ${value}`).join(", ");
|
|
544
|
+
}
|
|
545
|
+
if (intent.action === "compare") return `Comparing ${results.length} items.`;
|
|
546
|
+
const answers = results.filter((r) => r.answer).map((r) => r.answer);
|
|
547
|
+
if (answers.length > 0) return answers.join(" | ");
|
|
548
|
+
return `Found ${results.length} result(s).`;
|
|
549
|
+
}
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
// src/config/fetch-loader.ts
|
|
553
|
+
var FetchConfigLoader = class {
|
|
554
|
+
constructor(options) {
|
|
555
|
+
this.options = options;
|
|
556
|
+
}
|
|
557
|
+
options;
|
|
558
|
+
async loadKeywords() {
|
|
559
|
+
const response = await fetch(this.options.keywordsUrl);
|
|
560
|
+
if (!response.ok) {
|
|
561
|
+
throw new Error(
|
|
562
|
+
`FetchConfigLoader: Failed to fetch keywords from ${this.options.keywordsUrl} (${response.status})`
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
const data = await response.json();
|
|
566
|
+
return Array.isArray(data) ? data : [data];
|
|
567
|
+
}
|
|
568
|
+
async loadStories() {
|
|
569
|
+
const response = await fetch(this.options.storiesUrl);
|
|
570
|
+
if (!response.ok) {
|
|
571
|
+
throw new Error(
|
|
572
|
+
`FetchConfigLoader: Failed to fetch stories from ${this.options.storiesUrl} (${response.status})`
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
const data = await response.json();
|
|
576
|
+
return Array.isArray(data) ? data : [data];
|
|
577
|
+
}
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
// src/mock-llm-engine.ts
|
|
581
|
+
var MockLLMEngine = class {
|
|
582
|
+
constructor(keywords, stories, adapter, fallbackLLM) {
|
|
583
|
+
this.keywords = keywords;
|
|
584
|
+
this.stories = stories;
|
|
585
|
+
this.adapter = adapter;
|
|
586
|
+
this.fallbackLLM = fallbackLLM;
|
|
587
|
+
this.nlpMatcher = new NLPMatcher();
|
|
588
|
+
this.queryBuilder = new QueryBuilder();
|
|
589
|
+
this.responseBuilder = new ResponseBuilder();
|
|
590
|
+
this.keywordResolver = new KeywordResolver(keywords);
|
|
591
|
+
this.storyResolver = new StoryResolver(stories);
|
|
592
|
+
}
|
|
593
|
+
keywords;
|
|
594
|
+
stories;
|
|
595
|
+
adapter;
|
|
596
|
+
fallbackLLM;
|
|
597
|
+
nlpMatcher;
|
|
598
|
+
keywordResolver;
|
|
599
|
+
storyResolver;
|
|
600
|
+
queryBuilder;
|
|
601
|
+
responseBuilder;
|
|
602
|
+
// ─── Introspection API ───────────────────────────────────────────────────────
|
|
603
|
+
getKeywords() {
|
|
604
|
+
return this.keywords;
|
|
605
|
+
}
|
|
606
|
+
getStories() {
|
|
607
|
+
return this.stories;
|
|
608
|
+
}
|
|
609
|
+
listDataSources() {
|
|
610
|
+
const sources = /* @__PURE__ */ new Set();
|
|
611
|
+
for (const kw of this.keywords) {
|
|
612
|
+
if (kw.data_source) sources.add(kw.data_source);
|
|
613
|
+
}
|
|
614
|
+
for (const story of this.stories) {
|
|
615
|
+
if (story.contract?.sources) {
|
|
616
|
+
for (const source of story.contract.sources) {
|
|
617
|
+
if (source) sources.add(source);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
for (const step of story.resolution_steps) {
|
|
621
|
+
if (step.from_source) sources.add(step.from_source);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
return Array.from(sources);
|
|
625
|
+
}
|
|
626
|
+
async getDataSourceSnapshot(source, limit = 50) {
|
|
627
|
+
return this.adapter.query({ source, limit });
|
|
628
|
+
}
|
|
629
|
+
// ─── Query ───────────────────────────────────────────────────────────────────
|
|
630
|
+
async query(userQuery, opts = { debug: true }) {
|
|
631
|
+
const includeDebug = opts.debug !== false;
|
|
632
|
+
const startTime = Date.now();
|
|
633
|
+
const intent = this.nlpMatcher.parseQuery(userQuery);
|
|
634
|
+
const nlpTerms = intent.keywords;
|
|
635
|
+
const resolvedKeywords = this.keywordResolver.resolveAll(nlpTerms, userQuery);
|
|
636
|
+
const unresolvedTerms = nlpTerms.filter(
|
|
637
|
+
(term) => !resolvedKeywords.some(
|
|
638
|
+
(kw) => kw.keyword.toLowerCase() === term.toLowerCase() || kw.aliases.some((a) => a.toLowerCase() === term.toLowerCase())
|
|
639
|
+
)
|
|
640
|
+
);
|
|
641
|
+
const storyCandidates = this.storyResolver.scoreAll(intent, resolvedKeywords);
|
|
642
|
+
const storyMatch = storyCandidates.find((c) => c.score > STORY_THRESHOLD);
|
|
643
|
+
const matchedStory = storyMatch ? this.stories.find((s) => (s.story_id || s.id) === storyMatch.storyId) : void 0;
|
|
644
|
+
const debugSteps = [];
|
|
645
|
+
if (!matchedStory && this.fallbackLLM?.enabled) {
|
|
646
|
+
const debugInfo = includeDebug ? {
|
|
647
|
+
rawQuery: userQuery,
|
|
648
|
+
intent,
|
|
649
|
+
resolvedKeywords,
|
|
650
|
+
unresolvedTerms,
|
|
651
|
+
storyCandidates,
|
|
652
|
+
selectedStory: void 0,
|
|
653
|
+
threshold: STORY_THRESHOLD,
|
|
654
|
+
decision: "no-story-fallback-llm",
|
|
655
|
+
steps: [],
|
|
656
|
+
totals: { results: 0, durationMs: Date.now() - startTime }
|
|
657
|
+
} : void 0;
|
|
658
|
+
const ans = await this.queryFallbackLLM(userQuery, intent, startTime);
|
|
659
|
+
if (includeDebug && debugInfo) {
|
|
660
|
+
debugInfo.totals.durationMs = Date.now() - startTime;
|
|
661
|
+
debugInfo.decision = ans.metadata.source === "fallback-llm" && ans.results.length > 0 ? "no-story-fallback-llm" : "fallback-llm-error";
|
|
662
|
+
ans.debug = debugInfo;
|
|
663
|
+
}
|
|
664
|
+
return ans;
|
|
665
|
+
}
|
|
666
|
+
if (!matchedStory) {
|
|
667
|
+
const answer2 = this.responseBuilder.buildAnswer(intent, void 0, [], Date.now() - startTime, "mock-llm");
|
|
668
|
+
if (includeDebug) {
|
|
669
|
+
answer2.debug = {
|
|
670
|
+
rawQuery: userQuery,
|
|
671
|
+
intent,
|
|
672
|
+
resolvedKeywords,
|
|
673
|
+
unresolvedTerms,
|
|
674
|
+
storyCandidates,
|
|
675
|
+
selectedStory: void 0,
|
|
676
|
+
threshold: STORY_THRESHOLD,
|
|
677
|
+
decision: "no-story-no-results",
|
|
678
|
+
steps: [],
|
|
679
|
+
totals: { results: 0, durationMs: Date.now() - startTime }
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
return answer2;
|
|
683
|
+
}
|
|
684
|
+
let results = [];
|
|
685
|
+
let stepIndex = 0;
|
|
686
|
+
for (const step of matchedStory.resolution_steps) {
|
|
687
|
+
if (step.action === "fetch") {
|
|
688
|
+
const queryParams = this.queryBuilder.buildQuery(step, intent);
|
|
689
|
+
const builtFilter = this.queryBuilder.buildSQLWhere(queryParams.filters || {});
|
|
690
|
+
const preview = this.queryBuilder.buildQueryPreview(step, intent, resolvedKeywords.map((kw) => kw.keyword));
|
|
691
|
+
const data = await this.adapter.query(queryParams);
|
|
692
|
+
if (includeDebug) {
|
|
693
|
+
debugSteps.push({
|
|
694
|
+
step: step.step ?? ++stepIndex,
|
|
695
|
+
action: step.action,
|
|
696
|
+
source: queryParams.source,
|
|
697
|
+
queryParams,
|
|
698
|
+
builtFilter: builtFilter || `GET ${queryParams.source}/*`,
|
|
699
|
+
generatedQuery: preview.generatedQuery,
|
|
700
|
+
searchPattern: preview.searchPattern,
|
|
701
|
+
rowsReturned: data.length,
|
|
702
|
+
sampleRows: data.slice(0, 3)
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
results = [...results, ...data];
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
const answer = this.responseBuilder.buildAnswer(intent, matchedStory, results, Date.now() - startTime, "mock-llm");
|
|
709
|
+
if (includeDebug) {
|
|
710
|
+
answer.debug = {
|
|
711
|
+
rawQuery: userQuery,
|
|
712
|
+
intent,
|
|
713
|
+
resolvedKeywords,
|
|
714
|
+
unresolvedTerms,
|
|
715
|
+
storyCandidates,
|
|
716
|
+
selectedStory: { storyId: storyMatch.storyId, score: storyMatch.score },
|
|
717
|
+
threshold: STORY_THRESHOLD,
|
|
718
|
+
decision: "matched-story",
|
|
719
|
+
steps: debugSteps,
|
|
720
|
+
totals: { results: results.length, durationMs: Date.now() - startTime }
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
return answer;
|
|
724
|
+
}
|
|
725
|
+
async queryFallbackLLM(userQuery, intent, startTime) {
|
|
726
|
+
const llmConfig = this.fallbackLLM;
|
|
727
|
+
try {
|
|
728
|
+
const response = await fetch(llmConfig.endpoint, {
|
|
729
|
+
method: "POST",
|
|
730
|
+
headers: {
|
|
731
|
+
"Content-Type": "application/json",
|
|
732
|
+
"Authorization": `Bearer ${llmConfig.apiKey}`
|
|
733
|
+
},
|
|
734
|
+
body: JSON.stringify({
|
|
735
|
+
model: llmConfig.model,
|
|
736
|
+
messages: [{ role: "user", content: userQuery }]
|
|
737
|
+
})
|
|
738
|
+
});
|
|
739
|
+
const data = await response.json();
|
|
740
|
+
const llmAnswer = data.choices?.[0]?.message?.content || "No response from LLM";
|
|
741
|
+
return {
|
|
742
|
+
intent,
|
|
743
|
+
story: void 0,
|
|
744
|
+
results: [{ answer: llmAnswer }],
|
|
745
|
+
summary: llmAnswer,
|
|
746
|
+
metadata: { execution_time_ms: Date.now() - startTime, source: "fallback-llm" }
|
|
747
|
+
};
|
|
748
|
+
} catch (error) {
|
|
749
|
+
return {
|
|
750
|
+
intent,
|
|
751
|
+
story: void 0,
|
|
752
|
+
results: [],
|
|
753
|
+
summary: `Fallback LLM error: ${error.message}`,
|
|
754
|
+
metadata: { execution_time_ms: Date.now() - startTime, source: "fallback-llm" }
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
};
|
|
759
|
+
var ConfigLoader = class {
|
|
760
|
+
constructor(source, connections) {
|
|
761
|
+
this.source = source;
|
|
762
|
+
this.connections = connections;
|
|
763
|
+
}
|
|
764
|
+
source;
|
|
765
|
+
connections;
|
|
766
|
+
async loadKeywords() {
|
|
767
|
+
if (this.source.type === "local") {
|
|
768
|
+
return this.loadLocalKeywords();
|
|
769
|
+
}
|
|
770
|
+
throw new Error(`Unsupported config source: ${this.source.type}`);
|
|
771
|
+
}
|
|
772
|
+
async loadStories() {
|
|
773
|
+
if (this.source.type === "local") {
|
|
774
|
+
return this.loadLocalStories();
|
|
775
|
+
}
|
|
776
|
+
throw new Error(`Unsupported config source: ${this.source.type}`);
|
|
777
|
+
}
|
|
778
|
+
loadLocalKeywords() {
|
|
779
|
+
const keywordsPath = path3.join(this.source.location.path, "keywords");
|
|
780
|
+
const files = fs.readdirSync(keywordsPath).filter((f) => f.endsWith(".json"));
|
|
781
|
+
const results = [];
|
|
782
|
+
for (const file of files) {
|
|
783
|
+
const content = fs.readFileSync(path3.join(keywordsPath, file), "utf8");
|
|
784
|
+
const parsed = JSON.parse(content);
|
|
785
|
+
if (Array.isArray(parsed)) {
|
|
786
|
+
results.push(...parsed);
|
|
787
|
+
} else {
|
|
788
|
+
results.push(parsed);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
return results;
|
|
792
|
+
}
|
|
793
|
+
loadLocalStories() {
|
|
794
|
+
const storiesPath = path3.join(this.source.location.path, "stories");
|
|
795
|
+
const files = fs.readdirSync(storiesPath).filter((f) => f.endsWith(".json"));
|
|
796
|
+
const results = [];
|
|
797
|
+
for (const file of files) {
|
|
798
|
+
const content = fs.readFileSync(path3.join(storiesPath, file), "utf8");
|
|
799
|
+
const parsed = JSON.parse(content);
|
|
800
|
+
if (Array.isArray(parsed)) {
|
|
801
|
+
results.push(...parsed);
|
|
802
|
+
} else {
|
|
803
|
+
results.push(parsed);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
return results;
|
|
807
|
+
}
|
|
808
|
+
};
|
|
809
|
+
|
|
810
|
+
// src/mock-llm.ts
|
|
811
|
+
var MockLLM = class {
|
|
812
|
+
constructor(options) {
|
|
813
|
+
this.options = options;
|
|
814
|
+
this.configLoader = new ConfigLoader(options.configSource, options.connections);
|
|
815
|
+
}
|
|
816
|
+
options;
|
|
817
|
+
configLoader;
|
|
818
|
+
engine;
|
|
819
|
+
async initialize() {
|
|
820
|
+
const [keywords, stories] = await Promise.all([
|
|
821
|
+
this.configLoader.loadKeywords(),
|
|
822
|
+
this.configLoader.loadStories()
|
|
823
|
+
]);
|
|
824
|
+
let adapter;
|
|
825
|
+
if (this.options.connections.mockCosmos) {
|
|
826
|
+
adapter = new MockCosmosAdapter(this.options.connections.mockCosmos);
|
|
827
|
+
} else if (this.options.connections.cosmos) {
|
|
828
|
+
adapter = new CosmosAdapter({
|
|
829
|
+
endpoint: this.options.connections.cosmos.endpoint,
|
|
830
|
+
key: this.options.connections.cosmos.key,
|
|
831
|
+
databaseId: "default"
|
|
832
|
+
});
|
|
833
|
+
} else {
|
|
834
|
+
throw new Error("No data adapter configured");
|
|
835
|
+
}
|
|
836
|
+
const fallbackLLM = this.options.connections.fallbackLLM?.enabled ? this.options.connections.fallbackLLM : void 0;
|
|
837
|
+
this.engine = new MockLLMEngine(keywords, stories, adapter, fallbackLLM);
|
|
838
|
+
}
|
|
839
|
+
// ─── Introspection API ───────────────────────────────────────────────────────
|
|
840
|
+
getKeywords() {
|
|
841
|
+
return this.engine.getKeywords();
|
|
842
|
+
}
|
|
843
|
+
getStories() {
|
|
844
|
+
return this.engine.getStories();
|
|
845
|
+
}
|
|
846
|
+
listDataSources() {
|
|
847
|
+
return this.engine.listDataSources();
|
|
848
|
+
}
|
|
849
|
+
async getDataSourceSnapshot(source, limit = 50) {
|
|
850
|
+
return this.engine.getDataSourceSnapshot(source, limit);
|
|
851
|
+
}
|
|
852
|
+
// ─── Query ───────────────────────────────────────────────────────────────────
|
|
853
|
+
async query(userQuery, opts = { debug: true }) {
|
|
854
|
+
return this.engine.query(userQuery, opts);
|
|
855
|
+
}
|
|
856
|
+
};
|
|
857
|
+
|
|
858
|
+
// src/browser-mock-llm.ts
|
|
859
|
+
var BrowserMockLLM = class {
|
|
860
|
+
constructor(options) {
|
|
861
|
+
this.options = options;
|
|
862
|
+
}
|
|
863
|
+
options;
|
|
864
|
+
engine;
|
|
865
|
+
async initialize() {
|
|
866
|
+
const loader = new FetchConfigLoader({
|
|
867
|
+
keywordsUrl: this.options.keywordsUrl,
|
|
868
|
+
storiesUrl: this.options.storiesUrl
|
|
869
|
+
});
|
|
870
|
+
const [keywords, stories] = await Promise.all([
|
|
871
|
+
loader.loadKeywords(),
|
|
872
|
+
loader.loadStories()
|
|
873
|
+
]);
|
|
874
|
+
const adapter = new HttpMockAdapter({ baseUrl: this.options.dataBaseUrl });
|
|
875
|
+
const fallbackLLM = this.options.fallbackLLM?.enabled ? this.options.fallbackLLM : void 0;
|
|
876
|
+
this.engine = new MockLLMEngine(keywords, stories, adapter, fallbackLLM);
|
|
877
|
+
}
|
|
878
|
+
// ─── Introspection API (mirrors MockLLM) ─────────────────────────────────────
|
|
879
|
+
getKeywords() {
|
|
880
|
+
return this.engine.getKeywords();
|
|
881
|
+
}
|
|
882
|
+
getStories() {
|
|
883
|
+
return this.engine.getStories();
|
|
884
|
+
}
|
|
885
|
+
listDataSources() {
|
|
886
|
+
return this.engine.listDataSources();
|
|
887
|
+
}
|
|
888
|
+
async getDataSourceSnapshot(source, limit = 50) {
|
|
889
|
+
return this.engine.getDataSourceSnapshot(source, limit);
|
|
890
|
+
}
|
|
891
|
+
// ─── Query ───────────────────────────────────────────────────────────────────
|
|
892
|
+
async query(userQuery, opts = { debug: true }) {
|
|
893
|
+
return this.engine.query(userQuery, opts);
|
|
894
|
+
}
|
|
895
|
+
};
|
|
896
|
+
|
|
897
|
+
export { BaseAdapter, BlobAdapter, BrowserMockLLM, CosmosAdapter, FetchConfigLoader, GCSAdapter, HttpMockAdapter, ImageAdapter, KeywordResolver, MockCosmosAdapter, MockLLM, MockLLMEngine, NLPMatcher, QueryBuilder, ResponseBuilder, STORY_THRESHOLD, StoryResolver };
|
|
898
|
+
//# sourceMappingURL=index.mjs.map
|
|
899
|
+
//# sourceMappingURL=index.mjs.map
|