@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.
@@ -0,0 +1,591 @@
1
+ import nlp from 'compromise';
2
+
3
+ // src/adapters/base.adapter.ts
4
+ var BaseAdapter = class {
5
+ buildWhereClause(filters) {
6
+ if (!filters || Object.keys(filters).length === 0) {
7
+ return "";
8
+ }
9
+ const conditions = Object.entries(filters).map(([key, value]) => {
10
+ if (typeof value === "string") {
11
+ return `c.${key} = '${value}'`;
12
+ }
13
+ return `c.${key} = ${value}`;
14
+ });
15
+ return "WHERE " + conditions.join(" AND ");
16
+ }
17
+ };
18
+ var STOP_WORDS = /* @__PURE__ */ new Set([
19
+ "the",
20
+ "a",
21
+ "an",
22
+ "is",
23
+ "are",
24
+ "was",
25
+ "were",
26
+ "be",
27
+ "been",
28
+ "being",
29
+ "have",
30
+ "has",
31
+ "had",
32
+ "do",
33
+ "does",
34
+ "did",
35
+ "will",
36
+ "would",
37
+ "shall",
38
+ "should",
39
+ "may",
40
+ "might",
41
+ "must",
42
+ "can",
43
+ "could",
44
+ "let",
45
+ "make",
46
+ "why",
47
+ "what",
48
+ "how",
49
+ "when",
50
+ "where",
51
+ "who",
52
+ "which",
53
+ "that",
54
+ "this",
55
+ "these",
56
+ "those",
57
+ "it",
58
+ "its",
59
+ "my",
60
+ "your",
61
+ "his",
62
+ "her",
63
+ "our",
64
+ "their",
65
+ "me",
66
+ "him",
67
+ "us",
68
+ "them",
69
+ "and",
70
+ "or",
71
+ "but",
72
+ "if",
73
+ "in",
74
+ "on",
75
+ "at",
76
+ "to",
77
+ "for",
78
+ "of",
79
+ "with",
80
+ "by",
81
+ "from",
82
+ "about",
83
+ "into",
84
+ "through",
85
+ "during",
86
+ "before",
87
+ "after",
88
+ "above",
89
+ "below",
90
+ "between",
91
+ "out",
92
+ "off",
93
+ "over",
94
+ "under",
95
+ "again",
96
+ "further",
97
+ "just",
98
+ "also",
99
+ "not",
100
+ "no",
101
+ "nor",
102
+ "so",
103
+ "yet",
104
+ "both",
105
+ "either"
106
+ ]);
107
+ var NLPMatcher = class {
108
+ parseQuery(query) {
109
+ const cleanQuery = query.replace(/[?!.,;:'"]/g, " ").trim();
110
+ const doc = nlp(cleanQuery);
111
+ const action = this.extractAction(query);
112
+ const keywords = this.extractKeywords(doc, cleanQuery);
113
+ const filters = this.extractFilters(query);
114
+ const confidence = keywords.length > 0 ? 0.8 : 0.3;
115
+ return { action, keywords, filters, confidence };
116
+ }
117
+ extractAction(query) {
118
+ const q = query.toLowerCase();
119
+ if (q.includes("compare") || q.includes("difference")) return "compare";
120
+ if (q.includes("diff") || q.includes("what changed")) return "diff";
121
+ if (q.includes("find") || q.includes("search") || q.includes("which")) return "find";
122
+ if (q.includes("explain") || q.includes("what is") || q.includes("tell me about") || q.startsWith("why")) return "explain";
123
+ return "list";
124
+ }
125
+ extractKeywords(doc, cleanQuery) {
126
+ const nouns = doc.nouns().out("array");
127
+ const adjectives = doc.adjectives().out("array");
128
+ const allTerms = [];
129
+ for (const term of [...nouns, ...adjectives]) {
130
+ const parts = term.toLowerCase().split(/\s+/);
131
+ allTerms.push(...parts);
132
+ }
133
+ const queryWords = cleanQuery.toLowerCase().split(/\s+/);
134
+ allTerms.push(...queryWords);
135
+ return [...new Set(allTerms)].map((w) => w.toLowerCase().replace(/[^a-z0-9-]/g, "")).filter((w) => w.length > 2 && !STOP_WORDS.has(w));
136
+ }
137
+ extractFilters(query) {
138
+ const filters = {};
139
+ const questionWords = /* @__PURE__ */ new Set(["why", "what", "how", "when", "where", "who", "which"]);
140
+ const pattern = /(?:where|with)\s+(\w+)\s+(?:is|=|equals?)\s+['"]?([^'"\s]+)['"]?/gi;
141
+ let match;
142
+ while ((match = pattern.exec(query)) !== null) {
143
+ const [, key, value] = match;
144
+ if (key && value && !questionWords.has(key.toLowerCase())) {
145
+ filters[key] = value;
146
+ }
147
+ }
148
+ return filters;
149
+ }
150
+ };
151
+
152
+ // src/engine/keyword-resolver.ts
153
+ var KeywordResolver = class {
154
+ constructor(keywords) {
155
+ this.keywords = keywords;
156
+ }
157
+ keywords;
158
+ resolve(term) {
159
+ const lowerTerm = term.toLowerCase();
160
+ let match = this.keywords.find((kw) => kw.keyword.toLowerCase() === lowerTerm);
161
+ if (!match) {
162
+ match = this.keywords.find(
163
+ (kw) => kw.aliases.some((alias) => alias.toLowerCase() === lowerTerm)
164
+ );
165
+ }
166
+ if (!match) {
167
+ match = this.keywords.find(
168
+ (kw) => kw.keyword.length > 2 && lowerTerm.includes(kw.keyword.toLowerCase())
169
+ );
170
+ }
171
+ return match;
172
+ }
173
+ /**
174
+ * Resolves a list of NLP-extracted terms AND also checks the full original query
175
+ * against all keyword aliases (to handle multi-word alias phrases).
176
+ */
177
+ resolveAll(terms, fullQuery) {
178
+ const resolved = /* @__PURE__ */ new Map();
179
+ for (const term of terms) {
180
+ const kw = this.resolve(term);
181
+ if (kw) resolved.set(kw.keyword, kw);
182
+ }
183
+ if (fullQuery) {
184
+ const lowerQuery = fullQuery.toLowerCase().replace(/[?!.,;:'"]/g, " ");
185
+ for (const kw of this.keywords) {
186
+ if (!resolved.has(kw.keyword)) {
187
+ const aliasMatch = kw.aliases.some((alias) => {
188
+ const lowerAlias = alias.toLowerCase();
189
+ return lowerAlias.length > 3 && lowerQuery.includes(lowerAlias);
190
+ });
191
+ if (aliasMatch) resolved.set(kw.keyword, kw);
192
+ }
193
+ }
194
+ }
195
+ return Array.from(resolved.values());
196
+ }
197
+ getRelatedKeywords(keyword) {
198
+ return this.keywords.filter(
199
+ (kw) => kw.category === keyword.category && kw.keyword !== keyword.keyword
200
+ );
201
+ }
202
+ };
203
+
204
+ // src/engine/story-resolver.ts
205
+ var STORY_THRESHOLD = 0.1;
206
+ var StoryResolver = class {
207
+ constructor(stories) {
208
+ this.stories = stories;
209
+ }
210
+ stories;
211
+ scoreAll(intent, resolvedKeywords) {
212
+ return this.stories.map((story) => {
213
+ const score = this.scoreStory(story, intent, resolvedKeywords);
214
+ const matchedKeywords = resolvedKeywords.filter((kw) => story.keywords.includes(kw.keyword)).map((kw) => kw.keyword);
215
+ const storyId = story.story_id || story.id || "";
216
+ return { storyId, score, matchedKeywords, storyKeywords: story.keywords };
217
+ }).sort((a, b) => b.score - a.score);
218
+ }
219
+ findBestStory(intent, resolvedKeywords) {
220
+ const matches = this.stories.map((story) => {
221
+ const score = this.scoreStory(story, intent, resolvedKeywords);
222
+ const matchedKeywords = resolvedKeywords.filter((kw) => story.keywords.includes(kw.keyword)).map((kw) => kw.keyword);
223
+ return { story, score, matchedKeywords };
224
+ });
225
+ matches.sort((a, b) => b.score - a.score);
226
+ const best = matches[0];
227
+ return best && best.score > STORY_THRESHOLD ? best : void 0;
228
+ }
229
+ scoreStory(story, intent, resolvedKeywords) {
230
+ let score = 0;
231
+ const keywordNames = resolvedKeywords.map((kw) => kw.keyword);
232
+ const overlap = story.keywords.filter((kw) => keywordNames.includes(kw)).length;
233
+ const keywordScore = overlap / Math.max(story.keywords.length, keywordNames.length, 1);
234
+ score += keywordScore * 0.7;
235
+ if (intent.action === "compare" && story.resolution_steps.some((s) => s.action === "compare")) score += 0.2;
236
+ if (intent.action === "diff" && story.resolution_steps.some((s) => s.action === "diff")) score += 0.2;
237
+ if (overlap === story.keywords.length) score += 0.1;
238
+ return Math.min(score, 1);
239
+ }
240
+ };
241
+
242
+ // src/engine/query-builder.ts
243
+ var QueryBuilder = class {
244
+ buildQuery(step, intent) {
245
+ return {
246
+ source: step.from_source || "",
247
+ filters: { ...intent.filters }
248
+ };
249
+ }
250
+ buildQueryPreview(step, intent, resolvedKeywords = []) {
251
+ const filters = intent.filters || {};
252
+ const where = Object.entries(filters).map(
253
+ ([key, value]) => typeof value === "string" ? `c.${key} = '${value}'` : `c.${key} = ${value}`
254
+ ).join(" AND ");
255
+ const generatedQuery = where ? `SELECT * FROM c WHERE ${where}` : `SELECT * FROM c /* ${step.from_source || "source"} */`;
256
+ const terms = [.../* @__PURE__ */ new Set([...step.keyword ? [step.keyword] : [], ...resolvedKeywords])];
257
+ const searchPattern = terms.length > 0 ? `MATCH ${terms.map((term) => `"${term}"`).join(" OR ")}` : `MATCH ${step.from_source || "source"}*`;
258
+ return { generatedQuery, searchPattern };
259
+ }
260
+ buildSQLWhere(filters) {
261
+ if (!filters || Object.keys(filters).length === 0) return "";
262
+ const conditions = Object.entries(filters).map(
263
+ ([key, value]) => typeof value === "string" ? `c.${key} = '${value}'` : `c.${key} = ${value}`
264
+ );
265
+ return "WHERE " + conditions.join(" AND ");
266
+ }
267
+ };
268
+
269
+ // src/engine/response-builder.ts
270
+ var ResponseBuilder = class {
271
+ buildAnswer(intent, story, results, executionTime, source) {
272
+ return {
273
+ intent,
274
+ story,
275
+ results,
276
+ summary: this.buildSummary(intent, results),
277
+ metadata: {
278
+ execution_time_ms: executionTime,
279
+ source
280
+ }
281
+ };
282
+ }
283
+ buildSummary(intent, results) {
284
+ if (results.length === 0) return "I couldn't find any results for your question.";
285
+ if (results.length === 1 && results[0]?.answer) {
286
+ return results[0].answer;
287
+ }
288
+ if (intent.action === "explain") {
289
+ const withAnswer = results.find((r) => r.answer);
290
+ if (withAnswer) return withAnswer.answer;
291
+ return Object.entries(results[0]).filter(([key]) => !key.startsWith("_") && key !== "id").map(([key, value]) => `${key}: ${value}`).join(", ");
292
+ }
293
+ if (intent.action === "compare") return `Comparing ${results.length} items.`;
294
+ const answers = results.filter((r) => r.answer).map((r) => r.answer);
295
+ if (answers.length > 0) return answers.join(" | ");
296
+ return `Found ${results.length} result(s).`;
297
+ }
298
+ };
299
+
300
+ // src/config/fetch-loader.ts
301
+ var FetchConfigLoader = class {
302
+ constructor(options) {
303
+ this.options = options;
304
+ }
305
+ options;
306
+ async loadKeywords() {
307
+ const response = await fetch(this.options.keywordsUrl);
308
+ if (!response.ok) {
309
+ throw new Error(
310
+ `FetchConfigLoader: Failed to fetch keywords from ${this.options.keywordsUrl} (${response.status})`
311
+ );
312
+ }
313
+ const data = await response.json();
314
+ return Array.isArray(data) ? data : [data];
315
+ }
316
+ async loadStories() {
317
+ const response = await fetch(this.options.storiesUrl);
318
+ if (!response.ok) {
319
+ throw new Error(
320
+ `FetchConfigLoader: Failed to fetch stories from ${this.options.storiesUrl} (${response.status})`
321
+ );
322
+ }
323
+ const data = await response.json();
324
+ return Array.isArray(data) ? data : [data];
325
+ }
326
+ };
327
+
328
+ // src/adapters/http-mock.adapter.ts
329
+ var HttpMockAdapter = class extends BaseAdapter {
330
+ constructor(options) {
331
+ super();
332
+ this.options = options;
333
+ }
334
+ options;
335
+ async query(params) {
336
+ if (params.source.includes("..") || params.source.startsWith("/") || params.source.includes("://")) {
337
+ throw new Error(`HttpMockAdapter: Invalid source path '${params.source}'`);
338
+ }
339
+ const base = this.options.baseUrl.replace(/\/$/, "");
340
+ const url = `${base}/${params.source}.json`;
341
+ const response = await fetch(url);
342
+ if (!response.ok) {
343
+ if (response.status === 404) return [];
344
+ throw new Error(`HttpMockAdapter: Failed to fetch ${url} (${response.status})`);
345
+ }
346
+ const data = await response.json();
347
+ let items = Array.isArray(data) ? data : [data];
348
+ if (params.filters && Object.keys(params.filters).length > 0) {
349
+ items = items.filter((item) => this.matchesFilters(item, params.filters));
350
+ }
351
+ if (params.limit) {
352
+ items = items.slice(0, params.limit);
353
+ }
354
+ return items;
355
+ }
356
+ async getById(_id) {
357
+ throw new Error("HttpMockAdapter.getById is not supported \u2014 use query() with a filters object");
358
+ }
359
+ matchesFilters(item, filters) {
360
+ return Object.entries(filters).every(([key, value]) => {
361
+ const itemValue = this.getNestedValue(item, key);
362
+ return itemValue === value;
363
+ });
364
+ }
365
+ getNestedValue(obj, path) {
366
+ return path.split(".").reduce((current, key) => current?.[key], obj);
367
+ }
368
+ };
369
+
370
+ // src/mock-llm-engine.ts
371
+ var MockLLMEngine = class {
372
+ constructor(keywords, stories, adapter, fallbackLLM) {
373
+ this.keywords = keywords;
374
+ this.stories = stories;
375
+ this.adapter = adapter;
376
+ this.fallbackLLM = fallbackLLM;
377
+ this.nlpMatcher = new NLPMatcher();
378
+ this.queryBuilder = new QueryBuilder();
379
+ this.responseBuilder = new ResponseBuilder();
380
+ this.keywordResolver = new KeywordResolver(keywords);
381
+ this.storyResolver = new StoryResolver(stories);
382
+ }
383
+ keywords;
384
+ stories;
385
+ adapter;
386
+ fallbackLLM;
387
+ nlpMatcher;
388
+ keywordResolver;
389
+ storyResolver;
390
+ queryBuilder;
391
+ responseBuilder;
392
+ // ─── Introspection API ───────────────────────────────────────────────────────
393
+ getKeywords() {
394
+ return this.keywords;
395
+ }
396
+ getStories() {
397
+ return this.stories;
398
+ }
399
+ listDataSources() {
400
+ const sources = /* @__PURE__ */ new Set();
401
+ for (const kw of this.keywords) {
402
+ if (kw.data_source) sources.add(kw.data_source);
403
+ }
404
+ for (const story of this.stories) {
405
+ if (story.contract?.sources) {
406
+ for (const source of story.contract.sources) {
407
+ if (source) sources.add(source);
408
+ }
409
+ }
410
+ for (const step of story.resolution_steps) {
411
+ if (step.from_source) sources.add(step.from_source);
412
+ }
413
+ }
414
+ return Array.from(sources);
415
+ }
416
+ async getDataSourceSnapshot(source, limit = 50) {
417
+ return this.adapter.query({ source, limit });
418
+ }
419
+ // ─── Query ───────────────────────────────────────────────────────────────────
420
+ async query(userQuery, opts = { debug: true }) {
421
+ const includeDebug = opts.debug !== false;
422
+ const startTime = Date.now();
423
+ const intent = this.nlpMatcher.parseQuery(userQuery);
424
+ const nlpTerms = intent.keywords;
425
+ const resolvedKeywords = this.keywordResolver.resolveAll(nlpTerms, userQuery);
426
+ const unresolvedTerms = nlpTerms.filter(
427
+ (term) => !resolvedKeywords.some(
428
+ (kw) => kw.keyword.toLowerCase() === term.toLowerCase() || kw.aliases.some((a) => a.toLowerCase() === term.toLowerCase())
429
+ )
430
+ );
431
+ const storyCandidates = this.storyResolver.scoreAll(intent, resolvedKeywords);
432
+ const storyMatch = storyCandidates.find((c) => c.score > STORY_THRESHOLD);
433
+ const matchedStory = storyMatch ? this.stories.find((s) => (s.story_id || s.id) === storyMatch.storyId) : void 0;
434
+ const debugSteps = [];
435
+ if (!matchedStory && this.fallbackLLM?.enabled) {
436
+ const debugInfo = includeDebug ? {
437
+ rawQuery: userQuery,
438
+ intent,
439
+ resolvedKeywords,
440
+ unresolvedTerms,
441
+ storyCandidates,
442
+ selectedStory: void 0,
443
+ threshold: STORY_THRESHOLD,
444
+ decision: "no-story-fallback-llm",
445
+ steps: [],
446
+ totals: { results: 0, durationMs: Date.now() - startTime }
447
+ } : void 0;
448
+ const ans = await this.queryFallbackLLM(userQuery, intent, startTime);
449
+ if (includeDebug && debugInfo) {
450
+ debugInfo.totals.durationMs = Date.now() - startTime;
451
+ debugInfo.decision = ans.metadata.source === "fallback-llm" && ans.results.length > 0 ? "no-story-fallback-llm" : "fallback-llm-error";
452
+ ans.debug = debugInfo;
453
+ }
454
+ return ans;
455
+ }
456
+ if (!matchedStory) {
457
+ const answer2 = this.responseBuilder.buildAnswer(intent, void 0, [], Date.now() - startTime, "mock-llm");
458
+ if (includeDebug) {
459
+ answer2.debug = {
460
+ rawQuery: userQuery,
461
+ intent,
462
+ resolvedKeywords,
463
+ unresolvedTerms,
464
+ storyCandidates,
465
+ selectedStory: void 0,
466
+ threshold: STORY_THRESHOLD,
467
+ decision: "no-story-no-results",
468
+ steps: [],
469
+ totals: { results: 0, durationMs: Date.now() - startTime }
470
+ };
471
+ }
472
+ return answer2;
473
+ }
474
+ let results = [];
475
+ let stepIndex = 0;
476
+ for (const step of matchedStory.resolution_steps) {
477
+ if (step.action === "fetch") {
478
+ const queryParams = this.queryBuilder.buildQuery(step, intent);
479
+ const builtFilter = this.queryBuilder.buildSQLWhere(queryParams.filters || {});
480
+ const preview = this.queryBuilder.buildQueryPreview(step, intent, resolvedKeywords.map((kw) => kw.keyword));
481
+ const data = await this.adapter.query(queryParams);
482
+ if (includeDebug) {
483
+ debugSteps.push({
484
+ step: step.step ?? ++stepIndex,
485
+ action: step.action,
486
+ source: queryParams.source,
487
+ queryParams,
488
+ builtFilter: builtFilter || `GET ${queryParams.source}/*`,
489
+ generatedQuery: preview.generatedQuery,
490
+ searchPattern: preview.searchPattern,
491
+ rowsReturned: data.length,
492
+ sampleRows: data.slice(0, 3)
493
+ });
494
+ }
495
+ results = [...results, ...data];
496
+ }
497
+ }
498
+ const answer = this.responseBuilder.buildAnswer(intent, matchedStory, results, Date.now() - startTime, "mock-llm");
499
+ if (includeDebug) {
500
+ answer.debug = {
501
+ rawQuery: userQuery,
502
+ intent,
503
+ resolvedKeywords,
504
+ unresolvedTerms,
505
+ storyCandidates,
506
+ selectedStory: { storyId: storyMatch.storyId, score: storyMatch.score },
507
+ threshold: STORY_THRESHOLD,
508
+ decision: "matched-story",
509
+ steps: debugSteps,
510
+ totals: { results: results.length, durationMs: Date.now() - startTime }
511
+ };
512
+ }
513
+ return answer;
514
+ }
515
+ async queryFallbackLLM(userQuery, intent, startTime) {
516
+ const llmConfig = this.fallbackLLM;
517
+ try {
518
+ const response = await fetch(llmConfig.endpoint, {
519
+ method: "POST",
520
+ headers: {
521
+ "Content-Type": "application/json",
522
+ "Authorization": `Bearer ${llmConfig.apiKey}`
523
+ },
524
+ body: JSON.stringify({
525
+ model: llmConfig.model,
526
+ messages: [{ role: "user", content: userQuery }]
527
+ })
528
+ });
529
+ const data = await response.json();
530
+ const llmAnswer = data.choices?.[0]?.message?.content || "No response from LLM";
531
+ return {
532
+ intent,
533
+ story: void 0,
534
+ results: [{ answer: llmAnswer }],
535
+ summary: llmAnswer,
536
+ metadata: { execution_time_ms: Date.now() - startTime, source: "fallback-llm" }
537
+ };
538
+ } catch (error) {
539
+ return {
540
+ intent,
541
+ story: void 0,
542
+ results: [],
543
+ summary: `Fallback LLM error: ${error.message}`,
544
+ metadata: { execution_time_ms: Date.now() - startTime, source: "fallback-llm" }
545
+ };
546
+ }
547
+ }
548
+ };
549
+
550
+ // src/browser-mock-llm.ts
551
+ var BrowserMockLLM = class {
552
+ constructor(options) {
553
+ this.options = options;
554
+ }
555
+ options;
556
+ engine;
557
+ async initialize() {
558
+ const loader = new FetchConfigLoader({
559
+ keywordsUrl: this.options.keywordsUrl,
560
+ storiesUrl: this.options.storiesUrl
561
+ });
562
+ const [keywords, stories] = await Promise.all([
563
+ loader.loadKeywords(),
564
+ loader.loadStories()
565
+ ]);
566
+ const adapter = new HttpMockAdapter({ baseUrl: this.options.dataBaseUrl });
567
+ const fallbackLLM = this.options.fallbackLLM?.enabled ? this.options.fallbackLLM : void 0;
568
+ this.engine = new MockLLMEngine(keywords, stories, adapter, fallbackLLM);
569
+ }
570
+ // ─── Introspection API (mirrors MockLLM) ─────────────────────────────────────
571
+ getKeywords() {
572
+ return this.engine.getKeywords();
573
+ }
574
+ getStories() {
575
+ return this.engine.getStories();
576
+ }
577
+ listDataSources() {
578
+ return this.engine.listDataSources();
579
+ }
580
+ async getDataSourceSnapshot(source, limit = 50) {
581
+ return this.engine.getDataSourceSnapshot(source, limit);
582
+ }
583
+ // ─── Query ───────────────────────────────────────────────────────────────────
584
+ async query(userQuery, opts = { debug: true }) {
585
+ return this.engine.query(userQuery, opts);
586
+ }
587
+ };
588
+
589
+ export { BaseAdapter, BrowserMockLLM, FetchConfigLoader, HttpMockAdapter, KeywordResolver, MockLLMEngine, NLPMatcher, QueryBuilder, ResponseBuilder, STORY_THRESHOLD, StoryResolver };
590
+ //# sourceMappingURL=browser.mjs.map
591
+ //# sourceMappingURL=browser.mjs.map